crossbind 2.0.0-beta.69 → 2.0.0-beta.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/package.json +3 -2
  2. package/src/actions/addExtension.js +8 -4
  3. package/src/actions/buildApp.js +38 -9
  4. package/src/actions/buildDependencies.js +3 -3
  5. package/src/actions/createImportedBridges.js +10 -8
  6. package/src/actions/createInterface.js +23 -21
  7. package/src/actions/createLib.js +3 -3
  8. package/src/actions/extensionCommands.js +2 -1
  9. package/src/actions/getAllBridges.js +20 -5
  10. package/src/actions/getDependLibs.js +6 -0
  11. package/src/actions/getLinkInputs.js +2 -1
  12. package/src/actions/gradleProject.js +13 -2
  13. package/src/actions/licenses.js +30 -24
  14. package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
  15. package/src/actions/writeRuntimeEntry.js +2 -2
  16. package/src/actions/xcodeProject.js +12 -6
  17. package/src/app/index.d.ts +19 -0
  18. package/src/app/index.js +32 -7
  19. package/src/assets/node-import-hooks.mjs +7 -6
  20. package/src/bin.js +37 -17
  21. package/src/index.js +4 -3
  22. package/src/integration/getCrossbindScript.js +3 -2
  23. package/src/integration/getDependFilePath.js +10 -10
  24. package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
  25. package/src/state/index.js +14 -11
  26. package/src/state/loadConfig.js +10 -4
  27. package/src/state/refreshPackageSources.js +19 -0
  28. package/src/utils/appApiHeader.js +5 -5
  29. package/src/utils/appBuild.js +8 -0
  30. package/src/utils/appExtensions.js +44 -28
  31. package/src/utils/appOptions.js +31 -10
  32. package/src/utils/appProject.js +24 -0
  33. package/src/utils/appRustCrates.js +2 -2
  34. package/src/utils/bridgeTools.js +6 -1
  35. package/src/utils/cppDts.js +11 -11
  36. package/src/utils/extensionSteps.js +38 -1
  37. package/src/utils/nativeImports.js +9 -2
  38. package/src/utils/nodeDevBuild.js +2 -1
  39. package/src/utils/packageSourceApi.js +87 -0
  40. package/src/utils/packageSourceImport.js +69 -0
  41. package/src/utils/packageSourceInstall.js +162 -0
  42. package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
  43. package/src/utils/packageSources.js +108 -0
  44. package/src/utils/printableText.js +5 -0
  45. package/src/utils/resolveAppHost.js +10 -0
  46. package/src/utils/runPackageSource.js +251 -0
  47. package/src/utils/rustBridgeGen.js +3 -3
  48. package/src/utils/targetToolchain.js +77 -0
  49. package/src/utils/xcframeworkSlice.js +6 -0
  50. package/src/state/refreshConanDependencies.js +0 -17
  51. package/src/utils/conanDependencies.js +0 -66
  52. package/src/utils/conanImport.js +0 -58
  53. package/src/utils/conanInstall.js +0 -154
  54. package/src/utils/conanProfile.js +0 -203
  55. package/src/utils/runConan.js +0 -257
@@ -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
- }
@@ -1,203 +0,0 @@
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
- // The app's own sources compile as C++20 (assets/cmake/CMakeLists.txt), so C++ packages do too.
15
- const CPP_STANDARD = '20';
16
- // The flags change the ABI without changing a setting, and glibc and musl builds share every setting,
17
- // so these have to change the package id. A conf the profile leaves unset adds nothing to it.
18
- const PACKAGE_ID_CONFS = ['tools.build:cflags', 'tools.build:cxxflags', 'tools.build:exelinkflags', 'tools.build:sharedlinkflags', 'user.crossbind:libc'];
19
-
20
- // Conan renders profiles as Jinja templates: these read the toolchain of whatever runs Conan, the image
21
- // or the host (under RUNNER=LOCAL, and for iOS and macOS).
22
- const EMCC_VERSION = "{{ subprocess.check_output(['emcc', '-dumpversion'], text=True).strip() }}";
23
- const EMSCRIPTEN_TOOLCHAIN = "{{ subprocess.check_output(['em-config', 'EMSCRIPTEN_ROOT'], text=True).strip() }}/cmake/Modules/Platform/Emscripten.cmake";
24
- const clangMajorVersion = (clang) => `{{ subprocess.check_output(['${clang}', '-dumpversion'], text=True).split('.')[0] }}`;
25
- const NDK_CLANG_VERSION = clangMajorVersion(`${ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang`);
26
- const XCODE_CLANG_VERSION = "{{ subprocess.check_output(['xcrun', 'clang', '-dumpversion'], text=True).split('.')[0] }}";
27
- const CMAKE_VERSION = "{{ subprocess.check_output(['cmake', '--version'], text=True).split()[2] }}";
28
-
29
- const ANDROID_ARCHS = { 'arm64-v8a': 'armv8', x86_64: 'x86_64' };
30
- // Node's names for the desktop arches, in Conan's spelling.
31
- const NODE_ARCHS = { x64: 'x86_64', arm64: 'armv8' };
32
- const LINUX_LIBCS = { linux: 'glibc', linuxmusl: 'musl' };
33
- const envLines = (env) => Object.entries(env).map(([variable, value]) => `${variable}=${value}`);
34
-
35
- // The image's clang wrappers through its CMake toolchain file for the triple, as a port archive for an
36
- // addon is built. The image runs no musl program, so a musl build is a cross build even on a machine of
37
- // the same arch, which Conan would take for a native one; configure is told the triple itself.
38
- const LINUX = {
39
- settings: (target) => [
40
- 'os=Linux',
41
- `arch=${NODE_ARCHS[target.arch]}`,
42
- 'compiler=clang',
43
- `compiler.version=${clangMajorVersion(linuxTool(target, 'clang'))}`,
44
- 'compiler.libcxx=libc++',
45
- `compiler.cppstd=${CPP_STANDARD}`,
46
- ],
47
- conf: (target) => [
48
- `tools.build:compiler_executables={'c': '${linuxTool(target, 'clang')}', 'cpp': '${linuxTool(target, 'clang++')}'}`,
49
- `tools.cmake.cmaketoolchain:user_toolchain=["${linuxToolchainFile(target)}"]`,
50
- `tools.gnu:host_triplet=${linuxTriple(target)}`,
51
- ...(target.platform === 'linuxmusl' ? ['tools.build.cross_building:cross_build=True'] : []),
52
- `user.crossbind:libc=${LINUX_LIBCS[target.platform]}`,
53
- ],
54
- buildenv: (target) => envLines(linuxBuildEnv(target)),
55
- flags: LINUX_ARCHIVE_FLAGS,
56
- };
57
-
58
- // What differs per platform; the rest of the host profile is shared.
59
- const PLATFORMS = {
60
- wasm: {
61
- settings: (target) => [
62
- 'os=Emscripten',
63
- `arch=${target.arch === 'wasm64' ? 'wasm64' : 'wasm'}`,
64
- 'compiler=emcc',
65
- `compiler.version=${EMCC_VERSION}`,
66
- 'compiler.libcxx=libc++',
67
- `compiler.cppstd=${CPP_STANDARD}`,
68
- ...(target.runtime === 'mt' ? ['compiler.threads=posix'] : []),
69
- ],
70
- conf: () => [
71
- "tools.build:compiler_executables={'c': 'emcc', 'cpp': 'em++'}",
72
- `tools.cmake.cmaketoolchain:user_toolchain=["${EMSCRIPTEN_TOOLCHAIN}"]`,
73
- ],
74
- buildenv: () => ['CC=emcc', 'CXX=em++', 'AR=emar', 'NM=emnm', 'RANLIB=emranlib', 'STRIP=emstrip'],
75
- flags: WASM_EXCEPTION_FLAGS,
76
- },
77
- // The image's NDK through its own CMake toolchain and default STL, as a port archive is built.
78
- android: {
79
- settings: (target) => [
80
- 'os=Android',
81
- `os.api_level=${ANDROID_API_LEVEL}`,
82
- `arch=${ANDROID_ARCHS[target.arch]}`,
83
- 'compiler=clang',
84
- `compiler.version=${NDK_CLANG_VERSION}`,
85
- 'compiler.libcxx=c++_static',
86
- `compiler.cppstd=${CPP_STANDARD}`,
87
- ],
88
- conf: () => [`tools.android:ndk_path=${ANDROID_NDK}`],
89
- buildenv: () => [],
90
- flags: [],
91
- },
92
- // Xcode's clang for the target's SDK, arm64 only like every crossbind iOS slice. No bitcode flag:
93
- // Xcode 27 ld takes the "marker" of -fembed-bitcode-marker for a file when CMake links its check.
94
- ios: {
95
- settings: (target) => [
96
- 'os=iOS',
97
- `os.version=${IOS_DEPLOYMENT_TARGET}`,
98
- `os.sdk=${target.arch}`,
99
- 'arch=armv8',
100
- 'compiler=apple-clang',
101
- `compiler.version=${XCODE_CLANG_VERSION}`,
102
- 'compiler.libcxx=libc++',
103
- `compiler.cppstd=${CPP_STANDARD}`,
104
- ],
105
- conf: () => [],
106
- buildenv: () => [],
107
- flags: [],
108
- },
109
- linux: LINUX,
110
- linuxmusl: LINUX,
111
- // The clang xcode-select picks, for the macOS the addons support, as a port archive for a macOS addon
112
- // is built; the machine's own packages stay out of reach.
113
- darwin: {
114
- settings: (target) => [
115
- 'os=Macos',
116
- `os.version=${DARWIN_DEPLOYMENT_TARGET}`,
117
- `arch=${NODE_ARCHS[target.arch]}`,
118
- 'compiler=apple-clang',
119
- `compiler.version=${clangMajorVersion(DARWIN_CC)}`,
120
- 'compiler.libcxx=libc++',
121
- `compiler.cppstd=${CPP_STANDARD}`,
122
- ],
123
- conf: () => [
124
- `tools.build:compiler_executables={'c': '${DARWIN_CC}', 'cpp': '${DARWIN_CXX}'}`,
125
- `tools.cmake.cmaketoolchain:extra_variables={'CMAKE_IGNORE_PREFIX_PATH': '${DARWIN_HOST_PACKAGE_PREFIXES.join(';')}'}`,
126
- ],
127
- buildenv: () => ['PKG_CONFIG_LIBDIR='],
128
- flags: [],
129
- },
130
- // The windows image's llvm-mingw through its CMake toolchain file for the triple, as a port archive for
131
- // an addon is built. No compiler.runtime: that would make it a clang standing in for MSVC.
132
- win32: {
133
- settings: (target) => [
134
- 'os=Windows',
135
- `arch=${NODE_ARCHS[target.arch]}`,
136
- 'compiler=clang',
137
- `compiler.version=${clangMajorVersion(windowsTool(target, 'clang'))}`,
138
- 'compiler.libcxx=libc++',
139
- `compiler.cppstd=${CPP_STANDARD}`,
140
- ],
141
- conf: (target) => [
142
- `tools.build:compiler_executables={'c': '${windowsTool(target, 'clang')}', 'cpp': '${windowsTool(target, 'clang++')}', 'rc': '${windowsTool(target, 'windres')}'}`,
143
- `tools.cmake.cmaketoolchain:user_toolchain=["${windowsToolchainFile(target)}"]`,
144
- `tools.gnu:host_triplet=${windowsTriple(target)}`,
145
- ],
146
- buildenv: (target) => envLines(windowsBuildEnv(target)),
147
- flags: [],
148
- },
149
- };
150
-
151
- export const BUILD_PROFILE = [
152
- '{% set compiler, version, compiler_exe = detect_api.detect_default_compiler() %}',
153
- '[settings]',
154
- 'os={{ detect_api.detect_os() }}',
155
- 'arch={{ detect_api.detect_arch() }}',
156
- 'build_type=Release',
157
- 'compiler={{ compiler }}',
158
- 'compiler.version={{ detect_api.default_compiler_version(compiler, version) }}',
159
- 'compiler.cppstd={{ detect_api.default_cppstd(compiler, version) }}',
160
- 'compiler.libcxx={{ detect_api.detect_libcxx(compiler, version, compiler_exe) }}',
161
- '',
162
- ].join('\n');
163
-
164
- const list = (values) => JSON.stringify(values);
165
-
166
- // Conan checks compiler.version against the versions its own release lists, and a compiler newer than the
167
- // Conan release lists none for it: Xcode's on the host, or an image's after a toolchain bump. Added to the
168
- // run's home, this lets any version of the profile's clang through.
169
- export function settingsUser(target) {
170
- const compiler = PLATFORMS[target.platform]?.settings(target).find((setting) => setting.startsWith('compiler='))?.slice('compiler='.length);
171
- return ['clang', 'apple-clang'].includes(compiler) ? `compiler:\n ${compiler}:\n version: ["ANY"]\n` : null;
172
- }
173
-
174
- export function hostProfile(target) {
175
- const platform = PLATFORMS[target.platform];
176
- if (!platform) {
177
- const names = Object.keys(PLATFORMS);
178
- throw new Error(`crossbind: conan: 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 conanDependencies.`);
179
- }
180
- const flags = list([...platform.flags, ...targetArchiveFlags(target)]);
181
- const buildenv = platform.buildenv(target);
182
- return [
183
- '[settings]',
184
- ...platform.settings(target),
185
- 'build_type=Release',
186
- '',
187
- '[options]',
188
- '*:shared=False',
189
- '',
190
- '[conf]',
191
- ...platform.conf(target),
192
- `tools.build:cflags=${flags}`,
193
- `tools.build:cxxflags=${flags}`,
194
- `tools.build:exelinkflags=${flags}`,
195
- `tools.build:sharedlinkflags=${flags}`,
196
- `tools.info.package_id:confs=${list(PACKAGE_ID_CONFS)}`,
197
- '',
198
- ...(buildenv.length > 0 ? ['[buildenv]', ...buildenv, ''] : []),
199
- '[platform_tool_requires]',
200
- `cmake/${CMAKE_VERSION}`,
201
- '',
202
- ].join('\n');
203
- }
@@ -1,257 +0,0 @@
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 conan invocation crossbind makes goes through here. The config is passed in instead of read
17
- // from state, because state attaches the staged Conan packages while it is still being built.
18
- //
19
- // A Conan home holds code conan runs (plugins, hooks) and settings every recipe obeys (global.conf,
20
- // remotes), and the recipes it builds can write to it. So every run gets a home of its own, made in a
21
- // fresh work directory and deleted with it; only the package store outlives a run. On the host the
22
- // environment is rebuilt from an allowlist, so CONAN_DEFAULT_PROFILE, a stray CC or a remote's
23
- // credentials never reach conan.
24
- const ALLOWED_ENV = [
25
- /^PATH$/,
26
- /^HOME$/,
27
- /^USER$/,
28
- /^LOGNAME$/,
29
- /^SHELL$/,
30
- /^TERM$/,
31
- /^TMPDIR$/,
32
- /^TEMP$/,
33
- /^TMP$/,
34
- /^LANG$/,
35
- /^LC_[A-Z_]+$/,
36
- /^(HTTP|HTTPS|ALL|NO)_PROXY$/i,
37
- /^SSL_CERT_(FILE|DIR)$/,
38
- /^REQUESTS_CA_BUNDLE$/,
39
- // A host Emscripten under RUNNER=LOCAL.
40
- /^EMSDK(_NODE|_PYTHON)?$/,
41
- /^EM_(CONFIG|CACHE)$/,
42
- /^SYSTEMROOT$/i,
43
- /^WINDIR$/i,
44
- /^USERPROFILE$/i,
45
- /^PROGRAMDATA$/i,
46
- ];
47
-
48
- // What conan can build on the host: wasm with a host Emscripten, iOS and macOS with Xcode.
49
- const HOST_PLATFORMS = ['wasm', 'ios', 'darwin'];
50
- const APPLE_PLATFORM_NAMES = { ios: 'iOS', darwin: 'macOS' };
51
- // Where a container sees conanRoot(): an exec container mounts all of it here, a run container only
52
- // the store and its own work directory, at the same places.
53
- const CONTAINER_ROOT = '/var/cache/crossbind/conan';
54
- // The package folders conan reports, which the host stages from: downloaded binaries sit in p/<ref>/p, ones
55
- // conan built in b/<ref>/p. Sources and build folders stay on a remote runner.
56
- const REMOTE_STORE_OUTPUTS = ['p/*/p', 'b/*/p'];
57
- // A recipe's build log is the whole compiler output of a library.
58
- const MAX_BUFFER = 256 * 1024 * 1024;
59
- // The JSON graph carries conandata, the source URL and SHA-256 of the license rows, from 2.19 on.
60
- const MIN_CONAN_VERSION = [2, 19];
61
-
62
- export function conanRunner(config, target) {
63
- const { runner } = runnerFor(imageRoleFor(target), config.system);
64
- // Xcode runs on the Mac alone, so iOS and macOS packages build there whatever runner the rest use.
65
- return HOST_BUILT_PLATFORMS.includes(target?.platform) ? 'LOCAL' : runner;
66
- }
67
-
68
- // Machine-wide like the cargo cache: built packages are reused by every project, and `pnpm run clear`
69
- // in one project must not throw them away. Containers write to the store and conan runs the recipes
70
- // it keeps there, so conan on the host never shares a store with them.
71
- export function conanRoot(runner = 'DOCKER_RUN') {
72
- return path.join(os.homedir(), '.crossbind', runner === 'LOCAL' ? 'conan-local' : 'conan');
73
- }
74
-
75
- // The directory one conan run reads its inputs from and writes its outputs to, with the home it runs
76
- // with. conanPath names a file of it as conan sees it.
77
- export function createConanWork(runner) {
78
- const root = conanRoot(runner);
79
- const store = path.join(root, 'store');
80
- fs.mkdirSync(store, { recursive: true });
81
- fs.mkdirSync(path.join(root, 'work'), { recursive: true });
82
- const dir = fs.mkdtempSync(path.join(root, 'work', 'run-'));
83
- const conanDir = runner === 'LOCAL' ? dir : `${CONTAINER_ROOT}/work/${path.basename(dir)}`;
84
- const conanPath = (file) => (runner === 'LOCAL' ? path.join(dir, file) : `${conanDir}/${file}`);
85
- // conan takes its home from the first .conanrc at or above its working directory, ahead of
86
- // CONAN_HOME; one in the working directory itself is always that first one.
87
- fs.writeFileSync(path.join(dir, '.conanrc'), 'conan_home=./home\n');
88
- fs.mkdirSync(path.join(dir, 'home'));
89
- fs.writeFileSync(path.join(dir, 'home', 'global.conf'), [
90
- `core.cache:storage_path=${runner === 'LOCAL' ? store : `${CONTAINER_ROOT}/store`}`,
91
- 'core:non_interactive=True',
92
- '',
93
- ].join('\n'));
94
- return {
95
- runner, root, store, dir, conanDir, conanPath,
96
- };
97
- }
98
-
99
- function removeTree(dir) {
100
- if (!fs.existsSync(dir)) return;
101
- makeTreeWritable(dir);
102
- fs.rmSync(dir, { recursive: true, force: true });
103
- }
104
-
105
- export const removeConanWork = (work) => removeTree(work.dir);
106
-
107
- // What runs that were killed left behind. Only while holding the install lock: no run is going then.
108
- export const clearConanWork = (runner) => removeTree(path.join(conanRoot(runner), 'work'));
109
-
110
- // conan reports paths as it sees them. In a container that is through the mounts, which all sit under
111
- // CONTAINER_ROOT, so mapping the prefix back is exact; a path anywhere else is none crossbind gave it.
112
- export function toHostPath(reported, work) {
113
- if (work.runner === 'LOCAL') return path.resolve(reported);
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(`${CONTAINER_ROOT}/`)) {
117
- throw new Error(`crossbind: conan reported ${JSON.stringify(reported)}, outside the directories crossbind mounts for it.`);
118
- }
119
- return path.join(work.root, clean.slice(CONTAINER_ROOT.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(), cwd = undefined) {
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, cwd });
157
- toolVersions.set(key, `${result.stdout ?? ''}`.split('\n')[0].trim());
158
- }
159
- return toolVersions.get(key);
160
- }
161
-
162
- // conan migrates the home it starts in, even for --version, so it is asked from a home of crossbind's
163
- // instead of the user's ~/.conan2.
164
- function conanVersion() {
165
- const dir = path.join(conanRoot('LOCAL'), 'probe');
166
- fs.mkdirSync(dir, { recursive: true });
167
- fs.writeFileSync(path.join(dir, '.conanrc'), 'conan_home=./home\n');
168
- return toolVersion(['conan'], allowedEnv(), dir);
169
- }
170
-
171
- function compilerIdentity(target) {
172
- if (target?.platform === 'ios') return toolVersion(['xcrun', 'clang'], iosEnv());
173
- if (target?.platform === 'darwin') return toolVersion([DARWIN_CC]);
174
- return toolVersion(['emcc']);
175
- }
176
-
177
- // What a host conan builds with; for a container the image digest says the same.
178
- export function localToolchainIdentity(target) {
179
- return `${compilerIdentity(target)}\n${conanVersion()}`;
180
- }
181
-
182
- function assertLocalConan(target) {
183
- const conan = conanVersion();
184
- const [major, minor] = (conan.match(/(\d+)\.(\d+)/) ?? []).slice(1).map(Number);
185
- const [needMajor, needMinor] = MIN_CONAN_VERSION;
186
- if (major > needMajor || (major === needMajor && minor >= needMinor)) return;
187
- const need = `Conan ${MIN_CONAN_VERSION.join('.')} or later; found ${conan || 'none'}.`;
188
- throw new Error(HOST_BUILT_PLATFORMS.includes(target.platform)
189
- ? `crossbind: Conan packages for ${APPLE_PLATFORM_NAMES[target.platform]} build on this Mac, with the conan on the PATH, which must be ${need} Install or upgrade it with \`brew install conan\` (\`brew upgrade conan\`) or \`pipx install conan\`.`
190
- : `crossbind: RUNNER=LOCAL runs conan from the PATH and needs ${need}`);
191
- }
192
-
193
- export default function runConan(args, { config, target, work }) {
194
- const options = { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', maxBuffer: MAX_BUFFER };
195
- const role = imageRoleFor(target);
196
- if (work.runner === 'LOCAL') {
197
- if (!HOST_PLATFORMS.includes(target.platform)) {
198
- throw new Error(`crossbind: Conan packages for ${target.platform} build in the ${role} image, which RUNNER=LOCAL does not use.`);
199
- }
200
- if (HOST_BUILT_PLATFORMS.includes(target.platform) && process.platform !== 'darwin') {
201
- throw new Error(`crossbind: Conan packages for ${APPLE_PLATFORM_NAMES[target.platform]} build with Xcode, on a Mac.`);
202
- }
203
- assertLocalConan(target);
204
- const env = withAppleTools(work, { ...(target.platform === 'ios' ? iosEnv() : allowedEnv()), CONAN_HOME: path.join(work.dir, 'home') }, target);
205
- return spawnSync('conan', args, { ...options, cwd: work.dir, env });
206
- }
207
-
208
- // Google ships the linux NDK for x86_64 only.
209
- const platform = target.platform === 'android' ? 'linux/amd64' : undefined;
210
- if (work.runner === 'REMOTE') {
211
- return spawnSync(...remoteExecParams({
212
- remote: runnerFor(role, config.system).remote,
213
- role,
214
- image: getDockerImage(role, platform),
215
- mounts: [
216
- { host: work.store, container: `${CONTAINER_ROOT}/store`, inputRoots: [], outputRoots: REMOTE_STORE_OUTPUTS },
217
- { host: work.dir, container: work.conanDir, inputRoots: ['.'], outputRoots: ['.'] },
218
- ],
219
- cwd: work.conanDir,
220
- argv: ['conan', ...args],
221
- env: { CONAN_HOME: work.conanPath('home') },
222
- }, options));
223
- }
224
-
225
- const env = ['-e', `CONAN_HOME=${work.conanPath('home')}`];
226
- let runnerArgs;
227
- if (work.runner === 'DOCKER_EXEC') {
228
- const name = getDockerContainerName(config.paths.base, role);
229
- assertExecContainer({
230
- name,
231
- mounts: [[CONTAINER_ROOT, work.root]],
232
- workdir: work.conanDir,
233
- hint: `Recreate it with \`crossbind docker stop ${role}\`, \`crossbind docker delete ${role}\` and \`crossbind docker create ${role}\`, which mount ${work.root} at ${CONTAINER_ROOT}.`,
234
- mountNote: 'It predates the conan mount.',
235
- });
236
- runnerArgs = ['exec', ...env, '--user', getOsUserAndGroupId(), '--workdir', work.conanDir, name];
237
- } else {
238
- pullDockerImage(role, platform);
239
- runnerArgs = [
240
- 'run',
241
- '--rm',
242
- ...(platform ? ['--platform', platform] : []),
243
- ...DOCKER_RUN_SECURITY_ARGS,
244
- '-v',
245
- `${work.store}:${CONTAINER_ROOT}/store`,
246
- '-v',
247
- `${work.dir}:${work.conanDir}`,
248
- ...env,
249
- '--user',
250
- getOsUserAndGroupId(),
251
- '--workdir',
252
- work.conanDir,
253
- getDockerImage(role, platform),
254
- ];
255
- }
256
- return spawnSync('docker', [...runnerArgs, 'conan', ...args], options);
257
- }