Clarification on scope delegation of dockerExposedPorts with sbt-native-packager - scala

I am using the sbt-native-packager plugin that comes with Scala Play:
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
I would like to know why the dockerExposedPorts setting isn't set for the root project when using:
name := """scala-play-react-seed"""
version := "1.0-SNAPSHOT"
lazy val root = (project in file("."))
.enablePlugins(PlayScala)
.settings(
Docker / dockerExposedPorts := Seq(9000), // <-- 1. doesn't work
)
Docker / dockerExposedPorts := Seq(9000), // <-- 2. also doesn't work
$ sbt show root/dockerExposedPorts
[info] *
However, it works if i remove the Docker config part:
.settings(
dockerExposedPorts := Seq(9000), // <-- 3. works
)
$ sbt show root/dockerExposedPorts
[info] * 9000
As far is i understand sbt's scope delegation, case 1. is scoped as root / Docker / Zero / dockerExposedPorts, which should be more specific than case 3. root / Zero / Zero / dockerExposedPorts. What am i missing here?

Yes, Docker / dockerExposedPorts is more specific than Zero / dockerExposedPorts. But unlike what you seem to assume, it's the more specific scopes that delegate to the less specific ones, not the other way around. The manual says so:
This feature allows you to set a value once in a more general scope, allowing multiple more-specific scopes to inherit the value.
And in fact, this is the only way it could be, because you might define e. g. both Docker / dockerExposedPorts and Universal / dockerExposedPorts. Which one of these would Zero / dockerExposedPorts delegate to? There's no sensible answer to that, hence delegation goes in the other direction.

Related

Environment variables set up from build.sbt

I have been trying to set up environment variables directly from build.sbt file as I need to use an assembly jar name which is defined in this file. So I've been trying to output the result of defined environment variable using echo and in the context of the Scala application code sys.props.get("SPARK_APP_JAR_PATH") but the result is blank or None, respectively.
What can be wrong with the environment variable configuration?
This how I defined the env variable in build.sbt:
assemblyJarName in assembly := "spark_app_example_test.jar"
mainClass in assembly := Some("example.SparkAppExample")
test in assembly := {}
fork := true
val envVars = sys.props("SPARK_APP_JAR_PATH") = "/path/to/jar/file"
That's how it works for me:
lazy val dockerRepo: String = sys.props.getOrElse("DOCKER_REPO", s"bpf.docker.repo")
Or with your example:
lazy val envVars = sys.props.getOrElse("SPARK_APP_JAR_PATH", "/path/to/jar/file")
According to the documentation:
System properties can be provided either as JVM options, or as SBT arguments, in both cases as -Dprop=value. The following properties influence SBT execution.
e.g. sbt -DSPARK_APP_JAR_PATH=/path/to/jar/file
See https://www.scala-sbt.org/release/docs/Command-Line-Reference.html#Command+Line+Options

Multiple SBT Configurations should be exclusive, but they all activate at the same time - why?

I have defined a minimal build.sbt with two custom profiles ‘dev’ and ‘staging’ (what SBT seems to call Configurations). However, when I run SBT with the Configuration that was defined first in the file (dev), both Configuration blocks are executed - and if both modify the same setting, the last one wins (staging).
This seems to break any notion of conditional activation, so what am I doing wrong with SBT?
For reference, I want to emulate the conditionally activated Profiles concept of Maven e.g. mvn test -P staging.
SBT version: 1.2.1
build.sbt:
name := "example-project"
scalaVersion := "2.12.6"
...
fork := true
// Environment-independent JVM property (always works)
javaOptions += "-Da=b"
// Environment-specific JVM property (doesn’t work)
lazy val Dev = config("dev") extend Test
lazy val Staging = config("staging") extend Test
val root = (project in file("."))
.configs(Dev, Staging)
.settings(inConfig(Dev)(Seq(javaOptions in Test += "-Dfoo=bar")))
.settings(inConfig(Staging)(Seq(javaOptions in Test += "-Dfoo=qux")))
Command:
# Bad
sbt test
=> foo=qux
a=b
# Bad
sbt clean dev:test
=> foo=qux
a=b
# Good
sbt clean staging:test
=> foo=qux
a=b
Notice that despite of the inConfig usage you're still setting javaOptions in Test, i.e. in the Test config. If you remove in Test, it works as expected:
...
.settings(inConfig(Dev)(javaOptions += "-Dfoo=bar"))
.settings(inConfig(Staging)(javaOptions += "-Dfoo=qux"))
(also Seq(...) wrapping is unnecessary)
Now in sbt:
> show Test/javaOptions
[info] *
> show Dev/javaOptions
[info] * -Dfoo=bar
> show Staging/javaOptions
[info] * -Dfoo=qux
You can achieve the same result by scoping each setting explicitly (without inConfig wrapping):
.settings(
Dev/javaOptions += "-Dfoo=bar",
Staging/javaOptions += "-Dfoo=qux",
...
)
(here Conf/javaOptions is the same as javaOptions in Conf)

How do I create a tarball and a zip for a single module using distinct configurations?

I have a multi-project build with a particularly messy module which contain several mainClasses. I would like to create several distribution packages for this messy module, each distribution package employing distinct file sets and employing different formats. Ideas?
This is the answer from the sbt-nativer-packager issue tracker where the same question was posted.
I'm adding this from the gitter chat as well:
I'm just arriving in this chat room and my knowledge of sbt-native-packager is virtually zero... but anyway... looks to me that JavaAppPackaging and other archetypes should actually be configurations extended from Universal. In this scenario, I would just create my own configuration extended from JavaAppPackaging and tweak the necessary bits according to my needs. And, finally, if the plugin just picks mappings in ThisScope... it would pick my own scope, and not JavaAppPackaging... and not Universal.
So, let's go through this one by one.
The sbt-native-packager plugin always pick mappings in Universal. This is not ideal. It should conceptually pick mappings in ThisScope
SBT native packager provides two categories of AutoPlugins: FormatPlugins and ArchetypePlugins. FormatPlugins provide a new package format, e.g. UniversalPlugin (zip, tarball) or DebianPlugins (.deb). These plugins form a a hierarchy as they are build on top of each other:
SbtNativePackager
+
|
|
+-------+ Universal +--------+
| |
| + |
| | |
+ + +
Docker +-+ Linux +-+ Windows
| |
| |
+ +
Debian RPM
mappings, which define a file -> targetpath relation, are inherited with this pattern
mappings in ParentFormatPluginScope := (mappings in FormatPluginScope).value
So for docker it looks like this
mappings in Docker := (mappings in Universal).value
The linux format plugins use specialized mappings to preserve file permissions, but are basically the same.
Since sbt-native-packager plugin always pick mappings in Universal, I have to redefine mappings in Universal in each of my configurations
Yes. If you want to define your own scope and inherit the mappings and change them you have to do this, like all other packaging plugins, too. I recommend putting this code into custom AutoPlugins in your project folder.
For example (not tested, imports may be missing )
import sbt._
object BuilderRSPlugin extends AutoPlugin {
def requires = JavaAppPackaging
object autoImport {
val BuilderRS = config("builderrs") extend Universal
}
import autoImport._
override lazy val projectSettings = Seq(
mappings in BuilderRS := (mappings in Universal).value
)
}
looks to me that JavaAppPackaging and other archetypes should actually be configurations extended from Universal
JavaAppPackaging is an archetype, which means this plugin doesn't bring any new packaging formats, thus no new scopes. It configures all the packaging formats it can and enables them.
You package stuff by specifying the scope:
universal:packageBin
debian:packageBin
windows:packageBin
So if you need to customize your output format you are doing this in the respecting scope.
mappings in Docker := (mappings in Docker).value.filter( /* what ever you want to filter */)
See: https://github.com/sbt/sbt-native-packager/issues/746
IMPORTANT: This is an "answer in progress". IT DOES NOT WORK YET!
This is an example of how one could achieve this.
The basic idea is that we add configurations for different packages to be generated. Each configuration tells which files will be present in the package. This does not work as expected. See my comments after the code.
lazy val BuilderRS = sbt.config("BuilderRS").extend(Compile,Universal)
lazy val BuilderRV = sbt.config("BuilderRV").extend(Compile,Universal)
addCommandAlias("buildRS", "MessyModule/BuilderRS:packageZipTarball")
addCommandAlias("buildRV", "MessyModule/BuilderRV:packageBin") // ideally should be named packageZip
lazy val Star5FunctionalTestSupport =
project
.in(file("MessyModule"))
.enablePlugins(JavaAppPackaging)
.settings((buildSettings): _*)
.configs(Universal,BuilderRS,BuilderRV)
.settings(inConfig(BuilderRS)(
Defaults.configSettings ++ JavaAppPackaging.projectSettings ++
Seq(
executableScriptName := "rs",
mappings in Universal :=
(mappings in Universal).value
.filter {
case (file, name) => ! file.getAbsolutePath.endsWith("/bin/rv")
},
topLevelDirectory in Universal :=
Some(
"ftreports-" +
new java.text.SimpleDateFormat("yyyyMMdd_HHmmss")
.format(new java.util.Date())),
mainClass in ThisScope := Option(mainClassRS))): _*)
//TODO: SEE COMMENTS BELOW ===============================================
// .settings(inConfig(BuilderRV)(
// Defaults.configSettings ++ JavaAppPackaging.projectSettings ++
// Seq(
// packageBin <<= packageBin in Universal,
// executableScriptName := "rv",
// mappings in ThisScope :=
// (mappings in Universal).value
// .filter {
// case (file, name) => ! file.getAbsolutePath.endsWith("/bin/rs")
// },
// topLevelDirectory in Universal :=
// Some(
// "ftviewer-" +
// new java.text.SimpleDateFormat("yyyyMMdd_HHmmss")
// .format(new java.util.Date())),
// mainClass in ThisScope := Option(mainClassRV))): _*)
Now observe configuration BuilderRV which in comments.
It is basically the same thing as configuration BuilderRS, except that we are now deploying a different shell script in the bin folder. There some other small differences, but not relevant to this argumentation. There are two problems:
The sbt-native-packager plugin always pick mappings in Universal. This is not ideal. It should conceptually pick mappings in ThisScope.
Since sbt-native-packager plugin always pick mappings in Universal, I have to redefine mappings in Universal in each of my configurations. And this is a problem because mappings in Universal is defined as a function of itself in all configurations: the result is that we ended up chaining logic to mapppings in Universal each time we redefined it in each configuration. This causes trouble in this example in particular because the configuration BuilderRV (the second one) will perform not only its filter, but also the filter defined in BuilderRS (the first one), which is not what I want.

How to pass scalacOptions (Xelide-below) to sbt via command line

I am trying to call sbt assembly from the command line passing it a scalac compiler flag to elides (elide-below 1).
I have managed to get the flag working in the build.sbt by adding this line to the build.sbt
scalacOptions ++= Seq("-Xelide-below", "1")
And also it's working fine when I start sbt and run the following:
$> sbt
$> set scalacOptions in ThisBuild ++=Seq("-Xelide-below", "0")
But I would like to know how to pass this in when starting sbt, so that my CI jobs can use it while doing different assembly targets (ie. dev/test/prod).
One way to pass the elide level as a command line option is to use system properties
scalacOptions ++= Seq("-Xelide-below", sys.props.getOrElse("elide.below", "0"))
and run sbt -Delide.below=20 assembly. Quick, dirty and easy.
Another more verbose way to accomplish the same thing is to define different commands for producing test/prod artifacts.
lazy val elideLevel = settingKey[Int]("elide code below this level.")
elideLevel in Global := 0
scalacOptions ++= Seq("-Xelide-below", elideLevel.value.toString)
def assemblyCommand(name: String, level: Int) =
Command.command(s"${name}Assembly") { s =>
s"set elideLevel in Global := $level" ::
"assembly" ::
s"set elideLevel in Global := 0" ::
s
}
commands += assemblyCommand("test", 10)
commands += assemblyCommand("prod", 1000)
and you can run sbt testAssembly prodAssembly. This buys you a cleaner command name in combination with the fact that you don't have to exit an active sbt-shell session to call for example testAssembly. My sbt-shell sessions tend to live for a long time so I personally prefer the second option.

Combine several sbt tasks into one

I'm a little confused on the Scala/SBT documentation for creating Scala tasks. Currently I can run the following from the command line:
sbt ";set target := file(\"$PWD/package/deb-upstart\"); set serverLoading in Debian := com.typesafe.sbt.packager.archetypes.ServerLoader.Upstart; debian:packageBin; set target := file(\"$PWD/package/deb-systemv\"); set serverLoading in Debian := com.typesafe.sbt.packager.archtypes.ServerLoader.SystemV; debian:packageBin; set target := file(\"$PWD/package/rpm-systemd\"); rpm:packageBin"
This resets my target each time to a different directory (deb-upstart, deb-systemv and rpm-systemd) and runs an sbt-native-package task for each of those settings. (Yes, I realizing I'm compiling it three different times; but sbt-native-packager doesn't seems to have a setting for the artifact directory)
This works fine from a bash prompt, but I've been trying to put the same target into jenkins (replacing $PWD with $WORKSPACE) and I can't seem to get the quote escaping correct. I thought it might be easier just to have a task in either by build.sbt or project/Build.scala that runs all three of those tasks, changing out the target variable each time (and replacing $PWD or $TARGET with the full path of the base directory).
I've attempted the following:
lazy val packageAll = taskKey[Unit]("Creates deb-upstart, deb-systenv and rpm-systemd packages")
packageAll := {
target := baseDirectory.value / "package" / "deb-upstart"
serverLoading in Debian := com.typesafe.sbt.packager.archetypes.ServerLoader.Upstart
(packageBin in Debian).value
target := baseDirectory.value / "package" / "deb-systemv"
serverLoading in Debian := com.typesafe.sbt.packager.archetypes.ServerLoader.SystemV
(packageBin in Debian).value
target := baseDirectory.value / "package" / "rpm-systemd"
(packageBin in Rpm).value
}
But the trouble is the .value causes the tasks to get evaluated before my task is even run, so they don't get the new target setting (as stated in this other question: How can I call another task from my SBT task?)
So, I figured this out for you :)
As you already mentioned, combining a multiple tasks in a single one where some of the tasks depend on the same setting, doesn't work out as expected.
Instead we do the following
Create a task for each of our custom steps, e.g. packaging debian for upstart
Define an alias that executes these commands in order
Define tasks
lazy val packageDebianUpstart = taskKey[File]("creates deb-upstart package")
lazy val packageDebianSystemV = taskKey[File]("creates deb-systenv package")
lazy val packageRpmSystemD = taskKey[File]("creates rpm-systenv package")
Example task implementation
The implementation is pretty simple and the same for each of
the tasks.
// don't forget this
import com.typesafe.sbt.packager.archetypes._
packageDebianUpstart := {
// define where you want to put your stuff
val output = baseDirectory.value / "package" / "deb-upstart"
// run task
val debianFile = (packageBin in Debian).value
// place it where you want
IO.move(debianFile, output)
output
}
Define alias
And now compose these tasks into a single alias with
addCommandAlias("packageAll", "; clean " +
"; set serverLoading in Debian := com.typesafe.sbt.packager.archetypes.ServerLoader.SystemV" +
"; packageDebianSystemV " +
"; clean " +
"; set serverLoading in Debian := com.typesafe.sbt.packager.archetypes.ServerLoader.Upstart" +
"; packageDebianUpstart " +
"; packageRpmSystemD")
You can look at the complete code here.
Update
Setting the SystemLoader inside the alias seems to be the right
way to solve this. A clean is unfortunately necessary between
each build for the same output format.