Files
omarchy-pkgs/helpers/package-metadata.sh
T
Ryan Hughes 699261471a Replace mise's upstream hook with a declarative GitHub-releases provider
After the quarantine moved into the manifest, all mise-bin's hook still knew
was data: the repository, the checksum manifest name, and the asset filename
patterns. That now lives in .omarchy/package.json as an upstream block --

  "upstream": {
    "github": "jdx/mise",
    "checksums": "SHASUMS256.txt",
    "assets": { "x86_64": "mise-{tag}-linux-x64.tar.xz", ... }
  }

-- handled by helpers/upstream-github.sh inside bin/sync-upstream. The
provider walks the release feed (drafts/prereleases excluded), honors
min_release_age and BYPASS_MIN_RELEASE_AGE during selection, reports
published_at so the framework backstop still applies, fails closed on any
unreadable tag or timestamp, and skips the checksum fetch when the newest
qualifying release is already checked in.

upstream.sh remains the escape hatch for feeds that fit no convention
(openai-codex-desktop's Debian index, tmog's version.txt, t3code's
electron-builder manifest); declaring both is an error.
2026-08-24 19:30:33 -04:00

394 lines
11 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": "aur", "rebuild_on": ["qt6-base"] }
# { "source": "local" }
# { "source": "local", "min_release_age": "24h" }
# { "source": "local", "upstream": { "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "x86_64": "name-{tag}-x64.tar.xz" } } }
#
# 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
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" ]]
}
# Quarantine window for upstream releases, in seconds. Accepts a bare number
# of seconds or a number suffixed s/m/h/d ("24h", "2d"). Unset means 0 (no
# hold); an unparseable value returns 1 so callers fail closed instead of
# silently dropping the hold.
package_min_release_age_seconds() {
local pkgdir="$1" raw
raw=$(package_metadata_value "$pkgdir" '.min_release_age' "")
if [[ -z "$raw" ]]; then
echo 0
return 0
fi
[[ "$raw" =~ ^([0-9]+)([smhd]?)$ ]] || return 1
local n=${BASH_REMATCH[1]}
case "${BASH_REMATCH[2]}" in
""|s) echo "$n" ;;
m) echo $((n * 60)) ;;
h) echo $((n * 3600)) ;;
d) echo $((n * 86400)) ;;
esac
}
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" ]]
}
package_has_upstream_provider() {
local pkgdir="$1"
[[ -n "$(package_metadata_value "$pkgdir" '.upstream.github' "")" ]]
}
packages_for_upstream_sync() {
package_dirs | while IFS= read -r pkgdir; do
if package_has_upstream_hook "$pkgdir" || package_has_upstream_provider "$pkgdir"; then
basename "$pkgdir"
fi
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"
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
if ! package_min_release_age_seconds "$pkgdir" >/dev/null; then
echo "invalid min_release_age for $(basename "$pkgdir"): must be a number with optional s/m/h/d suffix"
return 1
fi
if ! jq -e '
(.upstream // {}) | type == "object"
and (if . == {} then true else
((.github // "") | type == "string" and test("\\A[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\\z"))
and ((.checksums // "") | type == "string" and length > 0)
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)
)))
end)
' "$metadata" >/dev/null; then
echo "invalid upstream for $(basename "$pkgdir"): needs github owner/repo, checksums asset name, and an assets arch->name map"
return 1
fi
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
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
}