-- -- 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") 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