SBT: Continue even if a certain step fails - scala

During the sbt build of my scala program i write the current sha1 hash to a file so i can use it easily from the application. This is how it looks like in my build.sbt file:
val dummy = {
val sha1 = Process("git rev-parse HEAD").lines.head
IO.write(file("conf/version.conf"), s"""sha1="$sha1"""")
}
The problem ist that now the build has the dependency that git command line has to be installed, otherwise it will fail since it cannot execute the git command.
Is it possible in sbt to ignore an error that occurs during the build and somehow just take "unknown" as the sha1 hash? The sbt docs say something about "failures" http://www.scala-sbt.org/0.13.5/docs/Detailed-Topics/Tasks.html but i am not sure if this can be applied to my problem.

sbt files are normal Scala files in most regards. Simply assign to sha1 the result of a try / catch expression:
val sha1 = try {
Process("git rev-parse HEAD").lines.head
} catch { case e: NonFatal => "unknown" }
IO.write(file("conf/version.conf"), s"""sha1="$sha1"""")

Related

How to differentiate between a script and normal class files in Scala?

In the book, Programming in Scala 5th Edition, the author says the following for two classes:
Neither ChecksumAccumulator.scala nor Summer.scala are scripts, because they end in a definition. A script, by contrast, must end in a result expression.
The ChecksumAccumulator.scala is as follows:
import scala.collection.mutable
class CheckSumAccumulator:
private var sum = 0
def add(b: Byte): Unit = sum += b
def checksum(): Int = ~(sum & 0XFF) + 1
object CheckSumAccumulator:
private val cache = mutable.Map.empty[String, Int]
def calculate(s: String): Int =
if cache.contains(s) then
cache(s)
else
val acc = new CheckSumAccumulator
for c<-s do
acc.add((c >> 8).toByte)
acc.add(c.toByte)
val cs = acc.checksum()
cache += (s -> cs)
cs
whereas the Summer.scala is as follows:
import CheckSumAccumulator.calculate
object Summer:
def main(args: Array[String]): Unit =
for arg <- args do
println(arg + ": " + calculate(arg))
But when I run the Summer.scala file, I get a different error than what mentioned by the author:
➜ learning-scala git:(main) ./scala3-3.0.0-RC3/bin/scala Summer.scala
-- [E006] Not Found Error: /Users/avirals/dev/learning-scala/Summer.scala:1:7 --
1 |import CheckSumAccumulator.calculate
| ^^^^^^^^^^^^^^^^^^^
| Not found: CheckSumAccumulator
longer explanation available when compiling with `-explain`
1 error found
Error: Errors encountered during compilation
➜ learning-scala git:(main)
The author mentioned that the error would be around not having a result expression.
I also tried to compile CheckSumAccumulator only and then run Summer.scala as a script without compiling it:
➜ learning-scala git:(main) ./scala3-3.0.0-RC3/bin/scalac CheckSumAccumulator.scala
➜ learning-scala git:(main) ✗ ./scala3-3.0.0-RC3/bin/scala Summer.scala
<No output, given no input>
➜ learning-scala git:(main) ✗ ./scala3-3.0.0-RC3/bin/scala Summer.scala Summer of love
Summer: -121
of: -213
love: -182
It works.
Obviously, when I compile both, and then run Summer.scala, it works as expected. However, the differentiation of Summer.scala as a script vs normal file is unclear to me.
Let's start top-down...
The most regular way to compile Scala is to use a build tool like SBT/Maven/Mill/Gradle/etc. This build tool will help with a few things: downloading dependencies/libraries, downloading Scala compiler (optional), setting up CLASS_PATH and most importantly running scalac compiler and passing all flags to it. Additionally it can package compiled class files into JARs and other formats and do much more. Most relevant part is CP and compilation flags.
If you strip off the build tool you can compile your project by manually invoking scalac with all required arguments and making sure your working directory matches package structure, i.e. you are in the right directory. This can be tedious because you need to download all libraries manually and make sure they are on the class path.
So far build tool and manual compiler invocation are very similar to what you can also do in Java.
If you want to have an ah-hoc way of running some Scala code there are 2 options. scala let's you run scripts or REPL by simply compiling your uncompiled code before it executes it.
However, there are some caveats. Essentially REPL and shell scripts are the same - Scala wraps your code in some anonymous object and then runs it. This way you can write any expression without having to follow convention of using main function or App trait (which provides main). It will compile the script you are trying to run but will have no idea about imported classes. You can either compile them beforehand or make a large script that contains all code. Of course if it starts getting too large it's time to make a proper project.
So in a sense there is no such thing as script vs normal file because they both contain Scala code. The file you are running with scala is a script if it's an uncompiled code XXX.scala and "normal" compiled class XXX.class otherwise. If you ignore object wrapping I've mentioned above the rest is the same just different steps to compile and run them.
Here is the traditional 2.xxx scala runner code snippet with all possible options:
def runTarget(): Option[Throwable] = howToRun match {
case AsObject =>
ObjectRunner.runAndCatch(settings.classpathURLs, thingToRun, command.arguments)
case AsScript if isE =>
ScriptRunner(settings).runScriptText(combinedCode, thingToRun +: command.arguments)
case AsScript =>
ScriptRunner(settings).runScript(thingToRun, command.arguments)
case AsJar =>
JarRunner.runJar(settings, thingToRun, command.arguments)
case Error =>
None
case _ =>
// We start the repl when no arguments are given.
if (settings.Wconf.isDefault && settings.lint.isDefault) {
// If user is agnostic about -Wconf and -Xlint, enable -deprecation and -feature
settings.deprecation.value = true
settings.feature.value = true
}
val config = ShellConfig(settings)
new ILoop(config).run(settings)
None
}
This is what's getting invoked when you run scala.
In Dotty/Scala3 the idea is similar but split into multiple classes and classpath logic might be different: REPL, Script runner. Script runner invokes repl.

Test initialCommands in SBT

I have a subproject in my build.sbt with a rather long setting for initialCommands, comprising a list of imports and some definitions. I'd like to test this as part of regular CI, because otherwise I won't notice breaking changes after refactoring code. It is not clear to me how to do so.
Just running sbt console doesn't seem to cut it, because there is always a "successful" exit code even when code doesn't compile.
Moving the code out into an object defined in a special source file won't help because I need the list of imports to be present (and I don't want to cakeify my whole code base).
Moving the code out into a source file and then loading that with :load also always gives a successful exit code.
I found out about scala -e but that does strange things on my machine (see the error log below).
This is Scala 2.12.
$ scala -e '1'
cat: /release: No such file or directory
Exception in thread "main" java.net.UnknownHostException: <my-host-name-here>: <my-host-name-here>: Name or service not known
You could generate a file and run it like any other test file:
(sourceGenerators in Test) += Def.task {
val contents = """object TestRepl {
{{}}
}""".replace("{{}}", (initialCommands in console).value)
val file = (sourceManaged in Test).value / "repltest.scala"
IO.write(file, contents)
Seq(file)
}.taskValue

Excecuting svn commit during SBT build

I'm looking at coming up with a release process using the sbt release plugin, but I am getting an error when I try to commit to SVN as a release step.
svn: Commit failed (details follow):
svn: '/tmp/checkout/svn-test/commit' is not under version control
"/tmp/checkout/svn-test/" is the correct path to my project. I suspect I'm doing something wrong when I define the ReleaseStep to commit to SVN:
My build.sbt release config is as follows:
lazy val execScript = taskKey[Unit]("Commit to Subversion")
execScript := {
"svn commit -m 'test commit from release plugin'" !
}
val commitToSVN = () => ReleaseStep(
action = releaseStepTask(execScript)
)
releaseProcess := Seq[ReleaseStep](
ReleaseTransformations.checkSnapshotDependencies,
ReleaseTransformations.inquireVersions,
ReleaseTransformations.setReleaseVersion,
commitToSVN()
)
Any help would be really appreciated. Thanks
The problem appears to be the use of shell syntax, namely the use of single quotes, with !. As you have it, it thinks that 'test and commit and from and release and plugin' are all separate arguments.
The Java Process stuff, which Scala methods like ! are just thin wrappers for, don't run the process using a shell, and therefore shell syntax doesn't apply.
It should work if you do Seq("svn", "commit", "-m", ...) instead. Passing the arguments separately means you won't need single quotes to delimit arguments.

How to execute Scala script from Gradle?

I have a Scala script that looks something like:
#!/bin/sh
PATH=${SCALA_HOME}:${PATH}
exec scala "$0" "$#"
!#
Console.println("Hello, world!")
Is there some way in Gradle to set the version of Scala to be used, have it implicitly set SCALA_HOME, and execute the script?
There is no built-in feature for this. The way to tackle this is to declare two tasks: A Copy task that downloads the Scala distribution and unpacks it to a local directory, and an Exec task that sets the SCALA_HOME environment variable to the copy task's output directory and executes your script.
Here is an example of executing Scala from Gradle. It is a nascent attempt to build a plugin using Scalafmt. Of note is how nasty it is to use static values.
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.geirsson:scalafmt-core_2.11:0.4.2'
}
}
task scalafmt << {
String target = "object FormatMe { List(Split(Space, 0).withPolicy(SingleLineBlock(close)), Split(Newline, 1).withPolicy{ case Decision(t#FormatToken(_, `close`, _), s) => Decision(t, List(Split(Newline, 0)))}.withIndent(2, close, Right)) }"
def config = org.scalafmt.config.ScalafmtConfig$.MODULE$.default
def range = scala.collection.immutable.Set$EmptySet$.MODULE$
println org.scalafmt.Scalafmt.format(target, config, range).formattedCode()
}
I know this is not quite on topic but I couldn't find anywhere else to put this and I hope it is of some value to people who ended up here for the same reason I did.
Gradle has an Exec task in which you can set the environment to be passed to the new process. You could pass SCALA_HOME through that.

Why does Scala script fail if CLASSPATH is set?

I have the following Scala 2.10 script that works fine:
#!/bin/bash
classpath="${CLASSPATH}"
unset CLASSPATH
exec ${SCALA_HOME}/bin/scala -cp "${classpath}" "$0" "$#" 2>&1
!#
import stuff
But when CLASSPATH isn't unset, it fails with stuff like:
$ ./setter-for-catan.scala
./setter-for-catan.scala:12: error: not found: object play
import play.api.libs.json.JsArray
^
one error found
Why is this happening?
The scala script has a modest -debug option.
Use -Ylog-classpath to see what the compiler is using.
Use -nc to say "no compile server daemon".
Use fsc -shutdown to start over.
Package changes are anathema, so unexpected dirs in the path with package names, or old package objects, etc, cause inexplicable build problems.
Use PathResolver to dump what classpath it sees.
An empty directory with your package name can interfere with package discovery.
${SCALA_HOME}/bin/scala -cp "${classpath}" scala.tools.util.PathResolver
${SCALA_HOME}/bin/scala -cp "${classpath}" scala.tools.util.PathResolver some-args
You'll see something like:
apm#mara:~/tmp/scripts$ ./foo.sh
object Environment {
scalaHome = /media/Software/scala-2.10.1 (useJavaClassPath = true)
javaBootClassPath = <1122 chars>
javaExtDirs =
/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/ext
/usr/java/packages/lib/ext
javaUserClassPath = ""
scalaExtDirs =
}
object Defaults {
scalaHome = /media/Software/scala-2.10.1
javaBootClassPath =
/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/resources.jar
/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rt.jar
/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/sunrsasign.jar
/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/jsse.jar
/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/jce.jar
/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/charsets.jar
/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/netx.jar
/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/plugin.jar
/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/rhino.jar
/usr/lib/jvm/java-7-openjdk-amd64/jre/lib/jfr.jar
/usr/lib/jvm/java-7-openjdk-amd64/jre/classes
/media/Software/scala-2.10.1/lib/akka-actors.jar
/media/Software/scala-2.10.1/lib/jline.jar
/media/Software/scala-2.10.1/lib/scala-actors.jar
/media/Software/scala-2.10.1/lib/scala-actors-migration.jar
/media/Software/scala-2.10.1/lib/scala-compiler.jar
/media/Software/scala-2.10.1/lib/scala-library.jar
/media/Software/scala-2.10.1/lib/scala-partest.jar
/media/Software/scala-2.10.1/lib/scalap.jar
/media/Software/scala-2.10.1/lib/scala-reflect.jar
/media/Software/scala-2.10.1/lib/scala-swing.jar
/media/Software/scala-2.10.1/lib/typesafe-config.jar
scalaLibDirFound = Some(/media/Software/scala-2.10.1/lib)
scalaLibFound = /media/Software/scala-2.10.1/lib/scala-library.jar
scalaBootClassPath =
scalaPluginPath = /media/Software/scala-2.10.1/misc/scala-devel/plugins
}
COMMAND: 'scala some-args'
RESIDUAL: 'scala some-args'
There may be some funky state leftover from the compiler daemon. Try fsc -shutdown or scala -nc to reset the daemon.