Skip to content

Lua integration

Eugene Rysaj requested to merge erysaj/cmake:lua-integration into master

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() and cmMakefile::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 to BuiltinCommand return value, and non-nil "error" is equivalent to calling cmExecutionStatus::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())

Merge request reports