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/README.md +465 -19
- package/bin/wyvrn.js +175 -9
- package/claude/skills/wyvrnpm.skill +0 -0
- package/cmake/macros.cmake +51 -0
- package/cmake/variables.cmake +13 -3
- package/package.json +1 -1
- package/src/auth.js +66 -0
- package/src/build/cache.js +196 -0
- package/src/build/index.js +23 -2
- package/src/build/msvc-env.js +50 -7
- package/src/build/recipe.js +36 -3
- package/src/commands/add.js +140 -0
- package/src/commands/build.js +111 -7
- package/src/commands/cache.js +189 -0
- package/src/commands/configure.js +15 -4
- package/src/commands/init.js +36 -0
- package/src/commands/install-skill.js +8 -0
- package/src/commands/install.js +271 -5
- package/src/commands/profile.js +70 -7
- package/src/commands/publish.js +41 -4
- package/src/commands/show.js +14 -4
- package/src/compat.js +21 -9
- package/src/conf/index.js +391 -0
- package/src/conf/namespaces.js +94 -0
- package/src/download.js +136 -148
- package/src/logger.js +30 -2
- package/src/manifest.js +34 -7
- package/src/profile.js +148 -12
- package/src/resolve.js +47 -7
- package/src/settings-overrides.js +152 -0
- package/src/toolchain/index.js +71 -0
- package/src/toolchain/presets.js +108 -31
- package/src/toolchain/template.cmake +11 -0
- package/src/upload-built.js +11 -2
- package/src/zip-safe.js +52 -0
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:
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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
|
|
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]) =>
|
|
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
|
-
*
|
|
390
|
-
*
|
|
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
|
|
397
|
-
if (
|
|
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(
|
|
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:
|
|
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
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
+
};
|
package/src/toolchain/index.js
CHANGED
|
@@ -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
|
};
|