Files
software-mansion__argent/scripts/download-native-binaries.sh
Hubert Gancarczyk cdfa9fba98 fix(android-devtools): check the downloaded helper before it becomes the bundled one
The versionCode check ran after `mv -f "${TMP_APK}" "${ANDROID_TARGET}"` and
after the EXIT trap was cleared, so a release that fails it aborted having
installed the APK it had just diagnosed — at exactly the path
`bundledHelperApkPath()` reads, with the temp copy kept as well.

Reproduced against the live `argent-main` release (versionCode 1, manifest
requires 2) with a sentinel standing in for a previously downloaded helper: the
sentinel was replaced by the rejected APK before the error printed.

The check now reads the temp file and the move happens after it. Same run
against the same release: exit 1, the previous APK still in place, no temp left
behind. A matching versionCode still installs it (verified by lowering the
manifest to 1 and re-running).
2026-08-24 20:24:56 +02:00

286 lines
13 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"
# Verify the downloaded APK carries the versionCode the manifest promises.
#
# Three numbers must move together, and only one of them lives in this repo: the
# manifest `versionCode` (the bar `ensureAndroidDevtoolsInstalled` installs to),
# the helper's own `PROTOCOL_VERSION` (what the tool-server's protocol gates ask
# for, e.g. SET_TEXT_MIN_PROTOCOL), and the `aapt2 --version-code` the APK is
# linked with in argent-private's build-native-binaries.yml. A release that lags
# a manifest bump fails SILENTLY on every device: the install gate
# (device >= manifest) can never be satisfied, so each tool-server process
# reinstalls the helper once per device, and every protocol-gated call falls back
# to its weaker path while the build itself looks healthy. Fail here instead.
#
# Checked on the TEMP file, before the move below. The target path is the one
# `bundledHelperApkPath()` reads, so verifying after the move aborts having
# installed the bad state it just diagnosed — and having cleared the EXIT trap,
# so the download is kept twice over. Failing here leaves the previous APK in
# place and the trap removes the temp.
#
# aapt2 ships with the Android build-tools. Hosts without it (no Android SDK)
# skip the check, like the vtool and apksigner checks around it.
AAPT2="$(command -v aapt2 || true)"
if [ -z "${AAPT2}" ]; then
# Globs sort lexicographically, so the last match is the newest build-tools.
for candidate in "${ANDROID_HOME:-${HOME}/Library/Android/sdk}"/build-tools/*/aapt2; do
[ -x "${candidate}" ] && AAPT2="${candidate}"
done
fi
if [ -n "${AAPT2}" ]; then
ANDROID_VERSION_CODE="$(node -p "require('$PWD/${ANDROID_MANIFEST_FILE}').versionCode")"
BADGING="$("${AAPT2}" dump badging "${TMP_APK}")"
APK_VERSION_CODE="$(printf '%s\n' "${BADGING}" | sed -n "1s/.*versionCode='\([0-9]*\)'.*/\1/p")"
if [ "${APK_VERSION_CODE}" != "${ANDROID_VERSION_CODE}" ]; then
echo "Error: argent-android-devtools.apk on release '${TAG}' is versionCode" >&2
echo " '${APK_VERSION_CODE:-<unreadable>}', but ${ANDROID_MANIFEST_FILE} requires" >&2
echo " ${ANDROID_VERSION_CODE}. The helper release lags this repo's manifest." >&2
echo " Land the argent-private helper change, re-run build-native-binaries," >&2
echo " then retry — or lower the manifest versionCode to match." >&2
exit 1
fi
echo "Android helper versionCode OK (${APK_VERSION_CODE})."
else
echo " Skipping Android helper versionCode check: aapt2 not found." >&2
fi
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