wyvrnpm 2.10.2 → 2.12.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 +1914 -1860
- package/bin/{wyvrn.js → wyvrnpm.js} +66 -0
- package/cmake/cpp.cmake +9 -9
- package/cmake/functions.cmake +224 -224
- package/cmake/macros.cmake +284 -284
- package/package.json +3 -2
- package/src/auth.js +66 -66
- package/src/binary-dir.js +95 -0
- package/src/bootstrap/cookbook.js +196 -196
- package/src/bootstrap/detect.js +150 -150
- package/src/bootstrap/index.js +220 -220
- package/src/bootstrap/version.js +72 -72
- package/src/build/cache.js +344 -344
- package/src/build/clone.js +170 -170
- package/src/build/cmake.js +342 -342
- package/src/build/index.js +299 -297
- package/src/build/msvc-env.js +260 -260
- package/src/build/recipe.js +188 -188
- package/src/commands/add.js +141 -141
- package/src/commands/bootstrap.js +96 -96
- package/src/commands/build.js +482 -452
- package/src/commands/cache.js +189 -189
- package/src/commands/clean.js +80 -80
- package/src/commands/configure.js +92 -92
- package/src/commands/init.js +70 -70
- package/src/commands/install-skill.js +115 -115
- package/src/commands/install.js +730 -674
- package/src/commands/link.js +320 -320
- package/src/commands/profile.js +237 -237
- package/src/commands/publish.js +584 -555
- package/src/commands/show.js +252 -252
- package/src/commands/version.js +187 -0
- package/src/compat.js +273 -273
- package/src/conf/index.js +415 -391
- package/src/conf/namespaces.js +94 -94
- package/src/context.js +230 -230
- package/src/http-fetch.js +53 -53
- package/src/ignore.js +118 -118
- package/src/logger.js +122 -122
- package/src/options.js +303 -303
- package/src/settings-overrides.js +152 -152
- package/src/toolchain/deps.js +164 -164
- package/src/toolchain/index.js +212 -212
- package/src/toolchain/presets.js +356 -340
- package/src/toolchain/template.cmake +77 -77
- package/src/upload-built.js +265 -265
- package/src/version-range.js +301 -301
- package/src/zip-safe.js +52 -52
- package/src/zip-stream.js +126 -0
package/src/conf/index.js
CHANGED
|
@@ -1,391 +1,415 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Non-ABI build-time configuration — the `conf` system (PLAN-CONF.md).
|
|
4
|
-
//
|
|
5
|
-
// `conf` is the sibling of `options` with the opposite hashing
|
|
6
|
-
// property: options fold into `profileHash` (ABI-affecting), conf
|
|
7
|
-
// never does (build-time knobs). See plan §2 for the rationale.
|
|
8
|
-
//
|
|
9
|
-
// This module is the pure core. It parses the four layers
|
|
10
|
-
// (CLI, `wyvrn.local.json`, named profile `[conf]`, recipe
|
|
11
|
-
// `wyvrn.json`), merges them with per-leaf precedence, and produces
|
|
12
|
-
// a flat `{ "namespace.leaf": value }` map ready for each sink to
|
|
13
|
-
// consume. It does NOT know about CMake, filesystems beyond reading
|
|
14
|
-
// a given JSON file, or any command — those live one level up in
|
|
15
|
-
// src/commands/* and src/toolchain/*.
|
|
16
|
-
|
|
17
|
-
const fs = require('fs');
|
|
18
|
-
const path = require('path');
|
|
19
|
-
|
|
20
|
-
const { validateFlatConfKeys } = require('./namespaces');
|
|
21
|
-
|
|
22
|
-
const LOCAL_OVERLAY_FILENAME = 'wyvrn.local.json';
|
|
23
|
-
|
|
24
|
-
// Per plan §4.5 "Strict top-level allow-list".
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* -
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* @
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
node
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
throw new Error(
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
* }
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
*
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Non-ABI build-time configuration — the `conf` system (PLAN-CONF.md).
|
|
4
|
+
//
|
|
5
|
+
// `conf` is the sibling of `options` with the opposite hashing
|
|
6
|
+
// property: options fold into `profileHash` (ABI-affecting), conf
|
|
7
|
+
// never does (build-time knobs). See plan §2 for the rationale.
|
|
8
|
+
//
|
|
9
|
+
// This module is the pure core. It parses the four layers
|
|
10
|
+
// (CLI, `wyvrn.local.json`, named profile `[conf]`, recipe
|
|
11
|
+
// `wyvrn.json`), merges them with per-leaf precedence, and produces
|
|
12
|
+
// a flat `{ "namespace.leaf": value }` map ready for each sink to
|
|
13
|
+
// consume. It does NOT know about CMake, filesystems beyond reading
|
|
14
|
+
// a given JSON file, or any command — those live one level up in
|
|
15
|
+
// src/commands/* and src/toolchain/*.
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const { validateFlatConfKeys } = require('./namespaces');
|
|
21
|
+
|
|
22
|
+
const LOCAL_OVERLAY_FILENAME = 'wyvrn.local.json';
|
|
23
|
+
|
|
24
|
+
// Per plan §4.5 "Strict top-level allow-list". Two top-level keys
|
|
25
|
+
// today:
|
|
26
|
+
// - `conf` — non-ABI build-time knobs (PLAN-CONF.md).
|
|
27
|
+
// - `binaryDirRoot` — dev-local override of the preset's build
|
|
28
|
+
// subdir (sibling channel to --build-dir on
|
|
29
|
+
// install/build). See src/binary-dir.js.
|
|
30
|
+
// Adding dependencies/options/etc. is plan-2 territory.
|
|
31
|
+
const LOCAL_OVERLAY_ALLOWED_TOP_KEYS = new Set(['conf', 'binaryDirRoot']);
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Value normalisation
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Normalise a single leaf VALUE into the string CMake (or whichever
|
|
39
|
+
* sink consumes it) expects. JSON `true`/`false` become `"ON"`/`"OFF"`;
|
|
40
|
+
* numbers are stringified; strings pass through. Arrays / objects /
|
|
41
|
+
* null at a leaf position are a hard error — those live at branch
|
|
42
|
+
* positions, not leaves.
|
|
43
|
+
*
|
|
44
|
+
* CLI values arrive as strings and are passed through verbatim.
|
|
45
|
+
*
|
|
46
|
+
* @param {unknown} value
|
|
47
|
+
* @param {string} keyForError
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function normalizeLeafValue(value, keyForError) {
|
|
51
|
+
if (value === true) return 'ON';
|
|
52
|
+
if (value === false) return 'OFF';
|
|
53
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
54
|
+
if (typeof value === 'string') return value;
|
|
55
|
+
throw new Error(
|
|
56
|
+
`conf value for ${JSON.stringify(keyForError)} must be a string, ` +
|
|
57
|
+
`number, or boolean — got ${JSON.stringify(value)}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Flatten a nested conf object (recipe/local form) into a flat
|
|
63
|
+
* `{ "ns.leaf": stringValue }` map. Keys are joined with `.`. The
|
|
64
|
+
* recursion stops at any non-object (strings, numbers, booleans) —
|
|
65
|
+
* those are leaves.
|
|
66
|
+
*
|
|
67
|
+
* Throws on:
|
|
68
|
+
* - null at a leaf position
|
|
69
|
+
* - arrays anywhere (arrays are not meaningful for cmake cache vars
|
|
70
|
+
* and leaving them as "maybe later" invites footguns)
|
|
71
|
+
* - objects whose keys contain `.` (would clash with our separator)
|
|
72
|
+
*
|
|
73
|
+
* @param {object} nested
|
|
74
|
+
* @param {string} [prefix]
|
|
75
|
+
* @param {Record<string, string>} [out]
|
|
76
|
+
* @returns {Record<string, string>}
|
|
77
|
+
*/
|
|
78
|
+
function flattenConf(nested, prefix = '', out = {}) {
|
|
79
|
+
if (nested === null || typeof nested !== 'object') {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`conf block must be an object — got ${JSON.stringify(nested)} at ` +
|
|
82
|
+
`${prefix || '<root>'}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(nested)) {
|
|
86
|
+
throw new Error(`conf block must be an object, not an array — at ${prefix || '<root>'}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const [rawKey, value] of Object.entries(nested)) {
|
|
90
|
+
if (rawKey.includes('.')) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`conf key segment ${JSON.stringify(rawKey)} must not contain a period — ` +
|
|
93
|
+
`use nested objects or the flat "namespace.leaf" form, not both`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const fullKey = prefix ? `${prefix}.${rawKey}` : rawKey;
|
|
97
|
+
|
|
98
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
99
|
+
flattenConf(value, fullKey, out);
|
|
100
|
+
} else {
|
|
101
|
+
out[fullKey] = normalizeLeafValue(value, fullKey);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Re-expand a flat `{ "ns.leaf": value }` map back into nested form,
|
|
109
|
+
* for emitting into JSON manifests / lockfiles / payloads in the same
|
|
110
|
+
* shape authors author with. Values round-trip as strings (conf
|
|
111
|
+
* values are always strings post-normalisation).
|
|
112
|
+
*
|
|
113
|
+
* @param {Record<string, string>} flat
|
|
114
|
+
* @returns {object}
|
|
115
|
+
*/
|
|
116
|
+
function unflattenConf(flat) {
|
|
117
|
+
const root = {};
|
|
118
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
119
|
+
const parts = key.split('.');
|
|
120
|
+
let node = root;
|
|
121
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
122
|
+
const seg = parts[i];
|
|
123
|
+
if (node[seg] === undefined || typeof node[seg] !== 'object') {
|
|
124
|
+
node[seg] = {};
|
|
125
|
+
}
|
|
126
|
+
node = node[seg];
|
|
127
|
+
}
|
|
128
|
+
node[parts[parts.length - 1]] = value;
|
|
129
|
+
}
|
|
130
|
+
return root;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// CLI parsing
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Parse an array of `--conf KEY=VALUE` strings (yargs delivers
|
|
139
|
+
* `argv.conf` when the option is registered as `array: true`) into a
|
|
140
|
+
* flat map. See plan §4.4 for semantics:
|
|
141
|
+
*
|
|
142
|
+
* KEY=VALUE → set the key to VALUE verbatim (string).
|
|
143
|
+
* KEY → alias for KEY=true (matches CMake's -D convention).
|
|
144
|
+
* KEY= → unset (remove the key from lower layers on merge).
|
|
145
|
+
*
|
|
146
|
+
* Returns two maps — set values and unset keys — so the merger can
|
|
147
|
+
* treat the latter specially.
|
|
148
|
+
*
|
|
149
|
+
* @param {string[] | undefined} rawArgs
|
|
150
|
+
* @returns {{ sets: Record<string,string>, unsets: Set<string> }}
|
|
151
|
+
*/
|
|
152
|
+
function parseCliConf(rawArgs) {
|
|
153
|
+
const sets = {};
|
|
154
|
+
const unsets = new Set();
|
|
155
|
+
|
|
156
|
+
if (!rawArgs || rawArgs.length === 0) {
|
|
157
|
+
return { sets, unsets };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const arg of rawArgs) {
|
|
161
|
+
if (typeof arg !== 'string') {
|
|
162
|
+
throw new Error(`--conf expects KEY=VALUE string; got ${JSON.stringify(arg)}`);
|
|
163
|
+
}
|
|
164
|
+
const eqIdx = arg.indexOf('=');
|
|
165
|
+
if (eqIdx < 0) {
|
|
166
|
+
// Bare `KEY` → KEY=true (normalised to "ON").
|
|
167
|
+
const key = arg.trim();
|
|
168
|
+
if (key.length === 0) {
|
|
169
|
+
throw new Error('--conf was given an empty key');
|
|
170
|
+
}
|
|
171
|
+
sets[key] = 'ON';
|
|
172
|
+
} else {
|
|
173
|
+
const key = arg.slice(0, eqIdx).trim();
|
|
174
|
+
const value = arg.slice(eqIdx + 1);
|
|
175
|
+
if (key.length === 0) {
|
|
176
|
+
throw new Error(`--conf ${JSON.stringify(arg)} has an empty key`);
|
|
177
|
+
}
|
|
178
|
+
if (value.length === 0) {
|
|
179
|
+
// KEY= → unset. Distinguish from KEY="" which callers can
|
|
180
|
+
// achieve by quoting the value; here the shell has already
|
|
181
|
+
// stripped the quotes, so an empty string past the `=` means
|
|
182
|
+
// "clear any inherited value for this key."
|
|
183
|
+
unsets.add(key);
|
|
184
|
+
} else {
|
|
185
|
+
sets[key] = value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { sets, unsets };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Local-overlay file
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Read and validate `wyvrn.local.json` at the given project root.
|
|
199
|
+
* Returns the parsed `conf` block as a flat map, or an empty map when
|
|
200
|
+
* the file is absent.
|
|
201
|
+
*
|
|
202
|
+
* Enforces plan §4.5: the only top-level key this file may carry in
|
|
203
|
+
* phase 1 is `conf`; anything else is a hard error (not a warning).
|
|
204
|
+
*
|
|
205
|
+
* @param {string} rootDir
|
|
206
|
+
* @returns {{ flat: Record<string,string>, path: string|null, binaryDirRoot: string|null }}
|
|
207
|
+
*/
|
|
208
|
+
function readLocalOverlay(rootDir) {
|
|
209
|
+
const p = path.join(rootDir, LOCAL_OVERLAY_FILENAME);
|
|
210
|
+
if (!fs.existsSync(p)) return { flat: {}, path: null, binaryDirRoot: null };
|
|
211
|
+
|
|
212
|
+
let raw;
|
|
213
|
+
try {
|
|
214
|
+
raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
215
|
+
} catch (err) {
|
|
216
|
+
throw new Error(`cannot parse ${LOCAL_OVERLAY_FILENAME}: ${err.message}`);
|
|
217
|
+
}
|
|
218
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
219
|
+
throw new Error(`${LOCAL_OVERLAY_FILENAME} must contain a JSON object at the top level`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const key of Object.keys(raw)) {
|
|
223
|
+
if (!LOCAL_OVERLAY_ALLOWED_TOP_KEYS.has(key)) {
|
|
224
|
+
const allowed = [...LOCAL_OVERLAY_ALLOWED_TOP_KEYS].join(', ');
|
|
225
|
+
throw new Error(
|
|
226
|
+
`${LOCAL_OVERLAY_FILENAME}: unknown top-level key ${JSON.stringify(key)} — ` +
|
|
227
|
+
`allow-list: [${allowed}]. ` +
|
|
228
|
+
`If you meant to override dependencies or options, that is reserved ` +
|
|
229
|
+
`for a future phase; see claude/PLAN-CONF.md §4.5.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const confBlock = raw.conf ?? {};
|
|
235
|
+
const flat = flattenConf(confBlock);
|
|
236
|
+
validateFlatConfKeys(flat);
|
|
237
|
+
|
|
238
|
+
// `binaryDirRoot` is only validated for type/shape here; the actual
|
|
239
|
+
// path-syntax check (no abs, no `..`, etc.) lives in src/binary-dir.js
|
|
240
|
+
// which has the canonical ruleset. Returning the raw string keeps the
|
|
241
|
+
// two modules decoupled.
|
|
242
|
+
const binaryDirRoot = Object.prototype.hasOwnProperty.call(raw, 'binaryDirRoot')
|
|
243
|
+
? raw.binaryDirRoot
|
|
244
|
+
: null;
|
|
245
|
+
|
|
246
|
+
return { flat, path: p, binaryDirRoot };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Recipe extraction
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Pull the `conf` block out of an already-loaded recipe manifest and
|
|
255
|
+
* flatten it. A missing block is an empty map.
|
|
256
|
+
*
|
|
257
|
+
* Validation runs against the phase-1 allow-list; if a recipe declares
|
|
258
|
+
* a namespace we don't support yet, the install fails with a clear
|
|
259
|
+
* error rather than silently dropping the config.
|
|
260
|
+
*
|
|
261
|
+
* @param {object} manifest
|
|
262
|
+
* @returns {Record<string,string>}
|
|
263
|
+
*/
|
|
264
|
+
function readRecipeConf(manifest) {
|
|
265
|
+
const block = manifest?.conf ?? null;
|
|
266
|
+
if (block === null || block === undefined) return {};
|
|
267
|
+
const flat = flattenConf(block);
|
|
268
|
+
validateFlatConfKeys(flat);
|
|
269
|
+
return flat;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// Profile extraction
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Pull the optional `conf` block out of a loaded named profile JSON
|
|
278
|
+
* (see src/profile.js) and flatten it. A missing block is an empty
|
|
279
|
+
* map — profiles without `conf` round-trip byte-identically to
|
|
280
|
+
* pre-phase-2 behaviour.
|
|
281
|
+
*
|
|
282
|
+
* Validation runs against the phase-1 allow-list; an unknown
|
|
283
|
+
* namespace in a profile fails fast rather than silently dropping
|
|
284
|
+
* the config (CLAUDE.md §2 principle 8).
|
|
285
|
+
*
|
|
286
|
+
* @param {object|null|undefined} profile
|
|
287
|
+
* @returns {Record<string,string>}
|
|
288
|
+
*/
|
|
289
|
+
function readProfileConf(profile) {
|
|
290
|
+
const block = profile?.conf ?? null;
|
|
291
|
+
if (block === null || block === undefined) return {};
|
|
292
|
+
const flat = flattenConf(block);
|
|
293
|
+
validateFlatConfKeys(flat);
|
|
294
|
+
return flat;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Merge
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Merge the four layers with plan §4.2 precedence (post phase-2):
|
|
303
|
+
*
|
|
304
|
+
* CLI unsets — remove named keys from below.
|
|
305
|
+
* CLI sets — highest wins.
|
|
306
|
+
* local — overrides profile.
|
|
307
|
+
* profile — overrides recipe (team-shared team defaults).
|
|
308
|
+
* recipe — base defaults.
|
|
309
|
+
*
|
|
310
|
+
* Per-leaf override, not per-branch replace — so CLI setting only
|
|
311
|
+
* cmake.cache.FOO does not disturb a recipe's cmake.cache.BAR.
|
|
312
|
+
*
|
|
313
|
+
* The `profileFlat` parameter is optional. Pre-phase-2 callers that
|
|
314
|
+
* pass only three layers (recipe, local, cli) still work — profile
|
|
315
|
+
* defaults to an empty map, preserving the three-layer semantics
|
|
316
|
+
* exactly.
|
|
317
|
+
*
|
|
318
|
+
* @param {Record<string,string>} recipeFlat
|
|
319
|
+
* @param {Record<string,string>} localFlat
|
|
320
|
+
* @param {{ sets: Record<string,string>, unsets: Set<string> }} cli
|
|
321
|
+
* @param {Record<string,string>} [profileFlat]
|
|
322
|
+
* @returns {Record<string,string>}
|
|
323
|
+
*/
|
|
324
|
+
function mergeConfLayers(recipeFlat, localFlat, cli, profileFlat = {}) {
|
|
325
|
+
const out = { ...recipeFlat, ...profileFlat, ...localFlat, ...cli.sets };
|
|
326
|
+
for (const k of cli.unsets) delete out[k];
|
|
327
|
+
validateFlatConfKeys(out);
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Convenience: do the whole four-layer resolution at once. Most
|
|
333
|
+
* callers want this. Individual layer helpers are exported for tests.
|
|
334
|
+
*
|
|
335
|
+
* The `profile` argument is optional. Callers that don't have a
|
|
336
|
+
* resolved profile to hand (e.g. the legacy three-layer call sites in
|
|
337
|
+
* `publish`, which intentionally ignore the profile tier so published
|
|
338
|
+
* artefacts aren't skewed by dev machines) pass nothing and get the
|
|
339
|
+
* three-layer merge (CLI > local > recipe).
|
|
340
|
+
*
|
|
341
|
+
* @param {object} args
|
|
342
|
+
* @param {object} [args.manifest]
|
|
343
|
+
* @param {string} args.rootDir
|
|
344
|
+
* @param {string[]} [args.cliConf]
|
|
345
|
+
* @param {object} [args.profile] Resolved named profile JSON (may carry .conf).
|
|
346
|
+
* @returns {{
|
|
347
|
+
* flat: Record<string,string>,
|
|
348
|
+
* recipeFlat: Record<string,string>,
|
|
349
|
+
* profileFlat: Record<string,string>,
|
|
350
|
+
* localFlat: Record<string,string>,
|
|
351
|
+
* cli: { sets: Record<string,string>, unsets: Set<string> },
|
|
352
|
+
* localOverlayPath: string | null,
|
|
353
|
+
* }}
|
|
354
|
+
*/
|
|
355
|
+
function resolveEffectiveConf({ manifest, rootDir, cliConf, profile }) {
|
|
356
|
+
const recipeFlat = readRecipeConf(manifest);
|
|
357
|
+
const profileFlat = readProfileConf(profile);
|
|
358
|
+
const overlay = readLocalOverlay(rootDir);
|
|
359
|
+
const { flat: localFlat, path: localOverlayPath, binaryDirRoot: localBinaryDirRoot } = overlay;
|
|
360
|
+
const cli = parseCliConf(cliConf);
|
|
361
|
+
const flat = mergeConfLayers(recipeFlat, localFlat, cli, profileFlat);
|
|
362
|
+
return {
|
|
363
|
+
flat,
|
|
364
|
+
recipeFlat,
|
|
365
|
+
profileFlat,
|
|
366
|
+
localFlat,
|
|
367
|
+
cli,
|
|
368
|
+
localOverlayPath,
|
|
369
|
+
// Surfaced so install can route this into src/binary-dir.js without
|
|
370
|
+
// re-reading the overlay file. Validated downstream — this just
|
|
371
|
+
// forwards the raw value the user wrote.
|
|
372
|
+
localBinaryDirRoot,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Extract only the `cmake.cache.*` portion of a flat conf map as
|
|
378
|
+
* `{ VARNAME: stringValue }` — the shape
|
|
379
|
+
* `src/toolchain/presets.js` wants for `cacheVariables`. Stable
|
|
380
|
+
* alphabetical key order so preset diffs are deterministic.
|
|
381
|
+
*
|
|
382
|
+
* @param {Record<string,string>} flatConf
|
|
383
|
+
* @returns {Record<string,string>}
|
|
384
|
+
*/
|
|
385
|
+
function cmakeCacheVariables(flatConf) {
|
|
386
|
+
const out = {};
|
|
387
|
+
const prefix = 'cmake.cache.';
|
|
388
|
+
const keys = Object.keys(flatConf).filter((k) => k.startsWith(prefix)).sort();
|
|
389
|
+
for (const k of keys) {
|
|
390
|
+
out[k.slice(prefix.length)] = flatConf[k];
|
|
391
|
+
}
|
|
392
|
+
return out;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = {
|
|
396
|
+
// High-level
|
|
397
|
+
resolveEffectiveConf,
|
|
398
|
+
|
|
399
|
+
// Per-layer
|
|
400
|
+
parseCliConf,
|
|
401
|
+
readLocalOverlay,
|
|
402
|
+
readRecipeConf,
|
|
403
|
+
readProfileConf,
|
|
404
|
+
mergeConfLayers,
|
|
405
|
+
|
|
406
|
+
// Shape transforms
|
|
407
|
+
flattenConf,
|
|
408
|
+
unflattenConf,
|
|
409
|
+
normalizeLeafValue,
|
|
410
|
+
cmakeCacheVariables,
|
|
411
|
+
|
|
412
|
+
// Constants (exported for tests + integration)
|
|
413
|
+
LOCAL_OVERLAY_FILENAME,
|
|
414
|
+
LOCAL_OVERLAY_ALLOWED_TOP_KEYS,
|
|
415
|
+
};
|