peachsim 0.1.1625 → 0.1.1640

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +15 -2
  3. package/cli/app-config.ts +18 -6
  4. package/cli/app-fonts.ts +37 -17
  5. package/cli/app-stubs.ts +284 -20
  6. package/cli/bridge-flow-runner.ts +66 -45
  7. package/cli/cloud-client.ts +60 -40
  8. package/cli/command-registry.ts +1 -0
  9. package/cli/commands/control.ts +283 -49
  10. package/cli/commands/doctor.ts +30 -189
  11. package/cli/commands/flow.ts +79 -11
  12. package/cli/commands/inspect/core.ts +10 -5
  13. package/cli/commands/inspect/settling.ts +14 -2
  14. package/cli/commands/inspect/shared.ts +9 -4
  15. package/cli/commands/inspect/wait-ready.ts +12 -0
  16. package/cli/commands/inspect.ts +91 -50
  17. package/cli/commands/install-desktop.ts +1 -0
  18. package/cli/commands/maestro-compare.ts +97 -38
  19. package/cli/commands/maestro.ts +9 -1
  20. package/cli/commands/preview.ts +1 -1
  21. package/cli/commands/record.ts +118 -48
  22. package/cli/commands/screenshot.ts +23 -1
  23. package/cli/commands/serve.ts +7 -2
  24. package/cli/commands/setup.ts +19 -42
  25. package/cli/commands/swift.ts +14 -3
  26. package/cli/commands/upload.ts +93 -48
  27. package/cli/desktop-companion.ts +102 -17
  28. package/cli/drivers/desktop.ts +14 -1
  29. package/cli/drivers/playwright-sim-host.ts +103 -45
  30. package/cli/drivers/playwright.ts +9 -8
  31. package/cli/drivers/types.ts +5 -0
  32. package/cli/maestro-accessibility.ts +1 -1
  33. package/cli/maestro-compare.ts +84 -12
  34. package/cli/main.ts +3 -2
  35. package/cli/outbound-endpoints.ts +20 -6
  36. package/cli/pause-aware-timeout.ts +106 -0
  37. package/cli/scene-video.ts +89 -0
  38. package/cli/ws-bridge.ts +37 -19
  39. package/detox/attempt-events.cjs +66 -5
  40. package/detox/attempt-events.d.cts +7 -0
  41. package/detox/expectations.ts +7 -3
  42. package/detox/index.ts +269 -170
  43. package/detox/motion-change.cjs +206 -21
  44. package/detox/motion-change.d.cts +39 -2
  45. package/detox/navigation-lifecycle.ts +35 -3
  46. package/dist-lib/agent-daemon-client.cjs +1 -1
  47. package/dist-lib/agent-events.cjs +1 -1
  48. package/dist-lib/attached-projects.cjs +1 -1
  49. package/dist-lib/auth/shared-session.cjs +1 -1
  50. package/dist-lib/backend-origin.cjs +8 -7
  51. package/dist-lib/beta.cjs +1 -1
  52. package/dist-lib/beta.mjs +1 -1
  53. package/dist-lib/bridge-constants.cjs +13 -1
  54. package/dist-lib/bridge-contract-input.cjs +1 -1
  55. package/dist-lib/bridge-contract-input.mjs +1 -1
  56. package/dist-lib/bridge-contract.cjs +14 -8
  57. package/dist-lib/bridge-contract.mjs +13 -7
  58. package/dist-lib/bundle-channel.cjs +11887 -7900
  59. package/dist-lib/capture-contract.cjs +1 -1
  60. package/dist-lib/capture-contract.mjs +1 -1
  61. package/dist-lib/cloud-contract.cjs +1 -1
  62. package/dist-lib/cloud-contract.mjs +1 -1
  63. package/dist-lib/cloud-test-args.cjs +1 -1
  64. package/dist-lib/cloud-test-args.mjs +1 -1
  65. package/dist-lib/cloud.cjs +1 -1
  66. package/dist-lib/cloud.mjs +1 -1
  67. package/dist-lib/config.cjs +6 -3
  68. package/dist-lib/config.mjs +6 -3
  69. package/dist-lib/detox/index.cjs +1379 -725
  70. package/dist-lib/dev-bundle-resolution.cjs +14 -2
  71. package/dist-lib/expo-swift-bundle.cjs +9341 -5462
  72. package/dist-lib/expo-swift-metadata.cjs +11 -7
  73. package/dist-lib/expo-swift-metadata.mjs +11 -7
  74. package/dist-lib/home-paths.cjs +32 -3
  75. package/dist-lib/host/bridge-host.cjs +50204 -7258
  76. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  77. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  78. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  79. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  80. package/dist-lib/host/websocket-proxy.cjs +1 -1
  81. package/dist-lib/index.cjs +3541 -448
  82. package/dist-lib/jump-to-source-babel.cjs +1 -1
  83. package/dist-lib/jump-to-source-native.cjs +1 -1
  84. package/dist-lib/kotlin-host.cjs +1 -1
  85. package/dist-lib/menu.cjs +1 -1
  86. package/dist-lib/menu.mjs +1 -1
  87. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  88. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  89. package/dist-lib/metro-production-bundle.cjs +1 -1
  90. package/dist-lib/metro-production-bundle.mjs +1 -1
  91. package/dist-lib/metro.cjs +2931 -104
  92. package/dist-lib/native-app-identity-project.cjs +289 -49
  93. package/dist-lib/native-app-identity-project.mjs +289 -49
  94. package/dist-lib/native-app-identity.cjs +77 -26
  95. package/dist-lib/native-app-identity.mjs +76 -26
  96. package/dist-lib/native-firebase-metadata.cjs +1 -1
  97. package/dist-lib/native-firebase-metadata.mjs +1 -1
  98. package/dist-lib/plist.cjs +1 -1
  99. package/dist-lib/plist.mjs +1 -1
  100. package/dist-lib/profiles.cjs +1 -1
  101. package/dist-lib/public-brand.cjs +1 -1
  102. package/dist-lib/react-native-host-modules.cjs +6 -2
  103. package/dist-lib/react-native-host-modules.mjs +6 -2
  104. package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
  105. package/dist-lib/sdk.cjs +27 -11
  106. package/dist-lib/sdk.mjs +27 -10
  107. package/dist-lib/skills.cjs +72201 -26208
  108. package/dist-lib/swift-compile.cjs +1007 -140
  109. package/dist-lib/swift-compile.mjs +1049 -174
  110. package/dist-lib/swift-package.cjs +12 -3
  111. package/dist-lib/swift-package.mjs +12 -3
  112. package/dist-lib/swift-submit.cjs +13 -5
  113. package/dist-lib/swift-submit.mjs +11 -5
  114. package/dist-lib/swift-toolchain.cjs +423 -33
  115. package/dist-lib/swift.cjs +1 -1
  116. package/dist-lib/vite-swift.cjs +8628 -5268
  117. package/dist-lib/vite.cjs +2859 -12
  118. package/dist-lib/worklet-executable.cjs +14 -2
  119. package/dist-lib/worklet-executable.mjs +14 -2
  120. package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
  121. package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
  122. package/package.json +1 -1
  123. package/scripts/dev-server-scanner.ts +2 -1
  124. package/skills/peach-perf/SKILL.md +11 -5
  125. package/skills/peach-setup/SKILL.md +16 -19
  126. package/src/backend-origin.ts +13 -6
  127. package/src/bridge-constants.ts +11 -0
  128. package/src/bridge-contract.ts +22 -34
  129. package/src/config.ts +15 -0
  130. package/src/desktop-host-delivery.ts +5 -1
  131. package/src/dev-bundle-resolution.ts +2 -1
  132. package/src/expo-manifest-body.ts +13 -0
  133. package/src/expo-swift-host.ts +196 -49
  134. package/src/expo-swift-ready.ts +78 -0
  135. package/src/expoKotlinContract.ts +4 -7
  136. package/src/expoSwiftContract.ts +17 -6
  137. package/src/home-paths.ts +48 -2
  138. package/src/host/bridge-host.ts +5 -2
  139. package/src/host/staged-host-files.ts +9 -0
  140. package/src/native-app-identity-project.ts +27 -6
  141. package/src/native-app-identity.ts +91 -4
  142. package/src/native-firebase-config.ts +209 -13
  143. package/src/nativeAppIconContract.ts +1 -4
  144. package/src/output-lock-stdio.ts +36 -0
  145. package/src/process-group.ts +25 -2
  146. package/src/react-native-bridge-modules.ts +431 -0
  147. package/src/react-native-host-modules.ts +8 -1
  148. package/src/settings-bundle.ts +1 -4
  149. package/src/swift-binary-modules.ts +2 -2
  150. package/src/swift-compile.ts +918 -109
  151. package/src/swift-compiler-digest.ts +14 -0
  152. package/src/swift-distribution.lock.json +24 -12
  153. package/src/swift-distribution.ts +103 -4
  154. package/src/swift-host.ts +180 -55
  155. package/src/swift-icu.ts +27 -13
  156. package/src/swift-objc-compile.ts +355 -0
  157. package/src/swift-objc-sdk.ts +821 -27
  158. package/src/swift-objc-standin.ts +148 -14
  159. package/src/swift-package-manifest.ts +60 -7
  160. package/src/swift-package.ts +15 -1
  161. package/src/swift-packages.ts +154 -81
  162. package/src/swift-standin-emit.ts +96 -15
  163. package/src/swift-standins.ts +530 -116
  164. package/src/swift-submit.ts +13 -5
  165. package/src/swift-toolchain.ts +500 -42
  166. package/src/vite-plugin.ts +12 -1
  167. package/src/worker-system-chunks.ts +11 -12
  168. package/src/worklet-executable.ts +19 -1
  169. package/src/xcode-project.ts +270 -30
@@ -47,7 +47,10 @@ import {
47
47
  isReactNativeEnvironmentSetupModule,
48
48
  } from '../../peach-engine/src/guest-module-overrides.ts'
49
49
  import { readPeachWarmManifest } from '../../peach-engine/src/peach-warm-manifest.ts'
50
- import { resolveSootsimBridgePort } from './bridge-constants.ts'
50
+ import {
51
+ assertPeachPortEnvironment,
52
+ resolveSootsimBridgePort,
53
+ } from './bridge-constants.ts'
51
54
  import { isRecord } from './is-record.ts'
52
55
  import {
53
56
  SHELL_BRIDGE_IDENTITY_PATH,
@@ -1321,6 +1324,7 @@ const viteBridgeProcesses = (globalThis.__sootsimViteBridgeProcesses ??= new Map
1321
1324
  // installs it explicitly and owns the one shared bridge; sootsim() never
1322
1325
  // does, so an isolated dev server cannot bind the shared default port.
1323
1326
  export function wsBridgePlugin(options: { ownedBridgePort?: number } = {}): Plugin {
1327
+ assertPeachPortEnvironment(process.env)
1324
1328
  if (
1325
1329
  options.ownedBridgePort !== undefined &&
1326
1330
  (!Number.isInteger(options.ownedBridgePort) ||
@@ -1652,6 +1656,13 @@ function sootsimConfigPlugin(
1652
1656
  return nativeLibraryStub ? { id: nativeLibraryStub } : null
1653
1657
  },
1654
1658
  },
1659
+ {
1660
+ name: 'peach-one-native-backend',
1661
+ resolveId(source: string, importer: string | undefined) {
1662
+ const backend = resolveOneNativeBackend(source, importer)
1663
+ return backend ? { id: backend } : null
1664
+ },
1665
+ },
1655
1666
  {
1656
1667
  name: 'sootsim-published-browser-internals',
1657
1668
  resolveId(source: string, importer: string | undefined) {
@@ -242,10 +242,16 @@ const linkedChunks = new Set();
242
242
  function chunkBody(url) {
243
243
  let body = chunkBodies.get(url);
244
244
  if (!body) {
245
- body = fetchWorkerChunk(url, { credentials: 'same-origin' }).then(response => {
245
+ body = (async () => {
246
+ if (deliveredChunkList && (await deliveredChunkList.promise).has(url)) {
247
+ const bytes = await deliveredChunk(url).promise;
248
+ deliveredChunks.delete(url);
249
+ return new TextDecoder().decode(bytes);
250
+ }
251
+ const response = await fetchWorkerChunk(url, { credentials: 'same-origin' });
246
252
  if (!response.ok) throw new Error(url + ': HTTP ' + response.status);
247
253
  return response.text();
248
- });
254
+ })();
249
255
  body.catch(() => chunkBodies.delete(url));
250
256
  chunkBodies.set(url, body);
251
257
  }
@@ -261,16 +267,9 @@ const importModule = specifier => import(specifier);
261
267
  System.importModule = importModule;
262
268
  System.instantiate = async function(url) {
263
269
  if (url === workerEntryUrl) return workerEntryRegistration;
264
- let source;
265
- if (deliveredChunkList && (await deliveredChunkList.promise).has(url)) {
266
- const body = await deliveredChunk(url).promise;
267
- deliveredChunks.delete(url);
268
- source = new TextDecoder().decode(body);
269
- } else {
270
- source = await chunkBody(url);
271
- chunkBodies.delete(url);
272
- linkedChunks.add(url);
273
- }
270
+ const source = await chunkBody(url);
271
+ chunkBodies.delete(url);
272
+ linkedChunks.add(url);
274
273
  let registration;
275
274
  // minified module chunks assume strict block-scoped function semantics.
276
275
  new Function('System', '"use strict";\\n' + source)({
@@ -92,6 +92,7 @@ interface Scope {
92
92
 
93
93
  type Binding =
94
94
  | { kind: 'value'; init: SyntaxNode; scope: Scope }
95
+ | { kind: 'uninitialized' }
95
96
  // `key` is set when the parameter is a destructured member of that argument.
96
97
  | { kind: 'param'; fn: SyntaxNode; index: number; key?: string }
97
98
  | { kind: 'opaque' }
@@ -106,6 +107,9 @@ interface InitCollection {
106
107
  writes: Array<{ name: string; scope: Scope; value?: SyntaxNode }>
107
108
  // bindings written after their declaration; their value is not static.
108
109
  reassigned: Set<Binding>
110
+ // rolldown hoists declarations out of esm initializers. a single plain
111
+ // assignment still identifies the literal; multiple writes remain opaque.
112
+ assigned: Map<Binding, { value: SyntaxNode; scope: Scope } | null>
109
113
  // names the parsed source never declares but writes exactly once with a plain
110
114
  // assignment. a bundler that hoists a module's declarations out of its
111
115
  // initializer leaves only that write in the initializer.
@@ -227,6 +231,8 @@ function enter(node: SyntaxNode, scope: Scope): Scope {
227
231
  const name = identifierName(id)
228
232
  if (name !== undefined && isNode(init))
229
233
  declare(target, name, { kind: 'value', init, scope })
234
+ else if (name !== undefined && init === null)
235
+ declare(target, name, { kind: 'uninitialized' })
230
236
  else declareOpaque(target, patternNames(id))
231
237
  }
232
238
  return scope
@@ -311,12 +317,18 @@ function collectInitData(root: SyntaxNode): InitCollection {
311
317
  immediate: new Map(),
312
318
  writes: [],
313
319
  reassigned: new Set(),
320
+ assigned: new Map(),
314
321
  free: new Map(),
315
322
  }
316
323
  visit(root, newScope(undefined, true), collection)
317
324
  for (const { name, scope, value } of collection.writes) {
318
325
  const binding = lookup(scope, name)
319
- if (binding) collection.reassigned.add(binding)
326
+ if (binding?.kind === 'uninitialized')
327
+ collection.assigned.set(
328
+ binding,
329
+ collection.assigned.has(binding) || !value ? null : { value, scope },
330
+ )
331
+ else if (binding) collection.reassigned.add(binding)
320
332
  else
321
333
  collection.free.set(
322
334
  name,
@@ -415,6 +427,12 @@ function resolveObject(
415
427
  : undefined
416
428
  }
417
429
  if (collection.reassigned.has(binding)) return undefined
430
+ if (binding.kind === 'uninitialized') {
431
+ const assigned = collection.assigned.get(binding)
432
+ return assigned
433
+ ? resolveObject(assigned.value, assigned.scope, collection, depth + 1)
434
+ : undefined
435
+ }
418
436
  if (binding.kind === 'value')
419
437
  return resolveObject(binding.init, binding.scope, collection, depth + 1)
420
438
  if (binding.kind === 'param') {
@@ -1,11 +1,14 @@
1
1
  // reads an xcode project's application target the way xcode builds it: the
2
2
  // swift sources and resources that are members of the target, the local
3
3
  // swift packages its products come from, the remote package products it links,
4
- // and the build settings that change how its swift compiles. never runs xcode
5
- // or evaluates a build phase; the project file is data: project.pbxproj, or
4
+ // and the build settings that change how its swift compiles. project opening
5
+ // prepares declared generated swift sources without running xcode. the project
6
+ // file is data: project.pbxproj, or
6
7
  // xcode 27.2's json project.xcproj, which xcode-json-project.ts reads into
7
8
  // the same object table.
8
9
 
10
+ import { execFileSync } from 'node:child_process'
11
+ import { createHash } from 'node:crypto'
9
12
  import fs from 'node:fs'
10
13
  import path from 'node:path'
11
14
  import plist from 'plist'
@@ -45,6 +48,19 @@ export interface XcodeModuleTarget {
45
48
  mainActorIsolation: boolean
46
49
  /** the compilation conditions the build configuration defines, as `-D` passes them. */
47
50
  defines: string[]
51
+ /** the resolved source iOS minimum, including for native package dependencies. */
52
+ iosDeploymentTarget: string | null
53
+ /** original implementation inputs and clang settings, before any declaration import. */
54
+ objc?: {
55
+ headers: string[]
56
+ prefixHeader: string | null
57
+ bridgingHeader: string | null
58
+ headerSearchPaths: string[]
59
+ frameworkSearchPaths: string[]
60
+ flags: string[]
61
+ cxxFlags: string[]
62
+ fileFlags: Record<string, string[]>
63
+ }
48
64
  }
49
65
 
50
66
  /** an application target, resolved to files on disk with everything it builds. */
@@ -72,8 +88,8 @@ export interface XcodeAppTarget extends XcodeModuleTarget {
72
88
  localPackageDirs: string[]
73
89
  /** prebuilt frameworks and xcframeworks the projects vendor and link, at their path when it resolves. */
74
90
  binaryFrameworks: { name: string; path: string | null }[]
75
- /** the Package.resolved pinning the remote packages' revisions, when resolved. */
76
- packageResolved: string | null
91
+ /** workspace pins, or local package locks when the workspace has no lock. */
92
+ packageResolved: string[]
77
93
  /** every application target in the workspace, for a chooser. */
78
94
  appTargets: string[]
79
95
  /** the build configuration every target's settings are read in. */
@@ -498,14 +514,17 @@ function expand(value: string, lookup: (name: string) => string): string {
498
514
  )
499
515
  }
500
516
 
517
+ const preparedSourcePhases = new Map<string, string>()
518
+
501
519
  // the swift sources and resources that are members of a target.
502
- // run script phases never execute here; their declared swift outputs are
503
- // compile inputs xcode generates before compiling, so a missing one names
504
- // the script instead of silently dropping the file.
520
+ // metadata-only reads never execute scripts. opening a project prepares the
521
+ // declared swift outputs with the project's own shell and build settings.
505
522
  function targetMembers(
506
523
  ref: TargetRef,
507
524
  settings: (key: string) => string,
508
- options: { requireSources?: boolean } = {},
525
+ options: { requireSources?: boolean; prepareGeneratedSources?: boolean },
526
+ settingKeys: Set<string>,
527
+ projectInputs: Set<string>,
509
528
  ): {
510
529
  sources: string[]
511
530
  resources: string[]
@@ -517,30 +536,137 @@ function targetMembers(
517
536
  const allSources = new Set<string>()
518
537
  const resources = new Set<string>()
519
538
  const scriptForOutput = new Map<string, string>()
520
- const noteScriptOutput = (raw: string, script: string) => {
521
- const expanded = expand(raw, settings)
522
- if (!expanded.endsWith('.swift')) return
523
- const absolute = path.isAbsolute(expanded)
524
- ? path.normalize(expanded)
525
- : path.resolve(project.projectDir, expanded)
526
- if (!scriptForOutput.has(absolute)) scriptForOutput.set(absolute, script)
527
- }
528
539
  for (const phaseId of list(target.buildPhases)) {
529
540
  const phase = project.object(phaseId)
530
541
  if (phase?.isa !== 'PBXShellScriptBuildPhase') continue
531
542
  const script = str(phase.name) ?? phaseId
532
- for (const raw of list(phase.outputPaths)) noteScriptOutput(raw, script)
533
- for (const listPath of list(phase.outputFileListPaths)) {
534
- const expandedList = expand(listPath, settings)
535
- const absoluteList = path.isAbsolute(expandedList)
536
- ? expandedList
537
- : path.resolve(project.projectDir, expandedList)
538
- if (!fs.existsSync(absoluteList)) continue
539
- for (const line of fs.readFileSync(absoluteList, 'utf8').split('\n')) {
540
- const trimmed = line.trim()
541
- if (trimmed === '' || trimmed.startsWith('#')) continue
542
- noteScriptOutput(trimmed, script)
543
+ const absolute = (raw: string) =>
544
+ path.resolve(project.projectDir, expand(raw, settings))
545
+ const inputs = list(phase.inputPaths).map(absolute)
546
+ const outputs = list(phase.outputPaths).map(absolute)
547
+ const fileLists = new Map<string, { file: string; entries: string[] }[]>()
548
+ for (const kind of ['INPUT', 'OUTPUT']) {
549
+ const lists = list(
550
+ kind === 'INPUT' ? phase.inputFileListPaths : phase.outputFileListPaths,
551
+ ).map((raw) => {
552
+ const file = absolute(raw)
553
+ if (!fs.existsSync(file)) {
554
+ return { file, entries: [] }
555
+ }
556
+ const entries = fs
557
+ .readFileSync(file, 'utf8')
558
+ .split(/\r?\n/)
559
+ .map((line) => line.trim())
560
+ .filter((line) => line !== '' && !line.startsWith('#'))
561
+ .map(absolute)
562
+ return { file, entries }
563
+ })
564
+ fileLists.set(kind, lists)
565
+ }
566
+ const allOutputs = [
567
+ ...outputs,
568
+ ...fileLists.get('OUTPUT')!.flatMap((item) => item.entries),
569
+ ]
570
+ for (const output of allOutputs)
571
+ if (output.endsWith('.swift') && !scriptForOutput.has(output))
572
+ scriptForOutput.set(output, script)
573
+ const inputFiles = [
574
+ ...inputs,
575
+ ...fileLists.get('INPUT')!.flatMap((item) => [item.file, ...item.entries]),
576
+ ...fileLists.get('OUTPUT')!.map((item) => item.file),
577
+ ]
578
+ if (allOutputs.some((file) => file.endsWith('.swift')))
579
+ for (const file of inputFiles) if (fs.existsSync(file)) projectInputs.add(file)
580
+ if (
581
+ options.prepareGeneratedSources &&
582
+ options.requireSources !== false &&
583
+ allOutputs.some((file) => file.endsWith('.swift'))
584
+ ) {
585
+ const phaseDir = path.join(settings('TARGET_TEMP_DIR'), `Script-${phaseId}`)
586
+ for (const lists of fileLists.values())
587
+ for (const { file } of lists)
588
+ if (!fs.existsSync(file))
589
+ throw new Error(
590
+ `${targetName}: Run Script "${script}" missing file list ${file}`,
591
+ )
592
+ const env = {
593
+ ...process.env,
594
+ ...Object.fromEntries(
595
+ [...settingKeys]
596
+ .filter((key) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
597
+ .map((key) => [key, settings(key)]),
598
+ ),
599
+ }
600
+ const declared = { INPUT: inputs, OUTPUT: outputs }
601
+ for (const kind of ['INPUT', 'OUTPUT'] satisfies (keyof typeof declared)[]) {
602
+ env[`SCRIPT_${kind}_FILE_COUNT`] = String(declared[kind].length)
603
+ declared[kind].forEach((file, index) => {
604
+ env[`SCRIPT_${kind}_FILE_${index}`] = file
605
+ })
606
+ const lists = fileLists.get(kind)!
607
+ env[`SCRIPT_${kind}_FILE_LIST_COUNT`] = String(lists.length)
608
+ lists.forEach((_item, index) => {
609
+ env[`SCRIPT_${kind}_FILE_LIST_${index}`] = path.join(
610
+ phaseDir,
611
+ `${kind}-${index}.xcfilelist`,
612
+ )
613
+ })
614
+ }
615
+ const signature = createHash('sha256').update(
616
+ JSON.stringify([phase, [...settingKeys].map((key) => [key, settings(key)])]),
617
+ )
618
+ for (const file of inputFiles) {
619
+ signature.update(file)
620
+ if (fs.existsSync(file)) {
621
+ const stat = fs.statSync(file)
622
+ signature.update(`${stat.mtimeMs}:${stat.ctimeMs}:${stat.size}`)
623
+ } else signature.update('missing')
624
+ }
625
+ const stamp = signature.digest('hex')
626
+ const key = `${project.projectFile}:${targetId}:${phaseId}:${settings('CONFIGURATION')}`
627
+ if (
628
+ preparedSourcePhases.get(key) === stamp &&
629
+ allOutputs.every((file) => fs.existsSync(file))
630
+ )
631
+ continue
632
+ fs.mkdirSync(phaseDir, { recursive: true })
633
+ fs.mkdirSync(settings('BUILT_PRODUCTS_DIR'), { recursive: true })
634
+ fs.mkdirSync(settings('DERIVED_FILE_DIR'), { recursive: true })
635
+ for (const [kind, lists] of fileLists)
636
+ lists.forEach((item, index) => {
637
+ fs.writeFileSync(
638
+ env[`SCRIPT_${kind}_FILE_LIST_${index}`]!,
639
+ `${item.entries.join('\n')}\n`,
640
+ )
641
+ })
642
+ try {
643
+ execFileSync(
644
+ str(phase.shellPath) ?? '/bin/sh',
645
+ ['-c', str(phase.shellScript) ?? ''],
646
+ {
647
+ cwd: project.projectDir,
648
+ env,
649
+ stdio: ['ignore', 'pipe', 'pipe'],
650
+ timeout: 120_000,
651
+ },
652
+ )
653
+ } catch (error) {
654
+ const output =
655
+ error instanceof Error
656
+ ? `${error.message}\n${'stdout' in error ? String(error.stdout) : ''}${'stderr' in error ? String(error.stderr) : ''}`
657
+ : String(error)
658
+ throw new Error(
659
+ `${targetName}: Run Script "${script}" failed preparing generated Swift sources:\n${output}`,
660
+ )
543
661
  }
662
+ const missing = allOutputs.filter(
663
+ (file) => file.endsWith('.swift') && !fs.existsSync(file),
664
+ )
665
+ if (missing.length > 0)
666
+ throw new Error(
667
+ `${targetName}: Run Script "${script}" did not produce ${missing.join(', ')}`,
668
+ )
669
+ preparedSourcePhases.set(key, stamp)
544
670
  }
545
671
  }
546
672
  visitXcodeTargetMembers(
@@ -769,7 +895,7 @@ export function readXcodeAppTarget(
769
895
  input: string,
770
896
  targetName?: string,
771
897
  configurationName?: string,
772
- options: { requireSources?: boolean } = {},
898
+ options: { requireSources?: boolean; prepareGeneratedSources?: boolean } = {},
773
899
  ): XcodeAppTarget {
774
900
  // canonical, so every path returned agrees with the host's realpath checks
775
901
  const xcodeprojOrWorkspace = fs.realpathSync(input)
@@ -881,22 +1007,112 @@ export function readXcodeAppTarget(
881
1007
  SRCROOT: ref.project.projectDir,
882
1008
  SOURCE_ROOT: ref.project.projectDir,
883
1009
  PROJECT_DIR: ref.project.projectDir,
1010
+ SYMROOT: path.join(ref.project.projectDir, '.build/peach-xcode/Build/Products'),
1011
+ BUILD_DIR: '$(SYMROOT)',
1012
+ OBJROOT: path.join(
1013
+ ref.project.projectDir,
1014
+ '.build/peach-xcode/Build/Intermediates.noindex',
1015
+ ),
1016
+ CONFIGURATION_BUILD_DIR:
1017
+ '$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)',
1018
+ BUILT_PRODUCTS_DIR: '$(CONFIGURATION_BUILD_DIR)',
1019
+ TARGET_BUILD_DIR: '$(CONFIGURATION_BUILD_DIR)',
1020
+ PROJECT_TEMP_DIR: '$(OBJROOT)/$(PROJECT_NAME).build',
1021
+ CONFIGURATION_TEMP_DIR:
1022
+ '$(PROJECT_TEMP_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)',
1023
+ TARGET_TEMP_DIR: '$(CONFIGURATION_TEMP_DIR)/$(TARGET_NAME).build',
1024
+ DERIVED_FILE_DIR: '$(TARGET_TEMP_DIR)/DerivedSources',
1025
+ DERIVED_FILES_DIR: '$(DERIVED_FILE_DIR)',
1026
+ PLATFORM_NAME: 'iphonesimulator',
1027
+ EFFECTIVE_PLATFORM_NAME: '-iphonesimulator',
1028
+ CURRENT_ARCH: 'arm64',
1029
+ ARCHS: 'arm64',
1030
+ ACTION: 'build',
884
1031
  DEVELOPMENT_LANGUAGE: str(ref.project.project.developmentRegion) ?? 'en',
885
1032
  },
886
1033
  options,
887
1034
  )
888
1035
  for (const file of read) xcconfigs.add(file)
889
1036
  const swiftVersion = Number.parseFloat(settings('SWIFT_VERSION') || '5')
1037
+ const members = targetMembers(ref, settings, options, keys, xcconfigs)
1038
+ const implementations = members.allSources.filter((file) =>
1039
+ /\.(c|cc|cpp|cxx|m|mm)$/.test(file),
1040
+ )
1041
+ const absolute = (file: string) => path.resolve(ref.project.projectDir, file)
1042
+ const fileFlags: Record<string, string[]> = {}
1043
+ for (const phaseId of list(ref.target.buildPhases)) {
1044
+ const phase = ref.project.object(phaseId)
1045
+ if (phase?.isa !== 'PBXSourcesBuildPhase') continue
1046
+ for (const id of list(phase.files)) {
1047
+ const file = ref.project.object(id)
1048
+ const resolved = ref.project.resolvePath(str(file?.fileRef) ?? '')
1049
+ if (resolved && implementations.includes(resolved))
1050
+ fileFlags[resolved] = settingList(
1051
+ expand(
1052
+ str(isDict(file?.settings) ? file.settings.COMPILER_FLAGS : undefined) ??
1053
+ '',
1054
+ settings,
1055
+ ),
1056
+ )
1057
+ }
1058
+ }
890
1059
  return {
891
1060
  settings,
892
1061
  keys,
893
1062
  module: {
894
1063
  name,
895
1064
  moduleName: applyOperator(settings('PRODUCT_MODULE_NAME'), 'c99extidentifier'),
896
- ...targetMembers(ref, settings, options),
1065
+ ...members,
897
1066
  languageMode: swiftVersion >= 6 ? 6 : 5,
898
1067
  mainActorIsolation: settings('SWIFT_DEFAULT_ACTOR_ISOLATION') === 'MainActor',
899
1068
  defines: compilationConditions(settings),
1069
+ iosDeploymentTarget: settings('IPHONEOS_DEPLOYMENT_TARGET') || null,
1070
+ ...(implementations.some((file) => /\.mm?$/.test(file))
1071
+ ? {
1072
+ objc: {
1073
+ headers: Object.keys(ref.project.objects).flatMap((id) => {
1074
+ const file = ref.project.resolvePath(id)
1075
+ return file && /\.(h|hh|hpp|hxx|pch)$/.test(file) && fs.existsSync(file)
1076
+ ? [file]
1077
+ : []
1078
+ }),
1079
+ prefixHeader: settings('GCC_PREFIX_HEADER')
1080
+ ? absolute(settings('GCC_PREFIX_HEADER'))
1081
+ : null,
1082
+ bridgingHeader: settings('SWIFT_OBJC_BRIDGING_HEADER')
1083
+ ? absolute(settings('SWIFT_OBJC_BRIDGING_HEADER'))
1084
+ : null,
1085
+ headerSearchPaths: [
1086
+ ...settingList(settings('HEADER_SEARCH_PATHS')),
1087
+ ...settingList(settings('USER_HEADER_SEARCH_PATHS')),
1088
+ ].map(absolute),
1089
+ frameworkSearchPaths: settingList(settings('FRAMEWORK_SEARCH_PATHS')).map(
1090
+ absolute,
1091
+ ),
1092
+ flags: [
1093
+ ...(settings('CLANG_ENABLE_OBJC_ARC') === 'YES' ? ['-fobjc-arc'] : []),
1094
+ ...(settings('CLANG_ENABLE_OBJC_WEAK') === 'YES'
1095
+ ? ['-fobjc-weak']
1096
+ : []),
1097
+ ...(settings('CLANG_ENABLE_MODULES') === 'YES' ? ['-fmodules'] : []),
1098
+ ...(settings('GCC_C_LANGUAGE_STANDARD')
1099
+ ? [`-std=${settings('GCC_C_LANGUAGE_STANDARD')}`]
1100
+ : []),
1101
+ ...settingList(settings('GCC_PREPROCESSOR_DEFINITIONS')).map(
1102
+ (value) => `-D${value}`,
1103
+ ),
1104
+ ...settingList(settings('OTHER_CFLAGS')),
1105
+ ],
1106
+ cxxFlags: [
1107
+ ...(settings('CLANG_CXX_LANGUAGE_STANDARD')
1108
+ ? [`-std=${settings('CLANG_CXX_LANGUAGE_STANDARD')}`]
1109
+ : []),
1110
+ ...settingList(settings('OTHER_CPLUSPLUSFLAGS')),
1111
+ ],
1112
+ fileFlags,
1113
+ },
1114
+ }
1115
+ : {}),
900
1116
  } satisfies XcodeModuleTarget,
901
1117
  }
902
1118
  }
@@ -967,6 +1183,23 @@ export function readXcodeAppTarget(
967
1183
  ...projects.flatMap((project) =>
968
1184
  Object.entries(project.objects).flatMap(([id, entry]) => {
969
1185
  if (!isDict(entry)) return []
1186
+ if (entry.isa === 'PBXFileSystemSynchronizedRootGroup') {
1187
+ const folder = project.resolvePath(id)
1188
+ const pending = folder ? [folder] : []
1189
+ const packages: string[] = []
1190
+ while (pending.length > 0) {
1191
+ const dir = pending.pop()!
1192
+ if (fs.existsSync(path.join(dir, 'Package.swift'))) {
1193
+ packages.push(dir)
1194
+ continue
1195
+ }
1196
+ if (!isDirectory(dir)) continue
1197
+ for (const child of fs.readdirSync(dir, { withFileTypes: true }))
1198
+ if (child.isDirectory() && !child.name.startsWith('.'))
1199
+ pending.push(path.join(dir, child.name))
1200
+ }
1201
+ return packages
1202
+ }
970
1203
  const candidate =
971
1204
  entry.isa === 'XCLocalSwiftPackageReference'
972
1205
  ? path.resolve(project.projectDir, str(entry.relativePath) ?? '')
@@ -1046,6 +1279,13 @@ export function readXcodeAppTarget(
1046
1279
  'Package.resolved',
1047
1280
  )
1048
1281
 
1282
+ const packageResolved = fs.existsSync(resolvedFile)
1283
+ ? [fs.realpathSync(resolvedFile)]
1284
+ : [...new Set(localPackageDirs.map((dir) => path.join(dir, 'Package.resolved')))]
1285
+ .filter((file) => fs.existsSync(file))
1286
+ .map((file) => fs.realpathSync(file))
1287
+ .sort()
1288
+
1049
1289
  // the app is last in its own dependency order
1050
1290
  const used = order.map((ref) => ref.project)
1051
1291
  return {
@@ -1074,7 +1314,7 @@ export function readXcodeAppTarget(
1074
1314
  binaryFrameworks: [...binaryFrameworks]
1075
1315
  .map(([name, file]) => ({ name, path: file }))
1076
1316
  .sort((left, right) => left.name.localeCompare(right.name)),
1077
- packageResolved: fs.existsSync(resolvedFile) ? resolvedFile : null,
1317
+ packageResolved,
1078
1318
  appTargets: appTargetNames,
1079
1319
  configuration,
1080
1320
  settings,