そうだ Git、移行 (Reposurgeon編)

今年の春に、それまでプロジェクトで長年つかってきたSubversionをGitに移行した。比較的少人数での開発だしリモートで作業している人もいないし、分散SCMであるメリットは実はそれほどないのだけれど。やっぱり技術者として世の中の流れについて行きたいというのもあるし、リリース管理者としてリリースの度に実施するSubversionでのマージ処理が辛くて辛くて(追加/削除がディレクトリ操作になるので、ディレクトリ配下の一部だけをマージしたい時に余計なものまで一緒にマージされてしまって自動化の妨げになる)何とかしたかったのもある。

まぁ、正直なところSubversionもとても良くできた、そして何より実績が多いソフトウェアでなので、別にGitがなくても死にはしない。今Subversionでうまく回っているなら、急いで乗り換える必要は多分、ない。

だけど、これだけGithubがメジャーになって「OSSの管理といえばGit」っていう流れができあがってしまうと、個人的にはSubversionってもう、「移行するかべきか否か」ではなくて「いつ移行するか」を検討するくらいの時期には来ているとも思うわけで。十数年前にCVSとかVSSとかが主流だったSIerも、最近では大半が少なくともSubversionくらいには移行しているわけで、この先10年とかを考えると、SIerだからGitなんかいらないとかいうことはないのではないかと信じている。実際、最近書店にはデザイナーさん向けのGitの書籍が並んでいるくらい、Gitの認知度が上がっている。

そして何より、プロジェクトに新しく若者が入ってきたときに、Subversionという古の構成管理ツールの使い方を教えてあげるとかいうのは、お互い得るものが少なすぎて悲惨である。どうせ同じ時間を使うなら、その場限りのスキルなんかよりはいろいろなところで役に立つスキルを身につけることに時間を使いたい。まぁ、みんながみんなそういう気持ちではないかもしれないのだけど。

移行ツール

まぁ本音は「実は俺がGit使いたいだけ」でそこまで深い意味はなかったかもしれないけど、とにかくGitへの移行方法の話。かつてCVSからSubversionへの移行がスクリプトでサポートされていたように、Gitにも移行ツールがついている。それがGit-SVNというサブコマンドである。

Git-SVN

Git-SVNは事実上Subversionから移行する人のほとんどが一度は通る道だと思う。Git-SVNは移行ツールの一種ではあるのだけど、実際の所は「GitでSubversionのRepositoryにアクセスすることができる」というもので、GitへSubversionから取り込むという流れはもちろんのこと、逆にGit側からSubversionへ変更を送ることもできる。

Git-SVNについては↓こちらで@DQNEOさんが大変きれいにまとめられているので、詳しくはそちらを参照されたい。これは移行の時にじっくり読ませて頂いて、本当に参考になった。Git-SVNを使うかどうかを別としても、事前にSubversionのリポジトリをきれいにしておくとかサイズを小さくするとか、既存のコード資産を移行する人にとっては有益な情報が山ほど詰まっている。

仕事で使ってる巨大SVNレポジトリをGithubに移管するためにやったことまとめ

自分のプロジェクトでも最初はこれでいく想定で進めていた。でも、どうもGit-SVNの変換処理が結構遅いとか、できあがったGit Repositoryでテストしていたら、追加でコマンドをたたかないとSubversionにあったはずのブランチがみえないとか、あと(作業手順が漏れていただけなんだけど)そもそも移行されていないブランチがあったり、移行元が初期はtrunk/branches/tags構造じゃなかったのでそこが移行されずに落ちているとか、いろいろ課題とか問題が見えてきた。

それでほかにもう少しいい移行方法はないものか、と少しまじめに調べてみることにしたのだけど、その時に見つけたのが、ESRことEric S. Raymond大先生のRepository移行ツール、Reposurgeon

Git-SVNは言ってみれば双方向でGitとSubversionをつなぐツール。Subversionから一気にGit移行するのにも使えるけど、一部のメンバーだけ、あるいは一部のプロジェクトだけ、など段階的に移行することもできる。その気になれば移行以外の目的でも使える、案外汎用性の高いツール。

構成管理ツールを変更するときというのは、チームの規模が大きければ大きいほど混乱が伴うもの。だから一部のメンバーだけを段階的にGitに移行でき、しかも移行中もSubversion-Git双方向でお互いの変更が双方向に共有できるというメリットは非常に大きい。

Reposurgeon

対してReposurgeonはRepository編集を行うためのツールである。正規のツールが提供する通常のオペレーションでは絶対に実現できないような変更を履歴に対して加えることもできる。まさにRepositoryに対する外科手術。これもGit-SVNとは別の意味で、移行以外の目的で使えるツールだ。

Reposurgeonでの移行はRepositoryを一気に移行してしまう。そのため、Git-SVNのように同時並行稼働で双方向にいったりきたりすることはできず、Subversionから段階的にGitに移行する、という計画では基本的に使うことができない。ここがツール選択のポイントになるケースも多いはず。

その代わりに、より移行に適した形で、より高速に移行を済ませてくれる(と思う)。全員が一度に移行するなら個人的にはこちらのが良いのではないかと思う。ツール自体が難しく情報量もGit-SVNと比較するとかなり限られるので、自分で調べて問題を解決できる人向きではあるけど。

何れにせよ、絶対的にどちらのツールが優れているとかそういう類のものではない。リポジトリの分割方針とか利用者の数やスキルを考慮して適切なツールを選びたいところ。

Reposurgeonのメリット

  • 移行機能は基本的に一括移行
  • 追加手順なく、最初から意図した形の移行結果になりやすい
  • 通常のツールではできない、履歴に対する各種変換処理が可能

Reposurgeonのデメリット

  • Unix系プラットフォームのみサポート
  • Subversionとの相互運用不可
  • 情報が少ない、コマンドや作業フローがわかりづらい

ReposurgeonはUnix系のツールなので、Windowsでは動かない。ここは注意が必要。でも、WindowsでホストしているSubversionのリポジトリをMac OS XにもってきてGitに移行し、完成したGitリポジトリをWindowsに戻してWindowsのGitで運用する、といったことは問題なく行えるので、実際にはそれほど大きな制約ともいえない。実際、自分はOSXで移行したリポジトリをWindowsのGitで動かしてもうそろそろ1年だけど、何も問題になっていない。

Reposurgeonの導入

自分はOSXでしか試していないんだけど、きっと大抵のパッケージマネージャでサポートされていると思う。Homebrewがはいっていれば

brew install reposurgeon

これだけ。

conversion.mkによる変換

Reposurgeonのコマンドは最初は取っつきづらいと思う。そのため(かどうかは知らないけど)、SubversionからGitに移すだけ、を簡単に実現してくれるmakeファイルが配られている。

http://www.catb.org/~esr/reposurgeon/conversion.mk

makeファイルをダウンロードしたらエディタで開いて以下の2行を変更する。PROJECTは最終的に作られるGit Repositoryの名前になるけど、まぁ多分このMakefileで作成したリポジトリをそのまま使うことはないんじゃないかと思うので、あまり気にしなくていいと思う。SVN_URLはデータを取ってくる場所になるのでちゃんと書かないとダメ。

PROJECT = code-repo
SVN_URL = http://192.168.1.100/svn/code-repo

これでmake -f conversion.mkすれば、勝手に変換が行われてcode-repo-gitというディレクトリにgitのRepositoryが完成。

ちなみにGitに移行したときにリビジョンが数字からSHA1ハッシュに変わるのだけど、Subversionのリビジョン番号に依存する他のツールと連携しているような場合、Subversion側でのリビジョン番号を記録として残すことができる。この場合makefileを少し編集する必要がある。

44: # Build the second-stage fast-import stream from the first-stage stream dump
45: $(PROJECT).fi: $(PROJECT).$(SOURCE_VCS) $(PROJECT).lift $(PROJECT).map $(EXTRAS)
46:    $(REPOSURGEON) $(VERBOSITY) "read <$(PROJECT).$(SOURCE_VCS)" "authors read <$(PROJECT).map" "prefer git" "script $(PROJECT).lift" "fossils write >$(PROJECT).fo" "write >$(PROJECT).fi"

これを、

44: # Build the second-stage fast-import stream from the first-stage stream dump
45: $(PROJECT).fi: $(PROJECT).$(SOURCE_VCS) $(PROJECT).lift $(PROJECT).map $(EXTRAS)
46:     $(REPOSURGEON) $(VERBOSITY) "read <$(PROJECT).$(SOURCE_VCS)" "authors read <$(PROJECT).map" "prefer git" "script $(PROJECT).lift" "fossils write >$(PROJECT).fo" "write --fossilize >$(PROJECT).fi"

こんな感じにする。最後のwriteコマンドに–fossilizeオプションをつけるのがポイントだ。RedmineのようなIssue管理システムとRevision番号で連携を取っているような場合は、Gitのリビジョン情報で過去の関連づけを上書きできると理想的だけど、その材料となる大事な情報となる。

インタラクティブなコマンド実行による変換

conversion.mkによる変換はなにもわからない状態でも2、3行編集するだけでさくっとSubversionからGitに変換できてしまう手軽さがポイントで、できあがったGitリポジトリを見ていると結構ちゃんとしていて感激する。

だけど、変換処理について細かいカスタマイズをしようと思うと、結局最後はmake fileの中で呼び出されているコマンドを調べることになる。Makefileを読むのって結構面倒くさいのだけど、このツールについて言えば、説明を読むよりもまずはこれを見てみるのが効果的だと思う。いきなりオフィシャルの説明を読んだところで、普通の人は動かせないと思うので。

一連の流れは次のセクションを見てもらうとして、Reposurgeonを使うには基本形を知っておく必要がある。

  1. read <[ファイル]
    Subversionのダンプや、Reposurgeonや各種Version Control System (VCS)からwriteで書き出したfast-import形式のファイルを読み込む。ここで読み込んだファイルに対して、各種コマンドを実行してリポジトリの内容を編集していく。基本的にはReposurgeonを起動して一番最初に呼ぶコマンドになるはず。
  2. write >[ファイル]
    編集した内容を、exportする。自分は編集作業後のバックアップとしてしか使っていないけど、編集したリポジトリを他のfast-importできるツールにインポートする場合はその元ネタになる。
  3. rebuild [リポジトリ]
    編集後のリポジトリを実際にVCSで使えるリポジトリとして再構築する。

基本形はこの、read→編集→rebuildもしくはwriteという流れで、readからrebuildの間には好きなだけ編集コマンドを挟めばいい。例えば正規表現にマッチするファイルを含むリビジョンを探して、該当ファイルだけ削除してしまうとか、不要なタグを消し去る(削除コミットができるのではなく、履歴のどこにも登場しなくなる)とか。

コマンド実行する場合には、Reposurgeonを起動してreposurgeon>プロンプトにする。SubversionからGitに移行するには、以下のような流れでコマンドをたたいていけばよい。

  1. readコマンドを使って、Subversionのdumpファイル(code-repo.dmp)からリポジトリ情報を読み込む。dumpというのはもちろんsvnadmin dumpで作成したもの。ちなみにdumpと一言でいってもSubversionのダンプにはバージョンがあり、バージョンが高すぎるとreposurgeonが読み込めないときがある。–deltaとか特殊なオプションをつけるとバージョン指定が厳しくなったりするので、Reposurgeonに食わせる場合はオプションをつけずに出力するのが良いと思う。
     reposurgeon> read <code-repo.dmp

    ちなみにreposurgeonのリダイレクトはシェルのリダイレクトとは異なり、記号とファイル名との間にスペースをいれると動かない。必ず<とか>とファイル名を空白で区切らずに続けて書く。

  2. author map ファイルを読み込む (subversionのuser idをgit user name + emailにマッピングするファイル、git-svnで使うのと同じものがそのまま使える)
    reposurgeon> authors read <users.map
  3. 移行先VCSをgitに指定。
    reposurgeon> prefer git
  4. Subversionリビジョンと日付/コミットした人の一覧を出力(optional)
    reposurgeon> fossils write >dev.fo
  5. Repository名のリネーム(これもoptionalかな?実はどこに効いているのかよくわかっていない)
    reposurgeon> rename code-repo2
  6. Subversionからgitに間違ってtagとして認識されてしまうゴミタグの削除
    reposurgeon> =T & /(emptycommit-.*|tipdelete-.*|.*-root$)/n delete
  7. ここまで編集した内容でgit-fast-import用のファイル(dev.fi)を書き出す
    reposurgeon> write --fossilize >code-repo.fi
  8. とりあえずここまでで準備完了。あとのRepository作成はこんな感じ。
    $ reposurgeon
    reposurgeon> read <code-repo.fi
    reposurgeon> rebuild code-repo2
  9. ここまでの手順で作業コピー付きのGitリポジトリが作成される最後にbareリポジトリ作ってpush
    $ cd code-repo2
    $ git remote add origin ../code-repo2.git
    $ git push --all origin
    $ git push --tags origin
    $ git init --bare code-repo2.git
  10.  git gcでとどめの圧縮をしておく。
    $ git gc --aggressive
    Counting objects: 104054, done.
    Delta compression using up to 4 threads.
    Compressing objects: 100% (91278/91278), done.
    Writing objects: 100% (104054/104054), done.
    Total 104054 (delta 60931), reused 32576 (delta 0)

自分が作業しているとき、あまりReposurgeonの記事見かけなかったので、今後移行する人の参考になればということで記録を残しておく。自分もそれほど深く理解できていないので説明なんか書くのもどうかと思うけど、何もないよりいいだろう、と。

 

ゆうパック種類番号 75:上記以外

 

これが来ると再配達を依頼するわけだけど、いつも思うことがある。この紙を書いて投函して、受け取ったひとはそれを読んで再配達を依頼して、っていう全体のプロセスをちゃんとテストしてるのかなーって。

2014-01-08-00.36.23

 

再配達を申し込もうと日本郵便のページにいって入力画面見ると、こうなっている。

japanpost01

 

 

まずなんというか、「上記以外」が多すぎる。自分の郵便物が上記以外だったとき、どの「上記以外」が選択されても文句はいえないよね、これ。それできっとさっきの75って番号がヒントになるのではないかと探すわけです、先の種類番号。

でも、どこにも出てこない。75番、どこなのー。見当たらないんですけどー。

 

見るのではない。ソースを感じるのだ

 

誰かがそんなことを言ったかどうかは定かではないが、右クリックしてInspect ElementとかDeveloper Toolとか呼び出してソースを参照すると、ようやく<input type radio … value = “75”>を発見。そこに書いてあったか、むぅ、奥が深い(…というか回りくどいというか…)

japanpost02

ゆうパックとかの不在連絡票を受け取ったら、ソースを感じてください。きっと正しい種類番号を見つけることができるでしょう。

 

Oracle Instant ClientのSQL*PlusをMacで使う

Instantとかいうくらいだからアーカイブ展開したら即使えるとかなのかと思ったら、異常に面倒くさかった。

まずはOTNから適当に以下をダウンロードしてくる。SDKとかはいらない。

  • instantclient-basic-macos.x64-11.2.0.3.0.zip
  • instantclient-sqlplus-macos.x64-11.2.0.3.0.zip

これらを適当なディレクトリに展開する。自分は~/oracleとかつくってそこにばらまいた。

[~/oracle] ls -al
total 382920
drwxr-xr-x@ 20 gm7add9  staff        680 11 26 11:40 .
drwxr-xr-x+ 56 gm7add9  staff       1904 11 26 11:13 ..
-rw-r--r--@  1 gm7add9  staff       6148 11 26 11:40 .DS_Store
-rw-rw-rw-@  1 gm7add9  staff        484  2 15  2013 BASIC_README
-rw-rw-rw-@  1 gm7add9  staff        488  2 15  2013 SQLPLUS_README
-rwxrwxrwx@  1 gm7add9  staff      10252  2 15  2013 adrci
-rwxrwxrwx@  1 gm7add9  staff      40768  2 15  2013 genezi
-r--r--r--@  1 gm7add9  staff        368  4 10  2011 glogin.sql
-rwxrwxrwx@  1 gm7add9  staff   64771444 12 19  2012 libclntsh.dylib.11.1
-rwxrwxrwx@  1 gm7add9  staff    2808528  2 15  2013 libnnz11.dylib
-rwxrwxrwx@  1 gm7add9  staff    1897664 12 19  2012 libocci.dylib.11.1
-rwxrwxrwx@  1 gm7add9  staff  118379468  2 15  2013 libociei.dylib
-rwxrwxrwx@  1 gm7add9  staff     158924 12 19  2012 libocijdbc11.dylib
-rwxrwxrwx@  1 gm7add9  staff    1385860  2 12  2013 libsqlplus.dylib
-rwxrwxrwx@  1 gm7add9  staff    1504252  2 12  2013 libsqlplusic.dylib
-rw-rw-rw-@  1 gm7add9  staff    2095661  5 29  2012 ojdbc5.jar
-rw-rw-rw-@  1 gm7add9  staff    2714016  5 29  2012 ojdbc6.jar
-rwxr-xr-x@  1 gm7add9  staff       8744 12 19  2012 sqlplus
-rwxrwxrwx@  1 gm7add9  staff     162324  2 15  2013 uidrvci
-rw-rw-rw-@  1 gm7add9  staff      66779 12 19  2012 xstreams.jar

いざ、実行。

[~/oracle] sqlplus
dyld: Library not loaded: /ade/b/2649109290/oracle/sqlplus/lib/libsqlplus.dylib
Referenced from: /Users/gm7add9/oracle/sqlplus
Reason: image not found
zsh: trace trap sqlplus

はい、エラー。
/ade/b…とか変なパスでリンクしてんじゃねー。

全部install_name_toolで書き換え。

install_name_tool -change /ade/b/2649109290/oracle/sqlplus/lib/libsqlplus.dylib @executable_path/libsqlplus.dylib sqlplus
install_name_tool -change /ade/b/2649109290/oracle/rdbms/lib/libclntsh.dylib.11.1 @executable_path/libclntsh.dylib.11.1 sqlplus
install_name_tool -change /ade/b/2649109290/oracle/ldap/lib/libnnz11.dylib @executable_path/libnnz11.dylib sqlplus
install_name_tool -change /ade/b/1891624078/oracle/sqlplus/lib/libsqlplus.dylib @executable_path/libsqlplus.dylib sqlplus
install_name_tool -change /ade/b/2649109290/oracle/rdbms/lib/libclntsh.dylib.11.1 @loader_path/libclntsh.dylib.11.1 libsqlplus.dylib
install_name_tool -change /ade/b/2649109290/oracle/ldap/lib/libnnz11.dylib @loader_path/libnnz11.dylib libsqlplus.dylib
install_name_tool -change /ade/b/2649109290/oracle/ldap/lib/libnnz11.dylib @loader_path/libnnz11.dylib libclntsh.dylib.11.1
install_name_tool -change /ade/b/2649109290/oracle/rdbms/lib/libclntsh.dylib.11.1 @loader_path/libclntsh.dylib.11.1 libsqlplusic.dylib
install_name_tool -change /ade/b/2649109290/oracle/ldap/lib/libnnz11.dylib @loader_path/libnnz11.dylib libsqlplusic.dylib
install_name_tool -change /ade/b/2649109290/oracle/rdbms/lib/libclntsh.dylib.11.1 @loader_path/libclntsh.dylib.11.1 libociei.dylib

再実行。

[~/oracle] sqlplus                                                                                     [gm7add9@vivace]

SQL*Plus: Release 11.2.0.3.0 Production on 火 11月 26 11:49:30 2013

Copyright (c) 1982, 2012, Oracle.  All rights reserved.

ユーザー名を入力してください:

やっとうごいた。みんなこんな面倒くさいことしてんのだろうか…

Reading JAR/Zip Archive Comment (JDK1.6)

前回の話で、Pack200で圧縮するとJARファイルのファイルサイズがかなり小さくなり、JNLPのパフォーマンス向上に大きく寄与することがわかった。しかし、この高い圧縮率は

pack → unpack → jarsigner  →pack

という誰がどう見ても非効率なプリプロセスを前提としており、ネットワーク上の転送時間は高速化されるものの、その代償としての圧縮処理のパフォーマンス悪化があまりにも大きすぎる。特に最初のノーマライズをなんとかしたい。

もちろん、普通は配布前に一度だけオフラインバッチで流しておけばそれで問題ない。プリプロセスに少し手間をかけることで、本番でのネットワーク転送を高速化する、ここがPack200の本来の目的だろう。でも自分の場合もとはといえば、jarsignerの署名をオンデマンドで実行するアプリの高速化が目的で、テスト環境への配布がメインであるためリリースも多い。jarsignerまでオフラインに戻してしまうと、また署名忘れやバージョン不一致が起こりかねない。

かといって、この無駄に長い圧縮処理をそのままオンデマンドの処理に詰め込むとパフォーマンスが悪すぎて使い物にならない。

JAR.isNormalized?

そこで一つ考えたのが、最初のnormalizeを省略すること。文字通り丸っと省略するとさすがに署名検証エラーになるので、当然のことながら条件付きである。仮に、署名対象のJARが最初からnormalizeされているものであるとしたら、わざわざ再度pack/unpackするのは無駄以外の何ものでもない。だからJARファイルを見てnormalizeされているかどうかを事前に知ることができれば、最初のpack→unpackのステップを丸ごと省略できることになる。

この判定ができるのなら、最初からnormalizeされているJARはオンラインの自動処理でjarsigner → pack200してクライアントに送信、normalizeされていなければjarsignerだけかけて送信(normalizeとpackをかけてもいいけど、遅いのでここではしないことにする)、といった感じで処理を分けることができる。

まぁ、当然のことながら最初からnormalizeされているJARなんて存在しないので、pack200形式で流すためにはどこかで事前にpack200 –repackを流しておく必要はある。とすると、jarsignerだけオンライン/オンデマンドで、normalizeがオフライン/バッチに、とわざわざ分割する意味があるのか、jarsignerまで含めて全部バッチの方がいいんじゃないか、という意見もでるとは思う。

ここであえて2つに分けて考えるのにはもちろん理由がある。jarsignerをオフライン処理にしてしまうと署名忘れ=即障害になってしまうわけだけど、normalizeだけをオフラインバッチにすれば万が一normalizeし忘れたJARがあったとしても、最低限jarsignerはかかるのでエラーを回避することができる。本番運用では署名忘れなんてあってはいけないので論外で、事前のリリースプロセスにじっくり時間をかけて準備万端で挑むものだろう。しかしテスト環境ではリリースの速さとかダウンタイムの最小化が重要になってくるわけで、じっくり時間をかけてリリースに成功しても誰も褒めてはくれないし、そんなことをしていたら依頼されたリリース作業の半分もこなせない。jarsignerだけは何があっても必ずかかっている、ということに確信が持てるだけで、障害報告を受けたときに「jarsignerがうまくいっていないのかも?」という可能性をいきなり排除できるわけで、調査に無駄な時間を使うことを避けられるのだ。

Zipアーカイブコメントを読む

そんなわけで、JARファイルがnormalizeされているかどうかを調べる方法。unpackされたJARを判別するには、JARファイルにPACK200というアーカイブコメントがついているかどうかで判断できる。めったに使わないけど、Zipファイルってアーカイブレベルとファイルエントリーレベルでコメント書けるんだよね。

Javaの場合、アーカイブコメントはZipFileクラスのgetComment()で取得できる。しかしこのメソッド、残念なことに使えるのはJava SE 7からである。今開発しているシステムはJava SE 6なので、使えない。

とはいっても、まぁアーカイブコメントくらいなら、バイナリ形式が公開されているファイル形式だし何とかなるんじゃないかとおもって仕様書見ながら書いたのが以下のコード。

import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.math.*;
import static java.nio.channels.FileChannel.MapMode.*;

public class ZipParser {

  private final static int ARC_COMMENT_OFFSET = 22;
  private final static int MAX_COMMENT_LENGTH = (int) Character.MAX_VALUE;
  private final static int CENTRAL_DIR_LENGTH = ARC_COMMENT_OFFSET + MAX_COMMENT_LENGTH;

  public static void main(String... args) throws Exception {
    File source = new File(args[0]);
    if (Integer.MAX_VALUE < source.length()) {
       throw new IllegalArgumentException("File larger than 2GB is not supported.");
     }
     // Reverse iterate from end of file.  Stop search when;
     // 1) reached begining of file or
     // 2) scanned size of CENTRAL_DIR_LENGTH
     FileInputStream stream = new FileInputStream(source);
     try {
       long searchLimit = Math.max(0L, source.length() - CENTRAL_DIR_LENGTH);
       int i = (int) source.length();
       MappedByteBuffer buf = stream.getChannel().map(READ_ONLY, 0, (int) i);
       buf.order(ByteOrder.LITTLE_ENDIAN);
       while (i > searchLimit)
        if (0x06 == buf.get(--i))         // 0x06
          if (0x05 == buf.get(--i))       // 0x05
            if (0x4B == buf.get(--i))     // K
              if (0x50 == buf.get(--i)) { // P
                buf.position(i + ARC_COMMENT_OFFSET - 2 /* sizeof(short) */);
                break;
              }
      if (i >= searchLimit) {
        char commentLength = buf.getChar();
        if (0 < commentLength) {
          byte[] commentBinary = new byte[commentLength];
          buf.get(commentBinary, 0, commentLength);
          System.out.println(new String(commentBinary, Charset.forName("UTF-8")));
          return;
        }
        System.err.println("No archive comment found.");
        System.exit(1);
      }
    } finally {
      stream.close();
    }
  }
}

軽く説明

Zipファイルはファイル毎にエントリーがあって最後にcentral directoryという構造体がある。アーカイブコメントはこのcentral directoryの一番最後にあるend of central directoryについているので、今回の目的に限っていえば極端な話、このend of central directoryだけ検査すれば良い。

end of central directoryは22バイト+最大64KBの可変長アーカイブコメントからなる。ファイルの中身に全く興味がない以上頭から読んでいくのは馬鹿馬鹿しいので、ファイルの一番後ろから上に向かって最大65,558バイト分まで順番に読んでみて、end of central directoryのsignatureである0x06054b50(‘P’,’K’,0x05,0x06)を探す。

end of central directoryのsignatureを見つけたら晴れてend of central directoryの構造体が作れるわけだけど、今回はコメントにしか興味がないのでそれ用のclassは作らない。代わりに、最後の2バイトがアーカイブコメントの長さを表していることが分かっているので、いきなりoffset20バイト目を読んじゃう。16ビット整数はJavaだとshortだけど、この場合長さなので負の数ということはないはずで、Cでいうところのunsigned shortで取りたいのでByteBuffer#getShort()ではなくByteBuffer#getChar()で取る。長さが0よりも大きければ、シグネチャからのoffset22バイト目〜長さ分のbyte[]をもってきて、文字列に変換。

とりあえずこんな感じで無事、normalizeしたJARからPACK200が取れた。バイナリを読む処理とか久しぶりに書いたけどNew I/Oって便利だな。もっと広範囲につかわれてもいいAPIな気がするわ。

JNLP Performance Tuning with Pack200

今時どうかとは思うんだけど、今参画しているプロジェクトでJava Web Startのアプリを提供している。先日これをユーザーテスト環境へ配ったところ「以前のバージョンと比較して遅い」と苦情がでているそうで、パフォーマンスチューニングをさせられるはめに。

そもそもパッケージソフトについているライブラリのサイズが前バージョンよりも全然でかいので、同じ時間で出せるわけねーだろ、とかフツーの人なら考えるのだが。そういう事実をお客さんに何も説明しないまま「お客さんが遅いって障害票あげているからなんとかしろ」って話を一方的に受けてくるのはエンジニアとしてどうかと思うんだが、まぁ俺はその場にいなかったので今更騒いでも後の祭り。こんな感じで中身がよく分かってない人たちがものをつくったり運営したりすると、まじめに調べて仕事している方はいろいろ納得いかないことが多くて、ほんとストレスがたまる。

でもまぁ、そんな納得のいかない話を黙って受けたのは、その原因にまったく思い当たらないわけでもなかったから。このアプリはJavaのセキュリティでフルアクセスが必要なのでjarsignerの署名が必須なのだけど、実は対象ファイルの数が数百と多く、サイズも大きいため(全部で300MBくらい)、Deploymentの際に署名したJARを準備するのがかなり面倒くさい。それを今まで手作業で回していたのだから、署名し忘れたりとかクライアントとサーバーでバージョン違いのものが置かれていて「Unmarshalできねーよ例外」がでるとか、リリース関連の問題にもなっていたわけで。

今回はそこら辺を改善するために、JNLP/JARダウンロード用のServletを書いた。こいつは初回アクセス時にサーバー側のJARを持ってきてコッソリjarsignerを呼び出して署名をつけ、クライアントに送りつける仕組み。なので万が一署名し忘れても裏で勝手に署名されるからエラーになることはほとんどないし、サーバー側とバージョンがずれることもない。ただし、署名している時間がかかることは当然として、さらには既に署名済みJARが存在するか、存在する場合は前に署名されてから元ファイルが更新されていないか(再署名が必要か)、などのいくつかのチェック処理が入るので、単純に要求されたファイルをWebサーバーからストリームするのと比較すると、あちらこちらのファイルを見に行かなければならない分、多少オーバーヘッドがある。

JARに署名する時間はJarSignerクラスに任せている関係でほとんど調整の余地がないので、他の部分で埋め合わせするしかない。それで署名が終わった後のダウンロード時間を短縮するいい方法はないかと調べていて見つけたのが、署名済みJARをそのままダウンロードさせる代わりに、Pack200フォーマットというより高圧縮な形式を並行で用意することでダウンロードするファイルのサイズを大幅に削減する方法。実はJava 1.5の時代からあったらしい。全然知らなかった。そういえばeclipseの更新サイトに転がっていたよpack.gz。あれか。

Pack200形式と署名付きJAR

Pack200形式のファイルを作るにはJARを作ってからpack200コマンドにjarファイルを食わせれば良い。長年JavaやってるけどJDKにそんなコマンドあったのか!って感じでした、俺。

こんな感じか。

[~] cd classes
[~/classes] jar cf ../hoge.jar *
(略)
[~/classes] cd ..
[~] pack200 hoge.pack.gz hoge.jar

Pack200というのはJava特有の最適化を行うことで、JARの中身を不可逆圧縮する仕組みらしい。もちろん不可逆といってもJPEGとかMP3のように劣化してしまうのではなく、Java的に見て意味が変わってしまうような変更はされない。ただ、圧縮率を稼ぐために各種並べ替え等が発生し、バイナリレベルではかなり変わってしまう。これではバイナリに対してデジタル署名しているJARをpack200にかけるとせっかくのデジタル署名が全部無効になってしまい、使い物にならない。

JARのNormalize

署名済みJARのデジタル署名を壊さないためには、JARのnormalize作業が必要になる。一度Pack200形式に圧縮して、それをunpackすると元のJARファイルが出てくるわけだけど、このときに出てくるJARファイルはpack200向けに最適化されたレイアウトに変更されている(だから不可逆圧縮なんですね)。このことをnormalizeするという。これをもう一度pack200しなおしてもレイアウトの変更は行われないので、つまるところ、normalizeされたJARに対してjarsignerを実行すれば、pack200しても署名が無効にならずに済むというわけだ。

とはいっても、normalizeするだけのためにpackして即unpackするのも馬鹿馬鹿しい。だからそういうことをしないでも済むように、pack200には–repackというオプションが用意されている。これをつけて呼び出すとpack.gzを作るのではなく、JARファイルのnormalizeだけを実施する(–repackの場合pack.gzは作成されない)。

[~] ls -al commons-cli-1.0.jar
-rw-r--r--  1 debugjunkie  staff  30117  7 13 18:24 commons-cli-1.0.jar
[~] pack200 --repack commons-cli-1.0.jar
[~] ls -al commons-cli-1.0.jar
-rw-r--r--  1 debugjunkie  staff  29754  7 13 18:25 commons-cli-1.0.jar

この後にjarsignerを呼べば良い。

[~] jarsigner -keystore hoge.key -storepass hogepass commons-cli-1.0.jar hoge

んで、最後にpack200。こんな感じで、笑っちゃうくらい小さくなる。

[~] pack200 commons-cli-1.0.pack.gz commons-cli-1.0.jar
[~] ls -al commons-cli-1.0*
-rw-r--r-- 1 debugjunkie staff 32903 7 13 18:30 commons-cli-1.0.jar
-rw-r--r-- 1 debugjunkie staff 13912 7 13 18:31 commons-cli-1.0.pack.gz

署名もちゃんと検証可能。

[~] jarsigner -verify commons-cli-1.0.jar
jar verified.

Warning:
This jar contains entries whose certificate chain is not validated.

Re-run with the -verbose and -certs options for more details.

ただしjpgとかpngとかリソース系のものがたくさん入っているJARファイルだとpack200はむしろ逆効果で、倍以上のサイズになったりする場合もある。だから、Deploymentの前にちゃんとサイズを見ておく必要がある。通常のJARの方が小さい場合はpack.gzをおかないようにする方がいいだろう。

Server側の返し方

JNLPでは<property name=”jnlp.packEnabled” value=”true”/>をjnlpファイルに書いておくと、HTTP要求のaccept-encodingヘッダにpack200-gzipが付加されて送られるようになる。pack.gzをサポートしますよ、とクライアントから意思表示してもらうのであって、JNLPに「オマエはpack.gzを取りに来いや」とファイル名を直接書くのではないことに注意する。pack.gzはあくまでも最適化の一環であり、pack200をサポートしないクライアントも含めた全てのクライアントが正しくファイルを受信できるよう、pack200-gzipがaccept-encodingにない場合は通常のJARファイルを返せるようにしなければいけない。

Server側ではpack200-gzipが指定されていて、該当JARファイルに対応するpack.gzファイルがあればそちらを返し、なければjarファイルを返すようServletを実装しておけば良いだろう。このときのHTTPヘッダはJARファイルの場合は


content-type: application/x-java-archive

pack.gzの場合は


content-type: application/x-java-pack200
content-encoding: pack200-gzip

と返すようにする。あとはjavawsがよきにはからって処理してくれる。はず。

ちなみにJDKのSampleにこの辺をサポートしているJnlpDownloadServletなるものがあるらしいから、それを使うのがいいのだろう。自分はつい先日までそのサンプルの存在を知らなくて、自分で書いてしまったけど。

早くこの世からなくならないかなぁ、Java Web Start。

定数定義

プログラムを書くときに、変数とか定数に適切な名前をつけることが良いことであることに異論のある人はあまりいないと思うけど、現実には結構ひどいのとよく遭遇する。

文字そのまま

final static String COMMA = “,”;

記号系に英語名つけて、グローバル定数クラスに定数定義。これよく見るんだけど、なんのためにやっているのか理解に苦しむ。これを書く人の大半は文字列をそのまま使うのはいけない、というルールを何も考えずに適用して責任逃れをしているにすぎないのではないか。「, 」をそのまま書くよりも明らかに長くてコード書きづらいし、名前みても追加情報が得られるわけでもなく、何も嬉しくない。

文字の説明ではなく意味の説明を

カンマとかはよくCSVのセパレータとして定義されているけど、それを定数定義するなら何の文字が入っているかではなく、何のための記号なのかを書くべきではないか。例えばFIELD_SEPARATORとか。java.io.Fileにもいい例があるでしょ、File.separator。(まぁあっちはまたプラットフォームによって値が変わるとかちょっと色合いが違うけど)。

中の文字ではなく何のための識別子なのか定義しておけば、後から仕様変更がきてセパレータがタブになったとしても、FIELD_SEPARATORの定義箇所だけ直せば済む(これが元々文字列を定数定義する意義だろう)。COMMAって名前にしちゃう人は、名前がCOMMAのままで内容がタブだと混乱するので、(中身だけ修正するというのは議論するまでもないので置いておいて)final static String TAB = “\t”;とかにRefactoringで名称変更するのだろうけど、それ自体無駄な作業だし、Refactoringが正しく実施できるのは、修正対象の最新ソースをすべてワークスペースに持っている前提だ。大規模プロジェクトで一部のコンポーネントのみ開発している人は全ソースを手元に持っていなかったりするから、Refactor漏れする可能性もある。Refactoringってみんなが思っているほど当てにならない。

それに、ソースコードのオーナーシップを変な風に適用して、自分の担当分のソース以外はチェックインしない、みたいな人も結構いる(これもひどい責任逃れだよね…)。それじゃあんたがチェックインしたところ以外が軒並みビルドエラーですがな。

定数定義のスコープ

変数とか定数には適用されるコンテキストとスコープがある。本来モジュール間の結合度を下げるためにあっちのクラスからもこっちのクラスからも参照されるものは極力減らすべきだけど、大規模と呼ばれる案件でトラブっているところに支援で入ると確実に遭遇するのが巨大な定数定義クラス。CommonConstとかConstantsとかSharedConstantsとか[プロジェクト名]Constとか。みんなが使いそうな定数を定義してあちらこちらに同じようなものが定義されているのを防いでいるのだろうけど、使い方が正しいかというとたぶん半分くらいは間違ってると思う。

たとえば先のCOMMA。これを全体で使う定数インターフェースに定義したとき、カンマを使う人はこの定数を参照するようにはするだろうけど、果たしてそのときにただの「カンマという文字の別名」として扱うのか、あるいは「外部連携ファイルのファイルセパレーター文字」として使うのか意識しているだろうか?ファイルセパレーターがかわったときにこの定数の値を変更したら、ただのカンマだと思って使っていた人のプログラムは軒並み壊れるよね。

だから定数なり変数なりを定義するときは、その値がどこまで見えていればよいのかをちゃんと考えるべきで、自分が定義したその値は他の人にとって混乱するものではないか、誤解されないか、そもそも他の人に見える必要があるのか、をグローバルなインターフェースに置く前に考えろという話である(そういうコードを平気で書く人たちはモジュールの設計してないだろ絶対、って思うわけで)。

要するに

最初に戻って結論はグローバルの定数クラスにCOMMAとか意味不明な変数・定数を定義するなバカモノ、ってことである。フツーのプログラマだったらそんなことわざわざ書くなよ、って思う訳なんだが、自分が参加するプロジェクトはこんなのばっか。悲しい。

詳細設計書という名のゴミ

いろいろと厳しいご時世の中、確実に以前よりもお仕事の数は減っているようで、ウチの部門の人たちもプロジェクトから突如解任されたり、プロジェクト自体が自然消滅したりして、部門の席に帰ってきて暇している人がちらほらいる。同じ部門で、かつてトラブルプロジェクトを共に闘いぬいた某後輩くんもそんな中の一人で、つい最近帰ってきたんだが、しばらく暇らしい。

その彼が、何の前触れもなく言った。

「設計書を表記の揺れとかなく書くには、どうしたらいいんですかねぇ?」

作業に熱中していたところに不意打ちを食らった俺。意図をはかりかねて彼を見つめて黙って待っていると、彼は続けた。

「どういうツールがあればいいと思います?例えば、Javaのコード補完みたいに、決められた辞書から入力を補助するツール、あったらよくありません?」

俺ねぇ、そういう、人間の創造力を捨てて機械的にものを作るアプローチ、信じないんだよね。ツールは人の能力を増幅するものであって、足りない能力の補完はしてくれないんだぜ。話、聞く相手間違ってない?

「うーん、WordとかExcelでっていうこと?あるじゃん、IMEが。ツールなんか作んなくてもプロジェクト専用の単語詰めたユーザー辞書つくって、それメンバーに配ればそれでいいんじゃないの?ツール非依存だぜ」

俺は彼の出した要件を120%満たす完璧なソリューションだと確信した。しかも超汎用性が高い。

「えー、それは違うじゃないですかぁーw」

正解のつもりだったんだが…ギャグかなんかだと思われているらしい。「もーむす」って書いたら「モー娘。」になるとか、「あkb」って書いたらもしかして「AKB48」って変換してくれる、あれと今の話はいったい何が違うというのだ(なお彼の名誉のために言っておくと、彼も別の人から同じ話題を振られただけで、俺の意見を聞いてみたかっただけみたい。本当はとても優秀な人だ)。

まぁそんな雑談してたのだけど、この業界で繰り返されるこの手の話は本当に根が深い。いつも思うのだが、この議論の背景には必ず、プログラムを日本語で書いた「詳細設計書」がある。彼らはあえて口にしない、あるいは気づいていないが、これには以下のおかしな前提がある。

  1. コードを書くことは基本的に設計を伴わない単純作業である。コードを書き始めるということは、設計が終わったということ。
  2. プログラマーは「詳細設計書」がないとコードが書けない。単純作業をするためのほとんど考えない人たちだからな。
  3. だから要件を理解している上流担当の人が、日本語でプログラムをかくための説明書を作ってあげる必要がある。それが詳細設計書だ。

設計者は、処理の流れをExcelで書く。まぁExcelで書くかどうかはここではどうでもいいのだが、とにかくExcel、それがルールだ。で、入力情報はどこからどうやって持ってきて、どういう順番でどういう計算をして、監査記録をどこに残して最後にどこにどういう結果を出力するか、とかを、小学生の読書感想文以上につまらない感じで淡々と書く。愛とか感動とかそういう要素は一切ない。

プログラマーは、それを見てJavaとかのプログラミング言語に一字一句変換していく。この変換作業こそが、彼らのいう「実装」だ。ちなみに、プログラマーが親切心から設計書に書いていないことを実装なんかした日には「設計書と違う、勝手なことをするな」とか怒られかねないので、多少(いや、かなり)変だと思っても設計書に書いてあるままに作ることが多々ある。なにしろ、どんなに正しく作っても、スケジュール内に作らないとメチャクチャ怒られるからな。仕様が間違っていようが書いたプログラムが動かなかろうが、スケジュールに間に合わせることが全てだ。インド人や中国人と仕事をするならその辺どう考えているか、聞いてみてほしい。

SI業界はこの、「詳細設計書+実装」プロセスで長年やってきているが、どうしても良い品質のものが上がってこない。そこで彼らは、どうしたら「設計書」に書いてあることがうまく「コード」に間違いなく変換されていくか、ということを考え始めたわけだな。本人たちからは聞いてないけど、多分そうだ。

そこでまず、表記揺れがターゲットになった。同じ意味の言葉を何種類もの言葉で表現すると、正直よくわからない状態になる。だから、あまり設計者が意識しなくても、言葉があらかじめ決められた単語のセットに標準化されるようにしたい。辞書に載っている言葉しか使わなければ、誰が読んでも誤解はしないだろうし、入力ミスもなくなるから、つまらない誤植も減る。設計書の品質があがり、実装に伝わる際のコミュニケーションエラーも大幅削減。すばらしい!(…のか?)

しかし、だな。

俺からすると、どれだけ遠回りしてるんだあんたらは、って感じがしてならない。そんな無駄な苦労するくらいなら、最初からコード書け、と。

EclipseでJava書いてみろよ、クラス名間違ってタイプした数秒後には赤線が引かれている。途中までかいてCtrl+spaceすれば補完してくれるだろ?IMEの辞書に登録しなくても。しかもdeprecatedとか現在のContextで使えないエレメントは候補に出ないようにもできる。

クラス名変えたい?Refactor-renameすれば、間違いなく該当クラスのクラス名だけをほぼもれなくリネームしてくれるよな。ExcelでContextやらData Type意識した置換ができるか?

前のバージョンから何が変わったのか、Excelで正確に表現できるか?変更履歴シートを作る?その履歴人が書くんでしょ?正しいの?SCMのHistory diffするのとどっちが楽なのよ、それは。

もちろん、いきなり何でもかんでもコード書けっていっているわけではない。ただ、コードが得意としている分野を、なぜにそれを最も不得手とする自然言語とExcelで無理してやろうとするのか、そこが俺には全くもって解せない。全プロジェクトメンバーの中で、一番スキルレベルの低い人に合わせて作業しているとしか俺には思えないんだが、それは客から金もらって働くProfessionalな仕事の仕方なわけ?

それに、プログラミング能力のない人がプログラムの書き方を、Excelでプログラマに指南して、なんかいいことあるか?しかも、なんで、同じことを日本語とJavaにわけて、2人で2回も書いてるのよ?1人が1回Javaで書けばコスト半分だろ?GDつかってコスト1/3ってあんたらちゃんとトータルコスト計算してるのかよ。

…などとイロイロ一人で考えた一日だった。不毛だよな、SI業界。