aboutsummaryrefslogtreecommitdiff
path: root/usr.sbin/bsdinstall/scripts/pkgbase.in
blob: 5299d34fcb7107bdc647dd7596e7e4d555b0958b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
#!/usr/libexec/flua

-- SPDX-License-Identifier: BSD-2-Clause
--
-- Copyright(c) 2025 The FreeBSD Foundation.
--
-- This software was developed by Isaac Freund <ifreund@freebsdfoundation.org>
-- under sponsorship from the FreeBSD Foundation.

local sys_wait = require("posix.sys.wait")
local unistd = require("posix.unistd")

local all_libcompats <const> = "%%_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/"))
	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()