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>
156 lines
8.1 KiB
C++
156 lines
8.1 KiB
C++
// 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;
|
|
}
|