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.
Files changed (107) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -1
  3. package/src/actions/buildDependencies.js +15 -2
  4. package/src/actions/buildExternal.js +9 -1
  5. package/src/actions/buildLib.js +25 -8
  6. package/src/actions/buildNativeCommand.js +84 -0
  7. package/src/actions/buildNode.js +48 -7
  8. package/src/actions/buildTypes.js +8 -2
  9. package/src/actions/buildWasiCommand.js +0 -4
  10. package/src/actions/buildWasm.js +49 -29
  11. package/src/actions/cloudCommands.js +133 -0
  12. package/src/actions/createImportedBridges.js +64 -0
  13. package/src/actions/createInterface.js +247 -68
  14. package/src/actions/createLib.js +57 -41
  15. package/src/actions/getData.js +4 -1
  16. package/src/actions/getLinkInputs.js +12 -5
  17. package/src/actions/isSourceNewer.js +2 -0
  18. package/src/actions/licenses.js +143 -19
  19. package/src/actions/prepareConanDependencies.js +51 -0
  20. package/src/actions/publishBinary.js +32 -0
  21. package/src/actions/run.js +63 -66
  22. package/src/actions/runnerCommands.js +421 -0
  23. package/src/actions/writeRuntimeEntry.js +69 -0
  24. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  25. package/src/assets/cmake/command/command.cpp +1 -0
  26. package/src/assets/cmake/dist.cmake +2 -1
  27. package/src/assets/configure/install.sh +22 -0
  28. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  29. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  30. package/src/assets/js-runtime/adapters/worker-comlink.js +87 -60
  31. package/src/assets/js-runtime/core.js +11 -0
  32. package/src/assets/node-import-hooks.mjs +61 -0
  33. package/src/assets/toolchain-digests.json +19 -19
  34. package/src/assets/wasi-runtime/stubs.c +29 -0
  35. package/src/bin.js +257 -50
  36. package/src/index.js +3 -0
  37. package/src/integration/bindHeaderImports.js +18 -0
  38. package/src/integration/getCrossbindScript.js +46 -10
  39. package/src/integration/getDependFilePath.js +9 -0
  40. package/src/integration/resolveNativeImport.js +14 -0
  41. package/src/node/dev.mjs +19 -0
  42. package/src/runner/blobs.js +135 -0
  43. package/src/runner/files.js +193 -0
  44. package/src/runner/server.js +320 -0
  45. package/src/state/attachConanDependencies.js +56 -0
  46. package/src/state/index.js +17 -9
  47. package/src/state/loadConfig.js +33 -24
  48. package/src/state/refreshBuiltDependencies.js +20 -0
  49. package/src/state/refreshConanDependencies.js +17 -0
  50. package/src/utils/androidToolchain.js +4 -0
  51. package/src/utils/appRustCrates.js +15 -1
  52. package/src/utils/appSources.js +97 -6
  53. package/src/utils/archiveFlags.js +12 -0
  54. package/src/utils/boundHeaders.js +79 -0
  55. package/src/utils/bridgeExtras.js +486 -0
  56. package/src/utils/bridgeRuntimeFixes.js +48 -0
  57. package/src/utils/bundledLicenses.js +26 -0
  58. package/src/utils/cargoTarget.js +21 -3
  59. package/src/utils/cloudAccount.js +55 -0
  60. package/src/utils/conanDependencies.js +66 -0
  61. package/src/utils/conanImport.js +58 -0
  62. package/src/utils/conanInstall.js +154 -0
  63. package/src/utils/conanProfile.js +203 -0
  64. package/src/utils/conanStage.js +292 -0
  65. package/src/utils/copyToDistSource.js +13 -0
  66. package/src/utils/cppDts.js +58 -15
  67. package/src/utils/darwinToolchain.js +11 -0
  68. package/src/utils/dirLock.js +137 -28
  69. package/src/utils/downloadAndExtractFile.js +34 -7
  70. package/src/utils/embindArgumentGuards.js +41 -2
  71. package/src/utils/embindCallArguments.js +19 -0
  72. package/src/utils/embindJspiQueue.js +25 -0
  73. package/src/utils/execContainer.js +49 -0
  74. package/src/utils/headerImports.js +16 -5
  75. package/src/utils/hostPlatform.js +21 -0
  76. package/src/utils/iosToolchain.js +6 -0
  77. package/src/utils/isSourceCmakePackage.js +10 -0
  78. package/src/utils/licenseReport.js +11 -2
  79. package/src/utils/linuxToolchain.js +24 -0
  80. package/src/utils/makeTreeWritable.js +12 -0
  81. package/src/utils/nativeImports.js +23 -0
  82. package/src/utils/nativeSourceStamp.js +36 -0
  83. package/src/utils/nodeDevBuild.js +92 -0
  84. package/src/utils/packageLicense.js +101 -0
  85. package/src/utils/pthreadMainScript.js +14 -0
  86. package/src/utils/pullDockerImage.js +3 -1
  87. package/src/utils/relocatePrebuilt.js +119 -0
  88. package/src/utils/remoteClient.js +127 -0
  89. package/src/utils/remoteOutputs.js +39 -0
  90. package/src/utils/remoteRunner.js +97 -0
  91. package/src/utils/remoteUpload.js +134 -0
  92. package/src/utils/replaceFile.js +8 -0
  93. package/src/utils/runCargo.js +48 -51
  94. package/src/utils/runConan.js +257 -0
  95. package/src/utils/runtimeEntries.js +56 -0
  96. package/src/utils/rustBridgeGen.js +9 -4
  97. package/src/utils/rustMt.js +4 -0
  98. package/src/utils/scopedEmbind.js +22 -0
  99. package/src/utils/selectRunner.js +81 -0
  100. package/src/utils/sourceFingerprint.js +4 -3
  101. package/src/utils/swigInterface.js +69 -4
  102. package/src/utils/systemKeys.js +35 -3
  103. package/src/utils/targets.js +49 -4
  104. package/src/utils/toolchainNotices.js +5 -0
  105. package/src/utils/wasiToolchain.js +14 -1
  106. package/src/utils/windowsToolchain.js +18 -0
  107. 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
+ }
@@ -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 loadConfig from './loadConfig.js';
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
- // Recipes read the wasi-sdk location from the environment (lazily, in
25
- // getBuildParams); mirror the system-config value there so both sources
26
- // behave the same.
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
- d.export.libName.forEach((name) => {
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
  };
@@ -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
- newConfig.functions.isEnabled = newConfig.functions.isEnabled || ((target) => {
198
- // Source-cmake packages ship no prebuilt at all: their own CMakeLists (at the
199
- // package root, not a generated dist/prebuilt one) is compiled into the consuming
200
- // build via add_subdirectory, so they serve every target.
201
- const isSourceCmakePackage = newConfig.export?.type === 'cmake'
202
- && newConfig.paths.cmake !== newConfig.paths.cliCMakeListsTxt
203
- && !newConfig.paths.cmakeDir.endsWith('/prebuilt')
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);
@@ -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
- // Native projects and build outputs: large, and never the app's JavaScript.
6
- const SKIPPED_DIRECTORIES = new Set(['node_modules', 'dist', 'build', 'ios', 'android', 'Pods', 'target']);
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
- // The app's own JavaScript and TypeScript sources, whose import statements say what the app uses.
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('.') || SKIPPED_DIRECTORIES.has(entry.name)) continue;
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
+ }