crossbind 2.0.0-beta.60 → 2.0.0-beta.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/package.json +6 -1
- package/src/actions/buildDependencies.js +37 -9
- package/src/actions/buildExternal.js +9 -1
- package/src/actions/buildJs.js +1 -1
- package/src/actions/buildLib.js +27 -8
- package/src/actions/buildNativeCommand.js +84 -0
- package/src/actions/buildNode.js +190 -0
- package/src/actions/buildTypes.js +8 -2
- package/src/actions/buildWasiCommand.js +0 -4
- package/src/actions/buildWasm.js +92 -95
- package/src/actions/cloudCommands.js +133 -0
- package/src/actions/createImportedBridges.js +64 -0
- package/src/actions/createInterface.js +318 -65
- package/src/actions/createLib.js +60 -43
- package/src/actions/getAllBridges.js +1 -1
- package/src/actions/getData.js +4 -1
- package/src/actions/getLinkInputs.js +70 -0
- package/src/actions/isSourceNewer.js +2 -0
- package/src/actions/licenses.js +175 -17
- package/src/actions/prepareConanDependencies.js +51 -0
- package/src/actions/publishBinary.js +32 -0
- package/src/actions/run.js +124 -32
- package/src/actions/runnerCommands.js +421 -0
- package/src/actions/writeRuntimeEntry.js +69 -0
- package/src/assets/cmake/CMakeLists.txt +1 -1
- 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/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 +87 -56
- package/src/assets/js-runtime/core.js +11 -0
- package/src/assets/node-import-hooks.mjs +61 -0
- package/src/assets/toolchain-digests.json +27 -13
- package/src/assets/wasi-runtime/stubs.c +29 -0
- package/src/bin.js +290 -37
- package/src/index.js +6 -1
- package/src/integration/bindHeaderImports.js +18 -0
- package/src/integration/getCrossbindScript.js +53 -19
- package/src/integration/getDependFilePath.js +18 -12
- package/src/integration/resolveNativeImport.js +14 -0
- package/src/node/dev.mjs +19 -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 +31 -20
- package/src/state/loadConfig.js +36 -24
- 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 +17 -1
- package/src/utils/appSources.js +112 -0
- package/src/utils/archiveFlags.js +12 -0
- package/src/utils/boundHeaders.js +79 -0
- package/src/utils/bridgeExtras.js +486 -0
- package/src/utils/bridgeRuntimeFixes.js +48 -0
- package/src/utils/bundledLicenses.js +26 -0
- package/src/utils/cargoImport.js +44 -0
- package/src/utils/cargoTarget.js +23 -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 +134 -55
- 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 +84 -0
- package/src/utils/embindCallArguments.js +19 -0
- package/src/utils/embindJspiQueue.js +25 -0
- package/src/utils/embindRsFingerprint.js +26 -0
- package/src/utils/execContainer.js +49 -0
- package/src/utils/headerImports.js +56 -0
- package/src/utils/hostPlatform.js +21 -0
- package/src/utils/inputStamp.js +23 -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/linkLayout.js +8 -1
- 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 +101 -0
- package/src/utils/pthreadMainScript.js +14 -0
- package/src/utils/pullDockerImage.js +10 -5
- 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/resolveCompanionPackage.js +51 -0
- package/src/utils/resolveEmbindNapi.js +22 -0
- package/src/utils/resolveEmbindRust.js +17 -52
- package/src/utils/runCargo.js +51 -53
- package/src/utils/runConan.js +257 -0
- package/src/utils/runtimeEntries.js +56 -0
- package/src/utils/rustBridgeGen.js +903 -253
- package/src/utils/rustMt.js +4 -0
- 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 +86 -6
- package/src/utils/systemKeys.js +43 -3
- package/src/utils/targets.js +104 -3
- package/src/utils/toolchainNotices.js +5 -0
- package/src/utils/wasiToolchain.js +14 -1
- package/src/utils/windowsToolchain.js +18 -0
- package/vitest.config.js +11 -0
- package/src/utils/cppFieldBindings.js +0 -23
|
@@ -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,9 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
import loadJson from '../utils/loadJson.js';
|
|
4
3
|
import writeJson from '../utils/writeJson.js';
|
|
5
|
-
import { TARGETS, targetPathOf } from '../utils/targets.js';
|
|
6
|
-
import
|
|
4
|
+
import { TARGETS, targetPathOf, nodeAddonNamesOf, nativeCommandNamesOf } from '../utils/targets.js';
|
|
5
|
+
import { findCargoModuleImportsIn, writeCargoMarker } from '../utils/cargoImport.js';
|
|
6
|
+
import { exportWasiSdkPath } from '../utils/wasiToolchain.js';
|
|
7
|
+
import loadConfig, { assertBinarySelection } from './loadConfig.js';
|
|
8
|
+
import refreshConanDependencies from './refreshConanDependencies.js';
|
|
7
9
|
|
|
8
10
|
const cacheDir = `${process.cwd()}/.crossbind`;
|
|
9
11
|
|
|
@@ -22,18 +24,17 @@ await initProcessState();
|
|
|
22
24
|
async function initProcessState() {
|
|
23
25
|
state.cache = loadCacheState();
|
|
24
26
|
state.config = await loadConfig();
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (state.config.system.WASI_SDK_PATH && !process.env.CROSSBIND_WASI_SDK_PATH) {
|
|
29
|
-
process.env.CROSSBIND_WASI_SDK_PATH = state.config.system.WASI_SDK_PATH;
|
|
30
|
-
}
|
|
27
|
+
assertBinarySelection(state.config);
|
|
28
|
+
refreshConanDependencies(state.config);
|
|
29
|
+
exportWasiSdkPath(state.config.system);
|
|
31
30
|
// Same idea for the docker image overrides: pullDockerImage.js resolves them from the
|
|
32
31
|
// environment alone, so it stays free of a state import (and of the cycle that would create).
|
|
33
32
|
Object.entries({
|
|
34
33
|
DOCKER_REGISTRY_MIRROR: 'CROSSBIND_REGISTRY_MIRROR',
|
|
35
34
|
DOCKER_IMAGE_WEB: 'CROSSBIND_IMAGE_WEB',
|
|
36
35
|
DOCKER_IMAGE_ANDROID: 'CROSSBIND_IMAGE_ANDROID',
|
|
36
|
+
DOCKER_IMAGE_LINUX: 'CROSSBIND_IMAGE_LINUX',
|
|
37
|
+
DOCKER_IMAGE_WINDOWS: 'CROSSBIND_IMAGE_WINDOWS',
|
|
37
38
|
}).forEach(([key, envKey]) => {
|
|
38
39
|
if (state.config.system[key] && !process.env[envKey]) {
|
|
39
40
|
process.env[envKey] = state.config.system[key];
|
|
@@ -54,21 +55,26 @@ async function initProcessState() {
|
|
|
54
55
|
// A wasi build is a single command module - no JS glue, no preload.
|
|
55
56
|
target.wasmName = `${state.config.general.name}-${target.path}.wasm`;
|
|
56
57
|
}
|
|
58
|
+
if (target.runtimeEnv === 'node' && target.platform !== 'wasm') {
|
|
59
|
+
Object.assign(target, nodeAddonNamesOf(target, state.config.general.name));
|
|
60
|
+
}
|
|
61
|
+
if (target.runtimeEnv === 'native') {
|
|
62
|
+
Object.assign(target, nativeCommandNamesOf(target, state.config.general.name));
|
|
63
|
+
}
|
|
57
64
|
});
|
|
58
65
|
|
|
59
66
|
setAllDependecyPaths();
|
|
60
67
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
68
|
+
// Cargo-crate import markers must exist BEFORE any bundler starts: metro resolves against
|
|
69
|
+
// its startup file map, so a marker first created mid-resolution is invisible to that very
|
|
70
|
+
// build. Bare crates come from the config, module imports from the app's own sources.
|
|
64
71
|
const cargoDeps = state.config.cargoDependencies ?? {};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
+
const crateNames = Object.keys(cargoDeps);
|
|
73
|
+
const moduleImports = crateNames.length && state.config.paths.project
|
|
74
|
+
? findCargoModuleImportsIn(state.config.paths.project).filter((i) => Object.hasOwn(cargoDeps, i.crateName))
|
|
75
|
+
: [];
|
|
76
|
+
[...crateNames.map((crateName) => ({ crateName, modulePath: [] })), ...moduleImports]
|
|
77
|
+
.forEach((cargoImport) => writeCargoMarker(state.config.paths.cache, cargoImport));
|
|
72
78
|
|
|
73
79
|
if (state.config.build?.setState) {
|
|
74
80
|
state.config.build.setState(state);
|
|
@@ -86,7 +92,12 @@ export function setAllDependecyPaths() {
|
|
|
86
92
|
state.config.allDependencyPaths[target.path] = { cmake: {} };
|
|
87
93
|
state.config.allDependencies.forEach((d) => {
|
|
88
94
|
state.config.allDependencyPaths[target.path].cmake[d.general.name] = `${d.paths.output}/prebuilt`;
|
|
89
|
-
|
|
95
|
+
// A Conan recipe can name a library on another platform only (libpng is png16 on Windows).
|
|
96
|
+
const libNames = d.general.conan
|
|
97
|
+
? d.export.libName.filter((name) => [target.path, target.releasePath]
|
|
98
|
+
.some((targetPath) => fs.existsSync(`${d.paths.output}/prebuilt/${targetPath}/lib/lib${name}.a`)))
|
|
99
|
+
: d.export.libName;
|
|
100
|
+
libNames.forEach((name) => {
|
|
90
101
|
state.config.allDependencyPaths[target.path][name] = {
|
|
91
102
|
root: `${d.paths.output}/prebuilt/${target.path}`,
|
|
92
103
|
};
|
package/src/state/loadConfig.js
CHANGED
|
@@ -12,8 +12,23 @@ import fixPackageName from '../utils/fixPackageName.js';
|
|
|
12
12
|
import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
|
|
13
13
|
import calculateDependencyParameters from './calculateDependencyParameters.js';
|
|
14
14
|
import logger from '../utils/logger.js';
|
|
15
|
+
import { RUNTIME_ENVS } from '../utils/targets.js';
|
|
16
|
+
import { normalizeConanDependencies } from '../utils/conanDependencies.js';
|
|
17
|
+
import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
|
|
15
18
|
// import getCmakeParameters from './getCmakeParameters.js';
|
|
16
19
|
|
|
20
|
+
// For the project being built only: dependency builds load published configs that may still set
|
|
21
|
+
// export.bundle.
|
|
22
|
+
export function assertBinarySelection(config) {
|
|
23
|
+
if ('bundle' in config.export) {
|
|
24
|
+
throw new Error('crossbind: export.bundle was removed. A build makes binaries only for the runtime environments it is asked for: -e on the command line, target.runtimeEnv in crossbind.config.js, or a bundler plugin. Remove export.bundle.');
|
|
25
|
+
}
|
|
26
|
+
const { runtimeEnv } = config.target;
|
|
27
|
+
if (runtimeEnv !== undefined && runtimeEnv !== null && !RUNTIME_ENVS.includes(runtimeEnv)) {
|
|
28
|
+
throw new Error(`crossbind: target.runtimeEnv must be one of ${RUNTIME_ENVS.join(', ')} (got ${JSON.stringify(runtimeEnv)}).`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
export default async function loadConfig(configDir = process.cwd(), configName = 'crossbind.config') {
|
|
18
33
|
const config = await loadJs(configDir, configName) || {};
|
|
19
34
|
if (config.excludedDependencies?.length) {
|
|
@@ -80,6 +95,15 @@ export default async function loadConfig(configDir = process.cwd(), configName =
|
|
|
80
95
|
return output;
|
|
81
96
|
}
|
|
82
97
|
|
|
98
|
+
// Direct and transitive dependencies, one entry per package directory.
|
|
99
|
+
export function flattenDependencies(dependencies) {
|
|
100
|
+
const output = {};
|
|
101
|
+
[...dependencies, ...dependencies.map((d) => d.allDependencies).flat()].forEach((d) => {
|
|
102
|
+
output[d.paths.project] = d;
|
|
103
|
+
});
|
|
104
|
+
return Object.values(output);
|
|
105
|
+
}
|
|
106
|
+
|
|
83
107
|
export function getFilledConfig(config, options = { isDepend: false }) {
|
|
84
108
|
const exclude = options.exclude || [];
|
|
85
109
|
const { seen, replaces } = options;
|
|
@@ -101,6 +125,7 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
101
125
|
general: config.general || {},
|
|
102
126
|
dependencies,
|
|
103
127
|
cargoDependencies: config.cargoDependencies || {},
|
|
128
|
+
conanDependencies: normalizeConanDependencies(config.conanDependencies),
|
|
104
129
|
// 'sync' types the direct surface; 'promise' wraps every generated method return
|
|
105
130
|
// for useWorker-style runtimes (constructors stay sync-typed: await new X()).
|
|
106
131
|
dts: config.dts || 'sync',
|
|
@@ -126,6 +151,9 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
126
151
|
if (typeof newConfig.types !== 'boolean') {
|
|
127
152
|
throw new Error(`crossbind: "types" must be true or false (got '${newConfig.types}').`);
|
|
128
153
|
}
|
|
154
|
+
if (newConfig.targetSpecs.some((t) => Array.isArray(t?.specs?.cmake))) {
|
|
155
|
+
throw new Error('crossbind: "targetSpecs[].specs.cmake" must be an object such as { compileOptions: [\'-O3\'] } (got an array). CMake configure flags (-D...) belong in a recipe\'s getBuildParams.');
|
|
156
|
+
}
|
|
129
157
|
|
|
130
158
|
if (newConfig.paths.config && !newConfig.paths.project) {
|
|
131
159
|
newConfig.paths.project = getParentPath(newConfig.paths.config);
|
|
@@ -173,15 +201,8 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
173
201
|
newConfig.export.libPath = getPath(newConfig.export.libPath || 'lib');
|
|
174
202
|
newConfig.export.libName = newConfig.export.libName || [newConfig.general.name];
|
|
175
203
|
newConfig.export.binHeaders = newConfig.export.binHeaders || [];
|
|
176
|
-
newConfig.export.bundle = newConfig.export.bundle ?? true;
|
|
177
204
|
|
|
178
|
-
newConfig.allDependencies = (
|
|
179
|
-
const output = {};
|
|
180
|
-
[...newConfig.dependencies, ...newConfig.dependencies.map((d) => d.allDependencies).flat()].forEach((d) => {
|
|
181
|
-
output[d.paths.project] = d;
|
|
182
|
-
});
|
|
183
|
-
return Object.values(output);
|
|
184
|
-
})();
|
|
205
|
+
newConfig.allDependencies = flattenDependencies(newConfig.dependencies);
|
|
185
206
|
|
|
186
207
|
newConfig.extensions?.forEach(e => {
|
|
187
208
|
e?.loadConfig?.after(newConfig);
|
|
@@ -191,22 +212,13 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
191
212
|
newConfig.target.runtime = 'mt';
|
|
192
213
|
}
|
|
193
214
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
&& fs.existsSync(newConfig.paths.cmake);
|
|
202
|
-
|
|
203
|
-
return (
|
|
204
|
-
isSourceCmakePackage
|
|
205
|
-
|| fs.existsSync(`${newConfig.paths.cmakeDir}/${target.path}`)
|
|
206
|
-
|| fs.existsSync(`${newConfig.paths.cmakeDir}/${target.releasePath}`)
|
|
207
|
-
|| (target.platform === 'ios' && fs.existsSync(`${newConfig.paths.cmakeDir}/../../${newConfig.general.name}-${target.runtime}.xcframework`))
|
|
208
|
-
);
|
|
209
|
-
});
|
|
215
|
+
// A package that ships its sources serves every target.
|
|
216
|
+
newConfig.functions.isEnabled = newConfig.functions.isEnabled || ((target) => (
|
|
217
|
+
isSourceCmakePackage(newConfig)
|
|
218
|
+
|| fs.existsSync(`${newConfig.paths.cmakeDir}/${target.path}`)
|
|
219
|
+
|| fs.existsSync(`${newConfig.paths.cmakeDir}/${target.releasePath}`)
|
|
220
|
+
|| (target.platform === 'ios' && fs.existsSync(`${newConfig.paths.cmakeDir}/../../${newConfig.general.name}-${target.runtime}.xcframework`))
|
|
221
|
+
));
|
|
210
222
|
|
|
211
223
|
newConfig.dependencyParameters = calculateDependencyParameters(newConfig);
|
|
212
224
|
// newConfig.cmakeParameters = getCmakeParameters(newConfig);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import calculateDependencyParameters from './calculateDependencyParameters.js';
|
|
2
|
+
import getAbsolutePath from '../utils/getAbsolutePath.js';
|
|
3
|
+
import getCMakeListsFilePath, { getCliCMakeListsFile } from '../utils/getCMakeListsFilePath.js';
|
|
4
|
+
import getParentPath from '../utils/getParentPath.js';
|
|
5
|
+
|
|
6
|
+
// A dependency with no build when the config loaded resolved to the CLI's own CMakeLists, which keeps it out of the link
|
|
7
|
+
// and its headers out of the dependency include roots: a process that outlives the build, such as a Metro server, takes it
|
|
8
|
+
// in here. Says whether it took one in.
|
|
9
|
+
export default function refreshBuiltDependencies(config) {
|
|
10
|
+
const built = (config.allDependencies ?? [])
|
|
11
|
+
.filter((dep) => dep !== config && ['cmake', 'cargo'].includes(dep.export?.type) && dep.paths.cmake === config.paths.cliCMakeListsTxt)
|
|
12
|
+
.map((dep) => ({ dep, cmake: getCMakeListsFilePath(dep.paths.output) }))
|
|
13
|
+
.filter(({ cmake }) => cmake !== getCliCMakeListsFile());
|
|
14
|
+
built.forEach(({ dep, cmake }) => {
|
|
15
|
+
dep.paths.cmake = getAbsolutePath(dep.paths.project, cmake);
|
|
16
|
+
dep.paths.cmakeDir = getParentPath(dep.paths.cmake);
|
|
17
|
+
});
|
|
18
|
+
if (built.length > 0) config.dependencyParameters = calculateDependencyParameters(config);
|
|
19
|
+
return built.length > 0;
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import attachConanDependencies from './attachConanDependencies.js';
|
|
2
|
+
import { conanStageDir } from '../utils/conanImport.js';
|
|
3
|
+
import { conanManifestsStamp } from '../utils/conanStage.js';
|
|
4
|
+
|
|
5
|
+
const attachedFrom = new WeakMap();
|
|
6
|
+
|
|
7
|
+
// Attaches what a build staged since this config last attached: a process that loaded state before
|
|
8
|
+
// the build, such as a Metro server, would otherwise bind headers without the packages. Says whether
|
|
9
|
+
// it attached again.
|
|
10
|
+
export default function refreshConanDependencies(config) {
|
|
11
|
+
if (Object.keys(config.conanDependencies ?? {}).length === 0) return false;
|
|
12
|
+
const stamp = conanManifestsStamp(conanStageDir(config.paths.cache));
|
|
13
|
+
if (attachedFrom.get(config) === stamp) return false;
|
|
14
|
+
attachedFrom.set(config, stamp);
|
|
15
|
+
attachConanDependencies(config);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// The android image's NDK, and the API level every Android archive is built against: port archives
|
|
2
|
+
// and Conan packages link into the same library, so they move together.
|
|
3
|
+
export const ANDROID_NDK = '/opt/android-sdk/ndk/current';
|
|
4
|
+
export const ANDROID_API_LEVEL = 33;
|