peachsim 0.1.1635 → 0.1.1641
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/cli/app-config.ts +14 -5
- package/cli/app-stubs.ts +96 -22
- package/cli/bridge-flow-runner.ts +2 -2
- package/cli/cloud-client.ts +60 -40
- package/cli/cloud-session.ts +16 -0
- package/cli/commands/assert.ts +17 -4
- package/cli/commands/box.ts +33 -19
- package/cli/commands/doctor.ts +37 -20
- package/cli/commands/flow.ts +1 -1
- package/cli/commands/inspect/actions.ts +3 -2
- package/cli/commands/inspect/core.ts +0 -8
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect.ts +65 -61
- package/cli/commands/platform.ts +2 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +154 -68
- package/cli/commands/swift.ts +18 -4
- package/cli/commands/three-mode.ts +4 -38
- package/cli/commands/upload.ts +55 -49
- package/cli/drivers/playwright-sim-host.ts +65 -42
- package/cli/flow-live-status.ts +0 -2
- package/cli/main.ts +2 -2
- package/cli/outbound-endpoints.ts +7 -5
- package/cli/pause-aware-timeout.ts +4 -0
- package/cli/ws-bridge.ts +19 -16
- package/detox/attempt-events.cjs +99 -6
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +259 -139
- package/detox/jest-environment.cjs +5 -1
- 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 +17 -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 +5014 -2268
- 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 +1036 -166
- 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 +4613 -1963
- 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 +58072 -16400
- 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 +2790 -143
- 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 +2579 -95
- package/dist-lib/native-app-identity-project.cjs +243 -53
- package/dist-lib/native-app-identity-project.mjs +243 -53
- 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 +4 -3
- package/dist-lib/sdk.mjs +4 -3
- package/dist-lib/skills.cjs +66475 -23097
- package/dist-lib/swift-compile.cjs +452 -260
- package/dist-lib/swift-compile.mjs +451 -252
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +14 -7
- package/dist-lib/swift-submit.mjs +14 -7
- package/dist-lib/swift-toolchain.cjs +316 -41
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8188 -5636
- 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 +21 -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 +106 -78
- package/src/react-native-host-modules.ts +4 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-compile.ts +224 -139
- package/src/swift-distribution.lock.json +23 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +95 -30
- package/src/swift-icu.ts +16 -10
- package/src/swift-objc-compile.ts +547 -0
- package/src/swift-objc-metadata.ts +204 -0
- package/src/swift-objc-sdk.ts +883 -19
- package/src/swift-objc-standin.ts +126 -14
- package/src/swift-package-manifest.ts +42 -1
- package/src/swift-packages.ts +35 -21
- package/src/swift-root-lock.ts +153 -0
- package/src/swift-standin-emit.ts +40 -7
- package/src/swift-standins.ts +404 -138
- package/src/swift-submit.ts +18 -7
- package/src/swift-toolchain.ts +378 -46
- package/src/swiftContract.ts +12 -0
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +280 -33
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). */
|
|
@@ -152,8 +154,10 @@ export type SwiftCompileOutcome =
|
|
|
152
154
|
* the build is running; poll the same source set again. One build is tens of
|
|
153
155
|
* seconds and a service-binding request is cancelled at about four, so the
|
|
154
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.
|
|
155
159
|
*/
|
|
156
|
-
| { status: 'compiling'; hash: string }
|
|
160
|
+
| { status: 'compiling'; hash: string; stage?: string }
|
|
157
161
|
/** refused before any work: no entry, too many files, over a cap, over a rate limit. */
|
|
158
162
|
| { status: 'refused'; detail: string }
|
|
159
163
|
/** the compiler ran and failed; detail carries its diagnostic verbatim. */
|
|
@@ -294,10 +298,6 @@ export function httpSwiftCompileSubmit(
|
|
|
294
298
|
}
|
|
295
299
|
}
|
|
296
300
|
|
|
297
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
298
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
301
|
export function isSwiftNativeModules(value: unknown): value is SwiftNativeModules {
|
|
302
302
|
return (
|
|
303
303
|
isRecord(value) &&
|
|
@@ -335,7 +335,11 @@ function isSwiftCompileOutcome(value: unknown): value is SwiftCompileOutcome {
|
|
|
335
335
|
(value.native === undefined || isSwiftNativeModules(value.native))
|
|
336
336
|
)
|
|
337
337
|
case 'compiling':
|
|
338
|
-
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
|
+
)
|
|
339
343
|
case 'refused':
|
|
340
344
|
case 'failed':
|
|
341
345
|
return typeof value.detail === 'string'
|
|
@@ -407,7 +411,10 @@ export async function compileSwiftProject(
|
|
|
407
411
|
},
|
|
408
412
|
origin: string,
|
|
409
413
|
signal?: AbortSignal,
|
|
414
|
+
/** called when a poll reports a compiler stage the previous poll did not. */
|
|
415
|
+
onStage?: (stage: string) => void,
|
|
410
416
|
): Promise<SwiftModuleAddress> {
|
|
417
|
+
let lastStage: string | undefined
|
|
411
418
|
while (true) {
|
|
412
419
|
signal?.throwIfAborted()
|
|
413
420
|
const outcome = await submit(work)
|
|
@@ -433,6 +440,10 @@ export async function compileSwiftProject(
|
|
|
433
440
|
...(outcome.native === undefined ? {} : { native: outcome.native }),
|
|
434
441
|
}
|
|
435
442
|
case 'compiling':
|
|
443
|
+
if (outcome.stage !== undefined && outcome.stage !== lastStage) {
|
|
444
|
+
lastStage = outcome.stage
|
|
445
|
+
onStage?.(outcome.stage)
|
|
446
|
+
}
|
|
436
447
|
await new Promise<void>((resolve, reject) => {
|
|
437
448
|
const finish = () => {
|
|
438
449
|
signal?.removeEventListener('abort', abort)
|
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
|
}
|
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
|
}
|
|
@@ -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)({
|