peachsim 0.1.1619 → 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 +222 -175
- package/cli/cloud-client.ts +81 -48
- 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 +74 -17
- package/cli/commands/record.ts +20 -15
- package/cli/commands/upload.ts +5 -1
- package/cli/dev-build-freshness.ts +37 -2
- package/cli/drivers/playwright-sim-host.ts +18 -6
- package/cli/maestro-accessibility.ts +53 -0
- package/cli/maestro-compare.ts +24 -0
- package/cli/outbound-endpoints.ts +27 -8
- 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 +1528 -937
- 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 +4 -3
- package/dist-lib/cloud.mjs +4 -3
- 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 +1351 -804
- 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 +2475 -1343
- 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 +1269 -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 +806 -398
- package/dist-lib/native-app-identity-project.cjs +729 -21
- package/dist-lib/native-app-identity-project.mjs +728 -21
- package/dist-lib/native-app-identity.cjs +232 -9
- package/dist-lib/native-app-identity.mjs +230 -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 +4471 -2394
- package/dist-lib/swift-compile.cjs +336 -145
- package/dist-lib/swift-compile.mjs +350 -150
- 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 +891 -557
- 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/cloud.ts +2 -2
- 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 +118 -3
- package/src/native-app-identity.ts +103 -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 +185 -31
- 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,9 @@ 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++
|
|
725
|
+
// v47: target ownership honors exclusions before lowering manifest-named files
|
|
722
726
|
// v46: view builder literals compile as design-time lookups
|
|
723
727
|
// v45: the link runs no wasm-opt pass, whatever the compiling machine has
|
|
724
728
|
// v44: fixed-signature optional type methods retain metatype dispatch
|
|
@@ -765,7 +769,7 @@ export function swiftArtifactHash(
|
|
|
765
769
|
// v6: app and scene delegate classes compile final
|
|
766
770
|
// (v5: a target declaring resources gets SwiftPM's Bundle.module accessor;
|
|
767
771
|
// v4: sources compile through swiftSourceAsIOS)
|
|
768
|
-
hash.update(`swift-compile/
|
|
772
|
+
hash.update(`swift-compile/v49\n${release}\n`)
|
|
769
773
|
if (input.oneModule) hash.update('one-module\n')
|
|
770
774
|
hash.update(swiftModuleDigest(moduleRoot))
|
|
771
775
|
for (const file of Object.keys(input.files).sort()) {
|
|
@@ -1565,9 +1569,14 @@ function compiledSwiftSources(
|
|
|
1565
1569
|
const orderedTargets = [...targets].sort((a, b) => b.path.length - a.path.length)
|
|
1566
1570
|
for (const [file, contents] of Object.entries(files)) {
|
|
1567
1571
|
if (!file.endsWith('.swift')) continue
|
|
1568
|
-
const owner = orderedTargets.find(
|
|
1569
|
-
|
|
1570
|
-
|
|
1572
|
+
const owner = orderedTargets.find((candidate) => {
|
|
1573
|
+
const prefix = candidate.path === '.' ? '' : `${candidate.path}/`
|
|
1574
|
+
if (!file.startsWith(prefix)) return false
|
|
1575
|
+
const relative = file.slice(prefix.length)
|
|
1576
|
+
return !candidate.exclude?.some(
|
|
1577
|
+
(excluded) => relative === excluded || relative.startsWith(`${excluded}/`),
|
|
1578
|
+
)
|
|
1579
|
+
})
|
|
1571
1580
|
if (
|
|
1572
1581
|
/(?:^|\/)Package(?:@swift-[\d.]+)?\.swift$/.test(file) &&
|
|
1573
1582
|
(!owner || /^Package(?:@swift-[\d.]+)?\.swift$/.test(file))
|
|
@@ -1600,12 +1609,73 @@ function compiledSwiftSources(
|
|
|
1600
1609
|
const sources = lifted.map(([, contents]) => contents)
|
|
1601
1610
|
const extensions = objcExtensionMemberIndex(sources)
|
|
1602
1611
|
const objc = objcIndex(sources)
|
|
1612
|
+
const rooted = objcRootedClasses(sources)
|
|
1603
1613
|
for (const [file, contents] of lifted)
|
|
1604
|
-
compiled.set(
|
|
1614
|
+
compiled.set(
|
|
1615
|
+
file,
|
|
1616
|
+
hideObjCClassesFromCxx(swiftSourceAsIOS(contents, extensions, objc), rooted),
|
|
1617
|
+
)
|
|
1605
1618
|
}
|
|
1606
1619
|
return compiled
|
|
1607
1620
|
}
|
|
1608
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
|
+
|
|
1609
1679
|
// wasm Swift has no ObjC runtime, so a member read on AnyObject never
|
|
1610
1680
|
// resolves: `value of type 'AnyObject' has no member 'doubleValue'`. native
|
|
1611
1681
|
// code that bridges dictionary values through AnyObject to reach NSNumber's
|
|
@@ -3596,6 +3666,8 @@ export type ObjCIndex = {
|
|
|
3596
3666
|
// each @objc type method's selector to its result type, null when two
|
|
3597
3667
|
// declarations disagree
|
|
3598
3668
|
typeMethods: Map<string, string | null>
|
|
3669
|
+
// NSObject and protocols constrained to it without a Swift init requirement
|
|
3670
|
+
nsObjectTypes: Set<string>
|
|
3599
3671
|
}
|
|
3600
3672
|
|
|
3601
3673
|
// the @objc type methods `source` declares, by selector, into `found`
|
|
@@ -3733,15 +3805,43 @@ export function objcIndex(sources: string[]): ObjCIndex {
|
|
|
3733
3805
|
/\b(?:var|let)\s+([A-Za-z_]\w*)/g,
|
|
3734
3806
|
))
|
|
3735
3807
|
variables.add(declared[1]!)
|
|
3736
|
-
const parents =
|
|
3737
|
-
? inheritedTypes(sources)
|
|
3738
|
-
: new Map<string, Set<string>>()
|
|
3808
|
+
const parents = inheritedTypes(sources)
|
|
3739
3809
|
for (const [protocol, inherited] of sdkParents)
|
|
3740
3810
|
parents.set(protocol, new Set([...(parents.get(protocol) ?? []), ...inherited]))
|
|
3741
3811
|
const conformers = requirements.size
|
|
3742
3812
|
? conformingTypes(parents, new Set(requirements.keys()))
|
|
3743
3813
|
: new Map<string, Set<string>>()
|
|
3744
|
-
|
|
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
|
+
}
|
|
3745
3845
|
}
|
|
3746
3846
|
|
|
3747
3847
|
// the non-generic file-scope typealiases `sources` declare, each resolved
|
|
@@ -4111,7 +4211,7 @@ const IMP_CONVENTION = /@convention\(c\)(?=\s*\(\s*AnyObject\s*,\s*Selector\b)/g
|
|
|
4111
4211
|
|
|
4112
4212
|
function lowerObjCRuntimeCalls(source: string, index: ObjCIndex): string {
|
|
4113
4213
|
const casts = source.includes('@convention(c)')
|
|
4114
|
-
const classes = /\b(?:AnyClass|AnyObject)\b/.test(source)
|
|
4214
|
+
const classes = /\b(?:AnyClass|AnyObject)\b|\.\s*init\s*\(/.test(source)
|
|
4115
4215
|
if (!casts && !classes) return source
|
|
4116
4216
|
const code = swiftCodeMask(source)
|
|
4117
4217
|
const edits: [number, number, string][] = []
|
|
@@ -4142,15 +4242,33 @@ function lowerObjCRuntimeCalls(source: string, index: ObjCIndex): string {
|
|
|
4142
4242
|
}
|
|
4143
4243
|
}
|
|
4144
4244
|
if (classes) {
|
|
4145
|
-
// every declaration of a name, typed or not; a receiver
|
|
4146
|
-
//
|
|
4245
|
+
// every declaration of a name, typed or not; a receiver's innermost
|
|
4246
|
+
// declaration in scope determines its runtime operation
|
|
4147
4247
|
const declarations = new Map<string, { at: number; type: string }[]>()
|
|
4148
4248
|
for (const declared of code.matchAll(
|
|
4149
|
-
/(?:\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,
|
|
4150
4250
|
)) {
|
|
4151
4251
|
const name = declared[1] ?? declared[4] ?? declared[5]!
|
|
4152
4252
|
const list = declarations.get(name) ?? []
|
|
4153
|
-
|
|
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 })
|
|
4154
4272
|
declarations.set(name, list)
|
|
4155
4273
|
}
|
|
4156
4274
|
// a tuple or enum pattern (`let (a, b)`, `case let .some(a)`) binds
|
|
@@ -4168,7 +4286,7 @@ function lowerObjCRuntimeCalls(source: string, index: ObjCIndex): string {
|
|
|
4168
4286
|
}
|
|
4169
4287
|
for (const list of declarations.values()) list.sort((a, b) => a.at - b.at)
|
|
4170
4288
|
const braces = [...code.matchAll(/[{}]/g)].map((match) => match.index)
|
|
4171
|
-
const
|
|
4289
|
+
const receiverType = (name: string, at: number): string | undefined => {
|
|
4172
4290
|
let innermost: { from: number; type: string } | undefined
|
|
4173
4291
|
for (const declared of declarations.get(name) ?? []) {
|
|
4174
4292
|
if (declared.at > at) break
|
|
@@ -4176,22 +4294,40 @@ function lowerObjCRuntimeCalls(source: string, index: ObjCIndex): string {
|
|
|
4176
4294
|
if (at > from && at < to && (!innermost || from >= innermost.from))
|
|
4177
4295
|
innermost = { from, type: declared.type }
|
|
4178
4296
|
}
|
|
4179
|
-
return innermost?.type
|
|
4297
|
+
return innermost?.type
|
|
4180
4298
|
}
|
|
4181
4299
|
let reached = -1
|
|
4182
4300
|
for (const call of code.matchAll(
|
|
4183
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,
|
|
4184
4302
|
)) {
|
|
4185
|
-
if (
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
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
|
|
4192
4312
|
const open = call.index + call[0].length - 1
|
|
4193
4313
|
const shut = closingParen(code, open)
|
|
4194
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
|
|
4195
4331
|
const selector = objcSelectorName(
|
|
4196
4332
|
call[3]!,
|
|
4197
4333
|
passed.map(({ label }) => label),
|
|
@@ -5213,18 +5349,29 @@ export function compileSwiftProjectUnit(
|
|
|
5213
5349
|
multi: compileTargets(input).length > 0,
|
|
5214
5350
|
}
|
|
5215
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')
|
|
5216
5356
|
return serialize(
|
|
5217
5357
|
root,
|
|
5218
5358
|
async () => {
|
|
5219
5359
|
// reuse one verified result across server restarts before admission or
|
|
5220
5360
|
// swiftpm. the address covers sources, framework inputs and toolchain.
|
|
5221
5361
|
const retained = retainedArtifacts.get(root)?.find((old) => old.hash === hash)
|
|
5222
|
-
if (retained)
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
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.
|
|
5228
5375
|
if (
|
|
5229
5376
|
process.platform === 'darwin' &&
|
|
5230
5377
|
process.env.TM_SESSION &&
|
|
@@ -5248,6 +5395,7 @@ export function compileSwiftProjectUnit(
|
|
|
5248
5395
|
)
|
|
5249
5396
|
try {
|
|
5250
5397
|
const repo = path.resolve(moduleRoot, '../..')
|
|
5398
|
+
report('requesting builder admission or cached result')
|
|
5251
5399
|
await runCompiler(
|
|
5252
5400
|
'bun',
|
|
5253
5401
|
[
|
|
@@ -5276,6 +5424,8 @@ export function compileSwiftProjectUnit(
|
|
|
5276
5424
|
)
|
|
5277
5425
|
}
|
|
5278
5426
|
saveCompiledArtifact(root, artifact)
|
|
5427
|
+
saveCompiledArtifact(sharedRoot, artifact)
|
|
5428
|
+
report('builder artifact verified')
|
|
5279
5429
|
return retain(root, artifact)
|
|
5280
5430
|
} finally {
|
|
5281
5431
|
rmSync(requestRoot, { recursive: true, force: true })
|
|
@@ -5288,6 +5438,7 @@ export function compileSwiftProjectUnit(
|
|
|
5288
5438
|
`swift-compile: no swift.org toolchain at ${options.toolchainBin}; install ${options.release} and its matching wasm sdk`,
|
|
5289
5439
|
)
|
|
5290
5440
|
}
|
|
5441
|
+
report('preparing compiler plugins')
|
|
5291
5442
|
const macroPlugins = await ensureMacroPlugins(
|
|
5292
5443
|
moduleRoot,
|
|
5293
5444
|
options.toolchainBin,
|
|
@@ -5343,8 +5494,9 @@ export function compileSwiftProjectUnit(
|
|
|
5343
5494
|
timeout: options.timeoutMs,
|
|
5344
5495
|
signal: options.signal,
|
|
5345
5496
|
}
|
|
5497
|
+
report('compiling and linking Swift modules')
|
|
5346
5498
|
try {
|
|
5347
|
-
await runCompiler(swift, buildArgs, buildOptions)
|
|
5499
|
+
await runCompiler(swift, buildArgs, buildOptions, true)
|
|
5348
5500
|
} catch (error) {
|
|
5349
5501
|
const output = outputOf(error)
|
|
5350
5502
|
const moduleCache = path.join(
|
|
@@ -5360,7 +5512,7 @@ export function compileSwiftProjectUnit(
|
|
|
5360
5512
|
) {
|
|
5361
5513
|
rmSync(moduleCache, { recursive: true, force: true })
|
|
5362
5514
|
try {
|
|
5363
|
-
await runCompiler(swift, buildArgs, buildOptions)
|
|
5515
|
+
await runCompiler(swift, buildArgs, buildOptions, true)
|
|
5364
5516
|
} catch (retryError) {
|
|
5365
5517
|
throw new Error(
|
|
5366
5518
|
projectDiagnostic(outputOf(retryError), layout, input.files),
|
|
@@ -5381,6 +5533,8 @@ export function compileSwiftProjectUnit(
|
|
|
5381
5533
|
icu: await icu,
|
|
5382
5534
|
}
|
|
5383
5535
|
saveCompiledArtifact(root, artifact)
|
|
5536
|
+
saveCompiledArtifact(sharedRoot, artifact)
|
|
5537
|
+
report('compiled artifact verified')
|
|
5384
5538
|
return retain(root, artifact)
|
|
5385
5539
|
},
|
|
5386
5540
|
options.signal,
|