refactor: drop Windows host support
crafter-build is moving to a single Flatpak distribution, and there is no Windows host in that picture: no build.cmd, no MSVC or mingw host build, no DLL plus launcher. Delete every host-Windows path instead of leaving it to rot unverified: the cmd.exe command runner and quoting, the LOCALAPPDATA cache, the MSVC and mingw-host LoadProject variants with their LIBCXX_DIR and CRAFTER_MINGW_DIR plumbing, the winsock port probe, the LoadLibrary libclang loader, the console-width and isatty shims, and the preprocessor guards around the Linux bodies. The x86_64-w64-mingw32 cross-compile target stays. It is target logic (output names, -femulated-tls, the libstdc++ link extras, copying a dependency DLL next to a mingw exe, wine as the runner) and did not depend on any of the removed code. The msvc target goes with the host that was the only thing able to use it. Three target-path warts fixed on the way: ExpectedOutput named a mingw static library <name>.lib while the archive step wrote lib<name>.a, so those libraries relinked on every run; the import lib is no longer copied next to a mingw exe, since the dependency's own bin dir is already on the link line; and llvm-ar now writes every archive, not only the LTO ones, so Debug and Release archives are made the same way. CRAFTER_API and Crafter.Build-Api.h existed only for the DLL boundary. Nothing downstream used them, so they are removed rather than kept as an empty macro nobody can verify. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
f60ba809b5
commit
46082a3117
24 changed files with 138 additions and 1053 deletions
1
PKGBUILD
1
PKGBUILD
|
|
@ -29,7 +29,6 @@ package() {
|
||||||
install -Dm644 lib/libcrafter-build.a "$pkgdir/usr/lib/libcrafter-build.a"
|
install -Dm644 lib/libcrafter-build.a "$pkgdir/usr/lib/libcrafter-build.a"
|
||||||
install -dm755 "$pkgdir/usr/share/crafter-build"
|
install -dm755 "$pkgdir/usr/share/crafter-build"
|
||||||
install -m644 share/crafter-build/*.cppm "$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 -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/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/"
|
install -m644 share/crafter-build/wasi-runtime/index.html.in "$pkgdir/usr/share/crafter-build/wasi-runtime/"
|
||||||
|
|
|
||||||
137
build.cmd
137
build.cmd
|
|
@ -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"
|
|
||||||
1
build.sh
1
build.sh
|
|
@ -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-Lint.cppm share/crafter-build/
|
||||||
cp interfaces/Crafter.Build-Progress.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-Asset.cppm share/crafter-build/
|
||||||
cp interfaces/Crafter.Build-Api.h share/crafter-build/
|
|
||||||
|
|
||||||
git clone https://github.com/KhronosGroup/glslang.git ./build/glslang
|
git clone https://github.com/KhronosGroup/glslang.git ./build/glslang
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,9 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
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 <sys/socket.h>
|
||||||
#include <netinet/in.h>
|
#include <netinet/in.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#endif
|
|
||||||
export module Crafter.Build:Clang_impl;
|
export module Crafter.Build:Clang_impl;
|
||||||
import std;
|
import std;
|
||||||
import :Clang;
|
import :Clang;
|
||||||
|
|
@ -267,17 +259,19 @@ fs::path Crafter::StdPcmDir(const Configuration& config) {
|
||||||
|
|
||||||
fs::path Crafter::ExpectedOutput(const Configuration& config) {
|
fs::path Crafter::ExpectedOutput(const Configuration& config) {
|
||||||
const fs::path dir = config.BinDir();
|
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.type == ConfigurationType::Executable) {
|
||||||
if (config.target.starts_with("wasm32")) return dir / std::format("{}.wasm", config.outputName);
|
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) {
|
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.
|
// 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) {
|
CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
|
||||||
|
|
@ -322,22 +316,9 @@ CompileCommand Crafter::GetCompileCommand(const Configuration& config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(config.type == ConfigurationType::LibraryDynamic) {
|
if(config.type == ConfigurationType::LibraryDynamic) {
|
||||||
#ifdef __linux__
|
|
||||||
out.command += " -fPIC -D CRAFTER_BUILD_CONFIGURATION_TYPE_SHARED_LIBRARY";
|
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) {
|
} else if(config.type == ConfigurationType::Executable) {
|
||||||
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_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 {
|
} else {
|
||||||
out.command += " -D CRAFTER_BUILD_CONFIGURATION_TYPE_LIBRARY";
|
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();
|
config.ResolvePendingImports();
|
||||||
|
|
||||||
// Auto-detect the WASI sysroot before any compile step runs so BuildStdPcm
|
// Auto-detect the WASI sysroot before any compile step runs so BuildStdPcm
|
||||||
// and the main compile command see the same value. Linux-only — Windows
|
// and the main compile command see the same value. Covers all wasm32-*
|
||||||
// users supply cfg.sysroot pointing at their wasi-sdk install. Covers all
|
// triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple subdirs
|
||||||
// wasm32-* triples (wasi, wasip1, wasip2, ...); the sysroot's per-triple
|
// handle the differences.
|
||||||
// subdirs handle the differences.
|
|
||||||
#ifdef __linux__
|
|
||||||
if (config.sysroot.empty() && config.target.starts_with("wasm32")) {
|
if (config.sysroot.empty() && config.target.starts_with("wasm32")) {
|
||||||
config.sysroot = "/usr/share/wasi-sysroot";
|
config.sysroot = "/usr/share/wasi-sysroot";
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
fs::path buildDir = config.BuildDir();
|
fs::path buildDir = config.BuildDir();
|
||||||
fs::path outputDir = config.BinDir();
|
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& ltoCompileFlags = compile.ltoCompileFlags;
|
||||||
const std::string& ltoLinkFlags = compile.ltoLinkFlags;
|
const std::string& ltoLinkFlags = compile.ltoLinkFlags;
|
||||||
const bool isWasm = config.target.starts_with("wasm32");
|
const bool isWasm = config.target.starts_with("wasm32");
|
||||||
const bool useLto = compile.useLto;
|
|
||||||
|
|
||||||
std::string files;
|
std::string files;
|
||||||
std::unordered_set<std::string> libSet;
|
std::unordered_set<std::string> libSet;
|
||||||
|
|
@ -1125,7 +1102,6 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
|
||||||
if(buildResult.repack) {
|
if(buildResult.repack) {
|
||||||
Progress::Task task(std::format("Linking {}", config.outputName));
|
Progress::Task task(std::format("Linking {}", config.outputName));
|
||||||
if(config.type == ConfigurationType::Executable) {
|
if(config.type == ConfigurationType::Executable) {
|
||||||
#ifdef __linux__
|
|
||||||
if(config.target == "x86_64-w64-mingw32") {
|
if(config.target == "x86_64-w64-mingw32") {
|
||||||
try {
|
try {
|
||||||
// Copy any LibraryDynamic dependency DLLs alongside
|
// 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) {
|
std::function<void(Configuration*)> copyDepDlls = [&](Configuration* dep) {
|
||||||
if (!dllSeen.insert(dep).second) return;
|
if (!dllSeen.insert(dep).second) return;
|
||||||
if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") {
|
if (dep->type == ConfigurationType::LibraryDynamic && dep->target == "x86_64-w64-mingw32") {
|
||||||
fs::path depDir = dep->BinDir();
|
// Only the DLL: Windows resolves it from the exe's
|
||||||
// The DLL itself (Windows resolves it from the
|
// directory at load time. The import lib stays in
|
||||||
// exe's directory at load time) and the mingw
|
// the dependency's own bin dir, which is already
|
||||||
// import lib (so a downstream `crafter-build.exe`
|
// on the link line through buildResult.libs.
|
||||||
// can link a fresh project.dll against it without
|
fs::path src = dep->BinDir() / std::format("{}.dll", dep->outputName);
|
||||||
// hunting through sibling output dirs).
|
if (fs::exists(src)) {
|
||||||
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();
|
fs::path dest = outputDir / src.filename();
|
||||||
if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) {
|
if (!fs::exists(dest) || fs::last_write_time(src) > fs::last_write_time(dest)) {
|
||||||
fs::copy(src, dest, fs::copy_options::overwrite_existing);
|
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 {
|
} else {
|
||||||
buildResult.result = RunBuildCommand(std::format("{}{} -o {} -fuse-ld=lld{}", command, files, (outputDir/config.outputName).string(), linkExtras));
|
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) {
|
} else if(config.type == ConfigurationType::LibraryStatic) {
|
||||||
#ifdef __linux__
|
|
||||||
// ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive
|
// ThinLTO emits LLVM bitcode objects; plain `ar` writes an archive
|
||||||
// index that omits their symbols, so a consumer's lld link can't
|
// index that omits their symbols, so a consumer's lld link can't
|
||||||
// pull the needed members. llvm-ar writes a bitcode-aware index.
|
// 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));
|
// and handles plain ELF and COFF objects just as well, so every
|
||||||
#endif
|
// build uses it.
|
||||||
|
buildResult.result = RunBuildCommand(std::format("llvm-ar rcs {}.a {}", (outputDir/fs::path(std::string("lib")+config.outputName)).string(), files));
|
||||||
#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
|
|
||||||
} else {
|
} else {
|
||||||
// LibraryDynamic. Output names follow each target's convention so
|
// LibraryDynamic. Output names follow each target's convention so
|
||||||
// consumers can link via the standard linker search:
|
// consumers can link via the standard linker search:
|
||||||
// mingw: <name>.dll + lib<name>.dll.a (lld --out-implib)
|
// mingw: <name>.dll + lib<name>.dll.a (lld --out-implib)
|
||||||
// msvc: <name>.dll + <name>.lib (lld /IMPLIB)
|
|
||||||
// unix: lib<name>.so (rpath $ORIGIN)
|
// unix: lib<name>.so (rpath $ORIGIN)
|
||||||
if (config.target == "x86_64-w64-mingw32") {
|
if (config.target == "x86_64-w64-mingw32") {
|
||||||
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
|
fs::path dll = outputDir / std::format("{}.dll", config.outputName);
|
||||||
fs::path implib = outputDir / std::format("lib{}.dll.a", 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));
|
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 {
|
} 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));
|
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
|
// includes it. (Build() also runs this once more for callers that bypassed
|
||||||
// ApplyStandardArgs, but doing it here makes dep PcmDirs consistent
|
// ApplyStandardArgs, but doing it here makes dep PcmDirs consistent
|
||||||
// between the consumer's command-construction and the dep's own build.)
|
// between the consumer's command-construction and the dep's own build.)
|
||||||
#ifdef __linux__
|
|
||||||
if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) {
|
if (cfg.sysroot.empty() && cfg.target.starts_with("wasm32")) {
|
||||||
cfg.sysroot = "/usr/share/wasi-sysroot";
|
cfg.sysroot = "/usr/share/wasi-sysroot";
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
return ArgQuery{args};
|
return ArgQuery{args};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1644,7 +1572,6 @@ Environment:
|
||||||
Replace '-' and '.' with '_' in the
|
Replace '-' and '.' with '_' in the
|
||||||
triple. CLI --runner= overrides this.
|
triple. CLI --runner= overrides this.
|
||||||
CRAFTER_MINGW_DIR Override mingw-w64 sysroot auto-detect.
|
CRAFTER_MINGW_DIR Override mingw-w64 sysroot auto-detect.
|
||||||
LIBCXX_DIR Windows libc++ install (MSVC ABI builds).
|
|
||||||
|
|
||||||
Exit status:
|
Exit status:
|
||||||
0 success / all non-skipped tests passed / lint clean / format applied
|
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")) {
|
if (config.target.starts_with("wasm32")) {
|
||||||
const bool browserBuild = fs::exists(absDir / "index.html");
|
const bool browserBuild = fs::exists(absDir / "index.html");
|
||||||
auto have = [](std::string_view exe) {
|
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);
|
std::string probe = std::format("command -v {} > /dev/null 2>&1", exe);
|
||||||
#endif
|
|
||||||
return std::system(probe.c_str()) == 0;
|
return std::system(probe.c_str()) == 0;
|
||||||
};
|
};
|
||||||
if (browserBuild) {
|
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
|
// user has another dev server running we shift up rather
|
||||||
// than letting caddy/python exit with EADDRINUSE.
|
// than letting caddy/python exit with EADDRINUSE.
|
||||||
auto findFreePort = [](std::int32_t basePort, std::int32_t span) -> std::int32_t {
|
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;
|
using Sock = std::int32_t;
|
||||||
const Sock invalid = -1;
|
const Sock invalid = -1;
|
||||||
auto closesock = [](Sock s){ ::close(s); };
|
auto closesock = [](Sock s){ ::close(s); };
|
||||||
#endif
|
|
||||||
for (std::int32_t p = basePort; p < basePort + span; ++p) {
|
for (std::int32_t p = basePort; p < basePort + span; ++p) {
|
||||||
Sock s = ::socket(AF_INET, SOCK_STREAM, 0);
|
Sock s = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||||
if (s == invalid) return basePort;
|
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
|
// Silence the backend's own banner/request logs so the
|
||||||
// experience is identical regardless of which server is
|
// experience is identical regardless of which server is
|
||||||
// picked — the line above is the only output.
|
// picked — the line above is the only output.
|
||||||
#ifdef _WIN32
|
|
||||||
cmd += " > NUL 2>&1";
|
|
||||||
#else
|
|
||||||
cmd += " > /dev/null 2>&1";
|
cmd += " > /dev/null 2>&1";
|
||||||
#endif
|
|
||||||
return std::system(cmd.c_str()) == 0 ? 0 : 1;
|
return std::system(cmd.c_str()) == 0 ? 0 : 1;
|
||||||
}
|
}
|
||||||
// wasi-cli wasm — needs a standalone runtime.
|
// wasi-cli wasm — needs a standalone runtime.
|
||||||
|
|
@ -1952,10 +1863,6 @@ std::int32_t Crafter::Run(std::int32_t argc, char** argv) {
|
||||||
return 1;
|
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
|
// Run from the artifact's own directory so relative file opens
|
||||||
// (shaders, assets copied alongside the exe via cfg.files) resolve
|
// (shaders, assets copied alongside the exe via cfg.files) resolve
|
||||||
// against the bin dir rather than the user's cwd. We exit the
|
// against the bin dir rather than the user's cwd. We exit the
|
||||||
|
|
|
||||||
|
|
@ -21,18 +21,6 @@ std::string DeriveName(const GitSource& source) {
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ShellQuote(std::string_view s) {
|
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 = "'";
|
std::string out = "'";
|
||||||
for (char c : s) {
|
for (char c : s) {
|
||||||
if (c == '\'') out += "'\\''";
|
if (c == '\'') out += "'\\''";
|
||||||
|
|
@ -40,7 +28,6 @@ std::string ShellQuote(std::string_view s) {
|
||||||
}
|
}
|
||||||
out += "'";
|
out += "'";
|
||||||
return out;
|
return out;
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string JoinOptions(std::span<const std::string> options) {
|
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_BUILD_TYPE=Release";
|
||||||
out += " -DCMAKE_C_COMPILER=clang";
|
out += " -DCMAKE_C_COMPILER=clang";
|
||||||
out += " -DCMAKE_CXX_COMPILER=clang++";
|
out += " -DCMAKE_CXX_COMPILER=clang++";
|
||||||
// Stdlib choice: libc++ for the Linux host build (matches how crafter-build
|
// Stdlib choice: libc++ for Linux targets (matches how crafter-build
|
||||||
// itself is built with -stdlib=libc++); mingw cross-compile uses libstdc++
|
// itself is built with -stdlib=libc++); the mingw cross-compile uses the
|
||||||
// shipped by the mingw-w64 toolchain so we leave the default; Windows
|
// libstdc++ shipped by the mingw-w64 toolchain, so it keeps the default.
|
||||||
// native (msvc target) similarly uses libc++ via LIBCXX_DIR.
|
|
||||||
#ifdef __linux__
|
|
||||||
if (target.ends_with("-linux-gnu") || target.ends_with("-linux-musl")) {
|
if (target.ends_with("-linux-gnu") || target.ends_with("-linux-musl")) {
|
||||||
out += " -DCMAKE_CXX_FLAGS=-stdlib=libc++";
|
out += " -DCMAKE_CXX_FLAGS=-stdlib=libc++";
|
||||||
out += " -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++";
|
out += " -DCMAKE_EXE_LINKER_FLAGS=-stdlib=libc++";
|
||||||
out += " -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++";
|
out += " -DCMAKE_SHARED_LINKER_FLAGS=-stdlib=libc++";
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
if (target == "x86_64-w64-mingw32") {
|
if (target == "x86_64-w64-mingw32") {
|
||||||
out += " -DCMAKE_SYSTEM_NAME=Windows";
|
out += " -DCMAKE_SYSTEM_NAME=Windows";
|
||||||
out += std::format(" -DCMAKE_C_COMPILER_TARGET={}", target);
|
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
|
// (runtime-loader). The rpath stays embedded in the produced
|
||||||
// binary so it picks up shared deps from the cache without any
|
// binary so it picks up shared deps from the cache without any
|
||||||
// LD_LIBRARY_PATH gymnastics. Static deps (.a) ignore the rpath
|
// LD_LIBRARY_PATH gymnastics. Static deps (.a) ignore the rpath
|
||||||
// harmlessly. PE/COFF targets (mingw, msvc) resolve DLLs via PATH
|
// harmlessly. The PE/COFF target (mingw) resolves DLLs via PATH or
|
||||||
// or adjacency rather than rpath, so lld warns if we pass it.
|
// 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";
|
const bool isPe = target == "x86_64-w64-mingw32";
|
||||||
std::string buildDirAbs = fs::absolute(cmakeBuildDir).string();
|
std::string buildDirAbs = fs::absolute(cmakeBuildDir).string();
|
||||||
result.linkFlags.push_back(std::format("-L{}", buildDirAbs));
|
result.linkFlags.push_back(std::format("-L{}", buildDirAbs));
|
||||||
if (!isPe) result.linkFlags.push_back(std::format("-Wl,-rpath,{}", buildDirAbs));
|
if (!isPe) result.linkFlags.push_back(std::format("-Wl,-rpath,{}", buildDirAbs));
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,7 @@
|
||||||
|
|
||||||
module;
|
module;
|
||||||
#include <clang-c/Index.h>
|
#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>
|
#include <dlfcn.h>
|
||||||
#endif
|
|
||||||
export module Crafter.Build:Lint_impl;
|
export module Crafter.Build:Lint_impl;
|
||||||
import std;
|
import std;
|
||||||
import :Lint;
|
import :Lint;
|
||||||
|
|
@ -20,20 +16,13 @@ using namespace Crafter;
|
||||||
namespace {
|
namespace {
|
||||||
// ---------------- libclang ----------------
|
// ---------------- libclang ----------------
|
||||||
//
|
//
|
||||||
// libclang is loaded at runtime rather than linked. Linking -lclang would
|
// libclang is loaded at runtime rather than linked, so a plain build never
|
||||||
// break the mingw and MSVC cross-builds at link time and would put a
|
// needs it and the binary carries no libclang.so.NN loader dependency; the
|
||||||
// libclang.so.NN runtime dependency into the otherwise self-contained
|
// clang-c header is used for its declarations only, and every call goes
|
||||||
// release tarballs; the clang-c header is used for its declarations only,
|
// through a pointer resolved here. Failure to load is
|
||||||
// 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,
|
// a hard error surfaced once by RunLint — there is deliberately no second,
|
||||||
// weaker lexer to fall back to, because two engines disagreeing about what
|
// weaker lexer to fall back to, because two engines disagreeing about what
|
||||||
// is a comment is a worse failure than not running.
|
// 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*;
|
using LibHandle = void*;
|
||||||
LibHandle OpenLibrary(const std::string& name) { return dlopen(name.c_str(), RTLD_NOW | RTLD_LOCAL); }
|
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()); }
|
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", "libclang.so.22.1", "libclang.so.21.1", "libclang.so.20.1",
|
||||||
"libclang.so.1", "libclang.dylib",
|
"libclang.so.1", "libclang.dylib",
|
||||||
};
|
};
|
||||||
#endif
|
|
||||||
|
|
||||||
// Signatures come from decltype on the header's declarations, so they can
|
// Signatures come from decltype on the header's declarations, so they can
|
||||||
// never drift from the real API. decltype is unevaluated, so naming the
|
// never drift from the real API. decltype is unevaluated, so naming the
|
||||||
|
|
@ -104,8 +92,8 @@ namespace {
|
||||||
LibClang LoadLibClang() {
|
LibClang LoadLibClang() {
|
||||||
LibClang lib;
|
LibClang lib;
|
||||||
std::vector<std::string> tried;
|
std::vector<std::string> tried;
|
||||||
// CRAFTER_BUILD_LIBCLANG pins an exact path, mirroring the LIBCXX_DIR /
|
// CRAFTER_BUILD_LIBCLANG pins an exact path, mirroring the
|
||||||
// CRAFTER_MINGW_DIR overrides used elsewhere. It is exclusive: pointing
|
// CRAFTER_MINGW_DIR override used elsewhere. It is exclusive: pointing
|
||||||
// it at a broken path must fail loudly rather than quietly succeed with
|
// it at a broken path must fail loudly rather than quietly succeed with
|
||||||
// some other libclang, or the override is useless for diagnosing which
|
// some other libclang, or the override is useless for diagnosing which
|
||||||
// library is actually in play.
|
// library is actually in play.
|
||||||
|
|
@ -377,8 +365,8 @@ namespace {
|
||||||
}
|
}
|
||||||
if (!args.empty()) args.erase(args.begin()); // argv[0] is the compiler
|
if (!args.empty()) args.erase(args.begin()); // argv[0] is the compiler
|
||||||
for (std::string& arg : args) {
|
for (std::string& arg : args) {
|
||||||
// Only \" unescapes. Erasing every backslash would destroy the
|
// Only \" unescapes; a backslash anywhere else is part of the
|
||||||
// Windows include paths GetBaseCommand puts on the command line.
|
// argument.
|
||||||
std::string unescaped;
|
std::string unescaped;
|
||||||
unescaped.reserve(arg.size());
|
unescaped.reserve(arg.size());
|
||||||
for (std::size_t i = 0; i < arg.size(); ++i) {
|
for (std::size_t i = 0; i < arg.size(); ++i) {
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,15 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
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 <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#ifdef __linux__
|
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <sys/wait.h>
|
#include <sys/wait.h>
|
||||||
#include <sys/file.h>
|
#include <sys/file.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#endif
|
|
||||||
module Crafter.Build:Platform_impl;
|
module Crafter.Build:Platform_impl;
|
||||||
import std;
|
import std;
|
||||||
import :Platform;
|
import :Platform;
|
||||||
|
|
@ -43,7 +38,6 @@ namespace {
|
||||||
// descriptor is close-on-exec / non-inheritable so spawned clang processes
|
// descriptor is close-on-exec / non-inheritable so spawned clang processes
|
||||||
// don't keep the lock alive past our own release.
|
// don't keep the lock alive past our own release.
|
||||||
class CacheLock {
|
class CacheLock {
|
||||||
#ifdef __linux__
|
|
||||||
std::int32_t fd_ = -1;
|
std::int32_t fd_ = -1;
|
||||||
public:
|
public:
|
||||||
explicit CacheLock(const fs::path& cacheDir) {
|
explicit CacheLock(const fs::path& cacheDir) {
|
||||||
|
|
@ -57,22 +51,6 @@ namespace {
|
||||||
::close(fd_); // closing the descriptor drops the flock
|
::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(const CacheLock&) = delete;
|
||||||
CacheLock& operator=(const CacheLock&) = delete;
|
CacheLock& operator=(const CacheLock&) = delete;
|
||||||
};
|
};
|
||||||
|
|
@ -104,9 +82,6 @@ namespace {
|
||||||
for (std::string_view name : moduleNames) {
|
for (std::string_view name : moduleNames) {
|
||||||
append(fs::path(std::format("{}.cppm", name)));
|
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));
|
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")) {
|
if (const char* envHome = std::getenv("CRAFTER_BUILD_HOME")) {
|
||||||
return fs::path(envHome);
|
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");
|
fs::path hostExe = fs::read_symlink("/proc/self/exe");
|
||||||
#endif
|
|
||||||
|
|
||||||
// Walk up from the exe's directory looking for share/crafter-build with
|
// Walk up from the exe's directory looking for share/crafter-build with
|
||||||
// a known module source present as a sentinel. Handles:
|
// a known module source present as a sentinel. Handles:
|
||||||
|
|
@ -560,499 +526,6 @@ bool Crafter::IsTopLevelProject() {
|
||||||
return LoadingFrames.size() <= 1;
|
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) {
|
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
|
||||||
std::array<char, 128> buffer;
|
std::array<char, 128> buffer;
|
||||||
CommandResult result{};
|
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
|
// non-wasm cross builds: the driver ranks the host toolchain's own
|
||||||
// libc++ headers (<install>/../include/c++/v1) above --sysroot, so
|
// libc++ headers (<install>/../include/c++/v1) above --sysroot, so
|
||||||
// the PCM would mix host libc++ with the foreign libc — force the
|
// 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()) {
|
if (!isWasm && !config.sysroot.empty()) {
|
||||||
archFlags += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1", config.sysroot);
|
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);
|
guard.Frame().dependencies = std::move(preDeps.configs);
|
||||||
return fn(args);
|
return fn(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,8 @@
|
||||||
module;
|
module;
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#if defined(_WIN32)
|
#include <unistd.h>
|
||||||
#include <io.h>
|
#include <sys/ioctl.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
|
|
||||||
|
|
||||||
export module Crafter.Build:Progress_impl;
|
export module Crafter.Build:Progress_impl;
|
||||||
import std;
|
import std;
|
||||||
|
|
@ -32,19 +22,10 @@ namespace {
|
||||||
std::chrono::steady_clock::time_point StartTime = std::chrono::steady_clock::now();
|
std::chrono::steady_clock::time_point StartTime = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
std::int32_t TerminalWidth() {
|
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{};
|
winsize ws{};
|
||||||
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
|
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
|
||||||
return ws.ws_col;
|
return ws.ws_col;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
if (const char* col = std::getenv("COLUMNS")) {
|
if (const char* col = std::getenv("COLUMNS")) {
|
||||||
try { std::int32_t c = std::stoi(col); if (c > 0) return c; } catch (...) {}
|
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) {
|
void SetVerbosity(Verbosity v) {
|
||||||
std::lock_guard lock(StateMutex);
|
std::lock_guard lock(StateMutex);
|
||||||
ActiveVerbosity = v;
|
ActiveVerbosity = v;
|
||||||
IsTty = CRAFTER_PROGRESS_ISATTY(STDOUT_FILENO) != 0;
|
IsTty = isatty(STDOUT_FILENO) != 0;
|
||||||
StartTime = std::chrono::steady_clock::now();
|
StartTime = std::chrono::steady_clock::now();
|
||||||
Total.store(0);
|
Total.store(0);
|
||||||
Done.store(0);
|
Done.store(0);
|
||||||
|
|
|
||||||
|
|
@ -35,38 +35,11 @@ namespace {
|
||||||
return out;
|
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 JoinAndQuoteArgs(std::span<const std::string> args) {
|
||||||
std::string out;
|
std::string out;
|
||||||
for (const auto& a : args) {
|
for (const auto& a : args) {
|
||||||
if (!out.empty()) out.push_back(' ');
|
if (!out.empty()) out.push_back(' ');
|
||||||
out += ShellQuoteHost(a);
|
out += ShellQuoteSh(a);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
@ -191,11 +164,7 @@ TestRunner TestRunner::Cmd(std::string command) {
|
||||||
TestRunner r;
|
TestRunner r;
|
||||||
r.name = std::format("cmd:{}", command);
|
r.name = std::format("cmd:{}", command);
|
||||||
r.exec = std::format("{} {{bin}} {{args}}", command);
|
r.exec = std::format("{} {{bin}} {{args}}", command);
|
||||||
#ifdef _WIN32
|
|
||||||
r.probe = std::format("where {}", command);
|
|
||||||
#else
|
|
||||||
r.probe = std::format("which {}", command);
|
r.probe = std::format("which {}", command);
|
||||||
#endif
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,11 +172,7 @@ TestRunner TestRunner::Wine() {
|
||||||
TestRunner r;
|
TestRunner r;
|
||||||
r.name = "wine";
|
r.name = "wine";
|
||||||
r.exec = "wine {bin} {args}";
|
r.exec = "wine {bin} {args}";
|
||||||
#ifdef _WIN32
|
|
||||||
r.probe = "where wine";
|
|
||||||
#else
|
|
||||||
r.probe = "which wine";
|
r.probe = "which wine";
|
||||||
#endif
|
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -218,11 +183,8 @@ TestRunner TestRunner::ForTarget(const Configuration& cfg) {
|
||||||
// case (cfg.target defaulted to HostTarget()) without any wrapper.
|
// case (cfg.target defaulted to HostTarget()) without any wrapper.
|
||||||
if (target == HostTarget()) return Local();
|
if (target == HostTarget()) return Local();
|
||||||
|
|
||||||
// Windows targets: native on a Windows host, Wine on Linux. We don't
|
// Windows targets run under Wine; there is no Windows host.
|
||||||
// distinguish mingw vs msvc here — the produced .exe runs the same way.
|
if (TargetIsWindows(target)) return Wine();
|
||||||
if (TargetIsWindows(target)) {
|
|
||||||
return TargetIsWindows(HostTarget()) ? Local() : Wine();
|
|
||||||
}
|
|
||||||
|
|
||||||
// WASI: a .wasm file isn't directly executable; wasmtime is the canonical
|
// WASI: a .wasm file isn't directly executable; wasmtime is the canonical
|
||||||
// runtime. wasi-cli also works but the upstream Bytecode Alliance name is
|
// 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()) {
|
if (test.runner.exec.empty()) {
|
||||||
// Local runner: spawn the binary directly through the host shell.
|
// 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);
|
r = RunCommandWithTimeout(cmd, timeout);
|
||||||
} else {
|
} else {
|
||||||
// Prefix runner (qemu-user, wasmtime, wine, ...): templated exec
|
// Prefix runner (qemu-user, wasmtime, wine, ...): templated exec
|
||||||
|
|
@ -337,11 +299,7 @@ TestResult Crafter::RunSingleTest(const Test& test, const fs::path& binary, std:
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
bool ToolOnPath(std::string_view name) {
|
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);
|
std::string cmd = std::format("which {} > /dev/null 2>&1", name);
|
||||||
#endif
|
|
||||||
return std::system(cmd.c_str()) == 0;
|
return std::system(cmd.c_str()) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,7 @@
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#if defined(_WIN32)
|
#include <unistd.h>
|
||||||
#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
|
|
||||||
|
|
||||||
import std;
|
import std;
|
||||||
import Crafter.Build;
|
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
|
// 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
|
// already line-buffered and its in-place redraw path flushes explicitly, so
|
||||||
// only adjust when redirected.
|
// only adjust when redirected.
|
||||||
if (CRAFTER_MAIN_ISATTY(STDOUT_FILENO) == 0) {
|
if (isatty(STDOUT_FILENO) == 0) {
|
||||||
std::setvbuf(stdout, nullptr, _IOLBF, 0);
|
std::setvbuf(stdout, nullptr, _IOLBF, 0);
|
||||||
}
|
}
|
||||||
return Crafter::Run(argc, argv);
|
return Crafter::Run(argc, argv);
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
|
||||||
#include "Crafter.Build-Api.h"
|
|
||||||
export module Crafter.Build:Asset;
|
export module Crafter.Build:Asset;
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -19,7 +17,7 @@ namespace Crafter {
|
||||||
// that errors out at runtime if cfg.assets is non-empty — full
|
// that errors out at runtime if cfg.assets is non-empty — full
|
||||||
// support requires a self-host rebuild via crafter-build itself,
|
// support requires a self-host rebuild via crafter-build itself,
|
||||||
// which resolves the Crafter.Asset dep through project.cpp.
|
// 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
|
// Split a single .obj + .mtl file by material group into per-material
|
||||||
// .cmesh files, per-unique-albedo .ctex files, and a scene.txt manifest
|
// .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,
|
// Idempotent: returns "" immediately if outDir/scene.txt already exists,
|
||||||
// so repeated builds skip the expensive split+resize step.
|
// so repeated builds skip the expensive split+resize step.
|
||||||
// Returns "" on success, an error string on failure.
|
// 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& objPath,
|
||||||
const fs::path& mtlPath,
|
const fs::path& mtlPath,
|
||||||
const fs::path& outDir,
|
const fs::path& outDir,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
|
||||||
#include "Crafter.Build-Api.h"
|
|
||||||
export module Crafter.Build:Clang;
|
export module Crafter.Build:Clang;
|
||||||
import std;
|
import std;
|
||||||
import :Shader;
|
import :Shader;
|
||||||
|
|
@ -65,30 +63,28 @@ export namespace Crafter {
|
||||||
|
|
||||||
bool IsLocal() const { return exec.empty(); }
|
bool IsLocal() const { return exec.empty(); }
|
||||||
|
|
||||||
static CRAFTER_API TestRunner Local();
|
static TestRunner Local();
|
||||||
// Prefix runner: wraps the local binary in `<command> {bin} {args}`.
|
// Prefix runner: wraps the local binary in `<command> {bin} {args}`.
|
||||||
// Used for qemu-user, wasmtime, and similar single-binary wrappers.
|
// Used for qemu-user, wasmtime, and similar single-binary wrappers.
|
||||||
static CRAFTER_API TestRunner Cmd(std::string command);
|
static TestRunner Cmd(std::string command);
|
||||||
// Run a Windows .exe through Wine. Probes `wine` on PATH; on a Windows
|
// Run a Windows .exe through Wine. Probes `wine` on PATH.
|
||||||
// host the wine wrapper is pointless, so callers should route to Local
|
static TestRunner Wine();
|
||||||
// before reaching here.
|
|
||||||
static CRAFTER_API TestRunner Wine();
|
|
||||||
// Parse a `<kind>[:<arg>]` spec used by CRAFTER_BUILD_RUNNER_<target>
|
// Parse a `<kind>[:<arg>]` spec used by CRAFTER_BUILD_RUNNER_<target>
|
||||||
// and --runner=. Supported: "local", "cmd:<binary>". Returns nullopt
|
// and --runner=. Supported: "local", "cmd:<binary>". Returns nullopt
|
||||||
// for an empty string; throws on a non-empty unrecognized spec.
|
// 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
|
// Honor CRAFTER_BUILD_RUNNER_<target> (power-user override). Triple
|
||||||
// dashes/dots become underscores so they're valid in env-var names.
|
// dashes/dots become underscores so they're valid in env-var names.
|
||||||
// Returns `fallback` when the env var is unset.
|
// 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.
|
// Derive a runner from a Configuration's target triple + sysroot.
|
||||||
// Returns Local() when target equals the host, Wine() for Windows
|
// 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,
|
// set when cfg.sysroot is non-empty) for non-host -linux- triples,
|
||||||
// `wasmtime` for wasm32-wasi/wasm64-wasi, and Local() as a last
|
// `wasmtime` for wasm32-wasi/wasm64-wasi, and Local() as a last
|
||||||
// resort. CRAFTER_BUILD_RUNNER_<target> still wins as an override
|
// resort. CRAFTER_BUILD_RUNNER_<target> still wins as an override
|
||||||
// upstream of this — see FromEnv.
|
// 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 };
|
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
|
// 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
|
// function is out-of-line (defined in Crafter.Build:Lint's implementation
|
||||||
// implementation unit) because rule lambdas execute from the user's
|
// unit) because rule lambdas execute from the user's project.so and
|
||||||
// project DLL on Windows — clang does not emit module-attached in-class
|
// resolve against the running executable: clang does not emit
|
||||||
// inline bodies into consumers (see ArgQuery below).
|
// module-attached in-class inline bodies into consumers (see ArgQuery
|
||||||
|
// below).
|
||||||
struct LintContext {
|
struct LintContext {
|
||||||
fs::path file; // absolute path of the file under lint
|
fs::path file; // absolute path of the file under lint
|
||||||
std::string content; // whole file as read from disk
|
std::string content; // whole file as read from disk
|
||||||
std::vector<std::string_view> lines; // views into `content`, one per line, no '\n'
|
std::vector<std::string_view> lines; // views into `content`, one per line, no '\n'
|
||||||
|
|
||||||
CRAFTER_API std::string Extension() const; // ".cppm", ".cpp", ".h", ...
|
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_view Line(std::size_t n) const; // 1-based; empty if out of range
|
||||||
// `content` with comments and string/char literal bodies blanked to
|
// `content` with comments and string/char literal bodies blanked to
|
||||||
// spaces, newlines preserved — offsets and line numbers stay valid.
|
// spaces, newlines preserved — offsets and line numbers stay valid.
|
||||||
// Built from Tokens() on first call, cached per file, so raw strings,
|
// 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
|
// Convenient for a quick scan, but Tokens() is the better tool for
|
||||||
// anything structural: this view cannot tell an identifier from a
|
// anything structural: this view cannot tell an identifier from a
|
||||||
// keyword, and it has already thrown away where the literals were.
|
// 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).
|
// 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*:
|
// Replace the file's content. Makes this rule a *transform*:
|
||||||
// `crafter-build format` writes the result back to disk; `lint`
|
// `crafter-build format` writes the result back to disk; `lint`
|
||||||
// derives would-reformat findings from it (dry — never writes).
|
// 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
|
// pattern: build the new string, call SetContent once at the end. Do
|
||||||
// not assign `content` directly — that bypasses the re-split. May be
|
// not assign `content` directly — that bypasses the re-split. May be
|
||||||
// combined with Report() in the same rule.
|
// 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,
|
// True when `rule` is suppressed at `line` (1-based; 0 = whole-file,
|
||||||
// which only file-level directives cover) by a suppression comment:
|
// which only file-level directives cover) by a suppression comment:
|
||||||
// // lint-disable-next-line [rules...] applies to the following line
|
// // 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
|
// itself for every line its edit touches (the driver cannot map lines
|
||||||
// across a count-changing rewrite). Parsed lazily from the raw lines;
|
// across a count-changing rewrite). Parsed lazily from the raw lines;
|
||||||
// re-parsed after SetContent.
|
// 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
|
// The file lexed by clang, in source order. Built on first call, cached
|
||||||
// per file, re-lexed after SetContent. Empty for extensions that are
|
// 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
|
// raw strings, line splices, digraphs and nested quoting right, and
|
||||||
// offsets index straight into `content`, so a transform can find in
|
// offsets index straight into `content`, so a transform can find in
|
||||||
// the token stream and edit in place.
|
// 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).
|
// 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
|
// 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
|
// earlier and spans into `line` — a raw string, a block comment — is
|
||||||
// not included; ask Tokens() directly when that matters.
|
// 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("//")`,
|
// True when a comment token starts on `line`. Replaces `Line(n).contains("//")`,
|
||||||
// which false-positives on a `//` inside a string literal.
|
// 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
|
// 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
|
// 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
|
// 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
|
// Replaces `Line(n).contains("R\"")`, which both misses raw strings
|
||||||
// opened on an earlier line and fires on the characters R" appearing
|
// opened on an earlier line and fires on the characters R" appearing
|
||||||
// inside an ordinary literal.
|
// 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
|
// Declarations written in this file, flattened depth-first with
|
||||||
// parents before children (see LintDecl). Empty when the AST is not
|
// 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
|
// slice. Declarations inside a preprocessor branch that is inactive
|
||||||
// for the host are absent here, though Tokens() still sees them. Rules
|
// for the host are absent here, though Tokens() still sees them. Rules
|
||||||
// that must cover every platform belong on tokens.
|
// that must cover every platform belong on tokens.
|
||||||
CRAFTER_API std::span<const LintDecl> Decls();
|
std::span<const LintDecl> Decls();
|
||||||
CRAFTER_API bool AstAvailable();
|
bool AstAvailable();
|
||||||
// Why AstAvailable() is false — a missing PCM, a parse error, a file
|
// Why AstAvailable() is false — a missing PCM, a parse error, a file
|
||||||
// that is not a translation unit. Empty when the AST is available.
|
// 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.
|
// Driver wiring — set by RunLint before each check call. Not for rules.
|
||||||
std::string activeRule;
|
std::string activeRule;
|
||||||
|
|
@ -363,7 +360,7 @@ export namespace Crafter {
|
||||||
// Configuration::target so projects don't have to hardcode it for the
|
// 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
|
// 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.
|
// print a triple — those projects still need an explicit cfg.target.
|
||||||
CRAFTER_API std::string HostTarget();
|
std::string HostTarget();
|
||||||
|
|
||||||
struct Configuration {
|
struct Configuration {
|
||||||
fs::path path;
|
fs::path path;
|
||||||
|
|
@ -439,7 +436,7 @@ export namespace Crafter {
|
||||||
std::vector<Test> tests;
|
std::vector<Test> tests;
|
||||||
// Lint rules for `crafter-build lint`. Populate via AddLintRule.
|
// Lint rules for `crafter-build lint`. Populate via AddLintRule.
|
||||||
std::vector<LintRule> lintRules;
|
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
|
// Retry the `import X;` names GetInterfacesAndImplementations could not
|
||||||
// place, against the dependency DAG as it stands now. Sources are
|
// place, against the dependency DAG as it stands now. Sources are
|
||||||
// scanned when they're declared, but `dependencies` is often assigned
|
// 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
|
// change and this Configuration's objects would be silently reused
|
||||||
// against the new layout. Build() calls this before checking mtimes;
|
// against the new layout. Build() calls this before checking mtimes;
|
||||||
// calling it again is harmless.
|
// calling it again is harmless.
|
||||||
CRAFTER_API void ResolvePendingImports();
|
void ResolvePendingImports();
|
||||||
// Declare a test. Sources default to `tests/<name>/main.cpp` resolved
|
// Declare a test. Sources default to `tests/<name>/main.cpp` resolved
|
||||||
// against this Configuration's path; target/march/mtune/sysroot/debug
|
// against this Configuration's path; target/march/mtune/sysroot/debug
|
||||||
// are inherited from this Configuration so cross-arch projects don't
|
// are inherited from this Configuration so cross-arch projects don't
|
||||||
// have to re-specify. Returns a builder for chaining defines, deps,
|
// have to re-specify. Returns a builder for chaining defines, deps,
|
||||||
// etc. Defined in Crafter.Build:Test.
|
// 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
|
// Same as AddTest, but compiles the parent's `interfaces` directly
|
||||||
// into this test's Configuration (rather than going through a dep).
|
// into this test's Configuration (rather than going through a dep).
|
||||||
// Use when the test must rebuild those interfaces with its own
|
// Use when the test must rebuild those interfaces with its own
|
||||||
// compile flags — typically per-march SIMD codegen.
|
// 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
|
// Math-style fan-out: one Test per MarchTier, all sharing the same
|
||||||
// `tests/<name>/main.cpp` source and the same interface set, each
|
// `tests/<name>/main.cpp` source and the same interface set, each
|
||||||
// compiled with the tier's `-march`/`-mtune`. Test names are
|
// compiled with the tier's `-march`/`-mtune`. Test names are
|
||||||
// `<name>-<march>`.
|
// `<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
|
// Register a lint rule for `crafter-build lint` / `format`. Rules
|
||||||
// registered on any Configuration whose path lies inside the project
|
// registered on any Configuration whose path lies inside the project
|
||||||
// root are collected (deduplicated by name, root-first) — attach them
|
// root are collected (deduplicated by name, root-first) — attach them
|
||||||
// to the lib or the exe config, either works. Rules that call
|
// to the lib or the exe config, either works. Rules that call
|
||||||
// ctx.SetContent are transforms (see LintContext::SetContent).
|
// ctx.SetContent are transforms (see LintContext::SetContent).
|
||||||
// Defined in Crafter.Build:Lint.
|
// 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
|
// Same, for a rule that reads LintContext::Decls(). Declared
|
||||||
// separately rather than as a flag on AddLintRule so existing
|
// separately rather than as a flag on AddLintRule so existing
|
||||||
// registrations keep compiling. Such a rule makes the run build the
|
// 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
|
// produced becomes an error instead of a silent pass. Prefer
|
||||||
// report-only: a transform running after the first one forces a
|
// report-only: a transform running after the first one forces a
|
||||||
// re-parse of everything it changed.
|
// 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.
|
// Suffix that uniquely identifies this Configuration's compile state.
|
||||||
// target+march+mtune are spelled out for readability; the rest
|
// target+march+mtune are spelled out for readability; the rest
|
||||||
// (type, debug, sysroot, defines, compileFlags) collapse into a short
|
// (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
|
// 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
|
// 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.
|
// 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
|
// Where Build leaves this Configuration's linked artifact: the executable
|
||||||
// (with the target's suffix), `lib<name>.a` / `<name>.lib`, or the shared
|
// (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
|
// 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
|
// dependency" check reads, so a staleness test and the link step can't
|
||||||
// disagree about the file name.
|
// 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
|
// The clang invocation every C++ translation unit in this Configuration is
|
||||||
// compiled with, before anything that depends on work having happened
|
// compiled with, before anything that depends on work having happened
|
||||||
|
|
@ -586,14 +583,14 @@ export namespace Crafter {
|
||||||
fs::path stdPcmDir;
|
fs::path stdPcmDir;
|
||||||
fs::path pcmDir;
|
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;
|
// Takes main's argv verbatim so the CLI entry point is a one-liner;
|
||||||
// the shape is inherited from the language, not chosen here.
|
// the shape is inherited from the language, not chosen here.
|
||||||
// lint-disable-next-line no-char-pointer
|
// 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
|
// Delete the bin/ and build/ trees beside `projectFile`, returning the paths
|
||||||
// that existed and were removed. Backs `crafter-build clean`.
|
// 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
|
// to compile (and its git dependencies to be present) is useless exactly
|
||||||
// then. That means variant directories are not enumerated — the whole tree
|
// then. That means variant directories are not enumerated — the whole tree
|
||||||
// goes, which is what the by-hand `rm -rf bin build` did anyway.
|
// 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
|
// 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
|
// 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
|
// this and shouldn't carry the extra files. Call from project.cpp after
|
||||||
// outputName is set; index.html is generated against the current
|
// outputName is set; index.html is generated against the current
|
||||||
// outputName so renaming the binary later requires another call.
|
// 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
|
// Register the relaxed-SIMD codegen variant on cfg. Builds an additional
|
||||||
// outputName.relaxed-simd.wasm compiled with -mrelaxed-simd (FMA,
|
// outputName.relaxed-simd.wasm compiled with -mrelaxed-simd (FMA,
|
||||||
|
|
@ -622,17 +619,17 @@ export namespace Crafter {
|
||||||
// 2026). Idempotent. Call before EnableWasiBrowserRuntime so the emitted
|
// 2026). Idempotent. Call before EnableWasiBrowserRuntime so the emitted
|
||||||
// variants.json manifest includes it. A no-op for non-wasm targets at build
|
// variants.json manifest includes it. A no-op for non-wasm targets at build
|
||||||
// time (no wasm is produced), but harmless to declare unconditionally.
|
// 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
|
// View over the project's args with simple query helpers. Has(flag) for
|
||||||
// boolean switches; Get(prefix) for valued options (e.g. Get("--prefix=")
|
// boolean switches; Get(prefix) for valued options (e.g. Get("--prefix=")
|
||||||
// returns the substring after the equals). Definitions are out-of-line
|
// returns the substring after the equals). Definitions are out-of-line so
|
||||||
// and CRAFTER_API so they cross the Windows DLL boundary cleanly — clang
|
// project.so resolves them from the executable: clang does not emit
|
||||||
// does not emit module-attached in-class inline bodies into consumers.
|
// module-attached in-class inline bodies into consumers.
|
||||||
struct ArgQuery {
|
struct ArgQuery {
|
||||||
std::span<const std::string_view> args;
|
std::span<const std::string_view> args;
|
||||||
CRAFTER_API bool Has(std::string_view flag) const;
|
bool Has(std::string_view flag) const;
|
||||||
CRAFTER_API std::optional<std::string> Get(std::string_view prefix) const;
|
std::optional<std::string> Get(std::string_view prefix) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply the framework's standard CLI args + env vars onto cfg:
|
// 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
|
// pre-set defaults. Returns an ArgQuery over the same span so projects
|
||||||
// can query their own flags (`--timing`, ...) without re-rolling the
|
// can query their own flags (`--timing`, ...) without re-rolling the
|
||||||
// for-arg-in-args loop.
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
|
||||||
#include "Crafter.Build-Api.h"
|
|
||||||
export module Crafter.Build:External;
|
export module Crafter.Build:External;
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -48,13 +46,13 @@ export namespace Crafter {
|
||||||
// declaration and target — BuildExternal derives its own working directory
|
// declaration and target — BuildExternal derives its own working directory
|
||||||
// through this, so a caller that only needs to know where the headers ended
|
// through this, so a caller that only needs to know where the headers ended
|
||||||
// up cannot drift from where they actually are.
|
// 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.
|
// 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
|
// 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
|
// 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.
|
// 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);
|
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);
|
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.
|
// Specification for a sibling crafter-build project to fetch and depend on.
|
||||||
// GitSource picks the revision: leave branch + commit empty for the
|
// 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
|
// remote's project.cpp, and returns a stable Configuration* you can drop
|
||||||
// into cfg.dependencies. The Configuration is owned by an internal cache
|
// into cfg.dependencies. The Configuration is owned by an internal cache
|
||||||
// for the lifetime of the build; identical specs share one Configuration.
|
// 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
|
// Specification for a local sibling crafter-build project (e.g. an
|
||||||
// example folder depending on its parent project, or a workspace where
|
// 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
|
// cache copy. Resolves projectFile to its canonical absolute path and
|
||||||
// recursively LoadProject's it. Returns a stable Configuration* owned
|
// recursively LoadProject's it. Returns a stable Configuration* owned
|
||||||
// by the same internal cache as GitProject.
|
// 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
|
// Clone (or update) the given git source into the shared external
|
||||||
// cache and return the path to the cloned working tree. Intended for
|
// 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;
|
// ExternalDependency / GitProject. Caching key = url + branch + commit;
|
||||||
// identical calls reuse the existing clone. Pin `source.commit` to a
|
// identical calls reuse the existing clone. Pin `source.commit` to a
|
||||||
// SHA for reproducible, offline-after-first-fetch builds.
|
// SHA for reproducible, offline-after-first-fetch builds.
|
||||||
CRAFTER_API fs::path GitFetch(const GitSource& source);
|
fs::path GitFetch(const GitSource& source);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
|
||||||
#include "Crafter.Build-Api.h"
|
|
||||||
export module Crafter.Build:Implementation;
|
export module Crafter.Build:Implementation;
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -19,8 +17,8 @@ namespace Crafter {
|
||||||
// no reachable Configuration provided when the source was scanned.
|
// no reachable Configuration provided when the source was scanned.
|
||||||
std::vector<std::string> pendingImports;
|
std::vector<std::string> pendingImports;
|
||||||
fs::path path;
|
fs::path path;
|
||||||
CRAFTER_API Implementation(fs::path&& path);
|
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;
|
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;
|
void Compile(const std::string_view clang, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError) const;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
|
||||||
#include "Crafter.Build-Api.h"
|
|
||||||
export module Crafter.Build:Interface;
|
export module Crafter.Build:Interface;
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -26,9 +24,9 @@ namespace Crafter {
|
||||||
bool checked = false;
|
bool checked = false;
|
||||||
std::string name;
|
std::string name;
|
||||||
fs::path path;
|
fs::path path;
|
||||||
CRAFTER_API ModulePartition(std::string&& name, fs::path&& path);
|
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());
|
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);
|
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 {
|
export class Module {
|
||||||
|
|
@ -48,8 +46,8 @@ namespace Crafter {
|
||||||
std::vector<std::unique_ptr<ModulePartition>> partitions;
|
std::vector<std::unique_ptr<ModulePartition>> partitions;
|
||||||
std::string name;
|
std::string name;
|
||||||
fs::path path;
|
fs::path path;
|
||||||
CRAFTER_API Module(std::string&& name, fs::path&& path);
|
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());
|
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);
|
void Compile(const std::string_view clang, const fs::path& pcmDir, const fs::path& buildDir, std::atomic<bool>& buildCancelled, std::string& buildError);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
|
||||||
#include "Crafter.Build-Api.h"
|
|
||||||
export module Crafter.Build:Lint;
|
export module Crafter.Build:Lint;
|
||||||
import std;
|
import std;
|
||||||
import :Clang;
|
import :Clang;
|
||||||
|
|
@ -63,5 +61,5 @@ export namespace Crafter {
|
||||||
// final content differs from the original. Prints per-mode output:
|
// final content differs from the original. Prints per-mode output:
|
||||||
// findings compiler-style (Report), would-change paths (Check), or
|
// findings compiler-style (Report), would-change paths (Check), or
|
||||||
// formatted paths (Apply), plus a summary line.
|
// formatted paths (Apply), plus a summary line.
|
||||||
CRAFTER_API LintSummary RunLint(Configuration& projectCfg, const RunLintOptions& opts);
|
LintSummary RunLint(Configuration& projectCfg, const RunLintOptions& opts);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
|
||||||
#include "Crafter.Build-Api.h"
|
|
||||||
export module Crafter.Build:Platform;
|
export module Crafter.Build:Platform;
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -30,9 +28,9 @@ namespace Crafter {
|
||||||
// Progress::Diagnostic), otherwise the compiler's diagnostics. Callers
|
// Progress::Diagnostic), otherwise the compiler's diagnostics. Callers
|
||||||
// therefore keep the `if (!result.empty())` shape they already had.
|
// therefore keep the `if (!result.empty())` shape they already had.
|
||||||
std::string RunBuildCommand(std::string_view command);
|
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);
|
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
|
// A dependency the project file currently running its CrafterBuildProject
|
||||||
// declared with `#pragma crafter dependency`, so it can also be wired
|
// declared with `#pragma crafter dependency`, so it can also be wired
|
||||||
// into the build graph (a test that links the same module, say) without
|
// 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.
|
// name or any module it exports — whatever the project file `import`s.
|
||||||
// Throws when no such dependency was declared, or when called from
|
// Throws when no such dependency was declared, or when called from
|
||||||
// anywhere other than inside LoadProject.
|
// 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
|
// True while the project file crafter-build was pointed at runs its
|
||||||
// CrafterBuildProject, and when no project file is running at all; false
|
// CrafterBuildProject, and when no project file is running at all; false
|
||||||
// inside a dependency's project file (a `#pragma crafter dependency`,
|
// 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
|
// 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
|
// 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.
|
// 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
|
// Resolves the directory holding distributed runtime assets (Crafter.Build
|
||||||
// module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise
|
// module sources, wasi-runtime/, etc). Honors CRAFTER_BUILD_HOME; otherwise
|
||||||
// derives <prefix>/share/crafter-build from the running executable's path.
|
// 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`
|
// Newest mtime among the prerequisites a compiler recorded in `depFile`
|
||||||
// (clang's `-MD -MF <artifact>.d`). This is how a build step learns which
|
// (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`
|
// headers its source pulled in: the module scanner only reads `import`
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,20 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
|
||||||
#include "Crafter.Build-Api.h"
|
|
||||||
export module Crafter.Build:Progress;
|
export module Crafter.Build:Progress;
|
||||||
import std;
|
import std;
|
||||||
|
|
||||||
export namespace Crafter::Progress {
|
export namespace Crafter::Progress {
|
||||||
enum class Verbosity { Quiet, Default, Verbose };
|
enum class Verbosity { Quiet, Default, Verbose };
|
||||||
|
|
||||||
CRAFTER_API void SetVerbosity(Verbosity v);
|
void SetVerbosity(Verbosity v);
|
||||||
CRAFTER_API Verbosity GetVerbosity();
|
Verbosity GetVerbosity();
|
||||||
|
|
||||||
// RAII work-unit marker. Construction inflates the running total; destruction
|
// RAII work-unit marker. Construction inflates the running total; destruction
|
||||||
// increments completed and (on Default + TTY) redraws the status line. Move
|
// increments completed and (on Default + TTY) redraws the status line. Move
|
||||||
// semantics are disabled — keep these as plain stack locals inside the worker
|
// semantics are disabled — keep these as plain stack locals inside the worker
|
||||||
// lambda so the destructor fires when that thread's work ends.
|
// lambda so the destructor fires when that thread's work ends.
|
||||||
class CRAFTER_API Task {
|
class Task {
|
||||||
public:
|
public:
|
||||||
explicit Task(std::string label);
|
explicit Task(std::string label);
|
||||||
~Task();
|
~Task();
|
||||||
|
|
@ -29,18 +27,18 @@ export namespace Crafter::Progress {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Verbose-mode command echo. No-op outside Verbose.
|
// 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
|
// 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
|
// warnings. Erases the status line first so the text doesn't land on top of
|
||||||
// it, then writes to stderr. No-op on Quiet.
|
// 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,
|
// 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.
|
// 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.
|
// Print "Built N targets in Xms" (or equivalent) and clear in-place state.
|
||||||
// Safe to call multiple times; subsequent calls are no-ops.
|
// Safe to call multiple times; subsequent calls are no-ops.
|
||||||
CRAFTER_API void Finalize();
|
void Finalize();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
|
||||||
#include "Crafter.Build-Api.h"
|
|
||||||
export module Crafter.Build:Shader;
|
export module Crafter.Build:Shader;
|
||||||
import std;
|
import std;
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
@ -30,8 +28,8 @@ namespace Crafter {
|
||||||
fs::path path;
|
fs::path path;
|
||||||
std::string entrypoint;
|
std::string entrypoint;
|
||||||
ShaderType type;
|
ShaderType type;
|
||||||
CRAFTER_API Shader(fs::path&& path, std::string&& entrypoint, ShaderType type);
|
Shader(fs::path&& path, std::string&& entrypoint, ShaderType type);
|
||||||
CRAFTER_API bool Check(const fs::path& outputDir) const;
|
bool Check(const fs::path& outputDir) const;
|
||||||
CRAFTER_API std::string Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const;
|
std::string Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// SPDX-License-Identifier: LGPL-3.0-only
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||||
|
|
||||||
module;
|
|
||||||
#include "Crafter.Build-Api.h"
|
|
||||||
export module Crafter.Build:Test;
|
export module Crafter.Build:Test;
|
||||||
import std;
|
import std;
|
||||||
import :Clang;
|
import :Clang;
|
||||||
|
|
@ -28,7 +26,7 @@ export namespace Crafter {
|
||||||
// avoid the double-`GetInterfacesAndImplementations` footgun (calling
|
// avoid the double-`GetInterfacesAndImplementations` footgun (calling
|
||||||
// it twice would re-parse the same .cppm files into duplicate Module
|
// it twice would re-parse the same .cppm files into duplicate Module
|
||||||
// entries).
|
// entries).
|
||||||
struct CRAFTER_API TestBuilder {
|
struct TestBuilder {
|
||||||
Configuration* parent;
|
Configuration* parent;
|
||||||
std::size_t index;
|
std::size_t index;
|
||||||
|
|
||||||
|
|
@ -85,8 +83,8 @@ export namespace Crafter {
|
||||||
bool AllPassed() const { return failed == 0 && crashed == 0 && timedOut == 0; }
|
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 = {});
|
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);
|
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
|
// Parent-project access for test fixtures. Run() sets the pointer to the
|
||||||
// root project's Configuration before discovery; fixtures call ParentLib()
|
// 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
|
// parent itself) so they can declare it as a dependency without rebuilding
|
||||||
// the lib config from scratch. Lookup is by Configuration::name and
|
// the lib config from scratch. Lookup is by Configuration::name and
|
||||||
// recurses through the parent's dependency graph.
|
// recurses through the parent's dependency graph.
|
||||||
CRAFTER_API void SetParentProject(Configuration* parent);
|
void SetParentProject(Configuration* parent);
|
||||||
CRAFTER_API Configuration* ParentLib(std::string_view name);
|
Configuration* ParentLib(std::string_view name);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
19
project.cpp
19
project.cpp
|
|
@ -24,12 +24,9 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
CrafterBuildLib->name = "crafter.build-lib";
|
CrafterBuildLib->name = "crafter.build-lib";
|
||||||
CrafterBuildLib->outputName = "crafter-build";
|
CrafterBuildLib->outputName = "crafter-build";
|
||||||
ApplyStandardArgs(*CrafterBuildLib, args);
|
ApplyStandardArgs(*CrafterBuildLib, args);
|
||||||
// Windows builds (native msvc via build.cmd or cross-compiled mingw from
|
// Monolithic: project.so resolves Crafter:: symbols from the running
|
||||||
// Linux) need a DLL + import lib + launcher exe so LoadProject can
|
// executable, so the library is only ever linked statically into it.
|
||||||
// compile project.cpp against a stable ABI boundary. Linux is monolithic.
|
CrafterBuildLib->type = ConfigurationType::LibraryStatic;
|
||||||
CrafterBuildLib->type = (CrafterBuildLib->target == "x86_64-w64-mingw32" || CrafterBuildLib->target == "x86_64-pc-windows-msvc")
|
|
||||||
? ConfigurationType::LibraryDynamic
|
|
||||||
: ConfigurationType::LibraryStatic;
|
|
||||||
|
|
||||||
CrafterBuildLib->dependencies = { math, asset };
|
CrafterBuildLib->dependencies = { math, asset };
|
||||||
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_HAS_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.source.branch = "main";
|
||||||
glslang.builder = ExternalBuilder::CMake;
|
glslang.builder = ExternalBuilder::CMake;
|
||||||
glslang.options = { "-DENABLE_OPT=OFF" };
|
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.includeDirs = { "" };
|
||||||
glslang.libs = { "SPIRV", "GenericCodeGen", "glslang", "OSDependent", "MachineIndependent", "glslang-default-resource-limits" };
|
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("-Wl,--export-dynamic");
|
||||||
cfg.linkFlags.push_back("-ldl");
|
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
|
// Self-tests link the local crafter-build library and exercise it in
|
||||||
// process. The harness (whichever crafter-build invokes `test`) compiles
|
// process. The harness (whichever crafter-build invokes `test`) compiles
|
||||||
|
|
|
||||||
|
|
@ -71,11 +71,7 @@ namespace {
|
||||||
app.dependencies = { lib };
|
app.dependencies = { lib };
|
||||||
// The link step reads a command's output the same way a compile does,
|
// 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.
|
// 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;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@ import Crafter.Build;
|
||||||
using namespace Crafter;
|
using namespace Crafter;
|
||||||
|
|
||||||
// POSIX env helpers — libc++ exports only std::getenv via `import std`, so
|
// POSIX env helpers — libc++ exports only std::getenv via `import std`, so
|
||||||
// forward-declare the setters we need against the C library directly. The
|
// forward-declare the setters we need against the C library directly.
|
||||||
// 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.
|
|
||||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||||
extern "C" int unsetenv(const char* name);
|
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");
|
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;
|
Configuration cfg;
|
||||||
cfg.target = "x86_64-w64-mingw32";
|
cfg.target = "x86_64-w64-mingw32";
|
||||||
TestRunner r = TestRunner::ForTarget(cfg);
|
TestRunner r = TestRunner::ForTarget(cfg);
|
||||||
if (HostTarget().find("linux") != std::string::npos) {
|
Check(r.name == "wine", "ForTarget(mingw) uses wine");
|
||||||
Check(r.name == "wine", "ForTarget(mingw) on Linux host uses wine");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromEnv: when the env var is set, it wins over the fallback.
|
// FromEnv: when the env var is set, it wins over the fallback.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue