Clean build RPM with mock as like git-buildpackage

DebianパッケージのビルドシステムはTrustyでJenkinsとpbuilder & cowbuilder & git-buildpackage を使って実装している、という話を 7月のDebian勉強会 でお話しました。

一方、RPMについては、2年ほど前にFlaskとCouchDBで実装したパッケージ管理システムを作成しました。Webブラウザ経由でRPMbブラウザでアップロードすると、ローカルリポジトリに登録されるようなシロモノです。個人的にはRPM使わないのでまぁ良いのですけど、ブラウザ経由でアップロードとか面倒ですね。 1 また、Upstreamが配布しているRPMはまだしも、カスタムパッケージの場合、Debianパッケージでのpbuilderなどのように、mockでクリーンビルドを行っていないと、「オレの環境ではビルドできるんだけど?」という、「これはヒドイ」パッケージが配布されます。 2

そこで、前述のDebianパッケージのビルドシステム同様に、Debian上でmockを使ってGitリポジトリからクリーンビルドすることができないかを検証してみました。

環境

検証した環境は次のとおりです。Trustyのmockパッケージも1.1.33で、後述のJenkinsでの実行は、Trusty上での実行をサンプルリポジトリを使って確認済みです。

  • Debian GNU/Linux Sid

  • mock 1.1.33

  • chrootターゲット epel-6-x86_64 (CentOS6用) 3

    • chrootのターゲットは、/etc/mock以下の .cfgファイルから選びます。mock(1)には、デフォルトでは、/etc/mock/default.cfgというファイルが選択される、とあるのですがDebianパッケージにはこのファイルはありません。必要ならユーザが用意する必要があります。

また、site-wideの設定として/etc/mock/site-default.cfgが使用されます。こっちは後述のGit関連のオプションを指定する際に内容を確認しました。

環境準備

  1. mockパッケージのインストール:

    $ sudo apt-get install mock
    
  2. mockグループの作成:

    $ sudo addgroup mock
    
  3. 実行ユーザをmockグループに追加:

    $ sudo adduser mkouhei mock
    
  4. chroot用ディレクトリの作成:

    $ sudo install -g mock -m 2775 -d /var/lib/mock
    
  5. chrootツリー作成:

    $ mock -r epel-6-x86 --init
    

これを実行しなくても、初回のビルド実行すると、chrootツリーがなければ作成されます。 /var/lib/mock/epel-6-x86_64/rootに作成されます。

Source RPMをリビルド

まずは普通にSource RPMをリビルドしてみました。:

$ mock -r epel-6-x86_64 rebuild /path/to/example-0.1-1.src.rpm

実行すると、ログと正常に実行された場合に生成されるRPMファイルが/var/lib/mock/epel-6-x86_64/result以下に生成されます。生成されるファイルは下記の通りです。

  • available_pkgs

  • build.log

  • installed_pkgs

  • root.log

  • state.log

  • example-0.1-1.src.rpm

  • example-0.1-1.x86_64.rpm

root.log, state.log, build.logでビルドに問題無いかを確認できます。mock自体はPythonで書かれていて、実行時にエラーになるとPythonのログを吐いてくれるので、原因は割と追っかけやすいです。

chrootツリーは生成されるとepel-6-x86_64の場合約432MB、キャッシュが/var/cache/mock/epel-6-x86_64以下に約327MB、/var/cache/yum以下に約71MB程度できます。キャッシュは依存するパッケージなどによって増減するでしょうけど、まぁ最低1GB程度あれば事足りそうです。

Gitリポジトリからビルド

git-buildpackageと同様に、Gitリポジトリからビルドするには、 –scm-enable オプションおよび –scm-option オプションを使います。

mock(1)のサンプルでは、 –scm-option の引数には:

mock -r fedora-14-i386 --scm-enable --scm-option package=pkg

とだけあり、他のkey-valueが分からないのですが、ここで前述の/etc/mock/site-defaults.cfgが参考になります。次のような記述があります。

#
# Things that must be adjusted if SCM integration is used:
#
# config_opts['scm'] = True
# config_opts['scm_opts']['method'] = 'git'
# config_opts['scm_opts']['cvs_get'] = 'cvs -d /srv/cvs co SCM_BRN SCM_PKG'
# config_opts['scm_opts']['git_get'] = 'git clone SCM_BRN git://localhost/SCM_PKG.git SCM_PKG'
# config_opts['scm_opts']['svn_get'] = 'svn co file:///srv/svn/SCM_PKG/SCM_BRN SCM_PKG'
# config_opts['scm_opts']['spec'] = 'SCM_PKG.spec'
# config_opts['scm_opts']['ext_src_dir'] = '/dev/null'
# config_opts['scm_opts']['write_tar'] = True
# config_opts['scm_opts']['git_timestamps'] = True

# These options are also recognized but usually defined in cmd line
# with --scm-option package=<pkg> --scm-option branch=<branch>
# config_opts['scm_opts']['package'] = 'mypkg'
# config_opts['scm_opts']['branch'] = 'master'

この中で実質上必須のオプションは次の通りです。 これらのkey毎に –scm-option で指定する必要があります。

  • package

  • git_get

  • spec

  • wirte_tar

まず、 git_get を指定しないと上記の例がデフォルト設定になっているため、 git://localhost/package.git からcloneしようとします。 git clone をつけないといけないのと、既にclone済みのディレクトリをそのまま使えないのがイケてないですね。 ただ、後者については、 git_get=git clone /path/to/repo と指定すれば、clone済みのリポジトリからcloneできます。 4 また git_get でcloneすると、自動的にchroot内のローカルリポジトリにchdirします。

spec の指定はローカルリポジトリのディレクトリをrootとし、そこからの相対パスで指定できます。なので、upstreamのGitリポジトリにrpm用のspecファイルが含まれている場合、specファイルの相対パスを指定してビルドすることができます。簡便であるという点では良いのですが、Debianパッケージのように、upstreamのソースコードとメンテナスクリプトを分離する概念がmockしないため、第三者のFLOSSのGitリポジトリからforkしてパッケージ管理するとコミットログが混じってしまって混ぜるな危険な感じではあります。あるいは、自分で git-buildpackage のように upstream ブランチと master ブランチを分けて管理する、という方法も取れますが、自分で一からやるのは面倒ですね。

write_tar は、specファイルの中で Source タグを指定している場合、 True を指定します。デフォルトは False です。 True を指定するとchroot内の /builddir/build/SOURCES ディレクトリ以下にGitリポジトリから tar czf で生成されたtarballが配置されます。 5 これが生成されないと、mockでの rpmbuild 実行中にコケます。

実際にGitリポジトリからビルドするには次のように実行します。:

$ mock -r epel-6-x86_64 --scm-enable --scm-option package=example --scm-option git_get="git clone git@remote/example.git" --scm-option spec=rpm/example.spec --scm-option write_tar=True

ローカルミラーやローカルリポジトリを使う場合

カスタムパッケージなどに依存するパッケージを作るには、ローカルリポジトリが必要になります。 chroot環境で参照するyumリポジトリは、 -r オプションで指定した、 epel-6-x86_64.cfg に記述があります。

config_opts['root'] = 'epel-6-x86_64'
config_opts['target_arch'] = 'x86_64'
config_opts['legal_host_arches'] = ('x86_64',)
config_opts['chroot_setup_cmd'] = 'groupinstall buildsys-build'
config_opts['dist'] = 'el6' # only useful for --resultdir variable subst

config_opts['yum.conf'] = """
[main]
cachedir=/var/cache/yum
debuglevel=1
reposdir=/dev/null
logfile=/var/log/yum.log
retries=20
obsoletes=1
gpgcheck=0
assumeyes=1
syslog_ident=mock
syslog_device=

# repos
[base]
name=BaseOS
enabled=1
mirrorlist=http://mirrorlist.centos.org/?release=6&arch=x86_64&repo=os
failovermethod=priority

[updates]
name=updates
enabled=1
mirrorlist=http://mirrorlist.centos.org/?release=6&arch=x86_64&repo=updates
failovermethod=priority

[epel]
name=epel
mirrorlist=http://mirrors.fedoraproject.org/mirrorlist?repo=epel-6&arch=x86_64
failovermethod=priority

[testing]
name=epel-testing
enabled=0
mirrorlist=http://mirrors.fedoraproject.org/mirrorlist?repo=testing-epel6&arch=x86_64
failovermethod=priority

[local]
name=local
baseurl=http://kojipkgs.fedoraproject.org/repos/dist-6E-epel-build/latest/x86_64/
cost=2000
enabled=0

[epel-debug]
name=epel-debug
mirrorlist=http://mirrors.fedoraproject.org/mirrorlist?repo=epel-debug-6&arch=x86_64
failovermethod=priority
enabled=0
"""

なので、ローカルミラーやローカルリポジトリを使う場合にはここを変更すればよいでしょう。なお、デフォルトでgpgcheckは無効になっています。 6 gpgcheckを有効にして、ローカルリポジトリのGPG公開鍵を追加する場合は、/var/cache/mock/epel-6-x86_64/root_cache/cache.tar.gz を修正し、chrootツリーを作成しなおす必要があります。

Jenkinsでmockを実行する

Jenkinsでmockを実行する場合、ssh経由でしか git clone できないリポジトリを使う場合には、private keyのパスフレーズの入力面倒です。なので、JenkinsのGitプラグインを使って、JenkinsのWORKSPACEにcloneしたローカルリポジトリを使ってビルドすることになります。 ところが、前述の通り、mockは git_getgit clone する必要があります。なので、

  1. JenkinsのGitプラグインで git clone する

  2. mockに –scm-option git_get=’git clone ${WORKSPACE}/repo’ オプションで、JenkinsのWORKSPACEから更に git clone する

という2段階の git clone を行えば使えることになります。 なんかダサいですね。

Bitbucketに用意した サンプルリポジトリ を使うと、Jenkinのジョブ設定は下記のようになります。

mock -r epel-6-x86_64 \
    --resultdir=${WORKSPACE}/result \
    --scm-enable \
    --scm-option package=example \
    --scm-option git_get="git clone ${WORKSPACE}/example" \
    --scm-option spec=rpm/example.spec \
    --scm-option write_tar=True

–resultdir オプションで${WORKSPACE}/result を指定すると、ワークスペース下にresultディレクトリが生成されます。

残タスクとしては、生成するRPMにGPGで署名すること、ローカルリポジトリにpush & createrepoを実行することですね。

まとめ

以上で、Debian/UbuntuでJenkinsを使って、RHEL系のシステムのRPMの自動ビルドもできるようになりました。普段Debianシステムしか使ってないのに、やんごとなき事情でRPM作らざるを得なくなっても、Debianシステムだけで基本的には完結できますね。

Footnotes

1

自分で作って&メンテしておきながら、これはヒドイ。

2

他の人が作ったspecファイルを今回の検証に使ったらそうだったのです。

3

デフォルトでは、CentOSとFedoraの各バージョン用のファイルが用意されています。この辺はpbuilder/cowbuilderよりも親切で便利ですね。

4

CVS、SVNも対応できるようにするため、とは言え、 コマンドを書かないとアカン のは微妙ですね。

5

git archive コマンド ではない のです。これもCVS, SVNも対応するためでしょう。

6

upstream自体で無効 にされています

Segfault occurs when executing celery worker with librabbitmq1

Ubuntu Trustyのpython-celeryパッケージを使って、celery workerを実行すると、プロセスが突然死ぬ、という現象に遭遇しました。 原因は、AMQPクライアントライブラリであるlibrabbitmq1がsegfaultを起こしていたためでした。

[881933.805893] celery[12704]: segfault at 0 ip 00007f23cd79f6ab sp 00007fff5afe3f70 error 4 in librabbitmq.so.1.1.1[7f23cd797000+10000]

celerybeatを使うと、segfaultは起きないものの、RabbitMQにジョブだけたまり、ちっとも処理されない、という現象が発生します。

Sidで開発していたときには発生しなかった問題だったので、比較してみたらSidではlibrabbitmq1とこのライブラリに依存するpython-librabbitmqは使っておらず 1 、python-amqpを使っていました。依存関係としては下図のようになります。

python-amqpもインストールされていたのですが、python-librabbitmqがインストールされているとこちらが優先されるようです。

workaround

librabbitmq1とpython-librabbitmq 2 をアンインストールすると、この問題は解決します。ちなみにSidでこれらのパッケージをインストールしてみましたが再現しなかったのでTrusty(のパッケージのバージョン)だけでの問題のようです。

Footnotes

1

パッケージ自体インストールしていませんでした。

2

librabbitmq1パッケージをアンインストールすれば、依存関係でpython-librabbitmqも自動的にアンインストールされます。

I made debsign of Python libary that can be run without a TTY

この辺の話の続きです。

諸事情で、ローカル環境でDebianパッケージをbackportsしたり、 オリジナルのパッケージを諸事情で公式パッケージではなく、非公開パッケージとして、 それらをローカルアーカイブに突っ込むのに、Jenkinsで全て完結させたいなと思い、 1 いろいろ試行錯誤したところ、どう頑張っても debsing で署名することだけはできないことが分かりました。

debsign コマンド自体がつぎのように標準入力からパスフレーズを受け取ってプロンプトで代入する、ということができません。

$ echo -e "passphrase\npassphrase\n" | debsign some.changes

同じように試行錯誤している人はいるみたいですが、解決している人はいなさそうでした。 一方、 gpg コマンドは、 –batch および –no-tty というオプションがあります。 で、GnuPGのPythonバインディングである python_gnupg はこの機能を使えることが分かりました。

debsign コマンドの挙動としては、まず.dscファイルを署名し、署名後のファイルサイズとmd5, sha1, sha256のチェックサムを取得し、.changesのエントリを書き換え、.changesファイルを署名します。 で.changesファイルを sed コマンドなどで書き換えるのはちょいと面倒だなと思っていたら、.changesファイルを扱う、deb822というモジュールがありました。これは、python-debianパッケージとして提供されています。 2 これを使うと、.changesファイルの情報をDictに似たデータとして扱えます。

で、これらを使って、pydebsignというPythonライブラリを作りました。

このライブラリで debsign と同等の処理を、JenkinsなどのCIで実行させることができます。 そのサンプルが、 これ です。

このコードでJenkinsでパッケージビルド&署名&reprepro管理のローカルアーカイブへの登録まで全部自動で行えるようになりましたよ、 というお話でした。 3

footnote

1

一からソースパッケージを作るところの自動化は除く。

2

PyPI でも公開されています。 https://pypi.python.org/pypi/python-debian

3

リンク先のGistのコードを使うために、JenkinsおよびRepreproにも設定が必要なのですが、それはまた別の話。

difflib.ndiff() and assertMultiLineEqual() are very useful

Gitでdiffを行うとデフォルトでは、GNU diffの diff -u と同じ挙動なので行単位での差異がでます。 文章のdiffを取りたい場合には –word-diff というオプションをつけると、 [-word-]{+word+} という形式で違う箇所が表示されるので便利です。

さて本題。Pythonでテストコードを書いて、文字列の比較に assertEqual() を使うと、

_____________________________________ DebbuildTests.test_generate_batch_script ______________________________________

self = <debbuild.tests.test_debbuild.DebbuildTests testMethod=test_generate_batch_script>

    def test_generate_batch_script(self):
        """ unit test of generate_batch_script """
        debbuild.generate_batch_script(self.params)
        # self.assertMultiLineEqual(self.batch_content,
        self.assertEqual(self.batch_content,
>                        debbuild.generate_batch_script(self.params))
E       AssertionError: '#!/bin/sh -x\nexport DEBFULLNAME="Dummy Maintainer"\nexport DEBEMAIL=dummy@example.org\napt-get -y install curl devscripts quilt patch libdistro-info-perl fakeroot\napt-get -y build-dep shello\ndget -d http://example.org/debian/pool/main/s/shello/shello_0.1-1.dsc\ndpkg-source -x shello_0.1-1.dsc\n(\ncd shello-0.1\ndebuild -us -uc\n)\ncp -f shello_0.1-1.debian.tar.gz  shello_0.1.orig.tar.gz shello_0.1-1.dsc /home/mkouhei/debbuild/temp/\n' != '#!/bin/sh -x\nexport DEBFULLNAME="Dummy Maintainer"\nexport DEBEMAIL=dummy@example.org\napt-get -y install curl devscripts quilt patch libdistro-info-perl fakeroot\napt-get -y build-dep shello\ndget -d http://example.org/debian/pool/main/s/shello/shello_0.1-1.dsc\ndpkg-source -x shello_0.1-1.dsc\n(\ncd shello-0.1\ndebuild -us -uc\n)\ncp -f shello_0.1-1.debian.tar.gz shello_0.1.orig.tar.gz shello_0.1-1.dsc /home/mkouhei/debbuild/temp/\n'

tests/test_debbuild.py:220: AssertionError

とエラーは検出できてもどこが間違っているのか解読するのは困難です。なので、 assertMultiLineEqual() を使うとこの問題を解決できます。

(snip)
>                        debbuild.generate_batch_script(self.params))
E       AssertionError: '#!/bin/sh -x\nexport DEBFULLNAME="Dummy Maintainer"\nexport DEBEMAIL=dummy@exam [truncated].
.. != '#!/bin/sh -x\nexport DEBFULLNAME="Dummy Maintainer"\nexport DEBEMAIL=dummy@exam [truncated]...
   E         #!/bin/sh -x
   E         export DEBFULLNAME="Dummy Maintainer"
   E         export DEBEMAIL=dummy@example.org
   E         apt-get -y install curl devscripts quilt patch libdistro-info-perl fakeroot
   E         apt-get -y build-dep shello
   E         dget -d http://example.org/debian/pool/main/s/shello/shello_0.1-1.dsc
   E         dpkg-source -x shello_0.1-1.dsc
   E         (
   E         cd shello-0.1
   E         debuild -us -uc
   E         )
   E       - cp -f shello_0.1-1.debian.tar.gz  shello_0.1.orig.tar.gz shello_0.1-1.dsc /home/mkouhei/debbuild/temp/
   E       ?                                  -
   E       + cp -f shello_0.1-1.debian.tar.gz shello_0.1.orig.tar.gz shello_0.1-1.dsc /home/mkouhei/debbuild/temp/

上記の通り、cpコマンドの2つ目の引数の後ろにスペースが一つ余計に入っていることが、”-” で示してあるので分かりやすいです。 最近までこのメソッドの存在に気づいていませんでした。ちゃんとリファレンス読め、ワシ…。

自分で同様の処理を行うなら、 difflib.ndiff() を使うとできます。次のような関数を定義すれば assertMultiLineEqual() の代わりに通常のコードの中でも使えます。

import difflib

def worddiff(string1, string2):
    diff = difflib.ndiff(string1.splitlines(1), string2.splitlines(1))
    print(''.join(diff))

ググるとndiff()については結構ブログで書かれているみたいなのですが、assertMultiLineEqualの方はあまり見当たらないですね。

Infinite loop using chord of Celery

Cronのような使い方としては、既に Celery を使っていたのですが、複数のキューを並列実行かつ、各キュー自体はFIFOで処理させられないかなと、チュートリアルを順にやっていた時に見つけたバグです。 チュートリアル にある下記のコードを実行すると、

>>> from celery import chord
>>> from proj.tasks import add, xsum

>>> chord((add.s(i, i) for i in xrange(10)), xsum.s())().get()

次のようなログが出て無限ループします。:

[2014-04-25 00:04:45,623: INFO/MainProcess] Received task: celery.chord_unlock[dba8e0f8-cb96-4c91-948c-2acd5ccc3ae8] eta:[2014-04-25 00:04:46.620008+09:00]

Debian GNU/Linux Sidの今のCeleryのパッケージ(python-celery)のバージョンが、3.1.9なのですが、 このバグが修正されているのは、3.1.11に含まれる コミット です。

3.1.9から3.1.11の間には、他にもchordに関するバグが結構修正されているので、3.1.11をパッケージにしてもらうように、 BTSに登録して おきました。

なお、group()の方は問題ないので、3.1.11未満で同じことをしたければ、

>>> (group(add.s(i, i) for i in xrange(10)) | xsum.s())().get()

の方を使えば大丈夫です。

このコミットのパッチ を適用したdebdiffの結果も送付しようかと思ったのですが、現在の環境では、これとは別でテストがコケるFTBFSも存在したので それを報告して おきました。