sbt でファイル変更をフックしてコンソールをクリアしつつコンパイルする

Scala Advent Calendar 2014 の 12 日目です。

sbt で ~compile でファイル変更をフックしてコンパイルは皆さんよくやってると思いますが、

~ ;eval "\u001B[2J\u001B[0\u003B0H" ;compile

とにするとコンソールをクリアしつつコンパイル続行するのでちょっといいかんじになります。

~/.sbtrc に

alias cc = ~ ;eval "\u001B[2J\u001B[0\u003B0H" ;compile

って書けば cc で使えるようになります。

Heroku で JDK のバージョンを指定する

Heroku でサポートされている JDK は 1.6, 1.7, 1.8 です。 今ではデフォルトは 1.8 ですが、古いアプリではどうやらそのまま 1.6 が使われているようです。

JDK のバージョンを指定したいときには system.properties というファイルを使います。

java.runtime.version=1.8

このファイルをコミットし、PATH を設定します。

APP_PATH=`heroku config:get PATH`
heroku config:set PATH=/app/.jdk/bin:$APP_PATH

で、あとはこれを git push heroku master するだけで、JDK のバージョンアップができます。

% git push heroku master
Fetching repository, done.
Counting objects: 4, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 324 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)

-----> Play 2.x - Scala app detected
-----> Installing OpenJDK 1.8...done

参考: Updating Existing Java Apps to Use Java 7 | Heroku Dev Center

Play で Scalate を使う

play-scalate っていう名前のプラグインはいろんな人が書き散らかしてて github 検索するとボロボロ出てくるんですが、ついうっかり仲間に加わってしまいました。

https://github.com/tototoshi/play-scalate

build.sbt に設定すれば

libraryDependencies ++= Seq(
  "com.github.tototoshi" %% "play-scalate" % "0.1.0-SNAPSHOT",
  "org.scalatra.scalate" %% "scalate-core" % "1.7.0",
  "org.scala-lang" % "scala-compiler" % scalaVersion.value
)

unmanagedResourceDirectories in Compile += baseDirectory.value / "app" / "views"

こんな感じで使えます。

package controllers

import play.api._
import play.api.mvc._
import com.github.tototoshi.play2.scalate._

object Application extends Controller {

  def index = Action { implicit request =>
    Ok(Scalate.render("index.jade", Map("message" -> "hello")))
  }

}

ライブラリという形にして、SNAPSHOT を publish はしたけれど、そんな大したものではないのでいじりたければプロジェクト内にコピペして好きにいじってもらえばいいかなと思います。

Play で Scalate を使うときに気をつけることは、Dev モードで普通に動いてるのに Prod モードだと動かん!ってことにならないように、テンプレートファイルをファイルとしてではなくリソースとして読み込めってこどでしょうか。つまり、Play.api.current.getFile じゃなくて Play.api.current.resourceAsStream とか使おうってことです。Scalate はファイルを読み込むのを想定しているので、リソースを読み込むようにするのに一手間必要です。

Play ではリソースファイルは conf/ に置くことになってる(ちょっと気持ちわるい)ために app/views のファイルはリソースとして扱われないので、デプロイしたときにリソースが見つからん!というエラーも起きます。これを防ぐためにはテンプレートファイルは、app/views ではなく conf/views に置くか、conf 以下から app/viewsシンボリックリンクを...っていやまさか、

build.sbt で

unmanagedResourceDirectories in Compile += baseDirectory.value / "app" / "views"

とすれば良いです。

RFC 的には text/* のデフォルトエンコーディングは ISO-8859-1

以下のような、日本語のデータを text/csv という Content-Type で返す Play アプリケーションで、

GET     /test                 controllers.Application.test
package controllers

import play.api._
import play.api.mvc._
import play.api.libs.iteratee.Enumerator
import scala.concurrent.ExecutionContext.Implicits.global

object Application extends Controller {

  def test = Action {
    implicit request => {
      Result(
        header = ResponseHeader(200, Map(CONTENT_TYPE -> "text/csv", CONTENT_DISPOSITION -> ("attachment; filename=test.csv"))),
        body = Enumerator.outputStream { out =>
          try {
            // CSVデータを出力
            out.write("テスト".getBytes("UTF-8"))
          } finally {
            out.close()
          }
        }
      )
    }
  }

}

次のようなテストを書くとエラーになるんだけどなんで?と聞かれました。

import org.scalatest._
import play.api.test._
import play.api.test.Helpers._
import play.api.libs.ws.WS
import play.api.Play.current

class ApplicationSpec extends FlatSpec with Matchers {

  it should "work" in {
    running(TestServer(3333, FakeApplication())) {
      val testUrl = "http://localhost:3333/test"
      val body = await(WS.url(testUrl).get).body
      body should be("テスト")
    }
  }
}
[info] Compiling 1 Scala source to /Users/toshi/work/scala/play-hello/target/scala-2.11/test-classes...
[info] ApplicationSpec:
[info] - should works *** FAILED ***
[info]   "[ãã¹ã]" was not equal to "[テスト]" (ApplicationSpec.scala:13)
[info] ScalaTest
[info] Run completed in 3 seconds, 921 milliseconds.
[info] Total number of tests run: 1
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 0, failed 1, canceled 0, ignored 0, pending 0
[info] *** 1 TEST FAILED ***
[error] Failed: Total 1, Failed 1, Errors 0, Passed 0
[error] Failed tests:
[error]         ApplicationSpec
[error] (test:test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 7 s, completed 2014/10/18 23:44:56

見ての通り、エンコードがおかしくなってアサーションが通りません。

これ不思議だなあと思って調べたんですが、Play の WS(クライアント側) のソースコードの該当部分を見てみるとコメントがついていました。

https://github.com/playframework/playframework/blob/2.3.5/framework/src/play-ws/src/main/scala/play/api/libs/ws/ning/NingWS.scala#L681-L693

  lazy val body: String = {
    // RFC-2616#3.7.1 states that any text/* mime type should default to ISO-8859-1 charset if not
    // explicitly set, while Plays default encoding is UTF-8.  So, use UTF-8 if charset is not explicitly
    // set and content type is not text/*, otherwise default to ISO-8859-1
    val contentType = Option(ahcResponse.getContentType).getOrElse("application/octet-stream")
    val charset = Option(AsyncHttpProviderUtils.parseCharset(contentType)).getOrElse {
      if (contentType.startsWith("text/"))
        AsyncHttpProviderUtils.DEFAULT_CHARSET
      else
        "utf-8"
    }
    ahcResponse.getResponseBody(charset)
  }

Play のデフォルトのエンコーディングUTF-8 なのですが、 RFC-2616 的には Content-Type が text/* のデフォルトエンコーディングは ISO-8859-1 なので Content-Type が text/* なデータはあえて ISO-8859-1 にしてあります。

つまりサーバーの実装のほうが、

CONTENT_TYPE -> "text/csv; charset=utf-8;"

とちゃんと UTF-8 と書けって話でした。

なるほどなとは思いましたが、サーバー側のほうは Play のデフォルトである UTF-8 で返すのにクライアントだけ ISO-8859-1 で受け取るってのはハマるよなあと思いました。ただサーバーのほうでもエンコーディングが指定されていなければ ISO-8859-1 に変えるとするのは今の Play の実装では無理があるし、そんなに望まれている動作でもないかもなとも思います。微妙ですね。

なので、とりあえず RFC-2616 的には Content-Type が text/* のデフォルトエンコーディングは ISO-8859-1 という事実だけ覚えておくことにしました。

The "charset" parameter is used with some media types to define the character set (section 3.4) of the data. When no explicit charset parameter is provided by the sender, media subtypes of the "text" type are defined to have a default charset value of "ISO-8859-1" when received via HTTP. Data in character sets other than "ISO-8859-1" or its subsets MUST be labeled with an appropriate charset value. See section 3.4.1 for compatibility problems.

テンプレートエンジン Night で Scala のテンプレートエンジンの紹介をしました

テンプレートエンジンNight on Zusaar

テンプレートエンジン Night という不穏なイベントで Twirl を中心に Scala のテンプレートエンジンの紹介をしました。 Scala を知らない or そんなにやってない人向けの発表です。 スライドにはあまり内容詰め込んでなくて、キーワードだけ見てもらえればいいかなという感じです。 広義の?テンプレートエンジンとして String Interpolation とか XML リテラルも紹介しました。 ほかに紹介したかったけどやめたものとして quasiquote とかがあります。

テンプレートエンジン Night はいいかんじに荒れてて面白いイベントでした。

いくつか思ったこと。

Haml とかが dis られ気味だった件

Jade とか Haml みたいなインデントがシンタックスに入っているテンプレートが「コピペすると壊れる」として dis られていて、せやなって感じではあるけれど、見通しが良いのはやっぱり良いこと。 コピペができても見通しの悪いシンタックスだと、実はすでにタグの対応がとれてないけれど見通しが悪すぎてどこがどうぶっ壊れてるのかわからない、とりあえず壊れてるっぽいことだけはわかる、みたいなことがあって、そのほうがずっとつらい気がする。

mixer2 とか Lift とか

思想としては好きだし、デザイナーと共同作業するには良さそうなんだけど、 view と密結合すぎるところが辛い。懇親会で話に出たんだけど wicket とかもそうですね。

静的解析、コンパイルによるチェック

あまり先入観を与えたくないなと思って Twirl について意図的にあまりデメリットを話さなかったけれど、 思ったよりコンパイルするというアプローチが好意的に受け止められていました。

ただしコンパイル時間はたしかに伸びるし、開発時にいちいち全部コンパイルされても...ということは実際あります。 開発時にいちいちコンパイルするよりは、jsp だとか、Scalate とか、デプロイするときに、プリコンパイルをするっていうほうが現実には合ってるんじゃないかと思いました。

デザイナーとの共同作業問題

あまり触れられてなかったけれど、シンタックスどうこう以前に、コンパイルする言語だとデザイナー向けに環境を用意するのがめんどう、とかそういう問題があります。

PHP

メール送信とかできて便利です。

Xslate

すごい

Smarty

やばい

次は バリデーション Night、 フォーリンキー Night らしいです。荒れるな。

Scala 2.11.3 が生まれる前に死んでしまった話

↓に関して、https://gitter.im/scalajp/public で盛り上がってた内容をまとめました。

avatar

なぜこんなことになってしまうのだろう(未調査) https://github.com/skinny-framework/skinny-framework/issues/193

avatar

これ Skinny 側で何か work around できないですかね https://github.com/skinny-framework/skinny-framework/issues/193#issuecomment-58773130

avatar

2.11.3 を指定して build した Scalatra を scalaVersion := "2.11.0" なアプリから利用するのは問題なさそうだった。となると Scalatra に 2.11.3 でビルドした 2.3.1 を出してもらうようお願いするのが手っ取り早いのかな。こういう問題への対処、それでいいのかという気はするけど、ユーザが困ってしまうので。

avatar

Scalatra に issue 立てるのはまだ早いのでまずは scala-lang の JIRA へ...

avatar

2.11.3 の scala.collection.TraversableLike の呼び出すメソッドが変わったってこと,,?

avatar

bincompat-backward.whitelist.conf に

+    // see github.com/scala/scala/pull/3925, SI-8627, SI-6440
+    {
+        matchName="scala.collection.TraversableLike.filterImpl"
+        problemName=MissingMethodProblem
+    },

というのが増えてるな

avatar

これは影響範囲が大きすぎるような。

avatar

これが該当の変更かな? https://github.com/scala/scala/pull/3949 なぜ例外が起きるのかわかってないけど。

avatar

privateからprivate[scala]にすると、どちらにしろソース上はprivateだから問題なさそうに見えるけど、内部表現結構変わるとかそういう...

avatar

scalatraが標準ライブラリのコレクションを継承してることも一因だろうか。まぁ本当にあの変更が原因なら、scala本体側が明らかに悪そうだけど

avatar

2.11.3 はまだ様子見しよう...

avatar

これ、confに書いてるから確信犯だよなー。そんなに重要な修正だった and/or 他に修正方法なかったのだろうか。dbuildは全部同じversionでビルドやり直すから、こういう問題は多分検知されないし

avatar

キタコレ! play2でも trait Cookies extends Traversable[Cookie] というのあるから、Scala 2.11.3 使って、request.cookies.filter とかするとAbstractMethodError 発生する! https://github.com/playframework/playframework/blob/2.3.5/framework/src/play/src/main/scala/play/api/mvc/Http.scala#L683

avatar

@xuwei-k 2.11.3 はお蔵入り決定ですね

avatar

キタ━━━━(゜∀゜)━━━━ッ!!

gakuzzzz

キタ━━━━(゜∀゜)━━━━ッ!!

seraphr

2.11.3 生まれる前に死んでしまったか…

avatar

R.I.P. 2.11.3

avatar

scala-internalsのメーリングリストで SI-8899 の話が始まらないのだけど、(時差とかあるし)まだ気づいてないか問題の重要性認識してる人が少ないのか、いやこの程度なら予定通り出すのか、それともclosedなところでtypesafeの人たちは既に話を始めているのか。もうしばらく待って(待たなくても?) 「えっ、明らかにバイナリ互換崩れてるのに、予定通り 2.11.3 公式アナウンスするつもりなんですか?」って scala-internals のメーリングリストに誰か投稿したほうが?

avatar

スクリーンショット 2014-10-12 23.42.03.png

avatar

とりあえず知らせといた https://groups.google.com/d/msg/scala-internals/ZSx-ZW69BJA/bFF8PtjdzpsJ

avatar

お、はやい。ありがとうございます :+1:

avatar

:+1:

gakuzzzz

:+1:

avatar

おっ、このtweet https://twitter.com/xuwei_k/status/521272044663894016 がScalatra作者にretweetされたり、Scalazコミッターからmention来たりして広まってきた > /dev/null

avatar

お、「別の重要な問題も見つかったんだ」的な返信きてる・・・

avatar

"[SI-8900] Assertion failure in isAnonymousOrLocalClass - Scala" https://issues.scala-lang.org/browse/SI-8900

avatar

元のやつも、バグのPriorityをコミッターの人が1番上か2番目?に上げてるし、まぁ重要性は伝わってるみたいなので、あとは見守るしかないな

avatar

:pray:

avatar

BlockerでFix versionが2.11.4ってことは2.11.3はこのままお蔵入りか

avatar

2.11.3のtagできててcentralにもあがってるから、2.11.3がtarget versionのままではおかしいから、とりあえずtarget versionを2.11.4にあげただけで、お蔵入りかどうかなどの詳細はまだ決まってなさそう(というあくまでも予想)

アルパカの人より自分のほうがdbuildの仕組みとか把握してるっぽい https://twitter.com/extempore2/status/521379266789797888 し、とりあえずrevetして https://github.com/scala/scala/pull/4048 2.11.4 をはやめに出す方向になりそう?だし、大体予想通りなので言うこと無い > /dev/null

avatar

https://github.com/scalaz/scalaz/commit/6a1354d74#commitcomment-8130530 a few daysらしい

avatar

Also note 2.11.3 wasn't fully released (pushing binaries to Sonatype isn't the last step in the process).

アナウンスするまでがリリースってことかな

avatar

とりあえず、ぎりぎり公式アナウンス前に地雷踏んでくれた瀬良さんに感謝ですね・・・

avatar

ですね。アナウンス前なのでさほど混乱はなさそう。