wyvrnpm 2.1.0 → 2.4.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 +325 -6
- package/bin/wyvrn.js +167 -14
- package/claude/skills/wyvrnpm.skill +0 -0
- package/cmake/macros.cmake +51 -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/add.js +140 -0
- package/src/commands/build.js +96 -12
- package/src/commands/clean.js +39 -15
- package/src/commands/install-skill.js +107 -0
- package/src/commands/install.js +202 -5
- package/src/commands/publish.js +150 -35
- package/src/commands/show.js +237 -0
- package/src/compat.js +53 -12
- 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/index.js +71 -0
- package/src/toolchain/presets.js +125 -16
- package/src/toolchain/template.cmake +11 -0
- 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
|
@@ -34,12 +34,15 @@
|
|
|
34
34
|
* | version fields and leaves room for future "same major"
|
|
35
35
|
* | semantics without renaming.
|
|
36
36
|
*
|
|
37
|
-
* Missing fields default to `exact`. `os` and `arch` are
|
|
38
|
-
*
|
|
39
|
-
* binaries are never ABI-compatible.
|
|
37
|
+
* Missing fields default to `exact`. `os` and `arch` are normally forced
|
|
38
|
+
* to `exact` regardless of what the block says — cross-arch/cross-OS
|
|
39
|
+
* binaries are never ABI-compatible. The one exception is
|
|
40
|
+
* `kind: "HeaderOnlyLib"`: header-only packages ship no binary, so the
|
|
41
|
+
* publisher's declared modes for `os`/`arch` are respected. Binary kinds
|
|
42
|
+
* (`StaticLib`, `DynamicLib`, `ConsoleApp`, …) keep the hard guarantee.
|
|
40
43
|
*/
|
|
41
44
|
|
|
42
|
-
const
|
|
45
|
+
const OS_ARCH_FIELDS = new Set(['os', 'arch']);
|
|
43
46
|
|
|
44
47
|
const DEFAULT_COMPAT_FIELDS = [
|
|
45
48
|
'os',
|
|
@@ -55,19 +58,27 @@ const VALID_MODES = new Set(['exact', 'any', 'lte', 'gte', 'exact-or-newer']);
|
|
|
55
58
|
/**
|
|
56
59
|
* Normalise a raw compatibility block into `{ field → mode }`.
|
|
57
60
|
* Missing fields fall back to `exact`. Unknown modes fall back to `exact`.
|
|
58
|
-
*
|
|
61
|
+
*
|
|
62
|
+
* For binary kinds (`StaticLib`, `DynamicLib`, `ConsoleApp`, …) `os` and
|
|
63
|
+
* `arch` are forced to `exact` — publisher-declared modes are silently
|
|
64
|
+
* dropped because cross-arch binaries are never ABI-safe. For
|
|
65
|
+
* `kind: "HeaderOnlyLib"` the publisher's declared modes are respected,
|
|
66
|
+
* since there is no binary to be ABI-incompatible with.
|
|
59
67
|
*
|
|
60
68
|
* @param {object|null|undefined} rawCompat
|
|
69
|
+
* @param {string|null|undefined} [packageKind] Package manifest `kind` field.
|
|
61
70
|
* @returns {Record<string, string>}
|
|
62
71
|
*/
|
|
63
|
-
function normalizeCompat(rawCompat) {
|
|
72
|
+
function normalizeCompat(rawCompat, packageKind) {
|
|
64
73
|
const result = {};
|
|
65
74
|
for (const field of DEFAULT_COMPAT_FIELDS) result[field] = 'exact';
|
|
66
75
|
|
|
67
76
|
if (!rawCompat || typeof rawCompat !== 'object') return result;
|
|
68
77
|
|
|
78
|
+
const headerOnly = packageKind === 'HeaderOnlyLib';
|
|
79
|
+
|
|
69
80
|
for (const [field, spec] of Object.entries(rawCompat)) {
|
|
70
|
-
if (
|
|
81
|
+
if (!headerOnly && OS_ARCH_FIELDS.has(field)) continue;
|
|
71
82
|
const mode = typeof spec === 'string' ? spec : (spec && spec.mode);
|
|
72
83
|
if (mode && VALID_MODES.has(mode)) result[field] = mode;
|
|
73
84
|
}
|
|
@@ -137,20 +148,32 @@ function evaluateField(mode, consumerVal, packageVal) {
|
|
|
137
148
|
/**
|
|
138
149
|
* Evaluate a package's compatibility block against a consumer's profile.
|
|
139
150
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
151
|
+
* If either `consumerProfile` or `packageProfile` carries an `options`
|
|
152
|
+
* sub-object, options participate in compatibility: every option present
|
|
153
|
+
* on either side is evaluated, with mode `exact` by default. The compat
|
|
154
|
+
* block may override a specific option's mode via `options.<name>` (for
|
|
155
|
+
* example `"options.shared": "any"` to say "this build has no ABI
|
|
156
|
+
* dependence on `shared`"). See PLAN-OPTIONS §3.5.
|
|
157
|
+
*
|
|
158
|
+
* @param {Record<string, any>} consumerProfile may include `options: {...}`
|
|
159
|
+
* @param {Record<string, any>} packageProfile may include `options: {...}`
|
|
160
|
+
* @param {object|null|undefined} rawCompat compatibility block from the package manifest
|
|
161
|
+
* @param {string|null|undefined} [packageKind] package manifest `kind` field — unlocks `os`/`arch` modes for HeaderOnlyLib
|
|
143
162
|
* @returns {{
|
|
144
163
|
* compatible: boolean,
|
|
145
164
|
* reasons: Array<{ field: string, mode: string, consumer: any, package: any, outcome: string }>,
|
|
146
165
|
* }}
|
|
147
166
|
*/
|
|
148
|
-
function evaluateCompat(consumerProfile, packageProfile, rawCompat) {
|
|
149
|
-
const compat = normalizeCompat(rawCompat);
|
|
167
|
+
function evaluateCompat(consumerProfile, packageProfile, rawCompat, packageKind) {
|
|
168
|
+
const compat = normalizeCompat(rawCompat, packageKind);
|
|
150
169
|
const reasons = [];
|
|
151
170
|
let compatible = true;
|
|
152
171
|
|
|
153
172
|
for (const [field, mode] of Object.entries(compat)) {
|
|
173
|
+
// `options.<name>` keys in the compat block are handled below,
|
|
174
|
+
// together with any options present on either profile.
|
|
175
|
+
if (field.startsWith('options.')) continue;
|
|
176
|
+
|
|
154
177
|
const cVal = consumerProfile[field];
|
|
155
178
|
const pVal = packageProfile[field];
|
|
156
179
|
const outcome = evaluateField(mode, cVal, pVal);
|
|
@@ -158,6 +181,24 @@ function evaluateCompat(consumerProfile, packageProfile, rawCompat) {
|
|
|
158
181
|
if (outcome === 'incompatible') compatible = false;
|
|
159
182
|
}
|
|
160
183
|
|
|
184
|
+
// Evaluate every option the consumer or package declares. Default mode
|
|
185
|
+
// is `exact`; the compat block may relax to `any` (and the other modes
|
|
186
|
+
// work when the values are numeric strings). Forward-compat: an option
|
|
187
|
+
// present on one side only is treated as `incompatible` under exact —
|
|
188
|
+
// the recipe shapes differ.
|
|
189
|
+
const consumerOpts = consumerProfile.options ?? {};
|
|
190
|
+
const packageOpts = packageProfile.options ?? {};
|
|
191
|
+
const optionKeys = new Set([...Object.keys(consumerOpts), ...Object.keys(packageOpts)]);
|
|
192
|
+
for (const name of optionKeys) {
|
|
193
|
+
const field = `options.${name}`;
|
|
194
|
+
const mode = compat[field] ?? 'exact';
|
|
195
|
+
const cVal = consumerOpts[name];
|
|
196
|
+
const pVal = packageOpts[name];
|
|
197
|
+
const outcome = evaluateField(mode, cVal, pVal);
|
|
198
|
+
reasons.push({ field, mode, consumer: cVal, package: pVal, outcome });
|
|
199
|
+
if (outcome === 'incompatible') compatible = false;
|
|
200
|
+
}
|
|
201
|
+
|
|
161
202
|
return { compatible, reasons };
|
|
162
203
|
}
|
|
163
204
|
|