crossbind 2.0.0-beta.60 → 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 (118) hide show
  1. package/README.md +8 -6
  2. package/package.json +6 -1
  3. package/src/actions/buildDependencies.js +37 -9
  4. package/src/actions/buildExternal.js +9 -1
  5. package/src/actions/buildJs.js +1 -1
  6. package/src/actions/buildLib.js +27 -8
  7. package/src/actions/buildNativeCommand.js +84 -0
  8. package/src/actions/buildNode.js +190 -0
  9. package/src/actions/buildTypes.js +8 -2
  10. package/src/actions/buildWasiCommand.js +0 -4
  11. package/src/actions/buildWasm.js +92 -95
  12. package/src/actions/cloudCommands.js +133 -0
  13. package/src/actions/createImportedBridges.js +64 -0
  14. package/src/actions/createInterface.js +318 -65
  15. package/src/actions/createLib.js +60 -43
  16. package/src/actions/getAllBridges.js +1 -1
  17. package/src/actions/getData.js +4 -1
  18. package/src/actions/getLinkInputs.js +70 -0
  19. package/src/actions/isSourceNewer.js +2 -0
  20. package/src/actions/licenses.js +175 -17
  21. package/src/actions/prepareConanDependencies.js +51 -0
  22. package/src/actions/publishBinary.js +32 -0
  23. package/src/actions/run.js +124 -32
  24. package/src/actions/runnerCommands.js +421 -0
  25. package/src/actions/writeRuntimeEntry.js +69 -0
  26. package/src/assets/cmake/CMakeLists.txt +1 -1
  27. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  28. package/src/assets/cmake/command/command.cpp +1 -0
  29. package/src/assets/cmake/dist.cmake +2 -1
  30. package/src/assets/configure/install.sh +22 -0
  31. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  32. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  33. package/src/assets/js-runtime/adapters/worker-comlink.js +87 -56
  34. package/src/assets/js-runtime/core.js +11 -0
  35. package/src/assets/node-import-hooks.mjs +61 -0
  36. package/src/assets/toolchain-digests.json +27 -13
  37. package/src/assets/wasi-runtime/stubs.c +29 -0
  38. package/src/bin.js +290 -37
  39. package/src/index.js +6 -1
  40. package/src/integration/bindHeaderImports.js +18 -0
  41. package/src/integration/getCrossbindScript.js +53 -19
  42. package/src/integration/getDependFilePath.js +18 -12
  43. package/src/integration/resolveNativeImport.js +14 -0
  44. package/src/node/dev.mjs +19 -0
  45. package/src/runner/blobs.js +135 -0
  46. package/src/runner/files.js +193 -0
  47. package/src/runner/server.js +320 -0
  48. package/src/state/attachConanDependencies.js +56 -0
  49. package/src/state/index.js +31 -20
  50. package/src/state/loadConfig.js +36 -24
  51. package/src/state/refreshBuiltDependencies.js +20 -0
  52. package/src/state/refreshConanDependencies.js +17 -0
  53. package/src/utils/androidToolchain.js +4 -0
  54. package/src/utils/appRustCrates.js +17 -1
  55. package/src/utils/appSources.js +112 -0
  56. package/src/utils/archiveFlags.js +12 -0
  57. package/src/utils/boundHeaders.js +79 -0
  58. package/src/utils/bridgeExtras.js +486 -0
  59. package/src/utils/bridgeRuntimeFixes.js +48 -0
  60. package/src/utils/bundledLicenses.js +26 -0
  61. package/src/utils/cargoImport.js +44 -0
  62. package/src/utils/cargoTarget.js +23 -3
  63. package/src/utils/cloudAccount.js +55 -0
  64. package/src/utils/conanDependencies.js +66 -0
  65. package/src/utils/conanImport.js +58 -0
  66. package/src/utils/conanInstall.js +154 -0
  67. package/src/utils/conanProfile.js +203 -0
  68. package/src/utils/conanStage.js +292 -0
  69. package/src/utils/copyToDistSource.js +13 -0
  70. package/src/utils/cppDts.js +134 -55
  71. package/src/utils/darwinToolchain.js +11 -0
  72. package/src/utils/dirLock.js +137 -28
  73. package/src/utils/downloadAndExtractFile.js +34 -7
  74. package/src/utils/embindArgumentGuards.js +84 -0
  75. package/src/utils/embindCallArguments.js +19 -0
  76. package/src/utils/embindJspiQueue.js +25 -0
  77. package/src/utils/embindRsFingerprint.js +26 -0
  78. package/src/utils/execContainer.js +49 -0
  79. package/src/utils/headerImports.js +56 -0
  80. package/src/utils/hostPlatform.js +21 -0
  81. package/src/utils/inputStamp.js +23 -0
  82. package/src/utils/iosToolchain.js +6 -0
  83. package/src/utils/isSourceCmakePackage.js +10 -0
  84. package/src/utils/licenseReport.js +11 -2
  85. package/src/utils/linkLayout.js +8 -1
  86. package/src/utils/linuxToolchain.js +24 -0
  87. package/src/utils/makeTreeWritable.js +12 -0
  88. package/src/utils/nativeImports.js +23 -0
  89. package/src/utils/nativeSourceStamp.js +36 -0
  90. package/src/utils/nodeDevBuild.js +92 -0
  91. package/src/utils/packageLicense.js +101 -0
  92. package/src/utils/pthreadMainScript.js +14 -0
  93. package/src/utils/pullDockerImage.js +10 -5
  94. package/src/utils/relocatePrebuilt.js +119 -0
  95. package/src/utils/remoteClient.js +127 -0
  96. package/src/utils/remoteOutputs.js +39 -0
  97. package/src/utils/remoteRunner.js +97 -0
  98. package/src/utils/remoteUpload.js +134 -0
  99. package/src/utils/replaceFile.js +8 -0
  100. package/src/utils/resolveCompanionPackage.js +51 -0
  101. package/src/utils/resolveEmbindNapi.js +22 -0
  102. package/src/utils/resolveEmbindRust.js +17 -52
  103. package/src/utils/runCargo.js +51 -53
  104. package/src/utils/runConan.js +257 -0
  105. package/src/utils/runtimeEntries.js +56 -0
  106. package/src/utils/rustBridgeGen.js +903 -253
  107. package/src/utils/rustMt.js +4 -0
  108. package/src/utils/scopedEmbind.js +22 -0
  109. package/src/utils/selectRunner.js +81 -0
  110. package/src/utils/sourceFingerprint.js +4 -3
  111. package/src/utils/swigInterface.js +86 -6
  112. package/src/utils/systemKeys.js +43 -3
  113. package/src/utils/targets.js +104 -3
  114. package/src/utils/toolchainNotices.js +5 -0
  115. package/src/utils/wasiToolchain.js +14 -1
  116. package/src/utils/windowsToolchain.js +18 -0
  117. package/vitest.config.js +11 -0
  118. package/src/utils/cppFieldBindings.js +0 -23
@@ -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';
@@ -36,6 +36,8 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
36
36
  // perfectly live bridge.
37
37
  stale = !fs.existsSync(path.resolve(path.dirname(libRs), pathMod[1]));
38
38
  } else if (cargoDependencies && name.startsWith('crate_')) {
39
+ // One bridge per crate; a `crate_<crate>__<module>` left by an older build would
40
+ // register the crate's types a second time.
39
41
  stale = !Object.keys(cargoDependencies)
40
42
  .some((c) => `crate_${c.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()}` === name);
41
43
  }
@@ -54,6 +56,13 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
54
56
  if (isMt && !sysroot) assertMtRustToolchain();
55
57
 
56
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);
57
66
  const manifest = [
58
67
  '# Generated by crossbind - do not edit. Bundles every app-local Rust surface.',
59
68
  '[package]',
@@ -71,6 +80,7 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
71
80
  '[profile.release]',
72
81
  'panic = "abort"',
73
82
  '',
83
+ ...crates.flatMap((c) => [`[profile.release.package.${c.replaceAll('_', '-')}-crossbind-app]`, 'codegen-units = 1', '']),
74
84
  '[workspace]',
75
85
  '',
76
86
  ].join('\n');
@@ -79,6 +89,12 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
79
89
  '// its init-array registrations) into this single staticlib.',
80
90
  ...crates.map((c) => `use ${c}_crossbind_app as _;`),
81
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
+ '',
82
98
  ].join('\n');
83
99
  writeIfChanged(`${superDir}/Cargo.toml`, manifest);
84
100
  writeIfChanged(`${superDir}/src/lib.rs`, lib);
@@ -0,0 +1,112 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+
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];
10
+
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.
63
+ export default function appSourceFiles(projectDir) {
64
+ const files = [];
65
+ const walk = (dir) => {
66
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
67
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || (dir === projectDir && ROOT_OUTPUTS.has(entry.name))) continue;
68
+ const full = path.join(dir, entry.name);
69
+ if (entry.isDirectory()) walk(full);
70
+ else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) files.push(full);
71
+ }
72
+ };
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
+ }
101
+ return files;
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
+ }