index : devtools32 | |
Archlinux32 fork of devtools | gitolite user |
summaryrefslogtreecommitdiff |
author | Andreas Baumann <mail@andreasbaumann.cc> | 2023-05-29 16:05:59 +0200 |
---|---|---|
committer | Andreas Baumann <mail@andreasbaumann.cc> | 2023-05-29 16:05:59 +0200 |
commit | be8ac63f28a402acbf7e21972ac5d3b45bed1f79 (patch) | |
tree | 6eb83c09f6549ae41988a25243a2b424a4f99a4e /src/lib | |
parent | bfc22eea7e4c6877fe8b9d89fa574cb0729466db (diff) | |
parent | a07df0beeaeea1bf5665512bacc7a013eece4602 (diff) |
-rw-r--r-- | src/lib/api/gitlab.sh | 132 | ||||
-rw-r--r-- | src/lib/archroot.sh | 63 | ||||
-rw-r--r-- | src/lib/auth.sh | 72 | ||||
-rw-r--r-- | src/lib/auth/login.sh | 101 | ||||
-rw-r--r-- | src/lib/auth/status.sh | 69 | ||||
-rw-r--r-- | src/lib/build/build.sh | 420 | ||||
-rw-r--r-- | src/lib/common.sh | 313 | ||||
-rw-r--r-- | src/lib/config.sh | 48 | ||||
-rw-r--r-- | src/lib/db.sh | 80 | ||||
-rw-r--r-- | src/lib/db/move.sh | 64 | ||||
-rw-r--r-- | src/lib/db/remove.sh | 69 | ||||
-rw-r--r-- | src/lib/db/update.sh | 46 | ||||
-rw-r--r-- | src/lib/release.sh | 167 | ||||
-rw-r--r-- | src/lib/repo.sh | 110 | ||||
-rw-r--r-- | src/lib/repo/clone.sh | 199 | ||||
-rw-r--r-- | src/lib/repo/configure.sh | 259 | ||||
-rw-r--r-- | src/lib/repo/create.sh | 113 | ||||
-rw-r--r-- | src/lib/repo/switch.sh | 119 | ||||
-rw-r--r-- | src/lib/repo/web.sh | 84 | ||||
-rw-r--r-- | src/lib/util/git.sh | 24 | ||||
-rw-r--r-- | src/lib/util/pacman.sh | 52 | ||||
-rw-r--r-- | src/lib/valid-repos.sh | 22 | ||||
-rw-r--r-- | src/lib/valid-tags.sh | 26 | ||||
-rw-r--r-- | src/lib/version/version.sh | 47 |
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 +} |