Different project on the same base-directory (or share a single file between projects) - scala

My initial setup had two separate projects (sbt 1.2.6);
a web app (huge codebase, lot of dependencies, slow compile)
a command-line app (basically one file with 3-4 separate dependencies)
The feature request came in; we should show the "valid" values in the command line app. The valid values are in an enum in the web app. So I fired up the sbt documentation and came up with an idea which looked like this;
//main webapp
lazy val core = project
.in(file("."))
.withId("core") //I tried this just in case, not helped...
//... here comes all the plugins and deps
//my hack to get a single-file compile
lazy val `feature-signer-helper` = project
.in(file("."))
.withId("feature-signer-helper")
.settings(
target := { baseDirectory.value / "target" / "features" },
sources in Compile := {
((scalaSource in Compile).value ** "Features.scala").get
}
)
//the command line app
lazy val `feature-signer` = project
.in(file("feature-signer"))
.dependsOn(`feature-signer-helper`)
.settings(
libraryDependencies ++= signerDeps
)
The problem is that it seems like, that whatever the last lazy val xxx = project.in(file(y)) that will be the only project for the y dir.
Also, I don't want to move that one file to a separate directory structure... And logically the command line app and the web app are not "depends on" each other, they have different dependencies (and really different build times).
My questions are;
is there any quick-win in this situation? (I will copy the file worst-case...)
why we have this rich project and source settings if I can't bind them to the same dir?
EDIT:
The below code can copy the needed file (if you have the same dir structure). I'm not super happy with it, but it works. Still interested in other methods.
import sbt._
import Keys._
object FeaturesCopyTask {
val featuresCopyTask = {
sourceGenerators in Compile += Def.task {
val outFile = (sourceManaged in Compile).value / "Features.scala"
val rootDirSrc = (Compile / baseDirectory).value / ".." / "src"
val inFile: File = (rootDirSrc ** "Features.scala").get().head
IO.copyFile(inFile, outFile, preserveLastModified = true)
Seq(outFile)
}.taskValue
}
}
lazy val `feature-signer` = project
.in(file("feature-signer"))
.settings(
libraryDependencies ++= signerDeps,
FeaturesCopyTask.featuresCopyTask
)

I would have the tree be something more like
+- core/
+- webapp/
+- cli/
core is the small amount (mostly model type things) that webapp and cli have in common
webapp depends on core (among many other things)
cli depends on core (and not much else)
So the build.sbt would be something like
lazy val core = (project in file("core"))
// yadda yadda yadda
lazy val webapp = (project in file("webapp"))
.dependsOn(core)
// yadda yadda yadda
lazy val cli = (project in file("cli"))
.dependsOn(core)
// yadda yadda yadda
lazy val root = (project in file("."))
.aggregate(
core,
webapp,
cli
)

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

Per-project tasks in SBT

My .sbt file looks something like this:
lazy val common = (project in file("./common"))
.settings(
// other settings
)
lazy val one = (project in file("./one"))
.dependsOn(common)
.settings(
// other settings
addCommandAlias("build", ";clean;assembly;foo")
)
lazy val two = (project in file("./two"))
.dependsOn(common)
.settings(
// other settings
addCommandAlias("build", ";clean;compile;bar")
)
Additionally I have two tasks foo and bar which are only valid in their respective projects.
My tests show that upon calling build - no matter which project I am in - both aliases are being called.
And for tasks, the keys can already be only defined at top-level of the .sbt file (e.g. val foo = taskKey[Unit]("Does foo")).
I want to know how to correctly implement tasks and command aliases on project level.
Is that possible?
The problem you are having is with alias in sbt. When an alias is defined, it is attached to scope GlobalScope in the form of a command and therefore available for all sub-projects. When you do multiple definitions of aliases with addCommandAlias, the last execution wins as every executions removes previously created alias with the same name.
You can see the defined alias by running sbt alias and it will print that there is only one build alias.
You could achieve separations of build by introducing it as a taskKey
lazy val build = taskKey[Unit]("Builds")
lazy val root = (project in file("."))
.aggregate(one, two) // THIS IS NEED TO MAKE build TASK AVAILABLE IN ROOT
lazy val common = (project in file("./common"))
.settings(
//SOME KEYS
)
lazy val one = (project in file("./one"))
.dependsOn(common)
.settings(
build := {
Def.sequential(clean, Compile / compile).value
}
)
lazy val two = (project in file("./two"))
.dependsOn(common)
.settings(
build := {
Def.sequential(clean, assembly).value
}
)
EDIT: Added Def.sequential as suggested by #laughedelic in the comments

How to create a generic SBT root project with varying sub projects

I'm working on the Scala track in Exercism which means I have a lot of SBT projects in a root folder. I'd like to create a root SBT project which will automatically add new sub-projects as I download new exercises. Currently I have to add them manually, so my root build.sbt looks like this:
lazy val root = (project in file("."))
.aggregate(
hello_world,
sum_of_multiples,
robot_name)
lazy val hello_world = project in file("hello-world")
lazy val sum_of_multiples = project in file("sum-of-multiples")
lazy val robot_name = project in file("robot-name")
...but I'd like to avoid having to add every project manually. Is there a way to add new projects automatically?
I'd like to avoid having to add every project manually. Is there a way to add new projects automatically?
Sure. It's a bit advanced use of sbt, but you can create an ad-hoc plugin that generates subprojects programmatically.
build.sbt
ThisBuild / scalaVersion := "2.12.8"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organization := "com.example"
ThisBuild / organizationName := "example"
project/build.properties
sbt.version=1.2.8
project/SubprojectPlugin.scala
import sbt._
object SubprojectPlugin extends AutoPlugin {
override val requires = sbt.plugins.JvmPlugin
override val trigger = allRequirements
override lazy val extraProjects: Seq[Project] = {
val dirs = (file(".") * ("*" -- "project" -- "target")) filter { _.isDirectory }
dirs.get.toList map { dir =>
Project(dir.getName.replaceAll("""\W""", "_"), dir)
}
}
}
Now if you start up sbt, any directories that are not named target or project will result to a subproject.
sbt:generic-root> projects
[info] In file:/private/tmp/generic-root/
[info] * generic-root
[info] hello_world
[info] robot_name
[info] sum_of_multiple
hello-world/build.sbt
To add further settings you can create build.sbt file under the directory as follows:
libraryDependencies += "commons-io" % "commons-io" % "2.6"

SBT `dependsOn` Per-configuration dependency

I have a Build with two projects in it.
I want to make the root project classpath dependent on subProject, but only in certain configuration. Simplified project's config:
Subproject:
object HttpBuild{
import Dependencies._
lazy val http: Project = Project(
"http",
file("http"),
settings =
CommonSettings.settings ++
Seq(
version := "0.2-SNAPSHOT",
crossPaths := false,
libraryDependencies ++= akkaActor +: spray) ++
Packaging.defaultPackageSettings
)}
Root:
object RootBuild extends Build {
import HttpBuild._
lazy val http = HttpBuild.http
lazy val MyConfig = config("myconfig") extend Compile
private val defaultSettings = Defaults.coreDefaultSettings
lazy val api = Project("root", file("."))
.configs(MyConfig)
.settings(defaultSettings: _*)
.dependsOn(HttpBuild.http % MyConfig)
}
Now if i type myconfig:compile i want to have my root project compiled with subproject, but it doesn't seem to happen.
If i leave dependencies like this dependsOn(HttpBuild.http), it compiles, but it happens every time, no matter which configuration i use.
Have you looked at this example. I'm not an expert here, but comparing with your code above, the difference seems to be
that a CustomCompile configuration is defined and used as classpathConfiguration in Common := CustomCompile
that the dependency is indirect http % "compile->myconfig"
Perhaps try to get closer to that example.

Why does sbt report "not found: value PlayScala" with Build.scala while build.sbt works?

I am creating a multi-module sbt project, with following structure:
<root>
----build.sbt
----project
----Build.scala
----plugins.sbt
----common
----LoggingModule
LoggingModule is a Play Framework project, while common is a simple Scala project.
In plugins.sbt:
resolvers += "Typesafe repo" at "http://repo.typesafe.com/typesafe/releases/"
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.3.3")
While I have this in build.sbt, all works fine and it recognises PlayScala:
name := "Multi-Build"
lazy val root = project.in(file(".")).aggregate(common, LoggingModule).dependsOn(common, LoggingModule)
lazy val common = project in file("common")
lazy val LoggingModule = (project in file("LoggingModule")).enablePlugins(PlayScala)
However as soon I put this in project/Build.scala instead of `build.sbt' as follows:
object RootBuild extends Build {
lazy val root = project.in(file("."))
.aggregate(common, LoggingModule)
.dependsOn(common, LoggingModule)
lazy val common = project in file("common")
lazy val LoggingModule = (project in file("LoggingModule")).enablePlugins(PlayScala)
...//other settings
}
it generates error as:
not found: value PlayScala
lazy val LoggingModule = (project in file("LoggingModule")).enablePlugins(PlayScala)
^
How to solve the issue?
It's just a missing import.
In .sbt files, some things are automatically imported by default: contents of objects extending Plugin, and (>= 0.13.5) autoImport fields in AutoPlugins. This is the case of PlayScala.
In a Build.scala file, normal Scala import rules apply. So you have to import things a bit more explicitly. In this case, you need to import play.PlayScala (or use .enabledPlugins(play.PlayScala) directly).