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.
- package/AGENTS.md +92 -0
- package/CHANGELOG.md +156 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/package.json +52 -4
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildCargo.js +88 -0
- package/src/actions/buildDependencies.js +157 -0
- package/src/actions/buildExternal.js +57 -0
- package/src/actions/buildJs.js +93 -0
- package/src/actions/buildLib.js +115 -0
- package/src/actions/buildTypes.js +33 -0
- package/src/actions/buildWasiCommand.js +101 -0
- package/src/actions/buildWasm.js +322 -0
- package/src/actions/createInterface.js +172 -0
- package/src/actions/createLib.js +249 -0
- package/src/actions/createXCFramework.js +82 -0
- package/src/actions/extensions.js +7 -0
- package/src/actions/getAllBridges.js +20 -0
- package/src/actions/getCmakeParameters.js +66 -0
- package/src/actions/getData.js +46 -0
- package/src/actions/getDependLibs.js +25 -0
- package/src/actions/isSourceNewer.js +35 -0
- package/src/actions/licenses.js +156 -0
- package/src/actions/run.js +307 -0
- package/src/actions/target.js +54 -0
- package/src/assets/cmake/CMakeLists.txt +61 -0
- package/src/assets/cmake/dist.cmake +57 -0
- package/src/assets/cmake/ios.toolchain.cmake +1176 -0
- package/src/assets/cpp-runtime/browser.cpp +12 -0
- package/src/assets/cpp-runtime/commonBridges.cpp +17 -0
- package/src/assets/cpp-runtime/crossbindEmptySource.cpp +9 -0
- package/src/assets/cpp-runtime/node.cpp +19 -0
- package/src/assets/js-runtime/adapters/exception-decode.js +79 -0
- package/src/assets/js-runtime/adapters/fs-browser.js +173 -0
- package/src/assets/js-runtime/adapters/fs-node.js +28 -0
- package/src/assets/js-runtime/adapters/path-fs.js +6 -0
- package/src/assets/js-runtime/adapters/path-url.js +8 -0
- package/src/assets/js-runtime/adapters/vector-coercion.js +209 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +309 -0
- package/src/assets/js-runtime/browser.js +16 -0
- package/src/assets/js-runtime/core.js +193 -0
- package/src/assets/js-runtime/edge.js +18 -0
- package/src/assets/js-runtime/node.js +20 -0
- package/src/assets/packaging/crossbind-package.podspec +17 -0
- package/src/assets/wasi-runtime/stubs.c +84 -0
- package/src/bin.js +416 -0
- package/src/index.js +19 -0
- package/src/integration/getCrossbindScript.js +180 -0
- package/src/integration/getDependFilePath.js +132 -0
- package/src/runtime/wasiRun.mjs +118 -0
- package/src/state/calculateDependencyParameters.js +97 -0
- package/src/state/index.js +128 -0
- package/src/state/loadConfig.js +215 -0
- package/src/utils/appRustCrates.js +106 -0
- package/src/utils/binTools.js +76 -0
- package/src/utils/bridgeAsyncGuard.js +37 -0
- package/src/utils/cargoTarget.js +15 -0
- package/src/utils/cppDts.js +201 -0
- package/src/utils/cppFieldBindings.js +23 -0
- package/src/utils/dependencyRebuild.js +99 -0
- package/src/utils/dirLock.js +71 -0
- package/src/utils/downloadAndExtractFile.js +102 -0
- package/src/utils/familyManifest.js +17 -0
- package/src/utils/findFiles.js +5 -0
- package/src/utils/fixPackageName.js +7 -0
- package/src/utils/flattenConfigForTable.js +75 -0
- package/src/utils/getAbsolutePath.js +14 -0
- package/src/utils/getCMakeListsFilePath.js +17 -0
- package/src/utils/getOsUserAndGroupId.js +11 -0
- package/src/utils/getParentPath.js +12 -0
- package/src/utils/hash.js +33 -0
- package/src/utils/inputStamp.js +28 -0
- package/src/utils/licenseReport.js +94 -0
- package/src/utils/linkLayout.js +34 -0
- package/src/utils/loadJs.js +31 -0
- package/src/utils/loadJson.js +16 -0
- package/src/utils/logger.js +110 -0
- package/src/utils/ociArtifact.js +124 -0
- package/src/utils/overrideDependency.js +99 -0
- package/src/utils/provenance.js +54 -0
- package/src/utils/pullDockerImage.js +105 -0
- package/src/utils/replaceBasePathForDocker.js +18 -0
- package/src/utils/resolveEmbindRust.js +66 -0
- package/src/utils/runCargo.js +228 -0
- package/src/utils/rustBridgeGen.js +1013 -0
- package/src/utils/rustMt.js +76 -0
- package/src/utils/rustSysroot.js +171 -0
- package/src/utils/rustSysrootPin.js +11 -0
- package/src/utils/systemKeys.js +29 -0
- package/src/utils/targets.js +109 -0
- package/src/utils/wasiToolchain.js +47 -0
- package/src/utils/writeIfChanged.js +8 -0
- package/src/utils/writeJson.js +9 -0
- 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
|
+
}
|
package/vitest.config.js
ADDED
|
@@ -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
|
+
});
|