feat: add install command

This introduces a separate utility subcommand.
This commit is contained in:
oSumAtrIX 2023-08-24 16:50:10 +02:00
parent a3d8705e89
commit 0350b7f1a2
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
9 changed files with 155 additions and 141 deletions

View File

@ -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`.

View File

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

View File

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

View File

@ -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)) {
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")
}

View File

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

View File

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

View File

@ -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,25 +13,18 @@ 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 ->

View File

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

View File

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