背景
普段何気なく使っているaptコマンドだが、何気なく使えるほどよく出来ているとも言える。
これがどうやってパッケージを管理しているのかということが気になったので調べてみた。
結論とかは特に無いが、何かトラブルが合った時に仕組みを知っていれば解決が早くなるものだ。
.debファイルの構造
aptやdpkgはUbuntuなどのDebian系でよく使われるパッケージマネージャ。
ネットワークを介して依存関係を解決したりするためにはaptが使われる。
パッケージに含まれるファイルを1つの .deb
ファイルにまとめて管理することができるほか、そのメタデータやインストール・アンインストール用のスクリプトも同梱させることができる。
.deb
ファイルは以下のような構造になっている。
+ foo.deb
- debian-binary # バージョン情報(2.0)
+ control.tar # メタデータ
- control # 各種メタデータ、Dependするパッケージ情報などを含む
- md5sums # これ以外のファイルのMD5ハッシュ値(破損検知用)
- preinst # install前に実行するスクリプト
- postinst # install後に実行するスクリプト
- prerm # remove/purge前に実行するスクリプト
- postrm # remove/purge後に実行するスクリプト
- conffiles # パッケージのコンフィグファイル一覧
- ...
+ data.tar # インストールするファイル(ルートディレクトリ以下に展開される)
- ./
- ./usr/
- ./usr/bin/
- ./usr/bin/foo
- ./opt/
- ./opt/foo/
- ./opt/foo/config
- ...
もし手元に .deb
ファイルがあるのなら、次のコマンドで開いてみるのも良い。
もちろん実行結果は手元の .deb
ファイル次第で大きく変わる。
$ ls -la foo.deb
foo.deb
$ mkdir ./tmp
$ cd ./tmp
$ ar xv ../foo.deb
debian-binary
control.tar.xz
data.tar.xz
_gpgbuilder
$ tar xfv control.tar.xz # ファイルを展開
./
./control
./postinst
./postrm
./md5sums
$ head ./control
Package: foo
Version: 1.0.0
Vendor: Foo
Depends: bar
$ tar tf data.tar.xz | head # ファイル一覧の一部を見るだけ
./
./usr/
./usr/bin/
./usr/bin/foo
./opt/
./opt/foo/
./opt/foo/config
アップデート時(update)
aptは依存関係を解決するために、必要なパッケージを配布するサーバ(repository、レポジトリ)がどこかを把握する。
そのために実行するコマンドが、よくある次のコマンド。
$ sudo apt update
これは /etc/apt/sources.list
や /etc/apt/sources.list.d/
以下のファイルに書かれたサーバのURLとオプションを元にサーバにアクセスし、 /var/lib/apt/lists/
以下にそのレポジトリが配布するパッケージ情報の一覧をキャッシュする。
中身を見てみると、 .deb
ファイルの control
ファイルと似たようなものが羅列されている。
$ apt download coreutils
$ ls coreutils*
coreutils_8.28-1ubuntu1_amd64.deb
$ mkdir coreutils/
$ cd coreutils/
$ ar x ../coreutils_8.28-1ubuntu1_amd64.deb
$ tar xf ./control.tar.xz
$ head ./control
Package: coreutils
Version: 8.28-1ubuntu1
Architecture: amd64
Essential: yes
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Installed-Size: 6560
Pre-Depends: libacl1 (>= 2.2.51-8), libattr1 (>= 1:2.4.46-8), libc6 (>= 2.25), libselinux1 (>= 2.1.13)
Section: utils
Priority: required
Multi-Arch: foreign
つまり、apt update
をした時に得る情報はパッケージがどこにあるかだけでなく、依存関係を解決するために必要な情報も含まれている。
インストール時(install)
ここまでの話で想像はつくが、あらためてaptコマンドがどのように動いているのか、straceコマンドで得たシステムコールの順序を元に列挙すると次のとおりである。
-
/var/lib/dpkg/status
を見る -
/var/lib/apt/lists/*
を見る - 対象のパッケージのメタデータを見て依存解決
-
.deb
ファイルを取ってきてarchiveを展開する -
preinst
スクリプトがあれば実行する -
data.tar
内のファイルを展開する -
/var/lib/dpkg/info/foo.list
に展開したファイルの一覧を記録する -
/var/lib/dpkg/info/
にfoo.(pre|post)(inst|rm)
やfoo.md5sum
などを置く -
postinst
スクリプトがあれば実行する -
/var/lib/dpkg/status
を更新する
アンインストール時(remove/purge)
apt remove
や apt purge
コマンドはパッケージを削除する。
両者の違いは、前者が conffiles
で指定されたパッケージ内の設定ファイルを残すのに対して、後者は残さないことである。
また --auto-remove
オプションをつけることで、そのパッケージが依存するパッケージすべてを可能な限り(他のパッケージが依存していない限り、手動でインストールされていない限り)削除する。
-
/var/lib/dpkg/status
を見る - 対象のパッケージのメタデータを見て依存先で削除可能なパッケージを列挙
-
/var/lib/dpkg/info/foo.prerm
スクリプトがあれば実行する -
/var/lib/dpkg/info/foo.list
に列挙されたファイルを削除する -
/var/lib/dpkg/info/foo.postrm
スクリプトがあれば実行する -
/var/lib/dpkg/status
を更新する
どのパッケージのファイルかの判定時
Linuxのファイルシステムに展開されたファイルたちは、それ自体はパッケージで管理されていないファイルと違いがない。
しかし以下のコマンドを使って、あるファイルがどのパッケージに属するものであるかを判定することができる。
$ dpkg -S /bin/ls
coreutils: /bin/ls
これは次のように動いている。
まずはインストールされたパッケージの一覧を /var/lib/dpkg/status
から取得し、各パッケージのインストール時に展開したファイルたちのリストを保存した /var/lib/dpkg/info/<package-name>.list
から検索している。
上の例では /var/lib/dpkg/info/coreutils.list
の中に以下のような行を発見してcoreutilsのファイルであると判定をする。
1 /.
2 /bin
3 /bin/cat
4 /bin/chgrp
5 /bin/chmod
6 /bin/chown
7 /bin/cp
8 /bin/date
9 /bin/dd
10 /bin/df
11 /bin/dir
12 /bin/echo
13 /bin/false
14 /bin/ln
15 /bin/ls <----
そのため、coreutils以外のパッケージの .list
ファイルにうっかり「/bin/ls」を足してしまうと、
$ dpkg -S /bin/ls
coreutils, foo: /bin/ls
のように別のパッケージにも属する判定になってしまう。
決してそんなことをしてはいけない。
検証環境
- Architecture: amd64
- Distribution: Ubuntu 18.04.6 LTS
- apt: 1.6.14
- dpkg: 1.19.0.5