Play2/Scala向けライブラリを公開する(SBTでGithub上のMavenに)

はじめに
----
[Bounscale](https://devcenter.heroku.com/articles/bounscale)というオートスケールするHeroku Addonを作っています。

Bounscaleは各プログラミング言語/フレームワーク毎にエージェントを提供しています。
現在、Rails、node.jsなどに対応していますが、実験的にPlay Framework2(Scala)への提供を開始しました。

エージェントのjarはPlayの依存ライブラリの記述を追記するだけでインストールできるようにしたいところです。
今回は、Github PagesをMavenリポジトリ代わりに利用して、sbtで各種ファイルを生成してjarを公開したので下記に手順をメモしておきます。

流れ
----
* Github Pagesのリポジトリを作ってMaven形式のjarやpomなどを公開する場所を確保します
* Play Framework2をインストールして同梱のsbtを利用できるようにします
* ライブラリをビルドします
* ライブラリをgithub pagesに公開します
* Play Framework2のサンプルアプリケーションを作成し、公開したライブラリを利用してみます

sbt
----
[sbt](http://scalajp.github.io/sbt-getting-started-guide-ja/)はSimple Build Toolの略だそうで、Scalaのビルドをするためのツールです。
アプリケーションをコンパイルしたり、依存ライブラリをダウンロードして来たりできます。

位置づけ的にはRubyでいうところのbundlerに当たると言えなくもないです。
ただ、[プラグイン](http://scalajp.github.io/sbt-getting-started-guide-ja/using-plugins/)の機構があって、タスクを追加して機能拡張できるので、そういう意味ではrakeに近い面もあります。

今回はsbtを使ってライブラリをビルド、公開します。

ちなみにPlay Frameworkから提供されるplayコマンドでサーバを立ち上げたり、コンパイルしたり、プロジェクト作ったり色々できるんですが、このplayコマンドはsbtにアドオンを追加する形で実装されています。

サンプル
----
今回はhello.jarというライブラリを公開する事にします。
内容としては本当にシンプルに、Helloというシングルトンオブジェクトが提供されて、Hello.say とやると標準出力に"Hello!"と出るだけのものとします。

````scala:hello.scala
package sample.hello

object Hello {
  def say(){
    println("Hello!")
  }
}
````

GithubをMavenリポジトリにする
----
MavenのセントラルリポジトリにJarを公開するのは[ユーザ登録したりチケット登録したり人手の承認をもらったり](https://docs.sonatype.org/display/Repository/Sonatype+OSS+Maven+Repository+Usage+Guide)、なんだかとっても大変そうです。
ファイルをhttpで取得できれば、非公式リポジトリとして公開する事ができるので、今回はその方法でいきます。

置き場はどこでもいいのですが、Github Pagesは無料ですしgitでバージョン管理できるので便利です。[この辺りの手順](https://help.github.com/articles/creating-project-pages-manually)でリポジトリを作ります。

Github Pagesの作成
----
まずはブラウザで[Githubの新規リポジトリページ](https://github.com/new)を開いてMaven用のリポジトリを画面から普通に作成します。
今回はsamplerepoという名前で作ります。

続いて、下記の感じでリポジトリをGithub Pagesに対応させます。（とは言っても肝はgh-pagesというブランチを作るところだけですが）

````
# git clone https://github.com/bounscale/samplerepo.git
Cloning into 'samplerepo'...
warning: You appear to have cloned an empty repository.
# cd samplerepo/
# git checkout --orphan gh-pages
# rm .gitignore
rm: cannot remove `.gitignore': No such file or directory
# echo "My GitHub Page" > index.html
# git add index.html
# git commit -a -m "First pages commit"
[gh-pages (root-commit) 9700796] First pages commit
 1 file changed, 1 insertion(+)
 create mode 100644 index.html
# git push origin gh-pages
To https://github.com/bounscale/samplerepo.git
 * [new branch]      gh-pages -> gh-pages
````

この特殊なgh-pagesブランチにpushしたファイルは下記に公開されます。

````
http://[ユーザ名].github.io/[リポジトリ名]/
````

今回で言うと下記です。
http://bounscale.github.io/samplerepo/

試しに作ったindexページはすぐに公開されるようです。
実際にブラウザやcurlかwgetで開いてみて、通常のHTMLページとして参照できるかどうか確認するといいと思います。

````
# curl http://bounscale.github.io/samplerepo/
My GitHub Page
````

Play2のインストール
----
sbt単体でインストールしてもいいのですが、同梱されているsbtの利用も兼ねられますし、後でサンプルアプリケーションを作るので、ここでPlay2をインストールします。手順は[この辺り](http://www.playframework.com/documentation/2.1.3/Installing)を参照してください。
（男らしくインストーラも何もなく、解凍してパス通すだけ）

sbtを使えるようにする
----
Play2同梱のsbtの実行コマンドを作成します。
$PLAY_HOMEにはplayコマンドがありパスを通しているはずなのでここに置きます。

````
# cd $PLAY_HOME
# echo 'java -Dsbt.ivy.home=$PLAY_HOME/repository -Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=384M -jar $PLAY_HOME/framework/sbt/sbt-launch.jar "$@"' > sbt
# chmod u+x sbt
# sbt
[info] Set current project to default-483e75 (in build file:...)
> exit
````

sbtプロジェクトの作成
----
sbtでビルドをするにあたって、どこか適当にビルド用のディレクトリを掘って、下記のように既定のディレクトリ構成を作成します。

````
 build.sbt
 project/
 lib/
    <ビルドに必要なライブラリ>
 src/
   main/
     resources/
        <メインの jar に含むファイル>
     scala/
        <メインの Scala ソース>
     java/
        <メインの Java ソース>
   test/
     resources/
        <テストの jar に含むファイル>
     scala/
        <テストの Scala ソース>
     java/
        <テストの Java ソース>
````

ディレクトリ構成を自動で作ってくれてもよさそうなものですが、どうもジェネレートのコマンドが見つからないので、手でディレクトリ掘りました。

````
# mkdir -p project lib src/main/resources src/main/scala src/main/java test/resources test/scala test/java
# touch build.sbt
````

また、testにはたぶんScalaTestとかspec的なものを置くんだと思いますが、とりあえず今回は何もおいてないです。

ビルド設定
----
build.sbtにビルドに関する情報を書きます。とりあえず最低限下記を記載しました。

````build.sbt
name := "Hello"

version := "0.0.1"

scalaVersion := "2.10.0"

publishTo := Some(Resolver.file("hello",file("/path/to/github/pages/repository/samplerepo"))(Patterns(true, Resolver.mavenSty
leBasePattern)))
````
publishToはMavenリポジトリに必要なファイル類のジェネレート先を指定します。ここでは先ほど作成したgithubからcloneしてきたディレクトリを指しておきます。

コンパイルできるか確認
----
続いて先ほど提示したhello.scalaをsrc/main/scalaの下に配置します。

```
# cp /tmp/hello.scala src/main/scala
```

とりあえず普通にコンパイルできるか試しておきます。

````
# cp /tmp/hello.scala src/main/scala
# sbt compile
[info] Set current project to Hello (in build file:/.../samplerepo_build/)
[info] Updating {file:/.../samplerepo_build/}default-3d8bb9...
[info] Resolving org.scala-lang#scala-library;2.10.0 ...
[info] Done updating.
[info] Compiling 1 Scala source to /.../samplerepo_build/target/scala-2.10/classes...
[success] Total time: 78 s, completed Aug 28, 2013 3:35:13 PM
````

無事コンパイルできているようです。

なお、ビルドに必要なjarがある場合はlibディレクトリに入れておくと勝手にパスが通ります。

ビルド
----
Mavenの仕様に則ったファイルをジェネレートします。

````
# sbt publish
[info] Set current project to Hello (in build file:/.../samplerepo_build/)
[info] Packaging /.../samplerepo_build/target/scala-2.10/hello_2.10-0.0.1-sources.jar ...
[info] Done packaging.
[info] Wrote /.../samplerepo_build/target/scala-2.10/hello_2.10-0.0.1.pom
[info] :: delivering :: hello#hello_2.10;0.0.1 :: 0.0.1 :: release :: Wed Aug 28 15:43:56 JST 2013
[info]  delivering ivy file to /.../samplerepo_build/target/scala-2.10/ivy-0.0.1.xml
[info] Generating Scala API documentation for main sources to /.../samplerepo_build/target/scala-2.10/api...
model contains 4 documentable templates
[info] Scala API documentation generation successful.
[info] Packaging /.../samplerepo_build/target/scala-2.10/hello_2.10-0.0.1-javadoc.jar ...
[info] Done packaging.
[info] Packaging /.../samplerepo_build/target/scala-2.10/hello_2.10-0.0.1.jar ...
[info] Done packaging.
[info]  published hello_2.10 to /.../samplerepo/hello/hello_2.10/0.0.1/hello_2.10-0.0.1.pom
[info]  published hello_2.10 to /.../samplerepo/hello/hello_2.10/0.0.1/hello_2.10-0.0.1.jar
[info]  published hello_2.10 to /.../samplerepo/hello/hello_2.10/0.0.1/hello_2.10-0.0.1-sources.jar
[info]  published hello_2.10 to /.../samplerepo/hello/hello_2.10/0.0.1/hello_2.10-0.0.1-javadoc.jar
[success] Total time: 96 s, completed Aug 28, 2013 3:45:17 PM
````

publishToで指定したファイルパスにpomやjarなどのファイルがジェネレートされています。
ディレクトリについてはversionとscalaVersionで指定した内容に基づいて生成されます。

こんな感じです。

````
# tree
.
├── hello
│   └── hello_2.10
│       └── 0.0.1
│           ├── hello_2.10-0.0.1.jar
│           ├── hello_2.10-0.0.1.jar.md5
│           ├── hello_2.10-0.0.1.jar.sha1
│           ├── hello_2.10-0.0.1-javadoc.jar
│           ├── hello_2.10-0.0.1-javadoc.jar.md5
│           ├── hello_2.10-0.0.1-javadoc.jar.sha1
│           ├── hello_2.10-0.0.1.pom
│           ├── hello_2.10-0.0.1.pom.md5
│           ├── hello_2.10-0.0.1.pom.sha1
│           ├── hello_2.10-0.0.1-sources.jar
│           ├── hello_2.10-0.0.1-sources.jar.md5
│           └── hello_2.10-0.0.1-sources.jar.sha1
└── index.html
````

リリース
----
ジェネレートしたファイルをGithub Pagesに公開します。
これは単にgitでpushするだけです。

````
git add .
git commit -m "Release 0.0.1"
git push origin gh-pages
````

これでjarが公開されました。
pushした後、ブラウザなどから該当のファイルがダウンロードできるか確認するといいと思います。

````
# wget http://bounscale.github.io/samplerepo/hello/hello_2.10/0.0.1/hello_2.10-0.0.1.jar
--2013-08-28 06:53:40--  http://bounscale.github.io/samplerepo/hello/hello_2.10/0.0.1/hello_2.10-0.0.1.jar
....
2013-08-28 06:53:40 (173 MB/s) - `hello_2.10-0.0.1.jar' saved [1506/1506]
````

サンプルPlayプロジェクトの作成
----
公開したjarをアプリケーションから利用してみましょう。
Play Frameworkのアプリケーションを作成します。名前はplaysampleとします。Scalaアプリケーションとして作ってください。

````
# play new playsample
       _            _
 _ __ | | __ _ _  _| |
| '_ \| |/ _' | || |_|
|  __/|_|\____|\__ (_)
|_|            |__/

play! 2.1.3 (using Java 1.7.0_25 and Scala 2.10.0), http://www.playframework.org

The new application will be created in /root/work/play_work/play_app/playsample

What is the application name? [playsample]
>

Which template do you want to use for this new application?

  1             - Create a simple Scala application
  2             - Create a simple Java application

> 1
OK, application playsample is created.

Have fun!

````

dependencyの記述
----
生成されたPlay Frameworkのプロジェクトに先ほど作成したライブラリの依存関係を記述します。
記述する場所はproject/Build.scalaです。

````scala:project/Build.scala
import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {

  val appName         = "playsample"
  val appVersion      = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // 依存ライブラリの追加
    "hello" % "hello_2.10" % "0.0.1"
  )

  val main = play.Project(appName, appVersion, appDependencies).settings(
    // Mavenリポジトリの追加
    resolvers += "Maven Repository on Github" at "http://bounscale.github.io/samplerepo/"
  )

}
````

利用するライブラリの名前の記述と、取得しに行くMavenリポジトリの情報をリゾルバとして追加しています。

アプリケーションの初回起動時には不足している依存ライブラリが自動的にダウンロードされます。
起動してみると下記の感じでGithub上のjarファイルが正常にダウンロードされてとりこまれています。

````
# play run
...
[info] downloading http://bounscale.github.io/samplerepo/hello/hello_2.10/0.0.1/hello_2.10-0.0.1.jar ...
[info]  [SUCCESSFUL ] hello#hello_2.10;0.0.1!hello_2.10.jar (1048ms)
[info] Done updating.
...
````

ライブラリを使ってみる
----
Helloクラスを利用する記述をコントローラに書いてみます。
app/controllers/Application.scalaを下記のように更新します。

````scala:app/controllers/Application.scala
package controllers
import sample.hello.Hello //追加

import play.api._
import play.api.mvc._

object Application extends Controller {

  def index = Action {
    Hello.say //追加
    Ok(views.html.index("Your new application is ready."))
  }

}
````

Playを起動してアクセスしてみると、Helloクラスからのログが出ていることが分かります。

````
# play run
...
[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

(Server started, use Ctrl+D to stop and go back to the console...)

[info] Compiling 5 Scala sources and 1 Java source to /root/work/play_work/play_app/playsample/target/scala-2.10/classes...
[info] play - Application started (Dev)
Hello!
````

おまけ：Herokuでも動くよ
----
Herokuは色々な言語/フレームワークが動くPaaSです。Play2にも対応しています。

Herokuはよくできているので、Play2っぽいプロジェクトを自動で判断して適切なbuildpackを選択してくれます。
今回のサンプルをpushしたところ、依存ライブラリの情報も勝手に見てくれて、普通にGithub Pagesからダウンロードして利用してくれました。

````
# play clean compile stage
# echo "web: target/start -Dhttp.port=\$PORT \$JAVA_OPTS" > Procfile
# git init
# git add .
# git commit -m "Initial commit"
# heroku create sample-app-play
# git push heroku master
Counting objects: 28, done.
Compressing objects: 100% (21/21), done.
Writing objects: 100% (28/28), 37.00 KiB, done.
Total 28 (delta 0), reused 0 (delta 0)

-----> Play 2.x - Scala app detected
-----> Installing OpenJDK 1.6...done
-----> Building app with sbt
-----> Running: sbt clean compile stage
...
       [info] downloading http://bounscale.github.io/samplerepo/hello/hello_2.10/0.0.1/hello_2.10-0.0.1.jar ...
       [info]   [SUCCESSFUL ] hello#hello_2.10;0.0.1!hello_2.10.jar (59ms)
...
-----> Launching... done, v6
       http://sample-app-play.herokuapp.com deployed to Heroku
...
````

アクセスしてログを見てみるとたしかにHelloオブジェクトのメソッドがよばれています。

```
# heroku logs
...
2013-08-28T07:31:26.777738+00:00 app[web.1]: Hello!
...
```

終わりに
----
Rubyのgemや、node.jsのnpmに比べて自分でリポジトリ作ったりビルドが必要だったり諸々面倒ではありました。
でもちょっと頑張ればライブラリを自由に公開できます。

Mavenのセントラルリポジトリにもっと自由にあげられればいいのに、と思いますが、色々大人の事情があるのでしょうか。
完全プロ志向のJavaらしいといえばJavaらしいですけどね。