Files
software-mansion__argent/scripts/download-simulator-server.sh
Ignacy Łątka 7400179b5b Windows support: Android + Chromium host control plane (#421)
## What

Makes Argent run on Windows. iOS Simulator is macOS-only (Apple), so on
Windows that means the **Android + Chromium** host control plane. The
blocker was twofold: (1) no Windows `simulator-server` binary, and (2)
POSIX-only assumptions in the tool-server.

Companion radon PR builds the binary and runs the Android emulator E2E
on Windows: software-mansion/radon#134.

## Changes

**Windows-aware tool-server / resolver / bundling**
- New shared `commandOnPath()` helper — `where` on Windows, `command -v`
via `/bin/sh` on POSIX. `android-binary`, `check-deps`, and `vega-cli`
previously hardcoded `/bin/sh`, which never matches on Windows.
- `android-binary`: append `.exe` to `adb`/`emulator` in the
`$ANDROID_HOME` fallback; probe `%LOCALAPPDATA%\Android\Sdk` (Android
Studio's Windows default).
- simulator-server resolver + dispatcher + `bundle-tools` +
`download-simulator-server.sh`: ship and resolve `simulator-server.exe`
on `win32` (new `simulatorServerBinaryName()`), add `win32` to the
bundled host keys, add the `simulator-server-argent-windows.exe`
download target with PE arch validation.
- `stop-metro`: resolve listening PIDs via `netstat -ano` on Windows
(`lsof` is POSIX-only and threw ENOENT).
- `adb.ts`: AVD root resolution used `$HOME` + `startsWith("/")`
(rejects `C:\…`); now `os.homedir()` + `path.isAbsolute()`.
- **`boot-device`**: the emulator was spawned `detached` with
`stdio:"ignore"`; on Windows a detached emulator with NUL stdout/stderr
never reaches `sys.boot_completed` (verified on `windows-latest`: the
identical flag set boots fine with real handles, hangs with NUL).
Redirect the detached emulator's output to a temp log file on win32.
POSIX keeps `"ignore"`.

**Tests** — cross-platform unit coverage for `commandOnPath` (both
branches), win32 `.exe` Android resolution, win32 `.exe`
simulator-server resolution, and a win32 AVD drive-path case; dep-gate
tests now mock at the `commandOnPath` boundary so they're
platform-agnostic.

**CI** — `windows-e2e.yml` (`windows-chromium` on `windows-latest`):
runs the resolver/dep unit tests natively on Windows, confirms `adb.exe`
resolution via the SDK root, then drives a **real headless Chrome over
CDP** through the tool-server (discover → screenshot → describe → tap →
observe the DOM mutation). Flow in `scripts/ci/windows-chromium-e2e.mjs`
(also runnable locally / in Docker). The `simulator-server.exe` build +
the full **Android-emulator E2E on Windows** live in radon CI (the
binary's source is the private radon repo).

## Verification (all green)
- **Chromium control plane on real Windows** (`windows-latest`): `✓
discovered chromium-cdp-9222 → ✓ screenshot → ✓ describe → ✓ gesture-tap
→ ✓ DOM mutation observed`. Resolver/dep unit tests run natively;
`adb.exe` resolves via the SDK root (it's *not* on PATH on the runner —
the gap the resolver closes).
- **Android emulator on real Windows** (radon#134, `windows-latest` /
WHPX): the real Argent path — `boot-device` → `{ booted: true }` →
screenshot via `simulator-server.exe` → gesture-tap — all green. (The
boot-device fix above is what makes the detached emulator reach
`boot_completed`.)
- **Local clean-room E2E**: the Chromium flow runs green in a headless
Linux Docker container (fresh `npm ci`, no host pollution).
- **No regressions**: full tool-server suite (1624) +
native-devtools-ios (16); ESLint/Prettier/typecheck clean; **Wayland
E2E** (Linux AVD boot) and **Vega VVD E2E** still pass (they exercise
the changed `adb.ts` / `boot-device.ts` / `vega-cli.ts` paths).

## Notes
Android-emulator E2E needs WHPX nested virtualization — confirmed
available on the current hosted `windows-latest` image (Server 2025);
the emulator boots hardware-accelerated in ~3 min.

---------

Co-authored-by: Ignacy Łątka <claude-hubert.gancarczyk@swmansion.com>
Co-authored-by: Ignacy Łątka <latekvo@users.noreply.github.com>
2026-07-22 13:53:32 +02:00

156 lines
6.8 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Downloads the signed simulator-server (argent variant) for every supported
# host platform from simulator-server-releases, into platform-keyed
# subdirectories under packages/native-devtools-ios/bin/. The bundle step
# (packages/argent/scripts/bundle-tools.cjs) and the runtime resolver in
# @argent/native-devtools-ios both expect this layout.
#
# Usage: ./scripts/download-simulator-server.sh [release-tag]
# release-tag Optional tag to download from. Defaults to radon-main.
#
# Requires:
# - gh CLI, authenticated (`gh auth login` or `GH_TOKEN` env var). The release
# repo is public but `gh release download` still requires authentication.
REPO="software-mansion-labs/simulator-server-releases"
TAG="${1:-radon-main}"
DEST_DIR="packages/native-devtools-ios/bin"
# release-asset-name → host platform key. Asset names follow the upstream
# build matrix (`simulator-server-argent-{macos,linux,linux-arm64,windows.exe}`);
# the keys mirror hostPlatformKey() in @argent/native-devtools-ios
# (process.platform, except "linux-arm64" on arm64 Linux) so the resolver can
# look up by host. The Windows asset keeps its `.exe` extension end-to-end —
# the release asset, the local copy, and what the resolver/dispatcher spawn.
declare -a TARGETS=(
"simulator-server-argent-macos:darwin"
"simulator-server-argent-linux:linux"
"simulator-server-argent-linux-arm64:linux-arm64"
"simulator-server-argent-windows.exe:win32"
)
mkdir -p "${DEST_DIR}"
for entry in "${TARGETS[@]}"; do
ASSET_NAME="${entry%%:*}"
PLATFORM="${entry##*:}"
PLATFORM_DIR="${DEST_DIR}/${PLATFORM}"
# Windows ships a PE `.exe`; every other host an extensionless binary. The
# resolver (simulatorServerBinaryName()) and the dispatcher pick the same
# name by host, so the on-disk copy must match.
if [[ "${PLATFORM}" == "win32" ]]; then
BIN_BASENAME="simulator-server.exe"
else
BIN_BASENAME="simulator-server"
fi
# Purge then recreate the platform dir before each download so a previous
# run's stale binary can't ship if THIS run's download fails. Without this,
# the failure branch below would print a warning and continue, leaving the
# stale binary in place — and the publish workflow would silently package
# an outdated artifact.
rm -rf "${PLATFORM_DIR}"
mkdir -p "${PLATFORM_DIR}"
echo "Downloading ${ASSET_NAME} → ${PLATFORM_DIR}/${BIN_BASENAME}"
# Tolerate missing assets so the script keeps working for macOS-only
# consumers when the Linux artifact lags behind a release. Capture gh's
# stderr and print it on failure so "not authenticated" vs "asset missing"
# vs "rate-limited" stays distinguishable (the prior `2>/dev/null` masked
# all three).
GH_STDERR="$(mktemp)"
if ! gh release download "${TAG}" \
--repo "${REPO}" \
--pattern "${ASSET_NAME}" \
--dir "${PLATFORM_DIR}" \
--clobber 2>"${GH_STDERR}"; then
GH_MSG=$(<"${GH_STDERR}")
rm -f "${GH_STDERR}"
echo " ⚠ ${ASSET_NAME} not downloaded — skipping (binary won't be available on ${PLATFORM} hosts)"
[[ -n "${GH_MSG}" ]] && printf ' gh: %s\n' "${GH_MSG//$'\n'/$'\n gh: '}"
# PLATFORM_DIR is empty because we purged it above, so this rmdir
# succeeds and the dir disappears — keeping the inventory clean.
rmdir "${PLATFORM_DIR}" 2>/dev/null || true
continue
fi
rm -f "${GH_STDERR}"
mv "${PLATFORM_DIR}/${ASSET_NAME}" "${PLATFORM_DIR}/${BIN_BASENAME}"
chmod +x "${PLATFORM_DIR}/${BIN_BASENAME}"
# Architecture sanity check: a wrong-arch binary in a platform dir is worse
# than a missing one — the resolver would happily pick it and the user gets
# an ENOEXEC at spawn time with no hint of the root cause. Fail hard here
# (unlike the missing-asset case above) because a present-but-mislabeled
# asset is an upstream packaging bug that must not ship.
if command -v file >/dev/null 2>&1; then
DESC="$(file -b "${PLATFORM_DIR}/${BIN_BASENAME}")"
case "${PLATFORM}" in
darwin) EXPECT="Mach-O universal" ;;
linux) EXPECT="ELF 64-bit.*x86-64" ;;
linux-arm64) EXPECT="ELF 64-bit.*aarch64" ;;
win32) EXPECT="PE32\+.*x86-64" ;;
*) EXPECT="" ;;
esac
if [[ -n "${EXPECT}" ]] && ! [[ "${DESC}" =~ ${EXPECT} ]]; then
echo "✗ ${ASSET_NAME} has the wrong architecture for ${PLATFORM}:" >&2
echo " got: ${DESC}" >&2
echo " expected: ${EXPECT}" >&2
exit 1
fi
echo " ✓ arch ok: ${DESC}"
fi
done
echo ""
echo "Downloaded simulator-server binaries:"
find "${DEST_DIR}" \( -name simulator-server -o -name 'simulator-server.exe' \) -type f -exec ls -la {} \;
# Physical-Android-device support: the simulator-server `android_device`
# controller pushes the screen-sharing agent (a host-independent .jar + a
# per-ABI .so) to the phone over adb, resolving them at runtime from
# `resources/android/` relative to its working directory — which the blueprint
# sets to the binary's own platform dir. The agent payload runs on the phone,
# not the host, so a single release tarball serves every host platform; extract
# a copy next to each downloaded binary. Tolerate a missing asset the same way
# as a missing binary so macOS/Linux-only consumers and older releases (before
# the agent was published) still work — physical-device support just won't be
# available until the asset is present.
AGENT_ASSET="screen-sharing-agent.tar.gz"
AGENT_TMP="$(mktemp -d)"
echo ""
echo "Downloading ${AGENT_ASSET} (Android physical-device screen-sharing agent)"
GH_STDERR="$(mktemp)"
if gh release download "${TAG}" \
--repo "${REPO}" \
--pattern "${AGENT_ASSET}" \
--dir "${AGENT_TMP}" \
--clobber 2>"${GH_STDERR}"; then
rm -f "${GH_STDERR}"
# Extract once into every platform dir that actually has a binary, so the
# agent sits at <platform>/resources/android/ next to simulator-server.
while IFS= read -r bin; do
plat_dir="$(dirname "${bin}")"
res_dir="${plat_dir}/resources/android"
rm -rf "${res_dir}"
mkdir -p "${res_dir}"
tar -xzf "${AGENT_TMP}/${AGENT_ASSET}" -C "${res_dir}"
echo " ✓ screen-sharing agent → ${res_dir}"
done < <(find "${DEST_DIR}" \( -name simulator-server -o -name 'simulator-server.exe' \) -type f)
else
GH_MSG=$(<"${GH_STDERR}")
rm -f "${GH_STDERR}"
echo " ⚠ ${AGENT_ASSET} not downloaded — physical Android device support will be unavailable"
[[ -n "${GH_MSG}" ]] && printf ' gh: %s\n' "${GH_MSG//$'\n'/$'\n gh: '}"
fi
rm -rf "${AGENT_TMP}"
# Only the macOS binary is signed and codesignable; the Linux ELF doesn't
# carry an Apple signature, and `codesign` would noisily fail on it.
if command -v codesign &>/dev/null && [[ -f "${DEST_DIR}/darwin/simulator-server" ]]; then
codesign -dvv "${DEST_DIR}/darwin/simulator-server" 2>&1 \
|| echo "Warning: macOS signature verification failed"
fi