Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a cache based on hashing of the inputs of bundleTask #100

Merged
merged 4 commits into from
Jan 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 151 additions & 60 deletions src/main/scala/com/typesafe/sbt/osgi/Osgi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,24 @@

package com.typesafe.sbt.osgi

import java.nio.file.{ FileVisitOption, Files, Path }

import java.nio.file.{FileVisitOption, Files, Path}
import aQute.bnd.osgi.Builder
import aQute.bnd.osgi.Constants._
import aQute.bnd.osgi.Constants.*
import com.typesafe.sbt.osgi.OsgiKeys.CacheStrategy

import java.util.Properties
import java.util.function.Predicate
import java.util.stream.Collectors

import sbt._
import sbt.Keys._
import sbt.*
import sbt.Keys.*
import sbt.Package.ManifestAttributes

import scala.collection.JavaConverters._
import scala.collection.JavaConverters.*
import scala.language.implicitConversions

private object Osgi {

def bundleTask(
def cachedBundle(
headers: OsgiManifestHeaders,
additionalHeaders: Map[String, String],
fullClasspath: Seq[File],
Expand All @@ -44,62 +44,153 @@ 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
}
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
}
useJVMJar: Boolean,
cacheStrategy: Option[CacheStrategy]): Option[File] = cacheStrategy.flatMap { strategy =>

def fileFootprint(file: File) = {
def footprint(f: File) =
strategy match {
case CacheStrategy.LastModified => FileInfo.lastModified(f).lastModified.toString
case CacheStrategy.Hash => Hash.toHex(FileInfo.hash(f).hash.toArray)
}

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

IO.move(tmpArtifactPath, artifactPath)
artifactPath
def serialized =
s"""${headers}
|${additionalHeaders}
|${fullClasspath.flatMap(fileFootprint)}
|${artifactPath}
|${resourceDirectories.flatMap(fileFootprint)}
|${embeddedJars.flatMap(fileFootprint)}
|${explodedJars.flatMap(fileFootprint)}
|$failOnUndecidedPackage
|${sourceDirectories.flatMap(fileFootprint)}
|${packageOptions}
|$useJVMJar
|""".stripMargin

def footprint = Hash.apply(serialized).mkString("")

val footprintValue = footprint
val bundleCacheFootprint = file(artifactPath.absolutePath + "_footprint")

if (!bundleCacheFootprint.exists() || IO.read(bundleCacheFootprint) != footprintValue) {
IO.write(bundleCacheFootprint, footprintValue)
None
} else if (artifactPath.exists()) Some(artifactPath) else None
}
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,
cacheStrategy: Option[CacheStrategy])(produce: => File): File =
cachedBundle(
headers,
additionalHeaders,
fullClasspath,
artifactPath,
resourceDirectories,
embeddedJars,
explodedJars,
failOnUndecidedPackage,
sourceDirectories,
packageOptions,
useJVMJar,
cacheStrategy
).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,
cacheStrategy: Option[CacheStrategy],
streams: TaskStreams): File =
withCache(headers,
additionalHeaders,
fullClasspath,
artifactPath,
resourceDirectories,
embeddedJars,
explodedJars,
failOnUndecidedPackage,
sourceDirectories,
packageOptions,
useJVMJar,
cacheStrategy) {
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mdedetrich This "META-INF/MANIFEST.MF" maybe cause an issue on Windows.

could you change to this like #115 does?

 Files.walk(path).iterator.asScala.map(f => f.toFile -> path.relativize(f))
              .collect { case (f, p) if p != (file("META-INF") / "MANIFEST.MF").toPath => (f, p.toString) }

Copy link

@Roiocam Roiocam Jan 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NVM, I walked into the wrong CR path, this is an outdated comment.

}

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

IO.move(tmpArtifactPath, artifactPath)
artifactPath
}

private def addPackageOptions(props: Properties, packageOptions: Seq[PackageOption]) = {
packageOptions
Expand Down
11 changes: 11 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,17 @@ 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 cacheStrategy: SettingKey[Option[CacheStrategy]] =
SettingKey[Option[CacheStrategy]](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


sealed trait CacheStrategy

object CacheStrategy {
object Hash extends CacheStrategy
object LastModified extends CacheStrategy
}
}
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,
cacheStrategy.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,
cacheStrategy := None)
}
}
Loading