sbt.Package.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of actions_2.12 Show documentation
Show all versions of actions_2.12 Show documentation
sbt is an interactive build tool
/*
* 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))
)
}