1#!/usr/libexec/flua 2 3-- SPDX-License-Identifier: BSD-2-Clause 4-- 5-- Copyright(c) 2025 The FreeBSD Foundation. 6-- 7-- This software was developed by Isaac Freund <ifreund@freebsdfoundation.org> 8-- under sponsorship from the FreeBSD Foundation. 9 10local sys_wait = require("posix.sys.wait") 11local unistd = require("posix.unistd") 12 13local all_libcompats <const> = "%%_ALL_libcompats%%" 14 15-- Run a command using the OS shell and capture the stdout 16-- Strips exactly one trailing newline if present, does not strip any other whitespace. 17-- Asserts that the command exits cleanly 18local function capture(command) 19 local p = io.popen(command) 20 local output = p:read("*a") 21 assert(p:close()) 22 -- Strip exactly one trailing newline from the output, if there is one 23 return output:match("(.-)\n$") or output 24end 25 26local function append_list(list, other) 27 for _, item in ipairs(other) do 28 table.insert(list, item) 29 end 30end 31 32-- Read from the given fd until EOF 33-- Returns all the data read as a single string 34local function read_all(fd) 35 local ret = "" 36 repeat 37 local buffer = assert(unistd.read(fd, 1024)) 38 ret = ret .. buffer 39 until buffer == "" 40 return ret 41end 42 43-- Run bsddialog with the given argument list 44-- Returns the exit code and stderr output of bsddialog 45local function bsddialog(args) 46 local r, w = assert(unistd.pipe()) 47 48 local pid = assert(unistd.fork()) 49 if pid == 0 then 50 assert(unistd.close(r)) 51 assert(unistd.dup2(w, 2)) 52 assert(unistd.execp("bsddialog", args)) 53 unistd._exit() 54 end 55 assert(unistd.close(w)) 56 57 local output = read_all(r) 58 assert(unistd.close(r)) 59 60 local _, _, exit_code = assert(sys_wait.wait(pid)) 61 return exit_code, output 62end 63 64-- Prompts the user for a yes/no answer to the given question using bsddialog 65-- Returns true if the user answers yes and false if the user answers no. 66local function prompt_yn(question) 67 local exit_code = bsddialog({ 68 "--yesno", 69 "--disable-esc", 70 question, 71 0, 0, -- autosize 72 }) 73 return exit_code == 0 74end 75 76-- Creates a dialog for component selection mirroring the 77-- traditional tarball component selection dialog. 78local function select_components(components, options) 79 local descriptions = { 80 ["kernel-dbg"] = "Debug symbols for the kernel", 81 ["devel"] = "C/C++ compilers and related utilities", 82 ["base"] = "The complete base system (includes devel)", 83 ["src"] = "System source tree", 84 ["tests"] = "Test suite", 85 ["lib32"] = "32-bit compatibility libraries", 86 ["debug"] = "Debug symbols for the selected components", 87 } 88 89 -- These defaults match what the non-pkgbase installer selects 90 -- by default. 91 local defaults = { 92 ["base"] = "on", 93 ["kernel-dbg"] = "on", 94 } 95 -- Enable compat sets by default. 96 for compat in all_libcompats:gmatch("%S+") do 97 defaults["lib" .. compat] = "on" 98 end 99 100 -- Sorting the components is necessary to ensure that the ordering is 101 -- consistent in the UI. 102 local sorted_components = {} 103 for component, _ in pairs(components) do 104 -- Decide which sets we want to offer to the user: 105 -- 106 -- "minimal" is not offered since it's always included. 107 -- 108 -- "-dbg" sets are never offered, because those are handled 109 -- via the "debug" component. 110 -- 111 -- "kernels" is never offered because we only want one kernel, 112 -- which is handled separately. 113 -- 114 -- Sets whose name ends in "-jail" are intended for jails, and 115 -- are only offered if no_kernel is set. 116 if not component:match("^minimal") and 117 not component:match("%-dbg$") and 118 not (component == "kernels") and 119 not (not options.no_kernel and component:match("%-jail$")) then 120 table.insert(sorted_components, component) 121 end 122 end 123 table.sort(sorted_components) 124 125 local checklist_items = {} 126 for _, component in ipairs(sorted_components) do 127 if component ~= "kernel" and not 128 (component == "kernel-dbg" and options.no_kernel) then 129 local description = descriptions[component] or "" 130 local default = defaults[component] or "off" 131 table.insert(checklist_items, component) 132 table.insert(checklist_items, description) 133 table.insert(checklist_items, default) 134 end 135 end 136 137 local bsddialog_args = { 138 "--backtitle", "FreeBSD Installer", 139 "--title", "Select System Components", 140 "--nocancel", 141 "--disable-esc", 142 "--separate-output", 143 "--checklist", 144 "A minimal set of packages suitable for a multi-user system ".. 145 "is always installed. Select additional packages you wish ".. 146 "to install:", 147 "0", "0", "0", -- autosize 148 } 149 append_list(bsddialog_args, checklist_items) 150 151 local exit_code, output = bsddialog(bsddialog_args) 152 -- This should only be possible if bsddialog is killed by a signal 153 -- or buggy, we disable the cancel option and esc key. 154 -- If this does happen, there's not much we can do except exit with a 155 -- hopefully useful stack trace. 156 assert(exit_code == 0) 157 158 -- Always install the minimal set, since it's required for the system 159 -- to work. The base set depends on minimal, but it's fine to install 160 -- both, and this way the user can remove the base set without pkg 161 -- autoremove then trying to remove minimal. 162 local selected = {"minimal"} 163 164 if not options.no_kernel then 165 table.insert(selected, "kernel") 166 end 167 168 for component in output:gmatch("[^\n]+") do 169 table.insert(selected, component) 170 end 171 172 return selected 173end 174 175-- Returns a list of pkgbase packages selected by the user 176local function select_packages(pkg, options) 177 -- These are the components which aren't generated automatically from 178 -- package sets. 179 local components = { 180 ["kernel"] = {}, 181 ["kernel-dbg"] = {}, 182 ["debug"] = {}, 183 } 184 185 local rquery = capture(pkg .. "rquery -U -r FreeBSD-base %n") 186 for package in rquery:gmatch("[^\n]+") do 187 local setname = package:match("^FreeBSD%-set%-(.+)$") 188 189 if setname then 190 components[setname] = components[setname] or {} 191 table.insert(components[setname], package) 192 elseif package:match("^FreeBSD%-kernel%-.*") and 193 package ~= "FreeBSD-kernel-man" 194 then 195 -- Kernels other than FreeBSD-kernel-generic are ignored 196 if package == "FreeBSD-kernel-generic" then 197 table.insert(components["kernel"], package) 198 elseif package == "FreeBSD-kernel-generic-dbg" then 199 table.insert(components["kernel-dbg"], package) 200 end 201 end 202 end 203 204 -- Assert that both a kernel and the "minimal" set are available, since 205 -- those are both required to install a functional system. Don't worry 206 -- if other sets are missing (e.g. base or src), which might happen 207 -- when using custom install media. 208 assert(#components["kernel"] == 1) 209 assert(#components["minimal"] == 1) 210 211 -- Prompt the user for what to install. 212 local selected = select_components(components, options) 213 214 -- Determine if the "debug" component was selected. 215 local debug = false 216 for _, component in ipairs(selected) do 217 if component == "debug" then 218 debug = true 219 break 220 end 221 end 222 223 local packages = {} 224 for _, component in ipairs(selected) do 225 local pkglist = components[component] 226 append_list(packages, pkglist) 227 228 -- If the debug component was selected, install the -dbg 229 -- package for each set. We have to check if the dbg set 230 -- actually exists, because some sets (src, tests) don't 231 -- have a -dbg subpackage. 232 for _, c in ipairs(pkglist) do 233 local setname = c:match("^FreeBSD%-set%-(.*)$") 234 if debug and setname then 235 local dbgset = setname.."-dbg" 236 if components[dbgset] then 237 append_list(packages, components[dbgset]) 238 end 239 end 240 end 241 end 242 243 return packages 244end 245 246local function parse_options() 247 local options = {} 248 for _, a in ipairs(arg) do 249 if a == "--no-kernel" then 250 options.no_kernel = true 251 else 252 io.stderr:write("Error: unknown option " .. a .. "\n") 253 os.exit(1) 254 end 255 end 256 return options 257end 258 259-- Fetch and install pkgbase packages to BSDINSTALL_CHROOT. 260-- Respect BSDINSTALL_PKG_REPOS_DIR if set, otherwise use pkg.freebsd.org. 261local function pkgbase() 262 local options = parse_options() 263 264 -- TODO Support fully offline pkgbase installation by taking a new enough 265 -- version of pkg.pkg as input. 266 if not os.execute("pkg -N > /dev/null 2>&1") then 267 print("Bootstrapping pkg on the host system") 268 assert(os.execute("pkg bootstrap -y")) 269 end 270 271 local chroot = assert(os.getenv("BSDINSTALL_CHROOT")) 272 assert(os.execute("mkdir -p " .. chroot)) 273 274 -- Always install the default FreeBSD-base.conf file to the chroot, even 275 -- if we don't actually fetch the packages from the repository specified 276 -- there (e.g. because we are performing an offline installation). 277 local chroot_repos_dir = chroot .. "/usr/local/etc/pkg/repos/" 278 assert(os.execute("mkdir -p " .. chroot_repos_dir)) 279 assert(os.execute("cp /usr/share/bsdinstall/FreeBSD-base.conf " .. 280 chroot_repos_dir)) 281 282 local repos_dir = os.getenv("BSDINSTALL_PKG_REPOS_DIR") 283 if not repos_dir then 284 repos_dir = chroot_repos_dir 285 -- Since pkg always interprets fingerprints paths as relative to 286 -- the --rootdir we must copy the key from the host. 287 assert(os.execute("mkdir -p " .. chroot .. "/usr/share/keys")) 288 assert(os.execute("cp -R /usr/share/keys/pkg " .. chroot .. "/usr/share/keys/")) 289 end 290 291 -- We must use --repo-conf-dir rather than -o REPOS_DIR here as the latter 292 -- is interpreted relative to the --rootdir. BSDINSTALL_PKG_REPOS_DIR must 293 -- be allowed to point to a path outside the chroot. 294 local pkg = "pkg --rootdir " .. chroot .. 295 " --repo-conf-dir " .. repos_dir .. " -o IGNORE_OSVERSION=yes " 296 297 while not os.execute(pkg .. "update") do 298 if not prompt_yn("Updating repositories failed, try again?") then 299 os.exit(1) 300 end 301 end 302 303 local packages = table.concat(select_packages(pkg, options), " ") 304 305 while not os.execute(pkg .. "install -U -F -y -r FreeBSD-base " .. packages) do 306 if not prompt_yn("Fetching packages failed, try again?") then 307 os.exit(1) 308 end 309 end 310 311 if not os.execute(pkg .. "install -U -y -r FreeBSD-base " .. packages) then 312 os.exit(1) 313 end 314end 315 316pkgbase() 317