git: ee9cfd727578 - main - bsdinstall: add pkgbase target

From: Ed Maste <emaste_at_FreeBSD.org>
Date: Mon, 28 Apr 2025 16:14:07 UTC
The branch main has been updated by emaste:

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

commit ee9cfd7275784b61b030beb242df588bb35b21f0
Author:     Isaac Freund <ifreund@freebsdfoundation.org>
AuthorDate: 2025-04-01 23:25:49 +0000
Commit:     Ed Maste <emaste@FreeBSD.org>
CommitDate: 2025-04-28 16:12:27 +0000

    bsdinstall: add pkgbase target
    
    Reviewed by:    ziaee (manpages), kevans (lua), emaste
    Relnotes:       Yes
    Sponsored by:   The FreeBSD Foundation
    Differential Revision: https://reviews.freebsd.org/D49822
---
 usr.sbin/bsdinstall/FreeBSD-base.conf.in |   7 ++
 usr.sbin/bsdinstall/Makefile             |  17 +++
 usr.sbin/bsdinstall/bsdinstall.8         |  22 ++++
 usr.sbin/bsdinstall/scripts/Makefile     |   6 ++
 usr.sbin/bsdinstall/scripts/pkgbase.in   | 172 +++++++++++++++++++++++++++++++
 5 files changed, 224 insertions(+)

diff --git a/usr.sbin/bsdinstall/FreeBSD-base.conf.in b/usr.sbin/bsdinstall/FreeBSD-base.conf.in
new file mode 100644
index 000000000000..792c290facdf
--- /dev/null
+++ b/usr.sbin/bsdinstall/FreeBSD-base.conf.in
@@ -0,0 +1,7 @@
+FreeBSD-base: {
+  url: "pkg+https://pkg.FreeBSD.org/${ABI}/%%SUBURL%%",
+  mirror_type: "srv",
+  signature_type: "fingerprints",
+  fingerprints: "/usr/share/keys/pkg",
+  enabled: yes
+}
diff --git a/usr.sbin/bsdinstall/Makefile b/usr.sbin/bsdinstall/Makefile
index c9ba7cb52cd6..75db149b814b 100644
--- a/usr.sbin/bsdinstall/Makefile
+++ b/usr.sbin/bsdinstall/Makefile
@@ -12,4 +12,21 @@ SCRIPTSDIR_startbsdinstall=	${LIBEXECDIR}/bsdinstall
 
 UPDATE_DEPENDFILE= no
 
+FILESDIR=	${SHAREDIR}/bsdinstall
+FILES=		FreeBSD-base.conf
+
+_BRANCH!=	${MAKE} -C ${SRCTOP}/release -V BRANCH
+BRANCH?=	${_BRANCH}
+_REVISION!=	${MAKE} -C ${SRCTOP}/release -V REVISION
+REVISION?=	${_REVISION}
+
+.if ${BRANCH} == CURRENT || ${BRANCH} == STABLE
+SUBURL=		base_latest
+.else
+SUBURL=		base_release_${REVISION:C/[0-9]+\.//}
+.endif
+
+FreeBSD-base.conf: FreeBSD-base.conf.in
+	sed "s|%%SUBURL%%|${SUBURL}|" < ${.ALLSRC} > ${.TARGET}
+
 .include <bsd.prog.mk>
diff --git a/usr.sbin/bsdinstall/bsdinstall.8 b/usr.sbin/bsdinstall/bsdinstall.8
index 50c8948a7989..8fadacab9189 100644
--- a/usr.sbin/bsdinstall/bsdinstall.8
+++ b/usr.sbin/bsdinstall/bsdinstall.8
@@ -244,6 +244,17 @@ Extracts the distributions listed in
 .Ev DISTRIBUTIONS
 into
 .Ev BSDINSTALL_CHROOT .
+.It Cm pkgbase Op Fl --no-kernel
+Fetch and install base system packages to
+.Ev BSDINSTALL_CHROOT .
+Packages are fetched according to repository configuration in
+.Ev BSDINSTALL_PKG_REPOS_DIR
+if set, or
+.Lk pkg.freebsd.org
+otherwise.
+If the
+.Fl --no-kernel
+option is passed, no kernel is installed.
 .It Cm firmware
 executes
 .Xr fwget 8
@@ -324,6 +335,17 @@ Example:
 .Pa https://download.freebsd.org/ftp/releases/powerpc/powerpc64/13.1-RELEASE/
 or
 .Pa http://ftp-archive.freebsd.org/pub/FreeBSD-Archive/old-releases/amd64/12.2-RELEASE/ .
+.It Ev BSDINSTALL_PKG_REPOS_DIR
+Directory containing
+.Xr pkg 8
+repository configuration files used by the
+.Cm pkgbase
+target.
+See
+.Sx REPOSITORY CONFIGURATION
+in
+.Xr pkg.conf 5 .
+Default: unset
 .It Ev BSDINSTALL_CHROOT
 The directory into which the distribution files should be unpacked and the
 directory at which the root file system of the new system should be mounted.
diff --git a/usr.sbin/bsdinstall/scripts/Makefile b/usr.sbin/bsdinstall/scripts/Makefile
index f3b9f07ed376..4fd59e49d506 100644
--- a/usr.sbin/bsdinstall/scripts/Makefile
+++ b/usr.sbin/bsdinstall/scripts/Makefile
@@ -1,3 +1,5 @@
+.include <bsd.compat.pre.mk>
+
 SCRIPTS=auto \
 	adduser \
 	bootconfig \
@@ -17,6 +19,7 @@ SCRIPTS=auto \
 	netconfig \
 	netconfig_ipv4 \
 	netconfig_ipv6 \
+	pkgbase \
 	rootpass \
 	script \
 	services \
@@ -29,4 +32,7 @@ BINDIR= ${LIBEXECDIR}/bsdinstall
 
 MAN=
 
+pkgbase: pkgbase.in
+	sed "s|%%_ALL_libcompats%%|${_ALL_libcompats}|" < ${.ALLSRC} > ${.TARGET}
+
 .include <bsd.prog.mk>
diff --git a/usr.sbin/bsdinstall/scripts/pkgbase.in b/usr.sbin/bsdinstall/scripts/pkgbase.in
new file mode 100755
index 000000000000..7faeee40647b
--- /dev/null
+++ b/usr.sbin/bsdinstall/scripts/pkgbase.in
@@ -0,0 +1,172 @@
+#!/usr/libexec/flua
+
+-- SPDX-License-Identifier: BSD-2-Clause
+--
+-- Copyright(c) 2025 The FreeBSD Foundation.
+--
+-- This software was developed by Isaac Freund <ifreund@freebsdfoundation.org>
+-- under sponsorship from the FreeBSD Foundation.
+
+local all_libcompats <const> = "%%_ALL_libcompats%%"
+
+-- Run a command using the OS shell and capture the stdout
+-- Strips exactly one trailing newline if present, does not strip any other whitespace.
+-- Asserts that the command exits cleanly
+local function capture(command)
+	local p = io.popen(command)
+	local output = p:read("*a")
+	assert(p:close())
+	-- Strip exactly one trailing newline from the output, if there is one
+	return output:match("(.-)\n$") or output
+end
+
+local function prompt_yn(question)
+	while true do
+		io.write(question .. " (y/n) ")
+		local input = io.read()
+		if input == "y" or input == "Y" then
+			return true
+		elseif input == "n" or input == "N" then
+			return false
+		end
+	end
+end
+
+local function append_list(list, other)
+	for _, item in ipairs(other) do
+		table.insert(list, item)
+	end
+end
+
+-- Returns a list of pkgbase packages equivalent to the default base.txz and kernel.txz
+local function select_packages(pkg, options)
+	local components = {
+		["kernel"] = {},
+		["kernel-dbg"] = {},
+		["base"] = {},
+		["base-dbg"] = {},
+		["src"] = {},
+		["tests"] = {},
+	}
+
+	for compat in all_libcompats:gmatch("%S+") do
+		components["lib" .. compat] = {}
+		components["lib" .. compat .. "-dbg"] = {}
+	end
+
+	local rquery = capture(pkg .. "rquery -U -r FreeBSD-base %n")
+	for package in rquery:gmatch("[^\n]+") do
+		if package == "FreeBSD-src" or package:match("^FreeBSD%-src%-.*") then
+			table.insert(components["src"], package)
+		elseif package == "FreeBSD-tests" or package:match("^FreeBSD%-tests%-.*") then
+			table.insert(components["tests"], package)
+		elseif package:match("^FreeBSD%-kernel%-.*") then
+			-- Kernels other than FreeBSD-kernel-generic are ignored
+			if package == "FreeBSD-kernel-generic" then
+				table.insert(components["kernel"], package)
+			elseif package == "FreeBSD-kernel-generic-dbg" then
+				table.insert(components["kernel-dbg"], package)
+			end
+		elseif package:match(".*%-dbg$") then
+			table.insert(components["base-dbg"], package)
+		else
+			local found = false
+			for compat in all_libcompats:gmatch("%S+") do
+				if package:match(".*%-dbg%-lib" .. compat .. "$") then
+					table.insert(components["lib" .. compat .. "-dbg"], package)
+					found = true
+					break
+				elseif package:match(".*%-lib" .. compat .. "$") then
+					table.insert(components["lib" .. compat], package)
+					found = true
+					break
+				end
+			end
+			if not found then
+				table.insert(components["base"], package)
+			end
+		end
+	end
+	-- Don't assert the existence of dbg, tests, and src packages here. If using
+	-- a custom local repository with BSDINSTALL_PKG_REPOS_DIR we shouldn't
+	-- require it to have all packages.
+	assert(#components["kernel"] == 1)
+	assert(#components["base"] > 0)
+
+	local selected = {}
+	append_list(selected, components["base"])
+	if not options.no_kernel then
+		append_list(selected, components["kernel"])
+	end
+
+	return selected
+end
+
+local function parse_options()
+	local options = {}
+	for _, a in ipairs(arg) do
+		if a == "--no-kernel" then
+			options.no_kernel = true
+		else
+			io.stderr:write("Error: unknown option " .. a .. "\n")
+			os.exit(1)
+		end
+	end
+	return options
+end
+
+-- Fetch and install pkgbase packages to BSDINSTALL_CHROOT.
+-- Respect BSDINSTALL_PKG_REPOS_DIR if set, otherwise use pkg.freebsd.org.
+local function pkgbase()
+	local options = parse_options()
+
+	-- TODO Support fully offline pkgbase installation by taking a new enough
+	-- version of pkg.pkg as input.
+	if not os.execute("pkg -N > /dev/null 2>&1") then
+		print("Bootstrapping pkg on the host system")
+		assert(os.execute("pkg bootstrap -y"))
+	end
+
+	local chroot = assert(os.getenv("BSDINSTALL_CHROOT"))
+	assert(os.execute("mkdir -p " .. chroot))
+
+	local repos_dir = os.getenv("BSDINSTALL_PKG_REPOS_DIR")
+	if not repos_dir then
+		repos_dir = chroot .. "/usr/local/etc/pkg/repos/"
+		assert(os.execute("mkdir -p " .. repos_dir))
+		assert(os.execute("cp /usr/share/bsdinstall/FreeBSD-base.conf " .. repos_dir))
+
+		-- Since pkg always interprets fingerprints paths as relative to
+		-- the --rootdir we must copy the key from the host.
+		assert(os.execute("mkdir -p " .. chroot .. "/usr/share/keys"))
+		assert(os.execute("cp -R /usr/share/keys/pkg " .. chroot .. "/usr/share/keys/"))
+	end
+
+	-- We must use --repo-conf-dir rather than -o REPOS_DIR here as the latter
+	-- is interpreted relative to the --rootdir. BSDINSTALL_PKG_REPOS_DIR must
+	-- be allowed to point to a path outside the chroot.
+	local pkg = "pkg --rootdir " .. chroot ..
+		" --repo-conf-dir " .. repos_dir .. " -o IGNORE_OSVERSION=yes "
+
+	while not os.execute(pkg .. "update") do
+		if not prompt_yn("Updating repositories failed, try again?") then
+			print("Canceled")
+			os.exit(1)
+		end
+	end
+
+	local packages = table.concat(select_packages(pkg, options), " ")
+
+	while not os.execute(pkg .. "install -U -F -y -r FreeBSD-base " .. packages) do
+		if not prompt_yn("Fetching packages failed, try again?") then
+			print("Canceled")
+			os.exit(1)
+		end
+	end
+
+	if not os.execute(pkg .. "install -U -y -r FreeBSD-base " .. packages) then
+		os.exit(1)
+	end
+end
+
+pkgbase()