peachsim 0.1.1640 → 0.1.1641

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