From 628d908a5c053ae03a3bb19a28e96e9d389c3c3b Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 3 Jan 2021 12:31:12 -0500 Subject: [PATCH] packageTimestamp setting Fixes https://github.com/sbt/sbt/issues/6235 In sbt 1.4.0 (https://github.com/sbt/sbt/pull/5344) we started wiping out the timestamps in JAR to make the builds more repeatable. This had an unintended consequence of breaking Play's last-modified response header (https://github.com/playframework/playframework/issues/10572). This adds a global setting called `packageTimestamp`, which is initialized as follows: ```scala packageTimestamp :== Package.defaultTimestamp, ``` Here the `Package.defaultTimestamp` would pick either the value from the `SOURCE_DATE_EPOCH` environment variable or 2010-01-01. To opt out of this default, the user can use: ```scala ThisBuild / packageTimestamp := Package.keepTimestamps // or ThisBuild / packageTimestamp := Package.gitCommitDateTimestamp ``` Before (sbt 1.4.6) ------------------ ``` $ ll example total 32 -rw-r--r-- 1 eed3si9n wheel 901 Jan 1 1970 Greeting.class -rw-r--r-- 1 eed3si9n wheel 3079 Jan 1 1970 Hello$.class -rw-r--r-- 1 eed3si9n wheel 738 Jan 1 1970 Hello$delayedInit$body.class -rw-r--r-- 1 eed3si9n wheel 875 Jan 1 1970 Hello.class ``` After (using Package.gitCommitDateTimestamp) -------------------------------------------- ``` $ unzip -v target/scala-2.13/root_2.13-0.1.0-SNAPSHOT.jar Archive: target/scala-2.13/root_2.13-0.1.0-SNAPSHOT.jar Length Method Size Cmpr Date Time CRC-32 Name -------- ------ ------- ---- ---------- ----- -------- ---- 288 Defl:N 136 53% 01-25-2021 03:09 888682a9 META-INF/MANIFEST.MF 0 Stored 0 0% 01-25-2021 03:09 00000000 example/ 901 Defl:N 601 33% 01-25-2021 03:09 3543f377 example/Greeting.class 3079 Defl:N 1279 59% 01-25-2021 03:09 848b4386 example/Hello$.class 738 Defl:N 464 37% 01-25-2021 03:09 571f4288 example/Hello$delayedInit$body.class 875 Defl:N 594 32% 01-25-2021 03:09 ad295259 example/Hello.class -------- ------- --- ------- 5881 3074 48% 6 files ``` --- main-actions/src/main/scala/sbt/Package.scala | 49 +++++++++++++++++-- main/src/main/scala/sbt/Defaults.scala | 23 +++++---- main/src/main/scala/sbt/Keys.scala | 1 + 3 files changed, 61 insertions(+), 12 deletions(-) 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 0ad2bea7f..ed67ab3cb 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)