peachsim 0.1.578 → 0.1.580

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 (178) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +11 -9
  3. package/cli/app-catalog.ts +77 -0
  4. package/cli/app-fonts.ts +4 -20
  5. package/cli/app-splash.ts +2 -18
  6. package/cli/bridge-flow-runner.ts +505 -139
  7. package/cli/cloud-client.ts +1 -4
  8. package/cli/cloud-session.ts +1 -4
  9. package/cli/cloud-test-result-files.ts +3 -0
  10. package/cli/cloud-test-tunnel.ts +2 -5
  11. package/cli/command-registry.ts +2 -0
  12. package/cli/commands/agent.ts +149 -23
  13. package/cli/commands/assert.ts +1 -4
  14. package/cli/commands/box/peach-command.ts +62 -0
  15. package/cli/commands/box/plane-share.ts +0 -70
  16. package/cli/commands/box/rnx-command.ts +1 -60
  17. package/cli/commands/box/search-command.ts +14 -0
  18. package/cli/commands/box.ts +2 -2
  19. package/cli/commands/control.ts +121 -27
  20. package/cli/commands/cpu-profile.ts +23 -10
  21. package/cli/commands/daemon.ts +1 -9
  22. package/cli/commands/detox.ts +26 -18
  23. package/cli/commands/device.ts +129 -1
  24. package/cli/commands/film.ts +25 -16
  25. package/cli/commands/flow.ts +9 -2
  26. package/cli/commands/inspect/actions.ts +75 -1
  27. package/cli/commands/inspect/core.ts +81 -2
  28. package/cli/commands/inspect/describe.ts +2 -6
  29. package/cli/commands/inspect/env.ts +1 -1
  30. package/cli/commands/inspect/settling.ts +20 -3
  31. package/cli/commands/inspect/shared.ts +1 -4
  32. package/cli/commands/inspect/sleep.ts +1 -1
  33. package/cli/commands/inspect.ts +16 -49
  34. package/cli/commands/install-desktop.ts +8 -6
  35. package/cli/commands/login.ts +3 -5
  36. package/cli/commands/maestro-generate.ts +1 -4
  37. package/cli/commands/maestro.ts +38 -21
  38. package/cli/commands/no-bridge-hint.ts +0 -14
  39. package/cli/commands/permissions.ts +2 -2
  40. package/cli/commands/platform.ts +13 -6
  41. package/cli/commands/screenshot.ts +6 -1
  42. package/cli/commands/skills.ts +1 -1
  43. package/cli/commands/test.ts +6 -2
  44. package/cli/commands/three-mode.ts +162 -19
  45. package/cli/commands/upload.ts +3 -18
  46. package/cli/current-sim.ts +0 -1
  47. package/cli/desktop-companion.ts +40 -8
  48. package/cli/drivers/playwright-provisioning.ts +1 -4
  49. package/cli/flow-export.ts +1 -8
  50. package/cli/flow-session.ts +0 -4
  51. package/cli/hints.ts +1 -16
  52. package/cli/inspect-notice-state.ts +0 -4
  53. package/cli/main.ts +2 -4
  54. package/cli/outbound-endpoints.ts +1 -24
  55. package/cli/parse-args.ts +1 -2
  56. package/cli/rnx-in-process.ts +3 -1
  57. package/cli/run-rnx-core.ts +4 -2
  58. package/cli/run-rnx.ts +18 -10
  59. package/cli/send-to-box.ts +1 -4
  60. package/cli/telemetry.ts +1 -1
  61. package/cli/ws-bridge.ts +1 -5
  62. package/detox/gestures.ts +20 -6
  63. package/detox/index.ts +379 -123
  64. package/detox/navigation-lifecycle.ts +22 -13
  65. package/dist-lib/agent-daemon-client.cjs +21 -15
  66. package/dist-lib/agent-events.cjs +1 -1
  67. package/dist-lib/agent-identity.cjs +3 -17
  68. package/dist-lib/agent-sessions.cjs +5 -4
  69. package/dist-lib/attached-projects.cjs +1 -77
  70. package/dist-lib/auth/shared-session.cjs +1 -1
  71. package/dist-lib/backend-origin.cjs +11 -1
  72. package/dist-lib/beta.cjs +1 -1
  73. package/dist-lib/beta.mjs +1 -1
  74. package/dist-lib/bridge-constants.cjs +1 -1
  75. package/dist-lib/bridge-contract-input.cjs +7 -5
  76. package/dist-lib/bridge-contract-input.mjs +7 -5
  77. package/dist-lib/bridge-contract.cjs +1 -1
  78. package/dist-lib/bridge-contract.mjs +1 -1
  79. package/dist-lib/capture-contract.cjs +30 -7
  80. package/dist-lib/capture-contract.mjs +30 -7
  81. package/dist-lib/cli-constants.cjs +1 -1
  82. package/dist-lib/cloud-contract.cjs +1 -1
  83. package/dist-lib/cloud-contract.mjs +1 -1
  84. package/dist-lib/cloud-test-args.cjs +1 -1
  85. package/dist-lib/cloud-test-args.mjs +1 -1
  86. package/dist-lib/cloud.cjs +17 -11
  87. package/dist-lib/cloud.mjs +17 -11
  88. package/dist-lib/config.cjs +1 -1
  89. package/dist-lib/detox/index.cjs +321 -122
  90. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  91. package/dist-lib/home-paths.cjs +1 -1
  92. package/dist-lib/host/bridge-host.cjs +427 -212
  93. package/dist-lib/host/fetch-proxy-handler.cjs +5 -28
  94. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  95. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  96. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  97. package/dist-lib/host/websocket-proxy.cjs +1 -1
  98. package/dist-lib/index.cjs +315 -58
  99. package/dist-lib/jump-to-source-babel.cjs +1 -1
  100. package/dist-lib/jump-to-source-native.cjs +1 -1
  101. package/dist-lib/menu.cjs +67 -31
  102. package/dist-lib/menu.mjs +64 -28
  103. package/dist-lib/metro-fingerprint-registry.cjs +8 -4
  104. package/dist-lib/metro-fingerprint-registry.mjs +6 -4
  105. package/dist-lib/metro-production-bundle.cjs +2 -2
  106. package/dist-lib/metro-production-bundle.mjs +2 -2
  107. package/dist-lib/metro.cjs +8 -4
  108. package/dist-lib/profiles.cjs +1 -1
  109. package/dist-lib/public-brand.cjs +7 -4
  110. package/dist-lib/react-native-host-modules.cjs +1 -1
  111. package/dist-lib/react-native-host-modules.mjs +1 -1
  112. package/dist-lib/render-mode.cjs +3 -13
  113. package/dist-lib/scripts/dev-server-scanner.cjs +5 -17
  114. package/dist-lib/sdk.cjs +129 -10
  115. package/dist-lib/sdk.mjs +127 -10
  116. package/dist-lib/skills.cjs +1635 -1121
  117. package/dist-lib/swift-compile.cjs +277 -70
  118. package/dist-lib/swift-compile.mjs +275 -70
  119. package/dist-lib/swift-package.cjs +17 -5
  120. package/dist-lib/swift-package.mjs +15 -4
  121. package/dist-lib/swift-submit.cjs +32 -8
  122. package/dist-lib/swift-submit.mjs +29 -7
  123. package/dist-lib/swift.cjs +30912 -577
  124. package/dist-lib/vite-kotlin.cjs +356 -0
  125. package/dist-lib/vite-swift.cjs +1743 -0
  126. package/dist-lib/vite.cjs +208 -1039
  127. package/kotlin/README.md +18 -0
  128. package/kotlin/Sources/androidx/annotation/Keep.kt +4 -0
  129. package/kotlin/Sources/dev/onejs/one/OneNativeSourceDispatch.kt +7 -0
  130. package/kotlin/Sources/dev/onejs/one/source/OneModule.kt +4 -0
  131. package/kotlin/Sources/org/json/JSON.kt +244 -0
  132. package/kotlin/toolchain.json +19 -0
  133. package/package.json +14 -2
  134. package/scripts/dev-server-scanner.ts +0 -18
  135. package/scripts/optional-demo-registry.ts +0 -17
  136. package/skills/contrast/SKILL.md +2 -2
  137. package/skills/peach-setup/SKILL.md +16 -0
  138. package/src/agent-daemon-client.ts +36 -22
  139. package/src/agent-identity.ts +0 -15
  140. package/src/agent-prompt.ts +12 -0
  141. package/src/attached-projects.ts +0 -72
  142. package/src/backend-origin.ts +10 -0
  143. package/src/bridge-contract-input.ts +2 -5
  144. package/src/bridge-contract.ts +2 -0
  145. package/src/capture-contract.ts +53 -11
  146. package/src/cloud.ts +4 -9
  147. package/src/dev-server-open.ts +0 -99
  148. package/src/home-paths.ts +2 -3
  149. package/src/host/agent-host.ts +82 -36
  150. package/src/host/bridge-host.ts +5 -11
  151. package/src/host/fetch-proxy-handler.ts +19 -43
  152. package/src/is-record.ts +4 -0
  153. package/src/menu.ts +82 -37
  154. package/src/metro-fingerprint-registry.ts +1 -4
  155. package/src/metro-production-bundle.ts +1 -1
  156. package/src/plist.ts +1 -4
  157. package/src/public-brand.ts +5 -3
  158. package/src/render-mode.ts +0 -12
  159. package/src/runtime-assets.ts +9 -2
  160. package/src/screenshots/compose.ts +7 -1
  161. package/src/screenshots/frame-compose.ts +21 -10
  162. package/src/screenshots/schema.ts +1 -4
  163. package/src/sim-client.ts +2 -4
  164. package/src/skills/builtin/maestro-test.ts +1 -4
  165. package/src/sleep.ts +4 -0
  166. package/src/swift-compile.ts +209 -47
  167. package/src/swift-icu.ts +140 -0
  168. package/src/swift-package.ts +26 -2
  169. package/src/swift-submit.ts +84 -4
  170. package/src/swift.ts +2 -0
  171. package/src/vite-plugin-kotlin.ts +376 -0
  172. package/src/vite-plugin-one.ts +224 -36
  173. package/src/vite-plugin-swift.ts +382 -96
  174. package/src/vite-plugin.ts +121 -134
  175. package/src/vite.d.ts +10 -0
  176. package/cli/resolve-assets.ts +0 -63
  177. package/src/rolldown-plugin-swift.ts +0 -244
  178. package/src/team-machine-prompt.ts +0 -73
@@ -10,19 +10,41 @@ import {
10
10
  statSync,
11
11
  writeFileSync,
12
12
  } from 'node:fs'
13
+ import os from 'node:os'
13
14
  import path from 'node:path'
14
15
  import { promisify } from 'node:util'
15
- import { swiftManifestTargets, type SwiftPackageTarget } from './swift-package'
16
+ import { swiftIcuAsset, type SwiftIcuAsset } from './swift-icu'
17
+ import {
18
+ swiftManifestTargets,
19
+ swiftPackagePartition,
20
+ type SwiftPackageTarget,
21
+ } from './swift-package'
16
22
  import type { SwiftImageUpload } from './swift-submit'
17
23
 
24
+ export { swiftIcuAsset, type SwiftIcuAsset }
25
+
18
26
  const run = promisify(execFile)
19
27
  const WASM_TRIPLE = 'wasm32-unknown-wasip1'
20
28
 
29
+ // the link runs wasm-opt, which threads across every core (970% peak on 16
30
+ // cores for the counter fixture) and pins a shared machine for each compile.
31
+ // binaryen's own cap holds it to 4 threads (400%, 26 -> 32 s); swiftpm's jobs
32
+ // take half the cores, as the fixture build does.
33
+ const cores = os.availableParallelism()
34
+ const BUILD_ENV = { ...process.env, BINARYEN_CORES: String(Math.min(4, cores)) }
35
+ const BUILD_JOBS = String(Math.max(1, Math.floor(cores / 2)))
36
+
21
37
  export interface SwiftCompileInput {
22
38
  files: Record<string, string>
23
39
  resources?: Record<string, string>
24
40
  images?: Record<string, SwiftImageUpload>
25
41
  entry: string
42
+ /**
43
+ * the sources are a One swift package. One's pod compiles every file with
44
+ * `-import-module One`, so `RNXModule`, `RNXPackage` and `JSON` resolve
45
+ * without an import; the compile links Peach's `One` module the same way.
46
+ */
47
+ oneModule?: boolean
26
48
  }
27
49
 
28
50
  export interface SwiftCompileOptions {
@@ -36,6 +58,8 @@ export interface SwiftCompileOptions {
36
58
  export interface SwiftCompileArtifact {
37
59
  hash: string
38
60
  bytes: Buffer
61
+ /** the icu common data the artifact reserves and its instantiator fills. */
62
+ icu: SwiftIcuAsset
39
63
  }
40
64
 
41
65
  export interface SwiftProjectLayout {
@@ -44,19 +68,62 @@ export interface SwiftProjectLayout {
44
68
  multi: boolean
45
69
  }
46
70
 
71
+ // toolchain.json is an input for its wasm sdk checksum: the sdk's icu blob,
72
+ // which the host fills into every artifact, is part of what the address names.
47
73
  export function swiftModuleInputFiles(root: string): string[] {
48
- const files: string[] = ['Package.swift', 'scripts/contrast-app-Package.swift']
74
+ const files: string[] = [
75
+ 'Package.swift',
76
+ 'scripts/contrast-app-Package.swift',
77
+ 'toolchain.json',
78
+ ]
49
79
  const walk = (dir: string) => {
50
80
  for (const entry of readdirSync(path.join(root, dir), { withFileTypes: true })) {
51
81
  const relative = `${dir}/${entry.name}`
52
82
  if (entry.isDirectory()) walk(relative)
53
- else if (entry.name.endsWith('.swift')) files.push(relative)
83
+ else if (/\.(swift|c|h)$/.test(entry.name)) files.push(relative)
54
84
  }
55
85
  }
56
86
  walk('Sources')
57
87
  return files.sort()
58
88
  }
59
89
 
90
+ // the host halves of the module's macros: the rnx plugin (#Preview, @Entry,
91
+ // @Model) and #Predicate's FoundationMacros stand-in, which one script builds
92
+ // side by side. manifest swiftSettings cannot load a plugin library (RAN: the
93
+ // flag reaches the driver but the plugin never loads), so every build loads
94
+ // both with -Xswiftc, as scripts/build.sh does for the fixtures. their
95
+ // sources are under Sources/, which the artifact hash already covers.
96
+ async function ensureMacroPlugins(
97
+ moduleRoot: string,
98
+ toolchainBin: string,
99
+ ): Promise<string[]> {
100
+ const dir = path.join(moduleRoot, '.build/host/RnxPreviewMacros')
101
+ // linux names a plugin's module from lib<Module>.so
102
+ const ext = process.platform === 'darwin' ? 'dylib' : 'so'
103
+ const plugins = [
104
+ {
105
+ library: path.join(dir, `libRnxPreviewMacros.${ext}`),
106
+ source: path.join(moduleRoot, 'Sources/RnxPreviewMacros/PreviewMacro.swift'),
107
+ },
108
+ {
109
+ library: path.join(dir, `libFoundationMacros.${ext}`),
110
+ source: path.join(moduleRoot, 'Sources/FoundationMacros/PredicateMacro.swift'),
111
+ },
112
+ ]
113
+ const stale = plugins.some(
114
+ ({ library, source }) =>
115
+ !existsSync(library) || statSync(source).mtimeMs > statSync(library).mtimeMs,
116
+ )
117
+ if (stale) {
118
+ await run('sh', [
119
+ path.join(moduleRoot, 'scripts/build-preview-plugin.sh'),
120
+ toolchainBin,
121
+ plugins[0].library,
122
+ ])
123
+ }
124
+ return plugins.map(({ library }) => library)
125
+ }
126
+
60
127
  const linkedModuleDigests = new Map<string, { stamp: string; digest: Buffer }>()
61
128
 
62
129
  export function clearLinkedModuleDigests(root?: string): void {
@@ -93,7 +160,32 @@ function linkedModuleDigest(root: string): Buffer {
93
160
 
94
161
  export function compileTargets(input: SwiftCompileInput): SwiftPackageTarget[] {
95
162
  const manifest = input.files['Package.swift']
96
- return manifest === undefined ? [] : swiftManifestTargets(manifest)
163
+ return manifest === undefined ? [] : swiftPackagePartition(manifest)
164
+ }
165
+
166
+ /**
167
+ * the compile input with a package's generated native dispatch glue in its
168
+ * one target, linked against the One module the glue conforms to. vxrn's
169
+ * `renderSwiftSourceGlue` renders the glue; a package with native modules
170
+ * across several targets has no single place for it.
171
+ */
172
+ export function withNativeSourceGlue(
173
+ input: SwiftCompileInput,
174
+ glue: string,
175
+ ): SwiftCompileInput {
176
+ const targets = compileTargets(input)
177
+ if (targets.length > 1) {
178
+ throw new Error('swift native modules need one SwiftPM target')
179
+ }
180
+ const targetPath = targets[0]?.path === '.' ? '' : (targets[0]?.path ?? '')
181
+ return {
182
+ ...input,
183
+ files: {
184
+ ...input.files,
185
+ [path.posix.join(targetPath, 'OneNativeSource.generated.swift')]: glue,
186
+ },
187
+ oneModule: true,
188
+ }
97
189
  }
98
190
 
99
191
  function swiftSiblingDependencies(
@@ -107,7 +199,11 @@ function swiftSiblingDependencies(
107
199
  const deps = new Set<string>()
108
200
  for (const [file, contents] of Object.entries(files)) {
109
201
  if (file === 'Package.swift' || !file.startsWith(prefix)) continue
110
- for (const match of contents.matchAll(/^\s*import\s+([A-Za-z_][A-Za-z0-9_]*)/gm)) {
202
+ // an import may carry attributes (`@_exported`, `@preconcurrency`) and a
203
+ // declaration kind (`import struct Nuke.ImageRequest`).
204
+ for (const match of contents.matchAll(
205
+ /^\s*(?:@\w+(?:\([^)]*\))?\s+)*import\s+(?:(?:typealias|struct|class|enum|protocol|let|var|func)\s+)?([A-Za-z_][A-Za-z0-9_]*)/gm,
206
+ )) {
111
207
  if (names.has(match[1]) && match[1] !== target.name) deps.add(match[1])
112
208
  }
113
209
  }
@@ -116,8 +212,11 @@ function swiftSiblingDependencies(
116
212
  return siblings
117
213
  }
118
214
 
215
+ const ONE_PRODUCT = '.product(name: "One", package: "sootsim-swift")'
216
+ const ONE_IMPLICIT_FLAGS = ', "-Xfrontend", "-import-module", "-Xfrontend", "One"'
217
+
119
218
  /** bump with any change to the emitted multi-target manifest text. */
120
- export const MULTI_MANIFEST_VERSION = 1
219
+ export const MULTI_MANIFEST_VERSION = 5
121
220
 
122
221
  export function swiftArtifactHash(
123
222
  input: SwiftCompileInput,
@@ -126,6 +225,7 @@ export function swiftArtifactHash(
126
225
  ): string {
127
226
  const hash = createHash('sha256')
128
227
  hash.update(`swift-compile/v1\n${release}\n`)
228
+ if (input.oneModule) hash.update('one-module\n')
129
229
  hash.update(linkedModuleDigest(moduleRoot))
130
230
  for (const file of Object.keys(input.files).sort()) {
131
231
  hash.update(createHash('sha256').update(file).digest())
@@ -165,15 +265,21 @@ export function emitSwiftPackageManifest(args: {
165
265
  targets: SwiftPackageTarget[]
166
266
  siblings: Map<string, string[]>
167
267
  moduleRoot: string
268
+ oneModule?: boolean
168
269
  }): string {
169
- const { targets, siblings, moduleRoot } = args
270
+ const { targets, siblings, moduleRoot, oneModule } = args
170
271
  const executable = targets.find((target) => target.executable)
171
272
  if (!executable) throw new Error('swift-compile: no executable target to emit')
172
- const swiftSettings =
173
- 'swiftSettings: [.swiftLanguageMode(.v5), .unsafeFlags(["-Osize"])]'
273
+ // each target compiles in the language mode and default isolation its
274
+ // author declared, the way xcode builds the same package.
174
275
  const stanza = (target: SwiftPackageTarget): string => {
276
+ const isolation = target.mainActorIsolation
277
+ ? ' .defaultIsolation(MainActor.self),'
278
+ : ''
279
+ const swiftSettings = `swiftSettings: [.swiftLanguageMode(.v${target.languageMode}),${isolation} .unsafeFlags(["-Osize"${oneModule ? ONE_IMPLICIT_FLAGS : ''}])]`
175
280
  const deps = [
176
281
  '.product(name: "SwiftUI", package: "sootsim-swift")',
282
+ ...(oneModule ? [ONE_PRODUCT] : []),
177
283
  ...(siblings.get(target.name) ?? []).map((sibling) => `"${sibling}"`),
178
284
  ].join(', ')
179
285
  const kind = target.executable ? '.executableTarget' : '.target'
@@ -196,8 +302,10 @@ export function emitSwiftPackageManifest(args: {
196
302
  ` ),`,
197
303
  ].join('\n')
198
304
  }
305
+ // 6.2 is the first tools version with `.defaultIsolation`, which a target
306
+ // carries over from the project manifest.
199
307
  return [
200
- '// swift-tools-version: 6.0',
308
+ '// swift-tools-version: 6.2',
201
309
  'import PackageDescription',
202
310
  '',
203
311
  '// generated by swift-compile from the project package partition.',
@@ -213,46 +321,74 @@ export function emitSwiftPackageManifest(args: {
213
321
  ].join('\n')
214
322
  }
215
323
 
324
+ // the build tree mirrors the input: a file is written only when its text
325
+ // changed and a file the input dropped is removed, so an unchanged source
326
+ // keeps its mtime and swiftpm rebuilds only what moved. `.build` stays.
327
+ function syncSwiftTree(base: string, files: Map<string, string>): void {
328
+ mkdirSync(base, { recursive: true })
329
+ const walk = (dir: string): void => {
330
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
331
+ const full = path.join(dir, entry.name)
332
+ if (dir === base && entry.name === '.build') continue
333
+ if (entry.isDirectory()) walk(full)
334
+ else if (!files.has(full)) rmSync(full, { force: true })
335
+ }
336
+ }
337
+ walk(base)
338
+ for (const [full, contents] of files) {
339
+ if (existsSync(full) && readFileSync(full, 'utf8') === contents) continue
340
+ mkdirSync(path.dirname(full), { recursive: true })
341
+ writeFileSync(full, contents, 'utf8')
342
+ }
343
+ }
344
+
216
345
  export function writeSwiftProject(
217
346
  layout: SwiftProjectLayout,
218
347
  input: SwiftCompileInput,
219
348
  moduleRoot: string,
220
349
  ): void {
221
350
  const targets = compileTargets(input)
351
+ const files = new Map<string, string>()
222
352
  if (targets.length === 0) {
223
- rmSync(layout.sources, { recursive: true, force: true })
224
353
  for (const [file, contents] of Object.entries(input.files)) {
225
- const target = path.join(layout.sources, file)
226
- mkdirSync(path.dirname(target), { recursive: true })
227
- writeFileSync(target, contents, 'utf8')
354
+ if (file === 'Package.swift') continue
355
+ files.set(path.join(layout.sources, file), contents)
228
356
  }
229
- const manifest = readFileSync(
357
+ let manifest = readFileSync(
230
358
  path.join(moduleRoot, 'scripts/contrast-app-Package.swift'),
231
359
  'utf8',
232
360
  ).replaceAll('__SOOTSIM_SWIFT_PACKAGE__', moduleRoot)
233
- writeFileSync(path.join(layout.root, 'Package.swift'), manifest, 'utf8')
234
- return
235
- }
236
- mkdirSync(layout.root, { recursive: true })
237
- for (const child of readdirSync(layout.root)) {
238
- if (child === '.build') continue
239
- rmSync(path.join(layout.root, child), { recursive: true, force: true })
240
- }
241
- for (const [file, contents] of Object.entries(input.files)) {
242
- if (file === 'Package.swift') continue
243
- const target = path.join(layout.root, file)
244
- mkdirSync(path.dirname(target), { recursive: true })
245
- writeFileSync(target, contents, 'utf8')
361
+ if (input.oneModule) {
362
+ // anchored on lines the template already has, so the template stays a
363
+ // valid manifest for a compiler build that predates the One module
364
+ const product = '.product(name: "CoreLocation", package: "sootsim-swift"),'
365
+ const flags = '"-Osize"'
366
+ if (!manifest.includes(product) || !manifest.includes(flags)) {
367
+ throw new Error(
368
+ 'scripts/contrast-app-Package.swift lost the lines the One module links at',
369
+ )
370
+ }
371
+ manifest = manifest
372
+ .replace(product, `${product}\n ${ONE_PRODUCT},`)
373
+ .replace(flags, `${flags}${ONE_IMPLICIT_FLAGS}`)
374
+ }
375
+ files.set(path.join(layout.root, 'Package.swift'), manifest)
376
+ } else {
377
+ for (const [file, contents] of Object.entries(input.files)) {
378
+ if (file === 'Package.swift') continue
379
+ files.set(path.join(layout.root, file), contents)
380
+ }
381
+ files.set(
382
+ path.join(layout.root, 'Package.swift'),
383
+ emitSwiftPackageManifest({
384
+ targets,
385
+ siblings: swiftSiblingDependencies(input.files, targets),
386
+ moduleRoot,
387
+ oneModule: input.oneModule,
388
+ }),
389
+ )
246
390
  }
247
- writeFileSync(
248
- path.join(layout.root, 'Package.swift'),
249
- emitSwiftPackageManifest({
250
- targets,
251
- siblings: swiftSiblingDependencies(input.files, targets),
252
- moduleRoot,
253
- }),
254
- 'utf8',
255
- )
391
+ syncSwiftTree(layout.root, files)
256
392
  }
257
393
 
258
394
  function streamText(value: unknown): string {
@@ -274,17 +410,27 @@ function outputOf(error: unknown): string {
274
410
 
275
411
  function builtArtifact(layout: SwiftProjectLayout): string {
276
412
  const binDir = path.join(layout.root, '.build', WASM_TRIPLE, 'release')
277
- const artifacts = existsSync(binDir)
278
- ? readdirSync(binDir)
279
- .filter((name) => name.endsWith('.wasm'))
280
- .sort()
281
- : []
282
- if (artifacts.length !== 1) {
413
+ // the build root is shared across projects so the SwiftUI module stays
414
+ // warm, and every project's executable lands beside the others. the
415
+ // written manifest names this build's executable, so select it by name
416
+ // instead of requiring the directory to hold exactly one artifact.
417
+ const manifest = readFileSync(path.join(layout.root, 'Package.swift'), 'utf8')
418
+ const executable = swiftManifestTargets(manifest).find((target) => target.executable)
419
+ if (executable === undefined) {
420
+ throw new Error('swift-compile: the written manifest names no executable target')
421
+ }
422
+ const file = path.join(binDir, `${executable.name}.wasm`)
423
+ if (!existsSync(file)) {
424
+ const found = existsSync(binDir)
425
+ ? readdirSync(binDir)
426
+ .filter((name) => name.endsWith('.wasm'))
427
+ .sort()
428
+ : []
283
429
  throw new Error(
284
- `swift-compile: the build produced ${artifacts.length} wasm artifacts; exactly one executable is the app`,
430
+ `swift-compile: the build produced no ${executable.name}.wasm (found: ${found.join(', ') || 'none'})`,
285
431
  )
286
432
  }
287
- return path.join(binDir, artifacts[0])
433
+ return file
288
434
  }
289
435
 
290
436
  function projectDiagnostic(output: string, layout: SwiftProjectLayout): string {
@@ -335,11 +481,17 @@ export function compileSwiftProjectUnit(
335
481
  `swift-compile: no swift.org toolchain at ${options.toolchainBin}; install ${options.release} and its matching wasm sdk`,
336
482
  )
337
483
  }
484
+ const macroPlugins = await ensureMacroPlugins(moduleRoot, options.toolchainBin)
485
+ // memoized per toolchain, so only a process's first build waits on it,
486
+ // and that one reads and gzips it while swift builds
487
+ const icu = swiftIcuAsset(options.toolchainBin, options.release)
338
488
  try {
339
489
  await run(
340
490
  swift,
341
491
  [
342
492
  'build',
493
+ '-j',
494
+ BUILD_JOBS,
343
495
  '--package-path',
344
496
  root,
345
497
  '--swift-sdk',
@@ -348,12 +500,22 @@ export function compileSwiftProjectUnit(
348
500
  'release',
349
501
  '-Xswiftc',
350
502
  '-Osize',
503
+ ...macroPlugins.flatMap((plugin) => [
504
+ '-Xswiftc',
505
+ '-load-plugin-library',
506
+ '-Xswiftc',
507
+ plugin,
508
+ ]),
351
509
  ],
352
- { maxBuffer: 64 * 1024 * 1024, timeout: options.timeoutMs },
510
+ { env: BUILD_ENV, maxBuffer: 64 * 1024 * 1024, timeout: options.timeoutMs },
353
511
  )
354
512
  } catch (error) {
355
513
  throw new Error(projectDiagnostic(outputOf(error), layout), { cause: error })
356
514
  }
357
- return { hash, bytes: readFileSync(builtArtifact(layout)) }
515
+ return {
516
+ hash,
517
+ bytes: readFileSync(builtArtifact(layout)),
518
+ icu: await icu,
519
+ }
358
520
  })
359
521
  }
@@ -0,0 +1,140 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
3
+ import { readFileSync } from 'node:fs'
4
+ import path from 'node:path'
5
+ import { promisify } from 'node:util'
6
+ import { gzip } from 'node:zlib'
7
+
8
+ /**
9
+ * the icu common data a swift artifact reserves and its host fills
10
+ * (packages/sootsim-swift/Sources/PeachICU/icu.c). it is read straight from
11
+ * the wasm sdk the artifact links against, the archive member the link now
12
+ * leaves out, so it is exactly the bytes the linker used to place: one blob
13
+ * per toolchain release.
14
+ *
15
+ * node builtins only: the fixture harnesses import this file straight from
16
+ * the checkout.
17
+ */
18
+ export interface SwiftIcuAsset {
19
+ /** sha256 of `gzip`, the address a host serves it at. */
20
+ sha256: string
21
+ /**
22
+ * the blob gzipped, which is how every host stores and serves it: 34 MB
23
+ * raw is 13 MB gzipped, and cloudflare compresses no binary type on its
24
+ * own. the tenant inflates it with DecompressionStream.
25
+ */
26
+ gzip: Buffer
27
+ }
28
+
29
+ const run = promisify(execFile)
30
+ const deflate = promisify(gzip)
31
+ const ARCHIVE = 'wasi/lib_FoundationICU.a'
32
+ const MEMBER = 'icu_packaged_data.cpp.obj'
33
+ const raw = new Map<string, Promise<Buffer>>()
34
+ const assets = new Map<string, Promise<SwiftIcuAsset>>()
35
+
36
+ function memo<T>(
37
+ cache: Map<string, Promise<T>>,
38
+ key: string,
39
+ load: () => Promise<T>,
40
+ ): Promise<T> {
41
+ let pending = cache.get(key)
42
+ if (!pending) {
43
+ pending = load()
44
+ pending.catch(() => cache.delete(key))
45
+ cache.set(key, pending)
46
+ }
47
+ return pending
48
+ }
49
+
50
+ /** the raw blob, for a harness that fills an artifact in node. */
51
+ export function swiftIcuData(toolchainBin: string, release: string): Promise<Buffer> {
52
+ return memo(raw, `${toolchainBin}\0${release}`, () => load(toolchainBin, release))
53
+ }
54
+
55
+ /** the served form: gzipped and content-addressed. */
56
+ export function swiftIcuAsset(
57
+ toolchainBin: string,
58
+ release: string,
59
+ ): Promise<SwiftIcuAsset> {
60
+ return memo(assets, `${toolchainBin}\0${release}`, async () => {
61
+ const zipped = await deflate(await swiftIcuData(toolchainBin, release))
62
+ return { sha256: createHash('sha256').update(zipped).digest('hex'), gzip: zipped }
63
+ })
64
+ }
65
+
66
+ async function load(toolchainBin: string, release: string): Promise<Buffer> {
67
+ const { stdout } = await run(path.join(toolchainBin, 'swift'), [
68
+ 'sdk',
69
+ 'configure',
70
+ '--show-configuration',
71
+ `${release}_wasm`,
72
+ 'wasm32-unknown-wasip1',
73
+ ])
74
+ const resources = /^swiftStaticResourcesPath: (.+)$/m.exec(stdout)?.[1]?.trim()
75
+ if (!resources) throw new Error(`swift-icu: ${release}_wasm names no static resources`)
76
+ // a copy, so the 42 MB archive it was sliced from can be collected
77
+ return Buffer.from(
78
+ icuSegment(archiveMember(readFileSync(path.join(resources, ARCHIVE)), MEMBER)),
79
+ )
80
+ }
81
+
82
+ // a gnu ar archive: 60-byte headers, names over 15 characters live in the
83
+ // `//` table and the header says `/<offset>`, members pad to even offsets.
84
+ function archiveMember(archive: Buffer, name: string): Buffer {
85
+ if (archive.toString('latin1', 0, 8) !== '!<arch>\n')
86
+ throw new Error('swift-icu: not an ar archive')
87
+ let names = ''
88
+ for (let at = 8; at + 60 <= archive.length; ) {
89
+ const raw = archive.toString('latin1', at, at + 16).trimEnd()
90
+ const size = Number(archive.toString('latin1', at + 48, at + 58).trim())
91
+ const body = archive.subarray(at + 60, at + 60 + size)
92
+ if (raw === '//') names = body.toString('latin1')
93
+ const member = /^\/\d+$/.test(raw)
94
+ ? names.slice(Number(raw.slice(1)), names.indexOf('/\n', Number(raw.slice(1))))
95
+ : raw.replace(/\/$/, '')
96
+ if (member === name) return body
97
+ at += 60 + size + (size % 2)
98
+ }
99
+ throw new Error(`swift-icu: ${name} is not in the sdk's icu archive`)
100
+ }
101
+
102
+ // the relocatable object's one data segment whose bytes open with icu's
103
+ // data-header magic (0xda27): the packaged data carries no relocations, so
104
+ // the segment is the blob.
105
+ function icuSegment(object: Buffer): Buffer {
106
+ let at = 8
107
+ const leb = (): number => {
108
+ let result = 0
109
+ let shift = 0
110
+ let byte: number
111
+ do {
112
+ byte = object[at++]
113
+ result += (byte & 0x7f) * 2 ** shift
114
+ shift += 7
115
+ } while (byte & 0x80)
116
+ return result
117
+ }
118
+ const found: Buffer[] = []
119
+ while (at < object.length) {
120
+ const id = object[at++]
121
+ const size = leb()
122
+ const end = at + size
123
+ if (id === 11) {
124
+ for (let count = leb(); count > 0; count--) {
125
+ if (leb() !== 0) throw new Error('swift-icu: unexpected data segment kind')
126
+ at++ // i32.const
127
+ leb()
128
+ at++ // end
129
+ const length = leb()
130
+ const bytes = object.subarray(at, at + length)
131
+ if (bytes[2] === 0xda && bytes[3] === 0x27) found.push(bytes)
132
+ at += length
133
+ }
134
+ }
135
+ at = end
136
+ }
137
+ if (found.length !== 1)
138
+ throw new Error(`swift-icu: expected one icu data segment, found ${found.length}`)
139
+ return found[0]
140
+ }
@@ -6,6 +6,10 @@ export interface SwiftPackageTarget {
6
6
  path: string
7
7
  /** true for `.executableTarget`: the one that links the wasm artifact. */
8
8
  executable: boolean
9
+ /** the target's declared `.swiftLanguageMode(.vN)`; 5 when it declares none. */
10
+ languageMode: number
11
+ /** true when the target declares `.defaultIsolation(MainActor.self)`. */
12
+ mainActorIsolation: boolean
9
13
  }
10
14
 
11
15
  /** whether a project-relative key is a SwiftPM manifest, not a source. */
@@ -31,7 +35,8 @@ export function swiftManifestTargets(manifest: string): SwiftPackageTarget[] {
31
35
  while ((match = pattern.exec(code)) !== null) {
32
36
  const executable = match[1] === 'executableTarget'
33
37
  let depth = 1
34
- let i = pattern.lastIndex
38
+ const start = pattern.lastIndex
39
+ let i = start
35
40
  let inString = false
36
41
  let escaped = false
37
42
  let flat = ''
@@ -63,7 +68,26 @@ export function swiftManifestTargets(manifest: string): SwiftPackageTarget[] {
63
68
  rawPath == null || rawPath === ''
64
69
  ? `Sources/${name}`
65
70
  : rawPath.replace(/^\.\//, '').replace(/\/+$/, '')
66
- targets.push({ name, path: targetPath, executable })
71
+ // settings sit inside nested calls, which `flat` drops, so they are read
72
+ // from the call's whole text; a dependency list cannot spell either one.
73
+ const call = code.slice(start, i)
74
+ const languageMode = Number(
75
+ /\.swiftLanguageMode\(\s*\.v(\d+)\s*\)/.exec(call)?.[1] ?? '5',
76
+ )
77
+ const mainActorIsolation = /\.defaultIsolation\(\s*MainActor\.self\s*\)/.test(call)
78
+ targets.push({ name, path: targetPath, executable, languageMode, mainActorIsolation })
67
79
  }
68
80
  return targets
69
81
  }
82
+
83
+ /**
84
+ * the target partition a root manifest declares for the compile. a single
85
+ * executable target at the package root (`path: "."`) holds the whole project,
86
+ * which is the compiler's single-target shape, so it declares no partition.
87
+ */
88
+ export function swiftPackagePartition(manifest: string): SwiftPackageTarget[] {
89
+ const targets = swiftManifestTargets(manifest)
90
+ return targets.length === 1 && targets[0].executable && targets[0].path === '.'
91
+ ? []
92
+ : targets
93
+ }