可见性

本页介绍了 Bazel 的两个可见性系统: 目标可见性加载可见性

这两种类型的可见性都有助于其他开发者区分库的 公共 API 和实现细节,并有助于在工作区不断扩展时强制执行结构 。您还可以在弃用公共 API 时使用可见性,以允许当前用户,同时拒绝新用户。

目标可见性

目标可见性 用于控制谁可以依赖于您的目标,也就是说,谁可以 在 deps 等属性中使用目标的标签。如果目标违反了其某个依赖项的可见性,则在分析阶段构建该目标时会失败。

一般来说,如果目标 A 和目标 B 位于同一 位置,或者 A 授予 B'的位置可见性,则 AB 可见。如果没有 符号宏,则术语“位置”可以简化 为“软件包”;如需详细了解符号宏,请参阅下文

可见性通过列出允许的软件包来指定。允许某个软件包并不一定 意味着其子软件包也允许。如需详细了解 软件包和子软件包,请参阅概念和术语

对于原型设计,您可以通过设置 标志 --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:mytargetvisibility 设置为 [":__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 声明。

最佳实践: 如果可能,请优先公开规则目标,而不是 源文件。例如,不要对 exports_files 文件调用 .java,而是将该文件封装在非私享 java_library 目标中。一般来说,规则目标 应仅直接引用位于同一软件包中的源文件。

示例

文件 //frobber/data/BUILD

exports_files(["readme.txt"])

文件 //frobber/bin/BUILD

cc_binary(
  name = "my-program",
  data = ["//frobber/data:readme.txt"],
)

配置设置可见性

过去,Bazel 不会强制执行 config_setting目标的可见性,这些目标 在 select()的键中引用。有两个标志可用于移除此旧版行为:

  • --incompatible_enforce_config_setting_visibility 可为此类目标启用可见性检查。为了帮助迁移,它 还会导致任何未指定 visibilityconfig_setting 被 视为公开(无论软件包级 default_visibility 如何)。

  • --incompatible_config_setting_private_default_visibility 会导致未指定 config_settingvisibility 遵循 软件包的 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 文件的软件包。 但是,对于在符号宏中创建的目标,位置是包含宏定义( my_macro = macro(...) 语句)的 .bzl 文件的软件包。当目标在 多个嵌套目标内创建时,始终使用最内层符号宏的定义 。

系统使用相同的方式来确定要针对给定 依赖项的可见性检查哪个位置。如果使用目标是在宏内创建的,我们会 查看最内层宏的定义,而不是使用 目标所在的软件包。

这意味着,在同一软件包中定义代码的所有宏都会 自动成为彼此的“朋友”。由 //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.bzl 文件加载 .bzl 文件。

与目标可见性保护由目标封装的源代码的方式相同,加载可见性保护由 .bzl 文件封装的构建逻辑。例如,BUILD 文件作者可能希望将一些重复的 目标声明分解为 .bzl 文件中的宏。如果没有 加载可见性的保护,他们可能会发现自己的宏被 同一工作区中的其他协作者重复使用,因此修改宏会破坏其他团队的构建。

请注意,.bzl 文件可能具有也可能没有相应的源文件目标。 如果有,则无法保证加载可见性和目标 可见性一致。也就是说,同一个 BUILD 文件可能能够加载 .bzl 文件,但不会将其列在 srcsfilegroup 中, 反之亦然。这有时可能会给希望将 .bzl文件用作源代码的规则带来问题,例如用于文档生成或测试。

对于原型设计,您可以通过设置 --check_bzl_visibility=false来停用加载可见性强制执行。与 --check_visibility=false 一样,对于提交的代码,不应这样做。

Bazel 6.0 及更高版本提供加载可见性。

声明加载可见性

如需设置 .bzl 文件的加载可见性,请从该文件中调用 visibility() 函数。 visibility() 的实参是软件包规范列表,就像 packagespackage_group 属性一样。但是,visibility() 不接受负软件包 规范。

visibility() 的调用必须仅在每个文件的顶层(而不是 在函数内)发生一次,并且最好紧跟在 load() 语句之后。

与目标可见性不同,默认加载可见性始终是公开的。Files 不调用 visibility() 的文件始终可以从工作区中的任何位置加载。最好将 visibility("private") 添加到任何 新的 .bzl 文件的顶部,这些文件并非专门用于在软件包外部使用。

示例

# //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 文件可能需要对由多个较小的许可名单组成的许可名单可见。这类似于 a 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

如果用户从名为 internalprivate 的目录加载文件,而用户的文件本身不在该 目录的父级下,则 Buildifier lint 会提供警告。此 lint 早于加载可见性功能,并且在 声明可见性的 .bzl 文件的工作区中是不必要的。