Skip to content

Commit 7e0f29c

Browse files
committed
fix(limrun): require an Xcode 26 runner that embeds XCTest
Limrun simulators run iOS 26. An Xcode 27 simulator build embeds no XCTest frameworks and resolves them from the build machine, so it cannot start on a remote simulator. The release workflow now builds with Xcode 26, the package script refuses a runner without XCTest.framework, and the Limrun runner products check refuses such a runner before upload.
1 parent f9bd059 commit 7e0f29c

6 files changed

Lines changed: 53 additions & 10 deletions

File tree

‎.github/workflows/release-ios-runner.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ jobs:
5252
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
5353
shell: bash
5454

55+
# Limrun simulators run iOS 26 and cannot reach this machine's Xcode. Xcode 26 embeds XCTest in
56+
# the runner app; Xcode 27 leaves it out, and the packaging step refuses such a runner.
57+
- name: Select Xcode 26
58+
run: |
59+
set -euo pipefail
60+
XCODE_APP="$(ls -d /Applications/Xcode_26*.app | sort -V | tail -n 1)"
61+
echo "DEVELOPER_DIR=${XCODE_APP}/Contents/Developer" >> "$GITHUB_ENV"
62+
echo "Using ${XCODE_APP}"
63+
shell: bash
64+
5565
- name: Build iOS simulator runner
5666
env:
5767
AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/ios-runner-derived

‎scripts/package-ios-simulator-runner.sh‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,13 @@ if [ -n "$EXPECTED_RUNNER_BUNDLE_ID" ] && [ "$RUNNER_BUNDLE_ID" != "$EXPECTED_RU
9292
fi
9393

9494
ditto "$RUNNER_APP_PATH" "$STAGED_APP_PATH"
95-
# Remote simulators cannot resolve dependencies from the build machine's Xcode paths.
96-
PLATFORM_PATH="$(xcrun --sdk iphonesimulator --show-sdk-platform-path)"
97-
INTEROP_LIBRARY="$PLATFORM_PATH/Developer/usr/lib/lib_TestingInterop.dylib"
98-
mkdir -p "$STAGED_APP_PATH/Frameworks"
99-
if [ -f "$INTEROP_LIBRARY" ]; then
100-
cp "$INTEROP_LIBRARY" "$STAGED_APP_PATH/Frameworks/"
101-
elif [ -d "$STAGED_APP_PATH/Frameworks/Testing.framework" ] &&
102-
[ ! -f "$STAGED_APP_PATH/Frameworks/lib_TestingInterop.dylib" ]; then
103-
echo "Testing.framework requires lib_TestingInterop.dylib, but it was not found at $INTEROP_LIBRARY" >&2
95+
# Remote simulators cannot resolve dependencies from the build machine's Xcode paths, so the runner
96+
# must carry XCTest itself. Xcode 26 embeds it; Xcode 27 leaves it out, and Limrun simulators run iOS 26.
97+
if [ ! -d "$STAGED_APP_PATH/Frameworks/XCTest.framework" ]; then
98+
echo "The runner app does not embed XCTest.framework. Build the simulator runner with Xcode 26 (set DEVELOPER_DIR to an Xcode 26 install): Xcode 27 resolves XCTest from the build machine, which a remote simulator cannot reach." >&2
10499
exit 1
105100
fi
101+
PLATFORM_PATH="$(xcrun --sdk iphonesimulator --show-sdk-platform-path)"
106102
# Swift Testing loads overlays at runtime; keep them from the same toolchain as its bundled framework.
107103
if [ -d "$STAGED_APP_PATH/Frameworks/Testing.framework" ]; then
108104
for FRAMEWORK in "$PLATFORM_PATH"/Developer/Library/Frameworks/_Testing_*.framework; do

‎src/core/interactors/limrun-ios-runner-products.test.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,31 @@ test('a normal app without a test bundle reports the invalid runner path', async
3232
expect(fetch).not.toHaveBeenCalled();
3333
});
3434

35+
test('a runner built without its own XCTest is refused before it reaches Limrun', async () => {
36+
const directory = await mkdtempForTest('runner-products-');
37+
const appPath = path.join(directory, 'AgentDeviceRunnerUITests-Runner.app');
38+
// An Xcode 27 build: the test bundle is there, the XCTest frameworks are not.
39+
await fs.mkdir(path.join(appPath, 'PlugIns', 'AgentDeviceRunnerUITests.xctest'), {
40+
recursive: true,
41+
});
42+
const fetch = vi.fn();
43+
vi.stubGlobal('fetch', fetch);
44+
await expect(
45+
prepareLimrunRunnerProducts({
46+
directory,
47+
appPath,
48+
version: '0.1.0',
49+
bindAddress: '10.244.1.2',
50+
endpoint,
51+
signal: new AbortController().signal,
52+
}),
53+
).rejects.toMatchObject({
54+
code: 'INVALID_ARGS',
55+
message: 'The Limrun XCTest runner does not embed XCTest.framework',
56+
});
57+
expect(fetch).not.toHaveBeenCalled();
58+
});
59+
3560
test('an HTML upload failure preserves the HTTP status', async () => {
3661
const directory = await mkdtempForTest('runner-products-');
3762
await fs.writeFile(path.join(directory, 'Runner.xctestrun'), '<plist/>');

‎src/core/interactors/limrun-ios-runner-products.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,17 @@ export async function prepareLimrunRunnerProducts(input: {
9494
if (!plugins.includes('AgentDeviceRunnerUITests.xctest')) {
9595
throw new AppError('INVALID_ARGS', 'The app does not contain AgentDeviceRunnerUITests.xctest');
9696
}
97+
// A remote simulator cannot reach the build machine's Xcode, so the runner must carry XCTest
98+
// itself. Xcode 26 embeds it in the runner app; Xcode 27 leaves it out and resolves it from the
99+
// build machine, so such a runner never starts on a Limrun simulator.
100+
const xctest = await fs
101+
.stat(path.join(appPath, 'Frameworks', 'XCTest.framework'))
102+
.catch(() => null);
103+
if (!xctest?.isDirectory()) {
104+
throw new AppError('INVALID_ARGS', 'The Limrun XCTest runner does not embed XCTest.framework', {
105+
hint: 'Build and package the simulator runner with Xcode 26, for example with DEVELOPER_DIR set to an Xcode 26 install.',
106+
});
107+
}
97108
await fs.cp(appPath, path.join(products, 'Runner.app'), { recursive: true });
98109
await writeLimrunRunnerConfiguration(products, input.bindAddress, input.endpoint);
99110
return products;

‎src/core/interactors/limrun-ios-runner.fixtures.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export async function fixture(
6262
const directory = await mkdtempForTest('limrun-runner-');
6363
const app = path.join(directory, 'AgentDeviceRunnerUITests-Runner.app');
6464
await fs.mkdir(path.join(app, 'PlugIns', 'AgentDeviceRunnerUITests.xctest'), { recursive: true });
65+
await fs.mkdir(path.join(app, 'Frameworks', 'XCTest.framework'), { recursive: true });
6566
await fs.writeFile(path.join(app, 'Runner'), 'simulator runner', { mode: 0o755 });
6667
const requests: string[] = [];
6768
const commands: Record<string, unknown>[] = [];

‎website/docs/docs/limrun.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Open your app before taking a snapshot. Snapshots target the app you opened in t
102102

103103
`install` leaves your app stopped. Use `open` to launch it. To change launch arguments on an app that is already running, use `open --relaunch --launch-args`. Screenshots download as lossless PNG files, and recording downloads the finished MP4 when you stop it. Push notifications use the same command and payload format as local simulators.
104104

105-
If you use a nightly or unpublished agent-device build, obtain a matching packaged iOS runner from the build's maintainer. Set `AGENT_DEVICE_LIMRUN_IOS_RUNNER_APP` to the extracted `.app` path before starting agent-device. If the daemon is already running, stop it with `agent-device daemon stop` using the same `--state-dir`, then reconnect.
105+
If you use a nightly or unpublished agent-device build, obtain a matching packaged iOS runner from the build's maintainer. Set `AGENT_DEVICE_LIMRUN_IOS_RUNNER_APP` to the extracted `.app` path before starting agent-device. The runner must be built with Xcode 26, because Limrun simulators run iOS 26 and an Xcode 27 build does not include the XCTest frameworks it needs there; agent-device refuses such a runner. If the daemon is already running, stop it with `agent-device daemon stop` using the same `--state-dir`, then reconnect.
106106

107107
## Connect an iOS app to Metro
108108

0 commit comments

Comments
 (0)