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 ["optional"] = "Optional software (excluding compilers)", 83 ["base"] = "The complete base system (includes devel and optional)", 84 ["src"] = "System source tree", 85 ["tests"] = "Test suite", 86 ["lib32"] = "32-bit compatibility libraries", 87 ["debug"] = "Debug symbols for the selected components", 88 } 89 90 -- These defaults match what the non-pkgbase installer selects 91 -- by default. 92 local defaults = { 93 ["base"] = "on", 94 ["kernel-dbg"] = "on", 95 } 96 -- Enable compat sets by default. 97 for compat in all_libcompats:gmatch("%S+") do 98 defaults["lib" .. compat] = "on" 99 end 100 101 -- Sorting the components is necessary to ensure that the ordering is 102 -- consistent in the UI. 103 local sorted_components = {} 104 for component, _ in pairs(components) do 105 -- Decide which sets we want to offer to the user: 106 -- 107 -- "minimal" is not offered since it's always included, as is 108 -- "pkg" if it's present. 109 -- 110 -- "-dbg" sets are never offered, because those are handled 111 -- via the "debug" component. 112 -- 113 -- "kernels" is never offered because we only want one kernel, 114 -- which is handled separately. 115 -- 116 -- Sets whose name ends in "-jail" are intended for jails, and 117 -- are only offered if no_kernel is set. 118 if component ~= "pkg" and 119 not component:match("^minimal") and 120 not (component:match("%-dbg$") and component ~= "kernel-dbg") and 121 not (component == "kernels") and 122 not (not options.no_kernel and component:match("%-jail$")) then 123 table.insert(sorted_components, component) 124 end 125 end 126 table.sort(sorted_components) 127 128 local checklist_items = {} 129 for _, component in ipairs(sorted_components) do 130 if component ~= "kernel" and not 131 (component == "kernel-dbg" and options.no_kernel) then 132 local description = descriptions[component] or "" 133 local default = defaults[component] or "off" 134 table.insert(checklist_items, component) 135 table.insert(checklist_items, description) 136 table.insert(checklist_items, default) 137 end 138 end 139 140 local bsddialog_args = { 141 "--backtitle", "FreeBSD Installer", 142 "--title", "Select System Components", 143 "--nocancel", 144 "--disable-esc", 145 "--separate-output", 146 "--checklist", 147 "A minimal set of packages suitable for a multi-user system ".. 148 "is always installed. Select additional packages you wish ".. 149 "to install:", 150 "0", "0", "0", -- autosize 151 } 152 append_list(bsddialog_args, checklist_items) 153 154 local exit_code, output = bsddialog(bsddialog_args) 155 -- This should only be possible if bsddialog is killed by a signal 156 -- or buggy, we disable the cancel option and esc key. 157 -- If this does happen, there's not much we can do except exit with a 158 -- hopefully useful stack trace. 159 assert(exit_code == 0) 160 161 -- Always install the minimal set, since it's required for the system 162 -- to work. The base set depends on minimal, but it's fine to install 163 -- both, and this way the user can remove the base set without pkg 164 -- autoremove then trying to remove minimal. 165 local selected = {"minimal"} 166 167 -- If pkg is available, always install it so the user can manage the 168 -- installed system. This is optional, because a repository built 169 -- from src alone won't have a pkg package. 170 if components["pkg"] then 171 table.insert(selected, "pkg") 172 end 173 174 if not options.no_kernel then 175 table.insert(selected, "kernel") 176 end 177 178 for component in output:gmatch("[^\n]+") do 179 table.insert(selected, component) 180 end 181 182 return selected 183end 184 185-- Returns a list of pkgbase packages selected by the user 186local function select_packages(pkg, options) 187 -- These are the components which aren't generated automatically from 188 -- package sets. 189 local components = { 190 ["kernel"] = {}, 191 ["kernel-dbg"] = {}, 192 ["debug"] = {}, 193 } 194 195 -- Note: if you update this list, you must also update the list in 196 -- release/scripts/pkgbase-stage.lua. 197 local kernel_packages = { 198 -- Most architectures use this 199 ["FreeBSD-kernel-generic"] = true, 200 -- PowerPC uses either of these, depending on platform 201 ["FreeBSD-kernel-generic64"] = true, 202 ["FreeBSD-kernel-generic64le"] = true, 203 } 204 205 local rquery = capture(pkg .. "rquery -U -r FreeBSD-base %n") 206 for package in rquery:gmatch("[^\n]+") do 207 local setname = package:match("^FreeBSD%-set%-(.+)$") 208 209 if setname then 210 components[setname] = components[setname] or {} 211 table.insert(components[setname], package) 212 elseif kernel_packages[package] then 213 table.insert(components["kernel"], package) 214 elseif kernel_packages[package:match("(.*)%-dbg$")] then 215 table.insert(components["kernel-dbg"], package) 216 elseif package == "pkg" then 217 components["pkg"] = components["pkg"] or {} 218 table.insert(components["pkg"], package) 219 end 220 end 221 222 -- Assert that both a kernel and the "minimal" set are available, since 223 -- those are both required to install a functional system. Don't worry 224 -- if other sets are missing (e.g. base or src), which might happen 225 -- when using custom install media. 226 assert(#components["kernel"] == 1) 227 assert(#components["minimal"] == 1) 228 229 -- Prompt the user for what to install. 230 local selected = select_components(components, options) 231 232 -- Determine if the "debug" component was selected. 233 local debug = false 234 for _, component in ipairs(selected) do 235 if component == "debug" then 236 debug = true 237 break 238 end 239 end 240 241 local packages = {} 242 for _, component in ipairs(selected) do 243 local pkglist = components[component] 244 append_list(packages, pkglist) 245 246 -- If the debug component was selected, install the -dbg 247 -- package for each set. We have to check if the dbg set 248 -- actually exists, because some sets (src, tests) don't 249 -- have a -dbg subpackage. 250 for _, c in ipairs(pkglist) do 251 local setname = c:match("^FreeBSD%-set%-(.*)$") 252 if debug and setname then 253 local dbgset = setname.."-dbg" 254 if components[dbgset] then 255 append_list(packages, components[dbgset]) 256 end 257 end 258 end 259 end 260 261 return packages 262end 263 264local function parse_options() 265 local options = {} 266 for _, a in ipairs(arg) do 267 if a == "--no-kernel" then 268 options.no_kernel = true 269 else 270 io.stderr:write("Error: unknown option " .. a .. "\n") 271 os.exit(1) 272 end 273 end 274 return options 275end 276 277-- Fetch and install pkgbase packages to BSDINSTALL_CHROOT. 278-- Respect BSDINSTALL_PKG_REPOS_DIR if set, otherwise use pkg.freebsd.org. 279local function pkgbase() 280 local options = parse_options() 281 282 -- TODO Support fully offline pkgbase installation by taking a new enough 283 -- version of pkg.pkg as input. 284 if not os.execute("pkg -N > /dev/null 2>&1") then 285 print("Bootstrapping pkg on the host system") 286 assert(os.execute("pkg bootstrap -y")) 287 end 288 289 local chroot = assert(os.getenv("BSDINSTALL_CHROOT")) 290 assert(os.execute("mkdir -p " .. chroot)) 291 292 -- Always install the default FreeBSD-base.conf file to the chroot, even 293 -- if we don't actually fetch the packages from the repository specified 294 -- there (e.g. because we are performing an offline installation). 295 local chroot_repos_dir = chroot .. "/usr/local/etc/pkg/repos/" 296 assert(os.execute("mkdir -p " .. chroot_repos_dir)) 297 assert(os.execute("cp /usr/share/bsdinstall/FreeBSD-base.conf " .. 298 chroot_repos_dir)) 299 300 local repos_dir = os.getenv("BSDINSTALL_PKG_REPOS_DIR") 301 if not repos_dir then 302 repos_dir = chroot_repos_dir 303 -- Since pkg always interprets fingerprints paths as relative to 304 -- the --rootdir we must copy the key from the host. 305 assert(os.execute("mkdir -p " .. chroot .. "/usr/share/keys")) 306 assert(os.execute("cp -R /usr/share/keys/pkg " .. chroot .. "/usr/share/keys/")) 307 end 308 309 -- We must use --repo-conf-dir rather than -o REPOS_DIR here as the latter 310 -- is interpreted relative to the --rootdir. BSDINSTALL_PKG_REPOS_DIR must 311 -- be allowed to point to a path outside the chroot. 312 local pkg = "pkg --rootdir " .. chroot .. 313 " --repo-conf-dir " .. repos_dir .. " -o IGNORE_OSVERSION=yes " 314 315 while not os.execute(pkg .. "update") do 316 if not prompt_yn("Updating repositories failed, try again?") then 317 os.exit(1) 318 end 319 end 320 321 local packages = table.concat(select_packages(pkg, options), " ") 322 323 while not os.execute(pkg .. "install -U -F -y -r FreeBSD-base " .. packages) do 324 if not prompt_yn("Fetching packages failed, try again?") then 325 os.exit(1) 326 end 327 end 328 329 if not os.execute(pkg .. "install -U -y -r FreeBSD-base " .. packages) then 330 os.exit(1) 331 end 332end 333 334pkgbase() 335