当前位置: 首页 > 工具软件 > pwd.sh > 使用案例 >

build/envsetup.sh 简介

公冶同
2023-12-01

看了好多篇关于build/envsetup.sh 的介绍,记下的总结

build/envsetup.sh

每次开始编译开始的第一个命令便是source build/envsetup.sh,其中source命令就是用于运行shell脚本命令,功能等价于”.”,因此该命令也等价于. build/envsetup.sh。在文件envsetup.sh声明了当前会话终端可用的命令,这里需要注意的是当前会话终端,也就意味着每次新打开一个终端都必须再一次执行这些指令。build/envsetup.sh文件存在的意义就是,设置一些环境变量和shell函数为后续的编译工作做准备。 接下来我们分析下
envsetup.sh.

命令————envsetup.sh中的函数简介

导入envsetup.sh系统会都出一些命令,先看下命令的作用

常用的命令

编译指令解释
mm编译当前路径下所有模块,但不包含依赖
mmm [module_path]编译指定路径下所有模块,但不包含依赖
mma编译当前路径下所有模块,且包含依赖
mmma [module_path]编译指定路径下所有模块,且包含依赖
make [module_name]无参数,则表示编译整个Android代码
cgrep当前目录下所有C/C++文件执行搜索操作
jgrep当前目录下所有Java文件执行搜索操作
ggrep当前目录下所有Gradle文件执行搜索操作
mangrep [keyword]当前目录下所有AndroidManifest.xml文件执行搜索操作
mgrep [keyword]当前目录下所有Android.mk文件执行搜索操作
sepgrep [keyword]所有sepolicy文件执行搜索操作
resgrep [keyword]当前目录下所有本地res/*.xml文件执行搜索操作
sgrep [keyword]当前目录下基于(c
croot切换至Android根目录
cproj切换至工程的根目录
godir [filename]跳转到包含某个文件的目录
hmm查询所有的指令help信息

Tips: Android源码非常庞大,直接采用grep来搜索代码,不仅方法笨拙、浪费时间,而且搜索出很多无意义的混淆结果。根据具体需求,来选择合适的代码搜索指令,能节省代码搜索时间,提高搜索结果的精准度,方便定位目标代码。
godir [filename] 第一次执行速度较慢,之后运作效率比find命令高(具体见后面解析)

  • 其他指令
    make clean:执行清理操作,等价于 rm -rf out/
    make update-api:更新API,在framework API改动后需执行该指令,Api记录在目录frameworks/base/api

source build/envsetup.sh 执行流程

envsetup.sh 定义了很多函数,除此之外还执行了其它操作,以下是除去函数部分的代码::

VARIANT_CHOICES=(user userdebug eng)# 提前定义3种编译模式,供后面使用

# Clear this variable.  It will be built up again when the vendorsetup.sh
# files are included at the end of this file.
#LUNCH_MENU_CHOICES是供用户选择的prodcut列表,
#每次source build/envsetup.sh时需重置变量LUNCH_MENU_CHOICES
#不然后续的include vendor/cm/vendorsetup.sh时会继续添加产品至变量LUNCH_MENU_CHOICES里,
#导致出现很多重复产品
unset LUNCH_MENU_CHOICES   

#处理逻辑就是先从LUNCH_MENU_CHOICES中循环查找,看存不存在要添加的板型,如果存在就直接返回,如果不存在就添加到LUNCH_MENU_CHOICES中

function add_lunch_combo()
{
    local new_combo=$1
    local c
    for c in ${LUNCH_MENU_CHOICES[@]} ; do
        if [ "$new_combo" = "$c" ] ; then
            return
        fi
    done
    LUNCH_MENU_CHOICES=(${LUNCH_MENU_CHOICES[@]} $new_combo)
}



 #默认会加载如下6个板型选项
# add the default one here
add_lunch_combo aosp_arm-eng
add_lunch_combo aosp_arm64-eng
add_lunch_combo aosp_mips-eng
add_lunch_combo aosp_mips64-eng
add_lunch_combo aosp_x86-eng
add_lunch_combo aosp_x86_64-eng 

# 这行代码前定义了lunch和_lunch函数,这行代码的意思是通过_lunch函数实现lunch命令的自动补全功能
complete -F _lunch lunch 

#shell检查和警告,这里只支持bash,如果是其他的shell会发出这个WARNING
if [ "x$SHELL" != "x/bin/bash" ]; then
    case `ps -o command -p $$` in
        *bash*)
            ;;
        *)
            echo "WARNING: Only bash is supported, use of other shell would lead to erroneous results"
            ;;
    esac
fi

# Execute the contents of any vendorsetup.sh files we can find.
#source vendor和device下能找到的所有vendorsetup.sh
for f in `test -d device && find -L device -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort` \
         `test -d vendor && find -L vendor -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort`
do
    echo "including $f"
    . $f
done
unset f

addcompletions

最最后看看addcompletions干了啥

function addcompletions()
{
    local T dir f

    # Keep us from trying to run in something that isn't bash.
    if [ -z "${BASH_VERSION}" ]; then
        return
    fi

    # Keep us from trying to run in bash that's too old.
    if [ ${BASH_VERSINFO[0]} -lt 3 ]; then
        return
    fi

    dir="sdk/bash_completion"
    if [ -d ${dir} ]; then
        for f in `/bin/ls ${dir}/[a-z]*.bash 2> /dev/null`; do
            echo "including $f"
            . $f
        done
    fi
}

如果BASH的版本为空或者小于3都直接返回,否则打印android/sdk/bash_completion/目录下的以.bash结尾的所有文件,执行.bash文件,实现Linux终端下adb命令的补全

至此, source build/envsetup.sh的过程就分析完了。

build/envsetup.sh 常用函数介绍

lunch流程

在source流程之后,紧接着就是执行lunch操作,lunch操作执行的其实就是build/envsetup.sh脚本中的lunch函数,直接看代码:

function lunch()
{
    local answer

    #如果lunch命令后跟有参数,则直接赋给answer变量;
    #如果lunch命令后没有参数,则调用函数print_lunch_menu打印出板型列表供用户选择,并将用户的选择存储在answer变量中。
    if [ "$1" ] ; then
        answer=$1
    else
        print_lunch_menu #函数的作用就是打印数组LUNCH_MENU_CHOICES的内容
        echo -n "Which would you like? [aosp_arm-eng] "
        read answer
    fi

    local selection=


    #如果answer为空,则selection默认赋值为aosp_arm-eng;
    #如果answer是纯数字,则将answer作为数组下标从LUNCH_MENU_CHOICES数组中取出板型名称;
    #如果anwser是字符串,并且字符串使用”-”连接,而且”-”连接的前后两个子串中都没有”-”,则认为是板型名称字符串,直接赋给selection。
    if [ -z "$answer" ]
    then
        selection=aosp_arm-eng
    elif (echo -n $answer | grep -q -e "^[0-9][0-9]*$")
    then
        if [ $answer -le ${#LUNCH_MENU_CHOICES[@]} ]
        then
            selection=${LUNCH_MENU_CHOICES[$(($answer-1))]}
        fi
    elif (echo -n $answer | grep -q -e "^[^\-][^\-]*-[^\-][^\-]*$")
    then
        selection=$answer
    fi

    #如果selection仍然为空,则直接报错并退出
    if [ -z "$selection" ]
    then
        echo
        echo "Invalid lunch combo: $answer"
        return 1
    fi

    export TARGET_BUILD_APPS=


    #从selection中取出“-”前面的字符串到product,
    local product=$(echo -n $selection | sed -e "s/-.*$//")
    #检查product的合法性,涉及到make 不展开
    check_product $product
    if [ $? -ne 0 ]
    then
        echo
        echo "** Don't have a product spec for: '$product'"
        echo "** Do you have the right repo manifest?"
        product=
    fi


    #再从selection中取出“-”后面的字符串到variant,然后调用函数check_variant判断variant是否符合要求。check_variant函数很简单,主要就是用到前文source流程中初始化为"user userdebug eng"的VARIANT_CHOICES数组,判断variant是否是数组成员之一,是则返回0,不是则返回1。
    local variant=$(echo -n $selection | sed -e "s/^[^\-]*-//")
    check_variant $variant
    if [ $? -ne 0 ]
    then
        echo
        echo "** Invalid variant: '$variant'"
        echo "** Must be one of ${VARIANT_CHOICES[@]}"
        variant=
    fi

    if [ -z "$product" -o -z "$variant" ]
    then
        echo
        return 1
    fi

    #导出以下准备好的宏变量供整个shell环境使用
    export TARGET_PRODUCT=$product
    export TARGET_BUILD_VARIANT=$variant
    export TARGET_BUILD_TYPE=release

    echo

    #设置PROMPT_COMMAND,ANDROID_BUILD_PATHS,JAVA_HOME和BUILD_ENV_SEQUENCE_NUMBER等等环境变量
    set_stuff_for_environment

    #打印最终准备好的环境变量
    printconfig
} 

Tips:set_stuff_for_environment 里面的set_java_home函数,可以根据实际情况修改java路径等,这样可以避免编译不同的android版本还需手动切换java版本

_lunch函数

实现lunch命令的补全的步骤:

首先定义了lunch函数

然后定义了_lunch函数

使用complete命令

# Tab completion for lunch.
function _lunch()
{
    local cur prev opts
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"

    COMPREPLY=( $(compgen -W "${LUNCH_MENU_CHOICES[*]}" -- ${cur}) )
    return 0
}

++complete -F _lunch lunch++ 是上面代码中最关键的一行。

当bash在遇到lunch这个词的时候,会调用_lunch函数。
该函数会传入三个参数:要补全的命令名、当前的光标所在的词、当前光标所在的词的前一个词。
补全的结果需要存储到COMPREPLY变量中,以待bash获取。

gettop

function gettop
{
    local TOPFILE=build/core/envsetup.mk
    if [ -n "$TOP" -a -f "$TOP/$TOPFILE" ] ; then
        # The following circumlocution ensures we remove symlinks from TOP.
        (cd $TOP; PWD= /bin/pwd)
    else
        if [ -f $TOPFILE ] ; then
            # The following circumlocution (repeated below as well) ensures
            # that we record the true directory name and not one that is
            # faked up with symlink names.
            PWD= /bin/pwd
        else
            local HERE=$PWD
            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
}

-n 变量是否存在

-a 逻辑与

-f 文件是否存在

PWD 打印当前目录

这个函数主要是获取Android源码的根目录,根据根目录下的build/core/envsetup.mk文件来判断的。

注意:调用这个函数的时候有个要求:就是你必须在源码根目录的子目录中,如果你处在根目录的上级目录则调用失败,因为这个函数是通过不停的调用cd ..命令,然后通过文件build/core/envsetup.mk来判断源码根目录的。

hmm


function hmm() {
cat <<EOF
Invoke ". build/envsetup.sh" from your shell to add the following functions to your environment:
- lunch:   lunch <product_name>-<build_variant>
- tapas:   tapas [<App1> <App2> ...] [arm|x86|mips|armv5|arm64|x86_64|mips64] [eng|userdebug|user]
- croot:   Changes directory to the top of the tree.
- m:       Makes from the top of the tree.
- mm:      Builds all of the modules in the current directory, but not their dependencies.
- mmm:     Builds all of the modules in the supplied directories, but not their dependencies.
           To limit the modules being built use the syntax: mmm dir/:target1,target2.
- mma:     Builds all of the modules in the current directory, and their dependencies.
- mmma:    Builds all of the modules in the supplied directories, and their dependencies.
- cgrep:   Greps on all local C/C++ files.
- ggrep:   Greps on all local Gradle files.
- jgrep:   Greps on all local Java files.
- resgrep: Greps on all local res/*.xml files.
- mangrep: Greps on all local AndroidManifest.xml files.
- sepgrep: Greps on all local sepolicy files.
- sgrep:   Greps on all local source files.
- godir:   Go to the directory containing a file.

Environemnt options:
- SANITIZE_HOST: Set to 'true' to use ASAN for all host modules. Note that
                 ASAN_OPTIONS=detect_leaks=0 will be set by default until the
                 build is leak-check clean.

Look at the source to view more functions. The complete list is:
EOF
    T=$(gettop)
    local A
    A=""
    for i in `cat $T/build/envsetup.sh | sed -n "/^[ \t]*function /s/function \([a-z_]*\).*/\1/p" | sort | uniq`; do
      A="$A  $i"
    done
    echo $A
}

帮助函数,cat $T/build/envsetup.sh | sed -n “/^[ \t]function /s/function ([a-z_]).*/\1/p” | sort | uniq 这句脚本主要是获取envsetup.sh里面的以function开头的函数,函数名都是字母,并将函数名打印出来。
sed -n “/^[ \t]*function /p” 是打印出以function 开头的行
s/function ([a-z_])./\1 这部分是将函数名提取出来
sort 排序
uniq 去重

mm

function mm()
{
    local T=$(gettop)
    local DRV=$(getdriver $T)
    # If we're sitting in the root of the build tree, just do a
    # normal make.
    if [ -f build/core/envsetup.mk -a -f Makefile ]; then
        $DRV make $@
    else
        # Find the closest Android.mk file.
        local M=$(findmakefile)
        local MODULES=
        local GET_INSTALL_PATH=
        local ARGS=
        # Remove the path to top as the makefilepath needs to be relative
        local M=`echo $M|sed 's:'$T'/::'`
        if [ ! "$T" ]; then
            echo "Couldn't locate the top of the tree.  Try setting TOP."
            return 1
        elif [ ! "$M" ]; then
            echo "Couldn't locate a makefile from the current directory."
            return 1
        else
            for ARG in $@; do
                case $ARG in
                  GET-INSTALL-PATH) GET_INSTALL_PATH=$ARG;;
                esac
            done
            if [ -n "$GET_INSTALL_PATH" ]; then
              MODULES=
              ARGS=GET-INSTALL-PATH
            else
              MODULES=all_modules
              ARGS=$@
            fi
            ONE_SHOT_MAKEFILE=$M $DRV make -C $T -f build/core/main.mk $MODULES $ARGS
        fi
    fi
}

如果当前处在源码根目录下,且存在Makefile文件,直接执行make $@命令。

如果不是在源码根目录下:循环调用cd .. 调用findmakefile函数查找Android.mk文件,直到根目录,将Android.mk文件的绝对路径返回给M.

如找到了:M=/home/admin/android/frameworks/base/Android.mk,调用: local M=echo $M|sed 's}'$T'/}}'这句就是将源码根目录/home/admin/android/去掉,取得相对目录后,M=frameworks/base/Android.mk

最后执行:ONE_SHOT_MAKEFILE=frameworks/base/android.mk make -C /home/admin/android all_modules

相当于make -C /home/admin/android all_modules ONE_SHOT_MAKEFILE=frameworks/base/android.mk

接下来会调用到:源码根目录下的Makefile中,我们发现内容是include build/core/main.mk,即走到了build/core/main.mk文件中。后面就设计到Makefile的问题,我们后面会分析。现在只需要知道mm命令是怎么调用makefile里面去就行

mmm

mmm 命令就是从指定目录下开始编译所有模块

function mmm()
{
    local T=$(gettop)
    local DRV=$(getdriver $T)
    if [ "$T" ]; then
        local MAKEFILE=
        local MODULES=
        local ARGS=
        local DIR TO_CHOP
        local GET_INSTALL_PATH=
        local DASH_ARGS=$(echo "$@" | awk -v RS=" " -v ORS=" " '/^-.*$/')
        local DIRS=$(echo "$@" | awk -v RS=" " -v ORS=" " '/^[^-].*$/')
        for DIR in $DIRS ; do
            MODULES=`echo $DIR | sed -n -e 's/.*:\(.*$\)/\1/p' | sed 's/,/ /'`
            if [ "$MODULES" = "" ]; then
                MODULES=all_modules
            fi
            DIR=`echo $DIR | sed -e 's/:.*//' -e 's:/$::'`
            if [ -f $DIR/Android.mk ]; then
                local TO_CHOP=`(\cd -P -- $T && pwd -P) | wc -c | tr -d ' '`
                local TO_CHOP=`expr $TO_CHOP + 1`
                local START=`PWD= /bin/pwd`
                local MFILE=`echo $START | cut -c${TO_CHOP}-`
                if [ "$MFILE" = "" ] ; then
                    MFILE=$DIR/Android.mk
                else
                    MFILE=$MFILE/$DIR/Android.mk
                fi
                MAKEFILE="$MAKEFILE $MFILE"
            else
                case $DIR in
                  showcommands | snod | dist | incrementaljavac | *=*) ARGS="$ARGS $DIR";;
                  GET-INSTALL-PATH) GET_INSTALL_PATH=$DIR;;
                  *) echo "No Android.mk in $DIR."; return 1;;
                esac
            fi
        done
        if [ -n "$GET_INSTALL_PATH" ]; then
          ARGS=$GET_INSTALL_PATH
          MODULES=
        fi
        ONE_SHOT_MAKEFILE="$MAKEFILE" $DRV make -C $T -f build/core/main.mk $DASH_ARGS $MODULES $ARGS
    else
        echo "Couldn't locate the top of the tree.  Try setting TOP."
        return 1
    fi
}

首先T=$(gettop)获取源码根目录,

local DASH_ARGS=$(echo "$@" | awk -v RS=" " -v ORS=" " '/^-.*$/') 

根据mmm后面的参数获取以空格隔开,以-开头的字符串,如:mmm -B frameworks/base 则DASH_ARGS = -B

local DIRS=$(echo "$@" | awk -v RS=" " -v ORS=" " '/^[^-].*$/') 

根据mmm后面参数获取以空格隔开,不以-开头的字符串。如:DIRS=frameworks/base/

DIR=`echo $DIR | sed -e 's:/$::'` 

去掉DIR结尾的/,DIR=frameworks/base

if [ -f $DIR/Android.mk ] ;then
TO_CHOP=`(cd -P -- $T && pwd -P) | wc -c | tr -d ' '`

跳到源码根目录,pwd -P打印出来,获取到字符串长度如:/home/admin/androids TO_CHOP就是20

TO_CHOP=`expr $TO_CHOP + 1`

START=`PWD= /bin/pwd`

MFILE=`echo $START | cut -c${TO_CHOP}-`

获取当前目录字符串,去掉前面21个字符,如:当前目录为/home/admin/androids,则MFILE=”“

  if [ "$MFILE" = "" ] ; then
    MFILE=$DIR/Android.mk
   else
    MFILE=$MFILE/frameworks/base/Android.mk 
 fi

这里进行判断当前目录是否就是源码目录,如果是的话,MFILE为空,所以MFILe=frameworks/base/Android.mk

否则:MFILE=$MFILE/frameworks/base/Android.mk

总之:就是把当前目录的前面源码根目录去了,然后加上后面mmm -B **参数部分相对的目录,最后加上Android.mk就是,最后Android.mk文件的相对路径。

ONE_SHOT_MAKEFILE=”MAKEFILE” make -C T T DASH_ARGS all_modules $ARGS

相当于:make -C /home/admin/android -B all_modules ONE_SHOT_MAKEFILE=frameworks/base/Android.mk

godir

function godir () {
    #如果没有带参数直接提示错误返回
    if [[ -z "$1" ]]; then
        echo "Usage: godir <regex>"
        return
    fi

    T=$(gettop)
    #如果OUT_DIR 不等于空创建OUT_DIR这个目录,设置FILELIST文件路径
    if [ ! "$OUT_DIR" = "" ]; then
        mkdir -p $OUT_DIR
        FILELIST=$OUT_DIR/filelist
    else
        FILELIST=$T/filelist
    fi

    #判断filelist是否存在,不存在新建
    if [[ ! -f $FILELIST ]]; then
        echo -n "Creating index..."

        # 使用find命令从编译的根目录下查找文件"-type f"(目录out和.repo除外),并将结果输出到$FILELIST文件中
        # out目录和.repo目录的排除选项分别为:
        # - "-wholename ./out -prune"
        # - "-wholename ./.repo -prune"
        # 关于find的"-wholename pattern"选项,其行为跟"-path pattern"基本一样,具体可以查看find的帮助信息
        # 因此filelist文件保存了除out和.repo目录外其余目录的完整文件名

        (\cd $T; find . -wholename ./out -prune -o -wholename ./.repo -prune -o -type f > $FILELIST)
        echo " Done"
        echo ""
    fi
    local lines

    # 根据传入godir的参数,在filelist中搜索,并用sed处理后将结果存放在lines中
    # 操作"sed -e 's/\/[^/]*$//'"仅保留完整文件名的路径部分
    lines=($(\grep "$1" $FILELIST | sed -e 's/\/[^/]*$//' | sort | uniq))

     # 检查lines中的结果,即filelist通过grep和sed操作后,是否还有匹配的目录
    if [[ ${#lines[@]} = 0 ]]; then
        echo "Not found"
        return
    fi
    local pathname
    local choice
    # 如果lines的结果多于1行,则对各行进行编号并输出
    if [[ ${#lines[@]} > 1 ]]; then
        while [[ -z "$pathname" ]]; do
            # 从1开始编号
            local index=1
            local line
            for line in ${lines[@]}; do
                # 对每行以类似以下的格式进行输出:
                printf "%6s %s\n" "[$index]" $line
                # 序号自增
                index=$(($index + 1))
            done
            echo
            echo -n "Select one: "
            unset choice
             # 读取输入序号
            read choice
            if [[ $choice -gt ${#lines[@]} || $choice -lt 1 ]]; then
                echo "Invalid choice"
                continue
            fi
            # 取得输入序号对应的目录
            pathname=${lines[$(($choice-1))]}
        done
    else
        # 如果符合匹配的路径只有一条,则直接将匹配的路径存放到pathname中
        pathname=${lines[0]}
    fi
    跳转到
    \cd $T/$pathname
}

godir 是在编译路径下搜索匹配模式的目录,然后跳转到此目录,同时可以用来快速查找文件的路径
如像查找frameworks/base/core/res/res/values在那些地方被overlay 了直接godir frameworks/base/core/res/res/values/ 查找包含这个路径的所有文件夹,及所有可能被覆盖的地方

sgrep

case `uname -s` in
    Darwin)
        function sgrep()
        {
            find -E . -name .repo -prune -o -name .git -prune -o  -type f -iregex '.*\.(c|h|cc|cpp|S|java|xml|sh|mk|aidl)' -print0 | xargs -0 grep --color -n "$@"
        }

        ;;
    *)
        function sgrep()
        {   
            # 排除 .repo, .git目录
            # 并对后缀(c|h|cc|cpp|S|java|xml|sh|mk|aidl|vts)的文件执行grep模式搜索
            find . -name .repo -prune -o -name .git -prune -o  -type f -iregex '.*\.\(c\|h\|cc\|cpp\|S\|java\|xml\|sh\|mk\|aidl\)' -print0 | xargs -0 grep --color -n "$@"
        }
        ;;
esac

这里为什么没有把out目录去了表示无法理解

其他代码搜索原理类似就不一一列举

 类似资料: