fix: rebuild and relink on what changed, submodules at their revisions, and mingw found where it is
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>
This commit is contained in:
Jorijn van der Graaf 2026-10-20 19:08:13 +02:00
commit 653dd0bb9b
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
26 changed files with 1075 additions and 109 deletions

View file

@ -8,6 +8,7 @@ module;
#include "../lib/DirStackFileIncluder.h"
module Crafter.Build:Shader_impl;
import :Shader;
import :Platform;
import std;
namespace fs = std::filesystem;
@ -31,6 +32,39 @@ namespace {
}
return EShLangVertex;
}
// A path as a Make prerequisite, escaped the way NewestPrerequisite reads it.
std::string MakeEscaped(const std::string& path) {
std::string escaped;
for (const char c : path) {
if (c == ' ' || c == '\t' || c == '#') escaped += '\\';
if (c == '$') escaped += '$';
escaped += c;
}
return escaped;
}
std::string ReadFile(const fs::path& file) {
std::ifstream stream(file, std::ios::binary);
return std::string(std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
}
// What a compile's output depends on besides files' contents, one line
// each: two entries of one source differ here, and an #include searched
// for from another source directory or in other directories can find
// another file.
std::string Settings(const Crafter::Shader& shader, std::span<const fs::path> includeDirs) {
std::string settings = std::format("source {}\nstage {}\nentry {}\n", fs::absolute(shader.path).string(), static_cast<int>(shader.type), shader.entrypoint);
for (const std::string& define : shader.defines) settings += std::format("define {}\n", define);
for (const fs::path& dir : includeDirs) settings += std::format("include {}\n", dir.string());
return settings;
}
// Where Compile records, for Check, what the output in the bin dir was
// built from: beside the configuration's objects, as their depfiles are.
fs::path Record(const Crafter::Shader& shader, const fs::path& buildDir, std::string_view kind) {
return buildDir / std::format("{}.{}", shader.OutputName().string(), kind);
}
}
namespace Crafter {
@ -45,11 +79,25 @@ namespace Crafter {
name.replace_extension(variant.empty() ? std::string("spv") : std::format("{}.spv", variant));
return name;
}
bool Shader::Check(const fs::path& outputDir) const {
fs::path spv = outputDir / OutputName();
return fs::exists(spv) && fs::last_write_time(path) < fs::last_write_time(spv);
bool Shader::Check(const fs::path& outputDir, const fs::path& buildDir, std::span<const fs::path> includeDirs) const {
std::error_code ec;
const fs::file_time_type built = fs::last_write_time(outputDir / OutputName(), ec);
if (ec) return false;
// The source and every file it #included, as the last compile
// recorded them: without a record, or with an included file since
// deleted, NewestPrerequisite reads max(), which is stale.
if (std::max(fs::last_write_time(path), NewestPrerequisite(Record(*this, buildDir, "d"))) >= built) return false;
return ReadFile(Record(*this, buildDir, "settings")) == Settings(*this, includeDirs);
}
std::string Shader::Compile(const fs::path& outputDir, std::span<const fs::path> includeDirs) const {
std::string Shader::Compile(const fs::path& outputDir, const fs::path& buildDir, std::span<const fs::path> includeDirs) const {
// The records go first, so a compile that fails leaves the output
// stale, whatever it wrote.
const fs::path depFile = Record(*this, buildDir, "d");
const fs::path settingsFile = Record(*this, buildDir, "settings");
std::error_code removed;
fs::remove(depFile, removed);
fs::remove(settingsFile, removed);
EShLanguage glslangType = ToEShLanguage(type);
glslang::InitializeProcess();
@ -157,6 +205,25 @@ namespace Crafter {
if (!spvLog.empty()) {
return fail("SPIR-V codegen reported issues", std::move(spvLog));
}
// What Check compares the output against next time: the source and
// the files it included, as absolute paths so that reading them back
// doesn't depend on the directory a build runs from, and the settings.
std::ofstream dep(depFile, std::ios::binary);
dep << MakeEscaped(fs::absolute(outputDir / filename).string()) << ": " << MakeEscaped(fs::absolute(path).string());
for (const std::string& included : includeDir.getIncludedFiles()) {
dep << " \\\n " << MakeEscaped(fs::absolute(included).string());
}
dep << '\n';
dep.close();
std::ofstream settings(settingsFile, std::ios::binary);
settings << Settings(*this, includeDirs);
settings.close();
if (!dep || !settings) {
fs::remove(depFile, removed);
fs::remove(settingsFile, removed);
return fail("failed to record what the shader was built from", buildDir.string());
}
glslang::FinalizeProcess();
return {};
}