安卓模拟器(Android Emulator AVD)移植PixelExperiece教程
背景
PixelExperience是一个基于AOSP和LineageOS的安卓系统,因为其采用了和Pixel一样的UI界面,受到很多原生党的喜爱。最近项目需要在安卓模拟器AVD中移植其他厂商的操作系统,因为PE和AOSP最接近并且开源,调试比较方便,因此打算从PE开始尝试。
移植过程
首先PE的构建系统是基于AOSP的,通常模拟器镜像在AOSP中通常为sdk_phone_arm64或者sdk_phone_x86_64,其中从安卓11开始,Google默认去掉了sdk_phone_arm64的target(其实mk文件还在,只是需要自己加到lunch列表里而已),不过因为我需要在M1 Macbook Pro的Android SDK Emulator中使用,因此需要编译成arm64镜像,这篇文章就记录一下折腾过程。
获取代码
这一步比较简单,照着PE的github上的指引来做即可。我这里需要编译的是基于安卓12的版本,就选择twelve-plus或者twelve对应的分支即可:
克隆代码需要安装repo工具,安装方法网上教程很多,就不再浪费口舌了,大家自己搜,这里直接就直接从克隆代码开始。
mkdir pe-plus
cd pe-plus
repo init -u https://github.com/PixelExperience/manifest -b twelve-plus
repo sync -c -j$(nproc --all) --force-sync --no-clone-bundle --no-tags
接下来就是漫长的下载代码的过程,如果大家有国外服务器的尽量选择硅谷的独立服务器,这样速度会快很多,实测可以在30min左右拉完代码,在国内下载代码速度太慢,需要等待更长时间。
下载模拟器镜像相关的device支持
代码下载完成之后,我们需要把编译Emulator AVD镜像所需要的device代码添加上。在PE的gituhub中我们看到一个snippets目录里面有一个remove.xml文件,其中删除了它认为不需要的aosp源码树中的一些模块,大概如下的样子:
<!-- Device repos -->
<remove-project name="device/amlogic/yukawa" />
<remove-project name="device/amlogic/yukawa-kernel" />
<remove-project name="device/common" />
<remove-project name="device/generic/arm64" />
<remove-project name="device/generic/armv7-a-neon" />
<remove-project name="device/generic/art" />
<remove-project name="device/generic/car" />
<remove-project name="device/generic/common" />
<remove-project name="device/generic/goldfish" />
<remove-project name="device/generic/goldfish-opengl" />
<remove-project name="device/generic/mini-emulator-arm64" />
<remove-project name="device/generic/mini-emulator-armv7-a-neon" />
<remove-project name="device/generic/mini-emulator-x86" />
<remove-project name="device/generic/mini-emulator-x86_64" />
<remove-project name="device/generic/opengl-transport" />
<remove-project name="device/generic/qemu" />
<remove-project name="device/generic/trusty" />
<remove-project name="device/generic/uml" />
<remove-project name="device/generic/x86" />
<remove-project name="device/generic/x86_64" />
<remove-project name="device/google/atv" />
<remove-project name="device/google/barbet" />
<remove-project name="device/google/barbet-kernel" />
<remove-project name="device/google/barbet-sepolicy" />
<remove-project name="device/google/bonito" />
...
但是我们要编译的是AVD镜像,会用到device/common和device/generic目录下的代码,所以我们需要把这部分内容删除以后再重新同步repo。修改.repo/manifests/snippets/remove.xml文件,删除name里device/common和device/generic开头的条目:
vim .repo/manifests/snippets/remove.xml
<!-- Device repos -->
<remove-project name="device/amlogic/yukawa" />
<remove-project name="device/amlogic/yukawa-kernel" />
<!-- 这中间的全部删掉 -->
<remove-project name="device/google/atv" />
<remove-project name="device/google/barbet" />
<remove-project name="device/google/barbet-kernel" />
<remove-project name="device/google/barbet-sepolicy" />
<remove-project name="device/google/bonito" />
...
最后重新repo sync -jX即可。
添加编译target
进入pe-plus目录,设置环境变量:
. build/envsetup.sh
可以发现在设置环境变量时,PE的编译链包含了他自己的一个vendorsetup脚本。位置在vendor/aosp/vendorsetup.sh,打开看看源码可以发现PE通过vendor/aosp/tools/get_official_devices.py从github网站上拉取目前支持的device target列表,添加到一个数组里面,所以如果要添加自己的target的话我们直接加在后面就可以了。编辑vendor/aosp/vendorsetup.sh文件:
lunch_others_targets=()
for device in $(python vendor/aosp/tools/get_official_devices.py)
do
for var in user userdebug eng; do
lunch_others_targets+=("aosp_$device-$var")
done
done
在最后添加上一行lunch_others_targets+=(“sdk_phone_arm64-userdebug” “sdk_phone_arm64-eng”)即可,因为我需要的是arm64的target,所以就加上arm64,如果需要x86_64就加上sdk_phone_x86_64-userdebug或者sdk_phone_x86_64-eng都可以。
接下来重新运行build/envsetup.sh,然后lunch,就能看到我们自己添加的target了。
选择292或者293即可。
引入PE编译配置
这时候,直接编译还是不行的,会报错,因为虽然AOSP本身支持sdk_phone的编译target,但是并没有加入到PE的编译环境里面,因此编译过程会报错:
错误原因是unknown variable ‘$(PATH_OVERRIDE_SOONG)’,这里需要找一下PATH_OVERRIDE_SOONG在哪里定义的,发现是在aosp/config/BoardConfigSoong.mk里面,说明这时候sdk_phone的配置还没有引入PE需要的mk文件。为此,对比了一下其他的target,比如Pixel 5,对比PE的aosp_redfin.mk和google原生的mk文件,发现PE的mk文件中多了一行:
$(call inherit-product, vendor/aosp/config/common_full_phone.mk)
这就是引入了PE的编译设置了,我们同样要在build/target/product/sdk_phone_arm64.mk中引入这一行,可以加在PRODUCT_PACKAGES之前即可:
...上面省略...
# Inherit some common Pixel Experience stuff. <----加在这里:
$(call inherit-product, vendor/aosp/config/common_full_phone.mk)
# keep this apk for sdk targets for now
PRODUCT_PACKAGES += \
EmulatorSmokeTests
# Overrides
PRODUCT_BRAND := Android
PRODUCT_NAME := sdk_phone_arm64
PRODUCT_DEVICE := emulator_arm64
PRODUCT_MODEL := Android SDK built for arm64
# Disable <uses-library> checks for SDK product. It lacks some libraries (e.g.
# RadioConfigLib), which makes it impossible to translate their module names to
# library name, so the check fails.
加上之后我们重新envsetup然后lunch,再编译试试看。
这次发现,错误还是cmd: unknown variable ‘$(PATH_OVERRIDE_SOONG)’,经过了一些调试,发现common_full_phone.mk确实已经被include了,但是不知为何BoardConfigSoong.mk仍然没有被正常include,这次我们从BoardConfigSoong.mk开始一级一级向上追溯,最后发现问题出现在build/make/core/config.mk文件,里面有这么一段:
...上面省略...
ifneq ($(CUSTOM_BUILD),)
include vendor/aosp/config/BoardConfig.mk
endif
...
只有当CUSTOM_BUILD变量不为空的时候,才会include BoardConfig.mk,那么这个CUSTOM_BUILD变量是什么呢?经过搜索最后在build/make/envsetup.sh文件里找到这么一段代码:
....
function check_product()
{
local T=$(gettop)
if [ ! "$T" ]; then
echo "Couldn't locate the top of the tree. Try setting TOP." >&2
return
fi
if (echo -n $1 | grep -q -e "^aosp_") ; then
CUSTOM_BUILD=$(echo -n $1 | sed -e 's/^aosp_//g')
else
CUSTOM_BUILD=
fi
export CUSTOM_BUILD
TARGET_PRODUCT=$1 \
TARGET_BUILD_VARIANT= \
TARGET_BUILD_TYPE= \
TARGET_BUILD_APPS= \
get_build_var TARGET_DEVICE > /dev/null
# hide successful answers, but allow the errors to show
}
...
这里可以看出来,只有aosp_开头的target,才会设定一个CUSTOM_BUILD,否则这个变量就是空的,而这个变量如果是空的,就不会include BoardConfig.mk,因此造成错误。而我们编译成sdk_phone的target时,是没有CUSTOM_BUILD的,所以需要自己设定一个,比如我这里就叫emu好了,按如下的方式改:
....
function check_product()
{
local T=$(gettop)
if [ ! "$T" ]; then
echo "Couldn't locate the top of the tree. Try setting TOP." >&2
return
fi
if (echo -n $1 | grep -q -e "^aosp_") ; then
CUSTOM_BUILD=$(echo -n $1 | sed -e 's/^aosp_//g')
elif (echo -n $1 | grep -q -e "^sdk_") ; then
CUSTOM_BUILD="emu"
else
CUSTOM_BUILD=
fi
export CUSTOM_BUILD
TARGET_PRODUCT=$1 \
TARGET_BUILD_VARIANT= \
TARGET_BUILD_TYPE= \
TARGET_BUILD_APPS= \
get_build_var TARGET_DEVICE > /dev/null
# hide successful answers, but allow the errors to show
}
...
重新走envsetup流程和lunch,继续尝试重新编译。
去掉不需要的soong_namespace
lunch之后对比官方的AOSP sdk_phone target可以发现:
PRODUCT_SOONG_NAMESPACES=device/generic/goldfish device/generic/goldfish-opengl hardware/google/camera hardware/google/camera/devices/EmulatedCamera device/generic/goldfish device/generic/goldfish-opengl packages/apps/Bluetooth hardware/nxp hardware/qcom-caf/common/fwk-detect
目前的NAMESPACES多出了hardware/nxp hardware/qcom-caf/common/fwk-detect这两个模块,这是我们不需要的,需要去除,否则会导致编译出的镜像无法在AVD中启动。经过搜索,可以发现,需要修改
aosp/config/BoardConfigSoong.mk,去掉尾部如下的内容:
ifneq ($(TARGET_USES_NQ_NFC),true)
PRODUCT_SOONG_NAMESPACES += hardware/nxp
endif #TARGET_USES_NQ_NFC
修改aosp/config/BoardConfig.mk文件:
去掉尾部如下的内容:
TARGET_FWK_DETECT_PATH ?= hardware/qcom-caf/common
PRODUCT_SOONG_NAMESPACES += \
$(TARGET_FWK_DETECT_PATH)/fwk-detect
解决编译过程错误
继续尝试编译,这次的错误变成了
具体错误发生在build/make/core/artifact_path_requirements.mk,这个错误在网上搜索了一下,发现是ARTIFACT_PATH_REQUIREMENTS校验引起的,编译emulator镜像把这个检查给DISABLE就可以了,我们可以直接修改vendor/aosp/config/common_full_phone.mk,在最顶上加上如下的代码即可:
ifeq ($(CUSTOM_BUILD),emu)
DISABLE_ARTIFACT_PATH_REQUIREMENTS := true
endif
接下来就可以继续尝试编译。
这次的错误是:
这个错误就比较常见了,问题出在vendor/aosp/build/tasks/kernel.mk,因为sdk_phone target的编译是使用prebuilt内核的,所以在vendor/aosp/build/tasks/kernel.mk把编译kernel部分给排除即可:
在最顶上加上CUSTOM_BUILD的判断即可
ifneq ($(CUSTOM_BUILD),emu)
ifneq ($(TARGET_NO_KERNEL),true)
...中间省略...
endif # TARGET_NO_KERNEL
endif
再次尝试编译,这次终于可以正常进行编译了:
编译的过程可能会出现You have tried to change the API from what has been previously approved.类似的问题,大概如下图
FAILED: out/soong/.intermediates/frameworks/base/api-stubs-docs-non-updatable/android_common/metalava/check_current_api.timestamp
( true && diff -u -F '{ *$' frameworks/base/core/api/current.txt out/soong/.intermediates/frameworks/base/api-stubs-docs-non-updatable/android_common/metalava/api-stubs-docs-non-updatable_api.txt && diff -u -F '{ *$' frameworks/base/core/api/removed.txt out/soong/.intermediates/frameworks/base/api-stubs-docs-non-updatable/android_common/metalava/api-stubs-docs-non-updatable_removed.txt && touch out/soong/.intermediates/frameworks/base/api-stubs-docs-non-updatable/android_comm
on/metalava/check_current_api.timestamp ) || ( echo -e "\n******************************\nYou have tried to change the A
PI from what has been previously approved.\n\nTo make these errors go away, you have two choices:\n 1. You can add '@h
ide' javadoc comments (and remove @SystemApi/@TestApi/etc)\n to the new methods, etc. shown in the above diff.\n\n
2. You can update current.txt and/or removed.txt by executing the following command:\n m api-stubs-docs-non-up
datable-update-current-api\n\n To submit the revised current.txt to the main Android repository,\n you will ne
ed approval.\n******************************\n" ; exit 38 ) # hash of input list: 029245c8d5b1fb42417b35080e5be16671c76f
ca0c74c985b4a579247f7b7b1e
--- frameworks/base/core/api/current.txt 2022-11-09 03:55:30.306190310 +0000
+++ out/soong/.intermediates/frameworks/base/api-stubs-docs-non-updatable/android_common/metalava/api-stubs-docs-non-updatable_api.txt 2022-11-09 09:04:10.522007109 +0000
@@ -35422,6 +35422,7 @@ public static final class Settings.Secur
field public static final String SELECTED_INPUT_METHOD_SUBTYPE = "selected_input_method_subtype";
field public static final String SETTINGS_CLASSNAME = "settings_classname";
field public static final String SKIP_FIRST_USE_HINTS = "skip_first_use_hints";
+ field public static final String TETHERING_ALLOW_VPN_UPSTREAMS = "tethering_allow_vpn_upstreams";
field public static final String TOUCH_EXPLORATION_ENABLED = "touch_exploration_enabled";
field @Deprecated public static final String TTS_DEFAULT_COUNTRY = "tts_default_country";
field @Deprecated public static final String TTS_DEFAULT_LANG = "tts_default_lang";
-e
******************************
You have tried to change the API from what has been previously approved.
To make these errors go away, you have two choices:
1. You can add '@hide' javadoc comments (and remove @SystemApi/@TestApi/etc)
to the new methods, etc. shown in the above diff.
2. You can update current.txt and/or removed.txt by executing the following command:
m api-stubs-docs-non-updatable-update-current-api
To submit the revised current.txt to the main Android repository,
you will need approval.
******************************
10:12:31 ninja failed with: exit status 1
#### failed to build some targets (02:24 (mm:ss)) ####
这是因为PE可能修改了系统Framework中的API源码,如果要继续编译,需要先运行:
make update-api
运行之后就会更新current.txt,就可以正常继续编译流程了。
编译完成后,会在out/target/product/emulator_arm64目录下生成镜像。
运行镜像
把out/target/product/emulator_arm64目录下的system-qemu.img,vendor-qemu.img,ramdisk-qemu.img,encryptionkey.img覆盖到SDK system-images目录下的某个arm64镜像目录下面(文件需要重命名,把-qemu后缀去掉),然后用这个镜像一个创建AVD,试试看是否可以运行:
到此,我们编译的镜像已经可以正常在Android Emulator中运行起来。
注意事项
PE官方给的编译教程是使用mka bacon -jX命令进行编译,这个命令最终会生成.zip刷机包,但是我们编译的是模拟器镜像,最后生成的文件也缺少打包成.zip刷机包的部分文件,因此使用官方编译命令会编译失败。而实际上PE使用的是LineageOS的编译系统,我们编译直接输入mka -jX即可正常编译,不需要加bacon这个target。