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:
parent
f60ba809b5
commit
46082a3117
24 changed files with 138 additions and 1053 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -21,18 +21,6 @@ std::string DeriveName(const GitSource& source) {
|
|||
}
|
||||
|
||||
std::string ShellQuote(std::string_view s) {
|
||||
// Windows cmd treats single quotes as literal characters — only double
|
||||
// quotes work. Paths don't contain " in practice, so a simple wrap
|
||||
// suffices; backslashes inside the path stay backslashes.
|
||||
#ifdef _WIN32
|
||||
std::string out = "\"";
|
||||
for (char c : s) {
|
||||
if (c == '"') out += "\\\"";
|
||||
else out += c;
|
||||
}
|
||||
out += "\"";
|
||||
return out;
|
||||
#else
|
||||
std::string out = "'";
|
||||
for (char c : s) {
|
||||
if (c == '\'') out += "'\\''";
|
||||
|
|
@ -40,7 +28,6 @@ std::string ShellQuote(std::string_view s) {
|
|||
}
|
||||
out += "'";
|
||||
return out;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string JoinOptions(std::span<const std::string> options) {
|
||||
|
|
@ -133,17 +120,14 @@ std::string BuildInjectedCMakeFlags(const fs::path& cmakeBuildDir, std::string_v
|
|||
out += " -DCMAKE_BUILD_TYPE=Release";
|
||||
out += " -DCMAKE_C_COMPILER=clang";
|
||||
out += " -DCMAKE_CXX_COMPILER=clang++";
|
||||
// Stdlib choice: libc++ for the Linux host build (matches how crafter-build
|
||||
// itself is built with -stdlib=libc++); mingw cross-compile uses libstdc++
|
||||
// shipped by the mingw-w64 toolchain so we leave the default; Windows
|
||||
// native (msvc target) similarly uses libc++ via LIBCXX_DIR.
|
||||
#ifdef __linux__
|
||||
// Stdlib choice: libc++ for Linux targets (matches how crafter-build
|
||||
// itself is built with -stdlib=libc++); the mingw cross-compile uses the
|
||||
// libstdc++ shipped by the mingw-w64 toolchain, so it keeps the default.
|
||||
if (target.ends_with("-linux-gnu") || target.ends_with("-linux-musl")) {
|
||||
out += " -DCMAKE_CXX_FLAGS=-stdlib=libc++";
|
||||
out += " -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++";
|
||||
out += " -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++";
|
||||
}
|
||||
#endif
|
||||
if (target == "x86_64-w64-mingw32") {
|
||||
out += " -DCMAKE_SYSTEM_NAME=Windows";
|
||||
out += std::format(" -DCMAKE_C_COMPILER_TARGET={}", target);
|
||||
|
|
@ -277,9 +261,9 @@ ExternalBuildResult Crafter::BuildExternal(const ExternalDependency& dep, std::s
|
|||
// (runtime-loader). The rpath stays embedded in the produced
|
||||
// binary so it picks up shared deps from the cache without any
|
||||
// LD_LIBRARY_PATH gymnastics. Static deps (.a) ignore the rpath
|
||||
// harmlessly. PE/COFF targets (mingw, msvc) resolve DLLs via PATH
|
||||
// or adjacency rather than rpath, so lld warns if we pass it.
|
||||
const bool isPe = target == "x86_64-w64-mingw32" || target == "x86_64-pc-windows-msvc";
|
||||
// harmlessly. The PE/COFF target (mingw) resolves DLLs via PATH or
|
||||
// adjacency rather than rpath, so lld warns if we pass it.
|
||||
const bool isPe = target == "x86_64-w64-mingw32";
|
||||
std::string buildDirAbs = fs::absolute(cmakeBuildDir).string();
|
||||
result.linkFlags.push_back(std::format("-L{}", buildDirAbs));
|
||||
if (!isPe) result.linkFlags.push_back(std::format("-Wl,-rpath,{}", buildDirAbs));
|
||||
|
|
|
|||
|
|
@ -3,11 +3,7 @@
|
|||
|
||||
module;
|
||||
#include <clang-c/Index.h>
|
||||
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
export module Crafter.Build:Lint_impl;
|
||||
import std;
|
||||
import :Lint;
|
||||
|
|
@ -20,20 +16,13 @@ using namespace Crafter;
|
|||
namespace {
|
||||
// ---------------- libclang ----------------
|
||||
//
|
||||
// libclang is loaded at runtime rather than linked. Linking -lclang would
|
||||
// break the mingw and MSVC cross-builds at link time and would put a
|
||||
// libclang.so.NN runtime dependency into the otherwise self-contained
|
||||
// release tarballs; the clang-c header is used for its declarations only,
|
||||
// and every call goes through a pointer resolved here. Failure to load is
|
||||
// libclang is loaded at runtime rather than linked, so a plain build never
|
||||
// needs it and the binary carries no libclang.so.NN loader dependency; the
|
||||
// clang-c header is used for its declarations only, and every call goes
|
||||
// through a pointer resolved here. Failure to load is
|
||||
// a hard error surfaced once by RunLint — there is deliberately no second,
|
||||
// weaker lexer to fall back to, because two engines disagreeing about what
|
||||
// is a comment is a worse failure than not running.
|
||||
#if defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc) || defined(CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_w64_mingw32)
|
||||
using LibHandle = HMODULE;
|
||||
LibHandle OpenLibrary(const std::string& name) { return LoadLibraryA(name.c_str()); }
|
||||
void* LibrarySymbol(LibHandle handle, const std::string& name) { return reinterpret_cast<void*>(GetProcAddress(handle, name.c_str())); }
|
||||
constexpr std::string_view LibClangNames[] = {"libclang.dll", "clang.dll"};
|
||||
#else
|
||||
using LibHandle = void*;
|
||||
LibHandle OpenLibrary(const std::string& name) { return dlopen(name.c_str(), RTLD_NOW | RTLD_LOCAL); }
|
||||
void* LibrarySymbol(LibHandle handle, const std::string& name) { return dlsym(handle, name.c_str()); }
|
||||
|
|
@ -41,7 +30,6 @@ namespace {
|
|||
"libclang.so", "libclang.so.22.1", "libclang.so.21.1", "libclang.so.20.1",
|
||||
"libclang.so.1", "libclang.dylib",
|
||||
};
|
||||
#endif
|
||||
|
||||
// Signatures come from decltype on the header's declarations, so they can
|
||||
// never drift from the real API. decltype is unevaluated, so naming the
|
||||
|
|
@ -104,8 +92,8 @@ namespace {
|
|||
LibClang LoadLibClang() {
|
||||
LibClang lib;
|
||||
std::vector<std::string> tried;
|
||||
// CRAFTER_BUILD_LIBCLANG pins an exact path, mirroring the LIBCXX_DIR /
|
||||
// CRAFTER_MINGW_DIR overrides used elsewhere. It is exclusive: pointing
|
||||
// CRAFTER_BUILD_LIBCLANG pins an exact path, mirroring the
|
||||
// CRAFTER_MINGW_DIR override used elsewhere. It is exclusive: pointing
|
||||
// it at a broken path must fail loudly rather than quietly succeed with
|
||||
// some other libclang, or the override is useless for diagnosing which
|
||||
// library is actually in play.
|
||||
|
|
@ -377,8 +365,8 @@ namespace {
|
|||
}
|
||||
if (!args.empty()) args.erase(args.begin()); // argv[0] is the compiler
|
||||
for (std::string& arg : args) {
|
||||
// Only \" unescapes. Erasing every backslash would destroy the
|
||||
// Windows include paths GetBaseCommand puts on the command line.
|
||||
// Only \" unescapes; a backslash anywhere else is part of the
|
||||
// argument.
|
||||
std::string unescaped;
|
||||
unescaped.reserve(arg.size());
|
||||
for (std::size_t i = 0; i < arg.size(); ++i) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,18 +4,8 @@
|
|||
module;
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#if defined(_WIN32)
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#ifndef STDOUT_FILENO
|
||||
#define STDOUT_FILENO _fileno(stdout)
|
||||
#endif
|
||||
#define CRAFTER_PROGRESS_ISATTY _isatty
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#define CRAFTER_PROGRESS_ISATTY isatty
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
export module Crafter.Build:Progress_impl;
|
||||
import std;
|
||||
|
|
@ -32,19 +22,10 @@ namespace {
|
|||
std::chrono::steady_clock::time_point StartTime = std::chrono::steady_clock::now();
|
||||
|
||||
std::int32_t TerminalWidth() {
|
||||
#if defined(_WIN32)
|
||||
HANDLE h = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
CONSOLE_SCREEN_BUFFER_INFO info;
|
||||
if (h != INVALID_HANDLE_VALUE && GetConsoleScreenBufferInfo(h, &info)) {
|
||||
std::int32_t w = info.srWindow.Right - info.srWindow.Left + 1;
|
||||
if (w > 0) return w;
|
||||
}
|
||||
#else
|
||||
winsize ws{};
|
||||
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
|
||||
return ws.ws_col;
|
||||
}
|
||||
#endif
|
||||
if (const char* col = std::getenv("COLUMNS")) {
|
||||
try { std::int32_t c = std::stoi(col); if (c > 0) return c; } catch (...) {}
|
||||
}
|
||||
|
|
@ -86,7 +67,7 @@ namespace Crafter::Progress {
|
|||
void SetVerbosity(Verbosity v) {
|
||||
std::lock_guard lock(StateMutex);
|
||||
ActiveVerbosity = v;
|
||||
IsTty = CRAFTER_PROGRESS_ISATTY(STDOUT_FILENO) != 0;
|
||||
IsTty = isatty(STDOUT_FILENO) != 0;
|
||||
StartTime = std::chrono::steady_clock::now();
|
||||
Total.store(0);
|
||||
Done.store(0);
|
||||
|
|
|
|||
|
|
@ -35,38 +35,11 @@ namespace {
|
|||
return out;
|
||||
}
|
||||
|
||||
// cmd.exe doesn't recognize '...' as quoting (it would pass the single
|
||||
// quotes through to the executable). Wrap in "..." for cmd; embedded "
|
||||
// is rare in paths but escape it to be safe. Backslash sequences before
|
||||
// " don't need the MS-CRT doubling rules because we go through
|
||||
// `cmd /C "..."`, which uses cmd's parser, not the CRT's argv splitter.
|
||||
std::string ShellQuoteCmd(std::string_view s) {
|
||||
std::string out;
|
||||
out.reserve(s.size() + 2);
|
||||
out.push_back('"');
|
||||
for (char c : s) {
|
||||
if (c == '"') out += "\\\"";
|
||||
else out.push_back(c);
|
||||
}
|
||||
out.push_back('"');
|
||||
return out;
|
||||
}
|
||||
|
||||
// Host-shell quoting: sh on Linux, cmd on Windows. For args/paths that
|
||||
// hit the local shell (Local runner exec, Cmd-prefix runner exec).
|
||||
std::string ShellQuoteHost(std::string_view s) {
|
||||
#ifdef _WIN32
|
||||
return ShellQuoteCmd(s);
|
||||
#else
|
||||
return ShellQuoteSh(s);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string JoinAndQuoteArgs(std::span<const std::string> args) {
|
||||
std::string out;
|
||||
for (const auto& a : args) {
|
||||
if (!out.empty()) out.push_back(' ');
|
||||
out += ShellQuoteHost(a);
|
||||
out += ShellQuoteSh(a);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
@ -191,11 +164,7 @@ TestRunner TestRunner::Cmd(std::string command) {
|
|||
TestRunner r;
|
||||
r.name = std::format("cmd:{}", command);
|
||||
r.exec = std::format("{} {{bin}} {{args}}", command);
|
||||
#ifdef _WIN32
|
||||
r.probe = std::format("where {}", command);
|
||||
#else
|
||||
r.probe = std::format("which {}", command);
|
||||
#endif
|
||||
return r;
|
||||
}
|
||||
|
||||
|
|
@ -203,11 +172,7 @@ TestRunner TestRunner::Wine() {
|
|||
TestRunner r;
|
||||
r.name = "wine";
|
||||
r.exec = "wine {bin} {args}";
|
||||
#ifdef _WIN32
|
||||
r.probe = "where wine";
|
||||
#else
|
||||
r.probe = "which wine";
|
||||
#endif
|
||||
return r;
|
||||
}
|
||||
|
||||
|
|
@ -218,11 +183,8 @@ TestRunner TestRunner::ForTarget(const Configuration& cfg) {
|
|||
// case (cfg.target defaulted to HostTarget()) without any wrapper.
|
||||
if (target == HostTarget()) return Local();
|
||||
|
||||
// Windows targets: native on a Windows host, Wine on Linux. We don't
|
||||
// distinguish mingw vs msvc here — the produced .exe runs the same way.
|
||||
if (TargetIsWindows(target)) {
|
||||
return TargetIsWindows(HostTarget()) ? Local() : Wine();
|
||||
}
|
||||
// Windows targets run under Wine; there is no Windows host.
|
||||
if (TargetIsWindows(target)) return Wine();
|
||||
|
||||
// WASI: a .wasm file isn't directly executable; wasmtime is the canonical
|
||||
// runtime. wasi-cli also works but the upstream Bytecode Alliance name is
|
||||
|
|
@ -311,7 +273,7 @@ TestResult Crafter::RunSingleTest(const Test& test, const fs::path& binary, std:
|
|||
|
||||
if (test.runner.exec.empty()) {
|
||||
// Local runner: spawn the binary directly through the host shell.
|
||||
std::string cmd = std::format("{} {}", ShellQuoteHost(binary.string()), ph["{args}"]);
|
||||
std::string cmd = std::format("{} {}", ShellQuoteSh(binary.string()), ph["{args}"]);
|
||||
r = RunCommandWithTimeout(cmd, timeout);
|
||||
} else {
|
||||
// Prefix runner (qemu-user, wasmtime, wine, ...): templated exec
|
||||
|
|
@ -337,11 +299,7 @@ TestResult Crafter::RunSingleTest(const Test& test, const fs::path& binary, std:
|
|||
|
||||
namespace {
|
||||
bool ToolOnPath(std::string_view name) {
|
||||
#ifdef _WIN32
|
||||
std::string cmd = std::format("where {} > nul 2>&1", name);
|
||||
#else
|
||||
std::string cmd = std::format("which {} > /dev/null 2>&1", name);
|
||||
#endif
|
||||
return std::system(cmd.c_str()) == 0;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,16 +2,7 @@
|
|||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
#include <cstdio>
|
||||
#if defined(_WIN32)
|
||||
#include <io.h>
|
||||
#ifndef STDOUT_FILENO
|
||||
#define STDOUT_FILENO _fileno(stdout)
|
||||
#endif
|
||||
#define CRAFTER_MAIN_ISATTY _isatty
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#define CRAFTER_MAIN_ISATTY isatty
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
|
|
@ -26,7 +17,7 @@ int main(int argc, char** argv) {
|
|||
// process blocks in its serve loop and never returns to flush. A TTY is
|
||||
// already line-buffered and its in-place redraw path flushes explicitly, so
|
||||
// only adjust when redirected.
|
||||
if (CRAFTER_MAIN_ISATTY(STDOUT_FILENO) == 0) {
|
||||
if (isatty(STDOUT_FILENO) == 0) {
|
||||
std::setvbuf(stdout, nullptr, _IOLBF, 0);
|
||||
}
|
||||
return Crafter::Run(argc, argv);
|
||||
|
|
|
|||
Loading…
Reference in a new issue