Skip to content

Commit b0a35de

Browse files
authored
Merge pull request #20 from callstackincubator/feature/turbo-substitution
feat: native module substitution
2 parents a7231d7 + 899ac6f commit b0a35de

75 files changed

Lines changed: 7178 additions & 1550 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
name: Boot iOS Simulator
2+
description: >
3+
Find, boot, and optionally install an app on an iOS simulator.
4+
When force=false (default), picks the closest available device
5+
matching the requested name/version.
6+
7+
inputs:
8+
device:
9+
description: 'Simulator device name (e.g. "iPhone 16 Pro")'
10+
required: true
11+
ios-version:
12+
description: 'Preferred iOS version (e.g. "18.4"). When force=false the closest available version is used.'
13+
required: false
14+
default: ''
15+
force:
16+
description: 'If true, fail when the exact device + version combo is not found.'
17+
required: false
18+
default: 'false'
19+
app-path:
20+
description: 'Path to .app bundle to install after boot. Skipped if empty.'
21+
required: false
22+
default: ''
23+
24+
outputs:
25+
udid:
26+
description: 'UDID of the booted simulator'
27+
value: ${{ steps.boot.outputs.udid }}
28+
ios-version:
29+
description: 'Actual iOS version of the booted simulator'
30+
value: ${{ steps.boot.outputs.ios-version }}
31+
32+
runs:
33+
using: composite
34+
steps:
35+
- name: Boot simulator
36+
id: boot
37+
uses: actions/github-script@v7
38+
env:
39+
INPUT_DEVICE: ${{ inputs.device }}
40+
INPUT_IOS_VERSION: ${{ inputs.ios-version }}
41+
INPUT_FORCE: ${{ inputs.force }}
42+
INPUT_APP_PATH: ${{ inputs.app-path }}
43+
with:
44+
script: |
45+
const { execSync } = require('child_process');
46+
47+
const device = process.env.INPUT_DEVICE;
48+
const wantVersion = process.env.INPUT_IOS_VERSION || '';
49+
const force = process.env.INPUT_FORCE === 'true';
50+
const appPath = process.env.INPUT_APP_PATH || '';
51+
52+
const raw = execSync('xcrun simctl list devices available --json', {
53+
encoding: 'utf-8',
54+
});
55+
const { devices } = JSON.parse(raw);
56+
57+
// Flatten into [{ name, udid, version }]
58+
const candidates = [];
59+
for (const [runtime, list] of Object.entries(devices)) {
60+
const m = runtime.match(/iOS[- ]([\d-]+)/);
61+
if (!m) continue;
62+
const version = m[1].replace(/-/g, '.');
63+
for (const d of list) {
64+
if (d.name === device && d.isAvailable) {
65+
candidates.push({ name: d.name, udid: d.udid, version });
66+
}
67+
}
68+
}
69+
70+
if (candidates.length === 0) {
71+
core.setFailed(`No available simulator found for "${device}"`);
72+
return;
73+
}
74+
75+
let pick;
76+
77+
if (wantVersion) {
78+
pick = candidates.find(c => c.version === wantVersion);
79+
if (!pick && force) {
80+
core.setFailed(
81+
`Exact match "${device}" (iOS ${wantVersion}) not found. ` +
82+
`Available: ${candidates.map(c => c.version).join(', ')}`
83+
);
84+
return;
85+
}
86+
if (!pick) {
87+
// Pick closest version by sorting by distance to requested
88+
const wanted = wantVersion.split('.').map(Number);
89+
candidates.sort((a, b) => {
90+
const va = a.version.split('.').map(Number);
91+
const vb = b.version.split('.').map(Number);
92+
const distA = Math.abs(va[0] - wanted[0]) * 1000 + Math.abs((va[1] || 0) - (wanted[1] || 0));
93+
const distB = Math.abs(vb[0] - wanted[0]) * 1000 + Math.abs((vb[1] || 0) - (wanted[1] || 0));
94+
return distA - distB;
95+
});
96+
pick = candidates[0];
97+
core.warning(
98+
`iOS ${wantVersion} not available for "${device}", ` +
99+
`using iOS ${pick.version} instead`
100+
);
101+
}
102+
} else {
103+
// No version preference — pick the latest
104+
candidates.sort((a, b) => {
105+
const va = a.version.split('.').map(Number);
106+
const vb = b.version.split('.').map(Number);
107+
return (vb[0] - va[0]) * 1000 + (vb[1] || 0) - (va[1] || 0);
108+
});
109+
pick = candidates[0];
110+
}
111+
112+
core.info(`Booting ${pick.name} (iOS ${pick.version}) — ${pick.udid}`);
113+
try {
114+
execSync(`xcrun simctl boot "${pick.udid}"`, { stdio: 'inherit' });
115+
} catch {
116+
core.info('Simulator already booted or boot returned non-zero (continuing)');
117+
}
118+
119+
core.info('Waiting for simulator to finish booting…');
120+
execSync(`xcrun simctl bootstatus "${pick.udid}" -b`, { stdio: 'inherit' });
121+
122+
if (appPath) {
123+
core.info(`Installing ${appPath}`);
124+
execSync(`xcrun simctl install "${pick.udid}" "${appPath}"`, {
125+
stdio: 'inherit',
126+
});
127+
}
128+
129+
core.setOutput('udid', pick.udid);
130+
core.setOutput('ios-version', pick.version);

.github/actions/setup/action.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Setup environment
2+
description: Install asdf tools and run bun install (repo must already be checked out)
3+
4+
inputs:
5+
java-version:
6+
description: 'JDK version to install (skipped if empty)'
7+
required: false
8+
default: ''
9+
10+
runs:
11+
using: composite
12+
steps:
13+
- name: Install asdf
14+
uses: asdf-vm/actions/setup@v4
15+
16+
- name: Tools cache
17+
id: asdf-cache
18+
uses: actions/cache@v4
19+
with:
20+
path: ~/.asdf/
21+
key: ${{ runner.os }}-${{ hashFiles('**/.tool-versions') }}
22+
23+
- name: Install tools from .tool-versions
24+
if: steps.asdf-cache.outputs.cache-hit != 'true'
25+
uses: asdf-vm/actions/install@v4
26+
27+
- name: Set up JDK
28+
if: inputs.java-version != ''
29+
uses: actions/setup-java@v4
30+
with:
31+
distribution: zulu
32+
java-version: ${{ inputs.java-version }}
33+
34+
- name: Install dependencies
35+
shell: bash
36+
run: bun install --frozen-lockfile

0 commit comments

Comments
 (0)