peachsim 0.1.1635 → 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 (129) hide show
  1. package/README.md +5 -1
  2. package/cli/app-config.ts +14 -5
  3. package/cli/app-stubs.ts +62 -13
  4. package/cli/bridge-flow-runner.ts +2 -2
  5. package/cli/cloud-client.ts +60 -40
  6. package/cli/commands/doctor.ts +13 -5
  7. package/cli/commands/flow.ts +1 -1
  8. package/cli/commands/inspect/shared.ts +9 -4
  9. package/cli/commands/inspect.ts +65 -47
  10. package/cli/commands/preview.ts +1 -1
  11. package/cli/commands/record.ts +97 -47
  12. package/cli/commands/swift.ts +9 -3
  13. package/cli/commands/upload.ts +48 -42
  14. package/cli/drivers/playwright-sim-host.ts +65 -42
  15. package/cli/main.ts +1 -1
  16. package/cli/outbound-endpoints.ts +7 -5
  17. package/cli/pause-aware-timeout.ts +4 -0
  18. package/cli/ws-bridge.ts +19 -10
  19. package/detox/attempt-events.cjs +62 -5
  20. package/detox/attempt-events.d.cts +7 -0
  21. package/detox/expectations.ts +7 -3
  22. package/detox/index.ts +121 -47
  23. package/detox/motion-change.cjs +170 -21
  24. package/detox/motion-change.d.cts +32 -0
  25. package/detox/navigation-lifecycle.ts +35 -3
  26. package/dist-lib/agent-daemon-client.cjs +1 -1
  27. package/dist-lib/agent-events.cjs +1 -1
  28. package/dist-lib/attached-projects.cjs +1 -1
  29. package/dist-lib/auth/shared-session.cjs +1 -1
  30. package/dist-lib/backend-origin.cjs +8 -7
  31. package/dist-lib/beta.cjs +1 -1
  32. package/dist-lib/beta.mjs +1 -1
  33. package/dist-lib/bridge-constants.cjs +1 -1
  34. package/dist-lib/bridge-contract-input.cjs +1 -1
  35. package/dist-lib/bridge-contract-input.mjs +1 -1
  36. package/dist-lib/bridge-contract.cjs +1 -1
  37. package/dist-lib/bridge-contract.mjs +1 -1
  38. package/dist-lib/bundle-channel.cjs +4038 -1983
  39. package/dist-lib/capture-contract.cjs +1 -1
  40. package/dist-lib/capture-contract.mjs +1 -1
  41. package/dist-lib/cloud-contract.cjs +1 -1
  42. package/dist-lib/cloud-contract.mjs +1 -1
  43. package/dist-lib/cloud-test-args.cjs +1 -1
  44. package/dist-lib/cloud-test-args.mjs +1 -1
  45. package/dist-lib/cloud.cjs +1 -1
  46. package/dist-lib/cloud.mjs +1 -1
  47. package/dist-lib/config.cjs +1 -1
  48. package/dist-lib/config.mjs +1 -1
  49. package/dist-lib/detox/index.cjs +509 -86
  50. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  51. package/dist-lib/expo-swift-bundle.cjs +3551 -1574
  52. package/dist-lib/expo-swift-metadata.cjs +9 -7
  53. package/dist-lib/expo-swift-metadata.mjs +9 -7
  54. package/dist-lib/home-paths.cjs +1 -1
  55. package/dist-lib/host/bridge-host.cjs +43633 -2635
  56. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  57. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  58. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  59. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  60. package/dist-lib/host/websocket-proxy.cjs +1 -1
  61. package/dist-lib/index.cjs +2748 -128
  62. package/dist-lib/jump-to-source-babel.cjs +1 -1
  63. package/dist-lib/jump-to-source-native.cjs +1 -1
  64. package/dist-lib/kotlin-host.cjs +1 -1
  65. package/dist-lib/menu.cjs +1 -1
  66. package/dist-lib/menu.mjs +1 -1
  67. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  68. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  69. package/dist-lib/metro-production-bundle.cjs +1 -1
  70. package/dist-lib/metro-production-bundle.mjs +1 -1
  71. package/dist-lib/metro.cjs +2547 -83
  72. package/dist-lib/native-app-identity-project.cjs +237 -50
  73. package/dist-lib/native-app-identity-project.mjs +237 -50
  74. package/dist-lib/native-app-identity.cjs +29 -30
  75. package/dist-lib/native-app-identity.mjs +29 -30
  76. package/dist-lib/native-firebase-metadata.cjs +1 -1
  77. package/dist-lib/native-firebase-metadata.mjs +1 -1
  78. package/dist-lib/plist.cjs +1 -1
  79. package/dist-lib/plist.mjs +1 -1
  80. package/dist-lib/profiles.cjs +1 -1
  81. package/dist-lib/public-brand.cjs +1 -1
  82. package/dist-lib/react-native-host-modules.cjs +2 -2
  83. package/dist-lib/react-native-host-modules.mjs +2 -2
  84. package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
  85. package/dist-lib/sdk.cjs +1 -1
  86. package/dist-lib/sdk.mjs +1 -1
  87. package/dist-lib/skills.cjs +65403 -22739
  88. package/dist-lib/swift-compile.cjs +93 -32
  89. package/dist-lib/swift-compile.mjs +93 -32
  90. package/dist-lib/swift-package.cjs +1 -1
  91. package/dist-lib/swift-package.mjs +1 -1
  92. package/dist-lib/swift-submit.cjs +7 -5
  93. package/dist-lib/swift-submit.mjs +7 -5
  94. package/dist-lib/swift-toolchain.cjs +316 -41
  95. package/dist-lib/swift.cjs +1 -1
  96. package/dist-lib/vite-swift.cjs +7335 -5432
  97. package/dist-lib/vite.cjs +2857 -12
  98. package/dist-lib/worklet-executable.cjs +14 -2
  99. package/dist-lib/worklet-executable.mjs +14 -2
  100. package/package.json +1 -1
  101. package/src/backend-origin.ts +13 -6
  102. package/src/expo-swift-host.ts +5 -2
  103. package/src/expoKotlinContract.ts +4 -7
  104. package/src/expoSwiftContract.ts +4 -6
  105. package/src/host/staged-host-files.ts +9 -0
  106. package/src/native-app-identity-project.ts +1 -4
  107. package/src/native-app-identity.ts +1 -4
  108. package/src/nativeAppIconContract.ts +1 -4
  109. package/src/output-lock-stdio.ts +36 -0
  110. package/src/react-native-bridge-modules.ts +21 -6
  111. package/src/react-native-host-modules.ts +4 -1
  112. package/src/settings-bundle.ts +1 -4
  113. package/src/swift-compile.ts +91 -33
  114. package/src/swift-distribution.lock.json +22 -12
  115. package/src/swift-distribution.ts +103 -4
  116. package/src/swift-host.ts +89 -26
  117. package/src/swift-icu.ts +16 -10
  118. package/src/swift-objc-compile.ts +355 -0
  119. package/src/swift-objc-sdk.ts +689 -18
  120. package/src/swift-objc-standin.ts +126 -14
  121. package/src/swift-package-manifest.ts +39 -1
  122. package/src/swift-packages.ts +35 -21
  123. package/src/swift-standin-emit.ts +40 -7
  124. package/src/swift-standins.ts +404 -138
  125. package/src/swift-submit.ts +3 -5
  126. package/src/swift-toolchain.ts +378 -46
  127. package/src/worker-system-chunks.ts +11 -12
  128. package/src/worklet-executable.ts +19 -1
  129. package/src/xcode-project.ts +270 -30
package/src/swift-host.ts CHANGED
@@ -15,6 +15,8 @@ import { authHeaderValue, resolveCliAuth } from '../cli/auth'
15
15
  import { resolveContrastBackendOrigin } from './backend-origin'
16
16
  import { cacheDir } from './home-paths'
17
17
  import iosSdk from './ios-sdk-frameworks.json' with { type: 'json' }
18
+ import { type NativeAppIdentity } from './native-app-identity'
19
+ import { readNativeAppIdentityFromXcodeTarget } from './native-app-identity-project'
18
20
  import { swiftBinaryModules } from './swift-binary-modules'
19
21
  import {
20
22
  compileSwiftProjectUnit,
@@ -28,6 +30,7 @@ import {
28
30
  } from './swift-compile'
29
31
  import { designTimeValues } from './swift-design-time'
30
32
  import { swiftDistributionRoot, swiftModuleToolchain } from './swift-distribution'
33
+ import { compileXcodeObjCImplementations } from './swift-objc-compile'
31
34
  import {
32
35
  swiftImplicitResourcePaths,
33
36
  swiftMetalSourceFiles,
@@ -267,7 +270,9 @@ export function openXcodeProject(
267
270
  moduleRoot: string | undefined,
268
271
  configuration?: string,
269
272
  ): string {
270
- const app = readXcodeAppTarget(xcodeproj, target, configuration)
273
+ const app = readXcodeAppTarget(xcodeproj, target, configuration, {
274
+ prepareGeneratedSources: true,
275
+ })
271
276
  const root = app.rootDir
272
277
  xcodeProjects.set(root, {
273
278
  xcodeproj,
@@ -312,20 +317,20 @@ function swiftPackages(
312
317
  projectGaps: Map<string, string>
313
318
  binaries: SwiftPackageBinary[]
314
319
  } {
315
- const stamp = (file: string | null) =>
316
- file && fs.existsSync(file) ? fs.statSync(file).mtimeMs : 0
320
+ const stamp = (file: string) =>
321
+ fs.existsSync(file)
322
+ ? createHash('sha256').update(fs.readFileSync(file)).digest('hex')
323
+ : null
317
324
  const key = JSON.stringify([
318
325
  app.linkedProducts,
319
- app.packageResolved,
320
- stamp(app.packageResolved),
326
+ app.packageResolved.map((file) => [file, stamp(file)]),
321
327
  app.localPackageDirs.map((dir) => stamp(path.join(dir, 'Package.swift'))),
322
328
  app.linkedProducts.map(({ source }) =>
323
329
  source.kind === 'local' ? stamp(path.join(source.dir, 'Package.swift')) : 0,
324
330
  ),
325
331
  ])
326
- // the package key is cheap stamps; the stand-in key covers the generation
327
- // inputs the stamps cannot see (artifact contents, peach's modules, the
328
- // emitter), so a changed input rebuilds instead of reusing stale output.
332
+ // resolution follows lock and manifest contents even when a checkout restores
333
+ // timestamps. the stand-in key also covers artifacts, peach modules and emitter.
329
334
  let cached = entry.packages
330
335
  if (cached?.key !== key) {
331
336
  const packages = resolveSwiftPackages({
@@ -345,6 +350,13 @@ function swiftPackages(
345
350
  }
346
351
  entry.packages = cached
347
352
  }
353
+ if (
354
+ entry.moduleRoot &&
355
+ [app, ...app.frameworks].some((target) =>
356
+ target.allSources.some((file) => /\.mm?$/.test(file)),
357
+ )
358
+ )
359
+ compileXcodeObjCImplementations(app, cached.binaries, entry.moduleRoot)
348
360
  // a reached binary target with stand-ins for every module it ships
349
361
  // resolves as provided, so what depends on it compiles; the rest keep
350
362
  // their reason with why no stand-in was built. surfaces peach already
@@ -450,11 +462,26 @@ function xcodeLayout(root: string): XcodeLayout | null {
450
462
  registered.xcodeproj,
451
463
  registered.target,
452
464
  registered.configuration,
465
+ { prepareGeneratedSources: true },
453
466
  )
467
+ const implementations = [app, ...app.frameworks].flatMap((target) =>
468
+ target.allSources
469
+ .filter((file) => /\.mm?$/i.test(file))
470
+ .map((file) => `${target.name}: ${path.relative(app.rootDir, file)}`),
471
+ )
472
+ if (implementations.length > 0 && !registered.moduleRoot)
473
+ throw new Error(
474
+ `Peach cannot compile Objective-C implementations in Xcode targets until the runtime and shared Swift object bridge are available:\n${implementations.map((file) => ` ${file}`).join('\n')}`,
475
+ )
454
476
  const appSources = app.allSources.filter(
455
477
  (file) => file.endsWith('.swift') || file.endsWith('.metal'),
456
478
  )
457
- const files = new Set([...app.projectFiles, ...appSources, ...app.resources])
479
+ const files = new Set([
480
+ ...app.projectFiles,
481
+ ...app.packageResolved,
482
+ ...appSources,
483
+ ...app.resources,
484
+ ])
458
485
  if (app.infoPlistFile) files.add(app.infoPlistFile)
459
486
  const targetDirs = new Map(
460
487
  [...appSources, ...app.resources].map((file) => [file, XCODE_APP_DIR]),
@@ -605,10 +632,18 @@ function xcodeManifest(layout: XcodeLayout): string {
605
632
 
606
633
  // preflight the entire input tree before reading any source or resource bytes.
607
634
  // a nested package is a separate compile unit; ignored build trees never enter it.
608
- function packageFiles(root: string): string[] {
635
+ function packageFiles(root: string, includeBuildInputs = true): string[] {
609
636
  const canonicalRoot = fs.realpathSync(root)
610
637
  const xcode = xcodeLayout(canonicalRoot)
611
- return xcode ? xcode.files : sourceTreeFiles(canonicalRoot, canonicalRoot)
638
+ if (!xcode) return sourceTreeFiles(canonicalRoot, canonicalRoot)
639
+ if (includeBuildInputs) return xcode.files
640
+ const buildInputs = new Set(xcode.app.projectFiles)
641
+ return xcode.files.filter(
642
+ (file) =>
643
+ !buildInputs.has(file) ||
644
+ xcode.targetDirs.has(file) ||
645
+ xcode.packages.targets.some((target) => target.files.includes(file)),
646
+ )
612
647
  }
613
648
 
614
649
  // remote dependencies are enumerated from peach's checkout cache, while app
@@ -668,16 +703,18 @@ function isPackageSource(file: string): boolean {
668
703
  }
669
704
 
670
705
  export function sourceFiles(root: string): string[] {
671
- return packageFiles(root).filter(isPackageSource)
706
+ return packageFiles(root, false).filter(isPackageSource)
672
707
  }
673
708
 
674
709
  // project files and the xcconfigs their settings read are build inputs, never resources
675
710
  function resourceCandidateFiles(root: string): string[] {
676
- return packageFiles(root).filter(
711
+ return packageFiles(root, false).filter(
677
712
  (file) =>
678
713
  !isPackageSource(file) &&
679
714
  !file.endsWith('.xcconfig') &&
680
- !['project.pbxproj', 'project.xcproj'].includes(path.basename(file)),
715
+ !['project.pbxproj', 'project.xcproj', 'Package.resolved'].includes(
716
+ path.basename(file),
717
+ ),
681
718
  )
682
719
  }
683
720
 
@@ -685,6 +722,7 @@ function resourceCandidateFiles(root: string): string[] {
685
722
  export function swiftProjectAppInfo(packagePath: string): {
686
723
  name: string
687
724
  icon: { bytes: Buffer; contentType: string } | null
725
+ identity: NativeAppIdentity | null
688
726
  } {
689
727
  const registered = xcodeProjects.get(packagePath)
690
728
  const app = registered
@@ -747,12 +785,17 @@ export function swiftProjectAppInfo(packagePath: string): {
747
785
  }
748
786
  break
749
787
  }
750
- return { name: app?.name ?? path.basename(packagePath), icon }
788
+ return {
789
+ name: app?.name ?? path.basename(packagePath),
790
+ icon,
791
+ identity: app ? readNativeAppIdentityFromXcodeTarget(app) : null,
792
+ }
751
793
  }
752
794
 
753
795
  // the hosted compile service caps what a project sends it; a local compile
754
796
  // reads the author's own files, so nothing caps them there.
755
797
  const HOSTED_LIMITS = {
798
+ sourceBytes: MAX_SWIFT_COMPILE_SOURCE_BYTES,
756
799
  resources: MAX_SWIFT_COMPILE_RESOURCES,
757
800
  resourceBytes: MAX_SWIFT_COMPILE_RESOURCE_BYTES,
758
801
  resourceTotalBytes: MAX_SWIFT_COMPILE_RESOURCE_TOTAL_BYTES,
@@ -761,6 +804,7 @@ const HOSTED_LIMITS = {
761
804
  imageTotalBytes: MAX_SWIFT_COMPILE_IMAGE_TOTAL_BYTES,
762
805
  }
763
806
  const LOCAL_LIMITS: typeof HOSTED_LIMITS = {
807
+ sourceBytes: Infinity,
764
808
  resources: Infinity,
765
809
  resourceBytes: Infinity,
766
810
  resourceTotalBytes: Infinity,
@@ -969,7 +1013,7 @@ export function buildSwiftBundleFiles(
969
1013
  const xcode = xcodeLayout(packagePath)
970
1014
  const bundles =
971
1015
  xcode?.resourceBundles ??
972
- swiftResourceBundles(inputs ?? packageSources(packagePath), relative)
1016
+ swiftResourceBundles(inputs ?? packageSources(packagePath, hosted), relative)
973
1017
  const addFile = (key: string, bytes: Buffer) => {
974
1018
  if (
975
1019
  Object.keys(files).length >= limits.resources ||
@@ -1175,6 +1219,7 @@ export function standinLibraries(
1175
1219
 
1176
1220
  function packageSources(
1177
1221
  packagePath: string,
1222
+ hosted = false,
1178
1223
  linked: ReadonlySet<SwiftStandin> = new Set(),
1179
1224
  ): Record<string, string> {
1180
1225
  const xcode = xcodeLayout(packagePath)
@@ -1184,13 +1229,14 @@ function packageSources(
1184
1229
  ),
1185
1230
  )
1186
1231
  const files: Record<string, string> = {}
1232
+ const limits = inputLimits(hosted)
1187
1233
  for (const file of sourceFiles(packagePath)) {
1188
1234
  const relative = path.relative(packagePath, file).split(path.sep).join(path.posix.sep)
1189
1235
  if (manifests.has(file)) continue
1190
1236
  files[xcode ? xcodeInputKey(xcode, file, relative) : relative] = readPackageFile(
1191
1237
  packagePath,
1192
1238
  file,
1193
- MAX_SWIFT_COMPILE_SOURCE_BYTES,
1239
+ limits.sourceBytes,
1194
1240
  ).toString('utf8')
1195
1241
  }
1196
1242
  if (xcode) {
@@ -1221,7 +1267,11 @@ function packageSnapshot(project: SwiftProject, entry: string): SwiftPackageSnap
1221
1267
  const linked = layout
1222
1268
  ? standinLibraries(layout.binaries, layout.standins, layout.app.rootDir)
1223
1269
  : new Map<string, SwiftStandin[]>()
1224
- const files = packageSources(project.packagePath, new Set([...linked.values()].flat()))
1270
+ const files = packageSources(
1271
+ project.packagePath,
1272
+ Boolean(project.hosted),
1273
+ new Set([...linked.values()].flat()),
1274
+ )
1225
1275
  const native = nativeSourceDescriptor(project.packagePath)
1226
1276
  return {
1227
1277
  files,
@@ -1262,7 +1312,7 @@ function packageSnapshot(project: SwiftProject, entry: string): SwiftPackageSnap
1262
1312
  glue: readPackageFile(
1263
1313
  project.packagePath,
1264
1314
  path.join(project.packagePath, NATIVE_GLUE),
1265
- MAX_SWIFT_COMPILE_SOURCE_BYTES,
1315
+ inputLimits(Boolean(project.hosted)).sourceBytes,
1266
1316
  ).toString('utf8'),
1267
1317
  }
1268
1318
  : {}),
@@ -1411,6 +1461,21 @@ const API_GAP_PATTERNS: [RegExp, (match: RegExpExecArray) => string][] = [
1411
1461
  (match) => `${match[1]}.${match[2]}`,
1412
1462
  ],
1413
1463
  [/^'([^']+)' is unavailable/, (match) => match[1]!],
1464
+ // an instance api Peach declares static, or the reverse
1465
+ [
1466
+ /^(?:static|instance) member '([^']+)' cannot be used on (?:instance of )?type '([^']+)'/,
1467
+ (match) => `${match[2]}.${match[1]}`,
1468
+ ],
1469
+ // an overridable api Peach declares in an extension
1470
+ [
1471
+ /^(?:non-@objc )?[\w ]+ '([^']+)' is declared in extension of '([^']+)' and cannot be overridden/,
1472
+ (match) => `${match[2]}.${match[1]}`,
1473
+ ],
1474
+ // a form of a macro Peach's expansion does not accept
1475
+ [
1476
+ /^(@\w+) requires an initial value/,
1477
+ (match) => `${match[1]} without an initial value`,
1478
+ ],
1414
1479
  [/^no such module '([^']+)'/, (match) => `module ${match[1]}`],
1415
1480
  ]
1416
1481
 
@@ -1425,7 +1490,7 @@ export function swiftApiGapReport(output: string): string | null {
1425
1490
  const file = diagnostic[1]!
1426
1491
  .replace(new RegExp(`^(?:${XCODE_APP_DIR}|${XCODE_FRAMEWORKS_DIR}/[^/]+)/`), '')
1427
1492
  .replace(new RegExp(`^${XCODE_PACKAGES_DIR}/`), '')
1428
- const message = diagnostic[4]!
1493
+ const message = diagnostic[4]!.trim()
1429
1494
  const rule = API_GAP_PATTERNS.find(([pattern]) => pattern.test(message))
1430
1495
  const key = rule ? rule[1](rule[0].exec(message)!) : message
1431
1496
  const table = rule ? gaps : other
@@ -1726,9 +1791,8 @@ export function swiftPackageWatchFiles(
1726
1791
  packagePath = fs.realpathSync(packagePath)
1727
1792
  return [
1728
1793
  ...(xcodeProjects.has(packagePath) ? [] : [path.join(packagePath, 'Package.swift')]),
1729
- ...sourceFiles(packagePath),
1794
+ ...packageFiles(packagePath),
1730
1795
  ...nativeSourceFiles(packagePath),
1731
- ...resourceCandidateFiles(packagePath),
1732
1796
  ...(moduleRoot
1733
1797
  ? swiftModuleInputFiles(moduleRoot).map((file) => path.join(moduleRoot, file))
1734
1798
  : []),
@@ -1744,14 +1808,14 @@ export async function swiftPackageMetadata(
1744
1808
  signal?: AbortSignal,
1745
1809
  ): Promise<SwiftEntryModuleArgs> {
1746
1810
  packagePath = fs.realpathSync(packagePath)
1811
+ const project = projectFor(packagePath, hosted, signal)
1812
+ const limits = inputLimits(Boolean(project.hosted))
1747
1813
  const mains = sourceFiles(packagePath).filter(
1748
1814
  (file) =>
1749
1815
  file.endsWith('.swift') &&
1750
1816
  swiftApplicationEntry(
1751
1817
  file,
1752
- readPackageFile(packagePath, file, MAX_SWIFT_COMPILE_SOURCE_BYTES).toString(
1753
- 'utf8',
1754
- ),
1818
+ readPackageFile(packagePath, file, limits.sourceBytes).toString('utf8'),
1755
1819
  ),
1756
1820
  )
1757
1821
  const native = nativeSourceDescriptor(packagePath)
@@ -1760,7 +1824,6 @@ export async function swiftPackageMetadata(
1760
1824
  `swift package ${packageName} needs exactly one application entry source or generated native modules; found ${mains.length}`,
1761
1825
  )
1762
1826
  }
1763
- const project = projectFor(packagePath, hosted, signal)
1764
1827
  const entry =
1765
1828
  mains.length === 1
1766
1829
  ? path.relative(packagePath, mains[0]).split(path.sep).join(path.posix.sep)
package/src/swift-icu.ts CHANGED
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from 'node:fs'
4
4
  import path from 'node:path'
5
5
  import { promisify } from 'node:util'
6
6
  import { gzip } from 'node:zlib'
7
- import { swiftSdkOptions } from './swift-toolchain.ts'
7
+ import { swiftEnvironment, swiftSdkOptions } from './swift-toolchain.ts'
8
8
 
9
9
  /**
10
10
  * the icu common data a swift artifact reserves and its host fills
@@ -73,17 +73,23 @@ async function load(toolchainBin: string, sdk: string): Promise<Buffer> {
73
73
  // missing piece instead of shelling out to sdk configure.
74
74
  if (!existsSync(swift))
75
75
  throw new Error(`swift-icu: no swift at ${swift} for the ${sdk} SDK's icu data`)
76
- const { stdout: listed } = await run(swift, ['sdk', 'list', ...swiftSdkOptions()])
76
+ const { stdout: listed } = await run(swift, ['sdk', 'list', ...swiftSdkOptions()], {
77
+ env: swiftEnvironment(),
78
+ })
77
79
  if (!listed.split('\n').some((line) => line.trim() === sdk))
78
80
  throw new Error(`swift-icu: ${sdk} is not installed for ${swift}`)
79
- const { stdout } = await run(swift, [
80
- 'sdk',
81
- 'configure',
82
- ...swiftSdkOptions(),
83
- '--show-configuration',
84
- sdk,
85
- 'wasm32-unknown-wasip1',
86
- ])
81
+ const { stdout } = await run(
82
+ swift,
83
+ [
84
+ 'sdk',
85
+ 'configure',
86
+ ...swiftSdkOptions(),
87
+ '--show-configuration',
88
+ sdk,
89
+ 'wasm32-unknown-wasip1',
90
+ ],
91
+ { env: swiftEnvironment() },
92
+ )
87
93
  const resources = /^swiftStaticResourcesPath: (.+)$/m.exec(stdout)?.[1]?.trim()
88
94
  if (!resources) throw new Error(`swift-icu: ${sdk} names no static resources`)
89
95
  // a copy, so the 42 MB archive it was sliced from can be collected
@@ -0,0 +1,355 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { cacheDir } from './home-paths'
6
+ import { swiftSdkPaths, swiftModuleSdk } from './swift-compile'
7
+ import { swiftModuleToolchain } from './swift-distribution'
8
+ import { IOS_SDK_ID, materializeObjcSdk } from './swift-objc-standin'
9
+ import { swiftTargetModuleName } from './swift-package'
10
+ import { swiftPackageManifest, swiftTargetFiles } from './swift-package-manifest'
11
+ import { remoteSwiftCheckouts, type SwiftPackageBinary } from './swift-packages'
12
+ import { readPackageFile } from './swift-source-file'
13
+ import { swiftEnvironment } from './swift-toolchain'
14
+ import type { XcodeAppTarget } from './xcode-project'
15
+
16
+ /** compile original implementations before any header-derived replacement can hide them.
17
+ * successful objects still require the canonical framework bridge before linking. */
18
+ export function compileXcodeObjCImplementations(
19
+ app: XcodeAppTarget,
20
+ binaries: readonly SwiftPackageBinary[],
21
+ moduleRoot: string,
22
+ ): never {
23
+ const toolchain = swiftModuleToolchain(moduleRoot)
24
+ const sdk = swiftSdkPaths(moduleRoot, toolchain.toolchainBin)
25
+ const sources = new Map<string, Buffer>()
26
+ const links: Record<string, string> = {}
27
+ const checkouts = remoteSwiftCheckouts(app.rootDir)
28
+ const add = (file: string, header = false) => {
29
+ const resolved = header ? fs.realpathSync(file) : file
30
+ if (resolved !== file) links[file] = resolved
31
+ if (!sources.has(file))
32
+ sources.set(
33
+ file,
34
+ readPackageFile(
35
+ file.startsWith(`${checkouts}${path.sep}`) ? checkouts : app.rootDir,
36
+ resolved,
37
+ Infinity,
38
+ ),
39
+ )
40
+ }
41
+ const addHeaders = (directory: string): void => {
42
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
43
+ const file = path.join(directory, entry.name)
44
+ if (entry.isDirectory()) addHeaders(file)
45
+ else if (/\.(h|hh|hpp|hxx|inc|inl|modulemap)$/.test(entry.name)) add(file, true)
46
+ }
47
+ }
48
+ const targets: {
49
+ name: string
50
+ deploymentTarget: string | null
51
+ files: string[]
52
+ includes: string[]
53
+ frameworks: string[]
54
+ flags: string[]
55
+ cxxFlags: string[]
56
+ prefix: string | null
57
+ fileFlags: Record<string, string[]>
58
+ publicHeaders?: string
59
+ }[] = []
60
+ const configuration = app.configuration === 'Debug' ? 'debug' : 'release'
61
+ for (const binary of binaries) {
62
+ if (!binary.reached || binary.publicHeaders === undefined) continue
63
+ const manifest = swiftPackageManifest(binary.dir)
64
+ const target = manifest.targets.get(binary.target)
65
+ if (!target)
66
+ throw new Error(`missing original implementation target ${binary.target}`)
67
+ const files = swiftTargetFiles(manifest, target)
68
+ for (const file of files) add(file)
69
+ addHeaders(path.join(target.dir, target.publicHeadersPath))
70
+ for (const dependency of binary.headerDependencies ?? [])
71
+ addHeaders(path.resolve(binary.dir, dependency.path))
72
+ const settings = target.nativeSettings.filter(
73
+ (setting) =>
74
+ setting.configuration === null || setting.configuration === configuration,
75
+ )
76
+ targets.push({
77
+ name: target.name,
78
+ deploymentTarget: app.iosDeploymentTarget,
79
+ files: files.filter((file) => /\.(c|cc|cpp|cxx|m|mm)$/.test(file)),
80
+ includes: [
81
+ path.join(target.dir, target.publicHeadersPath),
82
+ ...(binary.headerDependencies ?? []).map((dependency) =>
83
+ path.resolve(binary.dir, dependency.path),
84
+ ),
85
+ ],
86
+ frameworks: [],
87
+ // retain the source iOS package's ARC and module semantics on the wasm ABI.
88
+ flags: [
89
+ '-fobjc-arc',
90
+ '-fmodules',
91
+ '-DSWIFT_PACKAGE',
92
+ ...(configuration === 'debug' ? ['-DDEBUG'] : []),
93
+ ...settings
94
+ .filter((setting) => setting.tool === 'c')
95
+ .flatMap((setting) => setting.flags),
96
+ ],
97
+ cxxFlags: settings
98
+ .filter((setting) => setting.tool === 'cxx')
99
+ .flatMap((setting) => setting.flags),
100
+ prefix: null,
101
+ fileFlags: {},
102
+ publicHeaders: path.join(target.dir, target.publicHeadersPath),
103
+ })
104
+ }
105
+ for (const target of [...app.frameworks, app]) {
106
+ if (!target.objc) continue
107
+ const files = target.allSources.filter((file) => /\.(c|cc|cpp|cxx|m|mm)$/.test(file))
108
+ for (const file of files) add(file)
109
+ for (const file of [
110
+ ...target.objc.headers,
111
+ ...[target.objc.prefixHeader, target.objc.bridgingHeader].filter(
112
+ (file): file is string => file !== null,
113
+ ),
114
+ ])
115
+ add(file, true)
116
+ targets.push({
117
+ name: target.moduleName,
118
+ deploymentTarget: target.iosDeploymentTarget,
119
+ files,
120
+ includes: target.objc.headerSearchPaths,
121
+ frameworks: target.objc.frameworkSearchPaths,
122
+ flags: target.objc.flags,
123
+ cxxFlags: target.objc.cxxFlags,
124
+ prefix: target.objc.prefixHeader,
125
+ fileFlags: target.objc.fileFlags,
126
+ })
127
+ }
128
+ const digest = (bytes: Buffer | string) =>
129
+ createHash('sha256').update(bytes).digest('hex')
130
+ const inputs = Object.fromEntries(
131
+ [...sources].map(([file, bytes]) => [file, digest(bytes)]),
132
+ )
133
+ const identity = {
134
+ version: 1,
135
+ release: toolchain.release,
136
+ compiler: digest(fs.readFileSync(path.join(toolchain.toolchainBin, 'clang'))),
137
+ sdk: swiftModuleSdk(moduleRoot),
138
+ declarations: IOS_SDK_ID,
139
+ targets,
140
+ inputs,
141
+ links,
142
+ }
143
+ const parent = path.join(cacheDir(), 'swift-objc-implementations')
144
+ fs.mkdirSync(parent, { recursive: true })
145
+ const key = digest(JSON.stringify(identity))
146
+ const work = path.join(parent, key)
147
+ const resultFile = path.join(work, 'result.json')
148
+ const record = (value: unknown): value is Record<string, unknown> =>
149
+ value !== null && typeof value === 'object' && !Array.isArray(value)
150
+ if (fs.existsSync(work)) {
151
+ let message: string
152
+ try {
153
+ const result: unknown = JSON.parse(
154
+ readPackageFile(work, resultFile, Infinity).toString(),
155
+ )
156
+ if (
157
+ !record(result) ||
158
+ result.key !== key ||
159
+ typeof result.message !== 'string' ||
160
+ !result.message ||
161
+ !record(result.files) ||
162
+ Object.keys(result.files).length < sources.size + 1 ||
163
+ !('receipt.json' in result.files)
164
+ )
165
+ throw new Error('invalid completed receipt')
166
+ for (const [file, hash] of Object.entries(result.files)) {
167
+ if (
168
+ typeof hash !== 'string' ||
169
+ digest(readPackageFile(work, path.resolve(work, file), Infinity)) !== hash
170
+ )
171
+ throw new Error(`retained file changed: ${file}`)
172
+ }
173
+ message = result.message
174
+ } catch (cause) {
175
+ throw new Error(
176
+ `Objective-C implementation preparation has an incomplete or damaged receipt at ${work}`,
177
+ { cause },
178
+ )
179
+ }
180
+ throw new Error(message)
181
+ }
182
+ // exclusive creation prevents another caller from writing the same objects.
183
+ fs.mkdirSync(work)
184
+ const staged = (file: string): string => {
185
+ for (const { root, name } of [
186
+ { root: checkouts, name: 'packages' },
187
+ { root: app.rootDir, name: 'project' },
188
+ ]) {
189
+ const relative = path.relative(root, file)
190
+ if (
191
+ relative !== '..' &&
192
+ !relative.startsWith(`..${path.sep}`) &&
193
+ !path.isAbsolute(relative)
194
+ )
195
+ return path.join(work, name, relative)
196
+ }
197
+ // native SDK include paths select the wasm SDK, never host system headers.
198
+ if (file.startsWith('/usr/include/'))
199
+ return path.join(sdk.sysroot, 'include', file.slice('/usr/include/'.length))
200
+ throw new Error(
201
+ `Objective-C compiler input is outside the project and its pinned packages: ${file}`,
202
+ )
203
+ }
204
+ for (const [file, bytes] of sources) {
205
+ const output = staged(file)
206
+ fs.mkdirSync(path.dirname(output), { recursive: true })
207
+ fs.writeFileSync(output, bytes)
208
+ }
209
+ const headers = materializeObjcSdk(path.join(cacheDir(), 'swift-standins'))
210
+ const modulemaps = targets.flatMap((target, index) => {
211
+ if (!target.publicHeaders) return []
212
+ const custom = path.join(target.publicHeaders, 'module.modulemap')
213
+ if (sources.has(custom)) return [`-fmodule-map-file=${staged(custom)}`]
214
+ const output = path.join(work, `module-${index}.modulemap`)
215
+ fs.writeFileSync(
216
+ output,
217
+ `module ${swiftTargetModuleName(target.name)} {\n umbrella ${JSON.stringify(staged(target.publicHeaders))}\n export *\n}\n`,
218
+ )
219
+ return [`-fmodule-map-file=${output}`]
220
+ })
221
+ const commands: {
222
+ target: string
223
+ source: string
224
+ args: string[]
225
+ status: number | null
226
+ signal: string | null
227
+ object?: string
228
+ }[] = []
229
+ const receipt = path.join(work, 'receipt.json')
230
+ const save = (status: string) =>
231
+ fs.writeFileSync(
232
+ receipt,
233
+ JSON.stringify({ ...identity, work, status, commands }, null, 2),
234
+ )
235
+ const finish = (status: string, message: string): never => {
236
+ save(status)
237
+ const files = [
238
+ ...[...sources.keys()].map(staged),
239
+ ...modulemaps.map((flag) => flag.slice('-fmodule-map-file='.length)),
240
+ receipt,
241
+ ...commands.flatMap((command) => {
242
+ const output = command.args[command.args.lastIndexOf('-o') + 1]!
243
+ return [output.replace(/\.o$/, '.log'), ...(command.object ? [output] : [])]
244
+ }),
245
+ ]
246
+ const result = {
247
+ key,
248
+ message,
249
+ files: Object.fromEntries(
250
+ files.map((file) => [path.relative(work, file), digest(fs.readFileSync(file))]),
251
+ ),
252
+ }
253
+ fs.writeFileSync(`${resultFile}.pending`, JSON.stringify(result, null, 2))
254
+ fs.renameSync(`${resultFile}.pending`, resultFile)
255
+ throw new Error(message)
256
+ }
257
+ save('compiling')
258
+ for (const [index, target] of targets.entries()) {
259
+ const version = /^(\d{1,2})(?:\.(\d{1,2}))?(?:\.(\d{1,2}))?$/.exec(
260
+ target.deploymentTarget ?? '',
261
+ )
262
+ if (!version) {
263
+ return finish(
264
+ 'unsupported-deployment-target',
265
+ `${target.name}: Objective-C compilation requires an explicit numeric IPHONEOS_DEPLOYMENT_TARGET; received ${JSON.stringify(target.deploymentTarget)}. Inputs: ${receipt}`,
266
+ )
267
+ }
268
+ const minimum =
269
+ Number(version[1]) * 10_000 +
270
+ Number(version[2] ?? 0) * 100 +
271
+ Number(version[3] ?? 0)
272
+ for (const [unit, file] of target.files.entries()) {
273
+ const output = path.join(work, `${index}-${unit}.o`)
274
+ const cxx = /\.(mm|cc|cpp|cxx)$/.test(file)
275
+ const objc = /\.mm?$/.test(file)
276
+ const flags = [
277
+ ...target.flags.filter(
278
+ (flag) =>
279
+ (!cxx || !flag.startsWith('-std=')) && (objc || flag !== '-fobjc-arc'),
280
+ ),
281
+ ...(cxx ? target.cxxFlags : []),
282
+ ...(target.fileFlags[file] ?? []),
283
+ ].map((flag, index, flags) => {
284
+ if (
285
+ ['-I', '-F', '-iquote', '-isystem', '-include'].includes(flags[index - 1] ?? '')
286
+ )
287
+ return staged(flag)
288
+ const include = /^(-I|-F)(\/.*)$/.exec(flag)
289
+ return include ? `${include[1]}${staged(include[2]!)}` : flag
290
+ })
291
+ const args = [
292
+ '--target=wasm32-unknown-wasip1',
293
+ `--sysroot=${sdk.sysroot}`,
294
+ '-fwasm-exceptions',
295
+ '-fexceptions',
296
+ '-D_WASI_EMULATED_SIGNAL',
297
+ '-D_WASI_EMULATED_MMAN',
298
+ `-D__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__=${minimum}`,
299
+ `-D__IPHONE_OS_VERSION_MIN_REQUIRED=${minimum}`,
300
+ ...(objc ? ['-fobjc-runtime=gnustep-2.2', '-fobjc-exceptions', '-fblocks'] : []),
301
+ '-F',
302
+ path.join(headers, 'System/Library/Frameworks'),
303
+ // installed libc and objc runtime headers own the target ABI.
304
+ '-idirafter',
305
+ path.join(headers, 'usr/include'),
306
+ ...target.includes.flatMap((include) => ['-I', staged(include)]),
307
+ ...target.frameworks.flatMap((framework) => ['-F', staged(framework)]),
308
+ ...modulemaps,
309
+ `-fmodule-name=${swiftTargetModuleName(target.name)}`,
310
+ `-fmodules-cache-path=${path.join(work, 'module-cache')}`,
311
+ ...flags,
312
+ ...(target.prefix ? ['-include', staged(target.prefix)] : []),
313
+ '-Os',
314
+ '-c',
315
+ staged(file),
316
+ '-o',
317
+ output,
318
+ ]
319
+ const result = spawnSync(
320
+ path.join(toolchain.toolchainBin, cxx ? 'clang++' : 'clang'),
321
+ args,
322
+ {
323
+ cwd: work,
324
+ encoding: 'utf8',
325
+ env: swiftEnvironment(),
326
+ timeout: 120_000,
327
+ maxBuffer: 16 * 1024 * 1024,
328
+ },
329
+ )
330
+ fs.writeFileSync(
331
+ path.join(work, `${index}-${unit}.log`),
332
+ `${result.stdout ?? ''}${result.stderr ?? ''}${result.error?.message ?? ''}`,
333
+ )
334
+ commands.push({
335
+ target: target.name,
336
+ source: file,
337
+ args,
338
+ status: result.status,
339
+ signal: result.signal,
340
+ ...(result.status === 0 ? { object: digest(fs.readFileSync(output)) } : {}),
341
+ })
342
+ if (result.error || result.status !== 0) {
343
+ return finish(
344
+ 'compile-failed',
345
+ `Objective-C implementation compile failed in ${target.name}: ${file}\n${result.stderr || result.stdout || result.error?.message}\nOriginal inputs, compiler command and diagnostics: ${receipt}`,
346
+ )
347
+ }
348
+ save('compiling')
349
+ }
350
+ }
351
+ return finish(
352
+ 'requires-framework-bridge',
353
+ `Objective-C implementations compiled, but linking their original method bodies requires the canonical Swift framework object bridge. No implementation was replaced. Inputs and objects: ${receipt}`,
354
+ )
355
+ }