diff --git a/.gitignore b/.gitignore index 34703ff..6d5e9d8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ lib/*.a # written next to the manifest: by CI with the version stamped in, and by # `crafter-build run flatpak --base=` for a local base branch packaging/flatpak/ci-manifest.yaml -packaging/flatpak/local-base.yaml +packaging/flatpak/*-base.yaml diff --git a/packaging/flatpak/net.catcrafts.CrafterReview.flatpakref b/packaging/flatpak/net.catcrafts.CrafterReview.flatpakref new file mode 100644 index 0000000..9dd721f --- /dev/null +++ b/packaging/flatpak/net.catcrafts.CrafterReview.flatpakref @@ -0,0 +1,13 @@ +[Flatpak Ref] +Title=Crafter Review +Name=net.catcrafts.CrafterReview +Branch=stable +Url=https://flatpak.catcrafts.net/repo/ +SuggestRemoteName=catcrafts +Homepage=https://forgejo.catcrafts.net/Catcrafts/Crafter.Review +Comment=Adversarial pre-push review of a frozen commit range +Description=crafter-review with its own Node and ripgrep, built on the Crafter.Build toolchain +Icon= +RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo +IsRuntime=false +GPGKey=GPG_KEY_BASE64_PLACEHOLDER diff --git a/packaging/flatpak/net.catcrafts.CrafterReview.metainfo.xml b/packaging/flatpak/net.catcrafts.CrafterReview.metainfo.xml new file mode 100644 index 0000000..0b90ee1 --- /dev/null +++ b/packaging/flatpak/net.catcrafts.CrafterReview.metainfo.xml @@ -0,0 +1,36 @@ + + + + net.catcrafts.CrafterReview + Crafter Review + Adversarial pre-push review of a frozen commit range + CC0-1.0 + AGPL-3.0-only + + Catcrafts + + +

+ The pre-push review pipeline: crafter-review freezes a commit range, + partitions it into units, and runs adversarial lenses over it — the + approach, the running application through the Playwright MCP server, + a bug proof and a security review — one claude process per unit, on the + seats the repository provides. A range is send-ready only when every + gate passes on the exact frozen hash. +

+
+ https://forgejo.catcrafts.net/Catcrafts/Crafter.Review + https://forgejo.catcrafts.net/Catcrafts/Crafter.Review/issues + https://forgejo.catcrafts.net/Catcrafts/Crafter.Review + + crafter-review + + + + + +

First release: the pre-push review gates in C++26, shipped as a Flatpak.

+
+
+
+
diff --git a/packaging/flatpak/net.catcrafts.CrafterReview.yaml b/packaging/flatpak/net.catcrafts.CrafterReview.yaml new file mode 100644 index 0000000..64179ee --- /dev/null +++ b/packaging/flatpak/net.catcrafts.CrafterReview.yaml @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +# +# crafter-review as a Flatpak. The runtime is the Freedesktop *Sdk*, as +# Crafter.Build's is, not for a compiler but for what a lens runs through its +# Bash tool: git, python3 for `python3 -` heredocs, curl, jq and coreutils. +# Node (for the Playwright MCP server) and ripgrep (`Bash(rg:*)`, which the Sdk +# lacks) are bundled. claude is not: it is a proprietary binary, found in the +# host's home where its installers put it, and run inside the sandbox like +# everything else, so the memory sampler sees every pass and a pass's whole +# tree can be killed. +# +# The base app is the Crafter.Build Flatpak: its crafter-build, LLVM 23 and +# libc++ build this project inside flatpak-builder, so no second LLVM is +# built. Everything of it but the libc++ the binary links is pruned at the +# end (cleanup-commands below); bump `runtime-version` together with the +# base's. +app-id: net.catcrafts.CrafterReview +runtime: org.freedesktop.Sdk +runtime-version: '26.08' +sdk: org.freedesktop.Sdk +base: net.catcrafts.CrafterBuild +base-version: stable +command: crafter-review +finish-args: + # claude to the API, npx to the npm registry, the local stacks on the + # host's loopback, and the driver's SQL broker on 127.0.0.1 + - --share=network + # repositories, ~/.claude, ~/.cache/review-gates, ~/.cache/ms-playwright, + # ~/.npm: anywhere a developer keeps them. A convenience for a developer + # tool, not an isolation boundary. + - --filesystem=host + # The sandbox's own /tmp is private; this makes /tmp the host's, so a + # repository, a config or an artifact under /tmp works as it does natively. + - --filesystem=/tmp + # freeze reads `%G?`; gnupg with use-keyboxd keeps its public keys behind + # this socket. Only this one: the agent, which signs, stays out of reach. + - --filesystem=xdg-run/gnupg/S.keyboxd + - --env=PATH=/app/bin:/usr/bin +modules: + - name: ripgrep + buildsystem: simple + build-commands: + - install -Dm755 rg /app/bin/rg + sources: + - type: archive + url: https://github.com/BurntSushi/ripgrep/releases/download/15.2.0/ripgrep-15.2.0-x86_64-unknown-linux-musl.tar.gz + sha256: 33e15bcf1624b25cdd2a55813a47a2f95dbe126268203e76aa6a585d1e7b149c + + - name: node + # For the Playwright MCP server, which the claude CLI starts through + # `npx`. Unpacked whole, with links in /app/bin. + buildsystem: simple + build-commands: + - mkdir -p /app/lib/node /app/bin + - cp -a bin include lib share /app/lib/node/ + - ln -sf ../lib/node/bin/node /app/bin/node + - ln -sf ../lib/node/bin/npm /app/bin/npm + - ln -sf ../lib/node/bin/npx /app/bin/npx + sources: + - type: archive + url: https://nodejs.org/dist/v24.22.0/node-v24.22.0-linux-x64.tar.xz + sha256: cc2f601cee82d9a5a233302fe307abc600727035fd7d2c3a3571755989e95008 + + - name: crafter-review + buildsystem: simple + build-options: + # crafter-build fetches Crafter.Network, Crafter.Thread and + # Crafter.LintRules from git, each at the commit project.cpp pins. + build-args: + - --share=network + # On this module, not at the top: top-level build options are part of + # every module's cache key, and the version changes with every commit. + env: + # The shipped binary runs on any x86_64 from the last decade. + CRAFTER_BUILD_MARCH: x86-64-v2 + CRAFTER_BUILD_MTUNE: generic + # CI replaces this with the version; a local build is a development + # build. + CRAFTER_REVIEW_VERSION: 0.0.0-dev + build-commands: + - crafter-build + - crafter-build test + - install -Dm755 bin/crafter.review-exe-*/crafter-review /app/bin/crafter-review + - mkdir -p /app/share/crafter-review && cp -r process /app/share/crafter-review/ + - install -Dm644 packaging/flatpak/prune-base.sh /app/share/crafter-review/prune-base.sh + - install -Dm644 LICENSE -t /app/share/licenses/net.catcrafts.CrafterReview/ + - install -Dm644 packaging/flatpak/net.catcrafts.CrafterReview.metainfo.xml -t /app/share/metainfo/ + sources: + - type: dir + path: ../.. + skip: + - .git + - bin + - build + +# The base's files are no module's of this manifest, so `cleanup` patterns do +# not reach them: the prune keeps a named list and removes the rest. +cleanup-commands: + - sh /app/share/crafter-review/prune-base.sh diff --git a/packaging/flatpak/prune-base.sh b/packaging/flatpak/prune-base.sh new file mode 100644 index 0000000..3399833 --- /dev/null +++ b/packaging/flatpak/prune-base.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-only +# SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® +# +# The base app, net.catcrafts.CrafterBuild, brings Crafter.Build's whole +# toolchain into /app so this project can be built inside flatpak-builder. +# The shipped app keeps what crafter-review runs on and nothing else: a list +# of what stays rather than of what goes, so whatever a newer base adds goes +# without anyone having to know about it. +set -eu +cd /app + +keep() { + case "$1" in + bin | bin/crafter-review | bin/node | bin/npm | bin/npx | bin/rg) return 0 ;; + lib | lib/node | lib/node/*) return 0 ;; + # What a binary built by the base's crafter-build links through its + # /app/lib rpath. + lib/libc++.so.1* | lib/libc++abi.so.1* | lib/libunwind.so.1*) return 0 ;; + # Split into the .Debug extension at export. + lib/debug | lib/debug/*) return 0 ;; + share | share/crafter-review | share/crafter-review/*) return 0 ;; + # Named, not whole: the base's CrafterBuild metainfo and licences go. + share/metainfo | share/metainfo/net.catcrafts.CrafterReview.metainfo.xml) return 0 ;; + share/licenses | share/licenses/net.catcrafts.CrafterReview | share/licenses/net.catcrafts.CrafterReview/*) return 0 ;; + manifest*.json) return 0 ;; + esac + return 1 +} + +# Depth first: a directory comes after its contents, and goes only once it +# is empty. +find . -mindepth 1 -depth | sed 's|^\./||' | while IFS= read -r path; do + keep "$path" && continue + if [ -d "$path" ] && [ ! -L "$path" ]; then + rmdir "$path" 2>/dev/null || true + else + rm -f "$path" + fi +done +rm -f share/crafter-review/prune-base.sh diff --git a/project.cpp b/project.cpp index 0b6ed82..e506db6 100644 --- a/project.cpp +++ b/project.cpp @@ -53,6 +53,146 @@ namespace { 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= 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 /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(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= and --base= are accepted)", arg); + return 1; + } + jobs = std::format(" --jobs={}", count); + } + if (!std::ranges::all_of(base, [](char c) { return std::isalnum(static_cast(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(in)), std::istreambuf_iterator()); + 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 ` --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 +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 args) { @@ -105,5 +245,8 @@ extern "C" Configuration CrafterBuildProject(std::span a } LintRules::AddLiteRules(cfg); + + cfg.scripts.push_back({"flatpak", BuildFlatpak}); + cfg.scripts.push_back({"install", InstallFlatpak}); return cfg; }