Crafter.Build/implementations/Crafter.Build-Platform.cpp

912 lines
No EOL
37 KiB
C++

/*
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 <windows.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
#include <unistd.h>
#include <dlfcn.h>
#include <sys/wait.h>
#include <sys/file.h>
#include <fcntl.h>
#include <errno.h>
#endif
module Crafter.Build:Platform_impl;
import std;
import :Platform;
import :Clang;
import :Progress;
namespace fs = std::filesystem;
using namespace Crafter;
namespace {
// Exclusive advisory lock over one PCM cache dir (`<cache>/<target>-<march>/`),
// held for the duration of a (re)build. Concurrent crafter-build invocations —
// and the worker threads inside a single invocation — that share a cache
// serialize their builds through it: the first builder to acquire the lock
// precompiles std.pcm / the Crafter.Build-*.pcm modules while everyone else
// blocks, then re-checks freshness and reuses the finished file instead of
// re-running clang and racing to overwrite the same path. Each builder must
// pass through the lock before it reaches the compile steps that *read* those
// PCMs, so a reader never observes a half-written file.
//
// The lock lives in the OS, keyed on a `.lock` file in the cache dir, and is
// released automatically when the descriptor/handle closes — including on
// process death — so an interrupted build can never leave a stale lock. The
// descriptor is close-on-exec / non-inheritable so spawned clang processes
// don't keep the lock alive past our own release.
class CacheLock {
#ifdef CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_linux_gnu
int fd_ = -1;
public:
explicit CacheLock(const fs::path& cacheDir) {
fd_ = ::open((cacheDir / ".lock").c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0644);
if (fd_ >= 0) {
while (::flock(fd_, LOCK_EX) != 0 && errno == EINTR) {}
}
}
~CacheLock() {
if (fd_ >= 0) {
::close(fd_); // closing the descriptor drops the flock
}
}
#else
HANDLE handle_ = INVALID_HANDLE_VALUE;
public:
explicit CacheLock(const fs::path& cacheDir) {
handle_ = CreateFileW((cacheDir / ".lock").wstring().c_str(),
GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (handle_ != INVALID_HANDLE_VALUE) {
OVERLAPPED ov{};
LockFileEx(handle_, LOCKFILE_EXCLUSIVE_LOCK, 0, MAXDWORD, MAXDWORD, &ov);
}
}
~CacheLock() {
if (handle_ != INVALID_HANDLE_VALUE) {
CloseHandle(handle_); // closing the handle drops the lock
}
}
#endif
CacheLock(const CacheLock&) = delete;
CacheLock& operator=(const CacheLock&) = delete;
};
}
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:
// <prefix>/bin/crafter-build -> <prefix>/share/crafter-build (FHS install)
// <repo>/bin/crafter-build -> <repo>/share/crafter-build (build.sh)
// <repo>/bin/<arch>/crafter-build -> <repo>/share/crafter-build (self-host)
fs::path dir = hostExe.parent_path();
std::vector<fs::path> 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<char, 128> 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<int>(buffer.size()), pipe) != nullptr) {
result += buffer.data();
}
_pclose(pipe);
return result;
}
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> 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<int>(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<char> 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<DWORD>(std::min<long long>(
static_cast<long long>(timeout.count()) * 1000LL,
static_cast<long long>(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<int>(exit);
}
result.exitCode = static_cast<int>(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<std::string_view, 10> 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);
CacheLock lock(stdPcm.parent_path());
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 "";
}
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) {
CacheLock lock(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<const std::string_view> 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<const std::string_view>);
auto fn = reinterpret_cast<ProjectFn>(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<std::string> 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) {
CacheLock lock(stdPcm.parent_path());
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 "";
}
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()));
}
// 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. Held under the cache lock, so a plain
// filename is safe — no other builder writes this path concurrently.
fs::path stdCppm = stdPcm.parent_path() / "std.cppm";
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());
}
return 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(),
stdPcm.string()));
}
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) {
CacheLock lock(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;
}
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(), 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<const std::string_view> 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<exe-dir>
// -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<const std::string_view>);
auto fn = reinterpret_cast<ProjectFn>(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<char, 128> 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<char, 128> 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<char, 128> 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) {
CacheLock lock(stdPcm.parent_path());
if(config.target == "x86_64-w64-mingw32") {
std::vector<std::string> 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.
// 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::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());
}
return 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(), stdPcm.string()));
} else {
return "";
}
} else {
bool isWasm = config.target.starts_with("wasm32");
// wasi-sdk drops std.cppm at <sysroot>/share/libc++/v1/, the rest of
// the libc++ ecosystem (e.g. /opt/aarch64-rootfs) follows FHS at
// <sysroot>/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)) {
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<std::string_view, 10> 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) {
CacheLock lock(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<const std::string_view> 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<const std::string_view>);
dlerror();
auto fn = reinterpret_cast<ProjectFn>(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