You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(apple-runner): trim build scratch from runner cache keys and keep packaging out of the shared cache - #3248
Follow-up to #3246, covering the three items it lists after the eviction PR. This PR is independent of #3247: it touches buildXctestrunArtifact rather than ensureXctestrunArtifact. Merging the two textually conflicts in two places, both trivial: the decision-reason union in runner-cache.ts (each adds one member) and the Apple runner setup row in configuration.md (each adds one env var). Whichever lands second rebases.
1. Trim build scratch from a runner cache key after its build. After the manifest is written, and still under the cache lock, the build removes everything in the key except Build/Products and .agent-device-runner-cache.json: Build/Intermediates.noindex, SDKExplicitPrecompiledModules, ModuleCache.noindex, Logs, CompilationCache.noindex, SDKStatCaches.noindex, SourcePackages, info.plist. Reuse and launch read only the products and the metadata. The kept set is derived from the .xctestrun and its product paths, resolved through realpath: a product outside Build/Products is kept too, a symlinked product keeps its target's directory, and a product outside the key makes the trim a no-op.
A set AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH is never trimmed, and neither is a directory that is not a cache-<hash> key. A fixed override path is the development loop that rebuilds into the same tree, and eviction already skips it.
A trim failure never fails the runner start, and the module loads lazily, so the eager closure budgets are unchanged.
Trade-off: a trimmed key cannot be built into incrementally. Nothing in the daemon does that today. A source or Xcode change mints a new key (a cold build in either case), a key that fails certification is deleted and rebuilt from scratch (cleanRunnerDerivedArtifacts), and only a key with no metadata is built into, which a trimmed key never is. So the default costs no rebuild time. The only incremental build left is pnpm build:xcuitest:*, which uses its own path and is not trimmed.
2. Legacy ~/.agent-device/ios-runner. Documented, not deleted. Current versions never read it, but an older agent-device installed on the same machine still uses that layout, including leases next to it, and an automatic delete would pull products from under it. commands.md says to rm -rf ~/.agent-device/ios-runner once no old version runs.
3. pnpm build:package / prepack no longer write to the shared cache root.scripts/build-package-xcuitest.mjs (pnpm package:xcuitest, run by build:package) builds ios, macos, tvos and visionos with build-xcuitest-apple.sh into <repo>/.tmp/package-xcuitest/<platform>, and removes that directory before the builds (an interrupted run leaves nothing behind) and after them, also when a build fails. The script is not named build:xcuitest:* because setup-apple-runner-build hashes those script names into the CI runner cache key. Nothing reads those products (the package ships the runner source), so the compile check is unchanged. Packaging now always builds from scratch, so the isolation scan covers every file. pnpm build:xcuitest:<platform> is unchanged. npm users are unaffected: scripts are not in the tarball, and the daemon's keyed cache is not touched.
Validation
Measured on macOS 27 / Xcode 27.0, an iOS 27.0 simulator created for the run, with HOME pointed at a scratch directory so the shared ~/.agent-device was never touched.
Untrimmed key
Trimmed key
Total
165.7 MB
5.9 MB (-96%)
Build/Products
5.8 MB
5.8 MB
Build/Intermediates.noindex
67.0 MB
removed
SDKExplicitPrecompiledModules
76.1 MB
removed
ModuleCache.noindex
15.1 MB
removed
prepare ios-runner on a fresh home built the key (about 10 s) and trimmed it; the runner then launched from the trimmed key and open + snapshot -i returned the Settings tree through the xctest backend. The trim itself took 66 ms for 165 MB.
After stopping the daemon and the runner, a second prepare ios-runner logged reuse_ready and no new built_new: no rebuild.
After appending a line to a runner Swift source, prepare ios-runner built a second key (about 8 s, trimmed to 5.9 MB) and left the first in place.
For scale: build-xcuitest-apple.sh in one directory takes 8.0 s cold and 3.6 s incrementally, which is what an in-place rebuild would save, and the daemon never takes that path.
buildPackageXcuitest({ platforms: ['ios'] }) built into .tmp/package-xcuitest, removed it, and left the shared derived/ untouched.
Checks: pnpm check:affected --run (the full set, because package.json is workflow tooling) passes, including check:fallow --base origin/main, the eager-closure and package-closure tests, and all of packages/platform-apple/src/runner. src/__tests__/npm-package-scripts.test.ts now expects package:xcuitest in build:package. New tests cover the kept and removed entries, a build under a derived path override that keeps its scratch, a product outside the key, a symlinked product, a real ensureXctestrunArtifact build (stubbed xcodebuild) followed by a reuse with no second build, and the packaging script's scratch cleanup on success, failure and after an interrupted run.
Not run: macOS, tvOS and visionOS runners and a physical device from a trimmed key (the trim walks the same product paths for every platform, and the macOS product repair reads only those paths); the packaging script for macos, tvos and visionos; pnpm package:npm end to end.
The code in a790173 looks good and I found nothing that blocks it. Live evidence is still pending: the iOS simulator run (trim, then launch and reuse_ready) comes from the PR body, and I did not re-run it. The macOS and physical-device runner routes were not run by the author, and I did not check whether xcodebuild test-without-building reads anything outside Build/Products there, such as ModuleCache. The real runBuildScript path in the packaging script (execFileSync with the override env) is also not exercised, because the tests inject build. Could you run pnpm package:npm end to end and show that output, plus one macOS or device run that launches after a trim?
Not blocking, so take or leave these. runner-cache-trim.ts:319 copies the keyed-path decision with a regex and an env re-read, when resolveRunnerDerivedPath and resolveRunnerCacheKey already make it and buildXctestrunArtifact has expectedCacheMetadata in hand. The empty catch {} at runner-artifact.ts:303 hides a trim failure, so a best-effort diagnostic there would help. AGENT_DEVICE_IOS_RUNNER_CACHE_TRIM=0 at runner-cache-trim.ts:338 is a new public opt-out that #3246 did not ask for. The PR body says the test expects build:xcuitest:package, but the code and test use package:xcuitest.
On simplicity, the trim fits at the cache seam: it sits beside cleanRunnerDerivedArtifacts, runs under the same lock, and keeps the paths the manifest certifies, so I found no smaller owner. Could it be smaller still by gating on resolveRunnerCacheKey(expectedCacheMetadata) or a keyed flag from resolveRunnerDerivedPath, and by dropping the opt-out? The packaging script is already minimal.
All 16 checks pass and there are no conflicts. The PR is still a draft, so the author needs to mark it ready for review. After this PR or #3247 merges, the other must rebase over the textual conflicts in runner-cache.ts and configuration.md.
The reason will be displayed to describe this comment to others. Learn more.
1 issue found across 12 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/platform-apple/src/runner/runner-artifact.ts">
<violation number="1" location="packages/platform-apple/src/runner/runner-artifact.ts:395">
P2: This trim is uninterruptible and unbounded while holding the per-key cache lock. The `catch {}` swallows every error, including `createRequestCanceledError`, and `trimRunnerBuildScratch` has no deadline or signal of its own, so a canceled request waits out the full recursive delete (potentially hundreds of MB of scratch) before the phase returns — and the cache process lock is held for the whole abort. The PR treats trim failures as non-fatal, but cancellation should still be honored: check the request signal around the removal loop (and before the lock-hold work) instead of absorbing all errors indiscriminately.</violation>
</file>
Reply with feedback, questions, or to request a fix.
First run failed at prepare-publish-assets with ANDROID_HOME or ANDROID_SDK_ROOT must point to an Android SDK (no SDK variable in my shell, unrelated to the PR). With ANDROID_HOME set it passes. The relevant part of the output:
$ pnpm build && pnpm package:xcuitest && pnpm build:macos-helper:clean && pnpm prepare:publish-assets
$ tsdown ... (dist/src/runner-cache-trim.js is in the bundle)
$ node scripts/build-package-xcuitest.mjs
xcodebuild build-for-testing ... -derivedDataPath <repo>/.tmp/package-xcuitest/ios ... (also macos, tvos, visionos)
** TEST BUILD SUCCEEDED ** x4
$ node scripts/build-macos-helper ... swift build -c release -> Build complete!
$ node scripts/prepare-publish-assets.mjs -> Prepared publish assets for 0.21.20.
$ node --experimental-strip-types scripts/check-package.ts
Linted the tarball with publint and attw.
Confirmed the installed tree carries no daemon source, so its version pins its code.
Verified the dependency closure: ai.
Imported all 14 published entry points from a clean install.
Ran the published CLI 0.21.20 on Node 22.22.2.
The package npm would publish is sound.
The real runBuildScript path (execFileSync with the override env) ran for all four platforms, and .tmp/package-xcuitest was gone afterwards (.tmp held only tsconfig.tsbuildinfo).
macOS runner launch after a trim (partly blocked)
Setup: scratch HOME (runner cache, leases and daemon state under it; ~/Library/Developer symlinked so Xcode works), AGENT_DEVICE_MACOS_APP_BACKEND unset, my own fixture app (open <bundle id> --platform macos, then snapshot -i).
The first snapshot built the macOS key cache-62ae068c8e205be6 and trimmed it: diagnostics rebuild/cache_metadata_missing, clean/build_scratch_trimmed, build/built_new, and the key went to 5.7 MB (.agent-device-runner-cache.json, Build/Products with the .xctestrun, Debug/AgentDeviceRunner.app and AgentDeviceRunnerUITests-Runner.app, plus a Logs dir that xcodebuild test-without-building writes itself).
The same request then reused the trimmed key (reuse/reuse_ready) and started xcodebuild test-without-building, which never got past launching the UI test runner: Daemon request timed out after 90 s, with the xcodebuild sample parked in XCTHTestTargetRunner requestNewWorker. A second run from the trimmed key behaved the same.
Control: the same flow with the fix(apple-runner): evict stale runner cache keys after a build #3247 build (no trim) in a second scratch home, a full untrimmed key, hangs identically. So the hang is the host, not the trim. A macOS runner started by someone else 20 h ago (AgentDeviceRunner.app, parent launchd, session claim held by another workspace on host-macos-local) is still alive on this Mac, and I did not touch it; I suspect it holds the single automation session.
So I could not show a launched macOS runner from a trimmed key. What I can say about ModuleCache: after the launch attempts the trimmed key still held only metadata, Build and Logs; xcodebuild test-without-building did not recreate ModuleCache.noindex, SDKExplicitPrecompiledModules or Intermediates.noindex, and reuse certified the products without them. I did not observe a test actually executing, so this does not rule out reads after launch.
Not run: any physical device (none available), tvOS and visionOS launches. The iOS simulator launch after a trim is the one in the PR body.
Review nits
Gate: buildXctestrunArtifact now trims only when path.basename(derived) === resolveRunnerCacheKey(expectedCacheMetadata). The regex and the AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH re-read in runner-cache-trim.ts are gone. A new test builds under a derived path override and asserts the scratch is kept (it fails without the gate).
Empty catch: a trim failure now emits a warn diagnostic (runner_xctestrun_cache_trim_failed) and the start still never fails.
AGENT_DEVICE_IOS_RUNNER_CACHE_TRIM=0: dropped, along with its tests and the two docs mentions. Apple runner cache under ~/.agent-device/apple-runner grows without bound (4.4 GB measured) #3246 did not ask for it, and you asked whether it could go. Its only purpose would be an incremental build into a trimmed key, which nothing does, and the override path already covers that development loop.
PR body: the test name is now package:xcuitest and the test list matches.
apple-runner project (70 files), format:check, lint, typecheck, fallow audit --base origin/main, the eager-closure and test-size ratchets and npm-package-scripts pass locally.
One thing I noticed while debugging: on startup the daemon runs pkill -f 'xcodebuild.*test-without-building.*AgentDeviceRunner\.env\.session-host-macos-local-[0-9]'. That pattern matches any other daemon's macOS runner xcodebuild on the same Mac, whatever the state dir. Out of scope here, but it could surprise two agents sharing one Mac.
The cache-key trim in e88a58f looks correct to me, and it fixes what I raised on a790173: the opt-out env var and the hook that deleted it are gone. All 17 checks pass and there are no conflicts, but three open inline threads listed below still apply and need a fix before merge. I did not re-run pnpm package:npm, the iOS simulator launch after a trim, or the macOS trim plus reuse_ready run, so those rely on your quoted output. No macOS runner launch from a trimmed key was observed: the host hung, and an untrimmed control key hung the same way. I ran no physical device, tvOS, or visionOS launch after a trim, so a read outside Build/Products after launch on those routes is not ruled out, although the xctestrun ProductPaths are TESTROOT-relative. I also did not run the test suite, so I judged the override test from reading the code. A macOS launch from a trimmed key, on a host with no other runner session, would close the last evidence gap.
Of the open inline threads, these still apply: the override-basename trim in runner-artifact.ts (#3248 (comment), also #3248 (comment)), the symlinked derived path (#3248 (comment)), and the outside-product test that never reaches the check (#3248 (comment)). One lower-priority docs thread also still applies. The two env var threads (#3248 (comment) and #3248 (comment)) are fixed at e88a58f, so you can resolve them. The cancellation thread (#3248 (comment)) does not apply: the trim never reads the request signal, and it runs only after a successful build under the same lock.
@thymikee pushed c9fadb4. The trim now runs only when the key's real path is a direct child of the managed runner cache root, so an AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH override named like a cache key, or a symlinked key pointing outside the root, is left alone. isOutside is replaced by the exported isPathInsideDirectory. The outside-product test now uses an existing directory, and the docs state the one measured size instead of a range. I left the cancellation thread open with a reply: the trim does not read the request signal and runs after the successful build under the same lock. Local: format:check, lint, typecheck, fallow audit and the related test files pass. The full check:affected run had one timeout in daemon-entrypoint.test.ts (a startup sweep test under load), which passes when rerun alone. No new macOS launch from a trimmed key; that evidence gap is unchanged.
Thanks for the update. Commit c9fadb4 fixes the symlinked-key case, the elsewhere.app test, the docs figure, and the override that was named like a key outside the managed directory. One case from the earlier review (#3248 (comment)) still fails.
isManagedKey in runner-cache-trim.ts accepts any real path whose parent is the managed root. It does not check that the directory is the key for this build. If AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH points to ~/.agent-device/apple-runner/derived/<base>/development, resolveRunnerDerivedPath returns it unchanged. The parent check passes, and buildXctestrunArtifact then deletes the intermediates and logs of that fixed tree on every build. Each later build becomes a full rebuild, and the new docs line says such a tree is never trimmed. Please trim only when realpath(derived) equals realpath(path.join(resolveRunnerDerivedBasePath(device), resolveRunnerCacheKey(expectedCacheMetadata))). That is the path resolveRunnerDerivedPath returns with no override. Pass that expected key path into trimRunnerBuildScratch instead of the managed root, and compare real paths for equality. This also covers the stale-key case. Please add a test with the override at <managedRoot>/development that asserts the scratch survives.
On the open threads, the two Copilot threads on runner-cache-trim.ts still stand (r4202921073 and r4202921125), and the fix above resolves both. These threads no longer apply and can be resolved. r4195397414 does not apply because the trim takes no request signal, so there is no cancellation error to swallow, and a failed trim already emits runner_xctestrun_cache_trim_failed. r4195397400, r4195397433, r4195397460, r4195599241 and r4195626192 are fixed at c9fadb4.
All 17 checks pass at c9fadb4, and there are no conflicts. I did not run the test suite, so I judged the tests by reading the code. I have not seen a macOS runner launch from a trimmed key, and physical device, tvOS and visionOS launches after a trim have not been run. I also did not check whether any setup points the override inside the managed directory. Before merge, the equality check and the new test need to land.
@thymikee pushed 39ae002. trimRunnerBuildScratch now takes the expected key path, path.join(resolveRunnerDerivedBasePath(device), resolveRunnerCacheKey(expectedCacheMetadata)), and trims only when realpath(derived) equals realpath(expectedKeyPath). An AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH inside the managed root, such as <managedRoot>/development, is no longer trimmed, and isManagedKey and its root parameter are gone. New test: an override at <managedRoot>/development keeps its intermediates and logs across a rebuild (fails on c9fadb4). The existing trim tests now pass the expected key, and the docs line says the same. Local: format:check, lint, typecheck, fallow audit --base origin/main, and all 71 apple-runner test files pass; I did not run the full check:affected. No new macOS launch from a trimmed key; that evidence gap is unchanged.
An explicit derived-path override is still trimmed when it resolves to the computed default key itself (or an alias of it), because this call does not check whether the override was set and the real-path comparison then succeeds. That path is still a fixed incremental-build tree, so this violates the documented guarantee that configured overrides are never trimmed. Skip the trim whenever the non-empty override is present.
The earlier findings at c9fadb4 are fixed at 39ae002, and the new override test now covers the case that failed before. One problem remains, in the default route. The key path is the same value as the derived path there, so the equality check in runner-cache-trim.ts is always true: https://github.com/callstack/agent-device/blob/39ae002/packages/platform-apple/src/runner/runner-cache-trim.ts#L31. A cache-<hash> symlink that points outside the root would be trimmed again, which brings back the earlier symlink problem. The rule to satisfy is that the key entry itself must be a real directory under the real root. Please compare realpath(derived) to path.join(realpath(resolveRunnerDerivedBasePath(device)), key), and add a test where the default key itself is a symlink to an outside directory. The test at runner-cache-trim.test.ts:126 never creates other-key, so realpathSync throws and it passes for the wrong reason. Please create that directory too: #3248 (comment). I did not run the trim tests, and I did not check whether any real setup makes the cache-<hash> entry a symlink. I also did not see a macOS, physical device, tvOS or visionOS runner launch from a trimmed key. Smoke Tests is still running with no failure shown. The diff changes buildXctestrunArtifact, which the cold iOS smoke run uses, so please wait for a green result there. I see no conflicts. Please fix the symlink check in the default route first, then update the test.
Not blocking, and fine to take or leave: runner-artifact.ts:371 rebuilds the no-override branch of resolveRunnerDerivedPath inline, so the expected key path now has two spellings. Exporting a resolveRunnerKeyedDerivedPath(device, metadata) from runner-cache-metadata.ts and calling it from both places would keep one.
The open Copilot thread on the symlink check applies, and it is the same issue as above: #3248 (comment). The Cubic thread on the test also applies: #3248 (comment).
@thymikee pushed fbefaf0 and merged main (conflict was the diagnostic reason union in runner-cache.ts).
The trim now requires the key entry to be a real directory: realpath(derived) must equal realpath(parent of the expected key) joined with the key name. A symlinked key is skipped.
New test for a default key symlinked to an outside directory; it fails on 39ae002. The other-key test now creates the directory.
resolveRunnerKeyedDerivedPath is exported from runner-cache-metadata.ts and used by resolveRunnerDerivedPath and the trim call.
format:check, lint, typecheck and the apple-runner tests pass; fallow audit flags only findings outside this diff. Waiting on CI.
The PR is ready at fbefaf0. The earlier findings from 39ae002 are fixed: the trim guard now builds the canonical key from the real directory path plus the basename, so a symlinked default key no longer matches, and the new test creates the other managed key before it compares.
Not blocking: the isExpectedKey change and its test landed in merge commit abd5ad8 instead of fbefaf0, and that commit only rewires imports, so its message does not match its content; nothing to do if you squash-merge, and you can take or leave this.
All 17 checks pass at fbefaf0, including Smoke Tests, which runs the cold buildXctestrunArtifact route the trim runs in. I did not run the trim tests locally, and the claim that the new test fails on 39ae002 comes from reading the old code. The live run in the PR body was on an iOS simulator only, so macOS, tvOS, visionOS and physical-device launches from a trimmed key were not run. This update changes only the trim guard, so it needs no new live run. Nothing else stands in the way of merge.
On the other review threads, I found these two no longer apply, so please resolve them: the runner-artifact.ts:692 thread (an override equal to the keyed path names the managed cache- directory itself, which the default route trims the same way, and the key changes with any source or Xcode change, so no separate development tree is lost) at #3248 (comment), and the runner-cache-trim.ts:29-38 thread (fixed at fbefaf0) at #3248 (comment). The runner-cache-trim.test.ts:116 thread is also fixed at fbefaf0: #3248 (comment).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ready-for-humanValid work that needs human implementation, judgment, or maintainer merge
3 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #3246, covering the three items it lists after the eviction PR. This PR is independent of #3247: it touches
buildXctestrunArtifactrather thanensureXctestrunArtifact. Merging the two textually conflicts in two places, both trivial: the decision-reason union inrunner-cache.ts(each adds one member) and the Apple runner setup row inconfiguration.md(each adds one env var). Whichever lands second rebases.1. Trim build scratch from a runner cache key after its build. After the manifest is written, and still under the cache lock, the build removes everything in the key except
Build/Productsand.agent-device-runner-cache.json:Build/Intermediates.noindex,SDKExplicitPrecompiledModules,ModuleCache.noindex,Logs,CompilationCache.noindex,SDKStatCaches.noindex,SourcePackages,info.plist. Reuse and launch read only the products and the metadata. The kept set is derived from the.xctestrunand its product paths, resolved throughrealpath: a product outsideBuild/Productsis kept too, a symlinked product keeps its target's directory, and a product outside the key makes the trim a no-op.AGENT_DEVICE_IOS_RUNNER_DERIVED_PATHis never trimmed, and neither is a directory that is not acache-<hash>key. A fixed override path is the development loop that rebuilds into the same tree, and eviction already skips it.Trade-off: a trimmed key cannot be built into incrementally. Nothing in the daemon does that today. A source or Xcode change mints a new key (a cold build in either case), a key that fails certification is deleted and rebuilt from scratch (
cleanRunnerDerivedArtifacts), and only a key with no metadata is built into, which a trimmed key never is. So the default costs no rebuild time. The only incremental build left ispnpm build:xcuitest:*, which uses its own path and is not trimmed.2. Legacy
~/.agent-device/ios-runner. Documented, not deleted. Current versions never read it, but an older agent-device installed on the same machine still uses that layout, including leases next to it, and an automatic delete would pull products from under it.commands.mdsays torm -rf ~/.agent-device/ios-runneronce no old version runs.3.
pnpm build:package/prepackno longer write to the shared cache root.scripts/build-package-xcuitest.mjs(pnpm package:xcuitest, run bybuild:package) builds ios, macos, tvos and visionos withbuild-xcuitest-apple.shinto<repo>/.tmp/package-xcuitest/<platform>, and removes that directory before the builds (an interrupted run leaves nothing behind) and after them, also when a build fails. The script is not namedbuild:xcuitest:*becausesetup-apple-runner-buildhashes those script names into the CI runner cache key. Nothing reads those products (the package ships the runner source), so the compile check is unchanged. Packaging now always builds from scratch, so the isolation scan covers every file.pnpm build:xcuitest:<platform>is unchanged. npm users are unaffected: scripts are not in the tarball, and the daemon's keyed cache is not touched.Validation
Measured on macOS 27 / Xcode 27.0, an iOS 27.0 simulator created for the run, with
HOMEpointed at a scratch directory so the shared~/.agent-devicewas never touched.Build/ProductsBuild/Intermediates.noindexSDKExplicitPrecompiledModulesModuleCache.noindexprepare ios-runneron a fresh home built the key (about 10 s) and trimmed it; the runner then launched from the trimmed key andopen+snapshot -ireturned the Settings tree through the xctest backend. The trim itself took 66 ms for 165 MB.prepare ios-runnerloggedreuse_readyand no newbuilt_new: no rebuild.prepare ios-runnerbuilt a second key (about 8 s, trimmed to 5.9 MB) and left the first in place.build-xcuitest-apple.shin one directory takes 8.0 s cold and 3.6 s incrementally, which is what an in-place rebuild would save, and the daemon never takes that path.buildPackageXcuitest({ platforms: ['ios'] })built into.tmp/package-xcuitest, removed it, and left the sharedderived/untouched.Checks:
pnpm check:affected --run(the full set, becausepackage.jsonis workflow tooling) passes, includingcheck:fallow --base origin/main, the eager-closure and package-closure tests, and all ofpackages/platform-apple/src/runner.src/__tests__/npm-package-scripts.test.tsnow expectspackage:xcuitestinbuild:package. New tests cover the kept and removed entries, a build under a derived path override that keeps its scratch, a product outside the key, a symlinked product, a realensureXctestrunArtifactbuild (stubbedxcodebuild) followed by a reuse with no second build, and the packaging script's scratch cleanup on success, failure and after an interrupted run.Not run: macOS, tvOS and visionOS runners and a physical device from a trimmed key (the trim walks the same product paths for every platform, and the macOS product repair reads only those paths); the packaging script for macos, tvos and visionos;
pnpm package:npmend to end.