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 all_libcompats <const> = "%%_ALL_libcompats%%" 11 12-- Run a command using the OS shell and capture the stdout 13-- Strips exactly one trailing newline if present, does not strip any other whitespace. 14-- Asserts that the command exits cleanly 15local function capture(command) 16 local p = io.popen(command) 17 local output = p:read("*a") 18 assert(p:close()) 19 -- Strip exactly one trailing newline from the output, if there is one 20 return output:match("(.-)\n$") or output 21end 22 23local function prompt_yn(question) 24 while true do 25 io.write(question .. " (y/n) ") 26 local input = io.read() 27 if input == "y" or input == "Y" then 28 return true 29 elseif input == "n" or input == "N" then 30 return false 31 end 32 end 33end 34 35local function append_list(list, other) 36 for _, item in ipairs(other) do 37 table.insert(list, item) 38 end 39end 40 41-- Returns a list of pkgbase packages equivalent to the default base.txz and kernel.txz 42local function select_packages(pkg, options) 43 local components = { 44 ["kernel"] = {}, 45 ["kernel-dbg"] = {}, 46 ["base"] = {}, 47 ["base-dbg"] = {}, 48 ["src"] = {}, 49 ["tests"] = {}, 50 } 51 52 for compat in all_libcompats:gmatch("%S+") do 53 components["lib" .. compat] = {} 54 components["lib" .. compat .. "-dbg"] = {} 55 end 56 57 local rquery = capture(pkg .. "rquery -U -r FreeBSD-base %n") 58 for package in rquery:gmatch("[^\n]+") do 59 if package == "FreeBSD-src" or package:match("^FreeBSD%-src%-.*") then 60 table.insert(components["src"], package) 61 elseif package == "FreeBSD-tests" or package:match("^FreeBSD%-tests%-.*") then 62 table.insert(components["tests"], package) 63 elseif package:match("^FreeBSD%-kernel%-.*") then 64 -- Kernels other than FreeBSD-kernel-generic are ignored 65 if package == "FreeBSD-kernel-generic" then 66 table.insert(components["kernel"], package) 67 elseif package == "FreeBSD-kernel-generic-dbg" then 68 table.insert(components["kernel-dbg"], package) 69 end 70 elseif package:match(".*%-dbg$") then 71 table.insert(components["base-dbg"], package) 72 else 73 local found = false 74 for compat in all_libcompats:gmatch("%S+") do 75 if package:match(".*%-dbg%-lib" .. compat .. "$") then 76 table.insert(components["lib" .. compat .. "-dbg"], package) 77 found = true 78 break 79 elseif package:match(".*%-lib" .. compat .. "$") then 80 table.insert(components["lib" .. compat], package) 81 found = true 82 break 83 end 84 end 85 if not found then 86 table.insert(components["base"], package) 87 end 88 end 89 end 90 -- Don't assert the existence of dbg, tests, and src packages here. If using 91 -- a custom local repository with BSDINSTALL_PKG_REPOS_DIR we shouldn't 92 -- require it to have all packages. 93 assert(#components["kernel"] == 1) 94 assert(#components["base"] > 0) 95 96 local selected = {} 97 append_list(selected, components["base"]) 98 if not options.no_kernel then 99 append_list(selected, components["kernel"]) 100 end 101 102 return selected 103end 104 105local function parse_options() 106 local options = {} 107 for _, a in ipairs(arg) do 108 if a == "--no-kernel" then 109 options.no_kernel = true 110 else 111 io.stderr:write("Error: unknown option " .. a .. "\n") 112 os.exit(1) 113 end 114 end 115 return options 116end 117 118-- Fetch and install pkgbase packages to BSDINSTALL_CHROOT. 119-- Respect BSDINSTALL_PKG_REPOS_DIR if set, otherwise use pkg.freebsd.org. 120local function pkgbase() 121 local options = parse_options() 122 123 -- TODO Support fully offline pkgbase installation by taking a new enough 124 -- version of pkg.pkg as input. 125 if not os.execute("pkg -N > /dev/null 2>&1") then 126 print("Bootstrapping pkg on the host system") 127 assert(os.execute("pkg bootstrap -y")) 128 end 129 130 local chroot = assert(os.getenv("BSDINSTALL_CHROOT")) 131 assert(os.execute("mkdir -p " .. chroot)) 132 133 local repos_dir = os.getenv("BSDINSTALL_PKG_REPOS_DIR") 134 if not repos_dir then 135 repos_dir = chroot .. "/usr/local/etc/pkg/repos/" 136 assert(os.execute("mkdir -p " .. repos_dir)) 137 assert(os.execute("cp /usr/share/bsdinstall/FreeBSD-base.conf " .. repos_dir)) 138 139 -- Since pkg always interprets fingerprints paths as relative to 140 -- the --rootdir we must copy the key from the host. 141 assert(os.execute("mkdir -p " .. chroot .. "/usr/share/keys")) 142 assert(os.execute("cp -R /usr/share/keys/pkg " .. chroot .. "/usr/share/keys/")) 143 end 144 145 -- We must use --repo-conf-dir rather than -o REPOS_DIR here as the latter 146 -- is interpreted relative to the --rootdir. BSDINSTALL_PKG_REPOS_DIR must 147 -- be allowed to point to a path outside the chroot. 148 local pkg = "pkg --rootdir " .. chroot .. 149 " --repo-conf-dir " .. repos_dir .. " -o IGNORE_OSVERSION=yes " 150 151 while not os.execute(pkg .. "update") do 152 if not prompt_yn("Updating repositories failed, try again?") then 153 print("Canceled") 154 os.exit(1) 155 end 156 end 157 158 local packages = table.concat(select_packages(pkg, options), " ") 159 160 while not os.execute(pkg .. "install -U -F -y -r FreeBSD-base " .. packages) do 161 if not prompt_yn("Fetching packages failed, try again?") then 162 print("Canceled") 163 os.exit(1) 164 end 165 end 166 167 if not os.execute(pkg .. "install -U -y -r FreeBSD-base " .. packages) then 168 os.exit(1) 169 end 170end 171 172pkgbase() 173