mirror of
https://github.com/revanced/revanced-cli.git
synced 2024-12-03 08:52:53 +01:00
feat: add install command
This introduces a separate utility subcommand.
This commit is contained in:
parent
a3d8705e89
commit
0350b7f1a2
@ -86,12 +86,26 @@ Learn how to ReVanced CLI.
|
||||
|
||||
> **Note**: Some patches may require integrations
|
||||
such as [ReVanced Integrations](https://github.com/revanced/revanced-integrations).
|
||||
Supply them with the option `-m`. If any patches accepted by ReVanced Patcher require 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.
|
||||
|
||||
- ### 🗑️ Uninstall a patched APK file
|
||||
```bash
|
||||
java -jar revanced-cli.jar uninstall \
|
||||
java -jar revanced-cli.jar utility uninstall \
|
||||
--package-name <package-name> \
|
||||
<device-serial>
|
||||
```
|
||||
|
||||
> **Note**: You can unmount an APK file
|
||||
with the option `--unmount`.
|
||||
|
||||
- ### ️ ⚙️ 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`.
|
||||
|
@ -19,38 +19,33 @@ internal object ListPatchesCommand : Runnable {
|
||||
private val logger = Logger.getLogger(ListPatchesCommand::class.java.name)
|
||||
|
||||
@Parameters(
|
||||
description = ["Paths to patch bundles"],
|
||||
arity = "1..*"
|
||||
description = ["Paths to patch bundles"], arity = "1..*"
|
||||
)
|
||||
lateinit var patchBundles: Array<File>
|
||||
private lateinit var patchBundles: Array<File>
|
||||
|
||||
@Option(
|
||||
names = ["-d", "--with-descriptions"],
|
||||
description = ["List their descriptions"],
|
||||
showDefaultValue = ALWAYS
|
||||
names = ["-d", "--with-descriptions"], description = ["List their descriptions"], showDefaultValue = ALWAYS
|
||||
)
|
||||
var withDescriptions: Boolean = true
|
||||
private var withDescriptions: Boolean = true
|
||||
|
||||
@Option(
|
||||
names = ["-p", "--with-packages"],
|
||||
description = ["List the packages the patches are compatible with"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var withPackages: Boolean = false
|
||||
private var withPackages: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["-v", "--with-versions"],
|
||||
description = ["List the versions of the packages the patches are compatible with"],
|
||||
description = ["List the versions of the apps the patches are compatible with"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var withVersions: Boolean = false
|
||||
private var withVersions: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["-o", "--with-options"],
|
||||
description = ["List the options of the patches"],
|
||||
showDefaultValue = ALWAYS
|
||||
names = ["-o", "--with-options"], description = ["List the options of the patches"], showDefaultValue = ALWAYS
|
||||
)
|
||||
var withOptions: Boolean = false
|
||||
private var withOptions: Boolean = false
|
||||
|
||||
override fun run() {
|
||||
fun Package.buildString() = buildString {
|
||||
@ -58,8 +53,7 @@ internal object ListPatchesCommand : Runnable {
|
||||
appendLine("Package name: $name")
|
||||
appendLine("Compatible versions:")
|
||||
append(versions.joinToString("\n") { version -> version }.prependIndent("\t"))
|
||||
} else
|
||||
append("Package name: $name")
|
||||
} else append("Package name: $name")
|
||||
}
|
||||
|
||||
fun PatchOption<*>.buildString() = buildString {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package app.revanced.cli.command
|
||||
|
||||
import app.revanced.cli.command.utility.UtilityCommand
|
||||
import app.revanced.patcher.patch.PatchClass
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.Command
|
||||
@ -42,7 +43,7 @@ fun main(args: Array<String>) {
|
||||
|
||||
internal typealias PatchList = List<PatchClass>
|
||||
|
||||
object CLIVersionProvider : IVersionProvider {
|
||||
private object CLIVersionProvider : IVersionProvider {
|
||||
override fun getVersion(): Array<String> {
|
||||
Properties().apply {
|
||||
load(MainCommand::class.java.getResourceAsStream("/app/revanced/cli/version.properties"))
|
||||
@ -60,8 +61,8 @@ object CLIVersionProvider : IVersionProvider {
|
||||
subcommands = [
|
||||
ListPatchesCommand::class,
|
||||
PatchCommand::class,
|
||||
UninstallCommand::class,
|
||||
OptionsCommand::class,
|
||||
UtilityCommand::class,
|
||||
]
|
||||
)
|
||||
internal object MainCommand
|
||||
private object MainCommand
|
@ -16,38 +16,31 @@ internal object OptionsCommand : Runnable {
|
||||
private val logger = Logger.getLogger(OptionsCommand::class.java.name)
|
||||
|
||||
@CommandLine.Parameters(
|
||||
description = ["Paths to patch bundles"],
|
||||
arity = "1..*"
|
||||
description = ["Paths to patch bundles"], arity = "1..*"
|
||||
)
|
||||
lateinit var patchBundles: Array<File>
|
||||
private lateinit var patchBundles: Array<File>
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-p", "--path"],
|
||||
description = ["Path to patch options JSON file"],
|
||||
showDefaultValue = ALWAYS
|
||||
names = ["-p", "--path"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS
|
||||
)
|
||||
var path: File = File("options.json")
|
||||
private var path: File = File("options.json")
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-o", "--overwrite"],
|
||||
description = ["Overwrite existing options file"],
|
||||
showDefaultValue = ALWAYS
|
||||
names = ["-o", "--overwrite"], description = ["Overwrite existing options file"], showDefaultValue = ALWAYS
|
||||
)
|
||||
var overwrite: Boolean = false
|
||||
private var overwrite: Boolean = false
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-u", "--update"],
|
||||
description = ["Update existing options by adding missing and removing non-existent options"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var update: Boolean = false
|
||||
private var update: Boolean = false
|
||||
|
||||
override fun run() = if (!path.exists() || overwrite)
|
||||
with(PatchBundleLoader.Jar(*patchBundles)) {
|
||||
if (update) setOptions(path)
|
||||
override fun run() = if (!path.exists() || overwrite) with(PatchBundleLoader.Jar(*patchBundles)) {
|
||||
if (update) setOptions(path)
|
||||
|
||||
Options.serialize(this, prettyPrint = true)
|
||||
.let(path::writeText)
|
||||
}
|
||||
Options.serialize(this, prettyPrint = true).let(path::writeText)
|
||||
}
|
||||
else logger.severe("Options file already exists, use --override to override it")
|
||||
}
|
@ -23,84 +23,69 @@ import java.util.logging.Logger
|
||||
|
||||
|
||||
@CommandLine.Command(
|
||||
name = "patch",
|
||||
description = ["Patch the supplied APK file with the supplied patches and integrations"]
|
||||
name = "patch", description = ["Patch the supplied APK file with the supplied patches and integrations"]
|
||||
)
|
||||
internal object PatchCommand: Runnable {
|
||||
internal object PatchCommand : Runnable {
|
||||
private val logger = Logger.getLogger(PatchCommand::class.java.name)
|
||||
|
||||
@CommandLine.Parameters(
|
||||
description = ["APK file to be patched"],
|
||||
arity = "1..1"
|
||||
description = ["APK file to be patched"], arity = "1..1"
|
||||
)
|
||||
lateinit var apk: File
|
||||
private lateinit var apk: File
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-b", "--patch-bundle"],
|
||||
description = ["One or more bundles of patches"],
|
||||
required = true
|
||||
names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true
|
||||
)
|
||||
var patchBundles = emptyList<File>()
|
||||
private var patchBundles = emptyList<File>()
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-m", "--merge"],
|
||||
description = ["One or more DEX files or containers to merge into the APK"]
|
||||
names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"]
|
||||
)
|
||||
var integrations = listOf<File>()
|
||||
private var integrations = listOf<File>()
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-i", "--include"],
|
||||
description = ["List of patches to include"]
|
||||
names = ["-i", "--include"], description = ["List of patches to include"]
|
||||
)
|
||||
var includedPatches = arrayOf<String>()
|
||||
private var includedPatches = arrayOf<String>()
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-e", "--exclude"],
|
||||
description = ["List of patches to exclude"]
|
||||
names = ["-e", "--exclude"], description = ["List of patches to exclude"]
|
||||
)
|
||||
var excludedPatches = arrayOf<String>()
|
||||
private var excludedPatches = arrayOf<String>()
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--options"],
|
||||
description = ["Path to patch options JSON file"],
|
||||
showDefaultValue = ALWAYS
|
||||
names = ["--options"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS
|
||||
)
|
||||
var optionsFile: File = File("options.json")
|
||||
private var optionsFile: File = File("options.json")
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--exclusive"],
|
||||
description = ["Only include patches that are explicitly specified to be included"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var exclusive = false
|
||||
private var exclusive = false
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--experimental"],
|
||||
description = ["Ignore patches incompatibility to versions"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var experimental: Boolean = false
|
||||
private var experimental: Boolean = false
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-o", "--out"],
|
||||
description = ["Path to save the patched APK file to"],
|
||||
required = true
|
||||
names = ["-o", "--out"], description = ["Path to save the patched APK file to"], required = true
|
||||
)
|
||||
lateinit var outputFilePath: File
|
||||
private lateinit var outputFilePath: File
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-d", "--device-serial"],
|
||||
description = ["ADB device serial to install to"],
|
||||
showDefaultValue = ALWAYS
|
||||
names = ["-d", "--device-serial"], description = ["ADB device serial to install to"], showDefaultValue = ALWAYS
|
||||
)
|
||||
var deviceSerial: String? = null
|
||||
private var deviceSerial: String? = null
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--mount"],
|
||||
description = ["Install by mounting the patched package"],
|
||||
showDefaultValue = ALWAYS
|
||||
names = ["--mount"], description = ["Install by mounting the patched APK file"], showDefaultValue = ALWAYS
|
||||
)
|
||||
var mount: Boolean = false
|
||||
private var mount: Boolean = false
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--common-name"],
|
||||
@ -108,39 +93,36 @@ internal object PatchCommand: Runnable {
|
||||
showDefaultValue = ALWAYS
|
||||
|
||||
)
|
||||
var commonName = "ReVanced"
|
||||
private var commonName = "ReVanced"
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--keystore"],
|
||||
description = ["Path to the keystore to sign the patched APK file with"]
|
||||
names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"]
|
||||
)
|
||||
var keystorePath: String? = null
|
||||
private var keystorePath: String? = null
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--password"],
|
||||
description = ["The password of the keystore to sign the patched APK file with"]
|
||||
names = ["--password"], description = ["The password of the keystore to sign the patched APK file with"]
|
||||
)
|
||||
var password = "ReVanced"
|
||||
private var password = "ReVanced"
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-r", "--resource-cache"],
|
||||
description = ["Path to temporary resource cache directory"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var resourceCachePath = File("revanced-resource-cache")
|
||||
private var resourceCachePath = File("revanced-resource-cache")
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["--custom-aapt2-binary"],
|
||||
description = ["Path to a custom AAPT binary to compile resources with"]
|
||||
names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"]
|
||||
)
|
||||
var aaptBinaryPath = File("")
|
||||
private var aaptBinaryPath = File("")
|
||||
|
||||
@CommandLine.Option(
|
||||
names = ["-p", "--purge"],
|
||||
description = ["Purge the temporary resource cache directory after patching"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var purge: Boolean = false
|
||||
private var purge: Boolean = false
|
||||
|
||||
override fun run() {
|
||||
// region Prepare
|
||||
@ -206,8 +188,7 @@ internal object PatchCommand: Runnable {
|
||||
|
||||
val alignAndSignedFile = sign(
|
||||
apk.newAlignedFile(
|
||||
result,
|
||||
resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk")
|
||||
result, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk")
|
||||
)
|
||||
)
|
||||
|
||||
@ -287,19 +268,16 @@ internal object PatchCommand: Runnable {
|
||||
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(", ")}"
|
||||
}
|
||||
)
|
||||
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@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.")
|
||||
@ -341,8 +319,7 @@ internal object PatchCommand: Runnable {
|
||||
* @param outputFile The file to save the aligned APK to.
|
||||
*/
|
||||
private fun File.newAlignedFile(
|
||||
result: PatcherResult,
|
||||
outputFile: File
|
||||
result: PatcherResult, outputFile: File
|
||||
): File {
|
||||
logger.info("Aligning $name")
|
||||
|
||||
@ -351,23 +328,20 @@ internal object PatchCommand: Runnable {
|
||||
ZipFile(outputFile).use { file ->
|
||||
result.dexFiles.forEach {
|
||||
file.addEntryCompressData(
|
||||
ZipEntry.createWithName(it.name),
|
||||
it.stream.readBytes()
|
||||
ZipEntry.createWithName(it.name), it.stream.readBytes()
|
||||
)
|
||||
}
|
||||
|
||||
result.resourceFile?.let {
|
||||
file.copyEntriesFromFileAligned(
|
||||
ZipFile(it),
|
||||
ZipAligner::getEntryAlignment
|
||||
ZipFile(it), ZipAligner::getEntryAlignment
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Do not compress result.doNotCompress
|
||||
|
||||
file.copyEntriesFromFileAligned(
|
||||
ZipFile(this),
|
||||
ZipAligner::getEntryAlignment
|
||||
ZipFile(this), ZipAligner::getEntryAlignment
|
||||
)
|
||||
}
|
||||
|
||||
@ -380,32 +354,25 @@ internal object PatchCommand: Runnable {
|
||||
* @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
|
||||
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 keyStoreFilePath = keystorePath
|
||||
?: outputFilePath.absoluteFile.parentFile.resolve("${outputFilePath.nameWithoutExtension}.keystore").canonicalPath
|
||||
|
||||
val options = SigningOptions(
|
||||
commonName,
|
||||
password,
|
||||
keyStoreFilePath
|
||||
commonName, password, keyStoreFilePath
|
||||
)
|
||||
|
||||
ApkSigner(options)
|
||||
.signApk(
|
||||
inputFile,
|
||||
resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_signed.apk")
|
||||
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"
|
||||
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())
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.cli.command
|
||||
package app.revanced.cli.command.utility
|
||||
|
||||
import app.revanced.utils.adb.AdbManager
|
||||
import picocli.CommandLine.*
|
||||
@ -13,28 +13,21 @@ import java.util.logging.Logger
|
||||
internal object UninstallCommand : Runnable {
|
||||
private val logger = Logger.getLogger(UninstallCommand::class.java.name)
|
||||
|
||||
@Parameters(
|
||||
description = ["ADB device serials"],
|
||||
arity = "1..*"
|
||||
)
|
||||
lateinit var deviceSerials: Array<String>
|
||||
@Parameters(description = ["ADB device serials"], arity = "1..*")
|
||||
private lateinit var deviceSerials: Array<String>
|
||||
|
||||
@Option(
|
||||
names = ["-p", "--package-name"],
|
||||
description = ["Package name to uninstall"],
|
||||
required = true
|
||||
)
|
||||
lateinit var packageName: String
|
||||
@Option(names = ["-p", "--package-name"], description = ["Package name to uninstall"], required = true)
|
||||
private lateinit var packageName: String
|
||||
|
||||
@Option(
|
||||
names = ["-u", "--unmount"],
|
||||
description = ["Uninstall by unmounting the patched package"],
|
||||
description = ["Uninstall by unmounting the patched APK file"],
|
||||
showDefaultValue = ALWAYS
|
||||
)
|
||||
var unmount: Boolean = false
|
||||
private var unmount: Boolean = false
|
||||
|
||||
override fun run() = try {
|
||||
deviceSerials.forEach {deviceSerial ->
|
||||
deviceSerials.forEach { deviceSerial ->
|
||||
if (unmount) {
|
||||
AdbManager.RootAdbManager(deviceSerial)
|
||||
} else {
|
@ -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
|
@ -89,7 +89,7 @@ internal sealed class AdbManager(deviceSerial: String? = null) : Closeable {
|
||||
}
|
||||
|
||||
override fun uninstall(packageName: String) {
|
||||
logger.info("Uninstalling $packageName by unmounting and deleting the package")
|
||||
logger.info("Uninstalling $packageName by unmounting")
|
||||
|
||||
val applyReplacement = getPlaceholderReplacement(packageName)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user