peachsim 0.1.1625 → 0.1.1640

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +15 -2
  3. package/cli/app-config.ts +18 -6
  4. package/cli/app-fonts.ts +37 -17
  5. package/cli/app-stubs.ts +284 -20
  6. package/cli/bridge-flow-runner.ts +66 -45
  7. package/cli/cloud-client.ts +60 -40
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/control.ts +283 -49
  10. package/cli/commands/doctor.ts +30 -189
  11. package/cli/commands/flow.ts +79 -11
  12. package/cli/commands/inspect/core.ts +10 -5
  13. package/cli/commands/inspect/settling.ts +14 -2
  14. package/cli/commands/inspect/shared.ts +9 -4
  15. package/cli/commands/inspect/wait-ready.ts +12 -0
  16. package/cli/commands/inspect.ts +91 -50
  17. package/cli/commands/install-desktop.ts +1 -0
  18. package/cli/commands/maestro-compare.ts +97 -38
  19. package/cli/commands/maestro.ts +9 -1
  20. package/cli/commands/preview.ts +1 -1
  21. package/cli/commands/record.ts +118 -48
  22. package/cli/commands/screenshot.ts +23 -1
  23. package/cli/commands/serve.ts +7 -2
  24. package/cli/commands/setup.ts +19 -42
  25. package/cli/commands/swift.ts +14 -3
  26. package/cli/commands/upload.ts +93 -48
  27. package/cli/desktop-companion.ts +102 -17
  28. package/cli/drivers/desktop.ts +14 -1
  29. package/cli/drivers/playwright-sim-host.ts +103 -45
  30. package/cli/drivers/playwright.ts +9 -8
  31. package/cli/drivers/types.ts +5 -0
  32. package/cli/maestro-accessibility.ts +1 -1
  33. package/cli/maestro-compare.ts +84 -12
  34. package/cli/main.ts +3 -2
  35. package/cli/outbound-endpoints.ts +20 -6
  36. package/cli/pause-aware-timeout.ts +106 -0
  37. package/cli/scene-video.ts +89 -0
  38. package/cli/ws-bridge.ts +37 -19
  39. package/detox/attempt-events.cjs +66 -5
  40. package/detox/attempt-events.d.cts +7 -0
  41. package/detox/expectations.ts +7 -3
  42. package/detox/index.ts +269 -170
  43. package/detox/motion-change.cjs +206 -21
  44. package/detox/motion-change.d.cts +39 -2
  45. package/detox/navigation-lifecycle.ts +35 -3
  46. package/dist-lib/agent-daemon-client.cjs +1 -1
  47. package/dist-lib/agent-events.cjs +1 -1
  48. package/dist-lib/attached-projects.cjs +1 -1
  49. package/dist-lib/auth/shared-session.cjs +1 -1
  50. package/dist-lib/backend-origin.cjs +8 -7
  51. package/dist-lib/beta.cjs +1 -1
  52. package/dist-lib/beta.mjs +1 -1
  53. package/dist-lib/bridge-constants.cjs +13 -1
  54. package/dist-lib/bridge-contract-input.cjs +1 -1
  55. package/dist-lib/bridge-contract-input.mjs +1 -1
  56. package/dist-lib/bridge-contract.cjs +14 -8
  57. package/dist-lib/bridge-contract.mjs +13 -7
  58. package/dist-lib/bundle-channel.cjs +11887 -7900
  59. package/dist-lib/capture-contract.cjs +1 -1
  60. package/dist-lib/capture-contract.mjs +1 -1
  61. package/dist-lib/cloud-contract.cjs +1 -1
  62. package/dist-lib/cloud-contract.mjs +1 -1
  63. package/dist-lib/cloud-test-args.cjs +1 -1
  64. package/dist-lib/cloud-test-args.mjs +1 -1
  65. package/dist-lib/cloud.cjs +1 -1
  66. package/dist-lib/cloud.mjs +1 -1
  67. package/dist-lib/config.cjs +6 -3
  68. package/dist-lib/config.mjs +6 -3
  69. package/dist-lib/detox/index.cjs +1379 -725
  70. package/dist-lib/dev-bundle-resolution.cjs +14 -2
  71. package/dist-lib/expo-swift-bundle.cjs +9341 -5462
  72. package/dist-lib/expo-swift-metadata.cjs +11 -7
  73. package/dist-lib/expo-swift-metadata.mjs +11 -7
  74. package/dist-lib/home-paths.cjs +32 -3
  75. package/dist-lib/host/bridge-host.cjs +50204 -7258
  76. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  77. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  78. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  79. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  80. package/dist-lib/host/websocket-proxy.cjs +1 -1
  81. package/dist-lib/index.cjs +3541 -448
  82. package/dist-lib/jump-to-source-babel.cjs +1 -1
  83. package/dist-lib/jump-to-source-native.cjs +1 -1
  84. package/dist-lib/kotlin-host.cjs +1 -1
  85. package/dist-lib/menu.cjs +1 -1
  86. package/dist-lib/menu.mjs +1 -1
  87. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  88. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  89. package/dist-lib/metro-production-bundle.cjs +1 -1
  90. package/dist-lib/metro-production-bundle.mjs +1 -1
  91. package/dist-lib/metro.cjs +2931 -104
  92. package/dist-lib/native-app-identity-project.cjs +289 -49
  93. package/dist-lib/native-app-identity-project.mjs +289 -49
  94. package/dist-lib/native-app-identity.cjs +77 -26
  95. package/dist-lib/native-app-identity.mjs +76 -26
  96. package/dist-lib/native-firebase-metadata.cjs +1 -1
  97. package/dist-lib/native-firebase-metadata.mjs +1 -1
  98. package/dist-lib/plist.cjs +1 -1
  99. package/dist-lib/plist.mjs +1 -1
  100. package/dist-lib/profiles.cjs +1 -1
  101. package/dist-lib/public-brand.cjs +1 -1
  102. package/dist-lib/react-native-host-modules.cjs +6 -2
  103. package/dist-lib/react-native-host-modules.mjs +6 -2
  104. package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
  105. package/dist-lib/sdk.cjs +27 -11
  106. package/dist-lib/sdk.mjs +27 -10
  107. package/dist-lib/skills.cjs +72201 -26208
  108. package/dist-lib/swift-compile.cjs +1007 -140
  109. package/dist-lib/swift-compile.mjs +1049 -174
  110. package/dist-lib/swift-package.cjs +12 -3
  111. package/dist-lib/swift-package.mjs +12 -3
  112. package/dist-lib/swift-submit.cjs +13 -5
  113. package/dist-lib/swift-submit.mjs +11 -5
  114. package/dist-lib/swift-toolchain.cjs +423 -33
  115. package/dist-lib/swift.cjs +1 -1
  116. package/dist-lib/vite-swift.cjs +8628 -5268
  117. package/dist-lib/vite.cjs +2859 -12
  118. package/dist-lib/worklet-executable.cjs +14 -2
  119. package/dist-lib/worklet-executable.mjs +14 -2
  120. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  121. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  122. package/package.json +1 -1
  123. package/scripts/dev-server-scanner.ts +2 -1
  124. package/skills/peach-perf/SKILL.md +11 -5
  125. package/skills/peach-setup/SKILL.md +16 -19
  126. package/src/backend-origin.ts +13 -6
  127. package/src/bridge-constants.ts +11 -0
  128. package/src/bridge-contract.ts +22 -34
  129. package/src/config.ts +15 -0
  130. package/src/desktop-host-delivery.ts +5 -1
  131. package/src/dev-bundle-resolution.ts +2 -1
  132. package/src/expo-manifest-body.ts +13 -0
  133. package/src/expo-swift-host.ts +196 -49
  134. package/src/expo-swift-ready.ts +78 -0
  135. package/src/expoKotlinContract.ts +4 -7
  136. package/src/expoSwiftContract.ts +17 -6
  137. package/src/home-paths.ts +48 -2
  138. package/src/host/bridge-host.ts +5 -2
  139. package/src/host/staged-host-files.ts +9 -0
  140. package/src/native-app-identity-project.ts +27 -6
  141. package/src/native-app-identity.ts +91 -4
  142. package/src/native-firebase-config.ts +209 -13
  143. package/src/nativeAppIconContract.ts +1 -4
  144. package/src/output-lock-stdio.ts +36 -0
  145. package/src/process-group.ts +25 -2
  146. package/src/react-native-bridge-modules.ts +431 -0
  147. package/src/react-native-host-modules.ts +8 -1
  148. package/src/settings-bundle.ts +1 -4
  149. package/src/swift-binary-modules.ts +2 -2
  150. package/src/swift-compile.ts +918 -109
  151. package/src/swift-compiler-digest.ts +14 -0
  152. package/src/swift-distribution.lock.json +24 -12
  153. package/src/swift-distribution.ts +103 -4
  154. package/src/swift-host.ts +180 -55
  155. package/src/swift-icu.ts +27 -13
  156. package/src/swift-objc-compile.ts +355 -0
  157. package/src/swift-objc-sdk.ts +821 -27
  158. package/src/swift-objc-standin.ts +148 -14
  159. package/src/swift-package-manifest.ts +60 -7
  160. package/src/swift-package.ts +15 -1
  161. package/src/swift-packages.ts +154 -81
  162. package/src/swift-standin-emit.ts +96 -15
  163. package/src/swift-standins.ts +530 -116
  164. package/src/swift-submit.ts +13 -5
  165. package/src/swift-toolchain.ts +500 -42
  166. package/src/vite-plugin.ts +12 -1
  167. package/src/worker-system-chunks.ts +11 -12
  168. package/src/worklet-executable.ts +19 -1
  169. package/src/xcode-project.ts +270 -30
package/src/swift-host.ts CHANGED
@@ -2,7 +2,6 @@ import { createHash } from 'node:crypto'
2
2
  // the node host for every Swift preview: package inputs, one compile and
3
3
  // immutable artifact addresses. vite and the foreground CLI adapt this host.
4
4
  import fs from 'node:fs'
5
- import os from 'node:os'
6
5
  import path from 'node:path'
7
6
  import {
8
7
  enumerateBundleColors,
@@ -11,9 +10,13 @@ import {
11
10
  enumerateBundleResources,
12
11
  resolveSwiftImageResourceUrls,
13
12
  } from '../../sootsim-swift/src/bundleAssets'
13
+ import { compileStringCatalog } from '../../sootsim-swift/src/stringCatalog'
14
14
  import { authHeaderValue, resolveCliAuth } from '../cli/auth'
15
15
  import { resolveContrastBackendOrigin } from './backend-origin'
16
+ import { cacheDir } from './home-paths'
16
17
  import iosSdk from './ios-sdk-frameworks.json' with { type: 'json' }
18
+ import { type NativeAppIdentity } from './native-app-identity'
19
+ import { readNativeAppIdentityFromXcodeTarget } from './native-app-identity-project'
17
20
  import { swiftBinaryModules } from './swift-binary-modules'
18
21
  import {
19
22
  compileSwiftProjectUnit,
@@ -27,6 +30,7 @@ import {
27
30
  } from './swift-compile'
28
31
  import { designTimeValues } from './swift-design-time'
29
32
  import { swiftDistributionRoot, swiftModuleToolchain } from './swift-distribution'
33
+ import { compileXcodeObjCImplementations } from './swift-objc-compile'
30
34
  import {
31
35
  swiftImplicitResourcePaths,
32
36
  swiftMetalSourceFiles,
@@ -39,13 +43,13 @@ import {
39
43
  } from './swift-package'
40
44
  import { swiftPackageManifest } from './swift-package-manifest'
41
45
  import {
42
- REMOTE_CHECKOUTS,
46
+ remoteSwiftCheckouts,
43
47
  resolveSwiftPackages,
44
48
  type SwiftPackageBinary,
45
49
  type SwiftPackageGap,
46
50
  type SwiftPackages,
47
51
  } from './swift-packages'
48
- import { readPackageFile } from './swift-source-file'
52
+ import { readPackageFile as readSwiftSourceFile } from './swift-source-file'
49
53
  import { readSwiftStandinBehavior } from './swift-standin-config'
50
54
  import {
51
55
  standinGenerationInputs,
@@ -68,6 +72,7 @@ import {
68
72
  MAX_SWIFT_COMPILE_IMAGE_TOTAL_BYTES,
69
73
  isSwiftNativeModules,
70
74
  compileSwiftProject,
75
+ DEFAULT_SWIFT_COMPILE_BUILD_MS,
71
76
  httpSwiftCompileSubmit,
72
77
  swiftNativeEntry,
73
78
  type SwiftImageUpload,
@@ -242,6 +247,8 @@ interface XcodeProjectEntry {
242
247
  packages?: {
243
248
  key: string
244
249
  standinKey: string
250
+ /** generation always uses the original closure, before stand-ins replace targets. */
251
+ binaries: SwiftPackageBinary[]
245
252
  packages: SwiftPackages
246
253
  standins: SwiftStandin[]
247
254
  /** why a framework the project vendors got no stand-in, by name. */
@@ -263,7 +270,9 @@ export function openXcodeProject(
263
270
  moduleRoot: string | undefined,
264
271
  configuration?: string,
265
272
  ): string {
266
- const app = readXcodeAppTarget(xcodeproj, target, configuration)
273
+ const app = readXcodeAppTarget(xcodeproj, target, configuration, {
274
+ prepareGeneratedSources: true,
275
+ })
267
276
  const root = app.rootDir
268
277
  xcodeProjects.set(root, {
269
278
  xcodeproj,
@@ -308,37 +317,46 @@ function swiftPackages(
308
317
  projectGaps: Map<string, string>
309
318
  binaries: SwiftPackageBinary[]
310
319
  } {
311
- const stamp = (file: string | null) =>
312
- file && fs.existsSync(file) ? fs.statSync(file).mtimeMs : 0
320
+ const stamp = (file: string) =>
321
+ fs.existsSync(file)
322
+ ? createHash('sha256').update(fs.readFileSync(file)).digest('hex')
323
+ : null
313
324
  const key = JSON.stringify([
314
325
  app.linkedProducts,
315
- app.packageResolved,
316
- stamp(app.packageResolved),
326
+ app.packageResolved.map((file) => [file, stamp(file)]),
317
327
  app.localPackageDirs.map((dir) => stamp(path.join(dir, 'Package.swift'))),
318
328
  app.linkedProducts.map(({ source }) =>
319
329
  source.kind === 'local' ? stamp(path.join(source.dir, 'Package.swift')) : 0,
320
330
  ),
321
331
  ])
322
- // the package key is cheap stamps; the stand-in key covers the generation
323
- // inputs the stamps cannot see (artifact contents, peach's modules, the
324
- // emitter), so a changed input rebuilds instead of reusing stale output.
332
+ // resolution follows lock and manifest contents even when a checkout restores
333
+ // timestamps. the stand-in key also covers artifacts, peach modules and emitter.
325
334
  let cached = entry.packages
326
335
  if (cached?.key !== key) {
336
+ const packages = resolveSwiftPackages({
337
+ rootDir: app.rootDir,
338
+ resolved: app.packageResolved,
339
+ products: app.linkedProducts,
340
+ localDirs: app.localPackageDirs,
341
+ provided: entry.provided,
342
+ })
327
343
  cached = {
328
344
  key,
329
345
  standinKey: '',
330
- packages: resolveSwiftPackages({
331
- rootDir: app.rootDir,
332
- resolved: app.packageResolved,
333
- products: app.linkedProducts,
334
- localDirs: app.localPackageDirs,
335
- provided: entry.provided,
336
- }),
346
+ packages,
347
+ binaries: packages.binaries,
337
348
  standins: [],
338
349
  projectGaps: new Map(),
339
350
  }
340
351
  entry.packages = cached
341
352
  }
353
+ if (
354
+ entry.moduleRoot &&
355
+ [app, ...app.frameworks].some((target) =>
356
+ target.allSources.some((file) => /\.mm?$/.test(file)),
357
+ )
358
+ )
359
+ compileXcodeObjCImplementations(app, cached.binaries, entry.moduleRoot)
342
360
  // a reached binary target with stand-ins for every module it ships
343
361
  // resolves as provided, so what depends on it compiles; the rest keep
344
362
  // their reason with why no stand-in was built. surfaces peach already
@@ -360,10 +378,7 @@ function swiftPackages(
360
378
  ]
361
379
  : [],
362
380
  )
363
- const reached = [
364
- ...cached.packages.binaries.filter((binary) => binary.reached),
365
- ...vendored,
366
- ]
381
+ const reached = [...cached.binaries.filter((binary) => binary.reached), ...vendored]
367
382
  if (entry.moduleRoot && entry.provided && reached.length > 0) {
368
383
  const resolved = swiftModuleToolchain(entry.moduleRoot)
369
384
  const toolchain = {
@@ -385,6 +400,8 @@ function swiftPackages(
385
400
  ]),
386
401
  binaryGaps: built.gaps,
387
402
  })
403
+ for (const target of new Set(built.standins.map((standin) => standin.target)))
404
+ console.error(`Peach: target ${target} was replaced with a stand-in`)
388
405
  cached.standins = built.standins
389
406
  cached.projectGaps = new Map(
390
407
  vendored.flatMap(({ target }) => {
@@ -408,14 +425,17 @@ const XCODE_FRAMEWORKS_DIR = '__frameworks__'
408
425
  // remote package checkouts compile here: swiftpm owns `.build` in the
409
426
  // compile's package root as its scratch directory.
410
427
  const XCODE_PACKAGES_DIR = '__packages__'
411
- const CHECKOUTS_PREFIX = `${REMOTE_CHECKOUTS.split(path.sep).join(path.posix.sep)}/`
412
428
 
413
429
  // the compile input key of a root-relative path in an xcode layout.
414
430
  function xcodeInputKey(layout: XcodeLayout, file: string, relative: string): string {
415
431
  const owner = layout.targetDirs.get(file)
416
432
  if (owner) return path.posix.join(owner, relative)
417
- return relative.startsWith(CHECKOUTS_PREFIX)
418
- ? path.posix.join(XCODE_PACKAGES_DIR, relative.slice(CHECKOUTS_PREFIX.length))
433
+ const checkouts = remoteSwiftCheckouts(layout.app.rootDir)
434
+ return file.startsWith(`${checkouts}${path.sep}`)
435
+ ? path.posix.join(
436
+ XCODE_PACKAGES_DIR,
437
+ path.relative(checkouts, file).split(path.sep).join('/'),
438
+ )
419
439
  : relative
420
440
  }
421
441
 
@@ -442,11 +462,26 @@ function xcodeLayout(root: string): XcodeLayout | null {
442
462
  registered.xcodeproj,
443
463
  registered.target,
444
464
  registered.configuration,
465
+ { prepareGeneratedSources: true },
466
+ )
467
+ const implementations = [app, ...app.frameworks].flatMap((target) =>
468
+ target.allSources
469
+ .filter((file) => /\.mm?$/i.test(file))
470
+ .map((file) => `${target.name}: ${path.relative(app.rootDir, file)}`),
445
471
  )
472
+ if (implementations.length > 0 && !registered.moduleRoot)
473
+ throw new Error(
474
+ `Peach cannot compile Objective-C implementations in Xcode targets until the runtime and shared Swift object bridge are available:\n${implementations.map((file) => ` ${file}`).join('\n')}`,
475
+ )
446
476
  const appSources = app.allSources.filter(
447
477
  (file) => file.endsWith('.swift') || file.endsWith('.metal'),
448
478
  )
449
- const files = new Set([...app.projectFiles, ...appSources, ...app.resources])
479
+ const files = new Set([
480
+ ...app.projectFiles,
481
+ ...app.packageResolved,
482
+ ...appSources,
483
+ ...app.resources,
484
+ ])
450
485
  if (app.infoPlistFile) files.add(app.infoPlistFile)
451
486
  const targetDirs = new Map(
452
487
  [...appSources, ...app.resources].map((file) => [file, XCODE_APP_DIR]),
@@ -489,6 +524,7 @@ function xcodeLayout(root: string): XcodeLayout | null {
489
524
  targets.push({
490
525
  name: standin.module,
491
526
  path: path.posix.join(SWIFT_STANDINS_DIR, standin.module),
527
+ dependencies: standin.dependencies?.map((name) => ({ kind: 'target', name })),
492
528
  executable: false,
493
529
  resources: false,
494
530
  exclude: [],
@@ -500,7 +536,7 @@ function xcodeLayout(root: string): XcodeLayout | null {
500
536
  const packageConfiguration = app.configuration === 'Debug' ? 'debug' : 'release'
501
537
  for (const target of packages.targets) {
502
538
  const relative = path.relative(
503
- target.remote ? path.join(root, REMOTE_CHECKOUTS) : root,
539
+ target.remote ? remoteSwiftCheckouts(root) : root,
504
540
  target.dir,
505
541
  )
506
542
  if (
@@ -524,6 +560,13 @@ function xcodeLayout(root: string): XcodeLayout | null {
524
560
  )
525
561
  .map((define) => define.name),
526
562
  ],
563
+ linkedLibraries: target.packageLibraries
564
+ .filter(
565
+ (library) =>
566
+ library.configuration === null ||
567
+ library.configuration === packageConfiguration,
568
+ )
569
+ .map((library) => library.name),
527
570
  })
528
571
  if (target.resources) {
529
572
  resourceBundles.push({
@@ -568,14 +611,19 @@ function xcodeManifest(layout: XcodeLayout): string {
568
611
  ...(target.mainActorIsolation ? ['.defaultIsolation(MainActor.self)'] : []),
569
612
  ...(target.defines ?? []).map((name) => `.define(${JSON.stringify(name)})`),
570
613
  ].join(', ')}]`
614
+ const libraries = (target: SwiftPackageTarget) =>
615
+ target.linkedLibraries?.length
616
+ ? `, linkerSettings: [${target.linkedLibraries.map((name) => `.linkedLibrary(${JSON.stringify(name)})`).join(', ')}]`
617
+ : ''
571
618
  return [
572
619
  '// swift-tools-version: 6.2',
573
620
  'import PackageDescription',
574
621
  'let package = Package(name: "app", targets: [',
575
622
  ` .executableTarget(name: "${layout.app.moduleName}", path: "${XCODE_APP_DIR}", ${settings(layout.app)}),`,
576
- ...layout.targets.map(
577
- (target) =>
578
- ` .target(name: "${target.name}", dependencies: ${JSON.stringify((target.dependencies ?? []).map((dependency) => dependency.name))}, path: "${target.path}", ${target.exclude.length > 0 ? `exclude: ${JSON.stringify(target.exclude)}, ` : ''}${target.resources ? 'resources: [], ' : ''}${settings(target)}),`,
623
+ ...layout.targets.map((target) =>
624
+ target.systemLibrary
625
+ ? ` .systemLibrary(name: "${target.name}", path: "${target.path}"),`
626
+ : ` .target(name: "${target.name}", dependencies: ${JSON.stringify((target.dependencies ?? []).map((dependency) => dependency.name))}, path: "${target.path}", ${target.exclude.length > 0 ? `exclude: ${JSON.stringify(target.exclude)}, ` : ''}${target.resources ? 'resources: [], ' : ''}${settings(target)}${libraries(target)}),`,
579
627
  ),
580
628
  '])',
581
629
  '',
@@ -584,10 +632,29 @@ function xcodeManifest(layout: XcodeLayout): string {
584
632
 
585
633
  // preflight the entire input tree before reading any source or resource bytes.
586
634
  // a nested package is a separate compile unit; ignored build trees never enter it.
587
- function packageFiles(root: string): string[] {
635
+ function packageFiles(root: string, includeBuildInputs = true): string[] {
588
636
  const canonicalRoot = fs.realpathSync(root)
589
637
  const xcode = xcodeLayout(canonicalRoot)
590
- return xcode ? xcode.files : sourceTreeFiles(canonicalRoot, canonicalRoot)
638
+ if (!xcode) return sourceTreeFiles(canonicalRoot, canonicalRoot)
639
+ if (includeBuildInputs) return xcode.files
640
+ const buildInputs = new Set(xcode.app.projectFiles)
641
+ return xcode.files.filter(
642
+ (file) =>
643
+ !buildInputs.has(file) ||
644
+ xcode.targetDirs.has(file) ||
645
+ xcode.packages.targets.some((target) => target.files.includes(file)),
646
+ )
647
+ }
648
+
649
+ // remote dependencies are enumerated from peach's checkout cache, while app
650
+ // inputs retain their selected-project boundary and all symlink checks.
651
+ function readPackageFile(root: string, file: string, maxBytes: number): Buffer {
652
+ if (xcodeProjects.has(root)) {
653
+ const checkouts = remoteSwiftCheckouts(root)
654
+ if (file.startsWith(`${checkouts}${path.sep}`))
655
+ return readSwiftSourceFile(checkouts, file, maxBytes)
656
+ }
657
+ return readSwiftSourceFile(root, file, maxBytes)
591
658
  }
592
659
 
593
660
  // every input file under `dir`, which must stay inside `canonicalRoot`.
@@ -636,16 +703,18 @@ function isPackageSource(file: string): boolean {
636
703
  }
637
704
 
638
705
  export function sourceFiles(root: string): string[] {
639
- return packageFiles(root).filter(isPackageSource)
706
+ return packageFiles(root, false).filter(isPackageSource)
640
707
  }
641
708
 
642
709
  // project files and the xcconfigs their settings read are build inputs, never resources
643
710
  function resourceCandidateFiles(root: string): string[] {
644
- return packageFiles(root).filter(
711
+ return packageFiles(root, false).filter(
645
712
  (file) =>
646
713
  !isPackageSource(file) &&
647
714
  !file.endsWith('.xcconfig') &&
648
- !['project.pbxproj', 'project.xcproj'].includes(path.basename(file)),
715
+ !['project.pbxproj', 'project.xcproj', 'Package.resolved'].includes(
716
+ path.basename(file),
717
+ ),
649
718
  )
650
719
  }
651
720
 
@@ -653,6 +722,7 @@ function resourceCandidateFiles(root: string): string[] {
653
722
  export function swiftProjectAppInfo(packagePath: string): {
654
723
  name: string
655
724
  icon: { bytes: Buffer; contentType: string } | null
725
+ identity: NativeAppIdentity | null
656
726
  } {
657
727
  const registered = xcodeProjects.get(packagePath)
658
728
  const app = registered
@@ -715,12 +785,17 @@ export function swiftProjectAppInfo(packagePath: string): {
715
785
  }
716
786
  break
717
787
  }
718
- return { name: app?.name ?? path.basename(packagePath), icon }
788
+ return {
789
+ name: app?.name ?? path.basename(packagePath),
790
+ icon,
791
+ identity: app ? readNativeAppIdentityFromXcodeTarget(app) : null,
792
+ }
719
793
  }
720
794
 
721
795
  // the hosted compile service caps what a project sends it; a local compile
722
796
  // reads the author's own files, so nothing caps them there.
723
797
  const HOSTED_LIMITS = {
798
+ sourceBytes: MAX_SWIFT_COMPILE_SOURCE_BYTES,
724
799
  resources: MAX_SWIFT_COMPILE_RESOURCES,
725
800
  resourceBytes: MAX_SWIFT_COMPILE_RESOURCE_BYTES,
726
801
  resourceTotalBytes: MAX_SWIFT_COMPILE_RESOURCE_TOTAL_BYTES,
@@ -729,6 +804,7 @@ const HOSTED_LIMITS = {
729
804
  imageTotalBytes: MAX_SWIFT_COMPILE_IMAGE_TOTAL_BYTES,
730
805
  }
731
806
  const LOCAL_LIMITS: typeof HOSTED_LIMITS = {
807
+ sourceBytes: Infinity,
732
808
  resources: Infinity,
733
809
  resourceBytes: Infinity,
734
810
  resourceTotalBytes: Infinity,
@@ -937,7 +1013,7 @@ export function buildSwiftBundleFiles(
937
1013
  const xcode = xcodeLayout(packagePath)
938
1014
  const bundles =
939
1015
  xcode?.resourceBundles ??
940
- swiftResourceBundles(inputs ?? packageSources(packagePath), relative)
1016
+ swiftResourceBundles(inputs ?? packageSources(packagePath, hosted), relative)
941
1017
  const addFile = (key: string, bytes: Buffer) => {
942
1018
  if (
943
1019
  Object.keys(files).length >= limits.resources ||
@@ -983,6 +1059,13 @@ export function buildSwiftBundleFiles(
983
1059
  const disk = path.join(packagePath, ...file.split('/'))
984
1060
  const bytes = readPackageFile(packagePath, disk, limits.resourceBytes)
985
1061
  addFile(bundlePath === '.' ? base : `${bundlePath}/${base}`, bytes)
1062
+ if (base.endsWith('.xcstrings')) {
1063
+ for (const [name, table] of Object.entries(
1064
+ compileStringCatalog(bytes.toString('utf8'), base.slice(0, -10)),
1065
+ )) {
1066
+ addFile(bundlePath === '.' ? name : `${bundlePath}/${name}`, Buffer.from(table))
1067
+ }
1068
+ }
986
1069
  }
987
1070
  }
988
1071
  return { files, bundles, watched }
@@ -1048,6 +1131,24 @@ export function sourceStamp(
1048
1131
  return hash.digest('hex').slice(0, 16)
1049
1132
  }
1050
1133
 
1134
+ // sourceStamp's inputs read by content and relative to the package, so
1135
+ // another process's build copy of the same sources stamps the same.
1136
+ export function sourceContentStamp(
1137
+ packagePath: string,
1138
+ moduleRootOverride?: string,
1139
+ ): string {
1140
+ const hash = createHash('sha256')
1141
+ hash.update(JSON.stringify(xcodeProjects.get(packagePath)?.behavior ?? {}))
1142
+ const libraries = projects.get(packagePath)?.libraries
1143
+ if (libraries) hash.update(JSON.stringify(libraries))
1144
+ for (const file of [...new Set(packageFiles(packagePath))].sort()) {
1145
+ hash.update(path.relative(packagePath, file))
1146
+ hash.update(fs.readFileSync(file))
1147
+ }
1148
+ hash.update(moduleStamp(packagePath, moduleRootOverride, false))
1149
+ return hash.digest('hex').slice(0, 16)
1150
+ }
1151
+
1051
1152
  // the linked module's part of the source stamp
1052
1153
  function moduleStamp(
1053
1154
  packagePath: string,
@@ -1118,6 +1219,7 @@ export function standinLibraries(
1118
1219
 
1119
1220
  function packageSources(
1120
1221
  packagePath: string,
1222
+ hosted = false,
1121
1223
  linked: ReadonlySet<SwiftStandin> = new Set(),
1122
1224
  ): Record<string, string> {
1123
1225
  const xcode = xcodeLayout(packagePath)
@@ -1127,13 +1229,14 @@ function packageSources(
1127
1229
  ),
1128
1230
  )
1129
1231
  const files: Record<string, string> = {}
1232
+ const limits = inputLimits(hosted)
1130
1233
  for (const file of sourceFiles(packagePath)) {
1131
1234
  const relative = path.relative(packagePath, file).split(path.sep).join(path.posix.sep)
1132
1235
  if (manifests.has(file)) continue
1133
1236
  files[xcode ? xcodeInputKey(xcode, file, relative) : relative] = readPackageFile(
1134
1237
  packagePath,
1135
1238
  file,
1136
- MAX_SWIFT_COMPILE_SOURCE_BYTES,
1239
+ limits.sourceBytes,
1137
1240
  ).toString('utf8')
1138
1241
  }
1139
1242
  if (xcode) {
@@ -1164,7 +1267,11 @@ function packageSnapshot(project: SwiftProject, entry: string): SwiftPackageSnap
1164
1267
  const linked = layout
1165
1268
  ? standinLibraries(layout.binaries, layout.standins, layout.app.rootDir)
1166
1269
  : new Map<string, SwiftStandin[]>()
1167
- const files = packageSources(project.packagePath, new Set([...linked.values()].flat()))
1270
+ const files = packageSources(
1271
+ project.packagePath,
1272
+ Boolean(project.hosted),
1273
+ new Set([...linked.values()].flat()),
1274
+ )
1168
1275
  const native = nativeSourceDescriptor(project.packagePath)
1169
1276
  return {
1170
1277
  files,
@@ -1177,9 +1284,7 @@ function packageSnapshot(project: SwiftProject, entry: string): SwiftPackageSnap
1177
1284
  .relative(project.packagePath, file)
1178
1285
  .split(path.sep)
1179
1286
  .join(path.posix.sep)
1180
- return layout && file.endsWith('.metal')
1181
- ? xcodeInputKey(layout, file, relative)
1182
- : relative
1287
+ return layout ? xcodeInputKey(layout, file, relative) : relative
1183
1288
  }),
1184
1289
  ),
1185
1290
  resources: buildSwiftResourceTable(project, '', files),
@@ -1207,7 +1312,7 @@ function packageSnapshot(project: SwiftProject, entry: string): SwiftPackageSnap
1207
1312
  glue: readPackageFile(
1208
1313
  project.packagePath,
1209
1314
  path.join(project.packagePath, NATIVE_GLUE),
1210
- MAX_SWIFT_COMPILE_SOURCE_BYTES,
1315
+ inputLimits(Boolean(project.hosted)).sourceBytes,
1211
1316
  ).toString('utf8'),
1212
1317
  }
1213
1318
  : {}),
@@ -1304,13 +1409,20 @@ async function compile(
1304
1409
  if (process.env.CONTRAST_SWIFT_BUILD_ROOT)
1305
1410
  return process.env.CONTRAST_SWIFT_BUILD_ROOT
1306
1411
  const resolvedPath = path.resolve(packagePath)
1412
+ // units of one app share a root so the module cache stays warm.
1413
+ // peach-expo-swift/<app>/<scope>/<package> shares <app>/<scope>, and
1414
+ // the older <app>/<package> layout shares <app>. the scope is this
1415
+ // process, so two servers do not compile into one tree.
1416
+ const parts = resolvedPath.split(path.sep)
1417
+ const expoAt = parts.lastIndexOf('peach-expo-swift')
1418
+ const scoped = expoAt >= 0 && parts.length >= expoAt + 4
1307
1419
  const buildKeyPath =
1308
- path.basename(path.dirname(path.dirname(resolvedPath))) === 'peach-expo-swift'
1309
- ? path.dirname(resolvedPath)
1420
+ expoAt >= 0 && parts.length >= expoAt + 3
1421
+ ? parts.slice(0, scoped ? expoAt + 3 : expoAt + 2).join(path.sep)
1310
1422
  : resolvedPath
1311
1423
  return path.join(
1312
- os.tmpdir(),
1313
- 'contrast-swift-build',
1424
+ cacheDir(),
1425
+ 'swift-builds',
1314
1426
  createHash('sha256')
1315
1427
  .update(`${moduleRoot}\0${buildKeyPath}`)
1316
1428
  .digest('hex')
@@ -1349,6 +1461,21 @@ const API_GAP_PATTERNS: [RegExp, (match: RegExpExecArray) => string][] = [
1349
1461
  (match) => `${match[1]}.${match[2]}`,
1350
1462
  ],
1351
1463
  [/^'([^']+)' is unavailable/, (match) => match[1]!],
1464
+ // an instance api Peach declares static, or the reverse
1465
+ [
1466
+ /^(?:static|instance) member '([^']+)' cannot be used on (?:instance of )?type '([^']+)'/,
1467
+ (match) => `${match[2]}.${match[1]}`,
1468
+ ],
1469
+ // an overridable api Peach declares in an extension
1470
+ [
1471
+ /^(?:non-@objc )?[\w ]+ '([^']+)' is declared in extension of '([^']+)' and cannot be overridden/,
1472
+ (match) => `${match[2]}.${match[1]}`,
1473
+ ],
1474
+ // a form of a macro Peach's expansion does not accept
1475
+ [
1476
+ /^(@\w+) requires an initial value/,
1477
+ (match) => `${match[1]} without an initial value`,
1478
+ ],
1352
1479
  [/^no such module '([^']+)'/, (match) => `module ${match[1]}`],
1353
1480
  ]
1354
1481
 
@@ -1363,7 +1490,7 @@ export function swiftApiGapReport(output: string): string | null {
1363
1490
  const file = diagnostic[1]!
1364
1491
  .replace(new RegExp(`^(?:${XCODE_APP_DIR}|${XCODE_FRAMEWORKS_DIR}/[^/]+)/`), '')
1365
1492
  .replace(new RegExp(`^${XCODE_PACKAGES_DIR}/`), '')
1366
- const message = diagnostic[4]!
1493
+ const message = diagnostic[4]!.trim()
1367
1494
  const rule = API_GAP_PATTERNS.find(([pattern]) => pattern.test(message))
1368
1495
  const key = rule ? rule[1](rule[0].exec(message)!) : message
1369
1496
  const table = rule ? gaps : other
@@ -1496,7 +1623,7 @@ async function compileRemote(
1496
1623
  hosted?.authorization ?? (auth ? authHeaderValue(auth) : undefined)
1497
1624
  const signal = AbortSignal.any([
1498
1625
  ...(ownerSignal ? [ownerSignal] : []),
1499
- AbortSignal.timeout(5 * 60_000),
1626
+ AbortSignal.timeout(DEFAULT_SWIFT_COMPILE_BUILD_MS + 60_000),
1500
1627
  ])
1501
1628
  const submit = httpSwiftCompileSubmit({
1502
1629
  endpoint: origin,
@@ -1664,9 +1791,8 @@ export function swiftPackageWatchFiles(
1664
1791
  packagePath = fs.realpathSync(packagePath)
1665
1792
  return [
1666
1793
  ...(xcodeProjects.has(packagePath) ? [] : [path.join(packagePath, 'Package.swift')]),
1667
- ...sourceFiles(packagePath),
1794
+ ...packageFiles(packagePath),
1668
1795
  ...nativeSourceFiles(packagePath),
1669
- ...resourceCandidateFiles(packagePath),
1670
1796
  ...(moduleRoot
1671
1797
  ? swiftModuleInputFiles(moduleRoot).map((file) => path.join(moduleRoot, file))
1672
1798
  : []),
@@ -1682,14 +1808,14 @@ export async function swiftPackageMetadata(
1682
1808
  signal?: AbortSignal,
1683
1809
  ): Promise<SwiftEntryModuleArgs> {
1684
1810
  packagePath = fs.realpathSync(packagePath)
1811
+ const project = projectFor(packagePath, hosted, signal)
1812
+ const limits = inputLimits(Boolean(project.hosted))
1685
1813
  const mains = sourceFiles(packagePath).filter(
1686
1814
  (file) =>
1687
1815
  file.endsWith('.swift') &&
1688
1816
  swiftApplicationEntry(
1689
1817
  file,
1690
- readPackageFile(packagePath, file, MAX_SWIFT_COMPILE_SOURCE_BYTES).toString(
1691
- 'utf8',
1692
- ),
1818
+ readPackageFile(packagePath, file, limits.sourceBytes).toString('utf8'),
1693
1819
  ),
1694
1820
  )
1695
1821
  const native = nativeSourceDescriptor(packagePath)
@@ -1698,7 +1824,6 @@ export async function swiftPackageMetadata(
1698
1824
  `swift package ${packageName} needs exactly one application entry source or generated native modules; found ${mains.length}`,
1699
1825
  )
1700
1826
  }
1701
- const project = projectFor(packagePath, hosted, signal)
1702
1827
  const entry =
1703
1828
  mains.length === 1
1704
1829
  ? path.relative(packagePath, mains[0]).split(path.sep).join(path.posix.sep)
package/src/swift-icu.ts CHANGED
@@ -4,6 +4,7 @@ import { existsSync, readFileSync } from 'node:fs'
4
4
  import path from 'node:path'
5
5
  import { promisify } from 'node:util'
6
6
  import { gzip } from 'node:zlib'
7
+ import { swiftEnvironment, swiftSdkOptions } from './swift-toolchain.ts'
7
8
 
8
9
  /**
9
10
  * the icu common data a swift artifact reserves and its host fills
@@ -49,15 +50,21 @@ function memo<T>(
49
50
 
50
51
  /** the raw blob, for a harness that fills an artifact in node. */
51
52
  export function swiftIcuData(toolchainBin: string, sdk: string): Promise<Buffer> {
52
- return memo(raw, `${toolchainBin}\0${sdk}`, () => load(toolchainBin, sdk))
53
+ return memo(raw, JSON.stringify([toolchainBin, sdk, swiftSdkOptions()]), () =>
54
+ load(toolchainBin, sdk),
55
+ )
53
56
  }
54
57
 
55
58
  /** the served form: gzipped and content-addressed. */
56
59
  export function swiftIcuAsset(toolchainBin: string, sdk: string): Promise<SwiftIcuAsset> {
57
- return memo(assets, `${toolchainBin}\0${sdk}`, async () => {
58
- const zipped = await deflate(await swiftIcuData(toolchainBin, sdk))
59
- return { sha256: createHash('sha256').update(zipped).digest('hex'), gzip: zipped }
60
- })
60
+ return memo(
61
+ assets,
62
+ JSON.stringify([toolchainBin, sdk, swiftSdkOptions()]),
63
+ async () => {
64
+ const zipped = await deflate(await swiftIcuData(toolchainBin, sdk))
65
+ return { sha256: createHash('sha256').update(zipped).digest('hex'), gzip: zipped }
66
+ },
67
+ )
61
68
  }
62
69
 
63
70
  async function load(toolchainBin: string, sdk: string): Promise<Buffer> {
@@ -66,16 +73,23 @@ async function load(toolchainBin: string, sdk: string): Promise<Buffer> {
66
73
  // missing piece instead of shelling out to sdk configure.
67
74
  if (!existsSync(swift))
68
75
  throw new Error(`swift-icu: no swift at ${swift} for the ${sdk} SDK's icu data`)
69
- const { stdout: listed } = await run(swift, ['sdk', 'list'])
76
+ const { stdout: listed } = await run(swift, ['sdk', 'list', ...swiftSdkOptions()], {
77
+ env: swiftEnvironment(),
78
+ })
70
79
  if (!listed.split('\n').some((line) => line.trim() === sdk))
71
80
  throw new Error(`swift-icu: ${sdk} is not installed for ${swift}`)
72
- const { stdout } = await run(swift, [
73
- 'sdk',
74
- 'configure',
75
- '--show-configuration',
76
- sdk,
77
- 'wasm32-unknown-wasip1',
78
- ])
81
+ const { stdout } = await run(
82
+ swift,
83
+ [
84
+ 'sdk',
85
+ 'configure',
86
+ ...swiftSdkOptions(),
87
+ '--show-configuration',
88
+ sdk,
89
+ 'wasm32-unknown-wasip1',
90
+ ],
91
+ { env: swiftEnvironment() },
92
+ )
79
93
  const resources = /^swiftStaticResourcesPath: (.+)$/m.exec(stdout)?.[1]?.trim()
80
94
  if (!resources) throw new Error(`swift-icu: ${sdk} names no static resources`)
81
95
  // a copy, so the 42 MB archive it was sliced from can be collected