Compare commits

..

23 Commits

Author SHA1 Message Date
semantic-release-bot
3c264572a2 chore(release): 4.4.0 [skip ci]
# [4.4.0](https://github.com/ReVanced/revanced-cli/compare/v4.3.0...v4.4.0) (2023-12-28)

### Bug Fixes

* Add missing punctuation in command description ([8210351](821035107d))

### Features

* Log saved patched APK file path ([16109bd](16109bd8bc))
2023-12-28 21:34:45 +00:00
oSumAtrIX
63e8585652 chore: Merge branch dev to main (#305) 2023-12-28 22:33:09 +01:00
oSumAtrIX
8f59d94dc7 docs: Add building instructions 2023-12-21 15:10:02 +01:00
semantic-release-bot
3bcee04a7d chore(release): 4.4.0-dev.2 [skip ci]
# [4.4.0-dev.2](https://github.com/ReVanced/revanced-cli/compare/v4.4.0-dev.1...v4.4.0-dev.2) (2023-12-18)

### Bug Fixes

* Add missing punctuation in command description ([8210351](821035107d))
2023-12-18 18:47:08 +00:00
oSumAtrIX
821035107d fix: Add missing punctuation in command description 2023-12-18 19:45:35 +01:00
oSumAtrIX
8becebaa42 docs: Fix spelling mistakes 2023-12-12 13:43:36 +01:00
oSumAtrIX
fe563fff93 build: Simplify enabling local build cache 2023-12-10 21:57:12 +01:00
semantic-release-bot
2d17459fa3 chore(release): 4.4.0-dev.1 [skip ci]
# [4.4.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.3.0...v4.4.0-dev.1) (2023-12-01)

### Features

* Log saved patched APK file path ([16109bd](16109bd8bc))
2023-12-01 22:54:26 +00:00
oSumAtrIX
16109bd8bc feat: Log saved patched APK file path 2023-12-01 23:53:17 +01:00
semantic-release-bot
09bc652317 chore(release): 4.3.0 [skip ci]
# [4.3.0](https://github.com/ReVanced/revanced-cli/compare/v4.2.0...v4.3.0) (2023-12-01)

### Features

* Add `list-versions` command ([a974b8e](a974b8ea80))
2023-12-01 22:29:44 +00:00
oSumAtrIX
1d051365f3 chore: Merge branch dev to main (#304) 2023-12-01 01:24:43 +01:00
oSumAtrIX
ab7d9d8e1e build: Bump dependencies 2023-12-01 01:22:06 +01:00
oSumAtrIX
5e089ea9af docs: Update to latest GitHub Markdown syntax changes 2023-12-01 01:12:26 +01:00
semantic-release-bot
06c6a97915 chore(release): 4.3.0-dev.1 [skip ci]
# [4.3.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.2.0...v4.3.0-dev.1) (2023-11-27)

### Features

* Add `list-versions` command ([a974b8e](a974b8ea80))
2023-11-27 22:12:44 +00:00
oSumAtrIX
a974b8ea80 feat: Add list-versions command 2023-11-27 23:11:23 +01:00
semantic-release-bot
89c35ee21b chore(release): 4.2.0 [skip ci]
# [4.2.0](https://github.com/ReVanced/revanced-cli/compare/v4.1.0...v4.2.0) (2023-11-26)

### Bug Fixes

* Fix typo ([#300](https://github.com/ReVanced/revanced-cli/issues/300)) ([9d96bb7](9d96bb7b4c))

### Features

* Allow selecting first Adb device, if none supplied automatically by updating dependencies ([e7c3d64](e7c3d64bf1))
* Exit application with CLI exit code ([36c6a6a](36c6a6a5f7))
* Make `--out´ option optional ([3765957](3765957043))
2023-11-26 05:03:52 +00:00
oSumAtrIX
41bdb04ab0 chore: Merge branch dev to main (#299) 2023-11-26 06:02:21 +01:00
semantic-release-bot
a5542467c8 chore(release): 4.2.0-dev.1 [skip ci]
# [4.2.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.1.1-dev.1...v4.2.0-dev.1) (2023-11-26)

### Features

* Allow selecting first Adb device, if none supplied automatically by updating dependencies ([e7c3d64](e7c3d64bf1))
* Exit application with CLI exit code ([36c6a6a](36c6a6a5f7))
* Make `--out´ option optional ([3765957](3765957043))
2023-11-26 04:58:20 +00:00
oSumAtrIX
5fd205f77d chore: Lint code 2023-11-26 05:56:31 +01:00
oSumAtrIX
e7c3d64bf1 feat: Allow selecting first Adb device, if none supplied automatically by updating dependencies 2023-11-26 05:55:49 +01:00
oSumAtrIX
3765957043 feat: Make `--out´ option optional 2023-11-26 05:27:31 +01:00
oSumAtrIX
5e63e0a276 refactor: Use a newline for annotation properties 2023-11-26 05:27:30 +01:00
oSumAtrIX
36c6a6a5f7 feat: Exit application with CLI exit code 2023-11-26 05:27:30 +01:00
16 changed files with 564 additions and 298 deletions

View File

@@ -1,3 +1,66 @@
# [4.4.0](https://github.com/ReVanced/revanced-cli/compare/v4.3.0...v4.4.0) (2023-12-28)
### Bug Fixes
* Add missing punctuation in command description ([8210351](https://github.com/ReVanced/revanced-cli/commit/821035107d7264580275f395e9e3fcef91394afd))
### Features
* Log saved patched APK file path ([16109bd](https://github.com/ReVanced/revanced-cli/commit/16109bd8bc6236debf71cbc8db78fe452b2ed00d))
# [4.4.0-dev.2](https://github.com/ReVanced/revanced-cli/compare/v4.4.0-dev.1...v4.4.0-dev.2) (2023-12-18)
### Bug Fixes
* Add missing punctuation in command description ([8210351](https://github.com/ReVanced/revanced-cli/commit/821035107d7264580275f395e9e3fcef91394afd))
# [4.4.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.3.0...v4.4.0-dev.1) (2023-12-01)
### Features
* Log saved patched APK file path ([16109bd](https://github.com/ReVanced/revanced-cli/commit/16109bd8bc6236debf71cbc8db78fe452b2ed00d))
# [4.3.0](https://github.com/ReVanced/revanced-cli/compare/v4.2.0...v4.3.0) (2023-12-01)
### Features
* Add `list-versions` command ([a974b8e](https://github.com/ReVanced/revanced-cli/commit/a974b8ea80acd85f8dc472a3f93b8fd7bea08007))
# [4.3.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.2.0...v4.3.0-dev.1) (2023-11-27)
### Features
* Add `list-versions` command ([a974b8e](https://github.com/ReVanced/revanced-cli/commit/a974b8ea80acd85f8dc472a3f93b8fd7bea08007))
# [4.2.0](https://github.com/ReVanced/revanced-cli/compare/v4.1.0...v4.2.0) (2023-11-26)
### Bug Fixes
* Fix typo ([#300](https://github.com/ReVanced/revanced-cli/issues/300)) ([9d96bb7](https://github.com/ReVanced/revanced-cli/commit/9d96bb7b4cc4538da416db50bd689af1a632fc45))
### Features
* Allow selecting first Adb device, if none supplied automatically by updating dependencies ([e7c3d64](https://github.com/ReVanced/revanced-cli/commit/e7c3d64bf15bf84f3853e7ef699511bf72c13767))
* Exit application with CLI exit code ([36c6a6a](https://github.com/ReVanced/revanced-cli/commit/36c6a6a5f75f2e770a7941b3f83f430f62de13de))
* Make `--out´ option optional ([3765957](https://github.com/ReVanced/revanced-cli/commit/3765957043989fe7a8932a0c548566a78d04fc41))
# [4.2.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.1.1-dev.1...v4.2.0-dev.1) (2023-11-26)
### Features
* Allow selecting first Adb device, if none supplied automatically by updating dependencies ([e7c3d64](https://github.com/ReVanced/revanced-cli/commit/e7c3d64bf15bf84f3853e7ef699511bf72c13767))
* Exit application with CLI exit code ([36c6a6a](https://github.com/ReVanced/revanced-cli/commit/36c6a6a5f75f2e770a7941b3f83f430f62de13de))
* Make `--out´ option optional ([3765957](https://github.com/ReVanced/revanced-cli/commit/3765957043989fe7a8932a0c548566a78d04fc41))
## [4.1.1-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.1.0...v4.1.1-dev.1) (2023-11-25) ## [4.1.1-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.1.0...v4.1.1-dev.1) (2023-11-25)

View File

@@ -4,8 +4,8 @@ To use ReVanced CLI, you will need to fulfill specific requirements.
## 🤝 Requirements ## 🤝 Requirements
- Java SDK 11 (Azul Zulu JDK or OpenJDK) - Java Runtime Environment 11 (Azul Zulu JRE or OpenJDK)
- [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) if you want to install the patched APK file on your device - [Android Debug Bridge (ADB)](https://developer.android.com/studio/command-line/adb) if you want to install the patched APK file on your device
- An ABI other than ARMv7 such as x86 or x86-64 (or a custom AAPT binary that supports ARMv7) - An ABI other than ARMv7 such as x86 or x86-64 (or a custom AAPT binary that supports ARMv7)
## ⏭️ Whats next ## ⏭️ Whats next

View File

@@ -35,7 +35,7 @@ ReVanced CLI is divided into the following fundamental commands:
``` ```
> [!NOTE] > [!NOTE]
> A default `options.json` file will be automatically created, if it does not exist > A default `options.json` file will be automatically created if it does not exist
without any need for intervention when using the `patch` command. without any need for intervention when using the `patch` command.
- ### 💉 Patch an app - ### 💉 Patch an app
@@ -73,7 +73,6 @@ ReVanced CLI is divided into the following fundamental commands:
```bash ```bash
java -jar revanced-cli.jar patch \ java -jar revanced-cli.jar patch \
--patch-bundle revanced-patches.jar \ --patch-bundle revanced-patches.jar \
--out patched-app.apk \
--device-serial <device-serial> \ --device-serial <device-serial> \
input.apk input.apk
``` ```
@@ -81,7 +80,7 @@ ReVanced CLI is divided into the following fundamental commands:
- #### 👾 Patch an app and mount it on top of the un-patched app with root permissions - #### 👾 Patch an app and mount it on top of the un-patched app with root permissions
> [!IMPORTANT] > [!IMPORTANT]
> Ensure sure the same app you are patching and mounting over is installed on your device: > Ensure that the same app you are patching and mounting over is installed on your device:
> >
> ```bash > ```bash
> adb install app.apk > adb install app.apk
@@ -92,14 +91,14 @@ ReVanced CLI is divided into the following fundamental commands:
> patches by their index in relation to supplied patch bundles, > patches by their index in relation to supplied patch bundles,
> similarly to the option `--include` and `--exclude`. > similarly to the option `--include` and `--exclude`.
> >
> This is useful in case two patches have the same name, and you need to include or exclude one of them. > This is useful in case two patches have the same name, and you must include or exclude one.
> The index of a patch is calculated by the position of the patch in the list of patches > The patch index is calculated by the position of the patch in the list of patches
> from patch bundles supplied using the option `--patch-bundle`. > from patch bundles supplied using the option `--patch-bundle`.
> >
> You can list all patches with their indices using the command `list-patches`. > You can list all patches with their indices using the command `list-patches`.
> >
> Keep in mind, that the indices can change based on the order of the patch bundles supplied, > Keep in mind that the indices can change based on the order of the patch bundles supplied,
> as well if the patch bundles are updated, because patches can be added or removed. > as well if the patch bundles are updated because patches can be added or removed.
```bash ```bash
java -jar revanced-cli.jar patch \ java -jar revanced-cli.jar patch \
@@ -107,7 +106,6 @@ ReVanced CLI is divided into the following fundamental commands:
--include "Some patch" \ --include "Some patch" \
--ii 123 \ --ii 123 \
--exclude "Some other patch" \ --exclude "Some other patch" \
--out patched-app.apk \
--device-serial <device-serial> \ --device-serial <device-serial> \
--mount \ --mount \
app.apk app.apk
@@ -118,7 +116,7 @@ ReVanced CLI is divided into the following fundamental commands:
```bash ```bash
java -jar revanced-cli.jar utility uninstall \ java -jar revanced-cli.jar utility uninstall \
--package-name <package-name> \ --package-name <package-name> \
<device-serial> [<device-serial>]
``` ```
> [!NOTE] > [!NOTE]
@@ -130,9 +128,9 @@ ReVanced CLI is divided into the following fundamental commands:
```bash ```bash
java -jar revanced-cli.jar utility install \ java -jar revanced-cli.jar utility install \
-a input.apk \ -a input.apk \
<device-serial> [<device-serial>]
``` ```
> [!NOTE] > [!NOTE]
> You can mount an APK file > You can mount an APK file
> by supplying the package name of the app to mount the supplied APK file to over the option `--mount`. > by supplying the package name of the app to mount the supplied APK file over the option `--mount`.

26
docs/2_building.md Normal file
View File

@@ -0,0 +1,26 @@
# 🔨️ Building
Build ReVanced CLI from source.
## 📝 Requirements
- Java Development Kit 11 (Azul Zulu JRE or OpenJDK)
## 🏗️ Building
To build ReVanced CLI, follow these steps:
1. Clone the repository:
```bash
git clone git@github.com:ReVanced/revanced-cli.git
cd revanced-cli
```
2. Build the project:
```bash
./gradlew build
```
After the build succeeds, the built JAR file will be located at `build/libs/revanced-cli-<version>-all.jar`.

View File

@@ -1,8 +1,9 @@
# 💻 Documentation and guides of ReVanced CLI # 💻 Documentation and guides of ReVanced CLI
This documentation explains how to use [ReVanced CLI](https://github.com/revanced/revanced-cli). This documentation contains topics around [ReVanced CLI](https://github.com/revanced/revanced-cli).
## 📖 Table of contents ## 📖 Table of contents
1. [💼 Prerequisites](0_prerequisites.md) 1. [💼 Prerequisites](0_prerequisites.md)
2. [🛠️ Using ReVanced CLI](1_usage.md) 2. [🛠️ Using ReVanced CLI](1_usage.md)
3. [🔨 Building ReVanced CLI](2_building.md)

View File

@@ -1,4 +1,4 @@
org.gradle.parallel = true org.gradle.parallel = true
org.gradle.caching = true org.gradle.caching = true
kotlin.code.style = official kotlin.code.style = official
version = 4.1.1-dev.1 version = 4.4.0

View File

@@ -1,10 +1,10 @@
[versions] [versions]
shadow = "8.1.1" shadow = "8.1.1"
kotlin-test = "1.9.10" kotlin-test = "1.9.20"
kotlinx-coroutines-core = "1.7.3" kotlinx-coroutines-core = "1.7.3"
picocli = "4.7.3" picocli = "4.7.3"
revanced-patcher = "19.0.0" revanced-patcher = "19.1.0"
revanced-library = "1.2.0" revanced-library = "1.4.0"
[libraries] [libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" }

View File

@@ -2,6 +2,6 @@ rootProject.name = "revanced-cli"
buildCache { buildCache {
local { local {
isEnabled = !System.getenv().containsKey("CI") isEnabled = "CI" !in System.getenv()
} }
} }

View File

@@ -0,0 +1,67 @@
package app.revanced.cli.command
import app.revanced.library.PackageName
import app.revanced.library.PatchUtils
import app.revanced.library.VersionMap
import app.revanced.patcher.PatchBundleLoader
import picocli.CommandLine
import java.io.File
import java.util.logging.Logger
@CommandLine.Command(
name = "list-versions",
description = [
"List the most common compatible versions of apps that are compatible " +
"with the patches in the supplied patch bundles.",
],
)
internal class ListCompatibleVersions : Runnable {
private val logger = Logger.getLogger(ListCompatibleVersions::class.java.name)
@CommandLine.Parameters(
description = ["Paths to patch bundles."],
arity = "1..*",
)
private lateinit var patchBundles: Array<File>
@CommandLine.Option(
names = ["-f", "--filter-package-names"],
description = ["Filter patches by package name."],
)
private var packageNames: Set<String>? = null
@CommandLine.Option(
names = ["-u", "--count-unused-patches"],
description = ["Count patches that are not used by default."],
showDefaultValue = CommandLine.Help.Visibility.ALWAYS,
)
private var countUnusedPatches: Boolean = false
override fun run() {
val patches = PatchBundleLoader.Jar(*patchBundles)
fun VersionMap.buildVersionsString(): String {
if (isEmpty()) return "Any"
fun buildPatchesCountString(count: Int) = if (count == 1) "1 patch" else "$count patches"
return entries.joinToString("\n") { (version, count) ->
"$version (${buildPatchesCountString(count)})"
}
}
fun buildString(entry: Map.Entry<PackageName, VersionMap>) =
buildString {
val (name, versions) = entry
appendLine("Package name: $name")
appendLine("Most common compatible versions:")
appendLine(versions.buildVersionsString().prependIndent("\t"))
}
PatchUtils.getMostCommonCompatibleVersions(
patches,
packageNames,
countUnusedPatches,
).entries.joinToString("\n", transform = ::buildString).let(logger::info)
}
}

View File

@@ -8,69 +8,81 @@ import picocli.CommandLine.Help.Visibility.ALWAYS
import java.io.File import java.io.File
import java.util.logging.Logger import java.util.logging.Logger
@Command(
@Command(name = "list-patches", description = ["List patches from supplied patch bundles."]) name = "list-patches",
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(ListPatchesCommand::class.java.name)
@Parameters( @Parameters(
description = ["Paths to patch bundles."], arity = "1..*" description = ["Paths to patch bundles."],
arity = "1..*",
) )
private lateinit var patchBundles: Array<File> private lateinit var patchBundles: Array<File>
@Option( @Option(
names = ["-d", "--with-descriptions"], description = ["List their descriptions."], showDefaultValue = ALWAYS names = ["-d", "--with-descriptions"],
description = ["List their descriptions."],
showDefaultValue = ALWAYS,
) )
private var withDescriptions: Boolean = true private var withDescriptions: Boolean = true
@Option( @Option(
names = ["-p", "--with-packages"], names = ["-p", "--with-packages"],
description = ["List the packages the patches are compatible with."], description = ["List the packages the patches are compatible with."],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS,
) )
private var withPackages: Boolean = false private var withPackages: Boolean = false
@Option( @Option(
names = ["-v", "--with-versions"], names = ["-v", "--with-versions"],
description = ["List the versions of the apps the patches are compatible with."], description = ["List the versions of the apps the patches are compatible with."],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS,
) )
private var withVersions: Boolean = false private var withVersions: Boolean = false
@Option( @Option(
names = ["-o", "--with-options"], description = ["List the options of the patches."], showDefaultValue = ALWAYS names = ["-o", "--with-options"],
description = ["List the options of the patches."],
showDefaultValue = ALWAYS,
) )
private var withOptions: Boolean = false private var withOptions: Boolean = false
@Option( @Option(
names = ["-u", "--with-universal-patches"], names = ["-u", "--with-universal-patches"],
description = ["List patches which are compatible with any app."], description = ["List patches which are compatible with any app."],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS,
) )
private var withUniversalPatches: Boolean = true private var withUniversalPatches: Boolean = true
@Option( @Option(
names = ["-i", "--index"], names = ["-i", "--index"],
description = ["List the index of each patch in relation to the supplied patch bundles."], description = ["List the index of each patch in relation to the supplied patch bundles."],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS,
) )
private var withIndex: Boolean = true private var withIndex: Boolean = true
@Option( @Option(
names = ["-f", "--filter-package-name"], description = ["Filter patches by package name."] names = ["-f", "--filter-package-name"],
description = ["Filter patches by package name."],
) )
private var packageName: String? = null private var packageName: String? = null
override fun run() { override fun run() {
fun Patch.CompatiblePackage.buildString() = buildString { fun Patch.CompatiblePackage.buildString() =
buildString {
if (withVersions && versions != null) { if (withVersions && versions != null) {
appendLine("Package name: $name") appendLine("Package name: $name")
appendLine("Compatible versions:") appendLine("Compatible versions:")
append(versions!!.joinToString("\n") { version -> version }.prependIndent("\t")) append(versions!!.joinToString("\n") { version -> version }.prependIndent("\t"))
} else append("Package name: $name") } else {
append("Package name: $name")
}
} }
fun PatchOption<*>.buildString() = buildString { fun PatchOption<*>.buildString() =
buildString {
appendLine("Title: $title") appendLine("Title: $title")
description?.let { appendLine("Description: $it") } description?.let { appendLine("Description: $it") }
default?.let { default?.let {
@@ -84,7 +96,8 @@ internal object ListPatchesCommand : Runnable {
} }
} }
fun IndexedValue<Patch<*>>.buildString() = let { (index, patch) -> fun IndexedValue<Patch<*>>.buildString() =
let { (index, patch) ->
buildString { buildString {
if (withIndex) appendLine("Index: $index") if (withIndex) appendLine("Index: $index")
@@ -97,20 +110,23 @@ internal object ListPatchesCommand : Runnable {
append( append(
patch.options.values.joinToString("\n\n") { option -> patch.options.values.joinToString("\n\n") { option ->
option.buildString() option.buildString()
}.prependIndent("\t") }.prependIndent("\t"),
) )
} }
if (withPackages && patch.compatiblePackages != null) { if (withPackages && patch.compatiblePackages != null) {
appendLine("\nCompatible packages:") appendLine("\nCompatible packages:")
append(patch.compatiblePackages!!.joinToString("\n") { append(
patch.compatiblePackages!!.joinToString("\n") {
it.buildString() it.buildString()
}.prependIndent("\t")) }.prependIndent("\t"),
)
} }
} }
} }
fun Patch<*>.filterCompatiblePackages(name: String) = compatiblePackages?.any { it.name == name } fun Patch<*>.filterCompatiblePackages(name: String) =
compatiblePackages?.any { it.name == name }
?: withUniversalPatches ?: withUniversalPatches
val patches = PatchBundleLoader.Jar(*patchBundles).withIndex().toList() val patches = PatchBundleLoader.Jar(*patchBundles).withIndex().toList()

View File

@@ -7,21 +7,24 @@ import picocli.CommandLine.Command
import picocli.CommandLine.IVersionProvider import picocli.CommandLine.IVersionProvider
import java.util.* import java.util.*
fun main(args: Array<String>) { fun main(args: Array<String>) {
Logger.setDefault() Logger.setDefault()
CommandLine(MainCommand).execute(*args) CommandLine(MainCommand).execute(*args).let(System::exit)
} }
private object CLIVersionProvider : IVersionProvider { private object CLIVersionProvider : IVersionProvider {
override fun getVersion() = arrayOf( override fun getVersion() =
arrayOf(
MainCommand::class.java.getResourceAsStream( MainCommand::class.java.getResourceAsStream(
"/app/revanced/cli/version.properties" "/app/revanced/cli/version.properties",
)?.use { stream -> )?.use { stream ->
Properties().apply { load(stream) }.let { Properties().apply {
load(stream)
}.let {
"ReVanced CLI v${it.getProperty("version")}" "ReVanced CLI v${it.getProperty("version")}"
} }
} ?: "ReVanced CLI") } ?: "ReVanced CLI",
)
} }
@Command( @Command(
@@ -30,10 +33,11 @@ private object CLIVersionProvider : IVersionProvider {
mixinStandardHelpOptions = true, mixinStandardHelpOptions = true,
versionProvider = CLIVersionProvider::class, versionProvider = CLIVersionProvider::class,
subcommands = [ subcommands = [
ListPatchesCommand::class,
PatchCommand::class, PatchCommand::class,
OptionsCommand::class, OptionsCommand::class,
ListPatchesCommand::class,
ListCompatibleVersions::class,
UtilityCommand::class, UtilityCommand::class,
] ],
) )
private object MainCommand private object MainCommand

View File

@@ -16,35 +16,43 @@ internal object OptionsCommand : Runnable {
private val logger = Logger.getLogger(OptionsCommand::class.java.name) private val logger = Logger.getLogger(OptionsCommand::class.java.name)
@CommandLine.Parameters( @CommandLine.Parameters(
description = ["Paths to patch bundles."], arity = "1..*" description = ["Paths to patch bundles."],
arity = "1..*",
) )
private lateinit var patchBundles: Array<File> private lateinit var patchBundles: Array<File>
@CommandLine.Option( @CommandLine.Option(
names = ["-p", "--path"], description = ["Path to patch options JSON file."], showDefaultValue = ALWAYS names = ["-p", "--path"],
description = ["Path to patch options JSON file."],
showDefaultValue = ALWAYS,
) )
private var filePath: File = File("options.json") private var filePath: File = File("options.json")
@CommandLine.Option( @CommandLine.Option(
names = ["-o", "--overwrite"], description = ["Overwrite existing options file."], showDefaultValue = ALWAYS names = ["-o", "--overwrite"],
description = ["Overwrite existing options file."],
showDefaultValue = ALWAYS,
) )
private var overwrite: Boolean = false private var overwrite: Boolean = false
@CommandLine.Option( @CommandLine.Option(
names = ["-u", "--update"], names = ["-u", "--update"],
description = ["Update existing options by adding missing and removing non-existent options."], description = ["Update existing options by adding missing and removing non-existent options."],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS,
) )
private var update: Boolean = false private var update: Boolean = false
override fun run() = try { override fun run() =
try {
PatchBundleLoader.Jar(*patchBundles).let { patches -> PatchBundleLoader.Jar(*patchBundles).let { patches ->
val exists = filePath.exists() val exists = filePath.exists()
if (!exists || overwrite) { if (!exists || overwrite) {
if (exists && update) patches.setOptions(filePath) if (exists && update) patches.setOptions(filePath)
Options.serialize(patches, prettyPrint = true).let(filePath::writeText) Options.serialize(patches, prettyPrint = true).let(filePath::writeText)
} else throw OptionsFileAlreadyExistsException() } else {
throw OptionsFileAlreadyExistsException()
}
} }
} catch (ex: OptionsFileAlreadyExistsException) { } catch (ex: OptionsFileAlreadyExistsException) {
logger.severe("Options file already exists, use --overwrite to override it") logger.severe("Options file already exists, use --overwrite to override it")

View File

@@ -18,9 +18,9 @@ import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.util.logging.Logger import java.util.logging.Logger
@CommandLine.Command( @CommandLine.Command(
name = "patch", description = ["Patch an APK file."] name = "patch",
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(PatchCommand::class.java.name)
@@ -35,128 +35,150 @@ internal object PatchCommand : Runnable {
private var patchBundles = emptyList<File>() private var patchBundles = emptyList<File>()
@CommandLine.Option( @CommandLine.Option(
names = ["-i", "--include"], description = ["List of patches to include."] names = ["-i", "--include"],
description = ["List of patches to include."],
) )
private var includedPatches = hashSetOf<String>() private var includedPatches = hashSetOf<String>()
@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 = ["List of patches to include by their index in relation to the supplied patch bundles."],
) )
private var includedPatchesByIndex = arrayOf<Int>() private var includedPatchesByIndex = arrayOf<Int>()
@CommandLine.Option( @CommandLine.Option(
names = ["-e", "--exclude"], description = ["List of patches to exclude."] names = ["-e", "--exclude"],
description = ["List of patches to exclude."],
) )
private var excludedPatches = hashSetOf<String>() private var excludedPatches = hashSetOf<String>()
@CommandLine.Option( @CommandLine.Option(
names = ["--ei"], names = ["--ei"],
description = ["List of patches to exclude by their index in relation to the supplied patch bundles."] description = ["List of patches to exclude by their index in relation to the supplied patch bundles."],
) )
private var excludedPatchesByIndex = arrayOf<Int>() private var excludedPatchesByIndex = arrayOf<Int>()
@CommandLine.Option( @CommandLine.Option(
names = ["--options"], description = ["Path to patch options JSON file."], showDefaultValue = ALWAYS names = ["--options"],
description = ["Path to patch options JSON file."],
) )
private var optionsFile: File = File("options.json") private var optionsFile: File? = null
@CommandLine.Option( @CommandLine.Option(
names = ["--exclusive"], names = ["--exclusive"],
description = ["Only include patches that are explicitly specified to be included."], description = ["Only include patches that are explicitly specified to be included."],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS,
) )
private var exclusive = false private var exclusive = false
@CommandLine.Option( @CommandLine.Option(
names = ["-f", "--force"], names = ["-f", "--force"],
description = ["Bypass compatibility checks for the supplied APK's version."], description = ["Bypass compatibility checks for the supplied APK's version."],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS,
) )
private var force: Boolean = false private var force: Boolean = false
@CommandLine.Option( private var outputFilePath: File? = null
names = ["-o", "--out"], description = ["Path to save the patched APK file to."], required = true
)
private lateinit var outputFilePath: File
@CommandLine.Option( @CommandLine.Option(
names = ["-d", "--device-serial"], description = ["ADB device serial to install to."], showDefaultValue = ALWAYS names = ["-o", "--out"],
description = ["Path to save the patched APK file to. Defaults to the same directory as the supplied APK file."],
)
private fun setOutputFilePath(outputFilePath: File?) {
this.outputFilePath = outputFilePath?.absoluteFile
}
@CommandLine.Option(
names = ["-d", "--device-serial"],
description = ["ADB device serial to install to. If not supplied, the first connected device will be used."],
fallbackValue = "", // Empty string to indicate that the first connected device should be used.
arity = "0..1",
) )
private var deviceSerial: String? = null private var deviceSerial: String? = null
@CommandLine.Option( @CommandLine.Option(
names = ["--mount"], description = ["Install by mounting the patched APK file."], showDefaultValue = ALWAYS names = ["--mount"],
description = ["Install by mounting the patched APK file."],
showDefaultValue = ALWAYS,
) )
private var mount: Boolean = false private var mount: Boolean = false
@CommandLine.Option( @CommandLine.Option(
names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with."], names = ["--keystore"],
description = [
"Path to the keystore to sign the patched APK file with. " +
"Defaults to the same directory as the supplied APK file.",
],
) )
private var keystoreFilePath: File? = null private var keystoreFilePath: File? = null
// key store password // key store password
@CommandLine.Option( @CommandLine.Option(
names = ["--keystore-password"], names = ["--keystore-password"],
description = ["The password of the keystore to sign the patched APK file with."], description = ["The password of the keystore to sign the patched APK file with. Empty password by default."],
) )
private var keyStorePassword: String? = null // Empty password by default private var keyStorePassword: String? = null // Empty password by default
@CommandLine.Option( @CommandLine.Option(
names = ["--alias"], description = ["The alias of the key from the keystore to sign the patched APK file with."], names = ["--alias"],
showDefaultValue = ALWAYS description = ["The alias of the key from the keystore to sign the patched APK file with."],
showDefaultValue = ALWAYS,
) )
private var alias = "ReVanced Key" private var alias = "ReVanced Key"
@CommandLine.Option( @CommandLine.Option(
names = ["--keystore-entry-password"], names = ["--keystore-entry-password"],
description = ["The password of the entry from the keystore for the key to sign the patched APK file with."] description = ["The password of the entry from the keystore for the key to sign the patched APK file with."],
) )
private var password = "" // Empty password by default private var password = "" // Empty password by default
@CommandLine.Option( @CommandLine.Option(
names = ["--signer"], description = ["The name of the signer to sign the patched APK file with."], names = ["--signer"],
showDefaultValue = ALWAYS description = ["The name of the signer to sign the patched APK file with."],
showDefaultValue = ALWAYS,
) )
private var signer = "ReVanced" private var signer = "ReVanced"
@CommandLine.Option( @CommandLine.Option(
names = ["-r", "--resource-cache"], names = ["-r", "--resource-cache"],
description = ["Path to temporary resource cache directory."], description = ["Path to temporary resource cache directory."],
showDefaultValue = ALWAYS
) )
private var resourceCachePath = File("revanced-resource-cache.") private var resourceCachePath: File? = null
private var aaptBinaryPath: File? = null private var aaptBinaryPath: File? = null
@CommandLine.Option( @CommandLine.Option(
names = ["-p", "--purge"], names = ["-p", "--purge"],
description = ["Purge the temporary resource cache directory after patching."], description = ["Purge the temporary resource cache directory after patching."],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS,
) )
private var purge: Boolean = false private var purge: Boolean = false
@CommandLine.Option( @CommandLine.Option(
names = ["-w", "--warn"], names = ["-w", "--warn"],
description = ["Warn if a patch can not be found in the supplied patch bundles."], description = ["Warn if a patch can not be found in the supplied patch bundles."],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS,
) )
private var warn: Boolean = false private var warn: Boolean = false
@CommandLine.Parameters( @CommandLine.Parameters(
description = ["APK file to be patched."], arity = "1..1" description = ["APK file to be patched."],
arity = "1..1",
) )
@Suppress("unused") @Suppress("unused")
private fun setApk(apk: File) { private fun setApk(apk: File) {
if (!apk.exists()) throw CommandLine.ParameterException( if (!apk.exists()) {
throw CommandLine.ParameterException(
spec.commandLine(), spec.commandLine(),
"APK file ${apk.name} does not exist" "APK file ${apk.name} does not exist",
) )
}
this.apk = apk this.apk = apk
} }
@CommandLine.Option( @CommandLine.Option(
names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK."] names = ["-m", "--merge"],
description = ["One or more DEX files or containers to merge into the APK."],
) )
@Suppress("unused") @Suppress("unused")
private fun setIntegrations(integrations: Array<File>) { private fun setIntegrations(integrations: Array<File>) {
@@ -167,7 +189,9 @@ internal object PatchCommand : Runnable {
} }
@CommandLine.Option( @CommandLine.Option(
names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches."], required = true names = ["-b", "--patch-bundle"],
description = ["One or more bundles of patches."],
required = true,
) )
@Suppress("unused") @Suppress("unused")
private fun setPatchBundles(patchBundles: Array<File>) { private fun setPatchBundles(patchBundles: Array<File>) {
@@ -178,19 +202,43 @@ internal object PatchCommand : Runnable {
} }
@CommandLine.Option( @CommandLine.Option(
names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with."] names = ["--custom-aapt2-binary"],
description = ["Path to a custom AAPT binary to compile resources with."],
) )
@Suppress("unused") @Suppress("unused")
private fun setAaptBinaryPath(aaptBinaryPath: File) { private fun setAaptBinaryPath(aaptBinaryPath: File) {
if (!aaptBinaryPath.exists()) throw CommandLine.ParameterException( if (!aaptBinaryPath.exists()) {
throw CommandLine.ParameterException(
spec.commandLine(), spec.commandLine(),
"AAPT binary ${aaptBinaryPath.name} does not exist" "AAPT binary ${aaptBinaryPath.name} does not exist",
) )
}
this.aaptBinaryPath = aaptBinaryPath this.aaptBinaryPath = aaptBinaryPath
} }
override fun run() { override fun run() {
val adbManager = deviceSerial?.let { serial -> AdbManager.getAdbManager(serial, mount) } // region Setup
val outputFilePath =
outputFilePath ?: File("").absoluteFile.resolve(
"${apk.nameWithoutExtension}-patched.${apk.extension}",
)
val resourceCachePath =
resourceCachePath ?: outputFilePath.parentFile.resolve(
"${outputFilePath.nameWithoutExtension}-resource-cache",
)
val optionsFile =
optionsFile ?: outputFilePath.parentFile.resolve(
"${outputFilePath.nameWithoutExtension}-options.json",
)
val keystoreFilePath =
keystoreFilePath ?: outputFilePath.parentFile
.resolve("${outputFilePath.nameWithoutExtension}.keystore")
// endregion
// region Load patches // region Load patches
@@ -199,7 +247,8 @@ internal object PatchCommand : Runnable {
val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray()) val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray())
// Warn if a patch can not be found in the supplied patch bundles. // Warn if a patch can not be found in the supplied patch bundles.
if (warn) patches.map { it.name }.toHashSet().let { availableNames -> if (warn) {
patches.map { it.name }.toHashSet().let { availableNames ->
(includedPatches + excludedPatches).filter { name -> (includedPatches + excludedPatches).filter { name ->
!availableNames.contains(name) !availableNames.contains(name)
} }
@@ -207,6 +256,7 @@ internal object PatchCommand : Runnable {
if (unknownPatches.isEmpty()) return@let if (unknownPatches.isEmpty()) return@let
logger.warning("Unknown input of patches:\n${unknownPatches.joinToString("\n")}") logger.warning("Unknown input of patches:\n${unknownPatches.joinToString("\n")}")
} }
}
// endregion // endregion
@@ -216,19 +266,24 @@ internal object PatchCommand : Runnable {
resourceCachePath, resourceCachePath,
aaptBinaryPath?.path, aaptBinaryPath?.path,
resourceCachePath.absolutePath, resourceCachePath.absolutePath,
true true,
) ),
).use { patcher -> ).use { patcher ->
val filteredPatches = patcher.filterPatchSelection(patches).also { patches -> val filteredPatches =
patcher.filterPatchSelection(patches).also { patches ->
logger.info("Setting patch options") logger.info("Setting patch options")
if (optionsFile.exists()) patches.setOptions(optionsFile) if (optionsFile.exists()) {
else Options.serialize(patches, prettyPrint = true).let(optionsFile::writeText) patches.setOptions(optionsFile)
} else {
Options.serialize(patches, prettyPrint = true).let(optionsFile::writeText)
}
} }
// region Patch // region Patch
val patcherResult = patcher.apply { val patcherResult =
patcher.apply {
acceptIntegrations(integrations) acceptIntegrations(integrations)
acceptPatches(filteredPatches.toList()) acceptPatches(filteredPatches.toList())
@@ -249,14 +304,13 @@ internal object PatchCommand : Runnable {
// region Save // region Save
val alignedFile = resourceCachePath.resolve(apk.name).apply { val alignedFile =
resourceCachePath.resolve(apk.name).apply {
ApkUtils.copyAligned(apk, this, patcherResult) ApkUtils.copyAligned(apk, this, patcherResult)
} }
val keystoreFilePath = keystoreFilePath ?: outputFilePath.absoluteFile.parentFile if (!mount) {
.resolve("${outputFilePath.nameWithoutExtension}.keystore") ApkUtils.sign(
if (!mount) ApkUtils.sign(
alignedFile, alignedFile,
outputFilePath, outputFilePath,
ApkUtils.SigningOptions( ApkUtils.SigningOptions(
@@ -264,16 +318,22 @@ internal object PatchCommand : Runnable {
keyStorePassword, keyStorePassword,
alias, alias,
password, password,
signer signer,
),
) )
) } else {
else alignedFile.renameTo(outputFilePath) alignedFile.renameTo(outputFilePath)
}
logger.info("Saved to $outputFilePath")
// endregion // endregion
// region Install // region Install
adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName)) deviceSerial?.let { serial ->
AdbManager.getAdbManager(deviceSerial = serial.ifEmpty { null }, mount)
}?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName))
// endregion // endregion
} }
@@ -284,14 +344,14 @@ internal object PatchCommand : Runnable {
} }
} }
/** /**
* Filter the patches to be added to the patcher. The filter is based on the following: * Filter the patches to be added to the patcher. The filter is based on the following:
* *
* @param patches The patches to filter. * @param patches The patches to filter.
* @return The filtered patches. * @return The filtered patches.
*/ */
private fun Patcher.filterPatchSelection(patches: PatchSet): PatchSet = buildSet { private fun Patcher.filterPatchSelection(patches: PatchSet): PatchSet =
buildSet {
val packageName = context.packageMetadata.packageName val packageName = context.packageMetadata.packageName
val packageVersion = context.packageMetadata.packageVersion val packageVersion = context.packageMetadata.packageVersion
@@ -304,21 +364,25 @@ internal object PatchCommand : Runnable {
// 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 { it.name == packageName }?.let { `package` -> packages.singleOrNull { it.name == packageName }?.let { `package` ->
val matchesVersion = force || `package`.versions?.let { val matchesVersion =
force || `package`.versions?.let {
it.any { version -> version == packageVersion } it.any { version -> version == packageVersion }
} ?: true } ?: true
if (!matchesVersion) return@patch logger.warning( if (!matchesVersion) {
"$patchName is incompatible with version $packageVersion. " return@patch logger.warning(
+ "This patch is only compatible with version " "$patchName is incompatible with version $packageVersion. " +
+ packages.joinToString(";") { pkg -> "This patch is only compatible with version " +
packages.joinToString(";") { pkg ->
pkg.versions!!.joinToString(", ") pkg.versions!!.joinToString(", ")
} },
) )
}
} ?: return@patch logger.fine( } ?: return@patch logger.fine(
"$patchName is incompatible with $packageName. " "$patchName is incompatible with $packageName. " +
+ "This patch is only compatible with " "This patch is only compatible with " +
+ packages.joinToString(", ") { `package` -> `package`.name }) packages.joinToString(", ") { `package` -> `package`.name },
)
return@let return@let
} ?: logger.fine("$patchName has no constraint on packages.") } ?: logger.fine("$patchName has no constraint on packages.")
@@ -338,8 +402,12 @@ internal object PatchCommand : Runnable {
} }
private fun purge(resourceCachePath: File) { private fun purge(resourceCachePath: File) {
val result = if (resourceCachePath.deleteRecursively()) "Purged resource cache directory" val result =
else "Failed to purge resource cache directory" if (resourceCachePath.deleteRecursively()) {
"Purged resource cache directory"
} else {
"Failed to purge resource cache directory"
}
logger.info(result) logger.info(result)
} }
} }

View File

@@ -5,20 +5,23 @@ import picocli.CommandLine.*
import java.io.File import java.io.File
import java.util.logging.Logger import java.util.logging.Logger
@Command( @Command(
name = "install", description = ["Install an APK file to devices with the supplied ADB device serials"] name = "install",
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(InstallCommand::class.java.name)
@Parameters( @Parameters(
description = ["ADB device serials"], arity = "1..*" description = ["ADB device serials. If not supplied, the first connected device will be used."],
arity = "0..*",
) )
private lateinit var deviceSerials: Array<String> private var deviceSerials: Array<String>? = null
@Option( @Option(
names = ["-a", "--apk"], description = ["APK file to be installed"], required = true names = ["-a", "--apk"],
description = ["APK file to be installed"],
required = true,
) )
private lateinit var apk: File private lateinit var apk: File
@@ -28,11 +31,14 @@ internal object InstallCommand : Runnable {
) )
private var packageName: String? = null private var packageName: String? = null
override fun run() = deviceSerials.forEach { deviceSerial -> override fun run() {
fun install(deviceSerial: String? = null) =
try { try {
AdbManager.getAdbManager(deviceSerial, packageName != null).install(AdbManager.Apk(apk, packageName)) AdbManager.getAdbManager(deviceSerial, packageName != null).install(AdbManager.Apk(apk, packageName))
} catch (e: AdbManager.DeviceNotFoundException) { } catch (e: AdbManager.DeviceNotFoundException) {
logger.severe(e.toString()) logger.severe(e.toString())
} }
deviceSerials?.forEach(::install) ?: install()
} }
} }

View File

@@ -5,32 +5,41 @@ import picocli.CommandLine.*
import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Help.Visibility.ALWAYS
import java.util.logging.Logger import java.util.logging.Logger
@Command( @Command(
name = "uninstall", name = "uninstall",
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(UninstallCommand::class.java.name)
@Parameters(description = ["ADB device serials"], arity = "1..*") @Parameters(
private lateinit var deviceSerials: Array<String> description = ["ADB device serials. If not supplied, the first connected device will be used."],
arity = "0..*",
)
private var deviceSerials: Array<String>? = null
@Option(names = ["-p", "--package-name"], description = ["Package name of the app to uninstall"], required = true) @Option(
names = ["-p", "--package-name"],
description = ["Package name of the app to uninstall"],
required = true,
)
private lateinit var packageName: String private lateinit var packageName: String
@Option( @Option(
names = ["-u", "--unmount"], names = ["-u", "--unmount"],
description = ["Uninstall by unmounting the patched APK file"], description = ["Uninstall by unmounting the patched APK file"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS,
) )
private var unmount: Boolean = false private var unmount: Boolean = false
override fun run() = deviceSerials.forEach { deviceSerial -> override fun run() {
fun uninstall(deviceSerial: String? = null) =
try { try {
AdbManager.getAdbManager(deviceSerial, unmount).uninstall(packageName) AdbManager.getAdbManager(deviceSerial, unmount).uninstall(packageName)
} catch (e: AdbManager.DeviceNotFoundException) { } catch (e: AdbManager.DeviceNotFoundException) {
logger.severe(e.toString()) logger.severe(e.toString())
} }
deviceSerials?.forEach { uninstall(it) } ?: uninstall()
} }
} }

View File

@@ -4,7 +4,7 @@ import picocli.CommandLine
@CommandLine.Command( @CommandLine.Command(
name = "utility", name = "utility",
description = ["Commands for utility purposes"], description = ["Commands for utility purposes."],
subcommands = [InstallCommand::class, UninstallCommand::class], subcommands = [InstallCommand::class, UninstallCommand::class],
) )
internal object UtilityCommand internal object UtilityCommand