惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

小众软件
小众软件
A
About on SuperTechFans
博客园 - Franky
Engineering at Meta
Engineering at Meta
Recent Announcements
Recent Announcements
云风的 BLOG
云风的 BLOG
B
Blog
Microsoft Security Blog
Microsoft Security Blog
L
LangChain Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
U
Unit 42
Martin Fowler
Martin Fowler
Y
Y Combinator Blog
Stack Overflow Blog
Stack Overflow Blog
博客园 - 叶小钗
Vercel News
Vercel News
Apple Machine Learning Research
Apple Machine Learning Research
The Cloudflare Blog
Last Week in AI
Last Week in AI
腾讯CDC
Microsoft Azure Blog
Microsoft Azure Blog
爱范儿
爱范儿
V
V2EX
G
Google Developers Blog

TinyEdi

Escaping Huawei/Honor PC Manager Coroutine GDB with Python Unix related things Shared Library I dreamt for so long The Building Blocks of Transformers A note for cmake A Strory of Mixin Tablegen Language Tutorial Lit and FileCheck 比较运算符, Min, Max, Sort 和 Order IEEE 754 的 inf 比较问题 KD树与SKD树 汉诺塔问题-记录
Bazel Notes
Edimetia3D · 2024-02-19 · via TinyEdi
记录

Edimetia3D

10 min read

这是一篇2019年左右的记录, 内容可能过时, 也不太全面

杂谈

Bazel是Google为Monorepo服务而开发的构建工具.

首先是巨大,当问题的规模变大,事情总是会变得更复杂. 而Google面对的"巨大Monorepo",应该是世间罕有的.

然后是Monorepo,这极大的影响了代码的组织风格.例如,你要写一个操作系统内核ProjectOS,还要写一个游戏ProjectGame.在传统的开发习惯中,这两个项目会组织到两个不同的Repo里,PorjectOS和ProjectGame之间无法直接相互引用,例如,你在ProjectOS里写了一个高级的数据结构,想要在Game里也使用,要么直接复制粘贴,要么是创建一个新的CommonRepo,把可公用的代码都放在Common里,然后两个项目各自引入Common作为依赖.

使用MonoRepo则不存在这个问题,Game可以直接依赖OS内的组件,按照Bazel的语法描述,就是在Game中可以直接使用@ProjectOS//path/to/package:AdvancedStruct.当然,你仍然可以选择重构一个Common出来,但是现在已经没有这种必要了.

从技术层面说,在使用"巨大Monorepo"时,Bazel相对现有编译系统的优势能充分展现.你的项目离"巨大Monorepo"越远,使用Bazel的优势就越不明显. 宽泛的来说,对于大部分项目,使用Bazel都不能带来明显的技术收益.

一般而言,使用Bazel的优势是并不是体现在技术层面.而是让你的代码能无痛使用谷歌的开源组件. 原因很简单, Google的项目都是基于Bazel的,如果你不用Bazel,就需要自己做一些额外的工作.

换个角度来看,这也是谷歌的"阴谋":如果Google外的开发者不用Bazel,那么当Google需要依赖外部项目时,就需要手动把这些项目转换成Bazel的;而如果全世界的程序都用Bazel来组织编译,那么Google就可以无缝的从开源世界汲取力量.

吹Bazel的帖子到处都有,所以这里只说黑点,如果这些黑点你并不很care,那么使用Bazel应该是个不错的选择

  1. Bazel是后来者,熟悉它的开发者比较少,加上学习成本高,可能需要有若干"Bazel"专家负责整个构建系统的维护.
  2. 对于已经成熟的大中型项目,迁移到Bazel的时间/人力成本会比较高,而这一般也不会带来明显的收益.最好在项目早期使用Bazel
  3. Bazel是Java写的, 也就是说,它依赖JVM
  4. 使用Bazel的低级功能收益有限,而使用高级功能的学习成本又很大.
  5. 本质上说,BUILD文件和rule语法服务于构建action_graph,这是一种"声明式编程",如果你熟悉TF等框架的计算图,那么理解起来应该不难. 如果你不熟悉,那么可能就会需要适应一下.
  6. sandbox对构建系统也许是有帮助的,但是对开发者的心智和头发绝对是不友好的.
    • 例如1. 你生成了一些头文件,由于这些头文件在sandbox内,你必须用一些tricky的方法才能让IDE识别到这些头文件.
    • 例如2. 编译器报了某个文件的错误,你打开报错的文件一顿修改… 结果发现修改的只是sandbox内的副本.

How Bazel Works

Bazel的学习成本很高,为了实现很多高级功能,引入了一些复杂的概念,如果你不懂这些概念,那么有可能连帮助文档都看不懂.

Bazel的典型文件组织

  • Repo的根目录下有一个唯一的WORKSPACE文件,这个文件只用于描述该repo依赖的其他repo
  • 子目录下可以有BUILD文件,有BUILD文件的目录称为PACKAGE,BUILD文件用于描述target,PACKAGE之间没有交集.
  • BUILD文件所在目录为DIR,那么DIR内(递归)的所有文件都是PACKAGE的一个target,这个target的name就是相对路径
    • 例如,若存在/dir/foo/BUILD/dir/foo/bar/some_code.cc,那么foo这个PACKAGE内就存在一个name为bar/some_code.cc的target
  • 每个target都有一个全局唯一的label,这个label主要通过路径名和target名组合生成,典型结构为@repo_name//path_to_pacakge:target_name
    • @repo_name表示label所在的repo
    • @是一个特殊的case,它表示项目的绝对root repo
    • 例如,repoA在WORKSAPCE内依赖repoB,那么在这种场景下,repoB内的@实际代表了repoA.而对于单独的repoB,@代表了它自己
    • 在引用label时,存在各种语法糖来简化label的写法,这些语法糖并不重要

Bazel的运行流程

理解Bazel的运行流程是正常使用Bazel的前提. 然而, 即便是官方文档,对此也是只言片语的三句话"Loading,Analysis,Execution". 在此,我将从类比Python的角度去描述Bazel的执行流程.

这里可以先记忆以下信息

  1. Bazel相当于是Starlark语言的Interpreter.
  2. Bazel执行时,会创建一个Sandbox, 这个Sandbox体现为一个独立的工作目录,用于隔离文件操作.用户写的各类rule,target等操作都是在sandbox内执行的.只有bazel自身能访问sandbox外的原始文件.
  3. Bazel的执行逻辑大致可以用下面的伪代码描述.
  4. Bazel的Loading/Analysis截断是在构建一个称为Action Graph的图, 这个图的每个节点都是一个编译动作, 对应了一个编译Target, target之间的依赖决定了图的结构, 也决定了编译的顺序.

注意: Bazel里的macro是"函数"的意思,并不是C++那样的,在预处理阶段通过复制粘贴的形式展开到调用处。

############### Loading
import WORKSPACE # import is same as `load`
import ALL_BUILD_FILE # import BUILD recursively

############### Analysis
bazel_build_flag,commandline_target = parse_cli_args()

action_graph=Graph()
sorted_target = topo_sort(global_target_map.to_list())
if do_analysis:
    for target in sorted_target:
        target.update()
        ret_info = target.impl(target.ctx)
        new_action_nodes = create_new_nodes(target.ctx,ret_info)
        action_graph.insert(new_action_nodes)
############### Execution
if do_execution and action_graph.isValid()
    exec_in_sandbox(action_graph,commandline_target)
  • Load阶段将会加载所有的脚本,加载脚本的顺序为:先加载repo根目录的WORKSPACE文件,然后递归的加载所有子目录下的BUILD文件.
    • WORKSPACE文件仅用于调用repo_rule,repo_rule执行后,会把第三方依赖组织到sandbox的extern内(下载或者软链接)
    • BUILD文件仅能用于调用rule,rule执行后,仅仅是创建了Target对象.
    • 逻辑上看,rule创建的target会注册到一个全局的global_map
    • rule语句创建新target时, 可以使用其他target的label来描述自己的依赖,且依赖label对应的target可以不存在(声明式)
    • 在 WORKSPACE/BUILD load的过程中,一些额外的.bzl文件也会被load进来,这些.bzl仅能用于定义marco/rule/repo_rule
    • 所有starlark脚本都能在当前文件的全局作用域定义变量,对.bzl而言,这些变量由_命名控制可见性.(BUILD/WORKSPACE文件不会被其他文件load,也就不存在可见性的概念)
  • Analysis阶段则会
    • 先根据Target之间的依赖关系对所有Target进行拓扑排序
    • 按照拓扑序顺序调用target.update();target.impl(target.ctx).
    • 执行update()主要是根据已有的信息更新target自身的部分属性值,这些值需要从依赖中获得,而由于拓扑排序,target的所有依赖一定已经执行过impl()了,一定是可以提供信息的.
    • 执行impl()用于更新自己的状态,并向后续的其他target提供信息.
    • Bazel会根据impl()返回的信息和target.ctx来构建新的action_node,并插入action_graph中
    • target.ctx内会包含所有的候选action
    • impl()返回的信息ret_info是一个列表,可以包含多种信息,最基本的是DefaultInfo,这个信息的file属性将标明当前target的输出文件.而只有与输出文件相关联的候选action才会被构建成action_node
  • Execution阶段则比较简单.
    • 只需要根据command_line给出的build target,逆着 action_graph 回溯,得到编译target所需的最小子图,然后执行这个最小子图即可.
    • Execution阶段是完全在sandbox内执行的,action所需要的文件由Bazel软链接到sandbox内.
    • 换言之,如果action的描述不正确,那么就可能有文件没有被拷贝到sandbox,进一步导致编译失败.例如,编译a.cc时需要a.h,而在描述action时,若没有把a.h对应到文件加入依赖列表,那么a.h就不会被拷贝,在sandbox内就会编译失败,提示"找不到a.h"
    • 仅在execution阶段才会动态的逐步向sandbox内添加文件.

注意,WORKSPACE内调用repo_rule时,从功能上说类似于"函数",调用的repo_rule会立即执行,并不会产生target对象;而在BUILD文件内调用rule时,rule从功能上说则是class,仅仅用于创建新的target对象.

其他细节

  • 在Ananysis阶段,target.update()会做很多事,其中比较重要的一项就是更新target.ctx
    • 例如ctx.files就会被更新为具体的文件路径.例如,ctx.attr.depend_target是一个file型label,那么更新后,ctx.file.depend_target就是一个字符串,代表了对应的文件路径.
  • bazel rule的impl函数返回的DeafultInfo直接决定了最终的action_graph包含了哪些action.而没有进入action_graph的action,都不会被执行.
  • python是bazel的依赖,支持bazel的平台一定会有python,但是shell则不一定,所以跨平台项目中,当需要调用自定义脚本时,最好是python脚本
  • 只能使用--workspace_status_command来在原始的repo目录内执行用户自定义脚本,自定义脚本输出到std::cout的信息可以被bazel重定向到两个特殊的文本文件.
  • impl()函数内创建候选action时,粒度越细越好,这样可以充分利用bazel的cache及action_graph最小执行.
    • bazel的最小执行和cache系统都非常激进,如果impl给出的候选action和DefaultInfo不正确… 那么cache/最小执行就很可能不能正确工作.
  • action的输入/输出不能是目录

Bazel Document Note

bazel的platform分为三个概念: host:执行bazel程序的主机,execution:执行构建程序的主机,target:运行最终产物的主机

.bazelrc文件用于自动为bazel 执行提供额外的参数

build文件描述的依赖关系必须严格和实际代码一致,且包含所有的直接依赖,否则bazel的cache系统就无法正确的识别出依赖,导致使用错误的cache。