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>
971 lines
45 KiB
C++
971 lines
45 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
module;
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <limits.h>
|
|
#include <unistd.h>
|
|
#include <dlfcn.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/file.h>
|
|
#include <fcntl.h>
|
|
#include <errno.h>
|
|
module Crafter.Build:Platform_impl;
|
|
import std;
|
|
import :Platform;
|
|
import :Toolchain;
|
|
import :Clang;
|
|
import :Interface;
|
|
import :External;
|
|
import :Progress;
|
|
namespace fs = std::filesystem;
|
|
using namespace Crafter;
|
|
|
|
namespace {
|
|
// Exclusive advisory lock over one PCM cache dir (`<cache>/<target>-<march>/`),
|
|
// held for the duration of a (re)build. Concurrent crafter-build invocations —
|
|
// and the worker threads inside a single invocation — that share a cache
|
|
// serialize their builds through it: the first builder to acquire the lock
|
|
// precompiles std.pcm / the Crafter.Build-*.pcm modules while everyone else
|
|
// blocks, then re-checks freshness and reuses the finished file instead of
|
|
// re-running clang and racing to overwrite the same path. Each builder must
|
|
// pass through the lock before it reaches the compile steps that *read* those
|
|
// PCMs, so a reader never observes a half-written file.
|
|
//
|
|
// A FileLock on a `.lock` file in the cache dir.
|
|
struct CacheLock {
|
|
FileLock lock;
|
|
explicit CacheLock(const fs::path& cacheDir) : lock(cacheDir / ".lock") {}
|
|
};
|
|
|
|
// The cached Crafter.Build PCMs are keyed by `<target>-<march>` alone, so
|
|
// every crafter-build on the machine writes to the same files regardless of
|
|
// which share/crafter-build its module sources came from. Freshness used to
|
|
// be a per-file mtime comparison, which cannot distinguish "this PCM is
|
|
// newer than my source" from "this PCM was built from *different* sources
|
|
// that happen to be newer" — so a second install or checkout silently
|
|
// compiled its project against the other's declarations. Same failure shape
|
|
// as issue #27: no error, a binary built against a layout nobody linked.
|
|
//
|
|
// A stamp over the bytes of every module source answers what the mtime
|
|
// can't. It covers the whole set rather than one file at a time because the
|
|
// PCMs import each other: a change to :Interface invalidates :Clang's PCM
|
|
// even though Crafter.Build-Clang.cppm is untouched.
|
|
std::string CrafterBuildSourceStamp(const fs::path& sourceDir, std::span<const std::string_view> moduleNames) {
|
|
std::string all;
|
|
auto append = [&all, &sourceDir](const fs::path& relative) {
|
|
std::ifstream in(sourceDir / relative, std::ios::binary);
|
|
std::ostringstream buffer;
|
|
buffer << in.rdbuf();
|
|
all += relative.string();
|
|
all += '\0';
|
|
all += buffer.str();
|
|
all += '\0';
|
|
};
|
|
for (std::string_view name : moduleNames) {
|
|
append(fs::path(std::format("{}.cppm", name)));
|
|
}
|
|
return std::format("{:016x}", std::hash<std::string>{}(all));
|
|
}
|
|
|
|
fs::path CacheStampPath(const fs::path& cacheDir) {
|
|
return cacheDir / "crafter-build-sources.stamp";
|
|
}
|
|
|
|
// A stamp is a one-line content hash kept next to the artifact it
|
|
// describes. Written last, after the artifact, so an interrupted build
|
|
// leaves a stamp that disagrees and the next run starts over.
|
|
std::string ReadStamp(const fs::path& file) {
|
|
std::ifstream in(file, std::ios::binary);
|
|
std::string stamp;
|
|
std::getline(in, stamp);
|
|
return stamp;
|
|
}
|
|
|
|
void WriteStamp(const fs::path& file, std::string_view stamp) {
|
|
std::ofstream(file, std::ios::binary | std::ios::trunc) << stamp << '\n';
|
|
}
|
|
|
|
fs::path StampPath(const fs::path& artifact) {
|
|
fs::path stamp = artifact;
|
|
stamp += ".stamp";
|
|
return stamp;
|
|
}
|
|
|
|
std::string HashStamp(std::string_view content) {
|
|
return std::format("{:016x}", std::hash<std::string_view>{}(content));
|
|
}
|
|
|
|
std::string FileBytes(const fs::path& file) {
|
|
std::ifstream in(file, std::ios::binary);
|
|
std::ostringstream buffer;
|
|
buffer << in.rdbuf();
|
|
return buffer.str();
|
|
}
|
|
|
|
// The string value of `"key": "..."` in `object`, if it has one.
|
|
std::optional<std::string> JsonString(std::string_view object, std::string_view key) {
|
|
const std::string quoted = std::format("\"{}\"", key);
|
|
std::size_t at = object.find(quoted);
|
|
if (at == std::string_view::npos) return std::nullopt;
|
|
at = object.find_first_not_of(" \t\r\n", at + quoted.size());
|
|
if (at == std::string_view::npos || object[at] != ':') return std::nullopt;
|
|
at = object.find_first_not_of(" \t\r\n", at + 1);
|
|
if (at == std::string_view::npos || object[at] != '"') return std::nullopt;
|
|
std::string value;
|
|
for (std::size_t i = at + 1; i < object.size(); ++i) {
|
|
if (object[i] == '"') return value;
|
|
if (object[i] == '\\' && i + 1 < object.size()) ++i;
|
|
value += object[i];
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
// The std module's "source-path" in a libstdc++.modules.json: the object
|
|
// in its "modules" array whose "logical-name" is "std". The manifest is
|
|
// a flat list of such objects, so each one is the text between its
|
|
// braces.
|
|
std::optional<std::string> StdModuleSourcePath(std::string_view manifest) {
|
|
for (std::size_t open = manifest.find('{', manifest.find("\"modules\"")); open != std::string_view::npos; open = manifest.find('{', open + 1)) {
|
|
const std::size_t close = manifest.find('}', open);
|
|
if (close == std::string_view::npos) break;
|
|
const std::string_view object = manifest.substr(open, close - open);
|
|
if (JsonString(object, "logical-name") == "std") return JsonString(object, "source-path");
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
Crafter::FileLock::FileLock(const fs::path& path) {
|
|
fd = ::open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0644);
|
|
if (fd >= 0) {
|
|
while (::flock(fd, LOCK_EX) != 0 && errno == EINTR) {}
|
|
}
|
|
}
|
|
|
|
Crafter::FileLock::~FileLock() {
|
|
if (fd >= 0) {
|
|
::close(fd); // closing the descriptor drops the flock
|
|
}
|
|
}
|
|
|
|
fs::path Crafter::GetCrafterBuildHome() {
|
|
if (const char* envHome = std::getenv("CRAFTER_BUILD_HOME")) {
|
|
return fs::path(envHome);
|
|
}
|
|
fs::path hostExe = fs::read_symlink("/proc/self/exe");
|
|
|
|
// Walk up from the exe's directory looking for share/crafter-build with
|
|
// a known module source present as a sentinel. Handles:
|
|
// <prefix>/bin/crafter-build -> <prefix>/share/crafter-build (FHS install)
|
|
// <repo>/bin/crafter-build -> <repo>/share/crafter-build (build.sh)
|
|
// <repo>/bin/<arch>/crafter-build -> <repo>/share/crafter-build (self-host)
|
|
fs::path dir = hostExe.parent_path();
|
|
std::vector<fs::path> tried;
|
|
for (;;) {
|
|
fs::path candidate = dir / "share" / "crafter-build";
|
|
tried.push_back(candidate);
|
|
std::error_code ec;
|
|
if (fs::exists(candidate / "Crafter.Build.cppm", ec)) {
|
|
return candidate;
|
|
}
|
|
fs::path parent = dir.parent_path();
|
|
if (parent == dir) break;
|
|
dir = parent;
|
|
}
|
|
std::string msg = std::format("could not locate crafter-build runtime assets relative to {} (set CRAFTER_BUILD_HOME). Tried:", hostExe.string());
|
|
for (const auto& p : tried) {
|
|
msg += std::format("\n {}", p.string());
|
|
}
|
|
throw std::runtime_error(msg);
|
|
}
|
|
|
|
fs::file_time_type Crafter::NewestPrerequisite(const fs::path& depFile) {
|
|
std::ifstream in(depFile, std::ios::binary);
|
|
if (!in) return fs::file_time_type::max();
|
|
std::ostringstream buffer;
|
|
buffer << in.rdbuf();
|
|
const std::string text = std::move(buffer).str();
|
|
|
|
// Make-rule tokenizing, as clang writes it: `target: prereq prereq`, with a
|
|
// trailing '\' continuing the rule onto the next line (clang wraps at ~80
|
|
// columns, so any real depfile has several), '\ ' and '\#' escaping a
|
|
// character into a filename, and '$$' standing for a literal '$'. A
|
|
// backslash that is none of those is a Windows path separator and stays.
|
|
std::vector<std::string> tokens;
|
|
std::string token;
|
|
auto flush = [&tokens, &token]() {
|
|
if (!token.empty()) {
|
|
tokens.push_back(token);
|
|
token.clear();
|
|
}
|
|
};
|
|
for (std::size_t i = 0; i < text.size(); ++i) {
|
|
const char c = text[i];
|
|
const char next = i + 1 < text.size() ? text[i + 1] : '\0';
|
|
if (c == '\\' && (next == '\n' || next == '\r')) {
|
|
flush();
|
|
++i;
|
|
} else if (c == '\\' && (next == ' ' || next == '\t' || next == '#')) {
|
|
token += next;
|
|
++i;
|
|
} else if (c == '$' && next == '$') {
|
|
token += '$';
|
|
++i;
|
|
} else if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
|
|
flush();
|
|
} else {
|
|
token += c;
|
|
}
|
|
}
|
|
flush();
|
|
|
|
// A token ending in ':' names a rule target, not an input. The first one
|
|
// closes the target list (and a Windows `C:\build\x.o:` still ends in ':',
|
|
// where splitting on the first colon would not); a later one opens another
|
|
// rule, as `-MP` phony targets do, whose name is not a file to stat.
|
|
bool sawTarget = false;
|
|
fs::file_time_type newest = fs::file_time_type::min();
|
|
for (const std::string& prerequisite : tokens) {
|
|
if (prerequisite.ends_with(':')) {
|
|
sawTarget = true;
|
|
continue;
|
|
}
|
|
if (!sawTarget) continue;
|
|
std::error_code ec;
|
|
const fs::file_time_type stamp = fs::last_write_time(prerequisite, ec);
|
|
// A prerequisite that has since been deleted makes the recorded set
|
|
// unusable as evidence — the source that included it must have changed
|
|
// too, and rebuilding is what writes a depfile that no longer names it.
|
|
if (ec) return fs::file_time_type::max();
|
|
if (stamp > newest) newest = stamp;
|
|
}
|
|
// No rule at all: a truncated or empty depfile proves nothing.
|
|
if (!sawTarget) return fs::file_time_type::max();
|
|
return newest;
|
|
}
|
|
|
|
bool Crafter::MatchGlob(std::string_view glob, std::string_view name) {
|
|
std::size_t gi = 0;
|
|
std::size_t ni = 0;
|
|
std::size_t star = std::string_view::npos;
|
|
std::size_t mark = 0;
|
|
while (ni < name.size()) {
|
|
if (gi < glob.size() && (glob[gi] == '?' || glob[gi] == name[ni])) {
|
|
++gi; ++ni;
|
|
} else if (gi < glob.size() && glob[gi] == '*') {
|
|
star = gi++;
|
|
mark = ni;
|
|
} else if (star != std::string_view::npos) {
|
|
gi = star + 1;
|
|
ni = ++mark;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
while (gi < glob.size() && glob[gi] == '*') ++gi;
|
|
return gi == glob.size();
|
|
}
|
|
|
|
bool Crafter::MatchAny(std::span<const std::string> globs, std::string_view name) {
|
|
if (globs.empty()) return true;
|
|
for (const auto& g : globs) {
|
|
if (MatchGlob(g, name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// See the declaration in Crafter.Build-Platform.cppm for why this branches on
|
|
// the exit code rather than on whether the command said anything.
|
|
std::string Crafter::RunBuildCommand(std::string_view cmd) {
|
|
Progress::EchoCommand(cmd);
|
|
CommandResult r = RunCommandChecked(cmd);
|
|
|
|
if (r.exitCode == 0) {
|
|
// Warnings from a step that succeeded. Show them — until now they were
|
|
// invisible on an incremental build and fatal on a cold one.
|
|
if (!r.output.empty()) Progress::Diagnostic(r.output);
|
|
return "";
|
|
}
|
|
|
|
// A compiler killed outright (the OOM killer is the common one) prints
|
|
// nothing, so the old "output means failure" rule read it as success and
|
|
// let the build carry on with a missing object. Say what happened instead.
|
|
if (r.output.empty()) {
|
|
return r.crashed
|
|
? std::format("terminated by signal {}: {}", r.signal, cmd)
|
|
: std::format("exited with code {}: {}", r.exitCode, cmd);
|
|
}
|
|
if (r.crashed) {
|
|
return std::format("{}\nterminated by signal {}", r.output, r.signal);
|
|
}
|
|
return r.output;
|
|
}
|
|
|
|
// --- Dependencies of project.cpp itself -------------------------------------
|
|
//
|
|
// #pragma crafter dependency "<source>" [branch "<b>" | commit "<sha>"] [project "<file>"]
|
|
//
|
|
// names a sibling crafter-build project that project.cpp imports a module
|
|
// from. Anything a file needs before it can compile cannot be C++ inside that
|
|
// file, so this is a directive next to the `import` it enables — where
|
|
// `#include` lives. Each one is fetched, built for the host, and put on
|
|
// project.so's module path and link line before project.cpp is compiled. The
|
|
// dependency's own project.cpp goes through LoadProject, so its pragmas
|
|
// resolve recursively.
|
|
namespace {
|
|
struct PragmaDependency {
|
|
std::string source;
|
|
std::string branch;
|
|
std::string commit;
|
|
std::string project;
|
|
std::size_t line = 0;
|
|
};
|
|
|
|
[[noreturn]] void PragmaError(const fs::path& file, std::size_t line, std::string_view message) {
|
|
throw std::runtime_error(std::format("{}:{}: {}", file.string(), line, message));
|
|
}
|
|
|
|
// The tokens of one directive: bare words and double-quoted strings (no
|
|
// escapes — a path or URL never needs one). Reading stops at `//`.
|
|
class PragmaCursor {
|
|
public:
|
|
PragmaCursor(std::string_view text, const fs::path& file, std::size_t line) : text_(text), file_(file), line_(line) {}
|
|
|
|
bool AtEnd() {
|
|
SkipSpace();
|
|
return pos_ >= text_.size() || text_.substr(pos_).starts_with("//");
|
|
}
|
|
|
|
bool Take(char c) {
|
|
SkipSpace();
|
|
if (pos_ >= text_.size() || text_[pos_] != c) return false;
|
|
++pos_;
|
|
return true;
|
|
}
|
|
|
|
std::string_view Word() {
|
|
SkipSpace();
|
|
const std::size_t start = pos_;
|
|
while (pos_ < text_.size() && IsWordChar(text_[pos_])) ++pos_;
|
|
return text_.substr(start, pos_ - start);
|
|
}
|
|
|
|
std::optional<std::string_view> Quoted() {
|
|
SkipSpace();
|
|
if (pos_ >= text_.size() || text_[pos_] != '"') return std::nullopt;
|
|
const std::size_t close = text_.find('"', pos_ + 1);
|
|
if (close == std::string_view::npos) Fail("unterminated string");
|
|
const std::string_view inner = text_.substr(pos_ + 1, close - pos_ - 1);
|
|
pos_ = close + 1;
|
|
return inner;
|
|
}
|
|
|
|
[[noreturn]] void Fail(std::string_view message) const { PragmaError(file_, line_, message); }
|
|
|
|
private:
|
|
static bool IsWordChar(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; }
|
|
|
|
void SkipSpace() {
|
|
while (pos_ < text_.size() && (text_[pos_] == ' ' || text_[pos_] == '\t' || text_[pos_] == '\r')) ++pos_;
|
|
}
|
|
|
|
std::string_view text_;
|
|
const fs::path& file_;
|
|
std::size_t line_;
|
|
std::size_t pos_ = 0;
|
|
};
|
|
|
|
// One directive per line, anywhere in the file. Comments aren't parsed:
|
|
// a directive inside a block comment is honoured, and `//` lines fall out
|
|
// because they don't start with `#`.
|
|
std::vector<PragmaDependency> ScanProjectFilePragmas(const fs::path& absProject) {
|
|
std::vector<PragmaDependency> found;
|
|
std::ifstream in(absProject);
|
|
std::string raw;
|
|
for (std::size_t lineNo = 1; std::getline(in, raw); ++lineNo) {
|
|
PragmaCursor cur(raw, absProject, lineNo);
|
|
if (!cur.Take('#') || cur.Word() != "pragma" || cur.Word() != "crafter") continue;
|
|
const std::string_view verb = cur.Word();
|
|
if (verb != "dependency") cur.Fail(std::format("unknown crafter pragma '{}'", verb));
|
|
|
|
PragmaDependency dep;
|
|
dep.line = lineNo;
|
|
const std::optional<std::string_view> source = cur.Quoted();
|
|
if (!source) cur.Fail("expected a quoted source after 'dependency'");
|
|
dep.source = *source;
|
|
while (!cur.AtEnd()) {
|
|
const std::string_view key = cur.Word();
|
|
std::string* field = key == "branch" ? &dep.branch : key == "commit" ? &dep.commit : key == "project" ? &dep.project : nullptr;
|
|
if (!field) cur.Fail(key.empty() ? "unexpected text after the source" : std::format("unknown keyword '{}' (expected branch, commit or project)", key));
|
|
if (!field->empty()) cur.Fail(std::format("'{}' given twice", key));
|
|
const std::optional<std::string_view> value = cur.Quoted();
|
|
if (!value || value->empty()) cur.Fail(std::format("expected a quoted value after '{}'", key));
|
|
*field = *value;
|
|
}
|
|
if (dep.source.empty()) cur.Fail("the source is empty");
|
|
if (!dep.branch.empty() && !dep.commit.empty()) cur.Fail("'branch' and 'commit' are mutually exclusive");
|
|
found.push_back(std::move(dep));
|
|
}
|
|
return found;
|
|
}
|
|
|
|
struct ProjectFileFrame {
|
|
fs::path project;
|
|
std::vector<Configuration*> dependencies;
|
|
};
|
|
|
|
// LoadProject nests: a dependency's project.cpp loads inside its
|
|
// consumer's pre-pass, and GitProject inside CrafterBuildProject nests
|
|
// again. Several threads may also load at once (ConcurrentCacheRace), so
|
|
// the stack is per thread. ProjectDependency() reads the top frame.
|
|
thread_local std::vector<ProjectFileFrame> LoadingFrames;
|
|
|
|
// Held for the whole of one LoadProject call. Pushing a file that is
|
|
// already on the stack is a cycle, which used to recurse forever.
|
|
class ProjectFileGuard {
|
|
public:
|
|
explicit ProjectFileGuard(const fs::path& absProject) {
|
|
for (const ProjectFileFrame& frame : LoadingFrames) {
|
|
if (frame.project != absProject) continue;
|
|
std::string chain;
|
|
for (const ProjectFileFrame& f : LoadingFrames) chain += std::format("{} -> ", f.project.string());
|
|
throw std::runtime_error(std::format("circular project dependency: {}{}", chain, absProject.string()));
|
|
}
|
|
index_ = LoadingFrames.size();
|
|
LoadingFrames.push_back({ absProject, {} });
|
|
}
|
|
~ProjectFileGuard() { LoadingFrames.pop_back(); }
|
|
ProjectFileGuard(const ProjectFileGuard&) = delete;
|
|
ProjectFileGuard& operator=(const ProjectFileGuard&) = delete;
|
|
|
|
ProjectFileFrame& Frame() { return LoadingFrames[index_]; }
|
|
|
|
private:
|
|
std::size_t index_ = 0;
|
|
};
|
|
|
|
// A pre-dependency is host tooling: project.so is loaded into the running
|
|
// crafter-build, so the dependency is built for the host and against the
|
|
// same PCM cache LoadProject populates — the hostConfig LoadProject itself
|
|
// compiles project.so with, so the dependency's std/Crafter.Build PCMs
|
|
// land in the cache dir project.so is built against. Recursive, because
|
|
// the dependency's project.cpp forwards no arguments but still reads
|
|
// CRAFTER_BUILD_MARCH from the environment for its own deps.
|
|
void ForceHostTooling(Configuration& cfg, const Configuration& host, std::unordered_set<Configuration*>& seen) {
|
|
if (!seen.insert(&cfg).second) return;
|
|
cfg.target = host.target;
|
|
cfg.march = host.march;
|
|
cfg.mtune = host.mtune;
|
|
cfg.debug = false;
|
|
// project.so is a shared object, so the archive it links has to be
|
|
// position-independent; static libraries aren't by default. Part of
|
|
// VariantId, so this build never shares a bin dir with a normal one.
|
|
// Checked before pushing: the same Configuration comes back from the
|
|
// project cache for every consumer in this process. Linux hosts only,
|
|
// glibc or musl: Windows targets have no use for the flag.
|
|
const bool linuxHost = host.target.ends_with("-linux-gnu") || host.target.ends_with("-linux-musl");
|
|
if (linuxHost && !std::ranges::contains(cfg.compileFlags, "-fPIC")) {
|
|
cfg.compileFlags.emplace_back("-fPIC");
|
|
}
|
|
for (Configuration* dep : cfg.dependencies) ForceHostTooling(*dep, host, seen);
|
|
}
|
|
|
|
struct ProjectFileDependencies {
|
|
std::vector<Configuration*> configs;
|
|
std::string compileFlags; // " -fprebuilt-module-path=…" for each dependency and its transitive deps
|
|
std::string linkFlags; // every dependency's BuildResult.libs, verbatim
|
|
fs::file_time_type newestArtifact = fs::file_time_type::min();
|
|
};
|
|
|
|
bool IsGitSource(std::string_view source) {
|
|
return source.contains("://") || source.starts_with("git@");
|
|
}
|
|
|
|
ProjectFileDependencies ResolveProjectFileDependencies(const fs::path& absProject, const Configuration& host) {
|
|
ProjectFileDependencies out;
|
|
const std::vector<PragmaDependency> pragmas = ScanProjectFilePragmas(absProject);
|
|
if (pragmas.empty()) return out;
|
|
|
|
std::unordered_map<fs::path, std::shared_future<BuildResult>> depResults;
|
|
std::mutex depMutex;
|
|
std::unordered_set<Configuration*> forced;
|
|
std::unordered_set<Configuration*> walked;
|
|
std::set<std::string> libs;
|
|
|
|
for (const PragmaDependency& pragma : pragmas) {
|
|
Configuration* cfg = nullptr;
|
|
if (IsGitSource(pragma.source)) {
|
|
GitProjectSpec spec;
|
|
spec.source = { pragma.source, pragma.branch, pragma.commit };
|
|
if (!pragma.project.empty()) spec.projectFile = pragma.project;
|
|
cfg = GitProject(spec);
|
|
} else {
|
|
if (!pragma.branch.empty() || !pragma.commit.empty()) PragmaError(absProject, pragma.line, "'branch' and 'commit' only apply to a git source");
|
|
// Relative to the project file, not the cwd: LoadProject runs
|
|
// from wherever the consumer was invoked.
|
|
fs::path local = (absProject.parent_path() / pragma.source).lexically_normal();
|
|
if (fs::is_directory(local)) {
|
|
local /= pragma.project.empty() ? fs::path("project.cpp") : fs::path(pragma.project);
|
|
} else if (!pragma.project.empty()) {
|
|
PragmaError(absProject, pragma.line, "'project' only applies to a directory source");
|
|
}
|
|
if (!fs::exists(local)) PragmaError(absProject, pragma.line, std::format("no such project file: {}", local.string()));
|
|
cfg = LocalProject({ .projectFile = local });
|
|
}
|
|
if (std::ranges::contains(out.configs, cfg)) continue;
|
|
|
|
ForceHostTooling(*cfg, host, forced);
|
|
BuildResult built;
|
|
{
|
|
Progress::Task task(std::format("Building project dependency {}", cfg->name));
|
|
built = Build(*cfg, depResults, depMutex);
|
|
}
|
|
if (!built.result.empty()) PragmaError(absProject, pragma.line, std::format("project dependency '{}' failed to build: {}", cfg->name, built.result));
|
|
out.configs.push_back(cfg);
|
|
libs.insert(built.libs.begin(), built.libs.end());
|
|
|
|
// The module path needs the dependency and everything it depends
|
|
// on, same as GetCompileCommand does for a consumer; the newest
|
|
// artifact over the same set is what project.so is stale against.
|
|
std::function<void(Configuration*)> walk = [&](Configuration* c) {
|
|
if (!walked.insert(c).second) return;
|
|
out.compileFlags += std::format(" -fprebuilt-module-path={}", c->PcmDir().string());
|
|
std::error_code ec;
|
|
const fs::file_time_type t = fs::last_write_time(ExpectedOutput(*c), ec);
|
|
if (!ec && t > out.newestArtifact) out.newestArtifact = t;
|
|
for (Configuration* sub : c->dependencies) walk(sub);
|
|
};
|
|
walk(cfg);
|
|
}
|
|
for (const std::string& lib : libs) out.linkFlags += std::format(" {}", lib);
|
|
return out;
|
|
}
|
|
|
|
// project.so is stale when its stamp (crafter-build version, toolchain,
|
|
// module sources and compile command) disagrees, or when it predates its
|
|
// source or a pre-dependency artifact. The executable's mtime is not an
|
|
// input any more: an OSTree deployment dates every installed file at the
|
|
// epoch, so a comparison against it never fired after an update and the
|
|
// old project.so was loaded into a binary it did not match.
|
|
bool ProjectFileStale(const fs::path& artifact, const fs::path& project, std::string_view stamp, const ProjectFileDependencies& deps) {
|
|
if (!fs::exists(artifact)) return true;
|
|
if (ReadStamp(StampPath(artifact)) != stamp) return true;
|
|
const fs::file_time_type built = fs::last_write_time(artifact);
|
|
return built < fs::last_write_time(project) || built < deps.newestArtifact;
|
|
}
|
|
}
|
|
|
|
Configuration* Crafter::ProjectDependency(std::string_view name) {
|
|
if (LoadingFrames.empty()) throw std::runtime_error(std::format("ProjectDependency('{}'): called outside a project file", name));
|
|
const ProjectFileFrame& frame = LoadingFrames.back();
|
|
for (Configuration* cfg : frame.dependencies) {
|
|
if (cfg->name == name) return cfg;
|
|
for (const std::unique_ptr<Module>& iface : cfg->interfaces) {
|
|
if (iface->name == name) return cfg;
|
|
}
|
|
}
|
|
std::string declared;
|
|
for (Configuration* cfg : frame.dependencies) {
|
|
if (!declared.empty()) declared += ", ";
|
|
declared += cfg->name;
|
|
}
|
|
throw std::runtime_error(std::format("ProjectDependency('{}'): not declared by {} (declared: {})", name, frame.project.string(), declared.empty() ? "none" : declared));
|
|
}
|
|
|
|
bool Crafter::IsTopLevelProject() {
|
|
// One frame is the project file crafter-build was pointed at; a second
|
|
// is only ever pushed for a dependency's project file.
|
|
return LoadingFrames.size() <= 1;
|
|
}
|
|
|
|
namespace {
|
|
// The one reading of a wait status (pclose, system) for every helper
|
|
// that runs a command: a signal death is reported the way a shell
|
|
// reports it, as 128+signal.
|
|
void DecodeWaitStatus(std::int32_t status, CommandResult& result) {
|
|
if (WIFEXITED(status)) {
|
|
result.exitCode = WEXITSTATUS(status);
|
|
} else if (WIFSIGNALED(status)) {
|
|
result.signal = WTERMSIG(status);
|
|
result.exitCode = 128 + result.signal;
|
|
result.crashed = true;
|
|
} else {
|
|
result.exitCode = -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string Crafter::ShellQuote(std::string_view s) {
|
|
std::string out;
|
|
out.reserve(s.size() + 2);
|
|
out.push_back('\'');
|
|
for (char c : s) {
|
|
if (c == '\'') out += "'\\''";
|
|
else out.push_back(c);
|
|
}
|
|
out.push_back('\'');
|
|
return out;
|
|
}
|
|
|
|
CommandResult Crafter::RunCommandChecked(std::string_view cmd) {
|
|
std::array<char, 128> buffer;
|
|
CommandResult result{};
|
|
|
|
std::string with = std::format("{} 2>&1", cmd);
|
|
FILE* pipe = popen(with.c_str(), "r");
|
|
if (!pipe) throw std::runtime_error("popen() failed!");
|
|
|
|
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
|
|
result.output += buffer.data();
|
|
}
|
|
|
|
DecodeWaitStatus(pclose(pipe), result);
|
|
return result;
|
|
}
|
|
|
|
std::int32_t Crafter::RunCommandAttached(std::string_view cmd) {
|
|
// The child writes straight to the terminal. Anything of ours that is
|
|
// still pending — the in-place status line, buffered stdout when it is
|
|
// a pipe — has to be out before the child's first byte, or it lands in
|
|
// the middle of the child's output or after it.
|
|
Progress::Clear();
|
|
std::fflush(stdout);
|
|
std::fflush(stderr);
|
|
|
|
const std::string command(cmd);
|
|
const std::int32_t status = std::system(command.c_str());
|
|
if (status == -1) throw std::runtime_error(std::format("could not start a shell for: {}", cmd));
|
|
CommandResult result{};
|
|
DecodeWaitStatus(status, result);
|
|
return result.exitCode;
|
|
}
|
|
|
|
CommandResult Crafter::RunCommandWithTimeout(std::string_view cmd, std::chrono::seconds timeout) {
|
|
std::array<char, 128> buffer;
|
|
CommandResult result{};
|
|
|
|
// -k (not --kill-after=): the short form is what GNU coreutils and busybox
|
|
// (Alpine) have in common.
|
|
std::string wrapped = std::format("timeout -k 2 {} {} 2>&1", timeout.count(), cmd);
|
|
|
|
FILE* pipe = popen(wrapped.c_str(), "r");
|
|
if (!pipe) throw std::runtime_error("popen() failed!");
|
|
|
|
while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) {
|
|
result.output += buffer.data();
|
|
}
|
|
|
|
DecodeWaitStatus(pclose(pipe), result);
|
|
// What comes back is `timeout`'s own exit, and it speaks through the
|
|
// code: 124 for a command it had to stop, 128+signal for one that died.
|
|
if (!result.crashed) {
|
|
if (result.exitCode == 124) {
|
|
result.timedOut = true;
|
|
} else if (result.exitCode >= 128) {
|
|
result.signal = result.exitCode - 128;
|
|
result.crashed = true;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
|
CacheLock lock(stdPcm.parent_path());
|
|
if(config.target == "x86_64-w64-mingw32") {
|
|
// libstdc++ 15 and newer install libstdc++.modules.json beside the
|
|
// library, naming the std module's source relative to itself. It is
|
|
// looked for in the library directories the compiler searches for
|
|
// this target, as it would look for libstdc++ itself, but only in
|
|
// those that belong to the mingw toolchain: the list also has the
|
|
// generic <prefix>/lib, where the host's own (Linux) libstdc++ keeps
|
|
// its manifest. A toolchain that isn't there is a build error, not
|
|
// an exception: Build() runs this with its worker threads started.
|
|
const Toolchain& tc = GetToolchain();
|
|
const std::string where = tc.mingwSysroot.empty()
|
|
? std::string("where clang finds it by the x86_64-w64-mingw32 gcc on PATH")
|
|
: std::format("under {}", tc.mingwSysroot.string());
|
|
const std::string missing = std::format("no mingw-w64 libstdc++ with the std module (libstdc++ 15 or newer) {}. Install mingw-w64, or point CRAFTER_MINGW_DIR at its prefix, the directory holding x86_64-w64-mingw32/ and lib/gcc/", where);
|
|
// Asked once a run for each compiler and target: every mingw
|
|
// configuration comes through here, up to date or not, and the answer
|
|
// costs a clang.
|
|
static std::mutex probeMutex;
|
|
static std::unordered_map<std::string, CommandResult> probes;
|
|
const std::string probe = std::format("{} --target={} -print-search-dirs", GetBaseCommand(config), config.target);
|
|
CommandResult dirs;
|
|
{
|
|
std::lock_guard guard(probeMutex);
|
|
auto found = probes.find(probe);
|
|
if (found == probes.end()) found = probes.emplace(probe, RunCommandChecked(probe)).first;
|
|
dirs = found->second;
|
|
}
|
|
if (dirs.exitCode != 0) return std::format("{}: {}", missing, dirs.output);
|
|
fs::path manifestPath;
|
|
constexpr std::string_view librariesLine = "libraries: =";
|
|
if (const std::size_t at = dirs.output.find(librariesLine); at != std::string::npos) {
|
|
const std::size_t end = dirs.output.find('\n', at);
|
|
const std::string list = dirs.output.substr(at + librariesLine.size(), end == std::string::npos ? std::string::npos : end - at - librariesLine.size());
|
|
for (const auto part : std::views::split(list, ':')) {
|
|
const fs::path dir(std::string_view(part.begin(), part.end()));
|
|
const bool mingwOwn = std::ranges::contains(dir, fs::path("x86_64-w64-mingw32"));
|
|
if (mingwOwn && fs::exists(dir / "libstdc++.modules.json")) {
|
|
manifestPath = dir / "libstdc++.modules.json";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (manifestPath.empty()) return missing;
|
|
const std::optional<std::string> source = StdModuleSourcePath(FileBytes(manifestPath));
|
|
if (!source) return std::format("{} names no std module", manifestPath.string());
|
|
const fs::path stdCc = (manifestPath.parent_path() / *source).lexically_normal();
|
|
if (!fs::exists(stdCc)) return std::format("{}, which {} names as the std module, is missing", stdCc.string(), manifestPath.string());
|
|
|
|
// -femulated-tls keeps PCM TLS access matching libstdc++'s emutls
|
|
// definitions; mismatch surfaces as undefined std::__once_callable.
|
|
// The std.cc → std.cppm copy uses a plain filename: held under the
|
|
// cache lock, no other builder writes this path concurrently.
|
|
fs::path stdCppm = stdPcm.parent_path() / "std.cppm";
|
|
std::string command = std::format("{} --target={} -march={} -mtune={} -femulated-tls -O3 -std=c++26 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", GetBaseCommand(config), config.target, config.march, config.mtune, stdCppm.string(), stdPcm.string());
|
|
std::string stamp = HashStamp(command + '\0' + FileBytes(stdCc));
|
|
if (fs::exists(stdPcm) && ReadStamp(StampPath(stdPcm)) == stamp) return "";
|
|
std::error_code copyEc;
|
|
fs::copy_file(stdCc, stdCppm, fs::copy_options::overwrite_existing, copyEc);
|
|
if (copyEc) {
|
|
return std::format("copy {} -> {}: {}", stdCc.string(), stdCppm.string(), copyEc.message());
|
|
}
|
|
std::string result = RunBuildCommand(command);
|
|
if (result.empty()) WriteStamp(StampPath(stdPcm), stamp);
|
|
return result;
|
|
} else {
|
|
const bool isWasm = config.target.starts_with("wasm32");
|
|
// wasi-sdk drops std.cppm at <sysroot>/share/libc++/v1/, the rest of
|
|
// the libc++ ecosystem (e.g. /opt/aarch64-rootfs) follows FHS at
|
|
// <sysroot>/usr/share/libc++/v1/.
|
|
std::string stdCppm;
|
|
if (isWasm) {
|
|
stdCppm = std::format("{}/share/libc++/v1/std.cppm", config.sysroot);
|
|
} else if (config.sysroot.empty()) {
|
|
stdCppm = GetToolchain().stdCppm.string();
|
|
} else {
|
|
// The source has to belong to the libc++ release whose headers
|
|
// the sysroot carries. Handing clang the toolchain's own copy
|
|
// regardless used to end in a page of "no member named ... in
|
|
// namespace 'std'" once the two releases differed.
|
|
StdModuleSource source = SysrootStdModule(GetToolchain(), config.sysroot);
|
|
if (!source.error.empty()) return source.error;
|
|
stdCppm = source.stdCppm.string();
|
|
}
|
|
std::string sysrootFlag = config.sysroot.empty()
|
|
? std::string()
|
|
: std::format(" --sysroot={}", config.sysroot);
|
|
// wasm32 rejects -march. wasi-libc++ headers require these baselines
|
|
// to compile: setjmp.h needs -mllvm -wasm-enable-sjlj (lowered to wasm
|
|
// EH); signal.h requires the emulation define; and EH itself isn't
|
|
// wired up so -fno-exceptions stays.
|
|
std::string archFlags = isWasm
|
|
? std::string(" -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL")
|
|
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
|
// Match the variant codegen flags the consuming TUs are built with
|
|
// (e.g. -mrelaxed-simd) so the std BMI is feature-compatible. wasm only;
|
|
// wasmVariantFlags is empty on every other path.
|
|
if (isWasm) {
|
|
for (const std::string& f : config.wasmVariantFlags) archFlags += std::format(" {}", f);
|
|
}
|
|
// non-wasm cross builds: the driver ranks the host toolchain's own
|
|
// libc++ headers (<install>/../include/c++/v1) above --sysroot, so
|
|
// the PCM would mix host libc++ with the foreign libc — force the
|
|
// sysroot's copy.
|
|
if (!isWasm && !config.sysroot.empty()) {
|
|
archFlags += std::format(" -nostdinc++ -isystem {}/usr/include/c++/v1", config.sysroot);
|
|
}
|
|
// Keyed on the command and the module source's bytes, never on
|
|
// mtimes: an installed std.cppm can be dated at the epoch (OSTree),
|
|
// and a libc++ update under the same clang changes the bytes but
|
|
// not the path.
|
|
std::string command = std::format("{} --target={} -std=c++26{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", GetBaseCommand(config), config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string());
|
|
std::string stamp = HashStamp(command + '\0' + FileBytes(stdCppm));
|
|
if (fs::exists(stdPcm) && ReadStamp(StampPath(stdPcm)) == stamp) return "";
|
|
std::string result = RunBuildCommand(command);
|
|
if (result.empty()) WriteStamp(StampPath(stdPcm), stamp);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
fs::path Crafter::GetCacheDir() {
|
|
if (const char* xdg = std::getenv("XDG_CACHE_HOME"); xdg && *xdg) {
|
|
return fs::path(xdg) / "crafter.build";
|
|
}
|
|
|
|
if (const char* home = std::getenv("HOME")) {
|
|
return fs::path(home) / ".cache" / "crafter.build";
|
|
}
|
|
|
|
throw std::runtime_error("Neither XDG_CACHE_HOME nor HOME set");
|
|
}
|
|
|
|
std::string Crafter::GetBaseCommand(const Configuration& config) {
|
|
const Toolchain& tc = GetToolchain();
|
|
std::string command = tc.clangxx.string();
|
|
if (config.target == "x86_64-w64-mingw32") {
|
|
// libstdc++ from the mingw toolchain, said outright: a clang built to
|
|
// default to libc++ (the Flatpak's) would otherwise reach for that.
|
|
// Found by clang's own mingw discovery unless there is a sysroot.
|
|
command += " -stdlib=libstdc++";
|
|
if (!tc.mingwSysroot.empty()) command += std::format(" --sysroot={}", tc.mingwSysroot.string());
|
|
return command;
|
|
}
|
|
command += " -stdlib=libc++";
|
|
// A sysroot build (wasm, a cross rootfs) takes libc++ from the sysroot;
|
|
// only host builds can be pointed at an explicit libc++ install.
|
|
if (config.sysroot.empty()) command += tc.LibcxxCompileFlags();
|
|
return command;
|
|
}
|
|
|
|
namespace {
|
|
constexpr std::array<std::string_view, 13> CrafterBuildModules = {
|
|
"Crafter.Build-Toolchain",
|
|
"Crafter.Build-Shader",
|
|
"Crafter.Build-Platform",
|
|
"Crafter.Build-Interface",
|
|
"Crafter.Build-Implementation",
|
|
"Crafter.Build-External",
|
|
"Crafter.Build-Clang",
|
|
"Crafter.Build-Test",
|
|
"Crafter.Build-Lint",
|
|
"Crafter.Build-Script",
|
|
"Crafter.Build-Progress",
|
|
"Crafter.Build-Asset",
|
|
"Crafter.Build",
|
|
};
|
|
|
|
// Returns the stamp of the module sources the cache was built from; it is
|
|
// one of project.so's inputs.
|
|
std::string EnsureCrafterBuildPcms(const fs::path& sourceDir, const fs::path& cacheDir, const Configuration& host) {
|
|
CacheLock lock(cacheDir);
|
|
// Re-derived under the lock: another builder may have refreshed the
|
|
// cache from its own sources while we waited.
|
|
// The std PCM is an input to every module PCM: its stamp is folded in
|
|
// so a rebuilt std.pcm (a libc++ update under the same clang) rebuilds
|
|
// them too, instead of leaving PCMs that import a std.pcm that is gone.
|
|
std::string stamp = HashStamp(CrafterBuildSourceStamp(sourceDir, CrafterBuildModules) + '\0' + ReadStamp(StampPath(cacheDir / "std.pcm")));
|
|
const bool upToDate = ReadStamp(CacheStampPath(cacheDir)) == stamp;
|
|
for (std::string_view name : CrafterBuildModules) {
|
|
fs::path cppmPath = sourceDir / std::format("{}.cppm", name);
|
|
fs::path pcmPath = cacheDir / std::format("{}.pcm", name);
|
|
if (!fs::exists(cppmPath)) {
|
|
throw std::runtime_error(std::format("module source {} not found in {} (set CRAFTER_BUILD_HOME)", name, sourceDir.string()));
|
|
}
|
|
if (upToDate && fs::exists(pcmPath)) {
|
|
continue;
|
|
}
|
|
std::string cmd = std::format(
|
|
"{} --target={} -march=native -mtune=native "
|
|
"-std=c++26 -O3 "
|
|
"-Wno-reserved-identifier -Wno-reserved-module-identifier "
|
|
"-fprebuilt-module-path={} "
|
|
"--precompile {} -o {}",
|
|
GetBaseCommand(host), host.target, cacheDir.string(), cppmPath.string(), pcmPath.string());
|
|
CommandResult r = Crafter::RunCommandChecked(cmd);
|
|
if (r.exitCode != 0) {
|
|
throw std::runtime_error(std::format("Failed to precompile {} (exit {}): {}", name, r.exitCode, r.output));
|
|
}
|
|
}
|
|
// Last, so an interrupted rebuild leaves the stamp disagreeing with the
|
|
// sources and the next run starts over rather than trusting a half-built
|
|
// set of PCMs.
|
|
if (!upToDate) WriteStamp(CacheStampPath(cacheDir), stamp);
|
|
return stamp;
|
|
}
|
|
}
|
|
|
|
Configuration Crafter::LoadProject(const fs::path& projectFile, std::span<const std::string_view> args) {
|
|
RequireToolchain();
|
|
fs::path absProject = fs::canonical(projectFile);
|
|
ProjectFileGuard guard(absProject);
|
|
fs::path buildDir = absProject.parent_path() / "build";
|
|
if (!fs::exists(buildDir)) {
|
|
fs::create_directories(buildDir);
|
|
}
|
|
fs::path soPath = buildDir / std::format("{}.so", absProject.stem().string());
|
|
|
|
fs::path sourceDir = Crafter::GetCrafterBuildHome();
|
|
|
|
// The host toolchain's own triple (x86_64-pc-linux-gnu on glibc distros,
|
|
// x86_64-alpine-linux-musl on Alpine): project.cpp and the module PCMs
|
|
// it imports must be built for the libc crafter-build itself runs on.
|
|
Configuration hostConfig;
|
|
hostConfig.target = HostTarget();
|
|
hostConfig.march = "native";
|
|
hostConfig.mtune = "native";
|
|
// The same directory Build() resolves the std PCM from, toolchain key
|
|
// included (see StdPcmDir).
|
|
fs::path cacheDir = StdPcmDir(hostConfig);
|
|
if (!fs::exists(cacheDir)) fs::create_directories(cacheDir);
|
|
|
|
std::string stdResult = BuildStdPcm(hostConfig, cacheDir / "std.pcm");
|
|
if (!stdResult.empty()) {
|
|
throw std::runtime_error(std::format("Failed to build std.pcm: {}", stdResult));
|
|
}
|
|
|
|
const std::string sourcesStamp = EnsureCrafterBuildPcms(sourceDir, cacheDir, hostConfig);
|
|
|
|
// After the host PCMs exist (a dependency's build imports Crafter.Build
|
|
// from that cache) and before the staleness check (its artifacts are
|
|
// inputs to project.so). Built on every load, fresh or not: a dependency
|
|
// BMI whose own imports were rebuilt underneath it doesn't fail cleanly,
|
|
// clang crashes on it.
|
|
ProjectFileDependencies preDeps = ResolveProjectFileDependencies(absProject, hostConfig);
|
|
|
|
// -fuse-ld=lld like every other link the tool performs: lld is a hard
|
|
// dependency, binutils' ld is not (Alpine's clang would default to it),
|
|
// and a dependency archive built in release holds ThinLTO bitcode,
|
|
// which GNU ld can't read.
|
|
std::string compileCmd = std::format(
|
|
"{} --target={} -march=native -mtune=native "
|
|
"-std=c++26 -shared -fPIC -O3 -fuse-ld=lld "
|
|
"-Wno-return-type-c-linkage "
|
|
"-fprebuilt-module-path={}{} "
|
|
"{} -o {}{}{}",
|
|
GetBaseCommand(hostConfig), hostConfig.target, cacheDir.string(), preDeps.compileFlags,
|
|
absProject.string(), soPath.string(), preDeps.linkFlags, GetToolchain().LibcxxLinkFlags());
|
|
// What project.so was compiled against. The module source stamp stands
|
|
// in for the executable: project.so resolves Crafter:: symbols from the
|
|
// running binary, so only the declarations (the .cppm files) matter,
|
|
// and an implementation-only rebuild of crafter-build leaves it valid.
|
|
// The separators are appended, not written into a format string: a
|
|
// format string is read as a C string, so one with "\0" in it ends at the
|
|
// first NUL and everything after the version silently dropped out of the
|
|
// stamp. A project.so compiled against other declarations (a Shader with
|
|
// a different layout, say) was then loaded as current and corrupted the
|
|
// heap.
|
|
std::string projectStampInput(CrafterBuildVersion());
|
|
for (std::string_view part : {std::string_view(GetToolchain().Key()), std::string_view(sourcesStamp), std::string_view(compileCmd)}) {
|
|
projectStampInput += '\0';
|
|
projectStampInput += part;
|
|
}
|
|
const std::string projectStamp = HashStamp(projectStampInput);
|
|
|
|
if (ProjectFileStale(soPath, absProject, projectStamp, preDeps)) {
|
|
std::string result = RunBuildCommand(compileCmd);
|
|
if (!result.empty()) {
|
|
throw std::runtime_error(std::format("Failed to compile project {}: {}", absProject.string(), result));
|
|
}
|
|
WriteStamp(StampPath(soPath), projectStamp);
|
|
}
|
|
|
|
void* handle = dlopen(soPath.c_str(), RTLD_NOW | RTLD_GLOBAL);
|
|
if (!handle) {
|
|
throw std::runtime_error(std::format("Failed to load project {}: {}", soPath.string(), dlerror()));
|
|
}
|
|
|
|
using ProjectFn = Configuration (*)(std::span<const std::string_view>);
|
|
dlerror();
|
|
auto fn = reinterpret_cast<ProjectFn>(dlsym(handle, "CrafterBuildProject"));
|
|
if (const char* err = dlerror()) {
|
|
throw std::runtime_error(std::format("CrafterBuildProject not found in {}: {}", soPath.string(), err));
|
|
}
|
|
|
|
guard.Frame().dependencies = std::move(preDeps.configs);
|
|
return fn(args);
|
|
}
|