ドキュメント作成ツールの検討(Sphinx、ReVIEWとか)

今後、マニュアルを書いてメンテナンスも継続的にしていく機会が丁度ありそうなので、現時点(2012/05/02)でどのツールが自分たちにフィットするか検討してみた。ツールの候補は以下。なお、過去の仕事ではWordとDocBookは経験あり。

前提

今回のマニュアル作成の前提は以下の通り。

  • 複数人で作業し、1つのドキュメントを作成する(主に章単位でアサイン)。
  • 特定顧客向けに部分的にカスタマイズして提供することもある。
  • 外部向けのドキュメントなので、体裁はそれなりに整っている必要がある。
  • 製品のエディションによってマニュアル構成が変わる(上位のエディションだと機能が増えるので章や節、説明が増えるなど)。
  • 出力フォーマットはWordまたはHTMLならOK。PDFは必須ではない。
  • ドキュメント中に画像はそれなりにある。スクリーンキャプチャ、構成図みたいなダイアグラムもある。
  • ドキュメント作成者のメインPCはWindows。
  • ドキュメント作成者はプログラマやインフラ担当などの技術者で、コンピュータリテラシーは高い。

Word

知らない人はほとんどいないであろう、Microsoft社のWord。

Wordの良い点:
  • 初心者にもとっつきやすく書きやすいUI。
  • 日本語の文法チェック機能、文章校正の機能がある。
  • ツールを切り替えることなく文章中に図を作成することができる。
Wordの残念な点:
  • プレインテキストではないのでGitなどのバージョン管理ツールとの相性が悪い。
  • 採番機能はあるがよくずれるため当てにならない。
  • 1ファイルに大量に記述すると、良く落ちるようになったり開けなくなったりする。これは回避方法として、章単位にファイルを分割するといった方法がある。というか、分割しないと複数人で作業しづらいので、分割はした方が良い。分割したファイルから目次を生成する機能がWordにはあるのでこちらを使おう。
  • 自由に書き易い分、スタイルの統一が大変。特に複数人で1つのドキュメントを作成する場合。スタイル機能をちゃんと使いこなす必要が出てくるのだが、難易度がグンと上がる。
  • たとえスタイル機能を使っても、スタイルの適用をミスっていたりすると目視で確認するのが大変(よ〜く見ないと分からないとか)。前の仕事では、スタイルのミスを見つけるために人力でひたすらマニュアルのスタイルチェックを行っていました。画面では確認しにくいため印刷してチェックするとか、紙の無駄遣いにもなっていた。
  • その他、デフォルト設定だと使いづらく色々とカスタマイズした方が良い(標準だとおせっかいすぎるので)。以下の本を見て使いこなすのがおすすめだが、これを全利用者に個別に設定してもらうのも一苦労。

エンジニアのためのWord再入門講座 美しくメンテナンス性の高い開発ドキュメントの作り方

エンジニアのためのWord再入門講座 美しくメンテナンス性の高い開発ドキュメントの作り方

DocBook

DocBookは技術文書のためのマークアップ言語で、XMLでドキュメントを作成していきます。XMLからHTML、PDF、manページ、HTMLヘルプなどを生成することができる。
DocBookについては2008-2009年頃に使用していたことがあり、ブログにも書いていた。当時はEclipseXMLエディタでドキュメントを書いていたが、XMLスキーマを読んで検証もしてくれるし、補完もOKで、バージョン管理ツールも統合されているので便利だった。

DocBookの良い点:
  • プレインテキストで記述できるためGitなどのバージョン管理ツールと相性が良い。
  • プレインテキストなので軽量。サクサク書ける。
  • DocBook用のXMLスキーマが定義されているので、スキーマを読み込めば構造の検証も生成前に事前に行える。単純なタグの記述ミスも事前に防げる。
  • XMLエディタを使えば、XMLスキーマを読み込んでXMLタグの補完も可能。
DocBookの残念な点:
  • XMLなのでどうしても記述が冗長になりがち。特にリストとか表とか。コンピュータには優しいが人間には余り優しくない。
  • 図は画像ファイルとして管理する必要がある。Wordのようにシームレスに扱えない。また、JPGとかPNGで管理しちゃうと、オブジェクトのレイヤー情報が失われてちょっと位置をずらすとかの修正も大変。Visioなど別ツールのファイルでオリジナルを管理する必要が出てくる。
  • 出力フォーマットのスタイルカスタマイズが大変。DocBookではXSLTXML->HTMLに変換しているのだが、XSLTが難解すぎる。
  • ドキュメント生成環境を構築するのに一苦労(今だと改善されている?)
  • 日本語のPDF作成に難あり(今だと改善されている?)

Sphinx

Python製のドキュメント生成ツールです。DocBookとは異なり、XMLではなくreST(reStructuredText)記法でドキュメントを作成します。DocBookと同じように、1つのソースからHTML、PDF、ePubなどを生成することができる。

Sphinxの良い点:
  • プレインテキストで記述できるためGitなどのバージョン管理ツールと相性が良い。
  • プレインテキストなので軽量。サクサク書ける。
  • XMLではなくreST(reStructuredText)記法なので人間にも優しい。やはりXMLと比べると断然こちらの方が見やすさは上。
  • 環境構築も比較的容易。
  • 出力したHTML単体で、全文検索機能を提供するなど他のツールにはないオマケ機能もある。
Sphinxの残念な点:
  • ツールによる構文の事前チェックやreST記法の補完などのサポートが余りない。vimとかemacsならハイライト機能などあるが、ドキュメント書く人はWindowsユーザが大半なので難しい(ただし、reST記法はそこまで難しくないので補完は無くてもほとんど困らなさそう)。
  • DocBookと同じで、図は画像ファイルとして管理する必要がある。Sphinxにはblockdiagのようにテキストからダイアグラムを生成する機能も使えるけど、スクリーンキャプチャとかダイアグラムで表現できないケースは多い。

ReVIEW

Ruby製のドキュメント生成ツール。名前がSEO的に微妙でググってもヒットし辛いのが難点。参考サイトへのリンクを記載しておきます。

こちらもSphinxと同様に、XMLではなくWiki に似た簡易フォーマットでドキュメントを記述していく。出力も同様に、HTML、PDF、ePubなどに対応。

ReVIEWの良い点:
  • プレインテキストで記述できるためGitなどのバージョン管理ツールと相性が良い。
  • プレインテキストなので軽量。サクサク書ける。
  • XMLではなくWikiっぽい記法なので人間にも優しい。
  • 秀丸での編集支援マクロがありWindowsユーザでも書きやすそう。
  • 環境構築も比較的容易。
ReVIEWの残念な点:
  • DocBook、Sphinxと同じで、図は画像ファイルとして管理する必要がある。
  • 出版を目的にして開発されたためか、HTML出力機能は弱い。インデックスのページがなかったり、デフォルトのスタイルもない。ただし、スタイルはないが出力されるHTMLはシンプルできれいなので、CSSだけ作成すればスタイルのカスタマイズは容易と思われる。

結論

結論としては、今回はSphinxを選択する予定。

理由
  • Wordはバージョン管理との相性が最悪なのが一番痛い。あと、スタイルを統一するという点にすごいパワーがかかる(前の仕事で身を持って経験済み)ので外した。
  • 製品エディションでマニュアル構成が変わるのだが、共通部分は二重管理したくない。Wordだと動的な構成に対応するのは厳しい。ここはDocBook、Sphinx、ReVIEWであればプログラムで操作できるので対応しやすいはず。
  • DocBookは個人的にはなれているのだが、技術者がドキュメントを書くとはいえ、今後継続的にメンテナンスして行くことを考えると、人に優しいWiki的な記法が望ましい。
  • ReVIEWも記法としてはシンプルで魅力的だが、出力フォーマットが出版メインでHTMLだと現時点では貧弱。

正直なところ、Sphinx、ReVIEWの2つは全然使いこんでいないので、今後落とし穴にハマる可能性はあるが、新しいことにチャレンジするのも大切なのでいい機会と思って頑張ってみます。

おまけ

自分達で使うケース前提だが比較表も作ってみた。

項目 Word DocBook Sphinx Review
エディタのUI*1 ×
作図サポート ×
読み易さ
バージョン管理との相性 ×
日本語校正 × × ×
環境構築が容易か ×
章、節番号などの採番
スタイルの統一が容易か ×
スタイルのカスタマイズが可能か
HTML出力 -

*1:Sphinx、ReVIEWはWindows上の秀丸などのテキストエディタを想定。DocBookはXMLエディタを想定。

JackRabbitのリポジトリをファイルからDBに移行する

コンテンツリポジトリJava API(JCR)実装であるJackRabbitのリポジトリを移行しようとしてハマったのでメモ。

参考リンク

を見ると、JackRabbitの機能でXMLでExportしてImportする方法とかが紹介されている。しかし、試すとうまく移行できない。

今回はリポジトリ内の特定ノードだけ移行したいのではなく、リポジトリを丸ごと移行したい。正確には、データストアとしてファイルシステムを使っていたのを、データベースにただ変更したいだけ。ファイルシステムリポジトリ --> データベースのリポジトリに変換したいだけなのだが、Export、Importツールは特定ノードに移行には使えそうだが、リポジトリ丸ごととなるとどうもうまく動いてくれない。

結局のところ、リポジトリ丸ごと移行であれば、JackRabbitに用意されているRepositoryCopier APIを使うのがよさそうです。

ただし、JackRabbitのバグなのか、そのままこのAPIを使用するとコピーの途中で「com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry 'deadbeef-face-babe-cafe-babecafebabe' for key ...」と一意制約違反によるエラーが発生して最後まで完了しない現象となった。どうも、本来であればUPDATE文を発行すべきところをINSERT文で更新しようとしてエラーとなっているようです。

というわけで、JackRabbitにちょっとしたパッチをあてつつコピーツールを作成してみた。GitHubで公開しておきます。

使い方

Git cloneでソースを取得し、gradlewでビルドする。

git clone https://github.com/wadahiro/jackrabbit-copy-tool.git
gradlew distZip

build/lib/jackrabbit-copy-tool-{version}.zipが作成されるので、これをJackRabbitのデータを置いているサーバにインストールする。unzipで解凍するだけ。

以下のコマンドでリポジトリのコピーを実行する。パラメータとして、コピー元の設定ファイル・ディレクトリ、コピー先の設定ファイル・ディレクトリを指定すること。

cd jackrabbit-copy-tool/bin/
jackrabbit-copy-tool -sc /jackRabbitDir/repository.xml -sd /jackRabbitDir/home -tc /anotherJackRabbitDir/repository.xml -td /anotherJackRabbitDir/home

Chef SoloのRPMを作成してみた

Chef Soloをインストールするには、ビルドに必要なライブラリのインストールが必要だったり、gemでインストールが必要だったりする。インストール時間の短縮と、クローズドなネットワーク環境でもインストールできるようにrpmを作成してみた。

rpmを作成する

Chefはrubyでできているので、Gemでパッケージされている。fpmを使えばどうやらgemからrpmを作成できるらしい。gem以外にも対応しているし、rpm以外にdebなんかも出力できるようだ!
今回はこのfpmを使ってやってみた*1。作成環境は、CentOS 6.2 x86_64、前提としてChef Soloがインストールできるよう、rubyrubygems、コンパイルに必要なライブラリが入っていること。rubyrubygemsはyumでOS標準のものをインストールしていること。

  • まずはfpmをインストール。fpm自体もRubyでできており、gemで簡単にインストールできる。
gem install fpm

ここからはConverting rubygems · jordansissel/fpm Wiki · GitHubを参考にgem -> rpm化を行う。

  • Chef Soloと依存gemを/tmp/gems以下に集める。
mkdir /tmp/gems
gem install --no-ri --no-rdoc --install-dir /tmp/gems chef
  • 集めたgemファイルをfpmコマンドでrpm化する。以下のコマンドで一気に実行可能です。
find /tmp/gems/cache -name '*.gem' | xargs -rn1 fpm -s gem -t rpm

これで依存関係のあるgemも含めてまとめてrpm化することができた。

早速入れてみる

まっさらなCentOS 6.2 x86_64に早速インストールしてみる。

  • rubyrubygemsは必要なので、これを先にインストールする。オフライン環境ならDVDからインストールする。
yum ruby rubygems
  • 先ほど作成したrpmをまとめてインストール。「--nodeps」オプションをつけているのは、何故かrubygem-chefのインストールで依存関係エラーが出るため*2
rpm -ivh --nodeps *.rpm
  • chef-soloコマンドを実行して動作することを確認。
chef-solo

*1:これとは別に、[https://github.com/rubiojr/chef-rpms-builder:title=chef-rpms-build]なんてものがぐぐると見つかったけどこちらは使い方が分からず...

*2:とりあえず無視してインストールして使用しているが今のところ無事に動いている

SVN -> Gitへの移行メモ

SVNからGitへ移行したので、その時の実施内容をメモっておく。

git-svnで移行

移行にはgit-svnを使う。git-svnの使い方はこことかで解説されているが、「git svn clone」コマンドでは移行しなかった。
理由は以下。

  • 全てbranchesの下にあればよいのだが、物によってはbranches/customer_yyy/v1.xのようにサブディレクトリがさらに切ってあったりする
  • 移行したくないブランチもあったりする

というわけで、コマンドではなく設定を直接修正して移行することにした。

まずはリポジトリの作成

移行先は中央リポジトリとするのでワーキングツリーは不要なため--bareを付けた。

git init --bare myrepo.git
svn-remoteを設定

myrepo.git/configを以下のように編集し、svnからのfetch先設定を追記する。なお、今回はタグは移行はしていない。

  • fetchは何個でも書けるので、trunkと移行したいブランチを個別に書く。例だとSVNのtrunkとbranches/developブランチを移行するように設定している。
  • branchesで書くときは、特定ディレクトリ以下をまとめてブランチとして移行したい時に使う。
[svn-remote "svn"]
        url = http://my_svn_host/svn/myrepo
        fetch = trunk:refs/heads/master
        fetch = branches/develop:refs/heads/develop
        branches = branches/customer_yyy/*:refs/heads/*
git svn fetchでSVNから履歴を取り込む
git svn fetch

これでSVNからGitへの移行は完了。

Gitリポジトリをコンパクト化

これは必ずしも必要ではないが、SVNで管理していた時に、過去にJARなどのバイナリファイルもコミットしてしまっていた。そのため、リポジトリのサイズがかなり巨大になってしまっていた。
現在はJARファイルは格納していないし*1、過去の履歴でそのJARを参照したいケースもないので、歴史から抹消してコンパクト化することにする。

Gitでは歴史をさかのぼって特定のファイルやディレクトリを削除することも簡単にできる。簡単すぎて怖いくらい…
以下のように、git filter-branchというコマンドを使う。

git filter-branch --tree-filter 'rm -rf lib' HEAD --all

'rm -rf lib'のところに、削除コマンドを渡せばよい。libフォルダにJARファイルをたくさん格納していたので、libフォルダ以下を消すようにした。
「--all」は、全ブランチに対しても同様の削除を行うさいに必要。masterブランチだけ消しても他のブランチにバイナリがあると結局サイズは変わらないので、全ブランチから消しちゃう。

Cloneしなおしが必要?

git filter-branch完了後、du -hs . でリポジトリのサイズを見ても変わっていない・・・?(むしろ増えている??)

git gc

としてみても駄目。

git clone --bare http://my_git_host/git/myrepo.git

でcloneしなおすとこちらはOK。100MBオーバ⇒1MB以下と約1/100もコンパクトになりました。

*1:JARやZIPはMavenリポジトリで管理するようにした。

Gradleでマルチプロジェクトビルドする

マルチプロジェクトは個人的には大変なので余りやりたくないのだが、規模が大きくなると様々な理由で必要になってくる。Gradleにももちろんマルチプロジェクトをサポートする機能がある。今回はそれを紹介。

そもそもマルチプロジェクト化する理由って?

プロジェクトによって色んな理由があると思うが、うちだと以下のような理由がメインかな。

  • ビルドのルールを標準化したい
    • ビルド時のソースエンコードとかビルドターゲットのバージョンとかバラバラにならないように統一したい。
  • 依存ライブラリのバージョン定義を集約したい
    • 各プロジェクトで使っているOSSライブラリのバージョンがバラバラ...なんてことを避けたい。
  • ビルドスクリプトを簡略化したい
    • 同じような処理を各プロジェクトで記述するのは無駄だし、メンテナンスも大変。どこかに共通化して定義して、各プロジェクトのビルドスクリプトはすっきりさせたい。
  • サブプロジェクトも含めた統合ビルドを簡単に行いたい
    • 各プロジェクトを統合して1つのパッケージにするような時に、まとめて一度にビルドできるようにしたい。

マルチプロジェクトの構造

マルチプロジェクトの構造には大きく階層構造とフラット構造がある。

  • 階層構造は、親のプロジェクトがルートにあり、その下にサブディレクトリで各プロジェクトがあるようなケース。
  • フラット構造は、親が子プロジェクトをネストするのではなくて親も子も並列にあるようなケース。

Mavenもそうだが、Gradleも両方に対応している。

階層構造のプロジェクトの場合

今回は例として以下のような階層構造のプロジェクトの場合を紹介。良くありそうなJavaのWebアプリを想定して、

  • ルートプロジェクト(hierarchical-sample)
  • JARを作成するservice、shareプロジェクト
    • serviceはshareに依存している
  • WARを作成するwebプロジェクト
    • webはservice、shareに依存しており、WARにパッケージングするときもWEB-INF/libに格納する

という、ルートプロジェクト以下に3つのサブプロジェクトがあるケースで紹介する。ディレクトリ構成は以下のようになる。

hierarchical-sample
|-- build.gradle
|-- settings.gradle
|-- service
|   `-- src
|       `-- main
|           `-- java
|               `-- ...
|-- share
|   `-- src
|       `-- main
|           `-- java
|               `-- ...
`-- web
    `-- src
        `-- main
            |-- java
            |   `-- ...
            `-- webapp
                `-- WEB-INF
                    `-- web.xml
settings.gradleを定義する

Gracleでマルチプロジェクトを行うには、ルートプロジェクトにsettings.gradleを配置する。このファイルで以下のようにサブプロジェクトのディレクトリ名を記述する。これでGradleはサブディレクトリをサブプロジェクトとして認識する。Mavenな人には、親POMでmoduleを定義するような物と思ってもらえればいいです。

include 'web', 'service', 'share'
build.gradleの定義

ルートプロジェクトのbuild.gradleを定義するわけだが、今回はサブプロジェクトには一切ビルドスクリプトを置かないという方法をしてみる(もちろん、各サブプロジェクトにbuild.gradleを置くこともできる)。Mavenだとサブプロジェクトにpom.xmlを必ず置く必要があるが、Gradleだとルートプロジェクトでまとめて定義することもできる。

今回は以下のように定義してみた。

subprojects {
    repositories {
        mavenCentral()
    }
}

project(':service') {
    apply plugin: 'java'
    
    dependencies {
        compile project(':share')
    }
}

project(':share') {
    apply plugin: 'java'
}

project(':web') {
    apply plugin: 'war'
    
    dependencies {
        providedCompile 'javax.servlet:servlet-api:2.5'
        compile project(':service')
        compile project(':share')
    }
}

特徴的なのは以下。

  • subprojects { ... }で、サブプロジェクト全てに対してあらゆる設定ができる。上記ではMavenリポジトリを参照するよう設定している。
  • project(...) { ... }で、ルートプロジェクトのビルドスクリプトから各プロジェクトの個別設定を記述できる。
  • dependenciesでcompile project(':share')のように記述することで、プロジェクト間の依存関係を定義できる。別のプロジェクトで作成されるJARをクラスパスに登録したいとか、WARファイルの中に入れたいとか、そういう時に使う。
ビルドしてみる

ルートプロジェクトの位置からまずはビルドしてみる。タスクにはcompileJavaを指定しコンパイルまで実行してみる。なお、--daemonオプションをつけると次回のGradle起動が高速化されるのでオススメ(エイリアスとして登録すると良い)。

# cd hierarchical-sample
# gradle --daemon compileJava
:share:compileJava
:share:processResources UP-TO-DATE
:share:classes
:share:jar
:service:compileJava
:service:processResources UP-TO-DATE
:service:classes
:service:jar
:web:compileJava

BUILD SUCCESSFUL

Total time: 1.177 secs

ちゃんとプロジェクト間の依存関係を解釈して、share -> service -> webの順にコンパイルしてくれます。
ここまではMavenのマルチプロジェクト機能でも同じだが、次にGradleならではの機能を紹介。

サブプロジェクトからビルドする

サブプロジェクトにcdで移動し、そこから実行してみる。serviceに移動し、またcompileJavaを実行すると...

# cd service
# gradle --daemon compileJava
:share:compileJava UP-TO-DATE
:share:processResources UP-TO-DATE
:share:classes UP-TO-DATE
:share:jar UP-TO-DATE
:service:compileJava UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.926 secs

このように、serviceはshareに依存しているので、まずはshareをコンパイルしつつserviceをコンパイルしてくれる。Mavenだとこうはいかない。良くも悪くもPOMでの依存関係が徹底されているので、ローカルのMavenリポジトリに依存プロジェクトであるserviceのJARをまずinstallしてからでないと依存関係が解決できないのだ。

ちなみに、UP-TO-DATEというのは、実際は変更がないため何もコンパイルなど処理されていないことを表している。例えば、shareプロジェクトのソースに修正があったとすると、

# cd service
# gradle --daemon compileJava
:share:compileJava
:share:processResources UP-TO-DATE
:share:classes
:share:jar UP-TO-DATE
:service:compileJava UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.909 secs

のように、ちゃんとshareプロジェクトをコンパイルしてから処理してくれる。

サブプロジェクト一つだけビルドしたい!

shareプロジェクトは修正していないから、いちいち見に行かずにserviceプロジェクトだけコンパイルさせてよ!!という急いでいる人のためにも便利なオプションがある。-aオプションを付けて実行すると...

# cd service
# gradle --daemon -a compileJava
:service:compileJava UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.848 secs

お望み通りserviceプロジェクトだけコンパイルしてくれる。

各プロジェクトにcdするの面倒なんだけど?

各プロジェクトのディレクトリに移動するのが面倒だ!!って人には以下のようなこともできるよ。:プロジェクト名:タスクで実はどこからでも特定プロジェクトに対してビルドできたりする。もちろん、ルートプロジェクトからもできるので、全く移動せずに特定のサブプロジェクトだけビルドとかできる。

# cd service
# gradle --daemon :web:compileJava
:share:compileJava UP-TO-DATE
:share:processResources UP-TO-DATE
:share:classes UP-TO-DATE
:share:jar UP-TO-DATE
:service:compileJava UP-TO-DATE
:service:processResources UP-TO-DATE
:service:classes UP-TO-DATE
:service:jar UP-TO-DATE
:web:compileJava UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.941 secs
WARをビルドする

今回の例だとWebアプリの構成になっているので、最後にWARをビルドしてみる。ルートプロジェクトに戻り、warタスクを実行する。warタスクなんてものはWAR Pluginを利用しているwebプロジェクトでしか使えないのだが、ルートプロジェクトからの実行だとちゃんと認識して、依存関係も解決しつつwebプロジェクトのwarタスクをちゃんと実行してくれる。Gradle賢い!!

# cd hierarchical-sample
# gradle --daemon war
:share:compileJava UP-TO-DATE
:share:processResources UP-TO-DATE
:share:classes UP-TO-DATE
:share:jar UP-TO-DATE
:service:compileJava UP-TO-DATE
:service:processResources UP-TO-DATE
:service:classes UP-TO-DATE
:service:jar UP-TO-DATE
:web:compileJava UP-TO-DATE
:web:processResources UP-TO-DATE
:web:classes UP-TO-DATE
:web:war UP-TO-DATE

BUILD SUCCESSFUL

Total time: 1.009 secs

今回の紹介はこの辺で終了。フラット構造の場合とか、依存関係のまとめ方とかsettings.groovyによる動的なマルチプロジェクトとか書きたいことはあるけどまた今度。
なお、今回作成したサンプルは
https://github.com/wadahiro/gradle-samples/tree/master/multiproject-samples/hierarchical-sample
にあります。

Gradleのビルドライフサイクルについて

Gradleを使うにあたって知っておいた方が良いと思う。
詳しくはGradleのドキュメントを見てもらえば分かるのですが、結構後ろの方にあるので見つけにくい。個人的にはもっと前半に説明してほしかったかなと。日本語で読みたい場合はこちらです

ちょっと引用すると、

初期化
Gradleはシングルプロジェクト、マルチプロジェクトの双方をサポートします。初期化フェーズでは、どのプロジェクトがビルドに参加しているのか決定し、Projectインスタンスを生成します。


設定
ビルドに参加しているすべてのプロジェクトのビルドスクリプトを実行します。初期化フェーズで生成したProjectインスタンスは、このフェーズでビルドスクリプトの記述通りに設定されるのです。


実行
設定フェーズで初期化と設定が完了したタスクのうち、実行するべきタスクを抽出して実行します。実行するタスクは、gradleコマンドに引き渡されたタスク名と、コマンドの実行ディレクトリから決定されます

47.1. ビルドフェーズ

のように、Gradleを実行すると初期化->設定->実行という順番で各フェーズが動きます。ポイントは、実行フェーズは、指定したタスクと依存関係にあるものが実行されるが、設定フェーズまではビルドスクリプトの内容が全て実行されるという点です。

例として、http://d.hatena.ne.jp/Hirohiro/20120320/1332217853で使用したWARのマージを行うサンプルに少し手を加えた物で説明する。build.gradleの内容は以下の通りとする。

apply plugin: 'war'

war {
    // ここは設定フェーズで実行される。WARファイルの名前を設定する。
    archiveName = 'sample.war'
}

war << {
    // ここは実行フェーズで実行される。WARタスクで生成されたWARに別のWARをマージ。
    ant.zip(destfile: war.archivePath, duplicate: 'preserve', keepcompression: true, update: true) {
        zipfileset (src: project(':common').war.archivePath)
    }
}

実は、タスク定義の書き方によってどのフェーズで実行させるか変わってくる。

  • war { ... } と定義したところは、設定フェーズ(Configuration phase)で実行される。つまり、この箇所はユーザがどのタスクを実行したとしても常に動作する。例では、ここではWARファイルの名前を設定している。
  • war << { ... } と定義したところは、実行フェーズ(Execution phase)で実行される。つまり、関係のないタスク実行時は処理されない。例では、WARファイルのマージ処理を行っている。

なので、設定フェーズに誤ってファイル処理(コピーとかzip作成とか)を入れちゃうと、どのタスクを実行しても常に実行されるという残念なことになるので注意しましょう。

なお、タスクの実行フェーズへの設定方法としては、

  • 前に追加
  • 後ろに追加

の2つ方法があります。上記の例の、「<<」は実は「後ろに追加」という意味。「<<」の代わりに「doLast」と書くこともできる。前に追加はdoFirstを使う。

war << {
    // ここは実行フェーズで実行される
}

war.doLast {
    // ここは実行フェーズで実行される。<<と同じ。
}

war.doFirst {
    // ここは実行フェーズで実行される。上記より先に実行される。
}

要は各タスクの処理はリスト構造になっており、前に追加か後ろに追加ができるようになっているわけです。
上書きではなくて追加という形をとることで、既存のタスクに簡単に処理を追加させることができるわけです。WARのマージ処理のところも、Gradle War Pluginが持っているwarタスクの後処理として、マージする処理を追加しているだけです。

war << {
    ant.zip(destfile: war.archivePath, duplicate: 'preserve', keepcompression: true, update: true) {
        zipfileset (src: project(':common').war.archivePath)
    }
}

Jenkinsで1つのジョブで複数のGitリポジトリをビルドする方法

Jenkinsのジョブ1つに対して複数のGitリポジトリを登録してビルドする方法について調べたのでメモ。簡単にできると思いきや意外とハマってしまった。。。

確認した環境

  • Jenkins 1.456
  • Jenkins Git Plugin 1.1.16
  • Jenkins Multiple SCMs plugin 0.2

Jenkins Git Pluginのみではできない!?

まずはJenkins Git Pluginの設定のみでできないか試してみたのだが、残念ながらこの方法は失敗。一応設定方法を紹介すると以下の通り。

  • ジョブの設定で、ソースコード管理システムにGitを選択しつつ、Repository URLにGitリポジトリのURLを設定する。
  • さらに、Repository URLの下にある追加ボタンをクリックするとRepository URLの入力項目を増やすことができるので、ここに別プロジェクトのGitリポジトリURLを設定する。以下のように、2つのGitリポジトリが設定された形となる。

f:id:Hirohiro:20120324231812p:image:w640
これでビルドしてみると、残念なことに同一ディクレクトリに複数のプロジェクトがcloneされてしまった orz...

HUDSONのIssueだが、[#HUDSON-8082] git plugin multi-repo can't work - Hudson JIRAを見ると、どうもこの設定で複数リポジトリを登録する目的はバックアップ用??
という訳でこの方法では実現できない。

Multiple SCMs Pluginと組み合わせてみる

諦めずに「jenkins git multi repository」とかでググってみると、Multiple SCMs Pluginと組み合わせるとできるよ〜という情報をゲット。ということで早速Jenkinsの管理画面からMultiple SCMs Pluginをインストールして試してみる。

  • このプラグインを入れると、ジョブの設定>ソースコード管理システムにMultiple SCMsが選択可能になる。
  • これを選択してAdd SCMsからGitを選ぶ。そうすると、従来のGitリポジトリの設定画面が表示されるので、ここに1つ目のGitリポジトリURLを設定する。
  • さらに、Add SCMsから再びGitを選ぶと、2つ目のGitリポジトリの設定画面が追加される。ここに2つ目のGitリポジトリURLを設定する。以下のように、2つのGitリポジトリが設定された形となる。

f:id:Hirohiro:20120324231813p:image:w640

これでできそう!と思いきやこれだけでは実は駄目。この状態でジョブを実行しても、相変わらず同じディレクトリにcloneされる...

高度な設定に隠れた項目が実は超重要!

Gitリポジトリの設定の中にある、高度な設定が実は重要な設定だった。
リポジトリ・ブラウザ」の上にある高度な設定ボタンを押すと色々項目が表示されるのだが、ここにLocal subdirectory for repo (optional)という項目があることに気づく。
このオプションは、リポジトリのclone先にサブディレクトリを設定するためのもの。つまり、複数のリポジトリ設定に対して別々のサブディレクトリを指定してあげれば、同一ディレクトリにcloneされるという問題を防ぐことができるわけだ。

早速これを両方のリポジトリ設定に対して試してみると、今度は無事に別々のディレクトリにcloneされた!

...しかし、これだけではまたもや落とし穴があることが判明。この状態でしばらく様子を見ていると、Gitには何もpushはしていないのにSCMポーリングの度に毎回ビルドされてしまっていることに気づく。
Multiple SCMsのポーリングログを確認すると、前回ビルド時のリビジョン(GitなのでSHA1 ハッシュ値)には1つ目のリポジトリの物が必ず使われているようで、1つ目のリポジトリは更新なしと判断されてOKだが、2つ目のリポジトリでは常に異なるため、ビルドが実行されてしまっていた。うーん惜しい。

高度な設定でもう1つ設定が実は必要

なぜ1つ目のリポジトリのリビジョンが比較対象となってしまっているのかよくわからないので、結局Git Pluginのソースを確認してようやく判明。

同じ高度な設定の中にある、Unique SCM name (optional)が実は必要でした。この値をリポジトリごとにユニークに設定してあげないと、SCMポーリングでの更新チェックで1つ目のリポジトリのリビジョンが使われてしまう。

  • 以下のように、1つ目のリポジトリに対してSCM nameを設定する。

f:id:Hirohiro:20120324231814p:image:w640

  • 同様に、2つ目のリポジトリ設定に対しても設定する。注意点として、1つ目の設定値とは異なるようにすること

f:id:Hirohiro:20120324231815p:image:w640

これでジョブを実行すると、ようやく期待通りに動作するようになりました。

まとめ

まとめると、必要な設定としては以下になる。

  • Jenkins Multiple SCMs pluginをインストールする。
  • リポジトリの種類はAdd SCMsからGitを複数追加する。
  • 各Gitリポジトリの設定で以下を登録する。サブディレクトリとSCM nameはリポジトリごとにユニークになるように設定すること。
    • Repository URL
    • Local subdirectory for repo (optional)
    • Unique SCM name (optional)