wyvrnpm 2.0.2 → 2.1.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 +382 -4
- package/bin/wyvrn.js +96 -3
- package/cmake/cpp.cmake +9 -0
- package/cmake/functions.cmake +224 -0
- package/cmake/macros.cmake +233 -0
- package/cmake/options.cmake +23 -0
- package/cmake/variables.cmake +171 -0
- package/package.json +2 -1
- package/src/build/cache.js +101 -0
- package/src/build/clone.js +170 -0
- package/src/build/cmake.js +342 -0
- package/src/build/index.js +244 -0
- package/src/build/msvc-env.js +217 -0
- package/src/build/recipe.js +129 -0
- package/src/commands/build.js +242 -0
- package/src/commands/clean.js +25 -9
- package/src/commands/configure.js +6 -5
- package/src/commands/init.js +3 -2
- package/src/commands/install.js +61 -14
- package/src/commands/link.js +18 -15
- package/src/commands/profile.js +15 -12
- package/src/commands/publish.js +67 -31
- package/src/compat.js +232 -0
- package/src/config.js +3 -1
- package/src/download.js +369 -87
- package/src/logger.js +78 -0
- package/src/profile.js +28 -0
- package/src/providers/file.js +4 -3
- package/src/providers/http.js +3 -2
- package/src/providers/s3.js +3 -2
- package/src/resolve.js +33 -7
- package/src/toolchain/deps.js +164 -0
- package/src/toolchain/index.js +141 -0
- package/src/toolchain/presets.js +191 -0
- package/src/toolchain/template.cmake +66 -0
package/src/commands/profile.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
mergeProfile,
|
|
13
13
|
} = require('../profile');
|
|
14
14
|
const { readConfig, writeConfig } = require('../config');
|
|
15
|
+
const log = require('../logger');
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// list
|
|
@@ -27,8 +28,9 @@ function list() {
|
|
|
27
28
|
const dir = getProfilesDir();
|
|
28
29
|
const defName = config.defaultProfile ?? 'default';
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
log.info(`Profiles directory: ${dir}`);
|
|
32
|
+
log.info(`Default profile : ${defName}`);
|
|
33
|
+
console.log('');
|
|
32
34
|
|
|
33
35
|
if (names.length === 0) {
|
|
34
36
|
console.log(' (no profiles saved — run `wyvrnpm configure profile detect` to create one)');
|
|
@@ -60,12 +62,12 @@ function show(argv) {
|
|
|
60
62
|
const stored = readProfile(name);
|
|
61
63
|
|
|
62
64
|
if (stored) {
|
|
63
|
-
|
|
65
|
+
log.info(`Profile "${name}":`);
|
|
64
66
|
console.log(formatProfile(stored));
|
|
65
67
|
console.log(`\nProfile hash : ${hashProfile(stored)}`);
|
|
66
68
|
} else {
|
|
67
69
|
const detected = detectProfile();
|
|
68
|
-
|
|
70
|
+
log.info(`Profile "${name}" not found — showing auto-detected environment:`);
|
|
69
71
|
console.log(formatProfile(detected));
|
|
70
72
|
console.log(`\nProfile hash : ${hashProfile(detected)}`);
|
|
71
73
|
console.log(`\nRun \`wyvrnpm configure profile detect --name ${name}\` to save this.`);
|
|
@@ -85,13 +87,14 @@ function detect(argv) {
|
|
|
85
87
|
const name = argv.name ?? config.defaultProfile ?? 'default';
|
|
86
88
|
const p = detectProfile();
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
log.info('Detected profile:');
|
|
89
91
|
console.log(formatProfile(p));
|
|
90
92
|
console.log(`\nProfile hash : ${hashProfile(p)}`);
|
|
91
93
|
|
|
92
94
|
if (!argv.dryRun) {
|
|
93
95
|
writeProfile(name, p);
|
|
94
|
-
console.log(
|
|
96
|
+
console.log('');
|
|
97
|
+
log.success(`Saved as profile "${name}".`);
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -120,7 +123,7 @@ function set(argv) {
|
|
|
120
123
|
|
|
121
124
|
writeProfile(name, updated);
|
|
122
125
|
|
|
123
|
-
|
|
126
|
+
log.info(`Profile "${name}" updated:`);
|
|
124
127
|
console.log(formatProfile(updated));
|
|
125
128
|
console.log(`\nProfile hash : ${hashProfile(updated)}`);
|
|
126
129
|
}
|
|
@@ -136,13 +139,13 @@ function set(argv) {
|
|
|
136
139
|
function setDefault(argv) {
|
|
137
140
|
const name = argv.name;
|
|
138
141
|
if (!readProfile(name)) {
|
|
139
|
-
|
|
142
|
+
log.error(`profile "${name}" does not exist. Create it first.`);
|
|
140
143
|
process.exit(1);
|
|
141
144
|
}
|
|
142
145
|
const config = readConfig();
|
|
143
146
|
config.defaultProfile = name;
|
|
144
147
|
writeConfig(config);
|
|
145
|
-
|
|
148
|
+
log.info(`Default profile set to "${name}".`);
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
// ---------------------------------------------------------------------------
|
|
@@ -156,14 +159,14 @@ function setDefault(argv) {
|
|
|
156
159
|
function del(argv) {
|
|
157
160
|
const name = argv.name;
|
|
158
161
|
if (name === 'default') {
|
|
159
|
-
|
|
162
|
+
log.error('cannot delete the "default" profile.');
|
|
160
163
|
process.exit(1);
|
|
161
164
|
}
|
|
162
165
|
const removed = deleteProfile(name);
|
|
163
166
|
if (removed) {
|
|
164
|
-
|
|
167
|
+
log.info(`Profile "${name}" deleted.`);
|
|
165
168
|
} else {
|
|
166
|
-
|
|
169
|
+
log.error(`profile "${name}" not found.`);
|
|
167
170
|
process.exit(1);
|
|
168
171
|
}
|
|
169
172
|
}
|
package/src/commands/publish.js
CHANGED
|
@@ -8,7 +8,9 @@ const AdmZip = require('adm-zip');
|
|
|
8
8
|
const { readManifest } = require('../manifest');
|
|
9
9
|
const { getProvider } = require('../providers');
|
|
10
10
|
const { readConfig } = require('../config');
|
|
11
|
-
const { loadProfile, hashProfile, sha256Of, getGitSha, getGitRepo } = require('../profile');
|
|
11
|
+
const { loadProfile, hashProfile, sha256Of, getGitSha, getGitRepo, isShaOnRemote } = require('../profile');
|
|
12
|
+
const { defaultCompatBlock } = require('../compat');
|
|
13
|
+
const log = require('../logger');
|
|
12
14
|
|
|
13
15
|
// ---------------------------------------------------------------------------
|
|
14
16
|
// .wyvrnignore helpers (unchanged from v1)
|
|
@@ -40,7 +42,7 @@ function addFolderFiltered(zip, dir, base, ignoreRe) {
|
|
|
40
42
|
const fullPath = path.join(dir, entry);
|
|
41
43
|
const relPath = path.relative(base, fullPath).replace(/\\/g, '/');
|
|
42
44
|
if (ignoreRe.some((re) => re.test(relPath))) {
|
|
43
|
-
|
|
45
|
+
log.info(` Ignoring: ${relPath}`);
|
|
44
46
|
continue;
|
|
45
47
|
}
|
|
46
48
|
const stat = fs.statSync(fullPath);
|
|
@@ -64,21 +66,21 @@ function resolveSource(argv) {
|
|
|
64
66
|
if (source) {
|
|
65
67
|
const named = config.publishSources.find((s) => s.name === source);
|
|
66
68
|
if (named) {
|
|
67
|
-
|
|
69
|
+
log.info(`Using publish source "${named.name}" from config`);
|
|
68
70
|
source = named.url;
|
|
69
71
|
awsProfile = awsProfile ?? named.profile;
|
|
70
72
|
token = token ?? named.token;
|
|
71
73
|
}
|
|
72
74
|
} else {
|
|
73
75
|
if (config.publishSources.length === 0) {
|
|
74
|
-
|
|
75
|
-
'
|
|
76
|
+
log.error(
|
|
77
|
+
'--source is required (or configure one with: ' +
|
|
76
78
|
'wyvrnpm configure add-source --kind publish ...)',
|
|
77
79
|
);
|
|
78
80
|
process.exit(1);
|
|
79
81
|
}
|
|
80
82
|
const first = config.publishSources[0];
|
|
81
|
-
|
|
83
|
+
log.info(`Using publish source "${first.name}" from config`);
|
|
82
84
|
source = first.url;
|
|
83
85
|
awsProfile = awsProfile ?? first.profile;
|
|
84
86
|
token = token ?? first.token;
|
|
@@ -104,19 +106,19 @@ async function publish(argv) {
|
|
|
104
106
|
const manifestPath = path.resolve(manifestArg || './wyvrn.json');
|
|
105
107
|
const manifest = await readManifest(manifestPath);
|
|
106
108
|
if (!manifest) {
|
|
107
|
-
|
|
109
|
+
log.error(`Could not read manifest at ${manifestPath}`);
|
|
108
110
|
process.exit(1);
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
const { name, version } = manifest;
|
|
112
114
|
if (!name || !version) {
|
|
113
|
-
|
|
115
|
+
log.error('Manifest must have "name" and "version" fields');
|
|
114
116
|
process.exit(1);
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
const srcDir = path.resolve(publishPath || '.');
|
|
118
120
|
if (!fs.existsSync(srcDir)) {
|
|
119
|
-
|
|
121
|
+
log.error(`Publish path does not exist: ${srcDir}`);
|
|
120
122
|
process.exit(1);
|
|
121
123
|
}
|
|
122
124
|
|
|
@@ -125,7 +127,7 @@ async function publish(argv) {
|
|
|
125
127
|
try {
|
|
126
128
|
provider = getProvider(source);
|
|
127
129
|
} catch (err) {
|
|
128
|
-
|
|
130
|
+
log.error(err.message);
|
|
129
131
|
process.exit(1);
|
|
130
132
|
}
|
|
131
133
|
|
|
@@ -135,25 +137,25 @@ async function publish(argv) {
|
|
|
135
137
|
const { profile: buildProfile, fromFile } = loadProfile(activeProfileName);
|
|
136
138
|
const profileHash = hashProfile(buildProfile);
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
`
|
|
140
|
-
`
|
|
141
|
-
`
|
|
140
|
+
log.info(
|
|
141
|
+
`Publishing ${name}@${version}\n` +
|
|
142
|
+
`Profile : "${activeProfileName}"${fromFile ? '' : ' (auto-detected — save with `wyvrnpm configure profile detect`)'}\n` +
|
|
143
|
+
` ${buildProfile.os}/${buildProfile.arch} ` +
|
|
142
144
|
`${buildProfile.compiler} ${buildProfile['compiler.version']} ` +
|
|
143
145
|
`C++${buildProfile['compiler.cppstd']}` +
|
|
144
146
|
(buildProfile['compiler.runtime'] ? ` [${buildProfile['compiler.runtime']}]` : '') + '\n' +
|
|
145
|
-
`
|
|
146
|
-
`
|
|
147
|
+
`Profile hash: ${profileHash}\n` +
|
|
148
|
+
`Provider : ${provider.constructor.providerName}`,
|
|
147
149
|
);
|
|
148
150
|
|
|
149
151
|
// ── Check v2 existence ────────────────────────────────────────────────────
|
|
150
152
|
const v2AlreadyExists = await provider.v2Exists({ source, name, version, profileHash, awsProfile, token });
|
|
151
153
|
if (v2AlreadyExists) {
|
|
152
154
|
if (force) {
|
|
153
|
-
|
|
155
|
+
log.warn(`${name}@${version} [${profileHash}] already exists — overwriting (--force)`);
|
|
154
156
|
} else {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
log.error(
|
|
158
|
+
`${name}@${version} [${profileHash}] already exists.\n` +
|
|
157
159
|
' Bump the version, use a different profile, or pass --force to overwrite.',
|
|
158
160
|
);
|
|
159
161
|
process.exit(1);
|
|
@@ -170,43 +172,76 @@ async function publish(argv) {
|
|
|
170
172
|
const ver = typeof entry === 'string' ? entry : entry.version;
|
|
171
173
|
if (ver) lockedDependencies[pkgName] = ver;
|
|
172
174
|
}
|
|
173
|
-
|
|
175
|
+
log.info(`Lock file : ${Object.keys(lockedDependencies).length} pinned package(s)`);
|
|
174
176
|
} catch {
|
|
175
|
-
|
|
177
|
+
log.warn('could not read wyvrn.lock — dependency versions will be published as-is from wyvrn.json');
|
|
176
178
|
}
|
|
177
179
|
} else {
|
|
178
|
-
|
|
180
|
+
log.warn('no wyvrn.lock found — run `wyvrnpm install` before publishing to pin dependency versions');
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
// ── Git metadata ──────────────────────────────────────────────────────────
|
|
182
184
|
const gitSha = getGitSha(srcDir);
|
|
183
185
|
const gitRepo = getGitRepo(srcDir);
|
|
184
|
-
if (gitSha)
|
|
185
|
-
if (gitRepo)
|
|
186
|
+
if (gitSha) log.info(`Git commit : ${gitSha}`);
|
|
187
|
+
if (gitRepo) log.info(`Git remote : ${gitRepo}`);
|
|
188
|
+
|
|
189
|
+
// Warn (but don't block) if HEAD isn't reachable from origin. Downstream
|
|
190
|
+
// `--build=missing` consumers will fail to fetch this commit — the fix
|
|
191
|
+
// is almost always `git push` before publishing.
|
|
192
|
+
if (gitSha && gitRepo && !isShaOnRemote(gitSha, srcDir)) {
|
|
193
|
+
log.warn(
|
|
194
|
+
`commit ${gitSha.slice(0, 12)} is not reachable from any remote ` +
|
|
195
|
+
`branch or tag on origin.\n` +
|
|
196
|
+
` Consumers building this package from source will fail to retrieve it.\n` +
|
|
197
|
+
` Fix: push your branch (or tag the commit) before publishing.`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
186
200
|
|
|
187
201
|
// ── .wyvrnignore ──────────────────────────────────────────────────────────
|
|
188
202
|
const ignoreFile = path.join(srcDir, '.wyvrnignore');
|
|
189
203
|
const ignorePatterns = loadIgnorePatterns(ignoreFile);
|
|
190
204
|
const ignoreRe = ignorePatterns.map(patternToRegex);
|
|
191
205
|
if (ignorePatterns.length > 0) {
|
|
192
|
-
|
|
206
|
+
log.info(`Loaded ${ignorePatterns.length} pattern(s) from .wyvrnignore`);
|
|
193
207
|
}
|
|
194
208
|
|
|
195
209
|
// ── Build zip ─────────────────────────────────────────────────────────────
|
|
196
210
|
const tmpZipPath = path.join(os.tmpdir(), `wyvrnpm-${name}-${version}-${Date.now()}.zip`);
|
|
211
|
+
|
|
212
|
+
// ── Inject default `compatibility` block if the source manifest lacks one ──
|
|
213
|
+
// Written to a temp file so the source wyvrn.json is never modified.
|
|
214
|
+
// A published manifest must always carry a compat block so install-time
|
|
215
|
+
// resolution has something to evaluate (default = all fields `exact`,
|
|
216
|
+
// identical semantics to pre-phase-2 strict behaviour).
|
|
217
|
+
const tmpManifestPath = path.join(
|
|
218
|
+
os.tmpdir(),
|
|
219
|
+
`wyvrnpm-manifest-${name}-${version}-${Date.now()}.json`,
|
|
220
|
+
);
|
|
221
|
+
const manifestForPublish = {
|
|
222
|
+
...manifest,
|
|
223
|
+
compatibility: manifest.compatibility ?? defaultCompatBlock(),
|
|
224
|
+
};
|
|
225
|
+
fs.writeFileSync(tmpManifestPath, JSON.stringify(manifestForPublish, null, 2), 'utf8');
|
|
226
|
+
if (!manifest.compatibility) {
|
|
227
|
+
log.info('Injecting default `compatibility` block (all fields `exact`) — override by adding one to wyvrn.json');
|
|
228
|
+
} else {
|
|
229
|
+
log.info(`Using package-declared compatibility: ${JSON.stringify(manifest.compatibility)}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
197
232
|
try {
|
|
198
|
-
|
|
233
|
+
log.info(`Zipping ${srcDir} ...`);
|
|
199
234
|
const zip = new AdmZip();
|
|
200
235
|
addFolderFiltered(zip, srcDir, srcDir, ignoreRe);
|
|
201
236
|
zip.writeZip(tmpZipPath);
|
|
202
237
|
|
|
203
238
|
const contentSha256 = sha256Of(tmpZipPath);
|
|
204
|
-
|
|
239
|
+
log.info(`Content SHA256 : ${contentSha256}`);
|
|
205
240
|
|
|
206
241
|
// ── v2 publish ────────────────────────────────────────────────────────
|
|
207
|
-
|
|
242
|
+
log.info('Publishing (v2) ...');
|
|
208
243
|
await provider.v2Publish(
|
|
209
|
-
{ manifest:
|
|
244
|
+
{ manifest: tmpManifestPath, zip: tmpZipPath },
|
|
210
245
|
{
|
|
211
246
|
source,
|
|
212
247
|
name,
|
|
@@ -223,9 +258,10 @@ async function publish(argv) {
|
|
|
223
258
|
},
|
|
224
259
|
);
|
|
225
260
|
|
|
226
|
-
|
|
261
|
+
log.success(`Successfully published ${name}@${version} [${profileHash}] to ${source}`);
|
|
227
262
|
} finally {
|
|
228
|
-
if (fs.existsSync(tmpZipPath))
|
|
263
|
+
if (fs.existsSync(tmpZipPath)) fs.unlinkSync(tmpZipPath);
|
|
264
|
+
if (fs.existsSync(tmpManifestPath)) fs.unlinkSync(tmpManifestPath);
|
|
229
265
|
}
|
|
230
266
|
}
|
|
231
267
|
|
package/src/compat.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Declarative compatibility evaluator for v2 build profiles.
|
|
5
|
+
*
|
|
6
|
+
* Publishers declare a `compatibility` block in their package's `wyvrn.json`
|
|
7
|
+
* stating which profile fields must match exactly vs which may differ.
|
|
8
|
+
* At install time, when no exact profileHash match exists, this module
|
|
9
|
+
* evaluates each registry-side build against the consumer's profile and
|
|
10
|
+
* picks a compatible one if any.
|
|
11
|
+
*
|
|
12
|
+
* The block format is per-field: either a string shorthand or an object.
|
|
13
|
+
*
|
|
14
|
+
* "compatibility": {
|
|
15
|
+
* "compiler.cppstd": "lte",
|
|
16
|
+
* "compiler.runtime": "exact",
|
|
17
|
+
* "compiler.version": "exact-or-newer"
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* equivalent object form (allows future extensions):
|
|
21
|
+
*
|
|
22
|
+
* "compatibility": {
|
|
23
|
+
* "compiler.cppstd": { "mode": "lte" }
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Modes:
|
|
27
|
+
* exact | Package and consumer values must be equal.
|
|
28
|
+
* any | Field is ignored.
|
|
29
|
+
* lte | Package value ≤ consumer value (consumer is at least as
|
|
30
|
+
* | new/high — e.g. cppstd=20 package usable by cppstd=23).
|
|
31
|
+
* gte | Package value ≥ consumer value (symmetry; rarely useful).
|
|
32
|
+
* exact-or-newer | Semantically identical to `lte` at MVP — kept as a
|
|
33
|
+
* | distinct name because it reads naturally for compiler
|
|
34
|
+
* | version fields and leaves room for future "same major"
|
|
35
|
+
* | semantics without renaming.
|
|
36
|
+
*
|
|
37
|
+
* Missing fields default to `exact`. `os` and `arch` are ALWAYS evaluated
|
|
38
|
+
* as `exact` regardless of what the block says — cross-arch/cross-OS
|
|
39
|
+
* binaries are never ABI-compatible.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const OS_ARCH_ALWAYS_EXACT = new Set(['os', 'arch']);
|
|
43
|
+
|
|
44
|
+
const DEFAULT_COMPAT_FIELDS = [
|
|
45
|
+
'os',
|
|
46
|
+
'arch',
|
|
47
|
+
'compiler',
|
|
48
|
+
'compiler.version',
|
|
49
|
+
'compiler.cppstd',
|
|
50
|
+
'compiler.runtime',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const VALID_MODES = new Set(['exact', 'any', 'lte', 'gte', 'exact-or-newer']);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalise a raw compatibility block into `{ field → mode }`.
|
|
57
|
+
* Missing fields fall back to `exact`. Unknown modes fall back to `exact`.
|
|
58
|
+
* `os` and `arch` are forced to `exact`.
|
|
59
|
+
*
|
|
60
|
+
* @param {object|null|undefined} rawCompat
|
|
61
|
+
* @returns {Record<string, string>}
|
|
62
|
+
*/
|
|
63
|
+
function normalizeCompat(rawCompat) {
|
|
64
|
+
const result = {};
|
|
65
|
+
for (const field of DEFAULT_COMPAT_FIELDS) result[field] = 'exact';
|
|
66
|
+
|
|
67
|
+
if (!rawCompat || typeof rawCompat !== 'object') return result;
|
|
68
|
+
|
|
69
|
+
for (const [field, spec] of Object.entries(rawCompat)) {
|
|
70
|
+
if (OS_ARCH_ALWAYS_EXACT.has(field)) continue;
|
|
71
|
+
const mode = typeof spec === 'string' ? spec : (spec && spec.mode);
|
|
72
|
+
if (mode && VALID_MODES.has(mode)) result[field] = mode;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Return the default compatibility block — all fields `exact`.
|
|
79
|
+
* Used by `publish` when the source manifest lacks a block.
|
|
80
|
+
* @returns {Record<string, string>}
|
|
81
|
+
*/
|
|
82
|
+
function defaultCompatBlock() {
|
|
83
|
+
const result = {};
|
|
84
|
+
for (const field of DEFAULT_COMPAT_FIELDS) result[field] = 'exact';
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Evaluate a single field under a given mode.
|
|
90
|
+
*
|
|
91
|
+
* @returns {'match'|'compat-lte'|'compat-gte'|'compat-newer'|'compat-any'|'incompatible'}
|
|
92
|
+
*/
|
|
93
|
+
function evaluateField(mode, consumerVal, packageVal) {
|
|
94
|
+
switch (mode) {
|
|
95
|
+
case 'any':
|
|
96
|
+
return 'compat-any';
|
|
97
|
+
|
|
98
|
+
case 'exact':
|
|
99
|
+
return consumerVal === packageVal ? 'match' : 'incompatible';
|
|
100
|
+
|
|
101
|
+
case 'lte': {
|
|
102
|
+
if (consumerVal === packageVal) return 'match';
|
|
103
|
+
const c = Number(consumerVal);
|
|
104
|
+
const p = Number(packageVal);
|
|
105
|
+
if (Number.isFinite(c) && Number.isFinite(p)) {
|
|
106
|
+
return p <= c ? 'compat-lte' : 'incompatible';
|
|
107
|
+
}
|
|
108
|
+
return 'incompatible';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
case 'gte': {
|
|
112
|
+
if (consumerVal === packageVal) return 'match';
|
|
113
|
+
const c = Number(consumerVal);
|
|
114
|
+
const p = Number(packageVal);
|
|
115
|
+
if (Number.isFinite(c) && Number.isFinite(p)) {
|
|
116
|
+
return p >= c ? 'compat-gte' : 'incompatible';
|
|
117
|
+
}
|
|
118
|
+
return 'incompatible';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case 'exact-or-newer': {
|
|
122
|
+
if (consumerVal === packageVal) return 'match';
|
|
123
|
+
const c = Number(consumerVal);
|
|
124
|
+
const p = Number(packageVal);
|
|
125
|
+
if (Number.isFinite(c) && Number.isFinite(p)) {
|
|
126
|
+
return c >= p ? 'compat-newer' : 'incompatible';
|
|
127
|
+
}
|
|
128
|
+
return 'incompatible';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
default:
|
|
132
|
+
// Unknown mode → fail safe
|
|
133
|
+
return 'incompatible';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Evaluate a package's compatibility block against a consumer's profile.
|
|
139
|
+
*
|
|
140
|
+
* @param {Record<string, string|null>} consumerProfile
|
|
141
|
+
* @param {Record<string, string|null>} packageProfile - what the package was built with
|
|
142
|
+
* @param {object|null|undefined} rawCompat - compatibility block from the package manifest
|
|
143
|
+
* @returns {{
|
|
144
|
+
* compatible: boolean,
|
|
145
|
+
* reasons: Array<{ field: string, mode: string, consumer: any, package: any, outcome: string }>,
|
|
146
|
+
* }}
|
|
147
|
+
*/
|
|
148
|
+
function evaluateCompat(consumerProfile, packageProfile, rawCompat) {
|
|
149
|
+
const compat = normalizeCompat(rawCompat);
|
|
150
|
+
const reasons = [];
|
|
151
|
+
let compatible = true;
|
|
152
|
+
|
|
153
|
+
for (const [field, mode] of Object.entries(compat)) {
|
|
154
|
+
const cVal = consumerProfile[field];
|
|
155
|
+
const pVal = packageProfile[field];
|
|
156
|
+
const outcome = evaluateField(mode, cVal, pVal);
|
|
157
|
+
reasons.push({ field, mode, consumer: cVal, package: pVal, outcome });
|
|
158
|
+
if (outcome === 'incompatible') compatible = false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { compatible, reasons };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Deterministic tiebreaker when multiple compatible candidates exist.
|
|
166
|
+
*
|
|
167
|
+
* 1. Prefer candidates whose `compiler` matches the consumer's.
|
|
168
|
+
* 2. Prefer the highest numeric `compiler.version`.
|
|
169
|
+
* 3. Prefer candidates whose `compiler.runtime` matches the consumer's.
|
|
170
|
+
* 4. Prefer the lexicographically earliest profile hash (stable fallback).
|
|
171
|
+
*
|
|
172
|
+
* @param {Array<{ profileHash: string, packageProfile: Record<string, any>, reasons: Array<any> }>} candidates
|
|
173
|
+
* @param {Record<string, string|null>} consumerProfile
|
|
174
|
+
* @returns {object|null} The preferred candidate (or null if list is empty).
|
|
175
|
+
*/
|
|
176
|
+
function pickBestCandidate(candidates, consumerProfile) {
|
|
177
|
+
if (!candidates || candidates.length === 0) return null;
|
|
178
|
+
if (candidates.length === 1) return candidates[0];
|
|
179
|
+
|
|
180
|
+
const sorted = candidates.slice().sort((a, b) => {
|
|
181
|
+
const compilerA = a.packageProfile.compiler === consumerProfile.compiler ? 0 : 1;
|
|
182
|
+
const compilerB = b.packageProfile.compiler === consumerProfile.compiler ? 0 : 1;
|
|
183
|
+
if (compilerA !== compilerB) return compilerA - compilerB;
|
|
184
|
+
|
|
185
|
+
const verA = Number(a.packageProfile['compiler.version']) || 0;
|
|
186
|
+
const verB = Number(b.packageProfile['compiler.version']) || 0;
|
|
187
|
+
if (verA !== verB) return verB - verA; // descending
|
|
188
|
+
|
|
189
|
+
const runtimeA = a.packageProfile['compiler.runtime'] === consumerProfile['compiler.runtime'] ? 0 : 1;
|
|
190
|
+
const runtimeB = b.packageProfile['compiler.runtime'] === consumerProfile['compiler.runtime'] ? 0 : 1;
|
|
191
|
+
if (runtimeA !== runtimeB) return runtimeA - runtimeB;
|
|
192
|
+
|
|
193
|
+
return a.profileHash < b.profileHash ? -1 : 1;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return sorted[0];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Render a reasons array into a compact human-readable summary — used for
|
|
201
|
+
* the "chose candidate X because …" log line at install time.
|
|
202
|
+
*
|
|
203
|
+
* @param {Array<{ field, mode, consumer, package, outcome }>} reasons
|
|
204
|
+
* @returns {string}
|
|
205
|
+
*/
|
|
206
|
+
function summarizeReasons(reasons) {
|
|
207
|
+
return reasons
|
|
208
|
+
.filter((r) => r.outcome !== 'compat-any' || r.mode !== 'any') // skip trivial any
|
|
209
|
+
.map((r) => {
|
|
210
|
+
switch (r.outcome) {
|
|
211
|
+
case 'match': return `${r.field}=${r.consumer}`;
|
|
212
|
+
case 'compat-lte': return `${r.field} ${r.package}→${r.consumer} (lte)`;
|
|
213
|
+
case 'compat-gte': return `${r.field} ${r.package}←${r.consumer} (gte)`;
|
|
214
|
+
case 'compat-newer': return `${r.field} ${r.package}→${r.consumer} (newer)`;
|
|
215
|
+
case 'compat-any': return `${r.field}=* (any)`;
|
|
216
|
+
case 'incompatible': return `${r.field} INCOMPAT (need ${r.consumer}, have ${r.package})`;
|
|
217
|
+
default: return `${r.field} ${r.outcome}`;
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
.join(', ');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
normalizeCompat,
|
|
225
|
+
defaultCompatBlock,
|
|
226
|
+
evaluateCompat,
|
|
227
|
+
evaluateField,
|
|
228
|
+
pickBestCandidate,
|
|
229
|
+
summarizeReasons,
|
|
230
|
+
DEFAULT_COMPAT_FIELDS,
|
|
231
|
+
VALID_MODES,
|
|
232
|
+
};
|
package/src/config.js
CHANGED
|
@@ -4,6 +4,8 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
|
+
const log = require('./logger');
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Returns the directory where wyvrnpm stores its config.
|
|
9
11
|
* Windows : %LOCALAPPDATA%\wyvrnpm
|
|
@@ -41,7 +43,7 @@ function readConfig() {
|
|
|
41
43
|
defaultProfile: raw.defaultProfile ?? 'default',
|
|
42
44
|
};
|
|
43
45
|
} catch {
|
|
44
|
-
|
|
46
|
+
log.warn('could not parse config.json — using defaults');
|
|
45
47
|
return { installSources: [], publishSources: [], linkedPackages: {}, defaultProfile: 'default' };
|
|
46
48
|
}
|
|
47
49
|
}
|