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/gitlab.sh132
-rw-r--r--src/lib/archroot.sh63
-rw-r--r--src/lib/auth.sh72
-rw-r--r--src/lib/auth/login.sh101
-rw-r--r--src/lib/auth/status.sh69
-rw-r--r--src/lib/build/build.sh420
-rw-r--r--src/lib/common.sh313
-rw-r--r--src/lib/config.sh48
-rw-r--r--src/lib/db.sh80
-rw-r--r--src/lib/db/move.sh64
-rw-r--r--src/lib/db/remove.sh69
-rw-r--r--src/lib/db/update.sh46
-rw-r--r--src/lib/release.sh167
-rw-r--r--src/lib/repo.sh110
-rw-r--r--src/lib/repo/clone.sh199
-rw-r--r--src/lib/repo/configure.sh259
-rw-r--r--src/lib/repo/create.sh113
-rw-r--r--src/lib/repo/switch.sh119
-rw-r--r--src/lib/repo/web.sh84
-rw-r--r--src/lib/util/git.sh24
-rw-r--r--src/lib/util/pacman.sh52
-rw-r--r--src/lib/valid-repos.sh22
-rw-r--r--src/lib/valid-tags.sh26
-rw-r--r--src/lib/version/version.sh47
24 files changed, 2699 insertions, 0 deletions
diff --git a/src/lib/api/gitlab.sh b/src/lib/api/gitlab.sh
new file mode 100644
index 0000000..e5f4237
--- /dev/null
+++ b/src/lib/api/gitlab.sh
@@ -0,0 +1,132 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_API_GITLAB_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_API_GITLAB_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/config.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/config.sh
+
+set -e
+
+
+gitlab_api_call() {
+ local outfile=$1
+ local request=$2
+ local endpoint=$3
+ local data=${4:-}
+ local error
+
+ # empty token
+ if [[ -z "${GITLAB_TOKEN}" ]]; then
+ msg_error " api call failed: No token provided"
+ return 1
+ fi
+
+ if ! curl --request "${request}" \
+ --url "https://${GITLAB_HOST}/api/v4/${endpoint}" \
+ --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
+ --header "Content-Type: application/json" \
+ --data "${data}" \
+ --output "${outfile}" \
+ --silent; then
+ msg_error " api call failed: $(cat "${outfile}")"
+ return 1
+ fi
+
+ # check for general purpose api error
+ if error=$(jq --raw-output --exit-status '.error' < "${outfile}"); 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
+ 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
+ msg_error " api call failed: ${error}"
+ fi
+ return 1
+ fi
+
+ return 0
+}
+
+gitlab_api_get_user() {
+ local outfile username
+
+ [[ -z ${WORKDIR:-} ]] && setup_workdir
+ outfile=$(mktemp --tmpdir="${WORKDIR}" pkgctl-gitlab-api.XXXXXXXXXX)
+
+ # query user details
+ if ! gitlab_api_call "${outfile}" GET "user/"; then
+ msg_warn " Invalid token provided?"
+ exit 1
+ fi
+
+ # extract username from details
+ if ! username=$(jq --raw-output --exit-status '.username' < "${outfile}"); then
+ msg_error " failed to query username: $(cat "${outfile}")"
+ return 1
+ fi
+
+ printf "%s" "${username}"
+ return 0
+}
+
+# Convert arbitrary project names to GitLab valid path names.
+#
+# GitLab has several limitations on project and group names and also maintains
+# a list of reserved keywords as documented on their docs.
+# https://docs.gitlab.com/ee/user/reserved_names.html
+#
+# 1. replace single '+' between word boundaries with '-'
+# 2. replace any other '+' with literal 'plus'
+# 3. replace any special chars other than '_', '-' and '.' with '-'
+# 4. replace consecutive '_-' chars with a single '-'
+# 5. replace 'tree' with 'unix-tree' due to GitLab reserved keyword
+gitlab_project_name_to_path() {
+ local name=$1
+ printf "%s" "${name}" \
+ | sed -E 's/([a-zA-Z0-9]+)\+([a-zA-Z]+)/\1-\2/g' \
+ | sed -E 's/\+/plus/g' \
+ | sed -E 's/[^a-zA-Z0-9_\-\.]/-/g' \
+ | sed -E 's/[_\-]{2,}/-/g' \
+ | sed -E 's/^tree$/unix-tree/g'
+}
+
+gitlab_api_create_project() {
+ local pkgbase=$1
+ local outfile data path project_path
+
+ [[ -z ${WORKDIR:-} ]] && setup_workdir
+ outfile=$(mktemp --tmpdir="${WORKDIR}" pkgctl-gitlab-api.XXXXXXXXXX)
+
+ project_path=$(gitlab_project_name_to_path "${pkgbase}")
+
+ # create GitLab project
+ data='{
+ "name": "'"${pkgbase}"'",
+ "path": "'"${project_path}"'",
+ "namespace_id": "'"${GIT_PACKAGING_NAMESPACE_ID}"'",
+ "request_access_enabled": "false"
+ }'
+ if ! gitlab_api_call "${outfile}" POST "projects/" "${data}"; then
+ return 1
+ fi
+
+ if ! path=$(jq --raw-output --exit-status '.path' < "${outfile}"); then
+ msg_error " failed to query path: $(cat "${outfile}")"
+ return 1
+ fi
+
+ printf "%s" "${path}"
+ return 0
+}
diff --git a/src/lib/archroot.sh b/src/lib/archroot.sh
new file mode 100644
index 0000000..8386c6c
--- /dev/null
+++ b/src/lib/archroot.sh
@@ -0,0 +1,63 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+:
+
+# shellcheck disable=2034
+CHROOT_VERSION='v5'
+
+##
+# usage : check_root $keepenv
+##
+check_root() {
+ local keepenv=$1
+ shift
+ local orig_argv=("$@")
+
+ (( EUID == 0 )) && return
+ if type -P sudo >/dev/null; then
+ exec sudo --preserve-env="${keepenv}" -- "${orig_argv[@]}"
+ else
+ exec su root -c "$(printf ' %q' "${orig_argv[@]}")"
+ fi
+}
+
+##
+# usage : is_btrfs( $path )
+# return : whether $path is on a btrfs
+##
+is_btrfs() {
+ [[ -e "$1" && "$(stat -f -c %T "$1")" == btrfs ]]
+}
+
+##
+# usage : is_subvolume( $path )
+# return : whether $path is a the root of a btrfs subvolume (including
+# the top-level subvolume).
+##
+is_subvolume() {
+ [[ -e "$1" && "$(stat -f -c %T "$1")" == btrfs && "$(stat -c %i "$1")" == 256 ]]
+}
+
+##
+# usage : subvolume_delete_recursive( $path )
+#
+# Find all btrfs subvolumes under and including $path and delete them.
+##
+subvolume_delete_recursive() {
+ local subvol
+
+ is_subvolume "$1" || return 0
+
+ while IFS= read -d $'\0' -r subvol; do
+ if ! subvolume_delete_recursive "$subvol"; then
+ return 1
+ fi
+ done < <(find "$1" -mindepth 1 -xdev -depth -inum 256 -print0)
+ if ! btrfs subvolume delete "$1" &>/dev/null; then
+ error "Unable to delete subvolume %s" "$subvol"
+ return 1
+ fi
+
+ return 0
+}
diff --git a/src/lib/auth.sh b/src/lib/auth.sh
new file mode 100644
index 0000000..77d6a90
--- /dev/null
+++ b/src/lib/auth.sh
@@ -0,0 +1,72 @@
+#!/hint/bash
+#
+# This may be included with or without `set -euE`
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_AUTH_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_AUTH_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+
+set -e
+
+
+pkgctl_auth_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [COMMAND] [OPTIONS]
+
+ Authenticate with services like GitLab.
+
+ COMMANDS
+ login Authenticate with the GitLab instance
+ status View authentication status
+
+ OPTIONS
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} login --gen-access-token
+ $ ${COMMAND} status
+_EOF_
+}
+
+pkgctl_auth() {
+ if (( $# < 1 )); then
+ pkgctl_auth_usage
+ exit 0
+ fi
+
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_auth_usage
+ exit 0
+ ;;
+ login)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/auth/login.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/auth/login.sh
+ pkgctl_auth_login "$@"
+ exit 0
+ ;;
+ status)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/auth/status.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/auth/status.sh
+ pkgctl_auth_status "$@"
+ exit 0
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ die "invalid command: %s" "$1"
+ ;;
+ esac
+ done
+}
diff --git a/src/lib/auth/login.sh b/src/lib/auth/login.sh
new file mode 100644
index 0000000..d427676
--- /dev/null
+++ b/src/lib/auth/login.sh
@@ -0,0 +1,101 @@
+#!/hint/bash
+#
+# This may be included with or without `set -euE`
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_AUTH_LOGIN_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_AUTH_LOGIN_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/config.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/config.sh
+# shellcheck source=src/lib/api/gitlab.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/gitlab.sh
+
+set -e
+
+
+pkgctl_auth_login_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS]
+
+ Interactively authenticate with the GitLab instance.
+
+ The minimum required scopes for the token are: 'api', 'write_repository'.
+
+ The GitLab API token can either be stored in a plaintext file, or
+ supplied via the DEVTOOLS_GITLAB_TOKEN environment variable using a
+ vault, see pkgctl-auth-login(1) for details.
+
+ OPTIONS
+ -g, --gen-access-token Open the URL to generate a new personal access token
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND}
+ $ ${COMMAND} --gen-access-token
+_EOF_
+}
+
+
+pkgctl_auth_login() {
+ local token personal_access_token_url
+ local GEN_ACESS_TOKEN=0
+
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_auth_login_usage
+ exit 0
+ ;;
+ -g|--gen-access-token)
+ GEN_ACESS_TOKEN=1
+ shift
+ ;;
+ *)
+ die "invalid argument: %s" "$1"
+ ;;
+ esac
+ done
+
+ personal_access_token_url="https://${GITLAB_HOST}/-/profile/personal_access_tokens?name=pkgctl+token&scopes=api,write_repository"
+
+ cat <<- _EOF_
+ Logging into ${BOLD}${GITLAB_HOST}${ALL_OFF}
+
+ Tip: you can generate a Personal Access Token here ${personal_access_token_url}
+ The minimum required scopes are 'api' and 'write_repository'.
+
+ If you do not want to store the token in a plaintext file, you can abort
+ the following prompt and supply the token via the DEVTOOLS_GITLAB_TOKEN
+ environment variable using a vault, see pkgctl-auth-login(1) for details.
+_EOF_
+
+ if (( GEN_ACESS_TOKEN )); then
+ xdg-open "${personal_access_token_url}" 2>/dev/null
+ fi
+
+ # read token from stdin
+ read -s -r -p "${GREEN}?${ALL_OFF} ${BOLD}Paste your authentication token:${ALL_OFF} " token
+ echo
+
+ if [[ -z ${token} ]]; then
+ msg_error " No token provided"
+ exit 1
+ fi
+
+ # check if the passed token works
+ GITLAB_TOKEN="${token}"
+ if ! result=$(gitlab_api_get_user); then
+ printf "%s\n" "$result"
+ exit 1
+ fi
+
+ msg_success " Logged in as ${BOLD}${result}${ALL_OFF}"
+ save_devtools_config
+}
diff --git a/src/lib/auth/status.sh b/src/lib/auth/status.sh
new file mode 100644
index 0000000..6cbaab1
--- /dev/null
+++ b/src/lib/auth/status.sh
@@ -0,0 +1,69 @@
+#!/hint/bash
+#
+# This may be included with or without `set -euE`
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_AUTH_STATUS_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_AUTH_STATUS_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/gitlab.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/gitlab.sh
+
+set -e
+
+
+pkgctl_auth_status_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS]
+
+ Verifies and displays information about your authentication state of
+ services like the GitLab instance and reports issues if any.
+
+ OPTIONS
+ -t, --show-token Display the auth token
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND}
+ $ ${COMMAND} --show-token
+_EOF_
+}
+
+pkgctl_auth_status() {
+ local SHOW_TOKEN=0
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_auth_status_usage
+ exit 0
+ ;;
+ -t|--show-token)
+ SHOW_TOKEN=1
+ shift
+ ;;
+ *)
+ die "invalid argument: %s" "$1"
+ ;;
+ esac
+ done
+
+ printf "%s\n" "${BOLD}${GITLAB_HOST}${ALL_OFF}"
+ # shellcheck disable=2119
+ if ! username=$(gitlab_api_get_user); then
+ printf "%s\n" "${username}"
+ exit 1
+ fi
+
+ msg_success " Logged in as ${BOLD}${username}${ALL_OFF}"
+ if (( SHOW_TOKEN )); then
+ msg_success " Token: ${GITLAB_TOKEN}"
+ else
+ msg_success " Token: **************************"
+ fi
+}
diff --git a/src/lib/build/build.sh b/src/lib/build/build.sh
new file mode 100644
index 0000000..3394395
--- /dev/null
+++ b/src/lib/build/build.sh
@@ -0,0 +1,420 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_BUILD_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_BUILD_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/update.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/db/update.sh
+# shellcheck source=src/lib/release.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/pacman.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/pacman.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
+
+source /usr/share/makepkg/util/config.sh
+source /usr/share/makepkg/util/message.sh
+
+set -e
+set -o pipefail
+
+
+pkgctl_build_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [PATH]...
+
+ Build packages inside a clean chroot
+
+ When a new pkgver is set using the appropriate PKGBUILD options the
+ checksums are automatically updated.
+
+ TODO
+
+ BUILD OPTIONS
+ --arch ARCH Specify architectures to build for (disables auto-detection)
+ --repo REPO Specify a target repository (disables auto-detection)
+ -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
+ -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
+
+ 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
+ -e, --edit Edit the PKGBUILD before building
+
+ RELEASE OPTIONS
+ -r, --release Automatically commit, tag and release after building
+ -m, --message MSG Use the given <msg> as the commit message
+ -u, --db-update Automatically update the pacman database as last action
+
+ OPTIONS
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND}
+ $ ${COMMAND} --rebuild --staging --message 'libyay 0.42 rebuild' libfoo libbar
+ $ ${COMMAND} --pkgver 1.42 --release --db-update
+_EOF_
+}
+
+pkgctl_build_check_option_group_repo() {
+ local option=$1
+ local repo=$2
+ local testing=$3
+ local staging=$4
+ if ( (( testing )) && (( staging )) ) ||
+ ( [[ $repo =~ ^.*-(staging|testing)$ ]] && ( (( testing )) || (( staging )) )); then
+ die "The argument '%s' cannot be used with one or more of the other specified arguments" "${option}"
+ exit 1
+ fi
+ return 0
+}
+
+pkgctl_build_check_option_group_ver() {
+ local option=$1
+ local pkgver=$2
+ local pkgrel=$3
+ local rebuild=$4
+ if [[ -n "${pkgver}" ]] || [[ -n "${pkgrel}" ]] || (( rebuild )); then
+ die "The argument '%s' cannot be used with one or more of the other specified arguments" "${option}"
+ exit 1
+ fi
+ return 0
+}
+
+# TODO: import pgp keys
+pkgctl_build() {
+ if (( $# < 1 )) && [[ ! -f PKGBUILD ]]; then
+ pkgctl_build_usage
+ exit 1
+ fi
+
+ local UPDPKGSUMS=0
+ local EDIT=0
+ local REBUILD=0
+ local OFFLOAD=0
+ local STAGING=0
+ local TESTING=0
+ local RELEASE=0
+ local DB_UPDATE=0
+
+ local REPO=
+ local PKGVER=
+ local PKGREL=
+ local MESSAGE=
+
+ local paths=()
+ local BUILD_ARCH=()
+ local BUILD_OPTIONS=()
+ local MAKECHROOT_OPTIONS=()
+ local RELEASE_OPTIONS=()
+ local MAKEPKG_OPTIONS=()
+
+ local WORKER=
+ local WORKER_SLOT=
+
+ # variables
+ local path pkgbase pkgrepo source
+
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_build_usage
+ exit 0
+ ;;
+ --repo)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ REPO="${2}"
+ pkgctl_build_check_option_group_repo '--repo' "${REPO}" "${TESTING}" "${STAGING}"
+ shift 2
+ ;;
+ --arch)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ if [[ ${2} == all ]]; then
+ BUILD_ARCH=("${_arch[@]::${#_arch[@]}-1}")
+ elif [[ ${2} == any ]]; then
+ BUILD_ARCH=("${_arch[0]}")
+ elif ! in_array "${2}" "${BUILD_ARCH[@]}"; then
+ if ! in_array "${2}" "${_arch[@]}"; then
+ die 'invalid architecture: %s' "${2}"
+ fi
+ BUILD_ARCH+=("${2}")
+ fi
+ shift 2
+ ;;
+ --pkgver=*)
+ pkgctl_build_check_option_group_ver '--pkgver' "${PKGVER}" "${PKGREL}" "${REBUILD}"
+ PKGVER="${1#*=}"
+ PKGREL=1
+ UPDPKGSUMS=1
+ shift
+ ;;
+ --pkgrel=*)
+ pkgctl_build_check_option_group_ver '--pkgrel' "${PKGVER}" "${PKGREL}" "${REBUILD}"
+ PKGREL="${1#*=}"
+ shift
+ ;;
+ --rebuild)
+ # shellcheck source=src/lib/util/git.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/git.sh
+ pkgctl_build_check_option_group_ver '--rebuild' "${PKGVER}" "${PKGREL}" "${REBUILD}"
+ REBUILD=1
+ shift
+ ;;
+ -e|--edit)
+ EDIT=1
+ shift
+ ;;
+ -o|--offload)
+ OFFLOAD=1
+ shift
+ ;;
+ -s|--staging)
+ STAGING=1
+ pkgctl_build_check_option_group_repo '--staging' "${REPO}" "${TESTING}" "${STAGING}"
+ shift
+ ;;
+ -t|--testing)
+ TESTING=1
+ pkgctl_build_check_option_group_repo '--testing' "${REPO}" "${TESTING}" "${STAGING}"
+ shift
+ ;;
+ -c|--clean)
+ BUILD_OPTIONS+=("-c")
+ shift
+ ;;
+ -I|--install)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ MAKECHROOT_OPTIONS+=("-I" "$2")
+ warning 'installing packages into the chroot may break reproducible builds, use with caution!'
+ shift 2
+ ;;
+ --nocheck)
+ MAKEPKG_OPTIONS+=("--nocheck")
+ warning 'not running checks is disallowed for official packages, except for bootstrapping. Please rebuild after bootstrapping is completed!'
+ shift
+ ;;
+ -r|--release)
+ # shellcheck source=src/lib/release.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/release.sh
+ RELEASE=1
+ shift
+ ;;
+ -m|--message)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ MESSAGE=$2
+ RELEASE_OPTIONS+=("--message" "${MESSAGE}")
+ shift 2
+ ;;
+ -u|--db-update)
+ DB_UPDATE=1
+ shift
+ ;;
+ -w|--worker)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ WORKER_SLOT=$2
+ shift 2
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ paths=("$@")
+ break
+ ;;
+ esac
+ done
+
+ # check if any release specific options were specified without releasing
+ if (( ! RELEASE )); then
+ if (( DB_UPDATE )); then
+ die "cannot use --db-update without --release"
+ fi
+ if [[ -n "${MESSAGE}" ]]; then
+ die "cannot use --message without --release"
+ fi
+ fi
+
+ # check if invoked without any path from within a packaging repo
+ if (( ${#paths[@]} == 0 )); then
+ if [[ -f PKGBUILD ]]; then
+ paths=(".")
+ else
+ pkgctl_build_usage
+ exit 1
+ fi
+ fi
+
+ # assign default worker slot
+ if [[ -z ${WORKER_SLOT} ]] && ! WORKER_SLOT="$(tty | sed 's|/dev/pts/||')"; then
+ WORKER_SLOT=$(( RANDOM % $(nproc) + 1 ))
+ fi
+ WORKER="${USER}-${WORKER_SLOT}"
+
+ # Update pacman cache for auto-detection
+ if [[ -z ${REPO} ]]; then
+ update_pacman_repo_cache
+ # Check valid repos if not resolved dynamically
+ elif ! in_array "${REPO}" "${_repos[@]}"; then
+ die "Invalid repository target: %s" "${REPO}"
+ fi
+
+ for path in "${paths[@]}"; do
+ pushd "${path}" >/dev/null
+
+ if [[ ! -f PKGBUILD ]]; then
+ die 'PKGBUILD not found in %s' "${path}"
+ fi
+
+ source=()
+ # shellcheck source=contrib/makepkg/PKGBUILD.proto
+ . ./PKGBUILD
+ pkgbase=${pkgbase:-$pkgname}
+ pkgrepo=${REPO}
+ 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'
+ fi
+ fi
+
+ # special cases to resolve final build target
+ if (( TESTING )); then
+ pkgrepo="${pkgrepo}-testing"
+ elif (( STAGING )); then
+ pkgrepo="${pkgrepo}-staging"
+ elif [[ $pkgrepo == core ]]; then
+ pkgrepo="${pkgrepo}-testing"
+ fi
+
+ # auto-detection of build architecture
+ if [[ $pkgrepo = multilib* ]]; then
+ BUILD_ARCH=("")
+ elif (( ${#BUILD_ARCH[@]} == 0 )); then
+ if in_array any "${arch[@]}"; then
+ BUILD_ARCH=("${_arch[0]}")
+ else
+ BUILD_ARCH+=("${arch[@]}")
+ fi
+ fi
+
+ # print gathered build modes
+ msg2 " repo: ${pkgrepo}"
+ msg2 " arch: ${BUILD_ARCH[*]}"
+ msg2 "worker: ${WORKER}"
+
+ # increment pkgrel on rebuild
+ if (( REBUILD )); then
+ # try to figure out of 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
+ # check if pkgrel conforms expectations
+ [[ ${pkgrel/.*} =~ ^[0-9]+$ ]] || die "Non-standard pkgrel declaration"
+ [[ ${old_pkgrel/.*} =~ ^[0-9]+$ ]] || die "Non-standard pkgrel declaration"
+ # increment pkgrel if it hasn't been changed yet
+ if [[ ${pkgrel} = "${old_pkgrel}" ]]; then
+ PKGREL=$((${pkgrel/.*}+1))
+ else
+ warning 'ignoring --rebuild as pkgrel has already been incremented from %s to %s' "${old_pkgrel}" "${pkgrel}"
+ fi
+ fi
+
+ # 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
+ 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
+ fi
+
+ # edit PKGBUILD
+ if (( EDIT )); then
+ stat_busy 'Editing PKGBUILD'
+ if [[ -n $GIT_EDITOR ]]; then
+ $GIT_EDITOR PKGBUILD || die
+ elif [[ -n $VISUAL ]]; then
+ $VISUAL PKGBUILD || die
+ elif [[ -n $EDITOR ]]; then
+ $EDITOR PKGBUILD || die
+ elif giteditor=$(git config --get core.editor); then
+ $giteditor PKGBUILD || die
+ else
+ die "No usable editor found (tried \$GIT_EDITOR, \$VISUAL, \$EDITOR, git config [core.editor])."
+ fi
+ stat_done
+ fi
+
+
+ # update checksums if any sources are declared
+ if (( UPDPKGSUMS )) && (( ${#source[@]} >= 1 )); then
+ updpkgsums
+ fi
+
+ # execute build
+ for arch in "${BUILD_ARCH[@]}"; do
+ if [[ -n $arch ]]; then
+ msg "Building ${pkgbase} for [${pkgrepo}] (${arch})"
+ BUILDTOOL="${pkgrepo}-${arch}-build"
+ else
+ msg "Building ${pkgbase} for [${pkgrepo}]"
+ BUILDTOOL="${pkgrepo}-build"
+ fi
+
+ if (( OFFLOAD )); then
+ offload-build --repo "${pkgrepo}" -- "${BUILD_OPTIONS[@]}" -- "${MAKECHROOT_OPTIONS[@]}" -l "${WORKER}" -- "${MAKEPKG_OPTIONS[@]}"
+ else
+ "${BUILDTOOL}" "${BUILD_OPTIONS[@]}" -- "${MAKECHROOT_OPTIONS[@]}" -l "${WORKER}" -- "${MAKEPKG_OPTIONS[@]}"
+ fi
+ done
+
+ # release the build
+ if (( RELEASE )); then
+ pkgctl_release --repo "${pkgrepo}" "${RELEASE_OPTIONS[@]}"
+ fi
+
+ # reset common PKGBUILD variables
+ unset pkgbase pkgname arch pkgrepo source pkgver pkgrel validpgpkeys
+ popd >/dev/null
+ done
+
+ # update the binary package repo db as last action
+ if (( RELEASE )) && (( DB_UPDATE )); then
+ # shellcheck disable=2119
+ pkgctl_db_update
+ fi
+}
diff --git a/src/lib/common.sh b/src/lib/common.sh
new file mode 100644
index 0000000..3d1ee56
--- /dev/null
+++ b/src/lib/common.sh
@@ -0,0 +1,313 @@
+#!/hint/bash
+#
+# This may be included with or without `set -euE`
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_COMMON_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_COMMON_SH="$(set +o|grep nounset)"
+
+set +u +o posix
+# shellcheck disable=1091
+. /usr/share/makepkg/util.sh
+$DEVTOOLS_INCLUDE_COMMON_SH
+
+# Avoid any encoding problems
+export LANG=C
+
+# Set buildtool properties
+export BUILDTOOL=devtools
+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_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
+
+# check if messages are to be printed using color
+if [[ -t 2 && "$TERM" != dumb ]] || [[ ${DEVTOOLS_COLOR} == always ]]; then
+ colorize
+else
+ # shellcheck disable=2034
+ declare -gr ALL_OFF='' BOLD='' BLUE='' GREEN='' RED='' YELLOW=''
+fi
+
+stat_busy() {
+ local mesg=$1; shift
+ # shellcheck disable=2059
+ printf "${GREEN}==>${ALL_OFF}${BOLD} ${mesg}...${ALL_OFF}" "$@" >&2
+}
+
+stat_progress() {
+ # shellcheck disable=2059
+ printf "${BOLD}.${ALL_OFF}" >&2
+}
+
+stat_done() {
+ # shellcheck disable=2059
+ printf "${BOLD}done${ALL_OFF}\n" >&2
+}
+
+msg_success() {
+ local msg=$1
+ local padding
+ padding=$(echo "${msg}"|sed -E 's/( *).*/\1/')
+ msg=$(echo "${msg}"|sed -E 's/ *(.*)/\1/')
+ printf "%s %s\n" "${padding}${GREEN}✓${ALL_OFF}" "${msg}" >&2
+}
+
+msg_error() {
+ local msg=$1
+ local padding
+ padding=$(echo "${msg}"|sed -E 's/( *).*/\1/')
+ msg=$(echo "${msg}"|sed -E 's/ *(.*)/\1/')
+ printf "%s %s\n" "${padding}${RED}x${ALL_OFF}" "${msg}" >&2
+}
+
+msg_warn() {
+ local msg=$1
+ local padding
+ padding=$(echo "${msg}"|sed -E 's/( *).*/\1/')
+ msg=$(echo "${msg}"|sed -E 's/ *(.*)/\1/')
+ printf "%s %s\n" "${padding}${YELLOW}!${ALL_OFF}" "${msg}" >&2
+}
+
+_setup_workdir=false
+setup_workdir() {
+ [[ -z ${WORKDIR:-} ]] && WORKDIR=$(mktemp -d --tmpdir "${0##*/}.XXXXXXXXXX")
+ _setup_workdir=true
+ trap 'trap_abort' INT QUIT TERM HUP
+ trap 'trap_exit' EXIT
+}
+
+cleanup() {
+ if [[ -n ${WORKDIR:-} ]] && $_setup_workdir; then
+ rm -rf "$WORKDIR"
+ fi
+ exit "${1:-0}"
+}
+
+abort() {
+ error 'Aborting...'
+ cleanup 255
+}
+
+trap_abort() {
+ trap - EXIT INT QUIT TERM HUP
+ abort
+}
+
+trap_exit() {
+ local r=$?
+ trap - EXIT INT QUIT TERM HUP
+ cleanup $r
+}
+
+die() {
+ (( $# )) && error "$@"
+ cleanup 255
+}
+
+##
+# usage : lock( $fd, $file, $message, [ $message_arguments... ] )
+##
+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"'
+ fi
+
+ if ! flock -n "$1"; then
+ stat_busy "${@:3}"
+ flock "$1"
+ stat_done
+ fi
+}
+
+##
+# usage : slock( $fd, $file, $message, [ $message_arguments... ] )
+##
+slock() {
+ # 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"'
+ fi
+
+ if ! flock -sn "$1"; then
+ stat_busy "${@:3}"
+ flock -s "$1"
+ stat_done
+ fi
+}
+
+##
+# usage : lock_close( $fd )
+##
+lock_close() {
+ local fd=$1
+ # https://github.com/koalaman/shellcheck/issues/862
+ # shellcheck disable=2034
+ exec {fd}>&-
+}
+
+##
+# usage: pkgver_equal( $pkgver1, $pkgver2 )
+##
+pkgver_equal() {
+ if [[ $1 = *-* && $2 = *-* ]]; then
+ # if both versions have a pkgrel, then they must be an exact match
+ [[ $1 = "$2" ]]
+ else
+ # otherwise, trim any pkgrel and compare the bare version.
+ [[ ${1%%-*} = "${2%%-*}" ]]
+ fi
+}
+
+##
+# usage: find_cached_package( $pkgname, $pkgver, $arch )
+#
+# $pkgver can be supplied with or without a pkgrel appended.
+# If not supplied, any pkgrel will be matched.
+##
+shopt -s extglob
+find_cached_package() {
+ local searchdirs=("$PWD" "$PKGDEST") results=()
+ local targetname=$1 targetver=$2 targetarch=$3
+ local dir pkg packages pkgbasename name ver rel arch r results
+
+ for dir in "${searchdirs[@]}"; do
+ [[ -d $dir ]] || continue
+
+ shopt -s extglob nullglob
+ mapfile -t packages < <(printf "%s\n" "$dir"/"${targetname}"-"${targetver}"-*"${targetarch}".pkg.tar?(.!(sig|*.*)))
+ shopt -u extglob nullglob
+
+ for pkg in "${packages[@]}"; do
+ [[ -f $pkg ]] || continue
+
+ # avoid adding duplicates of the same inode
+ for r in "${results[@]}"; do
+ [[ $r -ef $pkg ]] && continue 2
+ done
+
+ # split apart package filename into parts
+ pkgbasename=${pkg##*/}
+ pkgbasename=${pkgbasename%.pkg.tar*}
+
+ arch=${pkgbasename##*-}
+ pkgbasename=${pkgbasename%-"$arch"}
+
+ rel=${pkgbasename##*-}
+ pkgbasename=${pkgbasename%-"$rel"}
+
+ ver=${pkgbasename##*-}
+ name=${pkgbasename%-"$ver"}
+
+ if [[ $targetname = "$name" && $targetarch = "$arch" ]] &&
+ pkgver_equal "$targetver" "$ver-$rel"; then
+ results+=("$pkg")
+ fi
+ done
+ done
+
+ case ${#results[*]} in
+ 0)
+ return 1
+ ;;
+ 1)
+ printf '%s\n' "${results[0]}"
+ return 0
+ ;;
+ *)
+ error 'Multiple packages found:'
+ printf '\t%s\n' "${results[@]}" >&2
+ return 1
+ esac
+}
+shopt -u extglob
+
+check_package_validity(){
+ local pkgfile=$1
+ if grep -q "packager = Unknown Packager" <(bsdtar -xOqf "$pkgfile" .PKGINFO); then
+ die "PACKAGER was not set when building package"
+ fi
+ hashsum=sha256sum
+ pkgbuild_hash=$(awk -v"hashsum=$hashsum" -F' = ' '$1 == "pkgbuild_"hashsum {print $2}' <(bsdtar -xOqf "$pkgfile" .BUILDINFO))
+ if [[ "$pkgbuild_hash" != "$($hashsum PKGBUILD|cut -d' ' -f1)" ]]; then
+ die "PKGBUILD $hashsum mismatch: expected $pkgbuild_hash"
+ fi
+}
+
+
+# usage: grep_pkginfo pkgfile pattern
+grep_pkginfo() {
+ local _ret=()
+ mapfile -t _ret < <(bsdtar -xOqf "$1" ".PKGINFO" | grep "^${2} = ")
+ printf '%s\n' "${_ret[@]#${2} = }"
+}
+
+
+# Get the package name
+getpkgname() {
+ local _name
+
+ _name="$(grep_pkginfo "$1" "pkgname")"
+ if [[ -z $_name ]]; then
+ error "Package '%s' has no pkgname in the PKGINFO. Fail!" "$1"
+ exit 1
+ fi
+
+ echo "$_name"
+}
+
+
+# Get the package base or name as fallback
+getpkgbase() {
+ local _base
+
+ _base="$(grep_pkginfo "$1" "pkgbase")"
+ if [[ -z $_base ]]; then
+ getpkgname "$1"
+ else
+ echo "$_base"
+ fi
+}
+
+
+getpkgdesc() {
+ local _desc
+
+ _desc="$(grep_pkginfo "$1" "pkgdesc")"
+ if [[ -z $_desc ]]; then
+ error "Package '%s' has no pkgdesc in the PKGINFO. Fail!" "$1"
+ exit 1
+ fi
+
+ echo "$_desc"
+}
+
+
+get_tag_from_pkgver() {
+ local pkgver=$1
+ local tag=${pkgver}
+
+ tag=${tag/:/-}
+ tag=${tag//~/.}
+ echo "${tag}"
+}
+
+
+is_debug_package() {
+ local pkgfile=${1} pkgbase pkgname pkgdesc
+ pkgbase="$(getpkgbase "${pkgfile}")"
+ pkgname="$(getpkgname "${pkgfile}")"
+ pkgdesc="$(getpkgdesc "${pkgfile}")"
+ [[ ${pkgdesc} == "Detached debugging symbols for "* && ${pkgbase}-debug = "${pkgname}" ]]
+}
diff --git a/src/lib/config.sh b/src/lib/config.sh
new file mode 100644
index 0000000..b09479a
--- /dev/null
+++ b/src/lib/config.sh
@@ -0,0 +1,48 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_CONFIG_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_CONFIG_SH=1
+
+set -e
+
+readonly XDG_DEVTOOLS_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/devtools"
+readonly XDG_DEVTOOLS_GITLAB_CONFIG="${XDG_DEVTOOLS_DIR}/gitlab.conf"
+
+# default config variables
+export GITLAB_TOKEN=""
+
+load_devtools_config() {
+ # temporary permission fixup
+ if [[ -d "${XDG_DEVTOOLS_DIR}" ]]; then
+ chmod 700 "${XDG_DEVTOOLS_DIR}"
+ fi
+ if [[ -f "${XDG_DEVTOOLS_GITLAB_CONFIG}" ]]; then
+ chmod 600 "${XDG_DEVTOOLS_GITLAB_CONFIG}"
+ fi
+ if [[ -n "${DEVTOOLS_GITLAB_TOKEN}" ]]; then
+ GITLAB_TOKEN="${DEVTOOLS_GITLAB_TOKEN}"
+ return
+ fi
+ if [[ -f "${XDG_DEVTOOLS_GITLAB_CONFIG}" ]]; then
+ GITLAB_TOKEN=$(grep GITLAB_TOKEN "${XDG_DEVTOOLS_GITLAB_CONFIG}"|cut -d= -f2|cut -d\" -f2)
+ return
+ fi
+ GITLAB_TOKEN=""
+}
+
+save_devtools_config() {
+ # temporary permission fixup
+ if [[ -d "${XDG_DEVTOOLS_DIR}" ]]; then
+ chmod 700 "${XDG_DEVTOOLS_DIR}"
+ fi
+ if [[ -f "${XDG_DEVTOOLS_GITLAB_CONFIG}" ]]; then
+ chmod 600 "${XDG_DEVTOOLS_GITLAB_CONFIG}"
+ fi
+ (
+ umask 0077
+ mkdir -p "${XDG_DEVTOOLS_DIR}"
+ printf 'GITLAB_TOKEN="%s"\n' "${GITLAB_TOKEN}" > "${XDG_DEVTOOLS_GITLAB_CONFIG}"
+ )
+}
diff --git a/src/lib/db.sh b/src/lib/db.sh
new file mode 100644
index 0000000..397ff0d
--- /dev/null
+++ b/src/lib/db.sh
@@ -0,0 +1,80 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_DB_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_DB_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+
+set -e
+
+
+pkgctl_db_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [COMMAND] [OPTIONS]
+
+ Pacman database modification for packge update, move etc
+
+ COMMANDS
+ move Move packages between pacman repositories
+ remove Remove packages from pacman repositories
+ update Update the pacman database as final release step
+
+ OPTIONS
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} move extra-staging extra-testing libfoo libbar
+ $ ${COMMAND} remove core-testing libfoo libbar
+ $ ${COMMAND} update
+_EOF_
+}
+
+pkgctl_db() {
+ if (( $# < 1 )); then
+ pkgctl_db_usage
+ exit 0
+ fi
+
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_db_usage
+ exit 0
+ ;;
+ move)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/db/move.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/db/move.sh
+ pkgctl_db_move "$@"
+ exit 0
+ ;;
+ remove)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/db/remove.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/db/remove.sh
+ pkgctl_db_remove "$@"
+ exit 0
+ ;;
+ update)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/db/update.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/db/update.sh
+ pkgctl_db_update "$@"
+ exit 0
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ die "invalid command: %s" "$1"
+ ;;
+ esac
+ done
+}
diff --git a/src/lib/db/move.sh b/src/lib/db/move.sh
new file mode 100644
index 0000000..825b350
--- /dev/null
+++ b/src/lib/db/move.sh
@@ -0,0 +1,64 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_DB_MOVE_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_DB_MOVE_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+# shellcheck source=src/lib/common.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/common.sh
+
+set -e
+
+
+pkgctl_db_move_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [SOURCE_REPO] [TARGET_REPO] [PKGBASE]...
+
+ Move packages between binary repositories.
+
+ OPTIONS
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} extra-staging extra-testing libfoo libbar
+ $ ${COMMAND} extra core libfoo libbar
+_EOF_
+}
+
+pkgctl_db_move() {
+ local SOURCE_REPO=""
+ local TARGET_REPO=""
+ local PKGBASES=()
+
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_db_move_usage
+ exit 0
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ break
+ ;;
+ esac
+ done
+
+ if (( $# < 3 )); then
+ pkgctl_db_move_usage
+ exit 1
+ fi
+
+ SOURCE_REPO=$1
+ TARGET_REPO=$2
+ shift 2
+ PKGBASES+=("$@")
+
+ # shellcheck disable=SC2029
+ ssh "${PACKAGING_REPO_RELEASE_HOST}" db-move "${SOURCE_REPO}" "${TARGET_REPO}" "${PKGBASES[@]}"
+}
diff --git a/src/lib/db/remove.sh b/src/lib/db/remove.sh
new file mode 100644
index 0000000..ba21c83
--- /dev/null
+++ b/src/lib/db/remove.sh
@@ -0,0 +1,69 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_DB_REMOVE_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_DB_REMOVE_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+# shellcheck source=src/lib/common.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/common.sh
+
+set -e
+
+
+pkgctl_db_remove_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [REPO] [PKGBASE]...
+
+ Remove packages from binary repositories.
+
+ OPTIONS
+ -a, --arch Override the architecture (disables auto-detection)
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} core-testing libfoo libbar
+ $ ${COMMAND} --arch x86_64 core libyay
+_EOF_
+}
+
+pkgctl_db_remove() {
+ local REPO=""
+ local ARCH=any
+ local PKGBASES=()
+
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_db_remove_usage
+ exit 0
+ ;;
+ -a|--arch)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ ARCH=$2
+ shift 2
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ break
+ ;;
+ esac
+ done
+
+ if (( $# < 2 )); then
+ pkgctl_db_remove_usage
+ exit 1
+ fi
+
+ REPO=$1
+ shift
+ PKGBASES+=("$@")
+
+ # shellcheck disable=SC2029
+ ssh "${PACKAGING_REPO_RELEASE_HOST}" db-remove "${REPO}" "${ARCH}" "${PKGBASES[@]}"
+}
diff --git a/src/lib/db/update.sh b/src/lib/db/update.sh
new file mode 100644
index 0000000..269720d
--- /dev/null
+++ b/src/lib/db/update.sh
@@ -0,0 +1,46 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_DB_UPDATE_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_DB_UPDATE_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+# shellcheck source=src/lib/common.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/common.sh
+
+set -e
+
+
+pkgctl_db_update_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS]
+
+ Update the binary repository as final release step for packages that
+ have been transfered and staged on ${PACKAGING_REPO_RELEASE_HOST}.
+
+ OPTIONS
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND}
+_EOF_
+}
+
+pkgctl_db_update() {
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_db_update_usage
+ exit 0
+ ;;
+ *)
+ die "invalid argument: %s" "$1"
+ ;;
+ esac
+ done
+
+ ssh "${PACKAGING_REPO_RELEASE_HOST}" db-update
+}
diff --git a/src/lib/release.sh b/src/lib/release.sh
new file mode 100644
index 0000000..aabbd35
--- /dev/null
+++ b/src/lib/release.sh
@@ -0,0 +1,167 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_RELEASE_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_RELEASE_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+# shellcheck source=src/lib/db/update.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/db/update.sh
+# shellcheck source=src/lib/util/pacman.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/util/pacman.sh
+# shellcheck source=src/lib/valid-repos.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/valid-repos.sh
+
+source /usr/share/makepkg/util/util.sh
+
+set -e
+
+
+pkgctl_release_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [PATH]...
+
+ Release step to commit, tag and upload build artifacts
+
+ Modified version controlled files will first be staged for commit,
+ afterwards a Git tag matching the pkgver will be created and finally
+ all build artifacts will be uploaded.
+
+ By default the target pacman repository will be auto-detected by querying
+ the repo it is currently released in. When initially adding a new package
+ to the repositories, the target repo must be specified manually.
+
+ OPTIONS
+ -m, --message MSG Use the given <msg> as the commit message
+ -r, --repo REPO Specify a target repository (disables auto-detection)
+ -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
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND}
+ $ ${COMMAND} --repo core-testing --message 'libyay 0.42 rebuild' libfoo libbar
+ $ ${COMMAND} --staging --db-update libfoo
+_EOF_
+}
+
+pkgctl_release_check_option_group() {
+ local option=$1
+ local repo=$2
+ local testing=$3
+ local staging=$4
+ 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
+ return 0
+}
+
+pkgctl_release() {
+ if (( $# < 1 )) && [[ ! -f PKGBUILD ]]; then
+ pkgctl_release_usage
+ exit 1
+ fi
+
+ local MESSAGE=""
+ local PKGBASES=()
+ local REPO=""
+ local TESTING=0
+ local STAGING=0
+ local DB_UPDATE=0
+
+ local path pkgbase pkgnames repo repos
+
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_release_usage
+ exit 0
+ ;;
+ -m|--message)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ MESSAGE=$2
+ shift 2
+ ;;
+ -r|--repo)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ pkgctl_release_check_option_group '--repo' "${REPO}" "${TESTING}" "${STAGING}"
+ REPO=$2
+ shift 2
+ ;;
+ -s|--staging)
+ pkgctl_release_check_option_group '--staging' "${REPO}" "${TESTING}" "${STAGING}"
+ STAGING=1
+ shift
+ ;;
+ -t|--testing)
+ pkgctl_release_check_option_group '--testing' "${REPO}" "${TESTING}" "${STAGING}"
+ TESTING=1
+ shift
+ ;;
+ -u|--db-update)
+ DB_UPDATE=1
+ shift
+ ;;
+ -*)
+ die "invalid option: %s" "$1"
+ ;;
+ *)
+ PKGBASES+=("$@")
+ break
+ ;;
+ esac
+ done
+
+ # Resolve package from current working directory
+ if (( 0 == ${#PKGBASES[@]} )); then
+ PKGBASES=("$PWD")
+ fi
+
+ # Update pacman cache for auto-detection
+ if [[ -z ${REPO} ]]; then
+ update_pacman_repo_cache
+ # Check valid repos if not resolved dynamically
+ elif ! in_array "${REPO}" "${_repos[@]}"; then
+ die "Invalid repository target: %s" "${REPO}"
+ fi
+
+ for path in "${PKGBASES[@]}"; do
+ 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'
+ fi
+ if [[ -z "${repo}" ]]; then
+ die 'Unknown repo, please specify --repo for new packages'
+ fi
+ fi
+
+ if (( TESTING )); then
+ repo="${repo}-testing"
+ elif (( STAGING )); then
+ repo="${repo}-staging"
+ elif [[ $repo == core ]]; then
+ repo="${repo}-testing"
+ fi
+
+ msg "Releasing ${pkgbase} to ${repo}"
+ commitpkg "${repo}" "${MESSAGE}"
+
+ unset repo
+ popd >/dev/null
+ done
+
+ if (( DB_UPDATE )); then
+ # shellcheck disable=2119
+ pkgctl_db_update
+ fi
+}
diff --git a/src/lib/repo.sh b/src/lib/repo.sh
new file mode 100644
index 0000000..9f545e9
--- /dev/null
+++ b/src/lib/repo.sh
@@ -0,0 +1,110 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_REPO_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_REPO_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+
+set -e
+
+
+pkgctl_repo_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [COMMAND] [OPTIONS]
+
+ Manage Git packaging repositories and helps with their configuration
+ according to distro specs.
+
+ Git author information and the used signing key is set up from
+ makepkg.conf read from any valid location like /etc or XDG_CONFIG_HOME.
+ The configure command can be used to synchronize the distro specs and
+ makepkg.conf settings for previously cloned repositories.
+
+ The unprivileged option can be used for cloning packaging repositories
+ without SSH access using read-only HTTPS.
+
+ COMMANDS
+ clone Clone a package repository
+ configure Configure a clone according to distro specs
+ create Create a new GitLab package repository
+ switch Switch a package repository to a specified version
+ web Open the packaging repository's website
+
+ OPTIONS
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} clone libfoo linux libbar
+ $ ${COMMAND} clone --maintainer mynickname
+ $ ${COMMAND} configure *
+ $ ${COMMAND} create libfoo
+ $ ${COMMAND} switch 2:1.19.5-1 libfoo
+ $ ${COMMAND} web linux
+_EOF_
+}
+
+pkgctl_repo() {
+ if (( $# < 1 )); then
+ pkgctl_repo_usage
+ exit 0
+ fi
+
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_repo_usage
+ exit 0
+ ;;
+ clone)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/repo/clone.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/clone.sh
+ pkgctl_repo_clone "$@"
+ exit 0
+ ;;
+ configure)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/repo/configure.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/configure.sh
+ pkgctl_repo_configure "$@"
+ exit 0
+ ;;
+ create)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/repo/create.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/create.sh
+ pkgctl_repo_create "$@"
+ exit 0
+ ;;
+ switch)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/repo/switch.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/switch.sh
+ pkgctl_repo_switch "$@"
+ exit 0
+ ;;
+ web)
+ _DEVTOOLS_COMMAND+=" $1"
+ shift
+ # shellcheck source=src/lib/repo/web.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/web.sh
+ pkgctl_repo_web "$@"
+ exit 0
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ die "invalid command: %s" "$1"
+ ;;
+ esac
+ done
+}
diff --git a/src/lib/repo/clone.sh b/src/lib/repo/clone.sh
new file mode 100644
index 0000000..08bded4
--- /dev/null
+++ b/src/lib/repo/clone.sh
@@ -0,0 +1,199 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_REPO_CLONE_SH:-} ]] || return 0
+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/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
+
+source /usr/share/makepkg/util/message.sh
+
+set -e
+
+
+pkgctl_repo_clone_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [PKGBASE]...
+
+ Clone Git packaging repositories from the canonical namespace.
+
+ The configure command is subsequently invoked to synchronize the distro
+ specs and makepkg.conf settings. The protocol option can be used
+ for cloning packaging repositories without SSH access using read-only
+ HTTPS.
+
+ OPTIONS
+ -m, --maintainer=NAME Clone all packages of the named maintainer
+ --protocol https Clone the repository over https
+ --switch VERSION Switch the current working tree to a specified version
+ --universe Clone all existing packages, useful for cache warming
+ -j, --jobs N Run up to N jobs in parallel (default: $(nproc))
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} libfoo linux libbar
+ $ ${COMMAND} --maintainer mynickname
+ $ ${COMMAND} --switch 1:1.0-2 libfoo
+_EOF_
+}
+
+pkgctl_repo_clone() {
+ if (( $# < 1 )); then
+ pkgctl_repo_clone_usage
+ exit 0
+ fi
+
+ # options
+ local GIT_REPO_BASE_URL=${GIT_PACKAGING_URL_SSH}
+ local CLONE_ALL=0
+ local MAINTAINER=
+ local VERSION=
+ local CONFIGURE_OPTIONS=()
+ local jobs=
+ jobs=$(nproc)
+
+ # variables
+ local command=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ local project_path
+
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_repo_clone_usage
+ exit 0
+ ;;
+ --protocol=https)
+ GIT_REPO_BASE_URL=${GIT_PACKAGING_URL_HTTPS}
+ CONFIGURE_OPTIONS+=("$1")
+ shift
+ ;;
+ --protocol)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ if [[ $2 == https ]]; then
+ GIT_REPO_BASE_URL=${GIT_PACKAGING_URL_HTTPS}
+ else
+ die "unsupported protocol: %s" "$2"
+ fi
+ CONFIGURE_OPTIONS+=("$1" "$2")
+ shift 2
+ ;;
+ -m|--maintainer)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ MAINTAINER="$2"
+ shift 2
+ ;;
+ --maintainer=*)
+ MAINTAINER="${1#*=}"
+ shift
+ ;;
+ --switch)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ # shellcheck source=src/lib/repo/switch.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/switch.sh
+ VERSION="$2"
+ shift 2
+ ;;
+ --switch=*)
+ # shellcheck source=src/lib/repo/switch.sh
+ source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/switch.sh
+ VERSION="${1#*=}"
+ shift
+ ;;
+ --universe)
+ CLONE_ALL=1
+ shift
+ ;;
+ -j|--jobs)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ jobs=$2
+ shift 2
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ pkgbases=("$@")
+ break
+ ;;
+ esac
+ done
+
+ # 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
+ 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"
+ 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
+ if [[ ${jobs} != 1 ]] && (( ${#pkgbases[@]} > 1 )); then
+ # force colors in parallel if parent process is colorized
+ if [[ -n ${BOLD} ]]; then
+ export DEVTOOLS_COLOR=always
+ fi
+ # assign command options
+ if [[ -n "${VERSION}" ]]; then
+ command+=" --switch '${VERSION}'"
+ fi
+ if ! parallel --bar --jobs "${jobs}" "${command}" ::: "${pkgbases[@]}"; then
+ die 'Failed to clone some packages, please check the output'
+ exit 1
+ fi
+ exit 0
+ fi
+
+ for pkgbase in "${pkgbases[@]}"; do
+ if [[ ! -d ${pkgbase} ]]; then
+ msg "Cloning ${pkgbase} ..."
+ project_path=$(gitlab_project_name_to_path "${pkgbase}")
+ remote_url="${GIT_REPO_BASE_URL}/${project_path}.git"
+ if ! git clone --origin origin "${remote_url}" "${pkgbase}"; then
+ die 'failed to clone %s' "${pkgbase}"
+ fi
+ else
+ warning "Skip cloning ${pkgbase}: Directory exists"
+ fi
+
+ pkgctl_repo_configure "${CONFIGURE_OPTIONS[@]}" "${pkgbase}"
+
+ if [[ -n "${VERSION}" ]]; then
+ pkgctl_repo_switch "${VERSION}" "${pkgbase}"
+ fi
+ done
+}
diff --git a/src/lib/repo/configure.sh b/src/lib/repo/configure.sh
new file mode 100644
index 0000000..73300ae
--- /dev/null
+++ b/src/lib/repo/configure.sh
@@ -0,0 +1,259 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_REPO_CONFIGURE_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_REPO_CONFIGURE_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/gitlab.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/gitlab.sh
+
+source /usr/share/makepkg/util/config.sh
+source /usr/share/makepkg/util/message.sh
+
+set -e
+
+
+pkgctl_repo_configure_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [PATH]...
+
+ Configure Git packaging repositories according to distro specs and
+ makepkg.conf settings.
+
+ Git author information and the used signing key is set up from
+ makepkg.conf read from any valid location like /etc or XDG_CONFIG_HOME.
+
+ The remote protocol is automatically determined from the author email
+ address by choosing SSH for all official packager identities and
+ read-only HTTPS otherwise.
+
+ OPTIONS
+ --protocol https Configure remote url to use https
+ -j, --jobs N Run up to N jobs in parallel (default: $(nproc))
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} *
+_EOF_
+}
+
+get_packager_name() {
+ local packager=$1
+ local packager_pattern="(.+) <(.+@.+)>"
+ local name
+
+ if [[ ! $packager =~ $packager_pattern ]]; then
+ return 1
+ fi
+
+ name=$(echo "${packager}"|sed -E "s/${packager_pattern}/\1/")
+ printf "%s" "${name}"
+}
+
+get_packager_email() {
+ local packager=$1
+ local packager_pattern="(.+) <(.+@.+)>"
+ local email
+
+ if [[ ! $packager =~ $packager_pattern ]]; then
+ return 1
+ fi
+
+ email=$(echo "${packager}"|sed -E "s/${packager_pattern}/\2/")
+ printf "%s" "${email}"
+}
+
+is_packager_name_valid() {
+ local packager_name=$1
+ if [[ -z ${packager_name} ]]; then
+ return 1
+ elif [[ ${packager_name} == "John Doe" ]]; then
+ return 1
+ elif [[ ${packager_name} == "Unknown Packager" ]]; then
+ return 1
+ fi
+ return 0
+}
+
+is_packager_email_official() {
+ local packager_email=$1
+ if [[ -z ${packager_email} ]]; then
+ return 1
+ elif [[ $packager_email =~ .+@archlinux.org ]]; then
+ return 0
+ fi
+ return 1
+}
+
+pkgctl_repo_configure() {
+ # options
+ local GIT_REPO_BASE_URL=${GIT_PACKAGING_URL_HTTPS}
+ local official=0
+ local proto=https
+ local proto_force=0
+ local jobs=
+ jobs=$(nproc)
+ local paths=()
+
+ # variables
+ local -r command=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ local path realpath pkgbase remote_url project_path
+ local PACKAGER GPGKEY packager_name packager_email
+
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_repo_configure_usage
+ exit 0
+ ;;
+ --protocol=https)
+ proto_force=1
+ shift
+ ;;
+ --protocol)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ if [[ $2 == https ]]; then
+ proto_force=1
+ else
+ die "unsupported protocol: %s" "$2"
+ fi
+ shift 2
+ ;;
+ -j|--jobs)
+ (( $# <= 1 )) && die "missing argument for %s" "$1"
+ jobs=$2
+ shift 2
+ ;;
+ --)
+ 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_repo_configure_usage
+ exit 1
+ fi
+ fi
+
+ # Load makepkg.conf variables to be available for packager identity
+ msg "Collecting packager identity from makepkg.conf"
+ # shellcheck disable=2119
+ load_makepkg_config
+ if [[ -n ${PACKAGER} ]]; then
+ if ! packager_name=$(get_packager_name "${PACKAGER}") || \
+ ! packager_email=$(get_packager_email "${PACKAGER}"); then
+ die "invalid PACKAGER format '${PACKAGER}' in makepkg.conf"
+ fi
+ if ! is_packager_name_valid "${packager_name}"; then
+ die "invalid PACKAGER '${PACKAGER}' in makepkg.conf"
+ fi
+ if is_packager_email_official "${packager_email}"; then
+ official=1
+ if (( ! proto_force )); then
+ proto=ssh
+ GIT_REPO_BASE_URL=${GIT_PACKAGING_URL_SSH}
+ fi
+ fi
+ fi
+
+ msg2 "name : ${packager_name:-${YELLOW}undefined${ALL_OFF}}"
+ msg2 "email : ${packager_email:-${YELLOW}undefined${ALL_OFF}}"
+ msg2 "gpg-key : ${GPGKEY:-${YELLOW}undefined${ALL_OFF}}"
+ if [[ ${proto} == ssh ]]; then
+ msg2 "protocol: ${GREEN}${proto}${ALL_OFF}"
+ else
+ msg2 "protocol: ${YELLOW}${proto}${ALL_OFF}"
+ fi
+
+ # parallelization
+ if [[ ${jobs} != 1 ]] && (( ${#paths[@]} > 1 )); then
+ if [[ -n ${BOLD} ]]; then
+ export DEVTOOLS_COLOR=always
+ fi
+ if ! parallel --bar --jobs "${jobs}" "${command}" ::: "${paths[@]}"; then
+ die 'Failed to configure some packages, please check the output'
+ exit 1
+ fi
+ exit 0
+ fi
+
+ for path in "${paths[@]}"; do
+ if ! realpath=$(realpath -e "${path}"); then
+ die "No such directory: ${path}"
+ fi
+
+ pkgbase=$(basename "${realpath}")
+ pkgbase=${pkgbase%.git}
+ msg "Configuring ${pkgbase}"
+
+ if [[ ! -d "${path}/.git" ]]; then
+ die "Not a Git repository: ${path}"
+ fi
+
+ pushd "${path}" >/dev/null
+
+ project_path=$(gitlab_project_name_to_path "${pkgbase}")
+ remote_url="${GIT_REPO_BASE_URL}/${project_path}.git"
+ if ! git remote add origin "${remote_url}" &>/dev/null; then
+ git remote set-url origin "${remote_url}"
+ fi
+
+ # move the master branch to main
+ if [[ $(git symbolic-ref --quiet --short HEAD) == master ]]; then
+ git branch --move main
+ git config branch.main.merge refs/heads/main
+ fi
+
+ git config devtools.version "${GIT_REPO_SPEC_VERSION}"
+ git config pull.rebase true
+ git config branch.autoSetupRebase always
+ git config branch.main.remote origin
+ git config branch.main.rebase true
+
+ git config transfer.fsckobjects true
+ git config fetch.fsckobjects true
+ git config receive.fsckobjects true
+
+ # setup author identity
+ if [[ -n ${packager_name} ]]; then
+ git config user.name "${packager_name}"
+ git config user.email "${packager_email}"
+ fi
+
+ # force gpg for official packagers
+ if (( official )); then
+ git config commit.gpgsign true
+ fi
+
+ # set custom pgp key from makepkg.conf
+ if [[ -n $GPGKEY ]]; then
+ git config commit.gpgsign true
+ git config user.signingKey "${GPGKEY}"
+ fi
+
+ if ! git ls-remote origin &>/dev/null; then
+ warning "configured remote origin may not exist, run:"
+ msg2 "pkgctl repo create ${pkgbase}"
+ fi
+
+ popd >/dev/null
+ done
+}
diff --git a/src/lib/repo/create.sh b/src/lib/repo/create.sh
new file mode 100644
index 0000000..31b46e1
--- /dev/null
+++ b/src/lib/repo/create.sh
@@ -0,0 +1,113 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_REPO_CREATE_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_REPO_CREATE_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/gitlab.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/gitlab.sh
+# shellcheck source=src/lib/repo/clone.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/clone.sh
+# shellcheck source=src/lib/repo/configure.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/repo/configure.sh
+
+set -e
+
+
+pkgctl_repo_create_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [PKGBASE]...
+
+ Create a new Git packaging repository in the canonical GitLab namespace.
+
+ This command requires a valid GitLab API authentication. To setup a new
+ GitLab token or check the currently configured one please consult the
+ 'auth' subcommand for further instructions.
+
+ If invoked without a parameter, try to create a packaging repository
+ based on the PKGBUILD from the current working directory and configure
+ the local repository afterwards.
+
+ OPTIONS
+ -c, --clone Clone the Git repository after creation
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} libfoo
+_EOF_
+}
+
+pkgctl_repo_create() {
+ # options
+ local pkgbases=()
+ local pkgbase
+ local clone=0
+ local configure=0
+
+ # variables
+ local path
+
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_repo_create_usage
+ exit 0
+ ;;
+ -c|--clone)
+ clone=1
+ shift
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ pkgbases=("$@")
+ break
+ ;;
+ esac
+ done
+
+ # check if invoked without any path from within a packaging repo
+ if (( ${#pkgbases[@]} == 0 )); then
+ if [[ -f PKGBUILD ]]; then
+ if ! path=$(realpath -e .); then
+ die "failed to read path from current directory"
+ fi
+ pkgbases=("$(basename "${path}")")
+ clone=0
+ configure=1
+ else
+ pkgctl_repo_create_usage
+ exit 1
+ fi
+ fi
+
+ # create projects
+ for pkgbase in "${pkgbases[@]}"; do
+ if ! gitlab_api_create_project "${pkgbase}" >/dev/null; then
+ die "failed to create project: ${pkgbase}"
+ fi
+ msg_success "Successfully created ${pkgbase}"
+ if (( clone )); then
+ pkgctl_repo_clone "${pkgbase}"
+ elif (( configure )); then
+ pkgctl_repo_configure
+ fi
+ done
+
+ # some convenience hints if not in auto clone/configure mode
+ if (( ! clone )) && (( ! configure )); then
+ cat <<- _EOF_
+
+ For new clones:
+ $(msg2 "pkgctl repo clone ${pkgbases[*]}")
+ For existing clones:
+ $(msg2 "pkgctl repo configure ${pkgbases[*]}")
+ _EOF_
+ fi
+}
diff --git a/src/lib/repo/switch.sh b/src/lib/repo/switch.sh
new file mode 100644
index 0000000..f411ac2
--- /dev/null
+++ b/src/lib/repo/switch.sh
@@ -0,0 +1,119 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_REPO_SWITCH_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_REPO_SWITCH_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/message.sh
+
+set -e
+
+
+pkgctl_repo_switch_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [VERSION] [PKGBASE]...
+
+ Switch a package source repository to a specified version, tag or
+ branch. The working tree and the index are updated to match the
+ specified ref.
+
+ If a version identifier is specified in the pacman version format, that
+ identifier is automatically translated to the Git tag name accordingly.
+
+ The current working directory is used if no PKGBASE is specified.
+
+ OPTIONS
+ --discard-changes Discard changes if index or working tree is dirty
+ -f, --force An alias for --discard-changes
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} 1.14.6-1 gopass gopass-jsonapi
+ $ ${COMMAND} --force 2:1.19.5-1
+ $ ${COMMAND} main
+_EOF_
+}
+
+pkgctl_repo_switch() {
+ if (( $# < 1 )); then
+ pkgctl_repo_switch_usage
+ exit 0
+ fi
+
+ # options
+ local VERSION
+ local GIT_REF
+ local GIT_CHECKOUT_OPTIONS=()
+ local paths path realpath pkgbase
+
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_repo_switch_usage
+ exit 0
+ ;;
+ -f|--force|--discard-changes)
+ GIT_CHECKOUT_OPTIONS+=("--force")
+ shift
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ # - is special to switch back to previous version
+ if [[ $1 != - ]]; then
+ die "invalid argument: %s" "$1"
+ fi
+ ;;&
+ *)
+ if [[ -n ${VERSION} ]]; then
+ break
+ fi
+ VERSION=$1
+ shift
+ ;;
+ esac
+ done
+
+ if [[ -z ${VERSION} ]]; then
+ error "missing positional argument 'VERSION'"
+ pkgctl_repo_switch_usage
+ exit 1
+ fi
+
+ GIT_REF="$(get_tag_from_pkgver "${VERSION}")"
+ paths=("$@")
+
+ # check if invoked without any path from within a packaging repo
+ if (( ${#paths[@]} == 0 )); then
+ if [[ -f PKGBUILD ]]; then
+ paths=(".")
+ else
+ die "Not a package repository: $(realpath -- .)"
+ fi
+ fi
+
+ for path in "${paths[@]}"; do
+ if ! realpath=$(realpath -e -- "${path}"); then
+ die "No such directory: ${path}"
+ fi
+ pkgbase=$(basename "${realpath}")
+
+ if [[ ! -d "${path}/.git" ]]; then
+ error "Not a Git repository: ${path}"
+ continue
+ fi
+
+ if ! git -C "${path}" checkout "${GIT_CHECKOUT_OPTIONS[@]}" "${GIT_REF}"; then
+ die "Failed to switch ${pkgbase} to version ${VERSION}"
+ fi
+ msg "Successfully switched ${pkgbase} to version ${VERSION}"
+ done
+}
diff --git a/src/lib/repo/web.sh b/src/lib/repo/web.sh
new file mode 100644
index 0000000..45ea53b
--- /dev/null
+++ b/src/lib/repo/web.sh
@@ -0,0 +1,84 @@
+#!/bin/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_REPO_WEB_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_REPO_WEB_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/gitlab.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/api/gitlab.sh
+
+set -e
+
+
+pkgctl_repo_web_usage() {
+ local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}}
+ cat <<- _EOF_
+ Usage: ${COMMAND} [OPTIONS] [PKGBASE]...
+
+ Open the packaging repository's website via xdg-open. If called with
+ no arguments, open the package cloned in the current working directory.
+
+ OPTIONS
+ -h, --help Show this help text
+
+ EXAMPLES
+ $ ${COMMAND} linux
+_EOF_
+}
+
+pkgctl_repo_web() {
+ local pkgbases=()
+ local path giturl pkgbase
+
+ # option checking
+ while (( $# )); do
+ case $1 in
+ -h|--help)
+ pkgctl_repo_web_usage
+ exit 0
+ ;;
+ --)
+ shift
+ break
+ ;;
+ -*)
+ die "invalid argument: %s" "$1"
+ ;;
+ *)
+ pkgbases=("$@")
+ break
+ ;;
+ esac
+ done
+
+ # Check if web mode has xdg-open
+ if ! command -v xdg-open &>/dev/null; then
+ die "The web command requires 'xdg-open'"
+ fi
+
+ # Check if used without pkgnames in a packaging directory
+ if (( ! $# )); then
+ path=${PWD}
+ if [[ ! -d "${path}/.git" ]]; then
+ die "Not a Git repository: ${path}"
+ fi
+
+ giturl=$(git -C "${path}" remote get-url origin)
+ if [[ ${giturl} != *${GIT_PACKAGING_NAMESPACE}* ]]; then
+ die "Not a packaging repository: ${path}"
+ fi
+
+ pkgbase=$(basename "${giturl}")
+ pkgbase=${pkgbase%.git}
+ pkgbases=("${pkgbase}")
+ fi
+
+ for pkgbase in "${pkgbases[@]}"; do
+ path=$(gitlab_project_name_to_path "${pkgbase}")
+ xdg-open "${GIT_PACKAGING_URL_HTTPS}/${path}"
+ done
+}
diff --git a/src/lib/util/git.sh b/src/lib/util/git.sh
new file mode 100644
index 0000000..c4af662
--- /dev/null
+++ b/src/lib/util/git.sh
@@ -0,0 +1,24 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_UTIL_GIT_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_UTIL_GIT_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+
+
+git_diff_tree() {
+ local commit=$1
+ local path=$2
+ git \
+ --no-pager \
+ diff \
+ --color=never \
+ --color-moved=no \
+ --unified=0 \
+ --no-prefix \
+ --no-ext-diff \
+ "${commit}" \
+ -- "${path}"
+}
diff --git a/src/lib/util/pacman.sh b/src/lib/util/pacman.sh
new file mode 100644
index 0000000..f6c2d5f
--- /dev/null
+++ b/src/lib/util/pacman.sh
@@ -0,0 +1,52 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[[ -z ${DEVTOOLS_INCLUDE_UTIL_PACMAN_SH:-} ]] || return 0
+DEVTOOLS_INCLUDE_UTIL_PACMAN_SH=1
+
+_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@}
+# shellcheck source=src/lib/common.sh
+source "${_DEVTOOLS_LIBRARY_DIR}"/lib/common.sh
+
+set -e
+
+
+readonly _DEVTOOLS_PACMAN_CACHE_DIR=${XDG_CACHE_DIR:-$HOME/.cache}/devtools/pacman/db
+readonly _DEVTOOLS_PACMAN_CONF_DIR=${_DEVTOOLS_LIBRARY_DIR}/pacman.conf.d
+readonly _DEVTOOLS_MAKEPKG_CONF_DIR=${_DEVTOOLS_LIBRARY_DIR}/makepkg.conf.d
+
+
+update_pacman_repo_cache() {
+ mkdir -p "${_DEVTOOLS_PACMAN_CACHE_DIR}"
+ msg "Updating pacman database cache"
+ lock 10 "${_DEVTOOLS_PACMAN_CACHE_DIR}.lock" "Locking pacman database cache"
+ fakeroot -- pacman --config "${_DEVTOOLS_PACMAN_CONF_DIR}/multilib.conf" \
+ --dbpath "${_DEVTOOLS_PACMAN_CACHE_DIR}" \
+ -Sy
+ lock_close 10
+}
+
+get_pacman_repo_from_pkgbuild() {
+ local path=${1:-PKGBUILD}
+
+ # shellcheck source=contrib/makepkg/PKGBUILD.proto
+ mapfile -t pkgnames < <(source "${path}"; printf "%s\n" "${pkgname[@]}")
+
+ if (( ${#pkgnames[@]} == 0 )); then
+ die 'Failed to get pkgname from %s' "${path}"
+ return
+ fi
+
+ slock 10 "${_DEVTOOLS_PACMAN_CACHE_DIR}.lock" "Locking pacman database cache"
+ mapfile -t repos < <(pacman --config "${_DEVTOOLS_PACMAN_CONF_DIR}/multilib.conf" \
+ --dbpath "${_DEVTOOLS_PACMAN_CACHE_DIR}" \
+ -S \
+ --print \
+ --print-format '%n %r' \
+ "${pkgnames[0]}" | grep -E "^${pkgnames[0]} " | awk '{print $2}'
+ )
+ lock_close 10
+
+ printf "%s" "${repos[0]}"
+}
diff --git a/src/lib/valid-repos.sh b/src/lib/valid-repos.sh
new file mode 100644
index 0000000..14f90ce
--- /dev/null
+++ b/src/lib/valid-repos.sh
@@ -0,0 +1,22 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+:
+
+# shellcheck disable=2034
+_repos=(
+ core core-staging core-testing
+ extra extra-staging extra-testing
+ multilib multilib-staging multilib-testing
+ gnome-unstable
+ kde-unstable
+)
+
+# shellcheck disable=2034
+_build_repos=(
+ core-staging core-testing
+ extra extra-staging extra-testing
+ multilib multilib-staging multilib-testing
+ gnome-unstable
+ kde-unstable
+)
diff --git a/src/lib/valid-tags.sh b/src/lib/valid-tags.sh
new file mode 100644
index 0000000..5382c5c
--- /dev/null
+++ b/src/lib/valid-tags.sh
@@ -0,0 +1,26 @@
+#!/hint/bash
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+:
+
+# shellcheck disable=2034
+_arch=(
+ i686
+ x86_64
+ any
+)
+
+# shellcheck disable=2034
+_tags=(
+ core-x86_64 core-any
+ core-staging-x86_64 core-staging-any
+ core-testing-x86_64 core-testing-any
+ extra-x86_64 extra-any
+ extra-staging-x86_64 extra-staging-any
+ extra-testing-x86_64 extra-testing-any
+ multilib-x86_64
+ multilib-testing-x86_64
+ multilib-staging-x86_64
+ kde-unstable-x86_64 kde-unstable-any
+ gnome-unstable-x86_64 gnome-unstable-any
+)
diff --git a/src/lib/version/version.sh b/src/lib/version/version.sh
new file mode 100644
index 0000000..d00a460
--- /dev/null
+++ b/src/lib/version/version.sh
@@ -0,0 +1,47 @@
+#!/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
+}