feat: Patch Options CLI implementation (#132)

* feat: Patch Options CLI implementation

* fix: remove leftover log message
This commit is contained in:
Sculas 2022-09-08 22:35:09 +02:00 committed by GitHub
parent 649d9bdb2a
commit 3f5345af6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 125 additions and 61 deletions

View File

@ -25,11 +25,12 @@ repositories {
dependencies {
implementation(kotlin("reflect"))
implementation("app.revanced:revanced-patcher:4.2.2")
implementation("app.revanced:revanced-patcher:4.2.3")
implementation("info.picocli:picocli:4.6.3")
implementation("com.android.tools.build:apksig:7.2.1")
implementation("com.github.revanced:jadb:master-SNAPSHOT") // updated fork
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
implementation("cc.ekblad:4koma:1.1.0")
}
tasks {

View File

@ -11,6 +11,7 @@ import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
import app.revanced.patcher.extensions.PatchExtensions.description
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.util.patch.impl.JarPatchBundle
import app.revanced.utils.OptionsLoader
import app.revanced.utils.adb.Adb
import picocli.CommandLine.*
import java.io.File
@ -51,6 +52,9 @@ internal object MainCommand : Runnable {
@Option(names = ["-b", "--bundles"], description = ["One or more bundles of patches"], required = true)
var patchBundles = arrayOf<String>()
@Option(names = ["--options"], description = ["Configuration file for all patch options"])
var options: File = File("options.toml")
@ArgGroup(exclusive = false)
var listingArgs: ListingArgs? = null
@ -123,20 +127,17 @@ internal object MainCommand : Runnable {
}
override fun run() {
if (args.patchArgs?.listingArgs?.listOnly == true) {
printListOfPatches()
return
}
if (args.uninstall) {
uninstall()
return
}
if (args.patchArgs?.listingArgs?.listOnly == true) return printListOfPatches()
if (args.uninstall) return uninstall()
val pArgs = this.args.patchArgs?.patchingArgs ?: return
val outputFile = File(pArgs.outputPath) // the file to write to
// the file to write to
val outputFile = File(pArgs.outputPath)
val allPatches = args.patchArgs!!.patchBundles.flatMap { bundle ->
JarPatchBundle(bundle).loadPatches()
}
OptionsLoader.init(args.patchArgs!!.options, allPatches)
val patcher = app.revanced.patcher.Patcher(
PatcherOptions(
@ -157,7 +158,7 @@ internal object MainCommand : Runnable {
val patchedFile = File(pArgs.cacheDirectory).resolve("${outputFile.nameWithoutExtension}_raw.apk")
// start the patcher
Patcher.start(patcher, patchedFile)
Patcher.start(patcher, patchedFile, allPatches)
val cacheDirectory = File(pArgs.cacheDirectory)

View File

@ -2,6 +2,8 @@ package app.revanced.cli.patcher
import app.revanced.cli.command.MainCommand.args
import app.revanced.cli.command.MainCommand.logger
import app.revanced.patcher.data.Data
import app.revanced.patcher.patch.Patch
import app.revanced.utils.filesystem.ZipFileSystemUtils
import app.revanced.utils.patcher.addPatchesFiltered
import app.revanced.utils.patcher.applyPatchesVerbose
@ -10,14 +12,14 @@ import java.io.File
import java.nio.file.Files
internal object Patcher {
internal fun start(patcher: app.revanced.patcher.Patcher, output: File) {
internal fun start(patcher: app.revanced.patcher.Patcher, output: File, allPatches: List<Class<out Patch<Data>>>) {
val inputFile = args.inputFile
val args = args.patchArgs?.patchingArgs!!
// merge files like necessary integrations
patcher.mergeFiles()
// add patches, but filter incompatible or excluded patches
patcher.addPatchesFiltered()
patcher.addPatchesFiltered(allPatches)
// apply patches
patcher.applyPatchesVerbose()

View File

@ -0,0 +1,62 @@
package app.revanced.utils
import app.revanced.cli.command.MainCommand.logger
import app.revanced.patcher.data.Data
import app.revanced.patcher.extensions.PatchExtensions.options
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
import cc.ekblad.toml.encodeTo
import cc.ekblad.toml.model.TomlValue
import cc.ekblad.toml.serialization.from
import cc.ekblad.toml.tomlMapper
import java.io.File
private typealias PatchList = List<Class<out Patch<Data>>>
private typealias OptionsMap = Map<String, Map<String, Any>>
private const val NULL = "null"
object OptionsLoader {
@JvmStatic
private val mapper = tomlMapper {}
@JvmStatic
fun init(file: File, patches: PatchList) {
if (!file.exists()) file.createNewFile()
val path = file.toPath()
val map = mapper.decodeWithDefaults(
generateDefaults(patches),
TomlValue.from(path)
).also { mapper.encodeTo(path, it) }
readAndSet(map, patches)
}
private fun readAndSet(map: OptionsMap, patches: PatchList) {
for ((patchName, options) in map) {
val patch = patches.find { it.patchName == patchName } ?: continue
val patchOptions = patch.options ?: continue
for ((key, value) in options) {
try {
patchOptions[key] = value.let {
if (it == NULL) null else it
}
} catch (e: Exception) {
logger.warn("Error while setting option $key for patch $patchName: ${e.message}")
e.printStackTrace()
}
}
}
}
private fun generateDefaults(patches: PatchList) = buildMap {
for (patch in patches) {
val options = patch.options ?: continue
if (!options.iterator().hasNext()) continue
put(patch.patchName, buildMap {
for (option in options) {
put(option.key, option.value ?: NULL)
}
})
}
}
}

View File

@ -10,61 +10,59 @@ import app.revanced.patcher.extensions.PatchExtensions.deprecated
import app.revanced.patcher.extensions.PatchExtensions.include
import app.revanced.patcher.extensions.PatchExtensions.patchName
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.util.patch.impl.JarPatchBundle
fun Patcher.addPatchesFiltered() {
fun Patcher.addPatchesFiltered(allPatches: List<Class<out Patch<Data>>>) {
val packageName = this.data.packageMetadata.packageName
val packageVersion = this.data.packageMetadata.packageVersion
args.patchArgs?.patchBundles!!.forEach { bundle ->
val includedPatches = mutableListOf<Class<out Patch<Data>>>()
JarPatchBundle(bundle).loadPatches().forEach patch@{ patch ->
val compatiblePackages = patch.compatiblePackages
val patchName = patch.patchName
val includedPatches = mutableListOf<Class<out Patch<Data>>>()
allPatches.forEach patchLoop@{ patch ->
val compatiblePackages = patch.compatiblePackages
val patchName = patch.patchName
val prefix = "Skipping $patchName, reason"
val prefix = "Skipping $patchName, reason"
val args = MainCommand.args.patchArgs?.patchingArgs!!
val args = MainCommand.args.patchArgs?.patchingArgs!!
if (args.excludedPatches.contains(patchName)) {
logger.info("$prefix: manually excluded")
return@patch
} else if ((!patch.include || args.defaultExclude) && !args.includedPatches.contains(patchName)) {
logger.info("$prefix: excluded by default")
return@patch
}
patch.deprecated?.let { (reason, replacement) ->
logger.warn("$prefix: deprecated: $reason")
if (replacement != null) logger.warn("Either use ${replacement.java.patchName} instead or include it manually")
return@patch
}
if (compatiblePackages == null) logger.warn("$prefix: Missing compatibility annotation. Continuing.")
else {
if (!compatiblePackages.any { it.name == packageName }) {
logger.warn("$prefix: incompatible with $packageName. This patch is only compatible with ${
compatiblePackages.joinToString(
", "
) { it.name }
}")
return@patch
}
if (!(args.experimental || compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == packageVersion } })) {
val compatibleWith = compatiblePackages.joinToString(";") { _package ->
"${_package.name}: ${_package.versions.joinToString(", ")}"
}
logger.warn("$prefix: incompatible with version $packageVersion. This patch is only compatible with version $compatibleWith")
return@patch
}
}
logger.trace("Adding $patchName")
includedPatches.add(patch)
if (args.excludedPatches.contains(patchName)) {
logger.info("$prefix: manually excluded")
return@patchLoop
} else if ((!patch.include || args.defaultExclude) && !args.includedPatches.contains(patchName)) {
logger.info("$prefix: excluded by default")
return@patchLoop
}
this.addPatches(includedPatches)
patch.deprecated?.let { (reason, replacement) ->
logger.warn("$prefix: deprecated: $reason")
if (replacement != null) logger.warn("Either use ${replacement.java.patchName} instead or include it manually")
return@patchLoop
}
if (compatiblePackages == null) logger.warn("$prefix: Missing compatibility annotation. Continuing.")
else {
if (!compatiblePackages.any { it.name == packageName }) {
logger.warn("$prefix: incompatible with $packageName. This patch is only compatible with ${
compatiblePackages.joinToString(
", "
) { it.name }
}")
return@patchLoop
}
if (!(args.experimental || compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == packageVersion } })) {
val compatibleWith = compatiblePackages.joinToString(";") { _package ->
"${_package.name}: ${_package.versions.joinToString(", ")}"
}
logger.warn("$prefix: incompatible with version $packageVersion. This patch is only compatible with version $compatibleWith")
return@patchLoop
}
}
logger.trace("Adding $patchName")
includedPatches.add(patch)
}
this.addPatches(includedPatches)
}
fun Patcher.applyPatchesVerbose() {