From 86fd3f427e8863cc924d98d3cc1b66402feac0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Kwas=CC=81niewski?= Date: Sat, 3 Oct 2026 13:47:44 +0200 Subject: [PATCH] fix(ios): pin runner build roots under derived data A custom or legacy build location in Xcode settings redirected runner products outside -derivedDataPath, so the xctestrun was never found. Pass SYMROOT, OBJROOT, and SHARED_PRECOMPS_DIR explicitly in the daemon build and build-xcuitest-apple.sh. --- .../runner-artifact-build-location.test.ts | 88 +++++++++++++++++++ .../src/runner/runner-artifact.ts | 2 + .../src/runner/runner-cache-metadata.ts | 14 +++ .../platform-apple/src/runner/runner-cache.ts | 1 + .../runner-isolation-diagnostics.test.ts | 15 ++++ scripts/build-xcuitest-apple.sh | 3 + 6 files changed, 123 insertions(+) create mode 100644 packages/platform-apple/src/runner/__tests__/runner-artifact-build-location.test.ts diff --git a/packages/platform-apple/src/runner/__tests__/runner-artifact-build-location.test.ts b/packages/platform-apple/src/runner/__tests__/runner-artifact-build-location.test.ts new file mode 100644 index 0000000000..0683ba524f --- /dev/null +++ b/packages/platform-apple/src/runner/__tests__/runner-artifact-build-location.test.ts @@ -0,0 +1,88 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { afterEach, beforeEach, test, vi } from 'vitest'; +import { resetAllProcessMemosForTests } from '@agent-device/kernel/ttl-memo'; +import { appleRunnerTestHost } from '../test-host.ts'; +import type { ExecResult } from '@agent-device/host-kit/command'; +import { ensureXctestrunArtifact } from '../runner-xctestrun.ts'; +import { appleToolchainProbeResult } from './apple-toolchain-fixtures.ts'; +import { IOS_SIMULATOR } from './device-fixtures.ts'; +import { seedRunnerProductBundle } from './runner-xctestrun.fixtures.ts'; +import { mkdtempForTestSync } from './tmp-dir.ts'; + +const XCTESTRUN_NAME = + 'AgentDeviceRunner_AgentDeviceRunnerUITests_iphonesimulator27.0-arm64.xctestrun'; + +const runCmdStreaming = vi.fn(); +let projectRoot: string; +let derived: string; +let customBuildLocation: string; + +beforeEach(() => { + resetAllProcessMemosForTests(); + projectRoot = mkdtempForTestSync('agent-device-runner-location-root-'); + fs.mkdirSync( + path.join(projectRoot, 'apple', 'runner', 'AgentDeviceRunner', 'AgentDeviceRunner.xcodeproj'), + { recursive: true }, + ); + derived = mkdtempForTestSync('agent-device-runner-location-derived-'); + customBuildLocation = mkdtempForTestSync('agent-device-runner-location-custom-'); + process.env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH = derived; + runCmdStreaming.mockReset().mockImplementation(xcodebuildHonoringCustomBuildLocation); + appleRunnerTestHost.update({ + runCmdSync: vi.fn().mockImplementation(appleToolchainProbeResult), + runCmdStreaming, + findProjectRoot: () => projectRoot, + readVersion: () => '0.0.0-test', + }); +}); + +afterEach(() => { + delete process.env.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH; +}); + +test('a custom Xcode build location cannot move the built xctestrun out of the runner cache', async () => { + const built = await ensureXctestrunArtifact(IOS_SIMULATOR, {}); + + assert.equal(built.artifact, 'rebuilt'); + assert.equal(built.xctestrunPath, path.join(derived, 'Build', 'Products', XCTESTRUN_NAME)); + assert.deepEqual(fs.readdirSync(customBuildLocation), []); + + const reused = await ensureXctestrunArtifact(IOS_SIMULATOR, {}); + + assert.equal(reused.artifact, 'valid'); + assert.equal(reused.xctestrunPath, built.xctestrunPath); + assert.equal(runCmdStreaming.mock.calls.length, 1); +}); + +/** + * Stands in for `xcodebuild build-for-testing` on a host whose Xcode settings set a custom build + * location: products land there unless the command line pins `SYMROOT`. + */ +async function xcodebuildHonoringCustomBuildLocation( + _command: string, + args: string[], +): Promise { + const symroot = args.find((arg) => arg.startsWith('SYMROOT='))?.slice('SYMROOT='.length); + const productsRoot = symroot ?? customBuildLocation; + await seedRunnerProductBundle( + path.join(productsRoot, 'Debug-iphonesimulator', 'AgentDeviceRunner.app'), + ); + fs.writeFileSync( + path.join(productsRoot, XCTESTRUN_NAME), + ` + + + + ProjectRootHint + ${projectRoot} + ProductPaths + + __TESTROOT__/Debug-iphonesimulator/AgentDeviceRunner.app + + +`, + ); + return { exitCode: 0, stdout: '', stderr: '' }; +} diff --git a/packages/platform-apple/src/runner/runner-artifact.ts b/packages/platform-apple/src/runner/runner-artifact.ts index 0447a71574..bb5ecaa504 100644 --- a/packages/platform-apple/src/runner/runner-artifact.ts +++ b/packages/platform-apple/src/runner/runner-artifact.ts @@ -31,6 +31,7 @@ import { requireRunnerPhaseRemainingMs, resolveExpectedRunnerCacheMetadata, resolveRunnerArchBuildSettings, + resolveRunnerBuildLocationSettings, resolveRunnerBundleBuildSettings, resolveRunnerDerivedPath, resolveRunnerMaxConcurrentDestinationsFlag, @@ -470,6 +471,7 @@ async function buildRunnerXctestrun( ...xcodebuildDestinationArgs(device, resolveRunnerBuildDestination(device)), '-derivedDataPath', derived, + ...resolveRunnerBuildLocationSettings(derived), ...performanceBuildSettings, ...archBuildSettings, ...sandboxBuildArgs, diff --git a/packages/platform-apple/src/runner/runner-cache-metadata.ts b/packages/platform-apple/src/runner/runner-cache-metadata.ts index cd5230d5a7..79954c917f 100644 --- a/packages/platform-apple/src/runner/runner-cache-metadata.ts +++ b/packages/platform-apple/src/runner/runner-cache-metadata.ts @@ -640,6 +640,20 @@ export function resolveRunnerArchBuildSettings(env: NodeJS.ProcessEnv = process. return archs ? [`ARCHS=${archs}`] : []; } +/** + * Pins the build roots to the default layout under `derived`. `-derivedDataPath` alone does not: + * a custom or legacy build location in the user's Xcode settings still redirects products and + * intermediates, so the `.xctestrun` would land outside the cache directory. + */ +export function resolveRunnerBuildLocationSettings(derived: string): string[] { + const intermediates = path.join(derived, 'Build', 'Intermediates.noindex'); + return [ + `SYMROOT=${path.join(derived, 'Build', 'Products')}`, + `OBJROOT=${intermediates}`, + `SHARED_PRECOMPS_DIR=${path.join(intermediates, 'PrecompiledHeaders')}`, + ]; +} + export function resolveRunnerSandboxBuildArgs(): string[] { return [ ...RUNNER_SANDBOX_BUILD_ARGS, diff --git a/packages/platform-apple/src/runner/runner-cache.ts b/packages/platform-apple/src/runner/runner-cache.ts index abfd2f9305..cf3670cb62 100644 --- a/packages/platform-apple/src/runner/runner-cache.ts +++ b/packages/platform-apple/src/runner/runner-cache.ts @@ -25,6 +25,7 @@ export { requireRunnerPhaseRemainingMs, resolveExpectedRunnerCacheMetadata, resolveRunnerArchBuildSettings, + resolveRunnerBuildLocationSettings, resolveRunnerBundleBuildSettings, resolveRunnerDerivedPath, resolveRunnerMaxConcurrentDestinationsFlag, diff --git a/scripts/__tests__/runner-isolation-diagnostics.test.ts b/scripts/__tests__/runner-isolation-diagnostics.test.ts index cd86eb5254..4d06d4c88f 100644 --- a/scripts/__tests__/runner-isolation-diagnostics.test.ts +++ b/scripts/__tests__/runner-isolation-diagnostics.test.ts @@ -185,3 +185,18 @@ describe('scripts/build-xcuitest-apple.sh isolation scan', () => { expect(result.stderr).not.toMatch(/runner isolation scan/); }); }); + +describe('scripts/build-xcuitest-apple.sh build location', () => { + test('pins every build root under the derived path so a custom Xcode build location cannot move products', () => { + const result = runBuildScript(log(...CANARY_DIAGNOSTICS), 65); + const args = fs.readFileSync(result.xcodebuildArgs, 'utf8').split('\n'); + const intermediates = path.join(result.derived, 'Build', 'Intermediates.noindex'); + expect(args).toEqual( + expect.arrayContaining([ + `SYMROOT=${path.join(result.derived, 'Build', 'Products')}`, + `OBJROOT=${intermediates}`, + `SHARED_PRECOMPS_DIR=${path.join(intermediates, 'PrecompiledHeaders')}`, + ]), + ); + }); +}); diff --git a/scripts/build-xcuitest-apple.sh b/scripts/build-xcuitest-apple.sh index 78887ddd7d..e306a8391a 100644 --- a/scripts/build-xcuitest-apple.sh +++ b/scripts/build-xcuitest-apple.sh @@ -144,6 +144,9 @@ build_for_testing() { -scheme "$SCHEME" \ -destination "$DESTINATION" \ -derivedDataPath "$DERIVED_PATH" \ + SYMROOT="$DERIVED_PATH/Build/Products" \ + OBJROOT="$DERIVED_PATH/Build/Intermediates.noindex" \ + SHARED_PRECOMPS_DIR="$DERIVED_PATH/Build/Intermediates.noindex/PrecompiledHeaders" \ "$@" }