diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5d1b4a..f971808 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,12 @@ 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. + MCPP_VERSION: 2026.10.26.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,7 +83,7 @@ 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.1 # AN ENGINE BUILT FROM SOURCE, WHEN A DISPATCH NAMES ONE. # # Empty on every push and pull request, so the steps run the release above. @@ -97,7 +102,7 @@ env: # and says so, which the lane counts as a failure. Raising either is a # one-line change here. MCPPLS_VERSION: 0.0.15 - MCPP_EDITOR_VERSION: 2026.10.11.1 + MCPP_EDITOR_VERSION: 2026.10.26.1 # PINNED, AND WITHOUT IT THE CACHE BELOW CACHED NOTHING. # # A released mcpp is self-contained: with no `MCPP_HOME`, `mcpp self env` diff --git a/README.md b/README.md index 6f79945..e3f1788 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 diff --git a/docs/plugin-development.md b/docs/plugin-development.md index 4e0c0f7..7a09678 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -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 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..763b020 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] name = "plugins" namespace = "mcpp" -version = "0.19.1" +version = "0.20.0" # 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 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/plugins.cppm b/src/plugins.cppm index eb91950..d574e34 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 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. //