crossbind 2.0.0-beta.60 → 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.
- package/README.md +7 -5
- package/package.json +1 -1
- package/src/actions/buildDependencies.js +22 -7
- package/src/actions/buildJs.js +1 -1
- package/src/actions/buildLib.js +2 -0
- package/src/actions/buildNode.js +149 -0
- package/src/actions/buildWasm.js +43 -66
- package/src/actions/createInterface.js +101 -27
- package/src/actions/createLib.js +3 -2
- package/src/actions/getAllBridges.js +1 -1
- package/src/actions/getLinkInputs.js +63 -0
- package/src/actions/licenses.js +34 -0
- package/src/actions/run.js +97 -2
- package/src/assets/cmake/CMakeLists.txt +1 -1
- package/src/assets/cmake/dist.cmake +1 -1
- package/src/assets/js-runtime/adapters/worker-comlink.js +6 -2
- package/src/assets/toolchain-digests.json +26 -12
- package/src/bin.js +59 -13
- package/src/index.js +3 -1
- package/src/integration/getCrossbindScript.js +8 -10
- package/src/integration/getDependFilePath.js +9 -12
- package/src/state/index.js +16 -13
- package/src/state/loadConfig.js +3 -0
- package/src/utils/appRustCrates.js +2 -0
- package/src/utils/appSources.js +21 -0
- package/src/utils/cargoImport.js +44 -0
- package/src/utils/cargoTarget.js +2 -0
- package/src/utils/cppDts.js +77 -41
- package/src/utils/embindArgumentGuards.js +45 -0
- package/src/utils/embindRsFingerprint.js +26 -0
- package/src/utils/headerImports.js +45 -0
- package/src/utils/inputStamp.js +23 -0
- package/src/utils/linkLayout.js +8 -1
- package/src/utils/pullDockerImage.js +8 -5
- package/src/utils/resolveCompanionPackage.js +51 -0
- package/src/utils/resolveEmbindNapi.js +22 -0
- package/src/utils/resolveEmbindRust.js +17 -52
- package/src/utils/runCargo.js +4 -3
- package/src/utils/rustBridgeGen.js +895 -250
- package/src/utils/swigInterface.js +19 -4
- package/src/utils/systemKeys.js +8 -0
- package/src/utils/targets.js +56 -0
- 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
|
|
72
|
-
its platform, architecture, runtime, environment, and build
|
|
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
|
|
257
|
-
matching `-android`, `-ios`,
|
|
258
|
-
`crossbind.config.js` into the
|
|
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
|
@@ -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
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
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
|
|
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
|
}
|
package/src/actions/buildJs.js
CHANGED
|
@@ -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);
|
package/src/actions/buildLib.js
CHANGED
|
@@ -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
|
+
}
|
package/src/actions/buildWasm.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
19
|
+
function guardBigIntConversions(target) {
|
|
20
20
|
const gluePath = `${state.config.paths.build}/${target.rawJsName}`;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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');
|