crossbind 0.0.1 → 2.0.0-beta.51

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.
Files changed (95) hide show
  1. package/AGENTS.md +92 -0
  2. package/CHANGELOG.md +156 -0
  3. package/LICENSE +21 -0
  4. package/README.md +133 -0
  5. package/package.json +52 -4
  6. package/src/actions/buildBinTools.js +0 -0
  7. package/src/actions/buildCargo.js +88 -0
  8. package/src/actions/buildDependencies.js +157 -0
  9. package/src/actions/buildExternal.js +57 -0
  10. package/src/actions/buildJs.js +93 -0
  11. package/src/actions/buildLib.js +115 -0
  12. package/src/actions/buildTypes.js +33 -0
  13. package/src/actions/buildWasiCommand.js +101 -0
  14. package/src/actions/buildWasm.js +322 -0
  15. package/src/actions/createInterface.js +172 -0
  16. package/src/actions/createLib.js +249 -0
  17. package/src/actions/createXCFramework.js +82 -0
  18. package/src/actions/extensions.js +7 -0
  19. package/src/actions/getAllBridges.js +20 -0
  20. package/src/actions/getCmakeParameters.js +66 -0
  21. package/src/actions/getData.js +46 -0
  22. package/src/actions/getDependLibs.js +25 -0
  23. package/src/actions/isSourceNewer.js +35 -0
  24. package/src/actions/licenses.js +156 -0
  25. package/src/actions/run.js +307 -0
  26. package/src/actions/target.js +54 -0
  27. package/src/assets/cmake/CMakeLists.txt +61 -0
  28. package/src/assets/cmake/dist.cmake +57 -0
  29. package/src/assets/cmake/ios.toolchain.cmake +1176 -0
  30. package/src/assets/cpp-runtime/browser.cpp +12 -0
  31. package/src/assets/cpp-runtime/commonBridges.cpp +17 -0
  32. package/src/assets/cpp-runtime/crossbindEmptySource.cpp +9 -0
  33. package/src/assets/cpp-runtime/node.cpp +19 -0
  34. package/src/assets/js-runtime/adapters/exception-decode.js +79 -0
  35. package/src/assets/js-runtime/adapters/fs-browser.js +173 -0
  36. package/src/assets/js-runtime/adapters/fs-node.js +28 -0
  37. package/src/assets/js-runtime/adapters/path-fs.js +6 -0
  38. package/src/assets/js-runtime/adapters/path-url.js +8 -0
  39. package/src/assets/js-runtime/adapters/vector-coercion.js +209 -0
  40. package/src/assets/js-runtime/adapters/worker-comlink.js +309 -0
  41. package/src/assets/js-runtime/browser.js +16 -0
  42. package/src/assets/js-runtime/core.js +193 -0
  43. package/src/assets/js-runtime/edge.js +18 -0
  44. package/src/assets/js-runtime/node.js +20 -0
  45. package/src/assets/packaging/crossbind-package.podspec +17 -0
  46. package/src/assets/wasi-runtime/stubs.c +84 -0
  47. package/src/bin.js +416 -0
  48. package/src/index.js +19 -0
  49. package/src/integration/getCrossbindScript.js +180 -0
  50. package/src/integration/getDependFilePath.js +132 -0
  51. package/src/runtime/wasiRun.mjs +118 -0
  52. package/src/state/calculateDependencyParameters.js +97 -0
  53. package/src/state/index.js +128 -0
  54. package/src/state/loadConfig.js +215 -0
  55. package/src/utils/appRustCrates.js +106 -0
  56. package/src/utils/binTools.js +76 -0
  57. package/src/utils/bridgeAsyncGuard.js +37 -0
  58. package/src/utils/cargoTarget.js +15 -0
  59. package/src/utils/cppDts.js +201 -0
  60. package/src/utils/cppFieldBindings.js +23 -0
  61. package/src/utils/dependencyRebuild.js +99 -0
  62. package/src/utils/dirLock.js +71 -0
  63. package/src/utils/downloadAndExtractFile.js +102 -0
  64. package/src/utils/familyManifest.js +17 -0
  65. package/src/utils/findFiles.js +5 -0
  66. package/src/utils/fixPackageName.js +7 -0
  67. package/src/utils/flattenConfigForTable.js +75 -0
  68. package/src/utils/getAbsolutePath.js +14 -0
  69. package/src/utils/getCMakeListsFilePath.js +17 -0
  70. package/src/utils/getOsUserAndGroupId.js +11 -0
  71. package/src/utils/getParentPath.js +12 -0
  72. package/src/utils/hash.js +33 -0
  73. package/src/utils/inputStamp.js +28 -0
  74. package/src/utils/licenseReport.js +94 -0
  75. package/src/utils/linkLayout.js +34 -0
  76. package/src/utils/loadJs.js +31 -0
  77. package/src/utils/loadJson.js +16 -0
  78. package/src/utils/logger.js +110 -0
  79. package/src/utils/ociArtifact.js +124 -0
  80. package/src/utils/overrideDependency.js +99 -0
  81. package/src/utils/provenance.js +54 -0
  82. package/src/utils/pullDockerImage.js +105 -0
  83. package/src/utils/replaceBasePathForDocker.js +18 -0
  84. package/src/utils/resolveEmbindRust.js +66 -0
  85. package/src/utils/runCargo.js +228 -0
  86. package/src/utils/rustBridgeGen.js +1013 -0
  87. package/src/utils/rustMt.js +76 -0
  88. package/src/utils/rustSysroot.js +171 -0
  89. package/src/utils/rustSysrootPin.js +11 -0
  90. package/src/utils/systemKeys.js +29 -0
  91. package/src/utils/targets.js +109 -0
  92. package/src/utils/wasiToolchain.js +47 -0
  93. package/src/utils/writeIfChanged.js +8 -0
  94. package/src/utils/writeJson.js +9 -0
  95. package/vitest.config.js +46 -0
@@ -0,0 +1,76 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+
4
+ // One argv per flag: runCargo joins these with cargo's 0x1F separator, so a flag must not
5
+ // contain a space of its own.
6
+ const MT_RUSTFLAGS = ['-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals'];
7
+
8
+ // mt (shared-memory) wasm needs every Rust object built with the atomics/bulk-memory features,
9
+ // std included - and rustup ships only a featureless std. The nightly-only -Zbuild-std rebuild
10
+ // below is the same recipe wasm-bindgen-rayon uses; probing first turns a cryptic wasm-ld error
11
+ // into an actionable install instruction. Shared by every cargo entry point (app-local super
12
+ // crate and cargo-type packages).
13
+ export function isMtWasm(target) {
14
+ return target.platform === 'wasm' && target.runtime === 'mt';
15
+ }
16
+
17
+ export function assertMtRustToolchain() {
18
+ const probe = spawnSync('rustup', ['component', 'list', '--toolchain', 'nightly', '--installed'], { encoding: 'utf8' });
19
+ if (probe.status !== 0 || !String(probe.stdout ?? '').includes('rust-src')) {
20
+ throw new Error(
21
+ "crossbind: Rust on the wasm 'mt' runtime needs the Rust std rebuilt with "
22
+ + 'atomics (nightly -Zbuild-std). Run `rustup toolchain install nightly '
23
+ + "--component rust-src` and rebuild, or build with runtime 'st'.",
24
+ );
25
+ }
26
+ }
27
+
28
+ // st and mt share a cargo triple, so they must not share a target dir: cargo would happily
29
+ // serve featureless objects to a shared-memory link (or atomics objects to an st one).
30
+ export function cargoTargetDirFor(baseDir, target) {
31
+ return `${baseDir}/${isMtWasm(target) ? 'target-mt' : 'target'}`;
32
+ }
33
+
34
+ // The wasm sysroots the image ships, each a COMPLETE sysroot (see web.Dockerfile): --sysroot is
35
+ // global, so build scripts and proc-macros compiled for the host resolve there too. `current`
36
+ // points at whatever version that image pinned, so the CLI carries no version of its own.
37
+ const CONTAINER_SYSROOT = '/opt/crossbind/rust/current';
38
+
39
+ export function cargoBuildInvocation({
40
+ target, triple, targetDir, manifestPath, sysroot = false,
41
+ }) {
42
+ const isMt = isMtWasm(target);
43
+ // With a prebuilt sysroot there is nothing to rebuild and nothing unstable to ask for: stable
44
+ // rustc links the std that was compiled with the same features. Without one - no pinned
45
+ // artifact, or a host on a different rustc - mt still rebuilds std the nightly way.
46
+ const buildStd = isMt && !sysroot;
47
+ const args = [
48
+ ...(buildStd ? ['+nightly'] : []),
49
+ 'build', '--release', '--target', triple,
50
+ ...(buildStd ? ['-Zbuild-std=std,panic_abort'] : []),
51
+ '--target-dir', targetDir, '--manifest-path', manifestPath,
52
+ ];
53
+ // Flags travel as data, not as an environment patch: cargo ignores RUSTFLAGS outright when
54
+ // CARGO_ENCODED_RUSTFLAGS is set, so inheriting the caller's environment here used to let a
55
+ // developer who has that variable exported silently link a featureless std into an mt module.
56
+ // panic is the other half of the same contract - the std being linked is built with abort.
57
+ // `true` is the image tree at a path only the container has; a string is a downloaded
58
+ // artifact on this machine (RUNNER=LOCAL). Same layout either way: <root>/<variant>.
59
+ const variant = isMt ? 'mt' : 'st';
60
+ let wasmSysroot = null;
61
+ if (sysroot && target.platform === 'wasm') {
62
+ wasmSysroot = sysroot === true ? `${CONTAINER_SYSROOT}/${variant}` : path.join(sysroot, variant);
63
+ }
64
+ return {
65
+ args,
66
+ rustflags: [
67
+ ...(wasmSysroot ? ['--sysroot', wasmSysroot] : []),
68
+ ...(isMt ? MT_RUSTFLAGS : []),
69
+ ],
70
+ panic: target.platform === 'wasm' ? 'abort' : undefined,
71
+ // Only the nightly build-std rebuild needs unstable flags; where a sysroot answers, stable
72
+ // is enough and the pin stays on.
73
+ allowUnstable: buildStd,
74
+ isMt,
75
+ };
76
+ }
@@ -0,0 +1,171 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import withDirLock from './dirLock.js';
6
+ import fetchOciLayer from './ociArtifact.js';
7
+ import { cargoRunner } from './runCargo.js';
8
+ import { isMtWasm } from './rustMt.js';
9
+ import PIN from './rustSysrootPin.js';
10
+
11
+ // The second consumption channel for the Rust sysroots. A containerized build gets them as an image
12
+ // layer; a host build (RUNNER=LOCAL) reads that SAME layer straight out of the registry over plain
13
+ // HTTPS. Not a repackaged copy hosted somewhere else - the same object, so the two channels cannot
14
+ // drift, and there is no second artifact to publish, verify or forget to delete.
15
+ //
16
+ // Everything hangs off one pinned index digest. Verify before unpacking, never let the archive
17
+ // choose where its files land, and key the cache by digest so two versions coexist.
18
+
19
+ const SCHEMA = 1;
20
+
21
+ export function sysrootRoot() {
22
+ return path.join(os.homedir(), '.crossbind', 'rust-sysroot');
23
+ }
24
+
25
+ // The rlibs carry the producing compiler's metadata hash: a sysroot is only consumable by the
26
+ // exact rustc that built it, so the manifest is checked against the host before anything links.
27
+ export function hostRustc() {
28
+ const probe = spawnSync('rustc', ['-vV'], { encoding: 'utf8' });
29
+ if (probe.status !== 0) {
30
+ throw new Error('crossbind: rustc not found on PATH - install Rust (https://rustup.rs) to build Rust packages.');
31
+ }
32
+ const read = (key) => probe.stdout.match(new RegExp(`^${key}: (.+)$`, 'm'))?.[1]?.trim();
33
+ return { version: read('release'), commit: read('commit-hash') };
34
+ }
35
+
36
+ export function assertManifest(manifest, host = hostRustc()) {
37
+ if (manifest?.schema !== SCHEMA) {
38
+ throw new Error(`crossbind: rust sysroot manifest schema ${manifest?.schema ?? '(missing)'} is not supported (expected ${SCHEMA}).`);
39
+ }
40
+ for (const variant of ['st', 'mt']) {
41
+ if (!manifest.variants?.[variant]) {
42
+ throw new Error(`crossbind: rust sysroot artifact is missing the '${variant}' variant.`);
43
+ }
44
+ }
45
+ if (manifest.rustc !== host.version || manifest.rustcCommit !== host.commit) {
46
+ throw new Error(
47
+ 'crossbind: the rust sysroot artifact was built by a different compiler than the one on PATH.\n'
48
+ + ` artifact: rustc ${manifest.rustc} (${String(manifest.rustcCommit).slice(0, 12)})\n`
49
+ + ` host: rustc ${host.version} (${String(host.commit).slice(0, 12)})\n`
50
+ + 'Prebuilt rlibs are only consumable by the exact compiler that produced them - install the pinned toolchain.',
51
+ );
52
+ }
53
+ return manifest;
54
+ }
55
+
56
+ function readManifest(dir) {
57
+ return JSON.parse(fs.readFileSync(path.join(dir, 'manifest.json'), 'utf8'));
58
+ }
59
+
60
+ // The archive is an image filesystem export, so the tree sits at opt/crossbind/rust/<version>.
61
+ function findSysrootTree(extracted) {
62
+ const versions = path.join(extracted, 'opt', 'crossbind', 'rust');
63
+ const entries = fs.existsSync(versions) ? fs.readdirSync(versions) : [];
64
+ const found = entries
65
+ .map((version) => path.join(versions, version))
66
+ .find((dir) => fs.existsSync(path.join(dir, 'manifest.json')));
67
+ if (!found) {
68
+ throw new Error('crossbind: the rust sysroot archive contains no opt/crossbind/rust/<version>/manifest.json.');
69
+ }
70
+ return found;
71
+ }
72
+
73
+ // An archive is untrusted input even when its digest checks out: the digest says "these are the
74
+ // bytes that were published", not "these bytes are harmless". Entries are listed first and anything
75
+ // that could write outside the staging directory is refused before a single file is created.
76
+ function extractTo(archive, work) {
77
+ const list = spawnSync('tar', ['-tzf', archive], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
78
+ if (list.status !== 0) {
79
+ throw new Error(`crossbind: cannot read the sysroot archive: ${(list.stderr || '').trim() || `tar exited ${list.status}`}`);
80
+ }
81
+ const escaping = list.stdout.split('\n').map((e) => e.trim()).filter(Boolean)
82
+ .filter((entry) => entry.startsWith('/') || entry.split('/').includes('..'));
83
+ if (escaping.length) {
84
+ throw new Error(`crossbind: the sysroot archive contains entries that escape the extraction directory: ${escaping.slice(0, 3).join(', ')}`);
85
+ }
86
+ fs.mkdirSync(work, { recursive: true });
87
+ const tar = spawnSync('tar', ['-xzf', archive, '-C', work], { encoding: 'utf8' });
88
+ if (tar.error) throw new Error(`crossbind: cannot run tar to extract ${archive}: ${tar.error.message}`);
89
+ if (tar.status !== 0) {
90
+ throw new Error(`crossbind: extracting the rust sysroot layer failed: ${(tar.stderr || '').trim() || `tar exited ${tar.status}`}`);
91
+ }
92
+ }
93
+
94
+ // The architecture whose leaf to pull. The trees hold wasm rlibs either way, but they were produced
95
+ // by a host-arch rustc and the manifest records which - so take the leaf built for this machine.
96
+ const hostArch = () => (process.arch === 'arm64' ? 'arm64' : 'amd64');
97
+
98
+ // Returns the directory holding {st,mt,manifest.json}, fetched once per pinned index digest.
99
+ export default async function ensureRustSysroot({
100
+ image, index, root = sysrootRoot(), host, arch = hostArch(),
101
+ }) {
102
+ if (!image || !index) throw new Error('crossbind: a rust sysroot must be pinned by image and index digest.');
103
+ const check = (manifest) => assertManifest(manifest, host ?? hostRustc());
104
+ const dest = path.join(root, index.replace(':', '-'));
105
+ if (fs.existsSync(path.join(dest, 'manifest.json'))) {
106
+ check(readManifest(dest));
107
+ return dest;
108
+ }
109
+
110
+ fs.mkdirSync(root, { recursive: true });
111
+ return withDirLock(`${dest}.lock`, async () => {
112
+ // Another build may have finished it while this one waited for the lock.
113
+ if (fs.existsSync(path.join(dest, 'manifest.json'))) {
114
+ check(readManifest(dest));
115
+ return dest;
116
+ }
117
+ const work = fs.mkdtempSync(path.join(root, '.staging-'));
118
+ try {
119
+ const archive = path.join(work, 'layer.tar.gz');
120
+ await fetchOciLayer({ image, index, arch, dest: archive });
121
+ const extracted = path.join(work, 'extract');
122
+ extractTo(archive, extracted);
123
+ const tree = findSysrootTree(extracted);
124
+ check(readManifest(tree));
125
+ // Atomic: a reader either sees no directory at all or a complete, verified one.
126
+ fs.renameSync(tree, dest);
127
+ } finally {
128
+ fs.rmSync(work, { recursive: true, force: true });
129
+ }
130
+ return dest;
131
+ });
132
+ }
133
+
134
+ // The directory rustc's --sysroot is pointed at for a given runtime.
135
+ export function sysrootPath(dir, variant) {
136
+ const target = path.join(dir, variant);
137
+ if (!fs.existsSync(target)) {
138
+ throw new Error(`crossbind: the rust sysroot at ${dir} has no '${variant}' variant.`);
139
+ }
140
+ return target;
141
+ }
142
+
143
+ // Resolved once per process. The download has to happen in an async step, but every consumer of
144
+ // the result - createLib, buildCargo, the bundler plugins - is synchronous, and threading a
145
+ // promise through them would mean making createLib and four plugins async for a case that only
146
+ // arises on RUNNER=LOCAL. So: prepare here, read synchronously below.
147
+ let resolved = null;
148
+
149
+ // Called from buildDependencies, the one async step every build path awaits before the sync work
150
+ // starts. Only downloads when a target will actually consume it.
151
+ export async function prepareRustSysroot(targets, log = console.log) {
152
+ if (resolved || !PIN) return resolved;
153
+ if (!targets.some((target) => isMtWasm(target) && cargoRunner(target) === 'LOCAL')) return null;
154
+ try {
155
+ resolved = await ensureRustSysroot({ image: PIN.image, index: PIN.index });
156
+ } catch (e) {
157
+ // A host on a different rustc, or a release that cannot be reached: mt still builds, the
158
+ // slow way. Refusing here would take away a build that works today.
159
+ log(`crossbind: prebuilt rust sysroot unavailable, falling back to the nightly std rebuild - ${e.message}`);
160
+ }
161
+ return resolved;
162
+ }
163
+
164
+ // What cargoBuildInvocation takes for `sysroot`: true where the image carries the trees, an
165
+ // absolute path where a downloaded artifact answers, false where mt must rebuild std on nightly.
166
+ // st needs nothing - rustup's stock std is already correct for it.
167
+ export function sysrootFor(target) {
168
+ if (cargoRunner(target) !== 'LOCAL') return true;
169
+ if (!isMtWasm(target)) return false;
170
+ return resolved ?? false;
171
+ }
@@ -0,0 +1,11 @@
1
+ // Generated by scripts/pin-rust-sysroot.js - do not edit by hand.
2
+ //
3
+ // The published sysroot image a host build (RUNNER=LOCAL) reads its Rust sysroots out of - the same
4
+ // object the web image embeds, not a repackaged copy, so the two channels cannot drift. The index
5
+ // digest is the whole contract: the loader verifies the index body against it, then each descriptor
6
+ // inside a verified body authenticates the next fetch, down to the layer.
7
+ export default {
8
+ version: '1.0.2',
9
+ image: 'ghcr.io/crossbind/rust-sysroot',
10
+ index: 'sha256:0ab2e522b1b09b2af7f312d4b940ab92ee470e8165df4b563e274a4eb2fdb903',
11
+ };
@@ -0,0 +1,29 @@
1
+ const systemKeys = {
2
+ XCODE_DEVELOPMENT_TEAM: {
3
+ description: 'The unique identifier of the development team used for code signing and app distribution in Xcode.',
4
+ default: '',
5
+ },
6
+ RUNNER: {
7
+ description: 'The execution environment for running the application.',
8
+ options: ['DOCKER_RUN', 'DOCKER_EXEC', 'LOCAL'],
9
+ default: 'DOCKER_RUN',
10
+ },
11
+ WASI_SDK_PATH: {
12
+ description: 'Path to an extracted wasi-sdk (>= 34, wasm32-wasip3 sysroot) used by platform:\'wasi\' builds. The CROSSBIND_WASI_SDK_PATH environment variable overrides it.',
13
+ default: '',
14
+ },
15
+ DOCKER_REGISTRY_MIRROR: {
16
+ description: 'Registry prefix to pull the build images from, e.g. registry.example.dev/crossbind. crossbind appends the release digest itself, so reproducibility is preserved. The CROSSBIND_REGISTRY_MIRROR environment variable overrides it.',
17
+ default: '',
18
+ },
19
+ DOCKER_IMAGE_WEB: {
20
+ description: 'Image reference used for wasm and wasi builds instead of the pinned one. A reference without the release digest disables the reproducibility guarantee. The CROSSBIND_IMAGE_WEB environment variable overrides it.',
21
+ default: '',
22
+ },
23
+ DOCKER_IMAGE_ANDROID: {
24
+ description: 'Image reference used for android builds instead of the pinned one. A reference without the release digest disables the reproducibility guarantee. The CROSSBIND_IMAGE_ANDROID environment variable overrides it.',
25
+ default: '',
26
+ },
27
+ };
28
+
29
+ export default systemKeys;
@@ -0,0 +1,109 @@
1
+ // Single source for the build-target matrix and its naming; state/ copies (and then
2
+ // decorates) these entries, and consumer runtimes read them without touching CLI state.
3
+
4
+ export const TARGETS = [
5
+ {
6
+ platform: 'wasm', arch: 'wasm32', runtime: 'st', buildType: 'release', runtimeEnv: 'browser',
7
+ },
8
+ {
9
+ platform: 'wasm', arch: 'wasm32', runtime: 'st', buildType: 'release', runtimeEnv: 'edge',
10
+ },
11
+ {
12
+ platform: 'wasm', arch: 'wasm32', runtime: 'st', buildType: 'release', runtimeEnv: 'node',
13
+ },
14
+ {
15
+ platform: 'wasm', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'browser',
16
+ },
17
+ {
18
+ platform: 'wasm', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'edge',
19
+ },
20
+ {
21
+ platform: 'wasm', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'node',
22
+ },
23
+ {
24
+ platform: 'wasm', arch: 'wasm32', runtime: 'mt', buildType: 'release', runtimeEnv: 'browser',
25
+ },
26
+ {
27
+ platform: 'wasm', arch: 'wasm32', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
28
+ },
29
+ {
30
+ platform: 'wasm', arch: 'wasm32', runtime: 'mt', buildType: 'debug', runtimeEnv: 'browser',
31
+ },
32
+ {
33
+ platform: 'wasm', arch: 'wasm32', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
34
+ },
35
+ {
36
+ platform: 'wasm', arch: 'wasm64', runtime: 'st', buildType: 'release', runtimeEnv: 'browser',
37
+ },
38
+ {
39
+ platform: 'wasm', arch: 'wasm64', runtime: 'st', buildType: 'release', runtimeEnv: 'edge',
40
+ },
41
+ {
42
+ platform: 'wasm', arch: 'wasm64', runtime: 'st', buildType: 'release', runtimeEnv: 'node',
43
+ },
44
+ {
45
+ platform: 'wasm', arch: 'wasm64', runtime: 'st', buildType: 'debug', runtimeEnv: 'browser',
46
+ },
47
+ {
48
+ platform: 'wasm', arch: 'wasm64', runtime: 'st', buildType: 'debug', runtimeEnv: 'edge',
49
+ },
50
+ {
51
+ platform: 'wasm', arch: 'wasm64', runtime: 'st', buildType: 'debug', runtimeEnv: 'node',
52
+ },
53
+ {
54
+ platform: 'wasm', arch: 'wasm64', runtime: 'mt', buildType: 'release', runtimeEnv: 'browser',
55
+ },
56
+ {
57
+ platform: 'wasm', arch: 'wasm64', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
58
+ },
59
+ {
60
+ platform: 'wasm', arch: 'wasm64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'browser',
61
+ },
62
+ {
63
+ platform: 'wasm', arch: 'wasm64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
64
+ },
65
+ {
66
+ platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'release',
67
+ },
68
+ {
69
+ platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug',
70
+ },
71
+ {
72
+ platform: 'android', arch: 'arm64-v8a', runtime: 'mt', buildType: 'release',
73
+ },
74
+ {
75
+ platform: 'android', arch: 'arm64-v8a', runtime: 'mt', buildType: 'debug',
76
+ },
77
+ {
78
+ platform: 'android', arch: 'x86_64', runtime: 'mt', buildType: 'release',
79
+ },
80
+ {
81
+ platform: 'android', arch: 'x86_64', runtime: 'mt', buildType: 'debug',
82
+ },
83
+ {
84
+ platform: 'ios', arch: 'iphoneos', runtime: 'mt', buildType: 'release',
85
+ },
86
+ {
87
+ platform: 'ios', arch: 'iphoneos', runtime: 'mt', buildType: 'debug',
88
+ },
89
+ {
90
+ platform: 'ios', arch: 'iphonesimulator', runtime: 'mt', buildType: 'release',
91
+ },
92
+ {
93
+ platform: 'ios', arch: 'iphonesimulator', runtime: 'mt', buildType: 'debug',
94
+ },
95
+ ];
96
+
97
+ export function targetPathOf(target) {
98
+ return `${target.platform}-${target.arch}-${target.runtime}-${target.buildType}`;
99
+ }
100
+
101
+ export function filterTargetSpecs(targetSpecs, target) {
102
+ return targetSpecs?.filter(t => (
103
+ (!t.platform || t.platform === target.platform)
104
+ && (!t.arch || t.arch === target.arch)
105
+ && (!t.runtime || t.runtime === target.runtime)
106
+ && (!t.buildType || t.buildType === target.buildType)
107
+ && (!t.runtimeEnv || t.runtimeEnv === target.runtimeEnv)
108
+ ))?.map(t => t?.specs)?.filter(t => t) || [];
109
+ }
@@ -0,0 +1,47 @@
1
+ // Toolchain surface for platform:'wasi' (wasm32-wasip3, wasi-sdk >= 34; run under `wasmtime`).
2
+
3
+ export const WASI_TARGET_TRIPLE = 'wasm32-wasip3';
4
+ // The sdk's clang still defaults to wasip1, so every compile/link carries --target explicitly.
5
+ export const WASI_TARGET_FLAGS = [`--target=${WASI_TARGET_TRIPLE}`];
6
+
7
+ export const WASI_COMPILE_DEFINES = [
8
+ '-D_WASI_EMULATED_SIGNAL',
9
+ '-D_WASI_EMULATED_PROCESS_CLOCKS',
10
+ '-D_WASI_EMULATED_MMAN',
11
+ '-D_WASI_EMULATED_GETPID',
12
+ ];
13
+
14
+ // -mexception-handling unlocks wasi-libc's setjmp.h (-mllvm flags are invisible to the preprocessor); engines only run standard Wasm 3.0 EH, hence legacy-eh=false.
15
+ export const WASI_EH_CFLAGS = [
16
+ '-mexception-handling',
17
+ '-mllvm', '-wasm-enable-sjlj',
18
+ '-mllvm', '-wasm-use-legacy-eh=false',
19
+ ];
20
+
21
+ // Back the _WASI_EMULATED_* defines; safe in configure-probe LIBS.
22
+ export const WASI_EMULATION_LIBS = [
23
+ '-lwasi-emulated-signal',
24
+ '-lwasi-emulated-process-clocks',
25
+ '-lwasi-emulated-mman',
26
+ '-lwasi-emulated-getpid',
27
+ ];
28
+
29
+ // Must trail every archive; -lunwind/-lsetjmp resolve only under -fwasm-exceptions' eh/ sysroot, so keep them out of C-mode probes.
30
+ export const WASI_LINK_LIBS = [
31
+ '-lunwind',
32
+ '-lsetjmp',
33
+ ...WASI_EMULATION_LIBS,
34
+ ];
35
+
36
+ export function wasiCFlags() {
37
+ return [...WASI_TARGET_FLAGS, ...WASI_COMPILE_DEFINES, ...WASI_EH_CFLAGS];
38
+ }
39
+
40
+ export function wasiCxxFlags() {
41
+ return [...WASI_TARGET_FLAGS, ...WASI_COMPILE_DEFINES, '-fwasm-exceptions', ...WASI_EH_CFLAGS];
42
+ }
43
+
44
+ // Env override first, then persistent ~/.crossbind.json; null when unset.
45
+ export function resolveWasiSdkPath(system, env = process.env) {
46
+ return env.CROSSBIND_WASI_SDK_PATH || system?.WASI_SDK_PATH || null;
47
+ }
@@ -0,0 +1,8 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ export default function writeIfChanged(file, content) {
5
+ if (fs.existsSync(file) && fs.readFileSync(file, 'utf8') === content) return;
6
+ fs.mkdirSync(path.dirname(file), { recursive: true });
7
+ fs.writeFileSync(file, content);
8
+ }
@@ -0,0 +1,9 @@
1
+ import fs from 'node:fs';
2
+
3
+ export default function writeJson(filePath, data) {
4
+ try {
5
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 4));
6
+ } catch (e) {
7
+ console.error(e);
8
+ }
9
+ }
@@ -0,0 +1,46 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ // loadJs/loadConfig dynamically import config files written to os.tmpdir()
5
+ // during tests. Vite 6 tightened server.fs.allow and rejects those out-of-root
6
+ // paths ("Does the file exist?"); relax the fs check for the test runner.
7
+ server: {
8
+ fs: {
9
+ strict: false,
10
+ },
11
+ },
12
+ test: {
13
+ include: ['test/**/*.test.js'],
14
+ environment: 'node',
15
+ coverage: {
16
+ provider: 'v8',
17
+ reporter: ['text', 'html'],
18
+ // src/actions/ used to be excluded on the grounds that it only shells out to
19
+ // docker/emcc/cargo, so a test there would pin a command line rather than behaviour,
20
+ // and that the e2e suites already covered it. 2.0.0-beta.50 disproved the second half:
21
+ // one call site asked docker for an image ref that does not exist on arm64, no e2e
22
+ // exercised a machine without that image already cached, and android builds shipped
23
+ // broken to every Apple Silicon user. Which command line these actions build IS the
24
+ // behaviour, so the layer is measured.
25
+ include: ['src/utils/**/*.js', 'src/state/**/*.js', 'src/assets/**/*.js', 'src/actions/**/*.js'],
26
+ all: true,
27
+ // A floor with a one-point band, not a ratchet. Coverage is not identical across
28
+ // environments - resolveEmbindRust walks the real node_modules layout, so which of its
29
+ // catch branches run differs between a dev machine and a fresh CI install - and the
30
+ // totals land a few hundredths apart. Auto-raising these locally while enforcing them
31
+ // exactly on CI therefore fails on that difference rather than on a regression: CI
32
+ // measured 81.67/76.66/84.31/82.72 against floors written from a local run. The
33
+ // figures below are a local run of the widened surface (62.18/55.64/65.96/62.09) minus
34
+ // about a point. They read lower than the numbers they replace because they now count
35
+ // src/actions/, not because anything regressed - the old floors simply were not
36
+ // measuring the layer that broke. Raise them deliberately when a gain is worth
37
+ // locking in.
38
+ thresholds: {
39
+ statements: 61.2,
40
+ branches: 54.6,
41
+ functions: 65.0,
42
+ lines: 61.1,
43
+ },
44
+ },
45
+ },
46
+ });