Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion .github/workflows/mobile-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions apps/ios/scripts/build-rust.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions tools/scripts/build-android-rust.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
75 changes: 75 additions & 0 deletions tools/scripts/check-ish-vdso.sh
Original file line number Diff line number Diff line change
@@ -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 <cargo-target-dir> <rust-target-triple> <profile>
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 <target>/<triple>/<profile>/build/ for
# custom profiles, but historically landed at <target>/<triple>/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)"
Loading