Archived
A second review pass found the PKGBUILD rewriting could still go wrong in ways the pattern matching did not anticipate: an array element carrying a ")" in a comment left the tail of the old array behind, and jq's "$" also matches before a trailing newline, so a pkgver of "1.0\n" passed validation and then broke sed after the checksum arrays had already been written. Rather than chase each shape, prove the result. Every edit now lands on a scratch copy that is parsed with bash -n and read back to confirm it holds the version and checksums we meant to write, and only then replaces the PKGBUILD in a single rename. Corruption that slips past the matching fails loudly with the original untouched instead of landing in a pull request. The validation anchors are \A and \z accordingly, empty checksum lists are rejected rather than written as '', and the hook picks the newest stanza with vercmp so it agrees with the comparator the updater uses. Also stop the launcher probing /.config when HOME and XDG_CONFIG_HOME are both unset, and require a regular file, so a directory at that path is skipped instead of crashing the app on startup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
391 lines
10 KiB
Bash
Executable File
391 lines
10 KiB
Bash
Executable File
#!/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=()
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $0 [PACKAGE...]
|
|
|
|
Update packages that track an upstream vendor release feed instead of the AUR.
|
|
|
|
A package opts in by providing pkgbuilds/<package>/.omarchy/upstream.sh, a hook
|
|
that reports the newest upstream release as JSON on stdout:
|
|
|
|
{
|
|
"pkgver": "1.2.3",
|
|
"sha256sums": { "x86_64": ["<sha256>"], "aarch64": ["<sha256>"] }
|
|
}
|
|
|
|
Architecture keys become sha256sums_<arch> in the PKGBUILD; the key "any" means
|
|
the unsuffixed sha256sums array. An empty object ({}) reports no update.
|
|
|
|
When the reported version is newer than the checked-in one, pkgver and the
|
|
listed checksum arrays are rewritten and pkgrel is reset to 1.
|
|
|
|
Arguments:
|
|
PACKAGE One or more package names to update (optional)
|
|
|
|
Examples:
|
|
$0 # Update every package with an upstream hook
|
|
$0 openai-codex-desktop # Update specific packages
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--*)
|
|
print_error "Unknown option: $1"
|
|
exit 1
|
|
;;
|
|
*)
|
|
SPECIFIC_PACKAGES+=("$1")
|
|
shift
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if ! command -v vercmp >/dev/null 2>&1; then
|
|
print_error "vercmp not found: this needs pacman to decide whether a release is an upgrade"
|
|
exit 1
|
|
fi
|
|
|
|
print_header "Upstream Package Sync"
|
|
|
|
UPDATED=0
|
|
SKIPPED=0
|
|
FAILED=0
|
|
SPECIFIC_MODE=false
|
|
|
|
get_pkgver() {
|
|
local package_dir="$1"
|
|
|
|
grep -m1 '^pkgver=' "$package_dir/PKGBUILD" | cut -d= -f2- | tr -d "\"'"
|
|
}
|
|
|
|
assert_single_assignment() {
|
|
local pkgbuild="$1"
|
|
local pattern="$2"
|
|
local label="$3"
|
|
|
|
if [[ $(grep -c "$pattern" "$pkgbuild") -ne 1 ]]; then
|
|
print_error "Expected exactly one $label assignment in $pkgbuild"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
set_pkgbuild_scalar() {
|
|
local pkgbuild="$1"
|
|
local field="$2"
|
|
local value="$3"
|
|
|
|
assert_single_assignment "$pkgbuild" "^${field}=" "$field" || return 1
|
|
sed -i "s/^${field}=.*/${field}=${value}/" "$pkgbuild"
|
|
}
|
|
|
|
# Replace an array assignment, however many lines the original spans.
|
|
set_pkgbuild_array() {
|
|
local pkgbuild="$1"
|
|
local name="$2"
|
|
shift 2
|
|
local values=("$@")
|
|
|
|
assert_single_assignment "$pkgbuild" "^${name}=(" "$name" || return 1
|
|
|
|
if [[ ${#values[@]} -eq 0 ]]; then
|
|
print_error "No values to write for ${name}"
|
|
return 1
|
|
fi
|
|
|
|
local block="$TEMP_DIR/array-block"
|
|
if [[ ${#values[@]} -eq 1 ]]; then
|
|
printf "%s=('%s')\n" "$name" "${values[0]}" > "$block" || return 1
|
|
else
|
|
{
|
|
printf '%s=(\n' "$name"
|
|
printf " '%s'\n" "${values[@]}"
|
|
printf ')\n'
|
|
} > "$block" || return 1
|
|
fi
|
|
|
|
local rewritten="$TEMP_DIR/pkgbuild-rewritten"
|
|
if ! awk -v prefix="${name}=(" -v block="$block" '
|
|
!replaced && index($0, prefix) == 1 {
|
|
while ((getline line < block) > 0) print line
|
|
close(block)
|
|
replaced = 1
|
|
# A ")" anywhere past the opening closes the array; testing for one at end
|
|
# of line instead would treat a trailing comment as a continuation and eat
|
|
# every line up to the next ")".
|
|
if (index(substr($0, length(prefix) + 1), ")") == 0) skipping = 1
|
|
next
|
|
}
|
|
skipping { if ($0 ~ /\)/) skipping = 0; next }
|
|
{ print }
|
|
' "$pkgbuild" > "$rewritten"; then
|
|
print_error "Failed to rewrite ${name} in $pkgbuild"
|
|
rm -f "$rewritten"
|
|
return 1
|
|
fi
|
|
|
|
mv "$rewritten" "$pkgbuild"
|
|
}
|
|
|
|
# pacman's own comparator, because nothing else agrees with it at the corners:
|
|
# sort -V calls 1.0a newer than 1.0, vercmp calls it older, and pacman is what
|
|
# decides whether a published package is an upgrade.
|
|
version_is_newer() {
|
|
local candidate="$1"
|
|
local current="$2"
|
|
|
|
[[ "$candidate" != "$current" ]] || return 1
|
|
[[ "$(vercmp "$candidate" "$current")" -gt 0 ]]
|
|
}
|
|
|
|
validate_release() {
|
|
local release="$1"
|
|
|
|
# pkgver is written into the PKGBUILD, which makepkg sources as shell, so it
|
|
# is held to pacman's own character set rather than merely being non-empty.
|
|
# The anchors are \A and \z, not ^ and $: jq's $ also matches before a
|
|
# trailing newline, which would let "1.0\n" through and break the rewrite.
|
|
jq -e '
|
|
(.pkgver | type == "string" and test("\\A[A-Za-z0-9._+]+\\z"))
|
|
and (.sha256sums | type == "object" and length > 0)
|
|
and (.sha256sums | to_entries | all(
|
|
.key | test("\\A[a-z0-9_]+\\z")
|
|
))
|
|
and (.sha256sums | to_entries | all(
|
|
.value | type == "array" and length > 0 and all(test("\\A[0-9a-f]{64}\\z"))
|
|
))
|
|
' <<<"$release" >/dev/null
|
|
}
|
|
|
|
# Confirm the rewritten PKGBUILD parses and actually holds what we meant to put
|
|
# in it. Editing shell with awk and sed can go wrong in ways no amount of
|
|
# pattern-matching anticipates -- an array element carrying a ")" in a comment,
|
|
# say -- so the result is checked rather than trusted.
|
|
verify_pkgbuild() {
|
|
local pkgbuild="$1"
|
|
local release="$2"
|
|
local pkgver="$3"
|
|
shift 3
|
|
local arrays=("$@")
|
|
|
|
if ! bash -n "$pkgbuild" 2>/dev/null; then
|
|
print_error "Rewritten PKGBUILD is not valid shell"
|
|
return 1
|
|
fi
|
|
|
|
local dump
|
|
if ! dump=$(CARCH=x86_64 bash -c '
|
|
source "$1" >/dev/null 2>&1 || exit 1
|
|
printf "pkgver\t%s\n" "$pkgver"
|
|
printf "pkgrel\t%s\n" "$pkgrel"
|
|
for name in "${@:2}"; do
|
|
declare -n array="$name"
|
|
printf "%s\t%s\n" "$name" "${array[*]}"
|
|
done
|
|
' _ "$pkgbuild" "${arrays[@]}" 2>/dev/null); then
|
|
print_error "Rewritten PKGBUILD could not be read back"
|
|
return 1
|
|
fi
|
|
|
|
local expected
|
|
expected=$(
|
|
printf 'pkgver\t%s\n' "$pkgver"
|
|
printf 'pkgrel\t1\n'
|
|
local array arch
|
|
for array in "${arrays[@]}"; do
|
|
arch="${array#sha256sums}"
|
|
arch="${arch#_}"
|
|
[[ -n "$arch" ]] || arch="any"
|
|
printf '%s\t%s\n' "$array" \
|
|
"$(jq -r --arg arch "$arch" '.sha256sums[$arch] | join(" ")' <<<"$release")"
|
|
done
|
|
)
|
|
|
|
if [[ "$dump" != "$expected" ]]; then
|
|
print_error "Rewritten PKGBUILD does not hold the reported release"
|
|
diff <(echo "$expected") <(echo "$dump") | sed 's/^/ /' >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
apply_release() {
|
|
local package_dir="$1"
|
|
local release="$2"
|
|
local pkgver="$3"
|
|
local pkgbuild="$package_dir/PKGBUILD"
|
|
|
|
local arch array values
|
|
local arrays=()
|
|
|
|
while IFS= read -r arch; do
|
|
if [[ "$arch" == "any" ]]; then
|
|
array="sha256sums"
|
|
else
|
|
array="sha256sums_$arch"
|
|
fi
|
|
arrays+=("$array")
|
|
done < <(jq -r '.sha256sums | keys[]' <<<"$release")
|
|
|
|
# validate_release guarantees at least one entry, so an empty list here means
|
|
# jq died inside the process substitution rather than that there is nothing
|
|
# to do.
|
|
if [[ ${#arrays[@]} -eq 0 ]]; then
|
|
print_error "Could not read the checksum architectures from the reported release"
|
|
return 1
|
|
fi
|
|
|
|
# Every edit lands on a scratch copy that replaces the PKGBUILD in one rename
|
|
# at the end, so a failure part way through leaves the original untouched
|
|
# rather than half updated.
|
|
local scratch="$pkgbuild.sync-upstream"
|
|
cp "$pkgbuild" "$scratch" || return 1
|
|
|
|
if ! (
|
|
assert_single_assignment "$scratch" '^pkgver=' pkgver || exit 1
|
|
assert_single_assignment "$scratch" '^pkgrel=' pkgrel || exit 1
|
|
|
|
for array in "${arrays[@]}"; do
|
|
arch="${array#sha256sums}"
|
|
arch="${arch#_}"
|
|
[[ -n "$arch" ]] || arch="any"
|
|
|
|
mapfile -t values < <(jq -r --arg arch "$arch" '.sha256sums[$arch][]' <<<"$release")
|
|
set_pkgbuild_array "$scratch" "$array" "${values[@]}" || exit 1
|
|
done
|
|
|
|
set_pkgbuild_scalar "$scratch" pkgver "$pkgver" || exit 1
|
|
set_pkgbuild_scalar "$scratch" pkgrel 1 || exit 1
|
|
|
|
verify_pkgbuild "$scratch" "$release" "$pkgver" "${arrays[@]}" || exit 1
|
|
); then
|
|
rm -f "$scratch"
|
|
return 1
|
|
fi
|
|
|
|
chmod --reference="$pkgbuild" "$scratch"
|
|
mv "$scratch" "$pkgbuild"
|
|
}
|
|
|
|
sync_package() {
|
|
local package="$1"
|
|
local package_dir="$PKGBUILDS_DIR/$package"
|
|
local hook="$package_dir/.omarchy/upstream.sh"
|
|
|
|
if [[ ! -f "$package_dir/PKGBUILD" ]]; then
|
|
print_error "Package $package has no PKGBUILD"
|
|
((++FAILED))
|
|
return 0
|
|
fi
|
|
|
|
if [[ ! -f "$hook" ]]; then
|
|
if [[ "$SPECIFIC_MODE" == true ]]; then
|
|
print_error "Package $package is missing .omarchy/upstream.sh"
|
|
((++FAILED))
|
|
else
|
|
print_info "Skipping $package: no upstream hook"
|
|
((++SKIPPED))
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
print_info "Checking $package for upstream releases..."
|
|
|
|
local release
|
|
if ! release=$(cd "$package_dir" && PACKAGE_NAME="$package" bash .omarchy/upstream.sh); then
|
|
print_error "Upstream hook failed for $package"
|
|
((++FAILED))
|
|
return 0
|
|
fi
|
|
|
|
if ! jq -e . >/dev/null 2>&1 <<<"$release"; then
|
|
print_error "Upstream hook for $package did not report valid JSON"
|
|
((++FAILED))
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$(jq -r 'has("pkgver")' <<<"$release")" == "false" ]]; then
|
|
print_info " No upstream update reported"
|
|
((++SKIPPED))
|
|
return 0
|
|
fi
|
|
|
|
if ! validate_release "$release"; then
|
|
print_error "Upstream hook for $package reported a malformed release"
|
|
((++FAILED))
|
|
return 0
|
|
fi
|
|
|
|
local pkgver current_pkgver
|
|
pkgver=$(jq -r '.pkgver' <<<"$release")
|
|
current_pkgver=$(get_pkgver "$package_dir")
|
|
|
|
if [[ -z "$current_pkgver" ]]; then
|
|
print_error "Could not read pkgver from $package_dir/PKGBUILD"
|
|
((++FAILED))
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$pkgver" == "$current_pkgver" ]]; then
|
|
print_info " Already at $current_pkgver"
|
|
((++SKIPPED))
|
|
return 0
|
|
fi
|
|
|
|
if ! version_is_newer "$pkgver" "$current_pkgver"; then
|
|
print_warning " Upstream reports $pkgver, older than the checked-in $current_pkgver; leaving it alone"
|
|
((++SKIPPED))
|
|
return 0
|
|
fi
|
|
|
|
if ! apply_release "$package_dir" "$release" "$pkgver"; then
|
|
print_error "Failed to update $package"
|
|
((++FAILED))
|
|
return 0
|
|
fi
|
|
|
|
print_success " $current_pkgver -> $pkgver"
|
|
((++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_upstream_sync)
|
|
fi
|
|
|
|
echo ""
|
|
if [[ $FAILED -gt 0 ]]; then
|
|
print_error "Upstream sync completed with failures"
|
|
else
|
|
print_success "Upstream sync complete!"
|
|
fi
|
|
echo " Target: $PKGBUILDS_DIR"
|
|
echo " Updated: $UPDATED"
|
|
echo " Skipped: $SKIPPED"
|
|
echo " Failed: $FAILED"
|
|
|
|
if [[ $FAILED -gt 0 ]]; then
|
|
exit 1
|
|
fi
|