Compare commits

..

1 Commits

Author SHA1 Message Date
oSumAtrIX
c335850cc5 build(Needs bump): Bump dependencies 2024-02-11 21:47:15 +01:00
33 changed files with 1712 additions and 3152 deletions

View File

@@ -70,7 +70,7 @@ body:
Before creating a new bug report, please keep the following in mind:
- **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).
- **Do not submit a duplicate bug report**: You can review existing bug reports [here](https://github.com/ReVanced/revanced-cli/labels/Bug%20report).
- **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: I have checked all open and closed bug reports and this is not a duplicate.
- label: This issue is not a duplicate of an existing bug report.
required: true
- label: I have chosen an appropriate title.
required: true

View File

@@ -70,8 +70,8 @@ body:
Before creating a new feature request, please keep the following in mind:
- **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 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 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: I have checked all open and closed feature requests and this is not a duplicate.
- label: This issue is not a duplicate of an existing feature request.
required: true
- label: I have chosen an appropriate title.
required: true

View File

@@ -1,23 +0,0 @@
name: Build pull request
on:
workflow_dispatch:
pull_request:
branches:
- dev
jobs:
release:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
- name: Build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./gradlew build --no-daemon

View File

@@ -6,18 +6,23 @@ on:
branches:
- main
- dev
pull_request:
branches:
- main
- dev
jobs:
release:
name: Release
permissions:
contents: write
id-token: write
attestations: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false
fetch-depth: 0
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
@@ -36,22 +41,7 @@ jobs:
- name: Install dependencies
run: npm install
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v6
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Release
uses: cycjimmy/semantic-release-action@v4
id: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Attest
if: steps.release.outputs.new_release_published == 'true'
uses: actions/attest-build-provenance@v2
with:
subject-name: 'ReVanced CLI ${{ steps.release.outputs.new_release_git_tag }}'
subject-path: build/libs/revanced-cli*.jar
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }}
run: npm exec semantic-release

View File

@@ -11,7 +11,7 @@ jobs:
name: Dispatch event to documentation repository
if: github.ref == 'refs/heads/main'
steps:
- uses: peter-evans/repository-dispatch@v3
- uses: peter-evans/repository-dispatch@v2
with:
token: ${{ secrets.DOCUMENTATION_REPO_ACCESS_TOKEN }}
repository: revanced/revanced-documentation

2
.gitignore vendored
View File

@@ -121,5 +121,5 @@ node_modules/
revanced-cache/
options.toml
# Generated by Android projects
# Generated by an Android project (such as ReVanced Integrations)
local.properties

View File

@@ -23,8 +23,7 @@
"assets": [
"CHANGELOG.md",
"gradle.properties"
],
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
]
}
],
[
@@ -32,7 +31,7 @@
{
"assets": [
{
"path": "build/libs/*-all*"
"path": "build/libs/*all.jar"
}
],
successComment: false

View File

@@ -1,221 +1,3 @@
## [5.0.2-dev.2](https://github.com/ReVanced/revanced-cli/compare/v5.0.2-dev.1...v5.0.2-dev.2) (2025-04-25)
### Bug Fixes
* Do not print patch description if null ([bba90fe](https://github.com/ReVanced/revanced-cli/commit/bba90fede85e4632efb9509e5bcf9091a9435549))
## [5.0.2-dev.1](https://github.com/ReVanced/revanced-cli/compare/v5.0.1...v5.0.2-dev.1) (2025-04-20)
### Bug Fixes
* Group `mount` and `install` options into an argument group ([#364](https://github.com/ReVanced/revanced-cli/issues/364)) ([0c53a2d](https://github.com/ReVanced/revanced-cli/commit/0c53a2d1d75d3d934d134594751fe6cd0b000d1a))
## [5.0.1](https://github.com/ReVanced/revanced-cli/compare/v5.0.0...v5.0.1) (2025-04-14)
### Bug Fixes
* Make mounting work again by bumping dependencies ([#359](https://github.com/ReVanced/revanced-cli/issues/359)) ([68a4872](https://github.com/ReVanced/revanced-cli/commit/68a48724ebf01a0c8f8adc0fec63037bff672dc9))
## [5.0.1-dev.1](https://github.com/ReVanced/revanced-cli/compare/v5.0.0...v5.0.1-dev.1) (2025-02-26)
### Bug Fixes
* Make mounting work again by bumping dependencies ([#359](https://github.com/ReVanced/revanced-cli/issues/359)) ([68a4872](https://github.com/ReVanced/revanced-cli/commit/68a48724ebf01a0c8f8adc0fec63037bff672dc9))
# [5.0.0](https://github.com/ReVanced/revanced-cli/compare/v4.6.0...v5.0.0) (2024-11-10)
### Bug Fixes
* Check for null when no device serial was specified ([1da8ae9](https://github.com/ReVanced/revanced-cli/commit/1da8ae9e46000dd3c4eecd793c559e75012cf535))
* List if patch option is required ([#346](https://github.com/ReVanced/revanced-cli/issues/346)) ([98ff0c3](https://github.com/ReVanced/revanced-cli/commit/98ff0c34fa71c3b3ecd96157d45a30ee2b8979c6))
* 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))
* 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))
* Print in new line correctly ([c2dc9d7](https://github.com/ReVanced/revanced-cli/commit/c2dc9d76be33c98284741e23c406500483c47753))
* Use the first connected device when no ADB device is specified ([5f952f3](https://github.com/ReVanced/revanced-cli/commit/5f952f35f5cb388b6509b2b4d905b8143ebc7996))
### Features
* Set patch options via CLI ([#336](https://github.com/ReVanced/revanced-cli/issues/336)) ([2300243](https://github.com/ReVanced/revanced-cli/commit/23002434b2d51c2a3b30b33dd0526261432d90ce))
* Show error about no installation device found at the beginning ([3300e6b](https://github.com/ReVanced/revanced-cli/commit/3300e6b4333ed1c4e6785cb82eca9016fc6d4a20))
* 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))
* Simplify option descriptions ([45c998b](https://github.com/ReVanced/revanced-cli/commit/45c998b335b65ac233fece8b804dc7410142691c))
### BREAKING CHANGES
* Options have been renamed.
* This commit changes various CLI options and removes the `options.json` file. Instead, patch options can now be passed via CLI options
# [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)
### Bug Fixes
* Copy APK to output path when it is not being signed ([366f400](https://github.com/ReVanced/revanced-cli/commit/366f400c5a46491f3f262c7ff4b0df1ae3721f74))
* Use correct option description ([45a2ffa](https://github.com/ReVanced/revanced-cli/commit/45a2ffa2dd95ee8ac3c4d466463c9a5b869b8da1))
### Features
* Use more consistent option name ([223629c](https://github.com/ReVanced/revanced-cli/commit/223629c663dcd94d237110e09e4e152aa03867f9))
# [4.6.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.5.1-dev.1...v4.6.0-dev.1) (2024-03-14)
### Bug Fixes
* Use correct option description ([45a2ffa](https://github.com/ReVanced/revanced-cli/commit/45a2ffa2dd95ee8ac3c4d466463c9a5b869b8da1))
### Features
* Use more consistent option name ([223629c](https://github.com/ReVanced/revanced-cli/commit/223629c663dcd94d237110e09e4e152aa03867f9))
## [4.5.1-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.5.0...v4.5.1-dev.1) (2024-03-12)
### Bug Fixes
* Copy APK to output path when it is not being signed ([366f400](https://github.com/ReVanced/revanced-cli/commit/366f400c5a46491f3f262c7ff4b0df1ae3721f74))
# [4.5.0](https://github.com/ReVanced/revanced-cli/compare/v4.4.2...v4.5.0) (2024-03-11)
### Bug Fixes
* Show path for missing files instead of just the name ([f0f3e56](https://github.com/ReVanced/revanced-cli/commit/f0f3e5614b99b34391e0492177706f9c09781cad))
* Sign APKs correctly ([5ff105c](https://github.com/ReVanced/revanced-cli/commit/5ff105cf6b3fac9cd12478efd10caf90d1ecf589))
### Features
* Remove deprecated CLI options ([48a1a39](https://github.com/ReVanced/revanced-cli/commit/48a1a39b94dd9121c400d28e3e93dec3fc13e3be))
# [4.5.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.4.2...v4.5.0-dev.1) (2024-03-11)
### Bug Fixes
* Show path for missing files instead of just the name ([f0f3e56](https://github.com/ReVanced/revanced-cli/commit/f0f3e5614b99b34391e0492177706f9c09781cad))
* Sign APKs correctly ([5ff105c](https://github.com/ReVanced/revanced-cli/commit/5ff105cf6b3fac9cd12478efd10caf90d1ecf589))
### Features
* Remove deprecated CLI options ([48a1a39](https://github.com/ReVanced/revanced-cli/commit/48a1a39b94dd9121c400d28e3e93dec3fc13e3be))
## [4.4.2](https://github.com/ReVanced/revanced-cli/compare/v4.4.1...v4.4.2) (2024-03-10)
## [4.4.2-dev.2](https://github.com/ReVanced/revanced-cli/compare/v4.4.2-dev.1...v4.4.2-dev.2) (2024-03-10)
## [4.4.2-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.4.1...v4.4.2-dev.1) (2024-03-09)
## [4.4.1](https://github.com/ReVanced/revanced-cli/compare/v4.4.0...v4.4.1) (2024-03-06)
### Bug Fixes
* Bump dependencies to support BCS keystore ([1c10a77](https://github.com/ReVanced/revanced-cli/commit/1c10a7760d76ea850260ca49b448be7ad121de44))
## [4.4.1-dev.2](https://github.com/ReVanced/revanced-cli/compare/v4.4.1-dev.1...v4.4.1-dev.2) (2024-03-04)
### Bug Fixes
* Bump dependencies to support BCS keystore ([1c10a77](https://github.com/ReVanced/revanced-cli/commit/1c10a7760d76ea850260ca49b448be7ad121de44))
## [4.4.1-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.4.0...v4.4.1-dev.1) (2024-02-21)
# [4.4.0](https://github.com/ReVanced/revanced-cli/compare/v4.3.0...v4.4.0) (2023-12-28)

View File

@@ -64,14 +64,14 @@ This document describes how to contribute to ReVanced CLI.
## 📖 Resources to help you get started
- The [documentation](/docs) explains how to use ReVanced CLI
- [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
- [Issues](https://github.com/ReVanced/revanced-cli/issues) are where we keep track of bugs and feature requests
* The [documentation](/docs) explains how to use ReVanced CLI
* [Our backlog](https://github.com/orgs/ReVanced/projects/12) is where we keep track of what we're working on
* [Issues](https://github.com/ReVanced/revanced-cli/issues) are where we keep track of bugs and feature requests
## 🙏 Submitting a feature request
Features can be requested by opening an issue using the
[Feature request issue template](https://github.com/ReVanced/revanced-cli/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+).
[Feature request issue template](https://github.com/ReVanced/revanced-cli/issues/new?assignees=&labels=Feature+request&projects=&template=feature-request.yml&title=feat%3A+).
> **Note**
> Requests can be accepted or rejected at the discretion of maintainers of ReVanced CLI.
@@ -80,7 +80,7 @@ Features can be requested by opening an issue using the
## 🐞 Submitting a bug report
If you encounter a bug while using ReVanced CLI, open an issue using the
[Bug report issue template](https://github.com/ReVanced/revanced-cli/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+).
[Bug report issue template](https://github.com/ReVanced/revanced-cli/issues/new?assignees=&labels=Bug+report&projects=&template=bug-report.yml&title=bug%3A+).
## 📝 How to contribute
@@ -88,11 +88,11 @@ If you encounter a bug while using ReVanced CLI, open an issue using the
with the maintainers of ReVanced CLI. This will help you determine whether your change is acceptable
and whether it is worth your time to implement it
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
3. Commit your changes
3. Commit your changes.
4. Submit a pull request to the `dev` branch of the repository and reference issues
that your pull request closes in the description of your pull request
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
it will be merged into the `dev` branch and will be included in the next release of ReVanced CLI
❤️ Thank you for considering contributing to ReVanced CLI,
❤️ Thank you for considering contributing to ReVanced CLI,
ReVanced

View File

@@ -63,27 +63,17 @@
![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 to patch apps using ReVanced.
ReVanced CLI also comes with commands to uninstall or install patched apps and list patches from supplied patch bundles.
## 💪 Features
## 🚀 Download
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
- 📃 **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
## 🔽 Download
You can download the most recent version of ReVanced CLI from
[here](https://github.com/ReVanced/revanced-cli/releases/latest).
Learn how to use ReVanced CLI by following the [documentation](/docs).
You can download the most recent version of ReVanced CLI from
[here](https://github.com/ReVanced/revanced-cli/releases/latest). Learn how to use ReVanced CLI by following the [documentation](/docs).
## 📚 Everything else
@@ -94,14 +84,10 @@ You can find the contribution guidelines [here](CONTRIBUTING.md).
### 🛠️ Building
To build a ReVanced CLI, you can follow the [documentation](/docs).
In order to build ReVanced CLI, you can follow the [documentation](/docs).
### 📃 Documentation
## 📜 Licence
You can find the documentation of ReVanced CLI [here](/docs).
## 📜 License
ReVanced CLI is licensed under the GPLv3 license. Please see the [license file](LICENSE) for more information.
ReVanced CLI is licensed under the GPLv3 licence. Please see the [licence file](LICENSE) for more information.
[tl;dr](https://www.tldrlegal.com/license/gnu-general-public-license-v3-gpl-3) you may copy, distribute and modify ReVanced CLI as long as you track changes/dates in source files.
Any modifications to ReVanced CLI must also be made available under the GPL, along with build & install instructions.
Any modifications to ReVanced CLI must also be made available under the GPL along with build & install instructions.

View File

@@ -1,30 +1,15 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlin)
kotlin("jvm") version "1.9.10"
alias(libs.plugins.shadow)
application
`maven-publish`
signing
}
group = "app.revanced"
application {
mainClass = "app.revanced.cli.command.MainCommandKt"
}
repositories {
mavenCentral()
mavenLocal()
google()
maven {
// 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")
password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN")
}
}
maven { url = uri("https://jitpack.io") }
}
dependencies {
@@ -36,15 +21,7 @@ dependencies {
testImplementation(libs.kotlin.test)
}
kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
java {
targetCompatibility = JavaVersion.VERSION_11
}
kotlin { jvmToolchain(11) }
tasks {
test {
@@ -59,36 +36,35 @@ tasks {
}
shadowJar {
exclude("/prebuilt/linux/aapt", "/prebuilt/windows/aapt.exe", "/prebuilt/*/aapt_*")
manifest {
attributes("Main-Class" to "app.revanced.cli.command.MainCommandKt")
}
minimize {
exclude(dependency("org.jetbrains.kotlin:.*"))
exclude(dependency("org.bouncycastle:.*"))
exclude(dependency("app.revanced:revanced-patcher"))
exclude(dependency("app.revanced:.*"))
}
}
publish {
build {
dependsOn(shadowJar)
}
}
// Needed by gradle-semantic-release-plugin.
// Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
/*
Dummy task to hack gradle-semantic-release-plugin to release this project.
// The maven-publish is also necessary to make the signing plugin work.
publishing {
repositories {
mavenLocal()
}
Explanation:
SemVer is a standard for versioning libraries.
For that reason the semantic-release plugin uses the "publish" task to publish libraries.
However, this subproject is not a library, and the "publish" task is not available for this subproject.
Because semantic-release is not designed to handle this case, we need to hack it.
publications {
create<MavenPublication>("revanced-cli-publication") {
from(components["java"])
}
RE: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
*/
register<DefaultTask>("publish") {
group = "publishing"
description = "Dummy task to hack gradle-semantic-release-plugin to release ReVanced CLI"
dependsOn(build)
}
}
signing {
useGpgCmd()
sign(publishing.publications["revanced-cli-publication"])
}

View File

@@ -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=jre#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=jdk#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
- 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)
- An ABI other than ARMv7 such as x86 or x86-64 (or a custom AAPT binary that supports ARMv7)
## ⏭️ Whats next

View File

@@ -1,171 +1,130 @@
# 🛠️ 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.
## 🚀 Show all commands
## 🔨 Usage
```bash
java -jar revanced-cli.jar -h
```
ReVanced CLI is divided into the following fundamental commands:
## 📃 List patches
- ### 🚀 Show all available options for ReVanced CLI
```bash
java -jar revanced-cli.jar list-patches --with-packages --with-versions --with-options patches.rvp
```
```bash
java -jar revanced-cli.jar -h
```
## 💉 Patch an app
- ### 📃 List patches
To patch an app using the default list of patches, use the `patch` command:
```bash
java -jar revanced-cli.jar list-patches \
--with-packages \
--with-versions \
--with-options \
revanced-patches.jar [<patch-bundle> ...]
```
```bash
java -jar revanced-cli.jar patch -p patches.rvp input.apk
```
- ### ⚙️ Generate options
You can also use multiple RVP files:
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.
```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:
> **💡 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
> ```
>
```bash
java -jar revanced-cli.jar patch -p patches.rvp --exclusive -e "Patch name" -e "Another patch name" input.apk
```
> **⚠️ 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.
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`:
- #### 👾 Patch an app and install it on your device regularly
```bash
java -jar revanced-cli.jar list-patches patches.rvp
```
```bash
java -jar revanced-cli.jar patch \
--patch-bundle revanced-patches.jar \
-d \
input.apk
```
Then you can use the indices to enable or disable patches:
- #### 👾 Patch an app and mount it on top of the un-patched app with root permissions
> **❗ 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 --ei 123 --di 456 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.
You can combine the option `-e`, `-d`, `--ei`, `--di` and `--exclusive`. Here is an example:
- ### 🗑️ Uninstall an app
```bash
java -jar revanced-cli.jar patch -p patches.rvp --exclusive -e "Patch name" --ei 123 input.apk
```
```bash
java -jar revanced-cli.jar utility uninstall \
--package-name <package-name> \
[<device-serial>]
```
> **💡 Tip**
> You can unmount an APK file
by adding the option `--unmount`.
> [!TIP]
> You can use the option `-i` to automatically install the patched app after patching.
> Make sure ADB is working:
>
> ```bash
> adb shell exit
> ```
- ### 📦 Install an app
```bash
java -jar revanced-cli.jar utility install \
-a input.apk \
[<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
> ```
Patches can have options you can set using the option `-O` alongside the option to include the patch by name or index.
To know the options of a patch, use the option `--with-options` when listing patches:
```bash
java -jar revanced-cli.jar list-patches --with-options patches.rvp
```
Each patch can have multiple options. You can set them using the option `-O`.
For example, to set the options for the patch with the name `Patch name`
with the key `key1` and `key2` to `value1` and `value2` respectively, use the following command:
```bash
java -jar revanced-cli.jar patch -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> ...]
> ```
> **💡 Tip**
> You can mount an APK file
> by supplying the app's package name to mount the supplied APK file over the option `-mount`.

View File

@@ -21,17 +21,6 @@ To build ReVanced CLI, follow these steps:
```bash
./gradlew build
```
> [!NOTE]
> If the build fails due to authentication, you may need to authenticate to GitHub Packages.
> Create a PAT with the scope `read:packages` [here](https://github.com/settings/tokens/new?scopes=read:packages&description=ReVanced) and add your token to ~/.gradle/gradle.properties.
>
> Example `gradle.properties` file:
>
> ```properties
> gpr.user = user
> gpr.key = key
> ```
```
After the build succeeds, the built JAR file will be located at `build/libs/revanced-cli-<version>-all.jar`.

View File

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

View File

@@ -1,18 +1,17 @@
[versions]
shadow = "8.1.1"
kotlin = "2.0.20"
kotlinx = "1.8.1"
picocli = "4.7.6"
revanced-patcher = "21.0.0"
revanced-library = "3.1.0"
kotlin = "1.9.22"
kotlinx-coroutines-core = "1.7.3"
picocli = "4.7.3"
revanced-patcher = "19.3.0"
revanced-library = "2.0.0"
[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" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
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-jvm", version.ref = "revanced-library" }
revanced-library = { module = "app.revanced:revanced-library", version.ref = "revanced-library" }
[plugins]
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

Binary file not shown.

View File

@@ -1,8 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStorePath=wrapper/dist

17
gradlew vendored
View File

@@ -83,8 +83,7 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# 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
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -145,7 +144,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=SC2039,SC3045
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -153,7 +152,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=SC2039,SC3045
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -202,11 +201,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, 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.
# 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.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \

20
gradlew.bat vendored
View File

@@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
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
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.
goto fail
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
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
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.
goto fail

3178
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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.10.1",
"semantic-release": "^24.1.2"
"gradle-semantic-release-plugin": "^1.9.1",
"semantic-release": "^23.0.0"
}
}

View File

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

View File

@@ -1,105 +0,0 @@
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
}
}
}

View File

@@ -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.library.mostCommonCompatibleVersions
import app.revanced.patcher.patch.loadPatchesFromJar
import app.revanced.patcher.PatchBundleLoader
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 from RVP files.",
"with the patches in the supplied patch bundles.",
],
)
internal class ListCompatibleVersions : Runnable {
private val logger = Logger.getLogger(this::class.java.name)
private val logger = Logger.getLogger(ListCompatibleVersions::class.java.name)
@CommandLine.Parameters(
description = ["Paths to RVP files."],
description = ["Paths to patch bundles."],
arity = "1..*",
)
private lateinit var patchesFiles: Set<File>
private lateinit var patchBundles: Array<File>
@CommandLine.Option(
names = ["-f", "--filter-package-names"],
@@ -38,6 +38,8 @@ 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"
@@ -56,9 +58,8 @@ internal class ListCompatibleVersions : Runnable {
appendLine(versions.buildVersionsString().prependIndent("\t"))
}
val patches = loadPatchesFromJar(patchesFiles)
patches.mostCommonCompatibleVersions(
PatchUtils.getMostCommonCompatibleVersions(
patches,
packageNames,
countUnusedPatches,
).entries.joinToString("\n", transform = ::buildString).let(logger::info)

View File

@@ -1,26 +1,25 @@
package app.revanced.cli.command
import app.revanced.patcher.patch.Package
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.loadPatchesFromJar
import app.revanced.patcher.patch.options.PatchOption
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 RVP files."],
description = ["List patches from supplied patch bundles."],
)
internal object ListPatchesCommand : Runnable {
private val logger = Logger.getLogger(this::class.java.name)
private val logger = Logger.getLogger(ListPatchesCommand::class.java.name)
@Parameters(
description = ["Paths to RVP files."],
description = ["Paths to patch bundles."],
arity = "1..*",
)
private lateinit var patchesFiles: Set<File>
private lateinit var patchBundles: Array<File>
@Option(
names = ["-d", "--with-descriptions"],
@@ -59,7 +58,7 @@ internal object ListPatchesCommand : Runnable {
@Option(
names = ["-i", "--index"],
description = ["List the index of each patch in relation to the supplied RVP files."],
description = ["List the index of each patch in relation to the supplied patch bundles."],
showDefaultValue = ALWAYS,
)
private var withIndex: Boolean = true
@@ -71,71 +70,66 @@ internal object ListPatchesCommand : Runnable {
private var packageName: String? = null
override fun run() {
fun Package.buildString(): String {
val (name, versions) = this
return buildString {
fun Patch.CompatiblePackage.buildString() =
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("\nPossible values:")
append(values.map { "${it.value} (${it.key})" }.joinToString("\n").prependIndent("\t"))
}
append("\nType: $type")
}
fun IndexedValue<Patch<*>>.buildString() = let { (index, patch) ->
fun PatchOption<*>.buildString() =
buildString {
if (withIndex) appendLine("Index: $index")
appendLine("Title: $title")
description?.let { appendLine("Description: $it") }
default?.let {
appendLine("Key: $key")
append("Default: $it")
} ?: append("Key: $key")
append("Name: ${patch.name}")
if (withDescriptions) patch.description?.let { append("\nDescription: $it") }
append("\nEnabled: ${patch.use}")
if (withOptions && patch.options.isNotEmpty()) {
appendLine("\nOptions:")
append(
patch.options.values.joinToString("\n\n") { option ->
option.buildString()
}.prependIndent("\t"),
)
}
if (withPackages && patch.compatiblePackages != null) {
appendLine("\nCompatible packages:")
append(
patch.compatiblePackages!!.joinToString("\n") {
it.buildString()
}.prependIndent("\t"),
)
values?.let { values ->
appendLine("\nValid values:")
append(values.map { "${it.value} (${it.key})" }.joinToString("\n").prependIndent("\t"))
}
}
}
fun Patch<*>.filterCompatiblePackages(name: String) = compatiblePackages?.any { (compatiblePackageName, _) -> compatiblePackageName == name }
?: withUniversalPatches
fun IndexedValue<Patch<*>>.buildString() =
let { (index, patch) ->
buildString {
if (withIndex) appendLine("Index: $index")
val patches = loadPatchesFromJar(patchesFiles).withIndex().toList()
append("Name: ${patch.name}")
if (withDescriptions) append("\nDescription: ${patch.description}")
if (withOptions && patch.options.isNotEmpty()) {
appendLine("\nOptions:")
append(
patch.options.values.joinToString("\n\n") { option ->
option.buildString()
}.prependIndent("\t"),
)
}
if (withPackages && patch.compatiblePackages != null) {
appendLine("\nCompatible packages:")
append(
patch.compatiblePackages!!.joinToString("\n") {
it.buildString()
}.prependIndent("\t"),
)
}
}
}
fun Patch<*>.filterCompatiblePackages(name: String) =
compatiblePackages?.any { it.name == name }
?: withUniversalPatches
val patches = PatchBundleLoader.Jar(*patchBundles).withIndex().toList()
val filtered =
packageName?.let { patches.filter { (_, patch) -> patch.filterCompatiblePackages(it) } } ?: patches

View File

@@ -34,6 +34,7 @@ private object CLIVersionProvider : IVersionProvider {
versionProvider = CLIVersionProvider::class,
subcommands = [
PatchCommand::class,
OptionsCommand::class,
ListPatchesCommand::class,
ListCompatibleVersions::class,
UtilityCommand::class,

View File

@@ -0,0 +1,62 @@
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()
}

View File

@@ -2,15 +2,16 @@ package app.revanced.cli.command
import app.revanced.library.ApkUtils
import app.revanced.library.ApkUtils.applyTo
import app.revanced.library.installation.installer.*
import app.revanced.library.setOptions
import app.revanced.library.ApkUtils.sign
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.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,82 +25,57 @@ import java.util.logging.Logger
description = ["Patch an APK file."],
)
internal object PatchCommand : Runnable {
private val logger = Logger.getLogger(this::class.java.name)
private val logger = Logger.getLogger(PatchCommand::class.java.name)
@Spec
private lateinit var spec: CommandSpec
lateinit var spec: CommandSpec // injected by picocli
@ArgGroup(exclusive = false, multiplicity = "0..*")
private var selection = mutableSetOf<Selection>()
private lateinit var apk: File
internal class Selection {
@ArgGroup(exclusive = false)
internal var enabled: EnableSelection? = null
private var integrations = setOf<File>()
internal class EnableSelection {
@ArgGroup(multiplicity = "1")
internal lateinit var selector: EnableSelector
private var patchBundles = emptySet<File>()
internal class EnableSelector {
@CommandLine.Option(
names = ["-e", "--enable"],
description = ["Name of the patch."],
required = true,
)
internal var name: String? = null
@CommandLine.Option(
names = ["-i", "--include"],
description = ["List of patches to include."],
)
private var includedPatches = hashSetOf<String>()
@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 = ["--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 = ["-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 = ["-e", "--exclude"],
description = ["List of patches to exclude."],
)
private var excludedPatches = hashSetOf<String>()
@ArgGroup(exclusive = false)
internal var disable: DisableSelection? = null
@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>()
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 = ["--options"],
description = ["Path to patch options JSON file."],
)
private var optionsFile: File? = null
@CommandLine.Option(
names = ["--exclusive"],
description = ["Disable all patches except the ones enabled."],
description = ["Only include patches that are explicitly specified to be included."],
showDefaultValue = ALWAYS,
)
private var exclusive = false
@CommandLine.Option(
names = ["-f", "--force"],
description = ["Don't check for compatibility with the supplied APK's version."],
description = ["Bypass compatibility checks for the supplied APK's version."],
showDefaultValue = ALWAYS,
)
private var force: Boolean = false
@@ -108,62 +84,56 @@ internal object PatchCommand : Runnable {
@CommandLine.Option(
names = ["-o", "--out"],
description = ["Path to save the patched APK file to. Defaults to the same path as the supplied APK file."],
description = ["Path to save the patched APK file to. Defaults to the same directory as the supplied APK file."],
)
@Suppress("unused")
private fun setOutputFilePath(outputFilePath: File?) {
this.outputFilePath = outputFilePath?.absoluteFile
}
@ArgGroup(exclusive = false, multiplicity = "0..1")
internal var installation: Installation? = null
@CommandLine.Option(
names = ["-d", "--device-serial"],
description = ["ADB device serial to install to. If not supplied, the first connected device will be used."],
// Empty string to indicate that the first connected device should be used.
fallbackValue = "",
arity = "0..1",
)
private var deviceSerial: String? = null
internal class Installation {
@CommandLine.Option(
names = ["-i", "--install"],
required = true,
description = ["Serial of the ADB device to install to. If not specified, the first connected device will be used."],
fallbackValue = "", // Empty string is used to select the first of connected devices.
arity = "0..1",
)
internal var deviceSerial: String? = null
@CommandLine.Option(
names = ["--mount"],
required = false,
description = ["Install the patched APK file by mounting."],
showDefaultValue = ALWAYS,
)
internal var mount: Boolean = false
}
@CommandLine.Option(
names = ["--mount"],
description = ["Install by mounting the patched APK file."],
showDefaultValue = ALWAYS,
)
private var mount: Boolean = false
@CommandLine.Option(
names = ["--keystore"],
description = [
"Path to the keystore file containing a private key and certificate pair to sign the patched APK file with. " +
"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
@CommandLine.Option(
names = ["--keystore-password"],
description = ["Password of the keystore. Empty password by default."],
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
@CommandLine.Option(
names = ["--keystore-entry-alias"],
description = ["Alias of the private key and certificate pair keystore entry."],
names = ["--alias"],
description = ["The alias of the key from the keystore to sign the patched APK file with."],
showDefaultValue = ALWAYS,
)
private var keyStoreEntryAlias = "ReVanced Key"
private var alias = "ReVanced Key"
@CommandLine.Option(
names = ["--keystore-entry-password"],
description = ["Password of the keystore entry."],
description = ["The password of the entry from the keystore for the key to sign the patched APK file with."],
)
private var keyStoreEntryPassword = "" // Empty password by default
private var password = "" // Empty password by default
@CommandLine.Option(
names = ["--signer"],
@@ -172,53 +142,79 @@ internal object PatchCommand : Runnable {
)
private var signer = "ReVanced"
@CommandLine.Option(
names = ["-r", "--resource-cache"],
description = ["Path to temporary resource cache directory."],
)
private var resourceCachePath: File? = null
set(value) {
logger.warning("The --resource-cache option is deprecated. Use --temporary-files-patch instead.")
field = value
temporaryFilesPath = value
}
@CommandLine.Option(
names = ["-t", "--temporary-files-path"],
description = ["Path to store temporary files."],
description = ["Path to temporary files directory."],
)
private var temporaryFilesPath: File? = null
private var aaptBinaryPath: File? = null
@CommandLine.Option(
names = ["--purge"],
description = ["Purge temporary files directory after patching."],
names = ["-p", "--purge"],
description = ["Purge the temporary resource cache 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 patch."],
arity = "1",
description = ["APK file to be patched."],
arity = "1..1",
)
@Suppress("unused")
private fun setApk(apk: File) {
if (!apk.exists()) {
throw CommandLine.ParameterException(
spec.commandLine(),
"APK file ${apk.path} does not exist",
"APK file ${apk.name} does not exist",
)
}
this.apk = apk
}
private lateinit var apk: File
@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.name} does not exist.")
}
this.integrations += integrations
}
@CommandLine.Option(
names = ["-p", "--patches"],
description = ["One or more path to RVP files."],
names = ["-b", "--patch-bundle"],
description = ["One or more bundles of patches."],
required = true,
)
@Suppress("unused")
private fun setPatchesFile(patchesFiles: Set<File>) {
patchesFiles.firstOrNull { !it.exists() }?.let {
throw CommandLine.ParameterException(spec.commandLine(), "${it.name} can't be found")
private fun setPatchBundles(patchBundles: Set<File>) {
patchBundles.firstOrNull { !it.exists() }?.let {
throw CommandLine.ParameterException(spec.commandLine(), "Patch bundle ${it.name} does not exist")
}
this.patchesFiles = patchesFiles
this.patchBundles = patchBundles
}
private var patchesFiles = emptySet<File>()
@CommandLine.Option(
names = ["--custom-aapt2-binary"],
description = ["Path to a custom AAPT binary to compile resources with."],
@@ -247,132 +243,110 @@ 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 (installation?.deviceSerial != null) {
val deviceSerial = installation?.deviceSerial!!.ifEmpty { null }
try {
if (installation?.mount == true) {
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 = loadPatchesFromJar(patchesFiles)
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")}")
}
}
// endregion
val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher")
val (packageName, patcherResult) = Patcher(
Patcher(
PatcherConfig(
apk,
patcherTemporaryFilesPath,
temporaryFilesPath,
aaptBinaryPath?.path,
patcherTemporaryFilesPath.absolutePath,
temporaryFilesPath.absolutePath,
true,
),
).use { patcher ->
val packageName = patcher.context.packageMetadata.packageName
val packageVersion = patcher.context.packageMetadata.packageVersion
val filteredPatches =
patcher.filterPatchSelection(patches).also { patches ->
logger.info("Setting patch options")
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")
if (optionsFile.exists()) {
patches.setOptions(optionsFile)
} else {
Options.serialize(patches, prettyPrint = true).let(optionsFile::writeText)
}
}
}
patcher.context.packageMetadata.packageName to patcher.get()
}
// region Patch
// region Save.
val patcherResult =
patcher.apply {
acceptIntegrations(integrations)
acceptPatches(filteredPatches)
apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).apply {
patcherResult.applyTo(this)
}.let { patchedApkFile ->
if (installation?.mount != true) {
ApkUtils.signApk(
patchedApkFile,
outputFilePath,
signer,
ApkUtils.KeyStoreDetails(
// 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
// region Save
apk.copyTo(outputFilePath, overwrite = true)
patcherResult.applyTo(outputFilePath)
if (!mount) {
outputFilePath.sign(
ApkUtils.SigningOptions(
keystoreFilePath,
keyStorePassword,
keyStoreEntryAlias,
keyStoreEntryPassword,
alias,
password,
signer,
),
)
} else {
patchedApkFile.copyTo(outputFilePath, overwrite = true)
}
logger.info("Saved to $outputFilePath")
// endregion
// region Install
deviceSerial?.let { serial ->
AdbManager.getAdbManager(deviceSerial = serial.ifEmpty { null }, mount)
}?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName))
// endregion
}
logger.info("Saved to $outputFilePath")
// endregion
// region Install.
installation?.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
if (purge) {
logger.info("Purging temporary files")
purge(temporaryFilesPath)
@@ -380,72 +354,61 @@ internal object PatchCommand : Runnable {
}
/**
* Filter the patches based on the selection.
* Filter the patches to be added to the patcher. The filter is based on the following:
*
* @param packageName The package name of the APK file to be patched.
* @param packageVersion The version of the APK file to be patched.
* @param patches The patches to filter.
* @return The filtered patches.
*/
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()
private fun Patcher.filterPatchSelection(patches: PatchSet): PatchSet =
buildSet {
val packageName = context.packageMetadata.packageName
val packageVersion = context.packageMetadata.packageVersion
val disabledPatches =
selection.mapNotNull { it.disable?.selector?.name }.toSet()
val disabledPatchesByIndex =
selection.mapNotNull { it.disable?.selector?.index }.toSet()
patches.withIndex().forEach patch@{ (i, patch) ->
val patchName = patch.name!!
this@filterPatchSelection.withIndex().forEach patchLoop@{ (i, patch) ->
val patchName = patch.name!!
val explicitlyExcluded = excludedPatches.contains(patchName) || excludedPatchesByIndex.contains(i)
if (explicitlyExcluded) return@patch logger.info("Excluding $patchName")
val isManuallyDisabled = patchName in disabledPatches || i in disabledPatchesByIndex
if (isManuallyDisabled) return@patchLoop logger.info("\"$patchName\" disabled manually")
// 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
// 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\"")
}
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 },
)
val matchesVersion =
force || versions?.let { it.any { version -> version == packageVersion } } ?: true
return@let
} ?: logger.fine("$patchName has no constraint on packages.")
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 },
)
// 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)
return@let
} ?: logger.fine("\"$patchName\" has no package constraints")
val included = implicitlyIncluded || explicitlyIncluded
if (!included) return@patch logger.info("$patchName excluded") // Case 1.
val isEnabled = !exclusive && patch.use
val isManuallyEnabled = patchName in enabledPatchesByName || i in enabledPatchesByIndex
logger.fine("Adding $patchName")
if (!(isEnabled || isManuallyEnabled)) {
return@patchLoop logger.info("\"$patchName\" disabled")
add(patch)
}
add(patch)
logger.fine("\"$patchName\" added")
}
}
private fun purge(resourceCachePath: File) {
val result =

View File

@@ -1,63 +1,44 @@
package app.revanced.cli.command.utility
import app.revanced.library.installation.installer.*
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import app.revanced.library.adb.AdbManager
import picocli.CommandLine.*
import java.io.File
import java.util.logging.Logger
@Command(
name = "install",
description = ["Install an APK file."],
description = ["Install an APK file to devices with the supplied ADB device serials"],
)
internal object InstallCommand : Runnable {
private val logger = Logger.getLogger(this::class.java.name)
private val logger = Logger.getLogger(InstallCommand::class.java.name)
@Parameters(
description = ["Serial of ADB devices. If not supplied, the first connected device will be used."],
description = ["ADB device serials. 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() {
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) {
fun install(deviceSerial: String? = null) =
try {
AdbManager.getAdbManager(deviceSerial, packageName != null).install(AdbManager.Apk(apk, packageName))
} catch (e: AdbManager.DeviceNotFoundException) {
logger.severe(e.toString())
}
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()
}
deviceSerials?.forEach(::install) ?: install()
}
}

View File

@@ -1,66 +1,45 @@
package app.revanced.cli.command.utility
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 app.revanced.library.adb.AdbManager
import picocli.CommandLine.*
import picocli.CommandLine.Help.Visibility.ALWAYS
import java.util.logging.Logger
@Command(
name = "uninstall",
description = ["Uninstall a patched app."],
description = ["Uninstall a patched app from the devices with the supplied ADB device serials"],
)
internal object UninstallCommand : Runnable {
private val logger = Logger.getLogger(this::class.java.name)
private val logger = Logger.getLogger(UninstallCommand::class.java.name)
@Parameters(
description = ["Serial of ADB devices. If not supplied, the first connected device will be used."],
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."],
description = ["Package name of the app to uninstall"],
required = true,
)
private lateinit var packageName: String
@Option(
names = ["-u", "--unmount"],
description = ["Uninstall the patched APK file by unmounting."],
description = ["Uninstall by unmounting the patched APK file"],
showDefaultValue = ALWAYS,
)
private var unmount: Boolean = false
override fun run() {
suspend fun uninstall(deviceSerial: String? = null) {
val result = try {
if (unmount) {
AdbRootInstaller(deviceSerial)
} else {
AdbInstaller(deviceSerial)
}.uninstall(packageName)
} catch (e: Exception) {
fun uninstall(deviceSerial: String? = null) =
try {
AdbManager.getAdbManager(deviceSerial, unmount).uninstall(packageName)
} catch (e: AdbManager.DeviceNotFoundException) {
logger.severe(e.toString())
}
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()
}
deviceSerials?.forEach { uninstall(it) } ?: uninstall()
}
}

View File

@@ -1,78 +0,0 @@
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 }
}