4

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?

GreyCat
  • 16,622
  • 18
  • 74
  • 112
  • What do you mean by "it's impossible to access the contents of these files from `shared`"? It's totally possible to call platform-specific code from shared code. – Travis Brown Apr 22 '19 at 10:23
  • @TravisBrown For me, it does not work — it looks like `shared` is compiled separately and `jvm` and `js` are not in its class path. Moreover, the mere fact that current solution with two `Version.scala` files work actually confirms that: if Version.scala were available, then it would have been a conflict of having 2 files with the same name. – GreyCat Apr 23 '19 at 13:18

1 Answers1

2

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 ...
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • Thanks, @Mario, this indeed works like a charm! I feel so bad for copy-pasting this solution blindly without understanding why it works, and why my previous attempt didn't — but that's it, problem solved! – GreyCat Apr 23 '19 at 18:39