wyvrnpm 2.0.4 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +639 -5
- package/bin/wyvrn.js +220 -10
- package/claude/skills/wyvrnpm.skill +0 -0
- package/cmake/cpp.cmake +9 -0
- package/cmake/functions.cmake +224 -0
- package/cmake/macros.cmake +233 -0
- package/cmake/options.cmake +23 -0
- package/cmake/variables.cmake +171 -0
- package/package.json +3 -1
- package/src/build/cache.js +148 -0
- package/src/build/clone.js +170 -0
- package/src/build/cmake.js +342 -0
- package/src/build/index.js +275 -0
- package/src/build/msvc-env.js +217 -0
- package/src/build/recipe.js +155 -0
- package/src/commands/build.js +283 -0
- package/src/commands/clean.js +56 -16
- package/src/commands/configure.js +6 -5
- package/src/commands/init.js +3 -2
- package/src/commands/install-skill.js +107 -0
- package/src/commands/install.js +262 -19
- package/src/commands/link.js +18 -15
- package/src/commands/profile.js +15 -12
- package/src/commands/publish.js +216 -65
- package/src/commands/show.js +237 -0
- package/src/compat.js +261 -0
- package/src/config.js +3 -1
- package/src/download.js +431 -87
- package/src/ignore.js +118 -0
- package/src/logger.js +94 -0
- package/src/manifest.js +12 -7
- package/src/options.js +303 -0
- package/src/profile.js +56 -4
- package/src/providers/base.js +16 -1
- package/src/providers/file.js +12 -6
- package/src/providers/http.js +15 -10
- package/src/providers/s3.js +14 -9
- package/src/resolve.js +179 -19
- package/src/toolchain/deps.js +164 -0
- package/src/toolchain/index.js +141 -0
- package/src/toolchain/presets.js +263 -0
- package/src/toolchain/template.cmake +66 -0
- package/src/upload-built.js +256 -0
- package/src/version-range.js +301 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const log = require('../logger');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Spawn a command with inherited stdio, resolve on exit 0, reject otherwise.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} cmd
|
|
13
|
+
* @param {string[]} args
|
|
14
|
+
* @param {{ cwd?: string }} [opts]
|
|
15
|
+
* @returns {Promise<void>}
|
|
16
|
+
*/
|
|
17
|
+
function run(cmd, args, opts = {}) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const proc = spawn(cmd, args, { stdio: 'inherit', ...opts });
|
|
20
|
+
proc.on('error', (err) => reject(new Error(
|
|
21
|
+
`Failed to spawn ${cmd}: ${err.message} (is it installed and on PATH?)`,
|
|
22
|
+
)));
|
|
23
|
+
proc.on('close', (code) => {
|
|
24
|
+
if (code === 0) resolve();
|
|
25
|
+
else reject(new Error(`${cmd} ${args.join(' ')} exited ${code}`));
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Ensure `gitRepo` is cloned into `destDir` at commit `gitSha`.
|
|
32
|
+
*
|
|
33
|
+
* Semantics:
|
|
34
|
+
* - If destDir does not exist → fresh clone + checkout gitSha.
|
|
35
|
+
* - If destDir exists and the HEAD commit already matches gitSha → no-op.
|
|
36
|
+
* - If destDir exists with a different HEAD → fetch + checkout the right
|
|
37
|
+
* SHA, reusing already-downloaded git objects.
|
|
38
|
+
* - If reuse fails for any reason (corrupted cache, unreachable SHA),
|
|
39
|
+
* blow away the cache and fresh-clone.
|
|
40
|
+
*
|
|
41
|
+
* Each path tries a fast shallow fetch by SHA first, then falls back to a
|
|
42
|
+
* full fetch. Some git servers disable `uploadpack.allowReachableSHA1InWant`
|
|
43
|
+
* (notably older Bitbucket Server installs), which makes direct SHA fetches
|
|
44
|
+
* fail with "upload-pack: not our ref"; the full-fetch fallback handles it.
|
|
45
|
+
*
|
|
46
|
+
* Credentials are delegated entirely to the user's ambient git config
|
|
47
|
+
* (SSH keys, credential helpers). We do NOT manage auth.
|
|
48
|
+
*
|
|
49
|
+
* @param {{ gitRepo: string, gitSha: string, destDir: string }} args
|
|
50
|
+
* @returns {Promise<void>}
|
|
51
|
+
*/
|
|
52
|
+
async function cloneAtSha({ gitRepo, gitSha, destDir }) {
|
|
53
|
+
if (!gitRepo) throw new Error('cloneAtSha: gitRepo is required');
|
|
54
|
+
if (!gitSha) throw new Error('cloneAtSha: gitSha is required');
|
|
55
|
+
|
|
56
|
+
// Reuse existing clone if possible.
|
|
57
|
+
if (fs.existsSync(path.join(destDir, '.git'))) {
|
|
58
|
+
try {
|
|
59
|
+
await reuseExistingClone({ destDir, gitSha, gitRepo });
|
|
60
|
+
return;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// An unreachable-commit error is fatal and won't be helped by rebuilding.
|
|
63
|
+
if (/not reachable/.test(err.message)) throw err;
|
|
64
|
+
log.warn(` cached clone unusable (${err.message}); rebuilding`);
|
|
65
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await freshClone({ gitRepo, gitSha, destDir });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Verify that `gitSha` is present and reachable in `destDir`. Throws with an
|
|
74
|
+
* actionable error message when it isn't — the common root cause is an
|
|
75
|
+
* unpushed publisher-side commit or a feature branch that was deleted after
|
|
76
|
+
* publish, leaving the commit dangling on origin.
|
|
77
|
+
*/
|
|
78
|
+
async function verifyShaPresent({ destDir, gitSha, gitRepo }) {
|
|
79
|
+
try {
|
|
80
|
+
await run('git', ['cat-file', '-e', `${gitSha}^{commit}`], { cwd: destDir });
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`commit ${gitSha.slice(0, 12)} is not reachable from any branch or tag on ${gitRepo}. ` +
|
|
84
|
+
`Likely cause: the publisher's HEAD was not pushed, OR the commit was on a ` +
|
|
85
|
+
`feature branch that was deleted/squash-merged after publish. ` +
|
|
86
|
+
`Fix: the package maintainer should republish from a commit that is pushed ` +
|
|
87
|
+
`and reachable from a remote branch or tag.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Checkout `gitSha` in an existing clone. Tries shallow-SHA fetch first,
|
|
94
|
+
* then full fetch (+ tags), then verifies reachability before checkout.
|
|
95
|
+
*/
|
|
96
|
+
async function reuseExistingClone({ destDir, gitSha, gitRepo }) {
|
|
97
|
+
const current = await getHeadSha(destDir);
|
|
98
|
+
if (current === gitSha) return;
|
|
99
|
+
|
|
100
|
+
log.info(` reusing existing clone, switching to ${gitSha.slice(0, 12)}`);
|
|
101
|
+
try {
|
|
102
|
+
// Fast path: shallow fetch the specific SHA directly.
|
|
103
|
+
await run('git', ['fetch', '--depth', '1', 'origin', gitSha], { cwd: destDir });
|
|
104
|
+
} catch {
|
|
105
|
+
// Fallback: full fetch + tags. Fetching tags catches commits that live
|
|
106
|
+
// only under `refs/tags/*` — e.g. a release commit whose branch was
|
|
107
|
+
// deleted but whose tag remains.
|
|
108
|
+
log.info(' shallow SHA fetch unsupported by server; fetching all refs + tags (slower)');
|
|
109
|
+
await run('git', ['fetch', '--tags', 'origin'], { cwd: destDir });
|
|
110
|
+
}
|
|
111
|
+
await verifyShaPresent({ destDir, gitSha, gitRepo });
|
|
112
|
+
await run('git', ['checkout', '--detach', gitSha], { cwd: destDir });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Fresh clone. Tries `init + remote add + shallow-fetch <sha>` first, falls
|
|
117
|
+
* back to a full `git clone --tags` if the server refuses direct-SHA
|
|
118
|
+
* fetches. Verifies the commit is reachable before attempting checkout so
|
|
119
|
+
* the failure mode is a clear error rather than a cryptic `unable to read
|
|
120
|
+
* tree`.
|
|
121
|
+
*/
|
|
122
|
+
async function freshClone({ gitRepo, gitSha, destDir }) {
|
|
123
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
124
|
+
log.info(` cloning ${gitRepo} at ${gitSha.slice(0, 12)} → ${destDir}`);
|
|
125
|
+
try {
|
|
126
|
+
await run('git', ['init', '--quiet'], { cwd: destDir });
|
|
127
|
+
await run('git', ['remote', 'add', 'origin', gitRepo], { cwd: destDir });
|
|
128
|
+
await run('git', ['fetch', '--depth', '1', 'origin', gitSha], { cwd: destDir });
|
|
129
|
+
await verifyShaPresent({ destDir, gitSha, gitRepo });
|
|
130
|
+
await run('git', ['checkout', '--detach', gitSha], { cwd: destDir });
|
|
131
|
+
} catch (err) {
|
|
132
|
+
// If the shallow-SHA fetch itself failed (server refuses it), fall
|
|
133
|
+
// through to a full clone. If instead the SHA-reachability check failed,
|
|
134
|
+
// re-throw with the actionable message — a full clone wouldn't help.
|
|
135
|
+
if (/not reachable/.test(err.message)) throw err;
|
|
136
|
+
|
|
137
|
+
log.warn(` shallow fetch by SHA failed (${err.message}); falling back to full clone`);
|
|
138
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
139
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
140
|
+
// `--tags` brings down tag refs in the same round-trip so tag-only
|
|
141
|
+
// commits are reachable too.
|
|
142
|
+
await run('git', ['clone', '--quiet', '--tags', gitRepo, '.'], { cwd: destDir });
|
|
143
|
+
await verifyShaPresent({ destDir, gitSha, gitRepo });
|
|
144
|
+
await run('git', ['checkout', '--detach', gitSha], { cwd: destDir });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Return the current HEAD commit SHA of a git working tree, or null on error.
|
|
150
|
+
* @param {string} dir
|
|
151
|
+
* @returns {Promise<string|null>}
|
|
152
|
+
*/
|
|
153
|
+
function getHeadSha(dir) {
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
const proc = spawn('git', ['rev-parse', 'HEAD'], {
|
|
156
|
+
cwd: dir,
|
|
157
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
158
|
+
});
|
|
159
|
+
let out = '';
|
|
160
|
+
proc.stdout.on('data', (c) => { out += c.toString(); });
|
|
161
|
+
proc.on('error', () => resolve(null));
|
|
162
|
+
proc.on('close', (code) => {
|
|
163
|
+
if (code !== 0) return resolve(null);
|
|
164
|
+
const sha = out.trim();
|
|
165
|
+
resolve(/^[0-9a-f]{40}$/i.test(sha) ? sha : null);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { cloneAtSha, getHeadSha };
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const { loadMsvcEnv } = require('./msvc-env');
|
|
8
|
+
const log = require('../logger');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* CMake configure + build + install orchestration for the source-build path.
|
|
12
|
+
*
|
|
13
|
+
* Separation of concerns:
|
|
14
|
+
* - This module does NOT compute CMake flags from a profile. That is the
|
|
15
|
+
* toolchain generator's job ([src/toolchain/index.js]). We take a
|
|
16
|
+
* pre-generated toolchain file path and hand it to CMake.
|
|
17
|
+
* - This module DOES translate a recipe (`{ generator, configure,
|
|
18
|
+
* buildArgs, installDir, sourceSubdir }`) into `cmake` argv.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DEFAULT_CONFIG = 'Release';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Probe whether a tool is available on PATH by spawning it with `--version`.
|
|
25
|
+
* Uses `shell: true` on Windows so `.cmd`/`.bat` shims resolve correctly.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} cmd
|
|
28
|
+
* @returns {Promise<boolean>}
|
|
29
|
+
*/
|
|
30
|
+
function isToolOnPath(cmd) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
const proc = spawn(cmd, ['--version'], {
|
|
33
|
+
stdio: 'ignore',
|
|
34
|
+
shell: process.platform === 'win32',
|
|
35
|
+
});
|
|
36
|
+
proc.on('error', () => resolve(false));
|
|
37
|
+
proc.on('close', (code) => resolve(code === 0));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Classify a CMake generator's availability on this machine. Tri-state so
|
|
43
|
+
* `pickGenerator` can distinguish "known-available" (take it) from
|
|
44
|
+
* "known-unavailable" (skip) from "unknown name" (use only if nothing else
|
|
45
|
+
* is confirmed — CMake will error itself if it can't handle the name).
|
|
46
|
+
*
|
|
47
|
+
* @param {string} name
|
|
48
|
+
* @returns {Promise<'yes'|'no'|'unknown'>}
|
|
49
|
+
*/
|
|
50
|
+
async function classifyGenerator(name) {
|
|
51
|
+
if (!name) return 'no';
|
|
52
|
+
if (name === 'Ninja' || name === 'Ninja Multi-Config') {
|
|
53
|
+
return (await isToolOnPath('ninja')) ? 'yes' : 'no';
|
|
54
|
+
}
|
|
55
|
+
if (name.startsWith('Visual Studio')) {
|
|
56
|
+
// CMake itself validates the exact VS version at configure time.
|
|
57
|
+
return process.platform === 'win32' ? 'yes' : 'no';
|
|
58
|
+
}
|
|
59
|
+
if (name === 'Unix Makefiles') {
|
|
60
|
+
return (await isToolOnPath('make')) ? 'yes' : 'no';
|
|
61
|
+
}
|
|
62
|
+
if (name === 'MinGW Makefiles' || name === 'MSYS Makefiles') {
|
|
63
|
+
if (process.platform !== 'win32') return 'no';
|
|
64
|
+
return (await isToolOnPath('mingw32-make') || await isToolOnPath('make')) ? 'yes' : 'no';
|
|
65
|
+
}
|
|
66
|
+
if (name === 'NMake Makefiles' || name === 'NMake Makefiles JOM') {
|
|
67
|
+
if (process.platform !== 'win32') return 'no';
|
|
68
|
+
return (await isToolOnPath('nmake')) ? 'yes' : 'no';
|
|
69
|
+
}
|
|
70
|
+
if (name === 'Xcode') {
|
|
71
|
+
if (process.platform !== 'darwin') return 'no';
|
|
72
|
+
return (await isToolOnPath('xcodebuild')) ? 'yes' : 'no';
|
|
73
|
+
}
|
|
74
|
+
return 'unknown';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Boolean convenience wrapper around `classifyGenerator` — true iff the
|
|
79
|
+
* generator is positively confirmed as available (status === 'yes'). Unknown
|
|
80
|
+
* generators return false here; use `classifyGenerator` directly if you
|
|
81
|
+
* need the distinction.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} name
|
|
84
|
+
* @returns {Promise<boolean>}
|
|
85
|
+
*/
|
|
86
|
+
async function isGeneratorAvailable(name) {
|
|
87
|
+
return (await classifyGenerator(name)) === 'yes';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Choose the best generator from an ordered candidate list.
|
|
92
|
+
*
|
|
93
|
+
* 1. First pass: the first candidate whose probe positively confirms
|
|
94
|
+
* availability (e.g. `ninja --version` succeeds) wins — array order
|
|
95
|
+
* is preserved within this pass.
|
|
96
|
+
* 2. Second pass: if no candidate is known-available, fall back to the
|
|
97
|
+
* first candidate with an unknown name (CMake gets a chance to resolve
|
|
98
|
+
* it, erroring clearly if it can't).
|
|
99
|
+
* 3. Otherwise return `null` so CMake uses its own platform default
|
|
100
|
+
* (VS on Windows, Make elsewhere).
|
|
101
|
+
*
|
|
102
|
+
* @param {string[]} candidates from recipe.generators
|
|
103
|
+
* @returns {Promise<string|null>}
|
|
104
|
+
*/
|
|
105
|
+
async function pickGenerator(candidates) {
|
|
106
|
+
if (!candidates || candidates.length === 0) return null;
|
|
107
|
+
|
|
108
|
+
const results = [];
|
|
109
|
+
for (const name of candidates) {
|
|
110
|
+
results.push({ name, status: await classifyGenerator(name) });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const good = results.find((r) => r.status === 'yes');
|
|
114
|
+
if (good) {
|
|
115
|
+
if (candidates.length > 1) {
|
|
116
|
+
log.info(` generator: ${good.name} (chosen from ${JSON.stringify(candidates)})`);
|
|
117
|
+
}
|
|
118
|
+
return good.name;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const unknown = results.find((r) => r.status === 'unknown');
|
|
122
|
+
if (unknown) {
|
|
123
|
+
log.info(
|
|
124
|
+
` generator: ${unknown.name} (unknown to wyvrnpm's probe; trusting CMake to resolve)`,
|
|
125
|
+
);
|
|
126
|
+
return unknown.name;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
log.warn(
|
|
130
|
+
` None of the requested generators ${JSON.stringify(candidates)} ` +
|
|
131
|
+
`are installed. Falling through to CMake's platform default.`,
|
|
132
|
+
);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Run `cmake` with the given args, inheriting stdio so the user sees compiler
|
|
138
|
+
* output in real time.
|
|
139
|
+
* @param {string[]} args
|
|
140
|
+
* @param {{ cwd?: string, env?: object }} [opts]
|
|
141
|
+
* @returns {Promise<void>}
|
|
142
|
+
*/
|
|
143
|
+
function runCmake(args, opts = {}) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const proc = spawn('cmake', args, {
|
|
146
|
+
stdio: 'inherit',
|
|
147
|
+
cwd: opts.cwd,
|
|
148
|
+
env: opts.env || process.env,
|
|
149
|
+
});
|
|
150
|
+
proc.on('error', (err) => reject(new Error(
|
|
151
|
+
`Failed to spawn cmake: ${err.message} (is CMake installed and on PATH?)`,
|
|
152
|
+
)));
|
|
153
|
+
proc.on('close', (code) => {
|
|
154
|
+
if (code === 0) resolve();
|
|
155
|
+
else reject(new Error(`cmake ${args.join(' ')} exited ${code}`));
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Multi-config CMake generators pick the build configuration at build time
|
|
162
|
+
* (via `--config`), not configure time. Everything else is single-config
|
|
163
|
+
* and bakes `CMAKE_BUILD_TYPE` into the cache at configure time, so
|
|
164
|
+
* multiple configs need multiple build directories.
|
|
165
|
+
*
|
|
166
|
+
* @param {string|null} generatorName
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
function isMultiConfigGenerator(generatorName) {
|
|
170
|
+
if (!generatorName) return false; // null = CMake default — usually Make on Linux, VS on Windows
|
|
171
|
+
if (generatorName.startsWith('Visual Studio')) return true;
|
|
172
|
+
return generatorName === 'Xcode' || generatorName === 'Ninja Multi-Config';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build cmake CONFIGURE argv for a source-build.
|
|
177
|
+
*
|
|
178
|
+
* Exactly one of `buildType` (single-config) or `configurations`
|
|
179
|
+
* (multi-config) should be non-null. Setting `configurations` explicitly
|
|
180
|
+
* lets us pin `CMAKE_CONFIGURATION_TYPES` so every expected `build-<Config>.ninja`
|
|
181
|
+
* file is actually generated — CMake's default for that variable varies
|
|
182
|
+
* across versions and user CMakeLists.
|
|
183
|
+
*
|
|
184
|
+
* @param {{
|
|
185
|
+
* sourceDir: string,
|
|
186
|
+
* buildDir: string,
|
|
187
|
+
* installDir: string,
|
|
188
|
+
* toolchainFile: string,
|
|
189
|
+
* generator?: string|null,
|
|
190
|
+
* configureExtra: string[],
|
|
191
|
+
* buildType?: string|null, // single-config only
|
|
192
|
+
* configurations?: string[]|null, // multi-config only, pins CMAKE_CONFIGURATION_TYPES
|
|
193
|
+
* }} args
|
|
194
|
+
* @returns {string[]}
|
|
195
|
+
*/
|
|
196
|
+
function buildConfigureArgs({
|
|
197
|
+
sourceDir, buildDir, installDir, toolchainFile, generator, configureExtra,
|
|
198
|
+
buildType, configurations,
|
|
199
|
+
}) {
|
|
200
|
+
const args = ['-S', sourceDir, '-B', buildDir];
|
|
201
|
+
if (generator) args.push('-G', generator);
|
|
202
|
+
args.push(`-DCMAKE_TOOLCHAIN_FILE=${toolchainFile}`);
|
|
203
|
+
args.push(`-DCMAKE_INSTALL_PREFIX=${installDir}`);
|
|
204
|
+
// Single-config generators bake CMAKE_BUILD_TYPE into the cache.
|
|
205
|
+
if (buildType) args.push(`-DCMAKE_BUILD_TYPE=${buildType}`);
|
|
206
|
+
// Multi-config generators ignore CMAKE_BUILD_TYPE and use
|
|
207
|
+
// CMAKE_CONFIGURATION_TYPES instead. Passed as a semicolon list.
|
|
208
|
+
if (configurations && configurations.length > 0) {
|
|
209
|
+
args.push(`-DCMAKE_CONFIGURATION_TYPES=${configurations.join(';')}`);
|
|
210
|
+
}
|
|
211
|
+
if (configureExtra.length > 0) args.push(...configureExtra);
|
|
212
|
+
return args;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Build cmake --build argv for the install target.
|
|
217
|
+
*
|
|
218
|
+
* @param {{
|
|
219
|
+
* buildDir: string,
|
|
220
|
+
* config: string,
|
|
221
|
+
* buildExtra: string[],
|
|
222
|
+
* }} args
|
|
223
|
+
* @returns {string[]}
|
|
224
|
+
*/
|
|
225
|
+
function buildInstallArgs({ buildDir, config, buildExtra }) {
|
|
226
|
+
const args = ['--build', buildDir, '--target', 'install', '--config', config];
|
|
227
|
+
if (buildExtra.length > 0) args.push('--', ...buildExtra);
|
|
228
|
+
return args;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Configure + build + install a package from source.
|
|
233
|
+
*
|
|
234
|
+
* @param {{
|
|
235
|
+
* recipe: import('./recipe').NormalizedRecipe,
|
|
236
|
+
* srcRoot: string, // absolute path to the cloned source root
|
|
237
|
+
* buildPaths: { build: string, install: string },
|
|
238
|
+
* toolchainFile: string, // absolute path to wyvrn_toolchain.cmake
|
|
239
|
+
* config?: string, // default 'Release'
|
|
240
|
+
* }} args
|
|
241
|
+
* @returns {Promise<void>}
|
|
242
|
+
*/
|
|
243
|
+
async function configureBuildInstall({ recipe, srcRoot, buildPaths, toolchainFile, configs, profile }) {
|
|
244
|
+
const configList = Array.isArray(configs) && configs.length > 0
|
|
245
|
+
? configs
|
|
246
|
+
: [DEFAULT_CONFIG];
|
|
247
|
+
|
|
248
|
+
const sourceDir = path.resolve(srcRoot, recipe.sourceSubdir);
|
|
249
|
+
if (!fs.existsSync(path.join(sourceDir, 'CMakeLists.txt'))) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`CMakeLists.txt not found at ${sourceDir}. ` +
|
|
252
|
+
`Check the package's build.sourceSubdir in its wyvrn.json.`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fs.mkdirSync(buildPaths.install, { recursive: true });
|
|
257
|
+
|
|
258
|
+
// Resolve generator from the fallback chain (probes each candidate's
|
|
259
|
+
// backing tool). `null` → omit `-G`, let CMake pick its default.
|
|
260
|
+
const generator = await pickGenerator(recipe.generators);
|
|
261
|
+
|
|
262
|
+
// On Windows with an MSVC profile + non-VS generator (Ninja, Make, ...),
|
|
263
|
+
// auto-enter a Developer Command Prompt-equivalent env so `cl.exe` is on
|
|
264
|
+
// PATH. VS generators don't need this — MSBuild resolves MSVC itself.
|
|
265
|
+
// Returns null when nothing needs to change, in which case we inherit
|
|
266
|
+
// the caller's env.
|
|
267
|
+
const env = await loadMsvcEnv({ profile, skipForGenerator: generator });
|
|
268
|
+
|
|
269
|
+
const multiConfig = isMultiConfigGenerator(generator);
|
|
270
|
+
log.info(
|
|
271
|
+
` generator: ${generator ?? '<CMake default>'} ` +
|
|
272
|
+
`(${multiConfig ? 'multi-config' : 'single-config'}), configs: ${configList.join(', ')}`,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
if (multiConfig) {
|
|
276
|
+
// ── Multi-config: one build dir, run install target once per config ─────
|
|
277
|
+
fs.mkdirSync(buildPaths.build, { recursive: true });
|
|
278
|
+
const configureArgs = buildConfigureArgs({
|
|
279
|
+
sourceDir,
|
|
280
|
+
buildDir: buildPaths.build,
|
|
281
|
+
installDir: buildPaths.install,
|
|
282
|
+
toolchainFile,
|
|
283
|
+
generator,
|
|
284
|
+
configureExtra: recipe.configure,
|
|
285
|
+
buildType: null, // multi-config picks at build time
|
|
286
|
+
configurations: configList, // pin CMAKE_CONFIGURATION_TYPES so all 4 build-<Config>.ninja files get generated
|
|
287
|
+
});
|
|
288
|
+
log.info(` cmake ${configureArgs.join(' ')}`);
|
|
289
|
+
await runCmake(configureArgs, { env });
|
|
290
|
+
|
|
291
|
+
for (const cfg of configList) {
|
|
292
|
+
const installArgs = buildInstallArgs({
|
|
293
|
+
buildDir: buildPaths.build,
|
|
294
|
+
config: cfg,
|
|
295
|
+
buildExtra: recipe.buildArgs,
|
|
296
|
+
});
|
|
297
|
+
log.info(` cmake ${installArgs.join(' ')}`);
|
|
298
|
+
await runCmake(installArgs, { env });
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Single-config: separate build dir + reconfigure per config ───────────
|
|
304
|
+
// `CMAKE_BUILD_TYPE` is baked into the cache, so mixing configs in one
|
|
305
|
+
// build dir would require clobbering the cache between each. Distinct
|
|
306
|
+
// dirs is simpler and lets CMake's own dependency tracking work per-config.
|
|
307
|
+
for (const cfg of configList) {
|
|
308
|
+
const perCfgBuildDir = `${buildPaths.build}-${cfg}`;
|
|
309
|
+
fs.mkdirSync(perCfgBuildDir, { recursive: true });
|
|
310
|
+
const configureArgs = buildConfigureArgs({
|
|
311
|
+
sourceDir,
|
|
312
|
+
buildDir: perCfgBuildDir,
|
|
313
|
+
installDir: buildPaths.install,
|
|
314
|
+
toolchainFile,
|
|
315
|
+
generator,
|
|
316
|
+
configureExtra: recipe.configure,
|
|
317
|
+
buildType: cfg,
|
|
318
|
+
});
|
|
319
|
+
log.info(` cmake ${configureArgs.join(' ')}`);
|
|
320
|
+
await runCmake(configureArgs, { env });
|
|
321
|
+
|
|
322
|
+
const installArgs = buildInstallArgs({
|
|
323
|
+
buildDir: perCfgBuildDir,
|
|
324
|
+
config: cfg,
|
|
325
|
+
buildExtra: recipe.buildArgs,
|
|
326
|
+
});
|
|
327
|
+
log.info(` cmake ${installArgs.join(' ')}`);
|
|
328
|
+
await runCmake(installArgs, { env });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = {
|
|
333
|
+
configureBuildInstall,
|
|
334
|
+
pickGenerator,
|
|
335
|
+
classifyGenerator,
|
|
336
|
+
isGeneratorAvailable,
|
|
337
|
+
isMultiConfigGenerator,
|
|
338
|
+
// Exported for unit tests
|
|
339
|
+
buildConfigureArgs,
|
|
340
|
+
buildInstallArgs,
|
|
341
|
+
DEFAULT_CONFIG,
|
|
342
|
+
};
|