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

@ -29,7 +29,6 @@ package() {
install -Dm644 lib/libcrafter-build.a "$pkgdir/usr/lib/libcrafter-build.a"
install -dm755 "$pkgdir/usr/share/crafter-build"
install -m644 share/crafter-build/*.cppm "$pkgdir/usr/share/crafter-build/"
install -m644 share/crafter-build/*.h "$pkgdir/usr/share/crafter-build/"
install -dm755 "$pkgdir/usr/share/crafter-build/wasi-runtime"
install -m644 share/crafter-build/wasi-runtime/runtime.js "$pkgdir/usr/share/crafter-build/wasi-runtime/"
install -m644 share/crafter-build/wasi-runtime/index.html.in "$pkgdir/usr/share/crafter-build/wasi-runtime/"

137
build.cmd
View file

@ -1,137 +0,0 @@
@REM SPDX-License-Identifier: LGPL-3.0-only
@REM SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
@echo OFF
mkdir build 2>nul
mkdir bin 2>nul
mkdir lib 2>nul
mkdir share\crafter-build 2>nul
mkdir share\crafter-build\wasi-runtime 2>nul
copy /Y wasi-runtime\runtime.js share\crafter-build\wasi-runtime\
copy /Y wasi-runtime\index.html.in share\crafter-build\wasi-runtime\
copy /Y interfaces\Crafter.Build.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Shader.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Platform.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Interface.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Implementation.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-External.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Clang.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Test.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Lint.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Progress.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Asset.cppm share\crafter-build\
copy /Y interfaces\Crafter.Build-Api.h share\crafter-build\
if not exist .\build\glslang\NUL git clone https://github.com/KhronosGroup/glslang.git .\build\glslang
set useLibcCommon=-nostdinc++ -nostdlib++
set useLibcLinker=-L %LIBCXX_DIR%\lib -lc++ %useLibcCommon%
set useLibcSource=-isystem %LIBCXX_DIR%\include\c++\v1 %useLibcCommon%
set buildDir=%CD%\build
if "%CRAFTER_BUILD_MARCH%"=="" set CRAFTER_BUILD_MARCH=native
if "%CRAFTER_BUILD_MTUNE%"=="" set CRAFTER_BUILD_MTUNE=native
cd .\build\glslang
cmake -G Ninja -B build ^
-DCMAKE_C_COMPILER=clang ^
-DCMAKE_CXX_COMPILER=clang++ ^
-DCMAKE_CXX_FLAGS="%useLibcSource%" ^
-DCMAKE_EXE_LINKER_FLAGS="%useLibcLinker%" ^
-DCMAKE_SHARED_LINKER_FLAGS="%useLibcLinker%" ^
-DCMAKE_BUILD_TYPE=Release ^
-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY="%buildDir%" ^
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY="%buildDir%" ^
-DENABLE_OPT=OFF
cmake --build build --config Release
cd ..\..\
set common_options=-I.\build\glslang -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -fprebuilt-module-path=.\build -D CRAFTER_BUILD_CONFIGURATION_TARGET_x86_64_pc_windows_msvc -D CRAFTER_BUILD_CONFIGURATION_TYPE_EXECUTABLE -D _CRT_SECURE_NO_WARNINGS %useLibcSource% -c
clang++ %useLibcSource% -std=c++26 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile %LIBCXX_DIR%\modules\c++\v1\std.cppm -o .\build\std.pcm
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Shader.cppm -o .\build\Crafter.Build-Shader.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Platform.cppm -o .\build\Crafter.Build-Platform.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Interface.cppm -o .\build\Crafter.Build-Interface.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Implementation.cppm -o .\build\Crafter.Build-Implementation.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-External.cppm -o .\build\Crafter.Build-External.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Clang.cppm -o .\build\Crafter.Build-Clang.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Test.cppm -o .\build\Crafter.Build-Test.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Lint.cppm -o .\build\Crafter.Build-Lint.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Progress.cppm -o .\build\Crafter.Build-Progress.o
REM Asset partition: bootstrap compiles it WITHOUT CRAFTER_BUILD_HAS_ASSET so
REM CompressAsset takes the stub branch (no Crafter.Asset PCM exists yet).
clang++ %common_options% -fmodule-output interfaces\Crafter.Build-Asset.cppm -o .\build\Crafter.Build-Asset.o
clang++ %common_options% -fmodule-output interfaces\Crafter.Build.cppm -o .\build\Crafter.Build.o
clang++ %common_options% .\implementations\Crafter.Build-Shader.cpp -o .\build\Crafter.Build-Shader_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Platform.cpp -o .\build\Crafter.Build-Platform_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Interface.cpp -o .\build\Crafter.Build-Interface_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Implementation.cpp -o .\build\Crafter.Build-Implementation_impl.o
clang++ %common_options% .\implementations\Crafter.Build-External.cpp -o .\build\Crafter.Build-External_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Clang.cpp -o .\build\Crafter.Build-Clang_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Test.cpp -o .\build\Crafter.Build-Test_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Lint.cpp -o .\build\Crafter.Build-Lint_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Progress.cpp -o .\build\Crafter.Build-Progress_impl.o
clang++ %common_options% .\implementations\Crafter.Build-Asset.cpp -o .\build\Crafter.Build-Asset_impl.o
clang++ %common_options% .\implementations\main.cpp -o .\build\main.o
REM Step 1: link all impl .o files into crafter-build.dll, generating crafter-build.lib import lib
clang++ %useLibcLinker% -shared -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -L.\build -fuse-ld=lld ^
-Wl,/IMPLIB:.\bin\crafter-build.lib ^
-lSPIRV -lGenericCodeGen -lglslang -lOSDependent -lMachineIndependent -lglslang-default-resource-limits ^
.\build\Crafter.Build-Shader.o ^
.\build\Crafter.Build-Platform.o ^
.\build\Crafter.Build-Interface.o ^
.\build\Crafter.Build-Implementation.o ^
.\build\Crafter.Build-External.o ^
.\build\Crafter.Build-Clang.o ^
.\build\Crafter.Build-Test.o ^
.\build\Crafter.Build-Lint.o ^
.\build\Crafter.Build-Progress.o ^
.\build\Crafter.Build-Asset.o ^
.\build\Crafter.Build.o ^
.\build\Crafter.Build-Shader_impl.o ^
.\build\Crafter.Build-Platform_impl.o ^
.\build\Crafter.Build-Interface_impl.o ^
.\build\Crafter.Build-Implementation_impl.o ^
.\build\Crafter.Build-External_impl.o ^
.\build\Crafter.Build-Clang_impl.o ^
.\build\Crafter.Build-Test_impl.o ^
.\build\Crafter.Build-Lint_impl.o ^
.\build\Crafter.Build-Progress_impl.o ^
.\build\Crafter.Build-Asset_impl.o ^
-o .\bin\crafter-build.dll
REM Step 2: link the launcher exe against crafter-build.lib
clang++ %useLibcLinker% -std=c++26 -O3 -march=%CRAFTER_BUILD_MARCH% -mtune=%CRAFTER_BUILD_MTUNE% -L.\bin -fuse-ld=lld ^
.\build\main.o ^
.\bin\crafter-build.lib ^
-o .\bin\crafter-build.exe
REM Step 3: bundle the same .o set as a static archive for downstream consumers
llvm-lib.exe /OUT:.\lib\crafter-build-static.lib ^
.\build\Crafter.Build-Shader.o ^
.\build\Crafter.Build-Platform.o ^
.\build\Crafter.Build-Interface.o ^
.\build\Crafter.Build-Implementation.o ^
.\build\Crafter.Build-External.o ^
.\build\Crafter.Build-Clang.o ^
.\build\Crafter.Build-Test.o ^
.\build\Crafter.Build-Lint.o ^
.\build\Crafter.Build-Progress.o ^
.\build\Crafter.Build-Asset.o ^
.\build\Crafter.Build.o ^
.\build\Crafter.Build-Shader_impl.o ^
.\build\Crafter.Build-Platform_impl.o ^
.\build\Crafter.Build-Interface_impl.o ^
.\build\Crafter.Build-Implementation_impl.o ^
.\build\Crafter.Build-External_impl.o ^
.\build\Crafter.Build-Clang_impl.o ^
.\build\Crafter.Build-Test_impl.o ^
.\build\Crafter.Build-Lint_impl.o ^
.\build\Crafter.Build-Progress_impl.o ^
.\build\Crafter.Build-Asset_impl.o
copy /Y "%LIBCXX_DIR%\lib\c++.dll" ".\bin\c++.dll"

View file

@ -21,7 +21,6 @@ cp interfaces/Crafter.Build-Test.cppm share/crafter-build/
cp interfaces/Crafter.Build-Lint.cppm share/crafter-build/
cp interfaces/Crafter.Build-Progress.cppm share/crafter-build/
cp interfaces/Crafter.Build-Asset.cppm share/crafter-build/
cp interfaces/Crafter.Build-Api.h share/crafter-build/
git clone https://github.com/KhronosGroup/glslang.git ./build/glslang

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

View file

@ -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));

View file

@ -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) {

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

View file

@ -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);

View file

@ -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;
}

View file

@ -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);

View file

@ -1,14 +0,0 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
#pragma once
#if defined(_WIN32)
#if defined(CRAFTER_BUILD_DLL_IMPORT)
#define CRAFTER_API __declspec(dllimport)
#else
#define CRAFTER_API __declspec(dllexport)
#endif
#else
#define CRAFTER_API
#endif

View file

@ -1,8 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Asset;
import std;
namespace fs = std::filesystem;
@ -19,7 +17,7 @@ namespace Crafter {
// that errors out at runtime if cfg.assets is non-empty — full
// support requires a self-host rebuild via crafter-build itself,
// which resolves the Crafter.Asset dep through project.cpp.
export CRAFTER_API std::string CompressAsset(const fs::path& input, const fs::path& output);
export std::string CompressAsset(const fs::path& input, const fs::path& output);
// Split a single .obj + .mtl file by material group into per-material
// .cmesh files, per-unique-albedo .ctex files, and a scene.txt manifest
@ -36,7 +34,7 @@ namespace Crafter {
// Idempotent: returns "" immediately if outDir/scene.txt already exists,
// so repeated builds skip the expensive split+resize step.
// Returns "" on success, an error string on failure.
export CRAFTER_API std::string BuildOBJBundle(
export std::string BuildOBJBundle(
const fs::path& objPath,
const fs::path& mtlPath,
const fs::path& outDir,

View file

@ -1,8 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Clang;
import std;
import :Shader;
@ -65,30 +63,28 @@ export namespace Crafter {
bool IsLocal() const { return exec.empty(); }
static CRAFTER_API TestRunner Local();
static TestRunner Local();
// Prefix runner: wraps the local binary in `<command> {bin} {args}`.
// Used for qemu-user, wasmtime, and similar single-binary wrappers.
static CRAFTER_API TestRunner Cmd(std::string command);
// Run a Windows .exe through Wine. Probes `wine` on PATH; on a Windows
// host the wine wrapper is pointless, so callers should route to Local
// before reaching here.
static CRAFTER_API TestRunner Wine();
static TestRunner Cmd(std::string command);
// Run a Windows .exe through Wine. Probes `wine` on PATH.
static TestRunner Wine();
// Parse a `<kind>[:<arg>]` spec used by CRAFTER_BUILD_RUNNER_<target>
// and --runner=. Supported: "local", "cmd:<binary>". Returns nullopt
// for an empty string; throws on a non-empty unrecognized spec.
static CRAFTER_API std::optional<TestRunner> FromSpec(std::string_view spec);
static std::optional<TestRunner> FromSpec(std::string_view spec);
// Honor CRAFTER_BUILD_RUNNER_<target> (power-user override). Triple
// dashes/dots become underscores so they're valid in env-var names.
// Returns `fallback` when the env var is unset.
static CRAFTER_API TestRunner FromEnv(std::string_view target, TestRunner fallback = Local());
static TestRunner FromEnv(std::string_view target, TestRunner fallback = Local());
// Derive a runner from a Configuration's target triple + sysroot.
// Returns Local() when target equals the host, Wine() for Windows
// targets on a non-Windows host, `qemu-<arch>` (with QEMU_LD_PREFIX
// targets, `qemu-<arch>` (with QEMU_LD_PREFIX
// set when cfg.sysroot is non-empty) for non-host -linux- triples,
// `wasmtime` for wasm32-wasi/wasm64-wasi, and Local() as a last
// resort. CRAFTER_BUILD_RUNNER_<target> still wins as an override
// upstream of this — see FromEnv.
static CRAFTER_API TestRunner ForTarget(const struct Configuration& cfg);
static TestRunner ForTarget(const struct Configuration& cfg);
};
enum class TestOutcome { Pass, Fail, Crash, Timeout, Skipped };
@ -233,17 +229,18 @@ export namespace Crafter {
};
// Per-file view handed to each LintRule's check callback. Every member
// function is out-of-line and CRAFTER_API (defined in Crafter.Build:Lint's
// implementation unit) because rule lambdas execute from the user's
// project DLL on Windows — clang does not emit module-attached in-class
// inline bodies into consumers (see ArgQuery below).
// function is out-of-line (defined in Crafter.Build:Lint's implementation
// unit) because rule lambdas execute from the user's project.so and
// resolve against the running executable: clang does not emit
// module-attached in-class inline bodies into consumers (see ArgQuery
// below).
struct LintContext {
fs::path file; // absolute path of the file under lint
std::string content; // whole file as read from disk
std::vector<std::string_view> lines; // views into `content`, one per line, no '\n'
CRAFTER_API std::string Extension() const; // ".cppm", ".cpp", ".h", ...
CRAFTER_API std::string_view Line(std::size_t n) const; // 1-based; empty if out of range
std::string Extension() const; // ".cppm", ".cpp", ".h", ...
std::string_view Line(std::size_t n) const; // 1-based; empty if out of range
// `content` with comments and string/char literal bodies blanked to
// spaces, newlines preserved — offsets and line numbers stay valid.
// Built from Tokens() on first call, cached per file, so raw strings,
@ -254,9 +251,9 @@ export namespace Crafter {
// Convenient for a quick scan, but Tokens() is the better tool for
// anything structural: this view cannot tell an identifier from a
// keyword, and it has already thrown away where the literals were.
CRAFTER_API const std::string& CommentStripped();
const std::string& CommentStripped();
// Record a finding at `line` (1-based; pass 0 for a whole-file finding).
CRAFTER_API void Report(std::size_t line, std::string message);
void Report(std::size_t line, std::string message);
// Replace the file's content. Makes this rule a *transform*:
// `crafter-build format` writes the result back to disk; `lint`
// derives would-reformat findings from it (dry — never writes).
@ -265,7 +262,7 @@ export namespace Crafter {
// pattern: build the new string, call SetContent once at the end. Do
// not assign `content` directly — that bypasses the re-split. May be
// combined with Report() in the same rule.
CRAFTER_API void SetContent(std::string newContent);
void SetContent(std::string newContent);
// True when `rule` is suppressed at `line` (1-based; 0 = whole-file,
// which only file-level directives cover) by a suppression comment:
// // lint-disable-next-line [rules...] applies to the following line
@ -277,7 +274,7 @@ export namespace Crafter {
// itself for every line its edit touches (the driver cannot map lines
// across a count-changing rewrite). Parsed lazily from the raw lines;
// re-parsed after SetContent.
CRAFTER_API bool Suppressed(std::string_view rule, std::size_t line);
bool Suppressed(std::string_view rule, std::size_t line);
// The file lexed by clang, in source order. Built on first call, cached
// per file, re-lexed after SetContent. Empty for extensions that are
@ -287,16 +284,16 @@ export namespace Crafter {
// raw strings, line splices, digraphs and nested quoting right, and
// offsets index straight into `content`, so a transform can find in
// the token stream and edit in place.
CRAFTER_API std::span<const LintToken> Tokens();
std::span<const LintToken> Tokens();
// The token's own bytes: content.substr(tok.offset, tok.length).
CRAFTER_API std::string_view TokenText(const LintToken& token) const;
std::string_view TokenText(const LintToken& token) const;
// Tokens whose FIRST byte is on `line` (1-based). A token that starts
// earlier and spans into `line` — a raw string, a block comment — is
// not included; ask Tokens() directly when that matters.
CRAFTER_API std::span<const LintToken> TokensOnLine(std::size_t line);
std::span<const LintToken> TokensOnLine(std::size_t line);
// True when a comment token starts on `line`. Replaces `Line(n).contains("//")`,
// which false-positives on a `//` inside a string literal.
CRAFTER_API bool LineHasComment(std::size_t line);
bool LineHasComment(std::size_t line);
// True when `line` (1-based) is touched by a token that spans more
// than one line — a raw string or a block comment. A transform that
// joins, splits or rewrites such a line changes what is inside that
@ -304,7 +301,7 @@ export namespace Crafter {
// Replaces `Line(n).contains("R\"")`, which both misses raw strings
// opened on an earlier line and fires on the characters R" appearing
// inside an ordinary literal.
CRAFTER_API bool LineHasMultiLineToken(std::size_t line);
bool LineHasMultiLineToken(std::size_t line);
// Declarations written in this file, flattened depth-first with
// parents before children (see LintDecl). Empty when the AST is not
@ -321,11 +318,11 @@ export namespace Crafter {
// slice. Declarations inside a preprocessor branch that is inactive
// for the host are absent here, though Tokens() still sees them. Rules
// that must cover every platform belong on tokens.
CRAFTER_API std::span<const LintDecl> Decls();
CRAFTER_API bool AstAvailable();
std::span<const LintDecl> Decls();
bool AstAvailable();
// Why AstAvailable() is false — a missing PCM, a parse error, a file
// that is not a translation unit. Empty when the AST is available.
CRAFTER_API std::string_view AstUnavailableReason();
std::string_view AstUnavailableReason();
// Driver wiring — set by RunLint before each check call. Not for rules.
std::string activeRule;
@ -363,7 +360,7 @@ export namespace Crafter {
// Configuration::target so projects don't have to hardcode it for the
// no-cross-compile case. Returns "" if clang isn't on PATH or doesn't
// print a triple — those projects still need an explicit cfg.target.
CRAFTER_API std::string HostTarget();
std::string HostTarget();
struct Configuration {
fs::path path;
@ -439,7 +436,7 @@ export namespace Crafter {
std::vector<Test> tests;
// Lint rules for `crafter-build lint`. Populate via AddLintRule.
std::vector<LintRule> lintRules;
CRAFTER_API void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
// Retry the `import X;` names GetInterfacesAndImplementations could not
// place, against the dependency DAG as it stands now. Sources are
// scanned when they're declared, but `dependencies` is often assigned
@ -448,30 +445,30 @@ export namespace Crafter {
// change and this Configuration's objects would be silently reused
// against the new layout. Build() calls this before checking mtimes;
// calling it again is harmless.
CRAFTER_API void ResolvePendingImports();
void ResolvePendingImports();
// Declare a test. Sources default to `tests/<name>/main.cpp` resolved
// against this Configuration's path; target/march/mtune/sysroot/debug
// are inherited from this Configuration so cross-arch projects don't
// have to re-specify. Returns a builder for chaining defines, deps,
// etc. Defined in Crafter.Build:Test.
CRAFTER_API struct TestBuilder AddTest(std::string_view name);
struct TestBuilder AddTest(std::string_view name);
// Same as AddTest, but compiles the parent's `interfaces` directly
// into this test's Configuration (rather than going through a dep).
// Use when the test must rebuild those interfaces with its own
// compile flags — typically per-march SIMD codegen.
CRAFTER_API struct TestBuilder AddTest(std::string_view name, std::span<fs::path> interfaces);
struct TestBuilder AddTest(std::string_view name, std::span<fs::path> interfaces);
// Math-style fan-out: one Test per MarchTier, all sharing the same
// `tests/<name>/main.cpp` source and the same interface set, each
// compiled with the tier's `-march`/`-mtune`. Test names are
// `<name>-<march>`.
CRAFTER_API void AddMarchVariants(std::string_view name, std::span<fs::path> interfaces, std::span<const struct MarchTier> tiers);
void AddMarchVariants(std::string_view name, std::span<fs::path> interfaces, std::span<const struct MarchTier> tiers);
// Register a lint rule for `crafter-build lint` / `format`. Rules
// registered on any Configuration whose path lies inside the project
// root are collected (deduplicated by name, root-first) — attach them
// to the lib or the exe config, either works. Rules that call
// ctx.SetContent are transforms (see LintContext::SetContent).
// Defined in Crafter.Build:Lint.
CRAFTER_API void AddLintRule(std::string name, std::function<void(LintContext&)> check);
void AddLintRule(std::string name, std::function<void(LintContext&)> check);
// Same, for a rule that reads LintContext::Decls(). Declared
// separately rather than as a flag on AddLintRule so existing
// registrations keep compiling. Such a rule makes the run build the
@ -479,7 +476,7 @@ export namespace Crafter {
// produced becomes an error instead of a silent pass. Prefer
// report-only: a transform running after the first one forces a
// re-parse of everything it changed.
CRAFTER_API void AddAstLintRule(std::string name, std::function<void(LintContext&)> check);
void AddAstLintRule(std::string name, std::function<void(LintContext&)> check);
// Suffix that uniquely identifies this Configuration's compile state.
// target+march+mtune are spelled out for readability; the rest
// (type, debug, sysroot, defines, compileFlags) collapse into a short
@ -544,14 +541,14 @@ export namespace Crafter {
// Directory holding the std module PCM for this Configuration. Keyed on
// target+march because a BMI is only loadable by a TU with matching target
// features, and suffixed with the wasm variant flags for the same reason.
CRAFTER_API fs::path StdPcmDir(const Configuration& config);
fs::path StdPcmDir(const Configuration& config);
// Where Build leaves this Configuration's linked artifact: the executable
// (with the target's suffix), `lib<name>.a` / `<name>.lib`, or the shared
// library / DLL. The one rule every "is the consumer older than its
// dependency" check reads, so a staleness test and the link step can't
// disagree about the file name.
CRAFTER_API fs::path ExpectedOutput(const Configuration& config);
fs::path ExpectedOutput(const Configuration& config);
// The clang invocation every C++ translation unit in this Configuration is
// compiled with, before anything that depends on work having happened
@ -586,14 +583,14 @@ export namespace Crafter {
fs::path stdPcmDir;
fs::path pcmDir;
};
CRAFTER_API CompileCommand GetCompileCommand(const Configuration& config);
CompileCommand GetCompileCommand(const Configuration& config);
CRAFTER_API BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex);
BuildResult Build(Configuration& config, std::unordered_map<fs::path, std::shared_future<BuildResult>>& depResults, std::mutex& depMutex);
// Takes main's argv verbatim so the CLI entry point is a one-liner;
// the shape is inherited from the language, not chosen here.
// lint-disable-next-line no-char-pointer
CRAFTER_API std::int32_t Run(std::int32_t argc, char** argv);
std::int32_t Run(std::int32_t argc, char** argv);
// Delete the bin/ and build/ trees beside `projectFile`, returning the paths
// that existed and were removed. Backs `crafter-build clean`.
@ -603,7 +600,7 @@ export namespace Crafter {
// to compile (and its git dependencies to be present) is useless exactly
// then. That means variant directories are not enumerated — the whole tree
// goes, which is what the by-hand `rm -rf bin build` did anyway.
CRAFTER_API std::vector<fs::path> CleanProject(const fs::path& projectFile);
std::vector<fs::path> CleanProject(const fs::path& projectFile);
// Add a small index.html + runtime.js pair next to the .wasm output so the
// build can be loaded directly in a browser (just `serve` the bin dir and
@ -611,7 +608,7 @@ export namespace Crafter {
// this and shouldn't carry the extra files. Call from project.cpp after
// outputName is set; index.html is generated against the current
// outputName so renaming the binary later requires another call.
CRAFTER_API void EnableWasiBrowserRuntime(Configuration& cfg);
void EnableWasiBrowserRuntime(Configuration& cfg);
// Register the relaxed-SIMD codegen variant on cfg. Builds an additional
// outputName.relaxed-simd.wasm compiled with -mrelaxed-simd (FMA,
@ -622,17 +619,17 @@ export namespace Crafter {
// 2026). Idempotent. Call before EnableWasiBrowserRuntime so the emitted
// variants.json manifest includes it. A no-op for non-wasm targets at build
// time (no wasm is produced), but harmless to declare unconditionally.
CRAFTER_API void EnableWasiRelaxedSimdVariant(Configuration& cfg);
void EnableWasiRelaxedSimdVariant(Configuration& cfg);
// View over the project's args with simple query helpers. Has(flag) for
// boolean switches; Get(prefix) for valued options (e.g. Get("--prefix=")
// returns the substring after the equals). Definitions are out-of-line
// and CRAFTER_API so they cross the Windows DLL boundary cleanly — clang
// does not emit module-attached in-class inline bodies into consumers.
// returns the substring after the equals). Definitions are out-of-line so
// project.so resolves them from the executable: clang does not emit
// module-attached in-class inline bodies into consumers.
struct ArgQuery {
std::span<const std::string_view> args;
CRAFTER_API bool Has(std::string_view flag) const;
CRAFTER_API std::optional<std::string> Get(std::string_view prefix) const;
bool Has(std::string_view flag) const;
std::optional<std::string> Get(std::string_view prefix) const;
};
// Apply the framework's standard CLI args + env vars onto cfg:
@ -651,5 +648,5 @@ export namespace Crafter {
// pre-set defaults. Returns an ArgQuery over the same span so projects
// can query their own flags (`--timing`, ...) without re-rolling the
// for-arg-in-args loop.
CRAFTER_API ArgQuery ApplyStandardArgs(Configuration& cfg, std::span<const std::string_view> args);
ArgQuery ApplyStandardArgs(Configuration& cfg, std::span<const std::string_view> args);
}

View file

@ -1,8 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:External;
import std;
namespace fs = std::filesystem;
@ -48,13 +46,13 @@ export namespace Crafter {
// declaration and target — BuildExternal derives its own working directory
// through this, so a caller that only needs to know where the headers ended
// up cannot drift from where they actually are.
CRAFTER_API fs::path ExternalCloneDir(const ExternalDependency& dep, std::string_view target);
fs::path ExternalCloneDir(const ExternalDependency& dep, std::string_view target);
// The -I flags this dependency contributes, from its declared includeDirs.
// Also pure, which is what lets the linter's AST layer parse a source that
// includes an external's headers without running a build to find out where
// they are. The paths only resolve once something has fetched the clone.
CRAFTER_API std::vector<std::string> ExternalIncludeFlags(const ExternalDependency& dep, std::string_view target);
CRAFTER_API ExternalBuildResult BuildExternal(const ExternalDependency& dep, std::string_view target, std::atomic<bool>& cancelled);
std::vector<std::string> ExternalIncludeFlags(const ExternalDependency& dep, std::string_view target);
ExternalBuildResult BuildExternal(const ExternalDependency& dep, std::string_view target, std::atomic<bool>& cancelled);
// Specification for a sibling crafter-build project to fetch and depend on.
// GitSource picks the revision: leave branch + commit empty for the
@ -73,7 +71,7 @@ export namespace Crafter {
// remote's project.cpp, and returns a stable Configuration* you can drop
// into cfg.dependencies. The Configuration is owned by an internal cache
// for the lifetime of the build; identical specs share one Configuration.
CRAFTER_API Configuration* GitProject(const GitProjectSpec& spec);
Configuration* GitProject(const GitProjectSpec& spec);
// Specification for a local sibling crafter-build project (e.g. an
// example folder depending on its parent project, or a workspace where
@ -87,7 +85,7 @@ export namespace Crafter {
// cache copy. Resolves projectFile to its canonical absolute path and
// recursively LoadProject's it. Returns a stable Configuration* owned
// by the same internal cache as GitProject.
CRAFTER_API Configuration* LocalProject(const LocalProjectSpec& spec);
Configuration* LocalProject(const LocalProjectSpec& spec);
// Clone (or update) the given git source into the shared external
// cache and return the path to the cloned working tree. Intended for
@ -97,5 +95,5 @@ export namespace Crafter {
// ExternalDependency / GitProject. Caching key = url + branch + commit;
// identical calls reuse the existing clone. Pin `source.commit` to a
// SHA for reproducible, offline-after-first-fetch builds.
CRAFTER_API fs::path GitFetch(const GitSource& source);
fs::path GitFetch(const GitSource& source);
}

View file

@ -1,8 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Implementation;
import std;
namespace fs = std::filesystem;
@ -19,8 +17,8 @@ namespace Crafter {
// no reachable Configuration provided when the source was scanned.
std::vector<std::string> pendingImports;
fs::path path;
CRAFTER_API Implementation(fs::path&& path);
CRAFTER_API bool Check(const fs::path& buildDir, const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min()) const;
CRAFTER_API void Compile(const std::string_view clang, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) const;
Implementation(fs::path&& path);
bool Check(const fs::path& buildDir, const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min()) const;
void Compile(const std::string_view clang, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) const;
};
}

View file

@ -1,8 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Interface;
import std;
namespace fs = std::filesystem;
@ -26,9 +24,9 @@ namespace Crafter {
bool checked = false;
std::string name;
fs::path path;
CRAFTER_API ModulePartition(std::string&& name, fs::path&& path);
CRAFTER_API bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
CRAFTER_API void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
ModulePartition(std::string&& name, fs::path&& path);
bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
};
export class Module {
@ -48,8 +46,8 @@ namespace Crafter {
std::vector<std::unique_ptr<ModulePartition>> partitions;
std::string name;
fs::path path;
CRAFTER_API Module(std::string&& name, fs::path&& path);
CRAFTER_API bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
CRAFTER_API void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
Module(std::string&& name, fs::path&& path);
bool Check(const fs::path& pcmDir, fs::file_time_type sourceFloor = fs::file_time_type::min());
void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
};
}

View file

@ -1,8 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Lint;
import std;
import :Clang;
@ -63,5 +61,5 @@ export namespace Crafter {
// final content differs from the original. Prints per-mode output:
// findings compiler-style (Report), would-change paths (Check), or
// formatted paths (Apply), plus a summary line.
CRAFTER_API LintSummary RunLint(Configuration& projectCfg, const RunLintOptions& opts);
LintSummary RunLint(Configuration& projectCfg, const RunLintOptions& opts);
}

View file

@ -1,8 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Platform;
import std;
namespace fs = std::filesystem;
@ -30,9 +28,9 @@ namespace Crafter {
// Progress::Diagnostic), otherwise the compiler's diagnostics. Callers
// therefore keep the `if (!result.empty())` shape they already had.
std::string RunBuildCommand(std::string_view command);
export CRAFTER_API CommandResult RunCommandWithTimeout(std::string_view command, std::chrono::seconds timeout);
export CommandResult RunCommandWithTimeout(std::string_view command, std::chrono::seconds timeout);
std::string GetBaseCommand(const Configuration& config);
export CRAFTER_API Configuration LoadProject(const fs::path& projectFile, std::span<const std::string_view> args);
export Configuration LoadProject(const fs::path& projectFile, std::span<const std::string_view> args);
// A dependency the project file currently running its CrafterBuildProject
// declared with `#pragma crafter dependency`, so it can also be wired
// into the build graph (a test that links the same module, say) without
@ -40,7 +38,7 @@ namespace Crafter {
// name or any module it exports — whatever the project file `import`s.
// Throws when no such dependency was declared, or when called from
// anywhere other than inside LoadProject.
export CRAFTER_API Configuration* ProjectDependency(std::string_view name);
export Configuration* ProjectDependency(std::string_view name);
// True while the project file crafter-build was pointed at runs its
// CrafterBuildProject, and when no project file is running at all; false
// inside a dependency's project file (a `#pragma crafter dependency`,
@ -49,11 +47,11 @@ namespace Crafter {
// library's test links crafter-build itself, so when crafter-build loads
// that library as a dependency, the test's GitProject loads crafter-build's
// project file in turn and the two recurse until the cycle guard throws.
export CRAFTER_API bool IsTopLevelProject();
export bool IsTopLevelProject();
// Resolves the directory holding distributed runtime assets (Crafter.Build
// module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise
// derives <prefix>/share/crafter-build from the running executable's path.
export CRAFTER_API fs::path GetCrafterBuildHome();
export fs::path GetCrafterBuildHome();
// Newest mtime among the prerequisites a compiler recorded in `depFile`
// (clang's `-MD -MF <artifact>.d`). This is how a build step learns which
// headers its source pulled in: the module scanner only reads `import`

View file

@ -1,22 +1,20 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Progress;
import std;
export namespace Crafter::Progress {
enum class Verbosity { Quiet, Default, Verbose };
CRAFTER_API void SetVerbosity(Verbosity v);
CRAFTER_API Verbosity GetVerbosity();
void SetVerbosity(Verbosity v);
Verbosity GetVerbosity();
// RAII work-unit marker. Construction inflates the running total; destruction
// increments completed and (on Default + TTY) redraws the status line. Move
// semantics are disabled — keep these as plain stack locals inside the worker
// lambda so the destructor fires when that thread's work ends.
class CRAFTER_API Task {
class Task {
public:
explicit Task(std::string label);
~Task();
@ -29,18 +27,18 @@ export namespace Crafter::Progress {
};
// Verbose-mode command echo. No-op outside Verbose.
CRAFTER_API void EchoCommand(std::string_view command);
void EchoCommand(std::string_view command);
// Non-fatal output from a build step that succeeded — compiler and linker
// warnings. Erases the status line first so the text doesn't land on top of
// it, then writes to stderr. No-op on Quiet.
CRAFTER_API void Diagnostic(std::string_view text);
void Diagnostic(std::string_view text);
// Erase the in-place status line so subsequent stderr writes (errors,
// banners) don't collide with it. No-op when not in TTY-redraw mode.
CRAFTER_API void Clear();
void Clear();
// Print "Built N targets in Xms" (or equivalent) and clear in-place state.
// Safe to call multiple times; subsequent calls are no-ops.
CRAFTER_API void Finalize();
void Finalize();
}

View file

@ -1,8 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Shader;
import std;
namespace fs = std::filesystem;
@ -30,8 +28,8 @@ namespace Crafter {
fs::path path;
std::string entrypoint;
ShaderType type;
CRAFTER_API Shader(fs::path&& path, std::string&& entrypoint, ShaderType type);
CRAFTER_API bool Check(const fs::path& outputDir) const;
CRAFTER_API std::string Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const;
Shader(fs::path&& path, std::string&& entrypoint, ShaderType type);
bool Check(const fs::path& outputDir) const;
std::string Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const;
};
}

View file

@ -1,8 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include "Crafter.Build-Api.h"
export module Crafter.Build:Test;
import std;
import :Clang;
@ -28,7 +26,7 @@ export namespace Crafter {
// avoid the double-`GetInterfacesAndImplementations` footgun (calling
// it twice would re-parse the same .cppm files into duplicate Module
// entries).
struct CRAFTER_API TestBuilder {
struct TestBuilder {
Configuration* parent;
std::size_t index;
@ -85,8 +83,8 @@ export namespace Crafter {
bool AllPassed() const { return failed == 0 && crashed == 0 && timedOut == 0; }
};
CRAFTER_API TestSummary RunTests(Configuration& projectCfg, const RunTestsOptions& opts, std::span<const std::string_view> projectArgs = {});
CRAFTER_API TestResult RunSingleTest(const Test& test, const std::filesystem::path& binary, std::chrono::seconds timeout);
TestSummary RunTests(Configuration& projectCfg, const RunTestsOptions& opts, std::span<const std::string_view> projectArgs = {});
TestResult RunSingleTest(const Test& test, const std::filesystem::path& binary, std::chrono::seconds timeout);
// Parent-project access for test fixtures. Run() sets the pointer to the
// root project's Configuration before discovery; fixtures call ParentLib()
@ -94,6 +92,6 @@ export namespace Crafter {
// parent itself) so they can declare it as a dependency without rebuilding
// the lib config from scratch. Lookup is by Configuration::name and
// recurses through the parent's dependency graph.
CRAFTER_API void SetParentProject(Configuration* parent);
CRAFTER_API Configuration* ParentLib(std::string_view name);
void SetParentProject(Configuration* parent);
Configuration* ParentLib(std::string_view name);
}

View file

@ -24,12 +24,9 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
CrafterBuildLib->name = "crafter.build-lib";
CrafterBuildLib->outputName = "crafter-build";
ApplyStandardArgs(*CrafterBuildLib, args);
// Windows builds (native msvc via build.cmd or cross-compiled mingw from
// Linux) need a DLL + import lib + launcher exe so LoadProject can
// compile project.cpp against a stable ABI boundary. Linux is monolithic.
CrafterBuildLib->type = (CrafterBuildLib->target == "x86_64-w64-mingw32" || CrafterBuildLib->target == "x86_64-pc-windows-msvc")
? ConfigurationType::LibraryDynamic
: ConfigurationType::LibraryStatic;
// Monolithic: project.so resolves Crafter:: symbols from the running
// executable, so the library is only ever linked statically into it.
CrafterBuildLib->type = ConfigurationType::LibraryStatic;
CrafterBuildLib->dependencies = { math, asset };
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_HAS_ASSET", ""});
@ -79,12 +76,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
glslang.source.branch = "main";
glslang.builder = ExternalBuilder::CMake;
glslang.options = { "-DENABLE_OPT=OFF" };
// mingw cross-build: skip the standalone executable. We only consume the
// libraries, and glslang.exe pulls in libgcc_eh which needs pthread that
// mingw-w64 doesn't link by default.
if (CrafterBuildLib->target == "x86_64-w64-mingw32") {
glslang.options.push_back("-DENABLE_GLSLANG_BINARIES=OFF");
}
glslang.includeDirs = { "" };
glslang.libs = { "SPIRV", "GenericCodeGen", "glslang", "OSDependent", "MachineIndependent", "glslang-default-resource-limits" };
@ -106,10 +97,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
cfg.linkFlags.push_back("-Wl,--export-dynamic");
cfg.linkFlags.push_back("-ldl");
}
if (cfg.target == "x86_64-w64-mingw32" || cfg.target == "x86_64-pc-windows-msvc") {
// winsock for the -r wasm port probe (bind/WSAStartup).
CrafterBuildLib->linkFlags.push_back("-lws2_32");
}
// Self-tests link the local crafter-build library and exercise it in
// process. The harness (whichever crafter-build invokes `test`) compiles

View file

@ -71,11 +71,7 @@ namespace {
app.dependencies = { lib };
// The link step reads a command's output the same way a compile does,
// so cover it too: an unknown -z value makes ld.lld warn and exit 0.
// GNU-style only — lld-link spells its options differently — so the
// link case rides along on hosts that use it and is skipped elsewhere.
if (!app.target.contains("windows")) {
app.linkFlags = { "-Wl,-z,crafter-build-nonexistent-z-value" };
}
app.linkFlags = { "-Wl,-z,crafter-build-nonexistent-z-value" };
return app;
}

View file

@ -6,9 +6,7 @@ import Crafter.Build;
using namespace Crafter;
// POSIX env helpers — libc++ exports only std::getenv via `import std`, so
// forward-declare the setters we need against the C library directly. The
// test only runs on POSIX hosts (the project.cpp gates new tests by
// x86_64-pc-linux-gnu) so we don't need a Windows alternative here.
// forward-declare the setters we need against the C library directly.
extern "C" int setenv(const char* name, const char* value, int overwrite);
extern "C" int unsetenv(const char* name);
@ -87,13 +85,11 @@ int main() {
Check(r.exec.find("QEMU_LD_PREFIX=/opt/alarm-sysroot") != std::string::npos, "ForTarget propagates sysroot to QEMU_LD_PREFIX");
}
{
// From a Linux host the only sensible way to run x86_64-w64-mingw32 is wine.
// The only way to run x86_64-w64-mingw32 output is wine.
Configuration cfg;
cfg.target = "x86_64-w64-mingw32";
TestRunner r = TestRunner::ForTarget(cfg);
if (HostTarget().find("linux") != std::string::npos) {
Check(r.name == "wine", "ForTarget(mingw) on Linux host uses wine");
}
Check(r.name == "wine", "ForTarget(mingw) uses wine");
}
// FromEnv: when the env var is set, it wins over the fallback.