--!A cross-platform build utility based on Lua -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- -- @author ruki -- @file option.lua -- -- define module: option local option = {} -- load modules local cli = require("base/cli") local table = require("base/table") local tty = require("base/tty") local colors = require("base/colors") local text = require("base/text") -- translate the menu function option._translate(menu) local submenus_all = {} for k, submenu in pairs(menu) do if type(submenu) == "function" then local ok, _submenus_or_errors = pcall(submenu) if ok and _submenus_or_errors then for k, m in pairs(_submenus_or_errors) do submenus_all[k] = m end else return false, (_submenus_or_errors or "translate option menu failed!") end else submenus_all[k] = submenu end end table.copy2(menu, submenus_all) option._MENU = menu return true end -- show menu in narrow mode? function option._modenarrow() local width = os.getwinsize().width return width > 0 and width < 60 end -- get the top context function option._context() local contexts = option._CONTEXTS if contexts then return contexts[#contexts] end end -- save option context (push a new context onto the stack) -- -- @param taskname the task name (optional) -- @return the new context -- function option.save(taskname) option._CONTEXTS = option._CONTEXTS or {} local context = {options = {}, defaults = {}, taskname = taskname} if taskname then context.defaults = option.defaults(taskname) or context.defaults end table.insert(option._CONTEXTS, context) return context end -- restore option context (pop the current context from the stack) function option.restore() if option._CONTEXTS then table.remove(option._CONTEXTS) end end -- the command line function option.cmdline() if not xmake._ARGS then xmake._ARGS = os.args(xmake._ARGV) end return "xmake " .. xmake._ARGS end -- init the option function option.init(menu) -- translate menu local ok, errors = option._translate(menu) if not ok then return false, errors end -- the main menu local main = option.taskmenu("main") assert(main) -- new top context local context = option.save() assert(context) -- check command if xmake._COMMAND then -- find the current task and save it for taskname, taskinfo in pairs(main.tasks) do if taskname == xmake._COMMAND or taskinfo.shortname == xmake._COMMAND then context.taskname = taskname break end end -- not found? if not context.taskname or not menu[context.taskname] then option.show_main() return false, "invalid task: " .. xmake._COMMAND end end local options = table.wrap(option.taskmenu().options) -- parse remain parts local results, err = option.parse(xmake._COMMAND_ARGV, options, { populate_defaults = false }) if not results then option.show_menu(context.taskname) return false, err end -- finish parsing context.options = results -- init the default value option.populate_defaults(options, context.defaults) return true end -- parse arguments with the given options definition -- -- @param argv the arguments array or string -- @param options the options definition table -- @param opt the description and usage strings -- @return the parsed options table -- function option.parse(argv, options, opt) assert(argv and options) opt = opt or { populate_defaults = true } -- parse arguments local results = {} local flags = {} for _, o in ipairs(options) do -- get mode and name local mode = o[3] local name = o[2] assert(o and ((mode ~= "v" and mode ~= "vs") or name)) -- fill short flags if o[3] == "k" and o[1] then table.insert(flags, o[1]) end end -- run parser local pargs = cli.parsev(argv, flags) -- save parse results for i, arg in ipairs(pargs) do if arg.type == "option" or arg.type == "flag" then -- find option or flag local name_idx = arg.short and 1 or 2 local match_opt = nil for _, o in pairs(options) do local name = o[name_idx] if name == arg.key then match_opt = o break end end -- save option if match_opt and ((arg.type == "option" and match_opt[3] ~= "k") or (arg.type == "flag" and match_opt[3] == "k")) then results[match_opt[2] or match_opt[1]] = option.boolean(arg.value) else if opt.allow_unknown then results[arg.key] = option.boolean(arg.value) else return nil, string.format("Invalid %s: %s", arg.type, arg) end end elseif arg.type == "arg" then -- find a value option with name local match_opt = nil for _, o in ipairs(options) do -- get mode and name local mode = o[3] local name = o[2] -- is value and with name? if mode == "v" and name and not results[name] then match_opt = o break -- is values and with name? elseif mode == "vs" and name then match_opt = o break end end -- ok? save this value with name opt[2] if match_opt then -- get mode and name local mode = match_opt[3] local name = match_opt[2] -- save value if mode == "v" then results[name] = arg.value elseif mode == "vs" then -- the option local o = results[name] if not o then results[name] = {} o = results[name] end -- append value table.insert(o, arg.value) end else -- failed if opt.allow_unknown then if arg.key then results[arg.key] = arg.value else -- the option local o = results["$ARGS"] if not o then results["$ARGS"] = {} o = results["$ARGS"] end -- append value table.insert(o, arg.value) end else return nil, "invalid argument: " .. arg.value end end end end -- init the default value if opt.populate_defaults then option.populate_defaults(options, results) end return results end -- fill defined with option's default value, in place function option.populate_defaults(options, defined) assert(options and defined) -- populate the default value for _, o in ipairs(options) do -- the long name local longname = o[2] -- key=value? if o[3] == "kv" then local shortname = o[1] -- the key local key = longname or shortname assert(key) -- move value to key if needed if shortname and defined[shortname] ~= nil then defined[key], defined[shortname] = defined[shortname], nil end -- save the default value if defined[key] == nil then defined[key] = o[4] end -- value with name? elseif o[3] == "v" and longname then -- save the default value if defined[longname] == nil then defined[longname] = o[4] end end end end -- get the current task name -- -- @return the task name string -- function option.taskname() return option._context().taskname end -- get the task menu function option.taskmenu(task) assert(option._MENU) task = task or option.taskname() or "main" local taskmenu = option._MENU[task] if type(taskmenu) == "function" then taskmenu = taskmenu() option._MENU[task] = taskmenu end return taskmenu end -- get the given option value for the current task -- -- @param name the option name, e.g. "verbose", "diagnosis", "target" -- @return the option value -- function option.get(name) local options = option.options() if options then local value = options[name] if value == nil then value = option.default(name) end return value end end -- set the given option for the current task -- -- @param name the option name -- @param value the option value -- function option.set(name, value) -- cannot be the first context for menu assert(#option._CONTEXTS > 1) -- the options local options = option.options() assert(options) -- set it options[name] = value end -- convert value to boolean (handles "y", "yes", "true", etc.) -- -- @param value the value to convert -- @return true, false, or nil -- function option.boolean(value) if type(value) == "string" then local v = value:lower() if v == "true" or v == "yes" or v == "y" then value = true elseif v == "false" or v == "no" or v == "n" then value = false end end return value end -- get the default option value for the current task -- -- @param name the option name -- @return the default value -- function option.default(name) assert(name) local defaults = option.defaults() assert(defaults) return defaults[name] end -- get the current options function option.options() local context = option._context() if context then return context.options end end -- get all default options for the current or given task function option.defaults(task) -- get the default options for the current task if task == nil then return option._context().defaults end -- the task menu local taskmenu = option.taskmenu(task) -- get the default options for the given task local defaults = {} if taskmenu then option.populate_defaults(taskmenu.options, defaults) end return defaults end -- show update tips function option.show_update_tips() -- show latest version local versionfile = path.join(os.tmpdir(), "latest_version") if os.isfile(versionfile) then local versioninfo = io.load(versionfile) if versioninfo and versioninfo.version and semver.compare(versioninfo.version, xmake._VERSION_SHORT) > 0 then local updatetips = nil if os.host() == "windows" then updatetips = string.format([[ ========================================================================== | ${bright yellow}A new version of xmake is available!${clear} | | | | To update to the latest version ${bright}%s${clear}, run "xmake update". | ========================================================================== ]], versioninfo.version) else updatetips = string.format([[ ╔════════════════════════════════════════════════════════════════════════════╗ ║ ${bright yellow}A new version of xmake is available!${clear} ║ ║ ║ ║ To update to the latest version ${bright}%s${clear}, run "xmake update". ║ ╚════════════════════════════════════════════════════════════════════════════╝ ]], versioninfo.version) end io.print(colors.translate(updatetips)) end end end -- show logo function option.show_logo(logo, opt) -- define logo logo = logo or [[ _ __ ___ __ __ __ _| | ______ \ \/ / | \/ |/ _ | |/ / __ \ > < | \__/ | /_| | < ___/ /_/\_\_|_| |_|\__ \|_|\_\____| by ruki, xmake.io ]] -- make rainbow for logo opt = opt or {} if tty.has_color24() or tty.has_color256() then local lines = {} local seed = opt.seed or 236 for _, line in ipairs(logo:split("\n")) do local i = 0 local line2 = "" line:gsub(".", function (c) local code = tty.has_color24() and colors.rainbow24(i, seed) or colors.rainbow256(i, seed) line2 = string.format("%s${bright %s}%s", line2, code, c) i = i + 1 end) table.insert(lines, line2) end logo = table.concat(lines, "\n") end -- show logo io.print(colors.translate(logo)) -- define footer local footer = [[ ${point_right} ${bright}Manual${clear}: ${underline}https://xmake.io/guide/quick-start${clear} ${pray} ${bright}Donate${clear}: ${underline}https://xmake.io/about/sponsor${clear} ]] -- show footer io.print(colors.translate(footer)) -- show update tips option.show_update_tips() end -- show the menu function option.show_menu(task) -- no task? print main menu if not task then option.show_main() return end -- the menu local menu = option._MENU assert(menu) -- the task menu local taskmenu = option.taskmenu(task) assert(taskmenu) -- print title if menu.title then io.print(colors.translate(menu.title)) end -- print copyright if menu.copyright then io.print(colors.translate(menu.copyright)) end -- show logo option.show_logo() -- print usage if taskmenu.usage then io.print("") io.print(colors.translate("${bright}Usage: $${default color.menu.usage}" .. taskmenu.usage .. "${clear}")) end -- print description if taskmenu.description then io.print("") io.print(colors.translate(taskmenu.description)) end -- print options if taskmenu.options then option.show_options(taskmenu.options, task) end end -- show the main menu function option.show_main() -- the menu local menu = option._MENU assert(menu) -- the main menu local main = option.taskmenu("main") assert(main) -- print title if menu.title then io.print(colors.translate(menu.title)) end -- print copyright if menu.copyright then io.print(colors.translate(menu.copyright)) end -- show logo option.show_logo() -- print usage if main.usage then io.print("") io.print(colors.translate("${bright}Usage: $${default color.menu.usage}" .. main.usage .. "${clear}")) end -- print description if main.description then io.print("") io.print(colors.translate(main.description)) end -- print tasks if main.tasks then local narrow = option._modenarrow() -- make task categories local categories = {} for taskname, taskinfo in pairs(main.tasks) do -- the category name local categoryname = taskinfo.category or "task" if categoryname == "main" then categoryname = "action" end -- the category task local category = categories[categoryname] or { name = categoryname, tasks = {} } categories[categoryname] = category -- add task to the category category.tasks[taskname] = taskinfo end -- sort categories categories = table.values(categories) table.sort(categories, function (a, b) if a.name == "action" then return true end return a.name < b.name end) -- dump tasks by categories local tablecontent = {} for _, category in ipairs(categories) do -- the category name and task assert(category.name and category.tasks) -- print category name table.insert(tablecontent, {}) table.insert(tablecontent, {{string.format("%s%ss: ", string.sub(category.name, 1, 1):upper(), string.sub(category.name, 2)), style="${reset bright}"}}) -- print tasks for taskname, taskinfo in table.orderpairs(category.tasks) do -- init the task line local taskline = string.format(narrow and " %s%s" or " %s%s", taskinfo.shortname and (taskinfo.shortname .. ", ") or " ", taskname) table.insert(tablecontent, {taskline, colors.translate(taskinfo.description or "")}) end end -- set table styles tablecontent.style = {"${color.menu.main.task.name}"} tablecontent.width = {nil, "auto"} tablecontent.sep = narrow and " " or " " -- print table io.write(text.table(tablecontent)) end -- print options if main.options then option.show_options(main.options, "build") end end -- show the options menu function option.show_options(options, taskname) assert(options) -- remove repeat empty lines local is_action = false local emptyline_count = 0 local printed_options = {} for _, opt in ipairs(options) do if opt.category and printed_options[#printed_options].category then table.remove(printed_options) end table.insert(printed_options, opt) if opt.category and opt.category == "action" then is_action = true end end if printed_options[#printed_options].category then table.remove(printed_options) end -- narrow mode? local narrow = option._modenarrow() -- print header local tablecontent = {} table.insert(tablecontent, {}) if is_action then table.insert(tablecontent, {{"Common options:", style="${reset bright}"}}) else table.insert(tablecontent, {{"Options:", style="${reset bright}"}}) end -- print options local categories = {} for _, opt in ipairs(printed_options) do if opt.category and opt.category == "action" then -- the following options are belong action? show command section -- -- @see core/base/task.lua: translate menu -- table.insert(tablecontent, {}) table.insert(tablecontent, {{"Command options (" .. taskname .. "):", style="${reset bright}"}}) elseif opt.category and opt.category ~= "." then local category_root = opt.category:split("/")[1] if not categories[category_root] then table.insert(tablecontent, {}) table.insert(tablecontent, {{"Command options (" .. opt.category .. "):", style="${reset bright}"}}) categories[category_root] = true end elseif opt[3] == nil then table.insert(tablecontent, {}) else -- append the shortname local shortname = opt[1] local name = opt[2] local mode = opt[3] local default = opt[4] local title1, title2 local kvplaceholder = mode == "kv" and ((type(default) == "boolean") and "[y|n]" or(name and name:upper() or "XXX")) if shortname then if mode == "kv" then title1 = "-" .. shortname .. " " .. kvplaceholder else title1 = "-" .. shortname end end -- append the name if name then local leading = mode:startswith("k") and "--" or " " local kv if mode == "k" then kv = name elseif mode == "kv" then kv = name .. "=" .. kvplaceholder elseif mode == "vs" then kv = name .. " ..." else kv = name end title2 = leading .. kv elseif mode == "v" or mode == "vs" then title2 = " ..." end -- get description local optdespn = table.maxn(opt) local description = table.move(opt, 5, optdespn, 1, table.new(optdespn - 5 + 1, 0)) if #description == 0 then description[1] = "" end -- transform description local desp_strs = table.new(#description, 0) for _, v in ipairs(description) do local v = v if type(v) == "function" then v = v() end if type(v) == "string" then table.insert(desp_strs, colors.translate(v)) elseif type(v) == "table" then table.move(v, 1, #v, #desp_strs + 1, desp_strs) end end -- append the default value if default then local defaultval = tostring(default) if type(default) == "boolean" then defaultval = default and "y" or "n" end local def_desp = colors.translate(string.format(" (default: ${bright}%s${clear})", defaultval)) desp_strs[1] = desp_strs[1] .. def_desp end -- append values local values = opt.values if type(values) == "function" then values = values(false, {helpmenu = true}) end if values then for _, value in ipairs(table.wrap(values)) do table.insert(desp_strs, " - " .. tostring(value)) end end -- insert row if narrow then if title1 then table.insert(tablecontent, {{" " .. title1, " " .. title2 }, desp_strs}) else table.insert(tablecontent, {{" " .. title2 }, desp_strs}) end else if title1 then table.insert(tablecontent, {" " .. title1 .. ", " .. title2 , desp_strs}) else table.insert(tablecontent, {" " .. title2 , desp_strs}) end end end end -- set table styles tablecontent.style = {"${color.menu.option.name}"} tablecontent.width = {nil, "auto"} tablecontent.sep = narrow and " " or " " -- print table io.write(text.table(tablecontent)) end -- return module: option return option