X-Git-Url: https://git.dogcows.com/gitweb?p=chaz%2Fyoink;a=blobdiff_plain;f=build%2Fconfig.lua;fp=build%2Fconfig.lua;h=fa522cf9247a31b0b530e5923d78fe63737d178b;hp=0000000000000000000000000000000000000000;hb=6c9943707d4f33035830eba0587a61a34eaecbc2;hpb=af88821a172c4dfd138b91b2a5148ae50b502fa2 diff --git a/build/config.lua b/build/config.lua new file mode 100644 index 0000000..fa522cf --- /dev/null +++ b/build/config.lua @@ -0,0 +1,998 @@ +-- +-- Yoink Build System +-- Execute this script with Lua to prepare the project for compiling. +-- +-- Copyright (c) 2011, Charles McGarvey +-- Distributable under the terms and conditions of the 2-clause BSD +-- license; see the file COPYING for a complete text of the license. +-- + + +--local function trace(event, line) + --local s = debug.getinfo(2).short_src + --print(s .. ":" .. line) +--end +--debug.sethook(trace, "l") + + +-- Convert the arguments into standard form. Each argument should be a +-- string. A table is returned with the arguments' standard form as keys. +-- If an argument has a value, it will correspond to the appropriate key in +-- the table. This function supports autoconf-style arguments, including: +-- 1. -h, --help produce a --help key with a value of true. +-- 2. --enable-feature=[yes|no|whatever], --disable-feature produce a +-- --enable-feature key with a value of true, false, or a string. +-- 3. --with-package=[yes|no|whatever], --without-feature produce a +-- --with-package key with a value of true, false, or a string. +-- 4. SYMBOL=VALUE produce a SYMBOL key with VALUE as the string value. +-- 5. Anything else will appear as a key in the table as-is with a value of +-- true. +-- If multiple arguments mapping to the same key are present in the +-- argument list, the last one is used. +local function parseArguments(...) + local result = {} + local filters = {} + + local function setPair(key, value) result[key] = value end + local function setTrue(key) result[key] = true end + local function addLibrarySearchPath(path) + package.path = path .. "/?.lua;" .. package.path + package.cpath = path .. "/?.so;" .. package.cpath + end + + table.insert(filters, {"^-h$", function() result["--help"] = true end}) + table.insert(filters, {"^-L(.*)$", addLibrarySearchPath}) + + table.insert(filters, {"^(--enable%-[%w_-]+)=?(.*)$", setPair}) + table.insert(filters, {"^--disable%-([%w_-]+)$", function(feature) + setPair("--enable-" .. feature, "no") + end}) + + table.insert(filters, {"^(--with%-[%w_-]+)=?(.*)$", setPair}) + table.insert(filters, {"^--without%-([%w_-]+)$", function(package) + setPair("--with-" .. package, "no") + end}) + + table.insert(filters, {"^([%w_-]+)=(.*)$", setPair}) + table.insert(filters, {"^([%w_-]+)$", setTrue}) + + for _,a in ipairs(arg) + do + for _,filter in pairs(filters) + do + local matches = {a:match(filter[1])} + if matches[1] then filter[2](unpack(matches)) break end + end + end + + return result +end + + +local arguments = parseArguments(unpack(arg)) +local interactive = arguments["--interactive"] +local rules = arguments["--rules"] or "options.lua" +local configfile = arguments["--configfile"] or "config.mk" +local printendmsg = arguments["--print-instructions"] +local exportHeader = arguments["--export-header"] +local exportTerms = arguments["--export-terms"] + + +local util = require "utility" +util.logfile = "config.log" + + +local printInfo = util.printer("%s - %s\n") +local printWarning = util.printer("\a%s - %s\n", "33") +local printError = util.printer("\a%s - %s\n", "31") +local function beginProcess(title, caption) + print(string.format("%s:\n", title)) + local updater = util.printer(" [%3d%%] %s\n") + updater(0, caption) + return function(progress, caption) + if progress ~= nil + then + if 0.0 <= progress and progress <= 1.0 then progress = progress * 100 end + return updater(progress, caption) + end + print() + return 0 + end +end + +local function loadDialog(name, configfile) + local dialog + local result, err = pcall(function() dialog = require "dialog" end) + if not result + then + printWarning(err) + return nil + end + dialog.title = string.format("%s - %s Configuration", configfile, name) + printInfo = dialog.msgbox + printWarning = dialog.msgbox + printError = dialog.msgbox + beginProcess = dialog.gauge + return dialog +end + + +local function topologicalSort(lookup) + local dependants = util.copy(lookup, "v", {v = function() return 0 end}) + for _,option in pairs(lookup) + do + for _,dep in ipairs(option:getDirectDependants()) + do + dependants[dep] = dependants[dep] + 1 + end + end + + local sorted = {} + local queued = {} + for symbol,count in pairs(dependants) + do + if count == 0 then table.insert(queued, symbol) end + end + while 0 < #queued + do + local symbol = table.remove(queued) + table.insert(sorted, symbol) + for _,dep in ipairs(lookup[symbol]:getDirectDependants()) + do + dependants[dep] = dependants[dep] - 1 + if dependants[dep] == 0 then table.insert(queued, dep) end + end + end + + local remaining = {} + for symbol,count in pairs(dependants) + do + if 0 < count then table.insert(remaining, symbol) end + end + if 0 < #remaining + then + printWarning("Q.A. Notice", "One or more circular dependencies were detected involving these symbols: " + .. table.concat(remaining, ", ")) + for _,symbol in ipairs(remaining) do table.insert(sorted, symbol) end + end + + return sorted +end + +local function checkSymbols(symbols) + local check = topologicalSort(symbols) + local isErr = false + local updateTask = function() end + if 1 < #check + then + updateTask = beginProcess("Checking Symbols", "Resolving dependencies...", 6, 65) + end + for i = 1, #check + do + local option = symbols[check[i]] + if option:validate() == nil then isErr = true end + updateTask(i / #check, + (option:getSymbol() .. ": " .. option:toExpandedString()):truncate(60)) + end + updateTask() + if isErr then return nil end + return true +end + + +--------------------------------------------------------------------------- +local Option = util.class() +--------------------------------------------------------------------------- + +function Option:__init(rule, lookup, objects) + self.name = rule.name + self.caption = rule.caption + self.help = rule.help + self.value = rule.value + self.symbol = rule.symbol + self.cmdline = rule.cmdline + self.config = rule.config + self.export = rule.export + self.visible = rule.visible + self.check = rule.check + self.temp = rule.temp + self.before = rule.before + self.after = rule.after + self.lookup = lookup or {} + self.objects = objects or {} + + if type(self.check) == "function" then setfenv(self.check, self.lookup) end + if type(self.visible) == "function" then setfenv(self.visible, self.lookup) end + + if self.symbol ~= nil + then + if self.lookup[self.symbol] ~= nil + then + printWarning("duplicate symbol defined:", self.symbol) + end + self.lookup[self.symbol] = self.value + self.objects[self.symbol] = self + end +end + +-- Get the symbol of the option. The symbol is used as a reference in the +-- string values of other options. It must be unique. +function Option:getSymbol() + return self.symbol +end + +-- Get the argument of the option. +function Option:getArg() + return self.cmdline +end + +-- Get the value of the option as a string. +function Option:toString() + return tostring(self.lookup[self.symbol]) +end + +-- Get the value of the option as an expanded string. For most types of +-- options, this is the same as Option:toString(). +function Option:toExpandedString() + return self:toString() +end + +function Option:getDirectDependants() + return self.before or {} +end + +function Option:getUnsortedDependants(dependants) + dependants = dependants or {} + if dependants[self.symbol] then return dependants end + dependants[self.symbol] = self + if self.before + then + for _,dep in ipairs(self.before) + do + local option = self.objects[dep] + if option + then + dependants = option:getUnsortedDependants(dependants) + else + printWarning("invalid dependency: " .. dep) + end + end + end + return dependants +end + +function Option:addDependant(dependency) + self.before = self.before or {} + table.insert(self.before, dependency) +end + +function Option:convertDependenciesToDependants() + if self.after + then + local dep = table.remove(self.after) + while dep ~= nil + do + local option = self.objects[dep] + if option + then + option:addDependant(self.symbol) + else + printWarning("invalid dependency: " .. dep) + end + dep = table.remove(self.after) + end + end +end + +function Option:getObjects() + return self.objects +end + +-- Check the value of the option for validity. +-- Returns a valid value based on the given value, or nil (with an error +-- message) if the value is invalid and could not be converted to a valid +-- value. +function Option:validate(value) + local err + if value == nil then value = self.lookup[self.symbol] end + self.problem = nil + if type(self.check) == "function" + then + value, err = self.check(value) + if value == nil then self.problem = err return nil, err end + end + self.lookup[self.symbol] = value + return value, err +end + +-- Set the value of the option to the value in a new lookup table. +function Option:applyTable(lookup) +end + +-- Set the value of the option based on a table of argument flags. +function Option:applyArguments() +end + +-- Set the value of the option. +function Option:set(value) + self.lookup[self.symbol] = value + if checkSymbols(self:getUnsortedDependants()) + then + return self.lookup[self.symbol] + end +end + +-- Write the symbol and current value to the config file. +function Option:writeSymbol(fd) + if self.symbol ~= nil and not self.temp + then + fd:write(util.align(self.symbol, "= " .. tostring(self.lookup[self.symbol]), 16) .. "\n") + --fd:write(string.format("%s\t= %s\n", self.symbol, tostring(self.lookup[self.symbol]))) + end +end + +-- Write the option value to the config header file. +function Option:writeCppLine(fd) +end + +-- Write the option value as a string replacement to the sed file. +function Option:writeSedLine(fd) + if self.export + then + local value = self:toExpandedString():gsub("/", "\\/") + local function writeLine(key) + key = tostring(key):gsub("/", "\\/") + fd:write(string.format("s/@%s@/%s/g\n", key, value)) + end + if type(self.export) == "table" + then + for _,key in ipairs(self.export) do writeLine(key) end + else + writeLine(self.export) + end + end +end + +-- Run an interactive menu with the given dialog context. The type of menu +-- run will depend on the type of option. +function Option:showMenu(dialog) +end + +-- Get whether or not there is a problem with the current value of the +-- option. +function Option:isValid() + return type(self.problem) ~= "string" +end + +-- Get a human-readable description of the problem(s) this option faces +-- before it can be considered valid. +function Option:getProblem() + return self.problem +end + +-- Get the name to be used for this option in the interactive menu. +function Option:getMenuItem() + if not self:isValid() then return self.name .. " " end + return self.name +end + +-- Get the label used to represent this option in a menu. +function Option:getLabel() + return "" +end + +-- Get whether or not this option should be visible in a menu of options. +-- Returns true if option is visible, false otherwise. +function Option:isVisible() + if type(self.visible) == "function" then return self.visible() end + return true +end + +-- If the option has an argument flag, print out a line with information +-- about the purpose of this option. +function Option:printHelpLine() + if self.cmdline ~= nil + then + print(util.align(" " .. self:getArg(), self.caption, 32)) + end +end + +function Option:getHelpText() + local value = self:toString() + local help = self.help or "No help available.\n" + local name = self.name or "Unnamed" + local symbol = self:getSymbol() or "None" + local arg = self:getArg() or "None" + + local problem = self:getProblem() + if problem then problem = "\nProblem(s):\n" .. problem + else problem = "" end + + return [[ + +]] .. help .. [[ + + Option Name: ]] .. name .. [[ + + Symbol: ]] .. symbol .. [[ + + Current Value: ]] .. value .. [[ + + Argument: ]] .. arg .. [[ + +]] .. problem +end + + +--------------------------------------------------------------------------- +local StringOption = util.class(Option) +--------------------------------------------------------------------------- + +-- Get the expanded option value. Symbols which begin with dollar signs +-- will be substituted for their values. +function StringOption:toExpandedString() + local function expand(value) + _, value, count = pcall(string.gsub, value, "%$%(([%w_]+)%)", self.lookup) + if not count then count = 0 end + return value, count + end + local value = self.lookup[self.symbol] + local iterations = 0 + while iterations < 8 + do + local count = 0 + value, count = expand(value) + if count == 0 then return value end + iterations = iterations + 1 + end + return value +end + +function StringOption:applyTable(lookup) + local value = lookup[self.symbol] + if type(value) == "string" then self.lookup[self.symbol] = tostring(value) end +end + +function StringOption:applyArguments(args) + local value = args[self.cmdline] + if value ~= nil then self:validate(tostring(value)) end +end + +function StringOption:writeCppLine(fd) + if self.config + then + local value = self:toExpandedString() + local function writeLine(key) + if type(self.value) == "string" + then + fd:write(string.format("#define %s %q\n", key, value)) + else + fd:write(string.format("#define %s %s\n", key, value)) + end + end + if type(self.config) == "table" + then + for _,key in ipairs(self.config) do writeLine(key) end + else + writeLine(self.config) + end + end +end + +function StringOption:showMenu(dialog) + local code, item = dialog.inputbox(self.name, self.caption, self.lookup[self.symbol]) + if code == 0 and item ~= self.lookup[self.symbol] + then + return self:set(item) + end +end + +function StringOption:getLabel() + return self:toExpandedString() +end + + +--------------------------------------------------------------------------- +local EnumOption = util.class(StringOption) +--------------------------------------------------------------------------- + +-- An enumeration option is like a string, but it can only hold certain +-- pre-determined values. In a rule, it is distinguished by its value key +-- which refers to an array of the possible (string) values. +function EnumOption:__init(rule, lookup, objects) + Option.__init(self, rule, lookup, objects) + self.lookup[self.symbol] = self.value[1] +end + +function EnumOption:getArg() + if self.cmdline == nil then return nil end + return self.cmdline .. "=[" .. table.concat(self.value, "|") .. "]" +end + +function EnumOption:validate(str) + str = str or self.lookup[self.symbol] + for _,value in ipairs(self.value) + do + if value == str then return Option.validate(self, str) end + end + self.problem = "The assigned value (" .. str .. ") is not a valid option." + return nil, self.problem +end + +function EnumOption:applyArguments(args) + -- Just like applying arguments for strings, but if the argument is + -- false, assume the first value should be assigned. + local value = args[self.cmdline] + if value == false then self:validate(self.value[1]) return end + StringOption.applyArguments(self, args) +end + +function EnumOption:writeCppLine(fd) + if self.config + then + local value = self:toString():upper() + local function writeLine(key) + key = tostring(key) .. "_" .. value + fd:write(string.format("#define %s 1\n", key)) + end + if type(self.config) == "table" + then + for _,key in ipairs(self.config) do writeLine(key) end + else + writeLine(self.config) + end + end +end + +function EnumOption:showMenu(dialog) + local menuitems = {} + for _,value in ipairs(self.value) + do + table.insert(menuitems, {value, ""}) + end + local code, item = dialog.menu( + self.name, + self.caption, + menuitems, + self.lookup[self.symbol], + {["--ok-label"] = "Select"} + ) + if code == 0 and item ~= self.lookup[self.symbol] + then + return self:set(item) + end +end + +function EnumOption:getLabel() + return "[" .. StringOption.getLabel(self) .. "]" +end + + +--------------------------------------------------------------------------- +local BooleanOption = util.class(Option) +--------------------------------------------------------------------------- + +function BooleanOption:toString() + if self.lookup[self.symbol] then return "Yes" else return "No" end +end + +function BooleanOption:applyTable(lookup) + local value = lookup[self.symbol] + if type(value) == "boolean" then self.lookup[self.symbol] = value end +end + +function BooleanOption:applyArguments(args) + local value = args[self.cmdline] + if value == "yes" or value == "" then value = true end + if value == "no" then value = false end + if type(value) == "boolean" then self:validate(value) end +end + +function BooleanOption:writeCppLine(fd) + if self.config + then + local value = self.lookup[self.symbol] + local function writeLine(key) + -- Reverse the value if key starts with a bang. + local value = value + if key:byte(1) == 33 then key = key:sub(2) value = not value end + if value + then + fd:write("#define " .. key .. " 1\n") + else + fd:write("/* #undef " .. key .. " */\n") + end + end + if type(self.config) == "table" + then + for _,key in ipairs(self.config) do writeLine(key) end + else + writeLine(self.config) + end + end +end + +function BooleanOption:showMenu(dialog) + local item = not self.lookup[self.symbol] + return self:set(item) +end + +function BooleanOption:getLabel() + if self.lookup[self.symbol] then return "[X]" else return "[ ]" end +end + + +--------------------------------------------------------------------------- +local NullOption = util.class(Option) +--------------------------------------------------------------------------- + +-- Null options don't really hold any useful information; they just +-- translate to a blank line in the menuconfig. Any non-table object in a +-- rule will become this type of option. +function NullOption:__init() + self.name = "" + self.lookup = {} +end + +function NullOption:validate() + return true +end +function NullOption:isVisible() + return true +end + + +--------------------------------------------------------------------------- +local GroupOption = util.class(Option) +--------------------------------------------------------------------------- + +local function optionFactory(rule, lookup, objects) + local jump = setmetatable({ + string = StringOption, + number = StringOption, + table = EnumOption, + boolean = BooleanOption + }, { + __index = function() return GroupOption end + }) + if type(rule) == "table" + then + return jump[type(rule.value)](rule, lookup, objects) + else + -- If the rule is not a table, just insert a placeholder option for + -- a blank line. + return NullOption() + end +end + +-- A GroupOption is not really an option itself, but a group of options. +function GroupOption:__init(rule, lookup, objects) + Option.__init(self, rule, lookup, objects) + self.children = {} + + for _,child in ipairs(rule) + do + table.insert(self.children, optionFactory(child, self.lookup, self.objects)) + end +end + +function GroupOption:toString() + return "n/a" +end + +-- Call the method with an arbitrary number of arguments to each +-- sub-option. +function GroupOption:recurse(method, ...) + for _,child in ipairs(self.children) + do + child[method](child, unpack(arg)) + end +end + +function GroupOption:convertDependenciesToDependants() + self:recurse("convertDependenciesToDependants") +end + +-- Validate each sub-option in order. The validation will short-circuit +-- and return nil (with an error message) upon the first failed validation. +function GroupOption:validate() + for _,child in ipairs(self.children) + do + local result, err = child:validate() + if result == nil then return result, err end + end + return true +end + +function GroupOption:isValid() + for _,child in ipairs(self.children) + do + if not child:isValid() then return false end + end + return true +end + +function GroupOption:getProblem() + local problems = "" + for _,child in ipairs(self.children) + do + local problem = child:getProblem() + if problem + then + local symbol = child:getSymbol() + if symbol + then + problems = problems .. util.align(symbol .. ":", problem, 16) .. "\n" + else + problems = problems .. problem .. "\n" + end + util.align(symbol, problem, 16) + end + end + if problems == "" then return nil end + return util.trim(problems) +end + +function GroupOption:applyTable(lookup) + self:recurse("applyTable", lookup) +end + +function GroupOption:applyArguments(args) + self:recurse("applyArguments", args) +end + +function GroupOption:writeSymbol(fd) + if self.name ~= nil then + fd:write(string.format("\n# %s\n", self.name)) + self:recurse("writeSymbol", fd) + end +end + +function GroupOption:writeCppLine(fd) + self:recurse("writeCppLine", fd) +end + +function GroupOption:writeSedLine(fd) + self:recurse("writeSedLine", fd) +end + + +function GroupOption:showMenu(dialog) + local running, dirty, selected = true + while running + do + local menuitems = {} + for _,value in ipairs(self.children) + do + if type(value) ~= "table" + then + table.insert(menuitems, value) + elseif type(value.name) == "string" and value:isVisible() + then + local name = value:getMenuItem() + local label = value:getLabel() + local caption = "" + if type(value.caption) == "string" + then + caption = value.caption + menuitems["HELP " .. value.caption] = value + end + menuitems[name] = value + table.insert(menuitems, {name, label, caption}) + end + end + + local code, item = dialog.menu( + self.name, + self.caption, + menuitems, + selected, + { + ["--ok-label"] = "Select", + ["--cancel-label"] = "Exit", + ["--item-help"] = true, + ["--help-button"] = true + } + ) + + if code == 0 + then + local value = menuitems[item] + if type(value) == "table" + then + if value:showMenu(dialog) ~= nil then dirty = "changed" end + selected = value:getMenuItem() + else + selected = value + end + elseif code == 2 + then + local value = menuitems[item] + if value + then + dialog.msgbox(value.name, value:getHelpText(), {["--no-collapse"] = true}) + selected = value:getMenuItem() + else + dialog.msgbox("No Help", + "Sorry, no help is available for this option.") + end + else + running = false + end + end + return dirty +end + +function GroupOption:getLabel() + return "-->" +end + +function GroupOption:printHelpLine() + if self:isVisible() and self.name ~= nil then + print(string.format("\n%s:", self.name)) + self:recurse("printHelpLine") + end +end + + +-- Print arguments and usage information for all of the sub-options. +function GroupOption:printHelp(name) + print([[ + +This script prepares ]] .. name .. [[ for building on your system. +Usage: ./configure [OPTION]... [VAR=VALUE]...]]) + + self:printHelpLine() + print() +end + +-- Set values of the sub-options based on a table that is loaded from a +-- file. The table is formatted as one or more lines with keys and values +-- separated by an equal sign. The pound sign (#) serves to start a +-- comment on the line. The first equal sign on a line is used as the +-- separator, so keys cannot have an equal sign. +function GroupOption:loadFromFile(filepath) + local lookup = {} + local fd = io.open(filepath) + if fd + then + for line in fd:lines() + do + if not line:find("^%s*#") + then + key, value = line:match("^%s*(%S.-)%s*=%s*(.*)%s*") + if value + then + if value:upper() == "TRUE" then value = true + elseif value:upper() == "FALSE" then value = false end + end + if key then lookup[key] = value end + end + end + fd:close() + end + self:applyTable(lookup) +end + +-- Save a table with the current values of the sub-options to a file. The +-- file can be reloaded with GroupOption:loadFromFile. +function GroupOption:saveToFile(filepath) + local fd = io.open(filepath, "w") + if fd + then + fd:write(string.format("\n# Auto-generated: %s", os.date())) + self:writeSymbol(fd) + fd:write("\n") + else + printError("couldn't save config file to:", filepath) + end +end + +-- Exports the config header file. The key-value pairs of sub-options with +-- "config" defined will be written to the file. +function GroupOption:exportHeader(filepath) + local fd = io.open(filepath, "w") + self:writeCppLine(fd) + fd:close() +end + +-- Exports the search 'n replace file. The key-value pairs of sub-options +-- with "export" defined will be written to the file. +function GroupOption:exportTerms(filepath) + local fd = io.open(filepath, "w") + self:writeSedLine(fd) + fd:close() +end + +-- Run menuconfig. This is different from showMenu in that this method +-- tracks changes and returns an action: save, cancel, nochange or error. +function GroupOption:runMenu() + local result, dirty = nil, false + while result == nil + do + if self:showMenu(dialog) ~= nil then dirty = true end + if not self:isValid() + then + local code = dialog.yesno("Oh drat!", + "There is at least one problem with the configuration, marked with , and the configuration will not be saved. Do you really want to exit without saving?", + { + ["--extra-button"] = false, + ["--ok-label"] = "Exit", + ["--cancel-label"] = "Back" + }) + if code == 0 or code == 255 then result = "error" end + elseif dirty + then + local code = dialog.yesno("Save Changes?", + "Your configuration has been altered. Do you want to save the changes?", + { + ["--ok-label"] = "Save", + ["--cancel-label"] = "Back", + ["--extra-button"] = true, + ["--extra-label"] = "Exit" + }) + if code == 0 then result = "save" + elseif code == 3 or code == 255 then result = "cancel" end + else + result = "nochange" + end + end + return result +end + + +local loadFn = assert(loadfile(rules)) +local name, rules = loadFn() + +local options = optionFactory(rules) +if arguments["--help"] then options:printHelp(name) os.exit() end + +options:loadFromFile(configfile) +if exportHeader or exportTerms +then + if exportHeader then options:exportHeader(exportHeader) end + if exportTerms then options:exportTerms(exportTerms) end + os.exit() +end + +print() +printInfo(name, "Preparing for building and installation on your system.\n") +options:applyArguments(arguments) + +if interactive then loadDialog(name, configfile) end + +options:convertDependenciesToDependants() +checkSymbols(options:getObjects()) + +if dialog +then + local action = options:runMenu() + if action == "exit" or action == "error" + then + print("configuration aborted by user request") + os.exit(1) + else + options:saveToFile(configfile) + print("configuration saved to " .. configfile) + end +elseif options:isValid() +then + options:saveToFile(configfile) +else + printError("Uh oh!", [[ +There is at least one unresolved problem with the configuration. +]] .. options:getProblem() .. "\n") + os.exit(2) +end + +if printendmsg ~= "no" +then + printInfo("Configuration complete!", [[ +To finish the installation, type: + make + make install +]]) +end + +-- vi:ts=4 sw=4 tw=75 +