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