--- title: Xmake v2.7.3 发布,包组件和 C++ 模块增量构建支持 tags: [xmake, lua, C/C++, package, components] date: 2022-11-08 author: Ruki outline: deep --- ## 新特性介绍 ### 包组件支持 #### 背景简介 这个新特性主要用于实现从一个 C/C++ 包中集成特定的子库,一般用于一些比较大的包中的库组件集成。 因为这种包里面提供了很多的子库,但不是每个子库用户都需要,全部链接反而有可能会出问题。 尽管,之前的版本也能够支持子库选择的特性,例如: ```lua 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~foo` 和 `sfml~bar` 会作为两个独立的包,重复安装,占用双倍的磁盘空间 2. 也会重复编译一些共用代码,影响安装效率 3. 如果一个目标同时依赖了 `sfml~foo` 和 `sfml~bar`,会存在链接冲突 如果是对于 boost 这种超大包的集成,重复编译和磁盘占用的影响会非常大,如果在子库组合非常多的情况下,甚至会导致超过 N 倍的磁盘占用。 为了解决这个问题,Xmake 新增了包组件模式,它提供了以下一些好处: 1. 仅仅一次编译安装,任意多个组件快速集成,极大提升安装效率,减少磁盘占用 2. 组件抽象化,跨编译器和平台,用户不需要关心如何配置每个子库之间链接顺序依赖 3. 使用更加方便 更多背景详情见:[#2636](https://github.com/xmake-io/xmake/issues/2636) #### 使用包组件 对于用户,使用包组件是非常方便的,因为用户是不需要维护包的,只要使用的包,它配置了相关的组件集,我们就可以快速集成和使用它,例如: ```lua add_requires("sfml") target("foo") set_kind("binary") add_packages("sfml", {components = "graphics"}) target("bar") set_kind("binary") add_packages("sfml", {components = "network"}) ``` #### 查看包组件 那么,如何知道指定的包提供了哪些组件呢?我们可以通过执行下面的命令查看: ```bash $ 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: 配置每个包组件 ##### 包组件的链接配置 大多数情况下,包组件只需要配置它自己的一些子链接信息,例如: ```lua 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](https://github.com/xmake-io/xmake/blob/master/tests/projects/package/components/xmake.lua) ##### 配置组件的编译信息 我们不仅可以配置每个组件的链接信息,还有 includedirs, defines 等等编译信息,我们也可以对每个组件单独配置。 ```lua package("sfml") on_component("graphics", function (package, component) package:add("defines", "TEST") end) ``` ##### 配置组件依赖 ```lua 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 组件还会额外依赖 `window` 和 `system` 两个组件。 因此,在用户端,我们对 graphics 的组件使用,可以从 ```lua add_packages("sfml", {components = {"graphics", "window", "system"}) ``` 简化为: ```lua add_packages("sfml", {components = "graphics") ``` 因为,只要我们开启了 graphics 组件,它也会自动启用依赖的 window 和 system 组件,并且自动保证链接顺序正确。 另外,我们也可以通过 `add_components("graphics", {deps = {"window", "system"}})` 来配置组件依赖关系。 ##### 从系统库中查找组件 我们知道,在包配置中,配置 `add_extsources` 可以改进包在系统中的查找,比如从 apt/pacman 等系统包管理器中找库。 当然,我们也可以让每个组件也能通过 `extsources` 配置,去优先从系统库中找到它们。 例如,sfml 包,它在 homebrew 中其实也是组件化的,我们完全可以让包从系统库中,找到对应的每个组件,而不需要每次源码安装它们。 ```bash $ 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: ```lua 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) ``` ##### 默认的全局组件配置 除了通过指定组件名的方式,配置特定组件,如果我们没有指定组件名,默认就是全局配置所有组件。 ```lua package("sfml") on_component(function (package, component) -- configure all components end) ``` 当然,我们也可以通过下面的方式,指定配置 graphics 组件,剩下的组件通过默认的全局配置接口进行配置: ```lua 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 无法检测到这种情况,遇到循环依赖,编译就会卡死,没有任何提示信息,这对用户非常不友好。 而新版本中,我们对这种情况做了改进,增加了模块的循环依赖检测,编译时候会出现以下错误提示,方便用户定位问题: ```bash $ 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 不是很友好,如果你格式化配置,缩进就完全错位了。 ```lua target("foo") set_kind("binary") add_files("src/*.cpp") ``` 另外,如果两个 target 之间配置了一些全局的配置,那么它不能自动结束当前 target 作用域,用户需要显式调用 `target_end()`。 ```lua 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()` 的问题还是存在。 ```lua 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 域隔离问题,例如: ```lua 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,来避免额外的警告,例如: ```lua add_cxxflags("clang::-stdlib=libc++") add_cxxflags("gcc::-stdlib=libc++") ``` 或者: ```lua add_cxxflags("-stdlib=libc++", {tools = "clang"}) add_cxxflags("-stdlib=libc++", {tools = "gcc"}) ``` 注:不仅仅是编译flags,对 add_ldflags 等链接 flags,也是同样生效的。 ### renderdoc 调试器支持 感谢 [@SirLynix](https://github.com/SirLynix) 贡献了这个很棒的特性,它可以让 Xmake 直接加载 renderdoc 去调试一些图形渲染程序。 使用非常简单,我们先确保安装了 renderdoc,然后配置调试器为 renderdoc,加载调试运行: ```bash $ xmake f --debugger=renderdoc $ xmake run -d ``` 具体使用效果如下: ### 新增 C++ 异常接口配置 Xmake 新增了一个 `set_exceptions` 抽象化配置接口,我们可以通过这个配置,配置启用和禁用 C++/Objc 的异常。 通常,如果我们通过 add_cxxflags 接口去配置它们,需要根据不同的平台,编译器分别处理它们,非常繁琐。 例如: ```lua 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++ 异常: ```lua set_exceptions("cxx") ``` 禁用 C++ 异常: ```lua set_exceptions("no-cxx") ``` 我们也可以同时配置开启 objc 异常。 ```lua set_exceptions("cxx", "objc") ``` 或者禁用它们。 ```lua set_exceptions("no-cxx", "no-objc") ``` Xmake 会在内部自动根据不同的编译器,去适配对应的 flags。 ### 支持 ispc 编译规则 Xmake 新增了 ipsc 编译器内置规则支持,非常感谢 [@star-hengxing](https://github.com/star-hengxing) 的贡献,具体使用方式如下: ```lua 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.exe` 和 `armasm64.exe` 都支持上了。 另外,我们也改进了包对 Windows ARM 平台的交叉编译支持。 ### 新增 gnu-rm 构建规则 Xmake 也新增了一个使用 gnu-rm 工具链去构建嵌入式项目的规则和例子工程,非常感谢 [@JacobPeng](https://github.com/JacobPeng) 的贡献。 ```lua 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](https://github.com/xmake-io/xmake/blob/master/tests/projects/embed/gnu-rm/hello/xmake.lua) ### 新增 OpenBSD 系统支持 之前的版本,Xmake 仅仅支持 FreeBSD 系统,而 OpenBSD 跟 FreeBSD 还是有不少差异的,导致 Xmake 无法在它上面正常编译安装。 而新版本已经完全支持在 OpenBSD 上运行 Xmake 了。 ## 更新内容 ### 新特性 * 一种新的可选域配置语法,对 LSP 友好,并且支持域隔离。 * [#2944](https://github.com/xmake-io/xmake/issues/2944): 为嵌入式工程添加 `gnu-rm.binary` 和 `gnu-rm.static` 规则和测试工程 * [#2636](https://github.com/xmake-io/xmake/issues/2636): 支持包组件 * 支持 msvc 的 armasm/armasm64 * [#3023](https://github.com/xmake-io/xmake/pull/3023): 改进 xmake run -d,添加 renderdoc 调试器支持 * [#3022](https://github.com/xmake-io/xmake/issues/3022): 为特定编译器添加 flags * [#3025](https://github.com/xmake-io/xmake/pull/3025): 新增 C++ 异常接口配置 * [#3017](https://github.com/xmake-io/xmake/pull/3017): 支持 ispc 编译器规则 ### 改进 * [#2925](https://github.com/xmake-io/xmake/issues/2925): 改进 doxygen 插件 * [#2948](https://github.com/xmake-io/xmake/issues/2948): 支持 OpenBSD * 添加 `xmake g --insecure-ssl=y` 配置选项去禁用 ssl 证书检测 * [#2971](https://github.com/xmake-io/xmake/pull/2971): 使 vs/vsxmake 工程生成的结果每次保持一致 * [#3000](https://github.com/xmake-io/xmake/issues/3000): 改进 C++ 模块构建支持,实现增量编译支持 * [#3016](https://github.com/xmake-io/xmake/pull/3016): 改进 clang/msvc 去更好地支持 std 模块 ### Bugs 修复 * [#2949](https://github.com/xmake-io/xmake/issues/2949): 修复 vs 分组 * [#2952](https://github.com/xmake-io/xmake/issues/2952): 修复 armlink 处理长命令失败问题 * [#2954](https://github.com/xmake-io/xmake/issues/2954): 修复 c++ module partitions 路径无效问题 * [#3033](https://github.com/xmake-io/xmake/issues/3033): 探测循环模块依赖