// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® import std; import Crafter.Build; namespace fs = std::filesystem; using namespace Crafter; // A sysroot build precompiles the std module from a module source that has to // belong to the libc++ release whose headers the sysroot carries. Not every // distro packages that source with its libc++ (Alpine does not), and the // fallback used to be the toolchain's own copy whatever its release: with a // libc++ 22 toolchain and a libc++ 23 sysroot the build died on a page of "no // member named 'runtime_format' in namespace 'std'" that named neither // release nor what to do about it. // // Staged, compiler-free layouts cover the order SysrootStdModule picks in: the // sysroot's own source, the toolchain's for the same major release, the copy // crafter-build ships for another release, and otherwise one line naming both // releases and where the matching source belongs. The last block drives // Build() against a sysroot nothing matches and expects that line back in // place of compiler output. namespace { std::int32_t Failures = 0; void Check(bool cond, std::string_view msg) { if (!cond) { std::println(std::cerr, "FAIL: {}", msg); ++Failures; } } void Write(const fs::path& file, std::string_view content) { fs::create_directories(file.parent_path()); std::ofstream(file) << content; } // What libc++ installs as include/c++/v1/__config, reduced to the line // that states the release. void StageHeaders(const fs::path& includeDir, std::int32_t version) { Write(includeDir / "__config", std::format("#ifndef _LIBCPP___CONFIG\n# define _LIBCPP___CONFIG\n# define _LIBCPP_VERSION {}\n#endif\n", version)); } fs::path StageSysroot(const fs::path& sysroot, std::int32_t version) { StageHeaders(sysroot / "usr" / "include" / "c++" / "v1", version); return sysroot; } } int main() { const fs::path scratch = fs::temp_directory_path() / "crafter-build-sysroot-std-module"; std::error_code ec; fs::remove_all(scratch, ec); fs::create_directories(scratch); { StageHeaders(scratch / "headers", 230102); Check(LibcxxVersion(scratch / "headers") == 230102, "the release is read from __config"); Check(LibcxxVersion(scratch / "no-such-dir") == 0, "missing headers read as an unknown release"); Write(scratch / "other" / "__config", "#define _LIBCPP_ABI_VERSION 1\n"); Check(LibcxxVersion(scratch / "other") == 0, "a __config without the version line reads as an unknown release"); Check(LibcxxVersionString(230102) == "23.1.2" && LibcxxVersionString(220108) == "22.1.8", "XXYYZZ is spelled as a dotted release"); } // A toolchain on libc++ 22.1.8 that also ships the sources for 23. Toolchain tc; tc.stdCppm = scratch / "toolchain" / "share" / "libc++" / "v1" / "std.cppm"; tc.libcxxVersion = 220108; tc.stdModulesDir = scratch / "shipped"; Write(tc.stdCppm, ""); Write(tc.stdModulesDir / "23" / "std.cppm", ""); { // The sysroot's own source matches its headers by construction, so // it wins whatever the releases are. const fs::path sysroot = StageSysroot(scratch / "sysroot-own", 240100); const fs::path own = sysroot / "usr" / "share" / "libc++" / "v1" / "std.cppm"; Write(own, ""); StdModuleSource source = SysrootStdModule(tc, sysroot); Check(source.error.empty() && source.stdCppm == own, "a sysroot that ships the module source uses its own"); } { const fs::path sysroot = StageSysroot(scratch / "sysroot-same-major", 220103); StdModuleSource source = SysrootStdModule(tc, sysroot); Check(source.error.empty() && source.stdCppm == tc.stdCppm, "a point release of the toolchain's libc++ uses the toolchain's source"); } { // The reported case: libc++ 23 headers, a libc++ 22 toolchain. const fs::path sysroot = StageSysroot(scratch / "sysroot-next-major", 230102); StdModuleSource source = SysrootStdModule(tc, sysroot); Check(source.error.empty() && source.stdCppm == tc.stdModulesDir / "23" / "std.cppm", "another release uses the source shipped for it, not the toolchain's"); } { const fs::path sysroot = StageSysroot(scratch / "sysroot-unshipped", 210108); StdModuleSource source = SysrootStdModule(tc, sysroot); Check(source.stdCppm.empty() && !source.error.empty(), "a release nothing was shipped for is an error, not a compile against the wrong source"); Check(source.error.contains("21.1.8") && source.error.contains("22.1.8"), std::format("the error names both releases, got '{}'", source.error)); Check(source.error.contains((sysroot / "usr" / "share" / "libc++" / "v1").string()), std::format("the error says where the matching source belongs, got '{}'", source.error)); Check(!source.error.contains('\n'), "the error is one line"); } { const fs::path sysroot = scratch / "sysroot-no-libcxx"; fs::create_directories(sysroot / "usr" / "include"); StdModuleSource source = SysrootStdModule(tc, sysroot); Check(source.stdCppm.empty() && source.error.contains("no libc++ headers"), std::format("a sysroot without libc++ says so, got '{}'", source.error)); } { // Nothing to compare when either release is unknown: the toolchain's // source goes to the compiler, as it did before releases were read. const fs::path sysroot = scratch / "sysroot-unreadable"; Write(sysroot / "usr" / "include" / "c++" / "v1" / "__config", "#define _LIBCPP_ABI_VERSION 1\n"); Check(SysrootStdModule(tc, sysroot).stdCppm == tc.stdCppm, "an unreadable sysroot release falls back to the toolchain's source"); Toolchain unknown = tc; unknown.libcxxVersion = 0; unknown.stdModulesDir.clear(); Check(SysrootStdModule(unknown, StageSysroot(scratch / "sysroot-vs-unknown", 230102)).stdCppm == tc.stdCppm, "an unreadable toolchain release falls back to the toolchain's source"); } { Toolchain bare; bare.stdCppm = scratch / "nowhere" / "std.cppm"; bare.libcxxVersion = 220108; StdModuleSource source = SysrootStdModule(bare, StageSysroot(scratch / "sysroot-vs-bare", 220108)); Check(source.stdCppm.empty() && source.error.contains("no std module source"), std::format("a toolchain without a module source is an error too, got '{}'", source.error)); } { // End to end with the real toolchain: a sysroot on a libc++ release // that does not exist fails the build with the one-line explanation // before clang is run. The target is the host's so the std PCM cache // directory is the one every other test already uses. const Toolchain& real = GetToolchain(); Check(real.libcxxVersion != 0, "the running toolchain's libc++ release is read from its headers"); const fs::path project = scratch / "project"; Write(project / "main.cpp", "int main() { return 0; }\n"); Configuration app; app.path = project; app.name = "sysroot-std-module"; app.outputName = "sysroot-std-module"; app.target = HostTarget(); app.type = ConfigurationType::Executable; app.sysroot = StageSysroot(scratch / "sysroot-future", 990000).string(); std::array ifaces = {}; std::array impls = { "main" }; app.GetInterfacesAndImplementations(ifaces, impls); std::unordered_map> depResults; std::mutex depMutex; BuildResult r = Build(app, depResults, depMutex); Check(r.result.contains("libc++ 99.0.0"), std::format("the build fails naming the sysroot's release, got '{}'", r.result)); Check(r.result.contains(LibcxxVersionString(real.libcxxVersion)), std::format("the build fails naming the toolchain's release, got '{}'", r.result)); Check(!r.result.contains('\n') && !r.result.contains("error:"), std::format("the failure is one line, not compiler output, got '{}'", r.result)); } return Failures == 0 ? 0 : 1; }