As Sloth As Possible

可能な限りナマケモノでありたい


最近Safariが不安定になったりやたら重くなったりと愛機のMacBookが不調。なんか妙なものでもインストールしちゃったかなといろいろ見てみたけどこれと言って原因が掴めない。そんなわけでアクティビティモニタをしばらく眺めてたら、時々mixi stationのCPU使用率が50%を越えてることに気付いた。えええ。


メモリ食いつぶすことは結構あるけど、CPUをフルに使うのはコンパイルの最中と動画のエンコーディング中くらいだったのに…こいつが半分以上もリソース食ってるとは思わなかった。SafariやiTunesですら普通にしてると10%にもならないこと考えたらこれは重過ぎだよなぁ。Rosettaで動いてるわけでもないし。


最近mixiログインすらしてないし、大体mixi station入れててもあんまり見てる人いないしな。とりあえず消しておこう。




今日はちょっと別なことに興味を引かれたので、一旦Cocoa&RSpecでBDD大作戦のほうはお休み。

さて、Dashboad Widgetなら開発もCUI上で完結できるんじゃね?なんて言ってしまったのでTerminalから手動でバンドルをビルドする方法を探してみることにした、のはいいんだけど何だかえらい面倒なことになりそうだ。

バンドルの実態

OSX上で実行可能なモノとして扱われるアプリケーションバンドル(拡張子が.appのやつら)にしろ、ウィジェット(.wdgt)にしろ、実態は単なるディレクトリで、中に決まった構成でファイルを配置して特別な拡張子を付けたものにすぎない*1。フレームワーク(.framework)やローダブルバンドル(.bundle)も同様。その証拠にターミナルでcd /Application/Safari.appとかやるとSafariの中に入れるし、Finderからでも「パッケージの内容を表示」で中身を見ることができて、そこでいろいろいじくることでアイコンを変えたりローカライズしたりアプリケーションそのものの挙動を変えてしまったりも簡単にできる。

で、あるならばそれを自力で作ってしまえばいいわけで、rakeとかmakeとかで自動化してしまえばCUIでサクサク開発も夢じゃない。と、ここまでは知ってはいた。けども、実際何が必要でどんな手順でやればいいのかはわかんない。さて、どうしたもんかね。

とりあえず本職に聞いてみる

不可解なことでも実際誰かがやっていることであるはずなので、そいつの仕事ぶりを見て技を盗めばいい。つまり、Xcodeの様子を観察してみればいいのだ。Xcodeの仕事はと言えばまさにgccでソースをコンパイルして、バンドル用のディレクトリを作り、必要なファイルをそこにコピーすることに他ならないので、要は見様見真似でそれをやってのければいいのです。多分。

というわけで、早速Xcode上でCocoa Bundleプロジェクトを作成してそれをビルドさせてみる。動作としてはこんな感じだった。

  1. Test.bundle/Contentsを作ってそこにInfo.plistをぶち込む
  2. Test.bundle/Contents/Resourcesを作ってそこにローカライズ可能なリソースたち(デフォルトのプロジェクトだとEnglish.lprojにInfoPlist.stringsがあるだけ)をぶち込む
  3. プリコンパイル済みプレフィックスヘッダとやらを生成、または処理する
    1. '-arch ppc'で
    2. '-arch i386'で
  4. ソースをコンパイル
    1. '-arch ppc'で
    2. '-arch i386'で
  5. リンク
    1. ppc用のやつをppc用のフォルダに入れとく
    2. i386用のやつをi386用のフォルダに入れとく
  6. ppc用バイナリとi386用バイナリをくっつけてUniversal Binaryを生成、Test.bundle/Contents/MacOSにぶち込む

これで出来上がり。うん、大体予想してた通りだ。このうちプリコンパイル済みヘッダとやらは多分なくてもいい。あと最初のInfo.plist、com.apple.tools.info-plist-utilityとかいうので処理してるんだけど、それがどこにある何のことなのかよくわからない。出来たバンドルの中見たけど単に元のInfo.plistのコピーが入ってるだけに見えるんだけどな。

で、それぞれの段階で二回ずつ処理してるのがちょっと面白い。Universal Binaryって本当に二つのアーキテクチャ用にコンパイルしたバイナリを一纏めにしてるだけなのか。ふぅん。何だか面倒なので差しあたって手を出したくないところなんだけど、大した機能があるわけでもないのにIntel Mac専用なウィジェットとか作ったところで面白くもないので、これもちゃんと覚えておこう。多分同じことを二回やってくっつけるだけだ。きっと。おそらく。

ということで、こんなRakefileを作ってみた。Rakefileの書きかたちゃんと調べてないけどまぁ失敗したら後で直せばいいか。(こら)

task :default => :build
desc 'Build loadable bundle'
task :build => ['Hello'] do |t|
  bundle_name = 'build/Hello.bundle'
  Dir.mkdir bundle_name
  Dir.mkdir "#{bundle_name}/Contents"
  Dir.mkdir "#{bundle_name}/Contents/Resources"
  Dir.mkdir "#{bundle_name}/Contents/MacOS"
  sh "cp Info.plist #{bundle_name}/Contents"
  sh "cp -R English.lproj #{bundle_name}/Contents/Resources"
  sh "cp Hello #{bundle_name}/Contents/MacOS"
end

file 'Hello' => ['Hello.ppc', 'Hello.i386'] do |t|
  sh "/usr/bin/lipo -create #{t.prerequisites} -output #{t}"
end

file 'Hello.ppc' => ['Hello.ppc.o'] do |t|
  sh "/usr/bin/gcc-4.0 -o #{t} -framework Cocoa -arch ppc -filelist #{t}.LinkFileList -bundle -mmacosx-version-min=10.4"
end

file 'Hello.i386' => ['Hello.i386.o'] do |t|
  sh "/usr/bin/gcc-4.0 -o #{t} -framework Cocoa -arch i386 -filelist #{t}.LinkFileList -bundle -mmacosx-version-min=10.4"
end

file 'Hello.ppc.o' => ['Hello.m'] do |t|
  sh "/usr/bin/gcc-4.0 -x objective-c -arch ppc -o #{t} -c #{t.prerequisites}"
  File.open('Hello.ppc.LinkFileList', 'w') { |f| f.write('Hello.ppc.o') }
end

file 'Hello.i386.o' => ['Hello.m'] do |t|
  sh "/usr/bin/gcc-4.0 -x objective-c -arch i386 -o #{t} -c #{t.prerequisites}"
  File.open('Hello.i386.LinkFileList', 'w') { |f| f.write('Hello.i386.o') }
end

お。ちゃんとビルドできた。class-dumpしてみる。

$ class-dump build/Hello.bundle/Contents/MacOS/Hello
/*
 *     Generated by class-dump 3.1.1.
 *
 *     class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2006 by Steve Nygard.
 */
This file does not contain any Objective-C runtime information.

クラスがない。思いっきり失敗しとるやんけ。 orz

念のため普通にXcodeでビルドしたやつもclass-dumpしてみるとちゃんとクラスの情報取れる。他のCocoaアプリからロードしてみても、自分で構成したやつはprincipalClassがnilになる。…うー。よくわからないまま適当にコンパイラのオプション省略するからいけないんだよね…。もうちょいいじってみよう。

追記

ちゃんとビルドできました。リンクのタイミングでXcodeは-filelistってのを使ってるんだけど、それやめて引数でファイル名与えたら通った。ってかやっぱり上のRakefile手順間違ってる?

*1:と、書いてあるっぽい。多分。

BDDでCocoa開発をするのにRSpecを使ってみるの続き。

なんだか意外と簡単に使えそうだったのでこんどはCocoa組込みじゃないフレームワークで試してみよう、ということでOgreKit。こちらは事前にコンパイルして/Library/Frameworks以下に配置済。と、ここでそもそもどうやってフレームワークを呼び出すのかわからなくて一思案。本家サイトでなんか書いてないかなーと探してみるも、どこに書いてあるんだかわからん。ActiveRecordSupportとか興味深いことが書いてあるのも見つけたけどとりあえず後回し。

サンプルのソースを見てたらOSX.require_frameworkを使うらしいというのはわかったんだけど、読み込んだフレームワークのクラスがどこにあるのかわからずしばらく悩んだ。OgreKit::OGRegularExpressionとかやってみてそんなもん無いと怒られたり。冷静に考えれば、Objective-CだったらRubyみたいにモジュールがネストしてないのでOSX::OGRegularExpressionがでよかったんだけど、それを見つけるまでGoogle先生を質問攻めにしてしまった。さてそんなわけでできたのがこんな感じ。

require 'osx/cocoa'
include OSX
require_framework 'OgreKit'

describe OGRegularExpression do
  before :each do
    @match_string   = 'Ich liebe dich, aber du liebst ihn. Ich liebe dich...'
    @unmatch_string = 'Ich mag nicht nur Ruby, sondern Objective-C.'
    @expression     = 'dich'
    @ogregex        = OGRegularExpression.regularExpressionWithString(@expression)
  end
  
  before :all do
    @result         = []
  end
  
  after :all do
    p @result
  end 
  
  it "should replace matches" do
    # Rubyのメソッド呼び出しっぽいスタイル
    replaced = @ogregex.replaceAllMatchesInString_withString(@match_string, 'mich')
    replaced.to_s.should_not include(@expression)
    @result << replaced.to_s 
  end
  
  it 'should replace nothing when no matches in string' do
    # こっちはObjective-Cのメッセージパッシングっぽいスタイル
    not_replaced = @ogregex.objc_send(:replaceAllMatchesInString, @unmatch_string, :withString, 'mich')
    not_replaced.to_s.should eql(@unmatch_string)
    @result << not_replaced.to_s
  end
end
$ spec ogrekit_spec.rb
..["Ich liebe mich, aber du liebst ihn. Ich liebe mich...", "Ich mag nicht nur Ruby, sondern Objective-C."]


Finished in 0.018822 seconds

2 examples, 0 failures

でけたでけた。変換の例に使ってる文章が何なのかは気にしない方向で。OgreKitのSpecなのに正規表現使ってないのも気にしたら負けかなと思ってる。で、OgreKitは既に有効なロードパスの中にあったのでフレームワーク名で呼び出せたけど、OSX#require_frameworkは引数にフレームワークのフルパスも与えられるので、フレームワーク化してしまえば自分で作った任意のObjective-Cクラスも簡単に扱える。まだ試してないけどバンドルをロードする機能もある。思ったんだけど、gemのパッケージを自動生成するやつみたいにwigetのファイル構成とかビルドするRakefileとかのジェネレータを作ったら便利かもしれない。wdigetならインターフェースもバンドルの開発もCUI上で完結できるし、RSpecで確認しながら作るスタイルは向いてるかも。Dashcodeが出ようってご時世に何を言ってるんだか…でもvimのが楽なんだもん、Xcodeでいろいろやるより。

あと、初めてRubyCocoaを見たときにも思ったんだけど、Objective-Cはメソッド名が長いなぁやっぱり。CamelCaseもRubyの中にあるとどうしても違和感が拭えない。慣れるっきゃないかな。Objective-Cで書いてるとそんなに気にならないんだけど。何でだろ。

さて、なんだかRubyCocoaを触ってみよう、なノリになってきたけど、目的はRSpecでもRubyCocoaでもなくて「CocoaでBDD」。これをどうにかしてCocoaアプリの開発に取り入れてみたいと思う。いちいちアプリをフレームワーク化してビルドしてterminalからspec実行して、とかやりはじめたら本末転倒。Xcodeで上手く自動実行させる方法を考えよう、と。

前々からCocoaで開発してるときにTDDがいまひとつやりにくいのが気になってた。Xcodeにも組み込まれててデファクトになってるOCUnit、あれが結構使いにくい。単に使いかたを理解してないだけだという気もするけど、それを差し引いてもTDDのツールとしてはそんなに洗練されてないと思う。実行結果が視覚的に面白くないし(これはモチベーション下るので結構重要)、エラーの通知もあんまり解析しやすくない。そういえばアサーションの実態がCのマクロなのでObjective-Cの文法じゃないのも気になるし、あんまり柔軟じゃない。他にもいろいろな実装はあるらしいけど、ただでさえ情報の少ないCocoaのそれもTDDのしかも代替フレームワークともなると、日本語で読める最新の詳細な情報など皆無。ここでぬぬぬぬ、となるわけですよ。

なきゃ作れという話

ということで、最初は自分で作ることを試みた。もっとわかりやすいのがいいし、せっかくObjective-Cみたいな動的な言語なんだからリフレクションとか使って柔軟にやって欲しいし、ああどうせならBDDっぽい語彙で書きたいよなー、とか妄想を膨らます。イメージとしては

- (void) before {
  // 俺を作る
  faul = [[Namakemono alloc] initWithName:@"faultier"];
}

- (void) after {
  // 俺を逃がしてやる
  [faul release];
}

- (void) whenIntroduce {
  [faul shouldKindOf:Animal];
  [faul shouldKindOf:Namakemono];
  [[faul name] shouldEqual:@"faultier"];
  [[faul capitarizedName] shouldEqual:@"Faultier"];
}

- (void) whenStudy {
  // 来週はテストだ
  id study = [[Task alloc] initWithName:@"English Study"];
  [faul setTask:study key:@"study"];

  [[faul task:@"study"] shuldEqual:study];
  [faul shouldThrow:NullYarukiException message:@selector(reportProgress:) arguments:@"study"];

  // studyタスクを開放し忘れるとテスト直前にメモリリークしてることに気付く…
  [study release];
}

こんな感じ。あとモックも欲しいなぁ、CUI版とXcode組み込み版のRunnerはそれぞれいるよなぁ、とか言ってたら手に負えなくなってきた。あは。

じゃあRubyで書けばいいんじゃね?

RubyにはRSpecという素敵な代物が既にある。スクリプトなら拡張も容易だし、Rubyの表現力なら好き放題書ける。RubyCocoaはLeopardでは標準で入るらしいから環境整える手間もいらないし、それを使えばサクっとBDDできるんじゃない?と思い立ってやってみることにした。

まずはCocoaのクラスのSpecをどんなふうに書けばいいのか試してみる。

require 'osx/cocoa'
include OSX

describe NSString do
  before :each do
    @expected_string = 'Cocoa BDD with RSpec'
    @str = NSString.stringWithString(@expected_string)
  end

  it 'should be allocated from String' do
    @str.should be_kind_of(NSString)
    @str.to_s.should be_eql(@expected_string)
  end

  it 'should not be editable' do
    @str.to_s.should be_eql(@expected_string)
    lambda { @str << 'append' }.should raise_error(OCException)
  end

  it 'should be editable' do # わざと間違えてみる
   @str << 'append'
   @str.to_s.should be_eql("#{@expected_string}append")
  end
end

実行結果

$ spec nsstring_spec.rb
..F

1)
OSX::OCException in 'OSX::NSString should be editable'
NSInvalidArgumentException - Attempt to mutate immutable object with setString:
/Library/Frameworks/RubyCocoa.framework/Versions/A/Resources/ruby/osx/objc/oc_attachments.rb:57:in `setString'
/Library/Frameworks/RubyCocoa.framework/Versions/A/Resources/ruby/osx/objc/oc_attachments.rb:57:in `method_missing'
./nsstring_spec.rb:20:

Finished in 0.013094 seconds

3 examples, 1 failure

普通だ。なんらの違和感もない。これはまごうことなきRubyだ。素敵素敵。

あとは自作のクラスを検証するやりかたと、Cocoaアプリの開発サイクルにどう組込むかだねー。あとでやります。


ふぅー。言われたこっちが恐縮してしまうほどありがたい言葉をもらって色々と考えてしまった。


正直なところ、今のバイト先は非常に居心地がいい。仕事はそれなりに難しくてそれなりにやりがいがある。一緒にやってるメンバーはいい人ばかりだし、勝手に師と仰いでる、なんで俺なんかがこんな人と出会えてしまったんだろうというぐらい凄い人もいる。学生の身分にしては結構なお金を貰った上、結構好きにやらせてもらってる。煩わしい就活なんかも経験しないで済んでいる。親父や友人の話を聞く限りだと、多分これは割と得難い幸運なんだろうなと思う。


だからと言って今のままでいてはいけないことも自分でも薄々気付いてた。今の自分はまだ、贔屓目に見ても能力的にはせいぜい並のプログラマだし、経験や実績が足りないんだからまだまだ全然未熟だ。早くから自分の能力を発揮する場を与えられたことは幸運でもあるけど、本当なら「溜め」のフェーズであってもいい時間を自分から手放しているわけだし。今のままでもおだてられれば木ぐらいは登れるし、日々鍛錬を怠らなければそのうち屋久杉みたいな大木にも登るかもね。だけど、まぁ空を飛ぶのは無理でしょう。じゃあ溜めをたっぷり作れば空を飛べるんかというと、そんな保証はない。だから心のどこかでそれでもいいかなと甘えてたのは否めない。


ああ、だから一旦ここを離れてしっかり鍛えてもらってからにすればいいんじゃないなんて指摘は、まったくもって正しくて反論の余地もない。おまけにそれを、俺に対しての期待を込めて言ってくれてるんだから、聞き流しようがないじゃないか。


とりあえずは外を見てみよう。話はそれから。幸いなことに、俺の不精と上司の不精が連鎖して(笑)、今の会社とは口約束以上の話は何もしてない。ここで頑張るにせよ他で頑張るにせよ、選択肢は沢山あっても今のところ損にはならない、か。




↑このページのトップヘ