peachsim 0.1.1625 → 0.1.1640

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 (169) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +15 -2
  3. package/cli/app-config.ts +18 -6
  4. package/cli/app-fonts.ts +37 -17
  5. package/cli/app-stubs.ts +284 -20
  6. package/cli/bridge-flow-runner.ts +66 -45
  7. package/cli/cloud-client.ts +60 -40
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/control.ts +283 -49
  10. package/cli/commands/doctor.ts +30 -189
  11. package/cli/commands/flow.ts +79 -11
  12. package/cli/commands/inspect/core.ts +10 -5
  13. package/cli/commands/inspect/settling.ts +14 -2
  14. package/cli/commands/inspect/shared.ts +9 -4
  15. package/cli/commands/inspect/wait-ready.ts +12 -0
  16. package/cli/commands/inspect.ts +91 -50
  17. package/cli/commands/install-desktop.ts +1 -0
  18. package/cli/commands/maestro-compare.ts +97 -38
  19. package/cli/commands/maestro.ts +9 -1
  20. package/cli/commands/preview.ts +1 -1
  21. package/cli/commands/record.ts +118 -48
  22. package/cli/commands/screenshot.ts +23 -1
  23. package/cli/commands/serve.ts +7 -2
  24. package/cli/commands/setup.ts +19 -42
  25. package/cli/commands/swift.ts +14 -3
  26. package/cli/commands/upload.ts +93 -48
  27. package/cli/desktop-companion.ts +102 -17
  28. package/cli/drivers/desktop.ts +14 -1
  29. package/cli/drivers/playwright-sim-host.ts +103 -45
  30. package/cli/drivers/playwright.ts +9 -8
  31. package/cli/drivers/types.ts +5 -0
  32. package/cli/maestro-accessibility.ts +1 -1
  33. package/cli/maestro-compare.ts +84 -12
  34. package/cli/main.ts +3 -2
  35. package/cli/outbound-endpoints.ts +20 -6
  36. package/cli/pause-aware-timeout.ts +106 -0
  37. package/cli/scene-video.ts +89 -0
  38. package/cli/ws-bridge.ts +37 -19
  39. package/detox/attempt-events.cjs +66 -5
  40. package/detox/attempt-events.d.cts +7 -0
  41. package/detox/expectations.ts +7 -3
  42. package/detox/index.ts +269 -170
  43. package/detox/motion-change.cjs +206 -21
  44. package/detox/motion-change.d.cts +39 -2
  45. package/detox/navigation-lifecycle.ts +35 -3
  46. package/dist-lib/agent-daemon-client.cjs +1 -1
  47. package/dist-lib/agent-events.cjs +1 -1
  48. package/dist-lib/attached-projects.cjs +1 -1
  49. package/dist-lib/auth/shared-session.cjs +1 -1
  50. package/dist-lib/backend-origin.cjs +8 -7
  51. package/dist-lib/beta.cjs +1 -1
  52. package/dist-lib/beta.mjs +1 -1
  53. package/dist-lib/bridge-constants.cjs +13 -1
  54. package/dist-lib/bridge-contract-input.cjs +1 -1
  55. package/dist-lib/bridge-contract-input.mjs +1 -1
  56. package/dist-lib/bridge-contract.cjs +14 -8
  57. package/dist-lib/bridge-contract.mjs +13 -7
  58. package/dist-lib/bundle-channel.cjs +11887 -7900
  59. package/dist-lib/capture-contract.cjs +1 -1
  60. package/dist-lib/capture-contract.mjs +1 -1
  61. package/dist-lib/cloud-contract.cjs +1 -1
  62. package/dist-lib/cloud-contract.mjs +1 -1
  63. package/dist-lib/cloud-test-args.cjs +1 -1
  64. package/dist-lib/cloud-test-args.mjs +1 -1
  65. package/dist-lib/cloud.cjs +1 -1
  66. package/dist-lib/cloud.mjs +1 -1
  67. package/dist-lib/config.cjs +6 -3
  68. package/dist-lib/config.mjs +6 -3
  69. package/dist-lib/detox/index.cjs +1379 -725
  70. package/dist-lib/dev-bundle-resolution.cjs +14 -2
  71. package/dist-lib/expo-swift-bundle.cjs +9341 -5462
  72. package/dist-lib/expo-swift-metadata.cjs +11 -7
  73. package/dist-lib/expo-swift-metadata.mjs +11 -7
  74. package/dist-lib/home-paths.cjs +32 -3
  75. package/dist-lib/host/bridge-host.cjs +50204 -7258
  76. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  77. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  78. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  79. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  80. package/dist-lib/host/websocket-proxy.cjs +1 -1
  81. package/dist-lib/index.cjs +3541 -448
  82. package/dist-lib/jump-to-source-babel.cjs +1 -1
  83. package/dist-lib/jump-to-source-native.cjs +1 -1
  84. package/dist-lib/kotlin-host.cjs +1 -1
  85. package/dist-lib/menu.cjs +1 -1
  86. package/dist-lib/menu.mjs +1 -1
  87. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  88. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  89. package/dist-lib/metro-production-bundle.cjs +1 -1
  90. package/dist-lib/metro-production-bundle.mjs +1 -1
  91. package/dist-lib/metro.cjs +2931 -104
  92. package/dist-lib/native-app-identity-project.cjs +289 -49
  93. package/dist-lib/native-app-identity-project.mjs +289 -49
  94. package/dist-lib/native-app-identity.cjs +77 -26
  95. package/dist-lib/native-app-identity.mjs +76 -26
  96. package/dist-lib/native-firebase-metadata.cjs +1 -1
  97. package/dist-lib/native-firebase-metadata.mjs +1 -1
  98. package/dist-lib/plist.cjs +1 -1
  99. package/dist-lib/plist.mjs +1 -1
  100. package/dist-lib/profiles.cjs +1 -1
  101. package/dist-lib/public-brand.cjs +1 -1
  102. package/dist-lib/react-native-host-modules.cjs +6 -2
  103. package/dist-lib/react-native-host-modules.mjs +6 -2
  104. package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
  105. package/dist-lib/sdk.cjs +27 -11
  106. package/dist-lib/sdk.mjs +27 -10
  107. package/dist-lib/skills.cjs +72201 -26208
  108. package/dist-lib/swift-compile.cjs +1007 -140
  109. package/dist-lib/swift-compile.mjs +1049 -174
  110. package/dist-lib/swift-package.cjs +12 -3
  111. package/dist-lib/swift-package.mjs +12 -3
  112. package/dist-lib/swift-submit.cjs +13 -5
  113. package/dist-lib/swift-submit.mjs +11 -5
  114. package/dist-lib/swift-toolchain.cjs +423 -33
  115. package/dist-lib/swift.cjs +1 -1
  116. package/dist-lib/vite-swift.cjs +8628 -5268
  117. package/dist-lib/vite.cjs +2859 -12
  118. package/dist-lib/worklet-executable.cjs +14 -2
  119. package/dist-lib/worklet-executable.mjs +14 -2
  120. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  121. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  122. package/package.json +1 -1
  123. package/scripts/dev-server-scanner.ts +2 -1
  124. package/skills/peach-perf/SKILL.md +11 -5
  125. package/skills/peach-setup/SKILL.md +16 -19
  126. package/src/backend-origin.ts +13 -6
  127. package/src/bridge-constants.ts +11 -0
  128. package/src/bridge-contract.ts +22 -34
  129. package/src/config.ts +15 -0
  130. package/src/desktop-host-delivery.ts +5 -1
  131. package/src/dev-bundle-resolution.ts +2 -1
  132. package/src/expo-manifest-body.ts +13 -0
  133. package/src/expo-swift-host.ts +196 -49
  134. package/src/expo-swift-ready.ts +78 -0
  135. package/src/expoKotlinContract.ts +4 -7
  136. package/src/expoSwiftContract.ts +17 -6
  137. package/src/home-paths.ts +48 -2
  138. package/src/host/bridge-host.ts +5 -2
  139. package/src/host/staged-host-files.ts +9 -0
  140. package/src/native-app-identity-project.ts +27 -6
  141. package/src/native-app-identity.ts +91 -4
  142. package/src/native-firebase-config.ts +209 -13
  143. package/src/nativeAppIconContract.ts +1 -4
  144. package/src/output-lock-stdio.ts +36 -0
  145. package/src/process-group.ts +25 -2
  146. package/src/react-native-bridge-modules.ts +431 -0
  147. package/src/react-native-host-modules.ts +8 -1
  148. package/src/settings-bundle.ts +1 -4
  149. package/src/swift-binary-modules.ts +2 -2
  150. package/src/swift-compile.ts +918 -109
  151. package/src/swift-compiler-digest.ts +14 -0
  152. package/src/swift-distribution.lock.json +24 -12
  153. package/src/swift-distribution.ts +103 -4
  154. package/src/swift-host.ts +180 -55
  155. package/src/swift-icu.ts +27 -13
  156. package/src/swift-objc-compile.ts +355 -0
  157. package/src/swift-objc-sdk.ts +821 -27
  158. package/src/swift-objc-standin.ts +148 -14
  159. package/src/swift-package-manifest.ts +60 -7
  160. package/src/swift-package.ts +15 -1
  161. package/src/swift-packages.ts +154 -81
  162. package/src/swift-standin-emit.ts +96 -15
  163. package/src/swift-standins.ts +530 -116
  164. package/src/swift-submit.ts +13 -5
  165. package/src/swift-toolchain.ts +500 -42
  166. package/src/vite-plugin.ts +12 -1
  167. package/src/worker-system-chunks.ts +11 -12
  168. package/src/worklet-executable.ts +19 -1
  169. package/src/xcode-project.ts +270 -30
@@ -12,19 +12,30 @@
12
12
  import { execFileSync, spawnSync } from 'node:child_process'
13
13
  import { createHash } from 'node:crypto'
14
14
  import fs from 'node:fs'
15
- import os from 'node:os'
16
15
  import path from 'node:path'
17
16
  import { getCliVersion } from './cli-version'
17
+ import { cacheDir as peachCacheDir, swiftPackageCacheOptions } from './home-paths'
18
+ import { isRecord } from './is-record'
19
+ import { outputLockStdio } from './output-lock-stdio'
18
20
  import { parsePlistSource } from './plist'
19
21
  import {
20
22
  swiftDistributionAt,
23
+ swiftImportNames,
21
24
  swiftMacroPlugins,
22
25
  swiftModuleDigest,
23
26
  swiftModuleSdk,
24
27
  swiftPeachModules,
25
28
  swiftSdkPaths,
26
29
  } from './swift-compile'
27
- import { IOS_SDK_ID, objcView } from './swift-objc-standin'
30
+ import {
31
+ IOS_SDK_ID,
32
+ NATIVE_GRAPH_ID,
33
+ OBJC_IMPORT_VERSION,
34
+ nativeInterfaceView,
35
+ objcView,
36
+ type ObjcSurface,
37
+ } from './swift-objc-standin'
38
+ import { swiftTargetModuleName } from './swift-package'
28
39
  import {
29
40
  STANDIN_EMIT_VERSION,
30
41
  STANDIN_IMPORTS,
@@ -33,11 +44,13 @@ import {
33
44
  type SwiftStandinBehaviors,
34
45
  type StandinBehavior,
35
46
  } from './swift-standin-emit'
47
+ import { swiftEnvironment, swiftSdkOptions } from './swift-toolchain'
36
48
  import type { SwiftPackageBinary } from './swift-packages'
37
49
 
38
50
  const TRIPLE = 'wasm32-unknown-wasip1'
39
- // builder workspaces share this persistent cache; temporary directories hold only work.
40
- const cacheDir = path.join(os.homedir(), 'builders/cache/peach-swift-standins')
51
+ const WASI_EMULATION = ['_WASI_EMULATED_SIGNAL', '_WASI_EMULATED_MMAN']
52
+ // every stand-in artifact and temporary workspace belongs to peach.
53
+ const cacheDir = path.join(peachCacheDir(), 'swift-standins')
41
54
  // each binary stand-in is a target under its module's directory here.
42
55
  export const SWIFT_STANDINS_DIR = '__standins__'
43
56
 
@@ -48,6 +61,8 @@ export interface SwiftStandin {
48
61
  target: string
49
62
  /** the target's files, relative to its directory. */
50
63
  files: Record<string, string>
64
+ /** other generated modules named by this target's public headers. */
65
+ dependencies?: string[]
51
66
  }
52
67
 
53
68
  export interface SwiftStandins {
@@ -61,12 +76,20 @@ interface Toolchain {
61
76
  toolchainBin: string
62
77
  /** the toolchain release, read once where the toolchain is resolved. */
63
78
  release: string
79
+ /** modules generated earlier in this dependency closure. */
80
+ standinModules?: string
81
+ }
82
+
83
+ interface NativeFrameworkInputs {
84
+ dirs: string[]
85
+ digest: string
64
86
  }
65
87
 
66
88
  function exec(file: string, args: string[], cwd?: string): string {
67
89
  return execFileSync(file, args, {
68
90
  cwd,
69
91
  encoding: 'utf8',
92
+ env: swiftEnvironment(),
70
93
  maxBuffer: 256 * 1024 * 1024,
71
94
  stdio: ['ignore', 'pipe', 'pipe'],
72
95
  })
@@ -94,14 +117,20 @@ const digests = new Map<string, { stamp: string; digest: string }>()
94
117
 
95
118
  function treeDigest(root: string): string {
96
119
  const names: string[] = []
120
+ const ancestors = new Set<string>()
97
121
  const walk = (dir: string) => {
122
+ const resolved = fs.realpathSync(dir)
123
+ if (ancestors.has(resolved)) throw new Error(`cyclic header directory: ${dir}`)
124
+ ancestors.add(resolved)
98
125
  for (const entry of fs
99
126
  .readdirSync(dir, { withFileTypes: true })
100
127
  .sort((left, right) => (left.name < right.name ? -1 : 1))) {
101
128
  const full = path.join(dir, entry.name)
102
- if (entry.isDirectory()) walk(full)
103
- else if (entry.isFile()) names.push(path.relative(root, full))
129
+ const stat = entry.isSymbolicLink() ? fs.statSync(full) : entry
130
+ if (stat.isDirectory()) walk(full)
131
+ else if (stat.isFile()) names.push(path.relative(root, full))
104
132
  }
133
+ ancestors.delete(resolved)
105
134
  }
106
135
  walk(root)
107
136
  const stamp = names
@@ -217,8 +246,9 @@ const files = (dir: string, test: (name: string) => boolean): string[] =>
217
246
  /** the module surfaces an iOS simulator slice describes. */
218
247
  type Surface =
219
248
  | { kind: 'swift'; module: string; interface: string }
249
+ | (ObjcSurface & { kind: 'mixed'; interface: string })
220
250
  | { kind: 'c'; module: string; modulemap: string; headers: string }
221
- | { kind: 'objc'; module: string; modulemap: string; headers: string }
251
+ | (ObjcSurface & { kind: 'objc' })
222
252
 
223
253
  const OBJC = /^\s*(?:@interface|@protocol|@class|@import|#import)\b/m
224
254
 
@@ -257,15 +287,27 @@ function surfaces(xcframeworkDir: string): Surface[] {
257
287
  )
258
288
  const chosen =
259
289
  interfaces.find((name) => name.startsWith('arm64-')) ?? interfaces[0] ?? null
290
+ const modulemap = path.join(framework, 'Modules', 'module.modulemap')
260
291
  if (chosen) {
261
- found.push({
262
- kind: 'swift',
263
- module,
264
- interface: path.join(swiftmodule, chosen),
265
- })
292
+ const file = path.join(swiftmodule, chosen)
293
+ const underlying = [
294
+ ...fs
295
+ .readFileSync(file, 'utf8')
296
+ .matchAll(/^(?:@_exported[ \t]+)?import[ \t]+(\w+)[ \t]*$/gm),
297
+ ].some((match) => match[1] === module)
298
+ if (underlying) {
299
+ if (!fs.existsSync(modulemap))
300
+ throw new Error(`${module} imports its underlying module without a modulemap`)
301
+ found.push({
302
+ kind: 'mixed',
303
+ module,
304
+ interface: file,
305
+ modulemap,
306
+ headers: path.join(framework, 'Headers'),
307
+ })
308
+ } else found.push({ kind: 'swift', module, interface: file })
266
309
  continue
267
310
  }
268
- const modulemap = path.join(framework, 'Modules', 'module.modulemap')
269
311
  if (fs.existsSync(modulemap))
270
312
  found.push(headerSurface(module, modulemap, path.join(framework, 'Headers')))
271
313
  }
@@ -281,6 +323,14 @@ function surfaces(xcframeworkDir: string): Surface[] {
281
323
  return found
282
324
  }
283
325
 
326
+ export function standinsNeedNativeSdk(binaries: readonly SwiftPackageBinary[]): boolean {
327
+ return binaries.some(
328
+ (binary) =>
329
+ binary.publicHeaders === undefined &&
330
+ surfaces(xcframework(binary).dir).some((surface) => surface.kind === 'mixed'),
331
+ )
332
+ }
333
+
284
334
  // the peach modules an interface imports, compiled for wasm: a distribution
285
335
  // ships them; a checkout builds the ones named into its own build directory,
286
336
  // with the flags scripts/build.sh uses, so the two share one build.
@@ -305,10 +355,12 @@ function peachModuleDirs(toolchain: Toolchain, imports: string[]): string[] {
305
355
  path.join(toolchainBin, 'swift'),
306
356
  [
307
357
  'build',
358
+ ...swiftPackageCacheOptions(),
308
359
  '--build-system',
309
360
  'native',
310
361
  '-j',
311
362
  '4',
363
+ ...swiftSdkOptions(),
312
364
  '--swift-sdk',
313
365
  swiftModuleSdk(moduleRoot).wasmSdkId,
314
366
  '-c',
@@ -359,11 +411,14 @@ export function swiftInterfaceForWasm(text: string): string {
359
411
  ? rooted.replace(/^(?=import[ \t])/m, 'import Foundation\n')
360
412
  : rooted
361
413
  return foundation
362
- .replace(/^(\/\/ swift-module-flags:.*)$/m, (flags) =>
363
- flags
364
- .replace(/-target \S+/, `-target ${TRIPLE}`)
365
- .replace(/\s-enable-objc-interop\b/, '')
366
- .replace(/\s-target-variant \S+/, ''),
414
+ .replace(
415
+ /^(\/\/ swift-module-flags:.*)$/m,
416
+ (flags) =>
417
+ flags
418
+ .replace(/-target \S+/, `-target ${TRIPLE}`)
419
+ .replace(/\s-enable-objc-interop\b/, '')
420
+ .replace(/\s-target-variant \S+/, '') +
421
+ ` ${WASI_EMULATION.map((name) => `-Xcc -D${name}`).join(' ')}`,
367
422
  )
368
423
  .replace(
369
424
  /@(?:objc(?:\s*\([^()]*\))?|objcMembers|nonobjc|IBOutlet|IBAction|IBInspectable|IBDesignable|_objcRuntimeName\([^()]*\))(?![\w(])/g,
@@ -377,17 +432,14 @@ function swiftStandin(
377
432
  toolchain: Toolchain,
378
433
  work: string,
379
434
  behavior: ReadonlyMap<string, StandinBehavior>,
435
+ siblings: ReadonlySet<string>,
380
436
  ): SwiftStandin {
381
437
  const text = swiftInterfaceForWasm(fs.readFileSync(surface.interface, 'utf8'))
382
438
  const search = path.join(work, 'interfaces')
383
439
  const moduleDir = path.join(search, `${surface.module}.swiftmodule`)
384
440
  fs.mkdirSync(moduleDir, { recursive: true })
385
441
  fs.writeFileSync(path.join(moduleDir, `${TRIPLE}.swiftinterface`), text)
386
- const imports = [
387
- ...text.matchAll(
388
- /^(?:@_exported[ \t]+)?import[ \t]+(?:(?:struct|class|enum|protocol|typealias|func|var|let)[ \t]+)?(\w+)/gm,
389
- ),
390
- ].map((match) => match[1]!)
442
+ const imports = swiftImportNames(text)
391
443
  const wasm = wasmFlags(toolchain, imports)
392
444
  const out = path.join(work, 'graphs')
393
445
  fs.mkdirSync(out, { recursive: true })
@@ -418,6 +470,37 @@ function swiftStandin(
418
470
  toolchain,
419
471
  work,
420
472
  behavior,
473
+ [...new Set(imports)].filter((module) => siblings.has(module)),
474
+ )
475
+ }
476
+
477
+ function mixedStandin(
478
+ surface: Extract<Surface, { kind: 'mixed' }>,
479
+ binary: SwiftPackageBinary,
480
+ toolchain: Toolchain,
481
+ work: string,
482
+ behavior: ReadonlyMap<string, StandinBehavior>,
483
+ frameworkDirs: readonly string[],
484
+ siblings: ReadonlySet<string>,
485
+ ): SwiftStandin {
486
+ const view = nativeInterfaceView(surface, frameworkDirs, work)
487
+ const provided = new Set(swiftPeachModules(toolchain.moduleRoot))
488
+ const imports = [
489
+ ...new Set(swiftImportNames(fs.readFileSync(surface.interface, 'utf8'))),
490
+ ].filter(
491
+ (module) =>
492
+ module !== surface.module && (provided.has(module) || siblings.has(module)),
493
+ )
494
+ return graphStandin(
495
+ surface.module,
496
+ view.graphs,
497
+ view.values,
498
+ wasmFlags(toolchain, imports),
499
+ binary,
500
+ toolchain,
501
+ work,
502
+ behavior,
503
+ imports,
421
504
  )
422
505
  }
423
506
 
@@ -434,7 +517,10 @@ function wasmFlags(toolchain: Toolchain, imports: string[]): string[] {
434
517
  sysroot,
435
518
  '-resource-dir',
436
519
  resources,
520
+ // the sdk toolset's emulation defines, which raw swiftc does not read
521
+ ...WASI_EMULATION.flatMap((name) => ['-Xcc', `-D${name}`]),
437
522
  ...peachModuleDirs(toolchain, imports).flatMap((dir) => ['-I', dir]),
523
+ ...(toolchain.standinModules ? ['-I', toolchain.standinModules] : []),
438
524
  ]
439
525
  }
440
526
 
@@ -462,6 +548,9 @@ function objcStandin(
462
548
  toolchain,
463
549
  work,
464
550
  behavior,
551
+ binary.headerDependencies?.map((dependency) =>
552
+ swiftTargetModuleName(dependency.module),
553
+ ),
465
554
  )
466
555
  }
467
556
 
@@ -477,6 +566,7 @@ function graphStandin(
477
566
  toolchain: Toolchain,
478
567
  work: string,
479
568
  behavior: ReadonlyMap<string, StandinBehavior>,
569
+ imports: string[] = [],
480
570
  ): SwiftStandin {
481
571
  const paths = new Set(
482
572
  graphs.flatMap((graph) =>
@@ -500,6 +590,9 @@ function graphStandin(
500
590
  behavior,
501
591
  omit,
502
592
  plain,
593
+ inert: binary.publicHeaders !== undefined,
594
+ imports,
595
+ family: imports,
503
596
  })
504
597
  for (const entry of skipped) {
505
598
  if (behavior.has(entry.path))
@@ -510,23 +603,28 @@ function graphStandin(
510
603
  fs.writeFileSync(file, source)
511
604
  const check = spawnSync(
512
605
  path.join(toolchain.toolchainBin, 'swiftc'),
606
+ // emitting the module runs the diagnostics a typecheck skips, such as
607
+ // literal overflow, which the app's build of the stand-in reaches
513
608
  [
514
- '-typecheck',
609
+ '-emit-module',
515
610
  '-parse-as-library',
516
611
  '-swift-version',
517
612
  '5',
518
613
  '-module-name',
519
614
  module,
520
615
  ...wasm,
616
+ '-emit-module-path',
617
+ path.join(work, `${module}.swiftmodule`),
521
618
  file,
522
619
  ],
523
- { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 },
620
+ { encoding: 'utf8', env: swiftEnvironment(), maxBuffer: 256 * 1024 * 1024 },
524
621
  )
525
622
  if (check.status === 0)
526
623
  return {
527
624
  module,
528
625
  target: binary.target,
529
626
  files: { [`${module}.swift`]: source },
627
+ ...(imports.length ? { dependencies: imports } : {}),
530
628
  }
531
629
  const before = omit.size + plain.size
532
630
  const elsewhere: string[] = []
@@ -901,6 +999,7 @@ export function standinLibrary(
901
999
  binary: SwiftPackageBinary,
902
1000
  searchRoot: string,
903
1001
  ): string | null {
1002
+ if (binary.publicHeaders !== undefined) return null
904
1003
  const dir = xcframework(binary).dir
905
1004
  const library = simulatorLibraryName(dir)
906
1005
  const supplied = path.join(dir, RUST_TARGET)
@@ -922,14 +1021,18 @@ export function standinLibrary(
922
1021
  [
923
1022
  'build',
924
1023
  '--release',
1024
+ '--locked',
925
1025
  '--target',
926
1026
  RUST_TARGET,
927
1027
  '--message-format=json-render-diagnostics',
928
1028
  '--manifest-path',
929
1029
  manifest,
1030
+ '--target-dir',
1031
+ path.join(cacheDir, 'cargo', key(manifest)),
930
1032
  ],
931
1033
  {
932
1034
  encoding: 'utf8',
1035
+ env: { ...process.env, CARGO_HOME: path.join(cacheDir, 'cargo-home') },
933
1036
  maxBuffer: 256 * 1024 * 1024,
934
1037
  stdio: ['ignore', 'pipe', 'pipe'],
935
1038
  },
@@ -969,30 +1072,91 @@ export function standinLibrary(
969
1072
  throw new Error(`cargo built ${crate} for ${RUST_TARGET} but reported no ${library}`)
970
1073
  }
971
1074
 
972
- // source preflight shares project compilation's portable builder admission.
973
- // the complete resolved artifact travels as an input, including ignored local frameworks.
974
- function sourceBinaryStandins(
975
- binary: SwiftPackageBinary,
1075
+ // source preflight admits the complete closure once, including ignored local
1076
+ // frameworks. target generation and prerequisite modules stay in that one job.
1077
+ function sourceStandins(
1078
+ binaries: readonly SwiftPackageBinary[],
976
1079
  toolchain: Toolchain,
977
1080
  provided: ReadonlySet<string>,
978
1081
  behavior: SwiftStandinBehaviors,
979
- ): SwiftStandin[] | string {
1082
+ ): SwiftStandins {
980
1083
  const repo = path.resolve(toolchain.moduleRoot, '../..')
981
- const work = fs.mkdtempSync(path.join(os.tmpdir(), 'peach-standin-request-'))
1084
+ fs.mkdirSync(cacheDir, { recursive: true })
1085
+ const generation = standinGenerationInputs(binaries, toolchain, behavior)
1086
+ const cached = path.join(
1087
+ cacheDir,
1088
+ 'closures',
1089
+ `${key(generation, JSON.stringify([...provided].sort()))}.json`,
1090
+ )
1091
+ const targets = new Set(binaries.map((binary) => binary.target))
1092
+ if (fs.existsSync(cached))
1093
+ return readStandinResults(
1094
+ JSON.parse(fs.readFileSync(cached, 'utf8')),
1095
+ generation,
1096
+ targets,
1097
+ )
1098
+ const work = fs.mkdtempSync(path.join(cacheDir, 'request-'))
982
1099
  try {
983
- const artifact = xcframework(binary)
984
- const local = { ...binary, dir: artifact.dir, url: null, checksum: null }
985
- const generation = standinGenerationInputs([local], toolchain, behavior)
1100
+ const artifact = path.join(work, 'artifacts')
1101
+ fs.mkdirSync(artifact)
1102
+ const local: SwiftPackageBinary[] = []
1103
+ const gaps = new Map<string, string>()
1104
+ const headers = new Map<string, string>()
1105
+ for (const [index, binary] of binaries.entries()) {
1106
+ try {
1107
+ if (binary.publicHeaders !== undefined) {
1108
+ const copyHeader = (relative: string) => {
1109
+ const source = fs.realpathSync(path.join(binary.dir, relative))
1110
+ let destination = headers.get(source)
1111
+ if (destination === undefined) {
1112
+ destination = `headers/${headers.size}`
1113
+ fs.cpSync(source, path.join(artifact, destination), {
1114
+ recursive: true,
1115
+ dereference: true,
1116
+ })
1117
+ headers.set(source, destination)
1118
+ }
1119
+ return destination
1120
+ }
1121
+ local.push({
1122
+ ...binary,
1123
+ dir: artifact,
1124
+ url: null,
1125
+ checksum: null,
1126
+ publicHeaders: copyHeader(binary.publicHeaders),
1127
+ headerDependencies: (binary.headerDependencies ?? []).map((dependency) => ({
1128
+ module: dependency.module,
1129
+ path: copyHeader(dependency.path),
1130
+ })),
1131
+ })
1132
+ } else {
1133
+ const resolved = xcframework(binary)
1134
+ const dir = path.join(artifact, String(index), path.basename(resolved.dir))
1135
+ fs.cpSync(resolved.dir, dir, { recursive: true, dereference: true })
1136
+ local.push({ ...binary, dir, url: null, checksum: null })
1137
+ }
1138
+ } catch (error) {
1139
+ gaps.set(binary.target, error instanceof Error ? error.message : String(error))
1140
+ }
1141
+ }
1142
+ if (local.length === 0) return { standins: [], gaps }
1143
+ const preparedGeneration = standinGenerationInputs(local, toolchain, behavior)
1144
+ if (generation !== standinGenerationInputs(binaries, toolchain, behavior))
1145
+ throw new Error('Swift stand-in inputs changed during preparation')
986
1146
  const request = path.join(work, 'request.json')
987
1147
  const delivery = path.join(work, 'delivery')
988
1148
  fs.writeFileSync(
989
1149
  request,
990
1150
  JSON.stringify({
991
- binary: local,
1151
+ binaries: local.map((binary) => ({
1152
+ ...binary,
1153
+ dir: path.relative(artifact, binary.dir) || '.',
1154
+ })),
992
1155
  provided: [...provided],
993
1156
  behavior,
994
- generation,
1157
+ generation: preparedGeneration,
995
1158
  release: toolchain.release,
1159
+ native: standinsNeedNativeSdk(local),
996
1160
  }),
997
1161
  )
998
1162
  const build = spawnSync(
@@ -1002,56 +1166,104 @@ function sourceBinaryStandins(
1002
1166
  '--request',
1003
1167
  request,
1004
1168
  '--artifact',
1005
- artifact.dir,
1169
+ artifact,
1006
1170
  '--delivery-dir',
1007
1171
  delivery,
1008
1172
  ],
1009
- { cwd: repo, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 },
1173
+ {
1174
+ cwd: repo,
1175
+ encoding: 'utf8',
1176
+ maxBuffer: 8 * 1024 * 1024,
1177
+ stdio: outputLockStdio(['ignore', 'pipe', 'pipe']),
1178
+ },
1010
1179
  )
1011
1180
  if (build.stdout) process.stdout.write(build.stdout)
1012
1181
  if (build.stderr) process.stderr.write(build.stderr)
1013
1182
  if (build.error || build.status !== 0)
1014
1183
  throw new Error(
1015
- `source stand-in build for ${binary.target} failed (${build.error?.message ?? build.signal ?? `exit ${build.status}`}): ${build.stderr?.trim().slice(-4000) || build.stdout?.trim().slice(-4000) || 'builder returned no diagnostic'}`,
1184
+ `source stand-in build for ${local.map((binary) => binary.target).join(', ')} failed (${build.error?.message ?? build.signal ?? `exit ${build.status}`}): ${build.stderr?.trim().slice(-4000) || build.stdout?.trim().slice(-4000) || 'builder returned no diagnostic'}`,
1016
1185
  )
1017
1186
  const output = path.join(delivery, 'standins.json')
1018
1187
  if (!fs.existsSync(output))
1019
- throw new Error(
1020
- `source stand-in builder returned no standins.json for ${binary.target}`,
1021
- )
1188
+ throw new Error('source stand-in builder returned no standins.json')
1022
1189
  const receipt: unknown = JSON.parse(fs.readFileSync(output, 'utf8'))
1023
- if (
1024
- !receipt ||
1025
- typeof receipt !== 'object' ||
1026
- !('generation' in receipt) ||
1027
- receipt.generation !== generation ||
1028
- !('result' in receipt)
1029
- )
1030
- throw new Error('Swift stand-in builder source differs from the caller')
1031
- const result = receipt.result
1032
- if (typeof result === 'string') throw new Error(result)
1033
- if (
1034
- !Array.isArray(result) ||
1035
- !result.every(
1036
- (item) =>
1037
- item &&
1038
- typeof item === 'object' &&
1039
- typeof item.module === 'string' &&
1040
- typeof item.target === 'string' &&
1041
- item.target === binary.target &&
1042
- item.files &&
1043
- typeof item.files === 'object' &&
1044
- !Array.isArray(item.files) &&
1045
- Object.values(item.files).every((text) => typeof text === 'string'),
1046
- )
1190
+ const result = readStandinResults(
1191
+ receipt,
1192
+ preparedGeneration,
1193
+ new Set(local.map((binary) => binary.target)),
1047
1194
  )
1048
- throw new Error('invalid Swift stand-in builder result')
1195
+ if (generation !== standinGenerationInputs(binaries, toolchain, behavior))
1196
+ throw new Error('Swift stand-in inputs changed during generation')
1197
+ for (const [target, reason] of gaps) result.gaps.set(target, reason)
1198
+ // failures retry against the per-target generation cache on the builder.
1199
+ // only a complete successful closure skips the next admission entirely.
1200
+ if (result.gaps.size === 0 && isRecord(receipt)) {
1201
+ fs.mkdirSync(path.dirname(cached), { recursive: true })
1202
+ const pending = `${cached}.${process.pid}.tmp`
1203
+ fs.writeFileSync(pending, JSON.stringify({ ...receipt, generation }))
1204
+ fs.renameSync(pending, cached)
1205
+ }
1049
1206
  return result
1050
1207
  } finally {
1051
1208
  fs.rmSync(work, { recursive: true, force: true })
1052
1209
  }
1053
1210
  }
1054
1211
 
1212
+ function readStandinResults(
1213
+ receipt: unknown,
1214
+ generation: string,
1215
+ targets: ReadonlySet<string>,
1216
+ ): SwiftStandins {
1217
+ if (!isRecord(receipt) || receipt.generation !== generation)
1218
+ throw new Error('Swift stand-in builder source differs from the caller')
1219
+ if (!Array.isArray(receipt.results) || receipt.results.length !== targets.size)
1220
+ throw new Error('invalid Swift stand-in builder results')
1221
+ const standins: SwiftStandin[] = []
1222
+ const gaps = new Map<string, string>()
1223
+ const seen = new Set<string>()
1224
+ for (const entry of receipt.results) {
1225
+ if (
1226
+ !isRecord(entry) ||
1227
+ typeof entry.target !== 'string' ||
1228
+ !targets.has(entry.target) ||
1229
+ seen.has(entry.target)
1230
+ )
1231
+ throw new Error('invalid Swift stand-in builder target')
1232
+ seen.add(entry.target)
1233
+ if (typeof entry.result === 'string') {
1234
+ gaps.set(entry.target, entry.result)
1235
+ continue
1236
+ }
1237
+ if (!Array.isArray(entry.result))
1238
+ throw new Error('invalid Swift stand-in builder result')
1239
+ for (const item of entry.result) {
1240
+ if (
1241
+ !isRecord(item) ||
1242
+ typeof item.module !== 'string' ||
1243
+ item.target !== entry.target ||
1244
+ !isRecord(item.files) ||
1245
+ !Object.values(item.files).every((text) => typeof text === 'string') ||
1246
+ (item.dependencies !== undefined &&
1247
+ (!Array.isArray(item.dependencies) ||
1248
+ !item.dependencies.every((name) => typeof name === 'string')))
1249
+ )
1250
+ throw new Error('invalid Swift stand-in builder result')
1251
+ const files: Record<string, string> = {}
1252
+ for (const [name, text] of Object.entries(item.files)) {
1253
+ if (typeof text !== 'string') throw new Error('invalid Swift stand-in file')
1254
+ files[name] = text
1255
+ }
1256
+ standins.push({
1257
+ module: item.module,
1258
+ target: entry.target,
1259
+ files,
1260
+ ...(item.dependencies === undefined ? {} : { dependencies: item.dependencies }),
1261
+ })
1262
+ }
1263
+ }
1264
+ return { standins, gaps }
1265
+ }
1266
+
1055
1267
  const built = new Map<string, SwiftStandin[] | string>()
1056
1268
 
1057
1269
  /** what stand-ins are generated from besides the surfaces' files: the
@@ -1066,20 +1278,23 @@ export function standinGenerationInputs(
1066
1278
  const { moduleRoot, release } = toolchain
1067
1279
  const sdk = swiftModuleSdk(moduleRoot)
1068
1280
  const parts = [
1069
- 'standin/v4',
1281
+ 'standin/v9',
1070
1282
  release,
1071
1283
  sdk.wasmSdkId,
1072
1284
  sdk.wasmSdkChecksum,
1073
1285
  swiftModuleDigest(moduleRoot).toString('hex'),
1074
1286
  `emit/${STANDIN_EMIT_VERSION}`,
1075
1287
  `ios-sdk/${IOS_SDK_ID}`,
1288
+ `objc-import/${OBJC_IMPORT_VERSION}`,
1076
1289
  // a released cli can change the emitter without anyone bumping the constant
1077
1290
  `cli/${getCliVersion()}`,
1078
1291
  ]
1079
1292
  const modules = new Set(
1080
1293
  binaries.flatMap((binary) => {
1081
1294
  try {
1082
- return surfaces(xcframework(binary).dir).map((surface) => surface.module)
1295
+ return binary.publicHeaders !== undefined
1296
+ ? [swiftTargetModuleName(binary.target)]
1297
+ : surfaces(xcframework(binary).dir).map((surface) => surface.module)
1083
1298
  } catch {
1084
1299
  return []
1085
1300
  }
@@ -1100,13 +1315,36 @@ export function standinGenerationInputs(
1100
1315
  ),
1101
1316
  )
1102
1317
  for (const binary of binaries) {
1318
+ if (
1319
+ binary.publicHeaders !== undefined &&
1320
+ swiftTargetModuleName(binary.target) !== binary.target
1321
+ )
1322
+ parts.push(`module/${swiftTargetModuleName(binary.target)}`)
1103
1323
  try {
1324
+ if (
1325
+ binary.publicHeaders === undefined &&
1326
+ surfaces(xcframework(binary).dir).some((surface) => surface.kind === 'swift')
1327
+ )
1328
+ parts.push(`interface-emulation/${WASI_EMULATION.join(',')}`)
1329
+ if (
1330
+ binary.publicHeaders === undefined &&
1331
+ surfaces(xcframework(binary).dir).some((surface) => surface.kind === 'mixed')
1332
+ )
1333
+ parts.push(`native-graph/${NATIVE_GRAPH_ID}`)
1104
1334
  parts.push(
1105
1335
  binary.target,
1106
1336
  binary.source,
1107
1337
  binary.url ?? '',
1108
1338
  binary.checksum ?? '',
1109
- xcframework(binary).digest,
1339
+ binary.publicHeaders !== undefined
1340
+ ? [
1341
+ treeDigest(path.join(binary.dir, binary.publicHeaders)),
1342
+ ...(binary.headerDependencies ?? []).map(
1343
+ (dependency) =>
1344
+ `${swiftTargetModuleName(dependency.module)}\0${treeDigest(path.join(binary.dir, dependency.path))}`,
1345
+ ),
1346
+ ].join('\0')
1347
+ : xcframework(binary).digest,
1110
1348
  )
1111
1349
  } catch {
1112
1350
  // unreadable here fails again below, where it becomes a gap
@@ -1132,6 +1370,8 @@ function binaryStandins(
1132
1370
  toolchain: Toolchain,
1133
1371
  provided: ReadonlySet<string>,
1134
1372
  behavior: SwiftStandinBehaviors,
1373
+ frameworks: NativeFrameworkInputs,
1374
+ siblings: ReadonlySet<string>,
1135
1375
  ): SwiftStandin[] | string {
1136
1376
  if (provided.has(binary.target)) return []
1137
1377
  // a local artifact a script downloads or builds is absent from a fresh
@@ -1139,17 +1379,56 @@ function binaryStandins(
1139
1379
  // calls, so peach names the path and asks for the artifact
1140
1380
  if (!binary.url && !fs.existsSync(binary.dir))
1141
1381
  return `${path.relative(process.cwd(), binary.dir) || binary.dir} is not on disk, and Xcode fails without it too; run the script that downloads or builds it, then run again`
1382
+ fs.mkdirSync(cacheDir, { recursive: true })
1383
+ const work = fs.mkdtempSync(path.join(cacheDir, 'work-'))
1142
1384
  let all: Surface[]
1143
1385
  let digest: string
1144
1386
  try {
1145
- const artifact = xcframework(binary)
1146
- all = surfaces(artifact.dir)
1147
- digest = artifact.digest
1387
+ if (binary.publicHeaders !== undefined) {
1388
+ const headers = path.join(binary.dir, binary.publicHeaders)
1389
+ const module = swiftTargetModuleName(binary.target)
1390
+ const modulemap = path.join(work, 'module.modulemap')
1391
+ const supplied = path.join(headers, 'module.modulemap')
1392
+ const roots = files(headers, (name) => name.endsWith('.h'))
1393
+ if (roots.length === 0)
1394
+ throw new Error('its public header directory has no headers')
1395
+ fs.writeFileSync(
1396
+ modulemap,
1397
+ fs.existsSync(supplied)
1398
+ ? fs.readFileSync(supplied, 'utf8')
1399
+ : `module ${module} {\n${roots.map((file) => ` header "${file}"`).join('\n')}\n export *\n}\n`,
1400
+ )
1401
+ all = [
1402
+ {
1403
+ kind: 'objc',
1404
+ module,
1405
+ modulemap,
1406
+ headers,
1407
+ dependencies: binary.headerDependencies?.map((dependency) => ({
1408
+ module: swiftTargetModuleName(dependency.module),
1409
+ headers: path.join(binary.dir, dependency.path),
1410
+ })),
1411
+ },
1412
+ ]
1413
+ digest = [
1414
+ treeDigest(headers),
1415
+ ...(binary.headerDependencies ?? []).map(
1416
+ (dependency) =>
1417
+ `${swiftTargetModuleName(dependency.module)}\0${treeDigest(path.join(binary.dir, dependency.path))}`,
1418
+ ),
1419
+ ].join('\0')
1420
+ } else {
1421
+ const artifact = xcframework(binary)
1422
+ all = surfaces(artifact.dir)
1423
+ digest = artifact.digest
1424
+ }
1148
1425
  } catch (error) {
1426
+ fs.rmSync(work, { recursive: true, force: true })
1149
1427
  return error instanceof Error ? error.message : String(error)
1150
1428
  }
1151
1429
  const found = all.filter((surface) => !provided.has(surface.module))
1152
1430
  if (found.length === 0) {
1431
+ fs.rmSync(work, { recursive: true, force: true })
1153
1432
  if (all.length > 0) return []
1154
1433
  return 'its iOS simulator slice has no Swift interface or C modulemap'
1155
1434
  }
@@ -1159,11 +1438,14 @@ function binaryStandins(
1159
1438
  standinGenerationInputs([binary], toolchain, behavior),
1160
1439
  )
1161
1440
  hash.update(digest)
1441
+ hash.update([...siblings].sort().join('\0'))
1442
+ if (found.some((surface) => surface.kind === 'mixed')) hash.update(frameworks.digest)
1162
1443
  for (const surface of found) {
1163
1444
  hash.update(`${surface.kind}\0${surface.module}\0`)
1164
1445
  for (const file of surface.kind === 'swift'
1165
1446
  ? [surface.interface]
1166
1447
  : [
1448
+ ...(surface.kind === 'mixed' ? [surface.interface] : []),
1167
1449
  surface.modulemap,
1168
1450
  ...files(surface.headers, (name) => name.endsWith('.h')).map((file) =>
1169
1451
  path.join(surface.headers, file),
@@ -1173,54 +1455,60 @@ function binaryStandins(
1173
1455
  }
1174
1456
  const id = hash.digest('hex').slice(0, 32)
1175
1457
  const memo = built.get(id)
1176
- if (memo) return memo
1458
+ if (memo) {
1459
+ fs.rmSync(work, { recursive: true, force: true })
1460
+ return memo
1461
+ }
1177
1462
  const cached = path.join(cacheDir, 'standins', `${id}.json`)
1178
1463
  if (fs.existsSync(cached)) {
1179
1464
  const value: SwiftStandin[] = JSON.parse(fs.readFileSync(cached, 'utf8'))
1180
1465
  built.set(id, value)
1466
+ fs.rmSync(work, { recursive: true, force: true })
1181
1467
  return value
1182
1468
  }
1183
- const work = fs.mkdtempSync(path.join(os.tmpdir(), 'peach-standin-'))
1184
1469
  let result: SwiftStandin[] | string
1185
1470
  try {
1186
- result =
1187
- process.platform === 'darwin' &&
1188
- process.env.TM_EXEC_BUILD_PLATFORM !== 'mac' &&
1189
- process.env.TM_SESSION &&
1190
- process.env.CI !== 'true' &&
1191
- toolchain.moduleRoot === path.resolve(import.meta.dirname, '../../sootsim-swift') &&
1192
- !swiftDistributionAt(toolchain.moduleRoot)
1193
- ? sourceBinaryStandins(binary, toolchain, provided, behavior)
1194
- : found.map((surface) => {
1195
- const dir = path.join(work, surface.module)
1196
- try {
1197
- if (surface.kind === 'c' && configured.has(surface.module))
1198
- throw new Error(
1199
- "Configured behavior requires the framework's Swift declarations",
1200
- )
1201
- return surface.kind === 'swift'
1202
- ? swiftStandin(
1203
- surface,
1204
- binary,
1205
- toolchain,
1206
- dir,
1207
- new Map(Object.entries(configured.get(surface.module) ?? {})),
1208
- )
1209
- : surface.kind === 'objc'
1210
- ? objcStandin(
1211
- surface,
1212
- binary,
1213
- toolchain,
1214
- dir,
1215
- new Map(Object.entries(configured.get(surface.module) ?? {})),
1216
- )
1217
- : cStandin(surface, binary, toolchain, dir)
1218
- } catch (error) {
1219
- throw new Error(
1220
- `no stand-in for ${surface.module}: ${error instanceof Error && !('stderr' in error) ? error.message : failure(error)}`,
1471
+ result = found.map((surface) => {
1472
+ const dir = path.join(work, surface.module)
1473
+ try {
1474
+ if (surface.kind === 'c' && configured.has(surface.module))
1475
+ throw new Error(
1476
+ "Configured behavior requires the framework's Swift declarations",
1477
+ )
1478
+ return surface.kind === 'mixed'
1479
+ ? mixedStandin(
1480
+ surface,
1481
+ binary,
1482
+ toolchain,
1483
+ dir,
1484
+ new Map(Object.entries(configured.get(surface.module) ?? {})),
1485
+ frameworks.dirs,
1486
+ siblings,
1487
+ )
1488
+ : surface.kind === 'swift'
1489
+ ? swiftStandin(
1490
+ surface,
1491
+ binary,
1492
+ toolchain,
1493
+ dir,
1494
+ new Map(Object.entries(configured.get(surface.module) ?? {})),
1495
+ siblings,
1221
1496
  )
1222
- }
1223
- })
1497
+ : surface.kind === 'objc'
1498
+ ? objcStandin(
1499
+ surface,
1500
+ binary,
1501
+ toolchain,
1502
+ dir,
1503
+ new Map(Object.entries(configured.get(surface.module) ?? {})),
1504
+ )
1505
+ : cStandin(surface, binary, toolchain, dir)
1506
+ } catch (error) {
1507
+ throw new Error(
1508
+ `no stand-in for ${surface.module}: ${error instanceof Error && !('stderr' in error) ? error.message : failure(error)}`,
1509
+ )
1510
+ }
1511
+ })
1224
1512
  if (typeof result === 'string') throw new Error(result)
1225
1513
  fs.mkdirSync(path.dirname(cached), { recursive: true })
1226
1514
  // publish a complete entry so concurrent workspaces never read partial json.
@@ -1256,12 +1544,138 @@ export function swiftStandins(
1256
1544
  provided: ReadonlySet<string> = new Set(),
1257
1545
  behavior: SwiftStandinBehaviors = {},
1258
1546
  ): SwiftStandins {
1547
+ const requested = binaries.filter((binary) => !provided.has(binary.target))
1548
+ if (requested.length === 0) return { standins: [], gaps: new Map() }
1549
+ if (
1550
+ process.platform === 'darwin' &&
1551
+ process.env.TM_EXEC_BUILD_PLATFORM !== 'mac' &&
1552
+ process.env.TM_SESSION &&
1553
+ process.env.CI !== 'true' &&
1554
+ toolchain.moduleRoot === path.resolve(import.meta.dirname, '../../sootsim-swift') &&
1555
+ !swiftDistributionAt(toolchain.moduleRoot)
1556
+ )
1557
+ return sourceStandins(requested, toolchain, provided, behavior)
1259
1558
  const standins: SwiftStandin[] = []
1260
1559
  const gaps = new Map<string, string>()
1560
+ fs.mkdirSync(cacheDir, { recursive: true })
1561
+ const modules = fs.mkdtempSync(path.join(cacheDir, 'modules-'))
1562
+ const frameworkDirs = new Set<string>()
1563
+ const frameworkInputs: string[] = []
1564
+ const byModule = new Map(
1565
+ binaries.map((binary) => [swiftTargetModuleName(binary.target), binary]),
1566
+ )
1567
+ // the modules each binary's swift interfaces import
1568
+ const interfaceImports = new Map<SwiftPackageBinary, Set<string>>()
1261
1569
  for (const binary of binaries) {
1262
- const result = binaryStandins(binary, toolchain, provided, behavior)
1263
- if (typeof result === 'string') gaps.set(binary.target, result)
1264
- else standins.push(...result)
1570
+ if (binary.publicHeaders !== undefined) continue
1571
+ try {
1572
+ const artifact = xcframework(binary)
1573
+ frameworkInputs.push(`${binary.target}\0${artifact.digest}`)
1574
+ const imports = new Set<string>()
1575
+ interfaceImports.set(binary, imports)
1576
+ for (const surface of surfaces(artifact.dir)) {
1577
+ byModule.set(surface.module, binary)
1578
+ if (surface.kind === 'swift' || surface.kind === 'mixed')
1579
+ for (const module of swiftImportNames(
1580
+ fs.readFileSync(surface.interface, 'utf8'),
1581
+ ))
1582
+ imports.add(module)
1583
+ if (surface.kind === 'c') continue
1584
+ frameworkDirs.add(
1585
+ surface.kind === 'swift'
1586
+ ? path.resolve(surface.interface, '../../../..')
1587
+ : path.resolve(surface.modulemap, '../../..'),
1588
+ )
1589
+ }
1590
+ } catch {
1591
+ // the target reports its artifact gap when visited below.
1592
+ }
1593
+ }
1594
+ const generatedToolchain = { ...toolchain, standinModules: modules }
1595
+ const frameworks: NativeFrameworkInputs = {
1596
+ dirs: [...frameworkDirs].sort(),
1597
+ digest: createHash('sha256').update(frameworkInputs.sort().join('\0')).digest('hex'),
1598
+ }
1599
+ // another binary's module a swift interface imports: its stand-in is built
1600
+ // first and imported, as the vendor framework imports the other framework
1601
+ const siblings = new Map(
1602
+ binaries.map((binary) => [
1603
+ binary,
1604
+ new Set(
1605
+ [...(interfaceImports.get(binary) ?? [])].filter((module) => {
1606
+ const owner = byModule.get(module)
1607
+ return owner !== undefined && owner !== binary && !provided.has(module)
1608
+ }),
1609
+ ),
1610
+ ]),
1611
+ )
1612
+ const prerequisites = new Set(
1613
+ binaries.flatMap((binary) => [
1614
+ ...(binary.headerDependencies ?? []).map((dependency) =>
1615
+ swiftTargetModuleName(dependency.module),
1616
+ ),
1617
+ ...siblings.get(binary)!,
1618
+ ]),
1619
+ )
1620
+ const visited = new Set<string>()
1621
+ const visit = (binary: SwiftPackageBinary) => {
1622
+ if (visited.has(binary.target)) return
1623
+ visited.add(binary.target)
1624
+ for (const module of [
1625
+ ...(binary.headerDependencies ?? []).map((dependency) =>
1626
+ swiftTargetModuleName(dependency.module),
1627
+ ),
1628
+ ...siblings.get(binary)!,
1629
+ ]) {
1630
+ const target = byModule.get(module)
1631
+ if (target) visit(target)
1632
+ }
1633
+ const result = binaryStandins(
1634
+ binary,
1635
+ generatedToolchain,
1636
+ provided,
1637
+ behavior,
1638
+ frameworks,
1639
+ siblings.get(binary)!,
1640
+ )
1641
+ if (typeof result === 'string') {
1642
+ gaps.set(binary.target, result)
1643
+ return
1644
+ }
1645
+ for (const standin of result) {
1646
+ // source objc dependents typecheck against the same generated declarations
1647
+ // that the app links, including on cache hits.
1648
+ if (prerequisites.has(standin.module)) {
1649
+ const source = standin.files[`${standin.module}.swift`]
1650
+ if (source !== undefined) {
1651
+ const file = path.join(modules, `${standin.module}.swift`)
1652
+ fs.writeFileSync(file, source)
1653
+ try {
1654
+ exec(path.join(toolchain.toolchainBin, 'swiftc'), [
1655
+ '-emit-module',
1656
+ '-parse-as-library',
1657
+ '-swift-version',
1658
+ '5',
1659
+ '-module-name',
1660
+ standin.module,
1661
+ ...wasmFlags(generatedToolchain, standin.dependencies ?? []),
1662
+ '-emit-module-path',
1663
+ path.join(modules, `${standin.module}.swiftmodule`),
1664
+ file,
1665
+ ])
1666
+ } catch (error) {
1667
+ gaps.set(binary.target, failure(error))
1668
+ return
1669
+ }
1670
+ }
1671
+ }
1672
+ standins.push(standin)
1673
+ }
1674
+ }
1675
+ try {
1676
+ for (const binary of binaries) visit(binary)
1677
+ return { standins, gaps }
1678
+ } finally {
1679
+ fs.rmSync(modules, { recursive: true, force: true })
1265
1680
  }
1266
- return { standins, gaps }
1267
1681
  }