mirror of
https://github.com/revanced/revanced-cli.git
synced 2024-12-24 11:05:49 +01:00
chore: merge branch dev
to main
(#236)
This commit is contained in:
commit
2a3dbafd17
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@ -33,9 +33,10 @@ jobs:
|
|||||||
build
|
build
|
||||||
node_modules
|
node_modules
|
||||||
key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }}
|
key: ${{ runner.os }}-gradle-npm-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'package-lock.json') }}
|
||||||
- name: Build with Gradle
|
- name: Build
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# Cleaning is necessary to avoid uploading two identical artifacts with different versions
|
||||||
run: ./gradlew clean --no-daemon
|
run: ./gradlew clean --no-daemon
|
||||||
- name: Setup semantic-release
|
- name: Setup semantic-release
|
||||||
run: npm install
|
run: npm install
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
[
|
[
|
||||||
"@semantic-release/commit-analyzer", {
|
"@semantic-release/commit-analyzer", {
|
||||||
"releaseRules": [
|
"releaseRules": [
|
||||||
{ "type": "build", "scope": "revanced-patcher", "release": "patch" }
|
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
115
CHANGELOG.md
115
CHANGELOG.md
@ -1,3 +1,118 @@
|
|||||||
|
# [3.0.0-dev.10](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.9...v3.0.0-dev.10) (2023-08-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* filtration of patches malfunctioning ([2d5a7fd](https://github.com/ReVanced/revanced-cli/commit/2d5a7fdf1eb2e13f5013a790b03f09851b167fe0))
|
||||||
|
|
||||||
|
# [3.0.0-dev.9](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.8...v3.0.0-dev.9) (2023-08-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Check for missing integrations ([c93186f](https://github.com/ReVanced/revanced-cli/commit/c93186fb9700907e65f33442e88073783cc163de))
|
||||||
|
|
||||||
|
# [3.0.0-dev.8](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.7...v3.0.0-dev.8) (2023-08-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* do not delete output file ([0f3e090](https://github.com/ReVanced/revanced-cli/commit/0f3e090418771e951dfd15e5c193421f72cbe459))
|
||||||
|
|
||||||
|
# [3.0.0-dev.7](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.6...v3.0.0-dev.7) (2023-08-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* print stack trace when a patch failed ([924c1f8](https://github.com/ReVanced/revanced-cli/commit/924c1f80ec0d17a3bdc07a0fb2015e44c49162e4))
|
||||||
|
|
||||||
|
# [3.0.0-dev.6](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.5...v3.0.0-dev.6) (2023-08-24)
|
||||||
|
|
||||||
|
# [3.0.0-dev.5](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.4...v3.0.0-dev.5) (2023-08-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* also delete temporary files when uninstalling ([52c3be2](https://github.com/ReVanced/revanced-cli/commit/52c3be23f2915dccaee7f9941413c8f81e14acc8))
|
||||||
|
* delete temporary files after root installation ([a3d8705](https://github.com/ReVanced/revanced-cli/commit/a3d8705e89732a0dd4f51de28c405b6af13c8633))
|
||||||
|
* fix running commands not running ([2c7fcaf](https://github.com/ReVanced/revanced-cli/commit/2c7fcaf4add65a12052afc5bef779dbc73debd69))
|
||||||
|
* only check once for patch options ([11c3a6c](https://github.com/ReVanced/revanced-cli/commit/11c3a6cfd4fe59ba5d703358634a1853e1cc22a5))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add install command ([0350b7f](https://github.com/ReVanced/revanced-cli/commit/0350b7f1a276d9dc795b22442ba4f202855ea090))
|
||||||
|
* use friendly descriptions ([3dd875d](https://github.com/ReVanced/revanced-cli/commit/3dd875d14cca488ade6d21bbd4cce0d481692134))
|
||||||
|
|
||||||
|
# [3.0.0-dev.4](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.3...v3.0.0-dev.4) (2023-08-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* properly make use of logging facade ([41898d7](https://github.com/ReVanced/revanced-cli/commit/41898d7547690e3130372414515c5645e5dc2634))
|
||||||
|
|
||||||
|
# [3.0.0-dev.3](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.2...v3.0.0-dev.3) (2023-08-23)
|
||||||
|
|
||||||
|
# [3.0.0-dev.2](https://github.com/ReVanced/revanced-cli/compare/v3.0.0-dev.1...v3.0.0-dev.2) (2023-08-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* specify correct class containing entry-point ([1fcc591](https://github.com/ReVanced/revanced-cli/commit/1fcc591222ab67112f2b78174a8b94106846838c))
|
||||||
|
|
||||||
|
# [3.0.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.5...v3.0.0-dev.1) (2023-08-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* do not use absolute path from custom AAPT2 binary option ([a9c2a5f](https://github.com/ReVanced/revanced-cli/commit/a9c2a5f096627dbbf8ab1b8da26fb14529ce6bc3))
|
||||||
|
* use correct option name ([f8972ea](https://github.com/ReVanced/revanced-cli/commit/f8972eac3e5ee0a4a186c12cbe711925656d657b))
|
||||||
|
|
||||||
|
|
||||||
|
* refactor!: restructure code ([07da528](https://github.com/ReVanced/revanced-cli/commit/07da528ce2223582f84bf64d2fec69714c647ddc))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add options command ([9edbbf3](https://github.com/ReVanced/revanced-cli/commit/9edbbf31635603f89fc7bc5dcc6c023d4cdbb5a6))
|
||||||
|
* use better logging text ([b0e748d](https://github.com/ReVanced/revanced-cli/commit/b0e748daff527ee7f417b3069882e074896fc131))
|
||||||
|
* use separate command to list patches ([b74213f](https://github.com/ReVanced/revanced-cli/commit/b74213f66e0d04d3a0ae6197d069631388e06580))
|
||||||
|
* use separate command to patch ([32da961](https://github.com/ReVanced/revanced-cli/commit/32da961d57537e99b39fd92b625a1c73f8314bc6))
|
||||||
|
* use separate command to uninstall ([c0cc909](https://github.com/ReVanced/revanced-cli/commit/c0cc90962646cfffd5e2730ae556423271a7990b))
|
||||||
|
* use simpler log ([ba758f0](https://github.com/ReVanced/revanced-cli/commit/ba758f00f4ce18791439b7e72fe1ad2e7f11f8af))
|
||||||
|
|
||||||
|
|
||||||
|
### BREAKING CHANGES
|
||||||
|
|
||||||
|
* This introduces major changes to how ReVanced CLI is used from the command line.
|
||||||
|
|
||||||
|
# [2.23.0-dev.5](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.4...v2.23.0-dev.5) (2023-08-14)
|
||||||
|
|
||||||
|
# [2.23.0-dev.4](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.3...v2.23.0-dev.4) (2023-08-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* show full package name when listing patches ([#240](https://github.com/ReVanced/revanced-cli/issues/240)) ([7174364](https://github.com/ReVanced/revanced-cli/commit/7174364ef8ef5d6ce8351a8340f9c1a5b58eac3c))
|
||||||
|
|
||||||
|
# [2.23.0-dev.3](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.2...v2.23.0-dev.3) (2023-08-03)
|
||||||
|
|
||||||
|
# [2.23.0-dev.2](https://github.com/ReVanced/revanced-cli/compare/v2.23.0-dev.1...v2.23.0-dev.2) (2023-08-03)
|
||||||
|
|
||||||
|
# [2.23.0-dev.1](https://github.com/ReVanced/revanced-cli/compare/v2.22.1-dev.1...v2.23.0-dev.1) (2023-07-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Improve command line argument descriptions ([f9cf7d2](https://github.com/ReVanced/revanced-cli/commit/f9cf7d21b7f1c2f11234d604a1047b9d2b165f83))
|
||||||
|
|
||||||
|
## [2.22.1-dev.1](https://github.com/ReVanced/revanced-cli/compare/v2.22.0...v2.22.1-dev.1) (2023-07-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* print original instead of kebab cased names ([5eaad33](https://github.com/ReVanced/revanced-cli/commit/5eaad33dc1fbd24c36e1498f04e21d068e85f53e))
|
||||||
|
|
||||||
# [2.22.0](https://github.com/revanced/revanced-cli/compare/v2.21.5...v2.22.0) (2023-07-11)
|
# [2.22.0](https://github.com/revanced/revanced-cli/compare/v2.21.5...v2.22.0) (2023-07-11)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
# 💻 ReVanced CLI
|
# 💻 ReVanced CLI
|
||||||
|
|
||||||
Command line application as an alternative to the ReVanced Manager.
|
Command line application to use ReVanced.
|
||||||
|
@ -1,42 +1,23 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.8.20"
|
kotlin("jvm") version "1.8.20"
|
||||||
id("com.github.johnrengelman.shadow") version "7.1.2"
|
alias(libs.plugins.shadow)
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "app.revanced"
|
group = "app.revanced"
|
||||||
|
|
||||||
val githubUsername: String = project.findProperty("gpr.user") as? String ?: System.getenv("GITHUB_ACTOR")
|
|
||||||
val githubPassword: String = project.findProperty("gpr.key") as? String ?: System.getenv("GITHUB_TOKEN")
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
mavenLocal()
|
|
||||||
maven {
|
|
||||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
|
||||||
credentials {
|
|
||||||
username = githubUsername
|
|
||||||
password = githubPassword
|
|
||||||
}
|
|
||||||
}
|
|
||||||
maven { url = uri("https://jitpack.io") }
|
|
||||||
google()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC")
|
implementation(libs.revanced.patcher)
|
||||||
|
implementation(libs.kotlin.reflect)
|
||||||
implementation("app.revanced:revanced-patcher:11.0.3")
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
implementation("info.picocli:picocli:4.7.1")
|
implementation(libs.picocli)
|
||||||
implementation("com.github.revanced:jadb:2531a28109") // updated fork
|
implementation(libs.jadb) // Updated fork
|
||||||
implementation("com.android.tools.build:apksig:8.1.0-alpha09")
|
implementation(libs.apksig)
|
||||||
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
|
implementation(libs.bcpkix.jdk15on)
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.+")
|
implementation(libs.jackson.module.kotlin)
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
|
testImplementation(libs.kotlin.test)
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin { jvmToolchain(11) }
|
||||||
jvmToolchain(11)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
test {
|
test {
|
||||||
@ -45,12 +26,14 @@ tasks {
|
|||||||
events("PASSED", "SKIPPED", "FAILED")
|
events("PASSED", "SKIPPED", "FAILED")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
build {
|
|
||||||
dependsOn(shadowJar)
|
processResources {
|
||||||
|
expand("projectVersion" to project.version)
|
||||||
}
|
}
|
||||||
|
|
||||||
shadowJar {
|
shadowJar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes("Main-Class" to "app.revanced.cli.main.MainKt")
|
attributes("Main-Class" to "app.revanced.cli.command.MainCommandKt")
|
||||||
}
|
}
|
||||||
minimize {
|
minimize {
|
||||||
exclude(dependency("org.jetbrains.kotlin:.*"))
|
exclude(dependency("org.jetbrains.kotlin:.*"))
|
||||||
@ -58,6 +41,11 @@ tasks {
|
|||||||
exclude(dependency("app.revanced:.*"))
|
exclude(dependency("app.revanced:.*"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
build {
|
||||||
|
dependsOn(shadowJar)
|
||||||
|
}
|
||||||
|
|
||||||
// Dummy task to fix the Gradle semantic-release plugin.
|
// Dummy task to fix the Gradle semantic-release plugin.
|
||||||
// Remove this if you forked it to support building only.
|
// Remove this if you forked it to support building only.
|
||||||
// Tracking issue: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
|
// Tracking issue: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
# 💼 Prerequisites
|
# 💼 Prerequisites
|
||||||
|
|
||||||
To use ReVanced CLI, you will need to fulfill certain requirements.
|
To use ReVanced CLI, you will need to fulfil specific requirements.
|
||||||
|
|
||||||
## 🤝 Requirements
|
## 🤝 Requirements
|
||||||
|
|
||||||
- Java SDK 11 (Azul JDK or OpenJDK)
|
- Java SDK 11 (Azul Zulu JDK or OpenJDK)
|
||||||
- [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) if you want to deploy the patched APK file on your device
|
- [Android Debug Bridge (adb)](https://developer.android.com/studio/command-line/adb) if you want to install the patched APK file on your device
|
||||||
- An ABI other than ARMv7 such as x86 or x86-64 (or a custom AAPT binary that supports ARMv7)
|
- An ABI other than ARMv7 such as x86 or x86-64 (or a custom AAPT binary that supports ARMv7)
|
||||||
|
- ReVanced Patches
|
||||||
|
- ReVanced Integrations, if the patches require it
|
||||||
|
|
||||||
## ⏭️ Whats next
|
## ⏭️ Whats next
|
||||||
|
|
||||||
The next section will show, how to use [ReVanced CLI](https://github.com/revanced/revanced-cli).
|
The following section will show you how to use ReVanced CLI.
|
||||||
|
|
||||||
Continue: [🛠️ Using ReVanced CLI](1_usage.md)
|
Continue: [🛠️ Using ReVanced CLI](1_usage.md)
|
||||||
|
@ -10,13 +10,14 @@ Learn how to ReVanced CLI.
|
|||||||
adb shell exit
|
adb shell exit
|
||||||
```
|
```
|
||||||
|
|
||||||
If you want to deploy the patched APK file on your device by mounting it on top of the original APK file, you will need root access. This is optional.
|
Optionally, you can install the patched APK file on your device by mounting it on top of the original APK file.
|
||||||
|
You will need root permissions for this. Check if you have root permissions by running the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
adb shell su -c exit
|
adb shell su -c exit
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Get the name of your device
|
2. Get your device's serial
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
adb devices
|
adb devices
|
||||||
@ -30,47 +31,81 @@ Learn how to ReVanced CLI.
|
|||||||
java -jar revanced-cli.jar -h
|
java -jar revanced-cli.jar -h
|
||||||
```
|
```
|
||||||
|
|
||||||
- ### 📃 List all available patches from supplied patch bundles
|
- ### 📃 List patches from supplied patch bundles
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
java -jar revanced-cli.jar
|
java -jar revanced-cli.jar list-patches \
|
||||||
-b revanced-patches.jar \
|
--with-packages \
|
||||||
-l # Names of all patches will be in kebab-case
|
--with-versions \
|
||||||
|
--with-options \
|
||||||
|
revanced-patches.jar [<patch-bundle> ...]
|
||||||
```
|
```
|
||||||
|
|
||||||
- ### 💉 Use ReVanced CLI to patch an APK file but deploy without root permissions
|
- ### ⚙️ Generate options from patches using ReVanced CLI
|
||||||
|
|
||||||
This will deploy the patched APK file on your device by installing it.
|
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> ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note**: A default `options.json` file will be automatically generated, if it does not exist
|
||||||
|
without any need for intervention when using the `patch` command.
|
||||||
|
|
||||||
|
- ### 💉 Use ReVanced CLI to patch an APK file but install without root permissions
|
||||||
|
|
||||||
|
This will install the patched APK file regularly on your device.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
java -jar revanced-cli.jar \
|
java -jar revanced-cli.jar patch \
|
||||||
-a input.apk \
|
--patch-bundle revanced-patches.jar \
|
||||||
-o patched-output.apk \
|
--out output.apk \
|
||||||
-b revanced-patches.jar \
|
--device-serial <device-serial> \
|
||||||
-d device-name
|
input.apk
|
||||||
```
|
```
|
||||||
|
|
||||||
- ### 👾 Use ReVanced CLI to patch an APK file but deploy with root permissions
|
- ### 👾 Use ReVanced CLI to patch an APK file but install with root permissions
|
||||||
|
|
||||||
This will deploy the patched APK file on your device by mounting it on top of the original APK file.
|
This will install the patched APK file on your device by mounting it on top of the original APK file.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
adb install input.apk
|
adb install input.apk
|
||||||
java -jar revanced-cli.jar \
|
java -jar revanced-cli.jar patch \
|
||||||
-a input.apk \
|
--patch-bundle revanced-patches.jar \
|
||||||
-o patched-output.apk \
|
--include some-other-patch \
|
||||||
-b revanced-patches.jar \
|
--exclude some-patch \
|
||||||
-e vanced-microg-support \
|
--out patched-output.apk \
|
||||||
-d device-name \
|
--device-serial <device-serial> \
|
||||||
--mount
|
--mount \
|
||||||
|
input.apk
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note**: Some patches from [ReVanced Patches](https://github.com/revanced/revanced-patches) also require [ReVanced Integrations](https://github.com/revanced/revanced-integrations). Supply them with the option `-m`. ReVanced Patcher will merge ReVanced Integrations automatically, depending on if the supplied patches require them.
|
> **Note**: Some patches may require integrations
|
||||||
|
such as [ReVanced Integrations](https://github.com/revanced/revanced-integrations).
|
||||||
|
Supply them with the option `--merge`. If any patches accepted by ReVanced Patcher require ReVanced Integrations,
|
||||||
|
they will be merged into the APK file automatically.
|
||||||
|
|
||||||
- ### ⚙️ Supply options to patches using ReVanced CLI
|
- ### 🗑️ Uninstall a patched APK file
|
||||||
|
```bash
|
||||||
|
java -jar revanced-cli.jar utility uninstall \
|
||||||
|
--package-name <package-name> \
|
||||||
|
<device-serial>
|
||||||
|
```
|
||||||
|
|
||||||
Some patches provide options. Currently, ReVanced CLI will generate and consume an `options.json` file at the location that is specified in `-o`. If the option is not specified, the options file will be generated in the current working directory.
|
> **Note**: You can unmount an APK file
|
||||||
|
with the option `--unmount`.
|
||||||
The options file contains all options from supplied patch bundles.
|
|
||||||
|
|
||||||
> **Note**: The `options.json` file will be generated at the first time you use ReVanced CLI to patch an APK file for now. This will be changed in the future.
|
- ### ️ ⚙️ Manually install an APK file
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar revanced-cli.jar utility install \
|
||||||
|
-a input.apk \
|
||||||
|
<device-serial>
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note**: You can mount an APK file
|
||||||
|
by supplying the package name of the app to mount the supplied APK file to over the option `--mount`.
|
||||||
|
@ -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 = 2.22.0
|
version = 3.0.0-dev.10
|
||||||
|
25
gradle/libs.versions.toml
Normal file
25
gradle/libs.versions.toml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
[versions]
|
||||||
|
shadow = "8.1.1"
|
||||||
|
apksig = "8.1.0"
|
||||||
|
bcpkix-jdk15on = "1.70"
|
||||||
|
jackson-module-kotlin = "2.14.3"
|
||||||
|
jadb = "2531a28109"
|
||||||
|
kotlin-reflect = "1.9.0"
|
||||||
|
kotlin-test = "1.8.20-RC"
|
||||||
|
kotlinx-coroutines-core = "1.7.1"
|
||||||
|
picocli = "4.7.3"
|
||||||
|
revanced-patcher = "14.1.0"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }
|
||||||
|
bcpkix-jdk15on = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bcpkix-jdk15on" }
|
||||||
|
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson-module-kotlin" }
|
||||||
|
jadb = { module = "com.github.revanced:jadb", version.ref = "jadb" }
|
||||||
|
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
|
||||||
|
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" }
|
||||||
|
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" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" }
|
@ -1 +1,23 @@
|
|||||||
|
val githubUsername: String = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
|
||||||
|
val githubPassword: String = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
|
||||||
|
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
mavenLocal()
|
||||||
|
google()
|
||||||
|
maven { url = uri("https://jitpack.io") }
|
||||||
|
listOf("revanced-patcher", "jadb").forEach { repo ->
|
||||||
|
maven {
|
||||||
|
url = uri("https://maven.pkg.github.com/revanced/$repo")
|
||||||
|
credentials {
|
||||||
|
username = githubUsername
|
||||||
|
password = githubPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rootProject.name = "revanced-cli"
|
rootProject.name = "revanced-cli"
|
@ -1,37 +0,0 @@
|
|||||||
package app.revanced.cli.aligning
|
|
||||||
|
|
||||||
import app.revanced.cli.command.MainCommand.logger
|
|
||||||
import app.revanced.patcher.PatcherResult
|
|
||||||
import app.revanced.utils.signing.align.ZipAligner
|
|
||||||
import app.revanced.utils.signing.align.zip.ZipFile
|
|
||||||
import app.revanced.utils.signing.align.zip.structures.ZipEntry
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
object Aligning {
|
|
||||||
fun align(result: PatcherResult, inputFile: File, outputFile: File) {
|
|
||||||
logger.info("Aligning ${inputFile.name} to ${outputFile.name}")
|
|
||||||
|
|
||||||
if (outputFile.exists()) outputFile.delete()
|
|
||||||
|
|
||||||
ZipFile(outputFile).use { file ->
|
|
||||||
result.dexFiles.forEach {
|
|
||||||
file.addEntryCompressData(
|
|
||||||
ZipEntry.createWithName(it.name),
|
|
||||||
it.stream.readBytes()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.resourceFile?.let {
|
|
||||||
file.copyEntriesFromFileAligned(
|
|
||||||
ZipFile(it),
|
|
||||||
ZipAligner::getEntryAlignment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
file.copyEntriesFromFileAligned(
|
|
||||||
ZipFile(inputFile),
|
|
||||||
ZipAligner::getEntryAlignment
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,91 @@
|
|||||||
|
package app.revanced.cli.command
|
||||||
|
|
||||||
|
import app.revanced.patcher.PatchBundleLoader
|
||||||
|
import app.revanced.patcher.annotation.Package
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.description
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.options
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||||
|
import app.revanced.patcher.patch.PatchClass
|
||||||
|
import app.revanced.patcher.patch.PatchOption
|
||||||
|
import picocli.CommandLine.*
|
||||||
|
import picocli.CommandLine.Help.Visibility.ALWAYS
|
||||||
|
import java.io.File
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
|
||||||
|
@Command(name = "list-patches", description = ["List patches from supplied patch bundles"])
|
||||||
|
internal object ListPatchesCommand : Runnable {
|
||||||
|
private val logger = Logger.getLogger(ListPatchesCommand::class.java.name)
|
||||||
|
|
||||||
|
@Parameters(
|
||||||
|
description = ["Paths to patch bundles"], arity = "1..*"
|
||||||
|
)
|
||||||
|
private lateinit var patchBundles: Array<File>
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-d", "--with-descriptions"], description = ["List their descriptions"], showDefaultValue = ALWAYS
|
||||||
|
)
|
||||||
|
private var withDescriptions: Boolean = true
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-p", "--with-packages"],
|
||||||
|
description = ["List the packages the patches are compatible with"],
|
||||||
|
showDefaultValue = ALWAYS
|
||||||
|
)
|
||||||
|
private var withPackages: Boolean = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-v", "--with-versions"],
|
||||||
|
description = ["List the versions of the apps the patches are compatible with"],
|
||||||
|
showDefaultValue = ALWAYS
|
||||||
|
)
|
||||||
|
private var withVersions: Boolean = false
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-o", "--with-options"], description = ["List the options of the patches"], showDefaultValue = ALWAYS
|
||||||
|
)
|
||||||
|
private var withOptions: Boolean = false
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
fun Package.buildString() = buildString {
|
||||||
|
if (withVersions && versions.isNotEmpty()) {
|
||||||
|
appendLine("Package name: $name")
|
||||||
|
appendLine("Compatible versions:")
|
||||||
|
append(versions.joinToString("\n") { version -> version }.prependIndent("\t"))
|
||||||
|
} else append("Package name: $name")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PatchOption<*>.buildString() = buildString {
|
||||||
|
appendLine("Title: $title")
|
||||||
|
appendLine("Description: $description")
|
||||||
|
|
||||||
|
value?.let {
|
||||||
|
appendLine("Key: $key")
|
||||||
|
append("Value: $it")
|
||||||
|
} ?: append("Key: $key")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PatchClass.buildString() = buildString {
|
||||||
|
append("Name: $patchName")
|
||||||
|
|
||||||
|
if (withDescriptions) append("\nDescription: $description")
|
||||||
|
|
||||||
|
if (withOptions && options != null) {
|
||||||
|
appendLine("\nOptions:")
|
||||||
|
append(
|
||||||
|
options!!.joinToString("\n\n") { option -> option.buildString() }.prependIndent("\t")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withPackages && compatiblePackages != null) {
|
||||||
|
appendLine("\nCompatible packages:")
|
||||||
|
append(
|
||||||
|
compatiblePackages!!.joinToString("\n") { it.buildString() }.prependIndent("\t")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(PatchBundleLoader.Jar(*patchBundles).joinToString("\n\n") { it.buildString() })
|
||||||
|
}
|
||||||
|
}
|
@ -1,265 +1,67 @@
|
|||||||
package app.revanced.cli.command
|
package app.revanced.cli.command
|
||||||
|
|
||||||
import app.revanced.cli.aligning.Aligning
|
import app.revanced.cli.command.utility.UtilityCommand
|
||||||
import app.revanced.cli.logging.impl.DefaultCliLogger
|
import app.revanced.patcher.patch.PatchClass
|
||||||
import app.revanced.cli.patcher.Patcher
|
import picocli.CommandLine
|
||||||
import app.revanced.cli.patcher.logging.impl.PatcherLogger
|
import picocli.CommandLine.Command
|
||||||
import app.revanced.cli.signing.Signing
|
import picocli.CommandLine.IVersionProvider
|
||||||
import app.revanced.cli.signing.SigningOptions
|
import java.util.*
|
||||||
import app.revanced.patcher.PatcherOptions
|
import java.util.logging.*
|
||||||
import app.revanced.patcher.data.Context
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.description
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
|
||||||
import app.revanced.patcher.patch.Patch
|
|
||||||
import app.revanced.patcher.util.patch.PatchBundle
|
|
||||||
import app.revanced.utils.Options
|
|
||||||
import app.revanced.utils.Options.setOptions
|
|
||||||
import app.revanced.utils.adb.Adb
|
|
||||||
import picocli.CommandLine.*
|
|
||||||
import java.io.File
|
|
||||||
import java.nio.file.Files
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alias for return type of [PatchBundle.loadPatches].
|
|
||||||
*/
|
|
||||||
internal typealias PatchList = List<Class<out Patch<Context>>>
|
|
||||||
|
|
||||||
private class CLIVersionProvider : IVersionProvider {
|
fun main(args: Array<String>) {
|
||||||
override fun getVersion() = arrayOf(
|
System.setProperty("java.util.logging.SimpleFormatter.format", "%4\$s: %5\$s %n")
|
||||||
MainCommand::class.java.`package`.implementationVersion ?: "unknown"
|
Logger.getLogger("").apply {
|
||||||
)
|
handlers.forEach {
|
||||||
|
it.close()
|
||||||
|
removeHandler(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
object : Handler() {
|
||||||
|
override fun publish(record: LogRecord) = formatter.format(record).toByteArray().let {
|
||||||
|
if (record.level.intValue() > Level.INFO.intValue())
|
||||||
|
System.err.write(it)
|
||||||
|
else
|
||||||
|
System.out.write(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun flush() {
|
||||||
|
System.out.flush()
|
||||||
|
System.err.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() = flush()
|
||||||
|
}.also {
|
||||||
|
it.level = Level.ALL
|
||||||
|
it.formatter = SimpleFormatter()
|
||||||
|
}.let(::addHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandLine(MainCommand).execute(*args)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal typealias PatchList = List<PatchClass>
|
||||||
|
|
||||||
|
private object CLIVersionProvider : IVersionProvider {
|
||||||
|
override fun getVersion(): Array<String> {
|
||||||
|
Properties().apply {
|
||||||
|
load(MainCommand::class.java.getResourceAsStream("/app/revanced/cli/version.properties"))
|
||||||
|
}.let {
|
||||||
|
return arrayOf("ReVanced CLI v${it.getProperty("version")}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Command(
|
@Command(
|
||||||
name = "ReVanced-CLI",
|
name = "revanced-cli",
|
||||||
|
description = ["Command line application to use ReVanced"],
|
||||||
mixinStandardHelpOptions = true,
|
mixinStandardHelpOptions = true,
|
||||||
versionProvider = CLIVersionProvider::class
|
versionProvider = CLIVersionProvider::class,
|
||||||
|
subcommands = [
|
||||||
|
ListPatchesCommand::class,
|
||||||
|
PatchCommand::class,
|
||||||
|
OptionsCommand::class,
|
||||||
|
UtilityCommand::class,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
internal object MainCommand : Runnable {
|
private object MainCommand
|
||||||
val logger = DefaultCliLogger()
|
|
||||||
|
|
||||||
@ArgGroup(exclusive = false, multiplicity = "1")
|
|
||||||
lateinit var args: Args
|
|
||||||
|
|
||||||
class Args {
|
|
||||||
// TODO: Move this so it is not required when listing patches
|
|
||||||
@Option(names = ["-a", "--apk"], description = ["Input APK file to be patched"], required = true)
|
|
||||||
lateinit var inputFile: File
|
|
||||||
|
|
||||||
@Option(names = ["--uninstall"], description = ["Uninstall the mount variant"])
|
|
||||||
var uninstall: Boolean = false
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
names = ["-d", "--deploy-on"],
|
|
||||||
description = ["If specified, deploy to device over ADB with given name"]
|
|
||||||
)
|
|
||||||
var deploy: String? = null
|
|
||||||
|
|
||||||
@ArgGroup(exclusive = false)
|
|
||||||
var patchArgs: PatchArgs? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
class PatchArgs {
|
|
||||||
@Option(names = ["-b", "--bundle"], description = ["One or more bundles of patches"], required = true)
|
|
||||||
var patchBundles = arrayOf<String>()
|
|
||||||
|
|
||||||
@Option(names = ["--options"], description = ["Path to patch options JSON file"])
|
|
||||||
var optionsFile: File = File("options.json")
|
|
||||||
|
|
||||||
@ArgGroup(exclusive = false)
|
|
||||||
var listingArgs: ListingArgs? = null
|
|
||||||
|
|
||||||
@ArgGroup(exclusive = false)
|
|
||||||
var patchingArgs: PatchingArgs? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
class ListingArgs {
|
|
||||||
@Option(names = ["-l", "--list"], description = ["List patches"], required = true)
|
|
||||||
var listOnly: Boolean = false
|
|
||||||
|
|
||||||
@Option(names = ["--with-versions"], description = ["List patches with compatible versions"])
|
|
||||||
var withVersions: Boolean = false
|
|
||||||
|
|
||||||
@Option(names = ["--with-packages"], description = ["List patches with compatible packages"])
|
|
||||||
var withPackages: Boolean = false
|
|
||||||
}
|
|
||||||
|
|
||||||
class PatchingArgs {
|
|
||||||
@Option(names = ["-o", "--out"], description = ["Output file path"], required = true)
|
|
||||||
lateinit var outputPath: String
|
|
||||||
|
|
||||||
@Option(names = ["-e", "--exclude"], description = ["Explicitly exclude patches"])
|
|
||||||
var excludedPatches = arrayOf<String>()
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
names = ["--exclusive"],
|
|
||||||
description = ["Only installs the patches you include, not including any patch by default"]
|
|
||||||
)
|
|
||||||
var exclusive = false
|
|
||||||
|
|
||||||
@Option(names = ["-i", "--include"], description = ["Include patches"])
|
|
||||||
var includedPatches = arrayOf<String>()
|
|
||||||
|
|
||||||
@Option(names = ["--experimental"], description = ["Disable patch version compatibility patch"])
|
|
||||||
var experimental: Boolean = false
|
|
||||||
|
|
||||||
@Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"])
|
|
||||||
var mergeFiles = listOf<File>()
|
|
||||||
|
|
||||||
@Option(names = ["--mount"], description = ["If specified, instead of installing, mount"])
|
|
||||||
var mount: Boolean = false
|
|
||||||
|
|
||||||
@Option(names = ["--cn"], description = ["Overwrite the default CN for the signed file"])
|
|
||||||
var cn = "ReVanced"
|
|
||||||
|
|
||||||
@Option(names = ["--keystore"], description = ["File path to your keystore"])
|
|
||||||
var keystorePath: String? = null
|
|
||||||
|
|
||||||
@Option(names = ["-p", "--password"], description = ["Overwrite the default password for the signed file"])
|
|
||||||
var password = "ReVanced"
|
|
||||||
|
|
||||||
@Option(names = ["-t", "--temp-dir"], description = ["Temporary resource cache directory"])
|
|
||||||
var cacheDirectory = "revanced-cache"
|
|
||||||
|
|
||||||
@Option(
|
|
||||||
names = ["-c", "--clean"],
|
|
||||||
description = ["Clean the temporary resource cache directory. This will be done anyways when running the patcher"]
|
|
||||||
)
|
|
||||||
var clean: Boolean = false
|
|
||||||
|
|
||||||
@Option(names = ["--custom-aapt2-binary"], description = ["Path to custom aapt2 binary"])
|
|
||||||
var aaptPath: String = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
if (args.patchArgs?.listingArgs?.listOnly == true) return printListOfPatches()
|
|
||||||
if (args.uninstall) return uninstall()
|
|
||||||
|
|
||||||
val pArgs = this.args.patchArgs?.patchingArgs ?: return
|
|
||||||
val outputFile = File(pArgs.outputPath) // the file to write to
|
|
||||||
|
|
||||||
val allPatches = args.patchArgs!!.patchBundles.flatMap { bundle ->
|
|
||||||
PatchBundle.Jar(bundle).loadPatches()
|
|
||||||
}
|
|
||||||
|
|
||||||
args.patchArgs!!.optionsFile.let {
|
|
||||||
if (it.exists()) allPatches.setOptions(it, logger)
|
|
||||||
else Options.serialize(allPatches, prettyPrint = true).let(it::writeText)
|
|
||||||
}
|
|
||||||
|
|
||||||
val patcher = app.revanced.patcher.Patcher(
|
|
||||||
PatcherOptions(
|
|
||||||
args.inputFile.also { if (!it.exists()) return logger.error("Input file ${args.inputFile} does not exist.") },
|
|
||||||
pArgs.cacheDirectory,
|
|
||||||
pArgs.aaptPath,
|
|
||||||
pArgs.cacheDirectory,
|
|
||||||
PatcherLogger
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// prepare adb
|
|
||||||
val adb: Adb? = args.deploy?.let {
|
|
||||||
Adb(outputFile, patcher.context.packageMetadata.packageName, args.deploy!!, !pArgs.mount)
|
|
||||||
}
|
|
||||||
|
|
||||||
// start the patcher
|
|
||||||
val result = Patcher.start(patcher, allPatches)
|
|
||||||
|
|
||||||
val cacheDirectory = File(pArgs.cacheDirectory)
|
|
||||||
|
|
||||||
// align the file
|
|
||||||
val alignedFile = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_aligned.apk")
|
|
||||||
Aligning.align(result, args.inputFile, alignedFile)
|
|
||||||
|
|
||||||
// sign the file
|
|
||||||
val finalFile = if (!pArgs.mount) {
|
|
||||||
val signedOutput = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_signed.apk")
|
|
||||||
Signing.sign(
|
|
||||||
alignedFile,
|
|
||||||
signedOutput,
|
|
||||||
SigningOptions(
|
|
||||||
pArgs.cn,
|
|
||||||
pArgs.password,
|
|
||||||
pArgs.keystorePath ?: outputFile.absoluteFile.parentFile
|
|
||||||
.resolve("${outputFile.nameWithoutExtension}.keystore")
|
|
||||||
.canonicalPath
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
signedOutput
|
|
||||||
} else
|
|
||||||
alignedFile
|
|
||||||
|
|
||||||
// finally copy to the specified output file
|
|
||||||
logger.info("Copying ${finalFile.name} to ${outputFile.name}")
|
|
||||||
finalFile.copyTo(outputFile, overwrite = true)
|
|
||||||
|
|
||||||
// clean up the cache directory if needed
|
|
||||||
if (pArgs.clean)
|
|
||||||
cleanUp(pArgs.cacheDirectory)
|
|
||||||
|
|
||||||
// deploy if specified
|
|
||||||
adb?.deploy()
|
|
||||||
|
|
||||||
if (pArgs.clean && args.deploy != null) Files.delete(outputFile.toPath())
|
|
||||||
|
|
||||||
logger.info("Finished")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cleanUp(cacheDirectory: String) {
|
|
||||||
val result = if (File(cacheDirectory).deleteRecursively())
|
|
||||||
"Cleaned up cache directory"
|
|
||||||
else
|
|
||||||
"Failed to clean up cache directory"
|
|
||||||
logger.info(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun uninstall() {
|
|
||||||
val adb: Adb? = args.deploy?.let {
|
|
||||||
Adb(
|
|
||||||
File("placeholder_file"),
|
|
||||||
app.revanced.patcher.Patcher(PatcherOptions(args.inputFile, "")).context.packageMetadata.packageName,
|
|
||||||
args.deploy!!,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
adb?.uninstall()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun printListOfPatches() {
|
|
||||||
val logged = mutableListOf<String>()
|
|
||||||
for (patchBundlePath in args.patchArgs?.patchBundles!!) for (patch in PatchBundle.Jar(patchBundlePath)
|
|
||||||
.loadPatches()) {
|
|
||||||
if (patch.patchName in logged) continue
|
|
||||||
for (compatiblePackage in patch.compatiblePackages ?: continue) {
|
|
||||||
val packageEntryStr = buildString {
|
|
||||||
// Add package if flag is set
|
|
||||||
if (args.patchArgs?.listingArgs?.withPackages == true) {
|
|
||||||
val packageName = compatiblePackage.name.substringAfterLast(".").padStart(10)
|
|
||||||
append(packageName)
|
|
||||||
append("\t")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add patch name
|
|
||||||
val patchName = patch.patchName.lowercase().replace(" ", "-").padStart(25)
|
|
||||||
append(patchName)
|
|
||||||
|
|
||||||
// Add description if flag is set.
|
|
||||||
append("\t")
|
|
||||||
append(patch.description)
|
|
||||||
|
|
||||||
// Add compatible versions, if flag is set
|
|
||||||
if (args.patchArgs?.listingArgs?.withVersions == true) {
|
|
||||||
val compatibleVersions = compatiblePackage.versions.joinToString(separator = ", ")
|
|
||||||
append("\t")
|
|
||||||
append(compatibleVersions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logged.add(patch.patchName)
|
|
||||||
logger.info(packageEntryStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
46
src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt
Normal file
46
src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package app.revanced.cli.command
|
||||||
|
|
||||||
|
import app.revanced.patcher.PatchBundleLoader
|
||||||
|
import app.revanced.utils.Options
|
||||||
|
import app.revanced.utils.Options.setOptions
|
||||||
|
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 path: 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() = if (!path.exists() || overwrite) with(PatchBundleLoader.Jar(*patchBundles)) {
|
||||||
|
if (update) setOptions(path)
|
||||||
|
|
||||||
|
Options.serialize(this, prettyPrint = true).let(path::writeText)
|
||||||
|
}
|
||||||
|
else logger.severe("Options file already exists, use --override to override it")
|
||||||
|
}
|
342
src/main/kotlin/app/revanced/cli/command/PatchCommand.kt
Normal file
342
src/main/kotlin/app/revanced/cli/command/PatchCommand.kt
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
package app.revanced.cli.command
|
||||||
|
|
||||||
|
import app.revanced.patcher.PatchBundleLoader
|
||||||
|
import app.revanced.patcher.Patcher
|
||||||
|
import app.revanced.patcher.PatcherOptions
|
||||||
|
import app.revanced.patcher.PatcherResult
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.include
|
||||||
|
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||||
|
import app.revanced.utils.Options
|
||||||
|
import app.revanced.utils.Options.setOptions
|
||||||
|
import app.revanced.utils.adb.AdbManager
|
||||||
|
import app.revanced.utils.align.ZipAligner
|
||||||
|
import app.revanced.utils.align.zip.ZipFile
|
||||||
|
import app.revanced.utils.align.zip.structures.ZipEntry
|
||||||
|
import app.revanced.utils.signing.ApkSigner
|
||||||
|
import app.revanced.utils.signing.SigningOptions
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import picocli.CommandLine
|
||||||
|
import picocli.CommandLine.Help.Visibility.ALWAYS
|
||||||
|
import java.io.File
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.io.StringWriter
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
|
||||||
|
@CommandLine.Command(
|
||||||
|
name = "patch", description = ["Patch the supplied APK file with the supplied patches and integrations"]
|
||||||
|
)
|
||||||
|
internal object PatchCommand : Runnable {
|
||||||
|
private val logger = Logger.getLogger(PatchCommand::class.java.name)
|
||||||
|
|
||||||
|
@CommandLine.Parameters(
|
||||||
|
description = ["APK file to be patched"], arity = "1..1"
|
||||||
|
)
|
||||||
|
private lateinit var apk: File
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true
|
||||||
|
)
|
||||||
|
private var patchBundles = emptyList<File>()
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"]
|
||||||
|
)
|
||||||
|
private var integrations = listOf<File>()
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-i", "--include"], description = ["List of patches to include"]
|
||||||
|
)
|
||||||
|
private var includedPatches = arrayOf<String>()
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-e", "--exclude"], description = ["List of patches to exclude"]
|
||||||
|
)
|
||||||
|
private var excludedPatches = arrayOf<String>()
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["--options"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS
|
||||||
|
)
|
||||||
|
private var optionsFile: File = File("options.json")
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["--exclusive"],
|
||||||
|
description = ["Only include patches that are explicitly specified to be included"],
|
||||||
|
showDefaultValue = ALWAYS
|
||||||
|
)
|
||||||
|
private var exclusive = false
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-f","--force"],
|
||||||
|
description = ["Force inclusion of patches that are incompatible with the supplied APK file's version"],
|
||||||
|
showDefaultValue = ALWAYS
|
||||||
|
)
|
||||||
|
private var force: Boolean = false
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-o", "--out"], description = ["Path to save the patched APK file to"], required = true
|
||||||
|
)
|
||||||
|
private lateinit var outputFilePath: File
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-d", "--device-serial"], description = ["ADB device serial to install to"], showDefaultValue = ALWAYS
|
||||||
|
)
|
||||||
|
private var deviceSerial: String? = null
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["--mount"], description = ["Install by mounting the patched APK file"], showDefaultValue = ALWAYS
|
||||||
|
)
|
||||||
|
private var mount: Boolean = false
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["--common-name"],
|
||||||
|
description = ["The common name of the signer of the patched APK file"],
|
||||||
|
showDefaultValue = ALWAYS
|
||||||
|
|
||||||
|
)
|
||||||
|
private var commonName = "ReVanced"
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"]
|
||||||
|
)
|
||||||
|
private var keystorePath: String? = null
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["--password"], description = ["The password of the keystore to sign the patched APK file with"]
|
||||||
|
)
|
||||||
|
private var password = "ReVanced"
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-r", "--resource-cache"],
|
||||||
|
description = ["Path to temporary resource cache directory"],
|
||||||
|
showDefaultValue = ALWAYS
|
||||||
|
)
|
||||||
|
private var resourceCachePath = File("revanced-resource-cache")
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"]
|
||||||
|
)
|
||||||
|
private var aaptBinaryPath = File("")
|
||||||
|
|
||||||
|
@CommandLine.Option(
|
||||||
|
names = ["-p", "--purge"],
|
||||||
|
description = ["Purge the temporary resource cache directory after patching"],
|
||||||
|
showDefaultValue = ALWAYS
|
||||||
|
)
|
||||||
|
private var purge: Boolean = false
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
// region Prepare
|
||||||
|
|
||||||
|
if (!apk.exists()) {
|
||||||
|
logger.severe("APK file ${apk.name} does not exist")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
integrations.filter { !it.exists() }.let {
|
||||||
|
if (it.isEmpty()) return@let
|
||||||
|
|
||||||
|
it.forEach { integration ->
|
||||||
|
logger.severe("Integration file ${integration.name} does not exist")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val adbManager = deviceSerial?.let { serial ->
|
||||||
|
if (mount) AdbManager.RootAdbManager(serial)
|
||||||
|
else AdbManager.UserAdbManager(serial)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Load patches
|
||||||
|
|
||||||
|
logger.info("Loading patches")
|
||||||
|
|
||||||
|
val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray())
|
||||||
|
val integrations = integrations
|
||||||
|
|
||||||
|
logger.info("Setting patch options")
|
||||||
|
|
||||||
|
optionsFile.let {
|
||||||
|
if (it.exists()) patches.setOptions(it)
|
||||||
|
else Options.serialize(patches, prettyPrint = true).let(it::writeText)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Patch
|
||||||
|
|
||||||
|
val patcher = Patcher(
|
||||||
|
PatcherOptions(
|
||||||
|
apk,
|
||||||
|
resourceCachePath,
|
||||||
|
aaptBinaryPath.path,
|
||||||
|
resourceCachePath.absolutePath,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = patcher.apply {
|
||||||
|
acceptIntegrations(integrations)
|
||||||
|
acceptPatches(filterPatchSelection(patches))
|
||||||
|
|
||||||
|
// Execute patches.
|
||||||
|
runBlocking {
|
||||||
|
apply(false).collect { patchResult ->
|
||||||
|
patchResult.exception?.let {
|
||||||
|
StringWriter().use { writer ->
|
||||||
|
it.printStackTrace(PrintWriter(writer))
|
||||||
|
logger.severe("${patchResult.patchName} failed: $writer")
|
||||||
|
}
|
||||||
|
} ?: logger.info("${patchResult.patchName} succeeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.get()
|
||||||
|
|
||||||
|
patcher.close()
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Finish
|
||||||
|
|
||||||
|
val alignAndSignedFile = sign(
|
||||||
|
apk.newAlignedFile(
|
||||||
|
result, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Copying to ${outputFilePath.name}")
|
||||||
|
alignAndSignedFile.copyTo(outputFilePath, overwrite = true)
|
||||||
|
|
||||||
|
adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName))
|
||||||
|
|
||||||
|
if (purge) {
|
||||||
|
logger.info("Purging temporary files")
|
||||||
|
purge(resourceCachePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the patches to be added to the patcher. The filter is based on the following:
|
||||||
|
* - [includedPatches] (explicitly included)
|
||||||
|
* - [excludedPatches] (explicitly excluded)
|
||||||
|
* - [exclusive] (only include patches that are explicitly included)
|
||||||
|
* - [force] (ignore patches incompatibility to versions)
|
||||||
|
* - Package name and version of the input APK file (if [force] is false)
|
||||||
|
*
|
||||||
|
* @param patches The patches to filter.
|
||||||
|
* @return The filtered patches.
|
||||||
|
*/
|
||||||
|
private fun Patcher.filterPatchSelection(patches: PatchList) = buildList {
|
||||||
|
val packageName = context.packageMetadata.packageName
|
||||||
|
val packageVersion = context.packageMetadata.packageVersion
|
||||||
|
|
||||||
|
patches.forEach patch@{ patch ->
|
||||||
|
val formattedPatchName = patch.patchName.lowercase().replace(" ", "-")
|
||||||
|
|
||||||
|
val explicitlyExcluded = excludedPatches.contains(formattedPatchName)
|
||||||
|
if (explicitlyExcluded) return@patch logger.info("Excluding ${patch.patchName}")
|
||||||
|
|
||||||
|
// If the patch is explicitly included, it will be included if [exclusive] is false.
|
||||||
|
val explicitlyIncluded = exclusive && includedPatches.contains(formattedPatchName)
|
||||||
|
|
||||||
|
// If the patch is implicitly included, it will be only included if [exclusive] is false.
|
||||||
|
val implicitlyIncluded = !exclusive && patch.include
|
||||||
|
|
||||||
|
val included = implicitlyIncluded || explicitlyIncluded
|
||||||
|
if (!included) return@patch logger.info("${patch.patchName} excluded by default") // Case 1.
|
||||||
|
|
||||||
|
// At last 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.isEmpty() || it.any { version -> version == packageVersion }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchesVersion) return@patch logger.warning("${patch.patchName} is incompatible with version $packageVersion. " + "This patch is only compatible with version " + packages.joinToString(
|
||||||
|
";"
|
||||||
|
) { pkg ->
|
||||||
|
"${pkg.name}: ${pkg.versions.joinToString(", ")}"
|
||||||
|
})
|
||||||
|
|
||||||
|
} ?: return@patch logger.fine("${patch.patchName} is incompatible with $packageName. "
|
||||||
|
+ "This patch is only compatible with "
|
||||||
|
+ packages.joinToString(", ") { `package` -> `package`.name })
|
||||||
|
|
||||||
|
return@let
|
||||||
|
} ?: logger.fine("$formattedPatchName: No constraint on packages.")
|
||||||
|
|
||||||
|
logger.fine("Adding $formattedPatchName")
|
||||||
|
|
||||||
|
add(patch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new aligned APK file.
|
||||||
|
*
|
||||||
|
* @param result The result of the patching process.
|
||||||
|
* @param outputFile The file to save the aligned APK to.
|
||||||
|
*/
|
||||||
|
private fun File.newAlignedFile(
|
||||||
|
result: PatcherResult, outputFile: File
|
||||||
|
): File {
|
||||||
|
logger.info("Aligning $name")
|
||||||
|
|
||||||
|
if (outputFile.exists()) outputFile.delete()
|
||||||
|
|
||||||
|
ZipFile(outputFile).use { file ->
|
||||||
|
result.dexFiles.forEach {
|
||||||
|
file.addEntryCompressData(
|
||||||
|
ZipEntry.createWithName(it.name), it.stream.readBytes()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.resourceFile?.let {
|
||||||
|
file.copyEntriesFromFileAligned(
|
||||||
|
ZipFile(it), ZipAligner::getEntryAlignment
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Do not compress result.doNotCompress
|
||||||
|
|
||||||
|
file.copyEntriesFromFileAligned(
|
||||||
|
ZipFile(this), ZipAligner::getEntryAlignment
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputFile
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign the APK file.
|
||||||
|
*
|
||||||
|
* @param inputFile The APK file to sign.
|
||||||
|
* @return The signed APK file. If [mount] is true, the input file will be returned.
|
||||||
|
*/
|
||||||
|
private fun sign(inputFile: File) = if (mount) inputFile
|
||||||
|
else {
|
||||||
|
logger.info("Signing ${inputFile.name}")
|
||||||
|
|
||||||
|
val keyStoreFilePath = keystorePath
|
||||||
|
?: outputFilePath.absoluteFile.parentFile.resolve("${outputFilePath.nameWithoutExtension}.keystore").canonicalPath
|
||||||
|
|
||||||
|
val options = SigningOptions(
|
||||||
|
commonName, password, keyStoreFilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
ApkSigner(options).signApk(
|
||||||
|
inputFile, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_signed.apk")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun purge(resourceCachePath: File) {
|
||||||
|
val result = if (resourceCachePath.deleteRecursively()) "Purged resource cache directory"
|
||||||
|
else "Failed to purge resource cache directory"
|
||||||
|
logger.info(result)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package app.revanced.cli.command.utility
|
||||||
|
|
||||||
|
import app.revanced.utils.adb.AdbManager
|
||||||
|
import picocli.CommandLine.*
|
||||||
|
import java.io.File
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "install", description = ["Install an APK file to devices with the supplied ADB device serials"]
|
||||||
|
)
|
||||||
|
internal object InstallCommand : Runnable {
|
||||||
|
private val logger = Logger.getLogger(InstallCommand::class.java.name)
|
||||||
|
|
||||||
|
@Parameters(
|
||||||
|
description = ["ADB device serials"], arity = "1..*"
|
||||||
|
)
|
||||||
|
private lateinit var deviceSerials: Array<String>
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-a", "--apk"], 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"],
|
||||||
|
)
|
||||||
|
private var packageName: String? = null
|
||||||
|
|
||||||
|
override fun run() = try {
|
||||||
|
deviceSerials.forEach { deviceSerial ->
|
||||||
|
if (packageName != null) {
|
||||||
|
AdbManager.RootAdbManager(deviceSerial)
|
||||||
|
} else {
|
||||||
|
AdbManager.UserAdbManager(deviceSerial)
|
||||||
|
}.install(AdbManager.Apk(apk, packageName))
|
||||||
|
}
|
||||||
|
} catch (e: AdbManager.DeviceNotFoundException) {
|
||||||
|
logger.severe(e.toString())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package app.revanced.cli.command.utility
|
||||||
|
|
||||||
|
import app.revanced.utils.adb.AdbManager
|
||||||
|
import picocli.CommandLine.*
|
||||||
|
import picocli.CommandLine.Help.Visibility.ALWAYS
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
|
||||||
|
@Command(
|
||||||
|
name = "uninstall",
|
||||||
|
description = ["Uninstall a patched app from the devices with the supplied ADB device serials"]
|
||||||
|
)
|
||||||
|
internal object UninstallCommand : Runnable {
|
||||||
|
private val logger = Logger.getLogger(UninstallCommand::class.java.name)
|
||||||
|
|
||||||
|
@Parameters(description = ["ADB device serials"], arity = "1..*")
|
||||||
|
private lateinit var deviceSerials: Array<String>
|
||||||
|
|
||||||
|
@Option(names = ["-p", "--package-name"], description = ["Package name of the app to uninstall"], required = true)
|
||||||
|
private lateinit var packageName: String
|
||||||
|
|
||||||
|
@Option(
|
||||||
|
names = ["-u", "--unmount"],
|
||||||
|
description = ["Uninstall by unmounting the patched APK file"],
|
||||||
|
showDefaultValue = ALWAYS
|
||||||
|
)
|
||||||
|
private var unmount: Boolean = false
|
||||||
|
|
||||||
|
override fun run() = try {
|
||||||
|
deviceSerials.forEach { deviceSerial ->
|
||||||
|
if (unmount) {
|
||||||
|
AdbManager.RootAdbManager(deviceSerial)
|
||||||
|
} else {
|
||||||
|
AdbManager.UserAdbManager(deviceSerial)
|
||||||
|
}.uninstall(packageName)
|
||||||
|
}
|
||||||
|
} catch (e: AdbManager.DeviceNotFoundException) {
|
||||||
|
logger.severe(e.toString())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
package app.revanced.cli.command.utility
|
||||||
|
|
||||||
|
import picocli.CommandLine
|
||||||
|
|
||||||
|
@CommandLine.Command(
|
||||||
|
name = "utility",
|
||||||
|
description = ["Commands for utility purposes"],
|
||||||
|
subcommands = [InstallCommand::class, UninstallCommand::class],
|
||||||
|
)
|
||||||
|
internal object UtilityCommand
|
@ -1,8 +0,0 @@
|
|||||||
package app.revanced.cli.logging
|
|
||||||
|
|
||||||
internal interface CliLogger {
|
|
||||||
fun error(msg: String)
|
|
||||||
fun info(msg: String)
|
|
||||||
fun trace(msg: String)
|
|
||||||
fun warn(msg: String)
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
package app.revanced.cli.logging.impl
|
|
||||||
|
|
||||||
import app.revanced.cli.command.MainCommand
|
|
||||||
import app.revanced.cli.logging.CliLogger
|
|
||||||
import java.util.logging.Logger
|
|
||||||
import java.util.logging.SimpleFormatter
|
|
||||||
|
|
||||||
internal class DefaultCliLogger(
|
|
||||||
private val logger: Logger = Logger.getLogger(MainCommand::class.java.name),
|
|
||||||
private val errorLogger: Logger = Logger.getLogger(logger.name + "Err")
|
|
||||||
) : CliLogger {
|
|
||||||
|
|
||||||
init {
|
|
||||||
logger.useParentHandlers = false
|
|
||||||
if (logger.handlers.isEmpty()) {
|
|
||||||
logger.addHandler(FlushingStreamHandler(System.out, SimpleFormatter()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
init {
|
|
||||||
System.setProperty("java.util.logging.SimpleFormatter.format", "%4\$s: %5\$s %n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun error(msg: String) = errorLogger.severe(msg)
|
|
||||||
override fun info(msg: String) = logger.info(msg)
|
|
||||||
override fun trace(msg: String) = logger.finest(msg)
|
|
||||||
override fun warn(msg: String) = errorLogger.warning(msg)
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package app.revanced.cli.logging.impl
|
|
||||||
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.util.logging.Formatter
|
|
||||||
import java.util.logging.LogRecord
|
|
||||||
import java.util.logging.StreamHandler
|
|
||||||
|
|
||||||
internal class FlushingStreamHandler(out: OutputStream, format: Formatter) : StreamHandler(out, format) {
|
|
||||||
override fun publish(record: LogRecord) {
|
|
||||||
super.publish(record)
|
|
||||||
flush()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package app.revanced.cli.main
|
|
||||||
|
|
||||||
import app.revanced.cli.command.MainCommand
|
|
||||||
import picocli.CommandLine
|
|
||||||
|
|
||||||
internal fun main(args: Array<String>) {
|
|
||||||
CommandLine(MainCommand).execute(*args)
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package app.revanced.cli.patcher
|
|
||||||
|
|
||||||
import app.revanced.cli.command.PatchList
|
|
||||||
import app.revanced.patcher.PatcherResult
|
|
||||||
import app.revanced.utils.patcher.addPatchesFiltered
|
|
||||||
import app.revanced.utils.patcher.applyPatchesVerbose
|
|
||||||
import app.revanced.utils.patcher.mergeFiles
|
|
||||||
|
|
||||||
internal object Patcher {
|
|
||||||
internal fun start(
|
|
||||||
patcher: app.revanced.patcher.Patcher,
|
|
||||||
allPatches: PatchList
|
|
||||||
): PatcherResult {
|
|
||||||
// merge files like necessary integrations
|
|
||||||
patcher.mergeFiles()
|
|
||||||
// add patches, but filter incompatible or excluded patches
|
|
||||||
patcher.addPatchesFiltered(allPatches)
|
|
||||||
// apply patches
|
|
||||||
patcher.applyPatchesVerbose()
|
|
||||||
|
|
||||||
return patcher.save()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package app.revanced.cli.patcher.logging.impl
|
|
||||||
|
|
||||||
import app.revanced.cli.logging.impl.DefaultCliLogger
|
|
||||||
import java.util.logging.Logger
|
|
||||||
|
|
||||||
internal object PatcherLogger : app.revanced.patcher.logging.Logger{
|
|
||||||
private val logger = DefaultCliLogger(Logger.getLogger(app.revanced.patcher.Patcher::class.java.name))
|
|
||||||
|
|
||||||
override fun error(msg: String) = logger.error(msg)
|
|
||||||
override fun info(msg: String) = logger.info(msg)
|
|
||||||
override fun warn(msg: String)= logger.warn(msg)
|
|
||||||
override fun trace(msg: String)= logger.trace(msg)
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
package app.revanced.cli.signing
|
|
||||||
|
|
||||||
import app.revanced.cli.command.MainCommand.logger
|
|
||||||
import app.revanced.utils.signing.Signer
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
object Signing {
|
|
||||||
fun sign(alignedFile: File, signedOutput: File, signingOptions: SigningOptions) {
|
|
||||||
logger.info("Signing ${alignedFile.name} to ${signedOutput.name}")
|
|
||||||
Signer(signingOptions).signApk(alignedFile, signedOutput)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +1,18 @@
|
|||||||
package app.revanced.utils
|
package app.revanced.utils
|
||||||
|
|
||||||
import app.revanced.cli.command.PatchList
|
import app.revanced.cli.command.PatchList
|
||||||
import app.revanced.cli.logging.CliLogger
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.options
|
import app.revanced.patcher.extensions.PatchExtensions.options
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
||||||
import app.revanced.patcher.patch.NoSuchOptionException
|
import app.revanced.patcher.patch.NoSuchOptionException
|
||||||
import app.revanced.utils.Options.PatchOption.Option
|
import app.revanced.utils.Options.PatchOption.Option
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
|
||||||
internal object Options {
|
internal object Options {
|
||||||
|
private val logger = Logger.getLogger(Options::class.java.name)
|
||||||
|
|
||||||
private var mapper = jacksonObjectMapper()
|
private var mapper = jacksonObjectMapper()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,22 +55,24 @@ internal object Options {
|
|||||||
* Sets the options for the patches in the list.
|
* Sets the options for the patches in the list.
|
||||||
*
|
*
|
||||||
* @param json The JSON string containing the options.
|
* @param json The JSON string containing the options.
|
||||||
* @param logger The logger to use for logging.
|
|
||||||
*/
|
*/
|
||||||
fun PatchList.setOptions(json: String, logger: CliLogger? = null) {
|
fun PatchList.setOptions(json: String) {
|
||||||
filter { it.options?.any() == true }.let { patches ->
|
filter { it.options?.any() == true }.let { patches ->
|
||||||
if (patches.isEmpty()) return
|
if (patches.isEmpty()) return
|
||||||
|
|
||||||
val patchOptions = deserialize(json)
|
val patchOptions = deserialize(json)
|
||||||
|
|
||||||
patches.forEach { patch ->
|
patches.forEach patch@{ patch ->
|
||||||
patchOptions.find { option -> option.patchName == patch.patchName }?.let {
|
patchOptions.find { option -> option.patchName == patch.patchName }?.let {
|
||||||
it.options.forEach { option ->
|
it.options.forEach { option ->
|
||||||
try {
|
try {
|
||||||
patch.options?.set(option.key, option.value)
|
patch.options?.set(option.key, option.value)
|
||||||
?: logger?.warn("${patch.patchName} has no options")
|
?: run{
|
||||||
|
logger.warning("${patch.patchName} has no options")
|
||||||
|
return@patch
|
||||||
|
}
|
||||||
} catch (e: NoSuchOptionException) {
|
} catch (e: NoSuchOptionException) {
|
||||||
logger?.error(e.message ?: "Unknown error")
|
logger.info(e.toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,10 +84,9 @@ internal object Options {
|
|||||||
* Sets the options for the patches in the list.
|
* Sets the options for the patches in the list.
|
||||||
*
|
*
|
||||||
* @param file The file containing the JSON string containing the options.
|
* @param file The file containing the JSON string containing the options.
|
||||||
* @param logger The logger to use for logging.
|
|
||||||
* @see setOptions
|
* @see setOptions
|
||||||
*/
|
*/
|
||||||
fun PatchList.setOptions(file: File, logger: CliLogger? = null) = setOptions(file.readText(), logger)
|
fun PatchList.setOptions(file: File) = setOptions(file.readText())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data class for a patch and its [Option]s.
|
* Data class for a patch and its [Option]s.
|
||||||
|
@ -1,113 +0,0 @@
|
|||||||
package app.revanced.utils.adb
|
|
||||||
|
|
||||||
import app.revanced.cli.command.MainCommand.logger
|
|
||||||
import se.vidstige.jadb.JadbConnection
|
|
||||||
import se.vidstige.jadb.JadbDevice
|
|
||||||
import se.vidstige.jadb.managers.PackageManager
|
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
|
|
||||||
internal class Adb(
|
|
||||||
private val file: File,
|
|
||||||
private val packageName: String,
|
|
||||||
deviceName: String,
|
|
||||||
private val modeInstall: Boolean = false,
|
|
||||||
private val logging: Boolean = true
|
|
||||||
) {
|
|
||||||
private val device: JadbDevice
|
|
||||||
|
|
||||||
init {
|
|
||||||
device = JadbConnection().devices.let { device -> device.find { it.serial == deviceName } ?: device.first() }
|
|
||||||
?: throw IllegalArgumentException("No such device with name $deviceName")
|
|
||||||
|
|
||||||
if (!modeInstall && device.run("su -h", false) != 0)
|
|
||||||
throw IllegalArgumentException("Root required on $deviceName. Task failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.replacePlaceholder(with: String? = null): String {
|
|
||||||
return this.replace(Constants.PLACEHOLDER, with ?: packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun deploy() {
|
|
||||||
if (modeInstall) {
|
|
||||||
logger.info("Installing without mounting")
|
|
||||||
|
|
||||||
PackageManager(device).install(file)
|
|
||||||
} else {
|
|
||||||
logger.info("Installing by mounting")
|
|
||||||
|
|
||||||
// push patched file
|
|
||||||
device.copy(Constants.PATH_INIT_PUSH, file)
|
|
||||||
|
|
||||||
// create revanced folder path
|
|
||||||
device.run("${Constants.COMMAND_CREATE_DIR} ${Constants.PATH_REVANCED}")
|
|
||||||
|
|
||||||
// prepare mounting the apk
|
|
||||||
device.run(Constants.COMMAND_PREPARE_MOUNT_APK.replacePlaceholder())
|
|
||||||
|
|
||||||
// push mount script
|
|
||||||
device.createFile(
|
|
||||||
Constants.PATH_INIT_PUSH,
|
|
||||||
Constants.CONTENT_MOUNT_SCRIPT.replacePlaceholder()
|
|
||||||
)
|
|
||||||
// install mount script
|
|
||||||
device.run(Constants.COMMAND_INSTALL_MOUNT.replacePlaceholder())
|
|
||||||
|
|
||||||
// unmount the apk for sanity
|
|
||||||
device.run(Constants.COMMAND_UMOUNT.replacePlaceholder())
|
|
||||||
// mount the apk
|
|
||||||
device.run(Constants.PATH_MOUNT.replacePlaceholder())
|
|
||||||
|
|
||||||
// relaunch app
|
|
||||||
device.run(Constants.COMMAND_RESTART.replacePlaceholder())
|
|
||||||
|
|
||||||
// log the app
|
|
||||||
log()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun uninstall() {
|
|
||||||
logger.info("Uninstalling by unmounting")
|
|
||||||
|
|
||||||
// unmount the apk
|
|
||||||
device.run(Constants.COMMAND_UMOUNT.replacePlaceholder())
|
|
||||||
|
|
||||||
// delete revanced app
|
|
||||||
device.run(Constants.COMMAND_DELETE.replacePlaceholder(Constants.PATH_REVANCED_APP).replacePlaceholder())
|
|
||||||
|
|
||||||
// delete mount script
|
|
||||||
device.run(Constants.COMMAND_DELETE.replacePlaceholder(Constants.PATH_MOUNT).replacePlaceholder())
|
|
||||||
|
|
||||||
logger.info("Finished uninstalling")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun log() {
|
|
||||||
val executor = Executors.newSingleThreadExecutor()
|
|
||||||
val pipe = if (logging) {
|
|
||||||
ProcessBuilder.Redirect.INHERIT
|
|
||||||
} else {
|
|
||||||
ProcessBuilder.Redirect.PIPE
|
|
||||||
}
|
|
||||||
|
|
||||||
val process = device.buildCommand(Constants.COMMAND_LOGCAT.replacePlaceholder())
|
|
||||||
.redirectOutput(pipe)
|
|
||||||
.redirectError(pipe)
|
|
||||||
.useExecutor(executor)
|
|
||||||
.start()
|
|
||||||
|
|
||||||
Thread.sleep(500) // give the app some time to start up.
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
while (device.run("${Constants.COMMAND_PID_OF} $packageName") == 0) {
|
|
||||||
Thread.sleep(1000)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
} catch (e: Exception) {
|
|
||||||
throw RuntimeException("An error occurred while monitoring the state of app", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.info("Stopped logging because the app was closed")
|
|
||||||
process.destroy()
|
|
||||||
executor.shutdown()
|
|
||||||
}
|
|
||||||
}
|
|
140
src/main/kotlin/app/revanced/utils/adb/AdbManager.kt
Normal file
140
src/main/kotlin/app/revanced/utils/adb/AdbManager.kt
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
package app.revanced.utils.adb
|
||||||
|
|
||||||
|
import app.revanced.utils.adb.AdbManager.Apk
|
||||||
|
import app.revanced.utils.adb.Constants.CREATE_DIR
|
||||||
|
import app.revanced.utils.adb.Constants.DELETE
|
||||||
|
import app.revanced.utils.adb.Constants.INSTALLATION_PATH
|
||||||
|
import app.revanced.utils.adb.Constants.INSTALL_MOUNT
|
||||||
|
import app.revanced.utils.adb.Constants.INSTALL_PATCHED_APK
|
||||||
|
import app.revanced.utils.adb.Constants.MOUNT_PATH
|
||||||
|
import app.revanced.utils.adb.Constants.MOUNT_SCRIPT
|
||||||
|
import app.revanced.utils.adb.Constants.PATCHED_APK_PATH
|
||||||
|
import app.revanced.utils.adb.Constants.PLACEHOLDER
|
||||||
|
import app.revanced.utils.adb.Constants.RESTART
|
||||||
|
import app.revanced.utils.adb.Constants.TMP_PATH
|
||||||
|
import app.revanced.utils.adb.Constants.UMOUNT
|
||||||
|
import se.vidstige.jadb.JadbConnection
|
||||||
|
import se.vidstige.jadb.managers.Package
|
||||||
|
import se.vidstige.jadb.managers.PackageManager
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.File
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adb manager. Used to install and uninstall [Apk] files.
|
||||||
|
*
|
||||||
|
* @param deviceSerial The serial of the device.
|
||||||
|
*/
|
||||||
|
internal sealed class AdbManager(deviceSerial: String? = null) : Closeable {
|
||||||
|
protected val logger: Logger = Logger.getLogger(AdbManager::class.java.name)
|
||||||
|
|
||||||
|
protected val device = JadbConnection().devices.find { device -> device.serial == deviceSerial }
|
||||||
|
?: throw DeviceNotFoundException(deviceSerial)
|
||||||
|
|
||||||
|
init {
|
||||||
|
logger.fine("Established connection to $deviceSerial")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs the [Apk] file.
|
||||||
|
*
|
||||||
|
* @param apk The [Apk] file.
|
||||||
|
*/
|
||||||
|
open fun install(apk: Apk) {
|
||||||
|
logger.info("Finished installing ${apk.file.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uninstalls the package.
|
||||||
|
*
|
||||||
|
* @param packageName The package name.
|
||||||
|
*/
|
||||||
|
open fun uninstall(packageName: String) {
|
||||||
|
logger.info("Finished uninstalling $packageName")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the [AdbManager] instance.
|
||||||
|
*/
|
||||||
|
override fun close() {
|
||||||
|
logger.fine("Closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
class RootAdbManager(deviceSerial: String) : AdbManager(deviceSerial) {
|
||||||
|
init {
|
||||||
|
if (!device.hasSu()) throw IllegalArgumentException("Root required on $deviceSerial. Task failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun install(apk: Apk) {
|
||||||
|
logger.info("Installing by mounting")
|
||||||
|
|
||||||
|
val applyReplacement = getPlaceholderReplacement(
|
||||||
|
apk.packageName ?: throw IllegalArgumentException("Package name is required")
|
||||||
|
)
|
||||||
|
|
||||||
|
device.push(apk.file, TMP_PATH)
|
||||||
|
|
||||||
|
device.run("$CREATE_DIR $INSTALLATION_PATH")
|
||||||
|
device.run(INSTALL_PATCHED_APK.applyReplacement())
|
||||||
|
|
||||||
|
device.createFile(TMP_PATH, MOUNT_SCRIPT.applyReplacement())
|
||||||
|
|
||||||
|
device.run(INSTALL_MOUNT.applyReplacement())
|
||||||
|
device.run(UMOUNT.applyReplacement()) // Sanity check.
|
||||||
|
device.run(MOUNT_PATH.applyReplacement())
|
||||||
|
device.run(RESTART.applyReplacement())
|
||||||
|
device.run(DELETE.applyReplacement(TMP_PATH).applyReplacement())
|
||||||
|
|
||||||
|
super.install(apk)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun uninstall(packageName: String) {
|
||||||
|
logger.info("Uninstalling $packageName by unmounting")
|
||||||
|
|
||||||
|
val applyReplacement = getPlaceholderReplacement(packageName)
|
||||||
|
|
||||||
|
device.run(UMOUNT.applyReplacement(packageName))
|
||||||
|
device.run(DELETE.applyReplacement(PATCHED_APK_PATH).applyReplacement())
|
||||||
|
device.run(DELETE.applyReplacement(MOUNT_PATH).applyReplacement())
|
||||||
|
device.run(DELETE.applyReplacement(TMP_PATH).applyReplacement())
|
||||||
|
|
||||||
|
super.uninstall(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object Utils {
|
||||||
|
private fun getPlaceholderReplacement(with: String): String.() -> String = { replace(PLACEHOLDER, with) }
|
||||||
|
private fun String.applyReplacement(with: String) = replace(PLACEHOLDER, with)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserAdbManager(deviceSerial: String) : AdbManager(deviceSerial) {
|
||||||
|
private val packageManager = PackageManager(device)
|
||||||
|
|
||||||
|
override fun install(apk: Apk) {
|
||||||
|
PackageManager(device).install(apk.file)
|
||||||
|
|
||||||
|
super.install(apk)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun uninstall(packageName: String) {
|
||||||
|
logger.info("Uninstalling $packageName")
|
||||||
|
|
||||||
|
packageManager.uninstall(Package(packageName))
|
||||||
|
|
||||||
|
super.uninstall(packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apk file for [AdbManager].
|
||||||
|
*
|
||||||
|
* @param file The [Apk] file.
|
||||||
|
* @param packageName The package name of the [Apk] file.
|
||||||
|
*/
|
||||||
|
internal class Apk(val file: File, val packageName: String? = null)
|
||||||
|
|
||||||
|
internal class DeviceNotFoundException(deviceSerial: String?) :
|
||||||
|
Exception(deviceSerial?.let {
|
||||||
|
"The device with the ADB device serial \"$deviceSerial\" can not be found"
|
||||||
|
} ?: "No ADB device found")
|
||||||
|
}
|
@ -5,10 +5,9 @@ import se.vidstige.jadb.RemoteFile
|
|||||||
import se.vidstige.jadb.ShellProcessBuilder
|
import se.vidstige.jadb.ShellProcessBuilder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder {
|
internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder {
|
||||||
if (su) {
|
if (su) return shellProcessBuilder("su -c \'$command\'")
|
||||||
return shellProcessBuilder("su -c \'$command\'")
|
|
||||||
}
|
|
||||||
|
|
||||||
val args = command.split(" ") as ArrayList<String>
|
val args = command.split(" ") as ArrayList<String>
|
||||||
val cmd = args.removeFirst()
|
val cmd = args.removeFirst()
|
||||||
@ -20,10 +19,15 @@ internal fun JadbDevice.run(command: String, su: Boolean = true): Int {
|
|||||||
return this.buildCommand(command, su).start().waitFor()
|
return this.buildCommand(command, su).start().waitFor()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun JadbDevice.copy(targetPath: String, file: File) {
|
internal fun JadbDevice.hasSu() =
|
||||||
push(file, RemoteFile(targetPath))
|
this.startCommand("su -h", false).waitFor() == 0
|
||||||
}
|
|
||||||
|
|
||||||
internal fun JadbDevice.createFile(targetFile: String, content: String) {
|
internal fun JadbDevice.push(file: File, targetFilePath: String) =
|
||||||
|
push(file, RemoteFile(targetFilePath))
|
||||||
|
|
||||||
|
internal fun JadbDevice.createFile(targetFile: String, content: String) =
|
||||||
push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile))
|
push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile))
|
||||||
}
|
|
||||||
|
|
||||||
|
private fun JadbDevice.startCommand(command: String, su: Boolean) =
|
||||||
|
shellProcessBuilder(if (su) "su -c '$command'" else command).start()
|
@ -1,54 +1,37 @@
|
|||||||
package app.revanced.utils.adb
|
package app.revanced.utils.adb
|
||||||
|
|
||||||
internal object Constants {
|
internal object Constants {
|
||||||
// template placeholder to replace a string in commands
|
internal const val PLACEHOLDER = "PLACEHOLDER"
|
||||||
internal const val PLACEHOLDER = "TEMPLATE_PACKAGE_NAME"
|
|
||||||
|
|
||||||
// utility commands
|
internal const val TMP_PATH = "/data/local/tmp/revanced.tmp"
|
||||||
private const val COMMAND_CHMOD_MOUNT = "chmod +x"
|
internal const val INSTALLATION_PATH = "/data/adb/revanced/"
|
||||||
internal const val COMMAND_PID_OF = "pidof -s"
|
internal const val PATCHED_APK_PATH = "$INSTALLATION_PATH$PLACEHOLDER.apk"
|
||||||
internal const val COMMAND_CREATE_DIR = "mkdir -p"
|
internal const val MOUNT_PATH = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh"
|
||||||
internal const val COMMAND_LOGCAT = "logcat -c && logcat | grep AndroidRuntime"
|
|
||||||
internal const val COMMAND_RESTART = "pm resolve-activity --brief $PLACEHOLDER | tail -n 1 | xargs am start -n && kill ${'$'}($COMMAND_PID_OF $PLACEHOLDER)"
|
|
||||||
|
|
||||||
// default mount file name
|
internal const val DELETE = "rm -rf $PLACEHOLDER"
|
||||||
private const val NAME_MOUNT_SCRIPT = "mount_revanced_$PLACEHOLDER.sh"
|
internal const val CREATE_DIR = "mkdir -p"
|
||||||
|
internal const val RESTART = "pm resolve-activity --brief $PLACEHOLDER | tail -n 1 | " +
|
||||||
|
"xargs am start -n && kill ${'$'}(pidof -s $PLACEHOLDER)"
|
||||||
|
|
||||||
// initial directory to push files to via adb push
|
internal const val INSTALL_PATCHED_APK = "base_path=\"$PATCHED_APK_PATH\" && " +
|
||||||
internal const val PATH_INIT_PUSH = "/data/local/tmp/revanced.delete"
|
"mv $TMP_PATH ${'$'}base_path && " +
|
||||||
|
"chmod 644 ${'$'}base_path && " +
|
||||||
|
"chown system:system ${'$'}base_path && " +
|
||||||
|
"chcon u:object_r:apk_data_file:s0 ${'$'}base_path"
|
||||||
|
|
||||||
// revanced path
|
internal const val UMOUNT =
|
||||||
internal const val PATH_REVANCED = "/data/adb/revanced/"
|
|
||||||
|
|
||||||
// revanced apk path
|
|
||||||
internal const val PATH_REVANCED_APP = "$PATH_REVANCED$PLACEHOLDER.apk"
|
|
||||||
|
|
||||||
// delete command
|
|
||||||
internal const val COMMAND_DELETE = "rm -rf $PLACEHOLDER"
|
|
||||||
|
|
||||||
// mount script path
|
|
||||||
internal const val PATH_MOUNT = "/data/adb/service.d/$NAME_MOUNT_SCRIPT"
|
|
||||||
|
|
||||||
// move to revanced apk path & set permissions
|
|
||||||
internal const val COMMAND_PREPARE_MOUNT_APK =
|
|
||||||
"base_path=\"$PATH_REVANCED_APP\" && mv $PATH_INIT_PUSH ${'$'}base_path && chmod 644 ${'$'}base_path && chown system:system ${'$'}base_path && chcon u:object_r:apk_data_file:s0 ${'$'}base_path"
|
|
||||||
|
|
||||||
// unmount command
|
|
||||||
internal const val COMMAND_UMOUNT =
|
|
||||||
"grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d \" \" -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done"
|
"grep $PLACEHOLDER /proc/mounts | while read -r line; do echo ${'$'}line | cut -d \" \" -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done"
|
||||||
|
|
||||||
// install mount script & set permissions
|
internal const val INSTALL_MOUNT = "mv $TMP_PATH $MOUNT_PATH && chmod +x $MOUNT_PATH"
|
||||||
internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && $COMMAND_CHMOD_MOUNT $PATH_MOUNT"
|
|
||||||
|
|
||||||
// mount script
|
internal val MOUNT_SCRIPT =
|
||||||
internal val CONTENT_MOUNT_SCRIPT =
|
|
||||||
"""
|
"""
|
||||||
#!/system/bin/sh
|
#!/system/bin/sh
|
||||||
MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin
|
MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin
|
||||||
MIRROR="${'$'}MAGISKTMP/.magisk/mirror"
|
MIRROR="${'$'}MAGISKTMP/.magisk/mirror"
|
||||||
while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done
|
while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done
|
||||||
|
|
||||||
base_path="$PATH_REVANCED_APP"
|
base_path="$PATCHED_APK_PATH"
|
||||||
stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
|
stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' )
|
||||||
|
|
||||||
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
|
chcon u:object_r:apk_data_file:s0 ${'$'}base_path
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package app.revanced.utils.signing.align
|
package app.revanced.utils.align
|
||||||
|
|
||||||
import app.revanced.utils.signing.align.zip.structures.ZipEntry
|
import app.revanced.utils.align.zip.structures.ZipEntry
|
||||||
|
|
||||||
internal object ZipAligner {
|
internal object ZipAligner {
|
||||||
private const val DEFAULT_ALIGNMENT = 4
|
private const val DEFAULT_ALIGNMENT = 4
|
@ -1,4 +1,4 @@
|
|||||||
package app.revanced.utils.signing.align.zip
|
package app.revanced.utils.align.zip
|
||||||
|
|
||||||
import java.io.DataInput
|
import java.io.DataInput
|
||||||
import java.io.DataOutput
|
import java.io.DataOutput
|
@ -1,7 +1,7 @@
|
|||||||
package app.revanced.utils.signing.align.zip
|
package app.revanced.utils.align.zip
|
||||||
|
|
||||||
import app.revanced.utils.signing.align.zip.structures.ZipEndRecord
|
import app.revanced.utils.align.zip.structures.ZipEndRecord
|
||||||
import app.revanced.utils.signing.align.zip.structures.ZipEntry
|
import app.revanced.utils.align.zip.structures.ZipEntry
|
||||||
import java.io.Closeable
|
import java.io.Closeable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.RandomAccessFile
|
import java.io.RandomAccessFile
|
||||||
@ -11,15 +11,15 @@ import java.util.zip.CRC32
|
|||||||
import java.util.zip.Deflater
|
import java.util.zip.Deflater
|
||||||
|
|
||||||
class ZipFile(file: File) : Closeable {
|
class ZipFile(file: File) : Closeable {
|
||||||
var entries: MutableList<ZipEntry> = mutableListOf()
|
private var entries: MutableList<ZipEntry> = mutableListOf()
|
||||||
|
|
||||||
private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw")
|
private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw")
|
||||||
private var CDNeedsRewrite = false
|
private var centralDirectoryNeedsRewrite = false
|
||||||
|
|
||||||
private val compressionLevel = 5
|
private val compressionLevel = 5
|
||||||
|
|
||||||
init {
|
init {
|
||||||
//if file isn't empty try to load entries
|
// If file isn't empty try to load entries.
|
||||||
if (file.length() > 0) {
|
if (file.length() > 0) {
|
||||||
val endRecord = findEndRecord()
|
val endRecord = findEndRecord()
|
||||||
|
|
||||||
@ -29,17 +29,17 @@ class ZipFile(file: File) : Closeable {
|
|||||||
entries = readEntries(endRecord).toMutableList()
|
entries = readEntries(endRecord).toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
//seek back to start for writing
|
// Seek back to start for writing.
|
||||||
filePointer.seek(0)
|
filePointer.seek(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findEndRecord(): ZipEndRecord {
|
private fun findEndRecord(): ZipEndRecord {
|
||||||
//look from end to start since end record is at the end
|
// Look from end to start since end record is at the end.
|
||||||
for (i in filePointer.length() - 1 downTo 0) {
|
for (i in filePointer.length() - 1 downTo 0) {
|
||||||
filePointer.seek(i)
|
filePointer.seek(i)
|
||||||
//possible beginning of signature
|
// Possible beginning of signature.
|
||||||
if (filePointer.readByte() == 0x50.toByte()) {
|
if (filePointer.readByte() == 0x50.toByte()) {
|
||||||
//seek back to get the full int
|
// Seek back to get the full int.
|
||||||
filePointer.seek(i)
|
filePointer.seek(i)
|
||||||
val possibleSignature = filePointer.readUIntLE()
|
val possibleSignature = filePointer.readUIntLE()
|
||||||
if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) {
|
if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) {
|
||||||
@ -76,7 +76,7 @@ class ZipFile(file: File) : Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun writeCD() {
|
private fun writeCD() {
|
||||||
val CDStart = filePointer.channel.position().toUInt()
|
val centralDirectoryStartOffset = filePointer.channel.position().toUInt()
|
||||||
|
|
||||||
entries.forEach {
|
entries.forEach {
|
||||||
filePointer.channel.write(it.toCDE())
|
filePointer.channel.write(it.toCDE())
|
||||||
@ -89,8 +89,8 @@ class ZipFile(file: File) : Closeable {
|
|||||||
0u,
|
0u,
|
||||||
entriesCount,
|
entriesCount,
|
||||||
entriesCount,
|
entriesCount,
|
||||||
filePointer.channel.position().toUInt() - CDStart,
|
filePointer.channel.position().toUInt() - centralDirectoryStartOffset,
|
||||||
CDStart,
|
centralDirectoryStartOffset,
|
||||||
""
|
""
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ class ZipFile(file: File) : Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addEntry(entry: ZipEntry, data: ByteBuffer) {
|
private fun addEntry(entry: ZipEntry, data: ByteBuffer) {
|
||||||
CDNeedsRewrite = true
|
centralDirectoryNeedsRewrite = true
|
||||||
|
|
||||||
entry.localHeaderOffset = filePointer.channel.position().toUInt()
|
entry.localHeaderOffset = filePointer.channel.position().toUInt()
|
||||||
|
|
||||||
@ -114,8 +114,7 @@ class ZipFile(file: File) : Closeable {
|
|||||||
compressor.finish()
|
compressor.finish()
|
||||||
|
|
||||||
val uncompressedSize = data.size
|
val uncompressedSize = data.size
|
||||||
val compressedData =
|
val compressedData = ByteArray(uncompressedSize) // I'm guessing compression won't make the data bigger.
|
||||||
ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger
|
|
||||||
|
|
||||||
val compressedDataLength = compressor.deflate(compressedData)
|
val compressedDataLength = compressor.deflate(compressedData)
|
||||||
val compressedBuffer =
|
val compressedBuffer =
|
||||||
@ -126,7 +125,7 @@ class ZipFile(file: File) : Closeable {
|
|||||||
val crc = CRC32()
|
val crc = CRC32()
|
||||||
crc.update(data)
|
crc.update(data)
|
||||||
|
|
||||||
entry.compression = 8u //deflate compression
|
entry.compression = 8u // Deflate compression.
|
||||||
entry.uncompressedSize = uncompressedSize.toUInt()
|
entry.uncompressedSize = uncompressedSize.toUInt()
|
||||||
entry.compressedSize = compressedDataLength.toUInt()
|
entry.compressedSize = compressedDataLength.toUInt()
|
||||||
entry.crc32 = crc.value.toUInt()
|
entry.crc32 = crc.value.toUInt()
|
||||||
@ -136,14 +135,14 @@ class ZipFile(file: File) : Closeable {
|
|||||||
|
|
||||||
private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
|
private fun addEntryCopyData(entry: ZipEntry, data: ByteBuffer, alignment: Int? = null) {
|
||||||
alignment?.let {
|
alignment?.let {
|
||||||
//calculate where data would end up
|
// Calculate where data would end up.
|
||||||
val dataOffset = filePointer.filePointer + entry.LFHSize
|
val dataOffset = filePointer.filePointer + entry.LFHSize
|
||||||
|
|
||||||
val mod = dataOffset % alignment
|
val mod = dataOffset % alignment
|
||||||
|
|
||||||
//wrong alignment
|
// Wrong alignment.
|
||||||
if (mod != 0L) {
|
if (mod != 0L) {
|
||||||
//add padding at end of extra field
|
// Add padding at end of extra field.
|
||||||
entry.localExtraField =
|
entry.localExtraField =
|
||||||
entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt())
|
entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt())
|
||||||
}
|
}
|
||||||
@ -152,7 +151,7 @@ class ZipFile(file: File) : Closeable {
|
|||||||
addEntry(entry, data)
|
addEntry(entry, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDataForEntry(entry: ZipEntry): ByteBuffer {
|
private fun getDataForEntry(entry: ZipEntry): ByteBuffer {
|
||||||
return filePointer.channel.map(
|
return filePointer.channel.map(
|
||||||
FileChannel.MapMode.READ_ONLY,
|
FileChannel.MapMode.READ_ONLY,
|
||||||
entry.dataOffset.toLong(),
|
entry.dataOffset.toLong(),
|
||||||
@ -160,9 +159,15 @@ class ZipFile(file: File) : Closeable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies all entries from [file] to this file but skip already existing entries.
|
||||||
|
*
|
||||||
|
* @param file The file to copy entries from.
|
||||||
|
* @param entryAlignment A function that returns the alignment for a given entry.
|
||||||
|
*/
|
||||||
fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
|
fun copyEntriesFromFileAligned(file: ZipFile, entryAlignment: (entry: ZipEntry) -> Int?) {
|
||||||
for (entry in file.entries) {
|
for (entry in file.entries) {
|
||||||
if (entries.any { it.fileName == entry.fileName }) continue //don't add duplicates
|
if (entries.any { it.fileName == entry.fileName }) continue // Skip duplicates
|
||||||
|
|
||||||
val data = file.getDataForEntry(entry)
|
val data = file.getDataForEntry(entry)
|
||||||
addEntryCopyData(entry, data, entryAlignment(entry))
|
addEntryCopyData(entry, data, entryAlignment(entry))
|
||||||
@ -170,7 +175,7 @@ class ZipFile(file: File) : Closeable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun close() {
|
override fun close() {
|
||||||
if (CDNeedsRewrite) writeCD()
|
if (centralDirectoryNeedsRewrite) writeCD()
|
||||||
filePointer.close()
|
filePointer.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,9 +1,9 @@
|
|||||||
package app.revanced.utils.signing.align.zip.structures
|
package app.revanced.utils.align.zip.structures
|
||||||
|
|
||||||
import app.revanced.utils.signing.align.zip.putUInt
|
import app.revanced.utils.align.zip.putUInt
|
||||||
import app.revanced.utils.signing.align.zip.putUShort
|
import app.revanced.utils.align.zip.putUShort
|
||||||
import app.revanced.utils.signing.align.zip.readUIntLE
|
import app.revanced.utils.align.zip.readUIntLE
|
||||||
import app.revanced.utils.signing.align.zip.readUShortLE
|
import app.revanced.utils.align.zip.readUShortLE
|
||||||
import java.io.DataInput
|
import java.io.DataInput
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
@ -1,6 +1,6 @@
|
|||||||
package app.revanced.utils.signing.align.zip.structures
|
package app.revanced.utils.align.zip.structures
|
||||||
|
|
||||||
import app.revanced.utils.signing.align.zip.*
|
import app.revanced.utils.align.zip.*
|
||||||
import java.io.DataInput
|
import java.io.DataInput
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
@ -1,75 +0,0 @@
|
|||||||
package app.revanced.utils.patcher
|
|
||||||
|
|
||||||
import app.revanced.cli.command.MainCommand.args
|
|
||||||
import app.revanced.cli.command.MainCommand.logger
|
|
||||||
import app.revanced.cli.command.PatchList
|
|
||||||
import app.revanced.patcher.Patcher
|
|
||||||
import app.revanced.patcher.data.Context
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.include
|
|
||||||
import app.revanced.patcher.extensions.PatchExtensions.patchName
|
|
||||||
import app.revanced.patcher.patch.Patch
|
|
||||||
|
|
||||||
fun Patcher.addPatchesFiltered(allPatches: PatchList) {
|
|
||||||
val packageName = this.context.packageMetadata.packageName
|
|
||||||
val packageVersion = this.context.packageMetadata.packageVersion
|
|
||||||
|
|
||||||
val includedPatches = mutableListOf<Class<out Patch<Context>>>()
|
|
||||||
allPatches.forEach patchLoop@{ patch ->
|
|
||||||
val compatiblePackages = patch.compatiblePackages
|
|
||||||
val patchName = patch.patchName.lowercase().replace(" ", "-")
|
|
||||||
val args = args.patchArgs?.patchingArgs!!
|
|
||||||
|
|
||||||
val prefix = "Skipping $patchName"
|
|
||||||
|
|
||||||
if (compatiblePackages == null) logger.trace("$patchName: No constraint on packages.")
|
|
||||||
else {
|
|
||||||
if (!compatiblePackages.any { it.name == packageName }) {
|
|
||||||
logger.trace("$prefix: Incompatible with $packageName. This patch is only compatible with ${
|
|
||||||
compatiblePackages.joinToString(
|
|
||||||
", "
|
|
||||||
) { it.name }
|
|
||||||
}")
|
|
||||||
return@patchLoop
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(args.experimental || compatiblePackages.any { it.versions.isEmpty() || it.versions.any { version -> version == packageVersion } })) {
|
|
||||||
val compatibleWith = compatiblePackages.joinToString(";") { _package ->
|
|
||||||
"${_package.name}: ${_package.versions.joinToString(", ")}"
|
|
||||||
}
|
|
||||||
logger.warn("$prefix: Incompatible with version $packageVersion. This patch is only compatible with $compatibleWith")
|
|
||||||
return@patchLoop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.excludedPatches.contains(patchName)) {
|
|
||||||
logger.info("$prefix: Manually excluded")
|
|
||||||
return@patchLoop
|
|
||||||
} else if ((!patch.include || args.exclusive) && !args.includedPatches.contains(patchName)) {
|
|
||||||
logger.info("$prefix: Excluded by default")
|
|
||||||
return@patchLoop
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.trace("Adding $patchName")
|
|
||||||
includedPatches.add(patch)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addPatches(includedPatches)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Patcher.applyPatchesVerbose() {
|
|
||||||
this.executePatches().forEach { (patch, result) ->
|
|
||||||
if (result.isSuccess) {
|
|
||||||
logger.info("$patch succeeded")
|
|
||||||
return@forEach
|
|
||||||
}
|
|
||||||
logger.error("$patch failed:")
|
|
||||||
result.exceptionOrNull()!!.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Patcher.mergeFiles() {
|
|
||||||
this.addIntegrations(args.patchArgs?.patchingArgs!!.mergeFiles) { file ->
|
|
||||||
logger.info("Merging $file")
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,5 @@
|
|||||||
package app.revanced.utils.signing
|
package app.revanced.utils.signing
|
||||||
|
|
||||||
import app.revanced.cli.command.MainCommand.logger
|
|
||||||
import app.revanced.cli.signing.SigningOptions
|
|
||||||
import com.android.apksig.ApkSigner
|
import com.android.apksig.ApkSigner
|
||||||
import org.bouncycastle.asn1.x500.X500Name
|
import org.bouncycastle.asn1.x500.X500Name
|
||||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||||
@ -17,11 +15,44 @@ import java.math.BigInteger
|
|||||||
import java.security.*
|
import java.security.*
|
||||||
import java.security.cert.X509Certificate
|
import java.security.cert.X509Certificate
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import java.util.logging.Logger
|
||||||
|
|
||||||
internal class Signer(
|
internal class ApkSigner(
|
||||||
private val signingOptions: SigningOptions
|
private val signingOptions: SigningOptions
|
||||||
) {
|
) {
|
||||||
|
private val logger = Logger.getLogger(ApkSigner::class.java.name)
|
||||||
|
|
||||||
|
private val signer: ApkSigner.Builder
|
||||||
private val passwordCharArray = signingOptions.password.toCharArray()
|
private val passwordCharArray = signingOptions.password.toCharArray()
|
||||||
|
|
||||||
|
init {
|
||||||
|
Security.addProvider(BouncyCastleProvider())
|
||||||
|
|
||||||
|
val keyStore = KeyStore.getInstance("BKS", "BC")
|
||||||
|
val alias = keyStore.let { store ->
|
||||||
|
FileInputStream(File(signingOptions.keyStoreFilePath).also {
|
||||||
|
if (!it.exists()) {
|
||||||
|
logger.info("Creating keystore at ${it.absolutePath}")
|
||||||
|
newKeystore(it)
|
||||||
|
} else {
|
||||||
|
logger.info("Using keystore at ${it.absolutePath}")
|
||||||
|
}
|
||||||
|
}).use { fis -> store.load(fis, null) }
|
||||||
|
store.aliases().nextElement()
|
||||||
|
}
|
||||||
|
|
||||||
|
with(
|
||||||
|
ApkSigner.SignerConfig.Builder(
|
||||||
|
signingOptions.cn,
|
||||||
|
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
|
||||||
|
listOf(keyStore.getCertificate(alias) as X509Certificate)
|
||||||
|
).build()
|
||||||
|
) {
|
||||||
|
this@ApkSigner.signer = ApkSigner.Builder(listOf(this))
|
||||||
|
signer.setCreatedBy(signingOptions.cn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun newKeystore(out: File) {
|
private fun newKeystore(out: File) {
|
||||||
val (publicKey, privateKey) = createKey()
|
val (publicKey, privateKey) = createKey()
|
||||||
val privateKS = KeyStore.getInstance("BKS", "BC")
|
val privateKS = KeyStore.getInstance("BKS", "BC")
|
||||||
@ -50,30 +81,12 @@ internal class Signer(
|
|||||||
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
|
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private
|
||||||
}
|
}
|
||||||
|
|
||||||
fun signApk(input: File, output: File) {
|
fun signApk(input: File, output: File): File {
|
||||||
Security.addProvider(BouncyCastleProvider())
|
|
||||||
|
|
||||||
// TODO: keystore should be saved securely
|
|
||||||
val ks = File(signingOptions.keyStoreFilePath)
|
|
||||||
if (!ks.exists()) newKeystore(ks) else {
|
|
||||||
logger.info("Found existing keystore: ${ks.name}")
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyStore = KeyStore.getInstance("BKS", "BC")
|
|
||||||
FileInputStream(ks).use { fis -> keyStore.load(fis, null) }
|
|
||||||
val alias = keyStore.aliases().nextElement()
|
|
||||||
|
|
||||||
val config = ApkSigner.SignerConfig.Builder(
|
|
||||||
signingOptions.cn,
|
|
||||||
keyStore.getKey(alias, passwordCharArray) as PrivateKey,
|
|
||||||
listOf(keyStore.getCertificate(alias) as X509Certificate)
|
|
||||||
).build()
|
|
||||||
|
|
||||||
val signer = ApkSigner.Builder(listOf(config))
|
|
||||||
signer.setCreatedBy(signingOptions.cn)
|
|
||||||
signer.setInputApk(input)
|
signer.setInputApk(input)
|
||||||
signer.setOutputApk(output)
|
signer.setOutputApk(output)
|
||||||
|
|
||||||
signer.build().sign()
|
signer.build().sign()
|
||||||
|
|
||||||
|
return output
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package app.revanced.cli.signing
|
package app.revanced.utils.signing
|
||||||
|
|
||||||
data class SigningOptions(
|
data class SigningOptions(
|
||||||
val cn: String,
|
val cn: String,
|
1
src/main/resources/app/revanced/cli/version.properties
Normal file
1
src/main/resources/app/revanced/cli/version.properties
Normal file
@ -0,0 +1 @@
|
|||||||
|
version=${projectVersion}
|
@ -2,7 +2,10 @@ package app.revanced.patcher.options
|
|||||||
|
|
||||||
import app.revanced.patcher.data.BytecodeContext
|
import app.revanced.patcher.data.BytecodeContext
|
||||||
import app.revanced.patcher.data.Context
|
import app.revanced.patcher.data.Context
|
||||||
import app.revanced.patcher.patch.*
|
import app.revanced.patcher.patch.BytecodePatch
|
||||||
|
import app.revanced.patcher.patch.OptionsContainer
|
||||||
|
import app.revanced.patcher.patch.Patch
|
||||||
|
import app.revanced.patcher.patch.PatchOption
|
||||||
import app.revanced.utils.Options
|
import app.revanced.utils.Options
|
||||||
import app.revanced.utils.Options.setOptions
|
import app.revanced.utils.Options.setOptions
|
||||||
import org.junit.jupiter.api.MethodOrderer
|
import org.junit.jupiter.api.MethodOrderer
|
||||||
@ -11,8 +14,8 @@ import org.junit.jupiter.api.Test
|
|||||||
import org.junit.jupiter.api.TestMethodOrder
|
import org.junit.jupiter.api.TestMethodOrder
|
||||||
|
|
||||||
class PatchOptionsTestPatch : BytecodePatch() {
|
class PatchOptionsTestPatch : BytecodePatch() {
|
||||||
override fun execute(context: BytecodeContext): PatchResult {
|
override fun execute(context: BytecodeContext) {
|
||||||
return PatchResultSuccess()
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : OptionsContainer() {
|
companion object : OptionsContainer() {
|
||||||
@ -32,7 +35,7 @@ class PatchOptionsTestPatch : BytecodePatch() {
|
|||||||
|
|
||||||
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
|
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
|
||||||
internal object PatchOptionOptionsTest {
|
internal object PatchOptionOptionsTest {
|
||||||
private var patches = listOf(PatchOptionsTestPatch::class.java as Class<out Patch<Context>>)
|
private var patches = listOf(PatchOptionsTestPatch::class.java as Class<out Patch<Context<*>>>)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Order(1)
|
@Order(1)
|
||||||
|
Loading…
Reference in New Issue
Block a user