#!/usr/bin/env bash # OnionHEN one-shot build pipeline # # Phases: # 1) configure (prospero-cmake / PS5 payload SDK) # 2) build libs + shellui # 3) stage the kstuff dependency # 4) build daemon + util # 5) build bootstrapper (-> bin/bootstrapper.elf + .lzma) # 6) build unpacker / OnionHEN.elf (embeds bootstrapper.elf.lzma) # # Usage: # export PS5_PAYLOAD_SDK=/path/to/ps5-payload-sdk # ./scripts/build.sh # cleans previous outputs first # ./scripts/build.sh --clean # same as default; kept for compatibility # ./scripts/build.sh --release # non-interactive Release build # ./scripts/build.sh --jobs 8 # ./scripts/build.sh --stub-missing # compile-only placeholders for missing external ELFs # set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SOURCE="${ROOT}/source" # CMake binary dir + final ELFs/libs: /build/{bin,lib}/ BUILD="${BUILD_DIR:-${ROOT}/build}" CACHE="${ONIONHEN_CACHE_DIR:-${ROOT}/.cache/dependencies}" BIN="${BUILD}/bin" PS5_PAYLOAD_SDK="${PS5_PAYLOAD_SDK:-${PS5SDK:-}}" DEFAULT_BUILD_TYPE="Debug" BUILD_TYPE="${BUILD_TYPE:-}" BUILD_TYPE_EXPLICIT=0 [[ -n "${BUILD_TYPE}" ]] && BUILD_TYPE_EXPLICIT=1 JOBS="${JOBS:-}" CONFIGURE_ONLY=0 STUB_MISSING=0 SKIP_UNPACKER=0 SKIP_DEPENDENCY_SYNC=0 FORCE_DEPENDENCY_SYNC=0 INIT_SUBMODULES=0 # Auto job count if [[ -z "${JOBS}" ]]; then if command -v nproc >/dev/null 2>&1; then JOBS="$(nproc)" elif command -v sysctl >/dev/null 2>&1; then JOBS="$(sysctl -n hw.ncpu 2>/dev/null || echo 4)" else JOBS=4 fi fi log() { printf '\n\033[1;34m==>\033[0m %s\n' "$*"; } ok() { printf '\033[1;32m[ok]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; } die() { printf '\033[1;31m[error]\033[0m %s\n' "$*" >&2; exit 1; } usage() { cat < Parallel build jobs (default: ${JOBS}) --build-type Debug|Release (default: ${DEFAULT_BUILD_TYPE}) --debug Same as --build-type Debug --release Same as --build-type Release --build-dir CMake binary dir (default: /build) --cache-dir Download cache directory (default: /.cache/dependencies) --stub-missing Create tiny placeholder ELFs if external blobs are missing (links, but NOT for real hardware) --skip-unpacker Stop after bootstrapper (no OnionHEN.elf unpacker) --skip-dependency-sync Do not call scripts/sync_dependencies.sh --force-dependency-sync Re-download kstuff even if already cached --init-submodules git submodule update --init before sync -h, --help This help Environment: PS5_PAYLOAD_SDK Path to ps5-payload-sdk (required) ONIONHEN_CACHE_DIR Override dependency cache directory BUILD_DIR Override build directory BUILD_TYPE Debug|Release; skips the interactive build-type prompt Third-party (pinned source under third_party/ + release fallbacks): See third_party/README.md and scripts/sync_dependencies.sh kstuff.elf <- EchoStretch/kstuff-lite External elfldr @ 9021 is required for initial bootstrap but is not vendored. OnionHEN embeds its private runtime loader as onion_elfldr.elf @ 9020. Removed: external elfldr.elf (9021), ps5debug, app-dumper, Byepervisor/hen, Discord RPC Built-in outputs (under /build/): build/bin/*.elf final ELFs (util, daemon, bootstrapper, OnionHEN, …) build/lib/*.a first-party static libs build/bin/shellui.elf daemon embed input EOF } while [[ $# -gt 0 ]]; do case "$1" in --clean) shift ;; --configure-only) CONFIGURE_ONLY=1; shift ;; --jobs) JOBS="$2"; shift 2 ;; --build-type) BUILD_TYPE="$2"; BUILD_TYPE_EXPLICIT=1; shift 2 ;; --debug) BUILD_TYPE="Debug"; BUILD_TYPE_EXPLICIT=1; shift ;; --release) BUILD_TYPE="Release"; BUILD_TYPE_EXPLICIT=1; shift ;; --build-dir) BUILD="$2"; shift 2 ;; --cache-dir) CACHE="$2"; shift 2 ;; --stub-missing) STUB_MISSING=1; shift ;; --skip-unpacker) SKIP_UNPACKER=1; shift ;; --skip-dependency-sync) SKIP_DEPENDENCY_SYNC=1; shift ;; --force-dependency-sync) FORCE_DEPENDENCY_SYNC=1; shift ;; --init-submodules) INIT_SUBMODULES=1; shift ;; -h|--help) usage; exit 0 ;; *) die "Unknown option: $1 (see --help)" ;; esac done normalize_build_type() { case "$1" in [Dd][Ee][Bb][Uu][Gg]|[Dd]) printf 'Debug' ;; [Rr][Ee][Ll][Ee][Aa][Ss][Ee]|[Rr]) printf 'Release' ;; *) return 1 ;; esac } select_build_type() { local selected="${BUILD_TYPE:-}" if [[ "${BUILD_TYPE_EXPLICIT}" -eq 1 ]]; then BUILD_TYPE="$(normalize_build_type "${selected}")" || die "invalid build type: ${selected} (expected Debug or Release)" return 0 fi if [[ -t 0 && -t 1 ]]; then printf '\nSelect build type:\n' printf ' 1) Debug (default)\n' printf ' 2) Release\n' printf 'Build type [1]: ' read -r selected || selected="" case "${selected}" in ""|1|[Dd]|[Dd][Ee][Bb][Uu][Gg]) BUILD_TYPE="Debug" ;; 2|[Rr]|[Rr][Ee][Ll][Ee][Aa][Ss][Ee]) BUILD_TYPE="Release" ;; *) warn "unknown build type selection '${selected}', using ${DEFAULT_BUILD_TYPE}" BUILD_TYPE="${DEFAULT_BUILD_TYPE}" ;; esac else BUILD_TYPE="${DEFAULT_BUILD_TYPE}" fi } select_build_type # --------------------------------------------------------------------------- # Preconditions # --------------------------------------------------------------------------- [[ -n "${PS5_PAYLOAD_SDK}" ]] || die "PS5_PAYLOAD_SDK is not set" [[ -d "${PS5_PAYLOAD_SDK}" ]] || die "PS5_PAYLOAD_SDK not a directory: ${PS5_PAYLOAD_SDK}" [[ -x "${PS5_PAYLOAD_SDK}/bin/prospero-cmake" ]] || die "prospero-cmake not found under SDK" [[ -d "${SOURCE}" ]] || die "source/ missing at ${SOURCE}" export PS5_PAYLOAD_SDK export PATH="${PS5_PAYLOAD_SDK}/bin:${PATH}" export LLVM_CONFIG="${LLVM_CONFIG:-/opt/homebrew/opt/llvm@18/bin/llvm-config}" need_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing command: $1"; } need_cmd cmake need_cmd ninja need_cmd python3 # lzma or xz required later for bootstrapper packing if ! command -v lzma >/dev/null 2>&1 && ! command -v xz >/dev/null 2>&1; then die "need lzma or xz (e.g. brew install xz)" fi # # prospero-clang++ needs target libc++ / libc++abi / libunwind in the SDK. # Incomplete binary installs often only have C stubs; CMake then dies with # ld.lld: error: unable to find library -lc++ # Official fix: build SDK from source + ./libcxx.sh, or re-install a full # release zip that includes target/lib/libc++.a (v0.41+). # ensure_sdk_libcxx() { local libdir="${PS5_PAYLOAD_SDK}/target/lib" local need=0 for f in libc++.a libc++abi.a libunwind.a; do [[ -f "${libdir}/${f}" ]] || need=1 done [[ -d "${PS5_PAYLOAD_SDK}/target/include/c++/v1" ]] || need=1 if [[ "${need}" -eq 0 ]]; then ok "SDK C++ runtime present (libc++ / libc++abi / libunwind)" return 0 fi warn "SDK missing C++ runtime under ${libdir}" warn "Attempting to fetch from ps5-payload-dev/sdk latest release zip…" need_cmd curl need_cmd unzip local tmp tmp="$(mktemp -d "${TMPDIR:-/tmp}/onion-sdk-cxx.XXXXXX")" # shellcheck disable=SC2064 trap "rm -rf '${tmp}'" RETURN local zip_url="https://github.com/ps5-payload-dev/sdk/releases/latest/download/ps5-payload-sdk.zip" if ! curl -fsSL --retry 3 -o "${tmp}/sdk.zip" "${zip_url}"; then die "download failed: ${zip_url} Install a full SDK, or from SDK source tree run: sudo make DESTDIR=${PS5_PAYLOAD_SDK} install sudo -E ./libcxx.sh" fi unzip -q -o "${tmp}/sdk.zip" -d "${tmp}/out" local root root="$(find "${tmp}/out" -maxdepth 2 -type d -name 'ps5-payload-sdk' | head -1)" [[ -n "${root}" ]] || root="${tmp}/out/ps5-payload-sdk" [[ -d "${root}/target/lib" ]] || die "unexpected SDK zip layout" mkdir -p "${libdir}" "${PS5_PAYLOAD_SDK}/target/include" for f in libc++.a libc++abi.a libunwind.a libc++experimental.a; do if [[ -f "${root}/target/lib/${f}" ]]; then cp -f "${root}/target/lib/${f}" "${libdir}/${f}" fi done if [[ -d "${root}/target/include/c++" ]]; then rm -rf "${PS5_PAYLOAD_SDK}/target/include/c++" cp -a "${root}/target/include/c++" "${PS5_PAYLOAD_SDK}/target/include/" fi for f in libc++.a libc++abi.a libunwind.a; do [[ -f "${libdir}/${f}" ]] || die "still missing ${libdir}/${f} after merge" done ok "merged C++ runtime into ${PS5_PAYLOAD_SDK}" } CMAKE=("${PS5_PAYLOAD_SDK}/bin/prospero-cmake") # --------------------------------------------------------------------------- # Dependency source builds and fallback staging # --------------------------------------------------------------------------- stage_dependencies() { if [[ "${SKIP_DEPENDENCY_SYNC}" -eq 1 ]]; then warn "skipping dependency sync (--skip-dependency-sync)" return 0 fi log "Syncing third-party embeds (submodules / GitHub releases)" local args=() [[ "${STUB_MISSING}" -eq 1 ]] && args+=(--stub-missing) [[ "${FORCE_DEPENDENCY_SYNC}" -eq 1 ]] && args+=(--force) [[ "${INIT_SUBMODULES}" -eq 1 ]] && args+=(--init-submodules) if [[ ! -x "${ROOT}/scripts/sync_dependencies.sh" ]]; then die "missing ${ROOT}/scripts/sync_dependencies.sh" fi ONIONHEN_CACHE_DIR="${CACHE}" \ "${ROOT}/scripts/sync_dependencies.sh" "${args[@]+"${args[@]}"}" } # --------------------------------------------------------------------------- # Configure # --------------------------------------------------------------------------- clean_build_artifacts() { log "Cleaning previous build outputs" case "${BUILD}" in ""|"/"|"/Users"|"/Users/${USER:-}"|"${ROOT}"|"${SOURCE}") die "refusing to clean unsafe build dir: ${BUILD}" ;; esac rm -rf "${BUILD}" # Legacy in-tree bin (real dir or symlink). rm -rf "${SOURCE}/bin" # Legacy in-tree cmake tree rm -rf "${SOURCE}/build" ok "old build outputs removed" } configure() { log "Configure (${BUILD_TYPE})" mkdir -p "${BUILD}" "${CMAKE[@]}" \ -S "${SOURCE}" \ -B "${BUILD}" \ -G Ninja \ -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" \ -DONIONHEN_KSTUFF_ELF="${CACHE}/kstuff.elf" \ -DPS5_PAYLOAD_SDK="${PS5_PAYLOAD_SDK}" ok "configured -> ${BUILD}" } build_targets() { local targets=("$@") log "Build: ${targets[*]} (-j${JOBS})" cmake --build "${BUILD}" -j"${JOBS}" --target "${targets[@]}" } # --------------------------------------------------------------------------- # Main pipeline # --------------------------------------------------------------------------- main() { log "OnionHEN build" echo " ROOT = ${ROOT}" echo " SDK = ${PS5_PAYLOAD_SDK}" echo " BUILD = ${BUILD}" echo " CACHE = ${CACHE}" echo " TYPE = ${BUILD_TYPE}" echo " JOBS = ${JOBS}" clean_build_artifacts ensure_sdk_libcxx stage_dependencies configure if [[ "${CONFIGURE_ONLY}" -eq 1 ]]; then ok "configure-only done" exit 0 fi # Phase 1 — libraries + injectables (all outputs stay under build/bin) log "Phase 1/5: libraries + shellui" build_targets \ NidResolver \ hijacker \ NineS \ shellui # shellui.elf is a daemon embed input. if [[ -f "${BIN}/shellui.elf" ]]; then ok "built shellui.elf" else die "expected ${BIN}/shellui.elf after phase 1" fi # Phase 2 — daemons log "Phase 2/5: util + daemon" build_targets util daemon for f in daemon.elf util.elf; do [[ -f "${BIN}/${f}" ]] || die "missing ${BIN}/${f} after phase 2" ok "built ${f}" done # Phase 3 — bootstrapper (embeds daemon/util + kstuff + assets) log "Phase 3/5: bootstrapper" build_targets bootstrapper bootstrapper_packed # CMake declares bootstrapper.elf.lzma as an output and compresses it from # bootstrapper.elf. If the output is unavailable, retain the manual fallback # for older build trees. # and writes bootstrapper.elf.lzma.size. If lzma replaced the elf, restore # naming expected by unpacker. if [[ -f "${BIN}/bootstrapper.elf.lzma" ]]; then ok "bootstrapper.elf.lzma ready" elif [[ -f "${BIN}/bootstrapper.elf" ]]; then warn "lzma not produced by CMake output rule; packing manually" local elf="${BIN}/bootstrapper.elf" if stat -f%z "${elf}" >/dev/null 2>&1; then stat -f%z "${elf}" > "${elf}.lzma.size" else stat -c%s "${elf}" > "${elf}.lzma.size" fi if command -v lzma >/dev/null 2>&1; then # lzma -9 replaces file with .lzma suffix on some implementations cp -f "${elf}" "${elf}.bak" lzma -f -9 -k "${elf}" 2>/dev/null || lzma -f -9 "${elf}" if [[ -f "${elf}.lzma" ]]; then : elif [[ -f "${elf}" ]] && file "${elf}" | grep -qi lzma; then mv "${elf}" "${elf}.lzma" mv "${elf}.bak" "${elf}" fi [[ -f "${elf}.bak" ]] && mv -f "${elf}.bak" "${elf}" 2>/dev/null || true else xz -F lzma -f -9 -k -c "${elf}" > "${elf}.lzma" fi [[ -f "${BIN}/bootstrapper.elf.lzma" ]] || die "failed to produce bootstrapper.elf.lzma" ok "manual lzma pack done" else die "bootstrapper.elf missing in ${BIN}" fi if [[ ! -f "${BIN}/bootstrapper.elf.lzma.size" ]]; then # size file must be original decompressed size (ascii or raw — upstream uses stat text) if [[ -f "${BIN}/bootstrapper.elf" ]]; then if stat -f%z "${BIN}/bootstrapper.elf" >/dev/null 2>&1; then stat -f%z "${BIN}/bootstrapper.elf" > "${BIN}/bootstrapper.elf.lzma.size" else stat -c%s "${BIN}/bootstrapper.elf" > "${BIN}/bootstrapper.elf.lzma.size" fi fi fi if [[ ! -f "${BIN}/bootstrapper.elf.sha1" && -f "${BIN}/bootstrapper.elf" ]]; then local hash="" if command -v shasum >/dev/null 2>&1; then hash="$(shasum -a 1 "${BIN}/bootstrapper.elf" | awk '{print $1}')" elif command -v sha1sum >/dev/null 2>&1; then hash="$(sha1sum "${BIN}/bootstrapper.elf" | awk '{print $1}')" elif command -v openssl >/dev/null 2>&1; then hash="$(openssl dgst -sha1 "${BIN}/bootstrapper.elf" | awk '{print $NF}')" fi hash="$(printf '%s' "${hash}" | tr 'A-F' 'a-f')" if [[ "${hash}" =~ ^[0-9a-f]{40}$ ]]; then printf '%s\n' "${hash}" > "${BIN}/bootstrapper.elf.sha1" else die "failed to produce bootstrapper.elf.sha1" fi fi if [[ "${SKIP_UNPACKER}" -eq 1 ]]; then log "Skip unpacker (--skip-unpacker)" else # Phase 5 — final payload (OnionHEN.elf embeds lzma bootstrapper) log "Phase 5/5: unpacker (OnionHEN.elf)" # Target project name is OnionHEN (see unpacker/CMakeLists.txt) if cmake --build "${BUILD}" -j"${JOBS}" --target OnionHEN 2>/dev/null; then ok "OnionHEN target built" else build_targets unpacker 2>/dev/null || build_targets OnionHEN fi if [[ -f "${BIN}/OnionHEN.elf" ]]; then ok "final payload: ${BIN}/OnionHEN.elf" else warn "OnionHEN.elf not found under bin/ — check unpacker target name/output" ls -la "${BIN}" || true fi fi log "Build finished" echo echo "Artifacts in ${BIN}:" ls -lah "${BIN}" 2>/dev/null || true echo ok "done" } main