Lua integration
This MR adds support for implementing CMake modules/commands in Lua (inspired by Daniel Pfeifer's talk).
For MVP I set out to port an existing module to Lua (I chose FindLua51.cmake
) and fill in the gaps on the way (e.g. porting FindPackageHandleStandardArgs.cmake
)
Details
cmake
gets a new --lua
command-line option.
This option implies instantiating Lua VM and changes the behavior of a few methods:
-
cmMakefile::ReadListFile()
andcmMakefile::ReadDependendFile()
would check file extension and execute ".lua" files on Lua VM. This way Lua code can be executed via "include()"-ing the script by full path or script mode (cmake --lua -P <script>.lua
) -
cmFindPackageCommand::FindModule()
would also look for "Find.lua" module. This enables implementing "find" modules in Lua.
By default Lua scripts are executed in “strict” environment (setting or getting non-existing global variable is an error;
useful for catching typos and enforces discipline in working with dependencies),
but this behavior can be disabled by --no-sandbox
option (required to make "luaunit" framework work -- it relies on traversing global table for tests discovery).
Throwing Lua error (via "error" or "assert" built-in function) triggers a fatal error and Lua stacktrace is integrated into CMake stacktrace.
Defining commands
Lua functions can be directly registered as CMake scripted command.
Such function must conform to a protocol similar to cmState::BuiltinCommand
:
- accept a single in parameter (list of expanded arguments)
- return
success, error
pair -- "success" flag corresponds toBuiltinCommand
return value, and non-nil "error" is equivalent to callingcmExecutionStatus::SetError()
-- command implementaion
local function dosomething(args)
if #args == 0 then
return false, "called with incorrect number of arguments"
end
...
return true
end
-- register a new cmake function "myfunction"
local state = require "state"
state.commands.myfunction = dosomething
Such approach is low-level and not convenient for complex commands: arguments must be parsed manually and resulting "stringy" interface feels foreign to Lua. So another, more high-level way is available:
-
declare a type describing command arguments:
local rt = require "runtime" local Args = rt.struct { name = "cmd_fpm_Args"; rt.property("package", rt.String), rt.property("message", rt.String), rt.property("details", rt.String), }
Declaring property types enables automatic runtime type-checking
-
implement command functionality, but assume its single argument is of defined earlier type
local state = require "state" local vars = state.vars local function execute(args) local quiet = vars[args.package .. "_FIND_QUIETLY"]:is_truthy() if quiet then return true end local details = args.details:gsub("\n", "") local cache_key = "FIND_PACKAGE_MESSAGE_DETAILS_" .. args.package local cache_val = vars[cache_key]:value() if cache_val == details then return true end state.commands.message("STATUS", args.message) vars[cache_key]:set(details, "CACHE", "INTERNAL", "Details about finding " .. args.package ) return true end
-
generate command class
local cmd = require "command" local Command = cmd.make_command_type("cmd_fpm", Args, execute)
Generated class combines "command" and "builder" patterns, here is how its usage may look in Lua:
local cmd_fphsa = require "cmd_find_package_handle_standard_args" cmd_fphsa.Command() :package "Lua51" :required_vars { var_libs:name(), var_inc_dir:name(), } :version_var(var_version:name()) :execute()
-
define argument parser
local argparse = require "argparse" local function make_parser() local parser = argparse.ArgumentParser(Command) parser:posarg "package" parser:posarg "message" parser:posarg "details" return parser end
Parser is responsible for converting a list of string arguments into configured command instance. More complex parser definition:
local function make_parser() local parser = argparse.ArgumentParser:new(Command) parser:posarg "package" parser:options { -- alternative signature fallback = { "fail_message", "required_vars" } } parser:option "found_var" parser:option "required_vars" :argc("*") parser:option "version_var" parser:flag "handle_components" parser:flag "config_mode" parser:option "reason_failure_message" parser:option "fail_message" return parser end
-
register an automatically generated wrapper function as CMake command
cmd.install("find_package_message", execute, make_parser())