An example of this comes from a sample github project:
libraryDependencies ++= Seq(
"javax.servlet" % "servlet-api" % "2.5" % "provided->default",
...
}
I'm only vaguely clear on what the 'fourth column' in these configurations mean, but this is the first time I've seen either provided or provided->default, and it's unclear how I can go about finding what should be expected here in the documentation. Can anyone help explain this construct?
It means that your provided configuration depends on the default configuration of "java.servlet" % "servlet-api" % "2.5".
Maven scopes describe what these configurations or scopes mean.
For instance, if you're using a library to write your tests, you've probably come across something like "org.scalacheck" %% "scalacheck" % "1.13.2" % "test" or similar. Here, the second part of the configuration is omitted and refers to the default configuration (usually compile). Equivalently, you could write "org.scalacheck" %% "scalacheck" % "1.13.2" % "test->compile". It means that your test configuration depends on the default configuration of ScalaCheck: your tests need ScalaCheck on the class path to compile and run.
You may find more details in the Ivy documentation.
Related
I would like to have a different version of library in test scope.
I was hoping the simplified version of project descriptor could look like that.
Please mind it's a simplified view, in my real project it's more convoluted. I need to use dependencyOverrides to enforce certain library version.
import sbt._, Keys._
organization := "me"
name := "test"
version := "0.1"
libraryDependencies := Seq("ch.qos.logback" % "logback-classic" % "1.2.3")
dependencyOverrides := Seq(
"ch.qos.logback" % "logback-classic" % "1.2.2"
)
dependencyOverrides in Test := Seq(
"ch.qos.logback" % "logback-classic" % "1.2.1"
)
I'd be hoping to see logback-classic version 1.2.1 when I run:
show test:managedClasspath.
Instead I get logback-classic version 1.2.2 as if dependencyOverrides in Test was ignored.
At the same time when I run show Test/dependencyOverrides I get the expected result which is:
ch.qos.logback:logback-classic:1.2.1
Does anyone has a clue what am I missing in the relation between dependencyOverrides in Test scope and managedClasspath?
It appears the problem cannot be solve the way I imagined. The main reason is libraryDependencies and update are not scoped to configuration.
I think the best solution is in case I need a different version of library in some tests to extract those tests to a separate module with independent set of libraryDependencies.
In the following setting, I suppose the format is "groupId" % "artifactId" % "version"
libraryDependencies += "org.specs2" % "specs2_2.10" % "1.14" % "test"
What does test mean?
As described here
Declaring a dependency looks like this, where groupId, artifactId, and revision are strings:
libraryDependencies += groupID % artifactID % revision
or like this, where configuration can be a string or Configuration val:
libraryDependencies += groupID % artifactID % revision % configuration
So the 4th % meaning is to add a dependency only to a certain configuration. In your example it is "test", which could also be written as Test.
The meaning is that you don't usually need to keep in your runtime classpath classes for test framework which you only use in staging environment and never use in production.
To learn more about configurations you can read this.
I'd like to create a compile configuration which is the same as the default one but adds a compiler plugin. In my particular case, I want to have a "dev" configuration but with the linter plugin (https://github.com/HairyFotr/linter) because it slows down compile times and there's no need to run it in production or continuous integration.
Now this is what I tried:
lazy val Dev = config("dev") extend Compile
lazy val root = (project in file(".")).configs(Dev).settings(
inConfig(Dev)(addCompilerPlugin("org.psywerx.hairyfotr" %% "linter" % "0.1.12")): _*)
and it should work, since when I inspect dev:libraryDependencies, it's what I expect it to be- it has org.psywerx.hairyfotr:linter:0.1.12:plugin->default(compile). Normally if I add the library with a "plugin" scope, it does work for the default settings:
libraryDependencies += ("org.psywerx.hairyfotr" %% "linter" % "0.1.12" % "plugin"
It just does not work if I add this under a different configuration, so there must be something else going on here.
This solves the problem, but not exactly in a way was asked. Here's the full build.sbt:
libraryDependencies ++= Seq(
"org.psywerx.hairyfotr" %% "linter" % "0.1.14" % "test")
val linter = Command.command("linter")(state => {
val linterJar = for {
(newState, result) <- Project.runTask(fullClasspath in Test, state)
cp <- result.toEither.right.toOption
linter <- cp.find(
_.get(moduleID.key).exists(mId =>
mId.organization == "org.psywerx.hairyfotr" &&
mId.name == "linter_2.11"))
} yield linter.data.absolutePath
val res = Project.runTask(scalacOptions, state)
res match {
case Some((newState, result)) =>
result.toEither.right.foreach { defaultScalacOptions =>
Project.runTask(compile in Test,
Project.extract(state).append(
scalacOptions := defaultScalacOptions ++ linterJar.map(p => Seq(s"-Xplugin:$p")).getOrElse(Seq.empty),
newState))
}
case None => sys.error("Couldn't get defaultScalacOptions")
}
state
})
lazy val root = (project in file(".")).configs(Test).settings(commands ++= Seq(linter))
The fact that you return unmodified state means you don't change the project settings. So if you run sbt linter, you should get your project compiled with the additional scalacOptions, but if you run compile in the same sbt session, it will not use those additional settings.
The tricky thing here is that scalacOptions is actually a TaskKey, not a SettingKey. I don't know why is that, but to get its value, you have to run that task. One reason might be that in sbt you cannot make setting depending on a task, but you can make a task depending on a task. In other words, scalacOptions can depend on the other task value, and maybe internally it does, I haven't checked. If current answer will work for you, I can try and think about more elegant way of achieving the same result.
EDIT: modified the code to specify the scalacOptions for the linter plugin proper. Please note the plugin has to be a managed dependency, not just a downloaded jar, for this solution to work. If you want to have it unmanaged, there's a way, but I won't go into it for now. Additionally, I've taken a freedom of making it also work for testing code, for illustration purposes.
Looking at Defaults.scala in the source, it seems like the compile command is always taking the options from the compile scope. So if I'm correct, you can have only one set of compilation options!
This seems to be confirmed by the fact that scalacOptions behaves the same way, and this is also why I don't see a non-hacky answer for these similar questions:
Different scalac options for different scopes or tasks?
Different compile options for tests and release in SBT?
I'd be happy to be proven wrong.
EDIT: FWIW, one might not be able to define another scalac options profile in the same project, but you could do so in a "different" project:
lazy val dev = (project in file(".")).
settings(target := baseDirectory.value / "target" / "dev").
settings(addCompilerPlugin("org.psywerx.hairyfotr" %% "linter" % "0.1.12"): _*)
This has the disadvantage that it has a separate output directory, so it will take more space and, more importantly, will not get incremental compiles between the two projects. However, after spending some time thinking about it, this may be by design. After all, even though linters don't, some scalac compilation options could conceivably change the output. This would make it meaningless to try to keep the metadata for incremental compilation from one set of scalac options to another. Thus different scalac options would indeed require different target directories.
I am looking for a way to control what library dependency exported, and what not. Something along those lines:
"org.slf4j" % "slf4j-api" % "1.7.6" doNotExport
or perhaps at the point where the project is imported, like this:
lazy val main = Project(appName, file("."), settings = buildSettings)
.dependsOn(ProjectRef(uri("../Utils"), "Utils").exceptLibraryDependency(organization="org.slf4j"))
Is there anything like this in SBT?
Well, it all depends on configurations. Default configurations expose dependencies again. So similar behavior can be achieved like this:
val compileOnly = config("compileOnly").hide
ivyConfigurations += compileOnly
unmanagedClasspath in Compile ++=
update.value.select(configurationFilter(compileOnly.name))
"org.slf4j" % "slf4j-api" % "1.7.6" % compileOnly
Note that this technique was descibed in an answer to Add a compile time only dependency in sbt.
This question should be closed as a duplicate, but the bounty prevents this.
After many struggles I finally got a large project converted over from Maven to SBT. One of the remaining issues, however, is that some of the unit tests in the project use jMockit which can be a bit high-maintenance when it comes to configuring the environment.
Specifically the jmockit dependency/jar has two difficult requirements:
The jmockit jar must appear in the classpath before the junit jar
On many JVM's, such as the OpenJDK one I'm using, the JVM argument -javaagent:<path/to/jmockit.jar> is required
If both of these conditions are not met, I'm faced with the error:
[error] Test <mytestclass>.initializationError failed: java.lang.Exception: Method <mytestmethod> should have no parameters
[error] at mockit.integration.junit4.JMockit.<init>(JMockit.java:32)
I think I eventually managed to take care of #1 with SBT but I'm still having trouble with the second one. The debug SBT logs do not show enough detail about the forked process invocation to tell me if my settings are working or not. But the test output consistently indicates that it's not working. I have what I think are all the relevant settings:
lazy val myproj = Project(
...
settings = otherSettings ++ Seq(
libraryDependencies ++= Seq(
"com.googlecode.jmockit" % "jmockit" % "1.7" % "test",
"junit" % "junit" % "4.8.1" % "test"
),
fork in Test := true,
javaOptions in test += "-javaagent:<hardcode-path-to-jmockit.jar>"
)
I think the classpath is OK based on the output of the test:dependencyClasspath:
sbt> project <myproject>
sbt> show test:dependencyClasspath
[info] List(...., Attributed(/var/build/ivy2/cache/junit/junit/jars/junit-4.8.1.jar), ...
..., Attributed(/var/build/ivy2/cache/com.googlecode.jmockit/jmockit/jars/jmockit-1.7.jar), ...)
So I'm thinking that my javaagent setting is not having the intended result.
If I do happen to get this to work, my next question is how to get the hard-coded jmockit.jar path out of there but for now I'll settle for a passing test case.
So, how do I set the JVM options used for testing? How do I view/verify what options are actually used when the tests are launched?
You need to change your javaOptions to javaOptions in Test (note the T in Test is capitalized).
You can check your options by executing show test:javaOptions
> show test:javaOptions
[info] List(-javaagent:/home/lpiepiora/.ivy2/cache/com.googlecode.jmockit/jmockit/jars/jmockit-1.7.jar)
Additionally if you want to use dynamic path to the jmockit jar, you can set your javaOptions like this:
def jmockitPath(f: Seq[File]) = f.filter(_.name.endsWith("jmockit-1.7.jar")).head
javaOptions in Test += s"-javaagent:${jmockitPath((dependencyClasspath in Test).value.files)}"
build.sbt for reference
libraryDependencies += "com.novocode" % "junit-interface" % "0.9" % "test"
libraryDependencies ++= Seq(
"com.googlecode.jmockit" % "jmockit" % "1.7" % "test",
"junit" % "junit" % "4.8.1" % "test"
)
fork in Test := true
def jmockitPath(f: Seq[File]) = f.filter(_.name.endsWith("jmockit-1.7.jar")).head
javaOptions in Test += s"-javaagent:${jmockitPath((dependencyClasspath in Test).value.files)}"