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
@@ -20,6 +20,7 @@
20
20
  * Browser-safe: node-only submitters live beside their call sites.
21
21
  */
22
22
 
23
+ import { isRecord } from './is-record'
23
24
  import {
24
25
  isSwiftMetalLibrarySources,
25
26
  type SwiftMetalLibrarySource,
@@ -108,7 +109,8 @@ export const MAX_SWIFT_COMPILE_IMAGE_TOTAL_BYTES = 16 * 1024 * 1024
108
109
  // 45: confirmation dialogs present from a measured `presentationSource`, and a
109
110
  // refreshable's pull ends on the host's `refreshEnded` request.
110
111
  // 46: ImageRenderer produces shared CoreGraphics pixels through the isolated raster host.
111
- export const SWIFT_HOST_ABI = 46
112
+ // 47: backing-layer masks composite the view, border and children through mask alpha.
113
+ export const SWIFT_HOST_ABI = 47
112
114
 
113
115
  export interface SwiftImageUpload {
114
116
  /** the raster file picked for the asset (has the extension). */
@@ -152,8 +154,10 @@ export type SwiftCompileOutcome =
152
154
  * the build is running; poll the same source set again. One build is tens of
153
155
  * seconds and a service-binding request is cancelled at about four, so the
154
156
  * container answers this before the connection dies and the caller polls.
157
+ * `stage` is the latest stage the compiler reported for this build, absent
158
+ * before it reports one.
155
159
  */
156
- | { status: 'compiling'; hash: string }
160
+ | { status: 'compiling'; hash: string; stage?: string }
157
161
  /** refused before any work: no entry, too many files, over a cap, over a rate limit. */
158
162
  | { status: 'refused'; detail: string }
159
163
  /** the compiler ran and failed; detail carries its diagnostic verbatim. */
@@ -294,10 +298,6 @@ export function httpSwiftCompileSubmit(
294
298
  }
295
299
  }
296
300
 
297
- function isRecord(value: unknown): value is Record<string, unknown> {
298
- return typeof value === 'object' && value !== null && !Array.isArray(value)
299
- }
300
-
301
301
  export function isSwiftNativeModules(value: unknown): value is SwiftNativeModules {
302
302
  return (
303
303
  isRecord(value) &&
@@ -335,7 +335,11 @@ function isSwiftCompileOutcome(value: unknown): value is SwiftCompileOutcome {
335
335
  (value.native === undefined || isSwiftNativeModules(value.native))
336
336
  )
337
337
  case 'compiling':
338
- return typeof value.hash === 'string' && /^[a-f0-9]{64}$/.test(value.hash)
338
+ return (
339
+ typeof value.hash === 'string' &&
340
+ /^[a-f0-9]{64}$/.test(value.hash) &&
341
+ (value.stage === undefined || typeof value.stage === 'string')
342
+ )
339
343
  case 'refused':
340
344
  case 'failed':
341
345
  return typeof value.detail === 'string'
@@ -407,7 +411,10 @@ export async function compileSwiftProject(
407
411
  },
408
412
  origin: string,
409
413
  signal?: AbortSignal,
414
+ /** called when a poll reports a compiler stage the previous poll did not. */
415
+ onStage?: (stage: string) => void,
410
416
  ): Promise<SwiftModuleAddress> {
417
+ let lastStage: string | undefined
411
418
  while (true) {
412
419
  signal?.throwIfAborted()
413
420
  const outcome = await submit(work)
@@ -433,6 +440,10 @@ export async function compileSwiftProject(
433
440
  ...(outcome.native === undefined ? {} : { native: outcome.native }),
434
441
  }
435
442
  case 'compiling':
443
+ if (outcome.stage !== undefined && outcome.stage !== lastStage) {
444
+ lastStage = outcome.stage
445
+ onStage?.(outcome.stage)
446
+ }
436
447
  await new Promise<void>((resolve, reject) => {
437
448
  const finish = () => {
438
449
  signal?.removeEventListener('abort', abort)
@@ -10,6 +10,7 @@ import { createHash } from 'node:crypto'
10
10
  import fs from 'node:fs'
11
11
  import os from 'node:os'
12
12
  import path from 'node:path'
13
+ import { fileURLToPath } from 'node:url'
13
14
 
14
15
  /** swiftpm's SDK directory option, shared by discovery and compilation. */
15
16
  export function swiftSdkOptions(): string[] {
@@ -18,20 +19,20 @@ export function swiftSdkOptions(): string[] {
18
19
  }
19
20
 
20
21
  // 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'
22
+ export const SWIFT_HOST_COMPILER_RELEASE = 'swift-6.4.0-RELEASE-custom-0d775d32d7c74eae'
23
+ export const SWIFT_HOST_COMPILER_ID = '0d775d32d7c74eae'
23
24
  const compilerArchives: Record<string, { checksum: string; manifest: string }> = {
24
25
  'darwin-arm64': {
25
- checksum: '5d0ecbafd88c896beac74fd74c9d5fb20f973e0f58abbc1d2f27d5af0cbc4e59',
26
- manifest: '827130a60b782ed413c9df41410afe7baf32dcb132b945a70ac0ff4c0594c690',
26
+ checksum: '957e85486f92327ee5cf8d3c5bb9336d17ab80145c87645c637c49130a50477a',
27
+ manifest: 'c63e2f81694e8d8ad046d10c71d0638dbae4e5a36601294ed670988d1cb5b6f1',
27
28
  },
28
29
  'linux-x64': {
29
- checksum: '9d8491107efa347ccadf5f6974e80b2cff2b33cd8c34317dab777c5542512b3e',
30
- manifest: '6221c024fa86b2201dcada23d9e13f6a11d7450cf22164a9708703f992de129f',
30
+ checksum: '6302230fb126b79c9af5fc2bc0208a936bb573c343da7ae949c5c1687e761cd2',
31
+ manifest: '31b507a6a8648a2266a187b9fed092e29a63cae99d57c5defdf2e82a7cbbc113',
31
32
  },
32
33
  'linux-arm64': {
33
- checksum: '14f5c7b39b1e36a1d9ce426a875676182fdb5247763a7e99cb72f74957bc0d9a',
34
- manifest: '57ff31b79f9b3d9a6c0fd51c73271cdd88088522c0fc6d7a28ade03d68008b68',
34
+ checksum: '9b40d39ea5ba82fd9a417868f0a449f5e98e37cf83acaac9cedd3a8b8d91a218',
35
+ manifest: 'fe07a29d4f4374cad47b29de70e90aa2e3b7664e08c8b53d304585e65637da24',
35
36
  },
36
37
  }
37
38
 
@@ -65,10 +66,12 @@ export function swiftCompilerRelease(release: string): string {
65
66
 
66
67
  /**
67
68
  * the bin directory of the toolchain for `release`, unverified. macOS: the
68
- * Xcode Toolchains directory, the user's before the system's. Linux: a
69
- * builder's `$CONTRAST_SWIFT_TOOLCHAINS/<release>`
69
+ * Xcode Toolchains directory, the user's before the system's. Linux:
70
+ * `$CONTRAST_SWIFT_TOOLCHAINS/<release>` when set
70
71
  * (packages/sootsim-swift/scripts/install-linux-toolchain.sh fills it), else
71
- * the cf-build image's `/opt/swift/<release>`, else the `swift` first on PATH.
72
+ * the cf-build image's `/opt/swift/<release>` when present, else a pinned
73
+ * compiler's user-writable `$PEACH_HOME/swift/toolchains/<release>`, else the
74
+ * `swift` first on PATH.
72
75
  */
73
76
  export function swiftToolchainBin(release: string): string {
74
77
  if (process.platform === 'darwin') {
@@ -81,8 +84,9 @@ export function swiftToolchainBin(release: string): string {
81
84
  return path.join(process.env.CONTRAST_SWIFT_TOOLCHAINS, release, 'usr/bin')
82
85
  }
83
86
  const image = path.join('/opt/swift', release, 'usr/bin')
84
- if (release.includes('-custom-') || fs.existsSync(path.join(image, 'swift')))
85
- return image
87
+ if (fs.existsSync(path.join(image, 'swift'))) return image
88
+ if (release.includes('-custom-'))
89
+ return path.join(peachHome(), 'swift', 'toolchains', release, 'usr/bin')
86
90
  return (
87
91
  (process.env.PATH ?? '')
88
92
  .split(path.delimiter)
@@ -90,6 +94,106 @@ export function swiftToolchainBin(release: string): string {
90
94
  )
91
95
  }
92
96
 
97
+ // home-paths.ts's rnxHomeDir, repeated because this file imports node builtins only.
98
+ function peachHome(): string {
99
+ const override = process.env.PEACH_HOME
100
+ return override ? path.resolve(override) : path.join(os.homedir(), '.peach')
101
+ }
102
+
103
+ let scratch: string | undefined
104
+ let swiftHome: string | undefined
105
+ /**
106
+ * keep the temporary files swift processes write (swiftpm's lock files and
107
+ * TemporaryDirectory folders, which it never removes) in a per-process
108
+ * directory under `root`, removed when this process exits, and on linux its
109
+ * home directory in `root/home`, kept so the caches persist. directories left
110
+ * by processes that no longer run are removed first. without this call swift
111
+ * inherits the caller's TMPDIR.
112
+ */
113
+ export function confineSwiftScratch(root: string): void {
114
+ if (scratch) return
115
+ fs.mkdirSync(root, { recursive: true })
116
+ for (const name of fs.readdirSync(root)) {
117
+ const pid = Number(name)
118
+ if (Number.isInteger(pid) && pid > 0 && !processRuns(pid))
119
+ fs.rmSync(path.join(root, name), { recursive: true, force: true })
120
+ }
121
+ const dir = path.join(root, String(process.pid))
122
+ fs.mkdirSync(dir, { recursive: true })
123
+ scratch = dir
124
+ // swiftpm and clang write ~/.swiftpm and ~/.cache even with every path flag
125
+ // given; on linux no tool needs the real home, so they get a kept one here.
126
+ if (process.platform === 'linux') {
127
+ swiftHome = path.join(root, 'home')
128
+ fs.mkdirSync(swiftHome, { recursive: true })
129
+ }
130
+ // swiftpm makes ~/.swiftpm in the account's passwd home whatever HOME says;
131
+ // remove at exit only the entries swiftpm seeds there that were absent now:
132
+ // a cache link into a swiftpm cache folder and empty configuration and
133
+ // security folders. anything else, a link or file a user made included, stays.
134
+ const seeded = path.join(os.homedir(), '.swiftpm')
135
+ const dirAbsentBefore = lstatIfKnown(seeded) === null
136
+ const absentBefore = swiftpmSeeds.filter(
137
+ (name) => lstatIfKnown(path.join(seeded, name)) === null,
138
+ )
139
+ process.once('exit', () => {
140
+ fs.rmSync(dir, { recursive: true, force: true })
141
+ for (const name of absentBefore) removeSwiftpmSeed(path.join(seeded, name), name)
142
+ if (dirAbsentBefore && lstatIfKnown(seeded)?.isDirectory())
143
+ try {
144
+ fs.rmdirSync(seeded)
145
+ } catch {}
146
+ })
147
+ }
148
+
149
+ const swiftpmSeeds = ['cache', 'configuration', 'security']
150
+
151
+ // only a missing path establishes absence; other lookup failures stay unknown.
152
+ function lstatIfKnown(entry: string): fs.Stats | null | undefined {
153
+ try {
154
+ return fs.lstatSync(entry)
155
+ } catch (error) {
156
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return null
157
+ return undefined
158
+ }
159
+ }
160
+
161
+ function removeSwiftpmSeed(entry: string, name: string): void {
162
+ const stat = lstatIfKnown(entry)
163
+ if (!stat) return
164
+ try {
165
+ if (name === 'cache') {
166
+ if (
167
+ stat.isSymbolicLink() &&
168
+ fs.readlinkSync(entry).endsWith('/.cache/org.swift.swiftpm')
169
+ )
170
+ fs.unlinkSync(entry)
171
+ } else if (stat.isDirectory()) {
172
+ fs.rmdirSync(entry)
173
+ }
174
+ } catch {}
175
+ }
176
+
177
+ function processRuns(pid: number): boolean {
178
+ try {
179
+ process.kill(pid, 0)
180
+ return true
181
+ } catch (error) {
182
+ return error instanceof Error && 'code' in error && error.code === 'EPERM'
183
+ }
184
+ }
185
+
186
+ /** the environment every swift process runs with: `base`, with TMPDIR in the
187
+ * confined scratch directory when the caller set one. */
188
+ export function swiftEnvironment(
189
+ base: NodeJS.ProcessEnv = process.env,
190
+ ): NodeJS.ProcessEnv {
191
+ if (!scratch) return base
192
+ return swiftHome
193
+ ? { ...base, TMPDIR: scratch, HOME: swiftHome }
194
+ : { ...base, TMPDIR: scratch }
195
+ }
196
+
93
197
  /** the wasm SDK a release compiles with: its installed id, where it
94
198
  * downloads from and its archive checksum (packages/sootsim-swift/toolchain.json). */
95
199
  export interface SwiftWasmSdk {
@@ -98,17 +202,13 @@ export interface SwiftWasmSdk {
98
202
  wasmSdkChecksum: string
99
203
  }
100
204
 
101
- const verified = new Map<string, string>()
102
-
103
205
  /**
104
206
  * swiftToolchainBin, checked: its swift is exactly `release` and has the
105
207
  * pinned wasm SDK installed. anything else fails with one message naming the
106
208
  * release and the commands that install it.
107
209
  */
108
210
  export function swiftToolchain(release: string, sdk: SwiftWasmSdk): string {
109
- const key = JSON.stringify([release, sdk.wasmSdkId, swiftSdkOptions()])
110
- const cached = verified.get(key)
111
- if (cached) return cached
211
+ validateSwiftSdkIdentity(sdk)
112
212
  let bin: string
113
213
  try {
114
214
  bin = swiftCompiler(release)
@@ -121,17 +221,218 @@ export function swiftToolchain(release: string, sdk: SwiftWasmSdk): string {
121
221
  const swift = path.join(bin, 'swift')
122
222
  const sdks = execFileSync(swift, ['sdk', 'list', ...swiftSdkOptions()], {
123
223
  encoding: 'utf8',
224
+ env: swiftEnvironment(),
124
225
  stdio: ['ignore', 'pipe', 'pipe'],
125
226
  })
126
227
  if (!sdks.split('\n').some((line) => line.trim() === sdk.wasmSdkId)) {
127
228
  throw new Error(
128
- `peach compiles Swift with the ${sdk.wasmSdkId} SDK, which ${swift} does not have.\n${swiftInstallSteps(release, sdk)}`,
229
+ `peach compiles Swift with the ${sdk.wasmSdkId} SDK, which ${swift} does not have.\n${swiftSdkInstallStep(release, sdk)}`,
129
230
  )
130
231
  }
131
- verified.set(key, bin)
232
+ try {
233
+ verifySwiftSdk(swift, sdk)
234
+ } catch (error) {
235
+ const reason = error instanceof Error ? error.message : String(error)
236
+ throw new Error(`${reason}\n${swiftSdkInstallStep(release, sdk)}`)
237
+ }
132
238
  return bin
133
239
  }
134
240
 
241
+ // swiftpm's info.json describes SDK selection, not the archive it came from.
242
+ function validateSwiftSdkIdentity(sdk: SwiftWasmSdk): void {
243
+ if (
244
+ !/^[a-zA-Z0-9._-]+$/.test(sdk.wasmSdkId) ||
245
+ !/^[a-f0-9]{64}$/.test(sdk.wasmSdkChecksum)
246
+ )
247
+ throw new Error('invalid Swift SDK identity or checksum')
248
+ }
249
+
250
+ function swiftSdkBundle(swift: string, id: string, options: string[]): string {
251
+ const configuration = execFileSync(
252
+ swift,
253
+ ['sdk', 'configure', ...options, '--show-configuration', id, 'wasm32-unknown-wasip1'],
254
+ { encoding: 'utf8', env: swiftEnvironment(), stdio: ['ignore', 'pipe', 'pipe'] },
255
+ )
256
+ const sysroot = /^sdkRootPath: (.+)$/m.exec(configuration)?.[1]?.trim()
257
+ if (!sysroot || !path.isAbsolute(sysroot))
258
+ throw new Error(`Swift SDK ${id} reports no absolute sdkRootPath`)
259
+ let bundle = sysroot
260
+ while (!bundle.endsWith('.artifactbundle')) {
261
+ const parent = path.dirname(bundle)
262
+ if (parent === bundle)
263
+ throw new Error(`Swift SDK ${id} is outside an artifact bundle`)
264
+ bundle = parent
265
+ }
266
+ return fs.realpathSync(bundle)
267
+ }
268
+
269
+ // stat changes invalidate file hashes; every lookup enumerates the entire bundle,
270
+ // including additions, removals, executable modes and internal symlink targets.
271
+ const sdkFileHashes = new Map<string, { stamp: string; checksum: string }>()
272
+ function swiftSdkPayloadChecksum(bundle: string): string {
273
+ const hash = createHash('sha256')
274
+ function visit(relative: string): void {
275
+ const file = path.join(bundle, relative)
276
+ const stat = fs.lstatSync(file, { bigint: true })
277
+ if (stat.isSymbolicLink()) {
278
+ const resolved = path.relative(bundle, fs.realpathSync(file))
279
+ if (
280
+ resolved === '..' ||
281
+ resolved.startsWith(`..${path.sep}`) ||
282
+ path.isAbsolute(resolved)
283
+ )
284
+ throw new Error(`Swift SDK symlink escapes its bundle: ${relative}`)
285
+ hash.update(JSON.stringify([relative, 'link', fs.readlinkSync(file)]) + '\n')
286
+ } else if (stat.isDirectory()) {
287
+ hash.update(JSON.stringify([relative, 'directory']) + '\n')
288
+ for (const name of fs.readdirSync(file).sort()) visit(path.join(relative, name))
289
+ } else if (stat.isFile()) {
290
+ const stamp = [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(':')
291
+ let cached = sdkFileHashes.get(file)
292
+ if (cached?.stamp !== stamp) {
293
+ cached = { stamp, checksum: swiftFileChecksum(file) }
294
+ sdkFileHashes.set(file, cached)
295
+ }
296
+ hash.update(
297
+ JSON.stringify([relative, 'file', Number(stat.mode & 0o111n), cached.checksum]) +
298
+ '\n',
299
+ )
300
+ } else throw new Error(`unsupported Swift SDK payload entry: ${relative}`)
301
+ }
302
+ visit('')
303
+ return hash.digest('hex')
304
+ }
305
+
306
+ function swiftSdkVerificationFile(bundle: string, sdk: SwiftWasmSdk): string {
307
+ return path.join(
308
+ path.dirname(bundle),
309
+ '.peach-sdk-checksums',
310
+ `${sdk.wasmSdkId}-${sdk.wasmSdkChecksum}.json`,
311
+ )
312
+ }
313
+
314
+ function verifySwiftSdk(swift: string, sdk: SwiftWasmSdk): void {
315
+ const bundle = swiftSdkBundle(swift, sdk.wasmSdkId, swiftSdkOptions())
316
+ const file = swiftSdkVerificationFile(bundle, sdk)
317
+ if (!fs.existsSync(file))
318
+ throw new Error(
319
+ `Swift SDK ${sdk.wasmSdkId} has no verified archive binding for ${sdk.wasmSdkChecksum}. Hydrate it with installSwiftWasmSdk before compiling.`,
320
+ )
321
+ const receipt: unknown = JSON.parse(fs.readFileSync(file, 'utf8'))
322
+ if (
323
+ !receipt ||
324
+ typeof receipt !== 'object' ||
325
+ !('wasmSdkId' in receipt) ||
326
+ receipt.wasmSdkId !== sdk.wasmSdkId ||
327
+ !('wasmSdkChecksum' in receipt) ||
328
+ receipt.wasmSdkChecksum !== sdk.wasmSdkChecksum ||
329
+ !('payloadChecksum' in receipt) ||
330
+ receipt.payloadChecksum !== swiftSdkPayloadChecksum(bundle)
331
+ )
332
+ throw new Error(
333
+ `Swift SDK ${sdk.wasmSdkId} installed payload differs from its verified archive ${sdk.wasmSdkChecksum}`,
334
+ )
335
+ }
336
+
337
+ /** install or qualify existing bytes, serializing shared-store mutation without replacement. */
338
+ function installSwiftSdkArchive(swift: string, sdk: SwiftWasmSdk, archive: string): void {
339
+ validateSwiftSdkIdentity(sdk)
340
+ const staging = fs.realpathSync(
341
+ fs.mkdtempSync(path.join(scratch ?? os.tmpdir(), 'peach-sdk-verification-')),
342
+ )
343
+ try {
344
+ if (swiftFileChecksum(archive) !== sdk.wasmSdkChecksum)
345
+ throw new Error('declared Swift SDK archive checksum mismatch')
346
+ const store = path.join(staging, 'sdks')
347
+ fs.mkdirSync(store)
348
+ execFileSync(
349
+ swift,
350
+ [
351
+ 'sdk',
352
+ 'install',
353
+ archive,
354
+ '--checksum',
355
+ sdk.wasmSdkChecksum,
356
+ '--swift-sdks-path',
357
+ store,
358
+ ],
359
+ { env: swiftEnvironment(), stdio: 'inherit' },
360
+ )
361
+ const stagedBundle = swiftSdkBundle(swift, sdk.wasmSdkId, [
362
+ '--swift-sdks-path',
363
+ store,
364
+ ])
365
+ const expected = swiftSdkPayloadChecksum(stagedBundle)
366
+ const directory = process.env.PEACH_SWIFT_SDKS_PATH
367
+ const locks = directory
368
+ ? path.join(path.resolve(directory), '.peach-locks')
369
+ : path.join(os.homedir(), '.swiftpm', 'builder-locks')
370
+ fs.mkdirSync(locks, { recursive: true })
371
+ execFileSync(
372
+ 'perl',
373
+ [
374
+ '-e',
375
+ 'use Fcntl qw(:flock); my ($lock, $id, $bundle, $swift, @options) = @ARGV; open(my $fd, ">>", $lock) or die $!; flock($fd, LOCK_EX) or die $!; open(my $list, "-|", $swift, "sdk", "list", @options) or die $!; my $found = 0; while (<$list>) { s/^\\s+|\\s+$//g; $found = 1 if $_ eq $id; } close($list) or die "swift sdk list failed"; unless ($found) { system($swift, "sdk", "install", $bundle, @options); exit(($? & 127) ? 128 + ($? & 127) : $? >> 8); }',
376
+ path.join(locks, 'wasm-sdk.lock'),
377
+ sdk.wasmSdkId,
378
+ stagedBundle,
379
+ swift,
380
+ ...swiftSdkOptions(),
381
+ ],
382
+ { env: swiftEnvironment(), stdio: 'inherit' },
383
+ )
384
+ const bundle = swiftSdkBundle(swift, sdk.wasmSdkId, swiftSdkOptions())
385
+ if (swiftSdkPayloadChecksum(bundle) !== expected)
386
+ throw new Error(
387
+ `Swift SDK ${sdk.wasmSdkId} already exists with different payload bytes; select a separate --swift-sdks-path. No installed SDK was replaced.`,
388
+ )
389
+ const file = swiftSdkVerificationFile(bundle, sdk)
390
+ fs.mkdirSync(path.dirname(file), { recursive: true })
391
+ const temporary = `${file}.${process.pid}.tmp`
392
+ try {
393
+ fs.writeFileSync(
394
+ temporary,
395
+ JSON.stringify({
396
+ wasmSdkId: sdk.wasmSdkId,
397
+ wasmSdkChecksum: sdk.wasmSdkChecksum,
398
+ payloadChecksum: expected,
399
+ }) + '\n',
400
+ )
401
+ fs.renameSync(temporary, file)
402
+ } finally {
403
+ fs.rmSync(temporary, { force: true })
404
+ }
405
+ } finally {
406
+ fs.rmSync(staging, { recursive: true, force: true })
407
+ for (const file of sdkFileHashes.keys())
408
+ if (file.startsWith(staging + path.sep)) sdkFileHashes.delete(file)
409
+ }
410
+ }
411
+
412
+ /** verify the compiler and install a declared SDK under the shared store lock. */
413
+ export function installSwiftWasmSdk(release: string, sdk: SwiftWasmSdk): void {
414
+ validateSwiftSdkIdentity(sdk)
415
+ try {
416
+ swiftToolchain(release, sdk)
417
+ return
418
+ } catch {
419
+ // an unverified existing SDK must be compared with the declared archive.
420
+ }
421
+ const swift = path.join(swiftCompiler(release), 'swift')
422
+ const staging = fs.mkdtempSync(path.join(scratch ?? os.tmpdir(), 'builder-swift-sdk-'))
423
+ const archive = path.join(staging, 'sdk.artifactbundle.tar.gz')
424
+ try {
425
+ if (sdk.wasmSdkUrl.startsWith('file:'))
426
+ fs.copyFileSync(fileURLToPath(sdk.wasmSdkUrl), archive)
427
+ else
428
+ execFileSync('curl', ['-fsSL', sdk.wasmSdkUrl, '-o', archive], { stdio: 'inherit' })
429
+ installSwiftSdkArchive(swift, sdk, archive)
430
+ swiftToolchain(release, sdk)
431
+ } finally {
432
+ fs.rmSync(staging, { recursive: true, force: true })
433
+ }
434
+ }
435
+
135
436
  // stream large compiler binaries without retaining a second copy in memory.
136
437
  export function swiftFileChecksum(file: string): string {
137
438
  const hash = createHash('sha256')
@@ -170,7 +471,7 @@ export function verifySwiftHostCompiler(release: string, root: string): void {
170
471
  throw new Error(`Swift compiler identity mismatch at ${root}`)
171
472
  }
172
473
  const files = Object.entries(manifest.files)
173
- if (files.length !== 15) throw new Error('incomplete Swift compiler manifest')
474
+ if (files.length !== 16) throw new Error('incomplete Swift compiler manifest')
174
475
  for (const [relative, checksum] of files) {
175
476
  if (
176
477
  relative.startsWith('/') ||
@@ -193,6 +494,7 @@ export function swiftCompiler(release: string): string {
193
494
  verifySwiftHostCompiler(release, path.resolve(bin, '../..'))
194
495
  const version = execFileSync(path.join(bin, 'swift'), ['--version'], {
195
496
  encoding: 'utf8',
497
+ env: swiftEnvironment(),
196
498
  stdio: ['ignore', 'pipe', 'pipe'],
197
499
  })
198
500
  if (!version.includes(`(${swiftCompilerRelease(release)})`))
@@ -201,41 +503,71 @@ export function swiftCompiler(release: string): string {
201
503
  return bin
202
504
  }
203
505
 
506
+ /** the command that installs and qualifies the pinned SDK for an installed compiler. */
507
+ export function swiftSdkInstallStep(release: string, wasmSdk: SwiftWasmSdk): string {
508
+ return `install the pinned SDK:\n ${swiftSdkInstallCommand(release, wasmSdk)}`
509
+ }
510
+
511
+ function swiftSdkInstallCommand(release: string, wasmSdk: SwiftWasmSdk): string {
512
+ const { wasmSdkId, wasmSdkUrl, wasmSdkChecksum } = wasmSdk
513
+ const program = `import { installSwiftWasmSdk } from "peachsim/swift-toolchain"; installSwiftWasmSdk(${JSON.stringify(release)}, ${JSON.stringify({ wasmSdkId, wasmSdkUrl, wasmSdkChecksum })})`
514
+ const directory = process.env.PEACH_SWIFT_SDKS_PATH
515
+ const environment = directory
516
+ ? `PEACH_SWIFT_SDKS_PATH='${path.resolve(directory).replaceAll("'", "'\\''")}' `
517
+ : ''
518
+ return `${environment}node --input-type=module -e '${program.replaceAll("'", "'\\''")}'`
519
+ }
520
+
204
521
  /** the commands that install the pinned compiler and SDK on this host. */
205
522
  export function swiftInstallSteps(release: string, wasmSdk: SwiftWasmSdk): string {
206
- const sdkOptions = swiftSdkOptions()
207
- .map((arg) => ` '${arg.replaceAll("'", "'\\''")}'`)
208
- .join('')
523
+ const { note, commands } = swiftCompilerInstall(release)
524
+ return [
525
+ `install the pinned compiler and SDK${note}:`,
526
+ ...[...commands, swiftSdkInstallCommand(release, wasmSdk)].map((line) => ` ${line}`),
527
+ ].join('\n')
528
+ }
529
+
530
+ /** the commands that install the pinned compiler alone, for a caller that installs the SDK. */
531
+ export function swiftCompilerInstallSteps(release: string): string {
532
+ const { note, commands } = swiftCompilerInstall(release)
533
+ return [
534
+ `install the pinned compiler${note}:`,
535
+ ...commands.map((line) => ` ${line}`),
536
+ ].join('\n')
537
+ }
538
+
539
+ function swiftCompilerInstall(release: string): { note: string; commands: string[] } {
209
540
  if (release.includes('-custom-')) {
210
541
  const archive = swiftHostCompilerArchive(release)
211
542
  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')
543
+ return {
544
+ note: '',
545
+ commands: [
546
+ `curl -fsSL ${archive.url} -o compiler.tar.gz`,
547
+ `echo '${archive.checksum} compiler.tar.gz' | shasum -a 256 -c -`,
548
+ `mkdir -p '${directory}'`,
549
+ `tar -xzf compiler.tar.gz --strip-components=1 -C '${directory}'`,
550
+ ],
551
+ }
220
552
  }
221
553
  const version = release.replace(/^swift-/, '').toLowerCase()
222
554
  const downloads = `https://download.swift.org/swift-${version}`
223
- const sdk = `swift sdk install ${wasmSdk.wasmSdkUrl} --checksum ${wasmSdk.wasmSdkChecksum}${sdkOptions}`
224
555
  if (process.platform === 'darwin') {
225
- const swift = `~/Library/Developer/Toolchains/${release}.xctoolchain/usr/bin/swift`
226
- return [
227
- 'install it:',
228
- ` curl -fsSLO ${downloads}/xcode/${release}/${release}-osx.pkg`,
229
- ` installer -pkg ${release}-osx.pkg -target CurrentUserHomeDirectory`,
230
- ` ${swift} ${sdk.replace(/^swift /, '')}`,
231
- ].join('\n')
556
+ return {
557
+ note: '',
558
+ commands: [
559
+ `curl -fsSLO ${downloads}/xcode/${release}/${release}-osx.pkg`,
560
+ `installer -pkg ${release}-osx.pkg -target CurrentUserHomeDirectory`,
561
+ ],
562
+ }
232
563
  }
233
564
  const platform = `debian12${process.arch === 'arm64' ? '-aarch64' : ''}`
234
565
  const name = `${release}-${platform}`
235
- return [
236
- 'install it (Debian 12 shown; other distributions: https://www.swift.org/install/linux/):',
237
- ` curl -fsSL ${downloads}/${platform}/${release}/${name}.tar.gz | tar -xz -C /opt`,
238
- ` export PATH=/opt/${name}/usr/bin:$PATH`,
239
- ` ${sdk}`,
240
- ].join('\n')
566
+ return {
567
+ note: ' (Debian 12 shown; other distributions: https://www.swift.org/install/linux/)',
568
+ commands: [
569
+ `curl -fsSL ${downloads}/${platform}/${release}/${name}.tar.gz | tar -xz -C /opt`,
570
+ `export PATH=/opt/${name}/usr/bin:$PATH`,
571
+ ],
572
+ }
241
573
  }
@@ -17,6 +17,18 @@ export interface SwiftResourceBundle {
17
17
  path: string
18
18
  }
19
19
 
20
+ export interface SwiftObjCPeerClass {
21
+ name: string
22
+ header: string
23
+ }
24
+
25
+ export interface SwiftObjCImplementationPreparation {
26
+ receipt: string
27
+ objects: { target: string; source: string; file: string }[]
28
+ metadata?: string
29
+ registration?: string
30
+ }
31
+
20
32
  export interface SwiftMetalLibrarySource extends SwiftMetalSource {
21
33
  bundlePath: string
22
34
  }
@@ -242,10 +242,16 @@ const linkedChunks = new Set();
242
242
  function chunkBody(url) {
243
243
  let body = chunkBodies.get(url);
244
244
  if (!body) {
245
- body = fetchWorkerChunk(url, { credentials: 'same-origin' }).then(response => {
245
+ body = (async () => {
246
+ if (deliveredChunkList && (await deliveredChunkList.promise).has(url)) {
247
+ const bytes = await deliveredChunk(url).promise;
248
+ deliveredChunks.delete(url);
249
+ return new TextDecoder().decode(bytes);
250
+ }
251
+ const response = await fetchWorkerChunk(url, { credentials: 'same-origin' });
246
252
  if (!response.ok) throw new Error(url + ': HTTP ' + response.status);
247
253
  return response.text();
248
- });
254
+ })();
249
255
  body.catch(() => chunkBodies.delete(url));
250
256
  chunkBodies.set(url, body);
251
257
  }
@@ -261,16 +267,9 @@ const importModule = specifier => import(specifier);
261
267
  System.importModule = importModule;
262
268
  System.instantiate = async function(url) {
263
269
  if (url === workerEntryUrl) return workerEntryRegistration;
264
- let source;
265
- if (deliveredChunkList && (await deliveredChunkList.promise).has(url)) {
266
- const body = await deliveredChunk(url).promise;
267
- deliveredChunks.delete(url);
268
- source = new TextDecoder().decode(body);
269
- } else {
270
- source = await chunkBody(url);
271
- chunkBodies.delete(url);
272
- linkedChunks.add(url);
273
- }
270
+ const source = await chunkBody(url);
271
+ chunkBodies.delete(url);
272
+ linkedChunks.add(url);
274
273
  let registration;
275
274
  // minified module chunks assume strict block-scoped function semantics.
276
275
  new Function('System', '"use strict";\\n' + source)({