1616
1717package com .typesafe .sbt .osgi
1818
19- import java .nio .file .{ FileVisitOption , Files , Path }
20-
19+ import java .nio .file .{FileVisitOption , Files , Path }
2120import 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+
2324import java .util .Properties
2425import java .util .function .Predicate
2526import java .util .stream .Collectors
26-
27- import sbt ._
28- import sbt .Keys ._
27+ import sbt .*
28+ import sbt .Keys .*
2929import sbt .Package .ManifestAttributes
3030
31- import scala .collection .JavaConverters ._
31+ import scala .collection .JavaConverters .*
3232import scala .language .implicitConversions
3333
3434private 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
0 commit comments