Xmake v2.7.3 發佈,包組件和 C++ 模塊增量構建支持

Xmake 是一個基於 Lua 的輕量級跨平臺構建工具。

它非常的輕量,沒有任何依賴,因爲它內置了 Lua 運行時。

它使用 xmake.lua 維護項目構建,相比 makefile/CMakeLists.txt,配置語法更加簡潔直觀,對新手非常友好,短時間內就能快速入門,能夠讓用戶把更多的精力集中在實際的項目開發上。

我們能夠使用它像 Make/Ninja 那樣可以直接編譯項目,也可以像 CMake/Meson 那樣生成工程文件,另外它還有內置的包管理系統來幫助用戶解決 C/C++ 依賴庫的集成使用問題。

目前,Xmake 主要用於 C/C++ 項目的構建,但是同時也支持其他 native 語言的構建,可以實現跟 C/C++ 進行混合編譯,同時編譯速度也是非常的快,可以跟 Ninja 持平。

Xmake = Build backend + Project Generator + Package Manager + [Remote|Distributed] Build + Cache

儘管不是很準確,但我們還是可以把 Xmake 按下面的方式來理解:

Xmake ~= Make/Ninja + CMake/Meson + Vcpkg/Conan + distcc + ccache/sccache

新特性介紹

包組件支持

背景簡介

這個新特性主要用於實現從一個 C/C++ 包中集成特定的子庫,一般用於一些比較大的包中的庫組件集成。

因爲這種包裏面提供了很多的子庫,但不是每個子庫用戶都需要,全部鏈接反而有可能會出問題。

儘管,之前的版本也能夠支持子庫選擇的特性,例如:

add_requires("sfml~foo", {configs = {graphics = true, window = true}})
add_requires("sfml~bar", {configs = {network = true}})

target("foo")
    set_kind("binary")
    add_packages("sfml~foo")

target("bar")
    set_kind("binary")
    add_packages("sfml~bar")

這是通過每個包的自定義配置來實現的,但這種方式會存在一些問題:

  1. sfml~foosfml~bar 會作爲兩個獨立的包,重複安裝,佔用雙倍的磁盤空間
  2. 也會重複編譯一些共用代碼,影響安裝效率
  3. 如果一個目標同時依賴了 sfml~foosfml~bar,會存在鏈接衝突

如果是對於 boost 這種超大包的集成,重複編譯和磁盤佔用的影響會非常大,如果在子庫組合非常多的情況下,甚至會導致超過 N 倍的磁盤佔用。

爲了解決這個問題,Xmake 新增了包組件模式,它提供了以下一些好處:

  1. 僅僅一次編譯安裝,任意多個組件快速集成,極大提升安裝效率,減少磁盤佔用
  2. 組件抽象化,跨編譯器和平臺,用戶不需要關心如何配置每個子庫之間鏈接順序依賴
  3. 使用更加方便

更多背景詳情見:#2636

使用包組件

對於用戶,使用包組件是非常方便的,因爲用戶是不需要維護包的,只要使用的包,它配置了相關的組件集,我們就可以快速集成和使用它,例如:

add_requires("sfml")

target("foo")
    set_kind("binary")
    add_packages("sfml", {components = "graphics"})

target("bar")
    set_kind("binary")
    add_packages("sfml", {components = "network"})

查看包組件

那麼,如何知道指定的包提供了哪些組件呢?我們可以通過執行下面的命令查看:

$ xrepo info sfml
The package info of project:
    require(sfml):
      -> description: Simple and Fast Multimedia Library
      -> version: 2.5.1
      ...
      -> components:
         -> system:
         -> graphics: system, window
         -> window: system
         -> audio: system
         -> network: system

包組件配置

如果你是包的維護者,想要將一個包增加組件支持,那麼需要通過下面兩個接口來完成包組件的配置:

  • add_components: 添加包組件列表
  • on_component: 配置每個包組件
包組件的鏈接配置

大多數情況下,包組件只需要配置它自己的一些子鏈接信息,例如:

package("sfml")
    add_components("graphics")
    add_components("audio", "network", "window")
    add_components("system")

    on_component("graphics", function (package, component)
        local e = package:config("shared") and "" or "-s"
        component:add("links", "sfml-graphics" .. e)
        if package:is_plat("windows", "mingw") and not package:config("shared") then
            component:add("links", "freetype")
            component:add("syslinks", "opengl32", "gdi32", "user32", "advapi32")
        end
    end)

    on_component("window", function (package, component)
        local e = package:config("shared") and "" or "-s"
        component:add("links", "sfml-window" .. e)
        if package:is_plat("windows", "mingw") and not package:config("shared") then
            component:add("syslinks", "opengl32", "gdi32", "user32", "advapi32")
        end
    end)

    ...

上面是一個不完整的包配置,我僅僅摘取一部分跟包組件相關的配置。

一個關於包組件的配置和使用的完整例子見:components example

配置組件的編譯信息

我們不僅可以配置每個組件的鏈接信息,還有 includedirs, defines 等等編譯信息,我們也可以對每個組件單獨配置。

package("sfml")
    on_component("graphics", function (package, component)
        package:add("defines", "TEST")
    end)
配置組件依賴
package("sfml")
    add_components("graphics")
    add_components("audio", "network", "window")
    add_components("system")

    on_component("graphics", function (package, component)
          component:add("deps", "window", "system")
    end)

上面的配置,告訴包,我們的 graphics 組件還會額外依賴 windowsystem 兩個組件。

因此,在用戶端,我們對 graphics 的組件使用,可以從

    add_packages("sfml", {components = {"graphics", "window", "system"})

簡化爲:

    add_packages("sfml", {components = "graphics")

因爲,只要我們開啓了 graphics 組件,它也會自動啓用依賴的 window 和 system 組件,並且自動保證鏈接順序正確。

另外,我們也可以通過 add_components("graphics", {deps = {"window", "system"}}) 來配置組件依賴關係。

從系統庫中查找組件

我們知道,在包配置中,配置 add_extsources 可以改進包在系統中的查找,比如從 apt/pacman 等系統包管理器中找庫。

當然,我們也可以讓每個組件也能通過 extsources 配置,去優先從系統庫中找到它們。

例如,sfml 包,它在 homebrew 中其實也是組件化的,我們完全可以讓包從系統庫中,找到對應的每個組件,而不需要每次源碼安裝它們。

$ ls -l /usr/local/opt/sfml/lib/pkgconfig
-r--r--r--  1 ruki  admin  317 10 19 17:52 sfml-all.pc
-r--r--r--  1 ruki  admin  534 10 19 17:52 sfml-audio.pc
-r--r--r--  1 ruki  admin  609 10 19 17:52 sfml-graphics.pc
-r--r--r--  1 ruki  admin  327 10 19 17:52 sfml-network.pc
-r--r--r--  1 ruki  admin  302 10 19 17:52 sfml-system.pc
-r--r--r--  1 ruki  admin  562 10 19 17:52 sfml-window.pc

我們只需要,對每個組件配置它的 extsources:

    if is_plat("macosx") then
        add_extsources("brew::sfml/sfml-all")
    end

    on_component("graphics", function (package, component)
        -- ...
        component:add("extsources", "brew::sfml/sfml-graphics")
    end)
默認的全局組件配置

除了通過指定組件名的方式,配置特定組件,如果我們沒有指定組件名,默認就是全局配置所有組件。

package("sfml")
    on_component(function (package, component)
        -- configure all components
    end)

當然,我們也可以通過下面的方式,指定配置 graphics 組件,剩下的組件通過默認的全局配置接口進行配置:

package("sfml")
    add_components("graphics")
    add_components("audio", "network", "window")
    add_components("system")

    on_component("graphics", function (package, component)
        -- configure graphics
    end)

    on_component(function (package, component)
        -- component audio, network, window, system
    end)

C++ 模塊構建改進

增量構建支持

原本以爲 Xmake 對 C++ 模塊已經支持的比較完善了,後來才發現,它的增量編譯還無法正常工作。

因此,這個版本 Xmake 對 C++ 模塊的增量編譯也做了很好的支持,儘管支持過程還是花了很多精力的。

我分析了下,各家的編譯器對生成帶模塊的 include 依賴信息格式(*.d),差異還是非常大的。

gcc 的格式最複雜,不過我還是將它支持上了。

build/.objs/dependence/linux/x86_64/release/src/foo.mpp.o: src/foo.mpp\
build/.objs/dependence/linux/x86_64/release/src/foo.mpp.o  gcm.cache/foo.gcm: bar.c++m cat.c++m\
foo.c++m: gcm.cache/foo.gcm\
.PHONY: foo.c++m\
gcm.cache/foo.gcm:|  build/.objs/dependence/linux/x86_64/release/src/foo.mpp.o\
CXX_IMPORTS += bar.c++m cat.c++m\

clang 的格式兼容性最好,沒有做任何特殊改動就支持了。

build//hello.pcm:   /usr/lib/llvm-15/lib/clang/15.0.2/include/module.modulemap   src/hello.mpp\

msvc 的格式擴展性比較好,解析和支持起來比較方便:

{
    "Version": "1.2",
    "Data": {
        "Source": "c:\users\ruki\desktop\user_headerunit\src\main.cpp",
        "ProvidedModule": "",
        "Includes": [],
        "ImportedModules": [
            {
                "Name": "hello",
                "BMI": "c:\users\ruki\desktop\user_headerunit\src\hello.ifc"
            }
        ],
        "ImportedHeaderUnits": [
            {
                "Header": "c:\users\ruki\desktop\user_headerunit\src\header.hpp",
                "BMI": "c:\users\ruki\desktop\user_headerunit\src\header.hpp.ifc"
            }
        ]
    }
}

循環依賴檢測支持

由於模塊之間是存在依賴關係的,因此如果有幾個模塊之間存在循環依賴引用,那麼是無法編譯通過的。

但是之前的版本中,Xmake 無法檢測到這種情況,遇到循環依賴,編譯就會卡死,沒有任何提示信息,這對用戶非常不友好。

而新版本中,我們對這種情況做了改進,增加了模塊的循環依賴檢測,編譯時候會出現以下錯誤提示,方便用戶定位問題:

$ xmake
[  0%]: generating.cxx.module.deps Foo.mpp
[  0%]: generating.cxx.module.deps Foo2.mpp
[  0%]: generating.cxx.module.deps Foo3.mpp
[  0%]: generating.cxx.module.deps main.cpp
error: circular modules dependency(Foo2, Foo, Foo3, Foo2) detected!
  -> module(Foo2) in Foo2.mpp
  -> module(Foo) in Foo.mpp
  -> module(Foo3) in Foo3.mpp
  -> module(Foo2) in Foo2.mpp

更加 LSP 友好的語法格式

我們默認約定的域配置語法,儘管非常簡潔,但是對自動格式化縮進和 IDE 不是很友好,如果你格式化配置,縮進就完全錯位了。

target("foo")
    set_kind("binary")
    add_files("src/*.cpp")

另外,如果兩個 target 之間配置了一些全局的配置,那麼它不能自動結束當前 target 作用域,用戶需要顯式調用 target_end()

target("foo")
    set_kind("binary")
    add_files("src/*.cpp")
target_end()

add_defines("ROOT")

target("bar")
    set_kind("binary")
    add_files("src/*.cpp")

雖然,上面我們提到,可以使用 do end 模式來解決自動縮進問題,但是需要 target_end() 的問題還是存在。

target("foo") do
    set_kind("binary")
    add_files("src/*.cpp")
end
target_end()

add_defines("ROOT")

target("bar") do
    set_kind("binary")
    add_files("src/*.cpp")
end

因此,在新版本中,我們提供了一種更好的可選域配置語法,來解決自動縮進,target 域隔離問題,例如:

target("foo", function ()
    set_kind("binary")
    add_files("src/*.cpp")
end)

add_defines("ROOT")

target("bar", function ()
    set_kind("binary")
    add_files("src/*.cpp")
end)

foo 和 bar 兩個域是完全隔離的,我們即使在它們中間配置其他設置,也不會影響它們,另外,它還對 LSP 非常友好,即使一鍵格式化,也不會導致縮進混亂。

注:這僅僅只是一隻可選的擴展語法,現有的配置語法還是完全支持的,用戶可以根據自己的需求喜好,來選擇合適的配置語法。

爲特定編譯器添加 flags

使用 add_cflags, add_cxxflags 等接口配置的值,通常都是跟編譯器相關的,儘管 Xmake 也提供了自動檢測和映射機制,
即使設置了當前編譯器不支持的 flags,Xmake 也能夠自動忽略它,但是還是會有警告提示。

新版本中,我們改進了所有 flags 添加接口,可以僅僅對特定編譯器指定 flags,來避免額外的警告,例如:

add_cxxflags("clang::-stdlib=libc++")
add_cxxflags("gcc::-stdlib=libc++")

或者:

add_cxxflags("-stdlib=libc++", {tools = "clang"})
add_cxxflags("-stdlib=libc++", {tools = "gcc"})

注:不僅僅是編譯flags,對 add_ldflags 等鏈接 flags,也是同樣生效的。

renderdoc 調試器支持

感謝 @SirLynix 貢獻了這個很棒的特性,它可以讓 Xmake 直接加載 renderdoc 去調試一些圖形渲染程序。

使用非常簡單,我們先確保安裝了 renderdoc,然後配置調試器爲 renderdoc,加載調試運行:

$ xmake f --debugger=renderdoc
$ xmake run -d

具體使用效果如下:

新增 C++ 異常接口配置

Xmake 新增了一個 set_exceptions 抽象化配置接口,我們可以通過這個配置,配置啓用和禁用 C++/Objc 的異常。

通常,如果我們通過 add_cxxflags 接口去配置它們,需要根據不同的平臺,編譯器分別處理它們,非常繁瑣。

例如:

on_config(function (target)
    if (target:has_tool("cxx", "cl")) then
        target:add("cxflags", "/EHsc", {force = true})
        target:add("defines", "_HAS_EXCEPTIONS=1", {force = true})
    elseif(target:has_tool("cxx", "clang") or target:has_tool("cxx", "clang-cl")) then
        target:add("cxflags", "-fexceptions", {force = true})
        target:add("cxflags", "-fcxx-exceptions", {force = true})
    end
end)

而通過這個接口,我們就可以抽象化成編譯器無關的方式去配置它們。

開啓 C++ 異常:

set_exceptions("cxx")

禁用 C++ 異常:

set_exceptions("no-cxx")

我們也可以同時配置開啓 objc 異常。

set_exceptions("cxx", "objc")

或者禁用它們。

set_exceptions("no-cxx", "no-objc")

Xmake 會在內部自動根據不同的編譯器,去適配對應的 flags。

支持 ispc 編譯規則

Xmake 新增了 ipsc 編譯器內置規則支持,非常感謝 @star-hengxing 的貢獻,具體使用方式如下:

target("test")
    set_kind("binary")
    add_rules("utils.ispc", {header_extension = "_ispc.h"})
    set_values("ispc.flags", "--target=host")
    add_files("src/*.ispc")
    add_files("src/*.cpp")

支持 msvc 的 armasm 編譯器

之前的版本,Xmake 增加了 Windows ARM 的初步支持,但是對 asm 編譯還沒有很好的支持,因此這個版本,我們繼續完善 Windows ARM 的支持。

對 msvc 的 armasm.exearmasm64.exe 都支持上了。

另外,我們也改進了包對 Windows ARM 平臺的交叉編譯支持。

新增 gnu-rm 構建規則

Xmake 也新增了一個使用 gnu-rm 工具鏈去構建嵌入式項目的規則和例子工程,非常感謝 @JacobPeng 的貢獻。

add_rules("mode.debug", "mode.release")

add_requires("gnu-rm")
set_toolchains("@gnu-rm")
set_plat("cross")
set_arch("armv7")

target("foo")
    add_rules("gnu-rm.static")
    add_files("src/foo/*.c")

target("hello")
    add_deps("foo")
    add_rules("gnu-rm.binary")
    add_files("src/*.c", "src/*.S")
    add_files("src/*.ld")
    add_includedirs("src/lib/cmsis")

完整工程見:Embed GNU-RM Example

新增 OpenBSD 系統支持

之前的版本,Xmake 僅僅支持 FreeBSD 系統,而 OpenBSD 跟 FreeBSD 還是有不少差異的,導致 Xmake 無法在它上面正常編譯安裝。

而新版本已經完全支持在 OpenBSD 上運行 Xmake 了。

更新內容

新特性

  • 一種新的可選域配置語法,對 LSP 友好,並且支持域隔離。
  • #2944: 爲嵌入式工程添加 gnu-rm.binarygnu-rm.static 規則和測試工程
  • #2636: 支持包組件
  • 支持 msvc 的 armasm/armasm64
  • #3023: 改進 xmake run -d,添加 renderdoc 調試器支持
  • #3022: 爲特定編譯器添加 flags
  • #3025: 新增 C++ 異常接口配置
  • #3017: 支持 ispc 編譯器規則

改進

  • #2925: 改進 doxygen 插件
  • #2948: 支持 OpenBSD
  • 添加 xmake g --insecure-ssl=y 配置選項去禁用 ssl 證書檢測
  • #2971: 使 vs/vsxmake 工程生成的結果每次保持一致
  • #3000: 改進 C++ 模塊構建支持,實現增量編譯支持
  • #3016: 改進 clang/msvc 去更好地支持 std 模塊

Bugs 修復

  • #2949: 修復 vs 分組
  • #2952: 修復 armlink 處理長命令失敗問題
  • #2954: 修復 c++ module partitions 路徑無效問題
  • #3033: 探測循環模塊依賴

https://tboox.org/cn/2022/11/08/xmake-update-v2.7.3/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章