refactor: drop Windows host support

crafter-build is moving to a single Flatpak distribution, and there is
no Windows host in that picture: no build.cmd, no MSVC or mingw host
build, no DLL plus launcher. Delete every host-Windows path instead of
leaving it to rot unverified: the cmd.exe command runner and quoting,
the LOCALAPPDATA cache, the MSVC and mingw-host LoadProject variants
with their LIBCXX_DIR and CRAFTER_MINGW_DIR plumbing, the winsock port
probe, the LoadLibrary libclang loader, the console-width and isatty
shims, and the preprocessor guards around the Linux bodies.

The x86_64-w64-mingw32 cross-compile target stays. It is target logic
(output names, -femulated-tls, the libstdc++ link extras, copying a
dependency DLL next to a mingw exe, wine as the runner) and did not
depend on any of the removed code. The msvc target goes with the host
that was the only thing able to use it.

Three target-path warts fixed on the way: ExpectedOutput named a mingw
static library <name>.lib while the archive step wrote lib<name>.a, so
those libraries relinked on every run; the import lib is no longer
copied next to a mingw exe, since the dependency's own bin dir is
already on the link line; and llvm-ar now writes every archive, not
only the LTO ones, so Debug and Release archives are made the same way.

CRAFTER_API and Crafter.Build-Api.h existed only for the DLL boundary.
Nothing downstream used them, so they are removed rather than kept as
an empty macro nobody can verify.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-02 21:56:38 +02:00
commit 46082a3117
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
24 changed files with 138 additions and 1053 deletions

View file

@ -2,20 +2,15 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
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 __linux__
#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;
@ -43,7 +38,6 @@ namespace {
// descriptor is close-on-exec / non-inheritable so spawned clang processes
// don't keep the lock alive past our own release.
class CacheLock {
#ifdef __linux__
std::int32_t fd_ = -1;
public:
explicit CacheLock(const fs::path& cacheDir) {
@ -57,22 +51,6 @@ namespace {
::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;
};
@ -104,9 +82,6 @@ namespace {
for (std::string_view name : moduleNames) {
append(fs::path(std::format("{}.cppm", name)));
}
// Exported through `module;` preambles, so its contents land in the PCMs
// too.
append("Crafter.Build-Api.h");
return std::format("{:016x}", std::hash<std::string>{}(all));
}
@ -130,16 +105,7 @@ 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:
@ -560,499 +526,6 @@ bool Crafter::IsTopLevelProject() {
return LoadingFrames.size() <= 1;
}
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> buffer;
CommandResult result{};
std::string with = std::format("cmd /C \"{} 2>&1\"", std::string(cmd));
FILE* pipe = _popen(with.c_str(), "r");
if (!pipe) {
throw std::runtime_error("_popen() failed!");
}
while (fgets(buffer.data(), static_cast<std::int32_t>(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;
HANDLE 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 = std::format("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<std::int64_t>(static_cast<std::int64_t>(timeout.count()) * 1000LL, static_cast<std::int64_t>(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<std::int32_t>(exit);
}
result.exitCode = static_cast<std::int32_t>(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, 11> CrafterBuildModules = {
"Crafter.Build-Shader",
"Crafter.Build-Platform",
"Crafter.Build-Interface",
"Crafter.Build-Implementation",
"Crafter.Build-External",
"Crafter.Build-Clang",
"Crafter.Build-Test",
"Crafter.Build-Lint",
"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 RunBuildCommand(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);
// Re-derived under the lock: another builder may have refreshed the
// cache from its own sources while we waited.
std::string stamp = CrafterBuildSourceStamp(sourceDir, CrafterBuildModules);
bool upToDate = ReadCacheStamp(cacheDir) == stamp;
for (std::string_view name : CrafterBuildModules) {
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
fs::path pcmPath = cacheDir / std::format("{}.pcm", name);
if (!fs::exists(cppmPath)) {
throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string()));
}
if (upToDate && fs::exists(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));
}
}
// Last, so an interrupted rebuild leaves the stamp disagreeing with the
// sources and the next run starts over rather than trusting a half-built
// set of PCMs.
if (!upToDate) WriteCacheStamp(cacheDir, stamp);
}
}
Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const std::string_view> args) {
fs::path absProject = fs::canonical(projectFile);
ProjectFileGuard guard(absProject);
fs::path buildDir = absProject.parent_path() / "build";
if (!fs::exists(buildDir)) {
fs::create_directories(buildDir);
}
fs::path dllPath = buildDir / std::format("{}.dll", absProject.stem().string());
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);
// See the Linux LoadProject for why this sits here and runs every time.
ProjectFileDependencies preDeps = ResolveProjectFileDependencies(absProject, hostConfig);
bool stale = ProjectFileStale(dllPath, absProject, hostExe, preDeps);
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(), preDeps.compileFlags,
absProject.string(), crafterBuildLib.string(), preDeps.linkFlags, dllPath.string());
std::string result = RunBuildCommand(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()));
}
guard.Frame().dependencies = std::move(preDeps.configs);
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 RunBuildCommand(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`: keeping
// it in-process means one less shell round-trip and a std::error_code we
// can report directly. 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 RunBuildCommand(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);
// Re-derived under the lock: another builder may have refreshed the
// cache from its own sources while we waited.
std::string stamp = CrafterBuildSourceStamp(sourceDir, CrafterBuildModules);
bool upToDate = ReadCacheStamp(cacheDir) == stamp;
fs::path prefix = MingwPrefix();
for (std::string_view name : CrafterBuildModules) {
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
fs::path pcmPath = cacheDir / std::format("{}.pcm", name);
if (!fs::exists(cppmPath)) {
throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string()));
}
if (upToDate && fs::exists(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));
}
}
// Last, so an interrupted rebuild leaves the stamp disagreeing with the
// sources and the next run starts over rather than trusting a half-built
// set of PCMs.
if (!upToDate) WriteCacheStamp(cacheDir, stamp);
}
}
Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const std::string_view> args) {
fs::path absProject = fs::canonical(projectFile);
ProjectFileGuard guard(absProject);
fs::path buildDir = absProject.parent_path() / "build";
if (!fs::exists(buildDir)) fs::create_directories(buildDir);
fs::path dllPath = buildDir / std::format("{}.dll", absProject.stem().string());
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);
// See the Linux LoadProject for why this sits here and runs every time.
ProjectFileDependencies preDeps = ResolveProjectFileDependencies(absProject, hostConfig);
bool stale = ProjectFileStale(dllPath, absProject, hostExe, preDeps);
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 "
// Dependency archives go before -lcrafter-build: they reference
// Crafter.Build symbols, and ld.lld resolves archives in order.
"{} -o {}{} -L\"{}\" -lcrafter-build -lstdc++exp -lpthread",
prefix.string(),
cacheDir.string(), preDeps.compileFlags,
absProject.string(), dllPath.string(), preDeps.linkFlags,
hostExe.parent_path().string());
std::string result = RunBuildCommand(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()));
}
guard.Frame().dependencies = std::move(preDeps.configs);
return fn(args);
}
#endif
#ifdef __linux__
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> buffer;
CommandResult result{};
@ -1191,7 +664,7 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
// non-wasm cross builds: the driver ranks the host toolchain's own
// libc++ headers (<install>/../include/c++/v1) above --sysroot, so
// the PCM would mix host libc++ with the foreign libc — force the
// sysroot's copy (same trick as the Windows LIBCXX_DIR path)
// sysroot's copy.
if (!isWasm && !config.sysroot.empty()) {
archFlags += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1", config.sysroot);
}
@ -1348,5 +821,3 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
guard.Frame().dependencies = std::move(preDeps.configs);
return fn(args);
}
#endif