// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; // Supplied on the command line by build.sh and project.cpp (from the // CRAFTER_BUILD_VERSION environment variable or `git describe`); a build // that passes nothing is a development build. #ifndef CRAFTER_BUILD_VERSION #define CRAFTER_BUILD_VERSION "0.0.0-dev" #endif module Crafter.Build:Toolchain_impl; import std; import :Toolchain; import :Platform; namespace fs = std::filesystem; using namespace Crafter; namespace { constexpr std::string_view EnvVars[] = { "CRAFTER_BUILD_LLVM_DIR", "CRAFTER_BUILD_LIBCXX_DIR", "CRAFTER_BUILD_LIBCLANG", "CRAFTER_BUILD_WASI_SYSROOT", "CRAFTER_MINGW_DIR", }; std::string_view Env(const ToolchainInputs& in, std::string_view name) { auto it = in.env.find(name); return it == in.env.end() ? std::string_view{} : std::string_view(it->second); } // "Arch Linux clang version 22.1.8" / "clang version 22.1.8" -> "22.1.8". std::string ShortVersion(std::string_view versionLine) { constexpr std::string_view Marker = "version "; std::size_t at = versionLine.find(Marker); if (at == std::string_view::npos) return "unknown"; std::string_view rest = versionLine.substr(at + Marker.size()); std::size_t end = rest.find_first_of(" \t("); return std::string(rest.substr(0, end)); } std::string Trimmed(std::string s) { while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ')) s.pop_back(); return s; } } std::string Toolchain::LibcxxCompileFlags() const { if (libcxxDir.empty()) return {}; return std::format(" -nostdinc++ -isystem {}", (libcxxDir / "include" / "c++" / "v1").string()); } std::string Toolchain::LibcxxLinkFlags() const { std::string flags; if (!libcxxDir.empty()) { fs::path lib = libcxxDir / "lib"; flags += std::format(" -L{} -Wl,-rpath,{}", lib.string(), lib.string()); } else if (!prefix.empty() && prefix != "/usr") { flags += std::format(" -Wl,-rpath,{}", (prefix / "lib").string()); } return flags; } std::string Toolchain::Key() const { std::string identity; for (const std::string& part : {clangxx.string(), clangVersion, libcxxDir.string(), stdCppm.string()}) { identity += part; identity += '\0'; } return std::format("{}-{:08x}", ShortVersion(clangVersion), static_cast(std::hash{}(identity))); } Toolchain Crafter::ResolveToolchain(const ToolchainInputs& in) { Toolchain tc; // 1. The compiler: an explicit prefix, else one installed beside // crafter-build, else PATH. An explicit prefix without a clang++ is an // error rather than a fallback, for the same reason CRAFTER_BUILD_LIBCLANG // is exclusive: an override that silently yields to something else // cannot be used to diagnose which compiler is in play. if (std::string_view dir = Env(in, "CRAFTER_BUILD_LLVM_DIR"); !dir.empty()) { tc.prefix = fs::path(dir); if (!fs::exists(tc.prefix / "bin" / "clang++")) { tc.error = std::format("clang++ not found at {} (CRAFTER_BUILD_LLVM_DIR)", (tc.prefix / "bin" / "clang++").string()); } } else if (!in.crafterPrefix.empty() && fs::exists(in.crafterPrefix / "bin" / "clang++")) { tc.prefix = in.crafterPrefix; } if (tc.prefix.empty()) { tc.clangxx = "clang++"; tc.clang = "clang"; tc.llvmAr = "llvm-ar"; } else { tc.clangxx = tc.prefix / "bin" / "clang++"; tc.clang = tc.prefix / "bin" / "clang"; tc.llvmAr = tc.prefix / "bin" / "llvm-ar"; if (tc.prefix != "/usr" && fs::exists(tc.prefix / "include" / "clang-c" / "Index.h")) { tc.llvmIncludeDir = tc.prefix / "include"; } } // 2. libclang. The override is exclusive and unchecked on purpose: a // broken path must fail loudly when dlopened, not be papered over. if (std::string_view pinned = Env(in, "CRAFTER_BUILD_LIBCLANG"); !pinned.empty()) { tc.libclang = fs::path(pinned); } else if (!tc.prefix.empty() && fs::exists(tc.prefix / "lib" / "libclang.so")) { tc.libclang = tc.prefix / "lib" / "libclang.so"; } // 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"; 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"; } 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; } // 4. Sysroots for the cross targets that have a conventional location. if (std::string_view dir = Env(in, "CRAFTER_BUILD_WASI_SYSROOT"); !dir.empty()) { tc.wasiSysroot = fs::path(dir); } else if (!tc.prefix.empty() && fs::exists(tc.prefix / "share" / "wasi-sysroot")) { tc.wasiSysroot = tc.prefix / "share" / "wasi-sysroot"; } else { tc.wasiSysroot = "/usr/share/wasi-sysroot"; } if (std::string_view dir = Env(in, "CRAFTER_MINGW_DIR"); !dir.empty()) { tc.mingwSysroot = fs::path(dir); tc.mingwSysrootExplicit = true; } else { tc.mingwSysroot = "/usr/x86_64-w64-mingw32"; } return tc; } const Toolchain& Crafter::GetToolchain() { static const Toolchain Resolved = []() { ToolchainInputs in; for (std::string_view name : EnvVars) { if (const char* value = std::getenv(std::string(name).c_str()); value && *value) { in.env.emplace(std::string(name), value); } } // GetCrafterBuildHome throws when no share/crafter-build is found // near the executable; a toolchain can still come from PATH then. try { in.crafterPrefix = GetCrafterBuildHome().parent_path().parent_path(); } catch (const std::exception&) {} Toolchain tc = ResolveToolchain(in); if (!tc.error.empty()) return tc; CommandResult triple = RunCommandChecked(std::format("{} -print-target-triple", tc.clangxx.string())); if (triple.exitCode != 0) { tc.error = std::format("could not run {} (exit {}): {}", tc.clangxx.string(), triple.exitCode, Trimmed(triple.output)); if (tc.prefix.empty()) tc.error += "; install clang or set CRAFTER_BUILD_LLVM_DIR"; return tc; } tc.hostTriple = Trimmed(std::move(triple.output)); CommandResult version = RunCommandChecked(std::format("{} --version", tc.clangxx.string())); if (version.exitCode == 0) { std::string first = version.output.substr(0, version.output.find('\n')); tc.clangVersion = Trimmed(std::move(first)); } return tc; }(); return Resolved; } const Toolchain& Crafter::RequireToolchain() { const Toolchain& tc = GetToolchain(); if (!tc.error.empty()) throw std::runtime_error(tc.error); return tc; } std::string_view Crafter::CrafterBuildVersion() { return CRAFTER_BUILD_VERSION; }