Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/src/lib/version/check.sh
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/version/check.sh')
-rw-r--r--src/lib/version/check.sh340
1 files changed, 340 insertions, 0 deletions
diff --git a/src/lib/version/check.sh b/src/lib/version/check.sh
new file mode 100644
index 0000000..35d07e2
--- /dev/null
+++ b/src/lib/version/check.sh
@@ -0,0 +1,340 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+[[ -z ${DEVTOOLS_INCLUDE_VERSION_CHECK_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_VERSION_CHECK_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+# shellcheck source=src/lib/common.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/common.sh
+# shellcheck source=src/lib/util/term.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/term.sh
+
+source /usr/share/makepkg/util/message.sh
+
+set -eo pipefail
+
+readonly PKGCTL_VERSION_CHECK_EXIT_UP_TO_DATE=0
+export PKGCTL_VERSION_CHECK_EXIT_UP_TO_DATE
+readonly PKGCTL_VERSION_CHECK_EXIT_OUT_OF_DATE=2
+export PKGCTL_VERSION_CHECK_EXIT_OUT_OF_DATE
+readonly PKGCTL_VERSION_CHECK_EXIT_FAILURE=3
+export PKGCTL_VERSION_CHECK_EXIT_FAILURE
+
+
+pkgctl_version_check_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [PKGBASE]...
+
+ Compares the versions of packages in the local packaging repository against
+ their latest upstream versions.
+
+ Upon execution, it generates a grouped list that provides detailed insights
+ into each package's status. For each package, it displays the current local
+ version alongside the latest version available upstream.
+
+ Outputs a summary of up-to-date packages, out-of-date packages, and any
+ check failures.
+
+ OPTIONS
+ -v, --verbose Display results including up-to-date versions
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} neovim vim
+_EOF_
+}
+
+pkgctl_version_check() {
+ local pkgbases=()
+ local verbose=0
+
+ local path status_file path pkgbase upstream_version result
+
+ local up_to_date=()
+ local out_of_date=()
+ local failure=()
+ local current_item=0
+ local section_separator=''
+ local exit_code=${PKGCTL_VERSION_CHECK_EXIT_UP_TO_DATE}
+
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_version_check_usage
+ exit 0
+ ;;
+ -v|--verbose)
+ verbose=1
+ shift
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ pkgbases=("$@")
+ break
+ ;;
+ esac
+ done
+
+ if ! command -v nvchecker &>/dev/null; then
+ die "The \"$_DEVTOOLS_COMMAND\" command requires 'nvchecker'"
+ fi
+
+ # Check if used without pkgbases in a packaging directory
+ if (( ${#pkgbases[@]} == 0 )); then
+ if [[ -f PKGBUILD ]]; then
+ pkgbases=(".")
+ else
+ pkgctl_version_check_usage
+ exit 1
+ fi
+ fi
+
+ # enable verbose mode when we only have a single item to check
+ if (( ${#pkgbases[@]} == 1 )); then
+ verbose=1
+ fi
+
+ # start a terminal spinner as checking versions takes time
+ status_dir=$(mktemp --tmpdir="${WORKDIR}" --directory pkgctl-version-check-spinner.XXXXXXXXXX)
+ term_spinner_start "${status_dir}"
+
+ for path in "${pkgbases[@]}"; do
+ pushd "${path}" >/dev/null
+
+ if [[ ! -f "PKGBUILD" ]]; then
+ die "No PKGBUILD found for ${path}"
+ fi
+
+ # update the current terminal spinner status
+ (( ++current_item ))
+ pkgctl_version_check_spinner \
+ "${status_dir}" \
+ "${#up_to_date[@]}" \
+ "${#out_of_date[@]}" \
+ "${#failure[@]}" \
+ "${current_item}" \
+ "${#pkgbases[@]}"
+
+ # reset common PKGBUILD variables
+ unset pkgbase pkgname arch source pkgver pkgrel validpgpkeys
+ # shellcheck source=contrib/makepkg/PKGBUILD.proto
+ . ./PKGBUILD
+ pkgbase=${pkgbase:-$pkgname}
+
+ if ! result=$(get_upstream_version); then
+ result="${BOLD}${pkgbase}${ALL_OFF}: ${result}"
+ failure+=("${result}")
+ popd >/dev/null
+ continue
+ fi
+ upstream_version=${result}
+
+ if ! result=$(vercmp "${upstream_version}" "${pkgver}"); then
+ result="${BOLD}${pkgbase}${ALL_OFF}: failed to compare version ${upstream_version} against ${pkgver}"
+ failure+=("${result}")
+ popd >/dev/null
+ continue
+ fi
+
+ if (( result == 0 )); then
+ result="${BOLD}${pkgbase}${ALL_OFF}: current version ${PURPLE}${pkgver}${ALL_OFF} is latest"
+ up_to_date+=("${result}")
+ elif (( result < 0 )); then
+ result="${BOLD}${pkgbase}${ALL_OFF}: current version ${PURPLE}${pkgver}${ALL_OFF} is newer than ${DARK_GREEN}${upstream_version}${ALL_OFF}"
+ up_to_date+=("${result}")
+ elif (( result > 0 )); then
+ result="${BOLD}${pkgbase}${ALL_OFF}: upgrade from version ${PURPLE}${pkgver}${ALL_OFF} to ${DARK_GREEN}${upstream_version}${ALL_OFF}"
+ out_of_date+=("${result}")
+ fi
+
+ popd >/dev/null
+ done
+
+ # stop the terminal spinner after all checks
+ term_spinner_stop "${status_dir}"
+
+ if (( verbose )) && (( ${#up_to_date[@]} > 0 )); then
+ printf "%sUp-to-date%s\n" "${section_separator}${BOLD}${UNDERLINE}" "${ALL_OFF}"
+ section_separator=$'\n'
+ for result in "${up_to_date[@]}"; do
+ msg_success " ${result}"
+ done
+ fi
+
+ if (( ${#failure[@]} > 0 )); then
+ exit_code=${PKGCTL_VERSION_CHECK_EXIT_FAILURE}
+ printf "%sFailure%s\n" "${section_separator}${BOLD}${UNDERLINE}" "${ALL_OFF}"
+ section_separator=$'\n'
+ for result in "${failure[@]}"; do
+ msg_error " ${result}"
+ done
+ fi
+
+ if (( ${#out_of_date[@]} > 0 )); then
+ exit_code=${PKGCTL_VERSION_CHECK_EXIT_OUT_OF_DATE}
+ printf "%sOut-of-date%s\n" "${section_separator}${BOLD}${UNDERLINE}" "${ALL_OFF}"
+ section_separator=$'\n'
+ for result in "${out_of_date[@]}"; do
+ msg_warn " ${result}"
+ done
+ fi
+
+ # Show summary when processing multiple packages
+ if (( ${#pkgbases[@]} > 1 )); then
+ printf '%s' "${section_separator}"
+ pkgctl_version_check_summary \
+ "${#up_to_date[@]}" \
+ "${#out_of_date[@]}" \
+ "${#failure[@]}"
+ fi
+
+ # return status based on results
+ return "${exit_code}"
+}
+
+get_upstream_version() {
+ local config=.nvchecker.toml
+ local output errors upstream_version
+ local output
+ local opts=()
+ local keyfile="${XDG_CONFIG_HOME:-${HOME}/.config}/nvchecker/keyfile.toml"
+
+ # check nvchecker config file
+ if ! errors=$(nvchecker_check_config "${config}"); then
+ printf "%s" "${errors}"
+ return 1
+ fi
+
+ # populate keyfile to nvchecker opts
+ if [[ -f ${keyfile} ]]; then
+ opts+=(--keyfile "${keyfile}")
+ fi
+
+ if ! output=$(nvchecker --file "${config}" --logger json "${opts[@]}" 2>&1 | \
+ jq --raw-output 'select(.level != "debug")'); then
+ printf "failed to run nvchecker: %s" "${output}"
+ return 1
+ fi
+
+ if ! errors=$(nvchecker_check_error "${output}"); then
+ printf "%s" "${errors}"
+ return 1
+ fi
+
+ if ! upstream_version=$(jq --raw-output --exit-status '.version' <<< "${output}"); then
+ printf "failed to select version from result"
+ return 1
+ fi
+
+ printf "%s" "${upstream_version}"
+ return 0
+}
+
+nvchecker_check_config() {
+ local config=$1
+
+ local restricted_properties=(keyfile httptoken token)
+ local property
+
+ # check if the config file exists
+ if [[ ! -f ${config} ]]; then
+ printf "configuration file not found: %s" "${config}"
+ return 1
+ fi
+
+ # check if config contains any restricted properties like secrets
+ for property in "${restricted_properties[@]}"; do
+ if grep --max-count=1 --quiet "^${property}" < "${config}"; then
+ printf "restricted property in %s: %s" "${config}" "${property}"
+ return 1
+ fi
+ done
+
+ # check if the config contains a pkgbase section
+ if [[ -n ${pkgbase} ]] && ! grep --max-count=1 --extended-regexp --quiet "^\\[\"?${pkgbase}\"?\\]" < "${config}"; then
+ printf "missing pkgbase section in %s: %s" "${config}" "${pkgbase}"
+ return 1
+ fi
+
+ # check if the config contains any section other than pkgbase
+ if [[ -n ${pkgbase} ]] && property=$(grep --max-count=1 --perl-regexp "^\\[(?!\"?${pkgbase}\"?\\]).+\\]" < "${config}"); then
+ printf "non-pkgbase section not supported in %s: %s" "${config}" "${property}"
+ return 1
+ fi
+}
+
+nvchecker_check_error() {
+ local result=$1
+ local errors
+
+ if ! errors=$(jq --raw-output --exit-status \
+ 'select(.level == "error") | "\(.event)" + if .error then ": \(.error)" else "" end' \
+ <<< "${result}"); then
+ return 0
+ fi
+
+ mapfile -t errors <<< "${errors}"
+ printf "%s\n" "${errors[@]}"
+ return 1
+}
+
+pkgctl_version_check_summary() {
+ local up_to_date_count=$1
+ local out_of_date_count=$2
+ local failure_count=$3
+
+ # print nothing if all stats are zero
+ if (( up_to_date_count == 0 )) && \
+ (( out_of_date_count == 0 )) && \
+ (( failure_count == 0 )); then
+ return 0
+ fi
+
+ # print summary for all none zero stats
+ printf "%sSummary%s\n" "${BOLD}${UNDERLINE}" "${ALL_OFF}"
+ if (( up_to_date_count > 0 )); then
+ msg_success " Up-to-date: ${BOLD}${up_to_date_count}${ALL_OFF}" 2>&1
+ fi
+ if (( failure_count > 0 )); then
+ msg_error " Failure: ${BOLD}${failure_count}${ALL_OFF}" 2>&1
+ fi
+ if (( out_of_date_count > 0 )); then
+ msg_warn " Out-of-date: ${BOLD}${out_of_date_count}${ALL_OFF}" 2>&1
+ fi
+}
+
+pkgctl_version_check_spinner() {
+ local status_dir=$1
+ local up_to_date_count=$2
+ local out_of_date_count=$3
+ local failure_count=$4
+ local current=$5
+ local total=$6
+
+ local percentage=$(( 100 * current / total ))
+ local tmp_file="${status_dir}/tmp"
+ local status_file="${status_dir}/status"
+
+ # print the current summary
+ pkgctl_version_check_summary \
+ "${up_to_date_count}" \
+ "${out_of_date_count}" \
+ "${failure_count}" > "${tmp_file}"
+
+ # print the progress status
+ printf "📡 Checking: %s/%s [%s] %%spinner%%" \
+ "${BOLD}${current}" "${total}" "${percentage}%${ALL_OFF}" \
+ >> "${tmp_file}"
+
+ # swap the status file
+ mv "${tmp_file}" "${status_file}"
+}