peachsim 0.1.1625 → 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 (152) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +10 -1
  3. package/cli/app-config.ts +4 -1
  4. package/cli/app-fonts.ts +37 -17
  5. package/cli/app-stubs.ts +224 -9
  6. package/cli/bridge-flow-runner.ts +64 -43
  7. package/cli/command-registry.ts +1 -0
  8. package/cli/commands/control.ts +283 -49
  9. package/cli/commands/doctor.ts +17 -184
  10. package/cli/commands/flow.ts +79 -11
  11. package/cli/commands/inspect/core.ts +10 -5
  12. package/cli/commands/inspect/settling.ts +14 -2
  13. package/cli/commands/inspect/wait-ready.ts +12 -0
  14. package/cli/commands/inspect.ts +26 -3
  15. package/cli/commands/install-desktop.ts +1 -0
  16. package/cli/commands/maestro-compare.ts +97 -38
  17. package/cli/commands/maestro.ts +9 -1
  18. package/cli/commands/record.ts +21 -1
  19. package/cli/commands/screenshot.ts +23 -1
  20. package/cli/commands/serve.ts +7 -2
  21. package/cli/commands/setup.ts +19 -42
  22. package/cli/commands/swift.ts +5 -0
  23. package/cli/commands/upload.ts +46 -7
  24. package/cli/desktop-companion.ts +102 -17
  25. package/cli/drivers/desktop.ts +14 -1
  26. package/cli/drivers/playwright-sim-host.ts +38 -3
  27. package/cli/drivers/playwright.ts +9 -8
  28. package/cli/drivers/types.ts +5 -0
  29. package/cli/maestro-accessibility.ts +1 -1
  30. package/cli/maestro-compare.ts +84 -12
  31. package/cli/main.ts +2 -1
  32. package/cli/outbound-endpoints.ts +13 -1
  33. package/cli/pause-aware-timeout.ts +102 -0
  34. package/cli/scene-video.ts +89 -0
  35. package/cli/ws-bridge.ts +23 -14
  36. package/detox/attempt-events.cjs +4 -0
  37. package/detox/index.ts +167 -142
  38. package/detox/motion-change.cjs +57 -21
  39. package/detox/motion-change.d.cts +7 -2
  40. package/dist-lib/agent-daemon-client.cjs +1 -1
  41. package/dist-lib/agent-events.cjs +1 -1
  42. package/dist-lib/attached-projects.cjs +1 -1
  43. package/dist-lib/auth/shared-session.cjs +1 -1
  44. package/dist-lib/backend-origin.cjs +1 -1
  45. package/dist-lib/beta.cjs +1 -1
  46. package/dist-lib/beta.mjs +1 -1
  47. package/dist-lib/bridge-constants.cjs +13 -1
  48. package/dist-lib/bridge-contract-input.cjs +1 -1
  49. package/dist-lib/bridge-contract-input.mjs +1 -1
  50. package/dist-lib/bridge-contract.cjs +14 -8
  51. package/dist-lib/bridge-contract.mjs +13 -7
  52. package/dist-lib/bundle-channel.cjs +9536 -7604
  53. package/dist-lib/capture-contract.cjs +1 -1
  54. package/dist-lib/capture-contract.mjs +1 -1
  55. package/dist-lib/cloud-contract.cjs +1 -1
  56. package/dist-lib/cloud-contract.mjs +1 -1
  57. package/dist-lib/cloud-test-args.cjs +1 -1
  58. package/dist-lib/cloud-test-args.mjs +1 -1
  59. package/dist-lib/cloud.cjs +1 -1
  60. package/dist-lib/cloud.mjs +1 -1
  61. package/dist-lib/config.cjs +6 -3
  62. package/dist-lib/config.mjs +6 -3
  63. package/dist-lib/detox/index.cjs +895 -664
  64. package/dist-lib/dev-bundle-resolution.cjs +14 -2
  65. package/dist-lib/expo-swift-bundle.cjs +7818 -5916
  66. package/dist-lib/expo-swift-metadata.cjs +3 -1
  67. package/dist-lib/expo-swift-metadata.mjs +3 -1
  68. package/dist-lib/home-paths.cjs +32 -3
  69. package/dist-lib/host/bridge-host.cjs +8340 -6392
  70. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  71. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  72. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  73. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  74. package/dist-lib/host/websocket-proxy.cjs +1 -1
  75. package/dist-lib/index.cjs +812 -339
  76. package/dist-lib/jump-to-source-babel.cjs +1 -1
  77. package/dist-lib/jump-to-source-native.cjs +1 -1
  78. package/dist-lib/kotlin-host.cjs +1 -1
  79. package/dist-lib/menu.cjs +1 -1
  80. package/dist-lib/menu.mjs +1 -1
  81. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  82. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  83. package/dist-lib/metro-production-bundle.cjs +1 -1
  84. package/dist-lib/metro-production-bundle.mjs +1 -1
  85. package/dist-lib/metro.cjs +391 -28
  86. package/dist-lib/native-app-identity-project.cjs +55 -2
  87. package/dist-lib/native-app-identity-project.mjs +55 -2
  88. package/dist-lib/native-app-identity.cjs +53 -1
  89. package/dist-lib/native-app-identity.mjs +52 -1
  90. package/dist-lib/native-firebase-metadata.cjs +1 -1
  91. package/dist-lib/native-firebase-metadata.mjs +1 -1
  92. package/dist-lib/plist.cjs +1 -1
  93. package/dist-lib/plist.mjs +1 -1
  94. package/dist-lib/profiles.cjs +1 -1
  95. package/dist-lib/public-brand.cjs +1 -1
  96. package/dist-lib/react-native-host-modules.cjs +5 -1
  97. package/dist-lib/react-native-host-modules.mjs +5 -1
  98. package/dist-lib/scripts/dev-server-scanner.cjs +15 -3
  99. package/dist-lib/sdk.cjs +27 -11
  100. package/dist-lib/sdk.mjs +27 -10
  101. package/dist-lib/skills.cjs +12276 -8947
  102. package/dist-lib/swift-compile.cjs +924 -118
  103. package/dist-lib/swift-compile.mjs +966 -152
  104. package/dist-lib/swift-package.cjs +12 -3
  105. package/dist-lib/swift-package.mjs +12 -3
  106. package/dist-lib/swift-submit.cjs +7 -1
  107. package/dist-lib/swift-submit.mjs +5 -1
  108. package/dist-lib/swift-toolchain.cjs +133 -18
  109. package/dist-lib/swift.cjs +1 -1
  110. package/dist-lib/vite-swift.cjs +1955 -498
  111. package/dist-lib/vite.cjs +3 -1
  112. package/dist-lib/worklet-executable.cjs +1 -1
  113. package/dist-lib/worklet-executable.mjs +1 -1
  114. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  115. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  116. package/package.json +1 -1
  117. package/scripts/dev-server-scanner.ts +2 -1
  118. package/skills/peach-perf/SKILL.md +11 -5
  119. package/skills/peach-setup/SKILL.md +16 -19
  120. package/src/bridge-constants.ts +11 -0
  121. package/src/bridge-contract.ts +22 -34
  122. package/src/config.ts +15 -0
  123. package/src/desktop-host-delivery.ts +5 -1
  124. package/src/dev-bundle-resolution.ts +2 -1
  125. package/src/expo-manifest-body.ts +13 -0
  126. package/src/expo-swift-host.ts +191 -47
  127. package/src/expo-swift-ready.ts +78 -0
  128. package/src/expoSwiftContract.ts +13 -0
  129. package/src/home-paths.ts +48 -2
  130. package/src/host/bridge-host.ts +5 -2
  131. package/src/native-app-identity-project.ts +26 -2
  132. package/src/native-app-identity.ts +90 -0
  133. package/src/native-firebase-config.ts +209 -13
  134. package/src/process-group.ts +25 -2
  135. package/src/react-native-bridge-modules.ts +416 -0
  136. package/src/react-native-host-modules.ts +4 -0
  137. package/src/swift-binary-modules.ts +2 -2
  138. package/src/swift-compile.ts +835 -84
  139. package/src/swift-compiler-digest.ts +14 -0
  140. package/src/swift-distribution.lock.json +14 -12
  141. package/src/swift-host.ts +91 -29
  142. package/src/swift-icu.ts +14 -6
  143. package/src/swift-objc-sdk.ts +133 -10
  144. package/src/swift-objc-standin.ts +25 -3
  145. package/src/swift-package-manifest.ts +21 -6
  146. package/src/swift-package.ts +15 -1
  147. package/src/swift-packages.ts +119 -60
  148. package/src/swift-standin-emit.ts +57 -9
  149. package/src/swift-standins.ts +168 -20
  150. package/src/swift-submit.ts +10 -0
  151. package/src/swift-toolchain.ts +149 -23
  152. package/src/vite-plugin.ts +12 -1
@@ -8,8 +8,8 @@
8
8
  import { execFileSync } from 'node:child_process'
9
9
  import { createHash } from 'node:crypto'
10
10
  import fs from 'node:fs'
11
- import os from 'node:os'
12
11
  import path from 'node:path'
12
+ import { cacheDir as peachCacheDir, swiftPackageCacheOptions } from './home-paths'
13
13
  import lock from './swift-distribution.lock.json' with { type: 'json' }
14
14
  import { swiftToolchainBin } from './swift-toolchain'
15
15
  import type { SwiftTargetDependency } from './swift-package'
@@ -27,9 +27,9 @@ export type SwiftManifestTargetType = (typeof TARGET_TYPES)[number]
27
27
  const isTargetType = (value: string | undefined): value is SwiftManifestTargetType =>
28
28
  TARGET_TYPES.some((type) => type === value)
29
29
 
30
- /** a swift condition a target defines on ios, and the swiftpm build
30
+ /** a named target setting active on ios, and the swiftpm build
31
31
  * configuration it needs (null applies in both). */
32
- export interface SwiftManifestDefine {
32
+ export interface SwiftManifestSetting {
33
33
  name: string
34
34
  configuration: 'debug' | 'release' | null
35
35
  }
@@ -47,9 +47,12 @@ export interface SwiftManifestTarget {
47
47
  dependencies: SwiftTargetDependency[]
48
48
  languageMode: number
49
49
  mainActorIsolation: boolean
50
- defines: SwiftManifestDefine[]
50
+ defines: SwiftManifestSetting[]
51
+ libraries: SwiftManifestSetting[]
51
52
  /** true when the target declares `resources:`. */
52
53
  declaresResources: boolean
54
+ /** public header directory relative to the target, as SwiftPM declares it. */
55
+ publicHeadersPath: string
53
56
  /** a binary target's archive url; a local one's artifact is `dir`. */
54
57
  url: string | null
55
58
  /** the declared SHA-256 for a remote binary archive. */
@@ -80,7 +83,7 @@ function swiftCommand(): { file: string; args: string[] } {
80
83
  : { file: path.join(swiftToolchainBin(lock.release), 'swift'), args: [] }
81
84
  }
82
85
 
83
- const cacheDir = path.join(os.tmpdir(), 'peach-swift-manifests')
86
+ const cacheDir = path.join(peachCacheDir(), 'swift-manifests')
84
87
  const evaluated = new Map<string, SwiftPackageManifest>()
85
88
 
86
89
  function dump(dir: string): { key: string; raw: Record<string, Json> } {
@@ -108,6 +111,7 @@ function dump(dir: string): { key: string; raw: Record<string, Json> } {
108
111
  [
109
112
  ...command.args,
110
113
  'package',
114
+ ...swiftPackageCacheOptions(),
111
115
  '--package-path',
112
116
  dir,
113
117
  // swiftpm's scratch files never land in the app's own package
@@ -249,7 +253,8 @@ export function swiftPackageManifest(dir: string): SwiftPackageManifest {
249
253
  }
250
254
  let languageMode = packageMode
251
255
  let mainActorIsolation = false
252
- const defines: SwiftManifestDefine[] = []
256
+ const defines: SwiftManifestSetting[] = []
257
+ const libraries: SwiftManifestSetting[] = []
253
258
  for (const setting of array(target.settings)) {
254
259
  const { kind: rawKind, tool, condition } = record(setting)
255
260
  const settingKind = record(rawKind)
@@ -271,6 +276,14 @@ export function swiftPackageManifest(dir: string): SwiftPackageManifest {
271
276
  configuration: config === 'debug' || config === 'release' ? config : null,
272
277
  })
273
278
  }
279
+ const library = string(record(settingKind.linkedLibrary)._0)
280
+ if (library && string(tool) === 'linker' && linksOnIOS(condition, traits)) {
281
+ const config = string(record(condition).config)
282
+ libraries.push({
283
+ name: library,
284
+ configuration: config === 'debug' || config === 'release' ? config : null,
285
+ })
286
+ }
274
287
  }
275
288
  targets.set(name, {
276
289
  name,
@@ -285,7 +298,9 @@ export function swiftPackageManifest(dir: string): SwiftPackageManifest {
285
298
  languageMode,
286
299
  mainActorIsolation,
287
300
  defines,
301
+ libraries,
288
302
  declaresResources: array(target.resources).length > 0,
303
+ publicHeadersPath: string(target.publicHeadersPath) ?? 'include',
289
304
  url: string(target.url) ?? null,
290
305
  checksum: string(target.checksum) ?? null,
291
306
  })
@@ -8,6 +8,8 @@ export interface SwiftPackageTarget {
8
8
  path: string
9
9
  /** true for `.executableTarget`: the one that links the wasm artifact. */
10
10
  executable: boolean
11
+ /** true for a header-only `.systemLibrary` supplied by the target SDK. */
12
+ systemLibrary?: boolean
11
13
  /** the target's declared `.swiftLanguageMode(.vN)`; 5 when it declares none. */
12
14
  languageMode: number
13
15
  /** true when the target declares `.defaultIsolation(MainActor.self)`. */
@@ -16,6 +18,8 @@ export interface SwiftPackageTarget {
16
18
  cxxInterop?: boolean
17
19
  /** compilation conditions the target declares unconditionally with `.define`. */
18
20
  defines?: string[]
21
+ /** libraries the target declares unconditionally with `.linkedLibrary`. */
22
+ linkedLibraries?: string[]
19
23
  /** true when declared or implicit resources give the target `Bundle.module`. */
20
24
  resources: boolean
21
25
  /** declared dependencies on other targets in the same package. */
@@ -202,7 +206,11 @@ export function swiftManifestTargets(
202
206
  ): SwiftPackageTarget[] {
203
207
  const implicitResources = swiftImplicitResourcePaths(resourcePaths)
204
208
  const targets: SwiftPackageTarget[] = []
205
- const declarations = manifestCalls(manifest, ['executableTarget', 'target'])
209
+ const declarations = manifestCalls(manifest, [
210
+ 'executableTarget',
211
+ 'target',
212
+ 'systemLibrary',
213
+ ])
206
214
  const targetNames = new Set(
207
215
  declarations.flatMap(({ flat }) => {
208
216
  const name = /\bname\s*:\s*"([^"]+)"/.exec(flat)?.[1]
@@ -230,6 +238,9 @@ export function swiftManifestTargets(
230
238
  const defines = [
231
239
  ...call.matchAll(/\.define\(\s*"([A-Za-z_][A-Za-z0-9_]*)"\s*\)/g),
232
240
  ].map((match) => match[1]!)
241
+ const linkedLibraries = [...call.matchAll(/\.linkedLibrary\(\s*"([^"]+)"\s*\)/g)].map(
242
+ (match) => match[1]!,
243
+ )
233
244
  const excluded = [
234
245
  ...(/\bexclude\s*:\s*\[([^\]]*)\]/.exec(flat)?.[1] ?? '').matchAll(/"([^"]+)"/g),
235
246
  ].map((match) => match[1].replace(/^\.\//, '').replace(/\/+$/, ''))
@@ -270,10 +281,12 @@ export function swiftManifestTargets(
270
281
  name,
271
282
  path: targetPath,
272
283
  executable,
284
+ ...(kind === 'systemLibrary' ? { systemLibrary: true } : {}),
273
285
  languageMode,
274
286
  mainActorIsolation,
275
287
  ...(cxxInterop ? { cxxInterop: true } : {}),
276
288
  ...(defines.length === 0 ? {} : { defines }),
289
+ ...(linkedLibraries.length === 0 ? {} : { linkedLibraries }),
277
290
  resources,
278
291
  ...(dependencies.length === 0 ? {} : { dependencies }),
279
292
  ...(excluded.length === 0 ? {} : { exclude: excluded }),
@@ -380,6 +393,7 @@ export function swiftPackagePartition(
380
393
  targets[0].languageMode === 5 &&
381
394
  !targets[0].mainActorIsolation &&
382
395
  !targets[0].defines?.length &&
396
+ !targets[0].linkedLibraries?.length &&
383
397
  !targets[0].cxxInterop &&
384
398
  !swiftManifestCxxLanguageStandard(manifest)
385
399
  ? []
@@ -2,14 +2,16 @@
2
2
  // each linked product resolves through swiftpm's own reading of its package
3
3
  // (swift-package-manifest.ts) to the source targets that build it; remote
4
4
  // packages are fetched at the revision the app's Package.resolved pins, into
5
- // .build/peach/checkouts under the app's root. swift and c-family source
5
+ // PEACH_HOME/cache/swift-packages, isolated by the app root. swift and c-family source
6
6
  // targets use the common compiler backend. objective-c, assembly and binary
7
7
  // targets the host built no stand-in for stay missing with their reason;
8
8
  // the targets beside them still compile.
9
9
 
10
10
  import { execFileSync } from 'node:child_process'
11
+ import { createHash } from 'node:crypto'
11
12
  import fs from 'node:fs'
12
13
  import path from 'node:path'
14
+ import { cacheDir } from './home-paths'
13
15
  import {
14
16
  swiftImplicitResourcePaths,
15
17
  swiftPackageIdentity,
@@ -19,7 +21,7 @@ import {
19
21
  import {
20
22
  swiftPackageManifest,
21
23
  swiftTargetFiles,
22
- type SwiftManifestDefine,
24
+ type SwiftManifestSetting,
23
25
  type SwiftPackageManifest,
24
26
  } from './swift-package-manifest'
25
27
 
@@ -43,7 +45,8 @@ export interface SwiftPackageSourceTarget extends SwiftPackageTarget {
43
45
  packageDir: string
44
46
  remote: boolean
45
47
  /** the conditions its manifest defines, before a build configuration picks them. */
46
- packageDefines: SwiftManifestDefine[]
48
+ packageDefines: SwiftManifestSetting[]
49
+ packageLibraries: SwiftManifestSetting[]
47
50
  }
48
51
 
49
52
  /** why a product or module does not compile, and where it comes from. */
@@ -74,22 +77,29 @@ export interface SwiftPackageBinary {
74
77
  dir: string
75
78
  /** a linked product's closure reaches it, so the app links it. */
76
79
  reached: boolean
80
+ /** a source Objective-C target: public headers relative to dir. */
81
+ publicHeaders?: string
82
+ /** dependency module public headers, relative to dir. */
83
+ headerDependencies?: { module: string; path: string }[]
77
84
  }
78
85
 
79
- /** where checkouts live, relative to the app's root. */
80
- export const REMOTE_CHECKOUTS = path.join('.build', 'peach', 'checkouts')
86
+ /** dependency checkouts belong to peach, isolated by the canonical app root. */
87
+ export function remoteSwiftCheckouts(root: string): string {
88
+ const key = createHash('sha256')
89
+ .update(fs.realpathSync(root))
90
+ .digest('hex')
91
+ .slice(0, 32)
92
+ fs.mkdirSync(cacheDir(), { recursive: true })
93
+ return path.join(fs.realpathSync(cacheDir()), 'swift-packages', key, 'checkouts')
94
+ }
81
95
 
82
96
  // sources requiring an objective-c runtime or assembly lowering.
83
97
  const UNSUPPORTED_NATIVE = /\.(m|mm|s|S)$/
84
98
 
85
99
  const BINARY = 'is a binary framework'
86
- // of the reasons a target or product cannot compile, a binary framework's
87
- // leads: no source exists for it, while another target can retain its
88
- // source-specific reason
89
- const blocker = (reasons: (string | null)[]): string | null =>
90
- reasons.find((reason) => reason?.includes(BINARY)) ??
91
- reasons.find((reason) => reason !== null) ??
92
- null
100
+ // retain every independent blocker in dependency order. a product can reach
101
+ // several unsupported targets, and fixing one must not reveal a hidden next one.
102
+ const blockers = (reasons: readonly string[][]): string[] => [...new Set(reasons.flat())]
93
103
 
94
104
  interface Pin {
95
105
  location: string
@@ -185,7 +195,7 @@ export function resolveSwiftPackages(options: {
185
195
  binaries: [],
186
196
  }
187
197
  const pins = resolved ? readPins(resolved) : null
188
- const checkouts = path.join(rootDir, REMOTE_CHECKOUTS)
198
+ const checkouts = remoteSwiftCheckouts(rootDir)
189
199
 
190
200
  const packages = new Map<string, LoadedPackage | string>()
191
201
  // a package by local directory or remote url, or why it cannot load
@@ -205,8 +215,6 @@ export function resolveSwiftPackages(options: {
205
215
  : 'no Package.resolved pins it; resolve packages in Xcode once'
206
216
  } else {
207
217
  fs.mkdirSync(checkouts, { recursive: true })
208
- // fetched sources never show up as changes in the app's repository
209
- fs.writeFileSync(path.join(path.dirname(checkouts), '.gitignore'), '*\n')
210
218
  loaded = {
211
219
  source: source.url,
212
220
  remote: true,
@@ -219,35 +227,40 @@ export function resolveSwiftPackages(options: {
219
227
  }
220
228
 
221
229
  const targets = new Map<string, SwiftPackageSourceTarget>()
222
- // why each target of each package cannot compile, or null once it can
223
- const targetOutcomes = new Map<string, string | null>()
224
- const resolveTarget = (pkg: LoadedPackage, name: string): string | null => {
230
+ // every reason each target cannot compile; empty once it can
231
+ const targetOutcomes = new Map<string, string[]>()
232
+ const resolveTarget = (pkg: LoadedPackage, name: string): string[] => {
225
233
  const key = `${pkg.manifest.dir}\0${name}`
226
234
  if (targetOutcomes.has(key)) return targetOutcomes.get(key)!
227
235
  // a dependency cycle resolves as compiling; swiftpm rejects real cycles
228
- targetOutcomes.set(key, null)
229
- const outcome = ((): string | null => {
230
- if (provided.has(name)) return null
236
+ targetOutcomes.set(key, [])
237
+ const outcome = ((): string[] => {
238
+ if (provided.has(name)) return []
231
239
  const target = pkg.manifest.targets.get(name)
232
- if (!target) return `${path.basename(pkg.manifest.dir)} declares no target ${name}`
233
- if (target.type === 'binary') {
234
- const gap = options.binaryGaps?.get(name)
235
- return `${name} ${BINARY}${gap ? `; ${gap}` : ''}`
236
- }
237
- if (target.type === 'system') return `${name} is a system C library`
238
- if (target.type !== 'regular') return `${name} is a ${target.type} target`
239
- const files = swiftTargetFiles(pkg.manifest, target)
240
- if (files.some((file) => UNSUPPORTED_NATIVE.test(file)))
241
- return `${name} is an Objective-C or assembly target`
240
+ if (!target)
241
+ return [`${path.basename(pkg.manifest.dir)} declares no target ${name}`]
242
+ const files =
243
+ target.type === 'regular' || target.type === 'system'
244
+ ? swiftTargetFiles(pkg.manifest, target)
245
+ : []
246
+ const gap = options.binaryGaps?.get(name)
247
+ const ownReason =
248
+ target.type === 'binary'
249
+ ? `${name} ${BINARY}${gap ? `; ${gap}` : ''}`
250
+ : target.type !== 'regular' && target.type !== 'system'
251
+ ? `${name} is a ${target.type} target`
252
+ : files.some((file) => UNSUPPORTED_NATIVE.test(file))
253
+ ? `${name} is an Objective-C or assembly target${gap ? `; ${gap}` : ''}`
254
+ : null
242
255
  // every dependency resolves, so a failing one never hides the rest
243
256
  // of the closure from the compile or from the report
244
257
  const reasons = target.dependencies.map((dependency) => {
245
258
  if (dependency.kind === 'target') return resolveTarget(pkg, dependency.name)
246
259
  const nested = resolveDependencyProduct(pkg, dependency.package, dependency.name)
247
- return nested && `${dependency.name}: ${nested}`
260
+ return nested.map((reason) => `${dependency.name}: ${reason}`)
248
261
  })
249
- const blocked = blocker(reasons)
250
- if (blocked) return blocked
262
+ const blocked = blockers([ownReason ? [ownReason] : [], ...reasons])
263
+ if (blocked.length > 0) return blocked
251
264
  const relative = files.map((file) => path.relative(target.dir, file))
252
265
  targets.set(`${pkg.manifest.dir}\0${name}`, {
253
266
  name,
@@ -257,6 +270,7 @@ export function resolveSwiftPackages(options: {
257
270
  packageDir: pkg.manifest.dir,
258
271
  remote: pkg.remote,
259
272
  packageDefines: target.defines,
273
+ packageLibraries: target.libraries,
260
274
  dependencies: target.dependencies.flatMap((dependency) => {
261
275
  if (dependency.kind === 'target') return [dependency]
262
276
  if (provided.has(dependency.name)) return []
@@ -265,32 +279,35 @@ export function resolveSwiftPackages(options: {
265
279
  source &&
266
280
  packages.get('dir' in source ? path.resolve(source.dir) : source.url)
267
281
  return nested && typeof nested !== 'string'
268
- ? (nested.manifest.products.get(dependency.name) ?? []).map((name): SwiftTargetDependency => ({
269
- kind: 'target',
270
- name,
271
- }))
282
+ ? (nested.manifest.products.get(dependency.name) ?? []).map(
283
+ (name): SwiftTargetDependency => ({
284
+ kind: 'target',
285
+ name,
286
+ }),
287
+ )
272
288
  : []
273
289
  }),
274
290
  executable: false,
291
+ ...(target.type === 'system' ? { systemLibrary: true } : {}),
275
292
  languageMode: target.languageMode,
276
293
  mainActorIsolation: target.mainActorIsolation,
277
294
  resources:
278
295
  target.declaresResources || swiftImplicitResourcePaths(relative).length > 0,
279
296
  })
280
- return null
297
+ return []
281
298
  })()
282
299
  targetOutcomes.set(key, outcome)
283
300
  return outcome
284
301
  }
285
302
  // products resolved anywhere in the closure, by package, with why one
286
303
  // cannot compile: a wrapper package's product is often the module name
287
- const productOutcomes = new Map<string, { pkg: LoadedPackage; reason: string | null }>()
288
- const resolveProduct = (pkg: LoadedPackage, product: string): string | null => {
289
- if (provided.has(product)) return null
304
+ const productOutcomes = new Map<string, { pkg: LoadedPackage; reason: string[] }>()
305
+ const resolveProduct = (pkg: LoadedPackage, product: string): string[] => {
306
+ if (provided.has(product)) return []
290
307
  const roots = pkg.manifest.products.get(product)
291
308
  const reason = !roots
292
- ? `${path.basename(pkg.manifest.dir)} has no library ${product}`
293
- : blocker(roots.map((name) => resolveTarget(pkg, name)))
309
+ ? [`${path.basename(pkg.manifest.dir)} has no library ${product}`]
310
+ : blockers(roots.map((name) => resolveTarget(pkg, name)))
294
311
  productOutcomes.set(`${pkg.manifest.dir}\0${product}`, { pkg, reason })
295
312
  return reason
296
313
  }
@@ -299,18 +316,18 @@ export function resolveSwiftPackages(options: {
299
316
  pkg: LoadedPackage,
300
317
  packageName: string,
301
318
  product: string,
302
- ): string | null => {
303
- if (provided.has(product)) return null
319
+ ): string[] => {
320
+ if (provided.has(product)) return []
304
321
  const dependency = pkg.manifest.packages.get(packageName)
305
322
  if (!dependency)
306
- return `${path.basename(pkg.manifest.dir)} names no package ${packageName}`
323
+ return [`${path.basename(pkg.manifest.dir)} names no package ${packageName}`]
307
324
  if ('dir' in dependency) {
308
325
  const loaded = load({ kind: 'local', dir: dependency.dir })
309
- return typeof loaded === 'string' ? loaded : resolveProduct(loaded, product)
326
+ return typeof loaded === 'string' ? [loaded] : resolveProduct(loaded, product)
310
327
  }
311
- if (!options.provided) return null
328
+ if (!options.provided) return []
312
329
  const loaded = load({ kind: 'remote', url: dependency.url })
313
- return typeof loaded === 'string' ? loaded : resolveProduct(loaded, product)
330
+ return typeof loaded === 'string' ? [loaded] : resolveProduct(loaded, product)
314
331
  }
315
332
 
316
333
  const unnamed = () =>
@@ -326,11 +343,11 @@ export function resolveSwiftPackages(options: {
326
343
  : load(source)
327
344
  const reason =
328
345
  pkg === undefined
329
- ? 'no package the workspace references vends it'
346
+ ? ['no package the workspace references vends it']
330
347
  : typeof pkg === 'string'
331
- ? pkg
348
+ ? [pkg]
332
349
  : resolveProduct(pkg, product)
333
- if (reason)
350
+ if (reason.length > 0)
334
351
  result.unavailable.set(product, {
335
352
  source:
336
353
  source.kind === 'remote'
@@ -340,7 +357,7 @@ export function resolveSwiftPackages(options: {
340
357
  : source.kind === 'local'
341
358
  ? source.dir
342
359
  : 'the workspace',
343
- reason,
360
+ reason: reason.join('; '),
344
361
  })
345
362
  }
346
363
 
@@ -352,24 +369,35 @@ export function resolveSwiftPackages(options: {
352
369
  if (target.type === 'test' || target.type === 'plugin' || target.type === 'macro')
353
370
  continue
354
371
  if (target.type === 'executable') continue
355
- if (target.type === 'binary')
372
+ const objc =
373
+ target.type === 'regular' &&
374
+ swiftTargetFiles(pkg.manifest, target).some((file) => /\.(m|mm)$/.test(file))
375
+ if (target.type === 'binary' || objc)
356
376
  result.binaries.push({
357
377
  target: name,
358
378
  source: pkg.source,
359
379
  url: target.url,
360
380
  checksum: target.checksum,
361
- dir: target.dir,
381
+ dir: objc ? pkg.manifest.dir : target.dir,
362
382
  reached: false,
383
+ ...(objc
384
+ ? {
385
+ publicHeaders: path.relative(
386
+ pkg.manifest.dir,
387
+ path.join(target.dir, target.publicHeadersPath),
388
+ ),
389
+ }
390
+ : {}),
363
391
  })
364
392
  const reason = resolveTarget(pkg, name)
365
- if (reason && !result.modules.has(name))
366
- result.modules.set(name, { source: pkg.source, reason })
393
+ if (reason.length > 0 && !result.modules.has(name))
394
+ result.modules.set(name, { source: pkg.source, reason: reason.join('; ') })
367
395
  }
368
396
  }
369
397
  for (const [key, { pkg, reason }] of productOutcomes) {
370
398
  const product = key.slice(key.indexOf('\0') + 1)
371
- if (reason && !result.modules.has(product))
372
- result.modules.set(product, { source: pkg.source, reason })
399
+ if (reason.length > 0 && !result.modules.has(product))
400
+ result.modules.set(product, { source: pkg.source, reason: reason.join('; ') })
373
401
  }
374
402
  // what compiles is every buildable target in a linked product's closure,
375
403
  // decided per target: a product with one target behind a binary still
@@ -410,6 +438,37 @@ export function resolveSwiftPackages(options: {
410
438
  if (pkg && typeof pkg !== 'string')
411
439
  binary.reached = reached.has(`${pkg.manifest.dir}\0${binary.target}`)
412
440
  }
441
+ for (const binary of result.binaries) {
442
+ if (binary.publicHeaders === undefined) continue
443
+ const pkg = packages.get(binary.source)
444
+ if (!pkg || typeof pkg === 'string') continue
445
+ const headers = new Map<string, string>()
446
+ const visited = new Set<string>()
447
+ const visit = (owner: LoadedPackage, name: string) => {
448
+ const key = `${owner.manifest.dir}\0${name}`
449
+ if (visited.has(key)) return
450
+ visited.add(key)
451
+ const target = owner.manifest.targets.get(name)
452
+ if (!target) return
453
+ const dir = path.join(target.dir, target.publicHeadersPath)
454
+ for (const dependency of target.dependencies) {
455
+ if (dependency.kind === 'target') visit(owner, dependency.name)
456
+ else {
457
+ const source = owner.manifest.packages.get(dependency.package)
458
+ const nested =
459
+ source &&
460
+ packages.get('dir' in source ? path.resolve(source.dir) : source.url)
461
+ if (nested && typeof nested !== 'string')
462
+ for (const root of nested.manifest.products.get(dependency.name) ?? [])
463
+ visit(nested, root)
464
+ }
465
+ }
466
+ if (name !== binary.target && fs.existsSync(dir))
467
+ headers.set(name, path.relative(binary.dir, dir))
468
+ }
469
+ visit(pkg, binary.target)
470
+ binary.headerDependencies = [...headers].map(([module, path]) => ({ module, path }))
471
+ }
413
472
  result.targets = [...targets.entries()]
414
473
  .filter(([key]) => reached.has(key))
415
474
  .map(([, target]) => target)
@@ -20,6 +20,7 @@ export type GraphSymbol = {
20
20
  pathComponents: string[]
21
21
  declarationFragments?: SymbolFragment[]
22
22
  accessLevel: string
23
+ functionSignature?: { returns?: SymbolFragment[] }
23
24
  availability?: {
24
25
  domain?: string
25
26
  isUnconditionallyUnavailable?: boolean
@@ -43,6 +44,8 @@ export type StandinOptions = {
43
44
  module: string
44
45
  /** where the declarations came from, for the banner: sdk, version, artifact. */
45
46
  source: string
47
+ /** source Objective-C targets keep declarations with inert native bodies. */
48
+ inert?: boolean
46
49
  /** modules the source imports beside Foundation and UIKit. */
47
50
  imports?: string[]
48
51
  /** precise symbol id → swift literal for enum cases and option values. */
@@ -87,7 +90,7 @@ export type StandinResult = {
87
90
  }
88
91
 
89
92
  /** bump when emitted source changes: the stand-in cache keys on it. */
90
- export const STANDIN_EMIT_VERSION = '7'
93
+ export const STANDIN_EMIT_VERSION = '10'
91
94
 
92
95
  /** the modules every stand-in source imports. */
93
96
  export const STANDIN_IMPORTS = ['Foundation', 'UIKit']
@@ -331,6 +334,47 @@ export function emitSwiftStandin(
331
334
  ])
332
335
  }
333
336
 
337
+ // DEGRADED(swift-objc-runtime): source objc targets retain their api with inert native bodies.
338
+ // return values follow the imported declaration, never the app's call sites.
339
+ const inertValue = (type: string): string => {
340
+ const value = type.trim().replace(/!$/, '?')
341
+ if (value.endsWith('?')) return 'nil'
342
+ if (value === 'Void' || value === '()') return '()'
343
+ if (value === 'Bool') return 'false'
344
+ if (
345
+ /^(?:U?Int(?:8|16|32|64)?|Float|Double|CGFloat|TimeInterval|NSTimeInterval)$/.test(
346
+ value,
347
+ )
348
+ )
349
+ return '0'
350
+ if (value === 'String') return '""'
351
+ if (value.startsWith('[')) return value.includes(':') ? '[:]' : '[]'
352
+ if (value === 'Any' || value === 'AnyObject') return 'NSObject()'
353
+ const typeId = typeByPath.get(value.replace(`${options.module}.`, ''))
354
+ const symbol = typeId && byId.get(typeId)
355
+ if (typeId && symbol && symbol.kind.identifier === 'swift.enum') {
356
+ const first = membersOf(typeId).find(
357
+ (member) => member.kind.identifier === 'swift.enum.case',
358
+ )
359
+ if (first) return `.${leaf(first).replace(/\(.*/, '')}`
360
+ }
361
+ return `${value}()`
362
+ }
363
+ const inertBody = (symbol: GraphSymbol): string => {
364
+ const declaration = text(symbol)
365
+ if (symbol.kind.identifier === 'swift.init') {
366
+ if (/\bconvenience\b/.test(declaration)) return 'self.init()'
367
+ const parent = owner.get(symbol.identifier.precise)
368
+ return parent && superclass.has(parent) ? 'super.init()' : ''
369
+ }
370
+ const type =
371
+ symbol.kind.identifier.includes('property') ||
372
+ symbol.kind.identifier === 'swift.var'
373
+ ? /:\s*(.*?)\s*(?:\{|$)/.exec(declaration)?.[1]
374
+ : symbol.functionSignature?.returns?.map((fragment) => fragment.spelling).join('')
375
+ return type ? `return ${inertValue(type)}` : ''
376
+ }
377
+
334
378
  const lines: string[] = []
335
379
  const owners: string[] = []
336
380
  const omit = options.omit ?? new Map<string, string>()
@@ -426,7 +470,8 @@ export function emitSwiftStandin(
426
470
  : /\bResult</.test(core)
427
471
  ? measured?.resultBody
428
472
  : undefined
429
- const body = overloadBody ?? measured?.body ?? trap(path)
473
+ const body =
474
+ overloadBody ?? measured?.body ?? (options.inert ? inertBody(member) : trap(path))
430
475
  switch (member.kind.identifier) {
431
476
  case 'swift.property':
432
477
  case 'swift.type.property':
@@ -452,7 +497,7 @@ export function emitSwiftStandin(
452
497
  return
453
498
  }
454
499
  emit(
455
- `${head}${computed} { get${effects} { ${body} }${settable && !effects ? ` ${parsed.nonmutating ? 'nonmutating ' : ''}set { ${measured?.set ?? trap(path)} }` : ''} }`,
500
+ `${head}${computed} { get${effects} { ${body} }${settable && !effects ? ` ${parsed.nonmutating ? 'nonmutating ' : ''}set { ${measured?.set ?? (options.inert ? '' : trap(path))} }` : ''} }`,
456
501
  )
457
502
  return
458
503
  }
@@ -646,7 +691,7 @@ export function emitSwiftStandin(
646
691
  externalRoot(id) !== null
647
692
  )
648
693
  emit(
649
- `${inner}public required init?(coder: NSCoder) { ${trap(`${path}.init(coder:)`)} }`,
694
+ `${inner}public required init?(coder: NSCoder) { ${options.inert ? 'return nil' : trap(`${path}.init(coder:)`)} }`,
650
695
  )
651
696
  emit(`${indent}}`)
652
697
  return
@@ -814,7 +859,7 @@ export function emitSwiftStandin(
814
859
  .replace(/^(?:weak |unowned )/, '')
815
860
  .replace(/^((?:static )?)let /, '$1var ')
816
861
  const witness = `${owner}.${leaf(member)}`
817
- const body = trap(witness)
862
+ const body = options.inert ? inertBody(member) : trap(witness)
818
863
  if (
819
864
  member.kind.identifier === 'swift.property' ||
820
865
  member.kind.identifier === 'swift.type.property'
@@ -872,7 +917,7 @@ export function emitSwiftStandin(
872
917
  ? measured?.resultBody
873
918
  : undefined
874
919
  emit(
875
- `${attrs}public ${spelled} { ${overloadBody ?? measured?.body ?? trap(path)} }`,
920
+ `${attrs}public ${spelled} { ${overloadBody ?? measured?.body ?? (options.inert ? inertBody(symbol) : trap(path))} }`,
876
921
  )
877
922
  } else if (kind === 'swift.var') {
878
923
  const parsed = splitAccessors(text(symbol).replace(/^let /, 'var '))
@@ -882,7 +927,9 @@ export function emitSwiftStandin(
882
927
  // as `var` with no setter.
883
928
  const measured = behavior.get(path)
884
929
  if (measured) used.add(path)
885
- emit(`public ${parsed.decl} { ${measured?.body ?? trap(path)} }`)
930
+ emit(
931
+ `public ${parsed.decl} { ${measured?.body ?? (options.inert ? inertBody(symbol) : trap(path))} }`,
932
+ )
886
933
  } else skipped.push({ path, reason: `unhandled top-level ${kind}` })
887
934
  })
888
935
 
@@ -929,8 +976,9 @@ export function emitSwiftStandin(
929
976
  const imports = [...STANDIN_IMPORTS, ...(options.imports ?? [])]
930
977
  const banner = [
931
978
  `// stand-in for the binary ${options.module} module, generated from ${options.source}.`,
932
- `// do not hand-edit: unconfigured bodies trap because the vendor binary cannot run`,
933
- `// in peach; app native stubs may supply configured behavior.`,
979
+ options.inert
980
+ ? '// generated from public headers; native behavior is inert in peach.'
981
+ : '// unconfigured bodies trap because the vendor binary cannot run in peach.',
934
982
  '',
935
983
  ...imports.map((name) => `import ${name}`),
936
984
  '',