peachsim 0.1.1635 → 0.1.1640
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 -1
- package/cli/app-config.ts +14 -5
- package/cli/app-stubs.ts +62 -13
- package/cli/bridge-flow-runner.ts +2 -2
- package/cli/cloud-client.ts +60 -40
- package/cli/commands/doctor.ts +13 -5
- package/cli/commands/flow.ts +1 -1
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect.ts +65 -47
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +97 -47
- package/cli/commands/swift.ts +9 -3
- package/cli/commands/upload.ts +48 -42
- package/cli/drivers/playwright-sim-host.ts +65 -42
- package/cli/main.ts +1 -1
- package/cli/outbound-endpoints.ts +7 -5
- package/cli/pause-aware-timeout.ts +4 -0
- package/cli/ws-bridge.ts +19 -10
- package/detox/attempt-events.cjs +62 -5
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +121 -47
- package/detox/motion-change.cjs +170 -21
- package/detox/motion-change.d.cts +32 -0
- package/detox/navigation-lifecycle.ts +35 -3
- 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 +8 -7
- 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 +4038 -1983
- 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 +509 -86
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +3551 -1574
- package/dist-lib/expo-swift-metadata.cjs +9 -7
- package/dist-lib/expo-swift-metadata.mjs +9 -7
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +43633 -2635
- 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 +2748 -128
- 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 +1 -1
- package/dist-lib/menu.mjs +1 -1
- 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 +2547 -83
- package/dist-lib/native-app-identity-project.cjs +237 -50
- package/dist-lib/native-app-identity-project.mjs +237 -50
- package/dist-lib/native-app-identity.cjs +29 -30
- package/dist-lib/native-app-identity.mjs +29 -30
- 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 +2 -2
- package/dist-lib/react-native-host-modules.mjs +2 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +65403 -22739
- package/dist-lib/swift-compile.cjs +93 -32
- package/dist-lib/swift-compile.mjs +93 -32
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +7 -5
- package/dist-lib/swift-submit.mjs +7 -5
- package/dist-lib/swift-toolchain.cjs +316 -41
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +7335 -5432
- package/dist-lib/vite.cjs +2857 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/package.json +1 -1
- package/src/backend-origin.ts +13 -6
- package/src/expo-swift-host.ts +5 -2
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +4 -6
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +1 -4
- package/src/native-app-identity.ts +1 -4
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/react-native-bridge-modules.ts +21 -6
- package/src/react-native-host-modules.ts +4 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-compile.ts +91 -33
- package/src/swift-distribution.lock.json +22 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +89 -26
- package/src/swift-icu.ts +16 -10
- package/src/swift-objc-compile.ts +355 -0
- package/src/swift-objc-sdk.ts +689 -18
- package/src/swift-objc-standin.ts +126 -14
- package/src/swift-package-manifest.ts +39 -1
- package/src/swift-packages.ts +35 -21
- package/src/swift-standin-emit.ts +40 -7
- package/src/swift-standins.ts +404 -138
- package/src/swift-submit.ts +3 -5
- package/src/swift-toolchain.ts +378 -46
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +270 -30
package/src/swift-submit.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* Browser-safe: node-only submitters live beside their call sites.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { isRecord } from './is-record'
|
|
23
24
|
import {
|
|
24
25
|
isSwiftMetalLibrarySources,
|
|
25
26
|
type SwiftMetalLibrarySource,
|
|
@@ -108,7 +109,8 @@ export const MAX_SWIFT_COMPILE_IMAGE_TOTAL_BYTES = 16 * 1024 * 1024
|
|
|
108
109
|
// 45: confirmation dialogs present from a measured `presentationSource`, and a
|
|
109
110
|
// refreshable's pull ends on the host's `refreshEnded` request.
|
|
110
111
|
// 46: ImageRenderer produces shared CoreGraphics pixels through the isolated raster host.
|
|
111
|
-
|
|
112
|
+
// 47: backing-layer masks composite the view, border and children through mask alpha.
|
|
113
|
+
export const SWIFT_HOST_ABI = 47
|
|
112
114
|
|
|
113
115
|
export interface SwiftImageUpload {
|
|
114
116
|
/** the raster file picked for the asset (has the extension). */
|
|
@@ -294,10 +296,6 @@ export function httpSwiftCompileSubmit(
|
|
|
294
296
|
}
|
|
295
297
|
}
|
|
296
298
|
|
|
297
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
298
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
299
|
export function isSwiftNativeModules(value: unknown): value is SwiftNativeModules {
|
|
302
300
|
return (
|
|
303
301
|
isRecord(value) &&
|
package/src/swift-toolchain.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { createHash } from 'node:crypto'
|
|
|
10
10
|
import fs from 'node:fs'
|
|
11
11
|
import os from 'node:os'
|
|
12
12
|
import path from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
13
14
|
|
|
14
15
|
/** swiftpm's SDK directory option, shared by discovery and compilation. */
|
|
15
16
|
export function swiftSdkOptions(): string[] {
|
|
@@ -18,20 +19,20 @@ export function swiftSdkOptions(): string[] {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// host archives include the official driver/runtime and the qualified patched frontend.
|
|
21
|
-
export const SWIFT_HOST_COMPILER_RELEASE = 'swift-6.4.0-RELEASE-custom-
|
|
22
|
-
export const SWIFT_HOST_COMPILER_ID = '
|
|
22
|
+
export const SWIFT_HOST_COMPILER_RELEASE = 'swift-6.4.0-RELEASE-custom-0d775d32d7c74eae'
|
|
23
|
+
export const SWIFT_HOST_COMPILER_ID = '0d775d32d7c74eae'
|
|
23
24
|
const compilerArchives: Record<string, { checksum: string; manifest: string }> = {
|
|
24
25
|
'darwin-arm64': {
|
|
25
|
-
checksum: '
|
|
26
|
-
manifest: '
|
|
26
|
+
checksum: '957e85486f92327ee5cf8d3c5bb9336d17ab80145c87645c637c49130a50477a',
|
|
27
|
+
manifest: 'c63e2f81694e8d8ad046d10c71d0638dbae4e5a36601294ed670988d1cb5b6f1',
|
|
27
28
|
},
|
|
28
29
|
'linux-x64': {
|
|
29
|
-
checksum: '
|
|
30
|
-
manifest: '
|
|
30
|
+
checksum: '6302230fb126b79c9af5fc2bc0208a936bb573c343da7ae949c5c1687e761cd2',
|
|
31
|
+
manifest: '31b507a6a8648a2266a187b9fed092e29a63cae99d57c5defdf2e82a7cbbc113',
|
|
31
32
|
},
|
|
32
33
|
'linux-arm64': {
|
|
33
|
-
checksum: '
|
|
34
|
-
manifest: '
|
|
34
|
+
checksum: '9b40d39ea5ba82fd9a417868f0a449f5e98e37cf83acaac9cedd3a8b8d91a218',
|
|
35
|
+
manifest: 'fe07a29d4f4374cad47b29de70e90aa2e3b7664e08c8b53d304585e65637da24',
|
|
35
36
|
},
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -65,10 +66,12 @@ export function swiftCompilerRelease(release: string): string {
|
|
|
65
66
|
|
|
66
67
|
/**
|
|
67
68
|
* the bin directory of the toolchain for `release`, unverified. macOS: the
|
|
68
|
-
* Xcode Toolchains directory, the user's before the system's. Linux:
|
|
69
|
-
*
|
|
69
|
+
* Xcode Toolchains directory, the user's before the system's. Linux:
|
|
70
|
+
* `$CONTRAST_SWIFT_TOOLCHAINS/<release>` when set
|
|
70
71
|
* (packages/sootsim-swift/scripts/install-linux-toolchain.sh fills it), else
|
|
71
|
-
* the cf-build image's `/opt/swift/<release
|
|
72
|
+
* the cf-build image's `/opt/swift/<release>` when present, else a pinned
|
|
73
|
+
* compiler's user-writable `$PEACH_HOME/swift/toolchains/<release>`, else the
|
|
74
|
+
* `swift` first on PATH.
|
|
72
75
|
*/
|
|
73
76
|
export function swiftToolchainBin(release: string): string {
|
|
74
77
|
if (process.platform === 'darwin') {
|
|
@@ -81,8 +84,9 @@ export function swiftToolchainBin(release: string): string {
|
|
|
81
84
|
return path.join(process.env.CONTRAST_SWIFT_TOOLCHAINS, release, 'usr/bin')
|
|
82
85
|
}
|
|
83
86
|
const image = path.join('/opt/swift', release, 'usr/bin')
|
|
84
|
-
if (
|
|
85
|
-
|
|
87
|
+
if (fs.existsSync(path.join(image, 'swift'))) return image
|
|
88
|
+
if (release.includes('-custom-'))
|
|
89
|
+
return path.join(peachHome(), 'swift', 'toolchains', release, 'usr/bin')
|
|
86
90
|
return (
|
|
87
91
|
(process.env.PATH ?? '')
|
|
88
92
|
.split(path.delimiter)
|
|
@@ -90,6 +94,106 @@ export function swiftToolchainBin(release: string): string {
|
|
|
90
94
|
)
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
// home-paths.ts's rnxHomeDir, repeated because this file imports node builtins only.
|
|
98
|
+
function peachHome(): string {
|
|
99
|
+
const override = process.env.PEACH_HOME
|
|
100
|
+
return override ? path.resolve(override) : path.join(os.homedir(), '.peach')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let scratch: string | undefined
|
|
104
|
+
let swiftHome: string | undefined
|
|
105
|
+
/**
|
|
106
|
+
* keep the temporary files swift processes write (swiftpm's lock files and
|
|
107
|
+
* TemporaryDirectory folders, which it never removes) in a per-process
|
|
108
|
+
* directory under `root`, removed when this process exits, and on linux its
|
|
109
|
+
* home directory in `root/home`, kept so the caches persist. directories left
|
|
110
|
+
* by processes that no longer run are removed first. without this call swift
|
|
111
|
+
* inherits the caller's TMPDIR.
|
|
112
|
+
*/
|
|
113
|
+
export function confineSwiftScratch(root: string): void {
|
|
114
|
+
if (scratch) return
|
|
115
|
+
fs.mkdirSync(root, { recursive: true })
|
|
116
|
+
for (const name of fs.readdirSync(root)) {
|
|
117
|
+
const pid = Number(name)
|
|
118
|
+
if (Number.isInteger(pid) && pid > 0 && !processRuns(pid))
|
|
119
|
+
fs.rmSync(path.join(root, name), { recursive: true, force: true })
|
|
120
|
+
}
|
|
121
|
+
const dir = path.join(root, String(process.pid))
|
|
122
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
123
|
+
scratch = dir
|
|
124
|
+
// swiftpm and clang write ~/.swiftpm and ~/.cache even with every path flag
|
|
125
|
+
// given; on linux no tool needs the real home, so they get a kept one here.
|
|
126
|
+
if (process.platform === 'linux') {
|
|
127
|
+
swiftHome = path.join(root, 'home')
|
|
128
|
+
fs.mkdirSync(swiftHome, { recursive: true })
|
|
129
|
+
}
|
|
130
|
+
// swiftpm makes ~/.swiftpm in the account's passwd home whatever HOME says;
|
|
131
|
+
// remove at exit only the entries swiftpm seeds there that were absent now:
|
|
132
|
+
// a cache link into a swiftpm cache folder and empty configuration and
|
|
133
|
+
// security folders. anything else, a link or file a user made included, stays.
|
|
134
|
+
const seeded = path.join(os.homedir(), '.swiftpm')
|
|
135
|
+
const dirAbsentBefore = lstatIfKnown(seeded) === null
|
|
136
|
+
const absentBefore = swiftpmSeeds.filter(
|
|
137
|
+
(name) => lstatIfKnown(path.join(seeded, name)) === null,
|
|
138
|
+
)
|
|
139
|
+
process.once('exit', () => {
|
|
140
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
141
|
+
for (const name of absentBefore) removeSwiftpmSeed(path.join(seeded, name), name)
|
|
142
|
+
if (dirAbsentBefore && lstatIfKnown(seeded)?.isDirectory())
|
|
143
|
+
try {
|
|
144
|
+
fs.rmdirSync(seeded)
|
|
145
|
+
} catch {}
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const swiftpmSeeds = ['cache', 'configuration', 'security']
|
|
150
|
+
|
|
151
|
+
// only a missing path establishes absence; other lookup failures stay unknown.
|
|
152
|
+
function lstatIfKnown(entry: string): fs.Stats | null | undefined {
|
|
153
|
+
try {
|
|
154
|
+
return fs.lstatSync(entry)
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return null
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function removeSwiftpmSeed(entry: string, name: string): void {
|
|
162
|
+
const stat = lstatIfKnown(entry)
|
|
163
|
+
if (!stat) return
|
|
164
|
+
try {
|
|
165
|
+
if (name === 'cache') {
|
|
166
|
+
if (
|
|
167
|
+
stat.isSymbolicLink() &&
|
|
168
|
+
fs.readlinkSync(entry).endsWith('/.cache/org.swift.swiftpm')
|
|
169
|
+
)
|
|
170
|
+
fs.unlinkSync(entry)
|
|
171
|
+
} else if (stat.isDirectory()) {
|
|
172
|
+
fs.rmdirSync(entry)
|
|
173
|
+
}
|
|
174
|
+
} catch {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function processRuns(pid: number): boolean {
|
|
178
|
+
try {
|
|
179
|
+
process.kill(pid, 0)
|
|
180
|
+
return true
|
|
181
|
+
} catch (error) {
|
|
182
|
+
return error instanceof Error && 'code' in error && error.code === 'EPERM'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** the environment every swift process runs with: `base`, with TMPDIR in the
|
|
187
|
+
* confined scratch directory when the caller set one. */
|
|
188
|
+
export function swiftEnvironment(
|
|
189
|
+
base: NodeJS.ProcessEnv = process.env,
|
|
190
|
+
): NodeJS.ProcessEnv {
|
|
191
|
+
if (!scratch) return base
|
|
192
|
+
return swiftHome
|
|
193
|
+
? { ...base, TMPDIR: scratch, HOME: swiftHome }
|
|
194
|
+
: { ...base, TMPDIR: scratch }
|
|
195
|
+
}
|
|
196
|
+
|
|
93
197
|
/** the wasm SDK a release compiles with: its installed id, where it
|
|
94
198
|
* downloads from and its archive checksum (packages/sootsim-swift/toolchain.json). */
|
|
95
199
|
export interface SwiftWasmSdk {
|
|
@@ -98,17 +202,13 @@ export interface SwiftWasmSdk {
|
|
|
98
202
|
wasmSdkChecksum: string
|
|
99
203
|
}
|
|
100
204
|
|
|
101
|
-
const verified = new Map<string, string>()
|
|
102
|
-
|
|
103
205
|
/**
|
|
104
206
|
* swiftToolchainBin, checked: its swift is exactly `release` and has the
|
|
105
207
|
* pinned wasm SDK installed. anything else fails with one message naming the
|
|
106
208
|
* release and the commands that install it.
|
|
107
209
|
*/
|
|
108
210
|
export function swiftToolchain(release: string, sdk: SwiftWasmSdk): string {
|
|
109
|
-
|
|
110
|
-
const cached = verified.get(key)
|
|
111
|
-
if (cached) return cached
|
|
211
|
+
validateSwiftSdkIdentity(sdk)
|
|
112
212
|
let bin: string
|
|
113
213
|
try {
|
|
114
214
|
bin = swiftCompiler(release)
|
|
@@ -121,17 +221,218 @@ export function swiftToolchain(release: string, sdk: SwiftWasmSdk): string {
|
|
|
121
221
|
const swift = path.join(bin, 'swift')
|
|
122
222
|
const sdks = execFileSync(swift, ['sdk', 'list', ...swiftSdkOptions()], {
|
|
123
223
|
encoding: 'utf8',
|
|
224
|
+
env: swiftEnvironment(),
|
|
124
225
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
125
226
|
})
|
|
126
227
|
if (!sdks.split('\n').some((line) => line.trim() === sdk.wasmSdkId)) {
|
|
127
228
|
throw new Error(
|
|
128
|
-
`peach compiles Swift with the ${sdk.wasmSdkId} SDK, which ${swift} does not have.\n${
|
|
229
|
+
`peach compiles Swift with the ${sdk.wasmSdkId} SDK, which ${swift} does not have.\n${swiftSdkInstallStep(release, sdk)}`,
|
|
129
230
|
)
|
|
130
231
|
}
|
|
131
|
-
|
|
232
|
+
try {
|
|
233
|
+
verifySwiftSdk(swift, sdk)
|
|
234
|
+
} catch (error) {
|
|
235
|
+
const reason = error instanceof Error ? error.message : String(error)
|
|
236
|
+
throw new Error(`${reason}\n${swiftSdkInstallStep(release, sdk)}`)
|
|
237
|
+
}
|
|
132
238
|
return bin
|
|
133
239
|
}
|
|
134
240
|
|
|
241
|
+
// swiftpm's info.json describes SDK selection, not the archive it came from.
|
|
242
|
+
function validateSwiftSdkIdentity(sdk: SwiftWasmSdk): void {
|
|
243
|
+
if (
|
|
244
|
+
!/^[a-zA-Z0-9._-]+$/.test(sdk.wasmSdkId) ||
|
|
245
|
+
!/^[a-f0-9]{64}$/.test(sdk.wasmSdkChecksum)
|
|
246
|
+
)
|
|
247
|
+
throw new Error('invalid Swift SDK identity or checksum')
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function swiftSdkBundle(swift: string, id: string, options: string[]): string {
|
|
251
|
+
const configuration = execFileSync(
|
|
252
|
+
swift,
|
|
253
|
+
['sdk', 'configure', ...options, '--show-configuration', id, 'wasm32-unknown-wasip1'],
|
|
254
|
+
{ encoding: 'utf8', env: swiftEnvironment(), stdio: ['ignore', 'pipe', 'pipe'] },
|
|
255
|
+
)
|
|
256
|
+
const sysroot = /^sdkRootPath: (.+)$/m.exec(configuration)?.[1]?.trim()
|
|
257
|
+
if (!sysroot || !path.isAbsolute(sysroot))
|
|
258
|
+
throw new Error(`Swift SDK ${id} reports no absolute sdkRootPath`)
|
|
259
|
+
let bundle = sysroot
|
|
260
|
+
while (!bundle.endsWith('.artifactbundle')) {
|
|
261
|
+
const parent = path.dirname(bundle)
|
|
262
|
+
if (parent === bundle)
|
|
263
|
+
throw new Error(`Swift SDK ${id} is outside an artifact bundle`)
|
|
264
|
+
bundle = parent
|
|
265
|
+
}
|
|
266
|
+
return fs.realpathSync(bundle)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// stat changes invalidate file hashes; every lookup enumerates the entire bundle,
|
|
270
|
+
// including additions, removals, executable modes and internal symlink targets.
|
|
271
|
+
const sdkFileHashes = new Map<string, { stamp: string; checksum: string }>()
|
|
272
|
+
function swiftSdkPayloadChecksum(bundle: string): string {
|
|
273
|
+
const hash = createHash('sha256')
|
|
274
|
+
function visit(relative: string): void {
|
|
275
|
+
const file = path.join(bundle, relative)
|
|
276
|
+
const stat = fs.lstatSync(file, { bigint: true })
|
|
277
|
+
if (stat.isSymbolicLink()) {
|
|
278
|
+
const resolved = path.relative(bundle, fs.realpathSync(file))
|
|
279
|
+
if (
|
|
280
|
+
resolved === '..' ||
|
|
281
|
+
resolved.startsWith(`..${path.sep}`) ||
|
|
282
|
+
path.isAbsolute(resolved)
|
|
283
|
+
)
|
|
284
|
+
throw new Error(`Swift SDK symlink escapes its bundle: ${relative}`)
|
|
285
|
+
hash.update(JSON.stringify([relative, 'link', fs.readlinkSync(file)]) + '\n')
|
|
286
|
+
} else if (stat.isDirectory()) {
|
|
287
|
+
hash.update(JSON.stringify([relative, 'directory']) + '\n')
|
|
288
|
+
for (const name of fs.readdirSync(file).sort()) visit(path.join(relative, name))
|
|
289
|
+
} else if (stat.isFile()) {
|
|
290
|
+
const stamp = [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(':')
|
|
291
|
+
let cached = sdkFileHashes.get(file)
|
|
292
|
+
if (cached?.stamp !== stamp) {
|
|
293
|
+
cached = { stamp, checksum: swiftFileChecksum(file) }
|
|
294
|
+
sdkFileHashes.set(file, cached)
|
|
295
|
+
}
|
|
296
|
+
hash.update(
|
|
297
|
+
JSON.stringify([relative, 'file', Number(stat.mode & 0o111n), cached.checksum]) +
|
|
298
|
+
'\n',
|
|
299
|
+
)
|
|
300
|
+
} else throw new Error(`unsupported Swift SDK payload entry: ${relative}`)
|
|
301
|
+
}
|
|
302
|
+
visit('')
|
|
303
|
+
return hash.digest('hex')
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function swiftSdkVerificationFile(bundle: string, sdk: SwiftWasmSdk): string {
|
|
307
|
+
return path.join(
|
|
308
|
+
path.dirname(bundle),
|
|
309
|
+
'.peach-sdk-checksums',
|
|
310
|
+
`${sdk.wasmSdkId}-${sdk.wasmSdkChecksum}.json`,
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function verifySwiftSdk(swift: string, sdk: SwiftWasmSdk): void {
|
|
315
|
+
const bundle = swiftSdkBundle(swift, sdk.wasmSdkId, swiftSdkOptions())
|
|
316
|
+
const file = swiftSdkVerificationFile(bundle, sdk)
|
|
317
|
+
if (!fs.existsSync(file))
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Swift SDK ${sdk.wasmSdkId} has no verified archive binding for ${sdk.wasmSdkChecksum}. Hydrate it with installSwiftWasmSdk before compiling.`,
|
|
320
|
+
)
|
|
321
|
+
const receipt: unknown = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
322
|
+
if (
|
|
323
|
+
!receipt ||
|
|
324
|
+
typeof receipt !== 'object' ||
|
|
325
|
+
!('wasmSdkId' in receipt) ||
|
|
326
|
+
receipt.wasmSdkId !== sdk.wasmSdkId ||
|
|
327
|
+
!('wasmSdkChecksum' in receipt) ||
|
|
328
|
+
receipt.wasmSdkChecksum !== sdk.wasmSdkChecksum ||
|
|
329
|
+
!('payloadChecksum' in receipt) ||
|
|
330
|
+
receipt.payloadChecksum !== swiftSdkPayloadChecksum(bundle)
|
|
331
|
+
)
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Swift SDK ${sdk.wasmSdkId} installed payload differs from its verified archive ${sdk.wasmSdkChecksum}`,
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** install or qualify existing bytes, serializing shared-store mutation without replacement. */
|
|
338
|
+
function installSwiftSdkArchive(swift: string, sdk: SwiftWasmSdk, archive: string): void {
|
|
339
|
+
validateSwiftSdkIdentity(sdk)
|
|
340
|
+
const staging = fs.realpathSync(
|
|
341
|
+
fs.mkdtempSync(path.join(scratch ?? os.tmpdir(), 'peach-sdk-verification-')),
|
|
342
|
+
)
|
|
343
|
+
try {
|
|
344
|
+
if (swiftFileChecksum(archive) !== sdk.wasmSdkChecksum)
|
|
345
|
+
throw new Error('declared Swift SDK archive checksum mismatch')
|
|
346
|
+
const store = path.join(staging, 'sdks')
|
|
347
|
+
fs.mkdirSync(store)
|
|
348
|
+
execFileSync(
|
|
349
|
+
swift,
|
|
350
|
+
[
|
|
351
|
+
'sdk',
|
|
352
|
+
'install',
|
|
353
|
+
archive,
|
|
354
|
+
'--checksum',
|
|
355
|
+
sdk.wasmSdkChecksum,
|
|
356
|
+
'--swift-sdks-path',
|
|
357
|
+
store,
|
|
358
|
+
],
|
|
359
|
+
{ env: swiftEnvironment(), stdio: 'inherit' },
|
|
360
|
+
)
|
|
361
|
+
const stagedBundle = swiftSdkBundle(swift, sdk.wasmSdkId, [
|
|
362
|
+
'--swift-sdks-path',
|
|
363
|
+
store,
|
|
364
|
+
])
|
|
365
|
+
const expected = swiftSdkPayloadChecksum(stagedBundle)
|
|
366
|
+
const directory = process.env.PEACH_SWIFT_SDKS_PATH
|
|
367
|
+
const locks = directory
|
|
368
|
+
? path.join(path.resolve(directory), '.peach-locks')
|
|
369
|
+
: path.join(os.homedir(), '.swiftpm', 'builder-locks')
|
|
370
|
+
fs.mkdirSync(locks, { recursive: true })
|
|
371
|
+
execFileSync(
|
|
372
|
+
'perl',
|
|
373
|
+
[
|
|
374
|
+
'-e',
|
|
375
|
+
'use Fcntl qw(:flock); my ($lock, $id, $bundle, $swift, @options) = @ARGV; open(my $fd, ">>", $lock) or die $!; flock($fd, LOCK_EX) or die $!; open(my $list, "-|", $swift, "sdk", "list", @options) or die $!; my $found = 0; while (<$list>) { s/^\\s+|\\s+$//g; $found = 1 if $_ eq $id; } close($list) or die "swift sdk list failed"; unless ($found) { system($swift, "sdk", "install", $bundle, @options); exit(($? & 127) ? 128 + ($? & 127) : $? >> 8); }',
|
|
376
|
+
path.join(locks, 'wasm-sdk.lock'),
|
|
377
|
+
sdk.wasmSdkId,
|
|
378
|
+
stagedBundle,
|
|
379
|
+
swift,
|
|
380
|
+
...swiftSdkOptions(),
|
|
381
|
+
],
|
|
382
|
+
{ env: swiftEnvironment(), stdio: 'inherit' },
|
|
383
|
+
)
|
|
384
|
+
const bundle = swiftSdkBundle(swift, sdk.wasmSdkId, swiftSdkOptions())
|
|
385
|
+
if (swiftSdkPayloadChecksum(bundle) !== expected)
|
|
386
|
+
throw new Error(
|
|
387
|
+
`Swift SDK ${sdk.wasmSdkId} already exists with different payload bytes; select a separate --swift-sdks-path. No installed SDK was replaced.`,
|
|
388
|
+
)
|
|
389
|
+
const file = swiftSdkVerificationFile(bundle, sdk)
|
|
390
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
391
|
+
const temporary = `${file}.${process.pid}.tmp`
|
|
392
|
+
try {
|
|
393
|
+
fs.writeFileSync(
|
|
394
|
+
temporary,
|
|
395
|
+
JSON.stringify({
|
|
396
|
+
wasmSdkId: sdk.wasmSdkId,
|
|
397
|
+
wasmSdkChecksum: sdk.wasmSdkChecksum,
|
|
398
|
+
payloadChecksum: expected,
|
|
399
|
+
}) + '\n',
|
|
400
|
+
)
|
|
401
|
+
fs.renameSync(temporary, file)
|
|
402
|
+
} finally {
|
|
403
|
+
fs.rmSync(temporary, { force: true })
|
|
404
|
+
}
|
|
405
|
+
} finally {
|
|
406
|
+
fs.rmSync(staging, { recursive: true, force: true })
|
|
407
|
+
for (const file of sdkFileHashes.keys())
|
|
408
|
+
if (file.startsWith(staging + path.sep)) sdkFileHashes.delete(file)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** verify the compiler and install a declared SDK under the shared store lock. */
|
|
413
|
+
export function installSwiftWasmSdk(release: string, sdk: SwiftWasmSdk): void {
|
|
414
|
+
validateSwiftSdkIdentity(sdk)
|
|
415
|
+
try {
|
|
416
|
+
swiftToolchain(release, sdk)
|
|
417
|
+
return
|
|
418
|
+
} catch {
|
|
419
|
+
// an unverified existing SDK must be compared with the declared archive.
|
|
420
|
+
}
|
|
421
|
+
const swift = path.join(swiftCompiler(release), 'swift')
|
|
422
|
+
const staging = fs.mkdtempSync(path.join(scratch ?? os.tmpdir(), 'builder-swift-sdk-'))
|
|
423
|
+
const archive = path.join(staging, 'sdk.artifactbundle.tar.gz')
|
|
424
|
+
try {
|
|
425
|
+
if (sdk.wasmSdkUrl.startsWith('file:'))
|
|
426
|
+
fs.copyFileSync(fileURLToPath(sdk.wasmSdkUrl), archive)
|
|
427
|
+
else
|
|
428
|
+
execFileSync('curl', ['-fsSL', sdk.wasmSdkUrl, '-o', archive], { stdio: 'inherit' })
|
|
429
|
+
installSwiftSdkArchive(swift, sdk, archive)
|
|
430
|
+
swiftToolchain(release, sdk)
|
|
431
|
+
} finally {
|
|
432
|
+
fs.rmSync(staging, { recursive: true, force: true })
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
135
436
|
// stream large compiler binaries without retaining a second copy in memory.
|
|
136
437
|
export function swiftFileChecksum(file: string): string {
|
|
137
438
|
const hash = createHash('sha256')
|
|
@@ -170,7 +471,7 @@ export function verifySwiftHostCompiler(release: string, root: string): void {
|
|
|
170
471
|
throw new Error(`Swift compiler identity mismatch at ${root}`)
|
|
171
472
|
}
|
|
172
473
|
const files = Object.entries(manifest.files)
|
|
173
|
-
if (files.length !==
|
|
474
|
+
if (files.length !== 16) throw new Error('incomplete Swift compiler manifest')
|
|
174
475
|
for (const [relative, checksum] of files) {
|
|
175
476
|
if (
|
|
176
477
|
relative.startsWith('/') ||
|
|
@@ -193,6 +494,7 @@ export function swiftCompiler(release: string): string {
|
|
|
193
494
|
verifySwiftHostCompiler(release, path.resolve(bin, '../..'))
|
|
194
495
|
const version = execFileSync(path.join(bin, 'swift'), ['--version'], {
|
|
195
496
|
encoding: 'utf8',
|
|
497
|
+
env: swiftEnvironment(),
|
|
196
498
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
197
499
|
})
|
|
198
500
|
if (!version.includes(`(${swiftCompilerRelease(release)})`))
|
|
@@ -201,41 +503,71 @@ export function swiftCompiler(release: string): string {
|
|
|
201
503
|
return bin
|
|
202
504
|
}
|
|
203
505
|
|
|
506
|
+
/** the command that installs and qualifies the pinned SDK for an installed compiler. */
|
|
507
|
+
export function swiftSdkInstallStep(release: string, wasmSdk: SwiftWasmSdk): string {
|
|
508
|
+
return `install the pinned SDK:\n ${swiftSdkInstallCommand(release, wasmSdk)}`
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function swiftSdkInstallCommand(release: string, wasmSdk: SwiftWasmSdk): string {
|
|
512
|
+
const { wasmSdkId, wasmSdkUrl, wasmSdkChecksum } = wasmSdk
|
|
513
|
+
const program = `import { installSwiftWasmSdk } from "peachsim/swift-toolchain"; installSwiftWasmSdk(${JSON.stringify(release)}, ${JSON.stringify({ wasmSdkId, wasmSdkUrl, wasmSdkChecksum })})`
|
|
514
|
+
const directory = process.env.PEACH_SWIFT_SDKS_PATH
|
|
515
|
+
const environment = directory
|
|
516
|
+
? `PEACH_SWIFT_SDKS_PATH='${path.resolve(directory).replaceAll("'", "'\\''")}' `
|
|
517
|
+
: ''
|
|
518
|
+
return `${environment}node --input-type=module -e '${program.replaceAll("'", "'\\''")}'`
|
|
519
|
+
}
|
|
520
|
+
|
|
204
521
|
/** the commands that install the pinned compiler and SDK on this host. */
|
|
205
522
|
export function swiftInstallSteps(release: string, wasmSdk: SwiftWasmSdk): string {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
523
|
+
const { note, commands } = swiftCompilerInstall(release)
|
|
524
|
+
return [
|
|
525
|
+
`install the pinned compiler and SDK${note}:`,
|
|
526
|
+
...[...commands, swiftSdkInstallCommand(release, wasmSdk)].map((line) => ` ${line}`),
|
|
527
|
+
].join('\n')
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/** the commands that install the pinned compiler alone, for a caller that installs the SDK. */
|
|
531
|
+
export function swiftCompilerInstallSteps(release: string): string {
|
|
532
|
+
const { note, commands } = swiftCompilerInstall(release)
|
|
533
|
+
return [
|
|
534
|
+
`install the pinned compiler${note}:`,
|
|
535
|
+
...commands.map((line) => ` ${line}`),
|
|
536
|
+
].join('\n')
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function swiftCompilerInstall(release: string): { note: string; commands: string[] } {
|
|
209
540
|
if (release.includes('-custom-')) {
|
|
210
541
|
const archive = swiftHostCompilerArchive(release)
|
|
211
542
|
const directory = path.resolve(swiftToolchainBin(release), '../..')
|
|
212
|
-
return
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
543
|
+
return {
|
|
544
|
+
note: '',
|
|
545
|
+
commands: [
|
|
546
|
+
`curl -fsSL ${archive.url} -o compiler.tar.gz`,
|
|
547
|
+
`echo '${archive.checksum} compiler.tar.gz' | shasum -a 256 -c -`,
|
|
548
|
+
`mkdir -p '${directory}'`,
|
|
549
|
+
`tar -xzf compiler.tar.gz --strip-components=1 -C '${directory}'`,
|
|
550
|
+
],
|
|
551
|
+
}
|
|
220
552
|
}
|
|
221
553
|
const version = release.replace(/^swift-/, '').toLowerCase()
|
|
222
554
|
const downloads = `https://download.swift.org/swift-${version}`
|
|
223
|
-
const sdk = `swift sdk install ${wasmSdk.wasmSdkUrl} --checksum ${wasmSdk.wasmSdkChecksum}${sdkOptions}`
|
|
224
555
|
if (process.platform === 'darwin') {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
556
|
+
return {
|
|
557
|
+
note: '',
|
|
558
|
+
commands: [
|
|
559
|
+
`curl -fsSLO ${downloads}/xcode/${release}/${release}-osx.pkg`,
|
|
560
|
+
`installer -pkg ${release}-osx.pkg -target CurrentUserHomeDirectory`,
|
|
561
|
+
],
|
|
562
|
+
}
|
|
232
563
|
}
|
|
233
564
|
const platform = `debian12${process.arch === 'arm64' ? '-aarch64' : ''}`
|
|
234
565
|
const name = `${release}-${platform}`
|
|
235
|
-
return
|
|
236
|
-
'
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
566
|
+
return {
|
|
567
|
+
note: ' (Debian 12 shown; other distributions: https://www.swift.org/install/linux/)',
|
|
568
|
+
commands: [
|
|
569
|
+
`curl -fsSL ${downloads}/${platform}/${release}/${name}.tar.gz | tar -xz -C /opt`,
|
|
570
|
+
`export PATH=/opt/${name}/usr/bin:$PATH`,
|
|
571
|
+
],
|
|
572
|
+
}
|
|
241
573
|
}
|
|
@@ -242,10 +242,16 @@ const linkedChunks = new Set();
|
|
|
242
242
|
function chunkBody(url) {
|
|
243
243
|
let body = chunkBodies.get(url);
|
|
244
244
|
if (!body) {
|
|
245
|
-
body =
|
|
245
|
+
body = (async () => {
|
|
246
|
+
if (deliveredChunkList && (await deliveredChunkList.promise).has(url)) {
|
|
247
|
+
const bytes = await deliveredChunk(url).promise;
|
|
248
|
+
deliveredChunks.delete(url);
|
|
249
|
+
return new TextDecoder().decode(bytes);
|
|
250
|
+
}
|
|
251
|
+
const response = await fetchWorkerChunk(url, { credentials: 'same-origin' });
|
|
246
252
|
if (!response.ok) throw new Error(url + ': HTTP ' + response.status);
|
|
247
253
|
return response.text();
|
|
248
|
-
});
|
|
254
|
+
})();
|
|
249
255
|
body.catch(() => chunkBodies.delete(url));
|
|
250
256
|
chunkBodies.set(url, body);
|
|
251
257
|
}
|
|
@@ -261,16 +267,9 @@ const importModule = specifier => import(specifier);
|
|
|
261
267
|
System.importModule = importModule;
|
|
262
268
|
System.instantiate = async function(url) {
|
|
263
269
|
if (url === workerEntryUrl) return workerEntryRegistration;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
deliveredChunks.delete(url);
|
|
268
|
-
source = new TextDecoder().decode(body);
|
|
269
|
-
} else {
|
|
270
|
-
source = await chunkBody(url);
|
|
271
|
-
chunkBodies.delete(url);
|
|
272
|
-
linkedChunks.add(url);
|
|
273
|
-
}
|
|
270
|
+
const source = await chunkBody(url);
|
|
271
|
+
chunkBodies.delete(url);
|
|
272
|
+
linkedChunks.add(url);
|
|
274
273
|
let registration;
|
|
275
274
|
// minified module chunks assume strict block-scoped function semantics.
|
|
276
275
|
new Function('System', '"use strict";\\n' + source)({
|
|
@@ -92,6 +92,7 @@ interface Scope {
|
|
|
92
92
|
|
|
93
93
|
type Binding =
|
|
94
94
|
| { kind: 'value'; init: SyntaxNode; scope: Scope }
|
|
95
|
+
| { kind: 'uninitialized' }
|
|
95
96
|
// `key` is set when the parameter is a destructured member of that argument.
|
|
96
97
|
| { kind: 'param'; fn: SyntaxNode; index: number; key?: string }
|
|
97
98
|
| { kind: 'opaque' }
|
|
@@ -106,6 +107,9 @@ interface InitCollection {
|
|
|
106
107
|
writes: Array<{ name: string; scope: Scope; value?: SyntaxNode }>
|
|
107
108
|
// bindings written after their declaration; their value is not static.
|
|
108
109
|
reassigned: Set<Binding>
|
|
110
|
+
// rolldown hoists declarations out of esm initializers. a single plain
|
|
111
|
+
// assignment still identifies the literal; multiple writes remain opaque.
|
|
112
|
+
assigned: Map<Binding, { value: SyntaxNode; scope: Scope } | null>
|
|
109
113
|
// names the parsed source never declares but writes exactly once with a plain
|
|
110
114
|
// assignment. a bundler that hoists a module's declarations out of its
|
|
111
115
|
// initializer leaves only that write in the initializer.
|
|
@@ -227,6 +231,8 @@ function enter(node: SyntaxNode, scope: Scope): Scope {
|
|
|
227
231
|
const name = identifierName(id)
|
|
228
232
|
if (name !== undefined && isNode(init))
|
|
229
233
|
declare(target, name, { kind: 'value', init, scope })
|
|
234
|
+
else if (name !== undefined && init === null)
|
|
235
|
+
declare(target, name, { kind: 'uninitialized' })
|
|
230
236
|
else declareOpaque(target, patternNames(id))
|
|
231
237
|
}
|
|
232
238
|
return scope
|
|
@@ -311,12 +317,18 @@ function collectInitData(root: SyntaxNode): InitCollection {
|
|
|
311
317
|
immediate: new Map(),
|
|
312
318
|
writes: [],
|
|
313
319
|
reassigned: new Set(),
|
|
320
|
+
assigned: new Map(),
|
|
314
321
|
free: new Map(),
|
|
315
322
|
}
|
|
316
323
|
visit(root, newScope(undefined, true), collection)
|
|
317
324
|
for (const { name, scope, value } of collection.writes) {
|
|
318
325
|
const binding = lookup(scope, name)
|
|
319
|
-
if (binding
|
|
326
|
+
if (binding?.kind === 'uninitialized')
|
|
327
|
+
collection.assigned.set(
|
|
328
|
+
binding,
|
|
329
|
+
collection.assigned.has(binding) || !value ? null : { value, scope },
|
|
330
|
+
)
|
|
331
|
+
else if (binding) collection.reassigned.add(binding)
|
|
320
332
|
else
|
|
321
333
|
collection.free.set(
|
|
322
334
|
name,
|
|
@@ -415,6 +427,12 @@ function resolveObject(
|
|
|
415
427
|
: undefined
|
|
416
428
|
}
|
|
417
429
|
if (collection.reassigned.has(binding)) return undefined
|
|
430
|
+
if (binding.kind === 'uninitialized') {
|
|
431
|
+
const assigned = collection.assigned.get(binding)
|
|
432
|
+
return assigned
|
|
433
|
+
? resolveObject(assigned.value, assigned.scope, collection, depth + 1)
|
|
434
|
+
: undefined
|
|
435
|
+
}
|
|
418
436
|
if (binding.kind === 'value')
|
|
419
437
|
return resolveObject(binding.init, binding.scope, collection, depth + 1)
|
|
420
438
|
if (binding.kind === 'param') {
|