git-buildpackageを用いたdebパッケージ管理方法の紹介

こんにちは。サイボウズ・ラボ アルバイトの宮川(@miyagaw61)です。

今回は、Gitでソースコードを管理するソフトウェアにおいて、git-buildpackage(gbp)コマンドを使って複数のLinuxディストリビューションやそのバージョン毎に効率良くdebパッケージ化を行い、メンテナンスする方法について紹介したいと思います。

環境

Ubuntu Server 16.04 LTSで作業していますが、 Ubuntuの14.04や18.04でも問題なく動くと思います。

事前知識と準備

deb-version

deb-versionとはdebianパッケージで用いられているバージョン番号形式のことで、{アップストリームバージョン}もしくは{アップストリームバージョン}-{リビジョン}という形式で表されます。 アップストリームバージョンは英数字(A-Za-z0-9)と記号(.+:~)が使用可能で、先頭は数字である必要があります。 一般的には「X.Y.Z」という形式で表されることが多く、XはMajor番号、YはMiner番号、ZはPatch番号と呼ばれます。 それぞれは次の意味を持っています。

  • Major番号:後方互換性がない変更の時にインクリメントする番号
  • Minor番号:後方互換性がある変更の時にインクリメントする番号
  • Patch番号:バグ修正の時にインクリメントする番号

リビジョンは英数字(A-Za-z0-9)と記号(+.~)が使用可能で、付けるか否かは自由です。 一般的には先頭に数字が置かれます。そのあとに~+が置かれ、任意の文字列が置かれることもあります。 アップストリームバージョンもリビジョンも、いずれも以下のようなルールを持っています。 0.0 < 0.5 < 0.10 < 0.99 < 1 < 1.0~rc1 < 1.0 < 1.0+b1 < 1.0+nmu1 < 1.1 < 2.0

詳しくはこちらを参照ください。

上流と下流の用意

以下の二つのリポジトリを用意しておいてください。

  • 上流リポジトリ: debianディレクトリの無い大元のソースコード管理リポジトリです。このリポジトリは自分ではコントロールできないものとします。 例ではgit@github.com:miyagaw61/test.gitを使います。後で中身は作りますので、最初は空で構いません。
  • 下流リポジトリ:debianディレクトリの差異を管理するリポジトリです。 例ではgit@github.com:miyagaw61/pkg-test.gitを使います。上流リポジトリ同様、最初は空で構いません。

リポジトリ名やコミットメッセージなどは、適宜変更してください。

作業の流れ

  1. 下流リポジトリを作成してdebパッケージをビルドできるようになるまで
  2. debパッケージ側で管理するパッチの更新
  3. 上流の変更への追従

新規作成・初期設定

では、さっそく始めていきましょう。 上流リポジトリが無ければ何も始まりません。 ここでは、誰かが上流リポジトリで新規バージョンをリリースしたものとします。その内容は以下のようなものであるとします。

$ git clone git@github.com:miyagaw61/test.git
$ cd test
$ echo "echo hello" > hello.sh
$ git add hello.sh
$ git commit -m "Add hello.sh"
$ git tag upstream/1.0.0
$ git push origin master
$ git push origin upstream/1.0.0
$ cd ..

次に、予め作成しておいた空の下流リポジトリをクローンします。

$ git clone git@github.com:miyagaw61/pkg-test.git
$ cd pkg-test

以降、断わりがない限り下流リポジトリでの作業となります。 上流リポジトリをfetchします。

$ git remote add upstream git@github.com:miyagaw61/test.git
$ git fetch upstream
$ git checkout -b upstream upstream/master

任意のDebian系ディストリビューションの任意のバージョンにおいて、debianディレクトリ以下やソースコードなどをそのバージョンに合わせて管理するための、専用のブランチを作成します。

$ git checkout -b xenial

今回はxenial(Ubuntu16.04)環境で解説していくため、名前はxenialとしました。

次に、debianディレクトリを作成します。 どんな方法で作成しても大丈夫ですが、今回は簡単のためdh_makeを使用します。パッケージクラスはsingleに設定します。

$ dh_make --createorig -p test_1.0.0 -s -y

ここでのtest_1.0.0は、{パッケージ名}_{アップストリームバージョン}の形式にしてください。 このとき、{アップストリームバージョン}は先ほどのupstreamに付けたタグと同じにしてください。

debian/{パッケージ名}.installファイルを作成します。 このファイルでインストールするファイルとインストール先のパスを指定します。

$ echo "hello.sh usr/bin" > debian/test.install

スペース区切りで、左側にインストールするファイルをパッケージのディレクトリからの相対パスで、右側にインストール先のパスをルートディレクトリからの相対パスで記述します。 こちらに書いてあるように、DebianのPolicyにより/usr/local以下にパッケージをインストールすることは禁止されています1

変更をadd・commitします。

$ git add debian
$ git commit -m "Add debian directory"

ビルドが成功するか確認します。

$ gbp buildpackage -us -uc --git-ignore-new

-us -ucオプションを使用すると署名を行いません。 --git-ignore-newオプションを使用するとstagingされていないファイルが存在していてもビルドできるようになります。

ビルドが成功したら、インストールして実行してみます。

$ sudo dpkg -i ../test_1.0.0-1_amd64.deb
$ hello.sh

"hello"と出力されていれば正常です。

ビルドすると、debianディレクトリ以下に新たにファイルがいくつか生成されます。 無事ビルド・インストール・実行の全てが正常に行えることを確認したら、まずはビルド時に生成されたファイルを全て削除してビルド前の状態に戻しましょう。

$ git status
$ rm -rf debian/{debhelper-build-stamp,files,test.debhelper.log,test.substvars,test}

ビルド直前のデータはコミットされているので、rm -rfを使う代わりに以下のコマンドでも消せます。

$ git clean -df

PbuilderやCowbuilderなどを使用してビルドしたい方は、ビルド時に--git-pbuilderオプションを追加で使用してください2。 事前に以下の方法でベースイメージ(例:/var/cache/pbuilder/cow.base)を作成しておく必要があります。 ~/.pbuilderrcDIST環境変数の内容は適宜書き換えてください。

$ vim ~/.pbuilderrc
$ cat ~/.pbuilderrc
COMPONENTS="main restricted universe multiverse"
$ DIST=xenial BUILDER=cowbuilder git-pbuilder create

そして、現在のバージョンのタグを作成します。 現在のバージョンとは、現在のchangelog(debian/changelogファイル)に記述されている最新のバージョンのことです。 次のコマンドで、現在のバージョンのタグを自動で作成できます。(--git-tag-onlyにより実際のビルドはしません)

$ gbp buildpackage --git-tag-only --git-ignore-branch

現在のchangelogの最新バージョンは"1.0.0-1"なので、"debian/1.0.0-1"というタグが自動で作成されました。 上流と下流のタグはそれぞれ、次に示す命名規則に従っている必要があります。

  • 上流に付けるタグはupstream/{アップストリームバージョン}という表記でなければならない(--git-upstream-tagで変更可能)
  • 下流に付けるタグはdebian/{アップストリームバージョン}もしくはdebian/{アップストリームバージョン}-{リビジョン}という表記でなければならない(--git-debian-tagで変更可能)
  • 下流のタグに含まれるバージョン文字列はchangelogに記述されているバージョン文字列と完全に一致していなければならない

--git-upstream-tag--git-debian-tagなどのオプションを毎回指定するのが面倒な場合は、debian/gbp.confに設定を記述することで省略することができます3

無事タグの生成を済ませたら、タグをpushしましょう。

$ git push origin debian/1.0.0-1

pristine-tarについて

orig.tar.gzファイルは上流メンテナが提供するソースコードと同じ内容を含むアーカイブです。orig.tar.gzファイルは誰でも生成できるのですが、上流メンテナと全く同じコードをビルドしたとしても、環境によってはファイルとしては異なるものになってしまうことがあります。しかし、上流メンテナと全く同じorig.tar.gzを生成しなければいけない場合があります。そういう時は、pristine-tarを使用してください。 pristine-tarは、上流メンテナと全く同じorig.tar.gzファイルを再生成するためのpristine-tarデータの作成や、pristine-tarデータを用いて上流メンテナと全く同じorig.tar.gzの生成などを行うことができるコマンドです。 gbp buildpacakgeコマンドには、--git-pristine-tar--git-pristine-tar-commitという二つのオプションがあります。これらは、pristine-tarを内部で使用しており、上流メンテナと全く同じorig.tar.gzを用いてdebパッケージ化する際に役立ちます。 組み合わせごとの詳細な内部処理を解説します。以下の4パターンがあります。

  1. --git-pristine-tar--git-pristine-tar-commitも指定しなかった場合: changelogからアップストリームバージョンを抽出します。それを対応する上流のタグに変換し(変換方法は--git-upstream-tagオプションで指定可能)、そのタグが付いたコミットを探し、そのソースコードからorig.tar.gzを生成します(既にorig.tar.gzを持っていたら生成せずにそれを使います)。そして、そのorig.tar.gzを用いてdebパッケージ化します。

  2. --git-pristine-tar-commitのみを指定した場合: aの手法でビルドすると同時に、ビルドするバージョンのpristine-tarデータがpristine-tarブランチに存在していない場合、ビルドに使われたorig.tar.gzからpristine-tarデータを生成し、pristine-tarブランチにコミットします。

  3. --git-pristine-tarのみを指定した場合: changelogからアップストリームバージョンを抽出します。そのバージョンのpristine-tarデータがpristine-tarブランチに存在していないとエラーになります。存在していたらそこからorig.tar.gzを再生成(その再生成したorig.tar.gzと既に持っているorig.tar.gzのハッシュ値が異なる場合はエラー)し、そのorig.tar.gzを用いてdebパッケージ化します4

  4. --git-pristine-tar--git-pristine-tar-commitを両方を指定した場合: changelogからアップストリームバージョンを抽出します。そのバージョンのpristine-tarデータがpristine-tarブランチに存在していれば--git-pristine-tarオプションのみ有効に、存在していなければ--git-pristine-tar-commitオプションのみ有効になります。

通常は、--git-pristine-tar--git-pristine-tar-commitの二つのオプションを併用してビルドする運用で問題無いと思われます5

パッチの更新

パッケージ管理者がdebianディレクトリ以外に存在するソースコードを修正したい場合は、オリジナルソースコードに対するパッチファイルを作成し、quiltコマンドで管理します。 そのパッチファイルはdebian/patchesに格納されます。 gbpを用いると、quiltを直接使わずにgitと連携してパッチを管理することができます。 debian/patches以下のパッチファイルは本記事ではquiltパッチと呼んで区別することにします。

先程作成した、特定バージョンにおいてのパッケージ管理用ブランチに移動します(ここではxenialブランチを指します)。

$ git checkout xenial

xenialブランチにいる状態でgbp pq importというコマンドを実行するとpatch-queue/xenialというブランチが自動で作成され、xenialブランチのdebian/patches以下にあるパッチファイルが全てコミットとして再現されます。 これらのpatch-queue/BRANCHをここではpatch-queueブランチと呼ぶことにします。 既に対応するpatch-queueブランチが存在する場合は作成ではなくgbp pq switchgbp pq rebase(詳細は後述)で移動する必要があります。

$ gbp pq import # もしくはgbp pq switchかgbp pq rebase

quiltパッチがひとつも存在しない状況では、図にすると次のようになります。

初期状態のブランチ

quiltパッチを作成,編集,削除したい時はpatch-queueブランチで開発します。 通常の開発と同様にファイルを編集し、commitします。

$ sed "s/hello/hello, world/g" -i hello.sh
$ git add hello.sh
$ git commit -m "Add world to hello.sh"

patch-queueブランチで開発

開発がひと段落したら、xenialブランチとpatch-queue/xenialブランチとの差分コミットをパッチファイルという形にしてxenialブランチへ送ります。 --commitオプションを付けることで、自動でxenialブランチへの変更コミットも行ってくれます。

$ gbp pq export --commit

patch-queueブランチをxenialブランチに統合

patche-queue/xenialブランチ上ではオリジナルソースコードへの変更がそのままgitコミットとして存在しますが、xenialブランチ上ではdebian/patches/以下のパッチファイルを追加するgitコミットとして存在します。 xenialブランチに戻ってきたら、changelogを更新します。 次のコマンドで、バージョン1.0.0-2+xenialとしてchangelogを更新できます。

$ gbp dch -N 1.0.0-2+xenial --commit --ignore-branch

変更前の changelog:

test (1.0.0-1) unstable; urgency=medium

 * Initial release (Closes: #nnnn)  <nnnn is the bug number of your ITP>

 -- miyagaw61 <miyagaw61@unknown>  Fri, 08 Jun 2018 15:30:58 +0900

変更後の changelog:

test (1.0.0-2+xenial) UNRELEASED; urgency=medium

  * Add world to hello.sh

 -- miyagaw61 <miyagaw61@gmail.com>  Fri, 08 Jun 2018 15:54:26 +0900

test (1.0.0-1) unstable; urgency=medium

  * Initial release (Closes: #nnnn)  <nnnn is the bug number of your ITP>

 -- miyagaw61 <miyagaw61@unknown>  Fri, 08 Jun 2018 15:30:58 +0900

--commitオプションは自動でcommitも行ってくれるオプション、--ignore-branchオプションはmasterブランチ以外でも更新できるようにするオプションです。 ちなみに、-Nオプションを付けなければ現在のバージョンにパッチを追加、-Rオプションを付けるとchangelog中のUNRELEASEDが自分のマシンのOSバージョン(ここではxenial)に書き換わり、リリースに向けた最終調整のためにエディタが起動します。

同じバージョンのタグも生成します。

$ gbp buildpackage --git-tag-only --git-ignore-branch

"debian/1.0.0-2+xenial" というタグが git リポジトリに追加されました。 masterブランチ以外でタグを生成する時は--git-ignore-branchオプションが必要です。

ビルドが成功するか確認します。

$ gbp buildpackage -us -uc --git-ignore-branch

現在のchangelogの最新バージョン名は"1.0.0-2+xenial"なので、対応するアップストリームバージョンは"1.0.0"になります。 そのため、今回は"upstream/1.0.0"のタグが指しているコミットに対応するソースコードを使ってビルドします。 ビルドが成功したら、インストールして実際に実行してみましょう。

$ sudo dpkg -i ../test_1.0.0-2+xenial_amd64.deb
$ hello.sh

"hello, world"と出力されていれば正常です。 正しく変更が適応されていることがわかります。

また、先程と同様に、ビルドによって新たにファイルが作成されているため、全てを削除し、ビルド前の状態に戻します。

$ git clean -df

ビルド・インストール・実行が成功することを確かめたら、changelogを最終調整し、リリースしましょう。

$ gbp dch -R --commit --ignore-branch

変更前の changelog:

test (1.0.0-2+xenial) UNRELEASED; urgency=medium

  * Add world to hello.sh

 -- miyagaw61 <miyagaw61@miyagaw61-ubuntu.in.labs.cybozu.co.jp>  Fri, 14 Sep 2018 13:24:52 +0900

test (1.0.0-1) unstable; urgency=medium

   * Initial release (Closes: #nnnn)  <nnnn is the bug number of your ITP>

  -- miyagaw61 <miyagaw61@unknown>  Fri, 14 Sep 2018 11:49:46 +0900

変更後の changelog:

test (1.0.0-2+xenial) xenial; urgency=medium

  * Add world to hello.sh

 -- miyagaw61 <miyagaw61@miyagaw61-ubuntu.in.labs.cybozu.co.jp>  Fri, 14 Sep 2018 13:24:52 +0900

test (1.0.0-1) unstable; urgency=medium

   * Initial release (Closes: #nnnn)  <nnnn is the bug number of your ITP>

  -- miyagaw61 <miyagaw61@unknown>  Fri, 14 Sep 2018 11:49:46 +0900

タグを付けなおします。

$ gbp buildpackage --git-tag-only --git-ignore-branch --git-retag

ブランチとタグをpushします。

$ git push origin xenial
$ git push origin debian/1.0.0-2+xenial

上流の変更への追従

ここでは上流のソースコードがバージョンアップなどによって変更された場合に、 下流リポジトリのブランチで追従する方法について説明します。 上流の更新に追従する場合、まずxenialブランチを上流の適切なコミット(通常はバージョンタグ) に対してrebaseさせ、次に、patch-queue/xenialブランチをxenialブランチに対してrebaseさせるという 二段階の工程を踏みます。

準備

準備として、まずは更新された上流を用意します。

$ cd ../test
$ git checkout master
$ sed "s/hello/hi/g" -i hello.sh
$ git add hello.sh
$ git commit -m "Use hi instead of hello"
$ git push origin master
$ git tag upstream/2.0.0
$ git push origin upstream/2.0.0
$ cd ../pkg-test
$ git checkout upstream
$ git pull --rebase upstream master:upstream

タグの作成を忘れないでください。 上流に適切なタグがついていない場合は、下流でタグを付けて管理してください。

今の状況を図にすると次のようになります。

上流リポジトリのタグが付いた状態

追従

追従したい下流ブランチへ移動します。

$ git checkout xenial

上流ブランチに対してrebaseします。

$ git rebase upstream

上流ブランチに対してリベースした状態

通常であればコンフリクトは起きないはずです。xenialブランチではdebianディレクトリ以下しか編集しておらず、 上流にはdebianディレクトリが存在しないからです。 無事rebaseできたら、次のコマンドでpatch-queue/xenialブランチへの移動とxenialブランチでのrebaseを行います。 これは、quiltパッチがgitコミットとして表現されているpatch-queue/xenialブランチをxenialブランチにrebaseすることで、quiltパッチを上流の新しいバージョンに対してrebaseすることが目的です。

$ gbp pq rebase

patch-queueブランチへの移動とxenialブランチでのリベース

ここでコンフリクトが発生したら、解消します。

$ cat hello.sh
$ cat hello.sh | grep "hello, world" | sed "s/hello/hi/g" > tmp; mv tmp hello.sh # もしくはvim hello.sh
$ cat hello.sh
$ git add hello.sh
$ git rebase --continue

次に、xenialブランチとpatch-queue/xenialブランチとの差分コミットをパッチファイルという形にしてxenialブランチへ送ります。

$ gbp pq export --commit

patch-queueブランチのxenialブランチへの統合2

changelogを更新します。一気にリリースしてしまいます。

$ gbp dch -R -N 2.0.0-1+xenial --commit --ignore-branch

変更前の changelog:

test (1.0.0-2+xenial) xenial; urgency=medium

  * Add world to hello.sh

 -- miyagaw61 <miyagaw61@miyagaw61-ubuntu.in.labs.cybozu.co.jp>  Fri, 14 Sep 2018 13:24:52 +0900

test (1.0.0-1) unstable; urgency=medium

   * Initial release (Closes: #nnnn)  <nnnn is the bug number of your ITP>

  -- miyagaw61 <miyagaw61@unknown>  Fri, 14 Sep 2018 11:49:46 +0900

変更後の changelog:

test (2.0.0-1+xenial) xenial; urgency=medium

   * Use hi instead of hello
   * Add debian directory
   * Add world to hello.sh
   * Update changelog for 1.0.0-2+xenial release
   * Update changelog for 1.0.0-2+xenial release
   * Rediff patches

  -- miyagaw61 <miyagaw61@miyagaw61-ubuntu.in.labs.cybozu.co.jp>  Fri, 14 Sep 2018 13:43:56 +0900

test (1.0.0-2+xenial) xenial; urgency=medium

   * Add world to hello.sh

  -- miyagaw61 <miyagaw61@miyagaw61-ubuntu.in.labs.cybozu.co.jp>  Fri, 14 Sep 2018 13:24:52 +0900

test (1.0.0-1) unstable; urgency=medium

   * Initial release (Closes: #nnnn)  <nnnn is the bug number of your ITP>

  -- miyagaw61 <miyagaw61@unknown>  Fri, 14 Sep 2018 11:49:46 +0900

タグを作成します。

$ gbp buildpackage --git-tag-only --git-ignore-branch

ビルド・インストール・実行ができることを確認します。

$ gbp buildpackage -us -uc --git-ignore-branch
$ sudo dpkg -i ../test_2.0.0-1+xenial_amd64.deb
$ hello.sh

"hi, world"と出力されれば正常です。 正しく変更が適応されていることがわかります。

ビルド前の状態に戻します。

$ git clean -df

git rebase upstreamによってFast-forwardで適用できなくなってしまったため、このままgit push origin xenialしてしまうとrejectされてしまいます。 なので、force pushします。

$ git push -f origin xenial

タグをpushします。

$ git push origin debian/2.0.0-1+xenial

複数人で下流リポジトリを管理している場合の注意

複数人で管理している下流リポジトリでforce pushをしてしまうと、自分以外の人がリモートに反映した情報を消してしまう可能性があります。 そのような場合には、複数人が同じ名前のブランチで作業したり、同じ名前のタグを付けることが無いように運用する必要があります。GitHub等を使っている場合はforkしてpull requestするのが王道でしょうか。 patch-queue ブランチは必ずしも共有しなくても良いと思います。

debianディレクトリ以下の変更

最後に、debianディレクトリ以下のファイルの変更方法を解説します。 debianディレクトリ以下のファイルは、patch-queueブランチで手を入れるとビルド時に怒られてしまいます。 patch-queueブランチはあくまでquiltパッチを管理するためのブランチですので、 debianディレクトリ以外のファイルを変更するときのみ使います。 debianディレクトリ以下のファイルは、xenialブランチで直接編集し、コミットしましょう。

$ git checkout xenial
$ sed "s@<insert the upstream URL, if relevant>@https://miyagaw61.github.io@g" -i debian/control

ビルドし、変更が適応されているか確認してみましょう。

$ gbp buildpackage -us -uc --git-ignore-branch --git-ignore-new
$ dpkg -I ../test_2.0.0-1+xenial_amd64.deb

Homepage: https://miyagaw61.github.ioとなっていれば変更が正しく適応されています。

ビルド前の状態に戻します。

$ git clean -df

add・commit・pushします。

$ git add debian/control
$ git commit -m "Add Homepage-Url"
$ git push origin xenial

最後に

本記事では複数のLinuxディストリビューションやそのバージョン毎に効率良くdebパッケージ開発を行う方法を紹介しました。 我々はこのノウハウを得て、これまでオリジナルのリポジトリ上で直接debianディレクトリを管理していた walb-toolswalb-tools-pkgリポジトリと分離させました。 本記事が同様のdebパッケージ管理をしたい方々のお役に立てば幸いです。

参考


  1. そのため、何の対策もせずに/usr/local以下にパッケージを直接インストールしようとすると、dh_usrlocalというツールの実行に失敗してしまいます。どうしても/usr/local以下にパッケージを直接インストールしたい場合は、こちらに書いてあるように、debian/rulesにoverride_dh_usrlocal:の一行を追記し、dh_usrlocalの実行を無効化する必要があります。

  2. PbuilderやCowbuilderなどを使用するとビルド時にdebianディレクトリ以下に新たなファイルが生成されずに済みますし、依存パッケージの記述漏れに悩むことがなくなります。

  3. gbp.confを配置可能なパスはdebianディレクトリ以外にもいくつかあります。

  4. このオプションが有効な時、--git-upstream-tagオプションは効果が無くなります。--git-upstream-tagオプションは飽くまでchangelogから抽出したアップストリームバージョンからそれに対応する上流に付けられているタグに変換するための変換方法を指定するオプションであり、--git-pristine-tarオプションが有効な時はその変換処理が必要無いからです。

  5. 今回は簡略化のため、以降のコマンド例でもpristine-tarは使用していません。