wyvrnpm 2.4.1 → 2.9.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 +414 -16
- package/bin/wyvrn.js +154 -11
- package/claude/skills/wyvrnpm.skill +0 -0
- package/cmake/variables.cmake +13 -3
- package/package.json +1 -1
- package/src/auth.js +66 -0
- package/src/build/cache.js +196 -0
- package/src/build/index.js +26 -4
- package/src/build/msvc-env.js +50 -7
- package/src/build/recipe.js +36 -3
- package/src/commands/add.js +2 -1
- package/src/commands/build.js +162 -36
- package/src/commands/cache.js +189 -0
- package/src/commands/configure.js +15 -4
- package/src/commands/init.js +36 -0
- package/src/commands/install-skill.js +8 -0
- package/src/commands/install.js +674 -400
- package/src/commands/link.js +320 -319
- package/src/commands/profile.js +237 -174
- package/src/commands/publish.js +435 -383
- package/src/commands/show.js +24 -9
- package/src/conf/index.js +391 -0
- package/src/conf/namespaces.js +94 -0
- package/src/context.js +230 -0
- package/src/download.js +135 -147
- package/src/http-fetch.js +53 -0
- package/src/logger.js +30 -2
- package/src/manifest.js +34 -7
- package/src/profile.js +148 -12
- package/src/providers/file.js +5 -19
- package/src/providers/http.js +26 -26
- package/src/providers/s3.js +29 -25
- package/src/resolve.js +47 -7
- package/src/settings-overrides.js +152 -0
- package/src/toolchain/presets.js +71 -31
- package/src/upload-built.js +11 -2
- package/src/zip-safe.js +52 -0
package/src/providers/s3.js
CHANGED
|
@@ -9,21 +9,13 @@ const RE_S3_URI = /^s3:\/\//;
|
|
|
9
9
|
const RE_S3_VIRTUAL_HOSTED = /^https?:\/\/([^.]+)\.s3[.-][^/]*\.amazonaws\.com/;
|
|
10
10
|
const RE_S3_PATH_STYLE = /^https?:\/\/s3[.-][^/]*\.amazonaws\.com\/([^/]+)/;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
? { ...value, version: locked }
|
|
20
|
-
: locked;
|
|
21
|
-
} else {
|
|
22
|
-
result[pkgName] = value;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return result;
|
|
26
|
-
}
|
|
12
|
+
// CloudFront TTL cap for mutable manifest JSON (versions.json, latest.json)
|
|
13
|
+
// and the per-profile wyvrn.json / source.json. 5 min is short enough that
|
|
14
|
+
// a publish is visible to `install` / `add` well within a coffee break, but
|
|
15
|
+
// long enough that `install` at scale doesn't hammer the origin. Applied
|
|
16
|
+
// on every JSON PUT — consumers of the ZIPs get default (long) TTLs since
|
|
17
|
+
// a (name, version, profileHash) artefact is immutable by construction.
|
|
18
|
+
const JSON_CACHE_CONTROL = 'public, max-age=300';
|
|
27
19
|
|
|
28
20
|
class S3Provider extends BaseProvider {
|
|
29
21
|
static canHandle(source) {
|
|
@@ -76,25 +68,36 @@ class S3Provider extends BaseProvider {
|
|
|
76
68
|
|
|
77
69
|
_makeClient(awsProfile) {
|
|
78
70
|
const { S3Client, fromSSO } = this._loadSdk();
|
|
79
|
-
|
|
71
|
+
if (!awsProfile) return new S3Client({});
|
|
72
|
+
|
|
73
|
+
// fromSSO supplies credentials only. The SDK's region resolver is
|
|
74
|
+
// independent and consults AWS_REGION / AWS_DEFAULT_REGION / the profile
|
|
75
|
+
// named by AWS_PROFILE — NOT the profile handed to fromSSO. Propagate
|
|
76
|
+
// --aws-profile so its `region = …` line in ~/.aws/config is the fallback.
|
|
77
|
+
process.env.AWS_PROFILE = awsProfile;
|
|
78
|
+
return new S3Client({ credentials: fromSSO({ profile: awsProfile }) });
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
async _putFile(client, bucket, key, filePath, contentType, PutObjectCommand) {
|
|
83
82
|
log.info(` → s3://${bucket}/${key}`);
|
|
84
|
-
|
|
83
|
+
const params = {
|
|
85
84
|
Bucket: bucket, Key: key,
|
|
86
85
|
Body: fs.readFileSync(filePath),
|
|
87
86
|
ContentType: contentType,
|
|
88
|
-
}
|
|
87
|
+
};
|
|
88
|
+
if (contentType === 'application/json') params.CacheControl = JSON_CACHE_CONTROL;
|
|
89
|
+
await client.send(new PutObjectCommand(params));
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
async _putBuffer(client, bucket, key, content, contentType, PutObjectCommand) {
|
|
92
93
|
log.info(` → s3://${bucket}/${key}`);
|
|
93
|
-
|
|
94
|
+
const params = {
|
|
94
95
|
Bucket: bucket, Key: key,
|
|
95
96
|
Body: content,
|
|
96
97
|
ContentType: contentType,
|
|
97
|
-
}
|
|
98
|
+
};
|
|
99
|
+
if (contentType === 'application/json') params.CacheControl = JSON_CACHE_CONTROL;
|
|
100
|
+
await client.send(new PutObjectCommand(params));
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
async _getJson(client, bucket, key, GetObjectCommand) {
|
|
@@ -177,7 +180,7 @@ class S3Provider extends BaseProvider {
|
|
|
177
180
|
async v2Publish(files, options) {
|
|
178
181
|
const {
|
|
179
182
|
source, name, version, profileHash, buildSettings,
|
|
180
|
-
contentSha256, gitSha, gitRepo,
|
|
183
|
+
contentSha256, gitSha, gitRepo, publishedLock, awsProfile,
|
|
181
184
|
updateLatest = true, uploadedBy,
|
|
182
185
|
} = options;
|
|
183
186
|
const { PutObjectCommand, GetObjectCommand } = this._loadSdk();
|
|
@@ -190,14 +193,14 @@ class S3Provider extends BaseProvider {
|
|
|
190
193
|
// 1) Upload zip
|
|
191
194
|
await this._putFile(client, bucket, `${buildRoot}/wyvrn.zip`, files.zip, 'application/zip', PutObjectCommand);
|
|
192
195
|
|
|
193
|
-
// 2) Upload enriched wyvrn.json
|
|
196
|
+
// 2) Upload enriched wyvrn.json.
|
|
194
197
|
// buildSettings is stored for debugging purposes only.
|
|
195
|
-
// Dependencies are
|
|
198
|
+
// Dependencies are published verbatim from wyvrn.json — ranges stay as
|
|
199
|
+
// ranges so consumers can intersect them with their own. The author's
|
|
200
|
+
// lockfile snapshot is preserved in `publishedLock` for audit.
|
|
196
201
|
const rawManifest = JSON.parse(fs.readFileSync(files.manifest, 'utf8'));
|
|
197
|
-
const pinnedDeps = pinDependencies(rawManifest.dependencies, lockedDependencies);
|
|
198
202
|
const v2Meta = {
|
|
199
203
|
...rawManifest,
|
|
200
|
-
...(pinnedDeps !== undefined ? { dependencies: pinnedDeps } : {}),
|
|
201
204
|
schemaVersion: 2,
|
|
202
205
|
profileHash,
|
|
203
206
|
buildSettings,
|
|
@@ -205,6 +208,7 @@ class S3Provider extends BaseProvider {
|
|
|
205
208
|
gitSha: gitSha ?? null,
|
|
206
209
|
gitRepo: gitRepo ?? null,
|
|
207
210
|
publishedAt: new Date().toISOString(),
|
|
211
|
+
...(publishedLock && Object.keys(publishedLock).length > 0 ? { publishedLock } : {}),
|
|
208
212
|
...(uploadedBy ? { uploadedBy } : {}),
|
|
209
213
|
};
|
|
210
214
|
await this._putBuffer(
|
package/src/resolve.js
CHANGED
|
@@ -141,15 +141,27 @@ async function resolveLatestVersion(name, packageSources, platform, httpClient)
|
|
|
141
141
|
/**
|
|
142
142
|
* Fetches razer.json for a single package from the first source that responds.
|
|
143
143
|
*
|
|
144
|
+
* When `preferredProfileHash` is provided (EVALUATION.md F13 — closes the
|
|
145
|
+
* "picks arbitrary profileHash" latent bug), the v2 lookup tries that hash
|
|
146
|
+
* first and only falls back to the first-available profile if the preferred
|
|
147
|
+
* hash isn't on the registry. Dependency declarations are invariant across
|
|
148
|
+
* profiles today, so picking any hash gives the same answer — but the
|
|
149
|
+
* consumer's own hash is stable across re-resolves and future-proofs
|
|
150
|
+
* against any schema that starts varying wyvrn.json per profile.
|
|
151
|
+
*
|
|
144
152
|
* @param {string} name
|
|
145
153
|
* @param {string} version
|
|
146
154
|
* @param {string[]} packageSources
|
|
147
155
|
* @param {string} platform
|
|
148
156
|
* @param {Function} httpClient
|
|
149
157
|
* @param {Map<string, object>} cache - In-flight / completed fetch cache.
|
|
158
|
+
* @param {string|null} [preferredProfileHash] - try this hash first.
|
|
150
159
|
* @returns {Promise<object|null>} Parsed razer.json or null if not found anywhere.
|
|
151
160
|
*/
|
|
152
|
-
async function fetchPackageManifest(
|
|
161
|
+
async function fetchPackageManifest(
|
|
162
|
+
name, version, packageSources, platform, httpClient, cache,
|
|
163
|
+
preferredProfileHash = null,
|
|
164
|
+
) {
|
|
153
165
|
const cacheKey = `${name}@${version}`;
|
|
154
166
|
if (cache.has(cacheKey)) {
|
|
155
167
|
return cache.get(cacheKey);
|
|
@@ -158,14 +170,19 @@ async function fetchPackageManifest(name, version, packageSources, platform, htt
|
|
|
158
170
|
for (const source of packageSources) {
|
|
159
171
|
const base = source.replace(/\/$/, '');
|
|
160
172
|
|
|
161
|
-
// v2:
|
|
173
|
+
// v2: prefer the consumer's own profileHash; fall back to the first
|
|
174
|
+
// available if that hash isn't published.
|
|
162
175
|
try {
|
|
163
176
|
const idxResp = await httpClient(`${base}/v2/${name}/versions.json`);
|
|
164
177
|
if (idxResp.ok) {
|
|
165
178
|
const idx = await idxResp.json();
|
|
166
179
|
const profiles = idx?.versions?.[version]?.profiles;
|
|
167
180
|
if (profiles) {
|
|
168
|
-
const
|
|
181
|
+
const available = Object.keys(profiles);
|
|
182
|
+
const profileHash =
|
|
183
|
+
(preferredProfileHash && available.includes(preferredProfileHash))
|
|
184
|
+
? preferredProfileHash
|
|
185
|
+
: available[0];
|
|
169
186
|
if (profileHash) {
|
|
170
187
|
const mResp = await httpClient(`${base}/v2/${name}/${version}/${profileHash}/wyvrn.json`);
|
|
171
188
|
if (mResp.ok) {
|
|
@@ -215,15 +232,37 @@ async function fetchPackageManifest(name, version, packageSources, platform, htt
|
|
|
215
232
|
* PLAN-OPTIONS) use this to avoid a second round of HTTP round-trips.
|
|
216
233
|
* Passing `null` / omitting it keeps the legacy behaviour.
|
|
217
234
|
*
|
|
235
|
+
* Per-package source pinning (EVALUATION.md S4): when `pinnedSources` is
|
|
236
|
+
* provided, any entry `name → url` restricts that package's manifest +
|
|
237
|
+
* latest + versions lookups to just that URL — typo-squatting and mirror
|
|
238
|
+
* shadowing are closed for pinned deps. Pins propagate to direct deps
|
|
239
|
+
* only; transitive deps continue to fan out across all sources (a pinned
|
|
240
|
+
* dep's own recipe is authoritative for its own transitives).
|
|
241
|
+
*
|
|
218
242
|
* @param {Record<string, string>} rootDeps - Direct dependencies { name: version }.
|
|
219
243
|
* @param {string[]} packageSources - Ordered list of base URLs to try.
|
|
220
244
|
* @param {string} platform - Target platform string (e.g. "win_x64").
|
|
221
245
|
* @param {Function} httpClient - fetch-compatible function.
|
|
222
246
|
* @param {Map<string, string>} [lockedVersions]
|
|
223
247
|
* @param {Map<string, object>} [manifestsOut] - populated in-place when provided
|
|
248
|
+
* @param {string|null} [preferredProfileHash] - consumer's own profileHash;
|
|
249
|
+
* threaded to fetchPackageManifest so v2 manifest lookups prefer the
|
|
250
|
+
* consumer's own build entry over an arbitrary one (EVALUATION.md F13).
|
|
251
|
+
* @param {Map<string,string>|null} [pinnedSources] - per-package source URL pin (S4).
|
|
224
252
|
* @returns {Promise<Map<string, string>>} Resolved map of name -> version.
|
|
225
253
|
*/
|
|
226
|
-
async function resolveDependencies(
|
|
254
|
+
async function resolveDependencies(
|
|
255
|
+
rootDeps, packageSources, platform, httpClient,
|
|
256
|
+
lockedVersions = new Map(), manifestsOut = null,
|
|
257
|
+
preferredProfileHash = null,
|
|
258
|
+
pinnedSources = null,
|
|
259
|
+
) {
|
|
260
|
+
// For pinned deps, restrict the source list to the single pinned URL.
|
|
261
|
+
// `null` / no pin → fall back to the full caller-supplied list.
|
|
262
|
+
const sourcesFor = (name) => {
|
|
263
|
+
const pin = pinnedSources?.get?.(name);
|
|
264
|
+
return pin ? [pin] : packageSources;
|
|
265
|
+
};
|
|
227
266
|
/** @type {Map<string, string>} Final resolved versions. */
|
|
228
267
|
const resolved = new Map();
|
|
229
268
|
|
|
@@ -254,7 +293,7 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
|
|
|
254
293
|
const publishedCache = new Map();
|
|
255
294
|
const fetchPublished = async (name) => {
|
|
256
295
|
if (publishedCache.has(name)) return publishedCache.get(name);
|
|
257
|
-
const list = await fetchPublishedVersions(name,
|
|
296
|
+
const list = await fetchPublishedVersions(name, sourcesFor(name), httpClient);
|
|
258
297
|
publishedCache.set(name, list);
|
|
259
298
|
return list;
|
|
260
299
|
};
|
|
@@ -280,7 +319,7 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
|
|
|
280
319
|
// Skip this for linked packages which have "linked:" prefix.
|
|
281
320
|
if (version === 'latest') {
|
|
282
321
|
try {
|
|
283
|
-
version = await resolveLatestVersion(name,
|
|
322
|
+
version = await resolveLatestVersion(name, sourcesFor(name), platform, httpClient);
|
|
284
323
|
log.info(`Resolved "${name}@latest" → ${version}`);
|
|
285
324
|
} catch (err) {
|
|
286
325
|
log.warn(err.message);
|
|
@@ -405,10 +444,11 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
|
|
|
405
444
|
manifest = await fetchPackageManifest(
|
|
406
445
|
name,
|
|
407
446
|
version,
|
|
408
|
-
|
|
447
|
+
sourcesFor(name),
|
|
409
448
|
platform,
|
|
410
449
|
httpClient,
|
|
411
450
|
manifestCache,
|
|
451
|
+
preferredProfileHash,
|
|
412
452
|
);
|
|
413
453
|
}
|
|
414
454
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Pattern-based settings overrides (EVALUATION.md F11).
|
|
4
|
+
//
|
|
5
|
+
// `wyvrn.json` may declare a top-level `settingsOverrides` block whose
|
|
6
|
+
// keys are glob patterns over dependency names. Every matching pattern
|
|
7
|
+
// contributes to the resolved dep's `settings` merge; the per-dep
|
|
8
|
+
// literal settings (inside `dependencies`) still win over patterns.
|
|
9
|
+
//
|
|
10
|
+
// Conan users know this feature as profile-side `boost*:compiler.cppstd=17`.
|
|
11
|
+
// Ours lives in the manifest because that's where literal settings
|
|
12
|
+
// overrides already live — one fewer concept for recipe authors.
|
|
13
|
+
//
|
|
14
|
+
// Pure, no I/O. Exported helpers:
|
|
15
|
+
// - normalizeSettingsOverrides: validate + sort by specificity
|
|
16
|
+
// - resolveOverrideSettings: compute the merged settings for a dep name
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Valid profile field keys that settings overrides may target.
|
|
20
|
+
* Mirrors `HASHED_PROFILE_FIELDS` in src/profile.js, but as the allow-
|
|
21
|
+
* list for OVERRIDE values (os/arch/compiler/etc.). If a pattern
|
|
22
|
+
* override tries to set something else, reject at parse time rather
|
|
23
|
+
* than silently letting it through into mergeProfile.
|
|
24
|
+
*/
|
|
25
|
+
const ALLOWED_SETTINGS_KEYS = new Set([
|
|
26
|
+
'os', 'arch', 'compiler', 'compiler.version', 'compiler.cppstd', 'compiler.runtime',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Convert a glob pattern to a RegExp. Supports `*` (any chars),
|
|
31
|
+
* `?` (single char), literal everything else. No brace expansion,
|
|
32
|
+
* no negation — keep the mental model small.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} glob
|
|
35
|
+
* @returns {RegExp}
|
|
36
|
+
*/
|
|
37
|
+
function globToRegExp(glob) {
|
|
38
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
39
|
+
const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
40
|
+
return new RegExp(`^${pattern}$`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Specificity heuristic: fewer wildcards = more specific. Ties broken
|
|
45
|
+
* by longer literal prefix, then lexical order for determinism. More
|
|
46
|
+
* specific patterns get applied LAST so they override less-specific
|
|
47
|
+
* ones in the merge.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} glob
|
|
50
|
+
* @returns {[number, number, string]} sort key — lower = less specific
|
|
51
|
+
*/
|
|
52
|
+
function specificityKey(glob) {
|
|
53
|
+
const wildcards = (glob.match(/[*?]/g) ?? []).length;
|
|
54
|
+
const literalPrefix = glob.match(/^[^*?]*/)?.[0].length ?? 0;
|
|
55
|
+
// Invert sort order: more wildcards → lower score; more prefix → higher score.
|
|
56
|
+
return [-wildcards, literalPrefix, glob];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate + sort a raw `settingsOverrides` block. Returns an array of
|
|
61
|
+
* `{ pattern, regex, settings }` in application order (least-specific
|
|
62
|
+
* first). A string pattern alone (literal `"openssl"`) works too — it
|
|
63
|
+
* matches only that exact name.
|
|
64
|
+
*
|
|
65
|
+
* Throws on:
|
|
66
|
+
* - non-object block
|
|
67
|
+
* - non-string keys (JSON can't produce these but defensive)
|
|
68
|
+
* - non-object values
|
|
69
|
+
* - empty pattern strings
|
|
70
|
+
* - unknown settings keys in a value
|
|
71
|
+
* - non-string value leaves (settings values must be strings like
|
|
72
|
+
* the profile itself: "dynamic", "17", "gcc")
|
|
73
|
+
*
|
|
74
|
+
* @param {unknown} block
|
|
75
|
+
* @returns {Array<{ pattern: string, regex: RegExp, settings: Record<string, string> }>}
|
|
76
|
+
*/
|
|
77
|
+
function normalizeSettingsOverrides(block) {
|
|
78
|
+
if (block === undefined || block === null) return [];
|
|
79
|
+
if (typeof block !== 'object' || Array.isArray(block)) {
|
|
80
|
+
throw new Error('settingsOverrides must be an object mapping globs to settings');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const entries = [];
|
|
84
|
+
for (const [rawPattern, rawValue] of Object.entries(block)) {
|
|
85
|
+
if (typeof rawPattern !== 'string' || rawPattern.length === 0) {
|
|
86
|
+
throw new Error(`settingsOverrides: pattern must be a non-empty string, got ${JSON.stringify(rawPattern)}`);
|
|
87
|
+
}
|
|
88
|
+
if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) {
|
|
89
|
+
throw new Error(`settingsOverrides["${rawPattern}"]: value must be an object of settings`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const settings = {};
|
|
93
|
+
for (const [k, v] of Object.entries(rawValue)) {
|
|
94
|
+
if (!ALLOWED_SETTINGS_KEYS.has(k)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`settingsOverrides["${rawPattern}"]: unknown key "${k}" — ` +
|
|
97
|
+
`allowed: ${[...ALLOWED_SETTINGS_KEYS].join(', ')}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (typeof v !== 'string' || v.length === 0) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`settingsOverrides["${rawPattern}"].${k}: value must be a non-empty string, ` +
|
|
103
|
+
`got ${JSON.stringify(v)}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
settings[k] = v;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
entries.push({ pattern: rawPattern, regex: globToRegExp(rawPattern), settings });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Sort by specificity ascending so more-specific patterns apply LAST
|
|
113
|
+
// (their settings therefore win on per-key merge).
|
|
114
|
+
entries.sort((a, b) => {
|
|
115
|
+
const ka = specificityKey(a.pattern);
|
|
116
|
+
const kb = specificityKey(b.pattern);
|
|
117
|
+
for (let i = 0; i < ka.length; i++) {
|
|
118
|
+
if (ka[i] < kb[i]) return -1;
|
|
119
|
+
if (ka[i] > kb[i]) return 1;
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return entries;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Merge all matching pattern overrides for `depName` into a flat
|
|
129
|
+
* settings object. Less-specific patterns apply first; more-specific
|
|
130
|
+
* patterns overlay on top. Does NOT include the literal per-dep
|
|
131
|
+
* `settings` (caller layers that on after — literal always wins).
|
|
132
|
+
*
|
|
133
|
+
* @param {string} depName
|
|
134
|
+
* @param {ReturnType<typeof normalizeSettingsOverrides>} normalized
|
|
135
|
+
* @returns {Record<string, string>}
|
|
136
|
+
*/
|
|
137
|
+
function resolveOverrideSettings(depName, normalized) {
|
|
138
|
+
const out = {};
|
|
139
|
+
for (const entry of normalized) {
|
|
140
|
+
if (entry.regex.test(depName)) {
|
|
141
|
+
Object.assign(out, entry.settings);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
ALLOWED_SETTINGS_KEYS,
|
|
149
|
+
globToRegExp,
|
|
150
|
+
normalizeSettingsOverrides,
|
|
151
|
+
resolveOverrideSettings,
|
|
152
|
+
};
|
package/src/toolchain/presets.js
CHANGED
|
@@ -72,6 +72,20 @@ function isWyvrnGenerated(presets) {
|
|
|
72
72
|
* so `cmake --preset` produces
|
|
73
73
|
* binaries matching the profile
|
|
74
74
|
* (VS otherwise defaults to x64).
|
|
75
|
+
* @param {string[]|null} [args.configs] Build recipe's `configs` list
|
|
76
|
+
* (e.g. ['Debug','Release','MinSizeRel']).
|
|
77
|
+
* When provided, drives
|
|
78
|
+
* `CMAKE_CONFIGURATION_TYPES` so
|
|
79
|
+
* multi-config generators (Ninja
|
|
80
|
+
* Multi-Config, VS) actually
|
|
81
|
+
* produce every config the recipe
|
|
82
|
+
* declares. Null/empty → omitted
|
|
83
|
+
* (let CMake use its generator
|
|
84
|
+
* default). `extraCacheVariables`
|
|
85
|
+
* wins on explicit collision so
|
|
86
|
+
* authors that already bake the
|
|
87
|
+
* list into `build.configure`
|
|
88
|
+
* aren't overridden.
|
|
75
89
|
*/
|
|
76
90
|
function buildConfigurePreset({
|
|
77
91
|
profileName,
|
|
@@ -81,16 +95,20 @@ function buildConfigurePreset({
|
|
|
81
95
|
extraCacheVariables = null,
|
|
82
96
|
installDir = null,
|
|
83
97
|
profileArch = null,
|
|
98
|
+
configs = null,
|
|
84
99
|
}) {
|
|
85
100
|
const binaryDir = `\${sourceDir}/build/wyvrn-${profileName}`;
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
|
|
101
|
+
// CMAKE_CONFIGURATION_TYPES is a defensible default from the recipe's
|
|
102
|
+
// declared `configs` list. Applied FIRST (lowest precedence) so an
|
|
103
|
+
// explicit `-DCMAKE_CONFIGURATION_TYPES=...` in `build.configure`
|
|
104
|
+
// still wins — authors who already pin the list shouldn't silently
|
|
105
|
+
// lose their override. The wyvrn-owned entries (toolchain, install
|
|
106
|
+
// prefix) still win over both below.
|
|
107
|
+
const configsDefault = (Array.isArray(configs) && configs.length > 0)
|
|
108
|
+
? { CMAKE_CONFIGURATION_TYPES: configs.join(';') }
|
|
109
|
+
: {};
|
|
93
110
|
const cacheVariables = {
|
|
111
|
+
...configsDefault,
|
|
94
112
|
...(extraCacheVariables ?? {}),
|
|
95
113
|
CMAKE_TOOLCHAIN_FILE: `\${sourceDir}/${toolchainRelPath}`,
|
|
96
114
|
};
|
|
@@ -128,27 +146,32 @@ function buildConfigurePreset({
|
|
|
128
146
|
}
|
|
129
147
|
|
|
130
148
|
/**
|
|
131
|
-
*
|
|
149
|
+
* One build preset per configuration the recipe declares (or the
|
|
150
|
+
* legacy default pair `Debug` + `Release` when no list is provided).
|
|
151
|
+
*
|
|
152
|
+
* The preset name suffix is the configuration name lower-cased — matches
|
|
153
|
+
* the previous Debug/Release naming for those two, and extends to
|
|
154
|
+
* `wyvrn-<profile>-relwithdebinfo` / `-minsizerel` for the multi-config
|
|
155
|
+
* cases. Authors whose recipes include `MinSizeRel` in `build.configs`
|
|
156
|
+
* can now `cmake --build --preset wyvrn-<profile>-minsizerel` directly.
|
|
132
157
|
*
|
|
133
158
|
* @param {string} profileName
|
|
159
|
+
* @param {string[]|null} [configs] Recipe's declared configurations. When
|
|
160
|
+
* null/empty, emits the pre-2.8.1 `Debug` + `Release` pair so existing
|
|
161
|
+
* CMakePresets.json files remain byte-identical.
|
|
134
162
|
* @returns {Array<object>}
|
|
135
163
|
*/
|
|
136
|
-
function buildBuildPresets(profileName) {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
displayName: `wyvrn ${profileName} (Release)`,
|
|
148
|
-
configurePreset: config,
|
|
149
|
-
configuration: 'Release',
|
|
150
|
-
},
|
|
151
|
-
];
|
|
164
|
+
function buildBuildPresets(profileName, configs = null) {
|
|
165
|
+
const base = `wyvrn-${profileName}`;
|
|
166
|
+
const list = (Array.isArray(configs) && configs.length > 0)
|
|
167
|
+
? configs
|
|
168
|
+
: ['Debug', 'Release'];
|
|
169
|
+
return list.map((cfg) => ({
|
|
170
|
+
name: `${base}-${cfg.toLowerCase()}`,
|
|
171
|
+
displayName: `wyvrn ${profileName} (${cfg})`,
|
|
172
|
+
configurePreset: base,
|
|
173
|
+
configuration: cfg,
|
|
174
|
+
}));
|
|
152
175
|
}
|
|
153
176
|
|
|
154
177
|
/**
|
|
@@ -162,6 +185,7 @@ function buildFreshPresetsFile({
|
|
|
162
185
|
extraCacheVariables = null,
|
|
163
186
|
installDir = null,
|
|
164
187
|
profileArch = null,
|
|
188
|
+
configs = null,
|
|
165
189
|
}) {
|
|
166
190
|
return {
|
|
167
191
|
version: SCHEMA_VERSION,
|
|
@@ -173,9 +197,9 @@ function buildFreshPresetsFile({
|
|
|
173
197
|
},
|
|
174
198
|
},
|
|
175
199
|
configurePresets: [buildConfigurePreset({
|
|
176
|
-
profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch,
|
|
200
|
+
profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch, configs,
|
|
177
201
|
})],
|
|
178
|
-
buildPresets: buildBuildPresets(profileName),
|
|
202
|
+
buildPresets: buildBuildPresets(profileName, configs),
|
|
179
203
|
};
|
|
180
204
|
}
|
|
181
205
|
|
|
@@ -197,18 +221,33 @@ function mergeIntoExisting(existing, {
|
|
|
197
221
|
extraCacheVariables = null,
|
|
198
222
|
installDir = null,
|
|
199
223
|
profileArch = null,
|
|
224
|
+
configs = null,
|
|
200
225
|
}) {
|
|
201
226
|
const newConfigure = buildConfigurePreset({
|
|
202
|
-
profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch,
|
|
227
|
+
profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch, configs,
|
|
203
228
|
});
|
|
204
|
-
const newBuilds = buildBuildPresets(profileName);
|
|
229
|
+
const newBuilds = buildBuildPresets(profileName, configs);
|
|
205
230
|
const newConfigureNames = new Set([newConfigure.name]);
|
|
206
|
-
|
|
231
|
+
// When replacing THIS profile's build presets, drop every pre-existing
|
|
232
|
+
// one that targets the same configurePreset — the fresh `newBuilds`
|
|
233
|
+
// list fully replaces them. Scoping to configurePreset avoids stepping
|
|
234
|
+
// on presets for OTHER profiles in the same file, which is the whole
|
|
235
|
+
// point of the merge path.
|
|
236
|
+
//
|
|
237
|
+
// Without this, editing `build.configs` from 4 configs down to 2 would
|
|
238
|
+
// leave orphan build presets pointing at configurations CMake won't
|
|
239
|
+
// generate.
|
|
240
|
+
const myBase = `wyvrn-${profileName}`;
|
|
241
|
+
const isMyBuildPreset = (p) => p && p.configurePreset === myBase;
|
|
207
242
|
|
|
208
243
|
const configurePresets = (existing.configurePresets ?? []).filter((p) => !newConfigureNames.has(p.name));
|
|
209
244
|
configurePresets.push(newConfigure);
|
|
210
245
|
|
|
211
|
-
|
|
246
|
+
// Drop every pre-existing build preset that targets THIS profile's
|
|
247
|
+
// configurePreset — the fresh `newBuilds` list (one per current
|
|
248
|
+
// `configs` entry) fully replaces them. Presets for OTHER profiles
|
|
249
|
+
// stay untouched so the multi-profile merge story is preserved.
|
|
250
|
+
const buildPresets = (existing.buildPresets ?? []).filter((p) => !isMyBuildPreset(p));
|
|
212
251
|
buildPresets.push(...newBuilds);
|
|
213
252
|
|
|
214
253
|
return {
|
|
@@ -260,8 +299,9 @@ function generateCMakePresets({
|
|
|
260
299
|
extraCacheVariables = null,
|
|
261
300
|
installDir = null,
|
|
262
301
|
profileArch = null,
|
|
302
|
+
configs = null,
|
|
263
303
|
}) {
|
|
264
|
-
const args = { profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch };
|
|
304
|
+
const args = { profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch, configs };
|
|
265
305
|
const candidates = [
|
|
266
306
|
{ filePath: path.join(projectRoot, 'CMakePresets.json'), isUser: false },
|
|
267
307
|
{ filePath: path.join(projectRoot, 'CMakeUserPresets.json'), isUser: true },
|
package/src/upload-built.js
CHANGED
|
@@ -7,6 +7,7 @@ const path = require('path');
|
|
|
7
7
|
const { getProvider } = require('./providers');
|
|
8
8
|
const { defaultCompatBlock } = require('./compat');
|
|
9
9
|
const { getBuildPaths } = require('./build/cache');
|
|
10
|
+
const { resolveSourceAuth } = require('./auth');
|
|
10
11
|
const log = require('./logger');
|
|
11
12
|
|
|
12
13
|
const WYVRNPM_VERSION = require('../package.json').version;
|
|
@@ -193,6 +194,7 @@ function resolveUploadDestination(argv, config) {
|
|
|
193
194
|
let src = argv.uploadSource;
|
|
194
195
|
let awsProfile = argv.awsProfile;
|
|
195
196
|
let token = argv.token;
|
|
197
|
+
let sourceEntry = null;
|
|
196
198
|
|
|
197
199
|
const publishSources = config.publishSources ?? [];
|
|
198
200
|
|
|
@@ -202,17 +204,24 @@ function resolveUploadDestination(argv, config) {
|
|
|
202
204
|
if (named) {
|
|
203
205
|
src = named.url;
|
|
204
206
|
awsProfile = awsProfile ?? named.profile;
|
|
205
|
-
|
|
207
|
+
sourceEntry = named;
|
|
206
208
|
log.info(`upload-built: using publish source "${named.name}" from config`);
|
|
207
209
|
}
|
|
208
210
|
} else if (publishSources.length > 0) {
|
|
209
211
|
const first = publishSources[0];
|
|
210
212
|
src = first.url;
|
|
211
213
|
awsProfile = awsProfile ?? first.profile;
|
|
212
|
-
|
|
214
|
+
sourceEntry = first;
|
|
213
215
|
log.info(`upload-built: using first configured publish source "${first.name}"`);
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
// Fall back to the matched source entry's own token / tokenEnv only if
|
|
219
|
+
// the caller didn't pass one already (argv.token wins). resolveSourceAuth
|
|
220
|
+
// handles the literal-vs-env-backed precedence per side.
|
|
221
|
+
if (token === undefined || token === null) {
|
|
222
|
+
token = resolveSourceAuth({}, sourceEntry).token;
|
|
223
|
+
}
|
|
224
|
+
|
|
216
225
|
if (!src) return null;
|
|
217
226
|
return { uploadSource: src, uploadAuth: { awsProfile, token } };
|
|
218
227
|
}
|
package/src/zip-safe.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Defense-in-depth against Zip Slip (CVE-2018-1002200 family). Both
|
|
5
|
+
* node-stream-zip and adm-zip claim to handle this upstream, but we
|
|
6
|
+
* don't want to take the library's word for it on a supply-chain-
|
|
7
|
+
* critical code path. Pre-scan zip entry names before extraction and
|
|
8
|
+
* reject anything that could write outside the target directory.
|
|
9
|
+
*
|
|
10
|
+
* The ZIP spec uses forward slashes only, but Windows-produced archives
|
|
11
|
+
* sometimes leak backslashes — normalise both.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} entryName zip-internal path as reported by the zip library
|
|
14
|
+
* @param {string} [context] optional archive identifier for the error message
|
|
15
|
+
* @throws {Error} if the entry name is absolute, contains a path-traversal
|
|
16
|
+
* segment, or starts with a Windows drive-letter prefix.
|
|
17
|
+
*/
|
|
18
|
+
function assertSafeZipEntryName(entryName, context) {
|
|
19
|
+
const where = context ? ` in ${context}` : '';
|
|
20
|
+
|
|
21
|
+
if (/^[A-Za-z]:[\\/]/.test(entryName)) {
|
|
22
|
+
throw new Error(`unsafe zip entry${where}: drive-letter absolute path ${JSON.stringify(entryName)}`);
|
|
23
|
+
}
|
|
24
|
+
if (entryName.startsWith('/') || entryName.startsWith('\\')) {
|
|
25
|
+
throw new Error(`unsafe zip entry${where}: absolute path ${JSON.stringify(entryName)}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const segments = entryName.split(/[\\/]+/);
|
|
29
|
+
for (const seg of segments) {
|
|
30
|
+
if (seg === '..') {
|
|
31
|
+
throw new Error(`unsafe zip entry${where}: path-traversal segment in ${JSON.stringify(entryName)}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Convenience: validate every name in an iterable. Throws on the first
|
|
38
|
+
* offender (fail-fast) rather than collecting all violations.
|
|
39
|
+
*
|
|
40
|
+
* @param {Iterable<string>} names
|
|
41
|
+
* @param {string} [context]
|
|
42
|
+
*/
|
|
43
|
+
function assertAllSafeZipEntryNames(names, context) {
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
assertSafeZipEntryName(name, context);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
assertSafeZipEntryName,
|
|
51
|
+
assertAllSafeZipEntryNames,
|
|
52
|
+
};
|