daily dayflower

2012-10-19

Skype4Py で bot を作る

コンタクトリストのユーザーがオンラインになったら「おかえり」というストーキング bot

ちなみに Ubuntu 12.04 (Precise) で Skype4Py (パッケージ名 python-skype) をインストールするにはレポジトリppa:skype-wrapper/ppa を追加する必要がある (パッケージ skype-wrapper.deb 自体はインストールする必要はない)。

# -*- coding: utf-8 -*-

import Skype4Py
import Queue
import logging

# Skype4Py のログを出力する (DEBUG だと大量なので注意; 便利だけど)
logging.basicConfig(level=logging.DEBUG)

# ALTER CHAT の expected response が変わったので修正
def my_Chat__Alter(self, AlterName, Args=None):
    return self._Owner._Alter('CHAT', self.Name, AlterName, Args,
                              'ALTER CHAT %s' % AlterName)
Skype4Py.chat.Chat._Alter = my_Chat__Alter


q = Queue.Queue()

def on_online_status(user, status):
    print '%s (%s): %s' % (user.Handle, user.FullName, status)
    if status == 'ONLINE':
        q.put(user.Handle)

def main():
    skype = Skype4Py.Skype()
    skype.OnOnlineStatus = on_online_status
    skype.Attach()

    while True:
        try:
            item = q.get(True, 86400)   # 0 にすると KeyboardInterrupt が効かない
            chat = skype.CreateChatWith(item)
            chat.SendMessage('おかえり')
            chat.Leave()
        except Queue.Empty:
            pass

main()

skypeグローバル変数にぶっこんで,on_online_status() から CreateChatWith() してもいいんだけど,やり取り等は別スレッドで動いているらしく,ちょいと気持ち悪かったので Queueスレッド間通信することにした。おかげで while True: time.sleep(1) みたいなことをしなくてもよくなった (実質 Queue.get() してるので同じだけど)。

上のサンプルでは Queue にユーザーハンドルだけつっこんでるけど,将来的にはそれなりのコマンドオブジェクト等をつっこむようにしたほうがいいですね。

2012-10-17

uWSGI でファイルが更新された時にリロードする

最初は inotifyx 使って自力で書こうとしてたんだけど,ブロックしてうまくいかなかったりして,thread でも立てなきゃいけないのかなと思って uwsgidecorators を読んでたら,そもそも uwsgidecorators (というかそもそも uwsgi-core) にファイル更新検知機能がついてた。

メインとなる wsgi ファイルが main.wsgi というファイル名だったとして,監視するモジュール*1 watcher.py は:

# watcher.py
import uwsgi
from uwsgidecorators import filemon

@filemon('main.wsgi')
def reloaded(num):
    uwsgi.reload()

対応する uWSGI のパラメータは,たとえば:

; uwsgi.ini
[uwsgi]
master = true
plugins = python,http
http = :8000
wsgi-file = main.wsgi
import = watcher

ファイルがいっぱいあったらどうすんねーんと思うかもしれないけど,@filemon デコレータは,ディレクトリの監視もできます。

ちなみに内的には inotify 機構を使っているので無駄はないはず。IN_ALL_EVENTS レベルの監視をしてるんで,touch とかしてもちゃんとリロードされます。



あと今回の話と関係ないけど,たとえば 1 分間リクエストがなかった場合にワーカーを自動的に kill するには

idle = 60

のようにしておけばよい。uWSGI 使うようなシチュエーションでそんなにメモリをケチるシーンはないかもしんないけど。

追記: idle (や lazy) と @filemon の相性はよくないかもしんない。ワーカーがいない状態で編集してリクエスト投げたら編集前の内容だったことがあった。気のせいかもしれないけど。


もういっこわりと便利なオプションharakiri

harakiri = 60

のようにしておくと,処理に 60 秒以上かかるワーカーは強制的に落とされます。

*1:別モジュールだてにしなくてもいい (main.wsgi に入れ込む) のかもしれないけど追ってない追記: やってみたら別モジュールに独立させなくてもうまくいった。小さい捨て WSGI スクリプトであればリロードのロジックを入れ込むのが楽。(もちろん,WSGI アプリケーションとして独立させるなら入れ込まないほうがいいけど)

2010-09-16

Compiz を Python からあやつる

タイトルは大袈裟。


ここ半年ほど Compiz のワークスペース切替器にキューブではなくデスクトップの壁(wall)を使っています。こっちのほうがキビキビ動くし。

んでこの wall plugin ですが,システム起動直後だと,ワークスペース切替時に壁紙がスクロールしないんです。まぁそんな仕様だと思えばいいんですけど,CompizConfig 設定マネージャを立ち上げて,「デスクトップの壁」プラグインを一度無効化して有効にすると,壁紙もスクロールするようになります。じゃあそういう仕様じゃないじゃん。

いままでいちいち起動後に CompizConfig 設定マネージャを立ち上げて修正していたんですけど,めんどくさい。CompizConfig 設定マネージャ*1Python で書かれているっぽいので Python から Compiz の挙動を操作できるんじゃね,と思ってやってみました。

import time
import compizconfig
ccs = compizconfig

context = ccs.Context()
wall = ccs.Plugin(context, 'wall')

wall.Enabled = False

time.sleep(1)

wall.Enabled = True

いちいち compizconfig.Context() って書いてもいいんだけど長ったらしいので ccs = compizconfig ってしてます*2。って実は CompizConfig 設定マネージャのソースの真似。Python ってこういう書き方もできるのねと勉強になりました。

time.sleep() なしでもうまく動くかなと思ったけど,ムリだったので入れてる。1秒も待たされるのはアレだけど,まぁ実害ないというか設定マネージャ立ち上げるより百倍はやいので。


ソース見ると相当たいしたことないでしょ。でも API ドキュメントとかが全然なくて結構苦労しました。

CompizGit レポジトリno title とかで見られるんだけど,libcompizconfig のソースからinclude/ccs.h とか Python バインディングのソースからcompizconfig.pyx とか見ながら書いた。

*1:よくよく考えたら変な名前ですよね。まぁ英語でももともと CompizConfig Setting Manager ですけど。

*2:まぁ本気で短く書こうと思ったら compizconfig のままやったほうが今回は短いと思う。Context オブジェクトもわざわざ変数に代入する必要ないし。

2010-02-12

textwrap でマルチバイト文字列を fold する

Mercurial に日本語メッセージカタログが同梱されるようになって久しいんですが、Mercurial 1.4 以降で日本語ヘルプメッセージが一部文字化けするようになってしまいました。

化け方からすると text wrapping (folding) 処理まわりで化けているみたい(文字のバイト境界を無視して folding している)。ということでちょっと Mercurial のコードを浅追いしてみたところ、minirst.pytextwrap を呼び出していて textwrap がマルチバイトに対応していないからではないかと思いました。

なので、とりあえずマルチバイト文字列に対応した textwrap を書いてみようと思いました。

import sys, textwrap
from unicodedata import east_asian_width

__all__ = ['MBTextWrapper', 'wrap', 'fill']

def _mb_width(c):
    eaw = east_asian_width(c)
    if eaw == 'F':
        return 2
    elif eaw == 'W':
        return 2
    elif eaw == 'A':
        return 2

    return 1

class MBTextWrapper(textwrap.TextWrapper):
    def __init__(self,
                 width=70,
                 initial_indent="",
                 subsequent_indent="",
                 expand_tabs=True,
                 replace_whitespace=True,
                 fix_sentence_endings=False,
                 break_long_words=True,
                 encoding=None):
        textwrap.TextWrapper.__init__(self, width, initial_indent,
                                      subsequent_indent, expand_tabs,
                                      replace_whitespace,
                                      fix_sentence_endings,
                                      break_long_words)

        self.encoding = encoding or sys.stdout.encoding

    def _mb_len(self, str):
        if not isinstance(str, unicode):
            str = unicode(str, self.encoding)

        return sum(_mb_width(c) for c in str)

    def _mb_cut(self, str, max_len):
        want_unicode = isinstance(str, unicode)
        if not want_unicode:
            str = unicode(str, self.encoding)

        tok1, tok2 = '', ''
        l = 0
        for c in str:
            w = _mb_width(c)

            if l + w <= max_len:
                tok1 += c
            else:
                tok2 += c
            l += w

        if want_unicode:
            return tok1, tok2

        return tok1.encode(self.encoding), tok2.encode(self.encoding)

    def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
        space_left = max(width - cur_len, 1)

        if self.break_long_words:
            cut, res = self._mb_cut(reversed_chunks[-1], space_left)
            cur_line.append(cut)
            reversed_chunks[-1] = res
        elif not cur_line:
            cur_line.append(reversed_chunks.pop())

    def _wrap_chunks(self, chunks):
        lines = []
        if self.width <= 0:
            raise ValueError("invalid width %r (must be > 0)" % self.width)

        chunks.reverse()

        while chunks:
            cur_line = []
            cur_len = 0

            if lines:
                indent = self.subsequent_indent
            else:
                indent = self.initial_indent

            width = self.width - self._mb_len(indent)

            if chunks[-1].strip() == '' and lines:
                del chunks[-1]
            while chunks:
                l = self._mb_len(chunks[-1])

                if cur_len + l <= width:
                    cur_line.append(chunks.pop())
                    cur_len += l

                else:
                    break
            if chunks and self._mb_len(chunks[-1]) > width:
                self._handle_long_word(chunks, cur_line, cur_len, width)

            if cur_line and cur_line[-1].strip() == '':
                del cur_line[-1]

            if cur_line:
                lines.append(indent + ''.join(cur_line))

        return lines

def wrap(text, width=70, **kwargs):
    w = MBTextWrapper(width=width, **kwargs)
    return w.wrap(text)

def fill(text, width=70, **kwargs):
    w = MBTextWrapper(width=width, **kwargs)
    return w.fill(text)

コード量が多くなってしまっていますが、_handle_long_word()_wrap_chunks() はほぼ(添付の textwrap の)コピペで、len() を呼び出すところ等を置き換えているだけです。

エンコーディングのデフォルト値を sys.stdout.encoding にしてるのはちょっと問題かもしんないですね。sys.getdefaultencoding() にしようかとも思ったんですが、デフォルトのインストール状態だとたいがい ascii のままなのでアレだし。まぁ所詮デフォルト値ですが。Mercurial のことだけ考えるのならデフォルト値を 'utf-8' 決め打ちでいいかもしんないです。



Mercurial の文字化けを改善するには、上記のコードをたとえば /usr/lib/python2.4/site-packages/mercurial/ 以下などに mbtextwrap.py などの名前で置きます。

んで、mercurial/minirst.py のコードで textwrap を import してるところを mbtextwrap に置き換えて、textwrap.fill() を呼び出しているところを mbtextwrap.fill() に置換すれば*1、一応ヘルプメッセージの文字化けはなくなりました。

えっといいわけがましいですが Python は母国語でないんで、なんかこうしたほうがいいとかアドバイスがあれば是非是非。

*1:あと前述のようにエンコーディングが sys.stdout.encoding をデフォルトとしているので encoding='utf-8' のように引数を指定したほうがいいかもしんないです。

2009-06-23

MQ の適用済パッチのログメッセージを一括取得する extension を書いた

たとえば,

$ hg qpush
applying 248.diff
now at: 248.diff

$ hg qpush
applying 249.diff
now at: 249.diff

$ hg qseries -s
248.diff: 記事追加機能を仮実装した(うまくいっていない)
249.diff: 記事追加機能を修正した
250.diff: 記事編集機能を実装した

こんな状況の場合 hg qseries -s でだいたいの変更内容は取得できるんだけど,一つのパッチ(コミット)のログが複数行にわたる場合,これだけだとわかんない。

いちおう,hg log -v すると,複数行のログもとれる。

$ hg log -r qbase:qtip -v
changeset:   248:cf52d09c494d
tag:         qbase
user:        dayflower <dayflower@example.com>
date:        Tue Jun 23 14:22:08 2009 +0900
files:       lib/AddArticle.pm
description:
記事追加機能を仮実装した(うまくいっていない)


changeset:   249:94502ca09ad5
tag:         qtip
tag:         tip
user:        dayflower <dayflower@example.com>
date:        Tue Jun 23 14:41:07 2009 +0900
files:       lib/AddArticle.pm db/schema.sql
description:
記事追加機能を修正した
スキーマを変更した

だけど,なにかと冗長な情報になってしまう。

今回つくった extension を使うと,

$ hg qlog
[248:cf52d09c494d]
記事追加機能を仮実装した(うまくいっていない)

[249:94502ca09ad5]
記事追加機能を修正した
スキーマを変更した

のように,qbaseqtip(つまり applied queue)に限って,changeset ID とログメッセージを取得することができます。

これをコピペすれば,他の VCS へコミットするときのコミットメッセージに使える,というわけです。


実は hg log には表示に使うテンプレートを指定する機能があるので,それ使えば同じようなことは標準のモジュールだけでできるんだけど,勉強のために extension として書いてみました。

# qlog.py

from mercurial import context, util, cmdutil, error
from mercurial.i18n import _
from mercurial.node import hex, short

class qlog_printer(cmdutil.changeset_printer):
    def _show(self, ctx, copies, prop):
        changenode = ctx.node()
        log        = self.repo.changelog
        changes    = log.read(changenode)

        self.ui.write(_("[%d:%s]\n") % (ctx.rev(), short(changenode)))

        description = changes[4].strip()
        if description:
            self.ui.write(description)
            self.ui.write("\n")

        self.ui.write("\n")

def qlog(ui, repo):
    try:
        repo.lookup('qbase')
        repo.lookup('qtip')
    except error.RepoError:
        return

    get = util.cachefunc(lambda r: repo[r].changeset())
    changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, [], get,
                                                 {'rev': ['qbase:qtip']})

    patch = False       # matchfn or cmdutil.matchall(repo)

    displayer = qlog_printer(ui, repo, patch, {}, True)

    for st, rev, fns in changeiter:
        if st == 'add':
            displayer.show(context.changectx(repo, rev))
        elif st == 'iter':
            displayer.flush(rev)

cmdtable = {
    "qlog": (qlog, [], _('hg qlog')),
}

コード自体は Mercurial のコードをコピペしたので,あんまり見通しはよくないです*1

extension でコマンドを実装するには,このように cmdtable というテーブル変数に関数などを登録しておけばいいみたい。


この extension のインストールのしかたは,適当な場所において,.hgrc

[extensions]
qlog=/home/dayflower/hgext/qlog.py

とかみたく,extension を置いた場所をフルパスで指定すればいいです。このへんのしくみは非常にお手軽ですね。

*1:てか Mercurial のコードは期待したほど綺麗な構造でもなかった。

2008-05-09

Boodler で環境音を BGM に

ごくごく私的用事で環境音を延々と流す必要にせまられました。そういえば以前ためしてガッテンで環境音を聴いていると集中が続きやすいという話もありましたね。そのような環境音を生成する Web ページやシェアウェア*1,既製の素材もあるようです*2

ともかく,フリーウェアでそのようなニーズに答えられるものはないものか,と探していたら ピンクノイズを発生させる | OSDN Magazine に答がありました。Boodler というソフトウェアです。

Boodler とは

Boodler is a tool for creating soundscapes -- continuous, infinitely varying streams of sound.

Boodler Home

Boodler がしてくれることを簡単にまとめると,さまざまな sound snippets をシームレスかつダイナミックにつなぎあわせて sound stream を生成すること,です(デフォルトで停止させない限り延々と再生しますし,ファイルに落とし込むこともできます)。

Boodler は C で書かれたサブモジュールPython モジュールで構成されています。このため Linux(や *BSD) はもちろん,MacOS X や Windows でも動きます。ただし素敵な GUI はついておらず,コマンドラインから操る必要があります。

Boodler は下記の3つのコンポーネントにわかれます。

  • Python と C で書かれたコアモジュールBoodler-*.tar.gzsrc/ 以下)
  • 環境音ストリームを生成する agent(Boodler-*.tar.gzeffects/ 以下)
  • 素材となる sound snippets(boodler-snd-*.tar.gz

Boodler をインストール(Windows 編)

Python で書かれているので http://www.python.org/ より Windows 用インストーラ(現時点で Version 2.5.2)をダウンロードしてインストールしておきます。インストーラが *.py をプログラムとして認識するようにしてくれるみたいなので,特に PATH を設定する必要はありません。

次に Boodler 本体を取得します。http://eblong.com/zarf/boodler/ の「Get Boodler」から「download newer package for Windows」というリンクを見つけてダウンロードします。これを(どこでもいいですが)例えば C:\App\boodler などのディレクトリに展開しておきます。

この配布物は Python 2.5 用ではないようなので,「download cboodle module for Windows Python 2.5」というリンクから Python 2.5 用モジュールを取得して置き換えます。README.txt に手順が書いてありますが,C:\App\boodler\boodle\cboodle.dll を削除して代わりに cboodle.pyd を置きます。

最後に Boodler sound library を取得します。「download Boodler sound library」というリンクからダウンロードします。展開すると boodler-snd というディレクトリがあるので,これをアプリケーションのフォルダ内に移動します(先ほどの例だと C:\App\boodler\boodler-snd)。

ここまでで,C:\App\boodler フォルダの中身は以下のようになっているかと思います。

  • boodle/
  • boodler-snd/
  • doc/
  • effects/
  • boodler.py
  • boomsg.py
  • setpath.bat
  • 他雑多なファイル

Boodler を実行するには,コマンドプロンプトを開いて C:\App\boodler ディレクトリに移動し,環境変数setpath.bat で設定します。しかるのちに boodler.py コマンドを実行します。

C:\Documents and Settings\dayflower> cd C:\App\boodler

C:\App\boodler> setpath.bat

C:\App\boodler> boodler.py cricket.CricketMeadow
running "Texas Meadow Katydid and friends"

虫の音が聴こえてきたら成功です。「Ctrl+C」で止めます。


昨日家でここまで試して満足したので,以下は Linux でテストしました。

Boodler をインストール(Linux 編)

Ubuntu 8.04 で試しました。

Python が必要なのは当たり前ですが,ビルドするためにはヘッダやライブラリが必要となります。python2.5-dev をインストールしておきます。

ほか,あるとうれしいライブラリ。

  • libesd0-dev
  • liblame-dev
  • libogg-dev
  • libvorbis-dev

libogg-devlibvorbis-devlibshout3-dev をインストールすると依存で自動的に入りますからそれを入れると楽です。

んで,setuptools に対応してるので python setup.py build, sudo python setup.py install でビルド&インストールできます。ですが,これは上で述べたコアモジュールしかインストールしてくれないので,どうせなら,ということでユーザ環境にインストールしてみます。

$ mkdir tmp && cd tmp

$ tar zxf ~/Boodler-1.6.1.tar.gz

$ cd Boodler-1.6.1

$ python setup.py build

$ python setup.py install --prefix ~/bin/Boodler

ホームディレクトリbin/Boodler 以下にインストールしてみました。他に必要となる effect agents や sound library を同所にコピーします。

$ cp -R effects ~/bin/Boodler/

$ cd ~/bin/Boodler

$ tar zxf ~/boodler-snd-021902.tar.gz

$ ls -F

$ bin/ boodler-snd/ effects/ lib/

どうせ BOODLER_SOUND_PATHBOODLER_EFFECTS_PATH などの環境変数の設定が必要となるので起動用シェルスクリプトを作ります。

$ vi ~/bin/boodler.sh

$ vi ~/bin/boomsg.sh

$ chmod a+x ~/bin/boodler.sh ~/bin/boomsg.sh

それぞれの内容は,

#!/bin/sh
# --- boodler.sh

APPPATH=~/bin/Boodler

export BOODLER_SOUND_PATH=$APPPATH/boodler-snd
export BOODLER_EFFECTS_PATH=$APPPATH/effects

PYTHONPATH=$APPPATH/lib/python2.5/site-packages \
    python $APPPATH/bin/boodler.py $*
#!/bin/sh
# --- boomsg.sh

APPPATH=~/bin/Boodler

PYTHONPATH=$APPPATH/lib/python2.5/site-packages \
    python $APPPATH/bin/boomsg.py $*

こんな感じにしました。

ためしに実行してみます。

$ ~/bin/boodler.sh pwrain.Rainforest
running "rain in the rainforest"

沢のある森にいる気分になってきました?

Boodler を使ってみる

コマンドラインで指定している引数は agent を指定するためのものです。先ほどの例だと pwrain.Rainforest という引数でしたが,これは pwrain というパッケージの Rainforest というクラスで実装されているのです。

どのような agent があるのか確認してみます。

$ cd ~/bin/Boodler/effects

$ grep -e '^class ' *.py

blop.py:class BlopEchoes(Agent):
blop.py:class BlopSpace(Agent):
blop.py:class TapEchoes(Agent):
blop.py:class TapSpace(Agent):
blop.py:class OccasionalGong(Agent):
blop.py:class TonkEchoes(Agent):
blop.py:class TonkSpace(Agent):
blop.py:class EchoWorld(Agent):
cavepool.py:class Drip(Agent):
cavepool.py:class Still(Agent):
cavepool.py:class Water(Agent):
...... snip snip snip ......

たくさんあります。

それぞれがどのような音に対応するのかは Boodler: Catalog of Soundscapes に記述されています(Utilities, Tools, and Managers はとりあえず無視してください)。

環境音にかぎらず,こんな風変わりな agent もあります。

$ ~/bin/boodler.sh timespeak.Now
running "speak the current time"

英語で時刻を喋り,終了します。

複数のストリームを再生する

effect agent には音を生成するだけではなく,他の agent を制御するものもあります。

一番よく使うのは manager パッケージの agents です。

たとえば,manager.Simultaneous agent は複数の agent を同時に立ち上げます。つまり複数の soundscape streams を合成して再生してくれるわけです。

$ ~/bin/boodler.sh manager.Simultaneous frogs.FrogPond pwrain.RainSounds
running "start several agents simultaneously"

雨のなか,蛙が鳴いているさまが再現されます。

manager.Simultaneous は複数 stream を並列して同時に再生しましたが,順番に再生していく agent もあります。manager.Sequential がそうです。

$ ~/bin/boodler.sh manager.Sequential 5 10 frogs.FrogPond pwrain.RainSounds
running "cycle among several agents"

最初の二つの引数で,遷移する時間間隔(秒)を指定します。この例では5秒〜10秒です。きちんとクロスフェードしてつないでくれます。

Boodler server を立ち上げる

Boodler は他のプロセスからのメッセージを受信して動作を変更するサーバモードも備わっています。

サーバモードで立ち上げるにはコマンドラインにて -l オプションを指定します。また,メッセージ受信用の agent は listen パッケージにいろいろ備わっています。

機能が細かく分化しているので先ほどの manager.Simultaneous を併用してサーバを立ち上げます。

$ ~/bin/boodler.sh -l manager.Simultaneous listen.Agents listen.TimeSpeak listen.Shutdown
running "start several agents simultaneously"

待ちに入りましたので,別プロセスからメッセージを送ってみます。

$ ~/bin/boomsg.sh time

現在時刻を喋りました(listen.TimeSpeak が受信)。

$ ~/bin/boomsg.sh agent crows.SomeCrows

カラスが鳴いています(listen.Agents が受信)。

$ ~/bin/boomsg.sh agent fire.Bonfire

焚き火の音にクロスフェードしました。

$ ~/bin/boomsg.sh shutdown

サーバプロセスが終了しました(listen.Shutdown が受信)。


Boodler には shoutcast 用出力も用意されているので,この listen agent と組み合わせれば,BGM 環境として使うためには別に素敵な GUI はいらない気もしてきます。

$ sudo apt-get install icecast2

$ sudo icecast2 -b -c /etc/icecast2/icecast.xml

で Icecast2 サーバを立ち上げてあげて*3

$ ~/bin/boodler.sh -o shout owstorm.RainForever

で Icecast2 サーバに Boodler からつなぎ,ブラウザで http://localhost:8000/ を見ると mount point が表示されているので,こいつをクリックすると動画プレーヤで再生できます。この例では localhost で完結させましたが,なにか開いたサーバにこれらをつっこんでおけば環境音 BGM サーバのできあがり,というわけです。

ライセンスにご注意

Boodler 本体は LGPL なのですが,sound library がパブリックドメインとは限らないことに注意が必要です。

The sound files in the Boodler sound library are not all in the public domain. Most of them are licensed "for private and non-commercial use only". Some were found by random searching around the web, and appear without any copyright statement at all.

Boodler: Licensing

個人・非商用で使うには問題ないものが多いそうですが。

*1リラックスして眠りやすい音楽を作る「Sound Sleeping」 - GIGAZINE

*2先日、NHKの試してガッテンで、集中するのに役立つ「環境音」と… - 人力検索はてな

*3:デフォルトの /etc/icecast2/icecast.xml の設定がまずいので <changeowner>usergroupicecast2icecast に書き換えて有効にする必要があります

2008-04-01

Mercurial のインストールを bdist_rpm で

Pythonアプリケーション配布に distutils を利用している場合,setup.py の引数として bdist_rpm を与えると dist ディレクトリに RPM パッケージを生成してくれます。

% python setup.py bdist_rpm

running bdist_rpm
creating build
creating build/bdist.linux-i686
creating build/bdist.linux-i686/rpm

...... snip snip snip ......

実行中(--clean): /bin/sh -e /var/tmp/rpm-tmp.72448
+ umask 022
+ cd /home/dayflower/src/mercurial-1.0/build/bdist.linux-i686/rpm/BUILD
+ rm -rf mercurial-1.0
+ exit 0
moving build/bdist.linux-i686/rpm/SRPMS/mercurial-1.0-1.src.rpm -> dist
moving build/bdist.linux-i686/rpm/RPMS/i386/mercurial-1.0-1.i386.rpm -> dist
moving build/bdist.linux-i686/rpm/RPMS/i386/mercurial-debuginfo-1.0-1.i386.rpm
 -> dist

あとは rpm -Uvh してやればいいだけ。

% sudo rpm -Uvh dist/mercurial-1.0-1.i386.rpm

準備中...                ########################################### [100%]
   1:mercurial              ########################################### [100%]

RPM パッケージになっていると,アンインストールするときとか便利ですよね。

最近の RedHat 系 OS だと bdist_rpm がうまくいかない

ところが実際に setup.py bdist_rpm を実行すると下記のように怒られて RPM パッケージの生成に失敗してしまいます。

error: Installed (but unpackaged) file(s) found:
    ......

付属の Python の distutils で bdist_rpm から呼び出される install ターゲットの引数がうまくないらしいです。なので対処します。対策方法は no title 参照*1

ちなみに ~/.rpmmacros

%_unpackaged_files_terminate_build 0

という内容を書いても通るようにはなるみたい*2ですが……これだとパッケージに(エラーででてきた)ファイルが含まれなくなっちゃうんじゃないかなぁ。実際にためしていないのでわかりません。

Mercurial のパッケージ用に MANIFEST.in ファイルを置く

んでこれでめでたく RPM パッケージを作れるようになったんですが,Mercurial のパッケージを作るとウェブ用のコンテンツ(テンプレートとか画像とか)がパッケージに含まれないんですよね。このままだとこのパッケージをインストールしてもウェブインタフェースがうまく使えません。

なので,対処するために setup.py bdist_rpm するまえに配布物の展開ディレクトリに下記の内容で MANIFEST.in ファイルを作ります。

recursive-include templates *

なんでこれでうまくいくのかは,調べたのがずいぶん昔なので忘れました。ともかく,

% rpm2cpio dist/mercurial-1.0-1.i386.rpm | cpio -t

./usr/bin/hg
./usr/lib/python2.4/site-packages/hgext/__init__.py
./usr/lib/python2.4/site-packages/hgext/__init__.pyc
./usr/lib/python2.4/site-packages/hgext/__init__.pyo

...... snip snip snip ......

./usr/lib/python2.4/site-packages/mercurial/templates/atom/changelog.tmpl
./usr/lib/python2.4/site-packages/mercurial/templates/atom/changelogentry.tmpl
./usr/lib/python2.4/site-packages/mercurial/templates/atom/filelog.tmpl

...... snip snip snip ......

./usr/lib/python2.4/site-packages/mercurial/version.py
./usr/lib/python2.4/site-packages/mercurial/version.pyc
./usr/lib/python2.4/site-packages/mercurial/version.pyo
7612 blocks

無事 templates ディレクトリ以下のファイルもパッケージに含まれるようになりました。


Mercurial の README には付属の Makefile を使うようインストラクションが書いてありますが,今回のような bdist_rpm 経由でもうまくいきました*3

*1:なぜ --optimize 1 すると OK になるのかわかりませんが。

*2:see Bug 198877 – setup.py bdist_rpm fails with "Installed (but unpackaged) file(s) found"

*3:Makefile 経由だとなぜかバージョンが unknown になってしまったような。