AOSP 源码阅读环境与检索方法论源码剖析(Android 16 / aosp-main)
概述
AOSP 树体量非常大。直接用 grep -r 会扫进 .repo、.git、out,非常慢。官方的做法是:先 source build/envsetup.sh,把 build/soong/bin 等目录挂进 PATH,再用按后缀裁剪过的检索脚本(cgrep / jgrep / mgrep / sgrep 等)定位符号与调用点。
本文基于A16走读:
- 理清环境如何被
envsetup.sh装配; cgrep族工具的实现与用法;godir/afind/hmm/ IDEGen 等辅助手段;- 会提到一点
ctags/cscope
前置知识
- 已阅读并理解我发布的博客序章 01:工作树由 Manifest 拼装,顶层存在
build/envsetup.sh等 linkfile。 - 会在 Linux/WSL shell 里执行
. script.sh(注意点号,把函数导入当前 shell)。 - 知道
find的-prune可跳过目录,-exec ... {} +可批量喂给grep。 - 区分两类工具:
- 树内脚本 (随 AOSP 检出):
build/soong/bin/cgrep等。 - 宿主机工具 (需自行安装):
ctags、cscope、IDE 等。
- 树内脚本 (随 AOSP 检出):
核心数据结构
1. 源码树"顶"的判定锚点
环境脚本用固定相对路径识别树根:
bash
# AOSP aosp-main
# 文件:build/make/shell_utils.sh
function gettop
{
local TOPFILE=build/make/core/envsetup.mk
if [ -n "${TOP:-}" -a -f "${TOP:-}/$TOPFILE" ] ; then
(cd "$TOP"; PWD= /bin/pwd)
else
if [ -f $TOPFILE ] ; then
PWD= /bin/pwd
else
local HERE=$PWD
local T=
while [ \( ! \( -f $TOPFILE \) \) -a \( "$PWD" != "/" \) ]; do
\cd ..
T=`PWD= /bin/pwd -P`
done
\cd "$HERE"
if [ -f "$T/$TOPFILE" ]; then
echo "$T"
fi
fi
fi
}
含义:
- 优先采用环境变量
TOP(且其下确有build/make/core/envsetup.mk); - 否则从当前目录向上爬,直到找到该文件;
- 用
PWD= /bin/pwd -P消解符号链接,得到真实物理路径。
build/envsetup.sh 开头用 _gettop_once 做同样判定;失败则直接返回,并提示必须从树根 source。
2. 全局 PATH 注入列表
set_global_paths 维护 ANDROID_GLOBAL_BUILD_PATHS,把检索与辅助脚本目录配置到 PATH环境变量,配置过java环境变量的应该都懂:
| 追加路径 | 作用 |
|---|---|
$T/build/soong/bin |
cgrep / jgrep / hmm / m 等 |
$T/build/bazel/bin |
Bazel 相关入口 |
$T/development/scripts |
开发辅助脚本 |
$T/prebuilts/devtools/tools |
预编译开发工具 |
| 可选 emulator / dtc 等 | 视宿主 OS 与目录是否存在 |
3. 按语言/文件类型裁剪的 grep 族
hmm 列出的检索入口(节选)与脚本对应关系:
| 命令 | 脚本 | 检索范围(后缀/模式) |
|---|---|---|
cgrep |
build/soong/bin/cgrep |
*.c *.cc *.cpp *.h *.hpp |
jgrep |
build/soong/bin/jgrep |
*.java *.kt |
mgrep |
build/soong/bin/mgrep |
Makefile / *.mk / *.bp 等 |
sgrep |
build/soong/bin/sgrep |
C/Java/XML/mk/bp/aidl/proto 等"源码面" |
resgrep |
build/soong/bin/resgrep |
res/ 下 *.xml |
mangrep |
build/soong/bin/mangrep |
AndroidManifest.xml |
sepgrep |
build/soong/bin/sepgrep |
名为 sepolicy 的目录树 |
ggrep |
build/soong/bin/ggrep |
*.gradle |
共性:一律 find 时 -prune 掉 .repo、.git、out。
4. 目录索引与 IDE 工程
| 机制 | 位置 | 用途 |
|---|---|---|
godir |
仍在 build/envsetup.sh(shell 函数) |
基于 filelist 按路径正则跳转目录 |
afind |
build/soong/bin/afind |
带 prune 的 find 包装 |
| IDEGen | development/tools/idegen/ |
生成 IntelliJ / Eclipse 工程文件 |
调用链路
纯文本调用链路:
text
. build/envsetup.sh
├─ _gettop_once() # 找树根;失败则 return 1
├─ source build/make/shell_utils.sh # 提供 gettop 等
├─ ...(lunch 等,本篇不展开)
├─ unset cgrep jgrep hmm ... # 丢掉历史同名 shell 函数
├─ validate_current_shell
├─ set_global_paths
│ └─ PATH=$T/build/soong/bin:$T/build/bazel/bin:...:$PATH
├─ source_vendorsetup # 可选加载 device/vendor 的 vendorsetup.sh
└─ addcompletions
之后用户在任意子目录:
cgrep / jgrep / mgrep / sgrep ...
└─ find . -name .repo -prune -o ... -exec grep --color -n "$@" {} +
godir <regex>
└─ 维护/读取 filelist → 选目录 → cd
hmm
└─ 打印 envsetup 能力说明 + 枚举 function 名
源码逐段分析
1. 装配入口:必须从树根 source
bash
# AOSP aosp-main
# 文件:build/envsetup.sh
function _gettop_once
{
local TOPFILE=build/make/core/envsetup.mk
if [ -n "$TOP" -a -f "$TOP/$TOPFILE" ] ; then
(cd "$TOP"; PWD= /bin/pwd)
else
if [ -f $TOPFILE ] ; then
PWD= /bin/pwd
else
local HERE=$PWD
local T=
while [ \( ! \( -f $TOPFILE \) \) -a \( "$PWD" != "/" \) ]; do
\cd ..
T=`PWD= /bin/pwd -P`
done
\cd "$HERE"
if [ -f "$T/$TOPFILE" ]; then
echo "$T"
fi
fi
fi
}
T=$(_gettop_once)
if [ ! "$T" ]; then
echo "Couldn't locate the top of the tree. Always source build/envsetup.sh from the root of the tree." >&2
return 1
fi
IMPORTING_ENVSETUP=true source $T/build/make/shell_utils.sh
解读:
- 找不到树根就
return 1,后续函数不会被装载上,也就不会生效。 IMPORTING_ENVSETUP=true让shell_utils.sh跳过部分"独立脚本模式"断言,避免嵌套冲突。- 实际操作:在源码根目录执行
. build/envsetup.sh(或source build/envsetup.sh)。
文件末尾按固定顺序收尾:
bash
# AOSP aosp-main
# 文件:build/envsetup.sh
validate_current_shell
set_global_paths
source_vendorsetup
addcompletions
2. 把检索脚本挂进 PATH
bash
# AOSP aosp-main
# 文件:build/envsetup.sh
function set_global_paths()
{
local T=$(gettop)
if [ ! "$T" ]; then
echo "Couldn't locate the top of the tree. Try setting TOP."
return
fi
if [ -n "$ANDROID_GLOBAL_BUILD_PATHS" ] ; then
export PATH=${PATH/$ANDROID_GLOBAL_BUILD_PATHS/}
fi
ANDROID_GLOBAL_BUILD_PATHS=$T/build/soong/bin
ANDROID_GLOBAL_BUILD_PATHS+=:$T/build/bazel/bin
ANDROID_GLOBAL_BUILD_PATHS+=:$T/development/scripts
ANDROID_GLOBAL_BUILD_PATHS+=:$T/prebuilts/devtools/tools
# ... Linux 下追加 dtc/libufdt;可选 emulator ...
export PATH=$ANDROID_GLOBAL_BUILD_PATHS:$PATH
}
要点:
- 先将旧值从
PATH去掉,再写入新值,支持反复source而不无限拉长PATH。 cgrep等能直接敲命令,靠的是$T/build/soong/bin被前置,而不是 shellalias。
3. 历史同名函数必须 unset
本树 envsetup.sh 明确把旧实现unset掉了:
bash
# AOSP aosp-main
# 文件:build/envsetup.sh
# These functions used to be here but are now standalone scripts
# in build/soong/bin. Unset these for the time being so the real
# script is picked up.
# TODO: Remove this some time after a suitable delay (maybe 2025?)
unset allmod
unset aninja
unset cgrep
unset ggrep
unset hmm
unset jgrep
unset mangrep
unset mgrep
unset resgrep
unset sepgrep
unset sgrep
# ... 以及 mm/mmm 等构建相关入口 ...
若仍残留同名 shell 函数,会遮蔽 PATH 里的脚本。unset 之后,type cgrep 应指向 .../build/soong/bin/cgrep。
4. cgrep:C/C++ 面检索
bash
# AOSP aosp-main
# 文件:build/soong/bin/cgrep
find . -name .repo -prune -o -name .git -prune -o -name out -prune -o -type f \( \
-name '*.c' \
-o -name '*.cc' \
-o -name '*.cpp' \
-o -name '*.h' \
-o -name '*.hpp' \
\) -exec grep --color -n "$@" {} +
exit $?
执行语义:
- 从当前工作目录 起向下找(不是强制从树根);读 Framework 时通常先
cd frameworks/base再jgrep,缩小范围。 .repo/.git/out被 prune,不进入。- 仅匹配列出的 C/C++ 后缀;参数原样传给
grep(可用-n以外的grep选项,脚本已带--color -n)。 exit $?透传grep/find状态,便于脚本判断"有无命中"。
同类脚本只改后缀集合。例如 jgrep:
bash
# AOSP aosp-main
# 文件:build/soong/bin/jgrep
find . -name .repo -prune -o -name .git -prune -o -name out -prune -o -type f \( \
-name '*.java' \
-o -name '*.kt' \
\) -exec grep --color -n "$@" {} +
exit $?
mgrep 面向构建描述:
bash
# AOSP aosp-main
# 文件:build/soong/bin/mgrep
find . -name .repo -prune -o -name .git -prune -o -name out -prune -o -type f \( \
-name 'Makefile' \
-o -name 'Makefile.*' \
-o -name '*.make' \
-o -name '*.mak' \
-o -name '*.mk' \
-o -name '*.bp' \
\) -exec grep --color -n "$@" {} +
exit $?
sgrep 覆盖更广的"源码后缀并集",适合不确定语言边界时的第一枪;代价是更慢、噪声更大。
5. resgrep / mangrep / sepgrep:目录语义不同
resgrep 先找名为 res 的目录,再在其下搜 *.xml:
bash
# AOSP aosp-main
# 文件:build/soong/bin/resgrep
for dir in `find . -name .repo -prune -o -name .git -prune -o -name out -prune -o -name res -type d`; do
find $dir -type f -name '*\.xml' -exec grep --color -n "$@" {} +
done
mangrep 只盯 AndroidManifest.xml。sepgrep 进入名为 sepolicy 的目录做递归 grep,并排除其中的 .git。
6. afind:带剪枝的 find
bash
# AOSP aosp-main
# 文件:build/soong/bin/afind
set -f
dir=${1:-.}
shift
args=( $@ )
if [[ ${#args[@]} -eq 0 ]] ; then
args=( -print )
fi
find "$dir" -name .repo -prune -o -name .git -prune -o -name out -prune -o ${args[@]}
exit $?
用途:按文件名/路径枚举时,避免踩进 .repo 与 out。set -f 关闭 glob,防止参数被 shell 提前展开。
7. godir:文件路径索引跳转
bash
# AOSP aosp-main
# 文件:build/envsetup.sh
function godir () {
if [[ -z "$1" ]]; then
echo "Usage: godir <regex>"
return
fi
local T=$(gettop)
local FILELIST
if [ ! "$OUT_DIR" = "" ]; then
mkdir -p $OUT_DIR
FILELIST=$OUT_DIR/filelist
else
FILELIST=$T/filelist
fi
if [[ ! -f $FILELIST ]]; then
echo -n "Creating index..."
(\cd $T; find . -wholename ./out -prune -o -wholename ./.repo -prune -o -type f > $FILELIST)
echo " Done"
fi
local lines
lines=($(\grep "$1" $FILELIST | sed -e 's/\/[^/]*$//' | sort | uniq))
# 0 条 → Not found;多条 → 交互选择;1 条 → 直接 cd
\cd $T/$pathname
}
要点:
- 索引是全树文件路径列表,不是 tags 数据库。
- 同步后若大量增删文件,需删掉
filelist(或$OUT_DIR/filelist)重建。 - 匹配的是路径字符串;
sed去掉最后一段文件名,跳转的是目录。
croot 则简单回到树根或树根下相对路径:
bash
# AOSP aosp-main
# 文件:build/envsetup.sh
function croot()
{
local T=$(gettop)
if [ "$T" ]; then
if [ "$1" ]; then
\cd $(gettop)/$1
else
\cd $(gettop)
fi
else
echo "Couldn't locate the top of the tree. Try setting TOP."
fi
}
8. hmm:能力清单
bash
# AOSP aosp-main
# 文件:build/soong/bin/hmm
# ... 打印 lunch/croot/m/mm 以及 cgrep/jgrep/... 说明 ...
T=$(gettop)
A=""
for i in `cat $T/build/envsetup.sh | sed -n "/^[[:blank:]]*function /s/function \([a-z_]*\).*/\1/p" | sort | uniq`; do
A="$A $i"
done
echo $A
前半是固定帮助文本;后半扫描 envsetup.sh 里仍以 function 形式存在的名字。注意:已迁到 soong/bin 的 cgrep 等不会出现在后半枚举里,但会出现在前半说明中。
9. IDEGen:用 IDE 读代码树,比较适合做框架开发的,方便跳转代码
text
# AOSP aosp-main
# 文件:development/tools/idegen/README
From the project's root directory...
Repeat these steps after each sync...
1) make (to produce generated .java source)
2) development/tools/idegen/idegen.sh
3) Open android.ipr in IntelliJ.
阅读向用法:
- 需要跳转生成代码时,至少要有一次能产出 gen 源的构建(完整产品编译属后续篇章)。
- IDEA 需加大堆内存与
idea.max.intellisense.filesize,README 写明 Android 树体积带来的限制。 - 每次
repo sync后建议重跑idegen.sh。
10. 阅读方法论
推荐固定节奏(均可用本树脚本落地):
- 定模块目录 :
godir ActivityManagerService或afind frameworks/base -name '*AMS*'一类;不确定时先mgrep模块名。 - 定符号定义/引用 :Java/Kotlin 用
jgrep;Native 用cgrep;跨语言用sgrep;构建归属用mgrep。 - 收窄 cwd :在
frameworks/base下搜,而不是每次从整树顶扫。 - 顺着调用链 :命中文件后,在同目录继续
jgrep/cgrep追调用方;跨进程入口再换目录。 - 清单类文件 :权限/组件查
mangrep;资源 id/布局查resgrep;SELinux 查sepgrep。
11. ctags / cscope:宿主机索引
与 AOSP 树对接时,常见用法是(示意,需自行 apt install 等安装主机包):
bash
# 在树根生成 tags(示例;具体选项随 universal-ctags/cscope 版本变化)
afind . -name '*.c' -o -name '*.h' -o -name '*.cpp' -o -name '*.java' \
> cscope.files
# 再交给本机 ctags/cscope 读该文件列表
定位与跳转仍建议优先用官方 cgrep/jgrep;ctags/cscope 适合编辑器内"跳到定义"的索引加速,二者互补,不是替代关系。
边界场景与异常分支
-
未从树根 source
_gettop_once失败 →return 1,cgrep不在 PATH,表现为command not found。 -
source 了但旧函数未 unset(极老本地改动)
同名 shell 函数会压过
soong/bin脚本。本树通过批量unset规避;若自行改回 function 实现,行为以 function 为准。 -
在错误 cwd 下检索
脚本从
.开始 find。在很深的小目录搜"全树符号"会漏;在树根搜又会慢。应用croot/cd主动选范围。 -
godir无索引或索引过期无
filelist时现场find建索引;sync 后路径变更可能导致"假命中",删索引文件即可重建。 -
godir多命中进入交互选择;输入越界打印
Invalid choice并继续循环;0 命中打印Not found。 -
resgrep目录名恰好叫res的非资源树凡目录名匹配
res都会进入,可能带进无关 XML;结果需人工甄别。 -
source_vendorsetup加载厂商脚本会扫描
device/vendor/product下vendorsetup.sh。若存在allowed-vendorsetup_sh-files白名单则只加载名单内文件;多个白名单文件则全部不加载并告警。【不属于 AOSP 原生,是厂商扩展】的逻辑常藏在这些脚本里,阅读时注意区分。 -
非 bash/zsh
validate_current_shell对其他 shell 仅警告;补全与部分行为可能异常。
AOSP 原生 vs 厂商可定制点
| 类别 | 说明 |
|---|---|
| AOSP 原生 | envsetup.sh、shell_utils.sh、build/soong/bin/*grep、godir、afind、hmm、development/tools/idegen |
| 合法本地习惯 | 自装 ctags/cscope/rg、编辑器插件、个人 alias(勿与 unset 列表冲突) |
| 厂商扩展 | vendorsetup.sh 里追加的产品/路径/私有检索函数;厂商 IDE 插件与闭源索引服务 |
小结
- 阅读环境的第一动作是在树根执行
. build/envsetup.sh,由_gettop_once+set_global_paths把build/soong/bin挂进PATH。 - 本树中
cgrep/jgrep/mgrep/sgrep等已是独立脚本;envsetup.sh用unset防止旧函数遮蔽。 - 这类脚本的核心价值是:统一 prune
.repo/.git/out,并按后缀收窄扫描面。 godir/afind/croot/hmm/idegen分别解决跳转、枚举、回根、帮助与 IDE 工程生成。ctags/cscope可与afind文件列表配合做编辑器索引。
下一篇进入模块 1 第 1 篇:build/envsetup.sh 与 lunch 的产品选择机制