crossbind 2.0.0-beta.68 → 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/README.md +1 -1
- package/package.json +70 -59
- package/src/actions/addExtension.js +196 -0
- package/src/actions/androidGradle.js +26 -17
- package/src/actions/appInstaller.js +69 -153
- package/src/actions/appSwift.js +66 -0
- package/src/actions/buildApp.js +177 -74
- package/src/actions/buildDependencies.js +3 -3
- package/src/actions/buildNode.js +6 -1
- package/src/actions/buildWasm.js +0 -9
- package/src/actions/createImportedBridges.js +10 -8
- package/src/actions/createInterface.js +40 -29
- package/src/actions/createLib.js +3 -7
- package/src/actions/extensionApi.js +298 -0
- package/src/actions/extensionCommands.js +41 -0
- package/src/actions/extensionsInApp.js +19 -0
- 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 +207 -16
- package/src/actions/iosDevice.js +2 -2
- package/src/actions/iosSimulator.js +3 -3
- package/src/actions/licenses.js +113 -58
- package/src/actions/linuxPackages.js +48 -0
- package/src/actions/mavenVerification.js +37 -0
- package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
- package/src/actions/publish.js +220 -0
- package/src/actions/windowsSigning.js +1 -1
- package/src/actions/writeRuntimeEntry.js +2 -2
- package/src/actions/xcodeProject.js +60 -29
- package/src/actions/xcodeTargets.js +122 -0
- package/src/actions/xcodebuild.js +65 -0
- package/src/app/index.d.ts +144 -132
- package/src/app/index.js +147 -185
- package/src/assets/configure/install.sh +0 -0
- package/src/assets/node-import-hooks.mjs +7 -6
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +37 -37
- package/src/index.js +4 -3
- package/src/integration/getCrossbindScript.js +3 -2
- package/src/integration/getDependFilePath.js +10 -10
- package/src/program.js +32 -3
- package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
- package/src/state/index.js +8 -6
- package/src/state/loadConfig.js +26 -9
- package/src/state/refreshPackageSources.js +19 -0
- package/src/utils/appExtensions.js +777 -0
- package/src/utils/appFiles.js +58 -0
- package/src/utils/appLaunches.js +249 -0
- package/src/utils/appOptions.js +142 -112
- package/src/utils/appRustCrates.js +2 -2
- package/src/utils/appTargets.js +168 -0
- package/src/utils/bridgeTools.js +71 -0
- package/src/utils/configChanges.js +30 -0
- package/src/utils/configWriter.js +143 -26
- package/src/utils/cppDts.js +11 -11
- package/src/utils/extensionSteps.js +172 -0
- package/src/utils/nativeImports.js +9 -2
- package/src/utils/nodeDevBuild.js +2 -1
- package/src/utils/openUrl.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/plist.js +5 -0
- package/src/utils/printableText.js +5 -0
- package/src/utils/resolveAppHost.js +7 -0
- package/src/utils/runPackageSource.js +251 -0
- package/src/utils/rustBridgeGen.js +3 -3
- package/src/utils/systemConfig.js +16 -3
- package/src/utils/targetToolchain.js +77 -0
- package/vitest.config.js +6 -1
- package/src/actions/appSetup.js +0 -116
- package/src/actions/appStoreConnect.js +0 -188
- package/src/actions/appUpdate.js +0 -68
- package/src/actions/extensions.js +0 -7
- package/src/actions/googlePlay.js +0 -213
- package/src/actions/googleSignIn.js +0 -78
- package/src/actions/playSetup.js +0 -19
- package/src/actions/publishAndroid.js +0 -102
- package/src/actions/publishIos.js +0 -146
- 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,87 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import writeIfChanged from './writeIfChanged.js';
|
|
4
|
+
import logger from './logger.js';
|
|
5
|
+
import runPackageSource, { runProbe } from './runPackageSource.js';
|
|
6
|
+
import { printable } from './printableText.js';
|
|
7
|
+
|
|
8
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9
|
+
// Enough of a failed build's log to reach the error the tool ends it with.
|
|
10
|
+
const LOG_TAIL_LINES = 60;
|
|
11
|
+
|
|
12
|
+
const isText = (value) => typeof value === 'string' && !value.includes('\0');
|
|
13
|
+
|
|
14
|
+
// A file the tool left in the work directory, as text, or null where there is none. Recipes write there too, so
|
|
15
|
+
// a link, which could name any file the machine's user reads, is refused, as is a path that leaves the directory.
|
|
16
|
+
function readOwn(name, dir, segments) {
|
|
17
|
+
const root = fs.realpathSync(dir);
|
|
18
|
+
const relative = segments.every(isText) ? path.relative(root, path.join(root, ...segments)) : '';
|
|
19
|
+
const file = path.join(root, relative);
|
|
20
|
+
const isInside = relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
21
|
+
if (isInside && !fs.existsSync(file)) return null;
|
|
22
|
+
if (!isInside || fs.realpathSync(file) !== file || !fs.statSync(file).isFile()) {
|
|
23
|
+
throw new Error(`crossbind: ${name} reads ${printable(segments.join('/'))} from its work directory, which is no file of its own there: a link, a folder, or a path that leaves the directory.`);
|
|
24
|
+
}
|
|
25
|
+
return fs.readFileSync(file, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function assertRun(name, command, env) {
|
|
29
|
+
if (!Array.isArray(command) || command.length === 0 || !command.every(isText)) {
|
|
30
|
+
throw new Error(`crossbind: ${name}'s run takes a program and its arguments as a list of strings.`);
|
|
31
|
+
}
|
|
32
|
+
const isEnv = env !== null && typeof env === 'object' && !Array.isArray(env)
|
|
33
|
+
&& Object.entries(env).every(([key, value]) => ENV_NAME.test(key) && isText(value));
|
|
34
|
+
if (!isEnv) throw new Error(`crossbind: ${name}'s run takes env as variable names with their text.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runFailure(name, step, target, program, result, logFile) {
|
|
38
|
+
if (result.error && result.error.code !== 'ENOBUFS') {
|
|
39
|
+
return new Error(`crossbind: could not run ${program} for ${target.path} (${result.error.message}).`);
|
|
40
|
+
}
|
|
41
|
+
let ending = `failed (exit code ${result.status})`;
|
|
42
|
+
if (result.error) ending = 'was stopped: it printed more than crossbind reads';
|
|
43
|
+
else if (result.signal) ending = `was stopped by ${result.signal}`;
|
|
44
|
+
// A tool that says why on stdout alone would end the message with nothing.
|
|
45
|
+
const printed = `${result.stderr ?? ''}`.trim() ? result.stderr : result.stdout;
|
|
46
|
+
const tail = printable(`${printed ?? ''}`).trim().split('\n').slice(-LOG_TAIL_LINES).join('\n');
|
|
47
|
+
return new Error(`crossbind: ${name} ${step} for ${target.path} ${ending}.${logFile ? ` Its whole log is in ${logFile}.` : ''}\n${tail}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// What a package source's steps get of crossbind. In install, with a log file, `run` runs the source's tool
|
|
51
|
+
// where the target's packages build (its image, a remote runner, or the host where isLocal says so), in the
|
|
52
|
+
// run's work directory, and what the tool writes to stderr is the install's log. In inputs the work directory
|
|
53
|
+
// is the source's probe directory and only a tool of the host can be asked.
|
|
54
|
+
export default function packageSourceApi({
|
|
55
|
+
source, config, target, work, logFile = null,
|
|
56
|
+
}) {
|
|
57
|
+
const { name } = source;
|
|
58
|
+
const isLocal = work.runner === 'LOCAL';
|
|
59
|
+
const step = logFile ? 'install' : 'inputs';
|
|
60
|
+
const log = [];
|
|
61
|
+
|
|
62
|
+
function run(command, { env = {} } = {}) {
|
|
63
|
+
assertRun(name, command, env);
|
|
64
|
+
if (!logFile && !isLocal) {
|
|
65
|
+
throw new Error(`crossbind: ${name}'s inputs runs ${command[0]}, which it can where api.isLocal is true alone: elsewhere the image says what runs.`);
|
|
66
|
+
}
|
|
67
|
+
const result = logFile ? runPackageSource(command, {
|
|
68
|
+
source, config, target, work, env,
|
|
69
|
+
}) : runProbe(command, { work, env });
|
|
70
|
+
if (logFile) {
|
|
71
|
+
log.push(`${result.stderr ?? ''}`);
|
|
72
|
+
writeIfChanged(logFile, log.join(''));
|
|
73
|
+
}
|
|
74
|
+
if (result.error || result.status !== 0) throw runFailure(name, step, target, command[0], result, logFile);
|
|
75
|
+
return Object.freeze({ stdout: `${result.stdout ?? ''}`, stderr: `${result.stderr ?? ''}` });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
name,
|
|
80
|
+
isLocal,
|
|
81
|
+
logFile,
|
|
82
|
+
work: Object.freeze({ dir: work.dir, path: work.toolPath, read: (...segments) => readOwn(name, work.dir, segments) }),
|
|
83
|
+
store: Object.freeze({ dir: work.store, path: work.storePath }),
|
|
84
|
+
log: (text) => logger.info(text),
|
|
85
|
+
run,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -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 {
|
|
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('|');
|