/* 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; namespace fs = std::filesystem; using namespace Crafter; 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 return hostExe.parent_path().parent_path() / "share" / "crafter-build"; } #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) { 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; } 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)) { return 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, stdPcm.string())); } return ""; } fs::path Crafter::GetCacheDir() { if (const char* local = std::getenv("LOCALAPPDATA")) { return fs::path(local) / "crafter.build"; } throw std::runtime_error("LOCALAPPDATA not set"); } std::string Crafter::GetBaseCommand(const Configuration& config) { return std::format("clang++ -nostdinc++ -nostdlib++ -isystem %LIBCXX_DIR%\\include\\c++\\v1"); } 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", }; 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; } 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(), pcmPath.string()); CommandResult r = Crafter::RunCommandChecked(cmd); if (r.exitCode != 0) { throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output)); } } } } 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)); } } 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) { 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. return RunCommand(std::format("cp {} {}/std.cppm\nclang++ --target={} -march={} -mtune={} -femulated-tls -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {}/std.cppm -o {}", stdCc.string(), stdPcm.parent_path().string(), config.target, config.march, config.mtune, stdPcm.parent_path().string(), stdPcm.string())); } 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 -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)) { return 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, stdPcm.string())); } 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", }; 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; } 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(), pcmPath.string()); CommandResult r = Crafter::RunCommandChecked(cmd); if (r.exitCode != 0) { throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output)); } } } } 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