wyvrnpm 2.16.1 → 2.17.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 +9 -13
- package/bin/wyvrnpm.js +106 -1
- package/cmake/variables.cmake +12 -13
- package/package.json +1 -1
- package/src/commands/clean.js +13 -1
- package/src/commands/test.js +302 -0
- package/src/index.js +2 -0
- package/src/toolchain/presets.js +59 -4
package/README.md
CHANGED
|
@@ -39,12 +39,6 @@ npm install -g wyvrnpm
|
|
|
39
39
|
|
|
40
40
|
Requires Node.js >= 18.
|
|
41
41
|
|
|
42
|
-
For S3 publishing, also install the AWS SDK:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
npm install -g @aws-sdk/client-s3 @aws-sdk/credential-providers
|
|
46
|
-
```
|
|
47
|
-
|
|
48
42
|
### Update notifier
|
|
49
43
|
|
|
50
44
|
When a newer `wyvrnpm` is published to npm, the next interactive run prints a single-line banner on stderr:
|
|
@@ -391,15 +385,17 @@ After download, `install` also generates:
|
|
|
391
385
|
|
|
392
386
|
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.
|
|
393
387
|
|
|
394
|
-
The bundled `variables.cmake`
|
|
388
|
+
The bundled `variables.cmake` puts compiled artefacts inside the CMake binary dir so `wyvrnpm clean --all` (which sweeps `<binaryDirRoot>/wyvrn-*/`) catches them:
|
|
395
389
|
|
|
396
|
-
|
|
|
397
|
-
|
|
398
|
-
| `
|
|
399
|
-
| `
|
|
400
|
-
|
|
|
390
|
+
| Variable | Value |
|
|
391
|
+
|---|---|
|
|
392
|
+
| `CMAKE_ARCHIVE_OUTPUT_DIRECTORY` | `${CMAKE_BINARY_DIR}/lib/` |
|
|
393
|
+
| `CMAKE_LIBRARY_OUTPUT_DIRECTORY` | `${CMAKE_BINARY_DIR}/lib/` |
|
|
394
|
+
| `CMAKE_RUNTIME_OUTPUT_DIRECTORY` | `${CMAKE_BINARY_DIR}/bin` |
|
|
395
|
+
|
|
396
|
+
With per-profile build dirs (`build/wyvrn-x64-msvc/` vs `build/wyvrn-x86-msvc/`) already isolating multi-arch builds, the output directories themselves don't carry an arch suffix — but `WYVRN_APPLY_ARCH_SUFFIX` still stamps the suffix onto artefact **names** (`String64.lib` vs `String32.lib`) for the cross-arch ship-together case.
|
|
401
397
|
|
|
402
|
-
Prior to `2.
|
|
398
|
+
Prior to `2.17.0` outputs landed in `${CMAKE_SOURCE_DIR}/output/{bin,lib}${WYVRN_ARCH_SUFFIX}/` — outside the build dir, so `wyvrnpm clean --all` left binaries behind. Scripts that hard-code `output/bin64/foo.exe` must switch to `<binaryDir>/bin/<CONFIG>/foo.exe` (multi-config generators) or `<binaryDir>/bin/foo.exe` (single-config). Run `wyvrnpm clean --all` after upgrade — it sweeps the legacy `output/` tree as a migration aid.
|
|
403
399
|
|
|
404
400
|
```cmake
|
|
405
401
|
project(String CXX)
|
package/bin/wyvrnpm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
@@ -16,6 +16,7 @@ const publish = require('../src/commands/publish');
|
|
|
16
16
|
const configure = require('../src/commands/configure');
|
|
17
17
|
const profile = require('../src/commands/profile');
|
|
18
18
|
const build = require('../src/commands/build');
|
|
19
|
+
const test = require('../src/commands/test');
|
|
19
20
|
const show = require('../src/commands/show');
|
|
20
21
|
const installSkill = require('../src/commands/install-skill');
|
|
21
22
|
const cache = require('../src/commands/cache');
|
|
@@ -435,6 +436,110 @@ yargs
|
|
|
435
436
|
}),
|
|
436
437
|
)
|
|
437
438
|
|
|
439
|
+
// ── test ──────────────────────────────────────────────────────────────────
|
|
440
|
+
.command(
|
|
441
|
+
'test',
|
|
442
|
+
'Run ctest against the wyvrnpm-generated test preset. ' +
|
|
443
|
+
'Auto-runs `wyvrnpm build` if the toolchain is missing or stale (disable with --no-build).',
|
|
444
|
+
(y) => {
|
|
445
|
+
y
|
|
446
|
+
.option('preset', {
|
|
447
|
+
alias: 'P',
|
|
448
|
+
type: 'string',
|
|
449
|
+
description: 'CMake test preset name (default: wyvrn-<profile>-<config>)',
|
|
450
|
+
})
|
|
451
|
+
.option('profile', {
|
|
452
|
+
alias: 'p',
|
|
453
|
+
type: 'string',
|
|
454
|
+
description: 'Build profile name — determines the default preset',
|
|
455
|
+
})
|
|
456
|
+
.option('config', {
|
|
457
|
+
alias: 'c',
|
|
458
|
+
type: 'string',
|
|
459
|
+
default: 'Release',
|
|
460
|
+
description:
|
|
461
|
+
'Build configuration to test. Accepts a single value (Release) ' +
|
|
462
|
+
'or a comma-separated list (Debug,Release) to run ctest per ' +
|
|
463
|
+
'config. Valid: Debug | Release | RelWithDebInfo | MinSizeRel.',
|
|
464
|
+
})
|
|
465
|
+
.option('all-configs', {
|
|
466
|
+
type: 'boolean',
|
|
467
|
+
default: false,
|
|
468
|
+
description:
|
|
469
|
+
'Run tests for every config declared in the recipe\'s build.configs ' +
|
|
470
|
+
'(falls back to all four canonical configs when no recipe is present). ' +
|
|
471
|
+
'Equivalent to spelling out --config Debug,Release,... by hand.',
|
|
472
|
+
})
|
|
473
|
+
.option('verbose', {
|
|
474
|
+
alias: 'v',
|
|
475
|
+
type: 'boolean',
|
|
476
|
+
default: false,
|
|
477
|
+
description: 'Pass -V to ctest (extra detail per test)',
|
|
478
|
+
})
|
|
479
|
+
.option('tests-regex', {
|
|
480
|
+
alias: 'R',
|
|
481
|
+
type: 'string',
|
|
482
|
+
description: 'Run only tests whose name matches the regex (forwards to ctest -R)',
|
|
483
|
+
})
|
|
484
|
+
.option('label-regex', {
|
|
485
|
+
alias: 'L',
|
|
486
|
+
type: 'string',
|
|
487
|
+
description: 'Run only tests whose label matches the regex (forwards to ctest -L)',
|
|
488
|
+
})
|
|
489
|
+
.option('auto-build', {
|
|
490
|
+
type: 'boolean',
|
|
491
|
+
default: true,
|
|
492
|
+
description:
|
|
493
|
+
'Auto-run `wyvrnpm build` when the toolchain is stale. ' +
|
|
494
|
+
'Disable with --no-build (alias) or --no-auto-build.',
|
|
495
|
+
})
|
|
496
|
+
.option('no-build', {
|
|
497
|
+
type: 'boolean',
|
|
498
|
+
default: false,
|
|
499
|
+
description: 'Shorthand for --no-auto-build — skip the staleness check, just run ctest.',
|
|
500
|
+
})
|
|
501
|
+
.option('source', {
|
|
502
|
+
alias: 's',
|
|
503
|
+
type: 'string',
|
|
504
|
+
array: true,
|
|
505
|
+
description: 'Package source(s) — forwarded to auto-build when triggered',
|
|
506
|
+
})
|
|
507
|
+
.option('platform', {
|
|
508
|
+
type: 'string',
|
|
509
|
+
choices: ['win_x64', 'win_x86', 'linux_x64', 'linux_x86', 'osx_x64', 'osx_arm64'],
|
|
510
|
+
default: 'win_x64',
|
|
511
|
+
})
|
|
512
|
+
.option('timeout', {
|
|
513
|
+
type: 'number',
|
|
514
|
+
default: 300,
|
|
515
|
+
})
|
|
516
|
+
.option('build-dir', {
|
|
517
|
+
type: 'string',
|
|
518
|
+
description:
|
|
519
|
+
'Project-relative subdir for the build tree (default: "build"). ' +
|
|
520
|
+
'Must match the value used at install/build time. For persistent ' +
|
|
521
|
+
'override, set `binaryDirRoot` in wyvrn.local.json.',
|
|
522
|
+
})
|
|
523
|
+
.option('format', {
|
|
524
|
+
type: 'string',
|
|
525
|
+
choices: ['text', 'json'],
|
|
526
|
+
default: 'text',
|
|
527
|
+
description:
|
|
528
|
+
'Output mode. `json` emits a structured summary on stdout ' +
|
|
529
|
+
'(per-config total/passed/failed/duration/exitCode) and routes ' +
|
|
530
|
+
'log lines to stderr. Use for CI gating.',
|
|
531
|
+
});
|
|
532
|
+
},
|
|
533
|
+
(argv) => test({
|
|
534
|
+
...argv,
|
|
535
|
+
allConfigs: argv['all-configs'],
|
|
536
|
+
autoBuild: argv['no-build'] ? false : argv['auto-build'],
|
|
537
|
+
buildDir: argv['build-dir'],
|
|
538
|
+
testsRegex: argv['tests-regex'],
|
|
539
|
+
labelRegex: argv['label-regex'],
|
|
540
|
+
}),
|
|
541
|
+
)
|
|
542
|
+
|
|
438
543
|
// ── cache ─────────────────────────────────────────────────────────────────
|
|
439
544
|
.command(
|
|
440
545
|
'cache',
|
package/cmake/variables.cmake
CHANGED
|
@@ -161,19 +161,18 @@ else()
|
|
|
161
161
|
message(STATUS "Unknown architecture")
|
|
162
162
|
endif()
|
|
163
163
|
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
set(
|
|
175
|
-
set(
|
|
176
|
-
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/output/bin${WYVRN_ARCH_SUFFIX}")
|
|
164
|
+
# Put compiled artefacts inside the CMake binary dir so `wyvrnpm clean --all`
|
|
165
|
+
# (which sweeps `<binaryDirRoot>/wyvrn-*/`) catches them. Pre-2.17 wyvrnpm
|
|
166
|
+
# put these under `${CMAKE_SOURCE_DIR}/output/<bin|lib>${WYVRN_ARCH_SUFFIX}/`
|
|
167
|
+
# which leaked binaries into the source tree and survived clean.
|
|
168
|
+
# Per-profile build dirs (build/wyvrn-x64-msvc/ vs build/wyvrn-x86-msvc/)
|
|
169
|
+
# already isolate multi-arch builds, so the WYVRN_ARCH_SUFFIX directory
|
|
170
|
+
# suffix is redundant and dropped here. WYVRN_ARCH_SUFFIX itself stays
|
|
171
|
+
# exposed by wyvrn_toolchain.cmake and is still applied to artefact NAMES
|
|
172
|
+
# via WYVRN_APPLY_ARCH_SUFFIX (cmake/macros.cmake).
|
|
173
|
+
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/")
|
|
174
|
+
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/")
|
|
175
|
+
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
|
|
177
176
|
|
|
178
177
|
add_compile_definitions(
|
|
179
178
|
$<$<CONFIG:Debug>:WYVRN_DEBUG>
|
package/package.json
CHANGED
package/src/commands/clean.js
CHANGED
|
@@ -136,7 +136,7 @@ function stripPresetsFromWorkspace(rootDir, { dryRun }) {
|
|
|
136
136
|
* Flag matrix:
|
|
137
137
|
* - (default) wyvrn_internal/ + wyvrn.lock
|
|
138
138
|
* - --build-dir also the active profile's wyvrn-<profile> build dir
|
|
139
|
-
* - --all default set + every wyvrn-prefixed build dir + wyvrn-prefixed preset entries
|
|
139
|
+
* - --all default set + every wyvrn-prefixed build dir + wyvrn-prefixed preset entries + legacy <root>/output/ tree
|
|
140
140
|
* - --build-cache also the machine-wide source-build cache
|
|
141
141
|
* - --uploaded-built wipe just the upload sidecars (targeted-only when used
|
|
142
142
|
* alone — does not also do the default scrub)
|
|
@@ -197,6 +197,18 @@ async function clean(argv) {
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// --all: sweep the legacy <root>/output/ tree from pre-2.17 wyvrnpm. Old
|
|
201
|
+
// cmake/variables.cmake hardcoded CMAKE_{RUNTIME,LIBRARY,ARCHIVE}_OUTPUT_DIRECTORY
|
|
202
|
+
// under ${CMAKE_SOURCE_DIR}/output/{bin,lib}${WYVRN_ARCH_SUFFIX}/, outside
|
|
203
|
+
// the build dir — clean --all missed it. Projects that already rebuilt
|
|
204
|
+
// under the new layout simply won't have this directory.
|
|
205
|
+
if (argv.all) {
|
|
206
|
+
const legacyOutputDir = path.join(rootDir, 'output');
|
|
207
|
+
if (removePath(legacyOutputDir, { dryRun, label: legacyOutputDir })) {
|
|
208
|
+
removedAnything = true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
200
212
|
// --all: also strip wyvrn-* entries from CMakePresets.json / CMakeUserPresets.json.
|
|
201
213
|
if (argv.all) {
|
|
202
214
|
const touched = stripPresetsFromWorkspace(rootDir, { dryRun });
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const { normalizeRecipe, hasBuildRecipe } = require('../build/recipe');
|
|
6
|
+
const { buildContext } = require('../context');
|
|
7
|
+
const build = require('./build');
|
|
8
|
+
const log = require('../logger');
|
|
9
|
+
const { version: WYVRNPM_VERSION } = require('../../package.json');
|
|
10
|
+
|
|
11
|
+
const { isToolchainStale, resolveConfigList } = build._helpers;
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Pure helpers (unit-testable — no process spawning)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the test preset name for a given (profile, config). Mirrors the
|
|
19
|
+
* naming emitted by `buildTestPresets` in [src/toolchain/presets.js]:
|
|
20
|
+
* `wyvrn-<profile>-<config-lowercased>`. CLI `--preset` overrides the
|
|
21
|
+
* computed name when provided.
|
|
22
|
+
*
|
|
23
|
+
* @param {{ presetOverride?: string|null, profileName: string, config: string }} args
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function resolveTestPresetName({ presetOverride, profileName, config }) {
|
|
27
|
+
if (presetOverride) return presetOverride;
|
|
28
|
+
return `wyvrn-${profileName}-${config.toLowerCase()}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the ctest argv for a single preset invocation.
|
|
33
|
+
*
|
|
34
|
+
* @param {{
|
|
35
|
+
* presetName: string,
|
|
36
|
+
* verbose?: boolean,
|
|
37
|
+
* testsRegex?: string|null,
|
|
38
|
+
* labelRegex?: string|null,
|
|
39
|
+
* passthru?: string[],
|
|
40
|
+
* }} args
|
|
41
|
+
* @returns {string[]}
|
|
42
|
+
*/
|
|
43
|
+
function buildCtestArgs({
|
|
44
|
+
presetName,
|
|
45
|
+
verbose = false,
|
|
46
|
+
testsRegex = null,
|
|
47
|
+
labelRegex = null,
|
|
48
|
+
passthru = [],
|
|
49
|
+
}) {
|
|
50
|
+
const args = ['--preset', presetName];
|
|
51
|
+
if (verbose) args.push('-V');
|
|
52
|
+
if (testsRegex) args.push('-R', testsRegex);
|
|
53
|
+
if (labelRegex) args.push('-L', labelRegex);
|
|
54
|
+
if (passthru.length > 0) args.push(...passthru);
|
|
55
|
+
return args;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse ctest's tail summary from captured stdout. ctest emits a stable
|
|
60
|
+
* summary line near the end of every run:
|
|
61
|
+
*
|
|
62
|
+
* 100% tests passed, 0 tests failed out of 42
|
|
63
|
+
* Total Test time (real) = 1.23 sec
|
|
64
|
+
*
|
|
65
|
+
* Returns `null` for any field that couldn't be parsed (e.g. when ctest
|
|
66
|
+
* found no tests — "No tests were found!!!" — which is itself a valid
|
|
67
|
+
* outcome). Callers fall back to `exitCode` for pass/fail in that case.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} stdout
|
|
70
|
+
* @returns {{ total: number|null, passed: number|null, failed: number|null, durationMs: number|null }}
|
|
71
|
+
*/
|
|
72
|
+
function parseCtestSummary(stdout) {
|
|
73
|
+
const summaryMatch = stdout.match(
|
|
74
|
+
/(\d+(?:\.\d+)?)%\s+tests\s+passed,\s+(\d+)\s+tests?\s+failed\s+out\s+of\s+(\d+)/,
|
|
75
|
+
);
|
|
76
|
+
const timeMatch = stdout.match(
|
|
77
|
+
/Total Test time \(real\)\s*=\s*([\d.]+)\s*sec/,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (!summaryMatch) {
|
|
81
|
+
return {
|
|
82
|
+
total: null,
|
|
83
|
+
passed: null,
|
|
84
|
+
failed: null,
|
|
85
|
+
durationMs: timeMatch ? Math.round(parseFloat(timeMatch[1]) * 1000) : null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const failed = parseInt(summaryMatch[2], 10);
|
|
90
|
+
const total = parseInt(summaryMatch[3], 10);
|
|
91
|
+
return {
|
|
92
|
+
total,
|
|
93
|
+
failed,
|
|
94
|
+
passed: total - failed,
|
|
95
|
+
durationMs: timeMatch ? Math.round(parseFloat(timeMatch[1]) * 1000) : null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Spawn wrapper
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Run `ctest` with the given args.
|
|
105
|
+
*
|
|
106
|
+
* - Text mode: inherits stdio for real-time output. Returns `{ exitCode }`.
|
|
107
|
+
* - JSON mode: pipes stdout, tees to stderr so the user still sees progress,
|
|
108
|
+
* captures the full stdout for summary parsing. Returns
|
|
109
|
+
* `{ exitCode, capturedStdout }`.
|
|
110
|
+
*
|
|
111
|
+
* Resolves with the exit code (never rejects on non-zero — callers decide
|
|
112
|
+
* how to aggregate across configs).
|
|
113
|
+
*
|
|
114
|
+
* @param {string[]} args
|
|
115
|
+
* @param {{ captureStdout: boolean }} opts
|
|
116
|
+
* @returns {Promise<{ exitCode: number, capturedStdout: string }>}
|
|
117
|
+
*/
|
|
118
|
+
function runCtest(args, { captureStdout }) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const stdio = captureStdout
|
|
121
|
+
? ['inherit', 'pipe', 'inherit']
|
|
122
|
+
: 'inherit';
|
|
123
|
+
const proc = spawn('ctest', args, { stdio });
|
|
124
|
+
let captured = '';
|
|
125
|
+
if (captureStdout && proc.stdout) {
|
|
126
|
+
proc.stdout.on('data', (chunk) => {
|
|
127
|
+
const text = chunk.toString('utf8');
|
|
128
|
+
captured += text;
|
|
129
|
+
process.stderr.write(text);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
proc.on('error', (err) => {
|
|
133
|
+
reject(new Error(
|
|
134
|
+
`Failed to spawn ctest: ${err.message}\n` +
|
|
135
|
+
' Is CMake installed and on PATH?',
|
|
136
|
+
));
|
|
137
|
+
});
|
|
138
|
+
proc.on('close', (code) => {
|
|
139
|
+
resolve({ exitCode: code ?? 1, capturedStdout: captured });
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Command entry
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* `wyvrnpm test` — run ctest against the wyvrnpm-generated test preset.
|
|
150
|
+
*
|
|
151
|
+
* Auto-runs `wyvrnpm build` first when the toolchain is stale or missing
|
|
152
|
+
* (mirrors `wyvrnpm build`'s auto-install). Disable with `--no-build`.
|
|
153
|
+
*
|
|
154
|
+
* Per-config loop: same precedence as `wyvrnpm build` —
|
|
155
|
+
* `--all-configs` > `--config X,Y` > `Release`. ctest is one-config-per-
|
|
156
|
+
* invocation, so multi-config means N spawns. Exit code is 0 iff every
|
|
157
|
+
* config returns 0.
|
|
158
|
+
*
|
|
159
|
+
* `--format json` emits a structured summary on stdout (log lines route
|
|
160
|
+
* to stderr); summary stats parsed from ctest's tail output.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} argv
|
|
163
|
+
*/
|
|
164
|
+
async function test(argv) {
|
|
165
|
+
if (argv.format === 'json') log.setJsonMode(true);
|
|
166
|
+
|
|
167
|
+
let ctx;
|
|
168
|
+
try {
|
|
169
|
+
ctx = buildContext(argv);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
log.error(err.message);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const { rootDir, manifestPath } = ctx;
|
|
176
|
+
const { name: profileName } = ctx.profiles.host;
|
|
177
|
+
|
|
178
|
+
// Resolve the recipe's config list up front so --all-configs has
|
|
179
|
+
// something to consult. Same fallback chain as `wyvrnpm build`.
|
|
180
|
+
let configList;
|
|
181
|
+
try {
|
|
182
|
+
const recipe = hasBuildRecipe(ctx.manifest)
|
|
183
|
+
? (() => { try { return normalizeRecipe(ctx.manifest.build, null); } catch { return null; } })()
|
|
184
|
+
: null;
|
|
185
|
+
configList = resolveConfigList({ argv, recipeConfigs: recipe?.configs ?? null });
|
|
186
|
+
} catch (err) {
|
|
187
|
+
log.error(err.message);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Stale check / auto-build ─────────────────────────────────────────────
|
|
192
|
+
// `--no-build` (i.e. argv.autoBuild === false) opts out of the auto-build
|
|
193
|
+
// before ctest. Default behaviour matches `wyvrnpm build`'s auto-install
|
|
194
|
+
// gate: if the toolchain or build outputs look stale, run build first.
|
|
195
|
+
// We delegate to `build()` rather than reaching into install — build
|
|
196
|
+
// already handles its own staleness checks and forwards to install when
|
|
197
|
+
// needed. Pass --install:false so the build dir holds artefacts ctest
|
|
198
|
+
// can actually reach.
|
|
199
|
+
const staleness = isToolchainStale({ rootDir, manifestPath });
|
|
200
|
+
if (staleness.stale) {
|
|
201
|
+
if (argv.autoBuild === false) {
|
|
202
|
+
log.error(
|
|
203
|
+
`Toolchain is stale (${staleness.reason}) and --no-build was passed.\n` +
|
|
204
|
+
' Run: wyvrnpm build (or omit --no-build)',
|
|
205
|
+
);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
log.info(`Toolchain ${staleness.reason} — running build first`);
|
|
209
|
+
await build({
|
|
210
|
+
manifest: argv.manifest,
|
|
211
|
+
root: argv.root,
|
|
212
|
+
profile: argv.profile,
|
|
213
|
+
source: argv.source,
|
|
214
|
+
platform: argv.platform ?? 'win_x64',
|
|
215
|
+
timeout: argv.timeout ?? 300,
|
|
216
|
+
buildDir: argv.buildDir,
|
|
217
|
+
preset: argv.preset,
|
|
218
|
+
config: argv.config,
|
|
219
|
+
allConfigs: argv.allConfigs,
|
|
220
|
+
verbose: false, // build's stdout would muddy --format json
|
|
221
|
+
install: false, // we want artefacts in the build dir, not an install tree
|
|
222
|
+
autoInstall: true,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Per-config ctest loop ────────────────────────────────────────────────
|
|
227
|
+
if (configList.length > 1) {
|
|
228
|
+
log.info(`Testing ${configList.length} configs: ${configList.join(', ')}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const jsonMode = argv.format === 'json';
|
|
232
|
+
const passthru = argv['--'] ?? [];
|
|
233
|
+
const results = [];
|
|
234
|
+
|
|
235
|
+
for (const config of configList) {
|
|
236
|
+
const presetName = resolveTestPresetName({
|
|
237
|
+
presetOverride: argv.preset,
|
|
238
|
+
profileName,
|
|
239
|
+
config,
|
|
240
|
+
});
|
|
241
|
+
const ctestArgs = buildCtestArgs({
|
|
242
|
+
presetName,
|
|
243
|
+
verbose: argv.verbose,
|
|
244
|
+
testsRegex: argv.testsRegex,
|
|
245
|
+
labelRegex: argv.labelRegex,
|
|
246
|
+
passthru,
|
|
247
|
+
});
|
|
248
|
+
log.info(`ctest ${ctestArgs.join(' ')}`);
|
|
249
|
+
|
|
250
|
+
const { exitCode, capturedStdout } = await runCtest(ctestArgs, {
|
|
251
|
+
captureStdout: jsonMode,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const summary = jsonMode ? parseCtestSummary(capturedStdout) : {
|
|
255
|
+
total: null, passed: null, failed: null, durationMs: null,
|
|
256
|
+
};
|
|
257
|
+
results.push({
|
|
258
|
+
config,
|
|
259
|
+
preset: presetName,
|
|
260
|
+
total: summary.total,
|
|
261
|
+
passed: summary.passed,
|
|
262
|
+
failed: summary.failed,
|
|
263
|
+
durationMs: summary.durationMs,
|
|
264
|
+
exitCode,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const passedConfigs = results.filter((r) => r.exitCode === 0).length;
|
|
269
|
+
const failedConfigs = results.length - passedConfigs;
|
|
270
|
+
const ok = failedConfigs === 0;
|
|
271
|
+
|
|
272
|
+
if (jsonMode) {
|
|
273
|
+
const payload = {
|
|
274
|
+
command: 'test',
|
|
275
|
+
wyvrnpmVersion: WYVRNPM_VERSION,
|
|
276
|
+
profile: profileName,
|
|
277
|
+
configs: results,
|
|
278
|
+
totalConfigs: results.length,
|
|
279
|
+
passedConfigs,
|
|
280
|
+
failedConfigs,
|
|
281
|
+
ok,
|
|
282
|
+
};
|
|
283
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
284
|
+
} else if (ok) {
|
|
285
|
+
log.success(`Tests passed (${results.length} config${results.length === 1 ? '' : 's'}: ${configList.join(', ')})`);
|
|
286
|
+
} else {
|
|
287
|
+
const failedNames = results.filter((r) => r.exitCode !== 0).map((r) => r.config).join(', ');
|
|
288
|
+
log.error(`Tests failed in ${failedConfigs}/${results.length} config(s): ${failedNames}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (!ok) process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = test;
|
|
295
|
+
module.exports.default = test;
|
|
296
|
+
|
|
297
|
+
// Exported for unit tests
|
|
298
|
+
module.exports._helpers = {
|
|
299
|
+
resolveTestPresetName,
|
|
300
|
+
buildCtestArgs,
|
|
301
|
+
parseCtestSummary,
|
|
302
|
+
};
|
package/src/index.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module.exports = {
|
|
4
4
|
init: require('./commands/init'),
|
|
5
5
|
install: require('./commands/install'),
|
|
6
|
+
build: require('./commands/build'),
|
|
7
|
+
test: require('./commands/test'),
|
|
6
8
|
clean: require('./commands/clean'),
|
|
7
9
|
publish: require('./commands/publish'),
|
|
8
10
|
configure: require('./commands/configure'),
|
package/src/toolchain/presets.js
CHANGED
|
@@ -187,6 +187,41 @@ function buildBuildPresets(profileName, configs = null) {
|
|
|
187
187
|
}));
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* One ctest preset per configuration the recipe declares (or the
|
|
192
|
+
* legacy default pair `Debug` + `Release` when no list is provided).
|
|
193
|
+
*
|
|
194
|
+
* Test presets share their name with the matching build preset
|
|
195
|
+
* (`wyvrn-<profile>-<config>`) — CMake's preset name namespaces are
|
|
196
|
+
* per-array, so `cmake --build --preset wyvrn-default-release` and
|
|
197
|
+
* `ctest --preset wyvrn-default-release` can both resolve without
|
|
198
|
+
* collision. That symmetry is the whole point: the user types one
|
|
199
|
+
* preset name across both tools.
|
|
200
|
+
*
|
|
201
|
+
* `output.outputOnFailure: true` defaults to ctest's `--output-on-failure`
|
|
202
|
+
* — the only sane default for CI consumption. `execution.stopOnFailure:
|
|
203
|
+
* false` lets the full suite run rather than bailing on the first
|
|
204
|
+
* failure (Conan/Catch2 convention).
|
|
205
|
+
*
|
|
206
|
+
* @param {string} profileName
|
|
207
|
+
* @param {string[]|null} [configs] Recipe's declared configurations.
|
|
208
|
+
* @returns {Array<object>}
|
|
209
|
+
*/
|
|
210
|
+
function buildTestPresets(profileName, configs = null) {
|
|
211
|
+
const base = `wyvrn-${profileName}`;
|
|
212
|
+
const list = (Array.isArray(configs) && configs.length > 0)
|
|
213
|
+
? configs
|
|
214
|
+
: ['Debug', 'Release'];
|
|
215
|
+
return list.map((cfg) => ({
|
|
216
|
+
name: `${base}-${cfg.toLowerCase()}`,
|
|
217
|
+
displayName: `wyvrn ${profileName} (${cfg})`,
|
|
218
|
+
configurePreset: base,
|
|
219
|
+
configuration: cfg,
|
|
220
|
+
output: { outputOnFailure: true },
|
|
221
|
+
execution: { stopOnFailure: false },
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
|
|
190
225
|
/**
|
|
191
226
|
* Build a fresh presets file from scratch (no prior content to merge).
|
|
192
227
|
*/
|
|
@@ -214,6 +249,7 @@ function buildFreshPresetsFile({
|
|
|
214
249
|
profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch, configs, binaryDirRoot,
|
|
215
250
|
})],
|
|
216
251
|
buildPresets: buildBuildPresets(profileName, configs),
|
|
252
|
+
testPresets: buildTestPresets(profileName, configs),
|
|
217
253
|
};
|
|
218
254
|
}
|
|
219
255
|
|
|
@@ -242,6 +278,7 @@ function mergeIntoExisting(existing, {
|
|
|
242
278
|
profileName, profileHash, toolchainRelPath, generator, extraCacheVariables, installDir, profileArch, configs, binaryDirRoot,
|
|
243
279
|
});
|
|
244
280
|
const newBuilds = buildBuildPresets(profileName, configs);
|
|
281
|
+
const newTests = buildTestPresets(profileName, configs);
|
|
245
282
|
const newConfigureNames = new Set([newConfigure.name]);
|
|
246
283
|
// When replacing THIS profile's build presets, drop every pre-existing
|
|
247
284
|
// one that targets the same configurePreset — the fresh `newBuilds`
|
|
@@ -253,7 +290,7 @@ function mergeIntoExisting(existing, {
|
|
|
253
290
|
// leave orphan build presets pointing at configurations CMake won't
|
|
254
291
|
// generate.
|
|
255
292
|
const myBase = `wyvrn-${profileName}`;
|
|
256
|
-
const
|
|
293
|
+
const isMyChildPreset = (p) => p && p.configurePreset === myBase;
|
|
257
294
|
|
|
258
295
|
const configurePresets = (existing.configurePresets ?? []).filter((p) => !newConfigureNames.has(p.name));
|
|
259
296
|
configurePresets.push(newConfigure);
|
|
@@ -262,9 +299,16 @@ function mergeIntoExisting(existing, {
|
|
|
262
299
|
// configurePreset — the fresh `newBuilds` list (one per current
|
|
263
300
|
// `configs` entry) fully replaces them. Presets for OTHER profiles
|
|
264
301
|
// stay untouched so the multi-profile merge story is preserved.
|
|
265
|
-
const buildPresets = (existing.buildPresets ?? []).filter((p) => !
|
|
302
|
+
const buildPresets = (existing.buildPresets ?? []).filter((p) => !isMyChildPreset(p));
|
|
266
303
|
buildPresets.push(...newBuilds);
|
|
267
304
|
|
|
305
|
+
// Same logic for testPresets — scoped to THIS profile's configurePreset
|
|
306
|
+
// so user-named test presets and other-profile entries survive
|
|
307
|
+
// byte-identical. Files written by pre-2.18 wyvrnpm have no testPresets
|
|
308
|
+
// array; we just create one.
|
|
309
|
+
const testPresets = (existing.testPresets ?? []).filter((p) => !isMyChildPreset(p));
|
|
310
|
+
testPresets.push(...newTests);
|
|
311
|
+
|
|
268
312
|
return {
|
|
269
313
|
...existing,
|
|
270
314
|
version: existing.version ?? SCHEMA_VERSION,
|
|
@@ -278,6 +322,7 @@ function mergeIntoExisting(existing, {
|
|
|
278
322
|
},
|
|
279
323
|
configurePresets,
|
|
280
324
|
buildPresets,
|
|
325
|
+
testPresets,
|
|
281
326
|
};
|
|
282
327
|
}
|
|
283
328
|
|
|
@@ -387,6 +432,13 @@ function stripWyvrnPresets(existing) {
|
|
|
387
432
|
}
|
|
388
433
|
return true;
|
|
389
434
|
});
|
|
435
|
+
const testPresets = (existing.testPresets ?? []).filter((p) => {
|
|
436
|
+
if (p && (isWyvrnName(p.name) || isWyvrnName(p.configurePreset))) {
|
|
437
|
+
removed.push(p.name);
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
return true;
|
|
441
|
+
});
|
|
390
442
|
|
|
391
443
|
// Drop our vendor marker; preserve any foreign vendor entries.
|
|
392
444
|
let vendor = existing.vendor;
|
|
@@ -398,10 +450,10 @@ function stripWyvrnPresets(existing) {
|
|
|
398
450
|
// Decide if anything meaningful survives. The schema `version` field
|
|
399
451
|
// is metadata, not content — a file with only `{ version: 3 }` is a
|
|
400
452
|
// husk we should delete.
|
|
401
|
-
const hasSurvivingPresets = configurePresets.length > 0 || buildPresets.length > 0;
|
|
453
|
+
const hasSurvivingPresets = configurePresets.length > 0 || buildPresets.length > 0 || testPresets.length > 0;
|
|
402
454
|
const hasForeignVendor = vendor !== undefined;
|
|
403
455
|
const otherTopLevelKeys = Object.keys(existing).filter(
|
|
404
|
-
(k) => k !== 'version' && k !== 'vendor' && k !== 'configurePresets' && k !== 'buildPresets',
|
|
456
|
+
(k) => k !== 'version' && k !== 'vendor' && k !== 'configurePresets' && k !== 'buildPresets' && k !== 'testPresets',
|
|
405
457
|
);
|
|
406
458
|
|
|
407
459
|
if (!hasSurvivingPresets && !hasForeignVendor && otherTopLevelKeys.length === 0) {
|
|
@@ -413,6 +465,8 @@ function stripWyvrnPresets(existing) {
|
|
|
413
465
|
else delete result.configurePresets;
|
|
414
466
|
if (buildPresets.length > 0) result.buildPresets = buildPresets;
|
|
415
467
|
else delete result.buildPresets;
|
|
468
|
+
if (testPresets.length > 0) result.testPresets = testPresets;
|
|
469
|
+
else delete result.testPresets;
|
|
416
470
|
if (vendor !== undefined) result.vendor = vendor;
|
|
417
471
|
else delete result.vendor;
|
|
418
472
|
|
|
@@ -427,6 +481,7 @@ module.exports = {
|
|
|
427
481
|
isWyvrnGenerated,
|
|
428
482
|
buildConfigurePreset,
|
|
429
483
|
buildBuildPresets,
|
|
484
|
+
buildTestPresets,
|
|
430
485
|
buildFreshPresetsFile,
|
|
431
486
|
mergeIntoExisting,
|
|
432
487
|
VENDOR_KEY,
|