sbt: generating shared sources in cross-platform project

374 views Asked by At

Building my project on Scala with sbt, I want to have a task that will run prior to actual Scala compilation and will generate a Version.scala file with project version information. Here's a task I've came up with:

lazy val generateVersionTask = Def.task {
  // Generate contents of Version.scala
  val contents = s"""package io.kaitai.struct
                    |
                    |object Version {
                    |  val name = "${name.value}"
                    |  val version = "${version.value}"
                    |}
                    |""".stripMargin

  // Update Version.scala file, if needed
  val file = (sourceManaged in Compile).value / "version" / "Version.scala"
  println(s"Version file generated: $file")
  IO.write(file, contents)
  Seq(file)
}

This task seems to work, but the problem is how to plug it in, given that it's a cross project, targeting Scala/JVM, Scala/JS, etc.

This is how build.sbt looked before I started touching it:

lazy val root = project.in(file(".")).
  aggregate(fooJS, fooJVM).
  settings(
    publish := {},
    publishLocal := {}
  )

lazy val foo = crossProject.in(file(".")).
  settings(
    name := "foo",
    version := sys.env.getOrElse("CI_VERSION", "0.1"),
    // ...
  ).
  jvmSettings(/* JVM-specific settings */).
  jsSettings(/* JS-specific settings */)

lazy val fooJVM = foo.jvm
lazy val fooJS = foo.js

and, on the filesystem, I have:

  • shared/ — cross-platform code shared between JS/JVM builds
  • jvm/ — JVM-specific code
  • js/ — JS-specific code

The best I've came up so far with is adding this task to foo crossProject:

lazy val foo = crossProject.in(file(".")).
  settings(
    name := "foo",
    version := sys.env.getOrElse("CI_VERSION", "0.1"),
    sourceGenerators in Compile += generateVersionTask.taskValue, // <== !
    // ...
  ).
  jvmSettings(/* JVM-specific settings */).
  jsSettings(/* JS-specific settings */)

This works, but in a very awkward way, not really compatible with "shared" codebase. It generates 2 distinct Version.scala files for JS and JVM:

sbt:root> compile
Version file generated: /foo/js/target/scala-2.12/src_managed/main/version/Version.scala
Version file generated: /foo/jvm/target/scala-2.12/src_managed/main/version/Version.scala

Naturally, it's impossible to access contents of these files from shared, and this is where I want to access it.

So far, I've came with a very sloppy workaround:

  • There is a var declared in singleton object in shared
  • in both JVM and JS main entry points, the very first thing I do is that I assign that variable to match constants defined in Version.scala

Also, I've tried the same trick with sbt-buildinfo plugin — the result is exactly the same, it generated per-platform BuildInfo.scala, which I can't use directly from shared sources.

Are there any better solutions available?

1

There are 1 answers

1
Mario Galic On BEST ANSWER

Consider pointing sourceManaged to shared/src/main/scala/src_managed directory and scoping generateVersionTask to the root project like so

val sharedSourceManaged = Def.setting(
  baseDirectory.value / "shared" / "src" / "main" / "scala" / "src_managed"
)

lazy val root = project.in(file(".")).
  aggregate(fooJS, fooJVM).
  settings(
    publish := {},
    publishLocal := {},
    sourceManaged := sharedSourceManaged.value,
    sourceGenerators in Compile += generateVersionTask.taskValue,
    cleanFiles += sharedSourceManaged.value
  )

Now sbt compile should output something like

Version file generated: /Users/mario/IdeaProjects/scalajs-cross-compile-example/shared/src/main/scala/src_managed/version/Version.scala
...
[info] Compiling 3 Scala sources to /Users/mario/IdeaProjects/scalajs-cross-compile-example/js/target/scala-2.12/classes ...
[info] Compiling 1 Scala source to /Users/mario/IdeaProjects/scalajs-cross-compile-example/target/scala-2.12/classes ...
[info] Compiling 3 Scala sources to /Users/mario/IdeaProjects/scalajs-cross-compile-example/jvm/target/scala-2.12/classes ...