(注:本文转载自群英会博客,原文作者蒋鑫。蓝色文字为我的备注)
Android 为企业提供一个新的市场,无论大企业,小企业都是处于同一个起跑线上。研究 Android 尤其是 Android 系统核心或者是驱动的开发,首先需要做的就是本地克隆建立一套 Android 版本库管理机制。
Android 使用 Git 作为代码管理工具,开发了 Gerrit 进行代码审核以便更好的对代码进行集中式管理,还开发了 Repo 命令行工具,对 Git 部分命令封装,将 百多个 Git 库有效的进行组织。要想克隆和管理这百多个 Git 库,还真不是一件简单的事情。
在研究 Repo 的过程中,发现很多文档在 Google Group 上,非“翻墙”不可看。非法的事情咱不干,直接阅读 repo 的代码吧。
创建本地 Android 版本库镜像的思路如果了解了 Repo 的实现,参考 《Using Repo and Git》 , 建立一个本地的 android 版本库镜像还是不难的:
- 下载 repo bootstrap 脚本
- $ curl http://android.git.kernel.org/repo >~/bin/repo
- $ chmod a+x ~/bin/repo
- $ export PATH=$PATH:~/bin
- 提供 -mirror 参数调用 repo init ,建立 git 版本库克隆
- $ repo init -u git://android.git.kernel.org/platform/manifest.git --mirror
- 开始和源同步
- $ repo sync
- 修改 manifest ,修改 git 库地址,指向本地的 git 服务器
- 修改 platform/manifest.git 库中现有的 xml 文件,或者创建一个新的 xml 文件
- 将 git 的地址改为本地地址,提交并 push
- 本地 repo 镜像建立完毕之后,就可以在执行 repo init 时,使用本地更改后的 manifest 库,之后执行 repo sync 就是基于本地版本库进行同步了。
- 也可以改造 repo,使得不必为 repo 工具初始化,也在本地网络完成操作…
实际上,得到客户使用 repo 的信息后,首先下载 repo 执行脚本开始研究。
- curl http://android.git.kernel.org/repo >~/bin/repo
难道只有 600 行的 python 代码么?要是这样应该很简单的呀。可以看下来,却发现远非如此。
Shell script or python?首先 repo 脚本使用了一个魔法:从脚本第一行的 shebang 来看应该是 shell 脚本,但是满眼却都是 python 语法,怎么回事?
- 1 #!/bin/sh
- 2
- 3 ## repo default configuration
- 4 ##
- 5 REPO_URL='git://android.git.kernel.org/tools/repo.git'
- 6 REPO_REV='stable'
- 7
- 8 # Copyright (C) 2008 Google Inc.
- ...
-
- 22 magic='--calling-python-from-/bin/sh--'
- 23 """exec" python -E "$0" "$@" """#$magic"
- 24 if __name__ == '__main__':
- 25 import sys
- 26 if sys.argv[-1] == '#%s' % magic:
- 27 del sys.argv[-1]
- 28 del magic
魔法就在第 23 行,巧妙的通过 python 三引号字串写出了一个能被 python 和 shell script 都能理解的代码,以此为界,代码由 Shell 脚本进入了 Python 的世界。
为什么没有直接采用“#!/usr/bin/env python”形式的shebang?
因为repo的作者不希望将任何与repo无关的python环境变量引入repo进程,所以在执行python脚本时需要加上-E选项。然而,Mac OS和Linux均不支持“#!/usr/bin/env python -E"这种形式的shebang。所以,才引入了如上所示的非常巧妙,但又十分晦涩的用法。
【We need to pass the -E flag, but env on some platforms wasn't taking it. So I cooked up this work around. It mostly had to do with our internal desktops at Google; they have a lot of PYTHON environment flags that we didn't want to inherit into the repo process (because they are there for normal Google engineers, not Android Google engineers), and at least at the time env on either Mac OS or Linux (I can't remember which) was rejecting a shbang line of "#!/usr/bin/env python -E".】
通过 curl 下载的的 repo 并非完整的 repo 脚本,只是一个 bootstrap。当 repo 执行时,会负责下载完整的 repo 代码,并将控制权转移给真正的 repo。
通过 main 函数,可以看到 repo 运行的开始,就试图发现本地真正的完整的 repo 代码,以便移交控制权:
- 544 def main(orig_args):
- 545 main, dir = _FindRepo()
-
- 586 try:
- 587 os.execv(main, me)
其中 545 行的 _FindRepo() 会在当前目录开始向上递归查找 “.repo/repo/main.py”,如果找到则移交控制权(587行)。
Repo bootstrap 脚本调用 init 只完成第一阶段的初始化Repo 的 bootstrap 脚本只支持两个命令 help 和 init,而 init 也只完成 repo 版本库克隆(即安装 repo 完整工具),之后就转移控制权。
在 Repo bootstrap 执行 init 可以提供很多参数,但实际上第一阶段初始化,只用到两个参数(而且都有默认值)
- 参数:–repo-url=URL
repo 工具本身的 git 库地址。缺省为:git://android.git.kernel.org/tools/repo.git - 参数:–repo-branch=REVISION
使用 repo 的版本库,即 repo git 库的分支或者里程碑名称。缺省为 stable
执行第二阶段的 repo init,控制权已经移交给刚刚克隆出来的 repo git 库的脚本。
Repo git 库被克隆/检出到执行 repo init 命令当前目录下的 .repo/repo 子目录中,主要的执行脚本为 .repo/repo/main.py。main.py 接着执行 repo init 命令。
Repo 的代码组织的非常好,在 .repo/repo/subcmds/ 子目录下,是各个 repo 命令的处理脚本。repo init 的第二阶段脚本正是由 .repo/repo/subcmds/init.py 负责执行的。第二阶段主要完成:
- 克隆由 -u 参数提供的 manifest Git 库,如克隆 android 库时:
- $ repo init -u git://android.git.kernel.org/platform/manifest.git
- 如果不提供 -b REVISION 或者 –manifest-branch=REVISION参数,则检出 manifest Git 库的 master 分支
- 如果不提供 -m NAME.xml 或者 –manifest-name=NAME.xml 参数,则使用缺省值 default.xml
- 如果提供 –mirror 参数,则后续同步操作会有相应的体现
Android 源码网站在介绍 repo 的使用模型中,有一个图片: http://source.android.com/images/git-repo-1.png , 介绍了 repo 的使用流程。其中 “repo start” 是紧接着 “repo sync” 后的第一个动作。那么这个动作是干什么的呢?
得益于 repo 对 git 操作的封装,”repo start” 命令的处理代码只有区区 68 行。
- 37 def Execute(self, opt, args):
-
- 41 nb = args[0]
-
- 47 projects = []
- 48 if not opt.all:
- 49 projects = args[1:]
-
- 54 all = self.GetProjects(projects)
-
- 57 for project in all:
-
- 59 if not project.StartBranch(nb):
- 60 err.append(project)
看到第 59 行了么,就是对 repo 同步下来的项目的多个 Git 版本库,逐一执行 project.StartBranch 操作。 nb 是 repo start 的第一个参数,即分支名称。
关于 StartBranch 的代码,在 project.py 中:
- 857 def StartBranch(self, name):
- 858 """Create a new branch off the manifest's revision.
- 859 """
-
- 894 if GitCommand(self,
- 895 ['checkout', '-b', branch.name, revid],
- 896 capture_stdout = True,
- 897 capture_stderr = True).Wait() == 0:
- 898 branch.Save()
- 899 return True
原来如此, repo start <branch_name> 就是逐一为各个版本库创建工作分支,以便在此分支下进行工作。
读者可以按图索骥,找到 repo 各个命令的实现,破解心中的疑惑。