wyvrnpm 2.0.4 → 2.3.2

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 (44) hide show
  1. package/README.md +639 -5
  2. package/bin/wyvrn.js +220 -10
  3. package/claude/skills/wyvrnpm.skill +0 -0
  4. package/cmake/cpp.cmake +9 -0
  5. package/cmake/functions.cmake +224 -0
  6. package/cmake/macros.cmake +233 -0
  7. package/cmake/options.cmake +23 -0
  8. package/cmake/variables.cmake +171 -0
  9. package/package.json +3 -1
  10. package/src/build/cache.js +148 -0
  11. package/src/build/clone.js +170 -0
  12. package/src/build/cmake.js +342 -0
  13. package/src/build/index.js +275 -0
  14. package/src/build/msvc-env.js +217 -0
  15. package/src/build/recipe.js +155 -0
  16. package/src/commands/build.js +283 -0
  17. package/src/commands/clean.js +56 -16
  18. package/src/commands/configure.js +6 -5
  19. package/src/commands/init.js +3 -2
  20. package/src/commands/install-skill.js +107 -0
  21. package/src/commands/install.js +262 -19
  22. package/src/commands/link.js +18 -15
  23. package/src/commands/profile.js +15 -12
  24. package/src/commands/publish.js +216 -65
  25. package/src/commands/show.js +237 -0
  26. package/src/compat.js +261 -0
  27. package/src/config.js +3 -1
  28. package/src/download.js +431 -87
  29. package/src/ignore.js +118 -0
  30. package/src/logger.js +94 -0
  31. package/src/manifest.js +12 -7
  32. package/src/options.js +303 -0
  33. package/src/profile.js +56 -4
  34. package/src/providers/base.js +16 -1
  35. package/src/providers/file.js +12 -6
  36. package/src/providers/http.js +15 -10
  37. package/src/providers/s3.js +14 -9
  38. package/src/resolve.js +179 -19
  39. package/src/toolchain/deps.js +164 -0
  40. package/src/toolchain/index.js +141 -0
  41. package/src/toolchain/presets.js +263 -0
  42. package/src/toolchain/template.cmake +66 -0
  43. package/src/upload-built.js +256 -0
  44. package/src/version-range.js +301 -0
@@ -0,0 +1,170 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+
7
+ const log = require('../logger');
8
+
9
+ /**
10
+ * Spawn a command with inherited stdio, resolve on exit 0, reject otherwise.
11
+ *
12
+ * @param {string} cmd
13
+ * @param {string[]} args
14
+ * @param {{ cwd?: string }} [opts]
15
+ * @returns {Promise<void>}
16
+ */
17
+ function run(cmd, args, opts = {}) {
18
+ return new Promise((resolve, reject) => {
19
+ const proc = spawn(cmd, args, { stdio: 'inherit', ...opts });
20
+ proc.on('error', (err) => reject(new Error(
21
+ `Failed to spawn ${cmd}: ${err.message} (is it installed and on PATH?)`,
22
+ )));
23
+ proc.on('close', (code) => {
24
+ if (code === 0) resolve();
25
+ else reject(new Error(`${cmd} ${args.join(' ')} exited ${code}`));
26
+ });
27
+ });
28
+ }
29
+
30
+ /**
31
+ * Ensure `gitRepo` is cloned into `destDir` at commit `gitSha`.
32
+ *
33
+ * Semantics:
34
+ * - If destDir does not exist → fresh clone + checkout gitSha.
35
+ * - If destDir exists and the HEAD commit already matches gitSha → no-op.
36
+ * - If destDir exists with a different HEAD → fetch + checkout the right
37
+ * SHA, reusing already-downloaded git objects.
38
+ * - If reuse fails for any reason (corrupted cache, unreachable SHA),
39
+ * blow away the cache and fresh-clone.
40
+ *
41
+ * Each path tries a fast shallow fetch by SHA first, then falls back to a
42
+ * full fetch. Some git servers disable `uploadpack.allowReachableSHA1InWant`
43
+ * (notably older Bitbucket Server installs), which makes direct SHA fetches
44
+ * fail with "upload-pack: not our ref"; the full-fetch fallback handles it.
45
+ *
46
+ * Credentials are delegated entirely to the user's ambient git config
47
+ * (SSH keys, credential helpers). We do NOT manage auth.
48
+ *
49
+ * @param {{ gitRepo: string, gitSha: string, destDir: string }} args
50
+ * @returns {Promise<void>}
51
+ */
52
+ async function cloneAtSha({ gitRepo, gitSha, destDir }) {
53
+ if (!gitRepo) throw new Error('cloneAtSha: gitRepo is required');
54
+ if (!gitSha) throw new Error('cloneAtSha: gitSha is required');
55
+
56
+ // Reuse existing clone if possible.
57
+ if (fs.existsSync(path.join(destDir, '.git'))) {
58
+ try {
59
+ await reuseExistingClone({ destDir, gitSha, gitRepo });
60
+ return;
61
+ } catch (err) {
62
+ // An unreachable-commit error is fatal and won't be helped by rebuilding.
63
+ if (/not reachable/.test(err.message)) throw err;
64
+ log.warn(` cached clone unusable (${err.message}); rebuilding`);
65
+ fs.rmSync(destDir, { recursive: true, force: true });
66
+ }
67
+ }
68
+
69
+ await freshClone({ gitRepo, gitSha, destDir });
70
+ }
71
+
72
+ /**
73
+ * Verify that `gitSha` is present and reachable in `destDir`. Throws with an
74
+ * actionable error message when it isn't — the common root cause is an
75
+ * unpushed publisher-side commit or a feature branch that was deleted after
76
+ * publish, leaving the commit dangling on origin.
77
+ */
78
+ async function verifyShaPresent({ destDir, gitSha, gitRepo }) {
79
+ try {
80
+ await run('git', ['cat-file', '-e', `${gitSha}^{commit}`], { cwd: destDir });
81
+ } catch {
82
+ throw new Error(
83
+ `commit ${gitSha.slice(0, 12)} is not reachable from any branch or tag on ${gitRepo}. ` +
84
+ `Likely cause: the publisher's HEAD was not pushed, OR the commit was on a ` +
85
+ `feature branch that was deleted/squash-merged after publish. ` +
86
+ `Fix: the package maintainer should republish from a commit that is pushed ` +
87
+ `and reachable from a remote branch or tag.`,
88
+ );
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Checkout `gitSha` in an existing clone. Tries shallow-SHA fetch first,
94
+ * then full fetch (+ tags), then verifies reachability before checkout.
95
+ */
96
+ async function reuseExistingClone({ destDir, gitSha, gitRepo }) {
97
+ const current = await getHeadSha(destDir);
98
+ if (current === gitSha) return;
99
+
100
+ log.info(` reusing existing clone, switching to ${gitSha.slice(0, 12)}`);
101
+ try {
102
+ // Fast path: shallow fetch the specific SHA directly.
103
+ await run('git', ['fetch', '--depth', '1', 'origin', gitSha], { cwd: destDir });
104
+ } catch {
105
+ // Fallback: full fetch + tags. Fetching tags catches commits that live
106
+ // only under `refs/tags/*` — e.g. a release commit whose branch was
107
+ // deleted but whose tag remains.
108
+ log.info(' shallow SHA fetch unsupported by server; fetching all refs + tags (slower)');
109
+ await run('git', ['fetch', '--tags', 'origin'], { cwd: destDir });
110
+ }
111
+ await verifyShaPresent({ destDir, gitSha, gitRepo });
112
+ await run('git', ['checkout', '--detach', gitSha], { cwd: destDir });
113
+ }
114
+
115
+ /**
116
+ * Fresh clone. Tries `init + remote add + shallow-fetch <sha>` first, falls
117
+ * back to a full `git clone --tags` if the server refuses direct-SHA
118
+ * fetches. Verifies the commit is reachable before attempting checkout so
119
+ * the failure mode is a clear error rather than a cryptic `unable to read
120
+ * tree`.
121
+ */
122
+ async function freshClone({ gitRepo, gitSha, destDir }) {
123
+ fs.mkdirSync(destDir, { recursive: true });
124
+ log.info(` cloning ${gitRepo} at ${gitSha.slice(0, 12)} → ${destDir}`);
125
+ try {
126
+ await run('git', ['init', '--quiet'], { cwd: destDir });
127
+ await run('git', ['remote', 'add', 'origin', gitRepo], { cwd: destDir });
128
+ await run('git', ['fetch', '--depth', '1', 'origin', gitSha], { cwd: destDir });
129
+ await verifyShaPresent({ destDir, gitSha, gitRepo });
130
+ await run('git', ['checkout', '--detach', gitSha], { cwd: destDir });
131
+ } catch (err) {
132
+ // If the shallow-SHA fetch itself failed (server refuses it), fall
133
+ // through to a full clone. If instead the SHA-reachability check failed,
134
+ // re-throw with the actionable message — a full clone wouldn't help.
135
+ if (/not reachable/.test(err.message)) throw err;
136
+
137
+ log.warn(` shallow fetch by SHA failed (${err.message}); falling back to full clone`);
138
+ fs.rmSync(destDir, { recursive: true, force: true });
139
+ fs.mkdirSync(destDir, { recursive: true });
140
+ // `--tags` brings down tag refs in the same round-trip so tag-only
141
+ // commits are reachable too.
142
+ await run('git', ['clone', '--quiet', '--tags', gitRepo, '.'], { cwd: destDir });
143
+ await verifyShaPresent({ destDir, gitSha, gitRepo });
144
+ await run('git', ['checkout', '--detach', gitSha], { cwd: destDir });
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Return the current HEAD commit SHA of a git working tree, or null on error.
150
+ * @param {string} dir
151
+ * @returns {Promise<string|null>}
152
+ */
153
+ function getHeadSha(dir) {
154
+ return new Promise((resolve) => {
155
+ const proc = spawn('git', ['rev-parse', 'HEAD'], {
156
+ cwd: dir,
157
+ stdio: ['ignore', 'pipe', 'ignore'],
158
+ });
159
+ let out = '';
160
+ proc.stdout.on('data', (c) => { out += c.toString(); });
161
+ proc.on('error', () => resolve(null));
162
+ proc.on('close', (code) => {
163
+ if (code !== 0) return resolve(null);
164
+ const sha = out.trim();
165
+ resolve(/^[0-9a-f]{40}$/i.test(sha) ? sha : null);
166
+ });
167
+ });
168
+ }
169
+
170
+ module.exports = { cloneAtSha, getHeadSha };
@@ -0,0 +1,342 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+
7
+ const { loadMsvcEnv } = require('./msvc-env');
8
+ const log = require('../logger');
9
+
10
+ /**
11
+ * CMake configure + build + install orchestration for the source-build path.
12
+ *
13
+ * Separation of concerns:
14
+ * - This module does NOT compute CMake flags from a profile. That is the
15
+ * toolchain generator's job ([src/toolchain/index.js]). We take a
16
+ * pre-generated toolchain file path and hand it to CMake.
17
+ * - This module DOES translate a recipe (`{ generator, configure,
18
+ * buildArgs, installDir, sourceSubdir }`) into `cmake` argv.
19
+ */
20
+
21
+ const DEFAULT_CONFIG = 'Release';
22
+
23
+ /**
24
+ * Probe whether a tool is available on PATH by spawning it with `--version`.
25
+ * Uses `shell: true` on Windows so `.cmd`/`.bat` shims resolve correctly.
26
+ *
27
+ * @param {string} cmd
28
+ * @returns {Promise<boolean>}
29
+ */
30
+ function isToolOnPath(cmd) {
31
+ return new Promise((resolve) => {
32
+ const proc = spawn(cmd, ['--version'], {
33
+ stdio: 'ignore',
34
+ shell: process.platform === 'win32',
35
+ });
36
+ proc.on('error', () => resolve(false));
37
+ proc.on('close', (code) => resolve(code === 0));
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Classify a CMake generator's availability on this machine. Tri-state so
43
+ * `pickGenerator` can distinguish "known-available" (take it) from
44
+ * "known-unavailable" (skip) from "unknown name" (use only if nothing else
45
+ * is confirmed — CMake will error itself if it can't handle the name).
46
+ *
47
+ * @param {string} name
48
+ * @returns {Promise<'yes'|'no'|'unknown'>}
49
+ */
50
+ async function classifyGenerator(name) {
51
+ if (!name) return 'no';
52
+ if (name === 'Ninja' || name === 'Ninja Multi-Config') {
53
+ return (await isToolOnPath('ninja')) ? 'yes' : 'no';
54
+ }
55
+ if (name.startsWith('Visual Studio')) {
56
+ // CMake itself validates the exact VS version at configure time.
57
+ return process.platform === 'win32' ? 'yes' : 'no';
58
+ }
59
+ if (name === 'Unix Makefiles') {
60
+ return (await isToolOnPath('make')) ? 'yes' : 'no';
61
+ }
62
+ if (name === 'MinGW Makefiles' || name === 'MSYS Makefiles') {
63
+ if (process.platform !== 'win32') return 'no';
64
+ return (await isToolOnPath('mingw32-make') || await isToolOnPath('make')) ? 'yes' : 'no';
65
+ }
66
+ if (name === 'NMake Makefiles' || name === 'NMake Makefiles JOM') {
67
+ if (process.platform !== 'win32') return 'no';
68
+ return (await isToolOnPath('nmake')) ? 'yes' : 'no';
69
+ }
70
+ if (name === 'Xcode') {
71
+ if (process.platform !== 'darwin') return 'no';
72
+ return (await isToolOnPath('xcodebuild')) ? 'yes' : 'no';
73
+ }
74
+ return 'unknown';
75
+ }
76
+
77
+ /**
78
+ * Boolean convenience wrapper around `classifyGenerator` — true iff the
79
+ * generator is positively confirmed as available (status === 'yes'). Unknown
80
+ * generators return false here; use `classifyGenerator` directly if you
81
+ * need the distinction.
82
+ *
83
+ * @param {string} name
84
+ * @returns {Promise<boolean>}
85
+ */
86
+ async function isGeneratorAvailable(name) {
87
+ return (await classifyGenerator(name)) === 'yes';
88
+ }
89
+
90
+ /**
91
+ * Choose the best generator from an ordered candidate list.
92
+ *
93
+ * 1. First pass: the first candidate whose probe positively confirms
94
+ * availability (e.g. `ninja --version` succeeds) wins — array order
95
+ * is preserved within this pass.
96
+ * 2. Second pass: if no candidate is known-available, fall back to the
97
+ * first candidate with an unknown name (CMake gets a chance to resolve
98
+ * it, erroring clearly if it can't).
99
+ * 3. Otherwise return `null` so CMake uses its own platform default
100
+ * (VS on Windows, Make elsewhere).
101
+ *
102
+ * @param {string[]} candidates from recipe.generators
103
+ * @returns {Promise<string|null>}
104
+ */
105
+ async function pickGenerator(candidates) {
106
+ if (!candidates || candidates.length === 0) return null;
107
+
108
+ const results = [];
109
+ for (const name of candidates) {
110
+ results.push({ name, status: await classifyGenerator(name) });
111
+ }
112
+
113
+ const good = results.find((r) => r.status === 'yes');
114
+ if (good) {
115
+ if (candidates.length > 1) {
116
+ log.info(` generator: ${good.name} (chosen from ${JSON.stringify(candidates)})`);
117
+ }
118
+ return good.name;
119
+ }
120
+
121
+ const unknown = results.find((r) => r.status === 'unknown');
122
+ if (unknown) {
123
+ log.info(
124
+ ` generator: ${unknown.name} (unknown to wyvrnpm's probe; trusting CMake to resolve)`,
125
+ );
126
+ return unknown.name;
127
+ }
128
+
129
+ log.warn(
130
+ ` None of the requested generators ${JSON.stringify(candidates)} ` +
131
+ `are installed. Falling through to CMake's platform default.`,
132
+ );
133
+ return null;
134
+ }
135
+
136
+ /**
137
+ * Run `cmake` with the given args, inheriting stdio so the user sees compiler
138
+ * output in real time.
139
+ * @param {string[]} args
140
+ * @param {{ cwd?: string, env?: object }} [opts]
141
+ * @returns {Promise<void>}
142
+ */
143
+ function runCmake(args, opts = {}) {
144
+ return new Promise((resolve, reject) => {
145
+ const proc = spawn('cmake', args, {
146
+ stdio: 'inherit',
147
+ cwd: opts.cwd,
148
+ env: opts.env || process.env,
149
+ });
150
+ proc.on('error', (err) => reject(new Error(
151
+ `Failed to spawn cmake: ${err.message} (is CMake installed and on PATH?)`,
152
+ )));
153
+ proc.on('close', (code) => {
154
+ if (code === 0) resolve();
155
+ else reject(new Error(`cmake ${args.join(' ')} exited ${code}`));
156
+ });
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Multi-config CMake generators pick the build configuration at build time
162
+ * (via `--config`), not configure time. Everything else is single-config
163
+ * and bakes `CMAKE_BUILD_TYPE` into the cache at configure time, so
164
+ * multiple configs need multiple build directories.
165
+ *
166
+ * @param {string|null} generatorName
167
+ * @returns {boolean}
168
+ */
169
+ function isMultiConfigGenerator(generatorName) {
170
+ if (!generatorName) return false; // null = CMake default — usually Make on Linux, VS on Windows
171
+ if (generatorName.startsWith('Visual Studio')) return true;
172
+ return generatorName === 'Xcode' || generatorName === 'Ninja Multi-Config';
173
+ }
174
+
175
+ /**
176
+ * Build cmake CONFIGURE argv for a source-build.
177
+ *
178
+ * Exactly one of `buildType` (single-config) or `configurations`
179
+ * (multi-config) should be non-null. Setting `configurations` explicitly
180
+ * lets us pin `CMAKE_CONFIGURATION_TYPES` so every expected `build-<Config>.ninja`
181
+ * file is actually generated — CMake's default for that variable varies
182
+ * across versions and user CMakeLists.
183
+ *
184
+ * @param {{
185
+ * sourceDir: string,
186
+ * buildDir: string,
187
+ * installDir: string,
188
+ * toolchainFile: string,
189
+ * generator?: string|null,
190
+ * configureExtra: string[],
191
+ * buildType?: string|null, // single-config only
192
+ * configurations?: string[]|null, // multi-config only, pins CMAKE_CONFIGURATION_TYPES
193
+ * }} args
194
+ * @returns {string[]}
195
+ */
196
+ function buildConfigureArgs({
197
+ sourceDir, buildDir, installDir, toolchainFile, generator, configureExtra,
198
+ buildType, configurations,
199
+ }) {
200
+ const args = ['-S', sourceDir, '-B', buildDir];
201
+ if (generator) args.push('-G', generator);
202
+ args.push(`-DCMAKE_TOOLCHAIN_FILE=${toolchainFile}`);
203
+ args.push(`-DCMAKE_INSTALL_PREFIX=${installDir}`);
204
+ // Single-config generators bake CMAKE_BUILD_TYPE into the cache.
205
+ if (buildType) args.push(`-DCMAKE_BUILD_TYPE=${buildType}`);
206
+ // Multi-config generators ignore CMAKE_BUILD_TYPE and use
207
+ // CMAKE_CONFIGURATION_TYPES instead. Passed as a semicolon list.
208
+ if (configurations && configurations.length > 0) {
209
+ args.push(`-DCMAKE_CONFIGURATION_TYPES=${configurations.join(';')}`);
210
+ }
211
+ if (configureExtra.length > 0) args.push(...configureExtra);
212
+ return args;
213
+ }
214
+
215
+ /**
216
+ * Build cmake --build argv for the install target.
217
+ *
218
+ * @param {{
219
+ * buildDir: string,
220
+ * config: string,
221
+ * buildExtra: string[],
222
+ * }} args
223
+ * @returns {string[]}
224
+ */
225
+ function buildInstallArgs({ buildDir, config, buildExtra }) {
226
+ const args = ['--build', buildDir, '--target', 'install', '--config', config];
227
+ if (buildExtra.length > 0) args.push('--', ...buildExtra);
228
+ return args;
229
+ }
230
+
231
+ /**
232
+ * Configure + build + install a package from source.
233
+ *
234
+ * @param {{
235
+ * recipe: import('./recipe').NormalizedRecipe,
236
+ * srcRoot: string, // absolute path to the cloned source root
237
+ * buildPaths: { build: string, install: string },
238
+ * toolchainFile: string, // absolute path to wyvrn_toolchain.cmake
239
+ * config?: string, // default 'Release'
240
+ * }} args
241
+ * @returns {Promise<void>}
242
+ */
243
+ async function configureBuildInstall({ recipe, srcRoot, buildPaths, toolchainFile, configs, profile }) {
244
+ const configList = Array.isArray(configs) && configs.length > 0
245
+ ? configs
246
+ : [DEFAULT_CONFIG];
247
+
248
+ const sourceDir = path.resolve(srcRoot, recipe.sourceSubdir);
249
+ if (!fs.existsSync(path.join(sourceDir, 'CMakeLists.txt'))) {
250
+ throw new Error(
251
+ `CMakeLists.txt not found at ${sourceDir}. ` +
252
+ `Check the package's build.sourceSubdir in its wyvrn.json.`,
253
+ );
254
+ }
255
+
256
+ fs.mkdirSync(buildPaths.install, { recursive: true });
257
+
258
+ // Resolve generator from the fallback chain (probes each candidate's
259
+ // backing tool). `null` → omit `-G`, let CMake pick its default.
260
+ const generator = await pickGenerator(recipe.generators);
261
+
262
+ // On Windows with an MSVC profile + non-VS generator (Ninja, Make, ...),
263
+ // auto-enter a Developer Command Prompt-equivalent env so `cl.exe` is on
264
+ // PATH. VS generators don't need this — MSBuild resolves MSVC itself.
265
+ // Returns null when nothing needs to change, in which case we inherit
266
+ // the caller's env.
267
+ const env = await loadMsvcEnv({ profile, skipForGenerator: generator });
268
+
269
+ const multiConfig = isMultiConfigGenerator(generator);
270
+ log.info(
271
+ ` generator: ${generator ?? '<CMake default>'} ` +
272
+ `(${multiConfig ? 'multi-config' : 'single-config'}), configs: ${configList.join(', ')}`,
273
+ );
274
+
275
+ if (multiConfig) {
276
+ // ── Multi-config: one build dir, run install target once per config ─────
277
+ fs.mkdirSync(buildPaths.build, { recursive: true });
278
+ const configureArgs = buildConfigureArgs({
279
+ sourceDir,
280
+ buildDir: buildPaths.build,
281
+ installDir: buildPaths.install,
282
+ toolchainFile,
283
+ generator,
284
+ configureExtra: recipe.configure,
285
+ buildType: null, // multi-config picks at build time
286
+ configurations: configList, // pin CMAKE_CONFIGURATION_TYPES so all 4 build-<Config>.ninja files get generated
287
+ });
288
+ log.info(` cmake ${configureArgs.join(' ')}`);
289
+ await runCmake(configureArgs, { env });
290
+
291
+ for (const cfg of configList) {
292
+ const installArgs = buildInstallArgs({
293
+ buildDir: buildPaths.build,
294
+ config: cfg,
295
+ buildExtra: recipe.buildArgs,
296
+ });
297
+ log.info(` cmake ${installArgs.join(' ')}`);
298
+ await runCmake(installArgs, { env });
299
+ }
300
+ return;
301
+ }
302
+
303
+ // ── Single-config: separate build dir + reconfigure per config ───────────
304
+ // `CMAKE_BUILD_TYPE` is baked into the cache, so mixing configs in one
305
+ // build dir would require clobbering the cache between each. Distinct
306
+ // dirs is simpler and lets CMake's own dependency tracking work per-config.
307
+ for (const cfg of configList) {
308
+ const perCfgBuildDir = `${buildPaths.build}-${cfg}`;
309
+ fs.mkdirSync(perCfgBuildDir, { recursive: true });
310
+ const configureArgs = buildConfigureArgs({
311
+ sourceDir,
312
+ buildDir: perCfgBuildDir,
313
+ installDir: buildPaths.install,
314
+ toolchainFile,
315
+ generator,
316
+ configureExtra: recipe.configure,
317
+ buildType: cfg,
318
+ });
319
+ log.info(` cmake ${configureArgs.join(' ')}`);
320
+ await runCmake(configureArgs, { env });
321
+
322
+ const installArgs = buildInstallArgs({
323
+ buildDir: perCfgBuildDir,
324
+ config: cfg,
325
+ buildExtra: recipe.buildArgs,
326
+ });
327
+ log.info(` cmake ${installArgs.join(' ')}`);
328
+ await runCmake(installArgs, { env });
329
+ }
330
+ }
331
+
332
+ module.exports = {
333
+ configureBuildInstall,
334
+ pickGenerator,
335
+ classifyGenerator,
336
+ isGeneratorAvailable,
337
+ isMultiConfigGenerator,
338
+ // Exported for unit tests
339
+ buildConfigureArgs,
340
+ buildInstallArgs,
341
+ DEFAULT_CONFIG,
342
+ };