git: e6467516f584 - main - pkgbasify: Add new utility

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Mon, 29 Jun 2026 15:52:21 UTC
The branch main has been updated by des:

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

commit e6467516f584505ac29fd370966abd530a7b5454
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2026-06-29 00:41:11 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
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 des@FreeBSD.org .
+.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 "$@"