Ubuntu Weekly Recipe

第654回snapパッケージング入門

Ubuntuではディストリビューションを問わず利用できる「ユニバーサルパッケージ」としてsnapパッケージを開発しています。今回はこのsnapパッケージを作るための基本的な手順を紹介しましょう。

今回とそのあと数回で、snapパッケージを作ってSnap Storeに公開するまでの流れを一通り解説しています。

snapcraftコマンドで作るsnapパッケージ

Ubuntuではディストリビューションをまたいで利用できるユニバーサルパッケージシステムとしてsnapを開発しています。これはもともとIoT向けのミニマルなOSと連携して動く、独自のパッケージングシステムとして開発された「Snappy Ubuntu Core」Ubuntu Weekly Topics 2014年12月12日号が、時間を経て通常のUbuntuでも利用できるように進化したパッケージフォーマットです[1]⁠。

Snapパッケージは、YAMLフォーマットで記述したメタデータを元に、⁠snapcraft」と呼ばれるツールでビルドします。さらにビルドしたパッケージはSnap Store経由で、誰でも配布することが可能です。このためパッケージフォーマットとしては「snapパッケージ」と呼称されることが多いものの、パッケージの情報をまとめたサイトは「snapcraft.io」であり、パッケージングを含めたシステムそのものは「Snaps」と呼ばれることが多いようです[2]⁠。

「ユニバーサルパッケージ」とはなんぞや、という御託は後回しにすることにして、実際にsnapパッケージを作る方法を今回は紹介していきましょう。

実は第476回Nextcloudに学ぶsnapパッケージ入門ですでにsnapパッケージの作り方は紹介しています。しかしながら、その記事ではすでに存在するパッケージを少しカスタマイズして使う方法の説明でした。今回はイチから手順を紹介することにします。

パッケージ構築環境の準備

snapパッケージをビルドする流れは、公式ドキュメントのSnapcraft overviewによくまとまっています。ビルドに必要なものは次の3種類です。

  • パッケージビルドのためのsnapcraftコマンド
  • ビルド環境を構築するための仮想マシンシステム
  • パッケージ情報を記述したYAMLファイル

snapcraftコマンド自体はsnapパッケージ化されているため、最新のパッケージをsnapコマンドでインストール可能です。またmacOSでもHomebrew経由でインストールできるようです。

$ sudo snap install snapcraft --classic

もうひとつ必要になるのが仮想マシンシステムです。従来のパッケージを作成したことがあるならおそらくご存知だと思いますが、何らかのパッケージを構築するためには、その構築時に必要なツール類をビルド環境にインストールしておく必要があります。たとえばC言語で作られたプログラムならCのコンパイラが必要ですし、特定のライブラリにリンクするソフトウェアならそのライブラリのヘッダーファイルが必要になります。

作ろうとしているパッケージごとに必要となるツールが異なるため、パッケージごとに異なる構築環境を用意する必要が出てきます。Debianパッケージならpbuilderやsbuildをはじめとするツール類によって、比較的簡単に隔離された構築環境を作成できますが、それでもソースパッケージをビルドするためにはホストに「なんやかんや」をインストールしなくてはならないことがままあります。

snapcraftでは、パッケージのビルド作業はすべて仮想マシンの中で行うようにして、構築環境の隔離性、構築作業の再利用性を高めています。特に指定しなければ隔離環境としてMultipassを利用した仮想マシンシステムを作ります。よって仮想化支援機構がついたCPUに、それなりのメモリーサイズとストレージ容量を用意しておいてください[3]⁠。

MultipassはUbuntuだけでなくmacOSやWindowsでも利用できるため、HomebrewでインストールしたsnapcraftでもMultipassを使ってビルドできるというわけです。Multipassそのものについては、第590回のWindows/macOS/Linuxで使える仮想マシン管理ツール『multipass』を参照してください。

Multipassはsnapcraftの初回実行時にインストールするかどうかを問われます。もちろんあらかじめインストールしておいてもかまいませんので、ここではインストール方法を紹介しておきます。

$ sudo snap install multipass --classic

一点、注意が必要です。MultipassはLegacy iptablesのみサポートしており、nftablesはまだ対応できていません。しかしUbuntu 20.10以降はnftablesが使われるようになっています。このため20.10以降でsnapcraft/multipassを使う場合は、Multipassのiptablesルールをnftablesに合わせる必要があります。これをしておかないとインスタンスの中からインターネットへ通信できないので注意してください。

$ for table in filter nat mangle; do \
  sudo iptables-legacy -t $table -S | grep Multipass | \
  xargs -L1 sudo iptables-nft -t $table \
done

もちろんUbuntu 20.04 LTS以前で、なおかつnftablesに手動で移行していない場合は対応不要です。

サンプルパッケージの作成

snapcraft initコマンドを使うと、パッケージのメタデータであるsnapcraft.yamlを生成してくれます。何かパッケージを作るなら、まずはここから始めると良いでしょう。

$ mkdir snaptest && cd snaptest
$ snapcraft init
Created snap/snapcraft.yaml.
Go to https://docs.snapcraft.io/the-snapcraft-format/8337 for more information about the snapcraft.yaml format.
$ ls -R
.:
snap

./snap:
snapcraft.yaml

作られたのはsnap/snapcraft.yamlだけです。ここにパッケージングに関するすべての情報を記載していきます。現時点での内容を確認しましょう。

$ cat snap/snapcraft.yaml
name: my-snap-name # you probably want to 'snapcraft register <name>'
base: core18 # 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

逐一コメントが入っているので、比較的把握しやすいはずです。詳細はsnapcraft formatのページを参照してください。

  • name:パッケージの名前です。ちなみに構築時に作られる仮想マシンも同じ名前になります。小文字の英数字およびハイフンのみ利用可能で、Snap Storeで配布する際は一意である必要があります。snap findでマッチするパッケージ名がないか確認しておくと良いでしょう。
  • base:後述するベースシステムになるsnapパッケージを指定します。
  • version:パッケージのバージョンです。最大32文字で、YAML的に文字列として認識させるためにシングルクオートでくくることをおすすめします。ちなみに「git」を記述するとgit describeの結果がバージョンになります。
  • summarysnap find時に表示される78文字以内の概要です。
  • descriptionsnap info時に表示されるより詳細な説明です。概要だけでなく、そのパッケージの利用方法などを説明することもよくあります。
  • grade:そのパッケージの状態を示します。完全な開発版ならdevelを、一般的に利用できる状態ならstableを記述します。
  • confinement:ホストのリソースへのアクセス権限に関する情報を記述します。strictがsnapの通常状態でリソースへのアクセスが制限され、ユーザーによる許可が必要になります。classicはホストのリソースの利用を前提としたパッケージに使われます。classicを指定するためにはSnap Store側でレビューを受けた上で特別な許可が必要です。devmodeは開発時に使うモードで、原則としてリソースへのアクセスは制限されません。devmodeではSnap Store経由の公開はできません。
  • parts:実際にパッケージをビルドする手順やコンテンツの場所などを記述します。

base snapはそのsnapパッケージが動くベースシステムを提供するsnapパッケージです。snapパッケージは依存関係を原則としてすべてパッケージに内包するシステムですが、libcのように「ほとんどすべてのsnapパッケージで必要とするコンテンツ」までsnapパッケージごとに保持するのは不経済です。そこでbase snapで必要最低限のシステムを提供し、すべてのsnapパッケージはいずれかのbase snapに依存することになっています。2021年2月時点で、次のようなbase snapが存在します。

  • core20:Ubuntu 20.04 LTSのパッケージを元に作られたbase snap
  • core18:Ubuntu 18.04 LTSのパッケージを元に作られたbase snap
  • core:Ubuntu 20.04 LTSのパッケージを元に作られたbase snap
  • bare:何も提供しないbase snap

core20は2021年2月に正式リリースされたばかりのbase snapですUbuntu Weekly Topics 2021年2月5日号⁠。新規にsnapパッケージを作るならcore20を使いたいところですが、いくつかの拡張機能はまだcore20をサポートしていないため、今の段階ではcore18を使うことをおすすめします。

bareはたとえばすべてのライブラリを静的にリンクした単一のバイナリのみを提供するようなsnapパッケージに使われます。ただ、結局のところどんなツールもなんやかんやのシステムライブラリやファイルが必要になるため、使いどころが難しいです。Go言語のようなシングルバイナリ化しやすい言語のツールをsnap化するなら使えるかもしれません。

具体的なパッケージングの情報はpartsに記載します。partsは辞書形式になっており、複数のツールを同梱する場合はその数だけ情報を記述していきます。

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

今回の例だと作るのはmy-partという名前の一個のソフトウェアだけです。一般的なビルド方法はsnapcraft pluginsとして共通化しています。たとえばautotoolsに対応しているならautotoolsプラグインを、pipでインストールするタイプならpythonプラグインを使えば、snapcraft.yamlの記述が楽になります。

今回は「何も入っていない空のパッケージ」を作るため、plugin: nilとしています。

これで「必要最低限の設定」は完了しました。実用的なsnapパッケージを作る場合は、ここからsnapcraft.yamlにいろいろな情報を追記していくことになります。しかしながら、その対応は次回以降に後回しすることにして、今回はこのままパッケージをビルドしてみましょう。

サンプルパッケージのビルド

snapパッケージのビルドはシンプルで、次のコマンドを実行するだけです。

$ snapcraft --debug
Launching a VM.                                                                                                                               Launched: snapcraft-my-snap-name
(snip)
Pulling my-part
+ snapcraftctl pull
Building my-part
+ snapcraftctl build
Staging my-part
+ snapcraftctl stage
Priming my-part
+ snapcraftctl prime
Snapping |
Snapped my-snap-name_0.1_amd64.snap

Multipassをインストールしていないと、⁠Multipassをインストールするか」問い合わせがあるので「Y」と答えておきましょう。その後、次のような流れでビルドが行われます。

  1. ビルド用の仮想マシンを作成・起動する
  2. ローカルの環境を仮想マシンにsshfsでマウントする
  3. snap/snapcraft.yamlの記述に従ってビルドを実施する
  4. ビルドした成果物をひとつのSquashFSにまとめる
  5. snapパッケージの完成
  6. 仮想マシンのシャットダウン

パッケージファイルはパッケージ名_バージョン名_アーキテクチャー.snapという名前で作られます。

--debugを付けるとエラー発生時にビルド処理を行っている仮想マシンにログインしてシェルを起動します。ホスト上の端末でsnap/snapcraft.yamlを変更した上で、起動したシェルで再度snapcraftコマンドを実行したらビルドを継続してくれます。何がおかしいかをトライアンドエラーで調べたい際に便利です。

作成したパッケージのインストール

作成したsnapパッケージは次の方法でインストールできます。

$ sudo snap install my-snap-name_0.1_amd64.snap --dangerous --devmode
my-snap-name 0.1 installed

$ snap info my-snap-name
name:      my-snap-name
summary:   Single-line elevator pitch for your amazing snap
publisher: –
license:   unset
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.
refresh-date: today at 23:11 JST
installed:    0.1 (x1) 4kB devmode

confinementdevmodeなため、--demodeオプションは必須です[4]⁠。将来的にSnap Storeで公開するためには--devmodeなしでインストールできるようにする必要があります。今はまだSnap Storeで署名されていないパッケージをインストールするため、--dangerousを付ける必要もあります。

実際にパッケージの中身を見てみましょう。

$ ls -R /snap/my-snap-name/
/snap/my-snap-name/:
current  x1

/snap/my-snap-name/x1:
meta

/snap/my-snap-name/x1/meta:
snap.yaml

パッケージのメタデータだけがインストールされており他は何もないパッケージですね。ちなみにless my-snap-name_0.1_amd64.snapでも中身を確認できます。

サンプルパッケージを削除するには次のコマンドを実行してください。

$ sudo snap remove --purge my-snap-name
my-snap-name removed

適当なファイルを追加でインストールする

--debugオプションの動作確認も兼ねて、何か適当なファイルを追加でインストールしてみましょう。まずsnapcraft.yamlのpartsを次のように編集します。

parts:
  my-part:
    # See 'snapcraft plugins'
    plugin: nil
  my-part2:
    plugin: dump
    source: local-files/

dumpプラグインはソースツリーにある任意のファイルをそのままsnapパッケージに取り込むためのプラグインです。sourceで指定したファイルを、パッケージのルートファイルシステムの中にそのままコピーされます。ちなみにファイルとしてはローカルファイルだけでなく、HTTP経由のリソースやgitリポジトリなども指定可能です。

このままパッケージを作ってみましょう。今はまだlocal-filesが存在しないため、エラーになるはずです。

$ snapcraft --debug
Launching a VM.
Failed to pull source: unable to determine source type of 'local-files/'.
Check that the URL is correct or consider specifying `source-type` for this part. See `snapcraft help sources` for more information.
snapcraft-my-snap-name #

パッケージのビルドが失敗し、ビルド用仮想マシンのシェルが立ち上がりました。そこでlocal-filesディレクトリを作り、適当なファイルを配置してみましょう。ビルド用仮想マシンのシェルの中もしくはホスト上のパッケージディレクトリで次のコマンドを実行してください。

$ mkdir local-files
$ touch local-files/test.txt

ビルド用仮想マシンのシェルの中で再度snapcraftコマンドを実行します。

snapcraft-my-snap-name # snapcraft
(snip)
Snapped my-snap-name_0.1_amd64.snap
snapcraft-my-snap-name #

今度は無事にパッケージが完成したようです。exitコマンドでログアウトして、作られたパッケージの中身を確認してみましょう。

$ less my-snap-name_0.1_amd64.snap | cat
path:       "my-snap-name_0.1_amd64.snap"
name:       my-snap-name
summary:    Single-line elevator pitch for your amazing snap
version:    0.1 devmode
build-date: today at 23:21 JST
license:    unset
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.

*** Contents:
Parallel unsquashfs: Using 8 processors
2 inodes (1 blocks) to write

drwxr-xr-x root/root                43 2021-02-11 23:21
drwxr-xr-x root/root                32 2021-02-11 21:40 /meta
-rw-r--r-- root/root               407 2021-02-11 23:21 /meta/snap.yaml
-rw-rw-r-- root/root                 0 2021-02-11 23:20 /test.txt

きちんとlocal-files以下のtest.txtがインストールされるようになりました。

このようにsnapcraftは、構築エラー時にある程度のエラーメッセージとリファレンスを表示して、動作確認用の環境を提供してくれます。あとは「snapcraft.yamlを編集する」⁠ビルドする」を繰り返せば、求めるパッケージが作れるはずです。ちなみにビルド時のエラーのよくある例については公式ドキュメントのDebugging building snapsも参考になるでしょう。

ユニバーサルパッケージとは

最後にsnapパッケージやそれに類いするパッケージフォーマットで言及されることの多い、ユニバーサルパッケージについて説明しておきます。

「ユニバーサルパッケージ」とはLinuxディストリビューションに依存しないパッケージの俗称です[5]⁠。簡単にまとめると「LinuxシステムにAndroidやiOSのようなパッケージ管理機構をもたらすもの」と言えます。

歴史的経緯から、Debian系ならAPT/dpkg、RHEL系ならDNF/YUM/RPMなど、openSUSE系ならZypper/RPMなど、Linuxにおいてソフトウェアパッケージとその管理システムはディストリビューションに強く紐付いています。結果、ソフトウェアデベロッパーがバイナリパッケージを提供しようとすると、ディストリビューションごとのパッケージ作成方法を学ばなくてはなりません。結果として、ソースコードを配布しつつ開発者の好みのディストリビューションのみバイナリパッケージが提供されていることもよくあります。

パッケージングにおいて最もネックになるのが「依存関係の解決」です。特定のソフトウェアをビルドするためには、別のソフトウェアが必要であり、実行時にはさらに別のソフトウェアが必要になる、そんな依存関係を逐一解決しながらパッケージを作る必要があります。しかもその依存関係はソフトウェアの修正や、ディストリビューション側の変更によって随時変わっていきます[6]⁠。

そんな状況を打破するために考えられたのが「ユニバーサルパッケージ」という概念です。フォーマットによって多少違いはあるものの、おおよそ次のような機能を備えたものになります。

  • ディストリビューションをまたいで、同じバイナリパッケージをインストール可能
  • 依存関係は原則存在せず、必要なものはすべてパッケージの中に取り込んだ状態で提供する
  • サンドボックス化によるセキュリティの向上と、ディストリビューションとアプリケーションの分離の推進

ソフトウェアは多かれ少なかれ何らかの別のソフトウェアに依存しています。Linux上で動くELFバイナリであれば、最低限libcやリンカー・ローダーに依存しているでしょう。Pythonなどのスクリプト言語はそれぞれの処理系に依存しているはずです。これらはディストリビューションやそのリリースごとに異なるバージョンを採用しています。つまり「あるバージョンでは動く」ソフトウェアであっても、別のバージョンでは動かないことがままあります[7]⁠。

また「依存関係の解決方法」はディストリビューションごとに異なります。Debianならパッケージ「FOO」さえインストールすれば動いていたものが、RHELなら「BAR」「BAZ」の両方が必要ということもよくあります。また、コンパイル時のオプションを変える必要もあるかもしれません。

結果として任意のバイナリパッケージは、ディストリビューション・リリースごとに異なるパッケージデータになることが一般的です。

ユニバーサルパッケージでは、原則として依存するものをすべてひとつのパッケージの中に取り込みます。これにより「Linuxカーネルとパッケージ管理システムが動いているシステムならどこでも利用可能」「気軽にインストール・アンインストール・ロールバックが可能」なパッケージフォーマットを実現しているのです。

依存関係に基づくバージョンもパッケージを作る側でコントロールできるため、⁠ソフトウェア開発者が動作確認している組み合わせ」をそのままパッケージとして提供できます。よってバージョン不一致に伴う予期せぬトラブルや、RHELなら問題ないのにUbuntuならエラーになる、といった状況も防げる可能性が出てきます。

しかしながらこれらのメリットを実現するためには、次のようなデメリットも発生します。

  • パッケージサイズが大きくなる
  • ライブラリとの柔軟な組み合わせができない
  • パッケージ管理システムそのものの複雑性が増える
  • サードパーティのパッケージの利用に対する是非

ディストリビューションのパッケージは、そのディストリビューションをよく知っているメンテナーがパッケージを作成・管理しています。これにより柔軟かつ一貫性が保たれたパッケージを提供できていました。普段のLinuxマシンには多種多様なソフトウェアがインストールされているにも関わらず、アップグレード時に「多少のトラブル」で済んでいるのが、その証左でしょう。

この品質の高さが従来のパッケージ管理システムの強みであり、ユニバーサルパッケージで実現するのが難しい部分です。結果的に、従来のパッケージとユニバーサルパッケージは、相補的に共存する方向性を目指しているように見えます。

  • ソフトウェア開発者:ユニバーサルパッケージフォーマットを用いて、常に最新のディストリビューションに依存しないパッケージを提供する
  • ディストリビューター:従来のパッケージフォーマットを用いて柔軟に組み合わせ可能で、なおかつディストリビューションの中において一貫性の高いパッケージを提供する

ユーザーは用途に応じて2種類のパッケージを使い分けることになります。まず、最新の確実に動くパッケージがほしいならユニバーサルパッケージを使うと良いでしょう。それに対して複数の異なるパッケージのバージョンを指定したり、Dockerイメージの中で使いたいなら従来のパッケージのほうが柔軟な組み合わせが可能になります。

Snap以外のユニバーサルパッケージ

さて、そんな「ユニバーサルパッケージ」ですが、現在広く使われているパッケージフォーマットとして次のようなものが存在します。

Nix
おそらくユニバーサルパッケージ的な考え方の先駆けとも言うべき存在で、2000年代前半から存在していたようです。
AppImage
これも比較的早くから存在していた仕組みで、アプリケーションをISOイメージに閉じ込めることで、⁠パッケージ管理」が不要になっています。
Flatpak
FedoraおよびRHELが強力に推進しているフォーマットで、今のところGUIアプリケーションをメインターゲットにしているようです。
Snaps
Ubuntu/Canonicalが推進しているフォーマットです。他と比べると強力な権限管理機能が売りとなっています。
Docker
言わずとしれたコンテナプラットフォームの主役です。最近は任意のアプリケーションの実行環境としての地位も確立しているため、DockerHubて配布されているコンテナイメージはある種のユニバーサルパッケージと言えるでしょう。

Flatpakについては第513回の新しいパッケージの仕組み、Flatpakを使用するを参照してください。AppImageもスタンドアローンなアプリにおいて採用例はそれなりに存在します。Nixはすでにある程度のエコシステムを構築しているため、NixOSをはじめとしてそれらのエコシステムをメインにするのであれば選択肢に入ってくるでしょう。

今回紹介したSnapsは、Ubuntuで最初から使えるようになっていますし、第582回のいろいろなディストリビューションでsnapとLXDを利用するを参考にすれば、他のディストリビューションでも利用可能です。Flatpakと比べると、将来性にはまだ不安要素が残っているものの、Ubuntuで生活するならSnapを使うほうが楽なケースは確実に増えています[8]⁠。

たとえばLXD、Nextcloud、Microk8sなどのサーバー向けのサービスはsnapパッケージとしてインストールするほうが簡単ですし、Chromiumなどの更新頻度の高いデスクトップアプリケーションもsnapパッケージとして提供されています。

IoTからサーバー、デスクトップに至るまで統一的に利用できるのがSnapsの強みです。この強みが本当に活かせるかどうかは、まだ数年以上の経過観察が必要になるでしょう。

おすすめ記事

記事・ニュース一覧