diff --git a/implementations/Crafter.Build-Clang.cpp b/implementations/Crafter.Build-Clang.cpp index 138ffd7..a302ca9 100644 --- a/implementations/Crafter.Build-Clang.cpp +++ b/implementations/Crafter.Build-Clang.cpp @@ -247,36 +247,51 @@ void Configuration::GetInterfacesAndImplementations(std::span interfac } } +std::string Crafter::StdPcmFlags(const Configuration& config) { + if (config.target == "x86_64-w64-mingw32") { + // -femulated-tls keeps PCM TLS access matching libstdc++'s emutls + // definitions; a mismatch surfaces as undefined std::__once_callable. + return std::format(" --target={} -march={} -mtune={} -femulated-tls -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier", config.target, config.march, config.mtune); + } + const bool isWasm = config.target.starts_with("wasm32"); + const std::string sysrootFlag = config.sysroot.empty() ? std::string() : std::format(" --sysroot={}", config.sysroot); + // wasm32 rejects -march, and the std module has to be compiled the way + // the units importing it are, a variant pass's flags (e.g. + // -mrelaxed-simd) included, or the BMI is not feature-compatible. + std::string archFlags = isWasm ? WasmCompileFlags(config) : std::format(" -march={} -mtune={}", config.march, config.mtune); + // non-wasm cross builds: the driver ranks the host toolchain's own + // libc++ headers (/../include/c++/v1) above --sysroot, so the + // PCM would mix host libc++ with the foreign libc. Force the sysroot's + // copy. + if (!isWasm && !config.sysroot.empty()) { + archFlags += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1", config.sysroot); + } + return std::format(" --target={} -std=c++26{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier", config.target, sysrootFlag, archFlags); +} + +std::string Crafter::StdPcmKey(const Configuration& config, const Toolchain& toolchain) { + // A PCM is only valid for the clang that wrote it and the flags it was + // written with, so a compiler bump starts a cold directory instead of + // handing the new clang a PCM it will crash on, and so does any flag. + // With only some of them in the name, configurations that differed in + // the rest took turns rebuilding one std PCM in place: -mtune=generic + // against project.cpp's -mtune=native on every run, two sysroots, or two + // releases sharing a cache, as the Flatpak's branches do, each time under + // the PCMs built against the previous one, which clang crashes on rather + // than rejecting. + std::string compiled = StdPcmFlags(config); + // The base command's part: the toolchain's key covers the compiler and + // libc++, and for the mingw target what clang finds mingw-w64 through is + // the prefix, given whole or through a view (MingwSysroot), named here by + // the prefix rather than by the view's own path. + if (config.target == "x86_64-w64-mingw32" && !toolchain.mingwPrefix.empty()) { + compiled += std::format("|mingw:{}|{}", toolchain.mingwPrefix.string(), toolchain.mingwPrefixIsSystem ? "view" : "whole"); + } + return std::format("{}-{}-{}-{:08x}", config.target, config.march, toolchain.Key(), static_cast(std::hash{}(compiled))); +} + fs::path Crafter::StdPcmDir(const Configuration& config) { - // The std PCM is cached per target+march+toolchain: a PCM is only valid - // for the exact clang that wrote it, so a compiler bump starts a cold - // directory instead of handing the new clang a PCM it will crash on. A - // wasm variant pass compiles it with extra codegen flags (e.g. - // -mrelaxed-simd) — its BMI must not be shared with the baseline's, or - // the consuming TUs see a target-feature mismatch. Suffix the cache dir - // with the variant flags when present. - std::string stdPcmKey = std::format("{}-{}-{}", config.target, config.march, GetToolchain().Key()); - // The flags every wasm unit gets are compiled into the std PCM as well, - // and the importing units have to match them. Two releases that share a - // cache and a compiler, as the Flatpak's branches do, would otherwise - // take turns rebuilding this one std PCM, each time underneath the PCMs - // built against the previous one, and clang crashes on those rather than - // rejecting them. - if (config.target.starts_with("wasm32")) { - stdPcmKey += std::format("-{:08x}", static_cast(std::hash{}(WasmBaselineFlags))); - } - // The same goes for the mingw-w64 prefix the mingw std module is - // compiled against: releases up to the fix for issue #43 gave clang the - // prefix itself as the sysroot and the ones after give it a view of it, - // and two prefixes are two sets of headers. - if (config.target == "x86_64-w64-mingw32" && !GetToolchain().mingwPrefix.empty()) { - stdPcmKey += std::format("-{:08x}", static_cast(std::hash{}(GetToolchain().mingwPrefix.string()))); - } - for (const std::string& f : config.wasmVariantFlags) { - stdPcmKey += "+"; - for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c; - } - return GetCacheDir()/stdPcmKey; + return GetCacheDir() / StdPcmKey(config, GetToolchain()); } std::string Crafter::WasmCompileFlags(const Configuration& config) { diff --git a/implementations/Crafter.Build-Platform.cpp b/implementations/Crafter.Build-Platform.cpp index 2c33eca..5d0c5f4 100644 --- a/implementations/Crafter.Build-Platform.cpp +++ b/implementations/Crafter.Build-Platform.cpp @@ -790,12 +790,10 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) { const fs::path stdCc = (manifestPath.parent_path() / *source).lexically_normal(); if (!fs::exists(stdCc)) return std::format("{}, which {} names as the std module, is missing", stdCc.string(), manifestPath.string()); - // -femulated-tls keeps PCM TLS access matching libstdc++'s emutls - // definitions; mismatch surfaces as undefined std::__once_callable. // The std.cc → std.cppm copy uses a plain filename: held under the // cache lock, no other builder writes this path concurrently. fs::path stdCppm = stdPcm.parent_path() / "std.cppm"; - std::string command = std::format("{} --target={} -march={} -mtune={} -femulated-tls -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", GetBaseCommand(config), config.target, config.march, config.mtune, stdCppm.string(), stdPcm.string()); + std::string command = std::format("{}{} --precompile {} -o {}", GetBaseCommand(config), StdPcmFlags(config), stdCppm.string(), stdPcm.string()); std::string stamp = HashStamp(command + '\0' + FileBytes(stdCc)); if (fs::exists(stdPcm) && ReadStamp(StampPath(stdPcm)) == stamp) return ""; std::error_code copyEc; @@ -825,27 +823,11 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) { if (!source.error.empty()) return source.error; stdCppm = source.stdCppm.string(); } - std::string sysrootFlag = config.sysroot.empty() - ? std::string() - : std::format(" --sysroot={}", config.sysroot); - // wasm32 rejects -march, and the std module has to be compiled the way - // the units importing it are, a variant pass's flags (e.g. - // -mrelaxed-simd) included, or the BMI is not feature-compatible. - std::string archFlags = isWasm - ? WasmCompileFlags(config) - : std::format(" -march={} -mtune={}", config.march, config.mtune); - // non-wasm cross builds: the driver ranks the host toolchain's own - // libc++ headers (/../include/c++/v1) above --sysroot, so - // the PCM would mix host libc++ with the foreign libc — force the - // sysroot's copy. - if (!isWasm && !config.sysroot.empty()) { - archFlags += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1", config.sysroot); - } // Keyed on the command and the module source's bytes, never on // mtimes: an installed std.cppm can be dated at the epoch (OSTree), // and a libc++ update under the same clang changes the bytes but // not the path. - std::string command = std::format("{} --target={} -std=c++26{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", GetBaseCommand(config), config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string()); + std::string command = std::format("{}{} --precompile {} -o {}", GetBaseCommand(config), StdPcmFlags(config), stdCppm, stdPcm.string()); std::string stamp = HashStamp(command + '\0' + FileBytes(stdCppm)); if (fs::exists(stdPcm) && ReadStamp(StampPath(stdPcm)) == stamp) return ""; std::string result = RunBuildCommand(command); diff --git a/interfaces/Crafter.Build-Clang.cppm b/interfaces/Crafter.Build-Clang.cppm index 89583cc..50903d6 100644 --- a/interfaces/Crafter.Build-Clang.cppm +++ b/interfaces/Crafter.Build-Clang.cppm @@ -392,6 +392,15 @@ export namespace Crafter { }; struct Configuration; + // The name StdPcmDir gives the directory of this configuration's std + // module under `toolchain`: target, march and the toolchain's key, which + // say what it is, then a hash of everything else it is compiled with, + // each flag BuildStdPcm passes (StdPcmFlags) and for the mingw target the + // prefix behind its --sysroot. Two configurations that would compile the + // std module differently never share a directory, so neither rebuilds it + // under the PCMs the other compiled against it. Declared ahead of + // Configuration, whose VariantId keys on it. + std::string StdPcmKey(const Configuration& config, const Toolchain& toolchain); // What a Script's callback is handed when `crafter-build run ` // starts it. @@ -580,7 +589,8 @@ export namespace Crafter { void AddAstLintRule(std::string name, std::function check); // Suffix that uniquely identifies this Configuration's compile state. // target+march+mtune are spelled out for readability; the rest - // (type, debug, sysroot, defines, compileFlags, the toolchain) + // (type, debug, sysroot, defines, compileFlags, the std module, the + // toolchain) // collapse into a short hash so two Configurations sharing a path but // with different compile state can't clobber each other's outputs. std::string VariantId() const { return VariantId(GetToolchain()); } @@ -628,17 +638,15 @@ export namespace Crafter { compileKey += "|B:"; compileKey += WasmBaselineFlags; } - // The mingw-w64 prefix decides what the mingw target compiles - // against, and until issue #43 clang got it as the sysroot - // itself, which put the host's own headers on the include path. - // Unkeyed, the release that fixed it kept the objects compiled - // against glibc's headers: the build after the update only - // relinked them, and failed to link as before. No prefix keeps - // the id as it was, since clang's own discovery has not changed. - if (target == "x86_64-w64-mingw32" && !toolchain.mingwPrefix.empty()) { - compileKey += "|M:"; - compileKey += toolchain.mingwPrefix.string(); - } + // The std module the units import. Its PCMs and objects record + // it by path, and units do not notice it moving: when the mingw + // prefix stopped being clang's sysroot as is (issue #43), the + // std module was rebuilt and the objects compiled against the + // host's glibc headers were only relinked, failing as before. A + // std module that moves directories, because anything it is + // compiled with changed, moves the build with it. + compileKey += "|S:"; + compileKey += StdPcmKey(*this, toolchain); // Sorted by ApplyStandardArgs, so flag order on the command line // doesn't split the cache. for (const std::string& a : projectArgs) { @@ -673,10 +681,9 @@ export namespace Crafter { std::vector requires_; }; - // Directory holding the std module PCM for this Configuration. Keyed on - // target+march because a BMI is only loadable by a TU with matching target - // features, and suffixed with the wasm flags (WasmBaselineFlags, then the - // variant's) for the same reason. + // Directory holding the std module PCM for this Configuration, + // GetCacheDir() / StdPcmKey(config, GetToolchain()): a BMI is only + // loadable by a unit compiled the way it was. fs::path StdPcmDir(const Configuration& config); // Where Build leaves this Configuration's linked artifact: the executable @@ -805,4 +812,9 @@ namespace Crafter { // compile command and of the std module's, which have to agree flag for // flag. std::string WasmCompileFlags(const Configuration& config); + // The flags BuildStdPcm compiles the std module with, after the base + // command and before the source, each with a leading space. StdPcmKey + // hashes the same string, so the command and the directory it writes to + // cannot disagree about what decides the BMI. + std::string StdPcmFlags(const Configuration& config); } diff --git a/tests/VariantId/main.cpp b/tests/VariantId/main.cpp index e658b3e..0b4472d 100644 --- a/tests/VariantId/main.cpp +++ b/tests/VariantId/main.cpp @@ -178,6 +178,46 @@ int main() { Check(mingw.VariantId(hostUsr) != mingw.VariantId(optMingw), "two mingw prefixes are two variants"); Configuration host = MakeBase(); Check(host.VariantId(discovered) == host.VariantId(hostUsr), "the mingw prefix leaves other targets' variants alone"); + + // The std module's directory is keyed the same way, and so is a + // prefix given whole against one reached through a view. + Check(StdPcmKey(mingw, discovered) != StdPcmKey(mingw, hostUsr), "a mingw prefix moves the mingw std module"); + Check(StdPcmKey(mingw, hostUsr) != StdPcmKey(mingw, optMingw), "two mingw prefixes are two std modules"); + Toolchain hostUsrWhole = hostUsr; + hostUsrWhole.mingwPrefixIsSystem = !hostUsr.mingwPrefixIsSystem; + Check(StdPcmKey(mingw, hostUsr) != StdPcmKey(mingw, hostUsrWhole), "a prefix given whole and one shown through a view are two std modules"); + Check(mingw.VariantId(hostUsr) != mingw.VariantId(hostUsrWhole), "and two variants, through the std module"); + Check(StdPcmKey(host, discovered) == StdPcmKey(host, hostUsr), "the mingw prefix leaves other targets' std modules alone"); + } + { + // Everything the std module is compiled with keys its directory, not + // only target and march. -mtune was left out, so a project built + // with -mtune=generic shared one std PCM with project.cpp's + // -mtune=native, and the two rebuilt it in turn on every run, under + // every module built against it; two sysroots did the same. + Toolchain tc; + tc.clangxx = "/app/bin/clang++"; + tc.clangVersion = "clang version 23.1.2"; + Configuration native = MakeBase(); + Configuration generic = MakeBase(); + generic.mtune = "generic"; + Check(StdPcmKey(native, tc) != StdPcmKey(generic, tc), "-mtune keys the std module"); + Configuration sysrootA = MakeBase(); + sysrootA.sysroot = "/opt/sysroot-a"; + Configuration sysrootB = MakeBase(); + sysrootB.sysroot = "/opt/sysroot-b"; + Check(StdPcmKey(sysrootA, tc) != StdPcmKey(sysrootB, tc) && StdPcmKey(sysrootA, tc) != StdPcmKey(native, tc), "the sysroot keys the std module"); + Configuration wasm = MakeBase(); + wasm.target = "wasm32-wasip1"; + wasm.sysroot = "/opt/wasi-sysroot"; + Configuration relaxed = MakeBase(); + relaxed.target = wasm.target; + relaxed.sysroot = wasm.sysroot; + relaxed.wasmVariantFlags = {"-mrelaxed-simd"}; + Check(StdPcmKey(wasm, tc) != StdPcmKey(relaxed, tc), "a wasm variant pass's flags key the std module"); + Check(StdPcmKey(native, tc) == StdPcmKey(MakeBase(), tc), "one configuration, one std module"); + Check(StdPcmKey(native, tc).starts_with(std::format("{}-{}-{}-", native.target, native.march, tc.Key())), std::format("the directory still says target, march and toolchain (got '{}')", StdPcmKey(native, tc))); + Check(native.VariantId(tc) != generic.VariantId(tc) && sysrootA.VariantId(tc) != sysrootB.VariantId(tc), "a build moves with its std module"); } if (Failures > 0) {