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 > **Note**: Some patches may require integrations
such as [ReVanced Integrations](https://github.com/revanced/revanced-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. they will be merged into the APK file automatically.
- ### 🗑️ Uninstall a patched APK file - ### 🗑️ Uninstall a patched APK file
```bash ```bash
java -jar revanced-cli.jar uninstall \ java -jar revanced-cli.jar utility uninstall \
--package-name <package-name> \ --package-name <package-name> \
<device-serial> <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) private val logger = Logger.getLogger(ListPatchesCommand::class.java.name)
@Parameters( @Parameters(
description = ["Paths to patch bundles"], description = ["Paths to patch bundles"], arity = "1..*"
arity = "1..*"
) )
lateinit var patchBundles: Array<File> private lateinit var patchBundles: Array<File>
@Option( @Option(
names = ["-d", "--with-descriptions"], names = ["-d", "--with-descriptions"], description = ["List their descriptions"], showDefaultValue = ALWAYS
description = ["List their descriptions"],
showDefaultValue = ALWAYS
) )
var withDescriptions: Boolean = true private var withDescriptions: Boolean = true
@Option( @Option(
names = ["-p", "--with-packages"], names = ["-p", "--with-packages"],
description = ["List the packages the patches are compatible with"], description = ["List the packages the patches are compatible with"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var withPackages: Boolean = false private var withPackages: Boolean = false
@Option( @Option(
names = ["-v", "--with-versions"], 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 showDefaultValue = ALWAYS
) )
var withVersions: Boolean = false private var withVersions: Boolean = false
@Option( @Option(
names = ["-o", "--with-options"], names = ["-o", "--with-options"], description = ["List the options of the patches"], showDefaultValue = ALWAYS
description = ["List the options of the patches"],
showDefaultValue = ALWAYS
) )
var withOptions: Boolean = false private var withOptions: Boolean = false
override fun run() { override fun run() {
fun Package.buildString() = buildString { fun Package.buildString() = buildString {
@ -58,8 +53,7 @@ internal object ListPatchesCommand : Runnable {
appendLine("Package name: $name") appendLine("Package name: $name")
appendLine("Compatible versions:") appendLine("Compatible versions:")
append(versions.joinToString("\n") { version -> version }.prependIndent("\t")) append(versions.joinToString("\n") { version -> version }.prependIndent("\t"))
} else } else append("Package name: $name")
append("Package name: $name")
} }
fun PatchOption<*>.buildString() = buildString { fun PatchOption<*>.buildString() = buildString {

View File

@ -1,5 +1,6 @@
package app.revanced.cli.command package app.revanced.cli.command
import app.revanced.cli.command.utility.UtilityCommand
import app.revanced.patcher.patch.PatchClass import app.revanced.patcher.patch.PatchClass
import picocli.CommandLine import picocli.CommandLine
import picocli.CommandLine.Command import picocli.CommandLine.Command
@ -42,7 +43,7 @@ fun main(args: Array<String>) {
internal typealias PatchList = List<PatchClass> internal typealias PatchList = List<PatchClass>
object CLIVersionProvider : IVersionProvider { private object CLIVersionProvider : IVersionProvider {
override fun getVersion(): Array<String> { override fun getVersion(): Array<String> {
Properties().apply { Properties().apply {
load(MainCommand::class.java.getResourceAsStream("/app/revanced/cli/version.properties")) load(MainCommand::class.java.getResourceAsStream("/app/revanced/cli/version.properties"))
@ -60,8 +61,8 @@ object CLIVersionProvider : IVersionProvider {
subcommands = [ subcommands = [
ListPatchesCommand::class, ListPatchesCommand::class,
PatchCommand::class, PatchCommand::class,
UninstallCommand::class,
OptionsCommand::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) private val logger = Logger.getLogger(OptionsCommand::class.java.name)
@CommandLine.Parameters( @CommandLine.Parameters(
description = ["Paths to patch bundles"], description = ["Paths to patch bundles"], arity = "1..*"
arity = "1..*"
) )
lateinit var patchBundles: Array<File> private lateinit var patchBundles: Array<File>
@CommandLine.Option( @CommandLine.Option(
names = ["-p", "--path"], names = ["-p", "--path"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS
description = ["Path to patch options JSON file"],
showDefaultValue = ALWAYS
) )
var path: File = File("options.json") private var path: File = File("options.json")
@CommandLine.Option( @CommandLine.Option(
names = ["-o", "--overwrite"], names = ["-o", "--overwrite"], description = ["Overwrite existing options file"], showDefaultValue = ALWAYS
description = ["Overwrite existing options file"],
showDefaultValue = ALWAYS
) )
var overwrite: Boolean = false private var overwrite: Boolean = false
@CommandLine.Option( @CommandLine.Option(
names = ["-u", "--update"], names = ["-u", "--update"],
description = ["Update existing options by adding missing and removing non-existent options"], description = ["Update existing options by adding missing and removing non-existent options"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var update: Boolean = false private var update: Boolean = false
override fun run() = if (!path.exists() || overwrite) override fun run() = if (!path.exists() || overwrite) with(PatchBundleLoader.Jar(*patchBundles)) {
with(PatchBundleLoader.Jar(*patchBundles)) { if (update) setOptions(path)
if (update) setOptions(path)
Options.serialize(this, prettyPrint = true) Options.serialize(this, prettyPrint = true).let(path::writeText)
.let(path::writeText) }
}
else logger.severe("Options file already exists, use --override to override it") 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( @CommandLine.Command(
name = "patch", name = "patch", description = ["Patch the supplied APK file with the supplied patches and integrations"]
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) private val logger = Logger.getLogger(PatchCommand::class.java.name)
@CommandLine.Parameters( @CommandLine.Parameters(
description = ["APK file to be patched"], description = ["APK file to be patched"], arity = "1..1"
arity = "1..1"
) )
lateinit var apk: File private lateinit var apk: File
@CommandLine.Option( @CommandLine.Option(
names = ["-b", "--patch-bundle"], names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true
description = ["One or more bundles of patches"],
required = true
) )
var patchBundles = emptyList<File>() private var patchBundles = emptyList<File>()
@CommandLine.Option( @CommandLine.Option(
names = ["-m", "--merge"], names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"]
description = ["One or more DEX files or containers to merge into the APK"]
) )
var integrations = listOf<File>() private var integrations = listOf<File>()
@CommandLine.Option( @CommandLine.Option(
names = ["-i", "--include"], names = ["-i", "--include"], description = ["List of patches to include"]
description = ["List of patches to include"]
) )
var includedPatches = arrayOf<String>() private var includedPatches = arrayOf<String>()
@CommandLine.Option( @CommandLine.Option(
names = ["-e", "--exclude"], names = ["-e", "--exclude"], description = ["List of patches to exclude"]
description = ["List of patches to exclude"]
) )
var excludedPatches = arrayOf<String>() private var excludedPatches = arrayOf<String>()
@CommandLine.Option( @CommandLine.Option(
names = ["--options"], names = ["--options"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS
description = ["Path to patch options JSON file"],
showDefaultValue = ALWAYS
) )
var optionsFile: File = File("options.json") private var optionsFile: File = File("options.json")
@CommandLine.Option( @CommandLine.Option(
names = ["--exclusive"], names = ["--exclusive"],
description = ["Only include patches that are explicitly specified to be included"], description = ["Only include patches that are explicitly specified to be included"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var exclusive = false private var exclusive = false
@CommandLine.Option( @CommandLine.Option(
names = ["--experimental"], names = ["--experimental"],
description = ["Ignore patches incompatibility to versions"], description = ["Ignore patches incompatibility to versions"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var experimental: Boolean = false private var experimental: Boolean = false
@CommandLine.Option( @CommandLine.Option(
names = ["-o", "--out"], names = ["-o", "--out"], description = ["Path to save the patched APK file to"], required = true
description = ["Path to save the patched APK file to"],
required = true
) )
lateinit var outputFilePath: File private lateinit var outputFilePath: File
@CommandLine.Option( @CommandLine.Option(
names = ["-d", "--device-serial"], names = ["-d", "--device-serial"], description = ["ADB device serial to install to"], showDefaultValue = ALWAYS
description = ["ADB device serial to install to"],
showDefaultValue = ALWAYS
) )
var deviceSerial: String? = null private var deviceSerial: String? = null
@CommandLine.Option( @CommandLine.Option(
names = ["--mount"], names = ["--mount"], description = ["Install by mounting the patched APK file"], showDefaultValue = ALWAYS
description = ["Install by mounting the patched package"],
showDefaultValue = ALWAYS
) )
var mount: Boolean = false private var mount: Boolean = false
@CommandLine.Option( @CommandLine.Option(
names = ["--common-name"], names = ["--common-name"],
@ -108,39 +93,36 @@ internal object PatchCommand: Runnable {
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var commonName = "ReVanced" private var commonName = "ReVanced"
@CommandLine.Option( @CommandLine.Option(
names = ["--keystore"], names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"]
description = ["Path to the keystore to sign the patched APK file with"]
) )
var keystorePath: String? = null private var keystorePath: String? = null
@CommandLine.Option( @CommandLine.Option(
names = ["--password"], names = ["--password"], description = ["The password of the keystore to sign the patched APK file with"]
description = ["The password of the keystore to sign the patched APK file with"]
) )
var password = "ReVanced" private var password = "ReVanced"
@CommandLine.Option( @CommandLine.Option(
names = ["-r", "--resource-cache"], names = ["-r", "--resource-cache"],
description = ["Path to temporary resource cache directory"], description = ["Path to temporary resource cache directory"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var resourceCachePath = File("revanced-resource-cache") private var resourceCachePath = File("revanced-resource-cache")
@CommandLine.Option( @CommandLine.Option(
names = ["--custom-aapt2-binary"], names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"]
description = ["Path to a custom AAPT binary to compile resources with"]
) )
var aaptBinaryPath = File("") private var aaptBinaryPath = File("")
@CommandLine.Option( @CommandLine.Option(
names = ["-p", "--purge"], names = ["-p", "--purge"],
description = ["Purge the temporary resource cache directory after patching"], description = ["Purge the temporary resource cache directory after patching"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var purge: Boolean = false private var purge: Boolean = false
override fun run() { override fun run() {
// region Prepare // region Prepare
@ -206,8 +188,7 @@ internal object PatchCommand: Runnable {
val alignAndSignedFile = sign( val alignAndSignedFile = sign(
apk.newAlignedFile( apk.newAlignedFile(
result, result, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk")
resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk")
) )
) )
@ -287,19 +268,16 @@ internal object PatchCommand: Runnable {
it.isEmpty() || it.any { version -> version == packageVersion } it.isEmpty() || it.any { version -> version == packageVersion }
} }
if (!matchesVersion) return@patch logger.warning( if (!matchesVersion) return@patch logger.warning("${patch.patchName} is incompatible with version $packageVersion. " + "This patch is only compatible with version " + packages.joinToString(
"${patch.patchName} is incompatible with version $packageVersion. " + ";"
"This patch is only compatible with version " + ) { pkg ->
packages.joinToString(";") { pkg -> "${pkg.name}: ${pkg.versions.joinToString(", ")}"
"${pkg.name}: ${pkg.versions.joinToString(", ")}" })
}
)
} ?: return@patch logger.fine( }
"${patch.patchName} is incompatible with $packageName. " + ?: return@patch logger.fine("${patch.patchName} is incompatible with $packageName. " + "This patch is only compatible with " + packages.joinToString(
"This patch is only compatible with " + ", "
packages.joinToString(", ") { `package` -> `package`.name } ) { `package` -> `package`.name })
)
return@let return@let
} ?: logger.fine("$formattedPatchName: No constraint on packages.") } ?: 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. * @param outputFile The file to save the aligned APK to.
*/ */
private fun File.newAlignedFile( private fun File.newAlignedFile(
result: PatcherResult, result: PatcherResult, outputFile: File
outputFile: File
): File { ): File {
logger.info("Aligning $name") logger.info("Aligning $name")
@ -351,23 +328,20 @@ internal object PatchCommand: Runnable {
ZipFile(outputFile).use { file -> ZipFile(outputFile).use { file ->
result.dexFiles.forEach { result.dexFiles.forEach {
file.addEntryCompressData( file.addEntryCompressData(
ZipEntry.createWithName(it.name), ZipEntry.createWithName(it.name), it.stream.readBytes()
it.stream.readBytes()
) )
} }
result.resourceFile?.let { result.resourceFile?.let {
file.copyEntriesFromFileAligned( file.copyEntriesFromFileAligned(
ZipFile(it), ZipFile(it), ZipAligner::getEntryAlignment
ZipAligner::getEntryAlignment
) )
} }
// TODO: Do not compress result.doNotCompress // TODO: Do not compress result.doNotCompress
file.copyEntriesFromFileAligned( file.copyEntriesFromFileAligned(
ZipFile(this), ZipFile(this), ZipAligner::getEntryAlignment
ZipAligner::getEntryAlignment
) )
} }
@ -380,32 +354,25 @@ internal object PatchCommand: Runnable {
* @param inputFile The APK file to sign. * @param inputFile The APK file to sign.
* @return The signed APK file. If [mount] is true, the input file will be returned. * @return The signed APK file. If [mount] is true, the input file will be returned.
*/ */
private fun sign(inputFile: File) = if (mount) private fun sign(inputFile: File) = if (mount) inputFile
inputFile
else { else {
logger.info("Signing ${inputFile.name}") logger.info("Signing ${inputFile.name}")
val keyStoreFilePath = keystorePath ?: outputFilePath val keyStoreFilePath = keystorePath
.absoluteFile.parentFile.resolve("${outputFilePath.nameWithoutExtension}.keystore").canonicalPath ?: outputFilePath.absoluteFile.parentFile.resolve("${outputFilePath.nameWithoutExtension}.keystore").canonicalPath
val options = SigningOptions( val options = SigningOptions(
commonName, commonName, password, keyStoreFilePath
password,
keyStoreFilePath
) )
ApkSigner(options) ApkSigner(options).signApk(
.signApk( inputFile, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_signed.apk")
inputFile,
resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_signed.apk")
) )
} }
private fun purge(resourceCachePath: File) { private fun purge(resourceCachePath: File) {
val result = if (resourceCachePath.deleteRecursively()) val result = if (resourceCachePath.deleteRecursively()) "Purged resource cache directory"
"Purged resource cache directory" else "Failed to purge resource cache directory"
else
"Failed to purge resource cache directory"
logger.info(result) 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 app.revanced.utils.adb.AdbManager
import picocli.CommandLine.* import picocli.CommandLine.*
@ -13,28 +13,21 @@ import java.util.logging.Logger
internal object UninstallCommand : Runnable { internal object UninstallCommand : Runnable {
private val logger = Logger.getLogger(UninstallCommand::class.java.name) private val logger = Logger.getLogger(UninstallCommand::class.java.name)
@Parameters( @Parameters(description = ["ADB device serials"], arity = "1..*")
description = ["ADB device serials"], private lateinit var deviceSerials: Array<String>
arity = "1..*"
)
lateinit var deviceSerials: Array<String>
@Option( @Option(names = ["-p", "--package-name"], description = ["Package name to uninstall"], required = true)
names = ["-p", "--package-name"], private lateinit var packageName: String
description = ["Package name to uninstall"],
required = true
)
lateinit var packageName: String
@Option( @Option(
names = ["-u", "--unmount"], names = ["-u", "--unmount"],
description = ["Uninstall by unmounting the patched package"], description = ["Uninstall by unmounting the patched APK file"],
showDefaultValue = ALWAYS showDefaultValue = ALWAYS
) )
var unmount: Boolean = false private var unmount: Boolean = false
override fun run() = try { override fun run() = try {
deviceSerials.forEach {deviceSerial -> deviceSerials.forEach { deviceSerial ->
if (unmount) { if (unmount) {
AdbManager.RootAdbManager(deviceSerial) AdbManager.RootAdbManager(deviceSerial)
} else { } else {

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) { 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) val applyReplacement = getPlaceholderReplacement(packageName)