Passing a project to an sbt dynamic task - sbt

I have a sbt project that includes code generation.
Part of the build.sbt is
lazy val generator = (project in file("generator")).
settings(mainClass := Some("com.example.Generator"))
lazy val generate = (project in file("generate")).
dependsOn(generator).
settings(runGeneration)
def runGeneration: SettingsDefinition = sourceGenerators in Compile += Def.taskDyn {
val cachedFun = FileFunction.cached(
streams.value.cacheDirectory / "generation"
) { (in: Set[File]) =>
val dir = (sourceManaged in Compile).value
(generator / run in Compile).toTask(" " + dir.getAbsolutePath).value
collectFiles(dir)
}
val dependentFiles = ((generator / fullClasspath in Compile) map { cp => cp.files }).taskValue.value
val genFiles = cachedFun(dependenctFiles).toSeq
Def.task {
genFiles
}
}.taskValue
This seems to work and only generate files when a dependency has changed. However, I expect to have multiple generators. Rather than copy the code, I attempted to pass the generator project to it:
lazy val generate = (project in file("generate")).
dependsOn(generator).
settings(runGeneration(generator))
def runGeneration(p: project): SettingsDefinition =
<same as before but with p instead of generator>
This results in an error parsing the build file:
build.sbt:155: error: Illegal dynamic reference: File
val dependentFiles = ((p / fullClasspath in Compile) map { cp => cp.files }).taskValue.value
^
[error] sbt.compiler.EvalException: Type error in expression
[error] sbt.compiler.EvalException: Type error in expression
I am guessing the problem is that it cannot figure out at compile time if there is a dependency loop, so it convervatively gives an error.
Is there a way to get this to work? Is there an entirely different construct that lets me know if running generator will produce a different result?

The underlying problem is that task definitions in sbt have two components, which look like they can be intermingled, but cannot. If you write code like
Def.task {
val doIt = checkIfShouldDoIt()
if (doIt) {
someTask.value
} else {
()
}
}
this naively looks like it will only run someTask if doIt is true. What actually happens is that someTask.value declares a dependency of this task on someTask and someTask is run before anything is done for this task. To write the above code in a way that more directly maps to what actually happens, one would write
Def.task {
val someTaskValue = someTask.value
val doIt = checkIfShouldDoIt()
if (doIt) {
someTaskValue
} else {
()
}
}
The attempt to run the task only when the dependencies had changed could not work in a single task.
My working solution does the following. I modified the generator to take an additional argument and do nothing if that argument was false. The two tasks were
// Task to check if we need to generate
def checkGeneration(p: Project) = Def.taskDyn {
var needToGenerate = false
val cachedFunction = FileFunction.cached(someDir) {
(in: Set[File]) =>
needToGenerate = ! in.isEmpty
Set()
}
val dependentFiles = ((p / fullClasspath in Compile) map { cp => cp.files }).taskValue
Def.task {
cachedFun(dependentFiles.value.toSet)
needToGenerate
}
}
// Task to run generation
def runGeneration(p: Project): SettingsDefinition = sourceGenerators in Compile += Def.taskDyn {
val needToGenerate = checkGeneration(p).value
Def.task {
// Run generator as before but pass needToGenerate as additional argument
...
// Used FileFunction.cached as before to find the generated files (but not run the generator)
...
}
}
It is possible that I have more dynamic tasks than I need, but this works.

Related

Conditional scalacSettings / settingKey

I want my scalacSettings to be more strict (more linting) when I issue my own command validate.
What is the best way to achieve that?
A new scope (strict) did work, but it requires to compile the project two times when you issue test. So that's not a option.
SBT custom command allows for temporary modification of build state which can be discarded after command finishes:
def validate: Command = Command.command("validate") { state =>
import Project._
val stateWithStrictScalacSettings =
extract(state).appendWithSession(
Seq(Compile / scalacOptions ++= Seq(
"-Ywarn-unused:imports",
"-Xfatal-warnings",
"...",
))
,state
)
val (s, _) = extract(stateWithStrictScalacSettings).runTask(Test / test, stateWithStrictScalacSettings)
s
}
commands ++= Seq(validate)
or more succinctly using :: convenience method for State transformations:
commands += Command.command("validate") { state =>
"""set scalacOptions in Compile := Seq("-Ywarn-unused:imports", "-Xfatal-warnings", "...")""" ::
"test" :: state
}
This way we can use sbt test during development, while our CI hooks into sbt validate which uses stateWithStrictScalacSettings.

Using runTask(...mainClass...) inside inputTask with command line args, := vs <<= or?

I want to define a task which runs MyMainClass defined in the same module where task is defined, passing task command line to it:
$ sbt
> project myModule
> myKey2 someArgument
...compiles MyMainClass
...runs MyMainClass.main("someArgument")
Without command line args, this works:
val myKey1 = taskKey[Unit]("myKey1")
lazy val myModule = project.settings(
myKey1 <<= runTask(Compile, "MyMainClass", "mode1"),
myKey1 <<= myKey1.dependsOn(compile in Compile)
)
But I could not make it with command line args. Trying to use Def.spaceDelimited().parsed with taskKey gives me compilation error explicitly saying that I must use inputKey instead; trying to use <<= with inputKey does not compile either; this compiles but does not work:
val myKey2 = inputKey[Unit]("myKey2")
lazy val myModule = project.settings(
...
myKey2 := runTask(
Compile, "MyMainClass", "mode2",
{
val args = Def.spaceDelimited().parsed.head)
// This line is executed, but MyMainClass.main() is not:
System.err.println("***** args=" + args)
args.head
}
),
myKey2 <<= myKey2.dependsOn(compile in Compile)
)
Tried SBT 0.13.7 and 0.13.9. Please help. Thanks. :)
UPD. Or maybe I'm doing this completely wrong (deprecated) way? I could not find SBT 0.13 docs mention <<= at all.
Rewritten in new style (:= instead of <<=).
This worked:
myKey1 := {
// Works without this line, but kept it for clarity and just in case:
val _ = (compile in Compile).value
runTask(Compile, "MyMainClass1", "mode1").value
},
myKey2 := {
val _ = (compile in Compile).value
runInputTask(Compile, "MyMainClass", "mode2").evaluated
}
BTW directly accessing .value in procedural style feels much conceptually simpler than old ways I used to use (I guess that was before SBT has been rewritten using macros).

How to redirect console output of command to a file?

What are the means of redirecting output from a single command to a file in sbt?
I could exit sbt, execute sbt mycommand > out.txt, and start it again, but I'm wondering if there's an alternative?
In Add a custom logger you can find more information about what's required to have a custom logger that logs to a file - use extraLoggers and add an instance of sbt.AbstractLogger that does the saving.
You may find my answer to Displaying timestamp for debug mode in SBT? useful. The example copied here:
def datedPrintln = (m: String) =>
println(s"+++ ${java.util.Calendar.getInstance().getTime()} $m")
extraLoggers := {
val clientLogger = FullLogger {
new Logger {
def log(level: Level.Value, message: => String): Unit =
if(level >= Level.Info) datedPrintln(s"$message at $level")
def success(message: => String): Unit = datedPrintln(s"success: $message")
def trace(t: => Throwable): Unit = datedPrintln(s"trace: throwable: $t")
}
}
val currentFunction = extraLoggers.value
(key: ScopedKey[_]) => clientLogger +: currentFunction(key)
}

Producing two separate jars for sources and resources with package in SBT?

Because of the large size of some resource files, I'd like sbt package to create 2 jar files at the same time, e.g. project-0.0.1.jar for the classes and project-0.0.1-res.jar for the resources.
Is this doable?
[SOLUTION] based on the answer below thanks to #gilad-hoch
1) unmanagedResources in Compile := Seq()
Now it's just classes in the default jar.
2)
val packageRes = taskKey[File]("Produces a jar containing only the resources folder")
packageRes := {
val jarFile = new File("target/scala-2.10/" + name.value + "_" + "2.10" + "-" + version.value + "-res.jar")
sbt.IO.jar(files2TupleRec("", file("src/main/resources")), jarFile, new java.util.jar.Manifest)
jarFile
}
def files2TupleRec(pathPrefix: String, dir: File): Seq[Tuple2[File, String]] = {
sbt.IO.listFiles(dir) flatMap {
f => {
if (f.isFile) Seq((f, s"${pathPrefix}${f.getName}"))
else files2TupleRec(s"${pathPrefix}${f.getName}/", f)
}
}
}
(packageBin in Compile) <<= (packageBin in Compile) dependsOn (packageRes)
Now when I do "sbt package", both the default jar and a resource jar are produced at the same time.
to not include the resources in the main jar, you could simply add the following line:
unmanagedResources in Compile := Seq()
to add another jar, you could define a new task. it would generally be something like that:
use sbt.IO jar method to create the jar.
you could use something like:
def files2TupleRec(pathPrefix: String, dir: File): Seq[Tuple2[File,String]] = {
sbt.IO.listFiles(dir) flatMap {
f => {
if(f.isFile) Seq((f,s"${pathPrefix}${f.getName}"))
else files2TupleRec(s"${pathPrefix}${f.getName}/",f)
}
}
}
files2TupleRec("",file("path/to/resources/dir")) //usually src/main/resources
or use the built-in methods from Path to create the sources: Traversable[(File, String)] required by the jar method.
that's basically the whole deal...

How do I temporarily skip running the compile task in an custom sbt command?

I'm trying to temporarily skip the compile task when running the package task from within a quick-install command (defined in a sbt plugin I'm writing). I'm able to skip all compiles by putting the skip setting on the compile task, but that causes all compile tasks to be skipped:
object MyPlugin extends Plugin {
override lazy val settings = Seq(
(skip in compile) := true
)
...
}
What I need is to only skip the compile when running my quick-install command. Is there a way to modify a setting temporarily, or to scope it to only my quick-install command?
I've tried a settings transformation (based on https://github.com/harrah/xsbt/wiki/Advanced-Command-Example), that should replace all instances of skip := false with skip := true, but it doesn't have any effect (i.e. compiles still occur after the transformation):
object SbtQuickInstallPlugin extends Plugin {
private lazy val installCommand = Command.args("quick-install", "quick install that skips compile step")(doCommand(Configurations.Compile))
override lazy val settings = Seq(
commands ++= Seq(installCommand),
(Keys.skip in compile) := false // by default, don't skip compiles
)
def doCommand(configs: Configuration*)(state: State, args: Seq[String]): State = {
val extracted = Project.extract(state)
import extracted._
val oldStructure = structure
val transformedSettings = session.mergeSettings.map(
s => s.key.key match {
case skip.key => { skip in s.key.scope := true } // skip compiles
case _ => s
}
)
// apply transformed settings (in theory)
val newStructure = Load.reapply(transformedSettings, oldStructure)
Project.setProject(session, newStructure, state)
...
}
Any idea what I'm missing and/or a better way to do this?
Edit:
The skip setting is a Task, so an easy fix was to:
object SbtQuickInstallPlugin extends Plugin {
private lazy val installCommand = Command.args("quick-install", "quick install that skips compile step")(doCommand(Configurations.Compile))
private var shouldSkipCompile = false // by default, don't skip compiles
override lazy val settings = Seq(
commands ++= Seq(installCommand),
(Keys.skip in compile) := shouldSkipCompile
)
def doCommand(configs: Configuration*)(state: State, args: Seq[String]): State = {
shouldSkipCompile = true // start skipping compiles
... // do stuff that would normally trigger a compile such as running the packageBin task
shouldSkipCompile = false // stop skipping compiles
}
}
I'm not convinced this is the most robust solution, but it appears to work for what I needed.
object SbtQuickInstallPlugin extends Plugin {
private lazy val installCommand = Command.args("quick-install", "quick install that skips compile step")(doCommand(Configurations.Compile))
private var shouldSkipCompile = false // by default, don't skip compiles
override lazy val settings = Seq(
commands ++= Seq(installCommand),
(Keys.skip in compile) := shouldSkipCompile
)
def doCommand(configs: Configuration*)(state: State, args: Seq[String]): State = {
shouldSkipCompile = true // start skipping compiles
... // do stuff that would normally trigger a compile such as running the packageBin task
shouldSkipCompile = false // stop skipping compiles
}
}
Is fine, of course you can go with that!

Resources