The branch main has been updated by des:

URL: 
https://cgit.FreeBSD.org/src/commit/?id=e6467516f584505ac29fd370966abd530a7b5454

commit e6467516f584505ac29fd370966abd530a7b5454
Author:     Dag-Erling Smørgrav <[email protected]>
AuthorDate: 2026-06-29 00:41:11 +0000
Commit:     Dag-Erling Smørgrav <[email protected]>
CommitDate: 2026-06-29 13:49:22 +0000

    pkgbasify: Add new utility
    
    The pkgbasify utility converts a system installed from distribution sets
    to packaged base by registering, without actually installing, a set of
    packages that corresponds to what is already installed.
    
    MFC after:      1 week
---
 usr.sbin/Makefile               |   1 +
 usr.sbin/pkgbasify/Makefile     |   6 +
 usr.sbin/pkgbasify/pkgbasify.8  | 215 +++++++++++++++
 usr.sbin/pkgbasify/pkgbasify.sh | 596 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 818 insertions(+)

diff --git a/usr.sbin/Makefile b/usr.sbin/Makefile
index 9e885d5d45c5..2b478b26d3d8 100644
--- a/usr.sbin/Makefile
+++ b/usr.sbin/Makefile
@@ -57,6 +57,7 @@ SUBDIR=       adduser \
        nologin \
        pciconf \
        periodic \
+       pkgbasify \
        pnfsdscopymr \
        pnfsdsfile \
        pnfsdskill \
diff --git a/usr.sbin/pkgbasify/Makefile b/usr.sbin/pkgbasify/Makefile
new file mode 100644
index 000000000000..cbd8637b78c3
--- /dev/null
+++ b/usr.sbin/pkgbasify/Makefile
@@ -0,0 +1,6 @@
+SCRIPTS=pkgbasify.sh
+LINKS= ${BINDIR}/pkgbasify ${BINDIR}/depkgbasify
+MAN=   pkgbasify.8
+MLINKS=        pkgbasify.8 depkgbasify.8
+
+.include <bsd.prog.mk>
diff --git a/usr.sbin/pkgbasify/pkgbasify.8 b/usr.sbin/pkgbasify/pkgbasify.8
new file mode 100644
index 000000000000..9c44740e4132
--- /dev/null
+++ b/usr.sbin/pkgbasify/pkgbasify.8
@@ -0,0 +1,215 @@
+.\"-
+.\" Copyright (c) 2026 Dag-Erling Smørgrav
+.\"
+.\" SPDX-License-Identifier: BSD-2-Clause
+.\"
+.Dd June 28, 2026
+.Dt PKGBASIFY 8
+.Os
+.Sh NAME
+.Nm pkgbasify ,
+.Nm depkgbasify
+.Nd convert a system to and from pkgbase
+.Sh SYNOPSIS
+.Nm pkgbasify
+.Op Fl hnqtv
+.Op Fl a | Fl y
+.Op Fl b Ar basedir
+.Op Fl j Ar jail
+.Op Fl r Ar repo
+.Nm depkgbasify
+.Op Fl hnqv
+.Op Fl a | Fl y
+.Op Fl b Ar basedir
+.Op Fl j Ar jail
+.Sh DESCRIPTION
+The
+.Nm pkgbasify
+utility converts a system installed from distribution sets to packaged
+base by registering, without actually installing, a set of packages
+that corresponds to what is already installed.
+See
+.Sx Converting to Packaged Base
+below.
+.Pp
+The
+.Nm depkgbasify
+utility converts a packaged base system back to distribution sets by
+unregistering all base packages.
+See
+.Sx Converting from Packaged Base
+below.
+.Pp
+In both cases, the user is urged to take a backup or snapshot of the
+system and perform a dry run before the actual conversion.
+.Pp
+The following options are available:
+.Bl -tag -width indent
+.It Fl a
+Automatic mode.
+Do not prompt the user, but skip certain optional steps.
+Mutually exclusive with
+.Fl y .
+.It Fl b Ar basedir
+Operate on a system mounted at
+.Ar basedir .
+.It Fl h
+Print a usage message and exit.
+.It Fl j Ar jail
+Operate on the specified jail.
+.It Fl n
+Dry run.
+Examine the system and download the necessary files but do not
+register or unregister anything.
+.It Fl q
+Quiet mode.
+.It Fl r Ar repo
+.Po
+.Nm pkgbasify
+only
+.Pc
+Use the specified packaged base repository instead of the default.
+.It Fl t
+.Po
+.Nm pkgbasify
+only
+.Pc
+Trim.
+Not yet implemented.
+.It Fl v
+Verbose mode.
+.It Fl y
+Assume the answer to every prompt is
+.Dq yes .
+Mutually exclusive with
+.Fl a .
+.El
+.Ss Converting to Packaged Base
+The
+.Nm pkgbasify
+utility begins by inspecting the system to identify which distribution
+sets are installed.
+It then downloads and registers the sets of base packages that
+correspond to those distribution sets.
+If not running in automatic mode, it then performs a forced upgrade
+and enables the
+.Dv BACKUP_LIBRARIES
+options in the
+.Xr pkg 8
+configuration.
+.Pp
+It is important to note that after the conversion, files that should
+have been present on the system but weren't will still be recorded in
+the package database.
+This is why a non-automatic
+.Nm pkgbasify
+offers to perform a forced upgrade: to reset the system to a state
+where the files on disk match the recorded inventory.
+However, since
+.Nm pkgbasify
+does not make a backup of the system before converting it, this forced
+upgrade is irreversible, which is also why it is skipped in automatic
+mode.
+.Ss Converting from Packaged Base
+The
+.Nm depkgbasify
+utility begins by identifying the repository from which the base
+system was installed.
+It then attempts to identify which package sets are partially or
+completely installed.
+If not running in automatic mode, it will then offer to complete any
+partially installed sets, remove the source tree (if installed from
+packages), and perform a last upgrade before the conversion.
+Finally, it unregisters all remaining base packages without removing
+any of the files.
+.Ss Round-trip Conversion
+When run in automatic mode, neither
+.Nm pkgbasify
+nor
+.Nm depkgbasify
+will make any changes to the system that the other cannot undo.
+Thus, starting with a system installed from distribution sets and
+running
+.Ql pkgbasify -a
+followed by
+.Ql depkgbasify -a
+should have a net effect of no changes to the system except for the
+package cache.
+Conversely, starting with a freshly updated packaged base system and
+running
+.Ql depkgbasify -a
+followed by
+.Ql pkgbasify -a
+should also have a net zero effect.
+.Sh ENVIRONMENT
+.Bl -tag -width indent
+.It Ev BASEDIR
+base directory as if passed to the
+.Fl b
+option.
+.It Ev BASEREPO
+repository as if passed to the
+.Fl r
+option.
+.It Ev JAIL
+jail id or name as if passed to the
+.Fl j
+option.
+.El
+.Sh EXIT STATUS
+.Ex -std pkgbasify depkgbasify
+.Sh SEE ALSO
+.Xr build 7 ,
+.Xr pkg 7 ,
+.Xr pkgbase 7 ,
+.Xr freebsd-update 8 ,
+.Xr pkg 8
+.Sh HISTORY
+The
+.Nm pkgbasify
+and
+.Nm depkgbasify
+utilities first appeared in
+.Fx 15.2 .
+.Sh AUTHORS
+The
+.Nm pkgbasify
+and
+.Nm depkgbasify
+utilities and this manual page were written by
+.An Dag-Erling Sm\(/orgrav Aq Mt [email protected] .
+.Sh CAVEATS
+The
+.Nm pkgbasify
+utility is intended for use on systems installed from distribution
+sets.
+It will also work on systems installed from source, but if the system
+has been customized through the use of build options, the conversion
+will round it back up, so to speak, to the smallest set of
+distribution sets which include everything present on the system.
+Conversely,
+.Nm pkgbasify
+will not notice or register elements of the system which are disabled
+by default but have been locally enabled, built and installed, unless
+installing from a repository that has been built with the same build
+options.
+.Pp
+When converting a system installed from source to packaged base, the
+recommended procedure is to build a set of packages from the same
+source tree and using the same build options, then configure a
+repository pointing to those packages, and perform the conversion
+using that repository before switching to another and doing a forced
+upgrade.
+.Pp
+The
+.Nm depkgbasify
+utility merely unregisters the base packages.
+It does not revert the binaries to a state that could have been
+produced by an official installation image or
+.Xr freebsd-update 8 .
+However, if the system was using base packages from the official
+.Fx
+packaged base repository prior to the conversion, the contents should
+match what
+.Xr freebsd-update 8
+expects.
diff --git a/usr.sbin/pkgbasify/pkgbasify.sh b/usr.sbin/pkgbasify/pkgbasify.sh
new file mode 100755
index 000000000000..86f926bfcd83
--- /dev/null
+++ b/usr.sbin/pkgbasify/pkgbasify.sh
@@ -0,0 +1,596 @@
+#!/bin/sh
+#-
+# Copyright (c) 2026 Dag-Erling Smørgrav
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+set -eu
+exec 3>&2
+
+progname="${0##*/}"
+localbase="$(/sbin/sysctl -qn user.localbase || echo /usr/local)"
+
+#
+# All the top-level sets except src
+#
+topsets="base base-dbg kernels kernels-dbg lib32 lib32-dbg tests"
+
+#
+# Default repository
+#
+OS=FreeBSD
+BASEREPO=${BASEREPO:-${OS}-base}
+
+#
+# Command-line options
+#
+auto=
+base=${BASEDIR:-}
+dryrun=
+jail=${JAIL:-}
+quiet=
+repo=
+trim=
+verbose=
+yes=
+
+#
+# Print a message
+#
+msg() {
+       local width=$((${COLUMNS:-80} - ${#progname} - 4))
+       echo "$@" | /usr/bin/fmt -sw${width} |
+           /usr/bin/sed "s|^|${progname}: |"
+}
+
+#
+# Print an error message and exit
+#
+err() {
+       msg "$@" >&3
+       exit 1
+}
+
+#
+# Print a warning message
+#
+warn() {
+       if ! [ "${quiet}" ]; then
+               msg "$@" >&3
+       fi
+}
+
+#
+# Print an informational message
+#
+info() {
+       if ! [ "${quiet}" ]; then
+               msg "$@"
+       fi
+}
+
+#
+# Ask the user for confirmation
+#
+confirm() {
+       local ans=
+       if [ "${yes}" ]; then
+               return 0
+       fi
+       while :; do
+               read -p "$@ " -r ans
+               case ${ans} in
+               [Yy]|[Yy][Ee][Ss])
+                       return 0
+                       ;;
+               [Nn]|[Nn][Oo])
+                       return 1
+                       ;;
+               *)
+                       echo "Please answer with yes or no." >&3
+                       ;;
+               esac
+       done
+}
+
+#
+# Execute a command, but print it first if verbose
+#
+v() {
+       if [ "${verbose}" ]; then
+               echo "$@" >&3
+       fi
+       "$@"
+}
+
+#
+# Compute the path of a file in the target environment
+#
+path() {
+       local path="${base}$1"
+       if [ -n "${jail}" ]; then
+               path="$(/usr/sbin/jls -j "${jail}" path)${path}"
+       fi
+       echo "${path}"
+}
+
+#
+# Check if a file exists in the target environment
+#
+exists() {
+       v test -e "$(path "$@")"
+}
+
+#
+# Run pkg(7)
+#
+# pkg(7) currently lacks support for -j and -r, otherwise we could run
+# every pkg command through it, which would prepare us for when pkg
+# eventually moves into base.  For now, we can use jexec for -j, but
+# there is no way to emulate -r.
+#
+pkg7() {
+       if [ -n "${base}" ]; then
+               err "pkg(7) currently lacks -r option"
+       elif [ -n "${jail}" ]; then
+               v /usr/sbin/jexec "${jail}" /usr/sbin/pkg "$@"
+       else
+               v /usr/sbin/pkg "$@"
+       fi
+}
+
+#
+# Run pkg(8)
+#
+pkg() {
+       v "${localbase}"/sbin/pkg ${jail:+-j"${jail}"} ${base:+-r"${base}"} "$@"
+}
+
+#
+# Run pkg bootstrap
+#
+pkg_bootstrap() {
+       if ! pkg7 -N 2>/dev/null; then
+               warn "Package manager not installed"
+               if confirm "Bootstrap the package manager?"; then
+                       pkg7 bootstrap -r"${repo}" -y
+               else
+                       err "Unable to proceed without package manager"
+               fi
+       fi
+}
+
+#
+# Check if a package exists in the repository
+#
+pkg_exists() {
+       pkg rquery -r"${repo}" -U %v "$@" >/dev/null
+}
+
+#
+# Run pkg fetch
+#
+pkg_fetch() {
+       pkg fetch -r"${repo}" -U ${quiet:+-q} "$@"
+}
+
+#
+# Run pkg install
+#
+pkg_install() {
+       pkg install -r"${repo}" -U ${dryrun:+-n} ${quiet:+-q} "$@"
+}
+
+#
+# Check if a package is installed
+#
+pkg_installed() {
+       pkg info -q "$@"
+}
+
+#
+# Run pkg query
+#
+pkg_query() {
+       pkg query "$@"
+}
+
+#
+# Run pkg install --register-only
+#
+pkg_register() {
+       pkg install -r"${repo}" --register-only ${dryrun:+-n} ${quiet:+-q} "$@"
+}
+
+#
+# Run pkg remove
+#
+pkg_remove() {
+       pkg remove ${dryrun:+-n} ${quiet:+-q} "$@"
+}
+
+#
+# Run pkg rquery
+#
+pkg_rquery() {
+       pkg rquery -r"${repo}" -U "$@"
+}
+
+#
+# Run pkg unregister
+#
+pkg_unregister() {
+       pkg unregister ${dryrun:+-n} ${quiet:+-q} "$@"
+}
+
+#
+# Run pkg update
+#
+pkg_update() {
+       pkg update -r"${repo}" ${quiet:+-q} "$@"
+}
+
+#
+# Run pkg upgrade
+#
+pkg_upgrade() {
+       pkg upgrade -r"${repo}" ${dryrun:+-n} ${quiet:+-q} "$@"
+}
+
+#
+# Generate a complete transitive list of dependencies
+#
+pkg_alldeps() {
+       local deps=$(pkg rquery %dn "$@")
+       if [ "${deps}" ]; then
+               echo "${deps}"
+               pkg_alldeps ${deps}
+       fi | sort -u
+}
+
+#
+# Check if pkg is new enough
+#
+check_pkg() {
+       local ver=$(pkg -v)
+       case ${ver} in
+       1.*|2.[0-6].*|2.7.[0-4])
+               err "pkg ${ver} is too old, I need pkg 2.7.5 or newer"
+               ;;
+       esac
+}
+
+#
+# Try to ascertain if the system is running pkgbase
+#
+ispkgbase() {
+       pkg which -q /sbin/init >/dev/null
+}
+
+#
+# Convert to pkgbase
+#
+pkgbasify() {
+       local sets=base set=
+
+       # Check if we're running pkgbase
+       if ispkgbase; then
+               err "The system is already running packaged base"
+       fi
+
+       # Update the repository
+       pkg_update
+
+       # Some paths
+       local debug=/usr/lib/debug
+       local ld=/libexec/ld-elf.so.1
+       local ld32=/libexec/ld-elf32.so.1
+       local test=/usr/tests/Kyuafile
+       local src=/usr/src
+
+       # Sanity check
+       if ! exists "${ld}"; then
+               err "No base system found"
+       fi
+
+       # Decide whether to register base-dbg
+       if ! exists "${debug}${ld}.debug"; then
+               warn "No debugging symbols found"
+       else
+               sets="${sets} base-dbg"
+       fi
+
+       # Decide whether to register lib32 and lib32-dbg
+       if ! pkg_exists ${OS}-set-lib32; then
+               # Not available
+       elif ! exists "${ld32}"; then
+               warn "No 32-bit loader found"
+       else
+               sets="${sets} lib32"
+               if exists "${debug}${ld32}.debug"; then
+                       sets="${sets} lib32-dbg"
+               fi
+       fi
+
+       # Decide whether to register tests
+       if ! exists "${test}"; then
+               warn "No test suite found"
+       else
+               sets="${sets} tests"
+       fi
+
+       # Decide whether to register kernels and kernels-dbg
+       local kernel=/boot/kernel/kernel
+       if [ -z "${jail}${base}" ]; then
+               if ! kernel=$(/sbin/sysctl -qn kern.bootfile); then
+                       err "Unable to identify the running kernel"
+               fi
+               case ${kernel%/kernel} in
+               *.old)
+                       kernel=${kernel%.old/kernel}/kernel
+                       ;;
+               esac
+               case ${kernel} in
+               /boot/kernel/kernel)
+                       ;;
+               *)
+                       kernel=
+                       ;;
+               esac
+       fi
+       if [ -z "${kernel}" ] || ! exists "${kernel}"; then
+               warn "Non-standard kernel, or no kernel installed"
+               kernel=
+       else
+               sets="${sets} kernels"
+               if exists "${debug}${kernel}.debug"; then
+                       sets="${sets} kernels-dbg"
+               fi
+       fi
+
+       # Decide whether to register src
+       if ! exists "${src}/Makefile.inc1"; then
+               warn "No source tree found"
+       elif exists "${src}/.git"; then
+               warn "Source tree managed by Git "
+       else
+               sets="${sets} src"
+       fi
+
+       # We now have our list of sets
+       sets=$(for set in ${sets}; do echo ${OS}-set-$set; done)
+
+       if ! [ "${quiet}" ] || ! [ "${yes}" ]; then
+               echo "The following package sets will be installed:"
+               echo
+               pkg_rquery "%n %c" ${sets} | \
+                   /usr/bin/sed 's/ (metapackage)//' |
+                   /usr/bin/column -tl2 |
+                   /usr/bin/sed 's/^/    /'
+               echo
+       fi
+
+       # Final checkpoint
+       if ! [ "${auto}" ] && ! confirm "Proceed with conversion?"; then
+               err "Conversion abandoned"
+       fi
+
+       # Download the packages first
+       warn "Downloading packages, this may take a while..."
+       pkg_fetch -d -y ${sets}
+
+       # Now register them
+       pkg_register -y ${sets}
+
+       # Force-upgrade
+       if ! [ "${auto}" ] && confirm "Reinstall base packages?"; then
+               pkg_upgrade -f -y
+       fi
+
+       # Check if BACKUP_LIBRARIES is turned on
+       case $(pkg config BACKUP_LIBRARIES) in
+       no)
+               warn "The recommended BACKUP_LIBRARIES option is not enabled"
+               if ! [ "${auto}" ] &&
+                   confirm "Enable BACKUP_LIBRARIES option?"; then
+                       if ! [ "${dryrun}" ]; then
+                               v /usr/bin/sed -i.bak \
+                                   -e $'1i\\\nBACKUP_LIBRARIES = true;' \
+                                   $(path ${localbase}/etc/pkg.conf)
+                       fi
+                       info "BACKUP_LIBRARIES option enabled"
+               fi
+               ;;
+       esac
+
+       # Done!
+       if ! [ "${dryrun}" ]; then
+               info "Conversion complete"
+       fi
+}
+
+#
+# Convert from pkgbase
+#
+depkgbasify() {
+       local havesrc= pkg= set=
+
+       # Check if we're running pkgbase
+       if ! ispkgbase; then
+               err "The system is not running packaged base"
+       fi
+
+       # Identify and update the repository
+       repo=$(pkg_query %R ${OS}-runtime)
+       info "Using repository ${repo}"
+       pkg_update
+
+       # Check which sets are partially or completely installed
+       info "Examining the system..."
+       for set in ${topsets}; do
+               if pkg_installed ${OS}-set-${set}; then
+                       info "The ${set} set is installed"
+                       continue
+               elif ! pkg_exists %v ${OS}-set-${set}; then
+                       # set does not exist
+                       continue
+               fi
+               local want=$(pkg_alldeps ${OS}-set-${set} | grep -v ${OS}-set)
+               local have=$(pkg_query %n ${want} || true)
+               if [ "${have}" = "${want}" ]; then
+                       info "The ${set} set is complete"
+               elif [ "${have}" ]; then
+                       info "The ${set} set is partially installed"
+                       if ! [ "${auto}" ] &&
+                           confirm "Complete the ${set} set?"; then
+                               pkg_fetch -d -y ${OS}-set-${set}
+                               pkg_install -y ${OS}-set-${set}
+                       fi
+               else
+                       info "The ${set} set is absent"
+               fi
+       done
+
+       # Check if the source tree is installed
+       for pkg in set-src src-sys src; do
+               if pkg_installed ${OS}-${pkg}; then
+                       havesrc="${havesrc} ${OS}-${pkg}"
+               fi
+       done
+       if [ "${havesrc}" ]; then
+               info "The source tree appears to be installed.  You may want" \
+                    "to uninstall it before converting to make room for a" \
+                    "Git clone."
+               if ! [ "${auto}" ] && confirm "Uninstall the source tree?"; then
+                       pkg_remove -fy ${havesrc}
+               fi
+       fi
+
+       # One final upgrade?
+       if ! [ "${auto}" ] && confirm "Upgrade installed base packages?"; then
+               pkg_upgrade -y
+       fi
+
+       # Final checkpoint
+       if ! [ "${auto}" ] && ! confirm "Proceed with conversion?"; then
+               err "Conversion abandoned"
+       fi
+
+       # Unregister all base packages
+       pkg_unregister -f -y $(pkg_query -e '%o ~ base/*' '%n')
+
+       # Done!
+       if ! [ "${dryrun}" ]; then
+               info "Conversion complete"
+       fi
+}
+
+#
+# Print usage message and exit
+#
+usage() {
+       exec >&3
+       echo -n "usage: ${progname} "
+       case ${progname%.sh} in
+       pkgbasify)
+               echo "[-hnqtv] [-a | -y] [-b basedir] [-j jail] [-r repo]"
+               ;;
+       depkgbasify)
+               echo "[-hnqv] [-a | -y] [-b basedir] [-j jail]"
+               ;;
+       *)
+               echo "-h"
+               ;;
+       esac
+       exit 1
+}
+
+#
+# Entry point
+#
+main() {
+       local opt
+
+       # Check program name and set defaults
+       case ${progname%.sh} in
+       pkgbasify)
+               repo=${BASEREPO}
+               ;;
+       depkgbasify)
+               ;;
+       *)
+               usage
+               ;;
+       esac
+
+       # Parse options
+       while getopts "ab:hj:nr:qtvy" opt; do
+               case ${opt} in
+               a)
+                       auto=true
+                       ;;
+               b)
+                       base="${OPTARG}"
+                       ;;
+               h)
+                       usage
+                       ;;
+               j)
+                       jail="${OPTARG}"
+                       ;;
+               n)
+                       dryrun=true
+                       ;;
+               r)
+                       repo="${OPTARG}"
+                       ;;
+               q)
+                       quiet=true
+                       ;;
+               t)
+                       trim=true
+                       ;;
+               v)
+                       verbose=true
+                       ;;
+               y)
+                       yes=true
+                       ;;
+               *)
+                       usage
+                       ;;
+               esac
+       done
+       shift $((OPTIND - 1))
+
+       # Check for conflicting options
+       if [ "${auto}" ] && [ "${yes}" ]; then
+               usage
+       fi
+       case ${progname%.sh} in
+       depkgbasify)
+               if [ "${repo}" ] || [ "${trim}" ]; then
+                       usage
+               fi
+               ;;
+       esac
+
+       # Bootstrap pkg if necessary
+       pkg_bootstrap
+
+       # Check the pkg version
+       check_pkg
+
+       # Do what we were asked to do
+       case ${progname%.sh} in
+       pkgbasify)
+               pkgbasify "$@"
+               ;;
+       depkgbasify)
+               depkgbasify "$@"
+               ;;
+       esac
+}
+
+main "$@"

Reply via email to