构建策略
xmake 有很多的默认行为,比如:自动检测和映射 flags、跨 target 并行构建等,虽然提供了一定的智能化处理,但众口难调,不一定满足所有的用户的使用习惯和需求。
因此 xmake 提供默认构建策略的修改设置,开放给用户一定程度上的可配置性。
它主要通过 set_policy 接口来配置。
我们通常可以用它来配置修改 target,package 以及工程整体的一些行为策略。
使用方式如下:
set_policy("check.auto_ignore_flags", false)
只需要在项目根域设置这个配置,就可以禁用 flags 的自动检测和忽略机制,另外 set_policy
也可以针对某个特定的 target 局部生效。
target("test", function()
set_policy("check.auto_ignore_flags", false)
end)
另外,如果设置的策略名是无效的,xmake 也会有警告提示。
如果要获取当前 xmake 支持的所有策略配置列表和描述,可以执行下面的命令:
$ xmake l core.project.policy.policies
{
"check.auto_map_flags" = {
type = "boolean",
description = "Enable map gcc flags to the current compiler and linker automatically.",
default = true
},
"build.across_targets_in_parallel" = {
type = "boolean",
description = "Enable compile the source files for each target in parallel.",
default = true
},
"check.auto_ignore_flags" = {
type = "boolean",
description = "Enable check and ignore unsupported flags automatically.",
default = true
}
}
我们也可以通过命令行的方式去设置修改内部的策略:
$ xmake f --policies=package.fetch_only
默认设置策略名,就是启用状态,当然我们也可以指定设置其他值,禁用它。
$ xmake f --policies=package.precompiled:n
或者同时配置多个策略值,用逗号分割。
$ xmake f --policies=package.precompiled:n,package.install_only
check.auto_ignore_flags
xmake 默认会对所有 add_cxflags
, add_ldflags
接口设置的原始 flags 进行自动检测,如果检测当前编译器和链接器不支持它们,就会自动忽略。
这通常是很有用的,像一些可选的编译 flags,即使不支持也能正常编译,但是强行设置上去,其他用户在编译的时候,有可能会因为编译器的支持力度不同,出现一定程度的编译失败。
但由于自动检测并不保证100%可靠,有时候会有一定程度的误判,所以某些用户并不喜欢这个设定(尤其是针对交叉编译工具链,更容易出现失败)。
目前如果检测失败,会有警告提示避免用户莫名躺坑,例如:
warning: add_ldflags("-static") is ignored, please pass `{force = true}` or call `set_policy("check.auto_ignore_flags", false)` if you want to set it.
根据提示,我们可以自己分析判断,是否需要强制设置这个 flags,一种就是通过:
add_ldflags("-static", {force = true})
来显示的强制设置上它,跳过自动检测,这对于偶尔的 flags 失败,是很有效快捷的处理方式,但是对于交叉编译时候,一堆的 flags 设置检测不过的情况下,每个都设置 force 太过于繁琐。
这个时候,我们就可以通过 set_policy
来对某个 target 或者整个 project 直接禁用默认的自动检测行为:
set_policy("check.auto_ignore_flags", false)
target("test", function()
add_ldflags("-static")
end)
然后我们就可以随意设置各种原始 flags,xmake 不会去自动检测和忽略他们了。
check.auto_map_flags
这是 xmake 的另外一个对 flags 的智能分析处理,通常像 add_links
, add_defines
这种 xmake 内置的 api 去设置的配置,是具有跨平台特性的,不同编译器平台会自动处理成对应的原始 flags。
但是,有些情况,用户还是需要自己通过 add_cxflags
, add_ldflags
设置原始的编译链接 flags,这些 flags 并不能很好的跨编译器。
就拿 -O0
的编译优化 flags 来说,虽然有 set_optimize
来实现跨编译器配置,但如果用户直接设置 add_cxflags("-O0")
呢?gcc/clang 下可以正常处理,但是 msvc 下就不支持了。
也许我们能通过 if is_plat() then
来分平台处理,但很繁琐,因此 xmake 内置了 flags 的自动映射功能。
基于 gcc flags 的普及性,xmake 采用 gcc 的 flags 命名规范,对其根据不同的编译实现自动映射,例如:
add_cxflags("-O0")
这一行设置,在 gcc/clang 下还是 -O0
,但如果当前是 msvc 编译器,那边会自动映射为 msvc 对应 -Od
编译选项来禁用优化。
整个过程,用户是完全无感知的,直接执行 xmake 就可以跨编译器完成编译。
当然,目前的自动映射实现还不是很成熟,没有 100% 覆盖所有 gcc 的 flags,所以还是有不少 flags 是没去映射的。
也有部分用户并不喜欢这种自动映射行为,那么我们可以通过下面的设置完全禁用这个默认的行为:
set_policy("check.auto_map_flags", false)
build.across_targets_in_parallel
这个策略也是默认开启的,主要用于跨 target 间执行并行构建,v2.3.3 之前的版本,并行构建只能针对单个 target 内部的所有源文件,
跨 target 的编译,必须要要等先前的 target 完全 link 成功,才能执行下一个 target 的编译,这在一定程度上会影响编译速度。
然而每个 target 的源文件是可以完全并行化处理的,最终在一起执行 link 过程,v2.3.3 之后的版本通过这个优化,构建速度提升了30%。
当然,如果有些特殊的 target 里面的构建源文件要依赖先前的 target(尤其是一些自定义 rules 的情况,虽然很少遇到),我们也可以通过下面的设置禁用这个优化行为:
set_policy("build.across_targets_in_parallel", false)
build.fence
由于配置 set_policy("build.across_targets_in_parallel", false)
存在局限性,它会限制父 target 和它的所有依赖的子 target 之间的并行度,影响的范围有点大。
而我们做 codegen 时候,有时候仅仅只是想对其中某个依赖的 target 限制并行度,作为 codegen 程序,提前让它完成编译。
这个时候,build.across_targets_in_parallel
就无法精细控制了,编译速度也无法达到最优。
因此,我们新增了 build.fence
策略,它可以仅仅只针对特定的子 target 限制并行编译链接。
相关的背景细节,可以看下:#5003
例如:
target("autogen", function()
set_default(false)
set_kind("binary")
set_plat(os.host())
set_arch(os.arch())
add_files("src/autogen.cpp")
set_languages("c++11")
set_policy("build.fence", true)
end)
target("test", function()
set_kind("binary")
add_deps("autogen")
add_rules("autogen")
add_files("src/main.cpp")
add_files("src/*.in")
end)
其中 autogen 目标程序需要在 test 程序的源码被编译前,就要完成编译链接,因为 test 目标需要运行 autogen 程序,去动态生成一些源码参与编译。
而针对 autogen 配置 set_policy("build.fence", true)
就可以实现这个目的。
build.merge_archive
如果设置了这个策略,那么使用 add_deps()
依赖的目标库不再作为链接存在,而是直接把它们合并到父目标库中去。
例如:
add_rules("mode.debug", "mode.release")
target("add", function()
set_kind("static")
add_files("src/add.c")
add_files("src/subdir/add.c")
end)
target("sub", function()
set_kind("static")
add_files("src/sub.c")
add_files("src/subdir/sub.c")
end)
target("mul", function()
set_kind("static")
add_deps("add", "sub")
add_files("src/mul.c")
set_policy("build.merge_archive", true)
end)
target("test", function()
add_deps("mul")
add_files("src/main.c")
end)
libmul.a 静态库会自动合并 libadd.a 和 libsub.a 两个子依赖的静态库。
build.ccache
Xmake 默认是开启内置的编译缓存的,通过设置这个策略,可以显式禁用缓存。
set_policy("build.ccache", false)
当然,我们也可以命令行去禁用它。
$ xmake f --ccache=n
或者
$ xmake f --policies=build.ccache:n
build.warning
默认编译通常不会实时回显警告输出,我们通常需要使用 xmake -w
开启,或者通过 xmake g --build_warning=y
来全局开启它。
现在,我们也可以在 xmake.lua 配置中去默认启用警告回显输出。
set_policy("build.warning", true)
set_warnings("all", "extra")
这个时候,即使我们执行 xmake
命令,也能直接回显警告输出。
build.optimization.lto
xmake 改进了对 LTO 链接时优化的支持,对 gcc/clang/msvc 等不同平台下都进行了适配,只需要启用这个策略,就能对特定 target 开启 LTO。
set_policy("build.optimization.lto", true)
我们也可以通过命令行选项快速开启。
$ xmake f --policies=build.optimization.lto
build.cuda.devlink
可以通过这个配置,显示开启对特定目标的设备链接。
这通常用于 Cuda 项目的构建,以及非 Cuda binary/shared 依赖 Cuda static 目标的情况,这个时候,Cuda static 目标就需要显示配置这个,开启设备链接。
target("test", function()
set_kind("static")
set_policy("build.cuda.devlink", true)
end)
而默认 Cuda binary/shared 是开启 devlink 的,我们也可以通过策略显示禁用它。
关于这个的详细背景说明,见:#1976
build.sanitizer.address
Address Sanitizer(ASan)是一个快速的内存错误检测工具,由编译器内置支持,通常我们需要在编译和链接的 flags 中同时配置 -fsanitize-address
才能正确开启。
而我们可以通过开启这个策略,就可以快速全局启用它,这会使得编译出来的程序,直接支持 ASan 检测。
例如,我们可以通过命令行的方式去启用:
$ xmake f --policies=build.sanitizer.address
也可以通过接口配置去全局启用:
set_policy("build.sanitizer.address", true)
当然,我们也可以单独对某个特定的 target 去配置开启。
另外,如果全局配置它,我们就可以同时对所有依赖包也生效。
set_policy("build.sanitizer.address", true)
add_requires("zlib")
add_requires("libpng")
它等价于,对每个包依次设置 asan 配置。
add_requires("zlib", {configs = {asan = true}})
add_requires("libpng", {configs = {asan = true}})
add_rules("mode.asan", "mode.tsan", "mode.ubsan", "mode.msan")
将被废弃,尽可能使用这些新的策略,因为这些构建模式无法同步对依赖包生效。
另外,我们也可以同时生效多个 sanitizer 检测,例如:
set_policy("build.sanitizer.address", true)
set_policy("build.sanitizer.undefined", true)
或者
$ xmake f --policies=build.sanitizer.address,build.sanitizer.undefined
build.sanitizer.thread
与 build.sanitizer.address 类似,用于检测线程安全问题。
build.sanitizer.memory
与 build.sanitizer.address 类似,用于检测内存问题。
build.sanitizer.leak
与 build.sanitizer.address 类似,用于检测内存泄漏问题。
build.sanitizer.undefined
与 build.sanitizer.address 类似,用于检测 undefined 问题。
build.always_update_configfiles
这个策略用于对 add_configfiles
配置文件的自动生成行为。默认情况下,xmake 仅仅只会在首次 xmake config
时候,或者 xmake.lua 配置有改动的是否,才会触发 configfiles 的重新生成。
之后的每次构建,只要配置没有变化,就不会重新生成 configfiles。
但是,如果我们的 configfiles 中有使用 GIT_COMMIT 等变量,想要每次构建时候,总是重新生成最新的配置,那么可以配置它。
具体使用背景,可以看下:#4747
build.intermediate_directory
配置启用或禁用构建的内部子目录。
默认情况下,执行 xmake
编译项目会自动在 build 目录下根据平台。架构,编译模式生成子目录,分别存储对象文件,目标文件。例如:
build/
└── macosx
└── x86_64
└── release
└─test
如果配置禁用此策略,那么生成的产物将会直接生成到 build 根目录下。变成:
build/
└─ test
build.rpath
配置启用或者禁用构建时的 target rpath 设置。
默认情况下,如果 target(foo)
依赖动态库 bar,那么生成的 foo 可执行文件会自动加上 bar 的 rpath,这能保证用户直接执行 foo 程序,也能正确找到 bar。
如果你想禁用这个行为,可以显式配置禁用它。
install.rpath
尽管构建后的程序,会被设置 rpath,但是当 xmake install
安装后,它构建时候的 rpath 就不一定完全适用了,因此 xmake 会自动修改调整 rpath,使得安装后的程序,同样可以找到它的依赖库。
不过前提是,用户自己先得通过 add_rpathdirs("/xxx", {installonly = true})
去配置独立的安装 rpath。
而我们也可以通过这个 policy 去禁用默认的安装阶段 rpath 设置行为。
run.autobuild
这个策略用于调整 xmake run
的行为,默认情况下,执行 xmake run
并不会自动构建目标程序,如果程序还没被编译,就是提示用户手动构建一下。
而开启这个策略,我们就可以在运行程序前,先自动构建对应的目标程序。
$ xmake f --policies=run.autobuild
$ xmake run
如果想要全局生效这个策略,可以全局开启它。
$ xmake g --policies=run.autobuild
preprocessor.linemarkers
通常用户编译缓存中,预处理器的生成策略,默认开启,如果配置关闭这个策略,那么缓存生成的预处理文件内容将不包含 linemarkers 信息,这会极大减少预处理文件大小。
也会提升缓存的处理效率,但是缺点就是会丢失源码行信息,如果遇到编译错误,将无法看到准确的出错代码行。
preprocessor.gcc.directives_only
这也是用于预处理器的策略,默认开启,这会提升 gcc 下编译缓存预处理的效率,但是如果源文件中包含 __DATE__
, __TIME__
等宏,就会导致缓存出现不一致。
因此,可以根据自身工程代码,按需关闭此策略,确保生成的结果一致。
package.requires_lock
可用于开启 add_requires()
引入的依赖包的版本锁定。
具体看下:依赖包的锁定和升级
package.fetch_only
如果开启这个策略,那么所有的依赖包仅仅只会从系统获取,不会从远程下载安装。
package.install_only
如果开启这个策略,那么所有的依赖包仅仅只会走远程下载安装,不会从系统查找获取。
package.librarydeps.strict_compatibility
默认禁用,如果启用它,那么当前包和它的所有库依赖包之间会保持严格的兼容性,任何依赖包的版本更新,都会强制触发当前包的重新编译安装。
以确保所有的包都是二进制兼容的,不会因为某个依赖包接口改动,导致和其他已被安装的其他包一起链接时候,发生链接和运行错误。
package("foo")
add_deps("bar", "zoo")
set_policy("package.librarydeps.strict_compatibility", true)
例如,如果 bar 或者 zoo 的版本有更新,那么 foo 也会重新编译安装。
package.strict_compatibility
默认禁用,如果启用它,那么当前包和其他所有依赖它的包之间会保持严格的兼容性,这个包的版本更新,都会强制触发其他父包的重新编译安装。
以确保所有的包都是二进制兼容的,不会因为某个依赖包接口改动,导致和其他已被安装的其他包一起链接时候,发生链接和运行错误。
package("foo", function()
set_policy("package.strict_compatibility", true)
end)
package("bar", function()
add_deps("foo")
end)
package("zoo", function()
add_deps("foo")
end)
例如,如果 foo 的版本有更新,那么 bar 和 zoo 都会被强制重新编译安装。
package.install_always
每次运行 xmake f -c
重新配置的时候,总是会重新安装包,这对于本地第三方源码包集成时候比较有用。
因为,用户可能随时需要修改第三方源码,然后重新编译集成它们。
之前只能通过每次修改包版本号,来触发重新编译,但是有了这个策略,就能每次都会触发重编。
add_rules("mode.debug", "mode.release")
package("foo", function()
add_deps("cmake")
set_sourcedir(path.join(os.scriptdir(), "foo"))
set_policy("package.install_always", true)
on_install(function (package)
local configs = {}
table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:debug() and "Debug" or "Release"))
table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF"))
import("package.tools.cmake").install(package, configs)
end)
on_test(function (package)
assert(package:has_cfuncs("add", {includes = "foo.h"}))
end)
end)
add_requires("foo")
target("demo", function()
set_kind("binary")
add_files("src/main.c")
add_packages("foo")
end)
package.download.http_headers
设置包下载的 http headers
如果有些包的 url 下载,需要设置特定 http headers,才能通过下载,可以通过这个策略来指定。
package("xxx")
set_policy("package.download.http_headers", "TEST1: foo", "TEST2: bar")
我们也可以设置指定的 urls 的 http headers:
add_urls("https://github.com/madler/zlib/archive/$(version).tar.gz", {
http_headers = {"TEST1: foo", "TEST2: bar"}
})