xref: /freebsd/tools/build/options/makeman.lua (revision 8378665ff9176677c2ed117756afceb601a1dca0)
1--
2-- Copyright (c) 2023 Kyle Evans <kevans@FreeBSD.org>
3--
4-- SPDX-License-Identifier: BSD-2-Clause
5--
6
7local libgen = require('posix.libgen')
8local lfs = require('lfs')
9local stdlib = require('posix.stdlib')
10local unistd = require('posix.unistd')
11local sys_wait = require('posix.sys.wait')
12
13local curdate = os.date("%B %e, %Y")
14
15local output_head <const> = ".\\\" DO NOT EDIT-- this file is @" .. [[generated by tools/build/options/makeman.
16.Dd ]] .. curdate .. [[
17
18.Dt SRC.CONF 5
19.Os
20.Sh NAME
21.Nm src.conf
22.Nd "source build options"
23.Sh DESCRIPTION
24The
25.Nm
26file contains variables that control what components will be generated during
27the build process of the
28.Fx
29source tree; see
30.Xr build 7 .
31.Pp
32The
33.Nm
34file uses the standard makefile syntax.
35However,
36.Nm
37should not specify any dependencies to
38.Xr make 1 .
39Instead,
40.Nm
41is to set
42.Xr make 1
43variables that control the aspects of how the system builds.
44.Pp
45The default location of
46.Nm
47is
48.Pa /etc/src.conf ,
49though an alternative location can be specified in the
50.Xr make 1
51variable
52.Va SRCCONF .
53Overriding the location of
54.Nm
55may be necessary if the system-wide settings are not suitable
56for a particular build.
57For instance, setting
58.Va SRCCONF
59to
60.Pa /dev/null
61effectively resets all build controls to their defaults.
62.Pp
63The only purpose of
64.Nm
65is to control the compilation of the
66.Fx
67source code, which is usually located in
68.Pa /usr/src .
69As a rule, the system administrator creates
70.Nm
71when the values of certain control variables need to be changed
72from their defaults.
73.Pp
74In addition, control variables can be specified
75for a particular build via the
76.Fl D
77option of
78.Xr make 1
79or in its environment; see
80.Xr environ 7 .
81.Pp
82The environment of
83.Xr make 1
84for the build can be controlled via the
85.Va SRC_ENV_CONF
86variable, which defaults to
87.Pa /etc/src-env.conf .
88Some examples that may only be set in this file are
89.Va WITH_DIRDEPS_BUILD ,
90and
91.Va WITH_META_MODE ,
92and
93.Va MAKEOBJDIRPREFIX
94as they are environment-only variables.
95.Pp
96The values of
97.Va WITH_
98and
99.Va WITHOUT_
100variables are ignored regardless of their setting;
101even if they would be set to
102.Dq Li FALSE
103or
104.Dq Li NO .
105The presence of an option causes
106it to be honored by
107.Xr make 1 .
108.Pp
109This list provides a name and short description for variables
110that can be used for source builds.
111.Bl -tag -width indent
112]]
113
114local output_tail <const> = [[.El
115.Sh FILES
116.Bl -tag -compact -width Pa
117.It Pa /etc/src.conf
118.It Pa /etc/src-env.conf
119.It Pa /usr/share/mk/bsd.own.mk
120.El
121.Sh SEE ALSO
122.Xr make 1 ,
123.Xr make.conf 5 ,
124.Xr build 7 ,
125.Xr ports 7
126.Sh HISTORY
127The
128.Nm
129file appeared in
130.Fx 7.0 .
131.Sh AUTHORS
132This manual page was autogenerated by
133.An tools/build/options/makeman .
134]]
135
136local scriptdir <const> = libgen.dirname(stdlib.realpath(arg[0]))
137local srcdir <const> = stdlib.realpath(scriptdir .. "/../../../")
138local makesysdir <const> = srcdir .. "/share/mk"
139
140local make_envvar = os.getenv("MAKE")
141local make_cmd_override = {}
142if make_envvar then
143	for word in make_envvar:gmatch("[^%s]+") do
144		make_cmd_override[#make_cmd_override + 1] = word
145	end
146end
147
148-- Lifted from bsdinstall/scripts/pkgbase.in (read_all)
149local function read_pipe(pipe)
150	local ret = ""
151	repeat
152		local buffer = assert(unistd.read(pipe, 1024))
153		ret = ret .. buffer
154	until buffer == ""
155	return ret
156end
157local function run_make(args)
158	local cmd_args = {"env", "-i", "make", "-C", srcdir, "-m", makesysdir,
159	    "__MAKE_CONF=/dev/null", "SRCCONF=/dev/null"}
160
161	if #make_cmd_override > 0 then
162		cmd_args[3] = make_cmd_override[1]
163		for k = 2, #make_cmd_override do
164			local val = make_cmd_override[k]
165
166			table.insert(cmd_args, 3 + (k - 1), val)
167		end
168	end
169	for k, v in ipairs(args) do
170		cmd_args[#cmd_args + 1] = v
171	end
172
173	local r, w = assert(unistd.pipe())
174	local pid = assert(unistd.fork())
175	if pid == 0 then
176		-- Child
177		assert(unistd.close(r))
178		assert(unistd.dup2(w, 1))
179		assert(unistd.dup2(w, 2))
180		assert(unistd.execp("env", cmd_args))
181		unistd._exit()
182	end
183
184	-- Parent
185	assert(unistd.close(w))
186
187	local output = read_pipe(r)
188	assert(unistd.close(r))
189
190	local _, exit_type, exit_code = assert(sys_wait.wait(pid))
191	assert(exit_type == "exited", "make exited with wrong status")
192	assert(exit_code == 0, "make exited unsuccessfully")
193	return output
194end
195
196local function native_target()
197	local output = run_make({"MK_AUTO_OBJ=NO", "-V", "MACHINE",
198	    "-V", "MACHINE_ARCH"})
199
200	local arch, machine_arch
201	for x in output:gmatch("[^\n]+") do
202		if not arch then
203			arch = x
204		elseif not machine_arch then
205			machine_arch = x
206		end
207	end
208
209	return arch .. "/" .. machine_arch
210end
211
212local function src_targets()
213	local targets = {}
214	targets[native_target()] = true
215
216	local output = run_make({"MK_AUTO_OBJ=no", "targets"})
217	local curline = 0
218
219	for line in output:gmatch("[^\n]+") do
220		curline = curline + 1
221		if curline ~= 1 then
222			local arch = line:match("[^%s]+/[^%s]+")
223
224			-- Make sure we don't roll over our default arch
225			if arch and not targets[arch] then
226				targets[arch] = false
227			end
228		end
229	end
230
231	return targets
232end
233
234local function config_options(srcconf, env, take_dupes, linting)
235	srcconf = srcconf or "/dev/null"
236	env = env or {}
237
238	local option_args = {".MAKE.MODE=normal", "showconfig",
239	    "SRC_ENV_CONF=" .. srcconf}
240
241	for _, val in ipairs(env) do
242		option_args[#option_args + 1] = val
243	end
244
245	local output = run_make(option_args)
246
247	local options = {}
248	local known_dupes = {}
249
250	local function warn_on_dupe(option, val)
251		if not linting or known_dupes[option] then
252			return false
253		end
254		if not option:match("^OPT_") then
255			val = val == "yes"
256		end
257
258		known_dupes[option] = true
259		return val ~= options[val]
260	end
261
262	for opt in output:gmatch("[^\n]+") do
263		if opt:match("^MK_[%a%d_]+%s+=%s+.+") then
264			local name = opt:match("MK_[%a%d_]+")
265			local val = opt:match("= .+"):sub(3)
266
267			-- Some settings, e.g., MK_INIT_ALL_ZERO, may end up
268			-- output twice for some reason that I haven't dug into;
269			-- take the first value.  In some circumstances, though,
270			-- we do make an exception and actually want to take the
271			-- latest.
272			if take_dupes or options[name] == nil then
273				options[name] = val == "yes"
274			elseif warn_on_dupe(name, val) then
275				io.stderr:write("ignoring duplicate option " ..
276				    name .. "\n")
277			end
278		elseif opt:match("^OPT_[%a%d_]+%s+=%s+.+") then
279			local name = opt:match("OPT_[%a%d_]+")
280			local val = opt:match("= .+"):sub(3)
281
282			-- Multi-value options will arbitrarily use a table here
283			-- to indicate the difference.
284			if take_dupes or options[name] == nil then
285				options[name] = val
286			elseif warn_on_dupe(name, val) then
287				io.stderr:write("ignoring duplicate option " ..
288				    name .. "\n")
289			end
290		end
291	end
292
293	return options
294end
295
296local function env_only_options()
297	local output = run_make({"MK_AUTO_OBJ=no", "-V", "__ENV_ONLY_OPTIONS"})
298	local options = {}
299
300	for opt in output:gmatch("[^%s]+") do
301		options["MK_" .. opt] = true
302	end
303
304	return options
305end
306
307local function required_options()
308	local output = run_make({"-f", "share/mk/src.opts.mk", "-V",
309	    "__REQUIRED_OPTIONS"})
310	local options = {}
311
312	for opt in output:gmatch("[^%s]+") do
313		options["MK_" .. opt] = true
314	end
315
316	return options
317end
318
319local function config_description(option_name)
320	local fh = io.open(scriptdir .. "/" .. option_name)
321	local desc
322
323	if fh then
324		desc = ""
325		for line in fh:lines() do
326			if not line:match("%$FreeBSD%$") then
327				desc = desc .. line .. "\n"
328			end
329		end
330
331		assert(fh:close())
332	end
333
334	return desc
335end
336
337local function config_descriptions(options)
338	local desc = {}
339	for name, _ in pairs(options) do
340		if name:match("^MK_") then
341			local basename = name:gsub("^MK_", "")
342			local with_name = "WITH_" .. basename
343			local without_name = "WITHOUT_" .. basename
344
345			desc[with_name] = config_description(with_name)
346			desc[without_name] = config_description(without_name)
347		elseif name:match("^OPT_") then
348			local basename = name:gsub("^OPT_", "")
349
350			desc[name] = config_description(basename)
351		end
352	end
353	return desc
354end
355
356local function dependent_options(tmpdir, option_name, all_opts, omit_others)
357	local opt_sense = not not option_name:match("^WITH_")
358	local base_option_name = option_name:gsub("^[^_]+_", "")
359	local prefix = (opt_sense and "WITHOUT_") or "WITH_"
360
361	local srcconf = tmpdir .. "/src-" ..prefix .. "ALL_" ..
362	    option_name .. ".conf"
363	local fh = assert(io.open(srcconf, "w+"))
364
365	fh:write(option_name .. "=\"YES\"\n")
366	if not omit_others then
367		for opt, value in pairs(all_opts) do
368			local base_opt = opt:gsub("^MK_", "")
369
370			if base_opt ~= base_option_name then
371				local opt_prefix = (value and "WITH_") or "WITHOUT_"
372				fh:write(opt_prefix .. base_opt .. "=\"YES\"\n")
373			end
374		end
375	end
376	assert(fh:close())
377
378	local option_name_key = "MK_" .. base_option_name
379	local options = config_options(srcconf, nil, omit_others)
380	for name, value in pairs(options) do
381		if name == option_name_key or value == all_opts[name] then
382			options[name] = nil
383		elseif name:match("^OPT_") then
384			-- Strip out multi-option values at the moment, they do
385			-- not really make sense.
386			options[name] = nil
387		end
388	end
389
390	return options
391end
392
393local function export_option_table(fd, name, options)
394	unistd.write(fd, name .. " = {")
395	for k, v in pairs(options) do
396		v = (v and "true") or "false"
397		unistd.write(fd, "['" .. k .. "'] = " .. v .. ",")
398	end
399	unistd.write(fd, "}")
400end
401
402local function all_dependent_options(tmpdir, options, default_opts,
403    with_all_opts, without_all_opts)
404	local all_enforced_options = {}
405	local all_effect_options = {}
406	local children = {}
407
408	for _, name in ipairs(options) do
409		local rfd, wfd = assert(unistd.pipe())
410		local pid = assert(unistd.fork())
411
412		if pid == 0 then
413			-- We need to pcall() this so that errors bubble up to
414			-- our _exit() call rather than the main exit.
415			local ret, errobj = pcall(function()
416				unistd.close(rfd)
417
418				local compare_table
419				if name:match("^WITHOUT") then
420					compare_table = with_all_opts
421				else
422					compare_table = without_all_opts
423				end
424
425				-- List of knobs forced on by this one
426				local enforced_options = dependent_options(tmpdir, name,
427				    compare_table)
428				-- List of knobs implied by this by one (once additionally
429				-- filtered based on enforced_options values)
430				local effect_options = dependent_options(tmpdir, name,
431				    default_opts, true)
432
433				export_option_table(wfd, "enforced_options",
434				    enforced_options)
435				export_option_table(wfd, "effect_options",
436				    effect_options)
437			end)
438
439			io.stderr:write(".")
440
441			if ret then
442				unistd._exit(0)
443			else
444				unistd.write(wfd, errobj)
445				unistd._exit(1)
446			end
447		end
448
449		unistd.close(wfd)
450		children[pid] = {name, rfd}
451	end
452
453	while next(children) ~= nil do
454::again::
455		local pid, status, exitcode = sys_wait.wait(-1)
456
457		if status ~= "exited" then
458			goto again
459		end
460
461		local info = children[pid]
462		children[pid] = nil
463
464		local name = info[1]
465		local rfd = info[2]
466		local buf = ''
467		local rbuf, sz
468
469		-- Drain the pipe
470		rbuf = unistd.read(rfd, 512)
471		while #rbuf ~= 0 do
472			buf = buf .. rbuf
473			rbuf = unistd.read(rfd, 512)
474		end
475
476		unistd.close(rfd)
477
478		if exitcode ~= 0 then
479			error("Child " .. pid .. " failed, buf: " .. buf)
480		end
481
482		-- The child has written a pair of tables named enforced_options
483		-- and effect_options to the pipe.  We'll load the pipe buffer
484		-- as a string and then yank these out of the clean environment
485		-- that we execute the chunk in.
486		local child_env = {}
487		local res, err = pcall(load(buf, "child", "t", child_env))
488
489		all_enforced_options[name] = child_env["enforced_options"]
490		all_effect_options[name] = child_env["effect_options"]
491	end
492
493	io.stderr:write("\n")
494	return all_enforced_options, all_effect_options
495end
496
497local function get_defaults(target_archs, native_default_opts)
498	local target_defaults = {}
499	-- Set of options with differing defaults in some archs
500	local different_defaults = {}
501
502	for tgt, dflt in pairs(target_archs) do
503		if dflt then
504			local native_copy = {}
505			for opt, val in pairs(native_default_opts) do
506				native_copy[opt] = val
507			end
508			target_defaults[tgt] = native_copy
509			goto skip
510		end
511
512		local target = tgt:gsub("/.+$", "")
513		local target_arch = tgt:gsub("^.+/", "")
514
515		local target_opts = config_options(nil, {"TARGET=" .. target,
516		    "TARGET_ARCH=" .. target_arch})
517
518		for opt, val in pairs(target_opts) do
519			if val ~= native_default_opts[opt] then
520				different_defaults[opt] = true
521			end
522		end
523
524		target_defaults[tgt] = target_opts
525::skip::
526	end
527
528	for opt in pairs(native_default_opts) do
529		if different_defaults[opt] == nil then
530			for _, opts in pairs(target_defaults) do
531				opts[opt] = nil
532			end
533		end
534	end
535
536	for tgt, opts in pairs(target_defaults) do
537		local val = opts["MK_ACPI"]
538
539		if val ~= nil then
540			print(" - " .. tgt .. ": " .. ((val and "yes") or "no"))
541		end
542	end
543
544	return target_defaults, different_defaults
545end
546
547local function option_comparator(lhs, rhs)
548	-- Convert both options to the base name, compare that instead unless
549	-- they're the same option.  For the same option, we just want to get
550	-- ordering between WITH_/WITHOUT_ correct.
551	local base_lhs = lhs:gsub("^[^_]+_", "")
552	local base_rhs = rhs:gsub("^[^_]+_", "")
553
554	if base_lhs == base_rhs then
555		return lhs < rhs
556	else
557		return base_lhs < base_rhs
558	end
559end
560
561local function main(tmpdir)
562	io.stderr:write("building src.conf.5 man page from files in " ..
563	    scriptdir .. "\n")
564
565	local env_only_opts <const> = env_only_options()
566	local default_opts = config_options(nil, nil, nil, true)
567	local opt_descriptions = config_descriptions(default_opts)
568	local srcconf_all <const> = tmpdir .. "/src-all-enabled.conf"
569	local fh = io.open(srcconf_all, "w+")
570	local all_targets = src_targets()
571	local target_defaults, different_defaults = get_defaults(all_targets,
572	    default_opts)
573	local options = {}
574	local without_all_opts = {}
575
576	for name, value in pairs(default_opts) do
577		if name:match("^MK_") then
578			local base_name = name:gsub("^MK_", "")
579			local with_name = "WITH_" .. base_name
580			local without_name = "WITHOUT_" .. base_name
581			-- If it's differently defaulted on some architectures,
582			-- we'll split it into WITH_/WITHOUT_ just to simplify
583			-- some later bits.
584			if different_defaults[name] ~= nil then
585				options[#options + 1] = with_name
586				options[#options + 1] = without_name
587			elseif value then
588				options[#options + 1] = without_name
589			else
590				options[#options + 1] = with_name
591			end
592
593			without_all_opts[name] = false
594			assert(fh:write(with_name .. '="YES"\n'))
595		else
596			options[#options + 1] = name
597		end
598	end
599
600	assert(fh:close())
601
602	local with_all_opts = config_options(srcconf_all)
603	local all_enforced_options, all_effect_options
604	local all_required_options = required_options()
605
606	all_enforced_options, all_effect_options = all_dependent_options(tmpdir,
607	    options, default_opts, with_all_opts, without_all_opts)
608
609	table.sort(options, option_comparator)
610	io.stdout:write(output_head)
611	for _, name in ipairs(options) do
612		local value
613
614		if name:match("^OPT_") then
615			goto skip
616		end
617		assert(name:match("^WITH"), "Name looks wrong: " .. name)
618		local describe_option = name
619
620		value = not not name:match("^WITHOUT")
621
622		-- Normalize name to MK_ for indexing into various other
623		-- arrays
624		name = "MK_" .. name:gsub("^[^_]+_", "")
625
626		print(".It Va " .. describe_option:gsub("^OPT_", ""))
627		if opt_descriptions[describe_option] then
628			io.stdout:write(opt_descriptions[describe_option])
629		else
630			io.stderr:write("Missing description for " ..
631			    describe_option .. "\n")
632		end
633
634		local enforced_options = all_enforced_options[describe_option]
635		local effect_options = all_effect_options[describe_option]
636
637		if different_defaults[name] ~= nil then
638			print([[.Pp
639This is a default setting on]])
640
641			local which_targets = {}
642			for tgt, tgt_options in pairs(target_defaults) do
643				if tgt_options[name] ~= value then
644					which_targets[#which_targets + 1] = tgt
645				end
646			end
647
648			table.sort(which_targets)
649			for idx, tgt in ipairs(which_targets) do
650				io.stdout:write(tgt)
651				if idx < #which_targets - 1 then
652					io.stdout:write(", ")
653				elseif idx == #which_targets - 1 then
654					io.stdout:write(" and ")
655				end
656			end
657			print(".")
658		end
659
660		-- Unset any implied options that are actually required.
661		for dep_opt in pairs(enforced_options) do
662			if all_required_options[dep_opt] then
663				enforced_options[dep_opt] = nil
664			end
665		end
666		if next(enforced_options) ~= nil then
667			print([[When set, it enforces these options:
668.Pp
669.Bl -item -compact]])
670
671			local sorted_dep_opt = {}
672			for dep_opt in pairs(enforced_options) do
673				sorted_dep_opt[#sorted_dep_opt + 1] = dep_opt
674			end
675
676			table.sort(sorted_dep_opt)
677			for _, dep_opt in ipairs(sorted_dep_opt) do
678				local dep_val = enforced_options[dep_opt]
679				local dep_prefix = (dep_val and "WITH_") or
680				    "WITHOUT_"
681				local dep_name = dep_opt:gsub("^MK_",
682				    dep_prefix)
683				print(".It")
684				print(".Va " .. dep_name)
685			end
686
687			print(".El")
688		end
689
690		if next(effect_options) ~= nil then
691			if next(enforced_options) ~= nil then
692				-- Remove any options that were previously
693				-- noted as enforced...
694				for opt, val in pairs(effect_options) do
695					if enforced_options[opt] == val then
696						effect_options[opt] = nil
697					end
698				end
699
700				-- ... and this could leave us with an empty
701				-- set.
702				if next(effect_options) == nil then
703					goto noenforce
704				end
705
706				print(".Pp")
707			end
708
709			print([[When set, these options are also in effect:
710.Pp
711.Bl -inset -compact]])
712
713			local sorted_dep_opt = {}
714			for dep_opt in pairs(effect_options) do
715				sorted_dep_opt[#sorted_dep_opt + 1] = dep_opt
716			end
717
718			table.sort(sorted_dep_opt)
719			for _, dep_opt in ipairs(sorted_dep_opt) do
720				local dep_val = effect_options[dep_opt]
721				local dep_prefix = (dep_val and "WITH_") or
722				    "WITHOUT_"
723				local not_dep_prefix = ((not dep_val) and "WITH_") or
724				    "WITHOUT_"
725				local dep_name = dep_opt:gsub("^MK_",
726				    dep_prefix)
727				local not_dep_name = dep_opt:gsub("^MK_",
728				    not_dep_prefix)
729
730				print(".It Va " .. dep_name)
731				print("(unless")
732				print(".Va " .. not_dep_name)
733				print("is set explicitly)")
734			end
735
736			print(".El")
737::noenforce::
738		end
739
740		if env_only_opts[name] ~= nil then
741			print([[.Pp
742This must be set in the environment, make command line, or
743.Pa /etc/src-env.conf ,
744not
745.Pa /etc/src.conf .]])
746		end
747		::skip::
748	end
749	print([[.El
750.Pp
751The following options accept a single value from a list of valid values.
752.Bl -tag -width indent]])
753	for _, name in ipairs(options) do
754		if name:match("^OPT_") then
755			local desc = opt_descriptions[name]
756
757			print(".It Va " .. name:gsub("^OPT_", ""))
758			if desc then
759				io.stdout:write(desc)
760			else
761				io.stderr:write("Missing description for " ..
762				    name .. "\n")
763			end
764		end
765	end
766	io.stdout:write(output_tail)
767end
768
769local tmpdir = "/tmp/makeman." .. unistd.getpid()
770
771if not lfs.mkdir(tmpdir) then
772	error("Failed to create tempdir " .. tmpdir)
773end
774
775-- Catch any errors so that we can properly clean up, then re-throw it.
776local ret, errobj = pcall(main, tmpdir)
777
778for fname in lfs.dir(tmpdir) do
779	if fname ~= "." and fname ~= ".." then
780		assert(os.remove(tmpdir .. "/" .. fname))
781	end
782end
783
784if not lfs.rmdir(tmpdir) then
785	assert(io.stderr:write("Failed to clean up tmpdir: " .. tmpdir .. "\n"))
786end
787
788if not ret then
789	io.stderr:write(errobj .. "\n")
790	os.exit(1)
791end
792