diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5d1b4a..6356622 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,23 @@ env: # it is the release that honours the `provision = "on-request"` entries this # collection now declares, so a consumer that names its own tool downloads # nothing. An older engine refuses the manifest at the package floor. - MCPP_VERSION: 2026.10.1.3 + # + # 2026.10.26.1 IS WHAT 0.20.0'S SYCL LANE NEEDS (mcpp#841): a graph-supplied + # libstdc++ under clang (`gcc.libstdcxx`) and `MCPP_CXX_ABI_ROOT`. The package + # floor stays 2026.10.1.3: the rule reads the variable from the environment, + # so a gcc SYCL build still works on the older engine. + # + # 2026.10.26.2 writes a WebAssembly link in place again: 2026.10.26.1 linked + # every POSIX output through `$out.tmp`, Emscripten named its `.wasm` after + # that, and `dist-web` found no stem family (mcpp#844, found by this CI). + # + # 2026.10.27.1 IS WHAT 0.20.0's `plugins-platform` NEEDS, and the package + # floor states it (mcpp#847, SPEC-013, SPEC-014): protocol 17 -- the + # platform's directives `mcpp.plugins.platform` states, and the `platform` + # phase -- and `mcpp-package.json` beside every staged tree, which + # `mcpp::plugins::package` reads. Until that release exists the jobs build it + # from `MCPP_SOURCE_REF` (the repository variable names the branch). + MCPP_VERSION: 2026.10.27.1 # THE RELEASE EVERY JOB FETCHES, WHICH IS NOT ALWAYS THE ONE UNDER TEST. # # `MCPP_VERSION` states the engine this collection needs -- the package floor @@ -78,26 +94,30 @@ env: # # It moves to `MCPP_VERSION` once that release exists, so the ordinary path # runs the engine this collection states. - MCPP_BOOTSTRAP: 2026.10.1.3 + MCPP_BOOTSTRAP: 2026.10.26.2 # AN ENGINE BUILT FROM SOURCE, WHEN A DISPATCH NAMES ONE. # - # Empty on every push and pull request, so the steps run the release above. - # A dispatch that names a branch or tag of mcpp-community/mcpp through the - # `mcpp_source_ref` input has `.github/scripts/mcpp-under-review.sh` build + # Empty on every push and pull request, so the steps run the release above, + # unless the repository variable `MCPP_SOURCE_REF` names the engine a pending + # release of this collection is written against (it is removed once that + # engine is released). A dispatch that names a branch or tag of + # mcpp-community/mcpp through the `mcpp_source_ref` input, or the variable, + # has `.github/scripts/mcpp-under-review.sh` build # that reference with the release above and point `$MCPP` at the result, in # each job that fetches mcpp, and the step after it checks that the steps run # what it built. This is how a change to the engine is measured against this # collection before either is released. - MCPP_SOURCE_REF: ${{ inputs.mcpp_source_ref || '' }} + MCPP_SOURCE_REF: ${{ inputs.mcpp_source_ref || vars.MCPP_SOURCE_REF || '' }} # THE EDITOR LANE'S TWO PROGRAMS (0.19.1), PINNED. mcppls reads a fixture's # `build.mcpp` as an editor does, with its own clangd over the GCC # toolchain's libstdc++ -- the one combination no build compiles. The mcpp it # runs is the first release that describes build programs in the build # database (2026.10.11.1); under an older one mcppls reads no build program # and says so, which the lane counts as a failure. Raising either is a - # one-line change here. + # one-line change here. The mcpp is the bootstrap release, which also builds + # the engine under review when `MCPP_SOURCE_REF` names one. MCPPLS_VERSION: 0.0.15 - MCPP_EDITOR_VERSION: 2026.10.11.1 + MCPP_EDITOR_VERSION: 2026.10.26.2 # PINNED, AND WITHOUT IT THE CACHE BELOW CACHED NOTHING. # # A released mcpp is self-contained: with no `MCPP_HOME`, `mcpp self env` @@ -1828,8 +1848,23 @@ jobs: "$MCPP_EDITOR" --version "$MCPP_EDITOR" self config --mirror GLOBAL echo "MCPP_EDITOR=$MCPP_EDITOR" >> "$GITHUB_ENV" + echo "MCPP=$MCPP_EDITOR" >> "$GITHUB_ENV" echo "MCPP_VENDORED_XLINGS=$MCPP_VENDORED_XLINGS" >> "$GITHUB_ENV" + # THE EDITOR READS WITH THE ENGINE UNDER REVIEW WHEN ONE IS NAMED: a + # fixture whose package floor is that engine is refused by the release. + - name: the mcpp under review, built from MCPP_SOURCE_REF when it is set + timeout-minutes: 45 + run: bash .github/scripts/mcpp-under-review.sh + + - name: the editor runs the mcpp under review + if: env.MCPP_SOURCE_REF != '' + timeout-minutes: 5 + run: | + set -eu + echo "MCPP_EDITOR=$MCPP_UNDER_REVIEW" >> "$GITHUB_ENV" + echo "ok: the editor runs $MCPP_UNDER_REVIEW_VERSION, built from $MCPP_SOURCE_REF" + - name: Fetch the mcppls release payload, checked against its SHA256SUMS run: | base="https://github.com/Sunrisepeak/mcpp-language-server/releases/download/v${MCPPLS_VERSION}" diff --git a/README.md b/README.md index 6f79945..cfdd8f0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ module name the member declares, and configures it there. ```toml [build-dependencies.mcpp] -plugins = { version = "0.19.1", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.20.0", features = ["rules-spirv"], host-module = true } ``` ```cpp @@ -51,9 +51,11 @@ naming it to avoid a download, and the scenarios on both sides -- is in ## Members -From 0.19.0 the package as a whole needs mcpp 2026.10.1.3 (`[package] mcpp`), -whose protocol 15 states where each tool and payload comes from; the column -states the release each member's behaviour first needed. +From 0.20.0 the package as a whole needs mcpp 2026.10.27.1 (`[package] mcpp`), +whose protocol 17 states a platform's fields and whose `mcpp pack` writes +`mcpp-package.json` beside every staged tree (0.19.0 needed 2026.10.1.3, whose +protocol 15 states where each tool and payload comes from); the column states +the release each member's behaviour first needed. | feature | module | mcpp floor | what it does | doc | |---|---|---|---|---| @@ -91,7 +93,12 @@ dependency edge. `plugins-testing` adds `mcpp.plugins.testing`, which runs a plugin function against a stated build context and compares what it declared. `plugins-toolchain` adds `mcpp.plugins.toolchain`, the builders a root build program states the build toolchain with (0.19.0). -All three need mcpp 2026.10.1.3. `surface`, the name of `plugins-core` before +`plugins-platform` adds `mcpp.plugins.platform`, what a platform's plugin and a +package format's provider state -- the link, the artifacts' names, the seal, +where the runner runs, and `provide_format` -- and `mcpp.plugins` reads a staged +package's `mcpp-package.json` (`mcpp::plugins::package`), 0.20.0, mcpp +2026.10.27.1 ([plugin-development](docs/plugin-development.md#3-layers-names-and-the-engine-floor)). +The first three need mcpp 2026.10.1.3. `surface`, the name of `plugins-core` before 0.17.0, is kept until 2027-03-28. A feature states a mechanism and the tools that mechanism runs; the libraries and SDKs a program links are the project's declaration (0.15.0 removed `rules-qt-xim*`; see [rules-qt](docs/rules-qt.md)). @@ -101,11 +108,13 @@ declaration (0.15.0 removed `rules-qt-xim*`; see [rules-qt](docs/rules-qt.md)). ``` mcpp.toml the package: one feature per member src/plugins.cppm export module mcpp.plugins; the version, mcpp::plugins::surface - (the declarations a consumer names) and mcpp::plugins::xml + (the declarations a consumer names), mcpp::plugins::xml and + mcpp::plugins::package (a staged package's metadata) src/declare.cppm, the general library for build programs (`plugins-core`): src/toolset.cppm, mcpp.plugins.declare, mcpp.plugins.toolset, mcpp.plugins.fs src/fs.cppm src/testing.cppm mcpp.plugins.testing (`plugins-testing`) +src/platform.cppm mcpp.plugins.platform (`plugins-platform`) src/compat/, deps/compat/ compatibility units, each retired on the date its header states rules/.cppm export module mcpp.rules.; diff --git a/docs/plugin-development.md b/docs/plugin-development.md index 4e0c0f7..02a964b 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -34,7 +34,7 @@ mcpp is a general build engine with a framework for build plugins. Plugins come | layer | modules | provided by | |---|---|---| | L1 `mcpp.core` | `mcpp.core`, spelled `mcpp` as well (the two are permanently equivalent) | the engine | -| L2 general library | `mcpp.plugins.declare`, `mcpp.plugins.toolset`, `mcpp.plugins.fs`, `mcpp.plugins.tool`; `mcpp.plugins.testing`, `mcpp.plugins.toolchain` | this package, feature `plugins-core` (`plugins-testing` for the kit, `plugins-toolchain` for the toolchain builders) | +| L2 general library | `mcpp.plugins.declare`, `mcpp.plugins.toolset`, `mcpp.plugins.fs`, `mcpp.plugins.tool`; `mcpp.plugins.testing`, `mcpp.plugins.toolchain`, `mcpp.plugins.platform` | this package, feature `plugins-core` (`plugins-testing` for the kit, `plugins-toolchain` for the toolchain builders, `plugins-platform` for a platform's plugin and a package format's provider) | | L3 plugins | `mcpp.deps.*`, `mcpp.rules.*`, `mcpp.dist.*`, `mcpp.tools.*` here; `mcpp..*` elsewhere | this package, by feature; any package | A layer depends only on the layers below it. L2 knows no foreign tool: it turns @@ -52,7 +52,7 @@ mcpp-index refuses such a package. ```toml [build-dependencies.mcpp] -plugins = { version = "0.19.1", features = ["plugins-core"], host-module = true, reexport = true } +plugins = { version = "0.20.0", features = ["plugins-core"], host-module = true, reexport = true } ``` `reexport = true` is needed when the plugin's consumers import L2 modules in @@ -60,8 +60,10 @@ their own build programs. **The engine floor.** A plugin states the first mcpp release it needs in `[package] mcpp = ">="`; an older engine stops before any other work -and names the upgrade. This package's floor is 2026.10.1.3, the release that -states where each tool and payload comes from (protocol 15). +and names the upgrade. This package's floor is 2026.10.27.1, the release that +speaks protocol 17 (the platform's directives and the `platform` phase, mcpp +SPEC-013) and writes `mcpp-package.json` beside every staged tree (SPEC-014); +it carries protocol 15, which states where each tool and payload comes from. **Where a member's tool comes from.** A member that runs a program resolves it through `mcpp.plugins.tool`, which answers in one order for every member: @@ -110,11 +112,70 @@ toolchain phase, for a project whose `[toolchain]` says `newest_under`, `env`, the `with_launcher` / `with_sysroot` / `with_family` / `with_tool` refinements, and `configure(fn)` / `use(d)`. +**A platform's plugin and a package format's provider.** A platform package +(mcpp SPEC-013) states its targets' configuration in `[platform]` and the part +it computes in a plugin (`[platform] plugin = "build.mcpp"`), which runs in the +`platform` phase. `mcpp.plugins.platform` (feature `plugins-platform`) states +each field of that phase as one checked call: + +| call | directive | accepts | +|---|---|---| +| `link::driver(d)` | `link-driver` | `"lld"` (ld.lld itself links) or `"compiler"` | +| `link::start(symbol)` | `link-start` | a symbol | +| `link::input_file(path)` | `link-input=file:` | a file; a relative path is the platform package's own | +| `link::input_from(as, argv)` | `link-input=from:` | a program's standard output saved as `as` (no `:`), `argv[0]` a tool's name | +| `artifact_names(exe, shared)` | `artifact-name` | `{name}` once, an executable's at its start; an empty pattern leaves the kind unstated | +| `seal(steps)` | `seal` | at least one step, each a non-empty argv; `{}` the artifact, `{needed}` the shared artifacts it needs | +| `run_host(where)` | `run-host` | `"same-arch"` or `"any"` | + +A malformed value is refused at the call: nothing is stated, the call returns a +`statement` whose `reason` names the value and what the call accepts, and the +reason is written to standard error and stated as a warning. Every token is one +line, since a directive is. + +`provide_format(name, make)` provides a package format (SPEC-014 §6) in both of +its halves. The name is declared on every pass (`mcpp::provides_pack_format`), so +that `mcpp pack --pkg-format ` names it among the formats the graph +provides; only in the pass that asks for it is the staged package read and +`make(const package::info&, mcpp::action&)` called, and the action -- role +`artifact`, id and description the format's name unless `make` sets them -- is +submitted when `make` returns true. The engine's own formats (`tar`, `dir`, +`zip`, `single-file`) are refused as names. + +```cpp +import mcpp; +import mcpp.plugins.platform; +namespace pf = mcpp::plugins::platform; + +int main() { + if (!pf::link::driver("lld") || !pf::artifact_names("{name}.img", "lib{name}.so")) return 1; + pf::provide_format("img", [](const mcpp::plugins::package::info& p, mcpp::action& a) { + static std::string out; + out = p.name + "-" + p.version + ".img"; + a.arg("bundle").arg(out.c_str()).arg("${mcpp.stage_dir}").output(out.c_str()); + return true; + }); + return 0; +} +``` + +**The staged package.** `mcpp::plugins::package` (in `mcpp.plugins`, std-only, +so an ordinary program reads it too) reads the `mcpp-package.json` `mcpp pack` +writes at the root of a staged tree (SPEC-014 §4.5): `read(stage_dir)` returns +the package -- `name`, `version`, `platform`, `linkage`, `mode`, `legs`, +`systems`, `runner`, `entry` (each architecture's entry, in the document's +order), `program`, and `files`, each with its `path`, `role`, `leg` and `sha256` +-- or the reason it cannot, a document of another schema than 1 among them. +`files_with_role(p, role)` and `entry_of(p, arch)` select from it. A provider +reads which file is the program, an image or a resource here, rather than +deciding again from the tree. + **Testing a plugin.** `mcpp.plugins.testing` runs a plugin function in a child process against a stated build context (`row::windows_visual_studio()`, `row::windows_managed()`, `row::linux_gcc()`, `row::linux_libcxx()`, or a -context built with `set`, `file`, `xpkg`, `xpkg_source`, `xpkg_program` and -`phase`) and hands the lines it emitted and the +context built with `set`, `file`, `xpkg`, `xpkg_source`, `xpkg_program`, +`phase` and `pack`, the last a packaging pass's format and staged tree) and +hands the lines it emitted and the files it wrote to a check. The test is a build program; a failed case fails the build and prints the report. `tests/plugin-logic` is this package's own use. diff --git a/docs/rules.md b/docs/rules.md index d8b5517..ef10937 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -46,6 +46,8 @@ Module `mcpp.rules.swift`; engine floor: 2026.9.8.1. ## `rules-sycl` -Module `mcpp.rules.sycl`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.6.6 before). +Module `mcpp.rules.sycl`; engine floor: 2026.10.1.3 (0.19.0, mcpp#755; 2026.9.6.6 before); an llvm build needs 2026.10.26.1 (0.20.0). + +**One C++ runtime, libstdc++ (0.20.0).** `libsycl.so` is compiled against libstdc++ and throws `sycl::exception` on it, and a typed `catch` does not match across two C++ runtimes in one process (mcpp#838). The program's own C++ library is therefore libstdc++: a gcc toolchain, or llvm with `gcc.libstdcxx` in the graph (`[target.'cfg(accelerator = "sycl")'.dependencies] gcc.libstdcxx = "16.1.0.1"`). On Linux the rule refuses any other C++ library before a unit is compiled, naming both remedies, and `compat:sycl-runtime` states the same requirement. The rule names no C++ library on the link line (0.19 added `-l:libstdc++.so.6` beside an llvm build's libc++); `--gcc-install-dir` follows the build's GCC: the graph's supplier (`MCPP_CXX_ABI_ROOT`), else the gcc toolchain's payload, else the `xim:gcc` the rule declares. **Needs and behaviour.** `[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 diff --git a/mcpp.toml b/mcpp.toml index 8639f26..c2beb0f 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,12 +1,15 @@ [package] name = "plugins" namespace = "mcpp" -version = "0.19.1" -# The first mcpp release that states where each tool and payload comes from -# (protocol 15, mcpp#755): `xpkg_source`, `xpkg_program`, `xpkg_request`, -# `mcpp::decision`, `mcpp::toolchain`, `mcpp::phase`, and the +version = "0.20.0" +# The first mcpp release that speaks protocol 17 (SPEC-013, mcpp#847): the +# platform's directives `mcpp.plugins.platform` states (`link_driver`, +# `link_input`, `artifact_name`, `seal`, `run_host`), the `platform` phase, and +# `mcpp-package.json` beside every staged tree, which `mcpp::plugins::package` +# reads. It carries protocol 15 (mcpp#755): `xpkg_source`, `xpkg_program`, +# `xpkg_request`, `mcpp::decision`, `mcpp::toolchain`, `mcpp::phase`, and the # `provision = "on-request"` entries below. -mcpp = ">=2026.10.1.3" +mcpp = ">=2026.10.27.1" 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"] @@ -100,6 +103,16 @@ sources = ["src/declare.cppm", "src/toolset.cppm", "src/fs.cppm", "src/tool.cppm sources = ["src/toolchain.cppm"] implies = ["plugins-core"] +# `mcpp.plugins.platform` -- what a platform's plugin (mcpp SPEC-013 §12) and a +# package format's provider (SPEC-014 §6) state: the link's driver and inputs, +# the artifacts' names, the seal, where the runner runs, and `provide_format`, +# which declares a format on every pass and makes its action from the staged +# package's `mcpp-package.json` in the pass that asks for it. Its own feature: +# a project that provides no platform and no format compiles nothing of it. +[features.plugins-platform] +sources = ["src/platform.cppm"] +implies = ["plugins-core"] + # The test kit: a plugin function runs against a stated build context, and its # directives are compared. Separate from `plugins-core`, so that no build # program compiles it unless it asks. diff --git a/rules/sycl.cppm b/rules/sycl.cppm index 38880e3..ff28e9a 100644 --- a/rules/sycl.cppm +++ b/rules/sycl.cppm @@ -40,30 +40,25 @@ // measured: without it, clang finds the host's CUDA // installation and says nothing. // -// WHY THE LINK CARRIES `-l:libstdc++.so.6` AND NOT `-lstdc++` +// ONE C++ RUNTIME, AND IT IS libstdc++ (0.20.0; mcpp 2026.10.26.1+). // -// The SYCL unit and `libsycl.so` are compiled against libstdc++; mcpp's own -// clang toolchain uses libc++. Both may be in one process because the seam is -// `extern "C"` and no C++ type crosses it -- but the libstdc++ half must -// actually be linked. `-lstdc++` does not do that: clang's driver treats it -// as a selector for the C++ standard library and REWRITES it to `-lc++` -// under `-stdlib=libc++`, so the flag disappears from the link line with no -// diagnostic and `std::cerr` comes back undefined from inside a SYCL header. -// `-l:` names a file and is not rewritten. +// `libsycl.so` is compiled against libstdc++ and throws `sycl::exception` on +// it, and the device units catch that type. Two Itanium C++ runtimes in one +// process share the ABI names -- `__cxa_throw`, the personality routine, the +// type information of `std::exception` -- so a typed `catch` across them does +// not match (mcpp-community/mcpp#838: only `catch (...)` caught what the +// runtime threw). Until 0.19 this rule put `-l:libstdc++.so.6` beside the +// libc++ an llvm build links and called the seam safe; it was not. // -// AND THE BUILD SAYS SO, WHICH IS CORRECT AND EXPECTED. -// -// Two C++ runtimes in one image means two sets of unwinder symbols, and mcpp's -// duplicate-symbol check reports it: -// -// warning: 68 symbols in this image are also provided by a library it loads -// _Unwind_DeleteException() ... also provided by libgcc_s.so.1 -// -// That warning is accurate and is the reason the seam discipline is not -// optional here: a SYCL exception must be caught inside the device translation -// unit and turned into a return code, because the runtime that threw it is not -// the one the caller would unwind with. The fixture in tests/sycl-consumer -// does exactly that, and its comment says why. +// So the build's own C++ library is libstdc++ (mcpp SPEC-002 §7): with a gcc +// toolchain, or with llvm and `gcc.libstdcxx` in the graph. The rule reads the +// resolved library (`mcpp::cxx_stdlib()`) and refuses anything else before a +// unit is compiled, naming both remedies; `compat.sycl-runtime` states the same +// requirement, so the engine refuses it at planning as well. The C++ library +// is the engine's to link, so this rule names none, and `--gcc-install-dir` +// points the device compiler at the same GCC the program uses: the graph's +// supplier (`MCPP_CXX_ABI_ROOT`), else the gcc toolchain's own payload, +// else the `xim:gcc` this rule declares. module; #include #include @@ -228,6 +223,25 @@ inline std::string gcc_install_dir(const std::string& gcc_root) { return {}; } +// The GCC whose libstdc++ this build uses: the graph's supplier, whose `cxx` +// links the payload's include/c++/ (so the GCC root is three levels +// above it); else the gcc toolchain's own payload; else the `xim:gcc` this rule +// declares. +inline std::string cxx_gcc_root() { + std::error_code ec; + // Read from the environment rather than through `mcpp::cxx_abi_root()`, + // which an engine before 2026.10.26.1 does not have: with a gcc toolchain + // this rule still compiles and works there. + const char* abiRoot = std::getenv("MCPP_CXX_ABI_ROOT"); + if (const std::string root = abiRoot ? abiRoot : ""; !root.empty()) { + const auto cxx = std::filesystem::canonical(std::filesystem::path(root) / "cxx", ec); + if (!ec) return cxx.parent_path().parent_path().parent_path().string(); + } + if (std::string_view(mcpp::compiler()) == "gcc") + if (const std::string dir = mcpp::toolchain_dir(); !dir.empty()) return dir; + return payload("gcc"); +} + // `dpcpp --version` states the release and the intel/llvm revision it was // built from. Stated as a fact so a build log answers "which SYCL compiler" // without anyone reproducing the build. @@ -352,6 +366,21 @@ inline std::vector plan(std::span sources, options opt const auto dpcppTool = mcpp::plugins::tool::resolve(dpcppSpec, opt.compiler); // Asked for: the engine installs `xim:dpcpp` and runs this program again. if (dpcppTool.pending()) return out; + // One C++ runtime per process (file header). On Windows both halves use + // MSVC's, so there is nothing to choose. + if constexpr (!kWindows) { + if (std::string_view(mcpp::cxx_stdlib()) != "libstdc++") { + std::cerr << std::format("mcpp.rules.sycl: SYCL needs this build's C++ library to be libstdc++, and it is {}.\n" + " libsycl.so throws sycl::exception on libstdc++, and a typed catch does not match\n" + " across two C++ runtimes in one process.\n" + " help: add a libstdc++ supplier beside the llvm toolchain:\n" + " [target.'cfg(accelerator = \"sycl\")'.dependencies]\n" + " gcc.libstdcxx = \"16.1.0.1\"\n" + " help: or build with a gcc toolchain: [toolchain] default = \"gcc@16.1.0\"", + *mcpp::cxx_stdlib() ? mcpp::cxx_stdlib() : "unknown") << '\n'; + return out; + } + } // TWO VALUES, NOT ONE: the compiler to run, and the root its libraries sit // under. They were one string while this rule took the payload directory and // appended `/bin/clang++`; a resolver answers the program, and `-L` still @@ -359,7 +388,7 @@ inline std::vector plan(std::span sources, options opt // `unable to find library -lsycl` when the root was the program's path). const std::string dpcpp = dpcppTool.program; const std::string dpcppRoot = dpcppTool.root; - const std::string gcc = payload("gcc"); + const std::string gcc = cxx_gcc_root(); const std::string cuda = tg.cuda_archs.empty() ? std::string{} : payload("cuda-nvcc"); // THE C LIBRARY IS THE SAME QUESTION AS THE C++ ONE, ONE LAYER DOWN. // @@ -474,10 +503,6 @@ inline std::vector plan(std::span sources, options opt // rule resolved the payload, so the rule names where its libraries are. mcpp::link_search((dpcppRoot + "/lib").c_str()); mcpp::link_lib("sycl"); - // See the file header for why this is not `-lstdc++`. The reason is a - // Linux one: two C++ runtimes cannot share a process, and on Windows there - // is one -- MSVC's, which both this payload and mcpp's own compiler use. - if constexpr (!kWindows) mcpp::link_lib(":libstdc++.so.6"); // SPIR-V IS NOT A DEVICE, AND A BUILD THAT NAMES NO DEVICE SHOULD BE TOLD. // diff --git a/src/platform.cppm b/src/platform.cppm new file mode 100644 index 0000000..cc2aa19 --- /dev/null +++ b/src/platform.cppm @@ -0,0 +1,235 @@ +// mcpp.plugins.platform -- what a platform's plugin and a package format's +// provider state (L2 of the build-plugin architecture; feature +// `plugins-platform`; mcpp SPEC-013 §12, SPEC-014 §6). +// +// A PLATFORM PACKAGE STATES ITS TARGETS' CONFIGURATION IN `[platform]`, and a +// plugin (`[platform] plugin = "build.mcpp"`) states the part it computes: the +// link's driver and inputs, the artifacts' names, the seal steps, where the +// runner runs, and the package formats it provides. The program runs in the +// `platform` phase (`mcpp::phase()`), whose directives are exactly these. The +// engine's interface spells each as a line (`mcpp::link_driver`, +// `mcpp::seal(step, token)`, ...); this module states each as one call with +// the value checked where it is written, so a malformed field is refused in the +// plugin, naming the call, rather than by the engine, naming a line. +// +// import mcpp; +// import mcpp.plugins.platform; +// namespace pf = mcpp::plugins::platform; +// +// int main() { +// pf::link::driver("lld"); +// pf::artifact_names("{name}.img", "lib{name}.so"); +// pf::seal({ { "signer", "{}" } }); +// pf::run_host("same-arch"); +// pf::provide_format("img", [](const mcpp::plugins::package::info& p, mcpp::action& a) { +// a.arg("bundle").arg(p.root.c_str()); // ... the format's command +// return true; +// }); +// return 0; +// } +// +// A PACKAGE FORMAT IS PROVIDED IN TWO HALVES, AND `provide_format` IS BOTH. +// The name is declared on every pass (`mcpp::provides_pack_format`), so that +// `mcpp pack --pkg-format ` names what the graph provides; the action +// is submitted only in the pass that asks for the format, and is made from the +// staged package's `mcpp-package.json` (`mcpp::plugins::package`), which says +// which files are the program, the images and the resources. + +export module mcpp.plugins.platform; + +import std; +import mcpp; +import mcpp.plugins; + +export namespace mcpp::plugins::platform { + +// A builder's answer: stated, or refused with the reason. A refusal is also +// written to standard error and stated as a warning, so a plugin that goes on +// is still reported; one that stops returns non-zero. +struct statement { + bool ok = true; + std::string reason; + explicit operator bool() const { return ok; } +}; + +// What a format's provider makes from the staged package: the action's +// command, inputs and outputs. `false` submits nothing (the provider reports +// why). +using make_action = std::function; + +namespace detail { + +// One line of the warning channel: line breaks folded, as the channel is one +// line per directive. +inline statement refuse(std::string what, std::string reason) { + const std::string message = std::format("mcpp.plugins.platform: {}: {}", what, reason); + std::cerr << message << '\n'; + std::string folded; + for (char c : message) folded += (c == '\n' || c == '\r') ? ' ' : c; + mcpp::warning(folded.c_str()); + return { false, std::move(reason) }; +} + +// A token of a directive is one line, and not empty. +inline bool one_line(std::string_view t) { + return !t.empty() && t.find('\n') == std::string_view::npos && t.find('\r') == std::string_view::npos; +} + +// `{name}` once, and (an executable's) at the start. +inline bool names_once(std::string_view pattern, bool atStart) { + const auto at = pattern.find("{name}"); + if (at == std::string_view::npos || pattern.find("{name}", at + 1) != std::string_view::npos) return false; + return !atStart || at == 0; +} + +// The formats the engine writes itself; a provider cannot claim them. +inline bool engine_format(std::string_view n) { + return n == "tar" || n == "dir" || n == "zip" || n == "single-file"; +} + +// A relative path is the platform package's own. Spelled on strings: a module +// that instantiates `std::filesystem::path`'s iterator (`lexically_normal`) +// breaks every importer under MSVC 14.52 (docs/plugin-development.md §4). +inline std::string absolute_in_package(std::string_view path) { + const bool absolute = path.starts_with('/') || path.starts_with('\\') + || (path.size() > 2 && path[1] == ':' && (path[2] == '/' || path[2] == '\\')); + if (absolute) return std::string(path); + std::string dir = mcpp::manifest_dir(); + while (!dir.empty() && (dir.back() == '/' || dir.back() == '\\')) dir.pop_back(); + std::string rel(path); + while (rel.starts_with("./")) rel.erase(0, 2); + return dir.empty() ? rel : dir + "/" + rel; +} + +} // namespace detail + +namespace link { + +// `ld.lld` itself links ("lld"), or the compiler driver does ("compiler"). +inline statement driver(std::string_view d) { + if (d != "lld" && d != "compiler") + return detail::refuse("link::driver", std::format("'{}' is not a driver; the drivers are " + "\"lld\" and \"compiler\"", d)); + mcpp::link_driver(std::string(d).c_str()); + return {}; +} + +// The symbol every executable is entered at (protocol 16). +inline statement start(std::string_view symbol) { + const bool word = !symbol.empty() + && std::ranges::all_of(symbol, [](char c) { + return std::isalnum(static_cast(c)) || c == '_' || c == '.' || c == '$'; + }) + && !std::isdigit(static_cast(symbol.front())); + if (!word) + return detail::refuse("link::start", std::format("'{}' is not a symbol", symbol)); + mcpp::link_start(std::string(symbol).c_str()); + return {}; +} + +// A file linked into every executable and shared library, assembled first when +// it is a `.S` or `.s`. A relative path is the platform package's own. +inline statement input_file(std::string_view path) { + if (!detail::one_line(path)) + return detail::refuse("link::input_file", "the path is empty or spans lines"); + mcpp::link_input(detail::absolute_in_package(path).c_str()); + return {}; +} + +// The standard output of a program, saved as `as` and linked as `input_file` +// links a file; `argv[0]` names the program (a platform tool by its name). +inline statement input_from(std::string_view as, std::span argv) { + if (!detail::one_line(as) || as.find(':') != std::string_view::npos) + return detail::refuse("link::input_from", std::format( + "'{}' is not a file name to save the output as (one line, no ':')", as)); + if (argv.empty() || !std::ranges::all_of(argv, [](auto const& t) { return detail::one_line(t); })) + return detail::refuse("link::input_from", std::format( + "the program for '{}' is empty, or a word of it is empty or spans lines", as)); + const std::string a(as); + for (auto const& token : argv) mcpp::link_input_from(a.c_str(), token.c_str()); + return {}; +} + +inline statement input_from(std::string_view as, std::initializer_list argv) { + std::vector words(argv.begin(), argv.end()); + return input_from(as, std::span(words)); +} + +} // namespace link + +// How the artifacts are named: an executable's pattern begins with `{name}` +// ("{name}.oke"), a shared library's names it once ("lib{name}.okso"). An +// empty pattern leaves that kind as the target names it. +inline statement artifact_names(std::string_view exe, std::string_view shared = {}) { + if (!exe.empty() && (!detail::one_line(exe) || !detail::names_once(exe, true))) + return detail::refuse("artifact_names", std::format( + "the executable's pattern '{}' names the artifact as {{name}} once, at its start", exe)); + if (!shared.empty() && (!detail::one_line(shared) || !detail::names_once(shared, false))) + return detail::refuse("artifact_names", std::format( + "the shared library's pattern '{}' names the artifact as {{name}} once", shared)); + if (!exe.empty()) mcpp::artifact_name("exe", std::string(exe).c_str()); + if (!shared.empty()) mcpp::artifact_name("shared", std::string(shared).c_str()); + return {}; +} + +// The seal: steps run in order on each linked artifact (`{}`) before it is +// published, `{needed}` naming the shared artifacts it needs. A step that fails +// keeps the artifact from being published. +inline statement seal(std::span> steps) { + if (steps.empty()) return detail::refuse("seal", "no step"); + for (std::size_t i = 0; i < steps.size(); ++i) + if (steps[i].empty() || !std::ranges::all_of(steps[i], [](auto const& t) { return detail::one_line(t); })) + return detail::refuse("seal", std::format( + "step {} is empty, or a word of it is empty or spans lines", i)); + // Steps then words: each word is one directive line of its step. + for (std::size_t i = 0; i < steps.size(); ++i) + for (auto const& token : steps[i]) mcpp::seal(static_cast(i), token.c_str()); + return {}; +} + +inline statement seal(std::initializer_list> steps) { + std::vector> owned; + for (auto const& step : steps) owned.emplace_back(step.begin(), step.end()); + return seal(std::span>(owned)); +} + +// Where the runner runs an artifact: on a machine of its architecture +// ("same-arch"), or from any machine ("any": an emulator, a probe). +inline statement run_host(std::string_view where) { + if (where != "same-arch" && where != "any") + return detail::refuse("run_host", std::format( + "'{}' is neither \"same-arch\" nor \"any\"", where)); + mcpp::run_host(std::string(where).c_str()); + return {}; +} + +// A package format this program provides (SPEC-014 §6): declared on every +// pass, made and submitted only in the pass `mcpp pack --pkg-format ` +// runs, from the staged package. The action's role is `artifact`, its id and +// description the format's name unless `make` sets them. +inline statement provide_format(std::string_view name, const make_action& make) { + const std::string format(name); + if (!detail::one_line(format) || format.find_first_of(" \t:") != std::string::npos) + return detail::refuse("provide_format", std::format("'{}' is not a format's name", format)); + if (detail::engine_format(format)) + return detail::refuse("provide_format", std::format( + "'{}' is a format mcpp writes itself (tar, dir, zip, single-file)", format)); + mcpp::provides_pack_format(format.c_str()); + if (std::string_view(mcpp::pack_format()) != format) return {}; + + const std::string stage = mcpp::pack_stage_dir(); + if (stage.empty()) + return detail::refuse("provide_format", std::format( + "the pass for '{}' names no staged tree (mcpp::pack_stage_dir())", format)); + auto pkg = package::read(stage); + if (!pkg) return detail::refuse("provide_format", pkg.error()); + mcpp::action a; + a.id = format.c_str(); + a.role = mcpp::roles::artifact; + a.description = format.c_str(); + if (!make(*pkg, a)) return { false, std::format("the provider of '{}' made no action", format) }; + a.submit(); + return {}; +} + +} // namespace mcpp::plugins::platform diff --git a/src/plugins.cppm b/src/plugins.cppm index eb91950..8e30a34 100644 --- a/src/plugins.cppm +++ b/src/plugins.cppm @@ -49,7 +49,7 @@ export namespace mcpp::plugins { // // One package, one version: the number lives in mcpp.toml, and the CI step // `the collection states its own version` compares the two. -inline constexpr std::string_view version = "0.19.1"; +inline constexpr std::string_view version = "0.20.0"; } // namespace mcpp::plugins @@ -537,6 +537,118 @@ inline std::string label_of(const package& p) { } // namespace mcpp::plugins::graph +// mcpp::plugins::package -- the metadata of a staged package. +// +// `mcpp pack` (2026.10.27.1+, SPEC-014 §4.5) writes `mcpp-package.json` at the +// root of every staged tree: the package, its three axes, its legs, the systems +// whose loaders it carries, how it is started, and every file with its role, +// its leg and its digest. It is the input of a package format's provider, of +// `mcpp pack --merge` and of `mcpp emit xpkg`. A provider reads it here rather +// than walking the tree and deciding for itself which file is the program: the +// engine has already decided, once, for every format. +// +// SCHEMA 1 AND ONLY SCHEMA 1. A document of another schema is refused rather +// than read as far as it goes: a field whose meaning changed would be read +// with the old one. +export namespace mcpp::plugins::package { + +struct file { + std::string path; // relative to the tree's root, `/`-separated + std::string role; // program | image | library | loader | launcher | resource | metadata + std::string leg; // the leg's triple; empty when every leg shares the file + std::string sha256; +}; + +struct info { + int schema = 0; + std::string name; + std::string version; + std::string platform; // `@`; empty: none + std::string linkage; // static | shared | declared + std::string mode; // system | vendored | self-contained | static + std::vector legs; // canonical triples + std::vector systems; // `-` of the loaders under libexec/ + std::vector runner; // the platform's runner argv; empty: native + // The entry of each architecture (universal packages), in the document's + // order: architecture, then the entry's path in the tree. + std::vector> entry; + std::string program; // the program's path (one native leg) + std::vector files; + std::string root; // the tree the document was read from +}; + +namespace detail { + +inline std::vector strings_of(const json::value* v) { + std::vector out; + if (!v || v->type != json::value::kind::array) return out; + for (auto const& item : v->items) + if (item.type == json::value::kind::string) out.push_back(item.text); + return out; +} + +} // namespace detail + +// The document at `/mcpp-package.json`, or why it cannot be read. +inline std::expected read(std::string_view stage_dir) { + std::string root(stage_dir); + while (root.size() > 1 && (root.back() == '/' || root.back() == '\\')) root.pop_back(); + const std::string path = root + "/mcpp-package.json"; + std::ifstream in(path, std::ios::binary); + if (!in) return std::unexpected(std::format("{} cannot be read: the tree is not a staged package", path)); + const std::string text((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + json::value doc; + if (!json::parse_json(text, doc) || doc.type != json::value::kind::object) + return std::unexpected(std::format("{} is not a JSON object", path)); + info p; + p.root = root; + if (const json::value* s = doc.get("schema"); s && s->type == json::value::kind::number) + p.schema = std::atoi(s->text.c_str()); + if (p.schema != 1) + return std::unexpected(std::format( + "{} states schema {}; this collection reads schema 1 (mcpp SPEC-014 §4.5)", path, p.schema)); + p.name = json::string_of(doc.get("name")); + p.version = json::string_of(doc.get("version")); + p.platform = json::string_of(doc.get("platform")); + p.linkage = json::string_of(doc.get("linkage")); + p.mode = json::string_of(doc.get("mode")); + p.legs = detail::strings_of(doc.get("legs")); + p.systems = detail::strings_of(doc.get("systems")); + if (const json::value* launch = doc.get("launch"); launch && launch->type == json::value::kind::object) { + p.runner = detail::strings_of(launch->get("runner")); + p.program = json::string_of(launch->get("program")); + if (const json::value* e = launch->get("entry"); e && e->type == json::value::kind::object) + for (auto const& m : e->members) + if (m.second.type == json::value::kind::string) p.entry.emplace_back(m.first, m.second.text); + } + const json::value* files = doc.get("files"); + if (!files || files->type != json::value::kind::array) + return std::unexpected(std::format("{} has no files array", path)); + for (auto const& f : files->items) { + if (f.type != json::value::kind::object) continue; + p.files.push_back({ json::string_of(f.get("path")), json::string_of(f.get("role")), + json::string_of(f.get("leg")), json::string_of(f.get("sha256")) }); + } + return p; +} + +// The files of `p` with `role`, in the document's order. +inline std::vector files_with_role(const info& p, std::string_view role) { + std::vector out; + for (auto const& f : p.files) + if (f.role == role) out.push_back(f); + return out; +} + +// The entry of `arch`, or empty. +inline std::string entry_of(const info& p, std::string_view arch) { + for (auto const& [a, path] : p.entry) + if (a == arch) return path; + return {}; +} + +} // namespace mcpp::plugins::package + // mcpp::plugins::names -- the derivations that turn a path into a C++ name. // // THESE ARE SHARED BECAUSE THEY WERE COPIED. `common_base_dir` and diff --git a/src/testing.cppm b/src/testing.cppm index a834fcc..2810f40 100644 --- a/src/testing.cppm +++ b/src/testing.cppm @@ -74,6 +74,7 @@ inline constexpr std::string_view kKeys[] = { "MCPP_PROFILE", "MCPP_ACCEL", "MCPP_OUT_DIR", "MCPP_MANIFEST_DIR", "MCPP_TOOLCHAIN_DIR", "MCPP_TOOLCHAIN_SYSROOT", "MCPP_COMPILER", "MCPP_CXX_STDLIB", "MCPP_TARGET_SYSROOT", "MCPP_PKG_NAME", "MCPP_PKG_NAMESPACE", "MCPP_PKG_VERSION", "MCPP_PACK_FORMAT", + "MCPP_PACK_STAGE_DIR", "MCPP_TOOL_CC", "MCPP_TOOL_CXX", "MCPP_TOOL_LD", "MCPP_TOOL_AR", "MCPP_TOOL_RC", "MCPP_TOOL_AS", "MCPP_TOOL_MT", "MCPP_ABI_TOOL_CC", "MCPP_ABI_TOOL_CXX", "MCPP_ABI_TOOL_LD", "MCPP_ABI_TOOL_AR", @@ -170,8 +171,14 @@ struct context { context& xpkg_program(std::string_view ns, std::string_view name, std::string program) { return set(xpkg_key(ns, name, "PROGRAM"), std::move(program)); } - // The phase a case runs in (0.19.0): "toolchain" for a toolchain phase. + // The phase a case runs in (0.19.0): "toolchain" for a toolchain phase, + // "platform" for a platform's plugin (0.20.0). context& phase(std::string name) { return set("MCPP_PHASE", std::move(name)); } + // A packaging pass (0.20.0): the format `mcpp pack --pkg-format` asked + // for, and the staged tree it hands the provider. + context& pack(std::string format, std::string stage_dir) { + return set("MCPP_PACK_FORMAT", std::move(format)).set("MCPP_PACK_STAGE_DIR", std::move(stage_dir)); + } }; // Contexts that describe the rows a plugin meets. Paths are under `{root}`, diff --git a/tests/plugin-logic/build.mcpp b/tests/plugin-logic/build.mcpp index b66faa3..7ac9292 100644 --- a/tests/plugin-logic/build.mcpp +++ b/tests/plugin-logic/build.mcpp @@ -1,5 +1,6 @@ -// The deps members' decisions, and the files rules-qt reads by default, one -// case per decision (mcpp#734, design §6). +// The deps members' decisions, the files rules-qt reads by default, and what +// a platform's plugin states (mcpp.plugins.platform, the package reader), one +// case per decision (mcpp#734, design §6; mcpp SPEC-013 §12, SPEC-014 §4.5). // Each case states a build context, runs the member, and reads the action it // declared and the files it wrote. Nothing is installed or run. import std; @@ -11,9 +12,12 @@ import mcpp.deps.vcpkg; import mcpp.deps.cmake; import mcpp.rules.qt; import mcpp.plugins.tool; +import mcpp.plugins.platform; +import mcpp.plugins; namespace t = mcpp::plugins::testing; namespace ts = mcpp::plugins::toolset; +namespace pf = mcpp::plugins::platform; namespace { @@ -78,6 +82,46 @@ std::string derived_triplet(const t::result& r) { return {}; } +// A universal package's metadata as `mcpp pack` writes it (SPEC-014 §4.5). +constexpr std::string_view kPackage = R"({ + "schema": 1, + "name": "hello", + "version": "0.1.0", + "platform": "openkal-platform@0.17.2", + "linkage": "declared", + "mode": "vendored", + "legs": ["x86_64-openkal", "aarch64-openkal"], + "systems": [], + "launch": { + "entry": { "aarch64": "lib/aarch64/hello.oke", "x86_64": "lib/x86_64/hello.oke" }, + "runner": ["okrun", "{}"] + }, + "files": [ + { "leg": null, "path": "bin/hello", "role": "launcher", "sha256": "aa" }, + { "leg": "aarch64-openkal", "path": "lib/aarch64/hello.oke", "role": "image", "sha256": "bb" }, + { "leg": "x86_64-openkal", "path": "lib/x86_64/hello.oke", "role": "image", "sha256": "cc" }, + { "leg": null, "path": "share/data.txt", "role": "resource", "sha256": "dd" } + ] +} +)"; + +t::context with_stage(t::context c, std::string_view doc = kPackage) { + return c.file("stage/hello-0.1.0-openkal/mcpp-package.json", std::string(doc)) + .set("MCPP_PACK_STAGE_DIR", "{root}/stage/hello-0.1.0-openkal"); +} + +// The format a case provides: `okb`, its command the stage and the entry. +int provide_okb() { + auto r = pf::provide_format("okb", [](const mcpp::plugins::package::info& p, mcpp::action& a) { + static std::string out; + out = p.name + "-" + p.version + ".okb"; + a.arg("okld").arg("bundle").arg(out.c_str()).arg("--package").arg("${mcpp.stage_dir}") + .output(out.c_str()); + return true; + }); + return r ? 0 : 1; +} + std::string stem(const std::string& path) { return std::filesystem::path(path).stem().string(); } @@ -592,6 +636,135 @@ int main(int argc, char** argv) { "rcc tracks the aliased source after joining and trimming text children"); } }, + // ── the package reader (mcpp::plugins::package) ───────────────────── + { "package: a staged package's metadata is read, every field", + with_stage(t::row::linux_libcxx()), + [] { + auto p = mcpp::plugins::package::read(mcpp::pack_stage_dir()); + if (!p) { std::printf("error=%s\n", p.error().c_str()); return 1; } + std::printf("name=%s version=%s platform=%s legs=%zu runner=%s\n", p->name.c_str(), + p->version.c_str(), p->platform.c_str(), p->legs.size(), p->runner.front().c_str()); + std::printf("images=%zu entry.x86_64=%s first=%s\n", + mcpp::plugins::package::files_with_role(*p, "image").size(), + mcpp::plugins::package::entry_of(*p, "x86_64").c_str(), p->entry.front().first.c_str()); + std::printf("leg.launcher=[%s]\n", p->files.front().leg.c_str()); + return 0; + }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code == 0, "the document is read"); + c.expect(r.has_line("name=hello version=0.1.0 platform=openkal-platform@0.17.2 legs=2 runner=okrun"), + "the package, its platform, its legs and its runner"); + c.expect(r.has_line("images=2 entry.x86_64=lib/x86_64/hello.oke first=aarch64"), + "the images by role, the entry by architecture, in the document's order"); + c.expect(r.has_line("leg.launcher=[]"), "a file every leg shares has no leg"); + } }, + + { "package: a document of another schema is refused, naming it", + with_stage(t::row::linux_libcxx(), R"({"schema": 2, "name": "x", "files": []})"), + [] { + auto p = mcpp::plugins::package::read(mcpp::pack_stage_dir()); + if (p) return 0; + std::printf("error=%s\n", p.error().c_str()); + return 1; + }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code != 0, "the document is refused"); + c.expect(r.has_line("error=", {"schema 2", "reads schema 1"}), "the refusal names both schemas"); + } }, + + // ── a package format's provider (mcpp.plugins.platform) ───────────── + { "provide_format: an ordinary pass declares the format and submits nothing", + with_stage(t::row::linux_libcxx()), + [] { return provide_okb(); }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code == 0, "the provider succeeds"); + c.expect(r.has_line("mcpp:pack-format=okb"), "the format is declared on every pass"); + c.expect(r.count("mcpp:action=") == 0, "no action outside the pass that asks for the format"); + } }, + + { "provide_format: the pass for the format makes the action from the staged package", + with_stage(t::row::linux_libcxx()).pack("okb", "{root}/stage/hello-0.1.0-openkal"), + [] { return provide_okb(); }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code == 0, "the provider succeeds"); + c.expect(r.has_line("mcpp:pack-format=okb"), "the format is declared"); + c.expect(r.has_line("mcpp:action=", {"\"role\":\"artifact\"", "hello-0.1.0.okb", "${mcpp.stage_dir}"}), + "one artifact action, named from the package's metadata"); + } }, + + { "provide_format: the pass for the format without a staged package is refused", + t::row::linux_libcxx().pack("okb", "{root}/nowhere"), + [] { return provide_okb(); }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code != 0, "the provider fails"); + c.expect(r.count("mcpp:action=") == 0, "no action"); + c.expect(r.has_line("mcpp:warning=", {"provide_format", "mcpp-package.json"}), + "the refusal names the missing document"); + } }, + + { "provide_format: a format mcpp writes itself is refused", + t::row::linux_libcxx(), + [] { return pf::provide_format("zip", [](auto const&, mcpp::action&) { return true; }) ? 0 : 1; }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code != 0, "the provider fails"); + c.expect(!r.has_line("mcpp:pack-format="), "nothing is declared"); + c.expect(r.has_line("mcpp:warning=", {"'zip' is a format mcpp writes itself"}), "the refusal names it"); + } }, + + // ── the platform's fields (mcpp.plugins.platform) ─────────────────── + { "platform: the link, the names, the seal and the runner's host are stated as directives", + t::row::linux_libcxx().phase("platform"), + [] { + bool ok = pf::link::driver("lld") && pf::link::start("ok_main") + && pf::link::input_from("ok-notes.S", {"okld", "note-source"}) + && pf::link::input_file("link/startup.S") + && pf::artifact_names("{name}.oke", "lib{name}.okso") + && pf::seal({ { "okld", "note", "{}" }, { "okld", "check", "{}", "--with", "{needed}" } }) + && pf::run_host("same-arch"); + return ok ? 0 : 1; + }, + [](const t::result& r, t::checker& c) { + c.expect(r.exit_code == 0, "every builder states its field"); + c.expect(r.has_line("mcpp:link-driver=lld"), "the driver"); + c.expect(r.has_line("mcpp:link-start=ok_main"), "the entry"); + c.expect(r.has_line("mcpp:link-input=from:ok-notes.S:okld") + && r.has_line("mcpp:link-input=from:ok-notes.S:note-source"), "a program's output, word by word"); + c.expect(r.has_line("mcpp:link-input=file:", {"/pkg/link/startup.S"}), + "a relative input is the package's own, made absolute"); + c.expect(r.has_line("mcpp:artifact-name=exe:{name}.oke") + && r.has_line("mcpp:artifact-name=shared:lib{name}.okso"), "both names"); + c.expect(r.has_line("mcpp:seal=0:okld") && r.has_line("mcpp:seal=1:{needed}") + && r.count("mcpp:seal=") == 8, "two steps, every word in order"); + c.expect(r.has_line("mcpp:run-host=same-arch"), "the runner's host"); + } }, + + { "platform: a malformed field is refused at the builder, and nothing is stated", + t::row::linux_libcxx().phase("platform"), + [] { + int refused = 0; + refused += !pf::link::driver("gold"); + refused += !pf::link::start("9lives"); + refused += !pf::link::input_from("a:b", {"okld"}); + refused += !pf::link::input_from("notes.S", std::span{}); + refused += !pf::artifact_names("app-{name}"); + refused += !pf::artifact_names("{name}", "lib{name}{name}.so"); + refused += !pf::seal(std::span>{}); + refused += !pf::run_host("sometimes"); + std::printf("refused=%d\n", refused); + return refused == 8 ? 0 : 1; + }, + [](const t::result& r, t::checker& c) { + c.expect(r.has_line("refused=8"), "every malformed value is refused"); + c.expect(!r.has_line("mcpp:link-driver=") && !r.has_line("mcpp:link-start=") + && !r.has_line("mcpp:link-input=") && !r.has_line("mcpp:artifact-name=") + && !r.has_line("mcpp:seal=") && !r.has_line("mcpp:run-host="), + "no directive is stated for a refused value"); + c.expect(r.has_line("mcpp:warning=", {"link::driver", "\"lld\" and \"compiler\""}), + "a refusal names the call and the values it takes"); + c.expect(r.has_line("mcpp:warning=", {"artifact_names", "at its start"}), + "an executable's pattern begins with {name}"); + } }, + // ── compatibility ─────────────────────────────────────────────────── { "compat: program_compilers keeps its 0.16.0 answer and says until when", t::row::linux_libcxx().file("llvm/bin/clang").file("llvm/bin/clang++"), diff --git a/tests/plugin-logic/mcpp.toml b/tests/plugin-logic/mcpp.toml index 27e7e6b..b5b4c70 100644 --- a/tests/plugin-logic/mcpp.toml +++ b/tests/plugin-logic/mcpp.toml @@ -1,7 +1,8 @@ -# Fixture: the logic of the deps members and the files rules-qt reads by -# default, tested with `mcpp.plugins.testing` against stated build contexts -- -# a Visual Studio row, a managed MSVC row, a Linux GCC row and a Linux libc++ -# row -- on every host, with no foreign tool installed or run. The build +# Fixture: the logic of the deps members, the files rules-qt reads by +# default, and what a platform's plugin states (mcpp.plugins.platform, the +# package reader), tested with `mcpp.plugins.testing` against stated build +# contexts -- a Visual Studio row, a managed MSVC row, a Linux GCC row and a +# Linux libc++ row -- on every host, with no foreign tool installed or run. The build # program is the test: a failed case fails the build and prints the report; # `target/**/plugins-testing/results.txt` holds one verdict per case. [package] @@ -17,7 +18,7 @@ modules = true import_std = true [build-dependencies.mcpp] -plugins = { path = "../..", features = ["deps-vcpkg", "deps-cmake", "rules-qt", "plugins-testing"], host-module = true } +plugins = { path = "../..", features = ["deps-vcpkg", "deps-cmake", "rules-qt", "plugins-platform", "plugins-testing"], host-module = true } [targets.plugin-logic] kind = "bin" diff --git a/tests/sycl-consumer/mcpp.toml b/tests/sycl-consumer/mcpp.toml index 2db4702..580234a 100644 --- a/tests/sycl-consumer/mcpp.toml +++ b/tests/sycl-consumer/mcpp.toml @@ -26,7 +26,7 @@ default = "llvm@22.1.8" [build-dependencies.mcpp] plugins = { path = "../..", features = ["rules-sycl"], host-module = true } -# The SYCL runtime, on the artifact's runtime search path. mcpp's private +# The SYCL runtime, on the artifact's runtime search path, for a SYCL build. mcpp's private # loader does not consult /usr/lib, so `libsycl.so.9` -- which the rule # satisfied at LINK time from the payload -- would not be found at run time. # @@ -34,9 +34,17 @@ plugins = { path = "../..", features = ["rules-sycl"], host-module = true } # where the boundary was drawn: the runtime's CUDA back end dlopens the # driver, so the adapter carries that hop. A project that writes SYCL does not # have to know CUDA is underneath it. -[dependencies.compat] +[target.'cfg(accelerator = "sycl")'.dependencies.compat] sycl-runtime = "2026.09.07" +# THE C++ LIBRARY OF A SYCL BUILD IS libstdc++ (0.20.0; mcpp 2026.10.26.1+). +# `libsycl.so` throws `sycl::exception` on libstdc++, and a typed catch does not +# match across two C++ runtimes in one process (mcpp-community/mcpp#838), so +# the compiler stays llvm and the C++ library comes from the graph. Both edges +# are under the accelerator: the CPU variant (`--no-accel`) keeps llvm's libc++. +[target.'cfg(accelerator = "sycl")'.dependencies.gcc] +libstdcxx = "16.1.0.1" + # NO [xlings.workspace]. The rule declares the payloads it drives, under the # feature that selects it and the accelerator it serves, so this project names # the rule and nothing else. Needs mcpp 2026.9.6.6; before it, a payload a diff --git a/tests/sycl-consumer/src/kernels/saxpy.sycl b/tests/sycl-consumer/src/kernels/saxpy.sycl index 2f8f1fe..d50ccf7 100644 --- a/tests/sycl-consumer/src/kernels/saxpy.sycl +++ b/tests/sycl-consumer/src/kernels/saxpy.sycl @@ -5,19 +5,18 @@ // NOT. // // tests/cuda-consumer and tests/hip-consumer say in their own comments that -// their islands touch no standard library, because an island that pulls one in -// links a second C++ runtime into a program whose own came from mcpp's -// toolchain. A SYCL island cannot make that promise: SYCL IS a C++ library, -// `sycl::queue` and `sycl::buffer` are class templates over standard types, -// and `libsycl.so` is itself compiled against libstdc++. +// their islands touch no standard library. A SYCL island cannot make that +// promise: SYCL IS a C++ library, `sycl::queue` and `sycl::buffer` are class +// templates over standard types, and `libsycl.so` is itself compiled against +// libstdc++ and throws `sycl::exception` on it. // -// So the second runtime is present here by construction, and what makes that -// safe is the seam rather than an absence. Nothing below crosses -// `saxpy_device`'s signature: the parameters are pointers and a count, the -// return is an int, and no object allocated by one runtime is freed by the -// other. `mcpp.rules.sycl` links the libstdc++ half explicitly for the same -// reason -- it is a consequence of the compiler this rule chose, not something -// the manifest should have to know. +// So the whole program uses libstdc++ (0.20.0): the manifest supplies it to the +// llvm build through `gcc.libstdcxx`, and `mcpp.rules.sycl` points the device +// compiler at the same GCC. One C++ runtime in the process is what lets a +// `catch (sycl::exception const&)` match what the runtime threw; until 0.19 +// the program linked libc++ beside it, and only `catch (...)` matched +// (mcpp-community/mcpp#838). The seam stays `extern "C"` for the reason the +// siblings give: it is the one place a backend can be exchanged. // // WHAT THIS ISLAND CAN AND CANNOT TURN INTO A RETURN CODE. //