Mike Slinn

SBT Project Setup

— Draft —

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/mslinn/sbtTemplate. 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 the compile task at least 2 builds are initiated, one after the other, each with a unique instance of scalac, which is the Scala compiler.
    1. 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 of scalac by specifying scalaVersion in project/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.
    2. The user’s code is built according to build.sbt

project/build.properties

This file only needs to contain one line: the version of SBT required in order to build this project.

project/build.properties
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.

build.sbt from sbtTemplate
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 using camelCase to name your project – instead, use snake_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 SBT console or console-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 a List.
  • 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 in scalacOptions and javacOptions, 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 use major.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 version 0.1.0, or 0.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 modify changeMe 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:

Shell
$ 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:

Shell
$ 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:

Dependency form in build.sbt as BNF
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:

Dependency specified in build.sbt
"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.

Specifying the Scala 2 library dependency's Scala compiler version
"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 Scala Seq method creates a Scala List.
  • 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.

Shell
$ 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.

Second colon needed
$ 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.

Fetching Google Guava
$ 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:

Shell
$ 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:

Shell
$ 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 mvnrepository.com. Let’s look for nscala-time, which is a good Scala wrapper around the Joda time and date utility package.

  1. Point your browser to https://mvnrepository.com.
  2. Enter nscala-time into the search box and press Enter.
  3. The resulting page shows these results; Scala libraries are cross-compiled for various versions of the Scala compiler:
  4. Click on the highlighted link, which will display all available versions.
    Select 1.8.0 and then click on the SBT tab. You will see the incantation that you should add to build.sbt.
    Dependency specification for build.sbt
    libraryDependencies += "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:

Shell
$ 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:

~/.local/bin/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:

Shell
$ chmod a+x ~/.local/bin/sbtTemplate

Now you can use the script to make a new SBT project in the current directory:

Shell
$ 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 the src/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 point
    object Hello23 {
      def main(args: Array[String]): Unit =
        println("Hello, world!")
    }
    For Scala 2, if the App trait is mixed into a Scala object, that object 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 point
    object Hello2 extends App {
      println("Hello, world!")
    }
    See the Scala documentation for more information.
  • 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"
    To run the entry point that I provide in the solutions package of the courseNotes/ directory, type:
    Shell
    $ sbt "runMain solutions.Hello"
    We will learn more about packages in the Scala Imports and Packages lecture.

* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.