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.
Files changed (49) hide show
  1. package/README.md +1914 -1860
  2. package/bin/{wyvrn.js → wyvrnpm.js} +66 -0
  3. package/cmake/cpp.cmake +9 -9
  4. package/cmake/functions.cmake +224 -224
  5. package/cmake/macros.cmake +284 -284
  6. package/package.json +3 -2
  7. package/src/auth.js +66 -66
  8. package/src/binary-dir.js +95 -0
  9. package/src/bootstrap/cookbook.js +196 -196
  10. package/src/bootstrap/detect.js +150 -150
  11. package/src/bootstrap/index.js +220 -220
  12. package/src/bootstrap/version.js +72 -72
  13. package/src/build/cache.js +344 -344
  14. package/src/build/clone.js +170 -170
  15. package/src/build/cmake.js +342 -342
  16. package/src/build/index.js +299 -297
  17. package/src/build/msvc-env.js +260 -260
  18. package/src/build/recipe.js +188 -188
  19. package/src/commands/add.js +141 -141
  20. package/src/commands/bootstrap.js +96 -96
  21. package/src/commands/build.js +482 -452
  22. package/src/commands/cache.js +189 -189
  23. package/src/commands/clean.js +80 -80
  24. package/src/commands/configure.js +92 -92
  25. package/src/commands/init.js +70 -70
  26. package/src/commands/install-skill.js +115 -115
  27. package/src/commands/install.js +730 -674
  28. package/src/commands/link.js +320 -320
  29. package/src/commands/profile.js +237 -237
  30. package/src/commands/publish.js +584 -555
  31. package/src/commands/show.js +252 -252
  32. package/src/commands/version.js +187 -0
  33. package/src/compat.js +273 -273
  34. package/src/conf/index.js +415 -391
  35. package/src/conf/namespaces.js +94 -94
  36. package/src/context.js +230 -230
  37. package/src/http-fetch.js +53 -53
  38. package/src/ignore.js +118 -118
  39. package/src/logger.js +122 -122
  40. package/src/options.js +303 -303
  41. package/src/settings-overrides.js +152 -152
  42. package/src/toolchain/deps.js +164 -164
  43. package/src/toolchain/index.js +212 -212
  44. package/src/toolchain/presets.js +356 -340
  45. package/src/toolchain/template.cmake +77 -77
  46. package/src/upload-built.js +265 -265
  47. package/src/version-range.js +301 -301
  48. package/src/zip-safe.js +52 -52
  49. package/src/zip-stream.js +126 -0
@@ -1,297 +1,299 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const AdmZip = require('adm-zip');
6
-
7
- const { cloneAtSha } = require('./clone');
8
- const { normalizeRecipe, hasBuildRecipe } = require('./recipe');
9
- const { configureBuildInstall } = require('./cmake');
10
- const { getBuildPaths, hasValidArtefact } = require('./cache');
11
-
12
- const { normalizeDependencies } = require('../manifest');
13
- const { resolveDependencies } = require('../resolve');
14
- const { hashProfile, mergeProfile, sha256Of } = require('../profile');
15
- const { generateToolchain } = require('../toolchain');
16
- const { normalizeOptionsDeclaration, resolveEffectiveOptions } = require('../options');
17
- const { readRecipeConf, cmakeCacheVariables } = require('../conf');
18
- const { wyvrnFetch } = require('../http-fetch');
19
- const log = require('../logger');
20
-
21
- /**
22
- * Recursively walk a directory and add every file into an AdmZip at
23
- * paths relative to `base`. Matches the structure emitted by `publish` —
24
- * zip contents are the DIRECT SUBTREE of `base`, not `base` itself.
25
- */
26
- function addFolder(zip, dir, base) {
27
- for (const entry of fs.readdirSync(dir)) {
28
- const full = path.join(dir, entry);
29
- const rel = path.relative(base, full).replace(/\\/g, '/');
30
- const stat = fs.statSync(full);
31
- if (stat.isDirectory()) {
32
- addFolder(zip, full, base);
33
- } else {
34
- const zipDir = path.dirname(rel);
35
- zip.addLocalFile(full, zipDir === '.' ? '' : zipDir);
36
- }
37
- }
38
- }
39
-
40
- /**
41
- * Build a package from source and produce an artefact zip indistinguishable
42
- * from a pre-built download.
43
- *
44
- * ───────────────────────────────────────────────────────────────────────────
45
- * Flow (PLAN-BUILD-MISSING phase 3):
46
- * 1. Check the build cache for a ready artefact → copy and return.
47
- * 2. Shallow-clone `{gitRepo}` at `{gitSha}` into the cache.
48
- * 3. Read the cloned `wyvrn.json`; require a `build` section.
49
- * 4. Recursively resolve + download the package's OWN dependencies into a
50
- * scratch `wyvrn_internal/` co-located with the source. Source-build
51
- * propagates via `buildMode: "missing"`, so transitive source-only deps
52
- * Just Work.
53
- * 5. Generate the CMake toolchain via `generateToolchain()` — the shared
54
- * entry point documented in PLAN-CMAKE-TOOLCHAIN Phase F. Never hand-
55
- * roll profile→flag translation here.
56
- * 6. Configure + build + install via `configureBuildInstall()`.
57
- * 7. Zip the install dir → `wyvrn.zip`, compute `contentSha256`.
58
- * 8. Copy the zip to `destZipPath` so `downloadDependencies` can extract
59
- * it like any other v2 artefact.
60
- * ───────────────────────────────────────────────────────────────────────────
61
- *
62
- * @param {object} args
63
- * @param {string} args.name
64
- * @param {string} args.version
65
- * @param {import('../profile').WyvrnProfile} args.profile
66
- * @param {string} args.profileName
67
- * @param {string} args.profileHash
68
- * @param {Record<string, any>|null} [args.options] Effective options for this build.
69
- * Used to expand `${options.*}` in the recipe.
70
- * @param {string} args.gitRepo
71
- * @param {string} args.gitSha
72
- * @param {string} args.destZipPath Where the caller expects the artefact zip.
73
- * @param {string[]} args.packageSources Used to resolve this package's own deps.
74
- * @param {string} args.platform v1 fallback platform for transitive deps.
75
- * @param {number} args.timeoutMs
76
- * @param {string} [args.buildMode] Propagated into transitive downloads. Default 'missing'.
77
- * @param {string[]} [args.configs] Override recipe.configs (which defaults to
78
- * all four CMake configurations).
79
- * @param {object} [args.authOptions]
80
- * @returns {Promise<{
81
- * found: true,
82
- * contentSha256: string,
83
- * profileHash: string,
84
- * gitSha: string,
85
- * gitRepo: string,
86
- * source: string, // build-cache path (for logging)
87
- * resolvedFrom: 'v2-source-build',
88
- * artefactPath: string, // absolute path to the built zip in the cache
89
- * clonedManifestPath: string, // absolute path to the cloned wyvrn.json (for upload-built)
90
- * }>}
91
- * @throws {Error} when the cloned manifest lacks a `build` section, when
92
- * CMake is missing, or when the compile itself fails.
93
- */
94
- async function buildFromSource(args) {
95
- const {
96
- name, version,
97
- profile, profileName, profileHash,
98
- options = null,
99
- gitRepo, gitSha,
100
- destZipPath,
101
- packageSources,
102
- platform,
103
- timeoutMs,
104
- buildMode = 'missing',
105
- configs,
106
- authOptions = {},
107
- } = args;
108
-
109
- if (!gitRepo || !gitSha) {
110
- throw new Error(
111
- `Cannot source-build ${name}@${version}: the registry has no git metadata ` +
112
- `(gitRepo/gitSha) for this version. Publisher must re-publish with git info.`,
113
- );
114
- }
115
-
116
- const paths = getBuildPaths({ name, version, profileHash });
117
-
118
- // ── 1. Cache hit? Short-circuit to copy and return ──────────────────────
119
- if (hasValidArtefact({ name, version, profileHash })) {
120
- log.info(` source-build cache hit: ${paths.artefact}`);
121
- fs.copyFileSync(paths.artefact, destZipPath);
122
- const contentSha256 = sha256Of(paths.artefact);
123
- return {
124
- found: true,
125
- contentSha256,
126
- profileHash,
127
- gitSha,
128
- gitRepo,
129
- source: paths.root,
130
- resolvedFrom: 'v2-source-build',
131
- artefactPath: paths.artefact,
132
- clonedManifestPath: path.join(paths.src, 'wyvrn.json'),
133
- };
134
- }
135
-
136
- log.info(` source-build starting: ${name}@${version} [${profileHash}]`);
137
- log.info(` cache dir: ${paths.root}`);
138
-
139
- // ── 2. Clone source ─────────────────────────────────────────────────────
140
- fs.mkdirSync(paths.root, { recursive: true });
141
- await cloneAtSha({ gitRepo, gitSha, destDir: paths.src });
142
-
143
- // ── 3. Read cloned manifest + recipe ────────────────────────────────────
144
- const manifestPath = path.join(paths.src, 'wyvrn.json');
145
- if (!fs.existsSync(manifestPath)) {
146
- throw new Error(
147
- `Cloned source does not contain wyvrn.json at ${manifestPath}. ` +
148
- `${name}@${version} cannot be source-built.`,
149
- );
150
- }
151
- const clonedManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
152
- if (!hasBuildRecipe(clonedManifest)) {
153
- throw new Error(
154
- `${name}@${version} has no "build" section in its wyvrn.json at ${gitSha.slice(0, 12)}. ` +
155
- `Source-build is opt-in — the publisher must add a build recipe.`,
156
- );
157
- }
158
- // Pass effective options into the recipe so `${options.<name>}` tokens in
159
- // `build.configure` / `build.buildArgs` expand to the concrete values that
160
- // produced this build's profileHash. Pass the cloned manifest so PLAN-CONF.md
161
- // §4.7 collision detection catches a publisher who declared the same KEY in
162
- // both build.configure and conf.cmake.cache.
163
- const recipe = normalizeRecipe(clonedManifest.build, options, clonedManifest);
164
-
165
- // PLAN-CONF.md §4.8.2: source-build of a dep consumes ONLY the dep's own
166
- // recipe conf — never the root project's merged conf. Translating the
167
- // dep's cmake.cache namespace into -D args here keeps the dep's source-
168
- // build CMake invocation aligned with its author's declared defaults
169
- // while preserving cross-consumer reproducibility. The root's CLI
170
- // --conf / wyvrn.local.json overlay is invisible at this seam.
171
- const clonedConfFlat = readRecipeConf(clonedManifest);
172
- const clonedCacheVars = cmakeCacheVariables(clonedConfFlat);
173
- if (Object.keys(clonedCacheVars).length > 0) {
174
- const confDerivedArgs = Object.entries(clonedCacheVars)
175
- .map(([k, v]) => `-D${k}=${v}`);
176
- recipe.configure = [...recipe.configure, ...confDerivedArgs];
177
- log.info(
178
- ` conf.cmake.cache ${confDerivedArgs.length} source-build ` +
179
- `configure arg(s) from the dep's own recipe`,
180
- );
181
- }
182
-
183
- // ── 4. Recursively resolve + download this package's OWN deps ──────────
184
- // Lazy-require: downloadDependencies is where we were called from, so
185
- // pulling it in at module-load would be a circular import.
186
- // eslint-disable-next-line global-require
187
- const { downloadDependencies } = require('../download');
188
-
189
- const rawPackageDeps = normalizeDependencies(
190
- clonedManifest.dependencies ?? clonedManifest.Dependencies,
191
- );
192
- const depsForResolution = Object.fromEntries(
193
- Object.entries(rawPackageDeps).map(([n, d]) => [n, d.version]),
194
- );
195
-
196
- log.info(` resolving ${Object.keys(depsForResolution).length} transitive dep(s)`);
197
- const transitiveManifests = new Map();
198
- const resolvedDeps = await resolveDependencies(
199
- depsForResolution, packageSources, platform, wyvrnFetch, new Map(),
200
- transitiveManifests,
201
- );
202
-
203
- // Build an enriched deps map. Same logic as install.js: per-dep settings
204
- // + recipe-declared options resolved against what the *cloned* source
205
- // manifest asks for. No CLI overrides transitive options come solely
206
- // from the source package's `dependencies.<n>.options`.
207
- const enrichedDeps = new Map();
208
- for (const [depName, depVersion] of resolvedDeps) {
209
- const depOverrides = rawPackageDeps[depName]?.settings ?? {};
210
- const effectiveProfile = mergeProfile(profile, depOverrides);
211
-
212
- const transitiveManifest = transitiveManifests.get(depName);
213
- const declaration = transitiveManifest?.options
214
- ? normalizeOptionsDeclaration(transitiveManifest.options, `${depName}@${depVersion}`)
215
- : null;
216
- const effectiveOptions = resolveEffectiveOptions(
217
- declaration,
218
- rawPackageDeps[depName]?.options ?? {},
219
- {},
220
- `${depName}@${depVersion}`,
221
- );
222
-
223
- const depProfileHash = hashProfile(effectiveProfile, effectiveOptions);
224
- enrichedDeps.set(depName, {
225
- version: depVersion,
226
- profileHash: depProfileHash,
227
- profile: effectiveProfile,
228
- options: effectiveOptions,
229
- });
230
- }
231
-
232
- if (enrichedDeps.size > 0) {
233
- await downloadDependencies(
234
- enrichedDeps, packageSources, platform, paths.internal,
235
- wyvrnFetch, Math.max(30, Math.round(timeoutMs / 1000)),
236
- { ...authOptions, buildMode },
237
- );
238
- } else {
239
- fs.mkdirSync(paths.internal, { recursive: true });
240
- }
241
-
242
- // ── 5. Generate the CMake toolchain into wyvrn_internal/ ─────────────────
243
- const { toolchain } = generateToolchain({
244
- destDir: paths.internal,
245
- profile,
246
- profileName,
247
- profileHash,
248
- packageNames: [...resolvedDeps.keys()].sort(),
249
- });
250
-
251
- // ── 6. Configure + build + install ──────────────────────────────────────
252
- // Caller can override recipe.configs (e.g. for CI that only wants Release);
253
- // otherwise the recipe's own default of all four CMake configurations is
254
- // respected.
255
- const effectiveConfigs = (configs && configs.length > 0) ? configs : recipe.configs;
256
- log.info(` building ${name}@${version} (configs: ${effectiveConfigs.join(', ')})`);
257
- await configureBuildInstall({
258
- recipe,
259
- srcRoot: paths.src,
260
- buildPaths: { build: paths.build, install: paths.install },
261
- toolchainFile: toolchain,
262
- configs: effectiveConfigs,
263
- profile,
264
- });
265
-
266
- // ── 7. Zip install dir → wyvrn.zip ──────────────────────────────────────
267
- if (!fs.existsSync(paths.install) || fs.readdirSync(paths.install).length === 0) {
268
- throw new Error(
269
- `CMake install produced no files at ${paths.install}. ` +
270
- `Check the package's install() rules in its CMakeLists.txt.`,
271
- );
272
- }
273
- log.info(` zipping install dir → ${paths.artefact}`);
274
- const zip = new AdmZip();
275
- addFolder(zip, paths.install, paths.install);
276
- zip.writeZip(paths.artefact);
277
-
278
- const contentSha256 = sha256Of(paths.artefact);
279
- log.info(` source-build SHA256: ${contentSha256.slice(0, 16)}...`);
280
-
281
- // ── 8. Hand off to the caller's extraction flow ─────────────────────────
282
- fs.copyFileSync(paths.artefact, destZipPath);
283
-
284
- return {
285
- found: true,
286
- contentSha256,
287
- profileHash,
288
- gitSha,
289
- gitRepo,
290
- source: paths.root,
291
- resolvedFrom: 'v2-source-build',
292
- artefactPath: paths.artefact,
293
- clonedManifestPath: manifestPath,
294
- };
295
- }
296
-
297
- module.exports = { buildFromSource };
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const { StreamingZipWriter } = require('../zip-stream');
7
+ const { cloneAtSha } = require('./clone');
8
+ const { normalizeRecipe, hasBuildRecipe } = require('./recipe');
9
+ const { configureBuildInstall } = require('./cmake');
10
+ const { getBuildPaths, hasValidArtefact } = require('./cache');
11
+
12
+ const { normalizeDependencies } = require('../manifest');
13
+ const { resolveDependencies } = require('../resolve');
14
+ const { hashProfile, mergeProfile, sha256Of } = require('../profile');
15
+ const { generateToolchain } = require('../toolchain');
16
+ const { normalizeOptionsDeclaration, resolveEffectiveOptions } = require('../options');
17
+ const { readRecipeConf, cmakeCacheVariables } = require('../conf');
18
+ const { wyvrnFetch } = require('../http-fetch');
19
+ const log = require('../logger');
20
+
21
+ /**
22
+ * Recursively walk a directory and add every file into a streaming zip
23
+ * writer at paths relative to `base`. Matches the structure emitted by
24
+ * `publish` — zip contents are the DIRECT SUBTREE of `base`, not
25
+ * `base` itself. The `zip` argument can be any object exposing
26
+ * `addLocalFile(fullPath, zipDir)` (see [src/zip-stream.js](../zip-stream.js)).
27
+ */
28
+ function addFolder(zip, dir, base) {
29
+ for (const entry of fs.readdirSync(dir)) {
30
+ const full = path.join(dir, entry);
31
+ const rel = path.relative(base, full).replace(/\\/g, '/');
32
+ const stat = fs.statSync(full);
33
+ if (stat.isDirectory()) {
34
+ addFolder(zip, full, base);
35
+ } else {
36
+ const zipDir = path.dirname(rel);
37
+ zip.addLocalFile(full, zipDir === '.' ? '' : zipDir);
38
+ }
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Build a package from source and produce an artefact zip indistinguishable
44
+ * from a pre-built download.
45
+ *
46
+ * ───────────────────────────────────────────────────────────────────────────
47
+ * Flow (PLAN-BUILD-MISSING phase 3):
48
+ * 1. Check the build cache for a ready artefact → copy and return.
49
+ * 2. Shallow-clone `{gitRepo}` at `{gitSha}` into the cache.
50
+ * 3. Read the cloned `wyvrn.json`; require a `build` section.
51
+ * 4. Recursively resolve + download the package's OWN dependencies into a
52
+ * scratch `wyvrn_internal/` co-located with the source. Source-build
53
+ * propagates via `buildMode: "missing"`, so transitive source-only deps
54
+ * Just Work.
55
+ * 5. Generate the CMake toolchain via `generateToolchain()` — the shared
56
+ * entry point documented in PLAN-CMAKE-TOOLCHAIN Phase F. Never hand-
57
+ * roll profile→flag translation here.
58
+ * 6. Configure + build + install via `configureBuildInstall()`.
59
+ * 7. Zip the install dir → `wyvrn.zip`, compute `contentSha256`.
60
+ * 8. Copy the zip to `destZipPath` so `downloadDependencies` can extract
61
+ * it like any other v2 artefact.
62
+ * ───────────────────────────────────────────────────────────────────────────
63
+ *
64
+ * @param {object} args
65
+ * @param {string} args.name
66
+ * @param {string} args.version
67
+ * @param {import('../profile').WyvrnProfile} args.profile
68
+ * @param {string} args.profileName
69
+ * @param {string} args.profileHash
70
+ * @param {Record<string, any>|null} [args.options] Effective options for this build.
71
+ * Used to expand `${options.*}` in the recipe.
72
+ * @param {string} args.gitRepo
73
+ * @param {string} args.gitSha
74
+ * @param {string} args.destZipPath Where the caller expects the artefact zip.
75
+ * @param {string[]} args.packageSources Used to resolve this package's own deps.
76
+ * @param {string} args.platform v1 fallback platform for transitive deps.
77
+ * @param {number} args.timeoutMs
78
+ * @param {string} [args.buildMode] Propagated into transitive downloads. Default 'missing'.
79
+ * @param {string[]} [args.configs] Override recipe.configs (which defaults to
80
+ * all four CMake configurations).
81
+ * @param {object} [args.authOptions]
82
+ * @returns {Promise<{
83
+ * found: true,
84
+ * contentSha256: string,
85
+ * profileHash: string,
86
+ * gitSha: string,
87
+ * gitRepo: string,
88
+ * source: string, // build-cache path (for logging)
89
+ * resolvedFrom: 'v2-source-build',
90
+ * artefactPath: string, // absolute path to the built zip in the cache
91
+ * clonedManifestPath: string, // absolute path to the cloned wyvrn.json (for upload-built)
92
+ * }>}
93
+ * @throws {Error} when the cloned manifest lacks a `build` section, when
94
+ * CMake is missing, or when the compile itself fails.
95
+ */
96
+ async function buildFromSource(args) {
97
+ const {
98
+ name, version,
99
+ profile, profileName, profileHash,
100
+ options = null,
101
+ gitRepo, gitSha,
102
+ destZipPath,
103
+ packageSources,
104
+ platform,
105
+ timeoutMs,
106
+ buildMode = 'missing',
107
+ configs,
108
+ authOptions = {},
109
+ } = args;
110
+
111
+ if (!gitRepo || !gitSha) {
112
+ throw new Error(
113
+ `Cannot source-build ${name}@${version}: the registry has no git metadata ` +
114
+ `(gitRepo/gitSha) for this version. Publisher must re-publish with git info.`,
115
+ );
116
+ }
117
+
118
+ const paths = getBuildPaths({ name, version, profileHash });
119
+
120
+ // ── 1. Cache hit? Short-circuit to copy and return ──────────────────────
121
+ if (hasValidArtefact({ name, version, profileHash })) {
122
+ log.info(` source-build cache hit: ${paths.artefact}`);
123
+ fs.copyFileSync(paths.artefact, destZipPath);
124
+ const contentSha256 = sha256Of(paths.artefact);
125
+ return {
126
+ found: true,
127
+ contentSha256,
128
+ profileHash,
129
+ gitSha,
130
+ gitRepo,
131
+ source: paths.root,
132
+ resolvedFrom: 'v2-source-build',
133
+ artefactPath: paths.artefact,
134
+ clonedManifestPath: path.join(paths.src, 'wyvrn.json'),
135
+ };
136
+ }
137
+
138
+ log.info(` source-build starting: ${name}@${version} [${profileHash}]`);
139
+ log.info(` cache dir: ${paths.root}`);
140
+
141
+ // ── 2. Clone source ─────────────────────────────────────────────────────
142
+ fs.mkdirSync(paths.root, { recursive: true });
143
+ await cloneAtSha({ gitRepo, gitSha, destDir: paths.src });
144
+
145
+ // ── 3. Read cloned manifest + recipe ────────────────────────────────────
146
+ const manifestPath = path.join(paths.src, 'wyvrn.json');
147
+ if (!fs.existsSync(manifestPath)) {
148
+ throw new Error(
149
+ `Cloned source does not contain wyvrn.json at ${manifestPath}. ` +
150
+ `${name}@${version} cannot be source-built.`,
151
+ );
152
+ }
153
+ const clonedManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
154
+ if (!hasBuildRecipe(clonedManifest)) {
155
+ throw new Error(
156
+ `${name}@${version} has no "build" section in its wyvrn.json at ${gitSha.slice(0, 12)}. ` +
157
+ `Source-build is opt-in — the publisher must add a build recipe.`,
158
+ );
159
+ }
160
+ // Pass effective options into the recipe so `${options.<name>}` tokens in
161
+ // `build.configure` / `build.buildArgs` expand to the concrete values that
162
+ // produced this build's profileHash. Pass the cloned manifest so PLAN-CONF.md
163
+ // §4.7 collision detection catches a publisher who declared the same KEY in
164
+ // both build.configure and conf.cmake.cache.
165
+ const recipe = normalizeRecipe(clonedManifest.build, options, clonedManifest);
166
+
167
+ // PLAN-CONF.md §4.8.2: source-build of a dep consumes ONLY the dep's own
168
+ // recipe conf never the root project's merged conf. Translating the
169
+ // dep's cmake.cache namespace into -D args here keeps the dep's source-
170
+ // build CMake invocation aligned with its author's declared defaults
171
+ // while preserving cross-consumer reproducibility. The root's CLI
172
+ // --conf / wyvrn.local.json overlay is invisible at this seam.
173
+ const clonedConfFlat = readRecipeConf(clonedManifest);
174
+ const clonedCacheVars = cmakeCacheVariables(clonedConfFlat);
175
+ if (Object.keys(clonedCacheVars).length > 0) {
176
+ const confDerivedArgs = Object.entries(clonedCacheVars)
177
+ .map(([k, v]) => `-D${k}=${v}`);
178
+ recipe.configure = [...recipe.configure, ...confDerivedArgs];
179
+ log.info(
180
+ ` conf.cmake.cache → ${confDerivedArgs.length} source-build ` +
181
+ `configure arg(s) from the dep's own recipe`,
182
+ );
183
+ }
184
+
185
+ // ── 4. Recursively resolve + download this package's OWN deps ──────────
186
+ // Lazy-require: downloadDependencies is where we were called from, so
187
+ // pulling it in at module-load would be a circular import.
188
+ // eslint-disable-next-line global-require
189
+ const { downloadDependencies } = require('../download');
190
+
191
+ const rawPackageDeps = normalizeDependencies(
192
+ clonedManifest.dependencies ?? clonedManifest.Dependencies,
193
+ );
194
+ const depsForResolution = Object.fromEntries(
195
+ Object.entries(rawPackageDeps).map(([n, d]) => [n, d.version]),
196
+ );
197
+
198
+ log.info(` resolving ${Object.keys(depsForResolution).length} transitive dep(s)`);
199
+ const transitiveManifests = new Map();
200
+ const resolvedDeps = await resolveDependencies(
201
+ depsForResolution, packageSources, platform, wyvrnFetch, new Map(),
202
+ transitiveManifests,
203
+ );
204
+
205
+ // Build an enriched deps map. Same logic as install.js: per-dep settings
206
+ // + recipe-declared options resolved against what the *cloned* source
207
+ // manifest asks for. No CLI overrides — transitive options come solely
208
+ // from the source package's `dependencies.<n>.options`.
209
+ const enrichedDeps = new Map();
210
+ for (const [depName, depVersion] of resolvedDeps) {
211
+ const depOverrides = rawPackageDeps[depName]?.settings ?? {};
212
+ const effectiveProfile = mergeProfile(profile, depOverrides);
213
+
214
+ const transitiveManifest = transitiveManifests.get(depName);
215
+ const declaration = transitiveManifest?.options
216
+ ? normalizeOptionsDeclaration(transitiveManifest.options, `${depName}@${depVersion}`)
217
+ : null;
218
+ const effectiveOptions = resolveEffectiveOptions(
219
+ declaration,
220
+ rawPackageDeps[depName]?.options ?? {},
221
+ {},
222
+ `${depName}@${depVersion}`,
223
+ );
224
+
225
+ const depProfileHash = hashProfile(effectiveProfile, effectiveOptions);
226
+ enrichedDeps.set(depName, {
227
+ version: depVersion,
228
+ profileHash: depProfileHash,
229
+ profile: effectiveProfile,
230
+ options: effectiveOptions,
231
+ });
232
+ }
233
+
234
+ if (enrichedDeps.size > 0) {
235
+ await downloadDependencies(
236
+ enrichedDeps, packageSources, platform, paths.internal,
237
+ wyvrnFetch, Math.max(30, Math.round(timeoutMs / 1000)),
238
+ { ...authOptions, buildMode },
239
+ );
240
+ } else {
241
+ fs.mkdirSync(paths.internal, { recursive: true });
242
+ }
243
+
244
+ // ── 5. Generate the CMake toolchain into wyvrn_internal/ ─────────────────
245
+ const { toolchain } = generateToolchain({
246
+ destDir: paths.internal,
247
+ profile,
248
+ profileName,
249
+ profileHash,
250
+ packageNames: [...resolvedDeps.keys()].sort(),
251
+ });
252
+
253
+ // ── 6. Configure + build + install ──────────────────────────────────────
254
+ // Caller can override recipe.configs (e.g. for CI that only wants Release);
255
+ // otherwise the recipe's own default of all four CMake configurations is
256
+ // respected.
257
+ const effectiveConfigs = (configs && configs.length > 0) ? configs : recipe.configs;
258
+ log.info(` building ${name}@${version} (configs: ${effectiveConfigs.join(', ')})`);
259
+ await configureBuildInstall({
260
+ recipe,
261
+ srcRoot: paths.src,
262
+ buildPaths: { build: paths.build, install: paths.install },
263
+ toolchainFile: toolchain,
264
+ configs: effectiveConfigs,
265
+ profile,
266
+ });
267
+
268
+ // ── 7. Zip install dir → wyvrn.zip ──────────────────────────────────────
269
+ if (!fs.existsSync(paths.install) || fs.readdirSync(paths.install).length === 0) {
270
+ throw new Error(
271
+ `CMake install produced no files at ${paths.install}. ` +
272
+ `Check the package's install() rules in its CMakeLists.txt.`,
273
+ );
274
+ }
275
+ log.info(` zipping install dir → ${paths.artefact}`);
276
+ const zip = new StreamingZipWriter(paths.artefact);
277
+ addFolder(zip, paths.install, paths.install);
278
+ await zip.finalize();
279
+
280
+ const contentSha256 = sha256Of(paths.artefact);
281
+ log.info(` source-build SHA256: ${contentSha256.slice(0, 16)}...`);
282
+
283
+ // ── 8. Hand off to the caller's extraction flow ─────────────────────────
284
+ fs.copyFileSync(paths.artefact, destZipPath);
285
+
286
+ return {
287
+ found: true,
288
+ contentSha256,
289
+ profileHash,
290
+ gitSha,
291
+ gitRepo,
292
+ source: paths.root,
293
+ resolvedFrom: 'v2-source-build',
294
+ artefactPath: paths.artefact,
295
+ clonedManifestPath: manifestPath,
296
+ };
297
+ }
298
+
299
+ module.exports = { buildFromSource };