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 專案:
- Github 專案地址
- 專案主頁
- XMake 包管理倉庫
- 社群
- Telegram 群組
- Discord 聊天室
- QQ 群:343118190, 662147501
- 微信公眾號:tboox-os
- 課程:Xmake 帶你輕鬆構建 C/C++ 專案
- 活動:開源之夏 & Xmake