xref: /freebsd/tools/build/options/makeman.lua (revision ea27ec183d0ff26e1273202841a02041b6d93955)
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	if exit_code ~= 0 then
196		io.stderr:write("Warning: make exited unsuccessfully\n" ..
197		    "cmd: make " .. table.concat(args, " ") .. "\n")
198	end
199	return output
200end
201
202local function native_target()
203	local output = run_make({"MK_AUTO_OBJ=NO", "-V", "MACHINE",
204	    "-V", "MACHINE_ARCH"})
205
206	local arch, machine_arch
207	for x in output:gmatch("[^\n]+") do
208		if not arch then
209			arch = x
210		elseif not machine_arch then
211			machine_arch = x
212		end
213	end
214
215	return arch .. "/" .. machine_arch
216end
217
218local function src_targets()
219	local targets = {}
220	targets[native_target()] = true
221
222	local output = run_make({"MK_AUTO_OBJ=no", "targets"})
223	local curline = 0
224
225	for line in output:gmatch("[^\n]+") do
226		curline = curline + 1
227		if curline ~= 1 then
228			local arch = line:match("[^%s]+/[^%s]+")
229
230			-- Make sure we don't roll over our default arch
231			if arch and not targets[arch] then
232				targets[arch] = false
233			end
234		end
235	end
236
237	return targets
238end
239
240local function config_options(srcconf, env, take_dupes, linting)
241	srcconf = srcconf or "/dev/null"
242	env = env or {}
243
244	local option_args = {".MAKE.MODE=normal", "showconfig",
245	    "SRC_ENV_CONF=" .. srcconf}
246
247	for _, val in ipairs(env) do
248		option_args[#option_args + 1] = val
249	end
250
251	local output = run_make(option_args)
252
253	local options = {}
254	local known_dupes = {}
255
256	local function warn_on_dupe(option, val)
257		if not linting or known_dupes[option] then
258			return false
259		end
260		if not option:match("^OPT_") then
261			val = val == "yes"
262		end
263
264		known_dupes[option] = true
265		return val ~= options[val]
266	end
267
268	for opt in output:gmatch("[^\n]+") do
269		if opt:match("^MK_[%a%d_]+%s+=%s+.+") then
270			local name = opt:match("MK_[%a%d_]+")
271			local val = opt:match("= .+"):sub(3)
272
273			-- Some settings, e.g., MK_INIT_ALL_ZERO, may end up
274			-- output twice for some reason that I haven't dug into;
275			-- take the first value.  In some circumstances, though,
276			-- we do make an exception and actually want to take the
277			-- latest.
278			if take_dupes or options[name] == nil then
279				options[name] = val == "yes"
280			elseif warn_on_dupe(name, val) then
281				io.stderr:write("ignoring duplicate option " ..
282				    name .. "\n")
283			end
284		elseif opt:match("^OPT_[%a%d_]+%s+=%s+.+") then
285			local name = opt:match("OPT_[%a%d_]+")
286			local val = opt:match("= .+"):sub(3)
287
288			-- Multi-value options will arbitrarily use a table here
289			-- to indicate the difference.
290			if take_dupes or options[name] == nil then
291				options[name] = val
292			elseif warn_on_dupe(name, val) then
293				io.stderr:write("ignoring duplicate option " ..
294				    name .. "\n")
295			end
296		end
297	end
298
299	return options
300end
301
302local function env_only_options()
303	local output = run_make({"MK_AUTO_OBJ=no", "-V", "__ENV_ONLY_OPTIONS"})
304	local options = {}
305
306	for opt in output:gmatch("[^%s]+") do
307		options["MK_" .. opt] = true
308	end
309
310	return options
311end
312
313local function required_options()
314	local output = run_make({"-f", "share/mk/src.opts.mk", "-V",
315	    "REQUIRED_OPTIONS"})
316	local options = {}
317
318	for opt in output:gmatch("[^%s]+") do
319		options["MK_" .. opt] = true
320	end
321
322	return options
323end
324
325local function config_description(option_name)
326	local fh = io.open(scriptdir .. "/" .. option_name)
327	local desc
328
329	if fh then
330		desc = ""
331		for line in fh:lines() do
332			if not line:match("%$FreeBSD%$") then
333				desc = desc .. line .. "\n"
334			end
335		end
336
337		assert(fh:close())
338	end
339
340	return desc
341end
342
343local function config_descriptions(options)
344	local desc = {}
345	for name, _ in pairs(options) do
346		if name:match("^MK_") then
347			local basename = name:gsub("^MK_", "")
348			local with_name = "WITH_" .. basename
349			local without_name = "WITHOUT_" .. basename
350
351			desc[with_name] = config_description(with_name)
352			desc[without_name] = config_description(without_name)
353		elseif name:match("^OPT_") then
354			local basename = name:gsub("^OPT_", "")
355
356			desc[name] = config_description(basename)
357		end
358	end
359	return desc
360end
361
362local function dependent_options(tmpdir, option_name, all_opts, omit_others)
363	local opt_sense = not not option_name:match("^WITH_")
364	local base_option_name = option_name:gsub("^[^_]+_", "")
365	local prefix = (opt_sense and "WITHOUT_") or "WITH_"
366
367	local srcconf = tmpdir .. "/src-" ..prefix .. "ALL_" ..
368	    option_name .. ".conf"
369	local fh = assert(io.open(srcconf, "w+"))
370
371	fh:write(option_name .. "=\"YES\"\n")
372	if not omit_others then
373		for opt, value in pairs(all_opts) do
374			local base_opt = opt:gsub("^MK_", "")
375
376			if base_opt ~= base_option_name then
377				local opt_prefix = (value and "WITH_") or "WITHOUT_"
378				fh:write(opt_prefix .. base_opt .. "=\"YES\"\n")
379			end
380		end
381	end
382	assert(fh:close())
383
384	local option_name_key = "MK_" .. base_option_name
385	local options = config_options(srcconf, nil, omit_others)
386	for name, value in pairs(options) do
387		if name == option_name_key or value == all_opts[name] then
388			options[name] = nil
389		elseif name:match("^OPT_") then
390			-- Strip out multi-option values at the moment, they do
391			-- not really make sense.
392			options[name] = nil
393		end
394	end
395
396	return options
397end
398
399local function export_option_table(fd, name, options)
400	unistd.write(fd, name .. " = {")
401	for k, v in pairs(options) do
402		v = (v and "true") or "false"
403		unistd.write(fd, "['" .. k .. "'] = " .. v .. ",")
404	end
405	unistd.write(fd, "}")
406end
407
408local function all_dependent_options(tmpdir, options, default_opts,
409    with_all_opts, without_all_opts)
410	local all_enforced_options = {}
411	local all_effect_options = {}
412	local children = {}
413
414	for _, name in ipairs(options) do
415		local rfd, wfd = assert(unistd.pipe())
416		local pid = assert(unistd.fork())
417
418		if pid == 0 then
419			-- We need to pcall() this so that errors bubble up to
420			-- our _exit() call rather than the main exit.
421			local ret, errobj = pcall(function()
422				unistd.close(rfd)
423
424				local compare_table
425				if name:match("^WITHOUT") then
426					compare_table = with_all_opts
427				else
428					compare_table = without_all_opts
429				end
430
431				-- List of knobs forced on by this one
432				local enforced_options = dependent_options(tmpdir, name,
433				    compare_table)
434				-- List of knobs implied by this by one (once additionally
435				-- filtered based on enforced_options values)
436				local effect_options = dependent_options(tmpdir, name,
437				    default_opts, true)
438
439				export_option_table(wfd, "enforced_options",
440				    enforced_options)
441				export_option_table(wfd, "effect_options",
442				    effect_options)
443			end)
444
445			io.stderr:write(".")
446
447			if ret then
448				unistd._exit(0)
449			else
450				unistd.write(wfd, errobj)
451				unistd._exit(1)
452			end
453		end
454
455		unistd.close(wfd)
456		children[pid] = {name, rfd}
457	end
458
459	while next(children) ~= nil do
460::again::
461		local pid, status, exitcode = sys_wait.wait(-1)
462
463		if status ~= "exited" then
464			goto again
465		end
466
467		local info = children[pid]
468		children[pid] = nil
469
470		local name = info[1]
471		local rfd = info[2]
472		local buf = ''
473		local rbuf, sz
474
475		-- Drain the pipe
476		rbuf = unistd.read(rfd, 512)
477		while #rbuf ~= 0 do
478			buf = buf .. rbuf
479			rbuf = unistd.read(rfd, 512)
480		end
481
482		unistd.close(rfd)
483
484		if exitcode ~= 0 then
485			error("Child " .. pid .. " failed, buf: " .. buf)
486		end
487
488		-- The child has written a pair of tables named enforced_options
489		-- and effect_options to the pipe.  We'll load the pipe buffer
490		-- as a string and then yank these out of the clean environment
491		-- that we execute the chunk in.
492		local child_env = {}
493		local res, err = pcall(load(buf, "child", "t", child_env))
494
495		all_enforced_options[name] = child_env["enforced_options"]
496		all_effect_options[name] = child_env["effect_options"]
497	end
498
499	io.stderr:write("\n")
500	return all_enforced_options, all_effect_options
501end
502
503local function get_defaults(target_archs, native_default_opts)
504	local target_defaults = {}
505	-- Set of options with differing defaults in some archs
506	local different_defaults = {}
507
508	for tgt, dflt in pairs(target_archs) do
509		if dflt then
510			local native_copy = {}
511			for opt, val in pairs(native_default_opts) do
512				native_copy[opt] = val
513			end
514			target_defaults[tgt] = native_copy
515			goto skip
516		end
517
518		local target = tgt:gsub("/.+$", "")
519		local target_arch = tgt:gsub("^.+/", "")
520
521		local target_opts = config_options(nil, {"TARGET=" .. target,
522		    "TARGET_ARCH=" .. target_arch})
523
524		for opt, val in pairs(target_opts) do
525			if val ~= native_default_opts[opt] then
526				different_defaults[opt] = true
527			end
528		end
529
530		target_defaults[tgt] = target_opts
531::skip::
532	end
533
534	for opt in pairs(native_default_opts) do
535		if different_defaults[opt] == nil then
536			for _, opts in pairs(target_defaults) do
537				opts[opt] = nil
538			end
539		end
540	end
541
542	for tgt, opts in pairs(target_defaults) do
543		local val = opts["MK_ACPI"]
544
545		if val ~= nil then
546			print(" - " .. tgt .. ": " .. ((val and "yes") or "no"))
547		end
548	end
549
550	return target_defaults, different_defaults
551end
552
553local function option_comparator(lhs, rhs)
554	-- Convert both options to the base name, compare that instead unless
555	-- they're the same option.  For the same option, we just want to get
556	-- ordering between WITH_/WITHOUT_ correct.
557	local base_lhs = lhs:gsub("^[^_]+_", "")
558	local base_rhs = rhs:gsub("^[^_]+_", "")
559
560	if base_lhs == base_rhs then
561		return lhs < rhs
562	else
563		return base_lhs < base_rhs
564	end
565end
566
567local function main(tmpdir)
568	io.stderr:write("building src.conf.5 man page from files in " ..
569	    scriptdir .. "\n")
570
571	local env_only_opts <const> = env_only_options()
572	local default_opts = config_options(nil, nil, nil, true)
573	local opt_descriptions = config_descriptions(default_opts)
574	local srcconf_all <const> = tmpdir .. "/src-all-enabled.conf"
575	local fh = io.open(srcconf_all, "w+")
576	local all_targets = src_targets()
577	local target_defaults, different_defaults = get_defaults(all_targets,
578	    default_opts)
579	local options = {}
580	local without_all_opts = {}
581
582	for name, value in pairs(default_opts) do
583		if name:match("^MK_") then
584			local base_name = name:gsub("^MK_", "")
585			local with_name = "WITH_" .. base_name
586			local without_name = "WITHOUT_" .. base_name
587			-- If it's differently defaulted on some architectures,
588			-- we'll split it into WITH_/WITHOUT_ just to simplify
589			-- some later bits.
590			if different_defaults[name] ~= nil then
591				options[#options + 1] = with_name
592				options[#options + 1] = without_name
593			elseif value then
594				options[#options + 1] = without_name
595			else
596				options[#options + 1] = with_name
597			end
598
599			without_all_opts[name] = false
600			assert(fh:write(with_name .. '="YES"\n'))
601		else
602			options[#options + 1] = name
603		end
604	end
605
606	assert(fh:close())
607
608	local with_all_opts = config_options(srcconf_all)
609	local all_enforced_options, all_effect_options
610	local all_required_options = required_options()
611
612	all_enforced_options, all_effect_options = all_dependent_options(tmpdir,
613	    options, default_opts, with_all_opts, without_all_opts)
614
615	table.sort(options, option_comparator)
616	io.stdout:write(output_head)
617	for _, name in ipairs(options) do
618		local value
619
620		if name:match("^OPT_") then
621			goto skip
622		end
623		assert(name:match("^WITH"), "Name looks wrong: " .. name)
624		local describe_option = name
625
626		value = not not name:match("^WITHOUT")
627
628		-- Normalize name to MK_ for indexing into various other
629		-- arrays
630		name = "MK_" .. name:gsub("^[^_]+_", "")
631
632		print(".It Va " .. describe_option:gsub("^OPT_", ""))
633		if opt_descriptions[describe_option] then
634			io.stdout:write(opt_descriptions[describe_option])
635		else
636			io.stderr:write("Missing description for " ..
637			    describe_option .. "\n")
638		end
639
640		local enforced_options = all_enforced_options[describe_option]
641		local effect_options = all_effect_options[describe_option]
642
643		if different_defaults[name] ~= nil then
644			print([[.Pp
645This is a default setting on]])
646
647			local which_targets = {}
648			for tgt, tgt_options in pairs(target_defaults) do
649				if tgt_options[name] ~= value then
650					which_targets[#which_targets + 1] = tgt
651				end
652			end
653
654			table.sort(which_targets)
655			for idx, tgt in ipairs(which_targets) do
656				io.stdout:write(tgt)
657				if idx < #which_targets - 1 then
658					io.stdout:write(", ")
659				elseif idx == #which_targets - 1 then
660					io.stdout:write(" and ")
661				end
662			end
663			print(".")
664		end
665
666		-- Unset any implied options that are actually required.
667		for dep_opt in pairs(enforced_options) do
668			if all_required_options[dep_opt] then
669				enforced_options[dep_opt] = nil
670			end
671		end
672		if next(enforced_options) ~= nil then
673			print([[When set, it enforces these options:
674.Pp
675.Bl -item -compact]])
676
677			local sorted_dep_opt = {}
678			for dep_opt in pairs(enforced_options) do
679				sorted_dep_opt[#sorted_dep_opt + 1] = dep_opt
680			end
681
682			table.sort(sorted_dep_opt)
683			for _, dep_opt in ipairs(sorted_dep_opt) do
684				local dep_val = enforced_options[dep_opt]
685				local dep_prefix = (dep_val and "WITH_") or
686				    "WITHOUT_"
687				local dep_name = dep_opt:gsub("^MK_",
688				    dep_prefix)
689				print(".It")
690				print(".Va " .. dep_name)
691			end
692
693			print(".El")
694		end
695
696		if next(effect_options) ~= nil then
697			if next(enforced_options) ~= nil then
698				-- Remove any options that were previously
699				-- noted as enforced...
700				for opt, val in pairs(effect_options) do
701					if enforced_options[opt] == val then
702						effect_options[opt] = nil
703					end
704				end
705
706				-- ... and this could leave us with an empty
707				-- set.
708				if next(effect_options) == nil then
709					goto noenforce
710				end
711
712				print(".Pp")
713			end
714
715			print([[When set, these options are also in effect:
716.Pp
717.Bl -inset -compact]])
718
719			local sorted_dep_opt = {}
720			for dep_opt in pairs(effect_options) do
721				sorted_dep_opt[#sorted_dep_opt + 1] = dep_opt
722			end
723
724			table.sort(sorted_dep_opt)
725			for _, dep_opt in ipairs(sorted_dep_opt) do
726				local dep_val = effect_options[dep_opt]
727				local dep_prefix = (dep_val and "WITH_") or
728				    "WITHOUT_"
729				local not_dep_prefix = ((not dep_val) and "WITH_") or
730				    "WITHOUT_"
731				local dep_name = dep_opt:gsub("^MK_",
732				    dep_prefix)
733				local not_dep_name = dep_opt:gsub("^MK_",
734				    not_dep_prefix)
735
736				print(".It Va " .. dep_name)
737				print("(unless")
738				print(".Va " .. not_dep_name)
739				print("is set explicitly)")
740			end
741
742			print(".El")
743::noenforce::
744		end
745
746		if env_only_opts[name] ~= nil then
747			print([[.Pp
748This must be set in the environment, make command line, or
749.Pa /etc/src-env.conf ,
750not
751.Pa /etc/src.conf .]])
752		end
753		::skip::
754	end
755	print([[.El
756.Pp
757The following options accept a single value from a list of valid values.
758.Bl -tag -width indent]])
759	for _, name in ipairs(options) do
760		if name:match("^OPT_") then
761			local desc = opt_descriptions[name]
762
763			print(".It Va " .. name:gsub("^OPT_", ""))
764			if desc then
765				io.stdout:write(desc)
766			else
767				io.stderr:write("Missing description for " ..
768				    name .. "\n")
769			end
770		end
771	end
772	io.stdout:write(output_tail)
773end
774
775local tmpdir = "/tmp/makeman." .. unistd.getpid()
776
777if not lfs.mkdir(tmpdir) then
778	error("Failed to create tempdir " .. tmpdir)
779end
780
781-- Catch any errors so that we can properly clean up, then re-throw it.
782local ret, errobj = pcall(main, tmpdir)
783
784for fname in lfs.dir(tmpdir) do
785	if fname ~= "." and fname ~= ".." then
786		assert(os.remove(tmpdir .. "/" .. fname))
787	end
788end
789
790if not lfs.rmdir(tmpdir) then
791	assert(io.stderr:write("Failed to clean up tmpdir: " .. tmpdir .. "\n"))
792end
793
794if not ret then
795	io.stderr:write(errobj .. "\n")
796	os.exit(1)
797end
798