mirror of
https://github.com/revanced/revanced-cli.git
synced 2024-11-23 12:06:50 +01:00
feat: Set patch options via CLI (#336)
BREAKING CHANGE: This commit changes various CLI options and removes the `options.json` file. Instead, patch options can now be passed via CLI options
This commit is contained in:
parent
54ae01cd76
commit
23002434b2
@ -13,10 +13,12 @@ java -jar revanced-cli.jar -h
|
|||||||
## 📃 List patches
|
## 📃 List patches
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
java -jar revanced-cli.jar list-patches --with-descriptions --with-packages --with-versions --with-options --with-universal-patches revanced-patches.rvp
|
java -jar revanced-cli.jar list-patches --with-packages --with-versions --with-options revanced-patches.rvp
|
||||||
```
|
```
|
||||||
|
|
||||||
## 💉 Patch an app with the default list of patches
|
## 💉 Patch an app
|
||||||
|
|
||||||
|
To patch an app using the default list of patches, use the `patch` command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
java -jar revanced-cli.jar patch -b revanced-patches.rvp input.apk
|
java -jar revanced-cli.jar patch -b revanced-patches.rvp input.apk
|
||||||
@ -28,22 +30,37 @@ You can also use multiple patch bundles:
|
|||||||
java -jar revanced-cli.jar patch -b revanced-patches.rvp -b another-patches.rvp input.apk
|
java -jar revanced-cli.jar patch -b revanced-patches.rvp -b another-patches.rvp input.apk
|
||||||
```
|
```
|
||||||
|
|
||||||
To manually include or exclude patches, use the options `-i` and `-e`.
|
To change the default set of used patches, use the option `-i` or `-e` to use or disuse specific patches.
|
||||||
Keep in mind the name of the patch must be an exact match.
|
You can use the `list-patches` command to see which patches are used by default.
|
||||||
You can also use the options `--ii` and `--ie` to include or exclude patches by their index
|
|
||||||
if two patches have the same name.
|
To only use specific patches, you can use the option `--exclusive` combined with `-i`.
|
||||||
To know the indices of patches, use the option `--with-indices` when listing patches:
|
Remember that the options `-i` and `-e` match the patch's name exactly. Here is an example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
java -jar revanced-cli.jar list-patches --with-indices revanced-patches.rvp
|
java -jar revanced-cli.jar patch -b revanced-patches.rvp --exclusive -i "Patch name" -i "Another patch name" input.apk
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you can use the indices to include or exclude patches:
|
You can also use the options `--ii` and `--ie` to use or disuse patches by their index.
|
||||||
|
This is useful, if two patches happen to have the same name.
|
||||||
|
To know the indices of patches, use the command `list-patches`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar revanced-cli.jar list-patches revanced-patches.rvp
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can use the indices to use or disuse patches:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
java -jar revanced-cli.jar patch -b revanced-patches.rvp --ii 123 --ie 456 input.apk
|
java -jar revanced-cli.jar patch -b revanced-patches.rvp --ii 123 --ie 456 input.apk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can combine the option `-i`, `-e`, `--ii`, `--ie` and `--exclusive`. Here is an example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar revanced-cli.jar patch -b revanced-patches.rvp --exclusive -i "Patch name" --ii 123 input.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> You can use the option `-d` to automatically install the patched app after patching.
|
> You can use the option `-d` to automatically install the patched app after patching.
|
||||||
> Make sure ADB is working:
|
> Make sure ADB is working:
|
||||||
@ -62,6 +79,60 @@ java -jar revanced-cli.jar patch -b revanced-patches.rvp --ii 123 --ie 456 input
|
|||||||
> adb install input.apk
|
> adb install input.apk
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
|
Patches can have options you can set using the option `-O` alongside the option to include the patch by name or index.
|
||||||
|
To know the options of a patch, use the option `--with-options` when listing patches:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar revanced-cli.jar list-patches --with-options revanced-patches.rvp
|
||||||
|
```
|
||||||
|
|
||||||
|
Each patch can have multiple options. You can set them using the option `-O`.
|
||||||
|
For example, to set the options for the patch with the name `Patch name`
|
||||||
|
with the key `key1` and `key2` to `value1` and `value2` respectively, use the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar revanced-cli.jar patch -b revanced-patches.rvp -i "Patch name" -Okey1=value1 -Okey2=value2 input.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to set a value to `null`, you can omit the value:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar revanced-cli.jar patch -b revanced-patches.rvp -i "Patch name" -Okey1 input.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Option values are usually typed. If you set a value with the wrong type, the patch can fail.
|
||||||
|
> Option value types can be seen when listing patches with the option `--with-options`.
|
||||||
|
>
|
||||||
|
> Example option values:
|
||||||
|
>
|
||||||
|
> - String: `string`
|
||||||
|
> - Boolean: `true`, `false`
|
||||||
|
> - Integer: `123`
|
||||||
|
> - Double: `1.0`
|
||||||
|
> - Float: `1.0f`
|
||||||
|
> - Long: `1234567890`, `1L`
|
||||||
|
> - List: `[item1,item2,item3]`
|
||||||
|
> - List of type `Any`: `[item1,123,true,1.0]`
|
||||||
|
> - Empty list of type `Any`: `[]`
|
||||||
|
> - Typed empty list: `int[]`
|
||||||
|
> - Typed and nested empty list: `[int[]]`
|
||||||
|
> - List with null value and two empty strings: `[null,\'\',\"\"]`
|
||||||
|
>
|
||||||
|
> Quotes and commas escaped in strings (`\"`, `\'`, `\,`) are parsed as part of the string.
|
||||||
|
> List items are recursively parsed, so you can escape values in lists:
|
||||||
|
>
|
||||||
|
> - Escaped integer as a string: `[\'123\']`
|
||||||
|
> - Escaped boolean as a string: `[\'true\']`
|
||||||
|
> - Escaped list as a string: `[\'[item1,item2]\']`
|
||||||
|
> - Escaped null value as a string: `[\'null\']`
|
||||||
|
> - List with an integer, an integer as a string and a string with a comma, and an escaped list: [`123,\'123\',str\,ing`,`\'[]\'`]
|
||||||
|
>
|
||||||
|
> Example command with an escaped integer as a string:
|
||||||
|
>
|
||||||
|
> ```bash
|
||||||
|
> java -jar revanced-cli.jar -b revanced-patches.rvp -i "Patch name" -OstringKey=\'1\' input.apk
|
||||||
|
> ```
|
||||||
## 📦 Install an app manually
|
## 📦 Install an app manually
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
105
src/main/kotlin/app/revanced/cli/command/CommandUtils.kt
Normal file
105
src/main/kotlin/app/revanced/cli/command/CommandUtils.kt
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
package app.revanced.cli.command
|
||||||
|
|
||||||
|
import picocli.CommandLine
|
||||||
|
|
||||||
|
class OptionKeyConverter : CommandLine.ITypeConverter<String> {
|
||||||
|
override fun convert(value: String): String = value
|
||||||
|
}
|
||||||
|
|
||||||
|
class OptionValueConverter : CommandLine.ITypeConverter<Any?> {
|
||||||
|
override fun convert(value: String?): Any? {
|
||||||
|
value ?: return null
|
||||||
|
|
||||||
|
return when {
|
||||||
|
value.startsWith("[") && value.endsWith("]") -> {
|
||||||
|
val innerValue = value.substring(1, value.length - 1)
|
||||||
|
|
||||||
|
buildList {
|
||||||
|
var nestLevel = 0
|
||||||
|
var insideQuote = false
|
||||||
|
var escaped = false
|
||||||
|
|
||||||
|
val item = buildString {
|
||||||
|
for (char in innerValue) {
|
||||||
|
when (char) {
|
||||||
|
'\\' -> {
|
||||||
|
if (escaped || nestLevel != 0) {
|
||||||
|
append(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
escaped = !escaped
|
||||||
|
}
|
||||||
|
|
||||||
|
'"', '\'' -> {
|
||||||
|
if (!escaped) {
|
||||||
|
insideQuote = !insideQuote
|
||||||
|
} else {
|
||||||
|
escaped = false
|
||||||
|
}
|
||||||
|
|
||||||
|
append(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
'[' -> {
|
||||||
|
if (!insideQuote) {
|
||||||
|
nestLevel++
|
||||||
|
}
|
||||||
|
|
||||||
|
append(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
']' -> {
|
||||||
|
if (!insideQuote) {
|
||||||
|
nestLevel--
|
||||||
|
|
||||||
|
if (nestLevel == -1) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
append(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
',' -> if (nestLevel == 0) {
|
||||||
|
if (insideQuote) {
|
||||||
|
append(char)
|
||||||
|
} else {
|
||||||
|
add(convert(toString()))
|
||||||
|
setLength(0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
append(char)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> append(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.isNotEmpty()) {
|
||||||
|
add(convert(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value.startsWith("\"") && value.endsWith("\"") -> value.substring(1, value.length - 1)
|
||||||
|
value.startsWith("'") && value.endsWith("'") -> value.substring(1, value.length - 1)
|
||||||
|
value.endsWith("f") -> value.dropLast(1).toFloat()
|
||||||
|
value.endsWith("L") -> value.dropLast(1).toLong()
|
||||||
|
value.equals("true", ignoreCase = true) -> true
|
||||||
|
value.equals("false", ignoreCase = true) -> false
|
||||||
|
value.toIntOrNull() != null -> value.toInt()
|
||||||
|
value.toLongOrNull() != null -> value.toLong()
|
||||||
|
value.toDoubleOrNull() != null -> value.toDouble()
|
||||||
|
value.toFloatOrNull() != null -> value.toFloat()
|
||||||
|
value == "null" -> null
|
||||||
|
value == "int[]" -> emptyList<Int>()
|
||||||
|
value == "long[]" -> emptyList<Long>()
|
||||||
|
value == "double[]" -> emptyList<Double>()
|
||||||
|
value == "float[]" -> emptyList<Float>()
|
||||||
|
value == "boolean[]" -> emptyList<Boolean>()
|
||||||
|
value == "string[]" -> emptyList<String>()
|
||||||
|
else -> value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
package app.revanced.cli.command
|
package app.revanced.cli.command
|
||||||
|
|
||||||
import app.revanced.library.PackageName
|
import app.revanced.library.PackageName
|
||||||
import app.revanced.library.PatchUtils
|
|
||||||
import app.revanced.library.VersionMap
|
import app.revanced.library.VersionMap
|
||||||
|
import app.revanced.library.mostCommonCompatibleVersions
|
||||||
import app.revanced.patcher.patch.loadPatchesFromJar
|
import app.revanced.patcher.patch.loadPatchesFromJar
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -16,7 +16,7 @@ import java.util.logging.Logger
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
internal class ListCompatibleVersions : Runnable {
|
internal class ListCompatibleVersions : Runnable {
|
||||||
private val logger = Logger.getLogger(ListCompatibleVersions::class.java.name)
|
private val logger = Logger.getLogger(this::class.java.name)
|
||||||
|
|
||||||
@CommandLine.Parameters(
|
@CommandLine.Parameters(
|
||||||
description = ["Paths to patch bundles."],
|
description = ["Paths to patch bundles."],
|
||||||
@ -58,8 +58,7 @@ internal class ListCompatibleVersions : Runnable {
|
|||||||
|
|
||||||
val patches = loadPatchesFromJar(patchBundles)
|
val patches = loadPatchesFromJar(patchBundles)
|
||||||
|
|
||||||
PatchUtils.getMostCommonCompatibleVersions(
|
patches.mostCommonCompatibleVersions(
|
||||||
patches,
|
|
||||||
packageNames,
|
packageNames,
|
||||||
countUnusedPatches,
|
countUnusedPatches,
|
||||||
).entries.joinToString("\n", transform = ::buildString).let(logger::info)
|
).entries.joinToString("\n", transform = ::buildString).let(logger::info)
|
||||||
|
@ -14,7 +14,7 @@ import app.revanced.patcher.patch.Option as PatchOption
|
|||||||
description = ["List patches from supplied patch bundles."],
|
description = ["List patches from supplied patch bundles."],
|
||||||
)
|
)
|
||||||
internal object ListPatchesCommand : Runnable {
|
internal object ListPatchesCommand : Runnable {
|
||||||
private val logger = Logger.getLogger(ListPatchesCommand::class.java.name)
|
private val logger = Logger.getLogger(this::class.java.name)
|
||||||
|
|
||||||
@Parameters(
|
@Parameters(
|
||||||
description = ["Paths to patch bundles."],
|
description = ["Paths to patch bundles."],
|
||||||
@ -95,9 +95,11 @@ internal object ListPatchesCommand : Runnable {
|
|||||||
} ?: append("Key: $key")
|
} ?: append("Key: $key")
|
||||||
|
|
||||||
values?.let { values ->
|
values?.let { values ->
|
||||||
appendLine("\nValid values:")
|
appendLine("\nPossible values:")
|
||||||
append(values.map { "${it.value} (${it.key})" }.joinToString("\n").prependIndent("\t"))
|
append(values.map { "${it.value} (${it.key})" }.joinToString("\n").prependIndent("\t"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
append("\nType: $type")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun IndexedValue<Patch<*>>.buildString() =
|
fun IndexedValue<Patch<*>>.buildString() =
|
||||||
|
@ -34,7 +34,6 @@ private object CLIVersionProvider : IVersionProvider {
|
|||||||
versionProvider = CLIVersionProvider::class,
|
versionProvider = CLIVersionProvider::class,
|
||||||
subcommands = [
|
subcommands = [
|
||||||
PatchCommand::class,
|
PatchCommand::class,
|
||||||
OptionsCommand::class,
|
|
||||||
ListPatchesCommand::class,
|
ListPatchesCommand::class,
|
||||||
ListCompatibleVersions::class,
|
ListCompatibleVersions::class,
|
||||||
UtilityCommand::class,
|
UtilityCommand::class,
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
package app.revanced.cli.command
|
|
||||||
|
|
||||||
import app.revanced.library.Options
|
|
||||||
import app.revanced.library.Options.setOptions
|
|
||||||
import app.revanced.patcher.patch.loadPatchesFromJar
|
|
||||||
import picocli.CommandLine
|
|
||||||
import picocli.CommandLine.Help.Visibility.ALWAYS
|
|
||||||
import java.io.File
|
|
||||||
import java.util.logging.Logger
|
|
||||||
|
|
||||||
@CommandLine.Command(
|
|
||||||
name = "options",
|
|
||||||
description = ["Generate options file from patches."],
|
|
||||||
)
|
|
||||||
internal object OptionsCommand : Runnable {
|
|
||||||
private val logger = Logger.getLogger(OptionsCommand::class.java.name)
|
|
||||||
|
|
||||||
@CommandLine.Parameters(
|
|
||||||
description = ["Paths to patch bundles."],
|
|
||||||
arity = "1..*",
|
|
||||||
)
|
|
||||||
private lateinit var patchBundles: Set<File>
|
|
||||||
|
|
||||||
@CommandLine.Option(
|
|
||||||
names = ["-p", "--path"],
|
|
||||||
description = ["Path to patch options JSON file."],
|
|
||||||
showDefaultValue = ALWAYS,
|
|
||||||
)
|
|
||||||
private var filePath: File = File("options.json")
|
|
||||||
|
|
||||||
@CommandLine.Option(
|
|
||||||
names = ["-o", "--overwrite"],
|
|
||||||
description = ["Overwrite existing options file."],
|
|
||||||
showDefaultValue = ALWAYS,
|
|
||||||
)
|
|
||||||
private var overwrite: Boolean = false
|
|
||||||
|
|
||||||
@CommandLine.Option(
|
|
||||||
names = ["-u", "--update"],
|
|
||||||
description = ["Update existing options by adding missing and removing non-existent options."],
|
|
||||||
showDefaultValue = ALWAYS,
|
|
||||||
)
|
|
||||||
private var update: Boolean = false
|
|
||||||
|
|
||||||
override fun run() =
|
|
||||||
try {
|
|
||||||
loadPatchesFromJar(patchBundles).let { patches ->
|
|
||||||
val exists = filePath.exists()
|
|
||||||
if (!exists || overwrite) {
|
|
||||||
if (exists && update) patches.setOptions(filePath)
|
|
||||||
|
|
||||||
Options.serialize(patches, prettyPrint = true).let(filePath::writeText)
|
|
||||||
} else {
|
|
||||||
throw OptionsFileAlreadyExistsException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: OptionsFileAlreadyExistsException) {
|
|
||||||
logger.severe("Options file already exists, use --overwrite to override it")
|
|
||||||
}
|
|
||||||
|
|
||||||
class OptionsFileAlreadyExistsException : Exception()
|
|
||||||
}
|
|
@ -2,15 +2,15 @@ package app.revanced.cli.command
|
|||||||
|
|
||||||
import app.revanced.library.ApkUtils
|
import app.revanced.library.ApkUtils
|
||||||
import app.revanced.library.ApkUtils.applyTo
|
import app.revanced.library.ApkUtils.applyTo
|
||||||
import app.revanced.library.Options
|
|
||||||
import app.revanced.library.Options.setOptions
|
|
||||||
import app.revanced.library.installation.installer.*
|
import app.revanced.library.installation.installer.*
|
||||||
|
import app.revanced.library.setOptions
|
||||||
import app.revanced.patcher.Patcher
|
import app.revanced.patcher.Patcher
|
||||||
import app.revanced.patcher.PatcherConfig
|
import app.revanced.patcher.PatcherConfig
|
||||||
import app.revanced.patcher.patch.Patch
|
import app.revanced.patcher.patch.Patch
|
||||||
import app.revanced.patcher.patch.loadPatchesFromJar
|
import app.revanced.patcher.patch.loadPatchesFromJar
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import picocli.CommandLine
|
import picocli.CommandLine
|
||||||
|
import picocli.CommandLine.ArgGroup
|
||||||
import picocli.CommandLine.Help.Visibility.ALWAYS
|
import picocli.CommandLine.Help.Visibility.ALWAYS
|
||||||
import picocli.CommandLine.Model.CommandSpec
|
import picocli.CommandLine.Model.CommandSpec
|
||||||
import picocli.CommandLine.Spec
|
import picocli.CommandLine.Spec
|
||||||
@ -24,44 +24,71 @@ import java.util.logging.Logger
|
|||||||
description = ["Patch an APK file."],
|
description = ["Patch an APK file."],
|
||||||
)
|
)
|
||||||
internal object PatchCommand : Runnable {
|
internal object PatchCommand : Runnable {
|
||||||
private val logger = Logger.getLogger(PatchCommand::class.java.name)
|
private val logger = Logger.getLogger(this::class.java.name)
|
||||||
|
|
||||||
@Spec
|
@Spec
|
||||||
lateinit var spec: CommandSpec // injected by picocli
|
private lateinit var spec: CommandSpec
|
||||||
|
|
||||||
private lateinit var apk: File
|
@ArgGroup(multiplicity = "0..*")
|
||||||
|
private lateinit var selection: Set<Selection>
|
||||||
|
|
||||||
private var patchBundles = emptySet<File>()
|
internal class Selection {
|
||||||
|
@ArgGroup(exclusive = false, multiplicity = "1")
|
||||||
|
internal var include: IncludeSelection? = null
|
||||||
|
|
||||||
|
internal class IncludeSelection {
|
||||||
|
@ArgGroup(multiplicity = "1")
|
||||||
|
internal lateinit var selector: IncludeSelector
|
||||||
|
|
||||||
|
internal class IncludeSelector {
|
||||||
@CommandLine.Option(
|
@CommandLine.Option(
|
||||||
names = ["-i", "--include"],
|
names = ["-i", "--include"],
|
||||||
description = ["List of patches to include."],
|
description = ["The name of the patch."],
|
||||||
|
required = true,
|
||||||
)
|
)
|
||||||
private var includedPatches = hashSetOf<String>()
|
internal var name: String? = null
|
||||||
|
|
||||||
@CommandLine.Option(
|
@CommandLine.Option(
|
||||||
names = ["--ii"],
|
names = ["--ii"],
|
||||||
description = ["List of patches to include by their index in relation to the supplied patch bundles."],
|
description = ["The index of the patch in the combined list of all supplied patch bundles."],
|
||||||
|
required = true,
|
||||||
)
|
)
|
||||||
private var includedPatchesByIndex = arrayOf<Int>()
|
internal var index: Int? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-O", "--options"],
|
||||||
|
description = ["The option values keyed by the option keys."],
|
||||||
|
mapFallbackValue = CommandLine.Option.NULL_VALUE,
|
||||||
|
converter = [OptionKeyConverter::class, OptionValueConverter::class],
|
||||||
|
)
|
||||||
|
internal var options = mutableMapOf<String, Any?>()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ArgGroup(exclusive = false, multiplicity = "1")
|
||||||
|
internal var exclude: ExcludeSelection? = null
|
||||||
|
|
||||||
|
internal class ExcludeSelection {
|
||||||
|
@ArgGroup(multiplicity = "1")
|
||||||
|
internal lateinit var selector: ExcludeSelector
|
||||||
|
|
||||||
|
internal class ExcludeSelector {
|
||||||
@CommandLine.Option(
|
@CommandLine.Option(
|
||||||
names = ["-e", "--exclude"],
|
names = ["-e", "--exclude"],
|
||||||
description = ["List of patches to exclude."],
|
description = ["The name of the patch."],
|
||||||
|
required = true,
|
||||||
)
|
)
|
||||||
private var excludedPatches = hashSetOf<String>()
|
internal var name: String? = null
|
||||||
|
|
||||||
@CommandLine.Option(
|
@CommandLine.Option(
|
||||||
names = ["--ei"],
|
names = ["--ie"],
|
||||||
description = ["List of patches to exclude by their index in relation to the supplied patch bundles."],
|
description = ["The index of the patch in the combined list of all supplied patch bundles."],
|
||||||
|
required = true,
|
||||||
)
|
)
|
||||||
private var excludedPatchesByIndex = arrayOf<Int>()
|
internal var index: Int? = null
|
||||||
|
}
|
||||||
@CommandLine.Option(
|
}
|
||||||
names = ["--options"],
|
}
|
||||||
description = ["Path to patch options JSON file."],
|
|
||||||
)
|
|
||||||
private var optionsFile: File? = null
|
|
||||||
|
|
||||||
@CommandLine.Option(
|
@CommandLine.Option(
|
||||||
names = ["--exclusive"],
|
names = ["--exclusive"],
|
||||||
@ -141,7 +168,7 @@ internal object PatchCommand : Runnable {
|
|||||||
|
|
||||||
@CommandLine.Option(
|
@CommandLine.Option(
|
||||||
names = ["-t", "--temporary-files-path"],
|
names = ["-t", "--temporary-files-path"],
|
||||||
description = ["Path to temporary files directory."],
|
description = ["Path to store temporary files."],
|
||||||
)
|
)
|
||||||
private var temporaryFilesPath: File? = null
|
private var temporaryFilesPath: File? = null
|
||||||
|
|
||||||
@ -154,13 +181,6 @@ internal object PatchCommand : Runnable {
|
|||||||
)
|
)
|
||||||
private var purge: Boolean = false
|
private var purge: Boolean = false
|
||||||
|
|
||||||
@CommandLine.Option(
|
|
||||||
names = ["-w", "--warn"],
|
|
||||||
description = ["Warn if a patch can not be found in the supplied patch bundles."],
|
|
||||||
showDefaultValue = ALWAYS,
|
|
||||||
)
|
|
||||||
private var warn: Boolean = false
|
|
||||||
|
|
||||||
@CommandLine.Parameters(
|
@CommandLine.Parameters(
|
||||||
description = ["APK file to be patched."],
|
description = ["APK file to be patched."],
|
||||||
arity = "1..1",
|
arity = "1..1",
|
||||||
@ -176,14 +196,7 @@ internal object PatchCommand : Runnable {
|
|||||||
this.apk = apk
|
this.apk = apk
|
||||||
}
|
}
|
||||||
|
|
||||||
@CommandLine.Option(
|
private lateinit var apk: File
|
||||||
names = ["-m", "--merge"],
|
|
||||||
description = ["One or more DEX files or containers to merge into the APK."],
|
|
||||||
)
|
|
||||||
@Suppress("unused")
|
|
||||||
private fun setIntegrations(integrations: Set<File>) {
|
|
||||||
logger.warning("The --merge option is not used anymore.")
|
|
||||||
}
|
|
||||||
|
|
||||||
@CommandLine.Option(
|
@CommandLine.Option(
|
||||||
names = ["-b", "--patch-bundle"],
|
names = ["-b", "--patch-bundle"],
|
||||||
@ -198,6 +211,8 @@ internal object PatchCommand : Runnable {
|
|||||||
this.patchBundles = patchBundles
|
this.patchBundles = patchBundles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var patchBundles = emptySet<File>()
|
||||||
|
|
||||||
@CommandLine.Option(
|
@CommandLine.Option(
|
||||||
names = ["--custom-aapt2-binary"],
|
names = ["--custom-aapt2-binary"],
|
||||||
description = ["Path to a custom AAPT binary to compile resources with."],
|
description = ["Path to a custom AAPT binary to compile resources with."],
|
||||||
@ -226,11 +241,6 @@ internal object PatchCommand : Runnable {
|
|||||||
"${outputFilePath.nameWithoutExtension}-temporary-files",
|
"${outputFilePath.nameWithoutExtension}-temporary-files",
|
||||||
)
|
)
|
||||||
|
|
||||||
val optionsFile =
|
|
||||||
optionsFile ?: outputFilePath.parentFile.resolve(
|
|
||||||
"${outputFilePath.nameWithoutExtension}-options.json",
|
|
||||||
)
|
|
||||||
|
|
||||||
val keystoreFilePath =
|
val keystoreFilePath =
|
||||||
keystoreFilePath ?: outputFilePath.parentFile
|
keystoreFilePath ?: outputFilePath.parentFile
|
||||||
.resolve("${outputFilePath.nameWithoutExtension}.keystore")
|
.resolve("${outputFilePath.nameWithoutExtension}.keystore")
|
||||||
@ -243,21 +253,10 @@ internal object PatchCommand : Runnable {
|
|||||||
|
|
||||||
val patches = loadPatchesFromJar(patchBundles)
|
val patches = loadPatchesFromJar(patchBundles)
|
||||||
|
|
||||||
// Warn if a patch can not be found in the supplied patch bundles.
|
|
||||||
if (warn) {
|
|
||||||
patches.map { it.name }.toHashSet().let { availableNames ->
|
|
||||||
(includedPatches + excludedPatches).filter { name ->
|
|
||||||
!availableNames.contains(name)
|
|
||||||
}
|
|
||||||
}.let { unknownPatches ->
|
|
||||||
if (unknownPatches.isEmpty()) return@let
|
|
||||||
logger.warning("Unknown input of patches:\n${unknownPatches.joinToString("\n")}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher")
|
val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher")
|
||||||
|
|
||||||
val (packageName, patcherResult) = Patcher(
|
val (packageName, patcherResult) = Patcher(
|
||||||
PatcherConfig(
|
PatcherConfig(
|
||||||
apk,
|
apk,
|
||||||
@ -267,16 +266,20 @@ internal object PatchCommand : Runnable {
|
|||||||
true,
|
true,
|
||||||
),
|
),
|
||||||
).use { patcher ->
|
).use { patcher ->
|
||||||
val filteredPatches =
|
val packageName = patcher.context.packageMetadata.packageName
|
||||||
patcher.filterPatchSelection(patches).also { patches ->
|
val packageVersion = patcher.context.packageMetadata.packageVersion
|
||||||
|
|
||||||
|
val filteredPatches = patches.filterPatchSelection(packageName, packageVersion)
|
||||||
|
|
||||||
logger.info("Setting patch options")
|
logger.info("Setting patch options")
|
||||||
|
|
||||||
if (optionsFile.exists()) {
|
val patchesList = patches.toList()
|
||||||
patches.setOptions(optionsFile)
|
selection.filter { it.include != null }.associate {
|
||||||
} else {
|
val includeSelection = it.include!!
|
||||||
Options.serialize(patches, prettyPrint = true).let(optionsFile::writeText)
|
|
||||||
}
|
(includeSelection.selector.name ?: patchesList[includeSelection.selector.index!!].name!!) to
|
||||||
}
|
includeSelection.options
|
||||||
|
}.let(filteredPatches::setOptions)
|
||||||
|
|
||||||
patcher += filteredPatches
|
patcher += filteredPatches
|
||||||
|
|
||||||
@ -297,7 +300,7 @@ internal object PatchCommand : Runnable {
|
|||||||
patcher.context.packageMetadata.packageName to patcher.get()
|
patcher.context.packageMetadata.packageName to patcher.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Save
|
// region Save.
|
||||||
|
|
||||||
apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).apply {
|
apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).apply {
|
||||||
patcherResult.applyTo(this)
|
patcherResult.applyTo(this)
|
||||||
@ -323,9 +326,9 @@ internal object PatchCommand : Runnable {
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Install
|
// region Install.
|
||||||
|
|
||||||
deviceSerial?.let { it ->
|
deviceSerial?.let {
|
||||||
val deviceSerial = it.ifEmpty { null }
|
val deviceSerial = it.ifEmpty { null }
|
||||||
|
|
||||||
runBlocking {
|
runBlocking {
|
||||||
@ -352,35 +355,44 @@ internal object PatchCommand : Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter the patches to be added to the patcher. The filter is based on the following:
|
* Filter the patches based on the selection.
|
||||||
*
|
*
|
||||||
* @param patches The patches to filter.
|
* @param packageName The package name of the APK file to be patched.
|
||||||
|
* @param packageVersion The version of the APK file to be patched.
|
||||||
* @return The filtered patches.
|
* @return The filtered patches.
|
||||||
*/
|
*/
|
||||||
private fun Patcher.filterPatchSelection(patches: Set<Patch<*>>): Set<Patch<*>> =
|
private fun Set<Patch<*>>.filterPatchSelection(
|
||||||
buildSet {
|
packageName: String,
|
||||||
val packageName = context.packageMetadata.packageName
|
packageVersion: String,
|
||||||
val packageVersion = context.packageMetadata.packageVersion
|
): Set<Patch<*>> = buildSet {
|
||||||
|
val includedPatchesByName =
|
||||||
|
selection.asSequence().mapNotNull { it.include?.selector?.name }.toSet()
|
||||||
|
val includedPatchesByIndex =
|
||||||
|
selection.asSequence().mapNotNull { it.include?.selector?.index }.toSet()
|
||||||
|
|
||||||
patches.withIndex().forEach patch@{ (i, patch) ->
|
val excludedPatches =
|
||||||
|
selection.asSequence().mapNotNull { it.exclude?.selector?.name }.toSet()
|
||||||
|
val excludedPatchesByIndex =
|
||||||
|
selection.asSequence().mapNotNull { it.exclude?.selector?.index }.toSet()
|
||||||
|
|
||||||
|
this@filterPatchSelection.withIndex().forEach patchLoop@{ (i, patch) ->
|
||||||
val patchName = patch.name!!
|
val patchName = patch.name!!
|
||||||
|
|
||||||
val explicitlyExcluded = excludedPatches.contains(patchName) || excludedPatchesByIndex.contains(i)
|
val isManuallyExcluded = patchName in excludedPatches || i in excludedPatchesByIndex
|
||||||
if (explicitlyExcluded) return@patch logger.info("\"$patchName\" excluded manually")
|
if (isManuallyExcluded) return@patchLoop logger.info("\"$patchName\" excluded manually")
|
||||||
|
|
||||||
// Make sure the patch is compatible with the supplied APK files package name and version.
|
// Make sure the patch is compatible with the supplied APK files package name and version.
|
||||||
patch.compatiblePackages?.let { packages ->
|
patch.compatiblePackages?.let { packages ->
|
||||||
packages.singleOrNull { (name, _) -> name == packageName }?.let { (_, versions) ->
|
packages.singleOrNull { (name, _) -> name == packageName }?.let { (_, versions) ->
|
||||||
if (versions?.isEmpty() == true) {
|
if (versions?.isEmpty() == true) {
|
||||||
return@patch logger.warning("\"$patchName\" incompatible with \"$packageName\"")
|
return@patchLoop logger.warning("\"$patchName\" incompatible with \"$packageName\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
val matchesVersion = force ||
|
val matchesVersion =
|
||||||
versions?.let { it.any { version -> version == packageVersion } }
|
force || versions?.let { it.any { version -> version == packageVersion } } ?: true
|
||||||
?: true
|
|
||||||
|
|
||||||
if (!matchesVersion) {
|
if (!matchesVersion) {
|
||||||
return@patch logger.warning(
|
return@patchLoop logger.warning(
|
||||||
"\"$patchName\" incompatible with $packageName $packageVersion " +
|
"\"$patchName\" incompatible with $packageName $packageVersion " +
|
||||||
"but compatible with " +
|
"but compatible with " +
|
||||||
packages.joinToString("; ") { (packageName, versions) ->
|
packages.joinToString("; ") { (packageName, versions) ->
|
||||||
@ -388,7 +400,7 @@ internal object PatchCommand : Runnable {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} ?: return@patch logger.fine(
|
} ?: return@patchLoop logger.fine(
|
||||||
"\"$patchName\" incompatible with $packageName. " +
|
"\"$patchName\" incompatible with $packageName. " +
|
||||||
"It is only compatible with " +
|
"It is only compatible with " +
|
||||||
packages.joinToString(", ") { (name, _) -> name },
|
packages.joinToString(", ") { (name, _) -> name },
|
||||||
@ -397,13 +409,12 @@ internal object PatchCommand : Runnable {
|
|||||||
return@let
|
return@let
|
||||||
} ?: logger.fine("\"$patchName\" has no package constraints")
|
} ?: logger.fine("\"$patchName\" has no package constraints")
|
||||||
|
|
||||||
// If the patch is implicitly used, it will be only included if [exclusive] is false.
|
val isIncluded = !exclusive && patch.use
|
||||||
val implicitlyIncluded = !exclusive && patch.use
|
val isManuallyIncluded = patchName in includedPatchesByName || i in includedPatchesByIndex
|
||||||
// If the patch is explicitly used, it will be included even if [exclusive] is false.
|
|
||||||
val explicitlyIncluded = includedPatches.contains(patchName) || includedPatchesByIndex.contains(i)
|
|
||||||
|
|
||||||
val included = implicitlyIncluded || explicitlyIncluded
|
if (!(isIncluded || isManuallyIncluded)) {
|
||||||
if (!included) return@patch logger.info("\"$patchName\" excluded") // Case 1.
|
return@patchLoop logger.info("\"$patchName\" excluded")
|
||||||
|
}
|
||||||
|
|
||||||
add(patch)
|
add(patch)
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ import java.util.logging.Logger
|
|||||||
description = ["Install an APK file to devices with the supplied ADB device serials"],
|
description = ["Install an APK file to devices with the supplied ADB device serials"],
|
||||||
)
|
)
|
||||||
internal object InstallCommand : Runnable {
|
internal object InstallCommand : Runnable {
|
||||||
private val logger = Logger.getLogger(InstallCommand::class.java.name)
|
private val logger = Logger.getLogger(this::class.java.name)
|
||||||
|
|
||||||
@Parameters(
|
@Parameters(
|
||||||
description = ["ADB device serials. If not supplied, the first connected device will be used."],
|
description = ["ADB device serials. If not supplied, the first connected device will be used."],
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package app.revanced.cli.command.utility
|
package app.revanced.cli.command.utility
|
||||||
|
|
||||||
import app.revanced.library.installation.installer.*
|
import app.revanced.library.installation.installer.AdbInstaller
|
||||||
|
import app.revanced.library.installation.installer.AdbInstallerResult
|
||||||
|
import app.revanced.library.installation.installer.AdbRootInstaller
|
||||||
|
import app.revanced.library.installation.installer.RootInstallerResult
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@ -13,7 +16,7 @@ import java.util.logging.Logger
|
|||||||
description = ["Uninstall a patched app from the devices with the supplied ADB device serials"],
|
description = ["Uninstall a patched app from the devices with the supplied ADB device serials"],
|
||||||
)
|
)
|
||||||
internal object UninstallCommand : Runnable {
|
internal object UninstallCommand : Runnable {
|
||||||
private val logger = Logger.getLogger(UninstallCommand::class.java.name)
|
private val logger = Logger.getLogger(this::class.java.name)
|
||||||
|
|
||||||
@Parameters(
|
@Parameters(
|
||||||
description = ["ADB device serials. If not supplied, the first connected device will be used."],
|
description = ["ADB device serials. If not supplied, the first connected device will be used."],
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
package app.revanced.cli.command
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
|
||||||
|
class OptionValueConverterTest {
|
||||||
|
@Test
|
||||||
|
fun `converts to string`() {
|
||||||
|
"string" convertsTo "string" because "Strings should remain the same"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `converts to null`() {
|
||||||
|
"null" convertsTo null because "null should convert to null"
|
||||||
|
"\"null\"" convertsTo "null" because "Escaped null should convert to a string"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `converts to boolean`() {
|
||||||
|
"true" convertsTo true because "true should convert to a boolean true"
|
||||||
|
"True" convertsTo true because "Casing should not matter"
|
||||||
|
"\"true\"" convertsTo "true" because "Escaped booleans should be converted to strings"
|
||||||
|
"\'True\'" convertsTo "True" because "Casing in escaped booleans should not matter"
|
||||||
|
"tr ue" convertsTo "tr ue" because "Malformed booleans should be converted to strings"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `converts to numbers`() {
|
||||||
|
"1" convertsTo 1 because "Integers should convert to integers"
|
||||||
|
"1.0" convertsTo 1.0 because "Doubles should convert to doubles"
|
||||||
|
"1.0f" convertsTo 1.0f because "The suffix f should convert to a float"
|
||||||
|
Long.MAX_VALUE.toString() convertsTo Long.MAX_VALUE because "Values that are too large for an integer should convert to longs"
|
||||||
|
"1L" convertsTo 1L because "The suffix L should convert to a long"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `converts escaped numbers to string`() {
|
||||||
|
"\"1\"" convertsTo "1" because "Escaped numbers should convert to strings"
|
||||||
|
"\"1.0\"" convertsTo "1.0" because "Escaped doubles should convert to strings"
|
||||||
|
"\"1L\"" convertsTo "1L" because "Escaped longs should convert to strings"
|
||||||
|
"\'1\'" convertsTo "1" because "Single quotes should not be treated as escape symbols"
|
||||||
|
"\'.0\'" convertsTo ".0" because "Single quotes should not be treated as escape symbols"
|
||||||
|
"\'1L\'" convertsTo "1L" because "Single quotes should not be treated as escape symbols"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `trims escape symbols once`() {
|
||||||
|
"\"\"\"1\"\"\"" convertsTo "\"\"1\"\"" because "The escape symbols should be trimmed once"
|
||||||
|
"\'\'\'1\'\'\'" convertsTo "''1''" because "Single quotes should not be treated as escape symbols"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `converts lists`() {
|
||||||
|
"1,2" convertsTo "1,2" because "Lists without square brackets should not be converted to lists"
|
||||||
|
"[1,2" convertsTo "[1,2" because "Invalid lists should not be converted to lists"
|
||||||
|
"\"[1,2]\"" convertsTo "[1,2]" because "Lists with escaped square brackets should not be converted to lists"
|
||||||
|
|
||||||
|
"[]" convertsTo emptyList<Any>() because "Empty untyped lists should convert to empty lists of any"
|
||||||
|
"int[]" convertsTo emptyList<Int>() because "Empty typed lists should convert to lists of the specified type"
|
||||||
|
"[[int[]]]" convertsTo listOf(listOf(emptyList<Int>())) because "Nested typed lists should convert to nested lists of the specified type"
|
||||||
|
"[\"int[]\"]" convertsTo listOf("int[]") because "Lists of escaped empty typed lists should not be converted to lists"
|
||||||
|
|
||||||
|
"[1,2,3]" convertsTo listOf(1, 2, 3) because "Lists of integers should convert to lists of integers"
|
||||||
|
"[[1]]" convertsTo listOf(listOf(1)) because "Nested lists with one element should convert to nested lists"
|
||||||
|
"[[1,2],[3,4]]" convertsTo listOf(listOf(1, 2), listOf(3, 4)) because "Nested lists should convert to nested lists"
|
||||||
|
|
||||||
|
"[\"1,2\"]" convertsTo listOf("1,2") because "Values in lists should not be split by commas in strings"
|
||||||
|
"[[\"1,2\"]]" convertsTo listOf(listOf("1,2")) because "Values in nested lists should not be split by commas in strings"
|
||||||
|
|
||||||
|
"[\"\\\"\"]" convertsTo listOf("\"") because "Escaped quotes in strings should be converted to quotes"
|
||||||
|
"[[\"\\\"\"]]" convertsTo listOf(listOf("\"")) because "Escaped quotes in strings nested in lists should be converted to quotes"
|
||||||
|
"[.1,.2f,,true,FALSE]" convertsTo listOf(.1, .2f, "", true, false) because "Values in lists should be converted to the correct type"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val convert = OptionValueConverter()::convert
|
||||||
|
|
||||||
|
private infix fun String.convertsTo(to: Any?) = convert(this) to to
|
||||||
|
private infix fun Pair<Any?, Any?>.because(reason: String) = assert(this.first == this.second) { reason }
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user