diff --git a/project.cpp b/project.cpp index d3364d3..a7c3272 100644 --- a/project.cpp +++ b/project.cpp @@ -129,6 +129,7 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.AddTest("TestRunnerSpec").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("VariantId").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("ToolchainResolve").Dependencies({ CrafterBuildLib.get() }); + cfg.AddTest("SysrootStdModule").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("VersionFlag").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("WasiBrowserRuntime").Dependencies({ CrafterBuildLib.get() }); cfg.AddTest("WasmVariants").Dependencies({ CrafterBuildLib.get() }); diff --git a/tests/SysrootStdModule/main.cpp b/tests/SysrootStdModule/main.cpp new file mode 100644 index 0000000..e5e984f --- /dev/null +++ b/tests/SysrootStdModule/main.cpp @@ -0,0 +1,156 @@ +// 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; +} diff --git a/tests/ToolchainResolve/main.cpp b/tests/ToolchainResolve/main.cpp index 51d8af6..d535c6b 100644 --- a/tests/ToolchainResolve/main.cpp +++ b/tests/ToolchainResolve/main.cpp @@ -40,9 +40,11 @@ int main() { // A prefix with everything crafter-build looks for beside the compiler, // and one with the compiler alone. const fs::path full = scratch / "full"; - for (const char* rel : {"bin/clang++", "bin/clang", "bin/llvm-ar", "lib/libclang.so", "include/clang-c/Index.h", "share/libc++/v1/std.cppm", "share/wasi-sysroot/include/.keep"}) { + for (const char* rel : {"bin/clang++", "bin/clang", "bin/llvm-ar", "lib/libclang.so", "include/clang-c/Index.h", "include/c++/v1/__config", "share/libc++/v1/std.cppm", "share/wasi-sysroot/include/.keep"}) { Touch(full / rel); } + // The prefix's libc++ headers state its release. + std::ofstream(full / "include/c++/v1/__config") << "# define _LIBCPP_VERSION 230102\n"; const fs::path bare = scratch / "bare"; Touch(bare / "bin/clang++"); @@ -56,6 +58,8 @@ int main() { Check(!tc.libclang.has_value(), "the PATH fallback searches libclang by name"); Check(tc.llvmIncludeDir.empty(), "the PATH fallback adds no include dir"); Check(tc.stdCppm == "/usr/share/libc++/v1/std.cppm", "the system std module source is the default"); + Check(tc.libcxxVersion == LibcxxVersion("/usr/include/c++/v1"), "the system libc++ headers state the release of the system module source"); + Check(tc.stdModulesDir.empty(), "no crafter-build prefix, no shipped module sources"); Check(tc.wasiSysroot == "/usr/share/wasi-sysroot", "the system wasi sysroot is the default"); Check(tc.mingwSysroot == "/usr/x86_64-w64-mingw32" && !tc.mingwSysrootExplicit, "the default mingw dir is not passed as --sysroot"); Check(tc.LibcxxCompileFlags().empty() && tc.LibcxxLinkFlags().empty(), "the PATH fallback adds no libc++ flags"); @@ -71,6 +75,8 @@ int main() { Check(tc.libclang == full / "lib/libclang.so", "libclang beside the compiler is pinned"); Check(tc.llvmIncludeDir == full / "include", "clang-c headers beside the compiler are added to the include path"); Check(tc.stdCppm == full / "share/libc++/v1/std.cppm", "the prefix's std module source is used"); + Check(tc.libcxxVersion == 230102, "the prefix's libc++ headers state its release"); + Check(tc.stdModulesDir == full / "share/crafter-build/libc++-modules", "module sources for other releases are looked up in crafter-build's own share directory"); Check(tc.wasiSysroot == full / "share/wasi-sysroot", "the prefix's wasi sysroot is used"); Check(tc.LibcxxCompileFlags().empty(), "the compiler finds its own libc++ headers without flags"); Check(Contains(tc.LibcxxLinkFlags(), std::format("-L{}", (full / "lib").string())) && Contains(tc.LibcxxLinkFlags(), std::format("-Wl,-rpath,{}", (full / "lib").string())), "binaries get -L and an rpath for the prefix's lib dir"); @@ -120,12 +126,15 @@ int main() { // An explicit libc++ install: headers, library and std module all // come from it, whatever the compiler would have used. const fs::path libcxx = scratch / "libcxx"; + fs::create_directories(libcxx / "include/c++/v1"); + std::ofstream(libcxx / "include/c++/v1/__config") << "# define _LIBCPP_VERSION 210108\n"; ToolchainInputs in; in.crafterPrefix = full; in.env["CRAFTER_BUILD_LIBCXX_DIR"] = libcxx.string(); Toolchain tc = ResolveToolchain(in); Check(tc.libcxxDir == libcxx, "CRAFTER_BUILD_LIBCXX_DIR is recorded"); Check(tc.stdCppm == libcxx / "share/libc++/v1/std.cppm", "the std module source follows the explicit libc++"); + Check(tc.libcxxVersion == 210108, "the release is read from the explicit libc++, not the compiler's"); Check(Contains(tc.LibcxxCompileFlags(), std::format("-nostdinc++ -isystem {}", (libcxx / "include/c++/v1").string())), "compiles use the explicit libc++ headers"); Check(Contains(tc.LibcxxLinkFlags(), std::format("-L{}", (libcxx / "lib").string())) && Contains(tc.LibcxxLinkFlags(), std::format("-Wl,-rpath,{}", (libcxx / "lib").string())), "links use the explicit libc++ library dir, with an rpath"); }