peachsim 0.1.1640 → 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 (125) hide show
  1. package/README.md +5 -0
  2. package/cli/app-bundle-resources.ts +57 -0
  3. package/cli/app-stubs.ts +38 -13
  4. package/cli/cloud-session.ts +16 -0
  5. package/cli/commands/assert.ts +17 -4
  6. package/cli/commands/box.ts +33 -19
  7. package/cli/commands/control.ts +12 -0
  8. package/cli/commands/doctor.ts +24 -15
  9. package/cli/commands/inspect/actions.ts +3 -2
  10. package/cli/commands/inspect/core.ts +0 -8
  11. package/cli/commands/inspect.ts +0 -14
  12. package/cli/commands/platform.ts +2 -1
  13. package/cli/commands/record.ts +64 -28
  14. package/cli/commands/swift.ts +10 -2
  15. package/cli/commands/three-mode.ts +4 -38
  16. package/cli/commands/upload.ts +8 -8
  17. package/cli/drivers/playwright.ts +39 -0
  18. package/cli/flow-live-status.ts +0 -2
  19. package/cli/main.ts +1 -1
  20. package/cli/outbound-endpoints.ts +4 -2
  21. package/cli/ws-bridge.ts +0 -6
  22. package/detox/attempt-events.cjs +41 -5
  23. package/detox/index.ts +155 -109
  24. package/detox/jest-environment.cjs +5 -1
  25. package/detox/motion-change.cjs +7 -5
  26. package/detox/motion-change.d.cts +1 -0
  27. package/detox/motion-leg-starts.cjs +20 -0
  28. package/detox/motion-leg-starts.d.cts +8 -0
  29. package/dist-lib/agent-daemon-client.cjs +1 -1
  30. package/dist-lib/agent-events.cjs +1 -1
  31. package/dist-lib/attached-projects.cjs +1 -1
  32. package/dist-lib/auth/shared-session.cjs +1 -1
  33. package/dist-lib/backend-origin.cjs +12 -3
  34. package/dist-lib/beta.cjs +1 -1
  35. package/dist-lib/beta.mjs +1 -1
  36. package/dist-lib/bridge-constants.cjs +1 -1
  37. package/dist-lib/bridge-contract-input.cjs +1 -1
  38. package/dist-lib/bridge-contract-input.mjs +1 -1
  39. package/dist-lib/bridge-contract.cjs +1 -1
  40. package/dist-lib/bridge-contract.mjs +1 -1
  41. package/dist-lib/bundle-channel.cjs +2671 -1833
  42. package/dist-lib/capture-contract.cjs +1 -1
  43. package/dist-lib/capture-contract.mjs +1 -1
  44. package/dist-lib/cloud-contract.cjs +1 -1
  45. package/dist-lib/cloud-contract.mjs +1 -1
  46. package/dist-lib/cloud-test-args.cjs +1 -1
  47. package/dist-lib/cloud-test-args.mjs +1 -1
  48. package/dist-lib/cloud.cjs +1 -1
  49. package/dist-lib/cloud.mjs +1 -1
  50. package/dist-lib/config.cjs +1 -1
  51. package/dist-lib/config.mjs +1 -1
  52. package/dist-lib/detox/index.cjs +578 -130
  53. package/dist-lib/detox/jest-environment.cjs +5 -1
  54. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  55. package/dist-lib/expo-swift-bundle.cjs +2374 -1554
  56. package/dist-lib/expo-swift-metadata.cjs +1 -1
  57. package/dist-lib/expo-swift-metadata.mjs +1 -1
  58. package/dist-lib/home-paths.cjs +1 -1
  59. package/dist-lib/host/bridge-host.cjs +2968 -2141
  60. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  61. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  62. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  63. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  64. package/dist-lib/host/websocket-proxy.cjs +1 -1
  65. package/dist-lib/index.cjs +132 -57
  66. package/dist-lib/jump-to-source-babel.cjs +1 -1
  67. package/dist-lib/jump-to-source-native.cjs +1 -1
  68. package/dist-lib/kotlin-host.cjs +1 -1
  69. package/dist-lib/menu.cjs +14 -6
  70. package/dist-lib/menu.mjs +14 -6
  71. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  72. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  73. package/dist-lib/metro-production-bundle.cjs +1 -1
  74. package/dist-lib/metro-production-bundle.mjs +1 -1
  75. package/dist-lib/metro.cjs +120 -53
  76. package/dist-lib/native-app-identity-project.cjs +96 -44
  77. package/dist-lib/native-app-identity-project.mjs +95 -44
  78. package/dist-lib/native-app-identity.cjs +1 -1
  79. package/dist-lib/native-app-identity.mjs +1 -1
  80. package/dist-lib/native-firebase-metadata.cjs +1 -1
  81. package/dist-lib/native-firebase-metadata.mjs +1 -1
  82. package/dist-lib/plist.cjs +1 -1
  83. package/dist-lib/plist.mjs +1 -1
  84. package/dist-lib/profiles.cjs +1 -1
  85. package/dist-lib/public-brand.cjs +1 -1
  86. package/dist-lib/react-native-host-modules.cjs +1 -1
  87. package/dist-lib/react-native-host-modules.mjs +1 -1
  88. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  89. package/dist-lib/sdk.cjs +5 -3
  90. package/dist-lib/sdk.mjs +5 -3
  91. package/dist-lib/skills.cjs +3932 -2868
  92. package/dist-lib/swift-compile.cjs +587 -374
  93. package/dist-lib/swift-compile.mjs +595 -369
  94. package/dist-lib/swift-package.cjs +1 -1
  95. package/dist-lib/swift-package.mjs +1 -1
  96. package/dist-lib/swift-submit.cjs +8 -3
  97. package/dist-lib/swift-submit.mjs +8 -3
  98. package/dist-lib/swift-toolchain.cjs +1 -1
  99. package/dist-lib/swift.cjs +1 -1
  100. package/dist-lib/vite-swift.cjs +2263 -1467
  101. package/dist-lib/vite.cjs +1 -1
  102. package/dist-lib/worklet-executable.cjs +1 -1
  103. package/dist-lib/worklet-executable.mjs +1 -1
  104. package/package.json +1 -1
  105. package/src/appBundleResourcesContract.ts +51 -0
  106. package/src/backend-origin.ts +11 -1
  107. package/src/host/bridge-host.ts +17 -11
  108. package/src/host/staged-host-files.ts +28 -4
  109. package/src/launchMetadataContract.ts +1 -0
  110. package/src/menu.ts +12 -4
  111. package/src/native-app-identity-project.ts +47 -38
  112. package/src/react-native-bridge-modules.ts +89 -76
  113. package/src/runtime-delivery.ts +6 -7
  114. package/src/swift-compile.ts +150 -112
  115. package/src/swift-compiler-digest.ts +5 -3
  116. package/src/swift-distribution.lock.json +9 -8
  117. package/src/swift-host.ts +6 -4
  118. package/src/swift-objc-compile.ts +416 -194
  119. package/src/swift-objc-metadata.ts +204 -0
  120. package/src/swift-objc-sdk.ts +195 -2
  121. package/src/swift-package-manifest.ts +6 -3
  122. package/src/swift-root-lock.ts +302 -0
  123. package/src/swift-submit.ts +15 -2
  124. package/src/swiftContract.ts +12 -0
  125. package/src/xcode-project.ts +77 -7
@@ -5,14 +5,25 @@ import path from 'node:path'
5
5
  import { cacheDir } from './home-paths'
6
6
  import { swiftSdkPaths, swiftModuleSdk } from './swift-compile'
7
7
  import { swiftModuleToolchain } from './swift-distribution'
8
+ import {
9
+ compileObjcPeerMetadata,
10
+ ObjcPeerMetadataError,
11
+ validateObjcPeerClasses,
12
+ } from './swift-objc-metadata'
8
13
  import { IOS_SDK_ID, materializeObjcSdk } from './swift-objc-standin'
9
14
  import { swiftTargetModuleName } from './swift-package'
10
15
  import { swiftPackageManifest, swiftTargetFiles } from './swift-package-manifest'
11
16
  import { remoteSwiftCheckouts, type SwiftPackageBinary } from './swift-packages'
12
17
  import { readPackageFile } from './swift-source-file'
13
18
  import { swiftEnvironment } from './swift-toolchain'
19
+ import type {
20
+ SwiftObjCImplementationPreparation,
21
+ SwiftObjCPeerClass,
22
+ } from './swiftContract'
14
23
  import type { XcodeAppTarget } from './xcode-project'
15
24
 
25
+ export class SwiftObjCCompilationError extends Error {}
26
+
16
27
  /** compile original implementations before any header-derived replacement can hide them.
17
28
  * successful objects still require the canonical framework bridge before linking. */
18
29
  export function compileXcodeObjCImplementations(
@@ -20,6 +31,33 @@ export function compileXcodeObjCImplementations(
20
31
  binaries: readonly SwiftPackageBinary[],
21
32
  moduleRoot: string,
22
33
  ): never {
34
+ const prepared = prepareXcodeObjCImplementations(app, binaries, moduleRoot)
35
+ throw new SwiftObjCCompilationError(
36
+ `Objective-C implementations compiled, but linking their original method bodies requires the canonical Swift framework object bridge. No implementation was replaced. Inputs and objects: ${prepared.receipt}`,
37
+ )
38
+ }
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
+
53
+ /** prepares genuine objects without authorizing unsupported framework linkage. */
54
+ export function prepareXcodeObjCImplementations(
55
+ app: XcodeAppTarget,
56
+ binaries: readonly SwiftPackageBinary[],
57
+ moduleRoot: string,
58
+ peers?: readonly SwiftObjCPeerClass[],
59
+ ): SwiftObjCImplementationPreparation {
60
+ if (peers) validateObjcPeerClasses(peers)
23
61
  const toolchain = swiftModuleToolchain(moduleRoot)
24
62
  const sdk = swiftSdkPaths(moduleRoot, toolchain.toolchainBin)
25
63
  const sources = new Map<string, Buffer>()
@@ -38,12 +76,24 @@ export function compileXcodeObjCImplementations(
38
76
  ),
39
77
  )
40
78
  }
79
+ const walked = new Set<string>()
80
+ const ancestors = new Set<string>()
41
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)
42
87
  for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
88
+ if (entry.name.startsWith('.') && !entry.isFile()) continue
43
89
  const file = path.join(directory, entry.name)
44
- 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)
45
94
  else if (/\.(h|hh|hpp|hxx|inc|inl|modulemap)$/.test(entry.name)) add(file, true)
46
95
  }
96
+ ancestors.delete(real)
47
97
  }
48
98
  const targets: {
49
99
  name: string
@@ -66,13 +116,26 @@ export function compileXcodeObjCImplementations(
66
116
  throw new Error(`missing original implementation target ${binary.target}`)
67
117
  const files = swiftTargetFiles(manifest, target)
68
118
  for (const file of files) add(file)
69
- addHeaders(path.join(target.dir, target.publicHeadersPath))
119
+ // explicit sources select compile units, not the private headers they include.
120
+ addHeaders(target.dir)
70
121
  for (const dependency of binary.headerDependencies ?? [])
71
122
  addHeaders(path.resolve(binary.dir, dependency.path))
72
123
  const settings = target.nativeSettings.filter(
73
124
  (setting) =>
74
125
  setting.configuration === null || setting.configuration === configuration,
75
126
  )
127
+ const nativeFlags = settings.flatMap((setting) => setting.flags)
128
+ for (const [index, flag] of nativeFlags.entries()) {
129
+ const directory = ['-I', '-iquote', '-isystem'].includes(
130
+ nativeFlags[index - 1] ?? '',
131
+ )
132
+ ? flag
133
+ : /^-I(.+)$/.exec(flag)?.[1]
134
+ if (directory) {
135
+ const include = path.resolve(target.dir, directory)
136
+ if (fs.existsSync(include)) addHeaders(include)
137
+ }
138
+ }
76
139
  targets.push({
77
140
  name: target.name,
78
141
  deploymentTarget: app.iosDeploymentTarget,
@@ -113,6 +176,12 @@ export function compileXcodeObjCImplementations(
113
176
  ),
114
177
  ])
115
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)
116
185
  targets.push({
117
186
  name: target.moduleName,
118
187
  deploymentTarget: target.iosDeploymentTarget,
@@ -131,7 +200,9 @@ export function compileXcodeObjCImplementations(
131
200
  [...sources].map(([file, bytes]) => [file, digest(bytes)]),
132
201
  )
133
202
  const identity = {
134
- version: 1,
203
+ version: 8,
204
+ peers,
205
+ metadataDeploymentTarget: peers ? app.iosDeploymentTarget : undefined,
135
206
  release: toolchain.release,
136
207
  compiler: digest(fs.readFileSync(path.join(toolchain.toolchainBin, 'clang'))),
137
208
  sdk: swiftModuleSdk(moduleRoot),
@@ -144,212 +215,363 @@ export function compileXcodeObjCImplementations(
144
215
  fs.mkdirSync(parent, { recursive: true })
145
216
  const key = digest(JSON.stringify(identity))
146
217
  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)
218
+ const descriptor = fs.openSync(`${work}.lock`, 'a')
219
+ try {
220
+ // the inherited descriptor keeps the kernel lock after perl exits.
221
+ const locked = spawnSync(
222
+ 'perl',
223
+ ['-e', 'open(my $f, ">&=", 3) or die $!; flock($f, 2) or die $!'],
224
+ { stdio: ['ignore', 'pipe', 'pipe', descriptor], encoding: 'utf8' },
225
+ )
226
+ if (locked.error || locked.status !== 0)
227
+ throw new Error(
228
+ `Could not lock Objective-C implementation inputs: ${locked.error?.message ?? locked.stderr}`,
164
229
  )
165
- throw new Error('invalid completed receipt')
166
- for (const [file, hash] of Object.entries(result.files)) {
230
+ const resultFile = path.join(work, 'result.json')
231
+ const preparation = (): SwiftObjCImplementationPreparation => ({
232
+ receipt: path.join(work, 'receipt.json'),
233
+ objects: targets.flatMap((target, index) =>
234
+ target.files.map((source, unit) => ({
235
+ target: target.name,
236
+ source,
237
+ file: path.join(work, `${index}-${unit}.o`),
238
+ })),
239
+ ),
240
+ ...(peers
241
+ ? {
242
+ metadata: path.join(work, 'peer-metadata.o'),
243
+ registration: path.join(work, 'peer-registration.swift'),
244
+ }
245
+ : {}),
246
+ })
247
+ const record = (value: unknown): value is Record<string, unknown> =>
248
+ value !== null && typeof value === 'object' && !Array.isArray(value)
249
+ if (fs.existsSync(work)) {
250
+ let message: string | undefined
251
+ let interrupted = false
252
+ let prepared = false
253
+ try {
254
+ const result: unknown = JSON.parse(
255
+ readPackageFile(work, resultFile, Infinity).toString(),
256
+ )
257
+ if (
258
+ !record(result) ||
259
+ result.key !== key ||
260
+ typeof result.reusable !== 'boolean' ||
261
+ typeof result.prepared !== 'boolean' ||
262
+ typeof result.message !== 'string' ||
263
+ !result.message ||
264
+ !record(result.files) ||
265
+ Object.keys(result.files).length < sources.size + 1 ||
266
+ !('receipt.json' in result.files)
267
+ )
268
+ throw new Error('invalid completed receipt')
269
+ for (const [file, hash] of Object.entries(result.files)) {
270
+ if (
271
+ typeof hash !== 'string' ||
272
+ digest(readPackageFile(work, path.resolve(work, file), Infinity)) !== hash
273
+ )
274
+ throw new Error(`retained file changed: ${file}`)
275
+ }
276
+ if (result.prepared) {
277
+ for (const file of [
278
+ ...preparation().objects.map((object) => object.file),
279
+ ...(peers
280
+ ? [
281
+ 'peer-metadata.o',
282
+ 'peer-metadata.m',
283
+ 'peer-metadata.log',
284
+ 'peer-metadata.json',
285
+ 'peer-registration.swift',
286
+ ].map((file) => path.join(work, file))
287
+ : []),
288
+ ])
289
+ if (!(path.relative(work, file) in result.files))
290
+ throw new Error(`missing prepared object or metadata input: ${file}`)
291
+ prepared = true
292
+ } else if (result.reusable) message = result.message
293
+ else interrupted = true
294
+ } catch {
295
+ // no writer can remain under this kernel lock; rebuild invalid proof.
296
+ fs.rmSync(work, { recursive: true })
297
+ }
298
+ if (prepared) return preparation()
299
+ if (message !== undefined) throw new SwiftObjCCompilationError(message)
300
+ // retain the first and latest complete interruptions for this input identity.
301
+ if (interrupted) {
302
+ const first = `${work}-interrupted-first`
303
+ const destination = fs.existsSync(first) ? `${work}-interrupted-latest` : first
304
+ fs.rmSync(destination, { recursive: true, force: true })
305
+ fs.renameSync(work, destination)
306
+ }
307
+ }
308
+ fs.mkdirSync(work)
309
+ const staged = (file: string): string => {
310
+ for (const { root, name } of [
311
+ { root: checkouts, name: 'packages' },
312
+ { root: app.rootDir, name: 'project' },
313
+ ]) {
314
+ const relative = path.relative(root, file)
167
315
  if (
168
- typeof hash !== 'string' ||
169
- digest(readPackageFile(work, path.resolve(work, file), Infinity)) !== hash
316
+ relative !== '..' &&
317
+ !relative.startsWith(`..${path.sep}`) &&
318
+ !path.isAbsolute(relative)
170
319
  )
171
- throw new Error(`retained file changed: ${file}`)
320
+ return path.join(work, name, relative)
172
321
  }
173
- message = result.message
174
- } catch (cause) {
322
+ // native SDK include paths select the wasm SDK, never host system headers.
323
+ if (isSdkInclude(file))
324
+ return path.join(sdk.sysroot, 'include', file.slice('/usr/include'.length))
175
325
  throw new Error(
176
- `Objective-C implementation preparation has an incomplete or damaged receipt at ${work}`,
177
- { cause },
326
+ `Objective-C compiler input is outside the project and its pinned packages: ${file}`,
178
327
  )
179
328
  }
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)
329
+ for (const [file, bytes] of sources) {
330
+ const output = staged(file)
331
+ fs.mkdirSync(path.dirname(output), { recursive: true })
332
+ fs.writeFileSync(output, bytes)
196
333
  }
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',
334
+ const headers = materializeObjcSdk(path.join(cacheDir(), 'swift-standins'))
335
+ const modulemaps = targets.flatMap((target, index) => {
336
+ if (!target.publicHeaders) return []
337
+ const custom = path.join(target.publicHeaders, 'module.modulemap')
338
+ if (sources.has(custom)) return [`-fmodule-map-file=${staged(custom)}`]
339
+ const output = path.join(work, `module-${index}.modulemap`)
340
+ fs.writeFileSync(
317
341
  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
- },
342
+ `module ${swiftTargetModuleName(target.name)} {\n umbrella ${JSON.stringify(staged(target.publicHeaders))}\n export *\n}\n`,
329
343
  )
344
+ return [`-fmodule-map-file=${output}`]
345
+ })
346
+ const commands: {
347
+ target: string
348
+ source: string
349
+ args: string[]
350
+ status: number | null
351
+ signal: string | null
352
+ error?: string
353
+ object?: string
354
+ }[] = []
355
+ const receipt = path.join(work, 'receipt.json')
356
+ const save = (status: string) =>
330
357
  fs.writeFileSync(
331
- path.join(work, `${index}-${unit}.log`),
332
- `${result.stdout ?? ''}${result.stderr ?? ''}${result.error?.message ?? ''}`,
358
+ receipt,
359
+ JSON.stringify({ ...identity, work, status, commands }, null, 2),
360
+ )
361
+ let metadataFiles: string[] = []
362
+ const complete = (
363
+ status: string,
364
+ message: string,
365
+ reusable = true,
366
+ additionalFiles: string[] = [],
367
+ ): void => {
368
+ save(status)
369
+ const files = [
370
+ ...[...sources.keys()].map(staged),
371
+ ...modulemaps.map((flag) => flag.slice('-fmodule-map-file='.length)),
372
+ receipt,
373
+ ...metadataFiles,
374
+ ...additionalFiles,
375
+ ...commands.flatMap((command) => {
376
+ const output = command.args[command.args.lastIndexOf('-o') + 1]!
377
+ return [output.replace(/\.o$/, '.log'), ...(command.object ? [output] : [])]
378
+ }),
379
+ ]
380
+ const result = {
381
+ key,
382
+ message,
383
+ reusable,
384
+ prepared: status === 'prepared',
385
+ files: Object.fromEntries(
386
+ files.map((file) => [path.relative(work, file), digest(fs.readFileSync(file))]),
387
+ ),
388
+ }
389
+ fs.writeFileSync(`${resultFile}.pending`, JSON.stringify(result, null, 2))
390
+ fs.renameSync(`${resultFile}.pending`, resultFile)
391
+ }
392
+ const finish = (
393
+ status: string,
394
+ message: string,
395
+ reusable = true,
396
+ metadataFiles: string[] = [],
397
+ ): never => {
398
+ complete(status, message, reusable, metadataFiles)
399
+ throw new SwiftObjCCompilationError(message)
400
+ }
401
+ save('compiling')
402
+ const minimumFor = (target: {
403
+ name: string
404
+ deploymentTarget: string | null
405
+ }): number => {
406
+ const version = /^(\d{1,2})(?:\.(\d{1,2}))?(?:\.(\d{1,2}))?$/.exec(
407
+ target.deploymentTarget ?? '',
333
408
  )
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) {
409
+ if (!version) {
343
410
  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}`,
411
+ 'unsupported-deployment-target',
412
+ `${target.name}: Objective-C compilation requires an explicit numeric IPHONEOS_DEPLOYMENT_TARGET; received ${JSON.stringify(target.deploymentTarget)}. Inputs: ${receipt}`,
413
+ )
414
+ }
415
+ const minimum =
416
+ Number(version[1]) * 10_000 +
417
+ Number(version[2] ?? 0) * 100 +
418
+ Number(version[3] ?? 0)
419
+ return minimum
420
+ }
421
+ if (targets.length === 0)
422
+ return finish(
423
+ 'missing-target',
424
+ 'Objective-C preparation requires an original implementation target',
425
+ )
426
+ const minimums = targets.map(minimumFor)
427
+ if (peers) {
428
+ try {
429
+ metadataFiles = compileObjcPeerMetadata({
430
+ classes: peers,
431
+ work,
432
+ headerCache: path.join(cacheDir(), 'swift-standins'),
433
+ toolchainBin: toolchain.toolchainBin,
434
+ sysroot: sdk.sysroot,
435
+ minimum: minimumFor({
436
+ name: app.moduleName,
437
+ deploymentTarget: app.iosDeploymentTarget,
438
+ }),
439
+ descriptor,
440
+ }).files
441
+ } catch (error) {
442
+ if (error instanceof ObjcPeerMetadataError)
443
+ return finish(
444
+ error.reusable ? 'metadata-failed' : 'metadata-interrupted',
445
+ error.message,
446
+ error.reusable,
447
+ error.files,
448
+ )
449
+ throw error
450
+ }
451
+ }
452
+ const failures: string[] = []
453
+ let interrupted = false
454
+ compile: for (const [index, target] of targets.entries()) {
455
+ const minimum = minimums[index]!
456
+ for (const [unit, file] of target.files.entries()) {
457
+ const output = path.join(work, `${index}-${unit}.o`)
458
+ const cxx = /\.(mm|cc|cpp|cxx)$/.test(file)
459
+ const objc = /\.mm?$/.test(file)
460
+ const flags = [
461
+ ...target.flags.filter(
462
+ (flag) =>
463
+ (!cxx || !flag.startsWith('-std=')) && (objc || flag !== '-fobjc-arc'),
464
+ ),
465
+ ...(cxx ? target.cxxFlags : []),
466
+ ...(target.fileFlags[file] ?? []),
467
+ ].map((flag, index, flags) => {
468
+ if (
469
+ ['-I', '-F', '-iquote', '-isystem', '-include'].includes(
470
+ flags[index - 1] ?? '',
471
+ )
472
+ )
473
+ return staged(flag)
474
+ const include = /^(-I|-F)(\/.*)$/.exec(flag)
475
+ return include ? `${include[1]}${staged(include[2]!)}` : flag
476
+ })
477
+ const args = [
478
+ '--target=wasm32-unknown-wasip1',
479
+ `--sysroot=${sdk.sysroot}`,
480
+ '-fwasm-exceptions',
481
+ '-fexceptions',
482
+ '-D_WASI_EMULATED_SIGNAL',
483
+ '-D_WASI_EMULATED_MMAN',
484
+ `-D__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__=${minimum}`,
485
+ `-D__IPHONE_OS_VERSION_MIN_REQUIRED=${minimum}`,
486
+ ...(objc
487
+ ? ['-fobjc-runtime=gnustep-2.2', '-fobjc-exceptions', '-fblocks']
488
+ : []),
489
+ '-F',
490
+ path.join(headers, 'System/Library/Frameworks'),
491
+ // installed libc and objc runtime headers own the target ABI.
492
+ '-idirafter',
493
+ path.join(headers, 'usr/include'),
494
+ ...target.includes.flatMap((include) => ['-I', staged(include)]),
495
+ ...target.frameworks.flatMap((framework) => ['-F', staged(framework)]),
496
+ ...modulemaps,
497
+ `-fmodule-name=${swiftTargetModuleName(target.name)}`,
498
+ `-fmodules-cache-path=${path.join(work, 'module-cache')}`,
499
+ ...flags,
500
+ ...(target.prefix ? ['-include', staged(target.prefix)] : []),
501
+ '-Os',
502
+ '-c',
503
+ staged(file),
504
+ '-o',
505
+ output,
506
+ ]
507
+ const result = spawnSync(
508
+ path.join(toolchain.toolchainBin, cxx ? 'clang++' : 'clang'),
509
+ args,
510
+ {
511
+ cwd: work,
512
+ encoding: 'utf8',
513
+ stdio: ['ignore', 'pipe', 'pipe', descriptor],
514
+ env: swiftEnvironment(),
515
+ timeout: 120_000,
516
+ maxBuffer: 16 * 1024 * 1024,
517
+ },
346
518
  )
519
+ const diagnostics = `${result.stdout ?? ''}${result.stderr ?? ''}${result.error ? `\n${result.error.message}` : ''}${result.signal ? `\nClang terminated by ${result.signal}` : ''}`
520
+ fs.writeFileSync(path.join(work, `${index}-${unit}.log`), diagnostics)
521
+ commands.push({
522
+ target: target.name,
523
+ source: file,
524
+ args,
525
+ status: result.status,
526
+ signal: result.signal,
527
+ ...(result.error ? { error: result.error.message } : {}),
528
+ ...(!result.error && !result.signal && result.status === 0
529
+ ? { object: digest(fs.readFileSync(output)) }
530
+ : {}),
531
+ })
532
+ if (result.error || result.signal || result.status !== 0) {
533
+ failures.push(
534
+ `${target.name}: ${file}\n${diagnostics.trimEnd() || `Clang exited with status ${result.status}`}`,
535
+ )
536
+ // clang's driver also returns 1 when its frontend crashes or cannot write.
537
+ if (
538
+ result.error ||
539
+ result.signal ||
540
+ result.status !== 1 ||
541
+ /^(?:PLEASE submit a bug report|LLVM ERROR:|(?:clang(?:\+\+)?(?:-\d+)?: )?(?:fatal )?error: (?:error in backend:|unable to (?:execute command|open output file|make temporary file)|.*command failed))/m.test(
542
+ result.stderr ?? '',
543
+ )
544
+ ) {
545
+ interrupted = true
546
+ break compile
547
+ }
548
+ }
549
+ save('compiling')
347
550
  }
348
- save('compiling')
349
551
  }
552
+ if (failures.length > 0) {
553
+ const selected = targets.reduce((count, target) => count + target.files.length, 0)
554
+ const successes = commands.filter((command) => command.object !== undefined).length
555
+ return finish(
556
+ interrupted ? 'compile-interrupted' : 'compile-failed',
557
+ [
558
+ interrupted
559
+ ? `Objective-C implementation compilation interrupted after ${commands.length} of ${selected} translation units; this attempt will not be reused.`
560
+ : `Objective-C implementation compilation failed in ${failures.length} of ${selected} translation units.`,
561
+ `${successes} successful object${successes === 1 ? '' : 's'} retained.`,
562
+ ...failures,
563
+ `Original inputs, compiler commands and diagnostics: ${receipt}`,
564
+ ].join('\n\n'),
565
+ !interrupted,
566
+ )
567
+ }
568
+ complete(
569
+ 'prepared',
570
+ 'Original Objective-C objects prepared; framework linkage remains unqualified.',
571
+ true,
572
+ )
573
+ return preparation()
574
+ } finally {
575
+ fs.closeSync(descriptor)
350
576
  }
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
577
  }