crossbind 2.0.0-beta.69 → 2.0.0-beta.73

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 (55) hide show
  1. package/package.json +3 -2
  2. package/src/actions/addExtension.js +8 -4
  3. package/src/actions/buildApp.js +38 -9
  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/extensionCommands.js +2 -1
  9. package/src/actions/getAllBridges.js +20 -5
  10. package/src/actions/getDependLibs.js +6 -0
  11. package/src/actions/getLinkInputs.js +2 -1
  12. package/src/actions/gradleProject.js +13 -2
  13. package/src/actions/licenses.js +30 -24
  14. package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
  15. package/src/actions/writeRuntimeEntry.js +2 -2
  16. package/src/actions/xcodeProject.js +12 -6
  17. package/src/app/index.d.ts +19 -0
  18. package/src/app/index.js +32 -7
  19. package/src/assets/node-import-hooks.mjs +7 -6
  20. package/src/bin.js +37 -17
  21. package/src/index.js +4 -3
  22. package/src/integration/getCrossbindScript.js +3 -2
  23. package/src/integration/getDependFilePath.js +10 -10
  24. package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
  25. package/src/state/index.js +14 -11
  26. package/src/state/loadConfig.js +10 -4
  27. package/src/state/refreshPackageSources.js +19 -0
  28. package/src/utils/appApiHeader.js +5 -5
  29. package/src/utils/appBuild.js +8 -0
  30. package/src/utils/appExtensions.js +44 -28
  31. package/src/utils/appOptions.js +31 -10
  32. package/src/utils/appProject.js +24 -0
  33. package/src/utils/appRustCrates.js +2 -2
  34. package/src/utils/bridgeTools.js +6 -1
  35. package/src/utils/cppDts.js +11 -11
  36. package/src/utils/extensionSteps.js +38 -1
  37. package/src/utils/nativeImports.js +9 -2
  38. package/src/utils/nodeDevBuild.js +2 -1
  39. package/src/utils/packageSourceApi.js +87 -0
  40. package/src/utils/packageSourceImport.js +69 -0
  41. package/src/utils/packageSourceInstall.js +162 -0
  42. package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
  43. package/src/utils/packageSources.js +108 -0
  44. package/src/utils/printableText.js +5 -0
  45. package/src/utils/resolveAppHost.js +10 -0
  46. package/src/utils/runPackageSource.js +251 -0
  47. package/src/utils/rustBridgeGen.js +3 -3
  48. package/src/utils/targetToolchain.js +77 -0
  49. package/src/utils/xcframeworkSlice.js +6 -0
  50. package/src/state/refreshConanDependencies.js +0 -17
  51. package/src/utils/conanDependencies.js +0 -66
  52. package/src/utils/conanImport.js +0 -58
  53. package/src/utils/conanInstall.js +0 -154
  54. package/src/utils/conanProfile.js +0 -203
  55. package/src/utils/runConan.js +0 -257
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crossbind",
3
- "version": "2.0.0-beta.69",
3
+ "version": "2.0.0-beta.73",
4
4
  "license": "MIT",
5
5
  "homepage": "https://crossbind.dev",
6
6
  "repository": {
@@ -34,6 +34,7 @@
34
34
  "vitest": "^5.0.1"
35
35
  },
36
36
  "dependencies": {
37
+ "@crossbind/extension-conan": "2.0.0-beta.72",
37
38
  "@rollup/plugin-commonjs": "^29.0.3",
38
39
  "@rollup/plugin-json": "^6.1.0",
39
40
  "@rollup/plugin-node-resolve": "^16.0.3",
@@ -50,7 +51,7 @@
50
51
  "upath": "^3.0.8"
51
52
  },
52
53
  "optionalDependencies": {
53
- "@crossbind/bridge-tools": "2.0.0-beta.69"
54
+ "@crossbind/bridge-tools": "2.0.0-beta.73"
54
55
  },
55
56
  "keywords": [
56
57
  "webassembly",
@@ -4,6 +4,8 @@ import path from 'node:path';
4
4
  import { configFileOf, writeExtension } from '../utils/configWriter.js';
5
5
  import { isUnderBase } from '../utils/replaceBasePathForDocker.js';
6
6
  import { ownPackages } from '../utils/appExtensions.js';
7
+ import { packagesOfCrossbind } from '../utils/packageSources.js';
8
+ import { assertAppProject } from '../utils/appProject.js';
7
9
  import resolveAppHostRoot from '../utils/resolveAppHost.js';
8
10
 
9
11
  // The lockfiles by which a project names the package manager it is installed with.
@@ -108,9 +110,13 @@ function assertSpec(spec, platform) {
108
110
  }
109
111
  }
110
112
 
111
- // An extension an app has by having the host is none to add. A project without a host yet has none of them.
113
+ // An extension a project has by having crossbind, or an app by having the host, is none to add. A project without a
114
+ // host yet has none of the host's.
112
115
  function assertNotOwn(spec) {
113
116
  const name = registryName(spec);
117
+ if (packagesOfCrossbind().includes(name)) {
118
+ throw new Error(`crossbind: every project has ${name} by having crossbind, which brings it, so there is nothing to add.`);
119
+ }
114
120
  let root;
115
121
  try {
116
122
  root = resolveAppHostRoot();
@@ -139,9 +145,7 @@ function installedName(spec, before, after, manifest) {
139
145
  export default function addExtension(spec, {
140
146
  config, env = process.env, platform = process.platform, spawn = spawnSync,
141
147
  }) {
142
- if (config.target.runtimeEnv !== 'app') {
143
- throw new Error("crossbind: an extension builds into an app, which this project is not: set target: { runtimeEnv: 'app' } in crossbind.config.js, as the desktop-app template does.");
144
- }
148
+ assertAppProject(config, 'an extension builds into an app');
145
149
  assertSpec(spec, platform);
146
150
  assertNotOwn(spec);
147
151
  const { project, base } = config.paths;
@@ -29,8 +29,8 @@ import findFiles from '../utils/findFiles.js';
29
29
  import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
30
30
  import { xmlPlist } from '../utils/plist.js';
31
31
  import {
32
- androidAppId, appBackgroundColor, appCategory, appCopyright, appDisplayName, appEncryption, appFiles, appHeader, appId, appPermissions, appPrivacy,
33
- appUrlSchemes, iosAppGroup, iosMultipleWindows, urlSchemeOwners, usageDescriptions,
32
+ androidAppId, appBackgroundColor, appCategory, appCopyright, appDarkBackgroundColor, appDisplayName, appEncryption, appFiles, appHeader, appId,
33
+ appPermissions, appPrivacy, appUrlSchemes, iosAppGroup, iosMultipleWindows, urlSchemeOwners, usageDescriptions,
34
34
  } from '../utils/appOptions.js';
35
35
  import { carriedFilesOf, copyCarriedFiles } from '../utils/appFiles.js';
36
36
  import { activityTypeOwners, fileTypeOwners, linkOwners, macDocumentTypes, shortcutItemOwners } from '../utils/appLaunches.js';
@@ -381,6 +381,10 @@ const shippedFiles = () => new Set(Object.values(readAppOutputs()).filter(Array.
381
381
  // The command line's names of the desktop platforms.
382
382
  export const DESKTOP_PLATFORMS = { darwin: 'macos', linux: 'linux', win32: 'windows' };
383
383
 
384
+ // The platform an app is built for, which its page reads as import.meta.env.CROSSBIND_APP_PLATFORM: CROSSBIND_APP_PLATFORM,
385
+ // else this machine's desktop.
386
+ export const appPlatform = (env = process.env) => env.CROSSBIND_APP_PLATFORM || DESKTOP_PLATFORMS[process.platform];
387
+
384
388
  // Forgets what the builds wrote, as a publish's own build writes it anew, but for the installers' files, which a folder the
385
389
  // bundler kept holds still, for the page to leave out.
386
390
  export function clearAppOutputs() {
@@ -389,6 +393,33 @@ export function clearAppOutputs() {
389
393
  else fs.rmSync(appOutputsFile(), { force: true });
390
394
  }
391
395
 
396
+ // The app target an entry of the folder the bundler builds into was written for, by its name: the target's app, or what
397
+ // packages it.
398
+ function outputTarget(entry) {
399
+ const { name } = state.config.general;
400
+ return state.targets.filter((t) => t.runtimeEnv === 'app').find((t) => [appOutputName(t), `${name}-${t.platform}-${t.arch}`]
401
+ .some((app) => entry === app || entry.startsWith(`${app}.`) || entry.startsWith(`${app}-`)));
402
+ }
403
+
404
+ // The paths a recorded output names: each desktop app with its installers' files, or a mobile app's project with its APK,
405
+ // bundle and debug symbols.
406
+ const outputPaths = (output) => (Array.isArray(output)
407
+ ? output.flatMap(({ app, installers = [] }) => [app, ...installers.flatMap(({ files }) => files)])
408
+ : Object.values(output)).filter(Boolean);
409
+
410
+ // Empties the folder the bundler builds an app into, as the bundler would, but for git's own folder and what builds for the
411
+ // app's other platforms wrote there: their apps, named after their targets, and the files their installers recorded.
412
+ export function emptyAppOutDir(dir, platform = appPlatform()) {
413
+ if (!fs.existsSync(dir)) return;
414
+ const others = new Set(Object.entries(readAppOutputs()).filter(([name]) => name !== platform)
415
+ .flatMap(([, output]) => outputPaths(output)).map((file) => path.resolve(file)));
416
+ for (const entry of fs.readdirSync(dir)) {
417
+ const target = outputTarget(entry);
418
+ const isOthers = target ? (DESKTOP_PLATFORMS[target.platform] ?? target.platform) !== platform : others.has(path.resolve(dir, entry));
419
+ if (entry !== '.git' && !isOthers) fs.rmSync(path.join(dir, entry), { recursive: true, force: true });
420
+ }
421
+ }
422
+
392
423
  // What a publisher's upload gets of the app the build wrote for a platform: a desktop platform's apps, one for each arch.
393
424
  export function appOutputOf(platform) {
394
425
  const output = readAppOutputs()[platform];
@@ -441,8 +472,8 @@ export function publishApp(target, { to = appOutputPath(target), page = null } =
441
472
  build: build === null ? appVersion() : String(build), icon: icon && iosIcon(icon), urlSchemes: appUrlSchemes(state.config),
442
473
  fileTypes: fileTypeOwners(state.config), links: linkOwners(state.config).map(({ link }) => link),
443
474
  activityTypes: activityTypeOwners(state.config).map(({ type }) => type), shortcutItems: shortcutItemOwners(state.config),
444
- background: appBackgroundColor(state.config), usage: usageDescriptions(state.config, 'ios'), encryption: appEncryption(state.config),
445
- multipleWindows: iosMultipleWindows(state.config),
475
+ background: appBackgroundColor(state.config), darkBackground: appDarkBackgroundColor(state.config), usage: usageDescriptions(state.config, 'ios'),
476
+ encryption: appEncryption(state.config), multipleWindows: iosMultipleWindows(state.config),
446
477
  };
447
478
  const extensions = extensionsOf('ios');
448
479
  const privacy = mergedPrivacy(ownPrivacy, extensions.privacy);
@@ -455,7 +486,8 @@ export function publishApp(target, { to = appOutputPath(target), page = null } =
455
486
  const icon = readAppIcon(state.config);
456
487
  const options = {
457
488
  parts: appParts(target), id: androidAppId(state.config), label: appDisplayName(state.config), version: androidVersion(), icons: icon && androidIcons(icon),
458
- urlSchemes: appUrlSchemes(state.config), background: appBackgroundColor(state.config), permissions: Object.keys(appPermissions(state.config)),
489
+ urlSchemes: appUrlSchemes(state.config), background: appBackgroundColor(state.config), darkBackground: appDarkBackgroundColor(state.config),
490
+ permissions: Object.keys(appPermissions(state.config)),
459
491
  fileTypes: fileTypeOwners(state.config), links: linkOwners(state.config).map(({ link }) => link), shortcutItems: shortcutItemOwners(state.config),
460
492
  };
461
493
  const extensions = extensionsOf('android');
@@ -470,11 +502,8 @@ export function publishApp(target, { to = appOutputPath(target), page = null } =
470
502
  // The app may land inside the page's own directory, which it then leaves out, with every app built there and what
471
503
  // packages it: what is named after a target of the app, and every installer's files as a build recorded them, which
472
504
  // a folder the bundler left as it was holds still.
473
- const apps = state.targets.filter((t) => t.runtimeEnv === 'app')
474
- .flatMap((t) => [appOutputName(t), `${state.config.general.name}-${t.platform}-${t.arch}`]).filter((name) => !name.endsWith('.app'));
475
505
  const shipped = shippedFiles();
476
- const isApp = (entry) => entry === path.basename(to) || entry.endsWith('.app')
477
- || apps.some((app) => entry === app || entry.startsWith(`${app}.`) || entry.startsWith(`${app}-`))
506
+ const isApp = (entry) => entry === path.basename(to) || entry.endsWith('.app') || Boolean(outputTarget(entry))
478
507
  || shipped.has(path.resolve(page, entry));
479
508
  fs.readdirSync(page).filter((entry) => !isApp(entry)).forEach((entry) => {
480
509
  fs.cpSync(`${page}/${entry}`, `${to}/${appLayout(target).resources}/page/${entry}`, { recursive: true });
@@ -14,7 +14,7 @@ import {
14
14
  } from '../utils/dependencyRebuild.js';
15
15
  import withDirLock from '../utils/dirLock.js';
16
16
  import { prepareRustSysroot } from '../utils/rustSysroot.js';
17
- import prepareConanDependencies from './prepareConanDependencies.js';
17
+ import preparePackageSources from './preparePackageSources.js';
18
18
  import getAbsolutePath from '../utils/getAbsolutePath.js';
19
19
  import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
20
20
  import getParentPath from '../utils/getParentPath.js';
@@ -26,7 +26,7 @@ export default async function buildDependencies({ targetParams, rebuildOption })
26
26
  // sync cargo work downstream reads what it resolves. A project with nothing to rebuild still
27
27
  // needs its sysroot.
28
28
  await prepareRustSysroot(getBuildTargets(targetParams));
29
- await prepareConanDependencies(getBuildTargets(targetParams));
29
+ await preparePackageSources(getBuildTargets(targetParams));
30
30
  await buildMissingCargoDependencies(getBuildTargets(targetParams), targetParams);
31
31
 
32
32
  const rebuildDeps = getRebuildDeps(state.config.allDependencies, rebuildOption);
@@ -78,7 +78,7 @@ async function buildMissingCargoDependencies(buildTargets, targetParams) {
78
78
  const appConfig = state.config;
79
79
  const cargoDependencies = (appConfig.allDependencies ?? []).filter((dep) => dep !== appConfig && dep.export?.type === 'cargo');
80
80
  if (cargoDependencies.length === 0) return;
81
- // Bridges read every header for the first wasm target (see prepareConanDependencies), which an app configured for
81
+ // Bridges read every header for the first wasm target (see preparePackageSources), which an app configured for
82
82
  // another wasm runtime does not build: there a dependency built for the app's runtime alone counts as half built.
83
83
  const bridgeTarget = buildTargets.some(isWasm) ? state.targets.find(isWasm) : undefined;
84
84
  const extraTarget = bridgeTarget && !buildTargets.some((target) => target.path === bridgeTarget.path) ? bridgeTarget : undefined;
@@ -5,9 +5,9 @@ import createBridgeFile from './createInterface.js';
5
5
  import getDependFilePath from '../integration/getDependFilePath.js';
6
6
  import resolveNativeImport from '../integration/resolveNativeImport.js';
7
7
  import { getRustSymbols } from '../integration/getCrossbindScript.js';
8
- import { findNativeImportsIn, nativeSpecifierTest } from '../utils/nativeImports.js';
8
+ import { findNativeImportsIn, nativeSpecifierTest, schemeTest } from '../utils/nativeImports.js';
9
+ import { importSchemesOf } from '../utils/packageSources.js';
9
10
 
10
- const SCHEME = /^(cargo|conan):/;
11
11
  const isBare = (specifier) => !specifier.startsWith('.') && !upath.isAbsolute(specifier);
12
12
 
13
13
  function realPath(file) {
@@ -18,13 +18,13 @@ function realPath(file) {
18
18
  }
19
19
  }
20
20
 
21
- // The first target whose packages ship the file. A cargo: or conan: import that none resolves fails the build with
22
- // what to declare or install, as it does in a bundler.
23
- function resolveImport(specifier, importer, targets) {
21
+ // The first target whose packages ship the file. An import by a scheme, as cargo: and conan: are, that none resolves
22
+ // fails the build with what to declare or install, as it does in a bundler.
23
+ function resolveImport(specifier, importer, targets, hasScheme) {
24
24
  let failure = null;
25
25
  for (const target of targets) {
26
26
  try {
27
- const file = SCHEME.test(specifier) ? getDependFilePath(specifier, target) : resolveNativeImport(specifier, importer, target);
27
+ const file = hasScheme(specifier) ? getDependFilePath(specifier, target) : resolveNativeImport(specifier, importer, target);
28
28
  if (file && fs.existsSync(file)) return { target, file };
29
29
  } catch (error) {
30
30
  failure ??= error;
@@ -47,8 +47,10 @@ export function findAppNativeImports(targets) {
47
47
  const projectDir = state.config.paths.project;
48
48
  const imports = new Map();
49
49
  if (!projectDir || !fs.existsSync(projectDir)) return imports;
50
- for (const { importer, specifier } of findNativeImportsIn(projectDir, nativeSpecifierTest(state.config.ext))) {
51
- const resolved = resolveImport(specifier, importer, targets);
50
+ const schemes = importSchemesOf(state.config);
51
+ const hasScheme = schemeTest(schemes);
52
+ for (const { importer, specifier } of findNativeImportsIn(projectDir, nativeSpecifierTest(state.config.ext, schemes))) {
53
+ const resolved = resolveImport(specifier, importer, targets, hasScheme);
52
54
  if (!resolved) {
53
55
  console.warn(`crossbind: ${upath.relative(projectDir, importer)} imports ${specifier}, which no dependency ships for ${targets.map((t) => t.path).join(', ')}.`);
54
56
  continue;
@@ -2,9 +2,9 @@
2
2
  import fs from 'node:fs';
3
3
  import upath from 'upath';
4
4
  import state, { saveCache } from '../state/index.js';
5
- import refreshConanDependencies from '../state/refreshConanDependencies.js';
5
+ import refreshPackageSources from '../state/refreshPackageSources.js';
6
6
  import refreshBuiltDependencies from '../state/refreshBuiltDependencies.js';
7
- import { conanInputsOf } from '../utils/conanDependencies.js';
7
+ import { packageSourceInputsOf, sourceNamesOf } from '../utils/packageSources.js';
8
8
  import { getContentHash, getFileHash } from '../utils/hash.js';
9
9
  import guardAsyncBindings from '../utils/bridgeAsyncGuard.js';
10
10
  import fixBridgeRuntime from '../utils/bridgeRuntimeFixes.js';
@@ -12,7 +12,7 @@ import {
12
12
  interfaceExtras, bridgeExtras, hasPointerRuntime, withPointerRuntimeAnchor, withoutBridgeExtras, POINTER_RUNTIME_ANCHOR,
13
13
  } from '../utils/bridgeExtras.js';
14
14
  import resolveNativeImport from '../integration/resolveNativeImport.js';
15
- import { writeHeaderDts, writeConanImportDts } from '../utils/cppDts.js';
15
+ import { writeHeaderDts, writeSourceImportDts } from '../utils/cppDts.js';
16
16
  import { ALL_NAMES, findHeaderImportsIn } from '../utils/headerImports.js';
17
17
  import {
18
18
  buildInterfaceContent, completingIncludes, findHeaderPrelude, findIgnoredDeclarations, findSwigInlineIncludes, findSwigPreamble,
@@ -21,7 +21,7 @@ import {
21
21
  } from '../utils/swigInterface.js';
22
22
  import writeIfChanged from '../utils/writeIfChanged.js';
23
23
  import { withDirLockSync } from '../utils/dirLock.js';
24
- import { conanImportOfHeader } from '../utils/conanImport.js';
24
+ import { sourceImportOfHeader } from '../utils/packageSourceImport.js';
25
25
  import run, { cxxPreprocessorFor } from './run.js';
26
26
  import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
27
27
  import {
@@ -53,9 +53,9 @@ export default function createBridgeFile(headerOrModuleFilePath, target = state.
53
53
  }
54
54
 
55
55
  function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, wholeHeaders = [] } = {}) {
56
- // A header can include a Conan package's headers that a build staged after this process loaded, or belong to a
57
- // dependency built since.
58
- refreshConanDependencies(state.config);
56
+ // A header can include the headers of a package source's package that a build staged after this process loaded,
57
+ // or belong to a dependency built since.
58
+ refreshPackageSources(state.config);
59
59
  refreshBuiltDependencies(state.config);
60
60
  // Without bridge-tools on a Mac the macro step would fail quietly, and its interface would be kept without macros.
61
61
  if (usesBridgeTools(target)) loadBridgeTools();
@@ -76,7 +76,7 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
76
76
  const { headerPath } = includeLocation(interfaceFilePath, target);
77
77
  const inlined = findSwigInlineIncludes(interfaceFilePath, [state.config, ...state.config.allDependencies], headerPath);
78
78
  const swigView = inlined.length ? writeSwigView(interfaceFilePath, headerPath, inlined) : null;
79
- const sourceHash = [getFileHash(interfaceFilePath), ...conanInputsHash()].join('\n');
79
+ const sourceHash = [getFileHash(interfaceFilePath), ...packageInputsHash()].join('\n');
80
80
  // The names imported by name decide what the bridge adds to SWIG's output, so another one makes another bridge.
81
81
  const bridgeSourceHash = names.named.length ? `${sourceHash}\nnamed:${names.named.join(',')}` : sourceHash;
82
82
  const extras = { named: names.named, headerText: () => fs.readFileSync(swigView ? upath.join(swigView.dir, headerPath) : interfaceFilePath, 'utf8') };
@@ -91,13 +91,14 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
91
91
  exportsFile: `${bridgeFile}.exports.json`,
92
92
  projectPath: state.config.paths.project,
93
93
  cacheDir: state.config.paths.cache,
94
+ sources: sourceNamesOf(state.config),
94
95
  dtsMode: state.config.dts,
95
96
  };
96
97
  writeHeaderDts(dtsOptions);
97
- if (conanImportOfHeader(interfaceFilePath, state.config.paths.cache)) {
98
- dtsOptions.declarationsFile = createConanDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView);
98
+ if (sourceImportOfHeader(interfaceFilePath, dtsOptions.cacheDir, dtsOptions.sources)) {
99
+ dtsOptions.declarationsFile = createSourceDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView);
99
100
  }
100
- writeConanImportDts(dtsOptions);
101
+ writeSourceImportDts(dtsOptions);
101
102
  }
102
103
  if (bridgeFile && withDependencies && !moduleRegex.test(interfaceFilePath)) {
103
104
  // A header's own bindings need the types its declarations use registered, which only their headers' bridges do.
@@ -115,11 +116,11 @@ function bindHeader(headerOrModuleFilePath, target, { withDependencies = true, w
115
116
  return bridgeFile;
116
117
  }
117
118
 
118
- // The Conan packages a header's includes may reach, for the interface and bridge caches: they are
119
- // staged after a header may already be bound, and restaged in place for another version.
120
- function conanInputsHash() {
121
- const conan = conanInputsOf(state.config);
122
- return conan ? [`conan:${getContentHash(JSON.stringify(conan))}`] : [];
119
+ // The packages of package sources a header's includes may reach, for the interface and bridge caches: they
120
+ // are staged after a header may already be bound, and restaged in place for another version.
121
+ function packageInputsHash() {
122
+ const inputs = packageSourceInputsOf(state.config);
123
+ return inputs ? [`packages:${getContentHash(JSON.stringify(inputs))}`] : [];
123
124
  }
124
125
 
125
126
  // A header binds the constants the app imports from it, and so the free functions of a dependency's header: the project's
@@ -159,17 +160,18 @@ function createInterfaceFile(headerOrModuleFilePath, target, sourceDir, { consta
159
160
  const filePathWithoutExt = headerOrModuleFilePath.match(/^(.*)\..+?$/)?.[1];
160
161
  const interfaceFile = !isModule && filePathWithoutExt ? `${filePathWithoutExt}.i` : null;
161
162
  // A prelude or ignored-declaration change in the owning package, a completed class moving to another header, an
162
- // interface the package ships beside the header changing or going away, another imported name, or Conan packages
163
- // its includes may reach being staged or moving to another version must regenerate the interface as well.
163
+ // interface the package ships beside the header changing or going away, another imported name, packages of a
164
+ // package source its includes may reach being staged or moving to another version, or the header included by
165
+ // another path, as another target finds it, must regenerate the interface as well.
164
166
  const fileHash = [
165
- INTERFACE_FORMAT, getFileHash(headerOrModuleFilePath), ...prelude,
167
+ INTERFACE_FORMAT, getFileHash(headerOrModuleFilePath), ...(headerPath ? [`include:${headerPath}`] : []), ...prelude,
166
168
  ...(ignored.length ? [`ignored:${ignored.join(',')}`] : []), ...(completing.length ? [`completing:${completing.join(',')}`] : []),
167
169
  ...(preamble.length ? [`preamble:${preamble.join('\n')}`] : []),
168
170
  ...(interfaceFile && fs.existsSync(interfaceFile) ? [`shipped:${getFileHash(interfaceFile)}`] : []),
169
171
  ...(constants.length ? [`constants:${constants === ALL_NAMES ? ALL_NAMES : constants.join(',')}`] : []),
170
172
  ...(functions === ALL_NAMES ? [] : [`functions:${functions.join(',')}`]),
171
173
  ...(named.length ? [`named:${named.join(',')}`] : []),
172
- ...conanInputsHash(),
174
+ ...packageInputsHash(),
173
175
  ].join('\n');
174
176
  const cachedInterface = state.cache.interfaces[headerOrModuleFilePath];
175
177
  if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)
@@ -517,7 +519,7 @@ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir
517
519
  }
518
520
 
519
521
  // Generate a full export catalog for the editor, without compiling or linking these extra functions.
520
- function createConanDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView) {
522
+ function createSourceDeclarations(interfaceFile, target, sourceDir, sourceHash, swigView) {
521
523
  const dir = `${state.config.paths.build}/declarations`;
522
524
  fs.mkdirSync(dir, { recursive: true });
523
525
  const file = `${dir}/${upath.basename(interfaceFile)}`;
@@ -16,7 +16,7 @@ import {
16
16
  import { getFilesFingerprint, getContentHash } from '../utils/hash.js';
17
17
  import { withDependencyBridges } from '../utils/dependencyBridges.js';
18
18
  import { targetArchiveFlags } from '../utils/archiveFlags.js';
19
- import { conanInputsOf } from '../utils/conanDependencies.js';
19
+ import { packageSourceInputsOf } from '../utils/packageSources.js';
20
20
  import { nativeSourceStampChanged, writeNativeSourceStamp } from '../utils/nativeSourceStamp.js';
21
21
  import resetCMakeCacheForSource from '../utils/resetCMakeCacheForSource.js';
22
22
 
@@ -57,10 +57,10 @@ export default function createLib(target, fileType, options = {}) {
57
57
  const configEmccFlags = getData('binary', target)?.emccFlags || [];
58
58
  const compileOptions = getData('cmake', target)?.compileOptions || [];
59
59
  const flagsFingerprintFile = `${libdir}/crossbind-emccflags.fingerprint`;
60
- const conanInputs = conanInputsOf(state.config);
60
+ const packageInputs = packageSourceInputsOf(state.config);
61
61
  const compileInputs = compileOptions.length ? { emccFlags: configEmccFlags, compileOptions } : configEmccFlags;
62
62
  const flagsInputs = sourceReplacements.length ? { compileInputs, sourceReplacements } : compileInputs;
63
- const keyInputs = conanInputs ? [flagsInputs, conanInputs] : flagsInputs;
63
+ const keyInputs = packageInputs ? [flagsInputs, packageInputs] : flagsInputs;
64
64
  // An app's wasi source compiles against <crossbind/http.h>, so a new one misses this cache too.
65
65
  const httpHeader = target.platform === 'wasi' && !state.config.build.withBuildConfig
66
66
  ? getFilesFingerprint([wasiHttpHeader(state.config.paths.cli)])
@@ -3,6 +3,7 @@ import extensionApi from './extensionApi.js';
3
3
  import state from '../state/index.js';
4
4
  import { hasTerminal } from '../utils/ask.js';
5
5
  import { extensionStep, ownExtensions, withoutNamed } from '../utils/appExtensions.js';
6
+ import { isAppProject } from '../utils/appProject.js';
6
7
  import resolveAppHostRoot from '../utils/resolveAppHost.js';
7
8
 
8
9
  const kebab = (name) => name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
@@ -23,7 +24,7 @@ function ownOf(listed, app) {
23
24
  // Adds to crossbind app the commands of an app's extensions, crossbind's own and those crossbind.config.js lists: a name
24
25
  // another has stops the run, naming both.
25
26
  export default function addExtensionCommands(parent, config = state.config) {
26
- if (config?.target?.runtimeEnv !== 'app') return;
27
+ if (!config?.target || !isAppProject(config)) return;
27
28
  const listed = config.extensions ?? [];
28
29
  const owners = new Map(parent.commands.map((command) => [command.name(), 'crossbind']));
29
30
  for (const { name: extension, commands = {} } of [...ownOf(listed, config.app), ...listed]) {
@@ -2,19 +2,34 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import findFiles from '../utils/findFiles.js';
4
4
  import state from '../state/index.js';
5
+ import { declarationOf, dependenciesOf, sourceImportOfHeader } from '../utils/packageSourceImport.js';
6
+
7
+ // Why a bridge's header can no longer be included, or null. A package source's stage outlives the declaration, and
8
+ // the source itself, so the header of a package the project no longer declares is still there while its include
9
+ // directory is off the compile. The stage is named by the path, as the project may have dropped the source too.
10
+ function staleReasonOf(header) {
11
+ if (!fs.existsSync(header)) return 'its header is gone';
12
+ const { config } = state;
13
+ const stages = `${config.paths.cache}/`;
14
+ const [source] = header.startsWith(stages) ? header.slice(stages.length).split('/') : [];
15
+ const staged = source ? sourceImportOfHeader(header, config.paths.cache, [source]) : null;
16
+ if (!staged || Object.hasOwn(dependenciesOf(config, source), staged.name)) return null;
17
+ return `${declarationOf(source)} no longer declares ${staged.name}`;
18
+ }
5
19
 
6
20
  export default function getAllBridges(log = console.log) {
7
21
  return findFiles(`${state.config.paths.build}/bridge/*.i.cpp`).filter((bridge) => {
8
22
  // The .source sidecar records the header this bridge was generated from. When that
9
- // header is gone, the stale .i.cpp would #include a missing file and break the native
10
- // build - the same failure a deleted .rs used to cause. Bridges without a sidecar
11
- // predate it and are left alone.
23
+ // header can no longer be included, the stale .i.cpp would #include a missing file and
24
+ // break the native build - the same failure a deleted .rs used to cause. Bridges without
25
+ // a sidecar predate it and are left alone.
12
26
  const sidecar = `${bridge}.source`;
13
27
  if (!fs.existsSync(sidecar)) return true;
14
28
  const source = fs.readFileSync(sidecar, 'utf8').trim();
15
- if (!source || fs.existsSync(source)) return true;
29
+ const reason = source ? staleReasonOf(source) : null;
30
+ if (!reason) return true;
16
31
  [bridge, `${bridge}.exports.json`, `${bridge}.deps`, `${bridge}.warnings`, sidecar].forEach((f) => fs.rmSync(f, { force: true }));
17
- log(`crossbind: pruned stale bridge '${path.basename(bridge)}' - its header is gone`);
32
+ log(`crossbind: pruned stale bridge '${path.basename(bridge)}' - ${reason}`);
18
33
  return false;
19
34
  });
20
35
  }
@@ -1,4 +1,5 @@
1
1
  import findFiles from '../utils/findFiles.js';
2
+ import xcframeworkSliceOf from '../utils/xcframeworkSlice.js';
2
3
  import state from '../state/index.js';
3
4
  import { getFilteredTargetSpec } from './target.js';
4
5
 
@@ -21,6 +22,11 @@ export default function getDependLibs(target) {
21
22
  if (libPaths.length === 0) {
22
23
  libPaths = libsIn(target.releasePath);
23
24
  }
25
+ // A published port can leave its iOS archive to its xcframework: the one of this package, as a project
26
+ // can list the port's packages of other platforms, whose libraries share the name.
27
+ if (libPaths.length === 0 && target.platform === 'ios') {
28
+ libPaths = findFiles(`${xcframeworkSliceOf(d.paths.project, fileName, target)}/lib${fileName}.a`, { cwd: d.paths.project });
29
+ }
24
30
  dependLibs.push(...libPaths);
25
31
  });
26
32
  }
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import getDependLibs from './getDependLibs.js';
3
3
  import state from '../state/index.js';
4
4
  import buildAppRustCrates from '../utils/appRustCrates.js';
5
+ import { cratesOf } from '../utils/packageSourceImport.js';
5
6
 
6
7
  // The archives of a final link (wasm, Node-API addon, native executable or app host) and which of them must
7
8
  // survive whole. keepFlag spells "keep this symbol" for the target's linker; a native executable
@@ -22,7 +23,7 @@ export default function getLinkInputs(target, { keepFlag, withBridge = true, bri
22
23
  const bridgeLibCandidates = builtLib(bridge);
23
24
  // App-local Rust surfaces (imported .rs files) arrive as ONE super staticlib; it is the
24
25
  // single fully-loaded Rust archive of the link (see the libstd rule below).
25
- const appRustLibs = buildAppRustCrates(target, cache, state.config.cargoDependencies ?? {});
26
+ const appRustLibs = buildAppRustCrates(target, cache, cratesOf(state.config));
26
27
  const libs = [
27
28
  ...getDependLibs(target),
28
29
  ...appRustLibs,
@@ -36,6 +36,13 @@ const backgroundValues = (color) => `<?xml version="1.0" encoding="utf-8"?>
36
36
  </resources>
37
37
  `;
38
38
 
39
+ // The color while the system is dark, where app.window.backgroundColor gives one, which Android takes in place of the other.
40
+ const darkBackgroundValues = (color) => `<?xml version="1.0" encoding="utf-8"?>
41
+ <resources>
42
+ <color name="crossbind_background">${color}</color>
43
+ </resources>
44
+ `;
45
+
39
46
  // The devices of app.permissions, which alone the page may ask for, whatever an extension's Java asks Android for.
40
47
  const permissionValues = (permissions) => `<?xml version="1.0" encoding="utf-8"?>
41
48
  <resources>
@@ -269,8 +276,8 @@ ${libraries.map((library) => ` implementation '${library}'\n`).join('')}}
269
276
  // is an extension's folder of Java sources, by package, which the app's sources take beside the activity, and the
270
277
  // class in it the activity calls.
271
278
  export default function writeGradleProject(to, {
272
- parts, id, label, version, icons = null, urlSchemes = [], fileTypes = [], links = [], shortcutItems = [], background = null, permissions = [],
273
- java = [], extensionPermissions = [], manifests = [], resources = [], maven = [],
279
+ parts, id, label, version, icons = null, urlSchemes = [], fileTypes = [], links = [], shortcutItems = [], background = null, darkBackground = null,
280
+ permissions = [], java = [], extensionPermissions = [], manifests = [], resources = [], maven = [],
274
281
  }) {
275
282
  const { name } = state.config.general;
276
283
  if (parts.length === 0) throw new Error(`crossbind: no part of the Android app ${name} is built, so there is no Gradle project to write.`);
@@ -328,6 +335,10 @@ export default function writeGradleProject(to, {
328
335
  }
329
336
  fs.mkdirSync(path.join(main, 'res', 'values'), { recursive: true });
330
337
  fs.writeFileSync(path.join(main, 'res', 'values', 'background.xml'), backgroundValues(background));
338
+ if (background && darkBackground) {
339
+ fs.mkdirSync(path.join(main, 'res', 'values-night'), { recursive: true });
340
+ fs.writeFileSync(path.join(main, 'res', 'values-night', 'background.xml'), darkBackgroundValues(darkBackground));
341
+ }
331
342
  fs.writeFileSync(path.join(main, 'res', 'values', 'permissions.xml'), permissionValues(permissions));
332
343
  if (shortcutItems.length > 0) {
333
344
  fs.mkdirSync(path.join(main, 'res', 'xml'), { recursive: true });
@@ -17,6 +17,7 @@ import { DESKTOP_PLATFORMS } from './buildApp.js';
17
17
  import extensionsInApp from './extensionsInApp.js';
18
18
  import { ANDROIDX_WEBKIT } from './gradleProject.js';
19
19
  import { extensionPackage, extensionPart } from '../utils/appExtensions.js';
20
+ import { declaredSources, declarationOf, dependenciesOf } from '../utils/packageSources.js';
20
21
  import { shippedBundledLicense } from '../utils/bundledLicenses.js';
21
22
  import toolchainNoticesDir from '../utils/toolchainNotices.js';
22
23
 
@@ -458,9 +459,9 @@ async function buildRow(node) {
458
459
  };
459
460
  }
460
461
 
461
- // A Conan package's facts come from its recipe, and its texts from the licenses/ its package ships.
462
- function conanRow(node) {
463
- const { conan } = node.general;
462
+ // The facts of a package source's package come from its recipe, and its texts from the licenses/ its package ships.
463
+ function sourcePackageRow(node) {
464
+ const { name: source, package: pkg } = node.general.packageSource;
464
465
  const licenseDir = `${node.paths.project}/licenses`;
465
466
  const files = fs.existsSync(licenseDir)
466
467
  ? fs.readdirSync(licenseDir, { recursive: true, withFileTypes: true }).filter((entry) => entry.isFile())
@@ -468,29 +469,32 @@ function conanRow(node) {
468
469
  const texts = files.map((entry) => path.join(entry.parentPath, entry.name)).sort()
469
470
  .map((file) => `=== ${path.relative(licenseDir, file)} ===\n\n${fs.readFileSync(file, 'utf8').trim()}`);
470
471
  return {
471
- name: conan.name,
472
+ name: pkg.name,
472
473
  npmName: null,
473
474
  version: null,
474
- nativeVersion: conan.version,
475
- license: conan.license,
475
+ nativeVersion: pkg.version,
476
+ license: pkg.license,
476
477
  licenseDeclared: null,
477
478
  licenseSelected: null,
478
- licenseNotes: `built from the ConanCenter recipe ${conan.ref}`,
479
- sha256: conan.source?.sha256 ?? null,
480
- sourceUrl: conan.source?.url ?? conan.homepage ?? null,
479
+ licenseNotes: `built from the ${source} package ${pkg.ref}`,
480
+ sha256: pkg.source?.sha256 ?? null,
481
+ sourceUrl: pkg.source?.url ?? pkg.homepage ?? null,
481
482
  licenseText: texts.length > 0 ? texts.join('\n\n') : null,
482
- isCopyleft: isCopyleft(conan.license),
483
- purl: `pkg:conan/${conan.name}@${conan.version}`,
483
+ isCopyleft: isCopyleft(pkg.license),
484
+ purl: `pkg:${source}/${pkg.name}@${pkg.version}`,
484
485
  };
485
486
  }
486
487
 
487
- // Conan packages join the graph from the stage a build makes; without it they would be left out unseen.
488
- function assertConanStaged() {
489
- const attached = new Set(state.config.allDependencies.filter((d) => d.general.conan).map((d) => d.general.conan.name));
490
- const missing = Object.keys(state.config.conanDependencies ?? {}).filter((name) => !attached.has(name));
491
- if (missing.length > 0) {
492
- throw new Error(`crossbind: the Conan packages ${missing.join(', ')} are not installed for the current conanDependencies, so their rows would be missing. Build the project first.`);
493
- }
488
+ // The packages of package sources join the graph from the stage a build makes; without it they would be left out unseen.
489
+ function assertSourcesStaged() {
490
+ declaredSources(state.config).forEach(({ name }) => {
491
+ const attached = new Set(state.config.allDependencies.filter((d) => d.general.packageSource?.name === name)
492
+ .map((d) => d.general.packageSource.package.name));
493
+ const missing = Object.keys(dependenciesOf(state.config, name)).filter((pkg) => !attached.has(pkg));
494
+ if (missing.length > 0) {
495
+ throw new Error(`crossbind: the ${name} packages ${missing.join(', ')} are not installed for the current ${declarationOf(name)}, so their rows would be missing. Build the project first.`);
496
+ }
497
+ });
494
498
  }
495
499
 
496
500
  // platform (optional, e.g. 'wasi') additionally includes what that platform's
@@ -499,23 +503,25 @@ function assertConanStaged() {
499
503
  // addon of that platform compiles in, or without a platform the one its loader bundles;
500
504
  // runtimeEnv 'app' adds what an app's host carries.
501
505
  export default async function collectLicenseRows(platform = null, { runtimeEnv = null } = {}) {
502
- assertConanStaged();
506
+ assertSourcesStaged();
503
507
  // The root package is a component too when it is a port: leaf -wasi packages have no deps but ship
504
508
  // their own upstream. A dependency that is no port, such as a library a project publishes, is one by
505
509
  // its package name; a private package is the project's own code, like the app.
506
- const keyOf = (node) => (node.general.conan ? `conan:${node.general.conan.name}` : node.general.alias?.package ?? node.package?.name);
510
+ const keyOf = ({ general, package: manifest }) => (general.packageSource
511
+ ? `${general.packageSource.name}:${general.packageSource.package.name}`
512
+ : general.alias?.package ?? manifest?.name);
507
513
  const nodes = [state.config, ...state.config.allDependencies]
508
514
  .filter((node, index) => node?.paths?.project
509
- && (node.general?.alias?.package || node.general?.conan || (index > 0 && node.package?.name && !node.package.private)));
515
+ && (node.general?.alias?.package || node.general?.packageSource || (index > 0 && node.package?.name && !node.package.private)));
510
516
  const rows = [];
511
517
  const seen = new Set();
512
518
  for (const node of nodes) {
513
519
  const key = keyOf(node);
514
520
  if (seen.has(key)) continue;
515
521
  seen.add(key);
516
- rows.push(node.general.conan ? conanRow(node) : await buildRow(node));
517
- // A Conan package's folder is written from what its recipe produced: nothing there is run.
518
- if (platform && !node.general.conan) {
522
+ rows.push(node.general.packageSource ? sourcePackageRow(node) : await buildRow(node));
523
+ // A package source's package folder is written from what its recipe produced: nothing there is run.
524
+ if (platform && !node.general.packageSource) {
519
525
  const familyProjects = nodes.filter((other) => keyOf(other) === key).map((other) => other.paths.project);
520
526
  rows.push(...await bundledRowsOf(node, platform, familyProjects));
521
527
  }