wyvrnpm 2.4.1 → 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 +398 -15
- package/bin/wyvrn.js +139 -9
- 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 +23 -2
- package/src/build/msvc-env.js +50 -7
- package/src/build/recipe.js +36 -3
- package/src/commands/build.js +64 -3
- 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 +270 -5
- package/src/commands/profile.js +70 -7
- package/src/commands/publish.js +41 -4
- package/src/commands/show.js +14 -4
- package/src/conf/index.js +391 -0
- package/src/conf/namespaces.js +94 -0
- package/src/download.js +135 -147
- 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/presets.js +71 -31
- package/src/upload-built.js +11 -2
- package/src/zip-safe.js +52 -0
package/bin/wyvrn.js
CHANGED
|
@@ -13,6 +13,7 @@ const profile = require('../src/commands/profile');
|
|
|
13
13
|
const build = require('../src/commands/build');
|
|
14
14
|
const show = require('../src/commands/show');
|
|
15
15
|
const installSkill = require('../src/commands/install-skill');
|
|
16
|
+
const cache = require('../src/commands/cache');
|
|
16
17
|
const { link, unlink } = require('../src/commands/link');
|
|
17
18
|
|
|
18
19
|
yargs
|
|
@@ -86,8 +87,8 @@ yargs
|
|
|
86
87
|
choices: ['never', 'missing', 'always'],
|
|
87
88
|
default: 'never',
|
|
88
89
|
description:
|
|
89
|
-
'Resolution
|
|
90
|
-
'
|
|
90
|
+
'Resolution strategy. never=fail when no exact match; ' +
|
|
91
|
+
'missing=source-build only when no match; always=ignore registry binaries and rebuild every dep from source (debug/verification).',
|
|
91
92
|
})
|
|
92
93
|
.option('option', {
|
|
93
94
|
alias: 'o',
|
|
@@ -97,6 +98,25 @@ yargs
|
|
|
97
98
|
'Override a package option. Repeatable. Format: <pkg>:<name>=<value>. ' +
|
|
98
99
|
'Example: -o zlib:minizip=false -o OpenSSL:fips=true',
|
|
99
100
|
})
|
|
101
|
+
.option('dry-run', {
|
|
102
|
+
type: 'boolean',
|
|
103
|
+
default: false,
|
|
104
|
+
description:
|
|
105
|
+
'Run the resolver + profile-hash math and print the install plan (every ' +
|
|
106
|
+
'dep, its pinned version, effective profileHash, per-dep option overrides) ' +
|
|
107
|
+
'without downloading, extracting, writing wyvrn.lock, or emitting the ' +
|
|
108
|
+
'toolchain. Combine with --format=json for CI-friendly output.',
|
|
109
|
+
})
|
|
110
|
+
.option('conf', {
|
|
111
|
+
type: 'string',
|
|
112
|
+
array: true,
|
|
113
|
+
description:
|
|
114
|
+
'Non-ABI build-time override. Repeatable. Format: ' +
|
|
115
|
+
'<namespace.leaf>=<value> — e.g. --conf cmake.cache.CHROMA_ENABLE_TEST=ON. ' +
|
|
116
|
+
'Bare KEY is sugar for KEY=ON (CMake -D convention); KEY= (empty RHS) ' +
|
|
117
|
+
'clears an inherited value. Does NOT fold into profileHash — see ' +
|
|
118
|
+
'claude/PLAN-CONF.md for layering rules.',
|
|
119
|
+
})
|
|
100
120
|
.option('upload-built', {
|
|
101
121
|
type: 'boolean',
|
|
102
122
|
default: false,
|
|
@@ -117,7 +137,11 @@ yargs
|
|
|
117
137
|
})
|
|
118
138
|
.option('token', {
|
|
119
139
|
type: 'string',
|
|
120
|
-
description: 'Bearer token for --upload-built when uploading to HTTP',
|
|
140
|
+
description: 'Bearer token for --upload-built when uploading to HTTP. Prefer --token-env in CI so the literal value never lands in argv.',
|
|
141
|
+
})
|
|
142
|
+
.option('token-env', {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'Name of an env var whose value is the bearer token for --upload-built. Preferred over --token in CI.',
|
|
121
145
|
})
|
|
122
146
|
.option('platform', {
|
|
123
147
|
type: 'string',
|
|
@@ -142,6 +166,8 @@ yargs
|
|
|
142
166
|
uploadBuilt: argv['upload-built'],
|
|
143
167
|
uploadSource: argv['upload-source'],
|
|
144
168
|
awsProfile: argv['aws-profile'],
|
|
169
|
+
tokenEnv: argv['token-env'],
|
|
170
|
+
dryRun: argv['dry-run'],
|
|
145
171
|
}),
|
|
146
172
|
)
|
|
147
173
|
|
|
@@ -220,6 +246,16 @@ yargs
|
|
|
220
246
|
.option('timeout', {
|
|
221
247
|
type: 'number',
|
|
222
248
|
default: 300,
|
|
249
|
+
})
|
|
250
|
+
.option('conf', {
|
|
251
|
+
type: 'string',
|
|
252
|
+
array: true,
|
|
253
|
+
description:
|
|
254
|
+
'Non-ABI build-time override forwarded to cmake --preset as -D / -U. ' +
|
|
255
|
+
'Repeatable. Format: <namespace.leaf>=<value> — e.g. ' +
|
|
256
|
+
'--conf cmake.cache.CHROMA_ENABLE_TEST=ON. CLI --conf does NOT ' +
|
|
257
|
+
'regenerate CMakePresets.json; edit wyvrn.local.json and re-run ' +
|
|
258
|
+
'`wyvrnpm install` for persistent overrides. See claude/PLAN-CONF.md.',
|
|
223
259
|
});
|
|
224
260
|
},
|
|
225
261
|
(argv) => build({
|
|
@@ -229,6 +265,74 @@ yargs
|
|
|
229
265
|
}),
|
|
230
266
|
)
|
|
231
267
|
|
|
268
|
+
// ── cache ─────────────────────────────────────────────────────────────────
|
|
269
|
+
.command(
|
|
270
|
+
'cache',
|
|
271
|
+
'Inspect and selectively prune the machine-wide source-build cache (F12). Use `clean --build-cache` to wipe everything.',
|
|
272
|
+
(y) => {
|
|
273
|
+
y
|
|
274
|
+
.command(
|
|
275
|
+
'list [pattern]',
|
|
276
|
+
'List cache entries. Optional glob pattern filters by package name.',
|
|
277
|
+
(y2) => {
|
|
278
|
+
y2
|
|
279
|
+
.positional('pattern', {
|
|
280
|
+
type: 'string',
|
|
281
|
+
description: 'Glob over package names, e.g. "boost*" (alias for --pattern)',
|
|
282
|
+
})
|
|
283
|
+
.option('pattern', {
|
|
284
|
+
type: 'string',
|
|
285
|
+
description: 'Glob over package names; overrides the positional',
|
|
286
|
+
})
|
|
287
|
+
.option('format', {
|
|
288
|
+
type: 'string',
|
|
289
|
+
choices: ['text', 'json'],
|
|
290
|
+
default: 'text',
|
|
291
|
+
description: 'Output format',
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
(argv) => cache.list(argv),
|
|
295
|
+
)
|
|
296
|
+
.command(
|
|
297
|
+
'prune',
|
|
298
|
+
'Remove cache entries per policy. Requires at least one of --keep-last / --older-than.',
|
|
299
|
+
(y2) => {
|
|
300
|
+
y2
|
|
301
|
+
.option('keep-last', {
|
|
302
|
+
type: 'number',
|
|
303
|
+
description:
|
|
304
|
+
'Retain the N most recently-touched entries per (name, version). ' +
|
|
305
|
+
'Older variants beyond N are pruned.',
|
|
306
|
+
})
|
|
307
|
+
.option('older-than', {
|
|
308
|
+
type: 'string',
|
|
309
|
+
description:
|
|
310
|
+
'Remove entries whose mtime is older than this threshold. ' +
|
|
311
|
+
'Format: <number><unit>, unit ∈ s/m/h/d (e.g. 30d, 72h).',
|
|
312
|
+
})
|
|
313
|
+
.option('pattern', {
|
|
314
|
+
type: 'string',
|
|
315
|
+
description: 'Limit scope to package names matching this glob (e.g. "boost*")',
|
|
316
|
+
})
|
|
317
|
+
.option('dry-run', {
|
|
318
|
+
type: 'boolean',
|
|
319
|
+
default: false,
|
|
320
|
+
description: 'Print what would be removed without actually deleting',
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
(argv) => cache.prune({
|
|
324
|
+
...argv,
|
|
325
|
+
keepLast: argv['keep-last'],
|
|
326
|
+
olderThan: argv['older-than'],
|
|
327
|
+
dryRun: argv['dry-run'],
|
|
328
|
+
}),
|
|
329
|
+
)
|
|
330
|
+
.demandCommand(1)
|
|
331
|
+
.help();
|
|
332
|
+
},
|
|
333
|
+
() => {},
|
|
334
|
+
)
|
|
335
|
+
|
|
232
336
|
// ── clean ─────────────────────────────────────────────────────────────────
|
|
233
337
|
.command(
|
|
234
338
|
'clean',
|
|
@@ -278,7 +382,11 @@ yargs
|
|
|
278
382
|
})
|
|
279
383
|
.option('token', {
|
|
280
384
|
type: 'string',
|
|
281
|
-
description: 'Bearer token for HTTP sources',
|
|
385
|
+
description: 'Bearer token for HTTP sources. Prefer --token-env in CI so the literal value never lands in argv.',
|
|
386
|
+
})
|
|
387
|
+
.option('token-env', {
|
|
388
|
+
type: 'string',
|
|
389
|
+
description: 'Name of an env var whose value is the bearer token for HTTP sources.',
|
|
282
390
|
})
|
|
283
391
|
.option('format', {
|
|
284
392
|
type: 'string',
|
|
@@ -287,7 +395,7 @@ yargs
|
|
|
287
395
|
description: 'Output format. "json" emits a single JSON object on stdout; log lines go to stderr.',
|
|
288
396
|
});
|
|
289
397
|
},
|
|
290
|
-
(argv) => show({ ...argv, awsProfile: argv['aws-profile'] }),
|
|
398
|
+
(argv) => show({ ...argv, awsProfile: argv['aws-profile'], tokenEnv: argv['token-env'] }),
|
|
291
399
|
)
|
|
292
400
|
|
|
293
401
|
// ── publish ───────────────────────────────────────────────────────────────
|
|
@@ -318,7 +426,11 @@ yargs
|
|
|
318
426
|
})
|
|
319
427
|
.option('token', {
|
|
320
428
|
type: 'string',
|
|
321
|
-
description: 'Bearer token for HTTP server authentication',
|
|
429
|
+
description: 'Bearer token for HTTP server authentication. Prefer --token-env in CI so the literal value never lands in argv.',
|
|
430
|
+
})
|
|
431
|
+
.option('token-env', {
|
|
432
|
+
type: 'string',
|
|
433
|
+
description: 'Name of an env var whose value is the bearer token for HTTP authentication.',
|
|
322
434
|
})
|
|
323
435
|
.option('force', {
|
|
324
436
|
alias: 'f',
|
|
@@ -334,6 +446,15 @@ yargs
|
|
|
334
446
|
'Override an option value at publish time. Repeatable. ' +
|
|
335
447
|
'Format: <pkg>:<name>=<value> (pkg must match the package being published).',
|
|
336
448
|
})
|
|
449
|
+
.option('conf', {
|
|
450
|
+
type: 'string',
|
|
451
|
+
array: true,
|
|
452
|
+
description:
|
|
453
|
+
'Non-ABI build-time override recorded as `publishedConf` on the ' +
|
|
454
|
+
'uploaded manifest (informational — does not affect profileHash). ' +
|
|
455
|
+
'Reads recipe conf + CLI only; wyvrn.local.json is intentionally ' +
|
|
456
|
+
'ignored for publish.',
|
|
457
|
+
})
|
|
337
458
|
.option('format', {
|
|
338
459
|
type: 'string',
|
|
339
460
|
choices: ['text', 'json'],
|
|
@@ -342,7 +463,7 @@ yargs
|
|
|
342
463
|
})
|
|
343
464
|
;
|
|
344
465
|
},
|
|
345
|
-
(argv) => publish({ ...argv, awsProfile: argv['aws-profile'] }),
|
|
466
|
+
(argv) => publish({ ...argv, awsProfile: argv['aws-profile'], tokenEnv: argv['token-env'] }),
|
|
346
467
|
)
|
|
347
468
|
|
|
348
469
|
// ── configure ─────────────────────────────────────────────────────────────
|
|
@@ -364,7 +485,8 @@ yargs
|
|
|
364
485
|
.option('name', { type: 'string', demandOption: true })
|
|
365
486
|
.option('url', { type: 'string', demandOption: true })
|
|
366
487
|
.option('profile', { type: 'string', description: 'AWS SSO profile (S3 only)' })
|
|
367
|
-
.option('token', { type: 'string', description: 'Bearer token (HTTP only)' })
|
|
488
|
+
.option('token', { type: 'string', description: 'Bearer token (HTTP only). Persists in plaintext in config.json — prefer --token-env for CI/shared setups.' })
|
|
489
|
+
.option('token-env', { type: 'string', description: 'Name of an env var whose value is the bearer token (HTTP only). Only the name is persisted; the value is resolved at runtime.' });
|
|
368
490
|
},
|
|
369
491
|
(argv) => configure.addSource(argv),
|
|
370
492
|
)
|
|
@@ -438,7 +560,15 @@ yargs
|
|
|
438
560
|
.option('compiler', { type: 'string', description: 'Compiler (msvc | gcc | clang | apple-clang)' })
|
|
439
561
|
.option('compiler-version', { type: 'string', description: 'Compiler version (193 | 11 | 14 | ...)' })
|
|
440
562
|
.option('cppstd', { type: 'string', description: 'C++ standard (14 | 17 | 20 | 23)' })
|
|
441
|
-
.option('runtime', { type: 'string', description: 'Runtime linkage (static | dynamic)' })
|
|
563
|
+
.option('runtime', { type: 'string', description: 'Runtime linkage (static | dynamic)' })
|
|
564
|
+
.option('conf', {
|
|
565
|
+
type: 'string',
|
|
566
|
+
array: true,
|
|
567
|
+
description:
|
|
568
|
+
'Profile-level non-ABI build knob — <namespace.leaf>=<value> ' +
|
|
569
|
+
'(repeatable). `KEY` alone means `KEY=ON`; `KEY=` removes the ' +
|
|
570
|
+
'leaf. Phase 1 allow-list: cmake.cache.*. See PLAN-CONF.md §4.',
|
|
571
|
+
});
|
|
442
572
|
},
|
|
443
573
|
(argv) => profile.set({ ...argv, compilerVersion: argv['compiler-version'] }),
|
|
444
574
|
)
|
|
Binary file
|
package/cmake/variables.cmake
CHANGED
|
@@ -161,9 +161,19 @@ else()
|
|
|
161
161
|
message(STATUS "Unknown architecture")
|
|
162
162
|
endif()
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
# Output dirs are suffixed with WYVRN_ARCH_SUFFIX ("64" / "32" / "") so
|
|
165
|
+
# multi-arch builds from the same source tree don't clobber each other
|
|
166
|
+
# (e.g. building x64 then x86 used to silently overwrite the x64 DLLs
|
|
167
|
+
# in output/bin/<CONFIG>/). Single-arch consumers see no path change on
|
|
168
|
+
# x86 or unmapped arches — the suffix is empty there. Set defensively:
|
|
169
|
+
# if wyvrn_toolchain.cmake hasn't been included (standalone cmake runs),
|
|
170
|
+
# WYVRN_ARCH_SUFFIX is undefined and we fall back to the legacy layout.
|
|
171
|
+
if(NOT DEFINED WYVRN_ARCH_SUFFIX)
|
|
172
|
+
set(WYVRN_ARCH_SUFFIX "")
|
|
173
|
+
endif()
|
|
174
|
+
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/output/lib${WYVRN_ARCH_SUFFIX}/")
|
|
175
|
+
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/output/lib${WYVRN_ARCH_SUFFIX}/")
|
|
176
|
+
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/output/bin${WYVRN_ARCH_SUFFIX}")
|
|
167
177
|
|
|
168
178
|
add_compile_definitions(
|
|
169
179
|
$<$<CONFIG:Debug>:WYVRN_DEBUG>
|
package/package.json
CHANGED
package/src/auth.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve an authentication token for HTTP-source reads/writes.
|
|
5
|
+
*
|
|
6
|
+
* Closes EVALUATION.md S8 — passing `--token <value>` on the command line
|
|
7
|
+
* leaks the token into any shell history, process table, or CI job log
|
|
8
|
+
* that captures argv. `--token-env <VAR>` and the config equivalent
|
|
9
|
+
* `tokenEnv` let users keep the literal value in env only.
|
|
10
|
+
*
|
|
11
|
+
* Resolution order (first match wins):
|
|
12
|
+
* 1. `token` — literal string from CLI or config (explicit beats env).
|
|
13
|
+
* 2. `tokenEnv` — name of an env var; its value becomes the token.
|
|
14
|
+
*
|
|
15
|
+
* Returns `undefined` when nothing is configured (valid — not every source
|
|
16
|
+
* needs auth). Throws when `tokenEnv` names a var that is unset or empty.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ token?: string, tokenEnv?: string }} opts
|
|
19
|
+
* @returns {string|undefined}
|
|
20
|
+
*/
|
|
21
|
+
function resolveToken(opts = {}) {
|
|
22
|
+
const { token, tokenEnv } = opts;
|
|
23
|
+
|
|
24
|
+
if (typeof token === 'string' && token.length > 0) {
|
|
25
|
+
return token;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof tokenEnv === 'string' && tokenEnv.length > 0) {
|
|
29
|
+
const value = process.env[tokenEnv];
|
|
30
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`--token-env ${tokenEnv} (or config tokenEnv=${tokenEnv}) is set, ` +
|
|
33
|
+
`but the environment variable is unset or empty.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Merge CLI auth args + a source-config entry into a final `{ token }`
|
|
44
|
+
* object ready for passing to a provider. Centralises the "CLI wins,
|
|
45
|
+
* then env-backed CLI, then config literal, then config env-backed"
|
|
46
|
+
* precedence so call sites don't each re-implement it.
|
|
47
|
+
*
|
|
48
|
+
* @param {{ token?: string, tokenEnv?: string }} cli
|
|
49
|
+
* @param {{ token?: string, tokenEnv?: string } | null | undefined} source
|
|
50
|
+
* @returns {{ token: string|undefined }}
|
|
51
|
+
*/
|
|
52
|
+
function resolveSourceAuth(cli, source) {
|
|
53
|
+
// Try CLI first; if it produced nothing, fall back to the source's
|
|
54
|
+
// own stored auth. Each call to resolveToken handles its own
|
|
55
|
+
// precedence (literal > env) and throws on invalid tokenEnv.
|
|
56
|
+
const fromCli = resolveToken(cli);
|
|
57
|
+
if (fromCli !== undefined) return { token: fromCli };
|
|
58
|
+
|
|
59
|
+
const fromSource = source ? resolveToken(source) : undefined;
|
|
60
|
+
return { token: fromSource };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
resolveToken,
|
|
65
|
+
resolveSourceAuth,
|
|
66
|
+
};
|
package/src/build/cache.js
CHANGED
|
@@ -136,6 +136,196 @@ function clearUploadSidecars() {
|
|
|
136
136
|
return removed;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Cache inspection (F12)
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Dotted-version + 16+ hex-hash suffix. The hash tail is unambiguous
|
|
145
|
+
* (always hex, always 16 chars per src/profile.js) so parsing from the
|
|
146
|
+
* end avoids ambiguity when package names contain dashes
|
|
147
|
+
* (`boost-filesystem-1.80.0.0-abc1234567890def`).
|
|
148
|
+
*/
|
|
149
|
+
const CACHE_DIR_RE = /^(.+)-(\d+(?:\.\d+)+)-([a-f0-9]{16,})$/;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Recursively compute total size of a directory in bytes. Skips symlinks
|
|
153
|
+
* to avoid accidental cycles through user-linked packages.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} dir
|
|
156
|
+
* @returns {number}
|
|
157
|
+
*/
|
|
158
|
+
function dirSizeBytes(dir) {
|
|
159
|
+
let total = 0;
|
|
160
|
+
let entries;
|
|
161
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
162
|
+
catch { return 0; }
|
|
163
|
+
for (const e of entries) {
|
|
164
|
+
if (e.isSymbolicLink()) continue;
|
|
165
|
+
const p = path.join(dir, e.name);
|
|
166
|
+
try {
|
|
167
|
+
if (e.isDirectory()) {
|
|
168
|
+
total += dirSizeBytes(p);
|
|
169
|
+
} else if (e.isFile()) {
|
|
170
|
+
total += fs.statSync(p).size;
|
|
171
|
+
}
|
|
172
|
+
} catch { /* concurrent deletion — ignore */ }
|
|
173
|
+
}
|
|
174
|
+
return total;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* List every entry in the source-build cache, enriched with metadata.
|
|
179
|
+
* Unparseable directory names are skipped (corrupt / manual cruft).
|
|
180
|
+
*
|
|
181
|
+
* @returns {Array<{
|
|
182
|
+
* name: string,
|
|
183
|
+
* version: string,
|
|
184
|
+
* profileHash: string,
|
|
185
|
+
* cacheDir: string,
|
|
186
|
+
* sizeBytes: number,
|
|
187
|
+
* mtimeMs: number,
|
|
188
|
+
* hasArtefact: boolean,
|
|
189
|
+
* uploads: Array<object>,
|
|
190
|
+
* }>}
|
|
191
|
+
*/
|
|
192
|
+
function listCacheEntries() {
|
|
193
|
+
const root = getBuildCacheRoot();
|
|
194
|
+
if (!fs.existsSync(root)) return [];
|
|
195
|
+
|
|
196
|
+
const out = [];
|
|
197
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
198
|
+
if (!entry.isDirectory()) continue;
|
|
199
|
+
|
|
200
|
+
const m = entry.name.match(CACHE_DIR_RE);
|
|
201
|
+
if (!m) continue; // corrupt / unknown — skip silently
|
|
202
|
+
const [, name, version, profileHash] = m;
|
|
203
|
+
|
|
204
|
+
const cacheDir = path.join(root, entry.name);
|
|
205
|
+
const artefact = path.join(cacheDir, LAYOUT.artefactZip);
|
|
206
|
+
const sidecarPath = path.join(cacheDir, LAYOUT.uploadSidecar);
|
|
207
|
+
|
|
208
|
+
let mtimeMs = 0;
|
|
209
|
+
try { mtimeMs = fs.statSync(cacheDir).mtimeMs; } catch { /* skip */ }
|
|
210
|
+
|
|
211
|
+
let uploads = [];
|
|
212
|
+
if (fs.existsSync(sidecarPath)) {
|
|
213
|
+
try {
|
|
214
|
+
const raw = JSON.parse(fs.readFileSync(sidecarPath, 'utf8'));
|
|
215
|
+
if (Array.isArray(raw?.uploads)) uploads = raw.uploads;
|
|
216
|
+
} catch { /* corrupt sidecar — surface empty */ }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
out.push({
|
|
220
|
+
name, version, profileHash, cacheDir,
|
|
221
|
+
sizeBytes: dirSizeBytes(cacheDir),
|
|
222
|
+
mtimeMs,
|
|
223
|
+
hasArtefact: fs.existsSync(artefact),
|
|
224
|
+
uploads,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Parse `--older-than` duration strings (`7d`, `24h`, `30m`, `3600s`).
|
|
232
|
+
* Returns the cutoff timestamp in ms-since-epoch: entries with
|
|
233
|
+
* `mtimeMs < cutoff` are older than the threshold.
|
|
234
|
+
*
|
|
235
|
+
* @param {string} spec
|
|
236
|
+
* @param {number} [now=Date.now()]
|
|
237
|
+
* @returns {number}
|
|
238
|
+
*/
|
|
239
|
+
function parseDurationToCutoff(spec, now = Date.now()) {
|
|
240
|
+
if (typeof spec !== 'string') {
|
|
241
|
+
throw new Error(`--older-than expects a string like "30d", got ${JSON.stringify(spec)}`);
|
|
242
|
+
}
|
|
243
|
+
const m = spec.trim().match(/^(\d+)\s*([smhd])$/i);
|
|
244
|
+
if (!m) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`--older-than ${JSON.stringify(spec)} must be <number><unit> where unit is s/m/h/d`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
const n = parseInt(m[1], 10);
|
|
250
|
+
const unit = m[2].toLowerCase();
|
|
251
|
+
const ms = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[unit];
|
|
252
|
+
return now - n * ms;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Compute which cache entries would be pruned under the given policy.
|
|
257
|
+
* Pure — does no I/O of its own. Caller passes in `entries` (from
|
|
258
|
+
* `listCacheEntries()`) so tests can synthesise them.
|
|
259
|
+
*
|
|
260
|
+
* Policy semantics:
|
|
261
|
+
* - `keepLast` is **per (name, version) group** — preserves the N most
|
|
262
|
+
* recent profileHash variants for each version so multi-profile
|
|
263
|
+
* dev rigs don't lose every profile on a single prune.
|
|
264
|
+
* - `olderThanMs` applies after `keepLast`; it only widens the set
|
|
265
|
+
* of eligible-for-pruning entries; never adds entries back.
|
|
266
|
+
* - `patternRegex` filters entries by name at the very start.
|
|
267
|
+
*
|
|
268
|
+
* Returns the prune set in descending mtime order (newest-of-the-
|
|
269
|
+
* doomed first) — handy for human-friendly "about to remove" output.
|
|
270
|
+
*
|
|
271
|
+
* @param {ReturnType<typeof listCacheEntries>} entries
|
|
272
|
+
* @param {{ keepLast?: number, olderThanMs?: number, patternRegex?: RegExp }} policy
|
|
273
|
+
* @returns {ReturnType<typeof listCacheEntries>}
|
|
274
|
+
*/
|
|
275
|
+
function computePruneSet(entries, { keepLast, olderThanMs, patternRegex } = {}) {
|
|
276
|
+
let candidates = entries;
|
|
277
|
+
if (patternRegex) candidates = candidates.filter((e) => patternRegex.test(e.name));
|
|
278
|
+
|
|
279
|
+
// Group by (name, version); within each group, sort by mtime desc,
|
|
280
|
+
// mark the top N as keepers when keepLast is set.
|
|
281
|
+
const keep = new Set();
|
|
282
|
+
if (typeof keepLast === 'number' && keepLast >= 0) {
|
|
283
|
+
const groups = new Map();
|
|
284
|
+
for (const e of candidates) {
|
|
285
|
+
const k = `${e.name}\x1f${e.version}`;
|
|
286
|
+
if (!groups.has(k)) groups.set(k, []);
|
|
287
|
+
groups.get(k).push(e);
|
|
288
|
+
}
|
|
289
|
+
for (const group of groups.values()) {
|
|
290
|
+
group.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
291
|
+
for (let i = 0; i < Math.min(keepLast, group.length); i++) {
|
|
292
|
+
keep.add(group[i].cacheDir);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
// No keepLast → every candidate is eligible for pruning subject to
|
|
297
|
+
// the other filters.
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const toPrune = candidates.filter((e) => !keep.has(e.cacheDir));
|
|
301
|
+
|
|
302
|
+
const filtered = typeof olderThanMs === 'number'
|
|
303
|
+
? toPrune.filter((e) => e.mtimeMs < olderThanMs)
|
|
304
|
+
: toPrune;
|
|
305
|
+
|
|
306
|
+
filtered.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
307
|
+
return filtered;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Delete a list of cache entries. Returns the subset that was actually
|
|
312
|
+
* removed (permissions failures surface as misses). Caller should have
|
|
313
|
+
* already computed the set via `computePruneSet`.
|
|
314
|
+
*
|
|
315
|
+
* @param {ReturnType<typeof listCacheEntries>} entries
|
|
316
|
+
* @returns {ReturnType<typeof listCacheEntries>}
|
|
317
|
+
*/
|
|
318
|
+
function removeCacheEntries(entries) {
|
|
319
|
+
const removed = [];
|
|
320
|
+
for (const e of entries) {
|
|
321
|
+
try {
|
|
322
|
+
fs.rmSync(e.cacheDir, { recursive: true, force: true });
|
|
323
|
+
removed.push(e);
|
|
324
|
+
} catch { /* skip failures — caller reports the delta */ }
|
|
325
|
+
}
|
|
326
|
+
return removed;
|
|
327
|
+
}
|
|
328
|
+
|
|
139
329
|
module.exports = {
|
|
140
330
|
getBuildCacheRoot,
|
|
141
331
|
getPackageBuildDir,
|
|
@@ -145,4 +335,10 @@ module.exports = {
|
|
|
145
335
|
listUploadSidecars,
|
|
146
336
|
clearUploadSidecars,
|
|
147
337
|
LAYOUT,
|
|
338
|
+
// Cache inspection (F12)
|
|
339
|
+
listCacheEntries,
|
|
340
|
+
computePruneSet,
|
|
341
|
+
removeCacheEntries,
|
|
342
|
+
parseDurationToCutoff,
|
|
343
|
+
CACHE_DIR_RE,
|
|
148
344
|
};
|
package/src/build/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const { resolveDependencies } = require('../resolve');
|
|
|
14
14
|
const { hashProfile, mergeProfile, sha256Of } = require('../profile');
|
|
15
15
|
const { generateToolchain } = require('../toolchain');
|
|
16
16
|
const { normalizeOptionsDeclaration, resolveEffectiveOptions } = require('../options');
|
|
17
|
+
const { readRecipeConf, cmakeCacheVariables } = require('../conf');
|
|
17
18
|
const log = require('../logger');
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -155,8 +156,28 @@ async function buildFromSource(args) {
|
|
|
155
156
|
}
|
|
156
157
|
// Pass effective options into the recipe so `${options.<name>}` tokens in
|
|
157
158
|
// `build.configure` / `build.buildArgs` expand to the concrete values that
|
|
158
|
-
// produced this build's profileHash.
|
|
159
|
-
|
|
159
|
+
// produced this build's profileHash. Pass the cloned manifest so PLAN-CONF.md
|
|
160
|
+
// §4.7 collision detection catches a publisher who declared the same KEY in
|
|
161
|
+
// both build.configure and conf.cmake.cache.
|
|
162
|
+
const recipe = normalizeRecipe(clonedManifest.build, options, clonedManifest);
|
|
163
|
+
|
|
164
|
+
// PLAN-CONF.md §4.8.2: source-build of a dep consumes ONLY the dep's own
|
|
165
|
+
// recipe conf — never the root project's merged conf. Translating the
|
|
166
|
+
// dep's cmake.cache namespace into -D args here keeps the dep's source-
|
|
167
|
+
// build CMake invocation aligned with its author's declared defaults
|
|
168
|
+
// while preserving cross-consumer reproducibility. The root's CLI
|
|
169
|
+
// --conf / wyvrn.local.json overlay is invisible at this seam.
|
|
170
|
+
const clonedConfFlat = readRecipeConf(clonedManifest);
|
|
171
|
+
const clonedCacheVars = cmakeCacheVariables(clonedConfFlat);
|
|
172
|
+
if (Object.keys(clonedCacheVars).length > 0) {
|
|
173
|
+
const confDerivedArgs = Object.entries(clonedCacheVars)
|
|
174
|
+
.map(([k, v]) => `-D${k}=${v}`);
|
|
175
|
+
recipe.configure = [...recipe.configure, ...confDerivedArgs];
|
|
176
|
+
log.info(
|
|
177
|
+
` conf.cmake.cache → ${confDerivedArgs.length} source-build ` +
|
|
178
|
+
`configure arg(s) from the dep's own recipe`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
160
181
|
|
|
161
182
|
// ── 4. Recursively resolve + download this package's OWN deps ──────────
|
|
162
183
|
// Lazy-require: downloadDependencies is where we were called from, so
|
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
|
};
|