xmake 是一個基於 Lua 的輕量級跨平臺構建工具,使用 xmake.lua 維護專案構建,相比 makefile/CMakeLists.txt,配置語法更加簡潔直觀,對新手非常友好,短時間內就能快速入門,能夠讓使用者把更多的精力集中在實際的專案開發上。
在 2.5.2 版本中,我們增加了一個重量級的新特性:自動拉取遠端交叉編譯工具鏈
。
這是用來幹什麼的呢,做過交叉編譯以及有 C/C++ 專案移植經驗的同學應該知道,折騰各種交叉編譯工具鏈,移植編譯專案是非常麻煩的一件事,需要自己下載對應工具鏈,並且配置工具鏈和編譯環境很容易出錯導致編譯失敗。
現在,xmake 已經可以支援自動下載專案所需的工具鏈,然後使用對應工具鏈直接編譯專案,使用者不需要關心如何配置工具鏈,任何情況下只需要執行 xmake
命令即可完成編譯。
甚至對於 C/C++ 依賴包的整合,也可以自動切換到對應工具鏈編譯安裝整合,一切完全自動化,完全不需要使用者操心。
除了交叉編譯工具鏈,我們也可以自動拉取工具鏈,比如特定版本的 llvm,llvm-mingw, zig 等各種工具鏈來參與編譯 C/C++/Zig 專案的編譯。
即使是 cmake 也還不支援工具鏈的自動拉取,頂多只能配合 vcpkg/conan 等第三方包管理對 C/C++ 依賴包進行整合,另外,即使對於 C/C++依賴包,xmake 也有自己原生內建的包管理工具,完全無任何依賴。
新特性介紹
自動拉取遠端交叉編譯工具鏈
從 2.5.2 版本開始,我們可以拉取指定的工具鏈來整合編譯專案,我們也支援將依賴包切換到對應的遠端工具鏈參與編譯後整合進來。
相關例子程式碼見:Toolchain/Packages Examples
相關的 issue #1217
當前,我們已經在 xmake-repo 倉庫收錄了以下工具鏈包,可以讓 xmake 遠端拉取整合:
- llvm
- llvm-mingw
- gnu-rm
- muslcc
- zig
雖然現在支援的工具鏈包不多,當但是整體架構已經打通,後期我們只需要收錄更多的工具鏈進來就行,比如:gcc, tinyc, vs-buildtools 等工具鏈。
由於 xmake 的包支援語義版本,因此如果專案依賴特定版本的 gcc/clang 編譯器,就不要使用者去折騰安裝了,xmake 會自動檢測當前系統的 gcc/clang 版本是否滿足需求。
如果版本不滿足,那麼 xmake 就會走遠端拉取,自動幫使用者安裝整合特定版本的 gcc/clang,然後再去編譯專案。
拉取指定版本的 llvm 工具鏈
我們使用 llvm-10 中的 clang 來編譯專案。
add_requires("llvm 10.x", {alias = "llvm-10"})
target("test")
set_kind("binary")
add_files("src/*.c)
set_toolchains("llvm@llvm-10")
其中,llvm@llvm-10
的前半部分為工具鏈名,也就是 toolchain("llvm")
,後面的名字是需要被關聯工具鏈包名,也就是 package("llvm")
,不過如果設定了別名,那麼會優先使用別名:llvm-10
另外,我們後續還會增加 gcc 工具鏈包到 xmake-repo,使得使用者可以自由切換 gcc-10, gcc-11 等特定版本的 gcc 編譯器,而無需使用者去手動安裝。
拉取交叉編譯工具鏈
我們也可以拉取指定的交叉編譯工具鏈來編譯專案。
add_requires("muslcc")
target("test")
set_kind("binary")
add_files("src/*.c)
set_toolchains("@muslcc")
muslcc 是 https://musl.cc 提供的一款交叉編譯工具鏈,預設 xmake 會自動整合編譯 x86_64-linux-musl-
目標平臺。
當然,我們也可以通過 xmake f -a arm64
切換到 aarch64-linux-musl-
目標平臺來進行交叉編譯。
拉取工具鏈並且整合對應工具鏈編譯的依賴包
我們也可以使用指定的muslcc交叉編譯工具鏈去編譯和整合所有的依賴包。
add_requires("muslcc")
add_requires("zlib", "libogg", {system = false})
set_toolchains("@muslcc")
target("test")
set_kind("binary")
add_files("src/*.c")
add_packages("zlib", "libogg")
這個時候,工程裡面配置的 zlib, libogg 等依賴包,也會切換使用 muslcc 工具鏈,自動下載編譯然後整合進來。
我們也可以通過 set_plat/set_arch
固定平臺,這樣只需要一個 xmake 命令,就可以完成整個交叉編譯環境的整合以及架構切換。
add_requires("muslcc")
add_requires("zlib", "libogg", {system = false})
set_plat("cross")
set_arch("arm64")
set_toolchains("@muslcc")
target("test")
set_kind("binary")
add_files("src/*.c")
add_packages("zlib", "libogg")
完整例子見:Examples (muslcc)
拉取整合 Zig 工具鏈
xmake 會先下載特定版本的 zig 工具鏈,然後使用此工具鏈編譯 zig 專案,當然使用者如果已經自行安裝了 zig 工具鏈,xmake 也會自動檢測對應版本是否滿足,如果滿足需求,那麼會直接使用它,無需重複下載安裝。
add_rules("mode.debug", "mode.release")
add_requires("zig 0.7.x")
target("test")
set_kind("binary")
add_files("src/*.zig")
set_toolchains("@zig")
增加對 zig cc 編譯器支援
zig cc
是 zig 內建的 c/c++ 編譯器,可以完全獨立進行 c/c++ 程式碼的編譯和連結,完全不依賴 gcc/clang/msvc,非常給力。
因此,我們完全可以使用它來編譯 c/c++ 專案,關鍵是 zig 的工具鏈還非常輕量,僅僅幾十M 。
我們只需要切換到 zig 工具鏈即可完成編譯:
$ xmake f --toolchain=zig
$ xmake
[ 25%]: compiling.release src/main.c
"zig cc" -c -arch x86_64 -fvisibility=hidden -O3 -DNDEBUG -o build/.objs/xmake_test/macosx/x86_64/release/src/main.c.o src/main.c
[ 50%]: linking.release test
"zig c++" -o build/macosx/x86_64/release/test build/.objs/xmake_test/macosx/x86_64/release/src/main.c.o -arch x86_64 -stdlib=libc++ -Wl,-x -lz
[100%]: build ok!
另外,zig cc
的另外一個強大之處在於,它還支援不同架構的交叉編譯,太 happy 了。
通過 xmake,我們也只需再額外切換下架構到 arm64,即可實現對 arm64 的交叉編譯,例如:
$ xmake f -a arm64 --toolchain=zig
$ xmake
[ 25%]: compiling.release src/main.c
"zig cc" -c -target aarch64-macos-gnu -arch arm64 -fvisibility=hidden -O3 -DNDEBUG -o build/.objs/xmake_test/macosx/arm64/release/src/main.c.o src/main.c
checking for flags (-MMD -MF) ... ok
checking for flags (-fdiagnostics-color=always) ... ok
[ 50%]: linking.release xmake_test
"zig c++" -o build/macosx/arm64/release/xmake_test build/.objs/xmake_test/macosx/arm64/release/src/main.c.o -target aarch64-macos-gnu -arch arm64 -stdlib=libc++ -Wl,-x -lz
[100%]: build ok!
即使你是在在 macOS,也可以用 zig cc
去交叉編譯 windows/x64 目標程式,相當於替代了 mingw 乾的事情。
$ xmake f -p windows -a x64 --toolchain=zig
$ xmake
自動匯出所有 windows/dll 中的符號
cmake 中有這樣一個功能:WINDOWS_EXPORT_ALL_SYMBOLS
,安裝 cmake 文件中的說法:
https://cmake.org/cmake/help/latest/prop_tgt/WINDOWS_EXPORT_ALL_SYMBOLS.html
Enable this boolean property to automatically create a module definition (.def) file with all global symbols found
in the input .obj files for a SHARED library (or executable with ENABLE_EXPORTS) on Windows.
The module definition file will be passed to the linker causing all symbols to be exported from the .dll. For global data symbols,
__declspec(dllimport) must still be used when compiling against the code in the .dll. All other function symbols will be automatically exported and imported by callers.
This simplifies porting projects to Windows by reducing the need for explicit dllexport markup, even in C++ classes.
大體意思就是:
啟用此布林屬性,可以自動建立一個模組定義(.def)檔案,其中包含在Windows上的共享庫(或使用ENABLE_EXPORTS的可執行檔案)的輸入.obj檔案中找到的所有全域性符號。
模組定義檔案將被傳遞給連結器,使所有符號從.dll中匯出。對於全域性資料符號,當對.dll中的程式碼進行編譯時,仍然必須使用__declspec(dllimport)。
所有其它的函式符號將被呼叫者自動匯出和匯入。這就簡化了將專案移植到 Windows 的過程,減少了對顯式 dllexport 標記的需求,甚至在 C++ 類中也是如此。
現在,xmake 中也提供了類似的特性,可以快速全量匯出 windows/dll 中的符號,來簡化對第三方專案移植過程中,對符號匯出的處理。另外,如果專案中的符號太多,也可以用此來簡化程式碼中的顯式匯出需求。
我們只需在對應生成的 dll 目標上,配置 utils.symbols.export_all
規則即可。
target("foo")
set_kind("shared")
add_files("src/foo.c")
add_rules("utils.symbols.export_all")
target("test")
set_kind("binary")
add_deps("foo")
add_files("src/main.c")
xmake 會自動掃描所有的 obj 物件檔案,然後生成 def 符號匯出檔案,傳入 link.exe 實現快速全量匯出。
轉換 mingw/.dll.a 到 msvc/.lib
這個特性也是參考自 CMAKE_GNUtoMS 功能,可以把MinGW生成的動態庫(xxx.dll & xxx.dll.a)轉換成Visual Studio可以識別的格式(xxx.dll & xxx.lib),從而實現混合編譯。
這個功能對Fortran & C++混合專案特別有幫助,因為VS不提供fortran編譯器,只能用MinGW的gfortran來編譯fortran部分,然後和VS的專案連結。
往往這樣的專案同時有一些其他的庫以vs格式提供,因此純用MinGW編譯也不行,只能使用cmake的這個功能來混合編譯。
因此,xmake 也提供了一個輔助模組介面去支援它,使用方式如下:
import("utils.platform.gnu2mslib")
gnu2mslib("xxx.lib", "xxx.dll.a")
gnu2mslib("xxx.lib", "xxx.def")
gnu2mslib("xxx.lib", "xxx.dll.a", {dllname = "xxx.dll", arch = "x64"})
支援從 def 生成 xxx.lib ,也支援從 xxx.dll.a 自動匯出 .def ,然後再生成 xxx.lib
具體細節見:issue #1181
實現批處理命令來簡化自定義規則
為了簡化使用者自定義 rule 的配置,xmake 新提供了 on_buildcmd_file
, on_buildcmd_files
等自定義指令碼入口,
我們可以通過 batchcmds 物件,構造一個批處理命令列任務,xmake 在實際執行構建的時候,一次性執行這些命令。
這對於 xmake project
此類工程生成器外掛非常有用,因為生成器生成的第三方工程檔案並不支援 on_build_files
此類內建指令碼的執行支援。
但是 on_buildcmd_file
構造的最終結果,就是一批原始的 cmd 命令列,可以直接給其他工程檔案作為 custom commands 來執行。
另外,相比 on_build_file
,它也簡化對擴充套件檔案的編譯實現,更加的可讀易配置,對使用者也更加友好。
rule("foo")
set_extensions(".xxx")
on_buildcmd_file(function (target, batchcmds, sourcefile, opt)
batchcmds:vrunv("gcc", {"-o", objectfile, "-c", sourcefile})
end)
除了 batchcmds:vrunv
,我們還支援一些其他的批處理命令,例如:
batchcmds:show("hello %s", "xmake")
batchcmds:vrunv("gcc", {"-o", objectfile, "-c", sourcefile}, {envs = {LD_LIBRARY_PATH="/xxx"}})
batchcmds:mkdir("/xxx") -- and cp, mv, rm, ln ..
batchcmds:compile(sourcefile_cx, objectfile, {configs = {includedirs = sourcefile_dir, languages = (sourcekind == "cxx" and "c++11")}})
batchcmds:link(objectfiles, targetfile, {configs = {linkdirs = ""}})
同時,我們在裡面也簡化對依賴執行的配置,下面是一個完整例子:
rule("lex")
set_extensions(".l", ".ll")
on_buildcmd_file(function (target, batchcmds, sourcefile_lex, opt)
-- imports
import("lib.detect.find_tool")
-- get lex
local lex = assert(find_tool("flex") or find_tool("lex"), "lex not found!")
-- get c/c++ source file for lex
local extension = path.extension(sourcefile_lex)
local sourcefile_cx = path.join(target:autogendir(), "rules", "lex_yacc", path.basename(sourcefile_lex) .. (extension == ".ll" and ".cpp" or ".c"))
-- add objectfile
local objectfile = target:objectfile(sourcefile_cx)
table.insert(target:objectfiles(), objectfile)
-- add commands
batchcmds:show_progress(opt.progress, "${color.build.object}compiling.lex %s", sourcefile_lex)
batchcmds:mkdir(path.directory(sourcefile_cx))
batchcmds:vrunv(lex.program, {"-o", sourcefile_cx, sourcefile_lex})
batchcmds:compile(sourcefile_cx, objectfile)
-- add deps
batchcmds:add_depfiles(sourcefile_lex)
batchcmds:set_depmtime(os.mtime(objectfile))
batchcmds:set_depcache(target:dependfile(objectfile))
end)
我們從上面的配置可以看到,整體執行命令列表非常清晰,而如果我們用 on_build_file
來實現,可以對比下之前這個規則的配置,就能直觀感受到新介面的配置方式確實簡化了不少:
rule("lex")
-- set extension
set_extensions(".l", ".ll")
-- load lex/flex
before_load(function (target)
import("core.project.config")
import("lib.detect.find_tool")
local lex = config.get("__lex")
if not lex then
lex = find_tool("flex") or find_tool("lex")
if lex and lex.program then
config.set("__lex", lex.program)
cprint("checking for Lex ... ${color.success}%s", lex.program)
else
cprint("checking for Lex ... ${color.nothing}${text.nothing}")
raise("lex/flex not found!")
end
end
end)
-- build lex file
on_build_file(function (target, sourcefile_lex, opt)
-- imports
import("core.base.option")
import("core.theme.theme")
import("core.project.config")
import("core.project.depend")
import("core.tool.compiler")
import("private.utils.progress")
-- get lex
local lex = assert(config.get("__lex"), "lex not found!")
-- get extension: .l/.ll
local extension = path.extension(sourcefile_lex)
-- get c/c++ source file for lex
local sourcefile_cx = path.join(target:autogendir(), "rules", "lex_yacc", path.basename(sourcefile_lex) .. (extension == ".ll" and ".cpp" or ".c"))
local sourcefile_dir = path.directory(sourcefile_cx)
-- get object file
local objectfile = target:objectfile(sourcefile_cx)
-- load compiler
local compinst = compiler.load((extension == ".ll" and "cxx" or "cc"), {target = target})
-- get compile flags
local compflags = compinst:compflags({target = target, sourcefile = sourcefile_cx})
-- add objectfile
table.insert(target:objectfiles(), objectfile)
-- load dependent info
local dependfile = target:dependfile(objectfile)
local dependinfo = option.get("rebuild") and {} or (depend.load(dependfile) or {})
-- need build this object?
local depvalues = {compinst:program(), compflags}
if not depend.is_changed(dependinfo, {lastmtime = os.mtime(objectfile), values = depvalues}) then
return
end
-- trace progress info
progress.show(opt.progress, "${color.build.object}compiling.lex %s", sourcefile_lex)
-- ensure the source file directory
if not os.isdir(sourcefile_dir) then
os.mkdir(sourcefile_dir)
end
-- compile lex
os.vrunv(lex, {"-o", sourcefile_cx, sourcefile_lex})
-- trace
if option.get("verbose") then
print(compinst:compcmd(sourcefile_cx, objectfile, {compflags = compflags}))
end
-- compile c/c++ source file for lex
dependinfo.files = {}
assert(compinst:compile(sourcefile_cx, objectfile, {dependinfo = dependinfo, compflags = compflags}))
-- update files and values to the dependent file
dependinfo.values = depvalues
table.insert(dependinfo.files, sourcefile_lex)
depend.save(dependinfo, dependfile)
end)
關於這個的詳細說明和背景,見:issue 1246
依賴包配置改進
使用 add_extsources 改進包名查詢
關於遠端依賴包定義這塊,我們也新增了 add_extsources
和 on_fetch
兩個配置介面,可以更好的配置 xmake 在安裝 C/C++ 包的過程中,對系統庫的查詢過程。
至於具體背景,我們可以舉個例子,比如我們在 xmake-repo 倉庫新增了一個 package("libusb")
的包。
那麼使用者就可以通過下面的方式,直接整合使用它:
add_requires("libusb")
target("test")
set_kind("binary")
add_files("src/*.c")
add_packages("libusb")
如果使用者系統上確實沒有安裝 libusb,那麼 xmake 會自動下載 libusb 庫原始碼,自動編譯安裝整合,沒啥問題。
但如果使用者通過 apt install libusb-1.0
安裝了 libusb 庫到系統,那麼按理 xmake 應該會自動優先查詢使用者安裝到系統環境的 libusb 包,直接使用,避免額外的下載編譯安裝。
但是問題來了,xmake 內部通過 find_package("libusb")
並沒有找打它,這是為什麼呢?因為通過 apt 安裝的 libusb 包名是 libusb-1.0
, 而不是 libusb。
我們只能通過 pkg-config --cflags libusb-1.0
才能找到它,但是 xmake 內部的預設 find_package 邏輯並不知道 libusb-1.0
的存在,所以找不到。
因此為了更好地適配不同系統環境下,系統庫的查詢,我們可以通過 add_extsources("pkgconfig::libusb-1.0")
去讓 xmake 改進查詢邏輯,例如:
package("libusb")
add_extsources("pkgconfig::libusb-1.0")
on_install(function (package)
-- ...
end)
另外,我們也可以通過這個方式,改進查詢 homebrew/pacman 等其他包管理器安裝的包,例如:add_extsources("pacman::libusb-1.0")
。
使用 on_fetch 完全定製系統庫查詢
如果不同系統下安裝的系統庫,僅僅只是包名不同,那麼使用 add_extsources
改進系統庫查詢已經足夠,簡單方便。
但是如果有些安裝到系統的包,位置更加複雜,想要找到它們,也許需要一些額外的指令碼才能實現,例如:windows 下注冊表的訪問去查詢包等等,這個時候,我們就可以通過 on_fetch
完全定製化查詢系統庫邏輯。
還是以 libusb 為例,我們不用 add_extsources
,可以使用下面的方式,實現相同的效果,當然,我們可以在裡面做更多的事情。
package("libusb")
on_fetch("linux", function(package, opt)
if opt.system then
return find_package("pkgconfig::libusb-1.0")
end
end)
manifest 檔案支援
在新版本中,我們還新增了對 windows .manifest
檔案的支援,只需要通過 add_files
新增進來即可。
add_rules("mode.debug", "mode.release")
target("test")
set_kind("binary")
add_files("src/*.cpp")
add_files("src/*.manifest")
xrepo 命令改進
關於 xrepo 命令,我們也稍微改進了下,現在可以通過下面的命令,批量解除安裝刪除已經安裝的包,支援模式匹配:
$ xrepo remove --all
$ xrepo remove --all zlib pcr*
包的依賴匯出支援
我們也改進了 add_packages
,使其也支援 {public = true}
來匯出包配置給父 target。
add_rules("mode.debug", "mode.release")
add_requires("pcre2")
target("test")
set_kind("shared")
add_packages("pcre2", {public = true})
add_files("src/test.cpp")
target("demo")
add_deps("test")
set_kind("binary")
add_files("src/main.cpp") -- 我們可以在這裡使用被 test 目標匯出 pcre2 庫
至於具體匯出哪些配置呢?
-- 預設私有,但是 links/linkdirs 還是會自動匯出
add_packages("pcre2")
-- 全部匯出。包括 includedirs, defines
add_packages("pcre2", {public = true})
更新內容
新特性
- #955: 支援
zig cc
和zig c++
作為 c/c++ 編譯器 - #955: 支援使用 zig 進行交叉編譯
- #1177: 改進終端和 color codes 探測
- #1216: 傳遞自定義 includes 指令碼給 xrepo
- 新增 linuxos 內建模組獲取 linux 系統資訊
- #1217: 支援當編譯專案時自動拉取工具鏈
- #1123: 新增
rule("utils.symbols.export_all")
自動匯出所有 windows/dll 中的符號 - #1181: 新增
utils.platform.gnu2mslib(mslib, gnulib)
模組介面去轉換 mingw/xxx.dll.a 到 msvc xxx.lib - #1246: 改進規則支援新的批處理命令去簡化自定義規則實現
- #1239: 新增
add_extsources
去改進外部包的查詢 - #1241: 支援為 windows 程式新增 .manifest 檔案參與連結
- 支援使用
xrepo remove --all
命令去移除所有的包,並且支援模式匹配 - #1254: 支援匯出包配置給父 target,實現包配置的依賴繼承
改進
- #1226: 新增缺失的 Qt 標頭檔案搜尋路徑
- #1183: 改進 C++ 語言標準,以便支援 Qt6
- #1237: 為 vsxmake 外掛新增 qt.ui 檔案
- 改進 vs/vsxmake 外掛去支援預編譯標頭檔案和智慧提示
- #1090: 簡化自定義規則
- #1065: 改進 protobuf 規則,支援 compile_commands 生成器
- #1249: 改進 vs/vsxmake 生成器去支援啟動工程設定
- #605: 改進 add_deps 和 add_packages 直接的匯出 links 順序
- 移除廢棄的
add_defines_h_if_ok
andadd_defines_h
介面