xmake v2.5.9 釋出,改進 C++20 模組,並支援 Nim, Keil MDK 和 Unity Build

waruqi發表於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 編譯器,額外附帶了一些有趣的編譯期超程式設計特性,有興趣的同學可以到官網檢視:https://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 選項

相關文章