crossbind 2.0.0-beta.60 → 2.0.0-beta.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +7 -5
  2. package/package.json +1 -1
  3. package/src/actions/buildDependencies.js +22 -7
  4. package/src/actions/buildJs.js +1 -1
  5. package/src/actions/buildLib.js +2 -0
  6. package/src/actions/buildNode.js +149 -0
  7. package/src/actions/buildWasm.js +43 -66
  8. package/src/actions/createInterface.js +101 -27
  9. package/src/actions/createLib.js +3 -2
  10. package/src/actions/getAllBridges.js +1 -1
  11. package/src/actions/getLinkInputs.js +63 -0
  12. package/src/actions/licenses.js +34 -0
  13. package/src/actions/run.js +97 -2
  14. package/src/assets/cmake/CMakeLists.txt +1 -1
  15. package/src/assets/cmake/dist.cmake +1 -1
  16. package/src/assets/js-runtime/adapters/worker-comlink.js +6 -2
  17. package/src/assets/toolchain-digests.json +26 -12
  18. package/src/bin.js +59 -13
  19. package/src/index.js +3 -1
  20. package/src/integration/getCrossbindScript.js +8 -10
  21. package/src/integration/getDependFilePath.js +9 -12
  22. package/src/state/index.js +16 -13
  23. package/src/state/loadConfig.js +3 -0
  24. package/src/utils/appRustCrates.js +2 -0
  25. package/src/utils/appSources.js +21 -0
  26. package/src/utils/cargoImport.js +44 -0
  27. package/src/utils/cargoTarget.js +2 -0
  28. package/src/utils/cppDts.js +77 -41
  29. package/src/utils/embindArgumentGuards.js +45 -0
  30. package/src/utils/embindRsFingerprint.js +26 -0
  31. package/src/utils/headerImports.js +45 -0
  32. package/src/utils/inputStamp.js +23 -0
  33. package/src/utils/linkLayout.js +8 -1
  34. package/src/utils/pullDockerImage.js +8 -5
  35. package/src/utils/resolveCompanionPackage.js +51 -0
  36. package/src/utils/resolveEmbindNapi.js +22 -0
  37. package/src/utils/resolveEmbindRust.js +17 -52
  38. package/src/utils/runCargo.js +4 -3
  39. package/src/utils/rustBridgeGen.js +895 -250
  40. package/src/utils/swigInterface.js +19 -4
  41. package/src/utils/systemKeys.js +8 -0
  42. package/src/utils/targets.js +56 -0
  43. package/src/utils/cppFieldBindings.js +0 -23
@@ -0,0 +1,45 @@
1
+ import fs from 'node:fs';
2
+ import appSourceFiles from './appSources.js';
3
+
4
+ // `import * as x` and `export *` take every name the header exports.
5
+ export const ALL_NAMES = '*';
6
+
7
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
8
+ // The clause shapes are spelled out, so a match cannot run from one statement into the next.
9
+ const HEADER_IMPORT = /\b(import|export)\s+(type\s+)?((?:[A-Za-z_$][\w$]*\s*,\s*)?(?:\{[^}]*\}|\*(?:\s*as\s+[A-Za-z_$][\w$]*)?)|[A-Za-z_$][\w$]*)\s*from\s*(['"])([^'"\n]+)\4/g;
10
+
11
+ function listedNames(list) {
12
+ return list.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, '').split(',')
13
+ .map((entry) => entry.trim())
14
+ .filter((entry) => entry && !/^type\s/.test(entry))
15
+ .map((entry) => entry.split(/\s+as\s+/)[0])
16
+ .filter((name) => name !== 'default' && IDENTIFIER.test(name));
17
+ }
18
+
19
+ // A specifier built at run time is not found.
20
+ export function findHeaderImports(text, headerExtensions) {
21
+ const isHeader = new RegExp(`\\.(${headerExtensions.join('|')})$`);
22
+ return [...text.matchAll(HEADER_IMPORT)].flatMap(([, , type, clause, , specifier]) => {
23
+ if (type || !isHeader.test(specifier)) return [];
24
+ const list = clause.match(/\{([^}]*)\}/);
25
+ if (clause.replace(list?.[0] ?? '', '').includes('*')) return [{ specifier, names: ALL_NAMES }];
26
+ const names = listedNames(list?.[1] ?? '');
27
+ return names.length ? [{ specifier, names }] : [];
28
+ });
29
+ }
30
+
31
+ const scanned = new Map();
32
+
33
+ // A source is read again only when its size or modification time changed.
34
+ export function findHeaderImportsIn(projectDir, headerExtensions) {
35
+ const extensions = headerExtensions.join(',');
36
+ return appSourceFiles(projectDir).flatMap((importer) => {
37
+ const { mtimeMs, size } = fs.statSync(importer);
38
+ const cached = scanned.get(importer);
39
+ if (cached?.mtimeMs !== mtimeMs || cached.size !== size || cached.extensions !== extensions) {
40
+ const imports = findHeaderImports(fs.readFileSync(importer, 'utf8'), headerExtensions);
41
+ scanned.set(importer, { mtimeMs, size, extensions, imports });
42
+ }
43
+ return scanned.get(importer).imports.map((found) => ({ importer, ...found }));
44
+ });
45
+ }
@@ -21,6 +21,29 @@ export function collectInputFiles(roots, exts, extraFiles = []) {
21
21
  return [...files].filter((file) => fs.existsSync(file)).sort();
22
22
  }
23
23
 
24
+ // A cargo target dir holds build-script output, which changes on every build.
25
+ const RUST_EXCLUDED_DIRS = [...EXCLUDED_DIRS, 'target'];
26
+
27
+ // An app-local .rs file reaches its generated bridge through #[path], so the bridge text stays
28
+ // the same across a body edit: the sources themselves have to be stamped.
29
+ export function collectRustSources(roots) {
30
+ const files = new Set();
31
+ [...new Set(roots)].forEach((root) => {
32
+ findFiles('**/*.rs', {
33
+ cwd: root,
34
+ ignore: RUST_EXCLUDED_DIRS.map((dir) => `**/${dir}/**`),
35
+ }).forEach((file) => files.add(file));
36
+ });
37
+ return [...files].sort();
38
+ }
39
+
40
+ export function collectRustBridgeFiles(cacheDir) {
41
+ return [
42
+ ...findFiles('rust-bridges/*/{Cargo.toml,src/lib.rs}', { cwd: cacheDir }),
43
+ ...findFiles('rust-crates/*.rs', { cwd: cacheDir }),
44
+ ].sort();
45
+ }
46
+
24
47
  export function computeInputStamp(roots, exts, extraFiles, salt) {
25
48
  const lines = collectInputFiles(roots, exts, extraFiles).map((file) => `${file}:${getFileHash(file)}`);
26
49
  lines.push(salt);
@@ -13,7 +13,9 @@ export function libNameOf(libPath) {
13
13
  return match ? match[1] : base;
14
14
  }
15
15
 
16
- export function buildLinkLibArgs(libs, { wholeArchiveAll = false, wholeArchiveNames = new Set(), wrapLast = true } = {}) {
16
+ export function buildLinkLibArgs(libs, {
17
+ wholeArchiveAll = false, wholeArchiveNames = new Set(), wrapLast = true, forceLoad = false,
18
+ } = {}) {
17
19
  const args = [];
18
20
  let wrapped = false;
19
21
  libs.forEach((lib, index) => {
@@ -21,6 +23,11 @@ export function buildLinkLibArgs(libs, { wholeArchiveAll = false, wholeArchiveNa
21
23
  // have no bridge (main() is the GC root), so they pass false.
22
24
  const isBridge = wrapLast && index === libs.length - 1;
23
25
  const wantWrap = wholeArchiveAll || isBridge || wholeArchiveNames.has(libNameOf(lib));
26
+ // Apple's ld64 has no --whole-archive groups; it keeps archives one at a time.
27
+ if (forceLoad) {
28
+ args.push(wantWrap ? `-Wl,-force_load,${lib}` : lib);
29
+ return;
30
+ }
24
31
  if (wantWrap !== wrapped) {
25
32
  args.push(wantWrap ? '-Wl,--whole-archive' : '-Wl,--no-whole-archive');
26
33
  wrapped = wantWrap;
@@ -7,8 +7,11 @@ import { getContentHash } from './hash.js';
7
7
  const table = createRequire(import.meta.url)('../assets/toolchain-digests.json');
8
8
  const REGISTRY = table.registry;
9
9
  const IMAGE_VERSION = table.version;
10
+ const OVERRIDE_KEYS = {
11
+ web: 'CROSSBIND_IMAGE_WEB', android: 'CROSSBIND_IMAGE_ANDROID', linux: 'CROSSBIND_IMAGE_LINUX', windows: 'CROSSBIND_IMAGE_WINDOWS',
12
+ };
10
13
  const IMAGES = Object.fromEntries(
11
- ['web', 'android'].map((role) => [
14
+ Object.keys(OVERRIDE_KEYS).map((role) => [
12
15
  role,
13
16
  {
14
17
  ref: `${REGISTRY}/${role}@${table.images[role].index}`,
@@ -17,11 +20,11 @@ const IMAGES = Object.fromEntries(
17
20
  ]),
18
21
  );
19
22
 
20
- const OVERRIDE_KEYS = { web: 'CROSSBIND_IMAGE_WEB', android: 'CROSSBIND_IMAGE_ANDROID' };
21
-
22
- // Which image a target compiles in: wasm and wasi share the web image, ios never reaches docker.
23
+ // Which image a target compiles in: wasm and wasi share the web image, ios and darwin never reach docker.
23
24
  export function imageRoleFor(target) {
24
- return target?.platform === 'android' ? 'android' : 'web';
25
+ if (target?.platform === 'android' || target?.platform === 'linux') return target.platform;
26
+ if (target?.platform === 'win32') return 'windows';
27
+ return 'web';
25
28
  }
26
29
 
27
30
  const warned = new Set();
@@ -0,0 +1,51 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import { fileURLToPath } from 'node:url';
5
+ import state from '../state/index.js';
6
+
7
+ // The binding runtimes ship as their own packages and the ENGINE never depends on them: the
8
+ // consumer declares one - directly, or transitively through a platform plugin - and the engine
9
+ // only resolves it. pnpm keeps transitive deps unhoisted, so resolution may need to hop through
10
+ // the declaring plugin; inside this monorepo the package sits next to the engine under core/.
11
+ export default function createCompanionResolver({
12
+ pkg, anchors = [], siblingDir, siblingMarker, missingMessage,
13
+ }) {
14
+ let cached = null;
15
+
16
+ const resolveFrom = (baseDir) => {
17
+ try {
18
+ const req = createRequire(path.join(baseDir, 'package.json'));
19
+ return path.dirname(fs.realpathSync(req.resolve(`${pkg}/package.json`)));
20
+ } catch (e) {
21
+ return null;
22
+ }
23
+ };
24
+
25
+ return function resolveCompanionRoot() {
26
+ if (cached) return cached;
27
+
28
+ const project = state.config?.paths?.project;
29
+ let root = project ? resolveFrom(project) : null;
30
+ if (!root && project) {
31
+ for (const anchor of anchors) {
32
+ try {
33
+ const req = createRequire(path.join(project, 'package.json'));
34
+ const anchorDir = path.dirname(fs.realpathSync(req.resolve(`${anchor}/package.json`)));
35
+ root = resolveFrom(anchorDir);
36
+ if (root) break;
37
+ } catch (e) { /* this plugin is not installed - try the next anchor */ }
38
+ }
39
+ }
40
+ if (!root) {
41
+ const here = path.dirname(fileURLToPath(import.meta.url));
42
+ const sibling = path.resolve(here, '../../..', siblingDir);
43
+ if (fs.existsSync(`${sibling}/${siblingMarker}`)) root = sibling;
44
+ }
45
+ if (!root) {
46
+ throw new Error(missingMessage);
47
+ }
48
+ cached = root;
49
+ return root;
50
+ };
51
+ }
@@ -0,0 +1,22 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import createCompanionResolver from './resolveCompanionPackage.js';
5
+
6
+ const PKG = '@crossbind/core-embind-napi';
7
+
8
+ const resolveEmbindNapiRoot = createCompanionResolver({
9
+ pkg: PKG,
10
+ siblingDir: 'embind-napi',
11
+ siblingMarker: 'cpp/src/node_api_module.cpp',
12
+ missingMessage: `crossbind: native Node.js builds need ${PKG} - add it to your devDependencies.`,
13
+ });
14
+
15
+ export default resolveEmbindNapiRoot;
16
+
17
+ // The embind runtime itself ships in @crossbind/core-embind-jsi, a dependency of the Node-API
18
+ // package, so it resolves from there.
19
+ export function resolveEmbindJsiRoot() {
20
+ const req = createRequire(path.join(resolveEmbindNapiRoot(), 'package.json'));
21
+ return path.dirname(fs.realpathSync(req.resolve('@crossbind/core-embind-jsi/package.json')));
22
+ }
@@ -1,60 +1,25 @@
1
1
  import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { createRequire } from 'node:module';
4
- import { fileURLToPath } from 'node:url';
5
- import state from '../state/index.js';
2
+ import createCompanionResolver from './resolveCompanionPackage.js';
6
3
 
7
- // The Rust producer crate + adapters ship in @crossbind/core-embind-rust. The ENGINE never
8
- // depends on it (same direction rule as core-embind-jsi): the consumer declares it - directly,
9
- // or transitively through a platform plugin - and the engine only resolves it. pnpm keeps
10
- // transitive deps unhoisted, so resolution may need to hop through the declaring plugin.
4
+ // The Rust producer crate + adapters ship in @crossbind/core-embind-rust (same direction rule as
5
+ // core-embind-jsi: the consumer declares it, the engine only resolves it).
11
6
  const PKG = '@crossbind/core-embind-rust';
12
- const PLUGIN_ANCHORS = [
13
- '@crossbind/plugin-react-native',
14
- '@crossbind/plugin-vite',
15
- '@crossbind/plugin-rollup',
16
- '@crossbind/plugin-metro',
17
- '@crossbind/plugin-webpack',
18
- ];
19
7
 
20
- let cached = null;
8
+ const resolveEmbindRustRoot = createCompanionResolver({
9
+ pkg: PKG,
10
+ anchors: [
11
+ '@crossbind/plugin-react-native',
12
+ '@crossbind/plugin-vite',
13
+ '@crossbind/plugin-rollup',
14
+ '@crossbind/plugin-metro',
15
+ '@crossbind/plugin-webpack',
16
+ ],
17
+ siblingDir: 'embind-rust',
18
+ siblingMarker: 'crate/Cargo.toml',
19
+ missingMessage: `crossbind: Rust bindings need ${PKG} - add it to your (dev)dependencies.`,
20
+ });
21
21
 
22
- export default function resolveEmbindRustRoot() {
23
- if (cached) return cached;
24
-
25
- const resolveFrom = (baseDir) => {
26
- try {
27
- const req = createRequire(path.join(baseDir, 'package.json'));
28
- return path.dirname(fs.realpathSync(req.resolve(`${PKG}/package.json`)));
29
- } catch (e) {
30
- return null;
31
- }
32
- };
33
-
34
- const project = state.config?.paths?.project;
35
- let root = project ? resolveFrom(project) : null;
36
- if (!root && project) {
37
- for (const anchor of PLUGIN_ANCHORS) {
38
- try {
39
- const req = createRequire(path.join(project, 'package.json'));
40
- const anchorDir = path.dirname(fs.realpathSync(req.resolve(`${anchor}/package.json`)));
41
- root = resolveFrom(anchorDir);
42
- if (root) break;
43
- } catch (e) { /* this plugin is not installed - try the next anchor */ }
44
- }
45
- }
46
- if (!root) {
47
- // Monorepo layout: the package ships next to the engine.
48
- const here = path.dirname(fileURLToPath(import.meta.url));
49
- const sibling = path.resolve(here, '../../../embind-rust');
50
- if (fs.existsSync(`${sibling}/crate/Cargo.toml`)) root = sibling;
51
- }
52
- if (!root) {
53
- throw new Error(`crossbind: Rust bindings need ${PKG} - add it to your (dev)dependencies.`);
54
- }
55
- cached = root;
56
- return root;
57
- }
22
+ export default resolveEmbindRustRoot;
58
23
 
59
24
  // The package version, stamped into generated bridge manifests for debuggability.
60
25
  export function embindRustVersion() {
@@ -7,6 +7,7 @@ import getOsUserAndGroupId from './getOsUserAndGroupId.js';
7
7
  import replaceBasePathForDockerUtil, { DOCKER_BASE } from './replaceBasePathForDocker.js';
8
8
  import pullDockerImage, { getDockerImage, getDockerContainerName, imageRoleFor } from './pullDockerImage.js';
9
9
  import { DOCKER_RUN_SECURITY_ARGS } from './dockerSecurity.js';
10
+ import { HOST_BUILT_PLATFORMS } from './targets.js';
10
11
 
11
12
  // Every cargo invocation crossbind makes goes through here.
12
13
  //
@@ -107,10 +108,10 @@ export function assertCleanConfigChain(home, cwd) {
107
108
  }
108
109
  }
109
110
 
110
- // iOS links with Xcode, which is in no image, so its Rust stays on the host - as does every build
111
- // under RUNNER=LOCAL.
111
+ // Apple targets link with Xcode, which is in no image, so their Rust stays on the host - as does
112
+ // every build under RUNNER=LOCAL.
112
113
  export function cargoRunner(target) {
113
- if (target?.platform === 'ios') return 'LOCAL';
114
+ if (HOST_BUILT_PLATFORMS.includes(target?.platform)) return 'LOCAL';
114
115
  const runner = state.config?.system?.RUNNER;
115
116
  return runner === 'DOCKER_RUN' || runner === 'DOCKER_EXEC' ? runner : 'LOCAL';
116
117
  }