Skip to content

Commit 0cc2fb2

Browse files
committed
refactor(catch2): one package for both majors, fix the dead main feature
Collapse compat.catch2 + compat.catch2-v2 into a single `compat.catch2` carrying 2.13.10 and 3.15.2, and repair the v3 `main` feature, which did not work. ## One package The two majors' globs are DISJOINT — v2 has no `src/catch2/` (its only src/ file is `src/catch_with_main.cpp`) and v3 has no `single_include/` — so one `mcpp` block can carry the UNION and let each version light up exactly one half: v2 degenerates to header-only + anchor TU, v3 to a 106-TU static library. A glob that matches nothing is not an error, which is what makes this safe. Two packages was the wrong shape independent of that: `catch2-v2` encodes a major into the atomic `name` segment SPEC-001 identity reserves for the name (the same anti-pattern the compat.openssl → openssl rename removed), and it breaks semver across the major — a consumer cannot write `catch2 = "^3"`. It is also a one-way door: once `compat.catch2-v2` ships in a published artifact, withdrawing it is a breaking change. mcpp-community/mcpp#290 (per-version build blocks) is NOT a prerequisite — this works on 0.0.109 today — but it is the right destination: the disjointness is a property of two upstream trees, not something the descriptor can enforce. Recorded in the descriptor header as the premise to re-check when adding a version. ## The `main` feature was dead `features = ["main"]` on v3 produced `undefined reference to 'main'`. Measured on 0.0.109: glob + `!` negation, feature → negated path default ok | feature FAIL glob, no negation, feature → globbed path default FAIL| feature ok glob + negation + explicit re-add default ok | feature FAIL glob + `!` negation, feature → GENERATED TU default ok | feature ok Two mechanics: a `!` negation is an ABSOLUTE exclusion that a feature cannot add back, and feature gating matches LITERAL `sources` entries — a file pulled in by a glob is not gated at all. So compat.gtest's shape does not transfer (gtest_main.cc is a literal entry). The feature now compiles a generated TU that branches on `__has_include(<catch2/catch_all.hpp>)` to pick the v3 or v2 spelling. Cost: no LeakDetector registration and no wmain variant. Documented. ## Four members, because features are per-project One project cannot hold catch2 both with and without `main` (same constraint as asio-ssl vs asio-module), so 2 majors × {default, feature} = 4 members. The previous revision declared the feature on both packages and covered NEITHER — which is exactly why a non-functional feature passed CI. ## CN mirror Both versions mirrored to gitcode mcpp-res/catch2 and re-downloaded: byte-identical to upstream (same sha256, same size), so the single recorded sha256 covers either source. ## Verification (mcpp 0.0.109, gcc@16.1.0, linux-x86_64) - `mcpp xpkg parse` + `tests/check_mirror_urls.lua`: OK - all 4 members `mcpp test -p`: ok, 2 assertions each (not empty) - negative controls: dropping `features = ["main"]` from either -main member fails with `undefined reference to 'main'`, so the feature is demonstrably what supplies the entry point - both tarballs re-fetched from GLOBAL and CN and sha256-checked
1 parent 5b3d23a commit 0cc2fb2

10 files changed

Lines changed: 359 additions & 197 deletions

File tree

Lines changed: 160 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,177 @@
11
# Design doc: add Catch2 to mcpp-index
22

3-
Date: 2026-07-26
3+
Date: 2026-07-26 (revised 2026-07-27 — single package, see §"Revision")
44

55
## Source
66

77
- Repo: [catchorg/Catch2](https://github.com/catchorg/Catch2)
88
- License: BSL-1.0 (Boost Software License 1.0)
9-
- Upstream release model: amalgamated files published as GitHub release assets
109

1110
Catch2 has two major versions with incompatible APIs and source layouts:
1211

13-
| Version | Source layout | Shape |
14-
|---------|--------------|-------|
15-
| v2.13.10 | `single_include/catch2/catch.hpp` (single amalgamated header) | **header-only** |
16-
| v3.15.2 | `src/catch2/**/*.{hpp,cpp}` (individual source files) | **C-source compat (static lib)** |
12+
| Version | Source layout | Resulting shape |
13+
|---------|--------------|-----------------|
14+
| 2.13.10 | `single_include/catch2/catch.hpp` (single amalgamated header) | **header-only** |
15+
| 3.15.2 | `src/catch2/**/*.{hpp,cpp}` (individual source files) | **static library** |
16+
17+
## Revision: one package, not two
18+
19+
The first draft of this design shipped **two** packages, `compat.catch2` (v3)
20+
and `compat.catch2-v2` (v2), on the reasoning that "the v2→v3 jump changed the
21+
repo layout completely, so the two versions cannot share a single `mcpp`
22+
block."
23+
24+
The premise is right; the conclusion was not. The two layouts' globs are
25+
**disjoint**, so a single `mcpp` block can carry the **union** of both and let
26+
each version light up exactly one half:
27+
28+
| glob | 2.13.10 | 3.15.2 |
29+
|---|---|---|
30+
| `*/single_include` | exists | absent |
31+
| `*/src/catch2/**/*.cpp` | **0 matches** — v2's only `src/` file is `src/catch_with_main.cpp`, *not* under `src/catch2/` | 107 |
32+
| `catch2/catch_all.hpp` | absent | exists |
33+
34+
An `include_dirs` entry whose glob matches nothing is not an error, and a
35+
`sources` glob that matches nothing simply contributes no TUs — which is what
36+
makes the union safe. At 2.13.10 the package degenerates to header-only
37+
(`single_include` + an anchor TU); at 3.15.2 it is a 106-TU static library.
38+
39+
The last row doubles as a compile-time major discriminator:
40+
`__has_include(<catch2/catch_all.hpp>)`.
41+
42+
### Why one package is the better shape regardless
43+
44+
- **A version does not belong in `name`.** `catch2-v2` smuggles a major into
45+
the atomic name segment that SPEC-001 identity reserves for the name alone —
46+
the same anti-pattern the `compat.openssl``openssl` rename removed.
47+
- **Semver stops working across the split.** With two packages a consumer
48+
cannot write `catch2 = "^3"`, and version resolution cannot see v2 and v3 as
49+
the same library.
50+
- **Discovery.** One library should not answer to two package names.
51+
- **It is a one-way door.** Once `compat.catch2-v2` ships in a published index
52+
artifact, withdrawing it is a breaking change. The decision had to be made
53+
before merge, not after.
54+
55+
### Relationship to mcpp#290
56+
57+
[mcpp-community/mcpp#290](https://github.com/mcpp-community/mcpp/issues/290)
58+
asks for per-version `mcpp` build blocks. It is **not** a prerequisite here —
59+
the merge works on mcpp 0.0.109 today.
60+
61+
Nor is Catch2 the case #290 argues from. That issue's motivating example
62+
(llama.cpp b10069 vs b10107) shares ~95% of its build rules and differs in a
63+
few source files. Catch2 v2/v3 share essentially *nothing* but boilerplate
64+
(`language`, `import_std`, `c_standard`, `targets`, `deps`); they merge only
65+
because their globs happen not to overlap.
66+
67+
That "happen not to" is the weak point, and it is exactly what #290 would fix:
68+
the disjointness is a property of two upstream trees, not something this
69+
descriptor can enforce. When #290 lands, this collapses into explicit
70+
`["2.x"]` / `["3.x"]` blocks and the implicit assumption disappears. Until
71+
then the premise is documented in the descriptor header and must be re-checked
72+
whenever a version is added.
73+
74+
## Package shape
75+
76+
- **Sources**: anchor TU + `*/src/catch2/**/*.cpp` minus
77+
`!*/src/catch2/internal/catch_main.cpp` (107 files, 106 compiled).
78+
- **include_dirs**: `{ "*/single_include", "*/src", "mcpp_generated" }`.
79+
- **Anchor** (`mcpp_generated/catch2_anchor.c`): required at v2, where the
80+
sources glob matches nothing and a lib target still needs a TU. Same shape
81+
as compat.eigen / compat.khrplatform. Inert at v3.
82+
- **catch_user_config.hpp**: materialised via `generated_files` (upstream ships
83+
only `catch_user_config.hpp.in` and lets CMake fill it). Only the two VALUE
84+
defines are mandatory — `CATCH_CONFIG_DEFAULT_REPORTER` and
85+
`CATCH_CONFIG_CONSOLE_WIDTH`; every other entry in the `.in` is a
86+
`#cmakedefine`, i.e. absent means "use the compiler-detected default". Both
87+
are `#ifndef`-guarded so a consumer can override via `-D`. v2 never includes
88+
this file. **Re-read the upstream `.in` when bumping v3** — a newly added
89+
mandatory value-define would break silently here.
90+
91+
## The `main` feature
92+
93+
`features = ["main"]` compiles a **generated** TU that supplies a default entry
94+
point, branching on `__has_include(<catch2/catch_all.hpp>)` to pick the v3
95+
(`Catch::Session`) or v2 (`CATCH_CONFIG_MAIN`) spelling.
96+
97+
It deliberately does **not** point at upstream's
98+
`src/catch2/internal/catch_main.cpp`. That was the first draft's approach and
99+
it does not work. Measured on mcpp 0.0.109:
100+
101+
| descriptor form | default path | `features = ["main"]` |
102+
|---|---|---|
103+
| glob + `!` negation, feature → the negated path | ok | **FAIL** `undefined reference to 'main'` |
104+
| glob, no negation, feature → the globbed path | **FAIL** `multiple definition of 'main'` | ok |
105+
| glob + `!` negation + explicit re-add, feature → that path | ok | **FAIL** `undefined reference to 'main'` |
106+
| glob + `!` negation, feature → **generated TU** | ok | ok |
107+
108+
Two mechanics behind that table:
109+
110+
1. A `!` negation in `sources` is an **absolute** exclusion applied to the
111+
final source set. A feature cannot add the path back — not even if the path
112+
is also listed explicitly afterwards.
113+
2. Feature gating matches **literal** `sources` entries. A file pulled in by a
114+
glob is not gated at all, so it lands in the default build.
115+
116+
Together these mean the `compat.gtest` pattern does **not** transfer:
117+
`gtest_main.cc` is a literal `sources` entry, so listing it under a feature
118+
gates it. Under a glob the same idea silently breaks in one direction or the
119+
other. A generated TU sidesteps the conflict.
120+
121+
Cost of not reusing upstream's file: no `LeakDetector` registration (a Windows
122+
CRT-debug nicety) and no `wmain` variant (reachable only under `_UNICODE` on
123+
Windows). Consumers needing either write their own `main()`.
124+
125+
## Why this needs four workspace members
126+
127+
Features are resolved **per consuming project**, so one project cannot hold
128+
`catch2` both with and without `main` (same constraint that forces `asio-ssl`
129+
to be separate from `asio-module`). Two majors × {default, `main` feature} = 4:
130+
131+
| member | version | requests | what it would catch |
132+
|---|---|---|---|
133+
| `catch2` | 3.15.2 | — (own `main()`) | negation stops working → `multiple definition of 'main'` |
134+
| `catch2-main` | 3.15.2 | `features = ["main"]` | feature stops gating the TU in → `undefined reference to 'main'` |
135+
| `catch2-v2` | 2.13.10 | — (own `CATCH_CONFIG_MAIN`) | union bleeds across versions; `single_include` unresolved |
136+
| `catch2-v2-main` | 2.13.10 | `features = ["main"]` | the `__has_include` ELSE branch (v3 members only take THEN) |
137+
138+
The first draft declared a `main` feature on both packages and covered
139+
**neither** — which is precisely why the broken feature passed CI. Every test
140+
body keeps a real `REQUIRE`, and Catch2 prints its assertion count on exit, so
141+
a TU that compiles to nothing cannot pass quietly.
142+
143+
## Download URLs and mirrors
144+
145+
GitHub tag archive tarballs (not the individual amalgamated release assets):
146+
xpm takes one URL per version, and the tag archive is the only form that
147+
carries both layouts. Neither archive contains symlinks, so the Windows
148+
extraction path is safe.
149+
150+
| Version | GLOBAL | CN | SHA256 |
151+
|---|---|---|---|
152+
| 2.13.10 | `github.com/catchorg/Catch2/archive/refs/tags/v2.13.10.tar.gz` | `gitcode.com/mcpp-res/catch2/releases/download/2.13.10/catch2-2.13.10.tar.gz` | `d54a712b…9943` |
153+
| 3.15.2 | `github.com/catchorg/Catch2/archive/refs/tags/v3.15.2.tar.gz` | `gitcode.com/mcpp-res/catch2/releases/download/3.15.2/catch2-3.15.2.tar.gz` | `acfae120…0420` |
154+
155+
CN assets were uploaded to the `mcpp-res` gitcode org and re-downloaded: both
156+
are byte-identical to the upstream archives (same sha256, same size), so the
157+
single recorded `sha256` covers either source.
17158

18-
The v2→v3 jump changed the repo layout completely (`single_include/``src/catch2/`),
19-
so the two versions cannot share a single `mcpp` block. Two separate packages are used:
20-
21-
- `compat.catch2` — v3, C-source compat (static library from individual source files)
22-
- `compat.catch2-v2` — v2, header-only
23-
24-
## Package shape decision
25-
26-
### compat.catch2 (v3)
27-
28-
- **Shape**: C-source compat static library (same as compat.cjson / compat.gtest)
29-
- **Sources**: `*/src/catch2/**/*.cpp` — all Catch2 implementation files (107 TUs)
30-
- **include_dirs**: `{ "*/src", "mcpp_generated" }` — exposes `<catch2/catch_all.hpp>`
31-
etc. from the source tree, and `catch2/catch_user_config.hpp` from generated files
32-
- **catch_user_config.hpp**: materialised via `generated_files` (normally
33-
CMake-generated; provides `CATCH_CONFIG_DEFAULT_REPORTER` and
34-
`CATCH_CONFIG_CONSOLE_WIDTH`). Both value-defines use `#ifndef` guards so
35-
users can override them via `cxxflags = ["-DCATCH_CONFIG_DEFAULT_REPORTER=xml"]`
36-
in their own `mcpp.toml`. Other boolean toggles (e.g. `CATCH_CONFIG_WCHAR`)
37-
are auto-detected by `catch_compiler_capabilities.hpp` and can likewise be
38-
forced on/off via `-D` flags.
39-
- **catch_main.cpp**: provides a default `main()`. Excluded from the default
40-
source set via `!` negation (`"!*/src/catch2/internal/catch_main.cpp"`);
41-
only compiled when `features = ["main"]` is requested (same pattern as
42-
`compat.gtest`'s `main` feature gating `gtest_main.cc`)
43-
44-
### compat.catch2-v2 (v2)
45-
46-
- **Shape**: header-only (same as compat.eigen / compat.opengl)
47-
- **include_dirs**: `{ "*/single_include" }` — consumers `#include <catch2/catch.hpp>`
48-
- **Anchor**: trivial `.c` TU (`mcpp_generated/catch2_v2_anchor.c`) to give mcpp
49-
a buildable lib target
50-
- **`main` feature**: gates a generated `main.cpp` that defines
51-
`CATCH_CONFIG_MAIN` and includes `<catch2/catch.hpp>`, providing a ready-made
52-
`main()`. Excluded by default
53-
54-
## Download URLs
55-
56-
Both packages use GitHub tag archive tarballs (not individual release assets)
57-
because xpm supports one URL per version.
58-
59-
| Package | Version | GitHub archive URL | SHA256 |
60-
|---------|---------|--------------------|--------|
61-
| compat.catch2 | 3.15.2 | `https://github.com/catchorg/Catch2/archive/refs/tags/v3.15.2.tar.gz` | `acfae120892c2b67a74142d36d060c0caa96f1c3aaa8aabd96e19961163d0420` |
62-
| compat.catch2-v2 | 2.13.10 | `https://github.com/catchorg/Catch2/archive/refs/tags/v2.13.10.tar.gz` | `d54a712b7b1d7708bc7a819a8e6e47b2fde9536f487b89ccbca295072a7d9943` |
63-
64-
### Individual release asset SHAs (for reference)
65-
66-
| File | Release URL | SHA256 |
67-
|------|------------|--------|
68-
| catch.hpp (v2.13.10) | `https://github.com/catchorg/Catch2/releases/download/v2.13.10/catch.hpp` | `3725c0f0a75f376a5005dde31ead0feb8f7da7507644c201b814443de8355170` |
69-
| catch_amalgamated.cpp (v3.15.2) | `https://github.com/catchorg/Catch2/releases/download/v3.15.2/catch_amalgamated.cpp` | `1ec0b0c0d6133f76fea521295be3e69e3aa5464ab92972897414ca59b7f674d5` |
70-
| catch_amalgamated.hpp (v3.15.2) | `https://github.com/catchorg/Catch2/releases/download/v3.15.2/catch_amalgamated.hpp` | `f0573f46ac989896a20c524085307b633f01c8e1cdbbe6d9b39f63827c2d6c5e` |
71-
72-
## CN mirror
73-
74-
Not configured yet — no `mcpp-res` write access. Using plain-string upstream URLs
75-
(no `{ GLOBAL=…, CN=… }` table). CN users will fall back to the upstream source.
76-
Mirrors can be added later by a maintainer.
77-
78-
## Test examples
159+
## Verification
79160

80-
- **tests/examples/catch2/**: v3 static library build. Includes
81-
`<catch2/catch_all.hpp>`, provides own `main()` using
82-
`Catch::Session().run()`, runs two `TEST_CASE`s.
83-
- **tests/examples/catch2-v2/**: v2 header-only. Defines `CATCH_CONFIG_MAIN` to
84-
get a built-in main, includes `<catch2/catch.hpp>`, runs two `TEST_CASE`s
85-
within a single TU.
161+
Local, mcpp 0.0.109 (matching CI `MCPP_VERSION`), gcc@16.1.0, linux-x86_64:
86162

87-
## Verification
163+
- `mcpp xpkg parse` on the descriptor: OK
164+
- `tests/check_mirror_urls.lua`: OK
165+
- All four members via `mcpp test -p`: ok
166+
- **Negative controls** (not committed): dropping `features = ["main"]` from
167+
each `-main` member fails with `undefined reference to 'main'`, confirming
168+
the feature is what supplies the entry point rather than something else.
169+
- Both tarballs re-downloaded from GLOBAL and CN and sha256-checked.
88170

89-
- SHA256 computed twice (confirmed stable) via `sha256sum` on the codeload tarballs
90-
- File paths confirmed by `tar -tzf` against tag archives
91-
- Both packages pass `mcpp xpkg parse` lint
92-
- `mcpp test -p catch2`**test result ok** (2 assertions in 2 test cases)
93-
- `mcpp test -p catch2-v2`**test result ok** (2 assertions in 2 test cases)
94-
- Tested with mcpp 0.0.108 (matching CI `MCPP_VERSION`) on x86_64-windows-msvc
171+
CI covers linux / macOS / windows.
95172

96173
## Notes
97174

98-
- Catch2 v3 static library requires `catch_user_config.hpp` (normally
99-
CMake-generated). A minimal version is materialised via `generated_files` at
100-
`mcpp_generated/catch2/catch_user_config.hpp` with `#ifndef` guards on the
101-
two value-defines (`CATCH_CONFIG_DEFAULT_REPORTER` and
102-
`CATCH_CONFIG_CONSOLE_WIDTH`), allowing users to override them via `-D` flags.
103-
- `catch_main.cpp` is gated via `!` negation in `sources` + feature listing,
104-
following the exact `compat.gtest` pattern. No `-DCATCH_AMALGAMATED_CUSTOM_MAIN`
105-
needed.
106-
- Catch2 v3 requires C++14 or later; v2 requires C++11; both compatible with
107-
mcpp's `language = "c++23"`.
108-
- No `c_standard` needed for v3 (pure C++); anchor for v2 uses
109-
`c_standard = "c11"`.
175+
- Catch2 v3 requires C++14, v2 requires C++11; both build under
176+
`language = "c++23"`.
177+
- `c_standard = "c11"` is for the anchor TU.

mcpp.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ members = [
1212
"tests/examples/asio-ssl",
1313
"tests/examples/build-mcpp",
1414
"tests/examples/catch2",
15+
"tests/examples/catch2-main",
1516
"tests/examples/catch2-v2",
17+
"tests/examples/catch2-v2-main",
1618
"tests/examples/cjson",
1719
"tests/examples/core",
1820
"tests/examples/eigen",

pkgs/c/compat.catch2-v2.lua

Lines changed: 0 additions & 69 deletions
This file was deleted.

0 commit comments

Comments
 (0)