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