diff --git a/tools/build/options/makeman.lua b/tools/build/options/makeman.lua new file mode 100644 --- /dev/null +++ b/tools/build/options/makeman.lua @@ -0,0 +1,669 @@ +-- +-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD +-- +-- Copyright (c) 2023 Kyle Evans +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions +-- are met: +-- 1. Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- 2. Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- +-- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +-- ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +-- SUCH DAMAGE. +-- + +local libgen = require('posix.libgen') +local lfs = require('lfs') +local stdlib = require('posix.stdlib') +local unistd = require('posix.unistd') +local sys_wait = require('posix.sys.wait') + +local curdate = os.date("%B %e, %Y") + +local output_head = ".\\\" DO NOT EDIT-- this file is @" .. [[generated by tools/build/options/makeman. +.Dd ]] .. curdate .. [[ + +.Dt SRC.CONF 5 +.Os +.Sh NAME +.Nm src.conf +.Nd "source build options" +.Sh DESCRIPTION +The +.Nm +file contains variables that control what components will be generated during +the build process of the +.Fx +source tree; see +.Xr build 7 . +.Pp +The +.Nm +file uses the standard makefile syntax. +However, +.Nm +should not specify any dependencies to +.Xr make 1 . +Instead, +.Nm +is to set +.Xr make 1 +variables that control the aspects of how the system builds. +.Pp +The default location of +.Nm +is +.Pa /etc/src.conf , +though an alternative location can be specified in the +.Xr make 1 +variable +.Va SRCCONF . +Overriding the location of +.Nm +may be necessary if the system-wide settings are not suitable +for a particular build. +For instance, setting +.Va SRCCONF +to +.Pa /dev/null +effectively resets all build controls to their defaults. +.Pp +The only purpose of +.Nm +is to control the compilation of the +.Fx +source code, which is usually located in +.Pa /usr/src . +As a rule, the system administrator creates +.Nm +when the values of certain control variables need to be changed +from their defaults. +.Pp +In addition, control variables can be specified +for a particular build via the +.Fl D +option of +.Xr make 1 +or in its environment; see +.Xr environ 7 . +.Pp +The environment of +.Xr make 1 +for the build can be controlled via the +.Va SRC_ENV_CONF +variable, which defaults to +.Pa /etc/src-env.conf . +Some examples that may only be set in this file are +.Va WITH_DIRDEPS_BUILD , +and +.Va WITH_META_MODE , +and +.Va MAKEOBJDIRPREFIX +as they are environment-only variables. +.Pp +The values of variables are ignored regardless of their setting; +even if they would be set to +.Dq Li FALSE +or +.Dq Li NO . +The presence of an option causes +it to be honored by +.Xr make 1 . +.Pp +This list provides a name and short description for variables +that can be used for source builds. +.Bl -tag -width indent +]] + +local output_tail = [[.El +.Sh FILES +.Bl -tag -compact -width Pa +.It Pa /etc/src.conf +.It Pa /etc/src-env.conf +.It Pa /usr/share/mk/bsd.own.mk +.El +.Sh SEE ALSO +.Xr make 1 , +.Xr make.conf 5 , +.Xr build 7 , +.Xr ports 7 +.Sh HISTORY +The +.Nm +file appeared in +.Fx 7.0 . +.Sh AUTHORS +This manual page was autogenerated by +.An tools/build/options/makeman . +]] + +local scriptdir = libgen.dirname(stdlib.realpath(arg[0])) +local srcdir = stdlib.realpath(scriptdir .. "/../../../") +local make = "make -C " .. srcdir .. " -m " .. srcdir .. "/share/mk " .. + "__MAKE_CONF=/dev/null SRCCONF=/dev/null" + +local function native_target() + local fh = io.popen(make .. " MK_AUTO_OBJ=no " .. + "-V MACHINE -V MACHINE_ARCH") + + local arch, machine_arch + for x in fh:lines() do + if not arch then + arch = x + elseif not machine_arch then + machine_arch = x + end + end + + assert(fh:close()) + return arch .. "/" .. machine_arch +end + +local function src_targets() + local targets = {} + targets[native_target()] = true + + local fh = io.popen(make .. " MK_AUTO_OBJ=no targets") + local curline = 0 + + for line in fh:lines() do + curline = curline + 1 + if curline ~= 1 then + local arch = line:match("[^%s]+/[^%s]+") + + -- Make sure we don't roll over our default arch + if arch and not targets[arch] then + targets[arch] = false + end + end + end + + return targets +end + +local function config_options(srcconf, env, take_dupes) + srcconf = srcconf or "/dev/null" + env = env or "" + + local fh = io.popen(make .. " .MAKE.MODE=normal showconfig " .. + "SRC_ENV_CONF=" .. srcconf .. " " .. env) + local options = {} + + for opt in fh:lines() do + if opt:match("= .+") then + local name = opt:match("MK_[%a%d_]+") + local val = opt:match("= .+"):sub(3) + + -- Some settings, e.g., MK_INIT_ALL_ZERO, may end up + -- output twice for some reason that I haven't dug into; + -- take the first value. In some circumstances, though, + -- we do make an exception and actually want to take the + -- latest. + if take_dupes or options[name] == nil then + options[name] = val == "yes" + end + end + end + + fh:close() + return options +end + +local function env_only_options() + local fh = io.popen(make .. " MK_AUTO_OBJ=no -V __ENV_ONLY_OPTIONS") + local options = {} + local output = fh:read("a") + fh:close() + + for opt in output:gmatch("[^%s]+") do + options["MK_" .. opt] = true + end + + return options +end + +local function config_description(option_name) + local fh = io.open(scriptdir .. "/" .. option_name) + local desc + + if fh then + desc = "" + for line in fh:lines() do + if not line:match("%$FreeBSD%$") then + desc = desc .. line .. "\n" + end + end + + assert(fh:close()) + end + + return desc +end + +local function config_descriptions(options) + local desc = {} + for name, _ in pairs(options) do + local basename = name:gsub("^MK_", "") + local with_name = "WITH_" .. basename + local without_name = "WITHOUT_" .. basename + + desc[with_name] = config_description(with_name) + desc[without_name] = config_description(without_name) + end + return desc +end + +local function dependent_options(tmpdir, option_name, all_opts, omit_others) + local opt_sense = not not option_name:match("^WITH_") + local base_option_name = option_name:gsub("^[^_]+_", "") + local prefix = (opt_sense and "WITHOUT_") or "WITH_" + + local srcconf = tmpdir .. "/src-" ..prefix .. "ALL_" .. + option_name .. ".conf" + local fh = assert(io.open(srcconf, "w+")) + + fh:write(option_name .. "=\"YES\"\n") + if not omit_others then + for opt, value in pairs(all_opts) do + local base_opt = opt:gsub("^MK_", "") + + if base_opt ~= base_option_name then + local opt_prefix = (value and "WITH_") or "WITHOUT_" + fh:write(opt_prefix .. base_opt .. "=\"YES\"\n") + end + end + end + assert(fh:close()) + + local option_name_key = "MK_" .. base_option_name + local options = config_options(srcconf, nil, omit_others) + for name, value in pairs(options) do + if name == option_name_key or value == all_opts[name] then + options[name] = nil + end + end + + return options +end + +local function export_option_table(fd, name, options) + unistd.write(fd, name .. " = {") + for k, v in pairs(options) do + v = (v and "true") or "false" + unistd.write(fd, "['" .. k .. "'] = " .. v .. ",") + end + unistd.write(fd, "}") +end + +local function all_dependent_options(tmpdir, options, default_opts, + with_all_opts, without_all_opts) + local all_enforced_options = {} + local all_effect_options = {} + local children = {} + + for _, name in ipairs(options) do + local rfd, wfd = assert(unistd.pipe()) + local pid = assert(unistd.fork()) + + if pid == 0 then + -- We need to pcall() this so that errors bubble up to + -- our _exit() call rather than the main exit. + local ret, errobj = pcall(function() + unistd.close(rfd) + + local compare_table + if name:match("^WITHOUT") then + compare_table = with_all_opts + else + compare_table = without_all_opts + end + + -- List of knobs forced on by this one + local enforced_options = dependent_options(tmpdir, name, + compare_table) + -- List of knobs implied by this by one (once additionally + -- filtered based on enforced_options values) + local effect_options = dependent_options(tmpdir, name, + default_opts, true) + + export_option_table(wfd, "enforced_options", + enforced_options) + export_option_table(wfd, "effect_options", + effect_options) + end) + + io.stderr:write(".") + + if ret then + unistd._exit(0) + else + unistd.write(wfd, errobj) + unistd._exit(1) + end + end + + unistd.close(wfd) + children[pid] = {name, rfd} + end + + while next(children) ~= nil do +::again:: + local pid, status, exitcode = sys_wait.wait(-1) + + if status ~= "exited" then + goto again + end + + local info = children[pid] + children[pid] = nil + + local name = info[1] + local rfd = info[2] + local buf = '' + local rbuf, sz + + -- Drain the pipe + rbuf = unistd.read(rfd, 512) + while #rbuf ~= 0 do + buf = buf .. rbuf + rbuf = unistd.read(rfd, 512) + end + + unistd.close(rfd) + + if exitcode ~= 0 then + error("Child " .. pid .. " failed, buf: " .. buf) + end + + -- The child has written a pair of tables named enforced_options + -- and effect_options to the pipe. We'll load the pipe buffer + -- as a string and then yank these out of the clean environment + -- that we execute the chunk in. + local child_env = {} + local res, err = pcall(load(buf, "child", "t", child_env)) + + all_enforced_options[name] = child_env["enforced_options"] + all_effect_options[name] = child_env["effect_options"] + end + + io.stderr:write("\n") + return all_enforced_options, all_effect_options +end + +local function get_defaults(target_archs, native_default_opts) + local target_defaults = {} + -- Set of options with differing defaults in some archs + local different_defaults = {} + + for tgt, dflt in pairs(target_archs) do + if dflt then + local native_copy = {} + for opt, val in pairs(native_default_opts) do + native_copy[opt] = val + end + target_defaults[tgt] = native_copy + goto skip + end + + local target = tgt:gsub("/.+$", "") + local target_arch = tgt:gsub("^.+/", "") + + local target_opts = config_options(nil, "TARGET=" .. target .. + " TARGET_ARCH=" .. target_arch) + + for opt, val in pairs(target_opts) do + if val ~= native_default_opts[opt] then + different_defaults[opt] = true + end + end + + target_defaults[tgt] = target_opts +::skip:: + end + + for opt in pairs(native_default_opts) do + if different_defaults[opt] == nil then + for _, opts in pairs(target_defaults) do + opts[opt] = nil + end + end + end + + for tgt, opts in pairs(target_defaults) do + local val = opts["MK_ACPI"] + + if val ~= nil then + print(" - " .. tgt .. ": " .. ((val and "yes") or "no")) + end + end + + return target_defaults, different_defaults +end + +local function option_comparator(lhs, rhs) + -- Convert both options to the base name, compare that instead unless + -- they're the same option. For the same option, we just want to get + -- ordering between WITH_/WITHOUT_ correct. + local base_lhs = lhs:gsub("^[^_]+_", "") + local base_rhs = rhs:gsub("^[^_]+_", "") + + if base_lhs == base_rhs then + return lhs < rhs + else + return base_lhs < base_rhs + end +end + +local function main(tmpdir) + io.stderr:write("building src.conf.5 man page from files in " .. + scriptdir .. "\n") + + local env_only_opts = env_only_options() + local default_opts = config_options() + local opt_descriptions = config_descriptions(default_opts) + local srcconf_all = tmpdir .. "/src-all-enabled.conf" + local fh = io.open(srcconf_all, "w+") + local all_targets = src_targets() + local target_defaults, different_defaults = get_defaults(all_targets, + default_opts) + local options = {} + local without_all_opts = {} + + for name, value in pairs(default_opts) do + local base_name = name:gsub("^MK_", "") + local with_name = "WITH_" .. base_name + local without_name = "WITHOUT_" .. base_name + + -- If it's differently defaulted on some architectures, we'll + -- split it into WITH_/WITHOUT_ just to simplify some later + -- bits. + if different_defaults[name] ~= nil then + options[#options + 1] = with_name + options[#options + 1] = without_name + elseif value then + options[#options + 1] = without_name + else + options[#options + 1] = with_name + end + + without_all_opts[name] = false + assert(fh:write(with_name .. '="YES"\n')) + end + + assert(fh:close()) + + local with_all_opts = config_options(srcconf_all) + local all_enforced_options, all_effect_options + + all_enforced_options, all_effect_options = all_dependent_options(tmpdir, + options, default_opts, with_all_opts, without_all_opts) + + table.sort(options, option_comparator) + io.stdout:write(output_head) + for _, name in ipairs(options) do + local value + + assert(name:match("^WITH")) + local describe_option = name + + value = not not name:match("^WITHOUT") + + -- Normalize name to MK_ for indexing into various other + -- arrays + name = "MK_" .. name:gsub("^[^_]+_", "") + + print(".It Va " .. describe_option) + if opt_descriptions[describe_option] then + io.stdout:write(opt_descriptions[describe_option]) + else + io.stderr:write("Missing description for " .. + describe_option .. "\n") + end + + local enforced_options = all_enforced_options[describe_option] + local effect_options = all_effect_options[describe_option] + + if different_defaults[name] ~= nil then + print([[.Pp +This is a default setting on]]) + + local which_targets = {} + for tgt, tgt_options in pairs(target_defaults) do + if tgt_options[name] ~= value then + which_targets[#which_targets + 1] = tgt + end + end + + table.sort(which_targets) + for idx, tgt in ipairs(which_targets) do + io.stdout:write(tgt) + if idx < #which_targets - 1 then + io.stdout:write(", ") + elseif idx == #which_targets - 1 then + io.stdout:write(" and ") + end + end + print(".") + end + + if next(enforced_options) ~= nil then + print([[When set, it enforces these options: +.Pp +.Bl -item -compact]]) + + local sorted_dep_opt = {} + for dep_opt in pairs(enforced_options) do + sorted_dep_opt[#sorted_dep_opt + 1] = dep_opt + end + + table.sort(sorted_dep_opt) + for _, dep_opt in ipairs(sorted_dep_opt) do + local dep_val = enforced_options[dep_opt] + local dep_prefix = (dep_val and "WITH_") or + "WITHOUT_" + local dep_name = dep_opt:gsub("^MK_", + dep_prefix) + print(".It") + print(".Va " .. dep_name) + end + + print(".El") + end + + if next(effect_options) ~= nil then + if next(enforced_options) ~= nil then + -- Remove any options that were previously + -- noted as enforced... + for opt, val in pairs(effect_options) do + if enforced_options[opt] == val then + effect_options[opt] = nil + end + end + + -- ... and this could leave us with an empty + -- set. + if next(effect_options) == nil then + goto noenforce + end + + print(".Pp") + end + + print([[When set, these options are also in effect: +.Pp +.Bl -inset -compact]]) + + local sorted_dep_opt = {} + for dep_opt in pairs(effect_options) do + sorted_dep_opt[#sorted_dep_opt + 1] = dep_opt + end + + table.sort(sorted_dep_opt) + for _, dep_opt in ipairs(sorted_dep_opt) do + local dep_val = effect_options[dep_opt] + local dep_prefix = (dep_val and "WITH_") or + "WITHOUT_" + local not_dep_prefix = ((not dep_val) and "WITH_") or + "WITHOUT_" + local dep_name = dep_opt:gsub("^MK_", + dep_prefix) + local not_dep_name = dep_opt:gsub("^MK_", + not_dep_prefix) + + print(".It Va " .. dep_name) + print("(unless") + print(".Va " .. not_dep_name) + print("is set explicitly)") + end + + print(".El") +::noenforce:: + end + + if env_only_opts[name] ~= nil then + print([[.Pp +This must be set in the environment, make command line, or +.Pa /etc/src-env.conf , +not +.Pa /etc/src.conf .]]) + end + end + io.stdout:write(output_tail) +end + +local tmpdir = "/tmp/makeman." .. unistd.getpid() + +if not lfs.mkdir(tmpdir) then + error("Failed to create tempdir " .. tmpdir) +end + +-- Catch any errors so that we can properly clean up, then re-throw it. +local ret, errobj = pcall(main, tmpdir) + +for fname in lfs.dir(tmpdir) do + if fname ~= "." and fname ~= ".." then + assert(os.remove(tmpdir .. "/" .. fname)) + end +end + +if not lfs.rmdir(tmpdir) then + assert(io.stderr:write("Failed to clean up tmpdir: " .. tmpdir .. "\n")) +end + +if not ret then + io.stderr:write(errobj .. "\n") + os.exit(1) +end