Compare commits

...

57 Commits

Author SHA1 Message Date
oSumAtrIX
e87ee9c5b8 chore: Add click-to-run scripts 2024-10-19 06:35:25 +02:00
semantic-release-bot
504c6c1ea5 chore: Release v5.0.0-dev.8 [skip ci]
# [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)
2024-10-17 18:06:32 +00:00
oSumAtrIX
f58b101636 build(Needs bump): Bump ReVanced Patcher 2024-10-17 20:02:26 +02:00
cyberboh
292b93b609 docs: Use correct option name in command example (#342) 2024-10-16 15:27:58 +02:00
semantic-release-bot
e60a61b098 chore: Release v5.0.0-dev.7 [skip ci]
# [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](1da8ae9e46))
2024-10-16 02:57:57 +00:00
oSumAtrIX
1da8ae9e46 fix: Check for null when no device serial was specified 2024-10-16 04:55:52 +02:00
semantic-release-bot
a9f2538827 chore: Release v5.0.0-dev.6 [skip ci]
# [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](5f952f35f5))
2024-10-10 21:05:17 +00:00
oSumAtrIX
5f952f35f5 fix: Use the first connected device when no ADB device is specified 2024-10-10 23:03:20 +02:00
oSumAtrIX
280ded2281 ci: Adjust release commit message 2024-10-01 17:30:15 +02:00
semantic-release-bot
bb6bc1ba9a chore: Release 5.0.0-dev.5 [skip ci]
# [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)
2024-09-30 20:27:43 +00:00
oSumAtrIX
04b178b701 build(Needs bump): Update dependencies 2024-09-30 22:25:06 +02:00
oSumAtrIX
32e6aae132 ci: Adjust release commit message 2024-09-30 22:24:40 +02:00
oSumAtrIX
7ee4f15ab9 ci: Use permissions and regular GitHub token instead of PAT 2024-09-30 22:24:18 +02:00
oSumAtrIX
a7af611ef0 docs: Use correct option keys in command examples [skip ci] 2024-09-21 23:08:32 +02:00
semantic-release-bot
9440dc769e chore(release): 5.0.0-dev.4 [skip ci]
# [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](751fa1d889))
* Make the patch command work without specifying any selection ([ba159a3](ba159a35a9))

### Features

* Show error about no installation device found at the beginning ([3300e6b](3300e6b433))
2024-09-17 23:18:07 +00:00
oSumAtrIX
751fa1d889 fix: Make patches selectable by using a mutable collection for the selection option 2024-09-05 23:08:18 +04:00
oSumAtrIX
3300e6b433 feat: Show error about no installation device found at the beginning 2024-09-05 23:00:22 +04:00
oSumAtrIX
ba159a35a9 fix: Make the patch command work without specifying any selection 2024-09-05 22:54:09 +04:00
kitadai31
74ff94037e docs: List supported ABIs correctly and add a mention how to use other ones
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-09-01 16:36:34 +02:00
oSumAtrIX
7e8fb5722a docs: Link to JRE downloads instead of JDK 2024-08-23 13:21:27 +04:00
semantic-release-bot
7b4af46c94 chore(release): 5.0.0-dev.3 [skip ci]
# [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](45c998b335))
2024-08-14 13:36:05 +00:00
oSumAtrIX
45c998b335 feat: Simplify option descriptions 2024-08-14 15:34:02 +02:00
semantic-release-bot
9b03963687 chore(release): 5.0.0-dev.2 [skip ci]
# [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](6e7797a3f0))

### BREAKING CHANGES

* Options have been renamed.
2024-08-14 12:08:47 +00:00
oSumAtrIX
6e7797a3f0 feat: Simplify command and option names and descriptions (#338)
BREAKING CHANGE: Options have been renamed.
2024-08-14 14:06:58 +02:00
semantic-release-bot
6306b1abfe chore(release): 5.0.0-dev.1 [skip ci]
# [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](23002434b2))

### BREAKING CHANGES

* This commit changes various CLI options and removes the `options.json` file. Instead, patch options can now be passed via CLI options
2024-08-12 20:14:37 +00:00
oSumAtrIX
23002434b2 feat: Set patch options via CLI (#336)
BREAKING CHANGE: This commit changes various CLI options and removes the `options.json` file. Instead, patch options can now be passed via CLI options
2024-08-12 22:12:45 +02:00
oSumAtrIX
54ae01cd76 build: Bump ReVanced Patcher (#335) 2024-08-12 22:05:47 +02:00
oSumAtrIX
0a2e99c1c0 build: Remove local Maven repo and unnecessary build configuration 2024-07-26 02:03:51 +02:00
oSumAtrIX
832badef4a ci: Correct usage of repository variable 2024-07-13 00:45:05 +02:00
oSumAtrIX
40279e0369 docs: Fix invalid option name 2024-05-30 23:31:24 +02:00
oSumAtrIX
e65143851d build: Bump dependencies 2024-05-26 01:21:15 +02:00
oSumAtrIX
3bec9b5f28 docs: Improve issue templates 2024-05-26 00:43:40 +02:00
oSumAtrIX
6558aafbf1 chore: Fix spelling mistake 2024-04-05 21:17:42 +02:00
semantic-release-bot
1496e824d6 chore(release): 4.6.0 [skip ci]
# [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](366f400c5a))
* Use correct option description ([45a2ffa](45a2ffa2dd))

### Features

* Use more consistent option name ([223629c](223629c663))
2024-04-01 14:56:47 +00:00
oSumAtrIX
66a5fc2bf6 chore: Merge branch dev to main (#318) 2024-04-01 16:54:44 +02:00
oSumAtrIX
2b41fe6d0b docs: Add notice about authentication 2024-03-29 18:41:15 +01:00
semantic-release-bot
9dd460f056 chore(release): 4.6.0-dev.1 [skip ci]
# [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](45a2ffa2dd))

### Features

* Use more consistent option name ([223629c](223629c663))
2024-03-14 13:47:05 +00:00
oSumAtrIX
223629c663 feat: Use more consistent option name 2024-03-14 13:22:54 +01:00
oSumAtrIX
45a2ffa2dd fix: Use correct option description 2024-03-14 13:22:54 +01:00
oSumAtrIX
932494eeab build: Bump dependencies 2024-03-14 13:22:54 +01:00
semantic-release-bot
0d28a49773 chore(release): 4.5.1-dev.1 [skip ci]
## [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](366f400c5a))
2024-03-12 13:49:51 +00:00
oSumAtrIX
366f400c5a fix: Copy APK to output path when it is not being signed
When `--mount` is used, signing is skipped. For that reason the APK was never copied to the output path.
2024-03-12 14:47:45 +01:00
semantic-release-bot
d5c27408f3 chore(release): 4.5.0 [skip ci]
# [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](f0f3e5614b))
* Sign APKs correctly ([5ff105c](5ff105cf6b))

### Features

* Remove deprecated CLI options ([48a1a39](48a1a39b94))
2024-03-11 09:12:29 +00:00
oSumAtrIX
8ee5b754da chore: Merge branch dev to main (#317) 2024-03-11 10:10:56 +01:00
semantic-release-bot
9476f5a2e4 chore(release): 4.5.0-dev.1 [skip ci]
# [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](f0f3e5614b))
* Sign APKs correctly ([5ff105c](5ff105cf6b))

### Features

* Remove deprecated CLI options ([48a1a39](48a1a39b94))
2024-03-11 09:08:04 +00:00
oSumAtrIX
5ff105cf6b fix: Sign APKs correctly 2024-03-11 10:05:32 +01:00
oSumAtrIX
f0f3e5614b fix: Show path for missing files instead of just the name 2024-03-11 10:05:04 +01:00
oSumAtrIX
48a1a39b94 feat: Remove deprecated CLI options 2024-03-11 09:22:35 +01:00
semantic-release-bot
c45ed69adf chore(release): 4.4.2 [skip ci]
## [4.4.2](https://github.com/ReVanced/revanced-cli/compare/v4.4.1...v4.4.2) (2024-03-10)
2024-03-10 15:44:08 +00:00
oSumAtrIX
8641d301b1 chore: Merge branch dev to main (#315) 2024-03-10 16:42:17 +01:00
semantic-release-bot
44b72ca99d chore(release): 4.4.2-dev.2 [skip ci]
## [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)
2024-03-10 15:41:25 +00:00
oSumAtrIX
240f2cfb6b build(Needs bump): Bump dependencies to fix generating keystore 2024-03-10 16:39:43 +01:00
semantic-release-bot
304275ddbb chore(release): 4.4.2-dev.1 [skip ci]
## [4.4.2-dev.1](https://github.com/ReVanced/revanced-cli/compare/v4.4.1...v4.4.2-dev.1) (2024-03-09)
2024-03-09 04:22:02 +00:00
oSumAtrIX
483590726a build(Needs bump): Bump dependencies to fix signing issues 2024-03-09 05:20:11 +01:00
semantic-release-bot
cf20efd467 chore(release): 4.4.1 [skip ci]
## [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](1c10a7760d))
2024-03-06 19:14:29 +00:00
oSumAtrIX
164d09dec1 chore: Merge branch dev to main (#309) 2024-03-06 20:12:35 +01:00
oSumAtrIX
1e92239616 build: Set target bytecode level to JVM 11 2024-03-04 19:16:09 +01:00
45 changed files with 3004 additions and 1597 deletions

View File

@@ -70,7 +70,7 @@ body:
Before creating a new bug report, please keep the following in mind: 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). - **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). - **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 - type: textarea
@@ -102,7 +102,7 @@ body:
label: Acknowledgements label: Acknowledgements
description: Your bug report will be closed if you don't follow the checklist below. description: Your bug report will be closed if you don't follow the checklist below.
options: 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 required: true
- label: I have chosen an appropriate title. - label: I have chosen an appropriate title.
required: true required: true

View File

@@ -70,8 +70,8 @@ body:
Before creating a new feature request, please keep the following in mind: 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). - **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 bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-cli//blob/main/CONTRIBUTING.md). - **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). - **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 - type: textarea
attributes: attributes:
@@ -98,7 +98,7 @@ body:
label: Acknowledgements label: Acknowledgements
description: Your feature request will be closed if you don't follow the checklist below. description: Your feature request will be closed if you don't follow the checklist below.
options: 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 required: true
- label: I have chosen an appropriate title. - label: I have chosen an appropriate title.
required: true required: true

View File

@@ -10,6 +10,8 @@ on:
jobs: jobs:
release: release:
name: Release name: Release
permissions:
contents: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@@ -42,9 +44,9 @@ jobs:
with: with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }} passphrase: ${{ secrets.GPG_PASSPHRASE }}
fingerprint: ${{ env.GPG_FINGERPRINT }} fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Release - name: Release
env: env:
GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm exec semantic-release run: npm exec semantic-release

7
.gitignore vendored
View File

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

View File

@@ -23,7 +23,8 @@
"assets": [ "assets": [
"CHANGELOG.md", "CHANGELOG.md",
"gradle.properties" "gradle.properties"
] ],
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
} }
], ],
[ [

View File

@@ -1,3 +1,136 @@
# [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) ## [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)

View File

@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins { plugins {
alias(libs.plugins.kotlin) alias(libs.plugins.kotlin)
alias(libs.plugins.shadow) alias(libs.plugins.shadow)
@@ -14,10 +16,9 @@ application {
repositories { repositories {
mavenCentral() mavenCentral()
mavenLocal()
google() google()
maven { 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") url = uri("https://maven.pkg.github.com/revanced/registry")
credentials { credentials {
username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR")
@@ -35,7 +36,15 @@ dependencies {
testImplementation(libs.kotlin.test) testImplementation(libs.kotlin.test)
} }
kotlin { jvmToolchain(11) } kotlin {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
java {
targetCompatibility = JavaVersion.VERSION_11
}
tasks { tasks {
test { test {

View File

@@ -4,9 +4,9 @@ To use ReVanced CLI, you will need to fulfill specific requirements.
## 🤝 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 or higher ([Eclipse Temurin JRE](https://adoptium.net/temurin/releases/?package=jre) 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 - [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 ## ⏭️ Whats next

View File

@@ -1,129 +1,171 @@
# 🛠️ Using ReVanced CLI # 🛠️ Using ReVanced CLI
Learn how to use 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 ```bash
java -jar revanced-cli.jar -h java -jar revanced-cli.jar list-patches --with-packages --with-versions --with-options patches.rvp
``` ```
- ### 📃 List patches ## 💉 Patch an app
```bash To patch an app using the default list of patches, use the `patch` command:
java -jar revanced-cli.jar list-patches \
--with-packages \
--with-versions \
--with-options \
revanced-patches.jar [<patch-bundle> ...]
```
- ### ⚙️ 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. You can also use multiple RVP files:
The file can be supplied to ReVanced CLI later on.
```bash ```bash
java -jar revanced-cli.jar options \ java -jar revanced-cli.jar patch -p patches.rvp -p another-patches.rvp input.apk
--path options.json \ ```
--overwrite \
revanced-patches.jar [<patch-bundle> ...]
```
> ** Note** To change the default set of enabled or disabled patches, use the option `-e` or `-d` to enable or disable specific patches.
> A default `options.json` file will be automatically created if it does not exist You can use the `list-patches` command to see which patches are enabled by default.
> without any need for intervention when using the `patch` command.
- ### 💉 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. ```bash
After patching, ReVanced CLI can install the patched app on your device using two methods: java -jar revanced-cli.jar patch -p patches.rvp --exclusive -e "Patch name" -e "Another patch name" input.apk
```
> **💡 Tip** You can also use the options `--ei` or `--di` to enable or disable patches by their index.
> For ReVanced CLI to be able to install the patched app on your device, make sure ADB is working: 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`:
> ```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
> ```
> **⚠️ Warning** ```bash
> Some patches may require integrations java -jar revanced-cli.jar list-patches patches.rvp
> 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.
- #### 👾 Patch an app and install it on your device regularly Then you can use the indices to enable or disable patches:
```bash ```bash
java -jar revanced-cli.jar patch \ java -jar revanced-cli.jar patch -p patches.rvp --ei 123 --di 456 input.apk
--patch-bundle revanced-patches.jar \ ```
-d \
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** ```bash
> Ensure that the same app you are patching and mounting over is installed on your device: java -jar revanced-cli.jar patch -p patches.rvp --exclusive -e "Patch name" --ei 123 input.apk
> ```
> ```bash
> adb install app.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** > [!TIP]
> You can use the option `--ii` to include or `--ie` to exclude > You can use the option `-i` to automatically install the patched app after patching.
> patches by their index in relation to supplied patch bundles, > Make sure ADB is working:
> similarly to the option `--include` and `--exclude`. >
> > ```bash
> This is useful in case two patches have the same name, and you must include or exclude one. > adb shell exit
> 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.
- ### 🗑️ Uninstall an app
```bash > [!TIP]
java -jar revanced-cli.jar utility uninstall \ > You can use the option `--mount` to mount the patched app on top of the un-patched app.
--package-name <package-name> \ > Make sure you have root permissions and the same app you are patching and mounting over is installed on your device:
[<device-serial>] >
``` > ```bash
> adb shell su -c exit
> adb install input.apk
> ```
> **💡 Tip** Patches can have options you can set using the option `-O` alongside the option to include the patch by name or index.
> You can unmount an APK file To know the options of a patch, use the option `--with-options` when listing patches:
> by adding the option `--unmount`.
- ### 📦 Install an app ```bash
java -jar revanced-cli.jar list-patches --with-options patches.rvp
```
```bash Each patch can have multiple options. You can set them using the option `-O`.
java -jar revanced-cli.jar utility install \ For example, to set the options for the patch with the name `Patch name`
-a input.apk \ with the key `key1` and `key2` to `value1` and `value2` respectively, use the following command:
[<device-serial>]
```
> **💡 Tip** ```bash
> You can mount an APK file java -jar revanced-cli.jar patch -p patches.rvp -e "Patch name" -Okey1=value1 -Okey2=value2 input.apk
> by supplying the app's package name to mount the supplied APK file over the option `-mount`. ```
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> ...]
> ```

View File

@@ -23,4 +23,15 @@ To build ReVanced CLI, follow these steps:
./gradlew build ./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`. 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.parallel = true
org.gradle.caching = true org.gradle.caching = true
kotlin.code.style = official kotlin.code.style = official
version = 4.4.1-dev.2 version = 5.0.0-dev.8

View File

@@ -1,18 +1,18 @@
[versions] [versions]
shadow = "8.1.1" shadow = "8.1.1"
kotlin = "1.9.22" kotlin = "2.0.20"
kotlinx-coroutines-core = "1.7.3" kotlinx = "1.8.1"
picocli = "4.7.5" picocli = "4.7.6"
revanced-patcher = "19.3.1" revanced-patcher = "20.0.2"
revanced-library = "2.1.0" revanced-library = "3.0.0"
[libraries] [libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 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" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" }
revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } 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] [plugins]
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } 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" }

Binary file not shown.

View File

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

17
gradlew vendored
View File

@@ -83,7 +83,8 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} 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. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # 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 ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # 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" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac 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. # 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"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command; # Collect all arguments for the java command:
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# shell script including quotes and variable substitutions, so put them in # and any embedded shellness will be escaped.
# double quotes to make sure that they get re-expanded; and # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# * put everything else in single quotes, so that it's not re-expanded. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-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 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

3002
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", "@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3", "@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1", "@semantic-release/git": "^10.0.1",
"gradle-semantic-release-plugin": "^1.9.1", "gradle-semantic-release-plugin": "^1.10.1",
"semantic-release": "^23.0.2" "semantic-release": "^24.1.2"
} }
} }

View File

@@ -0,0 +1,29 @@
@echo off
echo Check, if OpenJDK/ Eclipse Temurin 11 or newer is installed.
pause
echo:
cd modules
call run java -version
cd ..
echo:
set /P INSTALLED_JAVA=Can you see OpenJDK/ Eclipse Temurin 11 or newer? (y/n)
if "%INSTALLED_JAVA%"=="y" (
echo The environment is set up.
pause
exit
)
cls
echo Install Eclipse Temurin JRE 21.
pause
cd modules
call install-java
cd ..
echo The environment is set up. Rerun this script to check your environment.
pause

11
scripts/2-download.bat Normal file
View File

@@ -0,0 +1,11 @@
@echo off
echo Download necessary files from GitHub.
pause
cd modules
call composite download %~dp0\workspace
cd ..
echo Files downloaded.
pause

14
scripts/3-patch.bat Normal file
View File

@@ -0,0 +1,14 @@
@echo off
setlocal
echo Patch an APK with the patches in the workspace.
pause
cd modules
call composite patch %~dp0\workspace
cd ..
mv %~dp0\workspace\patched.apk %~dp0\patched.apk
echo Patched APK saved at %~dp0\patched.apk.
pause

11
scripts/4-cleanup.bat Normal file
View File

@@ -0,0 +1,11 @@
@echo off
echo Clean the workspace.
pause
cd modules
call composite clean %~dp0\workspace
cd ..
echo Cleaned workspace.
pause

8
scripts/README.md Normal file
View File

@@ -0,0 +1,8 @@
# 📜 Scripts
This directory contain click-to-run scripts to use ReVanced CLI.
## Prerequisites
- [cURL](https://curl.haxx.se/)
- [WinGet](https://aka.ms/getwinget), if you are on Windows

1
scripts/customize.bat Normal file
View File

@@ -0,0 +1 @@
notepad modules\env.bat

View File

@@ -0,0 +1,12 @@
@echo off
setlocal
for /f "tokens=8 delims=. " %%a in ('java --version 2^>nul ^| findstr /r "build"') do (
set JAVA_VERSION_MAJOR=%%a
goto :break
)
:break
if %JAVA_VERSION_MAJOR% LSS 11 (
echo It looks like Java version is less than 11. Install OpenJDK/ Eclipse Temurin 11 or newer.
)

View File

@@ -0,0 +1,51 @@
@echo off
setlocal EnableDelayedExpansion
if "%~1"=="" (
set INVALID_ARGS=true
)
if "%~2"=="" (
set INVALID_ARGS=true
)
if defined INVALID_ARGS (
echo Runs curated compositions of scripts.
echo:
echo Usage: composite ^<script^> ^<workspace^>
echo Example: composite patch C:/revanced
echo:
echo Available commands:
echo: download - Download files
echo: patch - Patches an application
echo: clean - Cleans the workspace
echo: env - Check for a valid environment setup
exit /b 1
)
call env
set WORKSPACE=%2
if "%~1"=="download" (
call create %WORKSPACE%
if not exist %WORKSPACE%/revanced-cli.jar (
echo Downloading ReVanced CLI...
call download %CLI_REPO% jar %WORKSPACE%/revanced-cli.jar
)
if not exist %WORKSPACE%/patches.rvp (
echo Downloading ReVanced patches...
call download %PATCHES_REPO% jar %WORKSPACE%/patches.rvp
)
)
if "%~1"=="patch" (
set /p APK="Path to the APK file: "
call patch !APK! %WORKSPACE%
)
if "%~1"=="clean" (
call delete %WORKSPACE%
)
if "%~1"=="env" (
call check-java
)

View File

@@ -0,0 +1,19 @@
@echo off
setlocal
if "%~1"=="" (
set INVALID_ARGS=true
)
if defined INVALID_ARGS (
echo Creates a directory using the mkdir command.
echo:
echo Usage: create ^<path^>
echo Example: create C:/revanced
exit /b 1
)
set DIRECTORY=%1
if not exist %DIRECTORY% (
run "mkdir.exe" -p %DIRECTORY%
)

View File

@@ -0,0 +1,20 @@
@echo off
setlocal
if "%~1"=="" (
set INVALID_ARGS=true
)
if defined INVALID_ARGS (
echo Deletes a directory relative to the current directory using the rmdir command.
echo:
echo Usage: delete ^<path^>
echo Example: delete C:/revanced
exit /b 1
)
set DIRECTORY=%1
if exist %DIRECTORY% (
echo Confirm deletion of
run rmdir /s %DIRECTORY%
)

View File

@@ -0,0 +1,30 @@
@echo off
setlocal
if "%~1"=="" (
set INVALID_ARGS=true
)
if "%~2"=="" (
set INVALID_ARGS=true
)
if "%~3"=="" (
set INVALID_ARGS=true
)
if defined INVALID_ARGS (
echo Downloads a file from a GitHub repository release.
echo:
echo Usage: download ^<repo^> ^<asset-extension^> ^<out^>
echo Example: download revanced/revanced-cli jar C:/revanced/revanced-cli.jar
exit /b 1
)
set REPO=%1
set ASSET_EXTENSION=%2
set OUT=%3
set URL=https://api.github.com/repos/%REPO%/releases/latest
for /f "delims=" %%i in ('curl -s %URL% ^| jq -r ".assets[] | select(.name | endswith(\"%ASSET_EXTENSION%\")) | .browser_download_url"') do (
set JAR_URL=%%i
)
run curl --silent --location --output %OUT% %JAR_URL%

6
scripts/modules/env.bat Normal file
View File

@@ -0,0 +1,6 @@
@echo off
:: Customize your environment.
set CLI_REPO=revanced/revanced-cli
set PATCHES_REPO=revanced/revanced-patches

View File

@@ -0,0 +1 @@
run install EclipseAdoptium.Temurin.21.JRE

View File

@@ -0,0 +1 @@
run install jqlang.jq

View File

@@ -0,0 +1,14 @@
@echo off
if "%~1"=="" (
set INVALID_ARGS=true
)
if defined INVALID_ARGS (
echo Installs a package using winget.
echo:
echo Usage: install ^<id^>
echo Example: install jqlang.jq
exit /b 1
)
run winget install -e --id=%1

28
scripts/modules/patch.bat Normal file
View File

@@ -0,0 +1,28 @@
@echo off
setlocal
if "%~1"=="" (
set INVALID_ARGS=true
)
if "%~2"=="" (
set INVALID_ARGS=true
)
if defined INVALID_ARGS (
echo Patches an application using the specified patches.
echo:
echo Usage: patch ^<apk^> ^<workspace^>
echo Example: patch C:/app.apk C:/workspace
exit /b 1
)
set APK=%1
set WORKSPACE=%2
call run java -jar %WORKSPACE%/revanced-cli.jar patch ^
--patch-bundle %WORKSPACE%/patches.rvp ^
--temporary-files-path %WORKSPACE%/temporary-files ^
--out %WORKSPACE%/patched.apk ^
--purge ^
%APK%
mv %WORKSPACE%/patched.apk

23
scripts/modules/run.bat Normal file
View File

@@ -0,0 +1,23 @@
@echo off
if "%~1"=="" (
set INVALID_ARGS=true
)
if defined INVALID_ARGS (
echo Run a command with arguments.
echo:
echo Usage: run.bat ^<command^> [arguments]
echo Example: run.bat echo Hello, World!
exit /b 1
)
%*
if %ERRORLEVEL% NEQ 0 (
echo:
echo Failed to run command with exit code %ERRORLEVEL%.
echo Failed command: %*
echo:
pause
exit /b %ERRORLEVEL%
)

View File

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

View 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
}
}
}

View File

@@ -1,9 +1,9 @@
package app.revanced.cli.command package app.revanced.cli.command
import app.revanced.library.PackageName import app.revanced.library.PackageName
import app.revanced.library.PatchUtils
import app.revanced.library.VersionMap import app.revanced.library.VersionMap
import app.revanced.patcher.PatchBundleLoader import app.revanced.library.mostCommonCompatibleVersions
import app.revanced.patcher.patch.loadPatchesFromJar
import picocli.CommandLine import picocli.CommandLine
import java.io.File import java.io.File
import java.util.logging.Logger import java.util.logging.Logger
@@ -12,17 +12,17 @@ import java.util.logging.Logger
name = "list-versions", name = "list-versions",
description = [ description = [
"List the most common compatible versions of apps that are compatible " + "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 { internal class ListCompatibleVersions : Runnable {
private val logger = Logger.getLogger(ListCompatibleVersions::class.java.name) private val logger = Logger.getLogger(this::class.java.name)
@CommandLine.Parameters( @CommandLine.Parameters(
description = ["Paths to patch bundles."], description = ["Paths to RVP files."],
arity = "1..*", arity = "1..*",
) )
private lateinit var patchBundles: Array<File> private lateinit var patchesFiles: Set<File>
@CommandLine.Option( @CommandLine.Option(
names = ["-f", "--filter-package-names"], names = ["-f", "--filter-package-names"],
@@ -38,8 +38,6 @@ internal class ListCompatibleVersions : Runnable {
private var countUnusedPatches: Boolean = false private var countUnusedPatches: Boolean = false
override fun run() { override fun run() {
val patches = PatchBundleLoader.Jar(*patchBundles)
fun VersionMap.buildVersionsString(): String { fun VersionMap.buildVersionsString(): String {
if (isEmpty()) return "Any" if (isEmpty()) return "Any"
@@ -58,8 +56,9 @@ internal class ListCompatibleVersions : Runnable {
appendLine(versions.buildVersionsString().prependIndent("\t")) appendLine(versions.buildVersionsString().prependIndent("\t"))
} }
PatchUtils.getMostCommonCompatibleVersions( val patches = loadPatchesFromJar(patchesFiles)
patches,
patches.mostCommonCompatibleVersions(
packageNames, packageNames,
countUnusedPatches, countUnusedPatches,
).entries.joinToString("\n", transform = ::buildString).let(logger::info) ).entries.joinToString("\n", transform = ::buildString).let(logger::info)

View File

@@ -1,25 +1,26 @@
package app.revanced.cli.command 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.Patch
import app.revanced.patcher.patch.options.PatchOption import app.revanced.patcher.patch.loadPatchesFromJar
import picocli.CommandLine.* import picocli.CommandLine.*
import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Help.Visibility.ALWAYS
import java.io.File import java.io.File
import java.util.logging.Logger import java.util.logging.Logger
import app.revanced.patcher.patch.Option as PatchOption
@Command( @Command(
name = "list-patches", name = "list-patches",
description = ["List patches from supplied patch bundles."], description = ["List patches from supplied RVP files."],
) )
internal object ListPatchesCommand : Runnable { internal object ListPatchesCommand : Runnable {
private val logger = Logger.getLogger(ListPatchesCommand::class.java.name) private val logger = Logger.getLogger(this::class.java.name)
@Parameters( @Parameters(
description = ["Paths to patch bundles."], description = ["Paths to RVP files."],
arity = "1..*", arity = "1..*",
) )
private lateinit var patchBundles: Array<File> private lateinit var patchesFiles: Set<File>
@Option( @Option(
names = ["-d", "--with-descriptions"], names = ["-d", "--with-descriptions"],
@@ -58,7 +59,7 @@ internal object ListPatchesCommand : Runnable {
@Option( @Option(
names = ["-i", "--index"], names = ["-i", "--index"],
description = ["List the index of each patch in relation to the supplied patch bundles."], description = ["List the index of each patch in relation to the supplied RVP files."],
showDefaultValue = ALWAYS, showDefaultValue = ALWAYS,
) )
private var withIndex: Boolean = true private var withIndex: Boolean = true
@@ -70,16 +71,19 @@ internal object ListPatchesCommand : Runnable {
private var packageName: String? = null private var packageName: String? = null
override fun run() { override fun run() {
fun Patch.CompatiblePackage.buildString() = fun Package.buildString(): String {
buildString { val (name, versions) = this
return buildString {
if (withVersions && versions != null) { if (withVersions && versions != null) {
appendLine("Package name: $name") appendLine("Package name: $name")
appendLine("Compatible versions:") appendLine("Compatible versions:")
append(versions!!.joinToString("\n") { version -> version }.prependIndent("\t")) append(versions.joinToString("\n") { version -> version }.prependIndent("\t"))
} else { } else {
append("Package name: $name") append("Package name: $name")
} }
} }
}
fun PatchOption<*>.buildString() = fun PatchOption<*>.buildString() =
buildString { buildString {
@@ -91,9 +95,11 @@ internal object ListPatchesCommand : Runnable {
} ?: append("Key: $key") } ?: append("Key: $key")
values?.let { values -> values?.let { values ->
appendLine("\nValid values:") appendLine("\nPossible values:")
append(values.map { "${it.value} (${it.key})" }.joinToString("\n").prependIndent("\t")) append(values.map { "${it.value} (${it.key})" }.joinToString("\n").prependIndent("\t"))
} }
append("\nType: $type")
} }
fun IndexedValue<Patch<*>>.buildString() = fun IndexedValue<Patch<*>>.buildString() =
@@ -105,6 +111,8 @@ internal object ListPatchesCommand : Runnable {
if (withDescriptions) append("\nDescription: ${patch.description}") if (withDescriptions) append("\nDescription: ${patch.description}")
append("Enabled: ${patch.use}")
if (withOptions && patch.options.isNotEmpty()) { if (withOptions && patch.options.isNotEmpty()) {
appendLine("\nOptions:") appendLine("\nOptions:")
append( append(
@@ -126,10 +134,10 @@ internal object ListPatchesCommand : Runnable {
} }
fun Patch<*>.filterCompatiblePackages(name: String) = fun Patch<*>.filterCompatiblePackages(name: String) =
compatiblePackages?.any { it.name == name } compatiblePackages?.any { (compatiblePackageName, _) -> compatiblePackageName == name }
?: withUniversalPatches ?: withUniversalPatches
val patches = PatchBundleLoader.Jar(*patchBundles).withIndex().toList() val patches = loadPatchesFromJar(patchesFiles).withIndex().toList()
val filtered = val filtered =
packageName?.let { patches.filter { (_, patch) -> patch.filterCompatiblePackages(it) } } ?: patches packageName?.let { patches.filter { (_, patch) -> patch.filterCompatiblePackages(it) } } ?: patches

View File

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

View File

@@ -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()
}

View File

@@ -2,16 +2,15 @@ package app.revanced.cli.command
import app.revanced.library.ApkUtils import app.revanced.library.ApkUtils
import app.revanced.library.ApkUtils.applyTo import app.revanced.library.ApkUtils.applyTo
import app.revanced.library.ApkUtils.sign import app.revanced.library.installation.installer.*
import app.revanced.library.Options import app.revanced.library.setOptions
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.Patcher
import app.revanced.patcher.PatcherConfig import app.revanced.patcher.PatcherConfig
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.loadPatchesFromJar
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.ArgGroup
import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Help.Visibility.ALWAYS
import picocli.CommandLine.Model.CommandSpec import picocli.CommandLine.Model.CommandSpec
import picocli.CommandLine.Spec import picocli.CommandLine.Spec
@@ -25,57 +24,82 @@ import java.util.logging.Logger
description = ["Patch an APK file."], description = ["Patch an APK file."],
) )
internal object PatchCommand : Runnable { internal object PatchCommand : Runnable {
private val logger = Logger.getLogger(PatchCommand::class.java.name) private val logger = Logger.getLogger(this::class.java.name)
@Spec @Spec
lateinit var spec: CommandSpec // injected by picocli private lateinit var spec: CommandSpec
private lateinit var apk: File @ArgGroup(multiplicity = "0..*")
private var selection = mutableSetOf<Selection>()
private var integrations = setOf<File>() internal class Selection {
@ArgGroup(exclusive = false, multiplicity = "1")
internal var enabled: EnableSelection? = null
private var patchBundles = emptySet<File>() internal class EnableSelection {
@ArgGroup(multiplicity = "1")
internal lateinit var selector: EnableSelector
@CommandLine.Option( internal class EnableSelector {
names = ["-i", "--include"], @CommandLine.Option(
description = ["List of patches to include."], names = ["-e", "--enable"],
) description = ["Name of the patch."],
private var includedPatches = hashSetOf<String>() required = true,
)
internal var name: String? = null
@CommandLine.Option( @CommandLine.Option(
names = ["--ii"], names = ["--ei"],
description = ["List of patches to include by their index in relation to the supplied patch bundles."], description = ["Index of the patch in the combined list of the supplied RVP files."],
) required = true,
private var includedPatchesByIndex = arrayOf<Int>() )
internal var index: Int? = null
}
@CommandLine.Option( @CommandLine.Option(
names = ["-e", "--exclude"], names = ["-O", "--options"],
description = ["List of patches to exclude."], description = ["Option values keyed by option keys."],
) mapFallbackValue = CommandLine.Option.NULL_VALUE,
private var excludedPatches = hashSetOf<String>() converter = [OptionKeyConverter::class, OptionValueConverter::class],
)
internal var options = mutableMapOf<String, Any?>()
}
@CommandLine.Option( @ArgGroup(exclusive = false, multiplicity = "1")
names = ["--ei"], internal var disable: DisableSelection? = null
description = ["List of patches to exclude by their index in relation to the supplied patch bundles."],
)
private var excludedPatchesByIndex = arrayOf<Int>()
@CommandLine.Option( internal class DisableSelection {
names = ["--options"], @ArgGroup(multiplicity = "1")
description = ["Path to patch options JSON file."], internal lateinit var selector: DisableSelector
)
private var optionsFile: File? = null 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( @CommandLine.Option(
names = ["--exclusive"], names = ["--exclusive"],
description = ["Only include patches that are explicitly specified to be included."], description = ["Disable all patches except the ones enabled."],
showDefaultValue = ALWAYS, showDefaultValue = ALWAYS,
) )
private var exclusive = false private var exclusive = false
@CommandLine.Option( @CommandLine.Option(
names = ["-f", "--force"], names = ["-f", "--force"],
description = ["Bypass compatibility checks for the supplied APK's version."], description = ["Don't check for compatibility with the supplied APK's version."],
showDefaultValue = ALWAYS, showDefaultValue = ALWAYS,
) )
private var force: Boolean = false private var force: Boolean = false
@@ -84,15 +108,16 @@ internal object PatchCommand : Runnable {
@CommandLine.Option( @CommandLine.Option(
names = ["-o", "--out"], 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?) { private fun setOutputFilePath(outputFilePath: File?) {
this.outputFilePath = outputFilePath?.absoluteFile this.outputFilePath = outputFilePath?.absoluteFile
} }
@CommandLine.Option( @CommandLine.Option(
names = ["-d", "--device-serial"], names = ["-i", "--install"],
description = ["ADB device serial to install to. If not supplied, the first connected device will be used."], 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. // Empty string to indicate that the first connected device should be used.
fallbackValue = "", fallbackValue = "",
arity = "0..1", arity = "0..1",
@@ -101,7 +126,7 @@ internal object PatchCommand : Runnable {
@CommandLine.Option( @CommandLine.Option(
names = ["--mount"], names = ["--mount"],
description = ["Install by mounting the patched APK file."], description = ["Install the patched APK file by mounting."],
showDefaultValue = ALWAYS, showDefaultValue = ALWAYS,
) )
private var mount: Boolean = false private var mount: Boolean = false
@@ -109,31 +134,30 @@ internal object PatchCommand : Runnable {
@CommandLine.Option( @CommandLine.Option(
names = ["--keystore"], names = ["--keystore"],
description = [ 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.", "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( @CommandLine.Option(
names = ["--keystore-password"], 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 private var keyStorePassword: String? = null // Empty password by default
@CommandLine.Option( @CommandLine.Option(
names = ["--alias"], names = ["--keystore-entry-alias"],
description = ["The alias of the key from the keystore to sign the patched APK file with."], description = ["Alias of the private key and certificate pair keystore entry."],
showDefaultValue = ALWAYS, showDefaultValue = ALWAYS,
) )
private var alias = "ReVanced Key" private var keyStoreEntryAlias = "ReVanced Key"
@CommandLine.Option( @CommandLine.Option(
names = ["--keystore-entry-password"], names = ["--keystore-entry-password"],
description = ["The password of the entry from the keystore for the key to sign the patched APK file with."], description = ["Password of the keystore entry."],
) )
private var password = "" // Empty password by default private var keyStoreEntryPassword = "" // Empty password by default
@CommandLine.Option( @CommandLine.Option(
names = ["--signer"], names = ["--signer"],
@@ -142,79 +166,53 @@ internal object PatchCommand : Runnable {
) )
private var signer = "ReVanced" 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( @CommandLine.Option(
names = ["-t", "--temporary-files-path"], names = ["-t", "--temporary-files-path"],
description = ["Path to temporary files directory."], description = ["Path to store temporary files."],
) )
private var temporaryFilesPath: File? = null private var temporaryFilesPath: File? = null
private var aaptBinaryPath: File? = null private var aaptBinaryPath: File? = null
@CommandLine.Option( @CommandLine.Option(
names = ["-p", "--purge"], names = ["--purge"],
description = ["Purge the temporary resource cache directory after patching."], description = ["Purge temporary files directory after patching."],
showDefaultValue = ALWAYS, showDefaultValue = ALWAYS,
) )
private var purge: Boolean = false private var purge: Boolean = false
@CommandLine.Option(
names = ["-w", "--warn"],
description = ["Warn if a patch can not be found in the supplied patch bundles."],
showDefaultValue = ALWAYS,
)
private var warn: Boolean = false
@CommandLine.Parameters( @CommandLine.Parameters(
description = ["APK file to be patched."], description = ["APK file to patch."],
arity = "1..1", arity = "1",
) )
@Suppress("unused") @Suppress("unused")
private fun setApk(apk: File) { private fun setApk(apk: File) {
if (!apk.exists()) { if (!apk.exists()) {
throw CommandLine.ParameterException( throw CommandLine.ParameterException(
spec.commandLine(), spec.commandLine(),
"APK file ${apk.name} does not exist", "APK file ${apk.path} does not exist",
) )
} }
this.apk = apk this.apk = apk
} }
@CommandLine.Option( private lateinit var apk: File
names = ["-m", "--merge"],
description = ["One or more DEX files or containers to merge into the APK."],
)
@Suppress("unused")
private fun setIntegrations(integrations: Array<File>) {
integrations.firstOrNull { !it.exists() }?.let {
throw CommandLine.ParameterException(spec.commandLine(), "Integrations file ${it.name} does not exist.")
}
this.integrations += integrations
}
@CommandLine.Option( @CommandLine.Option(
names = ["-b", "--patch-bundle"], names = ["-p", "--patches"],
description = ["One or more bundles of patches."], description = ["One or more path to RVP files."],
required = true, required = true,
) )
@Suppress("unused") @Suppress("unused")
private fun setPatchBundles(patchBundles: Set<File>) { private fun setPatchesFile(patchesFiles: Set<File>) {
patchBundles.firstOrNull { !it.exists() }?.let { patchesFiles.firstOrNull { !it.exists() }?.let {
throw CommandLine.ParameterException(spec.commandLine(), "Patch bundle ${it.name} does not exist") throw CommandLine.ParameterException(spec.commandLine(), "${it.name} can't be found")
} }
this.patchBundles = patchBundles this.patchesFiles = patchesFiles
} }
private var patchesFiles = emptySet<File>()
@CommandLine.Option( @CommandLine.Option(
names = ["--custom-aapt2-binary"], names = ["--custom-aapt2-binary"],
description = ["Path to a custom AAPT binary to compile resources with."], description = ["Path to a custom AAPT binary to compile resources with."],
@@ -243,104 +241,132 @@ internal object PatchCommand : Runnable {
"${outputFilePath.nameWithoutExtension}-temporary-files", "${outputFilePath.nameWithoutExtension}-temporary-files",
) )
val optionsFile =
optionsFile ?: outputFilePath.parentFile.resolve(
"${outputFilePath.nameWithoutExtension}-options.json",
)
val keystoreFilePath = val keystoreFilePath =
keystoreFilePath ?: outputFilePath.parentFile keyStoreFilePath ?: outputFilePath.parentFile
.resolve("${outputFilePath.nameWithoutExtension}.keystore") .resolve("${outputFilePath.nameWithoutExtension}.keystore")
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 // endregion
// region Load patches // region Load patches
logger.info("Loading patches") logger.info("Loading patches")
val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray()) val patches = loadPatchesFromJar(patchesFiles)
// Warn if a patch can not be found in the supplied patch bundles.
if (warn) {
patches.map { it.name }.toHashSet().let { availableNames ->
(includedPatches + excludedPatches).filter { name ->
!availableNames.contains(name)
}
}.let { unknownPatches ->
if (unknownPatches.isEmpty()) return@let
logger.warning("Unknown input of patches:\n${unknownPatches.joinToString("\n")}")
}
}
// endregion // endregion
val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher")
val (packageName, patcherResult) = Patcher( val (packageName, patcherResult) = Patcher(
PatcherConfig( PatcherConfig(
apk, apk,
temporaryFilesPath, patcherTemporaryFilesPath,
aaptBinaryPath?.path, aaptBinaryPath?.path,
temporaryFilesPath.absolutePath, patcherTemporaryFilesPath.absolutePath,
true, true,
), ),
).use { patcher -> ).use { patcher ->
val filteredPatches = val packageName = patcher.context.packageMetadata.packageName
patcher.filterPatchSelection(patches).also { patches -> val packageVersion = patcher.context.packageMetadata.packageVersion
logger.info("Setting patch options")
if (optionsFile.exists()) { val filteredPatches = patches.filterPatchSelection(packageName, packageVersion)
patches.setOptions(optionsFile)
} else { logger.info("Setting patch options")
Options.serialize(patches, prettyPrint = true).let(optionsFile::writeText)
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.get()
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
} }
// region Save // region Save.
apk.copyTo(outputFilePath, overwrite = true) apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).apply {
patcherResult.applyTo(this)
patcherResult.applyTo(outputFilePath) }.let { patchedApkFile ->
if (!mount) {
if (!mount) { ApkUtils.signApk(
outputFilePath.sign( patchedApkFile,
ApkUtils.SigningOptions( outputFilePath,
keystoreFilePath,
keyStorePassword,
alias,
password,
signer, signer,
), ApkUtils.KeyStoreDetails(
) keystoreFilePath,
keyStorePassword,
keyStoreEntryAlias,
keyStoreEntryPassword,
),
)
} else {
patchedApkFile.copyTo(outputFilePath, overwrite = true)
}
} }
logger.info("Saved to $outputFilePath") logger.info("Saved to $outputFilePath")
// endregion // endregion
// region Install // region Install.
deviceSerial?.let { serial -> deviceSerial?.let {
AdbManager.getAdbManager(deviceSerial = serial.ifEmpty { null }, mount) val deviceSerial = it.ifEmpty { null }
}?.install(AdbManager.Apk(outputFilePath, packageName))
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 // endregion
@@ -351,61 +377,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. * @return The filtered patches.
*/ */
private fun Patcher.filterPatchSelection(patches: PatchSet): PatchSet = private fun Set<Patch<*>>.filterPatchSelection(
buildSet { packageName: String,
val packageName = context.packageMetadata.packageName packageVersion: String,
val packageVersion = context.packageMetadata.packageVersion ): 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 disabledPatches =
val patchName = patch.name!! selection.mapNotNull { it.disable?.selector?.name }.toSet()
val disabledPatchesByIndex =
selection.mapNotNull { it.disable?.selector?.index }.toSet()
val explicitlyExcluded = excludedPatches.contains(patchName) || excludedPatchesByIndex.contains(i) this@filterPatchSelection.withIndex().forEach patchLoop@{ (i, patch) ->
if (explicitlyExcluded) return@patch logger.info("Excluding $patchName") val patchName = patch.name!!
// Make sure the patch is compatible with the supplied APK files package name and version. val isManuallyDisabled = patchName in disabledPatches || i in disabledPatchesByIndex
patch.compatiblePackages?.let { packages -> if (isManuallyDisabled) return@patchLoop logger.info("\"$patchName\" disabled manually")
packages.singleOrNull { it.name == packageName }?.let { `package` ->
val matchesVersion =
force || `package`.versions?.let {
it.any { version -> version == packageVersion }
} ?: true
if (!matchesVersion) { // Make sure the patch is compatible with the supplied APK files package name and version.
return@patch logger.warning( patch.compatiblePackages?.let { packages ->
"$patchName is incompatible with version $packageVersion. " + packages.singleOrNull { (name, _) -> name == packageName }?.let { (_, versions) ->
"This patch is only compatible with version " + if (versions?.isEmpty() == true) {
packages.joinToString(";") { pkg -> return@patchLoop logger.warning("\"$patchName\" incompatible with \"$packageName\"")
pkg.versions!!.joinToString(", ") }
},
)
}
} ?: return@patch logger.fine(
"$patchName is incompatible with $packageName. " +
"This patch is only compatible with " +
packages.joinToString(", ") { `package` -> `package`.name },
)
return@let val matchesVersion =
} ?: logger.fine("$patchName has no constraint on packages.") force || versions?.let { it.any { version -> version == packageVersion } } ?: true
// If the patch is implicitly used, it will be only included if [exclusive] is false. if (!matchesVersion) {
val implicitlyIncluded = !exclusive && patch.use return@patchLoop logger.warning(
// If the patch is explicitly used, it will be included even if [exclusive] is false. "\"$patchName\" incompatible with $packageName $packageVersion " +
val explicitlyIncluded = includedPatches.contains(patchName) || includedPatchesByIndex.contains(i) "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 return@let
if (!included) return@patch logger.info("$patchName excluded") // Case 1. } ?: 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) { private fun purge(resourceCachePath: File) {
val result = val result =

View File

@@ -1,44 +1,63 @@
package app.revanced.cli.command.utility 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 picocli.CommandLine.*
import java.io.File import java.io.File
import java.util.logging.Logger import java.util.logging.Logger
@Command( @Command(
name = "install", name = "install",
description = ["Install an APK file to devices with the supplied ADB device serials"], description = ["Install an APK file."],
) )
internal object InstallCommand : Runnable { internal object InstallCommand : Runnable {
private val logger = Logger.getLogger(InstallCommand::class.java.name) private val logger = Logger.getLogger(this::class.java.name)
@Parameters( @Parameters(
description = ["ADB device serials. If not supplied, the first connected device will be used."], description = ["Serial of ADB devices. If not supplied, the first connected device will be used."],
arity = "0..*", arity = "0..*",
) )
private var deviceSerials: Array<String>? = null private var deviceSerials: Array<String>? = null
@Option( @Option(
names = ["-a", "--apk"], names = ["-a", "--apk"],
description = ["APK file to be installed"], description = ["APK file to be installed."],
required = true, required = true,
) )
private lateinit var apk: File private lateinit var apk: File
@Option( @Option(
names = ["-m", "--mount"], 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 private var packageName: String? = null
override fun run() { override fun run() {
fun install(deviceSerial: String? = null) = suspend fun install(deviceSerial: String? = null) {
try { val result = try {
AdbManager.getAdbManager(deviceSerial, packageName != null).install(AdbManager.Apk(apk, packageName)) if (packageName != null) {
} catch (e: AdbManager.DeviceNotFoundException) { AdbRootInstaller(deviceSerial)
} else {
AdbInstaller(deviceSerial)
}.install(Installer.Apk(apk, packageName))
} catch (e: Exception) {
logger.severe(e.toString()) 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()
}
} }
} }

View File

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

View File

@@ -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 }
}