wyvrnpm 2.8.1 → 2.10.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/README.md +75 -4
- package/bin/wyvrn.js +93 -2
- package/claude/skills/wyvrnpm.skill +0 -0
- package/package.json +1 -1
- package/src/bootstrap/cookbook.js +196 -0
- package/src/bootstrap/detect.js +150 -0
- package/src/bootstrap/index.js +220 -0
- package/src/bootstrap/version.js +72 -0
- package/src/build/index.js +3 -2
- package/src/commands/add.js +2 -1
- package/src/commands/bootstrap.js +96 -0
- package/src/commands/build.js +109 -44
- package/src/commands/install.js +674 -665
- package/src/commands/link.js +320 -319
- package/src/commands/profile.js +237 -237
- package/src/commands/publish.js +435 -420
- package/src/commands/show.js +18 -13
- package/src/context.js +230 -0
- package/src/http-fetch.js +53 -0
- package/src/providers/file.js +5 -19
- package/src/providers/http.js +26 -26
- package/src/providers/s3.js +29 -25
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const { tagToVersion, nameFromUrl } = require('./version');
|
|
8
|
+
const { detectFromRepo } = require('./detect');
|
|
9
|
+
const { lookupCookbook } = require('./cookbook');
|
|
10
|
+
const log = require('../logger');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Clone `gitUrl` into `destDir` so bootstrap can read the tree.
|
|
14
|
+
* Defaults to a full clone — bootstrap's target audience is "I forked
|
|
15
|
+
* these repos to maintain them", so they want the history. `--shallow`
|
|
16
|
+
* opts into `--depth 1` (faster, but `git describe --tags` will miss
|
|
17
|
+
* tags that aren't reachable from the single fetched commit, so the
|
|
18
|
+
* version detection degrades to HEAD SHA placeholder + TODO).
|
|
19
|
+
*
|
|
20
|
+
* Uses the user's ambient git auth (SSH keys, credential helper). We do
|
|
21
|
+
* NOT manage credentials here.
|
|
22
|
+
*
|
|
23
|
+
* @param {{ gitUrl: string, destDir: string, ref?: string | null, shallow?: boolean }} opts
|
|
24
|
+
*/
|
|
25
|
+
function cloneRepo({ gitUrl, destDir, ref = null, shallow = false }) {
|
|
26
|
+
const args = ['clone'];
|
|
27
|
+
if (shallow) args.push('--depth', '1');
|
|
28
|
+
if (ref) args.push('--branch', ref);
|
|
29
|
+
args.push('--', gitUrl, destDir);
|
|
30
|
+
log.info(` git ${args.join(' ')}`);
|
|
31
|
+
execFileSync('git', args, { stdio: 'inherit' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read HEAD SHA + nearest tag out of an existing clone. Pure
|
|
36
|
+
* read-side — never writes. Returns nulls on failure so the caller
|
|
37
|
+
* can surface a TODO rather than abort.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} repoDir
|
|
40
|
+
* @returns {{ sha: string | null, tag: string | null }}
|
|
41
|
+
*/
|
|
42
|
+
function readGitState(repoDir) {
|
|
43
|
+
const safe = (args) => {
|
|
44
|
+
try {
|
|
45
|
+
return execFileSync('git', args, { cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
46
|
+
.toString('utf8').trim() || null;
|
|
47
|
+
} catch { return null; }
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
sha: safe(['rev-parse', 'HEAD']),
|
|
51
|
+
// `--abbrev=0` gives the nearest tag without the "-<N>-g<sha>" suffix.
|
|
52
|
+
tag: safe(['describe', '--tags', '--abbrev=0']),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compose a first-draft `wyvrn.json` from detection + cookbook data.
|
|
58
|
+
* Pure: no filesystem writes, no network. `compose` is the unit-testable
|
|
59
|
+
* seam between orchestration (`bootstrap`) and persistence.
|
|
60
|
+
*
|
|
61
|
+
* Precedence for each field (highest wins):
|
|
62
|
+
* CLI override → cookbook hint → filesystem detection → safe default.
|
|
63
|
+
*
|
|
64
|
+
* @param {{
|
|
65
|
+
* url: string,
|
|
66
|
+
* repoDir: string,
|
|
67
|
+
* overrides?: { name?: string, version?: string, kind?: string, description?: string },
|
|
68
|
+
* detect?: ReturnType<typeof detectFromRepo>,
|
|
69
|
+
* gitState?: { sha: string | null, tag: string | null },
|
|
70
|
+
* }} args
|
|
71
|
+
* @returns {{ manifest: object, todos: string[] }}
|
|
72
|
+
*/
|
|
73
|
+
function compose({ url, repoDir, overrides = {}, detect = null, gitState = null }) {
|
|
74
|
+
const todos = [];
|
|
75
|
+
const detection = detect ?? detectFromRepo(repoDir);
|
|
76
|
+
const { sha, tag } = gitState ?? { sha: null, tag: null };
|
|
77
|
+
|
|
78
|
+
const repoTail = nameFromUrl(url);
|
|
79
|
+
const cookbook = lookupCookbook(repoTail) ?? {};
|
|
80
|
+
|
|
81
|
+
const name = overrides.name || repoTail;
|
|
82
|
+
if (!name) todos.push('name could not be derived from the URL — set "name" manually.');
|
|
83
|
+
|
|
84
|
+
let version = overrides.version || null;
|
|
85
|
+
if (!version && tag) {
|
|
86
|
+
version = tagToVersion(tag);
|
|
87
|
+
if (!version) {
|
|
88
|
+
todos.push(`nearest tag "${tag}" doesn't match a clean numeric version — set "version" manually (used HEAD SHA as a placeholder).`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!version) {
|
|
92
|
+
version = '0.0.0.0';
|
|
93
|
+
todos.push('no usable git tag found — "version" defaulted to 0.0.0.0. Pin to a real release before publish.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const kind = overrides.kind || cookbook.kind || detection.kind;
|
|
97
|
+
if (kind === 'unknown') {
|
|
98
|
+
todos.push('kind could not be detected — set "kind" to ConsoleApp | StaticLib | DynamicLib | HeaderOnlyLib | WebApp | Utility | Service | TestProject.');
|
|
99
|
+
}
|
|
100
|
+
for (const note of detection.notes) todos.push(`detect: ${note}`);
|
|
101
|
+
for (const note of (cookbook.notes ?? [])) todos.push(`cookbook: ${note}`);
|
|
102
|
+
|
|
103
|
+
// Safe default recipe for any CMake project. Header-only still goes
|
|
104
|
+
// through cmake --install so the INTERFACE target + install() rules
|
|
105
|
+
// land in the published zip.
|
|
106
|
+
const build = {
|
|
107
|
+
system: 'cmake',
|
|
108
|
+
configs: ['Debug', 'Release', 'RelWithDebInfo', 'MinSizeRel'],
|
|
109
|
+
configure: cookbook.configure ? [...cookbook.configure] : [],
|
|
110
|
+
installDir: 'install',
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const manifest = {
|
|
114
|
+
name,
|
|
115
|
+
version,
|
|
116
|
+
schemaVersion: 2,
|
|
117
|
+
kind: kind === 'unknown' ? 'StaticLib' : kind,
|
|
118
|
+
description: overrides.description || `TODO: one-line description of ${name}`,
|
|
119
|
+
dependencies: cookbook.dependencies ? { ...cookbook.dependencies } : {},
|
|
120
|
+
build,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (cookbook.dependencies) {
|
|
124
|
+
todos.push(
|
|
125
|
+
`dependencies seeded from cookbook: ${Object.keys(cookbook.dependencies).join(', ')}. ` +
|
|
126
|
+
`Verify versions and pin ranges before publish.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// `gitRepo` / `gitSha` are normally captured by `wyvrnpm publish` at
|
|
131
|
+
// publish time — we leave them out of the manifest (not an author
|
|
132
|
+
// field) but surface them in the TODO report so the user can
|
|
133
|
+
// double-check the clone landed at the ref they expected.
|
|
134
|
+
if (sha) todos.push(`git HEAD at bootstrap: ${sha}`);
|
|
135
|
+
if (tag) todos.push(`nearest git tag: ${tag}`);
|
|
136
|
+
|
|
137
|
+
return { manifest, todos };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Full bootstrap flow: optionally clone, read git state, compose manifest,
|
|
142
|
+
* write it to disk. Returns the result so callers (CLI, tests) can decide
|
|
143
|
+
* what to print.
|
|
144
|
+
*
|
|
145
|
+
* @param {{
|
|
146
|
+
* url: string,
|
|
147
|
+
* dest: string,
|
|
148
|
+
* ref?: string | null,
|
|
149
|
+
* name?: string,
|
|
150
|
+
* version?: string,
|
|
151
|
+
* kind?: string,
|
|
152
|
+
* description?: string,
|
|
153
|
+
* noClone?: boolean,
|
|
154
|
+
* dryRun?: boolean,
|
|
155
|
+
* force?: boolean,
|
|
156
|
+
* }} opts
|
|
157
|
+
* @returns {{ manifest: object, todos: string[], manifestPath: string, wrote: boolean }}
|
|
158
|
+
*/
|
|
159
|
+
function bootstrap(opts) {
|
|
160
|
+
const {
|
|
161
|
+
url, dest, ref = null,
|
|
162
|
+
name, version, kind, description,
|
|
163
|
+
noClone = false, dryRun = false, force = false, shallow = false,
|
|
164
|
+
} = opts;
|
|
165
|
+
|
|
166
|
+
if (!url && !noClone) throw new Error('bootstrap: --url (or a positional git URL) is required unless --no-clone');
|
|
167
|
+
if (!dest) throw new Error('bootstrap: --dest (or positional) is required');
|
|
168
|
+
|
|
169
|
+
const repoDir = path.resolve(dest);
|
|
170
|
+
|
|
171
|
+
if (!noClone) {
|
|
172
|
+
if (fs.existsSync(repoDir) && !force) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`bootstrap: destination already exists: ${repoDir}\n` +
|
|
175
|
+
' Pass --force to reuse it, or --no-clone to operate on it in place.',
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (fs.existsSync(repoDir) && force) {
|
|
179
|
+
fs.rmSync(repoDir, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
cloneRepo({ gitUrl: url, destDir: repoDir, ref, shallow });
|
|
182
|
+
} else if (!fs.existsSync(repoDir)) {
|
|
183
|
+
throw new Error(`bootstrap: --no-clone but directory does not exist: ${repoDir}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const detect = detectFromRepo(repoDir);
|
|
187
|
+
const gitState = readGitState(repoDir);
|
|
188
|
+
|
|
189
|
+
const { manifest, todos } = compose({
|
|
190
|
+
url: url || gitRemoteUrl(repoDir) || '',
|
|
191
|
+
repoDir,
|
|
192
|
+
overrides: { name, version, kind, description },
|
|
193
|
+
detect,
|
|
194
|
+
gitState,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const manifestPath = path.join(repoDir, 'wyvrn.json');
|
|
198
|
+
let wrote = false;
|
|
199
|
+
if (!dryRun) {
|
|
200
|
+
if (fs.existsSync(manifestPath) && !force) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`bootstrap: ${manifestPath} already exists. Pass --force to overwrite, or --dry-run to preview only.`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
206
|
+
wrote = true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { manifest, todos, manifestPath, wrote };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function gitRemoteUrl(repoDir) {
|
|
213
|
+
try {
|
|
214
|
+
return execFileSync('git', ['remote', 'get-url', 'origin'], {
|
|
215
|
+
cwd: repoDir, stdio: ['ignore', 'pipe', 'ignore'],
|
|
216
|
+
}).toString('utf8').trim() || null;
|
|
217
|
+
} catch { return null; }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { bootstrap, compose, readGitState, cloneRepo };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse an upstream git tag into wyvrnpm's 4-part version scheme.
|
|
5
|
+
*
|
|
6
|
+
* Most C++ OSS projects publish 3-part semver tags (`v1.2.3`, `11.0.2`,
|
|
7
|
+
* `1.3.12`). wyvrnpm uses a 4-part `major.minor.patch.build` scheme
|
|
8
|
+
* throughout (see CLAUDE.md §4.2). We pad the upstream tag with `.0` for
|
|
9
|
+
* the build component so published manifests look like `1.3.12.0`.
|
|
10
|
+
*
|
|
11
|
+
* Accepted tag shapes:
|
|
12
|
+
* v1.2.3 → "1.2.3.0"
|
|
13
|
+
* 1.2.3 → "1.2.3.0"
|
|
14
|
+
* v1.2.3.4 → "1.2.3.4"
|
|
15
|
+
* v11.0 → "11.0.0.0" (2-part — padded)
|
|
16
|
+
* v11 → "11.0.0.0" (1-part — padded)
|
|
17
|
+
* release-1.2 → "1.2.0.0" (strips a leading "release-" / "ver-")
|
|
18
|
+
*
|
|
19
|
+
* Rejected (returns null):
|
|
20
|
+
* empty / non-string
|
|
21
|
+
* tags with suffixes ("1.2.3-rc1", "1.2.3+git", "1.2.3a") — callers
|
|
22
|
+
* fall back to the HEAD SHA and flag a TODO
|
|
23
|
+
* tags with 5+ numeric components
|
|
24
|
+
*
|
|
25
|
+
* @param {string | null} tag
|
|
26
|
+
* @returns {string | null} 4-part version, or null when the tag can't be
|
|
27
|
+
* cleanly promoted.
|
|
28
|
+
*/
|
|
29
|
+
function tagToVersion(tag) {
|
|
30
|
+
if (typeof tag !== 'string') return null;
|
|
31
|
+
let t = tag.trim();
|
|
32
|
+
if (!t) return null;
|
|
33
|
+
|
|
34
|
+
// Strip the most common prefixes authors use on git tags. Longest
|
|
35
|
+
// alternatives first so `version-2.0` matches the whole word, not
|
|
36
|
+
// just the leading `v`. We keep the allowlist tight — we'd rather
|
|
37
|
+
// fall through than silently misparse `release-candidate-1.2.3`.
|
|
38
|
+
t = t.replace(/^(version|release|ver|v)[-_.]?/i, '');
|
|
39
|
+
|
|
40
|
+
// Must be bare numeric segments separated by dots, nothing else.
|
|
41
|
+
if (!/^\d+(\.\d+){0,3}$/.test(t)) return null;
|
|
42
|
+
|
|
43
|
+
const parts = t.split('.').map((n) => parseInt(n, 10));
|
|
44
|
+
if (parts.some((n) => !Number.isFinite(n) || n < 0)) return null;
|
|
45
|
+
|
|
46
|
+
while (parts.length < 4) parts.push(0);
|
|
47
|
+
return parts.join('.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Derive a wyvrnpm package name from a git URL.
|
|
52
|
+
*
|
|
53
|
+
* https://github.com/fmtlib/fmt.git → "fmt"
|
|
54
|
+
* git@github.com:fmtlib/fmt.git → "fmt"
|
|
55
|
+
* https://gitlab.com/bzip2/bzip2.git → "bzip2"
|
|
56
|
+
* https://github.com/Tencent/rapidjson.git → "rapidjson"
|
|
57
|
+
* https://github.com/nlohmann/json.git → "json" (caller may want "nlohmann-json")
|
|
58
|
+
*
|
|
59
|
+
* Pure: does not hit the network.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} url
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
function nameFromUrl(url) {
|
|
65
|
+
if (typeof url !== 'string' || !url.trim()) return '';
|
|
66
|
+
const trimmed = url.trim().replace(/\/$/, '');
|
|
67
|
+
// Split on the last slash or colon (SSH form).
|
|
68
|
+
const tail = trimmed.split(/[/:]/).pop() || '';
|
|
69
|
+
return tail.replace(/\.git$/i, '');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { tagToVersion, nameFromUrl };
|
package/src/build/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const { hashProfile, mergeProfile, sha256Of } = require('../profile');
|
|
|
15
15
|
const { generateToolchain } = require('../toolchain');
|
|
16
16
|
const { normalizeOptionsDeclaration, resolveEffectiveOptions } = require('../options');
|
|
17
17
|
const { readRecipeConf, cmakeCacheVariables } = require('../conf');
|
|
18
|
+
const { wyvrnFetch } = require('../http-fetch');
|
|
18
19
|
const log = require('../logger');
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -195,7 +196,7 @@ async function buildFromSource(args) {
|
|
|
195
196
|
log.info(` resolving ${Object.keys(depsForResolution).length} transitive dep(s)`);
|
|
196
197
|
const transitiveManifests = new Map();
|
|
197
198
|
const resolvedDeps = await resolveDependencies(
|
|
198
|
-
depsForResolution, packageSources, platform,
|
|
199
|
+
depsForResolution, packageSources, platform, wyvrnFetch, new Map(),
|
|
199
200
|
transitiveManifests,
|
|
200
201
|
);
|
|
201
202
|
|
|
@@ -231,7 +232,7 @@ async function buildFromSource(args) {
|
|
|
231
232
|
if (enrichedDeps.size > 0) {
|
|
232
233
|
await downloadDependencies(
|
|
233
234
|
enrichedDeps, packageSources, platform, paths.internal,
|
|
234
|
-
|
|
235
|
+
wyvrnFetch, Math.max(30, Math.round(timeoutMs / 1000)),
|
|
235
236
|
{ ...authOptions, buildMode },
|
|
236
237
|
);
|
|
237
238
|
} else {
|
package/src/commands/add.js
CHANGED
|
@@ -5,6 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const { readManifest, writeManifest } = require('../manifest');
|
|
6
6
|
const { resolveLatestVersion } = require('../resolve');
|
|
7
7
|
const { readConfig } = require('../config');
|
|
8
|
+
const { wyvrnFetch } = require('../http-fetch');
|
|
8
9
|
const log = require('../logger');
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -99,7 +100,7 @@ async function add(argv) {
|
|
|
99
100
|
versionToWrite = spec.version;
|
|
100
101
|
log.info(`${spec.name}: writing "${versionToWrite}" (as given)`);
|
|
101
102
|
} else {
|
|
102
|
-
const latest = await resolveLatestVersion(spec.name, packageSources, argv.platform,
|
|
103
|
+
const latest = await resolveLatestVersion(spec.name, packageSources, argv.platform, wyvrnFetch);
|
|
103
104
|
versionToWrite = `^${latest}`;
|
|
104
105
|
log.info(`${spec.name}: latest is ${latest} → writing "${versionToWrite}"`);
|
|
105
106
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const { bootstrap } = require('../bootstrap');
|
|
6
|
+
const { nameFromUrl } = require('../bootstrap/version');
|
|
7
|
+
const log = require('../logger');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `wyvrnpm bootstrap <git-url>` — scaffold a first-draft `wyvrn.json`
|
|
11
|
+
* inside a cloned OSS repo so it can be built + published as a wyvrnpm
|
|
12
|
+
* package. See CLAUDE.md §5.9 and
|
|
13
|
+
* claude/skills/wyvrnpm/references/oss-conversion-patterns.md.
|
|
14
|
+
*
|
|
15
|
+
* Typical use — batch-convert a list of OSS libraries into wyvrnpm:
|
|
16
|
+
*
|
|
17
|
+
* while read url; do
|
|
18
|
+
* wyvrnpm bootstrap "$url" --dest "./pkgs/$(basename "$url" .git)"
|
|
19
|
+
* done < urls.txt
|
|
20
|
+
*
|
|
21
|
+
* The output is always a *first draft* — bootstrap prints a TODO report
|
|
22
|
+
* flagging gotchas that need human review (install gates, test-skip
|
|
23
|
+
* flags upstream-specific, missing CMakeLists.txt on header-drop repos
|
|
24
|
+
* like stb/imgui). Always review before `wyvrnpm publish`.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} argv
|
|
27
|
+
*/
|
|
28
|
+
async function bootstrapCmd(argv) {
|
|
29
|
+
const url = argv.url ?? argv._?.[1] ?? null;
|
|
30
|
+
if (!url && !argv.noClone) {
|
|
31
|
+
log.error('bootstrap: missing git URL.\n Usage: wyvrnpm bootstrap <git-url> [--dest <dir>]');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dest = argv.dest || path.resolve(nameFromUrl(url ?? '') || 'package');
|
|
36
|
+
|
|
37
|
+
let result;
|
|
38
|
+
try {
|
|
39
|
+
result = bootstrap({
|
|
40
|
+
url,
|
|
41
|
+
dest,
|
|
42
|
+
ref: argv.ref,
|
|
43
|
+
name: argv.name,
|
|
44
|
+
version: argv.version,
|
|
45
|
+
kind: argv.kind,
|
|
46
|
+
description: argv.description,
|
|
47
|
+
noClone: Boolean(argv.noClone),
|
|
48
|
+
dryRun: Boolean(argv.dryRun),
|
|
49
|
+
force: Boolean(argv.force),
|
|
50
|
+
});
|
|
51
|
+
} catch (err) {
|
|
52
|
+
log.error(err.message);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { manifest, todos, manifestPath, wrote } = result;
|
|
57
|
+
|
|
58
|
+
if (argv.format === 'json') {
|
|
59
|
+
process.stdout.write(JSON.stringify({
|
|
60
|
+
command: 'bootstrap',
|
|
61
|
+
wyvrnpmVersion: require('../../package.json').version,
|
|
62
|
+
dest,
|
|
63
|
+
manifestPath,
|
|
64
|
+
wrote,
|
|
65
|
+
manifest,
|
|
66
|
+
todos,
|
|
67
|
+
}, null, 2) + '\n');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log();
|
|
72
|
+
console.log(`─── ${manifest.name}@${manifest.version} (${manifest.kind}) ───`);
|
|
73
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
74
|
+
console.log();
|
|
75
|
+
|
|
76
|
+
if (wrote) log.success(`Wrote ${manifestPath}`);
|
|
77
|
+
else log.info(`Dry run — no manifest written.`);
|
|
78
|
+
|
|
79
|
+
if (todos.length > 0) {
|
|
80
|
+
console.log();
|
|
81
|
+
console.log('TODOs — review before `wyvrnpm publish`:');
|
|
82
|
+
for (const t of todos) console.log(` • ${t}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (wrote) {
|
|
86
|
+
console.log();
|
|
87
|
+
console.log('Next steps:');
|
|
88
|
+
console.log(` cd ${path.relative(process.cwd(), path.resolve(dest)) || '.'}`);
|
|
89
|
+
console.log(' wyvrnpm install');
|
|
90
|
+
console.log(' wyvrnpm build --install --all-configs');
|
|
91
|
+
console.log(' # review the install tree, then:');
|
|
92
|
+
console.log(' wyvrnpm publish --source <your-s3-publish-source>');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = bootstrapCmd;
|
package/src/commands/build.js
CHANGED
|
@@ -4,14 +4,11 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawn } = require('child_process');
|
|
6
6
|
|
|
7
|
-
const {
|
|
8
|
-
const { loadProfile } = require('../profile');
|
|
9
|
-
const { readManifest } = require('../manifest');
|
|
10
|
-
const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
|
|
7
|
+
const { normalizeRecipe, hasBuildRecipe, VALID_CMAKE_CONFIGS, DEFAULT_RECIPE } = require('../build/recipe');
|
|
11
8
|
const { loadMsvcEnv } = require('../build/msvc-env');
|
|
12
9
|
const { archToVsPlatform, isVisualStudioGenerator } = require('../toolchain');
|
|
13
|
-
const { parseCliConf } = require('../conf');
|
|
14
10
|
const { LOCAL_OVERLAY_FILENAME } = require('../conf');
|
|
11
|
+
const { buildContext } = require('../context');
|
|
15
12
|
const install = require('./install');
|
|
16
13
|
const log = require('../logger');
|
|
17
14
|
|
|
@@ -141,6 +138,51 @@ function buildConfigureArgs({
|
|
|
141
138
|
return args;
|
|
142
139
|
}
|
|
143
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Resolve the list of CMake configs to build (and optionally install).
|
|
143
|
+
*
|
|
144
|
+
* Precedence:
|
|
145
|
+
* 1. `--all-configs` → recipe.configs (falls back to all four canonical
|
|
146
|
+
* configs when no recipe declares any).
|
|
147
|
+
* 2. `--config` → comma-separated list, validated against
|
|
148
|
+
* VALID_CMAKE_CONFIGS. Order preserved. Duplicates removed.
|
|
149
|
+
* 3. default → `['Release']`.
|
|
150
|
+
*
|
|
151
|
+
* Configure runs once per `wyvrnpm build` invocation regardless — multi-
|
|
152
|
+
* config generators (VS, Xcode, Ninja Multi-Config) produce all configs
|
|
153
|
+
* from one configure step. Single-config generators (plain Ninja, Make)
|
|
154
|
+
* bake CMAKE_BUILD_TYPE into the cache, so `cmake --build --config X`
|
|
155
|
+
* effectively ignores X for them; building two configs against a single-
|
|
156
|
+
* config generator rebuilds the same binary twice. That's wasteful but
|
|
157
|
+
* correct; fixing it properly requires per-config build dirs, which is
|
|
158
|
+
* out of scope for this helper.
|
|
159
|
+
*
|
|
160
|
+
* @param {{ argv: object, recipeConfigs?: string[] }} args
|
|
161
|
+
* @returns {string[]}
|
|
162
|
+
*/
|
|
163
|
+
function resolveConfigList({ argv, recipeConfigs = null }) {
|
|
164
|
+
if (argv.allConfigs) {
|
|
165
|
+
const fromRecipe = (recipeConfigs ?? []).filter((c) => VALID_CMAKE_CONFIGS.has(c));
|
|
166
|
+
return fromRecipe.length > 0 ? [...fromRecipe] : [...DEFAULT_RECIPE.configs];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const raw = typeof argv.config === 'string' && argv.config.trim() ? argv.config : 'Release';
|
|
170
|
+
const list = raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
171
|
+
|
|
172
|
+
const invalid = list.filter((c) => !VALID_CMAKE_CONFIGS.has(c));
|
|
173
|
+
if (invalid.length > 0) {
|
|
174
|
+
const valid = [...VALID_CMAKE_CONFIGS].join(' | ');
|
|
175
|
+
throw new Error(`invalid --config "${invalid.join(', ')}" — expected ${valid}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
const deduped = [];
|
|
180
|
+
for (const c of list) {
|
|
181
|
+
if (!seen.has(c)) { seen.add(c); deduped.push(c); }
|
|
182
|
+
}
|
|
183
|
+
return deduped.length > 0 ? deduped : ['Release'];
|
|
184
|
+
}
|
|
185
|
+
|
|
144
186
|
/**
|
|
145
187
|
* Args passed to `cmake` for the build step.
|
|
146
188
|
*
|
|
@@ -225,13 +267,34 @@ function runCmake(args, env = null) {
|
|
|
225
267
|
* @param {object} argv
|
|
226
268
|
*/
|
|
227
269
|
async function build(argv) {
|
|
228
|
-
|
|
229
|
-
|
|
270
|
+
// Shared per-invocation state. See src/context.js +
|
|
271
|
+
// claude/PLAN-COMMAND-CONTEXT.md. Parses --conf up front so a malformed
|
|
272
|
+
// flag fails here instead of after cmake's already configured.
|
|
273
|
+
let ctx;
|
|
274
|
+
try {
|
|
275
|
+
ctx = buildContext(argv);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
log.error(err.message);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
230
280
|
|
|
231
|
-
const
|
|
232
|
-
const profileName
|
|
281
|
+
const { rootDir, manifestPath } = ctx;
|
|
282
|
+
const { name: profileName, profile: activeProfile } = ctx.profiles.host;
|
|
233
283
|
const presetName = resolvePresetName({ presetOverride: argv.preset, profileName });
|
|
234
|
-
|
|
284
|
+
|
|
285
|
+
// Resolve the list of configs up front so a malformed --config fails
|
|
286
|
+
// before auto-install kicks off. `--all-configs` consults the recipe
|
|
287
|
+
// (if any); otherwise comma-separated --config, defaulting to Release.
|
|
288
|
+
let configList;
|
|
289
|
+
try {
|
|
290
|
+
const recipe = hasBuildRecipe(ctx.manifest)
|
|
291
|
+
? (() => { try { return normalizeRecipe(ctx.manifest.build, null); } catch { return null; } })()
|
|
292
|
+
: null;
|
|
293
|
+
configList = resolveConfigList({ argv, recipeConfigs: recipe?.configs ?? null });
|
|
294
|
+
} catch (err) {
|
|
295
|
+
log.error(err.message);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
235
298
|
|
|
236
299
|
// ── Stale check / auto-install ────────────────────────────────────────────
|
|
237
300
|
// `--auto-install` gates the "run `wyvrnpm install` first when the
|
|
@@ -278,13 +341,12 @@ async function build(argv) {
|
|
|
278
341
|
let effectiveGenerator = argv.generator ?? null;
|
|
279
342
|
let effectiveProfile = null;
|
|
280
343
|
try {
|
|
281
|
-
const { profile: activeProfile } = loadProfile(profileName);
|
|
282
344
|
effectiveProfile = activeProfile;
|
|
283
345
|
// Which generator will the preset actually use? Read it from the
|
|
284
346
|
// project's own recipe if one exists. If not, default to null and let
|
|
285
347
|
// cmake/CMakePresets decide — in which case MSVC env activation is
|
|
286
348
|
// skipped (because VS is usually the default on Windows).
|
|
287
|
-
const manifest =
|
|
349
|
+
const manifest = ctx.manifest;
|
|
288
350
|
let presetGenerator = null;
|
|
289
351
|
if (hasBuildRecipe(manifest)) {
|
|
290
352
|
try {
|
|
@@ -321,16 +383,10 @@ async function build(argv) {
|
|
|
321
383
|
// CLI --conf → -D / -U passthrough (PLAN-CONF.md §7). The preset's
|
|
322
384
|
// baked-in cacheVariables (from the last install) already reflect the
|
|
323
385
|
// recipe + local-overlay layers; CLI conf layers on top for this
|
|
324
|
-
// invocation only.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
cliConfSets = cli.sets;
|
|
329
|
-
cliConfUnsets = cli.unsets;
|
|
330
|
-
} catch (err) {
|
|
331
|
-
log.error(`--conf parse error: ${err.message}`);
|
|
332
|
-
process.exit(1);
|
|
333
|
-
}
|
|
386
|
+
// invocation only. ctx.cliConf is pre-parsed by buildContext; malformed
|
|
387
|
+
// values already threw above.
|
|
388
|
+
const cliConfSets = ctx.cliConf.sets;
|
|
389
|
+
const cliConfUnsets = ctx.cliConf.unsets;
|
|
334
390
|
|
|
335
391
|
const configureArgs = buildConfigureArgs({
|
|
336
392
|
presetName,
|
|
@@ -342,35 +398,43 @@ async function build(argv) {
|
|
|
342
398
|
log.info(`cmake ${configureArgs.join(' ')}`);
|
|
343
399
|
await runCmake(configureArgs, cmakeEnv);
|
|
344
400
|
|
|
345
|
-
// ── Build
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
// defaults to whatever was baked into the cache at configure time; pass
|
|
359
|
-
// `--install-prefix` to override without reconfiguring.
|
|
360
|
-
if (argv.install) {
|
|
361
|
-
const installArgs = buildInstallArgs({
|
|
401
|
+
// ── Build (+ optional install) per config ─────────────────────────────────
|
|
402
|
+
// Configure ran once above. Multi-config generators (VS, Xcode, Ninja
|
|
403
|
+
// Multi-Config) produce every config from that one configure step; for
|
|
404
|
+
// single-config generators each iteration simply re-`cmake --build`s the
|
|
405
|
+
// same baked CMAKE_BUILD_TYPE (harmless duplication). `--install` runs
|
|
406
|
+
// per config too — the usual reason to want multi-config is so that a
|
|
407
|
+
// downstream `find_package()` can pick whichever it needs.
|
|
408
|
+
if (configList.length > 1) {
|
|
409
|
+
log.info(`Building ${configList.length} configs: ${configList.join(', ')}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (const config of configList) {
|
|
413
|
+
const buildArgs = buildBuildArgs({
|
|
362
414
|
binaryDir,
|
|
363
|
-
config
|
|
364
|
-
|
|
415
|
+
config,
|
|
416
|
+
target: argv.target,
|
|
417
|
+
verbose: argv.verbose,
|
|
418
|
+
passthru: argv['--'] ?? [],
|
|
365
419
|
});
|
|
366
|
-
log.info(`cmake ${
|
|
367
|
-
await runCmake(
|
|
420
|
+
log.info(`cmake ${buildArgs.join(' ')}`);
|
|
421
|
+
await runCmake(buildArgs, cmakeEnv);
|
|
422
|
+
|
|
423
|
+
if (argv.install) {
|
|
424
|
+
const installArgs = buildInstallArgs({
|
|
425
|
+
binaryDir,
|
|
426
|
+
config,
|
|
427
|
+
prefix: argv.installPrefix,
|
|
428
|
+
});
|
|
429
|
+
log.info(`cmake ${installArgs.join(' ')}`);
|
|
430
|
+
await runCmake(installArgs, cmakeEnv);
|
|
431
|
+
}
|
|
368
432
|
}
|
|
369
433
|
|
|
370
434
|
const suffix = argv.install
|
|
371
435
|
? ` + installed${argv.installPrefix ? ` to ${argv.installPrefix}` : ''}`
|
|
372
436
|
: '';
|
|
373
|
-
log.success(`Build complete: ${binaryDir} (${
|
|
437
|
+
log.success(`Build complete: ${binaryDir} (${configList.join(', ')})${suffix}`);
|
|
374
438
|
}
|
|
375
439
|
|
|
376
440
|
module.exports = build;
|
|
@@ -381,6 +445,7 @@ module.exports._helpers = {
|
|
|
381
445
|
isToolchainStale,
|
|
382
446
|
resolvePresetName,
|
|
383
447
|
resolveBinaryDir,
|
|
448
|
+
resolveConfigList,
|
|
384
449
|
buildConfigureArgs,
|
|
385
450
|
buildBuildArgs,
|
|
386
451
|
buildInstallArgs,
|