wyvrnpm 2.3.2 → 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 CHANGED
@@ -4,7 +4,7 @@ A simple, private C++ package manager that works with any static file hosting pr
4
4
 
5
5
  There is no central registry. You control where packages are hosted.
6
6
 
7
- > **README version: 2.1.0** — Adds CMake toolchain generation, the `wyvrnpm build` command, declarative compatibility, and `--build=missing` source-build.
7
+ > **README version: 2.4.0** — Highlights since 2.1.0: `wyvrnpm add <name>` for dependency authoring, `HeaderOnlyLib` packages that survive cross-arch `find_package()`, `WYVRN_ARCH_SUFFIX` + the `WYVRN_APPLY_ARCH_SUFFIX(<target>)` helper so x64/x86 artefacts coexist on disk, `wyvrnpm build --generator / -G` to swap CMake generators from the CLI, package `options`, version ranges (`^`, `~`, explicit comparators), `--format=json` output, and `--upload-built` to warm the registry with source-built artefacts.
8
8
 
9
9
  ---
10
10
 
@@ -37,16 +37,19 @@ wyvrnpm configure add-source --kind publish --name default --url s3://my-bucket
37
37
  # 3. Detect and save your build environment as the default profile
38
38
  wyvrnpm configure profile detect
39
39
 
40
- # 4. Install dependencies (resolved against your build profile)
40
+ # 4. Add dependencies (resolves latest from your configured sources)
41
+ wyvrnpm add zlib fmt@^10.0.0
42
+
43
+ # 5. Install dependencies (resolved against your build profile)
41
44
  wyvrnpm install
42
45
 
43
- # 5. Configure and build via the generated CMake preset
46
+ # 6. Configure and build via the generated CMake preset
44
47
  wyvrnpm build
45
48
 
46
- # 6. Publish your package (uses the active profile automatically)
49
+ # 7. Publish your package (uses the active profile automatically)
47
50
  wyvrnpm publish
48
51
 
49
- # 7. (Optional) Link local packages for development
52
+ # 8. (Optional) Link local packages for development
50
53
  cd ../my-local-lib && wyvrnpm link
51
54
  cd ../my-app && wyvrnpm link my-local-lib
52
55
  ```
@@ -109,6 +112,42 @@ wyvrnpm init --root ./my-project
109
112
 
110
113
  ---
111
114
 
115
+ ### `wyvrnpm add`
116
+
117
+ Adds one or more dependencies to `wyvrn.json` without downloading them. Useful when you know the package name but not the latest version.
118
+
119
+ - **Bare name** — resolves `latest.json` from your configured install sources and writes `"^<latest>"` (caret range, major-pinned).
120
+ - **`name@<version>`** — writes the given string verbatim. Accepts exact pins (`1.2.3.4`), caret/tilde ranges (`^1.2.3.4`, `~1.2.3.4`), and explicit comparators (`>=1.80.0 <2.0.0`).
121
+
122
+ If the dependency is already listed, it is replaced. When the prior entry was an object (`{ version, settings, options }`), the `settings` and `options` are preserved — only the version field is updated.
123
+
124
+ `add` does **not** run install. Chain `wyvrnpm install` afterwards to download.
125
+
126
+ ```bash
127
+ # Resolve latest, write "^<latest>"
128
+ wyvrnpm add zlib
129
+
130
+ # Pin exactly
131
+ wyvrnpm add zlib@1.3.0.0
132
+
133
+ # Use a caret / tilde / comparator range
134
+ wyvrnpm add fmt@^10.0.0
135
+ wyvrnpm add boost@">=1.80.0 <2.0.0"
136
+
137
+ # Multiple in one call
138
+ wyvrnpm add zlib fmt@10.2.0.0 spdlog
139
+ ```
140
+
141
+ **Options**
142
+
143
+ | Option | Default | Description |
144
+ |---|---|---|
145
+ | `--source` / `-s` | *(config)* | Base URL used for the `latest` lookup. Repeat for multiple. |
146
+ | `--platform` | `win_x64` | v1 legacy platform sub-path (used as a fallback during `latest` lookup). |
147
+ | `--manifest` | `./wyvrn.json` | Path to the manifest file. |
148
+
149
+ ---
150
+
112
151
  ### `wyvrnpm install`
113
152
 
114
153
  Resolves the full dependency graph and downloads all packages.
@@ -164,6 +203,25 @@ After download, `install` also generates:
164
203
  - `wyvrn_internal/wyvrn_profile.json` — snapshot of the active build profile.
165
204
  - `CMakePresets.json` (or `CMakeUserPresets.json` if the project root file is user-owned) with a `wyvrn-<profile>` configure preset. Presets for other profiles are preserved so multiple profiles coexist.
166
205
 
206
+ **Toolchain variables exposed to your `CMakeLists.txt` (valid BEFORE `project()`)**:
207
+
208
+ | Variable | Value |
209
+ |---|---|
210
+ | `WYVRN_PROFILE_NAME` / `WYVRN_PROFILE_HASH` | Active profile name + 16-char hash |
211
+ | `WYVRN_PROFILE_OS` / `WYVRN_PROFILE_ARCH` | OS + architecture from the profile |
212
+ | `WYVRN_PROFILE_COMPILER` / `WYVRN_PROFILE_COMPILER_VERSION` | Compiler identity |
213
+ | `WYVRN_PROFILE_CPPSTD` / `WYVRN_PROFILE_RUNTIME` | C++ standard + MSVC runtime linkage |
214
+ | `WYVRN_ARCH_SUFFIX` | `"64"` for 64-bit arches, `"32"` for 32-bit, empty for unmapped. Derived at toolchain-generation time so it's usable before `project()` — unlike `CMAKE_SIZEOF_VOID_P`. |
215
+
216
+ The optional helper `WYVRN_APPLY_ARCH_SUFFIX(<target> [BASE_NAME <name>])` from the bundled `macros.cmake` applies the suffix to a target's `OUTPUT_NAME` so x64 and x86 builds of the same library coexist on disk without overwriting each other. Target linkage (`target_link_libraries`) is unaffected — only the produced filename changes.
217
+
218
+ ```cmake
219
+ project(String CXX)
220
+ add_library(String STATIC ...)
221
+ WYVRN_APPLY_ARCH_SUFFIX(String) # → String64.lib / String32.lib
222
+ WYVRN_APPLY_ARCH_SUFFIX(String BASE_NAME my-str) # → my-str64.lib / my-str32.lib
223
+ ```
224
+
167
225
  ---
168
226
 
169
227
  ### `wyvrnpm build`
@@ -189,6 +247,10 @@ wyvrnpm build --target my-lib
189
247
  # Wipe the build directory first
190
248
  wyvrnpm build --clean
191
249
 
250
+ # Override the preset's generator (pair with --clean when switching generators)
251
+ wyvrnpm build --clean --generator "Visual Studio 17 2022"
252
+ wyvrnpm build --clean -G "Ninja Multi-Config"
253
+
192
254
  # Build, then install to the CMake install prefix
193
255
  wyvrnpm build --install
194
256
  wyvrnpm build --install --install-prefix C:\out\stage
@@ -210,6 +272,7 @@ wyvrnpm build -- -j 8
210
272
  | `--target` / `-t` | *(all)* | Specific CMake target to build. |
211
273
  | `--verbose` / `-v` | `false` | Pass `--verbose` to `cmake --build`. |
212
274
  | `--clean` | `false` | Remove the build directory before configuring. |
275
+ | `--generator` / `-G` | *(from preset)* | Override the CMake generator used at configure time (e.g. `"Visual Studio 17 2022"`, `"Ninja"`, `"Ninja Multi-Config"`). CMake bakes the generator into the cache, so pair with `--clean` when switching. When the effective generator is Visual Studio, `-A <platform>` is auto-derived from the profile's arch (`x86→Win32`, `x86_64→x64`, `armv8→ARM64`, `armv7→ARM`) so VS doesn't silently default to x64 on 32-bit profiles. |
213
276
  | `--install` | `false` | Run `cmake --install` after a successful build. |
214
277
  | `--install-prefix` | *(baked in)* | Override `CMAKE_INSTALL_PREFIX` for `--install` without reconfiguring. |
215
278
  | `--auto-install` | `true` | Auto-run `wyvrnpm install` when the toolchain is stale. Disable with `--no-auto-install`. |
package/bin/wyvrn.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  const yargs = require('yargs');
6
6
  const init = require('../src/commands/init');
7
+ const add = require('../src/commands/add');
7
8
  const install = require('../src/commands/install');
8
9
  const clean = require('../src/commands/clean');
9
10
  const publish = require('../src/commands/publish');
@@ -35,6 +36,34 @@ yargs
35
36
  (argv) => init(argv),
36
37
  )
37
38
 
39
+ // ── add ───────────────────────────────────────────────────────────────────
40
+ .command(
41
+ 'add <packages...>',
42
+ 'Add dependencies to wyvrn.json. Without @version, resolves latest and writes "^<latest>"; ' +
43
+ 'with @version, writes the given string verbatim. Does not run install.',
44
+ (y) => {
45
+ y
46
+ .positional('packages', {
47
+ type: 'string',
48
+ array: true,
49
+ describe: 'One or more <name>[@<version>] specs',
50
+ })
51
+ .option('source', {
52
+ alias: 's',
53
+ type: 'string',
54
+ description: 'Base URL of a package source for `latest` lookup (can be repeated)',
55
+ array: true,
56
+ })
57
+ .option('platform', {
58
+ type: 'string',
59
+ description: 'v1 legacy platform path (used as fallback when looking up latest)',
60
+ choices: ['win_x64', 'win_x86', 'linux_x64', 'linux_x86', 'osx_x64', 'osx_arm64'],
61
+ default: 'win_x64',
62
+ });
63
+ },
64
+ (argv) => add(argv),
65
+ )
66
+
38
67
  // ── install ───────────────────────────────────────────────────────────────
39
68
  .command(
40
69
  'install',
@@ -156,6 +185,13 @@ yargs
156
185
  default: false,
157
186
  description: 'Remove the build directory before configuring',
158
187
  })
188
+ .option('generator', {
189
+ alias: 'G',
190
+ type: 'string',
191
+ description:
192
+ 'Override the preset\'s CMake generator (e.g. "Visual Studio 17 2022", "Ninja", "Ninja Multi-Config"). ' +
193
+ 'CMake locks the generator into the build-dir cache, so pair this with --clean when switching generators.',
194
+ })
159
195
  .option('install', {
160
196
  type: 'boolean',
161
197
  default: false,
Binary file
@@ -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.3.2",
3
+ "version": "2.4.1",
4
4
  "description": "A simple, static-hosting-compatible C++ package manager",
5
5
  "keywords": [
6
6
  "c++",
@@ -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;
@@ -9,6 +9,7 @@ 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');
12
13
  const install = require('./install');
13
14
  const log = require('../logger');
14
15
 
@@ -70,11 +71,29 @@ function resolveBinaryDir({ rootDir, presetName }) {
70
71
 
71
72
  /**
72
73
  * Args passed to `cmake` for the configure step.
73
- * @param {{ presetName: string }} args
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
74
90
  * @returns {string[]}
75
91
  */
76
- function buildConfigureArgs({ presetName }) {
77
- return ['--preset', presetName];
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;
78
97
  }
79
98
 
80
99
  /**
@@ -211,8 +230,11 @@ async function build(argv) {
211
230
  //
212
231
  // Same mechanism the source-build path already uses (src/build/msvc-env.js).
213
232
  let cmakeEnv = null;
233
+ let effectiveGenerator = argv.generator ?? null;
234
+ let effectiveProfile = null;
214
235
  try {
215
236
  const { profile: activeProfile } = loadProfile(profileName);
237
+ effectiveProfile = activeProfile;
216
238
  // Which generator will the preset actually use? Read it from the
217
239
  // project's own recipe if one exists. If not, default to null and let
218
240
  // cmake/CMakePresets decide — in which case MSVC env activation is
@@ -225,16 +247,37 @@ async function build(argv) {
225
247
  if (recipe.generators.length > 0) [presetGenerator] = recipe.generators;
226
248
  } catch { /* templates need options → skip env activation */ }
227
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;
228
253
  cmakeEnv = await loadMsvcEnv({
229
254
  profile: activeProfile,
230
- skipForGenerator: presetGenerator,
255
+ skipForGenerator: effectiveGenerator,
231
256
  });
232
257
  } catch (err) {
233
258
  log.warn(`Could not resolve MSVC env activation: ${err.message}`);
234
259
  }
235
260
 
236
261
  // ── Configure ─────────────────────────────────────────────────────────────
237
- const configureArgs = buildConfigureArgs({ presetName });
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
+ });
238
281
  log.info(`cmake ${configureArgs.join(' ')}`);
239
282
  await runCmake(configureArgs, cmakeEnv);
240
283
 
@@ -333,6 +333,7 @@ async function install(argv) {
333
333
  generator: presetGenerator,
334
334
  extraCacheVariables: presetCacheVariables,
335
335
  installDir: presetInstallDir,
336
+ profileArch: baseProfile.arch ?? null,
336
337
  });
337
338
  if (presetResult.action === 'skipped') {
338
339
  log.warn(
package/src/compat.js CHANGED
@@ -34,12 +34,15 @@
34
34
  * | version fields and leaves room for future "same major"
35
35
  * | semantics without renaming.
36
36
  *
37
- * Missing fields default to `exact`. `os` and `arch` are ALWAYS evaluated
38
- * as `exact` regardless of what the block says — cross-arch/cross-OS
39
- * binaries are never ABI-compatible.
37
+ * Missing fields default to `exact`. `os` and `arch` are normally forced
38
+ * to `exact` regardless of what the block says — cross-arch/cross-OS
39
+ * binaries are never ABI-compatible. The one exception is
40
+ * `kind: "HeaderOnlyLib"`: header-only packages ship no binary, so the
41
+ * publisher's declared modes for `os`/`arch` are respected. Binary kinds
42
+ * (`StaticLib`, `DynamicLib`, `ConsoleApp`, …) keep the hard guarantee.
40
43
  */
41
44
 
42
- const OS_ARCH_ALWAYS_EXACT = new Set(['os', 'arch']);
45
+ const OS_ARCH_FIELDS = new Set(['os', 'arch']);
43
46
 
44
47
  const DEFAULT_COMPAT_FIELDS = [
45
48
  'os',
@@ -55,19 +58,27 @@ const VALID_MODES = new Set(['exact', 'any', 'lte', 'gte', 'exact-or-newer']);
55
58
  /**
56
59
  * Normalise a raw compatibility block into `{ field → mode }`.
57
60
  * Missing fields fall back to `exact`. Unknown modes fall back to `exact`.
58
- * `os` and `arch` are forced to `exact`.
61
+ *
62
+ * For binary kinds (`StaticLib`, `DynamicLib`, `ConsoleApp`, …) `os` and
63
+ * `arch` are forced to `exact` — publisher-declared modes are silently
64
+ * dropped because cross-arch binaries are never ABI-safe. For
65
+ * `kind: "HeaderOnlyLib"` the publisher's declared modes are respected,
66
+ * since there is no binary to be ABI-incompatible with.
59
67
  *
60
68
  * @param {object|null|undefined} rawCompat
69
+ * @param {string|null|undefined} [packageKind] Package manifest `kind` field.
61
70
  * @returns {Record<string, string>}
62
71
  */
63
- function normalizeCompat(rawCompat) {
72
+ function normalizeCompat(rawCompat, packageKind) {
64
73
  const result = {};
65
74
  for (const field of DEFAULT_COMPAT_FIELDS) result[field] = 'exact';
66
75
 
67
76
  if (!rawCompat || typeof rawCompat !== 'object') return result;
68
77
 
78
+ const headerOnly = packageKind === 'HeaderOnlyLib';
79
+
69
80
  for (const [field, spec] of Object.entries(rawCompat)) {
70
- if (OS_ARCH_ALWAYS_EXACT.has(field)) continue;
81
+ if (!headerOnly && OS_ARCH_FIELDS.has(field)) continue;
71
82
  const mode = typeof spec === 'string' ? spec : (spec && spec.mode);
72
83
  if (mode && VALID_MODES.has(mode)) result[field] = mode;
73
84
  }
@@ -147,13 +158,14 @@ function evaluateField(mode, consumerVal, packageVal) {
147
158
  * @param {Record<string, any>} consumerProfile may include `options: {...}`
148
159
  * @param {Record<string, any>} packageProfile may include `options: {...}`
149
160
  * @param {object|null|undefined} rawCompat compatibility block from the package manifest
161
+ * @param {string|null|undefined} [packageKind] package manifest `kind` field — unlocks `os`/`arch` modes for HeaderOnlyLib
150
162
  * @returns {{
151
163
  * compatible: boolean,
152
164
  * reasons: Array<{ field: string, mode: string, consumer: any, package: any, outcome: string }>,
153
165
  * }}
154
166
  */
155
- function evaluateCompat(consumerProfile, packageProfile, rawCompat) {
156
- const compat = normalizeCompat(rawCompat);
167
+ function evaluateCompat(consumerProfile, packageProfile, rawCompat, packageKind) {
168
+ const compat = normalizeCompat(rawCompat, packageKind);
157
169
  const reasons = [];
158
170
  let compatible = true;
159
171
 
package/src/download.js CHANGED
@@ -209,7 +209,7 @@ async function findCompatibleBuild({ name, version, consumerProfile, consumerOpt
209
209
  : packageProfile);
210
210
 
211
211
  const rawCompat = meta.compatibility ?? null;
212
- const { compatible, reasons } = evaluateCompat(consumerFull, packageFull, rawCompat);
212
+ const { compatible, reasons } = evaluateCompat(consumerFull, packageFull, rawCompat, meta.kind);
213
213
  if (compatible) {
214
214
  candidates.push({ profileHash, packageProfile, reasons, buildEntry, meta });
215
215
  }
@@ -17,6 +17,73 @@ function getBundledCmakeDir() {
17
17
  return dir.replace(/\\/g, '/');
18
18
  }
19
19
 
20
+ // Arch bit-width mapping. Kept exhaustive against `ARCH_MAP` in profile.js
21
+ // so any arch a profile can hold has a deterministic "32"/"64" suffix.
22
+ // Unknown arch strings fall back to an empty suffix — the consumer's
23
+ // CMakeLists should then use another discriminator (e.g. profile hash).
24
+ const ARCH_BIT_SUFFIX = {
25
+ x86_64: '64',
26
+ x86: '32',
27
+ armv8: '64',
28
+ armv7: '32',
29
+ ppc64le: '64',
30
+ s390x: '64',
31
+ mips: '32',
32
+ mipsel: '32',
33
+ };
34
+
35
+ /**
36
+ * Map a profile `arch` value to the canonical "64" / "32" suffix that
37
+ * `WYVRN_ARCH_SUFFIX` exposes to consumer CMakeLists. Empty string for
38
+ * unknown arches — callers can pattern-match on that to fall back.
39
+ *
40
+ * @param {string|null|undefined} arch
41
+ * @returns {string}
42
+ */
43
+ function archToBitSuffix(arch) {
44
+ if (!arch) return '';
45
+ return ARCH_BIT_SUFFIX[arch] ?? '';
46
+ }
47
+
48
+ // Visual Studio generator `-A <platform>` mapping. VS defaults to x64
49
+ // when `-A` is unspecified — which silently wins against a 32-bit profile
50
+ // and causes consumer-side `find_package()` to reject 32-bit artefacts
51
+ // with "The version found is not compatible" (a CMake pointer-size check,
52
+ // not a version-number check). Auto-emitting `-A` keeps VS + x86 honest.
53
+ const ARCH_VS_PLATFORM = {
54
+ x86_64: 'x64',
55
+ x86: 'Win32',
56
+ armv8: 'ARM64',
57
+ armv7: 'ARM',
58
+ };
59
+
60
+ /**
61
+ * Map a profile arch to the Visual Studio generator's `-A <platform>` value.
62
+ * Returns null for arches that don't map to a known VS platform — callers
63
+ * should skip `-A` in that case and let the user pass one explicitly.
64
+ *
65
+ * @param {string|null|undefined} arch
66
+ * @returns {string|null}
67
+ */
68
+ function archToVsPlatform(arch) {
69
+ if (!arch) return null;
70
+ return ARCH_VS_PLATFORM[arch] ?? null;
71
+ }
72
+
73
+ /**
74
+ * True when the generator name identifies a Visual Studio generator (e.g.
75
+ * "Visual Studio 17 2022"). VS generators require `-A <platform>` /
76
+ * `architecture` in the preset; everyone else (Ninja, Make, Xcode, …)
77
+ * ignores it.
78
+ *
79
+ * @param {string|null|undefined} name
80
+ * @returns {boolean}
81
+ */
82
+ function isVisualStudioGenerator(name) {
83
+ if (!name) return false;
84
+ return /^Visual Studio \d/.test(name);
85
+ }
86
+
20
87
  /**
21
88
  * Substitute `{{KEY}}` placeholders in a template string.
22
89
  *
@@ -54,6 +121,7 @@ function buildPlaceholders({ profile, profileName, profileHash, packageNames })
54
121
  PROFILE_HASH: profileHash,
55
122
  PROFILE_OS: profile.os ?? '',
56
123
  PROFILE_ARCH: profile.arch ?? '',
124
+ PROFILE_ARCH_SUFFIX: archToBitSuffix(profile.arch),
57
125
  PROFILE_COMPILER: profile.compiler ?? '',
58
126
  PROFILE_COMPILER_VERSION: profile['compiler.version'] ?? '',
59
127
  PROFILE_CPPSTD: profile['compiler.cppstd'] ?? '20',
@@ -135,7 +203,10 @@ module.exports = {
135
203
  generateToolchain,
136
204
  generateCMakePresets,
137
205
  getBundledCmakeDir,
206
+ archToVsPlatform,
207
+ isVisualStudioGenerator,
138
208
  // Exported for tests
209
+ archToBitSuffix,
139
210
  buildPlaceholders,
140
211
  renderTemplate,
141
212
  };
@@ -4,6 +4,21 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
6
  const VENDOR_KEY = 'wyvrnpm/generated';
7
+
8
+ // Local copy of the arch→VS-platform mapping. Duplicated here rather than
9
+ // imported from ./index so preset generation can run standalone (keeps
10
+ // the dependency graph simple — presets.js doesn't otherwise need the
11
+ // toolchain generator). Keep in sync with `archToVsPlatform` in index.js.
12
+ const ARCH_VS_PLATFORM = {
13
+ x86_64: 'x64',
14
+ x86: 'Win32',
15
+ armv8: 'ARM64',
16
+ armv7: 'ARM',
17
+ };
18
+
19
+ function isVsGenerator(name) {
20
+ return typeof name === 'string' && /^Visual Studio \d/.test(name);
21
+ }
7
22
  const SCHEMA_VERSION = 3; // requires CMake ≥ 3.21
8
23
 
9
24
  /**
@@ -52,6 +67,11 @@ function isWyvrnGenerated(presets) {
52
67
  * `CMAKE_TOOLCHAIN_FILE`. Usually
53
68
  * derived from the project's own
54
69
  * `build.configure` -D args.
70
+ * @param {string|null} [args.profileArch] Active profile arch — used to pin
71
+ * `architecture` for VS generators
72
+ * so `cmake --preset` produces
73
+ * binaries matching the profile
74
+ * (VS otherwise defaults to x64).
55
75
  */
56
76
  function buildConfigurePreset({
57
77
  profileName,
@@ -60,6 +80,7 @@ function buildConfigurePreset({
60
80
  generator = null,
61
81
  extraCacheVariables = null,
62
82
  installDir = null,
83
+ profileArch = null,
63
84
  }) {
64
85
  const binaryDir = `\${sourceDir}/build/wyvrn-${profileName}`;
65
86
  // Spread `extraCacheVariables` FIRST, then the wyvrn-owned entries. This
@@ -90,6 +111,19 @@ function buildConfigurePreset({
90
111
  cacheVariables,
91
112
  };
92
113
  if (generator) preset.generator = generator;
114
+
115
+ // Visual Studio generators default to x64 when `architecture` is
116
+ // unspecified — which silently overrides the profile arch and makes
117
+ // find_package() reject 32-bit installed packages with a pointer-size
118
+ // mismatch. Pin `architecture` from the profile so `cmake --preset`
119
+ // produces the right artefacts end-to-end.
120
+ if (isVsGenerator(generator) && profileArch && ARCH_VS_PLATFORM[profileArch]) {
121
+ preset.architecture = {
122
+ value: ARCH_VS_PLATFORM[profileArch],
123
+ strategy: 'set',
124
+ };
125
+ }
126
+
93
127
  return preset;
94
128
  }
95
129
 
@@ -127,6 +161,7 @@ function buildFreshPresetsFile({
127
161
  generator = null,
128
162
  extraCacheVariables = null,
129
163
  installDir = null,
164
+ profileArch = null,
130
165
  }) {
131
166
  return {
132
167
  version: SCHEMA_VERSION,
@@ -138,7 +173,7 @@ function buildFreshPresetsFile({
138
173
  },
139
174
  },
140
175
  configurePresets: [buildConfigurePreset({
141
- profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir,
176
+ profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch,
142
177
  })],
143
178
  buildPresets: buildBuildPresets(profileName),
144
179
  };
@@ -161,9 +196,10 @@ function mergeIntoExisting(existing, {
161
196
  generator = null,
162
197
  extraCacheVariables = null,
163
198
  installDir = null,
199
+ profileArch = null,
164
200
  }) {
165
201
  const newConfigure = buildConfigurePreset({
166
- profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir,
202
+ profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch,
167
203
  });
168
204
  const newBuilds = buildBuildPresets(profileName);
169
205
  const newConfigureNames = new Set([newConfigure.name]);
@@ -223,8 +259,9 @@ function generateCMakePresets({
223
259
  generator = null,
224
260
  extraCacheVariables = null,
225
261
  installDir = null,
262
+ profileArch = null,
226
263
  }) {
227
- const args = { profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir };
264
+ const args = { profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch };
228
265
  const candidates = [
229
266
  { filePath: path.join(projectRoot, 'CMakePresets.json'), isUser: false },
230
267
  { filePath: path.join(projectRoot, 'CMakeUserPresets.json'), isUser: true },
@@ -17,6 +17,17 @@ set(WYVRN_PROFILE_NAME "{{PROFILE_NAME}}")
17
17
  set(WYVRN_PROFILE_HASH "{{PROFILE_HASH}}")
18
18
  set(WYVRN_PROFILE_OS "{{PROFILE_OS}}")
19
19
  set(WYVRN_PROFILE_ARCH "{{PROFILE_ARCH}}")
20
+
21
+ # "64" for 64-bit arches (x86_64, armv8, …), "32" for 32-bit (x86, armv7, …),
22
+ # empty for arches outside the known mapping. Derived at generation time from
23
+ # the profile so it is valid BEFORE `project()` — unlike CMAKE_SIZEOF_VOID_P,
24
+ # which is only set after the first project() call. Intended for suffixing
25
+ # artefact names (e.g. "String${WYVRN_ARCH_SUFFIX}" → String64 / String32) so
26
+ # x64 and x86 builds can coexist on disk without overwriting each other.
27
+ # Call WYVRN_APPLY_ARCH_SUFFIX(<target>) from macros.cmake for the common
28
+ # per-target shape, or reference the variable directly for other uses.
29
+ set(WYVRN_ARCH_SUFFIX "{{PROFILE_ARCH_SUFFIX}}")
30
+
20
31
  set(WYVRN_PROFILE_COMPILER "{{PROFILE_COMPILER}}")
21
32
  set(WYVRN_PROFILE_COMPILER_VERSION "{{PROFILE_COMPILER_VERSION}}")
22
33
  set(WYVRN_PROFILE_CPPSTD "{{PROFILE_CPPSTD}}")