peachsim 0.1.1620 → 0.1.1635

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +45 -6
  3. package/cli/app-config.ts +4 -1
  4. package/cli/app-fonts.ts +39 -18
  5. package/cli/app-stubs.ts +224 -9
  6. package/cli/bridge-flow-runner.ts +248 -156
  7. package/cli/cloud-client.ts +68 -57
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/box/search-command.ts +3 -2
  10. package/cli/commands/box.ts +10 -29
  11. package/cli/commands/control.ts +391 -92
  12. package/cli/commands/doctor.ts +17 -184
  13. package/cli/commands/flow.ts +79 -11
  14. package/cli/commands/inspect/core.ts +10 -5
  15. package/cli/commands/inspect/settling.ts +71 -22
  16. package/cli/commands/inspect/wait-ready.ts +12 -0
  17. package/cli/commands/inspect.ts +26 -3
  18. package/cli/commands/install-desktop.ts +1 -0
  19. package/cli/commands/maestro-compare.ts +105 -67
  20. package/cli/commands/maestro.ts +9 -1
  21. package/cli/commands/platform.ts +71 -36
  22. package/cli/commands/record.ts +21 -1
  23. package/cli/commands/screenshot.ts +23 -1
  24. package/cli/commands/serve.ts +7 -2
  25. package/cli/commands/setup.ts +19 -42
  26. package/cli/commands/swift.ts +5 -0
  27. package/cli/commands/upload.ts +51 -8
  28. package/cli/desktop-companion.ts +102 -17
  29. package/cli/dev-build-freshness.ts +37 -2
  30. package/cli/drivers/desktop.ts +14 -1
  31. package/cli/drivers/playwright-sim-host.ts +44 -8
  32. package/cli/drivers/playwright.ts +9 -8
  33. package/cli/drivers/types.ts +5 -0
  34. package/cli/maestro-accessibility.ts +53 -0
  35. package/cli/maestro-compare.ts +108 -12
  36. package/cli/main.ts +2 -1
  37. package/cli/outbound-endpoints.ts +35 -6
  38. package/cli/parent-pid.ts +0 -9
  39. package/cli/pause-aware-timeout.ts +102 -0
  40. package/cli/remote-simulators.ts +112 -0
  41. package/cli/remoteSimulatorContract.ts +21 -0
  42. package/cli/scene-video.ts +89 -0
  43. package/cli/ws-bridge.ts +23 -14
  44. package/detox/attempt-events.cjs +4 -0
  45. package/detox/expectations.ts +5 -1
  46. package/detox/index.ts +241 -181
  47. package/detox/motion-change.cjs +73 -0
  48. package/detox/motion-change.d.cts +21 -0
  49. package/detox/navigation-lifecycle.ts +22 -4
  50. package/dist-lib/agent-daemon-client.cjs +3 -2
  51. package/dist-lib/agent-events.cjs +1 -1
  52. package/dist-lib/attached-projects.cjs +1 -1
  53. package/dist-lib/auth/shared-session.cjs +3 -2
  54. package/dist-lib/backend-origin.cjs +1 -1
  55. package/dist-lib/beta.cjs +1 -1
  56. package/dist-lib/beta.mjs +1 -1
  57. package/dist-lib/bridge-constants.cjs +13 -1
  58. package/dist-lib/bridge-contract-input.cjs +1 -1
  59. package/dist-lib/bridge-contract-input.mjs +1 -1
  60. package/dist-lib/bridge-contract.cjs +14 -8
  61. package/dist-lib/bridge-contract.mjs +13 -7
  62. package/dist-lib/bundle-channel.cjs +10480 -7962
  63. package/dist-lib/capture-contract.cjs +1 -1
  64. package/dist-lib/capture-contract.mjs +1 -1
  65. package/dist-lib/cloud-contract.cjs +1 -1
  66. package/dist-lib/cloud-contract.mjs +1 -1
  67. package/dist-lib/cloud-test-args.cjs +1 -1
  68. package/dist-lib/cloud-test-args.mjs +1 -1
  69. package/dist-lib/cloud.cjs +3 -2
  70. package/dist-lib/cloud.mjs +3 -2
  71. package/dist-lib/config.cjs +10 -6
  72. package/dist-lib/config.mjs +10 -6
  73. package/dist-lib/detox/index.cjs +1854 -966
  74. package/dist-lib/dev-bundle-resolution.cjs +64 -24
  75. package/dist-lib/expo-swift-bundle.cjs +8715 -6271
  76. package/dist-lib/expo-swift-metadata.cjs +7 -4
  77. package/dist-lib/expo-swift-metadata.mjs +7 -4
  78. package/dist-lib/home-paths.cjs +32 -3
  79. package/dist-lib/host/bridge-host.cjs +10135 -7060
  80. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  81. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  82. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  83. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  84. package/dist-lib/host/websocket-proxy.cjs +1 -1
  85. package/dist-lib/index.cjs +1843 -408
  86. package/dist-lib/jump-to-source-babel.cjs +1 -1
  87. package/dist-lib/jump-to-source-native.cjs +1 -1
  88. package/dist-lib/kotlin-host.cjs +1 -1
  89. package/dist-lib/menu.cjs +3 -2
  90. package/dist-lib/menu.mjs +3 -2
  91. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  92. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  93. package/dist-lib/metro-production-bundle.cjs +5 -4
  94. package/dist-lib/metro-production-bundle.mjs +5 -4
  95. package/dist-lib/metro.cjs +1182 -418
  96. package/dist-lib/native-app-identity-project.cjs +782 -23
  97. package/dist-lib/native-app-identity-project.mjs +781 -23
  98. package/dist-lib/native-app-identity.cjs +276 -9
  99. package/dist-lib/native-app-identity.mjs +273 -8
  100. package/dist-lib/native-firebase-metadata.cjs +5 -4
  101. package/dist-lib/native-firebase-metadata.mjs +5 -4
  102. package/dist-lib/plist.cjs +1 -1
  103. package/dist-lib/plist.mjs +1 -1
  104. package/dist-lib/profiles.cjs +1 -1
  105. package/dist-lib/public-brand.cjs +3 -2
  106. package/dist-lib/react-native-host-modules.cjs +8 -1
  107. package/dist-lib/react-native-host-modules.mjs +8 -1
  108. package/dist-lib/scripts/dev-server-scanner.cjs +106 -28
  109. package/dist-lib/sdk.cjs +115 -34
  110. package/dist-lib/sdk.mjs +115 -33
  111. package/dist-lib/skills.cjs +15274 -9904
  112. package/dist-lib/swift-compile.cjs +1165 -173
  113. package/dist-lib/swift-compile.mjs +1212 -203
  114. package/dist-lib/swift-package.cjs +12 -3
  115. package/dist-lib/swift-package.mjs +12 -3
  116. package/dist-lib/swift-submit.cjs +7 -1
  117. package/dist-lib/swift-submit.mjs +5 -1
  118. package/dist-lib/swift-toolchain.cjs +133 -18
  119. package/dist-lib/swift.cjs +1 -1
  120. package/dist-lib/vite-swift.cjs +2648 -862
  121. package/dist-lib/vite.cjs +70 -14
  122. package/dist-lib/worklet-executable.cjs +25 -2
  123. package/dist-lib/worklet-executable.mjs +25 -2
  124. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  125. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  126. package/package.json +2 -2
  127. package/scripts/dev-server-scanner.ts +35 -14
  128. package/skills/peach-perf/SKILL.md +11 -5
  129. package/skills/peach-setup/SKILL.md +16 -19
  130. package/src/admitted-mac-build.ts +34 -8
  131. package/src/android-app-identity.ts +146 -0
  132. package/src/android-autolinking.ts +75 -0
  133. package/src/android-string-value.ts +36 -0
  134. package/src/android-strings.ts +5 -36
  135. package/src/androidAutolinkingContract.ts +40 -0
  136. package/src/bridge-constants.ts +11 -0
  137. package/src/bridge-contract.ts +22 -34
  138. package/src/bundleGlobalFooter.ts +26 -7
  139. package/src/codex-client.ts +3 -2
  140. package/src/config.ts +15 -0
  141. package/src/desktop-host-delivery.ts +5 -1
  142. package/src/dev-bundle-resolution.ts +60 -26
  143. package/src/expo-kotlin-host.ts +6 -36
  144. package/src/expo-manifest-body.ts +13 -0
  145. package/src/expo-swift-bundle.ts +17 -12
  146. package/src/expo-swift-host.ts +640 -192
  147. package/src/expo-swift-ready.ts +78 -0
  148. package/src/expoSwiftContract.ts +19 -0
  149. package/src/home-paths.ts +48 -2
  150. package/src/host/bridge-host.ts +12 -57
  151. package/src/host/staged-host-files.ts +29 -2
  152. package/src/installed-package-versions.ts +54 -14
  153. package/src/launchMetadataContract.ts +24 -0
  154. package/src/metro-plugin.ts +37 -42
  155. package/src/native-app-identity-project.ts +142 -5
  156. package/src/native-app-identity.ts +176 -11
  157. package/src/native-firebase-config.ts +209 -13
  158. package/src/nitro-compile.ts +2 -5
  159. package/src/nitro-source.ts +147 -63
  160. package/src/process-group.ts +114 -61
  161. package/src/process-output.ts +0 -9
  162. package/src/public-brand.ts +1 -0
  163. package/src/react-native-bridge-modules.ts +416 -0
  164. package/src/react-native-host-modules.ts +7 -0
  165. package/src/sim-client.ts +6 -4
  166. package/src/swift-binary-modules.ts +2 -2
  167. package/src/swift-compile.ts +1007 -108
  168. package/src/swift-compiler-digest.ts +14 -0
  169. package/src/swift-distribution.lock.json +40 -12
  170. package/src/swift-distribution.ts +61 -9
  171. package/src/swift-host.ts +149 -56
  172. package/src/swift-icu.ts +14 -6
  173. package/src/swift-objc-sdk.ts +133 -10
  174. package/src/swift-objc-standin.ts +38 -3
  175. package/src/swift-package-manifest.ts +21 -6
  176. package/src/swift-package.ts +15 -1
  177. package/src/swift-packages.ts +119 -60
  178. package/src/swift-standin-emit.ts +57 -9
  179. package/src/swift-standins.ts +185 -25
  180. package/src/swift-submit.ts +10 -0
  181. package/src/swift-toolchain.ts +149 -23
  182. package/src/vite-plugin.ts +20 -20
  183. package/src/worklet-executable.ts +30 -3
  184. package/src/worklets-babel.ts +35 -14
  185. package/src/xcode-project.ts +10 -0
@@ -0,0 +1,14 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { readFileSync } from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ // the lowering and manifest code in swift-compile.ts and its design-time lowering
6
+ // decide the emitted output, so their text addresses every swift artifact. bundled
7
+ // builds cannot read these files at runtime, so build-cli injects this digest.
8
+ export function swiftCompilerSourceDigest(srcDir: string): string {
9
+ const hash = createHash('sha256')
10
+ for (const file of ['swift-compile.ts', 'swift-design-time.ts']) {
11
+ hash.update(readFileSync(path.join(srcDir, file)))
12
+ }
13
+ return hash.digest('hex')
14
+ }
@@ -1,11 +1,34 @@
1
1
  {
2
- "id": "64fd4ea71bc39e4f",
3
- "release": "swift-6.4.0-RELEASE",
4
- "wasmSdkId": "swift-6.4.0-RELEASE_wasm-peach-dc9c8c67",
5
- "wasmSdkUrl": "https://r2.sootsim.com/swift-wasm-sdk/swift-6.4.0-RELEASE_wasm-peach-dc9c8c67.artifactbundle.tar.gz",
6
- "wasmSdkChecksum": "5d38f795f516ccfef3d7ac869b447cdedcd82a35af9761a65b9f88924eb6a89a",
7
- "source": "77fbca997182e53ceb1944e27d8689f469f632c5",
2
+ "id": "4fdcd218fa180b52",
3
+ "release": "swift-6.4.0-RELEASE-custom-91504a66b3f012ac",
4
+ "wasmSdkId": "swift-6.4.0-RELEASE_wasm-peach-7fd9a101",
5
+ "wasmSdkUrl": "https://r2.sootsim.com/swift-wasm-sdk/swift-6.4.0-RELEASE_wasm-peach-7fd9a101.artifactbundle.tar.gz",
6
+ "wasmSdkChecksum": "72f7fec1d090d142d26148d6cc0cd5b1ce78da70bb97bd577dc1482eaf0299b2",
7
+ "source": "80c26dea3c4bbf0e29458c33a3a0e856b0942564",
8
8
  "targets": [
9
+ "System",
10
+ "_LocationEssentials",
11
+ "GeoToolbox",
12
+ "DataDetection",
13
+ "Accessibility",
14
+ "Contacts",
15
+ "CoreML",
16
+ "EventKit",
17
+ "CloudKit",
18
+ "IOSurface",
19
+ "OpenGLES",
20
+ "ContactsUI",
21
+ "CoreLocationUI",
22
+ "MediaPlayer",
23
+ "MusicKit",
24
+ "RealityKit",
25
+ "SecureElementCredential",
26
+ "Social",
27
+ "Speech",
28
+ "Vision",
29
+ "VisionKit",
30
+ "WorkoutKit",
31
+ "_PassKit_SwiftUI",
9
32
  "ExpoModulesCore",
10
33
  "_MapKit_SwiftUI",
11
34
  "PeachJSON",
@@ -13,14 +36,17 @@
13
36
  "FoundationNetworking",
14
37
  "DeveloperToolsSupport",
15
38
  "SwiftUI",
39
+ "os_workgroup",
16
40
  "Dispatch",
17
41
  "DarwinRecords",
18
42
  "Darwin",
19
43
  "MachO",
20
44
  "ObjectiveC",
21
45
  "PeachObjC",
46
+ "UIUtilities",
22
47
  "UIKitCore",
23
48
  "PeachICU",
49
+ "PeachCMark",
24
50
  "CoreImage",
25
51
  "QuartzCore",
26
52
  "CoreGraphics",
@@ -51,6 +77,7 @@
51
77
  "ActivityKit",
52
78
  "SwiftData",
53
79
  "UIKit",
80
+ "React",
54
81
  "Combine",
55
82
  "PeachFoundationCombine",
56
83
  "AppIntents",
@@ -132,6 +159,7 @@
132
159
  "systemVersion": ["string"],
133
160
  "colorScheme": ["string:light", "string:dark"],
134
161
  "semanticColors": ["object"],
162
+ "imageAssets": ["object"],
135
163
  "locale": ["string"],
136
164
  "dynamicTypeSize": [
137
165
  "string:xSmall",
@@ -153,16 +181,16 @@
153
181
  },
154
182
  "hosts": {
155
183
  "darwin-arm64": {
156
- "sha256": "6b60bae2bc91957af3baee5e9cad44200f15ff3840d8e70afa5ddc63413d880f",
157
- "size": 58565898
184
+ "sha256": "2b6768539f39bbdc2d77447fc91b00e9a7ee2d9ed732ea10e67a7b05c1c854ea",
185
+ "size": 69899802
158
186
  },
159
187
  "linux-x64": {
160
- "sha256": "d97e287ca5d7ea0d00dfc9db60be746aec35a9b7412299461ce0d5e77f38b7a8",
161
- "size": 58569694
188
+ "sha256": "2d357dc6c24559b1e4f792c4142de2608e2dd91a2b322c74013af31a9346ad6e",
189
+ "size": 69902826
162
190
  },
163
191
  "linux-arm64": {
164
- "sha256": "495fe6adc7cf1c8ec9fc8de3410c976e65a7ff051ca50ee3f6d0d3fee8aa2c5d",
165
- "size": 58567328
192
+ "sha256": "fdbdc2b25d102db38da6c00e74c6d198b87537aefe74365ef65793ec6feb3faa",
193
+ "size": 69900830
166
194
  }
167
195
  }
168
196
  }
@@ -27,13 +27,23 @@ export interface SwiftModule {
27
27
  toolchainBin: string
28
28
  }
29
29
 
30
- const installs = new Map<string, Promise<string>>()
30
+ const installs = new Map<
31
+ string,
32
+ {
33
+ promise: Promise<string>
34
+ controller: AbortController
35
+ clients: number
36
+ }
37
+ >()
31
38
 
32
39
  /** the installed distribution's root, downloading it on first use, or null
33
40
  * when PEACH_SWIFT_ENDPOINT is unset. */
34
- export function swiftDistributionRoot(): Promise<string | null> {
41
+ export async function swiftDistributionRoot(
42
+ signal?: AbortSignal,
43
+ ): Promise<string | null> {
44
+ signal?.throwIfAborted()
35
45
  const endpoint = process.env[SWIFT_ENDPOINT_ENV]?.trim().replace(/\/+$/, '')
36
- if (!endpoint) return Promise.resolve(null)
46
+ if (!endpoint) return null
37
47
  const lock = parseSwiftDistributionLock(lockJson)
38
48
  const host = swiftDistributionHost(process.platform, process.arch)
39
49
  if (!host) {
@@ -46,13 +56,16 @@ export function swiftDistributionRoot(): Promise<string | null> {
46
56
  swiftToolchain(lock.release, lock)
47
57
  const dir = path.join(rnxHomeDir(), 'swift', `${lock.id}-${host}`)
48
58
  let pending = installs.get(dir)
59
+ if (pending?.controller.signal.aborted) pending = undefined
49
60
  if (!pending) {
50
- pending = (async () => {
61
+ const controller = new AbortController()
62
+ const promise = (async () => {
51
63
  if (!fs.existsSync(path.join(dir, SWIFT_DISTRIBUTION_MANIFEST))) {
52
64
  process.stderr.write(
53
65
  `peach: downloading Swift distribution ${lock.id} (${host})…\n`,
54
66
  )
55
67
  await installVerifiedTarball({
68
+ signal: controller.signal,
56
69
  url: `${endpoint}/${swiftDistributionKey(lock.id, host)}`,
57
70
  digest: {
58
71
  algorithm: 'sha256',
@@ -69,11 +82,48 @@ export function swiftDistributionRoot(): Promise<string | null> {
69
82
  throw new Error(`${dir} does not hold Swift distribution ${lock.id} (${host})`)
70
83
  }
71
84
  return dir
72
- })()
73
- pending.catch(() => installs.delete(dir))
85
+ })().finally(() => {
86
+ if (installs.get(dir)?.controller === controller) installs.delete(dir)
87
+ })
88
+ pending = { promise, controller, clients: 0 }
74
89
  installs.set(dir, pending)
75
90
  }
76
- return pending
91
+ const current = pending
92
+ current.clients++
93
+ let released = false
94
+ const release = () => {
95
+ if (released) return
96
+ released = true
97
+ if (--current.clients === 0) current.controller.abort(signal?.reason)
98
+ }
99
+ let abort: (() => void) | undefined
100
+ try {
101
+ const root = await new Promise<string>((resolve, reject) => {
102
+ abort = () => {
103
+ release()
104
+ const reason: unknown = signal?.reason
105
+ const error =
106
+ reason instanceof Error
107
+ ? reason
108
+ : new Error('Swift distribution installation canceled', { cause: reason })
109
+ // other callers retain ownership; the last caller waits for cleanup.
110
+ if (current.clients > 0) reject(error)
111
+ else
112
+ current.promise.then(
113
+ () => reject(error),
114
+ () => reject(error),
115
+ )
116
+ }
117
+ signal?.addEventListener('abort', abort, { once: true })
118
+ current.promise.then(resolve, reject)
119
+ if (signal?.aborted) abort()
120
+ })
121
+ signal?.throwIfAborted()
122
+ return root
123
+ } finally {
124
+ if (abort) signal?.removeEventListener('abort', abort)
125
+ release()
126
+ }
77
127
  }
78
128
 
79
129
  /** the module this process compiles app Swift against: the checkout beside a
@@ -81,7 +131,9 @@ export function swiftDistributionRoot(): Promise<string | null> {
81
131
  * nothing without it, so a checkout's toolchain error surfaces to them. */
82
132
  export async function resolveSwiftModule({
83
133
  required = false,
84
- }: { required?: boolean } = {}): Promise<SwiftModule | null> {
134
+ signal,
135
+ }: { required?: boolean; signal?: AbortSignal } = {}): Promise<SwiftModule | null> {
136
+ signal?.throwIfAborted()
85
137
  const checkout = path.resolve(import.meta.dirname, '../../sootsim-swift')
86
138
  if (fs.existsSync(path.join(checkout, 'toolchain.json'))) {
87
139
  // a checkout is implicit: without its toolchain, apps run without their
@@ -93,7 +145,7 @@ export async function resolveSwiftModule({
93
145
  return null
94
146
  }
95
147
  }
96
- const root = await swiftDistributionRoot()
148
+ const root = await swiftDistributionRoot(signal)
97
149
  return root ? { root, ...swiftModuleToolchain(root) } : null
98
150
  }
99
151
 
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,8 +10,10 @@ 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' }
17
18
  import { swiftBinaryModules } from './swift-binary-modules'
18
19
  import {
@@ -39,18 +40,19 @@ import {
39
40
  } from './swift-package'
40
41
  import { swiftPackageManifest } from './swift-package-manifest'
41
42
  import {
42
- REMOTE_CHECKOUTS,
43
+ remoteSwiftCheckouts,
43
44
  resolveSwiftPackages,
44
45
  type SwiftPackageBinary,
45
46
  type SwiftPackageGap,
46
47
  type SwiftPackages,
47
48
  } from './swift-packages'
48
- import { readPackageFile } from './swift-source-file'
49
+ import { readPackageFile as readSwiftSourceFile } from './swift-source-file'
49
50
  import { readSwiftStandinBehavior } from './swift-standin-config'
50
51
  import {
51
52
  standinGenerationInputs,
52
53
  standinLibrary,
53
54
  swiftStandins,
55
+ SWIFT_STANDINS_DIR,
54
56
  type SwiftStandin,
55
57
  } from './swift-standins'
56
58
  import {
@@ -67,6 +69,7 @@ import {
67
69
  MAX_SWIFT_COMPILE_IMAGE_TOTAL_BYTES,
68
70
  isSwiftNativeModules,
69
71
  compileSwiftProject,
72
+ DEFAULT_SWIFT_COMPILE_BUILD_MS,
70
73
  httpSwiftCompileSubmit,
71
74
  swiftNativeEntry,
72
75
  type SwiftImageUpload,
@@ -172,13 +175,18 @@ export interface SwiftProject {
172
175
  packagePath: string
173
176
  artifact: SwiftArtifact | null
174
177
  // the build in flight and the sources it was started from
175
- building: { stamp: string; promise: Promise<SwiftArtifact> } | null
178
+ building: {
179
+ stamp: string
180
+ promise: Promise<SwiftArtifact>
181
+ signal?: AbortSignal
182
+ } | null
176
183
  // two bounded generations of immutable bytes cover in-flight asset reads.
177
184
  resourceFiles: Map<string, { bytes: Buffer; contentType: string }>
178
185
  resourcesPrevious: Map<string, { bytes: Buffer; contentType: string }>
179
186
  artifacts: SwiftArtifact[]
180
187
  hosted?: SwiftHostedCompileOptions
181
188
  signal?: AbortSignal
189
+ libraries?: Record<string, string>
182
190
  }
183
191
 
184
192
  // a project is its swiftpm package: the build, the artifact, and the dev
@@ -236,6 +244,8 @@ interface XcodeProjectEntry {
236
244
  packages?: {
237
245
  key: string
238
246
  standinKey: string
247
+ /** generation always uses the original closure, before stand-ins replace targets. */
248
+ binaries: SwiftPackageBinary[]
239
249
  packages: SwiftPackages
240
250
  standins: SwiftStandin[]
241
251
  /** why a framework the project vendors got no stand-in, by name. */
@@ -318,16 +328,18 @@ function swiftPackages(
318
328
  // emitter), so a changed input rebuilds instead of reusing stale output.
319
329
  let cached = entry.packages
320
330
  if (cached?.key !== key) {
331
+ const packages = resolveSwiftPackages({
332
+ rootDir: app.rootDir,
333
+ resolved: app.packageResolved,
334
+ products: app.linkedProducts,
335
+ localDirs: app.localPackageDirs,
336
+ provided: entry.provided,
337
+ })
321
338
  cached = {
322
339
  key,
323
340
  standinKey: '',
324
- packages: resolveSwiftPackages({
325
- rootDir: app.rootDir,
326
- resolved: app.packageResolved,
327
- products: app.linkedProducts,
328
- localDirs: app.localPackageDirs,
329
- provided: entry.provided,
330
- }),
341
+ packages,
342
+ binaries: packages.binaries,
331
343
  standins: [],
332
344
  projectGaps: new Map(),
333
345
  }
@@ -354,10 +366,7 @@ function swiftPackages(
354
366
  ]
355
367
  : [],
356
368
  )
357
- const reached = [
358
- ...cached.packages.binaries.filter((binary) => binary.reached),
359
- ...vendored,
360
- ]
369
+ const reached = [...cached.binaries.filter((binary) => binary.reached), ...vendored]
361
370
  if (entry.moduleRoot && entry.provided && reached.length > 0) {
362
371
  const resolved = swiftModuleToolchain(entry.moduleRoot)
363
372
  const toolchain = {
@@ -379,6 +388,8 @@ function swiftPackages(
379
388
  ]),
380
389
  binaryGaps: built.gaps,
381
390
  })
391
+ for (const target of new Set(built.standins.map((standin) => standin.target)))
392
+ console.error(`Peach: target ${target} was replaced with a stand-in`)
382
393
  cached.standins = built.standins
383
394
  cached.projectGaps = new Map(
384
395
  vendored.flatMap(({ target }) => {
@@ -402,17 +413,17 @@ const XCODE_FRAMEWORKS_DIR = '__frameworks__'
402
413
  // remote package checkouts compile here: swiftpm owns `.build` in the
403
414
  // compile's package root as its scratch directory.
404
415
  const XCODE_PACKAGES_DIR = '__packages__'
405
- // each binary target's stand-in compiles as a target under its module's
406
- // directory here, from sources generated on this host.
407
- const XCODE_STANDINS_DIR = '__standins__'
408
- const CHECKOUTS_PREFIX = `${REMOTE_CHECKOUTS.split(path.sep).join(path.posix.sep)}/`
409
416
 
410
417
  // the compile input key of a root-relative path in an xcode layout.
411
418
  function xcodeInputKey(layout: XcodeLayout, file: string, relative: string): string {
412
419
  const owner = layout.targetDirs.get(file)
413
420
  if (owner) return path.posix.join(owner, relative)
414
- return relative.startsWith(CHECKOUTS_PREFIX)
415
- ? path.posix.join(XCODE_PACKAGES_DIR, relative.slice(CHECKOUTS_PREFIX.length))
421
+ const checkouts = remoteSwiftCheckouts(layout.app.rootDir)
422
+ return file.startsWith(`${checkouts}${path.sep}`)
423
+ ? path.posix.join(
424
+ XCODE_PACKAGES_DIR,
425
+ path.relative(checkouts, file).split(path.sep).join('/'),
426
+ )
416
427
  : relative
417
428
  }
418
429
 
@@ -485,7 +496,8 @@ function xcodeLayout(root: string): XcodeLayout | null {
485
496
  for (const standin of standins)
486
497
  targets.push({
487
498
  name: standin.module,
488
- path: path.posix.join(XCODE_STANDINS_DIR, standin.module),
499
+ path: path.posix.join(SWIFT_STANDINS_DIR, standin.module),
500
+ dependencies: standin.dependencies?.map((name) => ({ kind: 'target', name })),
489
501
  executable: false,
490
502
  resources: false,
491
503
  exclude: [],
@@ -497,7 +509,7 @@ function xcodeLayout(root: string): XcodeLayout | null {
497
509
  const packageConfiguration = app.configuration === 'Debug' ? 'debug' : 'release'
498
510
  for (const target of packages.targets) {
499
511
  const relative = path.relative(
500
- target.remote ? path.join(root, REMOTE_CHECKOUTS) : root,
512
+ target.remote ? remoteSwiftCheckouts(root) : root,
501
513
  target.dir,
502
514
  )
503
515
  if (
@@ -521,6 +533,13 @@ function xcodeLayout(root: string): XcodeLayout | null {
521
533
  )
522
534
  .map((define) => define.name),
523
535
  ],
536
+ linkedLibraries: target.packageLibraries
537
+ .filter(
538
+ (library) =>
539
+ library.configuration === null ||
540
+ library.configuration === packageConfiguration,
541
+ )
542
+ .map((library) => library.name),
524
543
  })
525
544
  if (target.resources) {
526
545
  resourceBundles.push({
@@ -565,14 +584,19 @@ function xcodeManifest(layout: XcodeLayout): string {
565
584
  ...(target.mainActorIsolation ? ['.defaultIsolation(MainActor.self)'] : []),
566
585
  ...(target.defines ?? []).map((name) => `.define(${JSON.stringify(name)})`),
567
586
  ].join(', ')}]`
587
+ const libraries = (target: SwiftPackageTarget) =>
588
+ target.linkedLibraries?.length
589
+ ? `, linkerSettings: [${target.linkedLibraries.map((name) => `.linkedLibrary(${JSON.stringify(name)})`).join(', ')}]`
590
+ : ''
568
591
  return [
569
592
  '// swift-tools-version: 6.2',
570
593
  'import PackageDescription',
571
594
  'let package = Package(name: "app", targets: [',
572
595
  ` .executableTarget(name: "${layout.app.moduleName}", path: "${XCODE_APP_DIR}", ${settings(layout.app)}),`,
573
- ...layout.targets.map(
574
- (target) =>
575
- ` .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)}),`,
596
+ ...layout.targets.map((target) =>
597
+ target.systemLibrary
598
+ ? ` .systemLibrary(name: "${target.name}", path: "${target.path}"),`
599
+ : ` .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)}),`,
576
600
  ),
577
601
  '])',
578
602
  '',
@@ -587,6 +611,17 @@ function packageFiles(root: string): string[] {
587
611
  return xcode ? xcode.files : sourceTreeFiles(canonicalRoot, canonicalRoot)
588
612
  }
589
613
 
614
+ // remote dependencies are enumerated from peach's checkout cache, while app
615
+ // inputs retain their selected-project boundary and all symlink checks.
616
+ function readPackageFile(root: string, file: string, maxBytes: number): Buffer {
617
+ if (xcodeProjects.has(root)) {
618
+ const checkouts = remoteSwiftCheckouts(root)
619
+ if (file.startsWith(`${checkouts}${path.sep}`))
620
+ return readSwiftSourceFile(checkouts, file, maxBytes)
621
+ }
622
+ return readSwiftSourceFile(root, file, maxBytes)
623
+ }
624
+
590
625
  // every input file under `dir`, which must stay inside `canonicalRoot`.
591
626
  function sourceTreeFiles(dir: string, canonicalRoot: string): string[] {
592
627
  const found: string[] = []
@@ -980,6 +1015,13 @@ export function buildSwiftBundleFiles(
980
1015
  const disk = path.join(packagePath, ...file.split('/'))
981
1016
  const bytes = readPackageFile(packagePath, disk, limits.resourceBytes)
982
1017
  addFile(bundlePath === '.' ? base : `${bundlePath}/${base}`, bytes)
1018
+ if (base.endsWith('.xcstrings')) {
1019
+ for (const [name, table] of Object.entries(
1020
+ compileStringCatalog(bytes.toString('utf8'), base.slice(0, -10)),
1021
+ )) {
1022
+ addFile(bundlePath === '.' ? name : `${bundlePath}/${name}`, Buffer.from(table))
1023
+ }
1024
+ }
983
1025
  }
984
1026
  }
985
1027
  return { files, bundles, watched }
@@ -1033,6 +1075,8 @@ export function sourceStamp(
1033
1075
  const hash = createHash('sha256')
1034
1076
  hash.update(packagePath)
1035
1077
  hash.update(JSON.stringify(xcodeProjects.get(packagePath)?.behavior ?? {}))
1078
+ const libraries = projects.get(packagePath)?.libraries
1079
+ if (libraries) hash.update(JSON.stringify(libraries))
1036
1080
  const projectInputs = new Set(packageFiles(packagePath))
1037
1081
  for (const file of [...projectInputs].sort()) {
1038
1082
  hash.update(path.relative(packagePath, file))
@@ -1043,6 +1087,24 @@ export function sourceStamp(
1043
1087
  return hash.digest('hex').slice(0, 16)
1044
1088
  }
1045
1089
 
1090
+ // sourceStamp's inputs read by content and relative to the package, so
1091
+ // another process's build copy of the same sources stamps the same.
1092
+ export function sourceContentStamp(
1093
+ packagePath: string,
1094
+ moduleRootOverride?: string,
1095
+ ): string {
1096
+ const hash = createHash('sha256')
1097
+ hash.update(JSON.stringify(xcodeProjects.get(packagePath)?.behavior ?? {}))
1098
+ const libraries = projects.get(packagePath)?.libraries
1099
+ if (libraries) hash.update(JSON.stringify(libraries))
1100
+ for (const file of [...new Set(packageFiles(packagePath))].sort()) {
1101
+ hash.update(path.relative(packagePath, file))
1102
+ hash.update(fs.readFileSync(file))
1103
+ }
1104
+ hash.update(moduleStamp(packagePath, moduleRootOverride, false))
1105
+ return hash.digest('hex').slice(0, 16)
1106
+ }
1107
+
1046
1108
  // the linked module's part of the source stamp
1047
1109
  function moduleStamp(
1048
1110
  packagePath: string,
@@ -1092,15 +1154,19 @@ export function icuUrl(origin: string, artifact: SwiftArtifact): string {
1092
1154
  // keyed by the stand-in target it links into; its own source then defines
1093
1155
  // nothing, so the library's functions run instead of the traps. one library
1094
1156
  // links once, through the first module its framework ships.
1095
- function standinLibraries(layout: XcodeLayout): Map<string, SwiftStandin[]> {
1157
+ export function standinLibraries(
1158
+ binaries: readonly SwiftPackageBinary[],
1159
+ standins: readonly SwiftStandin[],
1160
+ rootDir: string,
1161
+ ): Map<string, SwiftStandin[]> {
1096
1162
  const linked = new Map<string, SwiftStandin[]>()
1097
- for (const binary of layout.binaries) {
1098
- const modules = layout.standins.filter(
1163
+ for (const binary of binaries) {
1164
+ const modules = standins.filter(
1099
1165
  (standin) =>
1100
1166
  standin.target === binary.target && `${standin.module}.c` in standin.files,
1101
1167
  )
1102
1168
  if (modules.length === 0) continue
1103
- const library = standinLibrary(binary, layout.app.rootDir)
1169
+ const library = standinLibrary(binary, rootDir)
1104
1170
  if (!library) continue
1105
1171
  linked.set(library, modules)
1106
1172
  }
@@ -1131,7 +1197,7 @@ function packageSources(
1131
1197
  files['Package.swift'] = xcodeManifest(xcode)
1132
1198
  for (const standin of xcode.standins)
1133
1199
  for (const [file, text] of Object.entries(standin.files))
1134
- files[path.posix.join(XCODE_STANDINS_DIR, standin.module, file)] =
1200
+ files[path.posix.join(SWIFT_STANDINS_DIR, standin.module, file)] =
1135
1201
  linked.has(standin) && file === `${standin.module}.c`
1136
1202
  ? `// ${standin.target}'s WebAssembly library defines ${standin.module}'s functions.\n`
1137
1203
  : text
@@ -1152,7 +1218,9 @@ interface SwiftPackageSnapshot {
1152
1218
 
1153
1219
  function packageSnapshot(project: SwiftProject, entry: string): SwiftPackageSnapshot {
1154
1220
  const layout = xcodeLayout(project.packagePath)
1155
- const linked = layout ? standinLibraries(layout) : new Map<string, SwiftStandin[]>()
1221
+ const linked = layout
1222
+ ? standinLibraries(layout.binaries, layout.standins, layout.app.rootDir)
1223
+ : new Map<string, SwiftStandin[]>()
1156
1224
  const files = packageSources(project.packagePath, new Set([...linked.values()].flat()))
1157
1225
  const native = nativeSourceDescriptor(project.packagePath)
1158
1226
  return {
@@ -1166,9 +1234,7 @@ function packageSnapshot(project: SwiftProject, entry: string): SwiftPackageSnap
1166
1234
  .relative(project.packagePath, file)
1167
1235
  .split(path.sep)
1168
1236
  .join(path.posix.sep)
1169
- return layout && file.endsWith('.metal')
1170
- ? xcodeInputKey(layout, file, relative)
1171
- : relative
1237
+ return layout ? xcodeInputKey(layout, file, relative) : relative
1172
1238
  }),
1173
1239
  ),
1174
1240
  resources: buildSwiftResourceTable(project, '', files),
@@ -1178,14 +1244,17 @@ function packageSnapshot(project: SwiftProject, entry: string): SwiftPackageSnap
1178
1244
  files,
1179
1245
  ),
1180
1246
  native,
1181
- ...(linked.size > 0
1247
+ ...(linked.size > 0 || project.libraries
1182
1248
  ? {
1183
- libraries: Object.fromEntries(
1184
- [...linked].map(([library, [first]]) => [
1185
- first!.module,
1186
- fs.readFileSync(library).toString('base64'),
1187
- ]),
1188
- ),
1249
+ libraries: {
1250
+ ...project.libraries,
1251
+ ...Object.fromEntries(
1252
+ [...linked].map(([library, [first]]) => [
1253
+ first!.module,
1254
+ fs.readFileSync(library).toString('base64'),
1255
+ ]),
1256
+ ),
1257
+ },
1189
1258
  }
1190
1259
  : {}),
1191
1260
  ...(native
@@ -1252,7 +1321,7 @@ async function compile(
1252
1321
  linkedModuleRoots(packagePath).find((root) =>
1253
1322
  fs.existsSync(path.join(root, 'toolchain.json')),
1254
1323
  ) ??
1255
- (await swiftDistributionRoot())
1324
+ (await swiftDistributionRoot(signal))
1256
1325
  if (!moduleRoot)
1257
1326
  return {
1258
1327
  ...(await compileRemote(packagePath, snapshot, undefined, signal)),
@@ -1290,13 +1359,20 @@ async function compile(
1290
1359
  if (process.env.CONTRAST_SWIFT_BUILD_ROOT)
1291
1360
  return process.env.CONTRAST_SWIFT_BUILD_ROOT
1292
1361
  const resolvedPath = path.resolve(packagePath)
1362
+ // units of one app share a root so the module cache stays warm.
1363
+ // peach-expo-swift/<app>/<scope>/<package> shares <app>/<scope>, and
1364
+ // the older <app>/<package> layout shares <app>. the scope is this
1365
+ // process, so two servers do not compile into one tree.
1366
+ const parts = resolvedPath.split(path.sep)
1367
+ const expoAt = parts.lastIndexOf('peach-expo-swift')
1368
+ const scoped = expoAt >= 0 && parts.length >= expoAt + 4
1293
1369
  const buildKeyPath =
1294
- path.basename(path.dirname(path.dirname(resolvedPath))) === 'peach-expo-swift'
1295
- ? path.dirname(resolvedPath)
1370
+ expoAt >= 0 && parts.length >= expoAt + 3
1371
+ ? parts.slice(0, scoped ? expoAt + 3 : expoAt + 2).join(path.sep)
1296
1372
  : resolvedPath
1297
1373
  return path.join(
1298
- os.tmpdir(),
1299
- 'contrast-swift-build',
1374
+ cacheDir(),
1375
+ 'swift-builds',
1300
1376
  createHash('sha256')
1301
1377
  .update(`${moduleRoot}\0${buildKeyPath}`)
1302
1378
  .digest('hex')
@@ -1482,7 +1558,7 @@ async function compileRemote(
1482
1558
  hosted?.authorization ?? (auth ? authHeaderValue(auth) : undefined)
1483
1559
  const signal = AbortSignal.any([
1484
1560
  ...(ownerSignal ? [ownerSignal] : []),
1485
- AbortSignal.timeout(5 * 60_000),
1561
+ AbortSignal.timeout(DEFAULT_SWIFT_COMPILE_BUILD_MS + 60_000),
1486
1562
  ])
1487
1563
  const submit = httpSwiftCompileSubmit({
1488
1564
  endpoint: origin,
@@ -1559,8 +1635,11 @@ export function artifactFor(
1559
1635
  project: SwiftProject,
1560
1636
  entry: string,
1561
1637
  moduleRoot?: string,
1638
+ signal: AbortSignal | undefined = project.signal,
1562
1639
  ): Promise<SwiftArtifact> {
1563
- project.signal?.throwIfAborted()
1640
+ if (project.signal && signal !== project.signal)
1641
+ signal = signal ? AbortSignal.any([project.signal, signal]) : project.signal
1642
+ signal?.throwIfAborted()
1564
1643
  const stamp = sourceStamp(project.packagePath, moduleRoot, Boolean(project.hosted))
1565
1644
  if (project.artifact?.stamp === stamp) return Promise.resolve(project.artifact)
1566
1645
  if (project.building) {
@@ -1569,13 +1648,27 @@ export function artifactFor(
1569
1648
  // and ask again, which starts the build these sources need; joining it
1570
1649
  // would emit the older artifact and nothing would trigger another load.
1571
1650
  const running = project.building
1572
- if (running.stamp === stamp) return running.promise
1573
- const again = () => artifactFor(project, entry, moduleRoot)
1574
- return running.promise.then(again, again)
1651
+ if (running.stamp === stamp && running.signal === signal) return running.promise
1652
+ const again = () => artifactFor(project, entry, moduleRoot, signal)
1653
+ const queued = running.promise.then(again, again)
1654
+ if (!signal) return queued
1655
+ return new Promise<SwiftArtifact>((resolve, reject) => {
1656
+ const stop = () =>
1657
+ reject(
1658
+ signal.reason instanceof Error
1659
+ ? signal.reason
1660
+ : new Error('Swift compilation canceled', { cause: signal.reason }),
1661
+ )
1662
+ signal.addEventListener('abort', stop, { once: true })
1663
+ if (signal.aborted) stop()
1664
+ queued
1665
+ .then(resolve, reject)
1666
+ .finally(() => signal.removeEventListener('abort', stop))
1667
+ })
1575
1668
  }
1576
1669
  const snapshot = packageSnapshot(project, entry)
1577
1670
  if (sourceStamp(project.packagePath, moduleRoot, Boolean(project.hosted)) !== stamp) {
1578
- return artifactFor(project, entry, moduleRoot)
1671
+ return artifactFor(project, entry, moduleRoot, signal)
1579
1672
  }
1580
1673
  // a save that changed only view builder literals keeps the running bytes
1581
1674
  const compiled = project.artifact?.compiled
@@ -1600,7 +1693,7 @@ export function artifactFor(
1600
1693
  moduleRoot,
1601
1694
  project.hosted,
1602
1695
  snapshot,
1603
- project.signal,
1696
+ signal,
1604
1697
  ).then(
1605
1698
  (artifact) => {
1606
1699
  project.artifact = artifact
@@ -1620,9 +1713,9 @@ export function artifactFor(
1620
1713
  sourceStamp(project.packagePath, moduleRoot, Boolean(project.hosted)) ===
1621
1714
  artifact.stamp
1622
1715
  ? artifact
1623
- : artifactFor(project, entry, moduleRoot),
1716
+ : artifactFor(project, entry, moduleRoot, signal),
1624
1717
  )
1625
- project.building = { stamp, promise: fresh }
1718
+ project.building = { stamp, promise: fresh, signal }
1626
1719
  return fresh
1627
1720
  }
1628
1721