peachsim 0.1.570 → 0.1.571
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 +2 -2
- package/cli/bridge-flow-runner.ts +15 -7
- package/cli/cloud-dispatch.ts +1 -0
- package/cli/cloud-test-result-files.ts +97 -0
- package/cli/commands/maestro.ts +123 -31
- package/cli/commands/test.ts +161 -3
- package/cli/maestro-report.ts +3 -1
- package/cli/outbound-endpoints.ts +2 -0
- package/cli/run-registry.ts +2 -1
- package/detox/jest-preset.cjs +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.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 +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- 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 +116 -0
- package/dist-lib/cloud-test-args.mjs +86 -0
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1 -1
- package/dist-lib/detox/jest-preset.cjs +2 -2
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +16 -7
- 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 +16 -26
- 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 +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 +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +119 -106
- package/dist-lib/swift-compile.cjs +372 -0
- package/dist-lib/swift-compile.mjs +339 -0
- package/dist-lib/swift-package.cjs +78 -0
- package/dist-lib/swift-package.mjs +52 -0
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite.cjs +456 -138
- package/package.json +22 -1
- package/skills/rnx-test/SKILL.md +3 -3
- package/src/cloud-test-args.ts +100 -0
- package/src/rolldown-plugin-swift.ts +8 -7
- package/src/swift-compile.ts +359 -0
- package/src/swift-package.ts +69 -0
- package/src/swift-submit.ts +3 -2
- package/src/vite-plugin-swift.ts +103 -107
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peachsim",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.571",
|
|
4
4
|
"description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
|
|
5
5
|
"author": "Tamagui LLC",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -46,6 +46,20 @@
|
|
|
46
46
|
"import": "./dist-lib/swift-submit.mjs",
|
|
47
47
|
"default": "./dist-lib/swift-submit.cjs"
|
|
48
48
|
},
|
|
49
|
+
"./swift-package": {
|
|
50
|
+
"types": "./src/swift-package.ts",
|
|
51
|
+
"source": "./src/swift-package.ts",
|
|
52
|
+
"bun": "./src/swift-package.ts",
|
|
53
|
+
"import": "./dist-lib/swift-package.mjs",
|
|
54
|
+
"default": "./dist-lib/swift-package.cjs"
|
|
55
|
+
},
|
|
56
|
+
"./swift-compile": {
|
|
57
|
+
"types": "./src/swift-compile.ts",
|
|
58
|
+
"source": "./src/swift-compile.ts",
|
|
59
|
+
"bun": "./src/swift-compile.ts",
|
|
60
|
+
"import": "./dist-lib/swift-compile.mjs",
|
|
61
|
+
"default": "./dist-lib/swift-compile.cjs"
|
|
62
|
+
},
|
|
49
63
|
"./metro": {
|
|
50
64
|
"source": "./src/metro-plugin.ts",
|
|
51
65
|
"default": "./dist-lib/metro.cjs"
|
|
@@ -150,6 +164,13 @@
|
|
|
150
164
|
"import": "./dist-lib/cloud-contract.mjs",
|
|
151
165
|
"default": "./dist-lib/cloud-contract.cjs"
|
|
152
166
|
},
|
|
167
|
+
"./cloud-test-args": {
|
|
168
|
+
"types": "./src/cloud-test-args.ts",
|
|
169
|
+
"source": "./src/cloud-test-args.ts",
|
|
170
|
+
"bun": "./src/cloud-test-args.ts",
|
|
171
|
+
"import": "./dist-lib/cloud-test-args.mjs",
|
|
172
|
+
"default": "./dist-lib/cloud-test-args.cjs"
|
|
173
|
+
},
|
|
153
174
|
"./cloud": {
|
|
154
175
|
"types": "./src/cloud.ts",
|
|
155
176
|
"source": "./src/cloud.ts",
|
package/skills/rnx-test/SKILL.md
CHANGED
|
@@ -238,7 +238,7 @@ upstream detox surface.
|
|
|
238
238
|
```js
|
|
239
239
|
// rnx-detox.config.cjs
|
|
240
240
|
module.exports = {
|
|
241
|
-
preset: '
|
|
241
|
+
preset: 'peachsim/detox',
|
|
242
242
|
rootDir: __dirname,
|
|
243
243
|
testMatch: ['<rootDir>/e2e/**/*.test.ts'],
|
|
244
244
|
}
|
|
@@ -248,7 +248,7 @@ or, if you already have a jest config, pull in just the module mapper:
|
|
|
248
248
|
|
|
249
249
|
```js
|
|
250
250
|
moduleNameMapper: {
|
|
251
|
-
'^detox$': require.resolve('
|
|
251
|
+
'^detox$': require.resolve('peachsim/detox'),
|
|
252
252
|
}
|
|
253
253
|
```
|
|
254
254
|
|
|
@@ -294,7 +294,7 @@ rnx detox init # scaffold config + e2e/example.test.ts
|
|
|
294
294
|
- **"no Peach shell reachable"** — start one yourself
|
|
295
295
|
(`rnx open <port>`) or pass `--port <n>`.
|
|
296
296
|
- **"jest cannot find module 'detox'"** — the preset isn't wired. confirm
|
|
297
|
-
`preset: '
|
|
297
|
+
`preset: 'peachsim/detox'` is in the jest config jest
|
|
298
298
|
actually loaded (`--config` overrides package.json).
|
|
299
299
|
- **matchers fail on elements that exist** — canvas has no DOM. use
|
|
300
300
|
`by.id` (testID); `rnx describe` shows what the shell sees.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// hosted `rnx test` selection args: the argv slice a cloud submitter
|
|
2
|
+
// forwards and the recorder appends verbatim to its executor invocation.
|
|
3
|
+
//
|
|
4
|
+
// selection only. the flags below choose WHICH tests run; everything
|
|
5
|
+
// about HOW they run (workers, timeouts, shards, devices, environment)
|
|
6
|
+
// stays with the recorder that owns the execution environment, and
|
|
7
|
+
// everything about WHERE they run (app, origin, tunnel) is consumed by
|
|
8
|
+
// the submitter. a flag outside this list never reaches the executor:
|
|
9
|
+
// the submitter warns and the server refuses, so a filter is never
|
|
10
|
+
// silently dropped. pure: the gpu recorder daemon imports this too, so
|
|
11
|
+
// no ~/ aliases, no server-only, no node builtins.
|
|
12
|
+
export const CLOUD_TEST_ARGS_MAX_COUNT = 32
|
|
13
|
+
export const CLOUD_TEST_ARG_MAX_LENGTH = 1000
|
|
14
|
+
export const CLOUD_TEST_ARGS_MAX_BYTES = 16 * 1024
|
|
15
|
+
|
|
16
|
+
// every flag a hosted job honors from the submitting command line. each
|
|
17
|
+
// takes exactly one value, as a separate token or --flag=value.
|
|
18
|
+
export const CLOUD_TEST_VALUE_FLAGS: readonly string[] = [
|
|
19
|
+
'-t',
|
|
20
|
+
'--testNamePattern',
|
|
21
|
+
'--testPathPattern',
|
|
22
|
+
'--include-tags',
|
|
23
|
+
'--exclude-tags',
|
|
24
|
+
'--grep',
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const CLOUD_TEST_VALUE_FLAG_SET = new Set<string>(CLOUD_TEST_VALUE_FLAGS)
|
|
28
|
+
|
|
29
|
+
export function isCloudTestValueFlag(name: string): boolean {
|
|
30
|
+
return CLOUD_TEST_VALUE_FLAG_SET.has(name)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type CloudTestArgsVerdict =
|
|
34
|
+
| { ok: true; args: string[] }
|
|
35
|
+
| { ok: false; message: string }
|
|
36
|
+
|
|
37
|
+
// the submit payload's testArgs field, or null when the submission
|
|
38
|
+
// carries no selection. every violation names the offending arg.
|
|
39
|
+
export function parseCloudTestArgs(value: unknown): CloudTestArgsVerdict {
|
|
40
|
+
if (value === undefined || value === null) return { ok: true, args: [] }
|
|
41
|
+
if (!Array.isArray(value)) {
|
|
42
|
+
return { ok: false, message: 'testArgs must be an array of strings' }
|
|
43
|
+
}
|
|
44
|
+
if (value.length > CLOUD_TEST_ARGS_MAX_COUNT) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
message: `testArgs may contain at most ${CLOUD_TEST_ARGS_MAX_COUNT} entries`,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// utf-8 bytes, not utf-16 code units: the bound names bytes, and a
|
|
51
|
+
// multibyte pattern must not slip twice its size past the gate.
|
|
52
|
+
const encoder = new TextEncoder()
|
|
53
|
+
let bytes = 0
|
|
54
|
+
const entries: string[] = []
|
|
55
|
+
for (const entry of value) {
|
|
56
|
+
if (typeof entry !== 'string' || entry.length === 0) {
|
|
57
|
+
return { ok: false, message: 'testArgs entries must be non-empty strings' }
|
|
58
|
+
}
|
|
59
|
+
if (entry.length > CLOUD_TEST_ARG_MAX_LENGTH || entry.includes('\0')) {
|
|
60
|
+
return { ok: false, message: `testArgs entry is too long or carries a NUL byte: ${entry.slice(0, 60)}` }
|
|
61
|
+
}
|
|
62
|
+
bytes += encoder.encode(entry).length
|
|
63
|
+
if (bytes > CLOUD_TEST_ARGS_MAX_BYTES) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
message: `testArgs may contain at most ${CLOUD_TEST_ARGS_MAX_BYTES} bytes`,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
entries.push(entry)
|
|
70
|
+
}
|
|
71
|
+
const args: string[] = []
|
|
72
|
+
for (let i = 0; i < entries.length; i++) {
|
|
73
|
+
const entry = entries[i]
|
|
74
|
+
if (!entry.startsWith('-')) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
message: `testArgs carries a positional argument (selection flags only): ${entry.slice(0, 60)}`,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const eq = entry.indexOf('=')
|
|
81
|
+
const head = eq > 0 ? entry.slice(0, eq) : entry
|
|
82
|
+
if (!isCloudTestValueFlag(head)) {
|
|
83
|
+
return { ok: false, message: `testArgs carries an unknown flag: ${head.slice(0, 60)}` }
|
|
84
|
+
}
|
|
85
|
+
if (eq > 0) {
|
|
86
|
+
if (!entry.slice(eq + 1)) {
|
|
87
|
+
return { ok: false, message: `${head} expects a value` }
|
|
88
|
+
}
|
|
89
|
+
args.push(entry)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
const val = entries[i + 1]
|
|
93
|
+
if (val === undefined || val.startsWith('-')) {
|
|
94
|
+
return { ok: false, message: `${head} expects a value` }
|
|
95
|
+
}
|
|
96
|
+
args.push(entry, val)
|
|
97
|
+
i += 1
|
|
98
|
+
}
|
|
99
|
+
return { ok: true, args }
|
|
100
|
+
}
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
// swiftpm package to `/api/swift-compile` and answer every `.swift` import
|
|
3
3
|
// with the module that mounts the published artifact.
|
|
4
4
|
//
|
|
5
|
-
// this is the remote twin of `vite-plugin-swift.ts`. that plugin
|
|
6
|
-
//
|
|
7
|
-
// native engine is its own rolldown instance with no
|
|
8
|
-
// this plugin compiles nothing in-process and serves
|
|
9
|
-
// url the service answers is absolute (compile-cdn), so
|
|
10
|
-
// serve. same compiler, same store, same hash, same
|
|
11
|
-
// browser worker — one path, no second toolchain to install
|
|
5
|
+
// this is the remote twin of `vite-plugin-swift.ts`. that plugin calls the
|
|
6
|
+
// shared project-unit compiler and serves the artifact from dev-server
|
|
7
|
+
// middleware. the native engine is its own rolldown instance with no
|
|
8
|
+
// middleware seam, so this plugin compiles nothing in-process and serves
|
|
9
|
+
// nothing. the artifact url the service answers is absolute (compile-cdn), so
|
|
10
|
+
// there is nothing to serve. same compiler, same store, same hash, same
|
|
11
|
+
// credential as the browser worker — one path, no second toolchain to install
|
|
12
|
+
// for `one dev`.
|
|
12
13
|
//
|
|
13
14
|
// an app registers it through One's native plugin seam, not `config.plugins`
|
|
14
15
|
// (which never reach the native engine):
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
realpathSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
statSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { promisify } from 'node:util'
|
|
15
|
+
import { swiftManifestTargets, type SwiftPackageTarget } from './swift-package'
|
|
16
|
+
import type { SwiftImageUpload } from './swift-submit'
|
|
17
|
+
|
|
18
|
+
const run = promisify(execFile)
|
|
19
|
+
const WASM_TRIPLE = 'wasm32-unknown-wasip1'
|
|
20
|
+
|
|
21
|
+
export interface SwiftCompileInput {
|
|
22
|
+
files: Record<string, string>
|
|
23
|
+
resources?: Record<string, string>
|
|
24
|
+
images?: Record<string, SwiftImageUpload>
|
|
25
|
+
entry: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SwiftCompileOptions {
|
|
29
|
+
buildRoot: string
|
|
30
|
+
moduleRoot: string
|
|
31
|
+
release: string
|
|
32
|
+
toolchainBin: string
|
|
33
|
+
timeoutMs?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SwiftCompileArtifact {
|
|
37
|
+
hash: string
|
|
38
|
+
bytes: Buffer
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SwiftProjectLayout {
|
|
42
|
+
root: string
|
|
43
|
+
sources: string
|
|
44
|
+
multi: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function swiftModuleInputFiles(root: string): string[] {
|
|
48
|
+
const files: string[] = ['Package.swift', 'scripts/contrast-app-Package.swift']
|
|
49
|
+
const walk = (dir: string) => {
|
|
50
|
+
for (const entry of readdirSync(path.join(root, dir), { withFileTypes: true })) {
|
|
51
|
+
const relative = `${dir}/${entry.name}`
|
|
52
|
+
if (entry.isDirectory()) walk(relative)
|
|
53
|
+
else if (entry.name.endsWith('.swift')) files.push(relative)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
walk('Sources')
|
|
57
|
+
return files.sort()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const linkedModuleDigests = new Map<string, { stamp: string; digest: Buffer }>()
|
|
61
|
+
|
|
62
|
+
export function clearLinkedModuleDigests(root?: string): void {
|
|
63
|
+
if (root === undefined) linkedModuleDigests.clear()
|
|
64
|
+
else linkedModuleDigests.delete(root)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function linkedModuleDigest(root: string): Buffer {
|
|
68
|
+
const files = swiftModuleInputFiles(root)
|
|
69
|
+
const stampHash = createHash('sha256')
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const stat = statSync(path.join(root, file), { bigint: true })
|
|
72
|
+
stampHash.update(file)
|
|
73
|
+
stampHash.update(
|
|
74
|
+
`${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}\n`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
const stamp = stampHash.digest('hex')
|
|
78
|
+
const cached = linkedModuleDigests.get(root)
|
|
79
|
+
if (cached?.stamp === stamp) return cached.digest
|
|
80
|
+
const hash = createHash('sha256')
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
hash.update(createHash('sha256').update(file).digest())
|
|
83
|
+
hash.update(
|
|
84
|
+
createHash('sha256')
|
|
85
|
+
.update(readFileSync(path.join(root, file)))
|
|
86
|
+
.digest(),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
const digest = hash.digest()
|
|
90
|
+
linkedModuleDigests.set(root, { stamp, digest })
|
|
91
|
+
return digest
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function compileTargets(input: SwiftCompileInput): SwiftPackageTarget[] {
|
|
95
|
+
const manifest = input.files['Package.swift']
|
|
96
|
+
return manifest === undefined ? [] : swiftManifestTargets(manifest)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function swiftSiblingDependencies(
|
|
100
|
+
files: Record<string, string>,
|
|
101
|
+
targets: SwiftPackageTarget[],
|
|
102
|
+
): Map<string, string[]> {
|
|
103
|
+
const names = new Set(targets.map((target) => target.name))
|
|
104
|
+
const siblings = new Map<string, string[]>()
|
|
105
|
+
for (const target of targets) {
|
|
106
|
+
const prefix = `${target.path}/`
|
|
107
|
+
const deps = new Set<string>()
|
|
108
|
+
for (const [file, contents] of Object.entries(files)) {
|
|
109
|
+
if (file === 'Package.swift' || !file.startsWith(prefix)) continue
|
|
110
|
+
for (const match of contents.matchAll(/^\s*import\s+([A-Za-z_][A-Za-z0-9_]*)/gm)) {
|
|
111
|
+
if (names.has(match[1]) && match[1] !== target.name) deps.add(match[1])
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
siblings.set(target.name, [...deps].sort())
|
|
115
|
+
}
|
|
116
|
+
return siblings
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** bump with any change to the emitted multi-target manifest text. */
|
|
120
|
+
export const MULTI_MANIFEST_VERSION = 1
|
|
121
|
+
|
|
122
|
+
export function swiftArtifactHash(
|
|
123
|
+
input: SwiftCompileInput,
|
|
124
|
+
release: string,
|
|
125
|
+
moduleRoot: string,
|
|
126
|
+
): string {
|
|
127
|
+
const hash = createHash('sha256')
|
|
128
|
+
hash.update(`swift-compile/v1\n${release}\n`)
|
|
129
|
+
hash.update(linkedModuleDigest(moduleRoot))
|
|
130
|
+
for (const file of Object.keys(input.files).sort()) {
|
|
131
|
+
hash.update(createHash('sha256').update(file).digest())
|
|
132
|
+
hash.update(createHash('sha256').update(input.files[file]).digest())
|
|
133
|
+
}
|
|
134
|
+
for (const base of Object.keys(input.resources ?? {}).sort()) {
|
|
135
|
+
hash.update(createHash('sha256').update(base).digest())
|
|
136
|
+
hash.update(
|
|
137
|
+
createHash('sha256')
|
|
138
|
+
.update(input.resources?.[base] ?? '')
|
|
139
|
+
.digest(),
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
for (const name of Object.keys(input.images ?? {}).sort()) {
|
|
143
|
+
const image = input.images?.[name]
|
|
144
|
+
if (!image) continue
|
|
145
|
+
hash.update(createHash('sha256').update(name).digest())
|
|
146
|
+
hash.update(createHash('sha256').update(image.file).digest())
|
|
147
|
+
hash.update(createHash('sha256').update(image.data).digest())
|
|
148
|
+
}
|
|
149
|
+
const targets = compileTargets(input)
|
|
150
|
+
if (targets.length > 0) {
|
|
151
|
+
hash.update(`swift-multi/v${MULTI_MANIFEST_VERSION}\n`)
|
|
152
|
+
hash.update(
|
|
153
|
+
JSON.stringify({
|
|
154
|
+
targets,
|
|
155
|
+
siblings: [...swiftSiblingDependencies(input.files, targets)].sort((a, b) =>
|
|
156
|
+
String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0,
|
|
157
|
+
),
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
return hash.digest('hex')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function emitSwiftPackageManifest(args: {
|
|
165
|
+
targets: SwiftPackageTarget[]
|
|
166
|
+
siblings: Map<string, string[]>
|
|
167
|
+
moduleRoot: string
|
|
168
|
+
}): string {
|
|
169
|
+
const { targets, siblings, moduleRoot } = args
|
|
170
|
+
const executable = targets.find((target) => target.executable)
|
|
171
|
+
if (!executable) throw new Error('swift-compile: no executable target to emit')
|
|
172
|
+
const swiftSettings =
|
|
173
|
+
'swiftSettings: [.swiftLanguageMode(.v5), .unsafeFlags(["-Osize"])]'
|
|
174
|
+
const stanza = (target: SwiftPackageTarget): string => {
|
|
175
|
+
const deps = [
|
|
176
|
+
'.product(name: "SwiftUI", package: "sootsim-swift")',
|
|
177
|
+
...(siblings.get(target.name) ?? []).map((sibling) => `"${sibling}"`),
|
|
178
|
+
].join(', ')
|
|
179
|
+
const kind = target.executable ? '.executableTarget' : '.target'
|
|
180
|
+
const link = target.executable
|
|
181
|
+
? `,
|
|
182
|
+
linkerSettings: [
|
|
183
|
+
.unsafeFlags([
|
|
184
|
+
"-Xclang-linker", "-mexec-model=reactor",
|
|
185
|
+
"-Xlinker", "--export-if-defined=__main_argc_argv",
|
|
186
|
+
]),
|
|
187
|
+
.unsafeFlags(["-Xlinker", "--strip-all"], .when(configuration: .release)),
|
|
188
|
+
]`
|
|
189
|
+
: ''
|
|
190
|
+
return [
|
|
191
|
+
` ${kind}(`,
|
|
192
|
+
` name: "${target.name}",`,
|
|
193
|
+
` dependencies: [${deps}],`,
|
|
194
|
+
` path: "${target.path}",`,
|
|
195
|
+
` ${swiftSettings}${link}`,
|
|
196
|
+
` ),`,
|
|
197
|
+
].join('\n')
|
|
198
|
+
}
|
|
199
|
+
return [
|
|
200
|
+
'// swift-tools-version: 6.0',
|
|
201
|
+
'import PackageDescription',
|
|
202
|
+
'',
|
|
203
|
+
'// generated by swift-compile from the project package partition.',
|
|
204
|
+
'let package = Package(',
|
|
205
|
+
' name: "app",',
|
|
206
|
+
` products: [.executable(name: "${executable.name}", targets: ["${executable.name}"])],`,
|
|
207
|
+
` dependencies: [.package(path: "${moduleRoot}")],`,
|
|
208
|
+
' targets: [',
|
|
209
|
+
...targets.map(stanza),
|
|
210
|
+
' ]',
|
|
211
|
+
')',
|
|
212
|
+
'',
|
|
213
|
+
].join('\n')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function writeSwiftProject(
|
|
217
|
+
layout: SwiftProjectLayout,
|
|
218
|
+
input: SwiftCompileInput,
|
|
219
|
+
moduleRoot: string,
|
|
220
|
+
): void {
|
|
221
|
+
const targets = compileTargets(input)
|
|
222
|
+
if (targets.length === 0) {
|
|
223
|
+
rmSync(layout.sources, { recursive: true, force: true })
|
|
224
|
+
for (const [file, contents] of Object.entries(input.files)) {
|
|
225
|
+
const target = path.join(layout.sources, file)
|
|
226
|
+
mkdirSync(path.dirname(target), { recursive: true })
|
|
227
|
+
writeFileSync(target, contents, 'utf8')
|
|
228
|
+
}
|
|
229
|
+
const manifest = readFileSync(
|
|
230
|
+
path.join(moduleRoot, 'scripts/contrast-app-Package.swift'),
|
|
231
|
+
'utf8',
|
|
232
|
+
).replaceAll('__SOOTSIM_SWIFT_PACKAGE__', moduleRoot)
|
|
233
|
+
writeFileSync(path.join(layout.root, 'Package.swift'), manifest, 'utf8')
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
mkdirSync(layout.root, { recursive: true })
|
|
237
|
+
for (const child of readdirSync(layout.root)) {
|
|
238
|
+
if (child === '.build') continue
|
|
239
|
+
rmSync(path.join(layout.root, child), { recursive: true, force: true })
|
|
240
|
+
}
|
|
241
|
+
for (const [file, contents] of Object.entries(input.files)) {
|
|
242
|
+
if (file === 'Package.swift') continue
|
|
243
|
+
const target = path.join(layout.root, file)
|
|
244
|
+
mkdirSync(path.dirname(target), { recursive: true })
|
|
245
|
+
writeFileSync(target, contents, 'utf8')
|
|
246
|
+
}
|
|
247
|
+
writeFileSync(
|
|
248
|
+
path.join(layout.root, 'Package.swift'),
|
|
249
|
+
emitSwiftPackageManifest({
|
|
250
|
+
targets,
|
|
251
|
+
siblings: swiftSiblingDependencies(input.files, targets),
|
|
252
|
+
moduleRoot,
|
|
253
|
+
}),
|
|
254
|
+
'utf8',
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function streamText(value: unknown): string {
|
|
259
|
+
if (typeof value === 'string') return value.trim()
|
|
260
|
+
if (Buffer.isBuffer(value)) return value.toString('utf8').trim()
|
|
261
|
+
return ''
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function outputOf(error: unknown): string {
|
|
265
|
+
if (error && typeof error === 'object') {
|
|
266
|
+
const streams = [
|
|
267
|
+
streamText(Reflect.get(error, 'stdout')),
|
|
268
|
+
streamText(Reflect.get(error, 'stderr')),
|
|
269
|
+
].filter(Boolean)
|
|
270
|
+
if (streams.length > 0) return streams.join('\n')
|
|
271
|
+
}
|
|
272
|
+
return error instanceof Error ? error.message : String(error)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function builtArtifact(layout: SwiftProjectLayout): string {
|
|
276
|
+
const binDir = path.join(layout.root, '.build', WASM_TRIPLE, 'release')
|
|
277
|
+
const artifacts = existsSync(binDir)
|
|
278
|
+
? readdirSync(binDir)
|
|
279
|
+
.filter((name) => name.endsWith('.wasm'))
|
|
280
|
+
.sort()
|
|
281
|
+
: []
|
|
282
|
+
if (artifacts.length !== 1) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`swift-compile: the build produced ${artifacts.length} wasm artifacts; exactly one executable is the app`,
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
return path.join(binDir, artifacts[0])
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function projectDiagnostic(output: string, layout: SwiftProjectLayout): string {
|
|
291
|
+
const base = layout.multi ? layout.root : layout.sources
|
|
292
|
+
const prefix = `${base}${path.sep}`
|
|
293
|
+
return output
|
|
294
|
+
.split('\n')
|
|
295
|
+
.map((line) => (line.startsWith(prefix) ? line.slice(prefix.length) : line))
|
|
296
|
+
.join('\n')
|
|
297
|
+
.trim()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const ROOT_LOCKS = Symbol.for('peachsim.swiftCompile.rootLocks')
|
|
301
|
+
const host = globalThis as typeof globalThis & {
|
|
302
|
+
[ROOT_LOCKS]?: Map<string, Promise<unknown>>
|
|
303
|
+
}
|
|
304
|
+
const rootLocks = (host[ROOT_LOCKS] ??= new Map())
|
|
305
|
+
|
|
306
|
+
function serialize<T>(root: string, work: () => Promise<T>): Promise<T> {
|
|
307
|
+
const current = rootLocks.get(root) ?? Promise.resolve()
|
|
308
|
+
const next = current.then(work, work)
|
|
309
|
+
rootLocks.set(
|
|
310
|
+
root,
|
|
311
|
+
next.catch(() => {}),
|
|
312
|
+
)
|
|
313
|
+
return next
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** the one local compiler used by the route host and Vite host. */
|
|
317
|
+
export function compileSwiftProjectUnit(
|
|
318
|
+
input: SwiftCompileInput,
|
|
319
|
+
options: SwiftCompileOptions,
|
|
320
|
+
): Promise<SwiftCompileArtifact> {
|
|
321
|
+
mkdirSync(options.buildRoot, { recursive: true })
|
|
322
|
+
const root = realpathSync(options.buildRoot)
|
|
323
|
+
const moduleRoot = path.resolve(options.moduleRoot)
|
|
324
|
+
const layout: SwiftProjectLayout = {
|
|
325
|
+
root,
|
|
326
|
+
sources: path.join(root, 'Sources', 'App'),
|
|
327
|
+
multi: compileTargets(input).length > 0,
|
|
328
|
+
}
|
|
329
|
+
const hash = swiftArtifactHash(input, options.release, moduleRoot)
|
|
330
|
+
return serialize(root, async () => {
|
|
331
|
+
writeSwiftProject(layout, input, moduleRoot)
|
|
332
|
+
const swift = path.join(options.toolchainBin, 'swift')
|
|
333
|
+
if (!existsSync(swift)) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`swift-compile: no swift.org toolchain at ${options.toolchainBin}; install ${options.release} and its matching wasm sdk`,
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
await run(
|
|
340
|
+
swift,
|
|
341
|
+
[
|
|
342
|
+
'build',
|
|
343
|
+
'--package-path',
|
|
344
|
+
root,
|
|
345
|
+
'--swift-sdk',
|
|
346
|
+
`${options.release}_wasm`,
|
|
347
|
+
'-c',
|
|
348
|
+
'release',
|
|
349
|
+
'-Xswiftc',
|
|
350
|
+
'-Osize',
|
|
351
|
+
],
|
|
352
|
+
{ maxBuffer: 64 * 1024 * 1024, timeout: options.timeoutMs },
|
|
353
|
+
)
|
|
354
|
+
} catch (error) {
|
|
355
|
+
throw new Error(projectDiagnostic(outputOf(error), layout), { cause: error })
|
|
356
|
+
}
|
|
357
|
+
return { hash, bytes: readFileSync(builtArtifact(layout)) }
|
|
358
|
+
})
|
|
359
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** one SwiftPM target a sent root manifest declares. */
|
|
2
|
+
export interface SwiftPackageTarget {
|
|
3
|
+
/** the target name, as `import <name>` spells it. */
|
|
4
|
+
name: string
|
|
5
|
+
/** project-relative directory holding the target's sources. */
|
|
6
|
+
path: string
|
|
7
|
+
/** true for `.executableTarget`: the one that links the wasm artifact. */
|
|
8
|
+
executable: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** whether a project-relative key is a SwiftPM manifest, not a source. */
|
|
12
|
+
export function isSwiftManifestFile(file: string): boolean {
|
|
13
|
+
const segments = file.split('/')
|
|
14
|
+
return segments[segments.length - 1] === 'Package.swift'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* the library and executable targets a sent root manifest declares, in order.
|
|
19
|
+
*
|
|
20
|
+
* A manifest is Swift code, not data, so this is a scanner, not a parser:
|
|
21
|
+
* each `.target(` / `.executableTarget(` call is matched to its balanced
|
|
22
|
+
* close paren (string-aware, so a paren inside a literal cannot end it
|
|
23
|
+
* early), and `name:` / `path:` are read from the call's own depth only, so
|
|
24
|
+
* a nested `.product(name: "SwiftUI", ...)` inside dependencies never wins.
|
|
25
|
+
*/
|
|
26
|
+
export function swiftManifestTargets(manifest: string): SwiftPackageTarget[] {
|
|
27
|
+
const code = manifest.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '')
|
|
28
|
+
const targets: SwiftPackageTarget[] = []
|
|
29
|
+
const pattern = /\.(executableTarget|target)\s*\(/g
|
|
30
|
+
let match: RegExpExecArray | null
|
|
31
|
+
while ((match = pattern.exec(code)) !== null) {
|
|
32
|
+
const executable = match[1] === 'executableTarget'
|
|
33
|
+
let depth = 1
|
|
34
|
+
let i = pattern.lastIndex
|
|
35
|
+
let inString = false
|
|
36
|
+
let escaped = false
|
|
37
|
+
let flat = ''
|
|
38
|
+
while (i < code.length && depth > 0) {
|
|
39
|
+
const ch = code[i]
|
|
40
|
+
if (inString) {
|
|
41
|
+
if (escaped) escaped = false
|
|
42
|
+
else if (ch === '\\') escaped = true
|
|
43
|
+
else if (ch === '"') inString = false
|
|
44
|
+
if (depth === 1) flat += ch
|
|
45
|
+
} else if (ch === '"') {
|
|
46
|
+
inString = true
|
|
47
|
+
if (depth === 1) flat += ch
|
|
48
|
+
} else if (ch === '(') {
|
|
49
|
+
depth++
|
|
50
|
+
if (depth === 1) flat += ch
|
|
51
|
+
} else if (ch === ')') {
|
|
52
|
+
depth--
|
|
53
|
+
if (depth >= 1) flat += ch
|
|
54
|
+
} else if (depth === 1) {
|
|
55
|
+
flat += ch
|
|
56
|
+
}
|
|
57
|
+
i++
|
|
58
|
+
}
|
|
59
|
+
const name = /\bname\s*:\s*"([^"]+)"/.exec(flat)?.[1]
|
|
60
|
+
if (!name) continue
|
|
61
|
+
const rawPath = /\bpath\s*:\s*"([^"]*)"/.exec(flat)?.[1]
|
|
62
|
+
const targetPath =
|
|
63
|
+
rawPath == null || rawPath === ''
|
|
64
|
+
? `Sources/${name}`
|
|
65
|
+
: rawPath.replace(/^\.\//, '').replace(/\/+$/, '')
|
|
66
|
+
targets.push({ name, path: targetPath, executable })
|
|
67
|
+
}
|
|
68
|
+
return targets
|
|
69
|
+
}
|
package/src/swift-submit.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* its own origin's `/api/swift-compile`), the node bundle builder used by
|
|
8
8
|
* the test-drive CLI and the deploy publisher (posts an absolute endpoint),
|
|
9
9
|
* and the One/rolldown `.swift` transform (same, with a `ccdn_` credential).
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Remote callers never compile in-process: the container is the compiler for
|
|
11
|
+
* this path. The local Vite host uses the same project-unit compiler directly
|
|
12
|
+
* and does not pass through this HTTP adapter.
|
|
12
13
|
*
|
|
13
14
|
* It lives here (rather than in the app's `src/bundler/`) because the One
|
|
14
15
|
* transform ships in this package and cannot import app code; the app's own
|