wyvrnpm 2.1.0 → 2.3.2
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 +257 -1
- package/bin/wyvrn.js +131 -14
- package/claude/skills/wyvrnpm.skill +0 -0
- package/package.json +2 -1
- package/src/build/cache.js +47 -0
- package/src/build/index.js +42 -11
- package/src/build/recipe.js +30 -4
- package/src/commands/build.js +49 -8
- package/src/commands/clean.js +39 -15
- package/src/commands/install-skill.js +107 -0
- package/src/commands/install.js +201 -5
- package/src/commands/publish.js +150 -35
- package/src/commands/show.js +237 -0
- package/src/compat.js +32 -3
- package/src/download.js +65 -3
- package/src/ignore.js +118 -0
- package/src/logger.js +26 -10
- package/src/manifest.js +12 -7
- package/src/options.js +303 -0
- package/src/profile.js +28 -4
- package/src/providers/base.js +16 -1
- package/src/providers/file.js +8 -3
- package/src/providers/http.js +12 -8
- package/src/providers/s3.js +11 -7
- package/src/resolve.js +171 -13
- package/src/toolchain/presets.js +88 -16
- package/src/upload-built.js +256 -0
- package/src/version-range.js +301 -0
package/src/commands/publish.js
CHANGED
|
@@ -10,44 +10,37 @@ const { getProvider } = require('../provide
|
|
|
10
10
|
const { readConfig } = require('../config');
|
|
11
11
|
const { loadProfile, hashProfile, sha256Of, getGitSha, getGitRepo, isShaOnRemote } = require('../profile');
|
|
12
12
|
const { defaultCompatBlock } = require('../compat');
|
|
13
|
+
const {
|
|
14
|
+
normalizeOptionsDeclaration,
|
|
15
|
+
resolveEffectiveOptions,
|
|
16
|
+
parseCliOptions,
|
|
17
|
+
} = require('../options');
|
|
18
|
+
const { loadIgnorePatterns, isIgnored } = require('../ignore');
|
|
19
|
+
const { hasBuildRecipe, normalizeRecipe } = require('../build/recipe');
|
|
13
20
|
const log = require('../logger');
|
|
14
21
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.split(/\r?\n/)
|
|
24
|
-
.map((l) => l.trim())
|
|
25
|
-
.filter((l) => l.length > 0 && !l.startsWith('#'));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function patternToRegex(pattern) {
|
|
29
|
-
const anchored = pattern.startsWith('/');
|
|
30
|
-
const p = anchored ? pattern.slice(1) : pattern;
|
|
31
|
-
let re = p
|
|
32
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
33
|
-
.replace(/\*\*/g, '\x00')
|
|
34
|
-
.replace(/\*/g, '[^/]*')
|
|
35
|
-
.replace(/\?/g, '[^/]')
|
|
36
|
-
.replace(/\x00/g, '.*');
|
|
37
|
-
return anchored ? new RegExp(`^${re}(/|$)`) : new RegExp(`(^|/)${re}(/|$)`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function addFolderFiltered(zip, dir, base, ignoreRe) {
|
|
22
|
+
/**
|
|
23
|
+
* Walk `dir` recursively and add every file to `zip`, skipping anything that
|
|
24
|
+
* matches `.wyvrnignore` patterns OR whose zip-relative path is already in
|
|
25
|
+
* `reservedRelPaths`. The reserved set lets the install-tree overlay win
|
|
26
|
+
* on path collisions (e.g. `include/foo.h` shipped by both source and
|
|
27
|
+
* install trees).
|
|
28
|
+
*/
|
|
29
|
+
function addFolderFiltered(zip, dir, base, patterns, reservedRelPaths = new Set()) {
|
|
41
30
|
for (const entry of fs.readdirSync(dir)) {
|
|
42
31
|
const fullPath = path.join(dir, entry);
|
|
43
32
|
const relPath = path.relative(base, fullPath).replace(/\\/g, '/');
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
const stat = fs.statSync(fullPath);
|
|
34
|
+
const isDir = stat.isDirectory();
|
|
35
|
+
if (isIgnored(relPath, isDir, patterns)) {
|
|
36
|
+
log.info(` Ignoring: ${relPath}${isDir ? '/' : ''}`);
|
|
46
37
|
continue;
|
|
47
38
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
if (isDir) {
|
|
40
|
+
addFolderFiltered(zip, fullPath, base, patterns, reservedRelPaths);
|
|
41
|
+
} else if (reservedRelPaths.has(relPath)) {
|
|
42
|
+
// Install tree has a canonical version of this file; let that win.
|
|
43
|
+
log.info(` Install-tree wins: ${relPath}`);
|
|
51
44
|
} else {
|
|
52
45
|
const zipDir = path.dirname(relPath).replace(/\\/g, '/');
|
|
53
46
|
zip.addLocalFile(fullPath, zipDir === '.' ? '' : zipDir);
|
|
@@ -55,6 +48,42 @@ function addFolderFiltered(zip, dir, base, ignoreRe) {
|
|
|
55
48
|
}
|
|
56
49
|
}
|
|
57
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Collect every file under `dir` as a `{ fullPath, relPath }` pair. `relPath`
|
|
53
|
+
* is forward-slash-separated and relative to `dir` — so the install tree's
|
|
54
|
+
* `install/cmake/FooConfig.cmake` lands as `cmake/FooConfig.cmake` at the
|
|
55
|
+
* zip root, which is where `find_package(Foo CONFIG)` expects it on the
|
|
56
|
+
* consumer side after extraction into `wyvrn_internal/<name>/`.
|
|
57
|
+
*/
|
|
58
|
+
function collectInstallTreeFiles(dir) {
|
|
59
|
+
const files = [];
|
|
60
|
+
function walk(current) {
|
|
61
|
+
for (const entry of fs.readdirSync(current)) {
|
|
62
|
+
const full = path.join(current, entry);
|
|
63
|
+
const stat = fs.statSync(full);
|
|
64
|
+
if (stat.isDirectory()) {
|
|
65
|
+
walk(full);
|
|
66
|
+
} else {
|
|
67
|
+
const rel = path.relative(dir, full).replace(/\\/g, '/');
|
|
68
|
+
files.push({ fullPath: full, relPath: rel });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
walk(dir);
|
|
73
|
+
return files;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function addInstallTree(zip, installDir) {
|
|
77
|
+
const files = collectInstallTreeFiles(installDir);
|
|
78
|
+
const added = new Set();
|
|
79
|
+
for (const { fullPath, relPath } of files) {
|
|
80
|
+
const zipDir = path.dirname(relPath).replace(/\\/g, '/');
|
|
81
|
+
zip.addLocalFile(fullPath, zipDir === '.' ? '' : zipDir);
|
|
82
|
+
added.add(relPath);
|
|
83
|
+
}
|
|
84
|
+
return added;
|
|
85
|
+
}
|
|
86
|
+
|
|
58
87
|
// ---------------------------------------------------------------------------
|
|
59
88
|
// Source resolution
|
|
60
89
|
// ---------------------------------------------------------------------------
|
|
@@ -101,6 +130,8 @@ async function publish(argv) {
|
|
|
101
130
|
force,
|
|
102
131
|
manifest: manifestArg,
|
|
103
132
|
} = argv;
|
|
133
|
+
const jsonOut = argv.format === 'json';
|
|
134
|
+
if (jsonOut) log.setJsonMode(true);
|
|
104
135
|
|
|
105
136
|
// ── Load manifest ─────────────────────────────────────────────────────────
|
|
106
137
|
const manifestPath = path.resolve(manifestArg || './wyvrn.json');
|
|
@@ -135,7 +166,23 @@ async function publish(argv) {
|
|
|
135
166
|
const config = readConfig();
|
|
136
167
|
const activeProfileName = profileName ?? config.defaultProfile ?? 'default';
|
|
137
168
|
const { profile: buildProfile, fromFile } = loadProfile(activeProfileName);
|
|
138
|
-
|
|
169
|
+
|
|
170
|
+
// ── Resolve this package's own declared options ─────────────────────────
|
|
171
|
+
// Publishing needs to commit to a specific value for every option the
|
|
172
|
+
// recipe declares. Sources for overrides: CLI `-o <pkg>:...` flags
|
|
173
|
+
// (scoped to the package being published), else the recipe's defaults.
|
|
174
|
+
const ownDeclaration = manifest.options
|
|
175
|
+
? normalizeOptionsDeclaration(manifest.options, `${name}@${version}`)
|
|
176
|
+
: null;
|
|
177
|
+
const cliOptionsByPkg = parseCliOptions(argv.option);
|
|
178
|
+
const effectiveOptions = resolveEffectiveOptions(
|
|
179
|
+
ownDeclaration,
|
|
180
|
+
{}, // consumer-side overrides don't apply to self-publish
|
|
181
|
+
cliOptionsByPkg[name] ?? {},
|
|
182
|
+
`${name}@${version}`,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const profileHash = hashProfile(buildProfile, effectiveOptions);
|
|
139
186
|
|
|
140
187
|
log.info(
|
|
141
188
|
`Publishing ${name}@${version}\n` +
|
|
@@ -144,6 +191,7 @@ async function publish(argv) {
|
|
|
144
191
|
`${buildProfile.compiler} ${buildProfile['compiler.version']} ` +
|
|
145
192
|
`C++${buildProfile['compiler.cppstd']}` +
|
|
146
193
|
(buildProfile['compiler.runtime'] ? ` [${buildProfile['compiler.runtime']}]` : '') + '\n' +
|
|
194
|
+
(effectiveOptions ? `Options : ${JSON.stringify(effectiveOptions)}\n` : '') +
|
|
147
195
|
`Profile hash: ${profileHash}\n` +
|
|
148
196
|
`Provider : ${provider.constructor.providerName}`,
|
|
149
197
|
);
|
|
@@ -201,7 +249,6 @@ async function publish(argv) {
|
|
|
201
249
|
// ── .wyvrnignore ──────────────────────────────────────────────────────────
|
|
202
250
|
const ignoreFile = path.join(srcDir, '.wyvrnignore');
|
|
203
251
|
const ignorePatterns = loadIgnorePatterns(ignoreFile);
|
|
204
|
-
const ignoreRe = ignorePatterns.map(patternToRegex);
|
|
205
252
|
if (ignorePatterns.length > 0) {
|
|
206
253
|
log.info(`Loaded ${ignorePatterns.length} pattern(s) from .wyvrnignore`);
|
|
207
254
|
}
|
|
@@ -229,10 +276,55 @@ async function publish(argv) {
|
|
|
229
276
|
log.info(`Using package-declared compatibility: ${JSON.stringify(manifest.compatibility)}`);
|
|
230
277
|
}
|
|
231
278
|
|
|
279
|
+
// ── Install-tree overlay ──────────────────────────────────────────────────
|
|
280
|
+
// Publish zips the source tree (filtered by .wyvrnignore), but consumers
|
|
281
|
+
// actually need the install tree — `lib/cmake/<Name>/<Name>Config.cmake`,
|
|
282
|
+
// the installed headers, compiled libs — so find_package() works after
|
|
283
|
+
// extraction. When a build recipe is declared, locate the per-profile
|
|
284
|
+
// install dir (populated by `wyvrnpm build --config <cfg> --install`) and
|
|
285
|
+
// overlay its contents on top of the source zip. Install-tree files win
|
|
286
|
+
// on path collisions (e.g. `include/`).
|
|
287
|
+
let installOverlayDir = null;
|
|
288
|
+
let installOverlayPaths = new Set();
|
|
289
|
+
if (hasBuildRecipe(manifest)) {
|
|
290
|
+
try {
|
|
291
|
+
const recipe = normalizeRecipe(manifest.build, effectiveOptions);
|
|
292
|
+
const candidate = path.join(
|
|
293
|
+
srcDir, 'build', `wyvrn-${activeProfileName}`, recipe.installDir,
|
|
294
|
+
);
|
|
295
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
296
|
+
installOverlayDir = candidate;
|
|
297
|
+
} else {
|
|
298
|
+
log.warn(
|
|
299
|
+
`build recipe is declared but no install tree found at ${candidate}.\n` +
|
|
300
|
+
' Consumers\' find_package() will likely fail. Fix: run\n' +
|
|
301
|
+
' wyvrnpm build --config Debug --install\n' +
|
|
302
|
+
' wyvrnpm build --config Release --install\n' +
|
|
303
|
+
' wyvrnpm build --config RelWithDebInfo --install\n' +
|
|
304
|
+
' wyvrnpm build --config MinSizeRel --install\n' +
|
|
305
|
+
' then re-run publish.',
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
} catch (err) {
|
|
309
|
+
log.warn(`could not locate install tree: ${err.message} — publishing source tree only`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
232
313
|
try {
|
|
233
314
|
log.info(`Zipping ${srcDir} ...`);
|
|
234
315
|
const zip = new AdmZip();
|
|
235
|
-
|
|
316
|
+
if (installOverlayDir) {
|
|
317
|
+
// Collect install-tree paths first so the source-tree walker can skip
|
|
318
|
+
// anything the install tree will add canonically.
|
|
319
|
+
const peek = collectInstallTreeFiles(installOverlayDir);
|
|
320
|
+
installOverlayPaths = new Set(peek.map((f) => f.relPath));
|
|
321
|
+
}
|
|
322
|
+
addFolderFiltered(zip, srcDir, srcDir, ignorePatterns, installOverlayPaths);
|
|
323
|
+
if (installOverlayDir) {
|
|
324
|
+
log.info(`Overlaying install tree: ${installOverlayDir}`);
|
|
325
|
+
const added = addInstallTree(zip, installOverlayDir);
|
|
326
|
+
log.info(` Added ${added.size} file(s) from install tree`);
|
|
327
|
+
}
|
|
236
328
|
zip.writeZip(tmpZipPath);
|
|
237
329
|
|
|
238
330
|
const contentSha256 = sha256Of(tmpZipPath);
|
|
@@ -248,7 +340,9 @@ async function publish(argv) {
|
|
|
248
340
|
version,
|
|
249
341
|
profileHash,
|
|
250
342
|
// buildSettings stored in metadata for debugging — NOT wyvrn.json
|
|
251
|
-
buildSettings:
|
|
343
|
+
buildSettings: effectiveOptions
|
|
344
|
+
? { ...buildProfile, options: effectiveOptions }
|
|
345
|
+
: buildProfile,
|
|
252
346
|
contentSha256,
|
|
253
347
|
gitSha,
|
|
254
348
|
gitRepo,
|
|
@@ -259,6 +353,23 @@ async function publish(argv) {
|
|
|
259
353
|
);
|
|
260
354
|
|
|
261
355
|
log.success(`Successfully published ${name}@${version} [${profileHash}] to ${source}`);
|
|
356
|
+
|
|
357
|
+
if (jsonOut) {
|
|
358
|
+
const payload = {
|
|
359
|
+
command: 'publish',
|
|
360
|
+
wyvrnpmVersion: require('../../package.json').version,
|
|
361
|
+
name,
|
|
362
|
+
version,
|
|
363
|
+
profileHash,
|
|
364
|
+
source,
|
|
365
|
+
contentSha256,
|
|
366
|
+
options: effectiveOptions ?? null,
|
|
367
|
+
gitSha: gitSha ?? null,
|
|
368
|
+
gitRepo: gitRepo ?? null,
|
|
369
|
+
publishedAt: new Date().toISOString(),
|
|
370
|
+
};
|
|
371
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
372
|
+
}
|
|
262
373
|
} finally {
|
|
263
374
|
if (fs.existsSync(tmpZipPath)) fs.unlinkSync(tmpZipPath);
|
|
264
375
|
if (fs.existsSync(tmpManifestPath)) fs.unlinkSync(tmpManifestPath);
|
|
@@ -266,3 +377,7 @@ async function publish(argv) {
|
|
|
266
377
|
}
|
|
267
378
|
|
|
268
379
|
module.exports = publish;
|
|
380
|
+
// Exported for tests only
|
|
381
|
+
module.exports.addFolderFiltered = addFolderFiltered;
|
|
382
|
+
module.exports.addInstallTree = addInstallTree;
|
|
383
|
+
module.exports.collectInstallTreeFiles = collectInstallTreeFiles;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { readConfig } = require('../config');
|
|
4
|
+
const { getProvider } = require('../providers');
|
|
5
|
+
const log = require('../logger');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `wyvrnpm show <pkg>` — surface registry metadata for a package.
|
|
9
|
+
*
|
|
10
|
+
* Accepts:
|
|
11
|
+
* name → list every published version + profile.
|
|
12
|
+
* name@version → list every profile for that version,
|
|
13
|
+
* fetch full metadata for each, highlight
|
|
14
|
+
* consumer (`uploadedBy`) vs author builds.
|
|
15
|
+
* name@version@profileHash → dump full metadata for one build.
|
|
16
|
+
*
|
|
17
|
+
* Sources: CLI `--source` overrides config.installSources. First source
|
|
18
|
+
* that returns data wins (same first-match policy as `install`).
|
|
19
|
+
*
|
|
20
|
+
* Added for PLAN-UPLOAD-BUILT phase 3 so maintainers can spot-check which
|
|
21
|
+
* profileHashes were uploaded by consumers vs published by an author. The
|
|
22
|
+
* `uploadedBy` block is the signal; its absence means "author publish"
|
|
23
|
+
* by convention.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} argv
|
|
26
|
+
* @param {string} argv.pkg e.g. "zlib", "zlib@1.3.0.0", "zlib@1.3.0.0@a1b2..."
|
|
27
|
+
* @param {string[]} [argv.source] source URLs
|
|
28
|
+
* @param {string} [argv.awsProfile]
|
|
29
|
+
* @param {string} [argv.token]
|
|
30
|
+
* @returns {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
async function show(argv) {
|
|
33
|
+
const { name, version, profileHash } = parseSpec(argv.pkg);
|
|
34
|
+
if (!name) {
|
|
35
|
+
log.error('Usage: wyvrnpm show <name>[@<version>[@<profileHash>]]');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const jsonOut = argv.format === 'json';
|
|
40
|
+
if (jsonOut) log.setJsonMode(true);
|
|
41
|
+
|
|
42
|
+
const { sources, awsProfile, token } = resolveSources(argv);
|
|
43
|
+
if (sources.length === 0) {
|
|
44
|
+
log.error(
|
|
45
|
+
'no sources configured. ' +
|
|
46
|
+
'Pass --source <url> or run: wyvrnpm configure add-source --kind install ...',
|
|
47
|
+
);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const source of sources) {
|
|
52
|
+
let provider;
|
|
53
|
+
try { provider = getProvider(source); } catch { continue; }
|
|
54
|
+
|
|
55
|
+
const idx = await provider.v2GetVersionsIndex({ source, name, awsProfile, token });
|
|
56
|
+
if (!idx?.versions) continue;
|
|
57
|
+
|
|
58
|
+
const versionKeys = version
|
|
59
|
+
? (idx.versions[version] ? [version] : [])
|
|
60
|
+
: Object.keys(idx.versions).sort();
|
|
61
|
+
|
|
62
|
+
if (version && versionKeys.length === 0) {
|
|
63
|
+
log.error(`Version ${version} not found on ${source}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Collect a single structured result as we walk. Text rendering happens
|
|
68
|
+
// inline in the legacy path; JSON mode emits the whole object at the
|
|
69
|
+
// end of the first source that answered.
|
|
70
|
+
const result = {
|
|
71
|
+
command: 'show',
|
|
72
|
+
wyvrnpmVersion: require('../../package.json').version,
|
|
73
|
+
source,
|
|
74
|
+
package: name,
|
|
75
|
+
latest: idx.latest ?? null,
|
|
76
|
+
versions: {},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (!jsonOut) {
|
|
80
|
+
console.log(`${name} (source: ${source})`);
|
|
81
|
+
if (idx.latest) console.log(` latest: ${idx.latest}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const v of versionKeys) {
|
|
85
|
+
const ventry = idx.versions[v];
|
|
86
|
+
const profiles = ventry?.profiles ?? {};
|
|
87
|
+
const profileKeys = profileHash
|
|
88
|
+
? (profiles[profileHash] ? [profileHash] : [])
|
|
89
|
+
: Object.keys(profiles).sort();
|
|
90
|
+
|
|
91
|
+
if (profileHash && profileKeys.length === 0) {
|
|
92
|
+
log.error(`ProfileHash ${profileHash} not found for ${name}@${v}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!jsonOut) {
|
|
97
|
+
console.log(`\n ${name}@${v} (${profileKeys.length} profile${profileKeys.length === 1 ? '' : 's'})`);
|
|
98
|
+
if (ventry.source?.gitSha) {
|
|
99
|
+
console.log(` git: ${ventry.source.gitRepo ?? '?'} @ ${ventry.source.gitSha}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// For bare `name` queries we'd fetch O(M×N) manifests (versions ×
|
|
104
|
+
// profiles) — too noisy. Only deep-fetch when the user has narrowed
|
|
105
|
+
// to a specific version.
|
|
106
|
+
const deepFetch = Boolean(version);
|
|
107
|
+
|
|
108
|
+
const profileArr = [];
|
|
109
|
+
for (const hash of profileKeys) {
|
|
110
|
+
const entry = profiles[hash];
|
|
111
|
+
if (!jsonOut) {
|
|
112
|
+
console.log(`\n [${hash}]`);
|
|
113
|
+
if (entry.buildSettings) {
|
|
114
|
+
console.log(` profile : ${formatBuildSettings(entry.buildSettings)}`);
|
|
115
|
+
if (entry.buildSettings.options && Object.keys(entry.buildSettings.options).length > 0) {
|
|
116
|
+
console.log(` options : ${formatOptions(entry.buildSettings.options)}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
console.log(` sha256 : ${entry.contentSha256 ?? '?'}`);
|
|
120
|
+
if (entry.publishedAt) console.log(` published: ${entry.publishedAt}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let meta = null;
|
|
124
|
+
if (deepFetch) {
|
|
125
|
+
meta = await provider.v2GetMeta({
|
|
126
|
+
source, name, version: v, profileHash: hash, awsProfile, token,
|
|
127
|
+
});
|
|
128
|
+
if (!jsonOut) {
|
|
129
|
+
if (meta?.uploadedBy) {
|
|
130
|
+
console.log(` origin : consumer upload (${meta.uploadedBy.kind})`);
|
|
131
|
+
console.log(` uploadedAt : ${meta.uploadedBy.uploadedAt}`);
|
|
132
|
+
console.log(` wyvrnpmVersion : ${meta.uploadedBy.wyvrnpmVersion ?? '?'}`);
|
|
133
|
+
if (meta.uploadedBy.builtFromGit) {
|
|
134
|
+
console.log(` builtFromGit : ${meta.uploadedBy.builtFromGit}`);
|
|
135
|
+
}
|
|
136
|
+
if (meta.uploadedBy.builtFromSha) {
|
|
137
|
+
console.log(` builtFromSha : ${meta.uploadedBy.builtFromSha}`);
|
|
138
|
+
}
|
|
139
|
+
} else if (meta) {
|
|
140
|
+
console.log(` origin : author publish`);
|
|
141
|
+
}
|
|
142
|
+
if (meta?.compatibility) {
|
|
143
|
+
console.log(` compat : ${JSON.stringify(meta.compatibility)}`);
|
|
144
|
+
}
|
|
145
|
+
if (meta?.options && Object.keys(meta.options).length > 0) {
|
|
146
|
+
console.log(` declared: ${formatDeclaredOptions(meta.options)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
profileArr.push({
|
|
152
|
+
profileHash: hash,
|
|
153
|
+
contentSha256: entry.contentSha256 ?? null,
|
|
154
|
+
publishedAt: entry.publishedAt ?? null,
|
|
155
|
+
buildSettings: entry.buildSettings ?? null,
|
|
156
|
+
origin: meta?.uploadedBy ? 'consumer-source-build' : (meta ? 'author-publish' : null),
|
|
157
|
+
uploadedBy: meta?.uploadedBy ?? null,
|
|
158
|
+
compatibility: meta?.compatibility ?? null,
|
|
159
|
+
declaredOptions: meta?.options ?? null,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
result.versions[v] = {
|
|
164
|
+
source: ventry.source ?? null,
|
|
165
|
+
profiles: profileArr,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (jsonOut) {
|
|
170
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
log.error(`Package ${name} not found in any configured source`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parse a package spec "name[@version[@profileHash]]".
|
|
181
|
+
* @param {string} spec
|
|
182
|
+
* @returns {{ name?: string, version?: string, profileHash?: string }}
|
|
183
|
+
*/
|
|
184
|
+
function parseSpec(spec) {
|
|
185
|
+
if (!spec || typeof spec !== 'string') return {};
|
|
186
|
+
const parts = spec.split('@');
|
|
187
|
+
const [name, version, profileHash] = parts;
|
|
188
|
+
return {
|
|
189
|
+
name: name || undefined,
|
|
190
|
+
version: version || undefined,
|
|
191
|
+
profileHash: profileHash || undefined,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function resolveSources(argv) {
|
|
196
|
+
const config = readConfig();
|
|
197
|
+
const sources = (argv.source && argv.source.length > 0)
|
|
198
|
+
? argv.source
|
|
199
|
+
: config.installSources.map((s) => s.url);
|
|
200
|
+
|
|
201
|
+
const firstConfig = config.installSources?.[0];
|
|
202
|
+
return {
|
|
203
|
+
sources,
|
|
204
|
+
awsProfile: argv.awsProfile ?? firstConfig?.profile,
|
|
205
|
+
token: argv.token ?? firstConfig?.token,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function formatBuildSettings(bs) {
|
|
210
|
+
const parts = [
|
|
211
|
+
`${bs.os}/${bs.arch}`,
|
|
212
|
+
`${bs.compiler} ${bs['compiler.version']}`,
|
|
213
|
+
`C++${bs['compiler.cppstd']}`,
|
|
214
|
+
];
|
|
215
|
+
if (bs['compiler.runtime']) parts.push(`[${bs['compiler.runtime']}]`);
|
|
216
|
+
return parts.join(' ');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function formatOptions(options) {
|
|
220
|
+
return Object.entries(options)
|
|
221
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
222
|
+
.map(([k, v]) => `${k}=${typeof v === 'string' ? v : String(v)}`)
|
|
223
|
+
.join(', ');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function formatDeclaredOptions(declared) {
|
|
227
|
+
return Object.entries(declared)
|
|
228
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
229
|
+
.map(([name, spec]) => {
|
|
230
|
+
const allowed = Array.isArray(spec?.allowed) ? spec.allowed : [];
|
|
231
|
+
return `${name} (default=${String(spec?.default)}, allowed=${allowed.map(String).join('|')})`;
|
|
232
|
+
})
|
|
233
|
+
.join('; ');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
module.exports = show;
|
|
237
|
+
module.exports.parseSpec = parseSpec;
|
package/src/compat.js
CHANGED
|
@@ -137,9 +137,16 @@ function evaluateField(mode, consumerVal, packageVal) {
|
|
|
137
137
|
/**
|
|
138
138
|
* Evaluate a package's compatibility block against a consumer's profile.
|
|
139
139
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
140
|
+
* If either `consumerProfile` or `packageProfile` carries an `options`
|
|
141
|
+
* sub-object, options participate in compatibility: every option present
|
|
142
|
+
* on either side is evaluated, with mode `exact` by default. The compat
|
|
143
|
+
* block may override a specific option's mode via `options.<name>` (for
|
|
144
|
+
* example `"options.shared": "any"` to say "this build has no ABI
|
|
145
|
+
* dependence on `shared`"). See PLAN-OPTIONS §3.5.
|
|
146
|
+
*
|
|
147
|
+
* @param {Record<string, any>} consumerProfile may include `options: {...}`
|
|
148
|
+
* @param {Record<string, any>} packageProfile may include `options: {...}`
|
|
149
|
+
* @param {object|null|undefined} rawCompat compatibility block from the package manifest
|
|
143
150
|
* @returns {{
|
|
144
151
|
* compatible: boolean,
|
|
145
152
|
* reasons: Array<{ field: string, mode: string, consumer: any, package: any, outcome: string }>,
|
|
@@ -151,6 +158,10 @@ function evaluateCompat(consumerProfile, packageProfile, rawCompat) {
|
|
|
151
158
|
let compatible = true;
|
|
152
159
|
|
|
153
160
|
for (const [field, mode] of Object.entries(compat)) {
|
|
161
|
+
// `options.<name>` keys in the compat block are handled below,
|
|
162
|
+
// together with any options present on either profile.
|
|
163
|
+
if (field.startsWith('options.')) continue;
|
|
164
|
+
|
|
154
165
|
const cVal = consumerProfile[field];
|
|
155
166
|
const pVal = packageProfile[field];
|
|
156
167
|
const outcome = evaluateField(mode, cVal, pVal);
|
|
@@ -158,6 +169,24 @@ function evaluateCompat(consumerProfile, packageProfile, rawCompat) {
|
|
|
158
169
|
if (outcome === 'incompatible') compatible = false;
|
|
159
170
|
}
|
|
160
171
|
|
|
172
|
+
// Evaluate every option the consumer or package declares. Default mode
|
|
173
|
+
// is `exact`; the compat block may relax to `any` (and the other modes
|
|
174
|
+
// work when the values are numeric strings). Forward-compat: an option
|
|
175
|
+
// present on one side only is treated as `incompatible` under exact —
|
|
176
|
+
// the recipe shapes differ.
|
|
177
|
+
const consumerOpts = consumerProfile.options ?? {};
|
|
178
|
+
const packageOpts = packageProfile.options ?? {};
|
|
179
|
+
const optionKeys = new Set([...Object.keys(consumerOpts), ...Object.keys(packageOpts)]);
|
|
180
|
+
for (const name of optionKeys) {
|
|
181
|
+
const field = `options.${name}`;
|
|
182
|
+
const mode = compat[field] ?? 'exact';
|
|
183
|
+
const cVal = consumerOpts[name];
|
|
184
|
+
const pVal = packageOpts[name];
|
|
185
|
+
const outcome = evaluateField(mode, cVal, pVal);
|
|
186
|
+
reasons.push({ field, mode, consumer: cVal, package: pVal, outcome });
|
|
187
|
+
if (outcome === 'incompatible') compatible = false;
|
|
188
|
+
}
|
|
189
|
+
|
|
161
190
|
return { compatible, reasons };
|
|
162
191
|
}
|
|
163
192
|
|