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/download.js
CHANGED
|
@@ -9,6 +9,10 @@ const StreamZip = require('node-stream-zip');
|
|
|
9
9
|
const { isLink, getLinkTarget } = require('./link-utils');
|
|
10
10
|
const { sha256Of } = require('./profile');
|
|
11
11
|
const { getProvider } = require('./providers');
|
|
12
|
+
const { evaluateCompat, pickBestCandidate, summarizeReasons } = require('./compat');
|
|
13
|
+
const { buildFromSource } = require('./build');
|
|
14
|
+
const { uploadSourceBuiltArtefact } = require('./upload-built');
|
|
15
|
+
const log = require('./logger');
|
|
12
16
|
|
|
13
17
|
// ---------------------------------------------------------------------------
|
|
14
18
|
// v1 (legacy) download helpers
|
|
@@ -42,7 +46,7 @@ async function tryDownloadFromSources(name, version, packageSources, platform, h
|
|
|
42
46
|
await downloadFile(url, destZipPath, httpClient, timeoutMs);
|
|
43
47
|
return true;
|
|
44
48
|
} catch (err) {
|
|
45
|
-
|
|
49
|
+
log.warn(`Could not download ${name}@${version} from ${url}: ${err.message}`);
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
}
|
|
@@ -52,101 +56,257 @@ async function tryDownloadFromSources(name, version, packageSources, platform, h
|
|
|
52
56
|
// ---------------------------------------------------------------------------
|
|
53
57
|
// v2 download helpers
|
|
54
58
|
// ---------------------------------------------------------------------------
|
|
59
|
+
//
|
|
60
|
+
// PLAN-BUILD-MISSING phase 1: strict-by-default resolution.
|
|
61
|
+
//
|
|
62
|
+
// The v2 path is split in two:
|
|
63
|
+
//
|
|
64
|
+
// tryV2Exact — ONLY attempts the exact profile-hash match. Also returns
|
|
65
|
+
// the versions index if the package exists in v2 (so the
|
|
66
|
+
// caller can distinguish "v2 knows this package but not your
|
|
67
|
+
// profile" from "v2 doesn't know this package at all").
|
|
68
|
+
//
|
|
69
|
+
// tryV2Compat — the old OS+arch compatibility fallback. Now dead code on
|
|
70
|
+
// the default path; only reachable via the escape-hatch env
|
|
71
|
+
// var WYVRNPM_ALLOW_LOOSE_COMPAT=1 (deprecated — prints a
|
|
72
|
+
// warning when used). Will be deleted in a future release.
|
|
73
|
+
//
|
|
55
74
|
|
|
56
75
|
/**
|
|
57
|
-
*
|
|
76
|
+
* Attempt an exact v2 profile-hash download across all configured sources.
|
|
58
77
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
78
|
+
* Semantics:
|
|
79
|
+
* - If ANY source has a byte-exact match, we download from the first one
|
|
80
|
+
* and return `found: true`.
|
|
81
|
+
* - Otherwise we return `found: false`, along with whether any source had
|
|
82
|
+
* a versions.json entry for (name, version). The caller uses that flag
|
|
83
|
+
* to decide whether to fall through to the v1 legacy path (only when
|
|
84
|
+
* the package is unknown in v2 entirely) or to fail strictly (when v2
|
|
85
|
+
* knows the package but not the requested profile).
|
|
63
86
|
*
|
|
64
|
-
* @param {{
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* }} dep
|
|
70
|
-
* @param {string[]} packageSources
|
|
71
|
-
* @param {string} destZipPath
|
|
72
|
-
* @param {number} timeoutMs
|
|
73
|
-
* @param {object} authOptions - { awsProfile?, token? }
|
|
87
|
+
* @param {{ name: string, version: string, profileHash: string }} dep
|
|
88
|
+
* @param {string[]} packageSources
|
|
89
|
+
* @param {string} destZipPath
|
|
90
|
+
* @param {number} timeoutMs
|
|
91
|
+
* @param {object} authOptions { awsProfile?, token? }
|
|
74
92
|
* @returns {Promise<{
|
|
75
|
-
* found:
|
|
76
|
-
* contentSha256: string|null,
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* }|null>} null = not found in v2
|
|
93
|
+
* found: boolean,
|
|
94
|
+
* result?: { contentSha256: string|null, gitSha: string|null, gitRepo: string|null, profileHash: string, source: string },
|
|
95
|
+
* indexFoundInSource: string|null, // which source returned a versions index (null = none did)
|
|
96
|
+
* availableProfiles: string[], // profile hashes the registry HAS, for the error message
|
|
97
|
+
* versionsIndex?: object, // pre-fetched index, reusable by tryV2Compat
|
|
98
|
+
* }>}
|
|
82
99
|
*/
|
|
83
|
-
async function
|
|
84
|
-
const { name, version, profileHash
|
|
100
|
+
async function tryV2Exact(dep, packageSources, destZipPath, timeoutMs, authOptions = {}) {
|
|
101
|
+
const { name, version, profileHash } = dep;
|
|
85
102
|
const { awsProfile, token } = authOptions;
|
|
86
103
|
|
|
104
|
+
let indexFoundInSource = null;
|
|
105
|
+
let versionsIndex = null;
|
|
106
|
+
let availableProfiles = [];
|
|
107
|
+
|
|
87
108
|
for (const source of packageSources) {
|
|
88
109
|
let provider;
|
|
89
110
|
try { provider = getProvider(source); } catch { continue; }
|
|
90
111
|
|
|
91
|
-
// 1) Exact profile hash
|
|
92
112
|
const base = source.replace(/\/$/, '');
|
|
93
113
|
const metaUrl = `${base}/v2/${name}/${version}/${profileHash}/wyvrn.json`;
|
|
94
|
-
|
|
114
|
+
log.info(`Trying v2: ${metaUrl}`);
|
|
95
115
|
const exactMeta = await provider.v2GetMeta({ source, name, version, profileHash, awsProfile, token });
|
|
96
116
|
if (exactMeta) {
|
|
97
117
|
const ok = await provider.v2DownloadZip({ source, name, version, profileHash, awsProfile, token }, destZipPath, timeoutMs);
|
|
98
118
|
if (ok) {
|
|
99
119
|
return {
|
|
100
|
-
found:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
120
|
+
found: true,
|
|
121
|
+
result: {
|
|
122
|
+
contentSha256: exactMeta.contentSha256 ?? null,
|
|
123
|
+
gitSha: exactMeta.gitSha ?? null,
|
|
124
|
+
gitRepo: exactMeta.gitRepo ?? null,
|
|
125
|
+
profileHash,
|
|
126
|
+
source,
|
|
127
|
+
},
|
|
128
|
+
indexFoundInSource: source,
|
|
129
|
+
availableProfiles: [],
|
|
106
130
|
};
|
|
107
131
|
}
|
|
108
132
|
} else {
|
|
109
|
-
|
|
133
|
+
log.info(`v2 miss (no exact profile match): ${metaUrl}`);
|
|
110
134
|
}
|
|
111
135
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
136
|
+
// No exact match from this source. Check whether v2 knows the package
|
|
137
|
+
// at all — keep the first non-null index we find (for error messages
|
|
138
|
+
// and potential compat fallback).
|
|
139
|
+
if (!versionsIndex) {
|
|
140
|
+
const idx = await provider.v2GetVersionsIndex({ source, name, awsProfile, token });
|
|
141
|
+
if (idx?.versions?.[version]?.profiles) {
|
|
142
|
+
versionsIndex = idx;
|
|
143
|
+
indexFoundInSource = source;
|
|
144
|
+
availableProfiles = Object.keys(idx.versions[version].profiles);
|
|
145
|
+
}
|
|
119
146
|
}
|
|
147
|
+
}
|
|
120
148
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
return { found: false, indexFoundInSource, availableProfiles, versionsIndex };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Declarative per-package compatibility resolver (PLAN-BUILD-MISSING phase 2).
|
|
154
|
+
*
|
|
155
|
+
* When no exact profileHash match exists but the v2 registry knows the
|
|
156
|
+
* package, we evaluate each registry-side build's `compatibility` block
|
|
157
|
+
* (from the build's `wyvrn.json`) against the consumer's profile. If any
|
|
158
|
+
* candidates are compatible, the tiebreaker picks one.
|
|
159
|
+
*
|
|
160
|
+
* Cost: O(n) HTTP round-trips (one `v2GetMeta` per build) — acceptable at
|
|
161
|
+
* phase 2; phase 2.1 could fold the compat block into `versions.json` to
|
|
162
|
+
* make this O(1).
|
|
163
|
+
*
|
|
164
|
+
* @param {object} args
|
|
165
|
+
* @param {string} args.name
|
|
166
|
+
* @param {string} args.version
|
|
167
|
+
* @param {import('./profile').WyvrnProfile} args.consumerProfile
|
|
168
|
+
* @param {object} args.versionsIndex pre-fetched from tryV2Exact
|
|
169
|
+
* @param {string} args.source source URL that returned the index
|
|
170
|
+
* @param {object} args.authOptions
|
|
171
|
+
* @returns {Promise<{
|
|
172
|
+
* profileHash: string,
|
|
173
|
+
* contentSha256: string|null,
|
|
174
|
+
* gitSha: string|null,
|
|
175
|
+
* gitRepo: string|null,
|
|
176
|
+
* source: string,
|
|
177
|
+
* reasons: Array<object>,
|
|
178
|
+
* meta: object,
|
|
179
|
+
* }|null>}
|
|
180
|
+
*/
|
|
181
|
+
async function findCompatibleBuild({ name, version, consumerProfile, consumerOptions, versionsIndex, source, authOptions = {} }) {
|
|
182
|
+
const { awsProfile, token } = authOptions;
|
|
183
|
+
let provider;
|
|
184
|
+
try { provider = getProvider(source); } catch { return null; }
|
|
185
|
+
|
|
186
|
+
const versionEntry = versionsIndex?.versions?.[version];
|
|
187
|
+
if (!versionEntry?.profiles) return null;
|
|
188
|
+
|
|
189
|
+
// Consumer side includes options so the compat evaluator can see them.
|
|
190
|
+
const consumerFull = consumerOptions
|
|
191
|
+
? { ...consumerProfile, options: consumerOptions }
|
|
192
|
+
: consumerProfile;
|
|
193
|
+
|
|
194
|
+
const candidates = [];
|
|
195
|
+
for (const [profileHash, buildEntry] of Object.entries(versionEntry.profiles)) {
|
|
196
|
+
const packageProfile = buildEntry.buildSettings ?? buildEntry.profile;
|
|
197
|
+
if (!packageProfile) continue;
|
|
198
|
+
|
|
199
|
+
// Compatibility block lives on the per-build wyvrn.json. Fetch it.
|
|
200
|
+
const meta = await provider.v2GetMeta({ source, name, version, profileHash, awsProfile, token });
|
|
201
|
+
if (!meta) continue;
|
|
202
|
+
|
|
203
|
+
// Package-side options may live on the nested buildSettings.options (new
|
|
204
|
+
// publish path) or as a sibling key when publish-time metadata diverges.
|
|
205
|
+
const packageFull = packageProfile.options
|
|
206
|
+
? packageProfile
|
|
207
|
+
: (meta.buildSettings?.options
|
|
208
|
+
? { ...packageProfile, options: meta.buildSettings.options }
|
|
209
|
+
: packageProfile);
|
|
210
|
+
|
|
211
|
+
const rawCompat = meta.compatibility ?? null;
|
|
212
|
+
const { compatible, reasons } = evaluateCompat(consumerFull, packageFull, rawCompat);
|
|
213
|
+
if (compatible) {
|
|
214
|
+
candidates.push({ profileHash, packageProfile, reasons, buildEntry, meta });
|
|
144
215
|
}
|
|
145
216
|
}
|
|
146
217
|
|
|
218
|
+
const best = pickBestCandidate(candidates, consumerProfile);
|
|
219
|
+
if (!best) return null;
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
profileHash: best.profileHash,
|
|
223
|
+
contentSha256: best.buildEntry.contentSha256 ?? best.meta.contentSha256 ?? null,
|
|
224
|
+
gitSha: versionEntry.source?.gitSha ?? best.meta.gitSha ?? null,
|
|
225
|
+
gitRepo: versionEntry.source?.gitRepo ?? best.meta.gitRepo ?? null,
|
|
226
|
+
source,
|
|
227
|
+
reasons: best.reasons,
|
|
228
|
+
meta: best.meta,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* LEGACY compat fallback — scan versions.json for a build with the same OS +
|
|
234
|
+
* arch as the consumer's profile and use it. Reachable only via
|
|
235
|
+
* WYVRNPM_ALLOW_LOOSE_COMPAT=1. Prints a deprecation warning whenever it
|
|
236
|
+
* succeeds. Will be removed after one release cycle.
|
|
237
|
+
*
|
|
238
|
+
* @param {{ name: string, version: string, profile: import('./profile').WyvrnProfile }} dep
|
|
239
|
+
* @param {object} versionsIndex pre-fetched from tryV2Exact
|
|
240
|
+
* @param {string} source source URL that returned the index
|
|
241
|
+
* @param {string} destZipPath
|
|
242
|
+
* @param {number} timeoutMs
|
|
243
|
+
* @param {object} authOptions
|
|
244
|
+
* @returns {Promise<object|null>} same shape as tryV2Exact.result, or null
|
|
245
|
+
*/
|
|
246
|
+
async function tryV2Compat(dep, versionsIndex, source, destZipPath, timeoutMs, authOptions = {}) {
|
|
247
|
+
const { name, version, profile } = dep;
|
|
248
|
+
const { awsProfile, token } = authOptions;
|
|
249
|
+
|
|
250
|
+
let provider;
|
|
251
|
+
try { provider = getProvider(source); } catch { return null; }
|
|
252
|
+
|
|
253
|
+
const versionEntry = versionsIndex?.versions?.[version];
|
|
254
|
+
if (!versionEntry?.profiles) return null;
|
|
255
|
+
|
|
256
|
+
for (const [ph, buildEntry] of Object.entries(versionEntry.profiles)) {
|
|
257
|
+
const bp = buildEntry.buildSettings ?? buildEntry.profile;
|
|
258
|
+
if (!bp) continue;
|
|
259
|
+
if (bp.os !== profile.os) continue;
|
|
260
|
+
if (bp.arch !== profile.arch) continue;
|
|
261
|
+
|
|
262
|
+
log.warn(
|
|
263
|
+
`⚠ DEPRECATED loose-compat fallback (WYVRNPM_ALLOW_LOOSE_COMPAT=1):\n` +
|
|
264
|
+
` ${name}@${version} — using build [${ph}] (${bp.compiler} ${bp['compiler.version']})\n` +
|
|
265
|
+
` cppstd/runtime may differ from your profile. This fallback will be removed in the next release.`,
|
|
266
|
+
);
|
|
267
|
+
const ok = await provider.v2DownloadZip({ source, name, version, profileHash: ph, awsProfile, token }, destZipPath, timeoutMs);
|
|
268
|
+
if (ok) {
|
|
269
|
+
return {
|
|
270
|
+
contentSha256: buildEntry.contentSha256 ?? null,
|
|
271
|
+
gitSha: versionEntry.source?.gitSha ?? null,
|
|
272
|
+
gitRepo: versionEntry.source?.gitRepo ?? null,
|
|
273
|
+
profileHash: ph,
|
|
274
|
+
source,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
147
278
|
return null;
|
|
148
279
|
}
|
|
149
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Build the structured error message shown when a package exists in the v2
|
|
283
|
+
* registry but no build matches the consumer's profileHash.
|
|
284
|
+
*/
|
|
285
|
+
function buildStrictMissMessage({ name, version, profileHash, availableProfiles, buildMode, loose }) {
|
|
286
|
+
const lines = [
|
|
287
|
+
`no v2 build of ${name}@${version} matches profile [${profileHash}]`,
|
|
288
|
+
availableProfiles.length > 0
|
|
289
|
+
? ` Available builds on the registry: ${availableProfiles.map((p) => `[${p}]`).join(' ')}`
|
|
290
|
+
: ` The registry lists no builds for this version.`,
|
|
291
|
+
];
|
|
292
|
+
if (buildMode === 'missing') {
|
|
293
|
+
lines.push(
|
|
294
|
+
` --build=missing source-build was attempted but did not produce a usable binary. ` +
|
|
295
|
+
`See the error above for details (missing git metadata, missing "build" section in source manifest, or compile failure).`,
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
lines.push(
|
|
299
|
+
` Fix: publish a build for your profile, switch to a compatible one, or rerun with --build=missing to source-build.`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
if (!loose) {
|
|
303
|
+
lines.push(
|
|
304
|
+
` Escape hatch: WYVRNPM_ALLOW_LOOSE_COMPAT=1 (deprecated — accepts same OS+arch with possibly-different cppstd/runtime).`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return lines.join('\n');
|
|
308
|
+
}
|
|
309
|
+
|
|
150
310
|
// ---------------------------------------------------------------------------
|
|
151
311
|
// Extraction helper
|
|
152
312
|
// ---------------------------------------------------------------------------
|
|
@@ -163,7 +323,21 @@ async function extractZip(destZipPath, extractDir) {
|
|
|
163
323
|
|
|
164
324
|
/**
|
|
165
325
|
* Downloads and extracts all resolved dependencies.
|
|
166
|
-
*
|
|
326
|
+
*
|
|
327
|
+
* Resolution order (v2 path, i.e. enriched deps with a profileHash):
|
|
328
|
+
* 1. Exact profile-hash match from any configured v2 source → use it.
|
|
329
|
+
* 2. Otherwise, if the package exists in v2 (versions.json returned an
|
|
330
|
+
* entry for this version) but nothing matches our profile:
|
|
331
|
+
* - Default: STRICT FAIL with a structured error. Does not fall back
|
|
332
|
+
* to v1 (that would silently accept a build with different settings).
|
|
333
|
+
* - `buildMode === 'missing'`: same strict fail for now; error hints
|
|
334
|
+
* at PLAN-BUILD-MISSING phase 3.
|
|
335
|
+
* - WYVRNPM_ALLOW_LOOSE_COMPAT=1 (deprecated escape hatch): try the
|
|
336
|
+
* old OS+arch compat fallback with a deprecation warning.
|
|
337
|
+
* 3. Otherwise (v2 doesn't know the package at all) → v1 legacy fallback.
|
|
338
|
+
*
|
|
339
|
+
* Legacy deps (plain `Map<name, version>`, no profile info) skip v2 entirely
|
|
340
|
+
* and use the v1 path, same as before.
|
|
167
341
|
*
|
|
168
342
|
* @param {Map<string, string | {version:string, profileHash:string|null, profile:import('./profile').WyvrnProfile|null}>} deps
|
|
169
343
|
* Either a plain `Map<name, version>` (legacy callers / tests) or the enriched map
|
|
@@ -173,12 +347,29 @@ async function extractZip(destZipPath, extractDir) {
|
|
|
173
347
|
* @param {string} razerDir
|
|
174
348
|
* @param {Function} httpClient - fetch-compatible
|
|
175
349
|
* @param {number} timeout - seconds
|
|
176
|
-
* @param {object} [
|
|
350
|
+
* @param {object} [options] - { awsProfile?, token?, buildMode? }
|
|
351
|
+
* buildMode: 'never' (default) | 'missing' | 'always'
|
|
177
352
|
* @returns {Promise<Map<string, {version:string, profileHash?:string, contentSha256?:string, gitSha?:string, resolvedFrom:string}>>}
|
|
178
353
|
*/
|
|
179
|
-
async function downloadDependencies(deps, packageSources, platform, razerDir, httpClient, timeout,
|
|
354
|
+
async function downloadDependencies(deps, packageSources, platform, razerDir, httpClient, timeout, options = {}) {
|
|
180
355
|
const timeoutMs = timeout * 1000;
|
|
181
|
-
const {
|
|
356
|
+
const {
|
|
357
|
+
awsProfile, token, buildMode = 'never', profileName = 'default',
|
|
358
|
+
uploadBuilt = false, uploadSource, uploadAuth,
|
|
359
|
+
// Optional accumulator. When provided, upload outcomes are pushed here
|
|
360
|
+
// so the caller can print a single aggregated summary at the end of an
|
|
361
|
+
// install (see src/commands/install.js). Mutated in place.
|
|
362
|
+
uploadStats,
|
|
363
|
+
} = options;
|
|
364
|
+
|
|
365
|
+
if (buildMode === 'always') {
|
|
366
|
+
throw new Error(
|
|
367
|
+
'--build=always is not implemented (requires PLAN-BUILD-MISSING phase 3). ' +
|
|
368
|
+
'Use --build=never (default) or --build=missing.',
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const looseCompatAllowed = process.env.WYVRNPM_ALLOW_LOOSE_COMPAT === '1';
|
|
182
373
|
|
|
183
374
|
if (!fs.existsSync(razerDir)) {
|
|
184
375
|
fs.mkdirSync(razerDir, { recursive: true });
|
|
@@ -193,6 +384,7 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
193
384
|
const version = isLegacyEntry ? depInfoOrVersion : depInfoOrVersion.version;
|
|
194
385
|
const profileHash = isLegacyEntry ? null : (depInfoOrVersion.profileHash ?? null);
|
|
195
386
|
const profile = isLegacyEntry ? null : (depInfoOrVersion.profile ?? null);
|
|
387
|
+
const options = isLegacyEntry ? null : (depInfoOrVersion.options ?? null);
|
|
196
388
|
|
|
197
389
|
const extractDir = path.join(razerDir, name);
|
|
198
390
|
const versionFile = path.join(extractDir, '.wyvrn-version');
|
|
@@ -201,7 +393,7 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
201
393
|
// Skip linked packages
|
|
202
394
|
if (isLink(extractDir)) {
|
|
203
395
|
const linkTarget = getLinkTarget(extractDir);
|
|
204
|
-
|
|
396
|
+
log.info(`Linked: ${name} → ${linkTarget} (skipping download)`);
|
|
205
397
|
lockEntries.set(name, { version, resolvedFrom: 'link' });
|
|
206
398
|
continue;
|
|
207
399
|
}
|
|
@@ -219,47 +411,166 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
219
411
|
const sameProfile = !profileHash || installedMeta?.profileHash === profileHash;
|
|
220
412
|
|
|
221
413
|
if (sameVersion && sameProfile) {
|
|
222
|
-
|
|
414
|
+
log.info(`Already present: ${name}@${version}${profileHash ? ` [${installedMeta?.profileHash}]` : ''} — skipping`);
|
|
223
415
|
lockEntries.set(name, {
|
|
224
416
|
version,
|
|
225
417
|
profileHash: installedMeta?.profileHash ?? null,
|
|
226
418
|
contentSha256: installedMeta?.contentSha256 ?? null,
|
|
227
419
|
gitSha: installedMeta?.gitSha ?? null,
|
|
228
420
|
resolvedFrom: installedMeta?.resolvedFrom ?? 'cached',
|
|
421
|
+
...(options ? { options } : {}),
|
|
229
422
|
});
|
|
230
423
|
continue;
|
|
231
424
|
}
|
|
232
425
|
if (!sameVersion) {
|
|
233
|
-
|
|
426
|
+
log.info(`Version changed: ${name} ${installedVersion} → ${version}, re-downloading`);
|
|
234
427
|
} else {
|
|
235
|
-
|
|
428
|
+
log.info(`Profile changed for ${name}@${version}, re-downloading`);
|
|
236
429
|
}
|
|
237
430
|
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
238
431
|
}
|
|
239
432
|
|
|
240
|
-
|
|
433
|
+
log.info(`Downloading: ${name}@${version}`);
|
|
241
434
|
const destZipPath = path.join(razerDir, `${name}.zip`);
|
|
242
435
|
|
|
243
436
|
let downloadResult = null;
|
|
244
437
|
let resolvedFrom = 'v1';
|
|
438
|
+
let skipV1Fallback = false;
|
|
245
439
|
|
|
246
|
-
// ── Try v2
|
|
440
|
+
// ── Try v2 exact ────────────────────────────────────────────────────────
|
|
247
441
|
if (profileHash && profile) {
|
|
248
|
-
|
|
249
|
-
{ name, version, profileHash
|
|
442
|
+
const v2 = await tryV2Exact(
|
|
443
|
+
{ name, version, profileHash },
|
|
250
444
|
packageSources,
|
|
251
445
|
destZipPath,
|
|
252
446
|
timeoutMs,
|
|
253
447
|
{ awsProfile, token },
|
|
254
448
|
);
|
|
255
|
-
|
|
449
|
+
|
|
450
|
+
if (v2.found) {
|
|
451
|
+
downloadResult = v2.result;
|
|
452
|
+
resolvedFrom = 'v2';
|
|
453
|
+
} else if (v2.indexFoundInSource) {
|
|
454
|
+
// v2 knows this package but no exact profile match. Do NOT fall back
|
|
455
|
+
// to v1 — that would silently accept a binary with different build
|
|
456
|
+
// settings. Try declarative compatibility first (phase 2), then the
|
|
457
|
+
// deprecated env-var escape hatch, then fail strictly.
|
|
458
|
+
skipV1Fallback = true;
|
|
459
|
+
|
|
460
|
+
// ── Phase 2: declarative per-package compatibility ─────────────────
|
|
461
|
+
const compatResult = await findCompatibleBuild({
|
|
462
|
+
name, version,
|
|
463
|
+
consumerProfile: profile,
|
|
464
|
+
consumerOptions: options,
|
|
465
|
+
versionsIndex: v2.versionsIndex,
|
|
466
|
+
source: v2.indexFoundInSource,
|
|
467
|
+
authOptions: { awsProfile, token },
|
|
468
|
+
});
|
|
469
|
+
if (compatResult) {
|
|
470
|
+
// Download the chosen compatible build's zip.
|
|
471
|
+
const compatProvider = getProvider(compatResult.source);
|
|
472
|
+
const ok = await compatProvider.v2DownloadZip(
|
|
473
|
+
{ source: compatResult.source, name, version, profileHash: compatResult.profileHash, awsProfile, token },
|
|
474
|
+
destZipPath,
|
|
475
|
+
timeoutMs,
|
|
476
|
+
);
|
|
477
|
+
if (ok) {
|
|
478
|
+
log.info(
|
|
479
|
+
`Compatibility match for ${name}@${version}: [${compatResult.profileHash}] — ` +
|
|
480
|
+
`${summarizeReasons(compatResult.reasons)}`,
|
|
481
|
+
);
|
|
482
|
+
downloadResult = {
|
|
483
|
+
contentSha256: compatResult.contentSha256,
|
|
484
|
+
gitSha: compatResult.gitSha,
|
|
485
|
+
gitRepo: compatResult.gitRepo,
|
|
486
|
+
profileHash: compatResult.profileHash,
|
|
487
|
+
source: compatResult.source,
|
|
488
|
+
};
|
|
489
|
+
resolvedFrom = 'v2-compat';
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── Phase 3: source-build when `--build=missing` is active ─────────
|
|
494
|
+
if (!downloadResult && buildMode === 'missing') {
|
|
495
|
+
const sourceInfo = v2.versionsIndex?.versions?.[version]?.source;
|
|
496
|
+
if (sourceInfo?.gitRepo && sourceInfo?.gitSha) {
|
|
497
|
+
try {
|
|
498
|
+
const built = await buildFromSource({
|
|
499
|
+
name, version,
|
|
500
|
+
profile, profileName, profileHash,
|
|
501
|
+
options,
|
|
502
|
+
gitRepo: sourceInfo.gitRepo,
|
|
503
|
+
gitSha: sourceInfo.gitSha,
|
|
504
|
+
destZipPath,
|
|
505
|
+
packageSources,
|
|
506
|
+
platform,
|
|
507
|
+
timeoutMs,
|
|
508
|
+
buildMode,
|
|
509
|
+
authOptions: { awsProfile, token },
|
|
510
|
+
});
|
|
511
|
+
downloadResult = {
|
|
512
|
+
contentSha256: built.contentSha256,
|
|
513
|
+
gitSha: built.gitSha,
|
|
514
|
+
gitRepo: built.gitRepo,
|
|
515
|
+
profileHash: built.profileHash,
|
|
516
|
+
source: built.source,
|
|
517
|
+
// Carried through so the post-extraction upload step can
|
|
518
|
+
// read the manifest and push the zip without re-doing work.
|
|
519
|
+
artefactPath: built.artefactPath,
|
|
520
|
+
clonedManifestPath: built.clonedManifestPath,
|
|
521
|
+
};
|
|
522
|
+
resolvedFrom = 'v2-source-build';
|
|
523
|
+
} catch (err) {
|
|
524
|
+
log.error(`Source-build failed for ${name}@${version}: ${err.message}`);
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
log.error(
|
|
528
|
+
`Cannot source-build ${name}@${version}: no git metadata on registry ` +
|
|
529
|
+
`(gitRepo/gitSha). Publisher must re-publish with git info.`,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Deprecated escape hatch: WYVRNPM_ALLOW_LOOSE_COMPAT=1 ──────────
|
|
535
|
+
if (!downloadResult && looseCompatAllowed) {
|
|
536
|
+
const compat = await tryV2Compat(
|
|
537
|
+
{ name, version, profile },
|
|
538
|
+
v2.versionsIndex,
|
|
539
|
+
v2.indexFoundInSource,
|
|
540
|
+
destZipPath,
|
|
541
|
+
timeoutMs,
|
|
542
|
+
{ awsProfile, token },
|
|
543
|
+
);
|
|
544
|
+
if (compat) {
|
|
545
|
+
downloadResult = compat;
|
|
546
|
+
resolvedFrom = 'v2-compat-deprecated';
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (!downloadResult) {
|
|
551
|
+
log.error(buildStrictMissMessage({
|
|
552
|
+
name, version, profileHash,
|
|
553
|
+
availableProfiles: v2.availableProfiles,
|
|
554
|
+
buildMode,
|
|
555
|
+
loose: looseCompatAllowed,
|
|
556
|
+
}));
|
|
557
|
+
lockEntries.set(name, {
|
|
558
|
+
version,
|
|
559
|
+
profileHash,
|
|
560
|
+
resolvedFrom: buildMode === 'missing' ? 'source-build-failed' : 'no-exact-match',
|
|
561
|
+
availableProfiles: v2.availableProfiles,
|
|
562
|
+
});
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// else: no v2 index anywhere → fall through to v1
|
|
256
567
|
}
|
|
257
568
|
|
|
258
569
|
// ── Fall back to v1 ─────────────────────────────────────────────────────
|
|
259
|
-
if (!downloadResult) {
|
|
570
|
+
if (!downloadResult && !skipV1Fallback) {
|
|
260
571
|
const ok = await tryDownloadFromSources(name, version, packageSources, platform, httpClient, timeoutMs, destZipPath);
|
|
261
572
|
if (!ok) {
|
|
262
|
-
|
|
573
|
+
log.warn(`package ${name}@${version} was not found in any configured source`);
|
|
263
574
|
lockEntries.set(name, { version, resolvedFrom: 'not-found' });
|
|
264
575
|
continue;
|
|
265
576
|
}
|
|
@@ -269,8 +580,8 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
269
580
|
if (downloadResult?.contentSha256) {
|
|
270
581
|
const actual = sha256Of(destZipPath);
|
|
271
582
|
if (actual !== downloadResult.contentSha256) {
|
|
272
|
-
|
|
273
|
-
`
|
|
583
|
+
log.error(
|
|
584
|
+
`SHA256 mismatch for ${name}@${version}\n` +
|
|
274
585
|
` expected: ${downloadResult.contentSha256}\n` +
|
|
275
586
|
` actual: ${actual}`,
|
|
276
587
|
);
|
|
@@ -278,7 +589,7 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
278
589
|
lockEntries.set(name, { version, resolvedFrom: 'sha256-mismatch' });
|
|
279
590
|
continue;
|
|
280
591
|
}
|
|
281
|
-
|
|
592
|
+
log.info(`SHA256 verified: ${actual.slice(0, 16)}...`);
|
|
282
593
|
}
|
|
283
594
|
|
|
284
595
|
// ── Extract ─────────────────────────────────────────────────────────────
|
|
@@ -301,16 +612,16 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
301
612
|
};
|
|
302
613
|
fs.writeFileSync(metaFile, JSON.stringify(meta, null, 2) + '\n', 'utf8');
|
|
303
614
|
|
|
304
|
-
|
|
305
|
-
`
|
|
615
|
+
log.info(
|
|
616
|
+
`Extracted: ${name}@${version}` +
|
|
306
617
|
(downloadResult?.profileHash ? ` [${downloadResult.profileHash}]` : '') +
|
|
307
618
|
` (${resolvedFrom}) → ${extractDir}`,
|
|
308
619
|
);
|
|
309
620
|
|
|
310
621
|
if (downloadResult?.gitSha) {
|
|
311
|
-
|
|
622
|
+
log.info(` Source commit : ${downloadResult.gitSha}`);
|
|
312
623
|
if (downloadResult?.gitRepo) {
|
|
313
|
-
|
|
624
|
+
log.info(` Source repo : ${downloadResult.gitRepo}`);
|
|
314
625
|
}
|
|
315
626
|
}
|
|
316
627
|
|
|
@@ -320,9 +631,42 @@ async function downloadDependencies(deps, packageSources, platform, razerDir, ht
|
|
|
320
631
|
profileHash: downloadResult?.profileHash ?? null,
|
|
321
632
|
contentSha256: downloadResult?.contentSha256 ?? null,
|
|
322
633
|
gitSha: downloadResult?.gitSha ?? null,
|
|
634
|
+
...(options ? { options } : {}),
|
|
323
635
|
});
|
|
636
|
+
|
|
637
|
+
// ── Upload freshly source-built artefact back to the registry ────────
|
|
638
|
+
// Opt-in via --upload-built. Runs after extraction succeeds so a
|
|
639
|
+
// bad artefact is never uploaded. Upload failures log but never
|
|
640
|
+
// break the install — the consumer already has a working install.
|
|
641
|
+
if (uploadBuilt && resolvedFrom === 'v2-source-build' && downloadResult?.artefactPath) {
|
|
642
|
+
try {
|
|
643
|
+
const outcome = await uploadSourceBuiltArtefact({
|
|
644
|
+
name, version,
|
|
645
|
+
profile, profileHash,
|
|
646
|
+
artefactPath: downloadResult.artefactPath,
|
|
647
|
+
clonedManifestPath: downloadResult.clonedManifestPath,
|
|
648
|
+
contentSha256: downloadResult.contentSha256,
|
|
649
|
+
gitRepo: downloadResult.gitRepo,
|
|
650
|
+
gitSha: downloadResult.gitSha,
|
|
651
|
+
options,
|
|
652
|
+
uploadSource,
|
|
653
|
+
uploadAuth: uploadAuth ?? { awsProfile, token },
|
|
654
|
+
});
|
|
655
|
+
if (uploadStats) {
|
|
656
|
+
const bucket = outcome.uploaded
|
|
657
|
+
? 'uploaded'
|
|
658
|
+
: /already exists/.test(outcome.reason) ? 'skipped' : 'failed';
|
|
659
|
+
uploadStats[bucket].push({ name, version, profileHash, reason: outcome.reason });
|
|
660
|
+
}
|
|
661
|
+
} catch (err) {
|
|
662
|
+
log.warn(`upload-built: ${name}@${version} upload skipped — ${err.message}`);
|
|
663
|
+
if (uploadStats) {
|
|
664
|
+
uploadStats.failed.push({ name, version, profileHash, reason: err.message });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
324
668
|
} catch (err) {
|
|
325
|
-
|
|
669
|
+
log.warn(`Failed to extract ${name}@${version}: ${err.message}`);
|
|
326
670
|
lockEntries.set(name, { version, resolvedFrom: 'extract-failed' });
|
|
327
671
|
} finally {
|
|
328
672
|
try { fs.unlinkSync(destZipPath); } catch { /* ignore */ }
|