Downloads GitHub

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.

Short version
# 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 build

The 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

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
ScriptWhat it produces
build_cloud_tls.shDownloads 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.shBuilds 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.pyWrites .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.yaml

Builds 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 build

gen_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/:

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-ds34

configure_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 0xa0000

The 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.py

This 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" = 0

The 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-gcc

Use 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.zip

build.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