crossbind 2.0.0-beta.69 → 2.0.0-beta.72

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 (49) hide show
  1. package/package.json +3 -2
  2. package/src/actions/addExtension.js +6 -1
  3. package/src/actions/buildApp.js +10 -5
  4. package/src/actions/buildDependencies.js +3 -3
  5. package/src/actions/createImportedBridges.js +10 -8
  6. package/src/actions/createInterface.js +23 -21
  7. package/src/actions/createLib.js +3 -3
  8. package/src/actions/getAllBridges.js +20 -5
  9. package/src/actions/getDependLibs.js +5 -0
  10. package/src/actions/getLinkInputs.js +2 -1
  11. package/src/actions/gradleProject.js +13 -2
  12. package/src/actions/licenses.js +30 -24
  13. package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
  14. package/src/actions/writeRuntimeEntry.js +2 -2
  15. package/src/actions/xcodeProject.js +12 -6
  16. package/src/app/index.d.ts +19 -0
  17. package/src/app/index.js +25 -1
  18. package/src/assets/node-import-hooks.mjs +7 -6
  19. package/src/bin.js +17 -9
  20. package/src/index.js +2 -2
  21. package/src/integration/getCrossbindScript.js +3 -2
  22. package/src/integration/getDependFilePath.js +10 -10
  23. package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
  24. package/src/state/index.js +6 -5
  25. package/src/state/loadConfig.js +10 -4
  26. package/src/state/refreshPackageSources.js +19 -0
  27. package/src/utils/appExtensions.js +44 -28
  28. package/src/utils/appOptions.js +31 -10
  29. package/src/utils/appRustCrates.js +2 -2
  30. package/src/utils/bridgeTools.js +6 -1
  31. package/src/utils/cppDts.js +11 -11
  32. package/src/utils/extensionSteps.js +38 -1
  33. package/src/utils/nativeImports.js +9 -2
  34. package/src/utils/nodeDevBuild.js +2 -1
  35. package/src/utils/packageSourceApi.js +87 -0
  36. package/src/utils/packageSourceImport.js +69 -0
  37. package/src/utils/packageSourceInstall.js +162 -0
  38. package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
  39. package/src/utils/packageSources.js +108 -0
  40. package/src/utils/printableText.js +5 -0
  41. package/src/utils/runPackageSource.js +251 -0
  42. package/src/utils/rustBridgeGen.js +3 -3
  43. package/src/utils/targetToolchain.js +77 -0
  44. package/src/state/refreshConanDependencies.js +0 -17
  45. package/src/utils/conanDependencies.js +0 -66
  46. package/src/utils/conanImport.js +0 -58
  47. package/src/utils/conanInstall.js +0 -154
  48. package/src/utils/conanProfile.js +0 -203
  49. package/src/utils/runConan.js +0 -257
@@ -151,8 +151,9 @@ function objects(name, targetSettings, hasAssets, hasPrivacy, added) {
151
151
  };
152
152
  }
153
153
 
154
- // The asset catalog: the app's icon, from which Xcode makes every size, and the launch screen's color.
155
- function writeAssets(catalog, icon, background) {
154
+ // The asset catalog: the app's icon, from which Xcode makes every size, and the launch screen's color, with the one of
155
+ // the dark appearance where there is one.
156
+ function writeAssets(catalog, icon, background, darkBackground) {
156
157
  const info = { author: 'xcode', version: 1 };
157
158
  const assetSet = (set, contents) => {
158
159
  fs.mkdirSync(`${catalog}/${set}`, { recursive: true });
@@ -163,9 +164,13 @@ function writeAssets(catalog, icon, background) {
163
164
  assetSet('AppIcon.appiconset', { images: [{ filename: 'icon.png', idiom: 'universal', platform: 'ios', size: '1024x1024' }] });
164
165
  fs.writeFileSync(`${catalog}/AppIcon.appiconset/icon.png`, icon);
165
166
  }
167
+ const colorOf = (rgb) => {
168
+ const [red, green, blue] = [1, 3, 5].map((at) => `0x${rgb.slice(at, at + 2).toUpperCase()}`);
169
+ return { 'color-space': 'srgb', components: { alpha: '1.000', red, green, blue } };
170
+ };
166
171
  if (background) {
167
- const [red, green, blue] = [1, 3, 5].map((at) => `0x${background.slice(at, at + 2).toUpperCase()}`);
168
- assetSet(`${LAUNCH_COLOR}.colorset`, { colors: [{ color: { 'color-space': 'srgb', components: { alpha: '1.000', red, green, blue } }, idiom: 'universal' }] });
172
+ const dark = darkBackground ? [{ appearances: [{ appearance: 'luminosity', value: 'dark' }], color: colorOf(darkBackground), idiom: 'universal' }] : [];
173
+ assetSet(`${LAUNCH_COLOR}.colorset`, { colors: [{ color: colorOf(background), idiom: 'universal' }, ...dark] });
169
174
  }
170
175
  }
171
176
 
@@ -176,7 +181,8 @@ function writeAssets(catalog, icon, background) {
176
181
  // where one of them asks for it.
177
182
  export default function writeXcodeProject(to, {
178
183
  parts, identity, label, version, build = version, icon = null, urlSchemes = [], fileTypes = [], links = [], activityTypes = [], shortcutItems = [],
179
- background = null, usage = {}, encryption = null, multipleWindows = false, privacy = {}, entitlements = {}, plist = [], targets = [], appGroup = null,
184
+ background = null, darkBackground = null, usage = {}, encryption = null, multipleWindows = false, privacy = {}, entitlements = {}, plist = [], targets = [],
185
+ appGroup = null,
180
186
  }) {
181
187
  const { name } = state.config.general;
182
188
  if (parts.length === 0) throw new Error(`crossbind: no part of the iOS app ${name} is built, so there is no Xcode project to write.`);
@@ -206,7 +212,7 @@ export default function writeXcodeProject(to, {
206
212
 
207
213
  fs.writeFileSync(`${to}/main.m`, MAIN);
208
214
  fs.writeFileSync(`${to}/Info.plist`, info);
209
- writeAssets(`${to}/Assets.xcassets`, icon, background);
215
+ writeAssets(`${to}/Assets.xcassets`, icon, background, darkBackground);
210
216
  const hasPrivacy = Object.keys(privacy).length > 0;
211
217
  if (hasPrivacy) fs.writeFileSync(`${to}/PrivacyInfo.xcprivacy`, privacyManifest(privacy));
212
218
  const hasEntitlements = Object.keys(signed).length > 0;
@@ -24,6 +24,23 @@ export declare const app: {
24
24
  launchArgs(): Promise<Launch>;
25
25
  };
26
26
 
27
+ /** Where the app runs, as its host tells the page before the page's own scripts run; the same through the app's run. */
28
+ export interface Device {
29
+ /** The platform the app is built for. */
30
+ readonly platform: 'macos' | 'windows' | 'linux' | 'ios' | 'android';
31
+ /** The arch the app's process runs as. */
32
+ readonly arch: 'arm64' | 'x64';
33
+ /** What the device is to its user as the app starts: a phone; a tablet, an iPad or an Android device whose screen's shorter side has 600dp; or a desktop, a computer, a Chromebook or a Mac that runs an iOS app. */
34
+ readonly formFactor: 'phone' | 'tablet' | 'desktop';
35
+ /** The system's name and version, as its maker numbers it: iOS or iPadOS 18.4, Android 15 with its API level, macOS 26.1, Windows 10.0.26100, or a Linux distribution's, as Ubuntu 24.04, else its kernel's release. */
36
+ readonly os: { readonly name: string; readonly version: string; readonly apiLevel?: number };
37
+ /** Whether the iOS Simulator or an Android emulator runs the app. */
38
+ readonly isVirtual: boolean;
39
+ }
40
+
41
+ /** Where the app runs, read at once, as the page starts; reading it outside an app throws. */
42
+ export declare const device: Device;
43
+
27
44
  /** What windows.set() changes of an open window: sizes and places in logical pixels, a page's size without the frame, and x and y the frame's place from the top left of the main screen, given together. A limit of 0 takes it away. */
28
45
  export interface WindowChanges {
29
46
  title?: string;
@@ -57,6 +74,8 @@ export interface WindowOptions extends Omit<WindowChanges, 'center' | 'minWidth'
57
74
  backgroundColor?: string;
58
75
  /** false opens a window without a title bar and its buttons, which the page moves with data-crossbind-drag or windows.startDrag(). */
59
76
  frame?: boolean;
77
+ /** 'hidden' lays a macOS window's page under its title bar, whose buttons stay at its top left, as app.window.titleBar does; Windows and Linux keep their title bar and list it in ignored. */
78
+ titleBar?: 'visible' | 'hidden';
60
79
  /** The id of the window it stays above, and closes with. */
61
80
  parent?: number;
62
81
  /** With a parent: a sheet of the parent's on macOS, and on Windows and Linux a window the parent takes no input under until it closes. */
package/src/app/index.js CHANGED
@@ -39,8 +39,9 @@ const WINDOW_SHARED = {
39
39
  };
40
40
  const WINDOW_SET = { ...WINDOW_SHARED, center: 'boolean' };
41
41
  const WINDOW_OPEN = {
42
- ...WINDOW_SHARED, path: 'string', name: 'string', backgroundColor: 'string', frame: 'boolean', parent: 'number', modal: 'boolean',
42
+ ...WINDOW_SHARED, path: 'string', name: 'string', backgroundColor: 'string', frame: 'boolean', titleBar: 'string', parent: 'number', modal: 'boolean',
43
43
  };
44
+ const TITLE_BARS = ['visible', 'hidden'];
44
45
  const MAX_WINDOW_SIZE = 16384;
45
46
  const MAX_WINDOW_PLACE = 1000000;
46
47
  // The size a window opens at without one.
@@ -70,6 +71,10 @@ function checkedWindow(name, options, kinds) {
70
71
  throw new TypeError(`crossbind: ${name}'s path ${JSON.stringify(path)} is no path of the app's own pages, as /settings.html is.`);
71
72
  }
72
73
  if (backgroundColor !== undefined && !COLOR.test(backgroundColor)) throw new TypeError(`crossbind: ${name}'s backgroundColor is no color of the form #rrggbb.`);
74
+ if (options.titleBar !== undefined && !TITLE_BARS.includes(options.titleBar)) throw new TypeError(`crossbind: ${name}'s titleBar is neither 'visible' nor 'hidden'.`);
75
+ if (options.frame === false && options.titleBar === 'hidden') {
76
+ throw new TypeError(`crossbind: ${name} hides the title bar of a window with a frame alone: drop titleBar, or frame: false.`);
77
+ }
73
78
  if (parent !== undefined && !isWindowId(parent)) throw new TypeError(`crossbind: ${name}'s parent is no window's id.`);
74
79
  if (options.modal && parent === undefined) throw new TypeError(`crossbind: ${name} opens a modal window above a parent: give it parent, the id of a window.`);
75
80
  if (name === 'windows.open') checkedLimits(options);
@@ -96,6 +101,25 @@ function windowOf(name, id) {
96
101
  return { id };
97
102
  }
98
103
 
104
+ // What the app's host tells the page of where the app runs, from before the page's own scripts on.
105
+ function deviceOf(name) {
106
+ const device = globalThis.__crossbind_device;
107
+ if (device) return device[name];
108
+ if (globalThis.__crossbind_token) throw new Error(`crossbind: this app's host is older than crossbind/app; update @crossbind/core-app-host to read device.${name}`);
109
+ throw new Error(`crossbind: device.${name} works in an app alone, whose crossbind.config.js sets target.runtimeEnv: 'app'`);
110
+ }
111
+
112
+ // Where the app runs, the same through its run, which the page reads at once, as it starts: the platform and the arch
113
+ // the app is built for, what the device is to its user, its system's name and version, and whether a simulator or an
114
+ // emulator runs the app.
115
+ export const device = Object.freeze({
116
+ get platform() { return deviceOf('platform'); },
117
+ get arch() { return deviceOf('arch'); },
118
+ get formFactor() { return deviceOf('formFactor'); },
119
+ get os() { return deviceOf('os'); },
120
+ get isVirtual() { return deviceOf('isVirtual'); },
121
+ });
122
+
99
123
  export const app = {
100
124
  // The app's own folders, made when first asked for: { data, cache, logs }.
101
125
  paths: async () => call('app.paths', {}),
@@ -5,7 +5,6 @@ import { registerHooks } from 'node:module';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
7
7
 
8
- const SCHEME = /^(cargo|conan):/;
9
8
  const isBare = (specifier) => !/^(\.{1,2}\/|\/|[A-Za-z]:[\\/]|file:)/.test(specifier);
10
9
 
11
10
  function notBound(what, parentURL, cause) {
@@ -17,23 +16,25 @@ const exportOf = (name) => (typeof name === 'string' ? name : `${name.wire} as $
17
16
  const proxyOf = (entry, names) => `export { ${['initNative', 'AllSymbols', ...names.map(exportOf)].join(', ')} } from ${JSON.stringify(entry)};\n`;
18
17
 
19
18
  // Another hook may still serve it; Node's own resolver hands an unknown scheme on unresolved.
20
- function resolveUnbound(specifier, context, nextResolve) {
19
+ function resolveUnbound(specifier, context, nextResolve, scheme) {
21
20
  let resolved;
22
21
  try {
23
22
  resolved = nextResolve(specifier, context);
24
23
  } catch (error) {
25
24
  throw notBound(specifier, context.parentURL, error);
26
25
  }
27
- if (SCHEME.test(resolved.url)) throw notBound(specifier, context.parentURL);
26
+ if (scheme.test(resolved.url)) throw notBound(specifier, context.parentURL);
28
27
  return resolved;
29
28
  }
30
29
 
31
30
  // `natives` lists each bound file, relative to the entry, with the bare specifiers that name it and the names it
32
- // exports, a name the entry exports under another as { local, wire }.
33
- export default function registerNativeImports(entry, { extensions, natives }) {
31
+ // exports, a name the entry exports under another as { local, wire }. `schemes` are those an import names a store
32
+ // by: the two every build has, for a register file an older build wrote, which names none.
33
+ export default function registerNativeImports(entry, { extensions, schemes = ['cargo', 'conan'], natives }) {
34
34
  const entryUrl = new URL(entry).href;
35
35
  const dir = path.dirname(fileURLToPath(entryUrl));
36
36
  const isNative = new RegExp(`\\.(${extensions.join('|')})$`);
37
+ const scheme = new RegExp(`^(${schemes.join('|')}):`);
37
38
  const byFile = new Map();
38
39
  const bySpecifier = new Map();
39
40
  natives.forEach(({ file, specifiers, names }) => {
@@ -46,7 +47,7 @@ export default function registerNativeImports(entry, { extensions, natives }) {
46
47
  resolve(specifier, context, nextResolve) {
47
48
  const url = bySpecifier.get(specifier);
48
49
  if (url) return { url, format: 'module', shortCircuit: true };
49
- if (isBare(specifier) && (SCHEME.test(specifier) || isNative.test(specifier))) return resolveUnbound(specifier, context, nextResolve);
50
+ if (isBare(specifier) && (scheme.test(specifier) || isNative.test(specifier))) return resolveUnbound(specifier, context, nextResolve, scheme);
50
51
  return nextResolve(specifier, context);
51
52
  },
52
53
  load(url, context, nextLoad) {
package/src/bin.js CHANGED
@@ -44,7 +44,8 @@ import {
44
44
  RUNNER_PLATFORMS, RUNNER_ROLES, connectCommands, deleteRunner, deploySteps, initRunner, newRunnerToken, runnerHostPort, startRunner,
45
45
  } from './actions/runnerCommands.js';
46
46
  import { cargoHome } from './utils/runCargo.js';
47
- import { conanRoot } from './utils/runConan.js';
47
+ import { containerRootOf, sourceRoot } from './utils/runPackageSource.js';
48
+ import { importSchemesOf, sourceNamesOf } from './utils/packageSources.js';
48
49
  import { cleanDepsCache } from './utils/dependencyRebuild.js';
49
50
  import collectLicenseRows from './actions/licenses.js';
50
51
  import { formatNoticesMarkdown, formatCycloneDxSbom, validateSpdx } from './utils/licenseReport.js';
@@ -113,12 +114,18 @@ function declaredSbomPath(projectDir) {
113
114
  const APP_PLATFORMS = ['android', 'ios', 'linux', 'macos', 'windows'];
114
115
 
115
116
  function projectVite() {
117
+ let manifestPath;
116
118
  try {
117
- const manifest = createRequire(`${state.config.paths.project}/package.json`).resolve('vite/package.json');
118
- return path.join(path.dirname(manifest), JSON.parse(fs.readFileSync(manifest, 'utf8')).bin.vite);
119
+ manifestPath = createRequire(`${state.config.paths.project}/package.json`).resolve('vite/package.json');
119
120
  } catch {
120
121
  throw new Error("crossbind: crossbind app builds the app's page with the project's Vite, which the project does not have. Add vite and @crossbind/plugin-vite to it, as the desktop-app template has them.");
121
122
  }
123
+ // A workspace can map vite to a package of Vite's API alone.
124
+ const { name, version, bin } = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
125
+ if (!bin?.vite) {
126
+ throw new Error(`crossbind: crossbind app runs the vite command, and the project's vite resolves to ${name} ${version}, which has none. Have vite resolve to Vite itself for the project, with an override where its workspace maps vite to another package.`);
127
+ }
128
+ return path.join(path.dirname(manifestPath), bin.vite);
122
129
  }
123
130
 
124
131
  // The project's Vite, as `crossbind app dev`, `build` and `publish` run it, and its exit code once it ends. A signal sent
@@ -310,17 +317,18 @@ imageArgument(commandDocker.command('create').description('create docker contain
310
317
  .action((image) => {
311
318
  // Google ships the linux NDK for x86_64 only, so an android or android-app container is always amd64.
312
319
  const platform = ['android', 'android-app'].includes(image) ? 'linux/amd64' : undefined;
320
+ const sources = sourceNamesOf(state.config);
313
321
  // Docker on Linux makes a missing mount source owned by root, which the user's builds cannot write.
314
- [cargoHome(), conanRoot()].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
322
+ [cargoHome(), ...sources.map((name) => sourceRoot(name))].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
315
323
  dockerExec([
316
324
  'run', '-dit',
317
325
  ...(platform ? ['--platform', platform] : []),
318
326
  '--name', dockerContainerName(image),
319
327
  '-v', `${state.config.paths.base}:/tmp/crossbind/live`,
320
- // Cargo's registry cache, shared with host builds and with every other project, and the
321
- // Conan store and work directories every other container build uses.
328
+ // Cargo's registry cache, shared with host builds and with every other project, and each package
329
+ // source's store and work directories, which every other container build of it uses.
322
330
  '-v', `${cargoHome()}:/var/cache/crossbind/cargo`,
323
- '-v', `${conanRoot()}:/var/cache/crossbind/conan`,
331
+ ...sources.flatMap((name) => ['-v', `${sourceRoot(name)}:${containerRootOf(name)}`]),
324
332
  getDockerImage(image, platform),
325
333
  'bash',
326
334
  ]);
@@ -527,7 +535,7 @@ async function createNodeAddons(targetParams) {
527
535
  if (headers.length && fs.existsSync(`${state.config.paths.output}/${loaderTarget.jsName}`)) {
528
536
  const entry = `${state.config.paths.output}/node/napi.mjs`;
529
537
  writeRuntimeEntry(entry, headers, ENTRY_RUNTIMES.node(loaderTarget.jsName));
530
- if (imports.size) writeImportHooks(entry, headers, imports, nativeExtensions(state.config.ext));
538
+ if (imports.size) writeImportHooks(entry, headers, imports, nativeExtensions(state.config.ext), importSchemesOf(state.config));
531
539
  else removeImportHooks(entry);
532
540
  }
533
541
  }
@@ -603,7 +611,7 @@ async function createWasmJs(targetParams, wasmOptions = {}) {
603
611
  if (appHeaders.length && nodeTarget) {
604
612
  const entry = `${state.config.paths.output}/node/wasm.mjs`;
605
613
  writeRuntimeEntry(entry, appHeaders, ENTRY_RUNTIMES.node(nodeTarget.jsName));
606
- if (imports.size) writeImportHooks(entry, appHeaders, imports, nativeExtensions(state.config.ext));
614
+ if (imports.size) writeImportHooks(entry, appHeaders, imports, nativeExtensions(state.config.ext), importSchemesOf(state.config));
607
615
  else removeImportHooks(entry);
608
616
  }
609
617
  if (headers.length && edgeTarget) {
package/src/index.js CHANGED
@@ -4,7 +4,7 @@ export { default as createLib } from './actions/createLib.js';
4
4
  export { default as createBridgeFile } from './actions/createInterface.js';
5
5
  export { default as buildWasm } from './actions/buildWasm.js';
6
6
  export {
7
- default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appTargetParams,
7
+ default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appPlatform, appTargetParams,
8
8
  } from './actions/buildApp.js';
9
9
  export { checkInstaller, shipApp } from './actions/appInstaller.js';
10
10
  export { resolveAppPageApi } from './utils/resolveAppHost.js';
@@ -24,7 +24,7 @@ export { getTargetParams, getBuildTargets, getFilteredBuildTargets, getFilteredT
24
24
  export { default as getCrossbindScript, getRustJsScript } from './integration/getCrossbindScript.js';
25
25
  export { default as getDependFilePath } from './integration/getDependFilePath.js';
26
26
  export { default as buildDependencies, getDependenciesStamp } from './actions/buildDependencies.js';
27
- export { default as prepareConanDependencies } from './actions/prepareConanDependencies.js';
27
+ export { default as preparePackageSources } from './actions/preparePackageSources.js';
28
28
  export { default as findFiles } from './utils/findFiles.js';
29
29
  export { getContentHash, getFileHash } from './utils/hash.js';
30
30
  export {
@@ -5,6 +5,7 @@ import loadJson from '../utils/loadJson.js';
5
5
  import state from '../state/index.js';
6
6
  import { parseSurface, createRustBridgeCrate, createCrateImportBridge } from '../utils/rustBridgeGen.js';
7
7
  import { parseCargoMarkerName } from '../utils/cargoImport.js';
8
+ import { cratesOf } from '../utils/packageSourceImport.js';
8
9
  import { UNBOUND_MESSAGE } from '../assets/js-runtime/adapters/unboundMessage.js';
9
10
 
10
11
  // `base` is where the bundler serves the app from, as Vite's `base` names it; webpack's public path wins at run time.
@@ -62,7 +63,7 @@ export function getRustSymbols(rsFile) {
62
63
  // Marker under <cache>/rust-crates/: a direct crate import - the model comes from the
63
64
  // upstream crate's own source, and the bridge is synthesized against the cargo dependency.
64
65
  const { crateName, modulePath } = parseCargoMarkerName(path.basename(rsReal, '.rs'));
65
- const cargoDeps = state.config.cargoDependencies ?? {};
66
+ const cargoDeps = cratesOf(state.config);
66
67
  if (Object.hasOwn(cargoDeps, crateName)
67
68
  && path.dirname(rsReal) === fs.realpathSync(path.join(state.config.paths.cache, 'rust-crates'))) {
68
69
  const { exports } = createCrateImportBridge({
@@ -92,7 +93,7 @@ export function getRustSymbols(rsFile) {
92
93
  projectPath: state.config.paths.project,
93
94
  dtsMode: state.config.dts,
94
95
  vectors,
95
- cargoDependencies: state.config.cargoDependencies ?? {},
96
+ cargoDependencies: cratesOf(state.config),
96
97
  log: () => {},
97
98
  });
98
99
  }
@@ -2,8 +2,8 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import state from '../state/index.js';
4
4
  import { parseCargoImport, writeCargoMarker } from '../utils/cargoImport.js';
5
- import { CONAN_PREFIX, resolveConanHeader } from '../utils/conanImport.js';
6
- import refreshConanDependencies from '../state/refreshConanDependencies.js';
5
+ import { cratesOf, sourceOfImport, resolveSourceHeader } from '../utils/packageSourceImport.js';
6
+ import refreshPackageSources from '../state/refreshPackageSources.js';
7
7
 
8
8
  const LAYOUT = { header: 'include', module: 'swig', source: '' };
9
9
 
@@ -85,20 +85,20 @@ export default function getDependFilePath(source, target) {
85
85
  // an undeclared crate is a hard, actionable error instead of a silent npm fallthrough.
86
86
  if (source.startsWith('cargo:')) {
87
87
  const cargoImport = parseCargoImport(source);
88
- const cargoDeps = state.config.cargoDependencies ?? {};
89
- if (!Object.hasOwn(cargoDeps, cargoImport.crateName)) {
90
- throw new Error(`crossbind: '${source}' is not declared - add '${cargoImport.crateName}' to cargoDependencies in crossbind.config.`);
88
+ if (!Object.hasOwn(cratesOf(state.config), cargoImport.crateName)) {
89
+ throw new Error(`crossbind: '${source}' is not declared - add '${cargoImport.crateName}' to packages.cargo in crossbind.config.`);
91
90
  }
92
91
  const marker = writeCargoMarker(state.config.paths.cache, cargoImport);
93
92
  existsCache.set(marker, true);
94
93
  return marker;
95
94
  }
96
95
 
97
- // `conan:<package>/<header>` names a header of a declared Conan package, staged per target like a
98
- // port's prebuilt; from there the header takes the same path as any other.
99
- if (source.startsWith(CONAN_PREFIX)) {
100
- refreshConanDependencies(state.config);
101
- return resolveConanHeader(state.config, source, target ?? state.targets.find((t) => t.platform === 'wasm'));
96
+ // `<source>:<package>/<header>`, as conan:zlib/zlib.h, names a header of a declared package of a package
97
+ // source, staged per target like a port's prebuilt; from there the header takes the same path as any other.
98
+ const packageSource = sourceOfImport(state.config, source);
99
+ if (packageSource) {
100
+ refreshPackageSources(state.config);
101
+ return resolveSourceHeader(state.config, packageSource.name, source, target ?? state.targets.find((t) => t.platform === 'wasm'));
102
102
  }
103
103
 
104
104
  const matches = getMatchingPackages(source);
@@ -1,8 +1,8 @@
1
1
  import { getFilledConfig, flattenDependencies } from './loadConfig.js';
2
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';
3
+ import { sourceStageDir, sourcePackageDir } from '../utils/packageSourceImport.js';
4
+ import { declaredSources, dependenciesKey, dependenciesOf } from '../utils/packageSources.js';
5
+ import { readSourceManifest, sourceTargetName } from '../utils/packageSourceStage.js';
6
6
  import { TARGETS, targetPathOf } from '../utils/targets.js';
7
7
 
8
8
  // What a recipe declares for its consumers, per staged target since it declares other things on another
@@ -32,25 +32,29 @@ function targetSpecsOf(name, targets) {
32
32
  // staged for it, so a name it lacks drops out there.
33
33
  const libsOf = (name, targets) => [...new Set(Object.values(targets).flat().filter((entry) => entry.name === name).flatMap((entry) => entry.libs))];
34
34
 
35
- export function conanDependencyConfigs(manifest, stageDir) {
35
+ export function sourceDependencyConfigs(source, manifest, stageDir) {
36
36
  return manifest.packages.map((pkg) => ({
37
- general: { name: conanTargetName(pkg.name), conan: pkg },
37
+ general: { name: sourceTargetName(source, pkg.name), packageSource: { name: source, package: pkg } },
38
38
  export: { type: 'cmake', libName: libsOf(pkg.name, manifest.targets) },
39
- paths: { project: conanPackageDir(stageDir, pkg.name), output: `${conanPackageDir(stageDir, pkg.name)}/dist` },
39
+ paths: { project: sourcePackageDir(stageDir, pkg.name), output: `${sourcePackageDir(stageDir, pkg.name)}/dist` },
40
40
  targetSpecs: targetSpecsOf(pkg.name, manifest.targets),
41
41
  }));
42
42
  }
43
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];
44
+ function stagedDependencies(config, source) {
45
+ const stageDir = sourceStageDir(config.paths.cache, source);
46
+ const manifest = readSourceManifest(source, stageDir, dependenciesKey(dependenciesOf(config, source)));
47
+ return manifest ? sourceDependencyConfigs(source, manifest, stageDir).map((d) => getFilledConfig(d, { isDepend: true })) : [];
48
+ }
49
+
50
+ // The staged packages of each source join the config as dependencies, so header resolution, the link, the
51
+ // cmake graph and the license rows see them like any port. Only packages staged for what the config declares
52
+ // now join; reading a manifest runs nothing, so state can do it while it loads.
53
+ export default function attachPackageSources(config) {
54
+ const declared = declaredSources(config);
55
+ if (declared.length === 0 && !config.dependencies?.some((d) => d.general.packageSource)) return;
56
+ const staged = declared.flatMap(({ name }) => stagedDependencies(config, name));
57
+ config.dependencies = [...config.dependencies.filter((d) => !d.general.packageSource), ...staged];
54
58
  config.allDependencies = flattenDependencies(config.dependencies);
55
59
  config.dependencyParameters = calculateDependencyParameters(config);
56
60
  }
@@ -5,10 +5,11 @@ import {
5
5
  TARGETS, targetPathOf, nodeAddonNamesOf, nativeCommandNamesOf, wasiNamesOf, appNamesOf,
6
6
  } from '../utils/targets.js';
7
7
  import { findCargoModuleImportsIn, writeCargoMarker } from '../utils/cargoImport.js';
8
+ import { cratesOf } from '../utils/packageSourceImport.js';
8
9
  import { exportWasiSdkPath } from '../utils/wasiToolchain.js';
9
10
  import loadConfig, { assertBinarySelection } from './loadConfig.js';
10
11
  import { configChangesOf } from '../utils/configChanges.js';
11
- import refreshConanDependencies from './refreshConanDependencies.js';
12
+ import refreshPackageSources from './refreshPackageSources.js';
12
13
 
13
14
  const cacheDir = `${process.cwd()}/.crossbind`;
14
15
 
@@ -28,7 +29,7 @@ async function initProcessState() {
28
29
  state.cache = loadCacheState();
29
30
  state.config = await loadConfig(undefined, undefined, { changes: configChangesOf() });
30
31
  assertBinarySelection(state.config);
31
- refreshConanDependencies(state.config);
32
+ refreshPackageSources(state.config);
32
33
  exportWasiSdkPath(state.config.system);
33
34
  // Same idea for the docker image overrides: pullDockerImage.js resolves them from the
34
35
  // environment alone, so it stays free of a state import (and of the cycle that would create).
@@ -74,7 +75,7 @@ async function initProcessState() {
74
75
  // Cargo-crate import markers must exist BEFORE any bundler starts: metro resolves against
75
76
  // its startup file map, so a marker first created mid-resolution is invisible to that very
76
77
  // build. Bare crates come from the config, module imports from the app's own sources.
77
- const cargoDeps = state.config.cargoDependencies ?? {};
78
+ const cargoDeps = cratesOf(state.config);
78
79
  const crateNames = Object.keys(cargoDeps);
79
80
  const moduleImports = crateNames.length && state.config.paths.project
80
81
  ? findCargoModuleImportsIn(state.config.paths.project).filter((i) => Object.hasOwn(cargoDeps, i.crateName))
@@ -98,8 +99,8 @@ export function setAllDependecyPaths() {
98
99
  state.config.allDependencyPaths[target.path] = { cmake: {} };
99
100
  state.config.allDependencies.forEach((d) => {
100
101
  state.config.allDependencyPaths[target.path].cmake[d.general.name] = `${d.paths.output}/prebuilt`;
101
- // A Conan recipe can name a library on another platform only (libpng is png16 on Windows).
102
- const libNames = d.general.conan
102
+ // A package source's recipe can name a library on another platform only (libpng is png16 on Windows).
103
+ const libNames = d.general.packageSource
103
104
  ? d.export.libName.filter((name) => [target.path, target.releasePath]
104
105
  .some((targetPath) => fs.existsSync(`${d.paths.output}/prebuilt/${targetPath}/lib/lib${name}.a`)))
105
106
  : d.export.libName;
@@ -12,7 +12,7 @@ import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
12
12
  import calculateDependencyParameters from './calculateDependencyParameters.js';
13
13
  import logger from '../utils/logger.js';
14
14
  import { RUNTIME_ENVS } from '../utils/targets.js';
15
- import { normalizeConanDependencies } from '../utils/conanDependencies.js';
15
+ import { checkedPackages, packageSourcesOf } from '../utils/packageSources.js';
16
16
  import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
17
17
  import { listedExtension } from '../utils/appExtensions.js';
18
18
  import { mergePatch } from '../utils/configChanges.js';
@@ -44,7 +44,9 @@ export default async function loadConfig(configDir = process.cwd(), configName =
44
44
  const overrides = await loadJs(configDir, 'crossbind.overrides');
45
45
  const exclude = resolveExcludedNames(overrides);
46
46
  const seen = new Set();
47
- const output = getFilledConfig(config, { isDepend: false, exclude, seen, replaces: overrides });
47
+ const output = getFilledConfig(config, {
48
+ isDepend: false, exclude, seen, replaces: overrides, file: `${configDir}/${configName}`,
49
+ });
48
50
  output.excludedDependencies = exclude;
49
51
 
50
52
  const build = await loadJs(configDir, 'crossbind.build');
@@ -126,8 +128,6 @@ export function getFilledConfig(config, options = { isDepend: false }) {
126
128
  const newConfig = {
127
129
  general: config.general || {},
128
130
  dependencies,
129
- cargoDependencies: config.cargoDependencies || {},
130
- conanDependencies: normalizeConanDependencies(config.conanDependencies),
131
131
  // 'sync' types the direct surface; 'promise' wraps every generated method return
132
132
  // for useWorker-style runtimes (constructors stay sync-typed: await new X()).
133
133
  dts: config.dts || 'sync',
@@ -214,6 +214,12 @@ export function getFilledConfig(config, options = { isDepend: false }) {
214
214
  const twice = newConfig.extensions.find(({ name }, i) => newConfig.extensions.findIndex((other) => other.name === name) !== i);
215
215
  if (twice) throw new Error(`crossbind: "extensions" lists two extensions named ${twice.name}.`);
216
216
 
217
+ // What the project declares under packages: cargo's crates, and each package source's packages, as packages.conan,
218
+ // as the source's own check takes them. A package installs nothing for the project that depends on it, so a
219
+ // dependency's own declarations are not read.
220
+ newConfig.packageSources = packageSourcesOf(newConfig.extensions);
221
+ newConfig.packages = checkedPackages(newConfig.packageSources, options.isDepend ? {} : config, options.file);
222
+
217
223
  if (newConfig.target.runtime !== 'mt' && newConfig.allDependencies.some((d) => d?.target?.runtime === 'mt')) {
218
224
  newConfig.target.runtime = 'mt';
219
225
  }
@@ -0,0 +1,19 @@
1
+ import attachPackageSources from './attachPackageSources.js';
2
+ import { sourceStageDir } from '../utils/packageSourceImport.js';
3
+ import { declaredSources } from '../utils/packageSources.js';
4
+ import { sourceManifestsStamp } from '../utils/packageSourceStage.js';
5
+
6
+ const attachedFrom = new WeakMap();
7
+
8
+ // Attaches what a build staged since this config last attached: a process that loaded state before
9
+ // the build, such as a Metro server, would otherwise bind headers without the packages. Says whether
10
+ // it attached again.
11
+ export default function refreshPackageSources(config) {
12
+ const declared = declaredSources(config);
13
+ if (declared.length === 0) return false;
14
+ const stamp = declared.map(({ name }) => `${name}:${sourceManifestsStamp(sourceStageDir(config.paths.cache, name))}`).join('\n');
15
+ if (attachedFrom.get(config) === stamp) return false;
16
+ attachedFrom.set(config, stamp);
17
+ attachPackageSources(config);
18
+ return true;
19
+ }