Skip to content

Commit 9dfa338

Browse files
committed
0.19.1: json::value compiles under clang with libstdc++, and CI reads every build program as an editor does
`mcpp::plugins::json::value` held `std::vector<std::pair<std::string, value>>`. Under clang with libstdc++'s `std` module the pair is instantiated while `value` is incomplete, so `mcpp.plugins` failed to compile and an editor's clangd -- which reads a GCC project with exactly that combination -- showed every `import mcpp.…;` of a build program as a missing module. GCC and clang with libc++ accept the code, so no build ever failed. A member is now a plain struct `member { first; second; }` and `get()` is defined once both types are complete. `members` is used only inside the lib root and the fields keep their names, so the change is source-compatible. CI gains the two combinations it did not compile: `toolchain-llvm` builds and runs the toolchain-neutral fixtures under llvm@22.1.8 with libc++ (the Qt fixtures configure only, since Qt's Linux binaries are built against libstdc++), and `toolchain-editor` reads every fixture's `build.mcpp` with mcppls 0.0.15 over the GCC toolchain and fails on any error or on a file it did not read. On the previous code 34 of 37 fixtures fail there. docs/plugin-development.md §4 states the rule: a plugin is the C++23 that all three accept, with the differences met so far. Closes #46
1 parent caf136b commit 9dfa338

6 files changed

Lines changed: 319 additions & 15 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env bash
2+
# The two lanes that read this collection's C++ with something other than the
3+
# default Linux toolchain. A CI step names the lane:
4+
#
5+
# bash .github/scripts/check-toolchain-lanes.sh llvm
6+
# bash .github/scripts/check-toolchain-lanes.sh editor
7+
#
8+
# THREE COMBINATIONS READ THIS CODE, AND EACH ONE HAS REFUSED SOMETHING THE
9+
# OTHERS ACCEPT. GCC with libstdc++ builds it by default on Linux. clang with
10+
# libc++ builds it under `--toolchain llvm@…`, as on macOS. clang with
11+
# libstdc++'s `std` module is what an editor's clangd uses to read a project
12+
# built with GCC -- and no build ever compiled that combination, so a
13+
# `std::pair<std::string, value>` inside `value` (0.19.1) failed only there:
14+
# every build passed and every build program in an editor showed a module that
15+
# was "not found". docs/plugin-development.md §4 states the rule this enforces.
16+
#
17+
# Environment: MCPP (llvm); MCPP_EDITOR and MCPPLS (editor).
18+
set -euo pipefail
19+
20+
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
21+
LLVM="${MCPP_LLVM:-llvm@22.1.8}"
22+
23+
fail() { echo "FAIL: $*"; exit 1; }
24+
25+
# ── llvm: clang with libc++ ───────────────────────────────────────────────────
26+
#
27+
# FULL BUILDS for the fixtures whose build needs nothing tied to libstdc++, each
28+
# with the criterion its program prints. `all-rules-compile` imports every
29+
# member and calls it, so the whole collection is compiled under libc++ by its
30+
# first line; the others run what the members produce.
31+
#
32+
# CONFIGURE ONLY for the Qt fixtures. Qt's official Linux binaries are built
33+
# against libstdc++ and a libc++ program cannot link them, which is the Qt
34+
# fixtures' business and not this lane's. `--configure-only` still compiles and
35+
# runs the build program -- the member and the library under libc++ -- and that
36+
# is the part this lane is for.
37+
#
38+
# NOT HERE: the fixtures that pin a device toolchain (cuda, hip, sycl,
39+
# multi-rule) already build with llvm@22.1.8 in the consumers job, and the
40+
# deps fixtures' libc++ rows are `vcpkg-libcxx` and `cmake-cache` there.
41+
llvm_full=(
42+
"all-rules-compile|^all-rules-compile ok"
43+
"plugin-logic|"
44+
"embed-consumer|^size=33 "
45+
"embed-module-consumer|^all ok"
46+
"embed-table-consumer|^all ok"
47+
"island-interface|^all ok"
48+
"archive-consumer|^archive-consumer: 'greetings from the archive'"
49+
"spirv-module-consumer|^all ok"
50+
)
51+
llvm_configure_only=(qt-consumer qt-widgets-consumer qt-sdk-consumer qt-import-only)
52+
53+
llvm_lane() {
54+
: "${MCPP:?MCPP names the mcpp under test}"
55+
"$MCPP" toolchain install llvm "${LLVM#llvm@}"
56+
local entry fixture expect results declared passed
57+
for entry in "${llvm_full[@]}"; do
58+
fixture=${entry%%|*}; expect=${entry#*|}
59+
echo "── $fixture under $LLVM"
60+
cd "$ROOT/tests/$fixture"
61+
rm -rf target; mkdir -p target/ci
62+
"$MCPP" build --toolchain "$LLVM" > target/ci/llvm-build.log 2>&1 ||
63+
{ cat target/ci/llvm-build.log; fail "$fixture does not build under $LLVM"; }
64+
grep -q -- '-stdlib=libc++' target/*/*/build.ninja ||
65+
fail "$fixture was not linked against libc++: the lane did not build what it says it built"
66+
if [ "$fixture" = plugin-logic ]; then
67+
# The kit's verdicts, counted against the cases the fixture declares,
68+
# as check-deps-and-qt.sh counts them under the default toolchain.
69+
results=$(find target -path '*plugins-testing/results.txt' | head -1)
70+
[ -n "$results" ] || fail "the kit wrote no results.txt under $LLVM"
71+
declared=$(grep -cE '^ \{ "' build.mcpp)
72+
passed=$(grep -c '^PASS ' "$results")
73+
[ "$declared" -gt 0 ] && [ "$passed" -eq "$declared" ] ||
74+
{ cat "$results"; fail "$passed of $declared plugin-logic cases passed under $LLVM"; }
75+
echo "ok: $passed of $declared plugin-logic cases passed under $LLVM"
76+
continue
77+
fi
78+
"$MCPP" run --toolchain "$LLVM" > target/ci/llvm-run.log 2>&1 ||
79+
{ cat target/ci/llvm-run.log; fail "$fixture built under $LLVM and did not run"; }
80+
grep -qE "$expect" target/ci/llvm-run.log ||
81+
{ cat target/ci/llvm-run.log; fail "$fixture ran under $LLVM without printing '$expect'"; }
82+
echo "ok: $fixture builds and runs under $LLVM"
83+
done
84+
for fixture in "${llvm_configure_only[@]}"; do
85+
echo "── $fixture under $LLVM, configured"
86+
cd "$ROOT/tests/$fixture"
87+
rm -rf target; mkdir -p target/ci
88+
"$MCPP" build --configure-only --toolchain "$LLVM" > target/ci/llvm-configure.log 2>&1 ||
89+
{ cat target/ci/llvm-configure.log; fail "$fixture's build program does not compile or run under $LLVM"; }
90+
echo "ok: $fixture's build program compiles and runs under $LLVM"
91+
done
92+
}
93+
94+
# ── editor: clang with libstdc++, through mcppls ──────────────────────────────
95+
#
96+
# What an editor shows for each fixture's `build.mcpp`, read by mcppls with its
97+
# own clangd over the default (GCC) toolchain's standard library. A module the
98+
# build program imports that clangd cannot compile is an error on the import
99+
# line, so `counts.error` is the criterion; every diagnostic goes to the job
100+
# summary, so a failure names the line without downloading anything.
101+
#
102+
# A FILE THAT WAS NOT READ IS A FAILURE, NOT A PASS. mcppls says so with
103+
# `build-program-context` when the mcpp it runs describes no build programs
104+
# (before 2026.10.11.1); zero errors from a file nobody compiled is the silent
105+
# pass this lane exists to prevent.
106+
editor_lane() {
107+
: "${MCPP_EDITOR:?MCPP_EDITOR names the mcpp mcppls runs}"
108+
: "${MCPPLS:?MCPPLS names the mcppls binary of the payload}"
109+
export PATH="$(dirname "$MCPP_EDITOR"):$PATH"
110+
export MCPPLS_CACHE_DIR="${RUNNER_TEMP:-/tmp}/mcppls-cache"
111+
local summary="${GITHUB_STEP_SUMMARY:-/dev/null}" failed=0 checked=0 dir fixture out
112+
{
113+
echo "### build.mcpp in the editor ($("$MCPPLS" --version | head -1), $("$MCPP_EDITOR" --version | head -1))"
114+
echo
115+
echo "| fixture | errors | warnings | diagnostics |"
116+
echo "|---|---|---|---|"
117+
} >> "$summary"
118+
for dir in "$ROOT"/tests/*/; do
119+
fixture=$(basename "$dir")
120+
[ -f "$dir/build.mcpp" ] || continue
121+
echo "── $fixture"
122+
out="${RUNNER_TEMP:-/tmp}/editor-$fixture.json"
123+
# mcppls exits 1 when it reports an error and still writes the report,
124+
# so the report decides; only a run that wrote none is its own failure.
125+
local status=0
126+
(cd "$dir" && "$MCPPLS" diagnostics build.mcpp --format json --timeout 300) > "$out" || status=$?
127+
python3 -c 'import json, sys; json.load(open(sys.argv[1]))' "$out" 2> /dev/null ||
128+
{ echo "| $fixture | — | — | mcppls diagnostics exited $status without a report |" >> "$summary"; failed=1; continue; }
129+
checked=$((checked + 1))
130+
python3 - "$fixture" "$out" "$summary" <<'EOF' || failed=1
131+
import json, sys
132+
fixture, path, summary = sys.argv[1:4]
133+
report = json.load(open(path))
134+
counts = report["counts"]
135+
found = [d for f in report["files"] for d in f["diagnostics"]]
136+
unread = [d for d in found if d.get("code") == "build-program-context"]
137+
incomplete = [f["file"] for f in report["files"] if not f.get("complete", True)]
138+
cells = "<br>".join(
139+
f"{d['severity']} {d['location']['file']}:{d['location']['line']} `{d.get('code', '')}` {d['message'][:240]}".replace("|", "\\|")
140+
for d in found) or "none"
141+
with open(summary, "a") as out:
142+
out.write(f"| {fixture} | {counts['error']} | {counts['warning']} | {cells} |\n")
143+
for d in found:
144+
print(f" {d['severity']} {d['location']['file']}:{d['location']['line']} [{d.get('code', '')}] {d['message']}")
145+
if counts["error"] or unread or incomplete:
146+
why = "errors" if counts["error"] else "not read" if unread else "incomplete"
147+
print(f"FAIL: {fixture}'s build.mcpp in the editor: {why}")
148+
sys.exit(1)
149+
print(f"ok: {fixture}'s build.mcpp has no error in the editor")
150+
EOF
151+
done
152+
[ "$checked" -gt 0 ] || fail "no fixture's build.mcpp was checked"
153+
[ "$failed" = 0 ] || fail "an editor reports errors in a fixture's build.mcpp (the job summary lists them)"
154+
echo "ok: $checked build programs read in the editor without an error"
155+
}
156+
157+
case "${1:-}" in
158+
llvm) llvm_lane ;;
159+
editor) editor_lane ;;
160+
*) echo "usage: $0 llvm|editor"; exit 2 ;;
161+
esac

‎.github/workflows/ci.yml‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ env:
8989
# what it built. This is how a change to the engine is measured against this
9090
# collection before either is released.
9191
MCPP_SOURCE_REF: ${{ inputs.mcpp_source_ref || '' }}
92+
# THE EDITOR LANE'S TWO PROGRAMS (0.19.1), PINNED. mcppls reads a fixture's
93+
# `build.mcpp` as an editor does, with its own clangd over the GCC
94+
# toolchain's libstdc++ -- the one combination no build compiles. The mcpp it
95+
# runs is the first release that describes build programs in the build
96+
# database (2026.10.11.1); under an older one mcppls reads no build program
97+
# and says so, which the lane counts as a failure. Raising either is a
98+
# one-line change here.
99+
MCPPLS_VERSION: 0.0.15
100+
MCPP_EDITOR_VERSION: 2026.10.11.1
92101
# PINNED, AND WITHOUT IT THE CACHE BELOW CACHED NOTHING.
93102
#
94103
# A released mcpp is self-contained: with no `MCPP_HOME`, `mcpp self env`
@@ -1741,6 +1750,100 @@ jobs:
17411750
- name: a package that enables rules-qt only for its module builds without a report
17421751
run: bash .github/scripts/check-deps-and-qt.sh qt-import-only
17431752

1753+
# ── THE OTHER TWO WAYS THIS CODE IS COMPILED ON LINUX (0.19.1) ─────────────
1754+
#
1755+
# The consumers job builds with the default toolchain, GCC with libstdc++.
1756+
# Two more combinations read the same sources, and each has refused code the
1757+
# others accept: clang with libc++ (`--toolchain llvm@…`, macOS's family),
1758+
# and clang with libstdc++'s `std` module, which is how an editor's clangd
1759+
# reads a project built with GCC. `json::value` held a
1760+
# `std::pair<std::string, value>` that only the last one rejects, so every
1761+
# build passed while every build program showed a missing module in the
1762+
# editor. `.github/scripts/check-toolchain-lanes.sh` states what each lane
1763+
# runs; docs/plugin-development.md §4 states the rule.
1764+
toolchain-llvm:
1765+
name: llvm (linux x86_64, clang + libc++)
1766+
runs-on: ubuntu-24.04
1767+
env:
1768+
MCPP_HOME: /home/runner/.mcpp
1769+
timeout-minutes: 120
1770+
steps:
1771+
- uses: actions/checkout@v4
1772+
1773+
- name: Cache the mcpp sandbox
1774+
uses: actions/cache@v4
1775+
with:
1776+
path: ~/.mcpp
1777+
key: mcpp-sandbox-llvm-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_BOOTSTRAP }}-${{ hashFiles('tests/*/mcpp.toml') }}
1778+
restore-keys: |
1779+
mcpp-sandbox-llvm-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_BOOTSTRAP }}-
1780+
1781+
- name: Fetch the released mcpp
1782+
run: |
1783+
curl -L -fsS --retry 3 --retry-all-errors -o mcpp.tar.gz \
1784+
"https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_BOOTSTRAP}/mcpp-${MCPP_BOOTSTRAP}-linux-x86_64.tar.gz"
1785+
tar -xzf mcpp.tar.gz
1786+
MCPP="$PWD/mcpp-${MCPP_BOOTSTRAP}-linux-x86_64/bin/mcpp"
1787+
"$MCPP" --version
1788+
# The bundled xlings, named because `MCPP_HOME` is pinned (see consumers).
1789+
export MCPP_VENDORED_XLINGS="$PWD/mcpp-${MCPP_BOOTSTRAP}-linux-x86_64/registry/bin/xlings"
1790+
"$MCPP" self config --mirror GLOBAL
1791+
echo "MCPP=$MCPP" >> "$GITHUB_ENV"
1792+
echo "MCPP_VENDORED_XLINGS=$MCPP_VENDORED_XLINGS" >> "$GITHUB_ENV"
1793+
1794+
- name: the mcpp under review, built from MCPP_SOURCE_REF when it is set
1795+
timeout-minutes: 45
1796+
run: bash .github/scripts/mcpp-under-review.sh
1797+
1798+
- name: the collection builds and runs under clang with libc++
1799+
run: bash .github/scripts/check-toolchain-lanes.sh llvm
1800+
1801+
toolchain-editor:
1802+
name: editor (linux x86_64, clangd + libstdc++)
1803+
runs-on: ubuntu-24.04
1804+
env:
1805+
MCPP_HOME: /home/runner/.mcpp
1806+
timeout-minutes: 60
1807+
steps:
1808+
- uses: actions/checkout@v4
1809+
1810+
- name: Cache the mcpp sandbox
1811+
uses: actions/cache@v4
1812+
with:
1813+
path: ~/.mcpp
1814+
key: mcpp-sandbox-editor-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_EDITOR_VERSION }}-${{ hashFiles('tests/*/mcpp.toml') }}
1815+
restore-keys: |
1816+
mcpp-sandbox-editor-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.MCPP_EDITOR_VERSION }}-
1817+
1818+
- name: Fetch the mcpp the editor runs
1819+
run: |
1820+
curl -L -fsS --retry 3 --retry-all-errors -o mcpp.tar.gz \
1821+
"https://github.com/mcpp-community/mcpp/releases/download/v${MCPP_EDITOR_VERSION}/mcpp-${MCPP_EDITOR_VERSION}-linux-x86_64.tar.gz"
1822+
tar -xzf mcpp.tar.gz
1823+
MCPP_EDITOR="$PWD/mcpp-${MCPP_EDITOR_VERSION}-linux-x86_64/bin/mcpp"
1824+
export MCPP_VENDORED_XLINGS="$PWD/mcpp-${MCPP_EDITOR_VERSION}-linux-x86_64/registry/bin/xlings"
1825+
"$MCPP_EDITOR" --version
1826+
"$MCPP_EDITOR" self config --mirror GLOBAL
1827+
# The default toolchain, installed before the first fixture rather
1828+
# than inside mcppls's first build-database run and its deadline.
1829+
(cd tests/island-interface && "$MCPP_EDITOR" build --configure-only)
1830+
echo "MCPP_EDITOR=$MCPP_EDITOR" >> "$GITHUB_ENV"
1831+
echo "MCPP_VENDORED_XLINGS=$MCPP_VENDORED_XLINGS" >> "$GITHUB_ENV"
1832+
1833+
- name: Fetch the mcppls release payload, checked against its SHA256SUMS
1834+
run: |
1835+
base="https://github.com/Sunrisepeak/mcpp-language-server/releases/download/v${MCPPLS_VERSION}"
1836+
curl -L -fsS --retry 3 --retry-all-errors -o payload-linux-x64.tar.gz "$base/payload-linux-x64.tar.gz"
1837+
curl -L -fsS --retry 3 --retry-all-errors -o SHA256SUMS "$base/SHA256SUMS"
1838+
grep ' payload-linux-x64.tar.gz$' SHA256SUMS | sha256sum -c -
1839+
mkdir -p "$RUNNER_TEMP/mcppls" && tar -xzf payload-linux-x64.tar.gz -C "$RUNNER_TEMP/mcppls"
1840+
MCPPLS="$RUNNER_TEMP/mcppls/payload/bin/mcppls"
1841+
"$MCPPLS" --version
1842+
echo "MCPPLS=$MCPPLS" >> "$GITHUB_ENV"
1843+
1844+
- name: every fixture's build.mcpp reads without an error in the editor
1845+
run: bash .github/scripts/check-toolchain-lanes.sh editor
1846+
17441847
# ── THE SAME RULE ON THE OTHER TWO PLATFORMS ────────────────────────────────
17451848
#
17461849
# `rules-spirv` is the only member whose payload this ecosystem publishes for

‎README.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ module name the member declares, and configures it there.
99

1010
```toml
1111
[build-dependencies.mcpp]
12-
plugins = { version = "0.19.0", features = ["rules-spirv"], host-module = true }
12+
plugins = { version = "0.19.1", features = ["rules-spirv"], host-module = true }
1313
```
1414

1515
```cpp
@@ -120,7 +120,11 @@ docs/ one page per member or group of members
120120

121121
A member is one module file, one feature in `mcpp.toml`, a consumer under
122122
`tests/` that CI builds and asserts on, a row in the table above with its mcpp
123-
floor, and a version bump. The conventions a member follows -- the same for a
123+
floor, and a version bump. Its code is the C++23 that GCC with libstdc++, clang
124+
with libc++ and an editor's clangd with libstdc++ all accept, and CI builds or
125+
reads every fixture under each of the three
126+
([docs/plugin-development.md](docs/plugin-development.md#4-the-c-a-plugin-is-written-in)).
127+
The conventions a member follows -- the same for a
124128
third-party or a project's own plugin -- are in
125129
[docs/plugin-development.md](docs/plugin-development.md); the division between
126130
what the engine owns and what the member owns is in

‎docs/plugin-development.md‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ mcpp-index refuses such a package.
5252

5353
```toml
5454
[build-dependencies.mcpp]
55-
plugins = { version = "0.19.0", features = ["plugins-core"], host-module = true, reexport = true }
55+
plugins = { version = "0.19.1", features = ["plugins-core"], host-module = true, reexport = true }
5656
```
5757

5858
`reexport = true` is needed when the plugin's consumers import L2 modules in
@@ -124,7 +124,26 @@ what it keeps, since when, its retirement date (six months later), its
124124
replacement and the note it prints once per build.
125125
`.github/scripts/check-compat-retirement.sh` fails once a date has passed.
126126
127-
## 4. A new plugin
127+
## 4. The C++ a plugin is written in
128+
129+
A plugin is C++23 that three combinations of compiler and standard library accept, because each of them compiles it for some user:
130+
131+
| combination | who compiles the plugin with it | CI |
132+
|---|---|---|
133+
| GCC 16 with libstdc++ | `mcpp build` on Linux by default | `consumers` |
134+
| clang (LLVM 22) with libc++ | `mcpp build --toolchain llvm@22.1.8` on Linux, and every build on macOS | `toolchain-llvm`, `rules-cross-platform` |
135+
| clang with libstdc++'s `std` module | an editor's clangd (mcppls) reading a project built with GCC | `toolchain-editor` |
136+
137+
MSVC is a fourth for the members that claim Windows, checked by `rules-cross-platform` and `deps-managed-msvc`. The third combination is never used by a build, so only the editor lane sees what it rejects; it runs `mcppls diagnostics build.mcpp` on every fixture and fails on any error.
138+
139+
Known differences, each one met in this package:
140+
141+
- **A type that holds a container of itself does not use `std::pair` or `std::tuple` for the element.** Under clang with libstdc++'s `std` module, `std::vector<std::pair<std::string, value>>` inside `value` instantiates the pair while `value` is incomplete and fails with "incomplete type used in type trait expression"; GCC and clang with libc++ accept it. The element is a plain struct, and a member function that walks the container is defined after both types are complete (`mcpp::plugins::json::member`, 0.19.1).
142+
- **No compiler's leniency is relied on.** An extension, a missing `typename`, or a template accepted only because one compiler instantiates it later is a failure in another lane.
143+
- **A build program does not use `std::println`.** On macOS 14 the build program links the system libc++, which lacks a symbol it needs; `std::format` into a string and `std::cout` work everywhere.
144+
- **A module does not instantiate `std::filesystem::path`'s iterator.** Under MSVC 14.52 every importer that then touches `std::filesystem` fails inside the STL; `mcpp::plugins::names::components` splits a path as a string instead.
145+
146+
## 5. A new plugin
128147
129148
A plugin in this package consists of:
130149
- a module file under `rules/`, `tools/`, `dist/` or `deps/`;

‎mcpp.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "plugins"
33
namespace = "mcpp"
4-
version = "0.19.0"
4+
version = "0.19.1"
55
# The first mcpp release that states where each tool and payload comes from
66
# (protocol 15, mcpp#755): `xpkg_source`, `xpkg_program`, `xpkg_request`,
77
# `mcpp::decision`, `mcpp::toolchain`, `mcpp::phase`, and the

0 commit comments

Comments
 (0)