ci: the Flatpak workflow, local deps for the local build, the lens tools

- .forgejo/workflows/ci.yaml is Crafter.Build's workflow for this app: the
  base installed from the Catcrafts repo, the version stamped into the
  crafter-review module, the suite inside the build, then a smoke test of
  the app as it ships - the version and home, every library resolved and no
  RUNPATH outside /app, the bundled tools, a freeze and a dry run on a
  scratch repository. Publishing holds /srv/flatpak/.publish.lock and
  writes only this app's flatpakref. Unverified until a crafter-flatpak
  runner serves this repository.
- `run flatpak --deps=local` builds against the Crafter.Network and
  Crafter.Thread checkouts next to this one, as a native --local build
  does, while their commits are not on forgejo: a gitignored manifest copy
  with three dir sources side by side and --local.
- LensTools: every program the lens allowlist names, the shell and the MCP
  server's node and npx resolve on PATH; strict in the Flatpak build, where
  the suite runs in the sandbox that ships, a skip natively when this
  machine lacks one.

Built on the stable base (Crafter.Build 3.0.0) with --deps=local: the suite
passes inside flatpak-builder (25 passed, 2 skipped), and the workflow's
smoke test passes against that build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 15:59:20 +01:00
commit 5dab4b49d5
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
3 changed files with 373 additions and 17 deletions

View file

@ -84,7 +84,7 @@ namespace {
// 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, 25> Tests = {
constexpr std::array<std::string_view, 26> Tests = {
"Crypto",
"Text",
"Platform",
@ -110,6 +110,7 @@ namespace {
"ReviewFixes",
"Runs",
"Commands",
"LensTools",
};
// The version the binary reports: CI sets it from the tag, a checkout
@ -129,7 +130,9 @@ namespace {
// 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.
// `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.
@ -142,14 +145,15 @@ namespace {
// 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) {
// 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 = base == "stable" ? packaging / "net.catcrafts.CrafterReview.yaml" : packaging / std::format("{}-base.yaml", base),
.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",
@ -187,18 +191,52 @@ namespace {
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> and --base=<branch> are accepted)", arg);
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);
@ -216,18 +254,20 @@ namespace {
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.
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);
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));
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;
}
std::ofstream(paths.manifest) << manifest.substr(0, at) << std::format("base-version: {}\n", base) << manifest.substr(at + Line.size());
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;