peachsim 0.1.2221 → 0.1.2223

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 (127) hide show
  1. package/cli/commands/compat.ts +4 -4
  2. package/cli/commands/report-issue.ts +1 -1
  3. package/cli/runtime-compat.ts +7 -2
  4. package/dist-lib/agent-daemon-client.cjs +1 -1
  5. package/dist-lib/agent-events.cjs +1 -1
  6. package/dist-lib/attached-projects.cjs +1 -1
  7. package/dist-lib/auth/shared-session.cjs +1 -1
  8. package/dist-lib/backend-origin.cjs +1 -1
  9. package/dist-lib/beta.cjs +1 -1
  10. package/dist-lib/beta.mjs +1 -1
  11. package/dist-lib/bridge-constants.cjs +1 -1
  12. package/dist-lib/bridge-contract-input.cjs +1 -1
  13. package/dist-lib/bridge-contract-input.mjs +1 -1
  14. package/dist-lib/bridge-contract.cjs +1 -1
  15. package/dist-lib/bridge-contract.mjs +1 -1
  16. package/dist-lib/bundle-channel.cjs +461 -369
  17. package/dist-lib/capture-contract.cjs +1 -1
  18. package/dist-lib/capture-contract.mjs +1 -1
  19. package/dist-lib/cloud-contract.cjs +1 -1
  20. package/dist-lib/cloud-contract.mjs +1 -1
  21. package/dist-lib/cloud-test-args.cjs +1 -1
  22. package/dist-lib/cloud-test-args.mjs +1 -1
  23. package/dist-lib/cloud.cjs +1 -1
  24. package/dist-lib/cloud.mjs +1 -1
  25. package/dist-lib/config.cjs +1 -1
  26. package/dist-lib/config.mjs +1 -1
  27. package/dist-lib/detox/index.cjs +3 -2
  28. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  29. package/dist-lib/expo-swift-bundle.cjs +458 -367
  30. package/dist-lib/expo-swift-metadata.cjs +1 -1
  31. package/dist-lib/expo-swift-metadata.mjs +1 -1
  32. package/dist-lib/home-paths.cjs +1 -1
  33. package/dist-lib/host/bridge-host.cjs +681 -562
  34. package/dist-lib/host/fetch-proxy-handler.cjs +10 -7
  35. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  36. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  37. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  38. package/dist-lib/host/websocket-proxy.cjs +1 -1
  39. package/dist-lib/index.cjs +8232 -7815
  40. package/dist-lib/jump-to-source-babel.cjs +1 -1
  41. package/dist-lib/jump-to-source-native.cjs +1 -1
  42. package/dist-lib/kotlin-host.cjs +1 -1
  43. package/dist-lib/menu.cjs +1 -1
  44. package/dist-lib/menu.mjs +1 -1
  45. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  46. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  47. package/dist-lib/metro-production-bundle.cjs +1 -1
  48. package/dist-lib/metro-production-bundle.mjs +1 -1
  49. package/dist-lib/metro.cjs +8146 -7737
  50. package/dist-lib/native-app-identity-project.cjs +331 -13
  51. package/dist-lib/native-app-identity-project.mjs +331 -13
  52. package/dist-lib/native-app-identity.cjs +16 -3
  53. package/dist-lib/native-app-identity.mjs +16 -3
  54. package/dist-lib/native-firebase-metadata.cjs +1 -1
  55. package/dist-lib/native-firebase-metadata.mjs +1 -1
  56. package/dist-lib/pdfium-raster.cjs +1 -1
  57. package/dist-lib/pdfium-raster.mjs +1 -1
  58. package/dist-lib/plist.cjs +1 -1
  59. package/dist-lib/plist.mjs +1 -1
  60. package/dist-lib/profiles.cjs +1 -1
  61. package/dist-lib/public-brand.cjs +1 -1
  62. package/dist-lib/public-origins-lib-esm.json +25 -24
  63. package/dist-lib/public-origins-lib.json +99 -96
  64. package/dist-lib/react-native-host-modules.cjs +1 -1
  65. package/dist-lib/react-native-host-modules.mjs +1 -1
  66. package/dist-lib/reap-browser.cjs +1 -1
  67. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  68. package/dist-lib/sdk.cjs +1 -1
  69. package/dist-lib/sdk.mjs +1 -1
  70. package/dist-lib/skills.cjs +518 -408
  71. package/dist-lib/swift-compile.cjs +69 -36
  72. package/dist-lib/swift-compile.mjs +69 -36
  73. package/dist-lib/swift-package.cjs +1 -1
  74. package/dist-lib/swift-package.mjs +1 -1
  75. package/dist-lib/swift-submit.cjs +1 -1
  76. package/dist-lib/swift-submit.mjs +1 -1
  77. package/dist-lib/swift-toolchain.cjs +1 -1
  78. package/dist-lib/swift.cjs +1 -1
  79. package/dist-lib/vite-swift.cjs +143 -56
  80. package/dist-lib/vite.cjs +2930 -2800
  81. package/dist-lib/worklet-executable.cjs +1 -1
  82. package/dist-lib/worklet-executable.mjs +1 -1
  83. package/dist-types/packages/peach-compat/src/stubs/native-seams/react-native-cookies.d.ts +2 -1
  84. package/dist-types/packages/peach-engine/src/cli-companion.d.ts +1 -0
  85. package/dist-types/packages/peach-engine/src/cli-host-terser.d.ts +7 -0
  86. package/dist-types/packages/peach-engine/src/engine/glass-material.d.ts +1 -1
  87. package/dist-types/packages/peach-engine/src/engine/liquid-glass.d.ts +1 -0
  88. package/dist-types/packages/peach-engine/src/engine/native-layout.d.ts +1 -0
  89. package/dist-types/packages/peach-engine/src/engine/node.d.ts +5 -0
  90. package/dist-types/packages/peach-engine/src/engine/scene-table.d.ts +8 -1
  91. package/dist-types/packages/peach-engine/src/engine/types.d.ts +3 -0
  92. package/dist-types/packages/peach-engine/src/engine/yoga-layout.d.ts +1 -1
  93. package/dist-types/packages/peach-engine/src/remote-stream/cloud-display.d.ts +2 -2
  94. package/dist-types/packages/peach-engine/src/render-worker/shell-tree-wire.d.ts +1 -2
  95. package/dist-types/packages/peach-globals/src/browser-object-url.d.ts +10 -0
  96. package/dist-types/packages/peach-kit/src/internal.d.ts +1 -0
  97. package/dist-types/packages/peach-kit/src/ios-tint.d.ts +1 -0
  98. package/dist-types/packages/peach-kit/src/transitions/screen-transition-math.d.ts +25 -0
  99. package/kotlin/sdk.tar.gz +0 -0
  100. package/package.json +1 -1
  101. package/runtime-companion.json +1 -1
  102. package/sdk/ios/System/Library/Frameworks/SceneKit.framework/Headers/SCNAnimation.h +4 -4
  103. package/sdk/ios/System/Library/Frameworks/SceneKit.framework/Headers/SCNCameraController.h +1 -1
  104. package/sdk/ios/System/Library/Frameworks/SceneKit.framework/Headers/SCNConstraint.h +5 -5
  105. package/sdk/ios/System/Library/Frameworks/SceneKit.framework/Headers/SCNGeometry.h +1 -1
  106. package/sdk/ios/System/Library/Frameworks/SceneKit.framework/Headers/SCNLevelOfDetail.h +1 -1
  107. package/sdk/ios/System/Library/Frameworks/SceneKit.framework/Headers/SCNParametricGeometry.h +9 -9
  108. package/sdk/ios/System/Library/Frameworks/SceneKit.framework/Headers/SCNParticleSystem.h +1 -1
  109. package/sdk/ios/System/Library/Frameworks/SceneKit.framework/Headers/SCNPhysicsBehavior.h +7 -7
  110. package/sdk/ios/System/Library/Frameworks/SceneKit.framework/Headers/SCNRenderer.h +1 -1
  111. package/sdk/ios/System/Library/Frameworks/SceneKit.framework/Headers/SCNShadable.h +1 -1
  112. package/sdk/ios/System/Library/Frameworks/SceneKit.framework/Headers/SCNTechnique.h +1 -1
  113. package/sdk/ios/System/Library/Frameworks/UIKit.framework/Headers/UISegmentedControl.h +1 -1
  114. package/sdk/ios/System/Library/Frameworks/UIKit.framework/Headers/UITabBar.h +1 -1
  115. package/src/cli-runtime.ts +1 -0
  116. package/src/conformance-pixel-comparison.ts +16 -3
  117. package/src/expo-native-workspace.ts +14 -10
  118. package/src/expo-workspace-exclusion.ts +10 -7
  119. package/src/react-native-autolinking.ts +2 -1
  120. package/src/runtime-delivery.ts +7 -0
  121. package/src/swift-compile.ts +51 -35
  122. package/src/swift-coverage/xcode-assets.ts +4 -2
  123. package/src/swift-host.ts +53 -14
  124. package/src/swift-root-lock.ts +27 -4
  125. package/src/wasm-optimize.ts +5 -1
  126. package/src/xcode-project.ts +26 -5
  127. package/src/xcode-target-members.ts +25 -3
@@ -7,7 +7,7 @@ import path from 'node:path'
7
7
  import { promisify } from 'node:util'
8
8
  import { detectPackageManager } from '../cli/commands/detect'
9
9
  import { teamMachineInstalled } from './admitted-mac-build'
10
- import { workspaceExclusion } from './expo-workspace-exclusion'
10
+ import { workspaceExclusion, type WorkspaceExclusion } from './expo-workspace-exclusion'
11
11
  import { cacheDir } from './home-paths'
12
12
  import { failedOutputLine } from './native-failure-output'
13
13
  import { requireNativeGit } from './native-git'
@@ -49,7 +49,7 @@ export function reactNativePodsEnvironment() {
49
49
  function workspacePackageManager(
50
50
  root: string,
51
51
  declared: unknown,
52
- excluded: (relative: string) => boolean,
52
+ excluded: WorkspaceExclusion,
53
53
  ): string {
54
54
  const manager = typeof declared === 'string' ? declared : detectPackageManager(root)
55
55
  const locks = [
@@ -116,7 +116,7 @@ export async function verifyExpoWorkspaceCopy(app: string, work: string) {
116
116
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
117
117
  const file = path.join(directory, entry.name)
118
118
  const relative = path.relative(root, file)
119
- if (excluded(relative)) continue
119
+ if (excluded(relative, entry.isDirectory())) continue
120
120
  const copy = path.join(work, relative)
121
121
  if (entry.isDirectory()) walk(file)
122
122
  else if (entry.isSymbolicLink()) {
@@ -212,7 +212,7 @@ async function hashWorkspaceInputs(
212
212
  hash: string
213
213
  // the subset of inputs that decides the prepared workspace
214
214
  nativeHash: string
215
- excluded: (relative: string) => boolean
215
+ excluded: WorkspaceExclusion
216
216
  }> {
217
217
  const root = findWorkspaceRoot(app)
218
218
  const excluded = await workspaceExclusion(
@@ -286,7 +286,7 @@ async function hashWorkspaceInputs(
286
286
  signal?.throwIfAborted()
287
287
  const file = path.join(directory, entry.name)
288
288
  const relative = path.relative(root, file)
289
- if (excluded(relative)) continue
289
+ if (excluded(relative, entry.isDirectory())) continue
290
290
  hash.update(`${relative}\0`)
291
291
  if (entry.isSymbolicLink()) {
292
292
  const link = `link\0${await fs.promises.readlink(file)}\0`
@@ -446,7 +446,7 @@ function syncChangedFiles(
446
446
  root: string,
447
447
  work: string,
448
448
  appRelative: string,
449
- excluded: (relative: string) => boolean,
449
+ excluded: WorkspaceExclusion,
450
450
  installed: boolean,
451
451
  ) {
452
452
  const copyIfNewer = (file: string, relative: string, newerOnly: boolean) => {
@@ -464,8 +464,12 @@ function syncChangedFiles(
464
464
  )
465
465
  return
466
466
  fs.mkdirSync(path.dirname(target), { recursive: true })
467
- fs.copyFileSync(file, target)
468
- fs.utimesSync(target, source.atime, source.mtime)
467
+ // a tool already running from the copy (the autolinker) must never read a
468
+ // truncated script, so the new bytes land by rename
469
+ const staged = `${target}.${process.pid}.peach-sync`
470
+ fs.copyFileSync(file, staged)
471
+ fs.utimesSync(staged, source.atime, source.mtime)
472
+ fs.renameSync(staged, target)
469
473
  }
470
474
  const walkInstalled = (directory: string) => {
471
475
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
@@ -490,7 +494,7 @@ function syncChangedFiles(
490
494
  }
491
495
  // the app's ios project is prebuild output, not a source
492
496
  if (
493
- excluded(relative) ||
497
+ excluded(relative, entry.isDirectory()) ||
494
498
  entry.isSymbolicLink() ||
495
499
  (!sourceIos && relative === path.join(appRelative, 'ios'))
496
500
  )
@@ -640,7 +644,7 @@ function prepare(app: string, stage: 'project' | 'workspace', signal?: AbortSign
640
644
  async function copyInstalledWorkspace(
641
645
  root: string,
642
646
  destination: string,
643
- excluded: (relative: string) => boolean,
647
+ excluded: WorkspaceExclusion,
644
648
  ) {
645
649
  const installed: string[] = []
646
650
  await fs.promises.cp(root, destination, {
@@ -4,6 +4,8 @@ import path from 'node:path'
4
4
  import { promisify } from 'node:util'
5
5
  import ignore from 'ignore'
6
6
 
7
+ export type WorkspaceExclusion = (relative: string, isDirectory?: boolean) => boolean
8
+
7
9
  // eas build archives a repository without its gitignored paths, so the prepare
8
10
  // neither copies nor hashes them: files a dev server generates into an ignored
9
11
  // directory never invalidate the prepared workspace.
@@ -13,7 +15,7 @@ export async function workspaceExclusion(
13
15
  root: string,
14
16
  portable = false,
15
17
  linkedPackages: string[] = [],
16
- ) {
18
+ ): Promise<WorkspaceExclusion> {
17
19
  const listed = await promisify(execFile)(
18
20
  'git',
19
21
  ['ls-files', '-z', '--others', '--ignored', '--exclude-standard', '--directory'],
@@ -34,8 +36,8 @@ export async function workspaceExclusion(
34
36
  .filter(Boolean)
35
37
  .map((entry) => entry.replace(/\/$/, '')),
36
38
  )
37
- const ignoreFiles = new Map<string, ReturnType<typeof ignore>>()
38
- return (relative: string) => {
39
+ const ignoreFiles = new Map<string, ReturnType<typeof ignore> | null>()
40
+ return (relative, knownDirectory) => {
39
41
  const parts = relative.split(path.sep)
40
42
  const linked = linkedPackages.some((directory) =>
41
43
  relative.startsWith(directory + path.sep),
@@ -47,16 +49,17 @@ export async function workspaceExclusion(
47
49
  }
48
50
  if (portable && relative) {
49
51
  let excluded = false
50
- const isDirectory = fs.lstatSync(path.join(root, relative)).isDirectory()
52
+ const isDirectory =
53
+ knownDirectory ?? fs.lstatSync(path.join(root, relative)).isDirectory()
51
54
  for (let index = 0; index < parts.length; index++) {
52
55
  const directory = parts.slice(0, index).join(path.sep)
53
56
  let rules = ignoreFiles.get(directory)
54
- if (!rules) {
55
- rules = ignore()
57
+ if (rules === undefined) {
56
58
  const file = path.join(root, directory, '.gitignore')
57
- if (fs.existsSync(file)) rules.add(fs.readFileSync(file, 'utf8'))
59
+ rules = fs.existsSync(file) ? ignore().add(fs.readFileSync(file, 'utf8')) : null
58
60
  ignoreFiles.set(directory, rules)
59
61
  }
62
+ if (!rules) continue
60
63
  const item = parts.slice(index).join('/')
61
64
  const result = rules.test(isDirectory ? `${item}/` : item)
62
65
  if (result.ignored) excluded = true
@@ -29,6 +29,7 @@ async function cliAdvertises(
29
29
  appRoot: string,
30
30
  signal?: AbortSignal,
31
31
  ): Promise<boolean> {
32
+ const started = Date.now()
32
33
  const { stdout, stderr } = await run('node', [cli, ...args], {
33
34
  cwd: appRoot,
34
35
  signal,
@@ -38,7 +39,7 @@ async function cliAdvertises(
38
39
  })
39
40
  if (!stdout.trim())
40
41
  throw new Error(
41
- `${cli} ${args.join(' ')} returned empty help output${stderr.trim() ? `: ${stderr.trim()}` : ''}`,
42
+ `${cli} ${args.join(' ')} exited 0 after ${Date.now() - started}ms with empty help output${stderr.trim() ? `: ${stderr.trim()}` : ''}`,
42
43
  )
43
44
  return stdout.includes(token)
44
45
  }
@@ -39,6 +39,13 @@ export const RNX_RUNTIME_PRODUCT: RuntimeProduct = {
39
39
  // vite/metro plugins serve, so its absence means the tarball is not one.
40
40
  installedMarker: 'index.html',
41
41
  verifyInstall: ensureCliCompanion,
42
+ // canvaskit's ganesh backend is a runtime pack because a browser with WebGPU
43
+ // never runs it, but linux and CI browsers usually have no GPU and always do
44
+ afterInstall: async (dir) => {
45
+ if (process.platform !== 'linux' && !process.env.CI) return
46
+ const { ensureRuntimePackFor } = await import('./runtime-packs.ts')
47
+ await ensureRuntimePackFor(dir, '/engine/canvaskit-ganesh.wasm')
48
+ },
42
49
  env: {
43
50
  cdnOrigin: 'PEACH_CDN_ORIGIN',
44
51
  channel: 'PEACH_RUNTIME_CHANNEL',
@@ -140,7 +140,7 @@ import {
140
140
  SWIFT_FRAMEWORK_PRODUCT_FILE,
141
141
  swiftFrameworkProductSchema,
142
142
  } from './swiftRuntimeProductContract'
143
- import { optimizeWasm, WASM_OPTIMIZE_IDENTITY } from './wasm-optimize'
143
+ import { KEEP_WASM_NAMES, optimizeWasm, WASM_OPTIMIZE_IDENTITY } from './wasm-optimize'
144
144
  import { writeBuildFile } from './write-build-file'
145
145
  import { writeXcodeHeaderSearch } from './xcode-header-search'
146
146
  import type { SwiftImageUpload } from './swift-submit'
@@ -839,7 +839,8 @@ const OPTIONAL_FRAMEWORK_DEPENDENCIES: Record<string, string[]> = {
839
839
  PeachCFNetwork: [],
840
840
  JavaScriptCore: ['PeachJavaScriptCore'],
841
841
  PeachJavaScriptCore: [],
842
- GameplayKit: [],
842
+ // GameplayKit names SpriteKit and SceneKit nodes, as on iOS
843
+ GameplayKit: ['SpriteKit', 'SceneKit', 'PeachBox2D', 'simd', 'GLKit'],
843
844
  simd: [],
844
845
  ModelIO: ['simd'],
845
846
  GLKit: ['ModelIO'],
@@ -1596,14 +1597,25 @@ export function swiftTargetDependencies(
1596
1597
  }
1597
1598
 
1598
1599
  const ONE_PRODUCT = '.product(name: "One", package: "sootsim-swift")'
1599
- const SPRITEKIT_PRODUCT = '.product(name: "SpriteKit", package: "sootsim-swift")'
1600
- // SpriteKit is an optional family (plans/peach/spritekit.md): it links only
1601
- // into an app whose sources import it, so no other artifact carries it.
1602
- const importsSpriteKit = (files: Record<string, string>): boolean =>
1603
- Object.entries(files).some(
1604
- ([file, contents]) =>
1605
- file.endsWith('.swift') && swiftImportNames(contents).includes('SpriteKit'),
1600
+ // the game family (plans/peach/spritekit.md, plans/peach/scenekit.md) links
1601
+ // only into an app whose sources import it, so no other artifact carries it.
1602
+ // SceneKit names SpriteKit, so importing it links both.
1603
+ const GAME_FAMILY = ['SpriteKit', 'SceneKit'] as const
1604
+ const gameProducts = (files: Record<string, string>): string[] => {
1605
+ const imported = new Set(
1606
+ Object.entries(files).flatMap(([file, contents]) =>
1607
+ file.endsWith('.swift') ? swiftImportNames(contents) : [],
1608
+ ),
1606
1609
  )
1610
+ if (imported.has('SceneKit')) return [...GAME_FAMILY]
1611
+ return imported.has('SpriteKit') ? ['SpriteKit'] : []
1612
+ }
1613
+ const gameProduct = (name: string) =>
1614
+ `.product(name: "${name}", package: "sootsim-swift")`
1615
+ const GAME_REACH: Record<string, string[]> = {
1616
+ SpriteKit: ['SpriteKit', 'PeachBox2D', 'simd', 'GLKit'],
1617
+ SceneKit: ['SceneKit'],
1618
+ }
1607
1619
  // every iOS source sees through Foundation what darwin's Foundation exports:
1608
1620
  // PeachObjC supplies the ObjectiveC module's selectors, Dispatch Foundation's
1609
1621
  // Darwin queue exports, and FoundationNetworking (re-exporting PeachFoundation)
@@ -1675,6 +1687,7 @@ export function swiftArtifactHash(
1675
1687
  hash.update(`swift-compile\n${release}\n`)
1676
1688
  hash.update(compilerSourceDigest)
1677
1689
  if (input.oneModule) hash.update('one-module\n')
1690
+ if (KEEP_WASM_NAMES) hash.update('names\n')
1678
1691
  hash.update(swiftModuleDigest(moduleRoot))
1679
1692
  if (input.optimize) hash.update(`optimize\n${WASM_OPTIMIZE_IDENTITY}\n`)
1680
1693
  for (const file of Object.keys(input.files).sort()) {
@@ -1862,7 +1875,7 @@ export function emitSwiftPackageManifest(args: {
1862
1875
  siblings: Map<string, string[]>
1863
1876
  moduleRoot: string
1864
1877
  oneModule?: boolean
1865
- spriteKit?: boolean
1878
+ gameProducts?: string[]
1866
1879
  /** link the prebuilt distribution at moduleRoot instead of its sources. */
1867
1880
  distribution?: SwiftDistributionManifest
1868
1881
  /** prebuilt interfaces owned by local source or system-library targets. */
@@ -1887,7 +1900,14 @@ export function emitSwiftPackageManifest(args: {
1887
1900
  /** the development region SwiftPM needs as defaultLocalization beside localized resources. */
1888
1901
  defaultLocalization?: string
1889
1902
  }): string {
1890
- const { targets, siblings, moduleRoot, oneModule, spriteKit, distribution } = args
1903
+ const {
1904
+ targets,
1905
+ siblings,
1906
+ moduleRoot,
1907
+ oneModule,
1908
+ gameProducts = [],
1909
+ distribution,
1910
+ } = args
1891
1911
  const prebuilt = args.prebuilt
1892
1912
  // a distribution and a separately built framework package both link objects
1893
1913
  // instead of compiling the framework sources into this package's scratch.
@@ -1934,9 +1954,8 @@ export function emitSwiftPackageManifest(args: {
1934
1954
  for (const name of [...siblings.values()].flat())
1935
1955
  if (OPTIONAL_FRAMEWORKS.has(name) && !targets.some((target) => target.name === name))
1936
1956
  reach(name)
1937
- if (spriteKit) {
1938
- for (const name of ['SpriteKit', 'PeachBox2D', 'simd', 'GLKit']) reach(name)
1939
- }
1957
+ for (const product of gameProducts)
1958
+ for (const name of GAME_REACH[product] ?? []) reach(name)
1940
1959
  // the distribution builder names each object lib/<target>-<file>.o.
1941
1960
  // preserve link order; only reached framework registrants link eagerly.
1942
1961
  // the list is named by its content: SwiftPM relinks when the linker
@@ -1972,7 +1991,7 @@ export function emitSwiftPackageManifest(args: {
1972
1991
  const objects = available.filter(
1973
1992
  (object) =>
1974
1993
  !targets.some((target) => object.startsWith(`lib/${target.name}-`)) &&
1975
- [...OPTIONAL_FRAMEWORKS, 'SpriteKit', 'PeachBox2D'].every(
1994
+ [...OPTIONAL_FRAMEWORKS, ...GAME_FAMILY, 'PeachBox2D'].every(
1976
1995
  (name) => !object.startsWith(`lib/${name}-`) || reachedFrameworks.has(name),
1977
1996
  ),
1978
1997
  )
@@ -2248,7 +2267,7 @@ export function emitSwiftPackageManifest(args: {
2248
2267
  : [
2249
2268
  '.product(name: "SwiftUI", package: "sootsim-swift")',
2250
2269
  ...(oneModule ? [ONE_PRODUCT] : []),
2251
- ...(spriteKit ? [SPRITEKIT_PRODUCT] : []),
2270
+ ...gameProducts.map(gameProduct),
2252
2271
  ]),
2253
2272
  ...(siblings.get(target.name) ?? []).flatMap((dependency) =>
2254
2273
  clang &&
@@ -2302,7 +2321,7 @@ export function emitSwiftPackageManifest(args: {
2302
2321
  .unsafeFlags(${flagArray(`[
2303
2322
  ${REACTOR_FLAG}${executableLinkFlags(args.root)} "-Xlinker", "--export-if-defined=__main_argc_argv"${library},
2304
2323
  ]`)}),
2305
- .unsafeFlags(["-Xlinker", "--strip-all"], .when(configuration: .release)),
2324
+ ${KEEP_WASM_NAMES ? '' : '.unsafeFlags(["-Xlinker", "--strip-all"], .when(configuration: .release)),'}
2306
2325
  ]`
2307
2326
  : librarySettings.length > 0
2308
2327
  ? `,
@@ -3019,8 +3038,6 @@ const FOUNDATION_OVERLAY_GLOBALS = new Set([
3019
3038
  'NSURLProtectionSpaceHTTPS',
3020
3039
  'NSURLProtectionSpaceHTTPSProxy',
3021
3040
  'NSURLProtectionSpaceSOCKSProxy',
3022
- 'NSURLSessionDownloadTaskResumeData',
3023
- 'NSURLSessionTransferSizeUnknown',
3024
3041
  'NSURLSessionUploadTaskResumeData',
3025
3042
  'NSUTF16BigEndianStringEncoding',
3026
3043
  'NSUTF16LittleEndianStringEncoding',
@@ -5320,10 +5337,10 @@ export function writeSwiftProject(
5320
5337
  const moduleName = swiftTargetModuleName(name)
5321
5338
  const originalMap = originalMaps.get(moduleName)
5322
5339
  const stagedMap = sourceModuleMaps.get(moduleName)
5323
- const originalPath = originalMap
5324
- ? path.join(layout.root, originalMap)
5325
- : stagedMap?.path
5326
- const originalContents = originalMap ? input.files[originalMap]! : stagedMap?.contents
5340
+ const originalPath =
5341
+ stagedMap?.path ?? (originalMap ? path.join(layout.root, originalMap) : undefined)
5342
+ const originalContents =
5343
+ stagedMap?.contents ?? (originalMap && input.files[originalMap])
5327
5344
  const originalTokens = originalContents ? moduleMapTokens(originalContents) : []
5328
5345
  if (
5329
5346
  originalPath &&
@@ -5652,13 +5669,17 @@ export function writeSwiftProject(
5652
5669
  .replace(product, `${product}\n ${ONE_PRODUCT},`)
5653
5670
  .replace(flags, `${flags}${ONE_IMPLICIT_FLAGS}`)
5654
5671
  }
5655
- if (importsSpriteKit(input.files)) {
5672
+ const games = gameProducts(input.files)
5673
+ if (games.length > 0) {
5656
5674
  const anchor = '.product(name: "CoreLocation", package: "sootsim-swift"),'
5657
5675
  if (!manifest.includes(anchor))
5658
5676
  throw new Error(
5659
- 'scripts/contrast-app-Package.swift lost the line the SpriteKit product links at',
5677
+ 'scripts/contrast-app-Package.swift lost the line the game products link at',
5660
5678
  )
5661
- manifest = manifest.replace(anchor, `${anchor}\n ${SPRITEKIT_PRODUCT},`)
5679
+ manifest = manifest.replace(
5680
+ anchor,
5681
+ [anchor, ...games.map((name) => `${gameProduct(name)},`)].join('\n '),
5682
+ )
5662
5683
  }
5663
5684
  files.set(path.join(layout.root, 'Package.swift'), manifest + moduleFiles)
5664
5685
  } else {
@@ -5828,7 +5849,7 @@ export function writeSwiftProject(
5828
5849
  siblings: moduleSiblings,
5829
5850
  moduleRoot,
5830
5851
  oneModule: input.oneModule,
5831
- spriteKit: importsSpriteKit(input.files),
5852
+ gameProducts: gameProducts(input.files),
5832
5853
  distribution: distribution ?? undefined,
5833
5854
  moduleExcludes,
5834
5855
  deferDarwinOverlay:
@@ -6352,14 +6373,9 @@ function linkedFrameworkTargets(
6352
6373
  ))
6353
6374
  reach(match[1]!)
6354
6375
  }
6355
- if (importsSpriteKit(input.files))
6356
- for (const name of ['SpriteKit', 'PeachBox2D', 'simd', 'GLKit']) reach(name)
6357
- const wanted = [
6358
- 'PeachApp',
6359
- ...(input.oneModule ? ['One'] : []),
6360
- ...(importsSpriteKit(input.files) ? ['SpriteKit'] : []),
6361
- ...reached,
6362
- ]
6376
+ const games = gameProducts(input.files)
6377
+ for (const product of games) for (const name of GAME_REACH[product] ?? []) reach(name)
6378
+ const wanted = ['PeachApp', ...(input.oneModule ? ['One'] : []), ...games, ...reached]
6363
6379
  const linked = [...new Set(wanted)].filter((name) => libraries.has(name))
6364
6380
  if (!linked.includes('PeachApp'))
6365
6381
  throw new Error('swift-compile: the framework package declares no PeachApp library')
@@ -139,20 +139,22 @@ export function planAssetSymbols(
139
139
  // xcode 27.1 resolves an unset TARGETED_DEVICE_FAMILY to 1 for an iOS target
140
140
  // (xcodebuild -showBuildSettings on an application target that never sets it)
141
141
  const families = (settings.TARGETED_DEVICE_FAMILY || '1').split(',').sort().join(',')
142
- // families 6 and 7 change neither the devices nor symbols for the iOS slice
142
+ // families 3, 6 and 7 change neither the devices nor symbols for the iOS
143
+ // slice (3: actool output identical for 1,3,7 and 1,7, xcode 27.1)
143
144
  const devices: AssetDevice[] | undefined = (
144
145
  {
145
146
  '1': ['iphone'],
146
147
  '2': ['ipad'],
147
148
  '1,2': ['iphone', 'ipad'],
148
149
  '1,7': ['iphone'],
150
+ '1,3,7': ['iphone'],
149
151
  '1,2,6': ['iphone', 'ipad'],
150
152
  '1,2,7': ['iphone', 'ipad'],
151
153
  } satisfies Record<string, AssetDevice[]>
152
154
  )[families]
153
155
  if (!devices)
154
156
  return unknown(
155
- `Generated asset symbols are proven only for TARGETED_DEVICE_FAMILY 1, 2, 1,2, 1,7, 1,2,6 or 1,2,7; this target resolves to "${families}".`,
157
+ `Generated asset symbols are proven only for TARGETED_DEVICE_FAMILY 1, 2, 1,2, 1,7, 1,3,7, 1,2,6 or 1,2,7; this target resolves to "${families}".`,
156
158
  )
157
159
  const plan: SwiftAssetPlan = {
158
160
  catalogs: catalogs.map((path) => ({
package/src/swift-host.ts CHANGED
@@ -1773,6 +1773,33 @@ export function sourceFiles(root: string): string[] {
1773
1773
  }
1774
1774
 
1775
1775
  // project files and the xcconfigs their settings read are build inputs, never resources
1776
+ // the app target's resources Xcode copies to the bundle root: a folder
1777
+ // reference keeps its tree and a catalog compiles, so neither is flat
1778
+ function xcodeFlatResources(
1779
+ app: Pick<XcodeModuleTarget, 'resources' | 'resourceNames'> | undefined,
1780
+ ): ReadonlySet<string> {
1781
+ if (!app) return new Set()
1782
+ return new Set(
1783
+ app.resources.filter(
1784
+ (file) =>
1785
+ !Object.hasOwn(app.resourceNames, file) &&
1786
+ !file.split(path.sep).some((segment) => segment.endsWith('.xcassets')),
1787
+ ),
1788
+ )
1789
+ }
1790
+
1791
+ // an xcode target declares its resources independently of their source
1792
+ // directory; the bundle file and image enumerators use Resources as their root marker.
1793
+ function bundleResourcePath(
1794
+ flatResources: ReadonlySet<string>,
1795
+ disk: string,
1796
+ file: string,
1797
+ ): string {
1798
+ return flatResources.has(disk) && !file.split('/').includes('Resources')
1799
+ ? `Resources/${file}`
1800
+ : file
1801
+ }
1802
+
1776
1803
  function resourceCandidateFiles(root: string): string[] {
1777
1804
  return packageFiles(root, false).filter(
1778
1805
  (file) =>
@@ -1941,12 +1968,24 @@ export function buildSwiftResourceTable(
1941
1968
  const disk = byRelative.get(file)
1942
1969
  return !disk || !layout?.targetDirs.get(disk)?.startsWith(`${XCODE_FRAMEWORKS_DIR}/`)
1943
1970
  })
1944
- const found = enumerateBundleImages(relative, readContents, (file) => {
1945
- const disk = byRelative.get(file)
1946
- if (!disk) throw new Error(`image source ${file} has no input file`)
1947
- const key = layout ? xcodeInputKey(layout, disk, file) : file
1948
- return swiftResourceBundleForFile(bundles, key)?.path ?? '.'
1949
- })
1971
+ const flatResources = xcodeFlatResources(layout?.app)
1972
+ const bundled = new Map(
1973
+ relative.map((file) => [
1974
+ bundleResourcePath(flatResources, byRelative.get(file)!, file),
1975
+ file,
1976
+ ]),
1977
+ )
1978
+ const found = enumerateBundleImages(
1979
+ [...bundled.keys()],
1980
+ (file) => readContents(bundled.get(file) ?? file),
1981
+ (marked) => {
1982
+ const file = bundled.get(marked) ?? marked
1983
+ const disk = byRelative.get(file)
1984
+ if (!disk) throw new Error(`image source ${file} has no input file`)
1985
+ const key = layout ? xcodeInputKey(layout, disk, file) : file
1986
+ return swiftResourceBundleForFile(bundles, key)?.path ?? '.'
1987
+ },
1988
+ ).map((image) => ({ ...image, file: bundled.get(image.file) ?? image.file }))
1950
1989
  const colors = enumerateBundleColors(catalogFiles, readContents)
1951
1990
  const symbols = enumerateBundleSymbols(catalogFiles, readContents, (file) => {
1952
1991
  const disk = byRelative.get(file)
@@ -2180,20 +2219,20 @@ export function buildSwiftBundleFiles(
2180
2219
  }
2181
2220
  }
2182
2221
  const groups = new Map<string, Map<string, string>>()
2183
- const appResources = new Set(xcode?.app.resources ?? [])
2222
+ const flatResources = xcodeFlatResources(xcode?.app)
2184
2223
  for (const file of relative) {
2185
2224
  const disk = path.join(packagePath, ...file.split('/'))
2225
+ const folderName = xcode?.app.resourceNames[disk]
2226
+ if (folderName) {
2227
+ watched.push(file)
2228
+ addFile(folderName, readPackageFile(packagePath, disk, limits.resourceBytes))
2229
+ continue
2230
+ }
2186
2231
  const key = xcode ? xcodeInputKey(xcode, disk, file) : file
2187
2232
  const bundle = swiftResourceBundleForFile(bundles, key)
2188
2233
  const bundlePath = bundle?.path ?? '.'
2189
2234
  const group = groups.get(bundlePath) ?? new Map<string, string>()
2190
- // an xcode target declares its resources independently of their source
2191
- // directory; the bundle enumerator uses Resources as its root marker.
2192
- const resource =
2193
- appResources.has(disk) && !file.split('/').includes('Resources')
2194
- ? `Resources/${file}`
2195
- : file
2196
- group.set(resource, disk)
2235
+ group.set(bundleResourcePath(flatResources, disk, file), disk)
2197
2236
  groups.set(bundlePath, group)
2198
2237
  }
2199
2238
  for (const bundle of bundles) {
@@ -20,6 +20,7 @@ import { readProcessTable, type ProcessRow } from './process-table.ts'
20
20
  import type { Readable, Writable } from 'node:stream'
21
21
 
22
22
  const POLL_MS = 250
23
+ const INSPECT_ATTEMPTS = 5
23
24
  // ps derives a start time from the boot time, which drifts by a second or two
24
25
  // on linux; a reused pid starts later than this.
25
26
  const START_TOLERANCE_MS = 2_000
@@ -53,6 +54,24 @@ function processTable(pid?: number): ProcessRow[] {
53
54
  }
54
55
  }
55
56
 
57
+ // a loaded host can stall ps past its deadline once without the table being
58
+ // broken, and the lock only ever waits on that answer, so lock acquisition
59
+ // asks again before giving up.
60
+ async function processTableWithRetry(
61
+ pid: number | undefined,
62
+ signal: AbortSignal | undefined,
63
+ ): Promise<ProcessRow[]> {
64
+ for (let attempt = 1; ; attempt++) {
65
+ try {
66
+ return processTable(pid)
67
+ } catch (error) {
68
+ if (attempt >= INSPECT_ATTEMPTS) throw error
69
+ await pause(POLL_MS * attempt, signal)
70
+ signal?.throwIfAborted()
71
+ }
72
+ }
73
+ }
74
+
56
75
  function sameProcess(row: ProcessRow, identity: Identity): boolean {
57
76
  return (
58
77
  row.pid === identity.pid && Math.abs(row.start - identity.start) <= START_TOLERANCE_MS
@@ -204,7 +223,11 @@ export async function withSwiftRootLock<T>(
204
223
  )
205
224
  mkdirSync(directory, { recursive: true })
206
225
  const token = randomUUID()
207
- const owner = JSON.stringify(ownIdentity(process.pid))
226
+ const ownRow = (await processTableWithRetry(process.pid, signal)).find(
227
+ (candidate) => candidate.pid === process.pid && !candidate.zombie,
228
+ )
229
+ if (!ownRow) throw new Error(`swift root lock cannot find process ${process.pid}`)
230
+ const owner = JSON.stringify({ pid: process.pid, start: ownRow.start })
208
231
  const pending = path.join(directory, `pending.${token}`)
209
232
  let held = 0
210
233
  for (;;) {
@@ -213,15 +236,15 @@ export async function withSwiftRootLock<T>(
213
236
  const current = newest > 0 ? readGeneration(directory, newest) : null
214
237
  if (newest > 0 && !current) continue
215
238
  if (current && !current.released) {
216
- const owner = processTable(current.holder.pid)
217
- if (owner.some((row) => !row.zombie && sameProcess(row, current.holder))) {
239
+ const holder = await processTableWithRetry(current.holder.pid, signal)
240
+ if (holder.some((row) => !row.zombie && sameProcess(row, current.holder))) {
218
241
  if (!wait) return undefined
219
242
  await pause(POLL_MS, signal)
220
243
  continue
221
244
  }
222
245
  const children = readChildren(directory, newest)
223
246
  if (children.length) {
224
- const table = processTable()
247
+ const table = await processTableWithRetry(undefined, signal)
225
248
  if (children.some((child) => groupAlive(table, child))) {
226
249
  if (!wait) return undefined
227
250
  await pause(POLL_MS, signal)
@@ -22,8 +22,11 @@ const FEATURES = [
22
22
  'sign-ext',
23
23
  ]
24
24
 
25
+ // PEACH_SWIFT_NAMES=1 keeps the name section so a trap stack names its functions.
26
+ export const KEEP_WASM_NAMES = process.env.PEACH_SWIFT_NAMES === '1'
27
+
25
28
  /** feeds the artifact hash: a new optimizer or flag set is a new artifact. */
26
- export const WASM_OPTIMIZE_IDENTITY = `wasm-opt ${BINARYEN_VERSION} -Oz ${FEATURES.join(',')}`
29
+ export const WASM_OPTIMIZE_IDENTITY = `wasm-opt ${BINARYEN_VERSION} -Oz ${FEATURES.join(',')}${KEEP_WASM_NAMES ? ' -g' : ''}`
27
30
 
28
31
  // the sha256 of each Binaryen release archive wasm-opt resolves for its own platform
29
32
  // keys; update both when the wasm-opt package moves BINARYEN_VERSION.
@@ -73,6 +76,7 @@ async function runWasmOpt(
73
76
  const result = await wasmOpt(
74
77
  [
75
78
  '-Oz',
79
+ ...(KEEP_WASM_NAMES ? ['-g'] : []),
76
80
  ...skipPasses.flatMap((pass) => ['--skip-pass', pass]),
77
81
  ...FEATURES.map((feature) => `--enable-${feature}`),
78
82
  file,
@@ -12,6 +12,7 @@ import { createHash } from 'node:crypto'
12
12
  import fs from 'node:fs'
13
13
  import path from 'node:path'
14
14
  import plist from 'plist'
15
+ import iosSdk from './ios-sdk-frameworks.json' with { type: 'json' }
15
16
  import { isRecord } from './is-record'
16
17
  import { readKeychainAccessGroups } from './native-app-identity'
17
18
  import { preparedXcodePhase } from './prepared-xcode-phases'
@@ -93,6 +94,8 @@ export interface XcodeModuleTarget {
93
94
  allSources: string[]
94
95
  /** absolute paths of every resource file the target copies, bundles expanded. */
95
96
  resources: string[]
97
+ /** a folder reference's files by their path in the bundle, which keeps the folder. */
98
+ resourceNames: Record<string, string>
96
99
  /** files the target's shell phases copy with a plain `cp`, resolved from the phase's own assignments and the build settings. */
97
100
  copiedFiles: { from: string; to: string }[]
98
101
  /** a shell phase of the target generates its INFOPLIST_FILE, which exists only once prepared. */
@@ -1032,6 +1035,7 @@ function targetMembers(
1032
1035
  ): {
1033
1036
  sources: string[]
1034
1037
  resources: string[]
1038
+ resourceNames: Record<string, string>
1035
1039
  copiedFiles: { from: string; to: string }[]
1036
1040
  generatedInfoPlist: boolean
1037
1041
  allSources: string[]
@@ -1043,6 +1047,7 @@ function targetMembers(
1043
1047
  const allSources = new Set<string>()
1044
1048
  const headers = new Set<string>()
1045
1049
  const resources = new Set<string>()
1050
+ const resourceNames: Record<string, string> = {}
1046
1051
  const scriptForOutput = new Map<string, string>()
1047
1052
  const copiedFiles: { from: string; to: string }[] = []
1048
1053
  let generatedInfoPlist = false
@@ -1232,13 +1237,14 @@ function targetMembers(
1232
1237
  xcodePathResolver(project.objects, project.projectDir, path.join, settings),
1233
1238
  (file) => (isDirectory(file) ? walkFiles(file) : fs.existsSync(file) ? [file] : []),
1234
1239
  path.join,
1235
- (file, kind) => {
1240
+ (file, kind, bundleName) => {
1236
1241
  if (kind === 'header') {
1237
1242
  headers.add(file)
1238
1243
  return
1239
1244
  }
1240
1245
  if (kind === 'resource') {
1241
1246
  resources.add(file)
1247
+ if (bundleName) resourceNames[file] = bundleName
1242
1248
  return
1243
1249
  }
1244
1250
  if (!fs.existsSync(file)) {
@@ -1376,6 +1382,7 @@ function targetMembers(
1376
1382
  return {
1377
1383
  sources: [...sources].sort(),
1378
1384
  resources: [...resources].sort(),
1385
+ resourceNames,
1379
1386
  copiedFiles,
1380
1387
  generatedInfoPlist,
1381
1388
  allSources: [...allSources].sort(),
@@ -3117,10 +3124,17 @@ export function readXcodeAppTarget(
3117
3124
  )
3118
3125
  continue
3119
3126
  const resolved = fileId ? project.resolvePath(fileId) : null
3120
- if (fileId && resolved !== null && !fs.existsSync(resolved))
3121
- throw new Error(
3122
- `${str(target.name) ?? 'target'}: linked framework ${name} (${fileId}) points at missing ${resolved} in ${project.projectFile}`,
3123
- )
3127
+ // a reference to a file that is not there still links -framework
3128
+ // <name> (xcode 27.1, iOS simulator): the sdk's framework answers it,
3129
+ // and a name the sdk lacks fails the link
3130
+ if (fileId && resolved !== null && !fs.existsSync(resolved)) {
3131
+ if (!iosSdk.frameworks.includes(name))
3132
+ throw new Error(
3133
+ `${str(target.name) ?? 'target'}: linked framework ${name} (${fileId}) points at missing ${resolved} in ${project.projectFile}`,
3134
+ )
3135
+ sdkFrameworks.add(name)
3136
+ continue
3137
+ }
3124
3138
  if (!binaryFrameworks.get(name)) binaryFrameworks.set(name, resolved)
3125
3139
  }
3126
3140
  }
@@ -3234,6 +3248,13 @@ export function readXcodeAppTarget(
3234
3248
  source.kind === 'local' && fs.existsSync(source.dir) ? [source.dir] : [],
3235
3249
  ),
3236
3250
  ...new Set(podSourceDirs),
3251
+ // a target's own files may sit beside the project, such as an asset
3252
+ // catalog one folder up, and xcode builds them from there
3253
+ ...new Set(
3254
+ [...module.allSources, ...module.resources]
3255
+ .filter((file) => fs.existsSync(file))
3256
+ .map((file) => path.dirname(file)),
3257
+ ),
3237
3258
  ].map((dir) => fs.realpathSync.native(dir)),
3238
3259
  ),
3239
3260
  projectFiles: [