wyvrnpm 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/logger.js ADDED
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ // ANSI-coloured, level-tagged logger for wyvrnpm CLI output.
4
+ //
5
+ // All user-facing lines go through this module so the `[wyvrn]` prefix and
6
+ // level markers are applied consistently. Callers pass the message content
7
+ // only — the prefix is never written at call sites.
8
+ //
9
+ // Colour is emitted when the target stream is a TTY, unless overridden by
10
+ // the NO_COLOR or FORCE_COLOR environment variables (see https://no-color.org).
11
+
12
+ const PREFIX = '[wyvrn]';
13
+
14
+ const ANSI = {
15
+ reset: '\x1b[0m',
16
+ bold: '\x1b[1m',
17
+ dim: '\x1b[2m',
18
+ red: '\x1b[31m',
19
+ green: '\x1b[32m',
20
+ yellow: '\x1b[33m',
21
+ cyan: '\x1b[36m',
22
+ };
23
+
24
+ function colorEnabled(stream) {
25
+ if (process.env.NO_COLOR) return false;
26
+ if (process.env.FORCE_COLOR) return true;
27
+ return !!(stream && stream.isTTY);
28
+ }
29
+
30
+ function paint(text, code, stream) {
31
+ return colorEnabled(stream) ? `${code}${text}${ANSI.reset}` : text;
32
+ }
33
+
34
+ function emit(stream, consoleFn, coloredPrefix, message) {
35
+ // Every line in a multi-line message gets the prefix — so multi-line errors
36
+ // line up visually without the caller having to repeat `[wyvrn]` per line.
37
+ const out = String(message)
38
+ .split('\n')
39
+ .map((line) => `${coloredPrefix} ${line}`)
40
+ .join('\n');
41
+ consoleFn(out);
42
+ }
43
+
44
+ function info(message) {
45
+ emit(
46
+ process.stdout,
47
+ console.log,
48
+ paint(PREFIX, ANSI.dim, process.stdout),
49
+ message,
50
+ );
51
+ }
52
+
53
+ function warn(message) {
54
+ const p = paint(PREFIX, ANSI.yellow, process.stderr);
55
+ const tag = paint('warn', ANSI.yellow + ANSI.bold, process.stderr);
56
+ emit(process.stderr, console.warn, `${p} ${tag}`, message);
57
+ }
58
+
59
+ function error(message) {
60
+ const p = paint(PREFIX, ANSI.red, process.stderr);
61
+ const tag = paint('error', ANSI.red + ANSI.bold, process.stderr);
62
+ emit(process.stderr, console.error, `${p} ${tag}`, message);
63
+ }
64
+
65
+ function success(message) {
66
+ const p = paint(PREFIX, ANSI.green, process.stdout);
67
+ const tag = paint('ok', ANSI.green + ANSI.bold, process.stdout);
68
+ emit(process.stdout, console.log, `${p} ${tag}`, message);
69
+ }
70
+
71
+ function debug(message) {
72
+ if (!process.env.WYVRNPM_DEBUG) return;
73
+ const p = paint(PREFIX, ANSI.dim, process.stderr);
74
+ const tag = paint('debug', ANSI.cyan, process.stderr);
75
+ emit(process.stderr, console.error, `${p} ${tag}`, message);
76
+ }
77
+
78
+ module.exports = { info, warn, error, success, debug };
package/src/profile.js CHANGED
@@ -214,6 +214,33 @@ function getGitRepo(cwd) {
214
214
  return tryExec(`git -C "${cwd || '.'}" remote get-url origin`) || null;
215
215
  }
216
216
 
217
+ /**
218
+ * Check whether `sha` is reachable from any remote tracking branch or tag.
219
+ * Used at publish time to warn when the publisher's HEAD hasn't been pushed
220
+ * — a commit that isn't reachable from origin cannot be retrieved by
221
+ * downstream `--build=missing` consumers.
222
+ *
223
+ * @param {string} sha
224
+ * @param {string} [cwd]
225
+ * @returns {boolean} true if the commit is reachable on origin
226
+ */
227
+ function isShaOnRemote(sha, cwd) {
228
+ if (!sha) return false;
229
+ // --remotes limits to remote-tracking branches; include tags too so
230
+ // tagged release commits on deleted branches still count as reachable.
231
+ const branches = tryExec(`git -C "${cwd || '.'}" branch -r --contains ${sha}`);
232
+ if (branches && branches.trim().length > 0) return true;
233
+ const tags = tryExec(`git -C "${cwd || '.'}" tag --contains ${sha}`);
234
+ if (tags && tags.trim().length > 0) {
235
+ // A local tag is only useful if it's also on origin. Check.
236
+ const remoteTags = tryExec(`git -C "${cwd || '.'}" ls-remote --tags origin`);
237
+ if (remoteTags && tags.split(/\s+/).some((t) => t && remoteTags.includes(`refs/tags/${t}`))) {
238
+ return true;
239
+ }
240
+ }
241
+ return false;
242
+ }
243
+
217
244
  /**
218
245
  * Format a profile for human-readable display (Conan-style).
219
246
  *
@@ -354,6 +381,7 @@ module.exports = {
354
381
  sha256Of,
355
382
  getGitSha,
356
383
  getGitRepo,
384
+ isShaOnRemote,
357
385
  formatProfile,
358
386
  mergeProfile,
359
387
  // Profile file management
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const BaseProvider = require('./base');
6
+ const log = require('../logger');
6
7
 
7
8
  /**
8
9
  * File system provider — handles local directories and SMB/UNC shares.
@@ -56,12 +57,12 @@ class FileProvider extends BaseProvider {
56
57
  }
57
58
 
58
59
  _copy(src, dest) {
59
- console.log(`[wyvrn] → ${dest}`);
60
+ log.info(` → ${dest}`);
60
61
  fs.copyFileSync(src, dest);
61
62
  }
62
63
 
63
64
  _writeJson(filePath, data) {
64
- console.log(`[wyvrn] → ${filePath}`);
65
+ log.info(` → ${filePath}`);
65
66
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
66
67
  }
67
68
 
@@ -87,7 +88,7 @@ class FileProvider extends BaseProvider {
87
88
  this._copy(files.zip, path.join(destDir, 'wyvrn.zip'));
88
89
 
89
90
  const latestPath = path.join(packageDir, 'latest.json');
90
- console.log(`[wyvrn] → ${latestPath}`);
91
+ log.info(` → ${latestPath}`);
91
92
  fs.writeFileSync(latestPath, JSON.stringify({ version }), 'utf8');
92
93
  }
93
94
 
@@ -6,6 +6,7 @@ const https = require('https');
6
6
  const { pipeline } = require('stream/promises');
7
7
  const { Readable } = require('stream');
8
8
  const BaseProvider = require('./base');
9
+ const log = require('../logger');
9
10
 
10
11
  function pinDependencies(rawDeps, lockedDeps) {
11
12
  if (!rawDeps || !lockedDeps || Object.keys(lockedDeps).length === 0) return rawDeps;
@@ -45,7 +46,7 @@ class HttpProvider extends BaseProvider {
45
46
  const parsed = new URL(url);
46
47
  const headers = { 'Content-Type': contentType, 'Content-Length': body.length };
47
48
  if (token) headers['Authorization'] = `Bearer ${token}`;
48
- console.log(`[wyvrn] → PUT ${url}`);
49
+ log.info(` → PUT ${url}`);
49
50
  const req = this._lib(url).request(
50
51
  { hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'PUT', headers },
51
52
  (res) => {
@@ -66,7 +67,7 @@ class HttpProvider extends BaseProvider {
66
67
  const parsed = new URL(url);
67
68
  const headers = { 'Content-Type': contentType, 'Content-Length': buffer.length };
68
69
  if (token) headers['Authorization'] = `Bearer ${token}`;
69
- console.log(`[wyvrn] → PUT ${url}`);
70
+ log.info(` → PUT ${url}`);
70
71
  const req = this._lib(url).request(
71
72
  { hostname: parsed.hostname, port: parsed.port || undefined, path: parsed.pathname, method: 'PUT', headers },
72
73
  (res) => {
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const BaseProvider = require('./base');
6
+ const log = require('../logger');
6
7
 
7
8
  const RE_S3_URI = /^s3:\/\//;
8
9
  const RE_S3_VIRTUAL_HOSTED = /^https?:\/\/([^.]+)\.s3[.-][^/]*\.amazonaws\.com/;
@@ -79,7 +80,7 @@ class S3Provider extends BaseProvider {
79
80
  }
80
81
 
81
82
  async _putFile(client, bucket, key, filePath, contentType, PutObjectCommand) {
82
- console.log(`[wyvrn] → s3://${bucket}/${key}`);
83
+ log.info(` → s3://${bucket}/${key}`);
83
84
  await client.send(new PutObjectCommand({
84
85
  Bucket: bucket, Key: key,
85
86
  Body: fs.readFileSync(filePath),
@@ -88,7 +89,7 @@ class S3Provider extends BaseProvider {
88
89
  }
89
90
 
90
91
  async _putBuffer(client, bucket, key, content, contentType, PutObjectCommand) {
91
- console.log(`[wyvrn] → s3://${bucket}/${key}`);
92
+ log.info(` → s3://${bucket}/${key}`);
92
93
  await client.send(new PutObjectCommand({
93
94
  Bucket: bucket, Key: key,
94
95
  Body: content,
package/src/resolve.js CHANGED
@@ -3,6 +3,8 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
+ const log = require('./logger');
7
+
6
8
  /**
7
9
  * Checks if a version string represents a linked package.
8
10
  * @param {string} version
@@ -164,7 +166,7 @@ async function fetchPackageManifest(name, version, packageSources, platform, htt
164
166
  }
165
167
  }
166
168
 
167
- console.warn(`[wyvrn] Warning: could not fetch manifest for ${name}@${version} from any source`);
169
+ log.warn(`could not fetch manifest for ${name}@${version} from any source`);
168
170
  cache.set(cacheKey, null);
169
171
  return null;
170
172
  }
@@ -205,9 +207,9 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
205
207
  if (version === 'latest') {
206
208
  try {
207
209
  version = await resolveLatestVersion(name, packageSources, platform, httpClient);
208
- console.log(`[wyvrn] Resolved "${name}@latest" → ${version}`);
210
+ log.info(`Resolved "${name}@latest" → ${version}`);
209
211
  } catch (err) {
210
- console.warn(`[wyvrn] Warning: ${err.message}`);
212
+ log.warn(err.message);
211
213
  continue;
212
214
  }
213
215
  }
@@ -224,8 +226,8 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
224
226
  // Conflict: keep the latest version.
225
227
  const winner = compareVersions(version, existing) > 0 ? version : existing;
226
228
  if (winner !== existing) {
227
- console.warn(
228
- `[wyvrn] Version conflict for "${name}": ${existing} vs ${version} — using ${winner}`,
229
+ log.warn(
230
+ `Version conflict for "${name}": ${existing} vs ${version} — using ${winner}`,
229
231
  );
230
232
  resolved.set(name, winner);
231
233
  // Re-fetch the winning version's manifest to pull in its transitive deps.
@@ -242,9 +244,9 @@ async function resolveDependencies(rootDeps, packageSources, platform, httpClien
242
244
  const localPath = getLinkedPath(version);
243
245
  manifest = readLocalManifest(localPath);
244
246
  if (manifest) {
245
- console.log(`[wyvrn] Linked: ${name} → ${localPath}`);
247
+ log.info(`Linked: ${name} → ${localPath}`);
246
248
  } else {
247
- console.log(`[wyvrn] Linked: ${name} → ${localPath} (no manifest found)`);
249
+ log.info(`Linked: ${name} → ${localPath} (no manifest found)`);
248
250
  }
249
251
  } else {
250
252
  manifest = await fetchPackageManifest(
@@ -0,0 +1,164 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Generate the contents of `wyvrn_deps.cmake`.
5
+ *
6
+ * This file is loaded via `CMAKE_PROJECT_INCLUDE` — it runs after the
7
+ * consumer's first `project()` call, which is when CMake variables like
8
+ * `CMAKE_CXX_COMPILER_ID`, `CMAKE_SIZEOF_VOID_P`, `CMAKE_SYSTEM_NAME`, and
9
+ * `WIN32` / `APPLE` / `UNIX` become valid. The bundled utility scripts
10
+ * (`variables.cmake` etc.) rely on those, so they cannot be included from
11
+ * the toolchain body.
12
+ *
13
+ * @param {object} args
14
+ * @param {string[]} args.packageNames Resolved dep names in topological order.
15
+ * @param {string} args.profileHash 16-char profile hash (for logging).
16
+ * @returns {string} CMake file content.
17
+ */
18
+ function generateDepsCmake({ packageNames, profileHash }) {
19
+ const findPackageCalls = packageNames
20
+ .map((name) => ` find_package(${name} CONFIG QUIET)`)
21
+ .join('\n');
22
+
23
+ const resolvedList = packageNames.map((n) => ` ${n}`).join('\n');
24
+
25
+ return `# Generated by wyvrnpm — do not edit.
26
+ # Loaded after project() via CMAKE_PROJECT_INCLUDE.
27
+ # Profile hash: ${profileHash}
28
+
29
+ if(WYVRN_DEPS_INCLUDED)
30
+ return()
31
+ endif()
32
+ set(WYVRN_DEPS_INCLUDED TRUE)
33
+
34
+ # List of every package resolved by \`wyvrnpm install\`, in topological order.
35
+ set(WYVRN_RESOLVED_PACKAGES
36
+ ${resolvedList || ' # (none)'}
37
+ )
38
+
39
+ # ── Bundled wyvrnpm CMake utilities ───────────────────────────────────────────
40
+ # Included in the order the utility scripts expect.
41
+ include("\${WYVRNPM_CMAKE_DIR}/variables.cmake")
42
+ include("\${WYVRNPM_CMAKE_DIR}/options.cmake")
43
+ include("\${WYVRNPM_CMAKE_DIR}/cpp.cmake")
44
+ include("\${WYVRNPM_CMAKE_DIR}/functions.cmake")
45
+ include("\${WYVRNPM_CMAKE_DIR}/macros.cmake")
46
+
47
+ # ── Sanity checks (warn only — do not override the consumer's environment) ───
48
+ if(DEFINED CMAKE_CXX_COMPILER_ID AND DEFINED WYVRN_PROFILE_COMPILER)
49
+ set(_wyvrn_expected_compiler_id "")
50
+ if(WYVRN_PROFILE_COMPILER STREQUAL "msvc")
51
+ set(_wyvrn_expected_compiler_id "MSVC")
52
+ elseif(WYVRN_PROFILE_COMPILER STREQUAL "gcc")
53
+ set(_wyvrn_expected_compiler_id "GNU")
54
+ elseif(WYVRN_PROFILE_COMPILER STREQUAL "clang")
55
+ set(_wyvrn_expected_compiler_id "Clang")
56
+ elseif(WYVRN_PROFILE_COMPILER STREQUAL "apple-clang")
57
+ set(_wyvrn_expected_compiler_id "AppleClang")
58
+ endif()
59
+ if(_wyvrn_expected_compiler_id AND NOT CMAKE_CXX_COMPILER_ID STREQUAL _wyvrn_expected_compiler_id)
60
+ message(WARNING
61
+ "wyvrnpm profile expects compiler '\${WYVRN_PROFILE_COMPILER}' "
62
+ "(CMake id '\${_wyvrn_expected_compiler_id}') but the active compiler "
63
+ "is '\${CMAKE_CXX_COMPILER_ID}'. Installed binaries may be ABI-incompatible."
64
+ )
65
+ endif()
66
+ endif()
67
+
68
+ if(DEFINED CMAKE_SIZEOF_VOID_P AND DEFINED WYVRN_PROFILE_ARCH)
69
+ if(WYVRN_PROFILE_ARCH STREQUAL "x86_64" OR WYVRN_PROFILE_ARCH STREQUAL "armv8")
70
+ if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
71
+ message(WARNING
72
+ "wyvrnpm profile expects 64-bit arch '\${WYVRN_PROFILE_ARCH}' "
73
+ "but CMake is configuring a 32-bit build (CMAKE_SIZEOF_VOID_P=\${CMAKE_SIZEOF_VOID_P})."
74
+ )
75
+ endif()
76
+ elseif(WYVRN_PROFILE_ARCH STREQUAL "x86")
77
+ if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
78
+ message(WARNING
79
+ "wyvrnpm profile expects 32-bit arch 'x86' "
80
+ "but CMake is configuring a 64-bit build (CMAKE_SIZEOF_VOID_P=\${CMAKE_SIZEOF_VOID_P})."
81
+ )
82
+ endif()
83
+ endif()
84
+ endif()
85
+
86
+ # ── wyvrnpm_find_dependencies() ───────────────────────────────────────────────
87
+ # One-shot helper: resolves every package that wyvrnpm installed.
88
+ # Packages that don't ship a CMake config are silently skipped (CONFIG QUIET).
89
+ function(wyvrnpm_find_dependencies)
90
+ ${findPackageCalls || ' # (no dependencies)'}
91
+
92
+ # Post-find aliasing: ensure every resolved package has a wyvrn::<name>
93
+ # target, creating an INTERFACE alias when the package exports only
94
+ # <name>::<name>. Skipped when wyvrn::<name> already exists.
95
+ foreach(_wyvrn_pkg IN LISTS WYVRN_RESOLVED_PACKAGES)
96
+ if(TARGET wyvrn::\${_wyvrn_pkg})
97
+ # Already provided by the package's own CMake config.
98
+ elseif(TARGET \${_wyvrn_pkg}::\${_wyvrn_pkg})
99
+ add_library(wyvrn::\${_wyvrn_pkg} INTERFACE IMPORTED)
100
+ set_target_properties(wyvrn::\${_wyvrn_pkg} PROPERTIES
101
+ INTERFACE_LINK_LIBRARIES \${_wyvrn_pkg}::\${_wyvrn_pkg}
102
+ )
103
+ endif()
104
+ endforeach()
105
+ endfunction()
106
+
107
+ # ── wyvrnpm_enable_runtime_dll_copy(target) ───────────────────────────────────
108
+ # Adds a POST_BUILD command that copies every runtime DLL the target depends on
109
+ # (transitively, via \$<TARGET_RUNTIME_DLLS:...>) next to the target's own
110
+ # binary — so consumers can run the executable straight from the build dir
111
+ # without \`PATH\` tinkering. No-op on non-Windows platforms and on non-
112
+ # executable targets.
113
+ #
114
+ # Respects the per-target opt-out property WYVRN_COPY_RUNTIME_DLLS OFF.
115
+ # Requires CMake ≥ 3.21 (for TARGET_RUNTIME_DLLS).
116
+ function(wyvrnpm_enable_runtime_dll_copy target)
117
+ if(NOT WIN32)
118
+ return()
119
+ endif()
120
+ if(NOT TARGET \${target})
121
+ message(WARNING "wyvrnpm_enable_runtime_dll_copy: target '\${target}' does not exist")
122
+ return()
123
+ endif()
124
+ get_target_property(_wyvrn_type \${target} TYPE)
125
+ if(NOT _wyvrn_type STREQUAL "EXECUTABLE")
126
+ return()
127
+ endif()
128
+ get_target_property(_wyvrn_optout \${target} WYVRN_COPY_RUNTIME_DLLS)
129
+ if(_wyvrn_optout STREQUAL "OFF" OR _wyvrn_optout STREQUAL "FALSE")
130
+ return()
131
+ endif()
132
+ add_custom_command(TARGET \${target} POST_BUILD
133
+ COMMAND \${CMAKE_COMMAND} -E copy_if_different
134
+ "\$<TARGET_RUNTIME_DLLS:\${target}>"
135
+ "\$<TARGET_FILE_DIR:\${target}>"
136
+ COMMAND_EXPAND_LISTS
137
+ VERBATIM
138
+ )
139
+ endfunction()
140
+
141
+ # ── wyvrnpm_finalize_targets() ────────────────────────────────────────────────
142
+ # Discovers all executable targets in the calling directory (top-level only,
143
+ # not recursive) and wires each one up for runtime-DLL copy. Call this at the
144
+ # end of the consumer's top-level CMakeLists.txt, AFTER all add_executable()
145
+ # calls, so every target is visible.
146
+ #
147
+ # Scope: uses the BUILDSYSTEM_TARGETS directory property of the calling
148
+ # directory. For multi-subdirectory projects where executables are added from
149
+ # subdirectories, consumers should either:
150
+ # (a) call wyvrnpm_enable_runtime_dll_copy(<tgt>) explicitly for each exe, or
151
+ # (b) call wyvrnpm_finalize_targets() from each subdirectory.
152
+ function(wyvrnpm_finalize_targets)
153
+ get_property(_wyvrn_targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS)
154
+ foreach(_wyvrn_t IN LISTS _wyvrn_targets)
155
+ get_target_property(_wyvrn_type \${_wyvrn_t} TYPE)
156
+ if(_wyvrn_type STREQUAL "EXECUTABLE")
157
+ wyvrnpm_enable_runtime_dll_copy(\${_wyvrn_t})
158
+ endif()
159
+ endforeach()
160
+ endfunction()
161
+ `;
162
+ }
163
+
164
+ module.exports = { generateDepsCmake };
@@ -0,0 +1,141 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const { generateDepsCmake } = require('./deps');
7
+ const { generateCMakePresets } = require('./presets');
8
+
9
+ /**
10
+ * Absolute path to the bundled `cmake/` directory shipped inside the
11
+ * wyvrnpm npm package. Returned in CMake-friendly form (forward slashes).
12
+ *
13
+ * @returns {string}
14
+ */
15
+ function getBundledCmakeDir() {
16
+ const dir = path.resolve(__dirname, '..', '..', 'cmake');
17
+ return dir.replace(/\\/g, '/');
18
+ }
19
+
20
+ /**
21
+ * Substitute `{{KEY}}` placeholders in a template string.
22
+ *
23
+ * @param {string} template
24
+ * @param {Record<string, string>} values
25
+ * @returns {string}
26
+ */
27
+ function renderTemplate(template, values) {
28
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
29
+ if (Object.prototype.hasOwnProperty.call(values, key)) {
30
+ return values[key];
31
+ }
32
+ throw new Error(`Toolchain template has no value for placeholder {{${key}}}`);
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Produce the toolchain template's placeholder substitutions.
38
+ *
39
+ * @param {object} args
40
+ * @param {import('../profile').WyvrnProfile} args.profile
41
+ * @param {string} args.profileName
42
+ * @param {string} args.profileHash
43
+ * @param {string[]} args.packageNames
44
+ * @returns {Record<string, string>}
45
+ */
46
+ function buildPlaceholders({ profile, profileName, profileHash, packageNames }) {
47
+ const prefixPathEntries = packageNames
48
+ .map((name) => ` "\${CMAKE_CURRENT_LIST_DIR}/${name}"`)
49
+ .join('\n');
50
+
51
+ return {
52
+ GENERATED_AT: new Date().toISOString(),
53
+ PROFILE_NAME: profileName,
54
+ PROFILE_HASH: profileHash,
55
+ PROFILE_OS: profile.os ?? '',
56
+ PROFILE_ARCH: profile.arch ?? '',
57
+ PROFILE_COMPILER: profile.compiler ?? '',
58
+ PROFILE_COMPILER_VERSION: profile['compiler.version'] ?? '',
59
+ PROFILE_CPPSTD: profile['compiler.cppstd'] ?? '20',
60
+ PROFILE_RUNTIME: profile['compiler.runtime'] ?? '',
61
+ WYVRNPM_CMAKE_DIR: getBundledCmakeDir(),
62
+ PREFIX_PATH_ENTRIES: prefixPathEntries || ' # (no dependencies)',
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Generate the three toolchain artefacts under `destDir`:
68
+ * - wyvrn_toolchain.cmake
69
+ * - wyvrn_deps.cmake
70
+ * - wyvrn_profile.json
71
+ *
72
+ * ───────────────────────────────────────────────────────────────────────────
73
+ * CONTRACT (PLAN-CMAKE-TOOLCHAIN Phase F ↔ PLAN-BUILD-MISSING Phase 3)
74
+ * ───────────────────────────────────────────────────────────────────────────
75
+ * This function is the **single source of truth** for translating a build
76
+ * profile into a CMake configuration. Two callers exist:
77
+ *
78
+ * 1. `install` — writes the toolchain into a consumer project's
79
+ * `wyvrn_internal/`, so `cmake --preset` picks it
80
+ * up at configure time.
81
+ * 2. `buildFromSource` — (not yet implemented; PLAN-BUILD-MISSING §3)
82
+ * writes the toolchain into a scratch build dir
83
+ * when we source-build a package via
84
+ * `--build=missing`. The package is compiled
85
+ * with THIS function's output, guaranteeing the
86
+ * resulting binary's profile → flag mapping is
87
+ * bit-for-bit identical to what a consumer sees.
88
+ *
89
+ * Do NOT replicate this mapping elsewhere. If PLAN-BUILD-MISSING §3 finds it
90
+ * needs additional behaviour (e.g. extra CMake cache variables), extend this
91
+ * function — do not branch.
92
+ *
93
+ * Callers choose `destDir` and `packageNames`:
94
+ * - `destDir` — any directory; in source-build, points at the scratch
95
+ * `wyvrn_internal/` co-located with the cloned source.
96
+ * - `packageNames` — in `install`, the consumer's resolved deps; in
97
+ * source-build, the *package's own* resolved deps.
98
+ *
99
+ * Profile arguments (`profile`, `profileName`, `profileHash`) are always the
100
+ * active consumer profile — source-build still uses the consumer's profile,
101
+ * not the package's. That is the whole point of the source-build path.
102
+ * ───────────────────────────────────────────────────────────────────────────
103
+ *
104
+ * @param {object} args
105
+ * @param {string} args.destDir Where to write the three files.
106
+ * @param {import('../profile').WyvrnProfile} args.profile
107
+ * @param {string} args.profileName
108
+ * @param {string} args.profileHash
109
+ * @param {string[]} args.packageNames Resolved dep names in topological order.
110
+ * @returns {{ toolchain: string, deps: string, profile: string }}
111
+ * Absolute paths of the three files written.
112
+ */
113
+ function generateToolchain({ destDir, profile, profileName, profileHash, packageNames }) {
114
+ fs.mkdirSync(destDir, { recursive: true });
115
+
116
+ const templatePath = path.join(__dirname, 'template.cmake');
117
+ const template = fs.readFileSync(templatePath, 'utf8');
118
+ const placeholders = buildPlaceholders({ profile, profileName, profileHash, packageNames });
119
+ const toolchainContent = renderTemplate(template, placeholders);
120
+ const depsContent = generateDepsCmake({ packageNames, profileHash });
121
+ const profileContent = JSON.stringify({ name: profileName, hash: profileHash, ...profile }, null, 2) + '\n';
122
+
123
+ const toolchainPath = path.join(destDir, 'wyvrn_toolchain.cmake');
124
+ const depsPath = path.join(destDir, 'wyvrn_deps.cmake');
125
+ const profilePath = path.join(destDir, 'wyvrn_profile.json');
126
+
127
+ fs.writeFileSync(toolchainPath, toolchainContent, 'utf8');
128
+ fs.writeFileSync(depsPath, depsContent, 'utf8');
129
+ fs.writeFileSync(profilePath, profileContent, 'utf8');
130
+
131
+ return { toolchain: toolchainPath, deps: depsPath, profile: profilePath };
132
+ }
133
+
134
+ module.exports = {
135
+ generateToolchain,
136
+ generateCMakePresets,
137
+ getBundledCmakeDir,
138
+ // Exported for tests
139
+ buildPlaceholders,
140
+ renderTemplate,
141
+ };