Send patches - preferably formatted by git format-patch - to patches at archlinux32 dot org.
summaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorJelle van der Waa <jelle@archlinux.org>2024-01-21 13:25:57 +0100
committerChristian Heusel <christian@heusel.eu>2024-03-23 23:57:45 +0100
commit81f5e7b3b3f6e2687ebca03e212e0e7bc2da171f (patch)
treed84fce3878db9e52a8628e359e6d8882ebb22f78 /src/lib
parent01b6b0849ed098d4d3d8db3591443db3df6aa11b (diff)
feat(version): add command to automatically detect and setup nvchecker
Introduce a new version subcommand `setup` which does a best effort to generate the most minimal required .nvchecker.toml file for specific sources. It supports a wide range of common sources like: - Git, GitHub, GitLab, Hackage, NPM, PyPI, RubyGems, CPAN, crates.io The creation logic is based on matching a domain for a source which is something predictable and then simply passes an array of the url parts for every source creator to extract the useful bits out of the url array. Component: pkgctl version setup Co-authored-by: Levente Polyak <anthraxx@archlinux.org> Signed-off-by: Jelle van der Waa <jelle@archlinux.org>
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/common.sh6
-rw-r--r--src/lib/version.sh10
-rw-r--r--src/lib/version/setup.sh528
3 files changed, 544 insertions, 0 deletions
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
+}