GitHub ActionsでXamarin AndroidをReleaseビルド (ストア提出用)

GitHub ActionsでXamarin AndroidをReleaseビルドする方法です。
とりあえずyamlから。解説はその下にあります。

* [GitHub公式](https://docs.github.com/ja/actions/guides/building-and-testing-xamarin-applications)
* [当該ファイル](https://github.com/kurema/EarphoneLeftAndRightAndroid/blob/master/.github/workflows/build.yml)

```yaml
name: CI on Push and Pull Request
on:
  push:
    tags: 'v*'
  workflow_dispatch:
  pull_request:
jobs:
  Android:
    runs-on: macos-latest
    env:
      SlnPath: Earphone/
      AndroidCsprojPath: EarphoneLeftAndRight/EarphoneLeftAndRight.Android/
      AndroidCsprojName: EarphoneLeftAndRight.Android.csproj
      AndroidAppName: com.github.kurema.earphoneleftandright
    steps:
    - uses: actions/checkout@v1
# For Windows    
#    - name: Add msbuild to PATH
#      uses: microsoft/setup-msbuild@v1.0.3
    - name: write keystore
      run: |
        echo $KEYSTORE_BASE64 | base64 --decode > ${{ env.SlnPath }}${{ env.AndroidCsprojPath }}github.keystore
      env:
        KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64_ENCODED }}
    - name: Get tag
      id: tag
      if: contains(github.ref, 'tags/v')
      uses: dawidd6/action-get-tag@v1
    - name: Is tagged version
      id: tagged
      run: echo '::set-output name=tagged::yes'
      #https://pione30.hatenablog.com/entry/2021/02/05/015545
      if: contains(github.ref, 'tags/v')
    - name: android package format
      id: apf
      run: |
        if [ "${{steps.tagged.outputs.tagged}}" = "yes" ]; then
          echo '::set-output name=format::aab'
        else
          echo '::set-output name=format::apk'
        fi
    - name: Android
      run: |
        cd ${{ env.SlnPath }}
        nuget restore
        msbuild ${{ env.AndroidCsprojPath }}${{ env.AndroidCsprojName }} /verbosity:normal /t:Rebuild /t:PackageForAndroid /t:SignAndroidPackage /p:Configuration=Release /p:AndroidKeyStore=True /p:AndroidSigningKeyStore=github.keystore /p:AndroidSigningStorePass=${{ secrets.KEYSTORE_PASSWORD }} /p:AndroidSigningKeyAlias=github /p:AndroidSigningKeyPass=${{ secrets.KEYSTORE_PASSWORD }} /p:AndroidPackageFormat=${{ steps.apf.outputs.format }} /p:AotAssemblies=true /p:EnableLLVM=true
    - name: Build Apk version
      run: |
        cd ${{ env.SlnPath }}
        msbuild ${{ env.AndroidCsprojPath }}${{ env.AndroidCsprojName }} /verbosity:quiet /t:Build /t:PackageForAndroid /t:SignAndroidPackage /p:Configuration=Release /p:AndroidKeyStore=True /p:AndroidSigningKeyStore=github.keystore /p:AndroidSigningStorePass=${{ secrets.KEYSTORE_PASSWORD }} /p:AndroidSigningKeyAlias=github /p:AndroidSigningKeyPass=${{ secrets.KEYSTORE_PASSWORD }} /p:AndroidPackageFormat=apk /p:AotAssemblies=true /p:EnableLLVM=true
      if: contains(github.ref, 'tags/v')
    - name: delete keystore
      run: |
        rm ${{ env.SlnPath }}${{ env.AndroidCsprojPath }}github.keystore
      if: always()
    - uses: actions/upload-artifact@v2
      with:
        name: Android App
        path: ${{ env.SlnPath }}${{ env.AndroidCsprojPath }}bin/Release/${{ env.AndroidAppName }}-Signed.*
      if: ${{ !contains(github.ref, 'tags/v') }}
    - name: Create release
      id: create_release
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      if: contains(github.ref, 'tags/v')
      with:
        tag_name: ${{ github.ref }}
        release_name: ${{steps.tag.outputs.tag}}
        draft: false
        prerelease: false
    - name: Update release asset
      uses: actions/upload-release-asset@v1
      if: contains(github.ref, 'tags/v')
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.create_release.outputs.upload_url }}
        asset_path: ${{ env.SlnPath }}${{ env.AndroidCsprojPath }}bin/Release/${{ env.AndroidAppName }}-Signed.aab
        asset_name: ${{ env.AndroidAppName }}-Signed.aab
        asset_content_type: application/zip
    - name: Update release asset Apk
      uses: actions/upload-release-asset@v1
      if: contains(github.ref, 'tags/v')
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.create_release.outputs.upload_url }}
        asset_path: ${{ env.SlnPath }}${{ env.AndroidCsprojPath }}bin/Release/${{ env.AndroidAppName }}-Signed.apk
        asset_name: ${{ env.AndroidAppName }}-Signed.apk
        asset_content_type: application/zip
```

ちょっとおかしなところもありますが気にしないでください。試行錯誤の結果です。

# 宣伝
イヤホンの左右をQuick Settings Tileから確認するアプリを最近リリースしました。
これはその時、GitHub Actionsを利用しビルドした時の記録です。

* [Google Play](https://play.google.com/store/apps/details?id=com.github.kurema.earphoneleftandright)
* [GitHub](https://github.com/kurema/EarphoneLeftAndRightAndroid)
* [YouTube紹介動画](https://www.youtube.com/watch?v=4K_icWsNJlY)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146467/194bdc3c-9fbe-1196-8c59-1a076c6c9535.png)


# 解説
## メリット
CI/CDの一般的な話以外に、GitHub ActionsでReleaseビルドをするメリットはいくつかあります。
個人開発で自分しかレポジトリに触らないとしても対応しておくのがおすすめです。

* tagから自動で(GitHubの)Releaseを作成してくれる。
* Releaseタブからapkをインストールできる。
    * 世の中にはGoogle Playを利用できないAndroid端末利用者が結構います。そういう人への最低限の対応としてapk直接配布は便利です。
* Visual Studio CommunityなどのユーザーでもEnterprise機能が手軽に使える。
    * 特にAOTコンパイルに対応できる！
* 無料なのでお得感がある。
* 本格的な雰囲気が増す。ポートフォリオやアピールとしても、自己満足としても良し。

## なぜmacOS？
GitHub Actionsでのビルドは以下の価格になっています。

| OS | 料金 / 分 |
| -- | -- |
| Linux | $0.008 |
| macOS | $0.08 |
| Windows | $0.016 |

macOSはLinuxの10倍。クソ高いです。でも公式ドキュメントでもmacOSを使っています。
おそらく理由は以下です。

* iOSアプリはmacOSでしかビルドできない。Xamarin FormsでiOSを含むプラットフォームをビルドするならmacOS一択。
* Windowsでビルドすると、「パス名が長すぎます」系のエラーで落ちることが多い (Xamarin Androidあるある)。
* パブリックレポジトリなら無料。

Xamarin Androidアプリだけならパス名の問題を何とか回避すればWindowsでもビルドできますし、Linuxならパス名の問題は発生しないはずです。
プライベートレポジトリでビルドする方はmacOSは避けた方が節約になるでしょう。
パブリックレポジトリでビルドする方はどうせ無料なのでmacOSを使いましょう。

Windowsでビルドする場合、以下の追加が必要です。

```yaml
    - name: Add msbuild to PATH
      uses: microsoft/setup-msbuild@v1.0.3
```

## APIキーなど秘匿したいファイルがある
一応言っておくと、アプリとして公開する以上はガチの秘匿は出来ません。
ですが、とにかく`.gitignore`でアップロードしていないファイルを追加するなら以下のようにすればできます。

```yaml:テキストファイル
    - name: write secret.cs
      run: |
        echo $SOURCE> SolutionFolder/ProjectFolder/secret.cs
      env:
        SOURCE: ${{ secrets.SECRET_CS }}
```

```yaml:バイナリ
    - name: write binary
      run: |
        echo $BINARY | base64 --decode > out.bin
      env:
        BINARY: ${{ secrets.BINARY_BASE64_ENCODED }}
```

```yaml:置換(広告IDなど)
    - name: Replace Ad Unit ID
      run: |
        sed -i -e "s@$ADMOB_OLD@$ADMOB_NEW@g" ${{ env.SlnPath }}${{ env.AndroidCsprojPath }}AdMobBannerRenderer.cs
      env:
        ADMOB_OLD: ca-app-pub-3940256099942544/6300978111
        ADMOB_NEW: ${{ secrets.ADMOB_UNIT_ID_BANNER }}
```

Xamarin Androidでコンパイル後の情報を秘匿したい場合、難読化も必要です。
ですが難読化も手間を掛ければ、あるいは掛けなくても回避できます。
[Microsoft Docs](https://docs.microsoft.com/ja-jp/xamarin/android/deploy-test/release-prep/)では以下の手段が示されています。

* 「アセンブリをネイティブ コードにバンドルする」をオンにする。
* Dotfuscatorによるアプリケーションの保護。

XamarinではProGuardやr8は難読化の効果はありません。
「アセンブリをネイティブ コードにバンドルする」も最低限の難読化にしかなりません(後述)。
Dotfuscatorは試したことはないのでよく分かりません。

## Enterpriseライセンス機能を使う
Xamarin関係のいくつかの機能はVisual Studio Enterpriseライセンスが必要です。
GitHub Actionsのメリットの一つがそうした機能を手軽に使えることです。
Windowsの場合はVisual Studio Enterprise 2019が入っていますし、macOS版のXamarinでも普通にEnterprise機能が使えるみたいです。

これらの機能を使う一番手っ取り早い手段はcsprojを直接操作することです。
「Enterpriseライセンスが必要」と言うのはUIという話で、直接操作することは普通にできます。
なんならMSBuildを直接叩けばWindowsでEnterpriseライセンスを持っていなくても普通にコンパイルが通りそうな気もしますね。試してませんが。
ですから、csprojを書き換えてGitHub ActionsでビルドすればEnterprise機能は普通に使えます。

ただこれには一つ問題があって、Visual Studioでcsprojを操作すると知らない内に設定が戻っていたりします。
その場合は`msbuild`行のコマンドライン引数で固定することができます。
`/p:Configuration=Release`のような形です。
ただし、ローカルのcsprojと違う条件でコンパイルするわけですから、忘れて混乱することがあり得るので気を付けてください。

### AOTコンパイル
Enterprise機能で個人的に一番美味しいと思うのはこれです。
AOTコンパイルをすると起動が速くなり、APKのサイズが大きくなります。
Xamarin Androidアプリで気になるのが起動の遅さなのでこれは本気で嬉しい奴です。

msbuildの行に以下を追加してください。
もちろんcsprojのファイルをローカルで操作しても、GitHub Actions上で操作しても構いません。
ここではついでに「LLVM 最適化コンパイラ」もオンにしています(...は省略の意味です)。

```
msbuild ... /p:AotAssemblies=true /p:EnableLLVM=true
```

アプリサイズの増減は以下のようになりました。ABIによる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146467/22aba264-0609-2199-b54d-6fc9b0047c38.png)


### アセンブリをネイティブコードにバンドルする
これは「最低限の難読化」として機能する、つまり気休めにしかならないようです。
ただapkを解凍すればdllファイルがあって逆コンパイラで丸見え、という状況よりは技術的なハードルが上がるかもしれません。
またapkのサイズが小さくなるようです。こちらが主な目的でしょう。

Android App Bundleと同じようなものみたいなので私はオフにしています。
今回作ったXamarin Androidアプリはオープンソースですし、apkサイズはあまり気にしていないので、特にメリットは感じません。

GitHubの[Issue](https://github.com/xamarin/xamarin-android/issues/4527)が参考になります(英語)。

試していませんが、多分以下の行でオンにできます。

```
msbuild ... /p:BundleAssemblies=true
```

## Android App Bundleとapk
「2021 年 8 月より、Google Play での新規アプリの公開は Android App Bundle で行う必要があります」とのことで、ストア提出はAndroid App Bundleは必須です。
一方、直接インストールする際はapkを使うので使い分けが必要です。
今回git tagからGitHubのReleaseを生成するようにしていますが、そこにapkとaabの両方を添付しています。後者は自分用です。

```
msbuild ... /p:AndroidPackageFormat=aab
```

* [Android App Bundle について](https://developer.android.com/guide/app-bundle?hl=ja)

## コードshrinker / リンカープロパティ
プロジェクトの設定内「Androidオプション」で、「コードshrinker」を「r8」に、「リンク中」を「なし」以外にするとapkのサイズが小さくなるようです…がおすすめしません。
ビルド失敗の原因となるので、少なくとも最初は「コードshrinker」を「」に、「リンク中」を「なし」に設定しましょう(Visual Studio上で)。

詳しくは[こちらの記事](https://www.neputa-note.net/2021/03/xamarin-linker-r8.html)が参考になります。
要は辞めておいた方が良いようです。
2021年に数MBを節約するため苦労するのはおすすめしません。

## 鍵生成
鍵の生成はkeytoolを用います。
Visual Studio上では「Android adbコマンドプロンプトを開く」内で実行できます。
概ねこんなコマンドです。
(JKSキーストアの場合)パスワードはキーストアに対するものと個別の鍵に対するものがあるようなので、既存のkeystoreのパスワードを変更してSecretsに設定しようとする場合など注意してください(少しハマりました)。

```
keytool -genkeypair -keyalg RSA -keysize 4096 -validity 36500 -alias github -keystore github.keystore -deststoretype pkcs12
```

生成した鍵はストレージの破損で失われないよう暗号化してバックアップしておくことをおすすめします。
昔はkeystoreを紛失するとアプリを更新できなくなるようですが、今はそうでもないようです。
でも紛失しないに越したことはありません。

生成した鍵はバイナリ形式なのでSecretsに設定するにはbase64に変換が必要です。
WSLがインストールされている場合は、該当フォルダで以下のようにすれば良いです。
Explorerのアドレスバーにbashと入力するのが速いのでおすすめです。

```bash
$ cat *.keystore | base64 > b64.txt
```

Secretsの設定方法などはググってください。

## if: ${{ !contains(github.ref, 'tags/v') }}
yamlの仕様上、`if: contains(github.ref, 'tags/v')`は良くても`if: !contains(github.ref, 'tags/v')`はダメなようです。
謎ですね。

* [GitHub Actions の if, 否定演算子, そして YAML](https://pione30.hatenablog.com/entry/2021/02/05/015545)

# おまけ
## 履歴
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146467/0443db15-0dca-7bf9-b5e3-d8ae353baa96.png)

GitHub Actionsあるある。
`Git Squash`？ 何ですかねそれ。

## 結果
公開後一カ月時点で**インストール数 4**(自分含む)、**広告収入1円未満**(0円表示)です。
Google Playは宣伝しないとほぼ全くインストールされないのでやる気が出ません。