wyvrnpm 2.4.1 → 2.9.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/README.md +414 -16
- package/bin/wyvrn.js +154 -11
- package/claude/skills/wyvrnpm.skill +0 -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 +26 -4
- package/src/build/msvc-env.js +50 -7
- package/src/build/recipe.js +36 -3
- package/src/commands/add.js +2 -1
- package/src/commands/build.js +162 -36
- 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 +674 -400
- package/src/commands/link.js +320 -319
- package/src/commands/profile.js +237 -174
- package/src/commands/publish.js +435 -383
- package/src/commands/show.js +24 -9
- package/src/conf/index.js +391 -0
- package/src/conf/namespaces.js +94 -0
- package/src/context.js +230 -0
- package/src/download.js +135 -147
- package/src/http-fetch.js +53 -0
- package/src/logger.js +30 -2
- package/src/manifest.js +34 -7
- package/src/profile.js +148 -12
- package/src/providers/file.js +5 -19
- package/src/providers/http.js +26 -26
- package/src/providers/s3.js +29 -25
- package/src/resolve.js +47 -7
- package/src/settings-overrides.js +152 -0
- package/src/toolchain/presets.js +71 -31
- package/src/upload-built.js +11 -2
- package/src/zip-safe.js +52 -0
package/src/build/msvc-env.js
CHANGED
|
@@ -75,17 +75,35 @@ function findVcvarsall(vsInstallPath) {
|
|
|
75
75
|
return fs.existsSync(p) ? p : null;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The complete set of arch arguments `vcvarsall.bat` accepts AND that we
|
|
80
|
+
* are willing to splice into a shell command. Explicit allow-list — any
|
|
81
|
+
* value outside this set is rejected (EVALUATION.md S5).
|
|
82
|
+
*/
|
|
83
|
+
const VCVARSALL_ARCH_ALLOWLIST = new Set(['x64', 'x86', 'arm64', 'arm']);
|
|
84
|
+
|
|
85
|
+
const ARCH_MAP = {
|
|
86
|
+
x86_64: 'x64',
|
|
87
|
+
x86: 'x86',
|
|
88
|
+
armv8: 'arm64',
|
|
89
|
+
armv7: 'arm',
|
|
90
|
+
};
|
|
91
|
+
|
|
78
92
|
/**
|
|
79
93
|
* Map a wyvrnpm profile arch to the argument `vcvarsall.bat` expects.
|
|
94
|
+
* Throws on an unknown profile arch — silently defaulting to `x64` would
|
|
95
|
+
* (a) produce a build for the wrong target, and (b) hand any shell-unsafe
|
|
96
|
+
* input to cmd.exe through captureVcvarsEnv's template.
|
|
80
97
|
*/
|
|
81
98
|
function mapArch(profileArch) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
const arg = ARCH_MAP[profileArch];
|
|
100
|
+
if (!arg || !VCVARSALL_ARCH_ALLOWLIST.has(arg)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`unsupported MSVC profile arch ${JSON.stringify(profileArch)}; ` +
|
|
103
|
+
`expected one of ${Object.keys(ARCH_MAP).join(', ')}`,
|
|
104
|
+
);
|
|
88
105
|
}
|
|
106
|
+
return arg;
|
|
89
107
|
}
|
|
90
108
|
|
|
91
109
|
/**
|
|
@@ -105,6 +123,18 @@ function mapArch(profileArch) {
|
|
|
105
123
|
* @returns {Promise<object|null>}
|
|
106
124
|
*/
|
|
107
125
|
async function captureVcvarsEnv(vcvarsallPath, archArg) {
|
|
126
|
+
// Defense-in-depth at the shell boundary (EVALUATION.md S5). mapArch
|
|
127
|
+
// already restricts archArg to the allow-list; re-assert here so this
|
|
128
|
+
// function is safe to call directly (e.g. from a future caller that
|
|
129
|
+
// skips mapArch). Likewise refuse a vcvarsall path containing a
|
|
130
|
+
// double-quote — it would break out of the quoted command template.
|
|
131
|
+
if (!VCVARSALL_ARCH_ALLOWLIST.has(archArg)) {
|
|
132
|
+
throw new Error(`refusing to spawn cmd.exe with unsafe arch arg ${JSON.stringify(archArg)}`);
|
|
133
|
+
}
|
|
134
|
+
if (vcvarsallPath.includes('"')) {
|
|
135
|
+
throw new Error(`refusing to spawn cmd.exe with quote in vcvarsall path ${JSON.stringify(vcvarsallPath)}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
108
138
|
const MARKER = '___WYVRN_ENV_MARKER___';
|
|
109
139
|
// `call` is required so the batch file returns control instead of
|
|
110
140
|
// `exit`-ing the cmd shell; otherwise the `echo` + `set` after it
|
|
@@ -197,7 +227,18 @@ async function loadMsvcEnv({ profile, skipForGenerator }) {
|
|
|
197
227
|
return null;
|
|
198
228
|
}
|
|
199
229
|
|
|
200
|
-
|
|
230
|
+
let archArg;
|
|
231
|
+
try {
|
|
232
|
+
archArg = mapArch(profile.arch);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
// Unknown arch for an MSVC profile is genuinely a configuration bug
|
|
235
|
+
// (MSVC only ships toolsets for x64/x86/arm64/arm). Surface clearly
|
|
236
|
+
// and fall through to the ambient env — CMake will then error with
|
|
237
|
+
// a usable compiler-detection message instead of us silently
|
|
238
|
+
// cross-targeting x64.
|
|
239
|
+
log.warn(` ${err.message}`);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
201
242
|
log.info(` activating MSVC build env: vcvarsall.bat ${archArg} (${vsPath})`);
|
|
202
243
|
const env = await captureVcvarsEnv(vcvarsall, archArg);
|
|
203
244
|
if (!env) {
|
|
@@ -214,4 +255,6 @@ module.exports = {
|
|
|
214
255
|
findVsInstall,
|
|
215
256
|
findVcvarsall,
|
|
216
257
|
mapArch,
|
|
258
|
+
captureVcvarsEnv,
|
|
259
|
+
VCVARSALL_ARCH_ALLOWLIST,
|
|
217
260
|
};
|
package/src/build/recipe.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { substituteTemplateAll } = require('../options');
|
|
4
|
+
const { readRecipeConf } = require('../conf');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The `build` section of a source-repo `wyvrn.json` tells wyvrnpm how to
|
|
@@ -60,15 +61,22 @@ const SUPPORTED_SYSTEMS = new Set(['cmake']);
|
|
|
60
61
|
* no options), any `${options.*}` token in the recipe also fails fast —
|
|
61
62
|
* recipes don't get to reference undeclared options.
|
|
62
63
|
*
|
|
64
|
+
* When `manifestForConfCollisionCheck` is supplied, PLAN-CONF.md §4.7's
|
|
65
|
+
* collision detection runs: any `-DKEY=...` in `build.configure` whose
|
|
66
|
+
* KEY is ALSO declared in recipe-level `conf.cmake.cache` is a hard
|
|
67
|
+
* error. Recipes pick ONE route, avoiding silent duplication.
|
|
68
|
+
*
|
|
63
69
|
* @param {object|null|undefined} rawBuild
|
|
64
70
|
* @param {Record<string, any>|null} [effectiveOptions]
|
|
71
|
+
* @param {object|null} [manifestForConfCollisionCheck]
|
|
65
72
|
* @returns {{ system: string, generators: string[], configure: string[],
|
|
66
73
|
* buildArgs: string[], installDir: string, sourceSubdir: string,
|
|
67
74
|
* requiredTools: string[] }}
|
|
68
|
-
* @throws {Error} when `system` is set but not supported,
|
|
69
|
-
* `${options.<name>}` template references an undeclared option
|
|
75
|
+
* @throws {Error} when `system` is set but not supported, when a
|
|
76
|
+
* `${options.<name>}` template references an undeclared option,
|
|
77
|
+
* or when `build.configure` collides with `conf.cmake.cache`.
|
|
70
78
|
*/
|
|
71
|
-
function normalizeRecipe(rawBuild, effectiveOptions = null) {
|
|
79
|
+
function normalizeRecipe(rawBuild, effectiveOptions = null, manifestForConfCollisionCheck = null) {
|
|
72
80
|
const b = rawBuild ?? {};
|
|
73
81
|
const system = b.system ?? DEFAULT_RECIPE.system;
|
|
74
82
|
if (!SUPPORTED_SYSTEMS.has(system)) {
|
|
@@ -124,6 +132,31 @@ function normalizeRecipe(rawBuild, effectiveOptions = null) {
|
|
|
124
132
|
'build.buildArgs',
|
|
125
133
|
);
|
|
126
134
|
|
|
135
|
+
// PLAN-CONF.md §4.7: recipe-side collision between `build.configure`
|
|
136
|
+
// `-DKEY=...` args and declared `conf.cmake.cache.KEY` is a hard
|
|
137
|
+
// error. Forces the author to pick one route; avoids silent
|
|
138
|
+
// duplication where one route wins opaquely.
|
|
139
|
+
if (manifestForConfCollisionCheck) {
|
|
140
|
+
const recipeConfFlat = readRecipeConf(manifestForConfCollisionCheck);
|
|
141
|
+
const confCacheKeys = new Set(
|
|
142
|
+
Object.keys(recipeConfFlat)
|
|
143
|
+
.filter((k) => k.startsWith('cmake.cache.'))
|
|
144
|
+
.map((k) => k.slice('cmake.cache.'.length)),
|
|
145
|
+
);
|
|
146
|
+
if (confCacheKeys.size > 0) {
|
|
147
|
+
for (const arg of configure) {
|
|
148
|
+
const m = arg.match(/^-D([^=]+)=/);
|
|
149
|
+
if (m && confCacheKeys.has(m[1])) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`recipe collision: "${m[1]}" is set in both ` +
|
|
152
|
+
`build.configure (as ${JSON.stringify(arg)}) and ` +
|
|
153
|
+
`conf.cmake.cache. Pick one route — see claude/PLAN-CONF.md §4.7.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
127
160
|
return {
|
|
128
161
|
system,
|
|
129
162
|
generators,
|
package/src/commands/add.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const { readManifest, writeManifest } = require('../manifest');
|
|
6
6
|
const { resolveLatestVersion } = require('../resolve');
|
|
7
7
|
const { readConfig } = require('../config');
|
|
8
|
+
const { wyvrnFetch } = require('../http-fetch');
|
|
8
9
|
const log = require('../logger');
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -99,7 +100,7 @@ async function add(argv) {
|
|
|
99
100
|
versionToWrite = spec.version;
|
|
100
101
|
log.info(`${spec.name}: writing "${versionToWrite}" (as given)`);
|
|
101
102
|
} else {
|
|
102
|
-
const latest = await resolveLatestVersion(spec.name, packageSources, argv.platform,
|
|
103
|
+
const latest = await resolveLatestVersion(spec.name, packageSources, argv.platform, wyvrnFetch);
|
|
103
104
|
versionToWrite = `^${latest}`;
|
|
104
105
|
log.info(`${spec.name}: latest is ${latest} → writing "${versionToWrite}"`);
|
|
105
106
|
}
|
package/src/commands/build.js
CHANGED
|
@@ -4,12 +4,11 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawn } = require('child_process');
|
|
6
6
|
|
|
7
|
-
const {
|
|
8
|
-
const { loadProfile } = require('../profile');
|
|
9
|
-
const { readManifest } = require('../manifest');
|
|
10
|
-
const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
|
|
7
|
+
const { normalizeRecipe, hasBuildRecipe, VALID_CMAKE_CONFIGS, DEFAULT_RECIPE } = require('../build/recipe');
|
|
11
8
|
const { loadMsvcEnv } = require('../build/msvc-env');
|
|
12
9
|
const { archToVsPlatform, isVisualStudioGenerator } = require('../toolchain');
|
|
10
|
+
const { LOCAL_OVERLAY_FILENAME } = require('../conf');
|
|
11
|
+
const { buildContext } = require('../context');
|
|
13
12
|
const install = require('./install');
|
|
14
13
|
const log = require('../logger');
|
|
15
14
|
|
|
@@ -29,8 +28,10 @@ const log = require('../logger');
|
|
|
29
28
|
* @returns {{ stale: boolean, reason: string }}
|
|
30
29
|
*/
|
|
31
30
|
function isToolchainStale({ rootDir, manifestPath }) {
|
|
32
|
-
const lockPath
|
|
33
|
-
const toolchainPath
|
|
31
|
+
const lockPath = path.join(path.dirname(manifestPath), 'wyvrn.lock');
|
|
32
|
+
const toolchainPath = path.join(rootDir, 'wyvrn_internal', 'wyvrn_toolchain.cmake');
|
|
33
|
+
const localOverlayPath = path.join(rootDir, LOCAL_OVERLAY_FILENAME);
|
|
34
|
+
const presetPath = path.join(rootDir, 'CMakePresets.json');
|
|
34
35
|
|
|
35
36
|
if (!fs.existsSync(toolchainPath)) {
|
|
36
37
|
return { stale: true, reason: 'toolchain-missing' };
|
|
@@ -44,6 +45,19 @@ function isToolchainStale({ rootDir, manifestPath }) {
|
|
|
44
45
|
if (lockMtime > toolchainMtime) {
|
|
45
46
|
return { stale: true, reason: 'lock-newer-than-toolchain' };
|
|
46
47
|
}
|
|
48
|
+
|
|
49
|
+
// PLAN-CONF.md §7: if wyvrn.local.json was edited after the preset was
|
|
50
|
+
// written, the preset's cacheVariables no longer reflect the consumer's
|
|
51
|
+
// intent. Re-run install (which regenerates the preset) rather than
|
|
52
|
+
// silently running a stale configure.
|
|
53
|
+
if (fs.existsSync(localOverlayPath) && fs.existsSync(presetPath)) {
|
|
54
|
+
const overlayMtime = fs.statSync(localOverlayPath).mtimeMs;
|
|
55
|
+
const presetMtime = fs.statSync(presetPath).mtimeMs;
|
|
56
|
+
if (overlayMtime > presetMtime) {
|
|
57
|
+
return { stale: true, reason: 'local-overlay-newer-than-preset' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
47
61
|
return { stale: false, reason: 'fresh' };
|
|
48
62
|
}
|
|
49
63
|
|
|
@@ -89,13 +103,86 @@ function resolveBinaryDir({ rootDir, presetName }) {
|
|
|
89
103
|
* @param {{ presetName: string, generator?: string|null, vsPlatform?: string|null }} args
|
|
90
104
|
* @returns {string[]}
|
|
91
105
|
*/
|
|
92
|
-
function buildConfigureArgs({
|
|
106
|
+
function buildConfigureArgs({
|
|
107
|
+
presetName,
|
|
108
|
+
generator = null,
|
|
109
|
+
vsPlatform = null,
|
|
110
|
+
confSets = null,
|
|
111
|
+
confUnsets = null,
|
|
112
|
+
}) {
|
|
93
113
|
const args = ['--preset', presetName];
|
|
94
114
|
if (generator) args.push('-G', generator);
|
|
95
115
|
if (vsPlatform) args.push('-A', vsPlatform);
|
|
116
|
+
|
|
117
|
+
// PLAN-CONF.md §7: CLI --conf at build time applies as -D / -U
|
|
118
|
+
// overrides on top of whatever's baked into the preset. We do NOT
|
|
119
|
+
// silently regenerate the preset — that's a separate install.
|
|
120
|
+
// Confined to the cmake.cache namespace; other namespaces have
|
|
121
|
+
// different sinks (phase 3) and don't apply to `cmake --preset`.
|
|
122
|
+
if (confSets) {
|
|
123
|
+
const prefix = 'cmake.cache.';
|
|
124
|
+
for (const key of Object.keys(confSets).sort()) {
|
|
125
|
+
if (!key.startsWith(prefix)) continue;
|
|
126
|
+
const cmakeKey = key.slice(prefix.length);
|
|
127
|
+
args.push('-D', `${cmakeKey}=${confSets[key]}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (confUnsets) {
|
|
131
|
+
const prefix = 'cmake.cache.';
|
|
132
|
+
for (const key of [...confUnsets].sort()) {
|
|
133
|
+
if (!key.startsWith(prefix)) continue;
|
|
134
|
+
args.push('-U', key.slice(prefix.length));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
96
138
|
return args;
|
|
97
139
|
}
|
|
98
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Resolve the list of CMake configs to build (and optionally install).
|
|
143
|
+
*
|
|
144
|
+
* Precedence:
|
|
145
|
+
* 1. `--all-configs` → recipe.configs (falls back to all four canonical
|
|
146
|
+
* configs when no recipe declares any).
|
|
147
|
+
* 2. `--config` → comma-separated list, validated against
|
|
148
|
+
* VALID_CMAKE_CONFIGS. Order preserved. Duplicates removed.
|
|
149
|
+
* 3. default → `['Release']`.
|
|
150
|
+
*
|
|
151
|
+
* Configure runs once per `wyvrnpm build` invocation regardless — multi-
|
|
152
|
+
* config generators (VS, Xcode, Ninja Multi-Config) produce all configs
|
|
153
|
+
* from one configure step. Single-config generators (plain Ninja, Make)
|
|
154
|
+
* bake CMAKE_BUILD_TYPE into the cache, so `cmake --build --config X`
|
|
155
|
+
* effectively ignores X for them; building two configs against a single-
|
|
156
|
+
* config generator rebuilds the same binary twice. That's wasteful but
|
|
157
|
+
* correct; fixing it properly requires per-config build dirs, which is
|
|
158
|
+
* out of scope for this helper.
|
|
159
|
+
*
|
|
160
|
+
* @param {{ argv: object, recipeConfigs?: string[] }} args
|
|
161
|
+
* @returns {string[]}
|
|
162
|
+
*/
|
|
163
|
+
function resolveConfigList({ argv, recipeConfigs = null }) {
|
|
164
|
+
if (argv.allConfigs) {
|
|
165
|
+
const fromRecipe = (recipeConfigs ?? []).filter((c) => VALID_CMAKE_CONFIGS.has(c));
|
|
166
|
+
return fromRecipe.length > 0 ? [...fromRecipe] : [...DEFAULT_RECIPE.configs];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const raw = typeof argv.config === 'string' && argv.config.trim() ? argv.config : 'Release';
|
|
170
|
+
const list = raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
171
|
+
|
|
172
|
+
const invalid = list.filter((c) => !VALID_CMAKE_CONFIGS.has(c));
|
|
173
|
+
if (invalid.length > 0) {
|
|
174
|
+
const valid = [...VALID_CMAKE_CONFIGS].join(' | ');
|
|
175
|
+
throw new Error(`invalid --config "${invalid.join(', ')}" — expected ${valid}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
const deduped = [];
|
|
180
|
+
for (const c of list) {
|
|
181
|
+
if (!seen.has(c)) { seen.add(c); deduped.push(c); }
|
|
182
|
+
}
|
|
183
|
+
return deduped.length > 0 ? deduped : ['Release'];
|
|
184
|
+
}
|
|
185
|
+
|
|
99
186
|
/**
|
|
100
187
|
* Args passed to `cmake` for the build step.
|
|
101
188
|
*
|
|
@@ -180,13 +267,34 @@ function runCmake(args, env = null) {
|
|
|
180
267
|
* @param {object} argv
|
|
181
268
|
*/
|
|
182
269
|
async function build(argv) {
|
|
183
|
-
|
|
184
|
-
|
|
270
|
+
// Shared per-invocation state. See src/context.js +
|
|
271
|
+
// claude/PLAN-COMMAND-CONTEXT.md. Parses --conf up front so a malformed
|
|
272
|
+
// flag fails here instead of after cmake's already configured.
|
|
273
|
+
let ctx;
|
|
274
|
+
try {
|
|
275
|
+
ctx = buildContext(argv);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
log.error(err.message);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
185
280
|
|
|
186
|
-
const
|
|
187
|
-
const profileName
|
|
281
|
+
const { rootDir, manifestPath } = ctx;
|
|
282
|
+
const { name: profileName, profile: activeProfile } = ctx.profiles.host;
|
|
188
283
|
const presetName = resolvePresetName({ presetOverride: argv.preset, profileName });
|
|
189
|
-
|
|
284
|
+
|
|
285
|
+
// Resolve the list of configs up front so a malformed --config fails
|
|
286
|
+
// before auto-install kicks off. `--all-configs` consults the recipe
|
|
287
|
+
// (if any); otherwise comma-separated --config, defaulting to Release.
|
|
288
|
+
let configList;
|
|
289
|
+
try {
|
|
290
|
+
const recipe = hasBuildRecipe(ctx.manifest)
|
|
291
|
+
? (() => { try { return normalizeRecipe(ctx.manifest.build, null); } catch { return null; } })()
|
|
292
|
+
: null;
|
|
293
|
+
configList = resolveConfigList({ argv, recipeConfigs: recipe?.configs ?? null });
|
|
294
|
+
} catch (err) {
|
|
295
|
+
log.error(err.message);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
190
298
|
|
|
191
299
|
// ── Stale check / auto-install ────────────────────────────────────────────
|
|
192
300
|
// `--auto-install` gates the "run `wyvrnpm install` first when the
|
|
@@ -233,13 +341,12 @@ async function build(argv) {
|
|
|
233
341
|
let effectiveGenerator = argv.generator ?? null;
|
|
234
342
|
let effectiveProfile = null;
|
|
235
343
|
try {
|
|
236
|
-
const { profile: activeProfile } = loadProfile(profileName);
|
|
237
344
|
effectiveProfile = activeProfile;
|
|
238
345
|
// Which generator will the preset actually use? Read it from the
|
|
239
346
|
// project's own recipe if one exists. If not, default to null and let
|
|
240
347
|
// cmake/CMakePresets decide — in which case MSVC env activation is
|
|
241
348
|
// skipped (because VS is usually the default on Windows).
|
|
242
|
-
const manifest =
|
|
349
|
+
const manifest = ctx.manifest;
|
|
243
350
|
let presetGenerator = null;
|
|
244
351
|
if (hasBuildRecipe(manifest)) {
|
|
245
352
|
try {
|
|
@@ -273,43 +380,61 @@ async function build(argv) {
|
|
|
273
380
|
}
|
|
274
381
|
}
|
|
275
382
|
|
|
383
|
+
// CLI --conf → -D / -U passthrough (PLAN-CONF.md §7). The preset's
|
|
384
|
+
// baked-in cacheVariables (from the last install) already reflect the
|
|
385
|
+
// recipe + local-overlay layers; CLI conf layers on top for this
|
|
386
|
+
// invocation only. ctx.cliConf is pre-parsed by buildContext; malformed
|
|
387
|
+
// values already threw above.
|
|
388
|
+
const cliConfSets = ctx.cliConf.sets;
|
|
389
|
+
const cliConfUnsets = ctx.cliConf.unsets;
|
|
390
|
+
|
|
276
391
|
const configureArgs = buildConfigureArgs({
|
|
277
392
|
presetName,
|
|
278
393
|
generator: argv.generator,
|
|
279
394
|
vsPlatform,
|
|
395
|
+
confSets: cliConfSets,
|
|
396
|
+
confUnsets: cliConfUnsets,
|
|
280
397
|
});
|
|
281
398
|
log.info(`cmake ${configureArgs.join(' ')}`);
|
|
282
399
|
await runCmake(configureArgs, cmakeEnv);
|
|
283
400
|
|
|
284
|
-
// ── Build
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// defaults to whatever was baked into the cache at configure time; pass
|
|
298
|
-
// `--install-prefix` to override without reconfiguring.
|
|
299
|
-
if (argv.install) {
|
|
300
|
-
const installArgs = buildInstallArgs({
|
|
401
|
+
// ── Build (+ optional install) per config ─────────────────────────────────
|
|
402
|
+
// Configure ran once above. Multi-config generators (VS, Xcode, Ninja
|
|
403
|
+
// Multi-Config) produce every config from that one configure step; for
|
|
404
|
+
// single-config generators each iteration simply re-`cmake --build`s the
|
|
405
|
+
// same baked CMAKE_BUILD_TYPE (harmless duplication). `--install` runs
|
|
406
|
+
// per config too — the usual reason to want multi-config is so that a
|
|
407
|
+
// downstream `find_package()` can pick whichever it needs.
|
|
408
|
+
if (configList.length > 1) {
|
|
409
|
+
log.info(`Building ${configList.length} configs: ${configList.join(', ')}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (const config of configList) {
|
|
413
|
+
const buildArgs = buildBuildArgs({
|
|
301
414
|
binaryDir,
|
|
302
|
-
config
|
|
303
|
-
|
|
415
|
+
config,
|
|
416
|
+
target: argv.target,
|
|
417
|
+
verbose: argv.verbose,
|
|
418
|
+
passthru: argv['--'] ?? [],
|
|
304
419
|
});
|
|
305
|
-
log.info(`cmake ${
|
|
306
|
-
await runCmake(
|
|
420
|
+
log.info(`cmake ${buildArgs.join(' ')}`);
|
|
421
|
+
await runCmake(buildArgs, cmakeEnv);
|
|
422
|
+
|
|
423
|
+
if (argv.install) {
|
|
424
|
+
const installArgs = buildInstallArgs({
|
|
425
|
+
binaryDir,
|
|
426
|
+
config,
|
|
427
|
+
prefix: argv.installPrefix,
|
|
428
|
+
});
|
|
429
|
+
log.info(`cmake ${installArgs.join(' ')}`);
|
|
430
|
+
await runCmake(installArgs, cmakeEnv);
|
|
431
|
+
}
|
|
307
432
|
}
|
|
308
433
|
|
|
309
434
|
const suffix = argv.install
|
|
310
435
|
? ` + installed${argv.installPrefix ? ` to ${argv.installPrefix}` : ''}`
|
|
311
436
|
: '';
|
|
312
|
-
log.success(`Build complete: ${binaryDir} (${
|
|
437
|
+
log.success(`Build complete: ${binaryDir} (${configList.join(', ')})${suffix}`);
|
|
313
438
|
}
|
|
314
439
|
|
|
315
440
|
module.exports = build;
|
|
@@ -320,6 +445,7 @@ module.exports._helpers = {
|
|
|
320
445
|
isToolchainStale,
|
|
321
446
|
resolvePresetName,
|
|
322
447
|
resolveBinaryDir,
|
|
448
|
+
resolveConfigList,
|
|
323
449
|
buildConfigureArgs,
|
|
324
450
|
buildBuildArgs,
|
|
325
451
|
buildInstallArgs,
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// F12: source-build cache inspection + targeted pruning.
|
|
4
|
+
//
|
|
5
|
+
// `wyvrnpm cache list [pattern]` prints a human-readable table (or JSON
|
|
6
|
+
// with --format json) of everything in the machine-wide source-build
|
|
7
|
+
// cache at `%LOCALAPPDATA%\wyvrnpm\bc\`.
|
|
8
|
+
//
|
|
9
|
+
// `wyvrnpm cache prune` removes cache entries per a policy:
|
|
10
|
+
// --keep-last N retain N most-recently-touched per (name,version)
|
|
11
|
+
// --older-than 30d also remove entries older than the threshold
|
|
12
|
+
// --pattern <glob> restrict to matching package names
|
|
13
|
+
// --dry-run print what would go, remove nothing
|
|
14
|
+
//
|
|
15
|
+
// The existing `wyvrnpm clean --build-cache` nukes everything; this is
|
|
16
|
+
// the targeted alternative for teams whose `--upload-built` cache has
|
|
17
|
+
// ballooned.
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
listCacheEntries,
|
|
21
|
+
computePruneSet,
|
|
22
|
+
removeCacheEntries,
|
|
23
|
+
parseDurationToCutoff,
|
|
24
|
+
getBuildCacheRoot,
|
|
25
|
+
} = require('../build/cache');
|
|
26
|
+
const log = require('../logger');
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
function formatBytes(n) {
|
|
33
|
+
if (n < 1024) return `${n} B`;
|
|
34
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
35
|
+
let size = n / 1024, i = 0;
|
|
36
|
+
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; }
|
|
37
|
+
return `${size.toFixed(size >= 100 ? 0 : 1)} ${units[i]}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatMtime(ms) {
|
|
41
|
+
if (!ms) return '<unknown>';
|
|
42
|
+
return new Date(ms).toISOString().replace('T', ' ').slice(0, 16);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function globToRegExp(glob) {
|
|
46
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
47
|
+
return new RegExp(`^${escaped.replace(/\*/g, '.*').replace(/\?/g, '.')}$`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function patternFor(argPattern, positional) {
|
|
51
|
+
const src = argPattern ?? positional ?? null;
|
|
52
|
+
return src ? globToRegExp(src) : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// list
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* wyvrnpm cache list [pattern] [--format json|text]
|
|
61
|
+
*
|
|
62
|
+
* @param {object} argv
|
|
63
|
+
*/
|
|
64
|
+
function list(argv) {
|
|
65
|
+
const entries = listCacheEntries();
|
|
66
|
+
const regex = patternFor(argv.pattern, argv._?.[1]);
|
|
67
|
+
const filtered = regex ? entries.filter((e) => regex.test(e.name)) : entries;
|
|
68
|
+
|
|
69
|
+
// Newest first — matches what `ls -lt` users expect.
|
|
70
|
+
filtered.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
71
|
+
|
|
72
|
+
if (argv.format === 'json') {
|
|
73
|
+
process.stdout.write(JSON.stringify({
|
|
74
|
+
command: 'cache-list',
|
|
75
|
+
cacheRoot: getBuildCacheRoot(),
|
|
76
|
+
totalEntries: filtered.length,
|
|
77
|
+
totalBytes: filtered.reduce((s, e) => s + e.sizeBytes, 0),
|
|
78
|
+
entries: filtered.map((e) => ({
|
|
79
|
+
name: e.name,
|
|
80
|
+
version: e.version,
|
|
81
|
+
profileHash: e.profileHash,
|
|
82
|
+
sizeBytes: e.sizeBytes,
|
|
83
|
+
mtime: new Date(e.mtimeMs).toISOString(),
|
|
84
|
+
hasArtefact: e.hasArtefact,
|
|
85
|
+
uploadCount: e.uploads.length,
|
|
86
|
+
})),
|
|
87
|
+
}, null, 2) + '\n');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
log.info(`Cache root : ${getBuildCacheRoot()}`);
|
|
92
|
+
if (filtered.length === 0) {
|
|
93
|
+
log.info(' (empty)');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const rows = filtered.map((e) => [
|
|
98
|
+
e.name,
|
|
99
|
+
e.version,
|
|
100
|
+
e.profileHash,
|
|
101
|
+
formatBytes(e.sizeBytes),
|
|
102
|
+
formatMtime(e.mtimeMs),
|
|
103
|
+
e.uploads.length > 0 ? `${e.uploads.length}` : '-',
|
|
104
|
+
]);
|
|
105
|
+
const header = ['NAME', 'VERSION', 'PROFILE HASH', 'SIZE', 'MTIME', 'UPLOADS'];
|
|
106
|
+
const widths = header.map((h, i) =>
|
|
107
|
+
Math.max(h.length, ...rows.map((r) => String(r[i]).length)),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const format = (cells) => cells.map((c, i) => String(c).padEnd(widths[i])).join(' ');
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(' ' + format(header));
|
|
113
|
+
console.log(' ' + widths.map((w) => '-'.repeat(w)).join(' '));
|
|
114
|
+
for (const r of rows) console.log(' ' + format(r));
|
|
115
|
+
console.log('');
|
|
116
|
+
|
|
117
|
+
const totalBytes = filtered.reduce((s, e) => s + e.sizeBytes, 0);
|
|
118
|
+
log.info(`${filtered.length} entries, ${formatBytes(totalBytes)} total`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// prune
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* wyvrnpm cache prune [--keep-last N] [--older-than 30d] [--pattern X] [--dry-run]
|
|
127
|
+
*
|
|
128
|
+
* Requires at least one of --keep-last / --older-than to avoid footgun
|
|
129
|
+
* (use `wyvrnpm clean --build-cache` to wipe everything).
|
|
130
|
+
*
|
|
131
|
+
* @param {object} argv
|
|
132
|
+
*/
|
|
133
|
+
function prune(argv) {
|
|
134
|
+
const hasKeep = argv.keepLast !== undefined && argv.keepLast !== null;
|
|
135
|
+
const hasOlder = typeof argv.olderThan === 'string' && argv.olderThan.length > 0;
|
|
136
|
+
|
|
137
|
+
if (!hasKeep && !hasOlder) {
|
|
138
|
+
log.error(
|
|
139
|
+
'cache prune: specify --keep-last <N> and/or --older-than <duration>.\n' +
|
|
140
|
+
' To wipe the entire build cache, use `wyvrnpm clean --build-cache`.',
|
|
141
|
+
);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const policy = {};
|
|
146
|
+
if (hasKeep) {
|
|
147
|
+
const n = Number(argv.keepLast);
|
|
148
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
149
|
+
log.error(`--keep-last must be a non-negative integer, got ${argv.keepLast}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
policy.keepLast = n;
|
|
153
|
+
}
|
|
154
|
+
if (hasOlder) {
|
|
155
|
+
try { policy.olderThanMs = parseDurationToCutoff(argv.olderThan); }
|
|
156
|
+
catch (err) { log.error(err.message); process.exit(1); }
|
|
157
|
+
}
|
|
158
|
+
if (argv.pattern) {
|
|
159
|
+
policy.patternRegex = globToRegExp(argv.pattern);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const entries = listCacheEntries();
|
|
163
|
+
const pruneSet = computePruneSet(entries, policy);
|
|
164
|
+
|
|
165
|
+
if (pruneSet.length === 0) {
|
|
166
|
+
log.info('Nothing to prune under that policy.');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const totalBytes = pruneSet.reduce((s, e) => s + e.sizeBytes, 0);
|
|
171
|
+
const verb = argv.dryRun ? 'Would remove' : 'Removing';
|
|
172
|
+
log.info(`${verb} ${pruneSet.length} cache entr${pruneSet.length === 1 ? 'y' : 'ies'}, ${formatBytes(totalBytes)}:`);
|
|
173
|
+
for (const e of pruneSet) {
|
|
174
|
+
console.log(` - ${e.name}@${e.version} [${e.profileHash}] ${formatBytes(e.sizeBytes)} ${formatMtime(e.mtimeMs)}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (argv.dryRun) {
|
|
178
|
+
log.info('(dry-run — nothing deleted)');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const removed = removeCacheEntries(pruneSet);
|
|
183
|
+
log.info(`Removed ${removed.length} of ${pruneSet.length} cache entr${pruneSet.length === 1 ? 'y' : 'ies'}`);
|
|
184
|
+
if (removed.length < pruneSet.length) {
|
|
185
|
+
log.warn(`${pruneSet.length - removed.length} entr${pruneSet.length - removed.length === 1 ? 'y' : 'ies'} could not be removed (permissions or concurrent access)`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { list, prune };
|
|
@@ -27,8 +27,9 @@ function printSection(title, sources) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function authLabel(src) {
|
|
30
|
-
if (src.profile)
|
|
31
|
-
if (src.
|
|
30
|
+
if (src.profile) return `[profile: ${src.profile}]`;
|
|
31
|
+
if (src.tokenEnv) return `[token-env: ${src.tokenEnv}]`;
|
|
32
|
+
if (src.token) return '[token: ***]';
|
|
32
33
|
return '';
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -38,12 +39,22 @@ function authLabel(src) {
|
|
|
38
39
|
*/
|
|
39
40
|
function addSource(argv) {
|
|
40
41
|
const { kind, name, url, profile, token } = argv;
|
|
42
|
+
// yargs delivers --token-env as argv['token-env']; accept both shapes
|
|
43
|
+
// so programmatic callers with camelCase also work.
|
|
44
|
+
const tokenEnv = argv.tokenEnv ?? argv['token-env'];
|
|
45
|
+
|
|
46
|
+
if (token && tokenEnv) {
|
|
47
|
+
log.error('Pass --token OR --token-env, not both. --token-env is preferred for CI and shared setups (EVALUATION.md S8).');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
const config = readConfig();
|
|
42
52
|
const key = kind === 'install' ? 'installSources' : 'publishSources';
|
|
43
53
|
|
|
44
54
|
const entry = { name, url };
|
|
45
|
-
if (profile)
|
|
46
|
-
if (token)
|
|
55
|
+
if (profile) entry.profile = profile;
|
|
56
|
+
if (token) entry.token = token;
|
|
57
|
+
if (tokenEnv) entry.tokenEnv = tokenEnv;
|
|
47
58
|
|
|
48
59
|
const idx = config[key].findIndex((s) => s.name === name);
|
|
49
60
|
if (idx !== -1) {
|