Files
software-mansion__argent/scripts/download-native-binaries.sh
Juliusz Wajgelt 013d5b60c5 fix: get ax-service working on ios-remote (sim-remote --json spawn + TCP artifact diagnostics) (#536)
## Summary

Three fixes that together get `describe` (and the other ax-service /
native-devtools tools) working against **ios-remote** (`sim-remote`)
devices — including from a **Linux** host. Reproduced end-to-end against
a live remote iPhone 17 simulator over the QUIC relay.

Before: `argent run describe --udid remote:<UDID>` returned an empty
`ROOT AXGroup` with a misleading "reboot the simulator" hint.

### 1. `sim-remote spawn` output was never valid JSON
(`fix(tool-server)`)

`simctlSpawn` parses the CLI's stdout as one `{exit_code,stdout,stderr}`
(or `{pid}`) JSON object, but `sim-remote spawn` only emits that when
passed `--json`. Without it a **non-detached** spawn streams the child's
raw output live, so `JSON.parse` throws `Unexpected end of JSON input`.

This broke ios-remote describe at the first step:
`remoteIosHost.bootstrapAx` runs `defaults write com.apple.Accessibility
…` through a non-detached `simctlSpawn`, which failed before the
ax-service daemon ever started — surfacing only as an empty AX tree.
Fix: pass `--json` unconditionally (no-op for the already-JSON
`--detach` path).

### 2. The tcp ax-service was resolved/packaged under a host-platform
dir (`fix(native-devtools-ios)`)

The tcp `ax-service` is a **darwin/iOS-simulator** binary that is
uploaded to and `simctl spawn`d on the **remote** macOS orchestrator, so
it's needed on any *host* platform. But it was resolved (and packaged)
under `bin/<hostPlatform>/tcp` via `platformBinDir()`, so a **Linux host
looked at `bin/linux/tcp` and never found the darwin-built binary** —
the ios-remote path was effectively macOS-host-only. (The tcp dylibs
were already neutral at `dylibs/tcp`.)

Moved it to a platform-neutral `bin/tcp`, mirroring `dylibs/tcp`, and
rewired every producer/consumer:
- resolver: `platformTcpBinDir()` → `tcpBinDir()` = `BIN_DIR/tcp`
- `native-devtools-ios/scripts/build.sh`: `bin/darwin/tcp` → `bin/tcp`
- `scripts/download-native-binaries.sh`: `TCP_BIN_DIR` → `bin/tcp`
- `packages/argent/scripts/bundle-tools.cjs`: copy from/to `bin/tcp`
- tests assert the neutral location on both linux and darwin

### 3. Missing tcp artifacts were undiagnosable
(`fix(native-devtools-ios)`)

When a tcp artifact is absent (local/dev build, older package), the bare
`... not found` error was thrown deep in the ax-service factory, where
`describeIos` swallowed it and returned an empty tree plus
`DEGRADED_HINT` — telling the agent to `boot-device --force`, a dead
end. The only escape was discovering `ARGENT_SIMULATOR_SERVER_TCP_DIR`
by reading source. Fix: `requireTcpArtifact` throws an actionable error
naming the exact path, the override env var, and how to build it;
`describeIos` surfaces that instead of the reboot hint.

## Testing

- New `sim-remote-spawn-json.test.ts` — asserts `--json` on both plain
and `--bin/--detach` argv, before the `--` separator.
- `native-devtools-ios/test/index.test.ts` — the tcp binary resolves
from the neutral `bin/tcp` on **both** linux and darwin; the not-found
errors name their override env var.
- `native-devtools-ios` (16) + ios/describe tool-server suites pass;
typecheck, eslint, prettier clean.
- Manual end-to-end against `remote:<UDID>` (darwin host): binary at
`bin/tcp` → full tree from `source: "ax-service"`, no hint; binary
removed → actionable `ARGENT_SIMULATOR_SERVER_TCP_DIR` hint.
2026-07-22 08:47:52 +02:00

241 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Downloads signed native binaries from argent-private-releases:
# - iOS dylibs + ax-service
# - Android argent-android-devtools.apk (helper APK consumed by
# packages/native-devtools-android)
#
# The host-side Perfetto trace processor is no longer a native binary: it ships
# as a single ~13 MB `trace_processor.wasm` vendored into the repo
# (packages/native-devtools-android/assets/trace-processor/), so there is nothing
# to download for it here.
#
# Usage: ./scripts/download-native-binaries.sh [release-tag]
# release-tag Tag to download from (e.g. argent-v0.5.3). Defaults to argent-main.
#
# Requires:
# - gh CLI (no authentication needed — the repo is public)
REPO="software-mansion-labs/argent-private-releases"
TAG="${1:-argent-main}"
# Verify the release exists before attempting downloads.
if ! gh release view "${TAG}" --repo "${REPO}" &>/dev/null; then
echo "Error: release '${TAG}' not found in ${REPO}." >&2
echo "Build and publish the native binaries for this version first, then retry." >&2
exit 1
fi
DYLIBS_DIR="packages/native-devtools-ios/dylibs"
BIN_DIR="packages/native-devtools-ios/bin"
# ax-service is macOS-only (it spawns inside an iOS Simulator), so it lives
# under the darwin/ host-platform subdirectory — matching what
# axServiceBinaryPath() resolves (bin/<platform>/ax-service) and what
# packages/argent/scripts/bundle-tools.cjs copies into the published package
# (bin/darwin/ax-service). Writing it to the flat bin/ root instead silently
# drops it from every release: bundle-tools looks under darwin/, finds nothing,
# and skips the copy, so describe's ax-service path is unusable (regressed in
# the Linux-support layout migration, #249).
IOS_BIN_DIR="${BIN_DIR}/darwin"
ANDROID_BIN_DIR="packages/native-devtools-android/bin"
ANDROID_MANIFEST_FILE="packages/native-devtools-android/assets/manifest.json"
echo "Downloading native binaries from ${REPO} (tag: ${TAG})..."
mkdir -p "${DYLIBS_DIR}" "${BIN_DIR}" "${IOS_BIN_DIR}" "${ANDROID_BIN_DIR}"
for DYLIB in libNativeDevtoolsIos.dylib libKeyboardPatch.dylib libArgentInjectionBootstrap.dylib; do
echo " Downloading ${DYLIB}..."
gh release download "${TAG}" \
--repo "${REPO}" \
--pattern "${DYLIB}" \
--dir "${DYLIBS_DIR}" \
--clobber
done
echo " Downloading ax-service..."
gh release download "${TAG}" \
--repo "${REPO}" \
--pattern "ax-service" \
--dir "${IOS_BIN_DIR}" \
--clobber
chmod +x "${IOS_BIN_DIR}/ax-service"
# tvOS binaries (Apple TV support). The three tvOS injection dylibs share their
# filenames with the iOS dylibs, so the release ships them as a tarball
# (native-devtools-ios-tvos-dylibs.tar.gz) that we extract into dylibs/tvos/ —
# the directory bootstrapDylibPathTvos() reads from. The two daemons
# (tvos-ax-service spawned in-sim, tvos-hid-daemon on the host) have unique
# names and download flat into bin/darwin/.
#
# These assets only exist on releases built with TV support. A pre-TV-support
# tag (the optional [release-tag] arg lets you pull older releases) simply has
# no tvOS artifacts, so a missing asset is skipped with a warning rather than
# aborting the whole download (`gh release download` exits non-zero on no match,
# which under `set -e` would otherwise leave a half-populated tree).
TVOS_DYLIBS_DIR="${DYLIBS_DIR}/tvos"
mkdir -p "${TVOS_DYLIBS_DIR}"
echo " Downloading tvOS dylibs..."
TMP_TVOS_DYLIBS="$(mktemp -t native-devtools-ios-tvos-dylibs.XXXXXX.tar.gz)"
if gh release download "${TAG}" \
--repo "${REPO}" \
--pattern "native-devtools-ios-tvos-dylibs.tar.gz" \
--output "${TMP_TVOS_DYLIBS}" \
--clobber; then
tar -xzf "${TMP_TVOS_DYLIBS}" -C "${TVOS_DYLIBS_DIR}"
else
echo " Skipping tvOS dylibs: not present on '${TAG}' (pre-Apple-TV-support release)." >&2
fi
rm -f "${TMP_TVOS_DYLIBS}"
echo " Downloading tvos-ax-service..."
if gh release download "${TAG}" \
--repo "${REPO}" \
--pattern "tvos-ax-service" \
--dir "${IOS_BIN_DIR}" \
--clobber; then
chmod +x "${IOS_BIN_DIR}/tvos-ax-service"
else
echo " Skipping tvos-ax-service: not present on '${TAG}' (pre-Apple-TV-support release)." >&2
fi
echo " Downloading tvos-hid-daemon..."
if gh release download "${TAG}" \
--repo "${REPO}" \
--pattern "tvos-hid-daemon" \
--dir "${IOS_BIN_DIR}" \
--clobber; then
chmod +x "${IOS_BIN_DIR}/tvos-hid-daemon"
else
echo " Skipping tvos-hid-daemon: not present on '${TAG}' (pre-Apple-TV-support release)." >&2
fi
# TCP-transport iOS binaries (sim-remote / ios-remote support). The remote
# (ios-remote) code path spawns the ax-service and injects the native-devtools
# dylibs over a TCP socket tunnelled by sim-remote, rather than the AF_UNIX
# sockets the local path uses — so it needs -DARGENT_USE_TCP=1 builds kept in a
# separate tcp/ slot (axServiceBinaryPathTcp()/bootstrapDylibPathTcp() read from
# bin/tcp/ and dylibs/tcp/). Both are platform-NEUTRAL: these are darwin/iOS-sim
# artifacts uploaded to and run on the *remote* macOS orchestrator, so they must
# resolve from any host platform (a Linux host must not look under bin/linux/).
# Like the tvOS dylibs, the three TCP dylibs share basenames with the flat iOS
# dylibs, so they ship as a tarball extracted into dylibs/tcp/; tcp-ax-service
# has a unique release name and lands flat as bin/tcp/ax-service.
#
# These assets only exist on releases built with TCP support. A pre-sim-remote
# tag simply has no TCP artifacts, so a missing asset is skipped with a warning
# rather than aborting the whole download (`gh release download` exits non-zero
# on no match, which under `set -e` would otherwise leave a half-populated tree).
TCP_DYLIBS_DIR="${DYLIBS_DIR}/tcp"
# Platform-neutral (bin/tcp, not bin/darwin/tcp): see the comment above.
TCP_BIN_DIR="${BIN_DIR}/tcp"
mkdir -p "${TCP_DYLIBS_DIR}" "${TCP_BIN_DIR}"
echo " Downloading TCP dylibs..."
TMP_TCP_DYLIBS="$(mktemp -t native-devtools-ios-tcp-dylibs.XXXXXX.tar.gz)"
if gh release download "${TAG}" \
--repo "${REPO}" \
--pattern "native-devtools-ios-tcp-dylibs.tar.gz" \
--output "${TMP_TCP_DYLIBS}" \
--clobber; then
tar -xzf "${TMP_TCP_DYLIBS}" -C "${TCP_DYLIBS_DIR}"
else
echo " Skipping TCP dylibs: not present on '${TAG}' (pre-sim-remote-support release)." >&2
fi
rm -f "${TMP_TCP_DYLIBS}"
echo " Downloading tcp-ax-service..."
if gh release download "${TAG}" \
--repo "${REPO}" \
--pattern "tcp-ax-service" \
--dir "${TCP_BIN_DIR}" \
--clobber; then
# Uploaded under a unique name to avoid colliding with the iOS ax-service in
# the flattened release dir; restore the basename the runtime resolver expects.
mv -f "${TCP_BIN_DIR}/tcp-ax-service" "${TCP_BIN_DIR}/ax-service"
chmod +x "${TCP_BIN_DIR}/ax-service"
else
echo " Skipping tcp-ax-service: not present on '${TAG}' (pre-sim-remote-support release)." >&2
fi
echo " Downloading argent-android-devtools.apk..."
# The release publishes the APK under a stable name (no versioning in the
# filename) so this script doesn't have to know the version ahead of time;
# the local copy is renamed to match what manifest.json expects.
TMP_APK="$(mktemp -t argent-android-devtools.XXXXXX.apk)"
trap 'rm -f "$TMP_APK"' EXIT
gh release download "${TAG}" \
--repo "${REPO}" \
--pattern "argent-android-devtools.apk" \
--output "${TMP_APK}" \
--clobber
# Read the versionName from the local manifest so the filename matches
# bundledHelperApkPath()'s expectation.
ANDROID_VERSION_NAME="$(node -p "require('$PWD/${ANDROID_MANIFEST_FILE}').versionName")"
ANDROID_TARGET="${ANDROID_BIN_DIR}/argent-android-devtools-${ANDROID_VERSION_NAME}.apk"
mv -f "${TMP_APK}" "${ANDROID_TARGET}"
trap - EXIT
echo "Downloaded native binaries to ${DYLIBS_DIR}/, ${IOS_BIN_DIR}/, and ${ANDROID_BIN_DIR}/"
# Verify the downloaded injection dylibs carry the expected Mach-O platform.
# This is the exact failure a mis-built release can reintroduce: a tvOS-built
# libArgentInjectionBootstrap.dylib landing in the flat iOS slot. dyld silently
# skips a DYLD_INSERT_LIBRARIES library whose LC_BUILD_VERSION platform does not
# match the process, so native-devtools never injects on an iOS simulator and
# every native-* tool returns restart_required — with no error at download,
# sign, or pack time. Fail loudly here rather than bundle a dead dylib.
# vtool is macOS-only; on hosts without it (non-macOS) the check is skipped.
if command -v vtool &>/dev/null; then
echo "Verifying dylib platforms..."
dylib_verify_failed=0
assert_dylib_platform() { # <file> <expected-platform>
local f="$1" want="$2" arch got
[ -f "$f" ] || return 0 # tvOS dylibs are absent on pre-Apple-TV tags
for arch in arm64 x86_64; do
got="$(vtool -arch "$arch" -show-build "$f" 2>/dev/null | awk '/platform/{print $2}')"
if [ "$got" != "$want" ]; then
echo " ERROR: ${f} (${arch}) is '${got:-<none>}', expected ${want}" >&2
dylib_verify_failed=1
fi
done
}
for d in libNativeDevtoolsIos libKeyboardPatch libArgentInjectionBootstrap; do
assert_dylib_platform "${DYLIBS_DIR}/${d}.dylib" IOSSIMULATOR
assert_dylib_platform "${TVOS_DYLIBS_DIR}/${d}.dylib" TVOSSIMULATOR
# TCP dylibs are iOS-simulator Mach-Os (same SDK, -DARGENT_USE_TCP=1), so a
# tvOS slice leaking into dylibs/tcp/ is the same dyld-silent-skip failure.
assert_dylib_platform "${TCP_DYLIBS_DIR}/${d}.dylib" IOSSIMULATOR
done
if [ "${dylib_verify_failed}" -ne 0 ]; then
echo "Dylib platform verification failed for release '${TAG}'. The release" >&2
echo "shipped a mis-platformed dylib (see above) — refusing to use it. Fix" >&2
echo "the build-native-binaries workflow / re-publish the release, then retry." >&2
exit 1
fi
echo "Dylib platforms OK (iOS/TCP=IOSSIMULATOR, tvOS=TVOSSIMULATOR where present)."
fi
if command -v codesign &>/dev/null; then
for f in \
"${DYLIBS_DIR}"/*.dylib \
"${TVOS_DYLIBS_DIR}"/*.dylib \
"${TCP_DYLIBS_DIR}"/*.dylib \
"${IOS_BIN_DIR}/ax-service" \
"${IOS_BIN_DIR}/tvos-ax-service" \
"${IOS_BIN_DIR}/tvos-hid-daemon" \
"${TCP_BIN_DIR}/ax-service"; do
[ -f "$f" ] || continue
codesign -dvv "$f" 2>&1 || echo "Warning: signature verification failed for $f"
done
fi
if command -v "${ANDROID_HOME:-${HOME}/Library/Android/sdk}/build-tools/36.0.0/apksigner" &>/dev/null; then
"${ANDROID_HOME:-${HOME}/Library/Android/sdk}/build-tools/36.0.0/apksigner" verify --verbose "${ANDROID_TARGET}" 2>&1 \
|| echo "Warning: APK signature verification failed"
fi