diff --git a/.github/workflows/mobile-ci.yml b/.github/workflows/mobile-ci.yml index e0d05f8a2..826118867 100644 --- a/.github/workflows/mobile-ci.yml +++ b/.github/workflows/mobile-ci.yml @@ -177,7 +177,7 @@ jobs: sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0" "ndk;30.0.14904198" - name: Install system deps - run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev libcap-dev clang cmake + run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev libcap-dev clang lld cmake - name: Setup Zig (libghostty build) uses: mlugg/setup-zig@v2 @@ -382,3 +382,31 @@ jobs: ONLY_ACTIVE_ARCH=YES \ -only-testing:LitterTests \ test-without-building + + # Runtime smoke test: install the freshly built app, launch it, and + # assert the process survives. Unit tests never boot the embedded iSH + # guest runtime, so they cannot catch silently-broken embeds (e.g. + # litter-ish's ARM64 vdso degrading to an empty stub when no + # lld-capable clang is available, which SIGABRTs the app at launch). + - name: Smoke test (app launches and stays alive) + run: | + set -euo pipefail + xcrun simctl bootstatus "$IOS_SIMULATOR_UDID" -b + APP="$(find "$HOME/Library/Developer/Xcode/DerivedData" \ + -path "*Debug-iphonesimulator/Litter.app" -type d -print -quit 2>/dev/null || true)" + if [ -z "$APP" ]; then + echo "Could not locate Litter.app in DerivedData" >&2 + exit 1 + fi + echo "Using app: $APP" + xcrun simctl install "$IOS_SIMULATOR_UDID" "$APP" + xcrun simctl launch "$IOS_SIMULATOR_UDID" com.sigkitten.litter + sleep 10 + if ! xcrun simctl spawn "$IOS_SIMULATOR_UDID" launchctl list 2>/dev/null \ + | grep -q "UIKitApplication:com.sigkitten.litter"; then + echo "App exited or crashed within 10s of launch" >&2 + xcrun simctl spawn "$IOS_SIMULATOR_UDID" log show --last 2m \ + --predicate 'process == "Litter"' --style compact 2>/dev/null | tail -80 || true + exit 1 + fi + echo "App is alive" diff --git a/AGENTS.md b/AGENTS.md index cc7adfe75..e32d19e79 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,7 +97,7 @@ Before building on a new machine, verify: 1. `xcode-select -p` must print `/Applications/Xcode.app/Contents/Developer`, not `/Library/Developer/CommandLineTools`. Fix with `sudo xcode-select -s /Applications/Xcode.app/Contents/Developer`. The Command Line Tools do not include iOS simulator SDKs. 2. `cargo` and `rustc` must come from **rustup**, not Homebrew's `rust` formula. If `which cargo` points to `/opt/homebrew/bin/cargo` (a Homebrew standalone binary, not a rustup proxy), cross-compilation targets like `aarch64-apple-ios-sim` will fail even if `rustup target list` shows them installed. The Makefile prepends the rustup toolchain bin to PATH automatically, but standalone script runs and CI environments must also ensure the correct resolution. Either `brew uninstall rust` or put `~/.cargo/bin` (or the rustup toolchain bin from `rustup which cargo`) before `/opt/homebrew/bin` in PATH. -3. `xcodegen` must be installed (`brew install xcodegen`). Required for Xcode project generation. +3. Build toolchain dependencies must be installed via Homebrew: `brew install xcodegen meson ninja llvm lld` and `brew install zig@0.15`. `xcodegen` is required for Xcode project generation; `meson`/`ninja` are required by the embedded iSH build; and `llvm`/`lld` (not Apple's `/usr/bin/clang`, which lacks `lld`) are required to cross-compile the iSH ARM64 vdso. If `llvm`/`lld` are missing, `tools/scripts/check-ish-vdso.sh` fails the Rust build because litter-ish silently degrades the vdso to an empty stub that makes the app SIGABRT at launch. 4. *(Optional)* `pymobiledevice3` enables `make ios-device-run` over Tailscale when the device is not on the local network. Install with `pipx install pymobiledevice3` (or `uv tool install pymobiledevice3`). Also requires Tailscale on both the Mac and the iOS device. ## Build System diff --git a/apps/ios/scripts/build-rust.sh b/apps/ios/scripts/build-rust.sh index 8e55f240d..45a1a6854 100755 --- a/apps/ios/scripts/build-rust.sh +++ b/apps/ios/scripts/build-rust.sh @@ -302,6 +302,15 @@ copy_macabi_artifact() { -output "$GENERATED_MACABI_DIR/libcodex_mobile_client.a" } +# Fail the build instead of shipping an app whose embedded iSH runtime has a +# broken (empty-stub) ARM64 vdso. litter-ish silently degrades the vdso to an +# empty placeholder when no lld-capable clang is available; at runtime that +# makes the app SIGABRT as soon as a guest process hits a signal. +check_ish_vdso_for() { + local triple="$1" + "$REPO_DIR/tools/scripts/check-ish-vdso.sh" "$CARGO_TARGET_DIR_EFFECTIVE" "$triple" "$PROFILE" +} + echo "==> Preparing codex submodule..." "$SCRIPT_DIR/sync-codex.sh" "$SYNC_MODE" @@ -329,15 +338,18 @@ rustup target add i686-unknown-linux-musl aarch64-unknown-linux-musl if [ "$DEVICE_ONLY" -eq 1 ]; then echo "==> Building codex-mobile-client for aarch64-apple-ios ($PROFILE)..." cargo rustc --manifest-path "$RUST_BRIDGE_DIR/Cargo.toml" -p codex-mobile-client $CARGO_PROFILE_FLAG --target aarch64-apple-ios --crate-type staticlib $CARGO_FEATURES + check_ish_vdso_for aarch64-apple-ios copy_device_artifact elif [ "$SIM_ONLY" -eq 1 ]; then echo "==> Building codex-mobile-client for aarch64-apple-ios-sim ($PROFILE)..." cargo rustc --manifest-path "$RUST_BRIDGE_DIR/Cargo.toml" -p codex-mobile-client $CARGO_PROFILE_FLAG --target aarch64-apple-ios-sim --crate-type staticlib $CARGO_FEATURES + check_ish_vdso_for aarch64-apple-ios-sim copy_sim_artifact "$CARGO_TARGET_DIR_EFFECTIVE/aarch64-apple-ios-sim/$PROFILE/libcodex_mobile_client.a" elif [ "$MACABI_ONLY" -eq 1 ]; then if [ "$FAST_MACABI" -eq 1 ]; then echo "==> Building codex-mobile-client for $MACABI_HOST_TARGET ($PROFILE)..." cargo rustc --manifest-path "$RUST_BRIDGE_DIR/Cargo.toml" -p codex-mobile-client $CARGO_PROFILE_FLAG --target "$MACABI_HOST_TARGET" --crate-type staticlib $CARGO_FEATURES + check_ish_vdso_for "$MACABI_HOST_TARGET" copy_if_changed "$CARGO_TARGET_DIR_EFFECTIVE/$MACABI_HOST_TARGET/$PROFILE/libcodex_mobile_client.a" \ "$GENERATED_MACABI_DIR/libcodex_mobile_client.a" else @@ -367,6 +379,8 @@ elif [ "$MACABI_ONLY" -eq 1 ]; then fi [ "$FAILED" -eq 0 ] || exit 1 + check_ish_vdso_for aarch64-apple-ios-macabi + check_ish_vdso_for x86_64-apple-ios-macabi copy_macabi_artifact fi else @@ -417,6 +431,11 @@ else fi [ "$FAILED" -eq 0 ] || exit 1 + check_ish_vdso_for aarch64-apple-ios + check_ish_vdso_for aarch64-apple-ios-sim + check_ish_vdso_for aarch64-apple-ios-macabi + check_ish_vdso_for x86_64-apple-ios-macabi + copy_device_artifact copy_sim_artifact "$CARGO_TARGET_DIR_EFFECTIVE/aarch64-apple-ios-sim/$PROFILE/libcodex_mobile_client.a" copy_macabi_artifact diff --git a/tools/scripts/build-android-rust.sh b/tools/scripts/build-android-rust.sh index c4663f6ed..390348dc8 100755 --- a/tools/scripts/build-android-rust.sh +++ b/tools/scripts/build-android-rust.sh @@ -110,4 +110,12 @@ echo "==> Building codex_mobile_client Android shared libs..." cd "$WORKSPACE_DIR" cargo ndk "${ABI_ARGS[@]}" -o "$OUT_DIR" build --profile "$RUST_PROFILE" -p codex-mobile-client +# Fail the build instead of shipping an app whose embedded iSH runtime has a +# broken (empty-stub) ARM64 vdso; at runtime that SIGABRTs as soon as a guest +# process hits a signal. Mirrors the iOS guard in apps/ios/scripts/build-rust.sh. +TARGET_DIR="${CARGO_TARGET_DIR:-$WORKSPACE_DIR/target}" +for target in "${RUST_TARGETS[@]}"; do + "$REPO_DIR/tools/scripts/check-ish-vdso.sh" "$TARGET_DIR" "$target" "$RUST_PROFILE" +done + echo "==> Done. Android JNI libs are in: $OUT_DIR" diff --git a/tools/scripts/check-ish-vdso.sh b/tools/scripts/check-ish-vdso.sh new file mode 100755 index 000000000..354ff0631 --- /dev/null +++ b/tools/scripts/check-ish-vdso.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Validate that the ARM64 guest vdso embedded by litter-ish is a real aarch64 +# ELF rather than the empty placeholder that litter-ish's +# vdso/arm64/meson.build creates when it cannot find an lld-capable clang +# (e.g. only Apple's clang on PATH). An empty vdso makes the app SIGABRT at +# launch as soon as a guest process hits a signal, so the build must fail +# loudly instead of silently shipping a broken embed. +# +# Usage: check-ish-vdso.sh +set -euo pipefail + +CARGO_TARGET_DIR="${1:?missing cargo target dir}" +TRIPLE="${2:?missing rust target triple}" +PROFILE="${3:?missing cargo profile}" + +install_hint() { + case "$(uname -s)" in + Darwin) + echo "install LLVM's clang and lld, then rebuild: brew install llvm lld" + ;; + Linux) + if command -v apt-get >/dev/null 2>&1; then + echo "install clang and lld, then rebuild: apt-get install clang lld" + elif command -v dnf >/dev/null 2>&1; then + echo "install clang and lld, then rebuild: dnf install clang lld" + else + echo "install an lld-capable clang (clang + lld) and rebuild" + fi + ;; + *) + echo "install an lld-capable clang (clang + lld) and rebuild" + ;; + esac +} + +HINT="$(install_hint)" + +# Build-script outputs live under ///build/ for +# custom profiles, but historically landed at //build/. Search +# both layouts so the check is resilient to cargo layout changes. +VDSO_DIR="" +for base in \ + "$CARGO_TARGET_DIR/$TRIPLE/$PROFILE/build" \ + "$CARGO_TARGET_DIR/$TRIPLE/build" +do + VDSO_DIR="$(find "$base" -type d -path "*ish-embed-host-*/out/meson-build/vdso/arm64" 2>/dev/null | head -n 1 || true)" + [ -n "$VDSO_DIR" ] && break +done + +if [ -z "$VDSO_DIR" ]; then + # No embed build output for this target/profile (e.g. cleaned cache). The + # stub degradation always leaves the meson build dir behind, so there is + # nothing to validate here. + exit 0 +fi + +VDSO="$VDSO_DIR/libvdso.so.elf" +if [ ! -f "$VDSO" ] || [ ! -s "$VDSO" ]; then + echo "ERROR: iSH ARM64 vdso is missing or empty at $VDSO" >&2 + echo " litter-ish's vdso/arm64/meson.build builds an empty stub when it cannot find" >&2 + echo " an lld-capable clang; $HINT" >&2 + exit 1 +fi + +VDSO_KIND="$(file -b "$VDSO")" +case "$VDSO_KIND" in + *"ELF 64-bit"*"ARM aarch64"*) ;; + *) + echo "ERROR: iSH ARM64 vdso at $VDSO is not an aarch64 ELF: $VDSO_KIND" >&2 + echo " The app will SIGABRT at launch when a guest process hits a signal; $HINT" >&2 + exit 1 + ;; +esac + +echo "==> iSH ARM64 vdso OK ($(stat -f '%z' "$VDSO") bytes, $VDSO_KIND)"