SBT including the version number in a program - scala

I want a program I'm building to be able to report its own version at runtime (e.g. scala myprog.jar --version). Traditionally in a maven project, I'd use resource filtering (pom.xml -> file.properties -> read value at runtime). I know there's sbt-filter-plugin to emulate this functionality, but I'm curious if there's a more standard / preferred / clever way of doing this in SBT.
tl;dr how can I read the version number defined in build.sbt at runtime?

Update...
https://github.com/ritschwumm/xsbt-reflect (mentioned above) is Obsolete, but there is this cool SBT release tool that can automatically manage versions and more: https://github.com/sbt/sbt-release.
Alternatively, if you want a quick fix you can get version from manifest like this:
val version: String = getClass.getPackage.getImplementationVersion
This value will be equal to version setting in your project which you set either in build.sbt or Build.scala.
Another Update ...
Buildinfo SBT plugin can generate a class with version number based on build.sbt:
/** This object was generated by sbt-buildinfo. */
case object BuildInfo {
/** The value is "helloworld". */
val name: String = "helloworld"
/** The value is "0.1-SNAPSHOT". */
val version: String = "0.1-SNAPSHOT"
/** The value is "2.10.3". */
val scalaVersion: String = "2.10.3"
/** The value is "0.13.2". */
val sbtVersion: String = "0.13.2"
override val toString: String = "name: %s, version: %s, scalaVersion: %s, sbtVersion: %s" format (name, version, scalaVersion, sbtVersion)
}
See the docs on how to enable it here: https://github.com/sbt/sbt-buildinfo/.

Use the xsbt-reflect plugin. It will generate a source file that contains, among other things, the project version number.

In general, without any plugins, you can do something like this:
sourceGenerators in Compile += Def.task {
val file = (sourceManaged in Compile).value / "foo" / "bar" / "BuildInfo.scala"
IO.write(
file,
s"""package foo.bar
|object BuildInfo {
| val Version = "${version.value}"
|}""".stripMargin
)
Seq(file)
}.taskValue
And then do with foo.bar.BuildInfo.Version constant whatever you like.
Or more general:
def generateBuildInfo(packageName: String,
objectName: String = "BuildInfo"): Setting[_] =
sourceGenerators in Compile += Def.task {
val file =
packageName
.split('.')
.foldLeft((sourceManaged in Compile).value)(_ / _) / s"$objectName.scala"
IO.write(
file,
s"""package $packageName
|object $objectName {
| val Version = "${version.value}"
|}""".stripMargin
)
Seq(file)
}.taskValue
Example:
settings(generateBuildInfo("foo.bar"))
You can even change this to pass object properties as a Map[String, String] and generate the object appropriately.

I ended up making the build system (I use a Makefile on top of sbt) prepare a src/main/resources/version.txt file for Scala to read.
In the Makefile:
$(RESOURCES_VERSION): build.sbt
grep "^version := " $< | cut -f2 -d\" > $#
In Scala:
val version: String = {
val src = Source.fromURL( getClass.getResource("/version.txt") )
src.getLines.next // just take the first line
}
This works for me.
It's curious that such a needed feature (I would think) is not easily available in Scala. A very simple sbt plugin just for this would be welcome.

Related

sbt: generating shared sources in cross-platform project

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?
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 ...

sbt-buildinfo generated object cannot be referenced

I'm using the aforementioned sbt plugin to get the version of the app I'm working on.
The project has sub-modules. Here is the main build.sbt
...
lazy val abandon = (project in file(".")).
aggregate(base, cli, gui).
dependsOn(base, cli, gui).
enablePlugins(BuildInfoPlugin).
settings(commonSettings: _*).
settings(
name := "abandon",
fork in run := true,
buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion),
buildInfoPackage := "co.uproot.abandon"
)
lazy val base = (project in file("base")).
settings(commonSettings: _*).
settings(
name := "abandon-base",
fork in run := true
)
lazy val cli = (project in file("cli")).
dependsOn(base).
settings(commonSettings: _*).
settings(
name := "abandon-cli",
fork in run := true
)
lazy val gui = (project in file("gui")).
dependsOn(base).
settings(commonSettings: _*).
settings(
name := "abandon-gui",
fork in run := true
)
The generated BuildInfo.scala is located under target/scala-2.11/src_managed/main/sbt-buildinfo/BuildInfo.scala as expected.
package co.uproot.abandon
import scala.Predef._
/** This object was generated by sbt-buildinfo. */
case object BuildInfo {
/** The value is "abandon". */
val name: String = "abandon"
/** The value is "0.3.1". */
val version: String = "0.3.1"
/** The value is "2.11.8". */
val scalaVersion: String = "2.11.8"
/** The value is "0.13.12". */
val sbtVersion: String = "0.13.12"
override val toString: String = {
"name: %s, version: %s, scalaVersion: %s, sbtVersion: %s" format (
name, version, scalaVersion, sbtVersion
)
}
}
When I go to a file inside the package co.uproot.abandon and try to reference BuildInfo.version I get
Error:(256, 42) object BuildInfo is not a member of package co.uproot.abandon
co.uproot.abandon.BuildInfo.version
I read about the problem with submodules and this plugin here and ultimately tried this workaround but that didn't help.
Any help will be appreciated!
I wanted to add an image to illustrate zaxme answer, but I can't in the comments, so I am adding another answer to add more information about it.
So,
1 - Right click on target/scala-2.11/src_managed/main:
And,
2 - Select Mark Directory as and Unmark Generated Sources Root:
Then rebuild, it should work.
Issue solved.
Just make sure to set the main part of target/scala-2.11/src_managed/main/sbt-buildinfo/BuildInfo.scala as source in the Project structure for the particular sub-module you would like to use it for and not the whole project.
Other than that the syntax highlighting is screwed so it will show up the same way as before i.e. when it wasn't compiling. To avoid that follow the link in this that I posted in the question.

How can one pass the Scala version string to a code generator for SBT?

When using code generators with SBT, one uses constructs like
def genFile(out: File): Seq[File] = {
val file = new File(out, "generated.scala")
// Add stuff to file
Seq(file)
}
(sourceGenerators in Compile) <+= (sourceManaged in Compile) map (genFile _)
If your generator needs the Scala version string, how do you pass it in? Using scalaVersion.value in genFile results in an error.
This is the good old way. I'm sure there is a newer approach where you define genFile as a custom task and that would enable scalaVersion.value.
// build.sbt
scalaVersion := "2.11.2"
def genFile(out: File, v: String): Seq[File] = {
out.mkdirs()
val f = out / "generated.scala"
val w = new java.io.FileOutputStream(f)
w.write(s"""package object foo {
| val scalaVersion = "$v"
|}
|""".stripMargin.getBytes("UTF-8"))
w.close()
Seq(f)
}
(sourceGenerators in Compile) <+=
(sourceManaged in Compile, scalaVersion in Compile) map genFile
$ sbt console
...
[info] Starting scala interpreter...
[info]
Welcome to Scala version 2.11.2 (OpenJDK 64-Bit Server VM, Java 1.7.0_65).
Type in expressions to have them evaluated.
Type :help for more information.
scala> foo.scalaVersion
res0: String = 2.11.2

sbt test:doc Could not find any member to link

I'm attempting to run sbt test:doc and I'm seeing a number of warnings similar to below:
[warn] /Users/tleese/code/my/stuff/src/test/scala/com/my/stuff/common/tests/util/NumberExtractorsSpecs.scala:9: Could not find any member to link for "com.my.stuff.common.util.IntExtractor".
The problem appears to be that Scaladoc references from test sources to main sources are not able to link correctly. Any idea what I might be doing wrong or need to configure?
Below are the relevant sections of my Build.scala:
val docScalacOptions = Seq("-groups", "-implicits", "-external-urls:[urls]")
scalacOptions in (Compile, doc) ++= docScalacOptions
scalacOptions in (Test, doc) ++= docScalacOptions
autoAPIMappings := true
Not sure if this is a satisfactory solution, but...
Scaladoc currently expects pairs of jar and URL to get the external linking to work. You can force sbt to link internal dependencies using JARs using exportJars. Compare the value of
$ show test:fullClasspath
before and after setting exportJars. Next, grab the name of the JAR that's being used and link it to the URL you'll be uploading it to.
scalaVersion := "2.11.0"
autoAPIMappings := true
exportJars := true
scalacOptions in (Test, doc) ++= Opts.doc.externalAPI((
file(s"${(packageBin in Compile).value}") -> url("http://example.com/")) :: Nil)
Now I see that test:doc a Scaladoc with links to http://example.com/index.html#foo.IntExtractor from my foo.IntExtractor.
Using ideas from Eugene's answer I made a following snippet.
It uses apiMapping sbt variable as adviced in sbt manual.
Unfortunately it doesn't tell how to deal with managed dependencies, even the subsection title says so.
// External documentation
/* You can print computed classpath by `show compile:fullClassPath`.
* From that list you can check jar name (that is not so obvious with play dependencies etc).
*/
val documentationSettings = Seq(
autoAPIMappings := true,
apiMappings ++= {
// Lookup the path to jar (it's probably somewhere under ~/.ivy/cache) from computed classpath
val classpath = (fullClasspath in Compile).value
def findJar(name: String): File = {
val regex = ("/" + name + "[^/]*.jar$").r
classpath.find { jar => regex.findFirstIn(jar.data.toString).nonEmpty }.get.data // fail hard if not found
}
// Define external documentation paths
Map(
findJar("scala-library") -> url("http://scala-lang.org/api/" + currentScalaVersion + "/"),
findJar("play-json") -> url("https://playframework.com/documentation/2.3.x/api/scala/index.html")
)
}
)
This is a modification of the answer by #phadej. Unfortunately, that answer only works on Unix/Linux because it assumes that the path separator is a /. On Windows, the path separator is \.
The following works on all platforms, and is slightly more idiomatic IMHO:
/* You can print the classpath with `show compile:fullClassPath` in the SBT REPL.
* From that list you can find the name of the jar for the managed dependency.
*/
lazy val documentationSettings = Seq(
autoAPIMappings := true,
apiMappings ++= {
// Lookup the path to jar from the classpath
val classpath = (fullClasspath in Compile).value
def findJar(nameBeginsWith: String): File = {
classpath.find { attributed: Attributed[java.io.File] => (attributed.data ** s"$nameBeginsWith*.jar").get.nonEmpty }.get.data // fail hard if not found
}
// Define external documentation paths
Map(
findJar("scala-library") -> url("http://scala-lang.org/api/" + currentScalaVersion + "/"),
findJar("play-json") -> url("https://playframework.com/documentation/2.3.x/api/scala/index.html")
)
}
)

Can I access my Scala app's name and version (as set in SBT) from code?

I am building an app with SBT (0.11.0) using a Scala build definition like so:
object MyAppBuild extends Build {
import Dependencies._
lazy val basicSettings = Seq[Setting[_]](
organization := "com.my",
version := "0.1",
description := "Blah",
scalaVersion := "2.9.1",
scalacOptions := Seq("-deprecation", "-encoding", "utf8"),
resolvers ++= Dependencies.resolutionRepos
)
lazy val myAppProject = Project("my-app-name", file("."))
.settings(basicSettings: _*)
[...]
I'm packaging a .jar at the end of the process.
My question is a simple one: is there a way of accessing the application's name ("my-app-name") and version ("0.1") programmatically from my Scala code? I don't want to repeat them in two places if I can help it.
Any guidance greatly appreciated!
sbt-buildinfo
I just wrote sbt-buildinfo.
After installing the plugin:
lazy val root = (project in file(".")).
enablePlugins(BuildInfoPlugin).
settings(
buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion),
buildInfoPackage := "foo"
)
Edit: The above snippet has been updated to reflect more recent version of sbt-buildinfo.
It generates foo.BuildInfo object with any setting you want by customizing buildInfoKeys.
Ad-hoc approach
I've been meaning to make a plugin for this, (I wrote it) but here's a quick script to generate a file:
sourceGenerators in Compile <+= (sourceManaged in Compile, version, name) map { (d, v, n) =>
val file = d / "info.scala"
IO.write(file, """package foo
|object Info {
| val version = "%s"
| val name = "%s"
|}
|""".stripMargin.format(v, n))
Seq(file)
}
You can get your version as foo.Info.version.
Name and version are inserted into manifest. You can access them using java reflection from Package class.
val p = getClass.getPackage
val name = p.getImplementationTitle
val version = p.getImplementationVersion
You can also generate a dynamic config file, and read it from scala.
// generate config (to pass values from build.sbt to scala)
Compile / resourceGenerators += Def.task {
val file = baseDirectory.value / "conf" / "generated.conf"
val contents = "app.version=%s".format(version.value)
IO.write(file, contents)
Seq(file)
}.taskValue
When you run sbt universal:packageBin the file will be there.