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.
- package/package.json +3 -2
- package/src/actions/addExtension.js +6 -1
- package/src/actions/buildApp.js +10 -5
- package/src/actions/buildDependencies.js +3 -3
- package/src/actions/createImportedBridges.js +10 -8
- package/src/actions/createInterface.js +23 -21
- package/src/actions/createLib.js +3 -3
- package/src/actions/getAllBridges.js +20 -5
- package/src/actions/getDependLibs.js +5 -0
- package/src/actions/getLinkInputs.js +2 -1
- package/src/actions/gradleProject.js +13 -2
- package/src/actions/licenses.js +30 -24
- package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
- package/src/actions/writeRuntimeEntry.js +2 -2
- package/src/actions/xcodeProject.js +12 -6
- package/src/app/index.d.ts +19 -0
- package/src/app/index.js +25 -1
- package/src/assets/node-import-hooks.mjs +7 -6
- package/src/bin.js +17 -9
- package/src/index.js +2 -2
- package/src/integration/getCrossbindScript.js +3 -2
- package/src/integration/getDependFilePath.js +10 -10
- package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
- package/src/state/index.js +6 -5
- package/src/state/loadConfig.js +10 -4
- package/src/state/refreshPackageSources.js +19 -0
- package/src/utils/appExtensions.js +44 -28
- package/src/utils/appOptions.js +31 -10
- package/src/utils/appRustCrates.js +2 -2
- package/src/utils/bridgeTools.js +6 -1
- package/src/utils/cppDts.js +11 -11
- package/src/utils/extensionSteps.js +38 -1
- package/src/utils/nativeImports.js +9 -2
- package/src/utils/nodeDevBuild.js +2 -1
- package/src/utils/packageSourceApi.js +87 -0
- package/src/utils/packageSourceImport.js +69 -0
- package/src/utils/packageSourceInstall.js +162 -0
- package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
- package/src/utils/packageSources.js +108 -0
- package/src/utils/printableText.js +5 -0
- package/src/utils/runPackageSource.js +251 -0
- package/src/utils/rustBridgeGen.js +3 -3
- package/src/utils/targetToolchain.js +77 -0
- package/src/state/refreshConanDependencies.js +0 -17
- package/src/utils/conanDependencies.js +0 -66
- package/src/utils/conanImport.js +0 -58
- package/src/utils/conanInstall.js +0 -154
- package/src/utils/conanProfile.js +0 -203
- package/src/utils/runConan.js +0 -257
|
@@ -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 {
|
|
10
|
-
import {
|
|
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;
|
|
27
|
-
export const
|
|
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
|
-
|
|
32
|
-
|
|
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.
|
|
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
|
|
48
|
+
export function packageEntry(source, entry) {
|
|
49
49
|
const fail = (field) => {
|
|
50
|
-
throw new Error(`crossbind: the
|
|
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(
|
|
53
|
-
if (!matches(
|
|
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(
|
|
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(
|
|
90
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
102
|
-
|
|
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
|
|
97
|
+
return [...pkg[key]];
|
|
106
98
|
};
|
|
107
|
-
if (typeof
|
|
108
|
-
throw new Error(`crossbind:
|
|
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
|
-
...
|
|
112
|
-
|
|
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
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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: (
|
|
184
|
-
const entry = fs.lstatSync(
|
|
155
|
+
filter: (file) => {
|
|
156
|
+
const entry = fs.lstatSync(file);
|
|
185
157
|
if (!entry.isSymbolicLink()) return entry.isDirectory() || entry.isFile();
|
|
186
|
-
const real =
|
|
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
|
|
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
|
|
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
|
|
215
|
-
const
|
|
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'),
|
|
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) =>
|
|
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
|
|
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 =
|
|
236
|
-
if (shipped && fs.statSync(shipped).isDirectory()) copyTree(shipped, licenses,
|
|
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:
|
|
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(${
|
|
225
|
+
...hosts.map((host) => `set(${hostLibs}${host} "${archivesOf(host).join(';')}")`),
|
|
252
226
|
distCmake
|
|
253
|
-
.replace('___PROJECT_NAME___', () =>
|
|
227
|
+
.replace('___PROJECT_NAME___', () => sourceTargetName(source, pkg.name))
|
|
254
228
|
.replace('___PROJECT_HOST___', () => hosts.join(';'))
|
|
255
|
-
.replace('___PROJECT_LIBS___', () => `\${${
|
|
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
|
|
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
|
|
270
|
-
//
|
|
271
|
-
export function
|
|
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(
|
|
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
|
|
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
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// C0 and C1 controls, which carry terminal escape sequences.
|
|
2
|
+
export const isControlCharacter = (char) => char < ' ' || (char >= '\u007f' && char <= '\u009f');
|
|
3
|
+
|
|
4
|
+
// Text a recipe or a tool wrote, for a terminal or a message: its lines and tabs, without any other control character.
|
|
5
|
+
export const printable = (text) => [...text].filter((char) => char === '\n' || char === '\t' || !isControlCharacter(char)).join('');
|