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
@@ -0,0 +1,69 @@
1
+ import fs from 'node:fs';
2
+
3
+ // A package of a source, as an import and a folder name it. Lower-case, as Conan's are: a file system that
4
+ // ignores case holds one folder for two names that differ in case alone.
5
+ export const PACKAGE_NAME = /^[a-z0-9_][a-z0-9_+.-]*$/;
6
+ // No leading dot, so neither '.' nor '..' can climb out of the include directory.
7
+ const HEADER_SEGMENT = /^[A-Za-z0-9_+-][A-Za-z0-9_.+-]*$/;
8
+
9
+ export const sourceStageDir = (cacheDir, source) => `${cacheDir}/${source}`;
10
+ // Apart from the stage's own files, so no package name can land on one of them.
11
+ export const sourcePackageDir = (stageDir, name) => `${stageDir}/packages/${name}`;
12
+
13
+ // A source's name is its scheme and names all that is its own: packages.<name> in the config, <name>.lock and
14
+ // .crossbind/<name>. Where a project declares its packages, as a message names it, and what it declares there.
15
+ export const declarationOf = (source) => `packages.${source}`;
16
+ export const dependenciesOf = (config, source) => config.packages?.[source] ?? {};
17
+ // The crates a project declares: cargo is the core's own, declared beside the sources' packages.
18
+ export const cratesOf = (config) => dependenciesOf(config, 'cargo');
19
+
20
+ // The source whose scheme a specifier opens with, as conan:zlib/zlib.h names conan.
21
+ export const sourceOfImport = (config, specifier) => (config.packageSources ?? []).find(({ name }) => specifier.startsWith(`${name}:`));
22
+
23
+ export function parseSourceImport(source, specifier, headerExtensions) {
24
+ const [name, ...headerPath] = specifier.slice(source.length + 1).split('/');
25
+ if (!PACKAGE_NAME.test(name)) {
26
+ throw new Error(`crossbind: '${specifier}' - '${name}' is not a ${source} package name; write ${source}:<package>/<header>.`);
27
+ }
28
+ if (headerPath.length === 0) {
29
+ throw new Error(`crossbind: '${specifier}' names no header - import one of the package's headers as ${source}:${name}/<header>.`);
30
+ }
31
+ const bad = headerPath.find((segment) => !HEADER_SEGMENT.test(segment));
32
+ if (bad !== undefined) {
33
+ throw new Error(`crossbind: '${specifier}' - '${bad}' is not a header path segment.`);
34
+ }
35
+ const header = headerPath.join('/');
36
+ const extension = header.includes('.') ? header.split('.').at(-1) : '';
37
+ if (!headerExtensions.includes(extension)) {
38
+ throw new Error(`crossbind: '${specifier}' - a ${source}: import names a header (${headerExtensions.map((e) => `.${e}`).join(', ')}).`);
39
+ }
40
+ return { name, header };
41
+ }
42
+
43
+ // The inverse of resolveSourceHeader, for the editor types of a staged header.
44
+ export function sourceImportOfHeader(file, cacheDir, sources) {
45
+ const prefixOf = (source) => sourcePackageDir(sourceStageDir(cacheDir, source), '');
46
+ const source = sources.find((name) => file.startsWith(prefixOf(name)));
47
+ if (!source) return null;
48
+ const match = file.slice(prefixOf(source).length).match(/^([^/]+)\/dist\/prebuilt\/[^/]+\/include\/(.+)$/);
49
+ // Both end up in the module name of the generated types.
50
+ if (!match || !PACKAGE_NAME.test(match[1]) || !match[2].split('/').every((segment) => HEADER_SEGMENT.test(segment))) return null;
51
+ return { source, name: match[1], header: match[2] };
52
+ }
53
+
54
+ // Only declared packages are importable, as with cargo:; a package pulled in by another links but stays hidden.
55
+ export function resolveSourceHeader(config, source, specifier, target) {
56
+ const { name, header } = parseSourceImport(source, specifier, config.ext.header);
57
+ if (!Object.hasOwn(dependenciesOf(config, source), name)) {
58
+ throw new Error(`crossbind: '${specifier}' is not declared - add '${name}' to ${declarationOf(source)} in crossbind.config.`);
59
+ }
60
+ const dependency = config.allDependencies.find(({ general }) => general.packageSource?.name === source && general.packageSource.package.name === name);
61
+ if (!dependency) {
62
+ throw new Error(`crossbind: '${specifier}' - ${name} is declared in ${declarationOf(source)} but not installed; crossbind installs ${source} packages when a build starts.`);
63
+ }
64
+ const tried = [...new Set([target.releasePath, target.path].filter(Boolean))]
65
+ .map((targetPath) => `${dependency.paths.output}/prebuilt/${targetPath}/include/${header}`);
66
+ const found = tried.find((file) => fs.existsSync(file));
67
+ if (found) return found;
68
+ throw new Error(`crossbind: '${specifier}' - ${name} has no ${header}. Tried:\n${tried.map((file) => ` - ${file}`).join('\n')}`);
69
+ }
@@ -0,0 +1,162 @@
1
+ import fs from 'node:fs';
2
+ import upath from 'upath';
3
+ import withDirLock from './dirLock.js';
4
+ import logger from './logger.js';
5
+ import loadJson from './loadJson.js';
6
+ import writeIfChanged from './writeIfChanged.js';
7
+ import { getContentHash } from './hash.js';
8
+ import { getDockerImage, imageRoleFor } from './pullDockerImage.js';
9
+ import { extensionStep, isJsonValue, stepFailure } from './appExtensions.js';
10
+ import {
11
+ assertRunsOnHost, assertRunsRemotely, clearSourceWork, createSourceProbe, createSourceWork, localCompilerIdentity,
12
+ removeSourceWork, sourceRoot, sourceRunner, toHostPath,
13
+ } from './runPackageSource.js';
14
+ import packageSourceApi from './packageSourceApi.js';
15
+ import { TOOLCHAIN_PLATFORMS, targetToolchain } from './targetToolchain.js';
16
+ import {
17
+ declaredSources, dependenciesKey, declarationOf, dependenciesOf,
18
+ } from './packageSources.js';
19
+ import {
20
+ installedPackages, stagePackage, writeSourceManifest, readSourceManifest,
21
+ } from './packageSourceStage.js';
22
+ import { sourceStageDir } from './packageSourceImport.js';
23
+
24
+ // Bumped when the stage changes shape, so a stage an older crossbind made is made again.
25
+ const STAGE_FORMAT = 4;
26
+ const MAX_LOCK_LENGTH = 8 * 1024 * 1024;
27
+
28
+ const lockFileOf = (config, name) => upath.join(config.paths.project, `${name}.lock`);
29
+ const stampFileOf = (stageDir, target) => upath.join(stageDir, 'stamps', `${target.path}.json`);
30
+ const logFileOf = (stageDir, target) => upath.join(stageDir, 'logs', `${target.path}.log`);
31
+ const lockOf = (file) => (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null);
32
+ // A source gets a copy: what it does to one changes nothing of crossbind's.
33
+ const copyOf = (value) => JSON.parse(JSON.stringify(value));
34
+
35
+ // What crossbind builds the target with, which a source builds its packages to match.
36
+ function toolchainOf(name, target) {
37
+ const toolchain = targetToolchain(target);
38
+ if (toolchain) return toolchain;
39
+ const names = TOOLCHAIN_PLATFORMS;
40
+ throw new Error(`crossbind: ${name}: imports build for ${names.slice(0, -1).join(', ')} and ${names.at(-1)} so far; ${target.platform} is not supported yet, so its build cannot use ${declarationOf(name)}.`);
41
+ }
42
+
43
+ // What a source says its packages are made from beside what crossbind knows of.
44
+ function inputsOf(source, config, target, toolchain) {
45
+ const { name, packageSource } = source;
46
+ if (!packageSource.inputs) return null;
47
+ const api = packageSourceApi({
48
+ source, config, target, work: createSourceProbe(name, sourceRunner(config, target)),
49
+ });
50
+ let inputs;
51
+ try {
52
+ inputs = packageSource.inputs(api, { dependencies: copyOf(dependenciesOf(config, name)), toolchain: copyOf(toolchain) });
53
+ } catch (error) {
54
+ throw stepFailure(name, 'inputs', error);
55
+ }
56
+ if (!isJsonValue(inputs)) throw new Error(`crossbind: ${name}'s inputs gives no JSON value, whose change is what stages its packages again.`);
57
+ return inputs;
58
+ }
59
+
60
+ // What a staged target was made from. The identity is the image, or the host's compiler where the tool runs on
61
+ // the host (RUNNER=LOCAL, and iOS and macOS always): packages build with that compiler, so a new one must restage.
62
+ function stampKey(source, config, target) {
63
+ const { name } = source;
64
+ const toolchain = toolchainOf(name, target);
65
+ return getContentHash(JSON.stringify({
66
+ format: STAGE_FORMAT,
67
+ source: name,
68
+ dependencies: dependenciesOf(config, name),
69
+ toolchain,
70
+ identity: sourceRunner(config, target) === 'LOCAL' ? localCompilerIdentity(target) : getDockerImage(imageRoleFor(target)),
71
+ lock: lockOf(lockFileOf(config, name)),
72
+ inputs: inputsOf(source, config, target, toolchain),
73
+ }));
74
+ }
75
+
76
+ // The target's manifest names the dependencies it was staged for.
77
+ const isStaged = (source, config, stageDir, target) => loadJson(stampFileOf(stageDir, target))?.key === stampKey(source, config, target)
78
+ && readSourceManifest(source.name, stageDir, dependenciesKey(dependenciesOf(config, source.name)), target.path) !== null;
79
+
80
+ // The lock comes back from where the recipes ran, and every later build reads it again. The source says whether
81
+ // it is a lock; crossbind takes text alone, of no more than a lock gets.
82
+ function writeLock(name, file, lock) {
83
+ if (lock === null || lock === undefined) return;
84
+ if (typeof lock !== 'string' || lock.length > MAX_LOCK_LENGTH || lock.includes('\0')) {
85
+ throw new Error(`crossbind: ${name}'s install gives no lock crossbind can keep, which is text of 8 MiB at most, so ${name}.lock is left as it was.`);
86
+ }
87
+ writeIfChanged(file, lock);
88
+ }
89
+
90
+ async function installTarget(source, config, stageDir, target) {
91
+ const { name, packageSource } = source;
92
+ const dependencies = dependenciesOf(config, name);
93
+ const lockFile = lockFileOf(config, name);
94
+ const stampFile = stampFileOf(stageDir, target);
95
+ const logFile = logFileOf(stageDir, target);
96
+ const runner = sourceRunner(config, target);
97
+ const toolchain = toolchainOf(name, target);
98
+ if (runner === 'LOCAL') assertRunsOnHost(name, target);
99
+ if (runner === 'REMOTE') assertRunsRemotely(source);
100
+ // A restage that stops halfway must not pass for a finished one.
101
+ fs.rmSync(stampFile, { force: true });
102
+ const work = createSourceWork(name, runner);
103
+ const label = `${name} ${target.path}`;
104
+ logger.startTask(label);
105
+ try {
106
+ const api = packageSourceApi({
107
+ source, config, target, work, logFile,
108
+ });
109
+ const installed = await extensionStep(name, 'install', () => packageSource.install(api, {
110
+ dependencies: copyOf(dependencies), toolchain: copyOf(toolchain), lock: lockOf(lockFile),
111
+ }));
112
+ const packages = installedPackages(name, installed?.packages);
113
+ const distCmake = fs.readFileSync(upath.join(config.paths.cli, 'assets', 'cmake', 'dist.cmake'), 'utf8');
114
+ packages.forEach((pkg) => stagePackage(name, pkg, {
115
+ stageDir, targetPath: target.path, toHost: (reported) => toHostPath(reported, work), store: work.store, distCmake,
116
+ }));
117
+ writeSourceManifest(stageDir, target.path, {
118
+ key: dependenciesKey(dependencies),
119
+ packages: packages.map(({
120
+ packageFolder, includedirs, libdirs, ...entry
121
+ }) => entry),
122
+ });
123
+ writeLock(name, lockFile, installed.lock);
124
+ writeIfChanged(stampFile, `${JSON.stringify({ key: stampKey(source, config, target) })}\n`);
125
+ logger.doneTask(label, packages.map((pkg) => `${pkg.name} ${pkg.version}`).join(', '));
126
+ } finally {
127
+ // Not over the error that ended the run; the next install clears what is left.
128
+ try {
129
+ removeSourceWork(work);
130
+ } catch (e) {
131
+ logger.info(`crossbind: could not remove ${work.dir} (${e.message}).`);
132
+ }
133
+ }
134
+ }
135
+
136
+ async function installSource(source, config, targets) {
137
+ const { name } = source;
138
+ const stageDir = sourceStageDir(config.paths.cache, name);
139
+ const pending = targets.filter((target) => !isStaged(source, config, stageDir, target));
140
+ if (pending.length === 0) return false;
141
+ for (const runner of new Set(pending.map((target) => sourceRunner(config, target)))) {
142
+ // A store takes one writer at a time, and every project on the machine shares it.
143
+ await withDirLock(`${sourceRoot(name, runner)}-install.lock`, async () => {
144
+ clearSourceWork(name, runner);
145
+ for (const target of pending.filter((candidate) => sourceRunner(config, candidate) === runner)) {
146
+ // Another build may have staged it while this one waited for the store.
147
+ if (!isStaged(source, config, stageDir, target)) await installTarget(source, config, stageDir, target);
148
+ }
149
+ });
150
+ }
151
+ return true;
152
+ }
153
+
154
+ // Installs the packages a project declares of each source for every target not staged from the same inputs
155
+ // yet, and says whether it staged any, so the caller attaches the new manifests.
156
+ export default async function installPackageSources(config, targets) {
157
+ let hasStaged = false;
158
+ for (const source of declaredSources(config)) {
159
+ hasStaged = await installSource(source, config, targets) || hasStaged;
160
+ }
161
+ return hasStaged;
162
+ }
@@ -6,10 +6,11 @@ import writeIfChanged from './writeIfChanged.js';
6
6
  import makeTreeWritable from './makeTreeWritable.js';
7
7
  import logger from './logger.js';
8
8
  import { libNameOf } from './linkLayout.js';
9
- import { CONAN_NAME, conanPackageDir } from './conanImport.js';
10
- import { CONAN_VERSION } from './conanDependencies.js';
9
+ import { PACKAGE_NAME, sourcePackageDir } from './packageSourceImport.js';
10
+ import { isControlCharacter, printable } from './printableText.js';
11
11
 
12
12
  const MANIFESTS = 'manifests';
13
+ const PACKAGE_VERSION = /^[A-Za-z0-9_][A-Za-z0-9_.+-]*$/;
13
14
  const LIB_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.+-]*$/;
14
15
  // A macro name with a plain value, if any (a path or a list too): one compiler argument, in a list CMake
15
16
  // splits on ';'.
@@ -20,17 +21,16 @@ const URL = /^https?:\/\/[^\s"'<>`\\]+$/;
20
21
  const SHA256 = /^[0-9a-f]{64}$/;
21
22
  const MAX_TEXT = 1000;
22
23
  const TARGET_PATH = /^[A-Za-z0-9_-]+$/;
23
- // The prefix of the variables a package's CMakeLists names each target's libraries by.
24
- const HOST_LIBS = 'CROSSBIND_CONAN_LIBS_';
25
24
 
26
- // The CMake target of a package; Conan names may carry '.', '+' and '-'.
27
- export const conanTargetName = (name) => `conan_${name.replace(/[^A-Za-z0-9_]/g, '_')}`;
25
+ // The CMake target of a package; package names may carry '.', '+' and '-'.
26
+ export const sourceTargetName = (source, name) => `${source}_${name.replace(/[^A-Za-z0-9_]/g, '_')}`;
27
+ // The prefix of the variables a package's CMakeLists names each target's libraries by.
28
+ const hostLibsOf = (source) => `CROSSBIND_${source.toUpperCase()}_LIBS_`;
28
29
 
29
30
  const matches = (pattern, value) => typeof value === 'string' && pattern.test(value);
30
31
  const isListOf = (pattern, values) => Array.isArray(values) && values.every((value) => matches(pattern, value));
31
- const shown = (value) => `${JSON.stringify(value)}`.slice(0, 200);
32
- // C0 and C1 controls, which carry terminal escape sequences.
33
- export const isControlCharacter = (char) => char < ' ' || (char >= '\u007f' && char <= '\u009f');
32
+ // What a recipe wrote, as a message quotes it: JSON escapes the C0 controls, and the others go.
33
+ const shown = (value) => printable(`${JSON.stringify(value)}`).slice(0, 200);
34
34
  // Recipe text ends up in notices and terminals: one line, no control characters.
35
35
  const textOf = (value) => (typeof value === 'string' && value.length <= MAX_TEXT && ![...value].some(isControlCharacter) ? value : null);
36
36
  const urlOf = (value) => (matches(URL, value) && value.length <= MAX_TEXT ? value : null);
@@ -42,19 +42,19 @@ function licenseOf(license) {
42
42
  return parts.length === 1 ? parts[0] : parts.map((part) => (part.includes(' OR ') ? `(${part})` : part)).join(' AND ');
43
43
  }
44
44
 
45
- // The fields of a package that crossbind keeps. Conan's JSON comes from recipe code and these fields
46
- // end up in paths, CMake files and notices, so they are checked where they come in and again where
45
+ // The fields of a package that crossbind keeps. What a source reports comes from recipe code and these
46
+ // fields end up in paths, CMake files and notices, so they are checked where they come in and again where
47
47
  // the manifest is read back.
48
- export function conanPackageEntry(entry) {
48
+ export function packageEntry(source, entry) {
49
49
  const fail = (field) => {
50
- throw new Error(`crossbind: the Conan package ${shown(entry.ref ?? entry.name)} has an invalid ${field}: ${shown(entry[field])}.`);
50
+ throw new Error(`crossbind: the ${source} package ${shown(entry.ref ?? entry.name)} has an invalid ${field}: ${shown(entry[field])}.`);
51
51
  };
52
- if (!matches(CONAN_NAME, entry.name)) fail('name');
53
- if (!matches(CONAN_VERSION, entry.version)) fail('version');
52
+ if (!matches(PACKAGE_NAME, entry.name)) fail('name');
53
+ if (!matches(PACKAGE_VERSION, entry.version)) fail('version');
54
54
  const reference = `${entry.name}/${entry.version}`;
55
55
  if (typeof entry.ref !== 'string' || !entry.ref.startsWith(reference) || !REFERENCE_TAIL.test(entry.ref.slice(reference.length))) fail('ref');
56
56
  if (!isListOf(LIB_NAME, entry.libs)) fail('libs');
57
- if (!isListOf(CONAN_NAME, entry.requires)) fail('requires');
57
+ if (!isListOf(PACKAGE_NAME, entry.requires)) fail('requires');
58
58
  // They become linker and compiler arguments. A manifest an older crossbind wrote has none of them.
59
59
  const systemLibs = entry.systemLibs ?? [];
60
60
  const frameworks = entry.frameworks ?? [];
@@ -78,50 +78,30 @@ export function conanPackageEntry(entry) {
78
78
  };
79
79
  }
80
80
 
81
- function sourceOf(entry) {
82
- const source = Array.isArray(entry) ? entry[0] : entry;
83
- if (!source?.url) return null;
84
- return { url: Array.isArray(source.url) ? source.url[0] : source.url, sha256: source.sha256 ?? null };
85
- }
86
-
87
81
  // A define crossbind cannot pass as one plain argument was left out before crossbind took defines at all,
88
82
  // so it stays out, with a notice, instead of stopping the build.
89
- function plainDefinesOf(node, defines) {
90
- return defines.filter((define) => {
83
+ function plainDefinesOf(pkg) {
84
+ if (!Array.isArray(pkg.defines)) return pkg.defines;
85
+ return pkg.defines.filter((define) => {
91
86
  if (matches(DEFINE, define)) return true;
92
- logger.info(`crossbind: ${shown(node.ref)} asks the code that uses it to compile with ${shown(define)}, which crossbind leaves out: a define passes as a plain name with a plain value.`);
87
+ logger.info(`crossbind: ${shown(pkg.ref)} asks the code that uses it to compile with ${shown(define)}, which crossbind leaves out: a define passes as a plain name with a plain value.`);
93
88
  return false;
94
89
  });
95
90
  }
96
91
 
97
- function packageOf(node, requires) {
98
- const components = Object.values(node.cpp_info ?? {});
99
- const collect = (key) => [...new Set(components.flatMap((component) => component?.[key] ?? []))];
92
+ function installedPackage(source, pkg) {
100
93
  const folders = (key) => {
101
- const values = collect(key);
102
- if (!values.every((value) => typeof value === 'string')) {
103
- throw new Error(`crossbind: the Conan package ${shown(node.ref)} has invalid ${key}.`);
94
+ if (!Array.isArray(pkg[key]) || !pkg[key].every((value) => typeof value === 'string')) {
95
+ throw new Error(`crossbind: the ${source} package ${shown(pkg.ref)} has invalid ${key}.`);
104
96
  }
105
- return values;
97
+ return [...pkg[key]];
106
98
  };
107
- if (typeof node.package_folder !== 'string') {
108
- throw new Error(`crossbind: conan reported the package ${shown(node.ref)} without a package folder.`);
99
+ if (typeof pkg.packageFolder !== 'string') {
100
+ throw new Error(`crossbind: ${source} reported the package ${shown(pkg.ref)} without a package folder.`);
109
101
  }
110
102
  return {
111
- ...conanPackageEntry({
112
- name: node.name,
113
- version: node.version,
114
- ref: node.ref,
115
- license: node.license,
116
- homepage: node.homepage,
117
- source: sourceOf(node.conandata?.sources?.[node.version]),
118
- libs: collect('libs'),
119
- requires,
120
- systemLibs: collect('system_libs'),
121
- frameworks: collect('frameworks'),
122
- defines: plainDefinesOf(node, collect('defines')),
123
- }),
124
- packageFolder: node.package_folder,
103
+ ...packageEntry(source, { ...pkg, defines: plainDefinesOf(pkg) }),
104
+ packageFolder: pkg.packageFolder,
125
105
  includedirs: folders('includedirs'),
126
106
  libdirs: folders('libdirs'),
127
107
  };
@@ -143,30 +123,22 @@ function linkOrder(packages) {
143
123
  return order.reverse();
144
124
  }
145
125
 
146
- // Host packages of a `conan install --format=json` graph in link order. Left out are the consumer,
147
- // build tools, test requirements and packages conan skipped because the consumer needs nothing of them.
148
- export function conanPackagesOf(graph) {
149
- const nodes = graph?.nodes;
150
- if (!nodes || typeof nodes !== 'object' || !graph.root) {
151
- throw new Error('crossbind: conan install printed a graph without nodes or a root.');
126
+ // The packages a source's install reports, checked, in link order, with the folders the source found them in.
127
+ export function installedPackages(source, packages) {
128
+ if (!Array.isArray(packages) || !packages.every((pkg) => pkg !== null && typeof pkg === 'object')) {
129
+ throw new Error(`crossbind: ${source}'s install gives no list of the packages it installed.`);
152
130
  }
153
- const [root] = Object.keys(graph.root);
154
- const isPackage = (id) => id !== root && nodes[id]?.context === 'host' && !nodes[id].test && nodes[id].binary !== 'Skip';
155
- const ids = Object.keys(nodes).filter(isPackage);
156
- const requiresOf = (id) => Object.entries(nodes[id].dependencies ?? {})
157
- .filter(([dependency, edge]) => !edge?.build && !edge?.test && ids.includes(dependency))
158
- .map(([dependency]) => nodes[dependency].name);
159
- return linkOrder(ids.map((id) => packageOf(nodes[id], requiresOf(id))));
131
+ return linkOrder(packages.map((pkg) => installedPackage(source, pkg)));
160
132
  }
161
133
 
162
- // The real path of a file conan reported, or null when there is none; refused when it resolves
134
+ // The real path of a file a source reported, or null when there is none; refused when it resolves
163
135
  // outside `root`.
164
- function realPathWithin(file, root, pkg) {
136
+ function realPathWithin(source, file, root, pkg) {
165
137
  if (!fs.existsSync(file)) return null;
166
138
  const real = fs.realpathSync(file);
167
139
  const relative = path.relative(root, real);
168
140
  if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
169
- throw new Error(`crossbind: the Conan package ${pkg.ref} reports ${file}, which resolves outside ${root}.`);
141
+ throw new Error(`crossbind: the ${source} package ${pkg.ref} reports ${printable(file)}, which resolves outside ${root}.`);
170
142
  }
171
143
  return real;
172
144
  }
@@ -175,15 +147,15 @@ function realPathWithin(file, root, pkg) {
175
147
  // directories and regular files inside the package, and into each linked directory once: a link out
176
148
  // of the package could point the copy at any file the host can read, and links back up the tree
177
149
  // would copy it without end.
178
- function copyTree(from, to, packageRoot, pkg) {
150
+ function copyTree(from, to, within) {
179
151
  const linked = new Set([from]);
180
152
  fs.cpSync(from, to, {
181
153
  recursive: true,
182
154
  dereference: true,
183
- filter: (source) => {
184
- const entry = fs.lstatSync(source);
155
+ filter: (file) => {
156
+ const entry = fs.lstatSync(file);
185
157
  if (!entry.isSymbolicLink()) return entry.isDirectory() || entry.isFile();
186
- const real = realPathWithin(source, packageRoot, pkg);
158
+ const real = within(file);
187
159
  const target = real && fs.statSync(real);
188
160
  if (!target?.isDirectory()) return Boolean(target?.isFile());
189
161
  if (linked.has(real)) return false;
@@ -196,10 +168,10 @@ function copyTree(from, to, packageRoot, pkg) {
196
168
 
197
169
  // The package's own folder: no link, and below the store's own layout, so it cannot take in the
198
170
  // store or another package.
199
- function packageRootOf(pkg, folder, store) {
200
- const real = realPathWithin(folder, store, pkg);
171
+ function packageRootOf(source, pkg, folder, store) {
172
+ const real = realPathWithin(source, folder, store, pkg);
201
173
  if (!real || fs.lstatSync(folder).isSymbolicLink() || path.relative(store, real).split(path.sep).length < 2) {
202
- throw new Error(`crossbind: the Conan package ${pkg.ref} has no package folder crossbind can use at ${pkg.packageFolder}.`);
174
+ throw new Error(`crossbind: the ${source} package ${pkg.ref} has no package folder crossbind can use at ${printable(pkg.packageFolder)}.`);
203
175
  }
204
176
  return real;
205
177
  }
@@ -207,35 +179,36 @@ function packageRootOf(pkg, folder, store) {
207
179
  // A package lands where a published port keeps its prebuilt (<dir>/dist/prebuilt/<target>/{include,lib}),
208
180
  // so the dependency machinery links it and SWIG treats its headers as one across targets. Everything
209
181
  // is read from inside the package's own folder in the store.
210
- export function stageConanPackage(pkg, {
182
+ export function stagePackage(source, pkg, {
211
183
  stageDir, targetPath, toHost, store, distCmake,
212
184
  }) {
213
- const packageRoot = packageRootOf(pkg, toHost(pkg.packageFolder), fs.realpathSync(store));
214
- const reported = (folder) => realPathWithin(toHost(upath.isAbsolute(folder) ? folder : upath.join(pkg.packageFolder, folder)), packageRoot, pkg);
215
- const dir = conanPackageDir(stageDir, pkg.name);
185
+ const packageRoot = packageRootOf(source, pkg, toHost(pkg.packageFolder), fs.realpathSync(store));
186
+ const within = (file) => realPathWithin(source, file, packageRoot, pkg);
187
+ const reported = (folder) => within(toHost(upath.isAbsolute(folder) ? folder : upath.join(pkg.packageFolder, folder)));
188
+ const dir = sourcePackageDir(stageDir, pkg.name);
216
189
  const prebuilt = upath.join(dir, 'dist', 'prebuilt');
217
190
  const prefix = upath.join(prebuilt, targetPath);
218
191
 
219
192
  fs.rmSync(prefix, { recursive: true, force: true });
220
193
  fs.mkdirSync(upath.join(prefix, 'include'), { recursive: true });
221
194
  fs.mkdirSync(upath.join(prefix, 'lib'), { recursive: true });
222
- pkg.includedirs.map(reported).filter(Boolean).forEach((folder) => copyTree(folder, upath.join(prefix, 'include'), packageRoot, pkg));
195
+ pkg.includedirs.map(reported).filter(Boolean).forEach((folder) => copyTree(folder, upath.join(prefix, 'include'), within));
223
196
  const libdirs = pkg.libdirs.map(reported).filter(Boolean);
224
197
  pkg.libs.forEach((lib) => {
225
- const archive = libdirs.map((folder) => realPathWithin(path.join(folder, `lib${lib}.a`), packageRoot, pkg))
198
+ const archive = libdirs.map((folder) => within(path.join(folder, `lib${lib}.a`)))
226
199
  .find((file) => file && fs.statSync(file).isFile());
227
200
  if (!archive) {
228
- throw new Error(`crossbind: the Conan package ${pkg.ref} declares the library ${lib}, but its package has no lib${lib}.a.`);
201
+ throw new Error(`crossbind: the ${source} package ${pkg.ref} declares the library ${lib}, but its package has no lib${lib}.a.`);
229
202
  }
230
203
  fs.copyFileSync(archive, upath.join(prefix, 'lib', `lib${lib}.a`));
231
204
  });
232
205
 
233
206
  const licenses = upath.join(dir, 'licenses');
234
207
  fs.rmSync(licenses, { recursive: true, force: true });
235
- const shipped = realPathWithin(path.join(packageRoot, 'licenses'), packageRoot, pkg);
236
- if (shipped && fs.statSync(shipped).isDirectory()) copyTree(shipped, licenses, packageRoot, pkg);
208
+ const shipped = within(path.join(packageRoot, 'licenses'));
209
+ if (shipped && fs.statSync(shipped).isDirectory()) copyTree(shipped, licenses, within);
237
210
  writeIfChanged(upath.join(dir, 'package.json'), `${JSON.stringify({
238
- name: `conan:${pkg.name}`, version: pkg.version, nativeVersion: pkg.version, license: pkg.license, private: true,
211
+ name: `${source}:${pkg.name}`, version: pkg.version, nativeVersion: pkg.version, license: pkg.license, private: true,
239
212
  }, null, 4)}\n`);
240
213
 
241
214
  const hosts = fs.readdirSync(prebuilt, { withFileTypes: true })
@@ -247,12 +220,13 @@ export function stageConanPackage(pkg, {
247
220
  if (!fs.existsSync(libdir)) return [];
248
221
  return fs.readdirSync(libdir).filter((file) => file.endsWith('.a')).map(libNameOf).filter((name) => matches(LIB_NAME, name)).sort();
249
222
  };
223
+ const hostLibs = hostLibsOf(source);
250
224
  writeIfChanged(upath.join(prebuilt, 'CMakeLists.txt'), [
251
- ...hosts.map((host) => `set(${HOST_LIBS}${host} "${archivesOf(host).join(';')}")`),
225
+ ...hosts.map((host) => `set(${hostLibs}${host} "${archivesOf(host).join(';')}")`),
252
226
  distCmake
253
- .replace('___PROJECT_NAME___', () => conanTargetName(pkg.name))
227
+ .replace('___PROJECT_NAME___', () => sourceTargetName(source, pkg.name))
254
228
  .replace('___PROJECT_HOST___', () => hosts.join(';'))
255
- .replace('___PROJECT_LIBS___', () => `\${${HOST_LIBS}\${PROJECT_TARGET_HOST}}`)
229
+ .replace('___PROJECT_LIBS___', () => `\${${hostLibs}\${PROJECT_TARGET_HOST}}`)
256
230
  .replace('___PROJECT_WHOLE_ARCHIVE___', () => ''),
257
231
  ].join('\n'));
258
232
  }
@@ -261,14 +235,14 @@ const manifestsOf = (stageDir) => upath.join(stageDir, MANIFESTS);
261
235
  const manifestFileOf = (stageDir, targetPath) => upath.join(manifestsOf(stageDir), `${targetPath}.json`);
262
236
 
263
237
  // One manifest per target: a recipe can require other packages on another platform.
264
- export function writeConanManifest(stageDir, targetPath, manifest) {
238
+ export function writeSourceManifest(stageDir, targetPath, manifest) {
265
239
  writeIfChanged(manifestFileOf(stageDir, targetPath), `${JSON.stringify(manifest, null, 4)}\n`);
266
240
  }
267
241
 
268
242
  // The packages staged for one target, or for every target staged so far, in link order, and each
269
- // target's own list. A manifest staged for other conanDependencies describes packages the config no
270
- // longer asks for.
271
- export function readConanManifest(stageDir, key, targetPath = null) {
243
+ // target's own list. A manifest staged for other dependencies describes packages the config no longer
244
+ // asks for.
245
+ export function readSourceManifest(source, stageDir, key, targetPath = null) {
272
246
  const dir = manifestsOf(stageDir);
273
247
  let files = [];
274
248
  if (targetPath) files = [manifestFileOf(stageDir, targetPath)];
@@ -277,7 +251,7 @@ export function readConanManifest(stageDir, key, targetPath = null) {
277
251
  .filter(({ manifest }) => manifest?.key === key);
278
252
  if (manifests.length === 0) return null;
279
253
  try {
280
- const targets = Object.fromEntries(manifests.map(({ target, manifest }) => [target, manifest.packages.map(conanPackageEntry)]));
254
+ const targets = Object.fromEntries(manifests.map(({ target, manifest }) => [target, manifest.packages.map((entry) => packageEntry(source, entry))]));
281
255
  return { key, packages: linkOrder(Object.values(targets).flat()), targets };
282
256
  } catch (e) {
283
257
  throw new Error(`crossbind: ${dir} lists a package crossbind will not use - delete ${stageDir} and build again. ${e.message}`, { cause: e });
@@ -285,7 +259,7 @@ export function readConanManifest(stageDir, key, targetPath = null) {
285
259
  }
286
260
 
287
261
  // Changes whenever a build stages packages, so a process that attached before can tell.
288
- export function conanManifestsStamp(stageDir) {
262
+ export function sourceManifestsStamp(stageDir) {
289
263
  const dir = manifestsOf(stageDir);
290
264
  if (!fs.existsSync(dir)) return '';
291
265
  return fs.readdirSync(dir).sort().map((name) => `${name}@${fs.statSync(upath.join(dir, name)).mtimeMs}`).join('|');
@@ -0,0 +1,108 @@
1
+ import path from 'node:path';
2
+ import { createRequire } from 'node:module';
3
+ import {
4
+ broughtExtension, isJsonValue, ownPackages, stepFailure, withListed,
5
+ } from './appExtensions.js';
6
+ import { getContentHash } from './hash.js';
7
+ import { PACKAGE_NAME, declarationOf, dependenciesOf } from './packageSourceImport.js';
8
+
9
+ // crossbind's own folder, whose dependencies name the extensions it brings.
10
+ const CORE_ROOT = path.join(import.meta.dirname, '..', '..');
11
+
12
+ // The extension packages crossbind itself brings, which a project neither installs nor lists.
13
+ export const packagesOfCrossbind = () => ownPackages(CORE_ROOT);
14
+
15
+ let own = null;
16
+
17
+ // crossbind's own package sources: those the extension packages among its dependencies give, which every project
18
+ // has by having crossbind. Loaded once, as a source may keep what it asked its tool.
19
+ function ownSources() {
20
+ if (!own) {
21
+ const load = createRequire(path.join(CORE_ROOT, 'package.json'));
22
+ own = packagesOfCrossbind()
23
+ .map((name) => broughtExtension(load, name, `update ${name} to match crossbind`))
24
+ .filter((extension) => extension.packageSource);
25
+ }
26
+ return own;
27
+ }
28
+
29
+ // A project's package sources: crossbind's own, then those crossbind.config.js lists. One listed under the name of
30
+ // one of crossbind's takes its place.
31
+ export const packageSourcesOf = (listed) => withListed(ownSources(), listed.filter((extension) => extension.packageSource));
32
+
33
+ export { declarationOf, dependenciesOf };
34
+
35
+ // The sources a project declares packages of.
36
+ export const declaredSources = (config) => (config.packageSources ?? []).filter(({ name }) => Object.keys(dependenciesOf(config, name)).length > 0);
37
+
38
+ export const sourceNamesOf = (config) => (config.packageSources ?? []).map(({ name }) => name);
39
+
40
+ // What an import names a store by, as the node: and npm: schemes do: cargo's crates, and each source's packages.
41
+ export const importSchemesOf = (config) => ['cargo', ...sourceNamesOf(config)];
42
+
43
+ // What a project declares for a source, as the source's check takes it: a map of package names to what each asks
44
+ // for, in plain JSON, as it is hashed, kept in manifests and handed back to the source.
45
+ export function checkedDependencies({ name, packageSource }, raw) {
46
+ let dependencies;
47
+ try {
48
+ dependencies = packageSource.check(raw);
49
+ } catch (error) {
50
+ throw stepFailure(name, 'check', error);
51
+ }
52
+ const isMap = dependencies !== null && typeof dependencies === 'object' && !Array.isArray(dependencies) && isJsonValue(dependencies);
53
+ if (!isMap || !Object.keys(dependencies).every((pkg) => PACKAGE_NAME.test(pkg))) {
54
+ throw new Error(`crossbind: ${name}'s check gives no ${declarationOf(name)} crossbind can use: a JSON map of package names, of lower-case letters, digits and _ + . -, to what each asks for.`);
55
+ }
56
+ return dependencies;
57
+ }
58
+
59
+ // cargo's crates, each as Cargo.toml takes it: a version, or the table that would follow `<crate> =`. Text, where a
60
+ // source's package may be an object: a number would lose a trailing zero, 0.10 becoming 0.1.
61
+ function checkedCrates(crates = {}) {
62
+ if (crates === null || typeof crates !== 'object' || Array.isArray(crates)) {
63
+ throw new Error(`crossbind: ${declarationOf('cargo')} must be an object of crates, such as { uuid: '1' }.`);
64
+ }
65
+ const [crate] = Object.entries(crates).find(([, spec]) => typeof spec !== 'string') ?? [];
66
+ if (crate !== undefined) {
67
+ throw new Error(`crossbind: ${declarationOf('cargo')}.${crate} must be text as Cargo.toml takes it: a version such as '1', or a table such as '{ version = "1", features = ["v4"] }'.`);
68
+ }
69
+ return { ...crates };
70
+ }
71
+
72
+ // What a project declares under packages, by the name its imports open with: cargo's crates, which are the core's
73
+ // own, and each source's packages. Another name stops the config: a misspelt one would be left unread. So does the
74
+ // key cargo or a source once had of its own, with where its packages are declared now and the config that holds it:
75
+ // a package crossbind builds from source loads as a project too, and its key is not the app's developer's to find.
76
+ export function checkedPackages(sources, config, file = 'crossbind.config') {
77
+ const names = sources.map(({ name }) => name);
78
+ const moved = ['cargo', ...names].find((name) => config[`${name}Dependencies`] !== undefined);
79
+ if (moved) throw new Error(`crossbind: ${moved}Dependencies is now ${declarationOf(moved)}: write packages: { ${moved}: { … } } in ${file}.`);
80
+ const { packages = {} } = config;
81
+ if (packages === null || typeof packages !== 'object' || Array.isArray(packages)) {
82
+ throw new Error("crossbind: \"packages\" must be an object of each package source's packages, such as { conan: { zlib: '1.3.2' } }.");
83
+ }
84
+ const unknown = Object.keys(packages).find((name) => name !== 'cargo' && !names.includes(name));
85
+ if (unknown !== undefined) {
86
+ throw new Error(`crossbind: ${declarationOf(unknown)} names neither cargo nor a package source of this project, which has ${names.join(', ')}. List its extension in extensions, or fix the name.`);
87
+ }
88
+ const written = (name) => (Object.hasOwn(packages, name) ? packages[name] : undefined);
89
+ return {
90
+ cargo: checkedCrates(written('cargo')),
91
+ ...Object.fromEntries(sources.map((source) => [source.name, checkedDependencies(source, written(source.name))])),
92
+ };
93
+ }
94
+
95
+ // What staged packages were installed for; a change to the dependencies makes them stale.
96
+ export const dependenciesKey = (dependencies) => getContentHash(JSON.stringify(dependencies));
97
+
98
+ // What an app's headers and sources see of its sources' packages: the packages are staged after a header may
99
+ // already be bound, and restaged in place when one moves to another version. Null without any.
100
+ export function packageSourceInputsOf(config) {
101
+ const declared = declaredSources(config);
102
+ if (declared.length === 0) return null;
103
+ return declared.map(({ name }) => ({
104
+ source: name,
105
+ dependencies: dependenciesOf(config, name),
106
+ refs: config.allDependencies.filter(({ general }) => general.packageSource?.name === name).map(({ general }) => general.packageSource.package.ref),
107
+ }));
108
+ }