Crafter.Network master now carries, in their final shape, the pieces the HTTP layer and the SQL broker were written against on a side branch: ClientHTTP1::Fetch follows redirects by RFC 9110 §15.4 on a URL type of its own, and SocketErrorException is a std::system_error with the phase that failed. The broker and the HTTP layer read the errno or the resolver code from code() and the phase from the field, instead of matching message prefixes, and the pin moves to that commit, so the build no longer needs --local against a checkout of the side branch. The messages the run log, the preflight detail and the SQL helper show are unchanged, but for the wording of a refused URL.
364 lines
18 KiB
C++
364 lines
18 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, 26> Tests = {
|
|
"Crypto",
|
|
"Text",
|
|
"Platform",
|
|
"Process",
|
|
"Goldens",
|
|
"Stream",
|
|
"Normalize",
|
|
"Merge",
|
|
"Dispositions",
|
|
"Config",
|
|
"CommitsMode",
|
|
"CodeKeys",
|
|
"Freeze",
|
|
"PgWire",
|
|
"Seats",
|
|
"SeatPool",
|
|
"Drivers",
|
|
"OpenCode",
|
|
"Runner",
|
|
"Scheduler",
|
|
"StoreRules",
|
|
"StacksMemory",
|
|
"ReviewFixes",
|
|
"Runs",
|
|
"Commands",
|
|
"LensTools",
|
|
};
|
|
|
|
// 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. --deps=local takes
|
|
// Crafter.Network and Crafter.Thread from the checkouts next to this one,
|
|
// as a native --local build does, for commits not on forgejo yet.
|
|
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, or local deps,
|
|
// get a manifest copy of their own next to the original (gitignored), and
|
|
// with it a cache of their own.
|
|
FlatpakPaths FlatpakLayout(const fs::path& projectDir, std::string_view base, bool localDeps = false) {
|
|
const fs::path root = projectDir / "build" / "flatpak";
|
|
const fs::path packaging = projectDir / "packaging" / "flatpak";
|
|
const bool original = base == "stable" && !localDeps;
|
|
return {
|
|
.manifest = original ? packaging / "net.catcrafts.CrafterReview.yaml" : packaging / std::format("{}{}-base.yaml", base, localDeps ? "-localdeps" : ""),
|
|
.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;
|
|
}
|
|
|
|
// `old` replaced by `replacement` once in `text`; false when it is not
|
|
// there.
|
|
bool ReplaceOnce(std::string& text, std::string_view old, std::string_view replacement) {
|
|
const std::size_t at = text.find(old);
|
|
if (at == std::string::npos) return false;
|
|
text.replace(at, old.size(), replacement);
|
|
return true;
|
|
}
|
|
|
|
// The crafter-review module built from three dir sources side by side,
|
|
// so --local finds ../Crafter.Network and ../Crafter.Thread.
|
|
bool WithLocalDeps(std::string& manifest) {
|
|
constexpr std::string_view Source = R"( - type: dir
|
|
path: ../..
|
|
skip:
|
|
- .git
|
|
- bin
|
|
- build
|
|
)";
|
|
std::string sources;
|
|
for (const std::string_view project : {"Crafter.Review", "Crafter.Network", "Crafter.Thread"}) {
|
|
const std::string path = project == "Crafter.Review" ? std::string("../..") : std::format("../../../{}", project);
|
|
sources += std::format(" - type: dir\n path: {}\n dest: {}\n skip:\n - .git\n - bin\n - build\n", path, project);
|
|
}
|
|
return ReplaceOnce(manifest, " - name: crafter-review\n", " - name: crafter-review\n subdir: Crafter.Review\n")
|
|
&& ReplaceOnce(manifest, " - crafter-build\n - crafter-build test\n", " - crafter-build --local\n - crafter-build --local test\n")
|
|
&& ReplaceOnce(manifest, Source, sources);
|
|
}
|
|
|
|
std::int32_t BuildFlatpak(ScriptContext& ctx) {
|
|
if (RefusedInSandbox("flatpak")) return 1;
|
|
std::string jobs;
|
|
std::string base = "stable";
|
|
bool localDeps = false;
|
|
for (const std::string& arg : ctx.args) {
|
|
if (arg.starts_with("--base=") && arg.size() > 7) {
|
|
base = arg.substr(7);
|
|
continue;
|
|
}
|
|
if (arg == "--deps=local") {
|
|
localDeps = true;
|
|
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>, --base=<branch> and --deps=local 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, localDeps);
|
|
if (base != "stable" || localDeps) {
|
|
// The manifest with base-version changed, and the local deps.
|
|
std::ifstream in(FlatpakLayout(ctx.projectDir, "stable").manifest);
|
|
std::string manifest((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
|
if (!ReplaceOnce(manifest, "base-version: stable\n", std::format("base-version: {}\n", base))) {
|
|
std::println(std::cerr, "run flatpak: the manifest has no `base-version: stable` line to replace");
|
|
return 1;
|
|
}
|
|
if (localDeps && !WithLocalDeps(manifest)) {
|
|
std::println(std::cerr, "run flatpak: the crafter-review module is not in the shape --deps=local rewrites (its dir source and its two crafter-build lines)");
|
|
return 1;
|
|
}
|
|
std::ofstream(paths.manifest) << manifest;
|
|
}
|
|
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 = "7ee938c98e8c76290eef41b03e49ca5eed27a174" },
|
|
.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;
|
|
}
|