Yumの仕組みとGo言語での再実装

# はじめに
ここのところPM関連に関して以下のような取り組みを行っていた。

* [Go言語でRPMファイルを読む](http://qiita.com/necomeshi/items/84a40a47ae35939fee2f)
* [Go言語向けのAPIバインディングを作成](http://qiita.com/necomeshi/items/5ab23a1e5a80050be968)

上で紹介した記事（作成したプログラム）では、rpmコマンドの機能、つまり**クライアントに既にダウンロードされている単体のRPMファイル**について操作は可能となったが、以下については実現できていなかった。

* パッケージ間の依存関係の参照
* RPMファイルの集合であるパッケージグループの参照
* パッケージのダウンロード

しかし当初より、例えば、多くの開発業務ではクライアント端末となっているであろうWindows環境などのRPM/Yumでパッケージマネジメントをしていない環境にて、RPMファイルを操作する事を目的とするのであれば、上記３点はぜひとも必要な機能である。

そこで、本記事では上記機能を提供するYumの機能のGo言語での実装を紹介する。
なお本記事ではYumの機能の詳細については改めて記載することはしないので、Yumのマニュアルページか該当のRedhatのサイトをご確認いただきたい。

# Yum互換コマンド
今回の取り組みでできた成果物は、いつものようにgithubへアップロードしているので、ご興味がある方 or 利用したい方は参照いただきたい。

* github　https://github.com/necomeshi/goyum

## 実行例
今のところ、`search, provides, deplist, grouplist`に対応している。
また、Windows環境を考慮してパッケージをダウンロードする`download`サブコマンドを勝手に追加した。
その他のサブコマンドは**筆者の**必要に応じて適宜増やすつもりである。

#### パッケージ検索
```bash
$ ./goyum search vim-common
* main : ftp.tsukuba.wide.ad.jp/Linux/centos/6/os/x86_64/
============== N/S Matched: vim-common ================
vim-common.x86_64 : The common files needed by any version of the VIM editor
```

#### パッケージのダウンロード

```bash
$ ./goyum download vim-common
* main : ftp.tsukuba.wide.ad.jp/Linux/centos/6/os/x86_64/
=========================================================================
 Package              Arch           Version      Repository        Size
=========================================================================
Downloading:
  vim-common.x86_64 x86_64 7.4.629-5.el6_8.1 main 7047012
```

#### 


# Yumのしくみ
ここではYumの仕組みについて説明する。なお、はじめにで述べたとおり、Yumで管理されていない環境での利用を目的としているため、**インストール/アンインストール**については説明しない。

Yumのしくみは、ほとんどが**パッケージレポジトリ**と**レポジトリデータベース**についてが主となる。

## パッケージレポジトリ
Redhat or CentOSユーザである諸兄は、おそらくウェブブラウザなどでパッケージレポジトリにアクセスしたことがあるだろう。以下はCentOSのパッケージのソースコードを管理しているレポジトリである。
http://vault.centos.org/6.0/os/x86_64/

ご覧になればお分かりいただけるかと思うが、上記には以下のようなディレクトリ構成となっていることがわかる

* Package
RPMパッケージが置かれたディレクトリ

* repodata
なんだかよくわからないが.sqlite.bz2や、.xml等のファイルが置かれたディレクトリ

* その他もろもろ

ここで重要なのはrepodataに含まれるファイル群である。

### repomd.xml

repomd.xmlにはパッケージデータベースの（サーバー上の）Path、サイズ、チェックサムが記述されている。以下はRikenのCentOS6向けリポジトリのrepomd.xmlからの抜粋である。

```xml
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
 <revision>1490724196</revision>
<data type="primary_db">
  <checksum type="sha256">5d14ebd60604f4433dcc8a3a17cd3bbc7b80ec5dff74cbcc50dab6e711959265</checksum>
  <open-checksum type="sha256">a7b43ae178e3c7b216dd366f415c7fa1048298c2c572cb1a0a6e92cc478d060a</open-checksum>
  <location href="repodata/5d14ebd60604f4433dcc8a3a17cd3bbc7b80ec5dff74cbcc50dab6e711959265-primary.sqlite.bz2"/>
  <timestamp>1490724342.5</timestamp>
  <database_version>10</database_version>
  <size>4919110</size>
  <open-size>21343232</open-size>
</data>
以下続く
```

上記は`primary_db`の情報を記述したXMLである。
`primary_db`とはRPMパッケージの情報、RPMパッケージが提供する・依存するファイルの情報等を含み、殆どのyumクエリ(`yum search`, `yum provides`、`yum install`)等で使用するデータベースである。
実際のYumコマンド実行時の出力を確認すると、`primary_db`をダウンロードしている経過がよく分かる。

```
$ yum install net-snmp
Loaded plugins: fastestmirror
Setting up Install Process
Determining fastest mirrors
epel/metalink                                          | 7.3 kB     00:00     
 * base: ftp.tsukuba.wide.ad.jp
 * contrib: ftp.tsukuba.wide.ad.jp
 * extras: ftp.tsukuba.wide.ad.jp
 * updates: ftp.tsukuba.wide.ad.jp
base                                                   | 3.7 kB     00:00     
base/primary_db                                        | 4.7 MB     00:01     
contrib                                                | 2.9 kB     00:00     
contrib/primary_db                                     | 1.2 kB     00:
```

その他、others_db、filelist_db等、更新履歴やファイル一覧を保存したデータベースファイルの情報、および、パッケージグループの情報が記述されたXMLファイルへのパス、チェックサムも記述されている。

以降では、特に重要となるprimary_dbについて解説する。

#### repomd.xmlのUnmarshal
repomd.xmlは拡張子からわかるようにXMLファイルである。
Go言語においては`encoding/xml`パッケージでXMLパーサが実装されており、Java等では一般的なのかもしれないが、構造体に直接マップする方式のようである。

repomd.xmlの構造は次のようになっている。

* repomdのルート要素
    * レビジョン
    * リポジトリデータベース要素のリスト
        - データベースのへのパス、チェックサム、サイズ、タイムスタンプ

このような構造をUnmarshalするには次のような構造体を定義すると良い。

```go
type Location struct {
	Url string `xml:"href,attr"`
}
type Checksum struct {
	Type string `xml:"type,attr"`
	Data string `xml:",chardata"`
}
type RepomdDatabase struct {
	Name         string   `xml:"type,attr"`
	Location     Location `xml:"location"`
	Checksum     Checksum `xml:"checksum"`
	Timestamp    string   `xml:"timestamp"`
	Size         uint64   `xml:"size"`
	OpenSize     uint64   `xml:"open-size"`
	OpenChecksum Checksum `xml:"open-checksum"`
	Version      string   `xml:"version"`
}

type Repomd struct {
	Revision string           `xml:"revision"`
	Database []RepomdDatabase `xml:"data"`
}
```

構造体の要素のタグは、XML要素と一致するようにしなければならないようだ。さらに深く調査はしなかったが、構造体の要素がPublic(先頭が大文字）でなければUnmarshalできなかった。

また、次のようなXML、

```XML 
 <checksum type="sha256">5d14ebd60604f4433dcc8a3a17cd3bbc7b80ec5dff74cbcc50dab6e711959265</checksum>
```

をUnmarshalするには以下のような構造体を定義し、それぞれの要素がどこを指すか、(アトリビュートの場合`attr`、内部の文字列要素の場合`chardata`）をタグにて指定する必要がある。

```go
type Checksum struct {
	Type string `xml:"type,attr"`
	Data string `xml:",chardata"`
}
```

## レポジトリデータベース primary_db
上記したように、`primary_db`とはRPMパッケージの情報、RPMパッケージが提供する・依存するファイルの情報等を含む、sqliteのデータベースファイルである。

`primary_db`には次のようなテーブルが含まれている。
残念ながら、すべてののテーブルについては詳細な資料を見つけることができなかった。もし詳細についてご存じの方はご教授頂きたい。

現在筆者が調査した結果を以下に示す。

* confilcts
* db_info
* files
あるパッケージに含まれるファイルについてのテーブル

* obsolutes
* packages
パッケージ名、バージョン等のパッケージの基本情報。

* provides
あるパッケージが提供するファイルについてのテーブル

* requires
あるパッケージが依存するファイルについてのテーブル

`packages`テーブルは他のテーブルにも含まれるキーpkgkeyがあり、それをもとにパッケージ基本情報と、それ以外の情報を関連付けることができる。

例えば、あるパッケージが提供するファイル一覧を取得する場合には、次のようなクエリを行うことで取得することができる。

```SQL
SELECT provides.name FROM packages INNER JOIN provides ON packages.pkgkey=provides.pkgkey WHERE packages.name='vim-common'
```

### `yum search`, `yum provides` のSQLクエリ
各yumコマンドに対応するSQLは次のようになる。
なお、筆者はSQLやデータベースは専門外である。もしより効率的なクエリが記述できる場合にはご指摘をいただきたい。

* yum searchのSQL

```SQL
SELECT 
pkgkey, pkgid, name, 
arch, version, release,
summary, location_href,size_package,
FROM packages
WHERE name LIKE '%パッケージ名%'
```

* yum providesのSQL

```SQL
SELECT 
packages.pkgkey, 
packages.pkgid, 
packages.name, 
packages.arch,
packages.version, 
packages.release,
packages.summary, 
packages.location_href,
packages.size_package, 
files.name
FROM packages INNER JOIN files ON packages.pkgkey=files.pkgkey 
WHERE files.name='ファイル名'
```

#### Go言語でのデータベース操作
Go言語では`database/sql`パッケージを使用することで、実際のデータベースドライバがどのようなものであっても統一された操作が可能となる。

今回はsqliteを使用するため、`database/sql`に対応した[go-sqlite3パッケージ](https://github.com/mattn/go-sqlite3)を利用させていただいた。

`database/sql`パッケージでは、次のようにしてデータベースの操作が可能となる
以下はパッケージ名にて`primary_db`に検索を行う場合のコード例である。

```go
// ドライバを指定してデータベースに接続
db, err = sql.Open("sqlite3", destination)

query := `SELECT pkgkey, pkgid, name, 
　　　　　　　　　　arch, version, release,
　　　　　　　　　　summary, location_href,size_package,
　　　　　　　　　　FROM packages
　　　　　　　　　　WHERE name='?'`

// queryの"?"の部分を"vim-common"でリプレースしてクエリ実行
rows, err := primarydb.Query(query, "vim-common")
if err != nil {
　　return err
}

// false が返るまでマッチした行を取得
for rows.Next() {
　　var pkg Package
　　
　　// SELECTで指定した列の要素を取得
　　rows.Scan(&pkg.Key, &pkg.Id, &pkg.Name,
　　　　　　　 &pkg.Arch, &pkg.Version, &pkg.Release,
　　　　　　　 &pkg.Summary, &pkg.Location, &pkg.Size
}
```
### パッケージグループ
パッケージグループは、repomd.xmlのgroup_gzで示されたXMLファイルに記述されている。以下の例では`repodata/1cde788f77b08a7eb3dfdba12fa384a5f0214147a717a1e2d4504368037fba90-c6-x86_64-comps.xml.gz`がそれに当たる。

```xml
<data type="group_gz">
　<checksum type="sha256">
1cde788f77b08a7eb3dfdba12fa384a5f0214147a717a1e2d4504368037fba90
　</checksum>
　<open-checksum type="sha256">
43d8fd068164b0f042845474d6a22262798b9f0d1f49ad1bf9f95b953089777d
　</open-checksum>
　<location href="repodata/1cde788f77b08a7eb3dfdba12fa384a5f0214147a717a1e2d4504368037fba90-c6-x86_64-comps.xml.gz"/>
　<timestamp>1490724342.59</timestamp>
　<size>231286</size>
</data>
```

group_gzのXMLファイルには以下に示すように、パッケージグループ名、パッケージグループの説明、グループに所属するパッケージのリストが記述されており、先の節にて説明したXMLファイルのUnmarshalと同様の手法で解析することができる。

```xml
<comps>
  <group>
    <id>additional-devel</id>
    <name>Additional Development</name>
    <name xml:lang="as">অতিৰিক্ত উন্নয়ন</name>
    <name xml:lang="bn">অতিরিক্ত ডিভেলপমেন্ট</name>
    <name xml:lang="bn_IN">অতিরিক্ত ডিভেলপমেন্ট</name>

　　・・・各言語別のグループ名が続く・・・・

    <description>Additional development headers and libraries for developing applications</description>
    <description xml:lang="as">এপ্লিকেচনসমূহৰ উন্নয়নৰ কাৰণে অতিৰিক্ত উন্নয়ন হেডাৰ আৰু লাইব্ৰেৰীসমূহ</description>

　　・・・各言語別の説明が続く・・・・・

    <default>false</default>
    <uservisible>true</uservisible>
    <packagelist>
      <packagereq type="default">alsa-lib-devel</packagereq>
      <packagereq type="default">audit-libs-devel</packagereq>
　　・・・　省略　・・・・
```

ここに記載されたパッケージ名は、primary_dbのパッケージ名と一致するので、グループ名を指定してパッケージをインストールすることが可能となっている。