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