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/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
|
|
|
@@ -165,9 +191,21 @@ yargs
|
|
|
165
191
|
.option('config', {
|
|
166
192
|
alias: 'c',
|
|
167
193
|
type: 'string',
|
|
168
|
-
choices: ['Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel'],
|
|
169
194
|
default: 'Release',
|
|
170
|
-
description:
|
|
195
|
+
description:
|
|
196
|
+
'Build configuration. Accepts a single value (Release) or a ' +
|
|
197
|
+
'comma-separated list (Debug,Release) to build multiple configs ' +
|
|
198
|
+
'in one invocation — configure runs once, build (+ --install) ' +
|
|
199
|
+
'loops per config. Valid: Debug | Release | RelWithDebInfo | MinSizeRel.',
|
|
200
|
+
})
|
|
201
|
+
.option('all-configs', {
|
|
202
|
+
type: 'boolean',
|
|
203
|
+
default: false,
|
|
204
|
+
description:
|
|
205
|
+
'Build every config declared in the recipe\'s build.configs ' +
|
|
206
|
+
'(falls back to all four canonical configs when no recipe is ' +
|
|
207
|
+
'present). Equivalent to spelling out --config Debug,Release,... ' +
|
|
208
|
+
'by hand. Pairs naturally with --install.',
|
|
171
209
|
})
|
|
172
210
|
.option('target', {
|
|
173
211
|
alias: 't',
|
|
@@ -220,15 +258,94 @@ yargs
|
|
|
220
258
|
.option('timeout', {
|
|
221
259
|
type: 'number',
|
|
222
260
|
default: 300,
|
|
261
|
+
})
|
|
262
|
+
.option('conf', {
|
|
263
|
+
type: 'string',
|
|
264
|
+
array: true,
|
|
265
|
+
description:
|
|
266
|
+
'Non-ABI build-time override forwarded to cmake --preset as -D / -U. ' +
|
|
267
|
+
'Repeatable. Format: <namespace.leaf>=<value> — e.g. ' +
|
|
268
|
+
'--conf cmake.cache.CHROMA_ENABLE_TEST=ON. CLI --conf does NOT ' +
|
|
269
|
+
'regenerate CMakePresets.json; edit wyvrn.local.json and re-run ' +
|
|
270
|
+
'`wyvrnpm install` for persistent overrides. See claude/PLAN-CONF.md.',
|
|
223
271
|
});
|
|
224
272
|
},
|
|
225
273
|
(argv) => build({
|
|
226
274
|
...argv,
|
|
227
275
|
autoInstall: argv['auto-install'],
|
|
228
276
|
installPrefix: argv['install-prefix'],
|
|
277
|
+
allConfigs: argv['all-configs'],
|
|
229
278
|
}),
|
|
230
279
|
)
|
|
231
280
|
|
|
281
|
+
// ── cache ─────────────────────────────────────────────────────────────────
|
|
282
|
+
.command(
|
|
283
|
+
'cache',
|
|
284
|
+
'Inspect and selectively prune the machine-wide source-build cache (F12). Use `clean --build-cache` to wipe everything.',
|
|
285
|
+
(y) => {
|
|
286
|
+
y
|
|
287
|
+
.command(
|
|
288
|
+
'list [pattern]',
|
|
289
|
+
'List cache entries. Optional glob pattern filters by package name.',
|
|
290
|
+
(y2) => {
|
|
291
|
+
y2
|
|
292
|
+
.positional('pattern', {
|
|
293
|
+
type: 'string',
|
|
294
|
+
description: 'Glob over package names, e.g. "boost*" (alias for --pattern)',
|
|
295
|
+
})
|
|
296
|
+
.option('pattern', {
|
|
297
|
+
type: 'string',
|
|
298
|
+
description: 'Glob over package names; overrides the positional',
|
|
299
|
+
})
|
|
300
|
+
.option('format', {
|
|
301
|
+
type: 'string',
|
|
302
|
+
choices: ['text', 'json'],
|
|
303
|
+
default: 'text',
|
|
304
|
+
description: 'Output format',
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
(argv) => cache.list(argv),
|
|
308
|
+
)
|
|
309
|
+
.command(
|
|
310
|
+
'prune',
|
|
311
|
+
'Remove cache entries per policy. Requires at least one of --keep-last / --older-than.',
|
|
312
|
+
(y2) => {
|
|
313
|
+
y2
|
|
314
|
+
.option('keep-last', {
|
|
315
|
+
type: 'number',
|
|
316
|
+
description:
|
|
317
|
+
'Retain the N most recently-touched entries per (name, version). ' +
|
|
318
|
+
'Older variants beyond N are pruned.',
|
|
319
|
+
})
|
|
320
|
+
.option('older-than', {
|
|
321
|
+
type: 'string',
|
|
322
|
+
description:
|
|
323
|
+
'Remove entries whose mtime is older than this threshold. ' +
|
|
324
|
+
'Format: <number><unit>, unit ∈ s/m/h/d (e.g. 30d, 72h).',
|
|
325
|
+
})
|
|
326
|
+
.option('pattern', {
|
|
327
|
+
type: 'string',
|
|
328
|
+
description: 'Limit scope to package names matching this glob (e.g. "boost*")',
|
|
329
|
+
})
|
|
330
|
+
.option('dry-run', {
|
|
331
|
+
type: 'boolean',
|
|
332
|
+
default: false,
|
|
333
|
+
description: 'Print what would be removed without actually deleting',
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
(argv) => cache.prune({
|
|
337
|
+
...argv,
|
|
338
|
+
keepLast: argv['keep-last'],
|
|
339
|
+
olderThan: argv['older-than'],
|
|
340
|
+
dryRun: argv['dry-run'],
|
|
341
|
+
}),
|
|
342
|
+
)
|
|
343
|
+
.demandCommand(1)
|
|
344
|
+
.help();
|
|
345
|
+
},
|
|
346
|
+
() => {},
|
|
347
|
+
)
|
|
348
|
+
|
|
232
349
|
// ── clean ─────────────────────────────────────────────────────────────────
|
|
233
350
|
.command(
|
|
234
351
|
'clean',
|
|
@@ -278,7 +395,11 @@ yargs
|
|
|
278
395
|
})
|
|
279
396
|
.option('token', {
|
|
280
397
|
type: 'string',
|
|
281
|
-
description: 'Bearer token for HTTP sources',
|
|
398
|
+
description: 'Bearer token for HTTP sources. Prefer --token-env in CI so the literal value never lands in argv.',
|
|
399
|
+
})
|
|
400
|
+
.option('token-env', {
|
|
401
|
+
type: 'string',
|
|
402
|
+
description: 'Name of an env var whose value is the bearer token for HTTP sources.',
|
|
282
403
|
})
|
|
283
404
|
.option('format', {
|
|
284
405
|
type: 'string',
|
|
@@ -287,7 +408,7 @@ yargs
|
|
|
287
408
|
description: 'Output format. "json" emits a single JSON object on stdout; log lines go to stderr.',
|
|
288
409
|
});
|
|
289
410
|
},
|
|
290
|
-
(argv) => show({ ...argv, awsProfile: argv['aws-profile'] }),
|
|
411
|
+
(argv) => show({ ...argv, awsProfile: argv['aws-profile'], tokenEnv: argv['token-env'] }),
|
|
291
412
|
)
|
|
292
413
|
|
|
293
414
|
// ── publish ───────────────────────────────────────────────────────────────
|
|
@@ -318,7 +439,11 @@ yargs
|
|
|
318
439
|
})
|
|
319
440
|
.option('token', {
|
|
320
441
|
type: 'string',
|
|
321
|
-
description: 'Bearer token for HTTP server authentication',
|
|
442
|
+
description: 'Bearer token for HTTP server authentication. Prefer --token-env in CI so the literal value never lands in argv.',
|
|
443
|
+
})
|
|
444
|
+
.option('token-env', {
|
|
445
|
+
type: 'string',
|
|
446
|
+
description: 'Name of an env var whose value is the bearer token for HTTP authentication.',
|
|
322
447
|
})
|
|
323
448
|
.option('force', {
|
|
324
449
|
alias: 'f',
|
|
@@ -334,6 +459,15 @@ yargs
|
|
|
334
459
|
'Override an option value at publish time. Repeatable. ' +
|
|
335
460
|
'Format: <pkg>:<name>=<value> (pkg must match the package being published).',
|
|
336
461
|
})
|
|
462
|
+
.option('conf', {
|
|
463
|
+
type: 'string',
|
|
464
|
+
array: true,
|
|
465
|
+
description:
|
|
466
|
+
'Non-ABI build-time override recorded as `publishedConf` on the ' +
|
|
467
|
+
'uploaded manifest (informational — does not affect profileHash). ' +
|
|
468
|
+
'Reads recipe conf + CLI only; wyvrn.local.json is intentionally ' +
|
|
469
|
+
'ignored for publish.',
|
|
470
|
+
})
|
|
337
471
|
.option('format', {
|
|
338
472
|
type: 'string',
|
|
339
473
|
choices: ['text', 'json'],
|
|
@@ -342,7 +476,7 @@ yargs
|
|
|
342
476
|
})
|
|
343
477
|
;
|
|
344
478
|
},
|
|
345
|
-
(argv) => publish({ ...argv, awsProfile: argv['aws-profile'] }),
|
|
479
|
+
(argv) => publish({ ...argv, awsProfile: argv['aws-profile'], tokenEnv: argv['token-env'] }),
|
|
346
480
|
)
|
|
347
481
|
|
|
348
482
|
// ── configure ─────────────────────────────────────────────────────────────
|
|
@@ -364,7 +498,8 @@ yargs
|
|
|
364
498
|
.option('name', { type: 'string', demandOption: true })
|
|
365
499
|
.option('url', { type: 'string', demandOption: true })
|
|
366
500
|
.option('profile', { type: 'string', description: 'AWS SSO profile (S3 only)' })
|
|
367
|
-
.option('token', { type: 'string', description: 'Bearer token (HTTP only)' })
|
|
501
|
+
.option('token', { type: 'string', description: 'Bearer token (HTTP only). Persists in plaintext in config.json — prefer --token-env for CI/shared setups.' })
|
|
502
|
+
.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
503
|
},
|
|
369
504
|
(argv) => configure.addSource(argv),
|
|
370
505
|
)
|
|
@@ -438,7 +573,15 @@ yargs
|
|
|
438
573
|
.option('compiler', { type: 'string', description: 'Compiler (msvc | gcc | clang | apple-clang)' })
|
|
439
574
|
.option('compiler-version', { type: 'string', description: 'Compiler version (193 | 11 | 14 | ...)' })
|
|
440
575
|
.option('cppstd', { type: 'string', description: 'C++ standard (14 | 17 | 20 | 23)' })
|
|
441
|
-
.option('runtime', { type: 'string', description: 'Runtime linkage (static | dynamic)' })
|
|
576
|
+
.option('runtime', { type: 'string', description: 'Runtime linkage (static | dynamic)' })
|
|
577
|
+
.option('conf', {
|
|
578
|
+
type: 'string',
|
|
579
|
+
array: true,
|
|
580
|
+
description:
|
|
581
|
+
'Profile-level non-ABI build knob — <namespace.leaf>=<value> ' +
|
|
582
|
+
'(repeatable). `KEY` alone means `KEY=ON`; `KEY=` removes the ' +
|
|
583
|
+
'leaf. Phase 1 allow-list: cmake.cache.*. See PLAN-CONF.md §4.',
|
|
584
|
+
});
|
|
442
585
|
},
|
|
443
586
|
(argv) => profile.set({ ...argv, compilerVersion: argv['compiler-version'] }),
|
|
444
587
|
)
|
|
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,8 @@ 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');
|
|
18
|
+
const { wyvrnFetch } = require('../http-fetch');
|
|
17
19
|
const log = require('../logger');
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -155,8 +157,28 @@ async function buildFromSource(args) {
|
|
|
155
157
|
}
|
|
156
158
|
// Pass effective options into the recipe so `${options.<name>}` tokens in
|
|
157
159
|
// `build.configure` / `build.buildArgs` expand to the concrete values that
|
|
158
|
-
// produced this build's profileHash.
|
|
159
|
-
|
|
160
|
+
// produced this build's profileHash. Pass the cloned manifest so PLAN-CONF.md
|
|
161
|
+
// §4.7 collision detection catches a publisher who declared the same KEY in
|
|
162
|
+
// both build.configure and conf.cmake.cache.
|
|
163
|
+
const recipe = normalizeRecipe(clonedManifest.build, options, clonedManifest);
|
|
164
|
+
|
|
165
|
+
// PLAN-CONF.md §4.8.2: source-build of a dep consumes ONLY the dep's own
|
|
166
|
+
// recipe conf — never the root project's merged conf. Translating the
|
|
167
|
+
// dep's cmake.cache namespace into -D args here keeps the dep's source-
|
|
168
|
+
// build CMake invocation aligned with its author's declared defaults
|
|
169
|
+
// while preserving cross-consumer reproducibility. The root's CLI
|
|
170
|
+
// --conf / wyvrn.local.json overlay is invisible at this seam.
|
|
171
|
+
const clonedConfFlat = readRecipeConf(clonedManifest);
|
|
172
|
+
const clonedCacheVars = cmakeCacheVariables(clonedConfFlat);
|
|
173
|
+
if (Object.keys(clonedCacheVars).length > 0) {
|
|
174
|
+
const confDerivedArgs = Object.entries(clonedCacheVars)
|
|
175
|
+
.map(([k, v]) => `-D${k}=${v}`);
|
|
176
|
+
recipe.configure = [...recipe.configure, ...confDerivedArgs];
|
|
177
|
+
log.info(
|
|
178
|
+
` conf.cmake.cache → ${confDerivedArgs.length} source-build ` +
|
|
179
|
+
`configure arg(s) from the dep's own recipe`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
160
182
|
|
|
161
183
|
// ── 4. Recursively resolve + download this package's OWN deps ──────────
|
|
162
184
|
// Lazy-require: downloadDependencies is where we were called from, so
|
|
@@ -174,7 +196,7 @@ async function buildFromSource(args) {
|
|
|
174
196
|
log.info(` resolving ${Object.keys(depsForResolution).length} transitive dep(s)`);
|
|
175
197
|
const transitiveManifests = new Map();
|
|
176
198
|
const resolvedDeps = await resolveDependencies(
|
|
177
|
-
depsForResolution, packageSources, platform,
|
|
199
|
+
depsForResolution, packageSources, platform, wyvrnFetch, new Map(),
|
|
178
200
|
transitiveManifests,
|
|
179
201
|
);
|
|
180
202
|
|
|
@@ -210,7 +232,7 @@ async function buildFromSource(args) {
|
|
|
210
232
|
if (enrichedDeps.size > 0) {
|
|
211
233
|
await downloadDependencies(
|
|
212
234
|
enrichedDeps, packageSources, platform, paths.internal,
|
|
213
|
-
|
|
235
|
+
wyvrnFetch, Math.max(30, Math.round(timeoutMs / 1000)),
|
|
214
236
|
{ ...authOptions, buildMode },
|
|
215
237
|
);
|
|
216
238
|
} else {
|