wyvrnpm 2.0.4 → 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 +639 -5
- package/bin/wyvrn.js +220 -10
- package/claude/skills/wyvrnpm.skill +0 -0
- 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 +3 -1
- package/src/build/cache.js +148 -0
- package/src/build/clone.js +170 -0
- package/src/build/cmake.js +342 -0
- package/src/build/index.js +275 -0
- package/src/build/msvc-env.js +217 -0
- package/src/build/recipe.js +155 -0
- package/src/commands/build.js +283 -0
- package/src/commands/clean.js +56 -16
- package/src/commands/configure.js +6 -5
- package/src/commands/init.js +3 -2
- package/src/commands/install-skill.js +107 -0
- package/src/commands/install.js +262 -19
- package/src/commands/link.js +18 -15
- package/src/commands/profile.js +15 -12
- package/src/commands/publish.js +216 -65
- package/src/commands/show.js +237 -0
- package/src/compat.js +261 -0
- package/src/config.js +3 -1
- package/src/download.js +431 -87
- package/src/ignore.js +118 -0
- package/src/logger.js +94 -0
- package/src/manifest.js +12 -7
- package/src/options.js +303 -0
- package/src/profile.js +56 -4
- package/src/providers/base.js +16 -1
- package/src/providers/file.js +12 -6
- package/src/providers/http.js +15 -10
- package/src/providers/s3.js +14 -9
- package/src/resolve.js +179 -19
- package/src/toolchain/deps.js +164 -0
- package/src/toolchain/index.js +141 -0
- package/src/toolchain/presets.js +263 -0
- package/src/toolchain/template.cmake +66 -0
- package/src/upload-built.js +256 -0
- package/src/version-range.js +301 -0
package/src/compat.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
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
|
+
* 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
|
|
150
|
+
* @returns {{
|
|
151
|
+
* compatible: boolean,
|
|
152
|
+
* reasons: Array<{ field: string, mode: string, consumer: any, package: any, outcome: string }>,
|
|
153
|
+
* }}
|
|
154
|
+
*/
|
|
155
|
+
function evaluateCompat(consumerProfile, packageProfile, rawCompat) {
|
|
156
|
+
const compat = normalizeCompat(rawCompat);
|
|
157
|
+
const reasons = [];
|
|
158
|
+
let compatible = true;
|
|
159
|
+
|
|
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
|
+
|
|
165
|
+
const cVal = consumerProfile[field];
|
|
166
|
+
const pVal = packageProfile[field];
|
|
167
|
+
const outcome = evaluateField(mode, cVal, pVal);
|
|
168
|
+
reasons.push({ field, mode, consumer: cVal, package: pVal, outcome });
|
|
169
|
+
if (outcome === 'incompatible') compatible = false;
|
|
170
|
+
}
|
|
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
|
+
|
|
190
|
+
return { compatible, reasons };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Deterministic tiebreaker when multiple compatible candidates exist.
|
|
195
|
+
*
|
|
196
|
+
* 1. Prefer candidates whose `compiler` matches the consumer's.
|
|
197
|
+
* 2. Prefer the highest numeric `compiler.version`.
|
|
198
|
+
* 3. Prefer candidates whose `compiler.runtime` matches the consumer's.
|
|
199
|
+
* 4. Prefer the lexicographically earliest profile hash (stable fallback).
|
|
200
|
+
*
|
|
201
|
+
* @param {Array<{ profileHash: string, packageProfile: Record<string, any>, reasons: Array<any> }>} candidates
|
|
202
|
+
* @param {Record<string, string|null>} consumerProfile
|
|
203
|
+
* @returns {object|null} The preferred candidate (or null if list is empty).
|
|
204
|
+
*/
|
|
205
|
+
function pickBestCandidate(candidates, consumerProfile) {
|
|
206
|
+
if (!candidates || candidates.length === 0) return null;
|
|
207
|
+
if (candidates.length === 1) return candidates[0];
|
|
208
|
+
|
|
209
|
+
const sorted = candidates.slice().sort((a, b) => {
|
|
210
|
+
const compilerA = a.packageProfile.compiler === consumerProfile.compiler ? 0 : 1;
|
|
211
|
+
const compilerB = b.packageProfile.compiler === consumerProfile.compiler ? 0 : 1;
|
|
212
|
+
if (compilerA !== compilerB) return compilerA - compilerB;
|
|
213
|
+
|
|
214
|
+
const verA = Number(a.packageProfile['compiler.version']) || 0;
|
|
215
|
+
const verB = Number(b.packageProfile['compiler.version']) || 0;
|
|
216
|
+
if (verA !== verB) return verB - verA; // descending
|
|
217
|
+
|
|
218
|
+
const runtimeA = a.packageProfile['compiler.runtime'] === consumerProfile['compiler.runtime'] ? 0 : 1;
|
|
219
|
+
const runtimeB = b.packageProfile['compiler.runtime'] === consumerProfile['compiler.runtime'] ? 0 : 1;
|
|
220
|
+
if (runtimeA !== runtimeB) return runtimeA - runtimeB;
|
|
221
|
+
|
|
222
|
+
return a.profileHash < b.profileHash ? -1 : 1;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return sorted[0];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Render a reasons array into a compact human-readable summary — used for
|
|
230
|
+
* the "chose candidate X because …" log line at install time.
|
|
231
|
+
*
|
|
232
|
+
* @param {Array<{ field, mode, consumer, package, outcome }>} reasons
|
|
233
|
+
* @returns {string}
|
|
234
|
+
*/
|
|
235
|
+
function summarizeReasons(reasons) {
|
|
236
|
+
return reasons
|
|
237
|
+
.filter((r) => r.outcome !== 'compat-any' || r.mode !== 'any') // skip trivial any
|
|
238
|
+
.map((r) => {
|
|
239
|
+
switch (r.outcome) {
|
|
240
|
+
case 'match': return `${r.field}=${r.consumer}`;
|
|
241
|
+
case 'compat-lte': return `${r.field} ${r.package}→${r.consumer} (lte)`;
|
|
242
|
+
case 'compat-gte': return `${r.field} ${r.package}←${r.consumer} (gte)`;
|
|
243
|
+
case 'compat-newer': return `${r.field} ${r.package}→${r.consumer} (newer)`;
|
|
244
|
+
case 'compat-any': return `${r.field}=* (any)`;
|
|
245
|
+
case 'incompatible': return `${r.field} INCOMPAT (need ${r.consumer}, have ${r.package})`;
|
|
246
|
+
default: return `${r.field} ${r.outcome}`;
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
.join(', ');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = {
|
|
253
|
+
normalizeCompat,
|
|
254
|
+
defaultCompatBlock,
|
|
255
|
+
evaluateCompat,
|
|
256
|
+
evaluateField,
|
|
257
|
+
pickBestCandidate,
|
|
258
|
+
summarizeReasons,
|
|
259
|
+
DEFAULT_COMPAT_FIELDS,
|
|
260
|
+
VALID_MODES,
|
|
261
|
+
};
|
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
|
}
|