This page describes best practices for working with sbt.
project/
vs. ~/.sbt/
Anything that is necessary for building the project should go in
project/
. This includes things like the web plugin. ~/.sbt/
should
contain local customizations and commands for working with a build, but
are not necessary. An example is an IDE plugin.
There are two options for settings that are specific to a user. An example of such a setting is inserting the local Maven repository at the beginning of the resolvers list:
resolvers := {
val localMaven = "Local Maven Repository" at "file://"+Path.userHome.absolutePath+"/.m2/repository"
localMaven +: resolvers.value
}
.sbt
file, such as
$HOME/.sbt/1.0/global.sbt
. These settings will be applied to all projects.
.sbt
file in a project that isn’t checked into
version control, such as <project>/local.sbt
. sbt combines the
settings from multiple .sbt files, so you can still have the
standard <project>/build.sbt
and check that into version control.
Put commands to be executed when sbt starts up in a .sbtrc
file, one
per line. These commands run before a project is loaded and are useful
for defining aliases, for example. sbt executes commands in
$HOME/.sbtrc
(if it exists) and then <project>/.sbtrc
(if it
exists).
Write any generated files to a subdirectory of the output directory,
which is specified by the target
setting. This makes it easy to clean
up after a build and provides a single location to organize generated
files. Any generated files that are specific to a Scala version should
go in crossTarget
for efficient cross-building.
For generating sources and resources, see Generating Files.
Don’t hard code constants, like the output directory target/
. This is
especially important for plugins. A user might change the target
setting to point to build/
, for example, and the plugin needs to
respect that. Instead, use the setting, like:
myDirectory := target.value / "sub-directory"
A build naturally consists of a lot of file manipulation. How can we reconcile this with the task system, which otherwise helps us avoid mutable state? One approach, which is the recommended approach and the approach used by sbt’s default tasks, is to only write to any given file once and only from a single task.
A build product (or by-product) should be written exactly once by only one task. The task should then, at a minimum, provide the Files created as its result. Another task that wants to use Files should map the task, simultaneously obtaining the File reference and ensuring that the task has run (and thus the file is constructed). Obviously you cannot do much about the user or other processes modifying the files, but you can make the I/O that is under the build’s control more predictable by treating file contents as immutable at the level of Tasks.
For example:
lazy val makeFile = taskKey[File]("Creates a file with some content.")
// define a task that creates a file,
// writes some content, and returns the File
makeFile := {
val f: File = file("/tmp/data.txt")
IO.write(f, "Some content")
f
}
// The result of makeFile is the constructed File,
// so useFile can map makeFile and simultaneously
// get the File and declare the dependency on makeFile
useFile :=
doSomething( makeFile.value )
This arrangement is not always possible, but it should be the rule and not the exception.
Construct only absolute Files. Either specify an absolute path
file("/home/user/A.scala")
or construct the file from an absolute base:
base / "A.scala"
This is related to the no hard coding best practice because the proper
way involves referencing the baseDirectory
setting. For example, the
following defines the myPath setting to be the <base>/licenses/
directory.
myPath := baseDirectory.value / "licenses"
In Java (and thus in Scala), a relative File is relative to the current working directory. The working directory is not always the same as the build root directory for a number of reasons.
The only exception to this rule is when specifying the base directory for a Project. Here, sbt will resolve a relative File against the build root directory for you for convenience.
token
everywhere to clearly delimit tab completion boundaries.
flatMap
for general recursion. sbt’s combinators are strict to
limit the number of classes generated, so use flatMap like:
lazy val parser: Parser[Int] =
token(IntBasic) flatMap { i =>
if(i <= 0)
success(i)
else
token(Space ~> parser)
}
This example defines a parser a whitespace-delimited list of integers, ending with a negative number, and returning that final, negative number.