Xmake is a lightweight modern C/C ++ project building tool based on Lua. The main features are: simple syntax and easy to use, provide more readable project maintenance, achieve cross-platform behavior consistent build experience.

This article focuses on how to achieve more complex and flexible customization in the scripting domain by adding custom scripts.

  • Program source code
  • The official documentation

Its configuration

Xmake. Lua uses the 80/20 principle to implement a separate configuration of description domain and script domain.

What is the 80/20 rule? To put it simply, the configuration of most projects, 80% of the time, is basic general configuration, such as add_CXflags, add_links, etc. Only less than 20% of the configuration needs to be complicated to meet some special configuration requirements.

The remaining 20 percent of the configuration is usually complex, and if it is directly filled with the entire xmake. Lua, the configuration of the entire project will be very confusing and unreadable.

Therefore, Xmake separates 80% of the simple configuration from 20% of the complex configuration by using two different configuration methods: description field and script field, making the whole Xmake. Lua looks very clear and intuitive, and has the best readability and maintainability.

Describe the domain

What is a description domain? For novice users, or for maintaining simple small projects, the requirement is fully satisfied by describing the configuration entirely. It looks like this:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_defines("DEBUG")
    add_syslinks("pthread")
Copy the code

At first glance, it is a set_xxx/add_xxx configuration set. For beginners, it is not a Lua script at all, but a normal configuration file with some basic rules.

Does this look more like a configuration file? Description fields are configuration files, like key/values configurations like JSON, so even if you are completely new to Lua, you can get started quickly.

Also, for a normal project, just using set_xxx/add_xxx to configure various project Settings is sufficient.

80% of the time, the simplest configuration rules can be used to simplify the configuration of a project, improve readability and maintainability, and make it user-friendly and intuitive for both users and developers.

What if we have to make some conditional judgments on different platforms and architectures? In addition to the basic configuration, the description field also supports conditional judgment and for loop:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_defines("DEBUG")
    if is_plat("linux"."macosx") then
        add_links("pthread"."m"."dl")
    end
Copy the code
target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_defines("DEBUG")
    for _, name in ipairs({"pthread"."m"."dl"}) do
        add_links(name)
    end
Copy the code

Doesn’t this look a bit like Lua? Although this can be considered a common configuration problem, Xmake is based on Lua, so the description domain supports lua’s basic language features.

! < p style = “max-width: 100%; clear: both; min-height: 1em

In the description domain, the main purpose is to set configuration items, so Xmake does not completely open all module interfaces, many interfaces are forbidden to call in the description domain, even some open callable interfaces are completely read-only, not time-consuming security interfaces, such as: Os.getenv () and so on read some general system information for configuration logic control.

! > Xmake. lua is parsed multiple times to resolve different configuration fields at different stages: option(), target(), etc.

Therefore, don’t think about writing complex Lua scripts in the description field of xmake. Lua, and don’t call print in the description field to display information, because it will be executed multiple times, remember: it will be executed multiple times!

Script domain

Restrict description domain to write complex LuA, various LuA modules and interfaces can not use? How to do? This is where the scripting domain comes in.

If the user is completely familiar with xmake’s description domain configuration and feels a little overwhelmed by special configuration maintenance on the project, then we can do more complex configuration logic in the script domain:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    on_load(function (target)
        if is_plat("linux"."macosx") then
            target:add("links"."pthread"."m"."dl")
        end
    end)
    after_build(function (target)
        import("core.project.config")
        local targetfile = target:targetfile()
        os.cp(targetfile, path.join(config.buildir(), path.filename(targetfile)))
        print("build %s", targetfile)
    end)
Copy the code

Any script inside a function body like on_xxx, after_xxx, before_xxx, etc. belongs to the script domain.

In the script domain, users can do anything. Xmake provides an import interface to import various Lua modules built into Xmake as well as user-supplied Lua scripts.

We can implement any functionality you want in the scripting domain, even as a separate project.

For some script fragments that aren’t too bloated, built-in scripting like this is sufficient, but if you need to implement more complex scripts that don’t want to clutter up a single Xmake. lua file, you can maintain the scripts in separate Lua files.

Such as:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    on_load("modules.test.load")
    on_install("modules.test.install")
Copy the code

Lua, modules/test/load.lua and modules/test/install.lua.

Separate Lua script files have main as the main entry, for example:

We can also import some built-in modules or our own extension modules here to use
import("core.project.config")
import("mymodule")

function main(target)
    if is_plat("linux"."macosx") then
        target:add("links"."pthread"."m"."dl")
    end
end
Copy the code

These separate Lua scripts can also be used by importing various built-in modules and custom modules, just like writing Lua or Java.

For different stages of the script field, on_load is mainly used for target loading, doing some dynamic configuration, unlike the description field, which is only executed once!!

Other stage, there are many, such as: on/after/before_build/install/package/run, etc., we will describe in detail below.

import

Import extension touch block

Xmake uses import to introduce other extension modules, as well as user-defined modules, which can be used in the following places:

  • Custom scripts (on_build, on_run..)
  • Plug-in development
  • Template development
  • Platform extensions
  • Customize tasks task

The import mechanism is as follows:

  1. Import from the current script directory first
  2. Import it from the extended class library

Syntax rules for importing:

Class library path rules based on. For example:

import("core.base.option")
import("core.base.task")

function main(a)
    
    -- Gets parameter options
    print(option.get("version"))

    -- Run tasks and plug-ins
    task.run("hello")
end
Copy the code

Import custom modules in the current directory:

Directory structure:

plugin
  - xmake.lua
  - main.lua
  - modules
    - hello1.lua
    - hello2.lua
Copy the code

Import modules in main.lua

import("modules.hello1")
import("modules.hello2")
Copy the code

After the import, you can directly use all the public interfaces in the import. Private interfaces are marked with _ prefix, indicating that they will not be exported or called externally.

In addition to the current directory, we can also import libraries from other specified directories, for example:

import("hello3", {rootdir = "/home/xxx/modules"})
Copy the code

To prevent naming conflicts, you can also specify aliases after importing:

import("core.platform.platform", {alias = "p"})

function main(a)
    We can use p to call the plats interface of the platform module to get a list of all platforms supported by Xmake
    print(p.plats())
end
Copy the code

Import (“xxx.xxx”, {try = true, anonymous = true})

If xmake. Anonymous is true, the imported module does not introduce the current scope, but only returns references to imported objects on the import interface.

Test extension module

One way to test and verify is to call print directly in a script such as on_load to print the call result of the module.

However, Xmake also provides the Xmake Lua plug-in for more flexible and convenient test scripts.

Runs the specified script file

For example, you can specify lua scripts to load and run directly, which is a good way to quickly test some interface modules and validate your ideas.

Let’s start with a simple Lua script:

function main(a)
    print("hello xmake!")
end
Copy the code

Then run it directly:

$ xmake lua /tmp/test.lua
Copy the code

Call extension modules directly

All built-in modules and extension module interfaces can be directly called by xmake Lua, for example:

$ xmake lua lib.detect.find_tool gcc
Copy the code

For the above command, we invoked the import(“lib.detect.find_tool”) module interface directly to execute it quickly.

Run interactive commands (REPL)

Sometimes running commands in interactive mode makes it easier to test and validate modules and apis, and is more flexible without having to write an additional script file to load.

Let’s first look at how to enter interactive mode:

#Execute without any parameters to enter
$ xmake lua
>

#Evaluate an expression
> 1 + 2
3

#Assign and print variable values
> a = 1
> a
1

#Multi-line input and execution
> for _, v in pairs({1, 2, 3}) do
>> print(v)
>> end
1
2
3
Copy the code

We can also import extension modules via import:

> task = import("core.project.task")
> task.run("hello")
hello xmake!
Copy the code

To cancel multiple lines midway, simply type the character q

> for _, v in ipairs({1, 2}) do
>> print(v)
>> q <-- cancels multi-line input and empties previous input data
> 1 + 2
3
Copy the code

target:on_load

Custom target loading scripts

This script is executed when the target is initially loaded. It allows you to do some dynamic target configuration and achieve more flexible target description definitions, such as:

target("test")
    on_load(function (target)
        target:add("defines"."DEBUG"."TEST=\"hello\"")
        target:add("linkdirs"."/usr/lib"."/usr/local/lib")
        target:add({includedirs = "/usr/include"."links" = "pthread"})
    end)
Copy the code

Various target attributes can be dynamically added to on_load via target:set, target:add. All set_ and add_ configurations describing fields can be dynamically configured in this way.

Alternatively, we can call some of target’s interfaces to get and set some basic information, such as:

Target interface describe
target:name() Get target name
target:targetfile() Gets the destination file path
target:targetkind() Gets the build type of the target
target:get(“defines”) Gets the macro definition of the target
target:get(“xxx”) Other byset_/add_The target information set on this interface can be obtained through this interface
target:add(“links”, “pthread”) Add target Settings
target:set(“links”, “pthread”, “z”) Override target Settings
target:deps() Gets all dependent targets of the target
target:dep(“depname”) Gets the specified dependency target
target:opts() Gets all associated options for the target
target:opt(“optname”) Gets the specified association option
target:pkgs() Gets all associated dependency packages for the target
target:pkg(“pkgname”) Gets the specified associated dependency package
target:sourcebatches() Gets a list of all source files for the target

target:on_link

Custom link scripts

This interface was added after V2.2.7 to customize the target linking process.

target("test")
    on_link(function (target) 
        print("link it")
    end)
Copy the code

target:on_build

Custom compilation scripts

Override the target target’s default build behavior and implement a custom build process. In general, you don’t need to do this unless you actually need to do some build that xmake doesn’t provide by default.

You can override it by customizing the compile operation:

target("test")

    -- Set custom compilation scripts
    on_build(function (target) 
        print("build it")
    end)
Copy the code

Note: After version 2.1.5, all target custom scripts can be handled separately for different platforms and architectures. For example:

target("test")
    on_build("iphoneos|arm*".function (target)
        print("build for iphoneos and arm")
    end)
Copy the code

One if the first parameter is a string, then specify the script need | framework, in which platform will be implemented, and support pattern matching, such as arm * matches all the arm architecture.

Of course, you can only set the platform, not set the architecture, so that matches the specified platform, execute the script:

target("test")
    on_build("windows".function (target)
        print("build for windows")
    end)
Copy the code

Note: Once you have set your own build process for this target target, the xmake default build process will no longer be executed.

target:on_build_file

Custom compilation script, realize single file construction

This interface can be used to hook target’s built-in build process and re-implement each source file compilation process itself:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    on_build_file(function (target, sourcefile, opt)
    end)
Copy the code

target:on_build_files

Custom compilation script, realize multi-file construction

This interface can be used to hook the target built-in build process to replace a batch of the same type of source file compilation process:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    on_build_files(function (target, sourcebatch, opt)
    end)
Copy the code

When this interface is set, the files in the source file list will not appear in the custom target.on_build_file, because this is containment.

Sourcebatch describes the batch of source files of the same type:

  • sourcebatch.sourcekindGet the type of this batch of source files, such as cc, AS,..
  • sourcebatch.sourcefiles(): Gets the source file list
  • sourcebatch.objectfiles(): Gets the list of object files
  • sourcebatch.dependfiles(): Obtains the dependency file list and saves the compilation dependency information in the source file, for example, xxx.d

target:on_clean

Custom cleanup scripts

Cover the target goal xmake [c | clean} cleaning operations, implement custom clearing process.

target("test")

    Set up custom cleanup scripts
    on_clean(function (target) 

        Delete only the target file
        os.rm(target:targetfile())
    end)
Copy the code

target:on_package

Custom packaging scripts

Cover the target goal of xmake [p | package} packaging operations, implement custom packaging process, if you want to specify a target packaged into format they want, can through the interface to customize it.

target("demo")
    set_kind("shared")
    add_files("jni/*.c")
    on_package(function (target) 
        os.exec("./gradlew app:assembleDebug") 
    end)
Copy the code

Of course, this example is a bit old, and this is just an example of how to use it. Xmake now provides a special xmake-Gradle plugin for better integration with Gradle.

target:on_install

Custom installation scripts

Cover the target goal of xmake [I | install} installation operation, implement a custom installation process.

For example, the apK package will be generated for installation.

target("test")

    Set a custom installation script to automatically install apK files
    on_install(function (target) 

        -- Use adb installation package to generate APK files
        os.run("adb install -r ./bin/Demo-debug.apk")
    end)
Copy the code

target:on_uninstall

Custom uninstallation scripts

Covering the target goal xmake [u | uninstall} unloading operation, implement custom unloading process.

target("test")
    on_uninstall(function (target).end)
Copy the code

target:on_run

Customize running scripts

Covering the target goal xmake [r | run} operation, implement a custom operation process.

For example, to run the installed APK program:

target("test")

    -- Set a custom running script, automatically run the installed APP, and automatically obtain device output information
    on_run(function (target) 
        os.run("adb shell am start -n com.demo/com.demo.DemoTest")
        os.run("adb logcat")
    end)
Copy the code

Before_xxx and after_xxx

Note that all interfaces of target:on_xxx override the internal default implementation. Usually we do not need to completely override, but simply attach some additional logic of our own, using the target:before_xxx and target:after_xxx series of scripts.

All on_xxx versions have before_ and after_XX versions, and the parameters are identical, for example:

target("test")
    before_build(function (target)
        print("")
    end)
Copy the code

Built-in module

In the custom script, in addition to using the import interface to import various extension modules, Xmake also provides a lot of basic built-in modules, such as OS, IO and other basic operations, to achieve more cross-platform processing system interface.

os.cp

Os.cp behaves like the cp command in the shell, but is more powerful, not only supporting pattern matching (using Lua pattern matching), but also ensuring destination path recursive directory creation and supporting xmake’s built-in variables.

Such as:

os.cp("$(scriptdir)/*.h"."$(buildir)/inc")
os.cp("$(projectdir)/src/test/**.h"."$(buildir)/inc")
Copy the code

The above code copies all the header files in the current xmake.lua directory, and all the header files in the project source test directory to the $(buildir) output directory.

$(scriptdir) and $(projectdir) are built-in variables of Xmake. For details, see the documentation for built-in variables.

The matching pattern in *. H and **. H is similar to that in add_files, where the former is single-level directory matching and the latter is recursive multilevel directory matching.

If you want to keep the original directory structure, you can set the rootdir parameter:

os.cp("src/**.h"."/tmp/", {rootdir = "src"})
Copy the code

The script above copies all SRC child files as the SRC root directory.

Note: Try to use the OS.cp interface instead of os.run(“cp..” ) to ensure platform consistency and achieve cross-platform build descriptions.

os.run

This interface quietly runs native shell commands, which are used to execute third-party shell commands. However, no output is displayed. Only error information is highlighted when errors occur.

This interface supports parameter formatting, built-in variables, such as:

-- Format parameters passed in
os.run("echo hello %s!"."xmake")

-- Enumerates build directory files
os.run("ls -l $(buildir)")
Copy the code

os.execv

Compared with OS.run, this interface can output output during execution, and parameters are passed in the form of a list, which is more flexible.

os.execv("echo", {"hello"."xmake!"})
Copy the code

In addition, this interface supports an optional parameter to pass Settings: redirect output, perform environment variable Settings, for example:

os.execv("echo", {"hello"."xmake!"}, {stdout = outfile, stderr = errfile, envs = {PATH = "xxx; xx", CFLAGS = "xx", curdir = "/tmp"}}
Copy the code

The stdout and stderr arguments are used to pass redirected output and error output, either directly to the file path or to the file object opened by IO. Open.

In addition, if you want to temporarily set and overwrite some environment variables during this execution, you can pass the enVS parameter. The environment variable Settings will replace the existing Settings, but do not affect the outer execution environment, only the current command.

We can also get all of the current environment variables through the os.getenvs() interface, then rewrite the section and pass in the ENVS parameters.

In addition, the working directory of the child process can be changed during execution through the curdir parameter setting.

Related interfaces include os.runv, os.exec, os.execv, os.iorun, os.iorunv, etc. For example, os.iorun can fetch the output of a run.

For details and differences in this section, as well as more OS interfaces, see the OS Interfaces documentation.

io.readfile

This interface reads all contents from the specified path file. We can directly read the contents of the entire file without opening the file, which is more convenient, for example:

local data = io.readfile("xxx.txt")
Copy the code

io.writefile

This interface writes all contents to the specified path file. We can directly write the contents of the entire file without opening the file, which is more convenient, for example:

io.writefile("xxx.txt"."all data")
Copy the code

path.join

This interface implements cross-platform path splicing operation, adding multiple path items. Due to the difference of Windows/Unix style paths, using API to add paths is more cross-platform, for example:

print(path.join("$(tmpdir)"."dir1"."dir2"."file.txt"))
Copy the code

The above concatenation is equivalent to $(tmpdir)/dir1/dir2/file.txt on Unix and $(tmpdir)\\dir1\\dir2\ file.txt on Windows

See the built-in module documentation for more details

Tboox.org/cn/2020/07/…