Skip to content

Commit 4b00aa5

Browse files
authored
Merge pull request #52 from mpollmeier/scala3/remove-tasty-files
Scala3: remove tasty files
2 parents 95b2778 + 8c187e0 commit 4b00aa5

File tree

8 files changed

+153
-2
lines changed

8 files changed

+153
-2
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ proguardMergeStrategies in Proguard ++= Seq(
146146
Completely custom merge strategies can also be created. See the plugin source
147147
code for how this could be done.
148148

149+
Scala 3
150+
---------------
151+
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.
152+
149153

150154
Sample projects
151155
---------------

src/main/scala/com/typesafe/sbt/ProguardKeys.scala

+3
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ trait ProguardKeys {
6363
| public static $returnType $methodName ($inputType);
6464
|}""".stripMargin
6565
}
66+
67+
def mappingsFile(fileName: String): String =
68+
s"-printmapping $fileName"
6669
}
6770

6871
object ProguardMerge {

src/main/scala/com/typesafe/sbt/SbtProguard.scala

+90-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package com.lightbend.sbt
22

33
import com.lightbend.sbt.proguard.Merge
4-
import sbt.Keys._
4+
import java.nio.file.{Files, FileSystems}
55
import sbt._
6-
import java.nio.file.FileSystems
6+
import sbt.Keys._
7+
import sbt.internal.util.ManagedLogger
8+
import scala.collection.JavaConverters._
79
import scala.sys.process.Process
810

911
object SbtProguard extends AutoPlugin {
@@ -102,11 +104,29 @@ object SbtProguard extends AutoPlugin {
102104
val managedClasspathValue = managedClasspath.value
103105
val streamsValue = streams.value
104106
val outputsValue = proguardOutputs.value
107+
val proguardOutputJar = (Proguard/proguardOutputs).value.head
105108
val cachedProguard = FileFunction.cached(streams.value.cacheDirectory / "proguard", FilesInfo.hash) { _ =>
106109
outputsValue foreach IO.delete
107110
streamsValue.log.debug("Proguard configuration:")
108111
proguardOptions.value foreach (streamsValue.log.debug(_))
109112
runProguard(proguardConfigurationValue, javaOptionsInProguardValue, managedClasspathValue.files, streamsValue.log)
113+
114+
if (scalaBinaryVersion.value == "3") {
115+
streamsValue.log.info("This is a Scala 3 build - will now remove the TASTy files from the ProGuard outputs")
116+
val mappingsFile = findMappingsFileConfig(
117+
options = (Proguard/proguardOptions).value,
118+
baseDir = (Proguard/proguardDirectory).value
119+
).getOrElse(throw new AssertionError(
120+
"""mappings file not found in proguardOptions. Please configure it using e.g. `-printmapping mapings.txt`
121+
| - it must be configured for a Scala 3 build so we can remove the TASTy files for obfuscated classes""".stripMargin
122+
))
123+
removeTastyFilesForObfuscatedClasses(
124+
mappingsFile,
125+
proguardOutputJar = proguardOutputJar,
126+
logger = streamsValue.log
127+
)
128+
}
129+
110130
outputsValue.toSet
111131
}
112132
val inputs = (proguardConfiguration.value +: inputFiles(proguardFilteredInputs.value)).toSet
@@ -128,4 +148,72 @@ object SbtProguard extends AutoPlugin {
128148
val exitCode = Process("java", options) ! log
129149
if (exitCode != 0) sys.error("Proguard failed with exit code [%s]" format exitCode)
130150
}
151+
152+
def removeTastyFilesForObfuscatedClasses(mappingsFile: File, proguardOutputJar: File, logger: ManagedLogger): Unit = {
153+
val obfuscatedClasses = findObfuscatedClasses(mappingsFile)
154+
155+
if (obfuscatedClasses.nonEmpty) {
156+
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")
157+
// note: we must not delete the TASTy files for unobfuscated classes since that would break the REPL
158+
val tastyEntriesForObfuscatedClasses = obfuscatedClasses.map { className =>
159+
val zipEntry = "/" + className.replaceAll("\\.", "/") // `/` instead of `.`
160+
val tastyFileConvention = zipEntry.replaceFirst("\\$.*", "")
161+
s"$tastyFileConvention.tasty"
162+
}
163+
164+
val deletedEntries = deleteFromJar(proguardOutputJar, tastyEntriesForObfuscatedClasses)
165+
logger.info(s"deleted ${deletedEntries.size} TASTy files from $proguardOutputJar")
166+
deletedEntries.foreach(println)
167+
}
168+
}
169+
170+
def findMappingsFileConfig(options: Seq[String], baseDir: File): Option[File] = {
171+
options.find(_.startsWith("-printmapping")).flatMap { keyValue =>
172+
keyValue.split(" ") match {
173+
case Array(key, value) => Some(value)
174+
case _ =>
175+
None
176+
}
177+
}.map { value =>
178+
val mappingsFile = file(value)
179+
if (mappingsFile.isAbsolute) mappingsFile
180+
else baseDir / value
181+
}
182+
}
183+
184+
def findObfuscatedClasses(mappingsFile: File): Set[String] = {
185+
// a typical mapping file entry looks like this:
186+
// `io.joern.x2cpg.passes.linking.filecompat.FileNameCompat -> io.joern.x2cpg.passes.a.a.a:`
187+
val mapping = "(.*) -> (.*):".r
188+
189+
val classesThatHaveBeenObfuscated = for {
190+
line <- Files.lines(mappingsFile.toPath).iterator().asScala
191+
// the lines ending with `:` list the classname mappings:
192+
if line.endsWith(":")
193+
// extract the original and obfuscated name via regex matching:
194+
mapping(original, obfuscated) = line
195+
// if both sides are identical, this class didn't get obfuscated:
196+
if original != obfuscated
197+
} yield original
198+
199+
classesThatHaveBeenObfuscated.toSet
200+
}
201+
202+
/** Deletes all entries from a jar that is in the given set of entry paths.
203+
* Returns the Paths of the deleted entries. */
204+
def deleteFromJar(jar: File, toDelete: Set[String]): Seq[String] = {
205+
val zipFs = FileSystems.newFileSystem(jar.toPath, null: ClassLoader)
206+
207+
val deletedEntries = for {
208+
zipRootDir <- zipFs.getRootDirectories.asScala
209+
entry <- Files.walk(zipRootDir).iterator.asScala
210+
if (toDelete.contains(entry.toString))
211+
} yield {
212+
Files.delete(entry)
213+
entry.toString
214+
}
215+
216+
zipFs.close()
217+
deletedEntries.toSeq
218+
}
131219
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
addSbtPlugin("com.github.sbt" % "sbt-proguard" % sys.props("project.version"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
}

src/sbt-test/proguard/scala3/test

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
> check

src/sbt-test/proguard/scala3/test.sbt

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import java.nio.file.{Files, FileSystems}
2+
import scala.jdk.CollectionConverters.*
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+

0 commit comments

Comments
 (0)