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,23 @@
|
|
|
1
|
+
// SWIG's -embind backend emits functions and constructors but no member variables, so
|
|
2
|
+
// public fields never reach JS from the generated bridge alone. This post-pass injects
|
|
3
|
+
// embind .property lines for the value-semantics fields parseCppSurface captured - the
|
|
4
|
+
// same post-processing precedent as bridgeAsyncGuard.
|
|
5
|
+
|
|
6
|
+
export function buildFieldPropertyLines(cls) {
|
|
7
|
+
return (cls.fields ?? []).map((f) => `.property("${f.name}", &${cls.name}::${f.name})`);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function injectFieldBindings(bridgeContent, model) {
|
|
11
|
+
let out = bridgeContent;
|
|
12
|
+
for (const cls of model.classes ?? []) {
|
|
13
|
+
const lines = buildFieldPropertyLines(cls)
|
|
14
|
+
.filter((line) => !out.includes(line));
|
|
15
|
+
if (!lines.length) continue;
|
|
16
|
+
// Lazy template match still lands on the right opener: the first `>(\s*"Name")`
|
|
17
|
+
// closes the template list even when it contains nested arguments like base<T>.
|
|
18
|
+
const opener = new RegExp(`(emscripten::class_<[\\s\\S]*?>\\s*\\(\\s*"${cls.name}"\\s*\\))`);
|
|
19
|
+
if (!opener.test(out)) continue;
|
|
20
|
+
out = out.replace(opener, `$1\n ${lines.join('\n ')}`);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import loadJson from './loadJson.js';
|
|
3
|
+
import { getOverrideKey } from './overrideDependency.js';
|
|
4
|
+
import { getContentHash } from './hash.js';
|
|
5
|
+
|
|
6
|
+
export function isCached(depsDir, targets, key, dep, hasIos) {
|
|
7
|
+
if (loadJson(`${depsDir}/.crossbind-rebuild.json`)?.key !== key) return false;
|
|
8
|
+
// buildLib writes this last; without it the dep is silently dropped from CMake params,
|
|
9
|
+
// so a half-written cache (killed build) must count as a miss and trigger a rebuild.
|
|
10
|
+
if (!fs.existsSync(`${depsDir}/dist/prebuilt/CMakeLists.txt`)) return false;
|
|
11
|
+
if (!targets.every((t) => fs.existsSync(`${depsDir}/dist/prebuilt/${t.path}/lib`))) return false;
|
|
12
|
+
if (hasIos && !dep.export.libName.every((n) => fs.existsSync(`${depsDir}/${n}.xcframework`))) return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getRebuildDeps(allDependencies, rebuildOption) {
|
|
17
|
+
const selector = resolveSelector(rebuildOption);
|
|
18
|
+
return allDependencies.filter((d) => {
|
|
19
|
+
if (d.rebuild || d.overrideBuild) return true;
|
|
20
|
+
if (!selector) return false;
|
|
21
|
+
if (selector === 'ALL') return true;
|
|
22
|
+
const names = [d.general?.name, d.package?.name, d.general?.alias?.package].filter(Boolean);
|
|
23
|
+
return names.some((n) => selector.includes(n));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveSelector(rebuildOption) {
|
|
28
|
+
const raw = rebuildOption !== undefined ? rebuildOption : process.env.CROSSBIND_REBUILD_DEPS;
|
|
29
|
+
if (raw === undefined || raw === null || raw === false || raw === '') return null;
|
|
30
|
+
if (raw === true || raw === '1' || raw === 'true' || raw === 'all') return 'ALL';
|
|
31
|
+
return String(raw).split(',').map((s) => s.trim()).filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function orderByDependencies(deps) {
|
|
35
|
+
const byProject = new Map(deps.map((d) => [d.paths.project, d]));
|
|
36
|
+
const result = [];
|
|
37
|
+
const visited = new Set();
|
|
38
|
+
const visit = (d) => {
|
|
39
|
+
if (visited.has(d.paths.project)) return;
|
|
40
|
+
visited.add(d.paths.project);
|
|
41
|
+
(d.allDependencies || []).forEach((sub) => {
|
|
42
|
+
const inSet = byProject.get(sub.paths.project);
|
|
43
|
+
if (inSet) visit(inSet);
|
|
44
|
+
});
|
|
45
|
+
result.push(d);
|
|
46
|
+
};
|
|
47
|
+
deps.forEach(visit);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function cleanDepsCache(cacheDir, dirNames) {
|
|
52
|
+
const depsRoot = `${cacheDir}/deps`;
|
|
53
|
+
if (!fs.existsSync(depsRoot)) return [];
|
|
54
|
+
const all = fs.readdirSync(depsRoot, { withFileTypes: true })
|
|
55
|
+
.filter((entry) => entry.isDirectory())
|
|
56
|
+
.map((entry) => entry.name);
|
|
57
|
+
const removed = dirNames?.length ? all.filter((n) => dirNames.includes(n)) : all;
|
|
58
|
+
removed.forEach((n) => {
|
|
59
|
+
fs.rmSync(`${depsRoot}/${n}`, { recursive: true, force: true });
|
|
60
|
+
fs.rmSync(`${depsRoot}/${n}.lock`, { force: true });
|
|
61
|
+
});
|
|
62
|
+
if (!dirNames?.length) fs.rmSync(depsRoot, { recursive: true, force: true });
|
|
63
|
+
return removed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Mirrors loadConfig's marker consumption: a dep counts iff its marker key equals the
|
|
67
|
+
// current override key. The stamp changes exactly when the consumed set changes, so
|
|
68
|
+
// callers (e.g. the RN gradle plugin) can use it as a CMake-configure cache key.
|
|
69
|
+
// Which target dirs a dependency currently ships, so a prebuilt that appears (or is rebuilt)
|
|
70
|
+
// after the last configure busts it too. Rebuilt-from-source deps carry a marker key; prebuilt
|
|
71
|
+
// ones carry nothing, and without this they used to stay invisible to the stamp - a refreshed
|
|
72
|
+
// artifact then never entered the link until the CMake cache was wiped by hand.
|
|
73
|
+
function prebuiltFingerprint(dep) {
|
|
74
|
+
const dir = dep.paths?.cmakeDir;
|
|
75
|
+
// No prebuilt at all contributes nothing, so a dependency set with neither markers nor
|
|
76
|
+
// artifacts still stamps as "none".
|
|
77
|
+
if (!dir || !fs.existsSync(dir)) return null;
|
|
78
|
+
const targets = fs.readdirSync(dir, { withFileTypes: true })
|
|
79
|
+
.filter((e) => e.isDirectory())
|
|
80
|
+
.map((e) => {
|
|
81
|
+
const stamp = fs.statSync(`${dir}/${e.name}`).mtimeMs;
|
|
82
|
+
return `${e.name}@${stamp}`;
|
|
83
|
+
})
|
|
84
|
+
.sort();
|
|
85
|
+
return `${dep.general.name}:${targets.join(',')}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function computeDependenciesStamp(allDependencies, cacheDir) {
|
|
89
|
+
const keys = allDependencies
|
|
90
|
+
.map((d) => {
|
|
91
|
+
const marker = loadJson(`${cacheDir}/deps/${d.general.name}/.crossbind-rebuild.json`);
|
|
92
|
+
return marker?.key === getOverrideKey(d) ? marker.key : null;
|
|
93
|
+
})
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.sort();
|
|
96
|
+
const prebuilts = allDependencies.map(prebuiltFingerprint).filter(Boolean).sort();
|
|
97
|
+
if (keys.length === 0 && prebuilts.length === 0) return 'none';
|
|
98
|
+
return getContentHash([...keys, ...prebuilts].join('|'));
|
|
99
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import logger from './logger.js';
|
|
4
|
+
|
|
5
|
+
// A dependency source build can legitimately run for a long time; anything older
|
|
6
|
+
// is assumed to be a crashed process whose lock must be broken.
|
|
7
|
+
const DEFAULT_STALE_MS = 60 * 60 * 1000;
|
|
8
|
+
const DEFAULT_POLL_MS = 2000;
|
|
9
|
+
|
|
10
|
+
const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
11
|
+
|
|
12
|
+
// Reads the PID recorded in the lock file and reports whether that process is
|
|
13
|
+
// still running: 'alive' (signal delivered, or owned by another user), 'dead'
|
|
14
|
+
// (no such process), or 'unknown' (missing/unparseable PID).
|
|
15
|
+
export function lockHolderStatus(lockPath) {
|
|
16
|
+
let pid;
|
|
17
|
+
try {
|
|
18
|
+
pid = Number.parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
|
|
19
|
+
} catch {
|
|
20
|
+
return 'unknown';
|
|
21
|
+
}
|
|
22
|
+
if (!Number.isInteger(pid) || pid <= 0) return 'unknown';
|
|
23
|
+
try {
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
return 'alive';
|
|
26
|
+
} catch (err) {
|
|
27
|
+
if (err.code === 'ESRCH') return 'dead';
|
|
28
|
+
if (err.code === 'EPERM') return 'alive';
|
|
29
|
+
return 'unknown';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default async function withDirLock(lockPath, fn, options = {}) {
|
|
34
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
35
|
+
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
36
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
37
|
+
|
|
38
|
+
let hasWarned = false;
|
|
39
|
+
for (;;) {
|
|
40
|
+
try {
|
|
41
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
42
|
+
fs.writeSync(fd, String(process.pid));
|
|
43
|
+
fs.closeSync(fd);
|
|
44
|
+
break;
|
|
45
|
+
} catch (e) {
|
|
46
|
+
if (e.code !== 'EEXIST') throw e;
|
|
47
|
+
const status = lockHolderStatus(lockPath);
|
|
48
|
+
const mtimeMs = fs.statSync(lockPath, { throwIfNoEntry: false })?.mtimeMs;
|
|
49
|
+
const mtimeStale = mtimeMs !== undefined && Date.now() - mtimeMs > staleMs;
|
|
50
|
+
// A provably-dead holder (e.g. a Ctrl-C that skipped the finally cleanup) is broken
|
|
51
|
+
// at once; a live holder is never stolen, even past the stale window; when the holder
|
|
52
|
+
// can't be determined we fall back to the mtime age.
|
|
53
|
+
if (status === 'dead' || (status !== 'alive' && mtimeStale)) {
|
|
54
|
+
logger.info(`crossbind: breaking stale lock ${lockPath} (holder ${status}).`);
|
|
55
|
+
fs.rmSync(lockPath, { force: true });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!hasWarned) {
|
|
59
|
+
logger.info(`crossbind: waiting for ${lockPath} (another build is working on this dependency)…`);
|
|
60
|
+
hasWarned = true;
|
|
61
|
+
}
|
|
62
|
+
await sleep(pollMs);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
return await fn();
|
|
68
|
+
} finally {
|
|
69
|
+
fs.rmSync(lockPath, { force: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs, { mkdirSync } from 'node:fs';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
|
|
8
|
+
export default async function downloadAndExtractFile(url, output, sha256) {
|
|
9
|
+
if (fs.existsSync(`${output}/source`)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
const filePath = await downloadFile(url, output);
|
|
13
|
+
verifyIntegrity(filePath, url, sha256);
|
|
14
|
+
extractArchive(filePath, url, output);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Extraction runs through the system tar (GNU tar and bsdtar both detect gzip/bzip2/xz, and
|
|
19
|
+
// every platform the engine builds on ships one). Extracting into a scratch directory keeps
|
|
20
|
+
// the archive from choosing where its files land, and gives the upstream root folder - which
|
|
21
|
+
// recipes address as build/source - one place to be renamed from.
|
|
22
|
+
function extractArchive(filePath, url, output) {
|
|
23
|
+
const work = `${output}/.extract`;
|
|
24
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
25
|
+
mkdirSync(work, { recursive: true });
|
|
26
|
+
try {
|
|
27
|
+
const tar = spawnSync('tar', ['-xf', filePath, '-C', work], { encoding: 'utf8' });
|
|
28
|
+
if (tar.error) {
|
|
29
|
+
throw new Error(`crossbind: cannot run tar to extract ${filePath}: ${tar.error.message}`);
|
|
30
|
+
}
|
|
31
|
+
if (tar.status !== 0) {
|
|
32
|
+
throw new Error(`crossbind: extracting ${filePath} (from ${url}) failed: ${(tar.stderr || '').trim() || `tar exited ${tar.status}`}`);
|
|
33
|
+
}
|
|
34
|
+
const entries = fs.readdirSync(work, { withFileTypes: true });
|
|
35
|
+
if (entries.length === 0) {
|
|
36
|
+
throw new Error(`crossbind: downloaded archive ${filePath} is empty or not a supported archive (from ${url}).`);
|
|
37
|
+
}
|
|
38
|
+
// Release tarballs carry one root folder; anything else is treated as the root itself.
|
|
39
|
+
const root = entries.length === 1 && entries[0].isDirectory() ? `${work}/${entries[0].name}` : work;
|
|
40
|
+
fs.renameSync(root, `${output}/source`);
|
|
41
|
+
} finally {
|
|
42
|
+
fs.rmSync(work, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Verifies the downloaded archive against the sha256 pinned in the build recipe. A missing pin
|
|
47
|
+
// is skipped (packages are pinned incrementally; check:sources flags the gaps); a MISMATCH
|
|
48
|
+
// refuses the build, so a hijacked mirror or a re-tagged upstream cannot feed arbitrary C++ to
|
|
49
|
+
// the compiler.
|
|
50
|
+
export function verifyIntegrity(filePath, url, sha256) {
|
|
51
|
+
if (!sha256) return;
|
|
52
|
+
const actual = crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
53
|
+
if (actual !== String(sha256).toLowerCase()) {
|
|
54
|
+
fs.rmSync(filePath, { force: true });
|
|
55
|
+
throw new Error(
|
|
56
|
+
`crossbind: source integrity check failed for ${url}\n`
|
|
57
|
+
+ ` expected sha256: ${sha256}\n`
|
|
58
|
+
+ ` actual sha256: ${actual}\n`
|
|
59
|
+
+ 'Refusing to build. If you intentionally bumped nativeVersion, update the recipe sha256.',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
|
|
65
|
+
|
|
66
|
+
// What lands here is compiled, so it may not arrive over a connection anyone can rewrite.
|
|
67
|
+
// Loopback is exempt so tests can serve fixtures without a certificate.
|
|
68
|
+
export function assertHttps(target, context = target) {
|
|
69
|
+
const { protocol, hostname } = new URL(target);
|
|
70
|
+
if (protocol !== 'https:' && !LOOPBACK.has(hostname)) {
|
|
71
|
+
throw new Error(`crossbind: refusing to download ${context} over ${protocol}// - upstream sources must come over https.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// fetch follows redirects itself, which release downloads rely on (github and the osgeo
|
|
76
|
+
// mirrors both bounce), so the download needs no redirect library of its own.
|
|
77
|
+
export async function downloadFile(url, folder) {
|
|
78
|
+
mkdirSync(folder, { recursive: true });
|
|
79
|
+
const dest = `${folder}/${path.basename(url)}`;
|
|
80
|
+
if (fs.existsSync(dest)) return dest;
|
|
81
|
+
|
|
82
|
+
assertHttps(url);
|
|
83
|
+
let response;
|
|
84
|
+
try {
|
|
85
|
+
response = await fetch(url, { headers: { 'User-Agent': 'curl/8.7.1' }, redirect: 'follow' });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
throw new Error(`crossbind: cannot reach ${url}: ${err.message}`, { cause: err });
|
|
88
|
+
}
|
|
89
|
+
// A redirect chain must not be able to downgrade the transport on its last hop.
|
|
90
|
+
if (response.url) assertHttps(response.url, `${url} (redirected to ${response.url})`);
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
throw new Error(`crossbind: download failed for ${url} — HTTP ${response.status}.`);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(dest));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// A half-written archive would fail its hash check later with a confusing message.
|
|
98
|
+
fs.rmSync(dest, { force: true });
|
|
99
|
+
throw new Error(`crossbind: download failed for ${url}: ${err.message}`, { cause: err });
|
|
100
|
+
}
|
|
101
|
+
return dest;
|
|
102
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
|
|
5
|
+
// Upstream truth (license, provenance identity) lives on the FAMILY package (contract D);
|
|
6
|
+
// variants depend on their family.
|
|
7
|
+
export default function familyManifestOf(node) {
|
|
8
|
+
const familyName = node.general?.alias?.package;
|
|
9
|
+
if (!familyName || !node.paths?.project) return null;
|
|
10
|
+
try {
|
|
11
|
+
const req = createRequire(path.join(node.paths.project, 'package.json'));
|
|
12
|
+
const manifestPath = req.resolve(`${familyName}/package.json`);
|
|
13
|
+
return { manifest: JSON.parse(fs.readFileSync(manifestPath, 'utf8')), dir: path.dirname(manifestPath) };
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
2
|
+
|
|
3
|
+
// Object maps with at least this many object-valued entries (e.g. allDependencyPaths,
|
|
4
|
+
// keyed per target) collapse to their key list instead of exploding into rows.
|
|
5
|
+
const LARGE_MAP_MIN_KEYS = 8;
|
|
6
|
+
|
|
7
|
+
function relativizePath(value, base) {
|
|
8
|
+
if (base && typeof value === 'string' && value.startsWith(`${base}/`)) {
|
|
9
|
+
return value.slice(base.length + 1);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function truncateMiddle(value, max) {
|
|
15
|
+
if (typeof value !== 'string' || value.length <= max) return value;
|
|
16
|
+
const head = Math.ceil((max - 1) / 2);
|
|
17
|
+
const tail = Math.floor((max - 1) / 2);
|
|
18
|
+
return `${value.slice(0, head)}…${value.slice(value.length - tail)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatScalar(value, base, maxLen) {
|
|
22
|
+
if (typeof value === 'function') return '[Function]';
|
|
23
|
+
if (typeof value === 'string') return truncateMiddle(relativizePath(value, base), maxLen);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Flattens a resolved crossbind config into a { 'dotted.key': displayValue } map so
|
|
28
|
+
// console.table renders a readable two-column view instead of one column per
|
|
29
|
+
// nested sub-key. Paths are shown relative to base and long values truncated in
|
|
30
|
+
// the middle; dependency arrays collapse to names, large per-target maps collapse
|
|
31
|
+
// to their keys, and other structures recurse with dotted/indexed keys.
|
|
32
|
+
export default function flattenConfigForTable(config, { base = null, maxDepth = 5, maxValueLength = 80 } = {}) {
|
|
33
|
+
const out = {};
|
|
34
|
+
|
|
35
|
+
const walk = (value, prefix, depth) => {
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
if (value.length === 0) {
|
|
38
|
+
out[prefix] = '[]';
|
|
39
|
+
} else if (value.every((v) => v === null || typeof v !== 'object')) {
|
|
40
|
+
out[prefix] = truncateMiddle(value.map((v) => relativizePath(v, base)).join(', '), maxValueLength);
|
|
41
|
+
} else {
|
|
42
|
+
const names = value.map((v) => v?.general?.name ?? v?.package?.name).filter(Boolean);
|
|
43
|
+
if (names.length === value.length) {
|
|
44
|
+
out[prefix] = names.join(', ');
|
|
45
|
+
} else if (depth <= 0) {
|
|
46
|
+
out[prefix] = `<${value.length} items>`;
|
|
47
|
+
} else {
|
|
48
|
+
value.forEach((v, i) => walk(v, `${prefix}.${i}`, depth - 1));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!isPlainObject(value)) {
|
|
55
|
+
out[prefix] = formatScalar(value, base, maxValueLength);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const keys = Object.keys(value);
|
|
60
|
+
const isLargeObjectMap = prefix !== '' && keys.length >= LARGE_MAP_MIN_KEYS
|
|
61
|
+
&& keys.every((k) => isPlainObject(value[k]));
|
|
62
|
+
if (keys.length === 0) {
|
|
63
|
+
out[prefix] = '{}';
|
|
64
|
+
} else if (isLargeObjectMap) {
|
|
65
|
+
out[prefix] = truncateMiddle(keys.join(', '), maxValueLength);
|
|
66
|
+
} else if (depth <= 0) {
|
|
67
|
+
out[prefix] = '{…}';
|
|
68
|
+
} else {
|
|
69
|
+
keys.forEach((k) => walk(value[k], prefix ? `${prefix}.${k}` : k, depth - 1));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
walk(config, '', maxDepth);
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import upath from 'upath';
|
|
2
|
+
|
|
3
|
+
export default function getAbsolutePath(projectPath, path) {
|
|
4
|
+
if (!path) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
if (upath.isAbsolute(path)) {
|
|
8
|
+
return upath.resolve(path);
|
|
9
|
+
}
|
|
10
|
+
if (projectPath) {
|
|
11
|
+
return upath.resolve(upath.join(upath.resolve(projectPath), path));
|
|
12
|
+
}
|
|
13
|
+
return upath.resolve(path);
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import getParentPath from './getParentPath.js';
|
|
2
|
+
import findFiles from './findFiles.js';
|
|
3
|
+
|
|
4
|
+
export default function getCMakeListsFilePath(basePath = process.cwd()) {
|
|
5
|
+
let temp = findFiles('CMakeLists.txt', { cwd: basePath });
|
|
6
|
+
if (temp.length === 0) {
|
|
7
|
+
temp = findFiles('*/CMakeLists.txt', { cwd: basePath, ignore: ['node_modules/*', 'dist/*', 'build/*'] });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (temp.length > 0) return temp[0];
|
|
11
|
+
return getCliCMakeListsFile();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getCliCMakeListsFile() {
|
|
15
|
+
const cliPath = getParentPath(getParentPath(import.meta.url));
|
|
16
|
+
return `${cliPath}/assets/cmake/CMakeLists.txt`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
|
|
3
|
+
let osUserAndGroupId;
|
|
4
|
+
export default function getOsUserAndGroupId() {
|
|
5
|
+
const userInfo = os.userInfo();
|
|
6
|
+
if (!osUserAndGroupId) {
|
|
7
|
+
const isInvalid = userInfo.uid === -1 && userInfo.gid === -1;
|
|
8
|
+
osUserAndGroupId = isInvalid ? '0:0' : `${userInfo.uid}:${userInfo.gid}`;
|
|
9
|
+
}
|
|
10
|
+
return osUserAndGroupId;
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as url from 'node:url';
|
|
2
|
+
import upath from 'upath';
|
|
3
|
+
|
|
4
|
+
export default function getParentPath(importUrl) {
|
|
5
|
+
let input = importUrl;
|
|
6
|
+
try {
|
|
7
|
+
input = url.fileURLToPath(input);
|
|
8
|
+
} catch (e) { /* empty */ }
|
|
9
|
+
const filename = upath.normalize(input);
|
|
10
|
+
const temp = filename.split('/'); temp.pop();
|
|
11
|
+
return temp.join('/');
|
|
12
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
export function getFileHash(path) {
|
|
5
|
+
const data = fs.readFileSync(path);
|
|
6
|
+
return getContentHash(data);
|
|
7
|
+
/* return new Promise((resolve, reject) => {
|
|
8
|
+
const hash = createHash('sha256');
|
|
9
|
+
const rs = fs.createReadStream(path);
|
|
10
|
+
rs.on('error', reject);
|
|
11
|
+
rs.on('data', (chunk) => hash.update(chunk));
|
|
12
|
+
rs.on('end', () => resolve(hash.digest('hex')));
|
|
13
|
+
}); */
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getContentHash(content) {
|
|
17
|
+
const hash = createHash('sha256');
|
|
18
|
+
hash.update(content);
|
|
19
|
+
return hash.digest('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Order-insensitive fingerprint of a file LIST (path + content per entry).
|
|
23
|
+
// Used as the Bridge lib cache key: the same lib dir is reused while the
|
|
24
|
+
// bridge set grows, so a lib built from a different set must not satisfy the
|
|
25
|
+
// cache. Missing files fingerprint as such instead of throwing.
|
|
26
|
+
export function getFilesFingerprint(paths) {
|
|
27
|
+
return getContentHash(
|
|
28
|
+
(paths || [])
|
|
29
|
+
.map((path) => `${path}:${fs.existsSync(path) ? getFileHash(path) : 'missing'}`)
|
|
30
|
+
.sort()
|
|
31
|
+
.join('\n'),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import upath from 'upath';
|
|
3
|
+
import findFiles from './findFiles.js';
|
|
4
|
+
import { getContentHash, getFileHash } from './hash.js';
|
|
5
|
+
|
|
6
|
+
const EXCLUDED_DIRS = ['node_modules', '.crossbind', 'android', 'ios', 'build', 'dist', '.git'];
|
|
7
|
+
|
|
8
|
+
export function collectInputFiles(roots, exts, extraFiles = []) {
|
|
9
|
+
// A single-entry brace set ({js}) is not brace-expanded by glob; use a plain suffix then.
|
|
10
|
+
const pattern = exts.length === 1 ? `**/*.${exts[0]}` : `**/*.{${exts.join(',')}}`;
|
|
11
|
+
const files = new Set();
|
|
12
|
+
[...new Set(roots)].forEach((root) => {
|
|
13
|
+
findFiles(pattern, {
|
|
14
|
+
cwd: root,
|
|
15
|
+
ignore: EXCLUDED_DIRS.map((dir) => `**/${dir}/**`),
|
|
16
|
+
}).forEach((file) => files.add(file));
|
|
17
|
+
const packageJson = upath.join(root, 'package.json');
|
|
18
|
+
if (fs.existsSync(packageJson)) files.add(packageJson);
|
|
19
|
+
});
|
|
20
|
+
extraFiles.forEach((file) => files.add(upath.normalize(file)));
|
|
21
|
+
return [...files].filter((file) => fs.existsSync(file)).sort();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function computeInputStamp(roots, exts, extraFiles, salt) {
|
|
25
|
+
const lines = collectInputFiles(roots, exts, extraFiles).map((file) => `${file}:${getFileHash(file)}`);
|
|
26
|
+
lines.push(salt);
|
|
27
|
+
return getContentHash(lines.join('\n'));
|
|
28
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import spdxParse from 'spdx-expression-parse';
|
|
2
|
+
|
|
3
|
+
const COPYLEFT_PATTERN = /\b(AGPL|LGPL|GPL|MPL|EPL|EUPL|CDDL)\b/i;
|
|
4
|
+
|
|
5
|
+
export function isCopyleft(expression) {
|
|
6
|
+
return COPYLEFT_PATTERN.test(expression || '');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function validateSpdx(expression) {
|
|
10
|
+
if (!expression || typeof expression !== 'string') {
|
|
11
|
+
return { isValid: false, error: 'missing license field' };
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
spdxParse(expression);
|
|
15
|
+
return { isValid: true };
|
|
16
|
+
} catch (e) {
|
|
17
|
+
return { isValid: false, error: e.message };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatNoticeSections(rows) {
|
|
22
|
+
return [...rows]
|
|
23
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
24
|
+
.map((row) => {
|
|
25
|
+
const lines = [`## ${row.name}${row.license ? ` — ${row.license}` : ''}`, ''];
|
|
26
|
+
if (row.npmName) lines.push(`- package: ${row.npmName}${row.version ? `@${row.version}` : ''}`);
|
|
27
|
+
if (row.nativeVersion) lines.push(`- native version: ${row.nativeVersion}`);
|
|
28
|
+
if (row.sourceUrl) lines.push(`- source: ${row.sourceUrl}${row.sha256 ? ` (sha256: ${row.sha256})` : ''}`);
|
|
29
|
+
if (row.licenseSelected) lines.push(`- license election: ${row.licenseSelected} (declared: ${row.licenseDeclared})`);
|
|
30
|
+
else if (/\bOR\b/.test(row.license || '')) lines.push(`- no license election recorded; the declared expression applies${row.licenseText ? ' and all texts are included' : ' (texts linked above)'}`);
|
|
31
|
+
if (row.licenseNotes) lines.push(`- note: ${row.licenseNotes}`);
|
|
32
|
+
if (row.licenseText) lines.push('', '```', row.licenseText.trim(), '```');
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function formatNoticesMarkdown(rows) {
|
|
38
|
+
const sections = formatNoticeSections(rows);
|
|
39
|
+
|
|
40
|
+
return [
|
|
41
|
+
'# Third-Party Notices',
|
|
42
|
+
'',
|
|
43
|
+
'GENERATED by `crossbind licenses` from recipe metadata (contract D/E) - do not edit.',
|
|
44
|
+
'',
|
|
45
|
+
'This artifact bundles the following native libraries through crossbind packages.',
|
|
46
|
+
'',
|
|
47
|
+
sections.join('\n\n'),
|
|
48
|
+
'',
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// K4 companion: a -bin artifact statically links every row, so its npm license
|
|
53
|
+
// field is the AND of all effective component licenses - derived, never typed.
|
|
54
|
+
export function deriveLicenseExpression(rows) {
|
|
55
|
+
const missing = rows.filter((row) => !row.license);
|
|
56
|
+
if (missing.length > 0) {
|
|
57
|
+
throw new Error(`crossbind: cannot derive a license expression - rows without a license: ${missing.map((row) => row.name).join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
const parts = [...new Set(rows.map((row) => (/\s/.test(row.license) ? `(${row.license})` : row.license)))]
|
|
60
|
+
.sort((a, b) => a.localeCompare(b));
|
|
61
|
+
const expression = parts.join(' AND ');
|
|
62
|
+
const { isValid, error } = validateSpdx(expression);
|
|
63
|
+
if (!isValid) {
|
|
64
|
+
throw new Error(`crossbind: derived license expression is not valid SPDX (${error}): ${expression}`);
|
|
65
|
+
}
|
|
66
|
+
return expression;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatCycloneDxSbom(rows, target = {}) {
|
|
70
|
+
return `${JSON.stringify({
|
|
71
|
+
bomFormat: 'CycloneDX',
|
|
72
|
+
specVersion: '1.5',
|
|
73
|
+
version: 1,
|
|
74
|
+
metadata: {
|
|
75
|
+
component: {
|
|
76
|
+
type: 'library',
|
|
77
|
+
name: target.name || 'unknown',
|
|
78
|
+
...(target.version ? { version: target.version } : {}),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
components: [...rows]
|
|
82
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
83
|
+
.map((row) => ({
|
|
84
|
+
type: 'library',
|
|
85
|
+
name: row.name,
|
|
86
|
+
...(row.nativeVersion ? { version: row.nativeVersion } : {}),
|
|
87
|
+
...(row.nativeVersion ? { purl: `pkg:generic/${row.name}@${row.nativeVersion}` } : {}),
|
|
88
|
+
...(row.license ? { licenses: [{ expression: row.license }] } : {}),
|
|
89
|
+
...(row.sha256 ? { hashes: [{ alg: 'SHA-256', content: row.sha256 }] } : {}),
|
|
90
|
+
...(row.sourceUrl ? { externalReferences: [{ type: 'distribution', url: row.sourceUrl }] } : {}),
|
|
91
|
+
...(row.licenseSelected ? { properties: [{ name: 'crossbind:license-declared', value: row.licenseDeclared }] } : {}),
|
|
92
|
+
})),
|
|
93
|
+
}, null, 2)}\n`;
|
|
94
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Assembles the lib section of the emcc link line. --whole-archive is applied
|
|
2
|
+
// only where symbol retention genuinely needs it: always the Bridge archive
|
|
3
|
+
// (its embind registrations live in static initializers that no symbol
|
|
4
|
+
// references), plus any archive opted in via config - `wholeArchive: true` on
|
|
5
|
+
// the project (legacy everything-wholesale layout) or on a dependency's own
|
|
6
|
+
// config, or names listed in the project's `wholeArchiveDependencies`.
|
|
7
|
+
// Everything else links normally so wasm-ld keeps just the referenced call
|
|
8
|
+
// graph instead of every archive member.
|
|
9
|
+
|
|
10
|
+
export function libNameOf(libPath) {
|
|
11
|
+
const base = libPath.split('/').pop();
|
|
12
|
+
const match = /^lib(.+)\.a$/.exec(base);
|
|
13
|
+
return match ? match[1] : base;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function buildLinkLibArgs(libs, { wholeArchiveAll = false, wholeArchiveNames = new Set(), wrapLast = true } = {}) {
|
|
17
|
+
const args = [];
|
|
18
|
+
let wrapped = false;
|
|
19
|
+
libs.forEach((lib, index) => {
|
|
20
|
+
// wrapLast covers the Bridge archive convention; wasi command links
|
|
21
|
+
// have no bridge (main() is the GC root), so they pass false.
|
|
22
|
+
const isBridge = wrapLast && index === libs.length - 1;
|
|
23
|
+
const wantWrap = wholeArchiveAll || isBridge || wholeArchiveNames.has(libNameOf(lib));
|
|
24
|
+
if (wantWrap !== wrapped) {
|
|
25
|
+
args.push(wantWrap ? '-Wl,--whole-archive' : '-Wl,--no-whole-archive');
|
|
26
|
+
wrapped = wantWrap;
|
|
27
|
+
}
|
|
28
|
+
args.push(lib);
|
|
29
|
+
});
|
|
30
|
+
if (wrapped) {
|
|
31
|
+
args.push('-Wl,--no-whole-archive');
|
|
32
|
+
}
|
|
33
|
+
return args;
|
|
34
|
+
}
|