wyvrnpm 2.3.2 → 2.8.1

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.
@@ -14,6 +14,7 @@ const { resolveDependencies } = require('../resolve');
14
14
  const { hashProfile, mergeProfile, sha256Of } = require('../profile');
15
15
  const { generateToolchain } = require('../toolchain');
16
16
  const { normalizeOptionsDeclaration, resolveEffectiveOptions } = require('../options');
17
+ const { readRecipeConf, cmakeCacheVariables } = require('../conf');
17
18
  const log = require('../logger');
18
19
 
19
20
  /**
@@ -155,8 +156,28 @@ async function buildFromSource(args) {
155
156
  }
156
157
  // Pass effective options into the recipe so `${options.<name>}` tokens in
157
158
  // `build.configure` / `build.buildArgs` expand to the concrete values that
158
- // produced this build's profileHash.
159
- const recipe = normalizeRecipe(clonedManifest.build, options);
159
+ // produced this build's profileHash. Pass the cloned manifest so PLAN-CONF.md
160
+ // §4.7 collision detection catches a publisher who declared the same KEY in
161
+ // both build.configure and conf.cmake.cache.
162
+ const recipe = normalizeRecipe(clonedManifest.build, options, clonedManifest);
163
+
164
+ // PLAN-CONF.md §4.8.2: source-build of a dep consumes ONLY the dep's own
165
+ // recipe conf — never the root project's merged conf. Translating the
166
+ // dep's cmake.cache namespace into -D args here keeps the dep's source-
167
+ // build CMake invocation aligned with its author's declared defaults
168
+ // while preserving cross-consumer reproducibility. The root's CLI
169
+ // --conf / wyvrn.local.json overlay is invisible at this seam.
170
+ const clonedConfFlat = readRecipeConf(clonedManifest);
171
+ const clonedCacheVars = cmakeCacheVariables(clonedConfFlat);
172
+ if (Object.keys(clonedCacheVars).length > 0) {
173
+ const confDerivedArgs = Object.entries(clonedCacheVars)
174
+ .map(([k, v]) => `-D${k}=${v}`);
175
+ recipe.configure = [...recipe.configure, ...confDerivedArgs];
176
+ log.info(
177
+ ` conf.cmake.cache → ${confDerivedArgs.length} source-build ` +
178
+ `configure arg(s) from the dep's own recipe`,
179
+ );
180
+ }
160
181
 
161
182
  // ── 4. Recursively resolve + download this package's OWN deps ──────────
162
183
  // Lazy-require: downloadDependencies is where we were called from, so
@@ -75,17 +75,35 @@ function findVcvarsall(vsInstallPath) {
75
75
  return fs.existsSync(p) ? p : null;
76
76
  }
77
77
 
78
+ /**
79
+ * The complete set of arch arguments `vcvarsall.bat` accepts AND that we
80
+ * are willing to splice into a shell command. Explicit allow-list — any
81
+ * value outside this set is rejected (EVALUATION.md S5).
82
+ */
83
+ const VCVARSALL_ARCH_ALLOWLIST = new Set(['x64', 'x86', 'arm64', 'arm']);
84
+
85
+ const ARCH_MAP = {
86
+ x86_64: 'x64',
87
+ x86: 'x86',
88
+ armv8: 'arm64',
89
+ armv7: 'arm',
90
+ };
91
+
78
92
  /**
79
93
  * Map a wyvrnpm profile arch to the argument `vcvarsall.bat` expects.
94
+ * Throws on an unknown profile arch — silently defaulting to `x64` would
95
+ * (a) produce a build for the wrong target, and (b) hand any shell-unsafe
96
+ * input to cmd.exe through captureVcvarsEnv's template.
80
97
  */
81
98
  function mapArch(profileArch) {
82
- switch (profileArch) {
83
- case 'x86_64': return 'x64';
84
- case 'x86': return 'x86';
85
- case 'armv8': return 'arm64';
86
- case 'armv7': return 'arm';
87
- default: return 'x64';
99
+ const arg = ARCH_MAP[profileArch];
100
+ if (!arg || !VCVARSALL_ARCH_ALLOWLIST.has(arg)) {
101
+ throw new Error(
102
+ `unsupported MSVC profile arch ${JSON.stringify(profileArch)}; ` +
103
+ `expected one of ${Object.keys(ARCH_MAP).join(', ')}`,
104
+ );
88
105
  }
106
+ return arg;
89
107
  }
90
108
 
91
109
  /**
@@ -105,6 +123,18 @@ function mapArch(profileArch) {
105
123
  * @returns {Promise<object|null>}
106
124
  */
107
125
  async function captureVcvarsEnv(vcvarsallPath, archArg) {
126
+ // Defense-in-depth at the shell boundary (EVALUATION.md S5). mapArch
127
+ // already restricts archArg to the allow-list; re-assert here so this
128
+ // function is safe to call directly (e.g. from a future caller that
129
+ // skips mapArch). Likewise refuse a vcvarsall path containing a
130
+ // double-quote — it would break out of the quoted command template.
131
+ if (!VCVARSALL_ARCH_ALLOWLIST.has(archArg)) {
132
+ throw new Error(`refusing to spawn cmd.exe with unsafe arch arg ${JSON.stringify(archArg)}`);
133
+ }
134
+ if (vcvarsallPath.includes('"')) {
135
+ throw new Error(`refusing to spawn cmd.exe with quote in vcvarsall path ${JSON.stringify(vcvarsallPath)}`);
136
+ }
137
+
108
138
  const MARKER = '___WYVRN_ENV_MARKER___';
109
139
  // `call` is required so the batch file returns control instead of
110
140
  // `exit`-ing the cmd shell; otherwise the `echo` + `set` after it
@@ -197,7 +227,18 @@ async function loadMsvcEnv({ profile, skipForGenerator }) {
197
227
  return null;
198
228
  }
199
229
 
200
- const archArg = mapArch(profile.arch);
230
+ let archArg;
231
+ try {
232
+ archArg = mapArch(profile.arch);
233
+ } catch (err) {
234
+ // Unknown arch for an MSVC profile is genuinely a configuration bug
235
+ // (MSVC only ships toolsets for x64/x86/arm64/arm). Surface clearly
236
+ // and fall through to the ambient env — CMake will then error with
237
+ // a usable compiler-detection message instead of us silently
238
+ // cross-targeting x64.
239
+ log.warn(` ${err.message}`);
240
+ return null;
241
+ }
201
242
  log.info(` activating MSVC build env: vcvarsall.bat ${archArg} (${vsPath})`);
202
243
  const env = await captureVcvarsEnv(vcvarsall, archArg);
203
244
  if (!env) {
@@ -214,4 +255,6 @@ module.exports = {
214
255
  findVsInstall,
215
256
  findVcvarsall,
216
257
  mapArch,
258
+ captureVcvarsEnv,
259
+ VCVARSALL_ARCH_ALLOWLIST,
217
260
  };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { substituteTemplateAll } = require('../options');
4
+ const { readRecipeConf } = require('../conf');
4
5
 
5
6
  /**
6
7
  * The `build` section of a source-repo `wyvrn.json` tells wyvrnpm how to
@@ -60,15 +61,22 @@ const SUPPORTED_SYSTEMS = new Set(['cmake']);
60
61
  * no options), any `${options.*}` token in the recipe also fails fast —
61
62
  * recipes don't get to reference undeclared options.
62
63
  *
64
+ * When `manifestForConfCollisionCheck` is supplied, PLAN-CONF.md §4.7's
65
+ * collision detection runs: any `-DKEY=...` in `build.configure` whose
66
+ * KEY is ALSO declared in recipe-level `conf.cmake.cache` is a hard
67
+ * error. Recipes pick ONE route, avoiding silent duplication.
68
+ *
63
69
  * @param {object|null|undefined} rawBuild
64
70
  * @param {Record<string, any>|null} [effectiveOptions]
71
+ * @param {object|null} [manifestForConfCollisionCheck]
65
72
  * @returns {{ system: string, generators: string[], configure: string[],
66
73
  * buildArgs: string[], installDir: string, sourceSubdir: string,
67
74
  * requiredTools: string[] }}
68
- * @throws {Error} when `system` is set but not supported, or when a
69
- * `${options.<name>}` template references an undeclared option
75
+ * @throws {Error} when `system` is set but not supported, when a
76
+ * `${options.<name>}` template references an undeclared option,
77
+ * or when `build.configure` collides with `conf.cmake.cache`.
70
78
  */
71
- function normalizeRecipe(rawBuild, effectiveOptions = null) {
79
+ function normalizeRecipe(rawBuild, effectiveOptions = null, manifestForConfCollisionCheck = null) {
72
80
  const b = rawBuild ?? {};
73
81
  const system = b.system ?? DEFAULT_RECIPE.system;
74
82
  if (!SUPPORTED_SYSTEMS.has(system)) {
@@ -124,6 +132,31 @@ function normalizeRecipe(rawBuild, effectiveOptions = null) {
124
132
  'build.buildArgs',
125
133
  );
126
134
 
135
+ // PLAN-CONF.md §4.7: recipe-side collision between `build.configure`
136
+ // `-DKEY=...` args and declared `conf.cmake.cache.KEY` is a hard
137
+ // error. Forces the author to pick one route; avoids silent
138
+ // duplication where one route wins opaquely.
139
+ if (manifestForConfCollisionCheck) {
140
+ const recipeConfFlat = readRecipeConf(manifestForConfCollisionCheck);
141
+ const confCacheKeys = new Set(
142
+ Object.keys(recipeConfFlat)
143
+ .filter((k) => k.startsWith('cmake.cache.'))
144
+ .map((k) => k.slice('cmake.cache.'.length)),
145
+ );
146
+ if (confCacheKeys.size > 0) {
147
+ for (const arg of configure) {
148
+ const m = arg.match(/^-D([^=]+)=/);
149
+ if (m && confCacheKeys.has(m[1])) {
150
+ throw new Error(
151
+ `recipe collision: "${m[1]}" is set in both ` +
152
+ `build.configure (as ${JSON.stringify(arg)}) and ` +
153
+ `conf.cmake.cache. Pick one route — see claude/PLAN-CONF.md §4.7.`,
154
+ );
155
+ }
156
+ }
157
+ }
158
+ }
159
+
127
160
  return {
128
161
  system,
129
162
  generators,
@@ -0,0 +1,140 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const { readManifest, writeManifest } = require('../manifest');
6
+ const { resolveLatestVersion } = require('../resolve');
7
+ const { readConfig } = require('../config');
8
+ const log = require('../logger');
9
+
10
+ /**
11
+ * Parse a `<name>[@<version>]` positional into its parts.
12
+ *
13
+ * `@` inside the version (e.g. tags like `latest`) is allowed — only the
14
+ * first `@` is treated as the separator.
15
+ *
16
+ * @param {string} spec
17
+ * @returns {{ name: string, version: string | null }}
18
+ */
19
+ function parseSpec(spec) {
20
+ const at = spec.indexOf('@');
21
+ if (at === -1) return { name: spec, version: null };
22
+ if (at === 0) throw new Error(`invalid package spec "${spec}" — missing name before "@"`);
23
+ const name = spec.slice(0, at);
24
+ const version = spec.slice(at + 1);
25
+ if (!version) throw new Error(`invalid package spec "${spec}" — missing version after "@"`);
26
+ return { name, version };
27
+ }
28
+
29
+ /**
30
+ * Add one or more dependencies to wyvrn.json.
31
+ *
32
+ * wyvrnpm add <name> → resolves latest, writes "^<latest>"
33
+ * wyvrnpm add <name>@<version> → writes "<version>" verbatim (exact or range)
34
+ *
35
+ * Existing entries are replaced. If the prior entry was an object
36
+ * (`{ version, settings, options }`), its settings/options are preserved —
37
+ * only `version` is updated.
38
+ *
39
+ * Does not run install. The user must `wyvrnpm install` afterwards.
40
+ *
41
+ * @param {object} argv
42
+ */
43
+ async function add(argv) {
44
+ const manifestPath = path.resolve(argv.manifest);
45
+ const specs = (argv.packages ?? []).map(parseSpec);
46
+
47
+ if (specs.length === 0) {
48
+ log.error('no packages given. Usage: wyvrnpm add <name>[@<version>] [...]');
49
+ process.exit(1);
50
+ }
51
+
52
+ // Install sources — only needed when at least one spec omits @version and
53
+ // we have to hit `latest.json`.
54
+ let packageSources;
55
+ if (argv.source && argv.source.length > 0) {
56
+ packageSources = argv.source;
57
+ } else {
58
+ const config = readConfig();
59
+ packageSources = config.installSources.map((s) => s.url);
60
+ }
61
+
62
+ const needsResolve = specs.some((s) => s.version === null);
63
+ if (needsResolve && packageSources.length === 0) {
64
+ log.error(
65
+ 'no install sources configured — cannot look up latest versions.\n' +
66
+ ' Pass --source <url>, pin versions explicitly (`name@1.2.3.4`), or run:\n' +
67
+ ' wyvrnpm configure add-source --kind install --name ... --url ...',
68
+ );
69
+ process.exit(1);
70
+ }
71
+
72
+ const manifest = readManifest(manifestPath);
73
+
74
+ // Preserve whichever dependencies key the manifest already uses. If neither
75
+ // exists, create lowercase `dependencies` (the v2 convention).
76
+ const depsKey =
77
+ Object.prototype.hasOwnProperty.call(manifest, 'dependencies') ? 'dependencies' :
78
+ Object.prototype.hasOwnProperty.call(manifest, 'Dependencies') ? 'Dependencies' :
79
+ 'dependencies';
80
+
81
+ let deps = manifest[depsKey];
82
+ // Legacy array form → object. Editing by name is the whole point of `add`,
83
+ // and round-tripping as an array would silently drop per-dep settings.
84
+ if (Array.isArray(deps)) {
85
+ const obj = {};
86
+ for (const d of deps) {
87
+ const n = d.Name ?? d.name;
88
+ const v = d.Version ?? d.version;
89
+ if (n && v) obj[n] = v;
90
+ }
91
+ deps = obj;
92
+ } else if (!deps || typeof deps !== 'object') {
93
+ deps = {};
94
+ }
95
+
96
+ for (const spec of specs) {
97
+ let versionToWrite;
98
+ if (spec.version) {
99
+ versionToWrite = spec.version;
100
+ log.info(`${spec.name}: writing "${versionToWrite}" (as given)`);
101
+ } else {
102
+ const latest = await resolveLatestVersion(spec.name, packageSources, argv.platform, fetch);
103
+ versionToWrite = `^${latest}`;
104
+ log.info(`${spec.name}: latest is ${latest} → writing "${versionToWrite}"`);
105
+ }
106
+
107
+ const prior = deps[spec.name];
108
+ if (prior !== undefined) {
109
+ const priorVer =
110
+ typeof prior === 'string' ? prior :
111
+ (prior && typeof prior === 'object') ? (prior.version ?? prior.Version ?? '(unknown)') :
112
+ '(unknown)';
113
+ log.info(` replacing existing entry: "${priorVer}"`);
114
+ }
115
+
116
+ // If the prior entry was an object with settings/options, keep those —
117
+ // only bump the version field. Otherwise write the short-form string.
118
+ if (prior && typeof prior === 'object' && !Array.isArray(prior)) {
119
+ const updated = { ...prior };
120
+ if ('Version' in updated && !('version' in updated)) {
121
+ updated.Version = versionToWrite;
122
+ } else {
123
+ updated.version = versionToWrite;
124
+ }
125
+ deps[spec.name] = updated;
126
+ } else {
127
+ deps[spec.name] = versionToWrite;
128
+ }
129
+ }
130
+
131
+ manifest[depsKey] = deps;
132
+ writeManifest(manifestPath, manifest);
133
+
134
+ const n = specs.length;
135
+ log.success(`Updated ${manifestPath} — ${n} package${n !== 1 ? 's' : ''}.`);
136
+ log.info('Run `wyvrnpm install` to download.');
137
+ }
138
+
139
+ module.exports = add;
140
+ module.exports.parseSpec = parseSpec;
@@ -9,6 +9,9 @@ const { loadProfile } = require('../profile');
9
9
  const { readManifest } = require('../manifest');
10
10
  const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
11
11
  const { loadMsvcEnv } = require('../build/msvc-env');
12
+ const { archToVsPlatform, isVisualStudioGenerator } = require('../toolchain');
13
+ const { parseCliConf } = require('../conf');
14
+ const { LOCAL_OVERLAY_FILENAME } = require('../conf');
12
15
  const install = require('./install');
13
16
  const log = require('../logger');
14
17
 
@@ -28,8 +31,10 @@ const log = require('../logger');
28
31
  * @returns {{ stale: boolean, reason: string }}
29
32
  */
30
33
  function isToolchainStale({ rootDir, manifestPath }) {
31
- const lockPath = path.join(path.dirname(manifestPath), 'wyvrn.lock');
32
- const toolchainPath = path.join(rootDir, 'wyvrn_internal', 'wyvrn_toolchain.cmake');
34
+ const lockPath = path.join(path.dirname(manifestPath), 'wyvrn.lock');
35
+ const toolchainPath = path.join(rootDir, 'wyvrn_internal', 'wyvrn_toolchain.cmake');
36
+ const localOverlayPath = path.join(rootDir, LOCAL_OVERLAY_FILENAME);
37
+ const presetPath = path.join(rootDir, 'CMakePresets.json');
33
38
 
34
39
  if (!fs.existsSync(toolchainPath)) {
35
40
  return { stale: true, reason: 'toolchain-missing' };
@@ -43,6 +48,19 @@ function isToolchainStale({ rootDir, manifestPath }) {
43
48
  if (lockMtime > toolchainMtime) {
44
49
  return { stale: true, reason: 'lock-newer-than-toolchain' };
45
50
  }
51
+
52
+ // PLAN-CONF.md §7: if wyvrn.local.json was edited after the preset was
53
+ // written, the preset's cacheVariables no longer reflect the consumer's
54
+ // intent. Re-run install (which regenerates the preset) rather than
55
+ // silently running a stale configure.
56
+ if (fs.existsSync(localOverlayPath) && fs.existsSync(presetPath)) {
57
+ const overlayMtime = fs.statSync(localOverlayPath).mtimeMs;
58
+ const presetMtime = fs.statSync(presetPath).mtimeMs;
59
+ if (overlayMtime > presetMtime) {
60
+ return { stale: true, reason: 'local-overlay-newer-than-preset' };
61
+ }
62
+ }
63
+
46
64
  return { stale: false, reason: 'fresh' };
47
65
  }
48
66
 
@@ -70,11 +88,57 @@ function resolveBinaryDir({ rootDir, presetName }) {
70
88
 
71
89
  /**
72
90
  * Args passed to `cmake` for the configure step.
73
- * @param {{ presetName: string }} args
91
+ *
92
+ * `generator`, when set, appends `-G <name>` after `--preset`. CMake 3.21+
93
+ * lets CLI args override a preset's declared fields, so this swaps the
94
+ * generator at configure time without touching CMakePresets.json. Note:
95
+ * CMake bakes the chosen generator into the cache, so switching generators
96
+ * for an existing build directory requires `--clean` (otherwise CMake
97
+ * errors with "CMAKE_GENERATOR has changed").
98
+ *
99
+ * `vsPlatform`, when set, appends `-A <platform>`. Only meaningful for
100
+ * Visual Studio generators — they default to x64 when unspecified, which
101
+ * silently produces a 64-bit build even for a 32-bit profile. The caller
102
+ * is responsible for deriving this from the profile arch only when the
103
+ * effective generator is VS (see `isVisualStudioGenerator` +
104
+ * `archToVsPlatform`).
105
+ *
106
+ * @param {{ presetName: string, generator?: string|null, vsPlatform?: string|null }} args
74
107
  * @returns {string[]}
75
108
  */
76
- function buildConfigureArgs({ presetName }) {
77
- return ['--preset', presetName];
109
+ function buildConfigureArgs({
110
+ presetName,
111
+ generator = null,
112
+ vsPlatform = null,
113
+ confSets = null,
114
+ confUnsets = null,
115
+ }) {
116
+ const args = ['--preset', presetName];
117
+ if (generator) args.push('-G', generator);
118
+ if (vsPlatform) args.push('-A', vsPlatform);
119
+
120
+ // PLAN-CONF.md §7: CLI --conf at build time applies as -D / -U
121
+ // overrides on top of whatever's baked into the preset. We do NOT
122
+ // silently regenerate the preset — that's a separate install.
123
+ // Confined to the cmake.cache namespace; other namespaces have
124
+ // different sinks (phase 3) and don't apply to `cmake --preset`.
125
+ if (confSets) {
126
+ const prefix = 'cmake.cache.';
127
+ for (const key of Object.keys(confSets).sort()) {
128
+ if (!key.startsWith(prefix)) continue;
129
+ const cmakeKey = key.slice(prefix.length);
130
+ args.push('-D', `${cmakeKey}=${confSets[key]}`);
131
+ }
132
+ }
133
+ if (confUnsets) {
134
+ const prefix = 'cmake.cache.';
135
+ for (const key of [...confUnsets].sort()) {
136
+ if (!key.startsWith(prefix)) continue;
137
+ args.push('-U', key.slice(prefix.length));
138
+ }
139
+ }
140
+
141
+ return args;
78
142
  }
79
143
 
80
144
  /**
@@ -211,8 +275,11 @@ async function build(argv) {
211
275
  //
212
276
  // Same mechanism the source-build path already uses (src/build/msvc-env.js).
213
277
  let cmakeEnv = null;
278
+ let effectiveGenerator = argv.generator ?? null;
279
+ let effectiveProfile = null;
214
280
  try {
215
281
  const { profile: activeProfile } = loadProfile(profileName);
282
+ effectiveProfile = activeProfile;
216
283
  // Which generator will the preset actually use? Read it from the
217
284
  // project's own recipe if one exists. If not, default to null and let
218
285
  // cmake/CMakePresets decide — in which case MSVC env activation is
@@ -225,16 +292,53 @@ async function build(argv) {
225
292
  if (recipe.generators.length > 0) [presetGenerator] = recipe.generators;
226
293
  } catch { /* templates need options → skip env activation */ }
227
294
  }
295
+ // CLI --generator wins — that is the generator CMake will actually use,
296
+ // so it is the one that determines whether vcvarsall must be activated.
297
+ if (!effectiveGenerator) effectiveGenerator = presetGenerator;
228
298
  cmakeEnv = await loadMsvcEnv({
229
299
  profile: activeProfile,
230
- skipForGenerator: presetGenerator,
300
+ skipForGenerator: effectiveGenerator,
231
301
  });
232
302
  } catch (err) {
233
303
  log.warn(`Could not resolve MSVC env activation: ${err.message}`);
234
304
  }
235
305
 
236
306
  // ── Configure ─────────────────────────────────────────────────────────────
237
- const configureArgs = buildConfigureArgs({ presetName });
307
+ // Auto-emit `-A <platform>` for Visual Studio generators, mapped from the
308
+ // active profile's arch. VS otherwise defaults to x64 regardless of the
309
+ // profile, which breaks x86 builds: CMake produces 64-bit artefacts but
310
+ // find_package() rejects the 32-bit installed dependencies with "version
311
+ // not compatible" (actually a CMAKE_SIZEOF_VOID_P mismatch, not a
312
+ // version-number check).
313
+ let vsPlatform = null;
314
+ if (isVisualStudioGenerator(effectiveGenerator) && effectiveProfile) {
315
+ vsPlatform = archToVsPlatform(effectiveProfile.arch);
316
+ if (vsPlatform) {
317
+ log.info(`Visual Studio generator + arch=${effectiveProfile.arch} → passing -A ${vsPlatform}`);
318
+ }
319
+ }
320
+
321
+ // CLI --conf → -D / -U passthrough (PLAN-CONF.md §7). The preset's
322
+ // baked-in cacheVariables (from the last install) already reflect the
323
+ // recipe + local-overlay layers; CLI conf layers on top for this
324
+ // invocation only.
325
+ let cliConfSets, cliConfUnsets;
326
+ try {
327
+ const cli = parseCliConf(argv.conf);
328
+ cliConfSets = cli.sets;
329
+ cliConfUnsets = cli.unsets;
330
+ } catch (err) {
331
+ log.error(`--conf parse error: ${err.message}`);
332
+ process.exit(1);
333
+ }
334
+
335
+ const configureArgs = buildConfigureArgs({
336
+ presetName,
337
+ generator: argv.generator,
338
+ vsPlatform,
339
+ confSets: cliConfSets,
340
+ confUnsets: cliConfUnsets,
341
+ });
238
342
  log.info(`cmake ${configureArgs.join(' ')}`);
239
343
  await runCmake(configureArgs, cmakeEnv);
240
344