|
1 | 1 | # Design doc: add Catch2 to mcpp-index |
2 | 2 |
|
3 | | -Date: 2026-07-26 |
| 3 | +Date: 2026-07-26 (revised 2026-07-27 — single package, see §"Revision") |
4 | 4 |
|
5 | 5 | ## Source |
6 | 6 |
|
7 | 7 | - Repo: [catchorg/Catch2](https://github.com/catchorg/Catch2) |
8 | 8 | - License: BSL-1.0 (Boost Software License 1.0) |
9 | | -- Upstream release model: amalgamated files published as GitHub release assets |
10 | 9 |
|
11 | 10 | Catch2 has two major versions with incompatible APIs and source layouts: |
12 | 11 |
|
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. |
17 | 158 |
|
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 |
79 | 160 |
|
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: |
86 | 162 |
|
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. |
88 | 170 |
|
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. |
95 | 172 |
|
96 | 173 | ## Notes |
97 | 174 |
|
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. |
0 commit comments