diff --git a/usr.sbin/bsdinstall/bsdinstall.8 b/usr.sbin/bsdinstall/bsdinstall.8 --- a/usr.sbin/bsdinstall/bsdinstall.8 +++ b/usr.sbin/bsdinstall/bsdinstall.8 @@ -244,6 +244,14 @@ .Ev DISTRIBUTIONS into .Ev BSDINSTALL_CHROOT . +.It Cm pkgbase +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. .It Cm firmware executes .Xr fwget 8 @@ -324,6 +332,17 @@ .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 --- a/usr.sbin/bsdinstall/scripts/Makefile +++ b/usr.sbin/bsdinstall/scripts/Makefile @@ -17,6 +17,7 @@ netconfig \ netconfig_ipv4 \ netconfig_ipv6 \ + pkgbase \ rootpass \ script \ services \ diff --git a/usr.sbin/bsdinstall/scripts/pkgbase b/usr.sbin/bsdinstall/scripts/pkgbase new file mode 100755 --- /dev/null +++ b/usr.sbin/bsdinstall/scripts/pkgbase @@ -0,0 +1,191 @@ +#!/usr/libexec/flua + +-- SPDX-License-Identifier: BSD-2-Clause +-- +-- Copyright(c) 2025 The FreeBSD Foundation. +-- +-- This software was developed by Isaac Freund +-- under sponsorship from the FreeBSD Foundation. + +local function err(msg) + io.stderr:write("Error: " .. msg .. "\n") +end + +local function fatal(msg) + err(msg) + os.exit(1) +end + +-- 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 + +-- Returns the URL for the pkgbase repository that matches the version +-- reported by freebsd-version(1) +local function base_repo_url() + -- e.g. 15.0-CURRENT, 14.2-STABLE, 14.1-RELEASE, 14.1-RELEASE-p6, + local raw = capture("freebsd-version") + local major, minor, branch = assert(raw:match("(%d+)%.(%d+)%-(%u+)")) + + if math.tointeger(major) < 14 then + fatal("unsupported FreeBSD version: " .. raw) + end + + if branch == "RELEASE" then + return "pkg+https://pkg.FreeBSD.org/${ABI}/base_release_" .. minor + elseif branch == "CURRENT" or branch == "STABLE" then + return "pkg+https://pkg.FreeBSD.org/${ABI}/base_latest" + else + fatal("unsupported FreeBSD version: " .. raw) + end +end + +local function create_base_repo_conf(dir) + assert(os.execute("mkdir -p " .. dir)) + local f = assert(io.open(dir .. "/FreeBSD-base.conf", "w")) + assert(f:write(string.format([[ +FreeBSD-base: { + url: "%s", + mirror_type: "srv", + signature_type: "fingerprints", + fingerprints: "/usr/share/keys/pkg", + enabled: yes +} +]], base_repo_url()))) +end + +local function create_fingerprint(chroot) + local dir = chroot .. "/usr/share/keys/pkg/trusted/" + assert(os.execute("mkdir -p " .. dir)) + local f = assert(io.open(dir .. "/pkg.freebsd.org.2013102301", "w")) + assert(f:write([[ +function: "sha256" +fingerprint: "b0170035af3acc5f3f3ae1859dc717101b4e6c1d0a794ad554928ca0cbb2f438" +]])) +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) + local kernel = {} + local kernel_dbg = {} + local base = {} + local base_dbg = {} + local lib32 = {} + local lib32_dbg = {} + local src = {} + local tests = {} + + 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(src, package) + elseif package == "FreeBSD-tests" or package:match("FreeBSD%-tests%-.*") then + table.insert(tests, package) + elseif package:match("FreeBSD%-kernel%-.*") then + -- Kernels other than FreeBSD-kernel-generic are ignored + if package == "FreeBSD-kernel-generic" then + table.insert(kernel, package) + elseif package == "FreeBSD-kernel-generic-dbg" then + table.insert(kernel_dbg, package) + end + elseif package:match(".*%-dbg%-lib32") then + table.insert(lib32_dbg, package) + elseif package:match(".*%-lib32") then + table.insert(lib32, package) + elseif package:match(".*%-dbg") then + table.insert(base_dbg, package) + else + table.insert(base, package) + end + end + assert(#kernel == 1) + assert(#kernel_dbg == 1) + assert(#base > 0) + assert(#base_dbg > 0) + assert(#lib32 > 0) + assert(#lib32_dbg > 0) + assert(#src > 0) + assert(#tests > 0) + + local selected = {} + append_list(selected, kernel) + append_list(selected, base) + + return selected +end + +-- Fetch and install pkgbase packages to BSDINSTALL_CHROOT. +-- Respect BSDINSTALL_PKG_REPOS_DIR if set, otherwise use pkg.freebsd.org. +local function pkgbase() + -- 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/" + create_base_repo_conf(repos_dir) + create_fingerprint(chroot) + 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), " ") + + 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 os.execute(pkg .. "install -U -y -r FreeBSD-base " .. packages) then + os.exit(0) + else + os.exit(1) + end +end + +pkgbase()