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.
Files changed (132) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +2 -2
  3. package/package.json +7 -2
  4. package/src/actions/buildBinTools.js +0 -0
  5. package/src/actions/buildDependencies.js +15 -2
  6. package/src/actions/buildExternal.js +9 -1
  7. package/src/actions/buildJs.js +2 -9
  8. package/src/actions/buildLib.js +25 -8
  9. package/src/actions/buildNativeCommand.js +84 -0
  10. package/src/actions/buildNode.js +49 -10
  11. package/src/actions/buildTypes.js +8 -2
  12. package/src/actions/buildWasiCommand.js +10 -9
  13. package/src/actions/buildWasm.js +72 -31
  14. package/src/actions/cloudCommands.js +133 -0
  15. package/src/actions/createImportedBridges.js +71 -0
  16. package/src/actions/createInterface.js +247 -68
  17. package/src/actions/createLib.js +70 -42
  18. package/src/actions/getData.js +6 -2
  19. package/src/actions/getEnv.js +7 -0
  20. package/src/actions/getLinkInputs.js +12 -5
  21. package/src/actions/isSourceNewer.js +2 -0
  22. package/src/actions/licenses.js +143 -19
  23. package/src/actions/prepareConanDependencies.js +51 -0
  24. package/src/actions/publishBinary.js +32 -0
  25. package/src/actions/run.js +63 -66
  26. package/src/actions/runnerCommands.js +433 -0
  27. package/src/actions/writeRuntimeEntry.js +80 -0
  28. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  29. package/src/assets/cmake/command/command.cpp +1 -0
  30. package/src/assets/cmake/dist.cmake +2 -1
  31. package/src/assets/configure/install.sh +22 -0
  32. package/src/assets/cpp-runtime/node.cpp +18 -1
  33. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  34. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  35. package/src/assets/js-runtime/adapters/worker-comlink.js +138 -60
  36. package/src/assets/js-runtime/core.js +12 -0
  37. package/src/assets/node-import-hooks.mjs +61 -0
  38. package/src/assets/toolchain-digests.json +19 -19
  39. package/src/assets/wasi-runtime/http/README.md +17 -0
  40. package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
  41. package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
  42. package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
  43. package/src/assets/wasi-runtime/http/handler.cpp +298 -0
  44. package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
  45. package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
  46. package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
  47. package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
  48. package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
  49. package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
  50. package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
  51. package/src/assets/wasi-runtime/stubs.c +29 -0
  52. package/src/bin.js +218 -161
  53. package/src/cli.js +14 -0
  54. package/src/index.js +3 -0
  55. package/src/integration/bindHeaderImports.js +18 -0
  56. package/src/integration/getCrossbindScript.js +50 -13
  57. package/src/integration/getDependFilePath.js +9 -0
  58. package/src/integration/resolveNativeImport.js +14 -0
  59. package/src/node/dev.mjs +19 -0
  60. package/src/program.js +169 -0
  61. package/src/runner/blobs.js +135 -0
  62. package/src/runner/files.js +193 -0
  63. package/src/runner/server.js +320 -0
  64. package/src/state/attachConanDependencies.js +56 -0
  65. package/src/state/index.js +20 -11
  66. package/src/state/loadConfig.js +37 -35
  67. package/src/state/refreshBuiltDependencies.js +20 -0
  68. package/src/state/refreshConanDependencies.js +17 -0
  69. package/src/utils/androidToolchain.js +4 -0
  70. package/src/utils/appRustCrates.js +19 -1
  71. package/src/utils/appSources.js +97 -6
  72. package/src/utils/archiveFlags.js +12 -0
  73. package/src/utils/boundHeaders.js +79 -0
  74. package/src/utils/bridgeExtras.js +554 -0
  75. package/src/utils/bridgeRuntimeFixes.js +48 -0
  76. package/src/utils/bundledLicenses.js +26 -0
  77. package/src/utils/cargoLock.js +19 -0
  78. package/src/utils/cargoTarget.js +21 -3
  79. package/src/utils/cloudAccount.js +55 -0
  80. package/src/utils/conanDependencies.js +66 -0
  81. package/src/utils/conanImport.js +58 -0
  82. package/src/utils/conanInstall.js +154 -0
  83. package/src/utils/conanProfile.js +203 -0
  84. package/src/utils/conanStage.js +292 -0
  85. package/src/utils/copyToDistSource.js +13 -0
  86. package/src/utils/cppDts.js +60 -15
  87. package/src/utils/darwinToolchain.js +11 -0
  88. package/src/utils/dirLock.js +137 -28
  89. package/src/utils/downloadAndExtractFile.js +34 -7
  90. package/src/utils/embindArgumentGuards.js +41 -2
  91. package/src/utils/embindCallArguments.js +19 -0
  92. package/src/utils/embindJspiQueue.js +25 -0
  93. package/src/utils/embindObjectLifetime.js +35 -0
  94. package/src/utils/execContainer.js +49 -0
  95. package/src/utils/headerImports.js +16 -5
  96. package/src/utils/hostPlatform.js +21 -0
  97. package/src/utils/iosToolchain.js +6 -0
  98. package/src/utils/isSourceCmakePackage.js +10 -0
  99. package/src/utils/licenseReport.js +11 -2
  100. package/src/utils/linuxToolchain.js +24 -0
  101. package/src/utils/makeTreeWritable.js +12 -0
  102. package/src/utils/nativeImports.js +23 -0
  103. package/src/utils/nativeSourceStamp.js +36 -0
  104. package/src/utils/nodeDevBuild.js +92 -0
  105. package/src/utils/packageLicense.js +103 -0
  106. package/src/utils/pthreadMainScript.js +14 -0
  107. package/src/utils/pullDockerImage.js +3 -1
  108. package/src/utils/relocatePrebuilt.js +119 -0
  109. package/src/utils/remoteClient.js +127 -0
  110. package/src/utils/remoteOutputs.js +39 -0
  111. package/src/utils/remoteRunner.js +97 -0
  112. package/src/utils/remoteUpload.js +134 -0
  113. package/src/utils/replaceFile.js +8 -0
  114. package/src/utils/resetCMakeCacheForSource.js +26 -0
  115. package/src/utils/runCargo.js +48 -51
  116. package/src/utils/runConan.js +257 -0
  117. package/src/utils/runtimeEntries.js +56 -0
  118. package/src/utils/rustBridgeGen.js +22 -8
  119. package/src/utils/rustMt.js +9 -0
  120. package/src/utils/rustSysroot.js +7 -5
  121. package/src/utils/scopedEmbind.js +22 -0
  122. package/src/utils/selectRunner.js +81 -0
  123. package/src/utils/sourceFingerprint.js +4 -3
  124. package/src/utils/swigInterface.js +70 -4
  125. package/src/utils/systemConfig.js +13 -0
  126. package/src/utils/systemKeys.js +35 -3
  127. package/src/utils/targets.js +60 -4
  128. package/src/utils/toolchainNotices.js +5 -0
  129. package/src/utils/wasiHttp.js +50 -0
  130. package/src/utils/wasiToolchain.js +14 -1
  131. package/src/utils/windowsToolchain.js +18 -0
  132. package/vitest.config.js +11 -0
@@ -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
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
+ import { followCargoLock } from './cargoLock.js';
4
5
  import resolveEmbindRustRoot, { embindRustVersion } from './resolveEmbindRust.js';
5
6
  import runCargo, { toHostPath } from './runCargo.js';
6
7
  import writeIfChanged from './writeIfChanged.js';
@@ -179,6 +180,10 @@ function matchFnSignature(line) {
179
180
  return [line, head[1], params, ret];
180
181
  }
181
182
 
183
+ // What a lazy link pins to pull a bridge's registrations: the consumer's -u names `crossbind_keep_<lib>`.
184
+ export const keepSymbolName = (libName) => `crossbind_keep_${libName}`;
185
+ const keepSymbol = (libName) => `\n#[no_mangle]\npub extern "C" fn ${keepSymbolName(libName)}() {}\n`;
186
+
182
187
  export default function generateRustBridge({ crateDir, vectors = [], dtsFile = null, keepName = null, dtsMode = 'sync', log = console.log }) {
183
188
  const libRsPath = `${crateDir}/src/lib.rs`;
184
189
  if (!fs.existsSync(libRsPath)) throw new Error(`crossbind: rust bridge: ${libRsPath} not found`);
@@ -192,7 +197,7 @@ export default function generateRustBridge({ crateDir, vectors = [], dtsFile = n
192
197
  // own libstd, and fully loading two of them duplicates thousands of std symbols). Instead the
193
198
  // consumer pins this keep symbol (-u/--undefined); codegen-units=1 puts it in the same object
194
199
  // as the init-array constructor, so pulling it pulls the registrations - and only them.
195
- if (keepName) bridge += `\n#[no_mangle]\npub extern "C" fn crossbind_keep_${keepName}() {}\n`;
200
+ if (keepName) bridge += keepSymbol(keepName);
196
201
  if (dtsFile) writeIfChanged(dtsFile, emitDts(model, vectors, dtsMode));
197
202
 
198
203
  const bridgeDir = `${crateDir}/.crossbind/bridge-crate`;
@@ -234,7 +239,8 @@ export default function generateRustBridge({ crateDir, vectors = [], dtsFile = n
234
239
  // crate: the user file is embedded via `#[path] mod user;` (no copy) and the bridge lives in the
235
240
  // same crate, so registrations reference `user::Type` directly. The bundler transformer calls
236
241
  // this on import (like createBridgeFile for C++); the native builds compile every crate under
237
- // <project>/.crossbind/rust-bridges/ and link the staticlibs whole-archive.
242
+ // <project>/.crossbind/rust-bridges/ into one staticlib and link it whole-archive, or on Windows
243
+ // through the keep symbol each bridge exports.
238
244
  export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors = [], cargoDependencies = {}, dtsMode = 'sync', log = console.log }) {
239
245
  const stem = path.basename(rsFile, '.rs').replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase();
240
246
  const dir = `${cacheDir}/rust-bridges/${stem}`;
@@ -280,7 +286,7 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
280
286
  ].join('\n');
281
287
 
282
288
  writeIfChanged(`${dir}/Cargo.toml`, manifest);
283
- writeIfChanged(`${dir}/src/lib.rs`, bridge);
289
+ writeIfChanged(`${dir}/src/lib.rs`, bridge + keepSymbol(`${stem}_crossbind_app`));
284
290
  // Relative `./x.rs` imports are typed by path resolution (ambient declarations only work
285
291
  // for non-relative names like `cargo:x`), so the declaration mirrors the project-relative
286
292
  // path under the language-neutral <cache>/types/ overlay (same home as .h declarations);
@@ -298,7 +304,9 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
298
304
  // through the root and through a module registers once and is the same JS class for both.
299
305
  const crateSourceCache = new Map();
300
306
  const crateModelCache = new Map();
301
- export function createCrateImportBridge({ crateName, modulePath = [], spec, cacheDir, dtsMode = 'sync', log = console.log }) {
307
+ export function createCrateImportBridge({
308
+ crateName, modulePath = [], spec, cacheDir, projectDir, dtsMode = 'sync', log = console.log,
309
+ }) {
302
310
  const importName = [crateName, ...modulePath].join('/');
303
311
  const crateIdent = crateName.replaceAll('-', '_');
304
312
  const stem = `crate_${crateName.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()}`;
@@ -338,7 +346,9 @@ export function createCrateImportBridge({ crateName, modulePath = [], spec, cach
338
346
  // cargo metadata needs a resolvable lib target before the real bridge exists.
339
347
  if (!fs.existsSync(`${dir}/src/lib.rs`)) writeIfChanged(`${dir}/src/lib.rs`, '// crossbind placeholder\n');
340
348
 
341
- const { srcDir, features } = crateSource({ dir, depLine, crateName, importName });
349
+ const { srcDir, features } = crateSource({
350
+ dir, depLine, crateName, importName, projectDir,
351
+ });
342
352
  const ownKey = modulePath.join('::');
343
353
  const imports = [];
344
354
  for (const importPath of crateImports(cacheDir, crateName, modulePath)) {
@@ -367,7 +377,7 @@ export function createCrateImportBridge({ crateName, modulePath = [], spec, cach
367
377
  writeIfChanged(`${dir}/Cargo.toml`, manifestWith(shared.model.usesJson ? jsonDeps : []));
368
378
  writeIfChanged(`${dir}/src/lib.rs`, emitBridge(shared.model, {
369
379
  userCrate: crateIdent, vectors: [], log, rustPaths: shared.rustPaths, wireNames: shared.wireNames,
370
- }));
380
+ }) + keepSymbol(`${stem}_crossbind_app`));
371
381
  for (const { modulePath: importPath } of imports) {
372
382
  writeImportDts({ cacheDir, crateName, modulePath: importPath, shared, dtsMode });
373
383
  }
@@ -380,10 +390,14 @@ export function createCrateImportBridge({ crateName, modulePath = [], spec, cach
380
390
  }
381
391
 
382
392
  // cargo metadata locates the crate's source and the feature set it resolves to (fetching the crate
383
- // on first use); both hold for every import of the crate.
384
- function crateSource({ dir, depLine, crateName, importName }) {
393
+ // on first use); both hold for every import of the crate. It resolves with the project's pins, so
394
+ // the bridge is made from the version the app links.
395
+ function crateSource({
396
+ dir, depLine, crateName, importName, projectDir,
397
+ }) {
385
398
  const cacheKey = `${dir}|${depLine}`;
386
399
  if (crateSourceCache.has(cacheKey)) return crateSourceCache.get(cacheKey);
400
+ followCargoLock(projectDir, dir);
387
401
  let meta;
388
402
  try {
389
403
  const probe = runCargo(['metadata', '--format-version', '1', '--manifest-path', `${dir}/Cargo.toml`], {
@@ -1,9 +1,13 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
 
4
5
  // One argv per flag: runCargo joins these with cargo's 0x1F separator, so a flag must not
5
6
  // contain a space of its own.
6
7
  const MT_RUSTFLAGS = ['-Ctarget-feature=+atomics,+bulk-memory,+mutable-globals'];
8
+ // A musl std links Rust's own static libunwind under crt-static, its default; the addon's C++ already
9
+ // unwinds through libgcc_s, and one module must not carry two unwinders.
10
+ const MUSL_RUSTFLAGS = ['-Ctarget-feature=-crt-static'];
7
11
 
8
12
  // mt (shared-memory) wasm needs every Rust object built with the atomics/bulk-memory features,
9
13
  // std included - and rustup ships only a featureless std. The nightly-only -Zbuild-std rebuild
@@ -36,6 +40,10 @@ export function cargoTargetDirFor(baseDir, target) {
36
40
  // points at whatever version that image pinned, so the CLI carries no version of its own.
37
41
  const CONTAINER_SYSROOT = '/opt/crossbind/rust/current';
38
42
 
43
+ // True only inside the toolchain image. A CLI running there with RUNNER=LOCAL (the playground
44
+ // compiler) has these trees, but not rustup's stock wasm target.
45
+ export const hasContainerSysroot = () => fs.existsSync(CONTAINER_SYSROOT);
46
+
39
47
  export function cargoBuildInvocation({
40
48
  target, triple, targetDir, manifestPath, sysroot = false,
41
49
  }) {
@@ -66,6 +74,7 @@ export function cargoBuildInvocation({
66
74
  rustflags: [
67
75
  ...(wasmSysroot ? ['--sysroot', wasmSysroot] : []),
68
76
  ...(isMt ? MT_RUSTFLAGS : []),
77
+ ...(target.platform === 'linuxmusl' ? MUSL_RUSTFLAGS : []),
69
78
  ],
70
79
  panic: target.platform === 'wasm' ? 'abort' : undefined,
71
80
  // Only the nightly build-std rebuild needs unstable flags; where a sysroot answers, stable
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
5
5
  import withDirLock from './dirLock.js';
6
6
  import fetchOciLayer from './ociArtifact.js';
7
7
  import { cargoRunner } from './runCargo.js';
8
- import { isMtWasm } from './rustMt.js';
8
+ import { hasContainerSysroot, isMtWasm } from './rustMt.js';
9
9
  import PIN from './rustSysrootPin.js';
10
10
 
11
11
  // The second consumption channel for the Rust sysroots. A containerized build gets them as an image
@@ -149,7 +149,7 @@ let resolved = null;
149
149
  // Called from buildDependencies, the one async step every build path awaits before the sync work
150
150
  // starts. Only downloads when a target will actually consume it.
151
151
  export async function prepareRustSysroot(targets, log = console.log) {
152
- if (resolved || !PIN) return resolved;
152
+ if (resolved || !PIN || hasContainerSysroot()) return resolved;
153
153
  if (!targets.some((target) => isMtWasm(target) && cargoRunner(target) === 'LOCAL')) return null;
154
154
  try {
155
155
  resolved = await ensureRustSysroot({ image: PIN.image, index: PIN.index });
@@ -161,11 +161,13 @@ export async function prepareRustSysroot(targets, log = console.log) {
161
161
  return resolved;
162
162
  }
163
163
 
164
- // What cargoBuildInvocation takes for `sysroot`: true where the image carries the trees, an
165
- // absolute path where a downloaded artifact answers, false where mt must rebuild std on nightly.
166
- // st needs nothing - rustup's stock std is already correct for it.
164
+ // What cargoBuildInvocation takes for `sysroot`: true where the image carries the trees (also a
165
+ // LOCAL build running inside the image), an absolute path where a downloaded artifact answers,
166
+ // false where mt must rebuild std on nightly. st needs nothing - rustup's stock std is already
167
+ // correct for it.
167
168
  export function sysrootFor(target) {
168
169
  if (cargoRunner(target) !== 'LOCAL') return true;
170
+ if (target.platform === 'wasm' && hasContainerSysroot()) return true;
169
171
  if (!isMtWasm(target)) return false;
170
172
  return resolved ?? false;
171
173
  }
@@ -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
- // Packages that build from local sources pin neither; they keep the existence-only behaviour.
12
+ // Local-source packages pin neither; buildLib checks their native source inventory instead.
13
13
  if (!nativeVersion && !sha256) return null;
14
- return getContentHash(JSON.stringify({ nativeVersion, sha256 }));
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) {
@@ -16,6 +16,29 @@ export function findIgnoredDeclarations(headerFile, configs, headerPath) {
16
16
  return findHeaderOption('ignoredDeclarations', headerFile, configs, headerPath);
17
17
  }
18
18
 
19
+ // SWIG preprocessor lines a package puts right before one of its public headers, in export.swigPreamble: GDAL hides
20
+ // its C SRS API behind `#ifndef SWIG` for its own bindings, and picks VSIStatBufL's typedef by whether VSI_STAT64_T is
21
+ // defined, which a macro defined as itself answers while each platform's compiler still resolves the name.
22
+ export function findSwigPreamble(headerFile, configs, headerPath) {
23
+ return findHeaderOption('swigPreamble', headerFile, configs, headerPath);
24
+ }
25
+
26
+ // Includes SWIG reads inlined into one of a package's headers, in export.swigInlineIncludes. SWIG reads no #include, and
27
+ // libgeotiff fills its key and code enums from .inc files included inside the enum bodies, where the file markers
28
+ // -includeall wraps around an included file do not parse.
29
+ export function findSwigInlineIncludes(headerFile, configs, headerPath) {
30
+ return findHeaderOption('swigInlineIncludes', headerFile, configs, headerPath);
31
+ }
32
+
33
+ const INCLUDE_LINE = /^[ \t]*#[ \t]*include[ \t]*"([^"]+)"[^\n]*$/gm;
34
+
35
+ export function inlineIncludes(headerText, names, readInclude) {
36
+ const included = new Set([...headerText.matchAll(INCLUDE_LINE)].map(([, name]) => name));
37
+ const missing = names.filter((name) => !included.has(name));
38
+ if (missing.length) throw new Error(`crossbind: swigInlineIncludes lists ${missing.join(', ')}, which the header does not include.`);
39
+ return headerText.replace(INCLUDE_LINE, (line, name) => (names.includes(name) ? readInclude(name) : line));
40
+ }
41
+
19
42
  function findHeaderOption(option, headerFile, configs, headerPath) {
20
43
  const file = upath.normalize(headerFile);
21
44
  let owner = null;
@@ -40,26 +63,36 @@ export function interfaceIncludes(headerPath, prelude = []) {
40
63
 
41
64
  // The prelude and the completing includes reach only the compiled wrapper and swigMacros only SWIG's parse: SWIG still
42
65
  // wraps just the imported header. The ignored declarations follow the macros, so the retry without macros keeps them.
43
- export function buildInterfaceContent({ moduleName, headerPath, prelude = [], completing = [], swigMacros = [], ignored = [], constants = [] }) {
66
+ export function buildInterfaceContent({
67
+ moduleName, headerPath, prelude = [], completing = [], swigMacros = [], ignored = [], constants = [], preamble = [], functions = ALL_NAMES,
68
+ extras = [],
69
+ }) {
44
70
  const includes = [...new Set([...interfaceIncludes(headerPath, prelude), ...completing])]
45
71
  .map((header) => `#include "${header}"`)
46
72
  .join('\n');
47
73
  const macros = swigMacros.length ? `${swigMacros.join('\n')}\n\n` : '';
48
74
  const ignores = ignored.length ? `${ignored.map((name) => `%ignore ${name};`).join('\n')}\n\n` : '';
75
+ // The marker keeps a preamble that starts with #define apart from the macro block the retry drops.
76
+ const preambleBlock = preamble.length ? `// package preamble\n${preamble.join('\n')}\n\n` : '';
77
+ const extraBlock = extras.length ? `${extras.join('\n')}\n\n` : '';
78
+ // Named constants and functions are asked for ahead of the macros, which may define an imported name. Every constant
79
+ // is asked for after them: SWIG binds an interface's own #define lines, and the prelude only carries other headers'
80
+ // values for the parse.
81
+ const everyConstant = constants === ALL_NAMES;
49
82
  const module = moduleName.replace(/\W/g, '_');
50
83
  return `#ifndef _${module}_I
51
84
  #define _${module}_I
52
85
 
53
86
  %module ${module}
54
87
 
55
- %{
88
+ ${extraBlock}%{
56
89
  ${includes}
57
90
  %}
58
91
 
59
- ${constantRequests(constants)}%feature("shared_ptr");
92
+ ${everyConstant ? '' : constantRequests(constants)}${functionRequests(functions)}%feature("shared_ptr");
60
93
  %feature("polymorphic_shared_ptr");
61
94
 
62
- ${macros}${ignores}%include "${headerPath}"
95
+ ${macros}${ignores}${preambleBlock}${everyConstant ? constantRequests(constants) : ''}%include "${headerPath}"
63
96
 
64
97
  #endif
65
98
  `;
@@ -71,6 +104,19 @@ function constantRequests(constants) {
71
104
  return constants.length ? `${constants.map((name) => `%feature("embind:constant") ${name};`).join('\n')}\n\n` : '';
72
105
  }
73
106
 
107
+ // SWIG ignores every global function, then wraps the listed names again: class members, enums and constants stay. The
108
+ // package's own ignores come after, since SWIG applies the later of two renames of a name.
109
+ function functionRequests(functions) {
110
+ if (functions === ALL_NAMES) return '';
111
+ const names = [...new Set(functions)].sort();
112
+ return `%rename($ignore, %$isfunction, %$isglobal) "";\n${names.map((name) => `%rename("%s") ${name};\n`).join('')}\n`;
113
+ }
114
+
115
+ // Editor declarations need the whole header even when the compiled bridge selects functions by import.
116
+ export function interfaceWithAllFunctions(content) {
117
+ return content.replace(/%rename\(\$ignore, %\$isfunction, %\$isglobal\) "";\n(?:%rename\("%s"\) [A-Za-z_]\w*;\n)*\n/g, '');
118
+ }
119
+
74
120
  export function withoutSwigMacros(content) {
75
121
  return content.replace(/(%feature\("polymorphic_shared_ptr"\);\n\n)(?:#define [^\n]*\n)+\n/, '$1');
76
122
  }
@@ -333,6 +379,26 @@ function walkScopes(text, visit) {
333
379
  });
334
380
  }
335
381
 
382
+ // Whether the token at `index` sits inside the parentheses of the declaration it belongs to: a prototype's parameter.
383
+ function isInsideParentheses(tokens, index) {
384
+ let depth = 0;
385
+ for (let at = index - 1; at >= 0 && ![';', '{', '}'].includes(tokens[at]); at -= 1) {
386
+ if (tokens[at] === '(') depth += 1;
387
+ else if (tokens[at] === ')') depth -= 1;
388
+ }
389
+ return depth > 0;
390
+ }
391
+
392
+ // Whether a variable `name` is declared outside every class, namespace and block, as `extern int name;` is.
393
+ export function declaresAtFileScope(text, name) {
394
+ let found = false;
395
+ walkScopes(text, (token, index, tokens, scopes) => {
396
+ if (token === name && scopes.every((scope) => scope.kind === 'extern') && [';', '=', '[', ','].includes(tokens[index + 1])
397
+ && !isInsideParentheses(tokens, index)) found = true;
398
+ });
399
+ return found;
400
+ }
401
+
336
402
  // Classes, structs and enums by qualified name, including the names C typedefs give them, each mapped to the first header
337
403
  // defining it. Preprocessor branches are all read.
338
404
  export function indexTypeDefinitions(files) {
@@ -0,0 +1,13 @@
1
+ import os from 'node:os';
2
+ import loadJson from './loadJson.js';
3
+ import systemKeys from './systemKeys.js';
4
+
5
+ export const systemConfigPath = () => `${os.homedir()}/.crossbind.json`;
6
+
7
+ export default function loadSystemConfig() {
8
+ const system = loadJson(systemConfigPath()) || {};
9
+ const defaults = Object.entries(systemKeys)
10
+ .filter(([key]) => !(key in system))
11
+ .map(([key, value]) => [key, value.default]);
12
+ return { ...system, ...Object.fromEntries(defaults) };
13
+ }
@@ -4,12 +4,36 @@ const systemKeys = {
4
4
  default: '',
5
5
  },
6
6
  RUNNER: {
7
- description: 'The execution environment for running the application.',
8
- options: ['DOCKER_RUN', 'DOCKER_EXEC', 'LOCAL'],
7
+ description: 'Where toolchain steps run: DOCKER_RUN, DOCKER_EXEC, LOCAL, or REMOTE on the runners REMOTE_URL and REMOTE_URL_<IMAGE> name; a machine that names none builds on crossbind cloud once signed in with crossbind login. The CROSSBIND_RUNNER environment variable overrides it.',
8
+ options: ['DOCKER_RUN', 'DOCKER_EXEC', 'LOCAL', 'REMOTE'],
9
9
  default: 'DOCKER_RUN',
10
10
  },
11
+ REMOTE_URL: {
12
+ description: 'Under RUNNER=REMOTE, the runner of every image without an address of its own. Its token comes from the CROSSBIND_TOKEN environment variable. The CROSSBIND_REMOTE_URL environment variable overrides it.',
13
+ default: '',
14
+ },
15
+ REMOTE_URL_WEB: {
16
+ description: 'Under RUNNER=REMOTE, the runner of the web image: wasm builds, wasi builds and the bridge steps of every platform but android. Token: CROSSBIND_TOKEN_WEB. The CROSSBIND_REMOTE_URL_WEB environment variable overrides it.',
17
+ default: '',
18
+ },
19
+ REMOTE_URL_ANDROID: {
20
+ description: 'Under RUNNER=REMOTE, the runner of the android image. Token: CROSSBIND_TOKEN_ANDROID. The CROSSBIND_REMOTE_URL_ANDROID environment variable overrides it.',
21
+ default: '',
22
+ },
23
+ REMOTE_URL_LINUX: {
24
+ description: 'Under RUNNER=REMOTE, the runner of the linux image (linux and linuxmusl builds). Token: CROSSBIND_TOKEN_LINUX. The CROSSBIND_REMOTE_URL_LINUX environment variable overrides it.',
25
+ default: '',
26
+ },
27
+ REMOTE_URL_WINDOWS: {
28
+ description: 'Under RUNNER=REMOTE, the runner of the windows image (win32 builds). Token: CROSSBIND_TOKEN_WINDOWS. The CROSSBIND_REMOTE_URL_WINDOWS environment variable overrides it.',
29
+ default: '',
30
+ },
31
+ CLOUD_URL: {
32
+ description: 'The crossbind cloud that crossbind login, logout and usage talk to. The CROSSBIND_CLOUD_URL environment variable overrides it.',
33
+ default: 'https://api.crossbind.dev',
34
+ },
11
35
  WASI_SDK_PATH: {
12
- description: 'Path to an extracted wasi-sdk (>= 34, wasm32-wasip3 sysroot) used by platform:\'wasi\' builds. The CROSSBIND_WASI_SDK_PATH environment variable overrides it.',
36
+ description: 'Path to an extracted wasi-sdk (>= 34, wasm32-wasip3 sysroot) that platform:\'wasi\' builds use under RUNNER=LOCAL; the other runners use the sdk in the image. The CROSSBIND_WASI_SDK_PATH environment variable overrides it.',
13
37
  default: '',
14
38
  },
15
39
  DOCKER_REGISTRY_MIRROR: {
@@ -35,3 +59,11 @@ const systemKeys = {
35
59
  };
36
60
 
37
61
  export default systemKeys;
62
+
63
+ // Read as LOCAL, a runner crossbind does not know would run recipes and build scripts on the host.
64
+ export function assertRunner(runner, name = 'RUNNER') {
65
+ if (!systemKeys.RUNNER.options.includes(runner)) {
66
+ throw new Error(`crossbind: the runner ${runner} is invalid; ${name} is one of ${systemKeys.RUNNER.options.join(', ')}.`);
67
+ }
68
+ return runner;
69
+ }
@@ -1,7 +1,7 @@
1
1
  // Single source for the build-target matrix and its naming; state/ copies (and then
2
2
  // decorates) these entries, and consumer runtimes read them without touching CLI state.
3
3
 
4
- export const TARGETS = [
4
+ const BASE_TARGETS = [
5
5
  {
6
6
  platform: 'wasm', arch: 'wasm32', runtime: 'st', buildType: 'release', runtimeEnv: 'browser',
7
7
  },
@@ -63,10 +63,16 @@ export const TARGETS = [
63
63
  platform: 'wasm', arch: 'wasm64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
64
64
  },
65
65
  {
66
- platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'release',
66
+ platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'release', runtimeEnv: 'wasi',
67
+ },
68
+ {
69
+ platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'wasi',
67
70
  },
68
71
  {
69
- platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug',
72
+ platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'release', runtimeEnv: 'http',
73
+ },
74
+ {
75
+ platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'http',
70
76
  },
71
77
  {
72
78
  platform: 'android', arch: 'arm64-v8a', runtime: 'mt', buildType: 'release',
@@ -116,6 +122,18 @@ export const TARGETS = [
116
122
  {
117
123
  platform: 'linux', arch: 'x64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
118
124
  },
125
+ {
126
+ platform: 'linuxmusl', arch: 'arm64', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
127
+ },
128
+ {
129
+ platform: 'linuxmusl', arch: 'arm64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
130
+ },
131
+ {
132
+ platform: 'linuxmusl', arch: 'x64', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
133
+ },
134
+ {
135
+ platform: 'linuxmusl', arch: 'x64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
136
+ },
119
137
  {
120
138
  platform: 'win32', arch: 'arm64', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
121
139
  },
@@ -130,17 +148,43 @@ export const TARGETS = [
130
148
  },
131
149
  ];
132
150
 
151
+ // A native executable links the same archives as the Node-API addon of its platform and arch.
152
+ export const TARGETS = [
153
+ ...BASE_TARGETS,
154
+ ...BASE_TARGETS
155
+ .filter((target) => target.runtimeEnv === 'node' && target.platform !== 'wasm')
156
+ .map((target) => ({ ...target, runtimeEnv: 'native' })),
157
+ ];
158
+
133
159
  // Built only when named with -p: a plain `crossbind build` keeps producing what it did before
134
160
  // native Node.js addons existed.
135
- export const OPT_IN_PLATFORMS = ['darwin', 'linux', 'win32'];
161
+ export const OPT_IN_PLATFORMS = ['darwin', 'linux', 'linuxmusl', 'win32'];
136
162
 
137
163
  // Built on the host itself: Apple's linker and SDKs exist in no toolchain image.
138
164
  export const HOST_BUILT_PLATFORMS = ['ios', 'darwin'];
139
165
 
166
+ // The runtime environment names the binary a build makes; android and ios have none, since
167
+ // React Native links them.
168
+ export const runtimeEnvsOf = (platform) => [
169
+ ...new Set(TARGETS.filter((target) => target.platform === platform && target.runtimeEnv).map((target) => target.runtimeEnv)),
170
+ ];
171
+
172
+ export const RUNTIME_ENVS = [...new Set(TARGETS.map((target) => target.runtimeEnv).filter(Boolean))];
173
+
174
+ // A build makes only the binaries it is asked for: -e first, then the config's target.runtimeEnv.
175
+ // Bundler plugins ask through getTargetParams instead.
176
+ export function selectRuntimeEnvs(cliRuntimeEnvs, configRuntimeEnv) {
177
+ if (cliRuntimeEnvs?.length) return cliRuntimeEnvs;
178
+ return configRuntimeEnv ? [configRuntimeEnv] : [];
179
+ }
180
+
140
181
  export function targetPathOf(target) {
141
182
  return `${target.platform}-${target.arch}-${target.runtime}-${target.buildType}`;
142
183
  }
143
184
 
185
+ // The release build of a target, whose prebuilts a debug build of it falls back to.
186
+ export const releaseTargetOf = (target) => ({ ...target, buildType: 'release', path: target.releasePath });
187
+
144
188
  // Every platform/arch addon of one build type sits next to a single loader, which fills in
145
189
  // {platform} and {arch} from the running process. The loader is CommonJS whatever the package
146
190
  // "type" says, since it needs __dirname and process.dlopen.
@@ -154,6 +198,18 @@ export function nodeAddonNamesOf(target, projectName) {
154
198
  };
155
199
  }
156
200
 
201
+ // A wasi build is one component, with no JS glue: the command keeps the plain name, an http component adds its own.
202
+ export const wasiNamesOf = (target, projectName) => ({
203
+ wasmName: `${projectName}-${targetPathOf(target)}${target.runtimeEnv === 'http' ? '.http' : ''}.wasm`,
204
+ });
205
+
206
+ // One executable per platform, arch and build type, named like the addons it replaces.
207
+ export function nativeCommandNamesOf(target, projectName) {
208
+ const suffix = target.buildType === 'debug' ? '.debug' : '';
209
+ const extension = target.platform === 'win32' ? '.exe' : '';
210
+ return { commandName: `${projectName}.${target.platform}-${target.arch}${suffix}${extension}` };
211
+ }
212
+
157
213
  export function filterTargetSpecs(targetSpecs, target) {
158
214
  return targetSpecs?.filter(t => (
159
215
  (!t.platform || t.platform === target.platform)
@@ -0,0 +1,5 @@
1
+ // Where a build keeps the notices of the runtime a toolchain image linked into its binaries,
2
+ // relative to the build directory; crossbind licenses reads them from there.
3
+ export default function toolchainNoticesDir(platform) {
4
+ return `toolchain-licenses/${platform}`;
5
+ }