TWRP recovery 的强大功能使它成为刷机界的第一选择,常见品牌机型一般都有官方适配的 twrp,有些没有官方适配 twrp 的机型也有开发者制作了非官方版本。

然而开发者不是万能的,还是有很多机型没有人做适配。对于这种情况,你是坐等大神,还是自己动手丰衣足食?

当然是自己动手。

事实上,适配 twrp 并不如想像的那么难。理论上说,适配工作就是准备必要的安卓源码和运行编译命令。下面,笔者将根据经验一步一步介绍如何适配 recovery。

确认所需条件

编译 twrp 和编译安卓系统一样,非常消耗系统资源,所以需要保证计算机配置:

参数

需求

操作系统

64位 Linux,推荐 Ubuntu18.04

硬盘空间

最少 30GB,安卓源码非常吃空间

内存

最少 4GB,建议 8GB 或者更多

步骤1:准备编译环境

(1)安装必要的软件包

Twrp 编译需要一系列软件包,对于 Ubuntu 可以使用 apt 命令一次性安装:

#更新软件源
sudo apt update
#安装软件包
sudo apt install git-core gnupg flex bison gperf \
zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 \
lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z-dev ccache \
libgl1-mesa-dev libxml2-utils xsltproc unzip
#安装 openjdk
sudo apt install openjdk-8-jdk
#安装 build essential。如果报错,请改用第二行命令
sudo apt install build-essential
sudo apt install gcc g++ make

(2)设置 ccache

ccache 是缓存工具,临时存放编译 *.o 时生成的过程文件(预处理代码和输出文件),下次编译相同代码时就可以直接拷贝而不用再次生成,从而提高编译效率。在 make clean 后能看到明显效果,再编译时速度提高了很多。

波浪线“~”是用户目录(home)的简写,在 Bash 的 profile ~/.bashrc 末尾添加下面语句以启用 ccacheccache 默认存放在用户目录中(~/.ccache),可以通过修改环境变量 CCACHE_DIR 把它改到其它分区。

#启用 ccache
export USE_CCACHE=1
#改变 ccache 存放路径
export CCACHE_DIR=/mnt/seagate_drive/.ccache

重启终端或者运行 source ~/.bashrc,或者打开新的 bash 会话,使以上语句生效。

另外,可以通过设置 ccache 来改变 cache 占用空间的大小:

ccache -M 50G

步骤2:下载安卓源代码

编译 twrp 离不开安卓源码,因为它依赖安卓源码中的组件。推荐使用OmniROM,它是 teamwin(twrp 的开发团队)的官方合作伙伴。 最新版 OmniROM twrp 源码

(1)下载 repo

repo 是一款软件仓库管理工具,由谷歌用 Python2.7 编写,用于批量管理由 GIT 组织的源码。使用下面命令把 repo 下载到 /usr/bin目录下:

sudo curl https://storage.googleapis.com/git-repo-downloads/repo > /usr/bin/repo
sudo chmod +x /usr/bin/repo

(2)下载 omnirom 源码

首先建一个文件夹用于存放 ominirom 源码,然后用 repo init 初始化源码库。

mkdir omni9
cd omni9
repo init -u git://github.com/omnirom/android.git -b android-9.0

-b 参数定义了需要的安卓版本。一般 3.0.x 适用于 android-6.0, android-7.0 以版本则需要更新的 twrp,强烈建议选择最新版——3.3.1-0,配套使用 android-8.1android-9.0

初始化完毕后下载源码:

repo sync

根据网络和计算机状况,下载过程需要几个小时甚至超过半天,请耐心等待。下载完成后,除了 omni9,还有 .repo 都将出现在文件夹中。(Butterfly:实际下载发现,有些源码在库中没有,导致下载到97%时停止)

(3)技巧

  • 使用 -j 参数开启更多下载进程以提高下载效率。
repo sync -j8
  • 如果下载过程中出现错误,可以增加两个参数以使其继续下载。-f 使遇到网络错误时继续下载,--force-sync 表示出现冲突时继续下载。
repo sync -f --force-sync

步骤3:下载 twrp 源码

一般情况,omnirom 源码树默认包含 AOSP recovery,而不是 twrp 源码,因此需要手动下载 twrp 源码,并把它添加到 repo 的[配置文件(manifest) 中。Twrp 源码下载链接为 https://github.com/omnirom/an…

修改本地配置文件

本地仓库配置文件 roomservice.xml 位于源码树 .repo/local_manifests/ 目录中,它原本的功能是用来收集和存放 omnirom 官方支持机型的源码下载地址,供 repo sync 下载和更新源码时使用。Twrp 没有被配置在官方库中(.repo 目录中其它 XML manifest 文件因为升级原因存放位置有变化),因此需要手工增加相关的源码库地址。

假设源码树的根目录是 omni9,首先进入 omni9/.repo,检查是否有local_manifests文件夹,文件夹中是否有 roomservice.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--Please do not manually edit this file-->
<manifest>
    <project name="OmniROM/android_bootable_recovery" path="bootable/recovery" remote="github" revision="android-9.0" />
    <project name="OmniROM/android_external_busybox" path="external/busybox" remote="github" revision="android-9.0" />
</manifest>

然后删除默认配置文件 omni9/.repo/manifests/default.xml 中与 AOSP recovery 相关的内容:

-  <project path="bootable/recovery" name="platform/bootable/recovery" groups="pdk" />
+  <project path="bootable/recovery-aosp" name="platform/bootable/recovery" groups="pdk" />

最后回到 omni9 目录,运行下面命令下载 twrp 相关的源码:

repo sync --force-sync bootable/recovery
repo sync --force-sync external/busybox

步骤4:收集配置文件

(1)配置文件的组成

编译 twrp 离不开设备的配置文件,配置文件一般包含以下几部分(以下提到的路径都是相对于安卓源码根目录来说的):

  • 设备配置参数

设备配置参数放在 device 目录,定义了设备的一系列基本信息,包括一系列 makefile(*.mk)和设备专用源码。

  • 内核源码

内核源码位于 kernel 目录,安卓会同步对它进行编译。需要注意的是,不是所有设备都有源码,有的设备使用存放在定义参数目录中的预编译内核。

  • 厂家配置文件

厂家配置参数位于 vendor 文件夹,存放生产厂家专用的配置信息和各种预编译文件(可执行文件、运行时,等等,一般不是源代码),相当多数量的设备仅在编译整个安卓系统时才用到厂家配置文件,编译 twrp 时不需要。

(2)在 GitHub 上查找配置文件

怎么才能获取设备的配置文件呢?去 GitHub!GitHub 上有大量的不同设备的不同配置文件,要善于使用搜索。例如,笔者的手机是华为 P6(型号:P6-C00),在 GitHub上使用下面的关键词查找前面说的三种配置文件:

  • 设备配置参数:device p6device huawei p6
  • 内核源码:kernel p6kernel huawei p6
  • 厂家配置参数:vendor p6vendor huawei p6

需要注意的是,很多设备有自己的代号,开发者在 GitHub 上发布配置文件时常常只使用代号来表示设备,例如:小米 Max 的代号是 hydrogenhelium,三星 Galaxy S5 BOC 的代号是 kltechnduo,在这种情况下无法用 mimaxgalaxy s5G9008W 做关键词找到它们。要查询设备对应的代号,可以访问 Magic Fun download pageLineage OS download pageRecovery Remix download page 和开源 ROM 网站。

对于不同的开源 ROM,适合的配置文件往往是不同的,配置文件中的参数也是不同的。因为我们用 omnirom 作为编译 twrp 的载体,所以最好找到适合 ominirom 的配置文件——也就是有 omni_<设备名称>.mk 的那个配置参数文件夹。如果无法找到,请参考下一步方法“修改配置文件”。

(3)把配置文件放到正确的文件夹

在 GitHub 上找到可用的源后,可以直接把它 git clone 到相应的位置。怎么确认“相应的位置”呢?这个非常简单。

设备的配置参数路径为 Device/<生产厂家名>/<设备名>,内核源码路径与生产厂家配置参数很接近。例如,我手里华为 P6 的配置文件路径为:

  • 设备配置参数:device/huawei/hwp6_u06
  • 内核源码:kernel/huawei/hwp6_u06
  • 生产厂家配置参数:vendor/huawei

按照 AOSP 的规则,安卓源码中每个源都有固定的命令规范——用 “_” 替换 “/”,所以很容易判断该把它放到哪个位置。然而,如果遇到命令方法不一样的源,则只能参考这个规则进行推断了。

步骤5:修改配置文件

现有的配置文件不一定能即下即用,因为它们可能不是为我们的安卓源码设计的,只有有限的流行型号有即下即用的配置文件。大部分配置文件只适用于不同版本 omnirom,或者其它 rom,比如 CyanogenMod,所以修改配置文件以适配 twrp 是必不可少的步骤。

(1)判断配置文件是否可以直接使用

如果你的设备幸运地属于 twrp 官方支持的型号,就不用花大力气修改配置文件,直接用就可以了。在 twrp 官网中查找你的设备

即使未被官网支持也不要灰心,因为有很多开发者在做非官方的适配工作。查一下在 GitHub 上找到的设备参数源是否含有你的 omnirom 版本对应的分支,理论上说,适合安卓7.0及以上的版本可以直接用于 omnirom8.1。

(2)修改 BoardConfig.mk

BoardConfig.mk 是设备参数文件中必要的部分,存放了很多编译 recovery 和 boot.img 所需的参数,把它设置正确是保证 twrp 正常编译的首要条件。recovery 和 boot.img 实际上是一样的,都是安卓的启动 image。

注意:

  • 以下所有路径都是相对于安卓源码根目录来说的。
  • 由于排版限制,以下设置不包括对所需值的描述。布尔值的写法为 true 或 false;其它的是没有引号的数值或字符串。

1. 内核打包参数 Kernel packaging parameters

这些参数控制着如何将内核 image 打包成启动 image,它们一般由开发者提前设定好,不用修改。启动 image 由 mkbootimg 生成,源码位于 system/core/mkbootimg

参数名称

说明

BOARD_KERNEL_CMDLINE

内核操作参数

BOARD_KERNEL_BASE

内核在启动 image 中的位置

BOARD_KERNEL_PAGESIZE

内核页大小 Kernel page size

BOARD_MKBOOTIMG_ARGS

需要向 mkbootimg这个工具传递附加参数

BOARD_BOOTIMAGE_PARTITION_SIZE

启动分区的大小

BOARD_RECOVERYIMAGE_PARTITION_SIZE

恢复(recovery)分区的大小

BOARD_CUSTOM_MKBOOTIMG

某些特殊的设备需要特殊的 mkbootimg 来生成启动 image(比如 Ruixin micro)。这个参数指定这个工具的位置

BOARD_CUSTOM_BOOTIMG_MK

对于有特殊格式的启动 image,用户可以自己编写 makefile。这个参数指定 makefile 的位置

注意:

  • BOARD_CUSTOM_MKBOOTIMG 不再适用于安卓8.0 及以上版本,添加这个参数将引起报错。
  • BOARD_CUSTOM_BOOTIMG_MK 不被安卓8.x 编译系统支持,会引起错误,但在安卓9.0中却没问题,可以安全使用。

2. 内核编译参数 Kernel compilation parameters

很多第三方开源 ROM 开发者推荐你自己编译内核,而不是使用设备配置文件中的预编译内核,因为已编译好的内核无法修改,并且只适用于特定系统,把它放到新系统中可能无法正常工作甚至无法开机。如果你找到的设备配置文件含有预编译内核,并能找到内核源码,请设置下面的参数。

参数名称

说明

TARGET_KERNEL_SOURCE

指定内核源码位置

TARGET_KERNEL_CONFIG

指定用于编译内核的配置文件。这个配置文件位于内核源码 Arch / < system platform > / configs

BOARD_KERNEL_IMAGE_NAME

指定内核名称。安卓编译器用它来找到内核 image。编译好的内核 image 位置内核源码 Arch / < system platform> / boot

KERNEL_TOOLCHAIN

指定编译内核的交叉工具链。有些设备比较特殊,安卓源码提供的编译器编译的内核无法启动,需要使用专用的或老版本的编译器

TARGET_KERNEL_CROSS_COMPILE_PREFIX

KERNEL_TOOLCHAIN 共同指定交叉工具链的前缀

如果实在无法找到内核源码,也可以设置以下参数来使用已有的内核(比如某个能正常启动的boot.imgrecovery.img,来自设备参数文件提供者),不过,不保证新系统能正常使用。

参数名称

说明

TARGET_PREBUILT_KERNEL

指定预编译内核的位置

TARGET_PREBUILT_RECOVERY_KERNEL

指定 recovery 预编译内核的位置

注意:

  • 内核源码和预编译内核只能使用其中一个,并且以上两个表中的参数不能同时设置!
  • 对于不同平台,BOARD_KERNEL_IMAGE_NAME 不同,Arm 平台是 zImage,x86平台是 bzImage,u-boot (如NXP,raspberry pie)则是 uImage

3. Recovery 相关的选项

BoardConfig.mk 还有几个与设置 recovery 相关的参数,主要参数如下。一般来说,设备参数文件提供者已经把参数设置好了。

参数名称

说明

TARGET_RECOVERY_PIXEL_FORMAT

指定 recovery 的像素格式。不同设备的像素格式不同,一般是 RGB_8888RGB_565,不正确的设置会导致花屏、黑屏或其它问题

TARGET_RECOVERY_FSTAB

指定 recovery 分区表信息文件(fstab)的位置。这个文件记录着可加载的分区信息,用户可以选择是否在 recovery 的 “Mount” 界面加载这些分区

BOARD_RECOVERY_SWIPE

启用滑动操作。对于无触屏的 recovery,用户可以通过上下滑动屏幕来移动焦点。正常情况下是启用的

DEVICE_RESOLUTION

指定屏幕的分辨率

RECOVERY_GRAPHICS_USE_LINELENGTH

显示 recovery 的图片时使用的行间距( line spacing)。开发者没有解释它的作用,但如果没有正确设置,会导致 recovery 屏幕乱七八糟

BOARD_HAS_SDCARD_INTERNAL

设备是否有嵌入式的 SD 卡。目前所有新设备都至少有8GB的 EMMC 存储 /data/media/0,称为嵌入式 SD 卡

RECOVERY_SDCARD_ON_DATA

在 recovery 中,确保 SD 卡位于 data 分区。当前市面上的新设备都会有嵌入式存储 /data/media/0,称为嵌入式 SD 卡,由上一个参数BOARD_HAS_SDCARD_INTERNAL 所描述

TARGET_RECOVERY_INITRC

指定你自己的 init.rcinit.rc 是启动最重要脚本的安卓初始化者,在启动main()函数时发挥重要作用。这个参数允许用户写自己的init.rc ,以适应各种定制设备平台

注意:TARGET_RECOVERY_INITRC 只适用于 AOSP 官方 recovery 和安卓6.0 (如 twrp 2.x 和 clockworkmod)之前的 recovery。新版本 twrp (≥ 3.0) 会直接忽略这个选项,use only the information it provides init.rc

4. Twrp 特有参数

Twrp 有一些特有参数,如下:

参数名称

说明

TW_THEME

定义 twrp 的主题。不同主题决定不同 twrp 显示风格,包括分辨率、屏幕方向等。默认主题有:portrait_hdpiportrait_mdpilandscape_hdpilandscape_mdpiwatch_mdpi这个参数必须设置,否则编译时会报错!

TW_CUSTOM_BATTERY_PATH

指定电池路径。电池路径位于内核系统目录 /sys 中,twrp 访问它以显示电池电量。例如:华为 P6 的电池路径为 /sys/devices/platform/bq_bci_battery.1/power_supply/Battery

TW_BRIGHTNESS_PATH

指定亮度路径。位于内核系统目录 /sys 中,存放屏幕亮度调整文件, twrp 调整它来改变屏幕亮度。例如:华为 P6 的亮度路径为 /sys/devices/platform/k3_fb.1/leds/lcd_backlight0/brightness

TW_DEFAULT_BRIGHTNESS

指定默认亮度。数值范围为 [0-255]

TW_MAX_BRIGHTNESS

指定最大亮度。数值范围为 [0-255]

TW_FLASH_FROM_STORAGE

这个参数的作用未知,但仅对 2.x 版本有效,在 3.2.3-0 版本上已经过期

TW_EXTERNAL_STORAGE_PATH

指定外部存储的加载路径

TW_EXTERNAL_STORAGE_MOUNT_POINT

指定外部存储的加载点

TW_DEFAULT_EXTERNAL_STORAGE

是否将默认存储设置为外部存储。在 3.2.3-0 版本上已经过期

TW_EXCLUDE_SUPERSU

是否不包含超级用户。如果 twrp 包含超级用户,将在每次启动时提醒用户手机已 root

TW_INCLUDE_NTFS_3G

是否包含 ntfs-3g 组件以支持 NTFS 分区

TW_IGNORE_MISC_WIPE_DATA

是否忽略从 bootloader 传过来的清除 data 分区的指令。there miscThe partition stores the bootloader passed to the boot image(boot or recovery)Can control their starting behavior.

TW_EXTRA_LANGUAGES

是否增加附加语言。附加语言包括汉语、日语,等等。Twrp 默认仅包含英语和几种欧洲语言(如德语、法语、俄语、丹麦语,等等)

5. 加密相关的选项

现今市面上手机的 data 都是加密的,必须解密才能读取它的内容。官方系统(包括 recovery)启动时执行了解密过程;任何 twrp 要读取 data 分区都必须设置下面的选项,并且包含其它解密组件。

笔者知道有两种加密方案:骁龙的 qseecom 加密和华为的私有强制加密(基于 f2fs)。其中前者被大多数开发者支持。很多 twrp 大神把骁龙加密组件当作他们的必备模块。笔者打算专门为骁龙CPU写一个增加加密函数的教程。(我是小米 Max 官方 twrp 的骁龙解密组件的开发者!)

参数名称

说明

TW_INCLUDE_CRYPTO

twrp 是否包含加密组件,是否允许加解密

TARGET_HW_DISK_ENCRYPTION

设备是否包含硬件加密。目前支持加密的设备一般都是硬件加密

TARGET_KEYMASTER_WAIT_FOR_QSEE

对于使用骁龙的设备,当 recovery 启动 qseecomd 解密完毕后,是否等待骁龙加密服务程序。这个选项必须打开,否则 twrp 的解密功能无效

6. 任务相关的选项

Twrp 支持 logcat 调试功能,可在安卓系统中被读取为 logcat 日志。

参数名称

说明

TWRP_INCLUDE_LOGCAT

是否包含 twrplogcat

TARGET_USES_LOGD

日志服务在 twrplogd 中是否可用

(3) 对非 omnirom 配置文件的修改

不是所有设备都有适用于 omnirom 的配置文件,因此使用其它 ROM 的配置文件需要先进行修改。这种情况经常发生在老型号设备上,它们一般只有适用于老版 ROM 的配置文件,比如 CyanogenMod 4.x。不过修改工作不复杂。

1. 修改设备的 makefile (product makefile)

每个 ROM 设备参数文件都有一个专有的“Device makefile”,它定义了设备的基本信息,是很好的“入口”。

这种 makefile 分为三部分——继承部分设备定义部分用户定义部分。 下面是一个样例(设备是华为荣耀 10 view,忽略了开始部分的 Apache2.0 协议):

#
# This file is the build configuration for a full Android
# build for grouper hardware. This cleanly combines a set of
# device-specific aspects (drivers) with a device-agnostic
# product configuration (apps).
#

# Sample: This is where we'd set a backup provider if we had one
# $(call inherit-product, device/sample/products/backup_overlay.mk)
$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit.mk)

# Get the prebuilt list of APNs
$(call inherit-product, vendor/omni/config/gsm.mk)

# Inherit from the common Open Source product configuration
$(call inherit-product, $(SRC_TARGET_DIR)/product/aosp_base_telephony.mk)

#treble
$(call inherit-product, $(SRC_TARGET_DIR)/product/treble_common.mk)

# must be before including omni part
TARGET_BOOTANIMATION_SIZE := 1080p

# Inherit from our custom product configuration
$(call inherit-product, vendor/omni/config/common.mk)

# Inherit from hardware-specific part of the product configuration
$(call inherit-product, device/huawei/berkeley/device.mk)

PRODUCT_PROPERTY_OVERRIDES += ro.hardware.nfc_nci=nqx.default

ALLOW_MISSING_DEPENDENCIES := true

DEVICE_PACKAGE_OVERLAYS += device/huawei/berkeley/overlay

# Discard inherited values and use our own instead.
PRODUCT_NAME := omni_berkeley
PRODUCT_DEVICE := berkeley
PRODUCT_BRAND := Huawei
PRODUCT_MODEL := Honor View 10

TARGET_VENDOR := huawei

① 继承部分

继承部分是指上面源文件中的 call inherit-product 函数,使得当前设备配置文件能继承其它配置文件,包括位于 build/make/target 的由 ROM 提供的通用配置文件,和设备专用配置文件(一般位于设备参数文件目录, 文件名为 device_< 设备名 >. MKdevice.mk )。

当修改时,一般你只需使用其它设备的的代码,不过要注意32位与64位之间的区别。

② 设备定义部分

设备定义部分是整个配置文件的核心,它是系统编译时找到设备参数文件的基础。包含以下变量,每个变量都不可以省略

变量名称

说明

PRODUCT_DEVICE

设备名称,必须填写,这是编译系统识别设备名称的核心参数!

PRODUCT_NAME

产品名称,形式一般为 omni_<产品名称>,必须填写。编译系统根据这个设置编译环境!

PRODUCT_BRAND

品牌名称

PRODUCT_MODEL

设备型号,它一般显示在“系统设置” 中的 “关于设备” 里

TARGET_VENDOR

生产厂家名称

③ 用户定义部分

除了以上两部分外的其它代码属于用户定义代码,存放其它参数。

重命名 Device makefile

不同 ROM 的 device makefile 名称不同,形式一般如下:

  • OmniROM: omni_< 设备名称 >. MK
  • CyanogenMod: cm.mk
  • Magic Fun:mokee.mk(老版本)mk_< 设备名称 >. MK(Android 9)

虽然文件名称不同,但实际它们是相似的,只需把它的名称改为 omni_< 设备名称 >. MK 。注意:这个“设备名称”就是 PRODUCT_DEVICE 中的值,否则编译系统会报错并中止编译。

2. 指定 makefile 的调用链

Makefile(*.mk)有一个链式调用关系,调用链的起点是编译用于管理设备配置文件的系统 makefile 程序——build/make/core/product_config.mk,它会寻找设备描述目录 AndroidProducts.mk,并依据 AndroidProducts.mk 去定位 “meta makefile”。

AndroidProducts.mk

AndroidProducts.mk 是设备描述名的入口。系统编译通过在 AndroidProducts.mk 中寻找 device 来判断这个设备是否有配置文件,这个文件一般通过设置变量 PRODUCT_MAKEFILES 来包含 device makefile。 通常内容如下:

PRODUCT_MAKEFILES := \
    $(LOCAL_DIR)/omni_berkeley.mk

product_config.mk

product_config.mk 工作逻辑中的一步是接收用户输入的设备配置名称,然后从 device/ 获取配置文件。如果配置文件不存在,就会报错。相关代码如下(保留了原始注释,增加了笔者的说明):

# ---------------------------------------------------------------
# Include the product definitions.
# We need to do this to translate TARGET_PRODUCT into its
# underlying TARGET_DEVICE before we start defining any rules.
#
include $(BUILD_SYSTEM)/node_fns.mk
include $(BUILD_SYSTEM)/product.mk
include $(BUILD_SYSTEM)/device.mk

ifneq ($(strip $(TARGET_BUILD_APPS)),)
# An unbundled app build needs only the core product makefiles.
all_product_configs := $(call get-product-makefiles,\
    $(SRC_TARGET_DIR)/product/AndroidProducts.mk)
else
# Read in all of the product definitions specified by the AndroidProducts.mk
# files in the tree.
##[as mentioned in the above note, this sentence will search all androidproducts.mk in the device directory, and then read the products in these files_ Makefiles variable to get the path of the device makefile.]
##[all obtained device makefile paths will be saved to all_ product_ Configs is in this variable.]
all_product_configs := $(get-all-product-makefiles)
endif

all_named_products :=

# Find the product config makefile for the current product.
# all_product_configs consists items like:
# <product_name>:<path_to_the_product_makefile>
# or just <path_to_the_product_makefile> in case the product name is the
# same as the base filename of the product config makefile.
## 【current_ product_ Makefile is the device makefile file name of the device configuration file selected by the user]
##[the following long string of foreach functions are text processing. Change the path to a relative path referenced to the source root directory] 
current_product_makefile :=
all_product_makefiles :=
$(foreach f, $(all_product_configs),\
    $(eval _cpm_words := $(subst :,$(space),$(f)))\
    $(eval _cpm_word1 := $(word 1,$(_cpm_words)))\
    $(eval _cpm_word2 := $(word 2,$(_cpm_words)))\
    $(if $(_cpm_word2),\
        $(eval all_product_makefiles += $(_cpm_word2))\
        $(eval all_named_products += $(_cpm_word1))\
        $(if $(filter $(TARGET_PRODUCT),$(_cpm_word1)),\
            $(eval current_product_makefile += $(_cpm_word2)),),\
        $(eval all_product_makefiles += $(f))\
        $(eval all_named_products += $(basename $(notdir $(f))))\
        $(if $(filter $(TARGET_PRODUCT),$(basename $(notdir $(f)))),\
            $(eval current_product_makefile += $(f)),)))
_cpm_words :=
_cpm_word1 :=
_cpm_word2 :=
##[after the above processing, current_ product_ The value of makefile becomes the file name of the device Makefile, such as Omni_ berkeley.mk】
##[if it cannot be obtained, the variable value is empty]
current_product_makefile := $(strip $(current_product_makefile))
all_product_makefiles := $(strip $(all_product_makefiles))

load_all_product_makefiles :=
ifneq (,$(filter product-graph, $(MAKECMDGOALS)))
ifeq ($(ANDROID_PRODUCT_GRAPH),--all)
load_all_product_makefiles := true
endif
endif
ifneq (,$(filter dump-products,$(MAKECMDGOALS)))
ifeq ($(ANDROID_DUMP_PRODUCTS),all)
load_all_product_makefiles := true
endif
endif

##[a device can have more than one Makefile, but in most cases there will only be one]
ifeq ($(load_all_product_makefiles),true)
# Import all product makefiles.
$(call import-products, $(all_product_makefiles))
else
# Import just the current product.
##[if the makefile of the device cannot be found, report an error and exit directly]
ifndef current_product_makefile
$(error Can not locate config makefile for product "$(TARGET_PRODUCT)")
endif
ifneq (1,$(words $(current_product_makefile)))
$(error Product "$(TARGET_PRODUCT)" ambiguous: matches $(current_product_makefile))
endif
$(call import-products, $(current_product_makefile))
endif  # Import all or just the current product makefile

总体调用关系

根据以上分析,不难总结以下调用关系:

  1. build/make/core/product_config.mk 读取 device/ 下所有的设备参数文件目录 AndroidProducts.mk,获取 device makefiles 清单。
  2. product_config.mk 在清单中找到用户设备的 device makefile(例如华为荣耀 10 view 就是 omni_berkeley.mk)。
  3. 如果找到了,就读取这个 device makefile 中的信息,设置编译环境;否则报错并退出。

步骤6:开始编译

完成配置文件修改后,我们开始直接编译。

在安卓源码根目录,先初始化编译环境:

source build/envsetup.sh

然后运行 lunch 命令,选择编译目标。这步也可以直接运行 Lunch < 下面列表中的某个目标 >

$ lunch

You're building on Linux

Lunch menu... pick a combo:
     1\. aosp_arm-eng
     2\. aosp_arm64-eng
     3\. aosp_mips-eng
     4\. aosp_mips64-eng
     5\. aosp_x86-eng
     6\. aosp_x86_64-eng
     7\. omni_berkeley-user
     8\. omni_berkeley-userdebug
     9\. omni_berkeley-eng
     10\. omni_hwp6_u06-userdebug
     11\. omni_hwp6_u06-eng
     12\. omni_kenzo-userdebug
     13\. omni_emulator-userdebug

Which would you like? [aosp_arm-eng] 10

最后,开始编译 twrp:

make recoveryimage

编译成功生成 Out/target/product/<device name>/recovery.img,直接用 fastboot 刷到设备中。

步骤7:调试、检查错误和修改源码

适配 twrp 从来不是一夜之间就能完成的事,这意味着不可能一次成功。各种错误会潜伏在编译过程中、运行过程中,甚至成功启动后,你可能在任何时候都需要检查错误。

(1)编译错误

编译过程中的错误最容易检查,简单一句话,就是“出现一个错误,然后解决一个错误”。任何错误都会显示到终端上,你只需要根据报错来解决它。自从安卓7.0 使用 ninja后,当发生以 “FAILED:” 开头的报错时,编译工具会显示产生错误的语句,因此只需寻找 “FAILED:”,就可以快速定位发生错误的点。

(2) 运行前的错误

编译成功后,生成的 recovery 不一定能成功启动,比如会自动重启、黑屏,等等。这时,检查错误最直接的方法就是获取 kernel log

1. 获取 kernel log 的方法

一般来说,只要内核发生了严重错误,都会“尽量”在崩溃时记录 kernel log。不同平台和内核有不同方法获取 kernel log。一般如下:

  • 骁龙:kernel log 存放在 /proc/last_kmsg
  • Ruixin micro(例如 rk3188):与骁龙相同。
  • Hisilicon 早期芯片 (例如 k3v2):由开发区域的内核参数 CONFIG_APANIC_PLABEL指定,一般是 splash

Just brush in another normally started old recovery in the normally started systembootFor zoning)catJust read them with the command.(Butterfly:没看懂)

2. 阻碍内核日志的设计之坑 init

有些启动失败导致的严重错误发生在安卓启动程序 init,但 init 有一个非常坑人的设计,就是,对于编译为 debugging 的 ROM,如果发生严重错误,它将自动重启并进入 bootloader。官方说这样“有利于调试”,但这个设计导致内核无法传输日志。因此,有必要修改这个功能。

打开 init 目录 system/core/init,应用以下 git diff 对它打补丁:

diff --git a/init/Android.bp b/init/Android.bp
index 45cf327f8..8a16fb2d2 100644
--- a/init/Android.bp
+++ b/init/Android.bp
@@ -14,6 +14,9 @@
 // limitations under the License.
 //

+// AnClark modify: I want to read logs. If handles panic as rebooting into bootloader, I won't get any logs!
+// Force setting DREBOOT_BOOTLOADER_ON_PANIC=0 on product_variables. 
+
 cc_defaults {
     name: "init_defaults",
     cpp_std: "experimental",
@@ -41,7 +44,7 @@ cc_defaults {
                 "-UALLOW_PERMISSIVE_SELINUX",
                 "-DALLOW_PERMISSIVE_SELINUX=1",
                 "-UREBOOT_BOOTLOADER_ON_PANIC",
-                "-DREBOOT_BOOTLOADER_ON_PANIC=1",
+                "-DREBOOT_BOOTLOADER_ON_PANIC=0",
                 "-UWORLD_WRITABLE_KMSG",
                 "-DWORLD_WRITABLE_KMSG=1",
                 "-UDUMP_ON_UMOUNT_FAILURE",
diff --git a/init/Android.mk b/init/Android.mk
index f1fe5168b..d28b4e489 100644
--- a/init/Android.mk
+++ b/init/Android.mk
@@ -5,10 +5,12 @@ LOCAL_PATH:= $(call my-dir)
 # --

 ifneq (,$(filter userdebug eng,$(TARGET_BUILD_VARIANT)))
+# AnClark modify: I want to read logs. If handles panic as rebooting into bootloader, I won't get any logs!
+# Force setting DREBOOT_BOOTLOADER_ON_PANIC=0 on product_variables. 
 init_options += \
     -DALLOW_LOCAL_PROP_OVERRIDE=1 \
     -DALLOW_PERMISSIVE_SELINUX=1 \
-    -DREBOOT_BOOTLOADER_ON_PANIC=1 \
+    -DREBOOT_BOOTLOADER_ON_PANIC=0 \
     -DDUMP_ON_UMOUNT_FAILURE=1
 else
 init_options += \
diff --git a/init/util.cpp b/init/util.cpp
index fdcb22d1c..a468082af 100644
--- a/init/util.cpp
+++ b/init/util.cpp
@@ -370,11 +370,19 @@ bool expand_props(const std::string& src, std::string* dst) {
     return true;
 }

+// AnClark MODIFY: Use abort() instead of rebooting into BL to trigger panic.
+/**
 void panic() {
     LOG(ERROR) << "panic: rebooting to bootloader";
     // Do not queue "shutdown" trigger since we want to shutdown immediately
     DoReboot(ANDROID_RB_RESTART2, "reboot", "bootloader", false);
 }
+**/
+
+void panic() {
+   LOG(ERROR) << "android::init::panic() invoked. Abort init to trigger kernel panic!";
+   abort();
+}

 static std::string init_android_dt_dir() {
     // Use the standard procfs-based path by default

打补丁的方法:将以上代码存为 system/core/init/no_reboot_into_bootloader.diff,然后在 system/core/init 目录下运行下面命令:

patch -p2 < no_reboot_into_bootloader.diff

(3)功能测试和缺陷修复

当成功通过了前面所说的五个步骤,执行了第六步,久违的 twrp 开机画面出现在设备屏幕上,祝贺你!下面的工作就是测试各项功能,去发现各种影响使用的 bug。

一般来说,在 twrp 中,可以执行以下单元测试:

  • 加载是否正常
    进入主菜单的 “Mount” ,点击每个分区,看是否能正常加载。
  • 解密是否正常

对于骁龙设备,判断解密是否有问题的最直接的方法就是查看日志中是否有解密失败的记录,以及测试 data 分区是否能正常加载。

  • 是否能连接 adb

接上计算机,在计算机上运行 adb devices,看是否能检测到 recovery 状态下的设备,运行 adb shell 看是否能出现设备的终端窗口,并获得 root 权限(显示符号 “#”)。

  • 能否连接 MTP

连接计算机,看是否有一个 MTP 设备(Windows)或管理栏(Ubuntu)出现在计算机上。如果没有,点 “Mount” 中的 “enable MTP”。

  • 是否支持 OTG

从2014年开始,主流型号都支持 OTG。可以插上 OTG,然后在 twrp 终端 运行 lsusb,看能否检测到 USB 设备。此时插入一个新设备,再看 lsusb,是否输出变成了另一个设备。如果插入的是 USB 闪盘,也可以检查 /dev/block/,看看是否有更多的 sdx 设备(x 指代任何小写的英文字母)。

  • 能否备份 / 恢复系统

测试系统是否可以正常备份和恢复,检查备份选项中是否包含必要的分区(systemdatabootrecovery)。

需要注意的事

编译支持 shell 的系统

原则上,操作 shell 只能使用 bash,如果运行其它 shell,可能在运行某些命令时会出现无法意料的错误 — 例如,在 lunch 输入数字的菜单中选择设备 1\,可能实际选上的是设备 3。这非常困难,对于这点,编译系统将 source build/envsetup.sh,警告不要使用 bash 以外的 shell。

不过,从安卓9.0开始,编译系统支持除了 bash 以外的 shell,比如 Zsh。因此,如果你的 shell 是 Zsh,请放心使用。