diff --git a/main-actions/src/main/scala/sbt/Package.scala b/main-actions/src/main/scala/sbt/Package.scala index 2adb6dd87..e083cc494 100644 --- a/main-actions/src/main/scala/sbt/Package.scala +++ b/main-actions/src/main/scala/sbt/Package.scala @@ -8,6 +8,7 @@ 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.:+: @@ -23,6 +24,7 @@ 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 @@ -43,6 +45,40 @@ object Package { 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.collect { case t: FixedTimestamp => t }).headOption 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) @@ -70,9 +106,14 @@ object Package { val options: Seq[PackageOption] ) - @deprecated("Please specify whether to use a static timestamp", "1.4.0") + /** + * + * @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, None) + apply(conf, cacheStoreFactory, log, timeFromConfiguration(conf)) /** * @@ -94,6 +135,7 @@ object Package { 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) } } @@ -163,7 +205,8 @@ object Package { homepage map (h => (IMPLEMENTATION_URL, h.toString)) }: _*) } - @deprecated("Please specify whether to use a static timestamp", "1.4.0") + + @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) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 0c023f451..9355cd4be 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -223,6 +223,7 @@ object Defaults extends BuildCommon { bgCopyClasspath :== true, closeClassLoaders :== SysProp.closeClassLoaders, allowZombieClassLoaders :== true, + packageTimestamp :== Package.defaultTimestamp, ) ++ BuildServerProtocol.globalSettings private[sbt] lazy val globalIvyCore: Seq[Setting[_]] = @@ -1599,20 +1600,27 @@ object Defaults extends BuildCommon { val org = organization.value val orgName = organizationName.value val main = mainClass.value + val ts = packageTimestamp.value val old = packageOptions.value + Package.addSpecManifestAttributes(n, ver, orgName) +: Package.addImplManifestAttributes(n, ver, homepage.value, org, orgName) +: + Package.setFixedTimestamp(ts) +: main.map(Package.MainClass.apply) ++: old } ) ) ++ inTask(packageSrc)( Seq( - packageOptions := Package.addSpecManifestAttributes( - name.value, - version.value, - organizationName.value - ) +: packageOptions.value + packageOptions := { + val old = packageOptions.value + val ts = packageTimestamp.value + Package.addSpecManifestAttributes( + name.value, + version.value, + organizationName.value + ) +: Package.setFixedTimestamp(ts) +: old + } ) ) ++ packageTaskSettings(packageBin, packageBinMappings) ++ @@ -1778,10 +1786,7 @@ object Defaults extends BuildCommon { config, s.cacheStoreFactory, s.log, - sys.env - .get("SOURCE_DATE_EPOCH") - .map(_.toLong * 1000) - .orElse(Some(1262304000000L)) // 2010-01-01 + Package.timeFromConfiguration(config) ) config.jar } diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 81d6a6c61..185136395 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -265,6 +265,7 @@ object Keys { val packageCache = taskKey[File]("Produces the main artifact for caching.") val packageOptions = taskKey[Seq[PackageOption]]("Options for packaging.").withRank(BTask) + val packageTimestamp = settingKey[Option[Long]]("Overwrites timestamps in JAR file to make the build reproducible; None keeps the existing timestamps (useful for web resources)").withRank(CSetting) val packageConfiguration = taskKey[Package.Configuration]("Collects all inputs needed for packaging.").withRank(DTask) val artifactPath = settingKey[File]("The location of a generated artifact.").withRank(BPlusSetting) val artifact = settingKey[Artifact]("Describes an artifact.").withRank(BMinusSetting)