crossbind 0.0.1 → 2.0.0-beta.51
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/AGENTS.md +92 -0
- package/CHANGELOG.md +156 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/package.json +52 -4
- package/src/actions/buildBinTools.js +0 -0
- package/src/actions/buildCargo.js +88 -0
- package/src/actions/buildDependencies.js +157 -0
- package/src/actions/buildExternal.js +57 -0
- package/src/actions/buildJs.js +93 -0
- package/src/actions/buildLib.js +115 -0
- package/src/actions/buildTypes.js +33 -0
- package/src/actions/buildWasiCommand.js +101 -0
- package/src/actions/buildWasm.js +322 -0
- package/src/actions/createInterface.js +172 -0
- package/src/actions/createLib.js +249 -0
- package/src/actions/createXCFramework.js +82 -0
- package/src/actions/extensions.js +7 -0
- package/src/actions/getAllBridges.js +20 -0
- package/src/actions/getCmakeParameters.js +66 -0
- package/src/actions/getData.js +46 -0
- package/src/actions/getDependLibs.js +25 -0
- package/src/actions/isSourceNewer.js +35 -0
- package/src/actions/licenses.js +156 -0
- package/src/actions/run.js +307 -0
- package/src/actions/target.js +54 -0
- package/src/assets/cmake/CMakeLists.txt +61 -0
- package/src/assets/cmake/dist.cmake +57 -0
- package/src/assets/cmake/ios.toolchain.cmake +1176 -0
- package/src/assets/cpp-runtime/browser.cpp +12 -0
- package/src/assets/cpp-runtime/commonBridges.cpp +17 -0
- package/src/assets/cpp-runtime/crossbindEmptySource.cpp +9 -0
- package/src/assets/cpp-runtime/node.cpp +19 -0
- package/src/assets/js-runtime/adapters/exception-decode.js +79 -0
- package/src/assets/js-runtime/adapters/fs-browser.js +173 -0
- package/src/assets/js-runtime/adapters/fs-node.js +28 -0
- package/src/assets/js-runtime/adapters/path-fs.js +6 -0
- package/src/assets/js-runtime/adapters/path-url.js +8 -0
- package/src/assets/js-runtime/adapters/vector-coercion.js +209 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +309 -0
- package/src/assets/js-runtime/browser.js +16 -0
- package/src/assets/js-runtime/core.js +193 -0
- package/src/assets/js-runtime/edge.js +18 -0
- package/src/assets/js-runtime/node.js +20 -0
- package/src/assets/packaging/crossbind-package.podspec +17 -0
- package/src/assets/wasi-runtime/stubs.c +84 -0
- package/src/bin.js +416 -0
- package/src/index.js +19 -0
- package/src/integration/getCrossbindScript.js +180 -0
- package/src/integration/getDependFilePath.js +132 -0
- package/src/runtime/wasiRun.mjs +118 -0
- package/src/state/calculateDependencyParameters.js +97 -0
- package/src/state/index.js +128 -0
- package/src/state/loadConfig.js +215 -0
- package/src/utils/appRustCrates.js +106 -0
- package/src/utils/binTools.js +76 -0
- package/src/utils/bridgeAsyncGuard.js +37 -0
- package/src/utils/cargoTarget.js +15 -0
- package/src/utils/cppDts.js +201 -0
- package/src/utils/cppFieldBindings.js +23 -0
- package/src/utils/dependencyRebuild.js +99 -0
- package/src/utils/dirLock.js +71 -0
- package/src/utils/downloadAndExtractFile.js +102 -0
- package/src/utils/familyManifest.js +17 -0
- package/src/utils/findFiles.js +5 -0
- package/src/utils/fixPackageName.js +7 -0
- package/src/utils/flattenConfigForTable.js +75 -0
- package/src/utils/getAbsolutePath.js +14 -0
- package/src/utils/getCMakeListsFilePath.js +17 -0
- package/src/utils/getOsUserAndGroupId.js +11 -0
- package/src/utils/getParentPath.js +12 -0
- package/src/utils/hash.js +33 -0
- package/src/utils/inputStamp.js +28 -0
- package/src/utils/licenseReport.js +94 -0
- package/src/utils/linkLayout.js +34 -0
- package/src/utils/loadJs.js +31 -0
- package/src/utils/loadJson.js +16 -0
- package/src/utils/logger.js +110 -0
- package/src/utils/ociArtifact.js +124 -0
- package/src/utils/overrideDependency.js +99 -0
- package/src/utils/provenance.js +54 -0
- package/src/utils/pullDockerImage.js +105 -0
- package/src/utils/replaceBasePathForDocker.js +18 -0
- package/src/utils/resolveEmbindRust.js +66 -0
- package/src/utils/runCargo.js +228 -0
- package/src/utils/rustBridgeGen.js +1013 -0
- package/src/utils/rustMt.js +76 -0
- package/src/utils/rustSysroot.js +171 -0
- package/src/utils/rustSysrootPin.js +11 -0
- package/src/utils/systemKeys.js +29 -0
- package/src/utils/targets.js +109 -0
- package/src/utils/wasiToolchain.js +47 -0
- package/src/utils/writeIfChanged.js +8 -0
- package/src/utils/writeJson.js +9 -0
- package/vitest.config.js +46 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import replace from 'replace';
|
|
4
|
+
import run from './run.js';
|
|
5
|
+
import getDependLibs from './getDependLibs.js';
|
|
6
|
+
import getData from './getData.js';
|
|
7
|
+
import buildJs from './buildJs.js';
|
|
8
|
+
import triggerExtensions from './extensions.js';
|
|
9
|
+
import state from '../state/index.js';
|
|
10
|
+
import resolveEmbindRustRoot from '../utils/resolveEmbindRust.js';
|
|
11
|
+
import logger from '../utils/logger.js';
|
|
12
|
+
import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
|
|
13
|
+
import { buildLinkLibArgs } from '../utils/linkLayout.js';
|
|
14
|
+
import buildAppRustCrates from '../utils/appRustCrates.js';
|
|
15
|
+
|
|
16
|
+
export default async function buildWasm(target, options = {}) {
|
|
17
|
+
const isProd = target.buildType === 'release';
|
|
18
|
+
const buildType = isProd ? 'Release' : 'Debug';
|
|
19
|
+
|
|
20
|
+
// Caller can opt out of the final emcc link entirely (e.g. when the
|
|
21
|
+
// package is consumed only as a static library by downstream builds).
|
|
22
|
+
if (state.config.export.bundle === false) {
|
|
23
|
+
logger.info(`[${target.path}] wasm+js skipped (export.bundle = false)`);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
// Cargo packages only stage their staticlib; the consuming app links and bundles it.
|
|
27
|
+
if (state.config.export.type === 'cargo') {
|
|
28
|
+
logger.info(`[${target.path}] wasm+js skipped (cargo package - staticlib only)`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// buildLib's cache is keyed on paths.output/prebuilt; after a cache clean the
|
|
33
|
+
// build-dir copy can be gone while the output artifact is still valid — link it then.
|
|
34
|
+
const sourceLibCandidates = [
|
|
35
|
+
`${state.config.paths.build}/Source-${buildType}/${target.path}/lib${state.config.general.name}.a`,
|
|
36
|
+
`${state.config.paths.output}/prebuilt/${target.path}/lib/lib${state.config.general.name}.a`,
|
|
37
|
+
];
|
|
38
|
+
// App-local Rust surfaces (imported .rs files) arrive as ONE super staticlib; it is the
|
|
39
|
+
// single fully-loaded Rust archive of the link (see the libstd rule below).
|
|
40
|
+
const appRustLibs = buildAppRustCrates(target, state.config.paths.cache, state.config.cargoDependencies ?? {});
|
|
41
|
+
const libs = [
|
|
42
|
+
...getDependLibs(target),
|
|
43
|
+
...appRustLibs,
|
|
44
|
+
sourceLibCandidates.find((lib) => fs.existsSync(lib)) ?? sourceLibCandidates[0],
|
|
45
|
+
`${state.config.paths.build}/Bridge-${buildType}/${target.path}/lib${state.config.general.name}.a`,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// By default only the Bridge archive is --whole-archive'd (see
|
|
49
|
+
// linkLayout.js) so wasm-ld dead-code-eliminates everything unreferenced.
|
|
50
|
+
// Two escape hatches, both `export.wholeArchive: true`: on the APP it
|
|
51
|
+
// restores the legacy layout (every archive wholesale); on a LIBRARY's
|
|
52
|
+
// own config it keeps that library's archives whole in every consumer
|
|
53
|
+
// link (for members that self-register from static initializers).
|
|
54
|
+
const wholeArchiveAll = state.config.export.wholeArchive === true;
|
|
55
|
+
const wholeArchiveNames = new Set();
|
|
56
|
+
// Rust libstd rule: every Rust staticlib bundles its own libstd, so at most ONE Rust archive
|
|
57
|
+
// may be fully loaded (the app super staticlib). Generated cargo bridges are linked lazily
|
|
58
|
+
// with their keep symbol pinned (-u pulls just the registration object); manual-bindings
|
|
59
|
+
// crates have no keep symbol and fall back to whole-archive - safe only while they are the
|
|
60
|
+
// single loaded Rust archive.
|
|
61
|
+
const rustKeepFlags = [];
|
|
62
|
+
if (appRustLibs.length > 0) wholeArchiveNames.add('crossbind_app_super');
|
|
63
|
+
state.config.dependencyParameters.getCmakeDepends(target).forEach((dep) => {
|
|
64
|
+
if (dep.export.wholeArchive === true) {
|
|
65
|
+
(dep.export.libName || []).forEach((name) => wholeArchiveNames.add(name));
|
|
66
|
+
}
|
|
67
|
+
if (dep.export.type === 'cargo') {
|
|
68
|
+
const crateLibRs = `${dep.paths.project}/${dep.export.crate ?? 'crate'}/src/lib.rs`.replace('/./', '/');
|
|
69
|
+
const isManual = fs.existsSync(crateLibRs) && fs.readFileSync(crateLibRs, 'utf8').includes('bindings!');
|
|
70
|
+
(dep.export.libName || []).forEach((name) => {
|
|
71
|
+
// wasm-ld's -u does NOT pull archive members (it just leaves an import);
|
|
72
|
+
// --export forces the symbol to resolve, dragging the registration object in.
|
|
73
|
+
if (isManual) wholeArchiveNames.add(name);
|
|
74
|
+
else rustKeepFlags.push(`-Wl,--export=crossbind_keep_${name}`);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// The whole-archived app super-staticlib and a lazily-pulled package bridge object each
|
|
79
|
+
// carry rustc's allocator/panic shims (codegen-units=1 places them in the same object as
|
|
80
|
+
// the pulled registrations). They are bit-identical toolchain synthetics; Mach-O dedups
|
|
81
|
+
// them as weak defs, wasm-ld sees strong duplicates - allow them for this combination only.
|
|
82
|
+
if (appRustLibs.length > 0 && rustKeepFlags.length > 0) {
|
|
83
|
+
rustKeepFlags.push('-Wl,--allow-multiple-definition');
|
|
84
|
+
}
|
|
85
|
+
const linkLibs = buildLinkLibArgs(libs, { wholeArchiveAll, wholeArchiveNames });
|
|
86
|
+
|
|
87
|
+
// Any Rust archive in the link needs the web adapter TU: it provides the flat
|
|
88
|
+
// crossbind_embind_* C-ABI (typeid getters + passthroughs to emscripten's embind).
|
|
89
|
+
const hasRust = rustKeepFlags.length > 0 || appRustLibs.length > 0
|
|
90
|
+
|| state.config.dependencyParameters.getCmakeDepends(target).some((dep) => dep.export.type === 'cargo');
|
|
91
|
+
// The adapter ships in @crossbind/core-embind-rust (declared by the consumer, resolved here).
|
|
92
|
+
const rustSources = hasRust ? [`${resolveEmbindRustRoot()}/adapters/web.cpp`] : [];
|
|
93
|
+
|
|
94
|
+
const binary = getData('binary', target);
|
|
95
|
+
const emccFlags = [...(binary?.emccFlags || []), ...rustKeepFlags];
|
|
96
|
+
|
|
97
|
+
triggerExtensions('buildWasm', 'beforeBuild', [emccFlags]);
|
|
98
|
+
|
|
99
|
+
if (target.runtime === 'mt' && !emccFlags.includes('-pthread')) {
|
|
100
|
+
emccFlags.push('-pthread');
|
|
101
|
+
emccFlags.push('-sPTHREAD_POOL_SIZE=Math.min(navigator.hardwareConcurrency || 1, 2)');
|
|
102
|
+
emccFlags.push('-sPTHREAD_POOL_SIZE_STRICT=2');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (target.platform === 'wasm') {
|
|
106
|
+
emccFlags.push('-msimd128');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (target.arch === 'wasm64') {
|
|
110
|
+
emccFlags.push('-sMEMORY64=1');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (state.config.excludedDependencies?.length && !emccFlags.includes('-sERROR_ON_UNDEFINED_SYMBOLS=0')) {
|
|
114
|
+
emccFlags.push('-sERROR_ON_UNDEFINED_SYMBOLS=0');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Link inputs (flags, lib set, preloaded data) are invisible to a pure
|
|
118
|
+
// artifact-existence cache: a config emccFlags change used to keep
|
|
119
|
+
// serving the old wasm until a manual .crossbind clear. Fingerprint them next
|
|
120
|
+
// to the artifact and treat a mismatch as a cache miss. Lib entries carry
|
|
121
|
+
// size+mtime, so a rebuilt dependency archive (same path, new content)
|
|
122
|
+
// forces the relink too.
|
|
123
|
+
const linkFingerprintFile = `${state.config.paths.build}/${target.jsName}.fingerprint`;
|
|
124
|
+
// The artifact also bundles the JS runtime (buildJs) and links the C runtime
|
|
125
|
+
// entries from cpp-runtime/ - inputs a flag/lib fingerprint cannot see, so a
|
|
126
|
+
// runtime edit used to keep serving the previously linked output.
|
|
127
|
+
const runtimeAssetDirs = [
|
|
128
|
+
`${state.config.paths.cli}/assets/js-runtime`,
|
|
129
|
+
`${state.config.paths.cli}/assets/cpp-runtime`,
|
|
130
|
+
];
|
|
131
|
+
const runtimeAssets = runtimeAssetDirs.flatMap((dir) => (fs.existsSync(dir)
|
|
132
|
+
? fs.readdirSync(dir, { recursive: true })
|
|
133
|
+
.map((file) => `${dir}/${file}`)
|
|
134
|
+
.filter((file) => fs.statSync(file).isFile())
|
|
135
|
+
: []));
|
|
136
|
+
const linkFingerprint = getContentHash(JSON.stringify({
|
|
137
|
+
// Bump when the link-arg layout itself changes (e.g. the move to
|
|
138
|
+
// bridge-only --whole-archive), so cached artifacts from the old
|
|
139
|
+
// layout cannot satisfy the new one. The effective whole-archive
|
|
140
|
+
// set is part of the layout: flipping a config flag must relink.
|
|
141
|
+
linkLayout: 'v2-bridge-only-whole-archive',
|
|
142
|
+
// The emcc arg tail is hardcoded per environment in this file, invisible to the
|
|
143
|
+
// emccFlags entry below - hashing the builder itself makes any inline-arg edit
|
|
144
|
+
// (a new -s flag, an EXPORTED_RUNTIME_METHODS change) a guaranteed cache miss.
|
|
145
|
+
builder: getFilesFingerprint([fileURLToPath(import.meta.url)]),
|
|
146
|
+
wholeArchiveAll,
|
|
147
|
+
wholeArchiveNames: [...wholeArchiveNames].sort(),
|
|
148
|
+
emccFlags,
|
|
149
|
+
libs: libs.map((lib) => {
|
|
150
|
+
const stat = fs.existsSync(lib) ? fs.statSync(lib) : null;
|
|
151
|
+
return { lib, size: stat ? stat.size : null, mtimeMs: stat ? stat.mtimeMs : null };
|
|
152
|
+
}),
|
|
153
|
+
data: getData('data', target),
|
|
154
|
+
runtime: getFilesFingerprint(runtimeAssets),
|
|
155
|
+
rustSources: getFilesFingerprint(rustSources),
|
|
156
|
+
}));
|
|
157
|
+
const linkChanged = !fs.existsSync(linkFingerprintFile)
|
|
158
|
+
|| fs.readFileSync(linkFingerprintFile, { encoding: 'utf8' }) !== linkFingerprint;
|
|
159
|
+
|
|
160
|
+
if (!options.force && !linkChanged && fs.existsSync(`${state.config.paths.build}/${target.jsName}`) && fs.existsSync(`${state.config.paths.build}/${target.wasmName}`)) {
|
|
161
|
+
logger.cachedStep(target, 'wasm+js');
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (target.runtimeEnv === 'browser') {
|
|
166
|
+
logger.startStep(target, 'wasm');
|
|
167
|
+
const t0 = performance.now();
|
|
168
|
+
|
|
169
|
+
triggerExtensions('buildWasm', 'beforeBuildBrowser', [emccFlags]);
|
|
170
|
+
|
|
171
|
+
const data = Object.entries(getData('data', target)).map(([key, value]) => ['--preload-file', `${key.replaceAll('@', '@@')}@/crossbind/${value}`]).flat();
|
|
172
|
+
run('emcc', [
|
|
173
|
+
'-lembind',
|
|
174
|
+
...emccFlags,
|
|
175
|
+
// '-lwebsocket.js', '-sPROXY_POSIX_SOCKETS', '-sWEBSOCKET_DEBUG=1', '-sJSPI', '-g', '-sWASMFS',
|
|
176
|
+
'-sWASM_BIGINT=1', '-s', 'FORCE_FILESYSTEM=1',
|
|
177
|
+
'-sEXPORT_NAME=Module2', // '-pthread', '-sPTHREAD_POOL_SIZE=5',
|
|
178
|
+
...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/browser.cpp`,
|
|
179
|
+
...(isProd ? ['-O3'] : []),
|
|
180
|
+
'-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
|
|
181
|
+
'-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'FORCE_FILESYSTEM=1',
|
|
182
|
+
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
|
183
|
+
// emsdk 6 defaults GROWABLE_ARRAYBUFFERS=1; Firefox/WebKit TextDecoder rejects views
|
|
184
|
+
// over resizable ArrayBuffers, breaking every string crossing the wasm boundary.
|
|
185
|
+
'-s', 'GROWABLE_ARRAYBUFFERS=0',
|
|
186
|
+
'-s', 'WASMFS',
|
|
187
|
+
'-s', 'ENVIRONMENT=web,webview,worker',
|
|
188
|
+
// getExceptionMessage must be listed here too: the helpers flag alone defines it
|
|
189
|
+
// in the glue but does not attach it to the MODULARIZE instance.
|
|
190
|
+
'-s', 'EXPORTED_RUNTIME_METHODS=["FS", "ENV", "getExceptionMessage"]',
|
|
191
|
+
'-fwasm-exceptions',
|
|
192
|
+
// Exports getExceptionMessage so the runtime can decode raw WebAssembly.Exception
|
|
193
|
+
// values back into JS Errors carrying the C++ what() text.
|
|
194
|
+
'-sEXPORT_EXCEPTION_HANDLING_HELPERS',
|
|
195
|
+
'-o', `${state.config.paths.build}/${target.rawJsName}`,
|
|
196
|
+
...data,
|
|
197
|
+
], null, target);
|
|
198
|
+
const t1 = performance.now();
|
|
199
|
+
logger.doneStep(target, 'wasm');
|
|
200
|
+
logger.startStep(target, 'js');
|
|
201
|
+
// The pthread bootstrap spawns workers from _scriptName, captured from
|
|
202
|
+
// document.currentScript at LOAD time - undefined when the bridge is
|
|
203
|
+
// loaded as a module script, so workers would fetch "/undefined" and
|
|
204
|
+
// direct-mode mt init hangs. Inject Module.crossbindMainScript (set by the
|
|
205
|
+
// browser adapter from paths.worker/paths.js; a crossbind-specific key, as
|
|
206
|
+
// emscripten's own mainScriptUrlOrBlob trips emsdk 6 debug assertions
|
|
207
|
+
// on st targets) at SPAWN time instead. The previous spaced
|
|
208
|
+
// 'var _scriptName = ' rewrite no longer matched the minified glue and
|
|
209
|
+
// silently no-opped; the guard below turns any future format drift
|
|
210
|
+
// into a visible error instead.
|
|
211
|
+
replace({
|
|
212
|
+
regex: 'pthreadMainJs=_scriptName',
|
|
213
|
+
replacement: 'pthreadMainJs=Module["crossbindMainScript"]||_scriptName',
|
|
214
|
+
paths: [`${state.config.paths.build}/${target.rawJsName}`],
|
|
215
|
+
recursive: false,
|
|
216
|
+
silent: true,
|
|
217
|
+
});
|
|
218
|
+
const glue = fs.readFileSync(`${state.config.paths.build}/${target.rawJsName}`, 'utf8');
|
|
219
|
+
if (glue.includes('pthreadMainJs') && !glue.includes('Module["crossbindMainScript"]||_scriptName')) {
|
|
220
|
+
logger.error('pthread spawn rewrite missed (emscripten glue format changed?): mt builds may fetch /undefined workers');
|
|
221
|
+
}
|
|
222
|
+
/* replace({
|
|
223
|
+
regex: 'val === 10',
|
|
224
|
+
replacement: 'false',
|
|
225
|
+
paths: [`${state.config.paths.build}/${state.config.general.name}.js`],
|
|
226
|
+
recursive: false,
|
|
227
|
+
silent: true,
|
|
228
|
+
}); */
|
|
229
|
+
await buildJs(target);
|
|
230
|
+
// fs.rmSync(`${state.config.paths.build}/${state.config.general.name}.js`);
|
|
231
|
+
// fs.copyFileSync(`${state.config.paths.build}/${state.config.general.name}.browser.js`, `${state.config.paths.build}/${state.config.general.name}.js`);
|
|
232
|
+
// fs.renameSync(`${state.config.paths.build}/${state.config.general.name}.js`, `${state.config.paths.build}/${state.config.general.name}.worker.browser.js`);
|
|
233
|
+
logger.doneStep(target, 'js');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (target.runtimeEnv === 'edge') {
|
|
237
|
+
logger.startStep(target, 'wasm');
|
|
238
|
+
const t0 = performance.now();
|
|
239
|
+
|
|
240
|
+
triggerExtensions('buildWasm', 'beforeBuildEdge', [emccFlags]);
|
|
241
|
+
|
|
242
|
+
const data = Object.entries(getData('data', target)).map(([key, value]) => ['--preload-file', `${key.replaceAll('@', '@@')}@/crossbind/${value}`]).flat();
|
|
243
|
+
run('emcc', [
|
|
244
|
+
'-lembind',
|
|
245
|
+
...emccFlags,
|
|
246
|
+
'-sWASM_BIGINT=1',
|
|
247
|
+
'-sEXPORT_NAME=Module2',
|
|
248
|
+
// rustSources carries the embind-rust adapter TU (emval/json/tid hooks); without
|
|
249
|
+
// it any linked Rust package archive fails with undefined crossbind_* symbols.
|
|
250
|
+
...linkLibs, ...rustSources,
|
|
251
|
+
...(isProd ? ['-O3'] : []),
|
|
252
|
+
'-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
|
|
253
|
+
'-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'FORCE_FILESYSTEM=1',
|
|
254
|
+
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
|
255
|
+
// See the GROWABLE_ARRAYBUFFERS note in the browser block (Firefox/WebKit TextDecoder).
|
|
256
|
+
'-s', 'GROWABLE_ARRAYBUFFERS=0',
|
|
257
|
+
'-s', 'ENVIRONMENT=web',
|
|
258
|
+
'-s', 'EXPORTED_RUNTIME_METHODS=["ENV", "getExceptionMessage"]',
|
|
259
|
+
'-fwasm-exceptions',
|
|
260
|
+
'-sEXPORT_EXCEPTION_HANDLING_HELPERS',
|
|
261
|
+
'-o', `${state.config.paths.build}/${target.rawJsName}`,
|
|
262
|
+
...data,
|
|
263
|
+
], null, target);
|
|
264
|
+
const t1 = performance.now();
|
|
265
|
+
logger.doneStep(target, 'wasm');
|
|
266
|
+
logger.startStep(target, 'js');
|
|
267
|
+
await buildJs(target);
|
|
268
|
+
logger.doneStep(target, 'js');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (target.runtimeEnv === 'node') {
|
|
272
|
+
logger.startStep(target, 'wasm');
|
|
273
|
+
|
|
274
|
+
triggerExtensions('buildWasm', 'beforeBuildNodeJS', [emccFlags]);
|
|
275
|
+
|
|
276
|
+
run('emcc', [
|
|
277
|
+
'-lembind',
|
|
278
|
+
...emccFlags,
|
|
279
|
+
// '-s', 'FETCH', '-sJSPI', '-sWASM_BIGINT=1', '-pthread', '-sPTHREAD_POOL_SIZE=5',
|
|
280
|
+
'-sWASM_BIGINT=1', '-s', 'FORCE_FILESYSTEM=1',
|
|
281
|
+
...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/node.cpp`,
|
|
282
|
+
...(isProd ? ['-O3'] : []),
|
|
283
|
+
'-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
|
|
284
|
+
'-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'DISABLE_EXCEPTION_CATCHING=0', '-s', 'FORCE_FILESYSTEM=1',
|
|
285
|
+
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
|
286
|
+
// See the GROWABLE_ARRAYBUFFERS note in the browser block (older Node V8 lacks it too).
|
|
287
|
+
'-s', 'GROWABLE_ARRAYBUFFERS=0',
|
|
288
|
+
'-s', 'WASMFS',
|
|
289
|
+
'-s', 'NODE_HOST_ENV=1',
|
|
290
|
+
'-s', 'ENVIRONMENT=node',
|
|
291
|
+
'-s', 'EXPORTED_RUNTIME_METHODS=["FS", "ENV", "getExceptionMessage"]',
|
|
292
|
+
'-fwasm-exceptions',
|
|
293
|
+
'-sEXPORT_EXCEPTION_HANDLING_HELPERS',
|
|
294
|
+
'-o', `${state.config.paths.build}/${target.rawJsName}`,
|
|
295
|
+
], null, target);
|
|
296
|
+
logger.doneStep(target, 'wasm');
|
|
297
|
+
logger.startStep(target, 'js');
|
|
298
|
+
await buildJs(target);
|
|
299
|
+
if (emccFlags.includes('FETCH')) {
|
|
300
|
+
fs.appendFileSync(`${state.config.paths.build}/${target.jsName}`, 'var XMLHttpRequest = require(\'xhr2\');\n');
|
|
301
|
+
}
|
|
302
|
+
// fs.renameSync(`${state.config.paths.build}/${state.config.general.name}.js`, `${state.config.paths.build}/${state.config.general.name}.worker.node.js`);
|
|
303
|
+
logger.doneStep(target, 'js');
|
|
304
|
+
|
|
305
|
+
Object.entries(getData('data', target)).forEach(([key, value]) => {
|
|
306
|
+
if (fs.existsSync(key)) {
|
|
307
|
+
const dAssetPath = `${state.config.paths.build}/data/${value}`;
|
|
308
|
+
if (!fs.existsSync(dAssetPath)) {
|
|
309
|
+
fs.mkdirSync(dAssetPath, { recursive: true });
|
|
310
|
+
fs.cpSync(key, dAssetPath, { recursive: true });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (fs.existsSync(`${state.config.paths.build}/${target.dataName}`)) {
|
|
317
|
+
fs.renameSync(`${state.config.paths.build}/${target.dataName}`, `${state.config.paths.build}/${target.dataTxtName}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
fs.writeFileSync(linkFingerprintFile, linkFingerprint);
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import upath from 'upath';
|
|
4
|
+
import state, { saveCache } from '../state/index.js';
|
|
5
|
+
import { getFileHash } from '../utils/hash.js';
|
|
6
|
+
import guardAsyncBindings from '../utils/bridgeAsyncGuard.js';
|
|
7
|
+
import { writeHeaderDts, parseCppSurface } from '../utils/cppDts.js';
|
|
8
|
+
import { injectFieldBindings } from '../utils/cppFieldBindings.js';
|
|
9
|
+
import writeIfChanged from '../utils/writeIfChanged.js';
|
|
10
|
+
import run from './run.js';
|
|
11
|
+
|
|
12
|
+
export default function createBridgeFile(headerOrModuleFilePath, target = state.targets.find((t) => t.platform === 'wasm')) {
|
|
13
|
+
const interfaceFilePath = upath.resolve(headerOrModuleFilePath);
|
|
14
|
+
if (!fs.existsSync(`${state.config.paths.build}/interface`)) {
|
|
15
|
+
fs.mkdirSync(`${state.config.paths.build}/interface`, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
if (!fs.existsSync(`${state.config.paths.build}/bridge`)) {
|
|
18
|
+
fs.mkdirSync(`${state.config.paths.build}/bridge`, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
const interfaceFile = createInterfaceFile(interfaceFilePath, target);
|
|
21
|
+
// The header may live outside paths.native (e.g. a package-subpath import like
|
|
22
|
+
// '@scope/pkg/native/x.h'), so its own directory must reach swig's include list.
|
|
23
|
+
const bridgeFile = createBridgeFileFromInterfaceFile(interfaceFile, target, upath.dirname(interfaceFilePath));
|
|
24
|
+
const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
|
|
25
|
+
if (bridgeFile && !moduleRegex.test(interfaceFilePath)) {
|
|
26
|
+
// SWIG's -embind backend emits no member variables; inject .property lines for the
|
|
27
|
+
// header's public value fields. Idempotent, so the cached-bridge path is safe too.
|
|
28
|
+
const bridgeContent = fs.readFileSync(bridgeFile, 'utf8');
|
|
29
|
+
const injected = injectFieldBindings(bridgeContent, parseCppSurface(fs.readFileSync(interfaceFilePath, 'utf8'), () => {}));
|
|
30
|
+
if (injected !== bridgeContent) fs.writeFileSync(bridgeFile, injected);
|
|
31
|
+
}
|
|
32
|
+
if (bridgeFile) {
|
|
33
|
+
// Records which header this bridge came from, so directory-driven consumers
|
|
34
|
+
// (getAllBridges) can prune bridges whose header no longer exists.
|
|
35
|
+
writeIfChanged(`${bridgeFile}.source`, `${interfaceFilePath}\n`);
|
|
36
|
+
writeHeaderDts({
|
|
37
|
+
headerFile: interfaceFilePath,
|
|
38
|
+
exportsFile: `${bridgeFile}.exports.json`,
|
|
39
|
+
projectPath: state.config.paths.project,
|
|
40
|
+
cacheDir: state.config.paths.cache,
|
|
41
|
+
dtsMode: state.config.dts,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return bridgeFile;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createInterfaceFile(headerOrModuleFilePath, target) {
|
|
48
|
+
if (!headerOrModuleFilePath) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const fileHash = getFileHash(headerOrModuleFilePath);
|
|
52
|
+
const cachedInterface = state.cache.interfaces[headerOrModuleFilePath];
|
|
53
|
+
if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)) {
|
|
54
|
+
return cachedInterface;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
|
|
58
|
+
const isModule = moduleRegex.test(headerOrModuleFilePath);
|
|
59
|
+
if (isModule) {
|
|
60
|
+
const newPath = `${state.config.paths.build}/interface/${headerOrModuleFilePath.split('/').pop()}`;
|
|
61
|
+
fs.copyFileSync(headerOrModuleFilePath, newPath);
|
|
62
|
+
state.cache.interfaces[headerOrModuleFilePath] = newPath;
|
|
63
|
+
state.cache.hashes[headerOrModuleFilePath] = fileHash;
|
|
64
|
+
saveCache();
|
|
65
|
+
return newPath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const headerPaths = (state.config.dependencyParameters?.getCmakeDependsPathAndName(target).pathsOfCmakeDepends || [])
|
|
69
|
+
.filter((d) => d.startsWith(state.config.paths.base));
|
|
70
|
+
|
|
71
|
+
const temp2 = headerPaths
|
|
72
|
+
.map((p) => headerOrModuleFilePath.match(new RegExp(`^${p}/.*?/include/(.*?)$`, 'i')))
|
|
73
|
+
.filter((p) => p && p.length === 2);
|
|
74
|
+
|
|
75
|
+
const temp = headerOrModuleFilePath.match(/^(.*)\..+?$/);
|
|
76
|
+
if (!temp || temp.length < 2) return null;
|
|
77
|
+
|
|
78
|
+
const filePathWithoutExt = temp[1];
|
|
79
|
+
const interfaceFile = `${filePathWithoutExt}.i`;
|
|
80
|
+
|
|
81
|
+
if (fs.existsSync(interfaceFile)) {
|
|
82
|
+
const newPath = `${state.config.paths.build}/interface/${interfaceFile.split('/').at(-1)}`;
|
|
83
|
+
fs.copyFileSync(interfaceFile, newPath);
|
|
84
|
+
state.cache.interfaces[headerOrModuleFilePath] = newPath;
|
|
85
|
+
state.cache.hashes[headerOrModuleFilePath] = fileHash;
|
|
86
|
+
saveCache();
|
|
87
|
+
return newPath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fileName = filePathWithoutExt.split('/').at(-1);
|
|
91
|
+
|
|
92
|
+
let headerPath = state.config.paths.header.find((path) => headerOrModuleFilePath.startsWith(path));
|
|
93
|
+
if (headerPath) headerPath = headerOrModuleFilePath.substr(headerPath.length + 1);
|
|
94
|
+
else if (temp2 && temp2.length > 0) headerPath = temp2[0][1];
|
|
95
|
+
else headerPath = headerOrModuleFilePath.split('/').at(-1);
|
|
96
|
+
|
|
97
|
+
const content = `#ifndef _${fileName.toUpperCase()}_I
|
|
98
|
+
#define _${fileName.toUpperCase()}_I
|
|
99
|
+
|
|
100
|
+
%module ${fileName.toUpperCase()}
|
|
101
|
+
|
|
102
|
+
%{
|
|
103
|
+
#include "${headerPath}"
|
|
104
|
+
%}
|
|
105
|
+
|
|
106
|
+
%feature("shared_ptr");
|
|
107
|
+
%feature("polymorphic_shared_ptr");
|
|
108
|
+
|
|
109
|
+
%include "${headerPath}"
|
|
110
|
+
|
|
111
|
+
#endif
|
|
112
|
+
`;
|
|
113
|
+
const outputFilePath = `${state.config.paths.build}/interface/${fileName}.i`;
|
|
114
|
+
fs.writeFileSync(outputFilePath, content);
|
|
115
|
+
|
|
116
|
+
state.cache.interfaces[headerOrModuleFilePath] = outputFilePath;
|
|
117
|
+
state.cache.hashes[headerOrModuleFilePath] = fileHash;
|
|
118
|
+
saveCache();
|
|
119
|
+
|
|
120
|
+
return outputFilePath;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Idempotent: wraps every emscripten::async() registration in the generated
|
|
124
|
+
// bridge behind #ifdef CROSSBIND_JSPI (see utils/bridgeAsyncGuard.js). Applied on
|
|
125
|
+
// the cached path too, so bridges generated by older versions get guarded the
|
|
126
|
+
// next time they are picked up.
|
|
127
|
+
function applyAsyncGuard(bridgeFilePath) {
|
|
128
|
+
const bridgeText = fs.readFileSync(bridgeFilePath, { encoding: 'utf8' });
|
|
129
|
+
const guarded = guardAsyncBindings(bridgeText);
|
|
130
|
+
if (guarded !== bridgeText) {
|
|
131
|
+
fs.writeFileSync(bridgeFilePath, guarded);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir = null) {
|
|
136
|
+
if (!interfaceFilePath) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const fileHash = getFileHash(interfaceFilePath);
|
|
141
|
+
const cachedBridge = state.cache.bridges[interfaceFilePath];
|
|
142
|
+
if (state.cache.hashes[interfaceFilePath] === fileHash && cachedBridge && fs.existsSync(cachedBridge)) {
|
|
143
|
+
applyAsyncGuard(cachedBridge);
|
|
144
|
+
return cachedBridge;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const allHeaders = state.config.dependencyParameters.headerPathWithDepends.split(';');
|
|
148
|
+
|
|
149
|
+
let includePath = [
|
|
150
|
+
...state.config.allDependencies.map((d) => `${d.paths.output}/prebuilt/${target.path}/include`),
|
|
151
|
+
...state.config.allDependencies.map((d) => `${d.paths.output}/prebuilt/${target.path}/swig`),
|
|
152
|
+
...state.config.paths.header,
|
|
153
|
+
...allHeaders,
|
|
154
|
+
...(sourceDir ? [sourceDir] : []),
|
|
155
|
+
].filter((path) => !!path.toString()).map((path) => `-I${path}`);
|
|
156
|
+
includePath = [...new Set(includePath)];
|
|
157
|
+
|
|
158
|
+
run('swig', [
|
|
159
|
+
'-c++',
|
|
160
|
+
'-embind',
|
|
161
|
+
'-o', `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`,
|
|
162
|
+
...includePath,
|
|
163
|
+
interfaceFilePath,
|
|
164
|
+
], null, target);
|
|
165
|
+
applyAsyncGuard(`${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`);
|
|
166
|
+
|
|
167
|
+
state.cache.bridges[interfaceFilePath] = `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`;
|
|
168
|
+
state.cache.hashes[interfaceFilePath] = fileHash;
|
|
169
|
+
saveCache();
|
|
170
|
+
|
|
171
|
+
return state.cache.bridges[interfaceFilePath];
|
|
172
|
+
}
|