From 2fe4803bbef65723ee02e0ca36b1eea00ab610c9 Mon Sep 17 00:00:00 2001 From: Omabot Date: Thu, 20 Aug 2026 03:44:56 -0700 Subject: [PATCH 1/4] Rebuild packages when what they link against moves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A package that links Qt private API has to be rebuilt whenever qt6-base moves, because Qt_6_PRIVATE_API symbols are not covered by the soname and pacman upgrades Qt out from under the installed binary while the dependency stays unversioned. Nothing here noticed. Both version gates ask whether the package's own source moved, and for a VCS package pinned to a commit that answer stays no through every Qt release. Unlocking the build gate would not have been enough on its own. A rebuild that reuses the published version string produces a package pacman never offers anyone, so the trigger has to edit git and bump pkgrel, which is why it sits beside sync-aur and sync-upstream rather than inside check-versions or the builder. Once pkgrel moves, both existing gates already do the right thing untouched. Packages opt in with rebuild_on in .omarchy/package.json. bin/sync-rebuilds records what each was last bumped for in rebuilt_against and compares that to core, extra and multilib, ignoring testing and kde-unstable because those are not what the builder links against. A package with no record yet is only recorded, never bumped: what its published build linked against is not knowable from here, so the first run establishes the baseline. For an AUR-synced package the bump is written as the dotted Omarchy pkgrel suffix in the metadata as well, since the next sync replaces the PKGBUILD wholesale and would otherwise drop it. 🤖 Generated by Opus 5 in Claude Code. --- .github/workflows/sync-rebuilds.yml | 99 ++++++++ README.md | 29 +++ bin/sync-rebuilds | 340 ++++++++++++++++++++++++++++ helpers/package-metadata.sh | 46 +++- 4 files changed, 513 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sync-rebuilds.yml create mode 100755 bin/sync-rebuilds diff --git a/.github/workflows/sync-rebuilds.yml b/.github/workflows/sync-rebuilds.yml new file mode 100644 index 0000000..74fc0d8 --- /dev/null +++ b/.github/workflows/sync-rebuilds.yml @@ -0,0 +1,99 @@ +name: Sync Rebuild Triggers + +on: + schedule: + # Every 6 hours, off the hour to dodge the scheduling backlog at :00 + - cron: '40 */6 * * *' + workflow_dispatch: + inputs: + packages: + description: 'Specific packages to update (space-separated, leave empty for all)' + required: false + default: '' + +jobs: + sync: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + # Runs in an Arch container because the question being asked is what the + # build container will link against, and pacman's own view of core/extra is + # the only answer that matches. + - name: Bump pkgrel for packages whose dependencies moved + run: | + docker run --rm \ + -e PACKAGES="$PACKAGES" \ + -e HOST_UID="$(id -u)" \ + -e HOST_GID="$(id -g)" \ + -v "$PWD/bin:/workspace/bin:ro" \ + -v "$PWD/helpers:/workspace/helpers:ro" \ + -v "$PWD/pkgbuilds:/workspace/pkgbuilds" \ + -w /workspace \ + archlinux:base-devel bash -lc ' + set -euo pipefail + + pacman -Syu --noconfirm jq + + groupadd -g "$HOST_GID" runner + useradd -m -u "$HOST_UID" -g "$HOST_GID" runner + chown -R runner:runner /workspace/pkgbuilds + + if [[ -n "${PACKAGES:-}" ]]; then + read -r -a package_args <<< "$PACKAGES" + runuser -u runner -- ./bin/sync-rebuilds "${package_args[@]}" + else + runuser -u runner -- ./bin/sync-rebuilds + fi + ' + env: + PACKAGES: ${{ github.event.inputs.packages }} + + - name: Check for changes + id: changes + run: | + if [ -z "$(git status --porcelain)" ]; then + echo "has_changes=false" >> "$GITHUB_OUTPUT" + else + echo "has_changes=true" >> "$GITHUB_OUTPUT" + fi + + - name: Create Pull Request + if: steps.changes.outputs.has_changes == 'true' + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore: rebuild against updated dependencies' + title: 'chore: rebuild against updated dependencies' + body: | + Automated pkgrel bump for packages that link against a dependency + which has moved in the official repositories. + + Each package names those dependencies in `rebuild_on` and carries the + versions its current pkgrel was bumped for in `rebuilt_against`. The + bump is what makes the rebuilt package an upgrade pacman will offer; + without it the build produces the version already published and no + one receives it. + branch: auto/sync-rebuilds + delete-branch: true + labels: automated + reviewers: ryanrhughes + + - name: Notify Basecamp on failure + if: failure() && env.BASECAMP_CHATBOT_URL != '' + env: + BASECAMP_CHATBOT_URL: ${{ secrets.BASECAMP_CHATBOT_URL }} + run: | + curl -s -o /dev/null \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg content \ + "🔴 Rebuild trigger sync failed
View run" \ + '{content: $content}')" \ + "$BASECAMP_CHATBOT_URL" diff --git a/README.md b/README.md index 50c03f0..c2e7eff 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,29 @@ than downloading the artifacts — see `pkgbuilds/openai-codex-desktop/.omarchy/ which reads OpenAI's Debian package index and never fetches the 750 MB of debs it describes. +### Sync Rebuild Triggers + +```bash +bin/sync-rebuilds # Bump every package whose dependencies moved +bin/sync-rebuilds quickshell-git # Update specific packages +``` + +Some packages have to be rebuilt when something they link against changes, even though nothing in their own source moved. A Qt private-API consumer is the usual case: `Qt_6_PRIVATE_API` symbols are not covered by the soname, so a qt6-base point release can leave an installed binary unable to resolve a symbol at startup, and pacman upgrades Qt out from under it because the dependency is unversioned. The package still builds from the same git commit, so nothing in the normal version check notices. + +A package names those dependencies in `.omarchy/package.json`: + +```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. When they differ it bumps pkgrel and rewrites the record. A package with no record yet is only recorded, never bumped: what its published build linked against is not knowable from here, so the first run establishes the baseline and the next change acts on it. + +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. + +For an AUR-synced package the bump is expressed as the dotted Omarchy pkgrel suffix in the metadata as well as in the PKGBUILD, because the next AUR sync replaces the PKGBUILD wholesale and would otherwise drop it. + +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. + ### Other ```bash @@ -296,6 +319,7 @@ bin/add-package # Add an AUR/local package with metadata bin/package-worktree # Create upstream/patched/current scratch workspace bin/repo remove # Remove package bin/sync-upstream # Update packages that track a vendor release feed +bin/sync-rebuilds # Bump pkgrel for packages whose dependencies moved bin/clean-docker # Clear Docker images/cache (forces fresh rebuild) ``` @@ -439,6 +463,8 @@ Fields: - `release_ring`: optional. `fast` means the package is built directly for stable as well as edge. Packages without a ring build in edge and reach stable through tested artifact promotion (`bin/repo migrate`). - `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. - `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 @@ -567,6 +593,8 @@ Packages are only rebuilt if: - PKGBUILD version is newer than repository version - Package doesn't exist in production +Neither notices a package that has to be rebuilt because something underneath it changed. That case is handled by turning it into a version change: `bin/sync-rebuilds` bumps pkgrel when a dependency named in `rebuild_on` moves. + ## Automated Releases The repository includes GitHub workflows and systemd services for automated releases. @@ -577,6 +605,7 @@ The repository includes GitHub workflows and systemd services for automated rele 1. **sync-aur.yml** (Every 6 hours): Syncs AUR packages according to `.omarchy/package.json` and opens a PR when changes are found. 2. **sync-upstream.yml** (Every 6 hours): Runs `.omarchy/upstream.sh` for packages that track a vendor release feed and opens a PR when a newer version is out. +3. **sync-rebuilds.yml** (Every 6 hours): Bumps pkgrel for packages whose `rebuild_on` dependencies have moved in the official repositories and opens a PR. #### Systemd Services diff --git a/bin/sync-rebuilds b/bin/sync-rebuilds new file mode 100755 index 0000000..c9d5044 --- /dev/null +++ b/bin/sync-rebuilds @@ -0,0 +1,340 @@ +#!/bin/bash +set -euo pipefail + +BUILD_ROOT=$(realpath "${BASH_SOURCE[0]%/*}/..") +source "$BUILD_ROOT/helpers/message-helpers.sh" +source "$BUILD_ROOT/helpers/paths.sh" +source "$BUILD_ROOT/helpers/package-metadata.sh" + +SPECIFIC_PACKAGES=() + +# Only the repositories a user actually installs from. A Qt release sitting in +# testing or kde-unstable is not what the build container will link against, and +# rebuilding for it would ship a package built against the wrong ABI. +OFFICIAL_REPOS=" core extra multilib core-debug extra-debug " + +usage() { + cat </dev/null 2>&1; then + print_error "$tool not found: reading trigger versions and ordering pkgrels both need pacman" + exit 1 + fi +done + +print_header "Rebuild Trigger Sync" + +UPDATED=0 +SKIPPED=0 +FAILED=0 +SPECIFIC_MODE=false + +# The version of a trigger package as the build container would resolve it. +# A name pacman does not know reports nothing rather than failing, so the caller +# gets to say which package was left alone instead of the run dying here. +repo_version() { + local package="$1" + local info + + info=$(LC_ALL=C pacman -Si "$package" 2>/dev/null) || return 0 + + awk -v allowed="$OFFICIAL_REPOS" ' + /^Repository[[:space:]]*:/ { repo = $3 } + /^Version[[:space:]]*:/ { + if (index(allowed, " " repo " ") > 0) { print $3; exit } + } + ' <<<"$info" +} + +pkgbuild_field() { + local package_dir="$1" + local field="$2" + + (cd "$package_dir" && env -u OMARCHY_SRC bash -c "source PKGBUILD 2>/dev/null; echo \"\${$field:-}\"") +} + +# 2 -> 3, and 1.1 -> 1.2. Anything else is a pkgrel this command has no business +# rewriting. +bump_pkgrel() { + local pkgrel="$1" + + [[ "$pkgrel" =~ ^[0-9]+(\.[0-9]+)?$ ]] || return 1 + + local head tail + if [[ "$pkgrel" == *.* ]]; then + head="${pkgrel%.*}." + tail="${pkgrel##*.}" + else + head="" + tail="$pkgrel" + fi + + echo "${head}$((tail + 1))" +} + +write_pkgrel() { + local package_dir="$1" + local pkgrel="$2" + local pkgbuild="$package_dir/PKGBUILD" + + if [[ $(grep -c '^pkgrel=' "$pkgbuild") -ne 1 ]]; then + print_error "Expected exactly one pkgrel assignment in $pkgbuild" + return 1 + fi + + # Every edit lands on a scratch copy that replaces the PKGBUILD in one rename, + # so a failed rewrite leaves the original alone rather than half updated. + local scratch="$pkgbuild.sync-rebuilds" + cp "$pkgbuild" "$scratch" || return 1 + sed -i "s/^pkgrel=.*/pkgrel=$pkgrel/" "$scratch" + + if ! bash -n "$scratch" 2>/dev/null; then + print_error "Rewritten PKGBUILD is not valid shell" + rm -f "$scratch" + return 1 + fi + + local written + written=$(CARCH=x86_64 bash -c 'source "$1" >/dev/null 2>&1 || exit 1; echo "$pkgrel"' _ "$scratch" 2>/dev/null) + if [[ "$written" != "$pkgrel" ]]; then + print_error "Rewritten PKGBUILD reads back pkgrel=$written, not $pkgrel" + rm -f "$scratch" + return 1 + fi + + chmod --reference="$pkgbuild" "$scratch" + mv "$scratch" "$pkgbuild" +} + +write_metadata() { + local package_dir="$1" + local filter="$2" + shift 2 + local metadata + metadata=$(metadata_file_for_dir "$package_dir") + + local scratch="$metadata.sync-rebuilds" + jq "$@" "$filter" "$metadata" > "$scratch" || { rm -f "$scratch"; return 1; } + chmod --reference="$metadata" "$scratch" + mv "$scratch" "$metadata" +} + +record_triggers() { + local package_dir="$1" + local current="$2" + + write_metadata "$package_dir" '.rebuilt_against = $current' --argjson current "$current" +} + +sync_package() { + local package="$1" + local package_dir="$PKGBUILDS_DIR/$package" + + if [[ ! -f "$package_dir/PKGBUILD" ]]; then + print_error "Package $package has no PKGBUILD" + ((++FAILED)) + return 0 + fi + + local triggers=() + mapfile -t triggers < <(package_rebuild_triggers "$package_dir") + + if [[ ${#triggers[@]} -eq 0 ]]; then + if [[ "$SPECIFIC_MODE" == true ]]; then + print_error "Package $package does not declare rebuild_on" + ((++FAILED)) + else + print_info "Skipping $package: no rebuild triggers" + ((++SKIPPED)) + fi + return 0 + fi + + 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" + ((++FAILED)) + return 0 + fi + done + + local recorded + if ! recorded=$(package_metadata_value "$package_dir" '.rebuilt_against' ""); then + print_error " Could not read .omarchy/package.json for $package" + ((++FAILED)) + return 0 + fi + + if [[ -z "$recorded" || "$recorded" == "null" ]]; then + if ! record_triggers "$package_dir" "$current"; then + print_error " Failed to record trigger versions for $package" + ((++FAILED)) + return 0 + fi + print_success " Recorded baseline: $(jq -r 'to_entries | map("\(.key) \(.value)") | join(", ")' <<<"$current")" + ((++UPDATED)) + return 0 + fi + + local moved + if ! moved=$(jq -r --argjson current "$current" ' + to_entries + | map(select($current[.key] != .value) | "\(.key) \(.value) -> \($current[.key])") + | join(", ") + ' <<<"$recorded"); then + print_error " Could not compare recorded trigger versions for $package" + ((++FAILED)) + return 0 + fi + + if [[ -z "$moved" ]]; then + print_info " Already rebuilt against $(jq -r 'to_entries | map("\(.key) \(.value)") | join(", ")' <<<"$current")" + ((++SKIPPED)) + return 0 + fi + + local pkgrel next_pkgrel + pkgrel=$(pkgbuild_field "$package_dir" pkgrel) + + # An AUR-synced package gets its PKGBUILD replaced wholesale on the next sync, + # so the bump only survives as the dotted Omarchy suffix that sync-aur + # reapplies from .omarchy/package.json. + local suffix="" + if package_sync_enabled "$package_dir"; then + if [[ "$pkgrel" == *.* ]]; then + next_pkgrel=$(bump_pkgrel "$pkgrel") || next_pkgrel="" + suffix="${next_pkgrel##*.}" + else + next_pkgrel="$pkgrel.1" + suffix="1" + fi + else + next_pkgrel=$(bump_pkgrel "$pkgrel") || next_pkgrel="" + fi + + if [[ -z "$next_pkgrel" ]]; then + print_error " Cannot bump pkgrel=$pkgrel for $package; bump it by hand" + ((++FAILED)) + return 0 + fi + + local pkgver epoch old_version new_version + pkgver=$(pkgbuild_field "$package_dir" pkgver) + epoch=$(pkgbuild_field "$package_dir" epoch) + old_version="${epoch:+$epoch:}$pkgver-$pkgrel" + new_version="${epoch:+$epoch:}$pkgver-$next_pkgrel" + + if [[ "$(vercmp "$new_version" "$old_version")" -le 0 ]]; then + print_error " pkgrel $pkgrel -> $next_pkgrel would not be an upgrade for $package" + ((++FAILED)) + return 0 + fi + + if ! write_pkgrel "$package_dir" "$next_pkgrel"; then + print_error " Failed to bump pkgrel for $package" + ((++FAILED)) + return 0 + fi + + if [[ -n "$suffix" ]] && ! write_metadata "$package_dir" '.pkgrel.suffix = ($suffix | tonumber)' --arg suffix "$suffix"; then + print_error " Failed to record pkgrel suffix for $package" + ((++FAILED)) + return 0 + fi + + if ! record_triggers "$package_dir" "$current"; then + print_error " Failed to record trigger versions for $package" + ((++FAILED)) + return 0 + fi + + print_success " $moved; pkgrel $pkgrel -> $next_pkgrel" + ((++UPDATED)) +} + +if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then + SPECIFIC_MODE=true + for package in "${SPECIFIC_PACKAGES[@]}"; do + sync_package "$package" + done +else + while IFS= read -r package; do + sync_package "$package" + done < <(packages_for_rebuild_sync) +fi + +echo "" +if [[ $FAILED -gt 0 ]]; then + print_error "Rebuild trigger sync completed with failures" +else + print_success "Rebuild trigger sync complete!" +fi +echo " Target: $PKGBUILDS_DIR" +echo " Updated: $UPDATED" +echo " Skipped: $SKIPPED" +echo " Failed: $FAILED" + +if [[ $FAILED -gt 0 ]]; then + exit 1 +fi diff --git a/helpers/package-metadata.sh b/helpers/package-metadata.sh index b1f1b70..83ed37a 100644 --- a/helpers/package-metadata.sh +++ b/helpers/package-metadata.sh @@ -10,9 +10,11 @@ # { "source": "aur", "release_ring": "fast" } # { "source": "aur", "skip_build": true } # { "source": "aur", "pkgrel": { "suffix": 1, "offset": 1 } } +# { "source": "aur", "rebuild_on": ["qt6-base"] } # { "source": "local" } # -# bin/sync-aur also writes upstream_commit for AUR-backed packages. +# bin/sync-aur also writes upstream_commit for AUR-backed packages, and +# bin/sync-rebuilds writes rebuilt_against for packages declaring rebuild_on. if [[ -z "${PKGBUILDS_DIR:-}" ]]; then if [[ -n "${BUILD_ROOT:-}" ]]; then @@ -148,6 +150,33 @@ packages_for_upstream_sync() { done } +# Packages that must be rebuilt when a dependency they link against changes, +# even though nothing in their own source moved. `rebuild_on` names those +# dependencies; `rebuilt_against` records the versions the checked-in pkgrel was +# last bumped for. +package_rebuild_triggers() { + local pkgdir="$1" + local metadata + + metadata=$(metadata_file_for_dir "$pkgdir") + [[ -f "$metadata" ]] || return 0 + + jq -r '(.rebuild_on // [])[]' "$metadata" +} + +package_has_rebuild_triggers() { + local pkgdir="$1" + [[ -n "$(package_rebuild_triggers "$pkgdir")" ]] +} + +packages_for_rebuild_sync() { + package_dirs | while IFS= read -r pkgdir; do + if package_has_rebuild_triggers "$pkgdir"; then + basename "$pkgdir" + fi + done +} + packages_for_mirror() { local mirror="$1" @@ -299,4 +328,19 @@ validate_package_metadata() { echo "invalid upstream_commit for $(basename "$pkgdir"): must be a string" return 1 fi + + if ! jq -e '(.rebuild_on // []) | type == "array" and all(type == "string" and length > 0)' "$metadata" >/dev/null; then + echo "invalid rebuild_on for $(basename "$pkgdir"): must be an array of package names" + 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" + return 1 + fi + + if ! jq -e '((.rebuilt_against // {}) | keys) - (.rebuild_on // []) | length == 0' "$metadata" >/dev/null; then + echo "invalid rebuilt_against for $(basename "$pkgdir"): records a package that rebuild_on does not name" + return 1 + fi } From ced6963010e62d63a5d933ec9aa6aa745d3ac3cb Mon Sep 17 00:00:00 2001 From: Omabot Date: Thu, 20 Aug 2026 03:44:56 -0700 Subject: [PATCH 2/4] Rebuild quickshell-git against Qt 6.11.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Arch shipped qt6-base 6.11.2-2 on 2026-08-20, and the published 0.3.0.r20.g28771c7-1, built against 6.11.1, stopped starting: undefined symbol _ZN23QUntypedPropertyBindingC1EP23QPropertyBindingPrivate, version Qt_6_PRIVATE_API. quickshell-check.hook caught it post-transaction, but detecting is all it does, so pacman logged the failure and omarchy-update-restart went on to restart a shell whose binary could no longer launch. The git rev has not moved, so the rebuild only reaches anyone through a pkgrel bump. rebuild_on names the three Qt packages quickshell actually links against: qt6-base for Core, Gui, Widgets, Network, DBus and OpenGL, qt6-declarative for Quick and the Qml libraries, qt6-wayland for WaylandClient. rebuilt_against is seeded with the versions this rebuild will link against, so bin/sync-rebuilds starts from a correct baseline and fires on the next Qt release rather than repeating this one. 🤖 Generated by Opus 5 in Claude Code. --- pkgbuilds/quickshell-git/.omarchy/package.json | 12 +++++++++++- pkgbuilds/quickshell-git/PKGBUILD | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pkgbuilds/quickshell-git/.omarchy/package.json b/pkgbuilds/quickshell-git/.omarchy/package.json index cb131e0..4a7a06e 100644 --- a/pkgbuilds/quickshell-git/.omarchy/package.json +++ b/pkgbuilds/quickshell-git/.omarchy/package.json @@ -1,5 +1,15 @@ { "source": "aur", "upstream_commit": "2494139bce7b7fc71372da00b68fb745a2c72d90", - "sync": false + "sync": false, + "rebuild_on": [ + "qt6-base", + "qt6-declarative", + "qt6-wayland" + ], + "rebuilt_against": { + "qt6-base": "6.11.2-2", + "qt6-declarative": "6.11.2-1", + "qt6-wayland": "6.11.2-1" + } } diff --git a/pkgbuilds/quickshell-git/PKGBUILD b/pkgbuilds/quickshell-git/PKGBUILD index 5aa2973..9338de2 100644 --- a/pkgbuilds/quickshell-git/PKGBUILD +++ b/pkgbuilds/quickshell-git/PKGBUILD @@ -3,7 +3,7 @@ _pkgname=quickshell pkgname="$_pkgname-git" pkgver=0.3.0.r20.g28771c7 -pkgrel=1 +pkgrel=2 pkgdesc='Flexible toolkit for making desktop shells with QtQuick' arch=(x86_64 aarch64) url='https://git.outfoxxed.me/quickshell/quickshell' From 1b14682aca86f88646b97dd956719257572b92fc Mon Sep 17 00:00:00 2001 From: Omabot Date: Thu, 20 Aug 2026 04:28:15 -0700 Subject: [PATCH 3/4] Bump unless every trigger is recorded and matches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A review at xhigh found several ways this command could report success while delivering nothing, which is the exact failure it exists to prevent. A trigger named in rebuild_on but missing from rebuilt_against was never examined, because the comparison walked the record rather than the declared list. Adding a dependency to a package already opted in left that dependency untracked forever. The comparison now walks the declared triggers, so a name the record does not carry reads as changed. That also retires the separate baseline path. Recording a package's triggers without bumping pkgrel certified a build nobody had checked: a package already broken by a release that moved before it opted in would be recorded as current and never rebuilt. Opting in now costs one rebuild, which is much the cheaper mistake. A bumped version was only checked against the checked-in one. The floor is what users already have, so a checkout that had fallen behind the repository could be bumped to a version pacman orders below the package it means to replace, with the record advancing regardless. The published database is now the floor, and an unreadable one warns rather than blocks. Metadata that did not parse dropped its package out of an unscoped run without a word, an unreadable rebuild_on being indistinguishable from an absent one. It is now reported and fails the run. The workflow reads versions from mirror.omarchy.org, the mirror the x86_64 builder itself uses, rather than whichever mirror the container defaulted to. 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 stays uncovered and is documented as such: those builds resolve from Arch Linux ARM, one record cannot describe two architectures, and only x86_64 is published today. bin/sync-rebuilds --self-test covers each of these against a throwaway repository root with pacman and curl stubbed. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Codex XHigh --- .github/workflows/sync-rebuilds.yml | 9 +- README.md | 9 +- bin/sync-rebuilds | 402 ++++++++++++++++++++++++---- 3 files changed, 363 insertions(+), 57 deletions(-) diff --git a/.github/workflows/sync-rebuilds.yml b/.github/workflows/sync-rebuilds.yml index 74fc0d8..4d8af04 100644 --- a/.github/workflows/sync-rebuilds.yml +++ b/.github/workflows/sync-rebuilds.yml @@ -24,9 +24,11 @@ jobs: with: persist-credentials: false - # Runs in an Arch container because the question being asked is what the - # build container will link against, and pacman's own view of core/extra is - # the only answer that matches. + # Runs in an Arch container against the mirror the x86_64 builder itself + # uses, because the question being asked is what that builder will link + # against and a different mirror can be hours ahead of it. Recording a + # version the build never saw is the one failure this command must not + # have: nothing re-fires once the record matches. - name: Bump pkgrel for packages whose dependencies moved run: | docker run --rm \ @@ -40,6 +42,7 @@ jobs: archlinux:base-devel bash -lc ' set -euo pipefail + printf "Server = https://mirror.omarchy.org/\$repo/os/\$arch\n" > /etc/pacman.d/mirrorlist pacman -Syu --noconfirm jq groupadd -g "$HOST_GID" runner diff --git a/README.md b/README.md index c2e7eff..3530dab 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,7 @@ it describes. ```bash bin/sync-rebuilds # Bump every package whose dependencies moved bin/sync-rebuilds quickshell-git # Update specific packages +bin/sync-rebuilds --self-test # Run the regression tests ``` Some packages have to be rebuilt when something they link against changes, even though nothing in their own source moved. A Qt private-API consumer is the usual case: `Qt_6_PRIVATE_API` symbols are not covered by the soname, so a qt6-base point release can leave an installed binary unable to resolve a symbol at startup, and pacman upgrades Qt out from under it because the dependency is unversioned. The package still builds from the same git commit, so nothing in the normal version check notices. @@ -298,13 +299,17 @@ 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. When they differ it bumps pkgrel and rewrites the record. A package with no record yet is only recorded, never bumped: what its published build linked against is not knowable from here, so the first run establishes the baseline and the next change acts on it. +`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. 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. For an AUR-synced package the bump is expressed as the dotted Omarchy pkgrel suffix in the metadata as well as in the PKGBUILD, because the next AUR sync replaces the PKGBUILD wholesale and would otherwise drop it. -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 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. ### Other diff --git a/bin/sync-rebuilds b/bin/sync-rebuilds index c9d5044..7a0336b 100755 --- a/bin/sync-rebuilds +++ b/bin/sync-rebuilds @@ -6,13 +6,23 @@ source "$BUILD_ROOT/helpers/message-helpers.sh" source "$BUILD_ROOT/helpers/paths.sh" source "$BUILD_ROOT/helpers/package-metadata.sh" +TEMP_DIR=$(mktemp -d) +trap 'rm -rf "$TEMP_DIR"' EXIT + SPECIFIC_PACKAGES=() +SELF_TEST=false # Only the repositories a user actually installs from. A Qt release sitting in # testing or kde-unstable is not what the build container will link against, and # 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. +PUBLISHED_BASE_URL="${OMARCHY_PUBLISHED_BASE_URL:-https://pkgs.omarchy.org}" +PUBLISHED_MIRRORS=(edge stable) +PUBLISHED_ARCH=x86_64 + usage() { cat </dev/null 2>&1; then - print_error "$tool not found: reading trigger versions and ordering pkgrels both need pacman" - exit 1 - fi -done - -print_header "Rebuild Trigger Sync" - UPDATED=0 SKIPPED=0 FAILED=0 @@ -98,6 +108,73 @@ repo_version() { ' <<<"$info" } +declare -A PUBLISHED_VERSION=() +PUBLISHED_LOADED=false +PUBLISHED_AVAILABLE=true + +remember_published() { + local name="$1" + local version="$2" + local known="${PUBLISHED_VERSION[$name]:-}" + + # A package can be in both mirrors at different revisions. The floor is the + # highest of them, because that is what a user could already have installed. + if [[ -z "$known" || "$(vercmp "$version" "$known")" -gt 0 ]]; then + PUBLISHED_VERSION["$name"]="$version" + fi +} + +# Versions currently published, read from the repository databases. Split +# packages are stored under their own pkgname, so both %NAME% and %BASE% are +# recorded to make a pkgbase findable. +load_published_versions() { + [[ "$PUBLISHED_LOADED" == true ]] && return 0 + PUBLISHED_LOADED=true + + local mirror db name base version + + for mirror in "${PUBLISHED_MIRRORS[@]}"; do + db="$TEMP_DIR/published-$mirror.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 + + 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 +} + +published_version() { + local package="$1" + + load_published_versions + echo "${PUBLISHED_VERSION[$package]:-}" +} + +# The pkgver of a full version string, with any epoch and pkgrel removed. +version_pkgver() { + local version="${1#*:}" + echo "${version%-*}" +} + pkgbuild_field() { local package_dir="$1" local field="$2" @@ -178,6 +255,23 @@ record_triggers() { write_metadata "$package_dir" '.rebuilt_against = $current' --argjson current "$current" } +# Metadata that does not parse would otherwise drop its package out of the run +# without a word, because every query of it reports nothing and an absent +# rebuild_on is indistinguishable from an unreadable one. +check_metadata_readable() { + local package_dir="$1" + local metadata + metadata=$(metadata_file_for_dir "$package_dir") + + [[ -f "$metadata" ]] || return 0 + + if ! jq empty "$metadata" 2>/dev/null; then + print_error "Unreadable metadata, skipping $(basename "$package_dir"): $metadata" + ((++FAILED)) + return 1 + fi +} + sync_package() { local package="$1" local package_dir="$PKGBUILDS_DIR/$package" @@ -188,6 +282,8 @@ sync_package() { return 0 fi + check_metadata_readable "$package_dir" || return 0 + local triggers=() mapfile -t triggers < <(package_rebuild_triggers "$package_dir") @@ -225,24 +321,17 @@ sync_package() { ((++FAILED)) return 0 fi + [[ -n "$recorded" && "$recorded" != "null" ]] || recorded="{}" - if [[ -z "$recorded" || "$recorded" == "null" ]]; then - if ! record_triggers "$package_dir" "$current"; then - print_error " Failed to record trigger versions for $package" - ((++FAILED)) - return 0 - fi - print_success " Recorded baseline: $(jq -r 'to_entries | map("\(.key) \(.value)") | join(", ")' <<<"$current")" - ((++UPDATED)) - return 0 - 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 current "$current" ' + if ! moved=$(jq -r --argjson recorded "$recorded" ' to_entries - | map(select($current[.key] != .value) | "\(.key) \(.value) -> \($current[.key])") + | map(select($recorded[.key] != .value) + | "\(.key) \($recorded[.key] // "unrecorded") -> \(.value)") | join(", ") - ' <<<"$recorded"); then + ' <<<"$current"); then print_error " Could not compare recorded trigger versions for $package" ((++FAILED)) return 0 @@ -291,6 +380,19 @@ sync_package() { return 0 fi + # 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 pacman orders below what it would replace. + local floor + floor=$(published_version "$package") + if [[ -n "$floor" && "$(version_pkgver "$floor")" == "$pkgver" ]]; then + if [[ "$(vercmp "$new_version" "$floor")" -le 0 ]]; then + print_error " $new_version would not be an upgrade over the published $floor; leaving $package alone" + ((++FAILED)) + return 0 + fi + fi + if ! write_pkgrel "$package_dir" "$next_pkgrel"; then print_error " Failed to bump pkgrel for $package" ((++FAILED)) @@ -313,28 +415,224 @@ sync_package() { ((++UPDATED)) } -if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then - SPECIFIC_MODE=true - for package in "${SPECIFIC_PACKAGES[@]}"; do - sync_package "$package" - done -else - while IFS= read -r package; do - sync_package "$package" - done < <(packages_for_rebuild_sync) +run_sync() { + if [[ ${#SPECIFIC_PACKAGES[@]} -gt 0 ]]; then + SPECIFIC_MODE=true + for package in "${SPECIFIC_PACKAGES[@]}"; do + sync_package "$package" + done + else + # Enumerated up front rather than streamed, so a producer that dies partway + # cannot quietly shorten the list of packages considered. + local packages=() + mapfile -t packages < <(package_dirs) + + local package_dir + for package_dir in "${packages[@]}"; do + check_metadata_readable "$package_dir" || continue + if package_has_rebuild_triggers "$package_dir"; then + sync_package "$(basename "$package_dir")" + fi + done + fi + + echo "" + if [[ $FAILED -gt 0 ]]; then + print_error "Rebuild trigger sync completed with failures" + else + print_success "Rebuild trigger sync complete!" + fi + echo " Target: $PKGBUILDS_DIR" + echo " Updated: $UPDATED" + echo " Skipped: $SKIPPED" + echo " Failed: $FAILED" + + [[ $FAILED -eq 0 ]] +} + +# --- self-test --------------------------------------------------------------- + +# Each case runs the real script against a throwaway repository root, with +# pacman and curl replaced by stubs, so what is exercised is the decision path +# itself rather than a restatement of it. +selftest_root() { + local name="$1" + local root="$TEMP_DIR/case-$name" + + mkdir -p "$root/bin" "$root/helpers" "$root/pkgbuilds" "$root/stub" + cp "$BUILD_ROOT/bin/sync-rebuilds" "$root/bin/" + cp "$BUILD_ROOT"/helpers/*.sh "$root/helpers/" + echo "$root" +} + +selftest_package() { + local root="$1" name="$2" pkgrel="$3" metadata="$4" pkgver="${5:-1.0}" + 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 '%s\n' "$metadata" > "$dir/.omarchy/package.json" +} + +selftest_pacman() { + local root="$1" + shift + printf '%s\n' "$@" > "$root/stub/versions" + + cat > "$root/stub/pacman" <<'STUB' +#!/bin/bash +[[ "$1" == "-Si" ]] || exit 1 +version=$(awk -F= -v p="$2" '$1 == p { print $2; exit }' "$(dirname "$0")/versions") +[[ -n "$version" ]] || exit 1 +printf 'Repository : extra\nName : %s\nVersion : %s\n\n' "$2" "$version" +STUB + chmod +x "$root/stub/pacman" +} + +# Serves a repository database assembled by hand from name=version pairs. With +# none given the stub fails, which is how the unreachable-repository path is +# exercised. +selftest_published() { + local root="$1" + shift + local staging="$root/stub/db" + local entry name version + + if [[ $# -gt 0 ]]; then + rm -rf "$staging" + mkdir -p "$staging" + for entry in "$@"; do + name="${entry%=*}" + version="${entry#*=}" + mkdir -p "$staging/$name-$version" + printf '%%FILENAME%%\n%s-%s-x86_64.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 + tar --zstd -cf "$root/stub/omarchy.db.tar.zst" -C "$staging" . + fi + + cat > "$root/stub/curl" <<'STUB' +#!/bin/bash +out="" +while [[ $# -gt 0 ]]; do + case "$1" in + -o) out="$2"; shift 2 ;; + *) shift ;; + esac +done +db="$(dirname "$0")/omarchy.db.tar.zst" +[[ -f "$db" && -n "$out" ]] || exit 22 +cp "$db" "$out" +STUB + chmod +x "$root/stub/curl" +} + +cmd_self_test() { + local failures=0 + local root + + check() { + local label="$1" expected="$2" got="$3" + if [[ "$got" == "$expected" ]]; then + echo " ok: $label" + else + echo " FAIL: $label -> $got (expected $expected)" + failures=$((failures + 1)) + fi + } + + run_case() { + local root="$1" + shift + local status=0 + PATH="$root/stub:$PATH" "$root/bin/sync-rebuilds" "$@" > "$root/output" 2>&1 || status=$? + echo "$status" + } + + pkgrel_of() { + grep -m1 '^pkgrel=' "$1/PKGBUILD" | cut -d= -f2 + } + + print_header "sync-rebuilds self-test" + + echo "A trigger missing from the record counts as changed:" + root=$(selftest_root partial) + selftest_package "$root" t-partial 1 '{"source":"local","rebuild_on":["dep-a","dep-b"],"rebuilt_against":{"dep-a":"1-1"}}' + selftest_pacman "$root" dep-a=1-1 dep-b=2-2 + selftest_published "$root" + 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")" + + echo "Opting a package in buys a rebuild rather than a bare record:" + root=$(selftest_root fresh) + selftest_package "$root" t-fresh 1 '{"source":"local","rebuild_on":["dep-a"]}' + selftest_pacman "$root" dep-a=1-1 + selftest_published "$root" + 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")" + + echo "An unchanged package is left alone:" + root=$(selftest_root current) + selftest_package "$root" t-current 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"dep-a":"1-1"}}' + selftest_pacman "$root" dep-a=1-1 + selftest_published "$root" + check "run succeeds" 0 "$(run_case "$root")" + check "pkgrel untouched" 1 "$(pkgrel_of "$root/pkgbuilds/t-current")" + + echo "Metadata that does not parse fails the run instead of vanishing from it:" + root=$(selftest_root unreadable) + selftest_package "$root" t-broken 1 '{"source":"local","rebuild_on":["dep-a"' + selftest_package "$root" t-good 1 '{"source":"local","rebuild_on":["dep-a"]}' + selftest_pacman "$root" dep-a=1-1 + selftest_published "$root" + check "run fails" 1 "$(run_case "$root")" + check "the readable package is still processed" 2 "$(pkgrel_of "$root/pkgbuilds/t-good")" + + echo "A bump that pacman would not order above the published package is refused:" + root=$(selftest_root floor) + selftest_package "$root" t-floor 1 '{"source":"local","rebuild_on":["dep-a"],"rebuilt_against":{"dep-a":"0-0"}}' + selftest_pacman "$root" dep-a=1-1 + selftest_published "$root" t-floor=1.0-5 + check "run fails" 1 "$(run_case "$root")" + check "pkgrel untouched" 1 "$(pkgrel_of "$root/pkgbuilds/t-floor")" + check "record untouched" "0-0" \ + "$(jq -r '.rebuilt_against["dep-a"]' "$root/pkgbuilds/t-floor/.omarchy/package.json")" + + echo "An AUR-synced package is bumped as a dotted suffix that sync-aur reapplies:" + root=$(selftest_root aur) + selftest_package "$root" t-aur 3 '{"source":"aur","rebuild_on":["dep-a"],"rebuilt_against":{"dep-a":"0-0"}}' + selftest_pacman "$root" dep-a=1-1 + selftest_published "$root" + check "run succeeds" 0 "$(run_case "$root")" + check "pkgrel suffixed" "3.1" "$(pkgrel_of "$root/pkgbuilds/t-aur")" + check "suffix recorded for the next AUR sync" 1 \ + "$(jq -r '.pkgrel.suffix' "$root/pkgbuilds/t-aur/.omarchy/package.json")" + + echo "" + if [[ "$failures" -eq 0 ]]; then + print_success "Self-test passed" + return 0 + fi + print_error "$failures self-test failure(s)" + return 1 +} + +if [[ "$SELF_TEST" == true ]]; then + cmd_self_test + exit $? fi -echo "" -if [[ $FAILED -gt 0 ]]; then - print_error "Rebuild trigger sync completed with failures" -else - print_success "Rebuild trigger sync complete!" -fi -echo " Target: $PKGBUILDS_DIR" -echo " Updated: $UPDATED" -echo " Skipped: $SKIPPED" -echo " Failed: $FAILED" +for tool in pacman vercmp jq curl; do + if ! command -v "$tool" >/dev/null 2>&1; then + print_error "$tool not found: reading trigger versions and ordering pkgrels both need pacman" + exit 1 + fi +done -if [[ $FAILED -gt 0 ]]; then - exit 1 -fi +print_header "Rebuild Trigger Sync" + +run_sync From bed763639442a2a36f0f193c5fdc32c543469d8d Mon Sep 17 00:00:00 2001 From: Omabot Date: Thu, 20 Aug 2026 04:28:15 -0700 Subject: [PATCH 4/4] Read pkgver through one parser in sync-aur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apply_pkgrel_override compares the checked-in pkgver against the incoming one to decide whether Omarchy's pkgrel metadata has gone stale, but read the two sides differently: previous_pkgver came from get_pkgbuild_field, which strips quotes, while current_pkgver was parsed again in place and kept them. A PKGBUILD writing pkgver='1.0' therefore compared unequal to itself, and the pkgrel metadata of an unchanged package was deleted on every sync. spotify, rustdesk and limine-mkinitcpio-hook all quote pkgver. None carries pkgrel metadata today, so nothing has been losing a suffix, but rebuild_on writes exactly that metadata and would have had it thrown away on the next sync. Reading through the same accessor rather than parsing a second time removes the divergence instead of correcting one side of it. 🤖 Generated by Opus 5 in Claude Code. Reviewed by Codex XHigh. Co-Authored-By: Codex XHigh --- bin/sync-aur | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/sync-aur b/bin/sync-aur index fb493a8..59af6ca 100755 --- a/bin/sync-aur +++ b/bin/sync-aur @@ -246,7 +246,10 @@ apply_pkgrel_override() { jq -e 'has("pkgrel")' "$metadata" >/dev/null || return 1 local current_pkgver suffix offset base rel tmpfile - current_pkgver=$(grep -m1 '^pkgver=' "$pkgbuild" | cut -d= -f2) + # Read through the same accessor that produced previous_pkgver. Parsing it a + # second time here let a quoted pkgver= compare unequal to itself, which threw + # away the pkgrel metadata of an unchanged package on every sync. + current_pkgver=$(get_pkgbuild_field "$package_dir" pkgver) if [[ -n "$previous_pkgver" && "$current_pkgver" != "$previous_pkgver" ]]; then print_info "Removing stale pkgrel metadata for $(display_package_name "$package_dir") (pkgver changed: $previous_pkgver -> $current_pkgver)"