wyvrnpm 2.19.0 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wyvrnpm",
3
- "version": "2.19.0",
3
+ "version": "2.20.0",
4
4
  "description": "A simple, static-hosting-compatible C++ package manager",
5
5
  "keywords": [
6
6
  "c++",
@@ -5,6 +5,8 @@ const path = require('path');
5
5
  const { spawn } = require('child_process');
6
6
 
7
7
  const { loadMsvcEnv } = require('./msvc-env');
8
+ const { archToVsPlatform, isVisualStudioGenerator } = require('../toolchain');
9
+ const { detectDefaultGenerator } = require('./default-generator');
8
10
  const log = require('../logger');
9
11
 
10
12
  /**
@@ -190,15 +192,21 @@ function isMultiConfigGenerator(generatorName) {
190
192
  * configureExtra: string[],
191
193
  * buildType?: string|null, // single-config only
192
194
  * configurations?: string[]|null, // multi-config only, pins CMAKE_CONFIGURATION_TYPES
195
+ * vsPlatform?: string|null, // VS generators only — emits -A <platform>
193
196
  * }} args
194
197
  * @returns {string[]}
195
198
  */
196
199
  function buildConfigureArgs({
197
200
  sourceDir, buildDir, installDir, toolchainFile, generator, configureExtra,
198
- buildType, configurations,
201
+ buildType, configurations, vsPlatform = null,
199
202
  }) {
200
203
  const args = ['-S', sourceDir, '-B', buildDir];
201
204
  if (generator) args.push('-G', generator);
205
+ // Visual Studio generators default to host x64 unless the target platform is
206
+ // pinned with -A. Without this, an x86/ARM profile silently produces x64
207
+ // binaries (PLAN-VS-ARCH-PIN gate 3). Mapped from the profile arch by the
208
+ // caller; null for non-VS generators.
209
+ if (vsPlatform) args.push('-A', vsPlatform);
202
210
  args.push(`-DCMAKE_TOOLCHAIN_FILE=${toolchainFile}`);
203
211
  args.push(`-DCMAKE_INSTALL_PREFIX=${installDir}`);
204
212
  // Single-config generators bake CMAKE_BUILD_TYPE into the cache.
@@ -257,7 +265,22 @@ async function configureBuildInstall({ recipe, srcRoot, buildPaths, toolchainFil
257
265
 
258
266
  // Resolve generator from the fallback chain (probes each candidate's
259
267
  // backing tool). `null` → omit `-G`, let CMake pick its default.
260
- const generator = await pickGenerator(recipe.generators);
268
+ let generator = await pickGenerator(recipe.generators);
269
+
270
+ // No recipe generator → CMake's Windows default is the newest VS, which
271
+ // builds host x64 unless `-A` is pinned. Resolve it to a concrete VS
272
+ // generator so the -A below applies (PLAN-VS-ARCH-PIN gate 3). Non-Windows /
273
+ // non-MSVC leaves `generator` null → CMake default, no -A (target arch is
274
+ // the compiler's job there).
275
+ if (!generator && profile && profile.os === 'Windows' && profile.compiler === 'msvc') {
276
+ generator = await detectDefaultGenerator();
277
+ }
278
+
279
+ // VS generators need -A <platform> to honour the profile arch — recipe-pinned
280
+ // VS generators get it too, not just the detected default.
281
+ const vsPlatform = isVisualStudioGenerator(generator)
282
+ ? archToVsPlatform(profile && profile.arch)
283
+ : null;
261
284
 
262
285
  // On Windows with an MSVC profile + non-VS generator (Ninja, Make, ...),
263
286
  // auto-enter a Developer Command Prompt-equivalent env so `cl.exe` is on
@@ -281,6 +304,7 @@ async function configureBuildInstall({ recipe, srcRoot, buildPaths, toolchainFil
281
304
  installDir: buildPaths.install,
282
305
  toolchainFile,
283
306
  generator,
307
+ vsPlatform,
284
308
  configureExtra: recipe.configure,
285
309
  buildType: null, // multi-config picks at build time
286
310
  configurations: configList, // pin CMAKE_CONFIGURATION_TYPES so all 4 build-<Config>.ninja files get generated
@@ -313,6 +337,7 @@ async function configureBuildInstall({ recipe, srcRoot, buildPaths, toolchainFil
313
337
  installDir: buildPaths.install,
314
338
  toolchainFile,
315
339
  generator,
340
+ vsPlatform,
316
341
  configureExtra: recipe.configure,
317
342
  buildType: cfg,
318
343
  });
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('child_process');
4
+
5
+ const { isVisualStudioGenerator } = require('../toolchain');
6
+
7
+ /**
8
+ * Parse `cmake --help` output for the platform's default generator. CMake
9
+ * marks the default with a leading `* ` in the "Generators" section, e.g.:
10
+ *
11
+ * The following generators are available on this platform (* marks default):
12
+ * * Visual Studio 17 2022 [arch] = Generates Visual Studio 2022 project files.
13
+ * Visual Studio 16 2019 [arch] = Generates Visual Studio 2019 project files.
14
+ * Ninja = Generates build.ninja files.
15
+ *
16
+ * Returns the default generator's name verbatim (with any trailing ` [arch]`
17
+ * annotation stripped), or null when no default marker is present. The name is
18
+ * returned as-is — deciding whether it's a Visual Studio generator is the
19
+ * caller's job.
20
+ *
21
+ * @param {string} helpText
22
+ * @returns {string|null}
23
+ */
24
+ function parseDefaultGenerator(helpText) {
25
+ if (typeof helpText !== 'string') return null;
26
+ for (const line of helpText.split(/\r?\n/)) {
27
+ const m = line.match(/^\*\s+(.+?)\s*=/);
28
+ if (!m) continue;
29
+ return m[1].replace(/\s*\[arch\]\s*$/, '').trim();
30
+ }
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Spawn `cmake --help` and resolve its stdout. Resolves null (never rejects)
36
+ * when cmake is absent from PATH or exits non-zero — generator detection is
37
+ * best-effort, and an unavailable cmake simply means the caller falls back to
38
+ * leaving the generator unpinned, exactly as before this feature.
39
+ *
40
+ * @returns {Promise<string|null>}
41
+ */
42
+ function runCmakeHelp() {
43
+ return new Promise((resolve) => {
44
+ let out = '';
45
+ const proc = spawn('cmake', ['--help'], {
46
+ shell: process.platform === 'win32',
47
+ });
48
+ proc.stdout.on('data', (chunk) => { out += chunk.toString(); });
49
+ // cmake not on PATH → detection unavailable; recoverable, fall back to null.
50
+ proc.on('error', () => resolve(null));
51
+ proc.on('close', (code) => resolve(code === 0 ? out : null));
52
+ });
53
+ }
54
+
55
+ // Per-process memo of the production (no-injection) probe result. The default
56
+ // generator can't change mid-run, so a single `cmake --help` spawn is enough
57
+ // for the whole process — important when one process drives many installs
58
+ // (e.g. a source-build that resolves several deps, or the test suite). Calls
59
+ // that inject `platform`/`runHelp` (tests) bypass this cache to stay hermetic.
60
+ let cachedDefault; // undefined = unprobed; string|null = probed result
61
+
62
+ /**
63
+ * Detect the Visual Studio generator CMake would use by default on this
64
+ * machine. On Windows with VS installed, CMake's implicit default is the
65
+ * newest installed VS — which builds host x64 unless `-A`/architecture is
66
+ * pinned. Returns that VS generator name so callers can pin it (and the
67
+ * matching `-A`), or null when the default is not a VS generator
68
+ * (non-Windows, or VS not installed → today's behaviour, no pin).
69
+ *
70
+ * `platform` and `runHelp` are injectable for tests; the defaults probe the
71
+ * real machine and memoise the result per-process.
72
+ *
73
+ * @param {{ platform?: string, runHelp?: () => Promise<string|null> }} [opts]
74
+ * @returns {Promise<string|null>}
75
+ */
76
+ async function detectDefaultGenerator({ platform, runHelp } = {}) {
77
+ const injected = platform !== undefined || runHelp !== undefined;
78
+ if (!injected && cachedDefault !== undefined) return cachedDefault;
79
+
80
+ const plat = platform ?? process.platform;
81
+ let result = null;
82
+ if (plat === 'win32') {
83
+ const helpText = await (runHelp ?? runCmakeHelp)();
84
+ const name = helpText ? parseDefaultGenerator(helpText) : null;
85
+ // Only VS generators need an arch pin; everything else (Ninja, NMake, …)
86
+ // already honours the active vcvarsall env for target arch.
87
+ result = isVisualStudioGenerator(name) ? name : null;
88
+ }
89
+
90
+ if (!injected) cachedDefault = result;
91
+ return result;
92
+ }
93
+
94
+ /**
95
+ * Resolve the generator a CMake invocation will actually use, given an
96
+ * optional explicit choice (CLI `--generator` or a recipe `build.generator`)
97
+ * and the active profile.
98
+ *
99
+ * - An explicit non-empty generator always wins, verbatim.
100
+ * - Otherwise, on a Windows/MSVC profile, fall back to the detected default
101
+ * VS generator so callers can pin `-A`/architecture against it.
102
+ * - Otherwise null — the caller leaves the generator unpinned (CMake picks
103
+ * its default), exactly as before this feature. On non-Windows toolchains
104
+ * target arch is the compiler's job, not the generator's.
105
+ *
106
+ * @param {{ explicit?: string|null,
107
+ * profile?: { os?: string, compiler?: string }|null,
108
+ * detect?: () => Promise<string|null> }} [args]
109
+ * @returns {Promise<string|null>}
110
+ */
111
+ async function resolveEffectiveGenerator({ explicit = null, profile = null, detect = detectDefaultGenerator } = {}) {
112
+ if (typeof explicit === 'string' && explicit.trim()) return explicit;
113
+ if (profile && profile.os === 'Windows' && profile.compiler === 'msvc') {
114
+ return detect();
115
+ }
116
+ return null;
117
+ }
118
+
119
+ module.exports = {
120
+ resolveEffectiveGenerator,
121
+ detectDefaultGenerator,
122
+ // Exported for unit tests
123
+ parseDefaultGenerator,
124
+ };
@@ -7,6 +7,7 @@ const { spawn } = require('child_process');
7
7
  const { normalizeRecipe, hasBuildRecipe, VALID_CMAKE_CONFIGS, DEFAULT_RECIPE } = require('../build/recipe');
8
8
  const { loadMsvcEnv } = require('../build/msvc-env');
9
9
  const { archToVsPlatform, isVisualStudioGenerator } = require('../toolchain');
10
+ const { resolveEffectiveGenerator } = require('../build/default-generator');
10
11
  const { LOCAL_OVERLAY_FILENAME, readLocalOverlay } = require('../conf');
11
12
  const { resolveBinaryDirRoot, DEFAULT_BINARY_DIR_ROOT } = require('../binary-dir');
12
13
  const { buildContext } = require('../context');
@@ -384,9 +385,17 @@ async function build(argv) {
384
385
  if (recipe.generators.length > 0) [presetGenerator] = recipe.generators;
385
386
  } catch { /* templates need options → skip env activation */ }
386
387
  }
387
- // CLI --generator wins that is the generator CMake will actually use,
388
- // so it is the one that determines whether vcvarsall must be activated.
389
- if (!effectiveGenerator) effectiveGenerator = presetGenerator;
388
+ // CLI --generator wins, then the recipe's generator, then on a
389
+ // Windows/MSVC profile with neither CMake's detected default VS
390
+ // generator. Resolving the default here keeps the `-A` pin below and the
391
+ // vcvarsall-skip decision consistent with the generator `cmake --preset`
392
+ // will actually use (install pins the same generator into the preset).
393
+ // Non-Windows / non-MSVC → null, unchanged from before.
394
+ // See claude/PLAN-VS-ARCH-PIN.md.
395
+ effectiveGenerator = await resolveEffectiveGenerator({
396
+ explicit: effectiveGenerator ?? presetGenerator,
397
+ profile: activeProfile,
398
+ });
390
399
  cmakeEnv = await loadMsvcEnv({
391
400
  profile: activeProfile,
392
401
  skipForGenerator: effectiveGenerator,
@@ -18,6 +18,7 @@ const { resolveBinaryDirRoot } = require('../binary
18
18
  const { hashProfile, formatProfile, mergeProfile } = require('../profile');
19
19
  const { generateToolchain, generateCMakePresets } = require('../toolchain');
20
20
  const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
21
+ const { resolveEffectiveGenerator } = require('../build/default-generator');
21
22
  const { buildContext } = require('../context');
22
23
  const {
23
24
  normalizeOptionsDeclaration,
@@ -661,6 +662,21 @@ async function install(argv) {
661
662
  }
662
663
  }
663
664
 
665
+ // When no generator was pinned by a recipe (the common recipe-less consumer
666
+ // case), CMake falls back to its platform default. On Windows that's the
667
+ // newest Visual Studio, which silently targets host x64 unless
668
+ // `-A`/architecture is pinned — so an x86 (or any non-host-arch) profile
669
+ // would produce mismatched binaries that find_package() later rejects with a
670
+ // pointer-size error. Resolve the default to a concrete VS generator so the
671
+ // preset carries both `generator` and `architecture` (buildConfigurePreset
672
+ // pins arch only when it knows the generator is VS). Non-Windows / non-MSVC
673
+ // → null, leaving the preset generator-less exactly as before. See
674
+ // claude/PLAN-VS-ARCH-PIN.md.
675
+ presetGenerator = await resolveEffectiveGenerator({
676
+ explicit: presetGenerator,
677
+ profile: baseProfile,
678
+ });
679
+
664
680
  // PLAN-CONF.md §4.3 / §5 presets sink: fold effectiveConf's
665
681
  // cmake.cache namespace into the preset's cacheVariables. Runs
666
682
  // regardless of whether the project has its own build recipe —