test runner, cross-target runners, lib/exe split

- subprocess-isolated test runner (replaces V1 dlopen-RunTest);
  Pass/Fail/Crash/Timeout/Skipped outcomes via :Test partition
- TestRunner abstraction with command templates: Local, Ssh,
  SshWin (cmd.exe-shell), QemuUser, FromEnv; probe-based skip
  when runner unreachable
- transitive PCM-path propagation in Build(); resolveImport
  walks deps recursively; depResults cache keyed by PcmDir()
  so per-target builds don't collide
- cfg.sysroot threaded through BuildStdPcm + base compile/link
  command (enables aarch64 cross via Arch Linux ARM rootfs)
- lib + exe split: project.cpp defines crafterBuildLib
  (LibraryStatic) + crafterBuildExe (Executable depending on
  it); build.sh produces lib/libcrafter-build.a alongside
  bin/crafter-build for downstream static-link consumers
- Windows DLL+launcher: CRAFTER_API macro, /EXPORT flag for
  project.dll's CrafterBuildProject; Crafter::Run as the real
  entry point with main.cpp as a thin wrapper
- 18 tests: HelloWorld/WithModule/Defines/CrossProjectModule/
  Diamond × (Linux + sshwin:winvm), plus Incremental,
  BuildError, Libraries, RunnerClassification, QemuUser,
  SshRunner, WindowsViaSsh, CrossArchAarch64
- single ./bin/crafter-build test runs everything; Windows
  variants skip gracefully if winvm SSH alias unreachable

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-04-27 22:32:19 +02:00
commit cdfdb976c8
60 changed files with 2029 additions and 104 deletions

View file

@ -59,7 +59,7 @@ std::string Crafter::RunCommand(const std::string_view cmd) {
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> buffer;
CommandResult result{0, ""};
CommandResult result{};
std::string with = "cmd /C \"" + std::string(cmd) + " 2>&1\"";
@ -76,6 +76,10 @@ CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
return result;
}
CommandResult Crafter::RunCommandWithTimeout(std::string_view, std::chrono::seconds) {
throw std::runtime_error("RunCommandWithTimeout not yet implemented on Windows");
}
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);
@ -99,13 +103,14 @@ std::string Crafter::GetBaseCommand(const Configuration& config) {
}
namespace {
constexpr std::array<std::string_view, 7> kCrafterBuildModules = {
constexpr std::array<std::string_view, 8> kCrafterBuildModules = {
"Crafter.Build-Shader",
"Crafter.Build-Platform",
"Crafter.Build-Interface",
"Crafter.Build-Implementation",
"Crafter.Build-External",
"Crafter.Build-Clang",
"Crafter.Build-Test",
"Crafter.Build",
};
@ -121,7 +126,7 @@ namespace {
}
std::string cmd = std::format(
"clang++ --target=x86_64-pc-windows-msvc -march=native -mtune=native "
"-std=c++26 -O3 "
"-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={} "
@ -178,9 +183,11 @@ Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const
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 "
"-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(),
absProject.string(), crafterBuildLib.string(), dllPath.string());
@ -229,7 +236,7 @@ std::string Crafter::RunCommand(const std::string_view cmd) {
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
std::array<char, 128> buffer;
CommandResult result{0, ""};
CommandResult result{};
std::string with = std::string(cmd) + " 2>&1";
FILE* pipe = popen(with.c_str(), "r");
@ -243,7 +250,47 @@ CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
if (WIFEXITED(status)) {
result.exitCode = WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
result.exitCode = 128 + WTERMSIG(status);
result.signal = WTERMSIG(status);
result.exitCode = 128 + result.signal;
result.crashed = true;
} else {
result.exitCode = -1;
}
return result;
}
CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::seconds timeout) {
std::array<char, 128> buffer;
CommandResult result{};
std::string wrapped = std::format(
"timeout --kill-after=2 {} {} 2>&1",
timeout.count(), cmd);
FILE* pipe = popen(wrapped.c_str(), "r");
if (!pipe) throw std::runtime_error("popen() failed!");
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
result.output += buffer.data();
}
int status = pclose(pipe);
if (WIFEXITED(status)) {
int code = WEXITSTATUS(status);
if (code == 124) {
result.timedOut = true;
result.exitCode = 124;
} else if (code >= 128) {
result.signal = code - 128;
result.exitCode = code;
result.crashed = true;
} else {
result.exitCode = code;
}
} else if (WIFSIGNALED(status)) {
result.signal = WTERMSIG(status);
result.exitCode = 128 + result.signal;
result.crashed = true;
} else {
result.exitCode = -1;
}
@ -276,8 +323,14 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
return "";
}
} else {
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time("/usr/share/libc++/v1/std.cppm")) {
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++ -march={} -mtune={} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile /usr/share/libc++/v1/std.cppm -o {}", config.target, config.march, config.mtune, stdPcm.string()));
std::string stdCppm = config.sysroot.empty()
? std::string("/usr/share/libc++/v1/std.cppm")
: std::format("{}/usr/share/libc++/v1/std.cppm", config.sysroot);
std::string sysrootFlag = config.sysroot.empty()
? std::string()
: std::format(" --sysroot={}", config.sysroot);
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCppm)) {
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++{} -march={} -mtune={} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, sysrootFlag, config.march, config.mtune, stdCppm, stdPcm.string()));
} else {
return "";
}
@ -307,13 +360,14 @@ std::string Crafter::GetBaseCommand(const Configuration& config) {
}
namespace {
constexpr std::array<std::string_view, 7> kCrafterBuildModules = {
constexpr std::array<std::string_view, 8> kCrafterBuildModules = {
"Crafter.Build-Shader",
"Crafter.Build-Platform",
"Crafter.Build-Interface",
"Crafter.Build-Implementation",
"Crafter.Build-External",
"Crafter.Build-Clang",
"Crafter.Build-Test",
"Crafter.Build",
};