本地Manifest定制
AOSP 的上游 manifest 描述一套公共源码布局,但实际开发经常需要在本地增加额外 project、固定 某个 project 的 revision、替换 remote,或者临时移除不参与当前产品的 project。直接修改 .repo/manifests/default.xml 会把个人定制混入 manifest project,也容易在后续更新时产生冲突。
git-repo 为此提供 .repo/local_manifests/。其中的 XML 文件会叠加到当前 manifest:
- 增加 remote 和 project;
- 使用
extend-project修改已有 project; - 使用
remove-project移除或替换已有 project; - 使用
include拆分较大的本地配置; - 通过 groups 控制 project 选择。
local manifest 很灵活,也很容易制造不可复现工作区。本文不仅介绍元素写法,还会说明文件加载顺序、 路径冲突、revision 固定、凭据安全和团队共享方式。
AOSP目录结构全景 已说明 project name 与 path, repo初始化与同步 已说明 manifest 和 sync 边界。本文继续说明如何 选择 project、extend-project 或 remove-and-replace,如何解释冲突,并用导出 manifest 与 单 project sync 验证定制结果。
1. Local manifest
1.1 实现入口
推荐目录是:
.repo/
└── local_manifests/
├── 00-remotes.xml
├── 10-projects.xml
└── 20-overrides.xmlgit-repo 只加载该目录中以 .xml 结尾的文件,并按文件名排序。使用数字前缀不是语法要求, 但能让 remote、project 和 override 的先后关系更清晰。
不要继续使用历史上的单文件 .repo/local_manifest.xml 作为新配置入口。目录形式更容易拆分、 审查和版本管理。
1.2 加载顺序
git-repo 先解析当前 manifest,再遍历 local_manifests 中排序后的 XML 文件,把每个文件解析结果 追加到节点列表:
源码文件:git-repo/manifest_xml.py
相关函数/类型:XmlManifest._Load
nodes = []
nodes.append(
self._ParseManifestXml(
self.manifestFile,
self.manifestProject.worktree,
parent_groups=parent_groups,
restrict_includes=False,
)
)
if self._load_local_manifests and self.local_manifests:
try:
# 说明:文件名排序决定多份 local manifest 的解析顺序。
for local_file in sorted(
platform_utils.listdir(self.local_manifests)
):
if local_file.endswith(".xml"):
local = os.path.join(
self.local_manifests, local_file
)
# 说明:每份文件自动获得 local::<filename> group。
local_group = {
f"{LOCAL_MANIFEST_GROUP_PREFIX}:"
f"{local_file[:-4]}"
}
nodes.append(
self._ParseManifestXml(
local,
self.subdir,
parent_groups=local_group | parent_groups,
restrict_includes=False,
)
)
except OSError:
pass排序只保证解析顺序,不表示后面的任意重复定义都会覆盖前面的定义。例如,同名 remote 属性不同会 触发解析错误;重复 project path 也可能产生冲突。需要修改已有 project 时,应优先使用 extend-project 或明确的 remove-and-replace。
下面的图展示 base manifest 和多份 local manifest 如何形成最终 project 表。
2. remote/project
2.1 fetch
当新增 project 不属于现有 remote 的 fetch 根地址时,先声明 remote:
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<remote
name="company"
fetch="ssh://git.example.com/"
review="https://review.example.com/" />
</manifest>fetch 是 project name 的 URL 前缀。假设 project name 是 platform/vendor/example,实际获取地址 通常由 remote fetch 和 project name 组合得到。
不要把用户名、密码、access token 或私钥内容写进 XML。认证应交给 SSH agent、SSH 客户端配置、 Git credential helper 或受控凭据系统。
2.2 新增 project
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<project
name="platform/vendor/example"
path="vendor/example"
remote="company"
revision="refs/heads/android-17"
groups="vendor,device" />
</manifest>核心字段:
| 字段 | 含义 | 建议 |
|---|---|---|
name | remote 上的 project 名称 | 与服务器仓库路径一致 |
path | 工作区相对路径 | 小写、明确且不与现有 project 冲突 |
remote | remote 名称 | 必须引用已定义 remote |
revision | branch、tag 或 commit | 研究基线优先固定 tag/commit |
groups | project 分组 | 用于 init/list/forall 的 group 选择 |
clone-depth | project 级浅克隆深度 | 确认不需要完整历史后再设置 |
如果省略 path,默认使用 project name。私有仓名称包含组织前缀时,显式 path 通常更清晰。
2.3 10-projects.xml
来自 10-projects.xml 的 project 会自动属于 local::10-projects group。可以用它进行筛选:
repo list -g 'local::10-projects'
repo forall -g 'local::10-projects' -c 'echo "$REPO_PATH"'显式 groups 与自动 local group 同时存在。自动 group 适合识别配置来源,显式 group 适合表达产品 和职责。
3. extend-project
3.1 extend边界
如果只需要修改 revision、remote、dest branch、upstream、groups 或本地 path,没有必要复制完整 project 定义。extend-project 更能适应上游 manifest 增加新属性。
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<extend-project
name="platform/frameworks/base"
revision="refs/tags/android-17.0.0_r1"
dest-branch="android-17.0.0_r1" />
</manifest>如果同一个 project name 在不同 path 出现多次,可以使用 path 限定目标:
<extend-project
name="platform/example/shared"
path="vendor/example/shared"
revision="refs/heads/product-release" />3.2 可修改的字段
| 字段 | 作用 |
|---|---|
path | 限定要修改的现有 project path |
dest-path | 改变工作区目标 path |
groups | 增加 groups |
revision | 覆盖 revision |
remote | 覆盖 remote |
dest-branch | 覆盖 repo upload 目标分支 |
upstream | 为固定 SHA 指定可查找的上游 ref |
base-rev | 检查被修改 project 的原 revision 是否符合预期 |
base-rev 很适合防止 local override 悄悄覆盖已经变化的上游基线:
<extend-project
name="platform/frameworks/native"
base-rev="refs/tags/android-17.0.0_r1"
revision="0123456789abcdef0123456789abcdef01234567" />如果原 project revision 不等于 base-rev,manifest 解析失败,提醒维护者重新评估 override。
3.3 remove边界
解析器要求 project 必须已经存在。它再按可选 path 筛选匹配项,并修改指定字段:
源码文件:git-repo/manifest_xml.py
相关函数/类型:XmlManifest._ParseManifest
if node.nodeName == "extend-project":
name = self._reqatt(node, "name")
if name not in self._projects:
# 说明:extend-project 不能用来创建新 project。
raise ManifestParseError(
"extend-project element specifies non-existent "
"project: %s" % name
)
path = node.getAttribute("path")
revision = node.getAttribute("revision")
remote_name = node.getAttribute("remote")
base_revision = node.getAttribute("base-rev")
for p in self._projects[name]:
if path and p.relpath != path:
continue
if revision:
if base_revision and p.revisionExpr != base_revision:
# 说明:base-rev 不匹配会进入统一失败列表。
failed_revision_changes.append(
"extend-project name %s mismatch base "
"%s vs revision %s"
% (name, base_revision, p.revisionExpr)
)
p.SetRevision(revision)
if remote_name:
p.remote = remote.ToRemoteSpec(name)
# ...这段逻辑说明 extend 是对已解析 project 表的修改,不是文本层面的 XML 覆盖。
4. remove-project
4.1 按 name
<manifest>
<remove-project name="platform/external/example" />
</manifest>只写 name 时,会移除所有同名 project。只写 path 时,移除该工作区路径匹配的 project。 同时写 name 和 path 时,只有两个条件都匹配才移除。
<remove-project
name="platform/example/shared"
path="vendor/example/shared" />4.2 optional作用
默认情况下,移除不存在的 project 会导致 manifest 解析失败:
<remove-project
name="platform/external/optional-component"
optional="true" />optional="true" 适合上游不同 release 中可能存在也可能不存在的 project。它不应被用来掩盖 拼写错误;稳定基线下预期必须存在的 project,应保留默认失败行为。
4.3 实现入口
替换 project 的典型方式是先移除,再新增同 path project:
<manifest>
<remove-project
name="platform/external/example"
path="external/example" />
<project
name="forks/platform-external-example"
path="external/example"
remote="company"
revision="refs/heads/product-release" />
</manifest>最终工作区 path 不变,但 remote、project name 和 revision 已经变化。文章和构建记录必须明确这是 fork,而不是 AOSP 原 project。
下面的图展示 extend 与 remove-and-replace 的选择。
4.4 remove
git-repo 的 tests/test_manifest_xml.py 覆盖了几条容易误用的边界:
| 测试 | 关键断言 | 覆盖边界 |
|---|---|---|
test_remove_one_project_doesnt_exist | 无匹配且非 optional 时抛出 ManifestParseError | 拼写或基线错误不会静默通过 |
test_remove_one_optional_project_doesnt_exist | optional 移除无匹配时得到空 project 表 | optional 只放宽“目标不存在” |
test_base_revision_checks_on_patching | 错误 base-rev 的 remove/extend 均失败 | override 能检测预期基线变化 |
test_extend_project_dest_path_multi_match | 多个同名 project 且无 path 限定时失败 | dest-path 不能模糊作用于多个 project |
这些测试验证的是 manifest 解析与对象表结果,不证明 remote 可访问、目标 revision 存在或 project 能够完成 checkout。因此 XML 单元测试之后仍需要导出最终 manifest 和执行目标 project sync。
5. Manifest配置
local manifest 可以 include 同目录中的其他 manifest:
<?xml version="1.0" encoding="UTF-8"?>
<manifest>
<include name="remotes.xml" />
<include name="projects.xml" groups="product-x" />
</manifest>被 include 的文件必须本身是可解析 manifest。groups 会附加到 included manifest 中的 project 和 extend-project,并递归传递给更深 include。
适合拆分的维度包括:
- remote 定义;
- 平台公共 project;
- 产品或设备 project;
- revision override;
- 临时实验 project。
不要形成多层循环 include,也不要把加载顺序依赖隐藏在难以理解的文件名中。
6. Manifest 冲突
6.1 Project入口
两个不同 project 不能同时占用同一 worktree path。替换来源时必须先 remove 原 project,再新增。 仅添加一个同 path project 通常会导致冲突。
路径还应避免:
- 绝对路径;
..等越界片段;- 与 repo 管理目录重叠;
- 大小写不敏感文件系统上的大小写碰撞;
- 与 linkfile/copyfile 目标冲突。
6.2 Manifest入口
多份 local manifest 可以引用同一个 remote,但不能用相同 remote name 定义不同属性。需要连接 不同服务器时,应使用不同 name。
6.3 Revision约束
| Revision 类型 | 优点 | 风险 |
|---|---|---|
| 稳定 tag | 读者可理解,通常适合 release | 仍需解析到 project commit |
| 固定 commit | 不可变,适合复现 | 需要 upstream 才能配合精简 fetch |
| release branch | 可以接收维护更新 | 内容会随远端变化 |
| main | 便于跟踪最新开发 | 不适合作为稳定文章事实基线 |
需要长期复现时应优先使用稳定 tag 加固定 commit。必须使用 branch 时,应同时记录当时 commit, 不能只写 branch 名。
7. 私有仓与凭据安全
7.1 XML边界
可以保存:
- SSH/HTTPS remote 地址;
- review server 地址;
- project name、path 和 revision;
- groups、upstream 和 dest branch。
不能保存:
- access token;
- 密码;
- 私钥内容;
- 带凭据的 URL;
- 内网账号和个人身份信息。
认证应通过 SSH agent、credential helper、.netrc 或团队凭据系统处理。local manifest 即使不提交, 也可能被日志、备份或压缩包带走。
7.2 团队共享方式
.repo/local_manifests 通常不属于任何 AOSP project。需要团队复现时,可以:
- 建立独立、访问受控的 manifest 仓库;
- 保存不含秘密的 XML;
- 为 XML 版本打 tag 或固定 commit;
- 在初始化说明中记录如何部署到
.repo/local_manifests; - 同时保存 revision-locked manifest 作为结果快照。
不要只在聊天记录中粘贴一段 XML。它缺少版本、审查和变更原因。
8. 验证最终manifest
8.1 导出合并结果
# 输出 base manifest 与 local manifests 合并后的结果
repo manifest --pretty -o combined-manifest.xml
# 输出当前 HEAD 固定后的结果
repo manifest -r --pretty -o locked-manifest.xml
# 对比忽略 local manifests 时的基础结果
repo manifest --no-local-manifests --pretty -o base-manifest.xml对比 base 与 combined 可以确认新增、移除和 override 是否符合预期。
repo manifest 的输出是验证 local manifest 是否进入最终输入的直接消费者。测试时应先导出 --no-local-manifests,再导出默认结果,断言只在后者出现 local project、override revision 或 remove-project 效果;如果两份输出相同,说明文件可能不在 .repo/local_manifests、XML 未通过解析,或被 group 条件排除。
8.2 Manifest验证
repo list --all
repo list -g 'local::10-projects'
repo list <project-name-or-path>8.3 同步验证
验证 XML 后先缩小同步范围:
repo sync -j1 --fail-fast vendor/example确认 remote、revision、path 和 checkout 都正确,再扩大到其他 project。
这一步的状态边界是“manifest 已合并”与“Git worktree 已按合并结果更新”之间的提交屏障。repo manifest 成功并不代表 project 已 fetch;repo sync 成功也不代表其他未指定 project 被更新。读者可以故意把 local manifest 中的 revision 改成不存在的值,先断言导出阶段能显示该值,再断言目标 project 的 sync 在 fetch 阶段失败,证明错误发生在 Git 对象获取而不是 XML 合并。
下面的时序图给出一次安全变更流程。
9. 常见错误
9.1 上游修改
修改 .repo/manifests 会污染 manifest project 工作树,后续 init/sync 也可能覆盖或拒绝更新。 本地差异应进入 local manifests;需要成为团队正式产品 manifest 时,再提交到独立 manifest 仓库。
9.2 project复制
复制上游 project 定义容易漏掉新增属性,并与上游变化产生重复。只修改少数字段时优先 extend。
9.3 remove目标
optional="true" 会忽略无匹配目标。它适合跨版本兼容,不适合掩盖拼写错误。核心 project 替换 应让错误显式失败。
9.4 branch边界
local manifest 中的 branch 会继续移动。文章、构建和问题复现必须额外记录 commit。
9.5 local manifest
local manifest 只描述期望组合,真正检出的 HEAD 可能因同步失败、本地分支或未完成 checkout 而不同。 最终基线应由 repo manifest -r 和各 project HEAD 共同验证。
10. 可复现工作区
local manifest 的可靠性分成三层,三层不能互相替代:
| 层次 | 检查对象 | 能回答什么 |
|---|---|---|
| 声明层 | .repo/local_manifests/*.xml | 期望新增、覆盖或移除什么 |
| 解析层 | combined manifest、repo manifest -r | 合并后实际选择了哪些 path、remote 和 revision |
| 检出层 | repo list、project HEAD、目标 project sync | 工作区是否真的到达声明的 commit |
XML 解析成功只证明结构合法。remote 可能不可访问,revision 可能不存在,sync 也可能只完成 fetch 而 没有完成 checkout。反过来,工作区当前能编译也不能证明 manifest 可复现,因为本地 branch 和未提交 修改可能暂时补上了缺失内容。
长期维护时,应让敏感信息留在凭据系统,让 project path 保持唯一,让 revision 能固定到 commit,并 用 base-rev 暴露上游漂移。local manifest 的价值是把定制变成可审查配置,而不是给工作区增加一层 只有作者知道的隐含状态。
日常检查 project、分支和差异时,回到 repo日常命令实战。
