@geastack/apple 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +54 -0
  3. package/dist/index.d.ts +127 -0
  4. package/dist/index.js +3076 -0
  5. package/generated/AVFoundation.d.ts +112 -0
  6. package/generated/ActivityKit.d.ts +7 -0
  7. package/generated/AppKit.d.ts +268 -0
  8. package/generated/CoreGraphics.d.ts +18 -0
  9. package/generated/CoreLocation.d.ts +28 -0
  10. package/generated/CoreMedia.d.ts +8 -0
  11. package/generated/Dispatch.d.ts +2 -0
  12. package/generated/Foundation.d.ts +13 -0
  13. package/generated/MapKit.d.ts +28 -0
  14. package/generated/Metal.d.ts +91 -0
  15. package/generated/MetalKit.d.ts +22 -0
  16. package/generated/Photos.d.ts +5 -0
  17. package/generated/QuartzCore.d.ts +12 -0
  18. package/generated/SwiftData.d.ts +11 -0
  19. package/generated/UIKit.d.ts +187 -0
  20. package/package.json +123 -0
  21. package/runtime/AVFoundation.js +136 -0
  22. package/runtime/ActivityKit.js +18 -0
  23. package/runtime/AppKit.js +313 -0
  24. package/runtime/CoreGraphics.js +15 -0
  25. package/runtime/CoreLocation.js +30 -0
  26. package/runtime/CoreMedia.js +7 -0
  27. package/runtime/Dispatch.js +11 -0
  28. package/runtime/Foundation.js +21 -0
  29. package/runtime/MapKit.js +22 -0
  30. package/runtime/Metal.js +126 -0
  31. package/runtime/MetalKit.js +15 -0
  32. package/runtime/Photos.js +9 -0
  33. package/runtime/QuartzCore.js +9 -0
  34. package/runtime/SwiftData.js +29 -0
  35. package/runtime/UIKit.js +242 -0
  36. package/runtime/empty.js +1 -0
  37. package/targets/ios/Info.plist.in +25 -0
  38. package/targets/ios/README.md +46 -0
  39. package/targets/ios/build-ios.sh +395 -0
  40. package/targets/ios/detect-development-team.mjs +109 -0
  41. package/targets/ios/generate-resident-entry.mjs +67 -0
  42. package/targets/ios/generate-resident-registry.mjs +162 -0
  43. package/targets/ios/generate-xcode-project.mjs +520 -0
  44. package/targets/ios/main/canvas_view.h +7 -0
  45. package/targets/ios/main/canvas_view.mm +94 -0
  46. package/targets/ios/main/font_registry.h +17 -0
  47. package/targets/ios/main/font_registry.mm +129 -0
  48. package/targets/ios/main/image_bridge.h +13 -0
  49. package/targets/ios/main/image_bridge.mm +53 -0
  50. package/targets/ios/main/ios_apps.c +28 -0
  51. package/targets/ios/main/ios_camera.mm +446 -0
  52. package/targets/ios/main/ios_display.mm +344 -0
  53. package/targets/ios/main/ios_main.mm +811 -0
  54. package/targets/ios/main/ios_memory.cpp +55 -0
  55. package/targets/ios/main/ios_renderer.h +20 -0
  56. package/targets/ios/main/ios_renderer.mm +27 -0
  57. package/targets/ios/main/ios_root_background.cpp +20 -0
  58. package/targets/ios/main/ios_root_background.h +11 -0
  59. package/targets/ios/main/ios_sensors.mm +244 -0
  60. package/targets/ios/main/ios_timers.mm +53 -0
  61. package/targets/ios/main/renderer/ios_renderer_internal.h +65 -0
  62. package/targets/ios/main/renderer/ios_renderer_support.mm +111 -0
  63. package/targets/ios/main/renderer/native_button.mm +179 -0
  64. package/targets/ios/main/renderer/native_label.mm +161 -0
  65. package/targets/ios/main/renderer/native_scroll_container.mm +279 -0
  66. package/targets/ios/main/renderer/view_reconciler.mm +274 -0
  67. package/targets/macos/Info.plist.in +26 -0
  68. package/targets/macos/README.md +131 -0
  69. package/targets/macos/build-child-lifecycle.sh +119 -0
  70. package/targets/macos/build-macos.sh +2156 -0
  71. package/targets/macos/check-module-graph-freshness.mjs +82 -0
  72. package/targets/macos/compiler-input-fingerprint.mjs +32 -0
  73. package/targets/macos/finalize-apple-native-output.mjs +77 -0
  74. package/targets/macos/generate-resident-entry.mjs +80 -0
  75. package/targets/macos/generate-resident-registry.mjs +141 -0
  76. package/targets/macos/heavy-build-lock.sh +176 -0
  77. package/targets/macos/main/canvas_view.h +9 -0
  78. package/targets/macos/main/canvas_view.mm +176 -0
  79. package/targets/macos/main/color_convert.h +18 -0
  80. package/targets/macos/main/color_convert.mm +39 -0
  81. package/targets/macos/main/font_registry.h +35 -0
  82. package/targets/macos/main/font_registry.mm +350 -0
  83. package/targets/macos/main/image_bridge.h +16 -0
  84. package/targets/macos/main/image_bridge.mm +52 -0
  85. package/targets/macos/main/macos_app_platform.mm +95 -0
  86. package/targets/macos/main/macos_apps.c +31 -0
  87. package/targets/macos/main/macos_device_control.mm +56 -0
  88. package/targets/macos/main/macos_display.mm +484 -0
  89. package/targets/macos/main/macos_host_device_control.mm +95 -0
  90. package/targets/macos/main/macos_main.mm +839 -0
  91. package/targets/macos/main/macos_memory.cpp +31 -0
  92. package/targets/macos/main/macos_native_shell.h +31 -0
  93. package/targets/macos/main/macos_native_shell.mm +396 -0
  94. package/targets/macos/main/macos_network.mm +253 -0
  95. package/targets/macos/main/macos_renderer.h +39 -0
  96. package/targets/macos/main/macos_renderer.mm +1969 -0
  97. package/targets/macos/main/macos_sensors.cpp +148 -0
  98. package/targets/macos/main/macos_smoke_app.cpp +65 -0
  99. package/targets/macos/main/macos_timers.mm +52 -0
  100. package/targets/macos/main/press_bridge.h +21 -0
  101. package/targets/macos/main/press_bridge.mm +72 -0
  102. package/targets/macos/main/thermalright_hid_display.h +14 -0
  103. package/targets/macos/main/thermalright_hid_display.mm +995 -0
  104. package/targets/macos/native-artifact-provenance.mjs +421 -0
  105. package/targets/macos/order-compile-edges.mjs +64 -0
  106. package/targets/macos/preflight-hard-muted-artifact.mjs +93 -0
  107. package/targets/macos/prune-generated-modules.mjs +147 -0
  108. package/targets/macos/sync-resources.mjs +153 -0
  109. package/targets/shared/analyze-audio-capture.mjs +131 -0
  110. package/targets/shared/apple_audio.mm +1194 -0
  111. package/targets/shared/apple_audio_selftest.mm +602 -0
  112. package/targets/shared/apple_audio_testing.h +34 -0
  113. package/targets/shared/build-audio-selftest.sh +50 -0
@@ -0,0 +1,421 @@
1
+ import crypto from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { spawnSync } from 'node:child_process'
5
+
6
+ // This module only inspects files, code-signing metadata, and Git metadata. It
7
+ // deliberately has no facility for executing the selected native artifact.
8
+ export const HARD_MUTE_PROVENANCE_SCHEMA = 'gea.apple.native-hard-mute-provenance/v1'
9
+
10
+ export class ArtifactProvenanceError extends Error {
11
+ constructor(code, detail = '') {
12
+ super(detail ? `${code}: ${detail}` : code)
13
+ this.name = 'ArtifactProvenanceError'
14
+ this.code = code
15
+ this.detail = detail
16
+ }
17
+ }
18
+
19
+ function fail(code, detail = '') {
20
+ throw new ArtifactProvenanceError(code, detail)
21
+ }
22
+
23
+ function requireString(value, field) {
24
+ if (typeof value !== 'string' || value.length === 0) fail('invalid-manifest', `${field} must be a non-empty string`)
25
+ return value
26
+ }
27
+
28
+ function requireObject(value, field) {
29
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
30
+ fail('invalid-manifest', `${field} must be an object`)
31
+ }
32
+ return value
33
+ }
34
+
35
+ function canonicalDirectory(directory, code = 'directory-missing') {
36
+ let canonical
37
+ try {
38
+ canonical = fs.realpathSync(directory)
39
+ } catch {
40
+ fail(code, directory)
41
+ }
42
+ if (!fs.statSync(canonical).isDirectory()) fail(code, directory)
43
+ return canonical
44
+ }
45
+
46
+ function canonicalFile(file, code = 'file-missing') {
47
+ let canonical
48
+ try {
49
+ canonical = fs.realpathSync(file)
50
+ } catch {
51
+ fail(code, file)
52
+ }
53
+ if (!fs.statSync(canonical).isFile()) fail(code, file)
54
+ return canonical
55
+ }
56
+
57
+ export function sha256File(file) {
58
+ const hash = crypto.createHash('sha256')
59
+ const descriptor = fs.openSync(file, 'r')
60
+ const buffer = Buffer.allocUnsafe(1024 * 1024)
61
+ try {
62
+ while (true) {
63
+ const read = fs.readSync(descriptor, buffer, 0, buffer.length, null)
64
+ if (read === 0) break
65
+ hash.update(buffer.subarray(0, read))
66
+ }
67
+ } finally {
68
+ fs.closeSync(descriptor)
69
+ }
70
+ return hash.digest('hex')
71
+ }
72
+
73
+ function countBufferOccurrences(file, literal) {
74
+ const needle = Buffer.from(literal, 'utf8')
75
+ if (needle.length === 0) fail('invalid-gate-literal')
76
+ const haystack = fs.readFileSync(file)
77
+ let count = 0
78
+ let offset = 0
79
+ while (offset <= haystack.length - needle.length) {
80
+ const found = haystack.indexOf(needle, offset)
81
+ if (found < 0) break
82
+ count += 1
83
+ offset = found + needle.length
84
+ }
85
+ return count
86
+ }
87
+
88
+ function countExactCStringOccurrences(file, literal) {
89
+ const expected = Buffer.from(literal, 'utf8')
90
+ const contents = fs.readFileSync(file)
91
+ let count = 0
92
+ let start = 0
93
+ while (start < contents.length) {
94
+ while (start < contents.length && (contents[start] < 0x20 || contents[start] > 0x7e)) start += 1
95
+ let end = start
96
+ while (end < contents.length && contents[end] >= 0x20 && contents[end] <= 0x7e) end += 1
97
+ if (end - start === expected.length && contents.subarray(start, end).equals(expected)) count += 1
98
+ start = end + 1
99
+ }
100
+ return count
101
+ }
102
+
103
+ function requireSingleGateLiteral(file, literal, role, exactCString = false) {
104
+ const count = exactCString
105
+ ? countExactCStringOccurrences(file, literal)
106
+ : countBufferOccurrences(file, literal)
107
+ if (count !== 1) fail(`${role}-hard-mute-gate-count`, `expected 1, found ${count}: ${file}`)
108
+ return count
109
+ }
110
+
111
+ function runGit(directory, args, code) {
112
+ const result = spawnSync('git', ['-C', directory, ...args], {
113
+ encoding: 'utf8',
114
+ stdio: ['ignore', 'pipe', 'pipe'],
115
+ })
116
+ if (result.status !== 0) fail(code, result.stderr.trim() || `${directory}: git ${args.join(' ')}`)
117
+ return result.stdout.trim()
118
+ }
119
+
120
+ function repositoryForPath(target) {
121
+ const stat = fs.statSync(target)
122
+ const directory = stat.isDirectory() ? target : path.dirname(target)
123
+ const root = canonicalDirectory(runGit(directory, ['rev-parse', '--show-toplevel'], 'dependency-repository-missing'))
124
+ const revision = runGit(root, ['rev-parse', 'HEAD'], 'dependency-revision-missing')
125
+ return { root, revision }
126
+ }
127
+
128
+ function trackedFileRecord(file, role) {
129
+ const repository = repositoryForPath(file)
130
+ const relativePath = path.relative(repository.root, file).split(path.sep).join('/')
131
+ if (relativePath.startsWith('../') || path.isAbsolute(relativePath)) {
132
+ fail(`${role}-outside-dependency-repository`, file)
133
+ }
134
+ const committedBlob = runGit(
135
+ repository.root,
136
+ ['rev-parse', `${repository.revision}:${relativePath}`],
137
+ `${role}-not-tracked-at-dependency-revision`,
138
+ )
139
+ const workingBlob = runGit(repository.root, ['hash-object', file], `${role}-hash-object-failed`)
140
+ if (workingBlob !== committedBlob) {
141
+ fail(`${role}-does-not-match-dependency-revision`, `${relativePath} at ${repository.revision}`)
142
+ }
143
+ return { ...repository, relativePath, blob: committedBlob }
144
+ }
145
+
146
+ function fileRecord(file) {
147
+ return { path: file, sha256: sha256File(file) }
148
+ }
149
+
150
+ function tokenizeMakeDependencies(text) {
151
+ const collapsed = text.replace(/\\\r?\n/g, ' ')
152
+ let colon = -1
153
+ let escaped = false
154
+ for (let index = 0; index < collapsed.length; index += 1) {
155
+ const character = collapsed[index]
156
+ if (escaped) {
157
+ escaped = false
158
+ continue
159
+ }
160
+ if (character === '\\') {
161
+ escaped = true
162
+ continue
163
+ }
164
+ if (character === ':') {
165
+ colon = index
166
+ break
167
+ }
168
+ }
169
+ if (colon < 0) fail('invalid-depfile', 'missing target separator')
170
+
171
+ const dependencies = []
172
+ let token = ''
173
+ escaped = false
174
+ for (const character of collapsed.slice(colon + 1)) {
175
+ if (escaped) {
176
+ token += character
177
+ escaped = false
178
+ continue
179
+ }
180
+ if (character === '\\') {
181
+ escaped = true
182
+ continue
183
+ }
184
+ if (/\s/.test(character)) {
185
+ if (token.length > 0) {
186
+ dependencies.push(token)
187
+ token = ''
188
+ }
189
+ continue
190
+ }
191
+ token += character
192
+ }
193
+ if (escaped) token += '\\'
194
+ if (token.length > 0) dependencies.push(token)
195
+ return dependencies
196
+ }
197
+
198
+ function dependencyPaths(depfile, workingDirectory) {
199
+ return tokenizeMakeDependencies(fs.readFileSync(depfile, 'utf8')).map((dependency) =>
200
+ path.normalize(path.isAbsolute(dependency) ? dependency : path.resolve(workingDirectory, dependency)),
201
+ )
202
+ }
203
+
204
+ function walkDepfiles(directory) {
205
+ const depfiles = []
206
+ const visit = (current) => {
207
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
208
+ const full = path.join(current, entry.name)
209
+ if (entry.isDirectory()) visit(full)
210
+ else if (entry.isFile() && entry.name.endsWith('.o.d')) depfiles.push(full)
211
+ }
212
+ }
213
+ visit(directory)
214
+ return depfiles.sort()
215
+ }
216
+
217
+ function linkedObjects(linkSignature) {
218
+ const objects = new Set()
219
+ for (const line of fs.readFileSync(linkSignature, 'utf8').split(/\r?\n/)) {
220
+ if (!line.startsWith('object=')) continue
221
+ const candidate = line.slice('object='.length)
222
+ if (candidate.length === 0) continue
223
+ objects.add(path.normalize(path.isAbsolute(candidate) ? candidate : path.resolve(path.dirname(linkSignature), candidate)))
224
+ }
225
+ return objects
226
+ }
227
+
228
+ function findSelectedObject({ buildDirectory, source, linkSignature, workingDirectory }) {
229
+ const canonicalSource = canonicalFile(source, 'hard-mute-source-missing')
230
+ const linked = linkedObjects(linkSignature)
231
+ const matches = []
232
+ for (const depfileCandidate of walkDepfiles(buildDirectory)) {
233
+ const dependencies = dependencyPaths(depfileCandidate, workingDirectory)
234
+ let includesSource = false
235
+ for (const dependency of dependencies) {
236
+ try {
237
+ if (fs.realpathSync(dependency) === canonicalSource) {
238
+ includesSource = true
239
+ break
240
+ }
241
+ } catch {
242
+ // An unrelated removed dependency is not the selected source.
243
+ }
244
+ }
245
+ if (!includesSource) continue
246
+ const objectCandidate = path.normalize(depfileCandidate.slice(0, -2))
247
+ if (!linked.has(objectCandidate)) continue
248
+ matches.push({ depfile: depfileCandidate, object: objectCandidate, dependencies })
249
+ }
250
+ if (matches.length === 0) fail('hard-mute-source-not-linked', canonicalSource)
251
+ if (matches.length !== 1) fail('hard-mute-source-linked-by-multiple-objects', matches.map(({ object }) => object).join(', '))
252
+ const match = matches[0]
253
+ const object = canonicalFile(match.object, 'hard-mute-object-missing')
254
+ const depfile = canonicalFile(match.depfile, 'hard-mute-depfile-missing')
255
+ const translationUnitCandidate = match.dependencies.find((dependency) => fs.existsSync(dependency) && fs.statSync(dependency).isFile())
256
+ if (!translationUnitCandidate) fail('translation-unit-missing', depfile)
257
+ const translationUnit = canonicalFile(translationUnitCandidate, 'translation-unit-missing')
258
+ return { object, depfile, translationUnit }
259
+ }
260
+
261
+ export function defaultCodeSignatureVerifier(executable) {
262
+ const result = spawnSync('/usr/bin/codesign', ['--verify', '--strict', executable], {
263
+ encoding: 'utf8',
264
+ stdio: ['ignore', 'pipe', 'pipe'],
265
+ })
266
+ if (result.status !== 0) fail('invalid-code-signature', result.stderr.trim() || executable)
267
+ }
268
+
269
+ function dependencyRecords(paths) {
270
+ const byRoot = new Map()
271
+ for (const candidate of paths) {
272
+ const canonical = canonicalDirectory(candidate, 'dependency-root-missing')
273
+ const record = repositoryForPath(canonical)
274
+ byRoot.set(record.root, record)
275
+ }
276
+ return [...byRoot.values()].sort((left, right) => left.root.localeCompare(right.root))
277
+ }
278
+
279
+ export function createHardMuteArtifactManifest(options) {
280
+ const gate = requireString(options.gate, 'gate')
281
+ if (!/^[\x20-\x7e]+$/.test(gate)) fail('invalid-gate-literal', 'gate must contain printable ASCII only')
282
+ const executable = canonicalFile(options.executable, 'executable-missing')
283
+ const source = canonicalFile(options.source, 'hard-mute-source-missing')
284
+ const linkSignature = canonicalFile(options.linkSignature, 'link-signature-missing')
285
+ const buildDirectory = canonicalDirectory(options.buildDirectory ?? path.dirname(linkSignature), 'build-directory-missing')
286
+ const workingDirectory = canonicalDirectory(options.workingDirectory ?? process.cwd(), 'build-working-directory-missing')
287
+ const verifyCodeSignature = options.verifyCodeSignature ?? defaultCodeSignatureVerifier
288
+
289
+ verifyCodeSignature(executable)
290
+ requireSingleGateLiteral(source, gate, 'source')
291
+ const selected = findSelectedObject({ buildDirectory, source, linkSignature, workingDirectory })
292
+ requireSingleGateLiteral(selected.object, gate, 'object', true)
293
+ requireSingleGateLiteral(executable, gate, 'executable', true)
294
+
295
+ const sourceGit = trackedFileRecord(source, 'hard-mute-source')
296
+ const translationUnitGit = trackedFileRecord(selected.translationUnit, 'translation-unit')
297
+ const dependencyRoots = [
298
+ sourceGit.root,
299
+ translationUnitGit.root,
300
+ ...(options.dependencyRoots ?? []),
301
+ ]
302
+
303
+ return {
304
+ schema: HARD_MUTE_PROVENANCE_SCHEMA,
305
+ gate,
306
+ buildWorkingDirectory: workingDirectory,
307
+ hardMuteSource: { ...fileRecord(source), git: sourceGit },
308
+ translationUnit: { ...fileRecord(selected.translationUnit), git: translationUnitGit },
309
+ object: fileRecord(selected.object),
310
+ depfile: fileRecord(selected.depfile),
311
+ linkSignature: fileRecord(linkSignature),
312
+ executable: fileRecord(executable),
313
+ dependencies: dependencyRecords(dependencyRoots),
314
+ codeSignatureVerified: true,
315
+ }
316
+ }
317
+
318
+ export function writeManifestAtomic(manifestPath, manifest) {
319
+ const destination = path.resolve(manifestPath)
320
+ fs.mkdirSync(path.dirname(destination), { recursive: true })
321
+ const temporary = `${destination}.tmp.${process.pid}`
322
+ fs.writeFileSync(temporary, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' })
323
+ try {
324
+ fs.renameSync(temporary, destination)
325
+ } catch (error) {
326
+ fs.rmSync(temporary, { force: true })
327
+ throw error
328
+ }
329
+ return destination
330
+ }
331
+
332
+ function assertFileHash(record, role) {
333
+ const value = requireObject(record, role)
334
+ const file = canonicalFile(requireString(value.path, `${role}.path`), `${role}-missing`)
335
+ const expected = requireString(value.sha256, `${role}.sha256`)
336
+ const actual = sha256File(file)
337
+ if (actual !== expected) fail(`${role}-sha256-mismatch`, `${actual} != ${expected}: ${file}`)
338
+ return file
339
+ }
340
+
341
+ function assertExpectedPath(actual, expected, role) {
342
+ const canonicalExpected = canonicalFile(expected, `${role}-missing`)
343
+ if (actual !== canonicalExpected) fail(`${role}-path-mismatch`, `${actual} != ${canonicalExpected}`)
344
+ }
345
+
346
+ function verifyGitFileRecord(file, record, role) {
347
+ const git = requireObject(record, `${role}.git`)
348
+ const current = trackedFileRecord(file, role)
349
+ for (const field of ['root', 'revision', 'relativePath', 'blob']) {
350
+ if (current[field] !== requireString(git[field], `${role}.git.${field}`)) {
351
+ fail(`${role}-git-${field}-mismatch`, `${current[field]} != ${git[field]}`)
352
+ }
353
+ }
354
+ }
355
+
356
+ export function verifyHardMuteArtifactManifest(options) {
357
+ const manifestPath = canonicalFile(options.manifestPath, 'manifest-missing')
358
+ let manifest
359
+ try {
360
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
361
+ } catch (error) {
362
+ fail('invalid-manifest', error.message)
363
+ }
364
+ requireObject(manifest, 'manifest')
365
+ if (manifest.schema !== HARD_MUTE_PROVENANCE_SCHEMA) fail('unsupported-manifest-schema', String(manifest.schema))
366
+ const expectedGate = requireString(options.gate, 'gate')
367
+ if (!/^[\x20-\x7e]+$/.test(expectedGate)) fail('invalid-gate-literal', 'gate must contain printable ASCII only')
368
+ if (manifest.gate !== expectedGate) fail('hard-mute-gate-mismatch', `${manifest.gate} != ${expectedGate}`)
369
+
370
+ const source = assertFileHash(manifest.hardMuteSource, 'hard-mute-source')
371
+ const translationUnit = assertFileHash(manifest.translationUnit, 'translation-unit')
372
+ const object = assertFileHash(manifest.object, 'object')
373
+ const depfile = assertFileHash(manifest.depfile, 'depfile')
374
+ const linkSignature = assertFileHash(manifest.linkSignature, 'link-signature')
375
+ const executable = assertFileHash(manifest.executable, 'executable')
376
+ assertExpectedPath(source, options.source, 'hard-mute-source')
377
+ assertExpectedPath(executable, options.executable, 'executable')
378
+
379
+ const workingDirectory = canonicalDirectory(
380
+ requireString(manifest.buildWorkingDirectory, 'buildWorkingDirectory'),
381
+ 'build-working-directory-missing',
382
+ )
383
+ const selected = findSelectedObject({
384
+ buildDirectory: path.dirname(linkSignature),
385
+ source,
386
+ linkSignature,
387
+ workingDirectory,
388
+ })
389
+ if (selected.object !== object) fail('linked-object-path-mismatch', `${selected.object} != ${object}`)
390
+ if (selected.depfile !== depfile) fail('depfile-path-mismatch', `${selected.depfile} != ${depfile}`)
391
+ if (selected.translationUnit !== translationUnit) {
392
+ fail('translation-unit-path-mismatch', `${selected.translationUnit} != ${translationUnit}`)
393
+ }
394
+
395
+ requireSingleGateLiteral(source, expectedGate, 'source')
396
+ requireSingleGateLiteral(object, expectedGate, 'object', true)
397
+ requireSingleGateLiteral(executable, expectedGate, 'executable', true)
398
+ verifyGitFileRecord(source, manifest.hardMuteSource.git, 'hard-mute-source')
399
+ verifyGitFileRecord(translationUnit, manifest.translationUnit.git, 'translation-unit')
400
+
401
+ if (!Array.isArray(manifest.dependencies) || manifest.dependencies.length === 0) {
402
+ fail('invalid-manifest', 'dependencies must be a non-empty array')
403
+ }
404
+ const seenRoots = new Set()
405
+ for (const dependency of manifest.dependencies) {
406
+ requireObject(dependency, 'dependency')
407
+ const root = canonicalDirectory(requireString(dependency.root, 'dependency.root'), 'dependency-root-missing')
408
+ if (seenRoots.has(root)) fail('invalid-manifest', `duplicate dependency root: ${root}`)
409
+ seenRoots.add(root)
410
+ const revision = runGit(root, ['rev-parse', 'HEAD'], 'dependency-revision-missing')
411
+ const expectedRevision = requireString(dependency.revision, 'dependency.revision')
412
+ if (revision !== expectedRevision) {
413
+ fail('dependency-revision-mismatch', `${root}: ${revision} != ${expectedRevision}`)
414
+ }
415
+ }
416
+
417
+ const verifyCodeSignature = options.verifyCodeSignature ?? defaultCodeSignatureVerifier
418
+ verifyCodeSignature(executable)
419
+ if (manifest.codeSignatureVerified !== true) fail('invalid-manifest', 'codeSignatureVerified must be true')
420
+ return manifest
421
+ }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { statSync } from 'node:fs'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { resolve } from 'node:path'
6
+
7
+ const VALID_KINDS = new Set(['c', 'cxx', 'mm'])
8
+
9
+ function compareText(left, right) {
10
+ return left < right ? -1 : left > right ? 1 : 0
11
+ }
12
+
13
+ export function parseCompileEdges(input) {
14
+ if (input === '') return []
15
+
16
+ const lines = input.endsWith('\n') ? input.slice(0, -1).split('\n') : input.split('\n')
17
+ return lines.map((line, index) => {
18
+ const separator = line.indexOf('\t')
19
+ if (separator <= 0 || separator !== line.lastIndexOf('\t')) {
20
+ throw new Error(`compile edge ${index + 1} must be one kind<TAB>source record`)
21
+ }
22
+
23
+ const kind = line.slice(0, separator)
24
+ const source = line.slice(separator + 1)
25
+ if (!VALID_KINDS.has(kind)) {
26
+ throw new Error(`compile edge ${index + 1} has unsupported kind: ${kind}`)
27
+ }
28
+ if (source === '') {
29
+ throw new Error(`compile edge ${index + 1} has an empty source path`)
30
+ }
31
+ return { kind, source }
32
+ })
33
+ }
34
+
35
+ export function orderCompileEdges(edges, sizeOf = source => statSync(source).size) {
36
+ return edges
37
+ .map((edge, inputIndex) => ({ ...edge, inputIndex, size: sizeOf(edge.source) }))
38
+ .sort((left, right) =>
39
+ right.size - left.size ||
40
+ compareText(left.source, right.source) ||
41
+ compareText(left.kind, right.kind) ||
42
+ left.inputIndex - right.inputIndex,
43
+ )
44
+ .map(({ kind, source, size }) => ({ kind, source, size }))
45
+ }
46
+
47
+ export function formatCompileEdges(edges) {
48
+ return edges.map(({ kind, source, size }) => `${kind}\t${size}\t${source}\n`).join('')
49
+ }
50
+
51
+ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
52
+ if (isMain) {
53
+ let input = ''
54
+ process.stdin.setEncoding('utf8')
55
+ process.stdin.on('data', chunk => { input += chunk })
56
+ process.stdin.on('end', () => {
57
+ try {
58
+ process.stdout.write(formatCompileEdges(orderCompileEdges(parseCompileEdges(input))))
59
+ } catch (error) {
60
+ process.stderr.write(`order-compile-edges: ${error.message}\n`)
61
+ process.exitCode = 1
62
+ }
63
+ })
64
+ }
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import {
5
+ ArtifactProvenanceError,
6
+ createHardMuteArtifactManifest,
7
+ verifyHardMuteArtifactManifest,
8
+ writeManifestAtomic,
9
+ } from './native-artifact-provenance.mjs'
10
+
11
+ function usage() {
12
+ process.stderr.write(
13
+ 'usage:\n' +
14
+ ' preflight-hard-muted-artifact.mjs record --manifest <file> --executable <file> --link-signature <file> --source <file> --gate <literal> [--build-working-directory <dir>] [--dependency-root <dir> ...]\n' +
15
+ ' preflight-hard-muted-artifact.mjs verify --manifest <file> --executable <file> --source <file> --gate <literal>\n',
16
+ )
17
+ }
18
+
19
+ function parseArguments(argv) {
20
+ const [command, ...rest] = argv
21
+ if (command !== 'record' && command !== 'verify') {
22
+ usage()
23
+ process.exitCode = 64
24
+ return null
25
+ }
26
+ const values = { command, dependencyRoots: [] }
27
+ for (let index = 0; index < rest.length; index += 1) {
28
+ const option = rest[index]
29
+ const value = rest[index + 1]
30
+ if (!option.startsWith('--') || value === undefined || value.startsWith('--')) {
31
+ usage()
32
+ process.exitCode = 64
33
+ return null
34
+ }
35
+ index += 1
36
+ switch (option) {
37
+ case '--manifest': values.manifestPath = value; break
38
+ case '--executable': values.executable = value; break
39
+ case '--link-signature': values.linkSignature = value; break
40
+ case '--source': values.source = value; break
41
+ case '--gate': values.gate = value; break
42
+ case '--build-working-directory': values.workingDirectory = value; break
43
+ case '--dependency-root': values.dependencyRoots.push(value); break
44
+ default:
45
+ usage()
46
+ process.exitCode = 64
47
+ return null
48
+ }
49
+ }
50
+ const required = command === 'record'
51
+ ? ['manifestPath', 'executable', 'linkSignature', 'source', 'gate']
52
+ : ['manifestPath', 'executable', 'source', 'gate']
53
+ if (required.some((field) => !values[field])) {
54
+ usage()
55
+ process.exitCode = 64
56
+ return null
57
+ }
58
+ return values
59
+ }
60
+
61
+ export function run(argv) {
62
+ const options = parseArguments(argv)
63
+ if (!options) return
64
+ try {
65
+ if (options.command === 'record') {
66
+ const manifest = createHardMuteArtifactManifest(options)
67
+ const manifestPath = writeManifestAtomic(options.manifestPath, manifest)
68
+ process.stdout.write(`PASS artifact-provenance-recorded=${path.resolve(manifestPath)}\n`)
69
+ process.stdout.write(`executable_sha256=${manifest.executable.sha256}\n`)
70
+ process.stdout.write(`object_sha256=${manifest.object.sha256}\n`)
71
+ process.stdout.write(`source_sha256=${manifest.hardMuteSource.sha256}\n`)
72
+ for (const dependency of manifest.dependencies) {
73
+ process.stdout.write(`dependency_revision=${dependency.root}@${dependency.revision}\n`)
74
+ }
75
+ return
76
+ }
77
+ const manifest = verifyHardMuteArtifactManifest(options)
78
+ process.stdout.write('PASS artifact-static-hard-mute-gate=present\n')
79
+ process.stdout.write(`executable_sha256=${manifest.executable.sha256}\n`)
80
+ process.stdout.write(`object_sha256=${manifest.object.sha256}\n`)
81
+ process.stdout.write(`source_sha256=${manifest.hardMuteSource.sha256}\n`)
82
+ } catch (error) {
83
+ if (error instanceof ArtifactProvenanceError) {
84
+ process.stderr.write(`FAIL reason=${error.code}${error.detail ? ` detail=${JSON.stringify(error.detail)}` : ''}\n`)
85
+ process.exitCode = 1
86
+ return
87
+ }
88
+ throw error
89
+ }
90
+ }
91
+
92
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
93
+ if (isMain) run(process.argv.slice(2))