本页介绍了 Bazel 的三种可见性系统:目标可见性、传递可见性和加载可见性。
这些类型的可见性有助于其他开发者区分库的公共 API 及其实现细节,并有助于在工作区扩大时强制执行结构。您还可以在弃用公共 API 时使用公开范围,以允许当前用户使用,同时拒绝新用户。
目标可见度
目标可见性用于控制哪些人可以依赖您的目标,即哪些人可以在 deps 等属性中使用目标的标签。如果目标违反了其某个依赖项的公开范围,则在分析阶段将无法构建该目标。
通常,如果目标 和目标 位于同一
位置,或者 A 向 B 的位置授予了可见性,则 A 对 B 可见。在没有符号宏的情况下,“位置”一词可以简化为“软件包”;如需详细了解符号宏,请参阅下文。
可见性通过列出允许的软件包来指定。允许某个软件包并不一定意味着也允许其子软件包。如需详细了解软件包和子软件包,请参阅概念和术语。
对于原型设计,您可以通过设置标志 --check_visibility=false 来停用目标可见性强制执行。在提交的代码中,不应针对生产环境执行此操作。
控制可见性的主要方法是使用规则的 visibility 属性。以下各小节介绍了该属性的格式、如何将其应用于各种目标,以及可见性系统与符号宏之间的交互。
可见性规范
所有规则目标都有一个 visibility 属性,该属性接受标签列表。每个标签都具有以下形式之一。除了最后一种形式之外,这些只是语法占位符,不对应于任何实际目标。
"//visibility:public":授予对所有软件包的访问权限。"//visibility:private":不授予任何额外访问权限;只有此位置的软件包中的目标可以使用此目标。"//foo/bar:__pkg__":授予对//foo/bar(但不包括其子软件包)的访问权限。"//foo/bar:__subpackages__":授予对//foo/bar及其所有直接和间接子软件包的访问权限。"//some_pkg:my_package_group":授予对属于给定package_group的所有软件包的访问权限。- 软件包组使用不同的语法来指定软件包。在软件包组中,
"//foo/bar:__pkg__"和"//foo/bar:__subpackages__"形式分别替换为"//foo/bar"和"//foo/bar/..."。同样,"//visibility:public"和"//visibility:private"只是"public"和"private"。
- 软件包组使用不同的语法来指定软件包。在软件包组中,
例如,如果 //some/package:mytarget 的 visibility 设置为 [":__subpackages__", "//tests:__pkg__"],则 //some/package/... 源代码树中的任何目标以及 //tests/BUILD 中声明的目标都可以使用它,但 //tests/integration/BUILD 中定义的目标不能使用它。
最佳实践:如需让多个目标对同一组软件包可见,请使用 package_group,而不是在每个目标的 visibility 属性中重复该列表。这可以提高可读性,并防止列表不同步。
最佳实践:向其他团队的项目授予可见性时,最好使用 __subpackages__ 而不是 __pkg__,以避免随着该项目的发展和新子软件包的添加而产生不必要的可见性变动。
规则目标可见性
规则目标的可见性由其 visibility 属性(如果未指定,则为合适的默认值)决定,并附加声明目标的位置。对于未在符号宏中声明的目标,如果软件包指定了 default_visibility,则使用此默认值;对于所有其他软件包以及在符号宏中声明的目标,默认值为 ["//visibility:private"]。
# //mypkg/BUILD
package(default_visibility = ["//friend:__pkg__"])
cc_library(
name = "t1",
...
# No visibility explicitly specified.
# Effective visibility is ["//friend:__pkg__", "//mypkg:__pkg__"].
# If no default_visibility were given in package(...), the visibility would
# instead default to ["//visibility:private"], and the effective visibility
# would be ["//mypkg:__pkg__"].
)
cc_library(
name = "t2",
...
visibility = [":clients"],
# Effective visibility is ["//mypkg:clients, "//mypkg:__pkg__"], which will
# expand to ["//another_friend:__subpackages__", "//mypkg:__pkg__"].
)
cc_library(
name = "t3",
...
visibility = ["//visibility:private"],
# Effective visibility is ["//mypkg:__pkg__"]
)
package_group(
name = "clients",
packages = ["//another_friend/..."],
)
最佳实践:避免将 default_visibility 设置为公开。这可能便于进行原型设计或在小型代码库中使用,但随着代码库的增长,无意中创建公共目标的风险也会增加。最好明确说明哪些目标是软件包公共接口的一部分。
生成的文件目标可见性
生成的文件目标与生成它的规则目标具有相同的公开范围。
# //mypkg/BUILD
java_binary(
name = "foo",
...
visibility = ["//friend:__pkg__"],
)
# //friend/BUILD
some_rule(
name = "bar",
deps = [
# Allowed directly by visibility of foo.
"//mypkg:foo",
# Also allowed. The java_binary's "_deploy.jar" implicit output file
# target the same visibility as the rule target itself.
"//mypkg:foo_deploy.jar",
]
...
)
源文件目标公开范围
源文件目标可以使用 exports_files 显式声明,也可以通过在规则的标签属性(符号宏之外)中引用其文件名来隐式创建。与规则目标一样,对 exports_files 的调用位置或引用输入文件的 BUILD 文件始终会自动附加到文件的公开范围。
由 exports_files 声明的文件可以通过该函数的 visibility 参数设置其公开范围。如果未提供此参数,则公开范围为公开。
对于未出现在对 exports_files 的调用中的文件,其可见性取决于标志 --incompatible_no_implicit_file_export 的值:
如果该标志为 true,则公开范围为“私享”。
否则,系统会应用旧版行为:公开范围与
BUILD文件的default_visibility相同;如果未指定默认公开范围,则为私有。
避免依赖旧版行为。只要源文件目标需要非私有可见性,就始终编写 exports_files 声明。
最佳实践:尽可能公开规则目标,而不是源文件。例如,不要对 .java 文件调用 exports_files,而是将该文件封装在非私有 java_library 目标中。通常,规则目标应仅直接引用位于同一软件包中的源文件。
示例
文件 //frobber/data/BUILD:
exports_files(["readme.txt"])
文件 //frobber/bin/BUILD:
cc_binary(
name = "my-program",
data = ["//frobber/data:readme.txt"],
)
配置设置公开范围
过去,Bazel 并未对 select() 的键中引用的 config_setting 目标强制执行可见性。有两个标志可用于移除此旧版行为:
--incompatible_enforce_config_setting_visibility可为这些目标启用可见性检查。为了协助迁移,它还会导致任何未指定visibility的config_setting被视为公开(无论软件包级default_visibility如何)。--incompatible_config_setting_private_default_visibility会导致未指定visibility的config_setting遵循软件包的default_visibility并回退到不公开可见性,就像任何其他规则目标一样。如果未设置--incompatible_enforce_config_setting_visibility,则此方法不执行任何操作。
避免依赖旧版行为。如果软件包尚未指定合适的 default_visibility,则任何要在当前软件包之外使用的 config_setting 都应具有显式 visibility。
软件包组目标可见性
package_group 目标没有 visibility 属性。这些信息始终公开可见。
隐式依赖项的可见性
某些规则具有隐式依赖项,即未在 BUILD 文件中明确说明但该规则的每个实例都固有的依赖项。例如,cc_library 规则可能会创建从其每个规则目标到表示 C++ 编译器的可执行目标的隐式依赖项。
系统会根据包含定义了规则(或切面)的 .bzl 文件的软件包,检查此类隐式依赖项的公开范围。例如,只要 C++ 编译器与 cc_library 规则的定义位于同一软件包中,它就可以是私有的。作为后备,如果隐式依赖项在定义中不可见,则会根据 cc_library 目标进行检查。
如果您想将规则的使用范围限制为特定软件包,请改用加载可见性。
可见性和符号宏
本部分介绍了可见性系统如何与符号宏交互。
符号宏中的位置
可见性系统的一个关键细节是我们如何确定声明的位置。对于未在符号宏中声明的目标,其位置就是目标所在的软件包,即 BUILD 文件的软件包。
但对于在符号宏中创建的目标,其位置是包含 .bzl 文件的软件包,其中宏的定义(my_macro = macro(...) 语句)会显示在该文件中。在多个嵌套目标内创建目标时,始终使用最内层的符号宏定义。
系统会使用同一系统来确定要根据给定依赖项的可见性检查哪个位置。如果使用目标是在宏内创建的,我们会查看最内层宏的定义,而不是使用目标所在的软件包。
这意味着,代码在同一软件包中定义的所有宏都会自动成为彼此的“友元”。由 //lib:defs.bzl 中定义的宏直接创建的任何目标都可以从 //lib 中定义的任何其他宏中看到,无论这些宏实际在哪些软件包中实例化。同样,它们可以查看直接在 //lib/BUILD 及其旧版宏中声明的目标,也可以被这些目标查看。相反,如果至少有一个目标是由符号宏创建的,则位于同一软件包中的目标不一定能看到彼此。
在符号宏的实现函数中,visibility 参数具有宏的 visibility 属性的有效值,后跟宏的调用位置。宏将其某个目标导出到其调用方的标准方法是将此值转发到目标的声明,如 some_rule(..., visibility = visibility) 中所示。除非调用方恰好与宏定义位于同一软件包中,否则省略此属性的目标对宏的调用方不可见。此行为会进行组合,即对子宏的一系列嵌套调用可能会分别传递 visibility = visibility,从而在每个级别将内部宏的导出目标重新导出到调用方,而不会公开任何宏的实现细节。
将权限委托给子宏
可见性模型具有一项特殊功能,允许宏将其权限委托给子宏。这对于重构和组合宏非常重要。
假设您有一个宏 my_macro,它使用另一个软件包中的规则 some_library 创建依赖项边缘:
# //macro/defs.bzl
load("//lib:defs.bzl", "some_library")
def _impl(name, visibility, ...):
...
native.genrule(
name = name + "_dependency"
...
)
some_library(
name = name + "_consumer",
deps = [name + "_dependency"],
...
)
my_macro = macro(implementation = _impl, ...)
# //pkg/BUILD
load("//macro:defs.bzl", "my_macro")
my_macro(name = "foo", ...)
//pkg:foo_dependency 目标未指定 visibility,因此它仅在 //macro 中可见,这对于使用目标来说可以正常运行。现在,如果 //lib 的作者重构 some_library,改为使用宏来实现,会发生什么情况?
# //lib:defs.bzl
def _impl(name, visibility, deps, ...):
some_rule(
# Main target, exported.
name = name,
visibility = visibility,
deps = deps,
...)
some_library = macro(implementation = _impl, ...)
进行此更改后,//pkg:foo_consumer 的位置现在是 //lib,而不是 //macro,因此它对 //pkg:foo_dependency 的使用违反了依赖项的可见性。不能指望 my_macro 的作者将 visibility = ["//lib"] 传递给依赖项的声明,只是为了解决此实现细节。
因此,当目标的依赖项也是声明该目标的宏的属性值时,我们会根据宏的位置(而非使用目标的位置)检查依赖项的公开范围。
在此示例中,为了验证 //pkg:foo_consumer 是否可以看到 //pkg:foo_dependency,我们发现 //pkg:foo_dependency 也作为输入传递给 my_macro 内部对 some_library 的调用,并改为根据此调用的位置 //macro 检查依赖项的可见性。
只要目标或宏声明位于另一个符号宏内,并且该符号宏在其某个标签类型属性中采用依赖项的标签,此过程就可以递归重复。
终结器
在规则终结器(带有 finalizer = True 的符号宏)中声明的目标,除了能看到遵循常规符号宏可见性规则的目标外,还能看到对终结器目标的软件包可见的所有目标。
换句话说,如果您将基于 native.existing_rules() 的旧版宏迁移到终结器,则终结器声明的目标仍能看到其旧依赖项。
您可以定义一个终结器可以使用 native.existing_rules() 进行内省的目标,但该目标不能在可见性系统下用作依赖项。例如,如果宏定义的目标对其自己的软件包或终结器宏的定义不可见,并且未委托给终结器,则终结器无法看到此类目标。但请注意,基于 native.existing_rules() 的旧版宏也无法看到此类目标。
传递可见性
传递可见性是一种限制哪些人可以依赖目标的方法,包括依赖项只是间接依赖项的情况。它与普通目标可见性系统分开应用,因此即使依赖项链中的每个链接都有效,也可能会发生传递可见性错误。这样,目标的作者就可以与一组受信任的客户端目标共享该目标,同时防止其无意中泄露给 build 的其余部分。
传递可见性是在软件包级别声明的,使用 [package][package] 函数的 transitive_visibility 参数。此声明适用于软件包中的所有目标。该参数接受 package_group 的单个标签,用于指定其目标可能传递依赖于此软件包的目标的软件包。
# //third_party/sensitive_dep/BUILD
package(
# This must be explicitly listed
transitive_visibility = ":sensitive_dep_users",
)
package_group(
name = "sensitive_dep_users",
packages = [
"//third_party/sensitive_dep/...", # has to include itself
"//allowed",
],
)
sh_library(
name = "sensitive_dep"
)
# //not_allowed/BUILD
sh_binary(
name = "bad",
deps = ["//third_party/sensitive_dep:sensitive_dep"]
)
构建 //not_allowed:bad(依赖于 //third_party/sensitive_dep:sensitive_dep,但不是其 transitive_visibility 的一部分)会导致以下错误:
Transitive visibility error: //third_party/sensitive_dep:sensitive_dep is not transitively visible from ///not_allowed:bad. //"//third_party/sensitive_dep:sensitive_dep inherits a transitive_visibility declaration from its package or one of its dependencies that does not allow //not_allowed:bad.
transitive_visibility 中使用的软件包组不得include其他软件包组。
请注意,如果某个目标具有具有传递可见性限制的传递依赖项,则该目标本身会继承该限制,并将其传递给依赖于它的目标。如果一个目标具有多个具有传递可见性限制的传递依赖项,则它必须满足所有限制。
传递可见性也适用于在符号宏中声明的目标。不过,与普通目标可见性不同,它仅考虑这些目标所在的软件包,而不考虑定义符号宏的软件包。
加载可见性
加载可见性控制是否可以从当前软件包之外的其他 BUILD 或 .bzl 文件加载 .bzl 文件。
正如目标可见性可保护由目标封装的源代码一样,加载可见性可保护由 .bzl 文件封装的构建逻辑。例如,BUILD 文件作者可能希望将一些重复的目标声明重构为 .bzl 文件中的宏。如果没有加载可见性的保护,他们可能会发现自己的宏被同一工作区中的其他协作者重复使用,因此修改宏会破坏其他团队的 build。
请注意,.bzl 文件可能有也可能没有对应的源文件目标。
如果这样做,则无法保证加载可见性和目标可见性一致。也就是说,同一个 BUILD 文件可能能够加载 .bzl 文件,但无法在 filegroup 的 srcs 中列出该文件,反之亦然。这有时可能会导致希望将 .bzl 文件用作源代码的规则(例如用于生成文档或测试)出现问题。
对于原型设计,您可以通过设置 --check_bzl_visibility=false 来停用加载可见性强制执行。与 --check_visibility=false 一样,不应对已提交的代码执行此操作。
从 Bazel 6.0 开始,加载可见性可用。
声明加载可见性
如需设置 .bzl 文件的加载可见性,请从该文件中调用 visibility() 函数。visibility() 的参数是软件包规范列表,就像 package_group 的 packages 属性一样。但是,visibility() 不接受负软件包规范。
对 visibility() 的调用在每个文件中只能发生一次,且必须在顶层(不在函数内),最好紧跟在 load() 语句之后。
与目标公开范围不同,默认加载公开范围始终为公开。不调用 visibility() 的文件始终可以从工作区中的任何位置加载。最好在任何不专门用于软件包外部的新 .bzl 文件的顶部添加 visibility("private")。
示例
# //mylib/internal_defs.bzl
# Available to subpackages and to mylib's tests.
visibility(["//mylib/...", "//tests/mylib/..."])
def helper(...):
...
# //mylib/rules.bzl
load(":internal_defs.bzl", "helper")
# Set visibility explicitly, even though public is the default.
# Note the [] can be omitted when there's only one entry.
visibility("public")
myrule = rule(
...
)
# //someclient/BUILD
load("//mylib:rules.bzl", "myrule") # ok
load("//mylib:internal_defs.bzl", "helper") # error
...
加载可见性做法
本部分介绍了管理负载可见性声明的提示。
考虑可见性
如果多个 .bzl 文件应具有相同的公开范围,则可以将其软件包规范纳入一个通用列表中。例如:
# //mylib/internal_defs.bzl
visibility("private")
clients = [
"//foo",
"//bar/baz/...",
...
]
# //mylib/feature_A.bzl
load(":internal_defs.bzl", "clients")
visibility(clients)
...
# //mylib/feature_B.bzl
load(":internal_defs.bzl", "clients")
visibility(clients)
...
这有助于防止各种 .bzl 文件的可见性之间发生意外偏差。当 clients 列表很大时,这种方式也更易于阅读。
组合公开范围
有时,.bzl 文件可能需要对由多个较小的许可清单组成的许可清单可见。这类似于 package_group 如何通过其 includes 属性整合其他 package_group。
假设您要弃用一个广泛使用的宏。您希望它仅对现有用户和您自己的团队拥有的软件包可见。您可以这样写:
# //mylib/macros.bzl
load(":internal_defs.bzl", "our_packages")
load("//some_big_client:defs.bzl", "their_remaining_uses")
# List concatenation. Duplicates are fine.
visibility(our_packages + their_remaining_uses)
使用软件包组进行去重
与目标可见性不同,您无法根据 package_group 定义加载可见性。如果您想为目标可见性和加载可见性重复使用同一许可清单,最好将软件包规范列表移至 .bzl 文件,以便这两种声明都可以引用该文件。以上文考虑可见性中的示例为例,您可以编写以下代码:
# //mylib/BUILD
load(":internal_defs", "clients")
package_group(
name = "my_pkg_grp",
packages = clients,
)
仅当列表中不包含任何否定软件包规范时,此功能才有效。
保护单个符号
任何名称以下划线开头的 Starlark 符号都无法从其他文件加载。这样可以轻松创建私有符号,但无法与一组有限的可信文件共享这些符号。另一方面,加载可见性可让您控制哪些其他软件包可以查看您的 .bzl file,但不允许您阻止加载任何不带下划线的符号。
幸运的是,您可以结合使用这两项功能来实现精细控制。
# //mylib/internal_defs.bzl
# Can't be public, because internal_helper shouldn't be exposed to the world.
visibility("private")
# Can't be underscore-prefixed, because this is
# needed by other .bzl files in mylib.
def internal_helper(...):
...
def public_util(...):
...
# //mylib/defs.bzl
load(":internal_defs", "internal_helper", _public_util="public_util")
visibility("public")
# internal_helper, as a loaded symbol, is available for use in this file but
# can't be imported by clients who load this file.
...
# Re-export public_util from this file by assigning it to a global variable.
# We needed to import it under a different name ("_public_util") in order for
# this assignment to be legal.
public_util = _public_util
bzl-visibility Buildifier lint
如果用户从名为 internal 或 private 的目录加载文件,而用户的文件本身不在该目录的父级目录下,则 Buildifier lint 会发出警告。此 lint 早于加载可见性功能,在 .bzl 文件声明可见性的工作区中是不必要的。