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 = "Kernel debug info", 81 base_dbg = "Base system debug info", 82 src = "System source tree", 83 tests = "Test suite", 84 lib32 = "32-bit compatibility libraries", 85 lib32_dbg = "32-bit compatibility libraries debug info", 86 } 87 local defaults = { 88 kernel_dbg = "on", 89 base_dbg = "off", 90 src = "off", 91 tests = "off", 92 lib32 = "on", 93 lib32_dbg = "off", 94 } 95 96 -- Sorting the components is necessary to ensure that the ordering is 97 -- consistent in the UI. 98 local sorted_components = {} 99 for component, _ in pairs(components) do 100 table.insert(sorted_components, component) 101 end 102 table.sort(sorted_components) 103 104 local checklist_items = {} 105 for _, component in ipairs(sorted_components) do 106 if component ~= "base" and component ~= "kernel" and 107 not (component == "kernel_dbg" and options.no_kernel) and 108 #components[component] > 0 then 109 local description = descriptions[component] or "''" 110 local default = defaults[component] or "off" 111 table.insert(checklist_items, component) 112 table.insert(checklist_items, description) 113 table.insert(checklist_items, default) 114 end 115 end 116 117 local bsddialog_args = { 118 "--backtitle", "FreeBSD Installer", 119 "--title", "Select System Components", 120 "--nocancel", 121 "--disable-esc", 122 "--separate-output", 123 "--checklist", "Choose optional system components to install:", 124 "0", "0", "0", -- autosize 125 } 126 append_list(bsddialog_args, checklist_items) 127 128 local exit_code, output = bsddialog(bsddialog_args) 129 -- This should only be possible if bsddialog is killed by a signal 130 -- or buggy, we disable the cancel option and esc key. 131 -- If this does happen, there's not much we can do except exit with a 132 -- hopefully useful stack trace. 133 assert(exit_code == 0) 134 135 local selected = {"base"} 136 if not options.no_kernel then 137 table.insert(selected, "kernel") 138 end 139 for component in output:gmatch("[^\n]+") do 140 table.insert(selected, component) 141 end 142 143 return selected 144end 145 146-- Returns a list of pkgbase packages selected by the user 147local function select_packages(pkg, options) 148 local components = { 149 kernel = {}, 150 kernel_dbg = {}, 151 base = {}, 152 base_dbg = {}, 153 src = {}, 154 tests = {}, 155 } 156 157 for compat in all_libcompats:gmatch("%S+") do 158 components["lib" .. compat] = {} 159 components["lib" .. compat .. "_dbg"] = {} 160 end 161 162 local rquery = capture(pkg .. "rquery -U -r FreeBSD-base %n") 163 for package in rquery:gmatch("[^\n]+") do 164 if package == "FreeBSD-src" or package:match("^FreeBSD%-src%-.*") then 165 table.insert(components["src"], package) 166 elseif package == "FreeBSD-tests" or package:match("^FreeBSD%-tests%-.*") then 167 table.insert(components["tests"], package) 168 elseif package:match("^FreeBSD%-kernel%-.*") then 169 -- Kernels other than FreeBSD-kernel-generic are ignored 170 if package == "FreeBSD-kernel-generic" then 171 table.insert(components["kernel"], package) 172 elseif package == "FreeBSD-kernel-generic-dbg" then 173 table.insert(components["kernel_dbg"], package) 174 end 175 elseif package:match(".*%-dbg$") then 176 table.insert(components["base_dbg"], package) 177 else 178 local found = false 179 for compat in all_libcompats:gmatch("%S+") do 180 if package:match(".*%-dbg%-lib" .. compat .. "$") then 181 table.insert(components["lib" .. compat .. "_dbg"], package) 182 found = true 183 break 184 elseif package:match(".*%-lib" .. compat .. "$") then 185 table.insert(components["lib" .. compat], package) 186 found = true 187 break 188 end 189 end 190 if not found then 191 table.insert(components["base"], package) 192 end 193 end 194 end 195 -- Don't assert the existence of dbg, tests, and src packages here. If using 196 -- a custom local repository with BSDINSTALL_PKG_REPOS_DIR we shouldn't 197 -- require it to have all packages. 198 assert(#components["kernel"] == 1) 199 assert(#components["base"] > 0) 200 201 local selected = {} 202 for _, component in ipairs(select_components(components, options)) do 203 append_list(selected, components[component]) 204 end 205 206 return selected 207end 208 209local function parse_options() 210 local options = {} 211 for _, a in ipairs(arg) do 212 if a == "--no-kernel" then 213 options.no_kernel = true 214 else 215 io.stderr:write("Error: unknown option " .. a .. "\n") 216 os.exit(1) 217 end 218 end 219 return options 220end 221 222-- Fetch and install pkgbase packages to BSDINSTALL_CHROOT. 223-- Respect BSDINSTALL_PKG_REPOS_DIR if set, otherwise use pkg.freebsd.org. 224local function pkgbase() 225 local options = parse_options() 226 227 -- TODO Support fully offline pkgbase installation by taking a new enough 228 -- version of pkg.pkg as input. 229 if not os.execute("pkg -N > /dev/null 2>&1") then 230 print("Bootstrapping pkg on the host system") 231 assert(os.execute("pkg bootstrap -y")) 232 end 233 234 local chroot = assert(os.getenv("BSDINSTALL_CHROOT")) 235 assert(os.execute("mkdir -p " .. chroot)) 236 237 local repos_dir = os.getenv("BSDINSTALL_PKG_REPOS_DIR") 238 if not repos_dir then 239 repos_dir = chroot .. "/usr/local/etc/pkg/repos/" 240 assert(os.execute("mkdir -p " .. repos_dir)) 241 assert(os.execute("cp /usr/share/bsdinstall/FreeBSD-base.conf " .. repos_dir)) 242 243 -- Since pkg always interprets fingerprints paths as relative to 244 -- the --rootdir we must copy the key from the host. 245 assert(os.execute("mkdir -p " .. chroot .. "/usr/share/keys")) 246 assert(os.execute("cp -R /usr/share/keys/pkg " .. chroot .. "/usr/share/keys/")) 247 end 248 249 -- We must use --repo-conf-dir rather than -o REPOS_DIR here as the latter 250 -- is interpreted relative to the --rootdir. BSDINSTALL_PKG_REPOS_DIR must 251 -- be allowed to point to a path outside the chroot. 252 local pkg = "pkg --rootdir " .. chroot .. 253 " --repo-conf-dir " .. repos_dir .. " -o IGNORE_OSVERSION=yes " 254 255 while not os.execute(pkg .. "update") do 256 if not prompt_yn("Updating repositories failed, try again?") then 257 os.exit(1) 258 end 259 end 260 261 local packages = table.concat(select_packages(pkg, options), " ") 262 263 while not os.execute(pkg .. "install -U -F -y -r FreeBSD-base " .. packages) do 264 if not prompt_yn("Fetching packages failed, try again?") then 265 os.exit(1) 266 end 267 end 268 269 if not os.execute(pkg .. "install -U -y -r FreeBSD-base " .. packages) then 270 os.exit(1) 271 end 272end 273 274pkgbase() 275