wyvrnpm 2.10.2 → 2.12.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 (49) hide show
  1. package/README.md +1914 -1860
  2. package/bin/{wyvrn.js → wyvrnpm.js} +66 -0
  3. package/cmake/cpp.cmake +9 -9
  4. package/cmake/functions.cmake +224 -224
  5. package/cmake/macros.cmake +284 -284
  6. package/package.json +3 -2
  7. package/src/auth.js +66 -66
  8. package/src/binary-dir.js +95 -0
  9. package/src/bootstrap/cookbook.js +196 -196
  10. package/src/bootstrap/detect.js +150 -150
  11. package/src/bootstrap/index.js +220 -220
  12. package/src/bootstrap/version.js +72 -72
  13. package/src/build/cache.js +344 -344
  14. package/src/build/clone.js +170 -170
  15. package/src/build/cmake.js +342 -342
  16. package/src/build/index.js +299 -297
  17. package/src/build/msvc-env.js +260 -260
  18. package/src/build/recipe.js +188 -188
  19. package/src/commands/add.js +141 -141
  20. package/src/commands/bootstrap.js +96 -96
  21. package/src/commands/build.js +482 -452
  22. package/src/commands/cache.js +189 -189
  23. package/src/commands/clean.js +80 -80
  24. package/src/commands/configure.js +92 -92
  25. package/src/commands/init.js +70 -70
  26. package/src/commands/install-skill.js +115 -115
  27. package/src/commands/install.js +730 -674
  28. package/src/commands/link.js +320 -320
  29. package/src/commands/profile.js +237 -237
  30. package/src/commands/publish.js +584 -555
  31. package/src/commands/show.js +252 -252
  32. package/src/commands/version.js +187 -0
  33. package/src/compat.js +273 -273
  34. package/src/conf/index.js +415 -391
  35. package/src/conf/namespaces.js +94 -94
  36. package/src/context.js +230 -230
  37. package/src/http-fetch.js +53 -53
  38. package/src/ignore.js +118 -118
  39. package/src/logger.js +122 -122
  40. package/src/options.js +303 -303
  41. package/src/settings-overrides.js +152 -152
  42. package/src/toolchain/deps.js +164 -164
  43. package/src/toolchain/index.js +212 -212
  44. package/src/toolchain/presets.js +356 -340
  45. package/src/toolchain/template.cmake +77 -77
  46. package/src/upload-built.js +265 -265
  47. package/src/version-range.js +301 -301
  48. package/src/zip-safe.js +52 -52
  49. package/src/zip-stream.js +126 -0
@@ -1,342 +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
- };
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
+ };