Files
omarchy-pkgs/helpers/package-metadata.sh
T
David Heinemeier HanssonandClaude Opus 5 01a566f01a Add bin/sync-upstream for packages that track a vendor release feed
Some vendors publish a release feed of their own that is faster and more
precise than anyone's packaging of it. A package opts in with an
.omarchy/upstream.sh hook that reports the newest release as JSON, and the
driver rewrites pkgver, the checksum arrays the hook names, and pkgrel.

Writes are guarded on both ends: every assignment the update will touch is
verified to exist before anything is written, so a hook naming an array the
PKGBUILD lacks fails with the file untouched rather than half rewritten; and
pkgver is held to pacman's character set, because it lands in a file makepkg
sources as shell.

Ordering is vercmp's, not sort -V's -- they disagree about whether 1.0a
precedes 1.0, and pacman is what decides if a published package is an upgrade.
That is also why the workflow runs in an Arch container rather than straight on
the runner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 08:18:54 -07:00

303 lines
7.9 KiB
Bash

# Package metadata helpers for Omarchy package build tooling
#
# Expects package directories in $PKGBUILDS_DIR, each with:
# .omarchy/package.json
#
# Minimal schema:
# { "source": "aur" }
# { "source": "aur", "sync": false }
# { "source": "aur", "aur": "different-aur-name" }
# { "source": "aur", "release_ring": "fast" }
# { "source": "aur", "skip_build": true }
# { "source": "aur", "pkgrel": { "suffix": 1, "offset": 1 } }
# { "source": "local" }
#
# bin/sync-aur also writes upstream_commit for AUR-backed packages.
if [[ -z "${PKGBUILDS_DIR:-}" ]]; then
if [[ -n "${BUILD_ROOT:-}" ]]; then
PKGBUILDS_DIR="$BUILD_ROOT/pkgbuilds"
elif [[ -d /pkgbuilds ]]; then
PKGBUILDS_DIR="/pkgbuilds"
else
PKGBUILDS_DIR="pkgbuilds"
fi
fi
metadata_file_for_dir() {
local pkgdir="$1"
echo "$pkgdir/.omarchy/package.json"
}
package_dir_for_name() {
local package="$1"
local pkgdir="$PKGBUILDS_DIR/$package"
[[ -d "$pkgdir" && -f "$pkgdir/PKGBUILD" ]] || return 1
echo "$pkgdir"
}
package_metadata_value() {
local pkgdir="$1"
local jq_filter="$2"
local default="${3:-}"
local metadata
metadata=$(metadata_file_for_dir "$pkgdir")
if [[ ! -f "$metadata" ]]; then
echo "$default"
return 0
fi
jq -r --arg default "$default" "$jq_filter // \$default" "$metadata"
}
package_sync_enabled() {
local pkgdir="$1"
local metadata source sync
metadata=$(metadata_file_for_dir "$pkgdir")
[[ -f "$metadata" ]] || return 1
source=$(jq -r '.source // ""' "$metadata")
[[ "$source" == "aur" ]] || return 1
sync=$(jq -r 'if has("sync") then .sync else true end' "$metadata")
[[ "$sync" != "false" ]]
}
package_release_ring() {
local pkgdir="$1"
package_metadata_value "$pkgdir" '.release_ring' ""
}
package_is_fast_ring() {
local pkgdir="$1"
[[ "$(package_release_ring "$pkgdir")" == "fast" ]]
}
package_build_skipped() {
local pkgdir="$1"
local metadata skip_build
metadata=$(metadata_file_for_dir "$pkgdir")
[[ -f "$metadata" ]] || return 1
skip_build=$(jq -r 'if has("skip_build") then .skip_build else false end' "$metadata")
[[ "$skip_build" == "true" ]]
}
package_has_metadata() {
local pkgdir="$1"
[[ -f "$(metadata_file_for_dir "$pkgdir")" ]]
}
package_has_pkgbuild() {
local pkgdir="$1"
[[ -f "$pkgdir/PKGBUILD" ]]
}
package_builds_for_mirror() {
local pkgdir="$1"
local mirror="$2"
package_has_pkgbuild "$pkgdir" || return 1
package_has_metadata "$pkgdir" || return 1
case "$mirror" in
edge)
return 0
;;
stable)
package_is_fast_ring "$pkgdir"
;;
*)
return 1
;;
esac
}
package_dirs() {
[[ -d "$PKGBUILDS_DIR" ]] || return 0
find "$PKGBUILDS_DIR" -mindepth 1 -maxdepth 1 -type d -print | sort | while IFS= read -r pkgdir; do
package_has_pkgbuild "$pkgdir" || continue
package_has_metadata "$pkgdir" || continue
echo "$pkgdir"
done
}
packages_for_aur_sync() {
package_dirs | while IFS= read -r pkgdir; do
if package_sync_enabled "$pkgdir"; then
basename "$pkgdir"
fi
done
}
package_has_upstream_hook() {
local pkgdir="$1"
[[ -f "$pkgdir/.omarchy/upstream.sh" ]]
}
packages_for_upstream_sync() {
package_dirs | while IFS= read -r pkgdir; do
if package_has_upstream_hook "$pkgdir"; then
basename "$pkgdir"
fi
done
}
packages_for_mirror() {
local mirror="$1"
package_dirs | while IFS= read -r pkgdir; do
if package_builds_for_mirror "$pkgdir" "$mirror"; then
basename "$pkgdir"
fi
done
}
packages_for_unscoped_build() {
local mirror="$1"
package_dirs | while IFS= read -r pkgdir; do
if package_builds_for_mirror "$pkgdir" "$mirror" && ! package_build_skipped "$pkgdir"; then
basename "$pkgdir"
fi
done
}
package_extract_vcs_hash_from_version() {
local version="$1"
local version_no_pkgrel="${version%-*}"
local candidate hash=""
# Drop an epoch prefix before scanning for commit-looking components.
if [[ "$version_no_pkgrel" == *:* ]]; then
version_no_pkgrel="${version_no_pkgrel#*:}"
fi
while IFS= read -r candidate; do
# Prefer candidates explicitly prefixed with `g`, but also accept bare hex
# hashes that contain at least one a-f character (e.g. r21251.626ee68).
if [[ "$candidate" == g* || "$candidate" =~ [a-f] ]]; then
hash="${candidate#g}"
fi
done < <(echo "$version_no_pkgrel" | grep -oE 'g?[a-f0-9]{7,40}')
[[ -n "$hash" ]] && echo "${hash:0:7}"
}
package_first_git_source() {
local pkgdir="$1"
(cd "$pkgdir" && env -u OMARCHY_SRC bash -c '
source PKGBUILD 2>/dev/null
for s in "${source[@]}"; do
url="${s#*::}"
[[ "$url" == git+* ]] && { echo "${url#git+}"; break; }
done')
}
package_git_upstream_hash() {
local pkgdir="$1"
local source_spec source_url fragment ref hash
source_spec=$(package_first_git_source "$pkgdir") || return 1
[[ -n "$source_spec" ]] || return 1
source_url="${source_spec%%#*}"
fragment=""
if [[ "$source_spec" == *"#"* ]]; then
fragment="${source_spec#*#}"
fi
case "$fragment" in
"")
hash=$(git ls-remote "$source_url" HEAD 2>/dev/null | awk 'NR == 1 { print substr($1, 1, 7) }')
;;
branch=*)
ref="${fragment#branch=}"
[[ -n "$ref" ]] || return 1
hash=$(git ls-remote "$source_url" "refs/heads/$ref" 2>/dev/null | awk 'NR == 1 { print substr($1, 1, 7) }')
;;
tag=*)
ref="${fragment#tag=}"
[[ -n "$ref" ]] || return 1
hash=$(git ls-remote "$source_url" "refs/tags/$ref^{}" "refs/tags/$ref" 2>/dev/null | awk '
$2 ~ /\^\{\}$/ { print substr($1, 1, 7); found=1; exit }
NR == 1 { first=substr($1, 1, 7) }
END { if (!found && first != "") print first }
')
;;
commit=*)
ref="${fragment#commit=}"
[[ "$ref" =~ ^[a-f0-9]{7,40}$ ]] || return 1
hash="${ref:0:7}"
;;
*)
return 1
;;
esac
[[ -n "$hash" ]] || return 1
echo "$hash"
}
validate_package_metadata() {
local pkgdir="$1"
local metadata source sync skip_build aur ring pkgrel_type
metadata=$(metadata_file_for_dir "$pkgdir")
[[ -f "$metadata" ]] || { echo "missing metadata: $metadata"; return 1; }
jq empty "$metadata" >/dev/null || return 1
source=$(jq -r '.source // ""' "$metadata")
case "$source" in
aur|local) ;;
*) echo "invalid source for $(basename "$pkgdir"): $source"; return 1 ;;
esac
sync=$(jq -r 'if has("sync") then .sync | type else "missing" end' "$metadata")
case "$sync" in
boolean|missing) ;;
*) echo "invalid sync for $(basename "$pkgdir"): must be boolean"; return 1 ;;
esac
skip_build=$(jq -r 'if has("skip_build") then .skip_build | type else "missing" end' "$metadata")
case "$skip_build" in
boolean|missing) ;;
*) echo "invalid skip_build for $(basename "$pkgdir"): must be boolean"; return 1 ;;
esac
aur=$(jq -r 'if has("aur") then .aur | type else "missing" end' "$metadata")
case "$aur" in
string|missing) ;;
*) echo "invalid aur for $(basename "$pkgdir"): must be string"; return 1 ;;
esac
ring=$(jq -r '.release_ring // ""' "$metadata")
case "$ring" in
""|fast) ;;
*) echo "invalid release_ring for $(basename "$pkgdir"): $ring"; return 1 ;;
esac
pkgrel_type=$(jq -r 'if has("pkgrel") then .pkgrel | type else "missing" end' "$metadata")
case "$pkgrel_type" in
object|missing) ;;
*) echo "invalid pkgrel for $(basename "$pkgdir"): must be an object with optional suffix/offset"; return 1 ;;
esac
if ! jq -e '(.pkgrel // {}) | type == "object" and ((.suffix // 1) | type == "number" and floor == . and . >= 1) and ((.offset // 0) | type == "number" and floor == . and . >= 0)' "$metadata" >/dev/null; then
echo "invalid pkgrel for $(basename "$pkgdir"): must be an object with optional integer suffix >= 1 and offset >= 0"
return 1
fi
if ! jq -e '(.upstream_commit // "") | type == "string"' "$metadata" >/dev/null; then
echo "invalid upstream_commit for $(basename "$pkgdir"): must be a string"
return 1
fi
}