Building from Source
Launchstation is built with the PS2BUILD SDK and its LLVM/Clang toolchain. This page reproduces what .github/workflows/compile.yml does, step by step, so you can reproduce the build procedure locally. CI also embeds its configured Google OAuth client; local builds need not do so.
# from the repository root: install or refresh the CI-selected SDK
export PS2BUILD_ROOT="$HOME/.local/share/ps2build"
sh scripts/ps2build_sdk.sh --llvm "$PS2BUILD_ROOT"
export PATH="$PS2BUILD_ROOT:$PATH"
export PS2BUILD_TOOLCHAIN=llvm
# every checkout (and again after changing toolchain)
bash scripts/build_cloud_tls.sh
bash scripts/build_jpeg.sh
python3 scripts/gen_cloud_client.py
python3 scripts/gen_githash.py --require-git
ps2build buildThe release ELF is build/bin/wLaunchELF-packed.elf. ps2build is the only build path; the legacy Makefile build was retired on 2026-10-26.
Requirements
- Host: Linux (what CI uses). The helper scripts also contain MSYS/Git-Bash path handling for Windows, but only Linux is exercised by CI.
- Tools on
PATH:bash,curl,tarwith bzip2, gzip and Zstandard (zstd, used by the SDK mirror fallback),sha256sum,python33.11 or newer (the build scripts use only the standard library;release_notes.pyneedshashlib.file_digest),git, CMake and Make (the dependency scripts call plaincmake; its default Linux generator uses Unix Makefiles). A hostgccorclangand NumPy (python3 -m pip install --user numpy, as CI does;tests/test_sfx_ps2.pyimports it) are needed to run the test suite. - Network: the first run downloads the SDK, the LLVM toolchain, Mbed TLS and libjpeg-turbo. The two dependency tarballs are SHA-256 pinned in their scripts and cached under
.cache/. - Optional: Pillow, only for
scripts/gen_dash_font.pyandscripts/prepare_psbbn_theme.py.
1. Install the PS2BUILD SDK
Run the repository's SDK installer from the checkout root. It installs the exact versions pinned in .github/ps2build-sdk.env: it reuses an exact installed version and updates only components that differ, and it does not ask the release server which version is newest. Once a week .github/workflows/ps2build-sdk-watch.yml asks, and opens a draft pull request when new releases need new pins. This is the installer used by CI:
export PS2BUILD_ROOT="$HOME/.local/share/ps2build"
sh scripts/ps2build_sdk.sh --llvm "$PS2BUILD_ROOT"
export PATH="$PS2BUILD_ROOT:$PATH"
export PS2BUILD_TOOLCHAIN=llvm
test -f "$PS2BUILD_ROOT/llvm-toolchain/ee/bin/mips64r5900el-ps2-elf-clang.cfg"
ps2build version
The ps2build executable lives directly in the SDK directory, not in a bin/ subfolder. Keep PS2BUILD_ROOT set if you choose a different destination: the dependency scripts use it to find the compiler. Run the installer again after SDK pins change or when refreshing a development environment.
If git.techwritescode.dev is unavailable, the installer falls back to the NathanNeurotic/ps2build-mirror release pinned as PS2BUILD_SDK_TAG and verifies its files against that release's SHA-256 manifest, whose own hash is pinned as PS2BUILD_SDK_SUMS_SHA256. With no release pinned, it takes the newest mirror release and the manifest hash listed in that release's pin.env, and warns if that release does not hold the pinned SDK. A private mirror requires a token with read access supplied as PS2BUILD_SDK_MIRROR_TOKEN (CI binds its PS2BUILD_MIRROR_TOKEN secret to that name). Without mirror access, restore the official host or obtain authorized mirror access; do not bypass download verification.
2. Toolchain selection
ps2.yaml contains toolchain: llvm, so the EE and IOP code is compiled with Clang and linked with LLD against the SDK's packages-llvm libraries. Precedence is the --toolchain flag, then PS2BUILD_TOOLCHAIN, then toolchain: in the project file, then gcc. Every static library linked into a Clang ELF must itself be Clang-built, which is why the next three steps exist.
3. Third-party libraries and generated headers
bash scripts/build_cloud_tls.sh
bash scripts/build_jpeg.sh
python3 scripts/gen_cloud_client.py| Script | What it produces |
|---|---|
build_cloud_tls.sh | Downloads Mbed TLS 3.6.7 (SHA-256 checked), disables the Unix timing and net modules, supplies the millisecond clock and hardware-entropy hook from src/cloud_http.c, and builds libmbedtls.a, libmbedx509.a and libmbedcrypto.a with the active toolchain into .cloud-deps/ (untracked). |
build_jpeg.sh | Builds libjpeg-turbo 3.1.3 (8-bit, arithmetic coding, memory source/destination, no SIMD) into .jpeg-deps/ and installs it as libjpegturbo.a. The SDK's LLVM-tier jpeg archive is GCC-built and is rejected by ld.lld (symbol has invalid binding), so ps2.yaml links -ljpegturbo instead. |
gen_cloud_client.py | Writes .cloud-deps/include/wle_cloud_client.h. If both WLE_GOOGLE_CLIENT_ID and WLE_GOOGLE_CLIENT_SECRET are set (see below) they are embedded as the built-in Google OAuth client; if neither is set the header defines nothing and the build has no built-in client. Setting only one, or an ID that does not end in .apps.googleusercontent.com, is an error. |
Both dependency scripts follow the same toolchain precedence as ps2build (they source scripts/ee_toolchain.sh) and record the toolchain in a TOOLCHAIN file inside their output folder. Re-run them whenever you switch toolchains or update the SDK. If .cloud-deps/ or .jpeg-deps/ is missing, the compile fails on missing headers.
4. PC CARD CompactFlash driver check
ps2build build -c iop/pccf/ps2.yamlBuilds the isolated pccf IOP module on its own, the way CI does, to prove the vendored driver still compiles. The main ps2.yaml also builds and embeds it. Its host-side tests are tests/test_pccf.py, test_pccf_owner.py and test_pccf_fs.py.
5. Build Launchstation
python3 scripts/gen_githash.py --require-git
ps2build buildgen_githash.py writes githash.h (untracked) with the 12-character commit and commit date shown in Build information and checked by the updater. --require-git makes it fail outside a Git checkout, as CI does; without it, a tree that is not a Git checkout is stamped local/unknown, and the updater cannot compare such a build with a release. ps2build build then reads ps2.yaml, builds the IOP modules and EE libraries, the embedded loader, the wle_igr helper and the main wLaunchELF target, and writes the output to build/:
build/bin/wLaunchELF-packed.elfis the release ELF (compressed withps2pack).build/bin/wLaunchELF.elfis the stripped, unpacked ELF;build/obj/wLaunchELF/wLaunchELF.unstripped.elfis the linked ELF with symbols used by the checks below.build/bin/*.irxare the source-built IOP modules (udpfs_smap,ps2ips_rpc(vendored PS2IP socket RPC server iniop/ps2ips_rpc/, ETH builds only),usbd_ra,mmceman,vmcman,ps2ftpd,ps2host,pccf,ata_bdand others);loader.elfandwle_igr.elfare also embedded.
ELF sizes vary by build; CI reports packed and unpacked sizes. A change to the default feature profile belongs in ps2.yaml (defines:, embed_irx:, libs:); the standard profile is MMCE, MX4SIO, iLink, exFAT, UDPFS plus ETH and SMB, DVRP, XFROM, TMANIP and custom colors with native cloud enabled and DS34 off.
6. DualShock (DS34) variant
python3 scripts/configure_ds34.py
ps2build build -c .ps2-ds34.yaml -o build-ds34configure_ds34.py derives .ps2-ds34.yaml (untracked) from ps2.yaml by swapping src/pad.c for src/pad_ds34.c, adding the ds34usb/ds34bt IOP modules and EE libraries and the DS34 define, and fails if any anchor in ps2.yaml changed shape. Keep the generated file at the repository root so relative paths still resolve. The result is build-ds34/bin/wLaunchELF-packed.elf. The DS34 build identifies itself as LAUNCHSTATION-DS.ELF in APP_LAUNCHSTATION-DS for the updater.
7. Verification scripts
Set LLVM_BIN to the toolchain's LLVM tools, then run the same checks as CI against the unstripped ELFs:
export LLVM_BIN="$HOME/.local/share/ps2build/llvm-toolchain/llvm/bin"
# DS34 symbols must be absent from the standard ELF and present in the DS34 ELF
python3 scripts/check_ds34_build.py "$LLVM_BIN/llvm-nm" \
build/obj/wLaunchELF/wLaunchELF.unstripped.elf \
build-ds34/obj/wLaunchELF/wLaunchELF.unstripped.elf
# every producer in .comment must be Clang; any "GCC:" entry fails
python3 scripts/check_toolchain_purity.py "$LLVM_BIN/llvm-readobj" \
build/obj/wLaunchELF/wLaunchELF.unstripped.elf \
build-ds34/obj/wLaunchELF/wLaunchELF.unstripped.elf \
build/obj/loader/loader.unstripped.elf \
build/obj/wle_igr/wle_igr.unstripped.elf \
build/obj/usbd_ra/usbd_ra.notiopmod.elf \
build/obj/mmceman/mmceman.notiopmod.elf
# source-built UDPFS SMAP is embedded and still matches the prebuilt peer ABI
python3 scripts/check_udpfs_smap.py build build-ds34
# the embedded loader must fit the 0x84000..0xa0000 low-memory window
sh scripts/check_loader_layout.sh build/obj/loader/loader.unstripped.elf \
"$LLVM_BIN/llvm-nm" 0x84000 auto auto 0xa0000The layout script arguments are <loader.elf> <nm-tool> <loadaddr> <stackaddr|auto> <stacksize|auto> <low_mem_limit>. CI also prints Mbed TLS archive sizes, the llvm-size sections and the 40 largest symbols so footprint work targets measured contributors.
8. Package the app archives
python3 scripts/package_apps.py \
--standard build/bin/wLaunchELF-packed.elf \
--dualshock build-ds34/bin/wLaunchELF-packed.elf \
--output artifacts \
--revision "$(git rev-parse HEAD)" \
--epoch "$(git show -s --format=%ct HEAD)"
python3 tests/test_app_packages.pyThis writes APP_LAUNCHSTATION.psu/.zip and APP_LAUNCHSTATION-DS.psu/.zip, plus loose LAUNCHSTATION.ELF, LAUNCHSTATION-DS.ELF, BOOT.ELF and BOOT2.ELF (the last two are copies of the standard ELF). Each app archive contains nine files: the ELF, a generated title.cfg, APPINFO.PBT, WLE-UPDATE-CA.PEM, BGM.ADP (from res/music.sad), and the memory-card set icon.sys, list.icn, copy.icn and del.icn from res/memory-card/. The script re-reads every archive to confirm that the ZIP and PSU payloads are identical. --revision must be lowercase hexadecimal and --epoch must fall in 2000-2099 (PS2 timestamps). scripts/copy_test_pack.py --output test-fixtures/LS-COPY-TEST.zip creates the deterministic copy-test fixture CI uploads for hardware testers.
9. Run the tests
python3 tests/test_self_update.py # one test
# the whole suite
failed=0
for t in tests/test_*.py; do
python3 "$t" || failed=1
done
test "$failed" = 0The tests are plain Python scripts that exit non-zero on failure. Many compile production C sources against stubs with the host gcc or clang and exercise them; others assert on build definitions, workflows and documentation. They need no PS2 tools, need NumPy installed, and must run inside a Git checkout (test_build_identity.py and test_release_notes.py call git). CI runs an explicit list, so a new test must be added to the Test Settings sessions, navigation and theme compatibility step of compile.yml to count. Hardware timing, USB behavior and console quirks are not covered by any of them. See Contributing & Testing.
10. GCC comparison builds
export PS2BUILD_TOOLCHAIN=gcc
bash scripts/build_cloud_tls.sh
bash scripts/build_jpeg.sh
python3 scripts/gen_cloud_client.py
ps2build build -o build-gccUse a separate -o directory. The dependency scripts overwrite .cloud-deps/ and .jpeg-deps/, so unset the variable and re-run them before your next LLVM build. GCC output is for comparison only; the purity check does not apply to it and release packages are Clang-built.
11. Documentation site and wiki
python3 docs-site/build.py --out build/docs-site
python3 docs-site/build.py --out site --zip Launchstation-Documentation.zipbuild.py renders every page in docs-site/manifest.json from docs-site/content/<slug>.html, builds the search index and fails on missing pages, broken internal links, unresolved template tokens, missing images and stale upstream branding outside Credits. CI builds it on every run; the Publish Documentation workflow pushes it to gh-pages for changes under docs-site/** or to that workflow on master/main, and on manual dispatch. Synchronize GitHub Wiki runs scripts/sync_github_wiki.py to mirror docs/wiki/*.md, the site pages and docs/THIRD_PARTY_CREDITS.md into the repository wiki without deleting unmanaged pages or force-pushing.
12. Claude Code cloud sessions
The repository ships .claude/settings.json with a SessionStart hook that runs .claude/hooks/session-start.sh. In a Claude Code cloud session (CLAUDE_CODE_REMOTE=true; it does nothing elsewhere) the hook installs the PS2BUILD SDK with the upstream installer if ps2build is missing, runs ps2build update and fetches the LLVM toolchain if its compiler config is missing, and exports PATH, LLVM_BIN and PS2BUILD_TOOLCHAIN=llvm for the session through CLAUDE_ENV_FILE. It honors PS2DEV_INSTALL_DIR as the install location. This hook does not use CI's SDK resolver/mirror script. For a custom SDK directory, also set PS2BUILD_ROOT to the same path before running the dependency builds in sections 3 to 9.
Troubleshooting a build
ps2build: command not found: add~/.local/share/ps2buildtoPATH.PS2 EE clang config not found: runps2build update llvm-toolchain.- Missing
mbedtls/ssl.horjpeglib.h: run the two dependency scripts first. symbol has invalid bindingfromld.lld, or a purity failure listingGCC:: a GCC-built archive was linked; rebuild the dependencies with the same toolchain, and do not addjpegor the GCC-only SDKaudsrvpackage tops2.yaml(sound usesaudsrv-compatand theaudio:audsrvdriver) (tests/test_llvm_toolchain.pyguards this).Expected a Git revision/outside the PS2 timestamp range: pass a lowercase hex--revisionand a Unix--epochin 2000-2099 topackage_apps.py.- Update check says the build cannot be compared: it was stamped
local; rebuild with--require-gitfrom a real commit.