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:
Jorijn van der Graaf 2026-10-03 16:37:00 +02:00
commit cbf2b94f06
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
3 changed files with 167 additions and 1 deletions

View 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;
}