peachsim 0.1.1635 → 0.1.1641

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +10 -1
  2. package/cli/app-config.ts +14 -5
  3. package/cli/app-stubs.ts +96 -22
  4. package/cli/bridge-flow-runner.ts +2 -2
  5. package/cli/cloud-client.ts +60 -40
  6. package/cli/cloud-session.ts +16 -0
  7. package/cli/commands/assert.ts +17 -4
  8. package/cli/commands/box.ts +33 -19
  9. package/cli/commands/doctor.ts +37 -20
  10. package/cli/commands/flow.ts +1 -1
  11. package/cli/commands/inspect/actions.ts +3 -2
  12. package/cli/commands/inspect/core.ts +0 -8
  13. package/cli/commands/inspect/shared.ts +9 -4
  14. package/cli/commands/inspect.ts +65 -61
  15. package/cli/commands/platform.ts +2 -1
  16. package/cli/commands/preview.ts +1 -1
  17. package/cli/commands/record.ts +154 -68
  18. package/cli/commands/swift.ts +18 -4
  19. package/cli/commands/three-mode.ts +4 -38
  20. package/cli/commands/upload.ts +55 -49
  21. package/cli/drivers/playwright-sim-host.ts +65 -42
  22. package/cli/flow-live-status.ts +0 -2
  23. package/cli/main.ts +2 -2
  24. package/cli/outbound-endpoints.ts +7 -5
  25. package/cli/pause-aware-timeout.ts +4 -0
  26. package/cli/ws-bridge.ts +19 -16
  27. package/detox/attempt-events.cjs +99 -6
  28. package/detox/attempt-events.d.cts +7 -0
  29. package/detox/expectations.ts +7 -3
  30. package/detox/index.ts +259 -139
  31. package/detox/jest-environment.cjs +5 -1
  32. package/detox/motion-change.cjs +170 -21
  33. package/detox/motion-change.d.cts +32 -0
  34. package/detox/navigation-lifecycle.ts +35 -3
  35. package/dist-lib/agent-daemon-client.cjs +1 -1
  36. package/dist-lib/agent-events.cjs +1 -1
  37. package/dist-lib/attached-projects.cjs +1 -1
  38. package/dist-lib/auth/shared-session.cjs +1 -1
  39. package/dist-lib/backend-origin.cjs +17 -7
  40. package/dist-lib/beta.cjs +1 -1
  41. package/dist-lib/beta.mjs +1 -1
  42. package/dist-lib/bridge-constants.cjs +1 -1
  43. package/dist-lib/bridge-contract-input.cjs +1 -1
  44. package/dist-lib/bridge-contract-input.mjs +1 -1
  45. package/dist-lib/bridge-contract.cjs +1 -1
  46. package/dist-lib/bridge-contract.mjs +1 -1
  47. package/dist-lib/bundle-channel.cjs +5014 -2268
  48. package/dist-lib/capture-contract.cjs +1 -1
  49. package/dist-lib/capture-contract.mjs +1 -1
  50. package/dist-lib/cloud-contract.cjs +1 -1
  51. package/dist-lib/cloud-contract.mjs +1 -1
  52. package/dist-lib/cloud-test-args.cjs +1 -1
  53. package/dist-lib/cloud-test-args.mjs +1 -1
  54. package/dist-lib/cloud.cjs +1 -1
  55. package/dist-lib/cloud.mjs +1 -1
  56. package/dist-lib/config.cjs +1 -1
  57. package/dist-lib/config.mjs +1 -1
  58. package/dist-lib/detox/index.cjs +1036 -166
  59. package/dist-lib/detox/jest-environment.cjs +5 -1
  60. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  61. package/dist-lib/expo-swift-bundle.cjs +4613 -1963
  62. package/dist-lib/expo-swift-metadata.cjs +9 -7
  63. package/dist-lib/expo-swift-metadata.mjs +9 -7
  64. package/dist-lib/home-paths.cjs +1 -1
  65. package/dist-lib/host/bridge-host.cjs +58072 -16400
  66. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  67. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  68. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  69. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  70. package/dist-lib/host/websocket-proxy.cjs +1 -1
  71. package/dist-lib/index.cjs +2790 -143
  72. package/dist-lib/jump-to-source-babel.cjs +1 -1
  73. package/dist-lib/jump-to-source-native.cjs +1 -1
  74. package/dist-lib/kotlin-host.cjs +1 -1
  75. package/dist-lib/menu.cjs +1 -1
  76. package/dist-lib/menu.mjs +1 -1
  77. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  78. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  79. package/dist-lib/metro-production-bundle.cjs +1 -1
  80. package/dist-lib/metro-production-bundle.mjs +1 -1
  81. package/dist-lib/metro.cjs +2579 -95
  82. package/dist-lib/native-app-identity-project.cjs +243 -53
  83. package/dist-lib/native-app-identity-project.mjs +243 -53
  84. package/dist-lib/native-app-identity.cjs +29 -30
  85. package/dist-lib/native-app-identity.mjs +29 -30
  86. package/dist-lib/native-firebase-metadata.cjs +1 -1
  87. package/dist-lib/native-firebase-metadata.mjs +1 -1
  88. package/dist-lib/plist.cjs +1 -1
  89. package/dist-lib/plist.mjs +1 -1
  90. package/dist-lib/profiles.cjs +1 -1
  91. package/dist-lib/public-brand.cjs +1 -1
  92. package/dist-lib/react-native-host-modules.cjs +2 -2
  93. package/dist-lib/react-native-host-modules.mjs +2 -2
  94. package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
  95. package/dist-lib/sdk.cjs +4 -3
  96. package/dist-lib/sdk.mjs +4 -3
  97. package/dist-lib/skills.cjs +66475 -23097
  98. package/dist-lib/swift-compile.cjs +452 -260
  99. package/dist-lib/swift-compile.mjs +451 -252
  100. package/dist-lib/swift-package.cjs +1 -1
  101. package/dist-lib/swift-package.mjs +1 -1
  102. package/dist-lib/swift-submit.cjs +14 -7
  103. package/dist-lib/swift-submit.mjs +14 -7
  104. package/dist-lib/swift-toolchain.cjs +316 -41
  105. package/dist-lib/swift.cjs +1 -1
  106. package/dist-lib/vite-swift.cjs +8188 -5636
  107. package/dist-lib/vite.cjs +2857 -12
  108. package/dist-lib/worklet-executable.cjs +14 -2
  109. package/dist-lib/worklet-executable.mjs +14 -2
  110. package/package.json +1 -1
  111. package/src/backend-origin.ts +21 -6
  112. package/src/expo-swift-host.ts +5 -2
  113. package/src/expoKotlinContract.ts +4 -7
  114. package/src/expoSwiftContract.ts +4 -6
  115. package/src/host/staged-host-files.ts +9 -0
  116. package/src/native-app-identity-project.ts +1 -4
  117. package/src/native-app-identity.ts +1 -4
  118. package/src/nativeAppIconContract.ts +1 -4
  119. package/src/output-lock-stdio.ts +36 -0
  120. package/src/react-native-bridge-modules.ts +106 -78
  121. package/src/react-native-host-modules.ts +4 -1
  122. package/src/settings-bundle.ts +1 -4
  123. package/src/swift-compile.ts +224 -139
  124. package/src/swift-distribution.lock.json +23 -12
  125. package/src/swift-distribution.ts +103 -4
  126. package/src/swift-host.ts +95 -30
  127. package/src/swift-icu.ts +16 -10
  128. package/src/swift-objc-compile.ts +547 -0
  129. package/src/swift-objc-metadata.ts +204 -0
  130. package/src/swift-objc-sdk.ts +883 -19
  131. package/src/swift-objc-standin.ts +126 -14
  132. package/src/swift-package-manifest.ts +42 -1
  133. package/src/swift-packages.ts +35 -21
  134. package/src/swift-root-lock.ts +153 -0
  135. package/src/swift-standin-emit.ts +40 -7
  136. package/src/swift-standins.ts +404 -138
  137. package/src/swift-submit.ts +18 -7
  138. package/src/swift-toolchain.ts +378 -46
  139. package/src/swiftContract.ts +12 -0
  140. package/src/worker-system-chunks.ts +11 -12
  141. package/src/worklet-executable.ts +19 -1
  142. package/src/xcode-project.ts +280 -33
@@ -13,11 +13,12 @@
13
13
  // headers import; the stand-in's own typecheck then leaves out whatever peach
14
14
  // cannot spell.
15
15
 
16
- import { spawnSync } from 'node:child_process'
16
+ import { execFileSync, spawnSync } from 'node:child_process'
17
17
  import { createHash } from 'node:crypto'
18
18
  import fs from 'node:fs'
19
19
  import path from 'node:path'
20
20
  import { IOS_SDK_HEADERS, IOS_SDK_OVERLAYS, IOS_SDK_TARGET } from './swift-objc-sdk'
21
+ import { swiftEnvironment } from './swift-toolchain'
21
22
  import type { SymbolGraph } from './swift-standin-emit'
22
23
 
23
24
  /** a module map and the headers it reaches: a framework's, or a library
@@ -37,6 +38,100 @@ export interface ObjcView {
37
38
  imports: string[]
38
39
  }
39
40
 
41
+ /** bump when importer behavior changes the Swift view independently of SDK headers. */
42
+ export const OBJC_IMPORT_VERSION = '3'
43
+
44
+ export const NATIVE_GRAPH_SDK_VERSION = '27.1'
45
+
46
+ const NATIVE_GRAPH_INPUTS = {
47
+ sdk: NATIVE_GRAPH_SDK_VERSION,
48
+ settings: 'e68503e5e534e9194af5efe8c636ca8b824e2d8237c5f38ec84aa4d155bc8418',
49
+ build: 'ff785237ba22f1d8f347a5c4f4c68400a387c6d935ef3a443545a72faef57e70',
50
+ compiler: 'Apple Swift version 6.4 (swiftlang-6.4.0.34.1 clang-2100.3.34.1)',
51
+ target: IOS_SDK_TARGET,
52
+ }
53
+ export const NATIVE_GRAPH_ID = createHash('sha256')
54
+ .update(JSON.stringify(NATIVE_GRAPH_INPUTS))
55
+ .digest('hex')
56
+
57
+ /** a mixed framework's swift overlay and underlying clang module load together. */
58
+ export function nativeInterfaceView(
59
+ surface: ObjcSurface,
60
+ frameworks: readonly string[],
61
+ work: string,
62
+ ): ObjcView {
63
+ if (process.platform !== 'darwin')
64
+ throw new Error('mixed framework declarations require the pinned native iOS SDK')
65
+ const xcrun = (args: string[]) =>
66
+ execFileSync('xcrun', args, {
67
+ encoding: 'utf8',
68
+ stdio: ['ignore', 'pipe', 'pipe'],
69
+ }).trim()
70
+ const sdk = xcrun(['--sdk', 'iphonesimulator', '--show-sdk-path'])
71
+ const compiler = xcrun(['--find', 'swiftc'])
72
+ const version = execFileSync(compiler, ['--version'], { encoding: 'utf8' })
73
+ .trim()
74
+ .split('\n')[0]
75
+ const digest = (file: string) =>
76
+ createHash('sha256')
77
+ .update(fs.readFileSync(path.join(sdk, file)))
78
+ .digest('hex')
79
+ const actual = {
80
+ sdk: xcrun(['--sdk', 'iphonesimulator', '--show-sdk-version']),
81
+ compiler: version,
82
+ settings: digest('SDKSettings.json'),
83
+ build: digest('System/Library/CoreServices/SystemVersion.plist'),
84
+ }
85
+ const mismatches = Object.entries(actual)
86
+ .filter(([key, value]) => Reflect.get(NATIVE_GRAPH_INPUTS, key) !== value)
87
+ .map(
88
+ ([key, value]) =>
89
+ `${key}: expected ${Reflect.get(NATIVE_GRAPH_INPUTS, key)}, got ${value}`,
90
+ )
91
+ if (mismatches.length)
92
+ throw new Error(
93
+ `native declaration SDK or compiler differs from ${NATIVE_GRAPH_ID}: ${mismatches.join('; ')}`,
94
+ )
95
+ const toolchainBin = path.dirname(compiler)
96
+ const search = ['-sdk', sdk, ...frameworks.flatMap((dir) => ['-F', dir])]
97
+ const out = path.join(work, 'native-graphs')
98
+ fs.mkdirSync(out, { recursive: true })
99
+ const run = spawnSync(
100
+ path.join(toolchainBin, 'swift-symbolgraph-extract'),
101
+ [
102
+ '-module-name',
103
+ surface.module,
104
+ '-target',
105
+ IOS_SDK_TARGET,
106
+ ...search,
107
+ '-minimum-access-level',
108
+ 'public',
109
+ '-skip-synthesized-members',
110
+ '-omit-extension-block-symbols',
111
+ '-output-dir',
112
+ out,
113
+ ],
114
+ { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 },
115
+ )
116
+ if (run.error) throw run.error
117
+ if (run.status !== 0)
118
+ throw new Error(
119
+ `native interface extraction ${run.signal ?? `exit ${run.status}`}: ${run.stderr.trim().slice(-4000)}`,
120
+ )
121
+ const graphs = fs
122
+ .readdirSync(out)
123
+ .filter((file) => file.endsWith('.symbols.json'))
124
+ .sort()
125
+ .map((file): SymbolGraph => JSON.parse(fs.readFileSync(path.join(out, file), 'utf8')))
126
+ if (graphs.length === 0)
127
+ throw new Error('native interface extraction returned no graphs')
128
+ return {
129
+ graphs,
130
+ values: enumValues(graphs, surface, true, toolchainBin, search, work),
131
+ imports: [],
132
+ }
133
+ }
134
+
40
135
  // the opaque declarations a failed import asks for, by name.
41
136
  type Missing = Map<string, 'class' | 'protocol' | 'struct' | 'macro'>
42
137
 
@@ -63,7 +158,7 @@ export const IOS_SDK_ID = createHash('sha256')
63
158
 
64
159
  // the sdk, written once per content under the cache directory so the module
65
160
  // cache keyed by its paths stays warm across stand-ins.
66
- function iosSdk(cacheDir: string): string {
161
+ export function materializeObjcSdk(cacheDir: string): string {
67
162
  const sdk = path.join(cacheDir, 'ios-sdk', IOS_SDK_ID)
68
163
  if (fs.existsSync(sdk)) return sdk
69
164
  fs.mkdirSync(path.dirname(sdk), { recursive: true })
@@ -116,7 +211,7 @@ export function objcView(
116
211
  throw new Error(
117
212
  `this peach's Swift has no iOS simulator standard library at ${stdlib}`,
118
213
  )
119
- const sdk = iosSdk(cacheDir)
214
+ const sdk = materializeObjcSdk(cacheDir)
120
215
  // a framework's module map sits in Modules/ beside Headers/; a library
121
216
  // slice keeps it among its headers.
122
217
  const framework = path.basename(path.dirname(surface.modulemap)) === 'Modules'
@@ -127,13 +222,13 @@ export function objcView(
127
222
  : path.join(include, surface.module)
128
223
  const originals = new Map(
129
224
  headerFiles(surface.headers).map((file) => [
130
- file,
225
+ path.join(headersDir, file),
131
226
  fs.readFileSync(path.join(surface.headers, file), 'utf8'),
132
227
  ]),
133
228
  )
134
229
  for (const [file, text] of originals) {
135
- fs.mkdirSync(path.dirname(path.join(headersDir, file)), { recursive: true })
136
- fs.writeFileSync(path.join(headersDir, file), text)
230
+ fs.mkdirSync(path.dirname(file), { recursive: true })
231
+ fs.writeFileSync(file, text)
137
232
  }
138
233
  if (framework) {
139
234
  fs.mkdirSync(path.join(frameworks, `${surface.module}.framework`, 'Modules'), {
@@ -146,12 +241,18 @@ export function objcView(
146
241
  } else {
147
242
  fs.copyFileSync(surface.modulemap, path.join(headersDir, 'module.modulemap'))
148
243
  }
244
+ // dependency headers take the same learned declarations but never become
245
+ // the surface's own imports.
246
+ const dependencyOriginals = new Map<string, string>()
149
247
  for (const dependency of surface.dependencies ?? []) {
150
248
  const dir = path.join(include, dependency.module)
151
249
  fs.mkdirSync(dir, { recursive: true })
152
250
  for (const file of headerFiles(dependency.headers)) {
153
- fs.mkdirSync(path.dirname(path.join(dir, file)), { recursive: true })
154
- fs.copyFileSync(path.join(dependency.headers, file), path.join(dir, file))
251
+ const destination = path.join(dir, file)
252
+ const text = fs.readFileSync(path.join(dependency.headers, file), 'utf8')
253
+ dependencyOriginals.set(destination, text)
254
+ fs.mkdirSync(path.dirname(destination), { recursive: true })
255
+ fs.writeFileSync(destination, text)
155
256
  }
156
257
  const supplied = path.join(dependency.headers, 'module.modulemap')
157
258
  fs.writeFileSync(
@@ -168,6 +269,13 @@ export function objcView(
168
269
  const search = [
169
270
  '-sdk',
170
271
  sdk,
272
+ // keep compiler support headers without importing host platform modules.
273
+ '-resource-dir',
274
+ path.join(sdk, 'usr/lib/swift'),
275
+ '-I',
276
+ path.resolve(toolchainBin, '../lib/swift/clang/include'),
277
+ '-I',
278
+ path.resolve(toolchainBin, '../lib/swift/shims'),
171
279
  '-I',
172
280
  stdlib,
173
281
  '-F',
@@ -237,8 +345,8 @@ export function objcView(
237
345
  path.join(root, 'Modules', 'module.modulemap'),
238
346
  'framework module PeachMissing {\n umbrella header "PeachMissing.h"\n export *\n}\n',
239
347
  )
240
- for (const [file, text] of originals)
241
- fs.writeFileSync(path.join(headersDir, file), `${MISSING_IMPORT}${text}`)
348
+ for (const [file, text] of [...originals, ...dependencyOriginals])
349
+ fs.writeFileSync(file, `${MISSING_IMPORT}${text}`)
242
350
  }
243
351
  // the identifier a diagnostic points at, read from the header as compiled.
244
352
  const tokenAt = (file: string, line: number, column: number) => {
@@ -276,11 +384,11 @@ export function objcView(
276
384
  '-output-dir',
277
385
  out,
278
386
  ],
279
- { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 },
387
+ { encoding: 'utf8', env: swiftEnvironment(), maxBuffer: 256 * 1024 * 1024 },
280
388
  )
281
389
  if (run.error) throw run.error
282
390
  const errors = [
283
- ...run.stderr.matchAll(/^(?:(\S+?):(\d+):(\d+): )?(?:fatal )?error: (.*)$/gm),
391
+ ...run.stderr.matchAll(/^(?:(.+?):(\d+):(\d+): )?(?:fatal )?error: (.*)$/gm),
284
392
  ]
285
393
  if (errors.length === 0 && run.status === 0) break
286
394
  let progress = false
@@ -322,7 +430,11 @@ export function objcView(
322
430
  }
323
431
  if (!progress || round === ROUNDS)
324
432
  throw new Error(
325
- `its headers do not import as Swift: ${[...new Set(errors.map((match) => match[4]!))].slice(0, 10).join('; ')}`,
433
+ `its headers do not import as Swift: ${
434
+ errors.length > 0
435
+ ? [...new Set(errors.map((match) => match[4]!))].slice(0, 10).join('; ')
436
+ : `swift-symbolgraph-extract ${run.signal ?? `exit ${run.status}`}: ${(run.stderr.trim() || run.stdout.trim() || 'no diagnostic').slice(0, 4000)}`
437
+ }`,
326
438
  )
327
439
  if (missing.size > 0) declareMissing()
328
440
  }
@@ -405,7 +517,7 @@ function enumValues(
405
517
  '-ast-dump=json',
406
518
  probe,
407
519
  ],
408
- { encoding: 'utf8', maxBuffer: 1024 * 1024 * 1024 },
520
+ { encoding: 'utf8', env: swiftEnvironment(), maxBuffer: 1024 * 1024 * 1024 },
409
521
  )
410
522
  if (run.status !== 0)
411
523
  throw new Error(
@@ -11,7 +11,7 @@ import fs from 'node:fs'
11
11
  import path from 'node:path'
12
12
  import { cacheDir as peachCacheDir, swiftPackageCacheOptions } from './home-paths'
13
13
  import lock from './swift-distribution.lock.json' with { type: 'json' }
14
- import { swiftToolchainBin } from './swift-toolchain'
14
+ import { swiftEnvironment, swiftToolchainBin } from './swift-toolchain'
15
15
  import type { SwiftTargetDependency } from './swift-package'
16
16
 
17
17
  const TARGET_TYPES = [
@@ -49,6 +49,11 @@ export interface SwiftManifestTarget {
49
49
  mainActorIsolation: boolean
50
50
  defines: SwiftManifestSetting[]
51
51
  libraries: SwiftManifestSetting[]
52
+ nativeSettings: {
53
+ tool: 'c' | 'cxx'
54
+ flags: string[]
55
+ configuration: 'debug' | 'release' | null
56
+ }[]
52
57
  /** true when the target declares `resources:`. */
53
58
  declaresResources: boolean
54
59
  /** public header directory relative to the target, as SwiftPM declares it. */
@@ -121,6 +126,7 @@ function dump(dir: string): { key: string; raw: Record<string, Json> } {
121
126
  ],
122
127
  {
123
128
  encoding: 'utf8',
129
+ env: swiftEnvironment(),
124
130
  stdio: ['ignore', 'pipe', 'pipe'],
125
131
  maxBuffer: 64 * 1024 * 1024,
126
132
  },
@@ -255,9 +261,43 @@ export function swiftPackageManifest(dir: string): SwiftPackageManifest {
255
261
  let mainActorIsolation = false
256
262
  const defines: SwiftManifestSetting[] = []
257
263
  const libraries: SwiftManifestSetting[] = []
264
+ const nativeSettings: SwiftManifestTarget['nativeSettings'] = []
265
+ for (const [tool, standard] of [
266
+ ['c', string(raw.cLanguageStandard)],
267
+ ['cxx', string(raw.cxxLanguageStandard)],
268
+ ] satisfies ['c' | 'cxx', string | undefined][]) {
269
+ if (standard)
270
+ nativeSettings.push({ tool, flags: [`-std=${standard}`], configuration: null })
271
+ }
258
272
  for (const setting of array(target.settings)) {
259
273
  const { kind: rawKind, tool, condition } = record(setting)
260
274
  const settingKind = record(rawKind)
275
+ if ((tool === 'c' || tool === 'cxx') && linksOnIOS(condition, traits)) {
276
+ const config = string(record(condition).config)
277
+ const header = string(record(settingKind.headerSearchPath)._0)
278
+ const definition = string(record(settingKind.define)._0)
279
+ const value = string(record(settingKind.define)._1)
280
+ const disabledWarning = string(record(settingKind.disableWarning)._0)
281
+ const unsafe = record(settingKind.unsafeFlags)._0
282
+ const flags = header
283
+ ? ['-I', path.resolve(targetDir, header)]
284
+ : definition
285
+ ? [`-D${definition}${value === undefined ? '' : `=${value}`}`]
286
+ : disabledWarning !== undefined
287
+ ? [`-Wno-${disabledWarning}`]
288
+ : Array.isArray(unsafe)
289
+ ? unsafe.flatMap((flag) => string(flag) ?? [])
290
+ : null
291
+ if (!flags)
292
+ throw new Error(
293
+ `${name}: unsupported ${tool} compiler setting ${JSON.stringify(rawKind)}`,
294
+ )
295
+ nativeSettings.push({
296
+ tool,
297
+ flags,
298
+ configuration: config === 'debug' || config === 'release' ? config : null,
299
+ })
300
+ }
261
301
  const mode = string(record(settingKind.swiftLanguageMode)._0)
262
302
  if (mode)
263
303
  languageMode = Math.max(5, Number(/^(\d+)/.exec(mode)?.[1] ?? languageMode))
@@ -299,6 +339,7 @@ export function swiftPackageManifest(dir: string): SwiftPackageManifest {
299
339
  mainActorIsolation,
300
340
  defines,
301
341
  libraries,
342
+ nativeSettings,
302
343
  declaresResources: array(target.resources).length > 0,
303
344
  publicHeadersPath: string(target.publicHeadersPath) ?? 'include',
304
345
  url: string(target.url) ?? null,
@@ -106,25 +106,39 @@ interface Pin {
106
106
  revision: string
107
107
  }
108
108
 
109
- function readPins(resolved: string): Map<string, Pin> {
110
- const data: unknown = JSON.parse(fs.readFileSync(resolved, 'utf8'))
111
- const pins =
112
- data && typeof data === 'object' && 'pins' in data && Array.isArray(data.pins)
113
- ? data.pins
114
- : null
115
- if (!pins)
116
- throw new Error(`${resolved} has no pins; resolve packages in a current Xcode`)
109
+ function readPins(resolved: readonly string[]): Map<string, Pin> {
117
110
  const out = new Map<string, Pin>()
118
- for (const pin of pins) {
119
- const identity = pin?.identity
120
- const location = pin?.location
121
- const revision = pin?.state?.revision
122
- if (
123
- typeof identity === 'string' &&
124
- typeof location === 'string' &&
125
- typeof revision === 'string'
126
- )
127
- out.set(identity, { location, revision })
111
+ const owners = new Map<string, string>()
112
+ for (const file of resolved) {
113
+ const data: unknown = JSON.parse(fs.readFileSync(file, 'utf8'))
114
+ const pins =
115
+ data && typeof data === 'object' && 'pins' in data && Array.isArray(data.pins)
116
+ ? data.pins
117
+ : null
118
+ if (!pins) throw new Error(`${file} has no pins; resolve packages in a current Xcode`)
119
+ for (const pin of pins) {
120
+ const identity = pin?.identity
121
+ const location = pin?.location
122
+ const revision = pin?.state?.revision
123
+ if (
124
+ typeof identity === 'string' &&
125
+ typeof location === 'string' &&
126
+ typeof revision === 'string'
127
+ ) {
128
+ const previous = out.get(identity)
129
+ const repository = (url: string) => url.replace(/(?:\.git)?\/?$/, '')
130
+ if (
131
+ previous &&
132
+ (previous.revision !== revision ||
133
+ repository(previous.location) !== repository(location))
134
+ )
135
+ throw new Error(
136
+ `conflicting Package.resolved pins for ${identity}: ${owners.get(identity)} pins ${previous.location} at ${previous.revision}; ${file} pins ${location} at ${revision}`,
137
+ )
138
+ out.set(identity, { location, revision })
139
+ owners.set(identity, file)
140
+ }
141
+ }
128
142
  }
129
143
  return out
130
144
  }
@@ -177,8 +191,8 @@ interface LoadedPackage {
177
191
  */
178
192
  export function resolveSwiftPackages(options: {
179
193
  rootDir: string
180
- /** the Package.resolved pinning remote revisions, when the app has one. */
181
- resolved: string | null
194
+ /** the selected workspace or local Package.resolved files. */
195
+ resolved: readonly string[]
182
196
  products: readonly SwiftLinkedProduct[]
183
197
  /** local package directories the workspace or a project references. */
184
198
  localDirs: readonly string[]
@@ -194,7 +208,7 @@ export function resolveSwiftPackages(options: {
194
208
  modules: new Map(),
195
209
  binaries: [],
196
210
  }
197
- const pins = resolved ? readPins(resolved) : null
211
+ const pins = resolved.length > 0 ? readPins(resolved) : null
198
212
  const checkouts = remoteSwiftCheckouts(rootDir)
199
213
 
200
214
  const packages = new Map<string, LoadedPackage | string>()
@@ -0,0 +1,153 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { createHash, randomUUID } from 'node:crypto'
3
+ import {
4
+ linkSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from 'node:fs'
11
+ import path from 'node:path'
12
+ import { rnxHomeDir } from './home-paths'
13
+
14
+ const POLL_MS = 250
15
+
16
+ interface Holder {
17
+ pid: number
18
+ start: string | null
19
+ }
20
+
21
+ // a pid alone is reused; the start time names the process that wrote the lock.
22
+ function processStart(pid: number): string | null {
23
+ try {
24
+ return (
25
+ execFileSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
26
+ encoding: 'utf8',
27
+ stdio: ['ignore', 'pipe', 'ignore'],
28
+ }).trim() || null
29
+ )
30
+ } catch {
31
+ return null
32
+ }
33
+ }
34
+
35
+ function parseHolder(text: string): Holder | null {
36
+ try {
37
+ const value: unknown = JSON.parse(text)
38
+ if (
39
+ value &&
40
+ typeof value === 'object' &&
41
+ 'pid' in value &&
42
+ typeof value.pid === 'number' &&
43
+ 'start' in value &&
44
+ (typeof value.start === 'string' || value.start === null)
45
+ )
46
+ return { pid: value.pid, start: value.start }
47
+ } catch {}
48
+ return null
49
+ }
50
+
51
+ function readLock(lock: string): string | null {
52
+ try {
53
+ return readFileSync(lock, 'utf8')
54
+ } catch (error) {
55
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return null
56
+ throw error
57
+ }
58
+ }
59
+
60
+ function holderAlive(text: string): boolean {
61
+ const holder = parseHolder(text)
62
+ if (!holder) return false
63
+ const start = processStart(holder.pid)
64
+ return start !== null && start === holder.start
65
+ }
66
+
67
+ function pause(ms: number, signal?: AbortSignal): Promise<void> {
68
+ return new Promise((resolve) => {
69
+ const done = () => {
70
+ clearTimeout(timer)
71
+ signal?.removeEventListener('abort', done)
72
+ resolve()
73
+ }
74
+ const timer = setTimeout(done, ms)
75
+ signal?.addEventListener('abort', done, { once: true })
76
+ })
77
+ }
78
+
79
+ /** runs `work` as the only process using a swift build root. the in-process
80
+ * queue orders one process's requests; two processes sharing a root each
81
+ * rewrite its sources and manifest, and swiftpm's own build lock only orders
82
+ * the builds, so a build can compile the other process's sources and then
83
+ * read back that process's executable. the lock lives beside the root's
84
+ * tree, never inside it: a write sweeps every file of the tree it does not
85
+ * name. a lock whose owner process is gone is taken over. */
86
+ export async function withSwiftRootLock<T>(
87
+ root: string,
88
+ work: () => Promise<T>,
89
+ signal?: AbortSignal,
90
+ ): Promise<T> {
91
+ const directory = path.join(rnxHomeDir(), 'cache', 'swift-root-locks')
92
+ mkdirSync(directory, { recursive: true })
93
+ const lock = path.join(
94
+ directory,
95
+ `${createHash('sha256').update(root).digest('hex').slice(0, 32)}.lock`,
96
+ )
97
+ const token = randomUUID()
98
+ const owner = JSON.stringify({
99
+ pid: process.pid,
100
+ start: processStart(process.pid),
101
+ token,
102
+ })
103
+ const pending = `${lock}.${token}`
104
+ // the owner record is complete before the lock path exists, so a waiter
105
+ // never reads a half-written lock.
106
+ writeFileSync(pending, owner)
107
+ try {
108
+ for (;;) {
109
+ signal?.throwIfAborted()
110
+ try {
111
+ linkSync(pending, lock)
112
+ break
113
+ } catch (error) {
114
+ if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST'))
115
+ throw error
116
+ }
117
+ const held = readLock(lock)
118
+ if (held === null) continue
119
+ if (holderAlive(held)) {
120
+ await pause(POLL_MS, signal)
121
+ continue
122
+ }
123
+ // take over by moving the dead record aside. a different record in the
124
+ // moved file means a new owner landed first; put it back.
125
+ const dead = `${lock}.dead-${token}`
126
+ try {
127
+ renameSync(lock, dead)
128
+ } catch (error) {
129
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') continue
130
+ throw error
131
+ }
132
+ try {
133
+ if (readLock(dead) !== held) {
134
+ try {
135
+ linkSync(dead, lock)
136
+ } catch (error) {
137
+ if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST'))
138
+ throw error
139
+ }
140
+ }
141
+ } finally {
142
+ rmSync(dead, { force: true })
143
+ }
144
+ }
145
+ } finally {
146
+ rmSync(pending, { force: true })
147
+ }
148
+ try {
149
+ return await work()
150
+ } finally {
151
+ if (readLock(lock) === owner) rmSync(lock, { force: true })
152
+ }
153
+ }
@@ -90,7 +90,7 @@ export type StandinResult = {
90
90
  }
91
91
 
92
92
  /** bump when emitted source changes: the stand-in cache keys on it. */
93
- export const STANDIN_EMIT_VERSION = '10'
93
+ export const STANDIN_EMIT_VERSION = '12'
94
94
 
95
95
  /** the modules every stand-in source imports. */
96
96
  export const STANDIN_IMPORTS = ['Foundation', 'UIKit']
@@ -229,6 +229,11 @@ export function emitSwiftStandin(
229
229
  family.has(parts[0]!) && parts[0] !== 'SwiftUICore' ? parts.join('.') : unqualified
230
230
  return { name, module, local: false }
231
231
  }
232
+ const nsObject = nameOf('c:objc(cs)NSObject', 'ObjectiveC.NSObject')!.name
233
+ const nsObjectProtocol = nameOf(
234
+ 'c:objc(pl)NSObject',
235
+ 'ObjectiveC.NSObjectProtocol',
236
+ )!.name
232
237
  for (const graph of graphs)
233
238
  for (const edge of graph.relationships) {
234
239
  const source = byId.get(edge.source)
@@ -577,7 +582,18 @@ export function emitSwiftStandin(
577
582
  const inner = `${indent} `
578
583
  switch (symbol.kind.identifier) {
579
584
  case 'swift.typealias': {
580
- const { 1: attrs, 2: rest } = LEADING_ATTRS.exec(text(symbol))!
585
+ // a graph shortens an external alias to its leaf, which can shadow itself.
586
+ const fragments = (symbol.declarationFragments ?? []).map((fragment) =>
587
+ fragment.kind === 'typeIdentifier' &&
588
+ fragment.spelling === leaf(symbol) &&
589
+ fragment.preciseIdentifier?.startsWith('s:s') &&
590
+ fragment.preciseIdentifier !== id
591
+ ? { ...fragment, spelling: `Swift.${fragment.spelling}` }
592
+ : fragment,
593
+ )
594
+ const { 1: attrs, 2: rest } = LEADING_ATTRS.exec(
595
+ text({ ...symbol, declarationFragments: fragments }),
596
+ )!
581
597
  emit(`${indent}${attrs}public ${rest}`)
582
598
  return
583
599
  }
@@ -598,7 +614,8 @@ export function emitSwiftStandin(
598
614
  ...(conforms.get(id) ?? []).filter(
599
615
  (name) =>
600
616
  !ancestors.has(name) &&
601
- !(parent && [...KEEP_EXTERNAL.values()].includes(name)),
617
+ !(parent && [...KEEP_EXTERNAL.values()].includes(name)) &&
618
+ !(name === nsObjectProtocol && externalRoot(id) === nsObject),
602
619
  ),
603
620
  ].filter((name, at, all) => all.indexOf(name) === at)
604
621
  const parentSymbol = superId.has(id) ? byId.get(superId.get(id)!) : undefined
@@ -719,7 +736,8 @@ export function emitSwiftStandin(
719
736
  synthesizedEquality(member, list))
720
737
  )
721
738
  continue
722
- const value = values.get(member.identifier.precise)
739
+ const header = values.get(member.identifier.precise)
740
+ const value = header === undefined ? undefined : atWidth(header, raw)
723
741
  if (value !== undefined && member.kind.identifier === 'swift.type.property') {
724
742
  emit(
725
743
  `${inner}public static let ${tick(leaf(member))} = ${shape.name}(rawValue: ${value})`,
@@ -739,10 +757,13 @@ export function emitSwiftStandin(
739
757
  if (!shape)
740
758
  return void skipped.push({ path, reason: `unparsed enum ${text(symbol)}` })
741
759
  const members = membersOf(id)
742
- const raw = rawType(members)
743
760
  const cases = members.filter(
744
761
  (member) => member.kind.identifier === 'swift.enum.case',
745
762
  )
763
+ // payload cases use a manual RawRepresentable witness, not a raw enum.
764
+ const raw = cases.some((member) => text(member).includes('('))
765
+ ? null
766
+ : rawType(members)
746
767
  const missing = raw
747
768
  ? cases.filter((member) => !values.has(member.identifier.precise))
748
769
  : []
@@ -754,14 +775,15 @@ export function emitSwiftStandin(
754
775
  reason: `no header value for ${missing.map(leaf).join(', ')}`,
755
776
  })
756
777
  const list = inheritance(id, raw ?? undefined).filter(
757
- (name) => name !== 'RawRepresentable',
778
+ (name) => !raw || name !== 'RawRepresentable',
758
779
  )
759
780
  emit(
760
781
  `${indent}${shape.attrs}public enum ${shape.name}${shape.generics}${list.length ? `: ${list.join(', ')}` : ''}${shape.where} {`,
761
782
  )
762
783
  const seenValues = new Map<string, string>()
763
784
  for (const member of cases) {
764
- const value = values.get(member.identifier.precise)
785
+ const header = values.get(member.identifier.precise)
786
+ const value = header === undefined ? undefined : atWidth(header, raw)
765
787
  // objective-c aliases a value under a second name; swift spells
766
788
  // the alias as a static member of the first case.
767
789
  if (value !== undefined && seenValues.has(value)) {
@@ -891,6 +913,17 @@ export function emitSwiftStandin(
891
913
  const init = members.find((member) => leaf(member) === 'init(rawValue:)')
892
914
  return init ? (/rawValue:\s*([^)]+)\)/.exec(text(init))?.[1]?.trim() ?? null) : null
893
915
  }
916
+ // header values are clang's for 64-bit iOS. NSUIntegerMax, NSIntegerMax
917
+ // and NSIntegerMin are the platform-width type's own extremes, which on
918
+ // wasm32 are the 32-bit ones.
919
+ const atWidth = (value: string, raw: string | null) =>
920
+ (raw === 'UInt' || raw === 'Swift.UInt') && value === '18446744073709551615'
921
+ ? '4294967295'
922
+ : (raw === 'Int' || raw === 'Swift.Int') && value === '9223372036854775807'
923
+ ? '2147483647'
924
+ : (raw === 'Int' || raw === 'Swift.Int') && value === '-9223372036854775808'
925
+ ? '-2147483648'
926
+ : value
894
927
 
895
928
  const emitGlobal = scoped((symbol: GraphSymbol) => {
896
929
  const path = pathOf(symbol)