peachsim 0.1.1635 → 0.1.1641

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 (142) hide show
  1. package/README.md +10 -1
  2. package/cli/app-config.ts +14 -5
  3. package/cli/app-stubs.ts +96 -22
  4. package/cli/bridge-flow-runner.ts +2 -2
  5. package/cli/cloud-client.ts +60 -40
  6. package/cli/cloud-session.ts +16 -0
  7. package/cli/commands/assert.ts +17 -4
  8. package/cli/commands/box.ts +33 -19
  9. package/cli/commands/doctor.ts +37 -20
  10. package/cli/commands/flow.ts +1 -1
  11. package/cli/commands/inspect/actions.ts +3 -2
  12. package/cli/commands/inspect/core.ts +0 -8
  13. package/cli/commands/inspect/shared.ts +9 -4
  14. package/cli/commands/inspect.ts +65 -61
  15. package/cli/commands/platform.ts +2 -1
  16. package/cli/commands/preview.ts +1 -1
  17. package/cli/commands/record.ts +154 -68
  18. package/cli/commands/swift.ts +18 -4
  19. package/cli/commands/three-mode.ts +4 -38
  20. package/cli/commands/upload.ts +55 -49
  21. package/cli/drivers/playwright-sim-host.ts +65 -42
  22. package/cli/flow-live-status.ts +0 -2
  23. package/cli/main.ts +2 -2
  24. package/cli/outbound-endpoints.ts +7 -5
  25. package/cli/pause-aware-timeout.ts +4 -0
  26. package/cli/ws-bridge.ts +19 -16
  27. package/detox/attempt-events.cjs +99 -6
  28. package/detox/attempt-events.d.cts +7 -0
  29. package/detox/expectations.ts +7 -3
  30. package/detox/index.ts +259 -139
  31. package/detox/jest-environment.cjs +5 -1
  32. package/detox/motion-change.cjs +170 -21
  33. package/detox/motion-change.d.cts +32 -0
  34. package/detox/navigation-lifecycle.ts +35 -3
  35. package/dist-lib/agent-daemon-client.cjs +1 -1
  36. package/dist-lib/agent-events.cjs +1 -1
  37. package/dist-lib/attached-projects.cjs +1 -1
  38. package/dist-lib/auth/shared-session.cjs +1 -1
  39. package/dist-lib/backend-origin.cjs +17 -7
  40. package/dist-lib/beta.cjs +1 -1
  41. package/dist-lib/beta.mjs +1 -1
  42. package/dist-lib/bridge-constants.cjs +1 -1
  43. package/dist-lib/bridge-contract-input.cjs +1 -1
  44. package/dist-lib/bridge-contract-input.mjs +1 -1
  45. package/dist-lib/bridge-contract.cjs +1 -1
  46. package/dist-lib/bridge-contract.mjs +1 -1
  47. package/dist-lib/bundle-channel.cjs +5014 -2268
  48. package/dist-lib/capture-contract.cjs +1 -1
  49. package/dist-lib/capture-contract.mjs +1 -1
  50. package/dist-lib/cloud-contract.cjs +1 -1
  51. package/dist-lib/cloud-contract.mjs +1 -1
  52. package/dist-lib/cloud-test-args.cjs +1 -1
  53. package/dist-lib/cloud-test-args.mjs +1 -1
  54. package/dist-lib/cloud.cjs +1 -1
  55. package/dist-lib/cloud.mjs +1 -1
  56. package/dist-lib/config.cjs +1 -1
  57. package/dist-lib/config.mjs +1 -1
  58. package/dist-lib/detox/index.cjs +1036 -166
  59. package/dist-lib/detox/jest-environment.cjs +5 -1
  60. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  61. package/dist-lib/expo-swift-bundle.cjs +4613 -1963
  62. package/dist-lib/expo-swift-metadata.cjs +9 -7
  63. package/dist-lib/expo-swift-metadata.mjs +9 -7
  64. package/dist-lib/home-paths.cjs +1 -1
  65. package/dist-lib/host/bridge-host.cjs +58072 -16400
  66. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  67. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  68. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  69. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  70. package/dist-lib/host/websocket-proxy.cjs +1 -1
  71. package/dist-lib/index.cjs +2790 -143
  72. package/dist-lib/jump-to-source-babel.cjs +1 -1
  73. package/dist-lib/jump-to-source-native.cjs +1 -1
  74. package/dist-lib/kotlin-host.cjs +1 -1
  75. package/dist-lib/menu.cjs +1 -1
  76. package/dist-lib/menu.mjs +1 -1
  77. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  78. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  79. package/dist-lib/metro-production-bundle.cjs +1 -1
  80. package/dist-lib/metro-production-bundle.mjs +1 -1
  81. package/dist-lib/metro.cjs +2579 -95
  82. package/dist-lib/native-app-identity-project.cjs +243 -53
  83. package/dist-lib/native-app-identity-project.mjs +243 -53
  84. package/dist-lib/native-app-identity.cjs +29 -30
  85. package/dist-lib/native-app-identity.mjs +29 -30
  86. package/dist-lib/native-firebase-metadata.cjs +1 -1
  87. package/dist-lib/native-firebase-metadata.mjs +1 -1
  88. package/dist-lib/plist.cjs +1 -1
  89. package/dist-lib/plist.mjs +1 -1
  90. package/dist-lib/profiles.cjs +1 -1
  91. package/dist-lib/public-brand.cjs +1 -1
  92. package/dist-lib/react-native-host-modules.cjs +2 -2
  93. package/dist-lib/react-native-host-modules.mjs +2 -2
  94. package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
  95. package/dist-lib/sdk.cjs +4 -3
  96. package/dist-lib/sdk.mjs +4 -3
  97. package/dist-lib/skills.cjs +66475 -23097
  98. package/dist-lib/swift-compile.cjs +452 -260
  99. package/dist-lib/swift-compile.mjs +451 -252
  100. package/dist-lib/swift-package.cjs +1 -1
  101. package/dist-lib/swift-package.mjs +1 -1
  102. package/dist-lib/swift-submit.cjs +14 -7
  103. package/dist-lib/swift-submit.mjs +14 -7
  104. package/dist-lib/swift-toolchain.cjs +316 -41
  105. package/dist-lib/swift.cjs +1 -1
  106. package/dist-lib/vite-swift.cjs +8188 -5636
  107. package/dist-lib/vite.cjs +2857 -12
  108. package/dist-lib/worklet-executable.cjs +14 -2
  109. package/dist-lib/worklet-executable.mjs +14 -2
  110. package/package.json +1 -1
  111. package/src/backend-origin.ts +21 -6
  112. package/src/expo-swift-host.ts +5 -2
  113. package/src/expoKotlinContract.ts +4 -7
  114. package/src/expoSwiftContract.ts +4 -6
  115. package/src/host/staged-host-files.ts +9 -0
  116. package/src/native-app-identity-project.ts +1 -4
  117. package/src/native-app-identity.ts +1 -4
  118. package/src/nativeAppIconContract.ts +1 -4
  119. package/src/output-lock-stdio.ts +36 -0
  120. package/src/react-native-bridge-modules.ts +106 -78
  121. package/src/react-native-host-modules.ts +4 -1
  122. package/src/settings-bundle.ts +1 -4
  123. package/src/swift-compile.ts +224 -139
  124. package/src/swift-distribution.lock.json +23 -12
  125. package/src/swift-distribution.ts +103 -4
  126. package/src/swift-host.ts +95 -30
  127. package/src/swift-icu.ts +16 -10
  128. package/src/swift-objc-compile.ts +547 -0
  129. package/src/swift-objc-metadata.ts +204 -0
  130. package/src/swift-objc-sdk.ts +883 -19
  131. package/src/swift-objc-standin.ts +126 -14
  132. package/src/swift-package-manifest.ts +42 -1
  133. package/src/swift-packages.ts +35 -21
  134. package/src/swift-root-lock.ts +153 -0
  135. package/src/swift-standin-emit.ts +40 -7
  136. package/src/swift-standins.ts +404 -138
  137. package/src/swift-submit.ts +18 -7
  138. package/src/swift-toolchain.ts +378 -46
  139. package/src/swiftContract.ts +12 -0
  140. package/src/worker-system-chunks.ts +11 -12
  141. package/src/worklet-executable.ts +19 -1
  142. package/src/xcode-project.ts +280 -33
@@ -15,16 +15,27 @@ import fs from 'node:fs'
15
15
  import path from 'node:path'
16
16
  import { getCliVersion } from './cli-version'
17
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, type ObjcSurface } 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,10 +44,11 @@ import {
33
44
  type SwiftStandinBehaviors,
34
45
  type StandinBehavior,
35
46
  } from './swift-standin-emit'
36
- import { swiftSdkOptions } from './swift-toolchain'
47
+ import { swiftEnvironment, swiftSdkOptions } from './swift-toolchain'
37
48
  import type { SwiftPackageBinary } from './swift-packages'
38
49
 
39
50
  const TRIPLE = 'wasm32-unknown-wasip1'
51
+ const WASI_EMULATION = ['_WASI_EMULATED_SIGNAL', '_WASI_EMULATED_MMAN']
40
52
  // every stand-in artifact and temporary workspace belongs to peach.
41
53
  const cacheDir = path.join(peachCacheDir(), 'swift-standins')
42
54
  // each binary stand-in is a target under its module's directory here.
@@ -68,10 +80,16 @@ interface Toolchain {
68
80
  standinModules?: string
69
81
  }
70
82
 
83
+ interface NativeFrameworkInputs {
84
+ dirs: string[]
85
+ digest: string
86
+ }
87
+
71
88
  function exec(file: string, args: string[], cwd?: string): string {
72
89
  return execFileSync(file, args, {
73
90
  cwd,
74
91
  encoding: 'utf8',
92
+ env: swiftEnvironment(),
75
93
  maxBuffer: 256 * 1024 * 1024,
76
94
  stdio: ['ignore', 'pipe', 'pipe'],
77
95
  })
@@ -99,14 +117,20 @@ const digests = new Map<string, { stamp: string; digest: string }>()
99
117
 
100
118
  function treeDigest(root: string): string {
101
119
  const names: string[] = []
120
+ const ancestors = new Set<string>()
102
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)
103
125
  for (const entry of fs
104
126
  .readdirSync(dir, { withFileTypes: true })
105
127
  .sort((left, right) => (left.name < right.name ? -1 : 1))) {
106
128
  const full = path.join(dir, entry.name)
107
- if (entry.isDirectory()) walk(full)
108
- 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))
109
132
  }
133
+ ancestors.delete(resolved)
110
134
  }
111
135
  walk(root)
112
136
  const stamp = names
@@ -222,6 +246,7 @@ const files = (dir: string, test: (name: string) => boolean): string[] =>
222
246
  /** the module surfaces an iOS simulator slice describes. */
223
247
  type Surface =
224
248
  | { kind: 'swift'; module: string; interface: string }
249
+ | (ObjcSurface & { kind: 'mixed'; interface: string })
225
250
  | { kind: 'c'; module: string; modulemap: string; headers: string }
226
251
  | (ObjcSurface & { kind: 'objc' })
227
252
 
@@ -262,15 +287,27 @@ function surfaces(xcframeworkDir: string): Surface[] {
262
287
  )
263
288
  const chosen =
264
289
  interfaces.find((name) => name.startsWith('arm64-')) ?? interfaces[0] ?? null
290
+ const modulemap = path.join(framework, 'Modules', 'module.modulemap')
265
291
  if (chosen) {
266
- found.push({
267
- kind: 'swift',
268
- module,
269
- interface: path.join(swiftmodule, chosen),
270
- })
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 })
271
309
  continue
272
310
  }
273
- const modulemap = path.join(framework, 'Modules', 'module.modulemap')
274
311
  if (fs.existsSync(modulemap))
275
312
  found.push(headerSurface(module, modulemap, path.join(framework, 'Headers')))
276
313
  }
@@ -286,6 +323,14 @@ function surfaces(xcframeworkDir: string): Surface[] {
286
323
  return found
287
324
  }
288
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
+
289
334
  // the peach modules an interface imports, compiled for wasm: a distribution
290
335
  // ships them; a checkout builds the ones named into its own build directory,
291
336
  // with the flags scripts/build.sh uses, so the two share one build.
@@ -366,11 +411,14 @@ export function swiftInterfaceForWasm(text: string): string {
366
411
  ? rooted.replace(/^(?=import[ \t])/m, 'import Foundation\n')
367
412
  : rooted
368
413
  return foundation
369
- .replace(/^(\/\/ swift-module-flags:.*)$/m, (flags) =>
370
- flags
371
- .replace(/-target \S+/, `-target ${TRIPLE}`)
372
- .replace(/\s-enable-objc-interop\b/, '')
373
- .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(' ')}`,
374
422
  )
375
423
  .replace(
376
424
  /@(?:objc(?:\s*\([^()]*\))?|objcMembers|nonobjc|IBOutlet|IBAction|IBInspectable|IBDesignable|_objcRuntimeName\([^()]*\))(?![\w(])/g,
@@ -384,17 +432,14 @@ function swiftStandin(
384
432
  toolchain: Toolchain,
385
433
  work: string,
386
434
  behavior: ReadonlyMap<string, StandinBehavior>,
435
+ siblings: ReadonlySet<string>,
387
436
  ): SwiftStandin {
388
437
  const text = swiftInterfaceForWasm(fs.readFileSync(surface.interface, 'utf8'))
389
438
  const search = path.join(work, 'interfaces')
390
439
  const moduleDir = path.join(search, `${surface.module}.swiftmodule`)
391
440
  fs.mkdirSync(moduleDir, { recursive: true })
392
441
  fs.writeFileSync(path.join(moduleDir, `${TRIPLE}.swiftinterface`), text)
393
- const imports = [
394
- ...text.matchAll(
395
- /^(?:@_exported[ \t]+)?import[ \t]+(?:(?:struct|class|enum|protocol|typealias|func|var|let)[ \t]+)?(\w+)/gm,
396
- ),
397
- ].map((match) => match[1]!)
442
+ const imports = swiftImportNames(text)
398
443
  const wasm = wasmFlags(toolchain, imports)
399
444
  const out = path.join(work, 'graphs')
400
445
  fs.mkdirSync(out, { recursive: true })
@@ -425,6 +470,37 @@ function swiftStandin(
425
470
  toolchain,
426
471
  work,
427
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,
428
504
  )
429
505
  }
430
506
 
@@ -442,10 +518,7 @@ function wasmFlags(toolchain: Toolchain, imports: string[]): string[] {
442
518
  '-resource-dir',
443
519
  resources,
444
520
  // the sdk toolset's emulation defines, which raw swiftc does not read
445
- '-Xcc',
446
- '-D_WASI_EMULATED_SIGNAL',
447
- '-Xcc',
448
- '-D_WASI_EMULATED_MMAN',
521
+ ...WASI_EMULATION.flatMap((name) => ['-Xcc', `-D${name}`]),
449
522
  ...peachModuleDirs(toolchain, imports).flatMap((dir) => ['-I', dir]),
450
523
  ...(toolchain.standinModules ? ['-I', toolchain.standinModules] : []),
451
524
  ]
@@ -475,7 +548,9 @@ function objcStandin(
475
548
  toolchain,
476
549
  work,
477
550
  behavior,
478
- binary.headerDependencies?.map((dependency) => dependency.module),
551
+ binary.headerDependencies?.map((dependency) =>
552
+ swiftTargetModuleName(dependency.module),
553
+ ),
479
554
  )
480
555
  }
481
556
 
@@ -528,17 +603,21 @@ function graphStandin(
528
603
  fs.writeFileSync(file, source)
529
604
  const check = spawnSync(
530
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
531
608
  [
532
- '-typecheck',
609
+ '-emit-module',
533
610
  '-parse-as-library',
534
611
  '-swift-version',
535
612
  '5',
536
613
  '-module-name',
537
614
  module,
538
615
  ...wasm,
616
+ '-emit-module-path',
617
+ path.join(work, `${module}.swiftmodule`),
539
618
  file,
540
619
  ],
541
- { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 },
620
+ { encoding: 'utf8', env: swiftEnvironment(), maxBuffer: 256 * 1024 * 1024 },
542
621
  )
543
622
  if (check.status === 0)
544
623
  return {
@@ -993,51 +1072,91 @@ export function standinLibrary(
993
1072
  throw new Error(`cargo built ${crate} for ${RUST_TARGET} but reported no ${library}`)
994
1073
  }
995
1074
 
996
- // source preflight shares project compilation's portable builder admission.
997
- // the complete resolved artifact travels as an input, including ignored local frameworks.
998
- function sourceBinaryStandins(
999
- 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[],
1000
1079
  toolchain: Toolchain,
1001
1080
  provided: ReadonlySet<string>,
1002
1081
  behavior: SwiftStandinBehaviors,
1003
- ): SwiftStandin[] | string {
1082
+ ): SwiftStandins {
1004
1083
  const repo = path.resolve(toolchain.moduleRoot, '../..')
1005
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
+ )
1006
1098
  const work = fs.mkdtempSync(path.join(cacheDir, 'request-'))
1007
1099
  try {
1008
- const artifact =
1009
- binary.publicHeaders !== undefined
1010
- ? { dir: path.join(work, 'headers') }
1011
- : xcframework(binary)
1012
- const local = { ...binary, dir: artifact.dir, url: null, checksum: null }
1013
- if (binary.publicHeaders !== undefined) {
1014
- const dependencies = binary.headerDependencies ?? []
1015
- for (const header of [
1016
- { module: binary.target, path: binary.publicHeaders },
1017
- ...dependencies,
1018
- ])
1019
- fs.cpSync(
1020
- path.join(binary.dir, header.path),
1021
- path.join(artifact.dir, header.module),
1022
- { recursive: true },
1023
- )
1024
- local.publicHeaders = binary.target
1025
- local.headerDependencies = dependencies.map(({ module }) => ({
1026
- module,
1027
- path: module,
1028
- }))
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
+ }
1029
1141
  }
1030
- const generation = standinGenerationInputs([local], toolchain, behavior)
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')
1031
1146
  const request = path.join(work, 'request.json')
1032
1147
  const delivery = path.join(work, 'delivery')
1033
1148
  fs.writeFileSync(
1034
1149
  request,
1035
1150
  JSON.stringify({
1036
- binary: local,
1151
+ binaries: local.map((binary) => ({
1152
+ ...binary,
1153
+ dir: path.relative(artifact, binary.dir) || '.',
1154
+ })),
1037
1155
  provided: [...provided],
1038
1156
  behavior,
1039
- generation,
1157
+ generation: preparedGeneration,
1040
1158
  release: toolchain.release,
1159
+ native: standinsNeedNativeSdk(local),
1041
1160
  }),
1042
1161
  )
1043
1162
  const build = spawnSync(
@@ -1047,56 +1166,104 @@ function sourceBinaryStandins(
1047
1166
  '--request',
1048
1167
  request,
1049
1168
  '--artifact',
1050
- artifact.dir,
1169
+ artifact,
1051
1170
  '--delivery-dir',
1052
1171
  delivery,
1053
1172
  ],
1054
- { 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
+ },
1055
1179
  )
1056
1180
  if (build.stdout) process.stdout.write(build.stdout)
1057
1181
  if (build.stderr) process.stderr.write(build.stderr)
1058
1182
  if (build.error || build.status !== 0)
1059
1183
  throw new Error(
1060
- `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'}`,
1061
1185
  )
1062
1186
  const output = path.join(delivery, 'standins.json')
1063
1187
  if (!fs.existsSync(output))
1064
- throw new Error(
1065
- `source stand-in builder returned no standins.json for ${binary.target}`,
1066
- )
1188
+ throw new Error('source stand-in builder returned no standins.json')
1067
1189
  const receipt: unknown = JSON.parse(fs.readFileSync(output, 'utf8'))
1068
- if (
1069
- !receipt ||
1070
- typeof receipt !== 'object' ||
1071
- !('generation' in receipt) ||
1072
- receipt.generation !== generation ||
1073
- !('result' in receipt)
1074
- )
1075
- throw new Error('Swift stand-in builder source differs from the caller')
1076
- const result = receipt.result
1077
- if (typeof result === 'string') throw new Error(result)
1078
- if (
1079
- !Array.isArray(result) ||
1080
- !result.every(
1081
- (item) =>
1082
- item &&
1083
- typeof item === 'object' &&
1084
- typeof item.module === 'string' &&
1085
- typeof item.target === 'string' &&
1086
- item.target === binary.target &&
1087
- item.files &&
1088
- typeof item.files === 'object' &&
1089
- !Array.isArray(item.files) &&
1090
- Object.values(item.files).every((text) => typeof text === 'string'),
1091
- )
1190
+ const result = readStandinResults(
1191
+ receipt,
1192
+ preparedGeneration,
1193
+ new Set(local.map((binary) => binary.target)),
1092
1194
  )
1093
- 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
+ }
1094
1206
  return result
1095
1207
  } finally {
1096
1208
  fs.rmSync(work, { recursive: true, force: true })
1097
1209
  }
1098
1210
  }
1099
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
+
1100
1267
  const built = new Map<string, SwiftStandin[] | string>()
1101
1268
 
1102
1269
  /** what stand-ins are generated from besides the surfaces' files: the
@@ -1111,13 +1278,14 @@ export function standinGenerationInputs(
1111
1278
  const { moduleRoot, release } = toolchain
1112
1279
  const sdk = swiftModuleSdk(moduleRoot)
1113
1280
  const parts = [
1114
- 'standin/v6',
1281
+ 'standin/v9',
1115
1282
  release,
1116
1283
  sdk.wasmSdkId,
1117
1284
  sdk.wasmSdkChecksum,
1118
1285
  swiftModuleDigest(moduleRoot).toString('hex'),
1119
1286
  `emit/${STANDIN_EMIT_VERSION}`,
1120
1287
  `ios-sdk/${IOS_SDK_ID}`,
1288
+ `objc-import/${OBJC_IMPORT_VERSION}`,
1121
1289
  // a released cli can change the emitter without anyone bumping the constant
1122
1290
  `cli/${getCliVersion()}`,
1123
1291
  ]
@@ -1125,7 +1293,7 @@ export function standinGenerationInputs(
1125
1293
  binaries.flatMap((binary) => {
1126
1294
  try {
1127
1295
  return binary.publicHeaders !== undefined
1128
- ? [binary.target]
1296
+ ? [swiftTargetModuleName(binary.target)]
1129
1297
  : surfaces(xcframework(binary).dir).map((surface) => surface.module)
1130
1298
  } catch {
1131
1299
  return []
@@ -1147,7 +1315,22 @@ export function standinGenerationInputs(
1147
1315
  ),
1148
1316
  )
1149
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)}`)
1150
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}`)
1151
1334
  parts.push(
1152
1335
  binary.target,
1153
1336
  binary.source,
@@ -1158,7 +1341,7 @@ export function standinGenerationInputs(
1158
1341
  treeDigest(path.join(binary.dir, binary.publicHeaders)),
1159
1342
  ...(binary.headerDependencies ?? []).map(
1160
1343
  (dependency) =>
1161
- `${dependency.module}\0${treeDigest(path.join(binary.dir, dependency.path))}`,
1344
+ `${swiftTargetModuleName(dependency.module)}\0${treeDigest(path.join(binary.dir, dependency.path))}`,
1162
1345
  ),
1163
1346
  ].join('\0')
1164
1347
  : xcframework(binary).digest,
@@ -1187,6 +1370,8 @@ function binaryStandins(
1187
1370
  toolchain: Toolchain,
1188
1371
  provided: ReadonlySet<string>,
1189
1372
  behavior: SwiftStandinBehaviors,
1373
+ frameworks: NativeFrameworkInputs,
1374
+ siblings: ReadonlySet<string>,
1190
1375
  ): SwiftStandin[] | string {
1191
1376
  if (provided.has(binary.target)) return []
1192
1377
  // a local artifact a script downloads or builds is absent from a fresh
@@ -1201,6 +1386,7 @@ function binaryStandins(
1201
1386
  try {
1202
1387
  if (binary.publicHeaders !== undefined) {
1203
1388
  const headers = path.join(binary.dir, binary.publicHeaders)
1389
+ const module = swiftTargetModuleName(binary.target)
1204
1390
  const modulemap = path.join(work, 'module.modulemap')
1205
1391
  const supplied = path.join(headers, 'module.modulemap')
1206
1392
  const roots = files(headers, (name) => name.endsWith('.h'))
@@ -1210,16 +1396,16 @@ function binaryStandins(
1210
1396
  modulemap,
1211
1397
  fs.existsSync(supplied)
1212
1398
  ? fs.readFileSync(supplied, 'utf8')
1213
- : `module ${binary.target} {\n${roots.map((file) => ` header "${file}"`).join('\n')}\n export *\n}\n`,
1399
+ : `module ${module} {\n${roots.map((file) => ` header "${file}"`).join('\n')}\n export *\n}\n`,
1214
1400
  )
1215
1401
  all = [
1216
1402
  {
1217
1403
  kind: 'objc',
1218
- module: binary.target,
1404
+ module,
1219
1405
  modulemap,
1220
1406
  headers,
1221
1407
  dependencies: binary.headerDependencies?.map((dependency) => ({
1222
- module: dependency.module,
1408
+ module: swiftTargetModuleName(dependency.module),
1223
1409
  headers: path.join(binary.dir, dependency.path),
1224
1410
  })),
1225
1411
  },
@@ -1228,7 +1414,7 @@ function binaryStandins(
1228
1414
  treeDigest(headers),
1229
1415
  ...(binary.headerDependencies ?? []).map(
1230
1416
  (dependency) =>
1231
- `${dependency.module}\0${treeDigest(path.join(binary.dir, dependency.path))}`,
1417
+ `${swiftTargetModuleName(dependency.module)}\0${treeDigest(path.join(binary.dir, dependency.path))}`,
1232
1418
  ),
1233
1419
  ].join('\0')
1234
1420
  } else {
@@ -1252,11 +1438,14 @@ function binaryStandins(
1252
1438
  standinGenerationInputs([binary], toolchain, behavior),
1253
1439
  )
1254
1440
  hash.update(digest)
1441
+ hash.update([...siblings].sort().join('\0'))
1442
+ if (found.some((surface) => surface.kind === 'mixed')) hash.update(frameworks.digest)
1255
1443
  for (const surface of found) {
1256
1444
  hash.update(`${surface.kind}\0${surface.module}\0`)
1257
1445
  for (const file of surface.kind === 'swift'
1258
1446
  ? [surface.interface]
1259
1447
  : [
1448
+ ...(surface.kind === 'mixed' ? [surface.interface] : []),
1260
1449
  surface.modulemap,
1261
1450
  ...files(surface.headers, (name) => name.endsWith('.h')).map((file) =>
1262
1451
  path.join(surface.headers, file),
@@ -1279,44 +1468,47 @@ function binaryStandins(
1279
1468
  }
1280
1469
  let result: SwiftStandin[] | string
1281
1470
  try {
1282
- result =
1283
- process.platform === 'darwin' &&
1284
- process.env.TM_EXEC_BUILD_PLATFORM !== 'mac' &&
1285
- process.env.TM_SESSION &&
1286
- process.env.CI !== 'true' &&
1287
- toolchain.moduleRoot === path.resolve(import.meta.dirname, '../../sootsim-swift') &&
1288
- !swiftDistributionAt(toolchain.moduleRoot)
1289
- ? sourceBinaryStandins(binary, toolchain, provided, behavior)
1290
- : found.map((surface) => {
1291
- const dir = path.join(work, surface.module)
1292
- try {
1293
- if (surface.kind === 'c' && configured.has(surface.module))
1294
- throw new Error(
1295
- "Configured behavior requires the framework's Swift declarations",
1296
- )
1297
- return surface.kind === 'swift'
1298
- ? swiftStandin(
1299
- surface,
1300
- binary,
1301
- toolchain,
1302
- dir,
1303
- new Map(Object.entries(configured.get(surface.module) ?? {})),
1304
- )
1305
- : surface.kind === 'objc'
1306
- ? objcStandin(
1307
- surface,
1308
- binary,
1309
- toolchain,
1310
- dir,
1311
- new Map(Object.entries(configured.get(surface.module) ?? {})),
1312
- )
1313
- : cStandin(surface, binary, toolchain, dir)
1314
- } catch (error) {
1315
- throw new Error(
1316
- `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,
1317
1496
  )
1318
- }
1319
- })
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
+ })
1320
1512
  if (typeof result === 'string') throw new Error(result)
1321
1513
  fs.mkdirSync(path.dirname(cached), { recursive: true })
1322
1514
  // publish a complete entry so concurrent workspaces never read partial json.
@@ -1352,26 +1544,100 @@ export function swiftStandins(
1352
1544
  provided: ReadonlySet<string> = new Set(),
1353
1545
  behavior: SwiftStandinBehaviors = {},
1354
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)
1355
1558
  const standins: SwiftStandin[] = []
1356
1559
  const gaps = new Map<string, string>()
1357
1560
  fs.mkdirSync(cacheDir, { recursive: true })
1358
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>>()
1569
+ for (const binary of binaries) {
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
+ }
1359
1594
  const generatedToolchain = { ...toolchain, standinModules: modules }
1360
- const byTarget = new Map(binaries.map((binary) => [binary.target, binary]))
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
+ )
1361
1612
  const prerequisites = new Set(
1362
- binaries.flatMap((binary) =>
1363
- (binary.headerDependencies ?? []).map((dependency) => dependency.module),
1364
- ),
1613
+ binaries.flatMap((binary) => [
1614
+ ...(binary.headerDependencies ?? []).map((dependency) =>
1615
+ swiftTargetModuleName(dependency.module),
1616
+ ),
1617
+ ...siblings.get(binary)!,
1618
+ ]),
1365
1619
  )
1366
1620
  const visited = new Set<string>()
1367
1621
  const visit = (binary: SwiftPackageBinary) => {
1368
1622
  if (visited.has(binary.target)) return
1369
1623
  visited.add(binary.target)
1370
- for (const dependency of binary.headerDependencies ?? []) {
1371
- const target = byTarget.get(dependency.module)
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)
1372
1631
  if (target) visit(target)
1373
1632
  }
1374
- const result = binaryStandins(binary, generatedToolchain, provided, behavior)
1633
+ const result = binaryStandins(
1634
+ binary,
1635
+ generatedToolchain,
1636
+ provided,
1637
+ behavior,
1638
+ frameworks,
1639
+ siblings.get(binary)!,
1640
+ )
1375
1641
  if (typeof result === 'string') {
1376
1642
  gaps.set(binary.target, result)
1377
1643
  return