crossbind 2.0.0-beta.62 → 2.0.0-beta.64
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 +2 -1
- package/README.md +2 -2
- package/package.json +7 -2
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildDependencies.js +15 -2
- package/src/actions/buildExternal.js +9 -1
- package/src/actions/buildJs.js +2 -9
- package/src/actions/buildLib.js +25 -8
- package/src/actions/buildNativeCommand.js +84 -0
- package/src/actions/buildNode.js +49 -10
- package/src/actions/buildTypes.js +8 -2
- package/src/actions/buildWasiCommand.js +10 -9
- package/src/actions/buildWasm.js +72 -31
- package/src/actions/cloudCommands.js +133 -0
- package/src/actions/createImportedBridges.js +71 -0
- package/src/actions/createInterface.js +247 -68
- package/src/actions/createLib.js +70 -42
- package/src/actions/getData.js +6 -2
- package/src/actions/getEnv.js +7 -0
- package/src/actions/getLinkInputs.js +12 -5
- package/src/actions/isSourceNewer.js +2 -0
- package/src/actions/licenses.js +143 -19
- package/src/actions/prepareConanDependencies.js +51 -0
- package/src/actions/publishBinary.js +32 -0
- package/src/actions/run.js +63 -66
- package/src/actions/runnerCommands.js +433 -0
- package/src/actions/writeRuntimeEntry.js +80 -0
- package/src/assets/cmake/command/CMakeLists.txt +21 -0
- package/src/assets/cmake/command/command.cpp +1 -0
- package/src/assets/cmake/dist.cmake +2 -1
- package/src/assets/configure/install.sh +22 -0
- package/src/assets/cpp-runtime/node.cpp +18 -1
- package/src/assets/js-runtime/adapters/path-url.js +6 -3
- package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +138 -60
- package/src/assets/js-runtime/core.js +12 -0
- package/src/assets/node-import-hooks.mjs +61 -0
- package/src/assets/toolchain-digests.json +19 -19
- package/src/assets/wasi-runtime/http/README.md +17 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
- package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
- package/src/assets/wasi-runtime/http/handler.cpp +298 -0
- package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
- package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
- package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
- package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
- package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
- package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
- package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
- package/src/assets/wasi-runtime/stubs.c +29 -0
- package/src/bin.js +218 -161
- package/src/cli.js +14 -0
- package/src/index.js +3 -0
- package/src/integration/bindHeaderImports.js +18 -0
- package/src/integration/getCrossbindScript.js +50 -13
- package/src/integration/getDependFilePath.js +9 -0
- package/src/integration/resolveNativeImport.js +14 -0
- package/src/node/dev.mjs +19 -0
- package/src/program.js +169 -0
- package/src/runner/blobs.js +135 -0
- package/src/runner/files.js +193 -0
- package/src/runner/server.js +320 -0
- package/src/state/attachConanDependencies.js +56 -0
- package/src/state/index.js +20 -11
- package/src/state/loadConfig.js +37 -35
- package/src/state/refreshBuiltDependencies.js +20 -0
- package/src/state/refreshConanDependencies.js +17 -0
- package/src/utils/androidToolchain.js +4 -0
- package/src/utils/appRustCrates.js +19 -1
- package/src/utils/appSources.js +97 -6
- package/src/utils/archiveFlags.js +12 -0
- package/src/utils/boundHeaders.js +79 -0
- package/src/utils/bridgeExtras.js +554 -0
- package/src/utils/bridgeRuntimeFixes.js +48 -0
- package/src/utils/bundledLicenses.js +26 -0
- package/src/utils/cargoLock.js +19 -0
- package/src/utils/cargoTarget.js +21 -3
- package/src/utils/cloudAccount.js +55 -0
- package/src/utils/conanDependencies.js +66 -0
- package/src/utils/conanImport.js +58 -0
- package/src/utils/conanInstall.js +154 -0
- package/src/utils/conanProfile.js +203 -0
- package/src/utils/conanStage.js +292 -0
- package/src/utils/copyToDistSource.js +13 -0
- package/src/utils/cppDts.js +60 -15
- package/src/utils/darwinToolchain.js +11 -0
- package/src/utils/dirLock.js +137 -28
- package/src/utils/downloadAndExtractFile.js +34 -7
- package/src/utils/embindArgumentGuards.js +41 -2
- package/src/utils/embindCallArguments.js +19 -0
- package/src/utils/embindJspiQueue.js +25 -0
- package/src/utils/embindObjectLifetime.js +35 -0
- package/src/utils/execContainer.js +49 -0
- package/src/utils/headerImports.js +16 -5
- package/src/utils/hostPlatform.js +21 -0
- package/src/utils/iosToolchain.js +6 -0
- package/src/utils/isSourceCmakePackage.js +10 -0
- package/src/utils/licenseReport.js +11 -2
- package/src/utils/linuxToolchain.js +24 -0
- package/src/utils/makeTreeWritable.js +12 -0
- package/src/utils/nativeImports.js +23 -0
- package/src/utils/nativeSourceStamp.js +36 -0
- package/src/utils/nodeDevBuild.js +92 -0
- package/src/utils/packageLicense.js +103 -0
- package/src/utils/pthreadMainScript.js +14 -0
- package/src/utils/pullDockerImage.js +3 -1
- package/src/utils/relocatePrebuilt.js +119 -0
- package/src/utils/remoteClient.js +127 -0
- package/src/utils/remoteOutputs.js +39 -0
- package/src/utils/remoteRunner.js +97 -0
- package/src/utils/remoteUpload.js +134 -0
- package/src/utils/replaceFile.js +8 -0
- package/src/utils/resetCMakeCacheForSource.js +26 -0
- package/src/utils/runCargo.js +48 -51
- package/src/utils/runConan.js +257 -0
- package/src/utils/runtimeEntries.js +56 -0
- package/src/utils/rustBridgeGen.js +22 -8
- package/src/utils/rustMt.js +9 -0
- package/src/utils/rustSysroot.js +7 -5
- package/src/utils/scopedEmbind.js +22 -0
- package/src/utils/selectRunner.js +81 -0
- package/src/utils/sourceFingerprint.js +4 -3
- package/src/utils/swigInterface.js +70 -4
- package/src/utils/systemConfig.js +13 -0
- package/src/utils/systemKeys.js +35 -3
- package/src/utils/targets.js +60 -4
- package/src/utils/toolchainNotices.js +5 -0
- package/src/utils/wasiHttp.js +50 -0
- package/src/utils/wasiToolchain.js +14 -1
- package/src/utils/windowsToolchain.js +18 -0
- package/vitest.config.js +11 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
// Shared by the client (utils/remoteClient.js) and the runner (server.js). The runner reaches the toolchain
|
|
6
|
+
// image as this folder alone, mounted or copied in, so nothing here may import from outside it.
|
|
7
|
+
|
|
8
|
+
export const sha256Of = (data) => crypto.createHash('sha256').update(data).digest('hex');
|
|
9
|
+
|
|
10
|
+
// `name` against a file name pattern: a trailing `*` matches any rest (`.env.*`).
|
|
11
|
+
const matchesName = (name, pattern) => (pattern.endsWith('*') ? name.startsWith(pattern.slice(0, -1)) : name === pattern);
|
|
12
|
+
|
|
13
|
+
// `rel` is relative to the root being walked, so a root that itself sits in node_modules (an installed port) still counts.
|
|
14
|
+
export function isExcluded(rel, rules) {
|
|
15
|
+
const segments = rel.split('/');
|
|
16
|
+
if (segments.some((segment) => rules.dirs.includes(segment))) return true;
|
|
17
|
+
if (rules.files?.some((pattern) => matchesName(segments.at(-1), pattern))) return true;
|
|
18
|
+
return rules.extensions.includes(path.posix.extname(rel));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Segment by segment: `*` matches any one segment, `*.a` any segment with that ending.
|
|
22
|
+
export function matchesPattern(rel, pattern) {
|
|
23
|
+
const parts = rel.split('/');
|
|
24
|
+
const wanted = pattern.split('/');
|
|
25
|
+
return parts.length === wanted.length && wanted.every((want, i) => want === '*'
|
|
26
|
+
|| (want.startsWith('*.') ? parts[i].endsWith(want.slice(1)) : want === parts[i]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A build tree such as a cargo target folder stays on the runner: a sync walk skips it, so it is neither
|
|
30
|
+
// uploaded nor deleted, and an output walk returns only the artifacts its `keep` patterns name.
|
|
31
|
+
function isServerOnly(base, rel, rules) {
|
|
32
|
+
const serverOnly = rules.serverOnly;
|
|
33
|
+
if (!serverOnly?.names.includes(path.posix.basename(rel))) return false;
|
|
34
|
+
return fs.existsSync(path.join(base, path.posix.dirname(rel), serverOnly.marker));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const isWithinReal = (real, parent) => real === parent || real.startsWith(`${parent}${path.sep}`);
|
|
38
|
+
|
|
39
|
+
// A link is followed only while it stays inside the base: one that leads out would dangle in a local container,
|
|
40
|
+
// and following it would send what it points at. A link back to a folder the walk is already inside never ends.
|
|
41
|
+
function statInside(full, realBase) {
|
|
42
|
+
const stat = fs.lstatSync(full);
|
|
43
|
+
if (!stat.isSymbolicLink()) return stat;
|
|
44
|
+
const real = fs.realpathSync(full);
|
|
45
|
+
if (!isWithinReal(real, realBase)) return null;
|
|
46
|
+
const target = fs.statSync(full);
|
|
47
|
+
return target.isDirectory() && isWithinReal(fs.realpathSync(path.dirname(full)), real) ? null : target;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Every file below the roots that the rules keep, keyed by its path relative to base, and every folder:
|
|
51
|
+
// the host creates folders before a step writes into them, empty ones included.
|
|
52
|
+
export function walk(base, roots, rules, { outputs = false } = {}) {
|
|
53
|
+
const files = new Map();
|
|
54
|
+
const dirs = [];
|
|
55
|
+
let realBase;
|
|
56
|
+
try {
|
|
57
|
+
realBase = fs.realpathSync(base);
|
|
58
|
+
} catch {
|
|
59
|
+
return { files, dirs };
|
|
60
|
+
}
|
|
61
|
+
const visit = (rel, inRoot, serverOnlyRoot) => {
|
|
62
|
+
let stat;
|
|
63
|
+
try {
|
|
64
|
+
stat = statInside(path.join(base, rel), realBase);
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!stat) return;
|
|
69
|
+
if (stat.isFile()) {
|
|
70
|
+
const kept = serverOnlyRoot === null
|
|
71
|
+
|| rules.serverOnly.keep.some((pattern) => matchesPattern(rel.slice(serverOnlyRoot.length + 1), pattern));
|
|
72
|
+
if (kept) files.set(rel, { size: stat.size, mtimeMs: stat.mtimeMs, mode: stat.mode & 0o777 });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!stat.isDirectory()) return;
|
|
76
|
+
let insideServerOnly = serverOnlyRoot;
|
|
77
|
+
if (insideServerOnly === null && isServerOnly(base, rel, rules)) {
|
|
78
|
+
if (!outputs) return;
|
|
79
|
+
insideServerOnly = rel;
|
|
80
|
+
}
|
|
81
|
+
if (insideServerOnly === null) dirs.push(rel);
|
|
82
|
+
for (const name of fs.readdirSync(path.join(base, rel)).sort()) {
|
|
83
|
+
const childInRoot = inRoot ? `${inRoot}/${name}` : name;
|
|
84
|
+
if (!isExcluded(childInRoot, rules)) visit(rel === '.' ? name : `${rel}/${name}`, childInRoot, insideServerOnly);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
roots.forEach((root) => visit(root, '', null));
|
|
88
|
+
return { files, dirs };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const snapshot = (base, roots, rules, options) => walk(base, roots, rules, options).files;
|
|
92
|
+
|
|
93
|
+
// Hashes reused while size and mtime are unchanged; `index` is updated in place and pruned to `files`.
|
|
94
|
+
export function hashIndex(base, files, index) {
|
|
95
|
+
const hashes = new Map();
|
|
96
|
+
for (const [rel, meta] of files) {
|
|
97
|
+
const cached = index.get(rel);
|
|
98
|
+
const sha256 = cached && cached.size === meta.size && cached.mtimeMs === meta.mtimeMs
|
|
99
|
+
? cached.sha256
|
|
100
|
+
: sha256Of(fs.readFileSync(path.join(base, rel)));
|
|
101
|
+
index.set(rel, { size: meta.size, mtimeMs: meta.mtimeMs, sha256 });
|
|
102
|
+
hashes.set(rel, sha256);
|
|
103
|
+
}
|
|
104
|
+
[...index.keys()].filter((rel) => !files.has(rel)).forEach((rel) => index.delete(rel));
|
|
105
|
+
return hashes;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A `*` segment stands for every folder at that level, so `b/*/p` names each conan package folder that exists.
|
|
109
|
+
export function expandRoots(base, roots) {
|
|
110
|
+
const step = (partials, segment) => partials.flatMap((partial) => {
|
|
111
|
+
if (segment !== '*') return [partial ? `${partial}/${segment}` : segment];
|
|
112
|
+
const dir = path.join(base, partial);
|
|
113
|
+
if (!fs.existsSync(dir)) return [];
|
|
114
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
115
|
+
.filter((entry) => entry.isDirectory())
|
|
116
|
+
.map((entry) => (partial ? `${partial}/${entry.name}` : entry.name))
|
|
117
|
+
.sort();
|
|
118
|
+
});
|
|
119
|
+
return roots.flatMap((root) => root.split('/').reduce(step, ['']));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The store units a machine already holds (a crate version, a conan package folder). They never change once
|
|
123
|
+
// written, so the runner sends only the units missing there.
|
|
124
|
+
export function presentUnits(base, outputRoots) {
|
|
125
|
+
return expandRoots(base, outputRoots.filter((root) => root.includes('*'))).filter((unit) => {
|
|
126
|
+
try {
|
|
127
|
+
return fs.readdirSync(path.join(base, unit)).length > 0;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Permission bits only: a synced file never becomes setuid, setgid or sticky.
|
|
135
|
+
export const modeOf = (mode) => (Number.isInteger(mode) ? mode & 0o777 : 0o644);
|
|
136
|
+
|
|
137
|
+
export function planSync(current, manifest) {
|
|
138
|
+
const write = Object.entries(manifest).filter(([rel, entry]) => current.get(rel) !== entry.sha256).map(([rel]) => rel);
|
|
139
|
+
const remove = [...current.keys()].filter((rel) => !(rel in manifest));
|
|
140
|
+
return { write, remove };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Written files get the current time as mtime, so make and ninja rebuild what depends on them.
|
|
144
|
+
export function applySync(base, plan, manifest, blobPathOf) {
|
|
145
|
+
plan.remove.forEach((rel) => fs.rmSync(path.join(base, rel), { force: true }));
|
|
146
|
+
plan.write.forEach((rel) => {
|
|
147
|
+
const dest = path.join(base, rel);
|
|
148
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
149
|
+
fs.rmSync(dest, { force: true });
|
|
150
|
+
fs.copyFileSync(blobPathOf(manifest[rel].sha256), dest);
|
|
151
|
+
fs.chmodSync(dest, modeOf(manifest[rel].mode));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function diffSnapshots(before, after) {
|
|
156
|
+
const changed = [...after].filter(([rel, meta]) => {
|
|
157
|
+
const old = before.get(rel);
|
|
158
|
+
return !old || old.size !== meta.size || old.mtimeMs !== meta.mtimeMs;
|
|
159
|
+
}).map(([rel]) => rel);
|
|
160
|
+
const removed = [...before.keys()].filter((rel) => !after.has(rel));
|
|
161
|
+
return { changed, removed };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Characters a host path would read as structure: a backslash is a separator on Windows, where a colon
|
|
165
|
+
// names a drive or a stream, and NUL ends a path anywhere.
|
|
166
|
+
const UNSAFE_PATH = process.platform === 'win32' ? /[\\:\0]/ : /[\\\0]/;
|
|
167
|
+
|
|
168
|
+
// The runner names files by container path; only a path inside one of the step's mounts maps back,
|
|
169
|
+
// so the runner cannot make the client write anywhere else.
|
|
170
|
+
export function hostPathOf(mounts, containerPath) {
|
|
171
|
+
const outside = () => new Error(`crossbind: the remote runner returned ${containerPath}, outside the mounts it was given.`);
|
|
172
|
+
const isClean = typeof containerPath === 'string' && !UNSAFE_PATH.test(containerPath)
|
|
173
|
+
&& path.posix.normalize(containerPath) === containerPath;
|
|
174
|
+
const mount = isClean && mounts.find((m) => containerPath.startsWith(`${m.container}/`));
|
|
175
|
+
if (!mount) throw outside();
|
|
176
|
+
const rel = containerPath.slice(mount.container.length + 1);
|
|
177
|
+
const file = path.join(mount.host, rel);
|
|
178
|
+
const back = path.relative(mount.host, file);
|
|
179
|
+
if (back === '..' || back.startsWith(`..${path.sep}`) || path.isAbsolute(back)) throw outside();
|
|
180
|
+
return { mount, rel, file };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Whether a step may hand this file back: it lies below one of the step's output roots, and below a store
|
|
184
|
+
// root (`registry/src/*/*`) only inside a unit the machine does not hold yet, as those never change.
|
|
185
|
+
export function isDeclaredOutput(rel, outputRoots, present = []) {
|
|
186
|
+
const parts = rel.split('/');
|
|
187
|
+
return outputRoots.some((root) => {
|
|
188
|
+
if (!root.includes('*')) return root === '.' || rel === root || rel.startsWith(`${root}/`);
|
|
189
|
+
const depth = root.split('/').length;
|
|
190
|
+
const unit = parts.slice(0, depth).join('/');
|
|
191
|
+
return parts.length > depth && matchesPattern(unit, root) && !present.includes(unit);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import readline from 'node:readline';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { pathToFileURL } from 'node:url';
|
|
9
|
+
import { createBlobStore, isHash, isUploadId } from './blobs.js';
|
|
10
|
+
import {
|
|
11
|
+
snapshot, hashIndex, planSync, applySync, diffSnapshots, expandRoots, sha256Of,
|
|
12
|
+
} from './files.js';
|
|
13
|
+
|
|
14
|
+
// The runner side of `RUNNER=REMOTE`: it runs inside a crossbind toolchain image and executes the
|
|
15
|
+
// toolchain steps a client would otherwise `docker run`, against folders it keeps in sync with the client.
|
|
16
|
+
|
|
17
|
+
// 2: a blob too large for one request arrives in parts.
|
|
18
|
+
export const PROTOCOL_VERSION = 2;
|
|
19
|
+
export const MIN_TOKEN_LENGTH = 16;
|
|
20
|
+
const MAX_JSON_BYTES = 16 * 1024 * 1024;
|
|
21
|
+
const INLINE_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
22
|
+
const MAX_MOUNTS = 8;
|
|
23
|
+
const DEFAULT_PORT = 8787;
|
|
24
|
+
const CONTENT_RANGE = /^bytes (\d+)-(\d+)\/(\d+)$/;
|
|
25
|
+
// A proxy in front of a hosted runner (fly, cloudflare) closes a response that stays quiet, and a compile
|
|
26
|
+
// step can print nothing for minutes.
|
|
27
|
+
const HEARTBEAT_MS = 15_000;
|
|
28
|
+
// Where crossbind mounts things into its images: the project base, and the cargo and conan caches.
|
|
29
|
+
const DEFAULT_MOUNT_PREFIXES = ['/tmp/crossbind/live', '/var/cache/crossbind'];
|
|
30
|
+
// cargo runs from a directory outside every mount so its upward config search finds nothing.
|
|
31
|
+
const DEFAULT_SCRATCH_DIRS = ['/tmp/crossbind-cargo'];
|
|
32
|
+
|
|
33
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string');
|
|
34
|
+
const isSafeRel = (rel) => typeof rel === 'string' && rel.length > 0 && !path.posix.isAbsolute(rel)
|
|
35
|
+
&& path.posix.normalize(rel) === rel && !rel.split('/').includes('..');
|
|
36
|
+
const isCleanAbsolute = (value) => typeof value === 'string' && path.posix.isAbsolute(value)
|
|
37
|
+
&& path.posix.normalize(value) === value && (value === '/' || !value.endsWith('/'));
|
|
38
|
+
const isWithin = (child, parent) => child === parent || child.startsWith(`${parent}/`);
|
|
39
|
+
const isUnderRoot = (rel, root) => root === '.' || isWithin(rel, root);
|
|
40
|
+
const isPlainName = (value) => typeof value === 'string' && /^[\w.-]+$/.test(value) && value !== '.' && value !== '..';
|
|
41
|
+
const isNamePattern = (value) => isPlainName(typeof value === 'string' && value.endsWith('*') ? value.slice(0, -1) : value);
|
|
42
|
+
// A build and a runner name one toolchain by digest even when they pull it from different registries (a mirror).
|
|
43
|
+
const digestOf = (ref) => (typeof ref === 'string' && ref.includes('@') ? ref.slice(ref.indexOf('@') + 1) : ref);
|
|
44
|
+
|
|
45
|
+
class RequestError extends Error {
|
|
46
|
+
constructor(status, message, extra = {}) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.status = status;
|
|
49
|
+
this.extra = extra;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function send(res, status, body) {
|
|
54
|
+
res.writeHead(status, { 'content-type': 'application/json' });
|
|
55
|
+
res.end(JSON.stringify(body));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function readJson(req) {
|
|
59
|
+
let size = 0;
|
|
60
|
+
const chunks = [];
|
|
61
|
+
for await (const chunk of req) {
|
|
62
|
+
size += chunk.length;
|
|
63
|
+
if (size > MAX_JSON_BYTES) throw new RequestError(413, 'request body too large');
|
|
64
|
+
chunks.push(chunk);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
68
|
+
} catch {
|
|
69
|
+
throw new RequestError(400, 'request body is not JSON');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function tokenMatches(header, token) {
|
|
74
|
+
const expected = Buffer.from(`Bearer ${token}`);
|
|
75
|
+
const actual = Buffer.from(header ?? '');
|
|
76
|
+
return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function validateRules(rules) {
|
|
80
|
+
if (!rules || !isStringArray(rules.dirs) || !isStringArray(rules.extensions)) throw new RequestError(400, 'rules must list dirs and extensions');
|
|
81
|
+
if (rules.files !== undefined && !(isStringArray(rules.files) && rules.files.every(isNamePattern))) {
|
|
82
|
+
throw new RequestError(400, 'rules.files must list file names, each optionally ending in *');
|
|
83
|
+
}
|
|
84
|
+
const { serverOnly } = rules;
|
|
85
|
+
if (serverOnly === undefined) return;
|
|
86
|
+
if (!isStringArray(serverOnly.names) || !serverOnly.names.every(isPlainName) || !isPlainName(serverOnly.marker) || !isStringArray(serverOnly.keep)) {
|
|
87
|
+
throw new RequestError(400, 'rules.serverOnly must name folders, a marker file and keep patterns');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateMount(mount, mountPrefixes) {
|
|
92
|
+
if (!mount || !isCleanAbsolute(mount.path) || !mountPrefixes.some((prefix) => isWithin(mount.path, prefix))) {
|
|
93
|
+
throw new RequestError(400, `mount ${mount?.path} is not one this runner allows`);
|
|
94
|
+
}
|
|
95
|
+
const { roots, outputRoots, manifest, dirs } = mount;
|
|
96
|
+
if (!isStringArray(roots) || !roots.every(isSafeRel)) throw new RequestError(400, 'roots must be relative paths inside the mount');
|
|
97
|
+
if (!isStringArray(outputRoots) || !outputRoots.every(isSafeRel)) throw new RequestError(400, 'outputRoots must be relative paths inside the mount');
|
|
98
|
+
if (!manifest || typeof manifest !== 'object') throw new RequestError(400, 'manifest must be an object');
|
|
99
|
+
for (const [rel, entry] of Object.entries(manifest)) {
|
|
100
|
+
if (!isSafeRel(rel) || !roots.some((root) => isUnderRoot(rel, root))) throw new RequestError(400, `manifest path ${rel} is outside the roots`);
|
|
101
|
+
if (!isHash(entry?.sha256)) throw new RequestError(400, `manifest entry ${rel} has no sha256`);
|
|
102
|
+
}
|
|
103
|
+
if (!isStringArray(dirs) || !dirs.every((rel) => isSafeRel(rel) && roots.some((root) => isUnderRoot(rel, root)))) {
|
|
104
|
+
throw new RequestError(400, 'dirs must be folders inside the roots');
|
|
105
|
+
}
|
|
106
|
+
if (mount.present !== undefined && !(isStringArray(mount.present) && mount.present.every(isSafeRel))) {
|
|
107
|
+
throw new RequestError(400, 'present must list relative folders');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateExec(body, { mountPrefixes, scratchDirs, image, role }) {
|
|
112
|
+
if (body.role !== role) throw new RequestError(409, `this runner serves the ${role} toolchain, not ${body.role}`);
|
|
113
|
+
if (image && digestOf(body.image) !== digestOf(image)) throw new RequestError(409, `toolchain mismatch: runner has ${image}, build pins ${body.image}`);
|
|
114
|
+
validateRules(body.rules);
|
|
115
|
+
const { mounts, cwd, argv, env } = body;
|
|
116
|
+
if (!Array.isArray(mounts) || mounts.length === 0 || mounts.length > MAX_MOUNTS) throw new RequestError(400, `mounts must list 1 to ${MAX_MOUNTS} folders`);
|
|
117
|
+
mounts.forEach((mount) => validateMount(mount, mountPrefixes));
|
|
118
|
+
if (mounts.some((a, i) => mounts.some((b, j) => i !== j && isWithin(a.path, b.path)))) throw new RequestError(400, 'mounts must not overlap');
|
|
119
|
+
const allowedCwd = isCleanAbsolute(cwd) && [...mounts.map((mount) => mount.path), ...scratchDirs].some((dir) => isWithin(cwd, dir));
|
|
120
|
+
if (!allowedCwd) throw new RequestError(400, 'cwd must be inside a mount or a scratch folder');
|
|
121
|
+
if (!isStringArray(argv) || argv.length === 0) throw new RequestError(400, 'argv must be a non-empty string array');
|
|
122
|
+
if (!env || typeof env !== 'object' || !Object.values(env).every((value) => typeof value === 'string')) throw new RequestError(400, 'env must map names to strings');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// A command never sees the runner's own settings, its token above all.
|
|
126
|
+
const commandEnv = (env) => ({
|
|
127
|
+
...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith('CROSSBIND_RUNNER_'))),
|
|
128
|
+
...env,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// The job count a client wrote for its own machine means nothing here: a step runs as many jobs as this
|
|
132
|
+
// runner has cores.
|
|
133
|
+
const withJobs = (argv, jobs) => argv.map((arg, i) => {
|
|
134
|
+
if (/^-j\d+$/.test(arg)) return `-j${jobs}`;
|
|
135
|
+
return /^\d+$/.test(arg) && argv[i - 1] === '-j' ? String(jobs) : arg;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
function runCommand({ argv: given, cwd, env }, line, jobs) {
|
|
139
|
+
const argv = withJobs(given, jobs);
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
const child = spawn(argv[0], argv.slice(1), { cwd, env: commandEnv(env) });
|
|
142
|
+
child.stdout.on('data', (data) => line({ stdout: data.toString() }));
|
|
143
|
+
child.stderr.on('data', (data) => line({ stderr: data.toString() }));
|
|
144
|
+
child.on('error', (error) => {
|
|
145
|
+
line({ stderr: `crossbind runner: ${error.message}\n` });
|
|
146
|
+
resolve(127);
|
|
147
|
+
});
|
|
148
|
+
child.on('close', (code) => resolve(code ?? 1));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createRunnerServer({
|
|
153
|
+
mountPrefixes = DEFAULT_MOUNT_PREFIXES, scratchDirs = DEFAULT_SCRATCH_DIRS, blobDir, token, image, role, heartbeatMs = HEARTBEAT_MS,
|
|
154
|
+
jobs = os.availableParallelism(),
|
|
155
|
+
}) {
|
|
156
|
+
if (!token) throw new Error('crossbind runner: a token is required - the runner runs build commands for whoever holds it.');
|
|
157
|
+
if (token.length < MIN_TOKEN_LENGTH) throw new Error(`crossbind runner: the token needs at least ${MIN_TOKEN_LENGTH} characters.`);
|
|
158
|
+
const blobs = createBlobStore(blobDir);
|
|
159
|
+
const indexes = new Map();
|
|
160
|
+
const indexFor = (mountPath) => {
|
|
161
|
+
if (!indexes.has(mountPath)) indexes.set(mountPath, new Map());
|
|
162
|
+
return indexes.get(mountPath);
|
|
163
|
+
};
|
|
164
|
+
let queue = Promise.resolve();
|
|
165
|
+
|
|
166
|
+
function syncMount(mount, rules) {
|
|
167
|
+
fs.mkdirSync(mount.path, { recursive: true });
|
|
168
|
+
const current = hashIndex(mount.path, snapshot(mount.path, mount.roots, rules), indexFor(mount.path));
|
|
169
|
+
applySync(mount.path, planSync(current, mount.manifest), mount.manifest, blobs.pathOf);
|
|
170
|
+
// The host creates folders before a step writes into them (`-o build/bridge/x.cpp`), so even empty ones travel.
|
|
171
|
+
mount.dirs.forEach((rel) => fs.mkdirSync(path.join(mount.path, rel), { recursive: true }));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// One command at a time: each mount is a single tree, as with a local docker run.
|
|
175
|
+
async function execute(body, line) {
|
|
176
|
+
body.mounts.forEach((mount) => syncMount(mount, body.rules));
|
|
177
|
+
// Outputs come back whatever their type: the glue JavaScript a link writes is the build's product.
|
|
178
|
+
const outputRules = { ...body.rules, extensions: [] };
|
|
179
|
+
const isStore = (root) => root.includes('*');
|
|
180
|
+
const outputsOf = (mount) => snapshot(mount.path, mount.outputRoots.filter((root) => !isStore(root)), outputRules, { outputs: true });
|
|
181
|
+
// A store unit (a crate version, a conan package folder) never changes once written, so every unit the
|
|
182
|
+
// client does not report travels whole, touched by this command or not, and the rest are not walked.
|
|
183
|
+
const storeFilesOf = (mount) => {
|
|
184
|
+
const present = new Set(mount.present ?? []);
|
|
185
|
+
const missing = expandRoots(mount.path, mount.outputRoots.filter(isStore)).filter((unit) => !present.has(unit));
|
|
186
|
+
return snapshot(mount.path, missing, outputRules, { outputs: true });
|
|
187
|
+
};
|
|
188
|
+
const before = body.mounts.map(outputsOf);
|
|
189
|
+
fs.mkdirSync(body.cwd, { recursive: true });
|
|
190
|
+
const exit = await runCommand(body, line, jobs);
|
|
191
|
+
|
|
192
|
+
const outputs = {};
|
|
193
|
+
const removed = [];
|
|
194
|
+
body.mounts.forEach((mount, i) => {
|
|
195
|
+
const diffed = outputsOf(mount);
|
|
196
|
+
const store = storeFilesOf(mount);
|
|
197
|
+
const after = new Map([...diffed, ...store]);
|
|
198
|
+
const diff = diffSnapshots(before[i], diffed);
|
|
199
|
+
new Set([...diff.changed, ...store.keys()]).forEach((rel) => {
|
|
200
|
+
const file = `${mount.path}/${rel}`;
|
|
201
|
+
const data = fs.readFileSync(file);
|
|
202
|
+
const entry = { sha256: blobs.putBuffer(data), mode: after.get(rel).mode, size: data.length };
|
|
203
|
+
outputs[file] = entry;
|
|
204
|
+
// Small outputs ride along in the stream; a larger one is fetched by its hash afterwards.
|
|
205
|
+
if (data.length <= INLINE_OUTPUT_BYTES) line({ file, ...entry, data: data.toString('base64') });
|
|
206
|
+
});
|
|
207
|
+
removed.push(...diff.removed.map((rel) => `${mount.path}/${rel}`));
|
|
208
|
+
});
|
|
209
|
+
return { exit, outputs, removed };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function handleExec(req, res) {
|
|
213
|
+
const body = await readJson(req);
|
|
214
|
+
validateExec(body, {
|
|
215
|
+
mountPrefixes, scratchDirs, image, role,
|
|
216
|
+
});
|
|
217
|
+
const hashes = body.mounts.flatMap((mount) => Object.values(mount.manifest).map((entry) => entry.sha256));
|
|
218
|
+
const missing = [...new Set(hashes)].filter((hash) => !blobs.has(hash));
|
|
219
|
+
if (missing.length > 0) throw new RequestError(409, 'missing blobs', { missing });
|
|
220
|
+
// The answer starts at once and heartbeats until the end: a step can wait behind another one longer than
|
|
221
|
+
// a proxy, or the client's fetch, waits for a response.
|
|
222
|
+
res.writeHead(200, { 'content-type': 'application/x-ndjson' });
|
|
223
|
+
res.flushHeaders();
|
|
224
|
+
const line = (record) => res.write(`${JSON.stringify(record)}\n`);
|
|
225
|
+
const heartbeat = setInterval(() => line({ heartbeat: true }), heartbeatMs);
|
|
226
|
+
try {
|
|
227
|
+
const run = queue.then(() => execute(body, line));
|
|
228
|
+
queue = run.catch(() => {});
|
|
229
|
+
res.end(`${JSON.stringify(await run)}\n`);
|
|
230
|
+
} finally {
|
|
231
|
+
clearInterval(heartbeat);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// One JSON line per blob ({sha256, data: base64}), so a cold start uploads its inputs in a few requests.
|
|
236
|
+
async function handleBatchUpload(req, res) {
|
|
237
|
+
let stored = 0;
|
|
238
|
+
for await (const text of readline.createInterface({ input: req, crlfDelay: Infinity })) {
|
|
239
|
+
if (!text) continue;
|
|
240
|
+
const { sha256, data } = JSON.parse(text);
|
|
241
|
+
const bytes = Buffer.from(data ?? '', 'base64');
|
|
242
|
+
if (!isHash(sha256) || sha256Of(bytes) !== sha256) throw new RequestError(400, `content does not match ${sha256}`);
|
|
243
|
+
blobs.putBuffer(bytes);
|
|
244
|
+
stored += 1;
|
|
245
|
+
}
|
|
246
|
+
send(res, 201, { stored });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function route(req, res) {
|
|
250
|
+
const url = new URL(req.url, 'http://runner');
|
|
251
|
+
if (req.method === 'GET' && url.pathname === '/v1/health') return send(res, 200, { ok: true, protocol: PROTOCOL_VERSION, role, image });
|
|
252
|
+
if (!tokenMatches(req.headers.authorization, token)) return send(res, 401, { error: 'invalid token' });
|
|
253
|
+
|
|
254
|
+
if (req.method === 'POST' && url.pathname === '/v1/blobs') return handleBatchUpload(req, res);
|
|
255
|
+
const blobMatch = url.pathname.match(/^\/v1\/blobs\/([^/]+)$/);
|
|
256
|
+
if (blobMatch) {
|
|
257
|
+
const hash = decodeURIComponent(blobMatch[1]);
|
|
258
|
+
if (!isHash(hash)) return send(res, 400, { error: 'blob names are sha256 digests' });
|
|
259
|
+
if (req.method === 'PUT' && req.headers['content-range'] !== undefined) {
|
|
260
|
+
const range = CONTENT_RANGE.exec(req.headers['content-range']);
|
|
261
|
+
const [start, end, total] = range ? range.slice(1).map(Number) : [];
|
|
262
|
+
const upload = req.headers['x-crossbind-upload'];
|
|
263
|
+
if (!range || start > end || end >= total || !isUploadId(upload)) {
|
|
264
|
+
return send(res, 400, { error: 'a part needs a content-range of bytes start-end/total and an x-crossbind-upload id' });
|
|
265
|
+
}
|
|
266
|
+
const result = await blobs.putPart(hash, upload, { start, end, total }, req);
|
|
267
|
+
if (result.status === 'stored') return send(res, 201, { stored: hash });
|
|
268
|
+
if (result.status === 'partial') return send(res, 202, { received: result.received });
|
|
269
|
+
if (result.status === 'gap') return send(res, 409, { error: `the upload holds ${result.received} bytes, so its next part starts there`, received: result.received });
|
|
270
|
+
return send(res, 400, { error: result.status === 'short' ? 'the part is not as long as its content-range says' : 'content does not match the hash' });
|
|
271
|
+
}
|
|
272
|
+
if (req.method === 'PUT') {
|
|
273
|
+
return (await blobs.putStream(hash, req)) ? send(res, 201, { stored: hash }) : send(res, 400, { error: 'content does not match the hash' });
|
|
274
|
+
}
|
|
275
|
+
if (req.method === 'GET') {
|
|
276
|
+
if (!blobs.has(hash)) return send(res, 404, { error: 'unknown blob' });
|
|
277
|
+
res.writeHead(200, { 'content-type': 'application/octet-stream' });
|
|
278
|
+
return fs.createReadStream(blobs.pathOf(hash)).pipe(res);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (req.method === 'POST' && url.pathname === '/v1/missing') {
|
|
282
|
+
const { hashes } = await readJson(req);
|
|
283
|
+
if (!isStringArray(hashes) || !hashes.every(isHash)) return send(res, 400, { error: 'hashes must be sha256 digests' });
|
|
284
|
+
return send(res, 200, { missing: hashes.filter((hash) => !blobs.has(hash)) });
|
|
285
|
+
}
|
|
286
|
+
if (req.method === 'POST' && url.pathname === '/v1/exec') return handleExec(req, res);
|
|
287
|
+
return send(res, 404, { error: 'not found' });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return http.createServer((req, res) => {
|
|
291
|
+
route(req, res).catch((error) => {
|
|
292
|
+
if (res.headersSent) {
|
|
293
|
+
res.end(`${JSON.stringify({ exit: 1, error: error.message })}\n`);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
send(res, error.status ?? 500, { error: error.message, ...error.extra });
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const listFromEnv = (value, fallback) => (value ? value.split(',').map((item) => item.trim()).filter(Boolean) : fallback);
|
|
302
|
+
|
|
303
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
304
|
+
const token = process.env.CROSSBIND_RUNNER_TOKEN;
|
|
305
|
+
delete process.env.CROSSBIND_RUNNER_TOKEN;
|
|
306
|
+
if (!token || token.length < MIN_TOKEN_LENGTH) {
|
|
307
|
+
process.stderr.write(`crossbind runner: set CROSSBIND_RUNNER_TOKEN to at least ${MIN_TOKEN_LENGTH} characters - the runner runs build commands for whoever holds it.\n`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
const server = createRunnerServer({
|
|
311
|
+
mountPrefixes: listFromEnv(process.env.CROSSBIND_RUNNER_MOUNT_PREFIXES, DEFAULT_MOUNT_PREFIXES),
|
|
312
|
+
scratchDirs: listFromEnv(process.env.CROSSBIND_RUNNER_SCRATCH_DIRS, DEFAULT_SCRATCH_DIRS),
|
|
313
|
+
blobDir: process.env.CROSSBIND_RUNNER_BLOBS || '/tmp/crossbind-runner/blobs',
|
|
314
|
+
token,
|
|
315
|
+
image: process.env.CROSSBIND_RUNNER_IMAGE || '',
|
|
316
|
+
role: process.env.CROSSBIND_RUNNER_ROLE || 'web',
|
|
317
|
+
});
|
|
318
|
+
const port = Number(process.env.CROSSBIND_RUNNER_PORT || DEFAULT_PORT);
|
|
319
|
+
server.listen(port, '0.0.0.0', () => process.stdout.write(`crossbind runner (${process.env.CROSSBIND_RUNNER_ROLE || 'web'}) listening on ${port}\n`));
|
|
320
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getFilledConfig, flattenDependencies } from './loadConfig.js';
|
|
2
|
+
import calculateDependencyParameters from './calculateDependencyParameters.js';
|
|
3
|
+
import { conanStageDir, conanPackageDir } from '../utils/conanImport.js';
|
|
4
|
+
import { conanDependenciesKey } from '../utils/conanDependencies.js';
|
|
5
|
+
import { readConanManifest, conanTargetName } from '../utils/conanStage.js';
|
|
6
|
+
import { TARGETS, targetPathOf } from '../utils/targets.js';
|
|
7
|
+
|
|
8
|
+
// What a recipe declares for its consumers, per staged target since it declares other things on another
|
|
9
|
+
// platform: the defines your code compiles with, and the system libraries and frameworks a Node.js addon
|
|
10
|
+
// or a native executable links. A framework goes as one linker argument: CMake collapses a repeated
|
|
11
|
+
// -framework. A debug build takes the release packages, so the build type is left out, and a target's
|
|
12
|
+
// specs stay one object, as getData takes them.
|
|
13
|
+
function targetSpecsOf(name, targets) {
|
|
14
|
+
return Object.entries(targets).flatMap(([targetPath, packages]) => {
|
|
15
|
+
const target = TARGETS.find((t) => targetPathOf(t) === targetPath);
|
|
16
|
+
const pkg = packages.find((entry) => entry.name === name);
|
|
17
|
+
if (!target || !pkg) return [];
|
|
18
|
+
const compileOptions = pkg.defines.map((define) => `-D${define}`);
|
|
19
|
+
const addonFlags = [...pkg.frameworks.map((framework) => `-Wl,-framework,${framework}`), ...pkg.systemLibs.map((lib) => `-l${lib}`)];
|
|
20
|
+
const specs = {
|
|
21
|
+
...(compileOptions.length > 0 ? { cmake: { compileOptions } } : {}),
|
|
22
|
+
...(addonFlags.length > 0 ? { binary: { addonFlags } } : {}),
|
|
23
|
+
};
|
|
24
|
+
if (Object.keys(specs).length === 0) return [];
|
|
25
|
+
return [{
|
|
26
|
+
platform: target.platform, arch: target.arch, runtime: target.runtime, specs,
|
|
27
|
+
}];
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Every library a recipe names on some target (libpng is png16 on Windows): a target links the archives
|
|
32
|
+
// staged for it, so a name it lacks drops out there.
|
|
33
|
+
const libsOf = (name, targets) => [...new Set(Object.values(targets).flat().filter((entry) => entry.name === name).flatMap((entry) => entry.libs))];
|
|
34
|
+
|
|
35
|
+
export function conanDependencyConfigs(manifest, stageDir) {
|
|
36
|
+
return manifest.packages.map((pkg) => ({
|
|
37
|
+
general: { name: conanTargetName(pkg.name), conan: pkg },
|
|
38
|
+
export: { type: 'cmake', libName: libsOf(pkg.name, manifest.targets) },
|
|
39
|
+
paths: { project: conanPackageDir(stageDir, pkg.name), output: `${conanPackageDir(stageDir, pkg.name)}/dist` },
|
|
40
|
+
targetSpecs: targetSpecsOf(pkg.name, manifest.targets),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The staged Conan packages join the config as dependencies, so header resolution, the link, the
|
|
45
|
+
// cmake graph and the license rows see them like any port. Only packages staged for the current
|
|
46
|
+
// conanDependencies join; reading the manifest runs nothing, so state can do it while it loads.
|
|
47
|
+
export default function attachConanDependencies(config) {
|
|
48
|
+
const isDeclared = Object.keys(config.conanDependencies ?? {}).length > 0;
|
|
49
|
+
if (!isDeclared && !config.dependencies?.some((d) => d.general.conan)) return;
|
|
50
|
+
const stageDir = conanStageDir(config.paths.cache);
|
|
51
|
+
const manifest = isDeclared ? readConanManifest(stageDir, conanDependenciesKey(config.conanDependencies)) : null;
|
|
52
|
+
const conan = manifest ? conanDependencyConfigs(manifest, stageDir).map((d) => getFilledConfig(d, { isDepend: true })) : [];
|
|
53
|
+
config.dependencies = [...config.dependencies.filter((d) => !d.general.conan), ...conan];
|
|
54
|
+
config.allDependencies = flattenDependencies(config.dependencies);
|
|
55
|
+
config.dependencyParameters = calculateDependencyParameters(config);
|
|
56
|
+
}
|
package/src/state/index.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import loadJson from '../utils/loadJson.js';
|
|
2
3
|
import writeJson from '../utils/writeJson.js';
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
TARGETS, targetPathOf, nodeAddonNamesOf, nativeCommandNamesOf, wasiNamesOf,
|
|
6
|
+
} from '../utils/targets.js';
|
|
4
7
|
import { findCargoModuleImportsIn, writeCargoMarker } from '../utils/cargoImport.js';
|
|
5
|
-
import
|
|
8
|
+
import { exportWasiSdkPath } from '../utils/wasiToolchain.js';
|
|
9
|
+
import loadConfig, { assertBinarySelection } from './loadConfig.js';
|
|
10
|
+
import refreshConanDependencies from './refreshConanDependencies.js';
|
|
6
11
|
|
|
7
12
|
const cacheDir = `${process.cwd()}/.crossbind`;
|
|
8
13
|
|
|
@@ -21,12 +26,9 @@ await initProcessState();
|
|
|
21
26
|
async function initProcessState() {
|
|
22
27
|
state.cache = loadCacheState();
|
|
23
28
|
state.config = await loadConfig();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (state.config.system.WASI_SDK_PATH && !process.env.CROSSBIND_WASI_SDK_PATH) {
|
|
28
|
-
process.env.CROSSBIND_WASI_SDK_PATH = state.config.system.WASI_SDK_PATH;
|
|
29
|
-
}
|
|
29
|
+
assertBinarySelection(state.config);
|
|
30
|
+
refreshConanDependencies(state.config);
|
|
31
|
+
exportWasiSdkPath(state.config.system);
|
|
30
32
|
// Same idea for the docker image overrides: pullDockerImage.js resolves them from the
|
|
31
33
|
// environment alone, so it stays free of a state import (and of the cycle that would create).
|
|
32
34
|
Object.entries({
|
|
@@ -52,12 +54,14 @@ async function initProcessState() {
|
|
|
52
54
|
target.dataTxtName = `${state.config.general.name}-${target.path}.${target.runtimeEnv}.data.txt`;
|
|
53
55
|
}
|
|
54
56
|
if (target.platform === 'wasi') {
|
|
55
|
-
|
|
56
|
-
target.wasmName = `${state.config.general.name}-${target.path}.wasm`;
|
|
57
|
+
Object.assign(target, wasiNamesOf(target, state.config.general.name));
|
|
57
58
|
}
|
|
58
59
|
if (target.runtimeEnv === 'node' && target.platform !== 'wasm') {
|
|
59
60
|
Object.assign(target, nodeAddonNamesOf(target, state.config.general.name));
|
|
60
61
|
}
|
|
62
|
+
if (target.runtimeEnv === 'native') {
|
|
63
|
+
Object.assign(target, nativeCommandNamesOf(target, state.config.general.name));
|
|
64
|
+
}
|
|
61
65
|
});
|
|
62
66
|
|
|
63
67
|
setAllDependecyPaths();
|
|
@@ -89,7 +93,12 @@ export function setAllDependecyPaths() {
|
|
|
89
93
|
state.config.allDependencyPaths[target.path] = { cmake: {} };
|
|
90
94
|
state.config.allDependencies.forEach((d) => {
|
|
91
95
|
state.config.allDependencyPaths[target.path].cmake[d.general.name] = `${d.paths.output}/prebuilt`;
|
|
92
|
-
|
|
96
|
+
// A Conan recipe can name a library on another platform only (libpng is png16 on Windows).
|
|
97
|
+
const libNames = d.general.conan
|
|
98
|
+
? d.export.libName.filter((name) => [target.path, target.releasePath]
|
|
99
|
+
.some((targetPath) => fs.existsSync(`${d.paths.output}/prebuilt/${targetPath}/lib/lib${name}.a`)))
|
|
100
|
+
: d.export.libName;
|
|
101
|
+
libNames.forEach((name) => {
|
|
93
102
|
state.config.allDependencyPaths[target.path][name] = {
|
|
94
103
|
root: `${d.paths.output}/prebuilt/${target.path}`,
|
|
95
104
|
};
|