crossbind 2.0.0-beta.62 → 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 +2 -2
- package/package.json +6 -1
- package/src/actions/buildDependencies.js +15 -2
- package/src/actions/buildExternal.js +9 -1
- package/src/actions/buildLib.js +25 -8
- package/src/actions/buildNativeCommand.js +84 -0
- package/src/actions/buildNode.js +48 -7
- package/src/actions/buildTypes.js +8 -2
- package/src/actions/buildWasiCommand.js +0 -4
- package/src/actions/buildWasm.js +49 -29
- package/src/actions/cloudCommands.js +133 -0
- package/src/actions/createImportedBridges.js +64 -0
- package/src/actions/createInterface.js +247 -68
- package/src/actions/createLib.js +57 -41
- package/src/actions/getData.js +4 -1
- 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 +421 -0
- package/src/actions/writeRuntimeEntry.js +69 -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/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 -60
- package/src/assets/js-runtime/core.js +11 -0
- package/src/assets/node-import-hooks.mjs +61 -0
- package/src/assets/toolchain-digests.json +19 -19
- package/src/assets/wasi-runtime/stubs.c +29 -0
- package/src/bin.js +257 -50
- package/src/index.js +3 -0
- package/src/integration/bindHeaderImports.js +18 -0
- package/src/integration/getCrossbindScript.js +46 -10
- package/src/integration/getDependFilePath.js +9 -0
- 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 +17 -9
- package/src/state/loadConfig.js +33 -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 +15 -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 +486 -0
- package/src/utils/bridgeRuntimeFixes.js +48 -0
- package/src/utils/bundledLicenses.js +26 -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 +58 -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/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 +101 -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/runCargo.js +48 -51
- package/src/utils/runConan.js +257 -0
- package/src/utils/runtimeEntries.js +56 -0
- package/src/utils/rustBridgeGen.js +9 -4
- 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 +69 -4
- package/src/utils/systemKeys.js +35 -3
- package/src/utils/targets.js +49 -4
- 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/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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The one module a package publishes per runtime binary, dist/<runtimeEnv>/<format>.mjs. It exports every
|
|
2
|
+
// bound name as an ESM live binding that initNative() fills, the way a bundler's module for a header does,
|
|
3
|
+
// so `import { initNative, X } from '<package>/node/napi'` works with or without init options.
|
|
4
|
+
|
|
5
|
+
const PROXY_NAMES = new Set(['initNative', 'AllSymbols']);
|
|
6
|
+
|
|
7
|
+
// A name is a plain string or { local, wire }: the second exports `local` while reading the differently
|
|
8
|
+
// registered `wire` off the module. Two headers may bind one name; the module exports it once.
|
|
9
|
+
export function uniqueBindings(names) {
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
return names
|
|
12
|
+
.map((name) => (typeof name === 'string' ? { local: name, wire: name } : name))
|
|
13
|
+
.filter(({ local }) => {
|
|
14
|
+
if (PROXY_NAMES.has(local) || seen.has(local)) return false;
|
|
15
|
+
seen.add(local);
|
|
16
|
+
return true;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Each runtime's loader, imported as `boot`, and the config it boots with; the binaries sit in dist, one
|
|
21
|
+
// directory above the entry.
|
|
22
|
+
export const ENTRY_RUNTIMES = {
|
|
23
|
+
// The Node-API loader and the Node wasm build are both CommonJS.
|
|
24
|
+
node: (loader) => ({
|
|
25
|
+
imports: ["import { createRequire } from 'node:module';", `const boot = createRequire(import.meta.url)('../${loader}');`],
|
|
26
|
+
config: 'config',
|
|
27
|
+
}),
|
|
28
|
+
// Edge runtimes cannot compile WebAssembly from bytes, so the binary is a module import their bundler resolves.
|
|
29
|
+
edge: (loader, wasm) => ({
|
|
30
|
+
imports: [`import boot from '../${loader}';`, `import wasm from '../${wasm}';`],
|
|
31
|
+
config: '{ getWasmFunction: () => wasm, ...config }',
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// A bound name may be a JavaScript reserved word, which an export list accepts but a declaration does not.
|
|
36
|
+
export function runtimeEntryModule(names, { imports, config }) {
|
|
37
|
+
const bindings = uniqueBindings(names);
|
|
38
|
+
return [
|
|
39
|
+
'// Generated by crossbind - do not edit.',
|
|
40
|
+
...imports,
|
|
41
|
+
'',
|
|
42
|
+
'export let AllSymbols = {};',
|
|
43
|
+
...bindings.map(({ local }) => `let $${local} = null;`),
|
|
44
|
+
...(bindings.length ? [`export { ${bindings.map(({ local }) => `$${local} as ${local}`).join(', ')} };`] : []),
|
|
45
|
+
'',
|
|
46
|
+
'export function initNative(config = {}) {',
|
|
47
|
+
` return boot(${config}).then((m) => {`,
|
|
48
|
+
' AllSymbols = m;',
|
|
49
|
+
...bindings.map(({ local, wire }) => ` $${local} = m.${wire};`),
|
|
50
|
+
' return m;',
|
|
51
|
+
' });',
|
|
52
|
+
'}',
|
|
53
|
+
'initNative.terminate = () => boot.terminate();',
|
|
54
|
+
'',
|
|
55
|
+
].join('\n');
|
|
56
|
+
}
|
|
@@ -179,6 +179,10 @@ function matchFnSignature(line) {
|
|
|
179
179
|
return [line, head[1], params, ret];
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
// What a lazy link pins to pull a bridge's registrations: the consumer's -u names `crossbind_keep_<lib>`.
|
|
183
|
+
export const keepSymbolName = (libName) => `crossbind_keep_${libName}`;
|
|
184
|
+
const keepSymbol = (libName) => `\n#[no_mangle]\npub extern "C" fn ${keepSymbolName(libName)}() {}\n`;
|
|
185
|
+
|
|
182
186
|
export default function generateRustBridge({ crateDir, vectors = [], dtsFile = null, keepName = null, dtsMode = 'sync', log = console.log }) {
|
|
183
187
|
const libRsPath = `${crateDir}/src/lib.rs`;
|
|
184
188
|
if (!fs.existsSync(libRsPath)) throw new Error(`crossbind: rust bridge: ${libRsPath} not found`);
|
|
@@ -192,7 +196,7 @@ export default function generateRustBridge({ crateDir, vectors = [], dtsFile = n
|
|
|
192
196
|
// own libstd, and fully loading two of them duplicates thousands of std symbols). Instead the
|
|
193
197
|
// consumer pins this keep symbol (-u/--undefined); codegen-units=1 puts it in the same object
|
|
194
198
|
// as the init-array constructor, so pulling it pulls the registrations - and only them.
|
|
195
|
-
if (keepName) bridge +=
|
|
199
|
+
if (keepName) bridge += keepSymbol(keepName);
|
|
196
200
|
if (dtsFile) writeIfChanged(dtsFile, emitDts(model, vectors, dtsMode));
|
|
197
201
|
|
|
198
202
|
const bridgeDir = `${crateDir}/.crossbind/bridge-crate`;
|
|
@@ -234,7 +238,8 @@ export default function generateRustBridge({ crateDir, vectors = [], dtsFile = n
|
|
|
234
238
|
// crate: the user file is embedded via `#[path] mod user;` (no copy) and the bridge lives in the
|
|
235
239
|
// same crate, so registrations reference `user::Type` directly. The bundler transformer calls
|
|
236
240
|
// this on import (like createBridgeFile for C++); the native builds compile every crate under
|
|
237
|
-
// <project>/.crossbind/rust-bridges/ and link
|
|
241
|
+
// <project>/.crossbind/rust-bridges/ into one staticlib and link it whole-archive, or on Windows
|
|
242
|
+
// through the keep symbol each bridge exports.
|
|
238
243
|
export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors = [], cargoDependencies = {}, dtsMode = 'sync', log = console.log }) {
|
|
239
244
|
const stem = path.basename(rsFile, '.rs').replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase();
|
|
240
245
|
const dir = `${cacheDir}/rust-bridges/${stem}`;
|
|
@@ -280,7 +285,7 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
|
|
|
280
285
|
].join('\n');
|
|
281
286
|
|
|
282
287
|
writeIfChanged(`${dir}/Cargo.toml`, manifest);
|
|
283
|
-
writeIfChanged(`${dir}/src/lib.rs`, bridge);
|
|
288
|
+
writeIfChanged(`${dir}/src/lib.rs`, bridge + keepSymbol(`${stem}_crossbind_app`));
|
|
284
289
|
// Relative `./x.rs` imports are typed by path resolution (ambient declarations only work
|
|
285
290
|
// for non-relative names like `cargo:x`), so the declaration mirrors the project-relative
|
|
286
291
|
// path under the language-neutral <cache>/types/ overlay (same home as .h declarations);
|
|
@@ -367,7 +372,7 @@ export function createCrateImportBridge({ crateName, modulePath = [], spec, cach
|
|
|
367
372
|
writeIfChanged(`${dir}/Cargo.toml`, manifestWith(shared.model.usesJson ? jsonDeps : []));
|
|
368
373
|
writeIfChanged(`${dir}/src/lib.rs`, emitBridge(shared.model, {
|
|
369
374
|
userCrate: crateIdent, vectors: [], log, rustPaths: shared.rustPaths, wireNames: shared.wireNames,
|
|
370
|
-
}));
|
|
375
|
+
}) + keepSymbol(`${stem}_crossbind_app`));
|
|
371
376
|
for (const { modulePath: importPath } of imports) {
|
|
372
377
|
writeImportDts({ cacheDir, crateName, modulePath: importPath, shared, dtsMode });
|
|
373
378
|
}
|
package/src/utils/rustMt.js
CHANGED
|
@@ -4,6 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
// One argv per flag: runCargo joins these with cargo's 0x1F separator, so a flag must not
|
|
5
5
|
// contain a space of its own.
|
|
6
6
|
const MT_RUSTFLAGS = ['-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals'];
|
|
7
|
+
// A musl std links Rust's own static libunwind under crt-static, its default; the addon's C++ already
|
|
8
|
+
// unwinds through libgcc_s, and one module must not carry two unwinders.
|
|
9
|
+
const MUSL_RUSTFLAGS = ['-Ctarget-feature=-crt-static'];
|
|
7
10
|
|
|
8
11
|
// mt (shared-memory) wasm needs every Rust object built with the atomics/bulk-memory features,
|
|
9
12
|
// std included - and rustup ships only a featureless std. The nightly-only -Zbuild-std rebuild
|
|
@@ -66,6 +69,7 @@ export function cargoBuildInvocation({
|
|
|
66
69
|
rustflags: [
|
|
67
70
|
...(wasmSysroot ? ['--sysroot', wasmSysroot] : []),
|
|
68
71
|
...(isMt ? MT_RUSTFLAGS : []),
|
|
72
|
+
...(target.platform === 'linuxmusl' ? MUSL_RUSTFLAGS : []),
|
|
69
73
|
],
|
|
70
74
|
panic: target.platform === 'wasm' ? 'abort' : undefined,
|
|
71
75
|
// Only the nightly build-std rebuild needs unstable flags; where a sysroot answers, stable
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
// Every package's loader bundles its own embind runtime, which keeps its state, and the functions its addon calls by
|
|
4
|
+
// name, on globalThis and Module. Shadowing both gives each bundle a scope of its own with the real global as its
|
|
5
|
+
// prototype, which the loader hands to its addon: addons of several packages then run in one process.
|
|
6
|
+
export default function scopedEmbind(embindFile) {
|
|
7
|
+
const realPath = (file) => {
|
|
8
|
+
try {
|
|
9
|
+
return fs.realpathSync(file);
|
|
10
|
+
} catch (e) {
|
|
11
|
+
return file;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const embind = realPath(embindFile);
|
|
15
|
+
return {
|
|
16
|
+
name: 'crossbind-scoped-embind',
|
|
17
|
+
transform(code, id) {
|
|
18
|
+
if (realPath(id) !== embind) return null;
|
|
19
|
+
return `const globalThis = Object.create(global);\nconst Module = (globalThis.Module = {});\n${code}\nexport { globalThis as crossbindScope };\n`;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { cloudCredentials, cloudUrl } from './cloudAccount.js';
|
|
2
|
+
import logger from './logger.js';
|
|
3
|
+
import systemKeys, { assertRunner } from './systemKeys.js';
|
|
4
|
+
|
|
5
|
+
const RUNNER_VARIABLE = 'CROSSBIND_RUNNER';
|
|
6
|
+
const SYSTEM_CONFIG = '~/.crossbind.json';
|
|
7
|
+
const SHARED = { key: 'REMOTE_URL', url: 'CROSSBIND_REMOTE_URL', token: 'CROSSBIND_TOKEN' };
|
|
8
|
+
|
|
9
|
+
export const remoteVariables = (role) => {
|
|
10
|
+
const suffix = `_${role.toUpperCase()}`;
|
|
11
|
+
return { key: `${SHARED.key}${suffix}`, url: `${SHARED.url}${suffix}`, token: `${SHARED.token}${suffix}` };
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Unchecked, so commands that run no step still work with an invalid RUNNER, `crossbind config set` among them.
|
|
15
|
+
function runnerSetting(system, env) {
|
|
16
|
+
if (env[RUNNER_VARIABLE]) return { runner: env[RUNNER_VARIABLE], name: RUNNER_VARIABLE };
|
|
17
|
+
return { runner: system?.RUNNER ?? systemKeys.RUNNER.default, name: `RUNNER in ${SYSTEM_CONFIG}` };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function chosenRunner(system, env = process.env) {
|
|
21
|
+
const { runner, name } = runnerSetting(system, env);
|
|
22
|
+
return assertRunner(runner, name);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const isLocalRunner = (system, env = process.env) => runnerSetting(system, env).runner === 'LOCAL';
|
|
26
|
+
|
|
27
|
+
// A variable wins over the same key in the system config, and an image's own address over the shared one.
|
|
28
|
+
// A token goes only to the address it is paired with, so a shared token never reaches the runner of one image.
|
|
29
|
+
function remoteAddress(role, system, env) {
|
|
30
|
+
for (const names of [remoteVariables(role), SHARED]) {
|
|
31
|
+
const url = env[names.url] || system?.[names.key];
|
|
32
|
+
if (url) {
|
|
33
|
+
const from = env[names.url] ? `$${names.url}` : `${names.key} in ${SYSTEM_CONFIG}`;
|
|
34
|
+
if (!['http:', 'https:'].includes(URL.parse(url)?.protocol)) {
|
|
35
|
+
throw new Error(`crossbind: ${from} is ${JSON.stringify(url)}, not an http or https address.`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
url, from, token: env[names.token], tokenVariable: names.token,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const announced = new Set();
|
|
46
|
+
|
|
47
|
+
// Each image says once which runner its steps go to and which settings sent them there.
|
|
48
|
+
function announce(role, remote, env) {
|
|
49
|
+
const key = `${role} ${remote.url}`;
|
|
50
|
+
if (announced.has(key)) return;
|
|
51
|
+
announced.add(key);
|
|
52
|
+
const chosenBy = env[RUNNER_VARIABLE] ? `$${RUNNER_VARIABLE}` : SYSTEM_CONFIG;
|
|
53
|
+
const shown = remote.url.replace(/\/\/[^/]*@/, '//');
|
|
54
|
+
logger.info(`crossbind: ${role} steps run on the runner at ${shown} (RUNNER=REMOTE from ${chosenBy}, address from ${remote.from}).`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A runner address named anywhere, for any image, keeps every image off the cloud: the sources go only where the
|
|
58
|
+
// user sends them.
|
|
59
|
+
const namesOwnRunner = (system, env) => Object.keys(systemKeys).some((key) => key.startsWith(SHARED.key) && system?.[key])
|
|
60
|
+
|| Object.keys(env).some((name) => name.startsWith(SHARED.url) && env[name]);
|
|
61
|
+
|
|
62
|
+
// The sign-in of `crossbind login` carries its own token; a token set for runners never reaches the cloud.
|
|
63
|
+
function cloudAddress(role, system, env) {
|
|
64
|
+
const url = cloudUrl(system, env);
|
|
65
|
+
const signedIn = cloudCredentials(url);
|
|
66
|
+
if (!signedIn) return null;
|
|
67
|
+
return {
|
|
68
|
+
url: `${url}/runner/${role}`, from: `crossbind login as ${signedIn.login}`, token: signedIn.token, tokenVariable: null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Under REMOTE, an image runs on its own address, else the shared one; a machine that names no runner address at all
|
|
73
|
+
// runs on crossbind cloud once signed in. Without any it gets no runner: its steps stop instead of running in the
|
|
74
|
+
// local Docker, where the build would only seem to run on a runner.
|
|
75
|
+
export function runnerFor(role, system, env = process.env) {
|
|
76
|
+
const runner = chosenRunner(system, env);
|
|
77
|
+
if (runner !== 'REMOTE') return { runner };
|
|
78
|
+
const remote = remoteAddress(role, system, env) ?? (namesOwnRunner(system, env) ? null : cloudAddress(role, system, env));
|
|
79
|
+
if (remote) announce(role, remote, env);
|
|
80
|
+
return { runner, remote };
|
|
81
|
+
}
|
|
@@ -6,12 +6,13 @@ import { getContentHash } from './hash.js';
|
|
|
6
6
|
// version, with provenance and licence metadata already claiming the new one.
|
|
7
7
|
export const SOURCE_FINGERPRINT_FILE = 'crossbind-source.fingerprint';
|
|
8
8
|
|
|
9
|
-
export function getSourceFingerprint(config) {
|
|
9
|
+
export function getSourceFingerprint(config, target) {
|
|
10
10
|
const nativeVersion = config?.package?.nativeVersion || null;
|
|
11
11
|
const sha256 = config?.build?.sha256 || null;
|
|
12
|
-
//
|
|
12
|
+
// Local-source packages pin neither; buildLib checks their native source inventory instead.
|
|
13
13
|
if (!nativeVersion && !sha256) return null;
|
|
14
|
-
|
|
14
|
+
const sourceReplacements = target ? config?.build?.sourceReplaceList?.(target, config.allDependencyPaths?.[target.path] ?? {}) : undefined;
|
|
15
|
+
return getContentHash(JSON.stringify({ nativeVersion, sha256, ...(sourceReplacements?.length ? { sourceReplacements } : {}) }));
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function isSourceFingerprintStale(libdir, fingerprint) {
|