xmake 是一个基于 Lua 的轻量级跨平台构建工具,使用 xmake.lua 维护项目构建,相比 makefile/CMakeLists.txt ,配置语法更加简洁直观,对新手非常友好,短时间内就能快速入门,能够让用户把更多的精力集中在实际的项目开发上。
这个版本,我们增加了大量重量级的新特性,例如:Nim 语言项目的构建支持,Keil MDK ,Circle 和 Wasi 工具链支持。
另外,我们对 C++20 Modules 进行了大改进,不仅支持最新 gcc-11, clang 和 msvc 编译器,而且还得模块间依赖做了自动分析,实现最大程度的并行化编译支持。
最后,还有一个比较有用的特性就是 Unity Build 支持,通过它我们可以对 C++ 代码的编译速度做到很大程度的提升。
最近,我们新增了对 Nimlang 项目的构建支持,相关 issues 见:#1756
我们可以使用 xmake create
命令创建空工程。
xmake create -l nim -t console test
xmake create -l nim -t static test
xmake create -l nim -t shared test
add_rules("mode.debug", "mode.release")
target("test")
set_kind("binary")
add_files("src/main.nim")
$ xmake -v
[ 33%]: linking.release test
/usr/local/bin/nim c --opt:speed --nimcache:build/.gens/test/macosx/x86_64/release/nimcache -o:b
uild/macosx/x86_64/release/test src/main.nim
[100%]: build ok!
add_rules("mode.debug", "mode.release")
target("foo")
set_kind("static")
add_files("src/foo.nim")
target("test")
set_kind("binary")
add_deps("foo")
add_files("src/main.nim")
$ xmake -v
[ 33%]: linking.release libfoo.a
/usr/local/bin/nim c --opt:speed --nimcache:build/.gens/foo/macosx/x86_64/release/nimcache --app
:staticlib --noMain --passC:-DNimMain=NimMain_B6D5BD02 --passC:-DNimMainInner=NimMainInner_B6D5B
D02 --passC:-DNimMainModule=NimMainModule_B6D5BD02 --passC:-DPreMain=PreMain_B6D5BD02 --passC:-D
PreMainInner=PreMainInner_B6D5BD02 -o:build/macosx/x86_64/release/libfoo.a src/foo.nim
[ 66%]: linking.release test
/usr/local/bin/nim c --opt:speed --nimcache:build/.gens/test/macosx/x86_64/release/nimcache --pa
ssL:-Lbuild/macosx/x86_64/release --passL:-lfoo -o:build/macosx/x86_64/release/test src/main.nim
[100%]: build ok!
add_rules("mode.debug", "mode.release")
target("foo")
set_kind("shared")
add_files("src/foo.nim")
target("test")
set_kind("binary")
add_deps("foo")
add_files("src/main.nim")
$ xmake -rv
[ 33%]: linking.release libfoo.dylib
/usr/local/bin/nim c --opt:speed --nimcache:build/.gens/foo/macosx/x86_64/release/nimcache --app
:lib --noMain -o:build/macosx/x86_64/release/libfoo.dylib src/foo.nim
[ 66%]: linking.release test
/usr/local/bin/nim c --opt:speed --nimcache:build/.gens/test/macosx/x86_64/release/nimcache --pa
ssL:-Lbuild/macosx/x86_64/release --passL:-lfoo -o:build/macosx/x86_64/release/test src/main.nim
[100%]: build ok!
add_rules("mode.debug", "mode.release")
target("foo")
set_kind("static")
add_files("src/*.c")
target("test")
set_kind("binary")
add_deps("foo")
add_files("src/main.nim")
完整例子见:Nimble Package Example
add_rules("mode.debug", "mode.release")
add_requires("nimble::zip >0.3")
target("test")
set_kind("binary")
add_files("src/main.nim")
add_packages("nimble::zip")
main.nim
import zip/zlib
echo zlibVersion()
完整例子见:Native Package Example
add_rules("mode.debug", "mode.release")
add_requires("zlib")
target("test")
set_kind("binary")
add_files("src/main.nim")
add_packages("zlib")
main.nim
proc zlibVersion(): cstring {.cdecl, importc}
echo zlibVersion()
我们知道,C++ 代码编译速度通常很慢,因为每个代码文件都需要解析引入的头文件。
而通过 Unity Build ,我们通过将多个 cpp 文件组合成一个来加速项目的编译,其主要好处是减少了解析和编译包含在多个源文件中的头文件内容的重复工作,头文件的内容通常占预处理后源文件中的大部分代码。
Unity 构建还通过减少编译链创建和处理的目标文件的数量来减轻由于拥有大量小源文件而导致的开销,并允许跨形成统一构建任务的文件进行过程间分析和优化(类似于效果链接时优化)。
它可以极大提升 C/C++ 代码的编译速度,通常会有 30% 的速度提升,不过根据项目的复杂程度不同,其带来的效益还是要根据自身项目情况而定。
xmake 在 v2.5.9 版本中,也已经支持了这种构建模式。相关 issues 见 #1019。
我们提供了两个内置规则,分别处理对 C 和 C++ 代码的 Unity Build 。
add_rules("c.unity_build")
add_rules("c++.unity_build")
默认情况下,只要设置上述规则,就会启用 Batch 模式的 Unity Build ,也就是 xmake 自动根据项目代码文件,自动组织合并。
target("test")
set_kind("binary")
add_includedirs("src")
add_rules("c++.unity_build", {batchsize = 2})
add_files("src/*.c", "src/*.cpp")
我们可以额外通过设置 {batchsize = 2}
参数到规则,来指定每个合并 Batch 的大小数量,这里也就是每两个 C++ 文件自动合并编译。
编译效果大概如下:
$ xmake -r
[ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_642A245F.cpp
[ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_bar.cpp
[ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_73161A20.cpp
[ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_F905F036.cpp
[ 11%]: ccache compiling.release build/.gens/test/unity_build/unity_foo.cpp
[ 11%]: ccache compiling.release build/.gens/test/unity_build/main.c
[ 77%]: linking.release test
[100%]: build ok
由于我们仅仅启用了 C++ 的 Unity Build ,所以 C 代码还是正常挨个编译。另外在 Unity Build 模式下,我们还是可以做到尽可能的并行编译加速,互不冲突。
如果没有设置 batchsize
参数,那么默认会吧所有文件合并到一个文件中进行编译。
如果上面的 Batch 模式自动合并效果不理想,我们也可以使用自定义分组,来手动配置哪些文件合并到一起参与编译,这使得用户更加地灵活可控。
target("test")
set_kind("binary")
add_rules("c++.unity_build", {batchsize = 0}) -- disable batch mode
add_files("src/*.c", "src/*.cpp")
add_files("src/foo/*.c", {unity_group = "foo"})
add_files("src/bar/*.c", {unity_group = "bar"})
我们使用 {unity_group = "foo"}
来指定每个分组的名字,以及包含了哪些文件,每个分组的文件都会单独被合并到一个代码文件中去。
另外,batchsize = 0
也强行禁用了 Batch 模式,也就是说,没有设置 unity_group 分组的代码文件,我们还是会单独编译它们,也不会自动开启自动合并。
我们只要把上面的 batchsize = 0
改成非 0 值,就可以让分组模式下,剩余的代码文件继续开启 Batch 模式自动合并编译。
target("test")
set_kind("binary")
add_includedirs("src")
add_rules("c++.unity_build", {batchsize = 2})
add_files("src/*.c", "src/*.cpp")
add_files("src/foo/*.c", {unity_group = "foo"})
add_files("src/bar/*.c", {unity_group = "bar"})
如果是 Batch 模式下,由于是自动合并操作,所以默认会对所有文件执行合并,但如果有些代码文件我们不想让它参与合并,那么我们也可以通过 {unity_ignored = true}
去忽略它们。
target("test")
set_kind("binary")
add_includedirs("src")
add_rules("c++.unity_build", {batchsize = 2})
add_files("src/*.c", "src/*.cpp")
add_files("src/test/*.c", {unity_ignored = true}) -- ignore these files
尽管 Unity Build 带啦的收益不错,但是我们还是会遇到一些意外的情况,比如我们的两个代码文件里面,全局命名空间下,都存在相同名字的全局变量和函数。
那么,合并编译就会带来编译冲突问题,编译器通常会报全局变量重定义错误。
为了解决这个问题,我们需要用户代码上做一些修改,然后配合构建工具来解决。
比如,我们的 foo.cpp 和 bar.cpp 都有全局变量 i 。
foo.cpp
namespace {
int i = 42;
}
int foo()
{
return i;
}
bar.cpp
namespace {
int i = 42;
}
int bar()
{
return i;
}
那么,我们合并编译就会冲突,我们可以引入一个 Unique ID 来隔离全局的匿名空间。
foo.cpp
namespace MY_UNITY_ID {
int i = 42;
}
int foo()
{
return MY_UNITY_ID::i;
}
bar.cpp
namespace MY_UNITY_ID {
int i = 42;
}
int bar()
{
return MY_UNITY_ID::i;
}
接下来,我们还需要保证代码合并后, MY_UNITY_ID
在 foo 和 bar 中的定义完全不同,可以按文件名算一个唯一 ID 值出来,互不冲突,也就是实现下面的合并效果:
#define MY_UNITY_ID <hash(foo.cpp)>
#include "foo.c"
#undef MY_UNITY_ID
#define MY_UNITY_ID <hash(bar.cpp)>
#include "bar.c"
#undef MY_UNITY_ID
这看上去似乎很麻烦,但是用户不需要关心这些,xmake 会在合并时候自动处理它们,用户只需要指定这个 Unique ID 的名字就行了,例如下面这样:
target("test")
set_kind("binary")
add_includedirs("src")
add_rules("c++.unity_build", {batchsize = 2, uniqueid = "MY_UNITY_ID"})
add_files("src/*.c", "src/*.cpp")
处理全局变量,还有全局的重名宏定义,函数什么的,都可以采用这种方式来避免冲突。
xmake 采用 .mpp
作为默认的模块扩展名,但是也同时支持 .ixx
, .cppm
, .mxx
等扩展名。
早期,xmake 试验性支持过 C++ Modules TS ,但是那个时候,gcc 还不能很好的支持,并且模块间的依赖也不支持。
最近,我们对 xmake 做了大量改进,已经完整支持 gcc-11/clang/msvc 的 C++20 Modules 构建支持,并且能够自动分析模块间的依赖关系,实现最大化并行编译。
同时,对新版本的 clang/msvc 也做了更好地处理。
set_languages("c++20")
target("test")
set_kind("binary")
add_files("src/*.cpp", "src/*.mpp")
更多例子见:C++ Modules
上个版本,我们增加了对 Lua5.3 运行时支持,而在这个版本中,我们进一步升级 Lua 运行时到 5.4 ,相比 5.3 ,运行性能和内存利用率上都有很大的提升。
不过,目前 xmake 的默认运行时还是 luajit ,预计 2.6.1 版本(也就是下个版本),会正式切到 Lua5.4 作为默认的运行时。
尽管切换了 Lua 运行时,但是对于用户端,完全是无感知的,并且完全兼容现有工程配置,因为 xmake 原本就对暴露的 api 提供了一层封装, 对于 lua 版本之间存在兼容性问题的接口,例如 setfenv, ffi 等都隐藏在内部,原本就没有暴露给用户使用。
我们在这个版本中,还新增了 Keil/MDK 嵌入式编译工具链的支持,相关例子工程:Example
xmake 会自动探测 Keil/MDK 安装的编译器,相关 issues #1753。
$ xmake f -p cross -a cortex-m3 --toolchain=armcc -c
$ xmake
$ xmake f -p cross -a cortex-m3 --toolchain=armclang -c
$ xmake
target("hello")
add_deps("foo")
add_rules("mdk.console")
add_files("src/*.c", "src/*.s")
add_defines("__EVAL", "__MICROLIB")
add_includedirs("src/lib/cmsis")
add_rules("mode.debug", "mode.release")
target("foo")
add_rules("mdk.static")
add_files("src/foo/*.c")
之前我们支持了 wasm 平台的 emcc 工具链来构建 wasm 程序,而这里,我们新加了另外一个启用了 WASI 的 Wasm 工具链来替换 emcc 。
$ xmake f -p wasm --toolchain=wasi
$ xmake
我们还新增了 circle 编译器的支持,这是个新的 C++20 编译器,额外附带了一些有趣的编译期元编程特性,有兴趣的同学可以到官网查看: https://www.circle-lang.org/
$ xmake f --toolchain=circle
$ xmake
如果用户额外安装了 gcc-11, gcc-10 等特定版本的 gcc 工具链,在本地的 gcc 程序命名可能是 /usr/bin/gcc-11
。
一种办法是通过 xmake f --cc=gcc-11 --cxx=gcc-11 --ld=g++-11
挨个指定配置来切换,但非常繁琐。
所以,xmake 也提供了更加快捷的切换方式:
$ xmake f --toolchain=gcc-11 -c
$ xmake
只需要指定 gcc-11
对应的版本名,就可以快速切换整个 gcc 工具链。
xmake 提供了 check_features 辅助接口来检测编译器特性。
includes("check_features.lua")
target("test")
set_kind("binary")
add_files("*.c")
add_configfiles("config.h.in")
configvar_check_features("HAS_CONSTEXPR", "cxx_constexpr")
configvar_check_features("HAS_CONSEXPR_AND_STATIC_ASSERT", {"cxx_constexpr", "c_static_assert"}, {languages = "c++11"})
${define HAS_CONSTEXPR}
${define HAS_CONSEXPR_AND_STATIC_ASSERT}
config.h
/* #undef HAS_CONSTEXPR */
#define HAS_CONSEXPR_AND_STATIC_ASSERT 1
而在 2.5.9 版本中,我们新增了 c++17 特性检测:
特性名 |
---|
cxx_aggregate_bases |
cxx_aligned_new |
cxx_capture_star_this |
cxx_constexpr |
cxx_deduction_guides |
cxx_enumerator_attributes |
cxx_fold_expressions |
cxx_guaranteed_copy_elision |
cxx_hex_float |
cxx_if_constexpr |
cxx_inheriting_constructors |
cxx_inline_variables |
cxx_namespace_attributes |
cxx_noexcept_function_type |
cxx_nontype_template_args |
cxx_nontype_template_parameter_auto |
cxx_range_based_for |
cxx_static_assert |
cxx_structured_bindings |
cxx_template_template_args |
cxx_variadic_using |
还新增了 c++20 特性检测:
特性名 |
---|
cxx_aggregate_paren_init |
cxx_char8_t |
cxx_concepts |
cxx_conditional_explicit |
cxx_consteval |
cxx_constexpr |
cxx_constexpr_dynamic_alloc |
cxx_constexpr_in_decltype |
cxx_constinit |
cxx_deduction_guides |
cxx_designated_initializers |
cxx_generic_lambdas |
cxx_impl_coroutine |
cxx_impl_destroying_delete |
cxx_impl_three_way_comparison |
cxx_init_captures |
cxx_modules |
cxx_nontype_template_args |
cxx_using_enum |
xmake 自带的 xrepo 包管理工具,现在已经可以很好的支持包虚拟机环境管理,类似 nixos 的 nixpkgs 。
我们可以通过在当前目录下,添加 xmake.lua 文件,定制化一些包配置,然后进入特定的包虚拟环境。
add_requires("zlib 1.2.11")
add_requires("python 3.x", "luajit")
$ xrepo env shell
> python --version
> luajit --version
我们也可以在 xmake.lua 配置加载对应的工具链环境,比如加载 vs 的编译环境。
set_toolchains("msvc")
我们可以使用下面的命令,把指定的虚拟环境配置全局注册到系统中,方便快速切换。
$ xrepo env --add /tmp/base.lua
这个时候,我们就保存了一个名叫 base 的全局虚拟环境,我们可以通过 list 命令去查看它。
$ xrepo env --list
/Users/ruki/.xmake/envs:
- base
envs(1) found!
我们也可以删除它。
$ xrepo env --remove base
如果我们注册了多个虚拟环境,我们也可以快速切换它们。
$ xrepo env -b base shell
> python --version
或者直接加载指定虚拟环境运行特定命令
$ xrepo env -b base python --version
xrepo env -b/--bind
就是绑定指定的虚拟环境,更多详情见:#1762
对于 target ,我们新增了 headeronly
目标类型,这个类型的目标程序,我们不会实际编译它们,因为它没有源文件需要被编译。
但是它包含了头文件列表,这通常用于 headeronly 库项目的安装,IDE 工程的文件列表生成,以及安装阶段的 cmake/pkgconfig 导入文件的生成。
例如:
add_rules("mode.release", "mode.debug")
target("foo")
set_kind("headeronly")
add_headerfiles("src/foo.h")
add_rules("utils.install.cmake_importfiles")
add_rules("utils.install.pkgconfig_importfiles")
更多详情见:#1747
现在 cmake 已经是事实上的标准,所以 CMake 提供的 find_package 已经可以查找大量的库和模块,我们完全复用 cmake 的这部分生态来扩充 xmake 对包的集成。
我们可以通过 find_package("cmake::xxx")
去借助 cmake 来找一些包,xmake 会自动生成一个 cmake 脚本来调用 cmake 的 find_package 去查找一些包,获取里面包信息。
例如:
$ xmake l find_package cmake::ZLIB
{
links = {
"z"
},
includedirs = {
"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.
15.sdk/usr/include"
},
linkdirs = {
"/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.
15.sdk/usr/lib"
}
}
$ xmake l find_package cmake::LibXml2
{
links = {
"xml2"
},
includedirs = {
"/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/usr/include/libxml2"
},
linkdirs = {
"/usr/lib"
}
}
find_package("cmake::OpenCV", {required_version = "4.1.1"})
find_package("cmake::Boost", {components = {"regex", "system"}})
find_package("cmake::Boost", {components = {"regex", "system"}, presets = {Boost_USE_STATIC_LIB = true}})
set(Boost_USE_STATIC_LIB ON) -- will be used in FindBoost.cmake
find_package(Boost REQUIRED COMPONENTS regex system)
find_package("cmake::OpenCV", {envs = {CMAKE_PREFIX_PATH = "xxx"}})
mydir/cmake_modules/FindFoo.cmake
find_package("cmake::Foo", {moduledirs = "mydir/cmake_modules"})
package("xxx")
on_fetch(function (package, opt)
return package:find_package("cmake::xxx", opt)
end)
package_end()
add_requires("xxx")
package("boost")
add_configs("regex", { description = "Enable regex.", default = false, type = "boolean"})
on_fetch(function (package, opt)
opt.components = {}
if package:config("regex") then
table.insert(opt.components, "regex")
end
return package:find_package("cmake::Boost", opt)
end)
package_end()
add_requires("boost", {configs = {regex = true}})
相关 issues: #1632
我们进一步改进了 cmake 生成器,现在可以将 rule 里面自定义的脚本序列化成命令列表,一起生成到 CMakelists.txt
不过目前只能支持 batchcmds 系列脚本的序列化。
rule("foo")
after_buildcmd(function (target, batchcmds, opt)
batchcmds:show("hello xmake!")
batchcmds:cp("xmake.lua", "/tmp/")
-- batchcmds:execv("echo", {"hello", "world!"})
-- batchcmds:runv("echo", {"hello", "world!"})
end)
target("test")
set_kind("binary")
add_rules("foo")
add_files("src/*.c")
它将会生成类似如下的 CMakelists.txt
# ...
add_custom_command(TARGET test
POST_BUILD
COMMAND echo hello xmake!
VERBATIM
)
add_custom_command(TARGET test
POST_BUILD
COMMAND cp xmake.lua /tmp/
VERBATIM
)
target_sources(test PRIVATE
src/main.c
)
不过 cmake 的 ADD_CUSTOM_COMMAND
PRE_BUILD 实际效果在不同生成器上,差异比较大,无法满足我们的需求,因此我们做了很多处理来支持它。
相关 issues: #1735
我们还改进了 get.sh 安装脚本,来更好地支持 nixOS 。
set_kind("headeronly")
更好的处理 headeronly 库的安装xmake l cli.amalgamate
命令支持代码合并xrepo env
管理和切换指定的环境配置这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.