Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/api/archweb.sh57
-rw-r--r--src/lib/api/gitlab.sh186
-rw-r--r--src/lib/aur.sh65
-rw-r--r--src/lib/aur/drop-from-repo.sh168
-rw-r--r--src/lib/build/build.sh178
-rw-r--r--src/lib/cache.sh22
-rw-r--r--src/lib/common.sh39
-rw-r--r--src/lib/db.sh2
-rw-r--r--src/lib/release.sh34
-rw-r--r--src/lib/repo/clone.sh41
-rw-r--r--src/lib/repo/configure.sh33
-rw-r--r--src/lib/repo/web.sh23
-rw-r--r--src/lib/search.sh308
-rw-r--r--src/lib/util/git.sh10
-rw-r--r--src/lib/util/makepkg.sh37
-rw-r--r--src/lib/util/pacman.sh12
-rw-r--r--src/lib/util/pkgbuild.sh43
-rw-r--r--src/lib/util/srcinfo.sh69
-rw-r--r--src/lib/util/term.sh182
-rw-r--r--src/lib/valid-build-install.sh11
-rw-r--r--src/lib/valid-inspect.sh10
-rw-r--r--src/lib/valid-repos.sh4
-rw-r--r--src/lib/valid-search.sh11
-rw-r--r--src/lib/valid-tags.sh2
-rw-r--r--src/lib/version.sh65
-rw-r--r--src/lib/version/check.sh340
-rw-r--r--src/lib/version/upgrade.sh248
-rw-r--r--src/lib/version/version.sh47
28 files changed, 2109 insertions, 138 deletions
diff --git a/src/lib/api/archweb.sh b/src/lib/api/archweb.sh
new file mode 100644
index 0000000..34c7a9c
--- /dev/null
+++ b/src/lib/api/archweb.sh
@@ -0,0 +1,57 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_API_ARCHWEB_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_API_ARCHWEB_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+# shellcheck source=src/lib/common.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/common.sh
+
+set -e
+set -o pipefail
+
+
+archweb_query_all_packages() {
+ [[ -z ${WORKDIR:-} ]] && setup_workdir
+
+ stat_busy "Query all released packages"
+ mapfile -t pkgbases < <(
+ curl --location --show-error --no-progress-meter --fail --retry 3 --retry-delay 3 \
+ "${PKGBASE_MAINTAINER_URL}" 2> "${WORKDIR}/error" \
+ | jq --raw-output --exit-status 'keys[]' 2> "${WORKDIR}/error"
+ )
+ if ! wait $!; then
+ stat_failed
+ print_workdir_error
+ return 1
+ fi
+ stat_done
+
+ printf "%s\n" "${pkgbases[@]}"
+ return 0
+}
+
+
+archweb_query_maintainer_packages() {
+ local maintainer=$1
+
+ [[ -z ${WORKDIR:-} ]] && setup_workdir
+
+ stat_busy "Query maintainer packages"
+ mapfile -t pkgbases < <(
+ curl --location --show-error --no-progress-meter --fail --retry 3 --retry-delay 3 \
+ "${PKGBASE_MAINTAINER_URL}" 2> "${WORKDIR}/error" \
+ | jq --raw-output --exit-status '. as $parent | keys[] | select(. as $key | $parent[$key] | index("'"${maintainer}"'"))' 2> "${WORKDIR}/error"
+ )
+ if ! wait $!; then
+ stat_failed
+ print_workdir_error
+ return 1
+ fi
+ stat_done
+
+ printf "%s\n" "${pkgbases[@]}"
+ return 0
+}
diff --git a/src/lib/api/gitlab.sh b/src/lib/api/gitlab.sh
index e5f4237..115e58c 100644
--- a/src/lib/api/gitlab.sh
+++ b/src/lib/api/gitlab.sh
@@ -13,13 +13,63 @@ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/config.sh
set -e
+graphql_api_call() {
+ local outfile=$1
+ local request=$2
+ local node_type=$3
+ local data=$4
+ local hasNextPage cursor
+
+ # empty token
+ if [[ -z "${GITLAB_TOKEN}" ]]; then
+ msg_error " api call failed: No token provided"
+ return 1
+ fi
+
+ [[ -z ${WORKDIR:-} ]] && setup_workdir
+ api_workdir=$(mktemp --tmpdir="${WORKDIR}" --directory pkgctl-gitlab-api.XXXXXXXXXX)
+
+ # normalize graphql data and prepare query
+ data="${data//\"/\\\"}"
+ data='{
+ "query": "'"${data}"'"
+ }'
+ data="${data//$'\t'/ }"
+ data="${data//$'\n'/}"
+
+ cursor=""
+ hasNextPage=true
+ while [[ ${hasNextPage} == true ]]; do
+ data=$(sed -E 's|after: \\"[a-zA-Z0-9]*\\"|after: \\"'"${cursor}"'\\"|' <<< "${data}")
+ result="${api_workdir}/result.${cursor}"
+
+ if ! curl --request "${request}" \
+ --url "https://${GITLAB_HOST}/api/graphql" \
+ --header "Authorization: Bearer ${GITLAB_TOKEN}" \
+ --header "Content-Type: application/json" \
+ --data "${data}" \
+ --output "${result}" \
+ --silent; then
+ msg_error " api call failed: $(cat "${outfile}")"
+ return 1
+ fi
+
+ hasNextPage=$(jq --raw-output ".data | .${node_type} | .pageInfo | .hasNextPage" < "${result}")
+ cursor=$(jq --raw-output ".data | .${node_type} | .pageInfo | .endCursor" < "${result}")
+
+ cp "${result}" "${api_workdir}/tmp"
+ jq ".data.${node_type}.nodes" "${api_workdir}/tmp" > "${result}"
+ done
+
+ jq --slurp add "${api_workdir}"/result.* > "${outfile}"
+ return 0
+}
gitlab_api_call() {
local outfile=$1
local request=$2
local endpoint=$3
local data=${4:-}
- local error
# empty token
if [[ -z "${GITLAB_TOKEN}" ]]; then
@@ -38,27 +88,113 @@ gitlab_api_call() {
return 1
fi
+ if ! gitlab_check_api_errors "${outfile}"; then
+ return 1
+ fi
+
+ return 0
+}
+
+gitlab_api_call_paged() {
+ local outfile=$1
+ local status_file=$2
+ local request=$3
+ local endpoint=$4
+ local data=${5:-}
+ local result header
+
+ # empty token
+ if [[ -z "${GITLAB_TOKEN}" ]]; then
+ msg_error " api call failed: No token provided"
+ return 1
+ fi
+
+ [[ -z ${WORKDIR:-} ]] && setup_workdir
+ api_workdir=$(mktemp --tmpdir="${WORKDIR}" --directory pkgctl-gitlab-api.XXXXXXXXXX)
+ tmp_file=$(mktemp --tmpdir="${api_workdir}" spinner.tmp.XXXXXXXXXX)
+
+ local next_page=1
+ local total_pages=1
+
+ while [[ -n "${next_page}" ]]; do
+ percentage=$(( 100 * next_page / total_pages ))
+ printf "📡 Querying GitLab: %s/%s [%s] %%spinner%%" \
+ "${BOLD}${next_page}" "${total_pages}" "${percentage}%${ALL_OFF}" \
+ > "${tmp_file}"
+ mv "${tmp_file}" "${status_file}"
+
+ result="${api_workdir}/result.${next_page}"
+ header="${api_workdir}/header"
+ if ! curl --request "${request}" \
+ --get \
+ --url "https://${GITLAB_HOST}/api/v4/${endpoint}&per_page=100&page=${next_page}" \
+ --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
+ --header "Content-Type: application/json" \
+ --data-urlencode "${data}" \
+ --dump-header "${header}" \
+ --output "${result}" \
+ --silent; then
+ msg_error " api call failed: $(cat "${result}")"
+ return 1
+ fi
+
+ if ! gitlab_check_api_errors "${result}"; then
+ return 1
+ fi
+
+ next_page=$(grep "x-next-page" "${header}" | tr -d '\r' | awk '{ print $2 }')
+ total_pages=$(grep "x-total-pages" "${header}" | tr -d '\r' | awk '{ print $2 }')
+ done
+
+ jq --slurp add "${api_workdir}"/result.* > "${outfile}"
+ return 0
+}
+
+gitlab_check_api_errors() {
+ local file=$1
+ local error
+
+ # search API only returns an array, no errors
+ if [[ $(jq --raw-output 'type' < "${file}") == "array" ]]; then
+ return 0
+ fi
+
# check for general purpose api error
- if error=$(jq --raw-output --exit-status '.error' < "${outfile}"); then
+ if error=$(jq --raw-output --exit-status '.error' < "${file}"); then
msg_error " api call failed: ${error}"
return 1
fi
# check for api specific error messages
- if ! jq --raw-output --exit-status '.id' < "${outfile}" >/dev/null; then
- if jq --raw-output --exit-status '.message | keys[]' < "${outfile}" &>/dev/null; then
+ if ! jq --raw-output --exit-status '.id' < "${file}" >/dev/null; then
+ if jq --raw-output --exit-status '.message | keys[]' < "${file}" &>/dev/null; then
while read -r error; do
msg_error " api call failed: ${error}"
- done < <(jq --raw-output --exit-status '.message|to_entries|map("\(.key) \(.value[])")[]' < "${outfile}")
- elif error=$(jq --raw-output --exit-status '.message' < "${outfile}"); then
+ done < <(jq --raw-output --exit-status '.message|to_entries|map("\(.key) \(.value[])")[]' < "${file}")
+ elif error=$(jq --raw-output --exit-status '.message' < "${file}"); then
msg_error " api call failed: ${error}"
fi
return 1
fi
-
return 0
}
+graphql_check_api_errors() {
+ local file=$1
+ local error
+
+ # early exit if we do not have errors
+ if ! jq --raw-output --exit-status '.errors[]' < "${file}" &>/dev/null; then
+ return 0
+ fi
+
+ # check for api specific error messages
+ while read -r error; do
+ msg_error " api call failed: ${error}"
+ done < <(jq --raw-output --exit-status '.errors[].message' < "${file}")
+ return 1
+}
+
gitlab_api_get_user() {
local outfile username
@@ -81,6 +217,23 @@ gitlab_api_get_user() {
return 0
}
+gitlab_api_get_project_name_mapping() {
+ local query=$1
+ local outfile
+
+ [[ -z ${WORKDIR:-} ]] && setup_workdir
+ outfile=$(mktemp --tmpdir="${WORKDIR}" pkgctl-gitlab-api.XXXXXXXXXX)
+
+ # query user details
+ if ! graphql_api_call "${outfile}" POST projects "${query}"; then
+ msg_warn " Invalid token provided?"
+ exit 1
+ fi
+
+ cat "${outfile}"
+ return 0
+}
+
# Convert arbitrary project names to GitLab valid path names.
#
# GitLab has several limitations on project and group names and also maintains
@@ -130,3 +283,22 @@ gitlab_api_create_project() {
printf "%s" "${path}"
return 0
}
+
+# TODO: parallelize
+# https://docs.gitlab.com/ee/api/search.html#scope-blobs
+gitlab_api_search() {
+ local search=$1
+ local status_file=$2
+ local outfile
+
+ [[ -z ${WORKDIR:-} ]] && setup_workdir
+ outfile=$(mktemp --tmpdir="${WORKDIR}" pkgctl-gitlab-api.XXXXXXXXXX)
+
+ if ! gitlab_api_call_paged "${outfile}" "${status_file}" GET "/groups/archlinux%2fpackaging%2fpackages/search?scope=blobs" "search=${search}"; then
+ return 1
+ fi
+
+ cat "${outfile}"
+
+ return 0
+}
diff --git a/src/lib/aur.sh b/src/lib/aur.sh
new file mode 100644
index 0000000..24cbb62
--- /dev/null
+++ b/src/lib/aur.sh
@@ -0,0 +1,65 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_AUR_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_AUR_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+
+set -eo pipefail
+
+
+pkgctl_aur_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [COMMAND] [OPTIONS]
+
+ Interact with the Arch User Repository (AUR).
+
+ Provides a suite of tools designed for managing and interacting with the Arch
+ User Repository (AUR). It simplifies various tasks related to AUR, including
+ importing repositories, managing packages, and transitioning packages between
+ the official repositories and the AUR.
+
+ COMMANDS
+ drop-from-repo Drop a package from the official repository to the AUR
+
+ OPTIONS
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} drop-from-repo libfoo
+_EOF_
+}
+
+pkgctl_aur() {
+ if (( $# < 1 )); then
+ pkgctl_aur_usage
+ exit 0
+ fi
+
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_aur_usage
+ exit 0
+ ;;
+ drop-from-repo)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/aur/drop-from-repo.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/aur/drop-from-repo.sh
+ pkgctl_aur_drop_from_repo "$@"
+ exit 0
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ die "invalid command: %s" "$1"
+ ;;
+ esac
+ done
+}
diff --git a/src/lib/aur/drop-from-repo.sh b/src/lib/aur/drop-from-repo.sh
new file mode 100644
index 0000000..6ebe12a
--- /dev/null
+++ b/src/lib/aur/drop-from-repo.sh
@@ -0,0 +1,168 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_AUR_DROP_FROM_REPO_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_AUR_DROP_FROM_REPO_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/db/remove.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/db/remove.sh
+# shellcheck source=src//lib/util/pacman.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/pacman.sh
+
+source /usr/share/makepkg/util/message.sh
+
+set -eo pipefail
+
+
+pkgctl_aur_drop_from_repo_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [PATH]...
+
+ Drops a specified package from the official repositories to the Arch
+ User Repository.
+
+ This command requires a local Git clone of the package repository. It
+ reconfigures the repository for AUR compatibility and pushes it to the
+ AUR. Afterwards, the package is removed from the official repository.
+
+ By default, the package is automatically disowned in the AUR.
+
+ OPTIONS
+ --no-disown Do not disown the package on the AUR
+ -f, --force Force push to the AUR overwriting the remote repository
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} foo
+ $ ${COMMAND} --no-disown --force
+_EOF_
+}
+
+pkgctl_aur_drop_from_repo() {
+ # options
+ local paths=()
+ local DISOWN=1
+ local FORCE=0
+
+ # variables
+ local path realpath pkgbase pkgrepo remote_url
+
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_aur_drop_from_repo_usage
+ exit 0
+ ;;
+ --no-disown)
+ DISOWN=0
+ shift
+ ;;
+ -f|--force)
+ FORCE=1
+ shift
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ die "Invalid argument: %s" "$1"
+ ;;
+ *)
+ paths=("$@")
+ break
+ ;;
+ esac
+ done
+
+ # check if invoked without any path from within a packaging repo
+ if (( ${#paths[@]} == 0 )); then
+ if [[ -f PKGBUILD ]]; then
+ paths=(".")
+ else
+ pkgctl_aur_drop_from_repo_usage
+ exit 1
+ fi
+ fi
+
+ for path in "${paths[@]}"; do
+ if ! realpath=$(realpath -e "${path}"); then
+ die "No such directory: ${path}"
+ fi
+
+ pkgbase=$(basename "${realpath}")
+ pkgbase=${pkgbase%.git}
+
+ if [[ ! -d "${path}/.git" ]]; then
+ die "Not a Git repository: ${path}"
+ fi
+
+ pushd "${path}" >/dev/null
+
+ if [[ ! -f PKGBUILD ]]; then
+ die 'PKGBUILD not found in %s' "${path}"
+ fi
+
+ msg "Dropping ${pkgbase} to the AUR"
+
+ remote_url="${AUR_URL_SSH}:${pkgbase}.git"
+ if ! git remote add origin "${remote_url}" &>/dev/null; then
+ git remote set-url origin "${remote_url}"
+ fi
+
+ # move the main branch to master
+ if [[ $(git symbolic-ref --quiet --short HEAD) == main ]]; then
+ git branch --move master
+ git config branch.master.merge refs/heads/master
+ fi
+
+ # auto generate .SRCINFO if not already present
+ if [[ -z "$(git ls-tree -r HEAD --name-only .SRCINFO)" ]]; then
+ stat_busy 'Generating .SRCINFO'
+ makepkg --printsrcinfo > .SRCINFO
+ stat_done
+
+ git add --force -- .SRCINFO
+ git commit --quiet --message "Adding .SRCINFO" -- .SRCINFO
+ fi
+
+ msg "Pushing ${pkgbase} to the AUR"
+ if (( FORCE )); then
+ AUR_OVERWRITE=1 \
+ GIT_SSH_COMMAND="ssh -o SendEnv=AUR_OVERWRITE" \
+ git push --force origin master
+ else
+ git push origin master
+ fi
+
+ # update the local default branch in case this clone is used in the future
+ git remote set-head origin master
+
+ if (( DISOWN )); then
+ msg "Disowning ${pkgbase} on the AUR"
+ # shellcheck disable=SC2029
+ ssh "${AUR_URL_SSH}" disown "${pkgbase}"
+ fi
+
+ # auto-detection of the repo to remove from
+ if ! pkgrepo=$(get_pacman_repo_from_pkgbuild PKGBUILD); then
+ die 'Failed to get pacman repo'
+ fi
+
+ msg "Deleting ${pkgbase} from the official repository"
+ if [[ -z "${pkgrepo}" ]]; then
+ warning 'Did not find %s in any repository, please delete manually' "${pkgbase}"
+ else
+ msg2 " repo: ${pkgrepo}"
+ pkgctl_db_remove "${pkgrepo}" "${pkgbase}"
+ fi
+
+ popd >/dev/null
+ done
+}
diff --git a/src/lib/build/build.sh b/src/lib/build/build.sh
index 3394395..171bb9a 100644
--- a/src/lib/build/build.sh
+++ b/src/lib/build/build.sh
@@ -14,18 +14,25 @@ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/db/update.sh
source "${_DEVTOOLS_LIBRARY_DIR}"/lib/release.sh
# shellcheck source=src/lib/util/git.sh
source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/git.sh
+# shellcheck source=src/lib/util/srcinfo.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/srcinfo.sh
# shellcheck source=src/lib/util/pacman.sh
source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/pacman.sh
+# shellcheck source=src/lib/util/pkgbuild.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/pkgbuild.sh
+# shellcheck source=src/lib/valid-build-install.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/valid-build-install.sh
# shellcheck source=src/lib/valid-repos.sh
source "${_DEVTOOLS_LIBRARY_DIR}"/lib/valid-repos.sh
# shellcheck source=src/lib/valid-tags.sh
source "${_DEVTOOLS_LIBRARY_DIR}"/lib/valid-tags.sh
+# shellcheck source=src/lib/valid-inspect.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/valid-inspect.sh
source /usr/share/makepkg/util/config.sh
source /usr/share/makepkg/util/message.sh
-set -e
-set -o pipefail
+set -eo pipefail
pkgctl_build_usage() {
@@ -42,19 +49,24 @@ pkgctl_build_usage() {
BUILD OPTIONS
--arch ARCH Specify architectures to build for (disables auto-detection)
- --repo REPO Specify a target repository (disables auto-detection)
+ --repo REPO Specify target repository for new packages not in any official repo
-s, --staging Build against the staging counterpart of the auto-detected repo
-t, --testing Build against the testing counterpart of the auto-detected repo
-o, --offload Build on a remote server and transfer artifacts afterwards
-c, --clean Recreate the chroot before building
- -I, --install FILE Install a package into the working copy of the chroot
+ --inspect WHEN Spawn an interactive shell to inspect the chroot (never, always, failure)
-w, --worker SLOT Name of the worker slot, useful for concurrent builds (disables automatic names)
--nocheck Do not run the check() function in the PKGBUILD
+ INSTALL OPTIONS
+ -I, --install-to-chroot FILE Install a package to the working copy of the chroot
+ -i, --install-to-host MODE Install the built package to the host system, possible modes are 'all' and 'auto'
+
PKGBUILD OPTIONS
--pkgver=PKGVER Set pkgver, reset pkgrel and update checksums
--pkgrel=PKGREL Set pkgrel to a given value
--rebuild Increment the current pkgrel variable
+ --update-checksums Force computation and update of the checksums (disables auto-detection)
-e, --edit Edit the PKGBUILD before building
RELEASE OPTIONS
@@ -77,8 +89,7 @@ pkgctl_build_check_option_group_repo() {
local repo=$2
local testing=$3
local staging=$4
- if ( (( testing )) && (( staging )) ) ||
- ( [[ $repo =~ ^.*-(staging|testing)$ ]] && ( (( testing )) || (( staging )) )); then
+ if [[ -n "${repo}" ]] || (( testing )) || (( staging )); then
die "The argument '%s' cannot be used with one or more of the other specified arguments" "${option}"
exit 1
fi
@@ -104,7 +115,7 @@ pkgctl_build() {
exit 1
fi
- local UPDPKGSUMS=0
+ local UPDATE_CHECKSUMS=0
local EDIT=0
local REBUILD=0
local OFFLOAD=0
@@ -112,6 +123,7 @@ pkgctl_build() {
local TESTING=0
local RELEASE=0
local DB_UPDATE=0
+ local INSTALL_TO_HOST=none
local REPO=
local PKGVER=
@@ -124,12 +136,13 @@ pkgctl_build() {
local MAKECHROOT_OPTIONS=()
local RELEASE_OPTIONS=()
local MAKEPKG_OPTIONS=()
+ local INSTALL_HOST_PACKAGES=()
local WORKER=
local WORKER_SLOT=
# variables
- local path pkgbase pkgrepo source
+ local _arch path pkgbase pkgrepo source pkgbuild_checksum current_checksum
while (( $# )); do
case $1 in
@@ -139,18 +152,19 @@ pkgctl_build() {
;;
--repo)
(( $# <= 1 )) && die "missing argument for %s" "$1"
- REPO="${2}"
pkgctl_build_check_option_group_repo '--repo' "${REPO}" "${TESTING}" "${STAGING}"
+ REPO="${2}"
+ RELEASE_OPTIONS+=("--repo" "${REPO}")
shift 2
;;
--arch)
(( $# <= 1 )) && die "missing argument for %s" "$1"
if [[ ${2} == all ]]; then
- BUILD_ARCH=("${_arch[@]::${#_arch[@]}-1}")
+ BUILD_ARCH=("${DEVTOOLS_VALID_ARCHES[@]::${#DEVTOOLS_VALID_ARCHES[@]}-1}")
elif [[ ${2} == any ]]; then
- BUILD_ARCH=("${_arch[0]}")
+ BUILD_ARCH=("${DEVTOOLS_VALID_ARCHES[0]}")
elif ! in_array "${2}" "${BUILD_ARCH[@]}"; then
- if ! in_array "${2}" "${_arch[@]}"; then
+ if ! in_array "${2}" "${DEVTOOLS_VALID_ARCHES[@]}"; then
die 'invalid architecture: %s' "${2}"
fi
BUILD_ARCH+=("${2}")
@@ -161,7 +175,7 @@ pkgctl_build() {
pkgctl_build_check_option_group_ver '--pkgver' "${PKGVER}" "${PKGREL}" "${REBUILD}"
PKGVER="${1#*=}"
PKGREL=1
- UPDPKGSUMS=1
+ UPDATE_CHECKSUMS=1
shift
;;
--pkgrel=*)
@@ -169,6 +183,10 @@ pkgctl_build() {
PKGREL="${1#*=}"
shift
;;
+ --update-checksums)
+ UPDATE_CHECKSUMS=1
+ shift
+ ;;
--rebuild)
# shellcheck source=src/lib/util/git.sh
source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/git.sh
@@ -185,23 +203,37 @@ pkgctl_build() {
shift
;;
-s|--staging)
- STAGING=1
pkgctl_build_check_option_group_repo '--staging' "${REPO}" "${TESTING}" "${STAGING}"
+ STAGING=1
+ RELEASE_OPTIONS+=("--staging")
shift
;;
-t|--testing)
- TESTING=1
pkgctl_build_check_option_group_repo '--testing' "${REPO}" "${TESTING}" "${STAGING}"
+ TESTING=1
+ RELEASE_OPTIONS+=("--testing")
shift
;;
-c|--clean)
BUILD_OPTIONS+=("-c")
shift
;;
- -I|--install)
+ -I|--install-to-chroot)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ if (( OFFLOAD )); then
+ MAKECHROOT_OPTIONS+=("-I" "$2")
+ else
+ MAKECHROOT_OPTIONS+=("-I" "$(realpath "$2")")
+ fi
+ warning 'installing packages to the chroot may break reproducible builds, use with caution!'
+ shift 2
+ ;;
+ -i|--install-to-host)
(( $# <= 1 )) && die "missing argument for %s" "$1"
- MAKECHROOT_OPTIONS+=("-I" "$2")
- warning 'installing packages into the chroot may break reproducible builds, use with caution!'
+ if ! in_array "$2" "${DEVTOOLS_VALID_BUILD_INSTALL[@]}"; then
+ die 'invalid install mode: %s' "${2}"
+ fi
+ INSTALL_TO_HOST=$2
shift 2
;;
--nocheck)
@@ -209,6 +241,14 @@ pkgctl_build() {
warning 'not running checks is disallowed for official packages, except for bootstrapping. Please rebuild after bootstrapping is completed!'
shift
;;
+ --inspect)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ if ! in_array "${2}" "${DEVTOOLS_VALID_INSPECT_MODES[@]}"; then
+ die "Invalid inspect mode: %s" "${2}"
+ fi
+ MAKECHROOT_OPTIONS+=("-x" "${2}")
+ shift 2
+ ;;
-r|--release)
# shellcheck source=src/lib/release.sh
source "${_DEVTOOLS_LIBRARY_DIR}"/lib/release.sh
@@ -274,7 +314,7 @@ pkgctl_build() {
if [[ -z ${REPO} ]]; then
update_pacman_repo_cache
# Check valid repos if not resolved dynamically
- elif ! in_array "${REPO}" "${_repos[@]}"; then
+ elif ! in_array "${REPO}" "${DEVTOOLS_VALID_REPOS[@]}"; then
die "Invalid repository target: %s" "${REPO}"
fi
@@ -290,18 +330,32 @@ pkgctl_build() {
. ./PKGBUILD
pkgbase=${pkgbase:-$pkgname}
pkgrepo=${REPO}
+ pkgbuild_checksum=$(b2sum PKGBUILD | awk '{print $1}')
msg "Building ${pkgbase}"
- # auto-detection of build target
- if [[ -z ${pkgrepo} ]]; then
- if ! pkgrepo=$(get_pacman_repo_from_pkgbuild PKGBUILD); then
- die 'failed to get pacman repo'
- fi
- if [[ -z "${pkgrepo}" ]]; then
- die 'unknown repo, specify --repo for packages not currently in any official repo'
+ # auto-detect target repository
+ if ! repo=$(get_pacman_repo_from_pkgbuild PKGBUILD); then
+ die 'Failed to query pacman repo'
+ fi
+
+ # fail if an existing package specifies --repo
+ if [[ -n "${repo}" ]] && [[ -n ${pkgrepo} ]]; then
+ # allow unstable to use --repo
+ if [[ ${pkgrepo} == *unstable ]]; then
+ repo=${pkgrepo}
+ else
+ die 'Using --repo for packages that exist in official repositories is disallowed'
fi
fi
+ # assign auto-detected target repository
+ if [[ -n ${repo} ]]; then
+ pkgrepo=${repo}
+ # fallback to extra for unreleased packages
+ elif [[ -z ${pkgrepo} ]]; then
+ pkgrepo=extra
+ fi
+
# special cases to resolve final build target
if (( TESTING )); then
pkgrepo="${pkgrepo}-testing"
@@ -316,9 +370,15 @@ pkgctl_build() {
BUILD_ARCH=("")
elif (( ${#BUILD_ARCH[@]} == 0 )); then
if in_array any "${arch[@]}"; then
- BUILD_ARCH=("${_arch[0]}")
+ BUILD_ARCH=("${DEVTOOLS_VALID_ARCHES[0]}")
else
- BUILD_ARCH+=("${arch[@]}")
+ for _arch in "${arch[@]}"; do
+ if in_array "${_arch}" "${DEVTOOLS_VALID_ARCHES[@]}"; then
+ BUILD_ARCH+=("$_arch")
+ else
+ warning 'invalid architecture, not building for: %s' "${_arch}"
+ fi
+ done
fi
fi
@@ -329,7 +389,7 @@ pkgctl_build() {
# increment pkgrel on rebuild
if (( REBUILD )); then
- # try to figure out of pkgrel has been changed
+ # try to figure out if pkgrel has been changed
if ! old_pkgrel=$(git_diff_tree HEAD PKGBUILD | grep --perl-regexp --only-matching --max-count=1 '^-pkgrel=\K\w+'); then
old_pkgrel=${pkgrel}
fi
@@ -346,20 +406,14 @@ pkgctl_build() {
# update pkgver
if [[ -n ${PKGVER} ]]; then
- if [[ $(type -t pkgver) == function ]]; then
- # TODO: check if die or warn, if we provide _commit _gitcommit setter maybe?
- warning 'setting pkgver variable has no effect if the PKGBUILD has a pkgver() function'
- fi
msg "Bumping pkgver to ${PKGVER}"
- grep --extended-regexp --quiet --max-count=1 "^pkgver=${pkgver}$" PKGBUILD || die "Non-standard pkgver declaration"
- sed --regexp-extended "s|^(pkgver=)${pkgver}$|\1${PKGVER}|g" -i PKGBUILD
+ pkgbuild_set_pkgver "${PKGVER}"
fi
# update pkgrel
if [[ -n ${PKGREL} ]]; then
msg "Bumping pkgrel to ${PKGREL}"
- grep --extended-regexp --quiet --max-count=1 "^pkgrel=${pkgrel}$" PKGBUILD || die "Non-standard pkgrel declaration"
- sed --regexp-extended "s|^(pkgrel=)${pkgrel}$|\1${PKGREL}|g" -i PKGBUILD
+ pkgbuild_set_pkgrel "${PKGREL}"
fi
# edit PKGBUILD
@@ -381,10 +435,18 @@ pkgctl_build() {
# update checksums if any sources are declared
- if (( UPDPKGSUMS )) && (( ${#source[@]} >= 1 )); then
+ if (( UPDATE_CHECKSUMS )) && (( ${#source[@]} >= 1 )); then
updpkgsums
fi
+ # re-source the PKGBUILD if it changed
+ current_checksum="$(b2sum PKGBUILD | awk '{print $1}')"
+ if [[ ${pkgbuild_checksum} != "${current_checksum}" ]]; then
+ pkgbuild_checksum=${current_checksum}
+ # shellcheck source=contrib/makepkg/PKGBUILD.proto
+ . ./PKGBUILD
+ fi
+
# execute build
for arch in "${BUILD_ARCH[@]}"; do
if [[ -n $arch ]]; then
@@ -402,9 +464,41 @@ pkgctl_build() {
fi
done
+ # re-source the PKGBUILD if it changed
+ current_checksum="$(b2sum PKGBUILD | awk '{print $1}')"
+ if [[ ${pkgbuild_checksum} != "${current_checksum}" ]]; then
+ pkgbuild_checksum=${current_checksum}
+ # shellcheck source=contrib/makepkg/PKGBUILD.proto
+ . ./PKGBUILD
+ fi
+
+ # auto generate .SRCINFO
+ # shellcheck disable=SC2119
+ write_srcinfo_file
+
+ # test-install (some of) the produced packages
+ if [[ ${INSTALL_TO_HOST} == auto ]] || [[ ${INSTALL_TO_HOST} == all ]]; then
+ # shellcheck disable=2119
+ load_makepkg_config
+
+ # this is inspired by print_all_package_names from libmakepkg
+ local version pkg_architecture pkg pkgfile
+ version=$(get_full_version)
+
+ for pkg in "${pkgname[@]}"; do
+ pkg_architecture=$(get_pkg_arch "$pkg")
+ pkgfile=$(realpath "$(printf "%s/%s-%s-%s%s\n" "${PKGDEST:-.}" "$pkg" "$version" "$pkg_architecture" "$PKGEXT")")
+
+ # check if we install all packages or if the (split-)package is already installed
+ if [[ ${INSTALL_TO_HOST} == all ]] || ( [[ ${INSTALL_TO_HOST} == auto ]] && pacman -Qq -- "$pkg" &>/dev/null ); then
+ INSTALL_HOST_PACKAGES+=("$pkgfile")
+ fi
+ done
+ fi
+
# release the build
if (( RELEASE )); then
- pkgctl_release --repo "${pkgrepo}" "${RELEASE_OPTIONS[@]}"
+ pkgctl_release "${RELEASE_OPTIONS[@]}"
fi
# reset common PKGBUILD variables
@@ -412,6 +506,12 @@ pkgctl_build() {
popd >/dev/null
done
+ # install all collected packages to the host system
+ if (( ${#INSTALL_HOST_PACKAGES[@]} )); then
+ msg "Installing built packages to the host system"
+ sudo pacman -U -- "${INSTALL_HOST_PACKAGES[@]}"
+ fi
+
# update the binary package repo db as last action
if (( RELEASE )) && (( DB_UPDATE )); then
# shellcheck disable=2119
diff --git a/src/lib/cache.sh b/src/lib/cache.sh
new file mode 100644
index 0000000..24056fa
--- /dev/null
+++ b/src/lib/cache.sh
@@ -0,0 +1,22 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_CACHE_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_CACHE_SH=1
+
+set -e
+
+readonly XDG_DEVTOOLS_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/devtools"
+
+get_cache_file() {
+ local filename=$1
+ local path="${XDG_DEVTOOLS_CACHE_DIR}/${filename}"
+
+ mkdir --parents -- "$(dirname -- "$path")"
+ if [[ ! -f ${path} ]]; then
+ touch -- "${path}"
+ fi
+
+ printf '%s' "${path}"
+}
diff --git a/src/lib/common.sh b/src/lib/common.sh
index 3d1ee56..ff767c6 100644
--- a/src/lib/common.sh
+++ b/src/lib/common.sh
@@ -13,7 +13,7 @@ set +u +o posix
$DEVTOOLS_INCLUDE_COMMON_SH
# Avoid any encoding problems
-export LANG=C
+export LANG=C.UTF-8
# Set buildtool properties
export BUILDTOOL=devtools
@@ -22,19 +22,33 @@ export BUILDTOOLVER=@buildtoolver@
# Set common properties
export PACMAN_KEYRING_DIR=/etc/pacman.d/gnupg
export GITLAB_HOST=gitlab.archlinux.org
-export GIT_REPO_SPEC_VERSION=1
+export GIT_REPO_SPEC_VERSION=2
export GIT_PACKAGING_NAMESPACE=archlinux/packaging/packages
export GIT_PACKAGING_NAMESPACE_ID=11323
export GIT_PACKAGING_URL_SSH="git@${GITLAB_HOST}:${GIT_PACKAGING_NAMESPACE}"
export GIT_PACKAGING_URL_HTTPS="https://${GITLAB_HOST}/${GIT_PACKAGING_NAMESPACE}"
export PACKAGING_REPO_RELEASE_HOST=repos.archlinux.org
+export PKGBASE_MAINTAINER_URL=https://archlinux.org/packages/pkgbase-maintainer
+export AUR_URL_SSH=aur@aur.archlinux.org
+
+# ensure TERM is set with a fallback to dumb
+export TERM=${TERM:-dumb}
# check if messages are to be printed using color
if [[ -t 2 && "$TERM" != dumb ]] || [[ ${DEVTOOLS_COLOR} == always ]]; then
colorize
+ if tput setaf 0 &>/dev/null; then
+ PURPLE="$(tput setaf 5)"
+ DARK_GREEN="$(tput setaf 2)"
+ UNDERLINE="$(tput smul)"
+ else
+ PURPLE="\e[35m"
+ DARK_GREEN="\e[32m"
+ UNDERLINE="\e[4m"
+ fi
else
# shellcheck disable=2034
- declare -gr ALL_OFF='' BOLD='' BLUE='' GREEN='' RED='' YELLOW=''
+ declare -gr ALL_OFF='' BOLD='' BLUE='' GREEN='' RED='' YELLOW='' PURPLE='' DARK_GREEN='' UNDERLINE=''
fi
stat_busy() {
@@ -53,6 +67,11 @@ stat_done() {
printf "${BOLD}done${ALL_OFF}\n" >&2
}
+stat_failed() {
+ # shellcheck disable=2059
+ printf "${BOLD}${RED}failed${ALL_OFF}\n" >&2
+}
+
msg_success() {
local msg=$1
local padding
@@ -77,6 +96,15 @@ msg_warn() {
printf "%s %s\n" "${padding}${YELLOW}!${ALL_OFF}" "${msg}" >&2
}
+print_workdir_error() {
+ if [[ ! -f "${WORKDIR}"/error ]]; then
+ return
+ fi
+ while read -r LINE; do
+ error '%s' "${LINE}"
+ done < "${WORKDIR}/error"
+}
+
_setup_workdir=false
setup_workdir() {
[[ -z ${WORKDIR:-} ]] && WORKDIR=$(mktemp -d --tmpdir "${0##*/}.XXXXXXXXXX")
@@ -89,6 +117,9 @@ cleanup() {
if [[ -n ${WORKDIR:-} ]] && $_setup_workdir; then
rm -rf "$WORKDIR"
fi
+ if tput setaf 0 &>/dev/null; then
+ tput cnorm >&2
+ fi
exit "${1:-0}"
}
@@ -120,7 +151,7 @@ lock() {
# Only reopen the FD if it wasn't handed to us
if ! [[ "/dev/fd/$1" -ef "$2" ]]; then
mkdir -p -- "$(dirname -- "$2")"
- eval "exec $1>"'"$2"'
+ eval "exec $1>>"'"$2"'
fi
if ! flock -n "$1"; then
diff --git a/src/lib/db.sh b/src/lib/db.sh
index 397ff0d..91e4da5 100644
--- a/src/lib/db.sh
+++ b/src/lib/db.sh
@@ -15,7 +15,7 @@ pkgctl_db_usage() {
cat <<- _EOF_
Usage: ${COMMAND} [COMMAND] [OPTIONS]
- Pacman database modification for packge update, move etc
+ Pacman database modification for package update, move etc
COMMANDS
move Move packages between pacman repositories
diff --git a/src/lib/release.sh b/src/lib/release.sh
index aabbd35..acb3b54 100644
--- a/src/lib/release.sh
+++ b/src/lib/release.sh
@@ -35,7 +35,7 @@ pkgctl_release_usage() {
OPTIONS
-m, --message MSG Use the given <msg> as the commit message
- -r, --repo REPO Specify a target repository (disables auto-detection)
+ -r, --repo REPO Specify target repository for new packages not in any official repo
-s, --staging Release to the staging counterpart of the auto-detected repo
-t, --testing Release to the testing counterpart of the auto-detected repo
-u, --db-update Automatically update the pacman database after uploading
@@ -43,8 +43,8 @@ pkgctl_release_usage() {
EXAMPLES
$ ${COMMAND}
- $ ${COMMAND} --repo core-testing --message 'libyay 0.42 rebuild' libfoo libbar
- $ ${COMMAND} --staging --db-update libfoo
+ $ ${COMMAND} --staging --message 'libyay 0.42 rebuild' libfoo libbar
+ $ ${COMMAND} --repo extra --db-update new-package
_EOF_
}
@@ -126,7 +126,7 @@ pkgctl_release() {
if [[ -z ${REPO} ]]; then
update_pacman_repo_cache
# Check valid repos if not resolved dynamically
- elif ! in_array "${REPO}" "${_repos[@]}"; then
+ elif ! in_array "${REPO}" "${DEVTOOLS_VALID_REPOS[@]}"; then
die "Invalid repository target: %s" "${REPO}"
fi
@@ -134,15 +134,27 @@ pkgctl_release() {
pushd "${path}" >/dev/null
pkgbase=$(basename "${path}")
- if [[ -n ${REPO} ]]; then
- repo=${REPO}
- else
- if ! repo=$(get_pacman_repo_from_pkgbuild PKGBUILD); then
- die 'Failed to get pacman repo'
+ # auto-detect target repository
+ if ! repo=$(get_pacman_repo_from_pkgbuild PKGBUILD); then
+ die 'Failed to query pacman repo'
+ fi
+
+ # fail if an existing package specifies --repo
+ if [[ -n "${repo}" ]] && [[ -n ${REPO} ]]; then
+ # allow unstable to use --repo
+ if [[ ${REPO} == *unstable ]]; then
+ repo=${REPO}
+ else
+ die 'Using --repo for packages that exist in official repositories is disallowed'
fi
- if [[ -z "${repo}" ]]; then
- die 'Unknown repo, please specify --repo for new packages'
+ fi
+
+ # fail if a new package does not specify --repo
+ if [[ -z "${repo}" ]]; then
+ if [[ -z ${REPO} ]]; then
+ die 'Specify --repo for packages that do not yet exist in official repositories'
fi
+ repo=${REPO}
fi
if (( TESTING )); then
diff --git a/src/lib/repo/clone.sh b/src/lib/repo/clone.sh
index a8cf6f5..33a333f 100644
--- a/src/lib/repo/clone.sh
+++ b/src/lib/repo/clone.sh
@@ -8,16 +8,21 @@ DEVTOOLS_INCLUDE_REPO_CLONE_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/api/archweb.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/archweb.sh
# shellcheck source=src/lib/api/gitlab.sh
source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/gitlab.sh
# shellcheck source=src/lib/repo/configure.sh
source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/configure.sh
+# shellcheck source=src/lib/util/git.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/git.sh
# shellcheck source=src/lib/repo/arch32.sh
source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/arch32.sh
source /usr/share/makepkg/util/message.sh
set -e
+set -o pipefail
pkgctl_repo_clone_usage() {
@@ -55,6 +60,7 @@ pkgctl_repo_clone() {
fi
# options
+ local protocol=ssh
local GIT_REPO_BASE_URL=${GIT_PACKAGING_URL_SSH}
local CLONE_ALL=0
local MAINTAINER=
@@ -76,6 +82,7 @@ pkgctl_repo_clone() {
;;
--protocol=https)
GIT_REPO_BASE_URL=${GIT_PACKAGING_URL_HTTPS}
+ protocol=https
CONFIGURE_OPTIONS+=("$1")
shift
;;
@@ -86,6 +93,7 @@ pkgctl_repo_clone() {
else
die "unsupported protocol: %s" "$2"
fi
+ protocol="$2"
CONFIGURE_OPTIONS+=("$1" "$2")
shift 2
;;
@@ -140,33 +148,18 @@ pkgctl_repo_clone() {
# Query packages of a maintainer
if [[ -n ${MAINTAINER} ]]; then
- stat_busy "Query packages"
- max_pages=$(curl --silent --location --fail --retry 3 --retry-delay 3 "https://archlinux.org/packages/search/json/?sort=name&maintainer=${MAINTAINER}" | jq -r '.num_pages')
- if [[ ! ${max_pages} =~ ([[:digit:]]) ]]; then
- stat_done
- warning "found no packages for maintainer ${MAINTAINER}"
- exit 0
+ mapfile -t pkgbases < <(archweb_query_maintainer_packages "${MAINTAINER}")
+ if ! wait $!; then
+ die "Failed to query maintainer packages"
fi
- mapfile -t pkgbases < <(for page in $(seq "${max_pages}"); do
- curl --silent --location --fail --retry 3 --retry-delay 3 "https://archlinux.org/packages/search/json/?sort=name&maintainer=${MAINTAINER}&page=${page}" | jq -r '.results[].pkgbase'
- stat_progress
- done | sort --unique)
- stat_done
fi
# Query all released packages
if (( CLONE_ALL )); then
- stat_busy "Query all released packages"
- max_pages=$(curl --silent --location --fail --retry 3 --retry-delay 3 "https://archlinux.org/packages/search/json/?sort=name" | jq -r '.num_pages')
- if [[ ! ${max_pages} =~ ([[:digit:]]) ]]; then
- stat_done
- die "failed to query packages"
+ mapfile -t pkgbases < <(archweb_query_all_packages)
+ if ! wait $!; then
+ die "Failed to query all packages"
fi
- mapfile -t pkgbases < <(for page in $(seq "${max_pages}"); do
- curl --silent --location --fail --retry 3 --retry-delay 3 "https://archlinux.org/packages/search/json/?sort=name&page=${page}" | jq -r '.results[].pkgbase'
- stat_progress
- done | sort --unique)
- stat_done
fi
# parallelization
@@ -179,6 +172,12 @@ pkgctl_repo_clone() {
if [[ -n "${VERSION}" ]]; then
command+=" --switch '${VERSION}'"
fi
+
+ # warm up ssh connection as it may require user input (key unlock, hostkey verification etc)
+ if [[ ${protocol} == ssh ]]; then
+ git_warmup_ssh_connection
+ fi
+
if ! parallel --bar --jobs "${jobs}" "${command}" ::: "${pkgbases[@]}"; then
die 'Failed to clone some packages, please check the output'
exit 1
diff --git a/src/lib/repo/configure.sh b/src/lib/repo/configure.sh
index 73300ae..b3c188c 100644
--- a/src/lib/repo/configure.sh
+++ b/src/lib/repo/configure.sh
@@ -10,11 +10,14 @@ _DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
source "${_DEVTOOLS_LIBRARY_DIR}"/lib/common.sh
# shellcheck source=src/lib/api/gitlab.sh
source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/gitlab.sh
+# shellcheck source=src/lib/util/git.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/git.sh
source /usr/share/makepkg/util/config.sh
source /usr/share/makepkg/util/message.sh
set -e
+shopt -s nullglob
pkgctl_repo_configure_usage() {
@@ -32,6 +35,8 @@ pkgctl_repo_configure_usage() {
address by choosing SSH for all official packager identities and
read-only HTTPS otherwise.
+ Git default excludes and hooks are applied to the configured repo.
+
OPTIONS
--protocol https Configure remote url to use https
-j, --jobs N Run up to N jobs in parallel (default: $(nproc))
@@ -102,7 +107,7 @@ pkgctl_repo_configure() {
# variables
local -r command=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
- local path realpath pkgbase remote_url project_path
+ local path realpath pkgbase remote_url project_path hook
local PACKAGER GPGKEY packager_name packager_email
while (( $# )); do
@@ -188,6 +193,12 @@ pkgctl_repo_configure() {
if [[ -n ${BOLD} ]]; then
export DEVTOOLS_COLOR=always
fi
+
+ # warm up ssh connection as it may require user input (key unlock, hostkey verification etc)
+ if [[ ${proto} == ssh ]]; then
+ git_warmup_ssh_connection
+ fi
+
if ! parallel --bar --jobs "${jobs}" "${command}" ::: "${paths[@]}"; then
die 'Failed to configure some packages, please check the output'
exit 1
@@ -222,7 +233,15 @@ pkgctl_repo_configure() {
git config branch.main.merge refs/heads/main
fi
+ # configure spec version and variant to avoid using development hooks in production
git config devtools.version "${GIT_REPO_SPEC_VERSION}"
+ if [[ ${_DEVTOOLS_LIBRARY_DIR} == /usr/share/devtools ]]; then
+ git config devtools.variant canonical
+ else
+ warning "Configuring with development version of pkgctl, do not use this repo in production"
+ git config devtools.variant development
+ fi
+
git config pull.rebase true
git config branch.autoSetupRebase always
git config branch.main.remote origin
@@ -249,6 +268,18 @@ pkgctl_repo_configure() {
git config user.signingKey "${GPGKEY}"
fi
+ # set default git exclude
+ mkdir -p .git/info
+ ln -sf "${_DEVTOOLS_LIBRARY_DIR}/git.conf.d/template/info/exclude" \
+ .git/info/exclude
+
+ # set default git hooks
+ mkdir -p .git/hooks
+ rm -f .git/hooks/*.sample
+ for hook in "${_DEVTOOLS_LIBRARY_DIR}"/git.conf.d/template/hooks/*; do
+ ln -sf "${hook}" ".git/hooks/$(basename "${hook}")"
+ done
+
if ! git ls-remote origin &>/dev/null; then
warning "configured remote origin may not exist, run:"
msg2 "pkgctl repo create ${pkgbase}"
diff --git a/src/lib/repo/web.sh b/src/lib/repo/web.sh
index 45ea53b..ab3d8c7 100644
--- a/src/lib/repo/web.sh
+++ b/src/lib/repo/web.sh
@@ -23,6 +23,7 @@ pkgctl_repo_web_usage() {
no arguments, open the package cloned in the current working directory.
OPTIONS
+ --print Print the url instead of opening it with xdg-open
-h, --help Show this help text
EXAMPLES
@@ -32,7 +33,8 @@ _EOF_
pkgctl_repo_web() {
local pkgbases=()
- local path giturl pkgbase
+ local path giturl pkgbase url
+ local mode=open
# option checking
while (( $# )); do
@@ -41,6 +43,10 @@ pkgctl_repo_web() {
pkgctl_repo_web_usage
exit 0
;;
+ --print)
+ mode=print
+ shift
+ ;;
--)
shift
break
@@ -56,7 +62,7 @@ pkgctl_repo_web() {
done
# Check if web mode has xdg-open
- if ! command -v xdg-open &>/dev/null; then
+ if [[ ${mode} == open ]] && ! command -v xdg-open &>/dev/null; then
die "The web command requires 'xdg-open'"
fi
@@ -78,7 +84,18 @@ pkgctl_repo_web() {
fi
for pkgbase in "${pkgbases[@]}"; do
+ pkgbase=$(basename "${pkgbase}")
path=$(gitlab_project_name_to_path "${pkgbase}")
- xdg-open "${GIT_PACKAGING_URL_HTTPS}/${path}"
+ url="${GIT_PACKAGING_URL_HTTPS}/${path}"
+ case ${mode} in
+ open)
+ xdg-open "${url}"
+ ;;
+ print)
+ printf "%s\n" "${url}"
+ ;;
+ *)
+ die "Unknown mode: ${mode}"
+ esac
done
}
diff --git a/src/lib/search.sh b/src/lib/search.sh
new file mode 100644
index 0000000..d3bad68
--- /dev/null
+++ b/src/lib/search.sh
@@ -0,0 +1,308 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_SEARCH_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_SEARCH_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/cache.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/cache.sh
+# shellcheck source=src/lib/api/gitlab.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/gitlab.sh
+# shellcheck source=src/lib/valid-search.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/valid-search.sh
+# shellcheck source=src/lib/util/term.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/term.sh
+
+source /usr/share/makepkg/util/util.sh
+source /usr/share/makepkg/util/message.sh
+
+set -eo pipefail
+
+
+pkgctl_search_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] QUERY
+
+ Search for an expression across the GitLab packaging group.
+
+ To use a filter, include it in your query. You may use wildcards (*) to
+ use glob matching.
+
+ Available filters for the blobs scope: path, extension
+
+ Every usage of the search command must be authenticated. Consult the
+ 'pkgctl auth' command to authenticate with GitLab or view the
+ authentication status.
+
+ SEARCH TIPS
+ Syntax Description Example
+ ───────────────────────────────────────
+ " Exact search "gem sidekiq"
+ ~ Fuzzy search J~ Doe
+ | Or display | banner
+ + And display +banner
+ - Exclude display -banner
+ * Partial bug error 50*
+ \\ Escape \\*md
+ # Issue ID #23456
+ ! Merge request !23456
+
+ OPTIONS
+ -h, --help Show this help text
+
+ FILTER OPTIONS
+ --no-default-filter Do not apply default filter (like -path:keys/pgp/*.asc)
+
+ OUTPUT OPTIONS
+ --json Enable printing in JSON; Shorthand for '--format json'
+ -F, --format FORMAT Controls the formatting of the results; FORMAT is 'pretty',
+ 'plain', or 'json' (default: pretty)
+ -N, --no-line-number Don't show line numbers when formatting results
+
+ EXAMPLES
+ $ ${COMMAND} linux
+ $ ${COMMAND} --json '"pytest -v" +PYTHONPATH'
+_EOF_
+}
+
+pkgctl_search_check_option_group_format() {
+ local option=$1
+ local output_format=$2
+ if [[ -n ${output_format} ]]; then
+ die "The argument '%s' cannot be used with one or more of the other specified arguments" "${option}"
+ exit 1
+ fi
+ return 0
+}
+
+pkgctl_search() {
+ if (( $# < 1 )); then
+ pkgctl_search_usage
+ exit 0
+ fi
+
+ # options
+ local search
+ local output_format=
+ local use_default_filter=1
+ local line_numbers=1
+
+ # variables
+ local bat_style="header,grid"
+ local default_filter="-path:keys/pgp/*.asc"
+ local graphql_lookup_batch=200
+ local output result query entries from until length
+ local project_name_cache_file project_name_lookup project_ids project_id project_name project_slice
+ local mapping_output path startline currentline data line
+
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_search_usage
+ exit 0
+ ;;
+ --no-default-filter)
+ use_default_filter=0
+ shift
+ ;;
+ --json)
+ pkgctl_search_check_option_group_format "$1" "${output_format}"
+ output_format=json
+ shift
+ ;;
+ -F|--format)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ pkgctl_search_check_option_group_format "$1" "${output_format}"
+ output_format="${2}"
+ if ! in_array "${output_format}" "${valid_search_output_format[@]}"; then
+ die "Unknown output format: %s" "${output_format}"
+ fi
+ shift 2
+ ;;
+ -N|--no-line-number)
+ line_numbers=0
+ shift
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ break
+ ;;
+ esac
+ done
+
+ if (( $# == 0 )); then
+ pkgctl_search_usage
+ exit 1
+ fi
+
+ # assign search parameter
+ search="${*}"
+ if (( use_default_filter )); then
+ search+=" ${default_filter}"
+ fi
+
+ # assign default output format
+ if [[ -z ${output_format} ]]; then
+ output_format=pretty
+ fi
+
+ # check for optional dependencies
+ if [[ ${output_format} == pretty ]] && ! command -v bat &>/dev/null; then
+ warning "Failed to find optional dependency 'bat': falling back to plain output"
+ output_format=plain
+ fi
+
+ # populate line numbers option
+ if (( line_numbers )); then
+ bat_style="numbers,${bat_style}"
+ fi
+
+ # call the gitlab search API
+ status_dir=$(mktemp --tmpdir="${WORKDIR}" --directory pkgctl-gitlab-api.XXXXXXXXXX)
+ printf "📡 Querying GitLab search API..." > "${status_dir}/status"
+ term_spinner_start "${status_dir}"
+ output=$(gitlab_api_search "${search}" "${status_dir}/status")
+ term_spinner_stop "${status_dir}"
+ msg_success "Querying GitLab search API"
+
+ # collect project ids whose name needs to be looked up
+ project_name_cache_file=$(get_cache_file gitlab/project_id_to_name)
+ lock 11 "${project_name_cache_file}" "Locking project name cache"
+ mapfile -t project_ids < <(
+ jq --raw-output '[.[].project_id] | unique[]' <<< "${output}" | \
+ grep --invert-match --file <(awk '{ print $1 }' < "${project_name_cache_file}" ))
+
+ # look up project names
+ tmp_file=$(mktemp --tmpdir="${WORKDIR}" pkgctl-gitlab-api-spinner.tmp.XXXXXXXXXX)
+ printf "📡 Querying GitLab project names..." > "${status_dir}/status"
+ term_spinner_start "${status_dir}"
+ local entries="${#project_ids[@]}"
+ local until=0
+ while (( until < entries )); do
+ from=${until}
+ until=$(( until + graphql_lookup_batch ))
+ if (( until > entries )); then
+ until=${entries}
+ fi
+ length=$(( until - from ))
+
+ percentage=$(( 100 * until / entries ))
+ printf "📡 Querying GitLab project names: %s/%s [%s] %%spinner%%" \
+ "${BOLD}${until}" "${entries}" "${percentage}%${ALL_OFF}" \
+ > "${tmp_file}"
+ mv "${tmp_file}" "${status_dir}/status"
+
+ project_slice=("${project_ids[@]:${from}:${length}}")
+ printf -v projects '"gid://gitlab/Project/%s",' "${project_slice[@]}"
+ query='{
+ projects(after: "" ids: ['"${projects}"']) {
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ }
+ nodes {
+ id
+ name
+ }
+ }
+ }'
+ mapping_output=$(gitlab_api_get_project_name_mapping "${query}")
+
+ # update cache
+ while read -r project_id project_name; do
+ printf "%s %s\n" "${project_id}" "${project_name}" >> "${project_name_cache_file}"
+ done < <(jq --raw-output \
+ '.[] | "\(.id | rindex("/") as $lastSlash | .[$lastSlash+1:]) \(.name)"' \
+ <<< "${mapping_output}")
+ done
+ term_spinner_stop "${status_dir}"
+ msg_success "Querying GitLab project names"
+
+ # read project_id to name mapping from cache
+ declare -A project_name_lookup=()
+ while read -r project_id project_name; do
+ project_name_lookup[${project_id}]=${project_name}
+ done < "${project_name_cache_file}"
+
+ # close project name cache lock
+ lock_close 11
+
+ # output mode JSON
+ if [[ ${output_format} == json ]]; then
+ jq --from-file <(
+ for project_id in $(jq '.[].project_id' <<< "${output}"); do
+ project_name=${project_name_lookup[${project_id}]}
+ printf 'map(if .project_id == %s then . + {"project_name": "%s"} else . end) | ' \
+ "${project_id}" "${project_name}"
+ done
+ printf .
+ ) <<< "${output}"
+ exit 0
+ fi
+
+ # pretty print each result
+ while read -r result; do
+ # read properties from search result
+ mapfile -t data < <(jq --raw-output ".data" <<< "${result}")
+ { read -r project_id; read -r path; read -r startline; } < <(
+ jq --raw-output ".project_id, .path, .startline" <<< "${result}"
+ )
+ project_name=${project_name_lookup[${project_id}]}
+
+ # remove trailing newline for multiline results
+ if (( ${#data[@]} > 1 )) && [[ ${data[-1]} == "" ]]; then
+ unset "data[${#data[@]}-1]"
+ fi
+
+ # output mode plain
+ if [[ ${output_format} == plain ]]; then
+ printf "%s%s%s\n" "${PURPLE}" "${project_name}/${path}" "${ALL_OFF}"
+
+ currentline=${startline}
+ for line in "${data[@]}"; do
+ if (( line_numbers )); then
+ line="${DARK_GREEN}${currentline}${ALL_OFF}: ${line}"
+ currentline=$(( currentline + 1 ))
+ fi
+ printf "%s\n" "${line}"
+ done
+ printf "\n"
+
+ continue
+ fi
+
+ # prepend empty lines to match startline
+ if (( startline > 1 )); then
+ mapfile -t data < <(
+ printf '%.0s\n' $(seq 1 "$(( startline - 1 ))")
+ printf "%s\n" "${data[@]}"
+ )
+ fi
+
+ bat \
+ --file-name="${project_name}/${path}" \
+ --line-range "${startline}:" \
+ --paging=never \
+ --force-colorization \
+ --style "${bat_style}" \
+ --map-syntax "PKGBUILD:Bourne Again Shell (bash)" \
+ --map-syntax ".SRCINFO:INI" \
+ --map-syntax "*install:Bourne Again Shell (bash)" \
+ --map-syntax "*sysusers*:Bourne Again Shell (bash)" \
+ --map-syntax "*tmpfiles*:Bourne Again Shell (bash)" \
+ --map-syntax "*.hook:INI" \
+ <(printf "%s\n" "${data[@]}")
+ done < <(jq --compact-output '.[]' <<< "${output}")
+}
diff --git a/src/lib/util/git.sh b/src/lib/util/git.sh
index c4af662..82e4beb 100644
--- a/src/lib/util/git.sh
+++ b/src/lib/util/git.sh
@@ -7,6 +7,9 @@ DEVTOOLS_INCLUDE_UTIL_GIT_SH=1
_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+# shellcheck source=src/lib/common.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/common.sh
+
git_diff_tree() {
local commit=$1
@@ -22,3 +25,10 @@ git_diff_tree() {
"${commit}" \
-- "${path}"
}
+
+git_warmup_ssh_connection() {
+ msg 'Establishing ssh connection to git@%s' "${GITLAB_HOST}"
+ if ! ssh -T "git@${GITLAB_HOST}" >/dev/null; then
+ die 'Failed to establish ssh connection to git@%s' "${GITLAB_HOST}"
+ fi
+}
diff --git a/src/lib/util/makepkg.sh b/src/lib/util/makepkg.sh
new file mode 100644
index 0000000..22df247
--- /dev/null
+++ b/src/lib/util/makepkg.sh
@@ -0,0 +1,37 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_UTIL_MAKEPKG_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_UTIL_MAKEPKG_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/srcinfo.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/srcinfo.sh
+
+
+set -e
+
+makepkg_source_package() {
+ if (( EUID != 0 )); then
+ [[ -z ${WORKDIR:-} ]] && setup_workdir
+ export WORKDIR DEVTOOLS_INCLUDE_COMMON_SH
+ fakeroot -- bash -$- -c "source '${BASH_SOURCE[0]}' && ${FUNCNAME[0]}"
+ return
+ fi
+ (
+ export LIBMAKEPKG_LINT_PKGBUILD_SH=1
+ lint_pkgbuild() { :; }
+
+ export LIBMAKEPKG_SRCINFO_SH=1
+ write_srcinfo() { print_srcinfo; }
+
+ # explicitly instruct makepkg to not sign the source package, even when
+ # the BUILDENV array in makepkg.conf contains 'sign'
+ set +e -- -F --source --nosign
+ # shellcheck source=/usr/bin/makepkg
+ source "$(command -v makepkg)"
+ )
+}
diff --git a/src/lib/util/pacman.sh b/src/lib/util/pacman.sh
index f6c2d5f..620e1a8 100644
--- a/src/lib/util/pacman.sh
+++ b/src/lib/util/pacman.sh
@@ -38,13 +38,21 @@ get_pacman_repo_from_pkgbuild() {
return
fi
+ # update the pacman repo cache if it doesn't exist yet
+ if [[ ! -d "${_DEVTOOLS_PACMAN_CACHE_DIR}" ]]; then
+ update_pacman_repo_cache
+ fi
+
slock 10 "${_DEVTOOLS_PACMAN_CACHE_DIR}.lock" "Locking pacman database cache"
+ # query repo of passed pkgname, specify --nodeps twice to skip all dependency checks
mapfile -t repos < <(pacman --config "${_DEVTOOLS_PACMAN_CONF_DIR}/multilib.conf" \
--dbpath "${_DEVTOOLS_PACMAN_CACHE_DIR}" \
- -S \
+ --sync \
+ --nodeps \
+ --nodeps \
--print \
--print-format '%n %r' \
- "${pkgnames[0]}" | grep -E "^${pkgnames[0]} " | awk '{print $2}'
+ "${pkgnames[0]}" 2>/dev/null | awk '$1=="'"${pkgnames[0]}"'"{print $2}'
)
lock_close 10
diff --git a/src/lib/util/pkgbuild.sh b/src/lib/util/pkgbuild.sh
new file mode 100644
index 0000000..ebf8e5f
--- /dev/null
+++ b/src/lib/util/pkgbuild.sh
@@ -0,0 +1,43 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_UTIL_PKGBUILD_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_UTIL_PKGBUILD_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+
+source /usr/share/makepkg/util/message.sh
+
+set -e
+
+
+# set the pkgver variable in a PKGBUILD
+# assumes that the pkgbuild is sourced to detect the presence of a pkgver function
+pkgbuild_set_pkgver() {
+ local new_pkgver=$1
+ local pkgver=${pkgver}
+
+ if [[ $(type -t pkgver) == function ]]; then
+ # TODO: check if die or warn, if we provide _commit _gitcommit setter maybe?
+ warning 'setting pkgver variable has no effect if the PKGBUILD has a pkgver() function'
+ fi
+
+ if ! grep --extended-regexp --quiet --max-count=1 "^pkgver=${pkgver}$" PKGBUILD; then
+ die "Non-standard pkgver declaration"
+ fi
+ sed --regexp-extended "s|^(pkgver=)${pkgver}$|\1${new_pkgver}|g" --in-place PKGBUILD
+}
+
+# set the pkgrel variable in a PKGBUILD
+# assumes that the pkgbuild is sourced so pkgrel is present
+pkgbuild_set_pkgrel() {
+ local new_pkgrel=$1
+ local pkgrel=${pkgrel}
+
+ if ! grep --extended-regexp --quiet --max-count=1 "^pkgrel=${pkgrel}$" PKGBUILD; then
+ die "Non-standard pkgrel declaration"
+ fi
+ sed --regexp-extended "s|^(pkgrel=)${pkgrel}$|\1${new_pkgrel}|g" --in-place PKGBUILD
+}
+
diff --git a/src/lib/util/srcinfo.sh b/src/lib/util/srcinfo.sh
new file mode 100644
index 0000000..b646dc3
--- /dev/null
+++ b/src/lib/util/srcinfo.sh
@@ -0,0 +1,69 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_UTIL_SRCINFO_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_UTIL_SRCINFO_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+# shellcheck source=src/lib/common.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/common.sh
+
+source /usr/share/makepkg/util/util.sh
+source /usr/share/makepkg/srcinfo.sh
+
+set -eo pipefail
+
+
+print_srcinfo() {
+ local pkgpath=${1:-.}
+ local outdir pkg pid
+ local pids=()
+
+ # source the PKGBUILD
+ # shellcheck source=contrib/makepkg/PKGBUILD.proto
+ . "${pkgpath}"/PKGBUILD
+
+ # run without parallelization for single packages
+ if (( ${#pkgname[@]} == 1 )); then
+ write_srcinfo_content
+ return 0
+ fi
+
+ [[ -z ${WORKDIR:-} ]] && setup_workdir
+ outdir=$(mktemp --directory --tmpdir="${WORKDIR}" pkgctl-srcinfo.XXXXXXXXXX)
+
+ # fork workload for each split pkgname
+ for pkg in "${pkgname[@]}"; do
+ (
+ # deactivate errexit to avoid makepkg abort on grep_function
+ set +e
+ srcinfo_write_package "$pkg" > "${outdir}/${pkg}"
+ )&
+ pids+=($!)
+ done
+
+ # join workload
+ for pid in "${pids[@]}"; do
+ if ! wait "${pid}"; then
+ return 1
+ fi
+ done
+
+ # collect output
+ srcinfo_write_global
+ for pkg in "${pkgname[@]}"; do
+ srcinfo_separate_section
+ cat "${outdir}/${pkg}"
+ done
+}
+
+write_srcinfo_file() {
+ local pkgpath=${1:-.}
+ stat_busy 'Generating .SRCINFO'
+ if ! print_srcinfo "${pkgpath}" > "${pkgpath}"/.SRCINFO; then
+ error 'Failed to write .SRCINFO file'
+ return 1
+ fi
+ stat_done
+}
diff --git a/src/lib/util/term.sh b/src/lib/util/term.sh
new file mode 100644
index 0000000..853dccf
--- /dev/null
+++ b/src/lib/util/term.sh
@@ -0,0 +1,182 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_UTIL_TERM_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_UTIL_TERM_SH=1
+
+set -eo pipefail
+
+
+readonly PKGCTL_TERM_SPINNER_DOTS=Dots
+export PKGCTL_TERM_SPINNER_DOTS
+readonly PKGCTL_TERM_SPINNER_DOTS12=Dots12
+export PKGCTL_TERM_SPINNER_DOTS12
+readonly PKGCTL_TERM_SPINNER_LINE=Line
+export PKGCTL_TERM_SPINNER_LINE
+readonly PKGCTL_TERM_SPINNER_SIMPLE_DOTS_SCROLLING=SimpleDotsScrolling
+export PKGCTL_TERM_SPINNER_SIMPLE_DOTS_SCROLLING
+readonly PKGCTL_TERM_SPINNER_TRIANGLE=Triangle
+export PKGCTL_TERM_SPINNER_TRIANGLE
+readonly PKGCTL_TERM_SPINNER_RANDOM=Random
+export PKGCTL_TERM_SPINNER_RANDOM
+
+readonly PKGCTL_TERM_SPINNER_TYPES=(
+ "${PKGCTL_TERM_SPINNER_DOTS}"
+ "${PKGCTL_TERM_SPINNER_DOTS12}"
+ "${PKGCTL_TERM_SPINNER_LINE}"
+ "${PKGCTL_TERM_SPINNER_SIMPLE_DOTS_SCROLLING}"
+ "${PKGCTL_TERM_SPINNER_TRIANGLE}"
+)
+export PKGCTL_TERM_SPINNER_TYPES
+
+
+term_cursor_hide() {
+ tput civis >&2
+}
+
+term_cursor_show() {
+ tput cnorm >&2
+}
+
+term_cursor_up() {
+ tput cuu1
+}
+
+term_carriage_return() {
+ tput cr
+}
+
+term_erase_line() {
+ tput el
+}
+
+term_erase_lines() {
+ local lines=$1
+
+ local cursor_up erase_line
+ cursor_up=$(term_cursor_up)
+ erase_line="$(term_carriage_return)$(term_erase_line)"
+
+ local prefix=''
+ for _ in $(seq 1 "${lines}"); do
+ printf '%s' "${prefix}${erase_line}"
+ prefix="${cursor_up}"
+ done
+}
+
+_pkgctl_spinner_type=${PKGCTL_TERM_SPINNER_RANDOM}
+term_spinner_set_type() {
+ _pkgctl_spinner_type=$1
+}
+
+# takes a status directory that can be used to dynamically update the spinner
+# by writing to the `status` file inside that directory atomically.
+# replace the placeholder %spinner% with the currently configured spinner type
+term_spinner_start() {
+ local status_dir=$1
+ local parent_pid=$$
+ (
+ local spinner_type=${_pkgctl_spinner_type}
+ local spinner_offset=0
+ local frame_buffer=''
+ local spinner status_message line
+
+ local status_file="${status_dir}/status"
+ local next_file="${status_dir}/next"
+ local drawn_file="${status_dir}/drawn"
+
+ # assign random spinner type
+ if [[ ${spinner_type} == "${PKGCTL_TERM_SPINNER_RANDOM}" ]]; then
+ spinner_type=${PKGCTL_TERM_SPINNER_TYPES[$((RANDOM % ${#PKGCTL_TERM_SPINNER_TYPES[@]}))]}
+ fi
+
+ # select spinner based on the named type
+ case "${spinner_type}" in
+ "${PKGCTL_TERM_SPINNER_DOTS}")
+ spinner=("⠋" "⠙" "⠹" "⠸" "⠼" "⠴" "⠦" "⠧" "⠇" "⠏")
+ update_interval=0.08
+ ;;
+ "${PKGCTL_TERM_SPINNER_DOTS12}")
+ spinner=("⢀⠀" "⡀⠀" "⠄⠀" "⢂⠀" "⡂⠀" "⠅⠀" "⢃⠀" "⡃⠀" "⠍⠀" "⢋⠀" "⡋⠀" "⠍⠁" "⢋⠁" "⡋⠁" "⠍⠉" "⠋⠉" "⠋⠉" "⠉⠙" "⠉⠙" "⠉⠩" "⠈⢙" "⠈⡙" "⢈⠩" "⡀⢙" "⠄⡙" "⢂⠩" "⡂⢘" "⠅⡘" "⢃⠨" "⡃⢐" "⠍⡐" "⢋⠠" "⡋⢀" "⠍⡁" "⢋⠁" "⡋⠁" "⠍⠉" "⠋⠉" "⠋⠉" "⠉⠙" "⠉⠙" "⠉⠩" "⠈⢙" "⠈⡙" "⠈⠩" "⠀⢙" "⠀⡙" "⠀⠩" "⠀⢘" "⠀⡘" "⠀⠨" "⠀⢐" "⠀⡐" "⠀⠠" "⠀⢀" "⠀⡀")
+ update_interval=0.08
+ ;;
+ "${PKGCTL_TERM_SPINNER_LINE}")
+ spinner=("⎯" "\\" "|" "/")
+ update_interval=0.13
+ ;;
+ "${PKGCTL_TERM_SPINNER_SIMPLE_DOTS_SCROLLING}")
+ spinner=(". " ".. " "..." " .." " ." " ")
+ update_interval=0.2
+ ;;
+ "${PKGCTL_TERM_SPINNER_TRIANGLE}")
+ spinner=("◢" "◣" "◤" "◥")
+ update_interval=0.05
+ ;;
+ esac
+
+ # hide the cursor while spinning
+ term_cursor_hide
+
+ # run the spinner as long as the parent process didn't terminate
+ while ps -p "${parent_pid}" &>/dev/null; do
+ # cache the new status template if it exists
+ if mv "${status_file}" "${next_file}" &>/dev/null; then
+ status_message="$(cat "$next_file")"
+ elif [[ -z "${status_message}" ]]; then
+ # wait until we either have a new or cached status
+ sleep 0.05
+ fi
+
+ # fill the frame buffer with the current status
+ local prefix=''
+ while IFS= read -r line; do
+ # replace spinner placeholder
+ line=${line//%spinner%/${spinner[spinner_offset%${#spinner[@]}]}}
+
+ # append the current line to the frame buffer
+ frame_buffer+="${prefix}${line}"
+ prefix=$'\n'
+ done <<< "${status_message}"
+
+ # print current frame buffer
+ echo -n "${frame_buffer}" >&2
+ mv "${next_file}" "${drawn_file}" &>/dev/null ||:
+
+ # setup next frame buffer to clear current content
+ frame_buffer=$(term_erase_lines "$(awk 'END {print NR}' <<< "${status_message}")")
+
+ # advance the spinner animation offset
+ (( ++spinner_offset ))
+
+ # sleep for the spinner update interval
+ sleep "${update_interval}"
+ done
+ )&
+ _pkgctl_spinner_pid=$!
+ disown
+}
+
+term_spinner_stop() {
+ local status_dir=$1
+ local frame_buffer status_file
+
+ # kill the spinner process
+ if ! kill "${_pkgctl_spinner_pid}" > /dev/null 2>&1; then
+ return 1
+ fi
+ unset _pkgctl_spinner_pid
+
+ # acquire last drawn status
+ status_file="${status_dir}/drawn"
+ if [[ ! -f ${status_file} ]]; then
+ return 0
+ fi
+
+ # clear terminal based on last status line
+ frame_buffer=$(term_erase_lines "$(awk 'END {print NR}' < "${status_file}")")
+ echo -n "${frame_buffer}" >&2
+
+ # show the cursor after stopping the spinner
+ term_cursor_show
+}
diff --git a/src/lib/valid-build-install.sh b/src/lib/valid-build-install.sh
new file mode 100644
index 0000000..9e98be2
--- /dev/null
+++ b/src/lib/valid-build-install.sh
@@ -0,0 +1,11 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+:
+
+# shellcheck disable=2034
+DEVTOOLS_VALID_BUILD_INSTALL=(
+ none
+ auto
+ all
+)
diff --git a/src/lib/valid-inspect.sh b/src/lib/valid-inspect.sh
new file mode 100644
index 0000000..3b5dcad
--- /dev/null
+++ b/src/lib/valid-inspect.sh
@@ -0,0 +1,10 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# shellcheck disable=2034
+DEVTOOLS_VALID_INSPECT_MODES=(
+ never
+ always
+ failure
+)
diff --git a/src/lib/valid-repos.sh b/src/lib/valid-repos.sh
index 14f90ce..af8a552 100644
--- a/src/lib/valid-repos.sh
+++ b/src/lib/valid-repos.sh
@@ -4,7 +4,7 @@
:
# shellcheck disable=2034
-_repos=(
+DEVTOOLS_VALID_REPOS=(
core core-staging core-testing
extra extra-staging extra-testing
multilib multilib-staging multilib-testing
@@ -13,7 +13,7 @@ _repos=(
)
# shellcheck disable=2034
-_build_repos=(
+DEVTOOLS_VALID_BUILDREPOS=(
core-staging core-testing
extra extra-staging extra-testing
multilib multilib-staging multilib-testing
diff --git a/src/lib/valid-search.sh b/src/lib/valid-search.sh
new file mode 100644
index 0000000..43799a5
--- /dev/null
+++ b/src/lib/valid-search.sh
@@ -0,0 +1,11 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+:
+
+# shellcheck disable=2034
+valid_search_output_format=(
+ pretty
+ plain
+ json
+)
diff --git a/src/lib/valid-tags.sh b/src/lib/valid-tags.sh
index 5382c5c..abca7ef 100644
--- a/src/lib/valid-tags.sh
+++ b/src/lib/valid-tags.sh
@@ -5,7 +5,9 @@
# shellcheck disable=2034
_arch=(
+ pentium4
i686
+ i486
x86_64
any
)
diff --git a/src/lib/version.sh b/src/lib/version.sh
new file mode 100644
index 0000000..ac810ae
--- /dev/null
+++ b/src/lib/version.sh
@@ -0,0 +1,65 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_VERSION_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_VERSION_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+
+set -e
+
+
+pkgctl_version_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [COMMAND] [OPTIONS]
+
+ Check and manage package versions against upstream.
+
+ COMMANDS
+ check Compares local package versions against upstream
+ upgrade Adjust the PKGBUILD to match the latest upstream version
+
+ OPTIONS
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} check libfoo linux libbar
+_EOF_
+}
+
+pkgctl_version() {
+ if (( $# < 1 )); then
+ pkgctl_version_usage
+ exit 0
+ fi
+
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_version_usage
+ exit 0
+ ;;
+ check)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/version/check.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/version/check.sh
+ pkgctl_version_check "$@"
+ exit $?
+ ;;
+ upgrade)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/version/upgrade.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/version/upgrade.sh
+ pkgctl_version_upgrade "$@"
+ exit $?
+ ;;
+ *)
+ die "invalid argument: %s" "$1"
+ ;;
+ esac
+ done
+}
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}"
+}
diff --git a/src/lib/version/upgrade.sh b/src/lib/version/upgrade.sh
new file mode 100644
index 0000000..e217532
--- /dev/null
+++ b/src/lib/version/upgrade.sh
@@ -0,0 +1,248 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+[[ -z ${DEVTOOLS_INCLUDE_VERSION_UPGRADE_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_VERSION_UPGRADE_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/version/check.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/version/check.sh
+# shellcheck source=src/lib/util/pkgbuild.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/pkgbuild.sh
+# shellcheck source=src/lib/util/term.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/term.sh
+
+source /usr/share/makepkg/util/message.sh
+
+set -e
+
+pkgctl_version_upgrade_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [PKGBASE]...
+
+ Streamlines the process of keeping PKGBUILD files up-to-date with the latest
+ upstream versions.
+
+ Upon execution, it automatically adjusts the PKGBUILD file, ensuring that the
+ pkgver field is set to match the latest version available from the upstream
+ source. In addition to updating the pkgver, this command also resets the pkgrel
+ to 1.
+
+ Outputs a summary of upgraded packages, up-to-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_upgrade() {
+ local path upstream_version result
+ local pkgbases=()
+ local verbose=0
+ local exit_code=0
+ local current_item=0
+
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_version_upgrade_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_upgrade_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_upgrade_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}: upgraded from version ${PURPLE}${pkgver}${ALL_OFF} to ${DARK_GREEN}${upstream_version}${ALL_OFF}"
+ out_of_date+=("${result}")
+
+ # change the PKGBUILD
+ pkgbuild_set_pkgver "${upstream_version}"
+ pkgbuild_set_pkgrel 1
+ 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=1
+ 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
+ printf "%sUpgraded%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_upgrade_summary \
+ "${#up_to_date[@]}" \
+ "${#out_of_date[@]}" \
+ "${#failure[@]}"
+ fi
+
+ # return status based on results
+ return "${exit_code}"
+}
+
+pkgctl_version_upgrade_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 " Upgraded: ${BOLD}${out_of_date_count}${ALL_OFF}" 2>&1
+ fi
+}
+
+pkgctl_version_upgrade_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_upgrade_summary \
+ "${up_to_date_count}" \
+ "${out_of_date_count}" \
+ "${failure_count}" > "${tmp_file}"
+
+ # print the progress status
+ printf "📡 Upgrading: %s/%s [%s] %%spinner%%" \
+ "${BOLD}${current}" "${total}" "${percentage}%${ALL_OFF}" \
+ >> "${tmp_file}"
+
+ # swap the status file
+ mv "${tmp_file}" "${status_file}"
+}
diff --git a/src/lib/version/version.sh b/src/lib/version/version.sh
deleted file mode 100644
index d00a460..0000000
--- a/src/lib/version/version.sh
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/hint/bash
-#
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-[[ -z ${DEVTOOLS_INCLUDE_VERSION_SH:-} ]] || return 0
-DEVTOOLS_INCLUDE_VERSION_SH=1
-
-_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
-
-source /usr/share/makepkg/util/message.sh
-
-set -e
-
-
-pkgctl_version_usage() {
- local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
- cat <<- _EOF_
- Usage: ${COMMAND} [OPTIONS]
-
- Shows the current version information of pkgctl
-
- OPTIONS
- -h, --help Show this help text
-_EOF_
-}
-
-pkgctl_version_print() {
- cat <<- _EOF_
- pkgctl @buildtoolver@
-_EOF_
-}
-
-pkgctl_version() {
- while (( $# )); do
- case $1 in
- -h|--help)
- pkgctl_version_usage
- exit 0
- ;;
- *)
- die "invalid argument: %s" "$1"
- ;;
- esac
- done
-
- pkgctl_version_print
-}