Flutter Engine 许可证合规工具 licenses_cpp 完全指南:构建、运行与数据格式解析
licenses_cpp 是 Flutter Engine 仓库内置的一套用 C++ 编写的许可证收集与校验工具,在 CI 中运行,用于扫描引擎全部源码与第三方依赖,逐一核对版权声明(copyright header)与独立许可证文件,最终汇总生成 licenses.txt。本文以该工具的 README 与配套的 GEMINI.md 为骨架,结合 src/ 下的真实实现与 data/ 数据文件,说明如何本地构建、跑测试、对单个文件做校验,并深入讲解数据目录的每一种格式与底层工作流。
一、工具定位:CI 中的许可证“守门人”
Flutter Engine 的代码库包含大量第三方依赖(ICU、libjpeg、HarfBuzz、v8、inja 等),每个都带有各自的许可协议。为了满足合规要求,引擎需要:
- 确认每个源码文件头部都带有可识别的版权/许可证注释;
- 确认每个第三方包的
LICENSE文件属于已知、可接受的许可证集合; - 把扫描到的许可证文本按“包名 → 许可证全文”的形式统一输出。
licenses_cpp 就是这个流程的可执行实现。它在 CI 阶段被调用,收集并校验源码许可证,发现未知许可证或缺失版权头时会以非零退出码报告错误(实现见 src/license_checker.cc 的 Run/FileRun:return errors.empty() ? 0 : 1;)。
从 BUILD.gn 可以清晰看到整个工具由四个 GN target 组成:
| Target | 类型 | 说明 |
|---|---|---|
//flutter/tools/licenses_cpp |
source_set |
工具核心库(解析、匹配、过滤等模块),公开依赖 re2 与 absl(log、flags、status) |
//flutter/tools/licenses_cpp:licenses_cpp |
executable |
基于 src/main.cc 的命令行入口 |
//flutter/tools/licenses_cpp:license_file_compare |
executable |
独立的许可证文件比对工具(由 src/license_file_compare.cc + src/mmap_file.cc 组成) |
//flutter/tools/licenses_cpp:licenses_cpp_testrunner |
executable(testonly) |
基于 googletest 的单元测试集 |
除代码之外,目录中还有 tools/convert.dart(历史数据转换脚本)、CODEOWNERS 以及承载全部校验规则与样例的 data/ 目录。
二、目录结构与工程布局
engine/src/flutter/tools/licenses_cpp/
├── BUILD.gn # 上述四个 GN target 定义
├── CODEOWNERS
├── GEMINI.md # 本地构建与运行指引
├── README.md # 工具一句话说明(入口文档)
├── data/ # 校验所需的数据文件
│ ├── README.md # 数据格式规范(本文第四节详述)
│ ├── include.txt # 需要检查的文件路径正则清单
│ ├── exclude.txt # 需要排除的文件路径正则清单
│ ├── licenses/ # 已接受的许可证“模板库”(约 80 个)
│ └── secondary/ # 需原样加入输出的补充许可证
├── src/ # C++ 实现源码
│ ├── main.cc # 命令行入口,解析 absl flags
│ ├── license_checker.cc/.h # 核心扫描/校验/汇总逻辑
│ ├── catalog.cc/.h # 许可证两级匹配目录(selector + matcher)
│ ├── comments.cc / comments.l/.h # 源码注释提取(flex 词法)
│ ├── data.cc/.h # data/ 目录的内存表示
│ ├── deps_parser.cc/.h # 解析 DEPS 文件以补扫非 git 依赖
│ ├── filter.cc/.h # include/exclude 正则过滤器
│ ├── mmap_file.cc/.h # 内存映射文件读取(mmap)
│ └── license_file_compare.cc # license 文件两两比对入口
└── tools/convert.dart
src/:全部工具实现代码;data/:工具运行所需的数据,例如各类正则规则与许可证模板。
核心库 licenses 将“文件读取(mmap)”“注释提取(flex 词法)”“正则过滤(re2)”“许可证匹配(Catalog)”“包归属判定(GetPackage)”拆分为独立模块,每个模块都有对应的 *_unittests.cc 测试,代码组织非常便于单点验证。
三、本地构建:编译测试与校验工具
licenses_cpp 是 Flutter Engine monorepo 的一部分,需通过仓库内的 et(engine tool)构建。文档 GEMINI.md 给出的命令都假定当前 shell 已位于 engine/src/flutter/tools/licenses_cpp 目录。
3.1 构建并运行单元测试
# 1) 在仓库根目录执行构建(从工具目录向上两级进入 flutter/,再取 bin/et)
../../bin/et build --no-rbe -c host_debug_unopt_arm64 //flutter/tools/licenses_cpp:licenses_cpp_testrunner
# 2) 运行测试二进制(输出位于 engine/src/out/host_debug_unopt_arm64/ 下)
../../../out/host_debug_unopt_arm64/licenses_cpp_testrunner
拆解:
../../bin/et build:调用引擎构建工具;--no-rbe:不使用远程构建执行(本地编译);-c host_debug_unopt_arm64:选择构建配置(此处为 host 端、debug、未优化、arm64 目标);//flutter/tools/licenses_cpp:licenses_cpp_testrunner:编译含全部单元测试的 testonly target。
测试集对应 BUILD.gn 中 licenses_cpp_testrunner 的 5 个源文件:catalog_unittests.cc、comments_unittests.cc、deps_parser_unittests.cc、filter_unittests.cc、license_checker_unittests.cc,分别覆盖“许可证目录匹配”“注释提取”“DEPS 解析”“正则过滤”“整体校验”五个子系统。
提示:GEMINI.md 注明主工具最好以 profile 配置构建(下文示例使用
host_profile_arm64),以贴近真实性能场景。
3.2 对单个文件执行许可证检查
想快速验证“某一条规则/某一个文件是否命中”,不必全库扫描,工具提供了 --input 单文件模式:
../../bin/et build --no-rbe -c host_profile_arm64 //flutter/tools/licenses_cpp
../../../out/host_profile_arm64/licenses_cpp \
--working_dir ../.. \
--data_dir ./data \
--licenses_path licenses.txt \
--input ../../third_party/icu/source/i18n/collunsafe.h \
--v=3
其中各参数含义(与 src/main.cc 中 ABSL_FLAG 定义一一对应):
| 参数 | 必填 | 作用 |
|---|---|---|
--working_dir |
✅ | 要扫描的目录(第 21 行注释明确标注 [REQUIRED]) |
--data_dir |
✅ | 存放许可证模板的目录,即本工具的 data/ |
--licenses_path |
✅ | 许可证汇总结果的写出路径(会以 ofstream 打开,失败即报错退出) |
--input |
选填 | 只扫描指定文件;与 --include_filter 互斥(同时给出会报错退出) |
--include_filter |
选填 | 覆盖默认 include 过滤规则的正则 |
--v |
选填 | 日志详细级别(整数,默认 0;示例 --v=3 输出更细的 VLOG) |
--treat_unmatched_comments_as_errors |
选填 | 是否把“无法匹配的版权注释”视作错误(bool,默认 false) |
--root_package |
选填 | 根包名(覆盖默认从 working_dir 目录名推导的包名) |
以示例命令中的路径为例:从 tools/licenses_cpp/ 出发,--working_dir ../.. 指向 engine/src/flutter,--input ../../third_party/... 实际指向 engine/src/third_party/icu/...,即扫描范围被收窄到 flutter 代码树及其依赖。
main.cc 的启动逻辑值得一看(第 78–137 行):先 absl::ParseCommandLine 解析参数并设置全局 VLOG 级别,然后校验三个必填 flag——缺失任何一个都会在 stderr 打印 Expected --xxx flag. 并返回退出码 1;随后若提供 --input 走 LicenseChecker::FileRun(单文件),否则走整库 LicenseChecker::Run;全程通过 std::ofstream 把扫描结果写入 licenses_path。
四、数据目录规范:include / exclude / catalog / secondary
真正决定“检查什么、接受什么”的,是 data/ 下的四类文件。官方规范见 data/README.md,原文明确指出所有正则一律采用 re2 语法。
4.1 include.txt:决定哪些文件参与检查
include.txt 列出将要接受检查的文件,格式为每行一条正则,必须整行完全匹配才纳入检查。# 开头的行是注释;行尾注释(同一行其他字符后跟 #)暂不支持。
以仓库真实的 data/include.txt 为例:
# This file describes all the files we need to do a copyright header check on.
.*\.c$
.*\.cc$
.*\.cpp$
.*\.cxx$
.*\.dart$
.*\.frag$
.*\.glsl$
.*\.go$
.*\.h$
.*\.hh$
.*\.hpp$
.*\.hxx$
.*\.java$
.*\.js$
.*\.kt$
.*\.m$
.*\.mm$
.*\.py$
.*\.rb$
.*\.S$
.*\.sh$
.*\.swift$
.*\.ts$
.*\.txt$
.*\.ucm$
.*\.vert$
.*\.y$
^flutter/ci/licenses_golden/third_party/fuchsia_sdk/NOTICES$
^flutter/third_party/libjpeg-turbo/src/README\.ijg$
可以看到:常规规则按文件扩展名全匹配(.c/.cc/.cpp/.../.dart/.h/.../.kt/.mm/.py/.swift/.ts/.txt 等),末尾还以 ^...$ 锚定方式特例包含 fuchsia_sdk/NOTICES 与 libjpeg-turbo 的 README.ijg 两个非扩展名文件。实现层面对应 src/filter.h:一个 Filter 本质是把若干正则拼接成单个 RE2,Matches() 判断输入是否命中。
4.2 exclude.txt:剔除不需要检查的文件
exclude.txt 与 include.txt 同格式。关键约束:这些正则必须对“相对 --working_dir 的路径”做整行全匹配。执行时二者叠加使用——license_checker.cc 的 ProcessFile 中先判断 !include_filter.Matches(...) || exclude_filter.Matches(...),命中任一条件即打 EXCLUDE 日志并跳过该文件。
4.3 data/licenses/:可接受许可证的匹配模板库
data/licenses/(内存中由 Data.catalog 承载)存放的是“被认可、可出现在源码头或独立 LICENSE 文件中的许可证”,每个文件就是一种 matcher 条目,文件格式为三部分:
- 第一行:匹配器名称(例如
apache2 header、mit、bsd3); - 第二行:unique 正则——用于快速挑选候选 matcher,且不能与其他 matcher 的 unique 正则重叠;
- 其余行:抽取该许可证完整正文的 matcher 正则,规则如下:
- 所有空白一律按
\s+处理; - 行尾空白被忽略;
- 匹配出的分组会从输出中抽取,例如对
\[(.*)\]匹配[hi],得到[](即捕获组之外的结构会保留空文本、捕获内容进入正文)。
- 所有空白一律按
以仓库真实的 data/licenses/apache2_header.txt 为例:
apache2 header
(?s)^\s*Copyright[^\n]*\d\d\d\d.*Licensed under the Apache License, Version 2\.0.*See the License for the specific language
(?s)^\s*Copyright[^\n]*\d\d\d\d.*
Licensed under the Apache License, Version 2.0 \(the "License"\);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
\s+http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
第一行是名称 apache2 header;第二行是 unique 正则(要求文本同时含带四位年份的 Copyright、Apache License, Version 2.0 与 See the License for the specific language);从第三行起为正文 matcher,各关键字间的实际间距、换行由 .*/空白容错覆盖。data/licenses/ 下同类文件还包括 apache2.txt、bsd2_clause*.txt、bsd3_clause、mit*.txt、icu.txt、harfbuzz*.txt、sqlite.txt、unicode.txt、v8.txt 等约 80 个模板,基本覆盖引擎第三方依赖常见的 BSD/Apache/MIT/ISC/zlib/ICU 等许可文本变体。
实现层面 src/catalog.h 明确注释了设计动机:这是两级搜索——先用 RE2::Set 批量匹配 unique(selector)缩小候选集,再对命中的 matcher 做精匹配,从而“minimize the size of the RE2::Set”;当 selector 无命中时返回 kNotFound,多于一个时返回 kInvalidArgument。catalog_unittests.cc 对该匹配行为做了专项覆盖。
4.4 data/secondary/:原样附加的补充许可证
secondary/ 目录结构必须与 working_dir 中被扫描树的结构逐级一致。这里存放的许可证文件会被原样(verbatim)写入输出,不经过模板匹配。真实的 data/secondary/ 下能看到形如 flutter/third_party/inja/third_party/include/nlohmann/loitsch.txt、flutter/third_party/libcxx/boost.txt 的镜像路径——运行时会对每个 secondary 文件计算相对路径,若 working_dir 下不存在对应父目录,则报告 secondary license path mixmatch 错误(见 license_checker.cc 中对 data.secondary_dir 的处理)。这样能保证最终 licenses.txt 中附带一些“无独立源码可归属、但必须随包分发”的第三方许可文本。
4.5 include.txt 与 exclude.txt 实现细节
Filter::Open(filter.h)可接受文件路径或输入流两种来源——这正对应 main.cc 中 --include_filter 用一个字符串流构造自定义 Filter 以覆盖默认 include 规则的路径(main.cc 第 57–66 行),从而实现不修改数据文件即可定向收窄扫描范围。
五、源码级工作流:从整库扫描到 licenses.txt
了解了数据文件,再看核心执行器 src/license_checker.cc 是如何把它们串起来的。LicenseChecker::Run 的整体流程可归纳为 5 步:
-
发现 git 仓库:
GetGitRepos递归遍历 working_dir,找出所有含.git的目录;若 working_dir 自身不在其中则补入,从而允许扫描engine这类子目录。随后对每个仓库执行git ls-files拿到被 git 跟踪的文件清单,再对每个文件调用ProcessFile。 -
补扫 DEPS 中的非 git 依赖:不是每个依赖都是独立 git 仓库。工具向上查找
DEPS文件,用 deps_parser 解析其中的依赖路径,仅处理仍位于 working_dir 内的目录(越界依赖直接跳过),并对其做递归遍历。 -
确定文件归属的“包”:
GetPackage沿文件所在路径向上逐级寻找“归属包”。它维护一个从根包名开始的推导过程——一旦路径中出现third_party/,随后的下一级目录名就构成新包名,且遇到pkg、vulkan-deps(kThirdPartyIgnore)这类忽略目录时不产生包名拆分;同时用FindLicense沿路径向上查找最近一层的许可证文件。kLicenseFileNames中登记了 9 个会被识别为许可证文件的常见命名(LICENSE、LICENSE.TXT/.txt/.md/.MIT、COPYING、License.txt、docs/FTL.TXT、README.ijg)。 -
逐文件校验(ProcessFile):先经 include/exclude 过滤;有归属许可证文件的包先执行
MatchLicenseFile——用 mmap 读文件内容并在 Catalog 中查找匹配,命中则登记(license_map->Add(package.name, match)),未命中产生Unknown license in <路径>错误。同一许可证文件用seen_license_files集合保证只校验一次。随后对正文做处理:- 文件名恰好是
NOTICES时走ProcessNotices,用固定分隔线(80 个-)把文件切成“项目列表 + 许可证正文”区块,逐块匹配; - 其他文件走
ProcessSourceCode,通过 flex 词法器(comments.l)迭代提取注释,仅对含 license/copyright 字样的注释(正则(?i)(license|copyright))尝试匹配;若整份文件连一条注释都没有,则把全文当作文本文件直接匹配。未匹配到版权时:根包文件报Expected root copyright in ...,非根包若存在目录级许可证则放行,否则报Expected copyright in ...。
- 文件名恰好是
-
汇总输出:
LicenseMap以许可证文本为 key、包名集合为 value 做倒排去重;LicensesWriter把包名按字典序排序后,以“80 个-分隔线 + 包名列表 + 空行 + 许可证全文”的块结构写入输出流,即生成标准licenses.txt。全量扫描时终端还会打印progress: [ooo....]形式的 ASCII 进度条(仅当 stdout 是终端且 VLOG 未开启时)。
值得注意的性能与工程细节:文件读取统一走 mmap_file.cc 的内存映射而非流式读入,以支撑全库数万文件的扫描;错误通过 absl::Status 收集后统一打印,main.cc 依 errors.empty() 决定进程退出码,这正是 CI 失败判定的依据。
六、输出格式与 CI 判定
--licenses_path 指向的结果文件具备稳定可解析的结构。每次写入一个块时,LicensesWriter 会:
----------------------------------------------------------------------------
<包名A>
<包名B> # 同一份许可证的多个包,按字典序排列
<许可证全文> # 来自 Catalog 匹配抽取的正文,或 secondary 的原样内容
多个“许可证文本完全相同”的第三方包会共享同一个块,避免重复输出;文本不同的包各自成块。无论扫描单文件还是整库,Run/FileRun 都遵循同一输出与退出码约定,CI 只需检查退出码与生成的 licenses.txt 是否与 golden 一致即可完成合规门禁。
七、单元测试矩阵与回归保障
为保证 CI 门禁可靠,BUILD.gn 的 testrunner 集合了 5 组 googletest 用例,各组职责与校验焦点如下:
| 测试文件 | 验证对象 | 典型场景 |
|---|---|---|
filter_unittests.cc |
include/exclude 正则过滤器 | 多正则拼接、全匹配语义、注释行跳过 |
comments_unittests.cc |
flex 注释词法器 | 各语言注释形态的提取边界 |
deps_parser_unittests.cc |
DEPS 文件解析 | 依赖路径切分与过滤 |
catalog_unittests.cc |
许可证模板两级匹配 | unique 选择器命中/冲突、正文抽取、分组规则 |
license_checker_unittests.cc |
端到端扫描流程 | 整目录/单文件运行、错误收集与退出码 |
开发者新增一种许可证模板(例如引入新的第三方库)时,标准流程是:把模板按“名称 + unique 正则 + 正文正则”格式写入 data/licenses/,在 include.txt 补齐需要检查的文件类型(如需),必要时在 secondary/ 镜像添加原样分发的许可文本,最后跑一遍第三节的 testrunner 与单文件命令,确认 golden 与错误计数都符合预期后再提交。
八、实践小结
- 何时用单文件模式:调规则、debug 某文件为何报
Expected copyright时,用--input ... --v=3把 VLOG 开到 3,可看到Process:、EXCLUDE:、OK:、NOT_FOUND:等逐文件判定日志; - 何时用整库模式:提交/升级第三方依赖后,以
--working_dir指向引擎树、不传--input全量扫描,确认退出码为 0 且licenses.txt内容正确; - 数据文件是核心资产:
include.txt/exclude.txt决定扫描边界,data/licenses/的每个模板文件决定“接受哪些许可文本”,secondary/决定“哪些许可必须随包原样分发”——三者共同构成引擎许可证合规的策略面,而 src/ 中 mmap 读取、flex 注释提取、re2 两级匹配与LicenseMap汇总输出则构成高性能执行面。这套“数据与逻辑分离”的设计,也使其规则可测试、可审计、易维护。
进一步阅读:数据格式权威说明见 data/README.md,构建 target 一览见 BUILD.gn,命令行 flag 的完整定义见 src/main.cc,扫描主流程见 src/license_checker.cc。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00