Universal Plugin¶
The Universal Plugin creates a generic, or “universal” distribution package. This is called “universal packaging.” Universal packaging just takes a plain mappings
configuration and generates various
package files in the output format specified. Because it creates a distribution
that is not tied to any particular platform it may require manual labor (more work from your users) to correctly install and set up.
Requirements¶
Depending on what output format you want to use, you need one of the following applications installed
- zip (if native)
- gzip
- xz
- tar
- hdiutil (for dmg)
Build¶
There is a task for each output format
Zip
sbt universal:packageBinTar
sbt universal:packageZipTarballXz
sbt universal:packageXzTarballDmg
sbt universal:packageOsxDmg
Required Settings¶
The Universal Plugin has no mandatory fields.
Enable the universal plugin
enablePlugins(UniversalPlugin)
Configurations¶
Settings and Tasks inherited from parent plugins can be scoped with Universal
.
Universal packaging provides three Configurations:
universal
- For creating full distributions
universal-docs
- For creating bundles of documentation
universal-src
- For creating bundles of source.
Here is how the values for name
and packageName
are used by the three configurations:
name in Universal := name.value
name in UniversalDocs := (name in Universal).value
name in UniversalSrc := (name in Universal).value
packageName in Universal := packageName.value
Settings¶
As we showed before, the universal packages are completely configured through the use of mappings. Simply specify the desired mappings for a given configuration. For example:
mappings in Universal += (packageBin in Compile).value -> "lib/foo.jar"
However, sometimes it may be advantageous to customize the files for each archive separately. For example, perhaps the .tar.gz has an additional README plaintext file in addition to a README.html. To add this just to the .tar.gz file, use the task-scope feature of sbt:
mappings in Universal in packageZipTarball += file("README") -> "README"
Besides mappings
, the name
, sourceDirectory
and target
configurations are all respected by universal packaging.
Note: The Universal plugin will make anything in a bin/ directory executable. This is to work around issues with JVM and file system manipulations.
Tasks¶
universal:package-bin
- Creates the
zip
universal package.universal:package-zip-tarball
- Creates the
tgz
universal package.universal:package-xz-tarball
- Creates the
txz
universal package. Thexz
command can get better compression for some types of archives.universal:package-osx-dmg
- Creates the
dmg
universal package. This only work on macOS or systems withhdiutil
.universal-docs:package-bin
- Creates the
zip
universal documentation package.universal-docs:package-zip-tarball
- Creates the
tgz
universal documentation package.universal-docs:package-xz-tarball
- Creates the
txz
universal documentation package. Thexz
command can get better compression for some types of archives.
Customize¶
Universal Archive Options¶
You can customize the commandline options (if used) for the different zip formats. If you want to force local for the tgz output add this line:
universalArchiveOptions in (Universal, packageZipTarball) := Seq("--force-local", "-pcvf")
This will set the cli options for the packageZipTarball task in the Universal plugin to use the options --force-local
and pcvf
.
Be aware that the above line will overwrite the default options. You may want to prepend your options, doing something like:
universalArchiveOptions in (Universal, packageZipTarball) :=
(Seq("--exclude", "*~") ++ (universalArchiveOptions in (Universal, packageZipTarball)).value)
Currently, these task can be customized:
universal:package-zip-tarball
- universalArchiveOptions in (Universal, packageZipTarball)
universal:package-xz-tarball
- universalArchiveOptions in (Universal, packageXzTarball)
Getting Started with Universal Packaging¶
By default, all files found in the src/universal
directory are included in the distribution. So, the first step
in creating a distribution is to place files in this directory and organize them as you’d like in them to be in the distributed package.
If your output format is a zip file, for example, although the distribution will consist of just one zip file, the files and directories within that zip file will reflect the same organization and structure as src/universal
.
To add files generated by the build task to a distribution, simply add a mapping to the mappings in Universal
setting. Let’s
look at an example where we add the packaged jar of a project to the lib folder of a distribution:
mappings in Universal += {
val jar = (packageBin in Compile).value
jar -> ("lib/" + jar.getName)
}
The above does two things:
- It depends on
packageBin in Compile
which will generate a jar file form the project. - It creates a mapping (a
Tuple2[File, String]
) which denotes the file and the location in the distribution as a string.
You can use this pattern to add anything you desire to the package.
Note
If you are using anapplication archetype
or theplayframework
, the jar mapping is already defined and you should not include these in yourbuild.sbt
. issue 227
Universal Conventions¶
This plugin has a set of conventions for universal packages that enable the automatic generation of native packages. The universal convention has the following package layout:
bin/
<scripts and things you want on the path>
lib/
<shared libraries>
conf/
<configuration files that should be accessible using platform standard config locations.>
doc/
<Documentation files that should be easily accessible. (index.html treated specially)>
If your plugin matches these conventions, you can enable the settings to automatically generate native layouts based on your universal package. To do so, add the following to your build.sbt:
mapGenericFilesToLinux
mapGenericFilesToWinows
In Linux, this mapping creates symlinks from platform locations to the install location of the universal package. For example, given the following packaging:
bin/
cool-tool
lib/
cool-tool.jar
conf/
cool-tool.conf
The mapGenericFilesToLinux
settings will create the following package (symlinks denoted with ->
):
/usr/share/<pkg-name>/
bin/
cool-tool
lib/
cool-tool.jar
conf/
cool-tool.conf
/usr/bin/
cool-tool -> /usr/share/<package-name>/bin/cool-tool
/etc/<pkg-name> -> /usr/share/<package-name>/conf
The mapGenericFilesToWindows
will construct an MSI that installs the application in <Platform Program Files>\<Package Name>
and include
the bin
directory on Windows PATH
environment variable (optionally disabled).
While these mappings provide a great start to nice packaging, it still may be necessary to customize the native packaging for each platform. This can be done by configuring those settings directly.
For example, even using generic mapping, debian has a requirement for changelog files to be fully formed. Using the above generic mapping, we can configure just this
changelog in addition to the generic packaging by first defining a changelog in src/debian/changelog
and then adding the following setting:
linuxPackageMappings in Debian +=
(packageMapping(
((sourceDirectory in Debian).value / "changelog") -> "/usr/share/doc/sbt/changelog.gz"
) withUser "root" withGroup "root" withPerms "0644" gzipped) asDocs()
Notice how we’re only modifying the package mappings for Debian linux packages.
For more information on the underlying packaging settings, see Windows Plugin and Linux Plugin documentation.
Change/Remove Top Level Directory in Output¶
Your output package (zip, tar, gz) by default contains a single folder with your application. If you want to change this folder or remove this top level directory completely use the topLevelDirectory setting.
Removing the top level directory
topLevelDirectory := None
Changing it to another value, e.g. the packageName without the version
topLevelDirectory := Some(packageName.value)
Or just a plain hardcoded string
topLevelDirectory := Some("awesome-app")
Skip packageDoc task on stage¶
The stage
task forces a javadoc.jar build, which could slow down stage
tasks performance. In order to deactivate
this behaviour, add this to your build.sbt
mappings in (Compile, packageDoc) := Seq()
Source issue 651.
MappingsHelper¶
The MappingsHelper class provides a set of helper functions to make mapping directories easier.
sbt 0.13.5 and plugin 1.0.x or higher
import NativePackagerHelper._plugin version 0.8.x or lower
import com.typesafe.sbt.SbtNativePackager._ import NativePackagerHelper._
You get a set of methods which will help you to create mappings very easily.
mappings in Universal ++= directory("src/main/resources/cache")
mappings in Universal ++= contentOf("src/main/resources/docs")
mappings in Universal ++= directory(sourceDirectory.value / "main" / "resources" / "cache")
mappings in Universal ++= contentOf(sourceDirectory.value / "main" / "resources" / "docs")
Mapping Examples¶
SBT provides the IO and Path APIs, which help make defining custom mappings easy. The files will appear in the generate universal zip, but also in your debian/rpm/msi/dmg builds as described above in the conventions.
The packageBin in Compile
dependency is only needed if your files get generated
during the packageBin
command or before. For static files you can remove it.
Mapping a complete directory¶
There are some helper methods so you can create a mapping for a complete directory:
For static content, you can just add the directory to the mapping:
mappings in Universal ++= directory("SomeDirectoryNameToInclude")
If you want to add everything in a directory where the path for the directory is dynamic, e.g. the scala-2.10/api
directory that is nested under in the target
directory, and target
is defined in a task:
(mappings in Universal) ~= (_ ++ directory(target.value / "scala-2.10" / "api"))
You can also use the following approach if, for example, you need more flexibility:
(mappings in Universal) ++= {
val dir = target.value / "scala-2.10" / "api"
(dir ** AllPassFilter) pair relativeTo(dir.getParentFile)
}
Here is what happens in this code:
dir.***
is a PathFinder method that creates a sequence of every file under a directory, including the directory itself.
relativeTo()
returns a String that is the path relative to whatever you pass to it.
dir.getParentFile
returns the parent ofdir
. In this example, it’s the parent directory of whatevertarget
is.
pair
is a PathFinder method that takes a function and applies it to every file (in the sequence), and returns a (file, function-result) tuple.
Putting it all together, this creates a map of every file under target/scala-2.10/api
(including the directory target/scala-2.10/api
itself)
with a string that is the path to the parent of target
. This is a mapping for every file and a string that tells the universal packager where it is located.
For example:
if target = /Users/you/dev/fantasticApp/src/scala/fantasticApp-0.1-HOTFIX01
and
fantasticApp-0.1-HOTFIX01/scala-2.10/api/
contains the files
somedata.csv
README
Then the code above will produce this mapping:
((/Users/you/dev/fantasticApp/src/scala/fantasticApp-0.1-HOTFIX01,fantasticApp-0.1-HOTFIX01),
(/Users/you/dev/fantasticApp/src/scala/fantasticApp-0.1-HOTFIX01/README,fantasticApp-0.1-HOTFIX01/README),
(//Users/you/dev/fantasticApp/src/scala/fantasticApp-0.1-HOTFIX01/somedata.csv,fantasticApp-0.1-HOTFIX01/somedata.csv))
Note that the first item of each pair is the full path to where the file exists on the system /Users/you.....
, and the
second part is the just the path starting after .../scala
. That second part is what is returned from
<each file>.relativeTo(dir.getParentFile)
.
Mapping the content of a directory (excluding the directory itself)¶
mappings in Universal ++= {
val dir = target.value / "scala-2.10" / "api"
(dir ** AllPassFilter --- dir) pair relativeTo(dir)
}
The dir
gets excluded and is used as root for relativeTo(dir)
.
Filter/Remove mappings¶
If you want to remove mappings, you have to filter the current list of mappings. This example demonstrates how to build a fat jar with sbt-assembly, but using all the convenience of the sbt native packager archetypes.
tl;dr how to remove stuff
// removes all jar mappings in universal and appends the fat jar
mappings in Universal := {
// universalMappings: Seq[(File,String)]
val universalMappings = (mappings in Universal).value
val fatJar = (assembly in Compile).value
// removing means filtering
// notice the "!" - it means NOT, so only keep those that do NOT have a name ending with "jar"
val filtered = universalMappings filter {
case (file, name) => ! name.endsWith(".jar")
}
// add the fat jar to our sequence of things that we've filtered
filtered :+ (fatJar -> ("lib/" + fatJar.getName))
}
The complete build.sbt
should contain these settings if you want a single assembled fat jar.
// the assembly settings
assemblySettings
// we specify the name for our fat jar
jarName in assembly := "assembly-project.jar"
// using the java server for this application. java_application would be fine, too
packageArchetype.java_server
// removes all jar mappings in universal and appends the fat jar
mappings in Universal := {
val universalMappings = (mappings in Universal).value
val fatJar = (assembly in Compile).value
val filtered = universalMappings filter {
case (file, name) => ! name.endsWith(".jar")
}
filtered :+ (fatJar -> ("lib/" + fatJar.getName))
}
// the bash scripts classpath only needs the fat jar
scriptClasspath := Seq( (jarName in assembly).value )