Published 2013-09-19.
Last modified 2019-10-17.
Time to read: 9 minutes.
This lecture discusses how to set up a new SBT project.
I created a GitHub project for use as a template for new SBT 1.x projects at
github.com/
.
We’ll learn how SBT projects are defined by exploring two files contained in the sbtTemplate
project
that define the SBT meta-project – that is, the files that define how to build your program.
These files are build.sbt
and project/build.properties
.
SBT’s Recursive Build Process
The previous lecture, SBT Global Setup, talked about how the SBT build process works. Here is more information about the recursive nature of the SBT build process:
sbt-launcher
starts SBT proper, and for thecompile
task at least 2 builds are initiated, one after the other, each with a unique instance ofscalac
, which is the Scala compiler.-
The meta-project that builds the user code is built according to the defaults specific to the version of SBT that is running.
-
Meta-project defaults vary according to the version of SBT.
For example, SBT 0.13.11 defaults to being compiled with
scalac
2.10.6, but that can be changed to any 2.10.x version ofscalac
by specifyingscalaVersion
inproject/build.sbt
. SBT 1.x defaults to Scala 2.18. - The meta-project might itself be built with a meta-meta-project ... and it is SBT all the way down.
-
So
project/project/..../project/build.sbt
is a meta-project to the nth power, and it can use any compatible compiler. For sbt 1.2.8, that is Scala 2.12.7.
-
Meta-project defaults vary according to the version of SBT.
For example, SBT 0.13.11 defaults to being compiled with
- The user’s code is built according to
build.sbt
-
The meta-project that builds the user code is built according to the defaults specific to the version of SBT that is running.
project/build.properties
This file only needs to contain one line: the version of SBT required in order to build this project.
sbt.version=1.3.3
Scala 2 build.sbt
This is the main build.sbt
file from sbtTemplate
which specifies how your project should be built.
I try to list the settings in alphabetical order.
import Settings._
cancelable := true
developers := List( // TODO replace this with your information Developer("mslinn", "Mike Slinn", "mslinn@mslinn.com", url("https://github.com/mslinn") ) )
// define the statements initially evaluated when entering ’console’, ’console-quick’, but not ’console-project’ initialCommands in console := """ """.stripMargin
javacOptions ++= Seq( "-Xlint:deprecation", "-Xlint:unchecked", "-source", "1.8", "-target", "1.8", "-g:vars" )
libraryDependencies ++= Seq( "org.scalatest" %% "scalatest" % "3.0.8" % Test withSources(), "junit" % "junit" % "4.12" % Test )
licenses += ("CC0", url("https://creativecommons.org/publicdomain/zero/1.0/"))
logBuffered in Test := false
logLevel := Level.Warn
// Only show warnings and errors on the screen for compilations. // This applies to both test:compile and compile and is Info by default logLevel in compile := Level.Warn
// Level.INFO is needed to see detailed output when running tests logLevel in test := Level.Info
name := "sbt-template" // TODO provide a short yet descriptive name
organization := "com.micronautics" // TODO provide your organization’s information
resolvers ++= Seq( )
scalacOptions ++= Seq( "-deprecation", // Emit warning and location for usages of deprecated APIs. "-encoding", "utf-8", // Specify character encoding used by source files. "-explaintypes", // Explain type errors in more detail. "-feature", // Emit warning and location for usages of features that should be imported explicitly. "-language:existentials", // Existential types (besides wildcard types) can be written and inferred "-language:experimental.macros", // Allow macro definition (besides implementation and application) "-language:higherKinds", // Allow higher-kinded types "-language:implicitConversions", // Allow definition of implicit functions called views "-unchecked", // Enable additional warnings where generated code depends on assumptions. "-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access. "-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver. "-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error. "-Xlint:delayedinit-select", // Selecting member of DelayedInit. "-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element. "-Xlint:inaccessible", // Warn about inaccessible types in method signatures. "-Xlint:infer-any", // Warn when a type argument is inferred to be `Any`. "-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id. "-Xlint:nullary-override", // Warn when non-nullary `def f()’ overrides nullary `def f’. "-Xlint:nullary-unit", // Warn when nullary methods return Unit. "-Xlint:option-implicit", // Option.apply used implicit view. "-Xlint:package-object-classes", // Class or object defined in package object. "-Xlint:poly-implicit-overload", // Parameterized overloaded implicit methods are not visible as view bounds. "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field. "-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component. "-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope. )
scalacOptions ++= scalaVersion { case sv if sv.startsWith("2.13") => List( )
case sv if sv.startsWith("2.12") => List( "-Yno-adapted-args", // Do not adapt an argument list (either by inserting () or creating a tuple) to match the receiver. "-Ypartial-unification", // Enable partial unification in type constructor inference "-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined. "-Ywarn-inaccessible", // Warn about inaccessible types in method signatures. "-Ywarn-infer-any", // Warn when a type argument is inferred to be `Any`. "-Ywarn-nullary-override", // Warn when non-nullary `def f()’ overrides nullary `def f’. "-Ywarn-nullary-unit", // Warn when nullary methods return Unit. "-Ywarn-numeric-widen" // Warn when numerics are widened. )
case _ => Nil }.value
// The REPL can’t cope with -Ywarn-unused:imports or -Xfatal-warnings so turn them off for the console scalacOptions in (Compile, console) --= Seq("-Ywarn-unused:imports", "-Xfatal-warnings")
scalacOptions in (Compile, doc) ++= baseDirectory.map { bd: File => Seq[String]( "-sourcepath", bd.getAbsolutePath, // todo replace my-new-project with the github project name "-doc-source-url", s"https://github.com/$gitHubId/my-new-project/tree/master€{FILE_PATH}.scala" ) }.value
scalaVersion := "2.13.1"
scmInfo := Some( ScmInfo( url(s"https://github.com/$gitHubId/$name"), s"git@github.com:$gitHubId/$name.git" ) )
version := "0.1.0"
The above build.sbt
is somewhat imposing at first.
It was written for Scala 2 and has not been modified for Scala 3.
-
The
name
of the project should be one word. I don’t recommend usingcamelCase
to name your project – instead, usesnake_case
. This will give you greater flexibility and fewer surprises later on. -
developers
is an optional setting that can be used to document the people who worked on the code. -
As we saw above,
the
initialCommands
setting allows you to specify Scala statements that should be executed each time you enter the SBTconsole
orconsole-quick
REPL. This can be a real time-saver. -
The
javacOptions
line is there because you can mix Java and Scala source code in a project. The two compilers work together very closely. This line contains settings for the Java compiler. -
libraryDependencies
contains a list of the dependencies that you explicitly specify for your project.- If a dependency itself has dependencies, they are also incorporated into the project. As is the case with Maven, the recursive dependency lookup can cause a surprising amount of dependencies to be downloaded.
- Two dependencies are shown, and they are both commented out. Notice that dependencies are delimited with a comma. We will discuss the format of dependencies later in this lecture.
-
licenses
can contains a list of zero or more licenses. Common open source licenses are defined here. -
The global
loglevel
setting is overriden for compilation and testing. -
The
organization
setting is optional and you should change it to match your organization’s Internet domain. -
resolvers
is a comma-delimited list of resolvers that SBT should use when searching for dependencies.- Each resolver consists of a human-friendly name, followed by the word
at
, followed by the URL of a repository. The human-friendly name is arbitrary and you can call it whatever you want, so long as each name is unique. - More complex resolver incantations are possible.
- mirrors common Maven projects so you do not need to list them here. If you have other dependencies that do not reside in well-known public repositories, you will need to augment this list of resolvers. Normally each dependency’s README file indicates the repository that it can be found in.
-
The
++=
operator causes the current list of resolvers to be augmented by concatenating the list of resolvers that are specified here.Seq
is actually a way of creating aList
.
- Each resolver consists of a human-friendly name, followed by the word
-
Scala compiler settings are specified on two lines as
scalacOptions
. SBT launches the Scala compiler to build Scala code, and it also uses the Java compiler to compile Java code. Options for each of these compilers are specified inscalacOptions
andjavacOptions
, respectively. -
The
scalaVersion
is also specified. This is important because you want to ensure that the set of dependencies for your project is stable. If you neglect to specify this then as the Scala compiler continues to evolve your project will eventually not build anymore. -
The
version
of your project should be specified. Most repositories usemajor.minor.subminor
release numbers, optionally followed by-snapshot
for the prereleased versions, so I recommend you do the same. You might start your new project at version0.1.0
, or0.1.0-snapshot
depending on your infrastructure and the requirements of project you are creating. -
The line beginning with
scalacOptions in (Compile, doc)
is only there for projects hosted on GitHub so Scaladoc generated from your program can contain links to the original source files. If your project is not hosted on GitHub you can delete this expression, otherwise be sure to modifychangeMe
to the name of your GitHub project. Yes, that is a Euro symbol.
Sbt Is Noisy
You can suppress most of the log output by setting logLevel
to Level.Warn
as shown in the above
build.sbt
.
This also overrides the logLevels
for the compile
and test
scopes.
Here is an example of how to temporarily increase logging verbosity in order to identify a problem when compiling:
$ sbt > set logLevel in compile := Level.Info > compile
One-Line Incantation
Sbt
allows everything to be specified on the command line.
Here we see that a semicolon is treated like a newline, so the above is equivalent to the following:
$ sbt "; set logLevel in compile := Level.Info; compile"
Dependencies and Scopes
Your source code normally has dependencies, and they can be managed or unmanaged.
Unmanaged dependencies are jars which you manually place in the lib/
directory.
SBT fetches managed dependencies; you specify these in the top-level build.sbt
.
libraryDependencies
using the following fields.
I am using BNF to express the syntax:
groupId [% | %%] dependencyName % version [% SCOPE] [withSources()]
SCOPE
, if provided, is most
commonly
one of compile
, runtime
, test
and provided
.
Specifying the test
scope means that the dependency will only be activated when building unit tests.
The default scope is the union of compile
, runtime
and test
.
For example, you could specify that this project uses version v0.17.7 of the
Pureconfig
Scala library like this:
"com.github.pureconfig" && "pureconfig" & "0.17.7" withSources()
You can specify a scope within quotes ("test"
) or capitalized without quotes (Test
).
Other scopes include "docs"
/ Docs
,
"pom"
/ Pom
,
"optional"
/ Optional
,
and "sources"
/ Sources
.
Dependencies are specified as a series of fields, delimited by single or double percent characters
(%
or %%
).
For the above Pureconfig example:
- The Maven group id is
com.github.pureconfig
- The Maven artifact id is
pureconfig
- The version is
"0.17.7
- No scope was mentioned, so this dependency applies to compilations, unit tests and at runtime
- Source code is fetched in addition to the executable JAR.
Scala 2
Scala 3 is binary compatible between releases, so some of this section does not apply to Scala 3 programs.
Because Scala 2 is not binary compatible between releases,
a separate version of every library needs to be created for each version of the Scala 2.x compiler that programmers might want to link with.
The %%
in the above incantation performs name mangling to fetch the proper version of the dependency.
The name is mangled by appending the version of the Scala compiler used in your project to the Maven artifact id.
Double percent signs should only be used with dependencies written in Scala.
Dependencies written in Java are always written with only one percent sign.
You could also specify the Pureconfig dependency without using the double percent characters as follows, so the version that was compiled with Scala 2.12 is specifically pulled in. This is rarely required.
"com.typesafe.akka" % "akka-actor_2.12" % "0.17.7"
withSources()
is optional; feel free to specify it after the version of the dependency if you wish.
When you update the project to build with the Scala 2.13 compiler, you will need to update this dependency because you did not use the double percent sign:
"com.github.pureconfig" % "pureconfig_2.13" % "0.17.7"
List of Dependency Specifications
As we saw earlier, dependencies can be specified as a list:
libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % "3.0.8" % Test withSources(),
"junit" % "junit" % "4.12" % Test
)
In this format, notice:
- The
++=
operator (with two plus signs) concatenates two lists. -
Zero or more dependencies can be wrapped within a
Seq()
. The ScalaSeq
method creates a ScalaList
. - Each dependency is delimited from the next by a comma.
All Dependency Specifications
Instead of providing a list of dependencies, one or more dependencies can be specified individually.
The following applies to all dependency specifications, whether they are specified as a list or specified individually:
- Spaces can be used to align the fields of the dependencies.
-
The dependency’s source code is downloaded when the
withSources()
option is provided. If the source code is not available this will cause an error. Source code can be very helpful, so I recommend you include this whenever possible.
Individual Dependency Specifications
The operator used to append a single value to the list of libraryDependencies
is +=
(with only one plus sign).
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.0" % Test withSources() libraryDependencies += "junit" % "junit" % "4.12" % Test
Identifying Transitive Dependencies With Coursier
The cs resolve
command downloads the metadata files for a dependency and displays the transitive dependencies.
The following example displays the transitive dependencies for Pureconfig v0.17.7. Pureconfig is a library written in Scala.
While sbt
uses single and double ampersands as delimiters (&
and &&
),
Coursier uses colons (:
and ::
) as delimiters.
$ cs resolve com.github.pureconfig::pureconfig:0.17.7 https://repo1.maven.org/maven2/com/github/pureconfig/pureconfig-generic_2.13/0.17.7/pureconfig-generic_2.13-0.17.7.pom 100.0% [##########] 3.5 KiB (498.6 KiB / s) https://repo1.maven.org/maven2/com/github/pureconfig/pureconfig_2.13/0.17.7/pureconfig_2.13-0.17.7.pom 100.0% [##########] 2.9 KiB (190.6 KiB / s) com.chuusai:shapeless_2.13:2.3.12:default com.github.pureconfig:pureconfig-core_2.13:0.17.7:default com.github.pureconfig:pureconfig-generic-base_2.13:0.17.7:default com.github.pureconfig:pureconfig-generic_2.13:0.17.7:default com.github.pureconfig:pureconfig_2.13:0.17.7:default com.typesafe:config:1.4.3:default org.scala-lang:scala-library:2.13.14:default
For Scala libraries, be sure to separate the groupId
from the dependency name with two colons (::
),
or you will get an unhelpful error message.
$ cs resolve com.github.pureconfig:pureconfig:0.17.7 Resolution error: Error downloading com.github.pureconfig:pureconfig:0.17.7 not found: /home/mslinn/.ivy2/local/com.github.pureconfig/pureconfig/0.17.7/ivys/ivy.xml not found: https://repo1.maven.org/maven2/com/github/pureconfig/pureconfig/0.17.7/pureconfig-0.17.7.pom
Java libraries only need one colon between the groupId
from the dependency name.
The following example displays the transitive dependencies of
Google Guava v33.2.1-jre
.
$ cs resolve com.google.guava:guava:33.2.1-jre https://repo1.maven.org/maven2/com/google/guava/guava-parent/33.2.1-jre/guava-parent-33.2.1-jre.pom 100.0% [##########] 19.0 KiB (1.3 MiB / s) https://repo1.maven.org/maven2/com/google/guava/guava/33.2.1-jre/guava-33.2.1-jre.pom 100.0% [##########] 8.9 KiB (150.4 KiB / s) https://repo1.maven.org/maven2/org/checkerframework/checker-qual/3.42.0/checker-qual-3.42.0.pom 100.0% [##########] 2.0 KiB (341.1 KiB / s) com.google.code.findbugs:jsr305:3.0.2:default com.google.errorprone:error_prone_annotations:2.26.1:default com.google.guava:failureaccess:1.0.2:default com.google.guava:guava:33.2.1-jre:default com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava:default com.google.j2objc:j2objc-annotations:3.0.0:default org.checkerframework:checker-qual:3.42.0:default
Fetching a Dependencies and all Transitive Dependencies
The cs resolve
command only downloads metadata files for a particular library.
To actually download a library, and all its transitive dependencies, use the cs fetch
command.
Here is the help message:
$ cs fetch -h Usage: /home/mslinn/.local/share/coursier/bin/.cs.aux fetch [options] [org:name:version*|app-name[:version]] Transitively fetch the JARs of one or more dependencies or an application.
Examples: $ cs fetch io.circe::circe-generic:0.12.3
Help options: --usage Print usage and exit -h, -help, --help Print help message and exit -help-full, -full-help, --help-full, --full-help Print help message, including hidden options, and exit
Verbosity options: -q, --quiet Quiet output -v, --verbose Increase verbosity (specify several times to increase more) -P, --progress Force display of progress bars
App channel options: --channel org:name Channel for apps --contrib Add contrib channel
Fetch options: -p, --classpath Print java -cp compatible output --sources Fetch source artifacts --javadoc Fetch javadoc artifacts --default Fetch default artifacts (default: false if --sources or --javadoc or --classifier are passed, true else)
Repository options: -r, --repository maven|sonatype:$repo|ivy2local|bintray:$org/$repo|bintray-ivy:$org/$repo|typesafe:ivy-$repo|typesafe:$repo|sbt-plugin:$repo|scala-integration|scala-nightlies|ivy:$pattern|jitpack|clojars|jcenter|apache:$repo Repository - for multiple repositories, specify this option multiple times (e.g. -r central -r ivy2local -r sonatype:snapshots)
Dependency options: --sbt-plugin string* Add sbt plugin dependencies --scala-js Enable Scala.js -S, --native Enable scala-native --dependency-file string* Path to file with dependencies. Dependencies should be separated with newline character
Resolution options: -V, --force-version organization:name:forcedVersion Force module version -e, --scala, --scala-version string? Default scala version
Cache options: --cache string? Cache directory (defaults to environment variable COURSIER_CACHE, or ~/.cache/coursier/v1 on Linux and ~/Library/Caches/Coursier/v1 on Mac) -l, --ttl duration TTL duration (e.g. "24 hours") --credentials host(realm) user:pass|host user:pass Credentials to be used when fetching metadata or artifacts. Specify multiple times to pass multiple credentials. Alternatively, use the COURSIER_CREDENTIALS environment variable --credential-file string* Path to credential files to read credentials from
The following example downloads pureconfig v0.17.7
and all its transitive dependencies:
$ cs fetch com.github.pureconfig::pureconfig:0.17.7 https://repo1.maven.org/maven2/com/github/pureconfig/pureconfig-generic-base_2.13/0.17.7/pureconfig-generic-base_2.13-0.17.7.jar 100.0% [##########] 53.4 KiB (675.6 KiB / s) https://repo1.maven.org/maven2/com/github/pureconfig/pureconfig-generic_2.13/0.17.7/pureconfig-generic_2.13-0.17.7.jar 100.0% [##########] 93.5 KiB (1.2 MiB / s) https://repo1.maven.org/maven2/com/github/pureconfig/pureconfig_2.13/0.17.7/pureconfig_2.13-0.17.7.jar 100.0% [##########] 298B (3.7 KiB / s) https://repo1.maven.org/maven2/com/github/pureconfig/pureconfig-core_2.13/0.17.7/pureconfig-core_2.13-0.17.7.jar 100.0% [##########] 518.1 KiB (5.1 MiB / s) https://repo1.maven.org/maven2/com/chuusai/shapeless_2.13/2.3.12/shapeless_2.13-2.3.12.jar 100.0% [##########] 3.1 MiB (25.9 MiB / s) /home/mslinn/.cache/coursier/v1/https/repo1.maven.org/maven2/com/github/pureconfig/pureconfig_2.13/0.17.7/pureconfig_2.13-0.17.7.jar /home/mslinn/.cache/coursier/v1/https/repo1.maven.org/maven2/org/scala-lang/scala-library/2.13.14/scala-library-2.13.14.jar /home/mslinn/.cache/coursier/v1/https/repo1.maven.org/maven2/com/github/pureconfig/pureconfig-core_2.13/0.17.7/pureconfig-core_2.13-0.17.7.jar /home/mslinn/.cache/coursier/v1/https/repo1.maven.org/maven2/com/github/pureconfig/pureconfig-generic_2.13/0.17.7/pureconfig-generic_2.13-0.17.7.jar /home/mslinn/.cache/coursier/v1/https/repo1.maven.org/maven2/com/typesafe/config/1.4.3/config-1.4.3.jar /home/mslinn/.cache/coursier/v1/https/repo1.maven.org/maven2/com/github/pureconfig/pureconfig-generic-base_2.13/0.17.7/pureconfig-generic-base_2.13-0.17.7.jar /home/mslinn/.cache/coursier/v1/https/repo1.maven.org/maven2/com/chuusai/shapeless_2.13/2.3.12/shapeless_2.13-2.3.12.jar
Discovering Dependencies and Versions Manually
Open source projects normally contain a README
file that indicates the preferred version to use.
Library dependencies can also be found at
.
Let’s look for mvnrepository.com
nscala-time
, which is a good Scala wrapper around the
Joda time and date utility package.
- Point your browser to
https://mvnrepository.com
. - Enter
nscala-time
into the search box and press Enter. -
The resulting page shows these results;
Scala libraries are cross-compiled for various versions of the Scala compiler:
-
Click on the highlighted link, which will display all available versions.
build.sbt.
Dependency specification for build.sbtlibraryDependencies += "com.github.nscala-time" %% "nscala-time" % "2.32.0"
Using sbtTemplate
One way to use sbtTemplate
is type the following at a bash prompt,
and a directory called myproject
will be created containing the template.
First change your directory to the one you want the project to be created in.
Git will create a sub-directory holding the project:
$ cd ~/myScalaProjects
$ git clone https://github.com/mslinn/sbtTemplate.git myproject
$ cd myproject
$ rm -rf .git/
$ git init
The above shows that I deleted the myproject/.git/
directory, and created a new git project with git init
.
Here is a bash script that automatically does the necessary steps for creating a new SBT project from sbtTemplate
:
#!/bin/bash
# Clones sbtTemplate and starts a new SBT project # Optional argument specifies name of directory to place the new project into
DIR=sbtTemplate if [ "$1" ]; then DIR="$1"; fi git clone https://github.com/mslinn/sbtTemplate.git "$DIR" cd "$DIR" rm -rf .git git init echo "Remember to edit README.md and build.sbt ASAP"
To use the script, copy it to a directory on the PATH
and make it executable.
For example, if you have a ~/.local/bin
directory that is on the PATH
, type:
$ chmod a+x ~/.local/bin/sbtTemplate
Now you can use the script to make a new SBT project in the current directory:
$ sbtTemplate newProjectName
Giter8
SBT projects can be created by using an obscure and poorly documented configuration language,
giter8
.
I do not care for it, so I only mention it here for the sake of completeness.
The combination of giter8
and its principal dependency,
conscript
,
makes the working with Scala more complex than necessary.
I much prefer to use the sbtTemplate
bash script.
Open An SBT Project in Your Favorite IDE or Editor
IntelliJ IDEA has incorporated the ability to open SBT projects since 2014. The Working With IntelliJ IDEA lecture describes how to install and configure IDEA for use with Scala.
Exercise – Hello, world!
Create an SBT project that prints out Hello, world!
Hints:
- You don’t need an IDE for this, any simple editor will do fine.
- Your project should not require any dependencies.
- Do not create your project in a subdirectory of another SBT project.
-
You could use the
sbtTemplate
script I showed you earlier in this lecture to create the SBT project. - Your Scala program, a console app, could be called anything you like, so long as it has a
.scala
file type. -
The rest of these hints assume that you create a file in your SBT project called
Hello.scala
. Be sure to create that file in thesrc/main/scala
subdirectory of your new SBT project. -
If you are a Java programmer, you are used to creating an entry point for a class by defining a
static void main()
method. Scala 3 works that way, but Scala 2 did not define entry points that way.Scala 3 entry point@main def hello() = println("Hello, world!")
Backwards compatible Scala 2 or 3 entry pointobject Hello23 { def main(args: Array[String]): Unit = println("Hello, world!") }
App
trait is mixed into a Scalaobject
, thatobject
becomes an entry point for a console application. We will discuss traits in the Traits / Mixins lecture. For now, just follow this example. Your Scala program might look like this:Scala 2 entry pointobject Hello2 extends App { println("Hello, world!") }
-
You can run an SBT project from a bash prompt by typing:
Shell
$ sbt run
-
To run a specific entry point called
Hello
in a project that has many entry points, type:Shell$ sbt "runMain Hello2"
$ sbt "runMain Hello23"
$ sbt "runMain Hello3"solutions
package of thecourseNotes/
directory, type:Shell$ sbt "runMain solutions.Hello"
© Copyright 1994-2024 Michael Slinn. All rights reserved.
If you would like to request to use this copyright-protected work in any manner,
please send an email.
This website was made using Jekyll and Mike Slinn’s Jekyll Plugins.