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,17 +2,9 @@
// 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 <winsock2.h>
#include <ws2tcpip.h>
// <rpc.h> (pulled in transitively) defines `interface` as a macro for `struct`,
// which collides with local variables named `interface` in this TU.
#undef interface
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#endif
export module Crafter.Build:Clang_impl;
import std;
import :Clang;
@ -267,17 +259,19 @@ fs::path Crafter::StdPcmDir(const Configuration& config) {
fs::path Crafter::ExpectedOutput(const Configuration& config) {
const fs::path dir = config.BinDir();
const bool windowsTarget = config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc";
const bool mingwTarget = config.target == "x86_64-w64-mingw32";
if (config.type == ConfigurationType::Executable) {
if (config.target.starts_with("wasm32")) return dir / std::format("{}.wasm", config.outputName);
return config.target == "x86_64-w64-mingw32" ? dir / std::format("{}.exe", config.outputName) : dir / config.outputName;
return mingwTarget ? dir / std::format("{}.exe", config.outputName) : dir / config.outputName;
}
if (config.type == ConfigurationType::LibraryStatic) {
return windowsTarget ? dir / std::format("{}.lib", config.outputName) : dir / std::format("lib{}.a", config.outputName);
// The archive step writes lib<name>.a for every target, mingw included:
// llvm-ar does not care about the object format.
return dir / std::format("lib{}.a", config.outputName);
}
// LibraryDynamic — the .dll on Windows targets so an mtime check sees the
// LibraryDynamic — the .dll on the mingw target so an mtime check sees the
// freshly linked library; on Unix the .so is that file.
return windowsTarget ? dir / std::format("{}.dll", config.outputName) : dir / std::format("lib{}.so", config.outputName);
return mingwTarget ? dir / std::format("{}.dll", config.outputName) : dir / std::format("lib{}.so", config.outputName);
}
CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
@ -322,22 +316,9 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
}
if(config.type == ConfigurationType::LibraryDynamic) {
#ifdef __linux__
out.command += " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
#endif
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
#endif
} else if(config.type == ConfigurationType::Executable) {
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE";
// On Windows targets the API uses __declspec(dllimport) when consuming
// a DLL. Set the macro for executables so CRAFTER_API resolves to
// dllimport in their PCM cache (separate from the lib's PCM cache,
// which gets dllexport). Harmless if the exe doesn't actually link a
// crafter DLL — CRAFTER_API only matters at API call sites.
if (config.target == "x86_64-w64-mingw32" || config.target == "x86_64-pc-windows-msvc") {
out.command += " -D CRAFTER_BUILD_DLL_IMPORT";
}
} else {
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY";
}
@ -464,15 +445,12 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
config.ResolvePendingImports();
// Auto-detect the WASI sysroot before any compile step runs so BuildStdPcm
// and the main compile command see the same value. Linux-only — Windows
// users supply cfg.sysroot pointing at their wasi-sdk install. Covers all
// wasm32-* triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple
// subdirs handle the differences.
#ifdef __linux__
// and the main compile command see the same value. Covers all wasm32-*
// triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple subdirs
// handle the differences.
if (config.sysroot.empty() && config.target.starts_with("wasm32")) {
config.sysroot = "/usr/share/wasi-sysroot";
}
#endif
fs::path buildDir = config.BuildDir();
fs::path outputDir = config.BinDir();
@ -705,7 +683,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
const std::string& ltoCompileFlags = compile.ltoCompileFlags;
const std::string& ltoLinkFlags = compile.ltoLinkFlags;
const bool isWasm = config.target.starts_with("wasm32");
const bool useLto = compile.useLto;
std::string files;
std::unordered_set<std::string> libSet;
@ -1125,7 +1102,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
if(buildResult.repack) {
Progress::Task task(std::format("Linking {}", config.outputName));
if(config.type == ConfigurationType::Executable) {
#ifdef __linux__
if(config.target == "x86_64-w64-mingw32") {
try {
// Copy any LibraryDynamic dependency DLLs alongside
@ -1136,15 +1112,12 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
std::function<void(Configuration*)> copyDepDlls = [&](Configuration* dep) {
if (!dllSeen.insert(dep).second) return;
if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") {
fs::path depDir = dep->BinDir();
// The DLL itself (Windows resolves it from the
// exe's directory at load time) and the mingw
// import lib (so a downstream `crafter-build.exe`
// can link a fresh project.dll against it without
// hunting through sibling output dirs).
for (auto fname : {std::format("{}.dll", dep->outputName), std::format("lib{}.dll.a", dep->outputName)}) {
fs::path src = depDir / fname;
if (!fs::exists(src)) continue;
// Only the DLL: Windows resolves it from the exe's
// directory at load time. The import lib stays in
// the dependency's own bin dir, which is already
// on the link line through buildResult.libs.
fs::path src = dep->BinDir() / std::format("{}.dll", dep->outputName);
if (fs::exists(src)) {
fs::path dest = outputDir / src.filename();
if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) {
fs::copy(src, dest, fs::copy_options::overwrite_existing);
@ -1163,65 +1136,22 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
} else {
buildResult.result = RunBuildCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
}
#endif
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
if (config.target == "x86_64-w64-mingw32") {
// Windows host, mingw target: same shape as the Linux→mingw
// path (no LIBCXX_DIR / -lc++ / -nostdlib++ — those are MSVC
// libc++ flags). Copy LibraryDynamic dep DLLs + import libs
// alongside the launcher exe so Windows resolves them from
// the exe's own directory at load time. Runtime DLLs (libstdc++,
// libgcc, libwinpthread) come from msys2 on PATH.
std::unordered_set<Configuration*> dllSeen;
std::function<void(Configuration*)> copyDepDlls = [&](Configuration* dep) {
if (!dllSeen.insert(dep).second) return;
if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") {
fs::path depDir = dep->BinDir();
for (auto fname : {std::format("{}.dll", dep->outputName), std::format("lib{}.dll.a", dep->outputName)}) {
fs::path src = depDir / fname;
if (!fs::exists(src)) continue;
fs::path dest = outputDir / src.filename();
if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) {
fs::copy(src, dest, fs::copy_options::overwrite_existing);
}
}
}
for (Configuration* sub : dep->dependencies) copyDepDlls(sub);
};
for (Configuration* dep : config.dependencies) copyDepDlls(dep);
buildResult.result = RunBuildCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
} else {
std::system(std::format("copy \"%LIBCXX_DIR%\\lib\\c++.dll\" \"{}\\c++.dll\"", outputDir.string()).c_str());
buildResult.result = RunBuildCommand(std::format("{}{} -o {}.exe -fuse-ld=lld -L %LIBCXX_DIR%\\lib -lc++ -nostdinc++ -nostdlib++{}", command, files, (outputDir/config.outputName).string(), linkExtras));
}
#endif
} else if(config.type == ConfigurationType::LibraryStatic) {
#ifdef __linux__
// ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive
// index that omits their symbols, so a consumer's lld link can't
// pull the needed members. llvm-ar writes a bitcode-aware index.
buildResult.result = RunBuildCommand(std::format("{} rcs {}.a {}", useLto ? "llvm-ar" : "ar", (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
#endif
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
buildResult.result = RunBuildCommand(std::format("llvm-lib.exe {} /OUT:{}.lib", files, (outputDir/fs::path(config.outputName)).string()));
#endif
// pull the needed members. llvm-ar writes a bitcode-aware index
// and handles plain ELF and COFF objects just as well, so every
// build uses it.
buildResult.result = RunBuildCommand(std::format("llvm-ar rcs {}.a {}", (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
} else {
// LibraryDynamic. Output names follow each target's convention so
// consumers can link via the standard linker search:
// mingw: <name>.dll + lib<name>.dll.a (lld --out-implib)
// msvc: <name>.dll + <name>.lib (lld /IMPLIB)
// unix: lib<name>.so (rpath $ORIGIN)
if (config.target == "x86_64-w64-mingw32") {
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
fs::path implib = outputDir / std::format("lib{}.dll.a", config.outputName);
buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {} -Wl,--out-implib,{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras));
} else if (config.target == "x86_64-pc-windows-msvc") {
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
fs::path implib = outputDir / std::format("{}.lib", config.outputName);
buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {} -Wl,/IMPLIB:{} -fuse-ld=lld{}", command, files, dll.string(), implib.string(), linkExtras));
} else {
buildResult.result = RunBuildCommand(std::format("{}{} -shared -o {}.so -Wl,-rpath,'$ORIGIN' -fuse-ld=lld{}", command, files, (outputDir/(std::string("lib")+config.outputName)).string(), linkExtras));
}
@ -1544,11 +1474,9 @@ ArgQuery Crafter::ApplyStandardArgs(Configuration& cfg, std::span<const std::str
// includes it. (Build() also runs this once more for callers that bypassed
// ApplyStandardArgs, but doing it here makes dep PcmDirs consistent
// between the consumer's command-construction and the dep's own build.)
#ifdef __linux__
if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) {
cfg.sysroot = "/usr/share/wasi-sysroot";
}
#endif
return ArgQuery{args};
}
@ -1644,7 +1572,6 @@ Environment:
Replace '-' and '.' with '_' in the
triple. CLI --runner= overrides this.
CRAFTER_MINGW_DIR Override mingw-w64 sysroot auto-detect.
LIBCXX_DIR Windows libc++ install (MSVC ABI builds).
Exit status:
0 success / all non-skipped tests passed / lint clean / format applied
@ -1789,11 +1716,7 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
if (config.target.starts_with("wasm32")) {
const bool browserBuild = fs::exists(absDir / "index.html");
auto have = [](std::string_view exe) {
#ifdef _WIN32
std::string probe = std::format("where {} > NUL 2>&1", exe);
#else
std::string probe = std::format("command -v {} > /dev/null 2>&1", exe);
#endif
return std::system(probe.c_str()) == 0;
};
if (browserBuild) {
@ -1801,17 +1724,9 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
// user has another dev server running we shift up rather
// than letting caddy/python exit with EADDRINUSE.
auto findFreePort = [](std::int32_t basePort, std::int32_t span) -> std::int32_t {
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
static const bool WsaInit = []{ WSADATA d; return WSAStartup(MAKEWORD(2, 2), &d) == 0; }();
if (!WsaInit) return basePort;
using Sock = SOCKET;
const Sock invalid = INVALID_SOCKET;
auto closesock = [](Sock s){ closesocket(s); };
#else
using Sock = std::int32_t;
const Sock invalid = -1;
auto closesock = [](Sock s){ ::close(s); };
#endif
for (std::int32_t p = basePort; p < basePort + span; ++p) {
Sock s = ::socket(AF_INET, SOCK_STREAM, 0);
if (s == invalid) return basePort;
@ -1934,11 +1849,7 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
// Silence the backend's own banner/request logs so the
// experience is identical regardless of which server is
// picked — the line above is the only output.
#ifdef _WIN32
cmd += " > NUL 2>&1";
#else
cmd += " > /dev/null 2>&1";
#endif
return std::system(cmd.c_str()) == 0 ? 0 : 1;
}
// wasi-cli wasm — needs a standalone runtime.
@ -1952,10 +1863,6 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
return 1;
}
// Resolve to absolute — cmd.exe on Windows mishandles a leading
// "./" by trying to interpret it as a command. system() invokes
// through cmd /c, so the relative-prefixed path makes cmd error
// with "'.' is not recognized as an internal or external command".
// Run from the artifact's own directory so relative file opens
// (shaders, assets copied alongside the exe via cfg.files) resolve
// against the bin dir rather than the user's cwd. We exit the