-
-
Notifications
You must be signed in to change notification settings - Fork 819
Xmake v2.8.5 released, Support for link sorting and unit testing
Xmake is a lightweight cross-platform build utility based on Lua.
It is very lightweight and has no dependencies because it has a built-in Lua runtime.
It uses xmake.lua to maintain project builds and its configuration syntax is very simple and readable.
We can use it to build project directly like Make/Ninja, or generate project files like CMake/Meson, and it also has a built-in package management system to help users solve the integrated use of C/C++ dependent libraries.
Xmake = Build backend + Project Generator + Package Manager + [Remote|Distributed] Build + Cache
Although not very precise, we can still understand Xmake in the following way:
Xmake ≈ Make/Ninja + CMake/Meson + Vcpkg/Conan + distcc + ccache/sccache

Before introducing new features, we have good news to tell you that Xmake has recently entered Debian's official repository: https://packages.debian.org/sid/xmake,
When Ubuntu 24.04 is released in April next year, we should be able to quickly install Xmake directly through the apt install xmake
command.
I would also like to thank @Lance Lin for his help. He helped us maintain and upload the Xmake package to the Debian repository throughout the whole process. Thank you very much!
Next, let’s introduce some changes introduced in version 2.8.5. This version brings many new features, especially support for link sorting, link groups, and support for xmake test
built-in unit tests.
In addition, we have also added build support for the Apple XROS platform, which can be used to build programs on Apple's new VisionOS. We also provide a more flexible and versatile check_sizeof
detection interface for quickly detecting the size of types.
This is a requirement that has existed for more than two years and is mainly used to adjust the link order within the target.
Since xmake provides add_links
, add_deps
, add_packages
, add_options
interfaces, you can configure links in targets, dependencies, packages and options, although the link order of add_links
itself can be adjusted according to the order of addition.
However, the link order between links, deps and packages can only be generated in a fixed order and cannot be adjusted flexibly. This is a bit inadequate for some complex projects.
In this version, we have completely solved this problem and added the add_linkorders
interface, which can be used to configure various link orders introduced by targets, dependencies, packages, options, and link groups.
For more details and background, see: #1452
In order to more flexibly adjust the various link orders within the target, we can implement it through the new interface add_linkorders
, for example:
add_links("a", "b", "c", "d", "e")
-- e -> b -> a
add_linkorders("e", "b", "a")
--e->d
add_linkorders("e", "d")
add_links is the configured initial link order, and then we configure two local link dependencies e -> b -> a
and e -> d
through add_linkorders.
xmake will internally generate a DAG graph based on these configurations, and use topological sorting to generate the final link sequence and provide it to the linker.
Of course, if there is a circular dependency and a cycle is created, it will also provide warning information.
In addition, we can also solve the problem of circular dependencies by configuring link groups through add_linkgroups
.
And add_linkorders
can also sort link groups.
add_links("a", "b", "c", "d", "e")
add_linkgroups("c", "d", {name = "foo", group = true})
add_linkorders("e", "linkgroup::foo")
If we want to sort link groups, we need to give each link group a name, {name = "foo"}
, and then we can reference the configuration through linkgroup::foo
in add_linkorders
.
We can also sort links and frameworks for macOS/iPhoneOS.
add_links("a", "b", "c", "d", "e")
add_frameworks("Foundation", "CoreFoundation")
add_linkorders("e", "framework::CoreFoundation")
For a complete example, we can look at:
add_rules("mode.debug", "mode.release")
add_requires("libpng")
target("bar")
set_kind("shared")
add_files("src/foo.cpp")
add_linkgroups("m", "pthread", {whole = true})
target("foo")
set_kind("static")
add_files("src/foo.cpp")
add_packages("libpng", {public = true})
target("demo")
set_kind("binary")
add_deps("foo")
add_files("src/main.cpp")
if is_plat("linux", "macosx") then
add_syslinks("pthread", "m", "dl")
end
if is_plat("macosx") then
add_frameworks("Foundation", "CoreFoundation")
end
add_linkorders("framework::Foundation", "png16", "foo")
add_linkorders("dl", "linkgroup::syslib")
add_linkgroups("m", "pthread", {name = "syslib", group = true})
The complete project is at: linkorders example
In addition, in this version, we have also added native support for the link group, which is currently mainly used for compilation on the Linux platform and only supports the gcc/clang compiler.
It should be noted that the concept of link group in gcc/clang mainly refers to: -Wl,--start-group
Xmake is aligned and encapsulated, further abstracted, and not only used to process -Wl,--start-group
, but also -Wl,--whole-archive
and -Wl,-Bstatic
.
Below we will explain them one by one.
For more details, see: #1452
-Wl,--start-group
and -Wl,--end-group
are linker options for handling complex library dependencies, ensuring that the linker can resolve symbolic dependencies and successfully connect multiple libraries.
In xmake, we can achieve this in the following way:
add_linkgroups("a", "b", {group = true})
It will generate the corresponding -Wl,--start-group -la -lb -Wl,--end-group
link options.
If there is a symbolic circular dependency between libraries a and b, no link error will be reported and the link can be successful.
For unsupported platforms and compilations, it will fall back to -la -lb
--whole-archive
is a linker option commonly used when dealing with static libraries.
Its function is to tell the linker to include all object files in the specified static library into the final executable file, not just the object files that satisfy the current symbol dependencies.
This can be used to ensure that all code for certain libraries is linked, even if they are not directly referenced in the current symbol dependencies.
For more information, please refer to the gcc/clang documentation.
In xmake, we can achieve this in the following way:
add_linkgroups("a", "b", {whole = true})
It will generate the corresponding -Wl,--whole-archive -la -lb -Wl,--no-whole-archive
link options.
For unsupported platforms and compilations, it will fall back to -la -lb
Additionally, we can configure group/whole at the same time:
add_linkgroups("a", "b", {whole = true, group = true})
-Bstatic
is also an option for compilers (such as gcc) to instruct the compiler to use only static libraries and not shared libraries when linking.
For more information, please refer to the gcc/clang documentation.
In xmake, we can achieve this in the following way:
add_linkgroups("a", "b", {static = true})
It will generate the corresponding -Wl,-Bstatic -la -lb -Wl,-Bdynamic
linkage options.
In the new version, we have also added a built-in test command: xmake test
. We only need to configure some test cases through add_tests on the target that needs to be tested to automatically execute the test.
Even if the current target is set to set_default(false)
, when executing tests, xmake will still automatically compile them first, and then automatically run all tests.
We can first look at an overall example to get a rough idea of what it looks like.
add_rules("mode.debug", "mode.release")
for _, file in ipairs(os.files("src/test_*.cpp")) do
local name = path.basename(file)
target(name)
set_kind("binary")
set_default(false)
add_files("src/" .. name .. ".cpp")
add_tests("default")
add_tests("args", {runargs = {"foo", "bar"}})
add_tests("pass_output", {trim_output = true, runargs = "foo", pass_outputs = "hello foo"})
add_tests("fail_output", {fail_outputs = {"hello2 .*", "hello xmake"}})
end
This example automatically scans the test_*.cpp
source files in the source code directory, and then automatically creates a test target for each file. It is set to set_default(false)
, which means that under normal circumstances, it will not be compiled by default. they.
However, if you execute xmake test
for testing, they will be automatically compiled and then tested. The running effect is as follows:
ruki-2:test ruki$ xmake test
running tests...
[2%]: test_1/args .................................. passed 7.000s
[5%]: test_1/default .................................... passed 5.000s
[ 8%]: test_1/fail_output .................................... passed 5.000s
[ 11%]: test_1/pass_output .................................... passed 6.000s
[ 13%]: test_2/args .................................... passed 7.000s
[ 16%]: test_2/default .................................... passed 6.000s
[ 19%]: test_2/fail_output .................................... passed 6.000s
[ 22%]: test_2/pass_output .................................... passed 6.000s
[ 25%]: test_3/args .................................... passed 7.000s
[ 27%]: test_3/default .................................... passed 7.000s
[ 30%]: test_3/fail_output .................................... passed 6.000s
[ 33%]: test_3/pass_output .................................... passed 6.000s
[ 36%]: test_4/args .................................... passed 6.000s
[ 38%]: test_4/default .................................... passed 6.000s
[ 41%]: test_4/fail_output .................................... passed 5.000s
[ 44%]: test_4/pass_output .................................... passed 6.000s
[ 47%]: test_5/args .................................... passed 5.000s
[ 50%]: test_5/default .................................... passed 6.000s
[ 52%]: test_5/fail_output .................................... failed 6.000s
[ 55%]: test_5/pass_output .................................... failed 5.000s
[ 58%]: test_6/args .................................... passed 7.000s
[ 61%]: test_6/default .................................... passed 6.000s
[ 63%]: test_6/fail_output .................................... passed 6.000s
[ 66%]: test_6/pass_output .................................... passed 6.000s
[ 69%]: test_7/args .................................... failed 6.000s
[ 72%]: test_7/default .................................... failed 7.000s
[ 75%]: test_7/fail_output .................................... failed 6.000s
[ 77%]: test_7/pass_output .................................... failed 5.000s
[ 80%]: test_8/args .................................... passed 7.000s
[ 83%]: test_8/default .................................... passed 6.000s
[ 86%]: test_8/fail_output .................................... passed 6.000s
[ 88%]: test_8/pass_output .................................... failed 5.000s
[ 91%]: test_9/args .................................... passed 6.000s
[ 94%]: test_9/default .................................... passed 6.000s
[ 97%]: test_9/fail_output .................................... passed 6.000s
[100%]: test_9/pass_output .................................... passed 6.000s
80% tests passed, 7 tests failed out of 36, spent 0.242s
我们也可以执行 xmake test -vD
查看详细的测试失败的错误信息:
我们也可以指定运行指定 target 的某个测试:
$ xmake test targetname/testname
或者按模式匹配的方式,运行一个 target 的所有测试,或者一批测试:
$ xmake test targetname/*
$ xmake test targetname/foo*
也可以运行所有 target 的同名测试:
$ xmake test */testname
其实,默认就是并行化运行的,但是我们可以通过 -jN
调整运行的并行度。
$ xmake test -jN
$ xmake test -g "foo"
$ xmake test -g "foo*"
如果没有配置任何参数,仅仅配置了测试名到 add_tests
,那么仅仅测试这个目标程序的是否会运行失败,根据退出代码来判断是否通过测试。
target("test")
add_tests("testname")
我们也可以通过 {runargs = {"arg1", "arg2"}}
的方式,给 add_tests
配置指定测试需要运行的参数。
另外,一个 target 可以同时配置多个测试用例,每个测试用例可独立运行,互不冲突。
target("test")
add_tests("testname", {runargs = "arg1"})
add_tests("testname", {runargs = {"arg1", "arg2"}})
如果我们没有配置 runargs 到 add_tests
,那么我们也会尝试从被绑定的 target 中,获取 set_runargs
设置的运行参数。
target("test")
add_tests("testname")
set_runargs("arg1", "arg2")
我们也可以通过 rundir 设置测试运行的当前工作目录,例如:
target("test")
add_tests("testname", {rundir = os.projectdir()})
如果我们没有配置 rundir 到 add_tests
,那么我们也会尝试从被绑定的 target 中,获取 set_rundir
设置的运行目录。
target("test")
add_tests("testname")
set_rundir("$(projectdir)")
我们也可以通过 runenvs 设置一些运行时候的环境变量,例如:
target("test")
add_tests("testname", {runenvs = {LD_LIBRARY_PATH = "/lib"}})
如果我们没有配置 runenvs 到 add_tests
,那么我们也会尝试从被绑定的 target 中,获取 add_runenvs
设置的运行环境。
target("test")
add_tests("testname")
add_runenvs("LD_LIBRARY_PATH", "/lib")
默认情况下,xmake test
会根据测试运行的退出代码是否为 0,来判断是否测试通过。
当然,我们也可以通过配置测试运行的输出结果是否满足我们的指定的匹配模式,来判断是否测试通过。
主要通过这两个参数控制:
参数 | 说明 |
---|---|
pass_outputs | 如果输出匹配,则测试通过 |
fail_outputs | 如果输出匹配,则测试失败 |
传入 pass_outputs
和 fail_outputs
的是一个 lua 匹配模式的列表,但模式稍微做了一些简化,比如对 *
的处理。
如果要匹配成功,则测试通过,可以这么配置:
target("test")
add_tests("testname1", {pass_outputs = "hello"})
add_tests("testname2", {pass_outputs = "hello *"})
add_tests("testname3", {pass_outputs = {"hello", "hello *"}})
```If the match is successful, the test fails. You can configure it like this:
```lua
target("test")
add_tests("testname1", {fail_outputs = "hello"})
add_tests("testname2", {fail_outputs = "hello *"})
add_tests("testname3", {fail_outputs = {"hello", "hello *"}})
We can also configure them simultaneously:
target("test")
add_tests("testname", {pass_outputs = "foo", fail_outputs = "hello"})
Since some test output results will have some newline or other blank characters at the end, which interferes with the matching mode, we can configure trim_output = true
to truncate the blank characters before matching.
target("test")
add_tests("testname", {trim_output = true, pass_outputs = "foo", fail_outputs = "hello"})
We can also configure {plain = true}
to disable lua pattern matching and only do the most basic flat text matching.
target("test")
add_tests("testname", {plain = true, pass_outputs = "foo", fail_outputs = "hello"})
We can also configure a test group through group = "foo"
for group testing:
target("test")
add_tests("testname1", {group = "foo"})
add_tests("testname2", {group = "foo"})
add_tests("testname3", {group = "bar"})
add_tests("testname4", {group = "bae"})
Where testname1/testname2 is a group foo, and the other two are in another group.
Then, we can use xmake test -g groupname
to perform group testing.
$ xmake test -g "foo"
$ xmake test -g "foo*"
!> Running grouping also supports pattern matching.
In addition, if the group
parameter is not set to add_tests
, we can also get the group name bound to the target by default.
target("test")
add_tests("testname")
set_group("foo")
We have also added before_test
, on_test
and after_test
configuration scripts. Users can customize them in the rule and target fields to implement customized test execution.
target("test")
on_test(function (target, opt)
print(opt.name, opt.runenvs, opt.runargs, opt.pass_outputs)
-- do test
--...
-- passed
return true
-- failed
return false, errors
end)
Among them, all parameters passed into add_tests
can be obtained in opt. We customize the test logic in on_test, and then return true to indicate that the test passed, return false to indicate that the test failed, and then continue to return the error message of test failure.
Since the test target usually does not need to be built during the normal development build phase, we will set set_default(false)
.
target("test")
add_tests("testname")
set_default(false)
However, when running xmake test
for testing, the targets corresponding to these tests will still be automatically built to ensure that they can be run.
$ xmake test
[25%]: cache compiling.release src/main.cpp
[50%]: linking.release test
running tests...
[100%]: test/testname ............................. passed 6.000s
100% tests passed, 0 tests failed out of 1, spent 0.006s
By default, xmake test
will wait until all tests have been run, no matter how many of them failed.
Sometimes, we want to interrupt the test directly if the first test fails, then we can enable it through the following configuration:
set_policy("test.return_zero_on_failure", true)
By default, as long as a test fails, it will return a non-zero exit code when xmake test
is completed. This is very useful for some CI environments and can interrupt other CI scripts to continue running.
Then the trigger signal tells CI that we need to generate test reports and alarms.
Then, if we want to suppress this behavior, we can force the exit code of xmake test
to always be set to 0.
set_policy("test.return_zero_on_failure", true)
Sometimes, we just want to test whether the code compiles or fails without running them. This can be achieved by configuring build_should_pass
and build_should_fail
.
target("test_10")
set_kind("binary")
set_default(false)
add_files("src/compile.cpp")
add_tests("compile_fail", {build_should_fail = true})
target("test_11")
set_kind("binary")
set_default(false)
add_files("src/compile.cpp")
add_tests("compile_pass", {build_should_pass = true})
This is usually used in scenarios with static_assert
in some test code, for example:
template <typename T>
bool foo(T val) {
if constexpr (std::is_same_v<T, int>) {
printf("int!\n");
} else if constexpr (std::is_same_v<T, float>) {
printf("float!\n");
} else {
static_assert(false, "unsupported type");
}
}
int main(int, char**) {
foo("BAD");
return 0;
}
When configuring test cases, we can also configure additional code that needs to be compiled for each test, as well as some macro definitions to implement inline testing.
xmake will compile an independent executable program for each test to run it, but this will not affect the compilation results of the target in the production environment.
target("test_13")
set_kind("binary")
set_default(false)
add_files("src/test_1.cpp")
add_tests("stub_1", {files = "tests/stub_1.cpp", defines = "STUB_1"})
target("test_14")
set_kind("binary")
set_default(false)
add_files("src/test_2.cpp")
add_tests("stub_2", {files = "tests/stub_2.cpp", defines = "STUB_2"})
target("test_15")
set_kind("binary")
set_default(false)
add_files("src/test_1.cpp")
add_tests("stub_n", {files = "tests/stub_n*.cpp", defines = "STUB_N"})
Taking doctest as an example, we can externally unit test without modifying any main.cpp:
add_rules("mode.debug", "mode.release")
add_requires("doctest")
target("doctest")
set_kind("binary")
add_files("src/*.cpp")
for _, testfile in ipairs(os.files("tests/*.cpp")) do
add_tests(path.basename(testfile), {
files = testfile,
remove_files = "src/main.cpp",
languages = "c++11",
packages = "doctest",
defines = "DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN"})
end
Defining DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN will introduce additional main entry function, so we need to configure remove_files to remove the existing main.cpp file.
The running effect is as follows:
ruki-2:doctest ruki$ xmake test
running tests...
[50%]: doctest/test_1 ........................ failed 0.009s
[100%]: doctest/test_2 ........................ passed 0.009s
50% tests passed, 1 tests failed out of 2, spent 0.019s
ruki-2:doctest ruki$ xmake test -v
running tests...
[50%]: doctest/test_1....................... failed 0.026s
[doctest] doctest version is "2.4.11"
[doctest] run with "--help" for options
================================================== =============================
tests/test_1.cpp:7:
TEST CASE: testing the factorial function
tests/test_1.cpp:8: ERROR: CHECK( factorial(1) == 10 ) is NOT correct!
values: CHECK( 1 == 10 )
================================================== =============================
[doctest] test cases: 1 | 0 passed | 1 failed | 0 skipped
[doctest] assertions: 4 | 3 passed | 1 failed |
[doctest] Status: FAILURE!
run failed, exit code: 1
[100%]: doctest/test_2 ........................ passed 0.010s
50% tests passed, 1 tests failed out of 2, spent 0.038s
Usually, add_tests
is only used to run tests on executable programs. Running dynamic libraries requires an additional main entry, so we need to configure an additional executable program to load it, for example:
target("doctest_shared")
set_kind("shared")
add_files("src/foo.cpp")
for _, testfile in ipairs(os.files("tests/*.cpp")) do
add_tests(path.basename(testfile), {
kind = "binary",
files = testfile,
languages = "c++11",
packages = "doctest",
defines = "DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN"})
end
Each unit test can be changed to a binary executable program through kind = "binary"
, and the main entry function can be introduced through DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
.
This enables external runnable unit tests in dynamic library targets.
In previous versions, we could implement type detection through check_csnippets
and output = true
.
check_csnippets("INT_SIZE", 'printf("%d", sizeof(int)); return 0;', {output = true, number = true})
But this way is to extract the type size information by trying to run the test code, and then getting the running output results.
This does not apply to cross-compilation.
In version 2.8.5, we added the check_sizeof
auxiliary interface, which can extract type size information by directly parsing the binary file of the test program.
Since there is no need to run tests, this method not only supports cross-compilation, but also greatly improves detection efficiency and is simpler to use.
includes("@builtin/check")
target("test")
set_kind("static")
add_files("*.cpp")
check_sizeof("LONG_SIZE", "long")
check_sizeof("STRING_SIZE", "std::string", {includes = "string"})
$ xmake f -c
checking for LONG_SIZE ... 8
checking for STRING_SIZE ... 24
Alternatively, I can also check in the script domain via target:check_sizeof
.
Apple has added build support for visionOS devices in Xcode15, so we also support it as soon as possible. You only need to execute:
$ xmake f -p applexros
$ xmake
This will complete the construction of the visionOS/XROS platform.
Finally, we also provide a small tool module that can be used to quickly merge all c/c++ and header file source codes in a specified target into a single source file.
A single source code file similar to sqlite3.c will be generated. Users can decide whether to use this function according to their actual needs.
When merging, Xmake will expand all internal includes header files and generate DAG, which will be introduced through topological sorting.
By default it will handle the merging of all targets, for example:
$ xmake l cli.amalgamate
build/tbox.c generated!
build/tbox.h generated!
We can also specify the targets required for the merge:
$ xmake l cli.amalgamate tbox
build/tbox.c generated!
build/tbox.h generated!
You can also specify a custom unique ID macro definition when merging each source file to handle symbol conflicts.
$ xmake l cli.amalgamate -u MY_UNIQUEU_ID
build/tbox.c generated!
build/tbox.h generated!
If there are symbols with the same name in multiple source files, you can determine whether the MY_UNIQUEU_ID
macro is defined. If it is defined, it means it is in a single file, and you can handle the symbols with the same name in the source code yourself.
#ifdef MY_UNIQUEU_ID
//do something
#endif
We can also specify the output location:
$ xmake l cli.amalgamate -o /xxx
/xxx/tbox.c generated!
/xxx/tbox.h generated!
Through this strategy, we can quickly and easily set up and enable Windows UAC.
It supports the following levels:
- invoker: asInvoker
- admin: requireAdministrator
- highest: highestAvailable
For example:
set_policy("windows.manifest.uac", "admin")
It is equivalent to setting
if is_plat("windows") then
add_ldflags("/manifest:embed", {"/manifestuac:level='requireAdministrator' uiAccess='false'"}, {force = true, expand = false})
end
But it is more convenient and concise, and there is no need to judge the platform, other platforms are automatically ignored.
We can also set the uiAccess of Windows UAC through the windows.manifest.uac.ui
policy. If it is not set, the default is false.
set_policy("windows.manifest.uac.ui", true)
- #1452: Improve link mechanism and order
- #1438: Support code amalgamation
-
#3381: Add
xmake test
support - #4276: Support custom scope api
- #4286: Add Apple XROS support
- #4345: Support check sizeof
- #4369: Add windows.manifest.uac policy
- #4284: Improve builtin includes
- #4256: Fix intellisense for vsxmake/c++modules