diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4aaea2b0..b4b78b90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,7 @@ jobs: set -euo pipefail pacman -Syu --noconfirm jq ./bin/sync-upstream self-test + ./bin/sync-rebuilds --self-test ./bin/omarchy-pkgs self-test ./bin/omarchy-release self-test ' diff --git a/.gitignore b/.gitignore index d5f201f4..3f0c128d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.firecrawl/ src/ logs/ build-output/ diff --git a/README.md b/README.md index 15f55e17..d57fcb1a 100644 --- a/README.md +++ b/README.md @@ -30,18 +30,57 @@ The filesystem no longer encodes release policy. Instead: ## Prerequisites ### aarch64 Builds (Optional) -To build ARM64 packages on x86_64, enable QEMU emulation: +The repository host builds every architecture it publishes on the same +machine. A foreign architecture runs under QEMU user emulation, which +`bin/build` checks by actually running a container for the target platform. +Rootful Docker registers QEMU on first use. Rootless Podman uses the host's +registration and prints the one-time Arch setup commands when it is missing or +lacks the credential flag required by `sudo` inside the builder: ```bash -# Run after each reboot -docker run --privileged --rm tonistiigi/binfmt --install arm64 - # Verify -docker run --rm --platform linux/arm64 alpine:latest uname -m +podman run --rm --platform linux/arm64 docker.io/library/alpine:latest uname -m # Should output: aarch64 ``` -**Note**: aarch64 builds use QEMU and slower than native x86_64 builds. +**Note**: emulated builds are much slower than native ones. + +### Published architectures + +`helpers/paths.sh` names the architectures this repository publishes: + +```bash +PUBLISHED_ARCHES="${OMARCHY_ARCHES:-x86_64}" +``` + +That list drives the whole scheduled pipeline. `check-versions` compares +PKGBUILDs against each architecture's channel databases and writes one queue +file per channel and architecture (`.sync-needed--`); +`auto-release ` works through the queues one architecture at a +time, each with its own backoff (`.build-failed--`), so a +failing build on one architecture never holds up the other; and the release +train advances channels with `--arch all`: it takes one host-wide lock and +verifies every architecture's source database before moving any of them. The +first entry is the reference architecture the release train observes channels +through. A remote sync failure can still leave a promotion temporarily partial; +rerunning the same advance completes it safely. + +Adding an architecture to the scheduled pipeline is therefore one checked-in +change to that list: the next `check-versions` tick queues everything the new +architecture lacks, and the next `auto-release` tick starts building it. A +checked-in list also means the rebuild workflow and release host cannot drift +onto different architecture sets. For a one-off run, override it directly: + +```bash +OMARCHY_ARCHES=x86_64 bin/check-versions +OMARCHY_ARCHES=aarch64 bin/check-versions +OMARCHY_ARCHES="x86_64 aarch64" bin/check-versions +``` + +The builder image bootstraps +`omarchy-keyring` from the x86_64 tree for every architecture, so the first +build of a new architecture does not depend on a repository that only it can +create. ## Quick Start @@ -300,7 +339,8 @@ bin/sync-upstream openai-codex-desktop # Update specific packages Some vendors publish a release feed of their own that is faster and more precise than the AUR packaging of it. Those packages are `source: local` — Omarchy owns -the PKGBUILD — and declare where releases come from in one of two ways. +the PKGBUILD — and declare where releases come from either as data or, for an +unusual feed, a small hook. A vendor shipping tagged GitHub releases is pure data, declared as `upstream` in `.omarchy/package.json` with no code at all: @@ -322,6 +362,32 @@ digest GitHub's release API reports for every asset — see `pkgbuilds/schist-bin/.omarchy/package.json`. Either way the artifacts themselves are never downloaded. +An architecture may map to an ordered array when its PKGBUILD downloads more +than one release asset. Small versioned files outside the release assets can be +listed under `sources` and are downloaded and hashed when a new version appears: + +```json +"upstream": { + "github": "owner/project", + "digests": true, + "assets": { + "x86_64": ["tool-{pkgver}-x86_64", "tool-{pkgver}-x86_64.asc"], + "aarch64": ["tool-{pkgver}-aarch64", "tool-{pkgver}-aarch64.asc"] + }, + "sources": { + "any": ["https://raw.githubusercontent.com/owner/project/{tag}/LICENSE"] + } +} +``` + +Asset and source keys must be disjoint because each key maps to one PKGBUILD +checksum array (`any` means the unsuffixed `sha256sums`). + +Repositories whose historical releases use incompatible tag schemes may set +`"latest_only": true`. The provider then considers only the newest stable +GitHub release, while retaining all validation for that release. A quarantine +will wait for that release to age instead of falling back to an older one. + `{tag}` and `{pkgver}` interpolate into asset names; a leading `v` on the tag is stripped for `pkgver`; drafts and prereleases are ignored. Only the 100 most recent releases are considered. The provider fails closed on anything it cannot @@ -364,14 +430,33 @@ the tarball named by the selected dist-tag: `dist_tag` defaults to `latest`. The registry's publication timestamp is carried into the provider result, so `min_release_age` works for npm packages. -Exactly one of `github`, `git_tags`, or `npm` may appear in a declaration. + +A vendor with a plain-text Debian `Packages` index can use it to discover the +newest exact package version, then hash immutable source URLs: + +```json +"upstream": { + "debian": "https://example.com/debian/dists/stable/main/binary-amd64/Packages", + "package": "example-app", + "sources": { + "x86_64": ["https://example.com/tool-{pkgver}-x64.tar.gz"], + "aarch64": ["https://example.com/tool-{pkgver}-arm64.tar.gz"] + } +} +``` + +This deliberately accepts only Debian versions that are already valid Arch +`pkgver` values. Feeds needing epoch, revision, or filename translation retain +a hook. Exactly one of `github`, `git_tags`, `npm`, or `debian` may appear in a +declaration. A timestamped provider may also declare `"min_release_age": "24h"` (`s`/`m`/`h`/`d` suffix or bare seconds) to quarantine fresh releases until maintainers have had time to pull a bad or compromised one. GitHub Releases and -npm provide publication times; raw git tags do not, so combining `git_tags` -with this policy fails closed. The newest release that has cleared the window -ships, so a fast release cadence cannot starve updates. The window is enforced +npm provide publication times; raw git tags and Debian Packages indexes do not, +so combining either with this policy fails closed. The newest release that has +cleared the window ships, so a fast release cadence cannot starve updates. The +window is enforced centrally: whatever reports the release must prove its age via `published_at`, or the sync fails. A maintainer deliberately shipping inside the window runs `BYPASS_MIN_RELEASE_AGE=1 bin/sync-upstream ` locally and merges the @@ -424,7 +509,12 @@ A package names those dependencies in `.omarchy/package.json`: { "source": "aur", "sync": false, "rebuild_on": ["qt6-base", "qt6-declarative", "qt6-wayland"] } ``` -`bin/sync-rebuilds` reads each named package's version from the official repositories and compares it to `rebuilt_against`, the record of what the checked-in pkgrel was last bumped for. pkgrel is bumped unless every name in `rebuild_on` is recorded and still matches, so a name the record does not carry reads as changed rather than going unexamined forever. Opting a package in therefore buys one rebuild: what its published build actually linked against is not knowable from here, and a record written without a rebuild would certify a build nobody checked. +`bin/sync-rebuilds` reads each named package's version from the official +repositories for every published architecture the package supports and compares +it to `rebuilt_against`. Records are kept per architecture because Arch and +Arch Linux ARM can carry different dependency versions. pkgrel is bumped once +when any recorded version moves; that one source revision is then rebuilt by +each architecture's normal queue. The bump is the point of the command, and it has to land in git rather than in the builder. A rebuild that reuses the published version string produces a package pacman will never offer anyone, so merely unlocking the build gate would ship nothing. Bumping pkgrel needs no other change: `bin/check-versions` and the builder both already rebuild when pkgrel moves. @@ -432,9 +522,12 @@ For an AUR-synced package the bump is expressed as the dotted Omarchy pkgrel suf The bumped version is checked against the published one as well as the checked-in one, and refused when pacman would not order it higher. The checked-in version is not the floor; what a user already has is, and a checkout that has fallen behind the repository can otherwise be bumped to something that loses to the package it means to replace. That check is skipped with a warning when the published database cannot be read. -Versions are read from the local pacman database, so this runs on Arch or in an Arch container against a synced database. Only `core`, `extra` and `multilib` count: a Qt release sitting in testing or kde-unstable is not what the builder will link against, and rebuilding for it would ship a package built against the wrong ABI. The workflow points that database at `mirror.omarchy.org`, the mirror the x86_64 builder itself uses, because a mirror running ahead of the builder would record a version the build never linked against and nothing re-fires once the record matches. - -aarch64 is not covered. Those builds resolve Qt from Arch Linux ARM, which can lag Arch, so one record cannot describe both architectures. Only x86_64 is published today, so nothing currently ships from the untracked side; if ARM publishing starts, `rebuilt_against` has to become per-architecture before this can be trusted there. +x86_64 versions are read from the local pacman database, so the workflow runs +in an Arch container pointed at `mirror.omarchy.org`, the same mirror as the +x86_64 builder. aarch64 versions are read directly from the live Arch Linux ARM +repository database, which is also what the ARM builder uses. Testing and +staging repositories do not count. A legacy flat `rebuilt_against` record is +read as x86_64 and is migrated naturally the next time a rebuild is needed. ### Other @@ -632,7 +725,7 @@ Minimal examples: Fields: - `source`: `aur` or `local`. A `local` package can still follow an upstream release, either declaratively via `upstream` or with an `.omarchy/upstream.sh` hook. -- `upstream`: optional for `local` packages whose vendor ships tagged GitHub releases. `{ "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "": "name-{tag}.tar.xz" } }`, or `"digests": true` in place of `checksums` to use the release API's per-asset digests — see [Sync Upstream Releases](#sync-upstream-releases). Mutually exclusive with `.omarchy/upstream.sh`. +- `upstream`: optional for `local` packages following GitHub releases, git tags, npm dist-tags, or a Debian `Packages` index. GitHub architecture assets may be a string or an ordered array, and can be combined with disjoint versioned `sources` — see [Sync Upstream Releases](#sync-upstream-releases). Mutually exclusive with `.omarchy/upstream.sh`. - `min_release_age`: optional quarantine for upstream releases (`"24h"`, `"2d"`, or bare seconds). The newest release older than the window ships; anything younger waits, and a release whose age cannot be proven fails the sync. Bypass deliberately with `BYPASS_MIN_RELEASE_AGE=1 bin/sync-upstream `. - `sync`: optional for AUR packages; defaults to `true`. Set `false` for AUR-origin packages that Omarchy maintains manually. - `aur`: optional AUR package name when it differs from the local package directory, usually for split packages. @@ -642,7 +735,7 @@ Fields: - `skip_build`: optional boolean; defaults to `false`. Set `true` to exclude a package from scheduled version checks and unscoped builds. The package can still be built explicitly with `bin/repo release --package `. - `pkgrel`: optional Omarchy pkgrel suffix for a version-pinned rebuild bump. This emits `.` instead of replacing AUR's pkgrel. `offset` can be used only when preserving monotonic upgrades from old absolute pkgrel bumps. The metadata is removed automatically when AUR sync changes `pkgver`; the current package version is read from the checked-in PKGBUILD, so the version is not duplicated in JSON. - `rebuild_on`: optional array of package names this package links against closely enough that it must be rebuilt when they change, independent of its own source. Read by `bin/sync-rebuilds`. -- `rebuilt_against`: written by `bin/sync-rebuilds`. Records the version of each `rebuild_on` package that the current pkgrel was bumped for. +- `rebuilt_against`: written by `bin/sync-rebuilds`. Maps each published architecture to the versions of its `rebuild_on` packages that the current pkgrel was bumped for. - `upstream_commit`: set by `bin/sync-aur` for AUR packages. Used by `bin/package-worktree` to recreate the exact raw AUR package that Omarchy last synced. ### Build Matrix @@ -737,10 +830,15 @@ bin/repo release --package my-package - Mirrors: mirror.omarchy.org, rackspace, pkgbuild.com ### aarch64 -- QEMU emulation required on x86_64 hosts (slower) -- Uses Arch Linux ARM repositories +- Built on the repository host like x86_64; under QEMU when the host is x86_64 +- On an ARM host, package builds and the signing/database utility containers + run natively; only an explicitly requested x86_64 package build is emulated +- Uses Arch Linux ARM repositories through the same HTTPS mirror for every + channel (Arch Linux ARM publishes no dated snapshots to pin a channel's base) - Additional repos: `[alarm]`, `[aur]` -- Same workflow, just add `--arch aarch64` +- Same workflow, just add `--arch aarch64`; the scheduled pipeline runs it + automatically once `aarch64` is in `PUBLISHED_ARCHES` +- Packages whose `arch=()` lacks `aarch64` are skipped, not failed ### Building for Both Architectures @@ -791,8 +889,8 @@ The repository includes GitHub workflows and systemd services for automated rele All four units run **every 5 minutes**, staggered by a minute each, so a push reaches the mirror in minutes rather than hours: -1. **check-versions** (`*:0/5`): Pulls latest from git, compares PKGBUILD versions to published versions, creates state files if builds are needed -2. **auto-release-edge** (`*:1/5`): If a state file exists, builds all edge packages that need updates +1. **check-versions** (`*:0/5`): Pulls latest from git, compares PKGBUILD versions to published versions for every published architecture, creates one state file per channel and architecture if builds are needed +2. **auto-release-edge** (`*:1/5`): For each published architecture with a state file, builds all edge packages that need updates 3. **auto-release-rc** (`*:2/5`): Builds fast-ring packages for rc, from the main checkout like the other two — natively in the rc image, not copied from another channel. The pinned release pair is built separately by `omarchy-release rc` in the `rc` branch worktree 4. **auto-release-stable** (`*:3/5`): If a state file exists, builds `release_ring=fast` packages for stable and replicates them to rc @@ -806,10 +904,11 @@ That cadence is only safe because of three guards: an operator expects. `check-versions` takes it too — its `git pull` would otherwise swap PKGBUILDs out from under a running build. - **Backoff on failure.** A failed release records the attempt in - `.build-failed-` and backs off exponentially — 10m, 20m, 40m, up to + `.build-failed--` and backs off exponentially — 10m, 20m, 40m, up to a 6h ceiling — instead of rebuilding the same broken tree every 5 minutes. **Any new commit clears the backoff immediately**, since a push is the most - likely fix. Clear it by hand with `rm /root/.state/.build-failed-`. + likely fix. Clear it by hand with + `rm /root/.state/.build-failed--`. - **Quiet when idle.** With nothing queued a tick exits without output, so the journal shows the runs that mattered rather than 288 no-ops a day. @@ -862,10 +961,14 @@ bin/repo timers --local # inspect this machine instead ``` State files are stored in `/root/.state/`: -- `.sync-needed-` — the packages queued for that channel, one per - line; the release run reads them to name what it is building -- `.build-failed-` — consecutive failure count, timestamp, and the - commit it failed on (drives the backoff; removing it forces a retry) +- `.sync-needed--` — the packages queued for that channel and + architecture, one per line; the release run reads them to name what it is + building +- `.build-failed--` — consecutive failure count, timestamp, and + the commit it failed on (drives the backoff; removing it forces a retry) + +Legacy files without the architecture suffix are consumed once as x86_64 +state, so upgrading the host does not lose an in-flight build. ### Schedule (America/New_York) diff --git a/bin/advance-channel b/bin/advance-channel index 5581a8e2..1effbc64 100755 --- a/bin/advance-channel +++ b/bin/advance-channel @@ -25,6 +25,22 @@ SKIP_PROD_CHECK=false FAST_RING_ONLY=false BOOTSTRAP=false PACKAGES="" +ALL_ARCHES=false + +# Kept for --arch all, which re-invokes this script per architecture with the +# other arguments unchanged (minus the --arch all pair itself). +ORIGINAL_ARGS=() +arch_option=false +for arg in "$@"; do + if [[ "$arch_option" == true ]]; then + [[ "$arg" != "all" ]] && ORIGINAL_ARGS+=("--arch" "$arg") + arch_option=false + elif [[ "$arg" == "--arch" ]]; then + arch_option=true + else + ORIGINAL_ARGS+=("$arg") + fi +done usage() { echo "Usage: $0 --from --to [OPTIONS]" @@ -36,7 +52,8 @@ usage() { echo " or --bootstrap (one-time initial seed)" echo "" echo "Options:" - echo " --arch Target architecture (x86_64 or aarch64, default: x86_64)" + echo " --arch |all Target architecture (x86_64 or aarch64, default: x86_64);" + echo " all = every published architecture, one after another" echo " --package Advance only the named package(s)" echo " --fast-ring Restrict to fast-ring packages (stable -> rc parity copy)" echo " --bootstrap One-time stable -> rc seed before forward-only enforcement" @@ -65,8 +82,14 @@ while [[ $# -gt 0 ]]; do shift 2 ;; --arch) - ARCH="$2" - update_arch_paths + if [[ "$2" == "all" ]]; then + ALL_ARCHES=true + ARCH=all + else + require_valid_arch "$2" + ARCH="$2" + update_arch_paths + fi shift 2 ;; --package) @@ -134,6 +157,31 @@ case "$FROM->$TO" in ;; esac +if [[ "$ALL_ARCHES" == true ]]; then + # Hold one lock across the whole operation so a timer cannot mutate a + # channel between architectures. Check every source database before moving + # the first one; a failed sync can still require an idempotent retry, but a + # missing architecture never creates a knowingly partial advance. + if [[ "$DRY_RUN" != true ]]; then + acquire_release_lock || exit 1 + fi + + ARCHES=$(published_arches) + for arch in $ARCHES; do + source_db="$REPO_ROOT/$FROM/$arch/omarchy.db.tar.zst" + if [[ ! -f "$source_db" ]]; then + print_error "Source database not found: $source_db" + echo "Nothing has been published to the $FROM channel for $arch." + exit 1 + fi + done + + for arch in $ARCHES; do + "$0" --arch "$arch" "${ORIGINAL_ARGS[@]}" || exit $? + done + exit 0 +fi + SOURCE_DIR="$REPO_ROOT/$FROM/$ARCH" TARGET_DIR="$REPO_ROOT/$TO/$ARCH" SOURCE_DB="$SOURCE_DIR/omarchy.db.tar.zst" diff --git a/bin/auto-release b/bin/auto-release index 4d6d0648..b7094bb9 100755 --- a/bin/auto-release +++ b/bin/auto-release @@ -1,6 +1,10 @@ #!/bin/bash # Run the release workflow for a channel when work is queued. -# Usage: auto-release +# Usage: auto-release [|all] +# +# With no architecture (what the timers pass), every published architecture +# is processed in turn, each against its own queue and its own backoff, so a +# failing aarch64 build never holds up x86_64 or the other way round. # # Safe to run on a tight schedule. Three guards make that true: # @@ -21,7 +25,7 @@ source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/lock-helpers.sh" MIRROR="${1:-}" -STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}" +ARCH_ARG="${2:-all}" # Backoff schedule: 10m, 20m, 40m, 80m, 160m, 320m, then hourly-ish forever # (capped at 6h, the cadence this system ran at before frequent timers). @@ -29,8 +33,9 @@ BACKOFF_BASE_SECONDS="${OMARCHY_BACKOFF_BASE:-600}" BACKOFF_MAX_SECONDS="${OMARCHY_BACKOFF_MAX:-21600}" if [[ -z "$MIRROR" ]]; then - print_error "Usage: $0 " + print_error "Usage: $0 [|all]" echo " mirror: edge, rc, or stable" + echo " arch: one of $VALID_ARCHES, or all (default) for every published architecture" exit 1 fi @@ -39,17 +44,13 @@ if [[ "$MIRROR" != "edge" && "$MIRROR" != "rc" && "$MIRROR" != "stable" ]]; then exit 1 fi -STATE_FILE="$STATE_DIR/.sync-needed-$MIRROR" -FAIL_FILE="$STATE_DIR/.build-failed-$MIRROR" - -# Nothing queued: stay quiet. At a 5-minute cadence this is most invocations, -# and a header for each would bury the runs that matter in the journal. -if [[ ! -f "$STATE_FILE" ]]; then - exit 0 +if [[ "$ARCH_ARG" == "all" ]]; then + ARCHES=$(published_arches) +else + require_valid_arch "$ARCH_ARG" + ARCHES="$ARCH_ARG" fi -print_header "Processing Sync for $MIRROR" - # The build inputs are this checkout's contents; its HEAD identifies them. current_fingerprint() { git -C "$BUILD_ROOT" rev-parse HEAD 2>/dev/null || echo "unknown" @@ -65,57 +66,103 @@ backoff_seconds() { echo "$delay" } -FAIL_COUNT=0 -if [[ -f "$FAIL_FILE" ]]; then - # shellcheck disable=SC1090 - source "$FAIL_FILE" 2>/dev/null || true - FAIL_COUNT="${FAILURE_COUNT:-0}" - failed_at="${FAILURE_AT:-0}" - failed_fingerprint="${FAILURE_FINGERPRINT:-}" - - if [[ "$failed_fingerprint" != "$(current_fingerprint)" ]]; then - print_info "Repository changed since the last failure — clearing backoff and retrying" - rm -f "$FAIL_FILE" - FAIL_COUNT=0 - else - delay=$(backoff_seconds "$FAIL_COUNT") - now=$(date +%s) - retry_at=$((failed_at + delay)) - if ((now < retry_at)); then - print_warning "$MIRROR has failed $FAIL_COUNT time(s) on this tree — not retrying until $(date -d "@$retry_at" '+%H:%M:%S' 2>/dev/null || echo "+$((retry_at - now))s")" - echo " Push a fix (any new commit clears this), or: rm $FAIL_FILE" - exit 0 +# Returns 0 when this architecture's queue was processed (or was empty), 1 when +# the release failed. Backoff and "someone else holds the lock" are not +# failures: they are this tick deciding to do nothing. +release_arch() { + local arch="$1" + local state_file fail_file + state_file=$(sync_queue_file "$MIRROR" "$arch") + fail_file=$(sync_fail_file "$MIRROR" "$arch") + + # A queue written under the pre-architecture name belongs to x86_64. + if [[ "$arch" == "x86_64" && ! -f "$state_file" && -f "$(legacy_sync_queue_file "$MIRROR")" ]]; then + state_file=$(legacy_sync_queue_file "$MIRROR") + fi + if [[ "$arch" == "x86_64" && ! -f "$fail_file" && -f "$(legacy_sync_fail_file "$MIRROR")" ]]; then + fail_file=$(legacy_sync_fail_file "$MIRROR") + fi + + # Nothing queued: stay quiet. At a 5-minute cadence this is most + # invocations, and a header for each would bury the runs that matter in + # the journal. + [[ -f "$state_file" ]] || return 0 + + print_header "Processing Sync for $MIRROR ($arch)" + + local fail_count=0 failed_at failed_fingerprint delay now retry_at + if [[ -f "$fail_file" ]]; then + FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT="" + # shellcheck disable=SC1090 + source "$fail_file" 2>/dev/null || true + fail_count="${FAILURE_COUNT:-0}" + failed_at="${FAILURE_AT:-0}" + failed_fingerprint="${FAILURE_FINGERPRINT:-}" + + if [[ "$failed_fingerprint" != "$(current_fingerprint)" ]]; then + print_info "Repository changed since the last failure — clearing backoff and retrying" + rm -f "$fail_file" + fail_count=0 + else + delay=$(backoff_seconds "$fail_count") + now=$(date +%s) + retry_at=$((failed_at + delay)) + if ((now < retry_at)); then + print_warning "$MIRROR ($arch) has failed $fail_count time(s) on this tree — not retrying until $(date -d "@$retry_at" '+%H:%M:%S' 2>/dev/null || echo "+$((retry_at - now))s")" + echo " Push a fix (any new commit clears this), or: rm $fail_file" + return 0 + fi + print_info "Backoff elapsed — retrying $MIRROR ($arch) (failure #$((fail_count + 1)) if this fails)" fi - print_info "Backoff elapsed — retrying $MIRROR (failure #$((FAIL_COUNT + 1)) if this fails)" fi -fi -# Non-blocking: a build in progress means this tick has nothing to do. -if ! try_release_lock; then - holder=$(release_lock_holder) - print_info "A release is already running (${holder:-holder unknown}) — skipping this tick" - exit 0 -fi + # Non-blocking: a build in progress means this tick has nothing to do. The + # lock is reentrant, so once this run holds it the remaining architectures + # run under the same acquisition. + if ! try_release_lock; then + local holder + holder=$(release_lock_holder) + print_info "A release is already running (${holder:-holder unknown}) — skipping this tick" + return 0 + fi -print_info "State file found: $STATE_FILE" -print_info "Starting release workflow for $MIRROR..." + print_info "State file found: $state_file" + print_info "Starting release workflow for $MIRROR ($arch)..." -if "$BUILD_ROOT/bin/repo" release --mirror "$MIRROR" --skip-prod-check; then - print_success "Release completed successfully for $MIRROR" - rm -f "$STATE_FILE" - rm -f "$FAIL_FILE" - print_success "State file removed: $STATE_FILE" -else - status=$? - FAIL_COUNT=$((FAIL_COUNT + 1)) - cat >"$FAIL_FILE" <"$fail_file" </dev/null 2>&1; then - print_info "Setting up QEMU for ARM64 emulation..." - setup_qemu + # Rootless Podman cannot repair host binfmt state itself. Validate the flags + # before the basic probe, because an F-only registration can start an ARM + # container but silently breaks sudo inside it. + if [[ "$CONTAINER_ENGINE" == "podman" ]]; then + setup_qemu "$ARCH" + fi + + if ! "$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$ARCH")" "$PROBE_IMAGE" /bin/true >/dev/null 2>&1; then + if [[ "$CONTAINER_ENGINE" == "podman" ]]; then + print_error "QEMU $ARCH is registered, but the container probe failed" + print_info "Refresh the registration with: sudo systemctl restart systemd-binfmt" + exit 1 + else + print_info "Setting up QEMU for $ARCH emulation on this $HOST_ARCH host..." + setup_qemu "$ARCH" + fi fi fi @@ -179,7 +200,7 @@ if [[ $KEEP_BUILD_WORKSPACE == "1" ]]; then print_info "Keeping existing build workspace..." else print_info "Cleaning build workspace..." - rm -rf "$BUILD_OUTPUT_DIR"/* + rm -rf "${BUILD_OUTPUT_DIR:?}"/* fi mkdir -p "$BUILD_OUTPUT_DIR" @@ -196,7 +217,7 @@ fi # flag is that every job runs the same bytes. IMAGE_TAG="omarchy-pkg-builder:latest-$ARCH-$MIRROR" if [[ $SKIP_BUILDER_IMAGE == "1" ]]; then - if ! docker image inspect "$IMAGE_TAG" >/dev/null 2>&1; then + if ! "$CONTAINER_ENGINE" image inspect "$IMAGE_TAG" >/dev/null 2>&1; then print_error "Prepared builder image is unavailable: $IMAGE_TAG" exit 1 fi @@ -211,9 +232,12 @@ print_info "Running package build..." mkdir -p "$BUILD_OUTPUT_DIR" mkdir -p "$REPO_DIR" -# Ensure output directories are writable by container user -make_dir_writable "$BUILD_OUTPUT_DIR" -make_dir_writable "$REPO_DIR" +# Rootful Docker writes as the image uid, so retain its existing permission +# workaround. Rootless Podman uses keep-id and must leave ownership/modes alone. +if [[ "$CONTAINER_ENGINE" == "docker" ]]; then + make_dir_writable "$BUILD_OUTPUT_DIR" + make_dir_writable "$REPO_DIR" +fi # Build Docker arguments DOCKER_ARGS=( @@ -230,10 +254,16 @@ DOCKER_ARGS=( -v "$BUILD_ROOT/pkgbuilds:/pkgbuilds:ro" ) +# Podman-created images can leave WORKDIR owned by a remapped uid. Mount the +# existing host-user-owned workspace so the builder can write there. +if [[ "$CONTAINER_ENGINE" == "podman" ]]; then + DOCKER_ARGS+=(-v "$SRC_DIR:/src") +fi + # Run the builder with assembled args PLATFORM_ARG=$(get_platform_arg "$ARCH") -docker run $PLATFORM_ARG "${DOCKER_ARGS[@]}" "$IMAGE_TAG" /build/build.sh +"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" "$PLATFORM_ARG" "${DOCKER_ARGS[@]}" "$IMAGE_TAG" /build/build.sh BUILD_RESULT=$? diff --git a/bin/check-versions b/bin/check-versions index fe765839..9849701a 100755 --- a/bin/check-versions +++ b/bin/check-versions @@ -1,6 +1,10 @@ #!/bin/bash # Check PKGBUILD versions against published repo versions -# Creates state files for edge and/or stable if any packages need building +# Creates a state file per channel and architecture when packages need building +# +# Usage: check-versions [--pull] [--arch ] +# --pull pull the repository first (the scheduled run does this) +# --arch check one architecture; default: every published one set -e @@ -10,11 +14,27 @@ source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/package-metadata.sh" source "$BUILD_ROOT/helpers/lock-helpers.sh" -STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}" -ARCH="${ARCH:-x86_64}" PULL=false - -[[ "${1:-}" == "--pull" ]] && PULL=true +ARCHES="" + +while [[ $# -gt 0 ]]; do + case $1 in + --pull) + PULL=true + shift + ;; + --arch) + require_valid_arch "$2" + ARCHES="$2" + shift 2 + ;; + *) + print_error "Unknown option: $1" + exit 1 + ;; + esac +done +[[ -n "$ARCHES" ]] || ARCHES=$(published_arches) mkdir -p "$STATE_DIR" @@ -147,11 +167,12 @@ check_package() { check_mirror() { local mirror="$1" - local state_file="$STATE_DIR/.sync-needed-$mirror" + local state_file + state_file=$(sync_queue_file "$mirror" "$ARCH") local needs_build=false local packages=() - print_info "Checking $mirror packages..." + print_info "Checking $mirror packages for $ARCH..." while IFS= read -r pkg; do local pkgdir="$PKGBUILDS_DIR/$pkg" @@ -159,7 +180,7 @@ check_mirror() { needs_build=true packages+=("$pkg") fi - done < <(packages_for_unscoped_build "$mirror") + done < <(packages_for_unscoped_build "$mirror" "$ARCH") echo "" @@ -168,18 +189,23 @@ check_mirror() { # this to name the packages in its start report, which is the difference # between "a build is running" and "your package is in this build". printf '%s\n' "${packages[@]}" >"$state_file" - print_success "${mirror^} needs building (${#packages[@]} packages)" + print_success "${mirror^} ($ARCH) needs building (${#packages[@]} packages)" print_info "Packages: ${packages[*]}" print_info "State file created: $state_file" else - print_info "${mirror^} is up to date" + print_info "${mirror^} ($ARCH) is up to date" fi echo "" } -check_mirror edge -check_mirror rc -check_mirror stable +# One pass per published architecture: the version comparison reads that +# architecture's channel databases, and each queue is its own file. +for ARCH in $ARCHES; do + update_arch_paths + check_mirror edge + check_mirror rc + check_mirror stable +done print_success "Version check complete!" diff --git a/bin/clean-docker b/bin/clean-docker index 4a5fc945..93c9a659 100755 --- a/bin/clean-docker +++ b/bin/clean-docker @@ -1,21 +1,27 @@ #!/bin/bash -# Clean Docker builder images and cache +# Clean builder images and cache for the selected container engine # Forces a fresh image build on next run set -e BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") source "$BUILD_ROOT/helpers/message-helpers.sh" +source "$BUILD_ROOT/helpers/docker-helpers.sh" -print_header "Cleaning Docker Builder Images" +check_engine +print_header "Cleaning Container Builder Images" # Remove all omarchy-pkg-builder images print_info "Removing omarchy-pkg-builder images..." -docker images omarchy-pkg-builder -q | xargs -r docker rmi -f 2>/dev/null || true +"$CONTAINER_ENGINE" images omarchy-pkg-builder -q | xargs -r "$CONTAINER_ENGINE" rmi -f 2>/dev/null || true -# Clear buildx cache for these builds -print_info "Clearing buildx cache..." -docker buildx prune -a -f +# Clear this engine's build cache. +print_info "Clearing build cache..." +if [[ "$CONTAINER_ENGINE" == "docker" ]]; then + "$CONTAINER_ENGINE" buildx prune -a -f +else + "$CONTAINER_ENGINE" image prune --build-cache -f +fi -print_success "Docker builder cache cleared" +print_success "Container builder cache cleared" print_info "Next build will create fresh images" diff --git a/bin/clean-repo b/bin/clean-repo index 493b85a6..8543a8a7 100755 --- a/bin/clean-repo +++ b/bin/clean-repo @@ -31,9 +31,13 @@ clean_packages() { # Skip signature files [[ "$pkg" == *.sig ]] && continue - # Extract package name (remove version and architecture) - # Format: name-version-release-arch.pkg.tar.* - local pkgname=$(echo "$pkg" | sed -E 's/-[0-9]+.*-(any|x86_64|i686)\.pkg\.tar\..*//') + # Format: name-version-release-arch.pkg.tar.*. Work from the right because + # package names can themselves contain version-like pieces (qt6-5compat, + # nvidia-580xx-utils), while pkgver and pkgrel cannot contain hyphens. + local stem="${pkg%%.pkg.tar.*}" + stem="${stem%-*}" # architecture + stem="${stem%-*}" # pkgrel + local pkgname="${stem%-*}" # pkgver # Add to array if [[ -n "${packages[$pkgname]}" ]]; then diff --git a/bin/omarchy-pkgs b/bin/omarchy-pkgs index 58ff646b..0ca607b9 100755 --- a/bin/omarchy-pkgs +++ b/bin/omarchy-pkgs @@ -23,7 +23,7 @@ source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/host-helpers.sh" UPSTREAM_URL="${OMARCHY_UPSTREAM_URL:-https://github.com/basecamp/omarchy.git}" -EDGE_DB_URL="${OMARCHY_EDGE_DB_URL:-https://pkgs.omarchy.org/edge/x86_64/omarchy.db.tar.zst}" +EDGE_DB_URL="${OMARCHY_EDGE_DB_URL:-https://pkgs.omarchy.org/edge/$(reference_arch)/omarchy.db.tar.zst}" RELEASE_PACKAGES=(omarchy omarchy-settings) DEFAULT_RC_REF="quattro" SRCDEST_DIR="$BUILD_ROOT/.srcdest" @@ -327,13 +327,19 @@ regenerate_checksums() { trigger_build_host() { local host + queue_edge_builds() { + mkdir -p "$STATE_DIR" || return 1 + local arch + for arch in $(published_arches); do + touch "$(sync_queue_file edge "$arch")" || return 1 + done + } + # Explicit host configuration outranks the local-host inference (a # workstation that ran a full local release carries the db marker too). if ! resolve_repo_host "${REPO_HOST_OVERRIDE:-}" >/dev/null && on_repo_host; then print_info "Triggering edge build locally (this is the build host)..." - if mkdir -p "${OMARCHY_STATE_DIR:-/root/.state}" && - touch "${OMARCHY_STATE_DIR:-/root/.state}/.sync-needed-edge" && - systemctl start --no-block omarchy-auto-release-edge.service; then + if queue_edge_builds && systemctl start --no-block omarchy-auto-release-edge.service; then print_success "Edge build triggered" else print_warning "Could not start the edge release service — the 6-hourly timer will pick it up" @@ -343,11 +349,11 @@ trigger_build_host() { if ! host=$(resolve_repo_host "${REPO_HOST_OVERRIDE:-}"); then print_info "No repository host configured (set OMARCHY_REPO_HOST or $BUILD_ROOT/.repo-host — any ssh destination, e.g. root@ or an ssh-config alias)." print_info "The 6-hourly auto-release timer will pick this up, or trigger manually:" - echo " ssh 'git -C /root/omarchy-pkgs pull --ff-only && touch /root/.state/.sync-needed-edge && systemctl start omarchy-auto-release-edge.service'" + echo " ssh 'git -C /root/omarchy-pkgs pull --ff-only && systemctl start omarchy-check-versions.service omarchy-auto-release-edge.service'" return 0 fi print_info "Triggering edge build on $host..." - if ssh "$host" 'git -C /root/omarchy-pkgs pull --ff-only && mkdir -p /root/.state && touch /root/.state/.sync-needed-edge && systemctl start --no-block omarchy-auto-release-edge.service'; then + if ssh "$host" 'git -C /root/omarchy-pkgs pull --ff-only && cd /root/omarchy-pkgs && export BUILD_ROOT=/root/omarchy-pkgs && source helpers/paths.sh && mkdir -p "$STATE_DIR" && for arch in $(published_arches); do touch "$(sync_queue_file edge "$arch")"; done && systemctl start --no-block omarchy-auto-release-edge.service'; then print_success "Edge build triggered on $host" else print_warning "Could not trigger $host — the 6-hourly timer will pick it up" diff --git a/bin/omarchy-release b/bin/omarchy-release index c64d2ed5..12e689bd 100755 --- a/bin/omarchy-release +++ b/bin/omarchy-release @@ -18,6 +18,7 @@ set -e BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") source "$BUILD_ROOT/helpers/message-helpers.sh" +source "$BUILD_ROOT/helpers/docker-helpers.sh" source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/host-helpers.sh" @@ -28,7 +29,11 @@ ISO_REPO="${OMARCHY_ISO_REPO:-omacom-io/omarchy-iso}" DEV_BRANCH="${OMARCHY_DEV_BRANCH:-quattro}" PKGS_DB_BASE="${OMARCHY_PKGS_DB_BASE:-https://pkgs.omarchy.org}" -RC_DB_URL="$PKGS_DB_BASE/rc/x86_64/omarchy.db.tar.zst" +# The first published architecture supplies the version-ordering floor when +# cutting pins. Readiness checks below still verify every published +# architecture before an RC or final release can move forward. +OBSERVED_ARCH=$(reference_arch) +RC_DB_URL="$PKGS_DB_BASE/rc/$OBSERVED_ARCH/omarchy.db.tar.zst" SRCDEST_DIR="$BUILD_ROOT/.srcdest" MIRROR_CLONE="$SRCDEST_DIR/omarchy" # bare mirror (shared with bin/omarchy-pkgs) @@ -181,12 +186,12 @@ ensure_work_clone() { # Prints omarchy's published version in a channel; empty when absent, rc 2 when # the database cannot be read (callers must not mistake an outage for absence). published_version() { - local channel="$1" tmp descs + local channel="$1" arch="${2:-$OBSERVED_ARCH}" tmp descs tmp=$(mktemp) || return 2 # A unique query string busts the CDN cache: right after a sync the plain # URL can keep serving the previous db for a while, which reads as "not # published yet" to status, the wait loop, and ship's pre-checks. - if ! curl -sf "$PKGS_DB_BASE/$channel/x86_64/omarchy.db.tar.zst?$(date +%s%N)" -o "$tmp"; then + if ! curl -sf "$PKGS_DB_BASE/$channel/$arch/omarchy.db.tar.zst?$(date +%s%N)" -o "$tmp"; then rm -f "$tmp" return 2 fi @@ -207,6 +212,14 @@ published_version() { ' <<<"$descs" } +all_arches_at_version() { # all_arches_at_version + local channel="$1" want="$2" arch got + for arch in $(published_arches); do + got=$(published_version "$channel" "$arch" 2>/dev/null) || return 1 + [[ "${got%-*}" == "$want" ]] || return 1 + done +} + # The rc branch of THIS repo carries the current pins. Read them without # touching the working tree. rc_branch_pin() { # prints "pkgver commit", empty when no rc branch @@ -251,8 +264,12 @@ fi git -C /root/omarchy-pkgs-rc fetch origin rc git -C /root/omarchy-pkgs-rc reset --hard origin/rc cd /root/omarchy-pkgs-rc -OMARCHY_RC_PINS=1 OMARCHY_REPO_ROOT=/root/omarchy-pkgs/pkgs.omarchy.org \ - bin/repo release --mirror rc --package omarchy omarchy-settings --skip-prod-check +# The pair is built once per published architecture (helpers/paths.sh on the +# host decides which), so every architecture'"'"'s rc channel carries the pins. +for arch in $(BUILD_ROOT=/root/omarchy-pkgs-rc bash -c "source helpers/paths.sh; published_arches"); do + OMARCHY_RC_PINS=1 OMARCHY_REPO_ROOT=/root/omarchy-pkgs/pkgs.omarchy.org \ + bin/repo release --mirror rc --arch "$arch" --package omarchy omarchy-settings --skip-prod-check +done ' trigger_rc_build() { @@ -278,19 +295,25 @@ host_advance() { # host_advance [extra args...] # against this machine's (likely stale) local tree would be wrong. if ! resolve_repo_host "$REPO_HOST_OVERRIDE" >/dev/null && ! on_repo_host; then print_no_host_help "advance $from -> $to" \ - " cd /root/omarchy-pkgs && bin/repo advance --from $from --to $to --skip-prod-check $*" + " cd /root/omarchy-pkgs && bin/repo advance --from $from --to $to --arch all --skip-prod-check $*" return 1 fi - "$BUILD_ROOT/bin/repo" advance --from "$from" --to "$to" --skip-prod-check "$@" + # --arch all: the host advances every architecture it publishes, so a train + # never moves a channel for one architecture and not another. + "$BUILD_ROOT/bin/repo" advance --from "$from" --to "$to" --arch all --skip-prod-check "$@" } wait_for_published() { # wait_for_published [timeout-seconds] - local channel="$1" want="$2" timeout="${3:-3600}" waited=0 got - print_info "Waiting for $want to appear in the $channel channel (up to $((timeout / 60))m)..." + local channel="$1" want="$2" timeout="${3:-3600}" waited=0 arch got pending + print_info "Waiting for $want in $channel for: $(published_arches | tr '\n' ' ')" while ((waited < timeout)); do - got=$(published_version "$channel" 2>/dev/null) || got="" - if [[ "${got%-*}" == "$want" ]]; then - print_success "$channel now serves omarchy $got" + pending="" + for arch in $(published_arches); do + got=$(published_version "$channel" "$arch" 2>/dev/null) || got="" + [[ "${got%-*}" == "$want" ]] || pending+=" $arch=${got:-unreachable}" + done + if [[ -z "$pending" ]]; then + print_success "$channel now serves omarchy $want on every published architecture" return 0 fi sleep 60 @@ -298,7 +321,7 @@ wait_for_published() { # wait_for_published [timeout-seconds printf '.' >&2 done echo "" >&2 - print_warning "Timed out waiting for $want in $channel (currently: ${got:-unknown})" + print_warning "Timed out waiting for $want in $channel (pending:$pending)" print_info "The build may still be running — re-run this command to resume." return 1 } @@ -352,8 +375,8 @@ iso_checkout() { # prints a usable omarchy-iso checkout, cloning to tmp if neede build_iso() { # build_iso [--rc] local version="$1" rc_flag="${2:-}" dir dir=$(iso_checkout) || return 1 - if ! command -v docker >/dev/null || ! docker info >/dev/null 2>&1; then - print_warning "Docker unavailable — cannot build the ISO here. Run on a Docker machine:" + if [[ -z "$CONTAINER_ENGINE" ]] || ! "$CONTAINER_ENGINE" info >/dev/null 2>&1; then + print_warning "No container engine is available — cannot build the ISO here. Run on a Docker or Podman machine:" echo " cd $dir && bin/omarchy-iso-release ${rc_flag:+$rc_flag }$version" return 1 fi @@ -662,15 +685,13 @@ cmd_rc() { if [[ -n "$pin" ]]; then local pin_ver="${pin%% *}" pin_commit="${pin##* }" if [[ "$pin_commit" == "$head" && "$pin_ver" =~ ^${version//./\\.}rc[0-9]+$ ]]; then - local pub - pub=$(published_version rc 2>/dev/null) || pub="" - if [[ "${pub%-*}" == "$pin_ver" ]]; then - print_success "$pin_ver is already cut from this head and published to rc" + if all_arches_at_version rc "$pin_ver"; then + print_success "$pin_ver is already cut from this head and published to rc on every architecture" maybe_iso "$pin_ver" rc "$iso_mode" return 0 fi print_info "$pin_ver is pinned from this head but not published yet — re-triggering the build" - trigger_rc_build || true + trigger_rc_build || return 1 [[ "$wait" == true ]] && wait_for_published rc "$pin_ver" maybe_iso "$pin_ver" rc "$iso_mode" return 0 @@ -686,7 +707,7 @@ cmd_rc() { new_ver="${new_pin%% *}" print_success "Pinned $new_ver (rc branch pushed)" - trigger_rc_build || true + trigger_rc_build || return 1 if [[ "$wait" == true ]]; then wait_for_published rc "$new_ver" || return 1 fi @@ -708,9 +729,7 @@ cmd_ship() { exit 1 fi version=$(branch_to_version "$branch") - local stable_now - stable_now=$(published_version stable 2>/dev/null) || stable_now="" - if [[ "${stable_now%-*}" == "$version" ]] && + if all_arches_at_version stable "$version" && gh release view "v$version" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; then print_success "Nothing to ship — $version is tagged, released, and live on stable" exit 0 @@ -752,10 +771,8 @@ cmd_ship() { echo " omarchy-release rc" exit 1 fi - local pub - pub=$(published_version rc 2>/dev/null) || pub="" - if [[ "${pub%-*}" != "$pin_ver" ]]; then - print_error "$pin_ver is pinned but rc serves '${pub:-nothing}' — the candidate build hasn't published" + if ! all_arches_at_version rc "$pin_ver"; then + print_error "$pin_ver is pinned but is not published for every architecture" echo "Wait for it (or re-run: omarchy-release rc), then ship." exit 1 fi @@ -799,10 +816,9 @@ cmd_ship() { # 2. Final pins into rc. Resolve the tag we just established so the final # PKGBUILDs record both its provenance and its exact commit. - local rc_pub stable_pub - rc_pub=$(published_version rc 2>/dev/null) || rc_pub="" - if [[ "${rc_pub%-*}" == "$version" ]]; then - print_success "2/7 Final $version already published to rc" + local stable_pub + if all_arches_at_version rc "$version"; then + print_success "2/7 Final $version already published to rc on every architecture" else if [[ "$pin_ver" != "$version" ]]; then print_info "2/7 Pinning final $version from tag v$version..." @@ -810,22 +826,21 @@ cmd_ship() { else print_info "2/7 Final $version pinned — re-triggering build" fi - trigger_rc_build || true + trigger_rc_build || exit 1 wait_for_published rc "$version" || exit 1 fi # 3. Promote rc -> stable - stable_pub=$(published_version stable 2>/dev/null) || stable_pub="" - if [[ "${stable_pub%-*}" == "$version" ]]; then - print_success "3/7 Stable already serves $version" + if all_arches_at_version stable "$version"; then + print_success "3/7 Stable already serves $version on every architecture" else host_advance rc stable || exit 1 - stable_pub=$(published_version stable 2>/dev/null) || stable_pub="" - if [[ "${stable_pub%-*}" != "$version" ]]; then - print_error "Promotion ran but stable serves '${stable_pub:-nothing}' — investigate before continuing" + if ! all_arches_at_version stable "$version"; then + print_error "Promotion ran but stable does not serve $version on every architecture" exit 1 fi - print_success "3/7 Promoted to stable: omarchy $stable_pub" + stable_pub=$(published_version stable 2>/dev/null) || stable_pub="$version" + print_success "3/7 Promoted to stable: omarchy $stable_pub on every architecture" fi # 4. Final pins onto master (keeps edge overlap publishing and the repo record) @@ -908,7 +923,7 @@ next_step() { # prints "|" last=$(newest_release_branch 2>/dev/null) || last="" if [[ -n "$last" && "$STABLE_VER" != "" ]]; then last_ver=$(branch_to_version "$last") - if [[ "${STABLE_VER%-*}" != "$last_ver" ]] || + if ! all_arches_at_version stable "$last_ver" || { command -v gh >/dev/null && ! gh release view "v$last_ver" --repo "$UPSTREAM_REPO" >/dev/null 2>&1; }; then echo "ship|$last_ver is tagged but not fully shipped — resume ship" return @@ -924,7 +939,7 @@ next_step() { # prints "|" echo "ship|Final $TRAIN_VER is pinned — finish shipping (re-runs are safe)" elif [[ "$pin_commit" != "$TRAIN_HEAD" ]]; then echo "rc|$TRAIN has commits newer than $pin_ver — cut the next candidate" - elif [[ "${RC_VER%-*}" != "$pin_ver" ]]; then + elif ! all_arches_at_version rc "$pin_ver"; then echo "rc|$pin_ver is pinned but not published — re-run rc to re-trigger/wait" else echo "ship|$pin_ver is published to rc — test it, then ship" @@ -994,20 +1009,24 @@ cmd_doctor() { check "makepkg available (checksums)" command -v makepkg check "curl available" command -v curl check "upstream reachable ($UPSTREAM_URL)" git ls-remote "$UPSTREAM_URL" HEAD - local ch + local ch arch for ch in edge stable; do - if published_version "$ch" >/dev/null 2>&1; then - print_success "$ch channel db readable" + for arch in $(published_arches); do + if published_version "$ch" "$arch" >/dev/null 2>&1; then + print_success "$ch/$arch channel db readable" + else + print_error "$ch/$arch channel db readable" + failures=$((failures + 1)) + fi + done + done + for arch in $(published_arches); do + if published_version rc "$arch" >/dev/null 2>&1; then + print_success "rc/$arch channel db readable" else - print_error "$ch channel db readable" - failures=$((failures + 1)) + print_warning "rc/$arch channel db not readable — bootstrap it first: bin/repo bootstrap-rc (on the build host)" fi done - if published_version rc >/dev/null 2>&1; then - print_success "rc channel db readable" - else - print_warning "rc channel db not readable — bootstrap it first: bin/repo bootstrap-rc (on the build host)" - fi local host if host=$(repo_host); then check "build host ssh ($host)" ssh -o ConnectTimeout=10 "$host" true @@ -1016,10 +1035,10 @@ cmd_doctor() { else print_warning "no build host configured — set OMARCHY_REPO_HOST, --host, or write an ssh destination (root@ or an ssh-config alias) to $BUILD_ROOT/.repo-host; until then builds trigger on the 6h timer only" fi - if command -v docker >/dev/null && docker info >/dev/null 2>&1; then - print_success "docker available (ISO builds possible here)" + if [[ -n "$CONTAINER_ENGINE" ]] && "$CONTAINER_ENGINE" info >/dev/null 2>&1; then + print_success "$CONTAINER_ENGINE available (ISO builds possible here)" else - print_warning "docker unavailable — ISO builds will print instructions instead" + print_warning "container engine unavailable — ISO builds will print instructions instead" fi echo "" if ((failures == 0)); then @@ -1054,6 +1073,25 @@ cmd_self_test() { expect "version_is_patch 5.0.0" "$(version_is_patch 5.0.0 && echo yes || echo no)" "no" expect "previous_patch_tag 4.0.2" "$(previous_patch_tag 4.0.2)" "v4.0.1" expect "previous_patch_tag 4.0.10" "$(previous_patch_tag 4.0.10)" "v4.0.9" + + # Keep release readiness fail-closed when only one architecture has reached + # the requested version. This replaces the network reader for this process; + # self-test exits immediately afterwards. + published_version() { + case "$2" in + x86_64) echo "${TEST_X86_VERSION:-4.0.2-1}" ;; + aarch64) echo "${TEST_ARM_VERSION:-4.0.2-1}" ;; + esac + } + PUBLISHED_ARCHES=x86_64 + expect "x86-only readiness" "$(all_arches_at_version rc 4.0.2 && echo ready || echo blocked)" "ready" + PUBLISHED_ARCHES=aarch64 + expect "ARM-only readiness" "$(all_arches_at_version rc 4.0.2 && echo ready || echo blocked)" "ready" + PUBLISHED_ARCHES="x86_64 aarch64" + TEST_ARM_VERSION=4.0.1-1 + expect "mixed versions block release" "$(all_arches_at_version rc 4.0.2 && echo ready || echo blocked)" "blocked" + TEST_ARM_VERSION=4.0.2-1 + expect "both architectures ready" "$(all_arches_at_version rc 4.0.2 && echo ready || echo blocked)" "ready" echo "" if ((failures == 0)); then print_success "Self-test passed" diff --git a/bin/push-build b/bin/push-build index 5b850126..a4abae99 100755 --- a/bin/push-build +++ b/bin/push-build @@ -149,11 +149,16 @@ fi # leave nvidia-580xx-dkms and opencl-nvidia-580xx behind. An output's own name # still matches, for pushing just one of them on purpose. # -# pkgbase comes from .PKGINFO rather than the PKGBUILD: it is what makepkg -# actually recorded, and it needs no guessing about which directory built what. -pkgbase_of() { +# Package identity comes from .PKGINFO rather than the filename or PKGBUILD: it +# is what makepkg actually recorded, and it handles epochs and split packages. +package_identity_of() { bsdtar -xOf "$1" .PKGINFO 2>/dev/null | - awk -F ' = ' '$1 == "pkgbase" { print $2; exit }' + awk -F ' = ' ' + $1 == "pkgname" { name = $2 } + $1 == "pkgbase" { base = $2 } + $1 == "pkgver" { version = $2 } + END { print name "\t" base "\t" version } + ' } FILES=() @@ -161,14 +166,18 @@ if [[ -z "$PACKAGES" ]]; then FILES=("${ALL_FILES[@]}") else declare -A MATCHED=() + declare -A LATEST_FILE=() + declare -A LATEST_VERSION=() for file in "${ALL_FILES[@]}"; do - # name-version-release-arch.pkg.tar.zst -> name - pkgname="${file%-*-*-*.pkg.tar.*}" - pkgbase=$(pkgbase_of "$BUILD_OUTPUT_DIR/$file") + IFS=$'\t' read -r pkgname pkgbase pkgver < <(package_identity_of "$BUILD_OUTPUT_DIR/$file") for wanted in $PACKAGES; do if [[ "$pkgname" == "$wanted" || "$pkgbase" == "$wanted" ]]; then - FILES+=("$file") MATCHED["$wanted"]=1 + if [[ -z "${LATEST_FILE[$pkgname]:-}" ]] || + [[ $(vercmp "$pkgver" "${LATEST_VERSION[$pkgname]}") -gt 0 ]]; then + LATEST_FILE["$pkgname"]="$file" + LATEST_VERSION["$pkgname"]="$pkgver" + fi break fi done @@ -181,6 +190,8 @@ else exit 1 fi done + + mapfile -t FILES < <(printf '%s\n' "${LATEST_FILE[@]}" | sort) fi if [[ ${#FILES[@]} -eq 0 ]]; then diff --git a/bin/release b/bin/release index d40fa0f8..193bfb52 100755 --- a/bin/release +++ b/bin/release @@ -114,7 +114,11 @@ BUILT_FILES="" # What the scheduled version check queued, when it was the one that asked for # this run. Absent for a manual run, which is fine — the report just omits it. QUEUED_PACKAGES="" -QUEUE_FILE="${OMARCHY_STATE_DIR:-/root/.state}/.sync-needed-$MIRROR" +QUEUE_FILE=$(sync_queue_file "$MIRROR" "$ARCH") +# A queue written under the pre-architecture name belongs to x86_64. +if [[ "$ARCH" == "x86_64" && ! -s "$QUEUE_FILE" && -s "$(legacy_sync_queue_file "$MIRROR")" ]]; then + QUEUE_FILE=$(legacy_sync_queue_file "$MIRROR") +fi [[ -s "$QUEUE_FILE" ]] && QUEUED_PACKAGES=$(grep -c '' "$QUEUE_FILE") if [[ "$DRY_RUN" != true ]]; then diff --git a/bin/remove-package b/bin/remove-package index 6c798377..4700402b 100755 --- a/bin/remove-package +++ b/bin/remove-package @@ -62,8 +62,8 @@ if [[ ! -d "$REPO_DIR" ]]; then exit 1 fi -# Check Docker is available -check_docker +# Check the selected container engine is available +check_engine # Find package files to confirm before running Docker cd "$REPO_DIR" @@ -88,24 +88,29 @@ if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 0 fi -# Build/update the Docker image (always use x86_64 for removal - it's architecture independent) -# repo-remove is mirror-independent — always use the edge x86_64 image -build_docker_image "$BUILD_DIR" "x86_64" "edge" +# repo-remove is architecture-independent, so use a host-native edge image. +TOOL_ARCH=$(docker_native_arch) || { + print_error "Unsupported host architecture: $(uname -m)" + exit 1 +} +build_docker_image "$BUILD_DIR" "$TOOL_ARCH" "edge" acquire_release_lock || exit 1 print_info "Removing package..." -# Ensure directory is writable by container user -make_dir_writable "$REPO_DIR" +# Rootless Podman uses keep-id and leaves host ownership/modes intact. +if [[ "$CONTAINER_ENGINE" == "docker" ]]; then + make_dir_writable "$REPO_DIR" +fi -# Run the removal script in Docker (always use x86_64 image) -docker run --rm --platform linux/amd64 \ +# Run the removal script in the host-native image. +"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$TOOL_ARCH")" \ -e ARCH="$ARCH" \ -e MIRROR="$MIRROR" \ -v "$REPO_ROOT:/pkgs.omarchy.org" \ -v "$BUILD_DIR:/build:ro" \ - omarchy-pkg-builder:latest-x86_64-edge /build/remove-package.sh "$PACKAGE_NAME" + "omarchy-pkg-builder:latest-$TOOL_ARCH-edge" /build/remove-package.sh "$PACKAGE_NAME" RESULT=$? diff --git a/bin/setup b/bin/setup index 5467be64..69dfe0e9 100755 --- a/bin/setup +++ b/bin/setup @@ -15,6 +15,8 @@ set -e BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") source "$BUILD_ROOT/helpers/message-helpers.sh" +source "$BUILD_ROOT/helpers/docker-helpers.sh" +source "$BUILD_ROOT/helpers/paths.sh" CHECK_ONLY=false SKIP_TIMERS=false @@ -61,10 +63,12 @@ if command -v apt-get >/dev/null 2>&1; then DISTRO="debian" PKG_BSDTAR="libarchive-tools" PKG_DOCKER="docker.io" + PKG_PODMAN="podman" elif command -v pacman >/dev/null 2>&1; then DISTRO="arch" PKG_BSDTAR="libarchive" PKG_DOCKER="docker" + PKG_PODMAN="podman" else print_error "Unsupported distribution — need apt-get or pacman" exit 1 @@ -72,18 +76,36 @@ fi print_info "Distribution: $DISTRO" +# A fresh repository host still defaults to Docker. An explicit Podman choice, +# or a working Podman selected by the shared helper, is left alone. +if [[ -z "$CONTAINER_ENGINE" ]]; then + CONTAINER_ENGINE=docker + export CONTAINER_ENGINE +fi +if ! container_engine_supported; then + print_error "Unsupported CONTAINER_ENGINE: $CONTAINER_ENGINE (use docker or podman)" + exit 1 +fi +if [[ "$CONTAINER_ENGINE" == "docker" ]]; then + PKG_ENGINE="$PKG_DOCKER" + ENGINE_NAME="Docker" +else + PKG_ENGINE="$PKG_PODMAN" + ENGINE_NAME="Podman" +fi + if [[ "$CHECK_ONLY" != true && $EUID -ne 0 ]]; then print_error "Run as root (installing packages and systemd units)" exit 1 fi -# Docker and the release timers are both systemd units. Say so plainly rather -# than failing later on a missing command — a container is the usual way to end -# up here, and it cannot be a repository host. +# The release timers are systemd units. Say so plainly rather than failing +# later on a missing command — a container is the usual way to end up here, +# and it cannot be a repository host. if [[ "$CHECK_ONLY" != true ]] && ! command -v systemctl >/dev/null 2>&1; then print_error "systemctl not found — the repository host must run systemd" echo "" - echo "Docker and the release timers are systemd units. This looks like a" + echo "The release timers are systemd units. This looks like a" echo "container; run setup on the host itself." exit 1 fi @@ -139,24 +161,27 @@ else fi echo "" -# --- docker ------------------------------------------------------------------ +# --- container engine -------------------------------------------------------- -# Docker is left alone when it already works. A host may well be running a +# The selected engine is left alone when it already works. A host may be running a # version from Docker's own repository rather than the distribution's, and # replacing that underneath a working builder would be a poor trade for # tidiness. -print_info "Checking Docker..." +print_info "Checking $ENGINE_NAME..." -if command -v docker >/dev/null 2>&1; then - print_step "docker present: $(docker --version 2>/dev/null | head -1)" - if docker info >/dev/null 2>&1; then - print_success "Docker is installed and running — leaving it alone" +if command -v "$CONTAINER_ENGINE" >/dev/null 2>&1; then + print_step "$CONTAINER_ENGINE present: $("$CONTAINER_ENGINE" --version 2>/dev/null | head -1)" + if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then + print_success "$ENGINE_NAME is installed and available — leaving it alone" elif [[ "$CHECK_ONLY" == true ]]; then - print_warning "Docker is installed but not running; would start it" + print_warning "$ENGINE_NAME is installed but unavailable" + elif [[ "$CONTAINER_ENGINE" == "podman" ]]; then + print_error "Podman is installed but unavailable to the current user" + exit 1 else print_info "Docker is installed but not running — starting it" systemctl enable --now docker.service - if docker info >/dev/null 2>&1; then + if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then print_success "Docker started" else print_error "Docker is installed but still not responding" @@ -165,16 +190,24 @@ if command -v docker >/dev/null 2>&1; then fi fi elif [[ "$CHECK_ONLY" == true ]]; then - print_warning "Would install $PKG_DOCKER and enable it" + if [[ "$CONTAINER_ENGINE" == "docker" ]]; then + print_warning "Would install $PKG_ENGINE and enable it" + else + print_warning "Would install $PKG_ENGINE" + fi else - print_info "Installing $PKG_DOCKER..." - install_packages "$PKG_DOCKER" - systemctl enable --now docker.service - if docker info >/dev/null 2>&1; then - print_success "Docker installed and running" + print_info "Installing $PKG_ENGINE..." + install_packages "$PKG_ENGINE" + if [[ "$CONTAINER_ENGINE" == "docker" ]]; then + systemctl enable --now docker.service + fi + if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then + print_success "$ENGINE_NAME installed and available" else - print_error "Docker installed but not responding" - echo " Check 'systemctl status docker' — builds cannot run without it." + print_error "$ENGINE_NAME installed but not responding" + if [[ "$CONTAINER_ENGINE" == "docker" ]]; then + echo " Check 'systemctl status docker' — builds cannot run without it." + fi exit 1 fi fi @@ -237,6 +270,10 @@ echo "" # --- release timers ---------------------------------------------------------- +print_info "Published architectures: $(published_arches | tr '\n' ' ')" +echo " (PUBLISHED_ARCHES in helpers/paths.sh; OMARCHY_ARCHES overrides a one-off command)" +echo "" + TIMERS=(omarchy-check-versions omarchy-auto-release-edge omarchy-auto-release-rc omarchy-auto-release-stable) if [[ "$SKIP_TIMERS" == true ]]; then diff --git a/bin/sign b/bin/sign index 70779e8c..e1e99cf3 100755 --- a/bin/sign +++ b/bin/sign @@ -52,8 +52,8 @@ if [[ ! -d "$BUILD_OUTPUT_DIR" ]]; then exit 1 fi -# Check Docker is available -check_docker +# Check the selected container engine is available +check_engine # Check GPG credentials are in environment if [[ -z "$GPG_PRIVATE_KEY" ]]; then @@ -66,23 +66,30 @@ if [[ -z "$GPG_PASSPHRASE" ]]; then exit 1 fi -# Build/update the Docker image (always use x86_64 for signing - it's architecture independent) -build_docker_image "$BUILD_DIR" "x86_64" "$MIRROR" +# Signing is architecture-independent, so run its utility container natively +# on either an x86_64 or ARM host. +TOOL_ARCH=$(docker_native_arch) || { + print_error "Unsupported host architecture: $(uname -m)" + exit 1 +} +build_docker_image "$BUILD_DIR" "$TOOL_ARCH" "$MIRROR" print_info "Running package signing..." -# Ensure output directory is writable by container user -make_dir_writable "$BUILD_OUTPUT_DIR" +# Rootless Podman uses keep-id and leaves host ownership/modes intact. +if [[ "$CONTAINER_ENGINE" == "docker" ]]; then + make_dir_writable "$BUILD_OUTPUT_DIR" +fi -# Run the signing script in Docker (always use x86_64 image) -docker run --rm --platform linux/amd64 \ +# Run the signing script in the host-native image. +"$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$TOOL_ARCH")" \ -e ARCH="$ARCH" \ -e MIRROR="$MIRROR" \ -e GPG_PRIVATE_KEY="$GPG_PRIVATE_KEY" \ -e GPG_PASSPHRASE="$GPG_PASSPHRASE" \ -v "$BUILD_ROOT/build-output:/build-output" \ -v "$BUILD_DIR:/build:ro" \ - omarchy-pkg-builder:latest-x86_64-$MIRROR /build/sign.sh + "omarchy-pkg-builder:latest-$TOOL_ARCH-$MIRROR" /build/sign.sh SIGN_RESULT=$? diff --git a/bin/sync-rebuilds b/bin/sync-rebuilds index 7a0336b4..b9aa81d1 100755 --- a/bin/sync-rebuilds +++ b/bin/sync-rebuilds @@ -17,11 +17,14 @@ SELF_TEST=false # rebuilding for it would ship a package built against the wrong ABI. OFFICIAL_REPOS=" core extra multilib core-debug extra-debug " -# The published repository, used as the floor a bumped pkgrel has to clear. -# Only x86_64 is published today; aarch64 has no repository to compare against. +# The published repositories are the floor a bumped pkgrel has to clear. PUBLISHED_BASE_URL="${OMARCHY_PUBLISHED_BASE_URL:-https://pkgs.omarchy.org}" PUBLISHED_MIRRORS=(edge stable) -PUBLISHED_ARCH=x86_64 + +# Arch Linux ARM has no dated snapshots. This is the same live repository the +# aarch64 builder resolves; override it only when the builder mirror changes. +ALARM_BASE_URL="${OMARCHY_ALARM_BASE_URL:-https://fl.us.mirror.archlinuxarm.org/aarch64}" +ALARM_REPOS=(core extra alarm aur) usage() { cat </dev/null; then + print_error "Could not read the aarch64 $repo repository database" + return 1 + fi + if ! tar -tf "$db" >/dev/null 2>&1; then + print_error "Unreadable aarch64 $repo repository database" + return 1 + fi + + tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk ' + function emit() { + if (name != "" && version != "") { + print name "\t" version + if (base != "" && base != name) print base "\t" version + } + name=""; base=""; version="" + } + $0 == "%FILENAME%" { emit(); next } + $0 == "%NAME%" { if (name != "" && version != "") emit(); getline; name=$0; next } + $0 == "%BASE%" { getline; base=$0; next } + $0 == "%VERSION%" { getline; version=$0; next } + END { emit() } + ' >"$index" +} + +alarm_repo_version() { + local package="$1" repo version + for repo in "${ALARM_REPOS[@]}"; do + load_alarm_repo "$repo" || return 1 + version=$(awk -F '\t' -v package="$package" '$1 == package { print $2; exit }' "$TEMP_DIR/alarm-$repo.index") + if [[ -n "$version" ]]; then + echo "$version" + return 0 + fi + done +} + +repo_version() { # repo_version + case "$1" in + x86_64) native_repo_version "$2" ;; + aarch64) alarm_repo_version "$2" ;; + *) return 1 ;; + esac +} + declare -A PUBLISHED_VERSION=() PUBLISHED_LOADED=false -PUBLISHED_AVAILABLE=true remember_published() { local name="$1" @@ -131,34 +188,35 @@ load_published_versions() { [[ "$PUBLISHED_LOADED" == true ]] && return 0 PUBLISHED_LOADED=true - local mirror db name base version + local arch mirror db name base version - for mirror in "${PUBLISHED_MIRRORS[@]}"; do - db="$TEMP_DIR/published-$mirror.db.tar.zst" + for arch in $(published_arches); do + for mirror in "${PUBLISHED_MIRRORS[@]}"; do + db="$TEMP_DIR/published-$mirror-$arch.db.tar.zst" - if ! curl -fsSL --max-time 120 -o "$db" \ - "$PUBLISHED_BASE_URL/$mirror/$PUBLISHED_ARCH/omarchy.db.tar.zst" 2>/dev/null; then - print_warning "Could not read the published $mirror database; bumps are not checked against it this run" - PUBLISHED_AVAILABLE=false - continue - fi + if ! curl -fsSL --max-time 120 -o "$db" \ + "$PUBLISHED_BASE_URL/$mirror/$arch/omarchy.db.tar.zst" 2>/dev/null; then + print_warning "Could not read the published $mirror/$arch database; bumps are not checked against it this run" + continue + fi - while IFS=$'\t' read -r name base version; do - [[ -n "$name" && -n "$version" ]] && remember_published "$name" "$version" - [[ -n "$base" && -n "$version" ]] && remember_published "$base" "$version" - done < <( - tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk ' - function emit() { - if (name != "" && version != "") print name "\t" base "\t" version - name=""; base=""; version="" - } - $0 == "%FILENAME%" { emit(); next } - $0 == "%NAME%" { if (name != "" && version != "") emit(); getline; name=$0; next } - $0 == "%BASE%" { getline; base=$0; next } - $0 == "%VERSION%" { getline; version=$0; next } - END { emit() } - ' - ) + while IFS=$'\t' read -r name base version; do + [[ -n "$name" && -n "$version" ]] && remember_published "$name" "$version" + [[ -n "$base" && -n "$version" ]] && remember_published "$base" "$version" + done < <( + tar -xOf "$db" --wildcards '*/desc' 2>/dev/null | awk ' + function emit() { + if (name != "" && version != "") print name "\t" base "\t" version + name=""; base=""; version="" + } + $0 == "%FILENAME%" { emit(); next } + $0 == "%NAME%" { if (name != "" && version != "") emit(); getline; name=$0; next } + $0 == "%BASE%" { getline; base=$0; next } + $0 == "%VERSION%" { getline; version=$0; next } + END { emit() } + ' + ) + done done } @@ -252,7 +310,17 @@ record_triggers() { local package_dir="$1" local current="$2" - write_metadata "$package_dir" '.rebuilt_against = $current' --argjson current "$current" + # A flat record is the legacy x86_64 shape. Preserve records for + # architectures outside this run, then replace the ones just rebuilt. + write_metadata "$package_dir" ' + (.rebuilt_against // {}) as $old | + (if ($old | length) == 0 then {} + elif ($old | to_entries | all(.value | type == "string")) + then {x86_64: $old} + else $old + end) as $by_arch | + .rebuilt_against = ($by_arch * $current) + ' --argjson current "$current" } # Metadata that does not parse would otherwise drop its package out of the run @@ -300,21 +368,41 @@ sync_package() { print_info "Checking $package against ${triggers[*]}..." - local current="{}" trigger version - for trigger in "${triggers[@]}"; do - version=$(repo_version "$trigger") - if [[ -z "$version" ]]; then - print_error " $trigger is in no official repository; leaving $package alone" - ((++FAILED)) - return 0 - fi - if ! current=$(jq -c --arg name "$trigger" --arg version "$version" '.[$name] = $version' <<<"$current"); then - print_error " Could not record $trigger $version for $package" + local current="{}" arch arch_current trigger version considered=0 + for arch in $(published_arches); do + package_supports_arch "$package_dir" "$arch" || continue + considered=$((considered + 1)) + arch_current="{}" + for trigger in "${triggers[@]}"; do + if ! version=$(repo_version "$arch" "$trigger"); then + print_error " Could not read $arch repository versions; leaving $package alone" + ((++FAILED)) + return 0 + fi + if [[ -z "$version" ]]; then + print_error " $trigger is in no $arch repository; leaving $package alone" + ((++FAILED)) + return 0 + fi + if ! arch_current=$(jq -c --arg name "$trigger" --arg version "$version" '.[$name] = $version' <<<"$arch_current"); then + print_error " Could not record $arch/$trigger $version for $package" + ((++FAILED)) + return 0 + fi + done + if ! current=$(jq -c --arg arch "$arch" --argjson versions "$arch_current" '.[$arch] = $versions' <<<"$current"); then + print_error " Could not record $arch trigger versions for $package" ((++FAILED)) return 0 fi done + if ((considered == 0)); then + print_info " Skipping: not built for any published architecture" + ((++SKIPPED)) + return 0 + fi + local recorded if ! recorded=$(package_metadata_value "$package_dir" '.rebuilt_against' ""); then print_error " Could not read .omarchy/package.json for $package" @@ -323,13 +411,20 @@ sync_package() { fi [[ -n "$recorded" && "$recorded" != "null" ]] || recorded="{}" + # Before architecture-specific records existed, rebuilt_against described + # x86_64. Read it that way without forcing a metadata-only migration. + if jq -e 'to_entries | all(.value | type == "string")' >/dev/null <<<"$recorded"; then + recorded=$(jq -c '{x86_64: .}' <<<"$recorded") + fi + # Walk the declared triggers rather than the record, so a name the record does # not carry reads as changed instead of going unexamined forever. local moved if ! moved=$(jq -r --argjson recorded "$recorded" ' - to_entries - | map(select($recorded[.key] != .value) - | "\(.key) \($recorded[.key] // "unrecorded") -> \(.value)") + [to_entries[] as $arch + | $arch.value | to_entries[] as $trigger + | select($recorded[$arch.key][$trigger.key] != $trigger.value) + | "\($arch.key)/\($trigger.key) \($recorded[$arch.key][$trigger.key] // "unrecorded") -> \($trigger.value)"] | join(", ") ' <<<"$current"); then print_error " Could not compare recorded trigger versions for $package" @@ -338,7 +433,7 @@ sync_package() { fi if [[ -z "$moved" ]]; then - print_info " Already rebuilt against $(jq -r 'to_entries | map("\(.key) \(.value)") | join(", ")' <<<"$current")" + print_info " Already rebuilt against every published architecture" ((++SKIPPED)) return 0 fi @@ -466,11 +561,11 @@ selftest_root() { } selftest_package() { - local root="$1" name="$2" pkgrel="$3" metadata="$4" pkgver="${5:-1.0}" + local root="$1" name="$2" pkgrel="$3" metadata="$4" pkgver="${5:-1.0}" arches="${6:-x86_64}" local dir="$root/pkgbuilds/$name" mkdir -p "$dir/.omarchy" - printf 'pkgname=%s\npkgver=%s\npkgrel=%s\narch=(x86_64)\n' "$name" "$pkgver" "$pkgrel" > "$dir/PKGBUILD" + printf 'pkgname=%s\npkgver=%s\npkgrel=%s\narch=(%s)\n' "$name" "$pkgver" "$pkgrel" "$arches" > "$dir/PKGBUILD" printf '%s\n' "$metadata" > "$dir/.omarchy/package.json" } @@ -514,19 +609,51 @@ selftest_published() { cat > "$root/stub/curl" <<'STUB' #!/bin/bash out="" +url="" while [[ $# -gt 0 ]]; do case "$1" in -o) out="$2"; shift 2 ;; - *) shift ;; + *) url="$1"; shift ;; esac done -db="$(dirname "$0")/omarchy.db.tar.zst" +if [[ "$url" == */aarch64/* ]]; then + repo="${url%/*}" + repo="${repo##*/}" + db="$(dirname "$0")/alarm-$repo.db" +else + db="$(dirname "$0")/omarchy.db.tar.zst" +fi [[ -f "$db" && -n "$out" ]] || exit 22 cp "$db" "$out" STUB chmod +x "$root/stub/curl" } +selftest_alarm() { + local root="$1" + shift + local repo staging="$root/stub/alarm-db" entry name version + + for repo in core extra; do + rm -rf "$staging" + mkdir -p "$staging" + if [[ "$repo" == "core" ]]; then + mkdir -p "$staging/unrelated-1-1" + printf '%%FILENAME%%\nunrelated-1-1-aarch64.pkg.tar.zst\n\n%%NAME%%\nunrelated\n\n%%BASE%%\nunrelated\n\n%%VERSION%%\n1-1\n' \ + > "$staging/unrelated-1-1/desc" + else + for entry in "$@"; do + name="${entry%=*}" + version="${entry#*=}" + mkdir -p "$staging/$name-$version" + printf '%%FILENAME%%\n%s-%s-aarch64.pkg.tar.zst\n\n%%NAME%%\n%s\n\n%%BASE%%\n%s\n\n%%VERSION%%\n%s\n' \ + "$name" "$version" "$name" "$name" "$version" > "$staging/$name-$version/desc" + done + fi + tar -czf "$root/stub/alarm-$repo.db" -C "$staging" . + done +} + cmd_self_test() { local failures=0 local root @@ -545,7 +672,8 @@ cmd_self_test() { local root="$1" shift local status=0 - PATH="$root/stub:$PATH" "$root/bin/sync-rebuilds" "$@" > "$root/output" 2>&1 || status=$? + OMARCHY_ARCHES="${SELFTEST_ARCHES:-x86_64}" \ + PATH="$root/stub:$PATH" "$root/bin/sync-rebuilds" "$@" > "$root/output" 2>&1 || status=$? echo "$status" } @@ -563,7 +691,7 @@ cmd_self_test() { check "run succeeds" 0 "$(run_case "$root")" check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-partial")" check "unrecorded trigger now recorded" "2-2" \ - "$(jq -r '.rebuilt_against["dep-b"]' "$root/pkgbuilds/t-partial/.omarchy/package.json")" + "$(jq -r '.rebuilt_against.x86_64["dep-b"]' "$root/pkgbuilds/t-partial/.omarchy/package.json")" echo "Opting a package in buys a rebuild rather than a bare record:" root=$(selftest_root fresh) @@ -573,7 +701,7 @@ cmd_self_test() { check "run succeeds" 0 "$(run_case "$root")" check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-fresh")" check "trigger recorded" "1-1" \ - "$(jq -r '.rebuilt_against["dep-a"]' "$root/pkgbuilds/t-fresh/.omarchy/package.json")" + "$(jq -r '.rebuilt_against.x86_64["dep-a"]' "$root/pkgbuilds/t-fresh/.omarchy/package.json")" echo "An unchanged package is left alone:" root=$(selftest_root current) @@ -612,6 +740,47 @@ cmd_self_test() { check "suffix recorded for the next AUR sync" 1 \ "$(jq -r '.pkgrel.suffix' "$root/pkgbuilds/t-aur/.omarchy/package.json")" + echo "A dependency is tracked independently for both published architectures:" + root=$(selftest_root multiarch) + selftest_package "$root" t-multi 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"dep-a":"1-1"}}' 1.0 'x86_64 aarch64' + selftest_pacman "$root" dep-a=1-1 + selftest_published "$root" + selftest_alarm "$root" dep-a=2-1 + SELFTEST_ARCHES="x86_64 aarch64" + check "run succeeds" 0 "$(run_case "$root")" + unset SELFTEST_ARCHES + check "pkgrel bumped once" 2 "$(pkgrel_of "$root/pkgbuilds/t-multi")" + check "x86_64 trigger recorded" "1-1" \ + "$(jq -r '.rebuilt_against.x86_64["dep-a"]' "$root/pkgbuilds/t-multi/.omarchy/package.json")" + check "aarch64 trigger recorded" "2-1" \ + "$(jq -r '.rebuilt_against.aarch64["dep-a"]' "$root/pkgbuilds/t-multi/.omarchy/package.json")" + + echo "An ARM-only run records only the ARM dependency state:" + root=$(selftest_root arm-only) + selftest_package "$root" t-arm 1 '{"source":"local","rebuild_on":["dep-a"]}' 1.0 'x86_64 aarch64' + selftest_pacman "$root" + selftest_published "$root" + selftest_alarm "$root" dep-a=2-1 + SELFTEST_ARCHES=aarch64 + check "run succeeds" 0 "$(run_case "$root")" + unset SELFTEST_ARCHES + check "pkgrel bumped" 2 "$(pkgrel_of "$root/pkgbuilds/t-arm")" + check "ARM trigger recorded" "2-1" \ + "$(jq -r '.rebuilt_against.aarch64["dep-a"]' "$root/pkgbuilds/t-arm/.omarchy/package.json")" + check "x86_64 was not consulted" "false" \ + "$(jq -r '.rebuilt_against | has("x86_64")' "$root/pkgbuilds/t-arm/.omarchy/package.json")" + + echo "An x86-only package ignores ARM during a dual-architecture run:" + root=$(selftest_root x86-package) + selftest_package "$root" t-x86 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"dep-a":"1-1"}}' + selftest_pacman "$root" dep-a=1-1 + selftest_published "$root" + selftest_alarm "$root" dep-a=2-1 + SELFTEST_ARCHES="x86_64 aarch64" + check "run succeeds" 0 "$(run_case "$root")" + unset SELFTEST_ARCHES + check "pkgrel untouched" 1 "$(pkgrel_of "$root/pkgbuilds/t-x86")" + echo "" if [[ "$failures" -eq 0 ]]; then print_success "Self-test passed" @@ -626,9 +795,9 @@ if [[ "$SELF_TEST" == true ]]; then exit $? fi -for tool in pacman vercmp jq curl; do +for tool in pacman vercmp jq curl tar; do if ! command -v "$tool" >/dev/null 2>&1; then - print_error "$tool not found: reading trigger versions and ordering pkgrels both need pacman" + print_error "$tool not found: rebuild trigger sync cannot run" exit 1 fi done diff --git a/bin/sync-upstream b/bin/sync-upstream index 04c7ac62..8b2864d2 100755 --- a/bin/sync-upstream +++ b/bin/sync-upstream @@ -20,8 +20,8 @@ Update packages that track an upstream vendor release feed instead of the AUR. Packages opt in declaratively through "upstream" in .omarchy/package.json. Providers cover GitHub Releases with checksum manifests or API asset digests, -semver-shaped git tags whose source URLs can be hashed, and npm dist-tags. See README.md for the -schemas. Anything outside those conventions may provide +semver-shaped git tags, npm dist-tags, and plain Debian Packages indexes. See +README.md for the schemas. Anything outside those conventions may provide pkgbuilds//.omarchy/upstream.sh, a hook that reports JSON on stdout: { @@ -378,6 +378,7 @@ sync_package() { github) release=$(github_upstream_release "$package_dir" "$min_age") || release_status=$? ;; git_tags) release=$(git_tags_upstream_release "$package_dir") || release_status=$? ;; npm) release=$(npm_upstream_release "$package_dir") || release_status=$? ;; + debian) release=$(debian_upstream_release "$package_dir") || release_status=$? ;; esac if [[ ${release_status:-0} -ne 0 ]]; then print_error "$provider upstream provider failed for $package" @@ -544,6 +545,31 @@ EOF check "bypass lifts the quarantine" "2.0.0" "$(jq -r '.pkgver // ""' <<<"$out")" check "x86_64 checksum via * binary-mode prefix" "$sum_x20" "$(jq -r '.sha256sums.x86_64[0] // ""' <<<"$out")" + cp "$pkg/.omarchy/package.json" "$pkg/normal.json" + jq '.upstream.latest_only = true' "$pkg/normal.json" > "$pkg/.omarchy/package.json" + FIXTURE_RELEASES=$(jq -n --arg old "$old2d" '[ + {tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false}, + {tag_name: "v0.4.1-8", published_at: $old, draft: false, prerelease: false} + ]') + FIXTURE_CHECKSUMS=$(printf '%s\n' \ + "$sum_x19 ./tool-v1.9.0-x64.tar.xz" \ + "$sum_a19 ./tool-v1.9.0-arm64.tar.xz") + out=$(github_upstream_release "$pkg" 0 2>/dev/null) || out="" + check "latest_only ignores incompatible historical tags" "1.9.0" "$(jq -r '.pkgver // ""' <<<"$out")" + cp "$pkg/normal.json" "$pkg/.omarchy/package.json" + FIXTURE_RELEASES=$(jq -n --arg young "$young" --arg old2 "$old2d" --arg old3 "$old3d" '[ + {tag_name: "v2.0.0", published_at: $young, draft: false, prerelease: false}, + {tag_name: "v1.9.9", published_at: $old2, draft: false, prerelease: true}, + {tag_name: "v1.9.8", published_at: $old2, draft: true, prerelease: false}, + {tag_name: "v1.9.0", published_at: $old2, draft: false, prerelease: false}, + {tag_name: "v1.8.0", published_at: $old3, draft: false, prerelease: false} + ]') + FIXTURE_CHECKSUMS=$(printf '%s\n' \ + "$sum_x19 ./tool-v1.9.0-x64.tar.xz" \ + "$sum_a19 tool-v1.9.0-arm64.tar.xz" \ + "$sum_x20 *tool-v2.0.0-x64.tar.xz" \ + "$sum_a20 tool-v2.0.0-arm64.tar.xz") + out=$(github_upstream_release "$pkg" 8640000 2>/dev/null) || out="" check "everything quarantined reports no update" "{}" "$(jq -c . <<<"$out")" @@ -637,6 +663,117 @@ EOF check "provider rejects a non-boolean digests" "1" "$rc" cp "$digpkg/good.json" "$digpkg/.omarchy/package.json" + echo "Ordered GitHub assets with supplemental sources:" + local multipkg="$TEMP_DIR/selftest-multi-assets" multi_sum support_sum multi_vst + mkdir -p "$multipkg/.omarchy" + printf 'pkgver=1.0.0\npkgrel=1\n' > "$multipkg/PKGBUILD" + cat > "$multipkg/.omarchy/package.json" <<'EOF' +{ + "source": "local", + "upstream": { + "github": "example/tool", + "digests": true, + "assets": { + "x86_64": ["tool-{pkgver}-x86_64", "tool-{pkgver}-x86_64.asc"], + "aarch64": ["tool-{pkgver}-aarch64", "tool-{pkgver}-aarch64.asc"] + }, + "sources": { + "any": ["https://example.test/tool/{tag}/support.txt"] + } + } +} +EOF + local sum_x19_sig sum_a19_sig + sum_x19_sig=$(printf '1%.0s' {1..64}) + sum_a19_sig=$(printf '2%.0s' {1..64}) + FIXTURE_RELEASES=$(jq -n --arg old "$old2d" --arg x "$sum_x19" --arg xs "$sum_x19_sig" \ + --arg a "$sum_a19" --arg as "$sum_a19_sig" '[ + {tag_name: "v1.9.0", published_at: $old, draft: false, prerelease: false, assets: [ + {name: "tool-1.9.0-x86_64", digest: ("sha256:" + $x)}, + {name: "tool-1.9.0-x86_64.asc", digest: ("sha256:" + $xs)}, + {name: "tool-1.9.0-aarch64", digest: ("sha256:" + $a)}, + {name: "tool-1.9.0-aarch64.asc", digest: ("sha256:" + $as)} + ]} + ]') + upstream_fetch_source() { printf 'remote fixture for %s\n' "$1" > "$2"; } + multi_sum=$(github_upstream_release "$multipkg" 0 2>/dev/null) || multi_sum="" + support_sum=$(printf 'remote fixture for %s\n' 'https://example.test/tool/v1.9.0/support.txt' | sha256sum | cut -d' ' -f1) + check "ordered GitHub assets produce an ordered checksum array" "$sum_x19 $sum_x19_sig" \ + "$(jq -r '.sha256sums.x86_64 | join(" ")' <<<"$multi_sum")" + check "GitHub supplemental source is downloaded and hashed" "$support_sum" \ + "$(jq -r '.sha256sums.any[0]' <<<"$multi_sum")" + multi_vst=0; validate_package_metadata "$multipkg" >/dev/null || multi_vst=$? + check "GitHub asset lists and disjoint sources validate" "0" "$multi_vst" + jq '.upstream.sources.x86_64 = ["https://example.test/duplicate"]' \ + "$multipkg/.omarchy/package.json" > "$multipkg/overlap.json" + cp "$multipkg/.omarchy/package.json" "$multipkg/good.json" + cp "$multipkg/overlap.json" "$multipkg/.omarchy/package.json" + multi_vst=0; validate_package_metadata "$multipkg" >/dev/null || multi_vst=$? + check "GitHub assets and sources cannot target the same checksum array" "1" "$multi_vst" + cp "$multipkg/good.json" "$multipkg/.omarchy/package.json" + + echo "Debian Packages provider:" + local debpkg="$TEMP_DIR/selftest-debian" deb_sum deb_x_sum deb_a_sum deb_vst + mkdir -p "$debpkg/.omarchy" + printf 'pkgver=1.0.0\npkgrel=1\nsha256sums_x86_64=("old")\nsha256sums_aarch64=("old")\n' > "$debpkg/PKGBUILD" + cat > "$debpkg/.omarchy/package.json" <<'EOF' +{ + "source": "local", + "upstream": { + "debian": "https://packages.example.test/dists/stable/main/binary-amd64/Packages", + "package": "example-app", + "sources": { + "x86_64": ["https://downloads.example.test/app-{pkgver}-x64.tar.gz"], + "aarch64": ["https://downloads.example.test/app-{pkgver}-arm64.tar.gz"] + } + } +} +EOF + debian_fetch_packages() { + cat <<'EOF' +Package: unrelated +Version: 99.0.0 + +Package: example-app +Version: 1.9.0 + +Package: example-app +Version: 1.10.0 +EOF + } + deb_sum=$(debian_upstream_release "$debpkg") + deb_x_sum=$(printf 'remote fixture for %s\n' 'https://downloads.example.test/app-1.10.0-x64.tar.gz' | sha256sum | cut -d' ' -f1) + deb_a_sum=$(printf 'remote fixture for %s\n' 'https://downloads.example.test/app-1.10.0-arm64.tar.gz' | sha256sum | cut -d' ' -f1) + check "Debian provider selects the newest exact package stanza" "1.10.0" "$(jq -r '.pkgver' <<<"$deb_sum")" + check "Debian x86_64 source is hashed" "$deb_x_sum" "$(jq -r '.sha256sums.x86_64[0]' <<<"$deb_sum")" + check "Debian aarch64 source is hashed" "$deb_a_sum" "$(jq -r '.sha256sums.aarch64[0]' <<<"$deb_sum")" + deb_vst=0; validate_package_metadata "$debpkg" >/dev/null || deb_vst=$? + check "Debian declaration validates" "0" "$deb_vst" + printf 'pkgver=1.10.0\npkgrel=1\nsha256sums_x86_64=("old")\nsha256sums_aarch64=("old")\n' > "$debpkg/PKGBUILD" + check "checked-in Debian version avoids source downloads" "{}" "$(debian_upstream_release "$debpkg" | jq -c .)" + + echo "End-to-end Debian sync with the checked-in 1password recipe:" + local one_root="$TEMP_DIR/e2e-1password" one_dir one_version=8.12.35 + mkdir -p "$one_root" + cp -a "$BUILD_ROOT/pkgbuilds/1password" "$one_root/1password" + one_dir="$one_root/1password" + debian_fetch_packages() { + printf 'Package: 1password\nVersion: %s\n' "$one_version" + } + upstream_fetch_source() { printf 'remote fixture for %s\n' "$1" > "$2"; } + local one_prev_updated=$UPDATED one_prev_failed=$FAILED + PKGBUILDS_DIR="$one_root" sync_package 1password >/dev/null 2>&1 || true + check "1password sync updates without failures" "updated=1 failed=0" \ + "updated=$((UPDATED - one_prev_updated)) failed=$((FAILED - one_prev_failed))" + check "1password pkgver is the single download version source" "$one_version" \ + "$(grep -m1 '^pkgver=' "$one_dir/PKGBUILD" | cut -d= -f2-)" + check "1password pkgrel resets to 1" "1" \ + "$(grep -m1 '^pkgrel=' "$one_dir/PKGBUILD" | cut -d= -f2-)" + check "1password x86 URL follows the rewritten pkgver" "1password-${one_version}.x64.tar.gz" \ + "$(CARCH=x86_64 bash -c 'source "$1"; basename "${source_x86_64[0]}"' _ "$one_dir/PKGBUILD")" + check "1password ARM URL follows the rewritten pkgver" "1password-${one_version}.arm64.tar.gz" \ + "$(CARCH=aarch64 bash -c 'source "$1"; basename "${source_aarch64[0]}"' _ "$one_dir/PKGBUILD")" + echo "Quarantine backstop:" local rel st rel=$(jq -n --arg p "$old2d" '{pkgver: "1.9.0", published_at: $p, sha256sums: {}}') diff --git a/bin/timers b/bin/timers index 619a821e..4b39a292 100755 --- a/bin/timers +++ b/bin/timers @@ -84,15 +84,18 @@ echo "" # --- queued work ------------------------------------------------------------- -print_info "Queued builds (state files in $STATE_DIR)" +print_info "Queued builds (state files in $STATE_DIR; architectures: $(published_arches | tr '\n' ' '))" queued=false for channel in edge rc stable; do - state_file="$STATE_DIR/.sync-needed-$channel" - if [[ -f "$state_file" ]]; then - queued=true - since=$(date -r "$state_file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown") - printf ' • %-7s queued since %s\n' "$channel" "$since" - fi + for arch in $(published_arches); do + state_file=$(sync_queue_file "$channel" "$arch") + [[ "$arch" == "x86_64" && ! -f "$state_file" ]] && state_file=$(legacy_sync_queue_file "$channel") + if [[ -f "$state_file" ]]; then + queued=true + since=$(date -r "$state_file" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "unknown") + printf ' • %-7s %-8s queued since %s\n' "$channel" "$arch" "$since" + fi + done done [[ "$queued" == false ]] && echo " (nothing queued — all channels up to date)" echo "" @@ -101,34 +104,37 @@ echo "" # say so plainly: it is queued but deliberately not being retried yet. paused=false for channel in edge rc stable; do - fail_file="$STATE_DIR/.build-failed-$channel" - [[ -f "$fail_file" ]] || continue - if [[ "$paused" == false ]]; then - print_error "Failing builds (backoff active)" - paused=true - fi - FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT="" - # shellcheck disable=SC1090 - source "$fail_file" 2>/dev/null || true - delay=600 - for ((i = 1; i < FAILURE_COUNT; i++)); do - delay=$((delay * 2)) - ((delay >= 21600)) && { delay=21600; break; } + for arch in $(published_arches); do + fail_file=$(sync_fail_file "$channel" "$arch") + [[ "$arch" == "x86_64" && ! -f "$fail_file" ]] && fail_file=$(legacy_sync_fail_file "$channel") + [[ -f "$fail_file" ]] || continue + if [[ "$paused" == false ]]; then + print_error "Failing builds (backoff active)" + paused=true + fi + FAILURE_COUNT=0 FAILURE_AT=0 FAILURE_FINGERPRINT="" + # shellcheck disable=SC1090 + source "$fail_file" 2>/dev/null || true + delay=600 + for ((i = 1; i < FAILURE_COUNT; i++)); do + delay=$((delay * 2)) + ((delay >= 21600)) && { delay=21600; break; } + done + retry_at=$((FAILURE_AT + delay)) + now=$(date +%s) + if ((now < retry_at)); then + when="retries at $(date -d "@$retry_at" '+%H:%M:%S' 2>/dev/null || echo "+$((retry_at - now))s")" + else + when="retries on the next tick" + fi + printf ' ✗ %-7s %-8s %s consecutive failure(s), %s\n' "$channel" "$arch" "$FAILURE_COUNT" "$when" + printf ' last attempt %s on commit %s\n' \ + "$(date -d "@$FAILURE_AT" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$FAILURE_AT")" \ + "${FAILURE_FINGERPRINT:0:12}" done - retry_at=$((FAILURE_AT + delay)) - now=$(date +%s) - if ((now < retry_at)); then - when="retries at $(date -d "@$retry_at" '+%H:%M:%S' 2>/dev/null || echo "+$((retry_at - now))s")" - else - when="retries on the next tick" - fi - printf ' ✗ %-7s %s consecutive failure(s), %s\n' "$channel" "$FAILURE_COUNT" "$when" - printf ' last attempt %s on commit %s\n' \ - "$(date -d "@$FAILURE_AT" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$FAILURE_AT")" \ - "${FAILURE_FINGERPRINT:0:12}" done if [[ "$paused" == true ]]; then - echo " Any new commit clears the backoff; or: rm $STATE_DIR/.build-failed-" + echo " Any new commit clears the backoff; or: rm $STATE_DIR/.build-failed--" echo "" fi diff --git a/bin/update-repo b/bin/update-repo index 34d9f512..2fda5493 100755 --- a/bin/update-repo +++ b/bin/update-repo @@ -11,12 +11,11 @@ source "$BUILD_ROOT/helpers/docker-helpers.sh" source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/lock-helpers.sh" -# Function to update repository database using Docker +# Function to update repository database using the selected container engine update_database() { - print_info "Updating repository database in Docker container..." + print_info "Updating repository database in a container..." - # Check Docker is available - check_docker + check_engine # Ensure output directory exists if [[ ! -d "$REPO_DIR" ]]; then @@ -25,20 +24,25 @@ update_database() { exit 1 fi - # Make output directory writable for container - make_dir_writable "$REPO_DIR" + if [[ "$CONTAINER_ENGINE" == "docker" ]]; then + make_dir_writable "$REPO_DIR" + fi - # repo-add is architecture- and mirror-independent, so always use the edge - # x86_64 image. This also lets bootstrap-rc build the rc database before the - # rc channel exists remotely (an rc image can only build after it does). - build_docker_image "$BUILD_DIR" "x86_64" "edge" + # repo-add is architecture- and mirror-independent. Use the host-native edge + # image, which also lets bootstrap-rc run before that channel exists remotely. + local tool_arch + tool_arch=$(docker_native_arch) || { + print_error "Unsupported host architecture: $(uname -m)" + exit 1 + } + build_docker_image "$BUILD_DIR" "$tool_arch" "edge" - docker run --rm --platform linux/amd64 \ + "$CONTAINER_ENGINE" run "${CONTAINER_RUN_ARGS[@]}" --rm "$(get_platform_arg "$tool_arch")" \ -e ARCH="$ARCH" \ -e MIRROR="$MIRROR" \ -v "$REPO_ROOT:/output" \ -v "$BUILD_DIR:/build:ro" \ - omarchy-pkg-builder:latest-x86_64-edge /build/update-repo.sh + "omarchy-pkg-builder:latest-$tool_arch-edge" /build/update-repo.sh } # Main execution diff --git a/build/Dockerfile b/build/Dockerfile index fc1e66b7..af1bb5e9 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -45,9 +45,7 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then \ printf 'Server = https://mirror.omarchy.org/$repo/os/$arch\n' > /etc/pacman.d/mirrorlist; \ fi; \ else \ - curl -L "https://raw.githubusercontent.com/archlinuxarm/PKGBUILDs/master/core/pacman-mirrorlist/mirrorlist" 2>/dev/null | \ - sed -E 's/^\s*#\s*Server\s*=/Server =/g' > /etc/pacman.d/mirrorlist && \ - sed -i 's/\$arch/aarch64/g' /etc/pacman.d/mirrorlist; \ + printf 'Server = https://fl.us.mirror.archlinuxarm.org/aarch64/$repo\n' > /etc/pacman.d/mirrorlist; \ fi # Bootstrap keyrings (required before pacstrap can verify packages) @@ -108,11 +106,16 @@ RUN ln -sf /usr/lib/os-release /etc/os-release && \ # Setup Omarchy keyring manually before adding repo (avoids keyserver trust issues) # Note: Repository is removed at the end since build scripts add it dynamically. # The keyring comes from this image's own channel (the bare /$arch path is a -# stale legacy layout); %s keeps pacman's $arch literal while MIRROR expands. +# stale legacy layout). It is always taken from the x86_64 tree, whatever the +# image's own architecture: omarchy-keyring is an arch=any package, and the +# x86_64 tree is the one that exists before a new architecture has published +# anything. Bootstrapping from the target's own tree would make the first +# aarch64 build depend on an aarch64 repository that only that build can +# create. ARG MIRROR=edge RUN pacman-key --recv-keys 40DFB630FF42BCFFB047046CF0134EE680CAC571 --keyserver keys.openpgp.org && \ pacman-key --lsign-key 40DFB630FF42BCFFB047046CF0134EE680CAC571 && \ - printf '\n[omarchy]\nSigLevel = Optional\nServer = https://pkgs.omarchy.org/%s/$arch\n' "${MIRROR}" >> /etc/pacman.conf && \ + printf '\n[omarchy]\nSigLevel = Optional\nServer = https://pkgs.omarchy.org/%s/x86_64\n' "${MIRROR}" >> /etc/pacman.conf && \ pacman -Sy --noconfirm && \ pacman -S --noconfirm omarchy-keyring && \ pacman-key --populate omarchy && \ @@ -137,8 +140,13 @@ RUN useradd -m -G wheel -s /bin/bash builder && \ chmod 700 /home/builder/.gnupg && \ chown -R builder:builder /home/builder +# Arch Linux ARM's makepkg.conf still defaults PKGEXT to .pkg.tar.xz; every +# tool downstream of the build (sign.sh, push-build, sync-rebuilds, the +# notifier) expects .pkg.tar.zst, so an aarch64 package would build and then +# be skipped at signing. Pin the extension so both architectures match. RUN sed -i 's/^#MAKEFLAGS=.*/MAKEFLAGS="-j$(nproc)"/' /etc/makepkg.conf && \ - sed -i 's/^COMPRESSZST=.*/COMPRESSZST=(zstd -c -z -q --threads=0 -)/' /etc/makepkg.conf + sed -i 's/^COMPRESSZST=.*/COMPRESSZST=(zstd -c -z -q --threads=0 -)/' /etc/makepkg.conf && \ + sed -i "s|^PKGEXT=.*|PKGEXT='.pkg.tar.zst'|" /etc/makepkg.conf # Pacman wrapper that auto-resolves package conflicts (e.g. rustup vs rust). # makepkg only passes --noconfirm to pacman, which defaults to 'N' on conflict diff --git a/build/build.sh b/build/build.sh index e0f69acb..9be27162 100755 --- a/build/build.sh +++ b/build/build.sh @@ -5,6 +5,10 @@ # Setup directories ARCH=${ARCH:-x86_64} +# ARCH selects the repository target for this script, but make and Kbuild also +# interpret an exported ARCH themselves (Linux calls this target "arm64"). +# Keep the shell variable local to the orchestrator so PKGBUILDs see CARCH only. +export -n ARCH MIRROR=${MIRROR:-edge} DRY_RUN=${DRY_RUN:-false} PKGBUILDS_DIR=${PKGBUILDS_DIR:-/pkgbuilds} @@ -60,14 +64,11 @@ if [[ "$DRY_RUN" != true ]]; then # Configure Omarchy repositories for dependency resolution echo "==> Configuring Omarchy repositories for dependency resolution..." - # Always add omarchy-build repo (for incremental builds) - # Packages in build-output are unsigned, so use SigLevel = Never - sudo tee -a /etc/pacman.conf > /dev/null < omarchy-build (priority 1): $BUILD_OUTPUT_DIR" # Initialize empty build database if it doesn't exist @@ -90,12 +91,7 @@ EOF # Add omarchy repo if it has a database (stable packages) if [[ -f "$FINAL_OUTPUT_DIR/omarchy.db.tar.zst" ]] || [[ -f "$FINAL_OUTPUT_DIR/omarchy.db" ]]; then - sudo tee -a /etc/pacman.conf > /dev/null < omarchy (priority 2): $FINAL_OUTPUT_DIR" fi @@ -186,26 +182,9 @@ get_local_version() { # Returns 0 (success) if should build, 1 if should skip should_build_for_arch() { local pkg="$1" - local current_arch="$ARCH" - local pkgdir=$(find_package_dir "$pkg") - local pkgbuild="$pkgdir/PKGBUILD" - - [[ ! -f "$pkgbuild" ]] && return 1 - - # Check PKGBUILD arch=() array - local pkgbuild_archs=$(cd "$pkgdir" && bash -c 'source PKGBUILD 2>/dev/null; echo "${arch[@]}"') - - # If arch=('any'), build for all architectures - if [[ "$pkgbuild_archs" == "any" ]]; then - return 0 - fi - - # Check if current arch is in PKGBUILD arch=() - if echo "$pkgbuild_archs" | grep -qw "$current_arch"; then - return 0 # Build - else - return 1 # Skip - fi + local pkgdir + pkgdir=$(find_package_dir "$pkg") + [[ -n "$pkgdir" ]] && package_supports_arch "$pkgdir" "$ARCH" } # For VCS packages, makepkg recalculates pkgver() before the build. If the @@ -352,16 +331,48 @@ build_package() { if PACMAN=/usr/local/bin/pacman-for-makepkg makepkg "${makepkg_flags[@]}"; then # Ensure output directory exists mkdir -p "$BUILD_OUTPUT_DIR" - - for pkg_file in *.pkg.tar.*; do - [[ -f "$pkg_file" ]] && cp "$pkg_file" "$BUILD_OUTPUT_DIR/" + + # Copy only the artifacts makepkg declares as outputs. A PKGBUILD may use + # another pacman package as a source (schist-bin does); a *.pkg.tar.* glob + # would mistake that source archive for one of our freshly built packages. + local -a package_files=() + mapfile -t package_files < <(makepkg --packagelist) + + if [[ ${#package_files[@]} -eq 0 ]]; then + echo " Makepkg produced no package files for $pkg" + FAILED_PACKAGES="$FAILED_PACKAGES $pkg" + return 1 + fi + + local dependency_pkg_file="" + local -a new_pkgs=() + local pkg_path pkg_file + for pkg_path in "${package_files[@]}"; do + pkg_file=${pkg_path##*/} + if [[ ! -f "$pkg_file" ]]; then + # makepkg predicts an automatic -debug output whenever debug is + # enabled, but data-only packages may contain no symbols and therefore + # legitimately produce no debug archive. + if [[ "$pkg_file" == *-debug-*.pkg.tar.* ]]; then + continue + fi + + echo " Expected package file was not produced: $pkg_file" + FAILED_PACKAGES="$FAILED_PACKAGES $pkg" + return 1 + fi + + cp "$pkg_file" "$BUILD_OUTPUT_DIR/" + new_pkgs+=("$pkg_file") + + if [[ "$(bsdtar -xOf "$pkg_file" .PKGINFO 2>/dev/null | sed -n 's/^pkgname = //p')" == "$pkg" ]]; then + dependency_pkg_file="$BUILD_OUTPUT_DIR/$pkg_file" + fi done cd "$BUILD_OUTPUT_DIR" - # Find ALL package files (handles split packages) - local new_pkgs=($(ls -t ${pkg}-*.pkg.tar.* 2>/dev/null | grep -v '\.sig$' | grep -v 'omarchy-build\.db')) - + # Add every output from this build, including split packages. if [[ ${#new_pkgs[@]} -gt 0 ]]; then repo-add omarchy-build.db.tar.zst "${new_pkgs[@]}" >/dev/null 2>&1 ln -sf omarchy-build.db.tar.zst omarchy-build.db @@ -370,6 +381,24 @@ build_package() { cd /src/$pkg + # A lower-priority official repository may contain an older package with + # the same name. Install the exact artifact we just built before building + # its consumers, so pacman cannot select that older provider instead. + if [[ "${INSTALL_PACKAGES[$pkg]:-}" == "1" ]]; then + if [[ -z "$dependency_pkg_file" ]]; then + echo " Could not find the built $pkg package to install as a dependency" + FAILED_PACKAGES="$FAILED_PACKAGES $pkg" + return 1 + fi + + echo " Installing freshly built $pkg for dependent packages..." + if ! sudo /usr/local/bin/pacman-for-makepkg -U --needed --noconfirm "$dependency_pkg_file"; then + echo " Failed to install freshly built dependency $pkg" + FAILED_PACKAGES="$FAILED_PACKAGES $pkg" + return 1 + fi + fi + echo " Successfully built $pkg" SUCCESSFUL_PACKAGES="$SUCCESSFUL_PACKAGES $pkg" return 0 @@ -488,7 +517,7 @@ check_needs_build() { # Collect packages that should be built for the selected mirror collect_packages() { - packages_for_unscoped_build "$MIRROR" + packages_for_unscoped_build "$MIRROR" "$ARCH" } # Main execution @@ -540,13 +569,6 @@ if [[ -n "$PACKAGES" ]]; then else # Build all packages that need updates from the relevant directories while IFS= read -r pkg; do - # Check if package should be built for this architecture - if ! should_build_for_arch "$pkg"; then - echo " - $pkg - not built for $ARCH" - SKIPPED_PACKAGES="$SKIPPED_PACKAGES $pkg" - continue - fi - if check_needs_build "$pkg"; then PACKAGES_TO_BUILD+=("$pkg") else diff --git a/helpers/docker-helpers.sh b/helpers/docker-helpers.sh index 4a269041..cc18222a 100644 --- a/helpers/docker-helpers.sh +++ b/helpers/docker-helpers.sh @@ -1,25 +1,102 @@ -# Docker helper functions for Omarchy package build system +# Container engine helpers for Omarchy package build system -check_docker() { - if ! command -v docker &>/dev/null; then - print_error "Docker is not installed" +container_engine_supported() { + [[ "$CONTAINER_ENGINE" == "docker" || "$CONTAINER_ENGINE" == "podman" ]] +} + +if [[ -z "${CONTAINER_ENGINE:-}" ]]; then + for engine in docker podman; do + if command -v "$engine" >/dev/null 2>&1 && "$engine" info >/dev/null 2>&1; then + CONTAINER_ENGINE="$engine" + break + fi + done +fi +export CONTAINER_ENGINE + +# Rootless Podman otherwise maps the image's builder uid 1000 to a subordinate +# host uid. keep-id makes files written through bind mounts belong to the user +# who invoked the build. +CONTAINER_RUN_ARGS=() +if [[ "$CONTAINER_ENGINE" == "podman" ]]; then + CONTAINER_RUN_ARGS+=("--userns=keep-id:uid=1000,gid=1000") +fi + +check_engine() { + if [[ -n "$CONTAINER_ENGINE" ]] && ! container_engine_supported; then + print_error "Unsupported CONTAINER_ENGINE: $CONTAINER_ENGINE (use docker or podman)" + exit 1 + fi + + if [[ -z "$CONTAINER_ENGINE" ]]; then + print_error "No working container engine found (tried Docker, then Podman)" + if command -v docker >/dev/null 2>&1; then + print_warning "Docker is installed but unavailable. Start it with: sudo systemctl start docker" + elif command -v podman >/dev/null 2>&1; then + print_warning "Podman is installed but 'podman info' failed" + else + print_warning "Install Docker or Podman" + fi + exit 1 + fi + + if ! command -v "$CONTAINER_ENGINE" >/dev/null 2>&1; then + print_error "$CONTAINER_ENGINE is not installed" exit 1 fi - if ! docker info &>/dev/null; then - print_error "Docker daemon is not running" - print_warning "Start Docker with: sudo systemctl start docker" + if ! "$CONTAINER_ENGINE" info >/dev/null 2>&1; then + if [[ "$CONTAINER_ENGINE" == "docker" ]]; then + print_error "Docker daemon is not running or is not accessible" + print_warning "Start Docker with: sudo systemctl start docker" + else + print_error "Podman is not available to the current user" + fi exit 1 fi } +docker_native_arch() { + case "$(uname -m)" in + x86_64) echo x86_64 ;; + aarch64 | arm64) echo aarch64 ;; + *) return 1 ;; + esac +} + setup_qemu() { - # Setup QEMU for building ARM64 packages on x86_64 hosts - if ! docker run --rm --privileged multiarch/qemu-user-static --reset -p yes --credential yes >/dev/null 2>&1; then - print_error "Failed to setup QEMU for ARM64 emulation" + local target_arch="${1:-aarch64}" + + if [[ "$CONTAINER_ENGINE" == "podman" ]]; then + local registration="/proc/sys/fs/binfmt_misc/qemu-$target_arch" + local packaged_registration="/usr/lib/binfmt.d/qemu-$target_arch-static.conf" + local flags="" + + if [[ -r "$registration" ]]; then + flags=$(sed -n 's/^flags: //p' "$registration") + fi + + if [[ "$flags" == *F* && "$flags" == *C* ]]; then + print_success "QEMU $target_arch emulation is registered" + return 0 + fi + + print_error "Rootless Podman requires QEMU binfmt registration with the F and C flags" + print_info "F keeps the emulator available inside containers; C lets container sudo preserve credentials." + print_info "Configure it once with:" + echo " sudo pacman -S --needed qemu-user-static qemu-user-static-binfmt" + echo " sudo mkdir -p /etc/binfmt.d" + echo " sed 's/:FP$/:FPC/' $packaged_registration | sudo tee /etc/binfmt.d/qemu-$target_arch-static.conf >/dev/null" + echo " sudo systemctl restart systemd-binfmt" + exit 1 + fi + + # Register emulators for builds whose target differs from the host. + if ! "$CONTAINER_ENGINE" run --rm --privileged docker.io/multiarch/qemu-user-static --reset -p yes --credential yes >/dev/null 2>&1; then + print_error "Failed to set up QEMU emulation" exit 1 fi - print_success "QEMU ARM64 emulation enabled" + print_success "QEMU emulation enabled" } build_docker_image() { @@ -39,23 +116,32 @@ build_docker_image() { ;; esac - print_info "Building Docker image for $arch ($platform) using $mirror mirror..." - - docker buildx build \ - --platform "$platform" \ - --build-arg MIRROR="$mirror" \ - --load \ - -t "$image_tag" \ - -f "$build_dir/Dockerfile" \ - "$build_dir" + print_info "Building container image for $arch ($platform) using $mirror mirror..." + + if [[ "$CONTAINER_ENGINE" == "docker" ]]; then + "$CONTAINER_ENGINE" buildx build \ + --platform "$platform" \ + --build-arg MIRROR="$mirror" \ + --load \ + -t "$image_tag" \ + -f "$build_dir/Dockerfile" \ + "$build_dir" + else + "$CONTAINER_ENGINE" build \ + --platform "$platform" \ + --build-arg MIRROR="$mirror" \ + -t "$image_tag" \ + -f "$build_dir/Dockerfile" \ + "$build_dir" + fi } get_platform_arg() { local arch="$1" case "$arch" in - x86_64) echo "--platform linux/amd64" ;; - aarch64) echo "--platform linux/arm64" ;; - *) echo "" ;; + x86_64) echo "--platform=linux/amd64" ;; + aarch64) echo "--platform=linux/arm64" ;; + *) return 1 ;; esac } diff --git a/helpers/package-metadata.sh b/helpers/package-metadata.sh index 90961030..043e2fc5 100644 --- a/helpers/package-metadata.sh +++ b/helpers/package-metadata.sh @@ -15,10 +15,11 @@ # { "source": "local", "channels": ["edge"] } # { "source": "local", "channels": ["edge", "rc", "stable"] } # { "source": "local", "min_release_age": "24h" } -# { "source": "local", "upstream": { "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "x86_64": "name-{tag}-x64.tar.xz" } } } +# { "source": "local", "upstream": { "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "x86_64": ["name-{tag}-x64.tar.xz"] } } } # { "source": "local", "upstream": { "github": "owner/repo", "digests": true, "assets": { "x86_64": "name-{tag}-x64.tar.xz" } } } # { "source": "local", "upstream": { "git_tags": "https://example/repo.git", "tag_pattern": "v{pkgver}", "sources": { "any": ["https://example/archive/{tag}.tar.gz"] } } } # { "source": "local", "upstream": { "npm": "@scope/package", "sources": { "any": ["{npm_tarball}"] } } } +# { "source": "local", "upstream": { "debian": "https://example/debian/dists/stable/main/binary-amd64/Packages", "package": "example", "sources": { "any": ["https://example/releases/{pkgver}.tar.gz"] } } } # # bin/sync-aur also writes upstream_commit for AUR-backed packages, and # bin/sync-rebuilds writes rebuilt_against for packages declaring rebuild_on. @@ -143,6 +144,31 @@ package_has_pkgbuild() { [[ -f "$pkgdir/PKGBUILD" ]] } +# The architectures declared by a PKGBUILD. Set CARCH while reading it so a +# conditional arch=() assignment is evaluated for the architecture we are +# actually checking, even when the repository host is a different one. +package_arches() { + local pkgdir="$1" + local arch="${2:-${ARCH:-x86_64}}" + + (cd "$pkgdir" && env -u OMARCHY_SRC CARCH="$arch" bash -c ' + source PKGBUILD >/dev/null 2>&1 + printf "%s\n" "${arch[*]}" + ') +} + +package_supports_arch() { + local pkgdir="$1" + local target="${2:-${ARCH:-x86_64}}" + local arches + + arches=$(package_arches "$pkgdir" "$target") || return 1 + case " $arches " in + *" any "* | *" $target "*) return 0 ;; + *) return 1 ;; + esac +} + # Channel membership: where a package may be published. Packages without a # `channels` key are members of every channel (they flow edge -> rc -> stable). package_has_channels() { @@ -301,9 +327,12 @@ packages_for_mirror() { packages_for_unscoped_build() { local mirror="$1" + local arch="${2:-${ARCH:-x86_64}}" package_dirs | while IFS= read -r pkgdir; do - if package_builds_for_mirror "$pkgdir" "$mirror" && ! package_build_skipped "$pkgdir"; then + if package_builds_for_mirror "$pkgdir" "$mirror" && + ! package_build_skipped "$pkgdir" && + package_supports_arch "$pkgdir" "$arch"; then basename "$pkgdir" fi done @@ -454,30 +483,45 @@ validate_package_metadata() { (.key | test("\\A[a-z0-9_]+\\z")) and (.value | type == "array" and length > 0 and all(type == "string" and length > 0)) )); + def valid_assets: + type == "object" and length > 0 and (to_entries | all( + (.key | test("\\A[a-z0-9_]+\\z")) + and (.value | + (type == "string" and length > 0) + or (type == "array" and length > 0 and all(type == "string" and length > 0) and (unique | length) == length) + ) + )); if has("upstream") | not then true elif (.upstream | type) != "object" then false else .upstream | - ([has("github"), has("git_tags"), has("npm")] | map(select(.)) | length) == 1 + ([has("github"), has("git_tags"), has("npm"), has("debian")] | map(select(.)) | length) == 1 and if has("github") then (.github | type == "string" and test("\\A[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\\z")) and (if has("checksums") then (.checksums | type == "string" and length > 0) else true end) and (if has("digests") then (.digests | type == "boolean") else true end) + and (if has("latest_only") then (.latest_only | type == "boolean") else true end) and (has("checksums") != (has("digests") and .digests == true)) - and (.assets | type == "object" and length > 0 and (to_entries | all( - (.key | test("\\A[a-z0-9_]+\\z")) and (.value | type == "string" and length > 0) - ))) + and (.assets | valid_assets) + and (if has("sources") then + (.sources | valid_sources) + and ((.assets | keys) as $assets | (.sources | keys) as $sources | ($assets - $sources | length) == ($assets | length)) + else true end) elif has("git_tags") then (.git_tags | type == "string" and test("\\Ahttps://[^[:space:]]+\\.git\\z")) and (.tag_pattern | type == "string" and (split("{pkgver}") | length) == 2) and (.sources | valid_sources) - else + elif has("npm") then (.npm | type == "string" and test("\\A(@[a-z0-9_.-]+/)?[a-z0-9_.-]+\\z")) and ((.dist_tag // "latest") | type == "string" and test("\\A[a-z0-9_.-]+\\z")) and (.sources | valid_sources) + else + (.debian | type == "string" and test("\\Ahttps://[^[:space:]]+\\z")) + and (.package | type == "string" and test("\\A[a-z0-9][a-z0-9+.-]*\\z")) + and (.sources | valid_sources) end end ' "$metadata" >/dev/null; then - echo "invalid upstream for $(basename "$pkgdir"): configure exactly one valid github, git_tags, or npm provider" + echo "invalid upstream for $(basename "$pkgdir"): configure exactly one valid github, git_tags, npm, or debian provider" return 1 fi @@ -502,12 +546,28 @@ validate_package_metadata() { return 1 fi - if ! jq -e '(.rebuilt_against // {}) | type == "object" and (to_entries | all(.value | type == "string" and length > 0))' "$metadata" >/dev/null; then - echo "invalid rebuilt_against for $(basename "$pkgdir"): must be an object mapping package names to versions" + if ! jq -e ' + def version_map: + type == "object" and (to_entries | all(.value | type == "string" and length > 0)); + (.rebuilt_against // {}) as $record | + ($record | version_map) or + (($record | type) == "object" + and ((($record | keys) - ["x86_64", "aarch64"]) | length == 0) + and ($record | to_entries | all(.value | version_map))) + ' "$metadata" >/dev/null; then + echo "invalid rebuilt_against for $(basename "$pkgdir"): must map architectures to package-version maps" return 1 fi - if ! jq -e '((.rebuilt_against // {}) | keys) - (.rebuild_on // []) | length == 0' "$metadata" >/dev/null; then + if ! jq -e ' + (.rebuild_on // []) as $triggers | + (.rebuilt_against // {}) as $record | + if ($record | to_entries | all(.value | type == "string")) then + ((($record | keys) - $triggers) | length == 0) + else + ($record | to_entries | all((((.value | keys) - $triggers) | length) == 0)) + end + ' "$metadata" >/dev/null; then echo "invalid rebuilt_against for $(basename "$pkgdir"): records a package that rebuild_on does not name" return 1 fi diff --git a/helpers/paths.sh b/helpers/paths.sh index 42fb44bd..5d6191c3 100644 --- a/helpers/paths.sh +++ b/helpers/paths.sh @@ -5,6 +5,62 @@ ARCH=${ARCH:-x86_64} MIRROR=${MIRROR:-edge} +# Architectures the tooling knows how to build. +VALID_ARCHES="x86_64 aarch64" + +# Architectures this repository PUBLISHES. The scheduled pipeline (version +# check, auto-release, channel advance with --arch all) runs once per entry, +# in this order; the first entry is the reference architecture that the +# release train observes channels through. Adding an architecture here is the +# enablement step: the next check-versions tick queues its packages and the +# next auto-release tick builds them. OMARCHY_ARCHES overrides it for a +# one-off run. +PUBLISHED_ARCHES="${OMARCHY_ARCHES:-x86_64}" + +validate_arch() { + case " $VALID_ARCHES " in + *" $1 "*) return 0 ;; + *) return 1 ;; + esac +} + +require_valid_arch() { + if ! validate_arch "$1"; then + echo "Invalid architecture: $1 (must be one of: $VALID_ARCHES)" >&2 + exit 1 + fi +} + +published_arches() { + local arch + for arch in $PUBLISHED_ARCHES; do + require_valid_arch "$arch" + echo "$arch" + done +} + +reference_arch() { + published_arches | head -1 +} + +# Scheduled-pipeline state, one file per channel and architecture, so one +# architecture's queue or backoff never gates another's. +STATE_DIR="${OMARCHY_STATE_DIR:-/root/.state}" + +sync_queue_file() { # sync_queue_file + echo "$STATE_DIR/.sync-needed-$1-$2" +} + +sync_fail_file() { # sync_fail_file + echo "$STATE_DIR/.build-failed-$1-$2" +} + +# The pre-architecture names, .sync-needed- and .build-failed-, +# meant x86_64. A host upgraded mid-cycle may still hold one; readers treat +# it as the x86_64 file until it is consumed. +legacy_sync_queue_file() { echo "$STATE_DIR/.sync-needed-$1"; } +legacy_sync_fail_file() { echo "$STATE_DIR/.build-failed-$1"; } + # Valid package channels, in pipeline order: packages move edge -> rc -> stable VALID_MIRRORS="edge rc stable" diff --git a/helpers/upstream-github.sh b/helpers/upstream-github.sh index 1b6e64ce..6c7e1ba4 100644 --- a/helpers/upstream-github.sh +++ b/helpers/upstream-github.sh @@ -20,8 +20,8 @@ # "v", which is stripped for pkgver. Drafts and prereleases are ignored. The # provider emits the same JSON contract as an upstream.sh hook, so # bin/sync-upstream's validation and min_release_age backstop apply -# unchanged. Git-tag and npm providers below cover projects without GitHub -# releases; a feed that fits no convention keeps a bespoke hook. +# unchanged. Git-tag, npm, and Debian Packages providers below cover projects +# without GitHub releases; a feed that fits no convention keeps a bespoke hook. # Return the single declarative provider selected by a package. An empty # result means either no provider or an invalid/ambiguous declaration; the @@ -31,7 +31,7 @@ package_upstream_provider() { metadata=$(metadata_file_for_dir "$pkgdir") jq -r ' (.upstream? | objects) as $u - | [$u | keys[] | select(. == "github" or . == "git_tags" or . == "npm")] + | [$u | keys[] | select(. == "github" or . == "git_tags" or . == "npm" or . == "debian")] | if length == 1 then .[0] else "" end ' "$metadata" } @@ -64,6 +64,11 @@ npm_fetch_metadata() { curl -fsSL "https://registry.npmjs.org/$encoded" } +debian_fetch_packages() { + local url="$1" + curl --proto '=https' --proto-redir '=https' -fsSL "$url" +} + upstream_fetch_source() { local url="$1" output="$2" curl --proto '=https' --proto-redir '=https' -fsSL -o "$output" "$url" @@ -198,6 +203,71 @@ npm_upstream_release() { printf '%s\n' "$release" } +# Discover a package version from a plain-text Debian Packages index, then +# hash the declared immutable sources. This intentionally supports only +# versions that are already valid Arch pkgver values; feeds needing Debian +# epoch/revision translation keep a package-specific hook. +debian_upstream_release() { + local package_dir="$1" metadata index_url package packages + metadata=$(metadata_file_for_dir "$package_dir") + index_url=$(jq -r '.upstream.debian // ""' "$metadata") + package=$(jq -r '.upstream.package // ""' "$metadata") + + if [[ ! "$index_url" =~ ^https://[^[:space:]]+$ || ! "$package" =~ ^[a-z0-9][a-z0-9+.-]*$ ]]; then + echo "invalid Debian Packages provider configuration" >&2 + return 1 + fi + if ! jq -e ' + .upstream.sources | type == "object" and length > 0 and (to_entries | all( + (.key | test("\\A[a-z0-9_]+\\z")) + and (.value | type == "array" and length > 0 and all(type == "string" and length > 0)) + )) + ' "$metadata" >/dev/null; then + echo "invalid Debian Packages source mapping" >&2 + return 1 + fi + if ! packages=$(debian_fetch_packages "$index_url"); then + echo "could not fetch Debian Packages index: $index_url" >&2 + return 1 + fi + packages=${packages//$'\r'/} + + local best_pkgver="" candidate + while IFS= read -r candidate; do + if [[ ! "$candidate" =~ ^[A-Za-z0-9][A-Za-z0-9._+]*$ ]]; then + echo "$package has an unusable Debian version: ${candidate:-}" >&2 + return 1 + fi + if [[ -z "$best_pkgver" || $(vercmp "$candidate" "$best_pkgver") -gt 0 ]]; then + best_pkgver="$candidate" + fi + done < <(awk -v target="$package" ' + BEGIN { RS = ""; FS = "\n" } + { + name = version = "" + for (i = 1; i <= NF; i++) { + if ($i ~ /^Package: /) name = substr($i, 10) + if ($i ~ /^Version: /) version = substr($i, 10) + } + if (name == target) print version + } + ' <<<"$packages") + + [[ -n "$best_pkgver" ]] || { + echo "no usable $package release found in $index_url" >&2 + return 1 + } + + local current_pkgver + current_pkgver=$(grep -m1 '^pkgver=' "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'") + if [[ $(vercmp "$best_pkgver" "$current_pkgver") -le 0 ]]; then + echo '{}' + return 0 + fi + + upstream_hash_sources "$package_dir" "$best_pkgver" "$best_pkgver" +} + # Emits the newest qualifying release as hook-contract JSON. min_release_age # is honored during selection (newest release older than the window wins, # even when a younger one exists) and BYPASS_MIN_RELEASE_AGE=1 lifts it. @@ -206,7 +276,7 @@ npm_upstream_release() { # not silently choose from. github_upstream_release() { local package_dir="$1" min_age="${2:-0}" - local metadata repo checksums_name use_digests + local metadata repo checksums_name use_digests latest_only metadata=$(metadata_file_for_dir "$package_dir") repo=$(jq -r '(.upstream? | objects | .github) // ""' "$metadata") @@ -218,10 +288,15 @@ github_upstream_release() { # reaches this provider without running the validator first. checksums_name=$(jq -r '(.upstream? | objects | .checksums) | strings' "$metadata") use_digests=$(jq -r '(.upstream? | objects | .digests) | if . == null then "false" elif type == "boolean" then tostring else "invalid" end' "$metadata") + latest_only=$(jq -r '(.upstream? | objects | .latest_only) | if . == null then "false" elif type == "boolean" then tostring else "invalid" end' "$metadata") if [[ "$use_digests" == "invalid" ]]; then echo "upstream.digests must be true or false" >&2 return 1 fi + if [[ "$latest_only" == "invalid" ]]; then + echo "upstream.latest_only must be true or false" >&2 + return 1 + fi if [[ -n "$checksums_name" && "$use_digests" == "true" ]]; then echo "upstream sets both checksums and digests; keep exactly one" >&2 return 1 @@ -230,6 +305,27 @@ github_upstream_release() { echo "upstream needs either checksums (a manifest asset name) or digests: true" >&2 return 1 fi + if ! jq -e ' + def valid_sources: + type == "object" and length > 0 and (to_entries | all( + (.key | test("\\A[a-z0-9_]+\\z")) + and (.value | type == "array" and length > 0 and all(type == "string" and length > 0)) + )); + (.upstream.assets | type == "object" and length > 0 and (to_entries | all( + (.key | test("\\A[a-z0-9_]+\\z")) + and (.value | + (type == "string" and length > 0) + or (type == "array" and length > 0 and all(type == "string" and length > 0) and (unique | length) == length) + ) + ))) + and (if .upstream | has("sources") then + (.upstream.sources | valid_sources) + and ((.upstream.assets | keys) as $assets | (.upstream.sources | keys) as $sources | ($assets - $sources | length) == ($assets | length)) + else true end) + ' "$metadata" >/dev/null; then + echo "invalid upstream.assets or upstream.sources mapping" >&2 + return 1 + fi local arches mapfile -t arches < <(jq -r '(.upstream? | objects | .assets) // {} | keys[]' "$metadata") if [[ ${#arches[@]} -eq 0 ]]; then @@ -243,6 +339,9 @@ github_upstream_release() { echo "could not fetch the release feed for $repo" >&2 return 1 fi + if [[ "$latest_only" == "true" ]]; then + releases=$(jq '[.[] | select((.draft or .prerelease) | not)][0:1]' <<<"$releases") + fi local candidates=0 best_tag="" best_pkgver="" best_published_at="" local tag published_at pkgver published_epoch @@ -302,39 +401,50 @@ github_upstream_release() { return 1 fi - local jq_args=(--arg pkgver "$best_pkgver" --arg published_at "$best_published_at") - local jq_filter='{pkgver: $pkgver, published_at: $published_at, sha256sums: {}}' - local arch template filename checksum checksum_source + local result + result=$(jq -n --arg pkgver "$best_pkgver" --arg published_at "$best_published_at" \ + '{pkgver: $pkgver, published_at: $published_at, sha256sums: {}}') + local arch template filename checksum checksum_source sums for arch in "${arches[@]}"; do if [[ ! "$arch" =~ ^[a-z0-9_]+$ ]]; then echo "invalid architecture key in upstream.assets: '$arch'" >&2 return 1 fi - template=$(jq -r --arg arch "$arch" '.upstream.assets[$arch]' "$metadata") - filename=${template//\{pkgver\}/$best_pkgver} - filename=${filename//\{tag\}/$best_tag} - if [[ "$use_digests" == "true" ]]; then - # Only a "sha256:" digest is stripped to its hex; any other shape - # falls through empty and fails the check below. - checksum=$(jq -r --arg tag "$best_tag" --arg name "$filename" ' - first(.[] | select(.tag_name == $tag)) | (.assets // [])[] - | select(.name == $name) | (.digest // "") - | if type == "string" and test("\\Asha256:[0-9a-f]{64}\\z") then ltrimstr("sha256:") else "" end - ' <<<"$releases") - checksum_source="the release API digest" - else - # Manifest lines are " ", with the name sometimes prefixed - # "./" (sha256sum of a local path) or "*" (binary-mode marker). - checksum=$(awk -v f="$filename" '$2 == f || $2 == "./" f || $2 == "*" f { print $1; exit }' <<<"$checksums") - checksum_source="$checksums_name" - fi - if [[ ! "$checksum" =~ ^[0-9a-f]{64}$ ]]; then - echo "no valid checksum for $filename in $repo $best_tag $checksum_source" >&2 - return 1 - fi - jq_args+=(--arg "sum_$arch" "$checksum") - jq_filter+=" | .sha256sums[\"$arch\"] = [\$sum_$arch]" + sums='[]' + while IFS= read -r template; do + filename=${template//\{pkgver\}/$best_pkgver} + filename=${filename//\{tag\}/$best_tag} + if [[ "$use_digests" == "true" ]]; then + # Only a "sha256:" digest is stripped to its hex; any other shape + # falls through empty and fails the check below. + checksum=$(jq -r --arg tag "$best_tag" --arg name "$filename" ' + first(.[] | select(.tag_name == $tag)) | (.assets // [])[] + | select(.name == $name) | (.digest // "") + | if type == "string" and test("\\Asha256:[0-9a-f]{64}\\z") then ltrimstr("sha256:") else "" end + ' <<<"$releases") + checksum_source="the release API digest" + else + # Manifest lines are " ", with the name sometimes prefixed + # "./" (sha256sum of a local path) or "*" (binary-mode marker). + checksum=$(awk -v f="$filename" '$2 == f || $2 == "./" f || $2 == "*" f { print $1; exit }' <<<"$checksums") + checksum_source="$checksums_name" + fi + if [[ ! "$checksum" =~ ^[0-9a-f]{64}$ ]]; then + echo "no valid checksum for $filename in $repo $best_tag $checksum_source" >&2 + return 1 + fi + sums=$(jq -c --arg checksum "$checksum" '. + [$checksum]' <<<"$sums") + done < <(jq -r --arg arch "$arch" ' + .upstream.assets[$arch] | if type == "array" then .[] else . end + ' "$metadata") + result=$(jq -c --arg arch "$arch" --argjson sums "$sums" '.sha256sums[$arch] = $sums' <<<"$result") done - jq -n "${jq_args[@]}" "$jq_filter" + if jq -e '.upstream | has("sources")' "$metadata" >/dev/null; then + local source_release + source_release=$(upstream_hash_sources "$package_dir" "$best_pkgver" "$best_tag") || return 1 + result=$(jq -c --argjson source "$source_release" '.sha256sums += $source.sha256sums' <<<"$result") + fi + + printf '%s\n' "$result" } diff --git a/pkgbuilds/1password/.omarchy/package.json b/pkgbuilds/1password/.omarchy/package.json index b3137453..663db8e5 100644 --- a/pkgbuilds/1password/.omarchy/package.json +++ b/pkgbuilds/1password/.omarchy/package.json @@ -1,5 +1,18 @@ { - "source": "aur", + "source": "local", "release_ring": "fast", - "upstream_commit": "e323d0d1f8dea6b75bb651ce14acc73904cd0326" + "upstream": { + "debian": "https://downloads.1password.com/linux/debian/amd64/dists/stable/main/binary-amd64/Packages", + "package": "1password", + "sources": { + "x86_64": [ + "https://downloads.1password.com/linux/tar/stable/x86_64/1password-{pkgver}.x64.tar.gz", + "https://downloads.1password.com/linux/tar/stable/x86_64/1password-{pkgver}.x64.tar.gz.sig" + ], + "aarch64": [ + "https://downloads.1password.com/linux/tar/stable/aarch64/1password-{pkgver}.arm64.tar.gz", + "https://downloads.1password.com/linux/tar/stable/aarch64/1password-{pkgver}.arm64.tar.gz.sig" + ] + } + } } diff --git a/pkgbuilds/1password/PKGBUILD b/pkgbuilds/1password/PKGBUILD index 80d90d80..4e91196d 100644 --- a/pkgbuilds/1password/PKGBUILD +++ b/pkgbuilds/1password/PKGBUILD @@ -1,19 +1,33 @@ pkgname=1password -_tarver=8.12.34 -_tar="1password-${_tarver}.x64.tar.gz" -pkgver=${_tarver//-/_} -pkgrel=34 +case "$CARCH" in + x86_64) _tararch=x64 ;; + aarch64) _tararch=arm64 ;; + *) return 1 ;; +esac + +pkgver=8.12.34 +pkgrel=35 conflicts=('1password-beta' '1password-beta-bin') pkgdesc="Password manager and secure wallet" -arch=('x86_64') +arch=('x86_64' 'aarch64') url='https://1password.com' license=('LicenseRef-1Password-Proprietary') options=(!strip) install="1password.install" -source=(https://downloads.1password.com/linux/tar/stable/${CARCH}/${_tar}{,.sig}) -sha256sums=('297784aa66770b645607a7f04c9ba2c4aebed4f46d21202487f521ba572b7b13' - 'ec085bef60de748895d3c51a8208301ba2ac8fb47db99334539ba4bd1d3260d7' +source_x86_64=( + "https://downloads.1password.com/linux/tar/stable/x86_64/1password-${pkgver}.x64.tar.gz" + "https://downloads.1password.com/linux/tar/stable/x86_64/1password-${pkgver}.x64.tar.gz.sig" +) +source_aarch64=( + "https://downloads.1password.com/linux/tar/stable/aarch64/1password-${pkgver}.arm64.tar.gz" + "https://downloads.1password.com/linux/tar/stable/aarch64/1password-${pkgver}.arm64.tar.gz.sig" +) +sha256sums_x86_64=('297784aa66770b645607a7f04c9ba2c4aebed4f46d21202487f521ba572b7b13' + 'ec085bef60de748895d3c51a8208301ba2ac8fb47db99334539ba4bd1d3260d7' +) +sha256sums_aarch64=('ea5102363d6cf3442b96a7abd6743da8c1d261f56a628e1a3c183d84fa65fdcb' + 'f44db73fa44c3f68c3ab78a9cce140be9355de1dd9be4ce731c9d6597960c907' ) validpgpkeys=('3FEF9748469ADBE15DA7CA80AC2D62742012EA22') @@ -21,7 +35,7 @@ package() { depends=('hicolor-icon-theme' 'libgtk-3.so=0' 'nss' 'xdg-utils') # Go to source directory - cd "1password-${_tarver}.x64" + cd "1password-${pkgver}.${_tararch}" # Install icons resolutions=(32x32 64x64 256x256 512x512) @@ -49,7 +63,7 @@ EOF" > ./com.1password.1Password.policy # Move package contents to /opt/1Password cd "${srcdir}" install -dm0755 "${pkgdir}"/opt - mv "1password-${_tarver}.x64" "${pkgdir}/opt/1Password" + mv "1password-${pkgver}.${_tararch}" "${pkgdir}/opt/1Password" # Cleanup un-needed files rm "${pkgdir}"/opt/1Password/com.1password.1Password.policy "${pkgdir}"/opt/1Password/com.1password.1Password.policy.tpl "${pkgdir}"/opt/1Password/install_biometrics_policy.sh diff --git a/pkgbuilds/dropbox-cli/.omarchy/patches/x86-only.patch b/pkgbuilds/dropbox-cli/.omarchy/patches/x86-only.patch new file mode 100644 index 00000000..2448ae46 --- /dev/null +++ b/pkgbuilds/dropbox-cli/.omarchy/patches/x86-only.patch @@ -0,0 +1,11 @@ +--- a/PKGBUILD ++++ b/PKGBUILD +@@ -11,7 +11,7 @@ pkgname=dropbox-cli + pkgver=2024.04.17 + pkgrel=2 + pkgdesc="Command line interface for Dropbox" +-arch=("any") ++arch=("x86_64") + url="https://www.dropbox.com" + license=("GPL-3.0-or-later") + makedepends=("gdk-pixbuf2") diff --git a/pkgbuilds/dropbox-cli/PKGBUILD b/pkgbuilds/dropbox-cli/PKGBUILD index 7cc84b95..96f1777b 100644 --- a/pkgbuilds/dropbox-cli/PKGBUILD +++ b/pkgbuilds/dropbox-cli/PKGBUILD @@ -7,9 +7,9 @@ pkgname=dropbox-cli pkgver=2024.04.17 -pkgrel=2 +pkgrel=2.1 pkgdesc="Command line interface for Dropbox" -arch=("any") +arch=("x86_64") url="https://www.dropbox.com" license=("GPL-3.0-or-later") makedepends=("gdk-pixbuf2") @@ -36,4 +36,3 @@ package() { cd "nautilus-dropbox-${pkgver}" install -m 755 -D "${pkgname}" "${pkgdir}/usr/bin/${pkgname}" } - diff --git a/pkgbuilds/hyprland-guiutils/.omarchy/package.json b/pkgbuilds/hyprland-guiutils/.omarchy/package.json new file mode 100644 index 00000000..d87fa12f --- /dev/null +++ b/pkgbuilds/hyprland-guiutils/.omarchy/package.json @@ -0,0 +1,21 @@ +{ + "source": "local", + "release_ring": "fast", + "upstream": { + "git_tags": "https://github.com/hyprwm/hyprland-guiutils.git", + "tag_pattern": "v{pkgver}", + "sources": { + "any": [ + "https://github.com/hyprwm/hyprland-guiutils/archive/v{pkgver}/hyprland-guiutils-{pkgver}.tar.gz" + ] + } + }, + "rebuild_on": [ + "aquamarine" + ], + "rebuilt_against": { + "aarch64": { + "aquamarine": "0.15.0-2" + } + } +} diff --git a/pkgbuilds/hyprland-guiutils/PKGBUILD b/pkgbuilds/hyprland-guiutils/PKGBUILD new file mode 100644 index 00000000..036e6bf4 --- /dev/null +++ b/pkgbuilds/hyprland-guiutils/PKGBUILD @@ -0,0 +1,40 @@ +# Maintainer: Caleb Maclennan + +pkgname=hyprland-guiutils +pkgver=0.2.2 +pkgrel=3 +pkgdesc='Hyprland GUI utilities' +arch=(aarch64) +url="https://github.com/hyprwm/$pkgname" +license=(BSD-3-Clause) +depends=( + libgcc + libstdc++ + glibc # libc.so libm.so + hyprlang + hyprtoolkit libhyprtoolkit.so + hyprutils libhyprutils.so + libdrm + pixman +) +makedepends=(cmake) +replaces=(hyprland-qtutils) +_archive="$pkgname-$pkgver" +source=("$url/archive/v$pkgver/$_archive.tar.gz") +sha256sums=('16f92a6c5a22ac58e1fc313f6b202c188da45e804e1f21ff57dfd0da5c1a01b7') + +build() { + cd "$_archive" + local cmake_flags=( + -D CMAKE_BUILD_TYPE=Release + -D CMAKE_INSTALL_PREFIX=/usr + ) + cmake -B build ${cmake_flags[@]} + cmake --build build +} + +package() { + cd "$_archive" + DESTDIR="$pkgdir" cmake --install build + install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname" LICENSE +} diff --git a/pkgbuilds/hyprland/.omarchy/package.json b/pkgbuilds/hyprland/.omarchy/package.json new file mode 100644 index 00000000..9dd97453 --- /dev/null +++ b/pkgbuilds/hyprland/.omarchy/package.json @@ -0,0 +1,21 @@ +{ + "source": "local", + "release_ring": "fast", + "upstream": { + "git_tags": "https://github.com/hyprwm/Hyprland.git", + "tag_pattern": "v{pkgver}", + "sources": { + "any": [ + "https://github.com/hyprwm/Hyprland/releases/download/v{pkgver}/source-v{pkgver}.tar.gz" + ] + } + }, + "rebuild_on": [ + "aquamarine" + ], + "rebuilt_against": { + "aarch64": { + "aquamarine": "0.15.0-2" + } + } +} diff --git a/pkgbuilds/hyprland/PKGBUILD b/pkgbuilds/hyprland/PKGBUILD new file mode 100644 index 00000000..cbb70ed8 --- /dev/null +++ b/pkgbuilds/hyprland/PKGBUILD @@ -0,0 +1,105 @@ +# Maintainer: Caleb Maclennan +# Contributor: ThatOneCalculator +# Contributor: Brenno Lemos +# Contributor: Gabriel Fox + +pkgname=(hyprland hyprpm) +pkgver=0.56.2 +pkgrel=3 +pkgdesc='a highly customizable dynamic tiling Wayland compositor' +arch=(aarch64) +url="https://github.com/hyprwm/${pkgname^}" +license=(BSD-3-Clause) +depends=(cairo # libcairo.so + aquamarine libaquamarine.so + libgcc # libgcc_s.so + 'libstdc++' # libstdc++.so + glibc # libc.so libm.so + glib2 libgio-2.0.so libgobject-2.0.so + glslang # libglslang-default-resource-limits.so libglslang.so + hyprcursor libhyprcursor.so + hyprgraphics libhyprgraphics.so + hyprland-guiutils + hyprlang libhyprlang.so + hyprutils libhyprutils.so + hyprwire libhyprwire.so + lcms2 liblcms2.so + libdrm # libdrm.so + libglvnd libEGL.so libGLESv2.so + libinput # libinput.so + libx11 + libxcb # libxcb-dri3.so libxcb-present.so libxcb-render.so libxcb-res.so libxcb-shm.so libxcb.so libxcb-xfixes.so libxcb-xinput.so + libxcomposite # libxcb-composite.so + libxcursor # libXcursor.so + libxfixes + libxkbcommon libxkbcommon.so + libxrender + lua # liblua.so + mesa # libgbm.so + muparser # libmuparser.so + opengl-driver + pango libpango-1.0.so libpangocairo-1.0.so + pixman libpixman-1.so + re2 libre2.so + tomlplusplus libtomlplusplus.so + util-linux-libs libuuid.so + wayland libwayland-server.so + wayland-protocols + xcb-proto + xcb-util + xcb-util-errors # libxcb-errors.so + xcb-util-image + xcb-util-keysyms + xcb-util-renderutil # libxcb-render-util.so + xcb-util-wm # libxcb-ewmh.so libxcb-icccm.so + xorg-xwayland) +makedepends=(cmake + glaze + hyprland-protocols + hyprwayland-scanner + meson + ninja + xorgproto) +optdepends=('hyprpm: build and install plugins' + 'hyprshutdown: clean logout and shutdown helper' + 'xdg-desktop-portal-hyprland: xdg-desktop-portal backend for hyprland') +provides=(wayland-compositor) +_archive="${pkgname^}-$pkgver" +source=("$_archive.tar.gz::$url/releases/download/v$pkgver/source-v$pkgver.tar.gz") +sha256sums=('03ad3f5ef152ff44116ffd56fcf808486211ecabf4f0ba567108ee746ba5cd2e') + +prepare() { + ln -sf hyprland-source "$_archive" + cd "$_archive" + sed -i -e '/^release:/{n;s/-D/-DCMAKE_SKIP_RPATH=ON -D/}' Makefile + sed -i -e '/find_package.glaze/s/7...<8 //' {.,hyprpm,start}/CMakeLists.txt +} + +build() { + cd "$_archive" + make release PREFIX=/usr +} + +package_hyprland() { + cd "$_archive" + make DESTDIR="$pkgdir" install + rm -fv "$pkgdir/usr/include/hyprland/src/version.h.in" + find $pkgdir -name '*hyprpm*' -delete + install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname/" LICENSE +} + +package_hyprpm() { + pkgdesc='Plugin manager for Hyprland' + depends=(cmake + cpio + glaze + hyprland + hyprland-protocols + hyprwayland-scanner + meson) + cd "$_archive" + make DESTDIR="$pkgdir" install + find $pkgdir -type f,l -not -name '*hyprpm*' -delete + find $pkgdir -type d -empty -delete + install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname/" LICENSE +} diff --git a/pkgbuilds/hyprtoolkit/.omarchy/package.json b/pkgbuilds/hyprtoolkit/.omarchy/package.json new file mode 100644 index 00000000..ee5e6cbf --- /dev/null +++ b/pkgbuilds/hyprtoolkit/.omarchy/package.json @@ -0,0 +1,21 @@ +{ + "source": "local", + "release_ring": "fast", + "upstream": { + "git_tags": "https://github.com/hyprwm/hyprtoolkit.git", + "tag_pattern": "v{pkgver}", + "sources": { + "any": [ + "https://github.com/hyprwm/hyprtoolkit/archive/v{pkgver}/hyprtoolkit-{pkgver}.tar.gz" + ] + } + }, + "rebuild_on": [ + "aquamarine" + ], + "rebuilt_against": { + "aarch64": { + "aquamarine": "0.15.0-2" + } + } +} diff --git a/pkgbuilds/hyprtoolkit/PKGBUILD b/pkgbuilds/hyprtoolkit/PKGBUILD new file mode 100644 index 00000000..c6f1b490 --- /dev/null +++ b/pkgbuilds/hyprtoolkit/PKGBUILD @@ -0,0 +1,50 @@ +# Maintainer: Caleb Maclennan + +pkgname=hyprtoolkit +pkgver=0.5.4 +pkgrel=5.1 +pkgdesc='A modern C++ Wayland-native GUI toolkit' +arch=(aarch64) +url="https://github.com/hyprwm/$pkgname" +license=(BSD-3-Clause) +depends=( + libgcc + libstdc++ + aquamarine libaquamarine.so + cairo libcairo.so + glib2 libglib-2.0.so + glibc # libc.so libm.so + hyprgraphics libhyprgraphics.so + hyprlang libhyprlang.so + hyprutils libhyprutils.so + iniparser libiniparser.so + libdrm # libdrm.so + libglvnd libEGL.so libOpenGL.so + libxkbcommon libxkbcommon.so + mesa # libgbm.so + pango libpango-1.0.so # libpango.so + pixman libpixman-1.so + wayland libwayland-client.so +) +makedepends=(cmake + hyprwayland-scanner) +provides=(libhyprtoolkit.so) +_archive="$pkgname-$pkgver" +source=("$url/archive/v$pkgver/$_archive.tar.gz") +sha256sums=('2fb59789f231c1c4e9154ceffc1e7524c0cae154807c0d57e6166806255b570f') + +build() { + cd "$_archive" + local cmake_options=( + -D CMAKE_BUILD_TYPE=None + -D CMAKE_INSTALL_PREFIX=/usr + ) + cmake -B build -W no-dev ${cmake_options[@]} + cmake --build build +} + +package() { + cd "$_archive" + DESTDIR="$pkgdir" cmake --install build + install -Dm0644 -t "$pkgdir/usr/share/licenses/$pkgname/" LICENSE +} diff --git a/pkgbuilds/limine-mkinitcpio-hook/.omarchy/patches/arm-gradle.patch b/pkgbuilds/limine-mkinitcpio-hook/.omarchy/patches/arm-gradle.patch new file mode 100644 index 00000000..d7af3e8a --- /dev/null +++ b/pkgbuilds/limine-mkinitcpio-hook/.omarchy/patches/arm-gradle.patch @@ -0,0 +1,48 @@ +--- a/PKGBUILD ++++ b/PKGBUILD +@@ -3,6 +3,7 @@ + pkgname="limine-mkinitcpio-hook" + _pkgver=1.38.0 + _extver="" ++_gradle_version=9.7.1 + pkgver="${_pkgver}${_extver}" + pkgrel=1 + pkgdesc="Install kernels for the Limine bootloader." +@@ -10,7 +11,8 @@ + url="https://gitlab.com/Zesko/limine-entry-tool" + source=("${_pkgname}::git+${url}.git#tag=${pkgver}") + source_x86_64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/graal-25.2.4/graalvm-community-jdk-25i2-25.0.4_linux-x64_bin.tar.gz") +-source_aarch64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/graal-25.2.4/graalvm-community-jdk-25i2-25.0.4_linux-aarch64_bin.tar.gz") ++source_aarch64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/graal-25.2.4/graalvm-community-jdk-25i2-25.0.4_linux-aarch64_bin.tar.gz" ++ "https://services.gradle.org/distributions/gradle-${_gradle_version}-bin.zip") + license=("GPL3") + provides=('limine-entry-tool') + options=(!debug !strip) +@@ -27,12 +29,14 @@ + 'sbctl: Signs UEFI boot files for Secure Boot when enabled' + 'journalctl-desktop-notification: Sends desktop notifications when errors occur' + ) +-makedepends=('git' 'gradle') ++makedepends=('git') ++makedepends_x86_64=('gradle') + backup=(etc/limine-entry-tool.conf) + conflicts=('limine-entry-tool') + sha256sums=('11dd8211898585f79d8361d97606965bb2ca214dccace5b133dc5121e4bb4d01') + sha256sums_x86_64=('3f4a89de8eaa96f2ed677f09957c7e872cd8467aad3537f8b5394c1b8c4b942e') +-sha256sums_aarch64=('22286f7ecd21b9aedb3226b9bf797469e1bd3eefc491e12ef3dd49b452d230b7') ++sha256sums_aarch64=('22286f7ecd21b9aedb3226b9bf797469e1bd3eefc491e12ef3dd49b452d230b7' ++ 'acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a') + + prepare() { + [[ -d "${_graalvm_version}" ]] && rm -rf "${_graalvm_version}" +@@ -48,7 +52,9 @@ + export GRAALVM_HOME="$srcdir/${_graalvm_version}" + export JAVA_HOME="${GRAALVM_HOME}" + export NATIVE_IMAGE_OPTIONS="-march=compatibility --future-defaults=all" +- /usr/bin/gradle clean nativeCompile -Dorg.gradle.java.home="${JAVA_HOME}" ++ local gradle=/usr/bin/gradle ++ [[ $CARCH == aarch64 ]] && gradle="$srcdir/gradle-${_gradle_version}/bin/gradle" ++ "$gradle" clean nativeCompile -Dorg.gradle.java.home="${JAVA_HOME}" + } + + package() { diff --git a/pkgbuilds/limine-mkinitcpio-hook/PKGBUILD b/pkgbuilds/limine-mkinitcpio-hook/PKGBUILD index e7a24b7d..b2e63c8c 100644 --- a/pkgbuilds/limine-mkinitcpio-hook/PKGBUILD +++ b/pkgbuilds/limine-mkinitcpio-hook/PKGBUILD @@ -3,14 +3,16 @@ _pkgname="limine-entry-tool" pkgname="limine-mkinitcpio-hook" _pkgver=1.38.0 _extver="" +_gradle_version=9.7.1 pkgver="${_pkgver}${_extver}" -pkgrel=1 +pkgrel=1.1 pkgdesc="Install kernels for the Limine bootloader." arch=('x86_64' 'aarch64') url="https://gitlab.com/Zesko/limine-entry-tool" source=("${_pkgname}::git+${url}.git#tag=${pkgver}") source_x86_64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/graal-25.2.4/graalvm-community-jdk-25i2-25.0.4_linux-x64_bin.tar.gz") -source_aarch64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/graal-25.2.4/graalvm-community-jdk-25i2-25.0.4_linux-aarch64_bin.tar.gz") +source_aarch64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/graal-25.2.4/graalvm-community-jdk-25i2-25.0.4_linux-aarch64_bin.tar.gz" + "https://services.gradle.org/distributions/gradle-${_gradle_version}-bin.zip") license=("GPL3") provides=('limine-entry-tool') options=(!debug !strip) @@ -27,12 +29,14 @@ optdepends=( 'sbctl: Signs UEFI boot files for Secure Boot when enabled' 'journalctl-desktop-notification: Sends desktop notifications when errors occur' ) -makedepends=('git' 'gradle') +makedepends=('git') +makedepends_x86_64=('gradle') backup=(etc/limine-entry-tool.conf) conflicts=('limine-entry-tool') sha256sums=('11dd8211898585f79d8361d97606965bb2ca214dccace5b133dc5121e4bb4d01') sha256sums_x86_64=('3f4a89de8eaa96f2ed677f09957c7e872cd8467aad3537f8b5394c1b8c4b942e') -sha256sums_aarch64=('22286f7ecd21b9aedb3226b9bf797469e1bd3eefc491e12ef3dd49b452d230b7') +sha256sums_aarch64=('22286f7ecd21b9aedb3226b9bf797469e1bd3eefc491e12ef3dd49b452d230b7' + 'acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a') prepare() { [[ -d "${_graalvm_version}" ]] && rm -rf "${_graalvm_version}" @@ -48,7 +52,9 @@ build() { export GRAALVM_HOME="$srcdir/${_graalvm_version}" export JAVA_HOME="${GRAALVM_HOME}" export NATIVE_IMAGE_OPTIONS="-march=compatibility --future-defaults=all" - /usr/bin/gradle clean nativeCompile -Dorg.gradle.java.home="${JAVA_HOME}" + local gradle=/usr/bin/gradle + [[ $CARCH == aarch64 ]] && gradle="$srcdir/gradle-${_gradle_version}/bin/gradle" + "$gradle" clean nativeCompile -Dorg.gradle.java.home="${JAVA_HOME}" } package() { diff --git a/pkgbuilds/limine-snapper-sync/.omarchy/patches/arm-gradle.patch b/pkgbuilds/limine-snapper-sync/.omarchy/patches/arm-gradle.patch new file mode 100644 index 00000000..76ad31a0 --- /dev/null +++ b/pkgbuilds/limine-snapper-sync/.omarchy/patches/arm-gradle.patch @@ -0,0 +1,46 @@ +--- a/PKGBUILD ++++ b/PKGBUILD +@@ -2,12 +2,14 @@ + pkgname="limine-snapper-sync" + pkgver=1.31.0 + pkgrel=1 ++_gradle_version=9.7.1 + pkgdesc="Integrates Limine boot entries with Snapper snapshots." + arch=('x86_64' 'aarch64') + url="https://gitlab.com/Zesko/limine-snapper-sync" + source=("${pkgname}::git+${url}.git#tag=${pkgver}") + source_x86_64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-25.0.2/graalvm-community-jdk-25.0.2_linux-x64_bin.tar.gz") +-source_aarch64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-25.0.2/graalvm-community-jdk-25.0.2_linux-aarch64_bin.tar.gz") ++source_aarch64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-25.0.2/graalvm-community-jdk-25.0.2_linux-aarch64_bin.tar.gz" ++ "https://services.gradle.org/distributions/gradle-${_gradle_version}-bin.zip") + license=("GPL3") + options=(!debug !strip) + _graalvm_version=graalvm_ce_jdk25 +@@ -25,12 +27,14 @@ + 'b3sum: Fast Blake3 hash function to prevent duplication.' + 'xxhash: Fast hashing utility for deduplication with shorter hashes.' + ) +-makedepends=('git' 'gradle') ++makedepends=('git') ++makedepends_x86_64=('gradle') + backup=(etc/limine-snapper-sync.conf) + conflicts=('limine-snapper-cli' 'limine-snapper-sync-git') + sha256sums=('ed236f1bbab966950bf11ba5a7958e97a76e66db7fd647b7737bab4b48c9fc40') + sha256sums_x86_64=('e0be791c8fda4d03b6b0a0cb824fef3149736170057b3a515252b44419606af0') +-sha256sums_aarch64=('b4580d9f223d0a4b3a1757e58b18ff4c1db950e67e105fc5cb741457d2384a71') ++sha256sums_aarch64=('b4580d9f223d0a4b3a1757e58b18ff4c1db950e67e105fc5cb741457d2384a71' ++ 'acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a') + + prepare() { + [[ -d "${_graalvm_version}" ]] && rm -rf "${_graalvm_version}" +@@ -46,7 +50,9 @@ + export GRAALVM_HOME="$srcdir/${_graalvm_version}" + export JAVA_HOME="${GRAALVM_HOME}" + export NATIVE_IMAGE_OPTIONS="-march=compatibility" +- /usr/bin/gradle clean nativeCompile -Dorg.gradle.java.home="${JAVA_HOME}" ++ local gradle=/usr/bin/gradle ++ [[ $CARCH == aarch64 ]] && gradle="$srcdir/gradle-${_gradle_version}/bin/gradle" ++ "$gradle" clean nativeCompile -Dorg.gradle.java.home="${JAVA_HOME}" + } + + package() { diff --git a/pkgbuilds/limine-snapper-sync/PKGBUILD b/pkgbuilds/limine-snapper-sync/PKGBUILD index c8f19624..97d9add2 100644 --- a/pkgbuilds/limine-snapper-sync/PKGBUILD +++ b/pkgbuilds/limine-snapper-sync/PKGBUILD @@ -1,13 +1,15 @@ # Maintainer: Zesko pkgname="limine-snapper-sync" pkgver=1.31.0 -pkgrel=1 +pkgrel=1.1 +_gradle_version=9.7.1 pkgdesc="Integrates Limine boot entries with Snapper snapshots." arch=('x86_64' 'aarch64') url="https://gitlab.com/Zesko/limine-snapper-sync" source=("${pkgname}::git+${url}.git#tag=${pkgver}") source_x86_64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-25.0.2/graalvm-community-jdk-25.0.2_linux-x64_bin.tar.gz") -source_aarch64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-25.0.2/graalvm-community-jdk-25.0.2_linux-aarch64_bin.tar.gz") +source_aarch64=("https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-25.0.2/graalvm-community-jdk-25.0.2_linux-aarch64_bin.tar.gz" + "https://services.gradle.org/distributions/gradle-${_gradle_version}-bin.zip") license=("GPL3") options=(!debug !strip) _graalvm_version=graalvm_ce_jdk25 @@ -25,12 +27,14 @@ optdepends=( 'b3sum: Fast Blake3 hash function to prevent duplication.' 'xxhash: Fast hashing utility for deduplication with shorter hashes.' ) -makedepends=('git' 'gradle') +makedepends=('git') +makedepends_x86_64=('gradle') backup=(etc/limine-snapper-sync.conf) conflicts=('limine-snapper-cli' 'limine-snapper-sync-git') sha256sums=('ed236f1bbab966950bf11ba5a7958e97a76e66db7fd647b7737bab4b48c9fc40') sha256sums_x86_64=('e0be791c8fda4d03b6b0a0cb824fef3149736170057b3a515252b44419606af0') -sha256sums_aarch64=('b4580d9f223d0a4b3a1757e58b18ff4c1db950e67e105fc5cb741457d2384a71') +sha256sums_aarch64=('b4580d9f223d0a4b3a1757e58b18ff4c1db950e67e105fc5cb741457d2384a71' + 'acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a') prepare() { [[ -d "${_graalvm_version}" ]] && rm -rf "${_graalvm_version}" @@ -46,7 +50,9 @@ build() { export GRAALVM_HOME="$srcdir/${_graalvm_version}" export JAVA_HOME="${GRAALVM_HOME}" export NATIVE_IMAGE_OPTIONS="-march=compatibility" - /usr/bin/gradle clean nativeCompile -Dorg.gradle.java.home="${JAVA_HOME}" + local gradle=/usr/bin/gradle + [[ $CARCH == aarch64 ]] && gradle="$srcdir/gradle-${_gradle_version}/bin/gradle" + "$gradle" clean nativeCompile -Dorg.gradle.java.home="${JAVA_HOME}" } package() { diff --git a/pkgbuilds/sunshine/.omarchy/patches/arm-dependencies.patch b/pkgbuilds/sunshine/.omarchy/patches/arm-dependencies.patch new file mode 100644 index 00000000..8345fc50 --- /dev/null +++ b/pkgbuilds/sunshine/.omarchy/patches/arm-dependencies.patch @@ -0,0 +1,57 @@ +--- a/PKGBUILD ++++ b/PKGBUILD +@@ -36,7 +36,6 @@ depends=( + 'libcap' + 'libdrm' + 'libevdev' +- 'libmfx' + 'libnotify' + 'libpipewire' + 'libpulse' +@@ -53,6 +52,10 @@ depends=( + 'which' + ) + ++depends_x86_64=( ++ 'libmfx' ++) ++ + makedepends=( + 'appstream' + 'appstream-glib' +@@ -69,6 +72,10 @@ makedepends=( + 'shaderc' + ) + ++makedepends_aarch64=( ++ 'vulkan-headers' ++) ++ + checkdepends=( + 'gcovr' + ) +@@ -111,7 +118,12 @@ if [ -n "${optdepends+x}" ]; then + + prepare() { + cd "$pkgname" +- git submodule update --recursive --init ++ if [[ $CARCH == aarch64 ]]; then ++ git submodule update --init ++ git -C third-party/moonlight-common-c submodule update --init enet ++ else ++ git submodule update --recursive --init ++ fi + + # Backport https://github.com/LizardByte/Sunshine/commit/060b6b07d376e9ef4c4ce4a5c6d648d08fe0eab9 + patch -Np1 -i "${srcdir}/2026.516.143833-060b6b07-remove-setuptools-from-glad-dependencies.patch" +@@ -145,6 +157,10 @@ build() { + -D SUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' + ) + ++ if [[ $CARCH == aarch64 ]]; then ++ _cmake_options+=(-D SUNSHINE_SYSTEM_VULKAN_HEADERS=ON) ++ fi ++ + if [[ "${_use_cuda::1}" != "t" ]]; then + _cmake_options+=(-DSUNSHINE_ENABLE_CUDA=OFF -DCUDA_FAIL_ON_MISSING=OFF) + else diff --git a/pkgbuilds/sunshine/PKGBUILD b/pkgbuilds/sunshine/PKGBUILD index e6100a62..a6b3d23d 100644 --- a/pkgbuilds/sunshine/PKGBUILD +++ b/pkgbuilds/sunshine/PKGBUILD @@ -10,7 +10,7 @@ pkgname='sunshine' pkgver=2026.516.143833 -pkgrel=4 +pkgrel=4.1 pkgdesc="Self-hosted game stream host for Moonlight" arch=('x86_64' 'aarch64') url=https://app.lizardbyte.dev/Sunshine @@ -36,7 +36,6 @@ depends=( 'libcap' 'libdrm' 'libevdev' - 'libmfx' 'libnotify' 'libpipewire' 'libpulse' @@ -55,6 +54,10 @@ depends=( 'which' ) +depends_x86_64=( + 'libmfx' +) + makedepends=( 'appstream' 'appstream-glib' @@ -69,6 +72,10 @@ makedepends=( 'shaderc' ) +makedepends_aarch64=( + 'vulkan-headers' +) + checkdepends=( 'gcovr' ) @@ -123,7 +130,12 @@ fi prepare() { cd "$pkgname" - git submodule update --recursive --init + if [[ $CARCH == aarch64 ]]; then + git submodule update --init + git -C third-party/moonlight-common-c submodule update --init enet + else + git submodule update --recursive --init + fi # Backport https://github.com/LizardByte/Sunshine/commit/060b6b07d376e9ef4c4ce4a5c6d648d08fe0eab9 patch -Np1 -i "${srcdir}/2026.516.143833-060b6b07-remove-setuptools-from-glad-dependencies.patch" @@ -159,6 +171,10 @@ build() { -D SUNSHINE_PUBLISHER_ISSUE_URL='https://app.lizardbyte.dev/support' ) + if [[ $CARCH == aarch64 ]]; then + _cmake_options+=(-D SUNSHINE_SYSTEM_VULKAN_HEADERS=ON) + fi + if [[ "${_use_cuda::1}" != "t" ]]; then _cmake_options+=(-DSUNSHINE_ENABLE_CUDA=OFF -DCUDA_FAIL_ON_MISSING=OFF) else diff --git a/pkgbuilds/voxtype-bin/.omarchy/package.json b/pkgbuilds/voxtype-bin/.omarchy/package.json index 0e2fc7fa..778fbb52 100644 --- a/pkgbuilds/voxtype-bin/.omarchy/package.json +++ b/pkgbuilds/voxtype-bin/.omarchy/package.json @@ -1,5 +1,82 @@ { - "source": "aur", + "source": "local", "release_ring": "fast", - "upstream_commit": "6dbf4bedbba0a2f90d8e813becfa5ba289653562" + "min_release_age": "24h", + "upstream": { + "github": "peteonrails/voxtype", + "digests": true, + "latest_only": true, + "assets": { + "x86_64": [ + "voxtype-{pkgver}-linux-x86_64-avx2", + "voxtype-{pkgver}-linux-x86_64-avx2.asc", + "voxtype-{pkgver}-linux-x86_64-avx512", + "voxtype-{pkgver}-linux-x86_64-avx512.asc", + "voxtype-{pkgver}-linux-x86_64-vulkan", + "voxtype-{pkgver}-linux-x86_64-vulkan.asc", + "voxtype-{pkgver}-linux-x86_64-onnx-avx2", + "voxtype-{pkgver}-linux-x86_64-onnx-avx2.asc", + "voxtype-{pkgver}-linux-x86_64-onnx-avx512", + "voxtype-{pkgver}-linux-x86_64-onnx-avx512.asc", + "voxtype-{pkgver}-linux-x86_64-onnx-cuda-12", + "voxtype-{pkgver}-linux-x86_64-onnx-cuda-12.asc", + "voxtype-{pkgver}-linux-x86_64-onnx-cuda-12.libonnxruntime_providers_cuda.so", + "voxtype-{pkgver}-linux-x86_64-onnx-cuda-12.libonnxruntime_providers_shared.so", + "voxtype-{pkgver}-linux-x86_64-onnx-cuda-13", + "voxtype-{pkgver}-linux-x86_64-onnx-cuda-13.asc", + "voxtype-{pkgver}-linux-x86_64-onnx-cuda-13.libonnxruntime_providers_cuda.so", + "voxtype-{pkgver}-linux-x86_64-onnx-cuda-13.libonnxruntime_providers_shared.so", + "voxtype-{pkgver}-linux-x86_64-onnx-cuda-13.libonnxruntime.so.1.24.4", + "voxtype-{pkgver}-linux-x86_64-onnx-migraphx", + "voxtype-{pkgver}-linux-x86_64-onnx-migraphx.asc", + "voxtype-{pkgver}-linux-x86_64-onnx-migraphx.libonnxruntime_providers_migraphx.so", + "voxtype-{pkgver}-linux-x86_64-onnx-migraphx.libonnxruntime_providers_shared.so", + "voxtype-{pkgver}-linux-x86_64-osd", + "voxtype-{pkgver}-linux-x86_64-osd.asc", + "voxtype-{pkgver}-linux-x86_64-osd-gtk4", + "voxtype-{pkgver}-linux-x86_64-osd-gtk4.asc", + "voxtype-{pkgver}-linux-x86_64-osd-quickshell", + "voxtype-{pkgver}-linux-x86_64-osd-quickshell.asc", + "voxtype-{pkgver}-linux-x86_64-audio-bridge", + "voxtype-{pkgver}-linux-x86_64-audio-bridge.asc" + ], + "aarch64": [ + "voxtype-{pkgver}-linux-aarch64-cpu", + "voxtype-{pkgver}-linux-aarch64-cpu.asc", + "voxtype-{pkgver}-linux-aarch64-onnx", + "voxtype-{pkgver}-linux-aarch64-onnx.asc", + "voxtype-{pkgver}-linux-aarch64-osd", + "voxtype-{pkgver}-linux-aarch64-osd.asc", + "voxtype-{pkgver}-linux-aarch64-osd-gtk4", + "voxtype-{pkgver}-linux-aarch64-osd-gtk4.asc", + "voxtype-{pkgver}-linux-aarch64-osd-quickshell", + "voxtype-{pkgver}-linux-aarch64-osd-quickshell.asc", + "voxtype-{pkgver}-linux-aarch64-audio-bridge", + "voxtype-{pkgver}-linux-aarch64-audio-bridge.asc" + ] + }, + "sources": { + "any": [ + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/config/default.toml", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/packaging/systemd/voxtype.service", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/packaging/completions/voxtype.bash", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/packaging/completions/voxtype.zsh", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/packaging/completions/voxtype.fish", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/LICENSE", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/README.md", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/packaging/voxtype-configure.desktop", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/packaging/scripts/voxtype-configure-launcher", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/quickshell/shell.qml", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/quickshell/OsdSurface.qml", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/quickshell/EnginePicker.qml", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/quickshell/MeetingControls.qml", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/quickshell/voxtype-shared/Theme.qml", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/quickshell/voxtype-shared/StateReader.qml", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/quickshell/voxtype-shared/AudioBridge.qml", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/quickshell/voxtype-shared/StyleLoader.qml", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/quickshell/voxtype-shared/RecipeRenderer.qml", + "https://raw.githubusercontent.com/peteonrails/voxtype/{tag}/quickshell/voxtype-shared/qmldir" + ] + } + } } diff --git a/pkgbuilds/voxtype-bin/PKGBUILD b/pkgbuilds/voxtype-bin/PKGBUILD index 62ad9b9f..5333cd28 100644 --- a/pkgbuilds/voxtype-bin/PKGBUILD +++ b/pkgbuilds/voxtype-bin/PKGBUILD @@ -3,7 +3,7 @@ pkgname=voxtype-bin pkgver=1.0.1 pkgrel=1 pkgdesc="Push-to-talk voice-to-text for Linux (pre-built binaries)" -arch=('x86_64') +arch=('x86_64' 'aarch64') url="https://voxtype.io" license=('MIT') depends=( @@ -47,7 +47,7 @@ validpgpkeys=( '9CCF7915B750CAE8B095ED1AA3FC9F33FD209279' ) _github="https://github.com/peteonrails/voxtype/releases/download/v$pkgver" -source=( +source_x86_64=( # Whisper binaries "voxtype-$pkgver-avx2::$_github/voxtype-$pkgver-linux-x86_64-avx2" "voxtype-$pkgver-avx2.asc::$_github/voxtype-$pkgver-linux-x86_64-avx2.asc" @@ -97,7 +97,23 @@ source=( "voxtype-$pkgver-osd-quickshell.asc::$_github/voxtype-$pkgver-linux-x86_64-osd-quickshell.asc" "voxtype-$pkgver-audio-bridge::$_github/voxtype-$pkgver-linux-x86_64-audio-bridge" "voxtype-$pkgver-audio-bridge.asc::$_github/voxtype-$pkgver-linux-x86_64-audio-bridge.asc" - # Config and support files +) +source_aarch64=( + "voxtype-$pkgver-cpu::$_github/voxtype-$pkgver-linux-aarch64-cpu" + "voxtype-$pkgver-cpu.asc::$_github/voxtype-$pkgver-linux-aarch64-cpu.asc" + "voxtype-$pkgver-onnx::$_github/voxtype-$pkgver-linux-aarch64-onnx" + "voxtype-$pkgver-onnx.asc::$_github/voxtype-$pkgver-linux-aarch64-onnx.asc" + "voxtype-$pkgver-osd::$_github/voxtype-$pkgver-linux-aarch64-osd" + "voxtype-$pkgver-osd.asc::$_github/voxtype-$pkgver-linux-aarch64-osd.asc" + "voxtype-$pkgver-osd-gtk4::$_github/voxtype-$pkgver-linux-aarch64-osd-gtk4" + "voxtype-$pkgver-osd-gtk4.asc::$_github/voxtype-$pkgver-linux-aarch64-osd-gtk4.asc" + "voxtype-$pkgver-osd-quickshell::$_github/voxtype-$pkgver-linux-aarch64-osd-quickshell" + "voxtype-$pkgver-osd-quickshell.asc::$_github/voxtype-$pkgver-linux-aarch64-osd-quickshell.asc" + "voxtype-$pkgver-audio-bridge::$_github/voxtype-$pkgver-linux-aarch64-audio-bridge" + "voxtype-$pkgver-audio-bridge.asc::$_github/voxtype-$pkgver-linux-aarch64-audio-bridge.asc" +) +source=( + # Architecture-independent config and support files "config-$pkgver.toml::https://raw.githubusercontent.com/peteonrails/voxtype/v$pkgver/config/default.toml" "voxtype-$pkgver.service::https://raw.githubusercontent.com/peteonrails/voxtype/v$pkgver/packaging/systemd/voxtype.service" "voxtype-$pkgver.bash::https://raw.githubusercontent.com/peteonrails/voxtype/v$pkgver/packaging/completions/voxtype.bash" @@ -125,46 +141,62 @@ source=( "quickshell-voxtype-shared-RecipeRenderer-$pkgver.qml::https://raw.githubusercontent.com/peteonrails/voxtype/v$pkgver/quickshell/voxtype-shared/RecipeRenderer.qml" "quickshell-voxtype-shared-qmldir-$pkgver::https://raw.githubusercontent.com/peteonrails/voxtype/v$pkgver/quickshell/voxtype-shared/qmldir" ) -sha256sums=( +sha256sums_x86_64=( # Whisper binaries 'cb3843a894ef47aca230b30bb1c45c2ef8e0d015adf2fa754d60e55123165fd0' # voxtype-avx2 - 'SKIP' # voxtype-avx2.asc + '21357d39412619e8a3fc473153e6e2a28b219aa02007682737afedd73a08bdbe' # voxtype-avx2.asc '77d49275ae4c3a6d93671014278b0dabd69ba52e65d2ee6ab3ec1bff0af34d55' # voxtype-avx512 - 'SKIP' # voxtype-avx512.asc + 'e3421641b4b5fa5d42ee0698eb43552e177f8b095a1acea32445693404b4fd54' # voxtype-avx512.asc 'c569d038057464aa60290296794bcbd79b928ee0efd038e33062a4c015558ed8' # voxtype-vulkan - 'SKIP' # voxtype-vulkan.asc + 'a82bd4108dc70d7d2b6e25a83a6744170502f5110780d4a2e97a3e76047ea60d' # voxtype-vulkan.asc # ONNX CPU binaries '19b8716ff7bd388ffb18843fc0ee5e614faa84544d22918ecf72a7c3d6c02929' # voxtype-onnx-avx2 - 'SKIP' # voxtype-onnx-avx2.asc + '3021aa3a13555bdab0ecccfb522db8391d7e05810d218608a6372477f11cb2af' # voxtype-onnx-avx2.asc '69a6e6fc1c04a55eeb62cf1eaa5fe81873c70eabffff120ce1a4ff7ef62fd559' # voxtype-onnx-avx512 - 'SKIP' # voxtype-onnx-avx512.asc + '305d33b6f4dc8ac7a38a5f522c1af95b462355fa057b2eb39d3add59303145e1' # voxtype-onnx-avx512.asc # ONNX CUDA 12 binary + companion .so 'e7180f17add10bb441368d8495fc99bde0ba1a1978ea5f7a52d7e626779eca16' # voxtype-onnx-cuda-12 - 'SKIP' # voxtype-onnx-cuda-12.asc + '34f7242fe16867ab029af9ed4854d3930aa2ed91bc37e062962edbfc65522af5' # voxtype-onnx-cuda-12.asc 'a8584727d51ba646ac63fc991c2f36ad6cd5b8cc8b1141896e46938700b888d0' # cuda-12.libonnxruntime_providers_cuda.so '1b028afc079628d76a28d7eb09700a4baead4a27f9634ba82c35398486134114' # cuda-12.libonnxruntime_providers_shared.so # ONNX CUDA 13 binary + companion .so + dlopen-target libonnxruntime '4de2aac42b67a05c34ac0b23f771fc83968670fd77d14781cbf4483524304cd1' # voxtype-onnx-cuda-13 - 'SKIP' # voxtype-onnx-cuda-13.asc + 'adb768be0b115f38bc28ea35614ae59e1c1adfb6e11ae08a252c45d52932fab2' # voxtype-onnx-cuda-13.asc 'b6cb7744d0efd2faced5c83ead374c13e7f2630b5a249ffaf393cdb1e092c92b' # cuda-13.libonnxruntime_providers_cuda.so 'c6a12593396095f5670160e284c35d1700b7708cf3037b7042e2a5200ccae772' # cuda-13.libonnxruntime_providers_shared.so '1aacefdf0b4afa145d410b2381bbc3db3d978c485fb182c42a2b0b09f91f5310' # cuda-13.libonnxruntime.so.1.24.4 # ONNX MIGraphX binary + companion .so 'f7a4cc0deaf35110b5106f1fc1f83dded41c0b1780f9fb5aa9cccc3c62e5eb6a' # voxtype-onnx-migraphx - 'SKIP' # voxtype-onnx-migraphx.asc + '9e9c767605923c084c97659326cb0f03ad4e5db0aded4da052fa208b4de9cf45' # voxtype-onnx-migraphx.asc 'ddd67e6193ade819ee21f1706d1b03b9151f1d2d2843701e2d19d8b183631707' # migraphx.libonnxruntime_providers_migraphx.so '17f7cf47ad0d7b5ac895ae588fd62c7f85a13842588161b6a24c7d480f062be4' # migraphx.libonnxruntime_providers_shared.so # OSD launcher + GTK4 frontend '7250027b1672507a6d584f795731c87e1d3b5c1de891438bd55e34b136a2d5cc' # voxtype-osd - 'SKIP' # voxtype-osd.asc + '1a944200fbac1e773c70d036075745b0e868885443e29c30797f262d44928cc1' # voxtype-osd.asc '74fb0f6ad87feb0c1c9e06a8b28a7f7ecee101caef4248f77dcf613b5271238e' # voxtype-osd-gtk4 - 'SKIP' # voxtype-osd-gtk4.asc + '60224685717fea0582d570160282e9e215cd4713b3bdb695edc7bae8b6860cac' # voxtype-osd-gtk4.asc # Quickshell OSD launcher + audio-bridge sidecar (new in v0.7.5) 'b809c5140e844a6add801d7e592775cd89af8cce73fa399b6a3aec15dfd09533' # voxtype-osd-quickshell - 'SKIP' # voxtype-osd-quickshell.asc + '39412691ab1f8ffe4fa6bf1eb9763c29f94b8892d1ba59fd49dc65dd0d80297a' # voxtype-osd-quickshell.asc '45776290e364194d83a8b89445166406c278e890507bf07ec52a5f0e8fa57720' # voxtype-audio-bridge - 'SKIP' # voxtype-audio-bridge.asc - # Config and support files + '5be1174a78d70b78fd8d923c88e3406962443b39a1c452eb985b1e2484ef5668' # voxtype-audio-bridge.asc +) +sha256sums_aarch64=( + 'b5e31a85aaa952d1a78c12b8a16ba5cbdcd92eb31adc7d1a908f3c9d06edd4f1' # voxtype-cpu + '3cd2f3fafca40e394a42b90dac031c67160256898a7f4a52893585b99f8f74bb' # voxtype-cpu.asc + 'c3771f3e568629178201990976520f88da6d7599ec2d9e404a137570d6c1e108' # voxtype-onnx + 'cd58773349eac0108cdad0efcc78d13f5d7c21623117381afa99b912c397b94a' # voxtype-onnx.asc + 'ea910d4fd1fe331d38dbed1c3a639cb7e0c04542919192ff6f74be2139afe3c6' # voxtype-osd + 'e079ebdd567e318502b710af95e4987fe13623d65673e21877afe88fe18bda55' # voxtype-osd.asc + '0d2148e0cd32bac538692470edc06aa9a2f5c6a891aaa373f59fbe78c247fae3' # voxtype-osd-gtk4 + '0ce841f2caa9a1a7e8294a521bef0bab8823dddacd36fff66012b1127e0c1957' # voxtype-osd-gtk4.asc + '097bd518d5e2eac2c3cbad714b65dd8058c818dcb4d900b9a16e442af7d65b8a' # voxtype-osd-quickshell + 'f48b8071f78fde0b2d20072e875a8b8e0d8fab4caf6c8ec91cd8e2aa0b031063' # voxtype-osd-quickshell.asc + '35170ad89fea2874fce0f08758ccc2164892ed643aacae632bcfbc6f10433976' # voxtype-audio-bridge + '787965900647ee9b04c8b65123d04b63e38636bfc671fa3b53360823f886b42c' # voxtype-audio-bridge.asc +) +sha256sums=( + # Architecture-independent config and support files 'f4b2bccd56b31a6a50e1c0a8b6b72383dc1e636f9ccb8cb070442d58b7314579' # config/default.toml '531c3658e229619e56bb01659fb81f401767b85e1d6e2acd1ac67ee3414a168c' # voxtype.service '65c95805d9b03ccc2fadb9d63a03ab79974b00091df8457ee8ef290ec6bd5b12' # voxtype.bash @@ -189,16 +221,20 @@ sha256sums=( ) package() { - # Install Whisper CPU binaries to /usr/lib/voxtype/ - install -Dm755 "$srcdir/voxtype-$pkgver-avx2" "$pkgdir/usr/lib/voxtype/voxtype-avx2" - install -Dm755 "$srcdir/voxtype-$pkgver-avx512" "$pkgdir/usr/lib/voxtype/voxtype-avx512" + if [[ "$CARCH" == aarch64 ]]; then + install -Dm755 "$srcdir/voxtype-$pkgver-cpu" "$pkgdir/usr/lib/voxtype/voxtype-cpu" + install -Dm755 "$srcdir/voxtype-$pkgver-onnx" "$pkgdir/usr/lib/voxtype/voxtype-onnx" + else + # Install Whisper CPU binaries to /usr/lib/voxtype/ + install -Dm755 "$srcdir/voxtype-$pkgver-avx2" "$pkgdir/usr/lib/voxtype/voxtype-avx2" + install -Dm755 "$srcdir/voxtype-$pkgver-avx512" "$pkgdir/usr/lib/voxtype/voxtype-avx512" - # Install Whisper Vulkan GPU binary - install -Dm755 "$srcdir/voxtype-$pkgver-vulkan" "$pkgdir/usr/lib/voxtype/voxtype-vulkan" + # Install Whisper Vulkan GPU binary + install -Dm755 "$srcdir/voxtype-$pkgver-vulkan" "$pkgdir/usr/lib/voxtype/voxtype-vulkan" - # Install ONNX CPU binaries (no GPU EP, no companion .so files) - install -Dm755 "$srcdir/voxtype-$pkgver-onnx-avx2" "$pkgdir/usr/lib/voxtype/voxtype-onnx-avx2" - install -Dm755 "$srcdir/voxtype-$pkgver-onnx-avx512" "$pkgdir/usr/lib/voxtype/voxtype-onnx-avx512" + # Install ONNX CPU binaries (no GPU EP, no companion .so files) + install -Dm755 "$srcdir/voxtype-$pkgver-onnx-avx2" "$pkgdir/usr/lib/voxtype/voxtype-onnx-avx2" + install -Dm755 "$srcdir/voxtype-$pkgver-onnx-avx512" "$pkgdir/usr/lib/voxtype/voxtype-onnx-avx512" # GPU-using ONNX binaries each live in their own subdirectory alongside # the EP companion shared libs they dlopen at runtime. ort 2.0.0-rc.12's @@ -248,7 +284,8 @@ package() { # voxtype-onnx-rocm name. The AMD GPU EP changed from ROCm to MIGraphX # in v0.7.0; ship one release with both names to soften the transition. # Drop in v0.8.0. - ln -sf "voxtype-onnx-migraphx" "$pkgdir/usr/lib/voxtype/voxtype-onnx-rocm" + ln -sf "voxtype-onnx-migraphx" "$pkgdir/usr/lib/voxtype/voxtype-onnx-rocm" + fi # /usr/bin/voxtype symlink and the unversioned voxtype-onnx-cuda symlink # are created by the .install script's post_install/post_upgrade hooks diff --git a/pkgbuilds/xpadneo-dkms/.omarchy/patches/arm-kernel-headers.patch b/pkgbuilds/xpadneo-dkms/.omarchy/patches/arm-kernel-headers.patch new file mode 100644 index 00000000..9dcc175f --- /dev/null +++ b/pkgbuilds/xpadneo-dkms/.omarchy/patches/arm-kernel-headers.patch @@ -0,0 +1,11 @@ +--- a/PKGBUILD ++++ b/PKGBUILD +@@ -14,7 +14,9 @@ arch=(any) + license=('GPL-2.0-only AND GPL-3.0-or-later') +-checkdepends=('dkms' 'fakeroot' 'LINUX-HEADERS') ++checkdepends=('dkms' 'fakeroot') ++checkdepends_x86_64=('LINUX-HEADERS') ++checkdepends_aarch64=('linux-aarch64-headers') + source=("xpadneo-v${pkgver}.tar.gz::${url}/archive/v${pkgver}.tar.gz") + b2sums=('2858e466c5fde7e3d06d878dd7fb695751375e11a112bd3d389f9db5a82ef3ffcc1e3431b213f4807aeb8f05fd4280c7ca2a0a56305b0e984eb9b70df4843735') + diff --git a/pkgbuilds/xpadneo-dkms/PKGBUILD b/pkgbuilds/xpadneo-dkms/PKGBUILD index d14f85cb..3117cde6 100644 --- a/pkgbuilds/xpadneo-dkms/PKGBUILD +++ b/pkgbuilds/xpadneo-dkms/PKGBUILD @@ -9,11 +9,13 @@ pkgname=xpadneo-dkms pkgdesc='Advanced Linux Driver for Xbox One Wireless Gamepad' pkgver=0.10.4 -pkgrel=1 +pkgrel=1.1 url='https://github.com/atar-axis/xpadneo' arch=(any) license=('GPL-2.0-only AND GPL-3.0-or-later') -checkdepends=('dkms' 'fakeroot' 'LINUX-HEADERS') +checkdepends=('dkms' 'fakeroot') +checkdepends_x86_64=('LINUX-HEADERS') +checkdepends_aarch64=('linux-aarch64-headers') source=("xpadneo-v${pkgver}.tar.gz::${url}/archive/v${pkgver}.tar.gz") b2sums=('2858e466c5fde7e3d06d878dd7fb695751375e11a112bd3d389f9db5a82ef3ffcc1e3431b213f4807aeb8f05fd4280c7ca2a0a56305b0e984eb9b70df4843735') diff --git a/systemd/omarchy-auto-release-edge.service b/systemd/omarchy-auto-release-edge.service index 7658dfb7..3537ed95 100644 --- a/systemd/omarchy-auto-release-edge.service +++ b/systemd/omarchy-auto-release-edge.service @@ -7,7 +7,7 @@ Wants=network-online.target Type=oneshot ExecStart=/bin/bash -c 'source /root/.omarchy/build-credentials && /root/omarchy-pkgs/bin/auto-release edge' Environment=OMARCHY_STATE_DIR=/root/.state -TimeoutStartSec=7200 +TimeoutStartSec=43200 [Install] WantedBy=multi-user.target diff --git a/systemd/omarchy-auto-release-rc.service b/systemd/omarchy-auto-release-rc.service index 0078b3d1..c57f5f73 100644 --- a/systemd/omarchy-auto-release-rc.service +++ b/systemd/omarchy-auto-release-rc.service @@ -12,7 +12,7 @@ Type=oneshot # branch's worktree with OMARCHY_RC_PINS=1. ExecStart=/bin/bash -c 'source /root/.omarchy/build-credentials && /root/omarchy-pkgs/bin/auto-release rc' Environment=OMARCHY_STATE_DIR=/root/.state -TimeoutStartSec=7200 +TimeoutStartSec=43200 [Install] WantedBy=multi-user.target diff --git a/systemd/omarchy-auto-release-stable.service b/systemd/omarchy-auto-release-stable.service index e1979e3a..110bad3b 100644 --- a/systemd/omarchy-auto-release-stable.service +++ b/systemd/omarchy-auto-release-stable.service @@ -7,7 +7,7 @@ Wants=network-online.target Type=oneshot ExecStart=/bin/bash -c 'source /root/.omarchy/build-credentials && /root/omarchy-pkgs/bin/auto-release stable' Environment=OMARCHY_STATE_DIR=/root/.state -TimeoutStartSec=7200 +TimeoutStartSec=43200 [Install] WantedBy=multi-user.target