sbt 1.4.xで作ったjar/war内のファイルのタイムスタンプがおかしくなる問題を修正した

最近GitBucketでTomcatにデプロイするとエラーになるというレポートがあり、調べていたのですが、

github.com

どうやらwar内のファイルのタイムスタンプが負の値になっており、warの展開時にその値をファイルの更新日時として設定しようとしてエラーになっているようで、sbt 1.4.0での以下の修正が原因であるらしいことがわかりました。

github.com

以下のような感じで環境変数SOURCE_DATE_EPOCHで明示的にタイムスタンプを設定するようにすれば回避できます。

envVars := Map(
  "SOURCE_DATE_EPOCH" -> System.currentTimeMillis().toString
)

GitBucketではsbt-ioのAPIを直接使用して、warファイルをいったん展開して内容を変更してからwarファイルを作り直すということを行っているのですが、その際に引数でタイムスタンプを指定することもできます。

IO jar (
  sources = contentMappings.map { case (file, path) => (file, path.toString) }, 
  outputJar = outputFile, 
  manifest = manifestFile, 
  time = Some(System.currentTimeMillis())
)

とりあえずはこれで問題を回避できたのですが、なんでこんな動きをするのか不思議だったのでもう少し詳しく調べてみました。

sbt 1.4.0以降では前述の修正により、packageタスクでjarファイルやwarファイル*1を作成する際にデフォルトでが各エントリのタイムスタンプに0を設定するようになっているのですが、sbt-io側の以下のタイムゾーン調整*2により、タイムゾーンによってはセットする値が負の値になってしまっているようです。

さらにJDKZipEntryのコードを見ると、どうやらzipファイルの各エントリはxdostimeという値に加えてextraフィールドにmtimeというミリ秒単位のタイムスタンプを持っており、xdostimeの方には想定通りの値(= 0)がセットされるものの、mtimeの方には負の値がセットされてしまうようです。

github.com

というわけで最終的にsbtに以下のプルリクエストを送ってマージしていただきました。

github.com

修正は1行だけですし、実際にこれが原因で困るというケースはほとんどないような気もするのですが、原因を調べるのがなかなか大変だったので記事を書いてみました。*3

*1:GitBucketではxsbt-war-pluginを使っています。

*2:JDKのZipEntry.getTime()がタイムゾーンによって返すタイムスタンプを調整しているため、予めタイムゾーン調整済みのタイムスタンプを設定する必要がある。

*3:あとからさらに調べてわかったのですがunzipコマンドではちゃんとタイムスタンプのレンジチェックが入っているようなのでsbt-ioでの展開時の処理でも補正するべきなのかも…。