Skip to content

refactor(apple): give snapshot and fold one native-build owner - #3018

Merged
thymikee merged 2 commits into
mainfrom
refactor/apple-native-build-owner-2970
Sep 28, 2026
Merged

thymikee merged 2 commits into
mainfrom
refactor/apple-native-build-owner-2970

Conversation

@thymikee

@thymikee thymikee commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Summary

Moves the TS native-build cache, lock, toolchain-identity, and build-host code out of snapshot-source into a domain-neutral packages/platform-apple/src/native-build module group, so fold-helper-cache.ts no longer imports the snapshot bridge's deadlines, errors, host construction, or host types to compile a binary that needs none of them.

  • native-build/{cache,deadline,host,toolchain-identity,errors}.ts carry the one cache, one lock, and one toolchain-identity probe every runtime clang build in this package shares. NativeBuildHost exposes only file access, command execution, locking, and process identity — no bridge socket start/connect, no target-process inspection.
  • NativeBuildError is domain-neutral (cancelled/timeout/unsupported); snapshot-source/errors.ts and deadline.ts map it onto SnapshotSourceError at the boundary, and foldable/fold-helper-cache.ts maps it onto its own fold-helper-build-failed AppError, preserving both public error shapes byte for byte.
  • fold-helper-cache.ts drops constructing a full SnapshotSourceHost (and overriding run) for createNativeBuildHost(runAppleToolCommand), the narrow host the compile actually needs.
  • snapshot-source/host.ts drops its own copy of lock acquisition for the shared one; cache-identity.ts keeps only the bridge's simulatorRuntime addition, with retry/probe/cancellation logic moved to native-build/toolchain-identity.ts along with its tests.
  • No Swift changes; cache-key inputs, hash algorithm, manifest schema, and lock semantics are unchanged — only file/module ownership moved.

Closes #2970. 14 files touched.

Validation

Tested commit: baa48fa8ba5f10cff86a2aa69483bf5c399d3f0f.

pnpm check:affected --run: 277 test files / 1823 tests passed, including the Darwin conformance suites (native-runtime.test.ts, fold-helper's -Werror compile gate) that do genuine xcrun/clang compiles against the real toolchain.

Live device validation (production code path, no simulator needed — compilation targets the iphonesimulator SDK, not a booted device):

  • ensureSnapshotBridgeBinary: cache MISS build 1773ms, then cache HIT reuse 95ms, identical output path both times.
  • ensureFoldHelperBinary: cache MISS build 297ms, then cache HIT reuse 102ms, identical output path both times.
  • Cancellation contract: a pre-aborted ensureFoldHelperBinary call rejects with isRequestCanceledError(error) === true (this is the fix in this commit: a fold or toolchain-probe cancellation now keeps reason: 'request_canceled' the way it did before the native-build split).

No simulator was created: the change doesn't alter when the native artifact is built or reused (cache-key inputs, hash algorithm, manifest schema, and lock semantics are preserved unchanged — only module ownership moved), confirmed by inspection and by the unit + real-compile tests above producing byte-identical cache behavior on both paths.

No unresolved risk.

Review in cubic

Move the shared native-build cache, toolchain identity, and build host
out of snapshot-source into a domain-neutral packages/platform-apple/src
/native-build module group, so the fold helper no longer imports the
snapshot bridge's deadlines, errors, host construction, or host types
to compile a binary that needs none of them.

- native-build/{cache,deadline,host,toolchain-identity,errors}.ts carry
  the one cache implementation, one lock implementation, and one
  toolchain-identity read every runtime clang build in this package
  shares (#2796, #2712); NativeBuildHost exposes only file access,
  command execution, locking, and process identity, with no bridge
  socket start/connect and no target-process inspection.
- NativeBuildError is domain-neutral (cancelled/timeout/unsupported);
  no shared build module imports SnapshotSourceError or emits
  bridge-specific error fields. snapshot-source/errors.ts and
  deadline.ts map it onto SnapshotSourceError at the boundary, and
  foldable/fold-helper-cache.ts maps it onto its own
  fold-helper-build-failed AppError, preserving both public error
  shapes byte for byte (see the moved and updated test suites).
- fold-helper-cache.ts drops its whole-snapshot-host construction
  (building a full SnapshotSourceHost and overriding `run`) for
  createNativeBuildHost(runAppleToolCommand), the narrow host the
  compile actually needs.
- snapshot-source/host.ts drops its own copy of the lock-acquisition
  implementation in favor of the shared one.
- cache-identity.ts keeps only the bridge's simulatorRuntime addition;
  the toolchain retry/probe/cancellation logic it used to own moved to
  native-build/toolchain-identity.ts with its test coverage.

Net production lines grow (+132 across the touched files, per
`git diff --numstat -M`) because achieving actual decoupling needs a
second, narrow error/deadline type and a translation boundary, not
because anything was merely relocated: the whole-snapshot-host
construction, the duplicate lock implementation, and the fold helper's
dependency on the bridge's deadline/error/host types are all deleted,
and NativeBuildHost's type now statically forbids a future build/cache
change from reaching back into bridge-only capabilities.

Live-validated on this Darwin host with the production code: a cache
miss then a cache-key-stable cache hit for both the snapshot bridge
(779ms build -> 127ms reuse) and the fold helper (2.9s build -> 2.2s
reuse, dominated by the unchanged per-call toolchain probe) via
ensureSnapshotBridgeBinary/ensureFoldHelperBinary directly. No Swift
changes.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 14 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/platform-apple/src/foldable/fold-helper-cache.ts">

<violation number="1" location="packages/platform-apple/src/foldable/fold-helper-cache.ts:141">
P2: Abort during fold-helper preparation now returns `NativeBuildError` unchanged, so callers lose `reason: 'request_canceled'` and `isRequestCanceledError` no longer recognizes the cancellation. Map native-build cancellation to the request-canceled error shape, including the deadline construction path.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

if (!(error instanceof SnapshotSourceError) || error.failureKind === 'cancelled') return error;
const { bridgeFailure: _kind, bridgeFailureCode: cause, ...details } = error.details ?? {};
return foldHelperBuildFailed({ ...details, cause }, error);
if (!(error instanceof NativeBuildError) || error.buildFailureKind === 'cancelled') return error;

@cubic-dev-ai cubic-dev-ai Bot Sep 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Abort during fold-helper preparation now returns NativeBuildError unchanged, so callers lose reason: 'request_canceled' and isRequestCanceledError no longer recognizes the cancellation. Map native-build cancellation to the request-canceled error shape, including the deadline construction path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/platform-apple/src/foldable/fold-helper-cache.ts, line 141:

<comment>Abort during fold-helper preparation now returns `NativeBuildError` unchanged, so callers lose `reason: 'request_canceled'` and `isRequestCanceledError` no longer recognizes the cancellation. Map native-build cancellation to the request-canceled error shape, including the deadline construction path.</comment>

<file context>
@@ -137,13 +133,13 @@ async function compileFoldHelper(
-  if (!(error instanceof SnapshotSourceError) || error.failureKind === 'cancelled') return error;
-  const { bridgeFailure: _kind, bridgeFailureCode: cause, ...details } = error.details ?? {};
-  return foldHelperBuildFailed({ ...details, cause }, error);
+  if (!(error instanceof NativeBuildError) || error.buildFailureKind === 'cancelled') return error;
+  return foldHelperBuildFailed({ ...error.buildDetails, cause: error.buildFailureCode }, error);
 }
</file context>
Fix with cubic

Comment thread packages/platform-apple/src/native-build/host.ts Outdated
Comment thread packages/platform-apple/src/native-build/toolchain-identity.ts Outdated
Comment thread packages/platform-apple/src/snapshot-source/cache-identity.test.ts Outdated
Comment thread packages/platform-apple/src/native-build/toolchain-identity.test.ts Outdated
Comment thread packages/platform-apple/src/native-build/toolchain-identity.test.ts Outdated
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
Installed (including dependencies) 4.87 MB 4.88 MB +786 B
Package (unpacked) 4.87 MB 4.87 MB +786 B
Package (download) 1.46 MB 1.46 MB +165 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 27.3 ms 27.6 ms +0.3 ms
CLI --help 79.3 ms 82.6 ms +3.3 ms

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at 185da6d. The split changes how a cancelled fold is reported, so cancellation is no longer detected.

On main, a fold-helper cancellation was a SnapshotSourceError('cancelled') with details.reason='request_canceled', so isRequestCanceledError() returned true. At this head, the lock-wait abort in native-build/host.ts:81 and the deadline checks in deadline.ts:21 throw NativeBuildError('cancelled') with no reason. fold-helper-cache.ts:141 passes it through. A fold that is cancelled while the helper build waits now reports a generic COMMAND_FAILED, and retry and response layers treat it as a real failure. Please set reason: 'request_canceled' for kind === 'cancelled' in the NativeBuildError constructor at errors.ts:23, as SnapshotSourceError does. Add a test that aborts during the lock wait in ensureFoldHelperBinary and asserts isRequestCanceledError(error).

Could a smaller design do this? NativeBuildError can be the one shared failure type. SnapshotSourceHost can become NativeBuildHost & {bridge-only members}, with mapping only at ensureSnapshotBridgeBinary and asSnapshotSourceError. Then remainingSnapshotSourceMs and createSnapshotSourceDeadline can go where their callers already sit behind that mapping. That would bring the net growth (about +138 production lines here, +207 in the body) close to zero. What stops this? First, check that the lifecycle and preparation callers of remainingSnapshotSourceMs are behind asSnapshotSourceError.

Not blocking: the test at cache-identity.test.ts:44 says "rejected before any probe runs" but asserts probed === true, and toolchain-identity.test.ts imports its fixture from snapshot-source/__tests__.

CI: Smoke Tests is still queued. The iOS run builds the snapshot bridge, so it exercises this change.

Abort during fold-helper or toolchain-probe preparation lost
reason: 'request_canceled' once native-build/cache errors stopped
mapping onto SnapshotSourceError, so isRequestCanceledError no longer
recognized a canceled fold or lock wait. NativeBuildError now stamps
the same reason SnapshotSourceError already does; a lock wait with no
abort signal now maps an expired deadline to a typed timeout instead
of the raw lock error; and a mid-exec abort during a toolchain probe
now maps through the module's cancelled contract instead of leaking
the exec layer's raw cancellation.

Also: fixes the cache-identity and toolchain-identity test title/
comment mismatches cubic flagged, relocates the shared exec-timeout
test fixture out of snapshot-source so native-build's own tests stop
depending on it, folds SnapshotSourceHost's file/exec/lock members
into an intersection with NativeBuildHost instead of restating them,
and extracts the toolchain-probe failure classification into its own
function to keep runToolchainProbe under the complexity gate.
@thymikee

Copy link
Copy Markdown
Member Author

Fixed at baa48fa.

Cancellation reason: NativeBuildError now sets reason: 'request_canceled' for kind === 'cancelled', the same way SnapshotSourceError already does. A lock wait that never gets an abort signal and simply times out now maps to NativeBuildError('timeout', 'cache-lock-deadline') instead of returning the raw lock error (it only did that on the signal path before). A mid-exec abort during a toolchain probe now goes through the same cancelled mapping instead of leaking the exec layer's raw cancellation past it. Added a test in fold-helper-cache.test.ts that holds the fold-helper lock with one call, aborts a second call while it is waiting on that lock, and asserts isRequestCanceledError(error).

Smaller design: I did the safe part of this — SnapshotSourceHost is now NativeBuildHost & {bridge-only members} instead of repeating every file/exec/lock member by hand, which is a 12-line net cut with no behavior change. I did not fold NativeBuildError and SnapshotSourceError into one type. That would mean rewriting every one of the roughly dozen places that read the bridge's own failureKind/failureCode/bridgeFailure shape, which this PR's own summary commits to keeping byte for byte, to save under 60 lines in errors.ts/deadline.ts — a lot of blast radius for a small win, and outside what this decoupling needs to do. Happy to take that as a separate follow-up if you want it filed.

Not blocking, fixed anyway: the cache-identity test title said the blank-runtime rejection happens "before any probe runs" while the assertion (and the code) says the opposite — renamed it. The toolchain-identity retry-timeout comment claimed the retry was "charged the remainder" when the numbers show it got the full ceiling again — reworded to point at the case that actually shows the remainder. Moved the shared exec-timeout test fixture out of snapshot-source/__tests__ into native-build/__tests__ so native-build's own tests, and the fold-helper ones, stop reaching into the bridge package for it.

pnpm check:affected --run passes clean on the new head: 277 test files, 1823 tests, including the real xcrun/clang conformance suites. Re-ran the same live compile check as before (no simulator, both binaries build then cache-hit with identical paths) plus a new check that a pre-aborted build reports a canceled request; PR body's Validation section has the new numbers.

CI: still finishing on the new commit; the earlier run was all green except one Smoke Tests job that was still queued when it was last checked, unrelated to this change.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/platform-apple/src/foldable/fold-helper-cache.test.ts">

<violation number="1" location="packages/platform-apple/src/foldable/fold-helper-cache.test.ts:203">
P3: The 50 ms sleep is the only mechanism aiming the abort at the lock-wait branch, and the test still passes when it misses: if the abort lands before the waiter reaches `acquireLock`, `remainingNativeBuildMs` or the aborted-signal checks in `createNativeBuildDeadline`/`acquireNativeBuildLock` reject with `NativeBuildError('cancelled', 'abort-signal')`, so `isRequestCanceledError` holds without exercising the lock-wait path this test is named for. That silently weakens the regression coverage the fix (lock-wait abort keeping `reason: 'request_canceled'`) needs. Signal from the waiter's lock acquisition instead of sleeping: wrap `host.acquireLock` in the waiter's host so it resolves a `lockWaitStarted` promise, then `await lockWaitStarted` before `controller.abort()`. The outcome is not flaky either way: every pre-lock abort path also yields a cancelled error, so the assertion itself is deterministic.</violation>

<violation number="2" location="packages/platform-apple/src/foldable/fold-helper-cache.test.ts:211">
P3: `releaseClang()` and `await holder` only run when the cancellation assertion passes. If the waiter rejects with a non-cancelled error, `assert.rejects` throws, the gate is never released, and the `holder` build stays pending forever while the `finally` already removes the cache root. Release the gate in a `finally` so the gated build always settles on every exit path.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

return true;
});

releaseClang();

@cubic-dev-ai cubic-dev-ai Bot Sep 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: releaseClang() and await holder only run when the cancellation assertion passes. If the waiter rejects with a non-cancelled error, assert.rejects throws, the gate is never released, and the holder build stays pending forever while the finally already removes the cache root. Release the gate in a finally so the gated build always settles on every exit path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/platform-apple/src/foldable/fold-helper-cache.test.ts, line 211:

<comment>`releaseClang()` and `await holder` only run when the cancellation assertion passes. If the waiter rejects with a non-cancelled error, `assert.rejects` throws, the gate is never released, and the `holder` build stays pending forever while the `finally` already removes the cache root. Release the gate in a `finally` so the gated build always settles on every exit path.</comment>

<file context>
@@ -156,6 +157,64 @@ test('a compile exec killed at its budget reports the fold-helper build, not the
+      return true;
+    });
+
+    releaseClang();
+    await holder;
+  } finally {
</file context>
Fix with cubic

signal: controller.signal,
});
// Give the waiter time to reach the lock's poll loop before aborting it.
await new Promise((resolve) => setTimeout(resolve, 50));

@cubic-dev-ai cubic-dev-ai Bot Sep 28, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The 50 ms sleep is the only mechanism aiming the abort at the lock-wait branch, and the test still passes when it misses: if the abort lands before the waiter reaches acquireLock, remainingNativeBuildMs or the aborted-signal checks in createNativeBuildDeadline/acquireNativeBuildLock reject with NativeBuildError('cancelled', 'abort-signal'), so isRequestCanceledError holds without exercising the lock-wait path this test is named for. That silently weakens the regression coverage the fix (lock-wait abort keeping reason: 'request_canceled') needs. Signal from the waiter's lock acquisition instead of sleeping: wrap host.acquireLock in the waiter's host so it resolves a lockWaitStarted promise, then await lockWaitStarted before controller.abort(). The outcome is not flaky either way: every pre-lock abort path also yields a cancelled error, so the assertion itself is deterministic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/platform-apple/src/foldable/fold-helper-cache.test.ts, line 203:

<comment>The 50 ms sleep is the only mechanism aiming the abort at the lock-wait branch, and the test still passes when it misses: if the abort lands before the waiter reaches `acquireLock`, `remainingNativeBuildMs` or the aborted-signal checks in `createNativeBuildDeadline`/`acquireNativeBuildLock` reject with `NativeBuildError('cancelled', 'abort-signal')`, so `isRequestCanceledError` holds without exercising the lock-wait path this test is named for. That silently weakens the regression coverage the fix (lock-wait abort keeping `reason: 'request_canceled'`) needs. Signal from the waiter's lock acquisition instead of sleeping: wrap `host.acquireLock` in the waiter's host so it resolves a `lockWaitStarted` promise, then `await lockWaitStarted` before `controller.abort()`. The outcome is not flaky either way: every pre-lock abort path also yields a cancelled error, so the assertion itself is deterministic.</comment>

<file context>
@@ -156,6 +157,64 @@ test('a compile exec killed at its budget reports the fold-helper build, not the
+      signal: controller.signal,
+    });
+    // Give the waiter time to reach the lock's poll loop before aborting it.
+    await new Promise((resolve) => setTimeout(resolve, 50));
+    controller.abort();
+
</file context>
Fix with cubic

@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at baa48fa. There are no conflicts, the evidence gap from the earlier review is closed, and this is ready for human review.

Not blocking: in host.ts a no-signal lock-wait timeout now maps through mapExpiredLockError, and the toolchain probe now turns a cancelled exec into nativeBuildError('cancelled'). No test covers either, and the PR body still says lock semantics are unchanged. A test that expires a no-signal lock wait and checks the timeout, plus an updated body line, would help.

CI: Smoke Tests is still queued. It exercises the snapshot bridge through the native-build files this PR changes, so it needs to pass on baa48fa.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 28, 2026
@thymikee
thymikee merged commit 3fe2e69 into main Sep 28, 2026
19 checks passed
@thymikee
thymikee deleted the refactor/apple-native-build-owner-2970 branch September 28, 2026 16:53
@github-actions

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-28 17:18 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(apple): give snapshot and fold one native-build owner

1 participant