Skip to content

Commit

Permalink
Add new remapper, fix small issues
Browse files Browse the repository at this point in the history
- Fix proguard mappings produced by Google R8 not parsing correctly
- Add new remapper (which can perform tasks in parallel among other things)
- Add removeDuplicates to removeRedundancy, as R8 tends to produce a lot of junk
  • Loading branch information
770grappenmaker committed Jul 29, 2024
1 parent ea85bcd commit 2d73d2f
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 34 deletions.
33 changes: 33 additions & 0 deletions remapper/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
plugins {
alias(libs.plugins.kotlin.jvm)
`application`
}

repositories {
mavenCentral()
}

kotlin {
jvmToolchain(8)
}

dependencies {
implementation(project(":"))
}

application {
mainClass = "com.grappenmaker.mappings.remapper.RemapperKt"
}

open class InstallDistTo : Sync() {
@Option(option = "into", description = "Directory to copy the distribution into")
override fun into(destDir: Any): AbstractCopyTask = super.into(destDir)
}

tasks {
val installDist by getting(Sync::class)
val installDistTo by registering(InstallDistTo::class) {
dependsOn(installDist)
from(installDist.outputs.files.singleFile)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.grappenmaker.mappings.remapper

import com.grappenmaker.mappings.ClasspathLoader
import com.grappenmaker.mappings.ClasspathLoaders
import com.grappenmaker.mappings.ExperimentalJarRemapper
import com.grappenmaker.mappings.MappingsLoader
import com.grappenmaker.mappings.performRemap
import kotlinx.coroutines.runBlocking
import java.util.jar.JarFile
import kotlin.io.path.Path
import kotlin.io.path.absolute
import kotlin.io.path.exists
import kotlin.io.path.readLines
import kotlin.system.exitProcess

private const val parametersSeparator = "--"

@OptIn(ExperimentalJarRemapper::class)
fun main(args: Array<String>) {
println()
val sepIdx = args.indexOf(parametersSeparator)
if (sepIdx < 0) printUsageFatal("error: use a $parametersSeparator before positional parameters")

val options = args.take(sepIdx)
val positionalParameters = args.drop(sepIdx + 1)
if (parametersSeparator in positionalParameters) printUsageFatal("error: multiple $parametersSeparator were found")

val longOptions = linkedSetOf<String>()
val shortOptions = linkedSetOf<Char>()

for (o in options) {
if (o.isEmpty()) continue
if (!o.startsWith('-')) fatalError("error: options have to start with a - character")
if (o.length > 2 && o[1] != '-') fatalError("error: long options have to start with --")
if (o.length == 2) shortOptions += o[1] else longOptions += o.drop(2)
}

fun option(long: String, short: Char) = longOptions.remove(long) || shortOptions.remove(short)

val skipResources = option("skip-resources", 's')
val force = option("force", 'f')
val printStack = option("stacktrace", 'v')

val notFoundOptions = longOptions + shortOptions
if (notFoundOptions.isNotEmpty()) fatalError("unrecognized options: $notFoundOptions")

if (positionalParameters.size < 5) printUsageFatal()
val jars = positionalParameters.drop(5).map { JarFile(it.toPathOrFatal().toFile()) }

runBlocking {
try {
performRemap {
copyResources = !skipResources
mappings = MappingsLoader.loadMappings(positionalParameters[2].toPathOrFatal().readLines())
if (jars.isNotEmpty()) loader = ClasspathLoaders.fromJars(jars)

task(
input = positionalParameters[0].toPathOrFatal(),
output = Path(positionalParameters[1]).absolute().also {
if (!it.parent.exists()) fatalError("error: no such file or directory: $it")
if (it.exists() && !force) fatalError("error: will not overwrite output without --force")
},
fromNamespace = positionalParameters[3],
toNamespace = positionalParameters[4],
)
}
} catch (e: Throwable) {
if (printStack) e.printStackTrace() else fatalError("something went wrong (--stacktrace): ${e.message}")
}
}

jars.forEach { it.close() }
}

private fun printUsageFatal(msg: String? = null): Nothing {
if (msg != null) println(msg)
printUsage()
exitProcess(-1)
}

private fun printUsage() {
println("Usage: [-s | --skip-resources] [-f | --force] [-v | --stacktrace] " +
"-- <input> <output> <mappings> <from> <to> [classpath...]")
}

private fun fatalError(msg: String): Nothing {
println(msg)
exitProcess(-1)
}

private fun String.toPathOrFatal() = Path(this).also { if (!it.exists()) fatalError("error: $this does not exist") }
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}

include("samples")
include("samples", "remapper")
7 changes: 4 additions & 3 deletions src/main/kotlin/com/grappenmaker/mappings/ProguardMappings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,10 @@ public data object ProguardMappingsFormat : MappingsFormat<ProguardMappings> {
val trimmed = line.trim()

if ('(' in trimmed) {
val withoutJunk = trimmed.substringAfterLast(':')
val secondIndex = trimmed.indexOf(':', trimmed.indexOf(':') + 1)
val withoutJunk = if (secondIndex >= 0) trimmed.drop(secondIndex + 1) else trimmed
val (desc, name) = withoutJunk.asMapping()
val (returnName, rest) = desc.split(' ')
val (returnName, rest) = desc.substringBeforeLast(':').split(' ')

val params = rest.substringAfter('(').substringBefore(')').split(",")
.filter { it.isNotEmpty() }.map { it.parseType() }
Expand Down Expand Up @@ -197,7 +198,7 @@ public data object ProguardMappingsFormat : MappingsFormat<ProguardMappings> {

val type = Type.getMethodType(m.desc)
val args = type.argumentTypes.joinToString(",") { it.unparse() }
yield("${indent}1:1:${type.returnType.unparse()} ${m.names[0]}($args) -> ${m.names[1]}")
yield("$indent${type.returnType.unparse()} ${m.names[0]}($args) -> ${m.names[1]}")
}
}
}
Expand Down
75 changes: 45 additions & 30 deletions src/main/kotlin/com/grappenmaker/mappings/Transformations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -426,51 +426,66 @@ private fun <T> Iterable<T>.allIdentical(): Boolean {
* - a method being a data method ([MappedMethod.isData])
* - a method whose names are all identical
*
* If [removeDuplicates] is `true` (`false` by default), this method will also look for methods and fields with the
* same obfuscated/first name and descriptor. This is `false` by default since this might be expensive, and most of the
* time, there will not be any duplicates in the first place, since that could be seen as an invalid mappings file.
*
* @sample samples.Mappings.redundancy
*/
public fun Mappings.removeRedundancy(loader: ClasspathLoader): Mappings =
if (namespaces.isEmpty()) EmptyMappings else GenericMappings(
namespaces,
classes.asSequence().map { oc ->
val name = oc.names.first()
val ourSigs = hashSetOf<String>()
val superSigs = hashSetOf<String>()

walkInheritance(loader, name).forEach { curr ->
val target = if (curr == name) ourSigs else superSigs
val bytes = loader(curr)

if (bytes != null) {
val methods = ClassNode().also { ClassReader(bytes).accept(it, 0) }.methods
val toConsider = if (curr == name) methods
else methods.filter { it.access and Opcodes.ACC_PRIVATE == 0 }

target += toConsider.map { it.name + it.desc }
}
public fun Mappings.removeRedundancy(
loader: ClasspathLoader,
removeDuplicates: Boolean = false,
): Mappings = if (namespaces.isEmpty()) EmptyMappings else GenericMappings(
namespaces,
classes.asSequence().map { oc ->
val name = oc.names.first()
val ourSigs = hashSetOf<String>()
val superSigs = hashSetOf<String>()

walkInheritance(loader, name).forEach { curr ->
val target = if (curr == name) ourSigs else superSigs
val bytes = loader(curr)

if (bytes != null) {
val methods = ClassNode().also { ClassReader(bytes).accept(it, 0) }.methods
val toConsider = if (curr == name) methods
else methods.filter { it.access and Opcodes.ACC_PRIVATE == 0 }

target += toConsider.map { it.name + it.desc }
}
}

oc.copy(
methods = oc.methods.filter {
val sig = it.names.first() + it.desc
sig in ourSigs && sig !in superSigs && !it.isData() && !it.names.allIdentical()
},
fields = oc.fields.filter { !it.names.allIdentical() }
)
}.filterNot { it.methods.isEmpty() && it.fields.isEmpty() && it.names.allIdentical() }.toList()
)
oc.copy(
methods = oc.methods.filter {
val sig = it.names.first() + it.desc
sig in ourSigs && sig !in superSigs && !it.isData() && !it.names.allIdentical()
}.let { m -> if (removeDuplicates) m.distinctBy { it.names.first() + it.desc } else m },
fields = oc.fields.filter { !it.names.allIdentical() }
.let { m -> if (removeDuplicates) m.distinctBy { it.names.first() + it.desc } else m }
)
}.filterNot { it.methods.isEmpty() && it.fields.isEmpty() && it.names.allIdentical() }.toList()
)

/**
* See [removeRedundancy]. [file] is a jar file resource (caller is responsible for closing it) that contains the
* classes that are referenced in the generic overload. Calls with identical names are being cached by this function,
* the caller is not responsible for this.
*
* If [removeDuplicates] is `true` (`false` by default), this method will also look for methods and fields with the
* same obfuscated/first name and descriptor. This is `false` by default since this might be expensive, and most of the
* time, there will not be any duplicates in the first place, since that could be seen as an invalid mappings file.
*
* @sample samples.Mappings.redundancy
*/
public fun Mappings.removeRedundancy(file: JarFile): Mappings = removeRedundancy(
public fun Mappings.removeRedundancy(
file: JarFile,
removeDuplicates: Boolean,
): Mappings = removeRedundancy(
ClasspathLoaders.compound(
ClasspathLoaders.fromJar(file).memoized(),
ClasspathLoaders.fromSystemLoader()
)
),
removeDuplicates
)

/**
Expand Down

0 comments on commit 2d73d2f

Please sign in to comment.