mirror of
https://github.com/revanced/revanced-cli.git
synced 2025-01-25 02:07:36 +01:00
chore: Merge branch dev
to main
(#328)
This commit is contained in:
commit
5d8beae7be
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -70,7 +70,7 @@ body:
|
||||
|
||||
Before creating a new bug report, please keep the following in mind:
|
||||
|
||||
- **Do not submit a duplicate bug report**: You can review existing bug reports [here](https://github.com/ReVanced/revanced-cli/labels/Bug%20report).
|
||||
- **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-cli/issues?q=label%3A%22Bug+report%22).
|
||||
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-cli/blob/main/CONTRIBUTING.md).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||
- type: textarea
|
||||
@ -102,7 +102,7 @@ body:
|
||||
label: Acknowledgements
|
||||
description: Your bug report will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: This issue is not a duplicate of an existing bug report.
|
||||
- label: I have checked all open and closed bug reports and this is not a duplicate.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
|
6
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
6
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -70,8 +70,8 @@ body:
|
||||
|
||||
Before creating a new feature request, please keep the following in mind:
|
||||
|
||||
- **Do not submit a duplicate feature request**: You can review existing feature requests [here](https://github.com/ReVanced/revanced-cli//labels/Feature%20request).
|
||||
- **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-cli//blob/main/CONTRIBUTING.md).
|
||||
- **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-cli/issues?q=label%3A%22Feature+request%22).
|
||||
- **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-cli//blob/main/CONTRIBUTING.md).
|
||||
- **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app).
|
||||
- type: textarea
|
||||
attributes:
|
||||
@ -98,7 +98,7 @@ body:
|
||||
label: Acknowledgements
|
||||
description: Your feature request will be closed if you don't follow the checklist below.
|
||||
options:
|
||||
- label: This issue is not a duplicate of an existing feature request.
|
||||
- label: I have checked all open and closed feature requests and this is not a duplicate.
|
||||
required: true
|
||||
- label: I have chosen an appropriate title.
|
||||
required: true
|
||||
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@ -10,6 +10,8 @@ on:
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -42,9 +44,9 @@ jobs:
|
||||
with:
|
||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
fingerprint: ${{ env.GPG_FINGERPRINT }}
|
||||
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||
|
||||
- name: Release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npm exec semantic-release
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -121,5 +121,5 @@ node_modules/
|
||||
revanced-cache/
|
||||
options.toml
|
||||
|
||||
# Generated by an Android project (such as ReVanced Integrations)
|
||||
# Generated by Android projects
|
||||
local.properties
|
@ -23,7 +23,8 @@
|
||||
"assets": [
|
||||
"CHANGELOG.md",
|
||||
"gradle.properties"
|
||||
]
|
||||
],
|
||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||
}
|
||||
],
|
||||
[
|
||||
|
83
CHANGELOG.md
83
CHANGELOG.md
@ -1,3 +1,86 @@
|
||||
# [5.0.0-dev.11](https://github.com/ReVanced/revanced-cli/compare/v5.0.0-dev.10...v5.0.0-dev.11) (2024-11-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* List if patch option is required ([#346](https://github.com/ReVanced/revanced-cli/issues/346)) ([98ff0c3](https://github.com/ReVanced/revanced-cli/commit/98ff0c34fa71c3b3ecd96157d45a30ee2b8979c6))
|
||||
|
||||
# [5.0.0-dev.10](https://github.com/ReVanced/revanced-cli/compare/v5.0.0-dev.9...v5.0.0-dev.10) (2024-11-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Make CLI ArgGroup non-exclusive to be able to disable and enable patches at the same time ([1bb0d13](https://github.com/ReVanced/revanced-cli/commit/1bb0d13726fd5790c59cb6d28df3618c7606710d))
|
||||
|
||||
# [5.0.0-dev.9](https://github.com/ReVanced/revanced-cli/compare/v5.0.0-dev.8...v5.0.0-dev.9) (2024-11-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Print in new line correctly ([c2dc9d7](https://github.com/ReVanced/revanced-cli/commit/c2dc9d76be33c98284741e23c406500483c47753))
|
||||
|
||||
# [5.0.0-dev.8](https://github.com/ReVanced/revanced-cli/compare/v5.0.0-dev.7...v5.0.0-dev.8) (2024-10-17)
|
||||
|
||||
# [5.0.0-dev.7](https://github.com/ReVanced/revanced-cli/compare/v5.0.0-dev.6...v5.0.0-dev.7) (2024-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Check for null when no device serial was specified ([1da8ae9](https://github.com/ReVanced/revanced-cli/commit/1da8ae9e46000dd3c4eecd793c559e75012cf535))
|
||||
|
||||
# [5.0.0-dev.6](https://github.com/ReVanced/revanced-cli/compare/v5.0.0-dev.5...v5.0.0-dev.6) (2024-10-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Use the first connected device when no ADB device is specified ([5f952f3](https://github.com/ReVanced/revanced-cli/commit/5f952f35f5cb388b6509b2b4d905b8143ebc7996))
|
||||
|
||||
# [5.0.0-dev.5](https://github.com/ReVanced/revanced-cli/compare/v5.0.0-dev.4...v5.0.0-dev.5) (2024-09-30)
|
||||
|
||||
# [5.0.0-dev.4](https://github.com/ReVanced/revanced-cli/compare/v5.0.0-dev.3...v5.0.0-dev.4) (2024-09-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Make patches selectable by using a mutable collection for the selection option ([751fa1d](https://github.com/ReVanced/revanced-cli/commit/751fa1d889f40c51b291116029fd84f2b051f2f0))
|
||||
* Make the patch command work without specifying any selection ([ba159a3](https://github.com/ReVanced/revanced-cli/commit/ba159a35a9a99d18a4c1e04128b08ae336a49b3e))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Show error about no installation device found at the beginning ([3300e6b](https://github.com/ReVanced/revanced-cli/commit/3300e6b4333ed1c4e6785cb82eca9016fc6d4a20))
|
||||
|
||||
# [5.0.0-dev.3](https://github.com/ReVanced/revanced-cli/compare/v5.0.0-dev.2...v5.0.0-dev.3) (2024-08-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Simplify option descriptions ([45c998b](https://github.com/ReVanced/revanced-cli/commit/45c998b335b65ac233fece8b804dc7410142691c))
|
||||
|
||||
# [5.0.0-dev.2](https://github.com/ReVanced/revanced-cli/compare/v5.0.0-dev.1...v5.0.0-dev.2) (2024-08-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Simplify command and option names and descriptions ([#338](https://github.com/ReVanced/revanced-cli/issues/338)) ([6e7797a](https://github.com/ReVanced/revanced-cli/commit/6e7797a3f0525a8f48af7182157da0d045600ac2))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* Options have been renamed.
|
||||
|
||||
# [5.0.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.6.0...v5.0.0-dev.1) (2024-08-12)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Set patch options via CLI ([#336](https://github.com/ReVanced/revanced-cli/issues/336)) ([2300243](https://github.com/ReVanced/revanced-cli/commit/23002434b2d51c2a3b30b33dd0526261432d90ce))
|
||||
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* This commit changes various CLI options and removes the `options.json` file. Instead, patch options can now be passed via CLI options
|
||||
|
||||
# [4.6.0](https://github.com/ReVanced/revanced-cli/compare/v4.5.0...v4.6.0) (2024-04-01)
|
||||
|
||||
|
||||
|
@ -63,11 +63,11 @@
|
||||
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/ReVanced/revanced-cli/release.yml)
|
||||
![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg)
|
||||
|
||||
Command line application to use ReVanced.
|
||||
Command-line application to use ReVanced.
|
||||
|
||||
## ❓ About
|
||||
|
||||
ReVanced CLI is a command line application that uses [ReVanced Patcher](https://github.com/revanced/revanced-patcher) to patch Android apps.
|
||||
ReVanced CLI is a command-line application that uses [ReVanced Patcher](https://github.com/revanced/revanced-patcher) to patch Android apps.
|
||||
|
||||
## 💪 Features
|
||||
|
||||
@ -75,7 +75,7 @@ Some of the features ReVanced CLI provides are:
|
||||
|
||||
- 💉 **Patch apps**: Harness ReVanced Patcher to patch Android apps
|
||||
- 💾 **Install and uninstall apps**: Install and uninstall Apps via ADB,
|
||||
using the Android package manager, or by mounting using root permissions
|
||||
using the Android package manager or by mounting using root permissions
|
||||
- 📃 **List patches from patch bundles**: List available patches, compatible packages, and versions
|
||||
- 💪 **Flexibility and functionality**: Apply any combination of patches to any version of Android apps
|
||||
|
||||
|
@ -16,10 +16,9 @@ application {
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
google()
|
||||
maven {
|
||||
// A repository must be speficied for some reason. "registry" is a dummy.
|
||||
// A repository must be specified for some reason. "registry" is a dummy.
|
||||
url = uri("https://maven.pkg.github.com/revanced/registry")
|
||||
credentials {
|
||||
username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR")
|
||||
|
@ -4,9 +4,9 @@ To use ReVanced CLI, you will need to fulfill specific requirements.
|
||||
|
||||
## 🤝 Requirements
|
||||
|
||||
- Java Runtime Environment 11 ([Azul Zulu JRE](https://www.azul.com/downloads/?version=java-11-lts&package=jdk#zulu) or [OpenJDK](https://jdk.java.net/archive/))
|
||||
- Java Runtime Environment 11 ([Azul Zulu JRE](https://www.azul.com/downloads/?version=java-11-lts&package=jre#zulu) or [OpenJDK](https://jdk.java.net/archive/))
|
||||
- [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)
|
||||
- x86 or x86-64 (For [other architectures](https://github.com/ReVanced/revanced-manager/tree/main/android/app/src/main/jniLibs) use the `--custom-aapt2-binary` option)
|
||||
|
||||
## ⏭️ Whats next
|
||||
|
||||
|
242
docs/1_usage.md
242
docs/1_usage.md
@ -1,129 +1,171 @@
|
||||
# 🛠️ Using ReVanced CLI
|
||||
|
||||
Learn how to use ReVanced CLI.
|
||||
The following examples will show you how to perform basic operations.
|
||||
You can list patches, patch an app, uninstall, and install an app.
|
||||
|
||||
## 🔨 Usage
|
||||
## 🚀 Show all commands
|
||||
|
||||
ReVanced CLI is divided into the following fundamental commands:
|
||||
```bash
|
||||
java -jar revanced-cli.jar -h
|
||||
```
|
||||
|
||||
- ### 🚀 Show all available options for ReVanced CLI
|
||||
## 📃 List patches
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar -h
|
||||
```
|
||||
```bash
|
||||
java -jar revanced-cli.jar list-patches --with-packages --with-versions --with-options patches.rvp
|
||||
```
|
||||
|
||||
- ### 📃 List patches
|
||||
## 💉 Patch an app
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar list-patches \
|
||||
--with-packages \
|
||||
--with-versions \
|
||||
--with-options \
|
||||
revanced-patches.jar [<patch-bundle> ...]
|
||||
```
|
||||
To patch an app using the default list of patches, use the `patch` command:
|
||||
|
||||
- ### ⚙️ Generate options
|
||||
```bash
|
||||
java -jar revanced-cli.jar patch -p patches.rvp input.apk
|
||||
```
|
||||
|
||||
This will generate an `options.json` file for the patches from a list of supplied patch bundles.
|
||||
The file can be supplied to ReVanced CLI later on.
|
||||
You can also use multiple RVP files:
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar options \
|
||||
--path options.json \
|
||||
--overwrite \
|
||||
revanced-patches.jar [<patch-bundle> ...]
|
||||
```
|
||||
```bash
|
||||
java -jar revanced-cli.jar patch -p patches.rvp -p another-patches.rvp input.apk
|
||||
```
|
||||
|
||||
> **ℹ️ Note**
|
||||
> A default `options.json` file will be automatically created if it does not exist
|
||||
> without any need for intervention when using the `patch` command.
|
||||
To change the default set of enabled or disabled patches, use the option `-e` or `-d` to enable or disable specific patches.
|
||||
You can use the `list-patches` command to see which patches are enabled by default.
|
||||
|
||||
- ### 💉 Patch an app
|
||||
To only enable specific patches, you can use the option `--exclusive` combined with `-e`.
|
||||
Remember that the options `-e` and `-d` match the patch's name exactly. Here is an example:
|
||||
|
||||
You can patch apps by supplying patch bundles and the app to patch.
|
||||
After patching, ReVanced CLI can install the patched app on your device using two methods:
|
||||
```bash
|
||||
java -jar revanced-cli.jar patch -p patches.rvp --exclusive -e "Patch name" -e "Another patch name" input.apk
|
||||
```
|
||||
|
||||
> **💡 Tip**
|
||||
> For ReVanced CLI to be able to install the patched app on your device, make sure ADB is working:
|
||||
>
|
||||
> ```bash
|
||||
> adb shell exit
|
||||
> ```
|
||||
>
|
||||
> If you want to mount the patched app on top of the un-patched app, make sure you have root permissions:
|
||||
>
|
||||
> ```bash
|
||||
> adb shell su -c exit
|
||||
> ```
|
||||
You can also use the options `--ei` or `--di` to enable or disable patches by their index.
|
||||
This is useful, if two patches happen to have the same name, or if typing the names is too cumbersome.
|
||||
To know the indices of patches, use the command `list-patches`:
|
||||
|
||||
> **⚠️ Warning**
|
||||
> Some patches may require integrations
|
||||
> such as [ReVanced Integrations](https://github.com/revanced/revanced-integrations).
|
||||
> Supply them with the option `--merge`. ReVanced Patcher will automatically determine if they are necessary.
|
||||
```bash
|
||||
java -jar revanced-cli.jar list-patches patches.rvp
|
||||
```
|
||||
|
||||
- #### 👾 Patch an app and install it on your device regularly
|
||||
Then you can use the indices to enable or disable patches:
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar patch \
|
||||
--patch-bundle revanced-patches.jar \
|
||||
-d \
|
||||
input.apk
|
||||
```
|
||||
```bash
|
||||
java -jar revanced-cli.jar patch -p patches.rvp --ei 123 --di 456 input.apk
|
||||
```
|
||||
|
||||
- #### 👾 Patch an app and mount it on top of the un-patched app with root permissions
|
||||
You can combine the option `-e`, `-d`, `--ei`, `--di` and `--exclusive`. Here is an example:
|
||||
|
||||
> **❗ Caution**
|
||||
> Ensure that the same app you are patching and mounting over is installed on your device:
|
||||
>
|
||||
> ```bash
|
||||
> adb install app.apk
|
||||
> ```
|
||||
```bash
|
||||
java -jar revanced-cli.jar patch -p patches.rvp --exclusive -e "Patch name" --ei 123 input.apk
|
||||
```
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar patch \
|
||||
--patch-bundle revanced-patches.jar \
|
||||
--include "Some patch" \
|
||||
--ii 123 \
|
||||
--exclude "Some other patch" \
|
||||
-d \
|
||||
--mount \
|
||||
app.apk
|
||||
```
|
||||
|
||||
> **💡 Tip**
|
||||
> You can use the option `--ii` to include or `--ie` to exclude
|
||||
> patches by their index in relation to supplied patch bundles,
|
||||
> similarly to the option `--include` and `--exclude`.
|
||||
>
|
||||
> This is useful in case two patches have the same name, and you must include or exclude one.
|
||||
> 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`.
|
||||
>
|
||||
> 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,
|
||||
> as well if the patch bundles are updated because patches can be added or removed.
|
||||
> [!TIP]
|
||||
> You can use the option `-i` to automatically install the patched app after patching.
|
||||
> Make sure ADB is working:
|
||||
>
|
||||
> ```bash
|
||||
> adb shell exit
|
||||
> ```
|
||||
|
||||
- ### 🗑️ Uninstall an app
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar utility uninstall \
|
||||
--package-name <package-name> \
|
||||
[<device-serial>]
|
||||
```
|
||||
> [!TIP]
|
||||
> You can use the option `--mount` to mount the patched app on top of the un-patched app.
|
||||
> Make sure you have root permissions and the same app you are patching and mounting over is installed on your device:
|
||||
>
|
||||
> ```bash
|
||||
> adb shell su -c exit
|
||||
> adb install input.apk
|
||||
> ```
|
||||
|
||||
> **💡 Tip**
|
||||
> You can unmount an APK file
|
||||
> by adding the option `--unmount`.
|
||||
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:
|
||||
|
||||
- ### ️ 📦 Install an app
|
||||
```bash
|
||||
java -jar revanced-cli.jar list-patches --with-options patches.rvp
|
||||
```
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar utility install \
|
||||
-a input.apk \
|
||||
[<device-serial>]
|
||||
```
|
||||
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:
|
||||
|
||||
> **💡 Tip**
|
||||
> You can mount an APK file
|
||||
> by supplying the app's package name to mount the supplied APK file over the option `-mount`.
|
||||
```bash
|
||||
java -jar revanced-cli.jar patch -p patches.rvp -e "Patch name" -Okey1=value1 -Okey2=value2 input.apk
|
||||
```
|
||||
|
||||
If you want to set the option value to `null`, you can omit the value:
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar patch -p 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.
|
||||
> The 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 -p patches.rvp -e "Patch name" -OstringKey=\'1\' input.apk
|
||||
> ```
|
||||
## 📦 Install an app manually
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar utility install -a input.apk
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> You can use the option `--mount` to mount the patched app on top of the un-patched app.
|
||||
> Make sure you have root permissions and the same app you are patching and mounting over is installed on your device:
|
||||
>
|
||||
> ```bash
|
||||
> adb shell su -c exit
|
||||
> adb install input.apk
|
||||
> ```
|
||||
|
||||
## 🗑️ Uninstall an app manually
|
||||
|
||||
Here `<package-name>` is the package name of the app you want to uninstall:
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar utility uninstall --package-name <package-name>
|
||||
```
|
||||
|
||||
If the app is mounted, you need to unmount it by using the option `--unmount`:
|
||||
|
||||
```bash
|
||||
java -jar revanced-cli.jar utility uninstall --package-name <package-name> --unmount
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> By default, the app is installed or uninstalled to the first connected device.
|
||||
> You can append one or more devices by their serial to install or uninstall an app on your selected choice of devices:
|
||||
>
|
||||
> ```bash
|
||||
> java -jar revanced-cli.jar utility uninstall --package-name <package-name> [<device-serial> ...]
|
||||
> ```
|
||||
|
@ -1,4 +1,4 @@
|
||||
org.gradle.parallel = true
|
||||
org.gradle.caching = true
|
||||
kotlin.code.style = official
|
||||
version = 4.6.0
|
||||
version = 5.0.0-dev.11
|
||||
|
@ -1,18 +1,18 @@
|
||||
[versions]
|
||||
shadow = "8.1.1"
|
||||
kotlin = "1.9.22"
|
||||
kotlinx-coroutines-core = "1.7.3"
|
||||
picocli = "4.7.5"
|
||||
revanced-patcher = "19.3.1"
|
||||
revanced-library = "2.3.0"
|
||||
kotlin = "2.0.20"
|
||||
kotlinx = "1.8.1"
|
||||
picocli = "4.7.6"
|
||||
revanced-patcher = "21.0.0"
|
||||
revanced-library = "3.0.2"
|
||||
|
||||
[libraries]
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
|
||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx" }
|
||||
picocli = { module = "info.picocli:picocli", version.ref = "picocli" }
|
||||
revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" }
|
||||
revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" }
|
||||
revanced-library = { module = "app.revanced:revanced-library-jvm", version.ref = "revanced-library" }
|
||||
|
||||
[plugins]
|
||||
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }
|
||||
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
8
gradle/wrapper/gradle-wrapper.properties
vendored
8
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,8 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
|
||||
distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c
|
||||
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dist
|
||||
zipStorePath=wrapper/dists
|
||||
|
17
gradlew
vendored
17
gradlew
vendored
@ -83,7 +83,8 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@ -201,11 +202,11 @@ fi
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
20
gradlew.bat
vendored
20
gradlew.bat
vendored
@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
|
3002
package-lock.json
generated
3002
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@
|
||||
"@saithodev/semantic-release-backmerge": "^4.0.1",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"gradle-semantic-release-plugin": "^1.9.1",
|
||||
"semantic-release": "^23.0.2"
|
||||
"gradle-semantic-release-plugin": "^1.10.1",
|
||||
"semantic-release": "^24.1.2"
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1 @@
|
||||
rootProject.name = "revanced-cli"
|
||||
|
||||
buildCache {
|
||||
local {
|
||||
isEnabled = "CI" !in System.getenv()
|
||||
}
|
||||
}
|
||||
|
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,9 +1,9 @@
|
||||
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 app.revanced.library.mostCommonCompatibleVersions
|
||||
import app.revanced.patcher.patch.loadPatchesFromJar
|
||||
import picocli.CommandLine
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
@ -12,17 +12,17 @@ import java.util.logging.Logger
|
||||
name = "list-versions",
|
||||
description = [
|
||||
"List the most common compatible versions of apps that are compatible " +
|
||||
"with the patches in the supplied patch bundles.",
|
||||
"with the patches from RVP files.",
|
||||
],
|
||||
)
|
||||
internal class ListCompatibleVersions : Runnable {
|
||||
private val logger = Logger.getLogger(ListCompatibleVersions::class.java.name)
|
||||
private val logger = Logger.getLogger(this::class.java.name)
|
||||
|
||||
@CommandLine.Parameters(
|
||||
description = ["Paths to patch bundles."],
|
||||
description = ["Paths to RVP files."],
|
||||
arity = "1..*",
|
||||
)
|
||||
private lateinit var patchBundles: Array<File>
|
||||
private lateinit var patchesFiles: Set<File>
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-f", "--filter-package-names"],
|
||||
@ -38,8 +38,6 @@ internal class ListCompatibleVersions : Runnable {
|
||||
private var countUnusedPatches: Boolean = false
|
||||
|
||||
override fun run() {
|
||||
val patches = PatchBundleLoader.Jar(*patchBundles)
|
||||
|
||||
fun VersionMap.buildVersionsString(): String {
|
||||
if (isEmpty()) return "Any"
|
||||
|
||||
@ -58,8 +56,9 @@ internal class ListCompatibleVersions : Runnable {
|
||||
appendLine(versions.buildVersionsString().prependIndent("\t"))
|
||||
}
|
||||
|
||||
PatchUtils.getMostCommonCompatibleVersions(
|
||||
patches,
|
||||
val patches = loadPatchesFromJar(patchesFiles)
|
||||
|
||||
patches.mostCommonCompatibleVersions(
|
||||
packageNames,
|
||||
countUnusedPatches,
|
||||
).entries.joinToString("\n", transform = ::buildString).let(logger::info)
|
||||
|
@ -1,25 +1,26 @@
|
||||
package app.revanced.cli.command
|
||||
|
||||
import app.revanced.patcher.PatchBundleLoader
|
||||
import app.revanced.patcher.patch.Package
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.options.PatchOption
|
||||
import app.revanced.patcher.patch.loadPatchesFromJar
|
||||
import picocli.CommandLine.*
|
||||
import picocli.CommandLine.Help.Visibility.ALWAYS
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
import app.revanced.patcher.patch.Option as PatchOption
|
||||
|
||||
@Command(
|
||||
name = "list-patches",
|
||||
description = ["List patches from supplied patch bundles."],
|
||||
description = ["List patches from supplied RVP files."],
|
||||
)
|
||||
internal object ListPatchesCommand : Runnable {
|
||||
private val logger = Logger.getLogger(ListPatchesCommand::class.java.name)
|
||||
private val logger = Logger.getLogger(this::class.java.name)
|
||||
|
||||
@Parameters(
|
||||
description = ["Paths to patch bundles."],
|
||||
description = ["Paths to RVP files."],
|
||||
arity = "1..*",
|
||||
)
|
||||
private lateinit var patchBundles: Array<File>
|
||||
private lateinit var patchesFiles: Set<File>
|
||||
|
||||
@Option(
|
||||
names = ["-d", "--with-descriptions"],
|
||||
@ -58,7 +59,7 @@ internal object ListPatchesCommand : Runnable {
|
||||
|
||||
@Option(
|
||||
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 RVP files."],
|
||||
showDefaultValue = ALWAYS,
|
||||
)
|
||||
private var withIndex: Boolean = true
|
||||
@ -70,30 +71,36 @@ internal object ListPatchesCommand : Runnable {
|
||||
private var packageName: String? = null
|
||||
|
||||
override fun run() {
|
||||
fun Patch.CompatiblePackage.buildString() =
|
||||
buildString {
|
||||
fun Package.buildString(): String {
|
||||
val (name, versions) = this
|
||||
|
||||
return buildString {
|
||||
if (withVersions && versions != null) {
|
||||
appendLine("Package name: $name")
|
||||
appendLine("Compatible versions:")
|
||||
append(versions!!.joinToString("\n") { version -> version }.prependIndent("\t"))
|
||||
append(versions.joinToString("\n") { version -> version }.prependIndent("\t"))
|
||||
} else {
|
||||
append("Package name: $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun PatchOption<*>.buildString() =
|
||||
buildString {
|
||||
appendLine("Title: $title")
|
||||
description?.let { appendLine("Description: $it") }
|
||||
appendLine("Required: $required")
|
||||
default?.let {
|
||||
appendLine("Key: $key")
|
||||
append("Default: $it")
|
||||
} ?: append("Key: $key")
|
||||
|
||||
values?.let { values ->
|
||||
appendLine("\nValid values:")
|
||||
appendLine("\nPossible values:")
|
||||
append(values.map { "${it.value} (${it.key})" }.joinToString("\n").prependIndent("\t"))
|
||||
}
|
||||
|
||||
append("\nType: $type")
|
||||
}
|
||||
|
||||
fun IndexedValue<Patch<*>>.buildString() =
|
||||
@ -105,6 +112,8 @@ internal object ListPatchesCommand : Runnable {
|
||||
|
||||
if (withDescriptions) append("\nDescription: ${patch.description}")
|
||||
|
||||
append("\nEnabled: ${patch.use}")
|
||||
|
||||
if (withOptions && patch.options.isNotEmpty()) {
|
||||
appendLine("\nOptions:")
|
||||
append(
|
||||
@ -126,10 +135,10 @@ internal object ListPatchesCommand : Runnable {
|
||||
}
|
||||
|
||||
fun Patch<*>.filterCompatiblePackages(name: String) =
|
||||
compatiblePackages?.any { it.name == name }
|
||||
compatiblePackages?.any { (compatiblePackageName, _) -> compatiblePackageName == name }
|
||||
?: withUniversalPatches
|
||||
|
||||
val patches = PatchBundleLoader.Jar(*patchBundles).withIndex().toList()
|
||||
val patches = loadPatchesFromJar(patchesFiles).withIndex().toList()
|
||||
|
||||
val filtered =
|
||||
packageName?.let { patches.filter { (_, patch) -> patch.filterCompatiblePackages(it) } } ?: patches
|
||||
|
@ -34,7 +34,6 @@ private object CLIVersionProvider : IVersionProvider {
|
||||
versionProvider = CLIVersionProvider::class,
|
||||
subcommands = [
|
||||
PatchCommand::class,
|
||||
OptionsCommand::class,
|
||||
ListPatchesCommand::class,
|
||||
ListCompatibleVersions::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.PatchBundleLoader
|
||||
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: Array<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 {
|
||||
PatchBundleLoader.Jar(*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.applyTo
|
||||
import app.revanced.library.Options
|
||||
import app.revanced.library.Options.setOptions
|
||||
import app.revanced.library.adb.AdbManager
|
||||
import app.revanced.patcher.PatchBundleLoader
|
||||
import app.revanced.patcher.PatchSet
|
||||
import app.revanced.library.installation.installer.*
|
||||
import app.revanced.library.setOptions
|
||||
import app.revanced.patcher.Patcher
|
||||
import app.revanced.patcher.PatcherConfig
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.loadPatchesFromJar
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.ArgGroup
|
||||
import picocli.CommandLine.Help.Visibility.ALWAYS
|
||||
import picocli.CommandLine.Model.CommandSpec
|
||||
import picocli.CommandLine.Spec
|
||||
@ -24,57 +24,82 @@ import java.util.logging.Logger
|
||||
description = ["Patch an APK file."],
|
||||
)
|
||||
internal object PatchCommand : Runnable {
|
||||
private val logger = Logger.getLogger(PatchCommand::class.java.name)
|
||||
private val logger = Logger.getLogger(this::class.java.name)
|
||||
|
||||
@Spec
|
||||
lateinit var spec: CommandSpec // injected by picocli
|
||||
private lateinit var spec: CommandSpec
|
||||
|
||||
private lateinit var apk: File
|
||||
@ArgGroup(exclusive = false, multiplicity = "0..*")
|
||||
private var selection = mutableSetOf<Selection>()
|
||||
|
||||
private var integrations = setOf<File>()
|
||||
internal class Selection {
|
||||
@ArgGroup(exclusive = false)
|
||||
internal var enabled: EnableSelection? = null
|
||||
|
||||
private var patchBundles = emptySet<File>()
|
||||
internal class EnableSelection {
|
||||
@ArgGroup(multiplicity = "1")
|
||||
internal lateinit var selector: EnableSelector
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-i", "--include"],
|
||||
description = ["List of patches to include."],
|
||||
)
|
||||
private var includedPatches = hashSetOf<String>()
|
||||
internal class EnableSelector {
|
||||
@CommandLine.Option(
|
||||
names = ["-e", "--enable"],
|
||||
description = ["Name of the patch."],
|
||||
required = true,
|
||||
)
|
||||
internal var name: String? = null
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--ii"],
|
||||
description = ["List of patches to include by their index in relation to the supplied patch bundles."],
|
||||
)
|
||||
private var includedPatchesByIndex = arrayOf<Int>()
|
||||
@CommandLine.Option(
|
||||
names = ["--ei"],
|
||||
description = ["Index of the patch in the combined list of the supplied RVP files."],
|
||||
required = true,
|
||||
)
|
||||
internal var index: Int? = null
|
||||
}
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-e", "--exclude"],
|
||||
description = ["List of patches to exclude."],
|
||||
)
|
||||
private var excludedPatches = hashSetOf<String>()
|
||||
@CommandLine.Option(
|
||||
names = ["-O", "--options"],
|
||||
description = ["Option values keyed by option keys."],
|
||||
mapFallbackValue = CommandLine.Option.NULL_VALUE,
|
||||
converter = [OptionKeyConverter::class, OptionValueConverter::class],
|
||||
)
|
||||
internal var options = mutableMapOf<String, Any?>()
|
||||
}
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--ei"],
|
||||
description = ["List of patches to exclude by their index in relation to the supplied patch bundles."],
|
||||
)
|
||||
private var excludedPatchesByIndex = arrayOf<Int>()
|
||||
@ArgGroup(exclusive = false)
|
||||
internal var disable: DisableSelection? = null
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--options"],
|
||||
description = ["Path to patch options JSON file."],
|
||||
)
|
||||
private var optionsFile: File? = null
|
||||
internal class DisableSelection {
|
||||
@ArgGroup(multiplicity = "1")
|
||||
internal lateinit var selector: DisableSelector
|
||||
|
||||
internal class DisableSelector {
|
||||
@CommandLine.Option(
|
||||
names = ["-d", "--disable"],
|
||||
description = ["Name of the patch."],
|
||||
required = true,
|
||||
)
|
||||
internal var name: String? = null
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--di"],
|
||||
description = ["Index of the patch in the combined list of the supplied RVP files."],
|
||||
required = true,
|
||||
)
|
||||
internal var index: Int? = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--exclusive"],
|
||||
description = ["Only include patches that are explicitly specified to be included."],
|
||||
description = ["Disable all patches except the ones enabled."],
|
||||
showDefaultValue = ALWAYS,
|
||||
)
|
||||
private var exclusive = false
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-f", "--force"],
|
||||
description = ["Bypass compatibility checks for the supplied APK's version."],
|
||||
description = ["Don't check for compatibility with the supplied APK's version."],
|
||||
showDefaultValue = ALWAYS,
|
||||
)
|
||||
private var force: Boolean = false
|
||||
@ -83,7 +108,7 @@ internal object PatchCommand : Runnable {
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-o", "--out"],
|
||||
description = ["Path to save the patched APK file to. Defaults to the same directory as the supplied APK file."],
|
||||
description = ["Path to save the patched APK file to. Defaults to the same path as the supplied APK file."],
|
||||
)
|
||||
@Suppress("unused")
|
||||
private fun setOutputFilePath(outputFilePath: File?) {
|
||||
@ -91,8 +116,8 @@ internal object PatchCommand : Runnable {
|
||||
}
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-d", "--device-serial"],
|
||||
description = ["ADB device serial to install to. If not supplied, the first connected device will be used."],
|
||||
names = ["-i", "--install"],
|
||||
description = ["Serial of the ADB device to install to. If not specified, the first connected device will be used."],
|
||||
// Empty string to indicate that the first connected device should be used.
|
||||
fallbackValue = "",
|
||||
arity = "0..1",
|
||||
@ -101,7 +126,7 @@ internal object PatchCommand : Runnable {
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--mount"],
|
||||
description = ["Install by mounting the patched APK file."],
|
||||
description = ["Install the patched APK file by mounting."],
|
||||
showDefaultValue = ALWAYS,
|
||||
)
|
||||
private var mount: Boolean = false
|
||||
@ -109,38 +134,28 @@ internal object PatchCommand : Runnable {
|
||||
@CommandLine.Option(
|
||||
names = ["--keystore"],
|
||||
description = [
|
||||
"Path to the keystore to sign the patched APK file with. " +
|
||||
"Path to the keystore file containing a private key and certificate pair 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
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--keystore-password"],
|
||||
description = ["The password of the keystore to sign the patched APK file with. Empty password by default."],
|
||||
description = ["Password of the keystore. Empty password by default."],
|
||||
)
|
||||
private var keyStorePassword: String? = null // Empty password by default
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--alias"],
|
||||
description = ["The alias of the keystore entry to sign the patched APK file with."],
|
||||
showDefaultValue = ALWAYS,
|
||||
)
|
||||
private fun setKeyStoreEntryAlias(alias: String = "ReVanced Key") {
|
||||
logger.warning("The --alias option is deprecated. Use --keystore-entry-alias instead.")
|
||||
keyStoreEntryAlias = alias
|
||||
}
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--keystore-entry-alias"],
|
||||
description = ["The alias of the keystore entry to sign the patched APK file with."],
|
||||
description = ["Alias of the private key and certificate pair keystore entry."],
|
||||
showDefaultValue = ALWAYS,
|
||||
)
|
||||
private var keyStoreEntryAlias = "ReVanced Key"
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--keystore-entry-password"],
|
||||
description = ["The password of the entry from the keystore for the key to sign the patched APK file with."],
|
||||
description = ["Password of the keystore entry."],
|
||||
)
|
||||
private var keyStoreEntryPassword = "" // Empty password by default
|
||||
|
||||
@ -153,29 +168,22 @@ internal object PatchCommand : Runnable {
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-t", "--temporary-files-path"],
|
||||
description = ["Path to temporary files directory."],
|
||||
description = ["Path to store temporary files."],
|
||||
)
|
||||
private var temporaryFilesPath: File? = null
|
||||
|
||||
private var aaptBinaryPath: File? = null
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-p", "--purge"],
|
||||
description = ["Purge the temporary resource cache directory after patching."],
|
||||
names = ["--purge"],
|
||||
description = ["Purge temporary files directory after patching."],
|
||||
showDefaultValue = ALWAYS,
|
||||
)
|
||||
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(
|
||||
description = ["APK file to be patched."],
|
||||
arity = "1..1",
|
||||
description = ["APK file to patch."],
|
||||
arity = "1",
|
||||
)
|
||||
@Suppress("unused")
|
||||
private fun setApk(apk: File) {
|
||||
@ -188,31 +196,23 @@ internal object PatchCommand : Runnable {
|
||||
this.apk = apk
|
||||
}
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-m", "--merge"],
|
||||
description = ["One or more DEX files or containers to merge into the APK."],
|
||||
)
|
||||
@Suppress("unused")
|
||||
private fun setIntegrations(integrations: Array<File>) {
|
||||
integrations.firstOrNull { !it.exists() }?.let {
|
||||
throw CommandLine.ParameterException(spec.commandLine(), "Integrations file ${it.path} does not exist.")
|
||||
}
|
||||
this.integrations += integrations
|
||||
}
|
||||
private lateinit var apk: File
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-b", "--patch-bundle"],
|
||||
description = ["One or more bundles of patches."],
|
||||
names = ["-p", "--patches"],
|
||||
description = ["One or more path to RVP files."],
|
||||
required = true,
|
||||
)
|
||||
@Suppress("unused")
|
||||
private fun setPatchBundles(patchBundles: Set<File>) {
|
||||
patchBundles.firstOrNull { !it.exists() }?.let {
|
||||
throw CommandLine.ParameterException(spec.commandLine(), "Patch bundle ${it.name} does not exist")
|
||||
private fun setPatchesFile(patchesFiles: Set<File>) {
|
||||
patchesFiles.firstOrNull { !it.exists() }?.let {
|
||||
throw CommandLine.ParameterException(spec.commandLine(), "${it.name} can't be found")
|
||||
}
|
||||
this.patchBundles = patchBundles
|
||||
this.patchesFiles = patchesFiles
|
||||
}
|
||||
|
||||
private var patchesFiles = emptySet<File>()
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--custom-aapt2-binary"],
|
||||
description = ["Path to a custom AAPT binary to compile resources with."],
|
||||
@ -241,79 +241,94 @@ internal object PatchCommand : Runnable {
|
||||
"${outputFilePath.nameWithoutExtension}-temporary-files",
|
||||
)
|
||||
|
||||
val optionsFile =
|
||||
optionsFile ?: outputFilePath.parentFile.resolve(
|
||||
"${outputFilePath.nameWithoutExtension}-options.json",
|
||||
)
|
||||
|
||||
val keystoreFilePath =
|
||||
keystoreFilePath ?: outputFilePath.parentFile
|
||||
keyStoreFilePath ?: outputFilePath.parentFile
|
||||
.resolve("${outputFilePath.nameWithoutExtension}.keystore")
|
||||
|
||||
val installer = if (deviceSerial != null) {
|
||||
val deviceSerial = deviceSerial!!.ifEmpty { null }
|
||||
|
||||
try {
|
||||
if (mount) {
|
||||
AdbRootInstaller(deviceSerial)
|
||||
} else {
|
||||
AdbInstaller(deviceSerial)
|
||||
}
|
||||
} catch (e: DeviceNotFoundException) {
|
||||
if (deviceSerial?.isNotEmpty() == true) {
|
||||
logger.severe(
|
||||
"Device with serial $deviceSerial not found to install to. " +
|
||||
"Ensure the device is connected and the serial is correct when using the --install option.",
|
||||
)
|
||||
} else {
|
||||
logger.severe(
|
||||
"No device has been found to install to. " +
|
||||
"Ensure a device is connected when using the --install option.",
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Load patches
|
||||
|
||||
logger.info("Loading patches")
|
||||
|
||||
val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray())
|
||||
|
||||
// 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")}")
|
||||
}
|
||||
}
|
||||
val patches = loadPatchesFromJar(patchesFiles)
|
||||
|
||||
// endregion
|
||||
|
||||
val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher")
|
||||
|
||||
val (packageName, patcherResult) = Patcher(
|
||||
PatcherConfig(
|
||||
apk,
|
||||
patcherTemporaryFilesPath,
|
||||
aaptBinaryPath?.path,
|
||||
patcherTemporaryFilesPath.absolutePath,
|
||||
true,
|
||||
),
|
||||
).use { patcher ->
|
||||
val filteredPatches =
|
||||
patcher.filterPatchSelection(patches).also { patches ->
|
||||
logger.info("Setting patch options")
|
||||
val packageName = patcher.context.packageMetadata.packageName
|
||||
val packageVersion = patcher.context.packageMetadata.packageVersion
|
||||
|
||||
if (optionsFile.exists()) {
|
||||
patches.setOptions(optionsFile)
|
||||
} else {
|
||||
Options.serialize(patches, prettyPrint = true).let(optionsFile::writeText)
|
||||
val filteredPatches = patches.filterPatchSelection(packageName, packageVersion)
|
||||
|
||||
logger.info("Setting patch options")
|
||||
|
||||
val patchesList = patches.toList()
|
||||
selection.filter { it.enabled != null }.associate {
|
||||
val enabledSelection = it.enabled!!
|
||||
|
||||
(enabledSelection.selector.name ?: patchesList[enabledSelection.selector.index!!].name!!) to
|
||||
enabledSelection.options
|
||||
}.let(filteredPatches::setOptions)
|
||||
|
||||
patcher += filteredPatches
|
||||
|
||||
// Execute patches.
|
||||
runBlocking {
|
||||
patcher().collect { patchResult ->
|
||||
val exception = patchResult.exception
|
||||
?: return@collect logger.info("\"${patchResult.patch}\" succeeded")
|
||||
|
||||
StringWriter().use { writer ->
|
||||
exception.printStackTrace(PrintWriter(writer))
|
||||
|
||||
logger.severe("\"${patchResult.patch}\" failed:\n$writer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region Patch
|
||||
|
||||
patcher.context.packageMetadata.packageName to patcher.apply {
|
||||
acceptIntegrations(integrations)
|
||||
acceptPatches(filteredPatches)
|
||||
|
||||
// Execute patches.
|
||||
runBlocking {
|
||||
apply(false).collect { patchResult ->
|
||||
patchResult.exception?.let {
|
||||
StringWriter().use { writer ->
|
||||
it.printStackTrace(PrintWriter(writer))
|
||||
logger.severe("${patchResult.patch.name} failed:\n$writer")
|
||||
}
|
||||
} ?: logger.info("${patchResult.patch.name} succeeded")
|
||||
}
|
||||
}
|
||||
}.get()
|
||||
// endregion
|
||||
patcher.context.packageMetadata.packageName to patcher.get()
|
||||
}
|
||||
|
||||
// region Save
|
||||
// region Save.
|
||||
|
||||
apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).apply {
|
||||
patcherResult.applyTo(this)
|
||||
}.let { patchedApkFile ->
|
||||
@ -338,11 +353,17 @@ internal object PatchCommand : Runnable {
|
||||
|
||||
// endregion
|
||||
|
||||
// region Install
|
||||
// region Install.
|
||||
|
||||
deviceSerial?.let { serial ->
|
||||
AdbManager.getAdbManager(deviceSerial = serial.ifEmpty { null }, mount)
|
||||
}?.install(AdbManager.Apk(outputFilePath, packageName))
|
||||
deviceSerial?.let {
|
||||
runBlocking {
|
||||
when (val result = installer!!.install(Installer.Apk(outputFilePath, packageName))) {
|
||||
RootInstallerResult.FAILURE -> logger.severe("Failed to mount the patched APK file")
|
||||
is AdbInstallerResult.Failure -> logger.severe(result.exception.toString())
|
||||
else -> logger.info("Installed the patched APK file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
@ -353,61 +374,72 @@ 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.
|
||||
*/
|
||||
private fun Patcher.filterPatchSelection(patches: PatchSet): PatchSet =
|
||||
buildSet {
|
||||
val packageName = context.packageMetadata.packageName
|
||||
val packageVersion = context.packageMetadata.packageVersion
|
||||
private fun Set<Patch<*>>.filterPatchSelection(
|
||||
packageName: String,
|
||||
packageVersion: String,
|
||||
): Set<Patch<*>> = buildSet {
|
||||
val enabledPatchesByName =
|
||||
selection.mapNotNull { it.enabled?.selector?.name }.toSet()
|
||||
val enabledPatchesByIndex =
|
||||
selection.mapNotNull { it.enabled?.selector?.index }.toSet()
|
||||
|
||||
patches.withIndex().forEach patch@{ (i, patch) ->
|
||||
val patchName = patch.name!!
|
||||
val disabledPatches =
|
||||
selection.mapNotNull { it.disable?.selector?.name }.toSet()
|
||||
val disabledPatchesByIndex =
|
||||
selection.mapNotNull { it.disable?.selector?.index }.toSet()
|
||||
|
||||
val explicitlyExcluded = excludedPatches.contains(patchName) || excludedPatchesByIndex.contains(i)
|
||||
if (explicitlyExcluded) return@patch logger.info("Excluding $patchName")
|
||||
this@filterPatchSelection.withIndex().forEach patchLoop@{ (i, patch) ->
|
||||
val patchName = patch.name!!
|
||||
|
||||
// Make sure the patch is compatible with the supplied APK files package name and version.
|
||||
patch.compatiblePackages?.let { packages ->
|
||||
packages.singleOrNull { it.name == packageName }?.let { `package` ->
|
||||
val matchesVersion =
|
||||
force || `package`.versions?.let {
|
||||
it.any { version -> version == packageVersion }
|
||||
} ?: true
|
||||
val isManuallyDisabled = patchName in disabledPatches || i in disabledPatchesByIndex
|
||||
if (isManuallyDisabled) return@patchLoop logger.info("\"$patchName\" disabled manually")
|
||||
|
||||
if (!matchesVersion) {
|
||||
return@patch logger.warning(
|
||||
"$patchName is incompatible with version $packageVersion. " +
|
||||
"This patch is only compatible with version " +
|
||||
packages.joinToString(";") { pkg ->
|
||||
pkg.versions!!.joinToString(", ")
|
||||
},
|
||||
)
|
||||
}
|
||||
} ?: return@patch logger.fine(
|
||||
"$patchName is incompatible with $packageName. " +
|
||||
"This patch is only compatible with " +
|
||||
packages.joinToString(", ") { `package` -> `package`.name },
|
||||
)
|
||||
// Make sure the patch is compatible with the supplied APK files package name and version.
|
||||
patch.compatiblePackages?.let { packages ->
|
||||
packages.singleOrNull { (name, _) -> name == packageName }?.let { (_, versions) ->
|
||||
if (versions?.isEmpty() == true) {
|
||||
return@patchLoop logger.warning("\"$patchName\" incompatible with \"$packageName\"")
|
||||
}
|
||||
|
||||
return@let
|
||||
} ?: logger.fine("$patchName has no constraint on packages.")
|
||||
val matchesVersion =
|
||||
force || versions?.let { it.any { version -> version == packageVersion } } ?: true
|
||||
|
||||
// If the patch is implicitly used, it will be only included if [exclusive] is false.
|
||||
val implicitlyIncluded = !exclusive && patch.use
|
||||
// If the patch is explicitly used, it will be included even if [exclusive] is false.
|
||||
val explicitlyIncluded = includedPatches.contains(patchName) || includedPatchesByIndex.contains(i)
|
||||
if (!matchesVersion) {
|
||||
return@patchLoop logger.warning(
|
||||
"\"$patchName\" incompatible with $packageName $packageVersion " +
|
||||
"but compatible with " +
|
||||
packages.joinToString("; ") { (packageName, versions) ->
|
||||
packageName + " " + versions!!.joinToString(", ")
|
||||
},
|
||||
)
|
||||
}
|
||||
} ?: return@patchLoop logger.fine(
|
||||
"\"$patchName\" incompatible with $packageName. " +
|
||||
"It is only compatible with " +
|
||||
packages.joinToString(", ") { (name, _) -> name },
|
||||
)
|
||||
|
||||
val included = implicitlyIncluded || explicitlyIncluded
|
||||
if (!included) return@patch logger.info("$patchName excluded") // Case 1.
|
||||
return@let
|
||||
} ?: logger.fine("\"$patchName\" has no package constraints")
|
||||
|
||||
logger.fine("Adding $patchName")
|
||||
val isEnabled = !exclusive && patch.use
|
||||
val isManuallyEnabled = patchName in enabledPatchesByName || i in enabledPatchesByIndex
|
||||
|
||||
add(patch)
|
||||
if (!(isEnabled || isManuallyEnabled)) {
|
||||
return@patchLoop logger.info("\"$patchName\" disabled")
|
||||
}
|
||||
|
||||
add(patch)
|
||||
|
||||
logger.fine("\"$patchName\" added")
|
||||
}
|
||||
}
|
||||
|
||||
private fun purge(resourceCachePath: File) {
|
||||
val result =
|
||||
|
@ -1,44 +1,63 @@
|
||||
package app.revanced.cli.command.utility
|
||||
|
||||
import app.revanced.library.adb.AdbManager
|
||||
import app.revanced.library.installation.installer.*
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import picocli.CommandLine.*
|
||||
import java.io.File
|
||||
import java.util.logging.Logger
|
||||
|
||||
@Command(
|
||||
name = "install",
|
||||
description = ["Install an APK file to devices with the supplied ADB device serials"],
|
||||
description = ["Install an APK file."],
|
||||
)
|
||||
internal object InstallCommand : Runnable {
|
||||
private val logger = Logger.getLogger(InstallCommand::class.java.name)
|
||||
private val logger = Logger.getLogger(this::class.java.name)
|
||||
|
||||
@Parameters(
|
||||
description = ["ADB device serials. If not supplied, the first connected device will be used."],
|
||||
description = ["Serial of ADB devices. If not supplied, the first connected device will be used."],
|
||||
arity = "0..*",
|
||||
)
|
||||
private var deviceSerials: Array<String>? = null
|
||||
|
||||
@Option(
|
||||
names = ["-a", "--apk"],
|
||||
description = ["APK file to be installed"],
|
||||
description = ["APK file to be installed."],
|
||||
required = true,
|
||||
)
|
||||
private lateinit var apk: File
|
||||
|
||||
@Option(
|
||||
names = ["-m", "--mount"],
|
||||
description = ["Mount the supplied APK file over the app with the supplied package name"],
|
||||
description = ["Mount the supplied APK file over the app with the supplied package name."],
|
||||
)
|
||||
private var packageName: String? = null
|
||||
|
||||
override fun run() {
|
||||
fun install(deviceSerial: String? = null) =
|
||||
try {
|
||||
AdbManager.getAdbManager(deviceSerial, packageName != null).install(AdbManager.Apk(apk, packageName))
|
||||
} catch (e: AdbManager.DeviceNotFoundException) {
|
||||
suspend fun install(deviceSerial: String? = null) {
|
||||
val result = try {
|
||||
if (packageName != null) {
|
||||
AdbRootInstaller(deviceSerial)
|
||||
} else {
|
||||
AdbInstaller(deviceSerial)
|
||||
}.install(Installer.Apk(apk, packageName))
|
||||
} catch (e: Exception) {
|
||||
logger.severe(e.toString())
|
||||
}
|
||||
|
||||
deviceSerials?.forEach(::install) ?: install()
|
||||
when (result) {
|
||||
RootInstallerResult.FAILURE ->
|
||||
logger.severe("Failed to mount the APK file")
|
||||
is AdbInstallerResult.Failure ->
|
||||
logger.severe(result.exception.toString())
|
||||
else ->
|
||||
logger.info("Installed the APK file")
|
||||
}
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
deviceSerials?.map { async { install(it) } }?.awaitAll() ?: install()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +1,66 @@
|
||||
package app.revanced.cli.command.utility
|
||||
|
||||
import app.revanced.library.adb.AdbManager
|
||||
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.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import picocli.CommandLine.*
|
||||
import picocli.CommandLine.Help.Visibility.ALWAYS
|
||||
import java.util.logging.Logger
|
||||
|
||||
@Command(
|
||||
name = "uninstall",
|
||||
description = ["Uninstall a patched app from the devices with the supplied ADB device serials"],
|
||||
description = ["Uninstall a patched app."],
|
||||
)
|
||||
internal object UninstallCommand : Runnable {
|
||||
private val logger = Logger.getLogger(UninstallCommand::class.java.name)
|
||||
private val logger = Logger.getLogger(this::class.java.name)
|
||||
|
||||
@Parameters(
|
||||
description = ["ADB device serials. If not supplied, the first connected device will be used."],
|
||||
description = ["Serial of ADB devices. 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"],
|
||||
description = ["Package name of the app to uninstall."],
|
||||
required = true,
|
||||
)
|
||||
private lateinit var packageName: String
|
||||
|
||||
@Option(
|
||||
names = ["-u", "--unmount"],
|
||||
description = ["Uninstall by unmounting the patched APK file"],
|
||||
description = ["Uninstall the patched APK file by unmounting."],
|
||||
showDefaultValue = ALWAYS,
|
||||
)
|
||||
private var unmount: Boolean = false
|
||||
|
||||
override fun run() {
|
||||
fun uninstall(deviceSerial: String? = null) =
|
||||
try {
|
||||
AdbManager.getAdbManager(deviceSerial, unmount).uninstall(packageName)
|
||||
} catch (e: AdbManager.DeviceNotFoundException) {
|
||||
suspend fun uninstall(deviceSerial: String? = null) {
|
||||
val result = try {
|
||||
if (unmount) {
|
||||
AdbRootInstaller(deviceSerial)
|
||||
} else {
|
||||
AdbInstaller(deviceSerial)
|
||||
}.uninstall(packageName)
|
||||
} catch (e: Exception) {
|
||||
logger.severe(e.toString())
|
||||
}
|
||||
|
||||
deviceSerials?.forEach { uninstall(it) } ?: uninstall()
|
||||
when (result) {
|
||||
RootInstallerResult.FAILURE ->
|
||||
logger.severe("Failed to unmount the patched APK file")
|
||||
is AdbInstallerResult.Failure ->
|
||||
logger.severe(result.exception.toString())
|
||||
else -> logger.info("Uninstalled the patched APK file")
|
||||
}
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
deviceSerials?.map { async { uninstall(it) } }?.awaitAll() ?: uninstall()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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…
x
Reference in New Issue
Block a user