nplの小部屋

しっかりブログ的なものを書くときもあれば雑なメモのときもある

MiriaがSnapパッケージになるまで

Misskeyを開いたら、それはミス廃への片道切符。もう、後戻りはできない―――。 …ふざけました。NPLです。

今回はMisskey専用クライアントアプリ「Miria」のSnapパッケージ化の記録を簡単にまとめていきます。

Advent Calenderの季節ですが、特に関係はありません。

注意

この記事の内容はほとんどプログラミングをしたことが無い人間によって書かれています。間違った内容が紛れている可能性が高いため、後述する参考記事や、その他複数の情報を参考にしながら読み進めてください。特に、この記事では解説していない内容も多数存在するため、Snapcraft.ioのドキュメントを参考にしてください

Miriaとは

Miriaは、そらいろ氏が開発した、FlutterベースのMisskeyクライアントアプリです。

Misskeyに特化した機能開発による「複数アカウントのタブ切り替え」や「(初の)MFMの互換表示」などの特徴が存在します。

タブには通常のHTLやLTLに限らず、リスト・アンテナ・チャンネル・ロールなどを割り当てることもできます。また、Misskeyクライアントとしては珍しいWebViewを使わないMFM互換表示を利用することができます。

そんなMiriaですが、Flutterをベースに開発されていることから、AndroidWindowsiOS/iPadOS/macOSでサポートされています。

そして、Flutter SDKを利用すれば、Linux用アプリを同一のコードでビルドすることができます。

今回は、MiriaをSnapパッケージで利用できるようにするため、snapcarftに挑戦してみることにしました。

注意:MiriaのSnapパッケージを作成する話であり、現在Snap Storeを検索してもMiriaは存在しません。

Snapパッケージについて

SnapパッケージはUbuntuの開発元Canonicalが推奨しているパッケージです。debパッケージと違い、必要な依存関係を開発元が本体のパッケージにまとめて配布する方式です。snapコマンドやSnap Store(Snapcraft.io)からパッケージのインストールを行うことができます。

debパッケージやrpmパッケージの方がユーザー数は多いとは思いますが、Flutter公式ドキュメントにSnapパッケージ化の方法が存在することや、Snapcraft側にもドキュメントが揃っていることから、Snapパッケージを選択しました。

Flatpakじゃだめだったの?

日本語記事が少ない時点で諦めました。

一応パッケージ化することはできたものの、仕様の理解が進まなかった故、維持管理に問題のある状態の設定ファイルが出来上がってしまったため、お蔵入りになりました。

参考

この記事の内容のほとんどは、gihyo.jpの「Snapパッケージ入門」を参考にしています。この記事を読む前に、参考記事の方を読むことをオススメします(この記事より詳細なことが書かれており、そちらの方が理解しやすいため)。

MiriaをSnapパッケージ化する

ここでは、Flutter公式ドキュメントに従ったパッケージングを行います。

これからFlutterアプリをSnapパッケージにするときはSnapcraftのドキュメントを読むことをオススメします。(理由

1, プロジェクトフォルダ内にSnapパッケージ用ファイルを作成する

ここではmiria-snapディレクトリをプロジェクトフォルダとして作業を行います。

事前にsnapcraftをSnapでインストールしておきます。

~$ sudo snap install snapcraft --classic

プロジェクトフォルダに入り、Snapパッケージ作成のためのファイルを追加します。

~$ mkdir miria-snap && cd miria-snap
~/miria-snap$ snapcraft init

これでプロジェクトフォルダ内にsnapフォルダとsnap/snapcraft.yamlファイルが作成されました。

2, snapcraft.yamlをドキュメントに習って編集する

初期状態のsnapcraft.yamlの中身はこのようになっています。

name: my-snap-name # you probably want to 'snapcraft register <name>'
base: core22 # the base snap is the execution environment for this snap
version: '0.1' # just for humans, typically '1.2+git' or '1.3.2'
summary: Single-line elevator pitch for your amazing snap # 79 char long summary
description: |
  This is my-snap's description. You have a paragraph or two to tell the
  most important story about your snap. Keep it under 100 words though,
  we live in tweetspace and your description wants to look good in the snap
  store.

grade: devel # must be 'stable' to release into candidate/stable channels
confinement: devmode # use 'strict' once you have the right plugs and slots

parts:
  my-part:
    # See 'snapcraft plugins'
    plugin: nil

まずはこのファイルをFlutterドキュメントに合わせたものに変更します。

- name: my-snap-name # you probably want to 'snapcraft register <name>'
+ name: miria-snap
- base: core22 # the base snap is the execution environment for this snap
+ base: core18
- version: '0.1' # just for humans, typically '1.2+git' or '1.3.2'
+ version: '1.0.13+87'
- summary: Single-line elevator pitch for your amazing snap # 79 char long summary
+ summary: Misskey Client for Mobile
description: |
-   This is my-snap's description. You have a paragraph or two to tell the
-   most important story about your snap. Keep it under 100 words though,
-   we live in tweetspace and your description wants to look good in the snap
-   store.
+   Misskey Client App built with Flutter
grade: devel # must be 'stable' to release into candidate/stable channels
confinement: devmode # use 'strict' once you have the right plugs and slots

parts:
-   my-part:
+   miria-snap:
    # See 'snapcraft plugins'
-     plugin: nil
+     plugin: flutter
+     source: https://github.com/shiosyakeyakini-info/miria.git
+     flutter-target: lib/main.dart
+
+ apps:
+   miria-snap:
+     command: miria
+     extentions: [flutter-stable]

今はFlutterドキュメントに合わせてcore22からcore18に変更していることを覚えておいてください。

基本的に前半部分はアプリの名前や説明欄などを編集しています。

confinementはユーザーに対するアプリのセキュリティ設定であり、開発中はdevmodeを選択しますが、リリース時にはstrictで動くようにしなければなりません(ただし、エディターなどのファイルの書き込みの制限を緩める必要のある場合にはclassicが使用できます)。

partsでは、パッケージに含めるプログラムの追加などを行います。今回の場合、miria-snapのパーツとして、MiriaのソースコードをGitHubから取得し(source)、Flutterのビルド環境を用意して(plugin)、lib/main.dartを対象としたビルドを行う(flutter-target)ように記述しています。

appsでは、実際に実行するコマンドの記述などを行います。今回はアプリ名がmiria-snapであるため、apps:miria-snap:の並びで作成します。アプリのインストール後に端末からmiria-snapを入力してアプリを実行することができるようになります。

apps:command:には、実際に実行されるプログラムのコマンドを入力します。ここではビルドしたmiriaの実行ファイル名"miria"を入力します。

Flutterはビルド成果物をbuild/linux/x64/release/bundle以下にlib, data, miriaを生成しますが、snapcraftはそれらをSnapアプリのフォルダのルートに配置します。つまり、コマンドの内容は{Snapパッケージのフォルダルート}/miriaのような配置になります。

apps:miria:としてしまった場合、アプリ名とコマンド名が一致せず、端末からの実行時にmiria-snap.miriaと入力しなければならないため、apps以下の名前とsnapcraft.yaml先頭のnameは一致するようにします。

apps:miria-snap:extentions:には、Flutterに関連するplugs環境変数などを自動的に追加するためflutter-stableを入力します。

基本的な内容はこれで書き終えたので、このままsnapcraft buildを実行すればSnapパッケージのビルドが開始されます。

ただし、このままではMiriaのビルドは完了しません。

3, 依存関係を追加する

ログを確認すると、Snapパッケージのビルド中の"Miria本体のビルド"が完了できていません。これは依存関係が不足していることが原因です。

Miriaではflutter_secure_storagemedia_kitが使用されており、ビルド時にはlibsecret-1-dev, libmpb-devが、ユーザーの使用時にはlibsecret-1-0, libmpv1が必要となります。

このため、ビルド専用の依存関係と、Snapパッケージに同梱する用の依存関係を追加します。

依存関係をparts:miria-snap:以下のそれぞれbuild-packagesstage-packagesに記述しました。

[...]
parts:
    miria-snap:
  [...]
+   build-packages:
+     - libsecret-1-dev
+     - libmpv-dev
+   stage-packages:
+     - libsecret-1-0
+     - libmpv1

参考1: flutter_secure_storage | Flutter Package libjsoncppは最近のバージョンから不要になりました

参考2: media_kit | Dart Package mpvstage-packagesに含めるとMPVプレーヤーがデスクトップアプリとして一緒にインストールされてしまうのでlibmpvのみで十分です。

これで再度snapcraft buildを実行すると、Miria本体のビルドまで実行することができます。

これ以降のステップに進むには、一旦生成されたSnapパッケージをインストールして、動作を確認しながら進めていくことをおすすめします。

~/miria-snap$ sudo snap install --dangerous --devmode ./miria-snap_1.0.13+87_amd64.snap

4, セキュリティレベルをstrictにする

ある程度動くものになってきたため、今度はconfinementdevmodeからstrictに切り替え、必要な機能の洗い出しを行います。

[...]
- confinement: devmode
+ confinement: strict

Snapでは必要な機能のみをplugで接続する必要があります。

extentionsflutter-stableを設定しているので、desktop, desktop-legacy, gsettings, opengl, wayland, x11は、自動的に接続されます。

この時点で再度ビルドを実行し、すべての機能が動作するか確認します。Miriaの場合、"動画の音声が聞こえない"、"ログイン情報が保存されていない"という、2つの不具合がみつかりました。

音声の再生は、audio-playbackというプラグ、ログイン情報の保存はpassword-manager-serviceというプラグによって管理されているため、この2つをapps:以下に追加します。

[...]
apps:
  [...]
+   plugs:
+     - audio-playback
+     - password-manager-service

参考: The flutter extension | Snapcraft documentation

ただし、password-manager-serviceは機密情報を扱うプラグであるため、アプリをインストールしても自動ではプラグが機能しないようになっています。 ユーザーにアプリを公開する前に、password-manager-serviceへのアクセスを許可する操作を行うよう告知することを忘れないでください。

$ sudo snap connect miria-snap:password-manager-service

参考: Snapcraft.yaml reference | Snapcraft documentation

5, ファイル選択ダイアログを表示できるようにする

Miriaには画像ファイルをノートに添付して投稿する機能がありますが、SnapにしたMiriaではファイル選択のメニューが表示されなくなっています。

Miriaのファイル選択機能はflutter_file_pickerによって呼び出されていますが、flutter_file_picker内部ではzenity --file-selection(もしくは、qarmakdialogでそれに類するコマンド)を呼び出しています。

Ubuntu Desktop自体にはzenityが含まれているものの、Snapからは呼び出すことができないため、zenityをパッケージに含めるようにする必要があります。

FLutterはGTKを使用したアプリであるためzenityで統一しました。

[...]
parts:
  [...]
+   zenity:
+     plugin: nil
+     stage-packages:
+       - zenity
+     prime:
+       - usr/bin/zenity
+       - usr/share/zenity/*

parts以下に、miria-snapとは別にzenityを作成し、zenityを用意して、primeでパッケージに関連するファイルをSnapパッケージに含めるように指定しました。

参考: Inform users with custom dialogs - doc - snapcraft.io

6, デスクトップアイコンを追加する

今のままではデスクトップアプリなのにアプリのメニューボタンが存在しない状態になってしまうので、デスクトップエントリーファイルとアイコン画像を追加します。

デスクトップエントリーファイルとアイコン画像は次のコマンドでsnap/guiディレクトリを作成して、そこに追加します。

~/miria-snap $ mkdir -p snap/gui
~/miria-snap $ touch snap/gui/miria-snap.desktop

デスクトップエントリーファイルには以下の内容を追加します。

[Desktop Entry]
Version=1.0
Name=Miria Snap
GenericName=Misskey Client
Type=Application
Exec=miria-snap
Icon=${SNAP}/meta/gui/miria-snap.png
Comment=Misskey client app for mobile (Linux build)
Comment[ja]=モバイル向けのMisskeyクライアントアプリ(Linux向けビルド)
Keywords=Misskey;Miria;みりあ
Terminal=false
StartupNotify=false

Execにはアプリ名を追加します。Iconsnap/gui/mria-snap.pngを使用する場合、snap以下のファイルはSnapパッケージ作成時に${SNAP}/meta以下にコピーされるため${SNAP}/meta/gui/miria-png.pngを指定します。

keywordsは、Firefoxのデスクトップエントリーを見たところ、各言語ごとに分ける必要がありそうです(キーワードが多くなければ1つにまとめたままでもいいとは思いますが…)。

7, バージョン情報を自動で取得する

現在はversionを直接記述しています(version: 1.0.13+87はこの記事作成時の最新バージョン)が、Snapパッケージビルド時にこのバージョンを手動で書き換えるのは面倒なので、毎回バージョン情報を自動で取得するように変更します。

1, 直接記述されたバージョン情報を削除

[...]
- version: 1.0.13+87
+ adopt-info: miria-snap

versionを削除する代わりに、バージョンを取得する処理を行うパーツ名(今回はmiria-snap)をadopt-infoに追加します。

2, ソースコード取得の処理を上書き

本来自動的に行われるソースコードの取得処理に変更を加えます。

[...]
parts:
  miria-snap:
+     override-pull: |
+       snapcraftctl pull
+       VERSION=$(cat pubspac.yaml | grep "version[:]" | cut -d " " -f 2)
+       snapcraftctl set-version "$VERSION"

snapcraftctl pullは本来行われるソースコードの取得処理を行うコマンドです。

VERSION変数に、pubspac.yamlに記述されたバージョンを代入して、snapcraftctl set-versionに渡しています。

Miriaではflutter pub run cider versionでバージョン情報のみを取得する適切な方法がありますが、この時点ではFlutterのセットアップが済んでいないため利用できません。そのため、cat, grep, cutを駆使した酷いコードになっています。

参考1: 第660回 自作のsnapパッケージをSnap Storeに公開する | gihyo.jp

参考2: Override build steps | Snapcraft documentation

完成(のはずだった)

これにてリリース可能なMiria Snapパッケージが完成しました。お疲れ様でした。

…で終わらなかった。

core18からの脱却

ここまで作ってきたSnapパッケージはcore18というUbuntu 18.04 LTSベースのビルドが行われていました(Snapパッケージビルド時に起動するLXDコンテナもUbuntu 18.04です)。

しかし、Ubuntu 18.04 LTSのサポート期限はリリースから5年の2023年であり、MiriaのSnapパッケージを作成し始めた頃にはサポート終了の直前でした。Snapcraftも同様にcore18のサポートを終了することは容易に予想でき(そして的中し)たので、Ubuntu 22.04 LTSベースのcore22に移行する必要があります。

core22に移行する

core18からcore22の間にはいくつか変更点があります。

1, Flutter Extentionの廃止

core18ではextentionsflutter-stableを設定することでplugなどが自動的に接続されていましたが、core22では用意されていません。

代わりとしてgnomeを選択する必要があります。

flutter-stablegnomeのプラグの内容はほとんど同じなので不要だったのかもしれない。

2, snapcraftctlの廃止

override-pulloverride-build内で処理を上書きするときに、既定の処理を呼び出すsnapcraftctlが廃止され、代わりにcraftctlを利用するようになりました。

override-pull内でsnapcraftctl pullとしていたコマンドやoverride-build内でsnapcraftctl buildとしていたコマンドなどは、どちらもcraftctl defaultに置き換えられます。

参考: Using the craftctl tool | Snapcraft documentation

置き換える

上記の2つを置き換えていきます。

extentions

[...]
apps:
  [...]
-   extentions: [flutter-stable]
+   extentions: [gnome]

上記で記した通り、flutter-stablegnomeに置き換えるだけです。使用するplugsに変更はないのでそのままです。

snapcraftctl

[...]
parts:
  miria-snap:
    [...]
-     snapcraftctl pull
+     craftctl default
    VERSION=$(cat pubspac.yaml | grep "version[:]" | cut -d " " -f 2)
-     snapcraft set-version $VERSION
+     craftctl set version=$VERSION

上記で記したsnapcraftctl pullcraftctl defaultの他、set-versionオプションをset version=<version>に置き換える必要があります。

これでコマンドの置き換えなどが完了しました。

これにてMiriaのSnapパッケージ化は一旦完了です。

まとめ

今回は、MiriaをSnapパッケージにするまでの流れを解説(記録?)しました。

基本的にはgihyo.jpの記事を参考に大まかに内容を理解してから、Snapcraftのドキュメントを参考に詳細を埋めていく形でMiriaをSnapパッケージにすることができました。

作ってみた感想としては、「とりあえずパッケージ化して動くものを作りたい」という面では悪くない選択肢ではないかと思います。

また、「もう少し日本でSnapパッケージを利用する記事増えないかな〜?」と期待しています。

今後

今回はパッケージ化するところ(「とりあえず動くものを作る」段階)まで記事にしましたが、今のままでは起動時間が長いことやパッケージサイズが200MBとちょと大きいという問題を抱えているため、これを改善する(「細かい調整」の段階)内容を時間があればまとめていきたいと思います。