YARD で拡張ライブラリのドキュメントを書くときに気をつけること

Ruby の拡張ライブラリを書いていていて、YARD でドキュメントを生成しようと思ったらうまく生成されなくてしばらくはまっていた。

YARD も RDoc も、pure Ruby なライブラリだけでなく C で書く拡張ライブラリのドキュメンテーションにも対応している。 そのはずなんだけど、RDoc ではちゃんとメソッドのドキュメントが生成されているのに、YARD だと生成されない、という現象に悩まされた。 いろいろ調べた結果、YARD は簡単に言うと

  1. Ruby のメソッドの本体として使われていそうな関数定義を見つけたら、関数名からそのコメントや定義へのマッピングを追加 (symbol_handler.rb)
  2. 関数内で rb_define_method しているのを見つけたら、マッピングからコメントや定義を取得して、メソッド名と関連付ける (method_handler.rb)

みたいなアルゴリズムになっていて、つまり rb_define_method している行よりソースコード的に上でコメント付きのメソッド本体の関数の定義 (!= 宣言) する必要がある。

なので、

#include <ruby.h>

VALUE rb_cFoo;
static VALUE foo(VALUE self, VALUE x);

void Init_foo(void)
{
  rb_cFoo = rb_define_class("Foo", rb_cObject);
  rb_define_method(rb_cFoo, "foo", RUBY_METHOD_FUNC(foo), 1);
}

/* call-seq: foo(x)
 *
 * Foo method
 *
 * @param [Fixnum] x the argument
 * @return [Fixnum] the return value
 */
VALUE foo(VALUE self, VALUE x)
{
  return x;
}

みたいに書くと YARD のドキュメンテーションに失敗する。

#include <ruby.h>

VALUE rb_cFoo;

/* call-seq: foo(x)
 *
 * Foo method
 *
 * @param [Fixnum] x the argument
 * @return [Fixnum] the return value
 */
static VALUE foo(VALUE self, VALUE x)
{
  return x;
}

void Init_foo(void)
{
  rb_cFoo = rb_define_class("Foo", rb_cObject);
  rb_define_method(rb_cFoo, "foo", RUBY_METHOD_FUNC(foo), 1);
}

こう書くと成功する。

一方 RDoc では、どっちで書いてもちゃんとメソッドに対するドキュメントが生成される。

NFS をマウントしたままシャットダウンすると異常に時間がかかる問題

NFS をマウントしたままシャットダウンすると、電源が切れるまでに1分以上かかった。 しかし手動で NFS を umount し (これはすぐ終わる)、シャットダウンすると普通にスムーズにシャットダウンする。

ちょうど同じような症状で悩んでいる人が arch-general にいて、そこで解決しているのを見つけた https://mailman.archlinux.org/pipermail/arch-general/2013-January/032794.html 。 自分の環境でもこの方法で解決した。

原因は NFS を umount する前にネットワーク接続が切れていることだった。 これを解決するには、systemd に NFS にはネットワーク接続が必要であることを伝えればいい。 systemd はブート時に、/usr/lib/systemd/system-generators/systemd-fstab-generator で /etc/fstab から mount unit を生成する。 これは systemctl list-units で見ることができる。 systemctl list-units すると

mnt-nfs.mount       loaded active mounted /mnt/nfs

みたいな行があるので、ここからマウントポイントに対応する mount unit の名前がわかる。 あとは、これがネットワーク接続を必要としていることを設定すればいい。 自分も arch-general で回答している人のように dhcpcd@.service を使っていたので、

# mkdir /etc/systemd/system/mnt-nfs.mount.wants
# ln -s /usr/lib/systemd/system/dhcpcd@.service /etc/systemd/system/mnt-nfs.mount.wants/dhcpcd@eth0.service

とした。

Arch のパッケージのリポジトリを作る

Arch には公式のリポジトリとして core, extra, community, testing 等があるが、それ以外に非公式なものもいくつかある。 その中でも特に archlinuxfr は有名だと思う。linux-pae, linux-pf, repo-ck といったパッチをあてたカーネルのパッケージを提供しているリポジトリを利用している人もいるかもしれない。 そういった非公式リポジトリは wiki にまとまっている: Unofficial User Repositories - ArchWiki

自分も自分用に独自のリポジトリを作ってみたので、その手順を紹介する。 パッケージを作るための PKGBUILD は既に書いてあるとする。

ビルド環境の準備

まずビルド用のユーザ archbuild を作った。 ビルド用のツールは devtools というパッケージにまとまっているので、pacman -S devtools でインストールしておく。 archbuild は一部 root になる必要があるので、visudo で /usr/sbin/makechrootpkg を NOPASSWD で許可しておく。 必要に応じて /usr/bin/rsync も許可しておく。

パッケージを作るとき、パッケージを作っているマシンの環境に依存することは避けたい。 そのために、devtools の中に適当なディレクトリをルートとして Arch の環境を作ってそこに chroot してビルドするツールがある。 手順も wiki に書いてある: DeveloperWiki:Building in a Clean Chroot - ArchWiki

また、x86_64 な1台のマシンで i686 のパッケージも一緒に作りたい。 なので、自分は ~/chroot-x68_64 と ~/chroot-i686 の2つのディレクトリを用意し、それぞれに対して wiki の解説にあるように

sudo mkarchroot ~archbuild/chroot-x86_64/root base base-devel sudo
sudo setarch i686 mkarchroot ~archbuild/chroot-i686/root base base-devel sudo

とした。 そうしてから、それぞれの pacman.conf と makepkg.conf を編集した。 少なくとも makepkg.conf の PACKAGER は書いておくといいと思う。

ビルド

chroot してビルドするには makechrootpkg を使う。 ビルドの前に、chroot 環境のパッケージの状態を最新にするために

sudo arch-nspawn ~archbuild/chroot-x86_64/root pacman -Syu
sudo arch-nspawn ~archbuild/chroot-i686/root pacman -Syu

としておく。 そして PKGBUILD のあるディレクトリにいって

sudo makechrootpkg -c -u -r ~/chroot-x86_64/root
sudo setarch i686 makechrootpkg -c -u -r ~/chroot-i686/root

とすれば、カレントディレクトリに各アーキテクチャ用のパッケージができあがる。

公開

自分は HTTP サーバとして nginx を利用し、ディレクトリ構成は jaist のを真似た。 まず作ったパッケージはすべて ~/pool というディレクトリに置く。 そして、~/public/myrepo/os/{i686,x86_64} というディレクトリに、~/pool にあるパッケージへの symlink を置く。 nginx では root として ~/public を指定しておく。 最後に、repo-add を使ってパッケージをデータベースに登録する。 そうすると、利用者側は /etc/pacman.conf に

[myrepo]
Server = http://example.com/$repo/os/$arch

みたいに書けば利用できるようになる。

以上の公開手順をまとめて、以下のようなスクリプトを使っている。

PKG=$1
REPO=myrepo
POOL=$HOME/pool
PUBLIC_I686=$HOME/public/$REPO/os/i686
PUBLIC_X86_64=$HOME/public/$REPO/os/x86_64

i686() {
  ln -s $POOL/$1 $PUBLIC_I686
  repo-add $PUBLIC_I686/$REPO.db.tar.gz $POOL/$1
}

x86_64() {
  ln -s $POOL/$1 $PUBLIC_X86_64
  repo-add $PUBLIC_X86_64/$REPO.db.tar.gz $POOL/$1
}

cp $PKG $POOL
case $PKG in
  *-any.pkg.tar.xz)
    i686 $PKG
    x86_64 $PKG
    ;;
  *-i686.pkg.tar.xz)
    i686 $PKG
    ;;
  *-x86_64.pkg.tar.xz)
    x86_64 $PKG
    ;;
esac

補足

自分用にビルドするパッケージの中には、別の自分用にビルドしたパッケージに依存しているものもある。 なので、chroot 環境からもローカルリポジトリとして myrepo を参照したい。 HTTP で公開しているなら利用者側と同じように /etc/pacman.conf に追加してもいいし、 あるいはビルド前に

sudo rsync -avL ~/public/ ~/chroot-i686/root/extra/
sudo rsync -avL ~/public/ ~/chroot-x86_64/root/extra/

とかするようにして、~/chroot-{i686,x86_64}/root/etc/pacman.conf には

[myrepo]
Server = file:///extra/$repo/os/$arch

みたい書くようにしてもいいと思う。

テスト補助ライブラリとしての RoboGuice 2.0

そもそも DI というものを知らない自分にとって、Android のテストを書く上で RoboGuice を活用しようと思ったときに、 最初にどう使えばいいのかわからず苦労したので、そのときのまとめ的なもの。

目的

テストを書くときに、あるオブジェクトをテスト用のモックのようなもので置き換えたい。 例えばネットワークアクセスをするようなクラスがあったとき、テスト時には実際にネットワークアクセスするのではなく、ダミーのレスポンスを設定できるようなもので置き換えてテストしたり、 常に失敗するようなもので置き換えて異常系のテストをしたりしたい。

準備

roboguice-2.0.jarguice-3.0-no_aop.jarjavax.inject-1.jar をメインプロジェクトの libs というディレクトリに入れておく。

メインプロジェクト

こっちの書き方はググればいくつか解説記事や StackOverflow のページが見つかる。 RoboGuice 固有の話は

  • アクティビティは android.app.Activity ではなく roboguice.activity.RoboActivity を継承するようにする。
  • 使う Module はリソースとして書く。例えば res/values/roboguice.xmlroboguice_modules という string-array として Module の完全なクラス名を並べる。

あたりか。

それ以外の DI の話は Guice について調べるといいと思う。 Guice の wiki は絶対に読む必要がある。

テストプロジェクト

テスト時に inject するオブジェクトを変えるには、テスト時に Module を切り替えればいい。 例えばテスト時には FakeModule を使いたい場合、 ActivityInstrumentationTestCase2 を使ったテストケースでは、setUp() か各テストケースで getActivity() を呼ぶ前に

  Context ctx = getInstrumentation().getTargetContext();
  Application app = (Application) ctx.getApplicationContext();
  Module m = Modules.override(RoboGuice.newDefaultRoboModule(app)).with(new FakeModule());
  RoboGuice.setBaseApplicationInjector(app, RoboGuice.DEFAULT_STAGE, m);

というような操作を行うことで達成できる。

一方、tearDown() では

RoboGuice.util.reset();

としておく。

 

これでだいぶテストが書きやすくなった。 けどそもそも Android のテスト環境は (まだ) 中途半端なもののように見えて、テスト書くのがむずかしい……

コマンドラインからスクリーンロックを解除する

TouchUtils.clickView() とかで UI に触るようなテストを書いてみると、実行時に

java.lang.SecurityException: Injecting to another application requires INJECT_EVENTS permission

とか出て失敗することがあった。 ググってみるとスクリーンがロックされているのが原因らしく、実際スクリーンロックを解除してから実行するとちゃんと通った。

エミュレータを起動した直後は必ずロックされた状態になっていて、emulator-arm -help 見てもそれを解除する方法が見当らない。 emulator-arm -no-window で起動してすべてコマンドラインで完結させたかったので解決法を考えた結果、adb 経由で Android にキーイベントを送れることがわかったので、 スクリーンロックを解除するメニューキーを押すようなキーイベントを送る方法でいけた。

あとデバイスがオンラインになるまでブロックするのは adb wait-for-device でいけるけど、デバイスがオンラインになっている状態で adb shell input keyevent 82 しても Killed と言われたりすることもあって、 よくわからないけど Killed が出なくなるまで送り続けるようなスクリプトを書いて実行するようにしてる……

https://gist.github.com/3092913

Module#include, Object#extend の逆操作

include/extend したモジュールを後から取り外すことができたら便利そうだと思った。

RHG を読み返しながら、拡張ライブラリとして作ってみた。 クラスが管理している内部的な情報が r31627 から隠されてしまったようで、ruby/backward/classext.h をインクルードする必要があった。 ruby-trunk-changes r31623 - r31642 - PB memo

動作例は test.rb を参照。

ghcmod.vim 0.1.0

ghcmod.vim 0.1.0 をリリースした。 https://github.com/eagletmt/ghcmod-vim/downloads からダウンロードできます (リポジトリ内のファイルからプラグインとして利用する上で不必要なものを除いて zip で固めただけ)。

前回 からの変更点について書いていく。

:GhcModExpand コマンドを追加した

ghc-mod 1.10.8 で追加された ghc-mod expand のためのインターフェイス。 Template Haskell の展開の様子を quickfix に表示する。

ちなみに、ghc-mod expand と同等の出力は ghc -ddump-splices でコンパイルしても得られる。

GHC や hlint にオプションを渡せるようにした

ghc-mod は GHC や hlint を利用しており、それらにオプションを渡す機能が備わっている。 これらをそれぞれ g:ghcmod_ghc_optionsg:ghcmod_hlint_options でちゃんと設定できるようにした。

例えば :GhcModLint 時に "Redundant $" というヒントを出させないようにするためには以下のように ~/.vimrc で設定する。

let g:ghcmod_hlint_options = ['--ignore=Redundant $']

非同期チェックをサポートした

なぜ自動チェックが嫌いなのか - eagletmt's blog なんてことも書いたけど、 ブロックしない自動チェックならば便利だと思うので quickrun での非同期実行の実装を参考にして非同期的に :GhcModCheck:GhcModLint を実行できるようにした。 それぞれ :GhcModCheckAsync:GhcModLintAsync というコマンドを用意している。 さらに両方同時にチェックする :GhcModCheckAndLintAsync というコマンドも用意した。

バッファ保存時に自動的に実行するには ~/.vimrc で

autocmd BufWritePost *.hs GhcModCheckAsync

とか設定するといいと思う。

lint はともかく、check のほうは import しているモジュールが多かったりメタプログラミングをしていたりすると 結構時間が掛かるので、効果があるんじゃないかと思う。

ただし現状の非同期実行はちょっとロバストでない点があって、もしかしたらエラーを出してしまうかもしれない。 このへんは近いうちに改善したい…

Cabal を利用しているプロジェクトでの動作を改善した

Cabal はビルド時にやってくれていることがいくつかあって、それらに依存したプログラムのために 適切なオプションを GHC に渡すことで :GhcModCheck:GhcModType でエラーが出ないようにした。

具体的には、cabal build

  1. 依存パッケージに対してバージョンのテストを行うマクロを提供したり、
  2. 自身のパッケージに関する情報を提供する Paths_pkgname というモジュールを生成したり、
  3. 拡張子に応じて適切な前処理を実行したり

しているので、そのためのパスを設定したりしている。これら3点に関しては考慮しているつもりだけど、他にも何かあったりするのかな。

注意点としては、これらは cabal build 時に行われるので、一度 cabal build を実行して dist/build 以下にファイルを生成しておかないと意味がない。 1, 2 に関しては一度生成してしまえば後から変わることは無いので問題にはならないけど、 3 に関してはチェックの度に前処理が実行されて欲しいと思う。 単純な .cabal ファイルであれば https://gist.github.com/2227993 で前処理のみ実行できると思うけど、 Build-type: Simple ではない場合はどうするのがいいのかわからない。