xref: /freebsd/usr.sbin/bsdinstall/scripts/pkgbase.in (revision b670c9bafc0e31c7609969bf374b2e80bdc00211)
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