Grails2.0.2以降のbinding機構にハメられた件

ハマった。

Grails2.0.0で実装途中だったプロダクトを2.0.3に上げてみたらテストが通らなくなった。

半端なバージョンだけれども、例のgithubでもあったRailsの脆弱性と同じ問題に対処するために、Grails2.0.2からコントローラ上でhoge.properties = params や new Hoge(params)としたときにすべてのプロパティをがっつり反映しないように制御できるようになっています。
http://blog.springsource.org/2012/03/28/secure-data-binding-with-grails/

transientsなプロパティは明示的にbindable:trueしないと、そのプロパティはバインドされないようになってました。

In Grails 2.0.2 the data binding mechanism will by default exclude all properties which are static, transient or dynamically typed.

前述のブログにしっかり書いてた。staticとtransientと動的型はデフォルト除外(bindable=false)だそうな。

一度きちんと整理しないといけないなーとは思うけど、とりあえず試した結果をはっておく。

bindable:trueを設定した場合、これはグリーン

class Book {
    String name
    String author
    String transientField

    static transients = ['transientField']

    static constraints = {
        transientField bindable:true
    }
}
import grails.test.mixin.TestFor
import org.junit.Test

@TestFor(Book)
class BookTests {

    @Before
    void setUp() {
        mockForConstraintsTests(Book)
    }

    @Test
    void constraintsOfTransientField() {
        def book = new Book(name: "NAME", author: "AUTHOR", transientField: "TRANSIENT!")

        assert book.name == "NAME"
        assert book.author == "AUTHOR"
        assert book.transientField == "TRANSIENT!" // 設定される
    }
}

bindable:falseを設定した場合、これはグリーン

class Book {
    String name
    String author
    String transientField

    static transients = ['transientField']

    static constraints = {
        transientField bindable:false
    }
}
import grails.test.mixin.TestFor
import org.junit.Test

@TestFor(Book)
class BookTests {

    @Before
    void setUp() {
        mockForConstraintsTests(Book)
    }

    @Test
    void constraintsOfTransientField() {
        def book = new Book(name: "NAME", author: "AUTHOR", transientField: "TRANSIENT!")

        assert book.name == "NAME"
        assert book.author == "AUTHOR"
        assert book.transientField == null // 反映されてない
    }
}

constraintsで明示指定しない=transientなプロパティのデフォルトbindable:false、これはグリーン

class Book {
    String name
    String author
    String transientField

    static transients = ['transientField']
}
import grails.test.mixin.TestFor
import org.junit.Test

@TestFor(Book)
class BookTests {

    @Before
    void setUp() {
        mockForConstraintsTests(Book)
    }

    @Test
    void constraintsOfTransientField() {
        def book = new Book(name: "NAME", author: "AUTHOR", transientField: "TRANSIENT!")

        assert book.name == "NAME"
        assert book.author == "AUTHOR"
        assert book.transientField == null // 反映されてない
    }
}

constraintsで明示指定しない=通常のプロパティのデフォルトbindable:true、これはグリーン

class Book {
    String name
    String author
    String transientField
}
import grails.test.mixin.TestFor
import org.junit.Test

@TestFor(Book)
class BookTests {

    @Before
    void setUp() {
        mockForConstraintsTests(Book)
    }

    @Test
    void constraintsOfTransientField() {
        def book = new Book(name: "NAME", author: "AUTHOR", transientField: "TRANSIENT!")

        assert book.name == "NAME"
        assert book.author == "AUTHOR"
        assert book.transientField == "TRANSIENT!" // 反映された(まあname, authorとかわらんのだけど)
    }
}

JavaOne Tokyo 2012でマサカリを膝に受けてきた

この4/4-4/5に日本で7年ぶりに開催されたJavaOne Tokyo 2012のBOFで登壇させていただきました。

JVM言語BOFというセッションで、名前だけ見るとなんだかよくわかりませんが、セッション概要をみると

JavaVM上で動作するさまざまなプログラミング言語 (Groovy、JRubyScalaなど) のコミュニティや興味を持つ個人が集まって、各言語のチュートリアルや最新情報、使用実績などの情報交換を行います。プレゼンテーション、各言語を使ったコーディング大会、ライトニング・トークなどの形式で、ユーザ間の交流を促すBoFとして開催し、言語を超えて活用が進むJavaVMの日本ユーザの裾野を広げます。飛び入り参加大歓迎、楽しい時間にしたいと思います。
http://www.oracle.co.jp/javaone/2012/timetable/index.html

という感じ。

要はGroovy, JRuby, Scalaの代表者が集まって

  • 各言語の特徴アピール
  • コーディング大会:
    • 3つのお題に対する各言語での実装コードの特徴アピール
    • DISりあいパネル
  • LT

ということをやりましょう、というセッションです。
トータル2時間15分の2コマぶち抜きのロングセッションです。


ポイントとしては、各言語に対する応援やtsudaり、disりも含めてハッシュタグを使ってリアルタイム集計を行い、最終的にどの言語が一番人気かを決定する、というところですね。disりdisりもlikeの内、ということでdisだろうと+1ポイント扱いです。


で、自分はGroovyのコーディング大会担当でした。

各言語の実装は↓にまとめられてます。
https://github.com/nahi/javaone2012-benchmark/blob/master/Implementations.md

Groovyの実装コードはこちら↓。
https://github.com/nobeans/javaone-tokyo-2012-jvm-bof

JRubyの@nahiさん曰く「みなさんおなじみの赤黒木」を使ったベンチマークとか個人的にアルゴリズムものが苦手な自分としてはすごく実装が大変だったし*1ベンチマーク取得のためにGradleタスクで実行したんですけどタスク実行に1分とかかかってて「ベンチのお題で1分とか絶対負けるよなーgkbr」とか思ってたら、@nagai_masatoさん作のベンチマークツールGBenchが良かれと思ってやってくれる素敵なウォームアップが遅いだけで実際の性能値としてはGroovy++の反則的な静的コンパイル技術によってピュアJavaに匹敵する結果をたたき出していた!とか、なかなかドラマチックな時間を過ごすことができました。

実際のベンチマーク結果はこんな感じ
f:id:nobeans:20120406153434p:image

Groovy++(紫の棒)、反則的にすごい!!一番左のピュアJava実装(青の棒)とほとんど同じ性能です。
でもノーマルGroovy(緑の棒)だって他の言語の実装からみて「たかだか2倍」ぐらいなので、事前に恐れていたほど遅くはないですよ(キリッ
それに今まさにGroovyでのinvoke dynamic(=indy)対応が進められてますし*2 *3Groovy++相当の静的コンパイル系の機能がGroovy2.0系で取り込まれる方向なので、動的言語だといっても性能的なハンデはどんどん埋まっていくんじゃないですかね*4。少なくともベースとしてJRubyと同じくらいの性能は出せるはず、出したい、出ると良いな。


当日の発表も、すごくタイトな時間(1つのお題辺り1人3分)でうまいこと説明できるのか!とか、モヒカンにメッタ斬りにされて再起不能になるんじゃないかとか色々不安でしたが、一応無事に生還できましたし、うまいこといったようで盛り上がっていただけて良かったです。何よりやってる本人が超楽しかったです。

雰囲気はid:orangecloverさんがトゥギゃってくれたJavaOne Tokyo 2012 JVM言語BoF #jt12_b101 - Togetterを見ていただけるとだいたい察していただけるかと。


ツイッター投票結果は、Oracleさん一押しのJavaFXのGroovyラッパ的なアレであるGroovyFXを用いて実装されたid:kiy0takaのバルスカウンタ改によって無駄にかっこよく発表されましたが、もちろんGroovyが優勝を勝ち取りました! うろ覚えですが確かGroovyが200台前半、Scalaがそれよりちょっと少なくて200弱、JRubyは(ry だったと思います。 KinkとJavaも集計対象に入れておいたらどうなっていたか興味深いですね。


あと、JavaOneTokyo全体の感想としては、一言で言えば超楽しかったです。去年と一昨年にSFのJavaOneに参加してきたんですが、今回タワーホールとかでセッション開始待ちしてるときの雰囲気とか音楽とかもう去年のSFのJavaOneそのもので、すっごいテンションあがりました。もうそのままSFにいるみたいな気分。


というわけで、大変楽しい時間を過ごさせていただき、てらださんを始めOracleのみなさん、JGGUGのみなさん、各言語の方々、各登壇者の方々、各コミュニティのみなさま、そして何よりBOFを聞きに来てくださったみなさま、どうもありがとうございました。


で、今日なぜか左膝が痛んでるんだぜ...

*1:TDDで実装したんだけど、そもそもアルゴリズムの理解が追いついてなくて、デキた!(ドヤッ→期待値が間違ってた→修正みたいなループを何度かやった。

*2:ちなみにJRubyはindy対応がかなり進んでて、今回のベンチ結果はindyの恩恵をかなり受けてます。のはず。

*3:Groovyは3/30付けでindyブランチがmasterにマージされてました!まだ色々と調整中のようですが

*4:厳密には静的コンパイル対象部分のGroovyコードでは動的な機能は使えないので動的言語と言って良いか微妙ですけど

Groovyを野良ビルドしてみよう

はじめに

G* Advent Calendarの4日目を担当させていただきたいと存じますnobeansと申します。どうもどうも。

さて今回のネタとして、

  • 自プロダクトのGroovyServとか
  • 今一押しのビルドツールGradleとか
  • この前のTDDBC横浜で注目を集めたBDDフレームワークのSpockとか

そういうのを紹介しようかなとも一瞬思ったんですが、他の人のブログとかも含めて日本語情報も割と充実している気がするので、今日はちょっと毛色を変えてGroovyそのものを自前でビルドする方法について紹介してみます。

対象者

  • Groovyの処理系に興味がある人
  • Groovyの言語仕様を変更して愉しみたい人
  • バグFIXのパッチを書いて本家のGroovyコミュニティに貢献したい人

多忙な人のための要約

$ git clone git://git.codehaus.org/groovy-git.git && ant install

を実行したらいいよ。

ソースコードを持ってくる

何はともあれ、ソースコードをとってきましょう。

元々GroovyはCodehausのSubversionリポジトリを使ってましたが、ここ最近Gitに移行されました。とはいっても、GithubじゃなくてCodehaus上のGitリポジトリがメインリポジトリです。

$ git clone git://git.codehaus.org/groovy-git.git

としてcloneしましょう。結構時間がかかります。


Githubにもミラーが作られてるので、使い慣れたそっちからとってきてもOKです。自分はGithub派なのでこっちでやってます。
https://github.com/groovy/groovy-core

$ git clone https://github.com/groovy/groovy-core.git

ビルドする

詳細はこちら。
http://groovy.codehaus.org/Building+Groovy+from+Source

Antでビルド

公式ドキュメントに書かれてるのがなぜかAntを使う方法なのですね。意外。

$ ant install

と実行するとビルドが開始されます。
すると、テストが実行されて無事に通るまでかなり時間がかかります。Macbook Core2Duo/2.4GHz Memory 8GBなマシンで、12分弱かかりました。CPU振り切りっぱなしで、ファンがうるさいです。

手っ取り早くテストをスキップして実行ファイルを手に入れたいときは、オプションを指定しましょう。

$ ant install -DskipTests=true

ビルドが完了すると、target/installに実行ファイル一式が生成されます。普通にGroovyのtarballとかzipをダウンロードして展開したディレクトリと同じだと考えればOKです。

試しに実行してみると、

$ target/install/bin/groovy --version
Picked up _JAVA_OPTIONS: -Dfile.encoding=utf-8 -Xms128m -Xmx1024m
Groovy Version: 2.0.0-beta-2-SNAPSHOT JVM: 1.6.0_29

とでました。2.x系ですね。最先端。

Gradleでビルド

GroovyistならやっぱりGradleですよね。Gradle。
一応、既にGradleでもビルドできるようになってるんですけど、まだ正式じゃなってことなんでしょうかねぇ。ということで試してみます。

Gradleって何?インストールとかしてないよ?という人もご安心ください。GradleにはGradle Wrapperという仕組みがあって、Gradleを明示的にインストールしなくても色々自動でやってくれます。プロジェクト直下にあるgradlew (Windowsの場合はgradlew.bat)を実行すると、実行に必要なgradleの実行ファイル一式zipを自動的にダウンロードして、ビルドを開始してくれるのです。

$ ./gradlew assemble

まずzipのダウンロードに結構時間がかかります。あと、依存関係のjarも色々引っ張ってくるのでそれもかなり時間がかかります。トータルで12〜15分くらいはかかります。とはいえ、2度目以降はそれらのダウンロードは不要なので割とサクっと動くはずです。

ビルドが完了すると、target/distributionsの配下にzipファイルが3つできてるはずです。binary, docs, srcの3種類ですね。とりあえずbinary版を展開して実行してみましょう。

$ unzip target/distributions/groovy-binary-2.0.0-beta-2-SNAPSHOT.zip
Archive:  target/distributions/groovy-binary-2.0.0-beta-2-SNAPSHOT.zip
   creating: groovy-2.0.0-beta-2-SNAPSHOT/
..(snip)..
  inflating: groovy-2.0.0-beta-2-SNAPSHOT/bin/groovy.bat
replace groovy-2.0.0-beta-2-SNAPSHOT/bin/groovy.icns? [y]es, [n]o, [A]ll, [N]one, [r]ename: y
  inflating: groovy-2.0.0-beta-2-SNAPSHOT/bin/groovy.icns
..(snip)..
  inflating: groovy-2.0.0-beta-2-SNAPSHOT/CLI-LICENSE.txt
$ groovy-2.0.0-beta-2-SNAPSHOT/bin/groovy --version
Picked up _JAVA_OPTIONS: -Dfile.encoding=utf-8 -Xms128m -Xmx1024m
Groovy Version: ##ImplementationVersion## JVM: 1.6.0_29

なぜかzip中にbin/groovy.icnsのエントリがダブっているとか、GroovyVersionの出力文字列がビルド時に置換できてないとか、やはり標準になってないだけあって、Gradleビルドファイルは調整中のようです...。

まだしばらくは素直に公式ドキュメント通りにAntを使った方が無難そうです。

Groovyを改造してみよう

Groovy JDK、略してGDKって知ってますか?
Javaの標準クラスに、便利だと思うメソッドを片っ端からGroovyメタプログラミング機能(以降、MOP)を利用して突っ込んでみました、というAPI群のことです。

http://groovy.codehaus.org/groovy-jdk/

File#getText()とかDate#format()とかやたら便利なメソッドが一杯追加されてるんですけど、これを実現してるのがGroovyのDefaultGroovyMethodsというクラスです。

例えば、このクラスに

    public static String getText(File file) throws IOException {
        // ...
    }

というメソッドがあるんですが、これがMOPで適用されると、Fileインスタンスに対して引数なしでgetText()メソッドを呼び出したときにこのstaticメソッドの処理が実行されてStringがreturnされる、ことになります。メソッド・戻り値はそのまま同じですが、第1引数がレシーバで、(この例では存在しませんが)第2引数以降がそのレシーバに渡すメソッド引数となるところがポイントです。

というわけで、オレオレ便利メソッドを突っ込んで遊ぶのに、DefaultGroovyMethodsクラスはちょうどいい素材になります。

さて、はじめての改造なので、ここはやはり HelloWorld ですよね。景気よくObjectクラスにhelloWorld()メソッドを仕込んでみましょう。

src/main/org/codehaus/groovy/runtime/DefaultGroovyMethods.javaを開いて、

    public static String helloWorld(Object object) {
        return "Hello, World!";
    }

というstaticメソッドをDefaultGroovyMethodsクラスの適当なところに追加してから、無難にAntでビルドし直しましょう。

さて、コマンドラインhelloWorld()を試してみると...

$ target/install/bin/groovy -e "println helloWorld()"
Hello, World!

やりましたね!!

さいごに

使ってるOSSプロダクトのソースコード一式をいつでも参照できるようにローカルディスクに入れておくのは、開発者としての身だしなみだと思いますが、せっかくそこにコードがあるなら単に読むだけではなく、積極的に改造・バグ修正・パッチ作成をして、更にその成果を本家にコントリビュートできると素敵ですね。


というわけで、
Enjoy your Groovy life!

12/5追記

環境変数GROOVY_HOMEですが、最近のGroovyなら実行したgroovyシェルスクリプトから辿って自動解釈してくれるので、設定してなくてもOKです。自分は無設定で生活してます。
既にGROOVY_HOMEを設定してる人は、上記の野良ビルド版を実行するときに、GROOVY_HOMEを解除するかまたは野良ビルド版のディレクトリパスに変更してからお試しください。

vimのquickrun.vimプラグインを使って編集中のGroovyコードをGroovyServ上で実行する

みんな大好きvimに、quickrun.vimという素晴らしいプラグインがあります。

これを使うと、vimで編集しているコードをショートカットキー一発で実行することができます。
すんごい便利!

色んなプログラミング言語に対応していますが、もちろんgroovyにも対応してます。

してますが、そこはそれ、ノーマルgroovyなので実行する度に例の起動待ち時間が発生して、ウッとかグッとか効果音とともに思考の流れが分断されてしまうわけです。


そんなときには、そう、GroovyServの出番です。

ノーマルgroovyの代わりにGroovyServのgroovyclientを使うようにする設定はとても簡単。

.vimrcに

let g:quickrun_config = {}
let g:quickrun_config.groovy = {'command' : 'groovyclient'}

と書いておくだけ。

もちろん、GroovyServがインストールされていてPATHにgroovyclientコマンドが通っている必要がありますけども、それはきっと既に完了しているはず。


標準のキーバインドでは r で発動します。

カスタマイズしたければ、

let g:quickrun_no_default_key_mappings = 1
nmap <Leader>r <Plug>(quickrun)

などと明示的に書いてあげればOkです。


ちなみに自分の設定ではLeaderはカンマにしてるので、",r"という2ストロークでquickrun.vimが発動します。


なお、GroovyServの仕組み上、初回の起動には数秒かかるのでご注意を。起動時メッセージは表示されないので一見フリーズしたように見えてしまいます。別途ターミナルでgroovyserverを起動しておけば、初回のquickrun.vim起動時からサクっと一瞬で実行することができるので、不安な人はそっちをお勧めします。それから、GroovyServではどうにもうまく動かないスクリプトも中にはあるので、そういうときはコンソールに戻ってノーマルgroovyコマンドでの実行をお試しください。


そんな感じで、
Enjoy your Groovy and Vim life!

GradleでEclipseの設定ファイルを生成するときにクラスパス変数GRADLE_REPOを使う

2012/3/21追記

id:Hirohiroさんによれば、Gradle 1.0-milestone-9から後述の方法がdeprecatedになってるそうです。最新の方法は↓を参照ください。他にもEclipseでGradleを使うための素敵なTipsが紹介されてます。

以降はオリジナルのエントリ本編(参考として残しておく)

Maveneclipse:eclipseをやって生成させた.classpathもそうなんですけど、とりあえずデフォルトで生成するとソースディレクトリのパスとして、コマンド実行したユーザ環境のフルパスが入ってしまったりします*1Mavenの設定でそれを回避しようとしたこともありましたが、結局よくわからなくてあきらめました。頑張ればできるのかな?


さて、Gradleでもeclipseプラグインを導入するためにbuild.gradleに1行書けば

apply plugin:'eclipse'

あとは、gradle eclipseコマンドによって.projectと.classpathを生成できます。


で、Gradleの場合、デフォルトではバイナリjarもソースjarもどちらもフルパスで出力されてしまいます。これでは、他の開発者の環境でイヤんなことになりますし、ローカルのユーザ名が恥ずかしい感じの人は重大なセキュリティ事故になってしまいます。

というわけで、M2_REPOのような感じでGRADLE_REPOクラスパス変数を使ってもらうようしたいんですが、調べてみたらとても簡単でした。

build.gradleにさらにもう1行追加するだけ。

apply plugin:'eclipse'
eclipseClasspath.variables = [ GRADLE_REPO: file(System.properties['user.home'] + '/.gradle/cache') ]

これで、GRADLE_REPOを使ったパスになってくれます。

これで各開発者はEclipse上でクラスパス変数としてGRADLE_REPO=$HOME/.gradle/cache *2を登録しておけばよいことになります。

あと、$GRADLE_REPO配下に実際にjarが存在しないとだめなので、Eclipse世界しか知らない開発者も一度はGradleでビルドして各種依存jarをダウンロードしておく必要がありますのでご注意。

おまけ

よくわからないので、フックを使って

eclipseClasspath {
    whenConfigured { classpath ->
        // TODO 2回実行するとエントリが重複するのは何故?
        // jarファイルへのパスをEclipse上のクラスパス変数GRADLE_REPOを使った記述に置換する。
        def replaceByVariable = { it.replaceFirst('.*/.gradle/cache', 'GRADLE_REPO') }
        classpath.entries = classpath.entries.collect { entry ->
            if (entry.kind == 'lib') {
                def path = replaceByVariable(entry.path)
                def sourcePath = replaceByVariable(entry.sourcePath)
                return new org.gradle.plugins.ide.eclipse.model.Variable(
                    path,
                    entry.exported,
                    entry.nativeLibraryLocation,
                    entry.accessRules,
                    sourcePath,
                    entry.javadocPath
                )
            }
            return entry
        }
    }
}

とか努力してハマった後に、よくドキュメントを読んでみたらあっさりvariablesプロパティがみつかったという訳です。というわけでドキュメントはよく読みましょう。

*1:確かバイナリのjarのパスはM2_REPO変数が使われていたような...うろ覚えですが

*2:実際には絶対パスで展開して指定します

GradleとPMDで循環的複雑度(サイクロマティック数)を手っ取り早く測定するスクリプトを書いてみた

ソースコードの品質向上のための効果的で効率的なコードレビューを読んで、循環的複雑度(サイクロマティック数)を手っ取り早く測定してみたくなったので、GradleとPMDで試してみました。

この辺を読めば割と簡単にできます。プロジェクトとしてきちんと測定していくなら、現在お使いのGradleの設定ファイル*1にpmdタスクを追加してあげればいいんですけど、手元のプロジェクトでちょっと測定してみたい、というときに仕込みがだるいのでやっつけですが*2シェルスクリプトを用意してみました。

スクリプト

もう読んだそのままな感じで、pmd.gradleファイルとpmd-rules.xmlをカレントディレクトリに作って、gradleでPMDを実行して、その後一時ファイルを削除する、というだけのスクリプトです。

複雑度の測定結果はコンソールに出ます。
Mavenライクなsrc/main/java, src/main/testディレクトリありきで書いてるので、構成が違う人は適当にカスタマイズしてください。

あ、そうそう。gradleコマンドがパスに入ってることを前提にしてます。まだインストールしてない場合は先にGradleをインストールしましょう。Mac+Homebrewなら"brew install gradle"でOk.

実行結果例

Jenkinsのcore配下でやるとこんな感じ。なお、公開しても問題なくて、かつ、このスクリプトがうまく動くプロダクトを探してたらたまたまJenkinsがあっただけなので、このチョイスには他意は全くありません。あしからず。

[jenkins-git/core]$ pmd-adhoc
:pmd
Running PMD static code analysis

hudson/AbstractMarkupText.java:42       The class 'AbstractMarkupText' has a Cyclomatic Complexity of 2 (Highest = 8).
hudson/AbstractMarkupText.java:141      The method 'findTokens' has a Cyclomatic Complexity of 8.
hudson/ClassicPluginStrategy.java:58    The class 'ClassicPluginStrategy' has a Cyclomatic Complexity of 4 (Highest = 14).
hudson/ClassicPluginStrategy.java:77    The method 'createPluginWrapper' has a Cyclomatic Complexity of 14.
hudson/ClassicPluginStrategy.java:184   The method 'createClassLoader' has a Cyclomatic Complexity of 5.
hudson/ClassicPluginStrategy.java:213   The class 'DetachedPlugin' has a Cyclomatic Complexity of 4 (Highest = 5).
hudson/ClassicPluginStrategy.java:224   The method 'fix' has a Cyclomatic Complexity of 5.
hudson/ClassicPluginStrategy.java:259   The method 'load' has a Cyclomatic Complexity of 8.
hudson/ClassicPluginStrategy.java:314   The method 'parseClassPath' has a Cyclomatic Complexity of 6.
〜(省略)〜

なお、このスクリプトの実行または改造過程において何らかの被害が発生しても当方で責任は負いかねますので、自己責任でお願いします。

*1:もちろん使ってますよね?

*2:Gradleのマルチプロジェクト構成中のサブプロジェクトなんかにはそのまま実行できないのでご注意

Windowsのショートネーム変換の~sがバグってるのでGroovyServ0.9の起動に失敗する場合がある

事象

ふもさんが、WindowsXP上のJava1.7.0でGroovyServを使おうとしてハマッたという報告を受け、我々取材班オレは急遽調査を開始した*1

fumokmm: v0.9 on WindowsXP with jdk1.7.0 だと groovyclient が起動しないです。 JAVA_HOMEのパスがおかしいって言われます。 #groovyserv http://h.hatena.ne.jp/fumokmm/225868361023164635 #hatena819
http://twitter.com/#!/fumokmm/status/108039184668819456

しかも、わざわざ英語でMLに投げていただいたりして、どうもどうも。

https://groups.google.com/d/topic/groovyserv/_QSfpjQG_Vg/discussion

nemo_kazさんも英語で絡んでくれたりして、なんかインターナショナルですね!(日本人同士ですけど)


さて、エラーメッセージなんですが、こんな↓のでした。

ERROR: JAVA_HOME is set to an invalid directory: c:\PROGRA~1\Java\JDK17~1.0.7.0 
Please set the JAVA_HOME variable in your environment 
to match the location of your Java installation. 

このメッセージ、GroovyServのソースにgrepしてもヒットしません。というか、Gradleラッパスクリプトのgradlew, gradlew.batに見つかったんですけど、これはビルドファイルですからね、groovyclient実行時にこれが使われるわけがない。

じゃあ、Groovyかな、と手っ取り早くアクセスできたMac上のHomebrewでインストールしたGroovy配下でgrepしてみたんですけど、該当するファイルが見つからず。

えー、じゃあGradleなん?なんでなん?と若干パニックになったんですけども、それが昨日。


今日、改めてgroovyのソースコード配下で調べてみたら、ありましたよ。
このエラーメッセージ、startGroovy.batが出してました。
startGroovy(sh)とはメッセージが違うとかなにそれひどい。

とまあ、エラーメッセージの所在が分かれば、結局事象としては、

JAVA_HOME=c:\Program Files\java\jdk1.7.0

と指定してるのに、ショートネーム変換によって、

c:\PROGRA~1\Java\JDK17~1.0.7.0

になってて、お前ホントは

c:\PROGRA~1\Java\JDK17~1.0

じゃねーの?*2 末尾の".7.0"ってナニよ、という話。
当然そんなディレクトリはないのでstartGroovy.batがコケてた、という。

解析

さて、私の環境で

> set JAVA_HOME=C:\Program Files\java\jdk1.7.0
> groovyclient.exe -v

をやってみました。

とりあえず、

C:\Program Files\java\jdk1.7.0

ファイルは存在しない状態でやってみると、

ERROR: JAVA_HOME is set to an invalid directory: C:\PROGRA~1\Java\jdk1.7.0
Please set the JAVA_HOME variable in your environment
to match the location of your Java installation.

となり、ショートネーム自体になりません。アレ、そういうもんなの?

今度は

C:\Program Files\java\jdk1.7.0

ディレクトリを作成してから試してみると...

ERROR: JAVA_HOME is set to an invalid directory: C:\PROGRA~1\Java\JDK17~1.0.7.0
Please set the JAVA_HOME variable in your environment
to match the location of your Java installation.

再現した!!!! 何コレ。

うーん。というか、jdk1.7.0の"7.0"が拡張子っぽく扱われてるとか?

というわけで、ググッてみたら、これっぽい気がする...

本来は、「\LONGLO~1\ABBE64~1.TXT」 のように表示されるべき部分が、「\LONGLO~1\ABBE64~1.TXTB.txt」 のように後ろにごみ(LFNの残骸)が付いてしまう。 こういうバグがある以上、特定の環境で一時的に使うバッチならともかく、汎用ツールを作る時は ~s 修飾子を安易に使えないと言うことだ。
http://otnx.jp/CMD/?%A5%D0%A5%B0#geb5e062

でも、実体も存在する、拡張子っぽくない(ピリオドを含まない)名前のディレクトリで、かつそれ自体も長い名前だと、

> set JAVA_HOME=C:\Program Files\java\hogefoobar_longlongname
> groovyclient.exe -v
ERROR: No java.exe found at: C:\PROGRA~1\Java\HOGEFO~1\bin\java.exe
Please set the JAVA_HOME variable in your environment
to match the location of your Java installation.

のように、HOGEFO~1とショートネーム変換は成功したりしてる。

よくわからん。ひどすぎる。
Windowsェ....

とりあえずの回避策

とりあえず、ふもさんが

fumokmm: @nobeans メーリングリストに慣れない英語で追加情報を投稿しておきましま。
なんだかショートネームのアルゴリズムっぽいですね…。パスjdk7の後ろに無理やり_01とか付けたら動きました。_xxありきのアルゴリズムなんですかね?よろしくお願いします。
http://twitter.com/#!/fumokmm/status/108363229012570112

と書かれたように、末尾に違う文字を付けて拡張子と誤解されないようにすると、ショートネームの誤変換が回避できるようです。

今後の展開

うーん。ショートネームを使うこと自体が問題な気がしてきたなぁ。
空白を含むパスでもうまく動くように良かれと思って変換してたのだけど。

JAVA_HOMEに関しては正直GroovyServ自体では使ってなくて、そのままGroovyにスルーしてしまって良いので、ショートネーム変換をあきらめるというのはアリなのだれど、groovyコマンドパス(ローカルのGROOVY_BIN変数)については、startコマンドに食わせる必要があるので(startはダブルクォートをつけるとNGという超う○こ仕様)、空白を含むパスは厳禁なのだ。なのでそこにはショートネーム変換が必要(もしくはstartコマンドを使わない常駐的起動方法 or 他)。

空白含むパスにおくなよ、という制約を付ければいいのかもしれないけど、Windowsだと標準のProgram Filesからしてアレだからなぁ。

ああ、困った困った。

誰か、素敵なアイデア(できればパッチ付きで)をください。

*1:全然"急遽"じゃないですけど

*2:この変換自体よくわからなんけども