xmake v2.5.9 發佈,改進 C++20 模塊,並支持 Nim, Keil MDK 和 Unity Build

  • 2021 年 10 月 31 日
  • 筆記

xmake 是一個基於 Lua 的輕量級跨平台構建工具,使用 xmake.lua 維護項目構建,相比 makefile/CMakeLists.txt,配置語法更加簡潔直觀,對新手非常友好,短時間內就能快速入門,能夠讓用戶把更多的精力集中在實際的項目開發上。

這個版本,我們增加了大量重量級的新特性,例如:Nim 語言項目的構建支持,Keil MDK,Circle 和 Wasi 工具鏈支持。

另外,我們對 C++20 Modules 進行了大改進,不僅支持最新 gcc-11, clang 和 msvc 編譯器,而且還得模塊間依賴做了自動分析,實現最大程度的並行化編譯支持。

最後,還有一個比較有用的特性就是 Unity Build 支持,通過它我們可以對 C++ 代碼的編譯速度做到很大程度的提升。

新特性介紹

Nimlang 項目構建

最近,我們新增了對 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!

C 代碼混合編譯

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 依賴包集成

完整例子見: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 依賴包集成

完整例子見: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()

Unity Build 加速

我們知道,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 模式

默認情況下,只要設置上述規則,就會啟用 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 參數,那麼默認會吧所有文件合併到一個文件中進行編譯。

Group 模式

如果上面的 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 分組的代碼文件,我們還是會單獨編譯它們,也不會自動開啟自動合併。

Batch 和 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

Unique ID

儘管 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")

處理全局變量,還有全局的重名宏定義,函數什麼的,都可以採用這種方式來避免衝突。

C++20 Modules

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.4 運行時支持

上個版本,我們增加了對 Lua5.3 運行時支持,而在這個版本中,我們進一步升級 Lua 運行時到 5.4,相比 5.3,運行性能和內存利用率上都有很大的提升。

不過,目前 xmake 的默認運行時還是 luajit,預計 2.6.1 版本(也就是下個版本),會正式切到 Lua5.4 作為默認的運行時。

儘管切換了 Lua 運行時,但是對於用戶端,完全是無感知的,並且完全兼容現有工程配置,因為 xmake 原本就對暴露的 api 提供了一層封裝,
對於 lua 版本之間存在兼容性問題的接口,例如 setfenv, ffi 等都隱藏在內部,原本就沒有暴露給用戶使用。

Keil MDK 工具鏈支持

我們在這個版本中,還新增了 Keil/MDK 嵌入式編譯工具鏈的支持,相關例子工程:Example

xmake 會自動探測 Keil/MDK 安裝的編譯器,相關 issues #1753

使用 armcc 編譯

$ xmake f -p cross -a cortex-m3 --toolchain=armcc -c
$ xmake

使用 armclang 編譯

$ 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")

Wasi 工具鏈支持

之前我們支持了 wasm 平台的 emcc 工具鏈來構建 wasm 程序,而這裡,我們新加了另外一個啟用了 WASI 的 Wasm 工具鏈來替換 emcc。

$ xmake f -p wasm --toolchain=wasi
$ xmake

Circle 工具鏈支持

我們還新增了 circle 編譯器的支持,這是個新的 C++20 編譯器,額外附帶了一些有趣的編譯期元編程特性,有興趣的同學可以到官網查看://www.circle-lang.org/

$ xmake f --toolchain=circle
$ xmake

gcc-8/9/10/11 特定版本支持

如果用戶額外安裝了 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 工具鏈。

C++17/20 編譯器特性檢測

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"})

config.h.in

${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

Xrepo 包虛擬環境管理

進入虛擬環境

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

Header Only 目標類型

對於 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 已經是事實上的標準,所以 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"}})

指定自定義 FindFoo.cmake 模塊腳本目錄

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

添加自定義命令到 CMakelists.txt

我們進一步改進了 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

改進對 NixOS 的安裝支持

我們還改進了 get.sh 安裝腳本,來更好地支持 nixOS。

更新內容

新特性

  • #1736: 支持 wasi-sdk 工具鏈
  • 支持 Lua 5.4 運行時
  • 添加 gcc-8, gcc-9, gcc-10, gcc-11 工具鏈
  • #1623: 支持 find_package 從 cmake 查找包
  • #1747: 添加 set_kind("headeronly") 更好的處理 headeronly 庫的安裝
  • #1019: 支持 Unity build
  • #1438: 增加 xmake l cli.amalgamate 命令支持代碼合併
  • #1765: 支持 nim 語言
  • #1762: 為 xrepo env 管理和切換指定的環境配置
  • #1767: 支持 Circle 編譯器
  • #1753: 支持 Keil/MDK 的 armcc/armclang 工具鏈
  • #1774: 添加 table.contains api
  • #1735: 添加自定義命令到 cmake 生成器
  • #1781: 改進 get.sh 安裝腳本支持 nixos

改進

  • #1528: 檢測 c++17/20 特性
  • #1729: 改進 C++20 modules 對 clang/gcc/msvc 的支持,支持模塊間依賴編譯和並行優化
  • #1779: 改進 ml.exe/x86,移除內置的 -Gd 選項