crossbind 2.0.0-beta.58 → 2.0.0-beta.62

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 (43) hide show
  1. package/README.md +7 -5
  2. package/package.json +4 -4
  3. package/src/actions/buildDependencies.js +22 -7
  4. package/src/actions/buildJs.js +1 -1
  5. package/src/actions/buildLib.js +2 -0
  6. package/src/actions/buildNode.js +149 -0
  7. package/src/actions/buildWasm.js +43 -66
  8. package/src/actions/createInterface.js +101 -27
  9. package/src/actions/createLib.js +3 -2
  10. package/src/actions/getAllBridges.js +1 -1
  11. package/src/actions/getLinkInputs.js +63 -0
  12. package/src/actions/licenses.js +34 -0
  13. package/src/actions/run.js +98 -3
  14. package/src/assets/cmake/CMakeLists.txt +2 -2
  15. package/src/assets/cmake/dist.cmake +1 -1
  16. package/src/assets/js-runtime/adapters/worker-comlink.js +6 -2
  17. package/src/assets/toolchain-digests.json +26 -12
  18. package/src/bin.js +59 -13
  19. package/src/index.js +3 -1
  20. package/src/integration/getCrossbindScript.js +10 -12
  21. package/src/integration/getDependFilePath.js +9 -12
  22. package/src/state/index.js +16 -13
  23. package/src/state/loadConfig.js +3 -0
  24. package/src/utils/appRustCrates.js +2 -0
  25. package/src/utils/appSources.js +21 -0
  26. package/src/utils/cargoImport.js +44 -0
  27. package/src/utils/cargoTarget.js +2 -0
  28. package/src/utils/cppDts.js +77 -41
  29. package/src/utils/embindArgumentGuards.js +45 -0
  30. package/src/utils/embindRsFingerprint.js +26 -0
  31. package/src/utils/headerImports.js +45 -0
  32. package/src/utils/inputStamp.js +23 -0
  33. package/src/utils/linkLayout.js +8 -1
  34. package/src/utils/pullDockerImage.js +8 -5
  35. package/src/utils/resolveCompanionPackage.js +51 -0
  36. package/src/utils/resolveEmbindNapi.js +22 -0
  37. package/src/utils/resolveEmbindRust.js +17 -52
  38. package/src/utils/runCargo.js +4 -3
  39. package/src/utils/rustBridgeGen.js +895 -250
  40. package/src/utils/swigInterface.js +19 -4
  41. package/src/utils/systemKeys.js +8 -0
  42. package/src/utils/targets.js +56 -0
  43. package/src/utils/cppFieldBindings.js +0 -23
package/README.md CHANGED
@@ -68,8 +68,9 @@ target-specific artifacts, transitive dependencies, build recipes, and runtime
68
68
  adapters.
69
69
 
70
70
  - **Target-aware distribution.** A package family can carry WebAssembly, iOS,
71
- Android, and WASI variants; each build consumes only the artifact valid for
72
- its platform, architecture, runtime, environment, and build type.
71
+ Android, macOS, Linux, Windows, and WASI variants; each build consumes only the
72
+ artifact valid for its platform, architecture, runtime, environment, and build
73
+ type.
73
74
  - **Native dependency resolution.** Package manifests and crossbind configs
74
75
  carry the transitive native graph, so prerequisites are built and linked in
75
76
  the correct order.
@@ -253,9 +254,10 @@ The repository contains 16 port families:
253
254
  OpenSSL · PROJ · SpatiaLite · SQLite · libTIFF · WebP · zlib · Zstandard**
254
255
 
255
256
  Each family has target-specific packages. For example, a browser or Node.js
256
- project uses `@crossbind/port-gdal-wasm`, while mobile and WASI builds use the
257
- matching `-android`, `-ios`, or `-wasi` variant. Import the variant's
258
- `crossbind.config.js` into the application's dependency list:
257
+ project uses `@crossbind/port-gdal-wasm`, while mobile, native Node.js addon and
258
+ WASI builds use the matching `-android`, `-ios`, `-darwin`, `-linux`, `-win32`,
259
+ or `-wasi` variant. Import the variant's `crossbind.config.js` into the
260
+ application's dependency list:
259
261
 
260
262
  ```bash
261
263
  npm install @crossbind/port-gdal-wasm@beta
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crossbind",
3
- "version": "2.0.0-beta.58",
3
+ "version": "2.0.0-beta.62",
4
4
  "license": "MIT",
5
5
  "homepage": "https://crossbind.dev",
6
6
  "repository": "https://github.com/crossbind/crossbind.git",
@@ -19,8 +19,8 @@
19
19
  "test:coverage": "vitest run --coverage"
20
20
  },
21
21
  "devDependencies": {
22
- "@vitest/coverage-v8": "^4.1.10",
23
- "vitest": "^4.1.10"
22
+ "@vitest/coverage-v8": "^5.0.1",
23
+ "vitest": "^5.0.1"
24
24
  },
25
25
  "dependencies": {
26
26
  "@rollup/plugin-commonjs": "^29.0.3",
@@ -33,7 +33,7 @@
33
33
  "log-update": "^8.0.0",
34
34
  "picocolors": "^1.1.1",
35
35
  "replace": "^1.2.2",
36
- "rollup": "^4.62.4",
36
+ "rollup": "^4.63.4",
37
37
  "spdx-expression-parse": "^5.0.0",
38
38
  "upath": "^3.0.8"
39
39
  },
@@ -14,6 +14,11 @@ import {
14
14
  } from '../utils/dependencyRebuild.js';
15
15
  import withDirLock from '../utils/dirLock.js';
16
16
  import { prepareRustSysroot } from '../utils/rustSysroot.js';
17
+ import getAbsolutePath from '../utils/getAbsolutePath.js';
18
+ import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
19
+ import getParentPath from '../utils/getParentPath.js';
20
+ import calculateDependencyParameters from '../state/calculateDependencyParameters.js';
21
+ import { getEmbindRsFingerprint, isEmbindRsFingerprintStale } from '../utils/embindRsFingerprint.js';
17
22
 
18
23
  export default async function buildDependencies({ targetParams, rebuildOption }) {
19
24
  // Before the early returns below: this is the one async step every build path awaits, and the
@@ -64,17 +69,23 @@ export default async function buildDependencies({ targetParams, rebuildOption })
64
69
  async function buildMissingCargoDependencies(targets, targetParams) {
65
70
  if (targets.length === 0) return;
66
71
  const appConfig = state.config;
67
- const missing = (appConfig.allDependencies ?? []).filter((dep) => dep !== appConfig
68
- && dep.export?.type === 'cargo'
69
- && targets.some((target) => !dep.functions.isEnabled(target)));
72
+ const cargoDependencies = (appConfig.allDependencies ?? []).filter((dep) => dep !== appConfig && dep.export?.type === 'cargo');
73
+ if (cargoDependencies.length === 0) return;
74
+ // A prebuilt from another embind-rs counts as missing: linked, it carries stale glue or clashes with the others.
75
+ const fingerprint = getEmbindRsFingerprint();
76
+ const prebuiltOf = (dep, target) => [target.path, target.releasePath]
77
+ .map((targetPath) => `${dep.paths.output}/prebuilt/${targetPath}`)
78
+ .find((dir) => fs.existsSync(dir)) ?? `${dep.paths.output}/prebuilt/${target.path}`;
79
+ const isBuilt = (dep, target) => dep.functions.isEnabled(target) && !isEmbindRsFingerprintStale(prebuiltOf(dep, target), fingerprint);
80
+ const missing = cargoDependencies.filter((dep) => targets.some((target) => !isBuilt(dep, target)));
70
81
 
71
82
  for (const dep of missing) {
72
83
  const name = dep.general.name;
73
84
  // isEnabled reads the filesystem on every call, so a build that lands while this one waits
74
85
  // for the lock is picked up without any state to refresh.
75
86
  await withDirLock(`${dep.paths.output}.autobuild.lock`, async () => {
76
- if (targets.every((target) => dep.functions.isEnabled(target))) return;
77
- logger.info(`crossbind: building cargo dependency "${name}" - it has no prebuilt for this target yet…`);
87
+ if (targets.every((target) => isBuilt(dep, target))) return;
88
+ logger.info(`crossbind: building cargo dependency "${name}" - it has no prebuilt from this embind-rs for this target yet…`);
78
89
  const scoped = await loadConfig(dep.paths.project);
79
90
  const prev = state.config;
80
91
  state.config = scoped;
@@ -84,17 +95,21 @@ async function buildMissingCargoDependencies(targets, targetParams) {
84
95
  state.config = prev;
85
96
  }
86
97
  });
98
+ // loadConfig resolved a never-built package's CMakeLists to the CLI's own, and isEnabled and the link read it.
99
+ dep.paths.cmake = getAbsolutePath(dep.paths.project, getCMakeListsFilePath(dep.paths.output));
100
+ dep.paths.cmakeDir = getParentPath(dep.paths.cmake);
87
101
  }
102
+ if (missing.length > 0) appConfig.dependencyParameters = calculateDependencyParameters(appConfig);
88
103
 
89
104
  // The build above is the fix; this is the guard behind it. assertDependsBuilt only runs while
90
105
  // the cmake link is being configured, so a cached native build skips the check and a missing
91
106
  // cargo dependency reaches the app anyway - measured on wasm as a clean BUILD=0 whose module
92
107
  // then failed 22 of 43 conformance features at init. Here the answer is known either way.
93
- const unresolved = missing.filter((dep) => targets.some((target) => !dep.functions.isEnabled(target)));
108
+ const unresolved = missing.filter((dep) => targets.some((target) => !isBuilt(dep, target)));
94
109
  if (unresolved.length > 0) {
95
110
  const names = unresolved.map((dep) => `"${dep.general.name}"`).join(', ');
96
111
  throw new Error(`crossbind: cargo ${unresolved.length > 1 ? 'dependencies' : 'dependency'} ${names} still `
97
- + `${unresolved.length > 1 ? 'have' : 'has'} no prebuilt for ${targets.map((t) => t.path).join(', ')} `
112
+ + `${unresolved.length > 1 ? 'have' : 'has'} no prebuilt built from the current embind-rs for ${targets.map((t) => t.path).join(', ')} `
98
113
  + 'after building. Linking without it produces a module that builds clean and dies at init.');
99
114
  }
100
115
  }
@@ -13,7 +13,7 @@ const SUPPRESSED_WARN_CODES = new Set([
13
13
  'MIXED_EXPORTS',
14
14
  ]);
15
15
 
16
- function suppressNodeBuiltinWarnings(warning, defaultHandler) {
16
+ export function suppressNodeBuiltinWarnings(warning, defaultHandler) {
17
17
  if (SUPPRESSED_WARN_CODES.has(warning.code)) return;
18
18
  if (warning.code === 'UNRESOLVED_IMPORT' && /^node:/.test(warning.exporter || warning.source || warning.id || '')) return;
19
19
  defaultHandler(warning);
@@ -7,6 +7,7 @@ import state from '../state/index.js';
7
7
  import logger from '../utils/logger.js';
8
8
  import findFiles from '../utils/findFiles.js';
9
9
  import { getSourceFingerprint, isSourceFingerprintStale, staleTargetDirectories, writeSourceFingerprint } from '../utils/sourceFingerprint.js';
10
+ import { getEmbindRsFingerprint, writeEmbindRsFingerprint } from '../utils/embindRsFingerprint.js';
10
11
 
11
12
  export default function buildLib(targetParams, options = {}) {
12
13
  let isChanged = false;
@@ -48,6 +49,7 @@ export default function buildLib(targetParams, options = {}) {
48
49
  fs.copyFileSync(modulePath, `${state.config.paths.output}/prebuilt/${target.path}/swig/${fileName}`);
49
50
  });
50
51
  writeSourceFingerprint(libdir, sourceFingerprint);
52
+ if (isCargo) writeEmbindRsFingerprint(libdir, getEmbindRsFingerprint());
51
53
  isChanged = true;
52
54
  } else {
53
55
  // The skip is existence-only; without this warning a source edit is served stale silently.
@@ -0,0 +1,149 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { rollup } from 'rollup';
5
+ import { nodeResolve } from '@rollup/plugin-node-resolve';
6
+ import commonjs from '@rollup/plugin-commonjs';
7
+ import virtual from '@rollup/plugin-virtual';
8
+ import run from './run.js';
9
+ import getLinkInputs from './getLinkInputs.js';
10
+ import getData from './getData.js';
11
+ import { suppressNodeBuiltinWarnings } from './buildJs.js';
12
+ import state from '../state/index.js';
13
+ import logger from '../utils/logger.js';
14
+ import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
15
+ import { buildLinkLibArgs } from '../utils/linkLayout.js';
16
+ import resolveEmbindNapiRoot, { resolveEmbindJsiRoot } from '../utils/resolveEmbindNapi.js';
17
+ import resolveEmbindRustRoot from '../utils/resolveEmbindRust.js';
18
+
19
+ const cpuCount = Math.max(1, os.cpus().length - 1);
20
+
21
+ function filesUnder(dirs) {
22
+ return dirs.flatMap((dir) => (fs.existsSync(dir)
23
+ ? fs.readdirSync(dir, { recursive: true })
24
+ .map((file) => `${dir}/${file}`)
25
+ .filter((file) => fs.statSync(file).isFile())
26
+ : []));
27
+ }
28
+
29
+ function linkInputs(target) {
30
+ const {
31
+ libs, wholeArchiveAll, wholeArchiveNames, rustKeepFlags, hasRust,
32
+ } = getLinkInputs(target, {
33
+ // The linker loads the archive member that defines a -u symbol; Mach-O prefixes C names with _.
34
+ keepFlag: (name) => `-Wl,-u,${target.platform === 'darwin' ? '_' : ''}crossbind_keep_${name}`,
35
+ });
36
+ const linkArgs = [
37
+ ...buildLinkLibArgs(libs, { wholeArchiveAll, wholeArchiveNames, forceLoad: target.platform === 'darwin' }),
38
+ ...rustKeepFlags,
39
+ // The system libraries the archives need, declared by the packages that bring them in.
40
+ ...(getData('binary', target)?.addonFlags ?? []),
41
+ ];
42
+ // The addon runs embind-jsi, so Rust packages bind through the adapter React Native links.
43
+ const extraSources = hasRust ? [`${resolveEmbindRustRoot()}/adapters/jsi.cpp`] : [];
44
+ return { libs, linkArgs, extraSources };
45
+ }
46
+
47
+ function envOf(target) {
48
+ return Object.fromEntries(Object.entries(getData('env', target))
49
+ .map(([key, value]) => [key, typeof value === 'function' ? value(state, target) : value]));
50
+ }
51
+
52
+ // One loader serves every addon of a build type, so each addon's env is keyed the way the loader
53
+ // looks it up at runtime rather than taken from whichever arch happened to build last.
54
+ export function nodeLoaderConfig(target) {
55
+ const served = state.targets.filter((t) => t.addonPattern && t.jsName === target.jsName);
56
+ return {
57
+ env: Object.fromEntries(served.map((t) => [`${t.platform}-${t.arch}`, envOf(t)])),
58
+ general: { name: state.config.general.name },
59
+ paths: { addon: target.addonPattern },
60
+ };
61
+ }
62
+
63
+ // The loader hands the addon dist/data by default, as the wasm build does. A relinked addon may come
64
+ // from updated dependencies, so its data is copied again rather than kept.
65
+ export function publishNodeData(target, { refresh }) {
66
+ Object.entries(getData('data', target)).forEach(([source, name]) => {
67
+ const published = `${state.config.paths.output}/data/${name}`;
68
+ if (!fs.existsSync(source) || (!refresh && fs.existsSync(published))) {
69
+ return;
70
+ }
71
+ fs.rmSync(published, { recursive: true, force: true });
72
+ fs.cpSync(source, published, { recursive: true });
73
+ });
74
+ }
75
+
76
+ async function bundleLoader(target, napiRoot, loaderConfig) {
77
+ const systemConfig = `export default ${JSON.stringify(loaderConfig)};`;
78
+ const bundle = await rollup({
79
+ input: `${napiRoot}/js/loader.js`,
80
+ plugins: [virtual({ 'crossbind/systemConfig': systemConfig }), nodeResolve(), commonjs()],
81
+ onwarn: suppressNodeBuiltinWarnings,
82
+ });
83
+ await bundle.write({ file: `${state.config.paths.build}/${target.jsName}`, format: 'cjs', exports: 'default' });
84
+ await bundle.close();
85
+ }
86
+
87
+ // Links one Node-API addon from the project's archives and bundles the loader that picks the
88
+ // addon matching the running process.
89
+ export default async function buildNode(target, options = {}) {
90
+ if (target.platform === 'darwin' && process.platform !== 'darwin') {
91
+ logger.info(`[${target.path}] addon skipped (macOS addons build on a macOS host)`);
92
+ return false;
93
+ }
94
+ if (state.config.export.type === 'cargo') {
95
+ logger.info(`[${target.path}] addon skipped (cargo package - staticlib only)`);
96
+ return false;
97
+ }
98
+
99
+ const buildType = target.buildType === 'release' ? 'Release' : 'Debug';
100
+ const { build } = state.config.paths;
101
+ const napiRoot = resolveEmbindNapiRoot();
102
+ const jsiRoot = resolveEmbindJsiRoot();
103
+ const { libs, linkArgs, extraSources } = linkInputs(target);
104
+ const loaderConfig = nodeLoaderConfig(target);
105
+
106
+ const fingerprintFile = `${build}/${target.addonName}.fingerprint`;
107
+ const fingerprint = getContentHash(JSON.stringify({
108
+ builder: getFilesFingerprint([fileURLToPath(import.meta.url)]),
109
+ linkArgs,
110
+ libs: libs.map((lib) => {
111
+ const stat = fs.existsSync(lib) ? fs.statSync(lib) : null;
112
+ return { lib, size: stat ? stat.size : null, mtimeMs: stat ? stat.mtimeMs : null };
113
+ }),
114
+ loaderConfig,
115
+ runtime: getFilesFingerprint(filesUnder([
116
+ `${napiRoot}/cpp`, `${napiRoot}/js`, `${napiRoot}/third_party`, `${jsiRoot}/cpp/src`, `${jsiRoot}/js`,
117
+ ])),
118
+ extraSources: getFilesFingerprint(extraSources),
119
+ }));
120
+ const isLinkChanged = !fs.existsSync(fingerprintFile)
121
+ || fs.readFileSync(fingerprintFile, { encoding: 'utf8' }) !== fingerprint;
122
+ if (!options.force && !isLinkChanged
123
+ && fs.existsSync(`${build}/${target.addonName}`) && fs.existsSync(`${build}/${target.jsName}`)) {
124
+ logger.cachedStep(target, 'addon');
125
+ return false;
126
+ }
127
+
128
+ const platformPrefix = `Node-${buildType}`;
129
+ logger.startStep(target, 'addon');
130
+ run(null, [
131
+ 'cmake', `${napiRoot}/cpp`,
132
+ `-DCMAKE_BUILD_TYPE=${buildType}`,
133
+ `-DCROSSBIND_ADDON_FILE=${target.addonName}`,
134
+ `-DCROSSBIND_JSI_ROOT=${jsiRoot}`,
135
+ `-DCROSSBIND_EXTRA_SOURCES=${extraSources.join(';')}`,
136
+ `-DCROSSBIND_LINK_ARGS=${linkArgs.join(';')}`,
137
+ `-DCROSSBIND_LINK_DEPENDS=${libs.join(';')}`,
138
+ ], platformPrefix, target);
139
+ run(null, ['cmake', '--build', '.', '-j', String(cpuCount)], platformPrefix, target);
140
+ fs.copyFileSync(`${build}/${platformPrefix}/${target.path}/${target.addonName}`, `${build}/${target.addonName}`);
141
+ logger.doneStep(target, 'addon');
142
+
143
+ logger.startStep(target, 'js');
144
+ await bundleLoader(target, napiRoot, loaderConfig);
145
+ logger.doneStep(target, 'js');
146
+
147
+ fs.writeFileSync(fingerprintFile, fingerprint);
148
+ return true;
149
+ }
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import replace from 'replace';
4
4
  import run from './run.js';
5
- import getDependLibs from './getDependLibs.js';
5
+ import getLinkInputs from './getLinkInputs.js';
6
6
  import getData from './getData.js';
7
7
  import buildJs from './buildJs.js';
8
8
  import triggerExtensions from './extensions.js';
@@ -11,29 +11,33 @@ import resolveEmbindRustRoot from '../utils/resolveEmbindRust.js';
11
11
  import logger from '../utils/logger.js';
12
12
  import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
13
13
  import { buildLinkLibArgs } from '../utils/linkLayout.js';
14
- import buildAppRustCrates from '../utils/appRustCrates.js';
14
+ import { guardBigIntArguments, guardEmbindArguments } from '../utils/embindArgumentGuards.js';
15
15
 
16
16
  // embind's bigint converter turns any Number into a BigInt, so 2^53+1 silently becomes 2^53.
17
17
  // A 64-bit parameter takes a BigInt or a Number that is a safe integer instead; the jsi adapter
18
18
  // enforces the same rule natively. Each runtimeEnv links its own glue, so each one calls this.
19
- function guardBigIntArguments(target) {
19
+ function guardBigIntConversions(target) {
20
20
  const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
21
- replace({
22
- regex: 'if\\s*\\(\\s*typeof value\\s*==\\s*"number"\\s*\\)\\s*\\{\\s*value\\s*=\\s*BigInt\\(value\\)\\s*;?\\s*\\}',
23
- replacement: 'if(typeof value=="number"){if(!Number.isSafeInteger(value))throw new TypeError("a 64-bit integer parameter takes a BigInt or a safe integer Number, got "+value);value=BigInt(value)}',
24
- paths: [gluePath],
25
- recursive: false,
26
- silent: true,
27
- });
28
- const glue = fs.readFileSync(gluePath, 'utf8');
29
- if (glue.includes('__embind_register_bigint') && !glue.includes('Number.isSafeInteger(value)')) {
21
+ const { text, missed } = guardBigIntArguments(fs.readFileSync(gluePath, 'utf8'));
22
+ fs.writeFileSync(gluePath, text);
23
+ if (missed) {
30
24
  logger.error('bigint safe-integer rewrite missed (emscripten glue format changed?): 64-bit Number arguments round silently');
31
25
  }
32
26
  }
33
27
 
28
+ // Wrong-typed integer and enum arguments cross as 0 in a release build (utils/embindArgumentGuards.js);
29
+ // each runtimeEnv links its own glue, so each one calls this after its link.
30
+ function guardArgumentConversions(target) {
31
+ const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
32
+ const { text, missed } = guardEmbindArguments(fs.readFileSync(gluePath, 'utf8'));
33
+ fs.writeFileSync(gluePath, text);
34
+ if (missed.length) {
35
+ logger.error(`embind argument rewrite missed ${missed.join(', ')} (emscripten glue format changed?): wrong-typed arguments cross as 0`);
36
+ }
37
+ }
38
+
34
39
  export default async function buildWasm(target, options = {}) {
35
40
  const isProd = target.buildType === 'release';
36
- const buildType = isProd ? 'Release' : 'Debug';
37
41
 
38
42
  // Caller can opt out of the final emcc link entirely (e.g. when the
39
43
  // package is consumed only as a static library by downstream builds).
@@ -47,51 +51,12 @@ export default async function buildWasm(target, options = {}) {
47
51
  return false;
48
52
  }
49
53
 
50
- // buildLib's cache is keyed on paths.output/prebuilt; after a cache clean the
51
- // build-dir copy can be gone while the output artifact is still valid — link it then.
52
- const sourceLibCandidates = [
53
- `${state.config.paths.build}/Source-${buildType}/${target.path}/lib${state.config.general.name}.a`,
54
- `${state.config.paths.output}/prebuilt/${target.path}/lib/lib${state.config.general.name}.a`,
55
- ];
56
- // App-local Rust surfaces (imported .rs files) arrive as ONE super staticlib; it is the
57
- // single fully-loaded Rust archive of the link (see the libstd rule below).
58
- const appRustLibs = buildAppRustCrates(target, state.config.paths.cache, state.config.cargoDependencies ?? {});
59
- const libs = [
60
- ...getDependLibs(target),
61
- ...appRustLibs,
62
- sourceLibCandidates.find((lib) => fs.existsSync(lib)) ?? sourceLibCandidates[0],
63
- `${state.config.paths.build}/Bridge-${buildType}/${target.path}/lib${state.config.general.name}.a`,
64
- ];
65
-
66
- // By default only the Bridge archive is --whole-archive'd (see
67
- // linkLayout.js) so wasm-ld dead-code-eliminates everything unreferenced.
68
- // Two escape hatches, both `export.wholeArchive: true`: on the APP it
69
- // restores the legacy layout (every archive wholesale); on a LIBRARY's
70
- // own config it keeps that library's archives whole in every consumer
71
- // link (for members that self-register from static initializers).
72
- const wholeArchiveAll = state.config.export.wholeArchive === true;
73
- const wholeArchiveNames = new Set();
74
- // Rust libstd rule: every Rust staticlib bundles its own libstd, so at most ONE Rust archive
75
- // may be fully loaded (the app super staticlib). Generated cargo bridges are linked lazily
76
- // with their keep symbol pinned (-u pulls just the registration object); manual-bindings
77
- // crates have no keep symbol and fall back to whole-archive - safe only while they are the
78
- // single loaded Rust archive.
79
- const rustKeepFlags = [];
80
- if (appRustLibs.length > 0) wholeArchiveNames.add('crossbind_app_super');
81
- state.config.dependencyParameters.getCmakeDepends(target).forEach((dep) => {
82
- if (dep.export.wholeArchive === true) {
83
- (dep.export.libName || []).forEach((name) => wholeArchiveNames.add(name));
84
- }
85
- if (dep.export.type === 'cargo') {
86
- const crateLibRs = `${dep.paths.project}/${dep.export.crate ?? 'crate'}/src/lib.rs`.replace('/./', '/');
87
- const isManual = fs.existsSync(crateLibRs) && fs.readFileSync(crateLibRs, 'utf8').includes('bindings!');
88
- (dep.export.libName || []).forEach((name) => {
89
- // wasm-ld's -u does NOT pull archive members (it just leaves an import);
90
- // --export forces the symbol to resolve, dragging the registration object in.
91
- if (isManual) wholeArchiveNames.add(name);
92
- else rustKeepFlags.push(`-Wl,--export=crossbind_keep_${name}`);
93
- });
94
- }
54
+ const {
55
+ libs, appRustLibs, wholeArchiveAll, wholeArchiveNames, rustKeepFlags, hasRust,
56
+ } = getLinkInputs(target, {
57
+ // wasm-ld's -u does NOT pull archive members (it just leaves an import);
58
+ // --export forces the symbol to resolve, dragging the registration object in.
59
+ keepFlag: (name) => `-Wl,--export=crossbind_keep_${name}`,
95
60
  });
96
61
  // The whole-archived app super-staticlib and a lazily-pulled package bridge object each
97
62
  // carry rustc's allocator/panic shims (codegen-units=1 places them in the same object as
@@ -104,13 +69,19 @@ export default async function buildWasm(target, options = {}) {
104
69
 
105
70
  // Any Rust archive in the link needs the web adapter TU: it provides the flat
106
71
  // crossbind_embind_* C-ABI (typeid getters + passthroughs to emscripten's embind).
107
- const hasRust = rustKeepFlags.length > 0 || appRustLibs.length > 0
108
- || state.config.dependencyParameters.getCmakeDepends(target).some((dep) => dep.export.type === 'cargo');
109
72
  // The adapter ships in @crossbind/core-embind-rust (declared by the consumer, resolved here).
110
73
  const rustSources = hasRust ? [`${resolveEmbindRustRoot()}/adapters/web.cpp`] : [];
111
74
 
112
75
  const binary = getData('binary', target);
113
76
  const emccFlags = [...(binary?.emccFlags || []), ...rustKeepFlags];
77
+ // Rust code expects a native-sized stack (rustc's own wasm targets reserve 1 MiB) where
78
+ // emscripten reserves 64 KiB, and a wasm stack has no guard page: an overflow overwrites static
79
+ // data without a trap. A size set in the config stays; a debug build also checks for overflow.
80
+ if (hasRust) {
81
+ const set = emccFlags.join(' ');
82
+ if (!/-s\s*(?:STACK_SIZE|TOTAL_STACK)=/.test(set)) emccFlags.push('-sSTACK_SIZE=1MB');
83
+ if (!isProd && !/-s\s*STACK_OVERFLOW_CHECK=/.test(set)) emccFlags.push('-sSTACK_OVERFLOW_CHECK=1');
84
+ }
114
85
 
115
86
  triggerExtensions('buildWasm', 'beforeBuild', [emccFlags]);
116
87
 
@@ -189,12 +160,13 @@ export default async function buildWasm(target, options = {}) {
189
160
  const data = Object.entries(getData('data', target)).map(([key, value]) => ['--preload-file', `${key.replaceAll('@', '@@')}@/crossbind/${value}`]).flat();
190
161
  run('em++', [
191
162
  '-lembind',
163
+ // The config's emccFlags follow the default level, so its -Oz or -Os wins (the last -O does).
164
+ ...(isProd ? ['-O3'] : []),
192
165
  ...emccFlags,
193
166
  // '-lwebsocket.js', '-sPROXY_POSIX_SOCKETS', '-sWEBSOCKET_DEBUG=1', '-sJSPI', '-g', '-sWASMFS',
194
167
  '-s', 'FORCE_FILESYSTEM=1',
195
168
  '-sEXPORT_NAME=Module2', // '-pthread', '-sPTHREAD_POOL_SIZE=5',
196
169
  ...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/browser.cpp`,
197
- ...(isProd ? ['-O3'] : []),
198
170
  '-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
199
171
  '-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'FORCE_FILESYSTEM=1',
200
172
  '-s', 'ALLOW_MEMORY_GROWTH=1',
@@ -244,7 +216,8 @@ export default async function buildWasm(target, options = {}) {
244
216
  recursive: false,
245
217
  silent: true,
246
218
  }); */
247
- guardBigIntArguments(target);
219
+ guardBigIntConversions(target);
220
+ guardArgumentConversions(target);
248
221
  await buildJs(target);
249
222
  // fs.rmSync(`${state.config.paths.build}/${state.config.general.name}.js`);
250
223
  // fs.copyFileSync(`${state.config.paths.build}/${state.config.general.name}.browser.js`, `${state.config.paths.build}/${state.config.general.name}.js`);
@@ -261,12 +234,13 @@ export default async function buildWasm(target, options = {}) {
261
234
  const data = Object.entries(getData('data', target)).map(([key, value]) => ['--preload-file', `${key.replaceAll('@', '@@')}@/crossbind/${value}`]).flat();
262
235
  run('em++', [
263
236
  '-lembind',
237
+ // The config's emccFlags follow the default level, so its -Oz or -Os wins (the last -O does).
238
+ ...(isProd ? ['-O3'] : []),
264
239
  ...emccFlags,
265
240
  '-sEXPORT_NAME=Module2',
266
241
  // rustSources carries the embind-rust adapter TU (emval/json/tid hooks); without
267
242
  // it any linked Rust package archive fails with undefined crossbind_* symbols.
268
243
  ...linkLibs, ...rustSources,
269
- ...(isProd ? ['-O3'] : []),
270
244
  '-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
271
245
  '-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'FORCE_FILESYSTEM=1',
272
246
  '-s', 'ALLOW_MEMORY_GROWTH=1',
@@ -282,7 +256,8 @@ export default async function buildWasm(target, options = {}) {
282
256
  const t1 = performance.now();
283
257
  logger.doneStep(target, 'wasm');
284
258
  logger.startStep(target, 'js');
285
- guardBigIntArguments(target);
259
+ guardBigIntConversions(target);
260
+ guardArgumentConversions(target);
286
261
  await buildJs(target);
287
262
  logger.doneStep(target, 'js');
288
263
  }
@@ -294,11 +269,12 @@ export default async function buildWasm(target, options = {}) {
294
269
 
295
270
  run('em++', [
296
271
  '-lembind',
272
+ // The config's emccFlags follow the default level, so its -Oz or -Os wins (the last -O does).
273
+ ...(isProd ? ['-O3'] : []),
297
274
  ...emccFlags,
298
275
  // '-s', 'FETCH', '-sJSPI', '-pthread', '-sPTHREAD_POOL_SIZE=5',
299
276
  '-s', 'FORCE_FILESYSTEM=1',
300
277
  ...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/node.cpp`,
301
- ...(isProd ? ['-O3'] : []),
302
278
  '-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
303
279
  '-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'DISABLE_EXCEPTION_CATCHING=0', '-s', 'FORCE_FILESYSTEM=1',
304
280
  '-s', 'ALLOW_MEMORY_GROWTH=1',
@@ -314,7 +290,8 @@ export default async function buildWasm(target, options = {}) {
314
290
  ], null, target);
315
291
  logger.doneStep(target, 'wasm');
316
292
  logger.startStep(target, 'js');
317
- guardBigIntArguments(target);
293
+ guardBigIntConversions(target);
294
+ guardArgumentConversions(target);
318
295
  await buildJs(target);
319
296
  if (emccFlags.includes('FETCH')) {
320
297
  fs.appendFileSync(`${state.config.paths.build}/${target.jsName}`, 'var XMLHttpRequest = require(\'xhr2\');\n');