Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4872279
feat: add x64 and linux host targeting for simdeck launcher
Copilot May 29, 2026
ead8d82
fix: select platform-specific simdeck binary at runtime
Copilot May 29, 2026
16b2f28
fix: support android emulators on linux and windows
DjDeveloperr May 31, 2026
a684a8d
ci: harden windows android emulator setup
DjDeveloperr May 31, 2026
fc530ba
ci: make windows android sdk setup noninteractive
DjDeveloperr May 31, 2026
19214fa
ci: diagnose windows android emulator boot
DjDeveloperr May 31, 2026
496c7ea
ci: use windows emulator acceleration when available
DjDeveloperr May 31, 2026
bbce18f
ci: use atd image for windows android smoke
DjDeveloperr May 31, 2026
6a2e0d8
fix: disable vulkan for windows android emulators
DjDeveloperr May 31, 2026
569ccc6
ci: wait for online windows android emulator
DjDeveloperr May 31, 2026
e993d57
fix: resolve android integration root on windows
DjDeveloperr May 31, 2026
1aa9c5e
fix: detect single running android emulator
DjDeveloperr May 31, 2026
8be34b4
ci: keep windows android emulator alive for tests
DjDeveloperr May 31, 2026
9aada83
fix: launch android settings by component
DjDeveloperr May 31, 2026
8aebf00
test: discover android launch target on ci
DjDeveloperr May 31, 2026
962aac3
fix: tolerate windows temp cleanup races
DjDeveloperr May 31, 2026
ee894fc
test: retry slow native agent snapshots
DjDeveloperr May 31, 2026
a95b989
test: recover webrtc fixture launch timeouts
DjDeveloperr May 31, 2026
9f264ad
ci: align webrtc thresholds with main
DjDeveloperr May 31, 2026
409db2f
chore: merge main into android ci branch
DjDeveloperr May 31, 2026
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
193 changes: 188 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ jobs:
- name: Test integration harness helpers
run: npm run test:integration-harness

- name: Test workflow and CLI packaging guards
run: npm run test:github-actions

- name: Typecheck client
run: npm run --prefix packages/client typecheck

Expand Down Expand Up @@ -289,12 +292,16 @@ jobs:
SIMDECK_INTEGRATION_DEVICE_TYPE: iPhone SE (3rd generation)

integration-android:
name: Android emulator integration
runs-on: ubuntu-latest
timeout-minutes: 35
name: Android emulator integration (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 65
needs:
- client
- packages
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]

steps:
- uses: actions/checkout@v4
Expand All @@ -308,6 +315,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable

- name: Enable KVM access
if: runner.os == 'Linux'
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
Expand All @@ -316,12 +324,13 @@ jobs:
- name: Install root dependencies
run: npm ci --ignore-scripts --force

- name: Build Linux Android integration artifacts
- name: Build Android integration artifacts
run: |
npm run build:cli
npm run build:simdeck-test

- name: Android emulator integration tests
- name: Android emulator integration tests (Linux)
if: runner.os == 'Linux'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
Expand All @@ -330,8 +339,182 @@ jobs:
profile: pixel_6
avd-name: SimDeck_Pixel_CI
disable-animations: true
emulator-options: -no-window -no-audio -no-boot-anim -no-snapshot -gpu swiftshader_indirect -grpc 8554
script: npm run test:integration:android
env:
SIMDECK_INTEGRATION_ANDROID_AVD: SimDeck_Pixel_CI
SIMDECK_INTEGRATION_REQUIRE_RUNNING_ANDROID: "1"
SIMDECK_INTEGRATION_VERBOSE: "1"

- name: Create, boot, and test Android emulator (Windows)
if: runner.os == 'Windows'
shell: pwsh
timeout-minutes: 45
run: |
$ErrorActionPreference = "Stop"
$sdk = $env:ANDROID_HOME
if (-not $sdk) {
$sdk = $env:ANDROID_SDK_ROOT
}
if (-not $sdk) {
$sdk = Join-Path $env:LOCALAPPDATA "Android\Sdk"
}
$env:ANDROID_HOME = $sdk
$env:ANDROID_SDK_ROOT = $sdk
$cmdlineTools = Get-ChildItem -Path (Join-Path $sdk "cmdline-tools") -Directory -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
$toolsBin = if (Test-Path (Join-Path $sdk "cmdline-tools\latest\bin")) {
Join-Path $sdk "cmdline-tools\latest\bin"
} elseif ($cmdlineTools) {
Join-Path $cmdlineTools.FullName "bin"
} else {
Join-Path $sdk "tools\bin"
}
$sdkmanager = Join-Path $toolsBin "sdkmanager.bat"
$avdmanager = Join-Path $toolsBin "avdmanager.bat"
$adb = Join-Path $sdk "platform-tools\adb.exe"
$emulator = Join-Path $sdk "emulator\emulator.exe"
$windowsApi = "35"
$windowsPlatform = "platforms;android-$windowsApi"
$windowsSystemImage = "system-images;android-$windowsApi;google_atd;x86_64"

$yesFile = Join-Path $env:RUNNER_TEMP "android-sdk-yes.txt"
1..200 | ForEach-Object { "y" } | Set-Content -Path $yesFile -Encoding ascii
$noFile = Join-Path $env:RUNNER_TEMP "android-avd-no.txt"
"no" | Set-Content -Path $noFile -Encoding ascii

function Invoke-AndroidToolWithInput($tool, $arguments, $inputFile) {
$line = "`"$tool`" $arguments < `"$inputFile`""
Write-Host "cmd /c $line"
cmd /c $line
if ($LASTEXITCODE -ne 0) {
throw "$tool $arguments failed with exit code $LASTEXITCODE."
}
}

Write-Host "Accepting Android SDK licenses"
Invoke-AndroidToolWithInput $sdkmanager "--licenses" $yesFile
Write-Host "Installing Android SDK emulator packages"
Invoke-AndroidToolWithInput $sdkmanager "--install `"platform-tools`" `"emulator`" `"$windowsPlatform`" `"$windowsSystemImage`"" $yesFile
Write-Host "Checking Android emulator acceleration"
& $emulator -accel-check
$accelSupported = $LASTEXITCODE -eq 0
if (-not $accelSupported) {
Write-Host "Hosted Windows runner did not report VM acceleration; forcing software acceleration for the CI smoke emulator."
}
Write-Host "Creating Android AVD"
Invoke-AndroidToolWithInput $avdmanager "create avd --force --name SimDeck_Pixel_CI --package `"$windowsSystemImage`" --device `"pixel_6`"" $noFile

$stdout = Join-Path $env:RUNNER_TEMP "simdeck-android-emulator.out.log"
$stderr = Join-Path $env:RUNNER_TEMP "simdeck-android-emulator.err.log"
$serial = "emulator-5554"
$args = @(
"-avd", "SimDeck_Pixel_CI",
"-qt-hide-window",
"-no-audio",
"-no-boot-anim",
"-no-snapshot-load",
"-no-snapshot-save",
"-wipe-data",
"-gpu", "swiftshader_indirect",
"-feature", "-Vulkan",
"-grpc", "8554",
"-port", "5554",
"-no-metrics",
"-skip-adb-auth",
"-camera-back", "none",
"-camera-front", "none",
"-cores", "2",
"-memory", "2048",
"-verbose"
)
if ($accelSupported) {
$args += @("-accel", "on")
} else {
$args += @("-accel", "off")
}
function Write-EmulatorDiagnostics {
Write-Host "adb devices:"
& $adb devices -l
if (Test-Path $stdout) {
Write-Host "emulator stdout tail:"
Get-Content $stdout -Tail 80
}
if (Test-Path $stderr) {
Write-Host "emulator stderr tail:"
Get-Content $stderr -Tail 120
}
}
Write-Host "Starting Android emulator"
$process = Start-Process -FilePath $emulator -ArgumentList $args -PassThru -RedirectStandardOutput $stdout -RedirectStandardError $stderr
$process.Id | Out-File -FilePath emulator.pid -Encoding ascii
$deviceDeadline = (Get-Date).AddMinutes(10)
$deviceSeen = $false
do {
if ($process.HasExited) {
Write-EmulatorDiagnostics
throw "Android emulator exited early with code $($process.ExitCode)."
}
$devices = (& $adb devices)
if ($devices -match "$serial\s+device") {
$deviceSeen = $true
break
}
Start-Sleep -Seconds 5
} while ((Get-Date) -lt $deviceDeadline)
if (-not $deviceSeen) {
Write-EmulatorDiagnostics
throw "Android emulator did not appear in adb before the timeout."
}
$deadline = (Get-Date).AddMinutes(20)
do {
if ($process.HasExited) {
Write-EmulatorDiagnostics
throw "Android emulator exited early with code $($process.ExitCode)."
}
$booted = (& $adb -s $serial shell getprop sys.boot_completed 2>$null | Out-String).Trim()
if ($booted -eq "1") {
break
}
Start-Sleep -Seconds 5
} while ((Get-Date) -lt $deadline)
if ($booted -ne "1") {
Write-EmulatorDiagnostics
throw "Android emulator did not boot before the timeout."
}
& $adb -s $serial shell settings put global window_animation_scale 0
& $adb -s $serial shell settings put global transition_animation_scale 0
& $adb -s $serial shell settings put global animator_duration_scale 0
$env:SIMDECK_INTEGRATION_ANDROID_AVD = "SimDeck_Pixel_CI"
$env:SIMDECK_INTEGRATION_REQUIRE_RUNNING_ANDROID = "1"
$env:SIMDECK_INTEGRATION_VERBOSE = "1"
npm run test:integration:android
$testExitCode = $LASTEXITCODE
if ($testExitCode -ne 0) {
Write-EmulatorDiagnostics
throw "Android integration tests failed with exit code $testExitCode."
}

- name: Stop Android emulator (Windows)
if: always() && runner.os == 'Windows'
shell: pwsh
run: |
$sdk = $env:ANDROID_HOME
if (-not $sdk) {
$sdk = $env:ANDROID_SDK_ROOT
}
if ($sdk) {
$adb = Join-Path $sdk "platform-tools\adb.exe"
if (Test-Path $adb) {
& $adb emu kill
if ($LASTEXITCODE -ne 0) {
Write-Host "No Android emulator accepted adb emu kill."
$global:LASTEXITCODE = 0
}
}
}
if (Test-Path emulator.pid) {
Stop-Process -Id (Get-Content emulator.pid) -Force -ErrorAction SilentlyContinue
}
exit 0
27 changes: 19 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ jobs:
if: ${{ steps.meta.outputs.kind == 'npm-cli' }}
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
targets: aarch64-apple-darwin,x86_64-apple-darwin

- name: Install native build dependencies
if: ${{ steps.meta.outputs.kind == 'npm-cli' }}
Expand Down Expand Up @@ -265,21 +265,34 @@ jobs:

# ---------- Build (kind-specific) ----------

- name: Build root simdeck (arm64 native binary + client + simdeck-test)
- name: Build root simdeck (macOS arm64+x64 binaries + client + simdeck-test)
if: ${{ steps.meta.outputs.kind == 'npm-cli' }}
env:
SIMDECK_BUILD_TARGET: aarch64-apple-darwin
run: |
npm run build:cli
SIMDECK_BUILD_TARGET=aarch64-apple-darwin npm run build:cli
cp build/simdeck-bin build/simdeck-bin-darwin-arm64

SIMDECK_BUILD_TARGET=x86_64-apple-darwin npm run build:cli
cp build/simdeck-bin build/simdeck-bin-darwin-x64

lipo -create \
-output build/simdeck-bin \
build/simdeck-bin-darwin-arm64 \
build/simdeck-bin-darwin-x64

npm run build:client
npm run build:simdeck-test

- name: Verify CLI artifact is an arm64 Mach-O
- name: Verify CLI artifacts are macOS arm64+x64 binaries
if: ${{ steps.meta.outputs.kind == 'npm-cli' }}
run: |
test -x build/simdeck-bin
test -x build/simdeck-bin-darwin-arm64
test -x build/simdeck-bin-darwin-x64
file build/simdeck-bin
file build/simdeck-bin | grep -q 'arm64'
file build/simdeck-bin | grep -q 'x86_64'
file build/simdeck-bin-darwin-arm64 | grep -q 'arm64'
file build/simdeck-bin-darwin-x64 | grep -q 'x86_64'

# ---------- Apple codesign + notarize (root simdeck only) ----------

Expand Down Expand Up @@ -474,7 +487,6 @@ jobs:
NODE_AUTH_TOKEN: ""
DIST_TAG: ${{ steps.tag.outputs.dist_tag }}
PKG_DIR: ${{ steps.meta.outputs.dir }}
SIMDECK_BUILD_TARGET: aarch64-apple-darwin
run: |
set -euo pipefail
unset NODE_AUTH_TOKEN
Expand All @@ -493,7 +505,6 @@ jobs:
NODE_AUTH_TOKEN: ""
DIST_TAG: ${{ steps.tag.outputs.dist_tag }}
PKG_DIR: ${{ steps.meta.outputs.dir }}
SIMDECK_BUILD_TARGET: aarch64-apple-darwin
run: |
set -euo pipefail
unset NODE_AUTH_TOKEN
Expand Down
14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
"scripts/postinstall.mjs",
"build/simdeck-bin",
"build/camera/",
"build/simdeck-bin-darwin-arm64",
"build/simdeck-bin-darwin-x64",
"build/simdeck-bin-linux-arm64",
"build/simdeck-bin-linux-x64",
"build/simdeck-bin-win32-x64.exe",
"packages/client/dist/",
"packages/simdeck-test/dist/"
],
Expand All @@ -34,16 +39,19 @@
"node": ">=18"
},
"os": [
"darwin"
"darwin",
"linux",
"win32"
],
"cpu": [
"arm64"
"arm64",
"x64"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"build:cli": "scripts/build-cli.sh",
"build:cli": "node scripts/build-cli.mjs",
"build:client": "scripts/build-client.sh",
"build:app": "npm run build:cli && npm run build:client",
"build:inspectors": "npm run build:nativescript-inspector && npm run build:react-native-inspector",
Expand Down
38 changes: 35 additions & 3 deletions packages/cli/bin/simdeck.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ const RECOVERABLE_RESTART_EXIT_CODE = 75;

const launcherDir = path.dirname(fileURLToPath(import.meta.url));
const packageRoot = findPackageRoot(launcherDir);
const binaryPath = path.join(packageRoot, "build", "simdeck-bin");
const binaryPath = resolveBinaryPath(packageRoot);
const childArgs = process.argv.slice(2);
const isServiceRun = childArgs[0] === "service" && childArgs[1] === "run";

if (process.platform !== "darwin") {
console.error("simdeck only supports macOS.");
if (!binaryPath) {
console.error(
"simdeck only supports macOS, Linux, and Windows on arm64/x64.",
);
process.exit(1);
}

Expand All @@ -39,6 +41,36 @@ function findPackageRoot(startDir) {
}
}

function resolveBinaryPath(rootDir) {
const platform = process.platform;
const arch = process.arch;
const binaryByHost = {
"darwin-arm64": "simdeck-bin-darwin-arm64",
"darwin-x64": "simdeck-bin-darwin-x64",
"linux-arm64": "simdeck-bin-linux-arm64",
"linux-x64": "simdeck-bin-linux-x64",
"win32-x64": "simdeck-bin-win32-x64.exe",
};

const binary = binaryByHost[`${platform}-${arch}`];
if (!binary) {
return null;
}

const platformBinaryPath = path.join(rootDir, "build", binary);
if (existsSync(platformBinaryPath)) {
return platformBinaryPath;
}

for (const fallback of ["simdeck-bin.exe", "simdeck-bin"]) {
const fallbackBinaryPath = path.join(rootDir, "build", fallback);
if (existsSync(fallbackBinaryPath)) {
return fallbackBinaryPath;
}
}
return platformBinaryPath;
}

let child;
let terminating = false;

Expand Down
Loading
Loading