Skip to content

Commit cf4d073

Browse files
thymikeeclaude
andcommitted
refactor(ios): key native build cache hits on the cache key and keep fold build error details
ensureNativeBuildCacheEntry now takes the build's key inputs, derives the cache key itself, and returns it. A hit only needs the manifest's cacheKey and binary hash to match, because the key already hashes every field the per-caller MANIFEST_FIELDS lists compared again. That removes nativeBuildManifestFieldsMatch, both field lists, and the snapshotBridgeCacheKey/foldHelperCacheKey wrappers. Keys are byte-identical to before, so existing cache entries still hit. A fold helper cache failure now keeps the underlying failure's hint and typed details (the native-build-stalled hint and timeoutMs, toolchain-probe stderr, a missing source's filePath) instead of replacing them with a fixed hint. Tests: simulator-hid.test.ts mocks the fold helper cache instead of retesting it through exec fakes, which drops sendSimulatorFoldPose's test-only cacheRoot option. The -Werror absence checks assert the argv directly, and the darwin gates append -Werror instead of splicing it in. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1 parent 40e4fb2 commit cf4d073

9 files changed

Lines changed: 196 additions & 521 deletions

File tree

‎packages/platform-apple/src/foldable/fold-helper-cache.test.ts‎

Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import type { SnapshotSourceHost } from '../snapshot-source/types.ts';
1010
import {
1111
buildFoldHelperCompileArgv,
1212
ensureFoldHelperBinary,
13-
foldHelperCacheKey,
1413
FOLD_HELPER_BUILD_TIMEOUT_MS,
1514
} from './fold-helper-cache.ts';
1615

@@ -82,31 +81,42 @@ test('a cache hit does not build, and a source or toolchain change does', async
8281
}
8382
});
8483

85-
test('the fold helper cache key changes with the compile argv, independent of source and toolchain', () => {
86-
const sourceHash = 'same-source';
87-
const toolchain = {
88-
xcode: 'Xcode 16.4\nBuild version 16F6',
89-
macosProductVersion: '15.6',
90-
macosBuild: '24G90',
91-
architecture: 'arm64',
92-
} as const;
93-
const argv = ['clang', '-Wall'];
94-
const changedArgv = ['clang', '-Wall', '-DSomethingNew'];
84+
test('the runtime clang build never uses -Werror', () => {
85+
assert.ok(!buildFoldHelperCompileArgv({ sourceRoot: '', outputPath: '' }).includes('-Werror'));
86+
});
9587

96-
const key = foldHelperCacheKey({ sourceHash, toolchain, compileArgv: argv });
97-
const sameKey = foldHelperCacheKey({ sourceHash, toolchain, compileArgv: argv });
98-
const keyAfterArgvChange = foldHelperCacheKey({
99-
sourceHash,
100-
toolchain,
101-
compileArgv: changedArgv,
102-
});
88+
test('a failed compile reports fold-helper-build-failed with the compiler output', async () => {
89+
const root = await mkdtempForTest('agent-device-fold-helper-cache-failure-');
90+
const sourceRoot = path.join(root, 'source');
91+
const cacheRoot = path.join(root, 'cache');
92+
await (await import('@agent-device/host-kit/host-file')).ensureHostDirectory(sourceRoot);
93+
await writeFile(path.join(sourceRoot, 'Fold.m'), 'fold source');
94+
const okHost = fakeFoldHelperHost(() => 'binary');
95+
const host: SnapshotSourceHost = {
96+
...okHost,
97+
run: async (command, args, options) => {
98+
if (command === 'xcrun' && args.includes('clang')) {
99+
return { stdout: '', stderr: 'compiler detail', exitCode: 1 };
100+
}
101+
return await okHost.run(command, args, options);
102+
},
103+
};
103104

104-
assert.equal(key, sameKey, 'the same argv always keys the same');
105-
assert.notEqual(
106-
key,
107-
keyAfterArgvChange,
108-
'an argv-only change (same source, same toolchain) cannot serve a stale binary',
109-
);
105+
try {
106+
await assert.rejects(ensureFoldHelperBinary({ host, sourceRoot, cacheRoot }), {
107+
code: 'COMMAND_FAILED',
108+
details: {
109+
stdout: '',
110+
stderr: 'compiler detail',
111+
exitCode: 1,
112+
processExitError: true,
113+
hint: 'Select an Xcode with the iOS simulator SDK and foldable HID support using DEVELOPER_DIR.',
114+
reason: 'fold-helper-build-failed',
115+
},
116+
});
117+
} finally {
118+
await rm(root, { recursive: true, force: true });
119+
}
110120
});
111121

112122
test('a compile exec killed at its budget reports the fold-helper build, not the exec layer', async () => {
@@ -134,10 +144,10 @@ test('a compile exec killed at its budget reports the fold-helper build, not the
134144
(error as { details?: { reason?: string } }).details?.reason,
135145
'fold-helper-build-failed',
136146
);
137-
assert.equal(
138-
(error as { details?: { cause?: string } }).details?.cause,
139-
'native-build-stalled',
140-
);
147+
const details = (error as { details?: Record<string, unknown> }).details;
148+
assert.equal(details?.cause, 'native-build-stalled');
149+
assert.equal(details?.timeoutMs, FOLD_HELPER_BUILD_TIMEOUT_MS);
150+
assert.match(String(details?.hint), /stopped the fold helper build/);
141151
return true;
142152
},
143153
);
@@ -160,14 +170,7 @@ describe.skipIf(process.platform !== 'darwin')('fold helper warning gate', () =>
160170
const sourceRoot = path.resolve(import.meta.dirname, '../../../../apple/fold-helper');
161171
const binary = path.join(await mkdtempForTest('fold-helper-werror-'), 'fold-helper');
162172
const argv = buildFoldHelperCompileArgv({ sourceRoot, outputPath: binary });
163-
const wextraIndex = argv.indexOf('-Wextra');
164-
assert.ok(wextraIndex >= 0, 'the production argv carries -Wextra');
165-
const werrorArgv = [
166-
...argv.slice(0, wextraIndex + 1),
167-
'-Werror',
168-
...argv.slice(wextraIndex + 1),
169-
];
170-
compiled = await runCmd('xcrun', werrorArgv, {
173+
compiled = await runCmd('xcrun', [...argv, '-Werror'], {
171174
allowFailure: true,
172175
timeoutMs: FOLD_HELPER_BUILD_TIMEOUT_MS,
173176
});

‎packages/platform-apple/src/foldable/fold-helper-cache.ts‎

Lines changed: 24 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import { AppError } from '@agent-device/kernel/errors';
33
import { execFailureDetails } from '@agent-device/host-kit/command';
44
import { runAppleToolCommand } from '../core/tool-provider.ts';
55
import { COLD_TOOLCHAIN_PROBE_TIMEOUT_MS } from '../runner/apple-runner-platform.ts';
6-
import {
7-
readHostToolchainIdentity,
8-
type HostToolchainIdentity,
9-
} from '../snapshot-source/cache-identity.ts';
6+
import { readHostToolchainIdentity } from '../snapshot-source/cache-identity.ts';
107
import {
118
createSnapshotSourceDeadline,
129
type SnapshotSourceDeadline,
@@ -17,43 +14,15 @@ import {
1714
ensureNativeBuildCacheEntry,
1815
execNativeBuildClang,
1916
fingerprintNativeBuildSource,
20-
nativeBuildCacheKey,
21-
nativeBuildManifestFieldsMatch,
2217
} from '../snapshot-source/native-build-cache.ts';
2318
import type { SnapshotSourceHost } from '../snapshot-source/types.ts';
2419

2520
const FOLD_HELPER_SOURCE_FILENAME = 'Fold.m';
2621
const FOLD_HELPER_BINARY_FILENAME = 'fold-helper';
2722
const FOLD_HELPER_SCHEMA_VERSION = 1 as const;
2823
const FOLD_HELPER_LOCK_DESCRIPTION = 'iOS Simulator fold helper cache';
29-
const MANIFEST_FIELDS = [
30-
'schemaVersion',
31-
'sourceHash',
32-
'cacheKey',
33-
'toolchain',
34-
'compileArgv',
35-
] as const;
36-
37-
/**
38-
* The fold helper cache key, folding in the compile argv (fingerprinted with a placeholder
39-
* `sourceRoot` and `outputPath`) alongside `sourceHash` and `toolchain`, so a change to a compiler
40-
* flag or framework list cannot serve a binary built from a different command line (#2796
41-
* follow-up).
42-
*/
43-
export function foldHelperCacheKey(
44-
input: Readonly<{
45-
sourceHash: string;
46-
toolchain: HostToolchainIdentity;
47-
compileArgv: readonly string[];
48-
}>,
49-
): string {
50-
return nativeBuildCacheKey({
51-
schemaVersion: FOLD_HELPER_SCHEMA_VERSION,
52-
sourceHash: input.sourceHash,
53-
toolchain: input.toolchain,
54-
compileArgv: input.compileArgv,
55-
});
56-
}
24+
const FOLD_HELPER_BUILD_HINT =
25+
'Select an Xcode with the iOS simulator SDK and foldable HID support using DEVELOPER_DIR.';
5726

5827
/** Upper bound on a single fold-helper clang invocation; the same budget the prior per-call build used. */
5928
export const FOLD_HELPER_BUILD_TIMEOUT_MS = 30_000;
@@ -68,8 +37,9 @@ const FOLD_HELPER_PREPARATION_DEADLINE_MS =
6837
* call after the first serves a cached binary instead of recompiling `Fold.m`, and a `DEVELOPER_DIR`
6938
* switch busts the cache instead of serving a binary built against a different SDK (#2796).
7039
*
71-
* Failures surface as `AppError('COMMAND_FAILED', ..., {reason: 'fold-helper-build-failed'})`, the
72-
* error shape `sendSimulatorFoldPose` reported before this cache existed.
40+
* Build and cache failures surface as `AppError('COMMAND_FAILED', ..., {reason:
41+
* 'fold-helper-build-failed'})`, the error shape `sendSimulatorFoldPose` reported before this cache
42+
* existed, carrying the underlying failure's hint and details.
7343
*/
7444
export async function ensureFoldHelperBinary(
7545
input: Readonly<{
@@ -90,27 +60,21 @@ export async function ensureFoldHelperBinary(
9060
deadline,
9161
);
9262
const toolchain = await readHostToolchainIdentity(host, deadline);
93-
const compileArgv = buildFoldHelperCompileArgv({ sourceRoot: '', outputPath: '' });
94-
const cacheKey = foldHelperCacheKey({ sourceHash, toolchain, compileArgv });
9563
const cacheRoot =
9664
input.cacheRoot ?? path.join(host.homeDirectory(), '.agent-device', 'fold-helper');
97-
const manifest = {
98-
schemaVersion: FOLD_HELPER_SCHEMA_VERSION,
99-
sourceHash,
100-
cacheKey,
101-
toolchain,
102-
compileArgv,
103-
};
10465
return await ensureNativeBuildCacheEntry({
10566
host,
10667
deadline,
10768
lockDescription: FOLD_HELPER_LOCK_DESCRIPTION,
10869
cacheRoot,
109-
cacheKey,
11070
binaryFilename: FOLD_HELPER_BINARY_FILENAME,
111-
manifest,
112-
manifestMatches: (candidate) =>
113-
nativeBuildManifestFieldsMatch(candidate, manifest, MANIFEST_FIELDS),
71+
keyInputs: {
72+
schemaVersion: FOLD_HELPER_SCHEMA_VERSION,
73+
sourceHash,
74+
toolchain,
75+
// Placeholder paths keep the key independent of the install location and build directory.
76+
compileArgv: buildFoldHelperCompileArgv({ sourceRoot: '', outputPath: '' }),
77+
},
11478
build: (outputPath) => compileFoldHelper(host, deadline, sourceRoot, outputPath),
11579
});
11680
} catch (error) {
@@ -165,38 +129,28 @@ async function compileFoldHelper(
165129
deadline,
166130
argv: buildFoldHelperCompileArgv({ sourceRoot, outputPath }),
167131
budgetMs: FOLD_HELPER_BUILD_TIMEOUT_MS,
168-
deadlineReason: 'fold-helper-build-deadline',
169132
label: 'fold helper',
170133
});
171134
if (result.exitCode !== 0 || !host.exists(outputPath)) {
172-
throw new AppError(
173-
'COMMAND_FAILED',
174-
'Unable to build the simulator fold helper',
175-
execFailureDetails(result, {
176-
reason: 'fold-helper-build-failed',
177-
hint: 'Select an Xcode with the iOS simulator SDK and foldable HID support using DEVELOPER_DIR.',
178-
}),
179-
);
135+
throw foldHelperBuildFailed(execFailureDetails(result));
180136
}
181137
}
182138

183139
/**
184-
* `AppError('COMMAND_FAILED', ..., {reason: 'fold-helper-build-failed'})` for every cache failure,
185-
* matching the error `sendSimulatorFoldPose` reported before this cache existed, except a genuine
186-
* cancellation: `compileFoldHelper` already throws that exact shape on a build failure, so it
187-
* passes through unchanged.
140+
* Rewraps a cache failure as the fold helper's build error, keeping its hint and typed details; a
141+
* cancellation, and any error that is not a snapshot-source failure, passes through unchanged.
188142
*/
189143
function asFoldHelperCacheError(error: unknown): unknown {
190-
if (!(error instanceof SnapshotSourceError)) return error;
191-
if (error.failureKind === 'cancelled') return error;
144+
if (!(error instanceof SnapshotSourceError) || error.failureKind === 'cancelled') return error;
145+
const { bridgeFailure: _kind, bridgeFailureCode: cause, ...details } = error.details ?? {};
146+
return foldHelperBuildFailed({ ...details, cause }, error);
147+
}
148+
149+
function foldHelperBuildFailed(details: Readonly<Record<string, unknown>>, cause?: unknown) {
192150
return new AppError(
193151
'COMMAND_FAILED',
194152
'Unable to build the simulator fold helper',
195-
{
196-
reason: 'fold-helper-build-failed',
197-
hint: 'Select an Xcode with the iOS simulator SDK and foldable HID support using DEVELOPER_DIR.',
198-
cause: error.failureCode,
199-
},
200-
error,
153+
{ hint: FOLD_HELPER_BUILD_HINT, ...details, reason: 'fold-helper-build-failed' },
154+
cause,
201155
);
202156
}

0 commit comments

Comments
 (0)