C/C++ 構建系統,我用 xmake
- 2021 年 5 月 6 日
- 筆記
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 官方文檔上有解決方案,對齊進行支持,但是很複雜,我是沒看會,大家可以自行研究://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/[email protected])
- 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 //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 //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 項目:
- Github 項目地址
- 項目主頁
- XMake 包管理倉庫
- 社區
- Telegram 群組
- Discord 聊天室
- QQ 群:343118190, 662147501
- 微信公眾號:tboox-os
- 課程:Xmake 帶你輕鬆構建 C/C++ 項目
- 活動:開源之夏 & Xmake