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.
package/src/profile.js CHANGED
@@ -162,6 +162,13 @@ function detectProfile() {
162
162
  };
163
163
  }
164
164
 
165
+ // Profile fields that participate in profileHash — mirror Conan's
166
+ // `[settings]` strictly. Anything else at the top level (e.g. `conf`)
167
+ // is non-ABI and must never fold into the hash (PLAN-CONF.md constitution).
168
+ const HASHED_PROFILE_FIELDS = new Set([
169
+ 'os', 'arch', 'compiler', 'compiler.version', 'compiler.cppstd', 'compiler.runtime',
170
+ ]);
171
+
165
172
  /**
166
173
  * Compute a stable 16-char SHA256 prefix identifying a unique build.
167
174
  * Inputs:
@@ -169,14 +176,19 @@ function detectProfile() {
169
176
  * - `options`: (optional) recipe-declared feature toggles resolved to
170
177
  * their effective values for this build.
171
178
  *
172
- * Backward-compat contract: when `options` is null / undefined / empty,
173
- * the hash is byte-identical to what pre-F1 wyvrnpm produced. Packages
174
- * without an `options` block keep their existing registry addresses.
175
- * The caller therefore passes `null` (not `{}`) to mean "this recipe
176
- * has no options declared" consistent with what
177
- * `resolveEffectiveOptions` returns in that case.
179
+ * Backward-compat contract:
180
+ * - When `options` is null / undefined / empty, the hash is
181
+ * byte-identical to what pre-F1 wyvrnpm produced. Packages
182
+ * without an `options` block keep their existing registry
183
+ * addresses. The caller passes `null` (not `{}`) to mean "this
184
+ * recipe has no options declared" — consistent with what
185
+ * `resolveEffectiveOptions` returns in that case.
186
+ * - When a profile carries a `conf` block (phase 2 — PLAN-CONF.md),
187
+ * the hash is byte-identical to the same profile WITHOUT `conf`.
188
+ * Build-time knobs are non-ABI by design (constitution).
178
189
  *
179
- * See claude/PLAN-OPTIONS.md §3.2 for the full design.
190
+ * See claude/PLAN-OPTIONS.md §3.2 and claude/PLAN-CONF.md for the
191
+ * full design.
180
192
  *
181
193
  * @param {WyvrnProfile} profile
182
194
  * @param {Record<string, any>|null} [options] effective options (after defaults + overrides)
@@ -185,7 +197,10 @@ function detectProfile() {
185
197
  function hashProfile(profile, options = null) {
186
198
  const significant = Object.fromEntries(
187
199
  Object.entries(profile)
188
- .filter(([, v]) => v !== null && v !== undefined && v !== '*' && v !== '')
200
+ .filter(([k, v]) =>
201
+ HASHED_PROFILE_FIELDS.has(k) &&
202
+ v !== null && v !== undefined && v !== '*' && v !== '',
203
+ )
189
204
  .sort(([a], [b]) => a.localeCompare(b)),
190
205
  );
191
206
 
@@ -267,6 +282,8 @@ function isShaOnRemote(sha, cwd) {
267
282
 
268
283
  /**
269
284
  * Format a profile for human-readable display (Conan-style).
285
+ * Emits a `[conf]` block with flat dotted keys when the profile
286
+ * carries a non-empty `conf` block (PLAN-CONF.md phase 2).
270
287
  *
271
288
  * @param {WyvrnProfile} profile
272
289
  * @returns {string}
@@ -282,6 +299,23 @@ function formatProfile(profile) {
282
299
  lines.push(`${key}=${val}`);
283
300
  }
284
301
  }
302
+
303
+ // [conf] block, Conan-style. Rendered flat so it's clear these are
304
+ // dotted namespaced keys, not nested JSON. Alphabetical for stable diffs.
305
+ const confBlock = profile.conf;
306
+ if (confBlock && typeof confBlock === 'object' && !Array.isArray(confBlock)) {
307
+ // Lazy-require to avoid circular dep (src/conf/* → src/manifest → src/profile).
308
+ const { flattenConf } = require('./conf');
309
+ let flat;
310
+ try { flat = flattenConf(confBlock); } catch { flat = null; }
311
+ if (flat && Object.keys(flat).length > 0) {
312
+ lines.push('[conf]');
313
+ for (const key of Object.keys(flat).sort()) {
314
+ lines.push(`${key}=${flat[key]}`);
315
+ }
316
+ }
317
+ }
318
+
285
319
  return lines.join('\n');
286
320
  }
287
321
 
@@ -386,15 +420,116 @@ function listProfiles() {
386
420
  }
387
421
 
388
422
  /**
389
- * Load the active build profile by name. Falls back to auto-detection if the
390
- * named profile file does not exist yet.
423
+ * Resolve a profile-inheritance chain (F8). A profile may declare
424
+ *
425
+ * { "extends": "base-profile", ... }
426
+ * { "extends": ["base1", "base2"], ... } // later wins over earlier
427
+ *
428
+ * `extends` is resolved recursively; the child's fields overlay the
429
+ * merged parent(s). Cycle detection throws a clear error. Missing
430
+ * parents throw — we don't silently drop `extends`, that would hide
431
+ * typos.
432
+ *
433
+ * Merge rules:
434
+ * - `[settings]` fields: child overrides parent, per-key.
435
+ * - `conf` block: parent + child flattened with child wins per-leaf
436
+ * (same pattern as PLAN-CONF.md §4.2 within the single profile).
437
+ * - `extends` itself is stripped from the resolved output — the
438
+ * returned object carries no inheritance metadata, so existing
439
+ * consumers (formatProfile, hashProfile, readProfileConf) work
440
+ * unchanged.
441
+ *
442
+ * @param {string} name
443
+ * @param {string[]} [stack] visited names — for cycle detection
444
+ * @returns {WyvrnProfile|null} Fully-resolved profile, or null if `name`
445
+ * does not exist on disk at the root of the chain.
446
+ */
447
+ function resolveProfileChain(name, stack = []) {
448
+ if (stack.includes(name)) {
449
+ throw new Error(
450
+ `profile inheritance cycle: ${[...stack, name].join(' → ')}`,
451
+ );
452
+ }
453
+
454
+ const raw = readProfile(name);
455
+ if (!raw) return null;
456
+
457
+ const rawExtends = raw.extends;
458
+ if (rawExtends === undefined || rawExtends === null) {
459
+ // Flat profile — just strip any accidental `extends` and return.
460
+ const { extends: _, ...rest } = raw;
461
+ return rest;
462
+ }
463
+
464
+ const parents = Array.isArray(rawExtends) ? rawExtends : [rawExtends];
465
+ for (const p of parents) {
466
+ if (typeof p !== 'string' || p.length === 0) {
467
+ throw new Error(
468
+ `profile "${name}": extends entries must be non-empty strings, got ${JSON.stringify(p)}`,
469
+ );
470
+ }
471
+ }
472
+
473
+ // Merge parents left-to-right, then overlay the child.
474
+ let merged = {};
475
+ for (const parentName of parents) {
476
+ const parent = resolveProfileChain(parentName, [...stack, name]);
477
+ if (!parent) {
478
+ throw new Error(
479
+ `profile "${name}": extends parent "${parentName}" not found ` +
480
+ `at ${getProfilesDir()}`,
481
+ );
482
+ }
483
+ merged = mergeResolvedProfiles(merged, parent);
484
+ }
485
+
486
+ const { extends: _, ...child } = raw;
487
+ return mergeResolvedProfiles(merged, child);
488
+ }
489
+
490
+ /**
491
+ * Merge two already-resolved profile objects. Top-level `[settings]`
492
+ * fields: right wins. `conf` block: per-leaf merge, right wins.
493
+ * @param {object} base
494
+ * @param {object} overlay
495
+ * @returns {object}
496
+ */
497
+ function mergeResolvedProfiles(base, overlay) {
498
+ const out = { ...base, ...overlay };
499
+ // Nullable override semantics: `null` on the overlay explicitly
500
+ // unsets a field from the base (matches how Conan profiles
501
+ // handle `compiler.runtime=None`).
502
+ for (const k of Object.keys(overlay)) {
503
+ if (overlay[k] === null) delete out[k];
504
+ }
505
+ // Conf block is merged per-leaf, not per-branch.
506
+ if (base.conf || overlay.conf) {
507
+ const { flattenConf, unflattenConf } = require('./conf');
508
+ let baseFlat = {};
509
+ let overlayFlat = {};
510
+ try { if (base.conf) baseFlat = flattenConf(base.conf); } catch { /* leave empty */ }
511
+ try { if (overlay.conf) overlayFlat = flattenConf(overlay.conf); } catch { /* leave empty */ }
512
+ const merged = { ...baseFlat, ...overlayFlat };
513
+ if (Object.keys(merged).length > 0) {
514
+ out.conf = unflattenConf(merged);
515
+ } else {
516
+ delete out.conf;
517
+ }
518
+ }
519
+ return out;
520
+ }
521
+
522
+ /**
523
+ * Load the active build profile by name, resolving any `extends` chain.
524
+ * Falls back to auto-detection if the named profile file does not
525
+ * exist yet.
391
526
  *
392
527
  * @param {string} [name='default']
393
528
  * @returns {{ profile: WyvrnProfile, fromFile: boolean }}
394
529
  */
395
530
  function loadProfile(name = 'default') {
396
- const stored = readProfile(name);
397
- if (stored) return { profile: stored, fromFile: true };
531
+ const resolved = resolveProfileChain(name);
532
+ if (resolved) return { profile: resolved, fromFile: true };
398
533
  return { profile: detectProfile(), fromFile: false };
399
534
  }
400
535
 
@@ -415,6 +550,7 @@ module.exports = {
415
550
  deleteProfile,
416
551
  listProfiles,
417
552
  loadProfile,
553
+ resolveProfileChain,
418
554
  };
419
555
 
420
556
  /**
package/src/resolve.js CHANGED
@@ -141,15 +141,27 @@ async function resolveLatestVersion(name, packageSources, platform, httpClient)
141
141
  /**
142
142
  * Fetches razer.json for a single package from the first source that responds.
143
143
  *
144
+ * When `preferredProfileHash` is provided (EVALUATION.md F13 — closes the
145
+ * "picks arbitrary profileHash" latent bug), the v2 lookup tries that hash
146
+ * first and only falls back to the first-available profile if the preferred
147
+ * hash isn't on the registry. Dependency declarations are invariant across
148
+ * profiles today, so picking any hash gives the same answer — but the
149
+ * consumer's own hash is stable across re-resolves and future-proofs
150
+ * against any schema that starts varying wyvrn.json per profile.
151
+ *
144
152
  * @param {string} name
145
153
  * @param {string} version
146
154
  * @param {string[]} packageSources
147
155
  * @param {string} platform
148
156
  * @param {Function} httpClient
149
157
  * @param {Map<string, object>} cache - In-flight / completed fetch cache.
158
+ * @param {string|null} [preferredProfileHash] - try this hash first.
150
159
  * @returns {Promise<object|null>} Parsed razer.json or null if not found anywhere.
151
160
  */
152
- async function fetchPackageManifest(name, version, packageSources, platform, httpClient, cache) {
161
+ async function fetchPackageManifest(
162
+ name, version, packageSources, platform, httpClient, cache,
163
+ preferredProfileHash = null,
164
+ ) {
153
165
  const cacheKey = `${name}@${version}`;
154
166
  if (cache.has(cacheKey)) {
155
167
  return cache.get(cacheKey);
@@ -158,14 +170,19 @@ async function fetchPackageManifest(name, version, packageSources, platform, htt
158
170
  for (const source of packageSources) {
159
171
  const base = source.replace(/\/$/, '');
160
172
 
161
- // v2: look up any available profile hash from versions.json, then fetch that manifest
173
+ // v2: prefer the consumer's own profileHash; fall back to the first
174
+ // available if that hash isn't published.
162
175
  try {
163
176
  const idxResp = await httpClient(`${base}/v2/${name}/versions.json`);
164
177
  if (idxResp.ok) {
165
178
  const idx = await idxResp.json();
166
179
  const profiles = idx?.versions?.[version]?.profiles;
167
180
  if (profiles) {
168
- const profileHash = Object.keys(profiles)[0];
181
+ const available = Object.keys(profiles);
182
+ const profileHash =
183
+ (preferredProfileHash && available.includes(preferredProfileHash))
184
+ ? preferredProfileHash
185
+ : available[0];
169
186
  if (profileHash) {
170
187
  const mResp = await httpClient(`${base}/v2/${name}/${version}/${profileHash}/wyvrn.json`);
171
188
  if (mResp.ok) {
@@ -215,15 +232,37 @@ async function fetchPackageManifest(name, version, packageSources, platform, htt
215
232
  * PLAN-OPTIONS) use this to avoid a second round of HTTP round-trips.
216
233
  * Passing `null` / omitting it keeps the legacy behaviour.
217
234
  *
235
+ * Per-package source pinning (EVALUATION.md S4): when `pinnedSources` is
236
+ * provided, any entry `name → url` restricts that package's manifest +
237
+ * latest + versions lookups to just that URL — typo-squatting and mirror
238
+ * shadowing are closed for pinned deps. Pins propagate to direct deps
239
+ * only; transitive deps continue to fan out across all sources (a pinned
240
+ * dep's own recipe is authoritative for its own transitives).
241
+ *
218
242
  * @param {Record<string, string>} rootDeps - Direct dependencies { name: version }.
219
243
  * @param {string[]} packageSources - Ordered list of base URLs to try.
220
244
  * @param {string} platform - Target platform string (e.g. "win_x64").
221
245
  * @param {Function} httpClient - fetch-compatible function.
222
246
  * @param {Map<string, string>} [lockedVersions]
223
247
  * @param {Map<string, object>} [manifestsOut] - populated in-place when provided
248
+ * @param {string|null} [preferredProfileHash] - consumer's own profileHash;
249
+ * threaded to fetchPackageManifest so v2 manifest lookups prefer the
250
+ * consumer's own build entry over an arbitrary one (EVALUATION.md F13).
251
+ * @param {Map<string,string>|null} [pinnedSources] - per-package source URL pin (S4).
224
252
  * @returns {Promise<Map<string, string>>} Resolved map of name -> version.
225
253
  */
226
- async function resolveDependencies(rootDeps, packageSources, platform, httpClient, lockedVersions = new Map(), manifestsOut = null) {
254
+ async function resolveDependencies(
255
+ rootDeps, packageSources, platform, httpClient,
256
+ lockedVersions = new Map(), manifestsOut = null,
257
+ preferredProfileHash = null,
258
+ pinnedSources = null,
259
+ ) {
260
+ // For pinned deps, restrict the source list to the single pinned URL.
261
+ // `null` / no pin → fall back to the full caller-supplied list.
262
+ const sourcesFor = (name) => {
263
+ const pin = pinnedSources?.get?.(name);
264
+ return pin ? [pin] : packageSources;
265
+ };
227
266
  /** @type {Map<string, string>} Final resolved versions. */
228
267
  const resolved = new Map();
229
268
 
@@ -254,7 +293,7 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
254
293
  const publishedCache = new Map();
255
294
  const fetchPublished = async (name) => {
256
295
  if (publishedCache.has(name)) return publishedCache.get(name);
257
- const list = await fetchPublishedVersions(name, packageSources, httpClient);
296
+ const list = await fetchPublishedVersions(name, sourcesFor(name), httpClient);
258
297
  publishedCache.set(name, list);
259
298
  return list;
260
299
  };
@@ -280,7 +319,7 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
280
319
  // Skip this for linked packages which have "linked:" prefix.
281
320
  if (version === 'latest') {
282
321
  try {
283
- version = await resolveLatestVersion(name, packageSources, platform, httpClient);
322
+ version = await resolveLatestVersion(name, sourcesFor(name), platform, httpClient);
284
323
  log.info(`Resolved "${name}@latest" → ${version}`);
285
324
  } catch (err) {
286
325
  log.warn(err.message);
@@ -405,10 +444,11 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
405
444
  manifest = await fetchPackageManifest(
406
445
  name,
407
446
  version,
408
- packageSources,
447
+ sourcesFor(name),
409
448
  platform,
410
449
  httpClient,
411
450
  manifestCache,
451
+ preferredProfileHash,
412
452
  );
413
453
  }
414
454
 
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ // Pattern-based settings overrides (EVALUATION.md F11).
4
+ //
5
+ // `wyvrn.json` may declare a top-level `settingsOverrides` block whose
6
+ // keys are glob patterns over dependency names. Every matching pattern
7
+ // contributes to the resolved dep's `settings` merge; the per-dep
8
+ // literal settings (inside `dependencies`) still win over patterns.
9
+ //
10
+ // Conan users know this feature as profile-side `boost*:compiler.cppstd=17`.
11
+ // Ours lives in the manifest because that's where literal settings
12
+ // overrides already live — one fewer concept for recipe authors.
13
+ //
14
+ // Pure, no I/O. Exported helpers:
15
+ // - normalizeSettingsOverrides: validate + sort by specificity
16
+ // - resolveOverrideSettings: compute the merged settings for a dep name
17
+
18
+ /**
19
+ * Valid profile field keys that settings overrides may target.
20
+ * Mirrors `HASHED_PROFILE_FIELDS` in src/profile.js, but as the allow-
21
+ * list for OVERRIDE values (os/arch/compiler/etc.). If a pattern
22
+ * override tries to set something else, reject at parse time rather
23
+ * than silently letting it through into mergeProfile.
24
+ */
25
+ const ALLOWED_SETTINGS_KEYS = new Set([
26
+ 'os', 'arch', 'compiler', 'compiler.version', 'compiler.cppstd', 'compiler.runtime',
27
+ ]);
28
+
29
+ /**
30
+ * Convert a glob pattern to a RegExp. Supports `*` (any chars),
31
+ * `?` (single char), literal everything else. No brace expansion,
32
+ * no negation — keep the mental model small.
33
+ *
34
+ * @param {string} glob
35
+ * @returns {RegExp}
36
+ */
37
+ function globToRegExp(glob) {
38
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&');
39
+ const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
40
+ return new RegExp(`^${pattern}$`);
41
+ }
42
+
43
+ /**
44
+ * Specificity heuristic: fewer wildcards = more specific. Ties broken
45
+ * by longer literal prefix, then lexical order for determinism. More
46
+ * specific patterns get applied LAST so they override less-specific
47
+ * ones in the merge.
48
+ *
49
+ * @param {string} glob
50
+ * @returns {[number, number, string]} sort key — lower = less specific
51
+ */
52
+ function specificityKey(glob) {
53
+ const wildcards = (glob.match(/[*?]/g) ?? []).length;
54
+ const literalPrefix = glob.match(/^[^*?]*/)?.[0].length ?? 0;
55
+ // Invert sort order: more wildcards → lower score; more prefix → higher score.
56
+ return [-wildcards, literalPrefix, glob];
57
+ }
58
+
59
+ /**
60
+ * Validate + sort a raw `settingsOverrides` block. Returns an array of
61
+ * `{ pattern, regex, settings }` in application order (least-specific
62
+ * first). A string pattern alone (literal `"openssl"`) works too — it
63
+ * matches only that exact name.
64
+ *
65
+ * Throws on:
66
+ * - non-object block
67
+ * - non-string keys (JSON can't produce these but defensive)
68
+ * - non-object values
69
+ * - empty pattern strings
70
+ * - unknown settings keys in a value
71
+ * - non-string value leaves (settings values must be strings like
72
+ * the profile itself: "dynamic", "17", "gcc")
73
+ *
74
+ * @param {unknown} block
75
+ * @returns {Array<{ pattern: string, regex: RegExp, settings: Record<string, string> }>}
76
+ */
77
+ function normalizeSettingsOverrides(block) {
78
+ if (block === undefined || block === null) return [];
79
+ if (typeof block !== 'object' || Array.isArray(block)) {
80
+ throw new Error('settingsOverrides must be an object mapping globs to settings');
81
+ }
82
+
83
+ const entries = [];
84
+ for (const [rawPattern, rawValue] of Object.entries(block)) {
85
+ if (typeof rawPattern !== 'string' || rawPattern.length === 0) {
86
+ throw new Error(`settingsOverrides: pattern must be a non-empty string, got ${JSON.stringify(rawPattern)}`);
87
+ }
88
+ if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) {
89
+ throw new Error(`settingsOverrides["${rawPattern}"]: value must be an object of settings`);
90
+ }
91
+
92
+ const settings = {};
93
+ for (const [k, v] of Object.entries(rawValue)) {
94
+ if (!ALLOWED_SETTINGS_KEYS.has(k)) {
95
+ throw new Error(
96
+ `settingsOverrides["${rawPattern}"]: unknown key "${k}" — ` +
97
+ `allowed: ${[...ALLOWED_SETTINGS_KEYS].join(', ')}`,
98
+ );
99
+ }
100
+ if (typeof v !== 'string' || v.length === 0) {
101
+ throw new Error(
102
+ `settingsOverrides["${rawPattern}"].${k}: value must be a non-empty string, ` +
103
+ `got ${JSON.stringify(v)}`,
104
+ );
105
+ }
106
+ settings[k] = v;
107
+ }
108
+
109
+ entries.push({ pattern: rawPattern, regex: globToRegExp(rawPattern), settings });
110
+ }
111
+
112
+ // Sort by specificity ascending so more-specific patterns apply LAST
113
+ // (their settings therefore win on per-key merge).
114
+ entries.sort((a, b) => {
115
+ const ka = specificityKey(a.pattern);
116
+ const kb = specificityKey(b.pattern);
117
+ for (let i = 0; i < ka.length; i++) {
118
+ if (ka[i] < kb[i]) return -1;
119
+ if (ka[i] > kb[i]) return 1;
120
+ }
121
+ return 0;
122
+ });
123
+
124
+ return entries;
125
+ }
126
+
127
+ /**
128
+ * Merge all matching pattern overrides for `depName` into a flat
129
+ * settings object. Less-specific patterns apply first; more-specific
130
+ * patterns overlay on top. Does NOT include the literal per-dep
131
+ * `settings` (caller layers that on after — literal always wins).
132
+ *
133
+ * @param {string} depName
134
+ * @param {ReturnType<typeof normalizeSettingsOverrides>} normalized
135
+ * @returns {Record<string, string>}
136
+ */
137
+ function resolveOverrideSettings(depName, normalized) {
138
+ const out = {};
139
+ for (const entry of normalized) {
140
+ if (entry.regex.test(depName)) {
141
+ Object.assign(out, entry.settings);
142
+ }
143
+ }
144
+ return out;
145
+ }
146
+
147
+ module.exports = {
148
+ ALLOWED_SETTINGS_KEYS,
149
+ globToRegExp,
150
+ normalizeSettingsOverrides,
151
+ resolveOverrideSettings,
152
+ };
@@ -17,6 +17,73 @@ function getBundledCmakeDir() {
17
17
  return dir.replace(/\\/g, '/');
18
18
  }
19
19
 
20
+ // Arch bit-width mapping. Kept exhaustive against `ARCH_MAP` in profile.js
21
+ // so any arch a profile can hold has a deterministic "32"/"64" suffix.
22
+ // Unknown arch strings fall back to an empty suffix — the consumer's
23
+ // CMakeLists should then use another discriminator (e.g. profile hash).
24
+ const ARCH_BIT_SUFFIX = {
25
+ x86_64: '64',
26
+ x86: '32',
27
+ armv8: '64',
28
+ armv7: '32',
29
+ ppc64le: '64',
30
+ s390x: '64',
31
+ mips: '32',
32
+ mipsel: '32',
33
+ };
34
+
35
+ /**
36
+ * Map a profile `arch` value to the canonical "64" / "32" suffix that
37
+ * `WYVRN_ARCH_SUFFIX` exposes to consumer CMakeLists. Empty string for
38
+ * unknown arches — callers can pattern-match on that to fall back.
39
+ *
40
+ * @param {string|null|undefined} arch
41
+ * @returns {string}
42
+ */
43
+ function archToBitSuffix(arch) {
44
+ if (!arch) return '';
45
+ return ARCH_BIT_SUFFIX[arch] ?? '';
46
+ }
47
+
48
+ // Visual Studio generator `-A <platform>` mapping. VS defaults to x64
49
+ // when `-A` is unspecified — which silently wins against a 32-bit profile
50
+ // and causes consumer-side `find_package()` to reject 32-bit artefacts
51
+ // with "The version found is not compatible" (a CMake pointer-size check,
52
+ // not a version-number check). Auto-emitting `-A` keeps VS + x86 honest.
53
+ const ARCH_VS_PLATFORM = {
54
+ x86_64: 'x64',
55
+ x86: 'Win32',
56
+ armv8: 'ARM64',
57
+ armv7: 'ARM',
58
+ };
59
+
60
+ /**
61
+ * Map a profile arch to the Visual Studio generator's `-A <platform>` value.
62
+ * Returns null for arches that don't map to a known VS platform — callers
63
+ * should skip `-A` in that case and let the user pass one explicitly.
64
+ *
65
+ * @param {string|null|undefined} arch
66
+ * @returns {string|null}
67
+ */
68
+ function archToVsPlatform(arch) {
69
+ if (!arch) return null;
70
+ return ARCH_VS_PLATFORM[arch] ?? null;
71
+ }
72
+
73
+ /**
74
+ * True when the generator name identifies a Visual Studio generator (e.g.
75
+ * "Visual Studio 17 2022"). VS generators require `-A <platform>` /
76
+ * `architecture` in the preset; everyone else (Ninja, Make, Xcode, …)
77
+ * ignores it.
78
+ *
79
+ * @param {string|null|undefined} name
80
+ * @returns {boolean}
81
+ */
82
+ function isVisualStudioGenerator(name) {
83
+ if (!name) return false;
84
+ return /^Visual Studio \d/.test(name);
85
+ }
86
+
20
87
  /**
21
88
  * Substitute `{{KEY}}` placeholders in a template string.
22
89
  *
@@ -54,6 +121,7 @@ function buildPlaceholders({ profile, profileName, profileHash, packageNames })
54
121
  PROFILE_HASH: profileHash,
55
122
  PROFILE_OS: profile.os ?? '',
56
123
  PROFILE_ARCH: profile.arch ?? '',
124
+ PROFILE_ARCH_SUFFIX: archToBitSuffix(profile.arch),
57
125
  PROFILE_COMPILER: profile.compiler ?? '',
58
126
  PROFILE_COMPILER_VERSION: profile['compiler.version'] ?? '',
59
127
  PROFILE_CPPSTD: profile['compiler.cppstd'] ?? '20',
@@ -135,7 +203,10 @@ module.exports = {
135
203
  generateToolchain,
136
204
  generateCMakePresets,
137
205
  getBundledCmakeDir,
206
+ archToVsPlatform,
207
+ isVisualStudioGenerator,
138
208
  // Exported for tests
209
+ archToBitSuffix,
139
210
  buildPlaceholders,
140
211
  renderTemplate,
141
212
  };