/* Crafter® Build Copyright (C) 2026 Catcrafts® Catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ module; #if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32) #include #endif #include #include #include #ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu #include #include #include #endif module Crafter.Build:Platform_impl; import std; import :Platform; import :Clang; import :Progress; namespace fs = std::filesystem; using namespace Crafter; namespace { // Used to make per-process temp filenames so concurrent crafter-build // invocations sharing the host cache dir don't collide on the same // partially-written .pcm. Cross-process uniqueness comes from the PID; // intra-process uniqueness from the counter (multiple host PCMs precompile // back-to-back, and tests can call BuildStdPcm from several threads). unsigned long CurrentProcessId() { #ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu return static_cast(getpid()); #else return static_cast(GetCurrentProcessId()); #endif } fs::path MakeTempPcmPath(const fs::path& finalPath) { static std::atomic seq{0}; return fs::path(finalPath.string() + std::format(".tmp.{}.{}", CurrentProcessId(), seq.fetch_add(1, std::memory_order_relaxed))); } // Atomically swap `tmpPath` into `finalPath`. If a parallel crafter-build // committed its own equivalent build first (identical .cppm + flags → // identical .pcm), `fs::rename` may fail on some filesystems; accept the // outcome as long as the destination now exists. std::string CommitPcm(const fs::path& tmpPath, const fs::path& finalPath) { std::error_code ec; fs::rename(tmpPath, finalPath, ec); if (!ec) return ""; std::error_code rmEc; fs::remove(tmpPath, rmEc); if (fs::exists(finalPath)) return ""; return std::format("rename {} -> {}: {}", tmpPath.string(), finalPath.string(), ec.message()); } } fs::path Crafter::GetCrafterBuildHome() { if (const char* envHome = std::getenv("CRAFTER_BUILD_HOME")) { return fs::path(envHome); } #if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32) char buf[MAX_PATH]; DWORD len = GetModuleFileNameA(nullptr, buf, MAX_PATH); if (len == 0 || len == MAX_PATH) { throw std::runtime_error("GetModuleFileName failed"); } fs::path hostExe(std::string(buf, len)); #else fs::path hostExe = fs::read_symlink("/proc/self/exe"); #endif // Walk up from the exe's directory looking for share/crafter-build with // a known module source present as a sentinel. Handles: // /bin/crafter-build -> /share/crafter-build (FHS install) // /bin/crafter-build -> /share/crafter-build (build.sh) // /bin//crafter-build -> /share/crafter-build (self-host) fs::path dir = hostExe.parent_path(); std::vector tried; for (;;) { fs::path candidate = dir / "share" / "crafter-build"; tried.push_back(candidate); std::error_code ec; if (fs::exists(candidate / "Crafter.Build.cppm", ec)) { return candidate; } fs::path parent = dir.parent_path(); if (parent == dir) break; dir = parent; } std::string msg = std::format( "could not locate crafter-build runtime assets relative to {} (set CRAFTER_BUILD_HOME). Tried:", hostExe.string()); for (const auto& p : tried) { msg += "\n " + p.string(); } throw std::runtime_error(msg); } #if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32) std::string Crafter::RunCommand(const std::string_view cmd) { Progress::EchoCommand(cmd); std::array buffer; std::string result; // Use cmd.exe to interpret redirection std::string with = "cmd /C \"" + std::string(cmd) + " 2>&1\""; FILE* pipe = _popen(with.c_str(), "r"); if (!pipe) { throw std::runtime_error("_popen() failed!"); } while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) { result += buffer.data(); } _pclose(pipe); return result; } CommandResult Crafter::RunCommandChecked(std::string_view cmd) { std::array buffer; CommandResult result{}; std::string with = "cmd /C \"" + std::string(cmd) + " 2>&1\""; FILE* pipe = _popen(with.c_str(), "r"); if (!pipe) { throw std::runtime_error("_popen() failed!"); } while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) { result.output += buffer.data(); } result.exitCode = _pclose(pipe); return result; } CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::seconds timeout) { CommandResult result{}; SECURITY_ATTRIBUTES sa{ sizeof(sa), nullptr, TRUE }; HANDLE readEnd = nullptr, writeEnd = nullptr; if (!CreatePipe(&readEnd, &writeEnd, &sa, 0)) { throw std::runtime_error("CreatePipe failed"); } SetHandleInformation(readEnd, HANDLE_FLAG_INHERIT, 0); // KILL_ON_JOB_CLOSE so the cmd.exe wrapper plus whatever it spawned // (the test binary, ssh, etc.) all die when the job handle goes away — // that's how we enforce the timeout reliably across the whole tree. HANDLE job = CreateJobObjectA(nullptr, nullptr); if (!job) { CloseHandle(readEnd); CloseHandle(writeEnd); throw std::runtime_error("CreateJobObject failed"); } JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli{}; jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; SetInformationJobObject(job, JobObjectExtendedLimitInformation, &jeli, sizeof(jeli)); STARTUPINFOA si{}; si.cb = sizeof(si); si.dwFlags = STARTF_USESTDHANDLES; si.hStdOutput = writeEnd; si.hStdError = writeEnd; si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); PROCESS_INFORMATION pi{}; // cmd /C "...": gives users the same shell-syntax surface (redirects, // &&, ||) as popen("/bin/sh -c …") does on Linux. cmd's special-case // /C parsing strips the outer quote pair when the inner text contains // additional quotes, which is the common case here. std::string wrapped = "cmd /C \"" + std::string(cmd) + "\""; std::vector cmdBuf(wrapped.begin(), wrapped.end()); cmdBuf.push_back('\0'); BOOL ok = CreateProcessA( nullptr, cmdBuf.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr, nullptr, &si, &pi); if (!ok) { CloseHandle(readEnd); CloseHandle(writeEnd); CloseHandle(job); throw std::runtime_error("CreateProcess failed"); } AssignProcessToJobObject(job, pi.hProcess); ResumeThread(pi.hThread); // Drop our writer ref so the read end sees EOF once the child (its // own dup of writeEnd) exits. CloseHandle(writeEnd); std::string output; std::jthread reader([&output, readEnd]() { char buf[4096]; DWORD n = 0; while (ReadFile(readEnd, buf, sizeof(buf), &n, nullptr) && n > 0) { output.append(buf, n); } }); DWORD waitMs = static_cast(std::min( static_cast(timeout.count()) * 1000LL, static_cast(INFINITE) - 1)); DWORD waitResult = WaitForSingleObject(pi.hProcess, waitMs); if (waitResult == WAIT_TIMEOUT) { TerminateJobObject(job, 1); WaitForSingleObject(pi.hProcess, INFINITE); result.timedOut = true; result.exitCode = 124; } else { DWORD exit = 0; GetExitCodeProcess(pi.hProcess, &exit); // NTSTATUS error severity (top two bits set) means the process // terminated by exception — STATUS_ACCESS_VIOLATION 0xC0000005, // STATUS_STACK_OVERFLOW 0xC00000FD, etc. Surface those as crashes // so the runner shows 💥 instead of a numeric exit code. if ((exit & 0xC0000000) == 0xC0000000) { result.crashed = true; result.signal = static_cast(exit); } result.exitCode = static_cast(exit); } reader.join(); CloseHandle(readEnd); CloseHandle(pi.hThread); CloseHandle(pi.hProcess); CloseHandle(job); result.output = std::move(output); return result; } fs::path Crafter::GetCacheDir() { if (const char* local = std::getenv("LOCALAPPDATA")) { return fs::path(local) / "crafter.build"; } throw std::runtime_error("LOCALAPPDATA not set"); } namespace { constexpr std::array kCrafterBuildModules = { "Crafter.Build-Shader", "Crafter.Build-Platform", "Crafter.Build-Interface", "Crafter.Build-Implementation", "Crafter.Build-External", "Crafter.Build-Clang", "Crafter.Build-Test", "Crafter.Build-Progress", "Crafter.Build-Asset", "Crafter.Build", }; } #endif #ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc // Native Windows build via build.cmd: MSVC ABI + libc++ from %LIBCXX_DIR%. std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) { std::string libcxx = std::getenv("LIBCXX_DIR"); std::string stdcppm = std::format("{}\\modules\\c++\\v1\\std.cppm", libcxx); if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdcppm)) { fs::path tmpPcm = MakeTempPcmPath(stdPcm); std::string out = RunCommand(std::format("clang++ --target={} -march={} -mtune={} -isystem %LIBCXX_DIR%\\include\\c++\\v1 -nostdinc++ -nostdlib++ -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile %LIBCXX_DIR%\\modules\\c++\\v1\\std.cppm -o {}", config.target, config.march, config.mtune, tmpPcm.string())); if (!out.empty()) { std::error_code ec; fs::remove(tmpPcm, ec); return out; } return CommitPcm(tmpPcm, stdPcm); } return ""; } std::string Crafter::GetBaseCommand(const Configuration& config) { return std::format("clang++ -nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1"); } namespace { void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) { for (std::string_view name : kCrafterBuildModules) { fs::path cppmPath = sourceDir / (std::string(name) + ".cppm"); fs::path pcmPath = cacheDir / (std::string(name) + ".pcm"); if (!fs::exists(cppmPath)) { throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string())); } if (fs::exists(pcmPath) && fs::last_write_time(cppmPath) < fs::last_write_time(pcmPath)) { continue; } fs::path tmpPcm = MakeTempPcmPath(pcmPath); std::string cmd = std::format( "clang++ --target=x86_64-pc-windows-msvc -march=native -mtune=native " "-std=c++26 -O3 -D CRAFTER_BUILD_DLL_IMPORT " "-nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1 " "-Wno-reserved-identifier -Wno-reserved-module-identifier " "-fprebuilt-module-path={} " "--precompile {} -o {}", cacheDir.string(), cppmPath.string(), tmpPcm.string()); CommandResult r = Crafter::RunCommandChecked(cmd); if (r.exitCode != 0) { std::error_code ec; fs::remove(tmpPcm, ec); throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output)); } if (std::string err = CommitPcm(tmpPcm, pcmPath); !err.empty()) { throw std::runtime_error(std::format("Failed to commit {} pcm: {}", name, err)); } } } } Configuration Crafter::LoadProject(const fs::path& projectFile, std::span args) { fs::path absProject = fs::canonical(projectFile); fs::path buildDir = absProject.parent_path() / "build"; if (!fs::exists(buildDir)) { fs::create_directories(buildDir); } fs::path dllPath = buildDir / (absProject.stem().string() + ".dll"); char hostExeBuf[MAX_PATH]; DWORD hostExeLen = GetModuleFileNameA(nullptr, hostExeBuf, MAX_PATH); if (hostExeLen == 0 || hostExeLen == MAX_PATH) { throw std::runtime_error("GetModuleFileName failed"); } fs::path hostExe(std::string(hostExeBuf, hostExeLen)); fs::path sourceDir = Crafter::GetCrafterBuildHome(); Configuration hostConfig; hostConfig.target = "x86_64-pc-windows-msvc"; hostConfig.march = "native"; hostConfig.mtune = "native"; fs::path cacheDir = GetCacheDir() / std::format("{}-{}", hostConfig.target, hostConfig.march); if (!fs::exists(cacheDir)) fs::create_directories(cacheDir); std::string stdResult = BuildStdPcm(hostConfig, cacheDir / "std.pcm"); if (!stdResult.empty()) { throw std::runtime_error(std::format("Failed to build std.pcm: {}", stdResult)); } EnsureCrafterBuildPcms(sourceDir, cacheDir); bool stale = !fs::exists(dllPath) || fs::last_write_time(dllPath) < fs::last_write_time(absProject) || fs::last_write_time(dllPath) < fs::last_write_time(hostExe); if (stale) { fs::path crafterBuildLib = hostExe.parent_path() / "crafter-build.lib"; std::string compileCmd = std::format( "clang++ --target=x86_64-pc-windows-msvc -march=native -mtune=native " "-std=c++26 -shared -O3 -Wno-return-type-c-linkage -fuse-ld=lld " "-D CRAFTER_BUILD_DLL_IMPORT " "-nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1 " "-fprebuilt-module-path={} " "-Wl,/EXPORT:CrafterBuildProject " "{} {} -o {} -L %LIBCXX_DIR%\\lib -lc++", cacheDir.string(), absProject.string(), crafterBuildLib.string(), dllPath.string()); std::string result = RunCommand(compileCmd); if (!result.empty()) { throw std::runtime_error(std::format("Failed to compile project {}: {}", absProject.string(), result)); } } // project.dll links libc++ dynamically (-lc++) and the dll typically lives // under %LIBCXX_DIR%\lib. Add it to the loader's search path so the user // doesn't have to put it on PATH manually. if (const char* libcxx = std::getenv("LIBCXX_DIR"); libcxx && *libcxx) { fs::path libcxxLib = fs::path(libcxx) / "lib"; SetDllDirectoryA(libcxxLib.string().c_str()); } HMODULE handle = LoadLibraryA(dllPath.string().c_str()); if (!handle) { throw std::runtime_error(std::format("Failed to load project {}: error {}", dllPath.string(), GetLastError())); } using ProjectFn = Configuration (*)(std::span); auto fn = reinterpret_cast(GetProcAddress(handle, "CrafterBuildProject")); if (!fn) { throw std::runtime_error(std::format("CrafterBuildProject not found in {}: error {}", dllPath.string(), GetLastError())); } return fn(args); } #endif #ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32 // Linux→mingw cross-compile run on Windows: mingw ABI + libstdc++ from msys2. // Resolves the mingw sysroot at C:\msys64\ucrt64 by default (UCRT runtime, // matching the Arch cross-compile that produced this binary). User can // override with CRAFTER_MINGW_DIR if they have mingw-w64 installed elsewhere // or want the legacy MSVCRT mingw at C:\msys64\mingw64. clang itself is the // user's existing standalone clang on PATH. namespace { fs::path MingwPrefix() { if (const char* env = std::getenv("CRAFTER_MINGW_DIR"); env && *env) { return fs::path(env); } return "C:\\msys64\\ucrt64"; } std::string MingwGccVersion() { fs::path includeRoot = MingwPrefix() / "include" / "c++"; if (!fs::exists(includeRoot)) { throw std::runtime_error(std::format( "mingw-w64 not found at {} (install msys2 mingw-w64-x86_64-toolchain or set CRAFTER_MINGW_DIR)", MingwPrefix().string())); } std::vector versions; for (const auto& entry : fs::directory_iterator(includeRoot)) { if (entry.is_directory()) versions.push_back(entry.path().filename().string()); } if (versions.empty()) { throw std::runtime_error(std::format("no C++ versions under {}", includeRoot.string())); } std::sort(versions.begin(), versions.end(), std::greater<>()); return versions.front(); } } std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) { if (config.target == "x86_64-pc-windows-msvc") { // MSVC target on mingw host: same MSVC libc++ logic as the // native-MSVC LoadProject path. User must have LIBCXX_DIR pointing // at a Windows libc++ install. const char* libcxxEnv = std::getenv("LIBCXX_DIR"); if (!libcxxEnv || !*libcxxEnv) { return "MSVC target requires LIBCXX_DIR pointing at a Windows libc++ install"; } std::string stdcppm = std::format("{}\\modules\\c++\\v1\\std.cppm", libcxxEnv); if (fs::exists(stdPcm) && fs::last_write_time(stdPcm) >= fs::last_write_time(stdcppm)) { return ""; } fs::path tmpPcm = MakeTempPcmPath(stdPcm); std::string out = RunCommand(std::format( "clang++ --target={} -march={} -mtune={} -isystem %LIBCXX_DIR%\\include\\c++\\v1 " "-nostdinc++ -nostdlib++ -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier " "--precompile %LIBCXX_DIR%\\modules\\c++\\v1\\std.cppm -o {}", config.target, config.march, config.mtune, tmpPcm.string())); if (!out.empty()) { std::error_code ec; fs::remove(tmpPcm, ec); return out; } return CommitPcm(tmpPcm, stdPcm); } // Default: mingw target. Look up mingw-w64 libstdc++ via the msys2 prefix. fs::path prefix = MingwPrefix(); fs::path stdCc = prefix / "include" / "c++" / MingwGccVersion() / "bits" / "std.cc"; if (!fs::exists(stdCc)) { return std::format("std.cc not found at {}", stdCc.string()); } if (fs::exists(stdPcm) && fs::last_write_time(stdPcm) >= fs::last_write_time(stdCc)) { return ""; } // Copy std.cc → std.cppm in C++ rather than via cmd's `copy /Y` because // `copy` always prints "1 file(s) copied." to stdout and RunCommand // treats any output as an error. Per-PID filename so concurrent // crafter-build invocations don't see each other's half-written copies. fs::path stdCppm = stdPcm.parent_path() / std::format("std.cppm.{}", CurrentProcessId()); std::error_code ec; fs::copy_file(stdCc, stdCppm, fs::copy_options::overwrite_existing, ec); if (ec) { return std::format("copy {} -> {}: {}", stdCc.string(), stdCppm.string(), ec.message()); } fs::path tmpPcm = MakeTempPcmPath(stdPcm); std::string out = RunCommand(std::format( "clang++ --target={} -march={} -mtune={} " "--sysroot=\"{}\" -femulated-tls " "-O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier " "--precompile \"{}\" -o {}", config.target, config.march, config.mtune, prefix.string(), stdCppm.string(), tmpPcm.string())); std::error_code rmEc; fs::remove(stdCppm, rmEc); if (!out.empty()) { fs::remove(tmpPcm, rmEc); return out; } return CommitPcm(tmpPcm, stdPcm); } std::string Crafter::GetBaseCommand(const Configuration& config) { if (config.target == "x86_64-pc-windows-msvc") { return std::format("clang++ -nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1"); } return std::format("clang++ --sysroot=\"{}\" -femulated-tls", MingwPrefix().string()); } namespace { void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) { fs::path prefix = MingwPrefix(); for (std::string_view name : kCrafterBuildModules) { fs::path cppmPath = sourceDir / (std::string(name) + ".cppm"); fs::path pcmPath = cacheDir / (std::string(name) + ".pcm"); if (!fs::exists(cppmPath)) { throw std::runtime_error(std::format( "module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string())); } if (fs::exists(pcmPath) && fs::last_write_time(cppmPath) < fs::last_write_time(pcmPath)) { continue; } fs::path tmpPcm = MakeTempPcmPath(pcmPath); std::string cmd = std::format( "clang++ --target=x86_64-w64-mingw32 -march=native -mtune=native " "--sysroot=\"{}\" -femulated-tls " "-std=c++26 -O3 -D CRAFTER_BUILD_DLL_IMPORT " "-Wno-reserved-identifier -Wno-reserved-module-identifier " "-fprebuilt-module-path={} " "--precompile {} -o {}", prefix.string(), cacheDir.string(), cppmPath.string(), tmpPcm.string()); CommandResult r = Crafter::RunCommandChecked(cmd); if (r.exitCode != 0) { std::error_code ec; fs::remove(tmpPcm, ec); throw std::runtime_error(std::format( "Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output)); } if (std::string err = CommitPcm(tmpPcm, pcmPath); !err.empty()) { throw std::runtime_error(std::format( "Failed to commit {} pcm: {}", name, err)); } } } } Configuration Crafter::LoadProject(const fs::path& projectFile, std::span args) { fs::path absProject = fs::canonical(projectFile); fs::path buildDir = absProject.parent_path() / "build"; if (!fs::exists(buildDir)) fs::create_directories(buildDir); fs::path dllPath = buildDir / (absProject.stem().string() + ".dll"); char hostExeBuf[MAX_PATH]; DWORD hostExeLen = GetModuleFileNameA(nullptr, hostExeBuf, MAX_PATH); if (hostExeLen == 0 || hostExeLen == MAX_PATH) { throw std::runtime_error("GetModuleFileName failed"); } fs::path hostExe(std::string(hostExeBuf, hostExeLen)); fs::path sourceDir = Crafter::GetCrafterBuildHome(); Configuration hostConfig; hostConfig.target = "x86_64-w64-mingw32"; hostConfig.march = "native"; hostConfig.mtune = "native"; fs::path cacheDir = GetCacheDir() / std::format("{}-{}", hostConfig.target, hostConfig.march); if (!fs::exists(cacheDir)) fs::create_directories(cacheDir); std::string stdResult = BuildStdPcm(hostConfig, cacheDir / "std.pcm"); if (!stdResult.empty()) { throw std::runtime_error(std::format("Failed to build std.pcm: {}", stdResult)); } EnsureCrafterBuildPcms(sourceDir, cacheDir); bool stale = !fs::exists(dllPath) || fs::last_write_time(dllPath) < fs::last_write_time(absProject) || fs::last_write_time(dllPath) < fs::last_write_time(hostExe); if (stale) { // The import lib lives next to the launcher exe (Build()'s mingw // copy step puts libcrafter-build.dll.a there) so -L // -lcrafter-build resolves it. fs::path prefix = MingwPrefix(); std::string compileCmd = std::format( "clang++ --target=x86_64-w64-mingw32 -march=native -mtune=native " "--sysroot=\"{}\" -femulated-tls " "-std=c++26 -shared -O3 -Wno-return-type-c-linkage -fuse-ld=lld " "-D CRAFTER_BUILD_DLL_IMPORT " "-fprebuilt-module-path={} " // mingw-lld doesn't accept /EXPORT:NAME — use the catch-all so // CrafterBuildProject is reachable via GetProcAddress without // forcing project.cpp templates to add __declspec(dllexport). "-Wl,--export-all-symbols " "{} -o {} -L\"{}\" -lcrafter-build -lstdc++exp -lpthread", prefix.string(), cacheDir.string(), absProject.string(), dllPath.string(), hostExe.parent_path().string()); std::string result = RunCommand(compileCmd); if (!result.empty()) { throw std::runtime_error(std::format( "Failed to compile project {}: {}", absProject.string(), result)); } } // project.dll is a mingw DLL linking libstdc++-6.dll / libgcc_s_seh-1.dll / // libwinpthread-1.dll dynamically (LoadProject's compile uses -lstdc++exp // -lpthread, not -static). We already know where they live from // MingwPrefix(), so add bin/ to the loader's search path here instead of // requiring the user to prepend it to PATH manually. fs::path mingwBin = MingwPrefix() / "bin"; SetDllDirectoryA(mingwBin.string().c_str()); HMODULE handle = LoadLibraryA(dllPath.string().c_str()); if (!handle) { throw std::runtime_error(std::format( "Failed to load project {}: error {}", dllPath.string(), GetLastError())); } using ProjectFn = Configuration (*)(std::span); auto fn = reinterpret_cast(GetProcAddress(handle, "CrafterBuildProject")); if (!fn) { throw std::runtime_error(std::format( "CrafterBuildProject not found in {}: error {}", dllPath.string(), GetLastError())); } return fn(args); } #endif #ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu std::string Crafter::RunCommand(const std::string_view cmd) { Progress::EchoCommand(cmd); std::array buffer; std::string result; std::string with = std::string(cmd) + " 2>&1"; // Open pipe to file FILE* pipe = popen(with.c_str(), "r"); if (!pipe) throw std::runtime_error("popen() failed!"); // Read till end of process: while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { result += buffer.data(); } // Close pipe pclose(pipe); return result; } CommandResult Crafter::RunCommandChecked(std::string_view cmd) { std::array buffer; CommandResult result{}; std::string with = std::string(cmd) + " 2>&1"; FILE* pipe = popen(with.c_str(), "r"); if (!pipe) throw std::runtime_error("popen() failed!"); while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { result.output += buffer.data(); } int status = pclose(pipe); if (WIFEXITED(status)) { result.exitCode = WEXITSTATUS(status); } else if (WIFSIGNALED(status)) { result.signal = WTERMSIG(status); result.exitCode = 128 + result.signal; result.crashed = true; } else { result.exitCode = -1; } return result; } CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::seconds timeout) { std::array buffer; CommandResult result{}; std::string wrapped = std::format( "timeout --kill-after=2 {} {} 2>&1", timeout.count(), cmd); FILE* pipe = popen(wrapped.c_str(), "r"); if (!pipe) throw std::runtime_error("popen() failed!"); while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { result.output += buffer.data(); } int status = pclose(pipe); if (WIFEXITED(status)) { int code = WEXITSTATUS(status); if (code == 124) { result.timedOut = true; result.exitCode = 124; } else if (code >= 128) { result.signal = code - 128; result.exitCode = code; result.crashed = true; } else { result.exitCode = code; } } else if (WIFSIGNALED(status)) { result.signal = WTERMSIG(status); result.exitCode = 128 + result.signal; result.crashed = true; } else { result.exitCode = -1; } return result; } std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) { if(config.target == "x86_64-w64-mingw32") { std::vector folders; // Iterate through the directory and collect all subdirectories for (const auto& entry : fs::directory_iterator("/usr/x86_64-w64-mingw32/include/c++")) { if (entry.is_directory()) { folders.push_back(entry.path().filename().string()); } } // Sort the folders by version in descending order std::sort(folders.begin(), folders.end(), [](const std::string& a, const std::string& b) { return std::lexicographical_compare(b.begin(), b.end(), a.begin(), a.end()); }); std::string mingWversion = folders.front(); fs::path stdCc = fs::path(std::format("/usr/x86_64-w64-mingw32/include/c++/{}/bits/std.cc", mingWversion)); if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCc)) { // -femulated-tls keeps PCM TLS access matching libstdc++'s emutls // definitions; mismatch surfaces as undefined std::__once_callable. // Per-PID std.cppm and a tmp .pcm + atomic rename so concurrent // crafter-build invocations sharing the cache dir don't read each // other's half-written files. fs::path stdCppm = stdPcm.parent_path() / std::format("std.cppm.{}", CurrentProcessId()); std::error_code copyEc; fs::copy_file(stdCc, stdCppm, fs::copy_options::overwrite_existing, copyEc); if (copyEc) { return std::format("copy {} -> {}: {}", stdCc.string(), stdCppm.string(), copyEc.message()); } fs::path tmpPcm = MakeTempPcmPath(stdPcm); std::string out = RunCommand(std::format("clang++ --target={} -march={} -mtune={} -femulated-tls -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, config.march, config.mtune, stdCppm.string(), tmpPcm.string())); std::error_code rmEc; fs::remove(stdCppm, rmEc); if (!out.empty()) { fs::remove(tmpPcm, rmEc); return out; } return CommitPcm(tmpPcm, stdPcm); } else { return ""; } } else { bool isWasm = config.target.starts_with("wasm32"); // wasi-sdk drops std.cppm at /share/libc++/v1/, the rest of // the libc++ ecosystem (e.g. /opt/aarch64-rootfs) follows FHS at // /usr/share/libc++/v1/. std::string stdCppm; if (isWasm) { stdCppm = std::format("{}/share/libc++/v1/std.cppm", config.sysroot); } else if (config.sysroot.empty()) { stdCppm = "/usr/share/libc++/v1/std.cppm"; } else { stdCppm = std::format("{}/usr/share/libc++/v1/std.cppm", config.sysroot); } std::string sysrootFlag = config.sysroot.empty() ? std::string() : std::format(" --sysroot={}", config.sysroot); // wasm32 rejects -march. wasi-libc++ headers require these baselines // to compile: setjmp.h needs -mllvm -wasm-enable-sjlj (lowered to wasm // EH); signal.h requires the emulation define; and EH itself isn't // wired up so -fno-exceptions stays. std::string archFlags = isWasm ? std::string(" -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL") : std::format(" -march={} -mtune={}", config.march, config.mtune); if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCppm)) { fs::path tmpPcm = MakeTempPcmPath(stdPcm); std::string out = RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, sysrootFlag, archFlags, stdCppm, tmpPcm.string())); if (!out.empty()) { std::error_code ec; fs::remove(tmpPcm, ec); return out; } return CommitPcm(tmpPcm, stdPcm); } else { return ""; } } } fs::path Crafter::GetCacheDir() { if (const char* xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg) { return fs::path(xdg) / "crafter.build"; } if (const char* home = std::getenv("HOME")) { return fs::path(home) / ".cache" / "crafter.build"; } throw std::runtime_error("Neither XDG_CACHE_HOME nor HOME set"); } std::string Crafter::GetBaseCommand(const Configuration& config) { std::string stdlib; if(config.target == "x86_64-w64-mingw32") { stdlib = ""; } else { stdlib = "-stdlib=libc++"; } return std::format("clang++ {}", stdlib); } namespace { constexpr std::array kCrafterBuildModules = { "Crafter.Build-Shader", "Crafter.Build-Platform", "Crafter.Build-Interface", "Crafter.Build-Implementation", "Crafter.Build-External", "Crafter.Build-Clang", "Crafter.Build-Test", "Crafter.Build-Progress", "Crafter.Build-Asset", "Crafter.Build", }; void EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir) { for (std::string_view name : kCrafterBuildModules) { fs::path cppmPath = sourceDir / (std::string(name) + ".cppm"); fs::path pcmPath = cacheDir / (std::string(name) + ".pcm"); if (!fs::exists(cppmPath)) { throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string())); } if (fs::exists(pcmPath) && fs::last_write_time(cppmPath) < fs::last_write_time(pcmPath)) { continue; } fs::path tmpPcm = MakeTempPcmPath(pcmPath); std::string cmd = std::format( "clang++ --target=x86_64-pc-linux-gnu -march=native -mtune=native " "-std=c++26 -stdlib=libc++ -O3 " "-Wno-reserved-identifier -Wno-reserved-module-identifier " "-fprebuilt-module-path={} " "--precompile {} -o {}", cacheDir.string(), cppmPath.string(), tmpPcm.string()); CommandResult r = Crafter::RunCommandChecked(cmd); if (r.exitCode != 0) { std::error_code ec; fs::remove(tmpPcm, ec); throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output)); } if (std::string err = CommitPcm(tmpPcm, pcmPath); !err.empty()) { throw std::runtime_error(std::format("Failed to commit {} pcm: {}", name, err)); } } } } Configuration Crafter::LoadProject(const fs::path& projectFile, std::span args) { fs::path absProject = fs::canonical(projectFile); fs::path buildDir = absProject.parent_path() / "build"; if (!fs::exists(buildDir)) { fs::create_directories(buildDir); } fs::path soPath = buildDir / (absProject.stem().string() + ".so"); fs::path hostExe = fs::read_symlink("/proc/self/exe"); fs::path sourceDir = Crafter::GetCrafterBuildHome(); Configuration hostConfig; hostConfig.target = "x86_64-pc-linux-gnu"; hostConfig.march = "native"; hostConfig.mtune = "native"; fs::path cacheDir = GetCacheDir() / std::format("{}-{}", hostConfig.target, hostConfig.march); if (!fs::exists(cacheDir)) fs::create_directories(cacheDir); std::string stdResult = BuildStdPcm(hostConfig, cacheDir / "std.pcm"); if (!stdResult.empty()) { throw std::runtime_error(std::format("Failed to build std.pcm: {}", stdResult)); } EnsureCrafterBuildPcms(sourceDir, cacheDir); bool stale = !fs::exists(soPath) || fs::last_write_time(soPath) < fs::last_write_time(absProject) || fs::last_write_time(soPath) < fs::last_write_time(hostExe); if (stale) { std::string compileCmd = std::format( "clang++ --target=x86_64-pc-linux-gnu -march=native -mtune=native " "-std=c++26 -stdlib=libc++ -shared -fPIC -O3 " "-Wno-return-type-c-linkage " "-fprebuilt-module-path={} " "{} -o {}", cacheDir.string(), absProject.string(), soPath.string()); std::string result = RunCommand(compileCmd); if (!result.empty()) { throw std::runtime_error(std::format("Failed to compile project {}: {}", absProject.string(), result)); } } void* handle = dlopen(soPath.c_str(), RTLD_NOW | RTLD_GLOBAL); if (!handle) { throw std::runtime_error(std::format("Failed to load project {}: {}", soPath.string(), dlerror())); } using ProjectFn = Configuration (*)(std::span); dlerror(); auto fn = reinterpret_cast(dlsym(handle, "CrafterBuildProject")); if (const char* err = dlerror()) { throw std::runtime_error(std::format("CrafterBuildProject not found in {}: {}", soPath.string(), err)); } return fn(args); } #endif