Dummy Guide to sbt
Why sbt
In Scala Survey 2019 there was a question:
What are the *other pain points in your daily workflow?
And sbt is the "winner".
But in another question:
Which build tools do you use?
So, I can simply can interpret this as: Most people choose sbt, they feel it is pain using it, and they don't like to move to variety of other good options- i.e. mill, fury.
What is the pain actually?
So, it is a build tool, I guess most of users are coming from Makefile, Rakefile or npm already have a stereo type of what a build tool should be like - very low effort to learn.
npm
npm has almost 0 learning curve, since it is plain json file, `npm init` will get basically everything setup, `npm install –save blah` will get your dependency configed. For for advance usage all you need to do is to look up the document and copy paste key value into your json file.
It is actually more of dependencies manage tool rather than build tool, the build feature is very limited so you probably still need Makefile.
{ "scripts" :
{ "preinstall" : "./configure"
, "install" : "make && make install"
, "test" : "make test"
}
}
Rakefile
Rake has basically the same concept of Makefile, except you can do ruby script in it while Makefile you do bash.
Rakefile:
task name: [:prereq1, :prereq2] do |t|
# ruby script goes here
end
Makefile:
name: prereq1 prereq2
# bash script goes here
Then follow the Stereo type
So the common here they are both very simple to use, without understanding how it works even what it means, just follow the same pattern you could get the build working.
Same way if any user come from those build tool, they may assume the Scala build tool should share the same trait as well - a build tool should have low learning effort, just copy paste should be enough get it work.
With that kind of expectation, sbt will let you down. Since it has very good and detail documents, but no one is going to spends days to read the hundreds page of documents just to get a project compile and ship. Most people just what things like npm or makefile that you can just simply copy paste and everything just works.
sbt is actually both a build tool, can be use like make
and rake
, at the same time also a simple to use manage dependencies, just like
npm
or bundler
.
So let us start with package management.
Package management
in bundler dependencies can be defined as:
gem 'httparty', '0.17.3'
Let us define `finagle` as sbt project dependencies, you can find how to add it from the right hand side of: https://index.scala-lang.org/twitter/finagle/finagle-core/20.5.0?target=_2.13
libraryDependencies += "com.twitter" %% "finagle-core" % "20.5.0"
where
com.twitter
is group idfinagle-core
is package name20.5.0
is the version of course
Comparing to bundler, there a lot other thing you may need to pay attention to.
What is %%
and %
, what is +=
?
It is totally fine to not knowing those symbols, the build still works, but they are noises and if you put the wrong thing it won't compile.
%%
Here is a quick guide how to use %
// Java library
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
// Scala library %%
libraryDependencies += "com.twitter" %% "finagle-core" % "20.5.0"
// Scala lib % with hard coded Scala version
libraryDependencies += "com.twitter" % "finagle-core_2.13" % "20.5.0"
// Test only library
libraryDependencies += "org.scalameta" %% "munit-scalacheck" % "0.7.7" % Test
That is all you need to know to get a basic dependencies working.
Version handling
don't care the patch version, pick the bigest patch
libraryDependencies += "com.twitter" %% "finagle-core" % "20.5.+"
don't care the minor version, pick the bigest patch
libraryDependencies += "com.twitter" %% "finagle-core" % "20.+"
rang of version
libraryDependencies += "com.twitter" %% "finagle-core" % "[19.4.0, 20.5.0)"
Package management actually isn't the most painful setting in sbt, since all library will give you the config of how to install already in README already.
Simply copy paste generally works.
Task
In Rakefile, we can have very simply task flow, i.e.
- start database
- run test
- stop database
task :db_up do
sh "docker-compose up -d db"
end
task test: [:db_up] do
task(:spec).invoke
end
task :db_down do
`docker-compose stop db`
end
desc 'run db up test and db down'
task default: [:spec] do
task(:db_down).invoke
end
Let us define the same tasks in sbt https://www.scala-sbt.org/1.x/docs/Tasks.html :
// for the `!` syntax to exec external command
import scala.sys.process._
val dbUp = taskKey[Unit]("start database")
val dbDown = taskKey[Unit]("stop database")
val runTest = taskKey[Unit]("run test")
val default = taskKey[Unit]("default task")
dbUp := {
"docker-compose up -d db" !
}
dbDown := {
"docker-compose stop db" !
}
runTest := { dbUp.value;
Command.process("test", state.value)
}
default := Def.sequential(
runTest,
dbDown
).value
It is bit more verbose than Rake because of the Type things, but generally it is as simply as rake when defining external process https://www.scala-sbt.org/1.x/docs/Process.html and task dependency.
There are lot of ways you can define default
in sbt, as a new command:
commands += Command.command("defaultCommand") { state =>
"runTest" :: "dbDown" ::
state
}
or as command alias:
addCommandAlias("defaultCommand", "runTest;dbDown")
Another example is you can operate on files as well from sbt.
For instance there is built-in task in sbt called makePom
, but it will generate
pom.xml
to target
folder, we preferred to generate the file into .github/pom.xml
so Github can pick it up and analyst what jar file is in CVE list.
val genPom = taskKey[Unit]("generate pom for github to do security monitoring")
genPom := {
val pomFile = makePom.value
io.IO.copyFile(pomFile, file(".") / ".github" / "pom.xml")
}
Very simply and declarative task, right.
makePom: TaskKey[File]
is a task that return the pom file,makePom.value
will call the task and generate the file, and return the file aspomFile
io.IO.copyFile
will copy the file to expected path