specファイル大解剖

# はじめに

みなさんはspecファイル書いたことありますか？
specファイルを雰囲気フワフワな状態で書いていたエンジニア(私)が、
これを機にちゃんとまとめようと思った備忘録的なそれです。

# specファイルってなんぞ

rpmパッケージを作る時に必要になるレシピ的なもの

`rpmbuild`コマンドを実行するときspecファイルに沿ってrpmパッケージを作成して行きます。
今回`rpmbuild`コマンドを実行した時どのような流れでspecファイルが利用されていくのかに着目していきたいと思います。

## おことわり

`rpmbuild`コマンドは実行時`$HOME/rpmbuild/`ディレクトリをトップディレクトリとして実行されます。
任意のディレクトリで`rpmbuild`コマンドを実行した場合は`--define`オプションを用いて`_topdir`の定義を一時的に変更する必要があります。

つまり`$HOME/myrpm`配下で`rpmbuild`コマンドを実行しようとした場合、`rpmbuild --define="_topdir $HOME/myrpm"`となります。

この記事ではspecファイルに注目するため、このような`rpmbuild`コマンドの細かい部分はそれとなーく流します。

# ディレクトリ構成

`rpmbuild`コマンドを実行した時に作成されるディレクトリ構成はこちら

``` console
_topdir
├── BUILD
├── BUILDROOT
├── RPMS
├── SOURCES
├── SPECS
└── SRPMS
```

それぞれのディレクトリの用途は以下の通りです。

|ディレクトリ名|用途|作成されるタイミング|
|:--|:--|:--|
|BUILD|rpmパッケージを作成するときに使用する作業ディレクトリ|`rpmbuild`コマンド実行時に作成される|
|BUILDROOT|アプリケーションを仮想インストールする際にルートになるディレクトリ|`rpmbuild`コマンド実行時に作成される|
|RPMS|完成したrpmパッケージが置かれるディレクトリ|`rpmbuild`コマンド実行時に作成される|
|SRPMS|完成したsrpmファイルが置かれるディレクトリ|`rpmbuild`コマンド実行時に作成される|
|SOURCES|rpmパッケージに含めるソースコードを置くディレクトリ|自分で作成する|
|SPECS|rpmパッケージ作成に用いるspecファイルを置くディレクトリ|自分で作成する|

実際にrpmファイルを作成していくときは、`SPECS`ディレクトリにspecファイルを置き、`SOURCES`ディレクトリにソースコードを置き、
`rpmbuild`コマンドを実行し、完成したrpmパッケージを`RPMS`ディレクトリから取る形になると思います。

# specファイルの構成

specファイルは以下のようないくつかの章が合わさって構成されます。

* 基本情報
* スクリプト部
  * prepセクション
  * buildセクション
  * installセクション
  * checkセクション
  * cleanセクション
* ファイルリスト部
* 更新履歴

`rpmbuild`コマンドを実行したとき主に利用されるのがスクリプト部だと思います。
実際に私がrpmパッケージを作成したときもスクリプト部をいじっていた時間がほとんどでした。
各章・各セクションをちゃんとまとめたいと思ったのがこの記事を書くに至った経緯でもあります。

## マクロの定義

specファイル内では自分でマクロを定義することができます。
頻繁に使用する記述をマクロで定義しておくと、いろいろと便利になります。

```
%define version 1.0
```

例えばこのようなマクロを定義した場合、specファイル内で`%{version}`と書くと`1.0`に置換されます。
また、標準で定義されているマクロもあります。先程述べた`_topdir`も標準で定義されているマクロのひとつです。

標準で定義されているマクロは`rpmbuild --showrc`コマンドで確認できます。めちゃめちゃあるので全部把握するのは難しいかも..


## 基本情報

この章にはrpmパッケージの基本情報を記述します。
これらは`rpm -qi`などでパッケージ情報を呼び出す際に表示されるものになります。

基本情報として利用できるタグの一覧がこちら。

|タグ|説明|必須|
|:--|:--|:--|
|Summary|パッケージの簡単な説明|○|
|Name|パッケージ名。ここで定義した名前は以降`%{name}`というマクロで利用出来ます。|○|
|Version|パッケージのバージョン。ここで定義した値は以降`%{version}`というマクロで利用出来ます。|○|
|Release|パッケージのリリース番号。ここで定義した値は以降`%{release}`というマクロで利用できます。|○|
|License|アプリケーションのライセンス|○|
|Group|アプリケーションの種類。CentOS7.5では`/usr/share/doc/rpm-4.11.3/GROUPS`内に列挙されています。||
|Packager|パッケージメンテナの名前。複数いる場合はカンマ区切り||
|Url|アプリケーションの情報を提供しているURL||

また、パッケージ作成に必要なソースやパッチもここで列挙します。

|タグ|説明|必須|
|:--|:--|:--|
|Source0|パッケージの作成に必要なソースファイル名を指定します。指定された名前のソースファイルを`SOURCES`ディレクトリからさがします。ここで指定したファイルは以降`%{SOURCE0}`というマクロで利用出来ます。|○|
|Source1...|必要なソースファイルが2つ以上ある場合はSource1,Source2..と連番で列挙していきます。ここで指定したファイルは以降`%{SOURCE1}`や`%{SOURCE2}`のようなマクロで利用できます。||
|Patch0...|パッチファイルを指定します。2つ以上ある場合はPatch1,Patch2..と連番で列挙していきます。||
|BuildRoot|パッケージ作成時に仮想インストールされるディレクトリを指定します。ここで定義した値は以降`%{buildroot}`というマクロで利用できます。||

他のパッケージとの依存情報も列挙します。


|タグ|説明|必須|
|:--|:--|:--|
|Requires|作成したrpmパッケージが動作するのに必要なパッケージ名を列挙します。`yum`コマンド等でインストールする時、ここで列挙されているパッケージも同時にインストールされます。||
|BuildRequires|パッケージの作成に必要なパッケージ名を列挙します。`rpmbuild`コマンドを実行するときに列挙されているパッケージが足りてないとエラーになります。||
|Conflicts|共存できないパッケージ名を列挙します。||
|BuildConflicts|パッケージ作成時にインストールしておけないパッケージ名を列挙します。||
|Obsoletes|パッケージをインストールする際に、アンインストールするパッケージ名を列挙します。||

最後に`description`タグでパッケージの詳しい解説を記述します。

```
%description
This Application is 
very very happy Application
```


長い！そして多い！
こんな量の情報を一つのファイルに書くのでそれはそれは巨大なファイルになりますよ。
そしてこれはまだまだ序の口。次は実際にパッケージのインストールなどを行うスクリプト部です。

## スクリプト部

実際にrpmパッケージを作成していくスクリプトを書きます。
スクリプト部は以下のフォーマットで記述します。

``` bash
%セクション名
シェルスクリプト

```

次のセクション名が現れるまでのシェルスクリプトを順番に実行します。
`rpmbuild`コマンド実行時に`-v`オプションをつけるとわかりますが、呼び出されるシェルは`/bin/sh -e`として呼び出されます。
私が試しているCentOS7.5の環境では`bash`にリンクされているので、specファイルのシェルもbashの構文に則っている必要があります。

まぁspecファイルの中であんまりごちゃごちゃするものでも無いと思うので気にするほどじゃないかもしれませんが...


セクション一覧がこちら

|セクション|説明|
|:--|:--|
|prep|ソースをビルドする前処理|
|build|ソースのビルド処理|
|install|ソースをインストールする処理|
|check|ビルド結果を検証する処理|
|clean|パッケージ完成後の後処理|
|pre|RPMパッケージをインストールする時、パッケージ展開前に行う処理|
|post|RPMパッケージをインストールする時、パッケージ展開後に行う処理|
|preun|RRMパッケージをアンインストールする時、パッケージ削除前に行う処理|
|postun|RPMパッケージをアンインストールする時、パッケージ削除後に行う処理|
|triggerin|あるパッケージがインストールされていた、もしくはされたときに行う処理|
|triggerun|あるパッケージの削除前に行う処理|
|triggerpostun|あるパッケージの削除後に行う処理|
|verifyscript|RPMパッケージを検証するときに追加で行う処理|


この中から必要なセクションをspecファイルに記述していきます。すべて記述しなきゃいけないわけではありません。
なんなら全部なくても大丈夫です。何も実行されずrpmも作成されませんが...

最低限`prep`,`build`,`install`はないとrpmパッケージは作成されません。`check`,`clean`もあるとなおいいでしょう。
その他のセクションは必要に応じて利用してください。

`rpmbuild`コマンドは`prep→build→install→check`の順に実行しrpmパッケージを作成します。
rpmパッケージの作成が完了したら`clean`セクションを実行します。

### prepセクション

ソースのビルドをする前処理を行います。
以前`rpmbuild`コマンドを実行したゴミ等が残っている可能性があるため、
下記のように最初に`BuildRoot`のクリーンを行うことが一般的です。

``` bash
%prep
rm -rf %{buildroot}
```

`prep`セクションでは`%setup`マクロを使ってソースコードの展開を行ったり、
`%patch`マクロを使ってソースコードにパッチをあてることが可能です。
それぞれのマクロの挙動は下記の通りです。

#### %setupマクロ

主にソースコードの展開をおこないます。
`Source0`に`hogehoge.tar.gz`のようなファイルが指定されていることが想定されています。

実際の動きとしては下記の通り

1. `%{name}-%{version}`で指定されているディレクトリを削除します。
1. `Source0`に指定されているファイルを`gzip`で解凍し、`tar`で展開します。
1. `%{name}-%{version}`で指定されているディレクトリに`cd`で移動する。
1. `chmod`でパーミッションを変更する

chmodしてるのは知らなかった...

`rpmbuild`コマンドは実行するときに一時ファイルにシェルスクリプトを生成し、それを`/bin/sh`に読ませる形で実行しているので、
実際に生成された一時ファイルを見ることで何をやっているかがわかります。`%setup`マクロが実際に行っている処理は以下のようになっていました。百聞は一見にしかず


``` bash
cd '/home/shun/rpmbuild/BUILD'
rm -rf 'sample-script-1.0'
/usr/bin/gzip -dc '/home/shun/rpmbuild/SOURCES/sample-script-1.0.tar.gz' | /usr/bin/tar -xvvf - 
STATUS=$?
if [ $STATUS -ne 0 ]; then
  exit $STATUS
fi
cd 'sample-script-1.0'
/usr/bin/chmod -Rf a+rX,u+w,g-w,o-w .
```

GroupやOtherに書き込み権限があった場合はなくすようにしていたり、chmodで不要な権限は外しているんですねー

これは`%{name}`が`sample-script`、`%{version}`が`1.0`だったので`sample-script-1.0`というディレクトリを対象にしています。

対象にするディレクトリを変更したり`%setup`マクロにはさまざまなオプションがあるのですがそれはまた別の機会に...

#### %patchマクロ

主に`%setup`マクロで展開したソースにパッチをあてます。

特にオプションなく`%patch`マクロを実行した場合`patch -p0`が実行されます。しかし、`git diff`などでパッチファイルを作ると`-p1`オプションをつけるのが一般的です。
その場合は`%patch`マクロに`-p1`というふうにオプションを付けてあげれば実現可能です。

またパッチファイルが2つ以上設定されている場合は`%patch0`や、`%patch1`とすることで複数のパッチをあてることができます。
例えば以下のようになると思います。

``` bash
%patch0 -p1
%patch1 -p1
```

この場合`patch`マクロが実際に行っている処理は以下のようになっていました。

``` bash
echo "Patch #0 (sample-patch01.patch):"
/usr/bin/cat /home/shun/rpmbuild/SOURCES/sample-patch01.patch | /usr/bin/patch  -p1  --fuzz=0
echo "Patch #1 (sample-patch02.patch):"
/usr/bin/cat /home/shun/rpmbuild/SOURCES/sample-patch02.patch | /usr/bin/patch  -p1  --fuzz=0
```

### buildセクション

主にパッケージをビルドする処理を書きます。
`%setup`マクロで展開したディレクトリに`cd`してからスクリプトは実行されます。
`Makefile`にビルドするルールを記述し`make`のみで終わらせるのが一般的？
個人的にはそのほうがspecファイルがスッキリするのでお気に入りです。

また`autotools`を用いて開発されたソースなどで、`configure`スクリプトが用意されている場合`%configure`マクロが利用できます。
私自身が`autotools`を使ったことが無いので今回は割愛... `autotools`勉強して出直してきます。

### installセクション

パッケージを仮想インストールする処理を書きます。
`BUILD`ディレクトリでビルドしたアプリケーションを`BUILDROOT`ディレクトリをルートディレクトリとし、仮想インストールします。
`%build`セクション同様`%setup`マクロで展開したディレクトリに`cd`してからスクリプトは実行されます。
仮想インストールなので`%{buildroot}`以下にインストールする必要があるので少し工夫が必要かもしれません。

私の場合は`Makefile`内で`DESTDIR`というような値のない変数を定義し、specファイル内で利用する時に`make install DESTDIR=%{buildroot}`としたりします。
以下`Makefile`の例になります。

``` Makefile
DESTDIR  = #値は空のまま

.PHONY: install
install:
  install -d -m 0755 ${DESTDIR}/usr/bin
  install -m 0755 sample-script ${DESTDIR}/usr/bin/
```

specファイルの中身は以下のようになります。

``` bash
%install
make install DESTDIR=%{buildroot}
```

こうすることで`rpmbuild`コマンド実行時には`%{buildroot}`配下にディレクトリやスクリプトがインストールされます。
さらに、`rpm`コマンドなどで実際にインストールする際には`%{buildroot}`の値が定義されないので、通常のルートディレクトリ配下にインストールされるようになります。

これはあくまで一例にすぎないので、正しくインストールができさえすれば何でも大丈夫です。

### checkセクション

パッケージのビルドが正常に完了したかチェックする処理を書きます。
アプリケーションによってテストスクリプトなどがある場合はここで実行しましょう。

### cleanセクション

rpmパッケージが完成した後の後処理を行う処理を書きます。
下記のように`BuildRoot`の削除などを行いましょう。

``` bash
%clean
rm -rf %{buildroot}

```

## ファイルリスト部

rpmパッケージに含まれるファイル名を列挙します。
`%install`までスクリプトを実行した段階で列挙されたファイルが実際にインストールされている必要があります。
列挙されているファイルがインストールされていなかったり、逆に列挙されていないファイルがインストールされていたりした場合`rpmbuild`コマンドは失敗します。

### %attr

`%attr`を用いるとファイルのパーミッションやuserID, groupIDまで設定が可能です。
例えば以下の用に設定したとします。

``` bash
%files
%attr(0755,root,root) /usr/bin/sample-script
```

これは`/usr/bin/sample-script`というファイルが`0755`のパーミッションでuserIDが`root`、groupIDが`root`でインストールされることをあらわします。

### %defattr

`%defattr`を用いると以降すべてのファイルのパーミッション、userID、grouIDが指定出来ます。
例えば以下の用に設定したとします。

``` bash
%files
%defattr(0755,root,root)
/usr/bin/sample-script1
%attr(0644,root,root) /usr/bin/sample-script2
/usr/bin/sample-script3
```

この場合`sample-script1`と`sample-script3`は`%defattr`で設定したパーミッション`0755`、userIDが`root`、groupIDが`root`になります。
ただし、`sample-script2`は`%attr`で上書きしているのでパーミッションが`0644`となります。

### %config

設定ファイル(書き換えられる可能性のあるファイル)であることを示します。 
`%config`には`noreplace`と`missingok`があり、何も指定しなかった場合はデフォルトで`noreplace`になります。

#### noreplace

`%config(noreplace)`としたファイルに変更があった場合、アンインストール時や再インストール時に新しいファイルに置き換えられなくなります。
具体的に以下のような記述をしたとします。

``` bash
%files
%config(noreplace) /etc/sample/sample.conf
```

インストールされた設定ファイルに変更を加えます

``` console
$ echo "推しメンすちすちビーム" >> /etc/sample/sample.conf
```

パッケージをアンインストールすると、設定ファイルはリネームして残されます。

``` console
$ sudo rpm -e sample-script
警告: /etc/sample/sample.conf は /etc/sample/sample.conf.rpmsave として保存されました。
$
$ ls -1 /etc/sample
sample.conf.rpmsave
```

パッケージを再インストールすると再度設定ファイルが作られます。

``` console
$ sudo rpm -ih sample-script-1.0-1.el7.x86_64.rpm
################################# [100%]
更新中 / インストール中...
################################# [100%]

$ ls -1 /etc/sample/
sample.conf
sample.conf.rpmsave
```

またパッケージ側で設定ファイルに変更があった場合は、新しい設定ファイルはインストールされずに、
ファイル名に`.rpmnew`を付けた形でインストールされます。

``` console
$ echo "推しメンすちすちビーム" >> /etc/sample/sample.conf
$
$ sudo rpm -Uh sample-script-1.0-2.el7.x86_64.rpm 
################################# [100%]
更新中 / インストール中...
警告: /etc/sample/sample.conf は /etc/sample/sample.conf.rpmnew として作成されました。
################################# [ 50%]
整理中 / 削除中...
################################# [100%]
$
$ ls -1 /etc/sample/
sample.conf
sample.conf.rpmnew
```

#### missingok

`%config(missingok)`としたファイルは、ファイルが存在しなくてもエラーになりません。
これは`rpm -V`コマンドなどでチェックしたときにエラーにならないようにするためのものです。

## 更新履歴

rpmパッケージの更新履歴を英語で記述します。
記述するフォーマットはパッケージによって微妙に異なりますが、基本的には以下のフォーマットで大丈夫です。

``` bash
* 曜日 月 日 西暦 パッケージャーの名前 <メールアドレス> バージョン-リリース番号
- 更新内容
```

これを最新の更新情報が上に来るように書き加えていきます。
実際は下記のようになるとおもいます。

``` bash
%changelog
* Thu dec 6 2018 shun kawai <shun@example.com> 1.0-2
- second release

* Wed dec 5 2018 shun kawai <shun@example.com> 1.0-1
- first release
```

# 最後に

自作のスクリプトなどでもrpmパッケージになっていれば`rpm`コマンドや`yum`コマンドでインストール・アンインストールができるから便利！

debパッケージも作れるようになるとなお良いのかもしれません。勉強します。

# 参考にしたサイト

https://vinelinux.org/docs/vine6/making-rpm/vine-making-rpm.html