StoreRules (23 checks), StacksMemory (24) and ReviewFixes (19), each a test of its own. Run and SlotsPlan now have seams that start as the real ones: appReady, probe, query, bypassesRls, migrationsAtHead, migrationsDrift, treeDrift, promptFor, runPass, resumable, instrumentPreflight, and a Machine for SlotsPlan. MigrationsAtHead is split out of MigrationsDrift for it; the HEAD side is still worked out once per tree and base. The Postgres stand-in and the raw broker call move to tests/Common, shared by PgWire and ReviewFixes. ReviewFixes runs this binary as `sql`, and a real server that resets every connection makes the startup message either fail to go out or never get its answer - a DbError either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
322 lines
16 KiB
C++
322 lines
16 KiB
C++
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
import std;
|
|
import Crafter.Build;
|
|
// Pinned: the Flatpak builds with the base app's crafter-build, which can be
|
|
// older than the rules' master. Move the pin together with the base.
|
|
#pragma crafter dependency "https://forgejo.catcrafts.net/Catcrafts/Crafter.LintRules.git" commit "7d932892017800b4e1f1a15fe7aa49b3daee9ceb"
|
|
import Crafter.LintRules;
|
|
namespace fs = std::filesystem;
|
|
using namespace Crafter;
|
|
|
|
namespace {
|
|
// The umbrella, then the partitions in import order: each after the ones
|
|
// it imports, which is also the order the module map in AGENTS.md uses.
|
|
constexpr std::array<std::string_view, 31> Interfaces = {
|
|
"interfaces/Crafter.Review",
|
|
"interfaces/Crafter.Review-Unicode",
|
|
"interfaces/Crafter.Review-Json",
|
|
"interfaces/Crafter.Review-Text",
|
|
"interfaces/Crafter.Review-Crypto",
|
|
"interfaces/Crafter.Review-Platform",
|
|
"interfaces/Crafter.Review-Process",
|
|
"interfaces/Crafter.Review-Http",
|
|
"interfaces/Crafter.Review-Core",
|
|
"interfaces/Crafter.Review-Schema",
|
|
"interfaces/Crafter.Review-Tools",
|
|
"interfaces/Crafter.Review-Plan",
|
|
"interfaces/Crafter.Review-Artifact",
|
|
"interfaces/Crafter.Review-Inventory",
|
|
"interfaces/Crafter.Review-Dispositions",
|
|
"interfaces/Crafter.Review-State",
|
|
"interfaces/Crafter.Review-Normalize",
|
|
"interfaces/Crafter.Review-PgWire",
|
|
"interfaces/Crafter.Review-Totp",
|
|
"interfaces/Crafter.Review-Stream",
|
|
"interfaces/Crafter.Review-Prompt",
|
|
"interfaces/Crafter.Review-Config",
|
|
"interfaces/Crafter.Review-Memory",
|
|
"interfaces/Crafter.Review-Mcp",
|
|
"interfaces/Crafter.Review-Seats",
|
|
"interfaces/Crafter.Review-SeatPool",
|
|
"interfaces/Crafter.Review-OpenCode",
|
|
"interfaces/Crafter.Review-Pool",
|
|
"interfaces/Crafter.Review-Freeze",
|
|
"interfaces/Crafter.Review-Run",
|
|
"interfaces/Crafter.Review-Driver",
|
|
};
|
|
constexpr std::array<std::string_view, 28> Implementations = {
|
|
"implementations/Crafter.Review-Text",
|
|
"implementations/Crafter.Review-Crypto",
|
|
"implementations/Crafter.Review-Platform",
|
|
"implementations/Crafter.Review-Process",
|
|
"implementations/Crafter.Review-Http",
|
|
"implementations/Crafter.Review-Core",
|
|
"implementations/Crafter.Review-Schema",
|
|
"implementations/Crafter.Review-Tools",
|
|
"implementations/Crafter.Review-Plan",
|
|
"implementations/Crafter.Review-Artifact",
|
|
"implementations/Crafter.Review-Inventory",
|
|
"implementations/Crafter.Review-Dispositions",
|
|
"implementations/Crafter.Review-State",
|
|
"implementations/Crafter.Review-Normalize",
|
|
"implementations/Crafter.Review-PgWire",
|
|
"implementations/Crafter.Review-Totp",
|
|
"implementations/Crafter.Review-Stream",
|
|
"implementations/Crafter.Review-Prompt",
|
|
"implementations/Crafter.Review-Config",
|
|
"implementations/Crafter.Review-Memory",
|
|
"implementations/Crafter.Review-Mcp",
|
|
"implementations/Crafter.Review-Seats",
|
|
"implementations/Crafter.Review-SeatPool",
|
|
"implementations/Crafter.Review-OpenCode",
|
|
"implementations/Crafter.Review-Pool",
|
|
"implementations/Crafter.Review-Freeze",
|
|
"implementations/Crafter.Review-Run",
|
|
"implementations/Crafter.Review-Driver",
|
|
};
|
|
|
|
// With -march=native clang would fuse a multiply and an add: a cost
|
|
// estimate, a usage total or a memory threshold must come out to the
|
|
// same last bit on every machine, as the goldens record it.
|
|
constexpr std::string_view NoContraction = "-ffp-contract=off";
|
|
|
|
// One per directory under tests/, each an ordinary main() that links the
|
|
// library: exit 0 passes, nonzero fails, 77 skips.
|
|
constexpr std::array<std::string_view, 23> Tests = {
|
|
"Crypto",
|
|
"Text",
|
|
"Platform",
|
|
"Process",
|
|
"Goldens",
|
|
"Stream",
|
|
"Normalize",
|
|
"Merge",
|
|
"Dispositions",
|
|
"Config",
|
|
"CommitsMode",
|
|
"CodeKeys",
|
|
"Freeze",
|
|
"PgWire",
|
|
"Seats",
|
|
"SeatPool",
|
|
"Drivers",
|
|
"OpenCode",
|
|
"Runner",
|
|
"Scheduler",
|
|
"StoreRules",
|
|
"StacksMemory",
|
|
"ReviewFixes",
|
|
};
|
|
|
|
// 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_REVIEW_VERSION"); env && *env) return env;
|
|
auto described = RunCommandWithTimeout("git describe --tags --always --dirty --exclude=latest 2>/dev/null", std::chrono::seconds(10));
|
|
std::string version = described.output;
|
|
while (!version.empty() && (version.back() == '\n' || version.back() == '\r')) version.pop_back();
|
|
if (described.exitCode != 0 || version.empty()) return "0.0.0-dev";
|
|
return version;
|
|
}
|
|
|
|
// The local Flatpak: `crafter-build run flatpak` builds it from this
|
|
// checkout and `crafter-build run install` installs that build, as
|
|
// Crafter.Build's own scripts do. --base=<branch> builds on another branch
|
|
// of the Crafter.Build base app than stable: until a Crafter.Build release
|
|
// with scripts is on stable, `--base=local` (the branch Crafter.Build's own
|
|
// `run install` makes) is what builds this project.
|
|
struct FlatpakPaths {
|
|
fs::path manifest;
|
|
// flatpak-builder's downloads, ccache and per-module cache.
|
|
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, a fixed name rather than a
|
|
// BuildDir(): the Flatpak is compiled by the toolchain inside the sandbox,
|
|
// not by the host variant. flatpak-builder keys its cache on the
|
|
// manifest's absolute path, so a branch other than stable gets a manifest
|
|
// copy of its own next to the original (gitignored), and with it a cache
|
|
// of its own.
|
|
FlatpakPaths FlatpakLayout(const fs::path& projectDir, std::string_view base) {
|
|
const fs::path root = projectDir / "build" / "flatpak";
|
|
const fs::path packaging = projectDir / "packaging" / "flatpak";
|
|
return {
|
|
.manifest = base == "stable" ? packaging / "net.catcrafts.CrafterReview.yaml" : packaging / std::format("{}-base.yaml", base),
|
|
.state = root / "state",
|
|
.repo = root / "repo",
|
|
.buildDir = root / "build-dir",
|
|
};
|
|
}
|
|
|
|
// Both scripts drive the host's flatpak, which the sandbox of a Flatpak
|
|
// crafter-build does not have.
|
|
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 crafter-build", 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;
|
|
std::string jobs;
|
|
std::string base = "stable";
|
|
for (const std::string& arg : ctx.args) {
|
|
if (arg.starts_with("--base=") && arg.size() > 7) {
|
|
base = arg.substr(7);
|
|
continue;
|
|
}
|
|
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> and --base=<branch> are accepted)", arg);
|
|
return 1;
|
|
}
|
|
jobs = std::format(" --jobs={}", count);
|
|
}
|
|
if (!std::ranges::all_of(base, [](char c) { return std::isalnum(static_cast<std::uint8_t>(c)) || c == '-' || c == '_' || c == '.'; })) {
|
|
std::println(std::cerr, "run flatpak: '{}' is not a branch name", base);
|
|
return 1;
|
|
}
|
|
if (RunCommandWithTimeout(std::format("flatpak info net.catcrafts.CrafterBuild//{}", base), std::chrono::seconds(10)).exitCode != 0) {
|
|
std::println(std::cerr, "run flatpak: the base app is not installed: flatpak install catcrafts net.catcrafts.CrafterBuild//{}", base);
|
|
return 1;
|
|
}
|
|
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, base);
|
|
if (base != "stable") {
|
|
// The manifest with only base-version changed.
|
|
std::ifstream in(FlatpakLayout(ctx.projectDir, "stable").manifest);
|
|
const std::string manifest((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
|
constexpr std::string_view Line = "base-version: stable\n";
|
|
const std::size_t at = manifest.find(Line);
|
|
if (at == std::string::npos) {
|
|
std::println(std::cerr, "run flatpak: the manifest has no `{}` line to replace", Line.substr(0, Line.size() - 1));
|
|
return 1;
|
|
}
|
|
std::ofstream(paths.manifest) << manifest.substr(0, at) << std::format("base-version: {}\n", base) << manifest.substr(at + Line.size());
|
|
}
|
|
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, through `flatpak build` and not `<builder> --run`:
|
|
// the org.flatpak.Builder app reports --run's exit status inverted.
|
|
if (const std::int32_t code = ctx.Run(std::format("flatpak build {} crafter-review --version", buildDir)); code != 0) return code;
|
|
return ctx.Run(std::format("flatpak build {} sh -c 'ldd /app/bin/crafter-review | grep \"not found\" && exit 1 || exit 0'", 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, "stable");
|
|
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));
|
|
if (const std::int32_t code = ctx.Run(std::format("flatpak remote-add --user --if-not-exists --no-gpg-verify crafter-review-local {}", url)); code != 0) return code;
|
|
if (const std::int32_t code = ctx.Run(std::format("flatpak remote-modify --user --url={} crafter-review-local", url)); code != 0) return code;
|
|
if (const std::int32_t code = ctx.Run("flatpak install --user --noninteractive --or-update crafter-review-local net.catcrafts.CrafterReview//local"); code != 0) return code;
|
|
if (const std::int32_t code = ctx.Run("flatpak make-current --user net.catcrafts.CrafterReview local"); code != 0) return code;
|
|
std::println(
|
|
R"(
|
|
`flatpak run net.catcrafts.CrafterReview` now starts this build.
|
|
Back to a published branch, keeping this one installed:
|
|
flatpak make-current --user net.catcrafts.CrafterReview <branch>
|
|
Remove it (do this before deleting build/, which holds the repo the remote points at):
|
|
flatpak uninstall --user net.catcrafts.CrafterReview//local
|
|
flatpak remote-delete --user crafter-review-local)");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
|
// Crafter.Network for its TCP and HTTP/1.1 clients and listener, without
|
|
// QUIC: msquic is most of a fresh build and nothing here speaks HTTP/3.
|
|
// From forgejo at a pinned commit, or with --local from the checkout next
|
|
// to this one (which is also how Crafter.Network then finds
|
|
// Crafter.Thread).
|
|
std::vector<std::string> depArgs(args.begin(), args.end());
|
|
depArgs.emplace_back("--no-quic");
|
|
const bool useLocal = std::ranges::find(args, std::string_view("--local")) != args.end();
|
|
Configuration* network = useLocal
|
|
? LocalProject({ .projectFile = "../Crafter.Network/project.cpp", .args = depArgs })
|
|
: GitProject({
|
|
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Network.git", .commit = "f68321685006be65f6bc0436fd0175611f1e0a55" },
|
|
.args = depArgs,
|
|
});
|
|
|
|
static auto ReviewLib = std::make_unique<Configuration>();
|
|
ReviewLib->path = "./";
|
|
ReviewLib->name = "crafter.review-lib";
|
|
ReviewLib->outputName = "crafter-review";
|
|
ApplyStandardArgs(*ReviewLib, args);
|
|
ReviewLib->type = ConfigurationType::LibraryStatic;
|
|
ReviewLib->dependencies = { network };
|
|
ReviewLib->compileFlags.emplace_back(NoContraction);
|
|
// Backslash-escaped quotes: the define travels through the shell before
|
|
// it reaches clang, which must still see a string literal.
|
|
ReviewLib->defines.push_back({"CRAFTER_REVIEW_VERSION", std::format("\\\"{}\\\"", Version())});
|
|
{
|
|
std::vector<fs::path> interfaces(Interfaces.begin(), Interfaces.end());
|
|
std::vector<fs::path> implementations(Implementations.begin(), Implementations.end());
|
|
ReviewLib->GetInterfacesAndImplementations(interfaces, implementations);
|
|
}
|
|
|
|
Configuration cfg;
|
|
cfg.path = "./";
|
|
cfg.name = "crafter.review-exe";
|
|
cfg.outputName = "crafter-review";
|
|
ApplyStandardArgs(cfg, args);
|
|
cfg.type = ConfigurationType::Executable;
|
|
cfg.dependencies = { ReviewLib.get() };
|
|
cfg.compileFlags.emplace_back(NoContraction);
|
|
{
|
|
std::array<fs::path, 0> interfaces = {};
|
|
std::array<fs::path, 1> implementations = { "implementations/main" };
|
|
cfg.GetInterfacesAndImplementations(interfaces, implementations);
|
|
}
|
|
|
|
for (const std::string_view test : Tests) {
|
|
cfg.AddTest(test).Dependencies({ ReviewLib.get() }).CompileFlag(std::string(NoContraction));
|
|
}
|
|
|
|
LintRules::AddLiteRules(cfg);
|
|
|
|
cfg.scripts.push_back({"flatpak", BuildFlatpak});
|
|
cfg.scripts.push_back({"install", InstallFlatpak});
|
|
return cfg;
|
|
}
|