ExternalReconfigure builds a one-file C library from a local repository as a CMake external, then plants a file in its build directory to stand for what an old configure left. Built again with the same inputs, the directory is kept. With its recorded inputs rewritten the way another crafter-build's configure would have left them, the next build has to start from an empty directory and build the library again. Before the fix the planted file survived the reconfigure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
320 lines
18 KiB
C++
320 lines
18 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
import std;
|
|
import Crafter.Build;
|
|
#pragma crafter dependency "https://forgejo.catcrafts.net/Catcrafts/Crafter.LintRules.git"
|
|
import Crafter.LintRules;
|
|
namespace fs = std::filesystem;
|
|
using namespace Crafter;
|
|
|
|
// The local Flatpak: `crafter-build run flatpak` builds it from this checkout
|
|
// and `crafter-build run install` installs that build. The development box
|
|
// compiles with its own clang, the shipped artifact with the LLVM bundled in
|
|
// the Flatpak, so this is how a change is tried against the real thing.
|
|
namespace {
|
|
struct FlatpakPaths {
|
|
fs::path manifest;
|
|
// flatpak-builder's downloads, ccache and per-module cache. The
|
|
// LLVM build lives here: about 2 GB, and 25 minutes to rebuild.
|
|
fs::path state;
|
|
// The OSTree repo the build is exported to; `install` reads it.
|
|
fs::path repo;
|
|
fs::path buildDir;
|
|
};
|
|
|
|
// Everything under <project>/build/flatpak, and a fixed name on purpose
|
|
// instead of a BuildDir(): the Flatpak is compiled by the toolchain
|
|
// inside the sandbox, so it does not depend on the host variant, and a
|
|
// variant-keyed directory would strand the LLVM cache on every host
|
|
// compiler update. The manifest's `skip: build` keeps it out of the
|
|
// sources the sandbox copies. flatpak-builder keys its cache on the
|
|
// manifest's absolute path, so the manifest is used where it is, never
|
|
// through a rewritten copy the way CI stamps the version in; a local
|
|
// build therefore reports version 0.0.0-dev.
|
|
FlatpakPaths FlatpakLayout(const fs::path& projectDir) {
|
|
const fs::path root = projectDir / "build" / "flatpak";
|
|
return {
|
|
.manifest = projectDir / "packaging" / "flatpak" / "net.catcrafts.CrafterBuild.yaml",
|
|
.state = root / "state",
|
|
.repo = root / "repo",
|
|
.buildDir = root / "build-dir",
|
|
};
|
|
}
|
|
|
|
// Both scripts drive the host's flatpak, and the sandbox crafter-build
|
|
// itself runs in as a Flatpak has none.
|
|
bool RefusedInSandbox(std::string_view script) {
|
|
if (!fs::exists("/.flatpak-info")) return false;
|
|
std::println(std::cerr, "run {0}: needs the host's flatpak, which the Flatpak build of crafter-build cannot reach. Use a native build: ./build.sh, then ./bin/crafter-build run {0}", script);
|
|
return true;
|
|
}
|
|
|
|
// flatpak-builder as a host package, else the org.flatpak.Builder app.
|
|
// Empty when neither is installed.
|
|
std::string FlatpakBuilder() {
|
|
if (RunCommandWithTimeout("flatpak-builder --version", std::chrono::seconds(10)).exitCode == 0) return "flatpak-builder";
|
|
if (RunCommandWithTimeout("flatpak info org.flatpak.Builder", std::chrono::seconds(10)).exitCode == 0) return "flatpak run org.flatpak.Builder";
|
|
return "";
|
|
}
|
|
|
|
// A repo path as the file:// URL of a remote. Percent-encoded, because
|
|
// `remote-modify --url=` stores what it is given as it is.
|
|
std::string FileUrl(const fs::path& path) {
|
|
std::string url = "file://";
|
|
for (const char c : path.string()) {
|
|
const bool plain = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || std::string_view("/-._~").contains(c);
|
|
if (plain) url.push_back(c);
|
|
else url += std::format("%{:02X}", static_cast<std::uint8_t>(c));
|
|
}
|
|
return url;
|
|
}
|
|
|
|
std::int32_t BuildFlatpak(ScriptContext& ctx) {
|
|
if (RefusedInSandbox("flatpak")) return 1;
|
|
// Nothing else is passed through: a flag such as --stop-at or
|
|
// --download-only would end the run without a build and leave the
|
|
// smoke test below running against the previous one.
|
|
std::string jobs;
|
|
for (const std::string& arg : ctx.args) {
|
|
const std::string_view count = arg.starts_with("--jobs=") ? std::string_view(arg).substr(7) : std::string_view();
|
|
if (count.empty() || !std::ranges::all_of(count, [](char c) { return c >= '0' && c <= '9'; })) {
|
|
std::println(std::cerr, "run flatpak: unknown argument '{}' (only --jobs=<N> is accepted)", arg);
|
|
return 1;
|
|
}
|
|
jobs = std::format(" --jobs={}", count);
|
|
}
|
|
const std::string builder = FlatpakBuilder();
|
|
if (builder.empty()) {
|
|
std::println(std::cerr, "run flatpak: neither flatpak-builder nor the org.flatpak.Builder app is installed (flatpak install flathub org.flatpak.Builder)");
|
|
return 1;
|
|
}
|
|
const FlatpakPaths paths = FlatpakLayout(ctx.projectDir);
|
|
const std::string buildDir = ShellQuote(paths.buildDir.string());
|
|
if (const std::int32_t code = ctx.Run(std::format("{}{} --user --force-clean --ccache --state-dir={} --repo={} --default-branch=local {} {}", builder, jobs, ShellQuote(paths.state.string()), ShellQuote(paths.repo.string()), buildDir, ShellQuote(paths.manifest.string()))); code != 0) return code;
|
|
// The smoke test CI runs on its build, but through `flatpak build`
|
|
// and not `<builder> --run`: the org.flatpak.Builder app (1.4.9)
|
|
// exits 1 when the command it ran succeeded and 0 when it failed,
|
|
// and --run also wants the state dir again or it drops a
|
|
// .flatpak-builder/ into the working directory.
|
|
return ctx.Run(std::format("flatpak build {} crafter-build --version", buildDir));
|
|
}
|
|
|
|
std::int32_t InstallFlatpak(ScriptContext& ctx) {
|
|
if (RefusedInSandbox("install")) return 1;
|
|
if (!ctx.args.empty()) {
|
|
std::println(std::cerr, "run install: takes no arguments (got '{}')", ctx.args.front());
|
|
return 1;
|
|
}
|
|
const FlatpakPaths paths = FlatpakLayout(ctx.projectDir);
|
|
// Deliberately not built from here: flatpak-builder never caches a
|
|
// `type: dir` module, so every build reruns build.sh and the whole
|
|
// suite inside the sandbox. Installing is the cheap half.
|
|
if (!fs::exists(paths.repo / "config")) {
|
|
std::println(std::cerr, "run install: nothing built yet, run: crafter-build run flatpak");
|
|
return 1;
|
|
}
|
|
const std::string url = ShellQuote(FileUrl(paths.repo));
|
|
// Its own branch from its own remote, next to whichever published
|
|
// branch is installed. The remote may already exist and point at
|
|
// another checkout's repo; --if-not-exists would keep that silently.
|
|
if (const std::int32_t code = ctx.Run(std::format("flatpak remote-add --user --if-not-exists --no-gpg-verify crafter-build-local {}", url)); code != 0) return code;
|
|
if (const std::int32_t code = ctx.Run(std::format("flatpak remote-modify --user --url={} crafter-build-local", url)); code != 0) return code;
|
|
if (const std::int32_t code = ctx.Run("flatpak install --user --noninteractive --or-update crafter-build-local net.catcrafts.CrafterBuild//local"); code != 0) return code;
|
|
// Only a first install becomes the branch `flatpak run` starts; an
|
|
// update leaves that choice where it was.
|
|
if (const std::int32_t code = ctx.Run("flatpak make-current --user net.catcrafts.CrafterBuild local"); code != 0) return code;
|
|
std::println(
|
|
R"(
|
|
`flatpak run net.catcrafts.CrafterBuild` now starts this build.
|
|
Back to a published branch, keeping this one installed:
|
|
flatpak make-current --user net.catcrafts.CrafterBuild <branch>
|
|
Remove it (do this before `crafter-build clean`, which deletes the repo the remote points at):
|
|
flatpak uninstall --user net.catcrafts.CrafterBuild//local
|
|
flatpak remote-delete --user crafter-build-local)");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
|
Configuration* math = GitProject({ .source = { "https://forgejo.catcrafts.net/Catcrafts/Crafter.Math.git" } });
|
|
Configuration* asset = GitProject({ .source = { "https://forgejo.catcrafts.net/Catcrafts/Crafter.Asset.git" } });
|
|
|
|
static auto CrafterBuildLib = std::make_unique<Configuration>();
|
|
CrafterBuildLib->path = "./";
|
|
CrafterBuildLib->name = "crafter.build-lib";
|
|
CrafterBuildLib->outputName = "crafter-build";
|
|
ApplyStandardArgs(*CrafterBuildLib, args);
|
|
// Monolithic: project.so resolves Crafter:: symbols from the running
|
|
// executable, so the library is only ever linked statically into it.
|
|
CrafterBuildLib->type = ConfigurationType::LibraryStatic;
|
|
|
|
CrafterBuildLib->dependencies = { math, asset };
|
|
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_HAS_ASSET", ""});
|
|
// Crafter.Build-Lint includes libclang's clang-c/Index.h, which is only
|
|
// on the default include path for a toolchain installed in /usr.
|
|
if (const fs::path& llvmInclude = GetToolchain().llvmIncludeDir; !llvmInclude.empty()) {
|
|
CrafterBuildLib->compileFlags.push_back(std::format("-I{}", llvmInclude.string()));
|
|
}
|
|
// The version the binary reports: CI sets it from the tag, a checkout
|
|
// describes itself, anything else is a development build. A define on
|
|
// the library, so each version is its own library variant.
|
|
std::string version;
|
|
if (const char* env = std::getenv("CRAFTER_BUILD_VERSION"); env && *env) {
|
|
version = env;
|
|
} else {
|
|
auto described = RunCommandWithTimeout("git describe --tags --always --dirty --exclude=latest 2>/dev/null", std::chrono::seconds(10));
|
|
version = described.output;
|
|
while (!version.empty() && (version.back() == '\n' || version.back() == '\r')) version.pop_back();
|
|
if (described.exitCode != 0 || version.empty()) version = "0.0.0-dev";
|
|
}
|
|
// Backslash-escaped quotes: the define travels through the shell before
|
|
// it reaches clang, which must still see a string literal.
|
|
CrafterBuildLib->defines.push_back({"CRAFTER_BUILD_VERSION", std::format("\\\"{}\\\"", version)});
|
|
{
|
|
std::array<fs::path, 14> interfaces = {
|
|
"interfaces/Crafter.Build",
|
|
"interfaces/Crafter.Build-Toolchain",
|
|
"interfaces/Crafter.Build-Shader",
|
|
"interfaces/Crafter.Build-Platform",
|
|
"interfaces/Crafter.Build-Interface",
|
|
"interfaces/Crafter.Build-Implementation",
|
|
"interfaces/Crafter.Build-External",
|
|
"interfaces/Crafter.Build-Agents",
|
|
"interfaces/Crafter.Build-Clang",
|
|
"interfaces/Crafter.Build-Test",
|
|
"interfaces/Crafter.Build-Lint",
|
|
"interfaces/Crafter.Build-Script",
|
|
"interfaces/Crafter.Build-Progress",
|
|
"interfaces/Crafter.Build-Asset",
|
|
};
|
|
std::array<fs::path, 13> implementations = {
|
|
"implementations/Crafter.Build-Toolchain",
|
|
"implementations/Crafter.Build-Shader",
|
|
"implementations/Crafter.Build-Platform",
|
|
"implementations/Crafter.Build-Interface",
|
|
"implementations/Crafter.Build-Implementation",
|
|
"implementations/Crafter.Build-External",
|
|
"implementations/Crafter.Build-Agents",
|
|
"implementations/Crafter.Build-Clang",
|
|
"implementations/Crafter.Build-Test",
|
|
"implementations/Crafter.Build-Lint",
|
|
"implementations/Crafter.Build-Script",
|
|
"implementations/Crafter.Build-Progress",
|
|
"implementations/Crafter.Build-Asset",
|
|
};
|
|
CrafterBuildLib->GetInterfacesAndImplementations(interfaces, implementations);
|
|
}
|
|
ExternalDependency& glslang = CrafterBuildLib->externalDependencies.emplace_back();
|
|
glslang.name = "glslang";
|
|
glslang.source.url = "https://github.com/KhronosGroup/glslang.git";
|
|
glslang.source.branch = "main";
|
|
glslang.builder = ExternalBuilder::CMake;
|
|
glslang.options = { "-DENABLE_OPT=OFF" };
|
|
glslang.includeDirs = { "" };
|
|
glslang.libs = { "SPIRV", "GenericCodeGen", "glslang", "OSDependent", "MachineIndependent", "glslang-default-resource-limits" };
|
|
|
|
Configuration cfg;
|
|
cfg.path = "./";
|
|
cfg.name = "crafter.build-exe";
|
|
cfg.outputName = "crafter-build";
|
|
ApplyStandardArgs(cfg, args);
|
|
cfg.type = ConfigurationType::Executable;
|
|
cfg.dependencies = { CrafterBuildLib.get() };
|
|
{
|
|
std::array<fs::path, 0> interfaces = {};
|
|
std::array<fs::path, 1> implementations = { "implementations/main" };
|
|
cfg.GetInterfacesAndImplementations(interfaces, implementations);
|
|
}
|
|
// Linux hosts, glibc or musl (Alpine): -ldl is a libc stub on musl, harmless.
|
|
const bool linuxTarget = cfg.target.ends_with("-linux-gnu") || cfg.target.ends_with("-linux-musl");
|
|
if (linuxTarget) {
|
|
cfg.linkFlags.push_back("-Wl,--export-dynamic");
|
|
cfg.linkFlags.push_back("-ldl");
|
|
}
|
|
|
|
// Self-tests link the local crafter-build library and exercise it in
|
|
// process. The harness (whichever crafter-build invokes `test`) compiles
|
|
// these against the *installed* share/crafter-build .cppm files, then
|
|
// links each test exe against CrafterBuildLib built from the local
|
|
// sources — so the code under test is whatever's in this checkout.
|
|
// Mirrors how downstream consumers link their own libraries into tests.
|
|
if (linuxTarget) {
|
|
cfg.AddTest("HelloWorld").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("StaticLib").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("ModuleInterface").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("DependencyLink").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("IncrementalInterfaceChange").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("TransitiveInterfaceChange").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("IncrementalHeaderChange").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("StdPcmRebuild").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("CompilerWarning").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("CleanProject").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("ShaderCompile").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("ShaderVariantBuild").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("ShaderIncludeChange").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("MingwMissing").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("MingwSysroot").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("GitSubmodules").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("LinkFlagsChange").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("ExternalArtifactTime").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("ExternalReconfigure").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("ConcurrentExternalFetch").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("SharedExternalFetch").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("StandardArgs").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("TestRunnerSpec").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("VariantId").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("ToolchainResolve").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("SysrootStdModule").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("SysrootCompileFlags").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("VersionFlag").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("WasiBrowserRuntime").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("WasiBrowserRuntimeExit").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("WasmVariants").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("WasmHelloRuns").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("WasmExceptions").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("WasiSysrootWithoutExceptions").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("WasmBuildRefusals").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("RunSingleTestExit").Dependencies({ CrafterBuildLib.get() });
|
|
// LoadProject dlopens the synthesized project.so, which references
|
|
// Crafter:: symbols (HostTarget, Configuration ctors) that have to be
|
|
// visible from the test exe — same wiring crafter-build itself uses
|
|
// for project.so.
|
|
cfg.AddTest("ConcurrentCacheRace").Dependencies({ CrafterBuildLib.get() })
|
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
// Same LoadProject wiring as ConcurrentCacheRace above.
|
|
cfg.AddTest("HostCacheSourceStamp").Dependencies({ CrafterBuildLib.get() })
|
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
cfg.AddTest("HostCacheToolchainKey").Dependencies({ CrafterBuildLib.get() })
|
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
cfg.AddTest("ProjectDependency").Dependencies({ CrafterBuildLib.get() })
|
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
cfg.AddTest("ProjectArgs").Dependencies({ CrafterBuildLib.get() })
|
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
cfg.AddTest("HostToolingSibling").Dependencies({ CrafterBuildLib.get() })
|
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
cfg.AddTest("DuplicateModule").Dependencies({ CrafterBuildLib.get() })
|
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
cfg.AddTest("Script").Dependencies({ CrafterBuildLib.get() })
|
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
cfg.AddTest("DependencyNotes").Dependencies({ CrafterBuildLib.get() })
|
|
.LinkFlag("-Wl,--export-dynamic").LinkFlag("-ldl");
|
|
cfg.AddTest("ConcurrentDependencyReset").Dependencies({ CrafterBuildLib.get() });
|
|
cfg.AddTest("Lint").Dependencies({ CrafterBuildLib.get() });
|
|
}
|
|
|
|
// Dogfood: the lite house rules, imported from their own repo through
|
|
// the pragma above. Lite is the house rules without the ones that only
|
|
// decide layout, whitespace and naming, which this tree keeps by hand:
|
|
// the full set's naming rule, for one, wants camelCase where this tree
|
|
// spells its statics and constants PascalCase. Report rules gate
|
|
// `crafter-build lint`; the two transforms also auto-fix under
|
|
// `crafter-build format`.
|
|
LintRules::AddLiteRules(cfg);
|
|
|
|
// See the top of this file.
|
|
cfg.scripts.push_back({"flatpak", BuildFlatpak});
|
|
cfg.scripts.push_back({"install", InstallFlatpak});
|
|
|
|
return cfg;
|
|
}
|