diff options
-rw-r--r-- | contrib/completion/bash/devtools.in | 12 | ||||
-rw-r--r-- | contrib/completion/zsh/_devtools.in | 10 | ||||
-rw-r--r-- | doc/man/pkgctl-version-setup.1.asciidoc | 120 | ||||
-rw-r--r-- | doc/man/pkgctl-version.1.asciidoc | 7 | ||||
-rw-r--r-- | src/lib/common.sh | 6 | ||||
-rw-r--r-- | src/lib/version.sh | 10 | ||||
-rw-r--r-- | src/lib/version/setup.sh | 528 |
7 files changed, 693 insertions, 0 deletions
diff --git a/contrib/completion/bash/devtools.in b/contrib/completion/bash/devtools.in index 11fa234..136c80f 100644 --- a/contrib/completion/bash/devtools.in +++ b/contrib/completion/bash/devtools.in @@ -350,6 +350,7 @@ _pkgctl_repo_switch_opts() { _pkgctl_version_cmds=( check + setup upgrade ) @@ -360,6 +361,17 @@ _pkgctl_version_check_args=( _pkgctl_version_check_opts() { _filedir -d; } +_pkgctl_version_setup_args=( + --prefer-platform-api + --url + --no-check + -f --force + -h --help +) + +_pkgctl_version_setup_opts() { _filedir -d; } +_pkgctl_version_setup_args__url_opts() { :; } + _pkgctl_version_upgrade_args=( -v --verbose -h --help diff --git a/contrib/completion/zsh/_devtools.in b/contrib/completion/zsh/_devtools.in index ee6da85..f430dae 100644 --- a/contrib/completion/zsh/_devtools.in +++ b/contrib/completion/zsh/_devtools.in @@ -291,6 +291,7 @@ _pkgctl_args=( _pkgctl_version_cmds=( "pkgctl version command" "check[Compares local package versions against upstream versions]" + "setup[Automatically detect and setup a basic nvchecker config]" "upgrade[Adjust the PKGBUILD to match the latest upstream version]" ) @@ -300,6 +301,15 @@ _pkgctl_version_check_args=( '*:git_dir:_files -/' ) +_pkgctl_version_setup_args=( + '(-f --force)'{-f,--force}'[Do not prompt before overwriting]' + '--prefer-platform-api[Prefer platform specific GitHub/GitLab API for complex cases]' + '--url[Derive check target from URL instead of source array]:url:' + '--no-check[Do not run version check after setup]' + '(-h --help)'{-h,--help}'[Display usage]' + '*:git_dir:_files -/' +) + _pkgctl_version_upgrade_args=( '(-v --verbose)'{-v,--verbose}'[Display results including up-to-date versions]' '(-h --help)'{-h,--help}'[Display usage]' diff --git a/doc/man/pkgctl-version-setup.1.asciidoc b/doc/man/pkgctl-version-setup.1.asciidoc new file mode 100644 index 0000000..81ef008 --- /dev/null +++ b/doc/man/pkgctl-version-setup.1.asciidoc @@ -0,0 +1,120 @@ +pkgctl-version-setup(1) +======================= + +Name +---- +pkgctl-version-setup - Automatically detect and setup a basic nvchecker config + +Synopsis +-------- +pkgctl version setup [OPTIONS] [PKGBASE...] + +Description +----------- + +This subcommand automates the creation of a basic nvchecker(1) configuration +file by analyzing the source array specified in the PKGBUILD(1) file of a +package. This command intelligently detects various platforms and APIs (e.g., +GitHub, GitLab, PyPI) used by the package sources and generates a corresponding +`.nvchecker.toml` configuration based on its best guess. + +This is particularly useful for initializing nvchecker(1) settings for a +package without manually crafting the `.nvchecker.toml` file. It simplifies the +process of setting up version checks, especially when transitioning a package's +monitoring from one source platform to another or starting version checks for a +new package. + +If no `PKGBASE` is specified, the command defaults to using the current working +directory. + +To obtain a list of supported sources and their expected URL formats, please +consult the sources section. + +Options +------- + +*-f, --force*:: + Overwrite existing nvchecker(1) configuration + +*--prefer-platform-api*:: + Prefer platform specific GitHub/GitLab API over git for complex cases + +*--url* 'URL':: + Derive check target from the given URL instead of the source array entries + + +*--no-check*:: + Do not run pkgctl-version-check(1) after setup + +*-h, --help*:: + Show a help text + +Sources +------- + +Here are the currently supported platforms and sources, along with examples of +URL formats that enable their automatic detection as specific source types: + +*Git*:: + * https://github.com/example/project + * https://gitlab.com/example/group/project + * git://git.foobar.org/example + * git+https://git.foobar.org/example + +*GitHub*:: + * https://github.com/example/project + * https://github.com/example/project/archive/v1.0/project-v1.0.tar.gz + +*GitLab*:: + * https://gitlab.com/example/group/project + * https://gitlab.archlinux.org/archlinux/devtools.git + * https://gitlab.archlinux.org/archlinux/devtools/-/releases/v1.1.0/downloads/devtools-v1.1.0.tar.gz + +*Hackage*:: + * https://hackage.haskell.org/package/xmonad + * https://hackage.haskell.org/package/xmonad-0.18.0/xmonad-0.18.0.tar.gz + * https://hackage.haskell.org/packages/archive/xmonad/0.18.0/xmonad-0.18.0.tar.gz + +*NPM*:: + * https://registry.npmjs.org/node-gyp/-/node-gyp-10.0.1.tgz + * https://www.npmjs.com/package/node-gyp + +*PyPI*:: + * https://pypi.io/packages/source/p/pyflakes + * https://pypi.org/packages/source/b/bleach + * https://files.pythonhosted.org/packages/source/p/pyflakes + * https://pypi.org/project/SQLAlchemy/ + +*RubyGems*:: + * https://rubygems.org/downloads/diff-lcs-1.5.1.gem + * https://rubygems.org/gems/diff-lcs + +*CPAN*:: + * https://search.cpan.org/CPAN/authors/id/C/CO/COSIMO/Locale-PO-0.27.tar.gz + * https://cpan.metacpan.org/authors/id/C/CO/COSIMO/Locale-PO-0.27.tar.gz + +*crates.io*:: + * https://static.crates.io/crates/shotgun/shotgun-1.0.crate + * https://crates.io/api/v1/crates/shotgun/1.0/download + * https://crates.io/crates/git-smash + +Examples +-------- + +*pkgctl version setup*:: + Detects the source from the current directory's PKGBUILD(1) and + sets up a basic `.nvchecker.toml`. + +*pkgctl version setup --url https://github.com/example/project*:: + Generates an `.nvchecker.toml` for the current PKGBUILD(1) but + overrides the source URL with the specified GitHub project. + +See Also +-------- + +pkgctl-version(1) +pkgctl-version-check(1) +nvchecker(1) +PKGBUILD(5) + +include::include/footer.asciidoc[] diff --git a/doc/man/pkgctl-version.1.asciidoc b/doc/man/pkgctl-version.1.asciidoc index e6e4037..a72173b 100644 --- a/doc/man/pkgctl-version.1.asciidoc +++ b/doc/man/pkgctl-version.1.asciidoc @@ -26,6 +26,9 @@ package's pkgbase. The pkgbase section within the `.nvchecker.toml` file specifies the source and method for checking the latest version of the corresponding package. +Use pkgctl-version-setup(1) to automatically detect and setup a basic nvchecker +config based on the source array of the package PKGBUILD. + For detailed information on the various configuration options available for the `.nvchecker.toml` file, refer to the configuration files section in nvchecker(1). This documentation provides insights into the possible @@ -48,6 +51,9 @@ Subcommands pkgctl version check:: Compares local package versions against upstream +pkgctl version setup:: + Automatically detect and setup a basic nvchecker config + pkgctl version upgrade:: Adjust the PKGBUILD to match the latest upstream version @@ -55,6 +61,7 @@ See Also -------- pkgctl-version-check(1) +pkgctl-version-setup(1) pkgctl-version-upgrade(1) include::include/footer.asciidoc[] diff --git a/src/lib/common.sh b/src/lib/common.sh index ff767c6..00ece97 100644 --- a/src/lib/common.sh +++ b/src/lib/common.sh @@ -342,3 +342,9 @@ is_debug_package() { pkgdesc="$(getpkgdesc "${pkgfile}")" [[ ${pkgdesc} == "Detached debugging symbols for "* && ${pkgbase}-debug = "${pkgname}" ]] } + +join_by() { + local IFS="$1" + shift + echo "$*" +} diff --git a/src/lib/version.sh b/src/lib/version.sh index ac810ae..a18da83 100644 --- a/src/lib/version.sh +++ b/src/lib/version.sh @@ -19,6 +19,7 @@ pkgctl_version_usage() { COMMANDS check Compares local package versions against upstream + setup Automatically detect and setup a basic nvchecker config upgrade Adjust the PKGBUILD to match the latest upstream version OPTIONS @@ -26,6 +27,7 @@ pkgctl_version_usage() { EXAMPLES $ ${COMMAND} check libfoo linux libbar + $ ${COMMAND} setup libfoo _EOF_ } @@ -57,6 +59,14 @@ pkgctl_version() { pkgctl_version_upgrade "$@" exit $? ;; + setup) + _DEVTOOLS_COMMAND+=" $1" + shift + # shellcheck source=src/lib/version/setup.sh + source "${_DEVTOOLS_LIBRARY_DIR}"/lib/version/setup.sh + pkgctl_version_setup "$@" + exit 0 + ;; *) die "invalid argument: %s" "$1" ;; diff --git a/src/lib/version/setup.sh b/src/lib/version/setup.sh new file mode 100644 index 0000000..123862c --- /dev/null +++ b/src/lib/version/setup.sh @@ -0,0 +1,528 @@ +#!/bin/bash +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +[[ -z ${DEVTOOLS_INCLUDE_VERSION_SETUP_SH:-} ]] || return 0 +DEVTOOLS_INCLUDE_VERSION_SETUP_SH=1 + +_DEVTOOLS_LIBRARY_DIR=${_DEVTOOLS_LIBRARY_DIR:-@pkgdatadir@} +# shellcheck source=src/lib/common.sh +source "${_DEVTOOLS_LIBRARY_DIR}"/lib/common.sh +# shellcheck source=src/lib/version/check.sh +source "${_DEVTOOLS_LIBRARY_DIR}"/lib/version/check.sh + +source /usr/share/makepkg/util/message.sh +source /usr/share/makepkg/util/source.sh + +set -eo pipefail + + +pkgctl_version_setup_usage() { + local -r COMMAND=${_DEVTOOLS_COMMAND:-${BASH_SOURCE[0]##*/}} + cat <<- _EOF_ + Usage: ${COMMAND} [OPTIONS] [PKGBASE]... + + Automate the creation of a basic nvchecker configuration file by + analyzing the source array specified in the PKGBUILD file of a package. + + If no PKGBASE is specified, the command defaults to using the current + working directory. + + OPTIONS + -f, --force Overwrite existing nvchecker configuration + --prefer-platform-api Prefer platform specific GitHub/GitLab API for complex cases + --url URL Derive check target from URL instead of source array + --no-check Do not run version check after setup + -h, --help Show this help text + + EXAMPLES + $ ${COMMAND} neovim vim +_EOF_ +} + +pkgctl_version_setup() { + local pkgbases=() + local override_url= + local run_check=1 + local force=0 + local prefer_platform_api=0 + + local path ret + local checks=() + + while (( $# )); do + case $1 in + -h|--help) + pkgctl_version_setup_usage + exit 0 + ;; + -f|--force) + force=1 + shift + ;; + --prefer-platform-api) + prefer_platform_api=1 + shift + ;; + --url) + (( $# <= 1 )) && die "missing argument for %s" "$1" + override_url=$2 + shift 2 + ;; + --no-check) + run_check=0 + shift + ;; + --) + shift + break + ;; + -*) + die "invalid argument: %s" "$1" + ;; + *) + pkgbases=("$@") + break + ;; + esac + done + + # Check if used without pkgbases in a packaging directory + if (( ${#pkgbases[@]} == 0 )); then + if [[ -f PKGBUILD ]]; then + pkgbases=(".") + else + pkgctl_version_setup_usage + exit 1 + fi + fi + + ret=0 + for path in "${pkgbases[@]}"; do + # skip paths that are not directories + if [[ ! -d "${path}" ]]; then + continue + fi + + pushd "${path}" >/dev/null + if nvchecker_setup "${path}" "${force}" "${prefer_platform_api}" "${override_url}"; then + checks+=("${path}") + else + ret=1 + fi + popd >/dev/null + done + + # run checks on the setup targets + if (( run_check )) && (( ${#checks[@]} >= 1 )); then + echo + pkgctl_version_check --verbose "${checks[@]}" || true + fi + + return $ret +} + +nvchecker_setup() { + local path=$1 + local force=$2 + local prefer_platform_api=$3 + local override_url=$4 + local pkgbase pkgname source source_url proto domain url_parts section body + + if [[ ! -f PKGBUILD ]]; then + msg_error "${BOLD}${path}:${ALL_OFF} no PKGBUILD found" + return 1 + fi + + unset body pkgbase pkgname source url + # shellcheck source=contrib/makepkg/PKGBUILD.proto + if ! . ./PKGBUILD; then + msg_error "${BOLD}${path}:${ALL_OFF} failed to source PKGBUILD" + return 1 + fi + pkgbase=${pkgbase:-$pkgname} + + # try to guess from url as last try + if [[ -n ${url} ]]; then + source+=("${url}") + fi + + # handle overwrite of existing config + if [[ -f .nvchecker.toml ]] && (( ! force )); then + msg_warn "${BOLD}${pkgbase}:${ALL_OFF} nvchecker already configured" + return 1 + fi + + # override the source array with a passed URL + if [[ -n ${override_url} ]]; then + source=("${override_url}") + fi + + # skip empty source array + if (( ${#source[@]} == 0 )); then + msg_error "${BOLD}${pkgbase}:${ALL_OFF} PKGBUILD has no source array" + return 1 + fi + + for source_url in "${source[@]}"; do + # Strips out filename::http for example + source_url=$(get_url "${source_url}") + # discard query fragments + source_url=${source_url%\?*} + source_url=${source_url%#*} + + # skip patches + if [[ ${source_url} == *.patch ]]; then + continue + fi + # skip signatures + if [[ ${source_url} == *.asc ]] || [[ ${source_url} == *.sig ]]; then + continue + fi + # skip local files + if [[ ${source_url} != *://* ]]; then + continue + fi + + # split URL segments while avoiding empty element after protocol and newline at the end + mapfile -td / url_parts <<< "${source_url/:\/\//\/}/" + unset "url_parts[-1]" + + # extract protocol and domain to select the configuration type + proto=${url_parts[0]} + domain=${url_parts[1]} + + case "${domain}" in + gitlab.*) + if (( prefer_platform_api )); then + body=$(nvchecker_setup_gitlab "${url_parts[@]}") + else + body=$(nvchecker_setup_git "${url_parts[@]}") + fi + break + ;; + github.com) + if (( prefer_platform_api )); then + body=$(nvchecker_setup_github "${url_parts[@]}") + else + body=$(nvchecker_setup_git "${url_parts[@]}") + fi + break + ;; + codeberg.org) + body=$(nvchecker_setup_git "${url_parts[@]}") + break + ;; + pypi.org|pypi.io|files.pythonhosted.org) + body=$(nvchecker_setup_pypi "${url_parts[@]}") + break + ;; + hackage.haskell.org) + body=$(nvchecker_setup_hackage "${url_parts[@]}") + break + ;; + registry.npmjs.org|npmjs.com|www.npmjs.com) + body=$(nvchecker_setup_npm "${url_parts[@]}") + break + ;; + rubygems.org) + body=$(nvchecker_setup_rubygems "${url_parts[@]}") + break + ;; + *.cpan.org|*.mcpan.org|*.metacpan.org) + body=$(nvchecker_setup_cpan "${url_parts[@]}") + break + ;; + crates.io|*.crates.io) + body=$(nvchecker_setup_crates_io "${url_parts[@]}") + break + ;; + *) + if [[ ${proto} == git ]] || [[ ${proto} == git+https ]]; then + body=$(nvchecker_setup_git "${url_parts[@]}") + fi + ;; + esac + done + + if [[ -z "${body}" ]]; then + msg_error "${BOLD}${pkgbase}:${ALL_OFF} unable to automatically setup nvchecker" + return 1 + fi + + # escape the section if it contains toml subsection chars + section="${pkgbase}" + if [[ ${section} == *.* ]]; then + section="\"${section}\"" + fi + + msg_success "${BOLD}${pkgbase}:${ALL_OFF} successfully configured nvchecker" + cat > .nvchecker.toml << EOF +[${section}] +${body} +EOF +} + +get_git_url_from_parts() { + local url_parts=("$@") + local proto=${url_parts[0]#*+} + local domain=${url_parts[1]} + local url + url="${proto}://$(join_by / "${url_parts[@]:1}")" + + case "${domain}" in + gitlab.*) + url=${url%/-/*/*} + [[ ${url} != *.git ]] && url+=.git + ;; + github.com|codeberg.org) + url="${proto}://$(join_by / "${url_parts[@]:1:3}")" + [[ ${url} != *.git ]] && url+=.git + ;; + esac + + printf '%s' "${url}" +} + +# PyPI +# +# As Arch python packages don't necessarily match the pypi name, when the +# provided source url comes from pypi.io or pypi.org try to extract the package +# name from the (predictable) tarball download url for example: +# +# https://pypi.io/packages/source/p/pyflakes/pyflakes-3.1.0.tar.gz +# https://pypi.io/packages/source/p/pyflakes +# https://pypi.org/packages/source/b/bleach +# https://files.pythonhosted.org/packages/source/p/pyflakes +# https://pypi.org/project/SQLAlchemy/ +nvchecker_setup_pypi() { + local url_parts=("$@") + local pypi + + if [[ ${url_parts[2]} == packages ]]; then + pypi=${url_parts[5]} + elif [[ ${url_parts[2]} == project ]]; then + pypi=${url_parts[3]} + else + return 1 + fi + + cat << EOF +source = "pypi" +pypi = "${pypi}" +EOF +} + +# Git +# +# Set up a generic Git source, while removing the proto specific part from makepkg +# +# git+https://github.com/prometheus/prometheus.git +# https://git.foobar.com/some/path/group/project.git +# https://gitlab.com/sub/group/project/-/archive/8.0.0/packages-8.0.0.tar.gz +nvchecker_setup_git() { + local url_parts=("$@") + local url + url=$(get_git_url_from_parts "${url_parts[@]}") + + cat << EOF +source = "git" +git = "${url}" +EOF + + # best effort check if the tags are prefixed with v + if git_tags_have_version_prefix "${url}"; then + echo 'prefix = "v"' + fi +} + +git_tags_have_version_prefix() { + local url=$1 + # best effort check if the tags are prefixed with v + if ! grep --max-count=1 --quiet --extended-regex 'refs/tags/v[0-9]+[\.0-9]*$' \ + <(GIT_TERMINAL_PROMPT=0 git ls-remote --quiet --tags "${url}" 2>/dev/null); then + return 1 + fi + return 0 +} + +# Github +# +# We want to know the $org/$project name from the url +# +# https://github.com/prometheus/prometheus/archive/v2.49.1.tar.gz +nvchecker_setup_github() { + local url_parts=("$@") + local url project + if ! url=$(get_git_url_from_parts "${url_parts[@]}"); then + return 1 + fi + project=${url#*://*/} + project=${project%.git} + + cat << EOF +source = "github" +github = "${project}" +use_max_tag = true +EOF + + # best effort check if the tags are prefixed with v + if git_tags_have_version_prefix "${url}"; then + echo 'prefix = "v"' + fi +} + +# GitLab +# +# We want to know the $org/$project name from the url +# +# git+https://gitlab.com/inkscape/inkscape.git#tag=091e20ef0f204eb40ecde54436e1ef934a03d894 +nvchecker_setup_gitlab() { + local url_parts=("$@") + local url project host + if ! url=$(get_git_url_from_parts "${url_parts[@]}"); then + return 1 + fi + project=${url#*://*/} + project=${project%.git} + cat << EOF +source = "gitlab" +gitlab = "${project}" +EOF + + host=${url#*://} + host=${host%%/*} + if [[ ${host} != gitlab.com ]]; then + echo "host = \"${host}\"" + fi + + echo "use_max_tag = true" + + # best effort check if the tags are prefixed with v + if git_tags_have_version_prefix "${url}"; then + echo 'prefix = "v"' + fi +} + +# Hackage +# +# We want to know the project name +# +# https://hackage.haskell.org/package/xmonad +# https://hackage.haskell.org/package/xmonad-0.18.0/xmonad-0.18.0.tar.gz +# https://hackage.haskell.org/packages/archive/digits/0.3.1/digits-0.3.1.tar.gz +nvchecker_setup_hackage() { + local url_parts=("$@") + local hackage + + if [[ ${url_parts[2]} == packages ]]; then + hackage=${url_parts[4]} + elif [[ ${url_parts[2]} == package ]] && (( ${#url_parts[@]} == 4 )); then + hackage=${url_parts[3]} + elif [[ ${url_parts[2]} == package ]] && (( ${#url_parts[@]} >= 5 )); then + hackage=${url_parts[3]%-*} + else + return 1 + fi + + cat << EOF +source = "hackage" +hackage = "${hackage}" +EOF +} + +# NPM +# +# We want to know the project name +# +# https://registry.npmjs.org/eslint_d/-/eslint_d-12.1.0.tgz +# https://www.npmjs.com/package/node-gyp +nvchecker_setup_npm() { + local url_parts=("$@") + local npm + + if [[ ${url_parts[1]} == registry.npmjs.org ]]; then + npm=${url_parts[2]} + elif [[ ${url_parts[2]} == package ]] && (( ${#url_parts[@]} == 4 )); then + npm=${url_parts[3]} + else + return 1 + fi + + cat << EOF +source = "npm" +npm = "${npm}" +EOF +} + +# RubyGems +# +# We want to know the project name +# +# https://rubygems.org/downloads/polyglot-0.3.5.gem +# https://rubygems.org/gems/diff-lcs +nvchecker_setup_rubygems() { + local url_parts=("$@") + local gem + + if [[ ${url_parts[2]} == downloads ]]; then + gem=${url_parts[-1]%-*} + elif [[ ${url_parts[2]} == gems ]]; then + gem=${url_parts[3]} + else + return 1 + fi + + cat << EOF +source = "gems" +gems = "${gem}" +EOF +} + +# CPAN +# +# We want to know the project name +# +# source = https://search.cpan.org/CPAN/authors/id/C/CO/COSIMO/Locale-PO-1.2.3.tar.gz +nvchecker_setup_cpan() { + local url_parts=("$@") + local cpan=${url_parts[-1]} + cpan=${cpan%-*} + + cat << EOF +source = "cpan" +cpan = "${cpan}" +EOF +} + +# crates.io +# +# We want to know the crate name +# +# https://crates.io/api/v1/crates/${pkgname}/${pkgver}/download +# https://static.crates.io/crates/${pkgname}/$pkgname-$pkgver.crate +# https://crates.io/crates/git-smash +nvchecker_setup_crates_io() { + local url_parts=("$@") + local crate + + if [[ ${url_parts[2]} == crates ]]; then + crate=${url_parts[3]} + elif [[ ${url_parts[4]} == crates ]]; then + crate=${url_parts[5]} + else + return 1 + fi + + + for i in "${!url_parts[@]}"; do + if [[ ${url_parts[i]} == crates ]]; then + crate=${url_parts[(( i + 1 ))]} + fi + done + + cat << EOF +source = "cratesio" +cratesio = "${crate}" +EOF +} |