wyvrnpm 2.1.0 → 2.4.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 +325 -6
- package/bin/wyvrn.js +167 -14
- package/claude/skills/wyvrnpm.skill +0 -0
- package/cmake/macros.cmake +51 -0
- package/package.json +2 -1
- package/src/build/cache.js +47 -0
- package/src/build/index.js +42 -11
- package/src/build/recipe.js +30 -4
- package/src/commands/add.js +140 -0
- package/src/commands/build.js +96 -12
- package/src/commands/clean.js +39 -15
- package/src/commands/install-skill.js +107 -0
- package/src/commands/install.js +202 -5
- package/src/commands/publish.js +150 -35
- package/src/commands/show.js +237 -0
- package/src/compat.js +53 -12
- package/src/download.js +65 -3
- package/src/ignore.js +118 -0
- package/src/logger.js +26 -10
- package/src/manifest.js +12 -7
- package/src/options.js +303 -0
- package/src/profile.js +28 -4
- package/src/providers/base.js +16 -1
- package/src/providers/file.js +8 -3
- package/src/providers/http.js +12 -8
- package/src/providers/s3.js +11 -7
- package/src/resolve.js +171 -13
- package/src/toolchain/index.js +71 -0
- package/src/toolchain/presets.js +125 -16
- package/src/toolchain/template.cmake +11 -0
- package/src/upload-built.js +256 -0
- package/src/version-range.js +301 -0
package/cmake/macros.cmake
CHANGED
|
@@ -25,6 +25,44 @@ macro(WYVRN_CONFIGURE_VERSION_HEADER)
|
|
|
25
25
|
)
|
|
26
26
|
endmacro()
|
|
27
27
|
|
|
28
|
+
# Apply WYVRN_ARCH_SUFFIX to a target's OUTPUT_NAME so the produced artefact
|
|
29
|
+
# carries an unambiguous arch marker. Opt-in — call it from your CMakeLists
|
|
30
|
+
# only if you want the suffixing:
|
|
31
|
+
#
|
|
32
|
+
# WYVRN_APPLY_ARCH_SUFFIX(String) # String64.lib / String32.lib
|
|
33
|
+
# WYVRN_APPLY_ARCH_SUFFIX(String BASE_NAME my-string) # my-string64.lib / my-string32.lib
|
|
34
|
+
#
|
|
35
|
+
# BASE_NAME defaults to the target name. The target linkage name (what
|
|
36
|
+
# consumers pass to target_link_libraries) is untouched — only the on-disk
|
|
37
|
+
# output file changes. Safe to call for any target type; does nothing if
|
|
38
|
+
# WYVRN_ARCH_SUFFIX is empty (unknown / unmapped arch).
|
|
39
|
+
function(WYVRN_APPLY_ARCH_SUFFIX _wyvrn_target)
|
|
40
|
+
set(options)
|
|
41
|
+
set(oneValueArgs BASE_NAME)
|
|
42
|
+
set(multiValueArgs)
|
|
43
|
+
cmake_parse_arguments(_WYVRN_ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
|
|
44
|
+
|
|
45
|
+
if(NOT TARGET ${_wyvrn_target})
|
|
46
|
+
message(FATAL_ERROR "WYVRN_APPLY_ARCH_SUFFIX: '${_wyvrn_target}' is not a target")
|
|
47
|
+
endif()
|
|
48
|
+
|
|
49
|
+
if("${WYVRN_ARCH_SUFFIX}" STREQUAL "")
|
|
50
|
+
# Unknown arch — leave the output name alone rather than stamping a
|
|
51
|
+
# misleading value. The caller can still set OUTPUT_NAME manually.
|
|
52
|
+
return()
|
|
53
|
+
endif()
|
|
54
|
+
|
|
55
|
+
if(_WYVRN_ARG_BASE_NAME)
|
|
56
|
+
set(_wyvrn_base "${_WYVRN_ARG_BASE_NAME}")
|
|
57
|
+
else()
|
|
58
|
+
set(_wyvrn_base "${_wyvrn_target}")
|
|
59
|
+
endif()
|
|
60
|
+
|
|
61
|
+
set_target_properties(${_wyvrn_target} PROPERTIES
|
|
62
|
+
OUTPUT_NAME "${_wyvrn_base}${WYVRN_ARCH_SUFFIX}"
|
|
63
|
+
)
|
|
64
|
+
endfunction()
|
|
65
|
+
|
|
28
66
|
function(WYVRN_INSTALL_PACKAGE_EXPORT)
|
|
29
67
|
set(options)
|
|
30
68
|
set(oneValueArgs
|
|
@@ -84,10 +122,23 @@ function(WYVRN_INSTALL_PACKAGE_EXPORT)
|
|
|
84
122
|
INSTALL_DESTINATION "${WYVRN_EXPORT_INSTALL_CMAKEDIR}"
|
|
85
123
|
)
|
|
86
124
|
|
|
125
|
+
# INTERFACE libraries (header-only) have no compiled binary, so the arch
|
|
126
|
+
# stamped by `write_basic_package_version_file()` by default makes CMake
|
|
127
|
+
# reject the package when the consumer's CMAKE_SIZEOF_VOID_P differs —
|
|
128
|
+
# even though the headers themselves are arch-portable. Pass
|
|
129
|
+
# ARCH_INDEPENDENT so a HeaderOnlyLib published from x86_64 is consumable
|
|
130
|
+
# from x86 (and vice versa). Binary kinds keep the arch check.
|
|
131
|
+
set(_wyvrn_version_args)
|
|
132
|
+
get_target_property(_wyvrn_target_type ${WYVRN_EXPORT_TARGET} TYPE)
|
|
133
|
+
if(_wyvrn_target_type STREQUAL "INTERFACE_LIBRARY")
|
|
134
|
+
list(APPEND _wyvrn_version_args ARCH_INDEPENDENT)
|
|
135
|
+
endif()
|
|
136
|
+
|
|
87
137
|
write_basic_package_version_file(
|
|
88
138
|
"${WYVRN_EXPORT_VERSION_OUTPUT}"
|
|
89
139
|
VERSION "${WYVRN_EXPORT_PACKAGE_VERSION}"
|
|
90
140
|
COMPATIBILITY SameMajorVersion
|
|
141
|
+
${_wyvrn_version_args}
|
|
91
142
|
)
|
|
92
143
|
|
|
93
144
|
install(FILES
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wyvrnpm",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "A simple, static-hosting-compatible C++ package manager",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"c++",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"bin/",
|
|
21
21
|
"src/",
|
|
22
22
|
"cmake/",
|
|
23
|
+
"claude/skills/wyvrnpm.skill",
|
|
23
24
|
"README.md"
|
|
24
25
|
],
|
|
25
26
|
"scripts": {
|
package/src/build/cache.js
CHANGED
|
@@ -51,6 +51,7 @@ const LAYOUT = Object.freeze({
|
|
|
51
51
|
buildDir: 'build',
|
|
52
52
|
installSubdir: 'install', // relative to buildDir — can be overridden by recipe.installDir
|
|
53
53
|
artefactZip: 'wyvrn.zip',
|
|
54
|
+
uploadSidecar: '.uploaded-to.json',
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
/**
|
|
@@ -91,11 +92,57 @@ function hasValidArtefact(args) {
|
|
|
91
92
|
return fs.existsSync(artefact);
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Find every `.uploaded-to.json` sidecar under the build cache. Each file
|
|
97
|
+
* records the destinations that a cached artefact has been pushed to via
|
|
98
|
+
* `wyvrnpm install --build=missing --upload-built`.
|
|
99
|
+
*
|
|
100
|
+
* Used by `wyvrnpm clean --uploaded-built` and for audit tooling.
|
|
101
|
+
*
|
|
102
|
+
* @returns {Array<{ cacheDir: string, sidecarPath: string, uploads: Array<object> }>}
|
|
103
|
+
*/
|
|
104
|
+
function listUploadSidecars() {
|
|
105
|
+
const root = getBuildCacheRoot();
|
|
106
|
+
if (!fs.existsSync(root)) return [];
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
109
|
+
if (!entry.isDirectory()) continue;
|
|
110
|
+
const cacheDir = path.join(root, entry.name);
|
|
111
|
+
const sidecarPath = path.join(cacheDir, LAYOUT.uploadSidecar);
|
|
112
|
+
if (!fs.existsSync(sidecarPath)) continue;
|
|
113
|
+
let uploads = [];
|
|
114
|
+
try {
|
|
115
|
+
const raw = JSON.parse(fs.readFileSync(sidecarPath, 'utf8'));
|
|
116
|
+
if (Array.isArray(raw?.uploads)) uploads = raw.uploads;
|
|
117
|
+
} catch { /* corrupt — surface as empty uploads */ }
|
|
118
|
+
out.push({ cacheDir, sidecarPath, uploads });
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Remove every `.uploaded-to.json` sidecar under the build cache. Used by
|
|
125
|
+
* `wyvrnpm clean --uploaded-built`. Leaves all other cache content intact
|
|
126
|
+
* (the artefact zips, source clones, and build dirs stay put).
|
|
127
|
+
*
|
|
128
|
+
* @returns {number} count of sidecar files removed
|
|
129
|
+
*/
|
|
130
|
+
function clearUploadSidecars() {
|
|
131
|
+
const entries = listUploadSidecars();
|
|
132
|
+
let removed = 0;
|
|
133
|
+
for (const { sidecarPath } of entries) {
|
|
134
|
+
try { fs.unlinkSync(sidecarPath); removed++; } catch { /* ignore */ }
|
|
135
|
+
}
|
|
136
|
+
return removed;
|
|
137
|
+
}
|
|
138
|
+
|
|
94
139
|
module.exports = {
|
|
95
140
|
getBuildCacheRoot,
|
|
96
141
|
getPackageBuildDir,
|
|
97
142
|
getBuildPaths,
|
|
98
143
|
clearBuildCache,
|
|
99
144
|
hasValidArtefact,
|
|
145
|
+
listUploadSidecars,
|
|
146
|
+
clearUploadSidecars,
|
|
100
147
|
LAYOUT,
|
|
101
148
|
};
|
package/src/build/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const { normalizeDependencies } = require('../manifest');
|
|
|
13
13
|
const { resolveDependencies } = require('../resolve');
|
|
14
14
|
const { hashProfile, mergeProfile, sha256Of } = require('../profile');
|
|
15
15
|
const { generateToolchain } = require('../toolchain');
|
|
16
|
+
const { normalizeOptionsDeclaration, resolveEffectiveOptions } = require('../options');
|
|
16
17
|
const log = require('../logger');
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -62,6 +63,8 @@ function addFolder(zip, dir, base) {
|
|
|
62
63
|
* @param {import('../profile').WyvrnProfile} args.profile
|
|
63
64
|
* @param {string} args.profileName
|
|
64
65
|
* @param {string} args.profileHash
|
|
66
|
+
* @param {Record<string, any>|null} [args.options] Effective options for this build.
|
|
67
|
+
* Used to expand `${options.*}` in the recipe.
|
|
65
68
|
* @param {string} args.gitRepo
|
|
66
69
|
* @param {string} args.gitSha
|
|
67
70
|
* @param {string} args.destZipPath Where the caller expects the artefact zip.
|
|
@@ -73,13 +76,15 @@ function addFolder(zip, dir, base) {
|
|
|
73
76
|
* all four CMake configurations).
|
|
74
77
|
* @param {object} [args.authOptions]
|
|
75
78
|
* @returns {Promise<{
|
|
76
|
-
* found:
|
|
77
|
-
* contentSha256:
|
|
78
|
-
* profileHash:
|
|
79
|
-
* gitSha:
|
|
80
|
-
* gitRepo:
|
|
81
|
-
* source:
|
|
82
|
-
* resolvedFrom:
|
|
79
|
+
* found: true,
|
|
80
|
+
* contentSha256: string,
|
|
81
|
+
* profileHash: string,
|
|
82
|
+
* gitSha: string,
|
|
83
|
+
* gitRepo: string,
|
|
84
|
+
* source: string, // build-cache path (for logging)
|
|
85
|
+
* resolvedFrom: 'v2-source-build',
|
|
86
|
+
* artefactPath: string, // absolute path to the built zip in the cache
|
|
87
|
+
* clonedManifestPath: string, // absolute path to the cloned wyvrn.json (for upload-built)
|
|
83
88
|
* }>}
|
|
84
89
|
* @throws {Error} when the cloned manifest lacks a `build` section, when
|
|
85
90
|
* CMake is missing, or when the compile itself fails.
|
|
@@ -88,6 +93,7 @@ async function buildFromSource(args) {
|
|
|
88
93
|
const {
|
|
89
94
|
name, version,
|
|
90
95
|
profile, profileName, profileHash,
|
|
96
|
+
options = null,
|
|
91
97
|
gitRepo, gitSha,
|
|
92
98
|
destZipPath,
|
|
93
99
|
packageSources,
|
|
@@ -120,6 +126,8 @@ async function buildFromSource(args) {
|
|
|
120
126
|
gitRepo,
|
|
121
127
|
source: paths.root,
|
|
122
128
|
resolvedFrom: 'v2-source-build',
|
|
129
|
+
artefactPath: paths.artefact,
|
|
130
|
+
clonedManifestPath: path.join(paths.src, 'wyvrn.json'),
|
|
123
131
|
};
|
|
124
132
|
}
|
|
125
133
|
|
|
@@ -145,7 +153,10 @@ async function buildFromSource(args) {
|
|
|
145
153
|
`Source-build is opt-in — the publisher must add a build recipe.`,
|
|
146
154
|
);
|
|
147
155
|
}
|
|
148
|
-
|
|
156
|
+
// Pass effective options into the recipe so `${options.<name>}` tokens in
|
|
157
|
+
// `build.configure` / `build.buildArgs` expand to the concrete values that
|
|
158
|
+
// produced this build's profileHash.
|
|
159
|
+
const recipe = normalizeRecipe(clonedManifest.build, options);
|
|
149
160
|
|
|
150
161
|
// ── 4. Recursively resolve + download this package's OWN deps ──────────
|
|
151
162
|
// Lazy-require: downloadDependencies is where we were called from, so
|
|
@@ -161,20 +172,38 @@ async function buildFromSource(args) {
|
|
|
161
172
|
);
|
|
162
173
|
|
|
163
174
|
log.info(` resolving ${Object.keys(depsForResolution).length} transitive dep(s)`);
|
|
175
|
+
const transitiveManifests = new Map();
|
|
164
176
|
const resolvedDeps = await resolveDependencies(
|
|
165
177
|
depsForResolution, packageSources, platform, fetch, new Map(),
|
|
178
|
+
transitiveManifests,
|
|
166
179
|
);
|
|
167
180
|
|
|
168
|
-
// Build an enriched deps map
|
|
181
|
+
// Build an enriched deps map. Same logic as install.js: per-dep settings
|
|
182
|
+
// + recipe-declared options resolved against what the *cloned* source
|
|
183
|
+
// manifest asks for. No CLI overrides — transitive options come solely
|
|
184
|
+
// from the source package's `dependencies.<n>.options`.
|
|
169
185
|
const enrichedDeps = new Map();
|
|
170
186
|
for (const [depName, depVersion] of resolvedDeps) {
|
|
171
|
-
const depOverrides
|
|
187
|
+
const depOverrides = rawPackageDeps[depName]?.settings ?? {};
|
|
172
188
|
const effectiveProfile = mergeProfile(profile, depOverrides);
|
|
173
|
-
|
|
189
|
+
|
|
190
|
+
const transitiveManifest = transitiveManifests.get(depName);
|
|
191
|
+
const declaration = transitiveManifest?.options
|
|
192
|
+
? normalizeOptionsDeclaration(transitiveManifest.options, `${depName}@${depVersion}`)
|
|
193
|
+
: null;
|
|
194
|
+
const effectiveOptions = resolveEffectiveOptions(
|
|
195
|
+
declaration,
|
|
196
|
+
rawPackageDeps[depName]?.options ?? {},
|
|
197
|
+
{},
|
|
198
|
+
`${depName}@${depVersion}`,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const depProfileHash = hashProfile(effectiveProfile, effectiveOptions);
|
|
174
202
|
enrichedDeps.set(depName, {
|
|
175
203
|
version: depVersion,
|
|
176
204
|
profileHash: depProfileHash,
|
|
177
205
|
profile: effectiveProfile,
|
|
206
|
+
options: effectiveOptions,
|
|
178
207
|
});
|
|
179
208
|
}
|
|
180
209
|
|
|
@@ -238,6 +267,8 @@ async function buildFromSource(args) {
|
|
|
238
267
|
gitRepo,
|
|
239
268
|
source: paths.root,
|
|
240
269
|
resolvedFrom: 'v2-source-build',
|
|
270
|
+
artefactPath: paths.artefact,
|
|
271
|
+
clonedManifestPath: manifestPath,
|
|
241
272
|
};
|
|
242
273
|
}
|
|
243
274
|
|
package/src/build/recipe.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { substituteTemplateAll } = require('../options');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* The `build` section of a source-repo `wyvrn.json` tells wyvrnpm how to
|
|
5
7
|
* build the package from source when `--build=missing` fires. Only
|
|
@@ -50,13 +52,23 @@ const SUPPORTED_SYSTEMS = new Set(['cmake']);
|
|
|
50
52
|
* array of strings (fallback chain — tried in order; first available wins).
|
|
51
53
|
* Both forms produce a `generators: string[]` field in the normalised recipe.
|
|
52
54
|
*
|
|
55
|
+
* When `effectiveOptions` is supplied, `${options.<name>}` tokens inside
|
|
56
|
+
* `configure` and `buildArgs` are expanded using those values — bool → ON/OFF,
|
|
57
|
+
* strings verbatim. An unknown option in a template is a hard error (fail
|
|
58
|
+
* fast so the bad arg never reaches cmake). See src/options.js
|
|
59
|
+
* `substituteTemplate`. When `effectiveOptions` is null (recipe declares
|
|
60
|
+
* no options), any `${options.*}` token in the recipe also fails fast —
|
|
61
|
+
* recipes don't get to reference undeclared options.
|
|
62
|
+
*
|
|
53
63
|
* @param {object|null|undefined} rawBuild
|
|
64
|
+
* @param {Record<string, any>|null} [effectiveOptions]
|
|
54
65
|
* @returns {{ system: string, generators: string[], configure: string[],
|
|
55
66
|
* buildArgs: string[], installDir: string, sourceSubdir: string,
|
|
56
67
|
* requiredTools: string[] }}
|
|
57
|
-
* @throws {Error} when `system` is set but not supported
|
|
68
|
+
* @throws {Error} when `system` is set but not supported, or when a
|
|
69
|
+
* `${options.<name>}` template references an undeclared option
|
|
58
70
|
*/
|
|
59
|
-
function normalizeRecipe(rawBuild) {
|
|
71
|
+
function normalizeRecipe(rawBuild, effectiveOptions = null) {
|
|
60
72
|
const b = rawBuild ?? {};
|
|
61
73
|
const system = b.system ?? DEFAULT_RECIPE.system;
|
|
62
74
|
if (!SUPPORTED_SYSTEMS.has(system)) {
|
|
@@ -98,12 +110,26 @@ function normalizeRecipe(rawBuild) {
|
|
|
98
110
|
configs = configs.filter((c) => VALID_CMAKE_CONFIGS.has(c));
|
|
99
111
|
if (configs.length === 0) configs = DEFAULT_RECIPE.configs.slice();
|
|
100
112
|
|
|
113
|
+
// Expand ${options.<name>} tokens in args the recipe passes to cmake.
|
|
114
|
+
// Only `configure` and `buildArgs` are user-authored arg lists; the
|
|
115
|
+
// other fields are literals.
|
|
116
|
+
const configure = substituteTemplateAll(
|
|
117
|
+
cleanArray(b.configure),
|
|
118
|
+
effectiveOptions,
|
|
119
|
+
'build.configure',
|
|
120
|
+
);
|
|
121
|
+
const buildArgs = substituteTemplateAll(
|
|
122
|
+
cleanArray(b.buildArgs),
|
|
123
|
+
effectiveOptions,
|
|
124
|
+
'build.buildArgs',
|
|
125
|
+
);
|
|
126
|
+
|
|
101
127
|
return {
|
|
102
128
|
system,
|
|
103
129
|
generators,
|
|
104
130
|
configs,
|
|
105
|
-
configure
|
|
106
|
-
buildArgs
|
|
131
|
+
configure,
|
|
132
|
+
buildArgs,
|
|
107
133
|
installDir: b.installDir ?? DEFAULT_RECIPE.installDir,
|
|
108
134
|
sourceSubdir: b.sourceSubdir ?? DEFAULT_RECIPE.sourceSubdir,
|
|
109
135
|
requiredTools: cleanArray(b.requiredTools),
|
|
@@ -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
|
@@ -4,9 +4,14 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawn } = require('child_process');
|
|
6
6
|
|
|
7
|
-
const { readConfig }
|
|
8
|
-
const
|
|
9
|
-
const
|
|
7
|
+
const { readConfig } = require('../config');
|
|
8
|
+
const { loadProfile } = require('../profile');
|
|
9
|
+
const { readManifest } = require('../manifest');
|
|
10
|
+
const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
|
|
11
|
+
const { loadMsvcEnv } = require('../build/msvc-env');
|
|
12
|
+
const { archToVsPlatform, isVisualStudioGenerator } = require('../toolchain');
|
|
13
|
+
const install = require('./install');
|
|
14
|
+
const log = require('../logger');
|
|
10
15
|
|
|
11
16
|
// ---------------------------------------------------------------------------
|
|
12
17
|
// Pure helpers (unit-testable — no process spawning, no filesystem writes)
|
|
@@ -66,11 +71,29 @@ function resolveBinaryDir({ rootDir, presetName }) {
|
|
|
66
71
|
|
|
67
72
|
/**
|
|
68
73
|
* Args passed to `cmake` for the configure step.
|
|
69
|
-
*
|
|
74
|
+
*
|
|
75
|
+
* `generator`, when set, appends `-G <name>` after `--preset`. CMake 3.21+
|
|
76
|
+
* lets CLI args override a preset's declared fields, so this swaps the
|
|
77
|
+
* generator at configure time without touching CMakePresets.json. Note:
|
|
78
|
+
* CMake bakes the chosen generator into the cache, so switching generators
|
|
79
|
+
* for an existing build directory requires `--clean` (otherwise CMake
|
|
80
|
+
* errors with "CMAKE_GENERATOR has changed").
|
|
81
|
+
*
|
|
82
|
+
* `vsPlatform`, when set, appends `-A <platform>`. Only meaningful for
|
|
83
|
+
* Visual Studio generators — they default to x64 when unspecified, which
|
|
84
|
+
* silently produces a 64-bit build even for a 32-bit profile. The caller
|
|
85
|
+
* is responsible for deriving this from the profile arch only when the
|
|
86
|
+
* effective generator is VS (see `isVisualStudioGenerator` +
|
|
87
|
+
* `archToVsPlatform`).
|
|
88
|
+
*
|
|
89
|
+
* @param {{ presetName: string, generator?: string|null, vsPlatform?: string|null }} args
|
|
70
90
|
* @returns {string[]}
|
|
71
91
|
*/
|
|
72
|
-
function buildConfigureArgs({ presetName }) {
|
|
73
|
-
|
|
92
|
+
function buildConfigureArgs({ presetName, generator = null, vsPlatform = null }) {
|
|
93
|
+
const args = ['--preset', presetName];
|
|
94
|
+
if (generator) args.push('-G', generator);
|
|
95
|
+
if (vsPlatform) args.push('-A', vsPlatform);
|
|
96
|
+
return args;
|
|
74
97
|
}
|
|
75
98
|
|
|
76
99
|
/**
|
|
@@ -117,12 +140,18 @@ function buildInstallArgs({ binaryDir, config, prefix }) {
|
|
|
117
140
|
* Run `cmake` with the given args. Inherits stdio for real-time output.
|
|
118
141
|
* Rejects if the process exits non-zero or fails to spawn.
|
|
119
142
|
*
|
|
143
|
+
* When `env` is provided, it replaces the ambient environment (used to
|
|
144
|
+
* activate MSVC via vcvarsall for non-VS generators on Windows).
|
|
145
|
+
*
|
|
120
146
|
* @param {string[]} args
|
|
147
|
+
* @param {NodeJS.ProcessEnv|null} [env]
|
|
121
148
|
* @returns {Promise<void>}
|
|
122
149
|
*/
|
|
123
|
-
function runCmake(args) {
|
|
150
|
+
function runCmake(args, env = null) {
|
|
124
151
|
return new Promise((resolve, reject) => {
|
|
125
|
-
const
|
|
152
|
+
const opts = { stdio: 'inherit' };
|
|
153
|
+
if (env) opts.env = env;
|
|
154
|
+
const proc = spawn('cmake', args, opts);
|
|
126
155
|
proc.on('error', (err) => {
|
|
127
156
|
reject(new Error(
|
|
128
157
|
`Failed to spawn cmake: ${err.message}\n` +
|
|
@@ -192,10 +221,65 @@ async function build(argv) {
|
|
|
192
221
|
fs.rmSync(binaryDir, { recursive: true, force: true });
|
|
193
222
|
}
|
|
194
223
|
|
|
224
|
+
// ── MSVC env activation (Windows + non-VS generator) ───────────────────
|
|
225
|
+
// When the preset's generator is Ninja / Ninja Multi-Config / Make / etc.
|
|
226
|
+
// and the active profile is MSVC, we need `vcvarsall.bat` to have been
|
|
227
|
+
// sourced — otherwise cmake's compiler detection fails with "CXX compiler
|
|
228
|
+
// identification is unknown". VS generators sidestep this because MSBuild
|
|
229
|
+
// finds MSVC via its own toolset resolution.
|
|
230
|
+
//
|
|
231
|
+
// Same mechanism the source-build path already uses (src/build/msvc-env.js).
|
|
232
|
+
let cmakeEnv = null;
|
|
233
|
+
let effectiveGenerator = argv.generator ?? null;
|
|
234
|
+
let effectiveProfile = null;
|
|
235
|
+
try {
|
|
236
|
+
const { profile: activeProfile } = loadProfile(profileName);
|
|
237
|
+
effectiveProfile = activeProfile;
|
|
238
|
+
// Which generator will the preset actually use? Read it from the
|
|
239
|
+
// project's own recipe if one exists. If not, default to null and let
|
|
240
|
+
// cmake/CMakePresets decide — in which case MSVC env activation is
|
|
241
|
+
// skipped (because VS is usually the default on Windows).
|
|
242
|
+
const manifest = readManifest(manifestPath);
|
|
243
|
+
let presetGenerator = null;
|
|
244
|
+
if (hasBuildRecipe(manifest)) {
|
|
245
|
+
try {
|
|
246
|
+
const recipe = normalizeRecipe(manifest.build, null);
|
|
247
|
+
if (recipe.generators.length > 0) [presetGenerator] = recipe.generators;
|
|
248
|
+
} catch { /* templates need options → skip env activation */ }
|
|
249
|
+
}
|
|
250
|
+
// CLI --generator wins — that is the generator CMake will actually use,
|
|
251
|
+
// so it is the one that determines whether vcvarsall must be activated.
|
|
252
|
+
if (!effectiveGenerator) effectiveGenerator = presetGenerator;
|
|
253
|
+
cmakeEnv = await loadMsvcEnv({
|
|
254
|
+
profile: activeProfile,
|
|
255
|
+
skipForGenerator: effectiveGenerator,
|
|
256
|
+
});
|
|
257
|
+
} catch (err) {
|
|
258
|
+
log.warn(`Could not resolve MSVC env activation: ${err.message}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
195
261
|
// ── Configure ─────────────────────────────────────────────────────────────
|
|
196
|
-
|
|
262
|
+
// Auto-emit `-A <platform>` for Visual Studio generators, mapped from the
|
|
263
|
+
// active profile's arch. VS otherwise defaults to x64 regardless of the
|
|
264
|
+
// profile, which breaks x86 builds: CMake produces 64-bit artefacts but
|
|
265
|
+
// find_package() rejects the 32-bit installed dependencies with "version
|
|
266
|
+
// not compatible" (actually a CMAKE_SIZEOF_VOID_P mismatch, not a
|
|
267
|
+
// version-number check).
|
|
268
|
+
let vsPlatform = null;
|
|
269
|
+
if (isVisualStudioGenerator(effectiveGenerator) && effectiveProfile) {
|
|
270
|
+
vsPlatform = archToVsPlatform(effectiveProfile.arch);
|
|
271
|
+
if (vsPlatform) {
|
|
272
|
+
log.info(`Visual Studio generator + arch=${effectiveProfile.arch} → passing -A ${vsPlatform}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const configureArgs = buildConfigureArgs({
|
|
277
|
+
presetName,
|
|
278
|
+
generator: argv.generator,
|
|
279
|
+
vsPlatform,
|
|
280
|
+
});
|
|
197
281
|
log.info(`cmake ${configureArgs.join(' ')}`);
|
|
198
|
-
await runCmake(configureArgs);
|
|
282
|
+
await runCmake(configureArgs, cmakeEnv);
|
|
199
283
|
|
|
200
284
|
// ── Build ─────────────────────────────────────────────────────────────────
|
|
201
285
|
const buildArgs = buildBuildArgs({
|
|
@@ -206,7 +290,7 @@ async function build(argv) {
|
|
|
206
290
|
passthru: argv['--'] ?? [],
|
|
207
291
|
});
|
|
208
292
|
log.info(`cmake ${buildArgs.join(' ')}`);
|
|
209
|
-
await runCmake(buildArgs);
|
|
293
|
+
await runCmake(buildArgs, cmakeEnv);
|
|
210
294
|
|
|
211
295
|
// ── Install (optional) ────────────────────────────────────────────────────
|
|
212
296
|
// `--install` runs `cmake --install <binaryDir>` after the build. Prefix
|
|
@@ -219,7 +303,7 @@ async function build(argv) {
|
|
|
219
303
|
prefix: argv.installPrefix,
|
|
220
304
|
});
|
|
221
305
|
log.info(`cmake ${installArgs.join(' ')}`);
|
|
222
|
-
await runCmake(installArgs);
|
|
306
|
+
await runCmake(installArgs, cmakeEnv);
|
|
223
307
|
}
|
|
224
308
|
|
|
225
309
|
const suffix = argv.install
|