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
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.72",
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.72"
54
55
  },
55
56
  "keywords": [
56
57
  "webassembly",
@@ -4,6 +4,7 @@ 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';
7
8
  import resolveAppHostRoot from '../utils/resolveAppHost.js';
8
9
 
9
10
  // The lockfiles by which a project names the package manager it is installed with.
@@ -108,9 +109,13 @@ function assertSpec(spec, platform) {
108
109
  }
109
110
  }
110
111
 
111
- // An extension an app has by having the host is none to add. A project without a host yet has none of them.
112
+ // An extension a project has by having crossbind, or an app by having the host, is none to add. A project without a
113
+ // host yet has none of the host's.
112
114
  function assertNotOwn(spec) {
113
115
  const name = registryName(spec);
116
+ if (packagesOfCrossbind().includes(name)) {
117
+ throw new Error(`crossbind: every project has ${name} by having crossbind, which brings it, so there is nothing to add.`);
118
+ }
114
119
  let root;
115
120
  try {
116
121
  root = resolveAppHostRoot();
@@ -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() {
@@ -441,8 +445,8 @@ export function publishApp(target, { to = appOutputPath(target), page = null } =
441
445
  build: build === null ? appVersion() : String(build), icon: icon && iosIcon(icon), urlSchemes: appUrlSchemes(state.config),
442
446
  fileTypes: fileTypeOwners(state.config), links: linkOwners(state.config).map(({ link }) => link),
443
447
  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),
448
+ background: appBackgroundColor(state.config), darkBackground: appDarkBackgroundColor(state.config), usage: usageDescriptions(state.config, 'ios'),
449
+ encryption: appEncryption(state.config), multipleWindows: iosMultipleWindows(state.config),
446
450
  };
447
451
  const extensions = extensionsOf('ios');
448
452
  const privacy = mergedPrivacy(ownPrivacy, extensions.privacy);
@@ -455,7 +459,8 @@ export function publishApp(target, { to = appOutputPath(target), page = null } =
455
459
  const icon = readAppIcon(state.config);
456
460
  const options = {
457
461
  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)),
462
+ urlSchemes: appUrlSchemes(state.config), background: appBackgroundColor(state.config), darkBackground: appDarkBackgroundColor(state.config),
463
+ permissions: Object.keys(appPermissions(state.config)),
459
464
  fileTypes: fileTypeOwners(state.config), links: linkOwners(state.config).map(({ link }) => link), shortcutItems: shortcutItemOwners(state.config),
460
465
  };
461
466
  const extensions = extensionsOf('android');
@@ -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)])
@@ -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
  }
@@ -21,6 +21,11 @@ export default function getDependLibs(target) {
21
21
  if (libPaths.length === 0) {
22
22
  libPaths = libsIn(target.releasePath);
23
23
  }
24
+ // A published port can leave its iOS archive to its xcframework, where the package's paths lead.
25
+ const xcframeworkLib = target.platform === 'ios' && state.config.allDependencyPaths?.[target.path]?.[fileName]?.lib;
26
+ if (libPaths.length === 0 && xcframeworkLib) {
27
+ libPaths = findFiles(xcframeworkLib, { cwd: d.paths.project });
28
+ }
24
29
  dependLibs.push(...libPaths);
25
30
  });
26
31
  }
@@ -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
  }
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import state, { setAllDependecyPaths } from '../state/index.js';
3
- import refreshConanDependencies from '../state/refreshConanDependencies.js';
4
- import installConanPackages from '../utils/conanInstall.js';
3
+ import refreshPackageSources from '../state/refreshPackageSources.js';
4
+ import installPackageSources from '../utils/packageSourceInstall.js';
5
+ import { declaredSources } from '../utils/packageSources.js';
5
6
  import { releaseTargetOf, HOST_BUILT_PLATFORMS } from '../utils/targets.js';
6
7
  import createXCFramework from './createXCFramework.js';
7
8
 
@@ -11,11 +12,11 @@ const isBuiltHere = (target) => process.platform === 'darwin' || !HOST_BUILT_PLA
11
12
 
12
13
  // iOS headers and archives come out of an xcframework per library with a slice per SDK (state/index.js).
13
14
  // One older than an archive it wraps was made before the package was staged again.
14
- function makeConanXCFrameworks(iosTargets) {
15
+ function makeXCFrameworks(iosTargets) {
15
16
  const archiveOf = (dependency, target, name) => `${dependency.paths.output}/prebuilt/${target.path}/lib/lib${name}.a`;
16
17
  const isFresh = (dependency, name) => mtimeOf(`${dependency.paths.project}/${name}.xcframework/Info.plist`)
17
18
  >= Math.max(...iosTargets.map((target) => mtimeOf(archiveOf(dependency, target, name))));
18
- state.config.allDependencies.filter((dependency) => dependency.general.conan).forEach((dependency) => {
19
+ state.config.allDependencies.filter((dependency) => dependency.general.packageSource).forEach((dependency) => {
19
20
  // A library its recipe names on another platform only has no slice here.
20
21
  const libName = dependency.export.libName.filter((name) => iosTargets.every((target) => fs.existsSync(archiveOf(dependency, target, name))));
21
22
  if (libName.every((name) => isFresh(dependency, name))) return;
@@ -29,11 +30,11 @@ function makeConanXCFrameworks(iosTargets) {
29
30
  });
30
31
  }
31
32
 
32
- // Conan packages are staged before any header is read: SWIG parses headers that include theirs, and an
33
- // interface made without its includes stays that way until the header itself changes. Release trees
34
- // only - a debug build links them like any prebuilt.
35
- export default async function prepareConanDependencies(targets) {
36
- if (Object.keys(state.config.conanDependencies ?? {}).length === 0) return;
33
+ // The packages of a project's sources are staged before any header is read: SWIG parses headers that
34
+ // include theirs, and an interface made without its includes stays that way until the header itself
35
+ // changes. Release trees only - a debug build links them like any prebuilt.
36
+ export default async function preparePackageSources(targets) {
37
+ if (declaredSources(state.config).length === 0) return;
37
38
  // createBridgeFile reads every header for the first wasm target unless it is told otherwise, as it
38
39
  // is by Metro, which builds the React Native bridges for the platform's own target, and by a Node
39
40
  // build, which reads them for one of its own (nodeBridgeTarget).
@@ -43,9 +44,9 @@ export default async function prepareConanDependencies(targets) {
43
44
  const releaseTargets = [...new Map([...targets, bridgeTarget, ...iosTargets].filter(Boolean).map((target) => [
44
45
  target.releasePath, releaseTargetOf(target),
45
46
  ])).values()].filter(isBuiltHere);
46
- await installConanPackages(state.config, releaseTargets);
47
+ await installPackageSources(state.config, releaseTargets);
47
48
  // Also when another process did the staging: this one may have loaded before the manifests existed.
48
- refreshConanDependencies(state.config);
49
+ refreshPackageSources(state.config);
49
50
  setAllDependecyPaths();
50
- if (iosTargets.length > 0) makeConanXCFrameworks(releaseTargets.filter((target) => target.platform === 'ios'));
51
+ if (iosTargets.length > 0) makeXCFrameworks(releaseTargets.filter((target) => target.platform === 'ios'));
51
52
  }
@@ -50,7 +50,7 @@ function nativesOf(headers, imports, dir) {
50
50
  }
51
51
 
52
52
  // The module `node --import` loads ahead of the app, so the app imports the bound headers themselves.
53
- export function writeImportHooks(entry, headers, imports, extensions) {
53
+ export function writeImportHooks(entry, headers, imports, extensions, schemes) {
54
54
  // Node runs the register file from its real path, so the files it names are relative to that.
55
55
  const dir = upath.normalize(fs.realpathSync(upath.dirname(entry)));
56
56
  const natives = nativesOf(headers, imports, dir);
@@ -59,7 +59,7 @@ export function writeImportHooks(entry, headers, imports, extensions) {
59
59
  '// Generated by crossbind - do not edit.',
60
60
  "import registerNativeImports from './hooks.mjs';",
61
61
  '',
62
- `registerNativeImports(new URL('./${upath.basename(entry)}', import.meta.url), ${JSON.stringify({ extensions, natives }, null, 4)});`,
62
+ `registerNativeImports(new URL('./${upath.basename(entry)}', import.meta.url), ${JSON.stringify({ extensions, schemes, natives }, null, 4)});`,
63
63
  '',
64
64
  ].join('\n'));
65
65
  }