もしJenkinsでちょっとずつ違う100のジョブを管理しなくてはいけなくなったら

(2015/05/14 追記) このエントリで言及しているSubversionリポジトリ複数指定できない問題ですが、今は解決されています。

個人用メモも兼ねて。

ちょっとこんがらがったプロジェクトではありがちですが、ある特定の環境へのリリースのためにいくつかのジョブで構成されたビルドパイプラインがあり、それを環境ごとに微妙な差分をつけて、複数種類のビルドパイプラインを構成する必要があったりします。

環境の数やアプリの数によっては、これらの差分を含めてJenkins上にジョブを構成しようとすると、下手をすると100個近いジョブに到達することもあります。

(いうまでもないですが、こういうジョブ過多な状態を通常は目指すべきではありません。)

こういう場合に手作業でJenkinsのジョブを構成しようとすると恐ろしいクリックゲームになるので、Jenkins公式が用意しているJob DSL Plugin を使ってプログラミングで大量のジョブを効率よく構成しましょう。

Job DSL Pluginの概要

https://github.com/jenkinsci/job-dsl-plugin/wiki

  • 普通にプラグイン設定画面からインストール可能です。入れてない人はぜひ入れましょう。
  • GroovyのDSLで、Jenkinsのジョブやビューをサクッと構成可能。
    • 繰り返しや分岐やマップなどJava(Groovy)にある道具を使ってジョブの生成を柔軟に行える。
    • 基本的にDSLでジョブ・ビューごとの設定ファイル(XML)を出すようなイメージ。
    • 作ったジョブは、作成後、作成用のジョブのトップページに表示されます。
      • すでにあるジョブを作成した場合は、設定を上書き。今まであったジョブの履歴などはきちんと保存されます
      • 以前に作成したけれども、次回以降作成対象にならないジョブ(要するに定義が変更されて消えるジョブ)は、放置するか、無効にするか、削除するかといったことも指定可能です。
  • 意外なほど多種のプラグインに対応している。
    • このプラグインDSLが定義されているJenkinsプラグインはメジャーなプラグインと思ってもいいかもしれません。
    • Job DSL Pluginが対応外のものでも設定ファイル(XML)ほぼそのままに記述することにより対応可能。手作業ジョブ設定作業の完全代替にほぼなるはず。
  • (ちなみにSCM Sync Pluginとこれを一緒に使うと、内部のジョブ定義1つごとにSCMにコミットされます。ジョブ定義をするたびにコミットログがウザいことになりますが、気にしないことにしましょう:-p )

さっそくJob DSLを書いてみる

ローカル開発環境のインストール (Windows可)

JenkinsのJob DSL PluginのGroovyソースのテキストエリアにDSLを貼り付けてもジョブの生成はできるのですが、

  • Groovyの文法が正しいのか、
  • セマンティックなエラーは起きないのか、
  • どういうジョブ定義が出力されるのか、

などなどJenkins上だけで実行ボタンをおしたりしてチマチマ確認するのはとっても面倒なので、ローカルでDSLの実行結果を確認できる環境を作ると非常に捗ります。

詳しい手順は https://github.com/jenkinsci/job-dsl-plugin/wiki/User-Power-Moves に書いてあるのですが、とりあえずここでも紹介しておきます。

まずは、Groovyを扱うため、JDKを入れて使えるようにしておきましょう。

あとはJob DSL Pluginをgit cloneしたりして

$> git clone https://github.com/jenkinsci/job-dsl-plugin.git
$> cd job-dsl-plugin

ちょっと適当なJob DSLを所定のフォルダにおいてあげましょう。下記のJob DSLjob-dsl-core/sample.groovyとして保存してください。

def jobNames = ['Alpha', 'Bravo', 'Charlie']

for (jobName in jobNames) {
  job {
    name jobName
  }
}

ここまでやったら下記コマンドを実行すれば、

./gradlew run -Pargs=sample.groovy

job-dsl-core/ 以下に下記3つのそれぞれジョブの設定が入ったファイルができているはずです。

  -rw-r--r--   1 knjname  staff   588 Jan  6 01:29 Alpha.xml
  -rw-r--r--   1 knjname  staff   588 Jan  6 01:29 Bravo.xml
  -rw-r--r--   1 knjname  staff   588 Jan  6 01:29 Charlie.xml
(Alpha.xmlの中身)
<project>
    <actions></actions>
    <description></description>
    <keepDependencies>false</keepDependencies>
    <properties></properties>
    <scm class='hudson.scm.NullSCM'></scm>
    <canRoam>true</canRoam>
    <disabled>false</disabled>
    <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
    <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
    <triggers class='vector'></triggers>
    <concurrentBuild>false</concurrentBuild>
    <builders></builders>
    <publishers></publishers>
    <buildWrappers></buildWrappers>
</project>

基本的にはDSLを編集→gradlew run ...を実行→ローカルに出力されたXMLの中身を確認→DSLを編集、というループで開発していき、納得できる形になってから最後にJenkins上でDSLを実行という形になると思います。

Jenkins上でJob DSLを動かしてジョブを定義する

特に難しいことはなく、Tutorialにあるように、ジョブ生成用の種ジョブを作成するだけですね。

種ジョブは通常のジョブとほとんどかわらないので、たとえばJob DSLや特定の定義ファイルが更新されたら起動するように種ジョブのトリガをしかけておいて、状況に応じてジョブが勝手に増えるようにすることもできます。

ほか

プラグインに対応したビュー、ジョブ、トリガー、前処理、後処理などなどの指定方法はWikiを見ましょう。リファレンスが充実しています。意外に対応範囲が広いことに驚くと思います。

Wiki見てピンとこなければGithub上でソースで該当の関数を検索するのがてっとり早いです。

自分の使っているプラグインに該当するDSLがなければ configure ブロックを駆使して設定ファイルほぼそのまんまで記述しましょう。

ちょっとしたTipsなど

Gradleでの*.groovyファイルの指定方法いくつか

入力となるDSLのgroovyファイルを絶対パスで特定の場所で指定したい人は-Pargs=/root/hoge.groovy (Windowsなら -Pargs=\C:\Users\root\hoge.groovyみたいに指定)、複数指定したい人は"-Pargs=hoge.groovy fuga.groovy" とすれば大丈夫です。

ちなみにXMLの出力場所の指定方法は知りません!

Gradleの起動が遅くてイラつく

gradlewの起動が遅くてイライラする人はgradlewのユーザごとプロファイル設定ファイル(~/.gradle/gradle.properties)に下記の値を指定してGradle用のJVMをデーモン起動させるといいらしいよ!

org.gradle.daemon=true
DSL上で使えるコンテキスト依存の関数の作り方(+Subversionの場合の複数箇所チェックアウト)

たとえば、Subversionを使っている場合、普通に1ジョブ内で複数箇所からチェックアウトすると思うのですが、DSLではこの手のケースは想定されていません!

だから公式ではこうやれってよ!

scm {
  svn('http://hoge/svn/trunk', 'hoge') {
    it / locations << 'hudson.scm.SubversionSCM_-ModuleLocation' {
      remote 'http://fuga/svn/trunk'
      local 'fuga'
    }
    it / locations << 'hudson.scm.SubversionSCM_-ModuleLocation' {
      remote 'http://baz/svn/trunk'
      local 'baz'
    }
  }
}

ロケーションの1個目だけを特別扱いして書いて、あとは無理やり設定XMLまんま書けとかイケてないですね。

せっかくプログラミング言語を使っているので、関数に出してすっきりさせてあげましょう。

def svnRepositories = { repositories ->
  svn(repositories[0].remote, repositories[0].local) {
    if(repositories.size == 1) return
    for(repos in repositories[1..-1]) {
      it / locations << 'hudson.scm.SubversionSCM_-ModuleLocation' {
        remote repos.remote
        local repos.local
      }
    }
  }
}

// ...

scm {
  svnRepositories([
    [remote: 'http://hoge/svn/trunk', local: 'hoge'],
    [remote: 'http://fuga/svn/trunk', local: 'fuga'],
    [remote: 'http://baz/svn/trunk', local: 'baz'],
  ])
}

Groovy的にいいコードなのかどうかわかりませんが、適当に上記のようにしてみました。

しかし、これを実行すると下記のように例外が発生してしまいます。

Exception in thread "main" groovy.lang.MissingMethodException: No signature of method: sample.svn() is applicable for argument types: (java.lang.String, java.lang.String, sample$_run_closure1_closure3) values: [http://hoge/svn/trunk, hoge, sample$_run_closure1_closure3@279fedbd]
Possible solutions: run(), run(), any(), find(), wait(), dump()
    at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptBytecodeAdapter.java:55)

まあ当然といえば当然なのですが、GroovyのDSLと呼ばれているものは、DSLを書く場所に実行時、あらかじめコンテキストオブジェクトがはりついており、コンテキストオブジェクト上のメソッド―上記だとsvn(...)を呼ぶことによって実装しているわけですね。

ところが外部関数にいくと関数呼び出し元のコンテキストもへったくれもない。見えないというわけです。さあて困った。

なので、下記の苦しい呪文をsvnRepositories([...])の前に挟みます。

scm {
  svnRepositories.delegate = delegate // 苦しい呪文
  svnRepositories([
    [remote: 'http://hoge/svn/trunk', local: 'hoge'],
    [remote: 'http://fuga/svn/trunk', local: 'fuga'],
    [remote: 'http://baz/svn/trunk', local: 'baz'],
  ])
}

この苦しい呪文より、うまくいくようになります。同様にDSL内で関数くくりだしをした場合は上記呪文を挟めばいけます。

delegateでコンテキストオブジェクトを参照し、さらに関数の同名のプロパティにあらかじめ設定することで、その関数内のコンテキストオブジェクトを書き換えることができるので、関数呼び出し元のコンテキストオブジェクトを伝播させてやっているわけですね。正直Groovyはまったく詳しくないので、厳密にどうなっているのか知りませんが。)

WindowsでEmacsのClojure開発環境を立ち上げる

Windowsで最低限のEmacs+Clojureの開発環境を立ち上げる手順を書いてみました。

対象読者は、あやふやです。なんとなくEmacsClojureもやったことがある、ぐらいな感じ。

ちなみに全般的にWindowsでこの手のUnix世界に塗れたものを開発するのは地獄なので、LinuxOS Xという選択肢を選べる人は素直にそちらを選んだほうがいいでしょう。

まずはChocolateyを入れる

今や常識、Windowsのパッケージ管理ツール、Chocolatey(https://chocolatey.org/)を入れましょう。

そこまでこだわりのないパッケージであればChocolatey経由でインストールしたものでまかなえます。

基本的なツールを入れる

curlwget を入れておきましょう。

choco install curl wget

MsysGit(https://msysgit.github.io/)もChocolatey経由じゃなくてもいいですが、入れておきましょう。Chocolateyでは以下。

choco install git

Clojureの実行環境を作る

http://leiningen-win-installer.djpowell.net/ というのもあるみたいですが、使いません。

まずはJDK8をインストール。

choco install jdk8

次にLeiningen(Clojureのビルド兼実行ツール)をインストール。leiningenのインストール時、お呼びでないJDK7も入れようとするので -ignoreDependencies を指定しておきます。

choco install lein -ignoreDependencies

このままだと多分Leiningenが古いので、仕上げに下記コマンドを打っておきましょう。

lein upgrade
lein self-install

JDKもLeiningen自分で入れるんじゃーい!と思った方は、OracleからJDKダウンロードしてインストールして、Leiningenも公式サイト(http://leiningen.org/)からlein.batをダウンロードしてパスを通せばOKです。

最後に下のコマンドでREPLに入れればOKです。

lein repl

Emacsを入れる

EmacsはEmacs24以上を使います。色々Windows用にビルドはあるのですが、それぞれ長所短所があり、決定打というものがありません。

今回はemacs-w64(Windows x64用のEmacs25)を使いたいと思います。おそらく、日本語入力は辛いと思うので日本語は使わない想定で… (改行コードの扱いもあやしい気がするが…)

普通に解凍して、 bin フォルダにパスを通します。起動時は runemacs.exe を使うといいでしょう。

EmacsClojureを開発できる環境を作る(Caskを入れる)

Emacsといえばその拡張性の高さが有名ですが、拡張性が高いだけで最近までいい拡張の管理方法がありませんでした。

しかし今どきはCask(https://github.com/cask/cask)というツールがあり、これにパッケージ依存性を書いたファイルを食べさせると自動でEmacsにパッケージをインストールしてくれます。

基本的にCaskはUnix上じゃないとどうも怪しい動きをしているのですが、Windowsで使えないことはありません。インストールしてみましょう。

まず、Caskの動作のためにChocolateyでPython2を入れます。残念ながらPython3は不可です。

choco install python2

(Python3と同居させたい人には、pyenvというものがWindows以外ならありますが、 https://github.com/yyuu/pyenv/issues/62Windowsだとどうすればいいんでしょうね…)

次に公式サイトに載っているコマンド(http://cask.readthedocs.org/en/latest/guide/installation.html#manual-installation)でCaskをインストールします。

curl -fsSL https://raw.githubusercontent.com/cask/cask/master/go | python

上記コマンドで %userprofile%\.cask\bincask および cask.bat が用意されます。( %HOME% 変数を定義している人は %userprofile% のかわりに、そこを探しましょう) この bin フォルダにパスを通しておきましょう。

その状態で、 %userprofile%\.emacs.d フォルダ(なければ作成)上で下記コマンドを実行し、Caskの初期ファイルを作成します。

cask init

上記コマンドで出来た Cask ファイルに下記内容を追記しましょう。Emacsでcider(Clojureの開発用)とparedit(Lispのカッコ編集用)という拡張に依存するという内容です。

(depends-on "cider")
(depends-on "paredit")

そのまま同じフォルダでCaskに依存している拡張を .emacs.d フォルダ内にインストールさせます。

cask install

なんかこのコマンド、Windows上だと一瞬で終了しちゃいますが、非同期で結果がバカバカ吐かれるので、ちょっと待ちましょう。

(この際、Emacs24から使えるGnuTLSのサポートが無いEmacs(公式配布とか)を使っていると、Caskに文句言われます。)

上記インストールが終わったら、今度は %userprofile%\.emacs.d\init.el (Emacsが起動時に読むユーザーカスタマイズ用のファイル) に下記内容を追記します。

(add-to-list 'load-path "~/.cask")

(require 'cask)
(cask-initialize)

(show-paren-mode 1)

(require 'paredit)
(add-hook 'emacs-lisp-mode-hook #'enable-paredit-mode)

(require 'cider)
(add-hook 'clojure-mode-hook #'enable-paredit-mode)
(add-hook 'cider-repl-mode-hook #'enable-paredit-mode)

追記が終わったら、 runemacs.exe を実行してみましょう。エラーなく起動すればOKです。

試しに、Clojureのプロジェクトを作ってみる

簡単なClojureのプロジェクトを作成してREPLを開いてみましょう。

どっか作業用のフォルダ上で、下記コマンドを打ち、Clojure開発用の空のプロジェクト(my-test)を作成します。

lein new my-test

できた my-test フォルダ内の project.clj ファイルを開いて、 C-c M-jキーを押せばプロジェクトに対応したClojureのREPLを立ち上げることができます。

src フォルダの中にひな形のソースファイルが入っているので C-x C-e(カーソルの前の式を評価) などを使ってREPLに投げて遊んだりしましょう。

EmacsのCIDERとLeiningenのCIDERのバージョンが合わないエラー

EmacsのCaskで入れたEmacs側のCIDERのバージョンと、LeiningenがダウンロードしたJava側のCIDER(cider-nrepl)のJARのバージョンがあってねえよと文句言われることが多々あると思います。

CIDER's version (0.8.2-snapshot) does not match cider-nrepl's version (0.8.1)

ちゃんとLeiningenが持ってくるJava側のcider-nreplのバージョンをEmacsのCIDERにあわせてあげましょう。

LeiningenのユーザごとのプロファイルにCIDERのバージョンを指定してあげます。下記のいずれかのファイルを編集します。

  • %LEIN_HOME% を定義済みならば %LEIN_HOME%\profiles.clj
  • それ以外の場合は、 %userprofile%\.lein\profiles.clj ( 多分%HOME%は見てないかも )

ファイルに下記内容を書きます。

{:user
 {:plugins [[cider/cider-nrepl "0.8.2-SNAPSHOT"]]}} ; ←ここをEmacs側のバージョンにあうように変更しましょう!

ちょっと管理上よろしくないですが、上記のプラグインの設定を各プロジェクトの project.clj:plugins に追記してもいいです。

試しに Clojure+ClojureScript (+Facebook ReactのラッパーのOm) のプロジェクトを作ってみる

ちょっとだけ背伸びをしてみましょう。プロジェクトテンプレートとして、plexus/chestnut(https://github.com/plexus/chestnut)を使います。

どっか作業用のフォルダ上で、下記コマンドを打ち、ClojureScriptを開発する雛形のプロジェクト(my-test)を作成します。

lein new chestnut my-test

その中の project.clj ファイルをEmacsで開いて C-c M-j キーを押してプロジェクトに対応したClojureのREPLを立ち上げます。

REPLはデフォルトの名前空間( my-test.server )で起動していると思いますが、そこで (run) 関数を起動してWebサーバを起動します。

; CIDER 0.8.2snapshot (package: 20141130.803) (Java 1.8.0_25, Clojure 1.6.0, nREPL 0.2.6)
my-test.server> (run)
Starting figwheel.
Starting web server on port 10555 .
#<Server org.eclipse.jetty.server.Server@e7a73f8>
my-test.server> Compiling ClojureScript.
Figwheel: Starting server at http://localhost:3449
Figwheel: Serving files from '(dev-resources|resources)/public'
Compiling "resources/public/js/app.js" from ("src/cljs" "env/dev/cljs")...
WARNING: Use of undeclared Var cljs.core.async/do-alts at line 62 file:/C:/Users/owner/.m2/repository/org/clojure/core.async/0.1.278.0-76b25b-alpha/core.async-0.1.278.0-76b25b-alpha.jar!/cljs/core/async/impl/ioc_helpers.cljs
WARNING: Bad method signature in protocol implementation, impl/Handler does not declare method called lock-id at line 214 file:/C:/Users/owner/.m2/repository/org/clojure/core.async/0.1.278.0-76b25b-alpha/core.async-0.1.278.0-76b25b-alpha.jar!/cljs/core/async.cljs
WARNING: Use of undeclared Var cljs.core.async.impl.protocols/lock-id at line 217 file:/C:/Users/owner/.m2/repository/org/clojure/core.async/0.1.278.0-76b25b-alpha/core.async-0.1.278.0-76b25b-alpha.jar!/cljs/core/async.cljs
WARNING: Bad method signature in protocol implementation, impl/Handler does not declare method called lock-id at line 214 resources\public\js\out\cljs\core\async.cljs
WARNING: Use of undeclared Var cljs.core.async.impl.protocols/lock-id at line 217 resources\public\js\out\cljs\core\async.cljs
Successfully compiled "resources/public/js/app.js" in 13.856 seconds.
notifying browser that file changed:  /js/app.js
notifying browser that file changed:  /js/out/goog/deps.js
notifying browser that file changed:  /js/out/my_test/dev.js
notifying browser that file changed:  /js/out/my_test/core.js

上記出力が終わったら http://localhost:10555 を見ればClojureで立ち上がったWebサーバ上が見えると思います。この時のサーバのページの表示はClojureScriptで行われています。

今度はClojureScriptをいじってみましょう。REPL上で

(browser-repl)

でClojureScriptのREPLを起動してみましょう。これにより cljs.user 名前空間に切り替わるので、REPL上で

(js/alert "Hello!")

とか打ってみましょう。ちゃんとブラウザ上でalertが出たのが見えるでしょうか。

つづいて、Om(React)らしく、JS上のモデルを書き換えてページのDOMが書き換わるのを観察してみましょう。

src/cljs/my_test/core.cljs を開いて参考にしつつ、ファイルに下記式を書いて評価( C-x C-e)してみましょう。

(reset! app-state {:text "Goodbye Chestnut!"})

シームレスにブラウザ上のDOMがリレンダされたのがわかるでしょうか。

このまま Omのチュートリアル(https://github.com/swannodette/om/wiki/Basic-Tutorial)にすすんでみましょう。

公式チュートリアルはLightTable前提ですが、Emacsでもやっていけるはずです。

Jenkinsのビルドエグゼキュータ(ビルド状況)をREST APIで取得したい

個人的なメモ。

Jenkinsのビルドエグゼキュータは、たとえば https://ci.jenkins-ci.org/ajaxExecutors のように /ajaxExecutors 経由でJenkinsのウェブサイトに定期的に描画されているものですが、これはHTMLのスニペットを定期的に配信していることで実現していて、たとえばTwitter BootstrapのカッコイイUIでこのビルドエグゼキュータを描画したいといった場合、HTMLのスニペットの定期取得では難しいこともあるでしょう。

(以下はそのスニペット

<div id="executors" class="container-fluid pane-frame"><div class="row"><div class="col-xs-24 pane-header"><a title="collapse" class="collapse" href="/toggleCollapse?paneId=executors"><img style="width: 16px; height: 16px; " alt="collapse" class="icon-collapse icon-sm" src="/static/8e43c810/images/16x16/collapse.png" /></a><a href='/computer/'>ビルド実行状態</a></div></div><div class="row pane-content"><table class="pane "><colgroup><col width="30" /><col width="200*" />

<!-- 以下略 -->

ということで、Jenkinsに備え付けられているREST APIから、ビルドエグゼキュータの情報を取ろうと考えつくわけですが、どこを取ればいいかわからない。

まあ正解からいうと、 https://ci.jenkins-ci.org/ (公式Jenkins)を例にとったとして、 https://ci.jenkins-ci.org/computer/api/json?pretty=true を取ればいいんですが、エグゼキュータ描画に必要な情報が入っていない。( ?pretty=true は実装時には要らないです)

なので、より深い情報を取るために https://ci.jenkins-ci.org/computer/api/json?pretty=true&depth=2 などと depth=2 を指定すりゃあいいんですが、

  • 余計な情報つきすぎ
  • ノードをON/OFFしていると例外をBeanからシリアライズできないとかで500出る(バグ?)

といった苦難にあうので、きちんと取るべき情報を指定してあげましょう。

https://ci.jenkins-ci.org/api/ に書いてあるんですが、RESTで取る際のデータはある程度選べます。

今回のビルドエグゼキュータを自分で描画したいといった場合、JSON上の下記の構造の部分だけデータが取れれば、まあだいたい僕の欲望は満たせました。

  • computer (複数) スレーブごとに存在
    • displayName スレーブの名前
    • executors (複数) スレーブのエグゼキュータ(キュー)ごとに存在
      • progress ジョブの進捗%
      • idle 空いているか?
      • currentExecutable 実行中ビルドの詳細
        • estimatedDuration ジョブの完了見積もりミリ秒
        • fullDisplayName ビルドのフル名
        • url ビルドのURL

上記のデータだけを取る場合は tree パラメータを駆使して、 https://ci.jenkins-ci.org/computer/api/json?pretty=true&tree=computer[displayName,executors[progress,idle,currentExecutable[estimatedDuration,fullDisplayName,url]]] とすればよいです。前までつけていた depth は不要になります。

下記に(Lisp並みに)わかりやすく分解したURLを置いておきます。

https://ci.jenkins-ci.org/computer/api/json?pretty=true&tree=
    computer[
        displayName,
        executors[
            progress,
            idle,
            currentExecutable[
                estimatedDuration,
                fullDisplayName,
                url]]]

上記のURLで下記のようなJSONがとれるので、後は定期的に再取得しなおしたり、再取得するまでの間は estimatedDurationprogress からビルドの予定時間を算出してスムーズにプログレスバーでビルド進捗率を描画するなりすればいいと思います。

{
    "computer" : [
    {
      "displayName" : "master",
      "executors" : [
        {
          "currentExecutable" : {
            "estimatedDuration" : 8807897,
            "fullDisplayName" : "infra_update_center_v3 #57",
            "url" : "http://ci.jenkins-ci.org/job/infra_update_center_v3/57/"
          },
          "idle" : false,
          "progress" : 81
        },
        {
          "currentExecutable" : null,
          "idle" : true,
          "progress" : -1
        }
      ]
    },
    {
      "displayName" : "celery",
      "executors" : [
        {
          "currentExecutable" : null,
          "idle" : true,
          "progress" : -1
        },
        {
          "currentExecutable" : null,
          "idle" : true,
          "progress" : -1
        }
      ]
// 以下略

ちなみになんでこんなことを僕が調べているかというと、開発サーバ上で動くJenkins数台などを巻き込んだSPAをAngularJSとか使って作っているからです。

Java8対応のCheckstyle5.9が出ています

checkstyle – Checkstyle 6.0

Java8に対応したCheckstyle5.9が出ています。(リリースノート: checkstyle – Release Notes

→ (2014/10/27 追記) 6.0 が出てました。はやすぎっ

これでFindbugsCheckstyleと2大伝統的JavaインスペクションツールがJava8対応したわけで、だいたいの人はJava8に来れますね。

ちなみにEclipseプラグインの方は、まだ5.8っぽいです。 Eclipse Checkstyle Plug-in / Feature Requests / #145 Upgrade to checkstyle 5.9

→ (2014/10/29 追記)5.9対応のEclipseプラグインがリリースされています。

Dropboxの1TBのやつ使い始めた

Dropboxの1TBプランを契約して使い始めました。

もともとDropbox自体は使ってなかったのですが、1TBプランが月1200円ではじまったのを聞いて契約してみました。

私がよく愛用しているGoogle Driveも安価に1TBプランもあって、実はそっちも一度契約したんですが、Google DriveWindowsアプリが一度に1アカウントしか接続できないこともあり、仕事用アカウントと両立できないのですぐに契約ストップさせました。(1か月の払い損ですね)

あとGoogleはメールも併用していることもあり、たとえばファイル側の容量がオーバーフローしたらメールどうしようとか考えてしまい、クオータがあればいいなあ、とも思ったり。

暗号化ドライブをDropboxに入れる ~ BitLocker+VHD(VHDX)

で、バックアップされておいてほしいファイルをバンバカ入れていくんですが、AES 256bitで暗号化されているらしいとはいえ、ちょっと生でおきたくないファイルがいくつかあって、それを置くのにどうしようかなあと考え、すぐにTrueCryptを思い出したんですが、開発してるんだか怪しいので、使いたくなくないなーと思って調べたら、TrueCrypt公式にBitLocker使えばいいよーとのことが書いてあったので、

  1. Dropboxの共有フォルダ内にVHDXドライブイメージを作って(VHDに比べてWindows8.1以降で使えるVHDXはスパース可(可変長)です。)
  2. ドライブイメージを接続して
  3. 繋がった仮想ディスクにパーティション作ってNTFSで初期化して
  4. ドライブとしてマウントして
  5. マウントしたドライブをBitLockerでパスワードかければ

無事、暗号化された仮想ドライブのファイルが出来上がります。

200GBぐらいのファイルを作ったんですが、数日ほっておいたらDropboxが無事同期完了させてくれていました。

で、ドライブの中身を変更したら、また200GBを全部同期しなおしか、と思うところですが、そこはDropboxの底力、ちゃんと差分のみを送信してくれます。

こんな感じで快適バックアップライフを送っています。

だいたいの人は大事なファイルが1TB内におさまるはずなのでお試しあれ。

JMockit+JaCoCo トラブルあれこれ

JMockitはテストのために一部のクラスの定義を書き換えるライブラリ、JaCoCoはカバレッジ取得用のライブラリ、どちらも昨今のJava開発のユニットテストで(多分)よく使うものです。

Antで両方を使ったサンプルプロジェクトも用意してみました。 (ライブラリダウンロードのためにはじめてIvyも使ってみた)

https://github.com/knjname/jmockit-jacoco-javaagent-arg-order

JMockitもJaCoCo、どちらもinstrumentationという仕組みを用いてその機能を実現しています。

http://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html

instrumentationというのは、超簡単にいえば、Javaでロードするクラスの中身をロード前に書き換える仕組みです。

instrumentationを使用するには、下記のようにJVM起動時にinstrumentation用のクラスとマニフェストを内包したJARを指定してあげればOK。

java -javaagent:instrumentationに使うJARのパス ほかの引数...

上記のような指定で、-javaagent:~~ に指定されたJARのマニフェストにあるPre-Mainという項に書かれたクラスのpremainメソッドが起動し、そのメソッドの中でクラスの定義を書き換えるクラスがJVMに登録されます。

ま、ここらへんの詳しい仕組みはJMockitなど使う上ではどうでもいいです。

-javaagent複数指定できるので、たとえばJMockit、JaCoCo双方を使う場合は、jmockitのJAR、JaCoCoのJAR、どちらも指定してあげれば、どちらも有効になります。

java -javaagent jmockit.jar -javaagent jacocoagent.jar ほかの引数...

JMockit+JaCoCoがEclipse(EclEmma Java Code Coverage)で動かないよう!

上記解説でJVM起動時にjmockit.jarとjacocoagent.jarを両方指定しましたが、これには正しい指定順序(JMockit→JaCoCo)があり、これが逆(JaCoCo→JMockit)だと下記のようなエラーが出てしまいます。

java.lang.LinkageError: loader (instance of  sun/misc/Launcher$AppClassLoader): attempted  duplicate class definition for name: "knjname/AddRandomTest$1"
     at java.lang.ClassLoader.defineClass1(Native Method)
     at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
     at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
     at java.net.URLClassLoader.defineClass(URLClassLoader.java:455)
     at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
     at java.net.URLClassLoader$1.run(URLClassLoader.java:367)
     at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
     at java.security.AccessController.doPrivileged(Native Method)
     at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
     at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
     at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
     at knjname.AddRandomTest.testAddRandom(AddRandomTest.java:16)
     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
     at java.lang.reflect.Method.invoke(Method.java:483)
     at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
     at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
     at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
     at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
     at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
     at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

JaCoCoが先行してクラスロード部分に邪念を送ったことにより、JMockitのモック化のためのゴニョゴニョを邪魔してるんですね。

まあ、普通にAntで起動する場合は下記のようにjacocoのAntタスク使い、その場合はこの現象にあうことはないのですが

<jacoco:coverage destfile= "target/coverage.exec">
  <junit fork= "true">
    <jvmarg line= " -javaagent:lib/jmockit-1.11.jar " />
<!-- 以下略 -->

EclipseでEclEmmaとかいうJaCoCoでカバレッジを取得するプラグイン(http://www.eclemma.org/)を使って普通にJMockitを使用したテストを起動するとプラグインの動作で内部的にJaCoCoのJARが先行してしまい、上記のエラーが出てしまいます。

このエラーはEclipse上のカバレッジ取得アクション(Coverage As)起動時にプラグイン内部でたぶん -javaagent:jacocoのJAR を先頭に追記したせいで発生しているようで、JUnit起動構成のVM引数設定を編集して、-javaagentを追加したりなどしても無駄みたいです。

これを解決するには、JaCoCoの指定よりJMockitの指定が先行しさえすればいいので、

  • Eclipseの設定 -> Java -> Installed JREs から
  • JUnit用のJREを新規構成して
  • その構成のDefault VM argumentsに-javaagent:jmockitのJARのパス を追記し、(-javaagent:${workspace_loc}/プロジェクト名/lib/jmockit.jar のようにワークスペースまでの変数とかいれると便利)
  • その構成のJREJUnitコードのプロジェクトのJREライブラリに指定してあげれば

無事に動くと思います。

ちなみに私はこれに半日ハマっていました。酷い。

よその環境で何故かJMockitが動かないよう!

上記のEclipseとは別に、なんでかローカルで動いていたはずのJMockitさんが別Java環境で動かないことがあります。

java.lang.IllegalStateException : Native library for Attach API not available in this JRE
      at mockit.internal.startup.AgentLoader.getVirtualMachineImplementationFromEmbeddedOnes(AgentLoader.java:96)
      at mockit.internal.startup.AgentLoader.loadAgent(AgentLoader.java:50)
      at mockit.internal.startup.AgentInitialization.loadAgentFromLocalJarFile(AgentInitialization.java:29)
      at mockit.internal.startup.Startup.initializeIfPossible(Startup.java:212)
      at org.junit.runner.Runner.<clinit>( Runner.java:22)
      at org.junit.internal.builders.JUnit4Builder.runnerForClass(JUnit4Builder.java:10)
      at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:59)
      at org.junit.internal.builders.AllDefaultPossibilitiesBuilder.runnerForClass(AllDefaultPossibilitiesBuilder.java:26)
      at org.junit.runners.model.RunnerBuilder.safeRunnerForClass(RunnerBuilder.java:59)
      at org.junit.internal.requests.ClassRequest.getRunner(ClassRequest.java:26)
      at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.<init>(JUnit4TestReference.java:33)
      at org.eclipse.jdt.internal.junit4.runner.JUnit4TestClassReference.<init>(JUnit4TestClassReference.java:25)
      at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.createTest(JUnit4TestLoader.java:48)
      at org.eclipse.jdt.internal.junit4.runner.JUnit4TestLoader.loadTests(JUnit4TestLoader.java:38)
      at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:444)
      at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
      at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
      at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Caused by: java.lang.UnsatisfiedLinkError: no attach in java.library.path
      at java.lang.ClassLoader.loadLibrary(Unknown Source)
      at java.lang.Runtime.loadLibrary0(Unknown Source)
      at java.lang.System.loadLibrary(Unknown Source)
      at sun.tools.attach.WindowsVirtualMachine.<clinit>(WindowsVirtualMachine.java:185)
      at sun.reflect.NativeConstructorAccessorImpl.newInstance0( Native Method)
      at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
      at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
      at java.lang.reflect.Constructor.newInstance(Unknown Source)
      at mockit.internal.util.ConstructorReflection.invoke(ConstructorReflection.java:68)
      at mockit.internal.util.ConstructorReflection.newInstance(ConstructorReflection.java:37)
      at mockit.internal.startup.AgentLoader.getVirtualMachineImplementationFromEmbeddedOnes(AgentLoader.java:92)
      ... 17 more

これは、前の例で示していたような -javaagent:jmockitのJAR を指定しないと発生することがあります。

-javaagent:の指定がないし、今ここでそれ相当のを動的追加しよっか、ってことでAttach APIというのを探して使おうとして、なかったのでエラーということみたいです。

でもこれ確か最近のJavaじゃ指定しなくていいってマニュアルに書いてたはずじゃ?

Depending on your development environment, you may have to do one other thing:

  • If you are developing on JDK 1.5, then make sure that -javaagent:jmockit.jar (with the proper absolute or relative path tojmockit.jar) is passed as an initialization parameter to the JVM when running tests. This standard JVM initialization parameter causes it to load on start-up the "Java agent" that JMockit uses internally for bytecode instrumentation; this is required to work in all standard JVMs since version 1.5, in all OSs. You may have to use this parameter even on a newer JDK 1.6+, if its Attach API implementation is not supported by JMockit: such is the case with the IBM J9 JDK 1.6, the Mac OS X JDKs, and with JDKs for the Solaris OS.

  • If you use TestNG in a JDK 1.6+ environment, JMockit can be initialized in one of three possible ways (apart from use of "-javaagent" indicated above, which can also be used). See this page for details.

When using a HotSpot JDK 1.6+ or a JRockit JDK 1.6+ on Windows or Linux, neither of the extra steps above is necessary.

http://jmockit.googlecode.com/svn-history/r1166/trunk/www/installation.html

はい。最後。

When using a HotSpot JDK 1.6+ or a JRockit JDK 1.6+ on Windows or Linux, neither of the extra steps above is necessary.

(超訳:JDK1.6以上は上記の追加手順はどちらも不要。)

と、こういうことなのですが、これが適用可能なのは厳密にいうと、Attach API(http://docs.oracle.com/javase/8/docs/technotes/guides/attach/index.html)というものが使える環境限定の話らしく、JREJavaでは使えませんし、JDKを指定したつもりでもなんか使えない場合もありました。(Jenkins上とか。なんでなんだろう?)

Attach APIが利用可能であるには、具体的にはjavaが attach.dll (Windows) / attach.so (Linux) をライブラリとして参照可能である必要があるみたいです。

Attach APIに頼らずに無難にどこの環境でもJMockitを使えるようにするには -javaagent:JMockitのJAR は付けておいて損はないと思います。ビルドスクリプトとか。

これも私はハマってました。

っていうか、Attach APIって何?

-javaagent: 相当のAgentClassというやつをVM起動後に動的に追加できることもできるAPIと覚えればOK。出自はJava6から。

http://docs.oracle.com/javase/8/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html

VirtualMachineクラスのloadAgentメソッドにAgent-Classの指定があるJARのパスを渡せば、動的に -javaagent: 相当のクラス定義変換をVMに組み込むことができます。

JMockitみたいなのを作るには、こういうAPIを使って、あらゆるクラスについて好きなタイミングで、挙動を変えられるアスペクト的なバイトコードを全クラスに仕込むようにしておけば作れそうですね。

ほかJMockitの罠

  • クラスパス上、JMockitのJARがJUnitのJARより先行しなければならない。
  • EclipseJUnitプラグインJUnitだと動かないかも(JUnitは明示的に持ってくる派なので遭遇したことはないが
  • junitはfork="true"じゃないとJavaAgent指定する系は動かない。

まあ、一度動けばJMockitもJaCoCoも便利ですよ。

2014-09-01 追記

最近のマニュアル(←まずそっち読めよ)だと

JMockit - Tutorial - Running tests

To run tests that use any of the JMockit APIs, use your Java IDE, Ant/Maven script, etc. the way you normally would. In principle, any JDK of version 1.6 or newer, on Windows, Mac OS X, or Linux, can be used. JMockit supports (and requires) the use of JUnit or TestNG; details specific to each of these test frameworks are as follows:

For JUnit 4.5+ test suites, make sure that jmockit.jar appears before the JUnit jar in the classpath. Alternatively, annotate test classes with @RunWith(JMockit.class).

(Note for Eclipse users: when specifying the order of jars in the classpath, make sure to use the "Order and Export" tab of the "Java Build Path" window. Also, make sure the Eclipse project uses the JRE from a JDK installation instead of a "plain" JRE, since the latter lacks the "attach" native library.)

For TestNG 6.2+ test suites, simply add jmockit.jar to the classpath (at any position).

と書いてあるみたいで、Java6以降使ってね、ってのとjarの指定がアレならRunWithでも動くし、JREを使った場合はAttach APIがないから動かないよと書いてますね。

僕としては何かトラブルがあったら、-javaagent を指定することを薦めたい。

Lispのカッコは怖くないよ

最近Lispの連れション仲間を増やしたいので、いろんな初見の人に「Lispって知ってる?」と質問して回っています。

そこそこアンテナのある技術者ならLispというのがプログラミング言語の一派を意味しており、それが主に大量のカッコで構成されていることは知っているようなのですが、なんか拒否反応が多いんですよね。

拒否反応というのが、まあ、だいたい

  • 「カッコが多すぎて気がおかしくなる」
  • 「私の人生は大量のカッコに対応するには短すぎる」
  • 「大学でやったけどカッコ死ね。」

みたいなHTML初心者がタグのネストに敗北したみたいな感想ですね…。

まあ、パッと見てそういいたくなる気持ちは分かるんですが、それ自体がよくあるLispに対する誤解と言わざるを得ないです。

事実、Lispプログラミングは大量のカッコを相手にするのですが、誰もカッコの個数なんて見ていません。

Lispのプログラムの構造を読むときは、インデントしか見てません。

カッコを読む側の話

Lispプログラムの例として、下記のregexp-opt-depthという関数(Emacsregexp-opt.el)を見てください。

(defun regexp-opt-depth (regexp)
  (save-match-data
    (string-match regexp "")
    (let ((count 0) start last)
      (while (string-match "\\\\(\\(\\?[0-9]*:\\)?" regexp start)
        (setq start (match-end 0))
        (when (and (not (match-beginning 1))
                   (subregexp-context-p regexp (match-beginning 0) last))
          (setq last start)
          (setq count (1+ count))))
      count)))

Javaなどであれば { やら } などなどもうちょっと可愛げのある記号が出てくるのに、特徴も糞もない大量のカッコとアルファベット+記号の羅列でめまいがしてきますね。

海千山千と言われるLisperはこのカッコの対応を一瞬で読み解ける超人ばかりなのでしょうか?

もちろんNoです。

上記のようなLispプログラムを読む場合は、普通は下記のように思考しながら読んでいきます。(各行の;; 以降が思考内容です。)

;; defunがあるから、関数定義だなあ。
;; その次には関数名がくるはず。regexp-opt-depthという関数の定義をするんだなあ。
;; regexpという引数一つをとるんだなあ。
(defun regexp-opt-depth (regexp)

  ;; ネストされたところからカッコが始まっている。
  ;; 関数の処理の本体がここから始まるんだなあ。
  ;; save-match-dataを使っているんだな。
  (save-match-data

    ;; さらにネストされた位置にstring-matchの呼び出しがあるなあ。
    (string-match regexp "")

    ;; string-matchが終わったらletを呼び出すんだなあ。
    ;; letは変数の定義だからcountが0、startとlastは定義だけしてるんだなあ。
    (let ((count 0) start last)

      ;; 変数の定義(let)の中にwhileの呼び出しがあるなあ。whileはループ文だなあ
      ;; (string-match… 部分がループ条件か。
      (while (string-match "\\\\(\\(\\?[0-9]*:\\)?" regexp start)

        ;; こっからwhileのループ処理本体だなあ。
        ;; setqは変数代入だなあ。start変数の書き換えかあ。
        (setq start (match-end 0))

        ;; ループ処理の最後にwhenで条件を満たした場合のみの処理を書いているなあ。
        ;; インデント的に(and (not …) (subregexp-context-p ...))が条件か。
        (when (and (not (match-beginning 1))
                           (subregexp-context-p regexp (match-beginning 0) last))

          ;; 条件満たしたらsetqを2つやるんだなあ。
          (setq last start)
          (setq count (1+ count))))

      ;; インデントから見て、letの直下の処理で、最後にcountを返却しているんだなあ。
      count)))

上記の読み方のどこにも遠く離れたカッコの個数や対応を数える兆候が見られないことに注目してください。

カッコの対応を知るのにカッコの数ではなく、(が出現するインデント位置しか見ていません。

(局所的に近場のカッコの1つ2つは数えています。ただそれぐらいは通常の人間ができる芸当の範疇だと思います。)

このようにLispプログラムはインデントでしか読まないのが当たり前なので、逆にLispプログラムを書く場合はインデントで構造が読めるように配置しなければ相当読みづらいといえるでしょう。(普通はエディタが勝手にインデントとってくれます)

開きカッコのインデント位置は気にしますが、閉じカッコといえば、上記のインデントを守っていればただの開きカッコのつじつま合わせにしか過ぎないので、一箇所に集められるだけ集められているのがわかるでしょうか? count)))) などとなっている部分がそうですね。

このようにLispプログラムの構造は、構文上の絶対の規制はないものの、インデントで視覚的に示します。考え方はPythonHaskellCoffeeScriptと同じですね。JavaやCでもまともなプログラムなら構文に沿ってインデントをつけるはずです。

カッコを作る側の話

で、インデントで構造を示すのはいいんですが、インデントなぞただの努力目標、本質的にプログラムの構造はカッコの対応で示されます。(意地悪でカッコに反するインデントをすることも可能です。)

海千山千のLISPerはこれら大量のカッコをいちいちバランスとれるように開きの数と閉じの数を数えながら、Lispプログラムを編集しているのかというと、もちろんNoです。エディタの機能でどうにかします。

普通はpareditというカッコのバランスとったままカッコ単位の各種編集ができる便利なEmacsマクロを使います。これによりほとんどカッコの対応を崩さずに編集可能になります。逆に言うとこれがないとLispなんてやってられません。

Lispではすべてがカッコなので、pareditで好きな単位の式を綺麗に抜き取ることも可能です。

カッコ単位の編集に特化している感じですね。

カッコになっていれば、全部同じ方法で編集できるので、Lisperにとってすべてがカッコの世界はとてもハッピーかもしれません。

まとめ

カッコは怖くないよ!!

2014-08-05 追記

すごいブックマークついてる!!

Lispのカッコよりみなさんの忌憚なきコメントのほうがLisp初心者の僕にとっては怖いですね。

nazoking ((はてなブログで((((gyazo)に置かれた(gifアニメ)のURL)をツイッターでシェア)したツイート)を貼り付け)していて、結果gifアニメが動いていないという悲劇

すんません次からはちゃんとやります

Lispの魅力伝えられてないよ

まず、カッコごときでLispのことを食わず嫌いにならないで欲しいと思いました。

マクロとかは後でいいかと。できればハッカーと画家でも読んでもらえれば。

countの後ろのかっこの数

これは普通にカッコの釣り合いをとってくれるエディタで書いていけば、開き-閉じかっこの数なんかはずっと調整が取れている状態になり、気にしなくてもちゃんとなるようになってます。

Pythonとかでいいじゃん

今回書いたのは単にLispのカッコを恐れる必要がない理由だけですので、PythonLispと同じ土俵に立っているわけではありません。

マクロ機能はもちろん、各Lisp系言語ごとの目玉機能も違いがありますからね。

Lispの利点は他の言語で構文木取り扱う場合にASTパーサ持ち出したり、プリプロセッサだの普段使わない道具を持ってこざるをえなかったりしなきゃいけないといったことがないことです。以前のC++のように>>がテンプレート構文としてダメだどうかとかもありません。これはひたすらカッコしかないお陰です。

ひたすらカッコしかないお陰で、カッコ編集機能だけでいろんなLisp系言語(Common LispSchemeClojure、EmacsLisp)をカバーできたりします。これは他の言語には真似できないところでもあるし、永遠にカッコから脱却できない理由でもあるんじゃないでしょうか。

まあ、本質的には木構造的な何かが表せられればいいので、別にカッコじゃなくてもいいと思います。(編集は面倒そうだが)

emacs

今回はEmacsのpareditを紹介しましたが、vimや他IDEでもたぶんまともに編集できる環境はあるはずです。

LOL(let over lambda - http://letoverlambda.com/ )の人はvimで編集しているらしいです。

Clojure

にゃんぱすー!

tanimina: cf. 「S式の読みやすさ」http://blog.practical-scheme.net/shiro/20120823a-s-expression

このエントリの意味ないやん…

Shiroさんのサイトには全ての答えがありますね…

読み方がわかった、怖くなくなった、なるほど など

あなたたちを待っていました!

ほかみなさん

コメントありがとうございました。

ちなみに、私はもっぱら何か考える時はCoffeeScriptで考えます。

ま、普通に 1+2 とか i++ とか書きたいし、関数は (x) -> (y) -> x + y とかすっきり書けるし、分配もあるし、リスト内包記法みたいなのもあるし?

しかしCoffeeScriptでインデントがとち狂い始めた時、Lispのほうが私の木構造への意図がコードに残り続るし、カッコの編集楽でいいなあと思ったりします。

っていうかいろんな言語がわざわざASTとか作んないでさあ、S式に一度なってくれればいいんだよねー。