wyvrnpm 2.3.2 → 2.8.1
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 +465 -19
- package/bin/wyvrn.js +175 -9
- package/claude/skills/wyvrnpm.skill +0 -0
- package/cmake/macros.cmake +51 -0
- package/cmake/variables.cmake +13 -3
- package/package.json +1 -1
- package/src/auth.js +66 -0
- package/src/build/cache.js +196 -0
- package/src/build/index.js +23 -2
- package/src/build/msvc-env.js +50 -7
- package/src/build/recipe.js +36 -3
- package/src/commands/add.js +140 -0
- package/src/commands/build.js +111 -7
- package/src/commands/cache.js +189 -0
- package/src/commands/configure.js +15 -4
- package/src/commands/init.js +36 -0
- package/src/commands/install-skill.js +8 -0
- package/src/commands/install.js +271 -5
- package/src/commands/profile.js +70 -7
- package/src/commands/publish.js +41 -4
- package/src/commands/show.js +14 -4
- package/src/compat.js +21 -9
- package/src/conf/index.js +391 -0
- package/src/conf/namespaces.js +94 -0
- package/src/download.js +136 -148
- package/src/logger.js +30 -2
- package/src/manifest.js +34 -7
- package/src/profile.js +148 -12
- package/src/resolve.js +47 -7
- package/src/settings-overrides.js +152 -0
- package/src/toolchain/index.js +71 -0
- package/src/toolchain/presets.js +108 -31
- package/src/toolchain/template.cmake +11 -0
- package/src/upload-built.js +11 -2
- package/src/zip-safe.js +52 -0
package/src/build/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const { resolveDependencies } = require('../resolve');
|
|
|
14
14
|
const { hashProfile, mergeProfile, sha256Of } = require('../profile');
|
|
15
15
|
const { generateToolchain } = require('../toolchain');
|
|
16
16
|
const { normalizeOptionsDeclaration, resolveEffectiveOptions } = require('../options');
|
|
17
|
+
const { readRecipeConf, cmakeCacheVariables } = require('../conf');
|
|
17
18
|
const log = require('../logger');
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -155,8 +156,28 @@ async function buildFromSource(args) {
|
|
|
155
156
|
}
|
|
156
157
|
// Pass effective options into the recipe so `${options.<name>}` tokens in
|
|
157
158
|
// `build.configure` / `build.buildArgs` expand to the concrete values that
|
|
158
|
-
// produced this build's profileHash.
|
|
159
|
-
|
|
159
|
+
// produced this build's profileHash. Pass the cloned manifest so PLAN-CONF.md
|
|
160
|
+
// §4.7 collision detection catches a publisher who declared the same KEY in
|
|
161
|
+
// both build.configure and conf.cmake.cache.
|
|
162
|
+
const recipe = normalizeRecipe(clonedManifest.build, options, clonedManifest);
|
|
163
|
+
|
|
164
|
+
// PLAN-CONF.md §4.8.2: source-build of a dep consumes ONLY the dep's own
|
|
165
|
+
// recipe conf — never the root project's merged conf. Translating the
|
|
166
|
+
// dep's cmake.cache namespace into -D args here keeps the dep's source-
|
|
167
|
+
// build CMake invocation aligned with its author's declared defaults
|
|
168
|
+
// while preserving cross-consumer reproducibility. The root's CLI
|
|
169
|
+
// --conf / wyvrn.local.json overlay is invisible at this seam.
|
|
170
|
+
const clonedConfFlat = readRecipeConf(clonedManifest);
|
|
171
|
+
const clonedCacheVars = cmakeCacheVariables(clonedConfFlat);
|
|
172
|
+
if (Object.keys(clonedCacheVars).length > 0) {
|
|
173
|
+
const confDerivedArgs = Object.entries(clonedCacheVars)
|
|
174
|
+
.map(([k, v]) => `-D${k}=${v}`);
|
|
175
|
+
recipe.configure = [...recipe.configure, ...confDerivedArgs];
|
|
176
|
+
log.info(
|
|
177
|
+
` conf.cmake.cache → ${confDerivedArgs.length} source-build ` +
|
|
178
|
+
`configure arg(s) from the dep's own recipe`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
160
181
|
|
|
161
182
|
// ── 4. Recursively resolve + download this package's OWN deps ──────────
|
|
162
183
|
// Lazy-require: downloadDependencies is where we were called from, so
|
package/src/build/msvc-env.js
CHANGED
|
@@ -75,17 +75,35 @@ function findVcvarsall(vsInstallPath) {
|
|
|
75
75
|
return fs.existsSync(p) ? p : null;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* The complete set of arch arguments `vcvarsall.bat` accepts AND that we
|
|
80
|
+
* are willing to splice into a shell command. Explicit allow-list — any
|
|
81
|
+
* value outside this set is rejected (EVALUATION.md S5).
|
|
82
|
+
*/
|
|
83
|
+
const VCVARSALL_ARCH_ALLOWLIST = new Set(['x64', 'x86', 'arm64', 'arm']);
|
|
84
|
+
|
|
85
|
+
const ARCH_MAP = {
|
|
86
|
+
x86_64: 'x64',
|
|
87
|
+
x86: 'x86',
|
|
88
|
+
armv8: 'arm64',
|
|
89
|
+
armv7: 'arm',
|
|
90
|
+
};
|
|
91
|
+
|
|
78
92
|
/**
|
|
79
93
|
* Map a wyvrnpm profile arch to the argument `vcvarsall.bat` expects.
|
|
94
|
+
* Throws on an unknown profile arch — silently defaulting to `x64` would
|
|
95
|
+
* (a) produce a build for the wrong target, and (b) hand any shell-unsafe
|
|
96
|
+
* input to cmd.exe through captureVcvarsEnv's template.
|
|
80
97
|
*/
|
|
81
98
|
function mapArch(profileArch) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
99
|
+
const arg = ARCH_MAP[profileArch];
|
|
100
|
+
if (!arg || !VCVARSALL_ARCH_ALLOWLIST.has(arg)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`unsupported MSVC profile arch ${JSON.stringify(profileArch)}; ` +
|
|
103
|
+
`expected one of ${Object.keys(ARCH_MAP).join(', ')}`,
|
|
104
|
+
);
|
|
88
105
|
}
|
|
106
|
+
return arg;
|
|
89
107
|
}
|
|
90
108
|
|
|
91
109
|
/**
|
|
@@ -105,6 +123,18 @@ function mapArch(profileArch) {
|
|
|
105
123
|
* @returns {Promise<object|null>}
|
|
106
124
|
*/
|
|
107
125
|
async function captureVcvarsEnv(vcvarsallPath, archArg) {
|
|
126
|
+
// Defense-in-depth at the shell boundary (EVALUATION.md S5). mapArch
|
|
127
|
+
// already restricts archArg to the allow-list; re-assert here so this
|
|
128
|
+
// function is safe to call directly (e.g. from a future caller that
|
|
129
|
+
// skips mapArch). Likewise refuse a vcvarsall path containing a
|
|
130
|
+
// double-quote — it would break out of the quoted command template.
|
|
131
|
+
if (!VCVARSALL_ARCH_ALLOWLIST.has(archArg)) {
|
|
132
|
+
throw new Error(`refusing to spawn cmd.exe with unsafe arch arg ${JSON.stringify(archArg)}`);
|
|
133
|
+
}
|
|
134
|
+
if (vcvarsallPath.includes('"')) {
|
|
135
|
+
throw new Error(`refusing to spawn cmd.exe with quote in vcvarsall path ${JSON.stringify(vcvarsallPath)}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
108
138
|
const MARKER = '___WYVRN_ENV_MARKER___';
|
|
109
139
|
// `call` is required so the batch file returns control instead of
|
|
110
140
|
// `exit`-ing the cmd shell; otherwise the `echo` + `set` after it
|
|
@@ -197,7 +227,18 @@ async function loadMsvcEnv({ profile, skipForGenerator }) {
|
|
|
197
227
|
return null;
|
|
198
228
|
}
|
|
199
229
|
|
|
200
|
-
|
|
230
|
+
let archArg;
|
|
231
|
+
try {
|
|
232
|
+
archArg = mapArch(profile.arch);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
// Unknown arch for an MSVC profile is genuinely a configuration bug
|
|
235
|
+
// (MSVC only ships toolsets for x64/x86/arm64/arm). Surface clearly
|
|
236
|
+
// and fall through to the ambient env — CMake will then error with
|
|
237
|
+
// a usable compiler-detection message instead of us silently
|
|
238
|
+
// cross-targeting x64.
|
|
239
|
+
log.warn(` ${err.message}`);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
201
242
|
log.info(` activating MSVC build env: vcvarsall.bat ${archArg} (${vsPath})`);
|
|
202
243
|
const env = await captureVcvarsEnv(vcvarsall, archArg);
|
|
203
244
|
if (!env) {
|
|
@@ -214,4 +255,6 @@ module.exports = {
|
|
|
214
255
|
findVsInstall,
|
|
215
256
|
findVcvarsall,
|
|
216
257
|
mapArch,
|
|
258
|
+
captureVcvarsEnv,
|
|
259
|
+
VCVARSALL_ARCH_ALLOWLIST,
|
|
217
260
|
};
|
package/src/build/recipe.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { substituteTemplateAll } = require('../options');
|
|
4
|
+
const { readRecipeConf } = require('../conf');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The `build` section of a source-repo `wyvrn.json` tells wyvrnpm how to
|
|
@@ -60,15 +61,22 @@ const SUPPORTED_SYSTEMS = new Set(['cmake']);
|
|
|
60
61
|
* no options), any `${options.*}` token in the recipe also fails fast —
|
|
61
62
|
* recipes don't get to reference undeclared options.
|
|
62
63
|
*
|
|
64
|
+
* When `manifestForConfCollisionCheck` is supplied, PLAN-CONF.md §4.7's
|
|
65
|
+
* collision detection runs: any `-DKEY=...` in `build.configure` whose
|
|
66
|
+
* KEY is ALSO declared in recipe-level `conf.cmake.cache` is a hard
|
|
67
|
+
* error. Recipes pick ONE route, avoiding silent duplication.
|
|
68
|
+
*
|
|
63
69
|
* @param {object|null|undefined} rawBuild
|
|
64
70
|
* @param {Record<string, any>|null} [effectiveOptions]
|
|
71
|
+
* @param {object|null} [manifestForConfCollisionCheck]
|
|
65
72
|
* @returns {{ system: string, generators: string[], configure: string[],
|
|
66
73
|
* buildArgs: string[], installDir: string, sourceSubdir: string,
|
|
67
74
|
* requiredTools: string[] }}
|
|
68
|
-
* @throws {Error} when `system` is set but not supported,
|
|
69
|
-
* `${options.<name>}` template references an undeclared option
|
|
75
|
+
* @throws {Error} when `system` is set but not supported, when a
|
|
76
|
+
* `${options.<name>}` template references an undeclared option,
|
|
77
|
+
* or when `build.configure` collides with `conf.cmake.cache`.
|
|
70
78
|
*/
|
|
71
|
-
function normalizeRecipe(rawBuild, effectiveOptions = null) {
|
|
79
|
+
function normalizeRecipe(rawBuild, effectiveOptions = null, manifestForConfCollisionCheck = null) {
|
|
72
80
|
const b = rawBuild ?? {};
|
|
73
81
|
const system = b.system ?? DEFAULT_RECIPE.system;
|
|
74
82
|
if (!SUPPORTED_SYSTEMS.has(system)) {
|
|
@@ -124,6 +132,31 @@ function normalizeRecipe(rawBuild, effectiveOptions = null) {
|
|
|
124
132
|
'build.buildArgs',
|
|
125
133
|
);
|
|
126
134
|
|
|
135
|
+
// PLAN-CONF.md §4.7: recipe-side collision between `build.configure`
|
|
136
|
+
// `-DKEY=...` args and declared `conf.cmake.cache.KEY` is a hard
|
|
137
|
+
// error. Forces the author to pick one route; avoids silent
|
|
138
|
+
// duplication where one route wins opaquely.
|
|
139
|
+
if (manifestForConfCollisionCheck) {
|
|
140
|
+
const recipeConfFlat = readRecipeConf(manifestForConfCollisionCheck);
|
|
141
|
+
const confCacheKeys = new Set(
|
|
142
|
+
Object.keys(recipeConfFlat)
|
|
143
|
+
.filter((k) => k.startsWith('cmake.cache.'))
|
|
144
|
+
.map((k) => k.slice('cmake.cache.'.length)),
|
|
145
|
+
);
|
|
146
|
+
if (confCacheKeys.size > 0) {
|
|
147
|
+
for (const arg of configure) {
|
|
148
|
+
const m = arg.match(/^-D([^=]+)=/);
|
|
149
|
+
if (m && confCacheKeys.has(m[1])) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`recipe collision: "${m[1]}" is set in both ` +
|
|
152
|
+
`build.configure (as ${JSON.stringify(arg)}) and ` +
|
|
153
|
+
`conf.cmake.cache. Pick one route — see claude/PLAN-CONF.md §4.7.`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
127
160
|
return {
|
|
128
161
|
system,
|
|
129
162
|
generators,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const { readManifest, writeManifest } = require('../manifest');
|
|
6
|
+
const { resolveLatestVersion } = require('../resolve');
|
|
7
|
+
const { readConfig } = require('../config');
|
|
8
|
+
const log = require('../logger');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse a `<name>[@<version>]` positional into its parts.
|
|
12
|
+
*
|
|
13
|
+
* `@` inside the version (e.g. tags like `latest`) is allowed — only the
|
|
14
|
+
* first `@` is treated as the separator.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} spec
|
|
17
|
+
* @returns {{ name: string, version: string | null }}
|
|
18
|
+
*/
|
|
19
|
+
function parseSpec(spec) {
|
|
20
|
+
const at = spec.indexOf('@');
|
|
21
|
+
if (at === -1) return { name: spec, version: null };
|
|
22
|
+
if (at === 0) throw new Error(`invalid package spec "${spec}" — missing name before "@"`);
|
|
23
|
+
const name = spec.slice(0, at);
|
|
24
|
+
const version = spec.slice(at + 1);
|
|
25
|
+
if (!version) throw new Error(`invalid package spec "${spec}" — missing version after "@"`);
|
|
26
|
+
return { name, version };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add one or more dependencies to wyvrn.json.
|
|
31
|
+
*
|
|
32
|
+
* wyvrnpm add <name> → resolves latest, writes "^<latest>"
|
|
33
|
+
* wyvrnpm add <name>@<version> → writes "<version>" verbatim (exact or range)
|
|
34
|
+
*
|
|
35
|
+
* Existing entries are replaced. If the prior entry was an object
|
|
36
|
+
* (`{ version, settings, options }`), its settings/options are preserved —
|
|
37
|
+
* only `version` is updated.
|
|
38
|
+
*
|
|
39
|
+
* Does not run install. The user must `wyvrnpm install` afterwards.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} argv
|
|
42
|
+
*/
|
|
43
|
+
async function add(argv) {
|
|
44
|
+
const manifestPath = path.resolve(argv.manifest);
|
|
45
|
+
const specs = (argv.packages ?? []).map(parseSpec);
|
|
46
|
+
|
|
47
|
+
if (specs.length === 0) {
|
|
48
|
+
log.error('no packages given. Usage: wyvrnpm add <name>[@<version>] [...]');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Install sources — only needed when at least one spec omits @version and
|
|
53
|
+
// we have to hit `latest.json`.
|
|
54
|
+
let packageSources;
|
|
55
|
+
if (argv.source && argv.source.length > 0) {
|
|
56
|
+
packageSources = argv.source;
|
|
57
|
+
} else {
|
|
58
|
+
const config = readConfig();
|
|
59
|
+
packageSources = config.installSources.map((s) => s.url);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const needsResolve = specs.some((s) => s.version === null);
|
|
63
|
+
if (needsResolve && packageSources.length === 0) {
|
|
64
|
+
log.error(
|
|
65
|
+
'no install sources configured — cannot look up latest versions.\n' +
|
|
66
|
+
' Pass --source <url>, pin versions explicitly (`name@1.2.3.4`), or run:\n' +
|
|
67
|
+
' wyvrnpm configure add-source --kind install --name ... --url ...',
|
|
68
|
+
);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const manifest = readManifest(manifestPath);
|
|
73
|
+
|
|
74
|
+
// Preserve whichever dependencies key the manifest already uses. If neither
|
|
75
|
+
// exists, create lowercase `dependencies` (the v2 convention).
|
|
76
|
+
const depsKey =
|
|
77
|
+
Object.prototype.hasOwnProperty.call(manifest, 'dependencies') ? 'dependencies' :
|
|
78
|
+
Object.prototype.hasOwnProperty.call(manifest, 'Dependencies') ? 'Dependencies' :
|
|
79
|
+
'dependencies';
|
|
80
|
+
|
|
81
|
+
let deps = manifest[depsKey];
|
|
82
|
+
// Legacy array form → object. Editing by name is the whole point of `add`,
|
|
83
|
+
// and round-tripping as an array would silently drop per-dep settings.
|
|
84
|
+
if (Array.isArray(deps)) {
|
|
85
|
+
const obj = {};
|
|
86
|
+
for (const d of deps) {
|
|
87
|
+
const n = d.Name ?? d.name;
|
|
88
|
+
const v = d.Version ?? d.version;
|
|
89
|
+
if (n && v) obj[n] = v;
|
|
90
|
+
}
|
|
91
|
+
deps = obj;
|
|
92
|
+
} else if (!deps || typeof deps !== 'object') {
|
|
93
|
+
deps = {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const spec of specs) {
|
|
97
|
+
let versionToWrite;
|
|
98
|
+
if (spec.version) {
|
|
99
|
+
versionToWrite = spec.version;
|
|
100
|
+
log.info(`${spec.name}: writing "${versionToWrite}" (as given)`);
|
|
101
|
+
} else {
|
|
102
|
+
const latest = await resolveLatestVersion(spec.name, packageSources, argv.platform, fetch);
|
|
103
|
+
versionToWrite = `^${latest}`;
|
|
104
|
+
log.info(`${spec.name}: latest is ${latest} → writing "${versionToWrite}"`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const prior = deps[spec.name];
|
|
108
|
+
if (prior !== undefined) {
|
|
109
|
+
const priorVer =
|
|
110
|
+
typeof prior === 'string' ? prior :
|
|
111
|
+
(prior && typeof prior === 'object') ? (prior.version ?? prior.Version ?? '(unknown)') :
|
|
112
|
+
'(unknown)';
|
|
113
|
+
log.info(` replacing existing entry: "${priorVer}"`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If the prior entry was an object with settings/options, keep those —
|
|
117
|
+
// only bump the version field. Otherwise write the short-form string.
|
|
118
|
+
if (prior && typeof prior === 'object' && !Array.isArray(prior)) {
|
|
119
|
+
const updated = { ...prior };
|
|
120
|
+
if ('Version' in updated && !('version' in updated)) {
|
|
121
|
+
updated.Version = versionToWrite;
|
|
122
|
+
} else {
|
|
123
|
+
updated.version = versionToWrite;
|
|
124
|
+
}
|
|
125
|
+
deps[spec.name] = updated;
|
|
126
|
+
} else {
|
|
127
|
+
deps[spec.name] = versionToWrite;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
manifest[depsKey] = deps;
|
|
132
|
+
writeManifest(manifestPath, manifest);
|
|
133
|
+
|
|
134
|
+
const n = specs.length;
|
|
135
|
+
log.success(`Updated ${manifestPath} — ${n} package${n !== 1 ? 's' : ''}.`);
|
|
136
|
+
log.info('Run `wyvrnpm install` to download.');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = add;
|
|
140
|
+
module.exports.parseSpec = parseSpec;
|
package/src/commands/build.js
CHANGED
|
@@ -9,6 +9,9 @@ const { loadProfile } = require('../profile');
|
|
|
9
9
|
const { readManifest } = require('../manifest');
|
|
10
10
|
const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
|
|
11
11
|
const { loadMsvcEnv } = require('../build/msvc-env');
|
|
12
|
+
const { archToVsPlatform, isVisualStudioGenerator } = require('../toolchain');
|
|
13
|
+
const { parseCliConf } = require('../conf');
|
|
14
|
+
const { LOCAL_OVERLAY_FILENAME } = require('../conf');
|
|
12
15
|
const install = require('./install');
|
|
13
16
|
const log = require('../logger');
|
|
14
17
|
|
|
@@ -28,8 +31,10 @@ const log = require('../logger');
|
|
|
28
31
|
* @returns {{ stale: boolean, reason: string }}
|
|
29
32
|
*/
|
|
30
33
|
function isToolchainStale({ rootDir, manifestPath }) {
|
|
31
|
-
const lockPath
|
|
32
|
-
const toolchainPath
|
|
34
|
+
const lockPath = path.join(path.dirname(manifestPath), 'wyvrn.lock');
|
|
35
|
+
const toolchainPath = path.join(rootDir, 'wyvrn_internal', 'wyvrn_toolchain.cmake');
|
|
36
|
+
const localOverlayPath = path.join(rootDir, LOCAL_OVERLAY_FILENAME);
|
|
37
|
+
const presetPath = path.join(rootDir, 'CMakePresets.json');
|
|
33
38
|
|
|
34
39
|
if (!fs.existsSync(toolchainPath)) {
|
|
35
40
|
return { stale: true, reason: 'toolchain-missing' };
|
|
@@ -43,6 +48,19 @@ function isToolchainStale({ rootDir, manifestPath }) {
|
|
|
43
48
|
if (lockMtime > toolchainMtime) {
|
|
44
49
|
return { stale: true, reason: 'lock-newer-than-toolchain' };
|
|
45
50
|
}
|
|
51
|
+
|
|
52
|
+
// PLAN-CONF.md §7: if wyvrn.local.json was edited after the preset was
|
|
53
|
+
// written, the preset's cacheVariables no longer reflect the consumer's
|
|
54
|
+
// intent. Re-run install (which regenerates the preset) rather than
|
|
55
|
+
// silently running a stale configure.
|
|
56
|
+
if (fs.existsSync(localOverlayPath) && fs.existsSync(presetPath)) {
|
|
57
|
+
const overlayMtime = fs.statSync(localOverlayPath).mtimeMs;
|
|
58
|
+
const presetMtime = fs.statSync(presetPath).mtimeMs;
|
|
59
|
+
if (overlayMtime > presetMtime) {
|
|
60
|
+
return { stale: true, reason: 'local-overlay-newer-than-preset' };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
return { stale: false, reason: 'fresh' };
|
|
47
65
|
}
|
|
48
66
|
|
|
@@ -70,11 +88,57 @@ function resolveBinaryDir({ rootDir, presetName }) {
|
|
|
70
88
|
|
|
71
89
|
/**
|
|
72
90
|
* Args passed to `cmake` for the configure step.
|
|
73
|
-
*
|
|
91
|
+
*
|
|
92
|
+
* `generator`, when set, appends `-G <name>` after `--preset`. CMake 3.21+
|
|
93
|
+
* lets CLI args override a preset's declared fields, so this swaps the
|
|
94
|
+
* generator at configure time without touching CMakePresets.json. Note:
|
|
95
|
+
* CMake bakes the chosen generator into the cache, so switching generators
|
|
96
|
+
* for an existing build directory requires `--clean` (otherwise CMake
|
|
97
|
+
* errors with "CMAKE_GENERATOR has changed").
|
|
98
|
+
*
|
|
99
|
+
* `vsPlatform`, when set, appends `-A <platform>`. Only meaningful for
|
|
100
|
+
* Visual Studio generators — they default to x64 when unspecified, which
|
|
101
|
+
* silently produces a 64-bit build even for a 32-bit profile. The caller
|
|
102
|
+
* is responsible for deriving this from the profile arch only when the
|
|
103
|
+
* effective generator is VS (see `isVisualStudioGenerator` +
|
|
104
|
+
* `archToVsPlatform`).
|
|
105
|
+
*
|
|
106
|
+
* @param {{ presetName: string, generator?: string|null, vsPlatform?: string|null }} args
|
|
74
107
|
* @returns {string[]}
|
|
75
108
|
*/
|
|
76
|
-
function buildConfigureArgs({
|
|
77
|
-
|
|
109
|
+
function buildConfigureArgs({
|
|
110
|
+
presetName,
|
|
111
|
+
generator = null,
|
|
112
|
+
vsPlatform = null,
|
|
113
|
+
confSets = null,
|
|
114
|
+
confUnsets = null,
|
|
115
|
+
}) {
|
|
116
|
+
const args = ['--preset', presetName];
|
|
117
|
+
if (generator) args.push('-G', generator);
|
|
118
|
+
if (vsPlatform) args.push('-A', vsPlatform);
|
|
119
|
+
|
|
120
|
+
// PLAN-CONF.md §7: CLI --conf at build time applies as -D / -U
|
|
121
|
+
// overrides on top of whatever's baked into the preset. We do NOT
|
|
122
|
+
// silently regenerate the preset — that's a separate install.
|
|
123
|
+
// Confined to the cmake.cache namespace; other namespaces have
|
|
124
|
+
// different sinks (phase 3) and don't apply to `cmake --preset`.
|
|
125
|
+
if (confSets) {
|
|
126
|
+
const prefix = 'cmake.cache.';
|
|
127
|
+
for (const key of Object.keys(confSets).sort()) {
|
|
128
|
+
if (!key.startsWith(prefix)) continue;
|
|
129
|
+
const cmakeKey = key.slice(prefix.length);
|
|
130
|
+
args.push('-D', `${cmakeKey}=${confSets[key]}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (confUnsets) {
|
|
134
|
+
const prefix = 'cmake.cache.';
|
|
135
|
+
for (const key of [...confUnsets].sort()) {
|
|
136
|
+
if (!key.startsWith(prefix)) continue;
|
|
137
|
+
args.push('-U', key.slice(prefix.length));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return args;
|
|
78
142
|
}
|
|
79
143
|
|
|
80
144
|
/**
|
|
@@ -211,8 +275,11 @@ async function build(argv) {
|
|
|
211
275
|
//
|
|
212
276
|
// Same mechanism the source-build path already uses (src/build/msvc-env.js).
|
|
213
277
|
let cmakeEnv = null;
|
|
278
|
+
let effectiveGenerator = argv.generator ?? null;
|
|
279
|
+
let effectiveProfile = null;
|
|
214
280
|
try {
|
|
215
281
|
const { profile: activeProfile } = loadProfile(profileName);
|
|
282
|
+
effectiveProfile = activeProfile;
|
|
216
283
|
// Which generator will the preset actually use? Read it from the
|
|
217
284
|
// project's own recipe if one exists. If not, default to null and let
|
|
218
285
|
// cmake/CMakePresets decide — in which case MSVC env activation is
|
|
@@ -225,16 +292,53 @@ async function build(argv) {
|
|
|
225
292
|
if (recipe.generators.length > 0) [presetGenerator] = recipe.generators;
|
|
226
293
|
} catch { /* templates need options → skip env activation */ }
|
|
227
294
|
}
|
|
295
|
+
// CLI --generator wins — that is the generator CMake will actually use,
|
|
296
|
+
// so it is the one that determines whether vcvarsall must be activated.
|
|
297
|
+
if (!effectiveGenerator) effectiveGenerator = presetGenerator;
|
|
228
298
|
cmakeEnv = await loadMsvcEnv({
|
|
229
299
|
profile: activeProfile,
|
|
230
|
-
skipForGenerator:
|
|
300
|
+
skipForGenerator: effectiveGenerator,
|
|
231
301
|
});
|
|
232
302
|
} catch (err) {
|
|
233
303
|
log.warn(`Could not resolve MSVC env activation: ${err.message}`);
|
|
234
304
|
}
|
|
235
305
|
|
|
236
306
|
// ── Configure ─────────────────────────────────────────────────────────────
|
|
237
|
-
|
|
307
|
+
// Auto-emit `-A <platform>` for Visual Studio generators, mapped from the
|
|
308
|
+
// active profile's arch. VS otherwise defaults to x64 regardless of the
|
|
309
|
+
// profile, which breaks x86 builds: CMake produces 64-bit artefacts but
|
|
310
|
+
// find_package() rejects the 32-bit installed dependencies with "version
|
|
311
|
+
// not compatible" (actually a CMAKE_SIZEOF_VOID_P mismatch, not a
|
|
312
|
+
// version-number check).
|
|
313
|
+
let vsPlatform = null;
|
|
314
|
+
if (isVisualStudioGenerator(effectiveGenerator) && effectiveProfile) {
|
|
315
|
+
vsPlatform = archToVsPlatform(effectiveProfile.arch);
|
|
316
|
+
if (vsPlatform) {
|
|
317
|
+
log.info(`Visual Studio generator + arch=${effectiveProfile.arch} → passing -A ${vsPlatform}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// CLI --conf → -D / -U passthrough (PLAN-CONF.md §7). The preset's
|
|
322
|
+
// baked-in cacheVariables (from the last install) already reflect the
|
|
323
|
+
// recipe + local-overlay layers; CLI conf layers on top for this
|
|
324
|
+
// invocation only.
|
|
325
|
+
let cliConfSets, cliConfUnsets;
|
|
326
|
+
try {
|
|
327
|
+
const cli = parseCliConf(argv.conf);
|
|
328
|
+
cliConfSets = cli.sets;
|
|
329
|
+
cliConfUnsets = cli.unsets;
|
|
330
|
+
} catch (err) {
|
|
331
|
+
log.error(`--conf parse error: ${err.message}`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const configureArgs = buildConfigureArgs({
|
|
336
|
+
presetName,
|
|
337
|
+
generator: argv.generator,
|
|
338
|
+
vsPlatform,
|
|
339
|
+
confSets: cliConfSets,
|
|
340
|
+
confUnsets: cliConfUnsets,
|
|
341
|
+
});
|
|
238
342
|
log.info(`cmake ${configureArgs.join(' ')}`);
|
|
239
343
|
await runCmake(configureArgs, cmakeEnv);
|
|
240
344
|
|