diff --git a/build.sc b/build.sc index f3b74228f75..377579c7b91 100755 --- a/build.sc +++ b/build.sc @@ -454,6 +454,10 @@ object contrib extends MillModule { } } + object proguard extends MillModule { + override def compileModuleDeps = Seq(scalalib) + } + object tut extends MillModule { override def compileModuleDeps = Seq(scalalib) def testArgs = T{ diff --git a/contrib/proguard/src/Proguard.scala b/contrib/proguard/src/Proguard.scala new file mode 100644 index 00000000000..4ad1ff34399 --- /dev/null +++ b/contrib/proguard/src/Proguard.scala @@ -0,0 +1,129 @@ +package mill.contrib.proguard + +import coursier.Repositories +import mill.T +import mill.Agg +import mill.api.{Logger, Loose, PathRef, Result} +import mill.define.{Sources, Target} +import mill.scalalib.Lib.resolveDependencies +import mill.scalalib.{Dep, DepSyntax, Lib, ScalaModule} +import os.proc +import os.Path +import os.PathChunk + +/** Adds proguard capabilities when mixed-in to a module + * + * The target name is `proguard`. This runs proguard on the output jar of `asssembly` + * and outputs a shrunk/obfuscated/optimized jar under `out.jar` in the `dest/` folder. + * + * Sensible defaults are provided, so no members require overriding.. + * + */ +trait Proguard extends ScalaModule { + + /** The version of proguard to download from Maven. + * + * Note that currently this is the only available version. */ + def proguardVersion: T[String] = T { "7.0.0" } + + /** Run the "shrink" step in the proguard pipeline. Defaults to true. */ + def shrink: T[Boolean] = T { true } + + /** Run the "optimize" step in the proguard pipeline. Defaults to true. */ + def optimize: T[Boolean] = T { true } + + /** Run the "obfuscate" step in the proguard pipeline. Defaults to true. */ + def obfuscate: T[Boolean] = T { true } + + /** Run the "optimize" step in the proguard pipeline. Defaults to true. + * + * Note that this is required for Java 7 and above. + */ + def preverify: T[Boolean] = T { true } + + /** The path to JAVA_HOME. + * + * This is used for both the `java` command binary, + * as well as the standard library jars. + * Defaults to the `java.home` system property. */ + def javaHome: T[PathRef] = T.input { + PathRef(Path(System.getProperty("java.home"))) + } + + /** Specifies the input jar to proguard. Defaults to the output of the `assembly` task. */ + def inJar: T[PathRef] = T { assembly() } + + /** The library jars proguard requires + * Defaults the jars under `javaHome`. */ + def libraryJars: T[Seq[PathRef]] = T { + val javaJars = os.list(javaHome().path / "lib", sort = false).filter(_.ext == "jar") + javaJars.toSeq.map(PathRef(_)) + } + + /** Run the proguard task. + * + * The full command will be printed when run. + * The stdout and stderr of the command are written to the `dest/` folder. + * The output jar is written to `dest/our.jar`. */ + def proguard: T[PathRef] = T { + val outJar = T.dest / "out.jar" + val java = javaHome().path / "bin" / "java" + + val cmd = os.proc( + java, + "-cp", + proguardClasspath().map(_.path).mkString(":"), + "proguard.ProGuard", + steps(), + "-injars", + inJar().path, + "-outjars", + outJar, + "-libraryjars", + libraryJars().map(_.path).mkString(":"), + entryPoint(), + additionalOptions() + ) + System.out.println(cmd.command.flatMap(_.value).mkString(" ")) + cmd.call(stdout = T.dest / "stdout.txt", stderr = T.dest / "stderr.txt") + + // the call above already throws an exception on a non-zero exit code, + // so if we reached this point we've succeeded! + PathRef(outJar) + } + + /** The location of the proguard jar files. + * These are downloaded from JCenter and fed to `java -cp` + */ + def proguardClasspath: T[Loose.Agg[PathRef]] = T { + resolveDependencies( + Seq(Repositories.jcenter), + Lib.depToDependencyJava(_), + Seq(ivy"com.guardsquare:proguard-base:${proguardVersion()}")) + } + + private def steps: T[Seq[String]] = T { + (if (optimize()) Seq() else Seq("-dontoptimize")) ++ + (if (obfuscate()) Seq() else Seq("-dontobfuscate")) ++ + (if (shrink()) Seq() else Seq("-dontshrink")) ++ + (if (preverify()) Seq() else Seq("-dontpreverify")) + } + + /** The default `entrypoint` to proguard. + * + * Defaults to the `main` method of `finalMainClass`. + * Can be overriden to specify a different entrypoint, + * or additional entrypoints can be specified with `additionalOptions`. */ + def entryPoint: T[String] = T { + s"""|-keep public class ${finalMainClass()} { + | public static void main(java.lang.String[]); + |} + |""".stripMargin + } + + /** Specify any additional options to proguard. + * + * These are fed as-is to the proguard command. + * */ + def additionalOptions: T[Seq[String]] = T { Seq[String]() } +} diff --git a/contrib/proguard/test/resources/proguard/src/Main.scala b/contrib/proguard/test/resources/proguard/src/Main.scala new file mode 100644 index 00000000000..d435838289d --- /dev/null +++ b/contrib/proguard/test/resources/proguard/src/Main.scala @@ -0,0 +1,5 @@ +import java.nio.file.{Files, Paths} + +object Main extends App { + +} diff --git a/contrib/proguard/test/src/ProguardTests.scala b/contrib/proguard/test/src/ProguardTests.scala new file mode 100644 index 00000000000..2cac1096dcd --- /dev/null +++ b/contrib/proguard/test/src/ProguardTests.scala @@ -0,0 +1,50 @@ +package mill.contrib.proguard + +import mill._ +import mill.define.Sources +import mill.define.Target +import mill.scalalib.ScalaModule +import mill.util.TestEvaluator +import mill.util.TestUtil +import os.Path +import utest._ +import utest.framework.TestPath + +object ProguardTests extends TestSuite { + + object Proguard + extends TestUtil.BaseModule + with scalalib.ScalaModule + with Proguard { + // override build root to test custom builds/modules + override def millSourcePath: Path = TestUtil.getSrcPathStatic() + override def scalaVersion = "2.12.0" + } + + val testModuleSourcesPath: Path = os.pwd / 'contrib / 'proguard / 'test / 'resources / "proguard" + + def workspaceTest[T](m: TestUtil.BaseModule)(t: TestEvaluator => T)( + implicit tp: TestPath): T = { + val eval = new TestEvaluator(m) + os.remove.all(m.millSourcePath) + os.remove.all(eval.outPath) + os.makeDir.all(m.millSourcePath / os.up) + os.copy(testModuleSourcesPath, m.millSourcePath) + t(eval) + } + + def tests: Tests = Tests { + 'proguard - { + "should download proguard jars" - workspaceTest(Proguard) { eval => + val Right((agg, _)) = eval.apply(Proguard.proguardClasspath) + assert(!agg.isEmpty) + } + + "create proguarded jar" - workspaceTest(Proguard) { eval => + val Right((path, _)) = eval.apply(Proguard.proguard) + assert(os.exists(path.path)) + } + + } + } +} diff --git a/docs/pages/9 - Contrib Modules.md b/docs/pages/9 - Contrib Modules.md index 80fea16e8c9..74c1e2e045b 100644 --- a/docs/pages/9 - Contrib Modules.md +++ b/docs/pages/9 - Contrib Modules.md @@ -622,6 +622,38 @@ object app extends ScalaModule with RouterModule { } ``` +## Proguard + +This module allows [Proguard](https://www.guardsquare.com/en/products/proguard/manual/introduction) to be used in Mill builds. +ProGuard is a Java class file shrinker, optimizer, obfuscator, and preverifier. + +By default, all four steps - shrink, optimize, obfuscate, verify - are run, but this can be configured through task options. +Any additional options can be specified as a list of strings with `additionalOptions`. The full list of proguard options +can be found [here](https://www.guardsquare.com/en/products/proguard/manual/usage). + +The output of `assembly` is used as the input jar and the output is written to `out.jar` in the `dest` folder. + +The `stdout` and `stderr` from the proguard command can be found under the `dest` folder. + +The only default entrypoint is the main class (i.e. `finalMainClass` task). Additional entrypoints can be configured using `additionalOptions` as well. + +Here is a simple example: + +``` +import $ivy.`com.lihaoyi::mill-contrib-proguard:$MILL_VERSION` +import contrib.proguard._ + +object foo extends ScalaModule with Proguard { + def scalaVersion = "2.12.0" + + override def shrink: T[Boolean] = T { true } + override def optimize: T[Boolean] = T { false } + override def obfuscate: T[Boolean] = T { false } +} +``` + +Also, please note that Proguard doesn't seem to work with scala 2.13 yet. + ## ScalaPB This module allows [ScalaPB](https://scalapb.github.io) to be used in Mill builds. ScalaPB is a [Protocol Buffers](https://developers.google.com/protocol-buffers/) compiler plugin that generates Scala case classes, encoders and decoders for protobuf messages.