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,97 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { DOCKER_BASE } from './replaceBasePathForDocker.js';
|
|
5
|
+
import { remoteVariables } from './selectRunner.js';
|
|
6
|
+
|
|
7
|
+
const CLIENT = fileURLToPath(new URL('./remoteClient.js', import.meta.url));
|
|
8
|
+
|
|
9
|
+
// Only native inputs travel to the runner; the application's JavaScript and credential files stay on the
|
|
10
|
+
// machine. A cargo target folder stays on the runner, and only the static libraries crossbind links come back from it.
|
|
11
|
+
export const REMOTE_EXCLUDE_RULES = Object.freeze({
|
|
12
|
+
dirs: Object.freeze(['node_modules', '.git']),
|
|
13
|
+
files: Object.freeze(['.env', '.env.*', '.npmrc', '.yarnrc', '.yarnrc.yml', '.netrc']),
|
|
14
|
+
extensions: Object.freeze(['.js', '.mjs', '.cjs', '.ts', '.mts', '.cts', '.jsx', '.tsx']),
|
|
15
|
+
serverOnly: Object.freeze({ names: Object.freeze(['target', 'target-mt']), marker: 'Cargo.toml', keep: Object.freeze(['*/release/*.a']) }),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const TOKEN_VARIABLE = 'CROSSBIND_TOKEN';
|
|
19
|
+
|
|
20
|
+
const LOOPBACK = /^(localhost|127(\.\d{1,3}){3}|\[::1\])$/;
|
|
21
|
+
const warnedUrls = new Set();
|
|
22
|
+
|
|
23
|
+
// Plain http to another machine carries the token and the sources unencrypted.
|
|
24
|
+
function warnIfPlainHttp(url) {
|
|
25
|
+
const { protocol, hostname } = new URL(url);
|
|
26
|
+
if (protocol !== 'http:' || LOOPBACK.test(hostname) || warnedUrls.has(url)) return;
|
|
27
|
+
warnedUrls.add(url);
|
|
28
|
+
console.warn(`crossbind: ${url} is plain http to another machine - the token and your sources travel unencrypted; put TLS in front of the runner.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isInside = (rel, root) => root === '.' || rel === root || rel.startsWith(`${root}/`);
|
|
32
|
+
|
|
33
|
+
// Null for a folder outside the base: a local docker run mounts only the base, so it never sees one either.
|
|
34
|
+
function relativeToBase(base, folder) {
|
|
35
|
+
const rel = path.relative(base, folder);
|
|
36
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) return null;
|
|
37
|
+
return rel.split(path.sep).join('/') || '.';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function foldNested(roots) {
|
|
41
|
+
const unique = [...new Set(roots)].sort((a, b) => a.length - b.length);
|
|
42
|
+
return unique.filter((root, i) => !unique.slice(0, i).some((outer) => isInside(root, outer)));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// A file the command line names may include its neighbours (`#include "../include/x.h"`), so unless a
|
|
46
|
+
// configured folder holds it, the package that holds it travels: the nearest folder below the base with a
|
|
47
|
+
// package.json. Its JavaScript stays behind, as everywhere else.
|
|
48
|
+
function packageOf(base, configured, file) {
|
|
49
|
+
if (configured.some((root) => isInside(relativeToBase(base, file), root))) return file;
|
|
50
|
+
for (let dir = path.dirname(file); ![null, '.'].includes(relativeToBase(base, dir)); dir = path.dirname(dir)) {
|
|
51
|
+
if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
|
|
52
|
+
}
|
|
53
|
+
return file;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// What a toolchain step may read (inputRoots) and where it writes (outputRoots), relative to paths.base,
|
|
57
|
+
// the folder a local docker run mounts. `extraInputs` are only read (a file the link names);
|
|
58
|
+
// `extraOutputs` are read and written, like a crate's folder and its Cargo.lock.
|
|
59
|
+
export function remoteRoots(config, { extraInputs = [], extraOutputs = [] } = {}) {
|
|
60
|
+
const { paths, allDependencies = [] } = config;
|
|
61
|
+
const relOf = (folder) => relativeToBase(paths.base, folder);
|
|
62
|
+
const isBelowBase = (folder) => Boolean(folder) && relOf(folder) !== null;
|
|
63
|
+
const dependencyFolders = allDependencies.flatMap((d) => [d.paths.output, ...(d.paths.native ?? []), ...(d.paths.header ?? [])]);
|
|
64
|
+
const configured = [...paths.native, ...paths.header, paths.cache, paths.output, `${paths.cli}/assets`, ...dependencyFolders].filter(isBelowBase).map(relOf);
|
|
65
|
+
const outputs = [paths.cache, paths.output, ...extraOutputs].filter(isBelowBase).map(relOf);
|
|
66
|
+
return {
|
|
67
|
+
inputRoots: foldNested([...configured, ...extraInputs.filter(isBelowBase).map((file) => relOf(packageOf(paths.base, configured, file))), ...outputs]),
|
|
68
|
+
outputRoots: foldNested(outputs),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function baseMount(config, extra = {}) {
|
|
73
|
+
return { host: config.paths.base, container: DOCKER_BASE, ...remoteRoots(config, extra) };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
77
|
+
|
|
78
|
+
// Paths a command line names below the base (a source the link adds, an include folder, a preload file)
|
|
79
|
+
// travel even when no configured folder holds them. The base itself is never one: it would send everything.
|
|
80
|
+
export function referencedPaths(base, values) {
|
|
81
|
+
const pattern = new RegExp(`${escapeRegExp(base)}(?:/[^@=;:,'"\\s]*)?`, 'g');
|
|
82
|
+
const found = values.flatMap((value) => String(value).match(pattern) ?? []);
|
|
83
|
+
return [...new Set(found)].filter((candidate) => candidate !== base && fs.existsSync(candidate));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Arguments for execFileSync/spawnSync: the client runs as its own process, so the caller stays
|
|
87
|
+
// synchronous as it is with docker. The token reaches it through its environment, never its command line.
|
|
88
|
+
export function remoteExecParams({ remote, role, image, mounts, cwd, argv, env }, options) {
|
|
89
|
+
if (!remote) {
|
|
90
|
+
throw new Error(`crossbind: RUNNER=REMOTE, but the ${role} image has no runner address - set ${remoteVariables(role).key} (or REMOTE_URL), or build with CROSSBIND_RUNNER=DOCKER_RUN. A machine that names no runner address builds on crossbind cloud after crossbind login.`);
|
|
91
|
+
}
|
|
92
|
+
const { url, token, tokenVariable } = remote;
|
|
93
|
+
if (!token) throw new Error(`crossbind: the ${role} runner at ${url} needs a token - set ${tokenVariable}.`);
|
|
94
|
+
warnIfPlainHttp(url);
|
|
95
|
+
const payload = { url, role, image, mounts, cwd, argv, env, rules: REMOTE_EXCLUDE_RULES };
|
|
96
|
+
return [process.execPath, [CLIENT, JSON.stringify(payload)], { ...options, env: { ...process.env, [TOKEN_VARIABLE]: token } }];
|
|
97
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Raw bytes per upload request, a batch of small files or a part of a large one. With base64 a batch stays below the
|
|
5
|
+
// 32 MiB a Cloud Run request may carry, and a part below Cloudflare's 100 MB as well.
|
|
6
|
+
const UPLOAD_BATCH_BYTES = 16 * 1024 * 1024;
|
|
7
|
+
// The first runner protocol that joins a file sent in parts.
|
|
8
|
+
const PARTS_PROTOCOL = 2;
|
|
9
|
+
const MB = 1024 * 1024;
|
|
10
|
+
// A long upload to a hosted runner now and then loses its connection: a request goes again this many times, waiting a
|
|
11
|
+
// second longer each time.
|
|
12
|
+
const UPLOAD_RETRIES = 4;
|
|
13
|
+
const RETRY_DELAY_MS = 1000;
|
|
14
|
+
// Failures before a connection opened, such as a name lookup that failed for a moment: the request never reached the
|
|
15
|
+
// runner, so it goes again this many times. After a connection opened it never does, since a step may have started.
|
|
16
|
+
const UNSENT_CODES = new Set(['ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'UND_ERR_CONNECT_TIMEOUT']);
|
|
17
|
+
const UNSENT_RETRIES = 2;
|
|
18
|
+
|
|
19
|
+
const wait = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
20
|
+
|
|
21
|
+
export async function request(url, route, init = {}) {
|
|
22
|
+
const token = process.env.CROSSBIND_TOKEN;
|
|
23
|
+
const headers = { ...(token ? { authorization: `Bearer ${token}` } : {}), ...init.headers };
|
|
24
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
25
|
+
try {
|
|
26
|
+
// A redirect would send the request, sources included, to an address nobody configured.
|
|
27
|
+
return await fetch(`${url}${route}`, { ...init, headers, redirect: 'error' });
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (!UNSENT_CODES.has(error.cause?.code) || attempt > UNSENT_RETRIES) {
|
|
30
|
+
throw new Error(`crossbind: the remote runner at ${url} is unreachable (${error.cause?.code ?? error.message}).`, { cause: error });
|
|
31
|
+
}
|
|
32
|
+
await wait(RETRY_DELAY_MS * attempt);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function expectOk(response, what) {
|
|
38
|
+
if (response.ok) return response.json();
|
|
39
|
+
const body = await response.json().catch(() => ({}));
|
|
40
|
+
throw new Error(`crossbind: the remote runner failed ${what}: ${body.error ?? response.status}.`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function requestWithRetries(url, route, init) {
|
|
44
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
45
|
+
const response = await request(url, route, init).catch((error) => error);
|
|
46
|
+
const failed = response instanceof Error || response.status >= 500;
|
|
47
|
+
if (!failed || attempt > UPLOAD_RETRIES) {
|
|
48
|
+
if (response instanceof Error) throw response;
|
|
49
|
+
return response;
|
|
50
|
+
}
|
|
51
|
+
await wait(RETRY_DELAY_MS * attempt);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function runnerProtocol(url) {
|
|
56
|
+
const health = await expectOk(await request(url, '/v1/health'), 'to answer its health check');
|
|
57
|
+
return health.protocol ?? 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// One request per part, in order; the runner checks the joined file against its hash. After a part whose connection
|
|
61
|
+
// dropped, the upload goes on from where the runner says it stands. The upload is named after the file, so a later
|
|
62
|
+
// attempt, or another client sending the same file, goes on with what the runner holds.
|
|
63
|
+
async function uploadInParts(url, hash, file, total, partBytes) {
|
|
64
|
+
const upload = hash.slice(0, 32);
|
|
65
|
+
const fd = fs.openSync(file, 'r');
|
|
66
|
+
let resumes = 0;
|
|
67
|
+
try {
|
|
68
|
+
for (let start = 0; start < total;) {
|
|
69
|
+
const part = Buffer.alloc(Math.min(partBytes, total - start));
|
|
70
|
+
fs.readSync(fd, part, 0, part.length, start);
|
|
71
|
+
const response = await requestWithRetries(url, `/v1/blobs/${hash}`, {
|
|
72
|
+
method: 'PUT',
|
|
73
|
+
body: part,
|
|
74
|
+
headers: { 'content-range': `bytes ${start}-${start + part.length - 1}/${total}`, 'x-crossbind-upload': upload },
|
|
75
|
+
});
|
|
76
|
+
if (response.status === 413) {
|
|
77
|
+
throw new Error(`crossbind: the remote runner at ${url} took no part of ${path.basename(file)}: it refused a part of ${part.length} bytes, so the proxy in front of it takes less in one request.`);
|
|
78
|
+
}
|
|
79
|
+
if (response.status === 409 && resumes < UPLOAD_RETRIES) {
|
|
80
|
+
const { received } = await response.json();
|
|
81
|
+
if (!Number.isSafeInteger(received) || received < 0 || received >= total) throw new Error(`crossbind: the remote runner holds ${received} bytes of ${path.basename(file)}, which has ${total}.`);
|
|
82
|
+
resumes += 1;
|
|
83
|
+
start = received;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const answer = await expectOk(response, `to receive ${path.basename(file)}`);
|
|
87
|
+
if (response.status === 201) return;
|
|
88
|
+
start = answer.received;
|
|
89
|
+
}
|
|
90
|
+
} finally {
|
|
91
|
+
fs.closeSync(fd);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A runner deployed before uploads in parts takes a large file in one request, which a body limit in front of it refuses.
|
|
96
|
+
async function uploadWhole(url, hash, file, total) {
|
|
97
|
+
const response = await request(url, `/v1/blobs/${hash}`, { method: 'PUT', body: fs.readFileSync(file) });
|
|
98
|
+
if (response.status === 413) {
|
|
99
|
+
throw new Error(`crossbind: the remote runner at ${url} refused ${path.basename(file)} (${Math.ceil(total / MB)} MB) as too large for one request; deploy the runner again with this crossbind version, which sends large files in parts.`);
|
|
100
|
+
}
|
|
101
|
+
await expectOk(response, `to receive ${path.basename(file)}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// `sources` maps each hash to a file holding it; only what the runner lacks travels, small files in a few batches and a
|
|
105
|
+
// large one on its own.
|
|
106
|
+
export async function uploadMissing(url, sources, partBytes = UPLOAD_BATCH_BYTES) {
|
|
107
|
+
const listing = await request(url, '/v1/missing', { method: 'POST', body: JSON.stringify({ hashes: [...sources.keys()] }) });
|
|
108
|
+
const { missing } = await expectOk(listing, 'to list the files it lacks');
|
|
109
|
+
let protocol;
|
|
110
|
+
let batch = [];
|
|
111
|
+
let size = 0;
|
|
112
|
+
const flush = async () => {
|
|
113
|
+
if (batch.length === 0) return;
|
|
114
|
+
const body = batch.map(({ hash, data }) => JSON.stringify({ sha256: hash, data: data.toString('base64') })).join('\n');
|
|
115
|
+
await expectOk(await requestWithRetries(url, '/v1/blobs', { method: 'POST', body }), `to receive ${batch.length} files`);
|
|
116
|
+
batch = [];
|
|
117
|
+
size = 0;
|
|
118
|
+
};
|
|
119
|
+
for (const hash of missing) {
|
|
120
|
+
const file = sources.get(hash);
|
|
121
|
+
const fileSize = fs.statSync(file).size;
|
|
122
|
+
if (fileSize > partBytes) {
|
|
123
|
+
protocol ??= await runnerProtocol(url);
|
|
124
|
+
if (protocol >= PARTS_PROTOCOL) await uploadInParts(url, hash, file, fileSize, partBytes);
|
|
125
|
+
else await uploadWhole(url, hash, file, fileSize);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const data = fs.readFileSync(file);
|
|
129
|
+
if (size > 0 && size + data.length > partBytes) await flush();
|
|
130
|
+
batch.push({ hash, data });
|
|
131
|
+
size += data.length;
|
|
132
|
+
}
|
|
133
|
+
await flush();
|
|
134
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
// macOS keeps the code signature of a loaded binary per inode: a rebuilt addon or command copied over
|
|
4
|
+
// one that already ran gets the next process that loads it killed. A new file gets a new inode.
|
|
5
|
+
export default function replaceFile(source, destination) {
|
|
6
|
+
fs.rmSync(destination, { force: true });
|
|
7
|
+
fs.copyFileSync(source, destination);
|
|
8
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import state from '../state/index.js';
|
|
3
|
+
import replaceBasePathForDocker from './replaceBasePathForDocker.js';
|
|
4
|
+
import { isLocalRunner } from './selectRunner.js';
|
|
5
|
+
import { HOST_BUILT_PLATFORMS } from './targets.js';
|
|
6
|
+
|
|
7
|
+
const normalize = (dir) => dir.replaceAll('\\', '/').replace(/\/+$/, '');
|
|
8
|
+
|
|
9
|
+
// The directory as the cmake run() starts sees it: a container, Docker or a remote runner, has the project at
|
|
10
|
+
// DOCKER_BASE; macOS and iOS builds and the LOCAL runner see this machine's paths.
|
|
11
|
+
function seenByCMake(dir, target) {
|
|
12
|
+
const isLocal = HOST_BUILT_PLATFORMS.includes(target.platform) || isLocalRunner(state.config.system);
|
|
13
|
+
return isLocal ? dir : replaceBasePathForDocker(dir, state.config.paths.base);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// A CMake build tree remembers the source directory it was configured from and refuses another one. The CMake
|
|
17
|
+
// projects crossbind ships move with its version (node_modules/.pnpm/crossbind@<version>/...), so a tree configured
|
|
18
|
+
// from another directory starts afresh, as `cmake --fresh` would.
|
|
19
|
+
export default function resetCMakeCacheForSource(buildPath, sourceDir, target) {
|
|
20
|
+
const cache = `${buildPath}/CMakeCache.txt`;
|
|
21
|
+
if (!fs.existsSync(cache)) return;
|
|
22
|
+
const configuredFrom = fs.readFileSync(cache, 'utf8').match(/^CMAKE_HOME_DIRECTORY:INTERNAL=(.*)$/m)?.[1];
|
|
23
|
+
if (configuredFrom !== undefined && normalize(configuredFrom) === normalize(seenByCMake(sourceDir, target))) return;
|
|
24
|
+
fs.rmSync(cache, { force: true });
|
|
25
|
+
fs.rmSync(`${buildPath}/CMakeFiles`, { recursive: true, force: true });
|
|
26
|
+
}
|
package/src/utils/runCargo.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawnSync
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
@@ -8,6 +8,9 @@ import replaceBasePathForDockerUtil, { DOCKER_BASE } from './replaceBasePathForD
|
|
|
8
8
|
import pullDockerImage, { getDockerImage, getDockerContainerName, imageRoleFor } from './pullDockerImage.js';
|
|
9
9
|
import { DOCKER_RUN_SECURITY_ARGS } from './dockerSecurity.js';
|
|
10
10
|
import { HOST_BUILT_PLATFORMS } from './targets.js';
|
|
11
|
+
import assertExecContainer from './execContainer.js';
|
|
12
|
+
import { remoteExecParams, baseMount } from './remoteRunner.js';
|
|
13
|
+
import { runnerFor } from './selectRunner.js';
|
|
11
14
|
|
|
12
15
|
// Every cargo invocation crossbind makes goes through here.
|
|
13
16
|
//
|
|
@@ -60,6 +63,26 @@ const CONFIG_NAMES = ['config.toml', 'config'];
|
|
|
60
63
|
// cargo's upward config search finds nothing.
|
|
61
64
|
const CONTAINER_CARGO_HOME = '/var/cache/crossbind/cargo';
|
|
62
65
|
const CONTAINER_CWD = '/tmp/crossbind-cargo';
|
|
66
|
+
// What the host reads out of the registry a remote runner keeps: crate sources, for bridge generation, one
|
|
67
|
+
// folder per crate version or git revision.
|
|
68
|
+
const REMOTE_CARGO_HOME_OUTPUTS = ['registry/src/*/*', 'git/checkouts/*/*'];
|
|
69
|
+
|
|
70
|
+
// The crate a cargo call names travels whole, with every crate its `path =` dependencies reach (embind-rs,
|
|
71
|
+
// the generated bridges); their target folders stay on a remote runner. A `path` naming a file ([lib]
|
|
72
|
+
// path) holds no Cargo.toml and is skipped.
|
|
73
|
+
function crateFolders(args) {
|
|
74
|
+
const at = args.indexOf('--manifest-path');
|
|
75
|
+
if (at === -1) return [];
|
|
76
|
+
const folders = new Set();
|
|
77
|
+
const visit = (dir) => {
|
|
78
|
+
const manifest = path.join(dir, 'Cargo.toml');
|
|
79
|
+
if (folders.has(dir) || !fs.existsSync(manifest)) return;
|
|
80
|
+
folders.add(dir);
|
|
81
|
+
for (const [, rel] of fs.readFileSync(manifest, 'utf8').matchAll(/\bpath\s*=\s*"([^"]+)"/g)) visit(path.resolve(dir, rel));
|
|
82
|
+
};
|
|
83
|
+
visit(path.dirname(path.resolve(args[at + 1])));
|
|
84
|
+
return [...folders];
|
|
85
|
+
}
|
|
63
86
|
|
|
64
87
|
// Crossbind-owned and machine-wide: $CARGO_HOME/config.toml is one of the discovery legs, so the
|
|
65
88
|
// registry cache lives somewhere we can assert is clean instead of in the user's ~/.cargo. It is
|
|
@@ -111,9 +134,8 @@ export function assertCleanConfigChain(home, cwd) {
|
|
|
111
134
|
// Apple targets link with Xcode, which is in no image, so their Rust stays on the host - as does
|
|
112
135
|
// every build under RUNNER=LOCAL.
|
|
113
136
|
export function cargoRunner(target) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return runner === 'DOCKER_RUN' || runner === 'DOCKER_EXEC' ? runner : 'LOCAL';
|
|
137
|
+
const { runner } = runnerFor(imageRoleFor(target), state.config?.system);
|
|
138
|
+
return HOST_BUILT_PLATFORMS.includes(target?.platform) ? 'LOCAL' : runner;
|
|
117
139
|
}
|
|
118
140
|
|
|
119
141
|
function allowedEnv() {
|
|
@@ -154,52 +176,6 @@ function createHint(name, base, home, image) {
|
|
|
154
176
|
].join('\n');
|
|
155
177
|
}
|
|
156
178
|
|
|
157
|
-
function samePath(a, b) {
|
|
158
|
-
const real = (p) => {
|
|
159
|
-
try {
|
|
160
|
-
return fs.realpathSync(p);
|
|
161
|
-
} catch {
|
|
162
|
-
return path.resolve(p);
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
return real(a) === real(b);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function assertExecContainer(name, base, home, image) {
|
|
169
|
-
const hint = createHint(name, base, home, image);
|
|
170
|
-
let info;
|
|
171
|
-
try {
|
|
172
|
-
// stderr is dropped: docker's own "No such container" would land before the message below.
|
|
173
|
-
info = JSON.parse(execFileSync('docker', ['container', 'inspect', name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }))[0];
|
|
174
|
-
} catch {
|
|
175
|
-
throw new Error(`crossbind: RUNNER=DOCKER_EXEC needs a container named '${name}', which does not exist.\n${hint}`);
|
|
176
|
-
}
|
|
177
|
-
if (!info?.State?.Running) {
|
|
178
|
-
throw new Error(
|
|
179
|
-
`crossbind: the container '${name}' exists but is not running. Start it with \`docker start ${name}\`, or recreate it.\n${hint}`,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
for (const [destination, source] of [
|
|
183
|
-
[DOCKER_BASE, base],
|
|
184
|
-
[CONTAINER_CARGO_HOME, home],
|
|
185
|
-
]) {
|
|
186
|
-
const mount = (info.Mounts ?? []).find((m) => m.Destination === destination);
|
|
187
|
-
if (!mount || !samePath(mount.Source, source)) {
|
|
188
|
-
const found = mount ? ` - it mounts ${mount.Source} there` : '';
|
|
189
|
-
throw new Error(
|
|
190
|
-
`crossbind: the container '${name}' does not mount ${source} at ${destination}${found}. It predates the cargo mount, so recreate it.\n${hint}`,
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
// `docker run --workdir` creates the directory; `docker exec --workdir` does not, and cargo
|
|
195
|
-
// dies with an OCI "chdir to cwd" error that names nothing the user can act on.
|
|
196
|
-
try {
|
|
197
|
-
execFileSync('docker', ['exec', '--user', getOsUserAndGroupId(), name, 'mkdir', '-p', CONTAINER_CWD], { stdio: 'ignore' });
|
|
198
|
-
} catch (e) {
|
|
199
|
-
throw new Error(`crossbind: could not create the working directory ${CONTAINER_CWD} inside '${name}'.\n${hint}`, { cause: e });
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
179
|
export default function runCargo(args, { cwd, rustflags = [], panic, capture = false, maxBuffer, allowUnstable = false, target } = {}) {
|
|
204
180
|
const home = cargoHome();
|
|
205
181
|
const workdir = cwd ?? neutralCwd();
|
|
@@ -233,12 +209,33 @@ export default function runCargo(args, { cwd, rustflags = [], panic, capture = f
|
|
|
233
209
|
panic,
|
|
234
210
|
allowUnstable,
|
|
235
211
|
});
|
|
212
|
+
if (runner === 'REMOTE') {
|
|
213
|
+
return spawnSync(...remoteExecParams({
|
|
214
|
+
remote: runnerFor(role, state.config.system).remote,
|
|
215
|
+
role,
|
|
216
|
+
image: getDockerImage(role, platform),
|
|
217
|
+
mounts: [
|
|
218
|
+
baseMount(state.config, { extraOutputs: crateFolders(args) }),
|
|
219
|
+
{ host: home, container: CONTAINER_CARGO_HOME, inputRoots: [], outputRoots: REMOTE_CARGO_HOME_OUTPUTS },
|
|
220
|
+
],
|
|
221
|
+
cwd: CONTAINER_CWD,
|
|
222
|
+
argv: ['cargo', ...replaceBasePathForDockerUtil(args, base)],
|
|
223
|
+
env,
|
|
224
|
+
}, options));
|
|
225
|
+
}
|
|
226
|
+
|
|
236
227
|
const envArgs = Object.entries(env).flatMap(([key, value]) => ['-e', `${key}=${value}`]);
|
|
237
228
|
|
|
238
229
|
let runnerArgs;
|
|
239
230
|
if (runner === 'DOCKER_EXEC') {
|
|
240
231
|
const name = getDockerContainerName(base, role);
|
|
241
|
-
assertExecContainer(
|
|
232
|
+
assertExecContainer({
|
|
233
|
+
name,
|
|
234
|
+
mounts: [[DOCKER_BASE, base], [CONTAINER_CARGO_HOME, home]],
|
|
235
|
+
workdir: CONTAINER_CWD,
|
|
236
|
+
hint: createHint(name, base, home, getDockerImage(role, platform)),
|
|
237
|
+
mountNote: 'It predates the cargo mount, so recreate it.',
|
|
238
|
+
});
|
|
242
239
|
runnerArgs = ['exec', ...envArgs, '--user', getOsUserAndGroupId(), '--workdir', CONTAINER_CWD, name];
|
|
243
240
|
} else {
|
|
244
241
|
pullDockerImage(role, platform);
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import getOsUserAndGroupId from './getOsUserAndGroupId.js';
|
|
6
|
+
import makeTreeWritable from './makeTreeWritable.js';
|
|
7
|
+
import pullDockerImage, { getDockerImage, getDockerContainerName, imageRoleFor } from './pullDockerImage.js';
|
|
8
|
+
import { DOCKER_RUN_SECURITY_ARGS } from './dockerSecurity.js';
|
|
9
|
+
import assertExecContainer from './execContainer.js';
|
|
10
|
+
import { IOS_DEVELOPER_DIR, XCODE_TOOLCHAIN_BIN } from './iosToolchain.js';
|
|
11
|
+
import { DARWIN_CC, DARWIN_TOOLS_BIN } from './darwinToolchain.js';
|
|
12
|
+
import { HOST_BUILT_PLATFORMS } from './targets.js';
|
|
13
|
+
import { remoteExecParams } from './remoteRunner.js';
|
|
14
|
+
import { runnerFor } from './selectRunner.js';
|
|
15
|
+
|
|
16
|
+
// Every conan invocation crossbind makes goes through here. The config is passed in instead of read
|
|
17
|
+
// from state, because state attaches the staged Conan packages while it is still being built.
|
|
18
|
+
//
|
|
19
|
+
// A Conan home holds code conan runs (plugins, hooks) and settings every recipe obeys (global.conf,
|
|
20
|
+
// remotes), and the recipes it builds can write to it. So every run gets a home of its own, made in a
|
|
21
|
+
// fresh work directory and deleted with it; only the package store outlives a run. On the host the
|
|
22
|
+
// environment is rebuilt from an allowlist, so CONAN_DEFAULT_PROFILE, a stray CC or a remote's
|
|
23
|
+
// credentials never reach conan.
|
|
24
|
+
const ALLOWED_ENV = [
|
|
25
|
+
/^PATH$/,
|
|
26
|
+
/^HOME$/,
|
|
27
|
+
/^USER$/,
|
|
28
|
+
/^LOGNAME$/,
|
|
29
|
+
/^SHELL$/,
|
|
30
|
+
/^TERM$/,
|
|
31
|
+
/^TMPDIR$/,
|
|
32
|
+
/^TEMP$/,
|
|
33
|
+
/^TMP$/,
|
|
34
|
+
/^LANG$/,
|
|
35
|
+
/^LC_[A-Z_]+$/,
|
|
36
|
+
/^(HTTP|HTTPS|ALL|NO)_PROXY$/i,
|
|
37
|
+
/^SSL_CERT_(FILE|DIR)$/,
|
|
38
|
+
/^REQUESTS_CA_BUNDLE$/,
|
|
39
|
+
// A host Emscripten under RUNNER=LOCAL.
|
|
40
|
+
/^EMSDK(_NODE|_PYTHON)?$/,
|
|
41
|
+
/^EM_(CONFIG|CACHE)$/,
|
|
42
|
+
/^SYSTEMROOT$/i,
|
|
43
|
+
/^WINDIR$/i,
|
|
44
|
+
/^USERPROFILE$/i,
|
|
45
|
+
/^PROGRAMDATA$/i,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// What conan can build on the host: wasm with a host Emscripten, iOS and macOS with Xcode.
|
|
49
|
+
const HOST_PLATFORMS = ['wasm', 'ios', 'darwin'];
|
|
50
|
+
const APPLE_PLATFORM_NAMES = { ios: 'iOS', darwin: 'macOS' };
|
|
51
|
+
// Where a container sees conanRoot(): an exec container mounts all of it here, a run container only
|
|
52
|
+
// the store and its own work directory, at the same places.
|
|
53
|
+
const CONTAINER_ROOT = '/var/cache/crossbind/conan';
|
|
54
|
+
// The package folders conan reports, which the host stages from: downloaded binaries sit in p/<ref>/p, ones
|
|
55
|
+
// conan built in b/<ref>/p. Sources and build folders stay on a remote runner.
|
|
56
|
+
const REMOTE_STORE_OUTPUTS = ['p/*/p', 'b/*/p'];
|
|
57
|
+
// A recipe's build log is the whole compiler output of a library.
|
|
58
|
+
const MAX_BUFFER = 256 * 1024 * 1024;
|
|
59
|
+
// The JSON graph carries conandata, the source URL and SHA-256 of the license rows, from 2.19 on.
|
|
60
|
+
const MIN_CONAN_VERSION = [2, 19];
|
|
61
|
+
|
|
62
|
+
export function conanRunner(config, target) {
|
|
63
|
+
const { runner } = runnerFor(imageRoleFor(target), config.system);
|
|
64
|
+
// Xcode runs on the Mac alone, so iOS and macOS packages build there whatever runner the rest use.
|
|
65
|
+
return HOST_BUILT_PLATFORMS.includes(target?.platform) ? 'LOCAL' : runner;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Machine-wide like the cargo cache: built packages are reused by every project, and `pnpm run clear`
|
|
69
|
+
// in one project must not throw them away. Containers write to the store and conan runs the recipes
|
|
70
|
+
// it keeps there, so conan on the host never shares a store with them.
|
|
71
|
+
export function conanRoot(runner = 'DOCKER_RUN') {
|
|
72
|
+
return path.join(os.homedir(), '.crossbind', runner === 'LOCAL' ? 'conan-local' : 'conan');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The directory one conan run reads its inputs from and writes its outputs to, with the home it runs
|
|
76
|
+
// with. conanPath names a file of it as conan sees it.
|
|
77
|
+
export function createConanWork(runner) {
|
|
78
|
+
const root = conanRoot(runner);
|
|
79
|
+
const store = path.join(root, 'store');
|
|
80
|
+
fs.mkdirSync(store, { recursive: true });
|
|
81
|
+
fs.mkdirSync(path.join(root, 'work'), { recursive: true });
|
|
82
|
+
const dir = fs.mkdtempSync(path.join(root, 'work', 'run-'));
|
|
83
|
+
const conanDir = runner === 'LOCAL' ? dir : `${CONTAINER_ROOT}/work/${path.basename(dir)}`;
|
|
84
|
+
const conanPath = (file) => (runner === 'LOCAL' ? path.join(dir, file) : `${conanDir}/${file}`);
|
|
85
|
+
// conan takes its home from the first .conanrc at or above its working directory, ahead of
|
|
86
|
+
// CONAN_HOME; one in the working directory itself is always that first one.
|
|
87
|
+
fs.writeFileSync(path.join(dir, '.conanrc'), 'conan_home=./home\n');
|
|
88
|
+
fs.mkdirSync(path.join(dir, 'home'));
|
|
89
|
+
fs.writeFileSync(path.join(dir, 'home', 'global.conf'), [
|
|
90
|
+
`core.cache:storage_path=${runner === 'LOCAL' ? store : `${CONTAINER_ROOT}/store`}`,
|
|
91
|
+
'core:non_interactive=True',
|
|
92
|
+
'',
|
|
93
|
+
].join('\n'));
|
|
94
|
+
return {
|
|
95
|
+
runner, root, store, dir, conanDir, conanPath,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function removeTree(dir) {
|
|
100
|
+
if (!fs.existsSync(dir)) return;
|
|
101
|
+
makeTreeWritable(dir);
|
|
102
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const removeConanWork = (work) => removeTree(work.dir);
|
|
106
|
+
|
|
107
|
+
// What runs that were killed left behind. Only while holding the install lock: no run is going then.
|
|
108
|
+
export const clearConanWork = (runner) => removeTree(path.join(conanRoot(runner), 'work'));
|
|
109
|
+
|
|
110
|
+
// conan reports paths as it sees them. In a container that is through the mounts, which all sit under
|
|
111
|
+
// CONTAINER_ROOT, so mapping the prefix back is exact; a path anywhere else is none crossbind gave it.
|
|
112
|
+
export function toHostPath(reported, work) {
|
|
113
|
+
if (work.runner === 'LOCAL') return path.resolve(reported);
|
|
114
|
+
const clean = path.posix.normalize(reported);
|
|
115
|
+
// A backslash is a separator again once the path is joined on a Windows host.
|
|
116
|
+
if (reported.includes('\\') || !clean.startsWith(`${CONTAINER_ROOT}/`)) {
|
|
117
|
+
throw new Error(`crossbind: conan reported ${JSON.stringify(reported)}, outside the directories crossbind mounts for it.`);
|
|
118
|
+
}
|
|
119
|
+
return path.join(work.root, clean.slice(CONTAINER_ROOT.length + 1));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function allowedEnv() {
|
|
123
|
+
return Object.fromEntries(Object.entries(process.env).filter(([key]) => ALLOWED_ENV.some((allowed) => allowed.test(key))));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The Xcode every crossbind iOS archive builds with, whichever one xcode-select points at.
|
|
127
|
+
const iosEnv = () => ({ ...allowedEnv(), DEVELOPER_DIR: IOS_DEVELOPER_DIR });
|
|
128
|
+
|
|
129
|
+
// A GNU ar or nm ahead of Apple's on a Mac's PATH (Homebrew's binutils) writes archives Apple's linker
|
|
130
|
+
// cannot read, and recipes, the build tools they build and Meson all take these tools by name. So the
|
|
131
|
+
// run gets links to Xcode's first on its PATH; for macOS to the ones xcode-select picks, as the macOS
|
|
132
|
+
// ports are built with.
|
|
133
|
+
const APPLE_TOOLS = {
|
|
134
|
+
ar: 'ar', as: 'as', nm: 'llvm-nm', ranlib: 'ranlib', strip: 'strip',
|
|
135
|
+
};
|
|
136
|
+
const appleToolOf = (target, name) => (target.platform === 'darwin'
|
|
137
|
+
? path.join(DARWIN_TOOLS_BIN, name)
|
|
138
|
+
: path.join(XCODE_TOOLCHAIN_BIN, APPLE_TOOLS[name]));
|
|
139
|
+
|
|
140
|
+
function withAppleTools(work, env, target) {
|
|
141
|
+
if (process.platform !== 'darwin') return env;
|
|
142
|
+
const dir = path.join(work.dir, 'apple-tools');
|
|
143
|
+
if (!fs.existsSync(dir)) {
|
|
144
|
+
fs.mkdirSync(dir);
|
|
145
|
+
Object.keys(APPLE_TOOLS).forEach((name) => fs.symlinkSync(appleToolOf(target, name), path.join(dir, name)));
|
|
146
|
+
}
|
|
147
|
+
return { ...env, PATH: [dir, env.PATH].filter(Boolean).join(path.delimiter) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const toolVersions = new Map();
|
|
151
|
+
// The first line a tool prints for --version, asked once.
|
|
152
|
+
function toolVersion(command, env = allowedEnv(), cwd = undefined) {
|
|
153
|
+
const key = command.join(' ');
|
|
154
|
+
if (!toolVersions.has(key)) {
|
|
155
|
+
const [program, ...args] = command;
|
|
156
|
+
const result = spawnSync(program, [...args, '--version'], { encoding: 'utf8', env, cwd });
|
|
157
|
+
toolVersions.set(key, `${result.stdout ?? ''}`.split('\n')[0].trim());
|
|
158
|
+
}
|
|
159
|
+
return toolVersions.get(key);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// conan migrates the home it starts in, even for --version, so it is asked from a home of crossbind's
|
|
163
|
+
// instead of the user's ~/.conan2.
|
|
164
|
+
function conanVersion() {
|
|
165
|
+
const dir = path.join(conanRoot('LOCAL'), 'probe');
|
|
166
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
167
|
+
fs.writeFileSync(path.join(dir, '.conanrc'), 'conan_home=./home\n');
|
|
168
|
+
return toolVersion(['conan'], allowedEnv(), dir);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function compilerIdentity(target) {
|
|
172
|
+
if (target?.platform === 'ios') return toolVersion(['xcrun', 'clang'], iosEnv());
|
|
173
|
+
if (target?.platform === 'darwin') return toolVersion([DARWIN_CC]);
|
|
174
|
+
return toolVersion(['emcc']);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// What a host conan builds with; for a container the image digest says the same.
|
|
178
|
+
export function localToolchainIdentity(target) {
|
|
179
|
+
return `${compilerIdentity(target)}\n${conanVersion()}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function assertLocalConan(target) {
|
|
183
|
+
const conan = conanVersion();
|
|
184
|
+
const [major, minor] = (conan.match(/(\d+)\.(\d+)/) ?? []).slice(1).map(Number);
|
|
185
|
+
const [needMajor, needMinor] = MIN_CONAN_VERSION;
|
|
186
|
+
if (major > needMajor || (major === needMajor && minor >= needMinor)) return;
|
|
187
|
+
const need = `Conan ${MIN_CONAN_VERSION.join('.')} or later; found ${conan || 'none'}.`;
|
|
188
|
+
throw new Error(HOST_BUILT_PLATFORMS.includes(target.platform)
|
|
189
|
+
? `crossbind: Conan packages for ${APPLE_PLATFORM_NAMES[target.platform]} build on this Mac, with the conan on the PATH, which must be ${need} Install or upgrade it with \`brew install conan\` (\`brew upgrade conan\`) or \`pipx install conan\`.`
|
|
190
|
+
: `crossbind: RUNNER=LOCAL runs conan from the PATH and needs ${need}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export default function runConan(args, { config, target, work }) {
|
|
194
|
+
const options = { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', maxBuffer: MAX_BUFFER };
|
|
195
|
+
const role = imageRoleFor(target);
|
|
196
|
+
if (work.runner === 'LOCAL') {
|
|
197
|
+
if (!HOST_PLATFORMS.includes(target.platform)) {
|
|
198
|
+
throw new Error(`crossbind: Conan packages for ${target.platform} build in the ${role} image, which RUNNER=LOCAL does not use.`);
|
|
199
|
+
}
|
|
200
|
+
if (HOST_BUILT_PLATFORMS.includes(target.platform) && process.platform !== 'darwin') {
|
|
201
|
+
throw new Error(`crossbind: Conan packages for ${APPLE_PLATFORM_NAMES[target.platform]} build with Xcode, on a Mac.`);
|
|
202
|
+
}
|
|
203
|
+
assertLocalConan(target);
|
|
204
|
+
const env = withAppleTools(work, { ...(target.platform === 'ios' ? iosEnv() : allowedEnv()), CONAN_HOME: path.join(work.dir, 'home') }, target);
|
|
205
|
+
return spawnSync('conan', args, { ...options, cwd: work.dir, env });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Google ships the linux NDK for x86_64 only.
|
|
209
|
+
const platform = target.platform === 'android' ? 'linux/amd64' : undefined;
|
|
210
|
+
if (work.runner === 'REMOTE') {
|
|
211
|
+
return spawnSync(...remoteExecParams({
|
|
212
|
+
remote: runnerFor(role, config.system).remote,
|
|
213
|
+
role,
|
|
214
|
+
image: getDockerImage(role, platform),
|
|
215
|
+
mounts: [
|
|
216
|
+
{ host: work.store, container: `${CONTAINER_ROOT}/store`, inputRoots: [], outputRoots: REMOTE_STORE_OUTPUTS },
|
|
217
|
+
{ host: work.dir, container: work.conanDir, inputRoots: ['.'], outputRoots: ['.'] },
|
|
218
|
+
],
|
|
219
|
+
cwd: work.conanDir,
|
|
220
|
+
argv: ['conan', ...args],
|
|
221
|
+
env: { CONAN_HOME: work.conanPath('home') },
|
|
222
|
+
}, options));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const env = ['-e', `CONAN_HOME=${work.conanPath('home')}`];
|
|
226
|
+
let runnerArgs;
|
|
227
|
+
if (work.runner === 'DOCKER_EXEC') {
|
|
228
|
+
const name = getDockerContainerName(config.paths.base, role);
|
|
229
|
+
assertExecContainer({
|
|
230
|
+
name,
|
|
231
|
+
mounts: [[CONTAINER_ROOT, work.root]],
|
|
232
|
+
workdir: work.conanDir,
|
|
233
|
+
hint: `Recreate it with \`crossbind docker stop ${role}\`, \`crossbind docker delete ${role}\` and \`crossbind docker create ${role}\`, which mount ${work.root} at ${CONTAINER_ROOT}.`,
|
|
234
|
+
mountNote: 'It predates the conan mount.',
|
|
235
|
+
});
|
|
236
|
+
runnerArgs = ['exec', ...env, '--user', getOsUserAndGroupId(), '--workdir', work.conanDir, name];
|
|
237
|
+
} else {
|
|
238
|
+
pullDockerImage(role, platform);
|
|
239
|
+
runnerArgs = [
|
|
240
|
+
'run',
|
|
241
|
+
'--rm',
|
|
242
|
+
...(platform ? ['--platform', platform] : []),
|
|
243
|
+
...DOCKER_RUN_SECURITY_ARGS,
|
|
244
|
+
'-v',
|
|
245
|
+
`${work.store}:${CONTAINER_ROOT}/store`,
|
|
246
|
+
'-v',
|
|
247
|
+
`${work.dir}:${work.conanDir}`,
|
|
248
|
+
...env,
|
|
249
|
+
'--user',
|
|
250
|
+
getOsUserAndGroupId(),
|
|
251
|
+
'--workdir',
|
|
252
|
+
work.conanDir,
|
|
253
|
+
getDockerImage(role, platform),
|
|
254
|
+
];
|
|
255
|
+
}
|
|
256
|
+
return spawnSync('docker', [...runnerArgs, 'conan', ...args], options);
|
|
257
|
+
}
|