From 0350b7f1a276d9dc795b22442ba4f202855ea090 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Thu, 24 Aug 2023 16:50:10 +0200 Subject: [PATCH] feat: add install command This introduces a separate utility subcommand. --- docs/1_usage.md | 18 ++- .../cli/command/ListPatchesCommand.kt | 26 ++-- .../app/revanced/cli/command/MainCommand.kt | 7 +- .../revanced/cli/command/OptionsCommand.kt | 29 ++-- .../app/revanced/cli/command/PatchCommand.kt | 139 +++++++----------- .../cli/command/utility/InstallCommand.kt | 42 ++++++ .../command/{ => utility}/UninstallCommand.kt | 23 +-- .../cli/command/utility/UtilityCommand.kt | 10 ++ .../app/revanced/utils/adb/AdbManager.kt | 2 +- 9 files changed, 155 insertions(+), 141 deletions(-) create mode 100644 src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt rename src/main/kotlin/app/revanced/cli/command/{ => utility}/UninstallCommand.kt (62%) create mode 100644 src/main/kotlin/app/revanced/cli/command/utility/UtilityCommand.kt diff --git a/docs/1_usage.md b/docs/1_usage.md index 13d9170..3f30c71 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -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 \ ``` + + > **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 \ + + ``` + + > **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`. diff --git a/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt b/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt index 4ea13d2..28419d9 100644 --- a/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt @@ -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 + private lateinit var patchBundles: Array @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 { diff --git a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt index 20c6a47..f54c096 100644 --- a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt @@ -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) { internal typealias PatchList = List -object CLIVersionProvider : IVersionProvider { +private object CLIVersionProvider : IVersionProvider { override fun getVersion(): Array { 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 \ No newline at end of file +private object MainCommand \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt b/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt index ed623d6..91f38f2 100644 --- a/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt @@ -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 + private lateinit var patchBundles: Array @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") } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt index 3b6ce68..54d8986 100644 --- a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -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() + private var patchBundles = emptyList() @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() + private var integrations = listOf() @CommandLine.Option( - names = ["-i", "--include"], - description = ["List of patches to include"] + names = ["-i", "--include"], description = ["List of patches to include"] ) - var includedPatches = arrayOf() + private var includedPatches = arrayOf() @CommandLine.Option( - names = ["-e", "--exclude"], - description = ["List of patches to exclude"] + names = ["-e", "--exclude"], description = ["List of patches to exclude"] ) - var excludedPatches = arrayOf() + private var excludedPatches = arrayOf() @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) } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt b/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt new file mode 100644 index 0000000..593680d --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt @@ -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 + + @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()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/UninstallCommand.kt b/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt similarity index 62% rename from src/main/kotlin/app/revanced/cli/command/UninstallCommand.kt rename to src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt index a0f7f8d..d8dc336 100644 --- a/src/main/kotlin/app/revanced/cli/command/UninstallCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt @@ -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 + @Parameters(description = ["ADB device serials"], arity = "1..*") + private lateinit var deviceSerials: Array - @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 { diff --git a/src/main/kotlin/app/revanced/cli/command/utility/UtilityCommand.kt b/src/main/kotlin/app/revanced/cli/command/utility/UtilityCommand.kt new file mode 100644 index 0000000..70c77c6 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/utility/UtilityCommand.kt @@ -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 \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt b/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt index 6ddfe4a..ef27e11 100644 --- a/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt +++ b/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt @@ -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)