peachsim 0.1.1620 → 0.1.1635

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 (185) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +45 -6
  3. package/cli/app-config.ts +4 -1
  4. package/cli/app-fonts.ts +39 -18
  5. package/cli/app-stubs.ts +224 -9
  6. package/cli/bridge-flow-runner.ts +248 -156
  7. package/cli/cloud-client.ts +68 -57
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/box/search-command.ts +3 -2
  10. package/cli/commands/box.ts +10 -29
  11. package/cli/commands/control.ts +391 -92
  12. package/cli/commands/doctor.ts +17 -184
  13. package/cli/commands/flow.ts +79 -11
  14. package/cli/commands/inspect/core.ts +10 -5
  15. package/cli/commands/inspect/settling.ts +71 -22
  16. package/cli/commands/inspect/wait-ready.ts +12 -0
  17. package/cli/commands/inspect.ts +26 -3
  18. package/cli/commands/install-desktop.ts +1 -0
  19. package/cli/commands/maestro-compare.ts +105 -67
  20. package/cli/commands/maestro.ts +9 -1
  21. package/cli/commands/platform.ts +71 -36
  22. package/cli/commands/record.ts +21 -1
  23. package/cli/commands/screenshot.ts +23 -1
  24. package/cli/commands/serve.ts +7 -2
  25. package/cli/commands/setup.ts +19 -42
  26. package/cli/commands/swift.ts +5 -0
  27. package/cli/commands/upload.ts +51 -8
  28. package/cli/desktop-companion.ts +102 -17
  29. package/cli/dev-build-freshness.ts +37 -2
  30. package/cli/drivers/desktop.ts +14 -1
  31. package/cli/drivers/playwright-sim-host.ts +44 -8
  32. package/cli/drivers/playwright.ts +9 -8
  33. package/cli/drivers/types.ts +5 -0
  34. package/cli/maestro-accessibility.ts +53 -0
  35. package/cli/maestro-compare.ts +108 -12
  36. package/cli/main.ts +2 -1
  37. package/cli/outbound-endpoints.ts +35 -6
  38. package/cli/parent-pid.ts +0 -9
  39. package/cli/pause-aware-timeout.ts +102 -0
  40. package/cli/remote-simulators.ts +112 -0
  41. package/cli/remoteSimulatorContract.ts +21 -0
  42. package/cli/scene-video.ts +89 -0
  43. package/cli/ws-bridge.ts +23 -14
  44. package/detox/attempt-events.cjs +4 -0
  45. package/detox/expectations.ts +5 -1
  46. package/detox/index.ts +241 -181
  47. package/detox/motion-change.cjs +73 -0
  48. package/detox/motion-change.d.cts +21 -0
  49. package/detox/navigation-lifecycle.ts +22 -4
  50. package/dist-lib/agent-daemon-client.cjs +3 -2
  51. package/dist-lib/agent-events.cjs +1 -1
  52. package/dist-lib/attached-projects.cjs +1 -1
  53. package/dist-lib/auth/shared-session.cjs +3 -2
  54. package/dist-lib/backend-origin.cjs +1 -1
  55. package/dist-lib/beta.cjs +1 -1
  56. package/dist-lib/beta.mjs +1 -1
  57. package/dist-lib/bridge-constants.cjs +13 -1
  58. package/dist-lib/bridge-contract-input.cjs +1 -1
  59. package/dist-lib/bridge-contract-input.mjs +1 -1
  60. package/dist-lib/bridge-contract.cjs +14 -8
  61. package/dist-lib/bridge-contract.mjs +13 -7
  62. package/dist-lib/bundle-channel.cjs +10480 -7962
  63. package/dist-lib/capture-contract.cjs +1 -1
  64. package/dist-lib/capture-contract.mjs +1 -1
  65. package/dist-lib/cloud-contract.cjs +1 -1
  66. package/dist-lib/cloud-contract.mjs +1 -1
  67. package/dist-lib/cloud-test-args.cjs +1 -1
  68. package/dist-lib/cloud-test-args.mjs +1 -1
  69. package/dist-lib/cloud.cjs +3 -2
  70. package/dist-lib/cloud.mjs +3 -2
  71. package/dist-lib/config.cjs +10 -6
  72. package/dist-lib/config.mjs +10 -6
  73. package/dist-lib/detox/index.cjs +1854 -966
  74. package/dist-lib/dev-bundle-resolution.cjs +64 -24
  75. package/dist-lib/expo-swift-bundle.cjs +8715 -6271
  76. package/dist-lib/expo-swift-metadata.cjs +7 -4
  77. package/dist-lib/expo-swift-metadata.mjs +7 -4
  78. package/dist-lib/home-paths.cjs +32 -3
  79. package/dist-lib/host/bridge-host.cjs +10135 -7060
  80. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  81. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  82. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  83. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  84. package/dist-lib/host/websocket-proxy.cjs +1 -1
  85. package/dist-lib/index.cjs +1843 -408
  86. package/dist-lib/jump-to-source-babel.cjs +1 -1
  87. package/dist-lib/jump-to-source-native.cjs +1 -1
  88. package/dist-lib/kotlin-host.cjs +1 -1
  89. package/dist-lib/menu.cjs +3 -2
  90. package/dist-lib/menu.mjs +3 -2
  91. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  92. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  93. package/dist-lib/metro-production-bundle.cjs +5 -4
  94. package/dist-lib/metro-production-bundle.mjs +5 -4
  95. package/dist-lib/metro.cjs +1182 -418
  96. package/dist-lib/native-app-identity-project.cjs +782 -23
  97. package/dist-lib/native-app-identity-project.mjs +781 -23
  98. package/dist-lib/native-app-identity.cjs +276 -9
  99. package/dist-lib/native-app-identity.mjs +273 -8
  100. package/dist-lib/native-firebase-metadata.cjs +5 -4
  101. package/dist-lib/native-firebase-metadata.mjs +5 -4
  102. package/dist-lib/plist.cjs +1 -1
  103. package/dist-lib/plist.mjs +1 -1
  104. package/dist-lib/profiles.cjs +1 -1
  105. package/dist-lib/public-brand.cjs +3 -2
  106. package/dist-lib/react-native-host-modules.cjs +8 -1
  107. package/dist-lib/react-native-host-modules.mjs +8 -1
  108. package/dist-lib/scripts/dev-server-scanner.cjs +106 -28
  109. package/dist-lib/sdk.cjs +115 -34
  110. package/dist-lib/sdk.mjs +115 -33
  111. package/dist-lib/skills.cjs +15274 -9904
  112. package/dist-lib/swift-compile.cjs +1165 -173
  113. package/dist-lib/swift-compile.mjs +1212 -203
  114. package/dist-lib/swift-package.cjs +12 -3
  115. package/dist-lib/swift-package.mjs +12 -3
  116. package/dist-lib/swift-submit.cjs +7 -1
  117. package/dist-lib/swift-submit.mjs +5 -1
  118. package/dist-lib/swift-toolchain.cjs +133 -18
  119. package/dist-lib/swift.cjs +1 -1
  120. package/dist-lib/vite-swift.cjs +2648 -862
  121. package/dist-lib/vite.cjs +70 -14
  122. package/dist-lib/worklet-executable.cjs +25 -2
  123. package/dist-lib/worklet-executable.mjs +25 -2
  124. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  125. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  126. package/package.json +2 -2
  127. package/scripts/dev-server-scanner.ts +35 -14
  128. package/skills/peach-perf/SKILL.md +11 -5
  129. package/skills/peach-setup/SKILL.md +16 -19
  130. package/src/admitted-mac-build.ts +34 -8
  131. package/src/android-app-identity.ts +146 -0
  132. package/src/android-autolinking.ts +75 -0
  133. package/src/android-string-value.ts +36 -0
  134. package/src/android-strings.ts +5 -36
  135. package/src/androidAutolinkingContract.ts +40 -0
  136. package/src/bridge-constants.ts +11 -0
  137. package/src/bridge-contract.ts +22 -34
  138. package/src/bundleGlobalFooter.ts +26 -7
  139. package/src/codex-client.ts +3 -2
  140. package/src/config.ts +15 -0
  141. package/src/desktop-host-delivery.ts +5 -1
  142. package/src/dev-bundle-resolution.ts +60 -26
  143. package/src/expo-kotlin-host.ts +6 -36
  144. package/src/expo-manifest-body.ts +13 -0
  145. package/src/expo-swift-bundle.ts +17 -12
  146. package/src/expo-swift-host.ts +640 -192
  147. package/src/expo-swift-ready.ts +78 -0
  148. package/src/expoSwiftContract.ts +19 -0
  149. package/src/home-paths.ts +48 -2
  150. package/src/host/bridge-host.ts +12 -57
  151. package/src/host/staged-host-files.ts +29 -2
  152. package/src/installed-package-versions.ts +54 -14
  153. package/src/launchMetadataContract.ts +24 -0
  154. package/src/metro-plugin.ts +37 -42
  155. package/src/native-app-identity-project.ts +142 -5
  156. package/src/native-app-identity.ts +176 -11
  157. package/src/native-firebase-config.ts +209 -13
  158. package/src/nitro-compile.ts +2 -5
  159. package/src/nitro-source.ts +147 -63
  160. package/src/process-group.ts +114 -61
  161. package/src/process-output.ts +0 -9
  162. package/src/public-brand.ts +1 -0
  163. package/src/react-native-bridge-modules.ts +416 -0
  164. package/src/react-native-host-modules.ts +7 -0
  165. package/src/sim-client.ts +6 -4
  166. package/src/swift-binary-modules.ts +2 -2
  167. package/src/swift-compile.ts +1007 -108
  168. package/src/swift-compiler-digest.ts +14 -0
  169. package/src/swift-distribution.lock.json +40 -12
  170. package/src/swift-distribution.ts +61 -9
  171. package/src/swift-host.ts +149 -56
  172. package/src/swift-icu.ts +14 -6
  173. package/src/swift-objc-sdk.ts +133 -10
  174. package/src/swift-objc-standin.ts +38 -3
  175. package/src/swift-package-manifest.ts +21 -6
  176. package/src/swift-package.ts +15 -1
  177. package/src/swift-packages.ts +119 -60
  178. package/src/swift-standin-emit.ts +57 -9
  179. package/src/swift-standins.ts +185 -25
  180. package/src/swift-submit.ts +10 -0
  181. package/src/swift-toolchain.ts +149 -23
  182. package/src/vite-plugin.ts +20 -20
  183. package/src/worklet-executable.ts +30 -3
  184. package/src/worklets-babel.ts +35 -14
  185. package/src/xcode-project.ts +10 -0
@@ -12,9 +12,9 @@
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
18
  import { parsePlistSource } from './plist'
19
19
  import {
20
20
  swiftDistributionAt,
@@ -24,7 +24,7 @@ import {
24
24
  swiftPeachModules,
25
25
  swiftSdkPaths,
26
26
  } from './swift-compile'
27
- import { IOS_SDK_ID, objcView } from './swift-objc-standin'
27
+ import { IOS_SDK_ID, objcView, type ObjcSurface } from './swift-objc-standin'
28
28
  import {
29
29
  STANDIN_EMIT_VERSION,
30
30
  STANDIN_IMPORTS,
@@ -33,11 +33,14 @@ import {
33
33
  type SwiftStandinBehaviors,
34
34
  type StandinBehavior,
35
35
  } from './swift-standin-emit'
36
+ import { swiftSdkOptions } from './swift-toolchain'
36
37
  import type { SwiftPackageBinary } from './swift-packages'
37
38
 
38
39
  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')
40
+ // every stand-in artifact and temporary workspace belongs to peach.
41
+ const cacheDir = path.join(peachCacheDir(), 'swift-standins')
42
+ // each binary stand-in is a target under its module's directory here.
43
+ export const SWIFT_STANDINS_DIR = '__standins__'
41
44
 
42
45
  export interface SwiftStandin {
43
46
  /** the module it stands in for, which names its compile target. */
@@ -46,6 +49,8 @@ export interface SwiftStandin {
46
49
  target: string
47
50
  /** the target's files, relative to its directory. */
48
51
  files: Record<string, string>
52
+ /** other generated modules named by this target's public headers. */
53
+ dependencies?: string[]
49
54
  }
50
55
 
51
56
  export interface SwiftStandins {
@@ -59,6 +64,8 @@ interface Toolchain {
59
64
  toolchainBin: string
60
65
  /** the toolchain release, read once where the toolchain is resolved. */
61
66
  release: string
67
+ /** modules generated earlier in this dependency closure. */
68
+ standinModules?: string
62
69
  }
63
70
 
64
71
  function exec(file: string, args: string[], cwd?: string): string {
@@ -216,7 +223,7 @@ const files = (dir: string, test: (name: string) => boolean): string[] =>
216
223
  type Surface =
217
224
  | { kind: 'swift'; module: string; interface: string }
218
225
  | { kind: 'c'; module: string; modulemap: string; headers: string }
219
- | { kind: 'objc'; module: string; modulemap: string; headers: string }
226
+ | (ObjcSurface & { kind: 'objc' })
220
227
 
221
228
  const OBJC = /^\s*(?:@interface|@protocol|@class|@import|#import)\b/m
222
229
 
@@ -303,10 +310,12 @@ function peachModuleDirs(toolchain: Toolchain, imports: string[]): string[] {
303
310
  path.join(toolchainBin, 'swift'),
304
311
  [
305
312
  'build',
313
+ ...swiftPackageCacheOptions(),
306
314
  '--build-system',
307
315
  'native',
308
316
  '-j',
309
317
  '4',
318
+ ...swiftSdkOptions(),
310
319
  '--swift-sdk',
311
320
  swiftModuleSdk(moduleRoot).wasmSdkId,
312
321
  '-c',
@@ -432,7 +441,13 @@ function wasmFlags(toolchain: Toolchain, imports: string[]): string[] {
432
441
  sysroot,
433
442
  '-resource-dir',
434
443
  resources,
444
+ // 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',
435
449
  ...peachModuleDirs(toolchain, imports).flatMap((dir) => ['-I', dir]),
450
+ ...(toolchain.standinModules ? ['-I', toolchain.standinModules] : []),
436
451
  ]
437
452
  }
438
453
 
@@ -460,6 +475,7 @@ function objcStandin(
460
475
  toolchain,
461
476
  work,
462
477
  behavior,
478
+ binary.headerDependencies?.map((dependency) => dependency.module),
463
479
  )
464
480
  }
465
481
 
@@ -475,6 +491,7 @@ function graphStandin(
475
491
  toolchain: Toolchain,
476
492
  work: string,
477
493
  behavior: ReadonlyMap<string, StandinBehavior>,
494
+ imports: string[] = [],
478
495
  ): SwiftStandin {
479
496
  const paths = new Set(
480
497
  graphs.flatMap((graph) =>
@@ -498,6 +515,9 @@ function graphStandin(
498
515
  behavior,
499
516
  omit,
500
517
  plain,
518
+ inert: binary.publicHeaders !== undefined,
519
+ imports,
520
+ family: imports,
501
521
  })
502
522
  for (const entry of skipped) {
503
523
  if (behavior.has(entry.path))
@@ -525,6 +545,7 @@ function graphStandin(
525
545
  module,
526
546
  target: binary.target,
527
547
  files: { [`${module}.swift`]: source },
548
+ ...(imports.length ? { dependencies: imports } : {}),
528
549
  }
529
550
  const before = omit.size + plain.size
530
551
  const elsewhere: string[] = []
@@ -899,6 +920,7 @@ export function standinLibrary(
899
920
  binary: SwiftPackageBinary,
900
921
  searchRoot: string,
901
922
  ): string | null {
923
+ if (binary.publicHeaders !== undefined) return null
902
924
  const dir = xcframework(binary).dir
903
925
  const library = simulatorLibraryName(dir)
904
926
  const supplied = path.join(dir, RUST_TARGET)
@@ -920,14 +942,18 @@ export function standinLibrary(
920
942
  [
921
943
  'build',
922
944
  '--release',
945
+ '--locked',
923
946
  '--target',
924
947
  RUST_TARGET,
925
948
  '--message-format=json-render-diagnostics',
926
949
  '--manifest-path',
927
950
  manifest,
951
+ '--target-dir',
952
+ path.join(cacheDir, 'cargo', key(manifest)),
928
953
  ],
929
954
  {
930
955
  encoding: 'utf8',
956
+ env: { ...process.env, CARGO_HOME: path.join(cacheDir, 'cargo-home') },
931
957
  maxBuffer: 256 * 1024 * 1024,
932
958
  stdio: ['ignore', 'pipe', 'pipe'],
933
959
  },
@@ -976,10 +1002,31 @@ function sourceBinaryStandins(
976
1002
  behavior: SwiftStandinBehaviors,
977
1003
  ): SwiftStandin[] | string {
978
1004
  const repo = path.resolve(toolchain.moduleRoot, '../..')
979
- const work = fs.mkdtempSync(path.join(os.tmpdir(), 'peach-standin-request-'))
1005
+ fs.mkdirSync(cacheDir, { recursive: true })
1006
+ const work = fs.mkdtempSync(path.join(cacheDir, 'request-'))
980
1007
  try {
981
- const artifact = xcframework(binary)
1008
+ const artifact =
1009
+ binary.publicHeaders !== undefined
1010
+ ? { dir: path.join(work, 'headers') }
1011
+ : xcframework(binary)
982
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
+ }))
1029
+ }
983
1030
  const generation = standinGenerationInputs([local], toolchain, behavior)
984
1031
  const request = path.join(work, 'request.json')
985
1032
  const delivery = path.join(work, 'delivery')
@@ -993,7 +1040,7 @@ function sourceBinaryStandins(
993
1040
  release: toolchain.release,
994
1041
  }),
995
1042
  )
996
- execFileSync(
1043
+ const build = spawnSync(
997
1044
  'bun',
998
1045
  [
999
1046
  path.join(repo, 'scripts/build/swift-standins-submit.ts'),
@@ -1004,11 +1051,20 @@ function sourceBinaryStandins(
1004
1051
  '--delivery-dir',
1005
1052
  delivery,
1006
1053
  ],
1007
- { cwd: repo, stdio: 'inherit' },
1008
- )
1009
- const receipt: unknown = JSON.parse(
1010
- fs.readFileSync(path.join(delivery, 'standins.json'), 'utf8'),
1054
+ { cwd: repo, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 },
1011
1055
  )
1056
+ if (build.stdout) process.stdout.write(build.stdout)
1057
+ if (build.stderr) process.stderr.write(build.stderr)
1058
+ if (build.error || build.status !== 0)
1059
+ 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'}`,
1061
+ )
1062
+ const output = path.join(delivery, 'standins.json')
1063
+ if (!fs.existsSync(output))
1064
+ throw new Error(
1065
+ `source stand-in builder returned no standins.json for ${binary.target}`,
1066
+ )
1067
+ const receipt: unknown = JSON.parse(fs.readFileSync(output, 'utf8'))
1012
1068
  if (
1013
1069
  !receipt ||
1014
1070
  typeof receipt !== 'object' ||
@@ -1055,7 +1111,7 @@ export function standinGenerationInputs(
1055
1111
  const { moduleRoot, release } = toolchain
1056
1112
  const sdk = swiftModuleSdk(moduleRoot)
1057
1113
  const parts = [
1058
- 'standin/v4',
1114
+ 'standin/v6',
1059
1115
  release,
1060
1116
  sdk.wasmSdkId,
1061
1117
  sdk.wasmSdkChecksum,
@@ -1068,7 +1124,9 @@ export function standinGenerationInputs(
1068
1124
  const modules = new Set(
1069
1125
  binaries.flatMap((binary) => {
1070
1126
  try {
1071
- return surfaces(xcframework(binary).dir).map((surface) => surface.module)
1127
+ return binary.publicHeaders !== undefined
1128
+ ? [binary.target]
1129
+ : surfaces(xcframework(binary).dir).map((surface) => surface.module)
1072
1130
  } catch {
1073
1131
  return []
1074
1132
  }
@@ -1095,7 +1153,15 @@ export function standinGenerationInputs(
1095
1153
  binary.source,
1096
1154
  binary.url ?? '',
1097
1155
  binary.checksum ?? '',
1098
- xcframework(binary).digest,
1156
+ binary.publicHeaders !== undefined
1157
+ ? [
1158
+ treeDigest(path.join(binary.dir, binary.publicHeaders)),
1159
+ ...(binary.headerDependencies ?? []).map(
1160
+ (dependency) =>
1161
+ `${dependency.module}\0${treeDigest(path.join(binary.dir, dependency.path))}`,
1162
+ ),
1163
+ ].join('\0')
1164
+ : xcframework(binary).digest,
1099
1165
  )
1100
1166
  } catch {
1101
1167
  // unreadable here fails again below, where it becomes a gap
@@ -1128,17 +1194,55 @@ function binaryStandins(
1128
1194
  // calls, so peach names the path and asks for the artifact
1129
1195
  if (!binary.url && !fs.existsSync(binary.dir))
1130
1196
  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`
1197
+ fs.mkdirSync(cacheDir, { recursive: true })
1198
+ const work = fs.mkdtempSync(path.join(cacheDir, 'work-'))
1131
1199
  let all: Surface[]
1132
1200
  let digest: string
1133
1201
  try {
1134
- const artifact = xcframework(binary)
1135
- all = surfaces(artifact.dir)
1136
- digest = artifact.digest
1202
+ if (binary.publicHeaders !== undefined) {
1203
+ const headers = path.join(binary.dir, binary.publicHeaders)
1204
+ const modulemap = path.join(work, 'module.modulemap')
1205
+ const supplied = path.join(headers, 'module.modulemap')
1206
+ const roots = files(headers, (name) => name.endsWith('.h'))
1207
+ if (roots.length === 0)
1208
+ throw new Error('its public header directory has no headers')
1209
+ fs.writeFileSync(
1210
+ modulemap,
1211
+ fs.existsSync(supplied)
1212
+ ? fs.readFileSync(supplied, 'utf8')
1213
+ : `module ${binary.target} {\n${roots.map((file) => ` header "${file}"`).join('\n')}\n export *\n}\n`,
1214
+ )
1215
+ all = [
1216
+ {
1217
+ kind: 'objc',
1218
+ module: binary.target,
1219
+ modulemap,
1220
+ headers,
1221
+ dependencies: binary.headerDependencies?.map((dependency) => ({
1222
+ module: dependency.module,
1223
+ headers: path.join(binary.dir, dependency.path),
1224
+ })),
1225
+ },
1226
+ ]
1227
+ digest = [
1228
+ treeDigest(headers),
1229
+ ...(binary.headerDependencies ?? []).map(
1230
+ (dependency) =>
1231
+ `${dependency.module}\0${treeDigest(path.join(binary.dir, dependency.path))}`,
1232
+ ),
1233
+ ].join('\0')
1234
+ } else {
1235
+ const artifact = xcframework(binary)
1236
+ all = surfaces(artifact.dir)
1237
+ digest = artifact.digest
1238
+ }
1137
1239
  } catch (error) {
1240
+ fs.rmSync(work, { recursive: true, force: true })
1138
1241
  return error instanceof Error ? error.message : String(error)
1139
1242
  }
1140
1243
  const found = all.filter((surface) => !provided.has(surface.module))
1141
1244
  if (found.length === 0) {
1245
+ fs.rmSync(work, { recursive: true, force: true })
1142
1246
  if (all.length > 0) return []
1143
1247
  return 'its iOS simulator slice has no Swift interface or C modulemap'
1144
1248
  }
@@ -1162,18 +1266,22 @@ function binaryStandins(
1162
1266
  }
1163
1267
  const id = hash.digest('hex').slice(0, 32)
1164
1268
  const memo = built.get(id)
1165
- if (memo) return memo
1269
+ if (memo) {
1270
+ fs.rmSync(work, { recursive: true, force: true })
1271
+ return memo
1272
+ }
1166
1273
  const cached = path.join(cacheDir, 'standins', `${id}.json`)
1167
1274
  if (fs.existsSync(cached)) {
1168
1275
  const value: SwiftStandin[] = JSON.parse(fs.readFileSync(cached, 'utf8'))
1169
1276
  built.set(id, value)
1277
+ fs.rmSync(work, { recursive: true, force: true })
1170
1278
  return value
1171
1279
  }
1172
- const work = fs.mkdtempSync(path.join(os.tmpdir(), 'peach-standin-'))
1173
1280
  let result: SwiftStandin[] | string
1174
1281
  try {
1175
1282
  result =
1176
1283
  process.platform === 'darwin' &&
1284
+ process.env.TM_EXEC_BUILD_PLATFORM !== 'mac' &&
1177
1285
  process.env.TM_SESSION &&
1178
1286
  process.env.CI !== 'true' &&
1179
1287
  toolchain.moduleRoot === path.resolve(import.meta.dirname, '../../sootsim-swift') &&
@@ -1246,10 +1354,62 @@ export function swiftStandins(
1246
1354
  ): SwiftStandins {
1247
1355
  const standins: SwiftStandin[] = []
1248
1356
  const gaps = new Map<string, string>()
1249
- for (const binary of binaries) {
1250
- const result = binaryStandins(binary, toolchain, provided, behavior)
1251
- if (typeof result === 'string') gaps.set(binary.target, result)
1252
- else standins.push(...result)
1357
+ fs.mkdirSync(cacheDir, { recursive: true })
1358
+ const modules = fs.mkdtempSync(path.join(cacheDir, 'modules-'))
1359
+ const generatedToolchain = { ...toolchain, standinModules: modules }
1360
+ const byTarget = new Map(binaries.map((binary) => [binary.target, binary]))
1361
+ const prerequisites = new Set(
1362
+ binaries.flatMap((binary) =>
1363
+ (binary.headerDependencies ?? []).map((dependency) => dependency.module),
1364
+ ),
1365
+ )
1366
+ const visited = new Set<string>()
1367
+ const visit = (binary: SwiftPackageBinary) => {
1368
+ if (visited.has(binary.target)) return
1369
+ visited.add(binary.target)
1370
+ for (const dependency of binary.headerDependencies ?? []) {
1371
+ const target = byTarget.get(dependency.module)
1372
+ if (target) visit(target)
1373
+ }
1374
+ const result = binaryStandins(binary, generatedToolchain, provided, behavior)
1375
+ if (typeof result === 'string') {
1376
+ gaps.set(binary.target, result)
1377
+ return
1378
+ }
1379
+ for (const standin of result) {
1380
+ // source objc dependents typecheck against the same generated declarations
1381
+ // that the app links, including on cache hits.
1382
+ if (prerequisites.has(standin.module)) {
1383
+ const source = standin.files[`${standin.module}.swift`]
1384
+ if (source !== undefined) {
1385
+ const file = path.join(modules, `${standin.module}.swift`)
1386
+ fs.writeFileSync(file, source)
1387
+ try {
1388
+ exec(path.join(toolchain.toolchainBin, 'swiftc'), [
1389
+ '-emit-module',
1390
+ '-parse-as-library',
1391
+ '-swift-version',
1392
+ '5',
1393
+ '-module-name',
1394
+ standin.module,
1395
+ ...wasmFlags(generatedToolchain, standin.dependencies ?? []),
1396
+ '-emit-module-path',
1397
+ path.join(modules, `${standin.module}.swiftmodule`),
1398
+ file,
1399
+ ])
1400
+ } catch (error) {
1401
+ gaps.set(binary.target, failure(error))
1402
+ return
1403
+ }
1404
+ }
1405
+ }
1406
+ standins.push(standin)
1407
+ }
1408
+ }
1409
+ try {
1410
+ for (const binary of binaries) visit(binary)
1411
+ return { standins, gaps }
1412
+ } finally {
1413
+ fs.rmSync(modules, { recursive: true, force: true })
1253
1414
  }
1254
- return { standins, gaps }
1255
1415
  }
@@ -32,6 +32,10 @@ export const SWIFT_HMR_PATH = '/__peach/swift-hmr'
32
32
 
33
33
  // shared with the compile service; collection enforces these before reading.
34
34
  export const MAX_SWIFT_COMPILE_FILES = 256
35
+ /** how long one swift build may run when the caller asks for no budget. */
36
+ export const DEFAULT_SWIFT_COMPILE_BUILD_MS = 10 * 60_000
37
+ /** the longest build budget a caller may request. */
38
+ export const MAX_SWIFT_COMPILE_BUILD_MS = 60 * 60_000
35
39
  export const MAX_SWIFT_COMPILE_SOURCE_BYTES = 2 * 1024 * 1024
36
40
  export const MAX_SWIFT_COMPILE_RESOURCES = 128
37
41
  export const MAX_SWIFT_COMPILE_RESOURCE_BYTES = 2 * 1024 * 1024
@@ -237,6 +241,11 @@ export type SwiftCompileSubmit = (work: {
237
241
  entry: string
238
242
  /** the directory-derived id of the One package. */
239
243
  packageId: string
244
+ /**
245
+ * how long the build may run, up to `MAX_SWIFT_COMPILE_BUILD_MS`; absent means
246
+ * the default. hosted requests above the default require an internal compile key.
247
+ */
248
+ buildBudgetMs?: number
240
249
  }) => Promise<SwiftCompileOutcome>
241
250
 
242
251
  /** the HTTP submitter, shared by the browser worker and the node build. */
@@ -394,6 +403,7 @@ export async function compileSwiftProject(
394
403
  images?: Record<string, SwiftImageUpload>
395
404
  entry: string
396
405
  packageId: string
406
+ buildBudgetMs?: number
397
407
  },
398
408
  origin: string,
399
409
  signal?: AbortSignal,
@@ -1,4 +1,4 @@
1
- // the one place that finds the swift.org toolchain app Swift compiles with.
1
+ // the one place that selects and verifies the toolchain app Swift compiles with.
2
2
  // a binary .swiftmodule loads only in the compiler release that wrote it, so
3
3
  // every caller names the release from packages/sootsim-swift/toolchain.json
4
4
  // or the Swift distribution's lock (docs/swift-distribution.md).
@@ -6,13 +6,61 @@
6
6
  // node builtins only: packages/sootsim-swift/scripts/build.sh imports this
7
7
  // file straight from a checkout, with no install.
8
8
  import { execFileSync } from 'node:child_process'
9
+ import { createHash } from 'node:crypto'
9
10
  import fs from 'node:fs'
10
11
  import os from 'node:os'
11
12
  import path from 'node:path'
12
13
 
14
+ /** swiftpm's SDK directory option, shared by discovery and compilation. */
15
+ export function swiftSdkOptions(): string[] {
16
+ const directory = process.env.PEACH_SWIFT_SDKS_PATH
17
+ return directory ? ['--swift-sdks-path', path.resolve(directory)] : []
18
+ }
19
+
20
+ // host archives include the official driver/runtime and the qualified patched frontend.
21
+ export const SWIFT_HOST_COMPILER_RELEASE = 'swift-6.4.0-RELEASE-custom-91504a66b3f012ac'
22
+ export const SWIFT_HOST_COMPILER_ID = '91504a66b3f012ac'
23
+ const compilerArchives: Record<string, { checksum: string; manifest: string }> = {
24
+ 'darwin-arm64': {
25
+ checksum: '5d0ecbafd88c896beac74fd74c9d5fb20f973e0f58abbc1d2f27d5af0cbc4e59',
26
+ manifest: '827130a60b782ed413c9df41410afe7baf32dcb132b945a70ac0ff4c0594c690',
27
+ },
28
+ 'linux-x64': {
29
+ checksum: '9d8491107efa347ccadf5f6974e80b2cff2b33cd8c34317dab777c5542512b3e',
30
+ manifest: '6221c024fa86b2201dcada23d9e13f6a11d7450cf22164a9708703f992de129f',
31
+ },
32
+ 'linux-arm64': {
33
+ checksum: '14f5c7b39b1e36a1d9ce426a875676182fdb5247763a7e99cb72f74957bc0d9a',
34
+ manifest: '57ff31b79f9b3d9a6c0fd51c73271cdd88088522c0fc6d7a28ade03d68008b68',
35
+ },
36
+ }
37
+
38
+ export function swiftHostCompilerArchive(
39
+ release: string,
40
+ host = `${process.platform}-${process.arch}`,
41
+ ) {
42
+ if (release !== SWIFT_HOST_COMPILER_RELEASE)
43
+ throw new Error(`unqualified host compiler ${release}`)
44
+ const archive = compilerArchives[host]
45
+ if (!archive) throw new Error(`unsupported Swift compiler host ${host}`)
46
+ return {
47
+ ...archive,
48
+ identity: SWIFT_HOST_COMPILER_ID,
49
+ url: `https://r2.sootsim.com/swift-host-compiler/${SWIFT_HOST_COMPILER_ID}/${release}-${host}.tar.gz`,
50
+ }
51
+ }
52
+
53
+ // source archives and bootstrap images retain the official release name.
54
+ export function swiftUpstreamRelease(release: string): string {
55
+ return release.replace(/-custom-[a-f0-9]{16}$/, '')
56
+ }
57
+
13
58
  // swift.org's x.y.0 archives report the compiler identity as x.y.
14
59
  export function swiftCompilerRelease(release: string): string {
15
- return release.replace(/^swift-(\d+\.\d+)\.0-RELEASE$/, 'swift-$1-RELEASE')
60
+ return swiftUpstreamRelease(release).replace(
61
+ /^swift-(\d+\.\d+)\.0-RELEASE$/,
62
+ 'swift-$1-RELEASE',
63
+ )
16
64
  }
17
65
 
18
66
  /**
@@ -33,7 +81,8 @@ export function swiftToolchainBin(release: string): string {
33
81
  return path.join(process.env.CONTRAST_SWIFT_TOOLCHAINS, release, 'usr/bin')
34
82
  }
35
83
  const image = path.join('/opt/swift', release, 'usr/bin')
36
- if (fs.existsSync(path.join(image, 'swift'))) return image
84
+ if (release.includes('-custom-') || fs.existsSync(path.join(image, 'swift')))
85
+ return image
37
86
  return (
38
87
  (process.env.PATH ?? '')
39
88
  .split(path.delimiter)
@@ -57,27 +106,20 @@ const verified = new Map<string, string>()
57
106
  * release and the commands that install it.
58
107
  */
59
108
  export function swiftToolchain(release: string, sdk: SwiftWasmSdk): string {
60
- const cached = verified.get(`${release}\0${sdk.wasmSdkId}`)
109
+ const key = JSON.stringify([release, sdk.wasmSdkId, swiftSdkOptions()])
110
+ const cached = verified.get(key)
61
111
  if (cached) return cached
62
- const bin = swiftToolchainBin(release)
63
- const swift = path.join(bin, 'swift')
64
- const version = fs.existsSync(swift)
65
- ? execFileSync(swift, ['--version'], {
66
- encoding: 'utf8',
67
- stdio: ['ignore', 'pipe', 'pipe'],
68
- })
69
- : ''
70
- if (!version.includes(`(${swiftCompilerRelease(release)})`)) {
71
- const found = version
72
- ? `found ${version.split('\n')[0]?.trim()} at ${swift}`
73
- : process.platform === 'darwin'
74
- ? `found no ${release}.xctoolchain in ~/Library/Developer/Toolchains or /Library/Developer/Toolchains`
75
- : 'found no swift on PATH'
112
+ let bin: string
113
+ try {
114
+ bin = swiftCompiler(release)
115
+ } catch (error) {
116
+ const reason = error instanceof Error ? error.message : String(error)
76
117
  throw new Error(
77
- `peach compiles Swift with the swift.org toolchain ${release} and its wasm SDK; ${found}.\n${swiftInstallSteps(release, sdk)}`,
118
+ `Swift compiler ${release} is unavailable or differs from its pin: ${reason}\n${swiftInstallSteps(release, sdk)}`,
78
119
  )
79
120
  }
80
- const sdks = execFileSync(swift, ['sdk', 'list'], {
121
+ const swift = path.join(bin, 'swift')
122
+ const sdks = execFileSync(swift, ['sdk', 'list', ...swiftSdkOptions()], {
81
123
  encoding: 'utf8',
82
124
  stdio: ['ignore', 'pipe', 'pipe'],
83
125
  })
@@ -86,15 +128,99 @@ export function swiftToolchain(release: string, sdk: SwiftWasmSdk): string {
86
128
  `peach compiles Swift with the ${sdk.wasmSdkId} SDK, which ${swift} does not have.\n${swiftInstallSteps(release, sdk)}`,
87
129
  )
88
130
  }
89
- verified.set(`${release}\0${sdk.wasmSdkId}`, bin)
131
+ verified.set(key, bin)
90
132
  return bin
91
133
  }
92
134
 
93
- /** the commands that install `release` and its wasm SDK on this host. */
135
+ // stream large compiler binaries without retaining a second copy in memory.
136
+ export function swiftFileChecksum(file: string): string {
137
+ const hash = createHash('sha256')
138
+ const fd = fs.openSync(file, 'r')
139
+ const buffer = Buffer.allocUnsafe(1024 * 1024)
140
+ try {
141
+ let size = fs.readSync(fd, buffer, 0, buffer.length, null)
142
+ while (size > 0) {
143
+ hash.update(buffer.subarray(0, size))
144
+ size = fs.readSync(fd, buffer, 0, buffer.length, null)
145
+ }
146
+ } finally {
147
+ fs.closeSync(fd)
148
+ }
149
+ return hash.digest('hex')
150
+ }
151
+
152
+ /** validate the pinned manifest and every replaced binary, independently of --version. */
153
+ export function verifySwiftHostCompiler(release: string, root: string): void {
154
+ const pin = swiftHostCompilerArchive(release)
155
+ const file = path.join(root, 'compiler.json')
156
+ if (swiftFileChecksum(file) !== pin.manifest)
157
+ throw new Error(`Swift compiler manifest mismatch at ${root}`)
158
+ const manifest: unknown = JSON.parse(fs.readFileSync(file, 'utf8'))
159
+ if (
160
+ !manifest ||
161
+ typeof manifest !== 'object' ||
162
+ !('identity' in manifest) ||
163
+ manifest.identity !== pin.identity ||
164
+ !('host' in manifest) ||
165
+ manifest.host !== `${process.platform}-${process.arch}` ||
166
+ !('files' in manifest) ||
167
+ !manifest.files ||
168
+ typeof manifest.files !== 'object'
169
+ ) {
170
+ throw new Error(`Swift compiler identity mismatch at ${root}`)
171
+ }
172
+ const files = Object.entries(manifest.files)
173
+ if (files.length !== 15) throw new Error('incomplete Swift compiler manifest')
174
+ for (const [relative, checksum] of files) {
175
+ if (
176
+ relative.startsWith('/') ||
177
+ relative.split('/').includes('..') ||
178
+ typeof checksum !== 'string' ||
179
+ swiftFileChecksum(path.join(root, relative)) !== checksum
180
+ ) {
181
+ throw new Error(`Swift compiler binary mismatch: ${relative}`)
182
+ }
183
+ }
184
+ }
185
+
186
+ const verifiedCompilers = new Map<string, string>()
187
+ /** select the actual compiler, including its patched identity, before consulting SDKs. */
188
+ export function swiftCompiler(release: string): string {
189
+ const cached = verifiedCompilers.get(release)
190
+ if (cached) return cached
191
+ const bin = swiftToolchainBin(release)
192
+ if (release.includes('-custom-'))
193
+ verifySwiftHostCompiler(release, path.resolve(bin, '../..'))
194
+ const version = execFileSync(path.join(bin, 'swift'), ['--version'], {
195
+ encoding: 'utf8',
196
+ stdio: ['ignore', 'pipe', 'pipe'],
197
+ })
198
+ if (!version.includes(`(${swiftCompilerRelease(release)})`))
199
+ throw new Error(`compiler release mismatch at ${bin}`)
200
+ verifiedCompilers.set(release, bin)
201
+ return bin
202
+ }
203
+
204
+ /** the commands that install the pinned compiler and SDK on this host. */
94
205
  export function swiftInstallSteps(release: string, wasmSdk: SwiftWasmSdk): string {
206
+ const sdkOptions = swiftSdkOptions()
207
+ .map((arg) => ` '${arg.replaceAll("'", "'\\''")}'`)
208
+ .join('')
209
+ if (release.includes('-custom-')) {
210
+ const archive = swiftHostCompilerArchive(release)
211
+ const directory = path.resolve(swiftToolchainBin(release), '../..')
212
+ return [
213
+ 'install the pinned compiler and SDK:',
214
+ ` curl -fsSL ${archive.url} -o compiler.tar.gz`,
215
+ ` echo '${archive.checksum} compiler.tar.gz' | shasum -a 256 -c -`,
216
+ ` mkdir -p '${directory}'`,
217
+ ` tar -xzf compiler.tar.gz --strip-components=1 -C '${directory}'`,
218
+ ` '${directory}/usr/bin/swift' sdk install ${wasmSdk.wasmSdkUrl} --checksum ${wasmSdk.wasmSdkChecksum}${sdkOptions}`,
219
+ ].join('\n')
220
+ }
95
221
  const version = release.replace(/^swift-/, '').toLowerCase()
96
222
  const downloads = `https://download.swift.org/swift-${version}`
97
- const sdk = `swift sdk install ${wasmSdk.wasmSdkUrl} --checksum ${wasmSdk.wasmSdkChecksum}`
223
+ const sdk = `swift sdk install ${wasmSdk.wasmSdkUrl} --checksum ${wasmSdk.wasmSdkChecksum}${sdkOptions}`
98
224
  if (process.platform === 'darwin') {
99
225
  const swift = `~/Library/Developer/Toolchains/${release}.xctoolchain/usr/bin/swift`
100
226
  return [