查询快速入门

本教程介绍了如何使用 Bazel 通过预制 Bazel 项目跟踪代码中的依赖项。

如需详细了解语言和 --output 标志,请参阅 Bazel 查询参考Bazel cquery 参考 手册。如需在 IDE 中获得帮助,请在命令行中输入 bazel help querybazel help cquery

目标

本指南将引导您完成一系列基本查询,您可以使用这些查询详细了解项目的文件依赖项。本指南面向对 Bazel 和 BUILD 文件的工作原理有基本了解的新 Bazel 开发者。

前提条件

首先,安装 Bazel(如果尚未安装)。本教程使用 Git 进行源代码控制,因此为了获得最佳效果,请同时安装 Git

如需直观呈现依赖关系图,请使用名为 Graphviz 的工具,您可以下载该工具以便跟着操作。

获取示例项目

接下来,通过在您选择的命令行工具中运行以下命令,从 Bazel 的示例代码库中检索示例应用:

git clone https://github.com/bazelbuild/examples.git

本教程的示例项目位于 examples/query-quickstart 目录中。

使用入门

什么是 Bazel 查询?

查询有助于您了解 Bazel 代码库,方法是分析 BUILD 文件之间的关系并检查生成的输出以获取实用信息。本指南预览了一些基本查询函数,但如需了解更多选项,请参阅查询指南。查询有助于您了解大型项目中的依赖项,而无需手动浏览 BUILD 文件。

如需运行查询,请打开命令行终端并输入:

bazel query 'query_function'

场景

假设一个场景,深入探讨 Bazel 咖啡馆及其各自的厨师之间的关系。这家咖啡馆只出售披萨和芝士通心粉。请查看以下项目结构:

bazelqueryguide
├── BUILD
├── src
│   └── main
│       └── java
│           └── com
│               └── example
│                   ├── customers
│                   │   ├── Jenny.java
│                   │   ├── Amir.java
│                   │   └── BUILD
│                   ├── dishes
│                   │   ├── Pizza.java
│                   │   ├── MacAndCheese.java
│                   │   └── BUILD
│                   ├── ingredients
│                   │   ├── Cheese.java
│                   │   ├── Tomatoes.java
│                   │   ├── Dough.java
│                   │   ├── Macaroni.java
│                   │   └── BUILD
│                   ├── restaurant
│                   │   ├── Cafe.java
│                   │   ├── Chef.java
│                   │   └── BUILD
│                   ├── reviews
│                   │   ├── Review.java
│                   │   └── BUILD
│                   └── Runner.java
└── MODULE.bazel

在本教程中,除非另有说明,否则请尽量不要查看 BUILD 文件来查找所需信息,而应仅使用查询函数。

一个项目由构成咖啡馆的不同软件包组成。它们分为:restaurantingredientsdishescustomersreviews。这些软件包中的规则定义了咖啡馆的不同组件,并包含各种标记和依赖项。

运行构建

此项目在 Runner.java 中包含一个主方法,您可以执行 来输出咖啡馆的菜单。使用 Bazel 构建项目,命令为 bazel build,并使用 : 表示目标名为 runner。如需了解如何 引用目标,请参阅 目标名称

如需构建此项目,请将此命令粘贴到终端中:

bazel build :runner

如果构建成功,输出应如下所示。

INFO: Analyzed target //:runner (49 packages loaded, 784 targets configured).
INFO: Found 1 target...
Target //:runner up-to-date:
  bazel-bin/runner.jar
  bazel-bin/runner
INFO: Elapsed time: 16.593s, Critical Path: 4.32s
INFO: 23 processes: 4 internal, 10 darwin-sandbox, 9 worker.
INFO: Build completed successfully, 23 total actions

成功构建后,通过粘贴以下命令运行应用:

bazel-bin/runner
--------------------- MENU -------------------------

Pizza - Cheesy Delicious Goodness
Macaroni & Cheese - Kid-approved Dinner

----------------------------------------------------

这样,您将获得一个菜单项列表,其中包含简短说明。

探索目标

该项目会在各自的软件包中列出配料和菜品。如需使用查询查看软件包的规则,请运行命令 bazel query package/…

在这种情况下,您可以使用以下命令查看此咖啡馆提供的配料和菜品:

bazel query //src/main/java/com/example/dishes/...
bazel query //src/main/java/com/example/ingredients/...

如果您查询配料软件包的目标,输出应如下所示:

//src/main/java/com/example/ingredients:cheese
//src/main/java/com/example/ingredients:dough
//src/main/java/com/example/ingredients:macaroni
//src/main/java/com/example/ingredients:tomato

查找依赖项

您的 runner 依赖哪些目标来运行?

假设您想深入了解项目的结构,而不必深入文件系统(这对于大型项目来说可能不可行)。Bazel 咖啡馆使用哪些规则?

如果您的 runner 的目标是 runner(如本示例中所示),请运行以下命令来发现目标的基础依赖项:

bazel query --noimplicit_deps "deps(target)"
bazel query --noimplicit_deps "deps(:runner)"
//:runner
//:src/main/java/com/example/Runner.java
//src/main/java/com/example/dishes:MacAndCheese.java
//src/main/java/com/example/dishes:Pizza.java
//src/main/java/com/example/dishes:macAndCheese
//src/main/java/com/example/dishes:pizza
//src/main/java/com/example/ingredients:Cheese.java
//src/main/java/com/example/ingredients:Dough.java
//src/main/java/com/example/ingredients:Macaroni.java
//src/main/java/com/example/ingredients:Tomato.java
//src/main/java/com/example/ingredients:cheese
//src/main/java/com/example/ingredients:dough
//src/main/java/com/example/ingredients:macaroni
//src/main/java/com/example/ingredients:tomato
//src/main/java/com/example/restaurant:Cafe.java
//src/main/java/com/example/restaurant:Chef.java
//src/main/java/com/example/restaurant:cafe
//src/main/java/com/example/restaurant:chef

在大多数情况下,请使用查询函数 deps() 查看特定目标的各个输出依赖项。

直观呈现依赖关系图(可选)

本部分介绍了如何直观呈现特定查询的依赖项路径。Graphviz 有助于将路径显示为有向无环图,而不是扁平列表。您可以使用各种 --output 命令行选项来更改 Bazel 查询图的显示方式。如需了解相关选项,请参阅输出格式

首先,运行所需的查询,并添加标志 --noimplicit_deps 以移除过多的工具依赖项。然后,在查询后添加输出标志,并将图存储到名为 graph.in 的文件中,以创建图的文本表示形式。

如需搜索目标 :runner 的所有依赖项并将输出格式设置为图,请运行以下命令:

bazel query --noimplicit_deps 'deps(:runner)' --output graph > graph.in

这会创建一个名为 graph.in 的文件,它是构建图的文本表示形式。Graphviz 使用 dot (一种将文本处理为可视化的工具)来创建 png:

dot -Tpng < graph.in > graph.png

如果您打开 graph.png,您应该会看到类似如下的内容。为了在本指南中更清楚地显示基本路径详细信息,我们对下图进行了简化。

图表显示了从咖啡馆到厨师再到菜肴(披萨和通心粉奶酪)的关系,其中菜肴又分为单独的食材:奶酪、番茄、面团和通心粉。

如果您想查看本指南中不同查询函数的输出,这会有所帮助。

查找反向依赖项

如果您有一个目标,并且想分析其他哪些目标使用该目标,则可以使用查询来检查哪些目标依赖于特定规则。这称为“反向依赖项”。如果您要编辑自己不熟悉的代码库中的文件,使用 rdeps() 会很有用,并且可以避免您在不知情的情况下破坏依赖于该文件的其他文件。

例如,您想对配料 cheese 进行一些修改。为避免给 Bazel 咖啡馆带来问题,您需要检查哪些菜品依赖于 cheese

如需查看哪些目标依赖于特定目标/软件包,您可以使用 rdeps(universe_scope, target)rdeps() 查询函数至少需要两个实参:universe_scope(相关目录)和 target。Bazel 会在提供的 universe_scope 中搜索目标的反向依赖项。rdeps() 运算符接受可选的第三个实参:一个整数字面量,用于指定搜索深度的上限。

如需在整个项目“//…”的范围内查找目标 cheese 的反向依赖项,请运行以下命令:

bazel query "rdeps(universe_scope, target)"
ex) bazel query "rdeps(//... , //src/main/java/com/example/ingredients:cheese)"
//:runner
//src/main/java/com/example/dishes:macAndCheese
//src/main/java/com/example/dishes:pizza
//src/main/java/com/example/ingredients:cheese
//src/main/java/com/example/restaurant:cafe
//src/main/java/com/example/restaurant:chef

查询返回结果显示,披萨和芝士通心粉都依赖于奶酪。真是令人惊讶!

根据标记查找目标

两位顾客走进 Bazel 咖啡馆:Amir 和 Jenny。除了他们的名字之外,我们对他们一无所知。幸运的是,他们在“customers”BUILD 文件中标记了订单。如何访问此标记?

开发者可以使用不同的标识符标记 Bazel 目标,通常用于测试目的。例如,测试标记可以注释测试在调试和发布过程中的角色,尤其是对于缺少任何运行时注释功能的 C++ 和 Python 测试。使用标记和大小元素可以灵活地根据代码库的签入政策组装测试套件。

在此示例中,标记为 pizzamacAndCheese 之一,用于表示菜单项。此命令会查询在特定软件包中具有与您的标识符匹配的标记的目标。

bazel query 'attr(tags, "pizza", //src/main/java/com/example/customers/...)'

此查询会返回“customers”软件包中具有“pizza”标记的所有目标。

自我测试

使用此查询了解 Jenny 想订购什么。

答案

芝士通心粉

添加新的依赖项

Bazel 咖啡馆扩大了菜单,顾客现在可以订购冰沙了!这种特定的冰沙由 StrawberryBanana 配料组成。

首先,添加冰沙依赖的配料:Strawberry.javaBanana.java。添加空的 Java 类。

src/main/java/com/example/ingredients/Strawberry.java

package com.example.ingredients;

public class Strawberry {

}

src/main/java/com/example/ingredients/Banana.java

package com.example.ingredients;

public class Banana {

}

接下来,将 Smoothie.java 添加到相应的目录:dishes

src/main/java/com/example/dishes/Smoothie.java

package com.example.dishes;

public class Smoothie {
    public static final String DISH_NAME = "Smoothie";
    public static final String DESCRIPTION = "Yummy and Refreshing";
}

最后,将这些文件作为规则添加到相应的 BUILD 文件中。为每种新配料创建一个新的 Java 库,包括其名称、公开可见性和新创建的“src”文件。您应该会得到以下更新后的 BUILD 文件:

src/main/java/com/example/ingredients/BUILD

java_library(
    name = "cheese",
    visibility = ["//visibility:public"],
    srcs = ["Cheese.java"],
)

java_library(
    name = "dough",
    visibility = ["//visibility:public"],
    srcs = ["Dough.java"],
)

java_library(
    name = "macaroni",
    visibility = ["//visibility:public"],
    srcs = ["Macaroni.java"],
)

java_library(
    name = "tomato",
    visibility = ["//visibility:public"],
    srcs = ["Tomato.java"],
)

java_library(
    name = "strawberry",
    visibility = ["//visibility:public"],
    srcs = ["Strawberry.java"],
)

java_library(
    name = "banana",
    visibility = ["//visibility:public"],
    srcs = ["Banana.java"],
)

在菜品的 BUILD 文件中,您需要为 Smoothie 添加新规则。这样做会将为 Smoothie 创建的 Java 文件作为“src”文件,并包含您为冰沙的每种配料创建的新规则。

src/main/java/com/example/dishes/BUILD

java_library(
    name = "macAndCheese",
    visibility = ["//visibility:public"],
    srcs = ["MacAndCheese.java"],
    deps = [
        "//src/main/java/com/example/ingredients:cheese",
        "//src/main/java/com/example/ingredients:macaroni",
    ],
)

java_library(
    name = "pizza",
    visibility = ["//visibility:public"],
    srcs = ["Pizza.java"],
    deps = [
        "//src/main/java/com/example/ingredients:cheese",
        "//src/main/java/com/example/ingredients:dough",
        "//src/main/java/com/example/ingredients:tomato",
    ],
)

java_library(
    name = "smoothie",
    visibility = ["//visibility:public"],
    srcs = ["Smoothie.java"],
    deps = [
        "//src/main/java/com/example/ingredients:strawberry",
        "//src/main/java/com/example/ingredients:banana",
    ],
)

最后,您需要在厨师的 BUILD 文件中将冰沙作为依赖项。

src/main/java/com/example/restaurant/BUILD

java\_library(
    name = "chef",
    visibility = ["//visibility:public"],
    srcs = [
        "Chef.java",
    ],

    deps = [
        "//src/main/java/com/example/dishes:macAndCheese",
        "//src/main/java/com/example/dishes:pizza",
        "//src/main/java/com/example/dishes:smoothie",
    ],
)

java\_library(
    name = "cafe",
    visibility = ["//visibility:public"],
    srcs = [
        "Cafe.java",
    ],
    deps = [
        ":chef",
    ],
)

再次构建 cafe 以确认没有错误。如果构建成功,恭喜!您已为“Cafe”添加了新的依赖项。如果构建失败,请注意拼写错误和软件包命名。如需详细了解如何编写 BUILD 文件,请参阅 BUILD 样式指南

现在,直观呈现添加 Smoothie 后的新依赖关系图,以便与之前的依赖关系图进行比较。为清楚起见,将图输入命名为 graph2.ingraph2.png

bazel query --noimplicit_deps 'deps(:runner)' --output graph > graph2.in
dot -Tpng < graph2.in > graph2.png

与第一个图表相同的图表,但现在有一个从“厨师”目标开始的 spoke,其中包含“冰沙”,可到达“香蕉”和“草莓”

查看 graph2.png,您可以看到 Smoothie 与其他菜品没有共享依赖项,只是 Chef 依赖的另一个目标。

somepath() 和 allpaths()

如果您想查询一个软件包为何依赖于另一个软件包,该怎么办?显示这两个软件包之间的依赖项路径即可提供答案。

有两个函数可以帮助您查找依赖项路径:somepath()allpaths()。给定起始目标 S 和终点 E,使用 somepath(S,E) 查找 S 和 E 之间的路径。

通过查看“Chef”和“Cheese”目标之间的关系,了解这两个函数之间的区别。从一个目标到另一个目标有不同的可能路径:

  • Chef → MacAndCheese → Cheese
  • Chef → Pizza → Cheese

somepath() 会从两个选项中为您提供一条路径,而“allpaths()”会输出所有可能的路径。

以 Bazel 咖啡馆为例,运行以下命令:

bazel query "somepath(//src/main/java/com/example/restaurant/..., //src/main/java/com/example/ingredients:cheese)"
//src/main/java/com/example/restaurant:cafe
//src/main/java/com/example/restaurant:chef
//src/main/java/com/example/dishes:macAndCheese
//src/main/java/com/example/ingredients:cheese

输出遵循 Cafe → Chef → MacAndCheese → Cheese 的第一条路径。如果您改用 allpaths(),则会得到:

bazel query "allpaths(//src/main/java/com/example/restaurant/..., //src/main/java/com/example/ingredients:cheese)"
//src/main/java/com/example/dishes:macAndCheese
//src/main/java/com/example/dishes:pizza
//src/main/java/com/example/ingredients:cheese
//src/main/java/com/example/restaurant:cafe
//src/main/java/com/example/restaurant:chef

从咖啡馆到厨师,再到披萨、芝士通心粉和奶酪的输出路径

allpaths() 的输出是一个扁平的依赖项列表,因此难以阅读。使用 Graphviz 直观呈现此图有助于更清楚地了解关系。

自我测试

Bazel 咖啡馆的一位顾客对餐厅进行了首次评价!遗憾的是,评价缺少一些详细信息,例如评价者的身份以及评价所指的菜品。幸运的是,您可以使用 Bazel 访问此信息。reviews 软件包包含一个程序,用于输出神秘顾客的评价。使用以下命令构建并运行该程序:

bazel build //src/main/java/com/example/reviews:review
bazel-bin/src/main/java/com/example/reviews/review

仅使用 Bazel 查询,尝试找出谁撰写了评价,以及他们描述的是什么菜品。

提示

检查标记和依赖项以获取实用信息。

答案

此评价描述的是披萨,评价者是 Amir。如果您查看此规则使用 bazel query --noimplicit\_deps 'deps(//src/main/java/com/example/reviews:review)' 具有哪些依赖项,则此命令的结果会显示 Amir 是评价者! 接下来,既然您知道评价者是 Amir,您可以使用查询函数来查找 Amir 在 `BUILD` 文件中具有哪个标记,以查看其中有什么菜品。 命令 bazel query 'attr(tags, "pizza", //src/main/java/com/example/customers/...)' 输出 Amir 是唯一订购披萨的顾客,也是评价者,这为我们提供了答案。

总结

恭喜!您现在已经运行了几个基本查询,您可以在自己的项目中尝试这些查询。如需详细了解查询语言语法,请参阅查询参考页面。想要了解更多高级查询?查询指南展示了比本指南中介绍的更多用例的详细列表。