Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ on:
env:
# The mcpp release the consumers build with. Raising it is what admits a
# member that relies on a newer engine; the README states each member's floor.
MCPP_VERSION: 2026.9.6.1
MCPP_VERSION: 2026.9.6.6
# PINNED, AND WITHOUT IT THE CACHE BELOW CACHED NOTHING.
#
# A released mcpp is self-contained: with no `MCPP_HOME`, `mcpp self env`
# reports the extracted tarball's own directory as its home, so every install
# landed in `$GITHUB_WORKSPACE/mcpp-<ver>-linux-x86_64/` while the cache
# saved and restored `~/.mcpp` -- a directory mcpp never touched. Every run
# was therefore the cold case, which is what the 90-minute budget below was
# sized for. mcpp's own ci-linux.yml pins it for this reason and says so.
MCPP_HOME: /home/runner/.mcpp

jobs:
consumers:
Expand Down Expand Up @@ -39,8 +48,18 @@ jobs:
tar -xzf mcpp.tar.gz
MCPP="$PWD/mcpp-${MCPP_VERSION}-linux-x86_64/bin/mcpp"
"$MCPP" --version
# THE BUNDLED xlings, NAMED EXPLICITLY BECAUSE `MCPP_HOME` IS PINNED.
#
# A released tarball carries its own xlings at `registry/bin/xlings`
# and finds it there when its home IS that tarball. Pinning the home
# elsewhere moves the place mcpp looks, so the first command fails
# with `xlings binary not found` -- measured. The override is needed
# only for that first call: mcpp copies the binary into the pinned
# home and answers from there afterwards.
export MCPP_VENDORED_XLINGS="$PWD/mcpp-${MCPP_VERSION}-linux-x86_64/registry/bin/xlings"
"$MCPP" self config --mirror GLOBAL
echo "MCPP=$MCPP" >> "$GITHUB_ENV"
echo "MCPP_VENDORED_XLINGS=$MCPP_VENDORED_XLINGS" >> "$GITHUB_ENV"

# The rule's output is a header holding the SPIR-V module; the program
# checks the magic number in its first word, so no Vulkan runtime is
Expand Down Expand Up @@ -147,6 +166,72 @@ jobs:
grep -q '^magic=(no shader in this build)' run.log
grep -q '^12 24 36 48' run.log

# THE PAYLOADS CAME FROM THE RULE, NOT FROM THE FIXTURE.
#
# Every fixture above builds, and that alone does not distinguish "the
# rule declared its toolkit" from "the fixture still declares it": a
# re-added `[xlings.workspace]` block would keep all of them green while
# the claim this package makes stopped being true.
#
# The object is mcpp's own report of WHERE the entries came from. It
# names the pass -- the graph's, not the root's -- so it separates the
# two cases directly rather than by reading the manifests back.
- name: the rules declare their own payloads
run: |
set -e
# THE PROVISIONING LINE ONLY PRINTS ON A STAMP MISS, and the earlier
# steps of this job have already written those stamps. Without
# clearing them, the criterion's "no" and "this ran after the
# fixtures" are the same reading. Removing them costs nothing: the
# packages are still installed, so the re-provision is one xlings
# round trip.
#
# THE PATH IS ASKED FOR, NOT ASSUMED. A released mcpp is
# self-contained and its home is the extracted tarball unless
# MCPP_HOME says otherwise -- so `rm -rf ~/.mcpp/provisioned` removed
# nothing and this step failed on a working build. Measured: a local
# rehearsal passed because a DEV build is disqualified from
# self-contained mode and does use ~/.mcpp, so the rehearsal and CI
# were clearing different directories.
HOME_DIR=$("$MCPP" self env | awk -F'= *' '/^MCPP_HOME/{print $2; exit}')
[ -n "$HOME_DIR" ] || { echo "FAIL: could not read MCPP_HOME"; exit 1; }
echo "clearing provisioning stamps under $HOME_DIR"
rm -rf "$HOME_DIR/provisioned"
for d in tests/cuda-consumer tests/hip-consumer \
tests/sycl-consumer tests/spirv-consumer; do
# No project-side declaration is the first half of the claim, and
# it is a property of a file this repository owns.
if grep -qE '^\[(target\..*\.)?xlings\.workspace\]' "$d/mcpp.toml"; then
echo "FAIL: $d declares payloads itself, so this step proves nothing"
exit 1
fi
( cd "$d" && rm -rf target && "$MCPP" build > prov.log 2>&1 ) || {
echo "build failed in $d"; tail -20 "$d/prov.log"; exit 1; }
grep -q 'entries declared by dependencies' "$d/prov.log" || {
echo "FAIL: $d installed no payload the rule declared"
grep -i 'provisioning' "$d/prov.log" || echo "(no provisioning line at all)"
exit 1
}
echo "ok: $d — $(grep -m1 'entries declared by dependencies' "$d/prov.log")"
rm -f "$d/prov.log"
done
# And the one fixture that DOES override, so the override path is on
# this graph too: it declares one entry, and one version is installed.
d=tests/multi-rule-consumer
grep -q 'xlings.workspace' "$d/mcpp.toml" || {
echo "FAIL: $d no longer demonstrates the override"; exit 1; }
( cd "$d" && rm -rf target && "$MCPP" build > prov.log 2>&1 ) || {
echo "build failed in $d"; tail -20 "$d/prov.log"; exit 1; }
XPKGS="$HOME_DIR/registry/data/xpkgs"
n=$(ls -1 "$XPKGS/xim-x-glslang" | wc -l | tr -d ' ')
[ "$n" = "1" ] || {
echo "FAIL: the override installed $n versions of glslang:"
ls -1 "$XPKGS/xim-x-glslang"
exit 1
}
echo "ok: $d — one glslang, $(ls -1 "$XPKGS/xim-x-glslang")"
rm -f "$d/prov.log"

# THE HOST-LEAK ASSERTION, and what it measures was corrected once.
#
# Its first form grepped a verbose build for `/usr`. That reported
Expand Down
78 changes: 65 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,59 @@ engine's own module family and is not used here.

| feature | module | since mcpp | what it needs |
|---|---|---|---|
| `rules-ascendc` | `mcpp.rules.ascendc` | 2026.9.6.5 | `xim:cann-toolkit` in `[xlings.workspace]`, `[build] accel = "ascend8.5+{dav-c220}"`, a constrained glob for `*.asc`. Compiles with BiSheng in MIXED mode, so the object carries the device binary and a host-callable launcher and joins the ordinary link -- no registration file and no device-link step. The floor is the release whose device-source table carries `.asc` and whose `mcpp::link_flag` can emit the `-rpath-link` the toolkit's own shared libraries need |
| `rules-cuda` | `mcpp.rules.cuda` | 2026.9.5.2 | the toolkit named in `[xlings.workspace]` (`xim:cuda-nvcc`, `xim:cuda-cudart`, and `xim:libcurand` for the clang route, whose wrapper includes a cuRAND header unconditionally), `[build] accel = "cuda…"`, a constrained glob for `*.cu`; the clang route with an LLVM toolchain, the nvcc route with a GCC one |
| `rules-hip` | `mcpp.rules.hip` | 2026.9.5.2 | `xim:hip-nvidia` plus the CUDA back end it compiles through (`xim:cuda-nvcc`, `xim:cuda-cudart`, `xim:libcurand`, `xim:cuda-cccl`), `[build] accel = "hip, cuda12.9+{sm_89}"`, a constrained glob for `*.hip`. On the NVIDIA platform HIP is a header layer over the CUDA runtime, so the compiler is the project's own clang and there is no ROCm on the machine |
| `rules-spirv` | `mcpp.rules.spirv` | 2026.9.5.3 | `xim:glslang` or `xim:shaderc` in `[xlings.workspace]`, `[build] accel = "vulkan1.2"`, a constrained glob for the shader stages; emits one header per shader through a `role = "source"` action, and states which of the two compilers produced it |
| `rules-sycl` | `mcpp.rules.sycl` | 2026.9.6.1 | `xim:dpcpp` (the compiler), `xim:gcc` (the C++ standard library the unit compiles against, not a second toolchain) and `xim:cuda-nvcc` for an NVIDIA target; `[build] accel = "sycl"` or `"sycl, cuda12.9+{sm_89}"`, a constrained glob for `*.sycl`, and `compat:sycl-runtime` so the artifact can reach `libsycl.so.9` at run time. The floor is the release whose device-source table carries `.sycl` |
| `rules-ascendc` | `mcpp.rules.ascendc` | 2026.9.6.6 | `[build] accel = "ascend8.5+{dav-c220}"`, a constrained glob for `*.asc`. Compiles with BiSheng in MIXED mode, so the object carries the device binary and a host-callable launcher and joins the ordinary link -- no registration file and no device-link step. Its own engine needs are `.asc` in the device-source table and `mcpp::link_flag` for the `-rpath-link` the toolkit's shared libraries require, both 2026.9.6.5 |
| `rules-cuda` | `mcpp.rules.cuda` | 2026.9.6.6 | `[build] accel = "cuda…"`, a constrained glob for `*.cu`; the clang route with an LLVM toolchain, the nvcc route with a GCC one |
| `rules-hip` | `mcpp.rules.hip` | 2026.9.6.6 | `[build] accel = "hip, cuda12.9+{sm_89}"`, a constrained glob for `*.hip`. On the NVIDIA platform HIP is a header layer over the CUDA runtime, so the compiler is the project's own clang and there is no ROCm on the machine |
| `rules-spirv` | `mcpp.rules.spirv` | 2026.9.6.6 | `[build] accel = "vulkan1.2"`, a constrained glob for the shader stages; emits one header per shader through a `role = "source"` action, and states which of the two compilers produced it |
| `rules-sycl` | `mcpp.rules.sycl` | 2026.9.6.6 | `[build] accel = "sycl"` or `"sycl, cuda12.9+{sm_89}"`, a constrained glob for `*.sycl`, and `compat:sycl-runtime` so the artifact can reach `libsycl.so.9` at run time. Its own engine need is `.sycl` in the device-source table, 2026.9.6.1 |
| `tools-embed` | `mcpp.tools.embed` | 2026.9.5.4 | nothing beyond mcpp: it reads a file and writes a header while the build program runs. The floor is the release whose fast path compares a declared file input, without which an edit to the data does not reach the binary |

### Each rule brings its own environment

A project names the rule and nothing else:

```toml
[build-dependencies.mcpp]
plugins = { version = "0.2.4", features = ["rules-cuda"], host-module = true }
```

The payloads each rule drives are declared **here**, under the feature that
selects the rule and the accelerator it serves:

```toml
[target.'cfg(accelerator = "cuda")'.feature-xlings.rules-cuda]
"xim:cuda-nvcc" = "12.9.86"
"xim:cuda-cudart" = "12.9.79"
```

Two gates, and both must open before a byte is downloaded. The feature says
whether the rule is wanted; the selector says whether this build compiles for
the device. A CPU-only build opens neither.

**The shape of each default is a judgement about coupling.** An exact version
where the payload is coupled to something the rule cannot see -- a CUDA runtime
must not be newer than the driver it will meet, so the 12.9 line is offered and
a project with newer machines names 13.x itself. A floor (`>=`) where no such
coupling exists: a shader compiler, a SYCL compiler, a CANN toolkit.

mcpp reads the difference. A bare version is a **choice**, so a project pinning
a different one wins and the override is reported; a `>=` is a **requirement**,
so a project pinning below it is refused naming both sides. Either way one
version is installed. To override:

```toml
[target.'cfg(accelerator = "cuda")'.xlings.workspace]
"xim:cuda-nvcc" = "13.3.33"
```

**What is not here:** anything the produced program chooses to run *on*. A
Vulkan ICD (`xim:mesa-lavapipe`) is a device, and a rule that declared one would
force a software renderer onto consumers that have a GPU. The runtime adapters
(`compat:cuda-runtime`, `compat:sycl-runtime`, `compat:vulkan-runtime`) stay in
the project for that reason and for a second one: this package is reached
through a `[build-dependencies]` edge, so its own `[dependencies]` deliberately
do not reach the consumer's target.

### Each rule takes the extensions it claims

`mcpp::device_sources()` is the package's WHOLE device set, not one rule's
Expand All @@ -77,14 +123,20 @@ A device source that NO rule claims is not silently dropped: mcpp refuses a
device source that reached no action, naming the file. That is the engine's
half of this rule and it needs 2026.9.6.5.

The floor is the mcpp release whose engine carries what the member relies on:
`rules-spirv` needs the device-source table that classifies shader extensions,
which 2026.9.5.3 introduced; `tools-embed` needs the fast path to compare a
declared file input, which 2026.9.5.4 introduced; and `rules-sycl` needs `.sycl`
in that same device-source table, which 2026.9.6.1 introduced. The index
descriptor states the highest floor among the members, so it is the floor of the
collection rather than of any one feature; a project on an older mcpp is refused
at resolution rather than at the first shader.
The floor is the mcpp release whose engine carries what the member relies on.
From 0.2.4 every rule shares one: **2026.9.6.6**, the release in which a payload
a DEPENDENCY declared is both installed and answerable. Before it a rule could
declare `>=8.5.0`, have it installed, and still be told by `xpkg_dir` that
nothing was there -- which is why each rule's list used to be repeated in every
project that used it. The earlier per-member floors are still the floors of the
rules themselves (`rules-spirv` needs the shader extensions in the device-source
table, 2026.9.5.3; `tools-embed` needs the fast path to compare a declared file
input, 2026.9.5.4; `rules-sycl` needs `.sycl` in that table, 2026.9.6.1), and
they are all below the shared one.

The index descriptor states the highest floor among the members, so it is the
floor of the collection rather than of any one feature; a project on an older
mcpp is refused at resolution rather than at the first shader.

## How the engine sees this package

Expand Down
102 changes: 101 additions & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "plugins"
namespace = "mcpp"
version = "0.2.3"
version = "0.2.4"
description = "Official mcpp build plugins: rule packages under mcpp.rules.*, build-time utilities under mcpp.tools.*, each member selected by a feature"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand All @@ -28,5 +28,105 @@ rules-spirv = { sources = ["rules/spirv.cppm"] }
rules-sycl = { sources = ["rules/sycl.cppm"] }
tools-embed = { sources = ["tools/embed.cppm"] }

# ── The environment each rule needs (mcpp 2026.9.6.6+) ──────────────────────
#
# A RULE OWNS THE LIST OF PACKAGES IT DRIVES. It is the code that runs the
# compiler, reads the headers and puts the library directory on the link line,
# so "which package, and which version of it" is its knowledge. Before this, a
# project repeated the list in its own `[xlings.workspace]` -- a copy that goes
# stale silently, because the rule moves and the projects do not.
#
# TWO GATES, AND BOTH MUST OPEN BEFORE A BYTE IS DOWNLOADED. The feature says
# whether this rule is wanted at all; `cfg(accelerator = ...)` says whether this
# particular build compiles for the device. A CPU-only build of a project that
# depends on this package with `features = ["rules-cuda"]` opens the first and
# not the second, and installs nothing -- which is the build CI runs, and the
# one a multi-gigabyte vendor toolkit would otherwise make the most expensive.
#
# THE SHAPE OF EACH DEFAULT IS A JUDGEMENT ABOUT COUPLING, not a style.
#
# - An EXACT version where the payload's version is coupled to something the
# rule cannot see. A CUDA runtime must not be newer than the driver it will
# meet: the 12.9 line reaches every driver from r525 onward, and the 13.x
# line raises that to r580. That is a property of the consumer's machines,
# so the rule offers a line it has verified and the project moves it.
# - A FLOOR (`>=`) where no such coupling exists and newer is simply newer.
#
# mcpp reads the difference: a bare version is a CHOICE, so a project pinning a
# different one wins and is reported; a `>=` is a REQUIREMENT, so a project
# pinning below it is refused naming both sides. See "One package, one version"
# in mcpp's docs/05.
#
# WHAT IS NOT HERE: anything the produced PROGRAM chooses to run on. A Vulkan
# ICD (`xim:mesa-lavapipe`) is a device, not a build tool, and a rule that
# declared one would force a software renderer onto consumers that have a GPU.
# The rule declares what it needs to COMPILE; the project declares what it needs
# to RUN. The runtime adapters (`compat:cuda-runtime`, `compat:sycl-runtime`,
# `compat:vulkan-runtime`) stay in the project for the same reason and for a
# second one: this package is a `[build-dependencies]` edge, so its own
# `[dependencies]` deliberately do not reach the consumer's target.

[target.'cfg(accelerator = "cuda")'.feature-xlings.rules-cuda]
"xim:cuda-nvcc" = "12.9.86"
"xim:cuda-cudart" = "12.9.79"
# clang's CUDA wrapper includes a cuRAND header for every device unit, and that
# header includes <nv/target> from CCCL. Neither is called by an ordinary
# kernel; on a developer machine the host's /usr/include supplied them silently.
"xim:libcurand" = "10.3.10.19"
"xim:cuda-cccl" = "12.9.27"

# The driver sentinel is Linux-only, and the OS goes in the PREDICATE rather
# than in the value: a value carrying platform keys under an already-predicated
# table would state the condition twice and let the two disagree.
[target.'cfg(all(accelerator = "cuda", linux))'.feature-xlings.rules-cuda]
"xim:libcuda-host-link" = "0.0.1"

# HIP on the NVIDIA platform is a header layer over the CUDA runtime, so this
# list is the CUDA one plus the header package HIP includes at its second line.
[target.'cfg(accelerator = "hip")'.feature-xlings.rules-hip]
"xim:hip-nvidia" = "7.2.4"
"xim:cuda-nvcc" = "12.9.86"
"xim:cuda-cudart" = "12.9.79"
"xim:libcurand" = "10.3.10.19"
"xim:cuda-cccl" = "12.9.27"
"xim:cuda-profiler-api" = "12.9.79"

[target.'cfg(all(accelerator = "hip", linux))'.feature-xlings.rules-hip]
"xim:libcuda-host-link" = "0.0.1"

# A floor: the compiler's version is not coupled to any driver, and the rule
# needs `-fsycl-link` to behave as it does from 7.1.0 onward.
[target.'cfg(accelerator = "sycl")'.feature-xlings.rules-sycl]
"xim:dpcpp" = ">=7.1.0"

# THE SECOND COMPILER MUST BE TOLD WHERE THE C LIBRARY IS. dpcpp's clang is not
# configured with this ecosystem's glibc, so without these its device units
# compile against the host's -- which no command line shows, because an implicit
# include search never appears on one.
[target.'cfg(all(accelerator = "sycl", linux))'.feature-xlings.rules-sycl]
"xim:gcc" = "15.1.0"
"xim:glibc" = ""
"xim:linux-headers" = ""

# Only when the SYCL build actually names an NVIDIA target. `accelerator` is a
# SET, so this is a conjunction of two membership tests rather than a
# contradiction -- and without it every SYCL build would download a CUDA
# toolkit it may have no target for.
[target.'cfg(all(accelerator = "sycl", accelerator = "cuda"))'.feature-xlings.rules-sycl]
"xim:cuda-nvcc" = "12.9.86"

# glslang alone: it is the rule's first choice, and `xim:shaderc` is the
# fallback a project names when it wants glslc instead. Declaring both would
# install both and use one.
[target.'cfg(accelerator = "vulkan")'.feature-xlings.rules-spirv]
"xim:glslang" = ">=15.1.0"

# 8.5.0 is a real floor rather than a preference: the mixed-mode object -- the
# one carrying both the device binary and a host-callable launcher, which is
# what lets it join an ordinary link -- and the 38 SoC simulators are what this
# rule drives, and they are present from that version onward.
[target.'cfg(accelerator = "ascend")'.feature-xlings.rules-ascendc]
"xim:cann-toolkit" = ">=8.5.0"

[targets.plugins]
kind = "lib"
Loading
Loading