build.sbt上で依存ライブラリが依存してるライブラリのversionを取得する

タイトルがややこしくて何言ってるかわかりづらいと思うので、ひとまず具体的な例で説明します。

例えば以下のようなbuild.sbtだったときに

// playframeworkのproject
lazy val mainProject = project
  .dependsOn(lib)
  .enablePlugins(PlayScala)

lazy val lib = project
  .settings(
    libraryDependencies += // ここにplay-json
  )
  • playframework本体が依存しているplay-jsonと全く同じversionのものをlibのsub projectに追加したい(新し過ぎるのも、古過ぎるのも嫌)
  • でも、libのsub projectには、playframework本体の依存は加えたくない。あくまでplay-jsonだけ追加したい

というようなパターンは、sbtでmulti projectをやっていたら、playframeworkに限らず、結構出てきますよね???

最近自分のOSSの某ライブラリでは、sbtが依存してるものと全く同じsjson-newに依存したい、とか思ってました。*1

このパターン、何年も前から、たまに欲しいなぁ、と思う割には結局やったことなかったのですが、やってみたら普通に出来たのでblogを書いています。

もちろん「全く同じversionのものを追加したい」は、多少適当というか、多少ズレても問題ないことが多いので、結局はそこは厳密にやらずに雰囲気で済ませている場合が多いと思いますが、原理上「全く同じversion」は自動で取得出来るはず?なので、実用するかどうか?はともかく、それをやってみました。

import lmcoursier.internal.shaded.coursier

def reverseDependencyVersion(
   baseGroupId: String,
   baseArtifactId: String,
   revision: String,
   targetGroupId: String,
   targetArtifactId: String
): Option[String] = {
  val dependency = coursier.Dependency(
    coursier.Module(
      coursier.Organization(
        baseGroupId
      ),
      coursier.ModuleName(
        baseArtifactId
      ),
    ),
    revision
  )
  coursier.Fetch().addDependencies(dependency).runResult().detailedArtifacts.map(_._1).collect {
    case x if (x.module.organization.value == targetGroupId) && (x.module.name.value == targetArtifactId) => x.version
  } match {
    case Seq(x) =>
      Some(x)
    case Nil =>
      None
    case xs =>
      // 複数見つかるのはありえないはずなのでエラーにする
      sys.error(xs.toString)
  }
}

TaskKey[Unit]("testReverseDependencyVersion") := {
  val playJsonVersion = reverseDependencyVersion(
    "org.playframework",
    "play_3",
    "3.0.8",
    "org.playframework",
    "play-json_3"
  )
  assert(playJsonVersion == Some("3.0.5"), playJsonVersion)

  val jacksonVersion = reverseDependencyVersion(
    "org.playframework",
    "play_3",
    "3.0.8",
    "com.fasterxml.jackson.core",
    "jackson-core"
  )
  assert(jacksonVersion == Some("2.14.3"), jacksonVersion)
}

上記はsbt 1.11.5と2.0.0-RC3で動くことを確認しています。

sbt標準のものでは綺麗にversionが取得出来なそうでした。

(依存解決後のjarファイル、つまり java.io.File を直接見にいって、そこのpathからversionを推測、などなら可能だが・・・)

明らかにsbtの内部実装として埋め込んであるcoursierを使っていて、その点で良くないので、普通のcoursierの依存を追加することに抵抗がない(coursierが衝突したりしない)、という場合は project/plugins.sbt に以下を追加して

libraryDependencies += "io.get-coursier" %% "coursier" % "2.1.24"

build.sbtの以下のimportを消すだけでOKです。

- import lmcoursier.internal.shaded.coursier
+

せいぜい20〜30行で可能なので、全然許容範囲で実用的(?)だと思いますが、実はもっとさらに簡単というか綺麗に取得可能な方法があったら教えてください。