Skip to content

Commit 797e885

Browse files
authored
Merge pull request #100 from openmole/cache
Implement a cache based on hashing of the inputs of bundleTask
2 parents f05c83d + 0b5d97d commit 797e885

File tree

3 files changed

+167
-63
lines changed

3 files changed

+167
-63
lines changed

src/main/scala/com/typesafe/sbt/osgi/Osgi.scala

Lines changed: 151 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,24 @@
1616

1717
package com.typesafe.sbt.osgi
1818

19-
import java.nio.file.{ FileVisitOption, Files, Path }
20-
19+
import java.nio.file.{FileVisitOption, Files, Path}
2120
import aQute.bnd.osgi.Builder
22-
import aQute.bnd.osgi.Constants._
21+
import aQute.bnd.osgi.Constants.*
22+
import com.typesafe.sbt.osgi.OsgiKeys.CacheStrategy
23+
2324
import java.util.Properties
2425
import java.util.function.Predicate
2526
import java.util.stream.Collectors
26-
27-
import sbt._
28-
import sbt.Keys._
27+
import sbt.*
28+
import sbt.Keys.*
2929
import sbt.Package.ManifestAttributes
3030

31-
import scala.collection.JavaConverters._
31+
import scala.collection.JavaConverters.*
3232
import scala.language.implicitConversions
3333

3434
private object Osgi {
3535

36-
def bundleTask(
36+
def cachedBundle(
3737
headers: OsgiManifestHeaders,
3838
additionalHeaders: Map[String, String],
3939
fullClasspath: Seq[File],
@@ -44,62 +44,153 @@ private object Osgi {
4444
failOnUndecidedPackage: Boolean,
4545
sourceDirectories: Seq[File],
4646
packageOptions: scala.Seq[sbt.PackageOption],
47-
streams: TaskStreams,
48-
useJVMJar: Boolean): File = {
49-
val builder = new Builder
50-
51-
if (failOnUndecidedPackage) {
52-
streams.log.info("Validating all packages are set private or exported for OSGi explicitly...")
53-
val internal = headers.privatePackage
54-
val exported = headers.exportPackage
55-
validateAllPackagesDecidedAbout(internal, exported, sourceDirectories)
56-
}
57-
58-
builder.setClasspath(fullClasspath.toArray)
59-
60-
val props = headersToProperties(headers, additionalHeaders)
61-
addPackageOptions(props, packageOptions)
62-
builder.setProperties(props)
63-
64-
includeResourceProperty(resourceDirectories.filter(_.exists), embeddedJars, explodedJars) foreach (dirs =>
65-
builder.setProperty(INCLUDERESOURCE, dirs))
66-
bundleClasspathProperty(embeddedJars) foreach (jars =>
67-
builder.setProperty(BUNDLE_CLASSPATH, jars))
68-
// Write to a temporary file to prevent trying to simultaneously read from and write to the
69-
// same jar file in exportJars mode (which causes a NullPointerException).
70-
val tmpArtifactPath = file(artifactPath.absolutePath + ".tmp")
71-
// builder.build is not thread-safe because it uses a static SimpleDateFormat. This ensures
72-
// that all calls to builder.build are serialized.
73-
val jar = synchronized {
74-
builder.build
75-
}
76-
val log = streams.log
77-
builder.getWarnings.asScala.foreach(s => log.warn(s"bnd: $s"))
78-
builder.getErrors.asScala.foreach(s => log.error(s"bnd: $s"))
79-
80-
if (!useJVMJar) jar.write(tmpArtifactPath)
81-
else {
82-
val tmpArtifactDirectoryPath = file(artifactPath.absolutePath + "_tmpdir")
83-
IO.delete(tmpArtifactDirectoryPath)
84-
tmpArtifactDirectoryPath.mkdirs()
85-
86-
val manifest = jar.getManifest
87-
jar.writeFolder(tmpArtifactDirectoryPath)
88-
89-
def content = {
90-
import _root_.java.nio.file._
91-
import _root_.scala.collection.JavaConverters._
92-
val path = tmpArtifactDirectoryPath.toPath
93-
Files.walk(path).iterator.asScala.map(f => f.toFile -> path.relativize(f).toString).filterNot { case (_, p) => p == "META-INF/MANIFEST.MF" }.toTraversable
94-
}
47+
useJVMJar: Boolean,
48+
cacheStrategy: Option[CacheStrategy]): Option[File] = cacheStrategy.flatMap { strategy =>
49+
50+
def fileFootprint(file: File) = {
51+
def footprint(f: File) =
52+
strategy match {
53+
case CacheStrategy.LastModified => FileInfo.lastModified(f).lastModified.toString
54+
case CacheStrategy.Hash => Hash.toHex(FileInfo.hash(f).hash.toArray)
55+
}
9556

96-
IO.jar(content, tmpArtifactPath, manifest)
97-
IO.delete(tmpArtifactDirectoryPath)
57+
if (!file.exists()) Seq()
58+
else if (file.isDirectory) Files.walk(file.toPath).iterator().asScala.map(f => f.toAbsolutePath.toString -> footprint(f.toFile).toSeq)
59+
else Seq(file.absolutePath -> footprint(file))
9860
}
9961

100-
IO.move(tmpArtifactPath, artifactPath)
101-
artifactPath
62+
def serialized =
63+
s"""${headers}
64+
|${additionalHeaders}
65+
|${fullClasspath.flatMap(fileFootprint)}
66+
|${artifactPath}
67+
|${resourceDirectories.flatMap(fileFootprint)}
68+
|${embeddedJars.flatMap(fileFootprint)}
69+
|${explodedJars.flatMap(fileFootprint)}
70+
|$failOnUndecidedPackage
71+
|${sourceDirectories.flatMap(fileFootprint)}
72+
|${packageOptions}
73+
|$useJVMJar
74+
|""".stripMargin
75+
76+
def footprint = Hash.apply(serialized).mkString("")
77+
78+
val footprintValue = footprint
79+
val bundleCacheFootprint = file(artifactPath.absolutePath + "_footprint")
80+
81+
if (!bundleCacheFootprint.exists() || IO.read(bundleCacheFootprint) != footprintValue) {
82+
IO.write(bundleCacheFootprint, footprintValue)
83+
None
84+
} else if (artifactPath.exists()) Some(artifactPath) else None
10285
}
86+
def withCache(
87+
headers: OsgiManifestHeaders,
88+
additionalHeaders: Map[String, String],
89+
fullClasspath: Seq[File],
90+
artifactPath: File,
91+
resourceDirectories: Seq[File],
92+
embeddedJars: Seq[File],
93+
explodedJars: Seq[File],
94+
failOnUndecidedPackage: Boolean,
95+
sourceDirectories: Seq[File],
96+
packageOptions: scala.Seq[sbt.PackageOption],
97+
useJVMJar: Boolean,
98+
cacheStrategy: Option[CacheStrategy])(produce: => File): File =
99+
cachedBundle(
100+
headers,
101+
additionalHeaders,
102+
fullClasspath,
103+
artifactPath,
104+
resourceDirectories,
105+
embeddedJars,
106+
explodedJars,
107+
failOnUndecidedPackage,
108+
sourceDirectories,
109+
packageOptions,
110+
useJVMJar,
111+
cacheStrategy
112+
).getOrElse(produce)
113+
114+
def bundleTask(
115+
headers: OsgiManifestHeaders,
116+
additionalHeaders: Map[String, String],
117+
fullClasspath: Seq[File],
118+
artifactPath: File,
119+
resourceDirectories: Seq[File],
120+
embeddedJars: Seq[File],
121+
explodedJars: Seq[File],
122+
failOnUndecidedPackage: Boolean,
123+
sourceDirectories: Seq[File],
124+
packageOptions: scala.Seq[sbt.PackageOption],
125+
useJVMJar: Boolean,
126+
cacheStrategy: Option[CacheStrategy],
127+
streams: TaskStreams): File =
128+
withCache(headers,
129+
additionalHeaders,
130+
fullClasspath,
131+
artifactPath,
132+
resourceDirectories,
133+
embeddedJars,
134+
explodedJars,
135+
failOnUndecidedPackage,
136+
sourceDirectories,
137+
packageOptions,
138+
useJVMJar,
139+
cacheStrategy) {
140+
val builder = new Builder
141+
142+
if (failOnUndecidedPackage) {
143+
streams.log.info("Validating all packages are set private or exported for OSGi explicitly...")
144+
val internal = headers.privatePackage
145+
val exported = headers.exportPackage
146+
validateAllPackagesDecidedAbout(internal, exported, sourceDirectories)
147+
}
148+
149+
builder.setClasspath(fullClasspath.toArray)
150+
151+
val props = headersToProperties(headers, additionalHeaders)
152+
addPackageOptions(props, packageOptions)
153+
builder.setProperties(props)
154+
155+
includeResourceProperty(resourceDirectories.filter(_.exists), embeddedJars, explodedJars) foreach (dirs =>
156+
builder.setProperty(INCLUDERESOURCE, dirs))
157+
bundleClasspathProperty(embeddedJars) foreach (jars =>
158+
builder.setProperty(BUNDLE_CLASSPATH, jars))
159+
// Write to a temporary file to prevent trying to simultaneously read from and write to the
160+
// same jar file in exportJars mode (which causes a NullPointerException).
161+
val tmpArtifactPath = file(artifactPath.absolutePath + ".tmp")
162+
// builder.build is not thread-safe because it uses a static SimpleDateFormat. This ensures
163+
// that all calls to builder.build are serialized.
164+
val jar = synchronized {
165+
builder.build
166+
}
167+
val log = streams.log
168+
builder.getWarnings.asScala.foreach(s => log.warn(s"bnd: $s"))
169+
builder.getErrors.asScala.foreach(s => log.error(s"bnd: $s"))
170+
171+
if (!useJVMJar) jar.write(tmpArtifactPath)
172+
else {
173+
val tmpArtifactDirectoryPath = file(artifactPath.absolutePath + "_tmpdir")
174+
IO.delete(tmpArtifactDirectoryPath)
175+
tmpArtifactDirectoryPath.mkdirs()
176+
177+
val manifest = jar.getManifest
178+
jar.writeFolder(tmpArtifactDirectoryPath)
179+
180+
def content = {
181+
import _root_.java.nio.file._
182+
import _root_.scala.collection.JavaConverters._
183+
val path = tmpArtifactDirectoryPath.toPath
184+
Files.walk(path).iterator.asScala.map(f => f.toFile -> path.relativize(f).toString).filterNot { case (_, p) => p == "META-INF/MANIFEST.MF" }.toTraversable
185+
}
186+
187+
IO.jar(content, tmpArtifactPath, manifest)
188+
IO.delete(tmpArtifactDirectoryPath)
189+
}
190+
191+
IO.move(tmpArtifactPath, artifactPath)
192+
artifactPath
193+
}
103194

104195
private def addPackageOptions(props: Properties, packageOptions: Seq[PackageOption]) = {
105196
packageOptions

src/main/scala/com/typesafe/sbt/osgi/OsgiKeys.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,17 @@ object OsgiKeys {
107107
SettingKey[Boolean](prefix("PackageWithJVMJar"), "Use the JVM jar tools to craft the bundle instead of the one from BND." +
108108
"Without this setting the produced bundle are detected as corrupted by recent JVMs")
109109

110+
val cacheStrategy: SettingKey[Option[CacheStrategy]] =
111+
SettingKey[Option[CacheStrategy]](prefix("CacheBundle"), "Do not build a new bundle if a bundle already exists and has been crafted from identical inputs")
112+
113+
110114
private def prefix(key: String) = "osgi" + key
111115

116+
117+
sealed trait CacheStrategy
118+
119+
object CacheStrategy {
120+
object Hash extends CacheStrategy
121+
object LastModified extends CacheStrategy
122+
}
112123
}

src/main/scala/com/typesafe/sbt/osgi/SbtOsgi.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ object SbtOsgi extends AutoPlugin {
5252
failOnUndecidedPackage.value,
5353
(sourceDirectories in Compile).value,
5454
(packageOptions in (Compile, packageBin)).value,
55-
streams.value,
56-
packageWithJVMJar.value),
55+
packageWithJVMJar.value,
56+
cacheStrategy.value,
57+
streams.value),
5758
Compile / sbt.Keys.packageBin := bundle.value,
5859
manifestHeaders := OsgiManifestHeaders(
5960
bundleActivator.value,
@@ -87,6 +88,7 @@ object SbtOsgi extends AutoPlugin {
8788
additionalHeaders := Map.empty,
8889
embeddedJars := Nil,
8990
explodedJars := Nil,
90-
packageWithJVMJar := false)
91+
packageWithJVMJar := false,
92+
cacheStrategy := None)
9193
}
9294
}

0 commit comments

Comments
 (0)