ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Bazel Labels 全面解析:从规范形式到词法校验的权威指南

Bazel Labels 全面解析:从规范形式到词法校验的权威指南 Bazel Labels 全面解析从规范形式到词法校验的权威指南【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel导读Label标签是 Bazel 中标识构建目标target的核心概念无论是声明deps依赖、执行bazel build还是编写BUILD文件都离不开它。本文以 Bazel 官方文档《Labels》为骨架结合本仓库bazel 构建系统源码中的标签解析与校验实现src/main/java/com/google/devtools/build/lib/cmdline/LabelParser.java、LabelValidator.java完整讲解 Label 的规范形式、缩写规则、词法限制、包名与目标名的边界以及它在BUILD文件、查询语言和命令行中的实际用法。读完本文你将能准确写出无歧义的 Label、避开跨包引用的常见陷阱并理解 Bazel 为何要对 Label 字符集做如此严格的约束。一、Label 是什么目标的全局标识符Label是 Bazel 中一个 target构建目标的标识符。一个典型的完整规范形式full canonical form的 Label 长这样myrepo//my/app/main:app_binary它由三部分组成组成部分示例含义仓库名repository namemyrepo标识目标所在的仓库包名package namemy/app/main包相对于仓库根目录的路径目标名target nameapp_binary包内具体的目标1.1 规范仓库名Canonical Repo Name与双语法Label 的第一部分是仓库名。双语法表示这是一个canonical规范仓库名在整个 workspace 内全局唯一。带有规范仓库名的 Label 无论出现在什么上下文中都能无歧义地标识同一个目标相关概念见 外部依赖总览。不过规范的仓库名往往是一串晦涩的字符串例如rules_javatoolchainslocal_jdk这种名称由 Bzlmod 的模块解析机制生成可读性差。因此实际代码中更常见的是带apparent表面仓库名的 Label其唯一区别是仓库名前缀只有一个myrepo//my/app/main:app_binarymyrepo是 apparent 名称它可能因 Label 出现的上下文不同而指向不同的仓库。从源码结构看Bazel 在解析阶段会同时区分这两类语法LabelParser.java 中通过rawLabel.startsWith()判断repoIsCanonical从而决定后续按哪种语义解析仓库部分。1.2 仓库名可以省略的情形在典型场景下Label 引用的就是它所在的同一个仓库此时仓库名部分可以省略。例如在myrepo内部第一个 Label 通常写作//my/app/main:app_binary1.3 包名的两层含义未限定包名与全限定包名Label 的第二部分是未限定包名un-qualified package namemy/app/main即包相对于仓库根目录的路径。仓库名与未限定包名合在一起构成全限定包名fully-qualified package namemyrepo//my/app/main1.4 包名与冒号可以省略的情形当 Label 引用的是它所在包内的目标时包名以及可选的冒号都可以省略。因此在myrepo//my/app/main包内下面两种写法等价app_binary :app_binary按惯例文件目标省略冒号、规则目标保留冒号但这并非强制冒号本身在语法上没有其他含义。1.5 目标名与包路径末段重合时可以省略冒号后面的app_binary是未限定目标名。当它恰好与包路径的最后一个组成部分同名时目标名和冒号都可以省略。因此下面两个 Label 完全等价//my/app/lib //my/app/lib:lib1.6 包内子目录中的文件目标位于包内子目录中的文件目标其名称是文件相对于包根目录即包含BUILD文件的目录的路径。例如下面的文件位于仓库的my/app/main/testdata子目录中前提是my/app/main是一个包//my/app/main:testdata/input.txt二、//my/app的双重含义包还是目标像//my/app和some_repo//my/app这样的字符串在不同上下文中有两种含义当 Bazel期望一个 Label时它们分别等价于//my/app:app和some_repo//my/app:app当 Bazel期望一个包名例如在package_group规范中时它们引用的是包含该 Label 的那个包。2.1 最常见的错误用//my/app引用整个包在BUILD文件中一个常见错误是用//my/app来指代一个包或者指代包内所有目标——它并不会这么做。请记住//my/app等价于//my/app:app它命名的是当前仓库my/app包中的app目标。不过在package_group的规范中或在.bzl文件中用//my/app指代包是被鼓励的写法因为它能清楚地表达包名是绝对的、以 workspace 顶层目录为根。2.2 跨包引用必须使用完整路径相对 Label 不能用于引用其他包中的目标这种情况下必须始终给出仓库标识符和包名。例如假设源码树中同时存在包my/app和包my/app/testdata这两个目录各有自己的BUILD文件后者包含一个名为testdepot.zip的文件。下面是//my/app:BUILD中引用该文件的两种方式一错一对错误——testdata是另一个包不能使用相对路径testdata/testdepot.zip正确—— 使用完整路径引用testdata//my/app/testdata:testdepot.zip2.3//引用主仓库外部仓库也能用以//开头的 Label 是对**主仓库main repository**的引用即使从外部仓库中使用也依然有效。因此从外部仓库引用时//a/b/c与//a/b/c是不同的//a/b/c指回主仓库//a/b/c会在外部仓库自身内部查找//a/b/c。这一点在编写「主仓库中的规则、但会被外部仓库使用」的场景下尤为重要——如果规则内引用主仓库目标时写成//a/b/c一旦该规则被外部仓库引用就会解析失败。关于在命令行中指定构建目标的更多方式目标模式 target patterns见 build 命令的目标模式章节。三、Label 的词法规范Lexical SpecificationLabel 语法刻意避免使用对 shell 有特殊含义的元字符。这有助于避免意外的引号问题也让构造、操作 Label 的工具和脚本例如 Bazel Query 语言更加容易。3.1 目标名规则 —package-name:target-nametarget-name是目标在包内的名称规则目标的名称是其在BUILD文件声明中name属性的值文件目标的名称是文件相对于包含BUILD文件目录的路径名。目标名允许的字符集a–z、A–Z、0–9以及标点符号!%-^_#$()*,;?[]{|}~/.。文件名的额外约束文件名必须是正规形式的相对路径名即不能以斜杠开头或结尾例如/foo和foo/都是禁止的不能包含连续多个斜杠作为路径分隔符例如foo//bar被禁止不能包含上级引用..或当前目录引用./。错误—— 不要用..引用其他包中的文件../other_pkg/foo.cc正确—— 使用//package-name:filename//other_pkg:foo.cc斜杠的使用建议文件目标名中经常使用/但应尽量避免在规则名中使用/尤其在用 Label 的缩写形式时容易让读者混淆。Label//foo/bar/wiz永远是//foo/bar/wiz:wiz的缩写即使不存在foo/bar/wiz这个包也是如此它永远不会指//foo:bar/wiz即使该目标确实存在。当然也存在必须用斜杠的场景某些规则的名称必须与其主源文件同名而该源文件可能位于包的子目录中例如cc_library与同名头文件子目录的组合。源码级验证目标名校验实现从源码结构看Bazel 在 LabelValidator.java 的validateTargetName中逐字符执行这些约束以/开头或结尾均报错以..、../、./开头的路径段分别被标记为 up-level references 或 . as a path segment循环中遇到/../、/./、//会立即返回对应错误控制字符\u001f及以下和\u007fDEL被明确拒绝并给出\xXX十六进制提示未在允许集合内的字符统一报错target names may not contain c。值得注意的是ALWAYS_ALLOWED_TARGET_CHARACTERS还通过CharMatcher.inRange(128, 65535)放行了全部非 ASCII 字符源码注释说明这与 Bazel 内部字符串与 Unicode 字符串的双编码兼容有关因此非 ASCII 文件名在目标名中是允许的。3.2 包名规则 —//package-name:target-name包名是包含其BUILD文件的目录名相对于所在仓库的顶层目录。例如my/app。技术层面的强制约束Bazel 对包名施加以下硬性规则允许的字符小写字母a–z、大写字母A–Z、数字0–9以及字符! # $ % ( ) * , - . ; ? [ ] ^ _ { | }注意其中包含一个空格字符当然还有正斜杠/作为目录分隔符。不能以/开头或结尾。不能包含子串//——否则对应的目录路径无法定义。不能包含/./、/../、/.../等子串——这是为了避免在逻辑包名与物理目录名之间转换时路径字符串中.的语义造成混淆。源码 LabelValidator.java 的validatePackageName完整实现了上述规则先用ALLOWED_CHARACTERS_IN_PACKAGE_NAME字符矩阵做整体校验再从字符串尾部反向扫描检测//连续分隔符与纯.路径段PACKAGE_NAME_DOT_ERRORpackage name component contains only . characters。实践层面的建议对于目录结构对模块系统有意义的语言例如 Java务必选择在语言中合法的标识符作为目录名。例如不要以数字开头避免特殊字符尤其是下划线和连字符_、-因为它们在 Java 包名中会有问题。虽然 Bazel 支持 workspace 根包中的目标例如//:foo但最好让根包保持为空这样所有有意义的包都有描述性的名称。四、Rules规则与 Label 的关系规则rule描述的是输入与输出之间的关系以及构建输出的步骤。规则有很多种类有时称为rule class它们可以产出可执行文件与库、测试可执行文件等受支持的输出详见本仓库的 构建百科全书式参考 所关联的规则体系。BUILD文件通过调用规则rules来声明目标targets。下面这个例子用cc_binary规则声明了目标my_appcc_binary( name my_app, srcs [my_app.cc], deps [ //absl/base, //absl/strings, ], )4.1name属性与属性类型每次规则调用都必须有一个name属性必须是合法的 目标名它在BUILD文件所在的包内声明一个目标。每条规则都有一组属性attributes。某条规则适用的属性以及每个属性的意义和语义取决于规则的种类rule kind。每个属性都有名称和类型常见类型包括属性类型说明integer整数值label单个目标引用list of labels目标引用列表string字符串值list of strings字符串列表output label输出目标引用list of output labels输出目标引用列表并非所有属性都需要在每条规则中指定。属性由此构成一个从键名称到可选、类型化值的字典。许多规则都有的srcs属性类型是 list of labels若给出其值是一个 Label 列表每个 Label 都是该规则输入目标的名称。4.2 规则名何时重要有些情况下规则种类rule kind的名称有些随意更值得关注的是规则生成文件的名称——这正是 genrule 的情况见 General Rules: genrule 相关说明。而在另一些情况下名称至关重要例如对*_binary和*_test规则规则名直接决定了构建产出的可执行文件名。4.3 目标图与查询工具目标之间构成的这个有向无环图被称为目标图target graph或构建依赖图build dependency graph它是 Bazel Query 工具 作用的领域。理解 Label 的解析规则是正确使用bazel query、bazel cquery与bazel build目标模式的前提。五、延伸Label 解析与命令行目标模式5.1 解析器眼中的 Label 形态从源码结构看Bazel 的 LabelParser.java 将原始 Label 字符串拆解为repo仓库名、repoIsCanonical是否双、pkgIsAbsolute包部分是否以//开头、pkg包部分、pkgEndsWithTripleDots是否以...结尾、target目标部分等字段。其内置的解析表覆盖了全部常见形态原始字符串reporepoIsCanonicalpkgIsAbsolutepkg解析出的 targetfoo/barnullfalsefalsefoo/bar//foo/barnullfalsetruefoo/barbarreporepofalsetruereporeporepotruetruereporepo//foo/barrepofalsetruefoo/barbarrepo//foo/barrepotruetruefoo/barbar:quuxnullfalsefalsequux//foo/bar:quuxnullfalsetruefoo/barquuxrepo//foo/bar:quuxrepofalsetruefoo/barquux其中foo被特殊处理为foo//:foo的同义形式。这张表直观地印证了前文所述的省略规则无仓库名、无//前缀、无冒号时目标名就是整个字符串本身只有//前缀时目标名默认取包路径的最后一段。5.2 从 Label 到目标模式Target PatternsLabel 用于标识单个目标例如在BUILD文件的依赖声明中而 bazel build 等命令 接受的目标模式target patterns是 Label 语法的集合泛化支持通配符。最简单的情形下任何合法 Label 本身就是一个目标模式它标识恰好一个目标的集合。常用模式包括目标模式含义//foo/bar:wiz单个目标//foo/bar:wiz//foo/bar等价于//foo/bar:bar//foo/bar:all包foo/bar中的所有规则目标//foo/...foo目录下所有包中的所有规则目标//foo/...:*foo目录下所有包中的全部目标含规则和文件//...主仓库所有包中的规则目标不含外部仓库//:allworkspace 根包中的全部规则目标不带//开头的目标模式相对于当前工作目录解析:all是目标级通配符匹配包内所有规则...是包级通配符递归匹配目录下所有包二者可组合为foo/...:all并缩写为foo/...。另外:*或:all-targets匹配的是所有目标包括不被任何规则正常构建的文件如java_binary的_deploy.jar因此:*是:all的超集。六、常见误区速查写法实际含义是否推荐//my/app//my/app:app目标不是包、不是包内全部目标在deps等 Label 语境中要明确//my/app:all包内所有规则仅命令行目标模式可用testdata/input.txt跨包非法跨包必须写全路径错误写法//my/app/testdata:input.txt跨包引用的正确写法正确写法//foo/bar/wiz恒等于//foo/bar/wiz:wiz注意与//foo:bar/wiz区分//a/b/c主仓库中的目标外部仓库中也能用写规则时优先考虑结语Label 是 Bazel 世界的地基。理解其「仓库名 包名 目标名」的三段式结构、双规范仓库名与单表面仓库名的区别、各组成部分的省略时机以及目标名与包名各自的字符集约束能让你在编写BUILD文件、调试依赖解析、使用bazel query时少走大量弯路。本仓库中的 LabelValidator.java 与 LabelParser.java 是这些规则的权威实现遇到模糊的边界情况时直接查阅源码即是最可靠的答案。【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进