Skip to content

本地Manifest定制

解释 local manifests 的加载顺序、project 新增与覆盖、remove/extend 语义、私有 remote 接入和可复现性要求。

基于android-17.0.0_r1
AndroidAOSPrepoManifest

本地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 实现入口 ​

推荐目录是:

text
.repo/
└── local_manifests/
    ├── 00-remotes.xml
    ├── 10-projects.xml
    └── 20-overrides.xml

git-repo 只加载该目录中以 .xml 结尾的文件,并按文件名排序。使用数字前缀不是语法要求, 但能让 remote、project 和 override 的先后关系更清晰。

不要继续使用历史上的单文件 .repo/local_manifest.xml 作为新配置入口。目录形式更容易拆分、 审查和版本管理。

1.2 加载顺序 ​

git-repo 先解析当前 manifest,再遍历 local_manifests 中排序后的 XML 文件,把每个文件解析结果 追加到节点列表:

源码文件:git-repo/manifest_xml.py

相关函数/类型:XmlManifest._Load

python
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
<?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
<?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>

核心字段:

字段含义建议
nameremote 上的 project 名称与服务器仓库路径一致
path工作区相对路径小写、明确且不与现有 project 冲突
remoteremote 名称必须引用已定义 remote
revisionbranch、tag 或 commit研究基线优先固定 tag/commit
groupsproject 分组用于 init/list/forall 的 group 选择
clone-depthproject 级浅克隆深度确认不需要完整历史后再设置

如果省略 path,默认使用 project name。私有仓名称包含组织前缀时,显式 path 通常更清晰。

2.3 10-projects.xml ​

来自 10-projects.xml 的 project 会自动属于 local::10-projects group。可以用它进行筛选:

bash
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
<?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 限定目标:

xml
<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 悄悄覆盖已经变化的上游基线:

xml
<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

python
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 ​

xml
<manifest>
    <remove-project name="platform/external/example" />
</manifest>

只写 name 时,会移除所有同名 project。只写 path 时,移除该工作区路径匹配的 project。 同时写 name 和 path 时,只有两个条件都匹配才移除。

xml
<remove-project
    name="platform/example/shared"
    path="vendor/example/shared" />

4.2 optional作用 ​

默认情况下,移除不存在的 project 会导致 manifest 解析失败:

xml
<remove-project
    name="platform/external/optional-component"
    optional="true" />

optional="true" 适合上游不同 release 中可能存在也可能不存在的 project。它不应被用来掩盖 拼写错误;稳定基线下预期必须存在的 project,应保留默认失败行为。

4.3 实现入口 ​

替换 project 的典型方式是先移除,再新增同 path project:

xml
<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_existoptional 移除无匹配时得到空 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
<?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。需要团队复现时,可以:

  1. 建立独立、访问受控的 manifest 仓库;
  2. 保存不含秘密的 XML;
  3. 为 XML 版本打 tag 或固定 commit;
  4. 在初始化说明中记录如何部署到 .repo/local_manifests;
  5. 同时保存 revision-locked manifest 作为结果快照。

不要只在聊天记录中粘贴一段 XML。它缺少版本、审查和变更原因。

8. 验证最终manifest ​

8.1 导出合并结果 ​

bash
# 输出 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验证 ​

bash
repo list --all
repo list -g 'local::10-projects'
repo list <project-name-or-path>

8.3 同步验证 ​

验证 XML 后先缩小同步范围:

bash
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日常命令实战。