Skip to content

Commit

Permalink
Implement cache to skip bundle creation process when a bundle exists …
Browse files Browse the repository at this point in the history
…and has been produced using identical inputs
  • Loading branch information
romainreuillon committed Nov 8, 2023
1 parent f05c83d commit 95f152c
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 54 deletions.
187 changes: 136 additions & 51 deletions src/main/scala/com/typesafe/sbt/osgi/Osgi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import scala.language.implicitConversions

private object Osgi {

def bundleTask(
def cachedBundle(
headers: OsgiManifestHeaders,
additionalHeaders: Map[String, String],
fullClasspath: Seq[File],
Expand All @@ -44,63 +44,148 @@ private object Osgi {
failOnUndecidedPackage: Boolean,
sourceDirectories: Seq[File],
packageOptions: scala.Seq[sbt.PackageOption],
streams: TaskStreams,
useJVMJar: Boolean): File = {
val builder = new Builder

if (failOnUndecidedPackage) {
streams.log.info("Validating all packages are set private or exported for OSGi explicitly...")
val internal = headers.privatePackage
val exported = headers.exportPackage
validateAllPackagesDecidedAbout(internal, exported, sourceDirectories)
}

builder.setClasspath(fullClasspath.toArray)

val props = headersToProperties(headers, additionalHeaders)
addPackageOptions(props, packageOptions)
builder.setProperties(props)

includeResourceProperty(resourceDirectories.filter(_.exists), embeddedJars, explodedJars) foreach (dirs =>
builder.setProperty(INCLUDERESOURCE, dirs))
bundleClasspathProperty(embeddedJars) foreach (jars =>
builder.setProperty(BUNDLE_CLASSPATH, jars))
// Write to a temporary file to prevent trying to simultaneously read from and write to the
// same jar file in exportJars mode (which causes a NullPointerException).
val tmpArtifactPath = file(artifactPath.absolutePath + ".tmp")
// builder.build is not thread-safe because it uses a static SimpleDateFormat. This ensures
// that all calls to builder.build are serialized.
val jar = synchronized {
builder.build
useJVMJar: Boolean,
cacheBundle: Boolean): Option[File] = {

def footprint = {
val serialised =
s"""${headers}
|${additionalHeaders}
|${fullClasspath.map(f => FileInfo.lastModified(f).lastModified)}
|${artifactPath}
|${resourceDirectories.map(f => FileInfo.lastModified(f).lastModified)}
|${embeddedJars.map(f => FileInfo.lastModified(f).lastModified)}
|${explodedJars.map(f => FileInfo.lastModified(f).lastModified)}
|$failOnUndecidedPackage
|${sourceDirectories.map(f => FileInfo.lastModified(f).lastModified)}
|${packageOptions}
|$useJVMJar
|""".stripMargin

Hash.apply(serialised).mkString("")
}
val log = streams.log
builder.getWarnings.asScala.foreach(s => log.warn(s"bnd: $s"))
builder.getErrors.asScala.foreach(s => log.error(s"bnd: $s"))

if (!useJVMJar) jar.write(tmpArtifactPath)
if (!cacheBundle) None
else {
val tmpArtifactDirectoryPath = file(artifactPath.absolutePath + "_tmpdir")
IO.delete(tmpArtifactDirectoryPath)
tmpArtifactDirectoryPath.mkdirs()

val manifest = jar.getManifest
jar.writeFolder(tmpArtifactDirectoryPath)

def content = {
import _root_.java.nio.file._
import _root_.scala.collection.JavaConverters._
val path = tmpArtifactDirectoryPath.toPath
Files.walk(path).iterator.asScala.map(f => f.toFile -> path.relativize(f).toString).filterNot { case (_, p) => p == "META-INF/MANIFEST.MF" }.toTraversable
}
val footprintValue = footprint
val bundleCacheFootprint = file(artifactPath.absolutePath + "_footprint")

IO.jar(content, tmpArtifactPath, manifest)
IO.delete(tmpArtifactDirectoryPath)
if(!bundleCacheFootprint.exists() || IO.read(bundleCacheFootprint) != footprintValue) {
IO.write(bundleCacheFootprint, footprintValue)
None
} else if(artifactPath.exists()) Some(artifactPath) else None
}

IO.move(tmpArtifactPath, artifactPath)
artifactPath
}

def withCache(
headers: OsgiManifestHeaders,
additionalHeaders: Map[String, String],
fullClasspath: Seq[File],
artifactPath: File,
resourceDirectories: Seq[File],
embeddedJars: Seq[File],
explodedJars: Seq[File],
failOnUndecidedPackage: Boolean,
sourceDirectories: Seq[File],
packageOptions: scala.Seq[sbt.PackageOption],
useJVMJar: Boolean,
cacheBundle: Boolean)(produce: => File): File =
cachedBundle(
headers,
additionalHeaders,
fullClasspath,
artifactPath,
resourceDirectories,
embeddedJars,
explodedJars,
failOnUndecidedPackage,
sourceDirectories,
packageOptions,
useJVMJar,
cacheBundle
).getOrElse(produce)

def bundleTask(
headers: OsgiManifestHeaders,
additionalHeaders: Map[String, String],
fullClasspath: Seq[File],
artifactPath: File,
resourceDirectories: Seq[File],
embeddedJars: Seq[File],
explodedJars: Seq[File],
failOnUndecidedPackage: Boolean,
sourceDirectories: Seq[File],
packageOptions: scala.Seq[sbt.PackageOption],
useJVMJar: Boolean,
cacheBundle: Boolean,
streams: TaskStreams): File =
withCache(headers,
additionalHeaders,
fullClasspath,
artifactPath,
resourceDirectories,
embeddedJars,
explodedJars,
failOnUndecidedPackage,
sourceDirectories,
packageOptions,
useJVMJar,
cacheBundle) {
val builder = new Builder

if (failOnUndecidedPackage) {
streams.log.info("Validating all packages are set private or exported for OSGi explicitly...")
val internal = headers.privatePackage
val exported = headers.exportPackage
validateAllPackagesDecidedAbout(internal, exported, sourceDirectories)
}

builder.setClasspath(fullClasspath.toArray)

val props = headersToProperties(headers, additionalHeaders)
addPackageOptions(props, packageOptions)
builder.setProperties(props)

includeResourceProperty(resourceDirectories.filter(_.exists), embeddedJars, explodedJars) foreach (dirs =>
builder.setProperty(INCLUDERESOURCE, dirs))
bundleClasspathProperty(embeddedJars) foreach (jars =>
builder.setProperty(BUNDLE_CLASSPATH, jars))
// Write to a temporary file to prevent trying to simultaneously read from and write to the
// same jar file in exportJars mode (which causes a NullPointerException).
val tmpArtifactPath = file(artifactPath.absolutePath + ".tmp")
// builder.build is not thread-safe because it uses a static SimpleDateFormat. This ensures
// that all calls to builder.build are serialized.
val jar = synchronized {
builder.build
}
val log = streams.log
builder.getWarnings.asScala.foreach(s => log.warn(s"bnd: $s"))
builder.getErrors.asScala.foreach(s => log.error(s"bnd: $s"))

if (!useJVMJar) jar.write(tmpArtifactPath)
else {
val tmpArtifactDirectoryPath = file(artifactPath.absolutePath + "_tmpdir")
IO.delete(tmpArtifactDirectoryPath)
tmpArtifactDirectoryPath.mkdirs()

val manifest = jar.getManifest
jar.writeFolder(tmpArtifactDirectoryPath)

def content = {
import _root_.java.nio.file._
import _root_.scala.collection.JavaConverters._
val path = tmpArtifactDirectoryPath.toPath
Files.walk(path).iterator.asScala.map(f => f.toFile -> path.relativize(f).toString).filterNot { case (_, p) => p == "META-INF/MANIFEST.MF" }.toTraversable
}

IO.jar(content, tmpArtifactPath, manifest)
IO.delete(tmpArtifactDirectoryPath)
}

IO.move(tmpArtifactPath, artifactPath)
artifactPath
}

private def addPackageOptions(props: Properties, packageOptions: Seq[PackageOption]) = {
packageOptions
.collect({ case attr: ManifestAttributes attr.attributes })
Expand Down
4 changes: 4 additions & 0 deletions src/main/scala/com/typesafe/sbt/osgi/OsgiKeys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ object OsgiKeys {
SettingKey[Boolean](prefix("PackageWithJVMJar"), "Use the JVM jar tools to craft the bundle instead of the one from BND." +
"Without this setting the produced bundle are detected as corrupted by recent JVMs")

val cacheBundle: SettingKey[Boolean] =
SettingKey[Boolean](prefix("CacheBundle"), "Do not build a new bundle if a bundle already exists and has been crafted from identical inputs")


private def prefix(key: String) = "osgi" + key

}
8 changes: 5 additions & 3 deletions src/main/scala/com/typesafe/sbt/osgi/SbtOsgi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ object SbtOsgi extends AutoPlugin {
failOnUndecidedPackage.value,
(sourceDirectories in Compile).value,
(packageOptions in (Compile, packageBin)).value,
streams.value,
packageWithJVMJar.value),
packageWithJVMJar.value,
cacheBundle.value,
streams.value),
Compile / sbt.Keys.packageBin := bundle.value,
manifestHeaders := OsgiManifestHeaders(
bundleActivator.value,
Expand Down Expand Up @@ -87,6 +88,7 @@ object SbtOsgi extends AutoPlugin {
additionalHeaders := Map.empty,
embeddedJars := Nil,
explodedJars := Nil,
packageWithJVMJar := false)
packageWithJVMJar := false,
cacheBundle := false)
}
}

0 comments on commit 95f152c

Please sign in to comment.