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

66
tests/Libraries.cpp Normal file
View file

@ -0,0 +1,66 @@
/*
Crafter® Build
Copyright (C) 2026 Catcrafts®
Catcrafts.net
LGPL-3.0-only.
*/
import std;
#include "TestUtil.h"
namespace fs = std::filesystem;
using namespace TestUtil;
int main() {
try {
fs::path projectRoot = fs::current_path();
fs::path src = projectRoot / "tests" / "fixtures" / "libraries";
fs::path crafterBuild = projectRoot / "bin" / "crafter-build";
fs::path work = CopyFixtureToTemp("Libraries", src);
auto build = RunInDir(work, std::format("'{}'", crafterBuild.string()));
if (build.exitCode != 0) {
std::println(std::cerr, "build failed (rc={}):\n{}", build.exitCode, build.output);
return 1;
}
fs::path staticArchive = work / "mathlib" / "bin" / "MathLib-x86_64-pc-linux-gnu-native" / "libMathLib.a";
fs::path dynamicSO = work / "greetlib" / "bin" / "GreetLib-x86_64-pc-linux-gnu-native" / "libGreetLib.so";
fs::path artifact = work / "bin" / "libs-app-x86_64-pc-linux-gnu-native" / "libs-app";
if (!fs::exists(staticArchive)) {
std::println(std::cerr, "static archive missing at {}", staticArchive.string());
return 1;
}
if (!fs::exists(dynamicSO)) {
std::println(std::cerr, "dynamic .so missing at {}", dynamicSO.string());
return 1;
}
if (!fs::exists(artifact)) {
std::println(std::cerr, "exe missing at {}\nbuild log:\n{}", artifact.string(), build.output);
return 1;
}
// The exe linked against a dynamic .so needs LD_LIBRARY_PATH or rpath to find it.
// Build() already passes -Wl,-rpath,'$ORIGIN' for shared libs, but the .so lives in
// greetlib/bin/... while the exe lives in bin/libs-app-... — different dirs. Set
// LD_LIBRARY_PATH explicitly.
auto run = RunInDir(work, std::format(
"LD_LIBRARY_PATH='{}' '{}'",
dynamicSO.parent_path().string(), artifact.string()));
if (run.exitCode != 0) {
std::println(std::cerr, "artifact exited nonzero (rc={}):\n{}", run.exitCode, run.output);
return 1;
}
if (run.output != "hi=42\n") {
std::println(std::cerr, "output mismatch:\n expected: \"hi=42\\n\"\n got: {:?}", run.output);
return 1;
}
return 0;
} catch (const std::exception& e) {
std::println(std::cerr, "test exception: {}", e.what());
return 1;
}
}