view sbin/fports @ 786:b78815b47d5e

fports: deptree has now an option to print a flattened tree. The flattened tree is the transitive closure of all dependencies.
author Franz Glasner <fzglas.hg@dom66.de>
date Tue, 29 Oct 2024 11:03:22 +0100
parents 43cebff4ea0d
children 004c676c3415
line wrap: on
line source

#!/bin/sh
# -*- indent-tabs-mode: nil; -*-
#:
#: Check the version status of installed ports and compare them to
#: version in remote repositories and the local ports index.
#:
#: :Author:    Franz Glasner
#: :Copyright: (c) 2017-2024 Franz Glasner.
#:             All rights reserved.
#: :License:   BSD 3-Clause "New" or "Revised" License.
#:             See LICENSE for details.
#:             If you cannot find LICENSE see
#:             <https://opensource.org/licenses/BSD-3-Clause>
#: :ID:        @(#)@@SIMPLEVERSIONTAG@@
#:

: # separator for shellcheck: no module-level directives below

# shellcheck disable=SC2034    # VERSION appears unused
VERSION='@@VERSION@@'

# shellcheck disable=SC2016    # no expansion
USAGE='
USAGE: fports [ GLOBAL-OPTIONS ] COMMAND [ COMMAND-OPTIONS ]

GLOBAL OPTIONS:

  -V    Print the program name and version number to stdout and exit

  -h    Print this help message to stdout and exit

'


_p_datadir='@@DATADIR@@'
[ "${_p_datadir#@@DATADIR}" = '@@' ] && _p_datadir="$(dirname "$0")"/../share/local-bsdtools
. "${_p_datadir}/common.subr"
. "${_p_datadir}/farray.sh"
. "${_p_datadir}/ports.subr"


#:
#: Configuration directory.
#:
: "${CONFIGDIR:=@@ETCDIR@@}"

#:
#: Mapping configuration: installed package name -> original package name.
#:
#: Note:
#:   This is independent of any repo
#
: "${PACKAGE_MAPPING:=${CONFIGDIR}/package-mapping.conf}"

# shellcheck disable=SC1091   # does not exist -- cannot read
[ -r "${CONFIGDIR}/pkgtools.conf" ] && . "${CONFIGDIR}/pkgtools.conf"


# no unset variables
set -u


#:
#: Implementation of the "deptree" command.
#:
command_deptree() {
    local opt opt_reversed opt_maxlevel opt_flat
    # $@

    opt_maxlevel=0
    opt_reversed=no
    opt_flat=no
    while getopts "l:rt" opt; do
        case "${opt}" in
            l)
                opt_maxlevel=$(($OPTARG + 0));;
            r)
                # shellcheck disable=SC2034
                opt_reversed=yes;;
            t)
                opt_flat=yes;;
            \?)
                exit 2;;
            *)
                fatal 2 "option handling failed";;
        esac
    done
    shift $((OPTIND-1))
    OPTIND=1

    if checkyesno opt_reversed; then
        _command_deptree_reversed "${opt_maxlevel}" "${opt_flat}" "$@"
    else
        _command_deptree_normal "${opt_maxlevel}" "${opt_flat}" "$@"
    fi
}


#:
#: Implementation of printing a "normal" dependency tree
#:
_command_deptree_normal() {
    local maxlevel flat     # $@

    local pkgdeps pkgqueue curdeps pkg n v flatdeps

    maxlevel="${1}"
    flat="${2}"
    shift 2

    # shellcheck disable=SC2034    # pkgqueue seems unused
    pkgqueue=''
    farray_create pkgqueue  # queue (array) of packages that are queued for
                            # resolution

    for pkg in "$@"; do
        if ! pkg query '%n' "${pkg}" 1>/dev/null 2>/dev/null ; then
            farray_release pkgqueue
            fatal "${EX_DATAERR}" "Package not found: ${pkg}"
        fi
        farray_append pkgqueue "${pkg}"
    done
    pkgdeps=''
    falist_create pkgdeps   # alist of packagges with its direct dependencies
    while farray_pop pkg pkgqueue 1; do
        if ! falist_contains pkgdeps "${pkg}"; then
            curdeps=''
            farray_create curdeps
            while IFS=$' \t\n' read -r n v; do
                [ -z "${n}" ] || [ -z "${v}" ] && continue
                farray_append curdeps "${n}=${v}"
                farray_append pkgqueue "${n}"
            done <<EOF_01a8cebe-8659-4e32-87a4-bbce117e386b
$(LC_ALL=C.UTF-8 pkg query '%dn %dv' "${pkg}")
EOF_01a8cebe-8659-4e32-87a4-bbce117e386b
            falist_set pkgdeps "${pkg}" "${curdeps}"
            farray_release curdeps
            curdeps=''
        fi
    done
    farray_release pkgqueue
    #    falist_debug pkgdeps
    if checkyesno flat; then
        for pkg in "$@"; do
            _flatten_pkgdeps flatdeps "${pkgdeps}" "${pkg}"
            _print_flatdeps '-->' "${pkg}" "$(LC_ALL=C.UTF-8 pkg query '%v' "${pkg}")" "${flatdeps}"
            falist_release "${flatdeps}"
        done
    else
        for pkg in "$@"; do
            _print_dependency_tree 0 "${maxlevel}" '-->' "${pkg}" "$(LC_ALL=C.UTF-8 pkg query '%v' "${pkg}")" "${pkgdeps}"
        done
    fi
    falist_release pkgdeps
}


#:
#: Implementation of printing a reversed dependency tree
#:
_command_deptree_reversed() {
    local maxlevel flat  # $@

    local pkgdeps pkgqueue curdeps pkg n v flatdeps

    maxlevel="${1}"
    # shellcheck disable=SC2034    # appears unused
    flat="${2}"
    shift 2

    # shellcheck disable=SC2034    # pkgqueue seems unused
    pkgqueue=''
    farray_create pkgqueue  # queue (array) of packages that are queued for
                            # resolution

    for pkg in "$@"; do
        if ! pkg query '%n' "${pkg}" 1>/dev/null 2>/dev/null ; then
            farray_release pkgqueue
            fatal "${EX_DATAERR}" "Package not found: ${pkg}"
        fi
        farray_append pkgqueue "${pkg}"
    done
    pkgdeps=''
    falist_create pkgdeps   # alist of packagges with its direct dependencies
    while farray_pop pkg pkgqueue 1; do
        if ! falist_contains pkgdeps "${pkg}"; then
            curdeps=''
            farray_create curdeps
            while IFS=$' \t\n' read -r n v; do
                [ -z "${n}" ] || [ -z "${v}" ] && continue
                farray_append curdeps "${n}=${v}"
                farray_append pkgqueue "${n}"
            done <<EOF_5079e996-c6d2-4e6d-825d-53183a64ab06
$(LC_ALL=C.UTF-8 pkg query '%rn %rv' "${pkg}")
EOF_5079e996-c6d2-4e6d-825d-53183a64ab06
            falist_set pkgdeps "${pkg}" "${curdeps}"
            farray_release curdeps
            curdeps=''
        fi
    done
    farray_release pkgqueue
    #    falist_debug pkgdeps
    if checkyesno flat; then
        for pkg in "$@"; do
            _flatten_pkgdeps flatdeps "${pkgdeps}" "${pkg}"
            _print_flatdeps '<--' "${pkg}" "$(LC_ALL=C.UTF-8 pkg query '%v' "${pkg}")" "${flatdeps}"
            falist_release "${flatdeps}"
        done
    else
        for pkg in "$@"; do
            _print_dependency_tree 0 "${maxlevel}" '<--' "${pkg}" "$(LC_ALL=C.UTF-8 pkg query '%v' "${pkg}")" "${pkgdeps}"
        done
    fi
    falist_release pkgdeps
}


#:
#: Internal helper to print an indented dependency list for a package.
#:
#: Args:
#:   $1 (int): The (indentation) level where a level of `0` is the root level
#:   $2 (int): The maximum level (`$1`)  to print to
#:   $3 (str): The package tag to use to for non-root-levels
#:   $4 (str): The package name
#:   $5 (str): The package version
#:   $6 (alist): The alist of resolved packages and their dependencies
#:
_print_dependency_tree() {
    # $1 $2 $3 $4 $5 $6

    local i pkg ver curdeps

    if [ "${2}" -ge 1 ]; then
        [ "${1}" -gt "${2}" ] && return 0
    fi

    i="${1}"
    while [ "${i}" -gt 1 ]; do
        printf '%s' '    '
        i=$((i - 1))
    done
    [ "${1}" -ne 0 ] && printf '%s ' "${3}"
    printf '%s v%s\n' "${4}" "${5}"
    falist_get curdeps "${6}" "${pkg}"
    i=1
    while farray_tryget pkg "${curdeps}" "${i}"; do
        ver="${pkg#*=}"
        pkg="${pkg%%=*}"
        _print_dependency_tree $(($1 + 1)) "${2}" "${3}" "${pkg}" "${ver}" "${6}"
        i=$((i + 1))
    done
    farray_release curdeps
}


#:
#: Args:
#:   $1 (str): The package tag to use
#:   $2 (str): The root package name
#:   $3 (str): The package version of the root package in `$3`
#:   $4 (alist): The alist of the flattened dependencies
#:
_print_flatdeps() {
    # $1 $2 $3 $4

    local pkgnames i n v

    printf '%s v%s\n' "${2}" "${3}"

    pkgnames=''
    farray_create pkgnames
    falist_keys pkgnames "$4"
    farray_sort pkgnames
    i=1
    while farray_tryget n pkgnames "${i}"; do
        falist_get v "${4}" "${n}"
        printf '%s %s v%s\n' "${1}" "${n}" "${v}"
        i=$((i + 1))
    done

    farray_release pkgnames
}


#:
#: Flatten a package dependency alist.
#:
#: Args:
#:   $1 (str): The variable name where to store the flattened dependencies
#:             into. This object (alist) must be released by the caller.
#:   $2 (str): The alist with all packages and its dependencies
#:   $3 (str): The package for which to flatten its dependencies
#:
_flatten_pkgdeps() {
    local pkgdeps rootpkg     # and $1

    local alldeps queue pkg curdeps i depname depver

    pkgdeps="${2}"
    rootpkg="${3}"

    falist_contains pkgdeps "${rootpkg}" || fatal "${EX_SOFTWARE}" "given package \`${rootpkg}' not in the given package dependency map"

    # shellcheck disable=SC2034    # appears unused
    queue=''
    farray_create queue     # array with package names to be flattened

    farray_append queue "${rootpkg}"

    alldeps=''
    falist_create alldeps   # alist with pkgname -> version
    while farray_pop pkg queue 1; do
        if ! falist_contains alldeps "${pkg}"; then
            curdeps=''
            falist_get curdeps pkgdeps "${pkg}"
            i=1
            while farray_tryget depname curdeps "${i}"; do
                depver="${depname#*=}"
                depname="${depname%%=*}"
                if ! falist_contains alldeps "${depname}"; then
                    falist_set alldeps "${depname}" "${depver}"
                fi
                farray_append queue "${depname}"
                i=$((i + 1))
            done
            farray_release curdeps
        fi
    done

    farray_release queue

    setvar "${1}" "${alldeps}"
}


#
# Global option handling
#
while getopts "Vh" _opt ; do
    case "${_opt}" in
        V)
            printf 'fports %s\n' '@@SIMPLEVERSIONSTR@@'
            exit 0
            ;;
        h)
            echo "${USAGE}"
            exit 0
            ;;
        \?)
            exit 2;
            ;;
        *)
            fatal 2 "option handling failed"
            ;;
    esac
done

#
# Reset the Shell's option handling system to prepare for handling
# command-local options.
#
shift $((OPTIND-1))
OPTIND=1

command="${1-}"
shift

case "${command}" in
    '') fatal 2 "no command given";;
    deptree)
        command_deptree "$@";;
    *)
        fatal 2 "unknown command \`${command}'";;
esac