GitHub ActionsでMagisk Patchを自動化する

*この記事は韓国語から翻訳したものです。不十分な部分があれば、いつでもフィードバックをいただければありがたいです！ ([オリジナル記事](https://blog.chungjungsoo.dev/dev-posts/magisk-automation/), 同じく私が作成しました。)*

GitHub Actionsを利用してMagiskパッチを自動化した経験を書いてみます。

## 既存の方法

Magiskをパッチする方法は下記の通りです。

1. Factory Imageをダウンロードします。
2.解凍後、boot.imgを抽出します。
3.boot.imgを携帯電話に移動した後、Magiskアプリでboot.imgを入れてパッチを適用します。
4.パッチを適用したboot.imgをコンピュータに戻し、携帯電話をfastbootモードにします。
5. `fastboot` コマンドを使ってboot領域にパッチを適用したboot.imgをフラッシュ(flashing)します。
6.再起動したら完了


アンドロイドスマホは毎月セキュリティーアップデートが出ますが、root権限を取得するため、毎月一回は上記のようなプロセスを繰り返す必要があります。簡単だと思うかもしれないが、個人的にはboot.imgをスマホに移してコンピュータに戻すこのようなプロセスは非常に非効率的だと思い、毎月繰り返しているうちに面倒になった。

## 短縮させた方法 (まとめ)

まず、一番非効率的だと思ったMagisk boot.imgのパッチから自動化することにしました。

1.JSONファイルを一つ作ります。これは手動で自動化プロセスで変数を制御するためです。

  ```json
  {
    "name": "crosshatch",
    "build_number": "RQ3A.210905.001",
    "link": "https://dl.google.com/dl/android/aosp/crosshatch-rq3a.210905.001-factory-94be46cc.zip",
    "checksum": "94be46cc1873c273aaf16df6af2020980f90a402ace9f3327c1b839093cc133c",
    "magisk": "https://github.com/topjohnwu/Magisk/releases/download/v23.0/Magisk-v23.0.apk"
  }
  ```

名前、ビルド番号、factory imageのダウンロードリンク、checksum、Magiskアプリのダウンロードリンクを入力しました。

2. GitHubに[repo](https://github.com/Taewan-P/crosshatch-magisk-generator)を一つ作って、GitHub Actionsを利用してワークフローを構築します。ワークフローの構築は `.github/workflows` に yml [ファイル](https://github.com/Taewan-P/crosshatch-magisk-generator/blob/main/.github/workflows/main.yml)を作成します。

   作業順序は次の通りです。

   1.Magiskパッチに必要なファイルを受け取る
   2. Factory imageをダウンロードしてboot.imgを抽出します。
   3. Boot.imgをパッチして、パッチを適用したboot.imgをアップロードします。

   a番とb番は依存関係がないので、並列で処理するためjobを分離して、c番はa番とb番が終わらないと開始できないので、依存関係を付与したjobを作成しました。
   
![workflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/a958652e-3938-affe-660a-1b4a8b095aa8.png)

   
3.Workflowを作動させて、結果物をreleasesタブで確認します。

![releases.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/212c5446-faa5-f8ae-8ed6-37e9dae0e2c4.png)


   成果物のタグは `コードネーム-ビルド番号`に設定しました。 万が一の場合に備えて、作業する前に純正のboot.imgも添付しておきます。


## 具体的な動作原理

具体的にどのように実装したのか見てみましょう。まず、Magiskがどのようにboot.imgをパッチするのかから説明します。

### Magiskがboot.imgにパッチを適用する方法

まず、Magiskがboot.imgをパッチするコードを見つける必要があります。パッチするコードは [`scripts/boot_patch.sh`](https://github.com/topjohnwu/Magisk/blob/master/scripts/boot_patch.sh) にあります。具体的にどのようなパッチが適用されるかはこの記事では説明しません。このファイルのコメントを読むと使い方が説明されています。

```
#!/system/bin/sh
#######################################################################################
# Magisk Boot Image Patcher
#######################################################################################
#
# Usage: boot_patch.sh <bootimage>
#
# The following flags can be set in environment variables:
# KEEPVERITY, KEEPFORCEENCRYPT, RECOVERYMODE
#
# This script should be placed in a directory with the following files:
#
# File name          Type      Description
#
# boot_patch.sh      script    A script to patch boot image for Magisk.
#                  (this file) The script will use files in its same
#                              directory to complete the patching process
# util_functions.sh  script    A script which hosts all functions required
#                              for this script to work properly
# magiskinit         binary    The binary to replace /init
# magisk(32/64)      binary    The magisk binaries
# magiskboot         binary    A tool to manipulate boot images
# chromeos           folder    This folder includes the utility and keys to sign
#                  (optional)  chromeos boot images. Only used for Pixel C.
#
#######################################################################################
```

### Magisk binaries

上で見たようにboot.imgをパッチするためにはbinaryファイルが3つも必要です。しかし、自動化の過程でビルドをすると時間がかかりすぎるので、prebuilt binaryを使う予定です。しかし、Magiskは別にprebuilt binaryを提供していません。Magisk v22.0以前はMagisk Managerというアンドロイドアプリとパッチをする時使うmagisk.zipというパッチファイルで提供していました。magisk.zipには私たちが今必要なbinaryがx86とARMバージョンが入っていましたが、v22.0からMagisk ManagerがMagiskに統合されてバイナリは提供されなくなりました。現在はMagisk.apk一つだけ提供しています。

しかし、v22.0から変わったインストール方法についてのドキュメントを見ると、deprecatedされたが、カスタムリカバリを使う場合のインストール方法についての[説明](https://topjohnwu.github.io/Magisk/install.html#custom-recovery)があります。その説明を読んでみると、apkファイルをzipに拡張子を変更してカスタムリカバリでフラッシュが可能だと書いてある...! この意味は、apkファイルの中にbinaryが含まれていることを示している。 そこで、apkファイルを解凍してみた。

```
├── AndroidManifest.xml
├── META-INF
│   ├── CERT.RSA
│   ├── CERT.SF
│   ├── MANIFEST.MF
│   ├── com
│   │   ├── android
│   │   │   └── build
│   │   │       └── gradle
│   │   │           └── app-metadata.properties
│   │   └── google
│   │       └── android
│   │           ├── update-binary
│   │           └── updater-script
│   └── services
│       ├── kotlinx.coroutines.CoroutineExceptionHandler
│       └── kotlinx.coroutines.internal.MainDispatcherFactory
├── assets
│   ├── addon.d.sh
│   ├── boot_patch.sh
│   ├── chromeos
│   │   ├── futility
│   │   ├── kernel.keyblock
│   │   └── kernel_data_key.vbprivk
│   ├── uninstaller.sh
│   └── util_functions.sh
├── classes.dex
├── lib
│   ├── arm64-v8a
│   │   └── libstub.so
│   ├── armeabi-v7a
│   │   ├── libbusybox.so
│   │   ├── libmagisk32.so
│   │   ├── libmagisk64.so
│   │   ├── libmagiskboot.so
│   │   └── libmagiskinit.so
│   ├── x86
│   │   ├── libbusybox.so
│   │   ├── libmagisk32.so
│   │   ├── libmagisk64.so
│   │   ├── libmagiskboot.so
│   │   └── libmagiskinit.so
│   └── x86_64
│       └── libstub.so
├── org
│   └── commonmark
│       └── internal
│           └── util
│               └── entities.properties
├── res
│   ├── --.xml
│   ├── ...
│   └── zq.xml
└── resources.arsc
```


`lib<ファイル名>.so` ファイルが私たちが探しているバイナリです...！x86バイナリも存在します。早速、ローカルでテストしてみます。

### Ubuntuでパッチしてみる

Ubuntuでパッチが可能なら、当然GitHub Actionsで実装が可能です。早速、テストしてみたら...


![first_boot_patch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/c2995ca8-5017-64f5-a899-b54923b12d70.png)



こんな結果が出ました。まず、目に見えるものから見てみると、dos2unixがないと表示されます。探してみると、これはUbuntuに存在する[パッケージ](https://packages.ubuntu.com/bionic/dos2unix)なので解決できるようです。

しかし、ここで `getprop` が問題になります。getprop` はアンドロイドだけ存在するコマンドです。携帯電話のプロパティ(properties)を取得する時使うコマンドですが、Ubuntuに存在するはずがありません。 なので、`getprop`コマンドを直接実装することにしました。


### `getprop` コマンドを実装してみましょう。

まず、アンドロイドで `getprop` を実行したらどうなるか見てみましょう。


![adb_shell_getprop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/f6bcba15-bc2e-599d-b649-616739cd1be2.png)



[プロパティキー]： [プロパティ値]`の順番で出てきます。しかし、`getprop`を実行すると出てくる属性が570個以上あるので、これを全部実装するのは無理です。また、すべての値がMagiskのパッチを適用する時必要ないため、呼び出す値だけ探してみました。

```
[ro.crypto.state]: [encrypted]
[ro.build.ab_update]: [true]
[ro.boot.slot_suffix]: [_a]
[init.svc.vold]: [running]
[ro.build.version.sdk]: [30]
[ro.product.cpu.abi]: [arm64-v8a]
```

スクリプトを検索した結果、上の6つの値だけ参照します。そのため、GitHub Actionsで使えるように `GETPROP_OUTPUT` というファイル名で出力結果を保存します。

次は `getprop` の実装です。`getprop` は追加の引数がない場合は全ての属性キーと属性値を出力し、特定の属性キーを引数に指定すると属性値だけを出力します。


![getprop_with_arg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/b1c804eb-d35b-fc89-e776-1a7b62a83c6a.png)


ここまでしか実装しない予定です。なぜなら、パッチスクリプトではそれ以上は使われないからです。


```shell
#!/bin/bash

# Ubuntu does not have command `getprop`. Only android does.
# This function is a simple implementation of getprop command, but
# gets value from GETPROP_OUTPUT file. <- You should copy/paste it from your phone.
# However is not fully implemented. Only shows property values when argument exists. 

getprop() {
	if [ -f GETPROP_OUTPUT ]; then
		local prop_output=`cat GETPROP_OUTPUT`
		if [ $# -eq 0 ]; then
			# No specific property. Return whole getprop
			echo "$prop_output"
		else
			local key="$1"
			local key_value=`grep $key GETPROP_OUTPUT`
			# IFS does not work in zsh!
			IFS=':'
			value=($key_value)
			unset IFS
			local result=${value[1]}
			echo $result | sed 's/[][]//g'
		fi
	fi
}

export -f getprop
```


このスクリプトを実行することで作成した関数を最後にexportまでしてくれます。テストしてみましょう。


![implemented_getprop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/70bbda1c-b3e2-2210-395b-894e63c0a121.png)


完璧に動作します！では、先ほど実行したパッチスクリプトをもう一度実行してみましょう。


![interpreter_error.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/d8bf3b5a-6ad0-0aff-5d06-c6ac4dc3e4a2.png)


interpreter errorが出ます。これは `boot_patch.sh` の最初の行に interpreter が `/system/bin/sh` と書かれているからです。元々Magiskアプリで動いていたスクリプトなので、アンドロイドのshのパスに合わせたものです。

sed`を使ってUbuntuに合うように`/bin/bash`に変更します。


![fixed_boot_patch_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/4b682af9-4d6a-f396-f8f7-3d6976d2276f.png)



![fixed_boot_patch_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/7b724252-9e2c-ce0d-e6e3-93c35d949b37.png)



成功です！あとは、上の作業をGitHub Actionsに移動すれば終わりです。

### GitHub Actionsでパッチを適用する

GitHub Actionsで構成したworkflowコードは長すぎるので[リンク](https://github.com/Taewan-P/crosshatch-magisk-generator/blob/main/.github/workflows/main.yml)に置き換えます。上で説明したように順番に進めればいいです。


![github_actions_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/2f66a92c-6d52-2ee5-09af-92903d2e5226.png)



この記事を書いてる時、追加的に補完してコミットして戻ったworkflowです。3分でパッチが終わりました。



### パッチされたboot.imgをテストしてみます。

本当にうまくパッチが適用されたかテストしてみます。パッチされたboot.imgをダウンロードしてfastbootモードをオンにします。


![flash_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/95f03595-9c3b-938c-b204-395cd46c6fa8.png)



成功しましたが... 無限起動になりました... ㅠㅠ 再び正規のboot.imgにパッチを適用しました。

原因は簡単で、boot.imgをx86バイナリにパッチを当てて問題が発生しました。 なので、armeabi-v7aフォルダにあるbinaryを利用してパッチを当ててあげる必要があります。しかし、armeabi-v7aはARMデバイスなので、ARM仮想マシンをGitHub Actionsで起動して、仮想マシンで作業する必要があります。 ちょうど、GitHub Actionsのサードパーティパッケージの中にARM仮想マシンをサポートする[パッケージ](https://github.com/uraimo/run-on-arch-action)がありました！すぐに適用してテストしたら正常に起動することができ、Magiskの認識も完璧でした。


![magisk_4_in_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/2e3dc752-8765-4d68-812a-8b6f3d164e6d.png)


## 解決すべきこと

この記事はこのブログだけでなく、VoLTE関連のカフェでも共有しました。その中でも活発に活動してるNuriroさんがコメントをつけてくれたのですが、内容は次の通りです。
> 言いたいことがいくつかありますが、他のことは全部省略して一番大きなことだけ話します。
> マジスクをインストールするデバイスのMagisk appで直接boot.imgをパッチしなければならない重要な理由があります。
そうしないと、純正のboot.imgファイルが/dataにバックアップされます。そうしないとバックアップができません。
> MagiskをインストールするデバイスでMagiskアプリで直接boot.imgをパッチを当てずに、他のところで予め作っておいたmagisk_patched.imgをbootパーティションにフラッシュしてもMagiskのインストールはできます。
また、マジスクの機能を使うのにも問題はありません。
しかし、そうすると、後でマジスクを削除しようとする時、問題が発生します。
> 1) イメージ復旧 - 純正boot.imgファイルのバックアップがないので、イメージ復旧ができません。
> 2) 完全に削除 - 他の過程は問題ありませんが、やはり、純正boot.imgファイルのバックアップがないので、bootパーティションを純正に復旧することができません。
   この場合、fallbackでラムディスクの変更内容をいちいち戻す方式で回復されますが、こうすると、bootパーティションが純正と100%同じになりません。
   そうすると
       - 再起動後bootloopが発生したり、
       - 再起動に成功しても、後でOTAシステムアップデートができない問題が発生します。このようにしばらく経ってから他のところで問題が発生すると、すごくパニックになり、問題の原因が何なのか考え出すのが簡単ではありません。
> したがって、もし、純正boot.imgをMagisk appで直接パッチを当てずに、事前にパッチを当てたmagisk_patched.imgをフラッシュしてMagiskをインストールした場合、Magiskのアンインストールは
    1. 純正boot.imgファイルを手動でbootパーティションにフラッシュするか(これは'イメージ復旧'のようなものです)
    2. Magiskアプリで純正boot.imgにパッチを当てる。
       --> Magisk appを完全に終了(Recents screenからMagisk appをスワイプして削除)
       --> その後Magisk appを再起動して7-8秒wait
       こうして純正boot.imgのバックアップが/dataに生成されるようにした後、Magisk appで[アンインストール] / [イメージ復元] または[アンインストール] / [完全削除]をする必要があります。
作ったものと似たようなtoolのアイデアは私も以前ちょっと考えたことがあるのですが、
boot.imgがバックアップされない問題を綺麗に解決する方法が頭に浮かびませんでした。
もしかしたら、この問題はどのように解決する予定なのか、可能であれば、その部分についての考えを聞いてみたいです。

この部分はまだ自動化で解決できていませんが、今後解決してみたいと思いました。