diff --git a/usr.sbin/bsdinstall/scripts/pkgbase.in b/usr.sbin/bsdinstall/scripts/pkgbase.in index 5299d34fcb71..744e0daac6f8 100755 --- a/usr.sbin/bsdinstall/scripts/pkgbase.in +++ b/usr.sbin/bsdinstall/scripts/pkgbase.in @@ -1,368 +1,368 @@ #!/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 sys_wait = require("posix.sys.wait") local unistd = require("posix.unistd") local all_libcompats = "%%_ALL_libcompats%%" -- 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 append_list(list, other) for _, item in ipairs(other) do table.insert(list, item) end end -- Read from the given fd until EOF -- Returns all the data read as a single string local function read_all(fd) local ret = "" repeat local buffer = assert(unistd.read(fd, 1024)) ret = ret .. buffer until buffer == "" return ret end -- Run bsddialog with the given argument list -- Returns the exit code and stderr output of bsddialog local function bsddialog(args) local r, w = assert(unistd.pipe()) local pid = assert(unistd.fork()) if pid == 0 then assert(unistd.close(r)) assert(unistd.dup2(w, 2)) assert(unistd.execp("bsddialog", args)) unistd._exit() end assert(unistd.close(w)) local output = read_all(r) assert(unistd.close(r)) local _, _, exit_code = assert(sys_wait.wait(pid)) return exit_code, output end -- Prompts the user for a yes/no answer to the given question using bsddialog -- Returns true if the user answers yes and false if the user answers no. local function prompt_yn(question) local exit_code = bsddialog({ "--yesno", "--disable-esc", question, 0, 0, -- autosize }) return exit_code == 0 end -- Creates a dialog for component selection mirroring the -- traditional tarball component selection dialog. local function select_components(components, options) local descriptions = { ["kernel-dbg"] = "Debug symbols for the kernel", ["devel"] = "C/C++ compilers and related utilities", ["optional"] = "Optional software (excluding compilers)", ["optional-jail"] = "Optional software (excluding compilers)", ["base"] = "The complete base system (includes devel and optional)", ["base-jail"] = "The complete base system (includes devel and optional)", ["src"] = "System source tree", ["tests"] = "Test suite", ["lib32"] = "32-bit compatibility libraries", ["debug"] = "Debug symbols for the selected components", } -- These defaults match what the non-pkgbase installer selects -- by default. local defaults = { ["base"] = "on", ["base-jail"] = "on", ["kernel-dbg"] = "on", } -- Enable compat sets by default. for compat in all_libcompats:gmatch("%S+") do defaults["lib" .. compat] = "on" end -- Sorting the components is necessary to ensure that the ordering is -- consistent in the UI. local sorted_components = {} -- Determine which components we want to offer the user. local show_component = function (component) -- "pkg" is always installed if present. if component == "pkg" then return false end -- Don't include individual "-dbg" components, because those -- are handled via the "debug" component, except for kernel-dbg -- which is always shown for non-jail installations. if component == "kernel-dbg" then return (not options.jail) end if component:match("%-dbg$") then return false end -- Some sets have "-jail" variants which are jail-specific -- variants of the base set. if options.jail and components[component.."-jail"] then -- If we're installing in a jail, and this component -- has a jail variant, hide it. return false end if not options.jail and component:match("%-jail$") then -- Otherwise if we're not installing in a jail, and -- this is a jail variant, hide it. return false end -- "minimal(-jail)" is always installed if present. if component == "minimal" or component == "minimal-jail" then return false end -- "kernel" (the generic kernel) and "kernels" (the set) are -- never offered; we always install the kernel for a non-jail -- installation. if component == "kernel" or component == "kernels" then return false end -- If we didn't find a reason to hide this component, show it. return true end for component, _ in pairs(components) do if show_component(component) then table.insert(sorted_components, component) end end table.sort(sorted_components) local checklist_items = {} for _, component in ipairs(sorted_components) do local description = descriptions[component] or "" local default = defaults[component] or "off" table.insert(checklist_items, component) table.insert(checklist_items, description) table.insert(checklist_items, default) end local bsddialog_args = { "--backtitle", "FreeBSD Installer", "--title", "Select System Components", "--nocancel", "--disable-esc", "--separate-output", "--checklist", "A minimal set of packages suitable for a multi-user system ".. "is always installed. Select additional packages you wish ".. "to install:", "0", "0", "0", -- autosize } append_list(bsddialog_args, checklist_items) local exit_code, output = bsddialog(bsddialog_args) -- This should only be possible if bsddialog is killed by a signal -- or buggy, we disable the cancel option and esc key. -- If this does happen, there's not much we can do except exit with a -- hopefully useful stack trace. assert(exit_code == 0) -- Always install the minimal set, since it's required for the system -- to work. The base set depends on minimal, but it's fine to install -- both, and this way the user can remove the base set without pkg -- autoremove then trying to remove minimal. local selected = {} if options.jail then table.insert(selected, "minimal-jail") else table.insert(selected, "minimal") end -- If pkg is available, always install it so the user can manage the -- installed system. This is optional, because a repository built -- from src alone won't have a pkg package. if components["pkg"] then table.insert(selected, "pkg") end if not options.jail then table.insert(selected, "kernel") end for component in output:gmatch("[^\n]+") do table.insert(selected, component) end return selected end -- Returns a list of pkgbase packages selected by the user local function select_packages(pkg, options) -- These are the components which aren't generated automatically from -- package sets. local components = { ["kernel"] = {}, ["kernel-dbg"] = {}, ["debug"] = {}, } -- Note: if you update this list, you must also update the list in -- release/scripts/pkgbase-stage.lua. local kernel_packages = { -- Most architectures use this ["FreeBSD-kernel-generic"] = true, -- PowerPC uses either of these, depending on platform ["FreeBSD-kernel-generic64"] = true, ["FreeBSD-kernel-generic64le"] = true, } local rquery = capture(pkg .. "rquery -U -r FreeBSD-base %n") for package in rquery:gmatch("[^\n]+") do local setname = package:match("^FreeBSD%-set%-(.+)$") if setname then components[setname] = components[setname] or {} table.insert(components[setname], package) elseif kernel_packages[package] then table.insert(components["kernel"], package) elseif kernel_packages[package:match("(.*)%-dbg$")] then table.insert(components["kernel-dbg"], package) elseif package == "pkg" then components["pkg"] = components["pkg"] or {} table.insert(components["pkg"], package) end end -- Assert that both a kernel and the "minimal" set are available, since -- those are both required to install a functional system. Don't worry -- if other sets are missing (e.g. base or src), which might happen -- when using custom install media. assert(#components["kernel"] == 1) assert(#components["minimal"] == 1) -- Prompt the user for what to install. local selected = select_components(components, options) -- Determine if the "debug" component was selected. local debug = false for _, component in ipairs(selected) do if component == "debug" then debug = true break end end local packages = {} for _, component in ipairs(selected) do local pkglist = components[component] append_list(packages, pkglist) -- If the debug component was selected, install the -dbg -- package for each set. We have to check if the dbg set -- actually exists, because some sets (src, tests) don't -- have a -dbg subpackage. for _, c in ipairs(pkglist) do local setname = c:match("^FreeBSD%-set%-(.*)$") if debug and setname then local dbgset = setname.."-dbg" if components[dbgset] then append_list(packages, components[dbgset]) end end end end return packages end local function parse_options() local options = {} for _, a in ipairs(arg) do if a == "--jail" then options.jail = true else io.stderr:write("Error: unknown option " .. a .. "\n") os.exit(1) end end return options end -- Fetch and install pkgbase packages to BSDINSTALL_CHROOT. -- Respect BSDINSTALL_PKG_REPOS_DIR if set, otherwise use pkg.freebsd.org. local function pkgbase() local options = parse_options() -- 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)) -- Always install the default FreeBSD-base.conf file to the chroot, even -- if we don't actually fetch the packages from the repository specified -- there (e.g. because we are performing an offline installation). local chroot_repos_dir = chroot .. "/usr/local/etc/pkg/repos/" assert(os.execute("mkdir -p " .. chroot_repos_dir)) assert(os.execute("cp /usr/share/bsdinstall/FreeBSD-base.conf " .. chroot_repos_dir)) local repos_dir = os.getenv("BSDINSTALL_PKG_REPOS_DIR") if not repos_dir then repos_dir = chroot_repos_dir -- Since pkg always interprets fingerprints paths as relative to -- the --rootdir we must copy the key from the host. assert(os.execute("mkdir -p " .. chroot .. "/usr/share/keys")) - assert(os.execute("cp -R /usr/share/keys/pkg " .. chroot .. "/usr/share/keys/")) + assert(os.execute("cp -R /usr/share/keys/* " .. chroot .. "/usr/share/keys/")) 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 os.exit(1) end end local packages = table.concat(select_packages(pkg, options), " ") while not os.execute(pkg .. "install -U -F -y -r FreeBSD-base " .. packages) do if not prompt_yn("Fetching packages failed, try again?") then os.exit(1) end end if not os.execute(pkg .. "install -U -y -r FreeBSD-base " .. packages) then os.exit(1) end end pkgbase()