sbt-native-packager 基础理论基础

摘要:本文根据 sbt-native-packager 官方文档详细介绍其基本理论基础,重点在理解一些基础知识,便于理解复杂的配置,并且可以灵活自定义打包。参考文档

概述

sbt-native-packager 的初衷是:

“Code once, deploy anywhere”

也就是一次编码,在所有地方部署,可见其功能能够是比较强大,但是功能强大带来的问题是设计必须巧妙,能够兼容各种环境,所以如果希望灵活的使用该插件,就需要对其基本原理有较为深刻的理解,才能将其应用到各种环境中。

介绍

SBT native packager允许您以原始格式构建应用程序包,并为常见配置(如简单Java应用程序或服务器应用程序)提供不同的模型(archetypes)。

本节将介绍 native packager 概述及其核心概念。

Goals 目标

首先我们需要了解该插件的目标,这样我们才能合理的使用它:

  1. Native formats should build on their respective platform
    This allows native packager to support a wide range of formats as the packaging plugin serves as a wrapper around the actual packaging tool. However, alternative packaging plugins maybe provided if a java/scala implementation exists. As an example debian packages should always build on debian systems, however native packager provides an additional plugin that integrates JDeb for a platform independent packaging strategy.

原生格式应该构建与对应的平台,比如 debian 的安装包就应该在 debian 系统下构建。

  1. Provide archetypes for zero configuration builds
    While packaging plugins provide the how a package is created, archetypes provide the configuration for what gets packaged. Archetypes configure your build to create a package for a certain purpose. While an archetype may not support all packaging formats, it should work without configuration for the supported formats.

能够提供零配置的 archetypes。

  1. Enforce best-practices
    There is no single way to create a package. Native packager tries to create packages following best practices, e.g. for file names, installation paths or script layouts.

提供打包的最佳实践

Scope 范围

While native packager provides a wide range of formats and archetype configurations, its scope is relatively narrow. Native packager only takes care of packaging, the act of putting a list of mappings (source file to install target path) into a distinct package format (zip, rpm, etc.).

实际上该打包工具的核心功能很简单:打包,也就是将一系列的文件打包到对应的格式中。

我们还要知道哪些事情该插件是不负责的:

  1. 程序生命周期管理
  2. 部署配置

核心概念

职责分离

这里有两种插件

  1. format plugins: 如何创建 package (how)
  2. archetype plugins:将哪些内容放入 package 中 (what)

mappings 映射

这个概念是用于表示如何将构建的文件组织到目标系统中。

Mappings are a Seq[(File, String)], which translates to “a list of tuples, where each tuple defines a source file that gets mapped to a path on the target system”.

例如:Seq[(File, String)],表示一个 tuples 的序列,每个 tuple 定义一组源文件映射到目标系统的路径。

该理念是核心,需要重点理解。

1
mappings: TaskKey[Seq[(File, String)]]

The file part of the tuple must be available during the packaging phase. The String part represents the path inside the installation directory.

  1. 其中 file 部分是只需要被打包的文件,需要在打包阶段已经生成完毕
  2. 其中 String 部分表示安装目录的路径

进一步理解 mappings,我们可以通过 Universal Plugin 来学习。

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.

简单的说就是将项目打包为扩平台的 JVM 程序,用户需要在不同平台上手动执行命令来运行,比如在 Windows 上需要执行 bat 脚本,Linux 上需要执行 sh 脚本。

原理

参考 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:

我们需要通过 mapping 来想目标目录添加文件

1
2
3
mappings in Universal <+= (packageBin in Compile) map { jar =>
jar -> ("lib/" + jar.getName)
}

解释:

  1. 依赖于 Compile 中的 packageBin,因为这个 task 会在 target 目录下生成 jar,这些 jar 是打包必备的核心内容。
  2. 创建一个 mapping 是一个(Tuple2[File, String]),表示添加的文件和目标路径的字符串。

进一步理解 (packageBin in Compile) 会返回一个 jar 的序列,然后通过 map 操作,将每个 jar 映射为一个 Tuple2, 两个元素分别是 jar 文件本身,后一个元素是一个字符串,表示打包后目标路径,及 ("lib/" + jar.getName),也就是放置于 lib 目录下,文件名就是 jar 的名字。

关于 Mapping 的各种方法总结

关于 setting 的操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def +=[U](v: U)(implicit a: Append.Value[T, U]): Setting[Task[T]] =
macro std.TaskMacro.taskAppend1Impl[T, U]

def ++=[U](vs: U)(implicit a: Append.Values[T, U]): Setting[Task[T]] =
macro std.TaskMacro.taskAppendNImpl[T, U]

def <+=[V](v: Initialize[Task[V]])(implicit a: Append.Value[T, V]): Setting[Task[T]] =
macro std.TaskMacro.fakeTaskAppend1Position[T, V]

def <++=[V](vs: Initialize[Task[V]])(implicit a: Append.Values[T, V]): Setting[Task[T]] =
macro std.TaskMacro.fakeTaskAppendNPosition[T, V]

final def -=[U](v: U)(implicit r: Remove.Value[T, U]): Setting[Task[T]] =
macro std.TaskMacro.taskRemove1Impl[T, U]

final def --=[U](vs: U)(implicit r: Remove.Values[T, U]): Setting[Task[T]] =
macro std.TaskMacro.taskRemoveNImpl[T, U]

see https://www.scala-sbt.org/0.12.4/docs/Getting-Started/More-About-Settings.html#appending-to-previous-values-and

see https://stackoverflow.com/a/23693921/2000468

You cannot find documentation, because as @JacekLaskowski correctly pointed out all operators except for +=, ++= and := are deprecated.

You can however find the Documentation if you switch to older version of sbt.

If you however are stuck to older version, this is their meaning (via documentation):

+= and ++= append to previous value, where first appends single element and next appends a Seq
~= transforms value, e.g. you want to use value stored in a setting to get a new setting.
<<= depends on another key, for example if you call organization <<= name, then organization value is equal to name value. You can depend on multiple values, e.g. organization <<= (name, version) { (n, v) => / do something with values / }
<+= and <++= are appending with dependencies, like the append, but you can use another’s setting value to compute new value
Said that, @JacekLaskowski is right, and if you are using sbt 13.x or greater you should not have to use those operators in favours of macros.

使用方法案例

:= will replace the previous value
+= will append a single element to the sequence.
++= will concatenate another sequence.

1
2
3
4
5
6
sourceDirectories in Compile += new File("source")
sourceDirectories in Compile += file("source")

sourceDirectories in Compile ++= Seq(file("sources1"), file("sources2"))

sourceDirectories in Compile := Seq(file("sources1"), file("sources2"))

~= applies a function to the setting’s previous value, producing a new value of the same type. 也就是将之前的值经过一些处理得到新的值,再赋值。

1
2
3
4
5
6
7
// filter out src/main/scala
sourceDirectories in Compile ~= { srcDirs => srcDirs filter(!_.getAbsolutePath.endsWith("src/main/scala")) }

// srcDirs is old value of the `sourceDirectories in Compile`

// make the project name upper case
name ~= { _.toUpperCase }

Unlike <<= of course, <+= and <++= will append to the previous value of the key on the left, rather than replacing it.

For example, say you have a coverage report named after the project, and you want to add it to the files removed by clean:

1
cleanFiles <+= (name) { n => file("coverage-report-" + n + ".txt") }

关于路径的获取方法

参考链接 :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.

https://www.scala-sbt.org/0.13.1/docs/Detailed-Topics/Paths.html

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.

注意这里是一个旧的文档

这里需要实际案例测试各个功能。

案例解析

1
2
3
4
mappings in Universal <+= (packageBin in Compile) map { jar =>
jar -> ("lib/" + jar.getName)
}
// mappings in Universal 已经有值,在其基础上在 packageBin in Compile,然后再进行一个 map 操作。
1
2
3
4
5
6
7
8
mappings in Windows ++= {
// file("src/main/resources/wix/service.bat") -> "bin/service.bat"
val scriptsDir = new java.io.File("src/main/resources/wix")
scriptsDir.listFiles.toSeq.map { f =>
f -> ("bin/" + f.getName)
}
// file("src/main/resources/wix/*") -> "bin/"
}