This commit is contained in:
parent
c0e4067639
commit
19b059a88c
11 changed files with 286 additions and 1 deletions
|
|
@ -13,7 +13,7 @@ Bootstrap requires `clang`, `cmake`, `git`, `lld`, and `libc++`.
|
|||
|
||||
```bash
|
||||
./build.sh # produces bin/crafter-build
|
||||
CRAFTER_BUILD_HOME=$PWD/build ./bin/crafter-build # rebuild via project.cpp
|
||||
./bin/crafter-build # rebuild via project.cpp
|
||||
```
|
||||
|
||||
For distro-packaged installs, `crafter-build` finds its modules at `<prefix>/share/crafter-build/` automatically — no env var required.
|
||||
|
|
|
|||
94
tests/Cuda/Cuda.cpp
Normal file
94
tests/Cuda/Cuda.cpp
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
LGPL-3.0-only.
|
||||
|
||||
End-to-end CUDA build through the V2 pipeline:
|
||||
the inner project.cpp registers a .cu translation unit via cfg.cuda, and
|
||||
crafter.build-lib drives nvcc to produce a host object that links into
|
||||
the final executable. The test skips when the CUDA SDK is missing
|
||||
(CI hosts have neither nvcc nor libcudart) and otherwise runs the built
|
||||
binary to confirm the .cu's host-callable entry returns through the link.
|
||||
|
||||
No GPU is required: the kernel is staged but never launched, so cudart's
|
||||
fat-binary registration runs at startup but device init does not.
|
||||
*/
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
#include "../_shared/TestUtil.h"
|
||||
namespace fs = std::filesystem;
|
||||
using namespace TestUtil;
|
||||
using namespace Crafter;
|
||||
|
||||
namespace {
|
||||
bool ToolPresent(std::string_view name) {
|
||||
return std::system(std::format("which {} > /dev/null 2>&1", name).c_str()) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
if (!ToolPresent("nvcc")) {
|
||||
Skip("nvcc not on PATH — install the CUDA SDK to enable this test");
|
||||
}
|
||||
|
||||
// Resolve the CUDA root from nvcc's own location so the inner project
|
||||
// can point the linker at libcudart.so without a hardcoded path.
|
||||
// Standard SDK layout puts nvcc at <root>/bin/nvcc and cudart at
|
||||
// <root>/lib64; if either assumption is broken we skip rather than
|
||||
// fail, since that means the install is non-standard.
|
||||
fs::path cwd = fs::current_path();
|
||||
auto probe = RunInDir(cwd, "command -v nvcc");
|
||||
if (probe.exitCode != 0 || probe.output.empty()) {
|
||||
Skip("nvcc not on PATH");
|
||||
}
|
||||
std::string nvccPath = probe.output;
|
||||
while (!nvccPath.empty() && (nvccPath.back() == '\n' || nvccPath.back() == '\r')) {
|
||||
nvccPath.pop_back();
|
||||
}
|
||||
fs::path real = fs::canonical(nvccPath);
|
||||
fs::path cudaLibDir = real.parent_path().parent_path() / "lib64";
|
||||
if (!fs::exists(cudaLibDir / "libcudart.so")) {
|
||||
Skip(std::format("libcudart.so not found at {} — incomplete CUDA SDK install",
|
||||
cudaLibDir.string()));
|
||||
}
|
||||
::setenv("CRAFTER_CUDA_LIBDIR", cudaLibDir.string().c_str(), 1);
|
||||
|
||||
fs::path src = cwd / "tests" / "Cuda" / "inner";
|
||||
Configuration cfg = LoadFixture("Cuda", src);
|
||||
fs::path work = fs::current_path();
|
||||
|
||||
auto br = BuildOnce(cfg);
|
||||
if (!br.result.empty()) {
|
||||
std::println(std::cerr, "build failed:\n{}", br.result);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fs::path artifact = work / "bin" / "cuda-test-x86_64-pc-linux-gnu-native" / "cuda-test";
|
||||
if (!fs::exists(artifact)) {
|
||||
std::println(std::cerr, "expected artifact missing at {}", artifact.string());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// libcudart.so is on the system loader path on most CUDA installs
|
||||
// (Arch puts /opt/cuda/lib64 in /etc/ld.so.conf.d), but other distros
|
||||
// don't, so set LD_LIBRARY_PATH explicitly for the run.
|
||||
auto run = RunInDir(work, std::format(
|
||||
"LD_LIBRARY_PATH='{}' '{}'", cudaLibDir.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 != "answer=42\n") {
|
||||
std::println(std::cerr, "output mismatch:\n expected: \"answer=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;
|
||||
}
|
||||
}
|
||||
16
tests/Cuda/inner/kernel.cu
Normal file
16
tests/Cuda/inner/kernel.cu
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// A staged-but-unlaunched kernel forces nvcc to emit a real fatbin and
|
||||
// the cudart-registration glue, which is what makes this a meaningful
|
||||
// CUDA-pipeline test. The host entry the C++ side actually calls returns
|
||||
// the same constant via plain CPU code so the test passes on hosts that
|
||||
// have the SDK but no NVIDIA driver/GPU.
|
||||
__device__ int compute_on_device() {
|
||||
return 42;
|
||||
}
|
||||
|
||||
__global__ void kernel(int* out) {
|
||||
*out = compute_on_device();
|
||||
}
|
||||
|
||||
extern "C" int kernel_compute() {
|
||||
return 42;
|
||||
}
|
||||
8
tests/Cuda/inner/main.cpp
Normal file
8
tests/Cuda/inner/main.cpp
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import std;
|
||||
|
||||
extern "C" int kernel_compute();
|
||||
|
||||
int main() {
|
||||
std::println("answer={}", kernel_compute());
|
||||
return 0;
|
||||
}
|
||||
29
tests/Cuda/inner/project.cpp
Normal file
29
tests/Cuda/inner/project.cpp
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "cuda-test";
|
||||
cfg.outputName = "cuda-test";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
|
||||
cfg.cuda.push_back("kernel");
|
||||
|
||||
// The outer test driver discovers the SDK location and exports it; nvcc
|
||||
// emits __cudaRegisterFatBinary references into every .cu object even
|
||||
// when no kernel is launched, so libcudart has to be on the link line.
|
||||
if (const char* libDir = std::getenv("CRAFTER_CUDA_LIBDIR")) {
|
||||
cfg.linkFlags.push_back(std::format("-L{}", libDir));
|
||||
cfg.linkFlags.push_back("-lcudart");
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
20
tests/Cuda/project.cpp
Normal file
20
tests/Cuda/project.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
Configuration cfg;
|
||||
cfg.path = "tests/Cuda/";
|
||||
cfg.name = "Cuda";
|
||||
cfg.outputName = "Cuda";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
cfg.dependencies = { ParentLib("crafter.build-lib") };
|
||||
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
||||
cfg.linkFlags.push_back("-ldl");
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "Cuda" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
return cfg;
|
||||
}
|
||||
61
tests/Shader/Shader.cpp
Normal file
61
tests/Shader/Shader.cpp
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
Crafter® Build
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
LGPL-3.0-only.
|
||||
|
||||
End-to-end shader build through the V2 pipeline:
|
||||
the inner project.cpp registers a vertex shader via cfg.shaders, and
|
||||
crafter.build-lib invokes glslang in-process to lower it to SPIR-V. The
|
||||
test verifies the resulting .spv file lands in the per-target bin dir
|
||||
and starts with the SPIR-V magic word — a zero-length output or one
|
||||
written under a different name would be flagged here rather than
|
||||
silently passing.
|
||||
*/
|
||||
|
||||
import std;
|
||||
import Crafter.Build;
|
||||
#include "../_shared/TestUtil.h"
|
||||
namespace fs = std::filesystem;
|
||||
using namespace TestUtil;
|
||||
using namespace Crafter;
|
||||
|
||||
int main() {
|
||||
try {
|
||||
fs::path src = fs::current_path() / "tests" / "Shader" / "inner";
|
||||
Configuration cfg = LoadFixture("Shader", src);
|
||||
fs::path work = fs::current_path();
|
||||
|
||||
auto br = BuildOnce(cfg);
|
||||
if (!br.result.empty()) {
|
||||
std::println(std::cerr, "build failed:\n{}", br.result);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fs::path spv = work / "bin" / "shader-test-x86_64-pc-linux-gnu-native" / "triangle.spv";
|
||||
if (!fs::exists(spv)) {
|
||||
std::println(std::cerr, "expected SPIR-V output missing at {}", spv.string());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// SPIR-V module header magic is 0x07230203, written as a 32-bit word
|
||||
// in the module's endianness (always little-endian on the targets we
|
||||
// build). Matching the raw byte sequence catches truncated writes
|
||||
// and any future regression that produces a non-SPIR-V .spv.
|
||||
std::ifstream f(spv, std::ios::binary);
|
||||
unsigned char magic[4] = {};
|
||||
f.read(reinterpret_cast<char*>(magic), 4);
|
||||
if (magic[0] != 0x03 || magic[1] != 0x02 || magic[2] != 0x23 || magic[3] != 0x07) {
|
||||
std::println(std::cerr,
|
||||
"SPIR-V magic mismatch: got {:#04x} {:#04x} {:#04x} {:#04x}",
|
||||
magic[0], magic[1], magic[2], magic[3]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::println(std::cerr, "test exception: {}", e.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
3
tests/Shader/inner/main.cpp
Normal file
3
tests/Shader/inner/main.cpp
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import std;
|
||||
|
||||
int main() { return 0; }
|
||||
21
tests/Shader/inner/project.cpp
Normal file
21
tests/Shader/inner/project.cpp
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
Configuration cfg;
|
||||
cfg.path = "./";
|
||||
cfg.name = "shader-test";
|
||||
cfg.outputName = "shader-test";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "main" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
|
||||
cfg.shaders.emplace_back("triangle.glsl", "main", ShaderType::Vertex);
|
||||
|
||||
return cfg;
|
||||
}
|
||||
13
tests/Shader/inner/triangle.glsl
Normal file
13
tests/Shader/inner/triangle.glsl
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#version 450
|
||||
|
||||
layout(location = 0) out vec3 color;
|
||||
|
||||
void main() {
|
||||
vec2 positions[3] = vec2[](
|
||||
vec2( 0.0, -0.5),
|
||||
vec2( 0.5, 0.5),
|
||||
vec2(-0.5, 0.5)
|
||||
);
|
||||
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
|
||||
color = vec3(1.0, 0.0, 0.0);
|
||||
}
|
||||
20
tests/Shader/project.cpp
Normal file
20
tests/Shader/project.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view>) {
|
||||
Configuration cfg;
|
||||
cfg.path = "tests/Shader/";
|
||||
cfg.name = "Shader";
|
||||
cfg.outputName = "Shader";
|
||||
cfg.target = "x86_64-pc-linux-gnu";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
cfg.dependencies = { ParentLib("crafter.build-lib") };
|
||||
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
||||
cfg.linkFlags.push_back("-ldl");
|
||||
std::array<fs::path, 0> ifaces = {};
|
||||
std::array<fs::path, 1> impls = { "Shader" };
|
||||
cfg.GetInterfacesAndImplementations(ifaces, impls);
|
||||
return cfg;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue