Use xmake to describe the project gracefully

Posted by spectsteve7 on Fri, 12 Jul 2019 21:43:54 +0200

Descriptive grammar

xmake's description grammar is based on Lua implementation, so the description grammar inherits the flexibility and conciseness of lua, and separates description scope (simple description) from script scope (complex description) through 28 principles, which makes the project more concise, intuitive and readable.

Because 80% of the projects do not need very complex script control logic, only a few simple lines of configuration description are needed to meet the construction requirements. Based on this assumption, xmake separates scopes, making 80% of the xmake.lua files only need to be described as follows:

target("demo")
    set_kind("binary")
    add_files("src/*.c")

Only 20% of the projects need to be described in this way:

target("demo")
    set_kind("shared")
    set_objectdir("$(buildir)/.objs")
    set_targetdir("libs/armeabi")
    add_files("jni/*.c")

    on_package(function (target) 
        os.run("ant debug") 
    end)

    on_install(function (target) 
        os.run("adb install -r ./bin/Demo-debug.apk")
    end)

    on_run(function (target) 
        os.run("adb shell am start -n com.demo/com.demo.DemoTest")
        os.run("adb logcat")
    end)

The function () end part above belongs to the custom script domain. Normally, it does not need to be set up. Only when complex engineering description and highly customized requirements are required, they need to be customized. Extension modules provided by various xmake can be used in this scope. For more information on this, see: xmake Description Grammar and Scope Explanation.

And the above code is also a custom android project that mixes jni and java code. It can build, install and run apk program by one button directly through xmake run command.

Here are some common examples of xmake descriptions:

Building an executable program

target("demo")
    set_kind("binary")
    add_files("src/*.c")

This is the simplest classic example, in general, this situation, you do not need to write any xmake.lua file yourself, in the current code directory, directly execute the xmake command, you can complete the construction, and will automatically help you generate an xmake.lua.

For more information on automatic generation, see: xmake Intelligent Code Scanning Compilation Mode, No Handwritten make Files.

Building a configurable switchable library program

target("demo")
    set_kind("$(kind)")
    add_files("src/*.c")

By configuring, you can switch whether to compile dynamic or static libraries:

$ xmake f --kind=static; xmake
$ xmake f --kind=shared; xmake

Adding debug and release compilation mode support

Perhaps the default description configuration of several lines can no longer meet your needs. You need to build debug and release programs by switching compilation mode. Then you only need:

if is_mode("debug") then
    set_symbols("debug")
    set_optimize("none")
end

if is_mode("release") then
    set_symbols("hidden")
    set_optimize("fastest")
    set_strip("all")
end

target("demo")
    set_kind("binary")
    add_files("src/*.c") 

You only need to switch the build mode by configuration:

$ xmake f -m debug; xmake
$ xmake f -m release; xmake

[-m | - mode] is a built-in option that can be used without defining options. The value of the pattern is defined and maintained by the user himself. You can judge the state of various patterns in is_mode("xxx").

Signing ios program by custom script

The executable program of ios, which runs on the device, needs to be signed after the construction is completed. At this time, it can be implemented by using custom scripts:

target("demo")
    set_kind("binary")
    add_files("src/*.m") 
    after_build(function (target))
        os.run("ldid -S %s", target:targetfile())
    end

Here is just a fake signature with ldid program. It can only be used on jailbreak equipment. Just for example.

Built-in and external variables

xmake provides the syntax of $(varname) to support the acquisition of built-in variables, such as:

add_cxflags("-I$(buildir)")

It converts the built-in buildir variable to the actual build output directory at actual compilation time: - I./build

General built-in variables can be used to quickly obtain and stitch variable strings when passing parameters, for example:

target("test")
    add_files("$(projectdir)/src/*.c")
    add_includedirs("$(buildir)/inc")

It can also be used in modular interfaces of custom scripts, such as:

target("test")
    on_run(function (target)
        os.cp("$(scriptdir)/xxx.h", "$(buildir)/inc")
    end)

Of course, this variable pattern can also be extended. By default, the configuration parameters can be obtained directly through the xmake F - var = Val command, such as:

target("test")
    add_defines("-DTEST=$(var)")

Now that support is obtained directly from the configuration options, of course, it is very convenient to extend the custom options to obtain the custom variables. See how to customize the options: option

Modify the name of the target file

We can use built-in variables to separate the generated object files according to different architectures and platforms, such as:

target("demo")
    set_kind("binary")
    set_basename("demo_$(arch)")
    set_targetdir("$(buildir)/$(plat)")

Previous default settings, the target file will be generated as build demo, and through the above code settings, the target file under different configuration builds, paths and file names are also different, execution:

$ xmake f -p iphoneos -a arm64; xmake

The target file is build/iphoneos/demo_arm64.

Add subdirectory engineering module

If you have multiple target sub-modules, you can define them in an xmake.lua, for example:

target("demo")
    set_kind("binary")
    add_files("src/demo.c")

target("test")
    set_kind("binary")
    add_files("src/test.c")

However, if there are many sub-modules, it would be a bit bloated to place them in an xmake file, which can be placed in the sub-directory of the independent module:

target("demo")
    set_kind("binary")
    add_files("src/demo.c")

add_subdirs("src/test")

With the above code, associate a subproject directory with the project goal of test.

Installation header file

target("tbox")
    set_kind("static")
    add_files("src/*.c")

    add_headers("../(tbox/**.h)|**/impl/**.h")
    set_headerdir("$(buildir)/inc")

The location and directory structure of the installed header file are: build/inc/tbox/*.h.

The bracketed part of. /(tbox/**.h) is the actual root path to be installed, and the |**/impl/**.h part is used to exclude files that do not need to be installed.

Its wildcard matching rules and exclusion rules can be referred to. add_files.

Multiobjective Dependency Construction

The default build order for multiple target projects is undefined, usually in a sequential manner. If you need to adjust the build order, you can achieve it by adding a dependency order:

target("test1")
    set_kind("static")
    set_files("*.c")

target("test2")
    set_kind("static")
    set_files("*.c")

target("demo")
    add_deps("test1", "test2")
    add_links("test1", "test2")

In the example above, when compiling the target demo, you need to compile the test1, test2 targets first, because demo will use them.

Merge static libraries

xmake add_files The interface function is very powerful. It can not only support the mixed addition and construction of multi-language files, but also add static libraries directly and merge libraries automatically into the current project objectives.

We can write as follows:

target("demo")
    set_kind("static")
    add_files("src/*.c", "libxxx.a", "lib*.a", "xxx.lib")

When compiling static libraries directly, merge multiple existing static libraries. Note that it's not a link. add_links There is a difference.

And you can add object files directly:

target("demo")
    set_kind("binary")
    add_files("src/*.c", "objs/*.o")

Add custom configuration options

We can define a configuration option ourselves, for example, to enable test:

option("test")
    set_default(false)
    set_showmenu(true)
    add_defines("-DTEST")

Then associate it with the specified target:

target("demo")
    add_options("test")

In this way, even if an option is defined, if this option is enabled, the macro definition of - DTEST will be added automatically when compiling the target.

The above settings, by default, disable the test option, and then we configure to enable this option:

$ xmake f --test=y
$ xmake

The option support of xmake is very powerful. In addition to the basic usage mentioned above, it can also configure various detection conditions to realize automatic detection. Details can be referred to as follows: option and Addition of dependency packages and automatic detection mechanism.

Adding third-party dependency packages

In the target scope, add integrated third-party package dependencies, such as:

target("test")
    set_kind("binary")
    add_packages("zlib", "polarssl", "pcre", "mysql")

In this way, when compiling the test target, if the package exists, it will automatically append the macro definition, header file search path, link library directory in the package, and automatically link all the libraries in the package.

Users no longer need to call add_links, add_includedirs, add_ldflags and other interfaces separately to configure dependency library links.

For how to set up the package search directory, you can refer to add_packagedirs Interface, Dependency Package Details Refer to: Addition of dependency packages and automatic detection mechanism.

Generate configuration header file

If you want to write the results of the test to the configuration header file after the xmake configuration project is successful or after an option is passed automatically, you need to call this interface to enable the automatic generation of config.h files.

For example:

target("test")
    set_config_h("$(buildir)/config.h")
    set_config_h_prefix("TB_CONFIG")

If a dependency is enabled, the corresponding macro definition configurations will be automatically written into the config.h file set up after adding relevant option dependencies, package dependencies, and interface dependencies to the target through the following interfaces.

These interfaces, in fact, use some of the detection settings in the option options at the bottom, such as:

option("wchar")

    -- Add pairs wchar_t Type Detection
    add_ctypes("wchar_t")

    -- If the detection passes, automatic generation TB_CONFIG_TYPE_HAVE_WCHAR Macro switch to config.h
    add_defines_h_if_ok("$(prefix)_TYPE_HAVE_WCHAR")

target("test")

    -- Enable automatic header file generation
    set_config_h("$(buildir)/config.h")
    set_config_h_prefix("TB_CONFIG")

    -- Add pairs wchar The dependency Association of options is added only. wchar The test result of the option is written to the specified config.h Go in
    add_options("wchar")

Detecting library header files and interfaces

We can add some library interface detection to the newly generated config.h, such as:

target("demo")

    -- Setting and enabling config.h
    set_config_h("$(buildir)/config.h")
    set_config_h_prefix("TEST")

    -- Setting module name prefix only by parameter one
    add_cfunc("libc",       nil,        nil,        {"sys/select.h"},   "select")

    -- Through parameter three, set up simultaneous detection link library: libpthread.a
    add_cfunc("pthread",    nil,        "pthread",  "pthread.h",        "pthread_create")

    -- Setting interface aliases by parameter two
    add_cfunc(nil,          "PTHREAD",  nil,        "pthread.h",        "pthread_create")

The generated config.h results are as follows:

#ifndef TEST_H
#define TEST_H

// Macro Naming Rules: $(prefix) prefix_module name (if not nil) HAVE_interface name or alias (capitalization)
#define TEST_LIBC_HAVE_SELECT 1
#define TEST_PTHREAD_HAVE_PTHREAD_CREATE 1
#define TEST_HAVE_PTHREAD 1

#endif

In this way, we can control the compilation of the code according to the support of the interface.

Custom Plug-in Tasks

The task domain is used to describe a custom task implementation, and target and option At the same level.

For example, here we define the simplest task:

task("hello")

    -- Setting up run scripts
    on_run(function ()
        print("hello xmake!")
    end)

This task only needs to print hello xmake!, so how to run it?

Because it is not used here set_menu Set the menu so that the task can only be invoked within a custom script for xmake.lua or other tasks, such as:

target("test")

    after_build(function (target)

        -- Import task Modular
        import("core.project.task")

        -- Function hello task
        task.run("hello")
    end)

Here we run the hello task after we have built the test target. Of course, we can also pass parameters.

task("hello")
    on_run(function (arg1, arg2, arg3)
        print("hello xmake!", arg1, arg2, arg3)
    end)

target("test")
    after_build(function (target)
        import("core.project.task")
        task.run("hello", {}, "arg1", "arg2", "arg3")
    end)

The task.run {} above is used to pass parameters from the plug-in menu, which is not passed here. set_menu Set the menu and pass it empty here.

xmake plug-in support is also very powerful, and provides many built-in plug-ins for use, please refer to: xmake plug-in manual and task manual

Or you can refer to some of the xmake's own Plug-in demo.

Another grammatical style

In addition to supporting the most commonly used set-add descriptive style, xmake supports another grammatical style: key-val, for example:

target
{
    name = "test",
    defines = "DEBUG",
    files = {"src/*.c", "test/*.cpp"}
}

This is equivalent to:

target("test")
    set_kind("static")
    add_defines("DEBUG")
    add_files("src/*.c", "test/*.cpp")

Users can choose the appropriate style description according to their preferences, but the suggestion here is:

* For simple projects, it does not need too complicated conditional compilation, and can use the key-val mode, which is more concise and readable.
* For complex engineering, if more controllability and flexibility are needed, the set-add method is recommended.
* Try not to mix the two styles, although it is supported, but this description of the whole project will feel very confused, so try to unify the style as their own description specification.

In addition, not only target s, such as options, tasks, templates are set in two ways, such as:

-- set-add style
option("demo")
    set_default(true)
    set_showmenu(true)
    set_category("option")
    set_description("Enable or disable the demo module", "    =y|n")
-- key-val style
option
{
    name = "demo",
    default = true,
    showmenu = true,
    category = "option",
    desciption = {"Enable or disable the demo module", "    =y|n"}
}

Custom tasks or plug-ins can be written as follows:

-- set-add style
task("hello")
    on_run(function ()
        print("hello xmake!")

    end)
    set_menu {
        usage = "xmake hello [options]",
        description = "Hello xmake!",
        options = {}
    }
-- key-val style
task
{
    name = "hello",
    run = (function ()
        print("hello xmake!")
    end),
    menu = {
                usage = "xmake hello [options]",
                description = "Hello xmake!",
                options = {}
            }
}

epilogue

More descriptions are available for direct reading. xmake's official manual The complete api documentation and usage description are provided above.

Personal Home Page: TBOOX Open Source Project

Topics: iOS shell Android Java