Private Arch repo: publishing design (finalized)

Goal: host a private pacman repo on a web server reachable over SSH, and have local scripts that (1) scaffold the remote repo layout once, and (2) build, sign and publish packages from this arch/ tree into it.

Tooling constraint: oksh instead of bash, no Python. Scripts follow the house style already used in arch/scripts/ (POSIX-ish shell, tabs, set -u, plain test/[ ] guards, header comments explaining the why, config sourced from a key=value file via .).

1. Remote layout

Standard multi-arch pacman repo tree, one directory per architecture under an os/ subdir (this is what dbscripts/repod and every distro mirror use, and it’s what a Server = .../os/$arch line in pacman.conf expects):

${REPOSITORY_REMOTE_PATH}/os/x86_64/
${REPOSITORY_REMOTE_PATH}/os/i686/
${REPOSITORY_REMOTE_PATH}/os/pentium4/
${REPOSITORY_REMOTE_PATH}/os/i486/
...

(REPOSITORY_REMOTE_PATH already includes the repo name — /data/arch32/mirror/archlinuxaba — so no extra ${REPOSITORY_NAME} path segment is inserted; REPOSITORY_NAME is only used for the db filename itself.)

Each dir eventually holds *.pkg.tar.zst (+.sig, always) and the repo database (${REPOSITORY_NAME}.db.tar.gz / .files.tar.gz, unsigned by default — SIGN_DATABASE=no — since signing the database on top of already-signed packages isn’t done for repos like this one) — those are created/updated by repo-add, not by our scripts directly. repo_create_remote.sh seeds an empty database in every arch dir right away (see §3), so pointing a fresh machine’s pacman.conf at this repo before any package has ever been published doesn’t 404 on pacman -Sy.

Client-side pacman.conf entry (documented only, not written by any script):

[archlinuxaba]
SigLevel = Required DatabaseOptional
Server = https://archlinux32.andreasbaumann.cc/archlinuxaba/os/$arch

Which archs get a directory

repo_detect_archs.sh scans every arch=(...) line under arch/*/*/PKGBUILD, unions the values, drops any, and prints the result. Decision: auto-detect the full set (not narrowed to archlinux32’s 32-bit archs), even though the host/URL are archlinux32-branded — confirmed explicitly. Real set currently found:

x86_64  i686  pentium4  i486  aarch64  armv6h  armv7h  arm  riscv64

…but only x86_64, i686, pentium4, i486 have a local extra-<arch>-build script (i.e. can actually be built on this machine). repo_create_remote.sh creates dirs for the full detected set (cheap, idempotent mkdir -p); repo_build.sh only ever attempts archs it can build locally and skips+reports the rest (see §3).

REPO_ARCHS in the config can override the detected list if that’s ever wanted, but is left blank by default.

2. Config file

New file, arch/config/repo.conf, same key=value format as the existing arch/config/global.conf, sourced with . by every script:

REPOSITORY_HOST=euroweb.lan
REPOSITORY_SSH_USER=root
REPOSITORY_SSH_PORT=22
REPOSITORY_SSH_KEY=
REPOSITORY_REMOTE_PATH=/data/arch32/mirror/archlinuxaba
REPOSITORY_NAME=archlinuxaba
REPOSITORY_URL=https://archlinux32.andreasbaumann.cc/archlinuxaba/
GPG_SIGNING_KEY=16194A82231E9EF823562181C8E8F5A0AF9BA7E7
SIGN_DATABASE=no
REPO_ARCHS=

3. Scripts (all under arch/scripts/)

script purpose
repo_lib.sh not executed directly; sourced by the others. Loads config/repo.conf, validates required keys are non-empty, computes REPO_SSH_TARGET (user@host), and exposes repo_ssh/repo_rsync wrapper functions (inject port+identity file) plus repo_add_remote <dir> [pkgfile...] (runs repo-add for REPOSITORY_NAME’s db in a remote dir, adding -s -k only if SIGN_DATABASE=yes; with zero pkgfile args it seeds a fresh empty db, a safe no-op if one already exists).
repo_detect_archs.sh prints the union of arch=() values found under arch/*/*/PKGBUILD (minus any), or REPO_ARCHS if set. Used by both scaffolding and reporting.
repo_create_remote.sh idempotent. For each arch from repo_detect_archs.sh: ssh mkdir -p ${REPOSITORY_REMOTE_PATH}/os/<arch>, then repo_add_remote with no files to seed an empty database there (so a fresh pacman.conf pointed at the repo doesn’t 404 before anything’s published). Safe to re-run whenever a new arch shows up in the tree.
repo_build.sh <pkgdir> like the existing build_all_archs.sh, but a real build (no --nobuild). For arch=any (or an empty arch=()) packages: build once via the x86_64 build command only (an “any” package doesn’t need multiple chroots). For a normal multi-arch package: for each arch in its own arch=(), run that arch’s build command if a local one exists; otherwise print SKIPPED (no local archbuild) and continue — never aborts the whole run just because e.g. aarch64 isn’t buildable here. REPO_BUILD_ONLY_ARCH, if set, restricts this to just that one arch. The build command itself is resolved per-arch by a _build_cmd helper: x86_64 uses archlinuxaba-x86_64-build (see below), every other arch still uses plain extra-<arch>-build. Leaves the built *.pkg.tar.zst files in pkgdir. Exit status is non-zero only if zero archs built successfully. On success only (so a failed run’s logs survive for debugging), cleans up every untracked (git status --porcelain) byproduct from pkgdir – never anything git already tracks, or the *.pkg.tar.*/.sig outputs themselves.
archlinuxaba-archbuild (symlinked as archlinuxaba-x86_64-build) a copy of the system /usr/bin/archbuild (devtools32), changed in one place: pacman_config is resolved from arch/config/pacman.conf.d/ first, falling back to the system /usr/share/devtools/pacman.conf.d/. arch/config/pacman.conf.d/archlinuxaba.conf is a copy of stock extra.conf with our own [archlinuxaba] repo (SigLevel = Optional TrustAll, Server = ${REPOSITORY_URL}os/$arch) added ahead of [core]/[extra] — mirroring devtools32’s own *-with-build-support config layering pattern. Uses its own chroot (/var/lib/archbuild/archlinuxaba-x86_64), never touches the stock extra-x86_64 chroot or anything under /usr. Fixes the intra-tree dependency-ordering gap (a package here depending on another custom package from this same tree, already built+published earlier in the same repo_release_all.sh run, used to fail with error: target not found) — see memory/TOOLING_NOTES.md and memory/kdstatemachineeditor.md. Only wired up for x86_64 so far.
repo_sign.sh <pkgfile...> gpg --detach-sign --local-user "${GPG_SIGNING_KEY}" for each package file, producing .sig next to it. Refuses to overwrite an existing .sig unless -f is given.
repo_publish.sh <pkgfile...> for each package file: derive its pkgname/arch from the filename. Before publishing a new version: repo-remove the pkgname from the affected db(s) (harmless no-op on a first-ever publish), and move any older real package file for that exact pkgname (matched by exact string equality on the parsed-out name, not a glob — see memory/repo_publish_archive_bug.md for why a glob is unsafe here) into ${REPOSITORY_REMOTE_PATH}/archive/ (a flat dir, sibling of os/, append-only — never delete/move anything back out of it, see memory/feedback_archive_append_only.md). Then: if it’s an any package, rsync it to one real arch dir (x86_64), then for every other arch dir, ssh ln -sf ../<chosen-arch>/<file> <path> (relative symlink, so no bytes are duplicated) for both the package and its .sig, clearing old symlinks the same exact-match way first. If it’s a real per-arch package: rsync pkg+sig straight to the matching os/<arch>/ dir only. Then repo_add_remote on every arch db that was touched.
repo_release.sh <pkgdir> orchestrator, mirrors update_cycle.sh: build → sign → publish in one call for a single package directory. Once publish succeeds, removes the local *.pkg.tar.zst/.sig (safely on the repo host now) so the package directory doesn’t stay permanently dirty.
repo_release_all.sh walks every PKGBUILD under arch/ (excluding attic/), running repo_release.sh for each that can build for REPO_BUILD_ONLY_ARCH (default x86_64); prints one PASS/FAIL/SKIP summary line per package to stdout and each package’s full build/sign/publish output to ../repo_release_logs/<relpath-with-underscores>.log. Reads its package list via fd 3, not stdin — repo_ssh’s ssh -n plus this keeps any subprocess from ever consuming the loop’s remaining input and silently truncating the run after one package (a real bug hit and fixed 2026-07-07). Safe to re-run.

None of these touch git or the AUR — they only read PKGBUILD/build output and talk to the private repo host.

4. Workflow

# one-off / whenever a new arch appears
arch/scripts/repo_create_remote.sh

# per package release
cd arch/maintained/<pkg>
../../scripts/repo_release.sh .
# or, stepwise:
../../scripts/repo_build.sh .
../../scripts/repo_sign.sh *.pkg.tar.zst
../../scripts/repo_publish.sh *.pkg.tar.zst

5. Safety / idempotency conventions (matching existing scripts)

6. Resolved decisions (was “open questions”)

  1. arch=any handling → symlink into other arch dirs (§3).
  2. Config location → arch/config/repo.conf.
  3. GPG key → existing personal key 16194A82...BA7E7.
  4. Unbuildable arch in repo_build.shskip and report, never hard-fail; any/empty arch=() builds via x86_64 only.
  5. SSH identity → default agent/identity, REPOSITORY_SSH_KEY blank.
  6. Repo name → archlinuxaba.
  7. Real values → REPOSITORY_HOST=euroweb.lan, REPOSITORY_REMOTE_PATH=/data/arch32/mirror/archlinuxaba, REPOSITORY_URL=https://archlinux32.andreasbaumann.cc/archlinuxaba/.
  8. Arch scope → auto-detect full set from PKGBUILDs, not narrowed to archlinux32’s 32-bit-only archs.
  9. SSH login user → split into REPOSITORY_HOST (bare host) + REPOSITORY_SSH_USER=root (write access to the remote path), combined into REPO_SSH_TARGET in repo_lib.sh.
  10. Database signing → unsigned (SIGN_DATABASE=no), packages are still always signed by repo_sign.sh. Traditional for repos like this one.
  11. Empty-repo 404s → repo_create_remote.sh now seeds an empty repo-add database per arch dir as part of scaffolding, not just the directory.

All scripts written under arch/scripts/, syntax-checked with oksh -n, and functionally smoke-tested locally (fake extra-*-build stubs for repo_build.sh’s skip/build logic, real gpg signing for repo_sign.sh, a fake local ssh stub for repo_create_remote.sh’s mkdir+empty-db-seed flow including idempotency). Not yet run against the real euroweb.lan host by Claude — that’s for the user to do (or ask for explicitly), since it’s a live remote system.