Some checks failed
Flatpak / flatpak (push) Failing after 1m12s
- Shaders record the files glslang opened (a depfile) and their settings
(source path, stage, entry point, defines, include directories) in the
build directory, and rebuild when an included file or a setting changes,
not only the shader's own source.
- An executable relinks when its link command changed, not only when an
object is newer: a link flag in project.cpp, or a dependency's library
that moved (an ExternalDependency built into a new cache directory). The
command is recorded beside the objects; its flags come from ordered sets,
so the order the dependency threads finish in can't change it.
- An external's newest artifact counts its libDirs and versioned .so names.
- Submodules are checked out at the revisions the fetched commit records,
from the URLs its .gitmodules gives, after every clone, switch, checkout
and pull, so a pinned commit builds against its own submodules, and one
moved by hand is put back; with no .gitmodules, git isn't asked.
- Each external clone's fetch and build hold a file lock: parallel builds
pulled into one clone at once ("cannot lock ref").
- mingw:
- -stdlib=libstdc++ always;
- the std module's source is found through libstdc++.modules.json in the
mingw toolchain's own library directories (the generic lib/ holds the
host's Linux one), asking the compiler once a run;
- a toolchain that isn't there is a build error saying where it looked,
not std::terminate;
- CRAFTER_MINGW_DIR names the prefix, the directory holding
x86_64-w64-mingw32/ and lib/gcc/;
- inside the Flatpak the host's /usr mingw is used.
- RunScript's exit-status check reads as the one range it is.
- Tests: ConcurrentExternalFetch, ExternalArtifactTime, GitSubmodules,
LinkFlagsChange, MingwMissing, ShaderIncludeChange; ShaderCompile and
ToolchainResolve extended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
6.6 KiB
C++
146 lines
6.6 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
import std;
|
|
import Crafter.Build;
|
|
namespace fs = std::filesystem;
|
|
using namespace Crafter;
|
|
|
|
// Compile a tiny GLSL fragment shader directly via the Shader API. Verifies
|
|
// the SPIR-V output lands in the target dir and starts with the SPIR-V magic
|
|
// number, without dragging in the full Build() pipeline. The second half
|
|
// registers one source twice, with and without defines, and checks that the
|
|
// variant gets its own output, its own staleness and the other #ifdef branch
|
|
// (tests/ShaderVariantBuild drives the same thing through Build()).
|
|
int main() {
|
|
fs::path src = fs::current_path() / "tests" / "ShaderCompile" / "fixture" / "triangle.frag";
|
|
if (!fs::exists(src)) {
|
|
std::println(std::cerr, "fixture missing: {}", src.string());
|
|
return 1;
|
|
}
|
|
|
|
fs::path outDir = fs::temp_directory_path() / "crafter-build-shader-test";
|
|
std::error_code ec;
|
|
fs::remove_all(outDir, ec);
|
|
fs::create_directories(outDir);
|
|
|
|
Shader shader(fs::path(src), "main", ShaderType::Fragment);
|
|
|
|
std::array<fs::path, 0> includeDirs = {};
|
|
std::string err = shader.Compile(outDir, outDir, includeDirs);
|
|
if (!err.empty()) {
|
|
std::println(std::cerr, "shader compile failed: {}", err);
|
|
return 1;
|
|
}
|
|
|
|
fs::path spv = outDir / "triangle.spv";
|
|
if (!fs::exists(spv)) {
|
|
std::println(std::cerr, "spv not produced at {}", spv.string());
|
|
return 1;
|
|
}
|
|
|
|
// SPIR-V binaries start with the magic number 0x07230203 (little-endian
|
|
// when written by glslang on x86). Read the first four bytes to confirm
|
|
// the produced file is real SPIR-V, not a fluke empty file.
|
|
std::ifstream in(spv, std::ios::binary);
|
|
std::uint32_t magic = 0;
|
|
in.read(reinterpret_cast<char*>(&magic), sizeof(magic));
|
|
if (!in || magic != 0x07230203u) {
|
|
std::println(std::cerr, "spv has wrong magic: 0x{:08x}", magic);
|
|
return 1;
|
|
}
|
|
|
|
// Compiling again should be a no-op since the source isn't newer.
|
|
if (!shader.Check(outDir, outDir, includeDirs)) {
|
|
std::println(std::cerr, "Shader::Check returned false on an up-to-date spv");
|
|
return 1;
|
|
}
|
|
// Searched elsewhere, an #include could find another file: stale.
|
|
const std::array<fs::path, 1> elsewhere = { outDir / "elsewhere" };
|
|
if (shader.Check(outDir, outDir, elsewhere)) {
|
|
std::println(std::cerr, "Shader::Check called an spv up to date for other include directories");
|
|
return 1;
|
|
}
|
|
// So could a source of the same name moved to another directory, which
|
|
// its #includes are searched from first.
|
|
const fs::path movedDir = outDir / "moved";
|
|
fs::create_directories(movedDir);
|
|
fs::copy_file(src, movedDir / src.filename());
|
|
fs::last_write_time(movedDir / src.filename(), fs::last_write_time(src));
|
|
Shader moved(movedDir / src.filename(), "main", ShaderType::Fragment);
|
|
if (moved.Check(outDir, outDir, includeDirs)) {
|
|
std::println(std::cerr, "Shader::Check called an spv up to date for a source of the same name elsewhere");
|
|
return 1;
|
|
}
|
|
|
|
// One source registered twice: the second entry carries defines and a
|
|
// variant name. It must land in its own file instead of overwriting the
|
|
// first, and it must take the other #ifdef branch. Crafter.Graphics ships
|
|
// each UI shader in a descriptor-heap and a descriptor-set flavour this
|
|
// way; if the variant were dropped from the output name, whichever compile
|
|
// finished last would silently win.
|
|
fs::path variantSrc = fs::current_path() / "tests" / "ShaderCompile" / "fixture" / "variant.frag";
|
|
Shader plain(fs::path(variantSrc), "main", ShaderType::Fragment);
|
|
Shader red(fs::path(variantSrc), "main", ShaderType::Fragment);
|
|
red.defines = {"VARIANT_RED", "VARIANT_ALPHA=0.5"};
|
|
red.variant = "red";
|
|
|
|
if (plain.OutputName() != "variant.spv" || red.OutputName() != "variant.red.spv") {
|
|
std::println(std::cerr, "unexpected output names: {} and {}", plain.OutputName().string(), red.OutputName().string());
|
|
return 1;
|
|
}
|
|
for (const Shader* each : {&plain, &red}) {
|
|
// Staleness is per output, not per source: while only variant.spv
|
|
// exists, the variant is still due. A Check() that ignored the
|
|
// variant would call it up to date here, and Build() would then
|
|
// never compile a variant added to an already built project.
|
|
if (each == &red && red.Check(outDir, outDir, includeDirs)) {
|
|
std::println(std::cerr, "Shader::Check called the variant up to date before it was ever compiled");
|
|
return 1;
|
|
}
|
|
std::string variantErr = each->Compile(outDir, outDir, includeDirs);
|
|
if (!variantErr.empty()) {
|
|
std::println(std::cerr, "variant shader compile failed: {}", variantErr);
|
|
return 1;
|
|
}
|
|
if (!each->Check(outDir, outDir, includeDirs)) {
|
|
std::println(std::cerr, "Shader::Check returned false for {}", each->OutputName().string());
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Malformed defines and variants are refused by name. Left to glslang, an
|
|
// empty macro name surfaces as "-1:<line>" with no file, and a variant
|
|
// with a separator as a failed open after the whole compile.
|
|
for (const char* bad : {"", "=1", "A\nB"}) {
|
|
Shader malformed(fs::path(variantSrc), "main", ShaderType::Fragment);
|
|
malformed.defines = {bad};
|
|
malformed.variant = "malformed";
|
|
if (!malformed.Compile(outDir, outDir, includeDirs).contains("invalid shader define")) {
|
|
std::println(std::cerr, "a malformed define was not rejected by name");
|
|
return 1;
|
|
}
|
|
}
|
|
Shader nested(fs::path(variantSrc), "main", ShaderType::Fragment);
|
|
nested.variant = "sub/red";
|
|
if (!nested.Compile(outDir, outDir, includeDirs).contains("path separator")) {
|
|
std::println(std::cerr, "a variant with a path separator was not rejected by name");
|
|
return 1;
|
|
}
|
|
|
|
auto readAll = [](const fs::path& file) {
|
|
std::ifstream stream(file, std::ios::binary);
|
|
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
|
};
|
|
std::string plainSpv = readAll(outDir / "variant.spv");
|
|
std::string redSpv = readAll(outDir / "variant.red.spv");
|
|
if (plainSpv.empty() || redSpv.empty()) {
|
|
std::println(std::cerr, "a variant output is missing or empty");
|
|
return 1;
|
|
}
|
|
if (plainSpv == redSpv) {
|
|
std::println(std::cerr, "both variants compiled to the same SPIR-V: the defines did not reach the preprocessor");
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|