Android编译命令

说在前面

从最开始接触Android系统开始,每次进行代码编译都需要网上搜索编译指令。后来大致熟悉了Android的编译体系,加深了对Android编译的理解。

编译流程

编译 android 系统的流程,首先执行 source build/envsetup.sh,然后执行 lunch 选择板级配置,最后执行 make 编译

  • source build/envsetup.sh 流程

脚本中的第一个函数是hmm,介绍了脚本的一些功能,cat <<EOF 表示把 EOF 中的内容当做文件打印到标准输出,包含了脚本的使用说明和功能介绍。

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.
- provision: Flash device with all required partitions. Options will be passed on to fastboot.
- 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.
- mgrep:     Greps on all local Makefiles files.
- sepgrep:   Greps on all local sepolicy files.
- sgrep:     Greps on all local source files.
- godir:     Go to the directory containing a file.

Environment 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
    local T=$(gettop)
    local A=""
    local i
    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
}

函数只有在调用时才会执行,在执行 source build/envsetup.sh 时该函数并不会被调用,而只有在终端运行 hmm 时才会输出,输出使用说明和脚本中所有函数名称,但显然不够准确。

[android_8.1]$ source build/envsetup.sh
including device/rockchip/rk3399/vendorsetup.sh
including sdk/bash_completion/adb.bash
[android_8.1]$ hmm
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.
- provision: Flash device with all required partitions. Options will be passed on to fastboot.
- 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.
- mgrep:     Greps on all local Makefiles files.
- sepgrep:   Greps on all local sepolicy files.
- sgrep:     Greps on all local source files.
- godir:     Go to the directory containing a file.

Environment 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:
addcompletions add_lunch_combo build_build_var_cache cgrep check_product check_variant choosecombo chooseproduct choosetype choosevariant core coredump_enable coredump_setup cproj croot destroy_build_var_cache findmakefile get_abs_build_var getbugreports get_build_var getdriver getlastscreenshot get_make_command getprebuilt getscreenshotpath getsdcardpath gettargetarch gettop ggrep godir hmm is isviewserverstarted jgrep key_back key_home key_menu lunch _lunch m make mangrep mgrep mm mma mmm mmma pez pid printconfig print_lunch_menu provision qpid rcgrep resgrep runhat runtest sepgrep set_java_home setpaths set_sequence_number set_stuff_for_environment settitle sgrep smoketest stacks startviewserver stopviewserver systemstack tapas tracedmdump treegrep _wrap_build

下面先挑出只在函数外的内容进行分析

(1)
VARIANT_CHOICES=(user userdebug eng)

(2)
unset LUNCH_MENU_CHOICES

(3)
# 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

(4)
# Execute the contents of any vendorsetup.sh files we can find.
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`          `test -d product && find -L product -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort`
do
    echo "including $f"
    . $f
done
unset f

(5) addcompletions

(1) VARIANT_CHOICES 定义为一个数组,包含3种模式可选,在某些脚本函数中会使用到。

(2) unset LUNCH_MENU_CHOICES 删除给定的环境变量内容,重新进行设置

(3) 调用 add_lunch_combo 函数, 并传入参数,看下函数的具体实现

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)
}

这里对 LUNCH_MENU_CHOICES 数组的内容进行循环,与函数后传入的参数进行对比,如果数组中已经包含传入的参数项,直接返回;否则就把传入的参数加入到数组项中。

(4)检查是否存在 devicevendorproduct 目录,如果存在的话,就在目录下查找脚本 vendorsetup.sh,并设定了查找深度为4层目录,2> /dev/null 这里将标准错误重定向到/dev/null,也就是不输出,然后把找到的脚本文件进行排序,最后调用找到的脚本文件。当前的脚本文件内容如下:

including device/rockchip/rk3399/vendorsetup.sh

add_lunch_combo rk3399-userdebug
add_lunch_combo rk3399-user
add_lunch_combo rk3399_mid-userdebug
add_lunch_combo rk3399_mid-user
add_lunch_combo rk3399pro-userdebug
add_lunch_combo rk3399pro-user
add_lunch_combo rk3399_box-userdebug
add_lunch_combo rk3399_box-user

(5) 查找 sdk/bash_completion 目录下所有以 .bash 结尾的文件,当前仅有一个

including sdk/bash_completion/adb.bash

前面已经分析过这个函数了,实际上还是把这些板级配置添加到数组中,而通过运行 lunch 命令时进行选择。

  • lunch 流程

通过执行 lunch 选择合适的板级配置

function lunch()
{
    local answer

    if [ "$1" ] ; then
        answer=$1
    else
        print_lunch_menu
        echo -n "Which would you like? [aosp_arm-eng] "
        read answer
    fi

    local 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
    else
        selection=$answer
    fi

    export TARGET_BUILD_APPS=

    local product variant_and_version variant version

    product=${selection%%-*} # Trim everything after first dash
    variant_and_version=${selection#*-} # Trim everything up to first dash
    if [ "$variant_and_version" != "$selection" ]; then
        variant=${variant_and_version%%-*}
        if [ "$variant" != "$variant_and_version" ]; then
            version=${variant_and_version#*-}
        fi
    fi
    
    if [ -z "$product" ]
    then
        echo
        echo "Invalid lunch combo: $selection"
        return 1
    fi

    TARGET_PRODUCT=$product     TARGET_BUILD_VARIANT=$variant     TARGET_PLATFORM_VERSION=$version     build_build_var_cache
    if [ $? -ne 0 ]
    then
        return 1
    fi

    export TARGET_PRODUCT=$(get_build_var TARGET_PRODUCT)
    export TARGET_BUILD_VARIANT=$(get_build_var TARGET_BUILD_VARIANT)
    if [ -n "$version" ]; then
      export TARGET_PLATFORM_VERSION=$(get_build_var TARGET_PLATFORM_VERSION)
    else
      unset TARGET_PLATFORM_VERSION
    fi
    export TARGET_BUILD_TYPE=release

    echo

    set_stuff_for_environment
    printconfig
    destroy_build_var_cache
}

首先判断有没有传入参数,如果有参数就把值赋给answer变量,否则调用 print_lunch_menu 打印板级配置菜单项,并接受用户终端输入。

function print_lunch_menu()
{
    local uname=$(uname)
    echo
    echo "You're building on" $uname
    echo
    echo "Lunch menu... pick a combo:"

    local i=1
    local choice
    for choice in ${LUNCH_MENU_CHOICES[@]}
    do
        echo "     $i. $choice"
        i=$(($i+1))
    done

    echo
}

该函数遍历 LUNCH_MENU_CHOICES 数组,输出每一项内容,而这些数组项则是通过调用 add_lunch_combo 函数添加进来的。

如果传入的参数为零,selection 变量就会赋值为 aosp_arm-eng,否则会输出answer的值,并从该值中搜索以2位数开始的字符串,如果找到的话,再进一步对这个数字进行数组越界的检查。如果这个数字小于 LUNCH_MENU_CHOICES 的数组项,就会把 LUNCH_MENU_CHOICES 的这一项赋给 selection 变量。

如果 selection 为空,则直接报错退出。

然后根据获取的数组项,进行字符串截取,从 selection 中取出 - 前面的字符串保存到 product,后面的字符串保存到 variant_and_version,然后再对 variant_and_version 进行字符串截取,保存到 variant

然后根据以上的数据赋值给 TARGET_PRODUCTTARGET_BUILD_VARIANTTARGET_PLATFORM_VERSION等,然后调用 build_build_var_cache 对编译时必需的环境变量进行赋值和处理。

最后
调用 set_stuff_for_environment 函数设置编译环境的变量,包括 ANDROID_BUILD_PATHSJAVA_HOME等。
调用 printconfig 打印最终准备好的环境变量。参考如下:

============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=8.1.0
TARGET_PRODUCT=rk3399
TARGET_BUILD_VARIANT=userdebug
TARGET_BUILD_TYPE=release
TARGET_PLATFORM_VERSION=OPM1
TARGET_BUILD_APPS=
TARGET_ARCH=arm64
TARGET_ARCH_VARIANT=armv8-a
TARGET_CPU_VARIANT=cortex-a53
TARGET_2ND_ARCH=arm
TARGET_2ND_ARCH_VARIANT=armv7-a-neon
TARGET_2ND_CPU_VARIANT=cortex-a15
HOST_ARCH=x86_64
HOST_2ND_ARCH=x86
HOST_OS=linux
HOST_OS_EXTRA=Linux-3.10.0-862.el7.x86_64-x86_64-with-centos-7.5.1804-Core
HOST_CROSS_OS=windows
HOST_CROSS_ARCH=x86
HOST_CROSS_2ND_ARCH=x86_64
HOST_BUILD_TYPE=release
BUILD_ID=OPM8.181205.001
OUT_DIR=out
AUX_OS_VARIANT_LIST=
============================================

调用 destroy_build_var_cache 清除不需要的中间环节产生的变量值。

  • make 流程

谷歌在 Android 7.0 开始引入了 ninja 进行系统编译,相对于 Makefile 组织编译系统来说,ninja 在大的项目管理的速度和并行方面有突出的优势。但是现有的 Android 还是由 Makefile 组织,所以谷歌引入了 kati soong,将 Android.mk 文件转化成 ninja 文件,使用 ninja 文件对编译系统进行管理。

但从 Android 8.0 开始,引入了 Android.bp 文件来替代之前的 Android.mk 文件。与 Android.mk 文件不同,Android.bp 只是纯粹的配置文件,不包括分支、循环等流程控制,它使用 blueprint 框架来解析,最终转换成 ninja 文件。

Android 9.0 则强制使用 Android.bp 来构建编译。

blueprint 是生成、解析 Android.bp 的工具,是 soong 的一部分。 soong 则是专为 Android 编译而设计的工具,blueprint 只是解析文件的形式,而 soong 则解释内容的含义。

Android.mk 可以通过 soong 提供的 androidmk 转换成 Android.bp,但仅限简单配置。目前 Android 8.0 的编译流程中,仍然是使用 kati 来做的转换。

Android.bp --> blueprint --> soong --> ninja
Makefile && Android.mk --> kati --> ninja
Android.mk --> soong --> blueprint --> Android.bp

现存的 Android.mkAndroid.bp 都会被转换为 ninjaAndroid.mk 和其他的 Makefile,会生成out/build-<product_name>.ninja文件,而Android.bp则会生成out/soong/build.ninja。此外,还会生成一个较小的out/combined-<product_name>.ninja文件,负责把二者结合起来,作为执行入口。

当执行 make 的时候,会查找当前目录价的 Makefile 文件并执行,内容如下:

### DO NOT EDIT THIS FILE ###
include build/core/main.mk
### DO NOT EDIT THIS FILE ###

所以,真正的 Makefile 入口是 build/core/main.mk

而我们在执行编译的时候并没有传入目标,那么就一定会有一个默认的目标,在 build/core/main.mk 能够找到如下内容:

ifndef KATI

host_prebuilts := linux-x86
ifeq ($(shell uname),Darwin)
host_prebuilts := darwin-x86
endif

.PHONY: run_soong_ui
run_soong_ui:
    +@prebuilts/build-tools/$(host_prebuilts)/bin/makeparallel --ninja build/soong/soong_ui.bash --make-mode $(MAKECMDGOALS)

.PHONY: $(MAKECMDGOALS)
$(sort $(MAKECMDGOALS)) : run_soong_ui
    @#empty

else # KATI

其中 MAKECMDGOALS 这个命名是 make 执行时后面的参数赋值,也就是我们执行任何 make 的时候都是执行 run_soong_ui 这个目标,这样 androidMakefile 切换为 soong 进行了编译流程,后面跟 make 就没有关系了。

  • mm 流程

当我们单独编译某个模块时,可以在这个模块目录下输入 mm 命令进行编译,流程和make差不多,只不过目标是单独模块组成,会生成独立的ninja文件。

function mm()
{
    local T=$(gettop)
    local DRV=$(getdriver $T)
    # If we're sitting in the root of the build tree, just do a
    # normal build.
    if [ -f build/soong/soong_ui.bash ]; then
        _wrap_build $DRV $T/build/soong/soong_ui.bash --make-mode $@
    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
            local ARG
            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-IN-$(dirname ${M})
              ARGS=${ARGS//\//-}
            else
              MODULES=MODULES-IN-$(dirname ${M})
              # Convert "/" to "-".
              MODULES=${MODULES//\//-}
              ARGS=$@
            fi
            if [ "1" = "${WITH_TIDY_ONLY}" -o "true" = "${WITH_TIDY_ONLY}" ]; then
              MODULES=tidy_only
            fi
            ONE_SHOT_MAKEFILE=$M _wrap_build $DRV $T/build/soong/soong_ui.bash --make-mode $MODULES $ARGS
        fi
    fi
}

整个编译体系的Makefile大致可区分为:系统核心的Makefile,产品级的Makefile,具体模块的Makefile.

  • Android.mk语法

在源码树中每一个模块的根目录下都有一个Android.mk文件,编译系统正是以模块为单位进行编译的,每个模块有唯一的模块名,一个模块可以依赖多个其他模块,模块之间的依赖关系是通过模块名来引用的(熟悉OpenWRT或者Buildroot编译体系的同学对此应该不会陌生)。

frameworks/native/cmds/servicemanager/Android.mk 为例,解析下具体的语法格式:

LOCAL_PATH:= $(call my-dir) //设置为当前文件夹所在路径

svc_c_flags =       -Wall -Wextra -Werror \ //设置svc_c_flags编译选项

ifneq ($(TARGET_USES_64_BIT_BINDER),true)
ifneq ($(TARGET_IS_64_BIT),true)
svc_c_flags += -DBINDER_IPC_32BIT=1
endif
endif

include $(CLEAR_VARS)   //清空编译环境的变量,可能由其他模块设置过的变量
LOCAL_SHARED_LIBRARIES := liblog    //当前模块在运行时依赖的动态库
LOCAL_SRC_FILES := bctest.c binder.c        //当前模块包含的所有源码
LOCAL_CFLAGS += $(svc_c_flags)  //设置CFLAGS编译选项
LOCAL_MODULE := bctest  //当前模块的名称,具有唯一性
LOCAL_MODULE_TAGS := optional   //当前模块的标签,设置为默认值(可能为debug,eng,user等)
include $(BUILD_EXECUTABLE) //编译成可执行程序

include $(CLEAR_VARS)
LOCAL_SHARED_LIBRARIES := liblog libcutils libselinux
LOCAL_SRC_FILES := service_manager.c binder.c
LOCAL_CFLAGS += $(svc_c_flags)
LOCAL_MODULE := servicemanager
LOCAL_INIT_RC := servicemanager.rc
include $(BUILD_EXECUTABLE)

除了上面的环境变量,编译系统还设置了很多其他的环境变量,如下:

环境变量 说明
LOCAL_PACKAGE_NAME 当前APK应用的名称(具有唯一性)
LOCAL_C_INCLUDES C/C++所需的头文件路径
LOCAL_STATIC_LIBRARIES 当前模块静态连接需要的库
LOCAL_STATIC_JAVA_LIBRARIES 当前模块依赖的Java静态库
LOCAL_JAVA_LIBRARIES 当前模块依赖的Java动态库
LOCAL_CERTIFICATE 签署当前应用的证书名称
BUILD_EXECUTABLE 编译成可执行程序
BUILD_STATIC_JAVA_LIBRARY 编译成Java静态库
BUILD_SHARED_LIBRARY 编译成C/C++动态库

此外,编译系统还定义了一些函数,如下:

    $(call all-java-files-under, ):获取指定目录下的所有Java文件;
    $(call all-c-files-under, ):获取指定目录下的所有C文件;
    $(call all-Iaidl-files-under, ) :获取指定目录下的所有AIDL文件;
    $(call all-makefiles-under, ):获取指定目录下的所有Make文件;
  • Android.bp语法

Android.bp 是一种配置文件,语法类似于JSON,文件中仅记录当前模块的信息,没有条件判断或者控制流语句,控制逻辑需要Go来处理。bp文件的语法类似于Bazel,由Go语言进行解析,转换为Ninja文件。

Bazel的参考文档

frameworks/base/Android.bp 文件为例,具体如下:

// ====  c++ proto device library  ==============================
cc_library {
    name: "libplatformprotos",
    host_supported: true,
    proto: {
        export_proto_headers: true,
        include_dirs: ["external/protobuf/src"],
    },

    target: {
        host: {
            proto: {
                type: "full",
            },
            srcs: [
                "core/proto/**/*.proto",
                "libs/incident/**/*.proto",
            ],
        },
        android: {
            proto: {
                type: "lite",
            },
            // We only build the protos that are optimized for the lite
            // runtime, as well as the only protos that are actually
            // needed by the device.
            srcs: [
                "core/proto/android/service/graphicsstats.proto",
            ],
            shared: {
                enabled: false,
            },
        },
    },
}

subdirs = [
    "core/jni",
    "libs/*",
    "media/*",
    "tools/*",
    "native/android",
    "native/graphics/jni",
]

optional_subdirs = [
    "core/tests/utiltests/jni",
]

定义一个模块从模块的类型开始,如例子中的"cc_library",模块会包含一些属性,格式为"{name: value}".

编译指令

代码编译

编译指令 说明
m 在源码树的根目录执行编译
mm 编译当前路径下所有模块,不包含依赖
mmm [module_path] 编译指定路径下所有模块,不包含依赖
mma 编译当前路径下所有模块,包含依赖
mmma [module_path] 编译指定路径下所有模块,包含依赖
make [module_name] 无参数,则表示编译整个Android代码

部分模块的编译指令:

# make help

Common make targets:
--------------------------------------------------------------------------------
droid                   Default target
clean                   (aka clobber) equivalent to rm -rf out/
snod                    Quickly rebuild the system image from built packages
vnod                    Quickly rebuild the vendor image from built packages
offline-sdk-docs        Generate the HTML for the developer SDK docs
doc-comment-check-docs  Check HTML doc links & validity, without generating HTML
libandroid_runtime      All the JNI framework stuff
framework               All the java framework stuff
services                The system server (Java) and friends
help                    You're reading it right now

#### build completed successfully  ####

模块 make mmm
init make init mmm system/core/init
zygote make app_process mmm frameworks/base/cmds/app_process
system_services make services mmm frameworks/base/services
java framework make framework mmm framworks/base
framework-res make framework-res mmm frameworks/base/core/res
jni framework make libandroid_runtime mmm frameworks/base/core/jni
binder make libbinder mmm frameworks/native/libs/binder

上述mmm命令同样适用于mm/mma/mmma,编译系统采用的是增量编译,只会编译发生变化的目标文件。

代码检索

Android源码非常庞大,直接使用grep/ag来搜索代码,可能会耗费大量时间。根据具体需求,可以选择合适的检索指令,有效节省代码搜索时间,提高准确度。

指令 说明
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.
mgrep Greps on all local Makefiles files.
sepgrep Greps on all local sepolicy files.
sgrep Greps on all local source files.

其他指令

上面介绍了常用的编译和检索指令,还有一些其他指令,可以执行 hmm 查询指令的帮助信息。

指令 说明
hmm 查询所有指令的帮助信息
gettop 查询Android源码的根目录
printconfig 打印设置的编译参数配置
print_lunch_menu 打印lunch可选的板级配置
godir 跳转到包含某个文件的目录
findmakefile 查询当前目录所在工程的Android.mk文件路径

Android编译命令

上一篇:Android开发学习记录--ListView使用


下一篇:H5播放器内置播放视频(兼容绝大多数安卓和ios)