通过 Magisk 模块和 LineageOS 系统定制化 adb shell

Android 自带的 shell (/system/bin/sh) 太简单了,很多功能不支持。我个人一直喜欢用 LineageOS 的原因在于 LineageOS 编译了一个 bash 放入系统,相比 sh 功能强大一点。最近在和同事的折腾下,将 zsh 打包为一个 magisk 模块,将 adb shell 更换为 zsh,并能够使用 oh-my-zsh 以及相关的主题、插件等,大幅提高日常使用体验。

从系统上来说,我们需要使用 LineageOS。这是因为 LineageOS 提供了一个属性 persist.sys.adb.shell,可以让我们更改 adb 的 shell。原生 Android 和其他 Android 系统都不支持该功能。理论上用 aosp 也行,不过需要自己迁移这个功能。因此本文都建立在使用 LineageOS 的基础上。

其次我们使用 Magisk 模块的方式实现,因为这样做最简单,无需修改源码重新编译系统。使用 Magisk 或者 KernelSU 加载模块即可。

至于为什么不用 Termux + ssh 呢?这是因为每次重启都需要启动一下 sshd(Termux:Boot),且一直需要有一个 Termux 进程挂在后台。另外本机也要进行端口转发。而且 Termux 的 shell 里面还有太多 Termux 独特的环境,使用起来还是没有直接 adb shell 方便。

添加 zsh

尽管 LineageOS 提供的 bash 相比 sh 已经强大很多了,但是我还是希望用上功能更加强大的 zsh。LineageOS 是通过将 bash 添加进源码树,使得 bash 随系统一起编译,使得 ROM 自带 bash 了。

Termux 中提供了预编译的 zsh,但该 zsh 打了一些 patch,完全围绕 Termux 环境进行修补,不适合直接在 adb shell 使用。研究了一圈发现不能直接用 Termux 中的 zsh,故作罢。

最终我们需要自己编译 zsh。zsh 依赖 ncurses, pcre 和 gbdm。我们直接从官方渠道下载源码:

pcre 需要关闭动态链接库,静态链接至 zsh 。

编译 zsh 的时候需要注意,Android 平台上 zsh 无法动态加载模块,因此所有模块需要静态编译进去。在 zsh 执行完 configure 之后需要将 config.modules 中的所有模块的 link 选项改为 static。

编译脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/bin/sh

export TOOLCHAIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64"
export TARGET=aarch64-linux-android
export API=26
export AR="$TOOLCHAIN/bin/llvm-ar"
export CC="$TOOLCHAIN/bin/$TARGET$API-clang"
export AS="$CC"
export CXX="$TOOLCHAIN/bin/$TARGET$API-clang++"
export LD="$TOOLCHAIN/bin/ld"
export RANLIB="$TOOLCHAIN/bin/llvm-ranlib"
export STRIP="$TOOLCHAIN/bin/llvm-strip"

PCRE_SRC=$PWD/pcre-8.45
PCRE_DST=$PWD/dst/pcre
GDBM_SRC=$PWD/gdbm-1.23
GDBM_DST=$PWD/dst/gdbm
NCURSES_SRC=$PWD/ncurses-6.4
NCURSES_DST=$PWD/dst/ncurses
ZSH_SRC=$PWD/zsh-5.9
ZSH_DST=$PWD/dst/zsh

cd "$PCRE_SRC" || exit
./configure --host=aarch64-linux-android \
--target=aarch64-linux-android \
--disable-shared \
--prefix="/system/usr"
make -j16 && make install DESTDIR="$PCRE_DST"

cd "$NCURSES_SRC" || exit
./configure --host=aarch64-linux-android \
--prefix="/system/usr"
make -j16 && make install DESTDIR="$NCURSES_DST"

cd "$GDBM_SRC" || exit
./configure --host=aarch64-linux-android \
--prefix="/system/usr" \
--disable-shared
make -j16 && make install DESTDIR="$GDBM_DST"

cd "$ZSH_SRC" || exit
./configure --host=aarch64-linux-android \
--prefix="/system" \
--datarootdir="/system/usr/share" \
--enable-gdbm \
--enable-pcre \
--enable-cflags="-I$NCURSES_DST/system/usr/include -I$GDBM_DST/system/usr/include -I$PCRE_DST/system/usr/include" \
--enable-cppflags="-I$NCURSES_DST/system/usr/include -I$GDBM_DST/system/usr/include -I$PCRE_DST/system/usr/include" \
--enable-ldflags="-L$NCURSES_DST/system/usr/lib64 -L$GDBM_DST/system/usr/lib64 -L$PCRE_DST/system/usr/lib64"
sed -i"" -e "s/link=no/link=static/g" config.modules
make -j16 && make install DESTDIR="$ZSH_DST"

编译完成后,dst/zsh/system 就是我们所需的 zsh 了。将其放入 magisk 模块根目录。

接下来再添加一些额外的启动脚本 /system/etc/zshenv/system/etc/zshrc。zshenv 中放入全局 zsh 配置选项,zshrc 中放入交互 shell 的配置。

设置环境变量

根据 shell_service.cpp, shell 进程的环境变量继承自 adbd 进程,此时 HOME/SHELL/system/bin/sh。我们需要在 zshenv 中正确的设置他们。

交互 shell 当前目录

同理,adbd 运行 execle 的时候的当前目录为 /,这就导致执行 adb shell 之后总是在根目录。我们可以在 zshrc 中 cd 到 HOME 目录,这样仅交互 shell 会执行此命令。

zsh 未找到匹配时的行为

默认情况下,zsh 在未找到匹配时会由 zsh 进行报错。最直接的影响在于,当我们在 host 上使用 adb 进行命令补全的时候,如果没有匹配,zsh 会报错,导致 adb 无法正常补全:

1
2
3
$ adb pull /data/local/traces/zsh:1: no matches found: /data/local/traces/*/
zsh:1: no matches found: /data/local/traces/*/
zsh:1: no matches found: /data/local/traces/*/

我们可以通过 setopt +o nomatch 来解决这个问题,此时 pattern 会原样放入参数列表中。adb 的命令补全可以正常使用了。

zsh 配置文件

最终的zsh配置文件如下:

zshenv:

1
2
3
4
export SHELL=/system/bin/zsh
export PATH=/data/local/bin:$PATH
export HOME=/data/root
setopt +o nomatch

zshrc:

1
cd $HOME

添加额外 property 来更改 adb shell

更换 adb shell 非常简单。执行 setprop persist.sys.adb.shell /system_ext/bin/bash 后,再使用 adb shell 就会切换为 bash 了。利用 magisk 模块的 system.prop 功能即可作为系统 properties 加载。此外,我们还可以顺便添加额外一些额外的关于 adb 的配置,比如开启 adb root (service.adb.root=1),关闭adb提醒 (persist.adb.notify=0) 等。

system.prop 文件内容如下:

1
2
3
service.adb.root=1
persist.adb.notify=0
persist.sys.adb.shell=/system/bin/zsh

模块初始化脚本

customize.sh 中执行一些初始化工作。例如创建 HOME 文件夹,拷贝 zsh 相关文件等。

由于 HOME 目录在 /data/root,我们可以很轻松的安装 oh-my-zsh 和插件,可以根据自己的需要定制化 zsh。我在 adb shell 中配置好 oh-my-zsh、主题、插件后,将相关的配置文件拷贝出来再重新打包进模块,并在 customize.sh 中拷贝到 HOME 目录中。

如果使用了 powerlevel10k 主题,可能会遇到该主题无法正确设置系统图标的情况。这是因为 p10k 使用 uname -o 获取系统信息。Android 平台的 uname 为 toybox 实现,输出为 Toybox。可以通过将 uname 链接到 busybox 来解决。BusyBox For Android该模块似乎不会创建已有的软连接,需要自己额外创建。可以将软连接放入 magisk 模块的 /system/bin 中直接刷入,不过这样似乎不太符合本模块的核心功能。我这里创建了一个额外的 bin 目录 /data/local/bin,并将 uname 链接到 ksu/magisk 的 busybox。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/sh

MODDATA="$MODPATH/data"
HOME=/data/root

if ! [ -d $HOME ]; then
mkdir -p $HOME
fi

# custom binary path
if ! [ -d "/data/local/bin" ]; then
mkdir -p /data/local/bin
fi

# use busybox uname if available
if ! [ -f "/data/local/bin/uname" ]; then
if [ -d "/data/adb/ksu" ]; then
ln -s /data/adb/ksu/bin/busybox /data/local/bin/uname
elif [ -d "/data/adb/magisk" ]; then
ln -s /data/adb/magisk/busybox /data/local/bin/uname
else
ln -s /system/bin/toybox /data/local/bin/uname
fi
fi

cp -r "$MODDATA/.oh-my-zsh" $HOME/.oh-my-zsh
# cp -r "$MODDATA/.cache" $HOME/.cache

if ! [ -f "$HOME/.zshrc" ]; then
cp "$MODDATA/.zshrc" $HOME/.zshrc
fi

if ! [ -f "$HOME/.p10k.zsh" ]; then
cp "$MODDATA/.p10k.zsh" $HOME/.p10k.zsh
fi