From ca1def4d6f5c829701e04f86dacb720bb57f036f Mon Sep 17 00:00:00 2001 From: Andrew Richards Date: Mon, 29 Jul 2019 13:56:01 +0100 Subject: add ability to define jar manifest (#634) * add capability to define jar manifest * Don't use the custom manifest for doc-only jars This is to pass test mill.scalalib.HelloWorldTests.scalaDocOptions * Add JarManifest.Default and fix the build rebase fix --- build.sc | 1 + main/src/modules/Jvm.scala | 60 +++++++++++++++++++++------- main/test/src/eval/JavaCompileJarTests.scala | 4 +- scalalib/src/JavaModule.scala | 20 +++++++--- 4 files changed, 64 insertions(+), 21 deletions(-) diff --git a/build.sc b/build.sc index cba26933..2515abda 100755 --- a/build.sc +++ b/build.sc @@ -8,6 +8,7 @@ import mill._ import mill.scalalib._ import publish._ import mill.modules.Jvm.createAssembly + trait MillPublishModule extends PublishModule{ def artifactName = "mill-" + super.artifactName() diff --git a/main/src/modules/Jvm.scala b/main/src/modules/Jvm.scala index 39813217..fb93b839 100644 --- a/main/src/modules/Jvm.scala +++ b/main/src/modules/Jvm.scala @@ -6,7 +6,7 @@ import java.net.URI import java.nio.file.{FileSystems, Files, StandardOpenOption} import java.nio.file.attribute.PosixFilePermission import java.util.Collections -import java.util.jar.{JarEntry, JarFile, JarOutputStream} +import java.util.jar.{Attributes, JarEntry, JarFile, JarOutputStream, Manifest} import coursier.{Dependency, Fetch, Repository, Resolution} import coursier.util.{Gather, Task} @@ -19,6 +19,7 @@ import mill.api.Loose.Agg import scala.collection.mutable import scala.collection.JavaConverters._ +import upickle.default.{macroRW, ReadWriter => RW} object Jvm { /** @@ -189,15 +190,16 @@ object Jvm { } } - - private def createManifest(mainClass: Option[String]) = { - val m = new java.util.jar.Manifest() - m.getMainAttributes.put(java.util.jar.Attributes.Name.MANIFEST_VERSION, "1.0") - m.getMainAttributes.putValue( "Created-By", "Scala mill" ) - mainClass.foreach( - m.getMainAttributes.put(java.util.jar.Attributes.Name.MAIN_CLASS, _) - ) - m + def createManifest(mainClass: Option[String]): JarManifest = { + val main = + Map[String,String]( + java.util.jar.Attributes.Name.MANIFEST_VERSION.toString -> "1.0", + "Created-By" -> "Scala mill" + ) ++ + mainClass.map(mc => + Map(java.util.jar.Attributes.Name.MAIN_CLASS.toString -> mc) + ).getOrElse(Map.empty) + JarManifest(main) } /** @@ -214,7 +216,7 @@ object Jvm { * @return - a `PathRef` for the created jar. */ def createJar(inputPaths: Agg[os.Path], - mainClass: Option[String] = None, + manifest: JarManifest = JarManifest.Default, fileFilter: (os.Path, os.RelPath) => Boolean = (p: os.Path, r: os.RelPath) => true) (implicit ctx: Ctx.Dest): PathRef = { val outputPath = ctx.dest / "out.jar" @@ -224,7 +226,7 @@ object Jvm { seen.add(os.rel / "META-INF" / "MANIFEST.MF") val jar = new JarOutputStream( new FileOutputStream(outputPath.toIO), - createManifest(mainClass) + manifest.build ) try{ @@ -251,7 +253,7 @@ object Jvm { } def createAssembly(inputPaths: Agg[os.Path], - mainClass: Option[String] = None, + manifest: JarManifest = JarManifest.Default, prependShellScript: String = "", base: Option[os.Path] = None, assemblyRules: Seq[Assembly.Rule] = Assembly.defaultRules) @@ -269,7 +271,6 @@ object Jvm { val zipFs = FileSystems.newFileSystem(URI.create(baseUri), hm) - val manifest = createManifest(mainClass) val manifestPath = zipFs.getPath(JarFile.MANIFEST_NAME) Files.createDirectories(manifestPath.getParent) val manifestOut = Files.newOutputStream( @@ -277,7 +278,7 @@ object Jvm { StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE ) - manifest.write(manifestOut) + manifest.build.write(manifestOut) manifestOut.close() Assembly.groupAssemblyEntries(inputPaths, assemblyRules).view @@ -332,6 +333,7 @@ object Jvm { outputStream.close() is.close() } + def universalScript(shellCommands: String, cmdCommands: String, shebang: Boolean = false): String = { @@ -561,4 +563,32 @@ object Jvm { } } + object JarManifest { + implicit val jarManifestRW: RW[JarManifest] = upickle.default.macroRW + final val Default = createManifest(None) + } + + /** Represents a JAR manifest. + * @param main the main manifest attributes + * @param groups additional attributes for named entries + */ + final case class JarManifest(main: Map[String,String] = Map.empty, groups: Map[String, Map[String,String]] = Map.empty) { + def add(entries: (String,String)*): JarManifest = copy(main = main ++ entries) + def addGroup(group: String, entries: (String,String)*): JarManifest = + copy(groups = groups + (group -> (groups.getOrElse(group, Map.empty) ++ entries))) + + /** Constructs a [[java.util.jar.Manifest]] from this JarManifest. */ + def build: Manifest = { + val manifest = new Manifest + val mainAttributes = manifest.getMainAttributes + main.foreach{case(key,value) => mainAttributes.putValue(key, value)} + val entries = manifest.getEntries + for((group, attribs) <- groups) { + val attrib = new Attributes + attribs.foreach{case(key,value) => attrib.putValue(key, value)} + entries.put(group, attrib) + } + manifest + } + } } diff --git a/main/test/src/eval/JavaCompileJarTests.scala b/main/test/src/eval/JavaCompileJarTests.scala index 0f9002df..e243b915 100644 --- a/main/test/src/eval/JavaCompileJarTests.scala +++ b/main/test/src/eval/JavaCompileJarTests.scala @@ -2,6 +2,7 @@ package mill.eval import mill.define.{Discover, Input, Target, Task} import mill.modules.Jvm +import mill.modules.Jvm.JarManifest import mill.api.Ctx.Dest import mill.{Module, T} import mill.util.{DummyLogger, TestEvaluator, TestUtil} @@ -9,6 +10,7 @@ import mill.api.Strict.Agg import mill.api.Loose import utest._ import mill._ + object JavaCompileJarTests extends TestSuite{ def compileAll(sources: mill.api.Loose.Agg[PathRef])(implicit ctx: Dest) = { os.makeDir.all(ctx.dest) @@ -39,7 +41,7 @@ object JavaCompileJarTests extends TestSuite{ def classFiles = T{ compileAll(allSources()) } def jar = T{ Jvm.createJar(Loose.Agg(classFiles().path) ++ resourceRoot().map(_.path)) } // Test createJar() with optional file filter. - def filterJar(fileFilter: (os.Path, os.RelPath) => Boolean) = T{ Jvm.createJar(Loose.Agg(classFiles().path) ++ resourceRoot().map(_.path), None, fileFilter) } + def filterJar(fileFilter: (os.Path, os.RelPath) => Boolean) = T{ Jvm.createJar(Loose.Agg(classFiles().path) ++ resourceRoot().map(_.path), JarManifest.Default, fileFilter) } def run(mainClsName: String) = T.command{ os.proc('java, "-Duser.language=en", "-cp", classFiles().path, mainClsName).call() diff --git a/scalalib/src/JavaModule.scala b/scalalib/src/JavaModule.scala index 7b373650..c7066d15 100644 --- a/scalalib/src/JavaModule.scala +++ b/scalalib/src/JavaModule.scala @@ -1,6 +1,9 @@ package mill package scalalib +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream + import coursier.Repository import mill.define.Task import mill.define.TaskModule @@ -256,6 +259,14 @@ trait JavaModule extends mill.Module with TaskModule { outer => upstreamAssemblyClasspath() } + /** + * Creates a manifest representation which can be modifed or replaced + * The default implementation just adds the `Manifest-Version`, `Main-Class` and `Created-By` attributes + */ + def manifest = T{ + Jvm.createManifest(finalMainClassOpt().toOption) + } + /** * Build the assembly for upstream dependencies separate from the current * classpath @@ -266,7 +277,7 @@ trait JavaModule extends mill.Module with TaskModule { outer => def upstreamAssembly = T{ createAssembly( upstreamAssemblyClasspath().map(_.path), - mainClass(), + manifest(), assemblyRules = assemblyRules ) } @@ -278,7 +289,7 @@ trait JavaModule extends mill.Module with TaskModule { outer => def assembly = T{ createAssembly( Agg.from(localClasspath().map(_.path)), - finalMainClassOpt().toOption, + manifest(), prependShellScript(), Some(upstreamAssembly().path), assemblyRules @@ -292,7 +303,7 @@ trait JavaModule extends mill.Module with TaskModule { outer => def jar = T{ createJar( localClasspath().map(_.path).filter(os.exists), - mainClass() + manifest() ) } @@ -345,7 +356,7 @@ trait JavaModule extends mill.Module with TaskModule { outer => * The source jar, containing only source code for publishing to Maven Central */ def sourceJar = T { - createJar((allSources() ++ resources()).map(_.path).filter(os.exists)) + createJar((allSources() ++ resources()).map(_.path).filter(os.exists), manifest()) } /** @@ -626,4 +637,3 @@ object TestModule{ } } } - -- cgit v1.2.3