crossbind 2.0.0-beta.62 → 2.0.0-beta.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +6 -1
- package/src/actions/buildDependencies.js +15 -2
- package/src/actions/buildExternal.js +9 -1
- package/src/actions/buildLib.js +25 -8
- package/src/actions/buildNativeCommand.js +84 -0
- package/src/actions/buildNode.js +48 -7
- package/src/actions/buildTypes.js +8 -2
- package/src/actions/buildWasiCommand.js +0 -4
- package/src/actions/buildWasm.js +49 -29
- package/src/actions/cloudCommands.js +133 -0
- package/src/actions/createImportedBridges.js +64 -0
- package/src/actions/createInterface.js +247 -68
- package/src/actions/createLib.js +57 -41
- package/src/actions/getData.js +4 -1
- package/src/actions/getLinkInputs.js +12 -5
- package/src/actions/isSourceNewer.js +2 -0
- package/src/actions/licenses.js +143 -19
- package/src/actions/prepareConanDependencies.js +51 -0
- package/src/actions/publishBinary.js +32 -0
- package/src/actions/run.js +63 -66
- package/src/actions/runnerCommands.js +421 -0
- package/src/actions/writeRuntimeEntry.js +69 -0
- package/src/assets/cmake/command/CMakeLists.txt +21 -0
- package/src/assets/cmake/command/command.cpp +1 -0
- package/src/assets/cmake/dist.cmake +2 -1
- package/src/assets/configure/install.sh +22 -0
- package/src/assets/js-runtime/adapters/path-url.js +6 -3
- package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
- package/src/assets/js-runtime/adapters/worker-comlink.js +87 -60
- package/src/assets/js-runtime/core.js +11 -0
- package/src/assets/node-import-hooks.mjs +61 -0
- package/src/assets/toolchain-digests.json +19 -19
- package/src/assets/wasi-runtime/stubs.c +29 -0
- package/src/bin.js +257 -50
- package/src/index.js +3 -0
- package/src/integration/bindHeaderImports.js +18 -0
- package/src/integration/getCrossbindScript.js +46 -10
- package/src/integration/getDependFilePath.js +9 -0
- package/src/integration/resolveNativeImport.js +14 -0
- package/src/node/dev.mjs +19 -0
- package/src/runner/blobs.js +135 -0
- package/src/runner/files.js +193 -0
- package/src/runner/server.js +320 -0
- package/src/state/attachConanDependencies.js +56 -0
- package/src/state/index.js +17 -9
- package/src/state/loadConfig.js +33 -24
- package/src/state/refreshBuiltDependencies.js +20 -0
- package/src/state/refreshConanDependencies.js +17 -0
- package/src/utils/androidToolchain.js +4 -0
- package/src/utils/appRustCrates.js +15 -1
- package/src/utils/appSources.js +97 -6
- package/src/utils/archiveFlags.js +12 -0
- package/src/utils/boundHeaders.js +79 -0
- package/src/utils/bridgeExtras.js +486 -0
- package/src/utils/bridgeRuntimeFixes.js +48 -0
- package/src/utils/bundledLicenses.js +26 -0
- package/src/utils/cargoTarget.js +21 -3
- package/src/utils/cloudAccount.js +55 -0
- package/src/utils/conanDependencies.js +66 -0
- package/src/utils/conanImport.js +58 -0
- package/src/utils/conanInstall.js +154 -0
- package/src/utils/conanProfile.js +203 -0
- package/src/utils/conanStage.js +292 -0
- package/src/utils/copyToDistSource.js +13 -0
- package/src/utils/cppDts.js +58 -15
- package/src/utils/darwinToolchain.js +11 -0
- package/src/utils/dirLock.js +137 -28
- package/src/utils/downloadAndExtractFile.js +34 -7
- package/src/utils/embindArgumentGuards.js +41 -2
- package/src/utils/embindCallArguments.js +19 -0
- package/src/utils/embindJspiQueue.js +25 -0
- package/src/utils/execContainer.js +49 -0
- package/src/utils/headerImports.js +16 -5
- package/src/utils/hostPlatform.js +21 -0
- package/src/utils/iosToolchain.js +6 -0
- package/src/utils/isSourceCmakePackage.js +10 -0
- package/src/utils/licenseReport.js +11 -2
- package/src/utils/linuxToolchain.js +24 -0
- package/src/utils/makeTreeWritable.js +12 -0
- package/src/utils/nativeImports.js +23 -0
- package/src/utils/nativeSourceStamp.js +36 -0
- package/src/utils/nodeDevBuild.js +92 -0
- package/src/utils/packageLicense.js +101 -0
- package/src/utils/pthreadMainScript.js +14 -0
- package/src/utils/pullDockerImage.js +3 -1
- package/src/utils/relocatePrebuilt.js +119 -0
- package/src/utils/remoteClient.js +127 -0
- package/src/utils/remoteOutputs.js +39 -0
- package/src/utils/remoteRunner.js +97 -0
- package/src/utils/remoteUpload.js +134 -0
- package/src/utils/replaceFile.js +8 -0
- package/src/utils/runCargo.js +48 -51
- package/src/utils/runConan.js +257 -0
- package/src/utils/runtimeEntries.js +56 -0
- package/src/utils/rustBridgeGen.js +9 -4
- package/src/utils/rustMt.js +4 -0
- package/src/utils/scopedEmbind.js +22 -0
- package/src/utils/selectRunner.js +81 -0
- package/src/utils/sourceFingerprint.js +4 -3
- package/src/utils/swigInterface.js +69 -4
- package/src/utils/systemKeys.js +35 -3
- package/src/utils/targets.js +49 -4
- package/src/utils/toolchainNotices.js +5 -0
- package/src/utils/wasiToolchain.js +14 -1
- package/src/utils/windowsToolchain.js +18 -0
- package/vitest.config.js +11 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { getFilledConfig, flattenDependencies } from './loadConfig.js';
|
|
2
|
+
import calculateDependencyParameters from './calculateDependencyParameters.js';
|
|
3
|
+
import { conanStageDir, conanPackageDir } from '../utils/conanImport.js';
|
|
4
|
+
import { conanDependenciesKey } from '../utils/conanDependencies.js';
|
|
5
|
+
import { readConanManifest, conanTargetName } from '../utils/conanStage.js';
|
|
6
|
+
import { TARGETS, targetPathOf } from '../utils/targets.js';
|
|
7
|
+
|
|
8
|
+
// What a recipe declares for its consumers, per staged target since it declares other things on another
|
|
9
|
+
// platform: the defines your code compiles with, and the system libraries and frameworks a Node.js addon
|
|
10
|
+
// or a native executable links. A framework goes as one linker argument: CMake collapses a repeated
|
|
11
|
+
// -framework. A debug build takes the release packages, so the build type is left out, and a target's
|
|
12
|
+
// specs stay one object, as getData takes them.
|
|
13
|
+
function targetSpecsOf(name, targets) {
|
|
14
|
+
return Object.entries(targets).flatMap(([targetPath, packages]) => {
|
|
15
|
+
const target = TARGETS.find((t) => targetPathOf(t) === targetPath);
|
|
16
|
+
const pkg = packages.find((entry) => entry.name === name);
|
|
17
|
+
if (!target || !pkg) return [];
|
|
18
|
+
const compileOptions = pkg.defines.map((define) => `-D${define}`);
|
|
19
|
+
const addonFlags = [...pkg.frameworks.map((framework) => `-Wl,-framework,${framework}`), ...pkg.systemLibs.map((lib) => `-l${lib}`)];
|
|
20
|
+
const specs = {
|
|
21
|
+
...(compileOptions.length > 0 ? { cmake: { compileOptions } } : {}),
|
|
22
|
+
...(addonFlags.length > 0 ? { binary: { addonFlags } } : {}),
|
|
23
|
+
};
|
|
24
|
+
if (Object.keys(specs).length === 0) return [];
|
|
25
|
+
return [{
|
|
26
|
+
platform: target.platform, arch: target.arch, runtime: target.runtime, specs,
|
|
27
|
+
}];
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Every library a recipe names on some target (libpng is png16 on Windows): a target links the archives
|
|
32
|
+
// staged for it, so a name it lacks drops out there.
|
|
33
|
+
const libsOf = (name, targets) => [...new Set(Object.values(targets).flat().filter((entry) => entry.name === name).flatMap((entry) => entry.libs))];
|
|
34
|
+
|
|
35
|
+
export function conanDependencyConfigs(manifest, stageDir) {
|
|
36
|
+
return manifest.packages.map((pkg) => ({
|
|
37
|
+
general: { name: conanTargetName(pkg.name), conan: pkg },
|
|
38
|
+
export: { type: 'cmake', libName: libsOf(pkg.name, manifest.targets) },
|
|
39
|
+
paths: { project: conanPackageDir(stageDir, pkg.name), output: `${conanPackageDir(stageDir, pkg.name)}/dist` },
|
|
40
|
+
targetSpecs: targetSpecsOf(pkg.name, manifest.targets),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The staged Conan packages join the config as dependencies, so header resolution, the link, the
|
|
45
|
+
// cmake graph and the license rows see them like any port. Only packages staged for the current
|
|
46
|
+
// conanDependencies join; reading the manifest runs nothing, so state can do it while it loads.
|
|
47
|
+
export default function attachConanDependencies(config) {
|
|
48
|
+
const isDeclared = Object.keys(config.conanDependencies ?? {}).length > 0;
|
|
49
|
+
if (!isDeclared && !config.dependencies?.some((d) => d.general.conan)) return;
|
|
50
|
+
const stageDir = conanStageDir(config.paths.cache);
|
|
51
|
+
const manifest = isDeclared ? readConanManifest(stageDir, conanDependenciesKey(config.conanDependencies)) : null;
|
|
52
|
+
const conan = manifest ? conanDependencyConfigs(manifest, stageDir).map((d) => getFilledConfig(d, { isDepend: true })) : [];
|
|
53
|
+
config.dependencies = [...config.dependencies.filter((d) => !d.general.conan), ...conan];
|
|
54
|
+
config.allDependencies = flattenDependencies(config.dependencies);
|
|
55
|
+
config.dependencyParameters = calculateDependencyParameters(config);
|
|
56
|
+
}
|
package/src/state/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import loadJson from '../utils/loadJson.js';
|
|
2
3
|
import writeJson from '../utils/writeJson.js';
|
|
3
|
-
import { TARGETS, targetPathOf, nodeAddonNamesOf } from '../utils/targets.js';
|
|
4
|
+
import { TARGETS, targetPathOf, nodeAddonNamesOf, nativeCommandNamesOf } from '../utils/targets.js';
|
|
4
5
|
import { findCargoModuleImportsIn, writeCargoMarker } from '../utils/cargoImport.js';
|
|
5
|
-
import
|
|
6
|
+
import { exportWasiSdkPath } from '../utils/wasiToolchain.js';
|
|
7
|
+
import loadConfig, { assertBinarySelection } from './loadConfig.js';
|
|
8
|
+
import refreshConanDependencies from './refreshConanDependencies.js';
|
|
6
9
|
|
|
7
10
|
const cacheDir = `${process.cwd()}/.crossbind`;
|
|
8
11
|
|
|
@@ -21,12 +24,9 @@ await initProcessState();
|
|
|
21
24
|
async function initProcessState() {
|
|
22
25
|
state.cache = loadCacheState();
|
|
23
26
|
state.config = await loadConfig();
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (state.config.system.WASI_SDK_PATH && !process.env.CROSSBIND_WASI_SDK_PATH) {
|
|
28
|
-
process.env.CROSSBIND_WASI_SDK_PATH = state.config.system.WASI_SDK_PATH;
|
|
29
|
-
}
|
|
27
|
+
assertBinarySelection(state.config);
|
|
28
|
+
refreshConanDependencies(state.config);
|
|
29
|
+
exportWasiSdkPath(state.config.system);
|
|
30
30
|
// Same idea for the docker image overrides: pullDockerImage.js resolves them from the
|
|
31
31
|
// environment alone, so it stays free of a state import (and of the cycle that would create).
|
|
32
32
|
Object.entries({
|
|
@@ -58,6 +58,9 @@ async function initProcessState() {
|
|
|
58
58
|
if (target.runtimeEnv === 'node' && target.platform !== 'wasm') {
|
|
59
59
|
Object.assign(target, nodeAddonNamesOf(target, state.config.general.name));
|
|
60
60
|
}
|
|
61
|
+
if (target.runtimeEnv === 'native') {
|
|
62
|
+
Object.assign(target, nativeCommandNamesOf(target, state.config.general.name));
|
|
63
|
+
}
|
|
61
64
|
});
|
|
62
65
|
|
|
63
66
|
setAllDependecyPaths();
|
|
@@ -89,7 +92,12 @@ export function setAllDependecyPaths() {
|
|
|
89
92
|
state.config.allDependencyPaths[target.path] = { cmake: {} };
|
|
90
93
|
state.config.allDependencies.forEach((d) => {
|
|
91
94
|
state.config.allDependencyPaths[target.path].cmake[d.general.name] = `${d.paths.output}/prebuilt`;
|
|
92
|
-
|
|
95
|
+
// A Conan recipe can name a library on another platform only (libpng is png16 on Windows).
|
|
96
|
+
const libNames = d.general.conan
|
|
97
|
+
? d.export.libName.filter((name) => [target.path, target.releasePath]
|
|
98
|
+
.some((targetPath) => fs.existsSync(`${d.paths.output}/prebuilt/${targetPath}/lib/lib${name}.a`)))
|
|
99
|
+
: d.export.libName;
|
|
100
|
+
libNames.forEach((name) => {
|
|
93
101
|
state.config.allDependencyPaths[target.path][name] = {
|
|
94
102
|
root: `${d.paths.output}/prebuilt/${target.path}`,
|
|
95
103
|
};
|
package/src/state/loadConfig.js
CHANGED
|
@@ -12,8 +12,23 @@ import fixPackageName from '../utils/fixPackageName.js';
|
|
|
12
12
|
import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
|
|
13
13
|
import calculateDependencyParameters from './calculateDependencyParameters.js';
|
|
14
14
|
import logger from '../utils/logger.js';
|
|
15
|
+
import { RUNTIME_ENVS } from '../utils/targets.js';
|
|
16
|
+
import { normalizeConanDependencies } from '../utils/conanDependencies.js';
|
|
17
|
+
import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
|
|
15
18
|
// import getCmakeParameters from './getCmakeParameters.js';
|
|
16
19
|
|
|
20
|
+
// For the project being built only: dependency builds load published configs that may still set
|
|
21
|
+
// export.bundle.
|
|
22
|
+
export function assertBinarySelection(config) {
|
|
23
|
+
if ('bundle' in config.export) {
|
|
24
|
+
throw new Error('crossbind: export.bundle was removed. A build makes binaries only for the runtime environments it is asked for: -e on the command line, target.runtimeEnv in crossbind.config.js, or a bundler plugin. Remove export.bundle.');
|
|
25
|
+
}
|
|
26
|
+
const { runtimeEnv } = config.target;
|
|
27
|
+
if (runtimeEnv !== undefined && runtimeEnv !== null && !RUNTIME_ENVS.includes(runtimeEnv)) {
|
|
28
|
+
throw new Error(`crossbind: target.runtimeEnv must be one of ${RUNTIME_ENVS.join(', ')} (got ${JSON.stringify(runtimeEnv)}).`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
export default async function loadConfig(configDir = process.cwd(), configName = 'crossbind.config') {
|
|
18
33
|
const config = await loadJs(configDir, configName) || {};
|
|
19
34
|
if (config.excludedDependencies?.length) {
|
|
@@ -80,6 +95,15 @@ export default async function loadConfig(configDir = process.cwd(), configName =
|
|
|
80
95
|
return output;
|
|
81
96
|
}
|
|
82
97
|
|
|
98
|
+
// Direct and transitive dependencies, one entry per package directory.
|
|
99
|
+
export function flattenDependencies(dependencies) {
|
|
100
|
+
const output = {};
|
|
101
|
+
[...dependencies, ...dependencies.map((d) => d.allDependencies).flat()].forEach((d) => {
|
|
102
|
+
output[d.paths.project] = d;
|
|
103
|
+
});
|
|
104
|
+
return Object.values(output);
|
|
105
|
+
}
|
|
106
|
+
|
|
83
107
|
export function getFilledConfig(config, options = { isDepend: false }) {
|
|
84
108
|
const exclude = options.exclude || [];
|
|
85
109
|
const { seen, replaces } = options;
|
|
@@ -101,6 +125,7 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
101
125
|
general: config.general || {},
|
|
102
126
|
dependencies,
|
|
103
127
|
cargoDependencies: config.cargoDependencies || {},
|
|
128
|
+
conanDependencies: normalizeConanDependencies(config.conanDependencies),
|
|
104
129
|
// 'sync' types the direct surface; 'promise' wraps every generated method return
|
|
105
130
|
// for useWorker-style runtimes (constructors stay sync-typed: await new X()).
|
|
106
131
|
dts: config.dts || 'sync',
|
|
@@ -176,15 +201,8 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
176
201
|
newConfig.export.libPath = getPath(newConfig.export.libPath || 'lib');
|
|
177
202
|
newConfig.export.libName = newConfig.export.libName || [newConfig.general.name];
|
|
178
203
|
newConfig.export.binHeaders = newConfig.export.binHeaders || [];
|
|
179
|
-
newConfig.export.bundle = newConfig.export.bundle ?? true;
|
|
180
204
|
|
|
181
|
-
newConfig.allDependencies = (
|
|
182
|
-
const output = {};
|
|
183
|
-
[...newConfig.dependencies, ...newConfig.dependencies.map((d) => d.allDependencies).flat()].forEach((d) => {
|
|
184
|
-
output[d.paths.project] = d;
|
|
185
|
-
});
|
|
186
|
-
return Object.values(output);
|
|
187
|
-
})();
|
|
205
|
+
newConfig.allDependencies = flattenDependencies(newConfig.dependencies);
|
|
188
206
|
|
|
189
207
|
newConfig.extensions?.forEach(e => {
|
|
190
208
|
e?.loadConfig?.after(newConfig);
|
|
@@ -194,22 +212,13 @@ export function getFilledConfig(config, options = { isDepend: false }) {
|
|
|
194
212
|
newConfig.target.runtime = 'mt';
|
|
195
213
|
}
|
|
196
214
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
&& fs.existsSync(newConfig.paths.cmake);
|
|
205
|
-
|
|
206
|
-
return (
|
|
207
|
-
isSourceCmakePackage
|
|
208
|
-
|| fs.existsSync(`${newConfig.paths.cmakeDir}/${target.path}`)
|
|
209
|
-
|| fs.existsSync(`${newConfig.paths.cmakeDir}/${target.releasePath}`)
|
|
210
|
-
|| (target.platform === 'ios' && fs.existsSync(`${newConfig.paths.cmakeDir}/../../${newConfig.general.name}-${target.runtime}.xcframework`))
|
|
211
|
-
);
|
|
212
|
-
});
|
|
215
|
+
// A package that ships its sources serves every target.
|
|
216
|
+
newConfig.functions.isEnabled = newConfig.functions.isEnabled || ((target) => (
|
|
217
|
+
isSourceCmakePackage(newConfig)
|
|
218
|
+
|| fs.existsSync(`${newConfig.paths.cmakeDir}/${target.path}`)
|
|
219
|
+
|| fs.existsSync(`${newConfig.paths.cmakeDir}/${target.releasePath}`)
|
|
220
|
+
|| (target.platform === 'ios' && fs.existsSync(`${newConfig.paths.cmakeDir}/../../${newConfig.general.name}-${target.runtime}.xcframework`))
|
|
221
|
+
));
|
|
213
222
|
|
|
214
223
|
newConfig.dependencyParameters = calculateDependencyParameters(newConfig);
|
|
215
224
|
// newConfig.cmakeParameters = getCmakeParameters(newConfig);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import calculateDependencyParameters from './calculateDependencyParameters.js';
|
|
2
|
+
import getAbsolutePath from '../utils/getAbsolutePath.js';
|
|
3
|
+
import getCMakeListsFilePath, { getCliCMakeListsFile } from '../utils/getCMakeListsFilePath.js';
|
|
4
|
+
import getParentPath from '../utils/getParentPath.js';
|
|
5
|
+
|
|
6
|
+
// A dependency with no build when the config loaded resolved to the CLI's own CMakeLists, which keeps it out of the link
|
|
7
|
+
// and its headers out of the dependency include roots: a process that outlives the build, such as a Metro server, takes it
|
|
8
|
+
// in here. Says whether it took one in.
|
|
9
|
+
export default function refreshBuiltDependencies(config) {
|
|
10
|
+
const built = (config.allDependencies ?? [])
|
|
11
|
+
.filter((dep) => dep !== config && ['cmake', 'cargo'].includes(dep.export?.type) && dep.paths.cmake === config.paths.cliCMakeListsTxt)
|
|
12
|
+
.map((dep) => ({ dep, cmake: getCMakeListsFilePath(dep.paths.output) }))
|
|
13
|
+
.filter(({ cmake }) => cmake !== getCliCMakeListsFile());
|
|
14
|
+
built.forEach(({ dep, cmake }) => {
|
|
15
|
+
dep.paths.cmake = getAbsolutePath(dep.paths.project, cmake);
|
|
16
|
+
dep.paths.cmakeDir = getParentPath(dep.paths.cmake);
|
|
17
|
+
});
|
|
18
|
+
if (built.length > 0) config.dependencyParameters = calculateDependencyParameters(config);
|
|
19
|
+
return built.length > 0;
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import attachConanDependencies from './attachConanDependencies.js';
|
|
2
|
+
import { conanStageDir } from '../utils/conanImport.js';
|
|
3
|
+
import { conanManifestsStamp } from '../utils/conanStage.js';
|
|
4
|
+
|
|
5
|
+
const attachedFrom = new WeakMap();
|
|
6
|
+
|
|
7
|
+
// Attaches what a build staged since this config last attached: a process that loaded state before
|
|
8
|
+
// the build, such as a Metro server, would otherwise bind headers without the packages. Says whether
|
|
9
|
+
// it attached again.
|
|
10
|
+
export default function refreshConanDependencies(config) {
|
|
11
|
+
if (Object.keys(config.conanDependencies ?? {}).length === 0) return false;
|
|
12
|
+
const stamp = conanManifestsStamp(conanStageDir(config.paths.cache));
|
|
13
|
+
if (attachedFrom.get(config) === stamp) return false;
|
|
14
|
+
attachedFrom.set(config, stamp);
|
|
15
|
+
attachConanDependencies(config);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// The android image's NDK, and the API level every Android archive is built against: port archives
|
|
2
|
+
// and Conan packages link into the same library, so they move together.
|
|
3
|
+
export const ANDROID_NDK = '/opt/android-sdk/ndk/current';
|
|
4
|
+
export const ANDROID_API_LEVEL = 33;
|
|
@@ -4,7 +4,7 @@ import { cargoTripleFor } from './cargoTarget.js';
|
|
|
4
4
|
import { isMtWasm, assertMtRustToolchain, cargoTargetDirFor, cargoBuildInvocation } from './rustMt.js';
|
|
5
5
|
import runCargo from './runCargo.js';
|
|
6
6
|
import { sysrootFor } from './rustSysroot.js';
|
|
7
|
-
import { relPath } from './rustBridgeGen.js';
|
|
7
|
+
import { keepSymbolName, relPath } from './rustBridgeGen.js';
|
|
8
8
|
import writeIfChanged from './writeIfChanged.js';
|
|
9
9
|
|
|
10
10
|
const SUPER = '_app_super';
|
|
@@ -56,6 +56,13 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
|
|
|
56
56
|
if (isMt && !sysroot) assertMtRustToolchain();
|
|
57
57
|
|
|
58
58
|
const superDir = `${root}/${SUPER}`;
|
|
59
|
+
// A link that cannot load the super whole pins its keep symbol, which reaches each bridge's; one object per
|
|
60
|
+
// bridge, so that symbol brings the bridge's registrations. A bridge an older crossbind wrote has none.
|
|
61
|
+
const hasKeepSymbol = (c) => {
|
|
62
|
+
const libRs = `${root}/${c}/src/lib.rs`;
|
|
63
|
+
return fs.existsSync(libRs) && fs.readFileSync(libRs, 'utf8').includes(`fn ${keepSymbolName(`${c}_crossbind_app`)}(`);
|
|
64
|
+
};
|
|
65
|
+
const kept = crates.filter(hasKeepSymbol);
|
|
59
66
|
const manifest = [
|
|
60
67
|
'# Generated by crossbind - do not edit. Bundles every app-local Rust surface.',
|
|
61
68
|
'[package]',
|
|
@@ -73,6 +80,7 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
|
|
|
73
80
|
'[profile.release]',
|
|
74
81
|
'panic = "abort"',
|
|
75
82
|
'',
|
|
83
|
+
...crates.flatMap((c) => [`[profile.release.package.${c.replaceAll('_', '-')}-crossbind-app]`, 'codegen-units = 1', '']),
|
|
76
84
|
'[workspace]',
|
|
77
85
|
'',
|
|
78
86
|
].join('\n');
|
|
@@ -81,6 +89,12 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
|
|
|
81
89
|
'// its init-array registrations) into this single staticlib.',
|
|
82
90
|
...crates.map((c) => `use ${c}_crossbind_app as _;`),
|
|
83
91
|
'',
|
|
92
|
+
'#[no_mangle]',
|
|
93
|
+
`pub extern "C" fn ${keepSymbolName('crossbind_app_super')}() {`,
|
|
94
|
+
` let keep: [extern "C" fn(); ${kept.length}] = [${kept.map((c) => `${c}_crossbind_app::${keepSymbolName(`${c}_crossbind_app`)}`).join(', ')}];`,
|
|
95
|
+
' std::hint::black_box(keep);',
|
|
96
|
+
'}',
|
|
97
|
+
'',
|
|
84
98
|
].join('\n');
|
|
85
99
|
writeIfChanged(`${superDir}/Cargo.toml`, manifest);
|
|
86
100
|
writeIfChanged(`${superDir}/src/lib.rs`, lib);
|
package/src/utils/appSources.js
CHANGED
|
@@ -1,21 +1,112 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
3
4
|
|
|
4
|
-
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue', '.svelte']);
|
|
5
|
-
//
|
|
6
|
-
const
|
|
5
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts', '.vue', '.svelte', '.html', '.astro', '.mdx']);
|
|
6
|
+
// Skip root outputs, while allowing source folders named e.g. src/build or src/android.
|
|
7
|
+
const ROOT_OUTPUTS = new Set(['dist', 'build', 'ios', 'android', 'Pods', 'target']);
|
|
8
|
+
const IMPORT = /\b(?:from|import\s*\(?|require\s*\()\s*(['"])([^'"\n]+)\1/g;
|
|
9
|
+
const packageName = (specifier) => specifier.startsWith('@') ? specifier.split('/').slice(0, 2).join('/') : specifier.split('/')[0];
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
function sourceFile(file) {
|
|
12
|
+
const candidates = [file, ...[...SOURCE_EXTENSIONS].map((ext) => `${file}${ext}`), ...[...SOURCE_EXTENSIONS].map((ext) => path.join(file, `index${ext}`))];
|
|
13
|
+
return candidates.find((candidate) => {
|
|
14
|
+
if (!SOURCE_EXTENSIONS.has(path.extname(candidate))) return false;
|
|
15
|
+
try { return fs.statSync(candidate, { throwIfNoEntry: false })?.isFile(); } catch { return false; }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// require.resolve selects CommonJS conditions. Also read runtime export branches: a bundler may
|
|
20
|
+
// choose an import/browser entry whose header imports are absent from the CommonJS entry.
|
|
21
|
+
function dependencySources(specifier, importer) {
|
|
22
|
+
const name = packageName(specifier);
|
|
23
|
+
const subpath = specifier.slice(name.length);
|
|
24
|
+
for (let dir = path.dirname(importer); ; dir = path.dirname(dir)) {
|
|
25
|
+
const root = path.join(dir, 'node_modules', name);
|
|
26
|
+
const manifest = path.join(root, 'package.json');
|
|
27
|
+
if (fs.existsSync(manifest)) {
|
|
28
|
+
try {
|
|
29
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
30
|
+
let entry = pkg.exports;
|
|
31
|
+
let wildcard;
|
|
32
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry) && Object.keys(entry).some((key) => key.startsWith('.'))) {
|
|
33
|
+
const key = `.${subpath}`;
|
|
34
|
+
entry = entry[key];
|
|
35
|
+
if (entry === undefined) {
|
|
36
|
+
for (const pattern of Object.keys(pkg.exports).filter((key) => key.includes('*')).sort((a, b) => b.length - a.length)) {
|
|
37
|
+
const [before, after] = pattern.split('*');
|
|
38
|
+
if (key.startsWith(before) && key.endsWith(after) && key.length >= before.length + after.length) {
|
|
39
|
+
wildcard = key.slice(before.length, key.length - after.length);
|
|
40
|
+
entry = pkg.exports[pattern];
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} else if (subpath) entry = undefined;
|
|
46
|
+
const targets = (value) => typeof value === 'string' ? [value]
|
|
47
|
+
: Array.isArray(value) ? value.flatMap(targets)
|
|
48
|
+
: value && typeof value === 'object' ? Object.entries(value).filter(([condition]) => condition !== 'types').flatMap(([, branch]) => targets(branch)) : [];
|
|
49
|
+
const entries = pkg.exports ? targets(entry)
|
|
50
|
+
: subpath ? [`.${subpath}`] : [pkg.module, typeof pkg.browser === 'string' ? pkg.browser : null, pkg.main ?? 'index.js'];
|
|
51
|
+
return entries.filter(Boolean).flatMap((target) => {
|
|
52
|
+
const file = path.resolve(root, wildcard === undefined ? target : target.replaceAll('*', wildcard));
|
|
53
|
+
if (path.relative(root, file).startsWith('..')) return [];
|
|
54
|
+
return sourceFile(file) ?? [];
|
|
55
|
+
});
|
|
56
|
+
} catch { return []; }
|
|
57
|
+
}
|
|
58
|
+
if (path.dirname(dir) === dir) return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Own sources and the dependency modules they reach. Follow imports rather than walking every installed package.
|
|
9
63
|
export default function appSourceFiles(projectDir) {
|
|
10
64
|
const files = [];
|
|
11
65
|
const walk = (dir) => {
|
|
12
66
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
13
|
-
if (entry.name.startsWith('.') ||
|
|
67
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || (dir === projectDir && ROOT_OUTPUTS.has(entry.name))) continue;
|
|
14
68
|
const full = path.join(dir, entry.name);
|
|
15
69
|
if (entry.isDirectory()) walk(full);
|
|
16
70
|
else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) files.push(full);
|
|
17
71
|
}
|
|
18
72
|
};
|
|
19
|
-
walk(projectDir);
|
|
73
|
+
if (projectDir && fs.existsSync(projectDir)) walk(projectDir);
|
|
74
|
+
let dependencies = {};
|
|
75
|
+
try {
|
|
76
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf8'));
|
|
77
|
+
dependencies = { ...pkg.dependencies, ...pkg.optionalDependencies, ...pkg.peerDependencies };
|
|
78
|
+
} catch { /* A source-only project has no package manifest. */ }
|
|
79
|
+
const ownFiles = new Set(files);
|
|
80
|
+
const visited = new Set(files.map((file) => fs.realpathSync(file)));
|
|
81
|
+
for (let i = 0; i < files.length; i += 1) {
|
|
82
|
+
const importer = files[i];
|
|
83
|
+
const require = createRequire(importer);
|
|
84
|
+
const text = fs.readFileSync(importer, 'utf8');
|
|
85
|
+
for (const [, , specifier] of text.matchAll(IMPORT)) {
|
|
86
|
+
const relative = specifier.startsWith('.');
|
|
87
|
+
if (!relative && ownFiles.has(importer) && !Object.hasOwn(dependencies, packageName(specifier))) continue;
|
|
88
|
+
const resolvedFiles = relative ? [] : dependencySources(specifier, importer);
|
|
89
|
+
try { resolvedFiles.push(require.resolve(specifier)); } catch {
|
|
90
|
+
if (relative) resolvedFiles.push(sourceFile(path.resolve(path.dirname(importer), specifier)));
|
|
91
|
+
}
|
|
92
|
+
for (const resolved of resolvedFiles) {
|
|
93
|
+
if (!resolved || !SOURCE_EXTENSIONS.has(path.extname(resolved)) || !fs.existsSync(resolved)) continue;
|
|
94
|
+
const real = fs.realpathSync(resolved);
|
|
95
|
+
if (visited.has(real)) continue;
|
|
96
|
+
visited.add(real);
|
|
97
|
+
files.push(resolved);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
20
101
|
return files;
|
|
21
102
|
}
|
|
103
|
+
|
|
104
|
+
// Whether a changed file is one of those sources.
|
|
105
|
+
export function isAppSource(projectDir, file) {
|
|
106
|
+
const relative = path.relative(projectDir, file);
|
|
107
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return false;
|
|
108
|
+
const parts = relative.split(path.sep);
|
|
109
|
+
return SOURCE_EXTENSIONS.has(path.extname(file))
|
|
110
|
+
&& parts.every((part) => !part.startsWith('.'))
|
|
111
|
+
&& !ROOT_OUTPUTS.has(parts[0]);
|
|
112
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Every archive linked into one wasm module must agree on exception handling, SIMD, threads and
|
|
2
|
+
// 64-bit memory, or wasm-ld refuses it. Port recipes, the app's own libraries and Conan packages all
|
|
3
|
+
// take their flags from here.
|
|
4
|
+
export const WASM_EXCEPTION_FLAGS = ['-fwasm-exceptions'];
|
|
5
|
+
|
|
6
|
+
export function targetArchiveFlags(target) {
|
|
7
|
+
return [
|
|
8
|
+
...(target.runtime === 'mt' ? ['-pthread'] : []),
|
|
9
|
+
...(target.platform === 'wasm' ? ['-msimd128'] : []),
|
|
10
|
+
...(target.platform === 'wasm' && target.arch === 'wasm64' ? ['-sMEMORY64=1'] : []),
|
|
11
|
+
];
|
|
12
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Headers of dependencies that a build binds whole (export.bindings.headers), the way an app's
|
|
2
|
+
// `import * as x from '<pkg>/<header>'` would: a package that ships prebuilt bindings has no app
|
|
3
|
+
// importing them. An entry is a header path, or a package name standing for every header the package
|
|
4
|
+
// lists in export.publicHeaders.
|
|
5
|
+
|
|
6
|
+
import { releaseTargetOf } from './targets.js';
|
|
7
|
+
|
|
8
|
+
const isName = (entry) => typeof entry === 'string' && entry.length > 0;
|
|
9
|
+
|
|
10
|
+
function publicHeadersOf(packageName, dependencies) {
|
|
11
|
+
const owner = dependencies.find((d) => [d.package?.name, d.general?.alias?.package].includes(packageName)
|
|
12
|
+
&& d.export?.publicHeaders?.length);
|
|
13
|
+
if (!owner) {
|
|
14
|
+
throw new Error(`crossbind: export.bindings.headers names ${packageName}, but no dependency of that name lists export.publicHeaders.`);
|
|
15
|
+
}
|
|
16
|
+
return owner.export.publicHeaders.map((header) => `${packageName}/${header}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function boundHeaderSpecifiers({ export: exportConfig, ext, allDependencies }) {
|
|
20
|
+
const entries = exportConfig.bindings?.headers ?? [];
|
|
21
|
+
if (!Array.isArray(entries) || !entries.every(isName)) {
|
|
22
|
+
throw new Error('crossbind: export.bindings.headers must list header paths or package names.');
|
|
23
|
+
}
|
|
24
|
+
const isHeader = new RegExp(`\\.(${ext.header.join('|')})$`);
|
|
25
|
+
return [...new Set(entries.flatMap((entry) => (isHeader.test(entry) ? [entry] : publicHeadersOf(entry, allDependencies))))];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The bridges come from one target's headers and macros. A linux target goes first, since every host
|
|
29
|
+
// builds one, so a package comes out the same on any machine.
|
|
30
|
+
const BRIDGE_PLATFORM_ORDER = ['linux', 'linuxmusl', 'win32', 'darwin'];
|
|
31
|
+
const bridgeRank = ({ platform }) => {
|
|
32
|
+
const rank = BRIDGE_PLATFORM_ORDER.indexOf(platform);
|
|
33
|
+
return rank < 0 ? BRIDGE_PLATFORM_ORDER.length : rank;
|
|
34
|
+
};
|
|
35
|
+
export const bridgeTargetOrder = (targets) => [...targets].sort((a, b) => bridgeRank(a) - bridgeRank(b));
|
|
36
|
+
|
|
37
|
+
// A macOS host without Docker builds no linux addon, so the targets of the runtime environment it does not
|
|
38
|
+
// build are candidates too: a linux package installed beside it gives the same bridges a linux build made.
|
|
39
|
+
export const bridgeTargets = (buildTargets, allTargets) => bridgeTargetOrder([
|
|
40
|
+
...buildTargets,
|
|
41
|
+
...allTargets.filter((target) => target.platform !== 'wasm' && !buildTargets.includes(target)
|
|
42
|
+
&& buildTargets.some((built) => built.runtimeEnv === target.runtimeEnv)),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// A Node build reads the app's headers for the first of its own targets, in its release form, since Conan
|
|
46
|
+
// packages are staged for release targets only.
|
|
47
|
+
export const nodeBridgeTarget = (targets) => releaseTargetOf(bridgeTargetOrder(targets)[0]);
|
|
48
|
+
|
|
49
|
+
function resolveOrNull(resolve, specifier, target) {
|
|
50
|
+
try {
|
|
51
|
+
return resolve(specifier, target);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A desktop build has no target that every platform package covers, so each header resolves under
|
|
58
|
+
// the first target whose dependency ships it.
|
|
59
|
+
export function resolveBoundHeaders(specifiers, targets, resolve) {
|
|
60
|
+
return specifiers.map((specifier) => {
|
|
61
|
+
const target = targets.find((t) => resolveOrNull(resolve, specifier, t));
|
|
62
|
+
if (!target) {
|
|
63
|
+
throw new Error(`crossbind: export.bindings.headers: no dependency ships ${specifier} for ${targets.map((t) => t.path).join(', ')}.`);
|
|
64
|
+
}
|
|
65
|
+
return { specifier, file: resolve(specifier, target), target };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const OBJECT_LIKE_RENAME = /^\s*#\s*define\s+([A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*$/gm;
|
|
70
|
+
const localOf = (name) => (typeof name === 'string' ? name : name.local);
|
|
71
|
+
|
|
72
|
+
// A header that renames a bound function with an object-like macro (GNU libiconv binds libiconv_open as iconv_open)
|
|
73
|
+
// is called by the macro's name, so the entry exports that name too.
|
|
74
|
+
export function headerAliases(headerText, names) {
|
|
75
|
+
const bound = new Set(names.map(localOf));
|
|
76
|
+
return [...headerText.matchAll(OBJECT_LIKE_RENAME)]
|
|
77
|
+
.filter(([, local, wire]) => bound.has(wire) && !bound.has(local))
|
|
78
|
+
.map(([, local, wire]) => ({ local, wire }));
|
|
79
|
+
}
|