Ubuntu Weekly Recipe

第676回aptコマンドの最新機能あれこれ

第675回ではapt-keyコマンドが廃止される理由を説明しました。それ以外にもaptコマンドには常に様々な変更が加えられています。今回はそれらをいくつかピックアップして紹介しましょう。

Apt 1.0.xから2.3.xまでの流れ

本連載でaptコマンドそのものを紹介したのは、7年以上前の第327回aptコマンドを使ってみようまで遡ります。当時はApt 1.0がリリースされて間もないころで、数週間後に登場したUbuntu 14.04 LTSにもApt 1.0が取り込まれています。Apt 1.0ではこれまで別々のコマンドだった各種ツールがサブコマンドとして一元化して使えるaptコマンドが実装された記念すべきリリースでもありました。

その後7年を経て、2021年7月時点でのバージョンは2.3.6にまで到達しています。まもなくリリースされる予定のDebian 11ではApt 2.2.xが採用される見込みです。またUbuntuも20.04はApt 2.0.x、21.04でApt 2.2.xとなり、Apt 2.3.x以降は21.10で採用されるでしょう。

今回は2020年3月にリリースされた2.0を含む、最新の2.3.6までのバージョンアップで追加された機能・変更点などを紹介します。

apt-patterns機能の追加

Apt 2.0の目玉とも言うべき機能がapt-patternsでしょう。これはaptitudeに存在した、パッケージに対する強力なパターンマッチング機能を移植したものです。従来のaptコマンドもパッケージ名の検索時等に簡易的な正規表現は可能でした。apt-patternsでは、さらにパッケージのステータスなども組み合わせたより詳細なマッチングが可能になっています。

パターンは?用語~省略文字のいずれかで開始します。用語のほうは複数の文字からなる文字列で、さらに(文字列)などの記述が続きます。よってbash等で記述する場合はシングルクオートでくくるのが一般的です。省略文字のほうはよりシンプルに記述できますが、すべてをサポートしているわけではありません。さらに「文字列」には特定のパターンや正規表現が入ります。これは外側のパターンに依存します。

manページにあるものも含めて、いくつか実例を見ていきましょう。

sudo apt remove ?garbage

?garbageないし~gは、インストールされたものの依存していたパッケージ等が削除されたため、安全にアンインストール可能なパッケージを選択します。removeサブコマンドと一緒に単体で利用する場合は、sudo apt autoremoveとほぼ同じ動作です。

sudo apt purge ?config-files

?config-filesないし~cはパッケージそのものは削除されたものの設定ファイルが残っているパッケージdpkg -lrcと表示されるパッケージ)を選択します。purgeコマンドと一緒に使うことで、これらのパッケージを完全削除します。

apt list '~i !~M (~slibs|~sperl|~spython)'

?installedないし~iはインストール済みのパッケージを、?automaticないし~Mは依存関係などによって自動的にインストールされたパッケージを、?section(正規表現)ないし~s正規表現はSectionフィールドが指定した正規表現に一致するパッケージを選択します。さらに複数のパターンの連結はAND検索になり、?or(パターン)ないし|はOR検索になります。さらに?not(パターン)ないし!パターンは状態を反転させます。これらの組み合わせによって、上記は「手動でインストール」されている、Sectionが「libs」「perl」「python」かのパッケージをリストアップします。

apt list '~i ~Vubuntu'

?version(正規表現)ないし~V正規表現は正規表現にマッチするバージョンを選択します。上記ではインストール済みのUbuntuパッチがあたっている(バージョンにXubuntuYの文字列が入る)パッケージをリストアップします。

apt list '?and(~D~nlibyaml-0-2,~n^lib)'

?name(正規表現)ないし~n正規表現でパッケージ名が正規表現にマッチするパッケージを選択します。さらに?depends(パターン)ないし~Dパターンによって依存関係にパターンに一致するパッケージが存在するものを選択します。上記は若干ややこしいですが、まずは~D~nlibyaml-0-2によって「libyaml-0-2」がDependsフィールドに含まれるパッケージをリストアップしています。これはapt reverse libyaml-0-2と同じです。さらに~n^lib?and()で繋げることで、そのパッケージリストの中から「libで始まるパッケージ名」だけを抽出しています。

このようにapt-patternsを駆使するとかなり複雑なパッケージのマッチングが可能になるのです。

apt satisfyコマンドの追加

Apt 1.9.0からaptおよびapt-getにsatisfyサブコマンドが追加されました。これはパッケージのDependsフィールドにある文字列を解釈し、それに応じてインストールするコマンドです。たとえばリポジトリには存在しないdebian/controlファイルのフィールドを元にパッケージをインストールしたり、deb-srcを有効化していない状態でbuild-dep相当の機能を実現したい場合に便利なコマンドとなります。

もう少し具体的に説明しましょう。aptにはもともとbuild-depというサブコマンドが存在しました。Debianパッケージは、そのパッケージを実行する際に必要なパッケージリストを表示した「Dependsフィールド」とは別に、パッケージをビルドする際に必要な「Build-Dependsフィールド」が存在します。もし何か特定のパッケージを手元でビルドしたい場合は、まずBuild-Dependsフィールドのパッケージをインストールしておく必要があります[1]⁠。

パッケージをビルドする場合、大抵はソースパッケージリポジトリ/etc/apt/sources.listdeb-srcで始まる行)を有効化しています。よって開発者はsudo apt build-dep FOOとするだけで、FOOパッケージをビルドするために必要なパッケージ一式をインストールできました。

しかしながら環境によっては、わざわざソースパッケージリポジトリを有効化したくない場合や、ソースパッケージには書かれていない依存関係に基づいてパッケージをインストールしたいことがあります。そこで使えるのがsatisfyサブコマンドです。このコマンドはDependsやBuild-Dependsに書かれている文字列をそのまま渡すことで、その内容を適切に解釈し、apt install相当のコマンドに変換してくれます。

たとえばBIOSを書き換えるflashromの依存関係は次のようになっています。

$ apt list flashrom
(中略)
Depends: libc6 (>= 2.28), libftdi1-2 (>= 1.2), libpci3 (>= 1:3.5.1), libusb-1.0-0 (>= 2:1.0.22)
(後略)

さらにソースパッケージリポジトリを有効化しているのであれば、構築時の依存関係は次のとおりであると確認できます。

$ apt showsrc flashrom
(中略)
Build-Depends: debhelper (>= 11), pkg-config, libpci-dev, libusb-1.0-0-dev [!hurd-i386], libftdi1-dev [!hurd-i386], meson
(後略)

これらのパッケージはsatisfyコマンドによって次のようにインストール可能です。

Dependsフィールドの場合:
$ sudo apt satisfy 'libc6 (>= 2.28), libftdi1-2 (>= 1.2), libpci3 (>= 1:3.5.1), libusb-1.0-0 (>= 2:1.0.22)'

Build-Dependsフィールドの場合:
$ sudo apt satisfy 'debhelper (>= 11), pkg-config, libpci-dev, libusb-1.0-0-dev [!hurd-i386], libftdi1-dev [!hurd-i386], meson'

debian/controlをパースして必要なパッケージをインストールするようなスクリプトを書いている場合、とても役に立つでしょう。

Apt CLIの操作結果をJSON RPCで受け取る

Apt 1.6からApt CLIの操作結果をJSON RPCで叩く、Hook機構が追加されました

これはaptコマンドを使って何らかの操作を行ったときに、その状態遷移や結果を任意のスクリプトで受け取ることが可能な仕組みです。その使い方はドキュメントとaptパッケージのテストコードが参考になるでしょう。

まずはHookを受け取るスクリプトを作成します。

$ cat >> json-hook.sh << EOF
#!/bin/bash
trap '' SIGPIPE
while true; do
    read request <&\$APT_HOOK_SOCKET || exit 1

    if echo "\$request" | grep -q ".hello"; then
        echo "HOOK: HELLO"
    fi

    if echo "\$request" | grep -q ".bye"; then
        echo "HOOK: BYE"
        exit 0;
    fi

    echo HOOK: request \$request

    read empty <&\$APT_HOOK_SOCKET || exit 1

    echo HOOK: empty \$empty

    if echo "\$request" | grep -q ".hello"; then
        printf '{"jsonrpc": "2.0", "result": {"version": "'0.2'"}, "id": 0}\n\n' >&\$APT_HOOK_SOCKET 2>/dev/null || exit 1
    fi
done
EOF

$ chmod +x json-hook.sh
$ sudo cp json-hook.sh /usr/local/bin/

スクリプトでは$APT_HOOK_SOCKETによるUnixドメインソケットを経由して、aptコマンドからの結果をJSONフォーマットで受け取っています。また最後にHookスクリプトから応答を返しています。応答を返すプロトコルバージョンは最新の0.2にしています。Ubuntu 21.04より前のバージョンなら「0.1」に変更して試してください。

このスクリプトを、各状態に応じて叩くようにしましょう。

$ cat <<EOF | sudo tee /etc/apt/apt.conf.d/99-json-hooks
AptCli::Hooks::Install:: "/usr/local/bin/json-hook.sh";
AptCli::Hooks::Upgrade:: "/usr/local/bin/json-hook.sh";
AptCli::Hooks::Search:: "/usr/local/bin/json-hook.sh";
EOF

これで準備は完了です。試しに適当なパッケージを検索してみましょう。

$ apt search docker.io
HOOK: HELLO
HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1","0.2"]}}
HOOK: empty
HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.search.pre","params":{"command":"search","search-terms":["docker.io"],"unknown-packages":[],"packages":[]}}
HOOK: empty
HOOK: BYE
ソート中... 完了
全文検索... 完了
docker-doc/hirsute,hirsute 20.10.2-0ubuntu2 all
  Linux container runtime -- documentation

docker.io/hirsute,now 20.10.2-0ubuntu2 amd64 [インストール済み]
  Linux container runtime

python3-docker/hirsute,hirsute 4.1.0-1.2 all
  Python 3 wrapper to access docker.io's control socket

ruby-docker-api/hirsute,hirsute 1.22.2-1.1 all
  Ruby gem to interact with docker.io remote API

HOOK: HELLO
HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1","0.2"]}}
HOOK: empty
HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.search.post","params":{"command":"search","search-terms":["docker.io"],"unknown-packages":[],"packages":[]}}
HOOK: empty
HOOK: BYE

org.debian.apt.hooks.helloから.search.preが叩かれ、検索処理が走り、再度.helloで今度は.search.postが叩かれていることがわかります。検索の場合はsearch-termsに検索文字列が入るだけです。

試しに存在しないパッケージ名だと次のような感じになります。

$ apt search docker.iot
(前略)
HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.search.fail","params":{"command":"search","search-terms":["docker.iot"],"unknown-packages":[],"packages":[]}}
HOOK: empty
HOOK: BYE

違うのは最後が.search.postではなく.search.failになっていることです。

より実用的なのはinstallやupgradeでしょう。試しに次のようにパッケージのアップグレードが可能な状態で実行してみました。

$ apt list --upgradable
一覧表示... 完了
networkd-dispatcher/hirsute-updates,hirsute-updates 2.1-2~ubuntu21.04.1 all [2.1-1 からアップグレード可]
ubuntu-drivers-common/hirsute-updates 1:0.9.0~0.21.04.1 amd64 [1:0.8.9.1 からアップグレード可]

$ sudo apt upgrade
(前略)
HOOK: HELLO
HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.hello","id":0,"params":{"versions":["0.1","0.2"]}}
HOOK: empty
HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.install.pre-prompt",
  "params":{"command":"upgrade","search-terms":[],"unknown-packages":[],
  "packages":[{"id":767,"name":"networkd-dispatcher","architecture":"amd64","mode":"install","automatic":true,
    "versions":{"candidate":{"id":69990,"version":"2.1-2~ubuntu21.04.1","architecture":"all","pin":500,
      "origins":[{"archive":"hirsute-updates","codename":"hirsute","version":"21.04",
        "origin":"Ubuntu","label":"Ubuntu","site":"jp.archive.ubuntu.com"},
        {"archive":"hirsute-updates","codename":"hirsute","version":"21.04",
        "origin":"Ubuntu","label":"Ubuntu","site":"jp.archive.ubuntu.com"}]},
      "install":{"id":69990,"version":"2.1-2~ubuntu21.04.1","architecture":"all","pin":500,
      "origins":[{"archive":"hirsute-updates","codename":"hirsute","version":"21.04",
        "origin":"Ubuntu","label":"Ubuntu","site":"jp.archive.ubuntu.com"},
        {"archive":"hirsute-updates","codename":"hirsute","version":"21.04",
        "origin":"Ubuntu","label":"Ubuntu","site":"jp.archive.ubuntu.com"}]},
      "current":{"id":4737,"version":"2.1-1","architecture":"all","pin":500,
      "origins":[{"archive":"hirsute","codename":"hirsute","version":"21.04",
        "origin":"Ubuntu","label":"Ubuntu","site":"jp.archive.ubuntu.com"},
        {"archive":"hirsute","codename":"hirsute","version":"21.04",
        "origin":"Ubuntu","label":"Ubuntu","site":"jp.archive.ubuntu.com"}]}}},
      (中略)
以下のパッケージはアップグレードされます:
  networkd-dispatcher ubuntu-drivers-common
  (中略)
HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.install.package-list",
  "params":{"command":"upgrade","search-terms":[],"unknown-packages":[],
  (中略)
アップグレード: 2 個、新規インストール: 0 個、削除: 0 個、保留: 0 個。
  (中略)
HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.install.statistics",
  "params":{"command":"upgrade","search-terms":[],"unknown-packages":[],
  (中略)
67.3 kB のアーカイブを取得する必要があります。
この操作後に追加で 0 B のディスク容量が消費されます。
続行しますか? [Y/n]
取得:1 http://jp.archive.ubuntu.com/ubuntu hirsute-updates/main amd64 ubuntu-drivers-common amd64 1:0.9.0~0.21.04.1 [52.7 kB]
取得:2 http://jp.archive.ubuntu.com/ubuntu hirsute-updates/main amd64 networkd-dispatcher all 2.1-2~ubuntu21.04.1 [14.6 kB]
  (中略)
HOOK: request {"jsonrpc":"2.0","method":"org.debian.apt.hooks.install.post",
  "params":{"command":"upgrade","search-terms":[],"unknown-packages":[],
  (後略)

今回はかなり複雑な情報が記載されています。だいぶ省略しましたが、結局のところ次のような流れで呼ばれますす。

  • org.debian.apt.hooks.install.pre-prompt
  • org.debian.apt.hooks.install.package-list
  • org.debian.apt.hooks.install.statistics
  • org.debian.apt.hooks.install.post

packagesには変更対象のパッケージとその詳細な情報が含まれています。Hookスクリプト側で適宜パースして、別のところに渡すようにすれば、インストール・アップグレードされたパッケージを収集可能です。

ただし現時点でインターフェースは安定していません。Apt 1.6当時のプロトコルバージョンが0.1で、Apt 2.3.2で0.2になりました。プロトコルバージョンが1.0になるまでは、後方互換性の維持することは約束できないと名言しているため、当面は実験的な機能と思っておきましょう。

上記のHookが不要になったら、以下のファイルを削除しておきましょう。

$ sudo rm /etc/apt/apt.conf.d/99-json-hooks

その他の細かい機能変更

他にもいくつか機能変更が行われているため、利用者への影響がありそうなものをいくつか紹介しましょう。

Apt 1.6でHTTPSのリポジトリも正式にサポートするようになりました。これまでHTTPSなリポジトリを使うためには、apt-transport-httpsパッケージが必要でしたが、今後はaptパッケージがインストールされていれば十分となります。Ubuntu 18.04 LTS以降であれば、apt-transport-httpsはaptパッケージに依存するだけの移行パッケージになっているため、明示的にインストールする必要はないでしょう。

Apt 1.9.11でApt Pinningにsrc:ソースパッケージ名を指定できるようになりました。これによりそのソースパッケージから生成されるバイナリパッケージのバージョンをまとめて固定化できるので便利です。

Apt 2.1.6からPhased Updateに対応しました。これはもともとデスクトップ版のパッケージマネージャーに実装されていた、アップデートを段階的に展開することで問題発生時にアップデートの展開を止める仕組みです。個々のマシンはそのMACHINE-IDから生成されたUUIDに従って、どれくらいの順番で適用されるかが決定されます。アップデートがリリースされると、最初は10%以内に設定されているマシンに、次は20%以内にという風に増やしていき、最終的におよそ2日で誰もがアップデートをダウンロードできるようになっているのです。直近のアップデートがどれくらい適用されているかは、こちらのサイトで確認できます。

最後にちょっとした小技を。aptコマンドの結果をパイプに渡したときなどに、次のような警告が表示されます。

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

これは(apt-getなどではない)aptコマンドがエンドユーザー向けのインタラクティブツールとして作られているからで、将来的なアップデートによってその利便性に応じて出力結果を変える可能性があるためです。もしスクリプト等でaptコマンドを使い、かつ、その結果をどこかに渡すような処理をしているのなら、aptコマンドではなく従来のapt-getやapt-cacheを使うほうが無難です。

ただし「apt list」のように代替のコマンドがない場合もあります。特にapt list --upgradableや今回紹介したapt-patternsのうち、apt listを使うものがその最たるものでしょう。上記の事情はわかっているものの、どうしても警告を表示させたくないなら、次のようにオプションで一時的に回避することは可能です。

$ apt list -oApt::Cmd::Disable-Script-Warning=1 --upgradable | cat
一覧表示...

Aptには他にもさまざまなオプションが存在します。man apt.confman apt_preferencesapt-config dumpなどで有用なオプションを探してみると良いでしょう。

おすすめ記事

記事・ニュース一覧