diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index da2ddc4..d2aee22 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -1537,6 +1537,20 @@ static void PrintVersion() { return; } std::println("compiler {} ({})", tc.clangVersion, tc.clangxx.string()); + if (tc.libcxxVersion != 0) { + // A sysroot on another libc++ release builds only when its std + // module source is in the sysroot or among these; printed here + // because that is the first question when such a build fails. + std::vector shipped; + std::error_code ec; + for (const fs::directory_entry& entry : fs::directory_iterator(tc.stdModulesDir, ec)) { + if (fs::exists(entry.path() / "std.cppm")) shipped.push_back(entry.path().filename().string()); + } + std::ranges::sort(shipped); + std::string also; + for (const std::string& release : shipped) also += std::format(", {}", release); + std::println("libc++ {}{}", LibcxxVersionString(tc.libcxxVersion), also.empty() ? std::string() : std::format(" (std module sources for sysroots also for{})", also.substr(1))); + } std::println("host {}", tc.hostTriple); } diff --git a/implementations/Crafter.Build-Platform.cpp b/implementations/Crafter.Build-Platform.cpp index a6f8793..f8a7694 100644 --- a/implementations/Crafter.Build-Platform.cpp +++ b/implementations/Crafter.Build-Platform.cpp @@ -663,14 +663,13 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) { } else if (config.sysroot.empty()) { stdCppm = GetToolchain().stdCppm.string(); } else { - stdCppm = std::format("{}/usr/share/libc++/v1/std.cppm", config.sysroot); - // not every distro ships the std module sources (Alpine's - // libc++-dev doesn't); the host copy is interchangeable as long - // as the libc++ versions line up, which the compile of the PCM - // against the sysroot's headers verifies anyway - if (!fs::exists(stdCppm) && fs::exists(GetToolchain().stdCppm)) { - stdCppm = GetToolchain().stdCppm.string(); - } + // The source has to belong to the libc++ release whose headers + // the sysroot carries. Handing clang the toolchain's own copy + // regardless used to end in a page of "no member named ... in + // namespace 'std'" once the two releases differed. + StdModuleSource source = SysrootStdModule(GetToolchain(), config.sysroot); + if (!source.error.empty()) return source.error; + stdCppm = source.stdCppm.string(); } std::string sysrootFlag = config.sysroot.empty() ? std::string() diff --git a/implementations/Crafter.Build-Toolchain.cpp b/implementations/Crafter.Build-Toolchain.cpp index d7e4868..5c232ce 100644 --- a/implementations/Crafter.Build-Toolchain.cpp +++ b/implementations/Crafter.Build-Toolchain.cpp @@ -112,15 +112,19 @@ Toolchain Crafter::ResolveToolchain(const ToolchainInputs& in) { // 3. libc++ and its std module source. The compiler's own copy lives at // /share/libc++/v1 when the prefix carries libc++; the system - // copy is the last resort either way. - const fs::path systemStdCppm = "/usr/share/libc++/v1/std.cppm"; + // copy is the last resort either way. The headers of the same install + // say which release the source belongs to. + fs::path libcxxRoot = "/usr"; if (std::string_view dir = Env(in, "CRAFTER_BUILD_LIBCXX_DIR"); !dir.empty()) { tc.libcxxDir = fs::path(dir); - tc.stdCppm = tc.libcxxDir / "share" / "libc++" / "v1" / "std.cppm"; + libcxxRoot = tc.libcxxDir; } else if (!tc.prefix.empty() && fs::exists(tc.prefix / "share" / "libc++" / "v1" / "std.cppm")) { - tc.stdCppm = tc.prefix / "share" / "libc++" / "v1" / "std.cppm"; - } else { - tc.stdCppm = systemStdCppm; + libcxxRoot = tc.prefix; + } + tc.stdCppm = libcxxRoot / "share" / "libc++" / "v1" / "std.cppm"; + tc.libcxxVersion = LibcxxVersion(libcxxRoot / "include" / "c++" / "v1"); + if (!in.crafterPrefix.empty()) { + tc.stdModulesDir = in.crafterPrefix / "share" / "crafter-build" / "libc++-modules"; } // 4. Sysroots for the cross targets that have a conventional location. @@ -180,6 +184,56 @@ const Toolchain& Crafter::RequireToolchain() { return tc; } +std::int32_t Crafter::LibcxxVersion(const fs::path& includeDir) { + // "# define _LIBCPP_VERSION 230102", the one place libc++ states its + // release in a form the preprocessor does not have to be run for. + constexpr std::string_view Marker = "define _LIBCPP_VERSION "; + std::ifstream config(includeDir / "__config"); + std::string line; + while (std::getline(config, line)) { + const std::size_t at = line.find(Marker); + if (at == std::string::npos) continue; + const std::string_view digits = std::string_view(line).substr(at + Marker.size()); + std::int32_t version = 0; + if (std::from_chars(digits.data(), digits.data() + digits.size(), version).ec == std::errc{}) return version; + } + return 0; +} + +std::string Crafter::LibcxxVersionString(std::int32_t version) { + return std::format("{}.{}.{}", version / 10000, version / 100 % 100, version % 100); +} + +StdModuleSource Crafter::SysrootStdModule(const Toolchain& tc, const fs::path& sysroot) { + const fs::path own = sysroot / "usr" / "share" / "libc++" / "v1" / "std.cppm"; + if (fs::exists(own)) return {own, {}}; + + const fs::path includeDir = sysroot / "usr" / "include" / "c++" / "v1"; + if (!fs::exists(includeDir / "__config")) { + return {{}, std::format("sysroot {} has no libc++ headers at {}; install its libc++ development package", sysroot.string(), includeDir.string())}; + } + const std::int32_t wanted = LibcxxVersion(includeDir); + const bool comparable = wanted != 0 && tc.libcxxVersion != 0; + // Releases are matched on the major version: a release branch only takes + // fixes, so 22.1.3 headers and the 22.1.8 module source name the same + // declarations. + if (comparable && wanted / 10000 == tc.libcxxVersion / 10000 && fs::exists(tc.stdCppm)) return {tc.stdCppm, {}}; + if (wanted != 0 && !tc.stdModulesDir.empty()) { + const fs::path shipped = tc.stdModulesDir / std::to_string(wanted / 10000) / "std.cppm"; + if (fs::exists(shipped)) return {shipped, {}}; + } + // One of the two releases is unknown, so there is nothing to compare: + // hand over the toolchain's source and let the compile decide, which is + // what happened for every sysroot before releases were compared. + if (!comparable && fs::exists(tc.stdCppm)) return {tc.stdCppm, {}}; + + const std::string sysrootRelease = wanted != 0 ? LibcxxVersionString(wanted) : std::string("of an unknown release"); + const std::string toolchainSource = !fs::exists(tc.stdCppm) + ? std::string("the toolchain has no std module source at all") + : std::format("the toolchain's is for libc++ {}", LibcxxVersionString(tc.libcxxVersion)); + return {{}, std::format("sysroot {} carries libc++ {} without its std module source, and crafter-build has none for that release ({}); install the share/libc++/v1 directory of that libc++ release (std.cppm and std/) as {}", sysroot.string(), sysrootRelease, toolchainSource, own.parent_path().string())}; +} + std::string_view Crafter::CrafterBuildVersion() { return CRAFTER_BUILD_VERSION; } diff --git a/interfaces/Crafter.Build-Toolchain.cppm b/interfaces/Crafter.Build-Toolchain.cppm index 853b2b6..05d1d26 100644 --- a/interfaces/Crafter.Build-Toolchain.cppm +++ b/interfaces/Crafter.Build-Toolchain.cppm @@ -28,6 +28,14 @@ export namespace Crafter { fs::path libcxxDir; // libc++'s std module source for host builds. fs::path stdCppm; + // The _LIBCPP_VERSION of the libc++ stdCppm belongs to (220108 for + // 22.1.8); 0 when its <__config> could not be read. + std::int32_t libcxxVersion = 0; + // Std module sources for libc++ releases other than the toolchain's + // own, one /std.cppm each, for sysroots that carry libc++ + // without them: /share/crafter-build/libc++-modules. + // Empty when the crafter prefix is unknown; need not exist. + fs::path stdModulesDir; fs::path wasiSysroot; // Holds include/c++//bits/std.cc for the mingw target. Passed to // clang as --sysroot only when it was set explicitly: clang's own @@ -74,6 +82,29 @@ export namespace Crafter { const Toolchain& GetToolchain(); // GetToolchain(), or a std::runtime_error carrying its error. const Toolchain& RequireToolchain(); + // _LIBCPP_VERSION as the <__config> in `includeDir` (an include/c++/v1) + // defines it, XXYYZZ; 0 when the file is missing or has no such line. + std::int32_t LibcxxVersion(const fs::path& includeDir); + // "23.1.2" for 230102. + std::string LibcxxVersionString(std::int32_t version); + + struct StdModuleSource { + fs::path stdCppm; + // One line saying why no source fits; empty when stdCppm is set. + std::string error; + }; + // The std module source to precompile for a native (non-wasm) build + // against `sysroot`. The module source is a list of using-declarations + // for names the headers of the same libc++ release declare, so one from + // another release fails on every name that release added or dropped. In + // order: the sysroot's own /usr/share/libc++/v1/std.cppm, the + // toolchain's when both are the same major release, the copy for the + // sysroot's release under toolchain.stdModulesDir. Not every distro + // packages the module sources with its libc++ (Alpine does not), which + // is why the sysroot's own copy cannot be the only answer. When the + // sysroot's release cannot be read, the toolchain's source is returned + // and the compile is the check. + StdModuleSource SysrootStdModule(const Toolchain& toolchain, const fs::path& sysroot); // The version baked in when crafter-build was built; "0.0.0-dev" for a // build that was given none. std::string_view CrafterBuildVersion();