From f8a041f5b7bae74463d434b1ec943e6efc54faa5 Mon Sep 17 00:00:00 2001 From: Amadey Vorontsov Date: Sun, 17 May 2026 19:39:11 +0000 Subject: [PATCH] cargoxx-pkgs registry skeleton Empty package registry for cargoxx. flake.nix walks recipes//versions/*.toml, exposes each (name, version) as packages..{_, }, and builds via cargoxx.lib.${system}.buildCppPackage with pkgs.fetchgit. .gitea/workflows/validate-pr.yml validates schema, refetches and verifies source sha256, smoke-builds, pushes $out to the binary cache, and labels auto-merge once the PR author is in maintainers.txt. .gitea/workflows/auto-merge.yml merges via tea on the auto-merge label. --- .gitea/workflows/auto-merge.yml | 19 ++++++ .gitea/workflows/validate-pr.yml | 109 +++++++++++++++++++++++++++++++ .gitignore | 3 + README.md | 70 ++++++++++++++++++++ flake.nix | 88 +++++++++++++++++++++++++ recipes/.gitkeep | 0 6 files changed, 289 insertions(+) create mode 100644 .gitea/workflows/auto-merge.yml create mode 100644 .gitea/workflows/validate-pr.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 flake.nix create mode 100644 recipes/.gitkeep diff --git a/.gitea/workflows/auto-merge.yml b/.gitea/workflows/auto-merge.yml new file mode 100644 index 0000000..093907f --- /dev/null +++ b/.gitea/workflows/auto-merge.yml @@ -0,0 +1,19 @@ +name: auto-merge + +on: + pull_request: + types: [labeled] + +jobs: + merge: + if: github.event.label.name == 'auto-merge' + runs-on: self-hosted + steps: + - name: merge + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + tea pr merge \ + --repo "${{ github.repository }}" \ + --style squash \ + "${{ github.event.pull_request.number }}" diff --git a/.gitea/workflows/validate-pr.yml b/.gitea/workflows/validate-pr.yml new file mode 100644 index 0000000..abad671 --- /dev/null +++ b/.gitea/workflows/validate-pr.yml @@ -0,0 +1,109 @@ +name: validate-pr + +on: + pull_request: + paths: + - 'recipes/**' + +jobs: + validate: + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + + # 1. Identify which recipes the PR touches. + - name: detect changed packages + id: changed + run: | + set -e + base="${{ github.event.pull_request.base.sha }}" + changed=$(git diff --name-only "$base"...HEAD -- 'recipes/' \ + | awk -F/ '{print $2}' | sort -u) + if [[ -z "$changed" ]]; then + echo "no recipe changes — nothing to validate" + echo "packages=" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "changed packages: $changed" + echo "packages=$changed" >> "$GITHUB_OUTPUT" + + # 2. Schema check (placeholder — refine once a JSON schema lives in-tree). + - name: schema check + if: steps.changed.outputs.packages != '' + run: | + for pkg in ${{ steps.changed.outputs.packages }}; do + for f in recipes/$pkg/versions/*.toml; do + echo "checking $f" + # TODO: validate against a schema definition once one exists. + grep -q '^schema = 1$' "$f" || { echo "missing schema = 1"; exit 1; } + grep -q '^name =' "$f" || { echo "missing name"; exit 1; } + grep -q '^version =' "$f" || { echo "missing version"; exit 1; } + grep -q '^\[source\]' "$f" || { echo "missing [source]"; exit 1; } + grep -q '^commit =' "$f" || { echo "missing source.commit"; exit 1; } + grep -q '^sha256 =' "$f" || { echo "missing source.sha256"; exit 1; } + done + done + + # 3. Source fixity — re-fetch and confirm the sha256 matches. + - name: source fixity + if: steps.changed.outputs.packages != '' + run: | + for pkg in ${{ steps.changed.outputs.packages }}; do + for f in recipes/$pkg/versions/*.toml; do + url=$(awk -F'"' '/^url =/ {print $2; exit}' "$f") + rev=$(awk -F'"' '/^commit =/ {print $2; exit}' "$f") + expected=$(awk -F'"' '/^sha256 =/ {print $2; exit}' "$f") + actual=$(nix flake prefetch --extra-experimental-features 'nix-command flakes' \ + "git+${url}?rev=${rev}" --json | jq -r .hash) + if [[ "$actual" != "$expected" ]]; then + echo "sha256 mismatch in $f: expected $expected, got $actual" + exit 1 + fi + done + done + + # 4. Build smoke — every changed package must build. + - name: build smoke + if: steps.changed.outputs.packages != '' + run: | + for pkg in ${{ steps.changed.outputs.packages }}; do + nix build --extra-experimental-features 'nix-command flakes' \ + .#${pkg} --no-link --print-out-paths + done + + # 5. Cache push (only on the validated outputs, before merge). + - name: push to binary cache + if: steps.changed.outputs.packages != '' + env: + NIX_SECRET_KEY_FILE: ${{ secrets.NIX_CACHE_SECRET_KEY_FILE }} + CACHE_URL: ${{ vars.CARGOXX_CACHE_URL }} + run: | + for pkg in ${{ steps.changed.outputs.packages }}; do + nix copy --extra-experimental-features 'nix-command flakes' \ + --to "${CACHE_URL}?secret-key=${NIX_SECRET_KEY_FILE}" \ + .#${pkg} + done + + # 6. Maintainer match. + - name: maintainer check + if: steps.changed.outputs.packages != '' + run: | + author='${{ github.event.pull_request.user.login }}' + for pkg in ${{ steps.changed.outputs.packages }}; do + list="recipes/$pkg/maintainers.txt" + if [[ ! -f "$list" ]]; then + echo "new package $pkg — maintainers.txt will be added by this PR" + continue + fi + if ! grep -E -q "^\s*${author}\s*(\#.*)?$" "$list"; then + echo "PR author '$author' is not in $list" + gh pr edit ${{ github.event.pull_request.number }} \ + --add-label needs-human-review + exit 1 + fi + done + + - name: label auto-merge + if: steps.changed.outputs.packages != '' + run: | + gh pr edit ${{ github.event.pull_request.number }} --add-label auto-merge diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e42d3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/result +/result-* +flake.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..7cbfd12 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# cargoxx-pkgs + +Public package registry for [cargoxx](../cargoxx). + +## Structure + +``` +recipes/ + / + maintainers.txt # gitea usernames, one per line + meta.toml # description, homepage, license, repository + versions/ + 1.0.0.toml # one file per published version + 1.0.1.toml +``` + +## Version recipe schema + +`recipes//versions/.toml`: + +```toml +schema = 1 +name = "" +version = "" + +[source] +type = "git" +url = "https://gitea.example//" +commit = "<40-char-commit>" +sha256 = "sha256-" # SRI form; from `nix flake prefetch` + +[dependencies] # mirrors the package's Cargoxx.toml +fmt = "10.2" +otherlib = { version = "0.3", registry = "cargoxx" } + +[lock] +nixpkgs_rev = "<40-char>" +flake_utils_rev = "<40-char>" +cargoxx_rev = "<40-char>" # cargoxx version that built this + +[meta] +description = "..." +homepage = "https://..." +license = "MIT" +``` + +## Maintainers + +`recipes//maintainers.txt` lists Gitea usernames authorized to +publish new versions of `` or edit `maintainers.txt` itself. +Comments start with `#`; blank lines are ignored. + +## Publishing + +Use `cargoxx publish` from a checked-out cargoxx project. The tool +opens a PR against this repository. CI validates the PR (source +fixity, build smoke, dependency closure, maintainer match) and +auto-merges when all checks pass. + +## Building locally + +```sh +nix build .# +``` + +Yields the same `$out` layout cargoxx libraries always produce — +`lib/cmake//Config.cmake`, `lib/pkgconfig/.pc`, +`lib/lib.a`, `include//`. Consume from any CMake or +pkg-config project, or from another cargoxx project via +`{ version = "...", registry = "cargoxx" }`. diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c9bbc5e --- /dev/null +++ b/flake.nix @@ -0,0 +1,88 @@ +{ + description = "cargoxx package registry"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + + # During local development we point at the sibling cargoxx checkout + # via an absolute `git+file://` URL. Once the registry lives on Gitea + # this becomes a Gitea URL pinned to a specific cargoxx revision — + # that pin, alongside `lock.cargoxx_rev` in each recipe, is what + # makes registry derivations deterministic across consumers (see + # docs/library-reuse-and-publish.md in the cargoxx repo). + cargoxx.url = "git+file:///home/mozart/cargoxx"; + }; + + outputs = { self, nixpkgs, flake-utils, cargoxx }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = import nixpkgs { inherit system; }; + + # version_safe: 1.2.3 → "1_2_3". The attr name `_` is + # the canonical handle for a (name, version) pair; bare `` + # aliases the highest-versioned entry. + sanitizeVersion = v: + builtins.replaceStrings ["." "-" "+"] ["_" "_" "_"] v; + + # Read recipes//versions/.toml → buildCppPackage'd drv. + mkPackage = recipeFile: + let r = builtins.fromTOML (builtins.readFile recipeFile); + in cargoxx.lib.${system}.buildCppPackage { + src = pkgs.fetchgit { + inherit (r.source) url; + rev = r.source.commit; + hash = r.source.sha256; + }; + name = "${r.name}-${r.version}"; + }; + + # builtins.readDir → attrset of name → type ("regular"/"directory"). + # Yields [{ name, type }] entries, filtering on a predicate. + dirEntries = path: predicate: + if builtins.pathExists path + then pkgs.lib.filterAttrs (n: t: predicate n t) (builtins.readDir path) + else {}; + + # All package directories under recipes/. + packageNames = builtins.attrNames + (dirEntries ./recipes (_: t: t == "directory")); + + # For one package: enumerate its versions, returning an attrset + # { "_" = ; "" = ; }. + versionsFor = pkgName: + let + versionsDir = ./recipes + "/${pkgName}/versions"; + files = builtins.attrNames + (dirEntries versionsDir (n: t: + t == "regular" && pkgs.lib.hasSuffix ".toml" n)); + versions = map (f: + let + version = pkgs.lib.removeSuffix ".toml" f; + attr = "${pkgName}_${sanitizeVersion version}"; + drv = mkPackage (versionsDir + "/${f}"); + in { inherit version attr drv; } + ) files; + byVersion = pkgs.lib.listToAttrs + (map (v: { name = v.attr; value = v.drv; }) versions); + highest = pkgs.lib.last + (pkgs.lib.sort (a: b: + builtins.compareVersions a.version b.version < 0) versions); + latestAlias = + if versions == [] then {} + else { ${pkgName} = highest.drv; }; + in byVersion // latestAlias; + + packagesAttrs = pkgs.lib.foldl' (acc: n: + acc // (versionsFor n)) {} packageNames; + in { + packages = packagesAttrs; + + # Bare `nix flake check` should pass with zero recipes. + checks = { + schema-ok = pkgs.runCommand "registry-schema-ok" {} '' + touch $out + ''; + }; + }); +} diff --git a/recipes/.gitkeep b/recipes/.gitkeep new file mode 100644 index 0000000..e69de29