├── project ├── build.properties └── plugins.sbt ├── src ├── sbt-test │ └── proguard │ │ ├── filter │ │ ├── test │ │ ├── src │ │ │ └── main │ │ │ │ └── scala │ │ │ │ └── Test.scala │ │ ├── project │ │ │ └── plugins.sbt │ │ ├── test.sbt │ │ └── build.sbt │ │ ├── merge │ │ ├── test │ │ ├── src │ │ │ └── main │ │ │ │ └── scala │ │ │ │ └── Test.scala │ │ ├── project │ │ │ └── plugins.sbt │ │ ├── test.sbt │ │ └── build.sbt │ │ ├── scala3 │ │ ├── test │ │ ├── project │ │ │ └── plugins.sbt │ │ ├── src │ │ │ └── main │ │ │ │ └── scala │ │ │ │ └── Test.scala │ │ ├── build.sbt │ │ └── test.sbt │ │ └── simple │ │ ├── test │ │ ├── src │ │ └── main │ │ │ └── scala │ │ │ └── Test.scala │ │ ├── project │ │ └── plugins.sbt │ │ ├── test.sbt │ │ └── build.sbt └── main │ └── scala │ └── com │ └── typesafe │ └── sbt │ ├── ProguardKeys.scala │ ├── proguard │ └── Merge.scala │ └── SbtProguard.scala ├── .gitignore ├── .github └── workflows │ ├── release.yml │ └── ci.yml └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.7 2 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/filter/test: -------------------------------------------------------------------------------- 1 | > check 2 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/merge/test: -------------------------------------------------------------------------------- 1 | > check 2 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/scala3/test: -------------------------------------------------------------------------------- 1 | > check 2 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/simple/test: -------------------------------------------------------------------------------- 1 | > check 2 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/filter/src/main/scala/Test.scala: -------------------------------------------------------------------------------- 1 | object Test extends App { 2 | println("test") 3 | } -------------------------------------------------------------------------------- /src/sbt-test/proguard/merge/src/main/scala/Test.scala: -------------------------------------------------------------------------------- 1 | object Test extends App { 2 | println("test") 3 | } -------------------------------------------------------------------------------- /src/sbt-test/proguard/simple/src/main/scala/Test.scala: -------------------------------------------------------------------------------- 1 | object Test extends App { 2 | println("test") 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | project/project/ 2 | target/ 3 | .idea/* 4 | .bsp/ 5 | /src/sbt-test/proguard/*/project/build.properties 6 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/filter/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-proguard" % sys.props("project.version")) 2 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/merge/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-proguard" % sys.props("project.version")) 2 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/scala3/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-proguard" % sys.props("project.version")) 2 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/simple/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-proguard" % sys.props("project.version")) 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.9.2") 2 | addSbtPlugin("com.eed3si9n" % "sbt-nocomma" % "0.1.2") 3 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/scala3/src/main/scala/Test.scala: -------------------------------------------------------------------------------- 1 | object Test { 2 | def main(args: Array[String]) = { 3 | println(ObfuscateMe.foo) 4 | } 5 | } 6 | 7 | 8 | object ObfuscateMe { 9 | def foo: String = "test" 10 | } 11 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/simple/test.sbt: -------------------------------------------------------------------------------- 1 | import scala.sys.process.Process 2 | 3 | // for sbt scripted test: 4 | TaskKey[Unit]("check") := { 5 | val expected = "test\n" 6 | val output = Process("java", Seq("-jar", (Proguard / proguard).value.absString)).!! 7 | .replaceAllLiterally("\r\n", "\n") 8 | if (output != expected) sys.error("Unexpected output:\n" + output) 9 | } 10 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/merge/test.sbt: -------------------------------------------------------------------------------- 1 | import scala.sys.process.Process 2 | 3 | // for sbt scripted test: 4 | TaskKey[Unit]("check") := { 5 | val expected = "test\n" 6 | val output = Process("java", Seq("-classpath", (Proguard / proguard).value.absString, "Test")).!! 7 | .replaceAllLiterally("\r\n", "\n") 8 | if (output != expected) sys.error("Unexpected output:\n" + output) 9 | } 10 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/filter/test.sbt: -------------------------------------------------------------------------------- 1 | import scala.sys.process.Process 2 | 3 | // for sbt scripted test: 4 | TaskKey[Unit]("check") := { 5 | val cp = (Proguard / proguard).value 6 | val expected = "test\n" 7 | val output = Process("java", Seq("-classpath", cp.absString, "Test")).!! 8 | .replaceAllLiterally("\r\n", "\n") 9 | if (output != expected) sys.error("Unexpected output:\n" + output) 10 | } 11 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/merge/build.sbt: -------------------------------------------------------------------------------- 1 | import java.nio.file.FileSystems 2 | 3 | enablePlugins(SbtProguard) 4 | 5 | scalaVersion := "2.13.6" 6 | name := "merge" 7 | 8 | (Proguard / proguardMerge) := true 9 | (Proguard / proguardOptions) ++= Seq("-dontoptimize", "-dontnote", "-dontwarn", "-ignorewarnings") 10 | (Proguard / proguardOptions) += ProguardOptions.keepMain("Test") 11 | (Proguard / proguardMergeStrategies) += ProguardMerge.discard("META-INF/.*".r) 12 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/simple/build.sbt: -------------------------------------------------------------------------------- 1 | import java.nio.file.FileSystems 2 | 3 | enablePlugins(SbtProguard) 4 | 5 | scalaVersion := "2.13.6" 6 | name := "simple" 7 | 8 | Proguard / proguardOptions ++= Seq("-dontoptimize", "-dontnote", "-dontwarn", "-ignorewarnings") 9 | Proguard / proguardOptions += ProguardOptions.keepMain("Test") 10 | 11 | Proguard / proguardInputs := (Compile / dependencyClasspath).value.files 12 | 13 | Proguard / proguardFilteredInputs ++= ProguardOptions.noFilter((Compile / packageBin).value) 14 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/filter/build.sbt: -------------------------------------------------------------------------------- 1 | import java.nio.file.FileSystems 2 | 3 | enablePlugins(SbtProguard) 4 | 5 | scalaVersion := "2.13.6" 6 | name := "filter" 7 | 8 | (Proguard / proguardMerge) := true 9 | (Proguard / proguardOptions) ++= Seq("-dontoptimize", "-dontnote", "-dontwarn", "-ignorewarnings") 10 | (Proguard / proguardOptions) += ProguardOptions.keepMain("Test") 11 | (Proguard / proguardInputFilter) := { file => 12 | if (file.name == s"scala-library-${scalaVersion.value}.jar") 13 | Some("!META-INF/**") 14 | else 15 | None 16 | } 17 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/scala3/build.sbt: -------------------------------------------------------------------------------- 1 | import java.nio.file.FileSystems 2 | 3 | enablePlugins(SbtProguard) 4 | 5 | scalaVersion := "3.6.3" 6 | name := "scala3" 7 | 8 | Proguard / proguardOptions ++= Seq("-dontoptimize", "-dontnote", "-dontwarn", "-ignorewarnings") 9 | Proguard / proguardOptions += ProguardOptions.keepMain("Test") 10 | Proguard / proguardOptions += ProguardOptions.mappingsFile("mappings.txt") 11 | 12 | Proguard / proguardInputs := (Compile / dependencyClasspath).value.files 13 | 14 | Proguard / proguardFilteredInputs ++= ProguardOptions.noFilter((Compile / packageBin).value) 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: ["v*"] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: Setup JDK 12 | uses: actions/setup-java@v4 13 | with: 14 | distribution: "zulu" 15 | java-version: 8 16 | cache: sbt 17 | - uses: sbt/setup-sbt@v1 18 | - name: Release 19 | run: sbt ci-release 20 | env: 21 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 22 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 23 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 24 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | build_scala2_12: 7 | runs-on: ubuntu-latest 8 | env: 9 | # define Java options for both official sbt and sbt-extras 10 | JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 11 | JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Setup JDK 16 | uses: actions/setup-java@v4 17 | with: 18 | distribution: "zulu" 19 | java-version: 8 20 | cache: sbt 21 | - uses: sbt/setup-sbt@v1 22 | - name: Build and test 23 | run: sbt -v clean +test +scripted 24 | -------------------------------------------------------------------------------- /src/sbt-test/proguard/scala3/test.sbt: -------------------------------------------------------------------------------- 1 | import java.nio.file.{Files, FileSystems} 2 | import collection.JavaConverters._ 3 | import scala.sys.process.Process 4 | 5 | // for sbt scripted test: 6 | TaskKey[Unit]("check") := { 7 | val expected = "test\n" 8 | val proguardResultJar = (Proguard / proguard).value.head 9 | val output = Process("java", Seq("-jar", proguardResultJar.absString)).!! 10 | .replaceAllLiterally("\r\n", "\n") 11 | if (output != expected) sys.error("Unexpected output:\n" + output) 12 | 13 | // older java releases (e.g. java 11) requires a classloader parameter, which may be null... 14 | // for later java release this isn't required any more 15 | val zipFs = FileSystems.newFileSystem(proguardResultJar.toPath, null: ClassLoader) 16 | val jarEntries = zipFs.getRootDirectories.asScala 17 | .flatMap(Files.walk(_).iterator.asScala) 18 | .toSeq 19 | zipFs.close() 20 | 21 | val obfuscateMeEntries = jarEntries.filter(_.toString.contains("ObfuscateMe")) 22 | assert( 23 | obfuscateMeEntries.isEmpty, 24 | s"""class `ObfuscateMe` should be obfuscated and not appear in the proguard output jar, 25 | |neither the class file nor the tasty file. However, we found the following: ${obfuscateMeEntries.mkString(",")} 26 | |""".stripMargin 27 | 28 | ) 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/main/scala/com/typesafe/sbt/ProguardKeys.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.sbt 2 | 3 | import com.lightbend.sbt.SbtProguard.autoImport.ProguardOptions.Filtered 4 | import com.lightbend.sbt.proguard.Merge 5 | import com.lightbend.sbt.proguard.Merge.Strategy 6 | import sbt._ 7 | 8 | trait ProguardKeys { 9 | val proguardVersion = settingKey[String]("proguard version") 10 | val proguardDirectory = settingKey[File]("proguard directory") 11 | val proguardConfiguration = settingKey[File]("proguard configuration") 12 | val proguardInputs = taskKey[Seq[File]]("proguard inputs") 13 | val proguardLibraries = taskKey[Seq[File]]("proguard libraries") 14 | val proguardOutputs = taskKey[Seq[File]]("proguard outputs") 15 | val proguardDefaultInputFilter = taskKey[Option[String]]("proguard default input filter") 16 | val proguardInputFilter = taskKey[File => Option[String]]("proguard input filter") 17 | val proguardLibraryFilter = taskKey[File => Option[String]]("proguard library filter") 18 | val proguardOutputFilter = taskKey[File => Option[String]]("proguard output filter") 19 | val proguardFilteredInputs = taskKey[Seq[Filtered]]("proguard filtered inputs") 20 | val proguardFilteredLibraries = taskKey[Seq[Filtered]]("proguard filtered libraries") 21 | val proguardFilteredOutputs = taskKey[Seq[Filtered]]("proguard filtered outputs") 22 | val proguardMerge = taskKey[Boolean]("proguard merge") 23 | val proguardMergeDirectory = settingKey[File]("proguard merge directory") 24 | val proguardMergeStrategies = taskKey[Seq[Strategy]]("proguard merge strategies") 25 | val proguardMergedInputs = taskKey[Seq[Filtered]]("proguard merged inputs") 26 | val proguardOptions = taskKey[Seq[String]]("proguard options") 27 | val proguard = taskKey[Seq[File]]("proguard") 28 | 29 | object ProguardOptions { 30 | 31 | case class Filtered(file: File, filter: Option[String]) 32 | 33 | def noFilter(jar: File): Seq[Filtered] = Seq(Filtered(jar, None)) 34 | 35 | def noFilter(jars: Seq[File]): Seq[Filtered] = filtered(jars, None) 36 | 37 | def filtered(jars: Seq[File], filter: File => Option[String]): Seq[Filtered] = { 38 | jars map { jar => Filtered(jar, filter(jar)) } 39 | } 40 | 41 | def filtered(jars: Seq[File], filter: Option[String]): Seq[Filtered] = { 42 | jars map { jar => Filtered(jar, filter) } 43 | } 44 | 45 | def filterString(filter: Option[String]): String = { 46 | filter map { 47 | "(" + _ + ")" 48 | } getOrElse "" 49 | } 50 | 51 | def jarOptions(option: String, jars: Seq[Filtered]): Seq[String] = { 52 | jars map { jar => "%s \"%s\"%s" format(option, jar.file.getCanonicalPath, filterString(jar.filter)) } 53 | } 54 | 55 | def keepMain(name: String): String = { 56 | """-keep public class %s { 57 | | public static void main(java.lang.String[]); 58 | |}""".stripMargin.format(name) 59 | } 60 | 61 | def keepMethods(className: String, returnType: String, inputType: String, methodName: String): String = { 62 | s"""-keep public class $className { 63 | | public static $returnType $methodName ($inputType); 64 | |}""".stripMargin 65 | } 66 | 67 | def mappingsFile(fileName: String): String = 68 | s"-printmapping $fileName" 69 | } 70 | 71 | object ProguardMerge { 72 | 73 | import Merge.Strategy.{matchingRegex, matchingString} 74 | 75 | import scala.util.matching.Regex 76 | 77 | def defaultStrategies = Seq( 78 | discard("META-INF/MANIFEST.MF") 79 | ) 80 | 81 | def discard(exactly: String) = matchingString(exactly, Merge.discard) 82 | def first(exactly: String) = matchingString(exactly, Merge.first) 83 | def last(exactly: String) = matchingString(exactly, Merge.last) 84 | def rename(exactly: String) = matchingString(exactly, Merge.rename) 85 | def append(exactly: String) = matchingString(exactly, Merge.append) 86 | 87 | def discard(pattern: Regex) = matchingRegex(pattern, Merge.discard) 88 | def first(pattern: Regex) = matchingRegex(pattern, Merge.first) 89 | def last(pattern: Regex) = matchingRegex(pattern, Merge.last) 90 | def rename(pattern: Regex) = matchingRegex(pattern, Merge.rename) 91 | def append(pattern: Regex) = matchingRegex(pattern, Merge.append) 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sbt-proguard 2 | ============ 3 | 4 | [sbt] plugin for running [ProGuard]. This plugin requires sbt 1.0. 5 | 6 | [![Build Status](https://travis-ci.org/sbt/sbt-proguard.png?branch=master)](https://travis-ci.org/sbt/sbt-proguard) 7 | 8 | 9 | Add plugin 10 | ---------- 11 | 12 | Add plugin to `project/plugins.sbt`. For example: 13 | 14 | ```scala 15 | addSbtPlugin("com.github.sbt" % "sbt-proguard" % "{version}") 16 | ``` 17 | 18 | See [released versions][releases]. 19 | 20 | **Note**: earlier versions of sbt-proguard used the `"com.typesafe.sbt"` or `"com.lightbend.sbt"` organization. 21 | 22 | Example 23 | ------- 24 | 25 | A simple `build.sbt` with settings to configure sbt-proguard: 26 | 27 | ```scala 28 | enablePlugins(SbtProguard) 29 | Proguard/proguardOptions ++= Seq("-dontnote", "-dontwarn", "-ignorewarnings") 30 | Proguard/proguardOptions += ProguardOptions.keepMain("some.MainClass") 31 | Proguard/proguardOptions += ProguardOptions.mappingsFile("mappings.txt")` 32 | ``` 33 | 34 | Run proguard at the sbt shell with: 35 | 36 | ```shell 37 | proguard 38 | ``` 39 | 40 | Specifying the proguard version 41 | ------- 42 | In your `build.sbt`: 43 | ```scala 44 | Proguard/proguardVersion := "7.6.1" 45 | ``` 46 | 47 | Available proguard versions: https://github.com/Guardsquare/proguard/releases 48 | 49 | Filters 50 | ------- 51 | 52 | Proguard supports file filtering for inputs, libraries, and outputs. In 53 | sbt-proguard there are `File => Option[String]` settings for adding filters to 54 | files. 55 | 56 | For example, to add a `!META-INF/**` filter to just the scala-library jar: 57 | 58 | ```scala 59 | Proguard/proguardInputFilter := { file => 60 | file.name match { 61 | case "scala-library.jar" => Some("!META-INF/**") 62 | case _ => None 63 | } 64 | } 65 | ``` 66 | 67 | which will create the following proguard configuration: 68 | 69 | ``` 70 | -injars "/path/to/scala-library.jar"(!META-INF/**) 71 | ``` 72 | 73 | There are corresponding settings for libraries and outputs: `proguardLibraryFilter` and 74 | `proguardOutputFilter`. 75 | 76 | For more advanced usage the `proguardFilteredInputs`, `proguardFilteredLibraries`, and 77 | `proguardFilteredOutputs` settings can be set directly. 78 | 79 | 80 | Merging 81 | ------- 82 | 83 | If the same path exists in multiple inputs then proguard will throw an error. 84 | The conflicting paths can be resolved using file filters, as described above, 85 | but this is not always the most useful approach. For example, `reference.conf` 86 | files for the Typesafe Config library need to be retained and not discarded. 87 | 88 | The sbt-proguard plugin supports pre-merging inputs, similar to creating an 89 | assembly jar first. To enable this merging use: 90 | 91 | ```scala 92 | Proguard/proguardMerge := true 93 | ``` 94 | 95 | Conflicting paths that are not identical will now fail at the merge stage. These 96 | conflicting paths can have merge strategies applied, similar to the [sbt-assembly] 97 | plugin. 98 | 99 | Helper methods for creating common merges are available. These are: 100 | 101 | - `discard` -- discard all matching entries 102 | - `first` -- only keep the first entry 103 | - `last` -- only keep the last entry 104 | - `rename` -- rename entries adding the name of the source 105 | - `append` -- append entries together into one file 106 | 107 | The paths matched against in these helpers are normalised to be separated by `/` 108 | regardless of platform. Paths can be matched exactly with a string or with a 109 | regular expression. 110 | 111 | The default strategy is to only discard `META-INF/MANIFEST.MF`. This same 112 | strategy could be added with: 113 | 114 | ```scala 115 | Proguard/proguardMergeStrategies += ProguardMerge.discard("META-INF/MANIFEST.MF") 116 | ``` 117 | 118 | Or all `META-INF` contents could be discarded with a regular expression: 119 | 120 | ```scala 121 | Proguard/proguardMergeStrategies += ProguardMerge.discard("META-INF/.*".r) 122 | ``` 123 | 124 | To concatenate all `reference.conf` files together use: 125 | 126 | ```scala 127 | Proguard/proguardMergeStrategies += ProguardMerge.append("reference.conf") 128 | ``` 129 | 130 | To discard all `.html` and `.txt` files you may use two strategies together: 131 | 132 | ```scala 133 | Proguard/proguardMergeStrategies ++= Seq( 134 | ProguardMerge.discard("\\.html$".r), 135 | ProguardMerge.discard("\\.txt$".r) 136 | ) 137 | ``` 138 | 139 | Completely custom merge strategies can also be created. See the plugin source 140 | code for how this could be done. 141 | 142 | Scala 3 143 | --------------- 144 | ProGuard doesn't handle Scala 3's TASTy files, which contain much more information than Java's class files. Therefore, we need to post-process the ProGuard output JAR and remove all TASTy files for classes that have been obfuscated. To determine which classes have been obfuscated, you must configure the `-mappingsfile` option, e.g., via `Proguard/proguardOptions += ProguardOptions.mappingsFile("mappings.txt")`. See the [Scala 3 test project](src/sbt-test/proguard/scala3), which is included in the scripted tests. 145 | 146 | 147 | Sample projects 148 | --------------- 149 | 150 | There are some [runnable sample projects][samples] included as sbt scripted tests. 151 | 152 | 153 | Contribution policy 154 | ------------------- 155 | 156 | Contributions via GitHub pull requests are gladly accepted from their original 157 | author. Before we can accept pull requests, you will need to agree to the 158 | [Lightbend Contributor License Agreement][cla] online, using your GitHub account. 159 | 160 | 161 | License 162 | ------- 163 | 164 | [ProGuard] is licensed under the [GNU General Public License][gpl]. sbt and sbt scripts 165 | are included in a [special exception][except] to the GPL licensing. 166 | 167 | The code for this sbt plugin is licensed under the [Apache 2.0 License][apache]. 168 | 169 | 170 | [sbt]: https://github.com/sbt/sbt 171 | [ProGuard]: https://www.guardsquare.com/en/proguard 172 | [releases]: https://github.com/sbt/sbt-proguard/releases 173 | [sbt-assembly]: https://github.com/sbt/sbt-assembly 174 | [samples]: https://github.com/sbt/sbt-proguard/tree/master/src/sbt-test/proguard 175 | [cla]: https://www.lightbend.com/contribute/cla 176 | [gpl]: http://www.gnu.org/licenses/gpl.html 177 | [except]: http://proguard.sourceforge.net/GPL_exception.html 178 | [apache]: http://www.apache.org/licenses/LICENSE-2.0.html 179 | -------------------------------------------------------------------------------- /src/main/scala/com/typesafe/sbt/proguard/Merge.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.sbt.proguard 2 | 3 | import java.io.File 4 | import java.nio.file.Files 5 | import java.nio.file.Path 6 | import java.util.regex.Pattern 7 | 8 | import sbt._ 9 | import scala.util.matching.Regex 10 | import sbt.io.Path._ 11 | 12 | object Merge { 13 | 14 | object EntryPath { 15 | val pattern = Pattern.compile(if (File.separator == "\\") "\\\\" else File.separator) 16 | 17 | def matches(s: String)(p: EntryPath) = p.matches(s) 18 | def matches(r: Regex)(p: EntryPath) = p.matches(r) 19 | } 20 | 21 | case class EntryPath(path: String, isDirectory: Boolean) { 22 | val list = EntryPath.pattern.split(path).toList 23 | val name = if (list.isEmpty) "" else list.last 24 | val normalised = list.mkString("/") + (if (isDirectory) "/" else "") 25 | 26 | def file(base: File) = base / path 27 | def matches(s: String) = s == normalised 28 | def matches(r: Regex) = r.findFirstIn(normalised).isDefined 29 | 30 | override def toString = normalised 31 | } 32 | 33 | object Entry { 34 | def apply(path: String, file: File, source: File): Entry = 35 | Entry(EntryPath(path, file.isDirectory), file, source) 36 | } 37 | 38 | case class Entry(path: EntryPath, file: File, source: File) 39 | 40 | def entries(sources: Seq[File], tmp: File): Seq[Entry] = { 41 | sources flatMap { source => 42 | val base = if (isArchive(source.toPath)) { 43 | val path = 44 | if (source.getCanonicalPath.indexOf(":") > 0) 45 | source.getCanonicalPath.substring(source.getCanonicalPath.indexOf("\\") + 1, 46 | source.getCanonicalPath.length) 47 | else 48 | source.getCanonicalPath 49 | val unzipped = tmp / path 50 | IO.unzip(source, unzipped) 51 | unzipped 52 | } else source 53 | (base.allPaths --- base).get pair relativeTo(base) map { p => Entry(p._2, p._1, source) } 54 | } 55 | } 56 | 57 | // copy-pasted from Zinc instead of using internal code 58 | private def isArchive(file: Path): Boolean = 59 | Files.isRegularFile(file) && isArchiveName(file.getFileName.toString) 60 | 61 | private def isArchiveName(fileName: String) = fileName.endsWith(".jar") || fileName.endsWith(".zip") 62 | 63 | trait Strategy { 64 | def claims(path: EntryPath): Boolean 65 | def merge(path: EntryPath, entries: Seq[Entry], target: File, log: Logger): Unit 66 | } 67 | 68 | object Strategy { 69 | type RunMerge = (EntryPath, Seq[Entry], File, Logger) => Unit 70 | 71 | val deduplicate: Strategy = create(_ => true, Merge.deduplicate) 72 | 73 | def matchingString(string: String, run: RunMerge): Strategy = create(EntryPath.matches(string), run) 74 | def matchingRegex(regex: Regex, run: RunMerge): Strategy = create(EntryPath.matches(regex), run) 75 | 76 | def create(claim: EntryPath => Boolean, run: RunMerge): Strategy = new Strategy { 77 | def claims(path: EntryPath): Boolean = claim(path) 78 | def merge(path: EntryPath, entries: Seq[Entry], target: File, log: Logger): Unit = run(path, entries, target, log) 79 | } 80 | } 81 | 82 | def merge(sources: Seq[File], target: File, strategies: Seq[Strategy], log: Logger): Unit = { 83 | IO.withTemporaryDirectory { tmp => 84 | var failed = false 85 | val groupedEntries = entries(sources, tmp) groupBy (_.path) 86 | for ((path, entries) <- groupedEntries) { 87 | val strategy = strategies find { 88 | _.claims(path) 89 | } getOrElse Strategy.deduplicate 90 | try { 91 | strategy.merge(path, entries, target, log) 92 | } catch { 93 | case e: Exception => 94 | log.error(e.getMessage) 95 | failed = true 96 | } 97 | } 98 | if (failed) { 99 | sys.error("Failed to merge all inputs. Merge strategies can be used to resolve conflicts.") 100 | IO.delete(target) 101 | } 102 | } 103 | } 104 | 105 | def deduplicate(path: EntryPath, entries: Seq[Entry], target: File, log: Logger): Unit = { 106 | if (entries.size > 1) { 107 | if (path.isDirectory) { 108 | log.debug("Ignoring duplicate directories at '%s'" format path) 109 | path.file(target).mkdirs 110 | } else { 111 | entries foreach { e => log.debug("Matching entry at '%s' from %s" format(e.path, e.source.name)) } 112 | val hashes = (entries map { 113 | _.file.hashString 114 | }).toSet 115 | if (hashes.size <= 1) { 116 | log.debug("Identical duplicates found at '%s'" format path) 117 | copyFirst(entries, target, Some(log)) 118 | } else { 119 | sys.error("Multiple entries found at '%s'" format path) 120 | } 121 | } 122 | } else { 123 | if (path.isDirectory) path.file(target).mkdirs 124 | else copyFirst(entries, target) 125 | } 126 | } 127 | 128 | def copyFirst(entries: Seq[Entry], target: File, log: Option[Logger] = None): Unit = { 129 | entries.headOption foreach copyOne("first", target, log) 130 | } 131 | 132 | def copyLast(entries: Seq[Entry], target: File, log: Option[Logger] = None): Unit = { 133 | entries.lastOption foreach copyOne("last", target, log) 134 | } 135 | 136 | def copyOne(label: String, target: File, log: Option[Logger] = None)(entry: Entry): Unit = { 137 | log foreach { l => l.debug("Keeping %s entry at '%s' from %s" format(label, entry.path, entry.source.name)) } 138 | IO.copyFile(entry.file, entry.path.file(target)) 139 | } 140 | 141 | def discard(path: EntryPath, entries: Seq[Entry], target: File, log: Logger): Unit = { 142 | entries foreach { e => log.debug("Discarding entry at '%s' from %s" format(e.path, e.source.name)) } 143 | } 144 | 145 | def first(path: EntryPath, entries: Seq[Entry], target: File, log: Logger): Unit = { 146 | if (path.isDirectory) path.file(target).mkdirs 147 | else copyFirst(entries, target, Some(log)) 148 | } 149 | 150 | def last(path: EntryPath, entries: Seq[Entry], target: File, log: Logger): Unit = { 151 | if (path.isDirectory) path.file(target).mkdirs 152 | else copyLast(entries, target, Some(log)) 153 | } 154 | 155 | def rename(path: EntryPath, entries: Seq[Entry], target: File, log: Logger): Unit = { 156 | if (path.isDirectory) sys.error("Rename of directory entry at '%s' is not supported" format path) 157 | for (entry <- entries) { 158 | val file = path.file(target) 159 | val renamed = new File(file.getParentFile, file.name + "-" + entry.source.name) 160 | log.debug("Renaming entry at '%s' to '%s'" format(path, renamed.name)) 161 | IO.copyFile(entry.file, renamed) 162 | } 163 | } 164 | 165 | def append(path: EntryPath, entries: Seq[Entry], target: File, log: Logger): Unit = { 166 | if (path.isDirectory) sys.error("Append of directory entry at '%s' is not supported" format path) 167 | for (entry <- entries) { 168 | val file = path.file(target) 169 | log.debug("Appending entry at '%s' from '%s'" format(path, entry.source.name)) 170 | IO.append(file, IO.readBytes(entry.file)) 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/main/scala/com/typesafe/sbt/SbtProguard.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.sbt 2 | 3 | import com.lightbend.sbt.proguard.Merge 4 | import java.nio.file.{Files, FileSystems} 5 | import sbt._ 6 | import sbt.Keys._ 7 | import sbt.internal.util.ManagedLogger 8 | import scala.collection.JavaConverters._ 9 | import scala.sys.process.Process 10 | 11 | object SbtProguard extends AutoPlugin { 12 | 13 | object autoImport extends ProguardKeys { 14 | lazy val Proguard: Configuration = config("proguard").hide 15 | } 16 | 17 | import autoImport._ 18 | import ProguardOptions._ 19 | 20 | override def requires: Plugins = plugins.JvmPlugin 21 | 22 | override def trigger = allRequirements 23 | 24 | override def projectConfigurations: Seq[Configuration] = Seq(Proguard) 25 | 26 | override lazy val projectSettings: Seq[Setting[_]] = inConfig(Proguard)(baseSettings) ++ dependencies 27 | 28 | def baseSettings: Seq[Setting[_]] = Seq( 29 | proguardVersion := "7.0.0", 30 | proguardDirectory := crossTarget.value / "proguard", 31 | proguardConfiguration := proguardDirectory.value / "configuration.pro", 32 | artifactPath := proguardDirectory.value / ( Compile / packageBin / artifactPath).value.getName, 33 | managedClasspath := Classpaths.managedJars(configuration.value, classpathTypes.value, update.value), 34 | proguardInputs := (Runtime/fullClasspath).value.files, 35 | (proguard / javaHome) := Some(FileSystems.getDefault.getPath(System.getProperty("java.home")).toFile), 36 | proguardLibraries := { 37 | val dependencyJars = (Compile / dependencyClasspathAsJars).value.map(_.data) 38 | dependencyJars.filterNot(proguardInputs.value.toSet) ++ (proguard / javaHome).value 39 | }, 40 | proguardOutputs := Seq(artifactPath.value), 41 | proguardDefaultInputFilter := Some("!META-INF/MANIFEST.MF"), 42 | proguardInputFilter := { 43 | val defaultInputFilterValue = proguardDefaultInputFilter.value 44 | _ => defaultInputFilterValue 45 | }, 46 | proguardLibraryFilter := { _ => None }, 47 | proguardOutputFilter := { _ => None }, 48 | proguardFilteredInputs := filtered(proguardInputs.value, proguardInputFilter.value), 49 | proguardFilteredLibraries := filtered(proguardLibraries.value, proguardLibraryFilter.value), 50 | proguardFilteredOutputs := filtered(proguardOutputs.value, proguardOutputFilter.value), 51 | proguardMerge := false, 52 | proguardMergeDirectory := proguardDirectory.value / "merged", 53 | proguardMergeStrategies := ProguardMerge.defaultStrategies, 54 | proguardMergedInputs := mergeTask.value, 55 | proguardOptions := { 56 | jarOptions("-injars", proguardMergedInputs.value) ++ 57 | jarOptions("-libraryjars", proguardFilteredLibraries.value) ++ 58 | jarOptions("-outjars", proguardFilteredOutputs.value) 59 | }, 60 | proguard / javaOptions := Seq("-Xmx256M"), 61 | autoImport.proguard := proguardTask.value 62 | ) 63 | 64 | private def groupId(proguardVersionStr: String): String = 65 | "^(\\d+)\\.".r 66 | .findFirstMatchIn(proguardVersionStr) 67 | .map(_.group(1).toInt) match { 68 | case Some(v) => if (v > 6) "com.guardsquare" else "net.sf.proguard" 69 | case None => sys.error(s"Can't parse Proguard version: $proguardVersion") 70 | } 71 | 72 | def dependencies: Seq[Setting[_]] = Seq( 73 | libraryDependencies += groupId((Proguard / proguardVersion).value) % "proguard-base" % (Proguard / proguardVersion).value % Proguard 74 | ) 75 | 76 | lazy val mergeTask: Def.Initialize[Task[Seq[ProguardOptions.Filtered]]] = Def.task { 77 | val streamsValue = streams.value 78 | val mergeDirectoryValue = proguardMergeDirectory.value 79 | val mergeStrategiesValue = proguardMergeStrategies.value 80 | val filteredInputsValue = proguardFilteredInputs.value 81 | if (!proguardMerge.value) filteredInputsValue 82 | else { 83 | val cachedMerge = FileFunction.cached(streamsValue.cacheDirectory / "proguard-merge", FilesInfo.hash) { _ => 84 | streamsValue.log.info("Merging inputs before proguard...") 85 | IO.delete(mergeDirectoryValue) 86 | IO.createDirectory(mergeDirectoryValue) 87 | val inputs = filteredInputsValue map (_.file) 88 | Merge.merge(inputs, mergeDirectoryValue, mergeStrategiesValue.reverse, streamsValue.log) 89 | mergeDirectoryValue.allPaths.get.toSet 90 | } 91 | val inputs = inputFiles(filteredInputsValue).toSet 92 | cachedMerge(inputs) 93 | val filters = (filteredInputsValue flatMap (_.filter)).toSet 94 | val combinedFilter = if (filters.nonEmpty) Some(filters.mkString(",")) else None 95 | Seq(Filtered(mergeDirectoryValue, combinedFilter)) 96 | } 97 | } 98 | 99 | lazy val proguardTask: Def.Initialize[Task[Seq[File]]] = Def.task { 100 | writeConfiguration(proguardConfiguration.value, proguardOptions.value) 101 | val proguardConfigurationValue = proguardConfiguration.value 102 | val javaOptionsInProguardValue = (proguard / javaOptions).value 103 | val managedClasspathValue = managedClasspath.value 104 | val streamsValue = streams.value 105 | val outputsValue = proguardOutputs.value 106 | val proguardOutputJar = (Proguard/proguardOutputs).value.head 107 | val cachedProguard = FileFunction.cached(streams.value.cacheDirectory / "proguard", FilesInfo.hash) { _ => 108 | outputsValue foreach IO.delete 109 | streamsValue.log.debug("Proguard configuration:") 110 | proguardOptions.value foreach (streamsValue.log.debug(_)) 111 | runProguard(proguardConfigurationValue, javaOptionsInProguardValue, managedClasspathValue.files, streamsValue.log) 112 | 113 | if (scalaBinaryVersion.value == "3") { 114 | streamsValue.log.info("This is a Scala 3 build - will now remove the TASTy files from the ProGuard outputs") 115 | val mappingsFile = findMappingsFileConfig( 116 | options = (Proguard/proguardOptions).value, 117 | baseDir = (Proguard/proguardDirectory).value 118 | ).getOrElse(throw new AssertionError( 119 | """mappings file not found in proguardOptions. Please configure it using e.g. `-printmapping mapings.txt` 120 | | - it must be configured for a Scala 3 build so we can remove the TASTy files for obfuscated classes""".stripMargin 121 | )) 122 | removeTastyFilesForObfuscatedClasses( 123 | mappingsFile, 124 | proguardOutputJar = proguardOutputJar, 125 | logger = streamsValue.log 126 | ) 127 | } 128 | 129 | outputsValue.toSet 130 | } 131 | val inputs = (proguardConfiguration.value +: inputFiles(proguardFilteredInputs.value)).toSet 132 | cachedProguard(inputs) 133 | proguardOutputs.value 134 | } 135 | 136 | def inputFiles(inputs: Seq[Filtered]): Seq[File] = 137 | inputs flatMap { i => if (i.file.isDirectory) i.file.allPaths.get else Seq(i.file) } 138 | 139 | def writeConfiguration(config: File, options: Seq[String]): Unit = 140 | IO.writeLines(config, options) 141 | 142 | def runProguard(config: File, javaOptions: Seq[String], classpath: Seq[File], log: Logger): Unit = { 143 | require(classpath.nonEmpty, "Proguard classpath cannot be empty!") 144 | val options = javaOptions ++ Seq("-cp", Path.makeString(classpath), "proguard.ProGuard", "-include", config.getAbsolutePath) 145 | log.info("Proguard command:") 146 | log.info("java " + options.mkString(" ")) 147 | val exitCode = Process("java", options) ! log 148 | if (exitCode != 0) sys.error("Proguard failed with exit code [%s]" format exitCode) 149 | } 150 | 151 | def removeTastyFilesForObfuscatedClasses(mappingsFile: File, proguardOutputJar: File, logger: ManagedLogger): Unit = { 152 | val obfuscatedClasses = findObfuscatedClasses(mappingsFile) 153 | 154 | if (obfuscatedClasses.nonEmpty) { 155 | logger.info(s"found ${obfuscatedClasses.size} classes that have been obfuscated; will now remove their TASTy files (bar some that are still required), since those contain even more information than the class files") 156 | // note: we must not delete the TASTy files for unobfuscated classes since that would break the REPL 157 | val tastyEntriesForObfuscatedClasses = obfuscatedClasses.map { className => 158 | val zipEntry = "/" + className.replaceAll("\\.", "/") // `/` instead of `.` 159 | val tastyFileConvention = zipEntry.replaceFirst("\\$.*", "") 160 | s"$tastyFileConvention.tasty" 161 | } 162 | 163 | val deletedEntries = deleteFromJar(proguardOutputJar, tastyEntriesForObfuscatedClasses) 164 | logger.info(s"deleted ${deletedEntries.size} TASTy files from $proguardOutputJar") 165 | deletedEntries.foreach(logger.debug(_)) 166 | } 167 | } 168 | 169 | def findMappingsFileConfig(options: Seq[String], baseDir: File): Option[File] = { 170 | options.find(_.startsWith("-printmapping")).flatMap { keyValue => 171 | keyValue.split(" ") match { 172 | case Array(key, value) => Some(value) 173 | case _ => 174 | None 175 | } 176 | }.map { value => 177 | val mappingsFile = file(value) 178 | if (mappingsFile.isAbsolute) mappingsFile 179 | else baseDir / value 180 | } 181 | } 182 | 183 | def findObfuscatedClasses(mappingsFile: File): Set[String] = { 184 | // a typical mapping file entry looks like this: 185 | // `io.joern.x2cpg.passes.linking.filecompat.FileNameCompat -> io.joern.x2cpg.passes.a.a.a:` 186 | val mapping = "(.*) -> (.*):".r 187 | 188 | val classesThatHaveBeenObfuscated = for { 189 | line <- Files.lines(mappingsFile.toPath).iterator().asScala 190 | // the lines ending with `:` list the classname mappings: 191 | if line.endsWith(":") 192 | // extract the original and obfuscated name via regex matching: 193 | mapping(original, obfuscated) = line 194 | // if both sides are identical, this class didn't get obfuscated: 195 | if original != obfuscated 196 | } yield original 197 | 198 | classesThatHaveBeenObfuscated.toSet 199 | } 200 | 201 | /** Deletes all entries from a jar that is in the given set of entry paths. 202 | * Returns the Paths of the deleted entries. */ 203 | def deleteFromJar(jar: File, toDelete: Set[String]): Seq[String] = { 204 | val zipFs = FileSystems.newFileSystem(jar.toPath, null: ClassLoader) 205 | 206 | val deletedEntries = for { 207 | zipRootDir <- zipFs.getRootDirectories.asScala 208 | entry <- Files.walk(zipRootDir).iterator.asScala 209 | if (toDelete.contains(entry.toString)) 210 | } yield { 211 | Files.delete(entry) 212 | entry.toString 213 | } 214 | 215 | zipFs.close() 216 | deletedEntries.toSeq 217 | } 218 | } 219 | --------------------------------------------------------------------------------