Merge pull request #181 from omacom-io/quickshell-qt-abi-rebuild

Rebuild quickshell-git against Qt 6.11.2, and notice next time
This commit is contained in:
David Heinemeier Hansson
2026-08-20 13:53:58 +02:00
committed by GitHub
7 changed files with 835 additions and 4 deletions
+102
View File
@@ -0,0 +1,102 @@
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 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 \
-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
printf "Server = https://mirror.omarchy.org/\$repo/os/\$arch\n" > /etc/pacman.d/mirrorlist
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 \
"🔴 <strong>Rebuild trigger sync failed</strong><br><a href=\"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\">View run</a>" \
'{content: $content}')" \
"$BASECAMP_CHATBOT_URL"
+34
View File
@@ -283,6 +283,34 @@ 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 which reads OpenAI's Debian package index and never fetches the 750 MB of debs
it describes. it describes.
### Sync Rebuild Triggers
```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.
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. 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.
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 ### Other
```bash ```bash
@@ -296,6 +324,7 @@ bin/add-package <package> # Add an AUR/local package with metadata
bin/package-worktree <package> # Create upstream/patched/current scratch workspace bin/package-worktree <package> # Create upstream/patched/current scratch workspace
bin/repo remove <package> # Remove package bin/repo remove <package> # Remove package
bin/sync-upstream # Update packages that track a vendor release feed 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) bin/clean-docker # Clear Docker images/cache (forces fresh rebuild)
``` ```
@@ -439,6 +468,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`). - `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 <name>`. - `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 <name>`.
- `pkgrel`: optional Omarchy pkgrel suffix for a version-pinned rebuild bump. This emits `<aur pkgrel>.<suffix>` 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. - `pkgrel`: optional Omarchy pkgrel suffix for a version-pinned rebuild bump. This emits `<aur pkgrel>.<suffix>` 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. - `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 ### Build Matrix
@@ -567,6 +598,8 @@ Packages are only rebuilt if:
- PKGBUILD version is newer than repository version - PKGBUILD version is newer than repository version
- Package doesn't exist in production - 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 ## Automated Releases
The repository includes GitHub workflows and systemd services for automated releases. The repository includes GitHub workflows and systemd services for automated releases.
@@ -577,6 +610,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. 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. 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 #### Systemd Services
+4 -1
View File
@@ -246,7 +246,10 @@ apply_pkgrel_override() {
jq -e 'has("pkgrel")' "$metadata" >/dev/null || return 1 jq -e 'has("pkgrel")' "$metadata" >/dev/null || return 1
local current_pkgver suffix offset base rel tmpfile 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 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)" print_info "Removing stale pkgrel metadata for $(display_package_name "$package_dir") (pkgver changed: $previous_pkgver -> $current_pkgver)"
+638
View File
@@ -0,0 +1,638 @@
#!/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"
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 <<EOF
Usage: $0 [PACKAGE...]
Bump pkgrel for packages that have to be rebuilt when a dependency changes
underneath them, rather than when their own source moves.
A package opts in by naming those dependencies in .omarchy/package.json:
{ "source": "aur", "sync": false, "rebuild_on": ["qt6-base"] }
The versions the current pkgrel was bumped for are recorded alongside, in
rebuilt_against, and written by this command:
{ "rebuild_on": ["qt6-base"], "rebuilt_against": { "qt6-base": "6.11.2-2" } }
pkgrel is bumped unless every package named in rebuild_on is recorded and still
matches. A name that is missing from the record counts as changed, so opting a
package in, or adding a dependency to one already opted in, buys one rebuild
rather than a record that certifies a build nobody checked.
The bump has to happen in git rather than in the builder, because a rebuild that
reuses the published version string is a package pacman will never offer anyone.
For the same reason the bumped version is checked against the published one and
refused if it would not be an upgrade.
Arguments:
PACKAGE One or more package names to update (optional)
Options:
--self-test Run the built-in regression tests and exit
-h, --help Show this help
Examples:
$0 # Update every package that declares rebuild_on
$0 quickshell-git # Update specific packages
Trigger versions are read from the local pacman database, so sync it first
(pacman -Sy) or this reports whatever that database last saw.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
exit 0
;;
--self-test)
SELF_TEST=true
shift
;;
--*)
print_error "Unknown option: $1"
exit 1
;;
*)
SPECIFIC_PACKAGES+=("$1")
shift
;;
esac
done
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"
}
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"
(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"
}
# 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"
if [[ ! -f "$package_dir/PKGBUILD" ]]; then
print_error "Package $package has no PKGBUILD"
((++FAILED))
return 0
fi
check_metadata_readable "$package_dir" || return 0
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
[[ -n "$recorded" && "$recorded" != "null" ]] || recorded="{}"
# 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)")
| join(", ")
' <<<"$current"); 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
# 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))
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))
}
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
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
print_header "Rebuild Trigger Sync"
run_sync
+45 -1
View File
@@ -10,9 +10,11 @@
# { "source": "aur", "release_ring": "fast" } # { "source": "aur", "release_ring": "fast" }
# { "source": "aur", "skip_build": true } # { "source": "aur", "skip_build": true }
# { "source": "aur", "pkgrel": { "suffix": 1, "offset": 1 } } # { "source": "aur", "pkgrel": { "suffix": 1, "offset": 1 } }
# { "source": "aur", "rebuild_on": ["qt6-base"] }
# { "source": "local" } # { "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 [[ -z "${PKGBUILDS_DIR:-}" ]]; then
if [[ -n "${BUILD_ROOT:-}" ]]; then if [[ -n "${BUILD_ROOT:-}" ]]; then
@@ -148,6 +150,33 @@ packages_for_upstream_sync() {
done 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() { packages_for_mirror() {
local mirror="$1" local mirror="$1"
@@ -299,4 +328,19 @@ validate_package_metadata() {
echo "invalid upstream_commit for $(basename "$pkgdir"): must be a string" echo "invalid upstream_commit for $(basename "$pkgdir"): must be a string"
return 1 return 1
fi 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
} }
+11 -1
View File
@@ -1,5 +1,15 @@
{ {
"source": "aur", "source": "aur",
"upstream_commit": "2494139bce7b7fc71372da00b68fb745a2c72d90", "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"
}
} }
+1 -1
View File
@@ -3,7 +3,7 @@
_pkgname=quickshell _pkgname=quickshell
pkgname="$_pkgname-git" pkgname="$_pkgname-git"
pkgver=0.3.0.r20.g28771c7 pkgver=0.3.0.r20.g28771c7
pkgrel=1 pkgrel=2
pkgdesc='Flexible toolkit for making desktop shells with QtQuick' pkgdesc='Flexible toolkit for making desktop shells with QtQuick'
arch=(x86_64 aarch64) arch=(x86_64 aarch64)
url='https://git.outfoxxed.me/quickshell/quickshell' url='https://git.outfoxxed.me/quickshell/quickshell'