changeset 770:56ab5c012d5f

fports: Begin a new command "fports" and fully implemented its subcommand "fports deptree". fports is supposed to be the successor to check-ports.
author Franz Glasner <fzglas.hg@dom66.de>
date Wed, 23 Oct 2024 13:56:52 +0200
parents 03350d2a2af6
children 1c9c1cd4fd47
files Makefile docs/conf.py docs/man/index.rst docs/man/man5/package-mapping.conf.rst docs/man/man5/pkgtools.conf.rst docs/man/man8/check-ports.rst docs/man/man8/fports.rst docs/man/man8/local-bsdtools.rst pkg-plist sbin/fports share/local-bsdtools/ports.subr tests/etc/package-mapping.conf tests/ports.t
diffstat 13 files changed, 843 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Wed Oct 23 01:00:33 2024 +0200
+++ b/Makefile	Wed Oct 23 13:56:52 2024 +0200
@@ -80,7 +80,7 @@
 	${MKDIR} ${WRKSRC}/bin
 	${MKDIR} ${WRKSRC}/sbin
 	${CP} Makefile ${WRKSRC}/Makefile
-.for _rp in sbin/check-ports sbin/fjail sbin/ftjail sbin/fzfs sbin/fpkg sbin/bsmtp2dma
+.for _rp in sbin/check-ports sbin/fjail sbin/ftjail sbin/fzfs sbin/fpkg sbin/fports sbin/bsmtp2dma
 	${CP} -v ${SRC}/${_rp} ${WRKSRC}/${_rp}
 	${SED} -i "" -e "s|@@VERSION@@|${PORTVERSION}|" -e "s|@@ETCDIR@@|${ETCDIR}|" -e "s|@@DATADIR@@|${DATADIR}|" -e "s|@@SIMPLEVERSIONTAG@@|${SIMPLEVERSIONTAG}|" -e "s|@@SIMPLEVERSIONSTR@@|${SIMPLEVERSIONSTR}|" ${WRKSRC}/${_rp}
 .endfor
@@ -90,7 +90,7 @@
 	${SED} -i "" -e "s|@@SIMPLEVERSIONTAG@@|${SIMPLEVERSIONTAG}|" ${WRKSRC}/${_ef}
 .endfor
 	${MKDIR} ${WRKSRC}/share/${PORTNAME}
-.for _df in share/local-bsdtools/farray.sh share/local-bsdtools/common.subr
+.for _df in share/local-bsdtools/farray.sh share/local-bsdtools/common.subr share/local-bsdtools/ports.subr
 	${CP} -v ${SRC}/${_df} ${WRKSRC}/${_df}
 	${SED} -i "" -e "s|@@SIMPLEVERSIONTAG@@|${SIMPLEVERSIONTAG}|" ${WRKSRC}/${_df}
 .endfor
@@ -114,7 +114,7 @@
 .endif
 
 do-install:
-.for _rp in sbin/check-ports sbin/fjail sbin/ftjail sbin/fzfs sbin/fpkg sbin/bsmtp2dma
+.for _rp in sbin/check-ports sbin/fjail sbin/ftjail sbin/fzfs sbin/fpkg sbin/fports sbin/bsmtp2dma
 	${INSTALL_SCRIPT} ${WRKSRC}/${_rp} ${STAGEDIR}${PREFIX}/${_rp}
 .endfor
 	${MKDIR} ${STAGEDIR}${ETCDIR}
@@ -126,7 +126,7 @@
 	${INSTALL_SCRIPT} ${WRKSRC}/etc/periodic/daily/${_ps} ${STAGEDIR}${PREFIX}/etc/periodic/daily
 .endfor
 	${MKDIR} ${STAGEDIR}${DATADIR}
-.for _df in farray.sh common.subr
+.for _df in farray.sh common.subr ports.subr
 	${INSTALL_DATA} ${WRKSRC}/share/${PORTNAME}/${_df} ${STAGEDIR}${DATADIR}
 .endfor
 	${MKDIR} ${STAGEDIR}${EXAMPLESDIR}
--- a/docs/conf.py	Wed Oct 23 01:00:33 2024 +0200
+++ b/docs/conf.py	Wed Oct 23 13:56:52 2024 +0200
@@ -92,6 +92,7 @@
     #("man/man8/fjail-privs", "fjail-privs", "Adjust some privileges within a mounted jail", [author], 8),
     #("man/man8/fjail-umount", "fjail-umount", "Recursively unmount a ZFS datasets and its children", [author], 8),
     ("man/man8/fpkg", "fpkg", "A frontend for some pkg(8) commands that also operate on running jails", [author], 8),
+    ("man/man8/fports", "fports", "Report the version status of installed packages and check for them also in repositories", [author], 8),
     ("man/man8/ftjail", "ftjail", "Management of Thin Jails", [author], 8),
     ("man/man8/ftjail-build-etcupdate-current-tmpl", "ftjail-build-etcupdate-current-tmpl", "Build a \"current\" tree suitable for the default and extract mode of \"etcupdate\"", [author], 8),
     ("man/man8/ftjail-check-freebsd-update", "ftjail-check-freebsd-update", "Check preconditions to run freebsd-update for a Thin Jail successfully", [author], 8),
--- a/docs/man/index.rst	Wed Oct 23 01:00:33 2024 +0200
+++ b/docs/man/index.rst	Wed Oct 23 13:56:52 2024 +0200
@@ -17,6 +17,7 @@
    man8/fjail-freebsd-update
    man8/fjail-hostid
    man8/fpkg
+   man8/fports
    man8/fwireguard
    man8/ftjail
    man8/ftjail-build-etcupdate-current-tmpl
--- a/docs/man/man5/package-mapping.conf.rst	Wed Oct 23 01:00:33 2024 +0200
+++ b/docs/man/man5/package-mapping.conf.rst	Wed Oct 23 13:56:52 2024 +0200
@@ -6,7 +6,7 @@
 Description
 -----------
 
-Used by :manpage:`check-ports(8)`.
+Used by :manpage:`check-ports(8)` and :manpage:`fports(8)`.
 
 This is a textfile where every line contains two fields -- separated by
 white space (TAB or SPACE). Each field is a package name::
@@ -39,3 +39,4 @@
 See Also
 --------
 
+:manpage:`fports`, :manpage:`check-ports(8)`
--- a/docs/man/man5/pkgtools.conf.rst	Wed Oct 23 01:00:33 2024 +0200
+++ b/docs/man/man5/pkgtools.conf.rst	Wed Oct 23 13:56:52 2024 +0200
@@ -6,10 +6,11 @@
 Description
 -----------
 
-Sourced in by :manpage:`fpkg(8)` and :manpage:`check-ports(8)`.
+Sourced in by :manpage:`fpkg(8)`, :manpage:`check-ports(8)` and
+:manpage:`fports(8)`.
 
 
 See Also
 --------
 
-:manpage:`check-ports(8)`, :manpage:`fpkg(8)`
+:manpage:`fports(8)`, :manpage:`check-ports(8)`, :manpage:`fpkg(8)`
--- a/docs/man/man8/check-ports.rst	Wed Oct 23 01:00:33 2024 +0200
+++ b/docs/man/man8/check-ports.rst	Wed Oct 23 13:56:52 2024 +0200
@@ -21,6 +21,9 @@
 printed with respect to repositories that have the package and have
 differing versions. This includes the ports INDEX.
 
+The new tool :manpage:`fports(8)` is a more modernized successor to this
+tool.
+
 .. program:: check-ports
 
 .. option:: -h
@@ -231,4 +234,4 @@
 See Also
 --------
 
-:manpage:`fpkg(8)`
+:manpage:`fpkg(8)`, :manpage:`fports(8)`
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/man/man8/fports.rst	Wed Oct 23 13:56:52 2024 +0200
@@ -0,0 +1,245 @@
+.. -*- coding: utf-8; indent-tabs-mode: nil; -*-
+
+fports
+======
+
+.. program:: fports
+
+
+Synopsis
+--------
+
+**fports -h**
+
+**fports -V**
+
+**fports subcommand**
+
+
+Description
+-----------
+
+Report and check the version status of installed packages and compare
+them to version in remote repositories and the local ports index.
+
+By default (without any option) the status of every package is
+printed with respect to repositories that have the package and have
+differing versions. This includes the ports INDEX.
+
+:command:`fpkg` provides some subcommands.
+
+This tools is the successor of :manpage:`check-ports(8)`.
+
+
+Subcommands
+-----------
+
+These global options are implemented:
+
+.. option:: -h
+
+   Print a short usage message to stdout and exit.
+
+.. option:: -V
+
+   Print the program name and version number to stdout and exit.
+
+
+**fports deptree** `package`...
+
+  Print a dependency tree for every given `package`. A package tree is
+  a hierarchical list of packages that `packages` depends on.
+
+  .. program:: fports deptree
+
+  .. option:: -l <maxlevel>
+
+     Limit the output to sub-levels up to `maxlevel`. To print only
+     direct dependencies use a `maxlevel` of `1`.
+     Default is 0 (i.e. no limit).
+
+  .. option:: -r
+
+     Use reversed dependencies and print the package tree for all packages
+     that depend on a given `package`.
+
+
+Environment
+-----------
+
+.. envvar:: INDEXDIR
+
+   If set, the directory to search for `INDEXFILE`. If unset,
+   :envvar:`PORTSDIR` will be used instead.
+
+.. envvar:: INDEXFILE
+
+   The filename of the ports index, search for in :envvar:`INDEXDIR` or
+   :envvar:`PORTSDIR`.
+   Default: `INDEX-<N>` where `N` is the OS major version number.
+
+.. envvar:: PORTSDIR
+
+   Specifies the location to the Ports directory.
+   Default: :file:`/usr/ports`.
+
+
+Files
+-----
+
+:file:`/usr/local/etc/local-bsdtools/package-mapping.conf`
+
+:file:`/usr/local/etc/local-bsdtools/pkgtools.conf`
+
+
+Examples
+--------
+
+Report the status of all installed packages with respect to all configured
+repositories and the ports index (if available)::
+
+   # check-ports -A
+   tdb                                  1.4.3,1           (FreeBSD)
+     INDEX          : 1.4.7,1           < needs updating (index has 1.4.7,1)
+     FreeBSD        : 1.4.7,1           < needs updating (remote has 1.4.7,1)
+     LocalBSDPorts  :                   ?
+     SharedLocalRepo:                   ?
+     LocalRepo      :                   ?
+   teckit                               2.5.11            (FreeBSD)
+     INDEX          : 2.5.11            = up-to-date with index
+     FreeBSD        : 2.5.11            = up-to-date with remote
+     LocalBSDPorts  :                   ?
+     SharedLocalRepo:                   ?
+     LocalRepo      :                   ?
+   tevent                               0.10.2_1          (FreeBSD)
+     INDEX          : 0.13.0_1          < needs updating (index has 0.13.0_1)
+     FreeBSD        : 0.13.0            < needs updating (remote has 0.13.0)
+     LocalBSDPorts  :                   ?
+     SharedLocalRepo:                   ?
+     LocalRepo      :                   ?
+   tex-basic-engines                    20210325          (FreeBSD)
+     INDEX          : 20210325          = up-to-date with index
+     FreeBSD        : 20210325          = up-to-date with remote
+     LocalBSDPorts  :                   ?
+     SharedLocalRepo:                   ?
+     LocalRepo      :                   ?
+   #
+
+Report the status of all installed packages with respect to all configured
+repositories that provide the package::
+
+   # check-ports -a
+   tdb                                  1.4.3,1           (FreeBSD)
+     INDEX          : 1.4.7,1           < needs updating (index has 1.4.7,1)
+     FreeBSD        : 1.4.7,1           < needs updating (remote has 1.4.7,1)
+   teckit                               2.5.11            (FreeBSD)
+     INDEX          : 2.5.11            = up-to-date with index
+     FreeBSD        : 2.5.11            = up-to-date with remote
+   tevent                               0.10.2_1          (FreeBSD)
+     INDEX          : 0.13.0_1          < needs updating (index has 0.13.0_1)
+     FreeBSD        : 0.13.0            < needs updating (remote has 0.13.0)
+   tex-basic-engines                    20210325          (FreeBSD)
+     INDEX          : 20210325          = up-to-date with index
+     FreeBSD        : 20210325          = up-to-date with remote
+   #
+
+The standard output considers installed packages with versions that differ in any of
+the configured repositories *including*  a ports INDEX::
+
+   # check-ports
+   tdb                                  1.4.3,1           (FreeBSD)
+     INDEX          : 1.4.7,1           < needs updating (index has 1.4.7,1)
+     FreeBSD        : 1.4.7,1           < needs updating (remote has 1.4.7,1)
+   tevent                               0.10.2_1          (FreeBSD)
+     INDEX          : 0.13.0_1          < needs updating (index has 0.13.0_1)
+     FreeBSD        : 0.13.0            < needs updating (remote has 0.13.0)
+   tex-xetex                            0.99993_1         (FreeBSD)
+     INDEX          : 0.99993_2         < needs updating (index has 0.99993_2)
+     FreeBSD        : 0.99993_1         = up-to-date with remote
+   texlive-base                         20210325_5        (FreeBSD)
+     INDEX          : 20210325_10       < needs updating (index has 20210325_10)
+     FreeBSD        : 20210325_8        < needs updating (remote has 20210325_8)
+   #
+
+The effect of an additional :option:`-v` on :command:`check-ports` is::
+
+   # check-ports -v
+   tdb                                  1.4.3,1           (FreeBSD)
+     INDEX          : 1.4.7,1           < needs updating (index has 1.4.7,1)
+     FreeBSD        : 1.4.7,1           < needs updating (remote has 1.4.7,1)
+   teckit                               2.5.11            (FreeBSD)
+   tevent                               0.10.2_1          (FreeBSD)
+     INDEX          : 0.13.0_1          < needs updating (index has 0.13.0_1)
+     FreeBSD        : 0.13.0            < needs updating (remote has 0.13.0)
+   tex-basic-engines                    20210325          (FreeBSD)
+   tex-dvipdfmx                         20210325          (FreeBSD)
+   tex-dvipsk                           2021.1            (FreeBSD)
+   tex-formats                          20210325_1        (FreeBSD)
+   tex-jadetex                          3.13_4            (FreeBSD)
+   tex-kpathsea                         6.3.3             (FreeBSD)
+   tex-libtexlua                        5.3.6             (FreeBSD)
+   tex-libtexluajit                     2.1.0             (FreeBSD)
+   tex-luatex                           1.12.0            (FreeBSD)
+   tex-ptexenc                          1.3.9             (FreeBSD)
+   tex-synctex                          2.0.0_1           (FreeBSD)
+   tex-web2c                            20210325          (FreeBSD)
+   tex-xdvik                            22.87.06          (FreeBSD)
+   tex-xetex                            0.99993_1         (FreeBSD)
+     INDEX          : 0.99993_2         < needs updating (index has 0.99993_2)
+     FreeBSD        : 0.99993_1         = up-to-date with remote
+   tex-xmltex                           1.9_3             (FreeBSD)
+   texlive-base                         20210325_5        (FreeBSD)
+     INDEX          : 20210325_10       < needs updating (index has 20210325_10)
+     FreeBSD        : 20210325_8        < needs updating (remote has 20210325_8)
+   #
+
+The :option:`-s` suppresses the output if only the version of a ports INDEX differs::
+
+   # check-ports -s
+   tdb                                  1.4.3,1           (FreeBSD)
+     INDEX          : 1.4.7,1           < needs updating (index has 1.4.7,1)
+     FreeBSD        : 1.4.7,1           < needs updating (remote has 1.4.7,1)
+   tevent                               0.10.2_1          (FreeBSD)
+     INDEX          : 0.13.0_1          < needs updating (index has 0.13.0_1)
+     FreeBSD        : 0.13.0            < needs updating (remote has 0.13.0)
+   texlive-base                         20210325_5        (FreeBSD)
+     INDEX          : 20210325_10       < needs updating (index has 20210325_10)
+     FreeBSD        : 20210325_8        < needs updating (remote has 20210325_8)
+   #
+
+The effect of an additional :option:`-v` on :command:`checkports -s` is::
+
+   # check-ports -sv
+   tdb                                  1.4.3,1           (FreeBSD)
+     INDEX          : 1.4.7,1           < needs updating (index has 1.4.7,1)
+     FreeBSD        : 1.4.7,1           < needs updating (remote has 1.4.7,1)
+   teckit                               2.5.11            (FreeBSD)
+   tevent                               0.10.2_1          (FreeBSD)
+     INDEX          : 0.13.0_1          < needs updating (index has 0.13.0_1)
+     FreeBSD        : 0.13.0            < needs updating (remote has 0.13.0)
+   tex-basic-engines                    20210325          (FreeBSD)
+   tex-dvipdfmx                         20210325          (FreeBSD)
+   tex-dvipsk                           2021.1            (FreeBSD)
+   tex-formats                          20210325_1        (FreeBSD)
+   tex-jadetex                          3.13_4            (FreeBSD)
+   tex-kpathsea                         6.3.3             (FreeBSD)
+   tex-libtexlua                        5.3.6             (FreeBSD)
+   tex-libtexluajit                     2.1.0             (FreeBSD)
+   tex-luatex                           1.12.0            (FreeBSD)
+   tex-ptexenc                          1.3.9             (FreeBSD)
+   tex-synctex                          2.0.0_1           (FreeBSD)
+   tex-web2c                            20210325          (FreeBSD)
+   tex-xdvik                            22.87.06          (FreeBSD)
+   tex-xetex                            0.99993_1         (FreeBSD)
+   tex-xmltex                           1.9_3             (FreeBSD)
+   texlive-base                         20210325_5        (FreeBSD)
+     INDEX          : 20210325_10       < needs updating (index has 20210325_10)
+     FreeBSD        : 20210325_8        < needs updating (remote has 20210325_8)
+   #
+
+
+See Also
+--------
+
+:manpage:`fpkg(8)`, :manpage:`check-ports(8)`,
+:manpage:`pkgtools.conf(5)`, :manpage:`package-mapping.conf(5)`
--- a/docs/man/man8/local-bsdtools.rst	Wed Oct 23 01:00:33 2024 +0200
+++ b/docs/man/man8/local-bsdtools.rst	Wed Oct 23 13:56:52 2024 +0200
@@ -67,6 +67,8 @@
 
 - :manpage:`fpkg(8)`
 
+- :manpage:`fports(8)`  
+
 - :manpage:`fbhyve(8)`
 
 - :manpage:`fwireguard(8)`
--- a/pkg-plist	Wed Oct 23 01:00:33 2024 +0200
+++ b/pkg-plist	Wed Oct 23 13:56:52 2024 +0200
@@ -7,9 +7,11 @@
 sbin/fjail
 sbin/ftjail
 sbin/fpkg
+sbin/fports
 sbin/fzfs
 %%DATADIR%%/common.subr
 %%DATADIR%%/farray.sh
+%%DATADIR%%/ports.subr
 %%EXAMPLESDIR%%/freebsd-update-ftjail-template.sh
 %%EXAMPLESDIR%%/freebsd-update-ftjail.sh
 %%DOCS%%share/man/man5/bsmtp2dma.conf.5.gz
@@ -25,6 +27,7 @@
 %%DOCS%%share/man/man8/fjail-freebsd-update.8.gz
 %%DOCS%%share/man/man8/fjail-hostid.8.gz
 %%DOCS%%share/man/man8/fpkg.8.gz
+%%DOCS%%share/man/man8/fports.8.gz
 %%DOCS%%share/man/man8/ftjail.8.gz
 %%DOCS%%share/man/man8/ftjail-build-etcupdate-current-tmpl.8.gz
 %%DOCS%%share/man/man8/ftjail-check-freebsd-update.8.gz
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sbin/fports	Wed Oct 23 13:56:52 2024 +0200
@@ -0,0 +1,272 @@
+#!/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_maxlevel=0
+    opt_reversed=no
+    while getopts "l:r" opt; do
+        case "${opt}" in
+            l)
+                opt_maxlevel=$(($OPTARG + 0));;
+            r)
+                opt_reversed=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}" "$@"
+    else
+        _command_deptree_normal "${opt_maxlevel}" "$@"
+    fi
+}
+
+
+#:
+#: Implementation of printing a "normal" dependency tree
+#:
+_command_deptree_normal() {
+    local maxlevel     # $@
+    
+    local pkgdeps pkgqueue curdeps pkg n v
+
+    maxlevel="${1}"
+    shift
+    
+    # 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
+    for pkg in "$@"; do
+        _print_dependency_tree 0 "${maxlevel}" '-->' "${pkg}" "$(LC_ALL=C.UTF-8 pkg query '%v' "${pkg}")" "${pkgdeps}"
+    done
+    falist_release pkgdeps
+}
+
+
+#:
+#: Implementation of printing a reversed dependency tree
+#:
+_command_deptree_reversed() {
+    local maxlevel   # $@
+    
+    local pkgdeps pkgqueue curdeps pkg n v
+
+    maxlevel="${1}"
+    shift
+    
+    # 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
+    for pkg in "$@"; do
+        _print_dependency_tree 0 "${maxlevel}" '<--' "${pkg}" "$(LC_ALL=C.UTF-8 pkg query '%v' "${pkg}")" "${pkgdeps}"
+    done
+    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 llist 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
+}
+
+
+#
+# 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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/local-bsdtools/ports.subr	Wed Oct 23 13:56:52 2024 +0200
@@ -0,0 +1,229 @@
+#!/bin/sh
+# -*- mode: shell-script; indent-tabs-mode: nil; -*-
+#:
+#: Helper functions for :command:`fports` and friends.
+#:
+#: :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@@
+#:
+
+
+: # Dummy separator for shellcheck: no module-wide settings below this line
+
+
+# Need definitions from farray.subr
+type falist_create 1>/dev/null 2>/dev/null || { echo "ERROR: source \`farray.sh' first"; exit 70; }
+# Need definitions from common.subr
+type fatal 1>/dev/null 2>/dev/null || { echo "ERROR: source \`common.subr' first"; exit 70; }
+
+
+#:
+#: Determine whether a package `_package` is essentially the same as
+#: another package.
+#:
+#: Args:
+#:   $1 (str): The package mapping database alist. This database must have
+#:             been initialized by `init_package_mapping`.
+#:
+#:   $2 (str): The name of the installed package
+#:
+#: Returns:
+#:   int: 0 (truthy) when a package mapping has been found,
+#:        1 (falsy) otherwise (in this case the output to stdout is empty)
+#:
+#: Output (stdout):
+#:   The name of the package on which `_package` is/was based on
+#:
+get_package_mapping() {
+    local _mapping _installed_package
+
+    local _pos _iname _mapped_package
+
+    _mapping="${1-}"
+    _installed_package="${2-}"
+    [ -z "${_installed_package}" ] && fatal "${EX_USAGE}" "missing package name"
+
+    if falist_tryget _mapped_package "${_mapping}" "${_installed_package}"; then
+        printf '%s' "${_mapped_package}"
+        return 0
+    else
+        return 1
+    fi
+}
+
+
+#:
+#: Slurp in the configured :file:`package-mapping.conf`.
+#:
+#: This command reads in the the mapping database in in file which defaults
+#: to :file:`/usr/local/etc/local-bsdtools/package-mapping.conf`.
+#: It is read into a freshly created alist. Its variable name is given as
+#: `$1`.
+#:
+#: Args:
+#:   $1 (str): The variable where to create the alist
+#:
+#: Input (Globals):
+#:   PACKAGE_MAPPING: See also :manpage:`package-mapping.conf(5)`.
+#:
+#: Return:
+#:   int: 0 on success,
+#:        65 (aka `EX_DATAERR`) if a duplicate package name was encountered.
+#:
+#:        Note that an unreadable `PACKAGE_MAPPING` database file does not
+#:        error but yields an empty database.
+#:
+#: Example of a :file:`package-mapping.conf`::
+#:
+#:   #
+#:   # _installed_package               mapped_package_name
+#:   #
+#:   fmg-nextcloud-php71                nextcloud-php71
+#:   fmg-nextcloud-twofactor_totp-php71 nextcloud-twofactor_totp-php71
+#:
+init_package_mapping() {
+    local _mapping
+
+    local _iname _mapped_package
+
+    falist_create "$1" || return
+    _mapping="$1"
+
+    if [ -r "${PACKAGE_MAPPING}" ] ; then
+        while IFS=$' \t' read -r _iname _mapped_package ; do
+            case "${_iname}" in
+                '')
+                    # empty line
+                    continue;;
+                \#*)
+                    # comment
+                    continue;;
+                *)
+                    if ! falist_set_unique "${_mapping}" "${_iname}" "${_mapped_package}" ; then
+                        fatal "${EX_DATAERR}" "duplicate installed package name \`${_iname}'"
+                        return 1
+                    fi
+                    ;;
+            esac
+        done < "${PACKAGE_MAPPING}"
+    fi
+    return 0
+}
+
+
+#:
+#: Read in the list of configured and enabled package repositories
+#: from :manpage:`pkg(8)`.
+#:
+#: The output of :command:`pkg -vv` is parsed to get all the configured
+#: and enabled repositories.
+#:
+#: Output (stdout):
+#:   Each configured and enabled repository name is printed on a single
+#:   line.
+#:
+get_configured_pkg_repository_names() {
+
+    local _line _status _repository_name _repository_enabled _key _value _rest
+
+    _status=''
+    while IFS=$' \t' read -r _line; do
+        case "${_status}" in
+            '')
+                if [ "${_line}" = "Repositories:" ]; then
+                    _status="repositories"
+                fi
+                ;;
+            repositories)
+                case "${_line}" in
+                    *': {')
+                        _status=repository
+                        _repository_name="${_line%:*}"
+                        _repository_enabled=no
+                        ;;
+                    *)
+                        ;;
+                esac
+                ;;
+            repository)
+                case "${_line}" in
+                    *\})
+                        if [ "${_repository_enabled}" = 'yes' ] ; then
+                            printf '%s\n' "${_repository_name}"
+                        fi
+                        _repository_name=''
+                        _repository_enabled=no
+                        _status=repositories
+                        ;;
+                    *)
+                        _key=''
+                        _value=''
+                        IFS=$' \t:' read -r _key _value _rest || true <<EOF73d43dccec1a496f98cb519be160f549
+${_line}
+EOF73d43dccec1a496f98cb519be160f549
+                        if [ "${_key}" = "enabled" ]; then
+                            case "${_value}" in
+                                'yes,'|yes)
+                                    _repository_enabled=yes;;
+                                *)
+                                    _repository_enabled=no;;
+                            esac
+                        fi
+                        ;;
+                esac
+                ;;
+            *)
+                fatal "${EX_SOFTWARE}" "unhandled format of \`pkg -vv'"
+        esac
+    done <<EOF7c6ea1b0ce544021a7813757c7003392
+$(LC_ALL=C.UTF-8 pkg -vv)
+EOF7c6ea1b0ce544021a7813757c7003392
+}
+
+
+_cleanup_init_repositories() {
+    [ -n "${__repodb}" ] && falist_release "${_repodb}"
+    [ -n "${__allrepos}" ] && falist_release "${_allrepos}"
+}
+
+
+init_repositories() {
+    local _allrepos
+
+    local _reponame _repodb _pkgname _pkgversion
+    local _idx
+    local _test
+
+    falist_create "$1" || return
+    _allrepos="$1"
+
+    while IFS='' read -r _reponame; do
+        if ! falist_create _repodb; then
+            _cleanup_init_repositories
+            return 1
+        fi
+        farray_create _test
+        _idx=0
+        while IFS=$'\t' read -r _pkgname _pkgversion; do
+            _idx=$((_idx + 1))
+            echo "$_pkgname $_pkgversion"
+            #_test="$(/sbin/skein256 -q "${_pkgname}")"
+            falist_add _repodb "${_pkgname}" "${_pkgversion}"
+            #farray_append _test "${_pkgversion}"
+            #[ $_idx -gt 5000 ] && break
+        done <<EOF_pkg_9b5d20d4-805e-484e-9afb-ecc62e75f7cc
+$(LC_ALL=C.UTF-8 pkg rquery -U -r "${_reponame}" $'%n\t%v')
+EOF_pkg_9b5d20d4-805e-484e-9afb-ecc62e75f7cc
+        falist_set "${_allrepos}" "${_reponame}" "${_repodb}"
+        falist_release _repodb
+    done <<EOF_repos_d6177ceb-b027-4fe2-bc71-e6b5017c0663
+$(get_configured_pkg_repository_names)
+EOF_repos_d6177ceb-b027-4fe2-bc71-e6b5017c0663
+    falist_debug "${_allrepos}"
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/etc/package-mapping.conf	Wed Oct 23 13:56:52 2024 +0200
@@ -0,0 +1,4 @@
+install-package parent-package
+install-package-v2	parent-package-v2
+# commented out
+#install-package-v3	parant-package-v3
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/ports.t	Wed Oct 23 13:56:52 2024 +0200
@@ -0,0 +1,73 @@
+Basic tests of ports.subr
+
+Shell is /bin/sh
+
+
+Setup
+=====
+
+We need common.subr and ports.subr
+
+  $ set -u
+  $ . "${TESTDIR}/testsetup.sh"
+  $ _p_datadir="${TESTDIR}/../share/local-bsdtools"
+  $ . "${_p_datadir}/farray.sh"
+  $ . "${_p_datadir}/common.subr"
+  $ . "${_p_datadir}/ports.subr"
+
+
+Package Mapping
+===============
+
+init_package_mapping
+--------------------
+
+  $ init_package_mapping PMAPPING
+
+
+get_package_mapping
+-------------------
+
+An empty database errors fatally
+
+  $ (get_package_mapping '' whatever-package)
+  ERROR: missing falist name or token value
+  [70]
+
+Empty package name errors fatally
+
+  $ (get_package_mapping PMAPPING)
+  /bin/sh: ERROR: missing package name
+  [64]
+
+  $ get_package_mapping PMAPPING install-package
+  parent-package (no-eol)
+
+This is the target of the mapping
+
+  $ get_package_mapping PMAPPING parent-package
+  [1]
+
+Another package
+
+  $ get_package_mapping PMAPPING install-package-v2
+  parent-package-v2 (no-eol)
+
+Commented out
+
+  $ get_package_mapping PMAPPING install-package-v3
+  [1]
+
+
+Repositories
+============
+
+Assume that FreeBSD is always configured and enabled
+
+  $ get_configured_pkg_repository_names | grep -F FreeBSD
+  FreeBSD
+
+
+# Runtime much too long: need another strategy
+#  $ REPODB=''
+#  $ init_repositories REPODB