test(sysroot): cover the std module source choice for a sysroot build
SysrootStdModule runs over staged layouts that need no compiler: a sysroot with its own module source, one on a point release of the toolchain's libc++, one on the next major release with a shipped source, one nothing was shipped for, one without libc++ at all, and the two cases where a release cannot be read. The last block drives Build() against a sysroot claiming libc++ 99 and expects the one-line explanation back instead of compiler output, which is the failure the fix replaces. ToolchainResolve additionally checks that the release is read from the libc++ the module source comes from (the prefix's, an explicit libc++ install's, the system's) and where the shipped sources are looked up. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
e15cd3c14d
commit
cbf2b94f06
3 changed files with 167 additions and 1 deletions
|
|
@ -129,6 +129,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.AddTest("TestRunnerSpec").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("TestRunnerSpec").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("VariantId").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("VariantId").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("ToolchainResolve").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("ToolchainResolve").Dependencies({ CrafterBuildLib.get() });
|
||||||
|
cfg.AddTest("SysrootStdModule").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("VersionFlag").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("VersionFlag").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("WasiBrowserRuntime").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("WasiBrowserRuntime").Dependencies({ CrafterBuildLib.get() });
|
||||||
cfg.AddTest("WasmVariants").Dependencies({ CrafterBuildLib.get() });
|
cfg.AddTest("WasmVariants").Dependencies({ CrafterBuildLib.get() });
|
||||||
|
|
|
||||||
156
tests/SysrootStdModule/main.cpp
Normal file
156
tests/SysrootStdModule/main.cpp
Normal file
|
|
@ -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<fs::path, 0> ifaces = {};
|
||||||
|
std::array<fs::path, 1> impls = { "main" };
|
||||||
|
app.GetInterfacesAndImplementations(ifaces, impls);
|
||||||
|
|
||||||
|
std::unordered_map<fs::path, std::shared_future<BuildResult>> 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;
|
||||||
|
}
|
||||||
|
|
@ -40,9 +40,11 @@ int main() {
|
||||||
// A prefix with everything crafter-build looks for beside the compiler,
|
// A prefix with everything crafter-build looks for beside the compiler,
|
||||||
// and one with the compiler alone.
|
// and one with the compiler alone.
|
||||||
const fs::path full = scratch / "full";
|
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);
|
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";
|
const fs::path bare = scratch / "bare";
|
||||||
Touch(bare / "bin/clang++");
|
Touch(bare / "bin/clang++");
|
||||||
|
|
||||||
|
|
@ -56,6 +58,8 @@ int main() {
|
||||||
Check(!tc.libclang.has_value(), "the PATH fallback searches libclang by name");
|
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.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.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.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.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");
|
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.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.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.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.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(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");
|
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
|
// An explicit libc++ install: headers, library and std module all
|
||||||
// come from it, whatever the compiler would have used.
|
// come from it, whatever the compiler would have used.
|
||||||
const fs::path libcxx = scratch / "libcxx";
|
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;
|
ToolchainInputs in;
|
||||||
in.crafterPrefix = full;
|
in.crafterPrefix = full;
|
||||||
in.env["CRAFTER_BUILD_LIBCXX_DIR"] = libcxx.string();
|
in.env["CRAFTER_BUILD_LIBCXX_DIR"] = libcxx.string();
|
||||||
Toolchain tc = ResolveToolchain(in);
|
Toolchain tc = ResolveToolchain(in);
|
||||||
Check(tc.libcxxDir == libcxx, "CRAFTER_BUILD_LIBCXX_DIR is recorded");
|
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.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.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");
|
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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue