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