peachsim 0.1.1620 → 0.1.1625
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.
- package/README.md +35 -5
- package/cli/app-fonts.ts +2 -1
- package/cli/bridge-flow-runner.ts +184 -113
- package/cli/cloud-client.ts +68 -57
- package/cli/commands/box/search-command.ts +3 -2
- package/cli/commands/box.ts +10 -29
- package/cli/commands/control.ts +108 -43
- package/cli/commands/inspect/settling.ts +57 -20
- package/cli/commands/maestro-compare.ts +11 -32
- package/cli/commands/platform.ts +71 -36
- package/cli/commands/upload.ts +5 -1
- package/cli/dev-build-freshness.ts +37 -2
- package/cli/drivers/playwright-sim-host.ts +6 -5
- package/cli/maestro-accessibility.ts +53 -0
- package/cli/maestro-compare.ts +24 -0
- package/cli/outbound-endpoints.ts +22 -5
- package/cli/parent-pid.ts +0 -9
- package/cli/remote-simulators.ts +112 -0
- package/cli/remoteSimulatorContract.ts +21 -0
- package/detox/expectations.ts +5 -1
- package/detox/index.ts +133 -98
- package/detox/motion-change.cjs +37 -0
- package/detox/motion-change.d.cts +16 -0
- package/detox/navigation-lifecycle.ts +22 -4
- package/dist-lib/agent-daemon-client.cjs +3 -2
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +3 -2
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/bundle-channel.cjs +1520 -934
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +3 -2
- package/dist-lib/cloud.mjs +3 -2
- package/dist-lib/config.cjs +5 -4
- package/dist-lib/config.mjs +5 -4
- package/dist-lib/detox/index.cjs +1021 -364
- package/dist-lib/dev-bundle-resolution.cjs +51 -23
- package/dist-lib/expo-swift-bundle.cjs +1343 -801
- package/dist-lib/expo-swift-metadata.cjs +5 -4
- package/dist-lib/expo-swift-metadata.mjs +5 -4
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +2468 -1341
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1262 -300
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +1 -1
- package/dist-lib/menu.cjs +3 -2
- package/dist-lib/menu.mjs +3 -2
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +5 -4
- package/dist-lib/metro-production-bundle.mjs +5 -4
- package/dist-lib/metro.cjs +799 -398
- package/dist-lib/native-app-identity-project.cjs +728 -22
- package/dist-lib/native-app-identity-project.mjs +727 -22
- package/dist-lib/native-app-identity.cjs +224 -9
- package/dist-lib/native-app-identity.mjs +222 -8
- package/dist-lib/native-firebase-metadata.cjs +5 -4
- package/dist-lib/native-firebase-metadata.mjs +5 -4
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +3 -2
- package/dist-lib/react-native-host-modules.cjs +4 -1
- package/dist-lib/react-native-host-modules.mjs +4 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +94 -28
- package/dist-lib/sdk.cjs +89 -24
- package/dist-lib/sdk.mjs +89 -24
- package/dist-lib/skills.cjs +4391 -2350
- package/dist-lib/swift-compile.cjs +328 -142
- package/dist-lib/swift-compile.mjs +342 -147
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +888 -559
- package/dist-lib/vite.cjs +68 -14
- package/dist-lib/worklet-executable.cjs +25 -2
- package/dist-lib/worklet-executable.mjs +25 -2
- package/package.json +2 -2
- package/scripts/dev-server-scanner.ts +33 -13
- package/src/admitted-mac-build.ts +34 -8
- package/src/android-app-identity.ts +146 -0
- package/src/android-autolinking.ts +75 -0
- package/src/android-string-value.ts +36 -0
- package/src/android-strings.ts +5 -36
- package/src/androidAutolinkingContract.ts +40 -0
- package/src/bundleGlobalFooter.ts +26 -7
- package/src/codex-client.ts +3 -2
- package/src/dev-bundle-resolution.ts +58 -25
- package/src/expo-kotlin-host.ts +6 -36
- package/src/expo-swift-bundle.ts +17 -12
- package/src/expo-swift-host.ts +477 -173
- package/src/expoSwiftContract.ts +6 -0
- package/src/host/bridge-host.ts +7 -55
- package/src/host/staged-host-files.ts +29 -2
- package/src/installed-package-versions.ts +54 -14
- package/src/launchMetadataContract.ts +24 -0
- package/src/metro-plugin.ts +37 -42
- package/src/native-app-identity-project.ts +116 -3
- package/src/native-app-identity.ts +86 -11
- package/src/nitro-compile.ts +2 -5
- package/src/nitro-source.ts +147 -63
- package/src/process-group.ts +90 -60
- package/src/process-output.ts +0 -9
- package/src/public-brand.ts +1 -0
- package/src/react-native-host-modules.ts +3 -0
- package/src/sim-client.ts +6 -4
- package/src/swift-compile.ts +176 -28
- package/src/swift-distribution.lock.json +37 -11
- package/src/swift-distribution.ts +61 -9
- package/src/swift-host.ts +58 -27
- package/src/swift-objc-standin.ts +13 -0
- package/src/swift-standin-emit.ts +1 -1
- package/src/swift-standins.ts +17 -5
- package/src/vite-plugin.ts +8 -19
- package/src/worklet-executable.ts +30 -3
- package/src/worklets-babel.ts +35 -14
- package/src/xcode-project.ts +10 -0
package/src/process-group.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { spawn, spawnSync, type ChildProcess } from 'node:child_process'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
appendFileSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs'
|
|
3
10
|
import { tmpdir } from 'node:os'
|
|
4
11
|
import { join } from 'node:path'
|
|
5
12
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
@@ -7,6 +14,57 @@ import { setTimeout as sleep } from 'node:timers/promises'
|
|
|
7
14
|
const GRACEFUL_TIMEOUT_MS = 3_000
|
|
8
15
|
const FORCED_EXIT_TIMEOUT_MS = 1_000
|
|
9
16
|
|
|
17
|
+
// keep this self-contained: the detached browser host embeds its source.
|
|
18
|
+
export function listProcessChildren(parents: number[], timeoutMs = 3_000): number[] {
|
|
19
|
+
if (parents.some((pid) => !Number.isInteger(pid) || pid <= 1))
|
|
20
|
+
throw new Error('process inspection requires valid parent pids')
|
|
21
|
+
if (parents.length === 0) return []
|
|
22
|
+
const deadline = Date.now() + timeoutMs
|
|
23
|
+
let children: string
|
|
24
|
+
if (process.platform === 'linux') {
|
|
25
|
+
// pgrep scans unrelated host processes. every thread can own children.
|
|
26
|
+
const lists: string[] = []
|
|
27
|
+
for (const parent of parents) {
|
|
28
|
+
try {
|
|
29
|
+
const tasks = `/proc/${parent}/task`
|
|
30
|
+
for (const thread of readdirSync(tasks)) {
|
|
31
|
+
if (Date.now() >= deadline)
|
|
32
|
+
throw new Error('process descendant inspection exceeded its deadline')
|
|
33
|
+
try {
|
|
34
|
+
lists.push(readFileSync(`${tasks}/${thread}/children`, 'utf8'))
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
37
|
+
throw error
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// an exiting parent or thread has no remaining children to inspect.
|
|
42
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
43
|
+
throw error
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
children = lists.join(' ')
|
|
47
|
+
} else {
|
|
48
|
+
const result = spawnSync('pgrep', ['-P', parents.join(',')], {
|
|
49
|
+
encoding: 'utf8',
|
|
50
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
51
|
+
timeout: Math.max(1, timeoutMs),
|
|
52
|
+
killSignal: 'SIGKILL',
|
|
53
|
+
})
|
|
54
|
+
if (result.error || (result.status !== 0 && result.status !== 1)) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`could not inspect descendants of processes ${parents.join(',')}: ${
|
|
57
|
+
result.error?.message || result.stderr.trim() || `pgrep exited ${result.status}`
|
|
58
|
+
}`,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
children = result.stdout
|
|
62
|
+
}
|
|
63
|
+
return [...new Set(children.trim().split(/\s+/).map(Number))].filter(
|
|
64
|
+
(pid) => Number.isInteger(pid) && pid > 1 && !parents.includes(pid),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
10
68
|
// synchronous group-or-pid signal for `process.on('exit')` sweeps, where no
|
|
11
69
|
// async wait is possible. never throws: a dead group reads as ESRCH.
|
|
12
70
|
export function signalProcessGroup(child: ChildProcess, signal: NodeJS.Signals) {
|
|
@@ -93,63 +151,49 @@ async function protectProcessFreeze(rootPid: number) {
|
|
|
93
151
|
}
|
|
94
152
|
}
|
|
95
153
|
|
|
96
|
-
|
|
97
|
-
|
|
154
|
+
// reparented members remain in the group; detached descendants need their
|
|
155
|
+
// recorded pids. both can leave zombies, which signal zero cannot distinguish
|
|
156
|
+
// from running processes. inspect their states together before confirming exit.
|
|
157
|
+
function processesHaveExited(pids: number[], pgid: number): boolean {
|
|
158
|
+
const mayExist = [-pgid, ...pids].some((target) => {
|
|
98
159
|
try {
|
|
99
|
-
process.kill(
|
|
160
|
+
process.kill(target, 0)
|
|
100
161
|
return true
|
|
101
162
|
} catch (error) {
|
|
102
|
-
return error instanceof Error && 'code' in error && error.code === '
|
|
163
|
+
return !(error instanceof Error && 'code' in error && error.code === 'ESRCH')
|
|
103
164
|
}
|
|
104
165
|
})
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// a descendant whose parent exited first is reparented to init, so `pgrep -P`
|
|
108
|
-
// no longer finds it and it never enters the pid list. it stays in the leader's
|
|
109
|
-
// process group though, and the group outlives its leader while any member is
|
|
110
|
-
// left, so the group is the survivor check that still sees it.
|
|
111
|
-
//
|
|
112
|
-
// a signal cannot finish the answer. a zombie is an exit status its parent has
|
|
113
|
-
// not collected: it runs nothing and holds nothing, and it is what a finished
|
|
114
|
-
// tree leaves behind. macos reports one with EPERM, which otherwise means a
|
|
115
|
-
// live member we may not signal, and linux reports one as signalable, so on
|
|
116
|
-
// either platform a kill that does not fail reads a finished group as a live
|
|
117
|
-
// one, which holds shutdown open for the whole graceful timeout and then
|
|
118
|
-
// SIGKILLs something already dead. ps reports the state, so it settles which
|
|
119
|
-
// it is, and it costs a process only when a survivor we never enumerated is
|
|
120
|
-
// really there: this runs after the known pids are gone, and an empty group
|
|
121
|
-
// fails the kill with ESRCH before reaching it.
|
|
122
|
-
//
|
|
123
|
-
// exported for its test: node reaps its own children, so a group left holding
|
|
124
|
-
// only a zombie cannot be built through terminateProcessGroup from inside a
|
|
125
|
-
// test, and this is the function that has to read one correctly.
|
|
126
|
-
export function groupIsEmpty(pgid: number): boolean {
|
|
127
|
-
try {
|
|
128
|
-
process.kill(-pgid, 0)
|
|
129
|
-
} catch (error) {
|
|
130
|
-
if (!(error instanceof Error && 'code' in error && error.code === 'EPERM'))
|
|
131
|
-
return true
|
|
132
|
-
}
|
|
133
|
-
const result = spawnSync('ps', ['-A', '-o', 'pgid=,stat='], {
|
|
166
|
+
if (!mayExist) return true
|
|
167
|
+
const result = spawnSync('ps', ['-A', '-o', 'pid=,pgid=,stat='], {
|
|
134
168
|
encoding: 'utf8',
|
|
135
169
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
170
|
+
timeout: FORCED_EXIT_TIMEOUT_MS,
|
|
171
|
+
killSignal: 'SIGKILL',
|
|
136
172
|
})
|
|
137
|
-
// an unreadable process table
|
|
138
|
-
// and let the caller escalate to SIGKILL the way it does for any live tree.
|
|
173
|
+
// an unreadable process table cannot confirm exit.
|
|
139
174
|
if (result.error || result.status !== 0) return false
|
|
175
|
+
const knownPids = new Set(pids)
|
|
140
176
|
return !result.stdout.split('\n').some((line) => {
|
|
141
|
-
const [group, state] = line.trim().split(/\s+/)
|
|
142
|
-
return
|
|
177
|
+
const [pid, group, state] = line.trim().split(/\s+/)
|
|
178
|
+
return (
|
|
179
|
+
(Number(group) === pgid || knownPids.has(Number(pid))) &&
|
|
180
|
+
state !== undefined &&
|
|
181
|
+
!state.startsWith('Z')
|
|
182
|
+
)
|
|
143
183
|
})
|
|
144
184
|
}
|
|
145
185
|
|
|
186
|
+
export function groupIsEmpty(pgid: number): boolean {
|
|
187
|
+
return processesHaveExited([], pgid)
|
|
188
|
+
}
|
|
189
|
+
|
|
146
190
|
async function waitForExit(
|
|
147
191
|
pids: number[],
|
|
148
192
|
pgid: number,
|
|
149
193
|
timeoutMs: number,
|
|
150
194
|
): Promise<boolean> {
|
|
151
195
|
const deadline = Date.now() + timeoutMs
|
|
152
|
-
const gone = () =>
|
|
196
|
+
const gone = () => processesHaveExited(pids, pgid)
|
|
153
197
|
while (Date.now() < deadline) {
|
|
154
198
|
if (gone()) return true
|
|
155
199
|
await sleep(50)
|
|
@@ -197,30 +241,16 @@ export async function terminateProcessGroup(
|
|
|
197
241
|
inspectionError = new Error('process descendant inspection exceeded its deadline')
|
|
198
242
|
break
|
|
199
243
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
})
|
|
206
|
-
if (result.error || (result.status !== 0 && result.status !== 1)) {
|
|
207
|
-
inspectionError = new Error(
|
|
208
|
-
`could not inspect descendants of processes ${parents.join(',')}: ${
|
|
209
|
-
result.error?.message ||
|
|
210
|
-
result.stderr.trim() ||
|
|
211
|
-
`pgrep exited ${result.status}`
|
|
212
|
-
}`,
|
|
213
|
-
)
|
|
244
|
+
let children: number[]
|
|
245
|
+
try {
|
|
246
|
+
children = listProcessChildren(parents, remaining)
|
|
247
|
+
} catch (error) {
|
|
248
|
+
inspectionError = new Error(`could not inspect descendants: ${String(error)}`)
|
|
214
249
|
break
|
|
215
250
|
}
|
|
216
251
|
const next: number[] = []
|
|
217
|
-
for (const
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
Number.isInteger(descendantPid) &&
|
|
221
|
-
descendantPid > 1 &&
|
|
222
|
-
!descendants.has(descendantPid)
|
|
223
|
-
) {
|
|
252
|
+
for (const descendantPid of children) {
|
|
253
|
+
if (!descendants.has(descendantPid)) {
|
|
224
254
|
freeze.record(descendantPid)
|
|
225
255
|
descendants.add(descendantPid)
|
|
226
256
|
next.push(descendantPid)
|
package/src/process-output.ts
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
// a consumer such as head may close its pipe before the command finishes.
|
|
2
|
-
// exit directly: reporting this through stderr can fail on the same pipe.
|
|
3
|
-
for (const stream of [process.stdout, process.stderr]) {
|
|
4
|
-
stream.on('error', (error: NodeJS.ErrnoException) => {
|
|
5
|
-
if (error.code === 'EPIPE') process.exit(1)
|
|
6
|
-
throw error
|
|
7
|
-
})
|
|
8
|
-
}
|
|
9
|
-
|
|
10
1
|
// wait for both pipes before forcing an exit with live handles.
|
|
11
2
|
// console writers must use these streams rather than a separate runtime sink.
|
|
12
3
|
export async function exitWithFlushedOutput(code: number): Promise<never> {
|
package/src/public-brand.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* preserves upstream platform, style, event-emitter, and TurboModule behavior.
|
|
7
7
|
*/
|
|
8
8
|
export const REACT_NATIVE_PASSTHROUGH_SPECIFIERS: readonly string[] = [
|
|
9
|
+
// one's native bootstrap runs the upstream environment entry.
|
|
10
|
+
'react-native/setup-env',
|
|
11
|
+
'react-native/src/setup-env',
|
|
9
12
|
// sdk 58 reads these upstream getters and wraps the original metro logger.
|
|
10
13
|
'react-native/unstable-internals-do-not-use',
|
|
11
14
|
'react-native/src/unstable-internals-do-not-use',
|
package/src/sim-client.ts
CHANGED
|
@@ -48,6 +48,10 @@ import {
|
|
|
48
48
|
type WaitForSootsimIdleResult,
|
|
49
49
|
} from '../cli/commands/inspect/settling'
|
|
50
50
|
import { openAppUrl } from './app-url'
|
|
51
|
+
import {
|
|
52
|
+
LAUNCH_METADATA_KEYS,
|
|
53
|
+
LAUNCH_METADATA_QUERY_PARAM,
|
|
54
|
+
} from './launchMetadataContract'
|
|
51
55
|
import { sleep } from './sleep'
|
|
52
56
|
import type { BridgeClaimResult, BridgeSimInfo, WsBridge } from '../cli/ws-bridge'
|
|
53
57
|
import type {
|
|
@@ -187,10 +191,8 @@ const TARGET_SEARCH_PARAMS = [
|
|
|
187
191
|
'open',
|
|
188
192
|
'port',
|
|
189
193
|
'inspectOpen',
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
'appSplash',
|
|
193
|
-
'appCatalog',
|
|
194
|
+
...LAUNCH_METADATA_KEYS,
|
|
195
|
+
LAUNCH_METADATA_QUERY_PARAM,
|
|
194
196
|
]
|
|
195
197
|
|
|
196
198
|
export function buildSimOpenUrl(currentUrl: string, target: number | string): string {
|
package/src/swift-compile.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import os from 'node:os'
|
|
17
17
|
import path from 'node:path'
|
|
18
18
|
import { admittedMacBuilderSlot } from './admitted-mac-build'
|
|
19
|
+
import { rnxHomeDir } from './home-paths'
|
|
19
20
|
import { terminateProcessGroup } from './process-group'
|
|
20
21
|
import { designTimeDiagnostic, designTimeSource } from './swift-design-time'
|
|
21
22
|
import {
|
|
@@ -719,6 +720,8 @@ export function swiftArtifactHash(
|
|
|
719
720
|
moduleRoot: string,
|
|
720
721
|
): string {
|
|
721
722
|
const hash = createHash('sha256')
|
|
723
|
+
// v49: NSObject metatype initialization reaches its explicit runtime boundary
|
|
724
|
+
// v48: top-level classes rooted in an Apple framework class stay out of C++
|
|
722
725
|
// v47: target ownership honors exclusions before lowering manifest-named files
|
|
723
726
|
// v46: view builder literals compile as design-time lookups
|
|
724
727
|
// v45: the link runs no wasm-opt pass, whatever the compiling machine has
|
|
@@ -766,7 +769,7 @@ export function swiftArtifactHash(
|
|
|
766
769
|
// v6: app and scene delegate classes compile final
|
|
767
770
|
// (v5: a target declaring resources gets SwiftPM's Bundle.module accessor;
|
|
768
771
|
// v4: sources compile through swiftSourceAsIOS)
|
|
769
|
-
hash.update(`swift-compile/
|
|
772
|
+
hash.update(`swift-compile/v49\n${release}\n`)
|
|
770
773
|
if (input.oneModule) hash.update('one-module\n')
|
|
771
774
|
hash.update(swiftModuleDigest(moduleRoot))
|
|
772
775
|
for (const file of Object.keys(input.files).sort()) {
|
|
@@ -1606,12 +1609,73 @@ function compiledSwiftSources(
|
|
|
1606
1609
|
const sources = lifted.map(([, contents]) => contents)
|
|
1607
1610
|
const extensions = objcExtensionMemberIndex(sources)
|
|
1608
1611
|
const objc = objcIndex(sources)
|
|
1612
|
+
const rooted = objcRootedClasses(sources)
|
|
1609
1613
|
for (const [file, contents] of lifted)
|
|
1610
|
-
compiled.set(
|
|
1614
|
+
compiled.set(
|
|
1615
|
+
file,
|
|
1616
|
+
hideObjCClassesFromCxx(swiftSourceAsIOS(contents, extensions, objc), rooted),
|
|
1617
|
+
)
|
|
1611
1618
|
}
|
|
1612
1619
|
return compiled
|
|
1613
1620
|
}
|
|
1614
1621
|
|
|
1622
|
+
// an Apple framework protocol a class may list first, which is no superclass
|
|
1623
|
+
const APPLE_PROTOCOL =
|
|
1624
|
+
/^NS(?:Mutable)?Copying$|^NS(?:Secure)?Coding$|^NSLocking$|^NSFastEnumeration$|^NSObjectProtocol$|(?:Delegate|DataSource)$/
|
|
1625
|
+
|
|
1626
|
+
// the module's classes whose superclass chain, through the classes `sources`
|
|
1627
|
+
// declare, reaches an Apple framework class: Objective-C classes on iOS
|
|
1628
|
+
function objcRootedClasses(sources: string[]): Set<string> {
|
|
1629
|
+
const bases = new Map<string, string>()
|
|
1630
|
+
for (const source of sources)
|
|
1631
|
+
for (const match of swiftCodeMask(source).matchAll(
|
|
1632
|
+
/\bclass\s+(?!(?:func|var|let|subscript)\b)([A-Za-z_]\w*)\s*(?:<[^>{]*>)?\s*:\s*(?:[A-Za-z_]\w*\.)*([A-Za-z_]\w*)/g,
|
|
1633
|
+
))
|
|
1634
|
+
bases.set(match[1]!, match[2]!)
|
|
1635
|
+
const rooted = new Set<string>()
|
|
1636
|
+
for (const name of bases.keys()) {
|
|
1637
|
+
const seen = new Set([name])
|
|
1638
|
+
let base = bases.get(name)!
|
|
1639
|
+
while (bases.has(base) && !seen.has(base)) {
|
|
1640
|
+
seen.add(base)
|
|
1641
|
+
base = bases.get(base)!
|
|
1642
|
+
}
|
|
1643
|
+
if (
|
|
1644
|
+
!bases.has(base) &&
|
|
1645
|
+
APPLE_CLASS.test(base) &&
|
|
1646
|
+
!APPLE_PROTOCOL.test(base) &&
|
|
1647
|
+
!(base in SDK_OBJC_OPTIONAL)
|
|
1648
|
+
)
|
|
1649
|
+
rooted.add(name)
|
|
1650
|
+
}
|
|
1651
|
+
return rooted
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// the header swift emits on iOS keeps an Objective-C class out of its C++
|
|
1655
|
+
// section. here its root is a swift class from a module with no C++ header,
|
|
1656
|
+
// so C++ that includes the header fails on the undeclared base. a top-level
|
|
1657
|
+
// class in `rooted` gains @_expose(!Cxx) in front of its declaration,
|
|
1658
|
+
// shifting that one line's columns. swift rejects the attribute in a local
|
|
1659
|
+
// scope, so a nested class keeps its exposure.
|
|
1660
|
+
function hideObjCClassesFromCxx(source: string, rooted: Set<string>): string {
|
|
1661
|
+
if (rooted.size === 0) return source
|
|
1662
|
+
const code = swiftCodeMask(source)
|
|
1663
|
+
const starts: number[] = []
|
|
1664
|
+
let depth = 0
|
|
1665
|
+
for (const match of code.matchAll(
|
|
1666
|
+
/[{}]|\bclass\s+(?!(?:func|var|let|subscript)\b)([A-Za-z_]\w*)/g,
|
|
1667
|
+
)) {
|
|
1668
|
+
if (match[0] === '{') depth++
|
|
1669
|
+
else if (match[0] === '}') depth--
|
|
1670
|
+
else if (depth === 0 && rooted.has(match[1]!))
|
|
1671
|
+
starts.push(declarationStart(code, match.index, 0))
|
|
1672
|
+
}
|
|
1673
|
+
let out = source
|
|
1674
|
+
for (const at of starts.reverse())
|
|
1675
|
+
out = `${out.slice(0, at)}@_expose(!Cxx) ${out.slice(at)}`
|
|
1676
|
+
return out
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1615
1679
|
// wasm Swift has no ObjC runtime, so a member read on AnyObject never
|
|
1616
1680
|
// resolves: `value of type 'AnyObject' has no member 'doubleValue'`. native
|
|
1617
1681
|
// code that bridges dictionary values through AnyObject to reach NSNumber's
|
|
@@ -3602,6 +3666,8 @@ export type ObjCIndex = {
|
|
|
3602
3666
|
// each @objc type method's selector to its result type, null when two
|
|
3603
3667
|
// declarations disagree
|
|
3604
3668
|
typeMethods: Map<string, string | null>
|
|
3669
|
+
// NSObject and protocols constrained to it without a Swift init requirement
|
|
3670
|
+
nsObjectTypes: Set<string>
|
|
3605
3671
|
}
|
|
3606
3672
|
|
|
3607
3673
|
// the @objc type methods `source` declares, by selector, into `found`
|
|
@@ -3739,15 +3805,43 @@ export function objcIndex(sources: string[]): ObjCIndex {
|
|
|
3739
3805
|
/\b(?:var|let)\s+([A-Za-z_]\w*)/g,
|
|
3740
3806
|
))
|
|
3741
3807
|
variables.add(declared[1]!)
|
|
3742
|
-
const parents =
|
|
3743
|
-
? inheritedTypes(sources)
|
|
3744
|
-
: new Map<string, Set<string>>()
|
|
3808
|
+
const parents = inheritedTypes(sources)
|
|
3745
3809
|
for (const [protocol, inherited] of sdkParents)
|
|
3746
3810
|
parents.set(protocol, new Set([...(parents.get(protocol) ?? []), ...inherited]))
|
|
3747
3811
|
const conformers = requirements.size
|
|
3748
3812
|
? conformingTypes(parents, new Set(requirements.keys()))
|
|
3749
3813
|
: new Map<string, Set<string>>()
|
|
3750
|
-
|
|
3814
|
+
const protocols = new Set<string>()
|
|
3815
|
+
const initializers = new Set<string>()
|
|
3816
|
+
for (const source of sources) {
|
|
3817
|
+
if (!/\bprotocol\b/.test(source)) continue
|
|
3818
|
+
const code = swiftCodeMask(source)
|
|
3819
|
+
const braces = [...code.matchAll(/[{}]/g)].map((match) => match.index)
|
|
3820
|
+
for (const protocol of code.matchAll(/\bprotocol\s+([A-Za-z_]\w*)[^{}]*\{/g)) {
|
|
3821
|
+
const name = protocol[1]!
|
|
3822
|
+
protocols.add(name)
|
|
3823
|
+
const open = protocol.index + protocol[0].length - 1
|
|
3824
|
+
if (
|
|
3825
|
+
/\binit\s*[?!]?\s*\(/.test(code.slice(open + 1, closingBrace(code, braces, open)))
|
|
3826
|
+
)
|
|
3827
|
+
initializers.add(name)
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
const rooted = conformingTypes(parents, new Set(['NSObject']))
|
|
3831
|
+
const initialized = conformingTypes(parents, initializers)
|
|
3832
|
+
const nsObjectTypes = new Set([
|
|
3833
|
+
'NSObject',
|
|
3834
|
+
...[...protocols].filter((name) => rooted.has(name) && !initialized.has(name)),
|
|
3835
|
+
])
|
|
3836
|
+
return {
|
|
3837
|
+
requirements,
|
|
3838
|
+
conformers,
|
|
3839
|
+
callable,
|
|
3840
|
+
variables,
|
|
3841
|
+
aliases,
|
|
3842
|
+
typeMethods,
|
|
3843
|
+
nsObjectTypes,
|
|
3844
|
+
}
|
|
3751
3845
|
}
|
|
3752
3846
|
|
|
3753
3847
|
// the non-generic file-scope typealiases `sources` declare, each resolved
|
|
@@ -4117,7 +4211,7 @@ const IMP_CONVENTION = /@convention\(c\)(?=\s*\(\s*AnyObject\s*,\s*Selector\b)/g
|
|
|
4117
4211
|
|
|
4118
4212
|
function lowerObjCRuntimeCalls(source: string, index: ObjCIndex): string {
|
|
4119
4213
|
const casts = source.includes('@convention(c)')
|
|
4120
|
-
const classes = /\b(?:AnyClass|AnyObject)\b/.test(source)
|
|
4214
|
+
const classes = /\b(?:AnyClass|AnyObject)\b|\.\s*init\s*\(/.test(source)
|
|
4121
4215
|
if (!casts && !classes) return source
|
|
4122
4216
|
const code = swiftCodeMask(source)
|
|
4123
4217
|
const edits: [number, number, string][] = []
|
|
@@ -4148,15 +4242,33 @@ function lowerObjCRuntimeCalls(source: string, index: ObjCIndex): string {
|
|
|
4148
4242
|
}
|
|
4149
4243
|
}
|
|
4150
4244
|
if (classes) {
|
|
4151
|
-
// every declaration of a name, typed or not; a receiver
|
|
4152
|
-
//
|
|
4245
|
+
// every declaration of a name, typed or not; a receiver's innermost
|
|
4246
|
+
// declaration in scope determines its runtime operation
|
|
4153
4247
|
const declarations = new Map<string, { at: number; type: string }[]>()
|
|
4154
4248
|
for (const declared of code.matchAll(
|
|
4155
|
-
/(?:\b(?:let|var)\s+|[(,]\s*(?:[A-Za-z_]\w*\s+)?)([A-Za-z_]\w*)\s*:\s*(?:([A-Z]\w*(?:\.\w+)*)\s*([?!])?|[[(])|\b(?:let|var|for)\s+([A-Za-z_]\w*)\b(?!\s*:)|[{,]\s*([A-Za-z_]\w*)\s+in\b/g,
|
|
4249
|
+
/(?:\b(?:let|var)\s+|[(,]\s*(?:[A-Za-z_]\w*\s+)?)([A-Za-z_]\w*)\s*:\s*(?:(?:any\s+)?([A-Z]\w*(?:\.\w+)*)\s*([?!])?|[[(])|\b(?:let|var|for)\s+([A-Za-z_]\w*)\b(?!\s*:)|[{,]\s*([A-Za-z_]\w*)\s+in\b/g,
|
|
4156
4250
|
)) {
|
|
4157
4251
|
const name = declared[1] ?? declared[4] ?? declared[5]!
|
|
4158
4252
|
const list = declarations.get(name) ?? []
|
|
4159
|
-
|
|
4253
|
+
let type = declared[3] ? '' : (declared[2] ?? '')
|
|
4254
|
+
if (!type && declared[4]) {
|
|
4255
|
+
// NSClassFromString's checked cast establishes the metatype of a
|
|
4256
|
+
// successful conditional binding, including a guard's remaining body.
|
|
4257
|
+
const lookup = /^\s*=\s*NSClassFromString\s*\(/.exec(
|
|
4258
|
+
code.slice(declared.index + declared[0].length),
|
|
4259
|
+
)
|
|
4260
|
+
if (lookup) {
|
|
4261
|
+
const open = declared.index + declared[0].length + lookup[0].length - 1
|
|
4262
|
+
type =
|
|
4263
|
+
/^\s*as\?\s*([A-Za-z_]\w*(?:\.\w+)*\.Type)\b/.exec(
|
|
4264
|
+
code.slice(closingParen(code, open) + 1),
|
|
4265
|
+
)?.[1] ?? ''
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
const resolved = type.endsWith('.Type')
|
|
4269
|
+
? `${resolveAliases(type.slice(0, -5), index.aliases)}.Type`
|
|
4270
|
+
: resolveAliases(type, index.aliases)
|
|
4271
|
+
list.push({ at: declared.index, type: resolved })
|
|
4160
4272
|
declarations.set(name, list)
|
|
4161
4273
|
}
|
|
4162
4274
|
// a tuple or enum pattern (`let (a, b)`, `case let .some(a)`) binds
|
|
@@ -4174,7 +4286,7 @@ function lowerObjCRuntimeCalls(source: string, index: ObjCIndex): string {
|
|
|
4174
4286
|
}
|
|
4175
4287
|
for (const list of declarations.values()) list.sort((a, b) => a.at - b.at)
|
|
4176
4288
|
const braces = [...code.matchAll(/[{}]/g)].map((match) => match.index)
|
|
4177
|
-
const
|
|
4289
|
+
const receiverType = (name: string, at: number): string | undefined => {
|
|
4178
4290
|
let innermost: { from: number; type: string } | undefined
|
|
4179
4291
|
for (const declared of declarations.get(name) ?? []) {
|
|
4180
4292
|
if (declared.at > at) break
|
|
@@ -4182,22 +4294,40 @@ function lowerObjCRuntimeCalls(source: string, index: ObjCIndex): string {
|
|
|
4182
4294
|
if (at > from && at < to && (!innermost || from >= innermost.from))
|
|
4183
4295
|
innermost = { from, type: declared.type }
|
|
4184
4296
|
}
|
|
4185
|
-
return innermost?.type
|
|
4297
|
+
return innermost?.type
|
|
4186
4298
|
}
|
|
4187
4299
|
let reached = -1
|
|
4188
4300
|
for (const call of code.matchAll(
|
|
4189
4301
|
/(?<![\w.])((?:type\s*\(\s*of\s*:\s*([A-Za-z_]\w*)\s*\))|[A-Za-z_]\w*)\s*\.\s*([A-Za-z_]\w*)\s*\(/g,
|
|
4190
4302
|
)) {
|
|
4191
|
-
if (
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4303
|
+
if (call.index < reached) continue
|
|
4304
|
+
const type = receiverType(call[2] ?? call[1]!, call.index)
|
|
4305
|
+
const constructed = call[2] ? type : type?.replace(/\.Type$/, '')
|
|
4306
|
+
const constructs =
|
|
4307
|
+
call[3] === 'init' &&
|
|
4308
|
+
constructed &&
|
|
4309
|
+
(call[2] || type?.endsWith('.Type')) &&
|
|
4310
|
+
index.nsObjectTypes.has(constructed.replace(/^Foundation\./, ''))
|
|
4311
|
+
if (!constructs && type !== (call[2] ? 'AnyObject' : 'AnyClass')) continue
|
|
4198
4312
|
const open = call.index + call[0].length - 1
|
|
4199
4313
|
const shut = closingParen(code, open)
|
|
4200
4314
|
const passed = callArguments(source, code, open, shut)
|
|
4315
|
+
if (constructs && passed.length === 0) {
|
|
4316
|
+
// a class-constrained protocol metatype otherwise erases a generic
|
|
4317
|
+
// initializer result to NSObject, losing its protocol requirements.
|
|
4318
|
+
const result =
|
|
4319
|
+
constructed.replace(/^Foundation\./, '') === 'NSObject'
|
|
4320
|
+
? constructed
|
|
4321
|
+
: `any ${constructed}`
|
|
4322
|
+
edits.push([
|
|
4323
|
+
call.index,
|
|
4324
|
+
shut + 1 - call.index,
|
|
4325
|
+
`_objcNSObjectDefaultInit(${call[1]}, returning: (${result}).self)`,
|
|
4326
|
+
])
|
|
4327
|
+
reached = shut + 1
|
|
4328
|
+
continue
|
|
4329
|
+
}
|
|
4330
|
+
if (type !== (call[2] ? 'AnyObject' : 'AnyClass')) continue
|
|
4201
4331
|
const selector = objcSelectorName(
|
|
4202
4332
|
call[3]!,
|
|
4203
4333
|
passed.map(({ label }) => label),
|
|
@@ -5219,18 +5349,29 @@ export function compileSwiftProjectUnit(
|
|
|
5219
5349
|
multi: compileTargets(input).length > 0,
|
|
5220
5350
|
}
|
|
5221
5351
|
const hash = swiftArtifactHash(input, options.release, moduleRoot)
|
|
5352
|
+
const sharedRoot = path.join(rnxHomeDir(), 'cache', 'swift-projects', hash)
|
|
5353
|
+
const report = (message: string) =>
|
|
5354
|
+
console.error(`[swift-project ${hash.slice(0, 16)}] ${message}`)
|
|
5355
|
+
report('checking verified artifacts')
|
|
5222
5356
|
return serialize(
|
|
5223
5357
|
root,
|
|
5224
5358
|
async () => {
|
|
5225
5359
|
// reuse one verified result across server restarts before admission or
|
|
5226
5360
|
// swiftpm. the address covers sources, framework inputs and toolchain.
|
|
5227
5361
|
const retained = retainedArtifacts.get(root)?.find((old) => old.hash === hash)
|
|
5228
|
-
if (retained)
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5362
|
+
if (retained) {
|
|
5363
|
+
report('reusing memory artifact')
|
|
5364
|
+
return retain(root, retained)
|
|
5365
|
+
}
|
|
5366
|
+
const cached = readCompiledArtifact(path.join(sharedRoot, 'artifact'), hash)
|
|
5367
|
+
const previous = cached ?? readCompiledArtifact(path.join(root, 'artifact'), hash)
|
|
5368
|
+
if (previous) {
|
|
5369
|
+
if (!cached) saveCompiledArtifact(sharedRoot, previous)
|
|
5370
|
+
report('reusing verified disk artifact')
|
|
5371
|
+
return retain(root, previous)
|
|
5372
|
+
}
|
|
5373
|
+
// a mac builder with a live destination grant compiles here. inherited
|
|
5374
|
+
// transport markers alone still dispatch through admission.
|
|
5234
5375
|
if (
|
|
5235
5376
|
process.platform === 'darwin' &&
|
|
5236
5377
|
process.env.TM_SESSION &&
|
|
@@ -5254,6 +5395,7 @@ export function compileSwiftProjectUnit(
|
|
|
5254
5395
|
)
|
|
5255
5396
|
try {
|
|
5256
5397
|
const repo = path.resolve(moduleRoot, '../..')
|
|
5398
|
+
report('requesting builder admission or cached result')
|
|
5257
5399
|
await runCompiler(
|
|
5258
5400
|
'bun',
|
|
5259
5401
|
[
|
|
@@ -5282,6 +5424,8 @@ export function compileSwiftProjectUnit(
|
|
|
5282
5424
|
)
|
|
5283
5425
|
}
|
|
5284
5426
|
saveCompiledArtifact(root, artifact)
|
|
5427
|
+
saveCompiledArtifact(sharedRoot, artifact)
|
|
5428
|
+
report('builder artifact verified')
|
|
5285
5429
|
return retain(root, artifact)
|
|
5286
5430
|
} finally {
|
|
5287
5431
|
rmSync(requestRoot, { recursive: true, force: true })
|
|
@@ -5294,6 +5438,7 @@ export function compileSwiftProjectUnit(
|
|
|
5294
5438
|
`swift-compile: no swift.org toolchain at ${options.toolchainBin}; install ${options.release} and its matching wasm sdk`,
|
|
5295
5439
|
)
|
|
5296
5440
|
}
|
|
5441
|
+
report('preparing compiler plugins')
|
|
5297
5442
|
const macroPlugins = await ensureMacroPlugins(
|
|
5298
5443
|
moduleRoot,
|
|
5299
5444
|
options.toolchainBin,
|
|
@@ -5349,8 +5494,9 @@ export function compileSwiftProjectUnit(
|
|
|
5349
5494
|
timeout: options.timeoutMs,
|
|
5350
5495
|
signal: options.signal,
|
|
5351
5496
|
}
|
|
5497
|
+
report('compiling and linking Swift modules')
|
|
5352
5498
|
try {
|
|
5353
|
-
await runCompiler(swift, buildArgs, buildOptions)
|
|
5499
|
+
await runCompiler(swift, buildArgs, buildOptions, true)
|
|
5354
5500
|
} catch (error) {
|
|
5355
5501
|
const output = outputOf(error)
|
|
5356
5502
|
const moduleCache = path.join(
|
|
@@ -5366,7 +5512,7 @@ export function compileSwiftProjectUnit(
|
|
|
5366
5512
|
) {
|
|
5367
5513
|
rmSync(moduleCache, { recursive: true, force: true })
|
|
5368
5514
|
try {
|
|
5369
|
-
await runCompiler(swift, buildArgs, buildOptions)
|
|
5515
|
+
await runCompiler(swift, buildArgs, buildOptions, true)
|
|
5370
5516
|
} catch (retryError) {
|
|
5371
5517
|
throw new Error(
|
|
5372
5518
|
projectDiagnostic(outputOf(retryError), layout, input.files),
|
|
@@ -5387,6 +5533,8 @@ export function compileSwiftProjectUnit(
|
|
|
5387
5533
|
icu: await icu,
|
|
5388
5534
|
}
|
|
5389
5535
|
saveCompiledArtifact(root, artifact)
|
|
5536
|
+
saveCompiledArtifact(sharedRoot, artifact)
|
|
5537
|
+
report('compiled artifact verified')
|
|
5390
5538
|
return retain(root, artifact)
|
|
5391
5539
|
},
|
|
5392
5540
|
options.signal,
|