Skip to content

Commit 65a6a7e

Browse files
committed
fix: support android emulators on linux and windows
1 parent 59d08f9 commit 65a6a7e

16 files changed

Lines changed: 653 additions & 109 deletions

File tree

.github/workflows/ci.yml

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ jobs:
8080
- name: Test integration harness helpers
8181
run: npm run test:integration-harness
8282

83+
- name: Test workflow and CLI packaging guards
84+
run: npm run test:github-actions
85+
8386
- name: Typecheck client
8487
run: npm run --prefix packages/client typecheck
8588

@@ -289,12 +292,16 @@ jobs:
289292
SIMDECK_INTEGRATION_DEVICE_TYPE: iPhone SE (3rd generation)
290293

291294
integration-android:
292-
name: Android emulator integration
293-
runs-on: ubuntu-latest
294-
timeout-minutes: 35
295+
name: Android emulator integration (${{ matrix.os }})
296+
runs-on: ${{ matrix.os }}
297+
timeout-minutes: 50
295298
needs:
296299
- client
297300
- packages
301+
strategy:
302+
fail-fast: false
303+
matrix:
304+
os: [ubuntu-latest, windows-latest]
298305

299306
steps:
300307
- uses: actions/checkout@v4
@@ -308,6 +315,7 @@ jobs:
308315
- uses: dtolnay/rust-toolchain@stable
309316

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

319-
- name: Build Linux Android integration artifacts
327+
- name: Build Android integration artifacts
320328
run: |
321329
npm run build:cli
322330
npm run build:simdeck-test
323331
324-
- name: Android emulator integration tests
332+
- name: Android emulator integration tests (Linux)
333+
if: runner.os == 'Linux'
325334
uses: reactivecircus/android-emulator-runner@v2
326335
with:
327336
api-level: 35
@@ -330,8 +339,99 @@ jobs:
330339
profile: pixel_6
331340
avd-name: SimDeck_Pixel_CI
332341
disable-animations: true
342+
emulator-options: -no-window -no-audio -no-boot-anim -no-snapshot -gpu swiftshader_indirect -grpc 8554
333343
script: npm run test:integration:android
334344
env:
335345
SIMDECK_INTEGRATION_ANDROID_AVD: SimDeck_Pixel_CI
336346
SIMDECK_INTEGRATION_REQUIRE_RUNNING_ANDROID: "1"
337347
SIMDECK_INTEGRATION_VERBOSE: "1"
348+
349+
- name: Create and boot Android emulator (Windows)
350+
if: runner.os == 'Windows'
351+
shell: pwsh
352+
run: |
353+
$ErrorActionPreference = "Stop"
354+
$sdk = $env:ANDROID_HOME
355+
if (-not $sdk) {
356+
$sdk = $env:ANDROID_SDK_ROOT
357+
}
358+
if (-not $sdk) {
359+
$sdk = Join-Path $env:LOCALAPPDATA "Android\Sdk"
360+
}
361+
$env:ANDROID_HOME = $sdk
362+
$env:ANDROID_SDK_ROOT = $sdk
363+
$cmdlineTools = Get-ChildItem -Path (Join-Path $sdk "cmdline-tools") -Directory -ErrorAction SilentlyContinue |
364+
Sort-Object Name -Descending |
365+
Select-Object -First 1
366+
$toolsBin = if (Test-Path (Join-Path $sdk "cmdline-tools\latest\bin")) {
367+
Join-Path $sdk "cmdline-tools\latest\bin"
368+
} elseif ($cmdlineTools) {
369+
Join-Path $cmdlineTools.FullName "bin"
370+
} else {
371+
Join-Path $sdk "tools\bin"
372+
}
373+
$sdkmanager = Join-Path $toolsBin "sdkmanager.bat"
374+
$avdmanager = Join-Path $toolsBin "avdmanager.bat"
375+
$adb = Join-Path $sdk "platform-tools\adb.exe"
376+
$emulator = Join-Path $sdk "emulator\emulator.exe"
377+
378+
& $sdkmanager --install "platform-tools" "emulator" "platforms;android-35" "system-images;android-35;google_apis;x86_64"
379+
1..50 | ForEach-Object { "y" } | & $sdkmanager --licenses
380+
"no" | & $avdmanager create avd --force --name SimDeck_Pixel_CI --package "system-images;android-35;google_apis;x86_64" --device "pixel_6"
381+
382+
$args = @(
383+
"-avd", "SimDeck_Pixel_CI",
384+
"-no-window",
385+
"-no-audio",
386+
"-no-boot-anim",
387+
"-no-snapshot",
388+
"-gpu", "swiftshader_indirect",
389+
"-grpc", "8554"
390+
)
391+
$process = Start-Process -FilePath $emulator -ArgumentList $args -PassThru
392+
$process.Id | Out-File -FilePath emulator.pid -Encoding ascii
393+
& $adb wait-for-device
394+
$deadline = (Get-Date).AddMinutes(8)
395+
do {
396+
if ($process.HasExited) {
397+
throw "Android emulator exited early with code $($process.ExitCode)."
398+
}
399+
$booted = (& $adb shell getprop sys.boot_completed).Trim()
400+
if ($booted -eq "1") {
401+
break
402+
}
403+
Start-Sleep -Seconds 5
404+
} while ((Get-Date) -lt $deadline)
405+
if ($booted -ne "1") {
406+
& $adb devices
407+
throw "Android emulator did not boot before the timeout."
408+
}
409+
& $adb shell settings put global window_animation_scale 0
410+
& $adb shell settings put global transition_animation_scale 0
411+
& $adb shell settings put global animator_duration_scale 0
412+
413+
- name: Android emulator integration tests (Windows)
414+
if: runner.os == 'Windows'
415+
run: npm run test:integration:android
416+
env:
417+
SIMDECK_INTEGRATION_ANDROID_AVD: SimDeck_Pixel_CI
418+
SIMDECK_INTEGRATION_REQUIRE_RUNNING_ANDROID: "1"
419+
SIMDECK_INTEGRATION_VERBOSE: "1"
420+
421+
- name: Stop Android emulator (Windows)
422+
if: always() && runner.os == 'Windows'
423+
shell: pwsh
424+
run: |
425+
$sdk = $env:ANDROID_HOME
426+
if (-not $sdk) {
427+
$sdk = $env:ANDROID_SDK_ROOT
428+
}
429+
if ($sdk) {
430+
$adb = Join-Path $sdk "platform-tools\adb.exe"
431+
if (Test-Path $adb) {
432+
& $adb emu kill
433+
}
434+
}
435+
if (Test-Path emulator.pid) {
436+
Stop-Process -Id (Get-Content emulator.pid) -Force -ErrorAction SilentlyContinue
437+
}

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"build/simdeck-bin-darwin-x64",
2424
"build/simdeck-bin-linux-arm64",
2525
"build/simdeck-bin-linux-x64",
26+
"build/simdeck-bin-win32-x64.exe",
2627
"packages/client/dist/",
2728
"packages/simdeck-test/dist/"
2829
],
@@ -38,7 +39,8 @@
3839
},
3940
"os": [
4041
"darwin",
41-
"linux"
42+
"linux",
43+
"win32"
4244
],
4345
"cpu": [
4446
"arm64",
@@ -48,7 +50,7 @@
4850
"access": "public"
4951
},
5052
"scripts": {
51-
"build:cli": "scripts/build-cli.sh",
53+
"build:cli": "node scripts/build-cli.mjs",
5254
"build:client": "scripts/build-client.sh",
5355
"build:app": "npm run build:cli && npm run build:client",
5456
"build:inspectors": "npm run build:nativescript-inspector && npm run build:react-native-inspector",

packages/app-deeplink-pages/README.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,18 @@ The iOS app's `StudioLinkResolver` handles these paths on `app.simdeck.sh`:
6767

6868
Query params:
6969

70-
| param | meaning |
71-
| ---------------------------------- | ---------------------------------------------------------------- |
72-
| `host` (required) | Where SimDeck is reachable from the phone (LAN IP, tunnel host). |
73-
| `port` | Port (defaults vary). |
74-
| `scheme` | `http` or `https`. Default `http`. |
75-
| `udid` / `device` | Auto-select this simulator and auto-start streaming. |
76-
| `serverId` / `sid` / `s` | Hash identifying which SimDeck instance this is. |
77-
| `hostId` / `hid` | Stable host identifier (machine-level). |
78-
| `hostName` / `hname` | Human-readable host name for the picker. |
79-
| `serverKind` / `kind` | `launchAgent`, `foreground`, `standalone`, … |
80-
| `token` / `simdeckToken` | Pre-shared access token (optional). |
81-
| `code` / `pairingCode` (on /pair) | Pairing code for first-time link-up. |
70+
| param | meaning |
71+
| --------------------------------- | ---------------------------------------------------------------- |
72+
| `host` (required) | Where SimDeck is reachable from the phone (LAN IP, tunnel host). |
73+
| `port` | Port (defaults vary). |
74+
| `scheme` | `http` or `https`. Default `http`. |
75+
| `udid` / `device` | Auto-select this simulator and auto-start streaming. |
76+
| `serverId` / `sid` / `s` | Hash identifying which SimDeck instance this is. |
77+
| `hostId` / `hid` | Stable host identifier (machine-level). |
78+
| `hostName` / `hname` | Human-readable host name for the picker. |
79+
| `serverKind` / `kind` | `launchAgent`, `foreground`, `standalone`, … |
80+
| `token` / `simdeckToken` | Pre-shared access token (optional). |
81+
| `code` / `pairingCode` (on /pair) | Pairing code for first-time link-up. |
8282

8383
The Universal Link `https://app.simdeck.sh/open?...` and the custom-scheme
8484
`simdeck://open?...` carry the same query string and are interchangeable.

packages/app-deeplink-pages/public/index.html

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
<html lang="en">
33
<head>
44
<meta charset="utf-8" />
5-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
5+
<meta
6+
name="viewport"
7+
content="width=device-width, initial-scale=1, viewport-fit=cover"
8+
/>
69
<meta name="color-scheme" content="light dark" />
710
<title>SimDeck Launchpad</title>
811
<meta
@@ -17,13 +20,17 @@
1720
<main class="page" data-state="probing">
1821
<header class="page-header">
1922
<a class="brand" href="/">
20-
<img class="brand-mark" src="/simdeck.png" alt="" width="32" height="32" />
23+
<img
24+
class="brand-mark"
25+
src="/simdeck.png"
26+
alt=""
27+
width="32"
28+
height="32"
29+
/>
2130
<span class="brand-name">SimDeck</span>
2231
<span class="brand-tag">Launchpad</span>
2332
</a>
24-
<p class="tagline">
25-
Remote control iOS simulators from any device.
26-
</p>
33+
<p class="tagline">Remote control iOS simulators from any device.</p>
2734
</header>
2835

2936
<section class="card">
@@ -54,18 +61,30 @@ <h2>Local Server</h2>
5461
placeholder="10.0.0.55:4310 or https://tunnel.example.com"
5562
/>
5663
</div>
57-
<button id="manual-connect" type="button" class="primary">Connect</button>
64+
<button id="manual-connect" type="button" class="primary">
65+
Connect
66+
</button>
5867
</details>
5968
</div>
6069
</section>
6170

6271
<footer class="page-footer">
6372
<p>
6473
Need the iOS app?
65-
<a href="https://apps.apple.com/app/simdeck-studio/id6770182703" target="_blank" rel="noopener">Get it on the App Store</a>.
74+
<a
75+
href="https://apps.apple.com/app/simdeck-studio/id6770182703"
76+
target="_blank"
77+
rel="noopener"
78+
>Get it on the App Store</a
79+
>.
6680
</p>
6781
<p class="muted">
68-
<a href="https://github.com/NativeScript/SimDeck" target="_blank" rel="noopener">SimDeck</a>
82+
<a
83+
href="https://github.com/NativeScript/SimDeck"
84+
target="_blank"
85+
rel="noopener"
86+
>SimDeck</a
87+
>
6988
is open source.
7089
</p>
7190
</footer>

packages/app-deeplink-pages/public/styles.css

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,8 @@ body {
4040
background: var(--bg);
4141
color: var(--fg);
4242
font-family:
43-
-apple-system,
44-
BlinkMacSystemFont,
45-
"SF Pro Text",
46-
"Segoe UI",
47-
Inter,
48-
system-ui,
49-
sans-serif;
43+
-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Inter,
44+
system-ui, sans-serif;
5045
-webkit-font-smoothing: antialiased;
5146
}
5247

@@ -61,12 +56,7 @@ a:hover {
6156

6257
code {
6358
font-family:
64-
ui-monospace,
65-
SFMono-Regular,
66-
"SF Mono",
67-
Menlo,
68-
Consolas,
69-
monospace;
59+
ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
7060
background: rgba(127, 127, 127, 0.14);
7161
padding: 1px 6px;
7262
border-radius: 5px;

packages/cli/bin/simdeck.mjs

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ const childArgs = process.argv.slice(2);
1414
const isServiceRun = childArgs[0] === "service" && childArgs[1] === "run";
1515

1616
if (!binaryPath) {
17-
console.error("simdeck only supports macOS and Linux on arm64/x64.");
17+
console.error(
18+
"simdeck only supports macOS, Linux, and Windows on arm64/x64.",
19+
);
1820
process.exit(1);
1921
}
2022

@@ -42,31 +44,31 @@ function findPackageRoot(startDir) {
4244
function resolveBinaryPath(rootDir) {
4345
const platform = process.platform;
4446
const arch = process.arch;
45-
const suffixByHost = {
46-
"darwin-arm64": "darwin-arm64",
47-
"darwin-x64": "darwin-x64",
48-
"linux-arm64": "linux-arm64",
49-
"linux-x64": "linux-x64",
47+
const binaryByHost = {
48+
"darwin-arm64": "simdeck-bin-darwin-arm64",
49+
"darwin-x64": "simdeck-bin-darwin-x64",
50+
"linux-arm64": "simdeck-bin-linux-arm64",
51+
"linux-x64": "simdeck-bin-linux-x64",
52+
"win32-x64": "simdeck-bin-win32-x64.exe",
5053
};
5154

52-
const suffix = suffixByHost[`${platform}-${arch}`];
53-
if (!suffix) {
55+
const binary = binaryByHost[`${platform}-${arch}`];
56+
if (!binary) {
5457
return null;
5558
}
5659

57-
const platformBinaryPath = path.join(
58-
rootDir,
59-
"build",
60-
`simdeck-bin-${suffix}`,
61-
);
60+
const platformBinaryPath = path.join(rootDir, "build", binary);
6261
if (existsSync(platformBinaryPath)) {
6362
return platformBinaryPath;
6463
}
6564

66-
const fallbackBinaryPath = path.join(rootDir, "build", "simdeck-bin");
67-
return existsSync(fallbackBinaryPath)
68-
? fallbackBinaryPath
69-
: platformBinaryPath;
65+
for (const fallback of ["simdeck-bin.exe", "simdeck-bin"]) {
66+
const fallbackBinaryPath = path.join(rootDir, "build", fallback);
67+
if (existsSync(fallbackBinaryPath)) {
68+
return fallbackBinaryPath;
69+
}
70+
}
71+
return platformBinaryPath;
7072
}
7173

7274
let child;

packages/server/build.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ fn main() {
99
println!("cargo:rerun-if-changed={}", stub.display());
1010
cc::Build::new()
1111
.file(&stub)
12-
.flag("-Wall")
13-
.flag("-Wextra")
12+
.flag_if_supported("-Wall")
13+
.flag_if_supported("-Wextra")
1414
.compile("xcw_native_bridge");
1515
return;
1616
}
@@ -39,8 +39,8 @@ fn main() {
3939
.include(&native)
4040
.flag("-fobjc-arc")
4141
.flag("-fmodules")
42-
.flag("-Wall")
43-
.flag("-Wextra");
42+
.flag_if_supported("-Wall")
43+
.flag_if_supported("-Wextra");
4444
apply_pkg_config_compile_flags(&mut build, &x264_flags);
4545

4646
for file in &files {

0 commit comments

Comments
 (0)