wyvrnpm 2.1.0 → 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 +257 -1
- package/bin/wyvrn.js +131 -14
- package/claude/skills/wyvrnpm.skill +0 -0
- package/package.json +2 -1
- package/src/build/cache.js +47 -0
- package/src/build/index.js +42 -11
- package/src/build/recipe.js +30 -4
- package/src/commands/build.js +49 -8
- package/src/commands/clean.js +39 -15
- package/src/commands/install-skill.js +107 -0
- package/src/commands/install.js +201 -5
- package/src/commands/publish.js +150 -35
- package/src/commands/show.js +237 -0
- package/src/compat.js +32 -3
- package/src/download.js +65 -3
- package/src/ignore.js +118 -0
- package/src/logger.js +26 -10
- package/src/manifest.js +12 -7
- package/src/options.js +303 -0
- package/src/profile.js +28 -4
- package/src/providers/base.js +16 -1
- package/src/providers/file.js +8 -3
- package/src/providers/http.js +12 -8
- package/src/providers/s3.js +11 -7
- package/src/resolve.js +171 -13
- package/src/toolchain/presets.js +88 -16
- package/src/upload-built.js +256 -0
- package/src/version-range.js +301 -0
package/src/profile.js
CHANGED
|
@@ -163,25 +163,49 @@ function detectProfile() {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
|
-
* Compute a stable 16-char SHA256 prefix
|
|
167
|
-
*
|
|
166
|
+
* Compute a stable 16-char SHA256 prefix identifying a unique build.
|
|
167
|
+
* Inputs:
|
|
168
|
+
* - `profile`: the toolchain/ABI fields (os, arch, compiler, etc.).
|
|
169
|
+
* - `options`: (optional) recipe-declared feature toggles resolved to
|
|
170
|
+
* their effective values for this build.
|
|
171
|
+
*
|
|
172
|
+
* Backward-compat contract: when `options` is null / undefined / empty,
|
|
173
|
+
* the hash is byte-identical to what pre-F1 wyvrnpm produced. Packages
|
|
174
|
+
* without an `options` block keep their existing registry addresses.
|
|
175
|
+
* The caller therefore passes `null` (not `{}`) to mean "this recipe
|
|
176
|
+
* has no options declared" — consistent with what
|
|
177
|
+
* `resolveEffectiveOptions` returns in that case.
|
|
178
|
+
*
|
|
179
|
+
* See claude/PLAN-OPTIONS.md §3.2 for the full design.
|
|
168
180
|
*
|
|
169
181
|
* @param {WyvrnProfile} profile
|
|
182
|
+
* @param {Record<string, any>|null} [options] effective options (after defaults + overrides)
|
|
170
183
|
* @returns {string} 16-character lowercase hex string
|
|
171
184
|
*/
|
|
172
|
-
function hashProfile(profile) {
|
|
185
|
+
function hashProfile(profile, options = null) {
|
|
173
186
|
const significant = Object.fromEntries(
|
|
174
187
|
Object.entries(profile)
|
|
175
188
|
.filter(([, v]) => v !== null && v !== undefined && v !== '*' && v !== '')
|
|
176
189
|
.sort(([a], [b]) => a.localeCompare(b)),
|
|
177
190
|
);
|
|
191
|
+
|
|
192
|
+
const payload = (options && Object.keys(options).length > 0)
|
|
193
|
+
? { ...significant, options: sortOptionsForHashing(options) }
|
|
194
|
+
: significant;
|
|
195
|
+
|
|
178
196
|
return crypto
|
|
179
197
|
.createHash('sha256')
|
|
180
|
-
.update(JSON.stringify(
|
|
198
|
+
.update(JSON.stringify(payload))
|
|
181
199
|
.digest('hex')
|
|
182
200
|
.slice(0, 16);
|
|
183
201
|
}
|
|
184
202
|
|
|
203
|
+
function sortOptionsForHashing(options) {
|
|
204
|
+
return Object.fromEntries(
|
|
205
|
+
Object.entries(options).sort(([a], [b]) => a.localeCompare(b)),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
185
209
|
/**
|
|
186
210
|
* Compute the SHA256 hash of a file or Buffer.
|
|
187
211
|
*
|
package/src/providers/base.js
CHANGED
|
@@ -69,7 +69,20 @@ class BaseProvider {
|
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Publish to the v2 path.
|
|
72
|
-
* Writes wyvrn.json + wyvrn.zip under profileHash, updates versions.json and
|
|
72
|
+
* Writes wyvrn.json + wyvrn.zip under profileHash, updates versions.json and
|
|
73
|
+
* (when `updateLatest !== false`) latest.json.
|
|
74
|
+
*
|
|
75
|
+
* `updateLatest: false` is used by the `upload-built` flow (a consumer
|
|
76
|
+
* returning a source-built artefact — see PLAN-UPLOAD-BUILT.md). A consumer
|
|
77
|
+
* upload is NOT introducing a new package version, so `latest.json` must
|
|
78
|
+
* not be clobbered. For the same reason the `latest` pointer inside
|
|
79
|
+
* `versions.json` is also left alone. Defaults to `true` to preserve
|
|
80
|
+
* existing `publish` semantics.
|
|
81
|
+
*
|
|
82
|
+
* The optional `uploadedBy` field in `options` is written verbatim into
|
|
83
|
+
* the per-build `wyvrn.json` as provenance metadata. Absent by default
|
|
84
|
+
* (author publish); populated by the upload-built flow. Readers ignore
|
|
85
|
+
* the field — it is informational only.
|
|
73
86
|
*
|
|
74
87
|
* @param {{ manifest: string, zip: string }} files - local file paths
|
|
75
88
|
* @param {{
|
|
@@ -81,6 +94,8 @@ class BaseProvider {
|
|
|
81
94
|
* contentSha256: string,
|
|
82
95
|
* gitSha: string|null,
|
|
83
96
|
* gitRepo: string|null,
|
|
97
|
+
* updateLatest?: boolean, // default true; false for upload-built
|
|
98
|
+
* uploadedBy?: object, // optional provenance block, written into wyvrn.json
|
|
84
99
|
* awsProfile?: string,
|
|
85
100
|
* token?: string,
|
|
86
101
|
* }} options
|
package/src/providers/file.js
CHANGED
|
@@ -107,6 +107,7 @@ class FileProvider extends BaseProvider {
|
|
|
107
107
|
const {
|
|
108
108
|
source, name, version, profileHash, buildSettings,
|
|
109
109
|
contentSha256, gitSha, gitRepo, lockedDependencies,
|
|
110
|
+
updateLatest = true, uploadedBy,
|
|
110
111
|
} = options;
|
|
111
112
|
|
|
112
113
|
const v2root = this._v2Root(source, name);
|
|
@@ -130,6 +131,7 @@ class FileProvider extends BaseProvider {
|
|
|
130
131
|
gitSha: gitSha ?? null,
|
|
131
132
|
gitRepo: gitRepo ?? null,
|
|
132
133
|
publishedAt: new Date().toISOString(),
|
|
134
|
+
...(uploadedBy ? { uploadedBy } : {}),
|
|
133
135
|
};
|
|
134
136
|
this._writeJson(path.join(buildDir, 'wyvrn.json'), v2Meta);
|
|
135
137
|
|
|
@@ -155,11 +157,14 @@ class FileProvider extends BaseProvider {
|
|
|
155
157
|
if (gitSha || gitRepo) {
|
|
156
158
|
versionsIdx.versions[version].source = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
|
|
157
159
|
}
|
|
158
|
-
versionsIdx.latest = version;
|
|
160
|
+
if (updateLatest) versionsIdx.latest = version;
|
|
159
161
|
this._writeJson(versionsPath, versionsIdx);
|
|
160
162
|
|
|
161
|
-
// 5) v2 latest.json
|
|
162
|
-
|
|
163
|
+
// 5) v2 latest.json — skipped for consumer uploads (upload-built) so a
|
|
164
|
+
// historical-version rebuild cannot clobber the real "latest" pointer.
|
|
165
|
+
if (updateLatest) {
|
|
166
|
+
this._writeJson(path.join(v2root, 'latest.json'), { version, profileHash, contentSha256 });
|
|
167
|
+
}
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
async v2GetMeta({ source, name, version, profileHash }) {
|
package/src/providers/http.js
CHANGED
|
@@ -176,6 +176,7 @@ class HttpProvider extends BaseProvider {
|
|
|
176
176
|
const {
|
|
177
177
|
source, name, version, profileHash, buildSettings,
|
|
178
178
|
contentSha256, gitSha, gitRepo, lockedDependencies, token,
|
|
179
|
+
updateLatest = true, uploadedBy,
|
|
179
180
|
} = options;
|
|
180
181
|
const base = source.replace(/\/$/, '');
|
|
181
182
|
const v2root = `${base}/v2/${name}`;
|
|
@@ -199,6 +200,7 @@ class HttpProvider extends BaseProvider {
|
|
|
199
200
|
gitSha: gitSha ?? null,
|
|
200
201
|
gitRepo: gitRepo ?? null,
|
|
201
202
|
publishedAt: new Date().toISOString(),
|
|
203
|
+
...(uploadedBy ? { uploadedBy } : {}),
|
|
202
204
|
};
|
|
203
205
|
await this._putBuffer(
|
|
204
206
|
`${buildRoot}/wyvrn.json`,
|
|
@@ -232,7 +234,7 @@ class HttpProvider extends BaseProvider {
|
|
|
232
234
|
if (gitSha || gitRepo) {
|
|
233
235
|
versionsIdx.versions[version].source = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
|
|
234
236
|
}
|
|
235
|
-
versionsIdx.latest = version;
|
|
237
|
+
if (updateLatest) versionsIdx.latest = version;
|
|
236
238
|
await this._putBuffer(
|
|
237
239
|
`${v2root}/versions.json`,
|
|
238
240
|
Buffer.from(JSON.stringify(versionsIdx, null, 2)),
|
|
@@ -240,13 +242,15 @@ class HttpProvider extends BaseProvider {
|
|
|
240
242
|
token,
|
|
241
243
|
);
|
|
242
244
|
|
|
243
|
-
// 5)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
// 5) v2 latest.json — skipped for consumer uploads (see FileProvider note).
|
|
246
|
+
if (updateLatest) {
|
|
247
|
+
await this._putBuffer(
|
|
248
|
+
`${v2root}/latest.json`,
|
|
249
|
+
Buffer.from(JSON.stringify({ version, profileHash, contentSha256 })),
|
|
250
|
+
'application/json',
|
|
251
|
+
token,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
250
254
|
}
|
|
251
255
|
|
|
252
256
|
async v2GetMeta({ source, name, version, profileHash, token }) {
|
package/src/providers/s3.js
CHANGED
|
@@ -178,6 +178,7 @@ class S3Provider extends BaseProvider {
|
|
|
178
178
|
const {
|
|
179
179
|
source, name, version, profileHash, buildSettings,
|
|
180
180
|
contentSha256, gitSha, gitRepo, lockedDependencies, awsProfile,
|
|
181
|
+
updateLatest = true, uploadedBy,
|
|
181
182
|
} = options;
|
|
182
183
|
const { PutObjectCommand, GetObjectCommand } = this._loadSdk();
|
|
183
184
|
const client = this._makeClient(awsProfile);
|
|
@@ -204,6 +205,7 @@ class S3Provider extends BaseProvider {
|
|
|
204
205
|
gitSha: gitSha ?? null,
|
|
205
206
|
gitRepo: gitRepo ?? null,
|
|
206
207
|
publishedAt: new Date().toISOString(),
|
|
208
|
+
...(uploadedBy ? { uploadedBy } : {}),
|
|
207
209
|
};
|
|
208
210
|
await this._putBuffer(
|
|
209
211
|
client, bucket, `${buildRoot}/wyvrn.json`,
|
|
@@ -232,18 +234,20 @@ class S3Provider extends BaseProvider {
|
|
|
232
234
|
if (gitSha || gitRepo) {
|
|
233
235
|
versionsIdx.versions[version].source = { gitRepo: gitRepo ?? null, gitSha: gitSha ?? null };
|
|
234
236
|
}
|
|
235
|
-
versionsIdx.latest = version;
|
|
237
|
+
if (updateLatest) versionsIdx.latest = version;
|
|
236
238
|
await this._putBuffer(
|
|
237
239
|
client, bucket, `${v2root}/versions.json`,
|
|
238
240
|
Buffer.from(JSON.stringify(versionsIdx, null, 2)), 'application/json', PutObjectCommand,
|
|
239
241
|
);
|
|
240
242
|
|
|
241
|
-
// 5) latest.json
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
// 5) latest.json — skipped for consumer uploads (see FileProvider note).
|
|
244
|
+
if (updateLatest) {
|
|
245
|
+
await this._putBuffer(
|
|
246
|
+
client, bucket, `${v2root}/latest.json`,
|
|
247
|
+
Buffer.from(JSON.stringify({ version, profileHash, contentSha256 })),
|
|
248
|
+
'application/json', PutObjectCommand,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
247
251
|
}
|
|
248
252
|
|
|
249
253
|
async v2GetMeta({ source, name, version, profileHash, awsProfile }) {
|
package/src/resolve.js
CHANGED
|
@@ -4,6 +4,13 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
const log = require('./logger');
|
|
7
|
+
const {
|
|
8
|
+
parseRange,
|
|
9
|
+
matchesRange,
|
|
10
|
+
pickHighestInRange,
|
|
11
|
+
intersectRanges,
|
|
12
|
+
rangeToString,
|
|
13
|
+
} = require('./version-range');
|
|
7
14
|
|
|
8
15
|
/**
|
|
9
16
|
* Checks if a version string represents a linked package.
|
|
@@ -65,6 +72,33 @@ function compareVersions(a, b) {
|
|
|
65
72
|
return 0;
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Fetch every published version of `name` across the configured sources
|
|
77
|
+
* via each source's `versions.json` (v2 layout). Returns the union of
|
|
78
|
+
* version keys sorted ascending. Used by the range resolver to pick the
|
|
79
|
+
* highest version satisfying `^/~/>=` style specs.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} name
|
|
82
|
+
* @param {string[]} packageSources
|
|
83
|
+
* @param {Function} httpClient
|
|
84
|
+
* @returns {Promise<string[]>}
|
|
85
|
+
*/
|
|
86
|
+
async function fetchPublishedVersions(name, packageSources, httpClient) {
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
for (const source of packageSources) {
|
|
89
|
+
const base = source.replace(/\/$/, '');
|
|
90
|
+
try {
|
|
91
|
+
const resp = await httpClient(`${base}/v2/${name}/versions.json`);
|
|
92
|
+
if (!resp.ok) continue;
|
|
93
|
+
const idx = await resp.json();
|
|
94
|
+
for (const v of Object.keys(idx?.versions ?? {})) seen.add(v);
|
|
95
|
+
} catch {
|
|
96
|
+
// Try next source
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return [...seen].sort(compareVersions);
|
|
100
|
+
}
|
|
101
|
+
|
|
68
102
|
/**
|
|
69
103
|
* Resolves the "latest" tag for a package by fetching latest.json from the registry.
|
|
70
104
|
*
|
|
@@ -175,13 +209,21 @@ async function fetchPackageManifest(name, version, packageSources, platform, htt
|
|
|
175
209
|
* Resolves the full, flattened dependency graph starting from rootDeps.
|
|
176
210
|
* When the same package is required at multiple versions the highest version wins.
|
|
177
211
|
*
|
|
212
|
+
* Side-effect parameter: when `manifestsOut` is provided (a Map instance),
|
|
213
|
+
* the resolver populates it with `{name → recipe manifest}` for every
|
|
214
|
+
* fetched dependency. Callers that need recipe-declared `options` (see
|
|
215
|
+
* PLAN-OPTIONS) use this to avoid a second round of HTTP round-trips.
|
|
216
|
+
* Passing `null` / omitting it keeps the legacy behaviour.
|
|
217
|
+
*
|
|
178
218
|
* @param {Record<string, string>} rootDeps - Direct dependencies { name: version }.
|
|
179
219
|
* @param {string[]} packageSources - Ordered list of base URLs to try.
|
|
180
220
|
* @param {string} platform - Target platform string (e.g. "win_x64").
|
|
181
221
|
* @param {Function} httpClient - fetch-compatible function.
|
|
222
|
+
* @param {Map<string, string>} [lockedVersions]
|
|
223
|
+
* @param {Map<string, object>} [manifestsOut] - populated in-place when provided
|
|
182
224
|
* @returns {Promise<Map<string, string>>} Resolved map of name -> version.
|
|
183
225
|
*/
|
|
184
|
-
async function resolveDependencies(rootDeps, packageSources, platform, httpClient, lockedVersions = new Map()) {
|
|
226
|
+
async function resolveDependencies(rootDeps, packageSources, platform, httpClient, lockedVersions = new Map(), manifestsOut = null) {
|
|
185
227
|
/** @type {Map<string, string>} Final resolved versions. */
|
|
186
228
|
const resolved = new Map();
|
|
187
229
|
|
|
@@ -189,13 +231,45 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
|
|
|
189
231
|
const manifestCache = new Map();
|
|
190
232
|
|
|
191
233
|
/**
|
|
192
|
-
*
|
|
234
|
+
* Per-package effective range. For a package that only ever appears with
|
|
235
|
+
* exact versions, this stores `{ kind: 'exact', version }` — which still
|
|
236
|
+
* intersects cleanly with later range requests. For packages with range
|
|
237
|
+
* specs, we intersect contributors so conflicting ranges fail fast.
|
|
238
|
+
* @type {Map<string, ReturnType<typeof parseRange>>}
|
|
239
|
+
*/
|
|
240
|
+
const effectiveRange = new Map();
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Who contributed which raw spec for which package — used only for the
|
|
244
|
+
* conflict error message so the user knows whose declaration to fix.
|
|
245
|
+
* @type {Map<string, Array<{ by: string, spec: string }>>}
|
|
246
|
+
*/
|
|
247
|
+
const rangeContributors = new Map();
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Cache the published-versions list per package — one HTTP round trip
|
|
251
|
+
* per name regardless of how often it appears in the graph.
|
|
252
|
+
* @type {Map<string, string[]>}
|
|
253
|
+
*/
|
|
254
|
+
const publishedCache = new Map();
|
|
255
|
+
const fetchPublished = async (name) => {
|
|
256
|
+
if (publishedCache.has(name)) return publishedCache.get(name);
|
|
257
|
+
const list = await fetchPublishedVersions(name, packageSources, httpClient);
|
|
258
|
+
publishedCache.set(name, list);
|
|
259
|
+
return list;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Pending work queue: each entry is { name, version, by? }.
|
|
264
|
+
* `by` is the contributor label for conflict diagnostics.
|
|
193
265
|
* We process iteratively to avoid deep recursion on large dependency trees.
|
|
194
266
|
*/
|
|
195
|
-
const queue = Object.entries(rootDeps).map(
|
|
267
|
+
const queue = Object.entries(rootDeps).map(
|
|
268
|
+
([name, version]) => ({ name, version, by: 'wyvrn.json' }),
|
|
269
|
+
);
|
|
196
270
|
|
|
197
271
|
while (queue.length > 0) {
|
|
198
|
-
let { name, version } = queue.shift();
|
|
272
|
+
let { name, version, by } = queue.shift();
|
|
199
273
|
|
|
200
274
|
// Lock file always wins — use pinned version regardless of what was requested.
|
|
201
275
|
if (lockedVersions.has(name)) {
|
|
@@ -214,29 +288,108 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
|
|
|
214
288
|
}
|
|
215
289
|
}
|
|
216
290
|
|
|
291
|
+
// Parse the version spec. Ranges (`^`, `~`, explicit comparators) resolve
|
|
292
|
+
// to an exact version via `fetchPublishedVersions` + highest-in-range;
|
|
293
|
+
// intersected against any previously-seen range for the same package.
|
|
294
|
+
let parsedSpec;
|
|
295
|
+
try {
|
|
296
|
+
parsedSpec = parseRange(version);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
log.error(`Dependency "${name}": ${err.message}`);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (parsedSpec.kind === 'linked') {
|
|
303
|
+
// Linked packages bypass range resolution entirely.
|
|
304
|
+
parsedSpec = { kind: 'exact', version };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Intersect with any previously-seen range for this package.
|
|
308
|
+
//
|
|
309
|
+
// Backward-compat nuance: when both the previous and current spec are
|
|
310
|
+
// exact but differ, we preserve the legacy "pick higher with warning"
|
|
311
|
+
// behaviour rather than erroring. Intersection-strict conflict only
|
|
312
|
+
// applies when at least one side is an actual range — ranges are
|
|
313
|
+
// opt-in new territory where a declared range that cannot be satisfied
|
|
314
|
+
// IS a bug, not a soft conflict.
|
|
315
|
+
let effective = parsedSpec;
|
|
316
|
+
const prev = effectiveRange.get(name);
|
|
317
|
+
if (prev) {
|
|
318
|
+
const bothExact = prev.kind === 'exact' && parsedSpec.kind === 'exact';
|
|
319
|
+
if (bothExact) {
|
|
320
|
+
// Legacy behaviour: keep the HIGHER of the two exact versions.
|
|
321
|
+
// The warning is emitted in the "already resolved" branch below.
|
|
322
|
+
effective = compareVersions(parsedSpec.version, prev.version) > 0
|
|
323
|
+
? parsedSpec
|
|
324
|
+
: prev;
|
|
325
|
+
} else {
|
|
326
|
+
const combined = intersectRanges(prev, parsedSpec);
|
|
327
|
+
if (!combined) {
|
|
328
|
+
const sources = rangeContributors.get(name) ?? [];
|
|
329
|
+
const lines = sources.map((c) => ` required by ${c.by}: ${c.spec}`);
|
|
330
|
+
lines.push(` required by ${by}: ${version}`);
|
|
331
|
+
log.error(
|
|
332
|
+
`cannot satisfy conflicting version ranges for "${name}":\n` +
|
|
333
|
+
lines.join('\n') + '\n' +
|
|
334
|
+
` no version satisfies all ranges`,
|
|
335
|
+
);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
effective = combined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
effectiveRange.set(name, effective);
|
|
342
|
+
if (!rangeContributors.has(name)) rangeContributors.set(name, []);
|
|
343
|
+
rangeContributors.get(name).push({ by, spec: version });
|
|
344
|
+
|
|
345
|
+
// Resolve the (possibly intersected) spec down to an exact version.
|
|
346
|
+
let exactVersion;
|
|
347
|
+
if (effective.kind === 'exact') {
|
|
348
|
+
exactVersion = effective.version;
|
|
349
|
+
} else {
|
|
350
|
+
const published = await fetchPublished(name);
|
|
351
|
+
const picked = pickHighestInRange(published, effective);
|
|
352
|
+
if (!picked) {
|
|
353
|
+
log.error(
|
|
354
|
+
`no published version of "${name}" satisfies ${rangeToString(effective)}\n` +
|
|
355
|
+
(published.length > 0
|
|
356
|
+
? ` published versions: ${published.join(', ')}`
|
|
357
|
+
: ` no versions published on the configured sources`),
|
|
358
|
+
);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
exactVersion = picked;
|
|
362
|
+
if (!prev) log.info(`Resolved "${name}@${version}" → ${exactVersion}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
217
365
|
if (resolved.has(name)) {
|
|
218
366
|
const existing = resolved.get(name);
|
|
219
|
-
if (existing ===
|
|
367
|
+
if (existing === exactVersion) {
|
|
220
368
|
continue;
|
|
221
369
|
}
|
|
222
370
|
// Locked packages never lose to a conflict.
|
|
223
371
|
if (lockedVersions.has(name)) {
|
|
224
372
|
continue;
|
|
225
373
|
}
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
374
|
+
// After range-intersection resolution, we prefer the
|
|
375
|
+
// intersected-range's highest over a naive version bump — the
|
|
376
|
+
// intersection already enforces "both contributors agree", so
|
|
377
|
+
// trust it.
|
|
378
|
+
if (exactVersion !== existing) {
|
|
229
379
|
log.warn(
|
|
230
|
-
`Version conflict for "${name}": ${existing} vs ${
|
|
380
|
+
`Version conflict for "${name}": ${existing} vs ${exactVersion} — using ${exactVersion}`,
|
|
231
381
|
);
|
|
232
|
-
resolved.set(name,
|
|
382
|
+
resolved.set(name, exactVersion);
|
|
233
383
|
// Re-fetch the winning version's manifest to pull in its transitive deps.
|
|
234
|
-
queue.push({ name, version:
|
|
384
|
+
queue.push({ name, version: exactVersion, by });
|
|
235
385
|
}
|
|
236
386
|
continue;
|
|
237
387
|
}
|
|
238
388
|
|
|
239
|
-
resolved.set(name,
|
|
389
|
+
resolved.set(name, exactVersion);
|
|
390
|
+
// Shadow `version` with the resolved exact for the rest of this block
|
|
391
|
+
// — linked handling, manifest fetch, etc. all expect an exact string.
|
|
392
|
+
version = exactVersion;
|
|
240
393
|
|
|
241
394
|
// Handle linked packages: read local manifest instead of fetching from remote
|
|
242
395
|
let manifest;
|
|
@@ -263,6 +416,10 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
|
|
|
263
416
|
continue;
|
|
264
417
|
}
|
|
265
418
|
|
|
419
|
+
if (manifestsOut instanceof Map) {
|
|
420
|
+
manifestsOut.set(name, manifest);
|
|
421
|
+
}
|
|
422
|
+
|
|
266
423
|
// Support both formats:
|
|
267
424
|
// PascalCase array : { Dependencies: [ { Name, Version }, ... ] }
|
|
268
425
|
// string object : { dependencies: { name: "version", ... } }
|
|
@@ -281,7 +438,7 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
|
|
|
281
438
|
}));
|
|
282
439
|
}
|
|
283
440
|
for (const dep of deps) {
|
|
284
|
-
if (dep.version) queue.push({ name: dep.name, version: dep.version });
|
|
441
|
+
if (dep.version) queue.push({ name: dep.name, version: dep.version, by: `${name}@${version}` });
|
|
285
442
|
}
|
|
286
443
|
}
|
|
287
444
|
|
|
@@ -292,6 +449,7 @@ module.exports = {
|
|
|
292
449
|
compareVersions,
|
|
293
450
|
resolveLatestVersion,
|
|
294
451
|
resolveDependencies,
|
|
452
|
+
fetchPublishedVersions,
|
|
295
453
|
isLinkedVersion,
|
|
296
454
|
getLinkedPath,
|
|
297
455
|
};
|
package/src/toolchain/presets.js
CHANGED
|
@@ -33,22 +33,64 @@ function isWyvrnGenerated(presets) {
|
|
|
33
33
|
/**
|
|
34
34
|
* Build the configure preset object for the active profile.
|
|
35
35
|
*
|
|
36
|
+
* When the project has its own `build` recipe (i.e. it's a publishable
|
|
37
|
+
* library with `build.generator` / `build.configure` declared), those are
|
|
38
|
+
* propagated into the generated preset so `wyvrnpm build` locally honours
|
|
39
|
+
* the same knobs the source-build + publish paths use. Without this, a
|
|
40
|
+
* publisher's recipe is effectively advisory at install-time (F15 fix).
|
|
41
|
+
*
|
|
36
42
|
* @param {object} args
|
|
37
43
|
* @param {string} args.profileName
|
|
38
44
|
* @param {string} args.profileHash
|
|
39
|
-
* @param {string} args.toolchainRelPath
|
|
40
|
-
*
|
|
45
|
+
* @param {string} args.toolchainRelPath Relative from project root, CMake-style
|
|
46
|
+
* forward slashes.
|
|
47
|
+
* @param {string|null} [args.generator] Single generator name to pin on the
|
|
48
|
+
* preset (e.g. "Ninja Multi-Config").
|
|
49
|
+
* Null / undefined → let CMake pick.
|
|
50
|
+
* @param {Record<string,string>|null} [args.extraCacheVariables]
|
|
51
|
+
* Merged into cacheVariables alongside
|
|
52
|
+
* `CMAKE_TOOLCHAIN_FILE`. Usually
|
|
53
|
+
* derived from the project's own
|
|
54
|
+
* `build.configure` -D args.
|
|
41
55
|
*/
|
|
42
|
-
function buildConfigurePreset({
|
|
43
|
-
|
|
56
|
+
function buildConfigurePreset({
|
|
57
|
+
profileName,
|
|
58
|
+
profileHash,
|
|
59
|
+
toolchainRelPath,
|
|
60
|
+
generator = null,
|
|
61
|
+
extraCacheVariables = null,
|
|
62
|
+
installDir = null,
|
|
63
|
+
}) {
|
|
64
|
+
const binaryDir = `\${sourceDir}/build/wyvrn-${profileName}`;
|
|
65
|
+
// Spread `extraCacheVariables` FIRST, then the wyvrn-owned entries. This
|
|
66
|
+
// means the recipe cannot redirect `CMAKE_TOOLCHAIN_FILE` — our dep
|
|
67
|
+
// injection is always the authoritative toolchain pointer. A malicious
|
|
68
|
+
// or sloppy recipe trying to override it is silently ignored, not
|
|
69
|
+
// quietly obeyed. Same protection applies to `CMAKE_INSTALL_PREFIX`:
|
|
70
|
+
// if a recipe declares `build.installDir`, we resolve it under the
|
|
71
|
+
// binary dir; any cacheVariables override is overwritten below.
|
|
72
|
+
const cacheVariables = {
|
|
73
|
+
...(extraCacheVariables ?? {}),
|
|
74
|
+
CMAKE_TOOLCHAIN_FILE: `\${sourceDir}/${toolchainRelPath}`,
|
|
75
|
+
};
|
|
76
|
+
if (installDir) {
|
|
77
|
+
// `installDir` in wyvrn.json is defined as relative to the binary dir
|
|
78
|
+
// (same semantics as the source-build path, which passes
|
|
79
|
+
// -DCMAKE_INSTALL_PREFIX=<buildDir>/<installDir>). Without this, a
|
|
80
|
+
// `wyvrnpm build --install` falls back to CMake's system default —
|
|
81
|
+
// on Windows that's `C:/Program Files (x86)/<Project>`, which needs
|
|
82
|
+
// admin rights and almost always fails.
|
|
83
|
+
cacheVariables.CMAKE_INSTALL_PREFIX = `${binaryDir}/${installDir}`;
|
|
84
|
+
}
|
|
85
|
+
const preset = {
|
|
44
86
|
name: `wyvrn-${profileName}`,
|
|
45
87
|
displayName: `wyvrnpm (${profileName})`,
|
|
46
88
|
description: `Generated by wyvrnpm install — profile hash ${profileHash}`,
|
|
47
|
-
binaryDir
|
|
48
|
-
cacheVariables
|
|
49
|
-
CMAKE_TOOLCHAIN_FILE: `\${sourceDir}/${toolchainRelPath}`,
|
|
50
|
-
},
|
|
89
|
+
binaryDir,
|
|
90
|
+
cacheVariables,
|
|
51
91
|
};
|
|
92
|
+
if (generator) preset.generator = generator;
|
|
93
|
+
return preset;
|
|
52
94
|
}
|
|
53
95
|
|
|
54
96
|
/**
|
|
@@ -78,7 +120,14 @@ function buildBuildPresets(profileName) {
|
|
|
78
120
|
/**
|
|
79
121
|
* Build a fresh presets file from scratch (no prior content to merge).
|
|
80
122
|
*/
|
|
81
|
-
function buildFreshPresetsFile({
|
|
123
|
+
function buildFreshPresetsFile({
|
|
124
|
+
profileName,
|
|
125
|
+
profileHash,
|
|
126
|
+
toolchainRelPath,
|
|
127
|
+
generator = null,
|
|
128
|
+
extraCacheVariables = null,
|
|
129
|
+
installDir = null,
|
|
130
|
+
}) {
|
|
82
131
|
return {
|
|
83
132
|
version: SCHEMA_VERSION,
|
|
84
133
|
vendor: {
|
|
@@ -88,8 +137,10 @@ function buildFreshPresetsFile({ profileName, profileHash, toolchainRelPath }) {
|
|
|
88
137
|
generatedAt: new Date().toISOString(),
|
|
89
138
|
},
|
|
90
139
|
},
|
|
91
|
-
configurePresets: [buildConfigurePreset({
|
|
92
|
-
|
|
140
|
+
configurePresets: [buildConfigurePreset({
|
|
141
|
+
profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir,
|
|
142
|
+
})],
|
|
143
|
+
buildPresets: buildBuildPresets(profileName),
|
|
93
144
|
};
|
|
94
145
|
}
|
|
95
146
|
|
|
@@ -103,8 +154,17 @@ function buildFreshPresetsFile({ profileName, profileHash, toolchainRelPath }) {
|
|
|
103
154
|
* @param {object} args forwarded to the preset builders
|
|
104
155
|
* @returns {object}
|
|
105
156
|
*/
|
|
106
|
-
function mergeIntoExisting(existing, {
|
|
107
|
-
|
|
157
|
+
function mergeIntoExisting(existing, {
|
|
158
|
+
profileName,
|
|
159
|
+
profileHash,
|
|
160
|
+
toolchainRelPath,
|
|
161
|
+
generator = null,
|
|
162
|
+
extraCacheVariables = null,
|
|
163
|
+
installDir = null,
|
|
164
|
+
}) {
|
|
165
|
+
const newConfigure = buildConfigurePreset({
|
|
166
|
+
profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir,
|
|
167
|
+
});
|
|
108
168
|
const newBuilds = buildBuildPresets(profileName);
|
|
109
169
|
const newConfigureNames = new Set([newConfigure.name]);
|
|
110
170
|
const newBuildNames = new Set(newBuilds.map((b) => b.name));
|
|
@@ -150,9 +210,21 @@ function mergeIntoExisting(existing, { profileName, profileHash, toolchainRelPat
|
|
|
150
210
|
* @param {string} args.profileName
|
|
151
211
|
* @param {string} args.profileHash
|
|
152
212
|
* @param {string} args.toolchainRelPath CMake-style forward slashes, relative to projectRoot.
|
|
213
|
+
* @param {string|null} [args.generator] Propagated onto the configure preset's `generator` field.
|
|
214
|
+
* @param {Record<string,string>|null} [args.extraCacheVariables]
|
|
215
|
+
* Merged into the configure preset's `cacheVariables`.
|
|
153
216
|
* @returns {{ path: string|null, action: 'created'|'updated'|'skipped', isUser: boolean }}
|
|
154
217
|
*/
|
|
155
|
-
function generateCMakePresets({
|
|
218
|
+
function generateCMakePresets({
|
|
219
|
+
projectRoot,
|
|
220
|
+
profileName,
|
|
221
|
+
profileHash,
|
|
222
|
+
toolchainRelPath,
|
|
223
|
+
generator = null,
|
|
224
|
+
extraCacheVariables = null,
|
|
225
|
+
installDir = null,
|
|
226
|
+
}) {
|
|
227
|
+
const args = { profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir };
|
|
156
228
|
const candidates = [
|
|
157
229
|
{ filePath: path.join(projectRoot, 'CMakePresets.json'), isUser: false },
|
|
158
230
|
{ filePath: path.join(projectRoot, 'CMakeUserPresets.json'), isUser: true },
|
|
@@ -160,14 +232,14 @@ function generateCMakePresets({ projectRoot, profileName, profileHash, toolchain
|
|
|
160
232
|
|
|
161
233
|
for (const { filePath, isUser } of candidates) {
|
|
162
234
|
if (!fs.existsSync(filePath)) {
|
|
163
|
-
const content = buildFreshPresetsFile(
|
|
235
|
+
const content = buildFreshPresetsFile(args);
|
|
164
236
|
fs.writeFileSync(filePath, JSON.stringify(content, null, 2) + '\n', 'utf8');
|
|
165
237
|
return { path: filePath, action: 'created', isUser };
|
|
166
238
|
}
|
|
167
239
|
|
|
168
240
|
const existing = safeReadJson(filePath);
|
|
169
241
|
if (isWyvrnGenerated(existing)) {
|
|
170
|
-
const merged = mergeIntoExisting(existing,
|
|
242
|
+
const merged = mergeIntoExisting(existing, args);
|
|
171
243
|
fs.writeFileSync(filePath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
172
244
|
return { path: filePath, action: 'updated', isUser };
|
|
173
245
|
}
|