Files
omarchy-pkgs/helpers/package-metadata.sh
T
99234a4fbb Add schist-bin, the Schist image editor, to the fast ring (#293)
Schist is a layered image editor with PSD, Affinity and camera raw support,
developed by Infrawrench and packaged by its upstream author. The package
re-wraps the pacman-format payloads Schist's release workflow publishes for
x86_64 and aarch64, so the builder does no compiling, and both assets are
pinned by SHA-256.

Releases are tracked declaratively through the GitHub upstream provider,
which gains a "digests": true mode here: a vendor that publishes no checksum
manifest can have each asset's SHA-256 read from the digest GitHub's release
API reports, so the sync never downloads the artifacts. Exactly one of
"checksums" or "digests" must be set, and the provider enforces that itself
because scheduled runs reach it without the metadata validator.

Fresh releases wait 24 hours before the scheduled sync picks them up, as
mise-bin already does. vulkan-driver is an optional dependency rather than a
hard one: makepkg -s would otherwise satisfy the virtual package with
nvidia-utils in the build container, and Omarchy installs a Vulkan driver per
machine.

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-04 13:09:12 +02:00

515 lines
17 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", "channels": ["edge"] }
# { "source": "local", "channels": ["edge", "rc", "stable"] }
# { "source": "local", "min_release_age": "24h" }
# { "source": "local", "upstream": { "github": "owner/repo", "checksums": "SHASUMS256.txt", "assets": { "x86_64": "name-{tag}-x64.tar.xz" } } }
# { "source": "local", "upstream": { "github": "owner/repo", "digests": true, "assets": { "x86_64": "name-{tag}-x64.tar.xz" } } }
# { "source": "local", "upstream": { "git_tags": "https://example/repo.git", "tag_pattern": "v{pkgver}", "sources": { "any": ["https://example/archive/{tag}.tar.gz"] } } }
# { "source": "local", "upstream": { "npm": "@scope/package", "sources": { "any": ["{npm_tarball}"] } } }
#
# 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 -- including a non-string/non-number JSON type
# like false -- returns 1 so callers fail closed instead of silently dropping
# the hold. At most 9 digits: enough for three decades in seconds, and small
# enough that no suffix multiplication can overflow 64-bit arithmetic.
package_min_release_age_seconds() {
local pkgdir="$1" metadata raw
metadata=$(metadata_file_for_dir "$pkgdir")
if [[ ! -f "$metadata" ]]; then
echo 0
return 0
fi
# A present-but-empty value maps to "unparseable", not to "absent": only a
# missing key means no hold, so '"min_release_age": ""' cannot silently
# disable the quarantine.
raw=$(jq -r '
if has("min_release_age") | not then ""
elif (.min_release_age | type) == "string" or (.min_release_age | type) == "number" then
.min_release_age | tostring | if . == "" then "unparseable" else . end
else "unparseable" end
' "$metadata")
if [[ -z "$raw" ]]; then
echo 0
return 0
fi
[[ "$raw" =~ ^([0-9]{1,9})([smhd]?)$ ]] || return 1
# Forced base 10: bash arithmetic would otherwise read "010" as octal.
local n=$((10#${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" ]]
}
# Channel membership: where a package may be published. Packages without a
# `channels` key are members of every channel (they flow edge -> rc -> stable).
package_has_channels() {
local pkgdir="$1" metadata
metadata=$(metadata_file_for_dir "$pkgdir")
[[ -f "$metadata" ]] || return 1
jq -e 'has("channels")' "$metadata" >/dev/null
}
package_channels() {
local pkgdir="$1" metadata
metadata=$(metadata_file_for_dir "$pkgdir")
[[ -f "$metadata" ]] || return 0
jq -r '(.channels // [])[]' "$metadata"
}
package_in_channel() {
local pkgdir="$1" channel="$2"
package_has_channels "$pkgdir" || return 0
package_channels "$pkgdir" | grep -qx "$channel"
}
# A pinned package's version is set per release by the orchestrator on the rc
# branch, not by whatever the current checkout happens to say. Only a build
# running from that branch's worktree (OMARCHY_RC_PINS=1) may build it for rc;
# otherwise master's shipped pins would try to overwrite an in-flight RC with
# an older version.
package_is_pinned() {
local pkgdir="$1" metadata
metadata=$(metadata_file_for_dir "$pkgdir")
[[ -f "$metadata" ]] || return 1
[[ "$(jq -r 'if has("pinned") then .pinned else false end' "$metadata")" == "true" ]]
}
package_builds_for_mirror() {
local pkgdir="$1"
local mirror="$2"
package_has_pkgbuild "$pkgdir" || return 1
package_has_metadata "$pkgdir" || return 1
# An explicit `channels` key is the outer bound on where a package may be
# built at all.
if package_has_channels "$pkgdir" && ! package_in_channel "$pkgdir" "$mirror"; then
return 1
fi
case "$mirror" in
edge)
return 0
;;
rc)
# Fast-ring packages build for rc natively rather than being copied from
# stable: the rc channel's Arch base can be sitting anywhere between
# stable's snapshot and edge's, so an artifact linked against stable's
# libraries is not necessarily correct for rc.
package_is_pinned "$pkgdir" && { [[ -n "${OMARCHY_RC_PINS:-}" ]]; return; }
package_is_fast_ring "$pkgdir"
;;
stable)
package_is_pinned "$pkgdir" && return 1
package_is_fast_ring "$pkgdir"
;;
*)
return 1
;;
esac
}
# Whether `bin/repo advance` may carry this package into the given channel:
# a member of that channel that is not built there natively. Native builds are
# authoritative — advancing over them could pair a published filename with
# different bytes, which the R2 cache would never recover from.
package_moves_to_channel() {
local pkgdir="$1" channel="$2"
package_in_channel "$pkgdir" "$channel" || return 1
! package_builds_for_mirror "$pkgdir" "$channel"
}
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" metadata
metadata=$(metadata_file_for_dir "$pkgdir")
[[ -f "$metadata" ]] || return 1
# Any upstream key counts, valid or not: a malformed declaration must reach
# bin/sync-upstream and fail loudly there, not vanish from discovery.
jq -e 'has("upstream")' "$metadata" >/dev/null
}
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 ! jq -e 'if has("pinned") | not then true else (.pinned | type) == "boolean" end' "$metadata" >/dev/null; then
echo "invalid pinned for $(basename "$pkgdir"): must be boolean"
return 1
fi
if ! jq -e '
if has("channels") | not then true
else .channels | type == "array" and length > 0
and all(. == "edge" or . == "rc" or . == "stable")
and (unique | length) == length
end
' "$metadata" >/dev/null; then
echo "invalid channels for $(basename "$pkgdir"): must be a non-empty array of unique edge/rc/stable values"
return 1
fi
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
# `has` rather than `// {}`: jq's // treats false as absent, which would
# let "upstream": false slip through as an empty declaration.
if ! jq -e '
def valid_sources:
type == "object" and length > 0 and (to_entries | all(
(.key | test("\\A[a-z0-9_]+\\z"))
and (.value | type == "array" and length > 0 and all(type == "string" and length > 0))
));
if has("upstream") | not then true
elif (.upstream | type) != "object" then false
else .upstream |
([has("github"), has("git_tags"), has("npm")] | map(select(.)) | length) == 1
and if has("github") then
(.github | type == "string" and test("\\A[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+\\z"))
and (if has("checksums") then (.checksums | type == "string" and length > 0) else true end)
and (if has("digests") then (.digests | type == "boolean") else true end)
and (has("checksums") != (has("digests") and .digests == true))
and (.assets | type == "object" and length > 0 and (to_entries | all(
(.key | test("\\A[a-z0-9_]+\\z")) and (.value | type == "string" and length > 0)
)))
elif has("git_tags") then
(.git_tags | type == "string" and test("\\Ahttps://[^[:space:]]+\\.git\\z"))
and (.tag_pattern | type == "string" and (split("{pkgver}") | length) == 2)
and (.sources | valid_sources)
else
(.npm | type == "string" and test("\\A(@[a-z0-9_.-]+/)?[a-z0-9_.-]+\\z"))
and ((.dist_tag // "latest") | type == "string" and test("\\A[a-z0-9_.-]+\\z"))
and (.sources | valid_sources)
end
end
' "$metadata" >/dev/null; then
echo "invalid upstream for $(basename "$pkgdir"): configure exactly one valid github, git_tags, or npm provider"
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
}