peachsim 0.1.577 → 0.1.579
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/LICENSE +1 -1
- package/README.md +11 -9
- package/cli/app-catalog.ts +77 -0
- package/cli/app-fonts.ts +4 -20
- package/cli/app-splash.ts +2 -18
- package/cli/bridge-flow-runner.ts +505 -139
- package/cli/cloud-client.ts +1 -4
- package/cli/cloud-session.ts +1 -4
- package/cli/cloud-test-result-files.ts +3 -0
- package/cli/cloud-test-tunnel.ts +2 -5
- package/cli/command-registry.ts +2 -0
- package/cli/commands/agent.ts +149 -23
- package/cli/commands/assert.ts +1 -4
- package/cli/commands/box/peach-command.ts +62 -0
- package/cli/commands/box/plane-share.ts +0 -70
- package/cli/commands/box/rnx-command.ts +1 -60
- package/cli/commands/box/search-command.ts +14 -0
- package/cli/commands/box.ts +2 -2
- package/cli/commands/control.ts +121 -27
- package/cli/commands/cpu-profile.ts +23 -10
- package/cli/commands/daemon.ts +1 -9
- package/cli/commands/detox.ts +26 -18
- package/cli/commands/device.ts +129 -1
- package/cli/commands/film.ts +25 -16
- package/cli/commands/flow.ts +9 -2
- package/cli/commands/inspect/actions.ts +75 -1
- package/cli/commands/inspect/core.ts +81 -2
- package/cli/commands/inspect/describe.ts +2 -6
- package/cli/commands/inspect/env.ts +1 -1
- package/cli/commands/inspect/settling.ts +20 -3
- package/cli/commands/inspect/shared.ts +1 -4
- package/cli/commands/inspect/sleep.ts +1 -1
- package/cli/commands/inspect.ts +16 -49
- package/cli/commands/install-desktop.ts +8 -6
- package/cli/commands/login.ts +3 -5
- package/cli/commands/maestro-generate.ts +1 -4
- package/cli/commands/maestro.ts +38 -21
- package/cli/commands/no-bridge-hint.ts +0 -14
- package/cli/commands/permissions.ts +2 -2
- package/cli/commands/platform.ts +13 -6
- package/cli/commands/screenshot.ts +6 -1
- package/cli/commands/skills.ts +1 -1
- package/cli/commands/test.ts +6 -2
- package/cli/commands/three-mode.ts +162 -19
- package/cli/commands/upload.ts +3 -18
- package/cli/current-sim.ts +0 -1
- package/cli/desktop-companion.ts +40 -8
- package/cli/drivers/playwright-provisioning.ts +1 -4
- package/cli/flow-export.ts +1 -8
- package/cli/flow-session.ts +0 -4
- package/cli/hints.ts +1 -16
- package/cli/inspect-notice-state.ts +0 -4
- package/cli/main.ts +2 -4
- package/cli/outbound-endpoints.ts +1 -24
- package/cli/parse-args.ts +1 -2
- package/cli/rnx-in-process.ts +3 -1
- package/cli/run-rnx-core.ts +4 -2
- package/cli/run-rnx.ts +18 -10
- package/cli/send-to-box.ts +1 -4
- package/cli/telemetry.ts +1 -1
- package/cli/ws-bridge.ts +1 -5
- package/detox/gestures.ts +20 -6
- package/detox/index.ts +379 -123
- package/detox/navigation-lifecycle.ts +22 -13
- package/dist-lib/agent-daemon-client.cjs +21 -15
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +3 -17
- package/dist-lib/agent-sessions.cjs +5 -4
- package/dist-lib/attached-projects.cjs +1 -77
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +11 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +7 -5
- package/dist-lib/bridge-contract-input.mjs +7 -5
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +30 -7
- package/dist-lib/capture-contract.mjs +30 -7
- package/dist-lib/cli-constants.cjs +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 +17 -11
- package/dist-lib/cloud.mjs +17 -11
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +321 -122
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +427 -212
- package/dist-lib/host/fetch-proxy-handler.cjs +5 -28
- 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 +315 -58
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +67 -31
- package/dist-lib/menu.mjs +64 -28
- package/dist-lib/metro-fingerprint-registry.cjs +8 -4
- package/dist-lib/metro-fingerprint-registry.mjs +6 -4
- package/dist-lib/metro-production-bundle.cjs +2 -2
- package/dist-lib/metro-production-bundle.mjs +2 -2
- package/dist-lib/metro.cjs +8 -4
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +7 -4
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +3 -13
- package/dist-lib/scripts/dev-server-scanner.cjs +5 -17
- package/dist-lib/sdk.cjs +129 -10
- package/dist-lib/sdk.mjs +127 -10
- package/dist-lib/skills.cjs +1635 -1121
- package/dist-lib/swift-compile.cjs +277 -70
- package/dist-lib/swift-compile.mjs +275 -70
- package/dist-lib/swift-package.cjs +17 -5
- package/dist-lib/swift-package.mjs +15 -4
- package/dist-lib/swift-submit.cjs +32 -8
- package/dist-lib/swift-submit.mjs +29 -7
- package/dist-lib/swift.cjs +30912 -577
- package/dist-lib/vite-kotlin.cjs +356 -0
- package/dist-lib/vite-swift.cjs +1743 -0
- package/dist-lib/vite.cjs +208 -1039
- package/kotlin/README.md +18 -0
- package/kotlin/Sources/androidx/annotation/Keep.kt +4 -0
- package/kotlin/Sources/dev/onejs/one/OneNativeSourceDispatch.kt +7 -0
- package/kotlin/Sources/dev/onejs/one/source/OneModule.kt +4 -0
- package/kotlin/Sources/org/json/JSON.kt +244 -0
- package/kotlin/toolchain.json +19 -0
- package/package.json +14 -2
- package/scripts/dev-server-scanner.ts +0 -18
- package/scripts/optional-demo-registry.ts +0 -17
- package/skills/contrast/SKILL.md +2 -2
- package/skills/peach-setup/SKILL.md +16 -0
- package/src/agent-daemon-client.ts +36 -22
- package/src/agent-identity.ts +0 -15
- package/src/agent-prompt.ts +12 -0
- package/src/attached-projects.ts +0 -72
- package/src/backend-origin.ts +10 -0
- package/src/bridge-contract-input.ts +2 -5
- package/src/bridge-contract.ts +2 -0
- package/src/capture-contract.ts +53 -11
- package/src/cloud.ts +4 -9
- package/src/dev-server-open.ts +0 -99
- package/src/home-paths.ts +2 -3
- package/src/host/agent-host.ts +82 -36
- package/src/host/bridge-host.ts +5 -11
- package/src/host/fetch-proxy-handler.ts +19 -43
- package/src/is-record.ts +4 -0
- package/src/menu.ts +82 -37
- package/src/metro-fingerprint-registry.ts +1 -4
- package/src/metro-production-bundle.ts +1 -1
- package/src/plist.ts +1 -4
- package/src/public-brand.ts +5 -3
- package/src/render-mode.ts +0 -12
- package/src/runtime-assets.ts +9 -2
- package/src/screenshots/compose.ts +7 -1
- package/src/screenshots/frame-compose.ts +21 -10
- package/src/screenshots/schema.ts +1 -4
- package/src/sim-client.ts +2 -4
- package/src/skills/builtin/maestro-test.ts +1 -4
- package/src/sleep.ts +4 -0
- package/src/swift-compile.ts +209 -47
- package/src/swift-icu.ts +140 -0
- package/src/swift-package.ts +26 -2
- package/src/swift-submit.ts +84 -4
- package/src/swift.ts +2 -0
- package/src/vite-plugin-kotlin.ts +376 -0
- package/src/vite-plugin-one.ts +224 -36
- package/src/vite-plugin-swift.ts +382 -96
- package/src/vite-plugin.ts +121 -134
- package/src/vite.d.ts +10 -0
- package/cli/resolve-assets.ts +0 -63
- package/src/rolldown-plugin-swift.ts +0 -244
- package/src/team-machine-prompt.ts +0 -73
package/src/swift-compile.ts
CHANGED
|
@@ -10,19 +10,41 @@ import {
|
|
|
10
10
|
statSync,
|
|
11
11
|
writeFileSync,
|
|
12
12
|
} from 'node:fs'
|
|
13
|
+
import os from 'node:os'
|
|
13
14
|
import path from 'node:path'
|
|
14
15
|
import { promisify } from 'node:util'
|
|
15
|
-
import {
|
|
16
|
+
import { swiftIcuAsset, type SwiftIcuAsset } from './swift-icu'
|
|
17
|
+
import {
|
|
18
|
+
swiftManifestTargets,
|
|
19
|
+
swiftPackagePartition,
|
|
20
|
+
type SwiftPackageTarget,
|
|
21
|
+
} from './swift-package'
|
|
16
22
|
import type { SwiftImageUpload } from './swift-submit'
|
|
17
23
|
|
|
24
|
+
export { swiftIcuAsset, type SwiftIcuAsset }
|
|
25
|
+
|
|
18
26
|
const run = promisify(execFile)
|
|
19
27
|
const WASM_TRIPLE = 'wasm32-unknown-wasip1'
|
|
20
28
|
|
|
29
|
+
// the link runs wasm-opt, which threads across every core (970% peak on 16
|
|
30
|
+
// cores for the counter fixture) and pins a shared machine for each compile.
|
|
31
|
+
// binaryen's own cap holds it to 4 threads (400%, 26 -> 32 s); swiftpm's jobs
|
|
32
|
+
// take half the cores, as the fixture build does.
|
|
33
|
+
const cores = os.availableParallelism()
|
|
34
|
+
const BUILD_ENV = { ...process.env, BINARYEN_CORES: String(Math.min(4, cores)) }
|
|
35
|
+
const BUILD_JOBS = String(Math.max(1, Math.floor(cores / 2)))
|
|
36
|
+
|
|
21
37
|
export interface SwiftCompileInput {
|
|
22
38
|
files: Record<string, string>
|
|
23
39
|
resources?: Record<string, string>
|
|
24
40
|
images?: Record<string, SwiftImageUpload>
|
|
25
41
|
entry: string
|
|
42
|
+
/**
|
|
43
|
+
* the sources are a One swift package. One's pod compiles every file with
|
|
44
|
+
* `-import-module One`, so `RNXModule`, `RNXPackage` and `JSON` resolve
|
|
45
|
+
* without an import; the compile links Peach's `One` module the same way.
|
|
46
|
+
*/
|
|
47
|
+
oneModule?: boolean
|
|
26
48
|
}
|
|
27
49
|
|
|
28
50
|
export interface SwiftCompileOptions {
|
|
@@ -36,6 +58,8 @@ export interface SwiftCompileOptions {
|
|
|
36
58
|
export interface SwiftCompileArtifact {
|
|
37
59
|
hash: string
|
|
38
60
|
bytes: Buffer
|
|
61
|
+
/** the icu common data the artifact reserves and its instantiator fills. */
|
|
62
|
+
icu: SwiftIcuAsset
|
|
39
63
|
}
|
|
40
64
|
|
|
41
65
|
export interface SwiftProjectLayout {
|
|
@@ -44,19 +68,62 @@ export interface SwiftProjectLayout {
|
|
|
44
68
|
multi: boolean
|
|
45
69
|
}
|
|
46
70
|
|
|
71
|
+
// toolchain.json is an input for its wasm sdk checksum: the sdk's icu blob,
|
|
72
|
+
// which the host fills into every artifact, is part of what the address names.
|
|
47
73
|
export function swiftModuleInputFiles(root: string): string[] {
|
|
48
|
-
const files: string[] = [
|
|
74
|
+
const files: string[] = [
|
|
75
|
+
'Package.swift',
|
|
76
|
+
'scripts/contrast-app-Package.swift',
|
|
77
|
+
'toolchain.json',
|
|
78
|
+
]
|
|
49
79
|
const walk = (dir: string) => {
|
|
50
80
|
for (const entry of readdirSync(path.join(root, dir), { withFileTypes: true })) {
|
|
51
81
|
const relative = `${dir}/${entry.name}`
|
|
52
82
|
if (entry.isDirectory()) walk(relative)
|
|
53
|
-
else if (entry.name
|
|
83
|
+
else if (/\.(swift|c|h)$/.test(entry.name)) files.push(relative)
|
|
54
84
|
}
|
|
55
85
|
}
|
|
56
86
|
walk('Sources')
|
|
57
87
|
return files.sort()
|
|
58
88
|
}
|
|
59
89
|
|
|
90
|
+
// the host halves of the module's macros: the rnx plugin (#Preview, @Entry,
|
|
91
|
+
// @Model) and #Predicate's FoundationMacros stand-in, which one script builds
|
|
92
|
+
// side by side. manifest swiftSettings cannot load a plugin library (RAN: the
|
|
93
|
+
// flag reaches the driver but the plugin never loads), so every build loads
|
|
94
|
+
// both with -Xswiftc, as scripts/build.sh does for the fixtures. their
|
|
95
|
+
// sources are under Sources/, which the artifact hash already covers.
|
|
96
|
+
async function ensureMacroPlugins(
|
|
97
|
+
moduleRoot: string,
|
|
98
|
+
toolchainBin: string,
|
|
99
|
+
): Promise<string[]> {
|
|
100
|
+
const dir = path.join(moduleRoot, '.build/host/RnxPreviewMacros')
|
|
101
|
+
// linux names a plugin's module from lib<Module>.so
|
|
102
|
+
const ext = process.platform === 'darwin' ? 'dylib' : 'so'
|
|
103
|
+
const plugins = [
|
|
104
|
+
{
|
|
105
|
+
library: path.join(dir, `libRnxPreviewMacros.${ext}`),
|
|
106
|
+
source: path.join(moduleRoot, 'Sources/RnxPreviewMacros/PreviewMacro.swift'),
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
library: path.join(dir, `libFoundationMacros.${ext}`),
|
|
110
|
+
source: path.join(moduleRoot, 'Sources/FoundationMacros/PredicateMacro.swift'),
|
|
111
|
+
},
|
|
112
|
+
]
|
|
113
|
+
const stale = plugins.some(
|
|
114
|
+
({ library, source }) =>
|
|
115
|
+
!existsSync(library) || statSync(source).mtimeMs > statSync(library).mtimeMs,
|
|
116
|
+
)
|
|
117
|
+
if (stale) {
|
|
118
|
+
await run('sh', [
|
|
119
|
+
path.join(moduleRoot, 'scripts/build-preview-plugin.sh'),
|
|
120
|
+
toolchainBin,
|
|
121
|
+
plugins[0].library,
|
|
122
|
+
])
|
|
123
|
+
}
|
|
124
|
+
return plugins.map(({ library }) => library)
|
|
125
|
+
}
|
|
126
|
+
|
|
60
127
|
const linkedModuleDigests = new Map<string, { stamp: string; digest: Buffer }>()
|
|
61
128
|
|
|
62
129
|
export function clearLinkedModuleDigests(root?: string): void {
|
|
@@ -93,7 +160,32 @@ function linkedModuleDigest(root: string): Buffer {
|
|
|
93
160
|
|
|
94
161
|
export function compileTargets(input: SwiftCompileInput): SwiftPackageTarget[] {
|
|
95
162
|
const manifest = input.files['Package.swift']
|
|
96
|
-
return manifest === undefined ? [] :
|
|
163
|
+
return manifest === undefined ? [] : swiftPackagePartition(manifest)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* the compile input with a package's generated native dispatch glue in its
|
|
168
|
+
* one target, linked against the One module the glue conforms to. vxrn's
|
|
169
|
+
* `renderSwiftSourceGlue` renders the glue; a package with native modules
|
|
170
|
+
* across several targets has no single place for it.
|
|
171
|
+
*/
|
|
172
|
+
export function withNativeSourceGlue(
|
|
173
|
+
input: SwiftCompileInput,
|
|
174
|
+
glue: string,
|
|
175
|
+
): SwiftCompileInput {
|
|
176
|
+
const targets = compileTargets(input)
|
|
177
|
+
if (targets.length > 1) {
|
|
178
|
+
throw new Error('swift native modules need one SwiftPM target')
|
|
179
|
+
}
|
|
180
|
+
const targetPath = targets[0]?.path === '.' ? '' : (targets[0]?.path ?? '')
|
|
181
|
+
return {
|
|
182
|
+
...input,
|
|
183
|
+
files: {
|
|
184
|
+
...input.files,
|
|
185
|
+
[path.posix.join(targetPath, 'OneNativeSource.generated.swift')]: glue,
|
|
186
|
+
},
|
|
187
|
+
oneModule: true,
|
|
188
|
+
}
|
|
97
189
|
}
|
|
98
190
|
|
|
99
191
|
function swiftSiblingDependencies(
|
|
@@ -107,7 +199,11 @@ function swiftSiblingDependencies(
|
|
|
107
199
|
const deps = new Set<string>()
|
|
108
200
|
for (const [file, contents] of Object.entries(files)) {
|
|
109
201
|
if (file === 'Package.swift' || !file.startsWith(prefix)) continue
|
|
110
|
-
|
|
202
|
+
// an import may carry attributes (`@_exported`, `@preconcurrency`) and a
|
|
203
|
+
// declaration kind (`import struct Nuke.ImageRequest`).
|
|
204
|
+
for (const match of contents.matchAll(
|
|
205
|
+
/^\s*(?:@\w+(?:\([^)]*\))?\s+)*import\s+(?:(?:typealias|struct|class|enum|protocol|let|var|func)\s+)?([A-Za-z_][A-Za-z0-9_]*)/gm,
|
|
206
|
+
)) {
|
|
111
207
|
if (names.has(match[1]) && match[1] !== target.name) deps.add(match[1])
|
|
112
208
|
}
|
|
113
209
|
}
|
|
@@ -116,8 +212,11 @@ function swiftSiblingDependencies(
|
|
|
116
212
|
return siblings
|
|
117
213
|
}
|
|
118
214
|
|
|
215
|
+
const ONE_PRODUCT = '.product(name: "One", package: "sootsim-swift")'
|
|
216
|
+
const ONE_IMPLICIT_FLAGS = ', "-Xfrontend", "-import-module", "-Xfrontend", "One"'
|
|
217
|
+
|
|
119
218
|
/** bump with any change to the emitted multi-target manifest text. */
|
|
120
|
-
export const MULTI_MANIFEST_VERSION =
|
|
219
|
+
export const MULTI_MANIFEST_VERSION = 5
|
|
121
220
|
|
|
122
221
|
export function swiftArtifactHash(
|
|
123
222
|
input: SwiftCompileInput,
|
|
@@ -126,6 +225,7 @@ export function swiftArtifactHash(
|
|
|
126
225
|
): string {
|
|
127
226
|
const hash = createHash('sha256')
|
|
128
227
|
hash.update(`swift-compile/v1\n${release}\n`)
|
|
228
|
+
if (input.oneModule) hash.update('one-module\n')
|
|
129
229
|
hash.update(linkedModuleDigest(moduleRoot))
|
|
130
230
|
for (const file of Object.keys(input.files).sort()) {
|
|
131
231
|
hash.update(createHash('sha256').update(file).digest())
|
|
@@ -165,15 +265,21 @@ export function emitSwiftPackageManifest(args: {
|
|
|
165
265
|
targets: SwiftPackageTarget[]
|
|
166
266
|
siblings: Map<string, string[]>
|
|
167
267
|
moduleRoot: string
|
|
268
|
+
oneModule?: boolean
|
|
168
269
|
}): string {
|
|
169
|
-
const { targets, siblings, moduleRoot } = args
|
|
270
|
+
const { targets, siblings, moduleRoot, oneModule } = args
|
|
170
271
|
const executable = targets.find((target) => target.executable)
|
|
171
272
|
if (!executable) throw new Error('swift-compile: no executable target to emit')
|
|
172
|
-
|
|
173
|
-
|
|
273
|
+
// each target compiles in the language mode and default isolation its
|
|
274
|
+
// author declared, the way xcode builds the same package.
|
|
174
275
|
const stanza = (target: SwiftPackageTarget): string => {
|
|
276
|
+
const isolation = target.mainActorIsolation
|
|
277
|
+
? ' .defaultIsolation(MainActor.self),'
|
|
278
|
+
: ''
|
|
279
|
+
const swiftSettings = `swiftSettings: [.swiftLanguageMode(.v${target.languageMode}),${isolation} .unsafeFlags(["-Osize"${oneModule ? ONE_IMPLICIT_FLAGS : ''}])]`
|
|
175
280
|
const deps = [
|
|
176
281
|
'.product(name: "SwiftUI", package: "sootsim-swift")',
|
|
282
|
+
...(oneModule ? [ONE_PRODUCT] : []),
|
|
177
283
|
...(siblings.get(target.name) ?? []).map((sibling) => `"${sibling}"`),
|
|
178
284
|
].join(', ')
|
|
179
285
|
const kind = target.executable ? '.executableTarget' : '.target'
|
|
@@ -196,8 +302,10 @@ export function emitSwiftPackageManifest(args: {
|
|
|
196
302
|
` ),`,
|
|
197
303
|
].join('\n')
|
|
198
304
|
}
|
|
305
|
+
// 6.2 is the first tools version with `.defaultIsolation`, which a target
|
|
306
|
+
// carries over from the project manifest.
|
|
199
307
|
return [
|
|
200
|
-
'// swift-tools-version: 6.
|
|
308
|
+
'// swift-tools-version: 6.2',
|
|
201
309
|
'import PackageDescription',
|
|
202
310
|
'',
|
|
203
311
|
'// generated by swift-compile from the project package partition.',
|
|
@@ -213,46 +321,74 @@ export function emitSwiftPackageManifest(args: {
|
|
|
213
321
|
].join('\n')
|
|
214
322
|
}
|
|
215
323
|
|
|
324
|
+
// the build tree mirrors the input: a file is written only when its text
|
|
325
|
+
// changed and a file the input dropped is removed, so an unchanged source
|
|
326
|
+
// keeps its mtime and swiftpm rebuilds only what moved. `.build` stays.
|
|
327
|
+
function syncSwiftTree(base: string, files: Map<string, string>): void {
|
|
328
|
+
mkdirSync(base, { recursive: true })
|
|
329
|
+
const walk = (dir: string): void => {
|
|
330
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
331
|
+
const full = path.join(dir, entry.name)
|
|
332
|
+
if (dir === base && entry.name === '.build') continue
|
|
333
|
+
if (entry.isDirectory()) walk(full)
|
|
334
|
+
else if (!files.has(full)) rmSync(full, { force: true })
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
walk(base)
|
|
338
|
+
for (const [full, contents] of files) {
|
|
339
|
+
if (existsSync(full) && readFileSync(full, 'utf8') === contents) continue
|
|
340
|
+
mkdirSync(path.dirname(full), { recursive: true })
|
|
341
|
+
writeFileSync(full, contents, 'utf8')
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
216
345
|
export function writeSwiftProject(
|
|
217
346
|
layout: SwiftProjectLayout,
|
|
218
347
|
input: SwiftCompileInput,
|
|
219
348
|
moduleRoot: string,
|
|
220
349
|
): void {
|
|
221
350
|
const targets = compileTargets(input)
|
|
351
|
+
const files = new Map<string, string>()
|
|
222
352
|
if (targets.length === 0) {
|
|
223
|
-
rmSync(layout.sources, { recursive: true, force: true })
|
|
224
353
|
for (const [file, contents] of Object.entries(input.files)) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
writeFileSync(target, contents, 'utf8')
|
|
354
|
+
if (file === 'Package.swift') continue
|
|
355
|
+
files.set(path.join(layout.sources, file), contents)
|
|
228
356
|
}
|
|
229
|
-
|
|
357
|
+
let manifest = readFileSync(
|
|
230
358
|
path.join(moduleRoot, 'scripts/contrast-app-Package.swift'),
|
|
231
359
|
'utf8',
|
|
232
360
|
).replaceAll('__SOOTSIM_SWIFT_PACKAGE__', moduleRoot)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
361
|
+
if (input.oneModule) {
|
|
362
|
+
// anchored on lines the template already has, so the template stays a
|
|
363
|
+
// valid manifest for a compiler build that predates the One module
|
|
364
|
+
const product = '.product(name: "CoreLocation", package: "sootsim-swift"),'
|
|
365
|
+
const flags = '"-Osize"'
|
|
366
|
+
if (!manifest.includes(product) || !manifest.includes(flags)) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
'scripts/contrast-app-Package.swift lost the lines the One module links at',
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
manifest = manifest
|
|
372
|
+
.replace(product, `${product}\n ${ONE_PRODUCT},`)
|
|
373
|
+
.replace(flags, `${flags}${ONE_IMPLICIT_FLAGS}`)
|
|
374
|
+
}
|
|
375
|
+
files.set(path.join(layout.root, 'Package.swift'), manifest)
|
|
376
|
+
} else {
|
|
377
|
+
for (const [file, contents] of Object.entries(input.files)) {
|
|
378
|
+
if (file === 'Package.swift') continue
|
|
379
|
+
files.set(path.join(layout.root, file), contents)
|
|
380
|
+
}
|
|
381
|
+
files.set(
|
|
382
|
+
path.join(layout.root, 'Package.swift'),
|
|
383
|
+
emitSwiftPackageManifest({
|
|
384
|
+
targets,
|
|
385
|
+
siblings: swiftSiblingDependencies(input.files, targets),
|
|
386
|
+
moduleRoot,
|
|
387
|
+
oneModule: input.oneModule,
|
|
388
|
+
}),
|
|
389
|
+
)
|
|
246
390
|
}
|
|
247
|
-
|
|
248
|
-
path.join(layout.root, 'Package.swift'),
|
|
249
|
-
emitSwiftPackageManifest({
|
|
250
|
-
targets,
|
|
251
|
-
siblings: swiftSiblingDependencies(input.files, targets),
|
|
252
|
-
moduleRoot,
|
|
253
|
-
}),
|
|
254
|
-
'utf8',
|
|
255
|
-
)
|
|
391
|
+
syncSwiftTree(layout.root, files)
|
|
256
392
|
}
|
|
257
393
|
|
|
258
394
|
function streamText(value: unknown): string {
|
|
@@ -274,17 +410,27 @@ function outputOf(error: unknown): string {
|
|
|
274
410
|
|
|
275
411
|
function builtArtifact(layout: SwiftProjectLayout): string {
|
|
276
412
|
const binDir = path.join(layout.root, '.build', WASM_TRIPLE, 'release')
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
413
|
+
// the build root is shared across projects so the SwiftUI module stays
|
|
414
|
+
// warm, and every project's executable lands beside the others. the
|
|
415
|
+
// written manifest names this build's executable, so select it by name
|
|
416
|
+
// instead of requiring the directory to hold exactly one artifact.
|
|
417
|
+
const manifest = readFileSync(path.join(layout.root, 'Package.swift'), 'utf8')
|
|
418
|
+
const executable = swiftManifestTargets(manifest).find((target) => target.executable)
|
|
419
|
+
if (executable === undefined) {
|
|
420
|
+
throw new Error('swift-compile: the written manifest names no executable target')
|
|
421
|
+
}
|
|
422
|
+
const file = path.join(binDir, `${executable.name}.wasm`)
|
|
423
|
+
if (!existsSync(file)) {
|
|
424
|
+
const found = existsSync(binDir)
|
|
425
|
+
? readdirSync(binDir)
|
|
426
|
+
.filter((name) => name.endsWith('.wasm'))
|
|
427
|
+
.sort()
|
|
428
|
+
: []
|
|
283
429
|
throw new Error(
|
|
284
|
-
`swift-compile: the build produced ${
|
|
430
|
+
`swift-compile: the build produced no ${executable.name}.wasm (found: ${found.join(', ') || 'none'})`,
|
|
285
431
|
)
|
|
286
432
|
}
|
|
287
|
-
return
|
|
433
|
+
return file
|
|
288
434
|
}
|
|
289
435
|
|
|
290
436
|
function projectDiagnostic(output: string, layout: SwiftProjectLayout): string {
|
|
@@ -335,11 +481,17 @@ export function compileSwiftProjectUnit(
|
|
|
335
481
|
`swift-compile: no swift.org toolchain at ${options.toolchainBin}; install ${options.release} and its matching wasm sdk`,
|
|
336
482
|
)
|
|
337
483
|
}
|
|
484
|
+
const macroPlugins = await ensureMacroPlugins(moduleRoot, options.toolchainBin)
|
|
485
|
+
// memoized per toolchain, so only a process's first build waits on it,
|
|
486
|
+
// and that one reads and gzips it while swift builds
|
|
487
|
+
const icu = swiftIcuAsset(options.toolchainBin, options.release)
|
|
338
488
|
try {
|
|
339
489
|
await run(
|
|
340
490
|
swift,
|
|
341
491
|
[
|
|
342
492
|
'build',
|
|
493
|
+
'-j',
|
|
494
|
+
BUILD_JOBS,
|
|
343
495
|
'--package-path',
|
|
344
496
|
root,
|
|
345
497
|
'--swift-sdk',
|
|
@@ -348,12 +500,22 @@ export function compileSwiftProjectUnit(
|
|
|
348
500
|
'release',
|
|
349
501
|
'-Xswiftc',
|
|
350
502
|
'-Osize',
|
|
503
|
+
...macroPlugins.flatMap((plugin) => [
|
|
504
|
+
'-Xswiftc',
|
|
505
|
+
'-load-plugin-library',
|
|
506
|
+
'-Xswiftc',
|
|
507
|
+
plugin,
|
|
508
|
+
]),
|
|
351
509
|
],
|
|
352
|
-
{ maxBuffer: 64 * 1024 * 1024, timeout: options.timeoutMs },
|
|
510
|
+
{ env: BUILD_ENV, maxBuffer: 64 * 1024 * 1024, timeout: options.timeoutMs },
|
|
353
511
|
)
|
|
354
512
|
} catch (error) {
|
|
355
513
|
throw new Error(projectDiagnostic(outputOf(error), layout), { cause: error })
|
|
356
514
|
}
|
|
357
|
-
return {
|
|
515
|
+
return {
|
|
516
|
+
hash,
|
|
517
|
+
bytes: readFileSync(builtArtifact(layout)),
|
|
518
|
+
icu: await icu,
|
|
519
|
+
}
|
|
358
520
|
})
|
|
359
521
|
}
|
package/src/swift-icu.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
6
|
+
import { gzip } from 'node:zlib'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* the icu common data a swift artifact reserves and its host fills
|
|
10
|
+
* (packages/sootsim-swift/Sources/PeachICU/icu.c). it is read straight from
|
|
11
|
+
* the wasm sdk the artifact links against, the archive member the link now
|
|
12
|
+
* leaves out, so it is exactly the bytes the linker used to place: one blob
|
|
13
|
+
* per toolchain release.
|
|
14
|
+
*
|
|
15
|
+
* node builtins only: the fixture harnesses import this file straight from
|
|
16
|
+
* the checkout.
|
|
17
|
+
*/
|
|
18
|
+
export interface SwiftIcuAsset {
|
|
19
|
+
/** sha256 of `gzip`, the address a host serves it at. */
|
|
20
|
+
sha256: string
|
|
21
|
+
/**
|
|
22
|
+
* the blob gzipped, which is how every host stores and serves it: 34 MB
|
|
23
|
+
* raw is 13 MB gzipped, and cloudflare compresses no binary type on its
|
|
24
|
+
* own. the tenant inflates it with DecompressionStream.
|
|
25
|
+
*/
|
|
26
|
+
gzip: Buffer
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const run = promisify(execFile)
|
|
30
|
+
const deflate = promisify(gzip)
|
|
31
|
+
const ARCHIVE = 'wasi/lib_FoundationICU.a'
|
|
32
|
+
const MEMBER = 'icu_packaged_data.cpp.obj'
|
|
33
|
+
const raw = new Map<string, Promise<Buffer>>()
|
|
34
|
+
const assets = new Map<string, Promise<SwiftIcuAsset>>()
|
|
35
|
+
|
|
36
|
+
function memo<T>(
|
|
37
|
+
cache: Map<string, Promise<T>>,
|
|
38
|
+
key: string,
|
|
39
|
+
load: () => Promise<T>,
|
|
40
|
+
): Promise<T> {
|
|
41
|
+
let pending = cache.get(key)
|
|
42
|
+
if (!pending) {
|
|
43
|
+
pending = load()
|
|
44
|
+
pending.catch(() => cache.delete(key))
|
|
45
|
+
cache.set(key, pending)
|
|
46
|
+
}
|
|
47
|
+
return pending
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** the raw blob, for a harness that fills an artifact in node. */
|
|
51
|
+
export function swiftIcuData(toolchainBin: string, release: string): Promise<Buffer> {
|
|
52
|
+
return memo(raw, `${toolchainBin}\0${release}`, () => load(toolchainBin, release))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** the served form: gzipped and content-addressed. */
|
|
56
|
+
export function swiftIcuAsset(
|
|
57
|
+
toolchainBin: string,
|
|
58
|
+
release: string,
|
|
59
|
+
): Promise<SwiftIcuAsset> {
|
|
60
|
+
return memo(assets, `${toolchainBin}\0${release}`, async () => {
|
|
61
|
+
const zipped = await deflate(await swiftIcuData(toolchainBin, release))
|
|
62
|
+
return { sha256: createHash('sha256').update(zipped).digest('hex'), gzip: zipped }
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function load(toolchainBin: string, release: string): Promise<Buffer> {
|
|
67
|
+
const { stdout } = await run(path.join(toolchainBin, 'swift'), [
|
|
68
|
+
'sdk',
|
|
69
|
+
'configure',
|
|
70
|
+
'--show-configuration',
|
|
71
|
+
`${release}_wasm`,
|
|
72
|
+
'wasm32-unknown-wasip1',
|
|
73
|
+
])
|
|
74
|
+
const resources = /^swiftStaticResourcesPath: (.+)$/m.exec(stdout)?.[1]?.trim()
|
|
75
|
+
if (!resources) throw new Error(`swift-icu: ${release}_wasm names no static resources`)
|
|
76
|
+
// a copy, so the 42 MB archive it was sliced from can be collected
|
|
77
|
+
return Buffer.from(
|
|
78
|
+
icuSegment(archiveMember(readFileSync(path.join(resources, ARCHIVE)), MEMBER)),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// a gnu ar archive: 60-byte headers, names over 15 characters live in the
|
|
83
|
+
// `//` table and the header says `/<offset>`, members pad to even offsets.
|
|
84
|
+
function archiveMember(archive: Buffer, name: string): Buffer {
|
|
85
|
+
if (archive.toString('latin1', 0, 8) !== '!<arch>\n')
|
|
86
|
+
throw new Error('swift-icu: not an ar archive')
|
|
87
|
+
let names = ''
|
|
88
|
+
for (let at = 8; at + 60 <= archive.length; ) {
|
|
89
|
+
const raw = archive.toString('latin1', at, at + 16).trimEnd()
|
|
90
|
+
const size = Number(archive.toString('latin1', at + 48, at + 58).trim())
|
|
91
|
+
const body = archive.subarray(at + 60, at + 60 + size)
|
|
92
|
+
if (raw === '//') names = body.toString('latin1')
|
|
93
|
+
const member = /^\/\d+$/.test(raw)
|
|
94
|
+
? names.slice(Number(raw.slice(1)), names.indexOf('/\n', Number(raw.slice(1))))
|
|
95
|
+
: raw.replace(/\/$/, '')
|
|
96
|
+
if (member === name) return body
|
|
97
|
+
at += 60 + size + (size % 2)
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`swift-icu: ${name} is not in the sdk's icu archive`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// the relocatable object's one data segment whose bytes open with icu's
|
|
103
|
+
// data-header magic (0xda27): the packaged data carries no relocations, so
|
|
104
|
+
// the segment is the blob.
|
|
105
|
+
function icuSegment(object: Buffer): Buffer {
|
|
106
|
+
let at = 8
|
|
107
|
+
const leb = (): number => {
|
|
108
|
+
let result = 0
|
|
109
|
+
let shift = 0
|
|
110
|
+
let byte: number
|
|
111
|
+
do {
|
|
112
|
+
byte = object[at++]
|
|
113
|
+
result += (byte & 0x7f) * 2 ** shift
|
|
114
|
+
shift += 7
|
|
115
|
+
} while (byte & 0x80)
|
|
116
|
+
return result
|
|
117
|
+
}
|
|
118
|
+
const found: Buffer[] = []
|
|
119
|
+
while (at < object.length) {
|
|
120
|
+
const id = object[at++]
|
|
121
|
+
const size = leb()
|
|
122
|
+
const end = at + size
|
|
123
|
+
if (id === 11) {
|
|
124
|
+
for (let count = leb(); count > 0; count--) {
|
|
125
|
+
if (leb() !== 0) throw new Error('swift-icu: unexpected data segment kind')
|
|
126
|
+
at++ // i32.const
|
|
127
|
+
leb()
|
|
128
|
+
at++ // end
|
|
129
|
+
const length = leb()
|
|
130
|
+
const bytes = object.subarray(at, at + length)
|
|
131
|
+
if (bytes[2] === 0xda && bytes[3] === 0x27) found.push(bytes)
|
|
132
|
+
at += length
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
at = end
|
|
136
|
+
}
|
|
137
|
+
if (found.length !== 1)
|
|
138
|
+
throw new Error(`swift-icu: expected one icu data segment, found ${found.length}`)
|
|
139
|
+
return found[0]
|
|
140
|
+
}
|
package/src/swift-package.ts
CHANGED
|
@@ -6,6 +6,10 @@ export interface SwiftPackageTarget {
|
|
|
6
6
|
path: string
|
|
7
7
|
/** true for `.executableTarget`: the one that links the wasm artifact. */
|
|
8
8
|
executable: boolean
|
|
9
|
+
/** the target's declared `.swiftLanguageMode(.vN)`; 5 when it declares none. */
|
|
10
|
+
languageMode: number
|
|
11
|
+
/** true when the target declares `.defaultIsolation(MainActor.self)`. */
|
|
12
|
+
mainActorIsolation: boolean
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
/** whether a project-relative key is a SwiftPM manifest, not a source. */
|
|
@@ -31,7 +35,8 @@ export function swiftManifestTargets(manifest: string): SwiftPackageTarget[] {
|
|
|
31
35
|
while ((match = pattern.exec(code)) !== null) {
|
|
32
36
|
const executable = match[1] === 'executableTarget'
|
|
33
37
|
let depth = 1
|
|
34
|
-
|
|
38
|
+
const start = pattern.lastIndex
|
|
39
|
+
let i = start
|
|
35
40
|
let inString = false
|
|
36
41
|
let escaped = false
|
|
37
42
|
let flat = ''
|
|
@@ -63,7 +68,26 @@ export function swiftManifestTargets(manifest: string): SwiftPackageTarget[] {
|
|
|
63
68
|
rawPath == null || rawPath === ''
|
|
64
69
|
? `Sources/${name}`
|
|
65
70
|
: rawPath.replace(/^\.\//, '').replace(/\/+$/, '')
|
|
66
|
-
|
|
71
|
+
// settings sit inside nested calls, which `flat` drops, so they are read
|
|
72
|
+
// from the call's whole text; a dependency list cannot spell either one.
|
|
73
|
+
const call = code.slice(start, i)
|
|
74
|
+
const languageMode = Number(
|
|
75
|
+
/\.swiftLanguageMode\(\s*\.v(\d+)\s*\)/.exec(call)?.[1] ?? '5',
|
|
76
|
+
)
|
|
77
|
+
const mainActorIsolation = /\.defaultIsolation\(\s*MainActor\.self\s*\)/.test(call)
|
|
78
|
+
targets.push({ name, path: targetPath, executable, languageMode, mainActorIsolation })
|
|
67
79
|
}
|
|
68
80
|
return targets
|
|
69
81
|
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* the target partition a root manifest declares for the compile. a single
|
|
85
|
+
* executable target at the package root (`path: "."`) holds the whole project,
|
|
86
|
+
* which is the compiler's single-target shape, so it declares no partition.
|
|
87
|
+
*/
|
|
88
|
+
export function swiftPackagePartition(manifest: string): SwiftPackageTarget[] {
|
|
89
|
+
const targets = swiftManifestTargets(manifest)
|
|
90
|
+
return targets.length === 1 && targets[0].executable && targets[0].path === '.'
|
|
91
|
+
? []
|
|
92
|
+
: targets
|
|
93
|
+
}
|