peachsim 0.1.1640 → 0.1.1644
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/cli/app-bundle-resources.ts +57 -0
- package/cli/app-stubs.ts +38 -13
- package/cli/cloud-session.ts +16 -0
- package/cli/commands/assert.ts +17 -4
- package/cli/commands/box.ts +33 -19
- package/cli/commands/control.ts +12 -0
- package/cli/commands/doctor.ts +24 -15
- package/cli/commands/inspect/actions.ts +3 -2
- package/cli/commands/inspect/core.ts +0 -8
- package/cli/commands/inspect.ts +0 -14
- package/cli/commands/platform.ts +2 -1
- package/cli/commands/record.ts +64 -28
- package/cli/commands/swift.ts +10 -2
- package/cli/commands/three-mode.ts +4 -38
- package/cli/commands/upload.ts +8 -8
- package/cli/drivers/playwright.ts +39 -0
- package/cli/flow-live-status.ts +0 -2
- package/cli/main.ts +1 -1
- package/cli/outbound-endpoints.ts +4 -2
- package/cli/ws-bridge.ts +0 -6
- package/detox/attempt-events.cjs +41 -5
- package/detox/index.ts +155 -109
- package/detox/jest-environment.cjs +5 -1
- package/detox/motion-change.cjs +7 -5
- package/detox/motion-change.d.cts +1 -0
- package/detox/motion-leg-starts.cjs +20 -0
- package/detox/motion-leg-starts.d.cts +8 -0
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +12 -3
- 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 +2671 -1833
- 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 +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/config.mjs +1 -1
- package/dist-lib/detox/index.cjs +578 -130
- package/dist-lib/detox/jest-environment.cjs +5 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +2374 -1554
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +2968 -2141
- 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 +132 -57
- 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 +14 -6
- package/dist-lib/menu.mjs +14 -6
- 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 +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +120 -53
- package/dist-lib/native-app-identity-project.cjs +96 -44
- package/dist-lib/native-app-identity-project.mjs +95 -44
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- 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 +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +5 -3
- package/dist-lib/sdk.mjs +5 -3
- package/dist-lib/skills.cjs +3932 -2868
- package/dist-lib/swift-compile.cjs +587 -374
- package/dist-lib/swift-compile.mjs +595 -369
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +8 -3
- package/dist-lib/swift-submit.mjs +8 -3
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +2263 -1467
- package/dist-lib/vite.cjs +1 -1
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/package.json +1 -1
- package/src/appBundleResourcesContract.ts +51 -0
- package/src/backend-origin.ts +11 -1
- package/src/host/bridge-host.ts +17 -11
- package/src/host/staged-host-files.ts +28 -4
- package/src/launchMetadataContract.ts +1 -0
- package/src/menu.ts +12 -4
- package/src/native-app-identity-project.ts +47 -38
- package/src/react-native-bridge-modules.ts +89 -76
- package/src/runtime-delivery.ts +6 -7
- package/src/swift-compile.ts +150 -112
- package/src/swift-compiler-digest.ts +5 -3
- package/src/swift-distribution.lock.json +9 -8
- package/src/swift-host.ts +6 -4
- package/src/swift-objc-compile.ts +416 -194
- package/src/swift-objc-metadata.ts +204 -0
- package/src/swift-objc-sdk.ts +195 -2
- package/src/swift-package-manifest.ts +6 -3
- package/src/swift-root-lock.ts +302 -0
- package/src/swift-submit.ts +15 -2
- package/src/swiftContract.ts +12 -0
- package/src/xcode-project.ts +77 -7
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import {
|
|
2
|
+
spawn,
|
|
3
|
+
spawnSync,
|
|
4
|
+
type ChildProcess,
|
|
5
|
+
type ChildProcessByStdio,
|
|
6
|
+
type SpawnOptions,
|
|
7
|
+
} from 'node:child_process'
|
|
8
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
9
|
+
import {
|
|
10
|
+
linkSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from 'node:fs'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import { rnxHomeDir } from './home-paths'
|
|
19
|
+
import { terminateProcessGroup } from './process-group'
|
|
20
|
+
import type { Readable, Writable } from 'node:stream'
|
|
21
|
+
|
|
22
|
+
const POLL_MS = 250
|
|
23
|
+
// ps derives a start time from the boot time, which drifts by a second or two
|
|
24
|
+
// on linux; a reused pid starts later than this.
|
|
25
|
+
const START_TOLERANCE_MS = 2_000
|
|
26
|
+
|
|
27
|
+
interface ProcessRow {
|
|
28
|
+
pid: number
|
|
29
|
+
pgid: number
|
|
30
|
+
zombie: boolean
|
|
31
|
+
start: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Identity {
|
|
35
|
+
pid: number
|
|
36
|
+
start: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Generation {
|
|
40
|
+
number: number
|
|
41
|
+
holder: Identity
|
|
42
|
+
released: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function errorCode(error: unknown): string | undefined {
|
|
46
|
+
return error instanceof Error && 'code' in error && typeof error.code === 'string'
|
|
47
|
+
? error.code
|
|
48
|
+
: undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ps prints start times without a zone, so every reader asks for utc: a
|
|
52
|
+
// holder and a waiter then agree whatever their own time zones are.
|
|
53
|
+
function parseStart(text: string): number {
|
|
54
|
+
const start = Date.parse(`${text} UTC`)
|
|
55
|
+
if (Number.isNaN(start)) throw new Error(`unreadable process start time "${text}"`)
|
|
56
|
+
return start
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// the whole table in one read. an unreadable table throws: a lock that cannot
|
|
60
|
+
// tell whether its holder is gone must not guess.
|
|
61
|
+
function processTable(): ProcessRow[] {
|
|
62
|
+
const result = spawnSync('ps', ['-A', '-o', 'pid=,pgid=,stat=,lstart='], {
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
env: { ...process.env, LC_ALL: 'C', TZ: 'UTC' },
|
|
65
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
66
|
+
timeout: 10_000,
|
|
67
|
+
killSignal: 'SIGKILL',
|
|
68
|
+
})
|
|
69
|
+
if (result.error || result.status !== 0) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`swift root lock cannot read the process table: ${
|
|
72
|
+
result.error?.message || result.stderr.trim() || `ps exited ${result.status}`
|
|
73
|
+
}`,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
return result.stdout
|
|
77
|
+
.split('\n')
|
|
78
|
+
.filter((line) => line.trim())
|
|
79
|
+
.map((line) => {
|
|
80
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/.exec(line)
|
|
81
|
+
if (!match)
|
|
82
|
+
throw new Error(`swift root lock cannot parse a process table row: ${line}`)
|
|
83
|
+
return {
|
|
84
|
+
pid: Number(match[1]),
|
|
85
|
+
pgid: Number(match[2]),
|
|
86
|
+
zombie: match[3]!.startsWith('Z'),
|
|
87
|
+
start: parseStart(match[4]!),
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sameProcess(row: ProcessRow, identity: Identity): boolean {
|
|
93
|
+
return (
|
|
94
|
+
row.pid === identity.pid && Math.abs(row.start - identity.start) <= START_TOLERANCE_MS
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ownIdentity(pid: number): Identity {
|
|
99
|
+
const row = processTable().find(
|
|
100
|
+
(candidate) => candidate.pid === pid && !candidate.zombie,
|
|
101
|
+
)
|
|
102
|
+
if (!row) throw new Error(`swift root lock cannot find process ${pid}`)
|
|
103
|
+
return { pid, start: row.start }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseIdentity(text: string): Identity {
|
|
107
|
+
const value: unknown = JSON.parse(text)
|
|
108
|
+
if (
|
|
109
|
+
value &&
|
|
110
|
+
typeof value === 'object' &&
|
|
111
|
+
'pid' in value &&
|
|
112
|
+
typeof value.pid === 'number' &&
|
|
113
|
+
'start' in value &&
|
|
114
|
+
typeof value.start === 'number'
|
|
115
|
+
)
|
|
116
|
+
return { pid: value.pid, start: value.start }
|
|
117
|
+
throw new Error(`swift root lock record is not a process identity: ${text}`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function pause(ms: number, signal?: AbortSignal): Promise<void> {
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
const done = () => {
|
|
123
|
+
clearTimeout(timer)
|
|
124
|
+
signal?.removeEventListener('abort', done)
|
|
125
|
+
resolve()
|
|
126
|
+
}
|
|
127
|
+
const timer = setTimeout(done, ms)
|
|
128
|
+
signal?.addEventListener('abort', done, { once: true })
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const name = (number: number, suffix: string) =>
|
|
133
|
+
`${String(number).padStart(12, '0')}.${suffix}`
|
|
134
|
+
|
|
135
|
+
function generationNumbers(directory: string): number[] {
|
|
136
|
+
const numbers = new Set<number>()
|
|
137
|
+
for (const entry of readdirSync(directory)) {
|
|
138
|
+
const match = /^(\d{12})\./.exec(entry)
|
|
139
|
+
if (match) numbers.add(Number(match[1]))
|
|
140
|
+
}
|
|
141
|
+
return [...numbers].sort((a, b) => a - b)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readGeneration(directory: string, number: number): Generation | null {
|
|
145
|
+
let owner: string
|
|
146
|
+
try {
|
|
147
|
+
owner = readFileSync(path.join(directory, name(number, 'owner')), 'utf8')
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// a newer holder cleaned this generation after it was listed.
|
|
150
|
+
if (errorCode(error) === 'ENOENT') return null
|
|
151
|
+
throw error
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
number,
|
|
155
|
+
holder: parseIdentity(owner),
|
|
156
|
+
released: readdirSync(directory).includes(name(number, 'released')),
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// children are registered before they run and only while the holder lives, so
|
|
161
|
+
// reading them after the holder is seen dead lists every one it started.
|
|
162
|
+
function readChildren(directory: string, number: number): Identity[] {
|
|
163
|
+
const prefix = name(number, 'child.')
|
|
164
|
+
return readdirSync(directory)
|
|
165
|
+
.filter((entry) => entry.startsWith(prefix))
|
|
166
|
+
.map((entry) => parseIdentity(readFileSync(path.join(directory, entry), 'utf8')))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// a child leads its own session, so its group is its pid; a group id stays
|
|
170
|
+
// taken while any member lives, so a leader started later is another process.
|
|
171
|
+
function groupAlive(table: ProcessRow[], leader: Identity): boolean {
|
|
172
|
+
const row = table.find((candidate) => candidate.pid === leader.pid)
|
|
173
|
+
if (row && !sameProcess(row, leader)) return false
|
|
174
|
+
return table.some((candidate) => candidate.pgid === leader.pid && !candidate.zombie)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** compiler processes whose lifetime the root lock covers. */
|
|
178
|
+
export interface SwiftRootLease {
|
|
179
|
+
/** starts `command` in its own process group, recorded under the lock before
|
|
180
|
+
* it runs, so the root stays held while that group lives even if this
|
|
181
|
+
* process dies. */
|
|
182
|
+
spawn(
|
|
183
|
+
command: string,
|
|
184
|
+
args: string[],
|
|
185
|
+
options: SpawnOptions,
|
|
186
|
+
): ChildProcessByStdio<Writable, Readable, Readable>
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** runs `work` as the only process using a swift build root. the in-process
|
|
190
|
+
* queue orders one process's requests; two processes sharing a root each
|
|
191
|
+
* rewrite its sources and manifest, and swiftpm's own build lock only orders
|
|
192
|
+
* the builds, so a build can compile the other process's sources and then
|
|
193
|
+
* read back that process's executable.
|
|
194
|
+
*
|
|
195
|
+
* holders are numbered. the newest generation holds the root; claiming the
|
|
196
|
+
* next number is one exclusive link, so exactly one claimant wins and no
|
|
197
|
+
* record is ever moved or replaced. a generation is free once its holder
|
|
198
|
+
* released it, or once its holder process and every compiler group it
|
|
199
|
+
* recorded are gone. an unreadable process table or an unkillable compiler
|
|
200
|
+
* keeps the root held. the lock lives beside the root's tree, never inside
|
|
201
|
+
* it: a write sweeps every file of the tree it does not name. */
|
|
202
|
+
export async function withSwiftRootLock<T>(
|
|
203
|
+
root: string,
|
|
204
|
+
work: (lease: SwiftRootLease) => Promise<T>,
|
|
205
|
+
signal?: AbortSignal,
|
|
206
|
+
): Promise<T> {
|
|
207
|
+
const directory = path.join(
|
|
208
|
+
rnxHomeDir(),
|
|
209
|
+
'cache',
|
|
210
|
+
'swift-root-locks',
|
|
211
|
+
createHash('sha256').update(root).digest('hex').slice(0, 32),
|
|
212
|
+
)
|
|
213
|
+
mkdirSync(directory, { recursive: true })
|
|
214
|
+
const token = randomUUID()
|
|
215
|
+
const owner = JSON.stringify(ownIdentity(process.pid))
|
|
216
|
+
const pending = path.join(directory, `pending.${token}`)
|
|
217
|
+
let held = 0
|
|
218
|
+
for (;;) {
|
|
219
|
+
signal?.throwIfAborted()
|
|
220
|
+
const newest = generationNumbers(directory).at(-1) ?? 0
|
|
221
|
+
const current = newest > 0 ? readGeneration(directory, newest) : null
|
|
222
|
+
if (newest > 0 && !current) continue
|
|
223
|
+
if (current && !current.released) {
|
|
224
|
+
const table = processTable()
|
|
225
|
+
if (table.some((row) => !row.zombie && sameProcess(row, current.holder))) {
|
|
226
|
+
await pause(POLL_MS, signal)
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
const children = readChildren(directory, newest)
|
|
230
|
+
if (children.some((child) => groupAlive(table, child))) {
|
|
231
|
+
await pause(POLL_MS, signal)
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// the record is complete before it takes the number, so a waiter never
|
|
236
|
+
// reads half a record.
|
|
237
|
+
writeFileSync(pending, owner)
|
|
238
|
+
try {
|
|
239
|
+
linkSync(pending, path.join(directory, name(newest + 1, 'owner')))
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (errorCode(error) === 'EEXIST') continue
|
|
242
|
+
throw error
|
|
243
|
+
} finally {
|
|
244
|
+
rmSync(pending, { force: true })
|
|
245
|
+
}
|
|
246
|
+
// a claimant that listed the directory before older generations were
|
|
247
|
+
// cleaned can take a number below the newest; it never held the root.
|
|
248
|
+
const numbers = generationNumbers(directory)
|
|
249
|
+
if (numbers.at(-1) !== newest + 1) {
|
|
250
|
+
rmSync(path.join(directory, name(newest + 1, 'owner')), { force: true })
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
held = newest + 1
|
|
254
|
+
for (const number of numbers)
|
|
255
|
+
if (number < held)
|
|
256
|
+
for (const entry of readdirSync(directory))
|
|
257
|
+
if (entry.startsWith(name(number, '')))
|
|
258
|
+
rmSync(path.join(directory, entry), { force: true })
|
|
259
|
+
break
|
|
260
|
+
}
|
|
261
|
+
const children: ChildProcess[] = []
|
|
262
|
+
const lease: SwiftRootLease = {
|
|
263
|
+
spawn(command, args, options) {
|
|
264
|
+
// the child waits for its registration before it runs, and exits without
|
|
265
|
+
// running if this process dies first.
|
|
266
|
+
const child = spawn(
|
|
267
|
+
'/bin/sh',
|
|
268
|
+
['-c', 'IFS= read -r go || exit 1; exec "$@" </dev/null', 'sh', command, ...args],
|
|
269
|
+
{ ...options, detached: true, stdio: ['pipe', 'pipe', 'pipe'] },
|
|
270
|
+
)
|
|
271
|
+
child.stdin!.on('error', () => {})
|
|
272
|
+
children.push(child)
|
|
273
|
+
try {
|
|
274
|
+
if (child.pid === undefined) {
|
|
275
|
+
child.once('error', () => {})
|
|
276
|
+
throw new Error(`could not start ${command}`)
|
|
277
|
+
}
|
|
278
|
+
const childPending = `${pending}.child.${child.pid}`
|
|
279
|
+
try {
|
|
280
|
+
// publish a complete identity before opening the compiler gate. a
|
|
281
|
+
// crash or failed write leaves only an ignored pending file.
|
|
282
|
+
writeFileSync(childPending, JSON.stringify(ownIdentity(child.pid)))
|
|
283
|
+
linkSync(childPending, path.join(directory, name(held, `child.${child.pid}`)))
|
|
284
|
+
} finally {
|
|
285
|
+
rmSync(childPending, { force: true })
|
|
286
|
+
}
|
|
287
|
+
} catch (error) {
|
|
288
|
+
child.stdin!.destroy()
|
|
289
|
+
throw error
|
|
290
|
+
}
|
|
291
|
+
child.stdin!.end('go\n')
|
|
292
|
+
return child
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
return await work(lease)
|
|
297
|
+
} finally {
|
|
298
|
+
// a compiler that outlives its caller would still read the root.
|
|
299
|
+
await Promise.all(children.map((child) => terminateProcessGroup(child, 'SIGKILL', 0)))
|
|
300
|
+
writeFileSync(path.join(directory, name(held, 'released')), '')
|
|
301
|
+
}
|
|
302
|
+
}
|
package/src/swift-submit.ts
CHANGED
|
@@ -154,8 +154,10 @@ export type SwiftCompileOutcome =
|
|
|
154
154
|
* the build is running; poll the same source set again. One build is tens of
|
|
155
155
|
* seconds and a service-binding request is cancelled at about four, so the
|
|
156
156
|
* container answers this before the connection dies and the caller polls.
|
|
157
|
+
* `stage` is the latest stage the compiler reported for this build, absent
|
|
158
|
+
* before it reports one.
|
|
157
159
|
*/
|
|
158
|
-
| { status: 'compiling'; hash: string }
|
|
160
|
+
| { status: 'compiling'; hash: string; stage?: string }
|
|
159
161
|
/** refused before any work: no entry, too many files, over a cap, over a rate limit. */
|
|
160
162
|
| { status: 'refused'; detail: string }
|
|
161
163
|
/** the compiler ran and failed; detail carries its diagnostic verbatim. */
|
|
@@ -333,7 +335,11 @@ function isSwiftCompileOutcome(value: unknown): value is SwiftCompileOutcome {
|
|
|
333
335
|
(value.native === undefined || isSwiftNativeModules(value.native))
|
|
334
336
|
)
|
|
335
337
|
case 'compiling':
|
|
336
|
-
return
|
|
338
|
+
return (
|
|
339
|
+
typeof value.hash === 'string' &&
|
|
340
|
+
/^[a-f0-9]{64}$/.test(value.hash) &&
|
|
341
|
+
(value.stage === undefined || typeof value.stage === 'string')
|
|
342
|
+
)
|
|
337
343
|
case 'refused':
|
|
338
344
|
case 'failed':
|
|
339
345
|
return typeof value.detail === 'string'
|
|
@@ -405,7 +411,10 @@ export async function compileSwiftProject(
|
|
|
405
411
|
},
|
|
406
412
|
origin: string,
|
|
407
413
|
signal?: AbortSignal,
|
|
414
|
+
/** called when a poll reports a compiler stage the previous poll did not. */
|
|
415
|
+
onStage?: (stage: string) => void,
|
|
408
416
|
): Promise<SwiftModuleAddress> {
|
|
417
|
+
let lastStage: string | undefined
|
|
409
418
|
while (true) {
|
|
410
419
|
signal?.throwIfAborted()
|
|
411
420
|
const outcome = await submit(work)
|
|
@@ -431,6 +440,10 @@ export async function compileSwiftProject(
|
|
|
431
440
|
...(outcome.native === undefined ? {} : { native: outcome.native }),
|
|
432
441
|
}
|
|
433
442
|
case 'compiling':
|
|
443
|
+
if (outcome.stage !== undefined && outcome.stage !== lastStage) {
|
|
444
|
+
lastStage = outcome.stage
|
|
445
|
+
onStage?.(outcome.stage)
|
|
446
|
+
}
|
|
434
447
|
await new Promise<void>((resolve, reject) => {
|
|
435
448
|
const finish = () => {
|
|
436
449
|
signal?.removeEventListener('abort', abort)
|
package/src/swiftContract.ts
CHANGED
|
@@ -17,6 +17,18 @@ export interface SwiftResourceBundle {
|
|
|
17
17
|
path: string
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface SwiftObjCPeerClass {
|
|
21
|
+
name: string
|
|
22
|
+
header: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SwiftObjCImplementationPreparation {
|
|
26
|
+
receipt: string
|
|
27
|
+
objects: { target: string; source: string; file: string }[]
|
|
28
|
+
metadata?: string
|
|
29
|
+
registration?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
export interface SwiftMetalLibrarySource extends SwiftMetalSource {
|
|
21
33
|
bundlePath: string
|
|
22
34
|
}
|
package/src/xcode-project.ts
CHANGED
|
@@ -399,15 +399,22 @@ function targetSettings(
|
|
|
399
399
|
if (!config) return []
|
|
400
400
|
const name = str(config.name) ?? configuration
|
|
401
401
|
const base = str(config.baseConfigurationReference)
|
|
402
|
+
const anchor = str(config.baseConfigurationReferenceAnchor)
|
|
403
|
+
const relative = str(config.baseConfigurationReferenceRelativePath)
|
|
402
404
|
const own = isDict(config.buildSettings) ? config.buildSettings : {}
|
|
403
|
-
const
|
|
405
|
+
const directory = anchor ? project.resolvePath(anchor) : null
|
|
406
|
+
const baseFile = base
|
|
407
|
+
? project.resolvePath(base)
|
|
408
|
+
: directory && relative
|
|
409
|
+
? path.resolve(directory, relative)
|
|
410
|
+
: null
|
|
404
411
|
if (
|
|
405
412
|
options.requireSources !== false &&
|
|
406
|
-
base &&
|
|
413
|
+
(base || anchor || relative) &&
|
|
407
414
|
(!baseFile || !fs.existsSync(baseFile))
|
|
408
415
|
)
|
|
409
416
|
throw new Error(
|
|
410
|
-
`Unable to open base configuration reference file '${baseFile ?? base}'`,
|
|
417
|
+
`Unable to open base configuration reference file '${baseFile ?? base ?? relative ?? anchor}'`,
|
|
411
418
|
)
|
|
412
419
|
const read: string[] = []
|
|
413
420
|
const fromFile = baseFile ? readXcconfig(baseFile, read) : {}
|
|
@@ -458,6 +465,9 @@ function targetSettings(
|
|
|
458
465
|
return applyOperator(get('PRODUCT_NAME'), 'c99extidentifier')
|
|
459
466
|
case 'EXECUTABLE_NAME':
|
|
460
467
|
return get('PRODUCT_NAME')
|
|
468
|
+
// the native default of CoreBuildSystem.xcspec
|
|
469
|
+
case 'EXCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES':
|
|
470
|
+
return '*.nib *.lproj *.framework *.gch *.xcode* *.xcassets *.icon (*) .DS_Store CVS .svn .git .hg *.pbproj *.pbxproj'
|
|
461
471
|
case 'AppIdentifierPrefix':
|
|
462
472
|
case 'TeamIdentifierPrefix': {
|
|
463
473
|
// only a signed build has a team prefix
|
|
@@ -485,6 +495,65 @@ function settingList(value: string): string[] {
|
|
|
485
495
|
return items
|
|
486
496
|
}
|
|
487
497
|
|
|
498
|
+
// xcode expands a trailing `/**` into the directory and every directory below
|
|
499
|
+
// it, skipping subdirectories whose name matches EXCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES
|
|
500
|
+
// unless INCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES matches it too, and following
|
|
501
|
+
// links unless RECURSIVE_SEARCH_PATHS_FOLLOW_SYMLINKS is NO; a missing base names nothing.
|
|
502
|
+
function expandSearchPath(
|
|
503
|
+
directory: string,
|
|
504
|
+
rules: { excluded: RegExp[]; included: RegExp[]; followLinks: boolean },
|
|
505
|
+
): string[] {
|
|
506
|
+
if (path.basename(directory) !== '**') return [directory]
|
|
507
|
+
const expanded: string[] = []
|
|
508
|
+
const seen = new Set<string>()
|
|
509
|
+
// native lists each level before the next, entries sorted by name.
|
|
510
|
+
const queue = [path.dirname(directory)]
|
|
511
|
+
for (let at = 0; at < queue.length; at++) {
|
|
512
|
+
const current = queue[at]!
|
|
513
|
+
let real: string
|
|
514
|
+
try {
|
|
515
|
+
real = fs.realpathSync(current)
|
|
516
|
+
} catch {
|
|
517
|
+
continue
|
|
518
|
+
}
|
|
519
|
+
if (!fs.statSync(real).isDirectory() || seen.has(real)) continue
|
|
520
|
+
seen.add(real)
|
|
521
|
+
expanded.push(current)
|
|
522
|
+
for (const entry of fs
|
|
523
|
+
.readdirSync(real, { withFileTypes: true })
|
|
524
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
|
|
525
|
+
if (entry.isSymbolicLink() ? !rules.followLinks : !entry.isDirectory()) continue
|
|
526
|
+
if (
|
|
527
|
+
rules.excluded.some((pattern) => pattern.test(entry.name)) &&
|
|
528
|
+
!rules.included.some((pattern) => pattern.test(entry.name))
|
|
529
|
+
)
|
|
530
|
+
continue
|
|
531
|
+
queue.push(path.join(current, entry.name))
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return expanded
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// the recursive search path rules a target's build settings select.
|
|
538
|
+
function recursiveSearchRules(settings: (key: string) => string) {
|
|
539
|
+
const glob = (pattern: string) =>
|
|
540
|
+
new RegExp(
|
|
541
|
+
`^${pattern
|
|
542
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
543
|
+
.replaceAll('*', '.*')
|
|
544
|
+
.replaceAll('?', '.')}$`,
|
|
545
|
+
)
|
|
546
|
+
return {
|
|
547
|
+
excluded: settingList(settings('EXCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES')).map(
|
|
548
|
+
glob,
|
|
549
|
+
),
|
|
550
|
+
included: settingList(settings('INCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES')).map(
|
|
551
|
+
glob,
|
|
552
|
+
),
|
|
553
|
+
followLinks: settings('RECURSIVE_SEARCH_PATHS_FOLLOW_SYMLINKS') !== 'NO',
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
488
557
|
// the conditions swift compiles a target with: its active compilation
|
|
489
558
|
// conditions and each `-D` in its other swift flags, as xcode passes them.
|
|
490
559
|
function compilationConditions(settings: (key: string) => string): string[] {
|
|
@@ -1035,6 +1104,7 @@ export function readXcodeAppTarget(
|
|
|
1035
1104
|
for (const file of read) xcconfigs.add(file)
|
|
1036
1105
|
const swiftVersion = Number.parseFloat(settings('SWIFT_VERSION') || '5')
|
|
1037
1106
|
const members = targetMembers(ref, settings, options, keys, xcconfigs)
|
|
1107
|
+
const searchRules = recursiveSearchRules(settings)
|
|
1038
1108
|
const implementations = members.allSources.filter((file) =>
|
|
1039
1109
|
/\.(c|cc|cpp|cxx|m|mm)$/.test(file),
|
|
1040
1110
|
)
|
|
@@ -1085,10 +1155,10 @@ export function readXcodeAppTarget(
|
|
|
1085
1155
|
headerSearchPaths: [
|
|
1086
1156
|
...settingList(settings('HEADER_SEARCH_PATHS')),
|
|
1087
1157
|
...settingList(settings('USER_HEADER_SEARCH_PATHS')),
|
|
1088
|
-
].
|
|
1089
|
-
frameworkSearchPaths: settingList(
|
|
1090
|
-
|
|
1091
|
-
),
|
|
1158
|
+
].flatMap((raw) => expandSearchPath(absolute(raw), searchRules)),
|
|
1159
|
+
frameworkSearchPaths: settingList(
|
|
1160
|
+
settings('FRAMEWORK_SEARCH_PATHS'),
|
|
1161
|
+
).flatMap((raw) => expandSearchPath(absolute(raw), searchRules)),
|
|
1092
1162
|
flags: [
|
|
1093
1163
|
...(settings('CLANG_ENABLE_OBJC_ARC') === 'YES' ? ['-fobjc-arc'] : []),
|
|
1094
1164
|
...(settings('CLANG_ENABLE_OBJC_WEAK') === 'YES'
|