可见性

报告问题 查看源代码 每夜 build · 8.0 7.4 . 7.3 · 7.2 · 7.1 · 7.0 · 6.5

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

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

目标公开范围

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

通常,如果目标 A 与目标 B 位于同一位置,或者 AB 的位置授予了可见性,则 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 声明。

最佳实践:请尽可能公开规则目标,而不是源文件。例如,请勿对 .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 用于为这些目标启用可见性检查。为了协助迁移,它还会导致任何未指定 visibilityconfig_setting 都被视为公开(无论软件包级 default_visibility 如何)。

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

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

这意味着,代码在同一软件包中定义的所有宏都会自动成为彼此的“好友”。无论宏是在哪个软件包中实际实例化的,都可以通过在 //lib 中定义的任何其他宏看到由 //lib:defs.bzl 中定义的宏直接创建的任何目标。同样,它们可以看到并被 //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 检查依赖项的可见性。

只要目标或宏声明位于另一个符号宏的内部,并且该符号宏在其某个标签类型的属性中采用了依赖项的标签,此过程便可递归重复。

加载可见性

加载可见性用于控制是否可以从当前软件包之外的其他 BUILD.bzl 文件加载 .bzl 文件。

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

请注意,.bzl 文件可能有也可能没有相应的源文件目标。如果发生这种情况,则无法保证加载可见性与目标可见性一致。也就是说,同一 BUILD 文件或许能够加载 .bzl 文件,但无法在 filegroupsrcs 中列出该文件,反之亦然。这有时会导致希望将 .bzl 文件用作源代码的规则出现问题,例如在生成文档或进行测试时。

如需进行原型设计,您可以通过设置 --check_bzl_visibility=false 来停用加载可见性强制执行。与 --check_visibility=false 一样,不应对已提交的代码执行此操作。

从 Bazel 6.0 开始,可见加载情况。

声明加载可见性

如需设置 .bzl 文件的加载可见性,请从文件内调用 visibility() 函数。visibility() 的参数是软件包规范的列表,就像 package_grouppackages 属性一样。但是,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

有一个 Buildifier lint,如果用户从名为 internalprivate 的目录加载文件,而用户的文件本身不在该目录的父目录下,则会发出警告。此 lint 早于加载可见性功能,在 .bzl 文件声明可见性的工作区中不需要使用。