crossbind 2.0.0-beta.60 → 2.0.0-beta.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +8 -6
  2. package/package.json +6 -1
  3. package/src/actions/buildDependencies.js +37 -9
  4. package/src/actions/buildExternal.js +9 -1
  5. package/src/actions/buildJs.js +1 -1
  6. package/src/actions/buildLib.js +27 -8
  7. package/src/actions/buildNativeCommand.js +84 -0
  8. package/src/actions/buildNode.js +190 -0
  9. package/src/actions/buildTypes.js +8 -2
  10. package/src/actions/buildWasiCommand.js +0 -4
  11. package/src/actions/buildWasm.js +92 -95
  12. package/src/actions/cloudCommands.js +133 -0
  13. package/src/actions/createImportedBridges.js +64 -0
  14. package/src/actions/createInterface.js +318 -65
  15. package/src/actions/createLib.js +60 -43
  16. package/src/actions/getAllBridges.js +1 -1
  17. package/src/actions/getData.js +4 -1
  18. package/src/actions/getLinkInputs.js +70 -0
  19. package/src/actions/isSourceNewer.js +2 -0
  20. package/src/actions/licenses.js +175 -17
  21. package/src/actions/prepareConanDependencies.js +51 -0
  22. package/src/actions/publishBinary.js +32 -0
  23. package/src/actions/run.js +124 -32
  24. package/src/actions/runnerCommands.js +421 -0
  25. package/src/actions/writeRuntimeEntry.js +69 -0
  26. package/src/assets/cmake/CMakeLists.txt +1 -1
  27. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  28. package/src/assets/cmake/command/command.cpp +1 -0
  29. package/src/assets/cmake/dist.cmake +2 -1
  30. package/src/assets/configure/install.sh +22 -0
  31. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  32. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  33. package/src/assets/js-runtime/adapters/worker-comlink.js +87 -56
  34. package/src/assets/js-runtime/core.js +11 -0
  35. package/src/assets/node-import-hooks.mjs +61 -0
  36. package/src/assets/toolchain-digests.json +27 -13
  37. package/src/assets/wasi-runtime/stubs.c +29 -0
  38. package/src/bin.js +290 -37
  39. package/src/index.js +6 -1
  40. package/src/integration/bindHeaderImports.js +18 -0
  41. package/src/integration/getCrossbindScript.js +53 -19
  42. package/src/integration/getDependFilePath.js +18 -12
  43. package/src/integration/resolveNativeImport.js +14 -0
  44. package/src/node/dev.mjs +19 -0
  45. package/src/runner/blobs.js +135 -0
  46. package/src/runner/files.js +193 -0
  47. package/src/runner/server.js +320 -0
  48. package/src/state/attachConanDependencies.js +56 -0
  49. package/src/state/index.js +31 -20
  50. package/src/state/loadConfig.js +36 -24
  51. package/src/state/refreshBuiltDependencies.js +20 -0
  52. package/src/state/refreshConanDependencies.js +17 -0
  53. package/src/utils/androidToolchain.js +4 -0
  54. package/src/utils/appRustCrates.js +17 -1
  55. package/src/utils/appSources.js +112 -0
  56. package/src/utils/archiveFlags.js +12 -0
  57. package/src/utils/boundHeaders.js +79 -0
  58. package/src/utils/bridgeExtras.js +486 -0
  59. package/src/utils/bridgeRuntimeFixes.js +48 -0
  60. package/src/utils/bundledLicenses.js +26 -0
  61. package/src/utils/cargoImport.js +44 -0
  62. package/src/utils/cargoTarget.js +23 -3
  63. package/src/utils/cloudAccount.js +55 -0
  64. package/src/utils/conanDependencies.js +66 -0
  65. package/src/utils/conanImport.js +58 -0
  66. package/src/utils/conanInstall.js +154 -0
  67. package/src/utils/conanProfile.js +203 -0
  68. package/src/utils/conanStage.js +292 -0
  69. package/src/utils/copyToDistSource.js +13 -0
  70. package/src/utils/cppDts.js +134 -55
  71. package/src/utils/darwinToolchain.js +11 -0
  72. package/src/utils/dirLock.js +137 -28
  73. package/src/utils/downloadAndExtractFile.js +34 -7
  74. package/src/utils/embindArgumentGuards.js +84 -0
  75. package/src/utils/embindCallArguments.js +19 -0
  76. package/src/utils/embindJspiQueue.js +25 -0
  77. package/src/utils/embindRsFingerprint.js +26 -0
  78. package/src/utils/execContainer.js +49 -0
  79. package/src/utils/headerImports.js +56 -0
  80. package/src/utils/hostPlatform.js +21 -0
  81. package/src/utils/inputStamp.js +23 -0
  82. package/src/utils/iosToolchain.js +6 -0
  83. package/src/utils/isSourceCmakePackage.js +10 -0
  84. package/src/utils/licenseReport.js +11 -2
  85. package/src/utils/linkLayout.js +8 -1
  86. package/src/utils/linuxToolchain.js +24 -0
  87. package/src/utils/makeTreeWritable.js +12 -0
  88. package/src/utils/nativeImports.js +23 -0
  89. package/src/utils/nativeSourceStamp.js +36 -0
  90. package/src/utils/nodeDevBuild.js +92 -0
  91. package/src/utils/packageLicense.js +101 -0
  92. package/src/utils/pthreadMainScript.js +14 -0
  93. package/src/utils/pullDockerImage.js +10 -5
  94. package/src/utils/relocatePrebuilt.js +119 -0
  95. package/src/utils/remoteClient.js +127 -0
  96. package/src/utils/remoteOutputs.js +39 -0
  97. package/src/utils/remoteRunner.js +97 -0
  98. package/src/utils/remoteUpload.js +134 -0
  99. package/src/utils/replaceFile.js +8 -0
  100. package/src/utils/resolveCompanionPackage.js +51 -0
  101. package/src/utils/resolveEmbindNapi.js +22 -0
  102. package/src/utils/resolveEmbindRust.js +17 -52
  103. package/src/utils/runCargo.js +51 -53
  104. package/src/utils/runConan.js +257 -0
  105. package/src/utils/runtimeEntries.js +56 -0
  106. package/src/utils/rustBridgeGen.js +903 -253
  107. package/src/utils/rustMt.js +4 -0
  108. package/src/utils/scopedEmbind.js +22 -0
  109. package/src/utils/selectRunner.js +81 -0
  110. package/src/utils/sourceFingerprint.js +4 -3
  111. package/src/utils/swigInterface.js +86 -6
  112. package/src/utils/systemKeys.js +43 -3
  113. package/src/utils/targets.js +104 -3
  114. package/src/utils/toolchainNotices.js +5 -0
  115. package/src/utils/wasiToolchain.js +14 -1
  116. package/src/utils/windowsToolchain.js +18 -0
  117. package/vitest.config.js +11 -0
  118. package/src/utils/cppFieldBindings.js +0 -23
@@ -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
- // 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) {
@@ -1,4 +1,5 @@
1
1
  import upath from 'upath';
2
+ import { ALL_NAMES } from './headerImports.js';
2
3
 
3
4
  // Libraries whose headers are not self-contained (spatialite expects sqlite3.h first) list the
4
5
  // prerequisites in export.headerPrelude of the package that ships the header: its output or project
@@ -15,6 +16,29 @@ export function findIgnoredDeclarations(headerFile, configs, headerPath) {
15
16
  return findHeaderOption('ignoredDeclarations', headerFile, configs, headerPath);
16
17
  }
17
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
+
18
42
  function findHeaderOption(option, headerFile, configs, headerPath) {
19
43
  const file = upath.normalize(headerFile);
20
44
  let owner = null;
@@ -39,31 +63,59 @@ export function interfaceIncludes(headerPath, prelude = []) {
39
63
 
40
64
  // The prelude and the completing includes reach only the compiled wrapper and swigMacros only SWIG's parse: SWIG still
41
65
  // wraps just the imported header. The ignored declarations follow the macros, so the retry without macros keeps them.
42
- export function buildInterfaceContent({ moduleName, headerPath, prelude = [], completing = [], swigMacros = [], ignored = [] }) {
66
+ export function buildInterfaceContent({
67
+ moduleName, headerPath, prelude = [], completing = [], swigMacros = [], ignored = [], constants = [], preamble = [], functions = ALL_NAMES,
68
+ extras = [],
69
+ }) {
43
70
  const includes = [...new Set([...interfaceIncludes(headerPath, prelude), ...completing])]
44
71
  .map((header) => `#include "${header}"`)
45
72
  .join('\n');
46
73
  const macros = swigMacros.length ? `${swigMacros.join('\n')}\n\n` : '';
47
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 are asked for ahead of the macros, which may define one. Every constant is asked for after them:
79
+ // SWIG binds an interface's own #define lines, and the prelude only carries other headers' values for the parse.
80
+ const everyConstant = constants === ALL_NAMES;
48
81
  const module = moduleName.replace(/\W/g, '_');
49
82
  return `#ifndef _${module}_I
50
83
  #define _${module}_I
51
84
 
52
85
  %module ${module}
53
86
 
54
- %{
87
+ ${extraBlock}%{
55
88
  ${includes}
56
89
  %}
57
90
 
58
- %feature("shared_ptr");
91
+ ${everyConstant ? '' : constantRequests(constants)}%feature("shared_ptr");
59
92
  %feature("polymorphic_shared_ptr");
60
93
 
61
- ${macros}${ignores}%include "${headerPath}"
94
+ ${macros}${functionRequests(functions)}${ignores}${preambleBlock}${everyConstant ? constantRequests(constants) : ''}%include "${headerPath}"
62
95
 
63
96
  #endif
64
97
  `;
65
98
  }
66
99
 
100
+ // SWIG binds a constant only on request, so a header's thousands of macros cost nothing until the app imports one.
101
+ function constantRequests(constants) {
102
+ if (constants === ALL_NAMES) return '%feature("embind:constant");\n\n';
103
+ return constants.length ? `${constants.map((name) => `%feature("embind:constant") ${name};`).join('\n')}\n\n` : '';
104
+ }
105
+
106
+ // SWIG ignores every global function, then wraps the listed names again: class members, enums and constants stay. The
107
+ // package's own ignores come after, since SWIG applies the later of two renames of a name.
108
+ function functionRequests(functions) {
109
+ if (functions === ALL_NAMES) return '';
110
+ const names = [...new Set(functions)].sort();
111
+ return `%rename($ignore, %$isfunction, %$isglobal) "";\n${names.map((name) => `%rename("%s") ${name};\n`).join('')}\n`;
112
+ }
113
+
114
+ // Editor declarations need the whole header even when the compiled bridge selects functions by import.
115
+ export function interfaceWithAllFunctions(content) {
116
+ return content.replace(/%rename\(\$ignore, %\$isfunction, %\$isglobal\) "";\n(?:%rename\("%s"\) [A-Za-z_]\w*;\n)*\n/g, '');
117
+ }
118
+
67
119
  export function withoutSwigMacros(content) {
68
120
  return content.replace(/(%feature\("polymorphic_shared_ptr"\);\n\n)(?:#define [^\n]*\n)+\n/, '$1');
69
121
  }
@@ -111,7 +163,7 @@ const tokenize = (text) => text.match(TOKEN) ?? [];
111
163
  const identifiers = (text) => tokenize(text).filter((token) => IDENTIFIER.test(token));
112
164
  // Lines are spliced before comments go, as in the compiler, and a comment marker inside a literal is text.
113
165
  const LITERAL_OR_COMMENT = /"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g;
114
- const stripComments = (text) => text
166
+ export const stripComments = (text) => text
115
167
  .replace(/\\\r?\n/g, ' ')
116
168
  .replace(LITERAL_OR_COMMENT, (match) => (match.startsWith('/') ? ' ' : match));
117
169
 
@@ -154,7 +206,7 @@ function headerReferences(headerText) {
154
206
  // SWIG parses only the imported header, so export, linkage and attribute macros from its #includes read as syntax
155
207
  // errors. Those come from the compiler's macro table, with the flags and versions its #if lines test; type and value
156
208
  // macros stay undefined so every platform's compiler resolves them.
157
- export function selectSwigMacros({ headerText, macros, predefined }) {
209
+ export function selectSwigMacros({ headerText, macros, predefined, constants = [] }) {
158
210
  const memoize = (classify) => {
159
211
  const results = new Map();
160
212
  const check = (name) => {
@@ -190,6 +242,14 @@ export function selectSwigMacros({ headerText, macros, predefined }) {
190
242
  };
191
243
  declarations.forEach((name) => forward(name, isSyntax));
192
244
  conditions.forEach((name) => forward(name, isConstant));
245
+ // An imported constant reaches SWIG with the macros its value uses, so SWIG can type it: SWIG reads no #include, and
246
+ // drops a constant whose value uses a macro it has not seen.
247
+ const isObjectConstant = (name) => !macros.get(name).params && isConstant(name);
248
+ const imported = constants === ALL_NAMES ? [...defined] : constants;
249
+ imported.forEach((name) => {
250
+ if (!defined.has(name)) forward(name, isObjectConstant);
251
+ else identifiers(macros.get(name)?.body ?? '').forEach((token) => forward(token, isObjectConstant));
252
+ });
193
253
 
194
254
  const visible = new Set([
195
255
  ...tokenize(stripComments(headerText)),
@@ -318,6 +378,26 @@ function walkScopes(text, visit) {
318
378
  });
319
379
  }
320
380
 
381
+ // Whether the token at `index` sits inside the parentheses of the declaration it belongs to: a prototype's parameter.
382
+ function isInsideParentheses(tokens, index) {
383
+ let depth = 0;
384
+ for (let at = index - 1; at >= 0 && ![';', '{', '}'].includes(tokens[at]); at -= 1) {
385
+ if (tokens[at] === '(') depth += 1;
386
+ else if (tokens[at] === ')') depth -= 1;
387
+ }
388
+ return depth > 0;
389
+ }
390
+
391
+ // Whether a variable `name` is declared outside every class, namespace and block, as `extern int name;` is.
392
+ export function declaresAtFileScope(text, name) {
393
+ let found = false;
394
+ walkScopes(text, (token, index, tokens, scopes) => {
395
+ if (token === name && scopes.every((scope) => scope.kind === 'extern') && [';', '=', '[', ','].includes(tokens[index + 1])
396
+ && !isInsideParentheses(tokens, index)) found = true;
397
+ });
398
+ return found;
399
+ }
400
+
321
401
  // Classes, structs and enums by qualified name, including the names C typedefs give them, each mapped to the first header
322
402
  // defining it. Preprocessor branches are all read.
323
403
  export function indexTypeDefinitions(files) {
@@ -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: {
@@ -24,6 +48,22 @@ const systemKeys = {
24
48
  description: 'Image reference used for android builds instead of the pinned one. A reference without the release digest disables the reproducibility guarantee. The CROSSBIND_IMAGE_ANDROID environment variable overrides it.',
25
49
  default: '',
26
50
  },
51
+ DOCKER_IMAGE_LINUX: {
52
+ description: 'Image reference used for linux (Node.js addon) builds instead of the pinned one. A reference without the release digest disables the reproducibility guarantee. The CROSSBIND_IMAGE_LINUX environment variable overrides it.',
53
+ default: '',
54
+ },
55
+ DOCKER_IMAGE_WINDOWS: {
56
+ description: 'Image reference used for win32 (Node.js addon) builds instead of the pinned one. A reference without the release digest disables the reproducibility guarantee. The CROSSBIND_IMAGE_WINDOWS environment variable overrides it.',
57
+ default: '',
58
+ },
27
59
  };
28
60
 
29
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,10 @@ 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
67
  },
68
68
  {
69
- platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug',
69
+ platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'wasi',
70
70
  },
71
71
  {
72
72
  platform: 'android', arch: 'arm64-v8a', runtime: 'mt', buildType: 'release',
@@ -92,12 +92,113 @@ export const TARGETS = [
92
92
  {
93
93
  platform: 'ios', arch: 'iphonesimulator', runtime: 'mt', buildType: 'debug',
94
94
  },
95
+ {
96
+ platform: 'darwin', arch: 'arm64', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
97
+ },
98
+ {
99
+ platform: 'darwin', arch: 'arm64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
100
+ },
101
+ {
102
+ platform: 'darwin', arch: 'x64', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
103
+ },
104
+ {
105
+ platform: 'darwin', arch: 'x64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
106
+ },
107
+ {
108
+ platform: 'linux', arch: 'arm64', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
109
+ },
110
+ {
111
+ platform: 'linux', arch: 'arm64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
112
+ },
113
+ {
114
+ platform: 'linux', arch: 'x64', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
115
+ },
116
+ {
117
+ platform: 'linux', arch: 'x64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
118
+ },
119
+ {
120
+ platform: 'linuxmusl', arch: 'arm64', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
121
+ },
122
+ {
123
+ platform: 'linuxmusl', arch: 'arm64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
124
+ },
125
+ {
126
+ platform: 'linuxmusl', arch: 'x64', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
127
+ },
128
+ {
129
+ platform: 'linuxmusl', arch: 'x64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
130
+ },
131
+ {
132
+ platform: 'win32', arch: 'arm64', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
133
+ },
134
+ {
135
+ platform: 'win32', arch: 'arm64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
136
+ },
137
+ {
138
+ platform: 'win32', arch: 'x64', runtime: 'mt', buildType: 'release', runtimeEnv: 'node',
139
+ },
140
+ {
141
+ platform: 'win32', arch: 'x64', runtime: 'mt', buildType: 'debug', runtimeEnv: 'node',
142
+ },
143
+ ];
144
+
145
+ // A native executable links the same archives as the Node-API addon of its platform and arch.
146
+ export const TARGETS = [
147
+ ...BASE_TARGETS,
148
+ ...BASE_TARGETS
149
+ .filter((target) => target.runtimeEnv === 'node' && target.platform !== 'wasm')
150
+ .map((target) => ({ ...target, runtimeEnv: 'native' })),
95
151
  ];
96
152
 
153
+ // Built only when named with -p: a plain `crossbind build` keeps producing what it did before
154
+ // native Node.js addons existed.
155
+ export const OPT_IN_PLATFORMS = ['darwin', 'linux', 'linuxmusl', 'win32'];
156
+
157
+ // Built on the host itself: Apple's linker and SDKs exist in no toolchain image.
158
+ export const HOST_BUILT_PLATFORMS = ['ios', 'darwin'];
159
+
160
+ // The runtime environment names the binary a build makes; android and ios have none, since
161
+ // React Native links them.
162
+ export const runtimeEnvsOf = (platform) => [
163
+ ...new Set(TARGETS.filter((target) => target.platform === platform && target.runtimeEnv).map((target) => target.runtimeEnv)),
164
+ ];
165
+
166
+ export const RUNTIME_ENVS = [...new Set(TARGETS.map((target) => target.runtimeEnv).filter(Boolean))];
167
+
168
+ // A build makes only the binaries it is asked for: -e first, then the config's target.runtimeEnv.
169
+ // Bundler plugins ask through getTargetParams instead.
170
+ export function selectRuntimeEnvs(cliRuntimeEnvs, configRuntimeEnv) {
171
+ if (cliRuntimeEnvs?.length) return cliRuntimeEnvs;
172
+ return configRuntimeEnv ? [configRuntimeEnv] : [];
173
+ }
174
+
97
175
  export function targetPathOf(target) {
98
176
  return `${target.platform}-${target.arch}-${target.runtime}-${target.buildType}`;
99
177
  }
100
178
 
179
+ // The release build of a target, whose prebuilts a debug build of it falls back to.
180
+ export const releaseTargetOf = (target) => ({ ...target, buildType: 'release', path: target.releasePath });
181
+
182
+ // Every platform/arch addon of one build type sits next to a single loader, which fills in
183
+ // {platform} and {arch} from the running process. The loader is CommonJS whatever the package
184
+ // "type" says, since it needs __dirname and process.dlopen.
185
+ export function nodeAddonNamesOf(target, projectName) {
186
+ const suffix = target.buildType === 'debug' ? '.debug' : '';
187
+ const addonPattern = `${projectName}.{platform}-{arch}${suffix}.node`;
188
+ return {
189
+ addonPattern,
190
+ addonName: addonPattern.replace('{platform}', target.platform).replace('{arch}', target.arch),
191
+ jsName: `${projectName}.native${suffix}.cjs`,
192
+ };
193
+ }
194
+
195
+ // One executable per platform, arch and build type, named like the addons it replaces.
196
+ export function nativeCommandNamesOf(target, projectName) {
197
+ const suffix = target.buildType === 'debug' ? '.debug' : '';
198
+ const extension = target.platform === 'win32' ? '.exe' : '';
199
+ return { commandName: `${projectName}.${target.platform}-${target.arch}${suffix}${extension}` };
200
+ }
201
+
101
202
  export function filterTargetSpecs(targetSpecs, target) {
102
203
  return targetSpecs?.filter(t => (
103
204
  (!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
+ }
@@ -1,5 +1,7 @@
1
1
  // Toolchain surface for platform:'wasi' (wasm32-wasip3, wasi-sdk >= 34; run under `wasmtime`).
2
2
 
3
+ import { isLocalRunner } from './selectRunner.js';
4
+
3
5
  export const WASI_TARGET_TRIPLE = 'wasm32-wasip3';
4
6
  // The sdk's clang still defaults to wasip1, so every compile/link carries --target explicitly.
5
7
  export const WASI_TARGET_FLAGS = [`--target=${WASI_TARGET_TRIPLE}`];
@@ -31,6 +33,7 @@ export const WASI_LINK_LIBS = [
31
33
  '-lunwind',
32
34
  '-lsetjmp',
33
35
  ...WASI_EMULATION_LIBS,
36
+ '-Wl,--wrap=lseek',
34
37
  ];
35
38
 
36
39
  export function wasiCFlags() {
@@ -41,7 +44,17 @@ export function wasiCxxFlags() {
41
44
  return [...WASI_TARGET_FLAGS, ...WASI_COMPILE_DEFINES, '-fwasm-exceptions', ...WASI_EH_CFLAGS];
42
45
  }
43
46
 
44
- // Env override first, then persistent ~/.crossbind.json; null when unset.
47
+ // The host sdk of a RUNNER=LOCAL build: env override first, then persistent ~/.crossbind.json; null when
48
+ // unset, and under the other runners, whose builds use the sdk in the image.
45
49
  export function resolveWasiSdkPath(system, env = process.env) {
50
+ if (!isLocalRunner(system, env)) return null;
46
51
  return env.CROSSBIND_WASI_SDK_PATH || system?.WASI_SDK_PATH || null;
47
52
  }
53
+
54
+ // Recipes read the wasi-sdk location from the environment (lazily, in getBuildParams), so it names
55
+ // the sdk the build uses: the host's under RUNNER=LOCAL, none (the image's) under the other runners.
56
+ export function exportWasiSdkPath(system, env = process.env) {
57
+ const sdk = resolveWasiSdkPath(system, env);
58
+ if (sdk) env.CROSSBIND_WASI_SDK_PATH = sdk;
59
+ else delete env.CROSSBIND_WASI_SDK_PATH;
60
+ }
@@ -0,0 +1,18 @@
1
+ // The windows image's llvm-mingw and its CMake toolchain files, one per target triple: port archives and
2
+ // Conan packages link into the same addon, so they move together.
3
+ const LLVM_MINGW = '/opt/llvm-mingw';
4
+ const WINDOWS_TOOLCHAIN = '/opt/crossbind/windows';
5
+ const WINDOWS_TRIPLES = { x64: 'x86_64-w64-mingw32', arm64: 'aarch64-w64-mingw32' };
6
+
7
+ export const windowsTriple = (target) => WINDOWS_TRIPLES[target.arch];
8
+ export const windowsToolchainFile = (target) => `${WINDOWS_TOOLCHAIN}/${windowsTriple(target)}.cmake`;
9
+ export const windowsTool = (target, name) => `${LLVM_MINGW}/bin/${windowsTriple(target)}-${name}`;
10
+
11
+ // The compilers, archive and resource tools of the target's triple, by the variables build systems read
12
+ // them from. The image's own .pc files describe its libraries, not the target's, so pkg-config reads none.
13
+ export const windowsBuildEnv = (target) => ({
14
+ ...Object.fromEntries(Object.entries({
15
+ CC: 'clang', CXX: 'clang++', AR: 'ar', RANLIB: 'ranlib', NM: 'nm', STRIP: 'strip', RC: 'windres', WINDRES: 'windres',
16
+ }).map(([variable, tool]) => [variable, windowsTool(target, tool)])),
17
+ PKG_CONFIG_LIBDIR: '',
18
+ });
package/vitest.config.js CHANGED
@@ -12,6 +12,17 @@ export default defineConfig({
12
12
  test: {
13
13
  include: ['test/**/*.test.js'],
14
14
  environment: 'node',
15
+ // A developer's own runner settings must not decide where the tested steps run, and neither must their
16
+ // crossbind login: no one signs in to a cloud at this address.
17
+ env: {
18
+ CROSSBIND_RUNNER: '',
19
+ CROSSBIND_REMOTE_URL: '',
20
+ CROSSBIND_REMOTE_URL_WEB: '',
21
+ CROSSBIND_REMOTE_URL_ANDROID: '',
22
+ CROSSBIND_REMOTE_URL_LINUX: '',
23
+ CROSSBIND_REMOTE_URL_WINDOWS: '',
24
+ CROSSBIND_CLOUD_URL: 'http://127.0.0.1:9',
25
+ },
15
26
  coverage: {
16
27
  provider: 'v8',
17
28
  reporter: ['text', 'html'],
@@ -1,23 +0,0 @@
1
- // SWIG's -embind backend emits functions and constructors but no member variables, so
2
- // public fields never reach JS from the generated bridge alone. This post-pass injects
3
- // embind .property lines for the value-semantics fields parseCppSurface captured - the
4
- // same post-processing precedent as bridgeAsyncGuard.
5
-
6
- export function buildFieldPropertyLines(cls) {
7
- return (cls.fields ?? []).map((f) => `.property("${f.name}", &${cls.name}::${f.name})`);
8
- }
9
-
10
- export function injectFieldBindings(bridgeContent, model) {
11
- let out = bridgeContent;
12
- for (const cls of model.classes ?? []) {
13
- const lines = buildFieldPropertyLines(cls)
14
- .filter((line) => !out.includes(line));
15
- if (!lines.length) continue;
16
- // Lazy template match still lands on the right opener: the first `>(\s*"Name")`
17
- // closes the template list even when it contains nested arguments like base<T>.
18
- const opener = new RegExp(`(emscripten::class_<[\\s\\S]*?>\\s*\\(\\s*"${cls.name}"\\s*\\))`);
19
- if (!opener.test(out)) continue;
20
- out = out.replace(opener, `$1\n ${lines.join('\n ')}`);
21
- }
22
- return out;
23
- }