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,251 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import getOsUserAndGroupId from './getOsUserAndGroupId.js';
|
|
6
|
+
import makeTreeWritable from './makeTreeWritable.js';
|
|
7
|
+
import pullDockerImage, { getDockerImage, getDockerContainerName, imageRoleFor } from './pullDockerImage.js';
|
|
8
|
+
import { DOCKER_RUN_SECURITY_ARGS } from './dockerSecurity.js';
|
|
9
|
+
import assertExecContainer from './execContainer.js';
|
|
10
|
+
import { IOS_DEVELOPER_DIR, XCODE_TOOLCHAIN_BIN } from './iosToolchain.js';
|
|
11
|
+
import { DARWIN_CC, DARWIN_TOOLS_BIN } from './darwinToolchain.js';
|
|
12
|
+
import { HOST_BUILT_PLATFORMS } from './targets.js';
|
|
13
|
+
import { remoteExecParams } from './remoteRunner.js';
|
|
14
|
+
import { runnerFor } from './selectRunner.js';
|
|
15
|
+
|
|
16
|
+
// Every run of a package source's tool goes through here. The config is passed in instead of read from
|
|
17
|
+
// state, because state attaches the staged packages while it is still being built.
|
|
18
|
+
//
|
|
19
|
+
// The tool runs recipes, which are code, and can write wherever the tool keeps its settings and plugins.
|
|
20
|
+
// So every run gets a fresh work directory, deleted with the run, for whatever of the tool must not
|
|
21
|
+
// outlive it; only the package store does. On the host the environment is rebuilt from an allowlist, so
|
|
22
|
+
// the tool's own variables, a stray CC or a remote's credentials never reach it.
|
|
23
|
+
const ALLOWED_ENV = [
|
|
24
|
+
/^PATH$/,
|
|
25
|
+
/^HOME$/,
|
|
26
|
+
/^USER$/,
|
|
27
|
+
/^LOGNAME$/,
|
|
28
|
+
/^SHELL$/,
|
|
29
|
+
/^TERM$/,
|
|
30
|
+
/^TMPDIR$/,
|
|
31
|
+
/^TEMP$/,
|
|
32
|
+
/^TMP$/,
|
|
33
|
+
/^LANG$/,
|
|
34
|
+
/^LC_[A-Z_]+$/,
|
|
35
|
+
/^(HTTP|HTTPS|ALL|NO)_PROXY$/i,
|
|
36
|
+
/^SSL_CERT_(FILE|DIR)$/,
|
|
37
|
+
/^REQUESTS_CA_BUNDLE$/,
|
|
38
|
+
// A host Emscripten under RUNNER=LOCAL.
|
|
39
|
+
/^EMSDK(_NODE|_PYTHON)?$/,
|
|
40
|
+
/^EM_(CONFIG|CACHE)$/,
|
|
41
|
+
/^SYSTEMROOT$/i,
|
|
42
|
+
/^WINDIR$/i,
|
|
43
|
+
/^USERPROFILE$/i,
|
|
44
|
+
/^PROGRAMDATA$/i,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// What a tool can build on the host: wasm with a host Emscripten, iOS and macOS with Xcode.
|
|
48
|
+
const HOST_PLATFORMS = ['wasm', 'ios', 'darwin'];
|
|
49
|
+
const APPLE_PLATFORM_NAMES = { ios: 'iOS', darwin: 'macOS' };
|
|
50
|
+
// Where a container sees sourceRoot(): an exec container mounts all of it here, a run container only
|
|
51
|
+
// the store and its own work directory, at the same places.
|
|
52
|
+
export const containerRootOf = (name) => `/var/cache/crossbind/${name}`;
|
|
53
|
+
// A recipe's build log is the whole compiler output of a library.
|
|
54
|
+
const MAX_BUFFER = 256 * 1024 * 1024;
|
|
55
|
+
const SPAWN_OPTIONS = { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', maxBuffer: MAX_BUFFER };
|
|
56
|
+
|
|
57
|
+
export function sourceRunner(config, target) {
|
|
58
|
+
const { runner } = runnerFor(imageRoleFor(target), config.system);
|
|
59
|
+
// Xcode runs on the Mac alone, so iOS and macOS packages build there whatever runner the rest use.
|
|
60
|
+
return HOST_BUILT_PLATFORMS.includes(target?.platform) ? 'LOCAL' : runner;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Machine-wide like the cargo cache: built packages are reused by every project, and `pnpm run clear`
|
|
64
|
+
// in one project must not throw them away. Containers write to the store and a tool runs the recipes
|
|
65
|
+
// it keeps there, so a tool on the host never shares a store with them.
|
|
66
|
+
export function sourceRoot(name, runner = 'DOCKER_RUN') {
|
|
67
|
+
return path.join(os.homedir(), '.crossbind', runner === 'LOCAL' ? `${name}-local` : name);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A directory under a source's root, with the store beside it. toolPath and storePath name a file of
|
|
71
|
+
// each as the tool sees it.
|
|
72
|
+
function workAt(name, runner, relative) {
|
|
73
|
+
const root = sourceRoot(name, runner);
|
|
74
|
+
const viewOf = (base) => (...segments) => (runner === 'LOCAL'
|
|
75
|
+
? path.join(root, base, ...segments)
|
|
76
|
+
: [containerRootOf(name), base, ...segments].join('/'));
|
|
77
|
+
const toolPath = viewOf(relative);
|
|
78
|
+
return {
|
|
79
|
+
name, runner, root, store: path.join(root, 'store'), dir: path.join(root, relative), toolDir: toolPath(), toolPath, storePath: viewOf('store'),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The directory one run reads its inputs from and writes its outputs to.
|
|
84
|
+
export function createSourceWork(name, runner) {
|
|
85
|
+
const root = sourceRoot(name, runner);
|
|
86
|
+
fs.mkdirSync(path.join(root, 'store'), { recursive: true });
|
|
87
|
+
fs.mkdirSync(path.join(root, 'work'), { recursive: true });
|
|
88
|
+
return workAt(name, runner, `work/${path.basename(fs.mkdtempSync(path.join(root, 'work', 'run-')))}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Where a source asks the tool of the host what it is, outside any run. Only named where an image says what runs:
|
|
92
|
+
// nothing is asked there, and a build that finds every target staged touches nothing of the home.
|
|
93
|
+
export function createSourceProbe(name, runner) {
|
|
94
|
+
if (runner === 'LOCAL') fs.mkdirSync(path.join(sourceRoot(name, runner), 'probe'), { recursive: true });
|
|
95
|
+
return workAt(name, runner, 'probe');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function removeTree(dir) {
|
|
99
|
+
if (!fs.existsSync(dir)) return;
|
|
100
|
+
makeTreeWritable(dir);
|
|
101
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const removeSourceWork = (work) => removeTree(work.dir);
|
|
105
|
+
|
|
106
|
+
// What runs that were killed left behind. Only while holding the install lock: no run is going then.
|
|
107
|
+
export const clearSourceWork = (name, runner) => removeTree(path.join(sourceRoot(name, runner), 'work'));
|
|
108
|
+
|
|
109
|
+
// A tool reports paths as it sees them. In a container that is through the mounts, which all sit under
|
|
110
|
+
// the container's root, so mapping the prefix back is exact; a path anywhere else is none crossbind gave it.
|
|
111
|
+
export function toHostPath(reported, work) {
|
|
112
|
+
if (work.runner === 'LOCAL') return path.resolve(reported);
|
|
113
|
+
const containerRoot = containerRootOf(work.name);
|
|
114
|
+
const clean = path.posix.normalize(reported);
|
|
115
|
+
// A backslash is a separator again once the path is joined on a Windows host.
|
|
116
|
+
if (reported.includes('\\') || !clean.startsWith(`${containerRoot}/`)) {
|
|
117
|
+
throw new Error(`crossbind: ${work.name} reported ${JSON.stringify(reported)}, outside the directories crossbind mounts for it.`);
|
|
118
|
+
}
|
|
119
|
+
return path.join(work.root, clean.slice(containerRoot.length + 1));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function allowedEnv() {
|
|
123
|
+
return Object.fromEntries(Object.entries(process.env).filter(([key]) => ALLOWED_ENV.some((allowed) => allowed.test(key))));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// The Xcode every crossbind iOS archive builds with, whichever one xcode-select points at.
|
|
127
|
+
const iosEnv = () => ({ ...allowedEnv(), DEVELOPER_DIR: IOS_DEVELOPER_DIR });
|
|
128
|
+
|
|
129
|
+
// A GNU ar or nm ahead of Apple's on a Mac's PATH (Homebrew's binutils) writes archives Apple's linker
|
|
130
|
+
// cannot read, and recipes, the build tools they build and Meson all take these tools by name. So the
|
|
131
|
+
// run gets links to Xcode's first on its PATH; for macOS to the ones xcode-select picks, as the macOS
|
|
132
|
+
// ports are built with.
|
|
133
|
+
const APPLE_TOOLS = {
|
|
134
|
+
ar: 'ar', as: 'as', nm: 'llvm-nm', ranlib: 'ranlib', strip: 'strip',
|
|
135
|
+
};
|
|
136
|
+
const appleToolOf = (target, name) => (target.platform === 'darwin'
|
|
137
|
+
? path.join(DARWIN_TOOLS_BIN, name)
|
|
138
|
+
: path.join(XCODE_TOOLCHAIN_BIN, APPLE_TOOLS[name]));
|
|
139
|
+
|
|
140
|
+
function withAppleTools(work, env, target) {
|
|
141
|
+
if (process.platform !== 'darwin') return env;
|
|
142
|
+
const dir = path.join(work.dir, 'apple-tools');
|
|
143
|
+
if (!fs.existsSync(dir)) {
|
|
144
|
+
fs.mkdirSync(dir);
|
|
145
|
+
Object.keys(APPLE_TOOLS).forEach((name) => fs.symlinkSync(appleToolOf(target, name), path.join(dir, name)));
|
|
146
|
+
}
|
|
147
|
+
return { ...env, PATH: [dir, env.PATH].filter(Boolean).join(path.delimiter) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const toolVersions = new Map();
|
|
151
|
+
// The first line a tool prints for --version, asked once.
|
|
152
|
+
function toolVersion(command, env = allowedEnv()) {
|
|
153
|
+
const key = command.join(' ');
|
|
154
|
+
if (!toolVersions.has(key)) {
|
|
155
|
+
const [program, ...args] = command;
|
|
156
|
+
const result = spawnSync(program, [...args, '--version'], { encoding: 'utf8', env });
|
|
157
|
+
toolVersions.set(key, `${result.stdout ?? ''}`.split('\n')[0].trim());
|
|
158
|
+
}
|
|
159
|
+
return toolVersions.get(key);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The compiler a tool on the host builds with; for a container the image digest says the same.
|
|
163
|
+
export function localCompilerIdentity(target) {
|
|
164
|
+
if (target?.platform === 'ios') return toolVersion(['xcrun', 'clang'], iosEnv());
|
|
165
|
+
if (target?.platform === 'darwin') return toolVersion([DARWIN_CC]);
|
|
166
|
+
return toolVersion(['emcc']);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// What a tool on the host cannot build: the targets an image's toolchain builds, and Xcode's off a Mac.
|
|
170
|
+
export function assertRunsOnHost(name, target) {
|
|
171
|
+
if (!HOST_PLATFORMS.includes(target.platform)) {
|
|
172
|
+
throw new Error(`crossbind: ${name} packages for ${target.platform} build in the ${imageRoleFor(target)} image, which RUNNER=LOCAL does not use.`);
|
|
173
|
+
}
|
|
174
|
+
if (HOST_BUILT_PLATFORMS.includes(target.platform) && process.platform !== 'darwin') {
|
|
175
|
+
throw new Error(`crossbind: ${name} packages for ${APPLE_PLATFORM_NAMES[target.platform]} build with Xcode, on a Mac.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Of a store, a remote runner sends back the folders its source says never change once written, and nothing else.
|
|
180
|
+
export function assertRunsRemotely({ name, packageSource }) {
|
|
181
|
+
if (packageSource.storeUnits.length > 0) return;
|
|
182
|
+
throw new Error(`crossbind: ${name} names no storeUnits, so a remote runner would send none of its packages back: name the folders of its store that never change once written, as 'p/*/p', or build with another RUNNER.`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// A tool of the host asked what it is, in a source's probe directory: no target's tools are on its PATH.
|
|
186
|
+
export function runProbe([program, ...args], { work, env = {} }) {
|
|
187
|
+
return spawnSync(program, args, { ...SPAWN_OPTIONS, cwd: work.dir, env: { ...allowedEnv(), ...env } });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default function runPackageSource(command, {
|
|
191
|
+
source, config, target, work, env = {},
|
|
192
|
+
}) {
|
|
193
|
+
const [program, ...args] = command;
|
|
194
|
+
const role = imageRoleFor(target);
|
|
195
|
+
if (work.runner === 'LOCAL') {
|
|
196
|
+
const hostEnv = target.platform === 'ios' ? iosEnv() : allowedEnv();
|
|
197
|
+
return spawnSync(program, args, { ...SPAWN_OPTIONS, cwd: work.dir, env: withAppleTools(work, { ...hostEnv, ...env }, target) });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Google ships the linux NDK for x86_64 only.
|
|
201
|
+
const platform = target.platform === 'android' ? 'linux/amd64' : undefined;
|
|
202
|
+
if (work.runner === 'REMOTE') {
|
|
203
|
+
return spawnSync(...remoteExecParams({
|
|
204
|
+
remote: runnerFor(role, config.system).remote,
|
|
205
|
+
role,
|
|
206
|
+
image: getDockerImage(role, platform),
|
|
207
|
+
mounts: [
|
|
208
|
+
// Of the store, only the folders the source names come back: they never change once written.
|
|
209
|
+
{ host: work.store, container: work.storePath(), inputRoots: [], outputRoots: source.packageSource.storeUnits },
|
|
210
|
+
{ host: work.dir, container: work.toolDir, inputRoots: ['.'], outputRoots: ['.'] },
|
|
211
|
+
],
|
|
212
|
+
cwd: work.toolDir,
|
|
213
|
+
argv: command,
|
|
214
|
+
env,
|
|
215
|
+
}, SPAWN_OPTIONS));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const containerRoot = containerRootOf(source.name);
|
|
219
|
+
const envArgs = Object.entries(env).flatMap(([key, value]) => ['-e', `${key}=${value}`]);
|
|
220
|
+
let runnerArgs;
|
|
221
|
+
if (work.runner === 'DOCKER_EXEC') {
|
|
222
|
+
const name = getDockerContainerName(config.paths.base, role);
|
|
223
|
+
assertExecContainer({
|
|
224
|
+
name,
|
|
225
|
+
mounts: [[containerRoot, work.root]],
|
|
226
|
+
workdir: work.toolDir,
|
|
227
|
+
hint: `Recreate it with \`crossbind docker stop ${role}\`, \`crossbind docker delete ${role}\` and \`crossbind docker create ${role}\`, which mount ${work.root} at ${containerRoot}.`,
|
|
228
|
+
mountNote: `It predates the ${source.name} mount.`,
|
|
229
|
+
});
|
|
230
|
+
runnerArgs = ['exec', ...envArgs, '--user', getOsUserAndGroupId(), '--workdir', work.toolDir, name];
|
|
231
|
+
} else {
|
|
232
|
+
pullDockerImage(role, platform);
|
|
233
|
+
runnerArgs = [
|
|
234
|
+
'run',
|
|
235
|
+
'--rm',
|
|
236
|
+
...(platform ? ['--platform', platform] : []),
|
|
237
|
+
...DOCKER_RUN_SECURITY_ARGS,
|
|
238
|
+
'-v',
|
|
239
|
+
`${work.store}:${work.storePath()}`,
|
|
240
|
+
'-v',
|
|
241
|
+
`${work.dir}:${work.toolDir}`,
|
|
242
|
+
...envArgs,
|
|
243
|
+
'--user',
|
|
244
|
+
getOsUserAndGroupId(),
|
|
245
|
+
'--workdir',
|
|
246
|
+
work.toolDir,
|
|
247
|
+
getDockerImage(role, platform),
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
return spawnSync('docker', [...runnerArgs, ...command], SPAWN_OPTIONS);
|
|
251
|
+
}
|
|
@@ -267,7 +267,7 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
|
|
|
267
267
|
'',
|
|
268
268
|
'[dependencies]',
|
|
269
269
|
`embind-rs = { path = "${relPath(dir, resolveEmbindRsDir())}" }`,
|
|
270
|
-
// The app config's
|
|
270
|
+
// The app config's packages.cargo, so an app-local surface can use
|
|
271
271
|
// upstream crates directly (values: a version string, or a verbatim `{ ... }` spec).
|
|
272
272
|
...Object.entries(cargoDependencies).map(([name, spec]) => (
|
|
273
273
|
String(spec).trim().startsWith('{') ? `${name} = ${spec}` : `${name} = "${spec}"`
|
|
@@ -295,7 +295,7 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
|
|
|
295
295
|
return { bridgeDir: dir, crateName: `${stem}_crossbind_app`, model };
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
// Direct CRATE import (`import { X } from 'cargo:uuid'` with
|
|
298
|
+
// Direct CRATE import (`import { X } from 'cargo:uuid'` with `packages.cargo`
|
|
299
299
|
// declaring `uuid`, or `cargo:<crate>/<module>` for one public module): no surface file and no
|
|
300
300
|
// package - the bridge is generated from the upstream crate's OWN multi-file source. cargo
|
|
301
301
|
// metadata (which fetches on first run) locates the source and the resolved feature set; the
|
|
@@ -608,7 +608,7 @@ function findModule(lib, modulePath, enabled, srcDir) {
|
|
|
608
608
|
: undefined;
|
|
609
609
|
file = decl && modFile(file, name);
|
|
610
610
|
if (!file) {
|
|
611
|
-
throw new Error(`crossbind: rust bridge: module '${where}' not found in ${srcDir} - a module behind a cargo feature needs that feature in
|
|
611
|
+
throw new Error(`crossbind: rust bridge: module '${where}' not found in ${srcDir} - a module behind a cargo feature needs that feature in packages.cargo, and inline mod { } blocks are not read`);
|
|
612
612
|
}
|
|
613
613
|
if (!decl.isPub) throw new Error(`crossbind: rust bridge: module '${where}' in ${srcDir} is private to the crate`);
|
|
614
614
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { WASM_EXCEPTION_FLAGS, targetArchiveFlags } from './archiveFlags.js';
|
|
2
|
+
import { ANDROID_NDK, ANDROID_API_LEVEL } from './androidToolchain.js';
|
|
3
|
+
import { IOS_DEPLOYMENT_TARGET } from './iosToolchain.js';
|
|
4
|
+
import {
|
|
5
|
+
LINUX_ARCHIVE_FLAGS, linuxBuildEnv, linuxTool, linuxToolchainFile, linuxTriple,
|
|
6
|
+
} from './linuxToolchain.js';
|
|
7
|
+
import {
|
|
8
|
+
DARWIN_CC, DARWIN_CXX, DARWIN_DEPLOYMENT_TARGET, DARWIN_HOST_PACKAGE_PREFIXES,
|
|
9
|
+
} from './darwinToolchain.js';
|
|
10
|
+
import {
|
|
11
|
+
windowsBuildEnv, windowsTool, windowsToolchainFile, windowsTriple,
|
|
12
|
+
} from './windowsToolchain.js';
|
|
13
|
+
|
|
14
|
+
// What crossbind builds a target's own archives with, as plain data: a package source builds its packages to match,
|
|
15
|
+
// since they link into the same module.
|
|
16
|
+
const CPP_STANDARD = '20';
|
|
17
|
+
const NDK_BIN = `${ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin`;
|
|
18
|
+
const LINUX_LIBCS = { linux: 'glibc', linuxmusl: 'musl' };
|
|
19
|
+
|
|
20
|
+
const linux = (target) => ({
|
|
21
|
+
flags: LINUX_ARCHIVE_FLAGS,
|
|
22
|
+
compilers: { c: linuxTool(target, 'clang'), cpp: linuxTool(target, 'clang++') },
|
|
23
|
+
env: linuxBuildEnv(target),
|
|
24
|
+
cmakeToolchainFile: linuxToolchainFile(target),
|
|
25
|
+
triple: linuxTriple(target),
|
|
26
|
+
libc: LINUX_LIBCS[target.platform],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const PLATFORMS = {
|
|
30
|
+
wasm: () => ({
|
|
31
|
+
flags: WASM_EXCEPTION_FLAGS,
|
|
32
|
+
compilers: { c: 'emcc', cpp: 'em++' },
|
|
33
|
+
env: {
|
|
34
|
+
CC: 'emcc', CXX: 'em++', AR: 'emar', NM: 'emnm', RANLIB: 'emranlib', STRIP: 'emstrip',
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
android: () => ({
|
|
38
|
+
compilers: { c: `${NDK_BIN}/clang`, cpp: `${NDK_BIN}/clang++` },
|
|
39
|
+
ndk: ANDROID_NDK,
|
|
40
|
+
apiLevel: ANDROID_API_LEVEL,
|
|
41
|
+
}),
|
|
42
|
+
// No compilers: Xcode's own build it, which xcrun finds for the SDK the arch names.
|
|
43
|
+
ios: () => ({ deploymentTarget: IOS_DEPLOYMENT_TARGET }),
|
|
44
|
+
linux,
|
|
45
|
+
linuxmusl: linux,
|
|
46
|
+
darwin: () => ({
|
|
47
|
+
compilers: { c: DARWIN_CC, cpp: DARWIN_CXX },
|
|
48
|
+
env: { PKG_CONFIG_LIBDIR: '' },
|
|
49
|
+
deploymentTarget: DARWIN_DEPLOYMENT_TARGET,
|
|
50
|
+
ignoredPrefixes: [...DARWIN_HOST_PACKAGE_PREFIXES],
|
|
51
|
+
}),
|
|
52
|
+
win32: (target) => ({
|
|
53
|
+
compilers: {
|
|
54
|
+
c: windowsTool(target, 'clang'), cpp: windowsTool(target, 'clang++'), rc: windowsTool(target, 'windres'),
|
|
55
|
+
},
|
|
56
|
+
env: windowsBuildEnv(target),
|
|
57
|
+
cmakeToolchainFile: windowsToolchainFile(target),
|
|
58
|
+
triple: windowsTriple(target),
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const TOOLCHAIN_PLATFORMS = Object.keys(PLATFORMS);
|
|
63
|
+
|
|
64
|
+
// Null for a platform package sources build nothing for yet.
|
|
65
|
+
export function targetToolchain(target) {
|
|
66
|
+
const own = PLATFORMS[target.platform]?.(target);
|
|
67
|
+
if (!own) return null;
|
|
68
|
+
return {
|
|
69
|
+
platform: target.platform,
|
|
70
|
+
arch: target.arch,
|
|
71
|
+
runtime: target.runtime,
|
|
72
|
+
cppStandard: CPP_STANDARD,
|
|
73
|
+
env: {},
|
|
74
|
+
...own,
|
|
75
|
+
flags: [...(own.flags ?? []), ...targetArchiveFlags(target)],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import attachConanDependencies from './attachConanDependencies.js';
|
|
2
|
-
import { conanStageDir } from '../utils/conanImport.js';
|
|
3
|
-
import { conanManifestsStamp } from '../utils/conanStage.js';
|
|
4
|
-
|
|
5
|
-
const attachedFrom = new WeakMap();
|
|
6
|
-
|
|
7
|
-
// Attaches what a build staged since this config last attached: a process that loaded state before
|
|
8
|
-
// the build, such as a Metro server, would otherwise bind headers without the packages. Says whether
|
|
9
|
-
// it attached again.
|
|
10
|
-
export default function refreshConanDependencies(config) {
|
|
11
|
-
if (Object.keys(config.conanDependencies ?? {}).length === 0) return false;
|
|
12
|
-
const stamp = conanManifestsStamp(conanStageDir(config.paths.cache));
|
|
13
|
-
if (attachedFrom.get(config) === stamp) return false;
|
|
14
|
-
attachedFrom.set(config, stamp);
|
|
15
|
-
attachConanDependencies(config);
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { CONAN_NAME } from './conanImport.js';
|
|
2
|
-
import { getContentHash } from './hash.js';
|
|
3
|
-
|
|
4
|
-
// A plain version or a Conan range; a user, channel or recipe revision belongs in conan.lock instead.
|
|
5
|
-
export const CONAN_VERSION = /^[A-Za-z0-9_][A-Za-z0-9_.+-]*$/;
|
|
6
|
-
const VERSION_RANGE = /^\[[^\]\n]+\]$/;
|
|
7
|
-
const OPTION_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8
|
-
const OPTION_VALUE_TYPES = ['string', 'number', 'boolean'];
|
|
9
|
-
|
|
10
|
-
export function normalizeConanDependencies(raw) {
|
|
11
|
-
if (raw === undefined) return {};
|
|
12
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
13
|
-
throw new Error('crossbind: "conanDependencies" must be an object such as { zlib: \'1.3.1\' }.');
|
|
14
|
-
}
|
|
15
|
-
return Object.fromEntries(Object.entries(raw).map(([name, spec]) => [name, normalizeSpec(name, spec)]));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function normalizeSpec(name, spec) {
|
|
19
|
-
if (!CONAN_NAME.test(name)) {
|
|
20
|
-
throw new Error(`crossbind: conanDependencies key '${name}' is not a Conan package name (lower-case letters, digits and _ + . -).`);
|
|
21
|
-
}
|
|
22
|
-
const { version, options = {} } = typeof spec === 'string' ? { version: spec } : (spec ?? {});
|
|
23
|
-
if (typeof version !== 'string' || !(CONAN_VERSION.test(version) || VERSION_RANGE.test(version))) {
|
|
24
|
-
throw new Error(`crossbind: conanDependencies.${name} needs a version such as '1.3.1' or a range such as '[>=1.3 <2]' (got ${JSON.stringify(version)}).`);
|
|
25
|
-
}
|
|
26
|
-
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
|
|
27
|
-
throw new Error(`crossbind: conanDependencies.${name}.options must be an object such as { enable_fts5: true }.`);
|
|
28
|
-
}
|
|
29
|
-
Object.entries(options).forEach(([key, value]) => {
|
|
30
|
-
if (key === 'shared') {
|
|
31
|
-
throw new Error(`crossbind: conanDependencies.${name}.options.shared - crossbind links Conan packages statically into the module and sets shared=False itself.`);
|
|
32
|
-
}
|
|
33
|
-
if (!OPTION_NAME.test(key)) {
|
|
34
|
-
throw new Error(`crossbind: conanDependencies.${name}.options - '${key}' is not an option name.`);
|
|
35
|
-
}
|
|
36
|
-
if (!OPTION_VALUE_TYPES.includes(typeof value)) {
|
|
37
|
-
throw new Error(`crossbind: conanDependencies.${name}.options.${key} must be a string, number or boolean.`);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
return { version, options: { ...options } };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// What staged packages were installed for; a change to conanDependencies makes them stale.
|
|
44
|
-
export const conanDependenciesKey = (dependencies) => getContentHash(JSON.stringify(dependencies));
|
|
45
|
-
|
|
46
|
-
// What an app's headers and sources see of its Conan packages: the packages are staged after a header
|
|
47
|
-
// may already be bound, and restaged in place when one moves to another version. Null without any.
|
|
48
|
-
export function conanInputsOf(config) {
|
|
49
|
-
if (Object.keys(config.conanDependencies ?? {}).length === 0) return null;
|
|
50
|
-
return {
|
|
51
|
-
dependencies: config.conanDependencies,
|
|
52
|
-
refs: config.allDependencies.filter((d) => d.general.conan).map((d) => d.general.conan.ref),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export const conanRequires = (dependencies) => Object.entries(dependencies).map(([name, { version }]) => `${name}/${version}`);
|
|
57
|
-
|
|
58
|
-
function optionValue(value) {
|
|
59
|
-
if (value === true) return 'True';
|
|
60
|
-
if (value === false) return 'False';
|
|
61
|
-
return String(value);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export const conanOptionArgs = (dependencies) => Object.entries(dependencies).flatMap(([name, { options }]) => (
|
|
65
|
-
Object.entries(options).flatMap(([key, value]) => ['-o:h', `${name}/*:${key}=${optionValue(value)}`])
|
|
66
|
-
));
|
package/src/utils/conanImport.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
|
|
3
|
-
export const CONAN_PREFIX = 'conan:';
|
|
4
|
-
// Conan 2 refuses upper-case package names.
|
|
5
|
-
export const CONAN_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 conanStageDir = (cacheDir) => `${cacheDir}/conan`;
|
|
10
|
-
// Apart from the stage's own files, so no package name can land on one of them.
|
|
11
|
-
export const conanPackageDir = (stageDir, name) => `${stageDir}/packages/${name}`;
|
|
12
|
-
|
|
13
|
-
export function parseConanImport(source, headerExtensions) {
|
|
14
|
-
const [name, ...headerPath] = source.slice(CONAN_PREFIX.length).split('/');
|
|
15
|
-
if (!CONAN_NAME.test(name)) {
|
|
16
|
-
throw new Error(`crossbind: '${source}' - '${name}' is not a Conan package name; write conan:<package>/<header>, such as conan:zlib/zlib.h.`);
|
|
17
|
-
}
|
|
18
|
-
if (headerPath.length === 0) {
|
|
19
|
-
throw new Error(`crossbind: '${source}' names no header - import one of the package's headers as conan:${name}/<header>.`);
|
|
20
|
-
}
|
|
21
|
-
const bad = headerPath.find((segment) => !HEADER_SEGMENT.test(segment));
|
|
22
|
-
if (bad !== undefined) {
|
|
23
|
-
throw new Error(`crossbind: '${source}' - '${bad}' is not a header path segment.`);
|
|
24
|
-
}
|
|
25
|
-
const header = headerPath.join('/');
|
|
26
|
-
const extension = header.includes('.') ? header.split('.').at(-1) : '';
|
|
27
|
-
if (!headerExtensions.includes(extension)) {
|
|
28
|
-
throw new Error(`crossbind: '${source}' - a conan: import names a header (${headerExtensions.map((e) => `.${e}`).join(', ')}).`);
|
|
29
|
-
}
|
|
30
|
-
return { name, header };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// The inverse of resolveConanHeader, for the editor types of a staged header.
|
|
34
|
-
export function conanImportOfHeader(file, cacheDir) {
|
|
35
|
-
const prefix = conanPackageDir(conanStageDir(cacheDir), '');
|
|
36
|
-
if (!file.startsWith(prefix)) return null;
|
|
37
|
-
const match = file.slice(prefix.length).match(/^([^/]+)\/dist\/prebuilt\/[^/]+\/include\/(.+)$/);
|
|
38
|
-
// Both end up in the module name of the generated types.
|
|
39
|
-
if (!match || !CONAN_NAME.test(match[1]) || !match[2].split('/').every((segment) => HEADER_SEGMENT.test(segment))) return null;
|
|
40
|
-
return { name: match[1], header: match[2] };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Only declared packages are importable, as with cargo:; a package pulled in by another links but stays hidden.
|
|
44
|
-
export function resolveConanHeader(config, source, target) {
|
|
45
|
-
const { name, header } = parseConanImport(source, config.ext.header);
|
|
46
|
-
if (!Object.hasOwn(config.conanDependencies ?? {}, name)) {
|
|
47
|
-
throw new Error(`crossbind: '${source}' is not declared - add '${name}' to conanDependencies in crossbind.config.`);
|
|
48
|
-
}
|
|
49
|
-
const dependency = config.allDependencies.find((d) => d.general.conan?.name === name);
|
|
50
|
-
if (!dependency) {
|
|
51
|
-
throw new Error(`crossbind: '${source}' - ${name} is declared in conanDependencies but not installed; crossbind installs Conan packages when a build starts.`);
|
|
52
|
-
}
|
|
53
|
-
const tried = [...new Set([target.releasePath, target.path].filter(Boolean))]
|
|
54
|
-
.map((targetPath) => `${dependency.paths.output}/prebuilt/${targetPath}/include/${header}`);
|
|
55
|
-
const found = tried.find((file) => fs.existsSync(file));
|
|
56
|
-
if (found) return found;
|
|
57
|
-
throw new Error(`crossbind: '${source}' - ${name} has no ${header}. Tried:\n${tried.map((file) => ` - ${file}`).join('\n')}`);
|
|
58
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
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 runConan, {
|
|
10
|
-
clearConanWork, conanRoot, conanRunner, createConanWork, localToolchainIdentity, removeConanWork, toHostPath,
|
|
11
|
-
} from './runConan.js';
|
|
12
|
-
import { hostProfile, settingsUser, BUILD_PROFILE } from './conanProfile.js';
|
|
13
|
-
import { conanRequires, conanOptionArgs, conanDependenciesKey } from './conanDependencies.js';
|
|
14
|
-
import {
|
|
15
|
-
conanPackagesOf, stageConanPackage, writeConanManifest, readConanManifest, isControlCharacter,
|
|
16
|
-
} from './conanStage.js';
|
|
17
|
-
import { conanStageDir } from './conanImport.js';
|
|
18
|
-
|
|
19
|
-
// Bumped when the stage changes shape, so a stage an older crossbind made is made again.
|
|
20
|
-
const CONAN_STAGE_FORMAT = 4;
|
|
21
|
-
// Enough of a failed build's log to reach the error conan ends it with.
|
|
22
|
-
const LOG_TAIL_LINES = 60;
|
|
23
|
-
|
|
24
|
-
const lockFileOf = (config) => upath.join(config.paths.project, 'conan.lock');
|
|
25
|
-
const stampFileOf = (stageDir, target) => upath.join(stageDir, 'stamps', `${target.path}.json`);
|
|
26
|
-
const logFileOf = (stageDir, target) => upath.join(stageDir, 'logs', `${target.path}.log`);
|
|
27
|
-
const printable = (text) => [...text].filter((char) => char === '\n' || char === '\t' || !isControlCharacter(char)).join('');
|
|
28
|
-
|
|
29
|
-
// What a staged target was made from. The toolchain is the image, or the host's compiler and conan where
|
|
30
|
-
// conan runs on the host (RUNNER=LOCAL, and iOS always): the profile asks that compiler for its version,
|
|
31
|
-
// so a new one is a new package id and must restage.
|
|
32
|
-
function stampKey(config, target) {
|
|
33
|
-
const lock = lockFileOf(config);
|
|
34
|
-
return getContentHash(JSON.stringify({
|
|
35
|
-
format: CONAN_STAGE_FORMAT,
|
|
36
|
-
dependencies: config.conanDependencies,
|
|
37
|
-
host: hostProfile(target),
|
|
38
|
-
build: BUILD_PROFILE,
|
|
39
|
-
toolchain: conanRunner(config, target) === 'LOCAL' ? localToolchainIdentity(target) : getDockerImage(imageRoleFor(target)),
|
|
40
|
-
lock: fs.existsSync(lock) ? fs.readFileSync(lock, 'utf8') : null,
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// The target's manifest names the dependencies it was staged for.
|
|
45
|
-
const isStaged = (config, stageDir, target) => loadJson(stampFileOf(stageDir, target))?.key === stampKey(config, target)
|
|
46
|
-
&& readConanManifest(stageDir, conanDependenciesKey(config.conanDependencies), target.path) !== null;
|
|
47
|
-
|
|
48
|
-
function conanFailure(target, result, logFile) {
|
|
49
|
-
if (result.error && result.error.code !== 'ENOBUFS') {
|
|
50
|
-
return new Error(`crossbind: could not run conan for ${target.path} (${result.error.message}).`);
|
|
51
|
-
}
|
|
52
|
-
let ending = `failed (exit code ${result.status})`;
|
|
53
|
-
if (result.error) ending = 'was stopped: it printed more than crossbind reads';
|
|
54
|
-
else if (result.signal) ending = `was stopped by ${result.signal}`;
|
|
55
|
-
const tail = printable(`${result.stderr ?? ''}`).trim().split('\n').slice(-LOG_TAIL_LINES).join('\n');
|
|
56
|
-
return new Error(`crossbind: conan install for ${target.path} ${ending}. Its whole log is in ${logFile}.\n${tail}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// The parsed file, or null. The parse error is left out: it quotes the text, which recipes can write.
|
|
60
|
-
function readJson(file) {
|
|
61
|
-
try {
|
|
62
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// The lockfile comes back from where the recipes ran, so only a lockfile goes into the project.
|
|
69
|
-
function lockfileText(file) {
|
|
70
|
-
const lock = readJson(file);
|
|
71
|
-
if (typeof lock?.version !== 'string') {
|
|
72
|
-
throw new Error('crossbind: conan wrote no lockfile crossbind can read, so conan.lock is left as it was.');
|
|
73
|
-
}
|
|
74
|
-
return `${JSON.stringify(lock, null, 4)}\n`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function installTarget(config, stageDir, target) {
|
|
78
|
-
const dependencies = config.conanDependencies;
|
|
79
|
-
const lock = lockFileOf(config);
|
|
80
|
-
const hasLock = fs.existsSync(lock);
|
|
81
|
-
const stampFile = stampFileOf(stageDir, target);
|
|
82
|
-
const logFile = logFileOf(stageDir, target);
|
|
83
|
-
// A restage that stops halfway must not pass for a finished one.
|
|
84
|
-
fs.rmSync(stampFile, { force: true });
|
|
85
|
-
const work = createConanWork(conanRunner(config, target));
|
|
86
|
-
const label = `conan ${target.path}`;
|
|
87
|
-
logger.startTask(label);
|
|
88
|
-
try {
|
|
89
|
-
fs.writeFileSync(upath.join(work.dir, 'host.profile'), hostProfile(target));
|
|
90
|
-
fs.writeFileSync(upath.join(work.dir, 'build.profile'), BUILD_PROFILE);
|
|
91
|
-
const settings = settingsUser(target);
|
|
92
|
-
if (settings) fs.writeFileSync(upath.join(work.dir, 'home', 'settings_user.yml'), settings);
|
|
93
|
-
if (hasLock) fs.copyFileSync(lock, upath.join(work.dir, 'input.lock'));
|
|
94
|
-
const result = runConan([
|
|
95
|
-
'install',
|
|
96
|
-
...conanRequires(dependencies).flatMap((reference) => ['--requires', reference]),
|
|
97
|
-
...conanOptionArgs(dependencies),
|
|
98
|
-
'-pr:h', work.conanPath('host.profile'),
|
|
99
|
-
'-pr:b', work.conanPath('build.profile'),
|
|
100
|
-
'--output-folder', work.conanPath('output'),
|
|
101
|
-
'--build=missing',
|
|
102
|
-
'--format=json',
|
|
103
|
-
// A file, since a build tool writing to stdout would end up inside the JSON.
|
|
104
|
-
'--out-file', work.conanPath('graph.json'),
|
|
105
|
-
// Unless told which lockfile to use, conan takes any conan.lock in its working directory.
|
|
106
|
-
...(hasLock ? ['--lockfile', work.conanPath('input.lock'), '--lockfile-partial'] : ['--lockfile=']),
|
|
107
|
-
'--lockfile-out', work.conanPath('resolution.lock'),
|
|
108
|
-
], { config, target, work });
|
|
109
|
-
writeIfChanged(logFile, `${result.stderr ?? ''}`);
|
|
110
|
-
if (result.error || result.status !== 0) throw conanFailure(target, result, logFile);
|
|
111
|
-
|
|
112
|
-
const graph = readJson(upath.join(work.dir, 'graph.json'))?.graph;
|
|
113
|
-
if (!graph) throw new Error(`crossbind: conan install wrote no graph crossbind can read. Its log is in ${logFile}.`);
|
|
114
|
-
const packages = conanPackagesOf(graph);
|
|
115
|
-
const distCmake = fs.readFileSync(upath.join(config.paths.cli, 'assets', 'cmake', 'dist.cmake'), 'utf8');
|
|
116
|
-
packages.forEach((pkg) => stageConanPackage(pkg, {
|
|
117
|
-
stageDir, targetPath: target.path, toHost: (reported) => toHostPath(reported, work), store: work.store, distCmake,
|
|
118
|
-
}));
|
|
119
|
-
writeConanManifest(stageDir, target.path, {
|
|
120
|
-
key: conanDependenciesKey(dependencies),
|
|
121
|
-
packages: packages.map(({
|
|
122
|
-
packageFolder, includedirs, libdirs, ...entry
|
|
123
|
-
}) => entry),
|
|
124
|
-
});
|
|
125
|
-
writeIfChanged(lock, lockfileText(upath.join(work.dir, 'resolution.lock')));
|
|
126
|
-
writeIfChanged(stampFile, `${JSON.stringify({ key: stampKey(config, target) })}\n`);
|
|
127
|
-
logger.doneTask(label, packages.map((pkg) => `${pkg.name} ${pkg.version}`).join(', '));
|
|
128
|
-
} finally {
|
|
129
|
-
// Not over the error that ended the run; the next install clears what is left.
|
|
130
|
-
try {
|
|
131
|
-
removeConanWork(work);
|
|
132
|
-
} catch (e) {
|
|
133
|
-
logger.info(`crossbind: could not remove ${work.dir} (${e.message}).`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Installs the declared Conan packages for every target not staged from the same inputs yet, and
|
|
139
|
-
// says whether it staged any, so the caller attaches the new manifest.
|
|
140
|
-
export default async function installConanPackages(config, targets) {
|
|
141
|
-
if (Object.keys(config.conanDependencies ?? {}).length === 0) return false;
|
|
142
|
-
const stageDir = conanStageDir(config.paths.cache);
|
|
143
|
-
const pending = targets.filter((target) => !isStaged(config, stageDir, target));
|
|
144
|
-
if (pending.length === 0) return false;
|
|
145
|
-
for (const runner of new Set(pending.map((target) => conanRunner(config, target)))) {
|
|
146
|
-
// A conan store takes one writer at a time, and every project on the machine shares it.
|
|
147
|
-
await withDirLock(`${conanRoot(runner)}-install.lock`, async () => {
|
|
148
|
-
clearConanWork(runner);
|
|
149
|
-
pending.filter((target) => conanRunner(config, target) === runner && !isStaged(config, stageDir, target))
|
|
150
|
-
.forEach((target) => installTarget(config, stageDir, target));
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
return true;
|
|
154
|
-
}
|