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>
126 lines
5.4 KiB
C++
126 lines
5.4 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;
|
|
|
|
// A shader's output is stale when anything that went into it changed, not
|
|
// only its own source. Check() used to compare the .spv against the source's
|
|
// mtime alone, so:
|
|
// - editing a file the shader #includes left the .spv built against the old
|
|
// contents, the GLSL twin of IncrementalHeaderChange;
|
|
// - changing an entry's defines without touching the source did too.
|
|
// Each pass changes one thing and checks both directions: the shader it
|
|
// reaches is recompiled, to new contents, and the shader that includes
|
|
// nothing is left alone. A build with nothing changed compiles nothing.
|
|
namespace {
|
|
std::int32_t failures = 0;
|
|
|
|
void Check(bool cond, std::string_view msg) {
|
|
if (!cond) {
|
|
std::println(std::cerr, "FAIL: {}", msg);
|
|
++failures;
|
|
}
|
|
}
|
|
|
|
// The fixture is edited between passes, so work on a copy outside the repo.
|
|
fs::path StageFixture() {
|
|
fs::path source = fs::current_path() / "tests" / "ShaderIncludeChange" / "fixture";
|
|
fs::path staged = fs::temp_directory_path() / "crafter-build-shader-include-change";
|
|
fs::remove_all(staged);
|
|
fs::copy(source, staged, fs::copy_options::recursive);
|
|
return staged;
|
|
}
|
|
|
|
Configuration MakeApp(const fs::path& staged) {
|
|
Configuration app;
|
|
app.path = staged;
|
|
app.name = "lit";
|
|
app.outputName = "lit";
|
|
app.target = HostTarget();
|
|
app.type = ConfigurationType::Executable;
|
|
std::array<fs::path, 0> ifaces = {};
|
|
std::array<fs::path, 1> impls = { "main" };
|
|
app.GetInterfacesAndImplementations(ifaces, impls);
|
|
app.shaders.emplace_back(staged / "lit.frag", std::string("main"), ShaderType::Fragment);
|
|
app.shaders.emplace_back(staged / "plain.frag", std::string("main"), ShaderType::Fragment);
|
|
// Puts include/ on the shaders' #include path.
|
|
app.buildFiles = { staged / "include" / "palette.glsl" };
|
|
return app;
|
|
}
|
|
|
|
bool BuildOk(Configuration& app, std::string_view label) {
|
|
// A fresh depResults per pass: the map memoizes a Configuration's
|
|
// build for the duration of one pass.
|
|
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
|
std::mutex depMutex;
|
|
BuildResult r = Build(app, depResults, depMutex);
|
|
Check(r.result.empty(), std::format("{} builds: {}", label, r.result));
|
|
return r.result.empty();
|
|
}
|
|
|
|
std::string ReadAll(const fs::path& file) {
|
|
std::ifstream stream(file, std::ios::binary);
|
|
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
const fs::path staged = StageFixture();
|
|
Configuration app = MakeApp(staged);
|
|
const fs::path lit = app.BinDir() / "lit.spv";
|
|
const fs::path plain = app.BinDir() / "plain.spv";
|
|
const fs::path palette = staged / "include" / "palette.glsl";
|
|
|
|
// Pass 1: everything from scratch.
|
|
if (!BuildOk(app, "the first pass")) return 1;
|
|
Check(fs::exists(lit) && fs::exists(plain), "both shaders are in the bin dir");
|
|
const fs::file_time_type plainBuilt = fs::last_write_time(plain);
|
|
const std::string green = ReadAll(lit);
|
|
|
|
// Pass 2: nothing changed.
|
|
fs::file_time_type litBuilt = fs::last_write_time(lit);
|
|
BuildOk(app, "an unchanged project");
|
|
Check(fs::last_write_time(lit) == litBuilt && fs::last_write_time(plain) == plainBuilt, "with nothing changed, no shader is recompiled");
|
|
|
|
// Pass 3: the included file changes. Dated just after the output it must
|
|
// outdate, so the rebuilt output is newer than it again.
|
|
{
|
|
std::ofstream out(palette, std::ios::trunc);
|
|
out << "const vec4 kTint = vec4(1.0, 0.0, 0.0, 1.0);\n";
|
|
}
|
|
fs::last_write_time(palette, litBuilt + std::chrono::milliseconds(1));
|
|
BuildOk(app, "an edited include");
|
|
const std::string red = ReadAll(lit);
|
|
Check(fs::last_write_time(lit) > litBuilt && red != green, "editing an included file recompiles the shader that includes it, to new contents");
|
|
Check(fs::last_write_time(plain) == plainBuilt, "and leaves the shader that includes nothing alone");
|
|
|
|
// Pass 4: the entry's defines change, no file does.
|
|
app.shaders[0].defines = { "LIT_DIM" };
|
|
litBuilt = fs::last_write_time(lit);
|
|
BuildOk(app, "a new define");
|
|
const std::string dim = ReadAll(lit);
|
|
Check(fs::last_write_time(lit) > litBuilt && dim != red, "a define added to the entry recompiles it, taking the #ifdef branch");
|
|
Check(fs::last_write_time(plain) == plainBuilt, "and leaves the other entry alone");
|
|
|
|
// Pass 5: and back.
|
|
app.shaders[0].defines = {};
|
|
BuildOk(app, "the define taken out");
|
|
Check(ReadAll(lit) == red, "taking the define out again recompiles it as it was");
|
|
|
|
// Pass 6: nothing changed again; what the passes above recorded holds.
|
|
litBuilt = fs::last_write_time(lit);
|
|
BuildOk(app, "an unchanged project, again");
|
|
Check(fs::last_write_time(lit) == litBuilt && fs::last_write_time(plain) == plainBuilt, "with nothing changed since, no shader is recompiled");
|
|
|
|
std::error_code ec;
|
|
fs::remove_all(staged, ec);
|
|
|
|
if (failures > 0) {
|
|
std::println(std::cerr, "{} assertions failed", failures);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|