C/C++ 構建系統,我用 xmake

waruqi發表於2021-05-06

XMake 是什麼

XMake 是一個基於 Lua 的 現代化 C/C++ 構建系統。

它的語法簡潔易上手,對新手友好,即使完全不會 lua 也能夠快速入門,並且完全無任何依賴,輕量,跨平臺。

同時,它也是一個自滿足的構建系統,擁有強大的包管理系統,快速的構建引擎。

相比 Ninja/Scons/Make 作為 Build backend,CMake/Meson 作為 Project Generator,那麼 XMake 就是這兩者外加一個包管理。

xmake = Build backend + Project Generator + Package Manager

因此,只需要安裝一個不到 3M 的 XMake 安裝包,你就可以不用再安裝其他各種工具,甚至連 make 都不需要安裝,也不需要安裝 Python、Java 等重量級的執行時環境,就可以開始您的 C/C++ 開發之旅。

也許,有人會說,編譯器總需要安裝的吧。這也不是必須的,因為 XMake 的包管理也支援自動遠端拉取需要的各種編譯工具鏈,比如:llvm, Mingw, Android NDK 或者交叉編譯工具鏈。

為什麼要做 XMake

每當在 Reddit 社群跟別人討論起 XMake,大家總是會拿下面這張圖來吐槽。

儘管有些無奈,也被吐槽的有些麻木了,不過我還是想說明下,做 XMake 的初衷,並不是為了分裂 C/C++ 生態,相反,XMake 儘可能地複用了現有生態。

同時也讓使用者在開發 C/C++ 專案的時候,擁有與其他語言一樣的良好體驗,比如:Rust/Cargo,Nodejs/Npm, Dlang/Dub,不再為到處找第三包,研究如何移植編譯而折騰。

因此,如果您還不瞭解 XMake,請不要過早下定論,可以先嚐試使用下,或者花點時間看完下文的詳細介紹。

XMake 的特性和優勢

經常有人問我 XMake 有什麼特別之處,相比現有 CMake、Meson 此類構建工具有什麼優勢,我為什麼要使用 XMake 而不是 CMake?

先說特點和優勢,XMake 有以下幾點:

  • 簡潔易學的配置語法,非 DSL
  • 強大的包管理,支援語義版本,工具鏈管理
  • 足夠輕量,無依賴
  • 極速編譯,構建速度和 Ninja 一樣快
  • 簡單方便的多平臺、工具鏈切換
  • 完善的外掛系統
  • 靈活的構建規則

至於 CMake,畢竟已成事實上的標準,生態完善,功能強大。

我從沒想過讓 XMake 去替代它,也替代不了,完全不是一個量級的,但是 CMake 也有許多為人所詬病的短板,比如:語法複雜難懂,包管理支援不完善等等。

因此使用 XMake 可以作為一種補充,對於那些想要簡單快速入門 C/C++ 開發的新手,或者想要更加方便易用的包管理,或者想臨時快速寫一些短小的測試專案。

XMake 都可以幫他們提升開發效率,讓其更加關注 C/C++ 專案本身,而不是花更多的時間在構建工具和開發環境上。

下面,我來具體介紹 XMake 的這些主要特性。

語法簡潔易上手

CMake 自己設計一門 DSL 語言用來做專案配置,這對使用者來講提高了學習成本,而且它的語法可讀性不是很直觀,很容易寫出過於複雜的配置指令碼,也提高了維護成本。

而 XMake 複用現有知名的 Lua 語言作為基礎,並且其上提供了更加簡單直接的配置語法。

Lua 本身就是一門簡單輕量的膠水語言,關鍵字和內建型別就那麼幾種,看個一篇文章,就能基本入門了,並且相比 DSL,能夠從網上更方便的獲取到大量相關資料和教程。

基礎語法

不過,還是有人會吐槽:那不是還得學習 Lua 麼?

其實也不用,XMake 採用了 描述域指令碼域 分離的方式,使得初學者使用者在 80% 的情況下,只需要在描述域以更簡單直接的方式來配置,完全可以不把它當成 Lua 指令碼,例如:

target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_files("test/*.c", "example/**.cpp")

如果因為,看著有括號,還是像指令碼語言的函式呼叫,那我們也可以這麼寫(是否帶括號看個人喜好,不過我個人還是建議使用上面的方式)

target "test"
    set_kind "binary"
    add_files "src/*.c"
    add_files "test/*.c"
    add_files "example/**.cpp"

我們只需要知道常用配置介面,即使不完全不會 Lua 也能快速配置了。

我們可以對比下 CMake 的配置:

add_executable(test "")
file(GLOB SRC_FILES "src/*.c")
file(GLOB TEST_FILES "test/*.c")
file(GLOB_RECURSE EXAMPLE_FILES "example/*.cpp")
target_sources(test PRIVATE
    ${SRC_FILES}
    ${TEST_FILES}
    ${EXAMPLE_FILES}
)

哪個更直觀可讀,一目瞭然。

條件配置

如果,你已經初步瞭解了一些 Lua 等基礎知識,比如 if then 等條件判斷,那麼可以進一步做一些條件配置。

target("test")
    set_kind("binary")
    add_files("src/main.c")
    if is_plat("macosx", "linux") then
        add_defines("TEST1", "TEST2")
    end
    if is_plat("windows") and is_mode("release") then
        add_cxflags("-Ox", "-fp:fast")
    end

繼續對比下 CMake 版本配置:

add_executable(test "")
if (APPLE OR LINUX)
    target_compile_definitions(test PRIVATE TEST1 TEST2)
endif()
if (WIN32)
    target_compile_options(test PRIVATE $<$<CONFIG:Release>:-Ox -fp:fast>)
endif()
target_sources(test PRIVATE
    src/main.c
)

複雜指令碼

如果你已經晉升為 XMake 的高階玩家,Lua 語法瞭然於胸,想要更加靈活的定製化配置需要,並且描述域的幾行簡單配置已經滿足不了你的需求。

那麼 XMake 也提供了更加完整的 Lua 指令碼定製化能力,你可以寫任何複雜的指令碼。

比如在構建之前,對所有原始檔進行一些預處理,在構建之後,執行外部 gradle 命令進行後期打包,甚至我們還可以重寫內部連結規則,實現深度定製編譯,我們可以通過import 介面,匯入內建的 linker 擴充套件模組,實現複雜靈活的連結過程。

target("test")
    set_kind("binary")
    add_files("src/*.c")
    before_build_file(function (target, sourcefile)
        io.replace(sourcefile, "#define HAVE_XXX 1", "#define HAVE_XXX 0")
    end)
    on_link(function (target)
        import("core.tool.linker")
        linker.link("binary", "cc", target:objectfiles(), target:targetfile(), {target = target})
    end)
    after_build(function (target)
        if is_plat("android" then
            os.cd("android/app")
            os.exec("./gradlew app:assembleDebug")
        end
    end)

如果換成 CMake,也可以 add_custom_command 裡面實現,不過裡面似乎只能簡單的執行一些批處理命令,沒法做各種複雜的邏輯判斷,模組載入,自定義配置指令碼等等。

當然,使用 cmake 肯定也能實現上面描述的功能,但絕對不會那麼簡單。

如果有熟悉 cmake 的人,也可以嘗試幫忙完成下面的配置:

add_executable(test "")
file(GLOB SRC_FILES "src/*.c")
add_custom_command(TARGET test PRE_BUILD
    -- TODO
    COMMAND echo hello
)
add_custom_command(TARGET test POST_BUILD
    COMMAND cd android/app
    COMMAND ./gradlew app:assembleDebug
)
-- How can we override link stage?
target_sources(test PRIVATE
    ${SRC_FILES}
)

強大的包管理

眾所周知,做 C/C++ 相關專案開發,最頭大的就是各種依賴包的整合,由於沒有像 Rust/Cargo 那樣完善的包管理系統。

因此,我們每次想使用一個第三方庫,都需要各種找,研究各種平臺的移植編譯,還經常遇到各種編譯問題,極大耽誤了開發者時間,無法集中精力去投入到實際的專案開發中去。

好不容易當前平臺搞定了,換到其他平臺,有需要重新折騰一遍依賴包,為了解決這個問題,出現了一些第三方的包管理器,比如 vcpkg/conan/conda等等,但有些不支援語義版本,有些支援的平臺有限,但不管怎樣,總算是為解決 C/C++ 庫的依賴管理邁進了很大一步。

但是,光有包管理器,C/C++ 專案中使用它們還是比較麻煩,因為還需要對應構建工具能夠很好的對其進行整合支援才行。

CMake 和 Vcpkg

我們先來看下 CMake 和 Vcpkg 的整合支援:

cmake_minimum_required(VERSION 3.0)
project(test)
find_package(unofficial-sqlite3 CONFIG REQUIRED)
add_executable(main main.cpp)
target_link_libraries(main PRIVATE unofficial::sqlite3::sqlite3)

缺點:

  • 還需要額外配置 -DCMAKE_TOOLCHAIN_FILE=<vcpkg_dir>/scripts/buildsystems/vcpkg.cmake"
  • 不支援自動安裝依賴包,還需要使用者手動執行 vcpkg install xxx 命令安裝
  • vcpkg 的語義版本選擇不支援 (據說新版本開始支援了)

CMake 和 Conan

```cmake
cmake_minimum_required(VERSION 2.8.12)
project(Hello)

add_definitions("-std=c++11")

include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()

add_executable(hello hello.cpp)
target_link_libraries(hello gtest)

conanfile.txt

[requires]
gtest/1.10.0

[generators]
cmake

缺點:

  • 同樣,還是需要額外呼叫 conan install .. 來安裝包
  • 還需要額外配置一個 conanfile.txt 檔案去描述包依賴規則

Meson 和 Vcpkg

我沒找到如何在 Meson 中去使用 vcpkg 包,僅僅找到一篇相關的 Issue #3500 討論。

Meson 和 Conan

Meson 似乎還沒有對 Conan 進行支援,但是 Conan 官方文件上有解決方案,對齊進行支援,但是很複雜,我是沒看會,大家可以自行研究:https://docs.conan.io/en/latest/reference/build_helpers/meson.html

XMake 和 Vcpkg

前面講了這麼多,其他構建工具和包管理的整合,個人感覺用起來很麻煩,而且不同的包管理器,整合方式差別很大,使用者想要快速從 Vcpkg 切換到 Conan 包,改動量非常大。

接下來,我們來看看 XMake 中整合使用 Vcpkg 提供的包:

add_requires("vcpkg::zlib", {alias = "zlib"})
target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_packages("zlib")

我們只需要通過 add_requires 配置上對應的包名,以及 vcpkg:: 包名稱空間,就能直接整合使用 vcpkg 提供的 zlib 包。

然後,我們只需要執行 xmake 命令,既可完成整個編譯過程,包括 zlib 包的自動安裝,無需額外手動執行 vcpkg install zlib

$ xmake
note: try installing these packages (pass -y to skip confirm)?
-> vcpkg::zlib
please input: y (y/n)

=> install vcpkg::zlib .. ok
[ 25%]: compiling.release src\main.cpp
[ 50%]: linking.release test
[100%]: build ok!

XMake 和 Conan

接下來是整合 Conan 的包,完全一樣的方式,僅僅執行換個包管理器名字。

add_requires("conan::zlib", {alias = "zlib"})
target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_packages("zlib")

XMake 同樣會自動安裝 conan 中的 zlib 包,然後自動整合編譯。

XMake 自建包管理

XMake 跟 CMake 還有其他構建系統,最大的不同點,也就是最大的優勢之一,就是它有完全自建的包管理系統,我們完全可以不依賴 vcpkg/conan,也可以快速整合依賴包,比如:

add_requires("zlib 1.2.x", "tbox >= 1.6.0")
target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_packages("zlib", "tbox")

而且,它還支援完整的語義版本選擇,多平臺的包整合,交叉編譯工具鏈的包整合,甚至編譯工具鏈包的自動拉取使用。

不僅如此,我們開可以對定製化配置對自建包的依賴,例如:

使用調式版本依賴包

我們可以使用 debug 版本庫,實現對依賴庫的斷點除錯。

add_requires("zlib", {debug = true})
設定 msvc 執行時庫
add_requires("zlib", {configs = {vs_runtime = "MD"}})
使用動態庫

預設整合的是靜態庫,我們也可以切換到動態庫。

add_requires("zlib", {configs = {shared = true}})
語義版本支援

XMake 的自建包整合支援完整的版本語義規範。

add_requires("zlib 1.2.x")
add_requires("zlib >=1.2.10")
add_requires("zlib ~1.2.0")
禁止使用系統庫

預設情況下,如果版本匹配,XMake 會優先查詢使用系統上使用者已經安裝的庫,當然我們也可以強制禁止查詢使用系統庫,僅僅從自建包倉庫中下載安裝包。

add_requires("zlib", {system = true})
可選依賴包

如果依賴包整合失敗,XMake 會自動報錯,中斷編譯,提示使用者:zlib not found,但是我們也可以設定為可選包整合,這樣的話,即使庫最終沒安裝成功,也不影響專案的編譯,僅僅只是跳過這個依賴。

add_requires("zlib", {optional = true})
包的定製化配置

比如,整合使用開啟了 context/coroutine 模組配置的 boost 庫。

add_requires("boost", {configs = {context = true, coroutine = true}})

支援的包管理倉庫

XMake 除了支援 vcpkg/conan 還有自建倉庫的包整合支援,還支援其他的包管理倉庫,例如:Conda/Homebrew/Apt/Pacman/Clib/Dub 等等,而且整合方式完全一致。

使用者可與快速切換使用其他的倉庫包,而不需要花太多時間去研究如何整合它們。

因此,XMake 並沒有破壞 C/C++ 生態,而是極大的複用現有 C/C++ 生態的基礎上,努力改進使用者對 C/C++ 依賴包的使用體驗,提高開發效率,讓使用者能夠擁有更多的時間去關注專案本身。

  • 官方自建倉庫 xmake-repo (tbox >1.6.1)
  • 官方包管理器 Xrepo
  • 使用者自建倉庫
  • Conan (conan::openssl/1.1.1g)
  • Conda (conda::libpng 1.3.67)
  • Vcpkg (vcpkg:ffmpeg)
  • Homebrew/Linuxbrew (brew::pcre2/libpcre2-8)
  • Pacman on archlinux/msys2 (pacman::libcurl)
  • Apt on ubuntu/debian (apt::zlib1g-dev)
  • Clib (clib::clibs/bytes@0.0.4)
  • Dub (dub::log 0.4.3)

獨立的包管理命令(Xrepo)

為了方便 XMake 的自建倉庫中的包管理,以及第三方包的管理使用,我們也提供了獨立的 Xrepo cli 命令工具,來方便的管理我們的依賴包

我們可以使用這個工具,快速方便的完成下面的管理操作:

  • 安裝包:xrepo install zlib
  • 解除安裝包:xrepo remove zlib
  • 獲取包資訊:xrepo info zlib
  • 獲取包編譯連結 flags:xrepo fetch zlib
  • 載入包虛擬 Shell 環境:xrepo env shell (這是一個很強大的特性)

我們可以到 Xrepo 專案主頁 檢視更多的介紹和使用方式。

輕量無依賴

使用 Meson/Scons 需要先安裝 python/pip,使用 Bazel 需要先安裝 java 等執行時環境,而 XMake 不需要額外安裝任何依賴庫和環境,自身安裝包僅僅2-3M,非常的輕量。

儘管 XMake 是基於 lua,但是藉助於 lua 膠水語言的輕量級特性,xmake 已將其完全內建,因此安裝完 XMake 等同於擁有了一個完整的 lua vm。

有人會說,編譯工具鏈總還是需要的吧,也不完全是,Windows 上,我們提供了預編譯安裝包,可以直接下載安裝編譯,地址見:Releases

另外,XMake 還支援遠端拉取編譯工具鏈,因此即使你的系統環境,還沒有安裝任何編譯器,也沒關係,使用者完全不用考慮如何折騰編譯環境,只需要在 xmake.lua 裡面配置上需要的工具鏈即可。

比如,我們在 Windows 上使用 mingw-w64 工具鏈來編譯 C/C++ 工程,只需要做如下配置即可。

add_requires("mingw-w64")
target("test")
    set_kind("binary")
    add_files("src/*.c")
    set_toolchains("mingw@mingw-w64")

通過 set_toolchains 配置繫結 mingw-w64 工具鏈包後,XMake 就會自動檢測當前系統是否存在 mingw-64,如果還沒安裝,它會自動下載安裝,然後完成專案編譯,整個過程,使用者僅僅只需要執行 xmake 這個命令就能完成。

$ xmake
note: try installing these packages (pass -y to skip confirm)?
in xmake-repo:
-> mingw-w64 8.1.0 [vs_runtime:MT]
please input: y (y/n)

=> download https://jaist.dl.sourceforge.net/project/mingw-w64/Toolchains%20targetting%20Win64/Personal%20Builds/mingw-builds/8.1.0/threads-posix/seh/x86_64-8.1.0-release-posix-seh-rt_v6-rev0.7z .. ok
checking for mingw directory ... C:\Users\ruki\AppData\Local\.xmake\packages\m\mingw-w64\8.1.0\aad6257977e0449595004d7441358fc5
[ 25%]: compiling.release src\main.cpp
[ 50%]: linking.release test.exe
[100%]: build ok!

除了 mingw-w64,我們還可以配置遠端拉取使用其他的工具鏈,甚至交叉編譯工具鏈,例如:llvm-mingw, llvm, tinycc, muslcc, gnu-rm, zig 等等。

如果大家還想進一步瞭解遠端工具鏈的拉取整合,可以看下文件:自動拉取遠端工具鏈

極速並行編譯

大家都知道 Ninja 構建非常快,因此很多人都喜歡用 CMake/Meson 生成 build.ninja 後,使用 Ninja 來滿足極速構建的需求。

儘管 Ninja 很快,但是我們還是需要先通過 meson.build 和 CMakelist.txt 檔案生成 build.ninja 才行,這個生成過程也會佔用幾秒甚至十幾秒的時間。

而 XMake 不僅僅擁有和 Ninja 近乎相同的構建速度,而且不需要額外再生成其他構建檔案,直接內建構建系統,任何情況下,只需要一個 xmake 命令就可以實現極速編譯。

我們也做過一些對比測試資料,供大家參考:

多工並行編譯測試

構建系統 Termux (8core/-j12) 構建系統 MacOS (8core/-j12)
xmake 24.890s xmake 12.264s
ninja 25.682s ninja 11.327s
cmake(gen+make) 5.416s+28.473s cmake(gen+make) 1.203s+14.030s
cmake(gen+ninja) 4.458s+24.842s cmake(gen+ninja) 0.988s+11.644s

單任務編譯測試

構建系統 Termux (-j1) 構建系統 MacOS (-j1)
xmake 1m57.707s xmake 39.937s
ninja 1m52.845s ninja 38.995s
cmake(gen+make) 5.416s+2m10.539s cmake(gen+make) 1.203s+41.737s
cmake(gen+ninja) 4.458s+1m54.868s cmake(gen+ninja) 0.988s+38.022s

傻瓜式多平臺編譯

XMake 的另外一個特點,就是高效簡單的多平臺編譯,不管你是編譯 windows/linux/macOS 下的程式,還是編譯 iphoneos/android 又或者是交叉編譯。

編譯的配置方式大同小異,不必讓使用者去這折騰研究各個平臺下如何去編譯。

編譯本機 Windows/Linux/MacOS 程式

當前本機程式編譯,我們僅僅只需要執行:

$ xmake

對比 CMake

$ mkdir build
$ cd build
$ cmake --build ..

編譯 Android 程式

$ xmake f -p android --ndk=~/android-ndk-r21e
$ xmake

對比 CMake

$ mkdir build
$ cd build
$ cmake -DCMAKE_TOOLCHAIN_FILE=~/android-ndk-r21e/build/cmake/android.toolchain.cmake ..
$ make

編譯 iOS 程式

$ xmake f -p iphoneos
$ xmake

對比 CMake

$ mkdir build
$ cd build
$ wget https://raw.githubusercontent.com/leetal/ios-cmake/master/ios.toolchain.cmake
$ cmake -DCMAKE_TOOLCHAIN_FILE=`pwd`/ios.toolchain.cmake ..
$ make

我沒有找到很方便的方式去配置編譯 ios 程式,僅僅只能從其他地方找到一個第三方的 ios 工具鏈去配置編譯。

交叉編譯

我們通常只需要設定交叉編譯工具鏈根目錄,XMake 會自動檢測工具鏈結構,提取裡面的編譯器參與編譯,不需要額外配置什麼。

$ xmake f -p cross --sdk=~/aarch64-linux-musl-cross
$ xmake

對比 CMake

我們需要先額外寫一個 cross-toolchain.cmake 的交叉工具鏈配置檔案。

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)

set(TOOL_CHAIN_DIR ~/aarch64-linux-musl)
set(TOOL_CHAIN_INCLUDE ${TOOL_CHAIN_DIR}/aarch64-linux-musl/include)
set(TOOL_CHAIN_LIB ${TOOL_CHAIN_DIR}/aarch64-linux-musl/lib)

set(CMAKE_C_COMPILER "aarch64-linux-gcc")
set(CMAKE_CXX_COMPILER "aarch64-linux-g++")

set(CMAKE_FIND_ROOT_PATH ${TOOL_CHAIN_DIR}/aarch64-linux-musl)

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

include_directories(${TOOL_CHAIN_DIR}/aarch64-linux-musl/include)
set(CMAKE_INCLUDE_PATH ${TOOL_CHAIN_INCLUDE})
set(CMAKE_LIBRARY_PATH ${TOOL_CHAIN_LIB})
$ mkdir build
$ cd build
$ cmake -DCMAKE_TOOLCHAIN_FILE=../cross-toolchain.cmake ..
$ make

結語

如果你是 C/C++ 開發的新手,可以通過 XMake 快速上手入門 C/C++ 編譯構建。

如果你想開發維護跨平臺 C/C++ 專案,也可以考慮使用 XMake 來維護構建,提高開發效率,讓你更加專注於專案本身,不再為折騰移植依賴庫而煩惱。

歡迎關注 XMake 專案:

相關文章