|
1 | 1 | import { createHash } from 'node:crypto'; |
2 | 2 | import path from 'node:path'; |
| 3 | +import { withProcessLock } from '@agent-device/host-kit/file'; |
3 | 4 | import { SnapshotSourceError, snapshotSourceError } from './errors.ts'; |
4 | 5 | import { remainingSnapshotSourceMs, type SnapshotSourceDeadline } from './deadline.ts'; |
5 | 6 | import { |
@@ -61,103 +62,101 @@ export async function ensureSnapshotBridgeBinary( |
61 | 62 | const cacheRoot = |
62 | 63 | input.cacheRoot ?? path.join(input.host.homeDirectory(), '.agent-device', 'snapshot-source'); |
63 | 64 | const entryPath = path.join(cacheRoot, cacheKey); |
64 | | - const releaseLock = await input.host.acquireLock(path.join(cacheRoot, `${cacheKey}.lock`), { |
65 | | - deadline, |
66 | | - }); |
67 | | - try { |
68 | | - const cached = await readValidCache( |
69 | | - input.host, |
70 | | - entryPath, |
71 | | - { |
72 | | - sourceHash, |
73 | | - cacheKey, |
74 | | - toolchain, |
75 | | - }, |
76 | | - deadline, |
77 | | - ); |
78 | | - if (cached) return cached; |
79 | | - remainingSnapshotSourceMs(deadline, 'native-build-deadline'); |
80 | | - if (input.host.exists(entryPath)) await input.host.remove(entryPath); |
81 | | - |
82 | | - remainingSnapshotSourceMs(deadline, 'native-build-deadline'); |
83 | | - await input.host.ensureDirectory(cacheRoot); |
84 | | - const temporaryPath = path.join(cacheRoot, `.${cacheKey}.${input.host.processId()}.tmp`); |
85 | | - remainingSnapshotSourceMs(deadline, 'native-build-deadline'); |
86 | | - await input.host.remove(temporaryPath); |
87 | | - try { |
88 | | - remainingSnapshotSourceMs(deadline, 'native-build-deadline'); |
89 | | - await input.host.ensureDirectory(temporaryPath); |
90 | | - const outputPath = path.join(temporaryPath, BRIDGE_FILENAME); |
91 | | - const result = await input.host.run( |
92 | | - 'xcrun', |
93 | | - [ |
94 | | - '--sdk', |
95 | | - 'iphonesimulator', |
96 | | - 'clang', |
97 | | - '-arch', |
98 | | - toolchain.architecture, |
99 | | - '-mios-simulator-version-min=15.0', |
100 | | - '-fobjc-arc', |
101 | | - '-Werror', |
102 | | - '-Wall', |
103 | | - '-Wextra', |
104 | | - '-framework', |
105 | | - 'Foundation', |
106 | | - '-framework', |
107 | | - 'CoreGraphics', |
108 | | - ...SNAPSHOT_BRIDGE_COMPILE_FILENAMES.map((sourceFile) => |
109 | | - path.join(sourceRoot, sourceFile), |
110 | | - ), |
111 | | - '-o', |
112 | | - outputPath, |
113 | | - ], |
| 65 | + return await withProcessLock({ |
| 66 | + acquire: () => input.host.acquireLock(path.join(cacheRoot, `${cacheKey}.lock`), { deadline }), |
| 67 | + task: async () => { |
| 68 | + const cached = await readValidCache( |
| 69 | + input.host, |
| 70 | + entryPath, |
114 | 71 | { |
115 | | - signal: deadline.signal, |
116 | | - timeoutMs: Math.min( |
117 | | - BUILD_TIMEOUT_MS, |
118 | | - remainingSnapshotSourceMs(deadline, 'native-build-deadline'), |
119 | | - ), |
120 | | - allowFailure: true, |
| 72 | + sourceHash, |
| 73 | + cacheKey, |
| 74 | + toolchain, |
121 | 75 | }, |
| 76 | + deadline, |
122 | 77 | ); |
123 | | - if (result.exitCode !== 0 || !input.host.exists(outputPath)) { |
124 | | - throw snapshotSourceError('unsupported', 'native-build-failed', { |
125 | | - exitCode: result.exitCode, |
126 | | - stderr: result.stderr.slice(0, 4096), |
127 | | - }); |
128 | | - } |
| 78 | + if (cached) return cached; |
129 | 79 | remainingSnapshotSourceMs(deadline, 'native-build-deadline'); |
130 | | - await input.host.chmod(outputPath, 0o755); |
131 | | - const binarySha256 = await sha256File(input.host, outputPath, deadline); |
132 | | - const manifest: SnapshotBridgeCacheManifest = { |
133 | | - schemaVersion: CACHE_SCHEMA_VERSION, |
134 | | - protocolVersion: SNAPSHOT_SOURCE_PROTOCOL_VERSION, |
135 | | - sourceVersion: SNAPSHOT_SOURCE_VERSION, |
136 | | - sourceHash, |
137 | | - cacheKey, |
138 | | - toolchain, |
139 | | - binarySha256, |
140 | | - }; |
141 | | - await input.host.writeText( |
142 | | - path.join(temporaryPath, MANIFEST_FILENAME), |
143 | | - `${JSON.stringify(manifest, null, 2)}\n`, |
144 | | - ); |
| 80 | + if (input.host.exists(entryPath)) await input.host.remove(entryPath); |
| 81 | + |
| 82 | + remainingSnapshotSourceMs(deadline, 'native-build-deadline'); |
| 83 | + await input.host.ensureDirectory(cacheRoot); |
| 84 | + const temporaryPath = path.join(cacheRoot, `.${cacheKey}.${input.host.processId()}.tmp`); |
145 | 85 | remainingSnapshotSourceMs(deadline, 'native-build-deadline'); |
146 | | - await input.host.rename(temporaryPath, entryPath); |
147 | | - return { |
148 | | - path: path.join(entryPath, BRIDGE_FILENAME), |
149 | | - sourceHash, |
150 | | - cacheKey, |
151 | | - protocolVersion: SNAPSHOT_SOURCE_PROTOCOL_VERSION, |
152 | | - sourceVersion: SNAPSHOT_SOURCE_VERSION, |
153 | | - }; |
154 | | - } catch (error) { |
155 | 86 | await input.host.remove(temporaryPath); |
156 | | - throw error; |
157 | | - } |
158 | | - } finally { |
159 | | - await releaseLock(); |
160 | | - } |
| 87 | + try { |
| 88 | + remainingSnapshotSourceMs(deadline, 'native-build-deadline'); |
| 89 | + await input.host.ensureDirectory(temporaryPath); |
| 90 | + const outputPath = path.join(temporaryPath, BRIDGE_FILENAME); |
| 91 | + const result = await input.host.run( |
| 92 | + 'xcrun', |
| 93 | + [ |
| 94 | + '--sdk', |
| 95 | + 'iphonesimulator', |
| 96 | + 'clang', |
| 97 | + '-arch', |
| 98 | + toolchain.architecture, |
| 99 | + '-mios-simulator-version-min=15.0', |
| 100 | + '-fobjc-arc', |
| 101 | + '-Werror', |
| 102 | + '-Wall', |
| 103 | + '-Wextra', |
| 104 | + '-framework', |
| 105 | + 'Foundation', |
| 106 | + '-framework', |
| 107 | + 'CoreGraphics', |
| 108 | + ...SNAPSHOT_BRIDGE_COMPILE_FILENAMES.map((sourceFile) => |
| 109 | + path.join(sourceRoot, sourceFile), |
| 110 | + ), |
| 111 | + '-o', |
| 112 | + outputPath, |
| 113 | + ], |
| 114 | + { |
| 115 | + signal: deadline.signal, |
| 116 | + timeoutMs: Math.min( |
| 117 | + BUILD_TIMEOUT_MS, |
| 118 | + remainingSnapshotSourceMs(deadline, 'native-build-deadline'), |
| 119 | + ), |
| 120 | + allowFailure: true, |
| 121 | + }, |
| 122 | + ); |
| 123 | + if (result.exitCode !== 0 || !input.host.exists(outputPath)) { |
| 124 | + throw snapshotSourceError('unsupported', 'native-build-failed', { |
| 125 | + exitCode: result.exitCode, |
| 126 | + stderr: result.stderr.slice(0, 4096), |
| 127 | + }); |
| 128 | + } |
| 129 | + remainingSnapshotSourceMs(deadline, 'native-build-deadline'); |
| 130 | + await input.host.chmod(outputPath, 0o755); |
| 131 | + const binarySha256 = await sha256File(input.host, outputPath, deadline); |
| 132 | + const manifest: SnapshotBridgeCacheManifest = { |
| 133 | + schemaVersion: CACHE_SCHEMA_VERSION, |
| 134 | + protocolVersion: SNAPSHOT_SOURCE_PROTOCOL_VERSION, |
| 135 | + sourceVersion: SNAPSHOT_SOURCE_VERSION, |
| 136 | + sourceHash, |
| 137 | + cacheKey, |
| 138 | + toolchain, |
| 139 | + binarySha256, |
| 140 | + }; |
| 141 | + await input.host.writeText( |
| 142 | + path.join(temporaryPath, MANIFEST_FILENAME), |
| 143 | + `${JSON.stringify(manifest, null, 2)}\n`, |
| 144 | + ); |
| 145 | + remainingSnapshotSourceMs(deadline, 'native-build-deadline'); |
| 146 | + await input.host.rename(temporaryPath, entryPath); |
| 147 | + return { |
| 148 | + path: path.join(entryPath, BRIDGE_FILENAME), |
| 149 | + sourceHash, |
| 150 | + cacheKey, |
| 151 | + protocolVersion: SNAPSHOT_SOURCE_PROTOCOL_VERSION, |
| 152 | + sourceVersion: SNAPSHOT_SOURCE_VERSION, |
| 153 | + }; |
| 154 | + } catch (error) { |
| 155 | + await input.host.remove(temporaryPath); |
| 156 | + throw error; |
| 157 | + } |
| 158 | + }, |
| 159 | + }); |
161 | 160 | } |
162 | 161 |
|
163 | 162 | function resolveSnapshotBridgeSourceRoot(host: SnapshotSourceHost): string { |
|
0 commit comments