peachsim 0.1.1641 → 0.1.1644

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 (96) hide show
  1. package/cli/app-bundle-resources.ts +57 -0
  2. package/cli/commands/control.ts +12 -0
  3. package/cli/drivers/playwright.ts +39 -0
  4. package/cli/outbound-endpoints.ts +4 -2
  5. package/detox/motion-change.cjs +7 -5
  6. package/detox/motion-change.d.cts +1 -0
  7. package/detox/motion-leg-starts.cjs +20 -0
  8. package/detox/motion-leg-starts.d.cts +8 -0
  9. package/dist-lib/agent-daemon-client.cjs +1 -1
  10. package/dist-lib/agent-events.cjs +1 -1
  11. package/dist-lib/attached-projects.cjs +1 -1
  12. package/dist-lib/auth/shared-session.cjs +1 -1
  13. package/dist-lib/backend-origin.cjs +3 -3
  14. package/dist-lib/beta.cjs +1 -1
  15. package/dist-lib/beta.mjs +1 -1
  16. package/dist-lib/bridge-constants.cjs +1 -1
  17. package/dist-lib/bridge-contract-input.cjs +1 -1
  18. package/dist-lib/bridge-contract-input.mjs +1 -1
  19. package/dist-lib/bridge-contract.cjs +1 -1
  20. package/dist-lib/bridge-contract.mjs +1 -1
  21. package/dist-lib/bundle-channel.cjs +719 -572
  22. package/dist-lib/capture-contract.cjs +1 -1
  23. package/dist-lib/capture-contract.mjs +1 -1
  24. package/dist-lib/cloud-contract.cjs +1 -1
  25. package/dist-lib/cloud-contract.mjs +1 -1
  26. package/dist-lib/cloud-test-args.cjs +1 -1
  27. package/dist-lib/cloud-test-args.mjs +1 -1
  28. package/dist-lib/cloud.cjs +1 -1
  29. package/dist-lib/cloud.mjs +1 -1
  30. package/dist-lib/config.cjs +1 -1
  31. package/dist-lib/config.mjs +1 -1
  32. package/dist-lib/detox/index.cjs +7 -6
  33. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  34. package/dist-lib/expo-swift-bundle.cjs +656 -509
  35. package/dist-lib/expo-swift-metadata.cjs +1 -1
  36. package/dist-lib/expo-swift-metadata.mjs +1 -1
  37. package/dist-lib/home-paths.cjs +1 -1
  38. package/dist-lib/host/bridge-host.cjs +811 -658
  39. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  40. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  41. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  42. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  43. package/dist-lib/host/websocket-proxy.cjs +1 -1
  44. package/dist-lib/index.cjs +89 -41
  45. package/dist-lib/jump-to-source-babel.cjs +1 -1
  46. package/dist-lib/jump-to-source-native.cjs +1 -1
  47. package/dist-lib/kotlin-host.cjs +1 -1
  48. package/dist-lib/menu.cjs +14 -6
  49. package/dist-lib/menu.mjs +14 -6
  50. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  51. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  52. package/dist-lib/metro-production-bundle.cjs +1 -1
  53. package/dist-lib/metro-production-bundle.mjs +1 -1
  54. package/dist-lib/metro.cjs +88 -41
  55. package/dist-lib/native-app-identity-project.cjs +90 -41
  56. package/dist-lib/native-app-identity-project.mjs +89 -41
  57. package/dist-lib/native-app-identity.cjs +1 -1
  58. package/dist-lib/native-app-identity.mjs +1 -1
  59. package/dist-lib/native-firebase-metadata.cjs +1 -1
  60. package/dist-lib/native-firebase-metadata.mjs +1 -1
  61. package/dist-lib/plist.cjs +1 -1
  62. package/dist-lib/plist.mjs +1 -1
  63. package/dist-lib/profiles.cjs +1 -1
  64. package/dist-lib/public-brand.cjs +1 -1
  65. package/dist-lib/react-native-host-modules.cjs +1 -1
  66. package/dist-lib/react-native-host-modules.mjs +1 -1
  67. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  68. package/dist-lib/sdk.cjs +2 -1
  69. package/dist-lib/sdk.mjs +2 -1
  70. package/dist-lib/skills.cjs +1716 -1366
  71. package/dist-lib/swift-compile.cjs +297 -215
  72. package/dist-lib/swift-compile.mjs +315 -227
  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 +620 -473
  80. package/dist-lib/vite.cjs +1 -1
  81. package/dist-lib/worklet-executable.cjs +1 -1
  82. package/dist-lib/worklet-executable.mjs +1 -1
  83. package/package.json +1 -1
  84. package/src/appBundleResourcesContract.ts +51 -0
  85. package/src/backend-origin.ts +3 -1
  86. package/src/host/bridge-host.ts +17 -11
  87. package/src/host/staged-host-files.ts +28 -4
  88. package/src/launchMetadataContract.ts +1 -0
  89. package/src/menu.ts +12 -4
  90. package/src/native-app-identity-project.ts +47 -38
  91. package/src/runtime-delivery.ts +6 -7
  92. package/src/swift-compile.ts +18 -7
  93. package/src/swift-compiler-digest.ts +5 -3
  94. package/src/swift-objc-compile.ts +34 -4
  95. package/src/swift-root-lock.ts +241 -92
  96. package/src/xcode-project.ts +67 -4
package/dist-lib/vite.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1641 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1644 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1641 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1644 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1641 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1644 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/worklet-executable.ts
4
4
  import { parse } from "acorn";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.1641",
3
+ "version": "0.1.1644",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -0,0 +1,51 @@
1
+ import type { PeachBundleAssetReference } from './bundle-asset-resource'
2
+
3
+ // copied native resources keep their bundle-relative namespace, independently
4
+ // of the host URLs used to deliver their immutable bytes.
5
+ export type AppBundleResources = Record<string, PeachBundleAssetReference>
6
+
7
+ export function isAppBundleResourcePath(path: string): boolean {
8
+ return (
9
+ path.length > 0 &&
10
+ !path.includes('\0') &&
11
+ !/[\\?#]/.test(path) &&
12
+ path.split('/').every((part) => part !== '' && part !== '.' && part !== '..')
13
+ )
14
+ }
15
+
16
+ export function readAppBundleResources(wire: string | null): AppBundleResources {
17
+ if (wire === null) return {}
18
+ const value: unknown = JSON.parse(wire)
19
+ if (!value || typeof value !== 'object' || Array.isArray(value))
20
+ throw new Error('invalid native app bundle resources')
21
+ const resources: AppBundleResources = Object.create(null)
22
+ for (const [path, resource] of Object.entries(value)) {
23
+ if (
24
+ !isAppBundleResourcePath(path) ||
25
+ !resource ||
26
+ typeof resource !== 'object' ||
27
+ !('uri' in resource) ||
28
+ typeof resource.uri !== 'string' ||
29
+ !('sha256' in resource) ||
30
+ typeof resource.sha256 !== 'string' ||
31
+ !/^[a-f0-9]{64}$/.test(resource.sha256) ||
32
+ !('contentType' in resource) ||
33
+ typeof resource.contentType !== 'string' ||
34
+ !('byteLength' in resource) ||
35
+ typeof resource.byteLength !== 'number' ||
36
+ !Number.isSafeInteger(resource.byteLength) ||
37
+ resource.byteLength < 0
38
+ )
39
+ throw new Error('invalid native app bundle resource')
40
+ const url = new URL(resource.uri)
41
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
42
+ throw new Error('invalid native app bundle resource URL')
43
+ resources[path] = {
44
+ uri: url.href,
45
+ sha256: resource.sha256,
46
+ contentType: resource.contentType,
47
+ byteLength: resource.byteLength,
48
+ }
49
+ }
50
+ return resources
51
+ }
@@ -1,4 +1,4 @@
1
- const DEFAULT_CONTRAST_BACKEND_ORIGIN = 'https://contrast.dev'
1
+ const DEFAULT_CONTRAST_BACKEND_ORIGIN = 'https://peachsim.com'
2
2
 
3
3
  export const LOOPBACK_PROBE_HOSTNAMES = ['127.0.0.1', '::1'] as const
4
4
 
@@ -115,6 +115,8 @@ export function isContrastBackendHost(hostname: string): boolean {
115
115
  isLoopbackHost(normalized) ||
116
116
  normalized === 'contrast.dev' ||
117
117
  normalized.endsWith('.contrast.dev') ||
118
+ normalized === 'peachsim.com' ||
119
+ normalized === 'www.peachsim.com' ||
118
120
  normalized === PEACHSIM_NATIVE_HOST
119
121
  )
120
122
  }
@@ -1530,7 +1530,21 @@ export class SootSimBridgeHost {
1530
1530
  * non-upgrade routes that don't match serve index.html (SPA behavior) so
1531
1531
  * electron's webContents can navigate freely inside the runtime. */
1532
1532
  private handleHttpRequest(req: IncomingMessage, res: ServerResponse) {
1533
- if (rejectUntrustedBridgeOrigin(req, res)) return
1533
+ const method = (req.method || 'GET').toUpperCase()
1534
+ let url: URL
1535
+ try {
1536
+ url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
1537
+ } catch {
1538
+ res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
1539
+ res.end('invalid request URL or host')
1540
+ return
1541
+ }
1542
+ const publicEngineFont =
1543
+ (method === 'GET' || method === 'HEAD') &&
1544
+ /^\/(?:runtime\/|sootsim\/)?engine\/fonts\/[^\\]*\.(ttf|otf|woff2?)$/.test(
1545
+ url.pathname,
1546
+ )
1547
+ if (!publicEngineFont && rejectUntrustedBridgeOrigin(req, res)) return
1534
1548
  if (req.url === '/__peach/fetch-transport') {
1535
1549
  res.writeHead(200, {
1536
1550
  'Content-Type': 'application/json',
@@ -1619,16 +1633,6 @@ export class SootSimBridgeHost {
1619
1633
  return
1620
1634
  }
1621
1635
 
1622
- const method = (req.method || 'GET').toUpperCase()
1623
- let url: URL
1624
- try {
1625
- url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`)
1626
- } catch {
1627
- res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' })
1628
- res.end('invalid request URL or host')
1629
- return
1630
- }
1631
-
1632
1636
  // /__sootsim/shared-config is the one awaited HTTP write. ordinary
1633
1637
  // settings fan out over WS, but destructive reloads need confirmation
1634
1638
  // that the disk source of truth accepted their final reset first.
@@ -1972,6 +1976,8 @@ export class SootSimBridgeHost {
1972
1976
  res.writeHead(200, {
1973
1977
  'Content-Type': contentType,
1974
1978
  'Cache-Control': 'no-store',
1979
+ // native file documents have an opaque origin when reading public fonts.
1980
+ ...(publicEngineFont ? { 'Access-Control-Allow-Origin': '*' } : {}),
1975
1981
  })
1976
1982
  if (method === 'HEAD') {
1977
1983
  res.end()
@@ -51,11 +51,21 @@ export function stageHostBytes(
51
51
  origin: string,
52
52
  extension = '',
53
53
  ): string {
54
- const digest = createHash('sha256').update(bytes).digest('hex')
55
- extension = extension.toLowerCase()
56
- const name = STAGED_NAME.test(digest + extension) ? digest + extension : digest
54
+ const directory = prepareStagedDirectory()
55
+ return writeStagedBytes(bytes, origin, extension, directory)
56
+ }
57
+
58
+ // a resource pack prunes once, rather than scanning the cache for every file.
59
+ export function stageHostByteFiles(
60
+ files: readonly Uint8Array[],
61
+ origin: string,
62
+ ): string[] {
63
+ const directory = prepareStagedDirectory()
64
+ return files.map((bytes) => writeStagedBytes(bytes, origin, '', directory))
65
+ }
66
+
67
+ function prepareStagedDirectory(): string {
57
68
  const directory = stagedDir()
58
- const destination = path.join(directory, name)
59
69
  fs.mkdirSync(directory, { recursive: true })
60
70
  const now = Date.now()
61
71
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
@@ -63,6 +73,20 @@ export function stageHostBytes(
63
73
  if (entry.isFile() && now - fs.statSync(stale).mtimeMs > STAGED_RETENTION_MS)
64
74
  fs.rmSync(stale, { force: true })
65
75
  }
76
+ return directory
77
+ }
78
+
79
+ function writeStagedBytes(
80
+ bytes: Uint8Array,
81
+ origin: string,
82
+ extension: string,
83
+ directory: string,
84
+ ): string {
85
+ const digest = createHash('sha256').update(bytes).digest('hex')
86
+ extension = extension.toLowerCase()
87
+ const name = STAGED_NAME.test(digest + extension) ? digest + extension : digest
88
+ const destination = path.join(directory, name)
89
+ const now = Date.now()
66
90
  if (fs.existsSync(destination)) {
67
91
  fs.utimesSync(destination, new Date(now), new Date(now))
68
92
  } else {
@@ -6,6 +6,7 @@ export const LAUNCH_METADATA_KEYS: readonly string[] = [
6
6
  'appFonts',
7
7
  'appSplash',
8
8
  'appCatalog',
9
+ 'appBundleResources',
9
10
  'nativeAppIdentity',
10
11
  'nativeFirebase',
11
12
  'appManifest',
package/src/menu.ts CHANGED
@@ -71,7 +71,13 @@ export const SOOTSIM_RENDER_RESOLUTION_LABELS: Record<SootSimRenderResolution, s
71
71
  // engine's wireframe store draws. 'wireframe' is the clean structural sketch;
72
72
  // the others are its screen-space styles that ship as named effects.
73
73
  export type SootSimScreenEffect = 'none' | 'wireframe' | 'blueprint' | 'panel' | 'green'
74
- export const SOOTSIM_SCREEN_EFFECTS = ['none', 'wireframe', 'blueprint', 'panel', 'green'] as const
74
+ export const SOOTSIM_SCREEN_EFFECTS = [
75
+ 'none',
76
+ 'wireframe',
77
+ 'blueprint',
78
+ 'panel',
79
+ 'green',
80
+ ] as const
75
81
 
76
82
  export function isSootSimScreenEffect(value: unknown): value is SootSimScreenEffect {
77
83
  return (
@@ -373,9 +379,9 @@ function deviceMenu(
373
379
  },
374
380
  { type: 'separator' },
375
381
  { label: 'Trigger Screenshot', action: 'screenshot' },
376
- // pose controls for the current device: open/close on foldables,
377
- // rotate left/right wherever a turn changes geometry, the way ios
378
- // simulator holds them (rotate left is cmd+left, rotate right
382
+ // pose controls for the current device: open/close, book and laptop on
383
+ // foldables, rotate left/right wherever a turn changes geometry, the way
384
+ // ios simulator holds them (rotate left is cmd+left, rotate right
379
385
  // cmd+right). truthiness, not !== null: a partial state must hide
380
386
  // these, never show duo items for the wrong device.
381
387
  ...(state.devicePosture || state.canRotate
@@ -390,6 +396,8 @@ function deviceMenu(
390
396
  : 'Open iPhone Duo',
391
397
  action: 'toggle-duo-posture',
392
398
  },
399
+ { label: 'Book Pose', action: 'fold-duo-book' },
400
+ { label: 'Laptop Pose', action: 'fold-duo-laptop' },
393
401
  ]
394
402
  : []),
395
403
  ...(state.canRotate
@@ -326,44 +326,8 @@ function readIosNativeAppIdentity(
326
326
  parseInfoPlist: (source: string) => unknown,
327
327
  iosTarget: string | undefined,
328
328
  ): NativeAppIdentity | null {
329
- const iosRoot = path.join(projectRoot, 'ios')
330
- if (!fs.existsSync(iosRoot)) return null
331
- const projects = fs
332
- .readdirSync(iosRoot, { withFileTypes: true })
333
- .filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
334
- if (projects.length === 0) return null
335
- if (projects.length > 1 && !iosTarget) {
336
- throw new NativeAppTargetSelectionError(
337
- `native app identity requires a selected Xcode project; set withPeach's iosTarget to an app target in one of: ${projects.map((entry) => entry.name).join(', ')}`,
338
- )
339
- }
340
- let project = projects[0]!
341
- if (iosTarget && projects.length > 1) {
342
- const matches = projects.filter((entry) =>
343
- listXcodeAppTargets(path.join(iosRoot, entry.name), {
344
- requireSources: false,
345
- }).includes(iosTarget),
346
- )
347
- if (matches.length === 0) {
348
- throw new Error(`native app identity has no application target ${iosTarget}`)
349
- }
350
- if (matches.length > 1) {
351
- throw new Error(
352
- `native app identity target ${iosTarget} is ambiguous across ${matches.map((entry) => entry.name).join(', ')}`,
353
- )
354
- }
355
- project = matches[0]!
356
- }
357
- const xcodeproj = path.join(iosRoot, project.name)
358
- const appTargets = listXcodeAppTargets(xcodeproj, { requireSources: false })
359
- if (!iosTarget && appTargets.length > 1) {
360
- throw new NativeAppTargetSelectionError(
361
- `native app identity requires a selected app target; set withPeach's iosTarget to one of: ${appTargets.join(', ')}`,
362
- )
363
- }
364
- const app = readXcodeAppTarget(xcodeproj, iosTarget ?? appTargets[0], 'Release', {
365
- requireSources: false,
366
- })
329
+ const app = readIosAppTarget(projectRoot, iosTarget)
330
+ if (!app) return null
367
331
  const identity = readNativeAppIdentityFromXcodeTarget(app, parseInfoPlist)
368
332
  const projectManifestPath = path.join(projectRoot, 'package.json')
369
333
  const projectRequire = createRequire(projectManifestPath)
@@ -422,3 +386,48 @@ function readIosNativeAppIdentity(
422
386
  }
423
387
  return { ...identity, reanimatedStaticFeatureFlags: flags }
424
388
  }
389
+
390
+ // native identity and copied resources must select the same application target.
391
+ export function readIosAppTarget(
392
+ projectRoot: string,
393
+ iosTarget?: string,
394
+ ): XcodeAppTarget | null {
395
+ const iosRoot = path.join(projectRoot, 'ios')
396
+ if (!fs.existsSync(iosRoot)) return null
397
+ const projects = fs
398
+ .readdirSync(iosRoot, { withFileTypes: true })
399
+ .filter((entry) => entry.isDirectory() && entry.name.endsWith('.xcodeproj'))
400
+ if (projects.length === 0) return null
401
+ if (projects.length > 1 && !iosTarget) {
402
+ throw new NativeAppTargetSelectionError(
403
+ `native app identity requires a selected Xcode project; set withPeach's iosTarget to an app target in one of: ${projects.map((entry) => entry.name).join(', ')}`,
404
+ )
405
+ }
406
+ let project = projects[0]!
407
+ if (iosTarget && projects.length > 1) {
408
+ const matches = projects.filter((entry) =>
409
+ listXcodeAppTargets(path.join(iosRoot, entry.name), {
410
+ requireSources: false,
411
+ }).includes(iosTarget),
412
+ )
413
+ if (matches.length === 0) {
414
+ throw new Error(`native app identity has no application target ${iosTarget}`)
415
+ }
416
+ if (matches.length > 1) {
417
+ throw new Error(
418
+ `native app identity target ${iosTarget} is ambiguous across ${matches.map((entry) => entry.name).join(', ')}`,
419
+ )
420
+ }
421
+ project = matches[0]!
422
+ }
423
+ const xcodeproj = path.join(iosRoot, project.name)
424
+ const appTargets = listXcodeAppTargets(xcodeproj, { requireSources: false })
425
+ if (!iosTarget && appTargets.length > 1) {
426
+ throw new NativeAppTargetSelectionError(
427
+ `native app identity requires a selected app target; set withPeach's iosTarget to one of: ${appTargets.join(', ')}`,
428
+ )
429
+ }
430
+ return readXcodeAppTarget(xcodeproj, iosTarget ?? appTargets[0], 'Release', {
431
+ requireSources: false,
432
+ })
433
+ }
@@ -3,7 +3,7 @@
3
3
  // the machinery itself (hosted manifest, sha256-verified tarballs, channels,
4
4
  // channel resolution, and auto-update) lives in @contrast/runtime-delivery and
5
5
  // knows nothing about peach. this file supplies peach's identity, public env
6
- // overrides, and the contrast.dev/runtimes hosting layout.
6
+ // overrides, and the peachsim.com/runtimes hosting layout.
7
7
  //
8
8
  // @contrast/runtime-delivery is an internal workspace package that is never
9
9
  // published, so both the CLI bundle and dist-lib inline it (see the `alias`
@@ -20,14 +20,13 @@ import {
20
20
  runtimesDir,
21
21
  writeActiveRuntime,
22
22
  } from './home-paths.ts'
23
- import { rnxPublicBrand } from './public-brand.ts'
23
+ import { peachPublicBrand } from './public-brand.ts'
24
24
 
25
- export const DEFAULT_RUNTIME_CDN_ORIGIN = 'https://contrast.dev'
25
+ export const DEFAULT_RUNTIME_CDN_ORIGIN = peachPublicBrand.origin
26
26
 
27
- /** the official curated changelog and the machine-readable release feed. the
28
- * peach site is static, so its /api traffic is owned by contrast.dev. */
29
- export const RNX_CHANGELOG_URL = `${rnxPublicBrand.origin}/changelog`
30
- export const RNX_CHANGELOG_API_URL = 'https://contrast.dev/api/changelog/sootsim'
27
+ /** the official curated changelog and the machine-readable release feed. */
28
+ export const RNX_CHANGELOG_URL = `${peachPublicBrand.origin}/changelog`
29
+ export const RNX_CHANGELOG_API_URL = `${peachPublicBrand.origin}/api/changelog/sootsim`
31
30
 
32
31
  export const RNX_RUNTIME_PRODUCT: RuntimeProduct = {
33
32
  id: 'peach',
@@ -1,4 +1,9 @@
1
- import { spawn, execFileSync, type ExecFileOptions } from 'node:child_process'
1
+ import {
2
+ spawn,
3
+ execFileSync,
4
+ type ExecFileOptions,
5
+ type SpawnOptions,
6
+ } from 'node:child_process'
2
7
  import { createHash } from 'node:crypto'
3
8
  import {
4
9
  cpSync,
@@ -33,7 +38,7 @@ import {
33
38
  swiftTargetModuleName,
34
39
  type SwiftPackageTarget,
35
40
  } from './swift-package'
36
- import { withSwiftRootLock } from './swift-root-lock'
41
+ import { withSwiftRootLock, type SwiftRootLease } from './swift-root-lock'
37
42
  import { swiftEnvironment, swiftSdkOptions, type SwiftWasmSdk } from './swift-toolchain'
38
43
  import { isSwiftResourceBundles } from './swiftContract'
39
44
  import type { SwiftImageUpload } from './swift-submit'
@@ -43,24 +48,29 @@ export { swiftIcuAsset, type SwiftIcuAsset }
43
48
 
44
49
  // compiler drivers own descendants such as admission and swiftpm workers.
45
50
  // cancellation waits for their pipes to close before releasing the build root.
51
+ // a `lease` records the group under the root's lock, so the root stays held
52
+ // while the compiler lives even if this process does not.
46
53
  // `stream` echoes the output as it arrives: all of it, or only matching lines.
47
54
  // a successful run then echoes the rest minus lines matching `quiet`, its
48
55
  // warnings; a failed run's output goes to the caller's one diagnostic.
49
56
  async function runCompiler(
50
57
  command: string,
51
58
  args: string[],
52
- options: ExecFileOptions & { signal?: AbortSignal },
59
+ options: ExecFileOptions & { signal?: AbortSignal; lease?: SwiftRootLease },
53
60
  stream: boolean | RegExp = false,
54
61
  quiet?: RegExp,
55
62
  ): Promise<void> {
56
63
  const { signal } = options
57
64
  signal?.throwIfAborted()
58
- const child = spawn(command, args, {
65
+ const launch = {
59
66
  cwd: options.cwd,
60
67
  env: swiftEnvironment(options.env),
61
68
  detached: true,
62
- stdio: ['ignore', 'pipe', 'pipe'],
63
- })
69
+ stdio: ['ignore', 'pipe', 'pipe'] as ['ignore', 'pipe', 'pipe'],
70
+ } satisfies SpawnOptions
71
+ const child = options.lease
72
+ ? options.lease.spawn(command, args, launch)
73
+ : spawn(command, args, launch)
64
74
  let stopping: Promise<void> | undefined
65
75
  try {
66
76
  await new Promise<void>((resolve, reject) => {
@@ -6242,7 +6252,7 @@ export function compileSwiftProjectUnit(
6242
6252
  // using it, so one process writes, builds and reads them at a time.
6243
6253
  return withSwiftRootLock(
6244
6254
  root,
6245
- async () => {
6255
+ async (lease) => {
6246
6256
  // a process that held the root while this one waited may have built
6247
6257
  // this exact address.
6248
6258
  const built = readCompiledArtifact(path.join(sharedRoot, 'artifact'), hash)
@@ -6314,6 +6324,7 @@ export function compileSwiftProjectUnit(
6314
6324
  maxBuffer: 64 * 1024 * 1024,
6315
6325
  timeout: options.timeoutMs,
6316
6326
  signal: options.signal,
6327
+ lease,
6317
6328
  }
6318
6329
  report('compiling and linking Swift modules')
6319
6330
  try {
@@ -3,11 +3,13 @@ import { readFileSync } from 'node:fs'
3
3
  import path from 'node:path'
4
4
 
5
5
  // the lowering and manifest code in swift-compile.ts and its design-time lowering
6
- // decide the emitted output, so their text addresses every swift artifact. bundled
7
- // builds cannot read these files at runtime, so build-cli injects this digest.
6
+ // decide the emitted output, and the root lock decides whose sources and
7
+ // executable an address names, so their text addresses every swift artifact.
8
+ // bundled builds cannot read these files at runtime, so build-cli injects this
9
+ // digest.
8
10
  export function swiftCompilerSourceDigest(srcDir: string): string {
9
11
  const hash = createHash('sha256')
10
- for (const file of ['swift-compile.ts', 'swift-design-time.ts']) {
12
+ for (const file of ['swift-compile.ts', 'swift-design-time.ts', 'swift-root-lock.ts']) {
11
13
  hash.update(readFileSync(path.join(srcDir, file)))
12
14
  }
13
15
  return hash.digest('hex')
@@ -37,6 +37,19 @@ export function compileXcodeObjCImplementations(
37
37
  )
38
38
  }
39
39
 
40
+ // a link's target, or null when the target is proven absent; other failures surface.
41
+ function linkedStat(file: string): fs.Stats | null {
42
+ try {
43
+ return fs.statSync(file)
44
+ } catch (error) {
45
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return null
46
+ throw error
47
+ }
48
+ }
49
+
50
+ const isSdkInclude = (file: string): boolean =>
51
+ file === '/usr/include' || file.startsWith('/usr/include/')
52
+
40
53
  /** prepares genuine objects without authorizing unsupported framework linkage. */
41
54
  export function prepareXcodeObjCImplementations(
42
55
  app: XcodeAppTarget,
@@ -63,13 +76,24 @@ export function prepareXcodeObjCImplementations(
63
76
  ),
64
77
  )
65
78
  }
79
+ const walked = new Set<string>()
80
+ const ancestors = new Set<string>()
66
81
  const addHeaders = (directory: string): void => {
82
+ // a versioned framework links Headers to a real directory: stage each path it is reached by, once, never around a link cycle.
83
+ const real = fs.realpathSync(directory)
84
+ if (walked.has(directory) || ancestors.has(real)) return
85
+ walked.add(directory)
86
+ ancestors.add(real)
67
87
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
68
- if (entry.isDirectory() && entry.name.startsWith('.')) continue
88
+ if (entry.name.startsWith('.') && !entry.isFile()) continue
69
89
  const file = path.join(directory, entry.name)
70
- if (entry.isDirectory()) addHeaders(file)
90
+ // a link to nothing is never opened by the compiler: a header that is needed fails there as not found.
91
+ const linked = entry.isSymbolicLink() ? linkedStat(file) : null
92
+ if (entry.isSymbolicLink() && !linked) continue
93
+ if (entry.isDirectory() || linked?.isDirectory()) addHeaders(file)
71
94
  else if (/\.(h|hh|hpp|hxx|inc|inl|modulemap)$/.test(entry.name)) add(file, true)
72
95
  }
96
+ ancestors.delete(real)
73
97
  }
74
98
  const targets: {
75
99
  name: string
@@ -152,6 +176,12 @@ export function prepareXcodeObjCImplementations(
152
176
  ),
153
177
  ])
154
178
  add(file, true)
179
+ // search paths name directories of headers the target includes; /usr/include maps to the wasm sysroot.
180
+ for (const directory of [
181
+ ...target.objc.headerSearchPaths,
182
+ ...target.objc.frameworkSearchPaths,
183
+ ])
184
+ if (!isSdkInclude(directory) && fs.existsSync(directory)) addHeaders(directory)
155
185
  targets.push({
156
186
  name: target.moduleName,
157
187
  deploymentTarget: target.iosDeploymentTarget,
@@ -290,8 +320,8 @@ export function prepareXcodeObjCImplementations(
290
320
  return path.join(work, name, relative)
291
321
  }
292
322
  // native SDK include paths select the wasm SDK, never host system headers.
293
- if (file.startsWith('/usr/include/'))
294
- return path.join(sdk.sysroot, 'include', file.slice('/usr/include/'.length))
323
+ if (isSdkInclude(file))
324
+ return path.join(sdk.sysroot, 'include', file.slice('/usr/include'.length))
295
325
  throw new Error(
296
326
  `Objective-C compiler input is outside the project and its pinned packages: ${file}`,
297
327
  )