Bazel 平台与约束(Platforms & Constraints)完全指南:构建、交叉编译与不兼容目标跳过的底层机制
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
导读
Bazel 需要在多种硬件、操作系统与系统配置上构建和测试代码,这通常涉及不同版本的链接器、编译器乃至远程执行集群。为管理这一复杂性,Bazel 提供了约束(constraints)与平台(platforms)两个核心概念:用约束描述机器的可区分属性,用平台描述一台完整的机器。本文将围绕docs/concepts/platforms.mdx的完整体系,深入讲解三种平台角色(Host / Execution / Target)、--platforms的指定方式、constraint_setting/constraint_value/platform规则的写法,以及基于target_compatible_with跳过不兼容目标的完整机制,并结合本仓库源码(如 IncompatiblePlatformProvider.java)揭示其底层实现。读完本文,你将能够在自己的 BUILD 文件中独立定义平台、为规则声明平台兼容性,并利用bazel cquery排查不兼容目标。
为什么需要约束与平台
Bazel 可以构建面向多种机器的代码:x86 与 Arm 的 CPU 架构、Linux/macOS/Windows 等操作系统、有无 GPU、本地编译器版本各不相同。这些差异直接决定应该使用哪套编译工具链、哪个src文件以及哪些依赖。
- 约束(constraint):构建机器或生产机器的一种可区分属性(distinguishing property)。常见约束包括 CPU 架构、GPU 有无、本地安装的编译器版本。但约束可以是任何在编排构建任务时"有意义地区分机器"的属性——完全由你定义。
- 平台(platform):一组约束的集合,用于描述一台完整的机器。Bazel 用平台概念让开发者决定:为哪些机器构建(目标平台)、由哪些机器执行编译与测试动作(执行平台)、构建动作应使用哪套工具链。
此外,约束还能配合 select() 在构建规则上做自定义属性与依赖选择,例如"当构建目标是 Arm 机器时使用src_arm.cc"。这与可配置属性(configurable attributes)机制是一体的。
平台的三种角色
Bazel 识别一个平台可能扮演的三种角色:
| 角色 | 含义 |
|---|---|
| Host(宿主) | Bazel 自身运行所在的平台 |
| Execution(执行) | 运行编译动作以产出构建结果的平台 |
| Target(目标) | 被构建的代码最终要运行于其上的平台 |
需要注意,一次构建只有一个 Host 平台,但往往有多个 Execution 与 Target 平台。例如远程 Linux CI 机器与开发者本地的 Mac 可能同时执行构建动作;移动 App 的代码则面向多种手机型号与硬件扩展。
构建与平台的三种关系
一次构建与平台之间通常存在三种关系形态:
- 单平台构建(Single-platform):Host、Execution、Target 三者相同。典型场景是在开发者机器上不使用远程执行地直接构建,并在同一台机器上运行产物。
- 交叉编译构建(Cross-compilation):Host 与 Execution 相同,但 Target 不同。例如在 MacBook Pro 上(不启用远程执行)构建 iOS App。
- 多平台构建(Multi-platform):Host、Execution、Target 三者互不相同。例如在 MacBook Pro 上开发,同时用远程 Linux 机器编译不需要 Xcode 的 C++ 动作——Host 是 macOS,Execution 是 Linux CI 机器,Target 是 iOS 设备。
从源码结构看,这三者的区分在配置层由PlatformOptions管理:--host_platform与--target_platform分别控制宿主与目标平台,且--host_platform的旧名称是experimental_host_platform(见 PlatformOptions.java)。执行平台则由远程执行/动态执行策略按动作选择。
指定平台:--platforms标志
开发者使用平台最常见的方式,是通过--platforms标志指定期望的目标机器:
$ bazel build //:my_linux_app --platforms=//myplatforms:linux_x86由于各组织的构建机器配置差异很大,一般由组织自行维护平台定义(BUILD 文件中的platform规则)。
当未设置--platforms时,其默认值是@platforms//host。该平台是特殊定义的,会自动探测 Bazel 运行所在机器的 OS 与 CPU 属性,从而使构建默认面向 Bazel 所在的那台机器。构建规则可以借助@platforms//os与@platforms//cpu中的约束,配合select()基于这些属性做分支选择。
Bazel 内部将默认宿主平台别名指向@bazel_tools//tools:host_platform,这一点在源码中有直接对应:PlatformOptions.java中定义了DEFAULT_HOST_PLATFORM = "@bazel_tools//tools:host_platform",并通过--host_platform标志(oldName = "experimental_host_platform")暴露给用户。
常用的约束与平台
为保持生态一致性,Bazel 团队维护了一个包含主流 CPU 架构与操作系统约束定义的仓库(@platforms)。其中预定义了:
@platforms//os下的各操作系统约束值;@platforms//cpu下的各 CPU 架构约束值;@platforms//host:Bazel 内置的特殊平台定义(别名@bazel_tools//tools:host_platform),自动探测 Bazel 运行机器的 OS 与 CPU 属性。
在你的 MODULE 中,@platforms通常由 Bazel 自动提供(作为内置模块),可直接在 BUILD 文件中以@platforms//os:linux、@platforms//cpu:x86_64等标签引用。
定义约束:constraint_setting 与 constraint_value
约束通过constraint_setting与constraint_value两个构建规则建模。
constraint_setting声明一种属性类型:
constraint_setting(name = "cpu")constraint_value声明该属性的一个可能取值:
constraint_value( name = "x86", constraint_setting = ":cpu" )上述示例若定义在cpus/BUILD中,即可用标签//cpus:x86在定义平台或定制构建规则时引用。如果可见性允许,你也可以为已有的constraint_setting扩展自定义取值——例如为@platforms//cpu补充一个尚不存在的架构值。
在集成测试中可以找到更完整的用法示例:target_compatible_with_test.sh 的set_up()中同时定义了foo_version、bar_version两个constraint_setting,以及各自的多个constraint_value,并以不同组合构造出多个平台。
定义平台:platform 规则
platform构建规则把平台定义为一组constraint_value的集合:
platform( name = "linux_x86", constraint_values = [ "@platforms//os:linux", "@platforms//cpu:x86", ], )这描述了一台必须同时满足@platforms//os:linux与@platforms//cpu:x86的机器。
平台对同一个constraint_setting只能有一个constraint_value。这意味着,一个平台不能同时声明两个 CPU,除非你新建另一种constraint_setting类型来建模第二个取值。底层实现会在冲突时直接报错:Platform.java中调用platformBuilder.build()时,若出现重复约束会抛出ConstraintCollection.DuplicateConstraintException并定位到constraint_values属性(见 Platform.java)。
从Platform.java的实现可以进一步了解platform规则的完整能力面(源码):
parents:继承父平台,且只允许单个父平台(多于一个会触发属性错误);constraint_values:显式声明的约束值集合;exec_properties:附加到该平台执行动作上的远程执行属性键值对;flags:平台附加标志列表;required_settings:平台生效所需的config_setting匹配条件;check_toolchain_types/allowed_toolchain_types:可选地校验该平台允许使用的工具链类型;missing_toolchain_error:缺少工具链时的自定义报错文案。
注意Platform.java的注释还指出一个约束:若平台设置了host_platform或target_platform属性为 true,会自动纳入探测到的 CPU 与 OS 约束,此时再在constraint_values中重复添加同类约束会报错。
跳过不兼容目标:target_compatible_with
当针对特定目标平台构建时,往往希望跳过在该平台上永远不会工作的目标。例如在 Linux 机器上用//...全量构建时,Windows 设备驱动很可能产生大量编译错误。
此时使用target_compatible_with通用属性,告知 Bazel 你的代码需要满足哪些目标平台约束。
最简单的用法是把目标限制到单一平台:目标不会为任何无法满足全部约束的平台构建。以下示例把win_driver_lib.cc限制为仅 64 位 Windows 可用:
cc_library( name = "win_driver_lib", srcs = ["win_driver_lib.cc"], target_compatible_with = [ "@platforms//cpu:x86_64", "@platforms//os:windows", ], ):win_driver_lib仅与 64 位 Windows 构建兼容,与其他所有平台都不兼容。不兼容性是传递的(transitive):任何传递依赖某个不兼容目标的目标,自身也会被判定为不兼容。这一点与IncompatiblePlatformProvider的实现一致——provider 中专门记录了"因哪些不兼容依赖而导致自身不兼容"(targetsResponsibleForIncompatibility字段)。
目标在什么时机被跳过
当不兼容目标作为**目标模式展开(target pattern expansion)**的一部分被纳入构建时,它会被跳过。下面两种调用都会跳过模式展开中发现的不兼容目标:
$ bazel build --platforms=//:myplatform //...$ bazel build --platforms=//:myplatform //:alltest_suite中不兼容的测试,若该test_suite通过--expand_test_suites在命令行展开,同样会被跳过。换句话说,命令行上的test_suite目标表现得像:all和...。使用--noexpand_test_suites可阻止展开,此时包含不兼容测试的test_suite目标自身也会变成不兼容。(注:test_suite定义见 general 参考文档。)
如果在命令行显式指定一个不兼容目标,构建会直接报错并失败:
$ bazel build --platforms=//:myplatform //:target_incompatible_with_myplatform ... ERROR: Target //:target_incompatible_with_myplatform is incompatible and cannot be built, but was explicitly requested. ... FAILED: Build did NOT complete successfully若启用--skip_incompatible_explicit_targets,则显式指定的不兼容目标会被静默跳过而不是报错。
更富表达力的约束
@platforms还提供了@platforms//:incompatible这一特殊的constraint_value,任何平台都不会满足它。
把 select() 与@platforms//:incompatible结合,可以表达更复杂的限制,例如实现基础的 OR 逻辑。下面这个例子把库标记为仅兼容 macOS 与 Linux:
cc_library( name = "unixish_lib", srcs = ["unixish_lib.cc"], target_compatible_with = select({ "@platforms//os:osx": [], "@platforms//os:linux": [], "//conditions:default": ["@platforms//:incompatible"], }), )其语义可解读为:
- 目标为 macOS 时,该目标无约束;
- 目标为 Linux 时,该目标无约束;
- 其他情况下,目标带有
@platforms//:incompatible约束;由于该约束不属于任何平台,目标被判定为不兼容。
注意:空的约束列表等价于"与一切兼容"。
反向(排除式)兼容也可以类似表达。下面的例子描述一个除了 ARM 之外与所有平台兼容的库:
cc_library( name = "non_arm_lib", srcs = ["non_arm_lib.cc"], target_compatible_with = select({ "@platforms//cpu:arm": ["@platforms//:incompatible"], "//conditions:default": [], }), )为提升约束表达的可读性,可使用 skylib(bazel-skylib仓库)提供的selects.with_or()辅助函数来声明更直观的"或"逻辑。仓库集成测试target_compatible_with_test.sh中也有利用selects.config_setting_group(match_any = ...)组合config_setting的先例,可参考其写法。
值得一提的实现细节:target_compatible_with中不仅可以放约束值,也可以直接放config_setting标签。IncompatiblePlatformProvider会区分三种不兼容成因并分别记录:
- 约束未满足:
constraintsResponsibleForIncompatibility记录目标平台未满足的约束列表(按标签字符串排序、去重); - config_setting 未匹配:
configSettingsResponsibleForIncompatibility记录未匹配的config_setting标签; - 依赖不兼容:
targetsResponsibleForIncompatibility记录导致不兼容的传递依赖。
具体字段定义见 IncompatiblePlatformProvider.java,其构造逻辑保证排序与去重在该 provider 内一次性完成,方便下游直接消费。
用 bazel cquery 检测不兼容目标
可以在bazel cquery的 Starlark 输出格式中使用IncompatiblePlatformProvider来区分不兼容与兼容目标。
利用它可以过滤掉不兼容目标。下面的例子只打印兼容目标的标签,不兼容目标不打印:
$ cat example.cquery def format(target): if "IncompatiblePlatformProvider" not in providers(target): return target.label return "" $ bazel cquery //... --output=starlark --starlark:file=example.cquery在 Starlark 中IncompatiblePlatformProvider这个名字来自IncompatiblePlatformProvider.java中定义的STARLARK_NAME = "IncompatiblePlatformProvider"(源码),因此providers(target)的键与之一一对应。
已知问题
不兼容目标会忽略可见性限制(即不受 visibility 约束,对应 bazelbuild/bazel 的 issue #16044)。这意味着在排查"某目标为何仍被纳入构建"时,不能依赖可见性配置来兜底,而应显式使用target_compatible_with声明兼容性。
结合源码的完整工作流小结
- 在 MODULE 中确保
@platforms可用,优先复用其//os、//cpu下的标准约束; - 用
constraint_setting+constraint_value定义组织特有的属性(如 GPU 型号、编译器版本),参考 target_compatible_with_test.sh 中foo_version/bar_version的写法; - 用
platform规则把这些约束组合成具体机器(可用parents继承宿主平台以减少重复),参考Platform.java支持的全部属性; - 在命令行用
--platforms(必要时配合--host_platform,见 PlatformOptions.java)选择目标机器; - 为平台相关代码声明
target_compatible_with,配合@platforms//:incompatible与select()表达精确的兼容集合; - 用
bazel cquery --output=starlark结合IncompatiblePlatformProvider验证哪些目标被判定为不兼容。
这套机制同时是工具链解析(toolchains)与远程执行(Execution 平台选择)的基石:平台决定"在哪构建、为谁构建",工具链决定"用什么编译",二者共同支撑起 Bazel 在异构环境下的可扩展构建能力。
延伸阅读
- 工具链(Toolchains):平台如何参与工具链解析
- 可配置属性(Configurable Attributes):基于约束的
select()用法 - platforms-and-toolchains 构建规则参考:
constraint_setting/constraint_value/platform完整参数 - IncompatiblePlatformProvider 参考:cquery 检测所用的 provider
- cquery 指南:Starlark 输出格式与查询技巧
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考