/
Package.scala
243 lines (222 loc) · 8.45 KB
/
Package.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
import java.io.File
import java.time.OffsetDateTime
import java.util.jar.{ Attributes, Manifest }
import scala.collection.JavaConverters._
import sbt.internal.util.Types.:+:
import sbt.io.IO
import sjsonnew.JsonFormat
import sbt.util.Logger
import sbt.util.{ CacheStoreFactory, FilesInfo, ModifiedFileInfo, PlainFileInfo }
import sbt.internal.util.HNil
import sbt.internal.util.HListFormats._
import sbt.util.FileInfo.{ exists, lastModified }
import sbt.util.CacheImplicits._
import sbt.util.Tracked.{ inputChanged, outputChanged }
import scala.sys.process.Process
sealed trait PackageOption
/**
* == Package ==
*
* This module provides an API to package jar files.
*
* @see [[https://docs.oracle.com/javase/tutorial/deployment/jar/index.html]]
*/
object Package {
final case class JarManifest(m: Manifest) extends PackageOption {
assert(m != null)
}
final case class MainClass(mainClassName: String) extends PackageOption
final case class ManifestAttributes(attributes: (Attributes.Name, String)*) extends PackageOption
def ManifestAttributes(attributes: (String, String)*): ManifestAttributes = {
val converted = for ((name, value) <- attributes) yield (new Attributes.Name(name), value)
new ManifestAttributes(converted: _*)
}
// 2010-01-01
private val default2010Timestamp: Long = 1262304000000L
final case class FixedTimestamp(value: Option[Long]) extends PackageOption
val keepTimestamps: Option[Long] = None
val fixed2010Timestamp: Option[Long] = Some(default2010Timestamp)
def gitCommitDateTimestamp: Option[Long] =
try {
Some(
OffsetDateTime
.parse(Process("git show -s --format=%cI").!!.trim)
.toInstant()
.toEpochMilli()
)
} catch {
case e: Exception if e.getMessage.startsWith("Nonzero") =>
sys.error(
s"git repository was expected for package timestamp; use Package.fixed2010Timestamp or Package.keepTimestamps instead"
)
}
def setFixedTimestamp(value: Option[Long]): PackageOption =
FixedTimestamp(value)
/** by default we overwrite all timestamps in JAR to epoch time 2010-01-01 for repeatable build */
lazy val defaultTimestamp: Option[Long] =
sys.env
.get("SOURCE_DATE_EPOCH")
.map(_.toLong * 1000)
.orElse(Some(default2010Timestamp))
def timeFromConfiguration(config: Configuration): Option[Long] =
config.options.collectFirst { case t: FixedTimestamp => t } match {
case Some(FixedTimestamp(value)) => value
case _ => defaultTimestamp
}
def mergeAttributes(a1: Attributes, a2: Attributes) = a1.asScala ++= a2.asScala
// merges `mergeManifest` into `manifest` (mutating `manifest` in the process)
def mergeManifests(manifest: Manifest, mergeManifest: Manifest): Unit = {
mergeAttributes(manifest.getMainAttributes, mergeManifest.getMainAttributes)
val entryMap = manifest.getEntries.asScala
for ((key, value) <- mergeManifest.getEntries.asScala) {
entryMap.get(key) match {
case Some(attributes) => mergeAttributes(attributes, value); ()
case None => entryMap.put(key, value); ()
}
}
}
/**
* The jar package configuration. Contains all relevant information to create a jar file.
*
* @param sources the jar contents
* @param jar the destination jar file
* @param options additional package information, e.g. jar manifest, main class or manifest attributes
*/
final class Configuration(
val sources: Seq[(File, String)],
val jar: File,
val options: Seq[PackageOption]
)
/**
*
* @param conf the package configuration that should be build
* @param cacheStoreFactory used for jar caching. We try to avoid rebuilds as much as possible
* @param log feedback for the user
*/
def apply(conf: Configuration, cacheStoreFactory: CacheStoreFactory, log: Logger): Unit =
apply(conf, cacheStoreFactory, log, timeFromConfiguration(conf))
/**
*
* @param conf the package configuration that should be build
* @param cacheStoreFactory used for jar caching. We try to avoid rebuilds as much as possible
* @param log feedback for the user
* @param time static timestamp to use for all entries, if any.
*/
def apply(
conf: Configuration,
cacheStoreFactory: CacheStoreFactory,
log: Logger,
time: Option[Long]
): Unit = {
val manifest = new Manifest
val main = manifest.getMainAttributes
for (option <- conf.options) {
option match {
case JarManifest(mergeManifest) => mergeManifests(manifest, mergeManifest); ()
case MainClass(mainClassName) => main.put(Attributes.Name.MAIN_CLASS, mainClassName); ()
case ManifestAttributes(attributes @ _*) => main.asScala ++= attributes; ()
case FixedTimestamp(value) => ()
case _ => log.warn("Ignored unknown package option " + option)
}
}
setVersion(main)
type Inputs = Seq[(File, String)] :+: FilesInfo[ModifiedFileInfo] :+: Manifest :+: HNil
val cachedMakeJar = inputChanged(cacheStoreFactory make "inputs") {
(inChanged, inputs: Inputs) =>
import exists.format
val sources :+: _ :+: manifest :+: HNil = inputs
outputChanged(cacheStoreFactory make "output") { (outChanged, jar: PlainFileInfo) =>
if (inChanged || outChanged) {
makeJar(sources, jar.file, manifest, log, time)
jar.file
()
} else
log.debug("Jar uptodate: " + jar.file)
}
}
val inputFiles = conf.sources.map(_._1).toSet
val inputs = conf.sources.distinct :+: lastModified(inputFiles) :+: manifest :+: HNil
cachedMakeJar(inputs)(() => exists(conf.jar))
()
}
/**
* updates the manifest version is there is none present.
*
* @param main the current jar attributes
*/
def setVersion(main: Attributes): Unit = {
val version = Attributes.Name.MANIFEST_VERSION
if (main.getValue(version) eq null) {
main.put(version, "1.0")
()
}
}
def addSpecManifestAttributes(name: String, version: String, orgName: String): PackageOption = {
import Attributes.Name._
val attribKeys = Seq(SPECIFICATION_TITLE, SPECIFICATION_VERSION, SPECIFICATION_VENDOR)
val attribVals = Seq(name, version, orgName)
ManifestAttributes(attribKeys zip attribVals: _*)
}
def addImplManifestAttributes(
name: String,
version: String,
homepage: Option[java.net.URL],
org: String,
orgName: String
): PackageOption = {
import Attributes.Name._
// The ones in Attributes.Name are deprecated saying:
// "Extension mechanism will be removed in a future release. Use class path instead."
val IMPLEMENTATION_VENDOR_ID = new Attributes.Name("Implementation-Vendor-Id")
val IMPLEMENTATION_URL = new Attributes.Name("Implementation-URL")
val attribKeys = Seq(
IMPLEMENTATION_TITLE,
IMPLEMENTATION_VERSION,
IMPLEMENTATION_VENDOR,
IMPLEMENTATION_VENDOR_ID,
)
val attribVals = Seq(name, version, orgName, org)
ManifestAttributes((attribKeys zip attribVals) ++ {
homepage map (h => (IMPLEMENTATION_URL, h.toString))
}: _*)
}
@deprecated("Specify whether to use a static timestamp", "1.4.0")
def makeJar(sources: Seq[(File, String)], jar: File, manifest: Manifest, log: Logger): Unit =
makeJar(sources, jar, manifest, log, None)
def makeJar(
sources: Seq[(File, String)],
jar: File,
manifest: Manifest,
log: Logger,
time: Option[Long]
): Unit = {
val path = jar.getAbsolutePath
log.debug("Packaging " + path + " ...")
if (jar.exists)
if (jar.isFile)
IO.delete(jar)
else
sys.error(path + " exists, but is not a regular file")
log.debug(sourcesDebugString(sources))
IO.jar(sources, jar, manifest, time)
log.debug("Done packaging.")
}
def sourcesDebugString(sources: Seq[(File, String)]): String =
"Input file mappings:\n\t" + (sources map { case (f, s) => s + "\n\t " + f } mkString ("\n\t"))
implicit def manifestFormat: JsonFormat[Manifest] = projectFormat[Manifest, Array[Byte]](
m => {
val bos = new java.io.ByteArrayOutputStream()
m write bos
bos.toByteArray
},
bs => new Manifest(new java.io.ByteArrayInputStream(bs))
)
}