aptやdpkgはどのようにパッケージを管理するか

# 背景
普段何気なく使っている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コマンドで得たシステムコールの順序を元に列挙すると次のとおりである。

1. `/var/lib/dpkg/status` を見る
1. `/var/lib/apt/lists/*` を見る
1. 対象のパッケージのメタデータを見て依存解決
1. `.deb` ファイルを取ってきてarchiveを展開する
1. `preinst` スクリプトがあれば実行する
1. `data.tar` 内のファイルを展開する
1. `/var/lib/dpkg/info/foo.list` に展開したファイルの一覧を記録する
1. `/var/lib/dpkg/info/` に `foo.(pre|post)(inst|rm)` や `foo.md5sum` などを置く
1. `postinst` スクリプトがあれば実行する
1. `/var/lib/dpkg/status` を更新する

# アンインストール時(remove/purge)
`apt remove` や `apt purge`コマンドはパッケージを削除する。
両者の違いは、前者が `conffiles` で指定されたパッケージ内の設定ファイルを残すのに対して、後者は残さないことである。
また `--auto-remove` オプションをつけることで、そのパッケージが依存するパッケージすべてを可能な限り(他のパッケージが依存していない限り、手動でインストールされていない限り)削除する。

1. `/var/lib/dpkg/status` を見る
1. 対象のパッケージのメタデータを見て依存先で削除可能なパッケージを列挙
1. `/var/lib/dpkg/info/foo.prerm` スクリプトがあれば実行する
1. `/var/lib/dpkg/info/foo.list` に列挙されたファイルを削除する
1. `/var/lib/dpkg/info/foo.postrm` スクリプトがあれば実行する
1. `/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