sbt Library Dependencies et al

Programmer
3 min readJan 25, 2022

sbt provides Library Dependencies as a way to configure external dependencies to a project. This has implications on building, publishing and/or dockerizing outputs.

Hope to provide the differences between managedDependencies and unmanagedDependencies and also on how this can tweaked for certain packaging units.

Usually multi-project based sbt contains the following kinds of dependencies

  • 3rd party library dependency (commonly resolved using maven or ivy)
  • hard wired jar filepath (less commonly used and is found in situations where the dependency is not managed by a maven like repository).
  • sub-project based dependency using dependsOn

The first one is commonly referred as managedDependencies and the later two is referred as unmanagedDependencies .

Managed Dependencies

Managed dependencies usually provided using libraryDependencies and are typically chained with multiple of them.

libraryDependencies += organization %% moduleName % version

With managed dependencies, there are number of ways these dependencies can be sent to output.

Provided

Provided is a way dependencies are not transitive added during publish step.

libraryDependencies += "org.scalatest" %% "scalatest" % "3.1.0"     % "test" % Provided

exclude()

Its also possible to exclude selective transitive dependencies using exclude() . And these are excluded during publish step.

libraryDependencies += "log4j" % "log4j" % "1.2.15" exclude("javax.jms", "jms")

dependencyOverride

dependencyOverride is a way to override certain transitive dependencies that are resolved to a project.

dependencyOverrides += "com.fasterxml.jackson.core" % "jackson-core" % "3.7.0-M11"

This way jackson-core would be resolved to 3.7.0-M11 even though a dependency brings a newer version of this dependency.

Unmanaged Dependencies

While, unmanaged dependencies are provided using a lib directory as a convention. This can be overridden using

unmanagedBase := baseDirectory.value / "custom_lib"

Docker package

These days microservices are typically published as docker images. And this requires dependency on an sbt plugin

addSbtPlugin("se.marcuslonnberg" % "sbt-docker" % "1.8.2")

A typical docker configuration to generate Dockerfile may be based on

docker / dockerfile := {
val jarFile: File = (Compile / packageBin / sbt.Keys.`package`).value
val dependencies = (Compile / dependencyClasspathAsJars).value
val jarTarget = s"/app/${jarFile.getName}"
def addLibPath(files: Seq[File]) = files.map("/app/" + _.getName)
val classpathString = (Seq(jarTarget, "/app/config") ++ addLibPath(dependencies.files)).mkString(":")

val banner =
"""echo "Using JAVA_OPTS: ${JAVA_OPTS}"
|echo "Using args $@"
|""".stripMargin
val javaEnv = "java -Dfile.encoding=UTF-8 $JAVA_OPTS"
val args = """"$@""""

val content =
s"""#!/bin/bash
|$banner
|echo '${name.value} - v${version.value}'
|echo 'Built on ${LocalDateTime.now()} - ${git.gitHeadCommit.value.getOrElse("No head commit")}'
|$javaEnv -cp $classpathString $mainclass $args
|""".stripMargin

val programName = "my-app"

val startScript = file(s"""/tmp/$programName""")
IO.write(startScript, content, append = false)

new sbtdocker.Dockerfile {
from("amazoncorretto:11")
add(dependencies.files, "/app/")
add(startScript, s"""/$programName""")
run("chmod", "+x", s"""/$programName""")
add(jarFile, jarTarget)
run("mkdir", "-p", "/app/config")
run("touch", "/app/config/application.conf")
env("JAVA_OPTS", "-Xms8g")
env("JAVA_HOME", "/usr/lib/jvm/jre")
entryPoint(s"""/$programName""")
}
}

This can be verified using

sbt 'project sub-project-1' docker dockerPush

Note:

(1) the -Dfile.encoding=UTF-8 ensures that strings are processed using utf-8.

(2) Using Built on would ensure to verify the image build timestamp and/or version stamp of different dependencies.

(3) Using git.gitHeadCommit would get the git-commit HEAD SHA1 used for this build.

(4) The classpathString would enable customize classpath required for application start.

(5) The dependencyClasspathAsJars would normalize entries from managedClasspath as jar files as labelled as well as from dependsOn from a multi-project setup.

Publishing to Maven

Typically if one wants to publish to maven using artifactory

ThisBuild / resolvers += "Artifactory-release" at "https://test1.jfrog.io/artifactory/libs-release/"
ThisBuild / resolvers += "Artifactory-snapshot" at "https://test1.jfrog.io/artifactory/libs-snapshot/"
ThisBuild / credentials += Credentials(Path.userHome / ".sbt" / "credentials")

This can be verified using

sbt 'project sub-project-1' publishLocal publish

Tip

In the case, if one has multiple non-mono repository based dependencies, its easier to verify dependency integrity using publishLocal and by providing a local resolver using

resolvers += Resolver.file("local-ivy", file(Path.userHome.absolutePath + "/.ivy2/local"))(Resolver.ivyStylePatterns)

Thank you for reading this post and allowing me to learn to do this as well. Comments / suggestions welcome.

Source: https://www.scala-sbt.org/1.x/docs/Library-Dependencies.html

--

--