From 07da528ce2223582f84bf64d2fec69714c647ddc Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Sat, 19 Aug 2023 01:59:57 +0200 Subject: [PATCH 01/14] refactor!: restructure code This commit focuses on improving code quality in a couple of places and bumping the dependency to ReVanced Patcher. BREAKING CHANGE: This introduces major changes to how ReVanced CLI is used from the command line. --- build.gradle.kts | 4 +- .../app/revanced/cli/command/MainCommand.kt | 383 +++++++++++------- .../app/revanced/cli/patcher/Patcher.kt | 23 -- src/main/kotlin/app/revanced/utils/adb/Adb.kt | 113 ------ .../app/revanced/utils/adb/AdbManager.kt | 130 ++++++ .../kotlin/app/revanced/utils/adb/Commands.kt | 34 +- .../app/revanced/utils/adb/Constants.kt | 47 +-- .../app/revanced/utils/patcher/Patcher.kt | 75 ---- .../patcher/options/PatchOptionOptionsTest.kt | 11 +- 9 files changed, 414 insertions(+), 406 deletions(-) delete mode 100644 src/main/kotlin/app/revanced/cli/patcher/Patcher.kt delete mode 100644 src/main/kotlin/app/revanced/utils/adb/Adb.kt create mode 100644 src/main/kotlin/app/revanced/utils/adb/AdbManager.kt delete mode 100644 src/main/kotlin/app/revanced/utils/patcher/Patcher.kt diff --git a/build.gradle.kts b/build.gradle.kts index 3a0f812..3fa04cb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,9 +23,9 @@ repositories { } dependencies { + implementation("app.revanced:revanced-patcher:14.0.0") implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22") - - implementation("app.revanced:revanced-patcher:13.0.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation("info.picocli:picocli:4.7.3") implementation("com.github.revanced:jadb:2531a28109") // Updated fork implementation("com.android.tools.build:apksig:8.1.0") diff --git a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt index 38d9e23..5ef13f0 100644 --- a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt @@ -2,28 +2,26 @@ package app.revanced.cli.command import app.revanced.cli.aligning.Aligning import app.revanced.cli.logging.impl.DefaultCliLogger -import app.revanced.cli.patcher.Patcher import app.revanced.cli.patcher.logging.impl.PatcherLogger import app.revanced.cli.signing.Signing import app.revanced.cli.signing.SigningOptions +import app.revanced.patcher.PatchBundleLoader +import app.revanced.patcher.Patcher import app.revanced.patcher.PatcherOptions -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.include import app.revanced.patcher.extensions.PatchExtensions.patchName -import app.revanced.patcher.patch.Patch -import app.revanced.patcher.util.patch.PatchBundle +import app.revanced.patcher.patch.PatchClass import app.revanced.utils.Options import app.revanced.utils.Options.setOptions -import app.revanced.utils.adb.Adb +import app.revanced.utils.adb.AdbManager +import kotlinx.coroutines.runBlocking import picocli.CommandLine.* import java.io.File -import java.nio.file.Files -/** - * Alias for return type of [PatchBundle.loadPatches]. - */ -internal typealias PatchList = List>> + +internal typealias PatchList = List private class CLIVersionProvider : IVersionProvider { override fun getVersion() = arrayOf( @@ -42,156 +40,192 @@ internal object MainCommand : Runnable { @ArgGroup(exclusive = false, multiplicity = "1") lateinit var args: Args + /** + * Arguments for the CLI + */ class Args { - // TODO: Move this so it is not required when listing patches - @Option(names = ["-a", "--apk"], description = ["APK file to be patched"], required = true) - lateinit var inputFile: File + @Option(names = ["--uninstall"], description = ["Package name to uninstall"]) + var packageName: String? = null - @Option(names = ["--unmount"], description = ["Unmount a patched APK file"]) - var unmount: Boolean = false + @Option(names = ["-d", "--device-serial"], description = ["ADB device serial number to deploy to"]) + var deviceSerial: String? = null - @Option( - names = ["-d", "--deploy"], - description = ["Deploy to the specified device that is connected via ADB"] - ) - var deploy: String? = null + @Option(names = ["--mount"], description = ["Handle deployments by mounting"]) + var mount: Boolean = false @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() + /** + * Arguments for patches. + */ + class PatchArgs { + @Option(names = ["-b", "--bundle"], description = ["One or more bundles of patches"], required = true) + var patchBundles = emptyList() - @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 listingArgs: ListingArgs? = null + @ArgGroup(exclusive = false) + var patchingArgs: PatchingArgs? = null - @ArgGroup(exclusive = false) - var patchingArgs: PatchingArgs? = null - } + /** + * Arguments for patching. + */ + class PatchingArgs { + @Option(names = ["-a", "--apk"], description = ["APK file to be patched"], required = true) + lateinit var inputFile: File - class ListingArgs { - @Option(names = ["-l", "--list"], description = ["List patches"], required = true) - var listOnly: Boolean = false + @Option( + names = ["-o", "--out"], + description = ["Path to save the patched APK file to"], + required = true + ) + lateinit var outputFilePath: File - @Option(names = ["--with-versions"], description = ["List patches with version compatibilities"]) - var withVersions: Boolean = false + @Option(names = ["--options"], description = ["Path to patch options JSON file"]) + var optionsFile: File = File("options.json") - @Option(names = ["--with-packages"], description = ["List patches with package compatibilities"]) - var withPackages: Boolean = false - } + @Option(names = ["-e", "--exclude"], description = ["List of patches to exclude"]) + var excludedPatches = arrayOf() - class PatchingArgs { - @Option(names = ["-o", "--out"], description = ["Path to save the patched APK file to"], required = true) - lateinit var outputPath: String + @Option( + names = ["--exclusive"], + description = ["Only include patches that are explicitly specified to be included"] + ) + var exclusive = false - @Option(names = ["-e", "--exclude"], description = ["Exclude patches"]) - var excludedPatches = arrayOf() + @Option(names = ["-i", "--include"], description = ["List of patches to include"]) + var includedPatches = arrayOf() - @Option( - names = ["--exclusive"], - description = ["Only include patches that were explicitly specified to be included"] - ) - var exclusive = false + @Option(names = ["--experimental"], description = ["Ignore patches incompatibility to versions"]) + var experimental: Boolean = false - @Option(names = ["-i", "--include"], description = ["Include patches"]) - var includedPatches = arrayOf() + @Option( + names = ["-m", "--merge"], + description = ["One or more DEX files or containers to merge into the APK"] + ) + var integrations = listOf() - @Option(names = ["--experimental"], description = ["Ignore patches incompatibility to versions"]) - var experimental: Boolean = false + @Option(names = ["--cn"], description = ["The common name of the signer of the patched APK file"]) + var commonName = "ReVanced" - @Option(names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"]) - var mergeFiles = listOf() + @Option( + names = ["--keystore"], + description = ["Path to the keystore to sign the patched APK file with"] + ) + var keystorePath: String? = null - @Option( - names = ["--mount"], - description = ["Mount the patched APK file over the original file instead of installing it"] - ) - var mount: Boolean = false + @Option( + names = ["-p", "--password"], + description = ["The password of the keystore to sign the patched APK file with"] + ) + var password = "ReVanced" - @Option(names = ["--cn"], description = ["The common name of the signer of the patched APK file"]) - var cn = "ReVanced" + @Option( + names = ["-r", "--resource-cache"], + description = ["Path to temporary resource cache directory"] + ) + var resourceCachePath = File("revanced-resource-cache") - @Option(names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"]) - var keystorePath: String? = null + @Option( + names = ["-c", "--clean"], + description = ["Clean up the temporary resource cache directory after patching"] + ) + var clean: Boolean = false - @Option( - names = ["-p", "--password"], - description = ["The password of the keystore to sign the patched APK file with"] - ) - var password = "ReVanced" + @Option( + names = ["--custom-aapt2-binary"], + description = ["Path to a custom AAPT binary to compile resources with"] + ) + var aaptBinaryPath = File("") + } - @Option(names = ["-t", "--temp-dir"], description = ["Path to temporary resource cache directory"]) - var cacheDirectory = "revanced-cache" + /** + * Arguments for printing patches to the console. + */ + class ListingArgs { + @Option(names = ["-l", "--list"], description = ["List patches"], required = true) + var listOnly: Boolean = false - @Option( - names = ["-c", "--clean"], - description = ["Clean up the temporary resource cache directory after patching"] - ) - var clean: Boolean = false + @Option(names = ["--with-versions"], description = ["List patches and their compatible versions"]) + var withVersions: Boolean = false - @Option( - names = ["--custom-aapt2-binary"], - description = ["Path to custom AAPT binary to compile resources with"] - ) - var aaptPath: String = "" + @Option(names = ["--with-packages"], description = ["List patches and their compatible packages"]) + var withPackages: Boolean = false + } + } } override fun run() { - if (args.patchArgs?.listingArgs?.listOnly == true) return printListOfPatches() - if (args.unmount) return unmount() + val patchArgs = args.patchArgs - val pArgs = this.args.patchArgs?.patchingArgs ?: return - val outputFile = File(pArgs.outputPath) // the file to write to + if (patchArgs?.listingArgs?.listOnly == true) return printListOfPatches() + if (args.packageName != null) return uninstall() - val allPatches = args.patchArgs!!.patchBundles.flatMap { bundle -> - PatchBundle.Jar(bundle).loadPatches() + val patchingArgs = patchArgs?.patchingArgs ?: return + + if (!patchingArgs.inputFile.exists()) return logger.error("Input file ${patchingArgs.inputFile} does not exist.") + + logger.info("Loading patches") + + val patches = PatchBundleLoader.Jar(*patchArgs.patchBundles.toTypedArray()) + val integrations = patchingArgs.integrations + + logger.info("Setting up patch options") + + patchingArgs.optionsFile.let { + if (it.exists()) patches.setOptions(it, logger) + else Options.serialize(patches, prettyPrint = true).let(it::writeText) } - args.patchArgs!!.optionsFile.let { - if (it.exists()) allPatches.setOptions(it, logger) - else Options.serialize(allPatches, prettyPrint = true).let(it::writeText) + val adbManager = args.deviceSerial?.let { serial -> + if (args.mount) AdbManager.RootAdbManager(serial, logger) else AdbManager.UserAdbManager(serial, logger) } - val patcher = app.revanced.patcher.Patcher( + val 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, + patchingArgs.inputFile, + patchingArgs.resourceCachePath, + patchingArgs.aaptBinaryPath.absolutePath, + patchingArgs.resourceCachePath.absolutePath, PatcherLogger ) ) - // prepare adb - val adb: Adb? = args.deploy?.let { - Adb(outputFile, patcher.context.packageMetadata.packageName, args.deploy!!, !pArgs.mount) - } + val result = patcher.apply { + acceptIntegrations(integrations) + acceptPatches(filterPatchSelection(patches)) - // start the patcher - val result = Patcher.start(patcher, allPatches) + // Execute patches. + runBlocking { + apply(false).collect { patchResult -> + patchResult.exception?.let { + logger.error("${patchResult.patchName} failed:\n${patchResult.exception}") + } ?: logger.info("${patchResult.patchName} succeeded") + } + } + }.get() - val cacheDirectory = File(pArgs.cacheDirectory) + patcher.close() - // align the file - val alignedFile = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_aligned.apk") - Aligning.align(result, args.inputFile, alignedFile) + val outputFileNameWithoutExtension = patchingArgs.outputFilePath.nameWithoutExtension - // sign the file - val finalFile = if (!pArgs.mount) { - val signedOutput = cacheDirectory.resolve("${outputFile.nameWithoutExtension}_signed.apk") + // Align the file. + val alignedFile = patchingArgs.resourceCachePath.resolve("${outputFileNameWithoutExtension}_aligned.apk") + Aligning.align(result, patchingArgs.inputFile, alignedFile) + + // Sign the file if needed. + val finalFile = if (!args.mount) { + val signedOutput = patchingArgs.resourceCachePath.resolve("${outputFileNameWithoutExtension}_signed.apk") Signing.sign( alignedFile, signedOutput, SigningOptions( - pArgs.cn, - pArgs.password, - pArgs.keystorePath ?: outputFile.absoluteFile.parentFile - .resolve("${outputFile.nameWithoutExtension}.keystore") + patchingArgs.commonName, + patchingArgs.password, + patchingArgs.keystorePath ?: patchingArgs.outputFilePath.absoluteFile.parentFile + .resolve("${patchingArgs.outputFilePath.nameWithoutExtension}.keystore") .canonicalPath ) ) @@ -200,46 +234,41 @@ internal object MainCommand : Runnable { } else alignedFile - // finally copy to the specified output file - logger.info("Copying ${finalFile.name} to ${outputFile.name}") - finalFile.copyTo(outputFile, overwrite = true) + logger.info("Copying ${finalFile.name} to ${patchingArgs.outputFilePath.name}") - // clean up the cache directory if needed - if (pArgs.clean) - cleanUp(pArgs.cacheDirectory) + finalFile.copyTo(patchingArgs.outputFilePath, overwrite = true) + adbManager?.install(AdbManager.Apk(patchingArgs.outputFilePath, patcher.context.packageMetadata.packageName)) - // deploy if specified - adb?.deploy() - - if (pArgs.clean && args.deploy != null) Files.delete(outputFile.toPath()) - - logger.info("Finished") + if (patchingArgs.clean) { + logger.info("Cleaning up temporary files") + patchingArgs.outputFilePath.delete() + cleanUp(patchingArgs.resourceCachePath) + } } - private fun cleanUp(cacheDirectory: String) { - val result = if (File(cacheDirectory).deleteRecursively()) + private fun cleanUp(resourceCachePath: File) { + val result = if (resourceCachePath.deleteRecursively()) "Cleaned up cache directory" else "Failed to clean up cache directory" logger.info(result) } - private fun unmount() { - 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() - } + /** + * Uninstall the specified package from the specified device. + * + */ + private fun uninstall() = args.deviceSerial?.let { serial -> + if (args.mount) { + AdbManager.RootAdbManager(serial, logger) + } else { + AdbManager.UserAdbManager(serial, logger) + }.uninstall(args.packageName!!) + } ?: logger.error("No device serial specified") private fun printListOfPatches() { val logged = mutableListOf() - for (patchBundlePath in args.patchArgs?.patchBundles!!) for (patch in PatchBundle.Jar(patchBundlePath) - .loadPatches()) { + for (patch in PatchBundleLoader.Jar(*args.patchArgs!!.patchBundles.toTypedArray())) { if (patch.patchName in logged) continue for (compatiblePackage in patch.compatiblePackages ?: continue) { val packageEntryStr = buildString { @@ -271,4 +300,78 @@ internal object MainCommand : Runnable { } } } -} + + private fun Patcher.filterPatchSelection(patches: PatchList) = buildList { + val packageName = context.packageMetadata.packageName + val packageVersion = context.packageMetadata.packageVersion + val patchingArgs = args.patchArgs!!.patchingArgs!! + + patches.forEach patch@{ patch -> + val formattedPatchName = patch.patchName.lowercase().replace(" ", "-") + + /** + * Check if the patch is explicitly excluded. + * + * Cases: + * 1. -e patch.name + * 2. -i patch.name -e patch.name + */ + + val excluded = patchingArgs.excludedPatches.contains(formattedPatchName) + if (excluded) return@patch logger.info("Excluding ${patch.patchName}") + + /** + * Check if the patch is constrained to packages. + */ + + patch.compatiblePackages?.let { packages -> + packages.singleOrNull { it.name == packageName }?.let { `package` -> + /** + * Check if the package version matches. + * If experimental is true, version matching will be skipped. + */ + + val matchesVersion = patchingArgs.experimental || `package`.versions.let { + it.isEmpty() || it.any { version -> version == packageVersion } + } + + if (!matchesVersion) return@patch logger.warn( + "${patch.patchName} is incompatible with version $packageVersion. " + + "This patch is only compatible with version " + + packages.joinToString(";") { `package` -> + "${`package`.name}: ${`package`.versions.joinToString(", ")}" + } + ) + + } ?: return@patch logger.trace( + "${patch.patchName} is incompatible with $packageName. " + + "This patch is only compatible with " + + packages.joinToString(", ") { `package` -> `package`.name } + ) + + return@let + } ?: logger.trace("$formattedPatchName: No constraint on packages.") + + /** + * Check if the patch is explicitly included. + * + * Cases: + * 1. --exclusive + * 2. --exclusive -i patch.name + */ + + val exclusive = patchingArgs.exclusive + val explicitlyIncluded = patchingArgs.includedPatches.contains(formattedPatchName) + + val implicitlyIncluded = !exclusive && patch.include // Case 3. + val exclusivelyIncluded = exclusive && explicitlyIncluded // Case 2. + + val included = implicitlyIncluded || exclusivelyIncluded + if (!included) return@patch logger.info("${patch.patchName} excluded by default") // Case 1. + + logger.trace("Adding $formattedPatchName") + + add(patch) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/patcher/Patcher.kt b/src/main/kotlin/app/revanced/cli/patcher/Patcher.kt deleted file mode 100644 index 180703e..0000000 --- a/src/main/kotlin/app/revanced/cli/patcher/Patcher.kt +++ /dev/null @@ -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() - } -} diff --git a/src/main/kotlin/app/revanced/utils/adb/Adb.kt b/src/main/kotlin/app/revanced/utils/adb/Adb.kt deleted file mode 100644 index 3c51566..0000000 --- a/src/main/kotlin/app/revanced/utils/adb/Adb.kt +++ /dev/null @@ -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 install: 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 (!install && 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 (install) { - 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() - } -} diff --git a/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt b/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt new file mode 100644 index 0000000..1bbc28d --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt @@ -0,0 +1,130 @@ +package app.revanced.utils.adb + +import app.revanced.cli.logging.CliLogger +import app.revanced.utils.adb.AdbManager.Apk +import app.revanced.utils.adb.Constants.COMMAND_CREATE_DIR +import app.revanced.utils.adb.Constants.COMMAND_DELETE +import app.revanced.utils.adb.Constants.COMMAND_INSTALL_MOUNT +import app.revanced.utils.adb.Constants.COMMAND_PREPARE_MOUNT_APK +import app.revanced.utils.adb.Constants.COMMAND_RESTART +import app.revanced.utils.adb.Constants.COMMAND_UMOUNT +import app.revanced.utils.adb.Constants.CONTENT_MOUNT_SCRIPT +import app.revanced.utils.adb.Constants.PATH_INIT_PUSH +import app.revanced.utils.adb.Constants.PATH_INSTALLATION +import app.revanced.utils.adb.Constants.PATH_MOUNT +import app.revanced.utils.adb.Constants.PATH_PATCHED_APK +import app.revanced.utils.adb.Constants.PLACEHOLDER +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 + +/** + * Adb manager. Used to install and uninstall [Apk] files. + * + * @param deviceSerial The serial of the device. + */ +internal sealed class AdbManager(deviceSerial: String? = null, protected val logger: CliLogger? = null) : Closeable { + protected val device = JadbConnection().devices.find { device -> device.serial == deviceSerial } + ?: throw IllegalArgumentException("The device with the serial $deviceSerial can not be found.") + + init { + logger?.trace("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?.trace("Closed") + } + + class RootAdbManager(deviceSerial: String, logger: CliLogger? = null) : AdbManager(deviceSerial, logger) { + 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.copyFile(apk.file, PATH_INIT_PUSH) + + device.run("$COMMAND_CREATE_DIR $PATH_INSTALLATION") + device.run(COMMAND_PREPARE_MOUNT_APK.applyReplacement()) + + device.createFile(PATH_INIT_PUSH, CONTENT_MOUNT_SCRIPT.applyReplacement()) + + device.run(COMMAND_INSTALL_MOUNT.applyReplacement()) + device.run(COMMAND_UMOUNT.applyReplacement()) // Sanity check. + device.run(PATH_MOUNT.applyReplacement()) + device.run(COMMAND_RESTART.applyReplacement()) + + super.install(apk) + } + + override fun uninstall(packageName: String) { + logger?.info("Uninstalling $packageName by unmounting and deleting the package") + + val applyReplacement = getPlaceholderReplacement(packageName) + + device.run(COMMAND_UMOUNT.applyReplacement(packageName)) + device.run(COMMAND_DELETE.applyReplacement(PATH_PATCHED_APK).applyReplacement()) + device.run(COMMAND_DELETE.applyReplacement(PATH_MOUNT).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, logger: CliLogger? = null) : AdbManager(deviceSerial, logger) { + 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. + */ + internal class Apk(val file: File, val packageName: String? = null) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/Commands.kt b/src/main/kotlin/app/revanced/utils/adb/Commands.kt index 1b3af07..744ebda 100644 --- a/src/main/kotlin/app/revanced/utils/adb/Commands.kt +++ b/src/main/kotlin/app/revanced/utils/adb/Commands.kt @@ -2,28 +2,28 @@ package app.revanced.utils.adb import se.vidstige.jadb.JadbDevice import se.vidstige.jadb.RemoteFile -import se.vidstige.jadb.ShellProcessBuilder import java.io.File +import java.util.concurrent.Callable +import java.util.concurrent.Executors -internal fun JadbDevice.buildCommand(command: String, su: Boolean = true): ShellProcessBuilder { - if (su) { - return shellProcessBuilder("su -c \'$command\'") +// return the input or output stream, depending on which first returns a value +internal fun JadbDevice.run(command: String, su: Boolean = false) = with(this.startCommand(command, su)) { + Executors.newFixedThreadPool(2).let { service -> + arrayOf(inputStream, errorStream).map { stream -> + Callable { stream.bufferedReader().use { it.readLine() } } + }.let { tasks -> service.invokeAny(tasks).also { service.shutdown() } } } - - val args = command.split(" ") as ArrayList - val cmd = args.removeFirst() - - return shellProcessBuilder(cmd, *args.toTypedArray()) } -internal fun JadbDevice.run(command: String, su: Boolean = true): Int { - return this.buildCommand(command, su).start().waitFor() -} +internal fun JadbDevice.hasSu() = + this.startCommand("su -h", false).waitFor() == 0 -internal fun JadbDevice.copy(targetPath: String, file: File) { - push(file, RemoteFile(targetPath)) -} +internal fun JadbDevice.copyFile(file: File, targetFile: String) = + push(file, RemoteFile(targetFile)) -internal fun JadbDevice.createFile(targetFile: String, content: String) { +internal fun JadbDevice.createFile(targetFile: String, content: String) = push(content.byteInputStream(), System.currentTimeMillis(), 644, RemoteFile(targetFile)) -} \ No newline at end of file + + +private fun JadbDevice.startCommand(command: String, su: Boolean) = + shellProcessBuilder(if (su) "su -c '$command'" else command).start() \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/adb/Constants.kt b/src/main/kotlin/app/revanced/utils/adb/Constants.kt index f4aa5f2..0d97211 100644 --- a/src/main/kotlin/app/revanced/utils/adb/Constants.kt +++ b/src/main/kotlin/app/revanced/utils/adb/Constants.kt @@ -1,57 +1,40 @@ package app.revanced.utils.adb internal object Constants { - // template placeholder to replace a string in commands internal const val PLACEHOLDER = "TEMPLATE_PACKAGE_NAME" - // utility commands - private const val COMMAND_CHMOD_MOUNT = "chmod +x" - internal const val COMMAND_PID_OF = "pidof -s" - internal const val COMMAND_CREATE_DIR = "mkdir -p" - 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 - private const val NAME_MOUNT_SCRIPT = "mount_revanced_$PLACEHOLDER.sh" - - // initial directory to push files to via adb push internal const val PATH_INIT_PUSH = "/data/local/tmp/revanced.delete" + internal const val PATH_INSTALLATION = "/data/adb/revanced/" + internal const val PATH_PATCHED_APK = "$PATH_INSTALLATION$PLACEHOLDER.apk" + internal const val PATH_MOUNT = "/data/adb/service.d/mount_revanced_$PLACEHOLDER.sh" - // revanced path - 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" + internal const val COMMAND_CREATE_DIR = "mkdir -p" + internal const val COMMAND_RESTART = "pm resolve-activity --brief $PLACEHOLDER | tail -n 1 | " + + "xargs am start -n && kill ${'$'}(pidof -s $PLACEHOLDER)" - // mount script path - internal const val PATH_MOUNT = "/data/adb/service.d/$NAME_MOUNT_SCRIPT" + internal const val COMMAND_PREPARE_MOUNT_APK = "base_path=\"$PATH_PATCHED_APK\" && " + + "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" - // 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" - // install mount script & set permissions - internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && $COMMAND_CHMOD_MOUNT $PATH_MOUNT" + internal const val COMMAND_INSTALL_MOUNT = "mv $PATH_INIT_PUSH $PATH_MOUNT && chmod +x $PATH_MOUNT" - // mount script - internal val CONTENT_MOUNT_SCRIPT = + internal const val CONTENT_MOUNT_SCRIPT = """ #!/system/bin/sh MAGISKTMP="${'$'}(magisk --path)" || MAGISKTMP=/sbin MIRROR="${'$'}MAGISKTMP/.magisk/mirror" while [ "${'$'}(getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 1; done - base_path="$PATH_REVANCED_APP" + base_path="$PATH_PATCHED_APK" stock_path=${'$'}( pm path $PLACEHOLDER | grep base | sed 's/package://g' ) chcon u:object_r:apk_data_file:s0 ${'$'}base_path mount -o bind ${'$'}MIRROR${'$'}base_path ${'$'}stock_path - """.trimIndent() + """ } diff --git a/src/main/kotlin/app/revanced/utils/patcher/Patcher.kt b/src/main/kotlin/app/revanced/utils/patcher/Patcher.kt deleted file mode 100644 index fd61bae..0000000 --- a/src/main/kotlin/app/revanced/utils/patcher/Patcher.kt +++ /dev/null @@ -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>>() - allPatches.forEach patchLoop@{ patch -> - val compatiblePackages = patch.compatiblePackages - val args = args.patchArgs?.patchingArgs!! - - val prefix = "Skipping ${patch.patchName}" - - if (compatiblePackages == null) logger.trace("${patch.patchName}: No package constraints.") - 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 - } - } - - val kebabCasedPatchName = patch.patchName.lowercase().replace(" ", "-") - if (args.excludedPatches.contains(kebabCasedPatchName)) { - logger.info("$prefix: Manually excluded") - return@patchLoop - } else if ((!patch.include || args.exclusive) && !args.includedPatches.contains(kebabCasedPatchName)) { - logger.info("$prefix: Excluded by default") - return@patchLoop - } - - logger.trace("Adding ${patch.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") - } -} diff --git a/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt b/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt index 68759ee..9abfca5 100644 --- a/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt +++ b/src/test/kotlin/app/revanced/patcher/options/PatchOptionOptionsTest.kt @@ -2,7 +2,10 @@ package app.revanced.patcher.options import app.revanced.patcher.data.BytecodeContext 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.setOptions import org.junit.jupiter.api.MethodOrderer @@ -11,8 +14,8 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestMethodOrder class PatchOptionsTestPatch : BytecodePatch() { - override fun execute(context: BytecodeContext): PatchResult { - return PatchResultSuccess() + override fun execute(context: BytecodeContext) { + // Do nothing } companion object : OptionsContainer() { @@ -32,7 +35,7 @@ class PatchOptionsTestPatch : BytecodePatch() { @TestMethodOrder(MethodOrderer.OrderAnnotation::class) internal object PatchOptionOptionsTest { - private var patches = listOf(PatchOptionsTestPatch::class.java as Class>) + private var patches = listOf(PatchOptionsTestPatch::class.java as Class>>) @Test @Order(1) From fe75d4ab87aca686b277baf0d9f2e676bcce3ae0 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 22 Aug 2023 22:59:07 +0200 Subject: [PATCH 02/14] docs: use correct description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0810591..0b6f6f6 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # 💻 ReVanced CLI -Command line application as an alternative to the ReVanced Manager. +Command line application to use ReVanced. From b74213f66e0d04d3a0ae6197d069631388e06580 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 22 Aug 2023 22:59:32 +0200 Subject: [PATCH 03/14] feat: use separate command to list patches --- docs/1_usage.md | 11 ++- .../cli/command/ListPatchesCommand.kt | 94 +++++++++++++++++++ .../app/revanced/cli/command/MainCommand.kt | 68 +++----------- src/main/kotlin/app/revanced/cli/main/Main.kt | 8 -- 4 files changed, 113 insertions(+), 68 deletions(-) create mode 100644 src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt delete mode 100644 src/main/kotlin/app/revanced/cli/main/Main.kt diff --git a/docs/1_usage.md b/docs/1_usage.md index a3226de..2b81de6 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -30,12 +30,15 @@ Learn how to ReVanced CLI. java -jar revanced-cli.jar -h ``` -- ### 📃 List all available patches from supplied patch bundles +- ### 📃 List patches from supplied patch bundles ```bash - java -jar revanced-cli.jar - -b revanced-patches.jar \ - -l # Names of all patches will be in kebab-case + java -jar revanced-cli.jar \ + list-patches \ + --with-packages \ + --with-versions \ + --with-options \ + revanced-patches.jar ``` - ### 💉 Use ReVanced CLI to patch an APK file but deploy without root permissions diff --git a/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt b/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt new file mode 100644 index 0000000..9c42595 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt @@ -0,0 +1,94 @@ +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 + + +@CommandLine.Command(name = "list-patches", description = ["List patches from supplied patch bundles"]) +class ListPatchesCommand : Runnable { + @CommandLine.Parameters( + description = ["Paths to patch bundles"], + arity = "1..*" + ) + lateinit var patchBundles: Array + + @CommandLine.Option( + names = ["-d", "--with-descriptions"], + description = ["List their descriptions"], + showDefaultValue = ALWAYS + ) + var withDescriptions: Boolean = true + + @CommandLine.Option( + names = ["-p", "--with-packages"], + description = ["List the packages the patches are compatible with"], + showDefaultValue = ALWAYS + ) + var withPackages: Boolean = false + + @CommandLine.Option( + names = ["-v", "--with-versions"], + description = ["List the versions of the packages the patches are compatible with"], + showDefaultValue = ALWAYS + ) + var withVersions: Boolean = false + + @CommandLine.Option( + names = ["-o", "--with-options"], + description = ["List the options of the patches"], + showDefaultValue = ALWAYS + ) + 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") + ) + } + } + + MainCommand.logger.info(PatchBundleLoader.Jar(*patchBundles).joinToString("\n\n") { it.buildString() }) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt index 5ef13f0..8394a7b 100644 --- a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt @@ -9,7 +9,6 @@ import app.revanced.patcher.PatchBundleLoader import app.revanced.patcher.Patcher import app.revanced.patcher.PatcherOptions import app.revanced.patcher.extensions.PatchExtensions.compatiblePackages -import app.revanced.patcher.extensions.PatchExtensions.description import app.revanced.patcher.extensions.PatchExtensions.include import app.revanced.patcher.extensions.PatchExtensions.patchName import app.revanced.patcher.patch.PatchClass @@ -17,9 +16,13 @@ import app.revanced.utils.Options import app.revanced.utils.Options.setOptions import app.revanced.utils.adb.AdbManager import kotlinx.coroutines.runBlocking +import picocli.CommandLine import picocli.CommandLine.* import java.io.File +fun main(args: Array) { + CommandLine(MainCommand).execute(*args) +} internal typealias PatchList = List @@ -31,13 +34,15 @@ private class CLIVersionProvider : IVersionProvider { @Command( name = "ReVanced CLI", + description = ["Command line application to use ReVanced"], mixinStandardHelpOptions = true, - versionProvider = CLIVersionProvider::class + versionProvider = CLIVersionProvider::class, + subcommands = [ListPatchesCommand::class] ) internal object MainCommand : Runnable { val logger = DefaultCliLogger() - @ArgGroup(exclusive = false, multiplicity = "1") + // @ArgGroup(exclusive = false, multiplicity = "1") lateinit var args: Args /** @@ -63,9 +68,6 @@ internal object MainCommand : Runnable { @Option(names = ["-b", "--bundle"], description = ["One or more bundles of patches"], required = true) var patchBundles = emptyList() - @ArgGroup(exclusive = false) - var listingArgs: ListingArgs? = null - @ArgGroup(exclusive = false) var patchingArgs: PatchingArgs? = null @@ -140,27 +142,12 @@ internal object MainCommand : Runnable { ) var aaptBinaryPath = File("") } - - /** - * Arguments for printing patches to the console. - */ - class ListingArgs { - @Option(names = ["-l", "--list"], description = ["List patches"], required = true) - var listOnly: Boolean = false - - @Option(names = ["--with-versions"], description = ["List patches and their compatible versions"]) - var withVersions: Boolean = false - - @Option(names = ["--with-packages"], description = ["List patches and their compatible packages"]) - var withPackages: Boolean = false - } } } override fun run() { val patchArgs = args.patchArgs - if (patchArgs?.listingArgs?.listOnly == true) return printListOfPatches() if (args.packageName != null) return uninstall() val patchingArgs = patchArgs?.patchingArgs ?: return @@ -266,41 +253,6 @@ internal object MainCommand : Runnable { }.uninstall(args.packageName!!) } ?: logger.error("No device serial specified") - private fun printListOfPatches() { - val logged = mutableListOf() - for (patch in PatchBundleLoader.Jar(*args.patchArgs!!.patchBundles.toTypedArray())) { - 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.padStart(25) - 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) - } - } - } - private fun Patcher.filterPatchSelection(patches: PatchList) = buildList { val packageName = context.packageMetadata.packageName val packageVersion = context.packageMetadata.packageVersion @@ -374,4 +326,8 @@ internal object MainCommand : Runnable { add(patch) } } + + fun main(args: Array) { + CommandLine(MainCommand).execute(*args) + } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/main/Main.kt b/src/main/kotlin/app/revanced/cli/main/Main.kt deleted file mode 100644 index 3fc7b3d..0000000 --- a/src/main/kotlin/app/revanced/cli/main/Main.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.revanced.cli.main - -import app.revanced.cli.command.MainCommand -import picocli.CommandLine - -internal fun main(args: Array) { - CommandLine(MainCommand).execute(*args) -} \ No newline at end of file From c0cc90962646cfffd5e2730ae556423271a7990b Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Tue, 22 Aug 2023 23:35:02 +0200 Subject: [PATCH 04/14] feat: use separate command to uninstall --- docs/1_usage.md | 27 ++++++++---- .../app/revanced/cli/command/MainCommand.kt | 27 +++--------- .../revanced/cli/command/UninstallCommand.kt | 44 +++++++++++++++++++ .../app/revanced/utils/adb/AdbManager.kt | 7 ++- 4 files changed, 74 insertions(+), 31 deletions(-) create mode 100644 src/main/kotlin/app/revanced/cli/command/UninstallCommand.kt diff --git a/docs/1_usage.md b/docs/1_usage.md index 2b81de6..932fdb2 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -41,6 +41,14 @@ Learn how to ReVanced CLI. revanced-patches.jar ``` +- ### ⚙️ Supply options to patches using ReVanced CLI + + 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. + + 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. + - ### 💉 Use ReVanced CLI to patch an APK file but deploy without root permissions This will deploy the patched APK file on your device by installing it. @@ -50,7 +58,7 @@ Learn how to ReVanced CLI. -a input.apk \ -o patched-output.apk \ -b revanced-patches.jar \ - -d device-name + -d device-serial ``` - ### 👾 Use ReVanced CLI to patch an APK file but deploy with root permissions @@ -64,16 +72,17 @@ Learn how to ReVanced CLI. -o patched-output.apk \ -b revanced-patches.jar \ -e vanced-microg-support \ - -d device-name \ + -d device-serial \ --mount ``` > **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. + package -- ### ⚙️ Supply options to patches using ReVanced CLI - - 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. - - 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. +- ### 🗑️ Uninstall a patched + ```bash + java -jar revanced-cli.jar \ + uninstall \ + -p package-name \ + device-serial + ``` diff --git a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt index 8394a7b..58774c5 100644 --- a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt @@ -37,10 +37,10 @@ private class CLIVersionProvider : IVersionProvider { description = ["Command line application to use ReVanced"], mixinStandardHelpOptions = true, versionProvider = CLIVersionProvider::class, - subcommands = [ListPatchesCommand::class] + subcommands = [ListPatchesCommand::class, UninstallCommand::class] ) internal object MainCommand : Runnable { - val logger = DefaultCliLogger() + internal val logger = DefaultCliLogger() // @ArgGroup(exclusive = false, multiplicity = "1") lateinit var args: Args @@ -145,11 +145,12 @@ internal object MainCommand : Runnable { } } + fun main(args: Array) { + CommandLine(MainCommand).execute(*args) + } + override fun run() { val patchArgs = args.patchArgs - - if (args.packageName != null) return uninstall() - val patchingArgs = patchArgs?.patchingArgs ?: return if (!patchingArgs.inputFile.exists()) return logger.error("Input file ${patchingArgs.inputFile} does not exist.") @@ -241,18 +242,6 @@ internal object MainCommand : Runnable { logger.info(result) } - /** - * Uninstall the specified package from the specified device. - * - */ - private fun uninstall() = args.deviceSerial?.let { serial -> - if (args.mount) { - AdbManager.RootAdbManager(serial, logger) - } else { - AdbManager.UserAdbManager(serial, logger) - }.uninstall(args.packageName!!) - } ?: logger.error("No device serial specified") - private fun Patcher.filterPatchSelection(patches: PatchList) = buildList { val packageName = context.packageMetadata.packageName val packageVersion = context.packageMetadata.packageVersion @@ -326,8 +315,4 @@ internal object MainCommand : Runnable { add(patch) } } - - fun main(args: Array) { - CommandLine(MainCommand).execute(*args) - } } \ 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/UninstallCommand.kt new file mode 100644 index 0000000..070adb7 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/UninstallCommand.kt @@ -0,0 +1,44 @@ +package app.revanced.cli.command + +import app.revanced.utils.adb.AdbManager +import picocli.CommandLine +import picocli.CommandLine.Help.Visibility.ALWAYS + + +@CommandLine.Command( + name = "uninstall", + description = ["Uninstall a patched package from the devices with the supplied ADB device serials"] +) +class UninstallCommand : Runnable { + @CommandLine.Parameters( + description = ["ADB device serials"], + arity = "1..*" + ) + lateinit var deviceSerials: Array + + @CommandLine.Option( + names = ["-p", "--package-name"], + description = ["Package name to uninstall"], + required = true + ) + lateinit var packageName: String + + @CommandLine.Option( + names = ["-u", "--unmount"], + description = ["Uninstall by unmounting the patched package"], + showDefaultValue = ALWAYS + ) + var unmount: Boolean = false + + override fun run() = try { + deviceSerials.forEach {deviceSerial -> + if (unmount) { + AdbManager.RootAdbManager(deviceSerial, MainCommand.logger) + } else { + AdbManager.UserAdbManager(deviceSerial, MainCommand.logger) + }.uninstall(packageName) + } + } catch (e: AdbManager.DeviceNotFoundException) { + MainCommand.logger.error(e.toString()) + } +} \ 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 1bbc28d..2044f51 100644 --- a/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt +++ b/src/main/kotlin/app/revanced/utils/adb/AdbManager.kt @@ -27,7 +27,7 @@ import java.io.File */ internal sealed class AdbManager(deviceSerial: String? = null, protected val logger: CliLogger? = null) : Closeable { protected val device = JadbConnection().devices.find { device -> device.serial == deviceSerial } - ?: throw IllegalArgumentException("The device with the serial $deviceSerial can not be found.") + ?: throw DeviceNotFoundException(deviceSerial) init { logger?.trace("Established connection to $deviceSerial") @@ -127,4 +127,9 @@ internal sealed class AdbManager(deviceSerial: String? = null, protected val log * @param file 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") } \ No newline at end of file From 8a5daab2a36c5e865352ad27acde820934bb7d03 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 23 Aug 2023 03:06:27 +0200 Subject: [PATCH 05/14] build: add properties resource file --- build.gradle.kts | 19 +- .../cli/command/ListPatchesCommand.kt | 18 +- .../app/revanced/cli/command/MainCommand.kt | 318 ------------------ .../revanced/cli/command/UninstallCommand.kt | 18 +- .../cli/logging/impl/DefaultCliLogger.kt | 4 +- .../app/revanced/cli/signing/Signing.kt | 12 - .../revanced/cli/signing/SigningOptions.kt | 7 - .../app/revanced/cli/version.properties | 1 + 8 files changed, 31 insertions(+), 366 deletions(-) delete mode 100644 src/main/kotlin/app/revanced/cli/command/MainCommand.kt delete mode 100644 src/main/kotlin/app/revanced/cli/signing/Signing.kt delete mode 100644 src/main/kotlin/app/revanced/cli/signing/SigningOptions.kt create mode 100644 src/main/resources/app/revanced/cli/version.properties diff --git a/build.gradle.kts b/build.gradle.kts index 3fa04cb..5510647 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,7 +24,7 @@ repositories { dependencies { implementation("app.revanced:revanced-patcher:14.0.0") - implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.22") + implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation("info.picocli:picocli:4.7.3") implementation("com.github.revanced:jadb:2531a28109") // Updated fork @@ -34,9 +34,8 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC") } -kotlin { - jvmToolchain(11) -} +kotlin { jvmToolchain(11) } + tasks { test { @@ -45,9 +44,15 @@ tasks { events("PASSED", "SKIPPED", "FAILED") } } + + processResources { + expand("projectVersion" to project.version) + } + build { dependsOn(shadowJar) } + shadowJar { manifest { attributes("Main-Class" to "app.revanced.cli.main.MainKt") @@ -61,9 +66,5 @@ tasks { // Dummy task to fix the Gradle semantic-release plugin. // Remove this if you forked it to support building only. // Tracking issue: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 - register("publish") { - group = "publish" - description = "Dummy task" - dependsOn(build) - } + register("publish") { } } diff --git a/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt b/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt index 9c42595..931ec48 100644 --- a/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt @@ -8,41 +8,41 @@ 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.* import picocli.CommandLine.Help.Visibility.ALWAYS import java.io.File -@CommandLine.Command(name = "list-patches", description = ["List patches from supplied patch bundles"]) -class ListPatchesCommand : Runnable { - @CommandLine.Parameters( +@Command(name = "list-patches", description = ["List patches from supplied patch bundles"]) +internal object ListPatchesCommand : Runnable { + @Parameters( description = ["Paths to patch bundles"], arity = "1..*" ) lateinit var patchBundles: Array - @CommandLine.Option( + @Option( names = ["-d", "--with-descriptions"], description = ["List their descriptions"], showDefaultValue = ALWAYS ) var withDescriptions: Boolean = true - @CommandLine.Option( + @Option( names = ["-p", "--with-packages"], description = ["List the packages the patches are compatible with"], showDefaultValue = ALWAYS ) var withPackages: Boolean = false - @CommandLine.Option( + @Option( names = ["-v", "--with-versions"], description = ["List the versions of the packages the patches are compatible with"], showDefaultValue = ALWAYS ) var withVersions: Boolean = false - @CommandLine.Option( + @Option( names = ["-o", "--with-options"], description = ["List the options of the patches"], showDefaultValue = ALWAYS @@ -89,6 +89,6 @@ class ListPatchesCommand : Runnable { } } - MainCommand.logger.info(PatchBundleLoader.Jar(*patchBundles).joinToString("\n\n") { it.buildString() }) + logger.info(PatchBundleLoader.Jar(*patchBundles).joinToString("\n\n") { it.buildString() }) } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt b/src/main/kotlin/app/revanced/cli/command/MainCommand.kt deleted file mode 100644 index 58774c5..0000000 --- a/src/main/kotlin/app/revanced/cli/command/MainCommand.kt +++ /dev/null @@ -1,318 +0,0 @@ -package app.revanced.cli.command - -import app.revanced.cli.aligning.Aligning -import app.revanced.cli.logging.impl.DefaultCliLogger -import app.revanced.cli.patcher.logging.impl.PatcherLogger -import app.revanced.cli.signing.Signing -import app.revanced.cli.signing.SigningOptions -import app.revanced.patcher.PatchBundleLoader -import app.revanced.patcher.Patcher -import app.revanced.patcher.PatcherOptions -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.PatchClass -import app.revanced.utils.Options -import app.revanced.utils.Options.setOptions -import app.revanced.utils.adb.AdbManager -import kotlinx.coroutines.runBlocking -import picocli.CommandLine -import picocli.CommandLine.* -import java.io.File - -fun main(args: Array) { - CommandLine(MainCommand).execute(*args) -} - -internal typealias PatchList = List - -private class CLIVersionProvider : IVersionProvider { - override fun getVersion() = arrayOf( - MainCommand::class.java.`package`.implementationVersion ?: "unknown" - ) -} - -@Command( - name = "ReVanced CLI", - description = ["Command line application to use ReVanced"], - mixinStandardHelpOptions = true, - versionProvider = CLIVersionProvider::class, - subcommands = [ListPatchesCommand::class, UninstallCommand::class] -) -internal object MainCommand : Runnable { - internal val logger = DefaultCliLogger() - - // @ArgGroup(exclusive = false, multiplicity = "1") - lateinit var args: Args - - /** - * Arguments for the CLI - */ - class Args { - @Option(names = ["--uninstall"], description = ["Package name to uninstall"]) - var packageName: String? = null - - @Option(names = ["-d", "--device-serial"], description = ["ADB device serial number to deploy to"]) - var deviceSerial: String? = null - - @Option(names = ["--mount"], description = ["Handle deployments by mounting"]) - var mount: Boolean = false - - @ArgGroup(exclusive = false) - var patchArgs: PatchArgs? = null - - /** - * Arguments for patches. - */ - class PatchArgs { - @Option(names = ["-b", "--bundle"], description = ["One or more bundles of patches"], required = true) - var patchBundles = emptyList() - - @ArgGroup(exclusive = false) - var patchingArgs: PatchingArgs? = null - - /** - * Arguments for patching. - */ - class PatchingArgs { - @Option(names = ["-a", "--apk"], description = ["APK file to be patched"], required = true) - lateinit var inputFile: File - - @Option( - names = ["-o", "--out"], - description = ["Path to save the patched APK file to"], - required = true - ) - lateinit var outputFilePath: File - - @Option(names = ["--options"], description = ["Path to patch options JSON file"]) - var optionsFile: File = File("options.json") - - @Option(names = ["-e", "--exclude"], description = ["List of patches to exclude"]) - var excludedPatches = arrayOf() - - @Option( - names = ["--exclusive"], - description = ["Only include patches that are explicitly specified to be included"] - ) - var exclusive = false - - @Option(names = ["-i", "--include"], description = ["List of patches to include"]) - var includedPatches = arrayOf() - - @Option(names = ["--experimental"], description = ["Ignore patches incompatibility to versions"]) - var experimental: Boolean = false - - @Option( - names = ["-m", "--merge"], - description = ["One or more DEX files or containers to merge into the APK"] - ) - var integrations = listOf() - - @Option(names = ["--cn"], description = ["The common name of the signer of the patched APK file"]) - var commonName = "ReVanced" - - @Option( - names = ["--keystore"], - description = ["Path to the keystore to sign the patched APK file with"] - ) - var keystorePath: String? = null - - @Option( - names = ["-p", "--password"], - description = ["The password of the keystore to sign the patched APK file with"] - ) - var password = "ReVanced" - - @Option( - names = ["-r", "--resource-cache"], - description = ["Path to temporary resource cache directory"] - ) - var resourceCachePath = File("revanced-resource-cache") - - @Option( - names = ["-c", "--clean"], - description = ["Clean up the temporary resource cache directory after patching"] - ) - var clean: Boolean = false - - @Option( - names = ["--custom-aapt2-binary"], - description = ["Path to a custom AAPT binary to compile resources with"] - ) - var aaptBinaryPath = File("") - } - } - } - - fun main(args: Array) { - CommandLine(MainCommand).execute(*args) - } - - override fun run() { - val patchArgs = args.patchArgs - val patchingArgs = patchArgs?.patchingArgs ?: return - - if (!patchingArgs.inputFile.exists()) return logger.error("Input file ${patchingArgs.inputFile} does not exist.") - - logger.info("Loading patches") - - val patches = PatchBundleLoader.Jar(*patchArgs.patchBundles.toTypedArray()) - val integrations = patchingArgs.integrations - - logger.info("Setting up patch options") - - patchingArgs.optionsFile.let { - if (it.exists()) patches.setOptions(it, logger) - else Options.serialize(patches, prettyPrint = true).let(it::writeText) - } - - val adbManager = args.deviceSerial?.let { serial -> - if (args.mount) AdbManager.RootAdbManager(serial, logger) else AdbManager.UserAdbManager(serial, logger) - } - - val patcher = Patcher( - PatcherOptions( - patchingArgs.inputFile, - patchingArgs.resourceCachePath, - patchingArgs.aaptBinaryPath.absolutePath, - patchingArgs.resourceCachePath.absolutePath, - PatcherLogger - ) - ) - - val result = patcher.apply { - acceptIntegrations(integrations) - acceptPatches(filterPatchSelection(patches)) - - // Execute patches. - runBlocking { - apply(false).collect { patchResult -> - patchResult.exception?.let { - logger.error("${patchResult.patchName} failed:\n${patchResult.exception}") - } ?: logger.info("${patchResult.patchName} succeeded") - } - } - }.get() - - patcher.close() - - val outputFileNameWithoutExtension = patchingArgs.outputFilePath.nameWithoutExtension - - // Align the file. - val alignedFile = patchingArgs.resourceCachePath.resolve("${outputFileNameWithoutExtension}_aligned.apk") - Aligning.align(result, patchingArgs.inputFile, alignedFile) - - // Sign the file if needed. - val finalFile = if (!args.mount) { - val signedOutput = patchingArgs.resourceCachePath.resolve("${outputFileNameWithoutExtension}_signed.apk") - Signing.sign( - alignedFile, - signedOutput, - SigningOptions( - patchingArgs.commonName, - patchingArgs.password, - patchingArgs.keystorePath ?: patchingArgs.outputFilePath.absoluteFile.parentFile - .resolve("${patchingArgs.outputFilePath.nameWithoutExtension}.keystore") - .canonicalPath - ) - ) - - signedOutput - } else - alignedFile - - logger.info("Copying ${finalFile.name} to ${patchingArgs.outputFilePath.name}") - - finalFile.copyTo(patchingArgs.outputFilePath, overwrite = true) - adbManager?.install(AdbManager.Apk(patchingArgs.outputFilePath, patcher.context.packageMetadata.packageName)) - - if (patchingArgs.clean) { - logger.info("Cleaning up temporary files") - patchingArgs.outputFilePath.delete() - cleanUp(patchingArgs.resourceCachePath) - } - } - - private fun cleanUp(resourceCachePath: File) { - val result = if (resourceCachePath.deleteRecursively()) - "Cleaned up cache directory" - else - "Failed to clean up cache directory" - logger.info(result) - } - - private fun Patcher.filterPatchSelection(patches: PatchList) = buildList { - val packageName = context.packageMetadata.packageName - val packageVersion = context.packageMetadata.packageVersion - val patchingArgs = args.patchArgs!!.patchingArgs!! - - patches.forEach patch@{ patch -> - val formattedPatchName = patch.patchName.lowercase().replace(" ", "-") - - /** - * Check if the patch is explicitly excluded. - * - * Cases: - * 1. -e patch.name - * 2. -i patch.name -e patch.name - */ - - val excluded = patchingArgs.excludedPatches.contains(formattedPatchName) - if (excluded) return@patch logger.info("Excluding ${patch.patchName}") - - /** - * Check if the patch is constrained to packages. - */ - - patch.compatiblePackages?.let { packages -> - packages.singleOrNull { it.name == packageName }?.let { `package` -> - /** - * Check if the package version matches. - * If experimental is true, version matching will be skipped. - */ - - val matchesVersion = patchingArgs.experimental || `package`.versions.let { - it.isEmpty() || it.any { version -> version == packageVersion } - } - - if (!matchesVersion) return@patch logger.warn( - "${patch.patchName} is incompatible with version $packageVersion. " + - "This patch is only compatible with version " + - packages.joinToString(";") { `package` -> - "${`package`.name}: ${`package`.versions.joinToString(", ")}" - } - ) - - } ?: return@patch logger.trace( - "${patch.patchName} is incompatible with $packageName. " + - "This patch is only compatible with " + - packages.joinToString(", ") { `package` -> `package`.name } - ) - - return@let - } ?: logger.trace("$formattedPatchName: No constraint on packages.") - - /** - * Check if the patch is explicitly included. - * - * Cases: - * 1. --exclusive - * 2. --exclusive -i patch.name - */ - - val exclusive = patchingArgs.exclusive - val explicitlyIncluded = patchingArgs.includedPatches.contains(formattedPatchName) - - val implicitlyIncluded = !exclusive && patch.include // Case 3. - val exclusivelyIncluded = exclusive && explicitlyIncluded // Case 2. - - val included = implicitlyIncluded || exclusivelyIncluded - if (!included) return@patch logger.info("${patch.patchName} excluded by default") // Case 1. - - logger.trace("Adding $formattedPatchName") - - add(patch) - } - } -} \ 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/UninstallCommand.kt index 070adb7..27916de 100644 --- a/src/main/kotlin/app/revanced/cli/command/UninstallCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/UninstallCommand.kt @@ -1,29 +1,29 @@ package app.revanced.cli.command import app.revanced.utils.adb.AdbManager -import picocli.CommandLine +import picocli.CommandLine.* import picocli.CommandLine.Help.Visibility.ALWAYS -@CommandLine.Command( +@Command( name = "uninstall", description = ["Uninstall a patched package from the devices with the supplied ADB device serials"] ) -class UninstallCommand : Runnable { - @CommandLine.Parameters( +internal object UninstallCommand : Runnable { + @Parameters( description = ["ADB device serials"], arity = "1..*" ) lateinit var deviceSerials: Array - @CommandLine.Option( + @Option( names = ["-p", "--package-name"], description = ["Package name to uninstall"], required = true ) lateinit var packageName: String - @CommandLine.Option( + @Option( names = ["-u", "--unmount"], description = ["Uninstall by unmounting the patched package"], showDefaultValue = ALWAYS @@ -33,12 +33,12 @@ class UninstallCommand : Runnable { override fun run() = try { deviceSerials.forEach {deviceSerial -> if (unmount) { - AdbManager.RootAdbManager(deviceSerial, MainCommand.logger) + AdbManager.RootAdbManager(deviceSerial, logger) } else { - AdbManager.UserAdbManager(deviceSerial, MainCommand.logger) + AdbManager.UserAdbManager(deviceSerial, logger) }.uninstall(packageName) } } catch (e: AdbManager.DeviceNotFoundException) { - MainCommand.logger.error(e.toString()) + logger.error(e.toString()) } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/logging/impl/DefaultCliLogger.kt b/src/main/kotlin/app/revanced/cli/logging/impl/DefaultCliLogger.kt index db9306a..d3b22bd 100644 --- a/src/main/kotlin/app/revanced/cli/logging/impl/DefaultCliLogger.kt +++ b/src/main/kotlin/app/revanced/cli/logging/impl/DefaultCliLogger.kt @@ -1,12 +1,12 @@ package app.revanced.cli.logging.impl -import app.revanced.cli.command.MainCommand +import app.revanced.cli.command.Main 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 logger: Logger = Logger.getLogger(Main::class.java.name), private val errorLogger: Logger = Logger.getLogger(logger.name + "Err") ) : CliLogger { diff --git a/src/main/kotlin/app/revanced/cli/signing/Signing.kt b/src/main/kotlin/app/revanced/cli/signing/Signing.kt deleted file mode 100644 index 75b750c..0000000 --- a/src/main/kotlin/app/revanced/cli/signing/Signing.kt +++ /dev/null @@ -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) - } -} diff --git a/src/main/kotlin/app/revanced/cli/signing/SigningOptions.kt b/src/main/kotlin/app/revanced/cli/signing/SigningOptions.kt deleted file mode 100644 index 252ef65..0000000 --- a/src/main/kotlin/app/revanced/cli/signing/SigningOptions.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.revanced.cli.signing - -data class SigningOptions( - val cn: String, - val password: String, - val keyStoreFilePath: String -) \ No newline at end of file diff --git a/src/main/resources/app/revanced/cli/version.properties b/src/main/resources/app/revanced/cli/version.properties new file mode 100644 index 0000000..308c9f8 --- /dev/null +++ b/src/main/resources/app/revanced/cli/version.properties @@ -0,0 +1 @@ +version=${projectVersion} \ No newline at end of file From 32da961d57537e99b39fd92b625a1c73f8314bc6 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 23 Aug 2023 03:08:21 +0200 Subject: [PATCH 06/14] feat: use separate command to patch --- docs/0_prerequisites.md | 2 +- docs/1_usage.md | 32 +- .../app/revanced/cli/aligning/Aligning.kt | 37 -- .../kotlin/app/revanced/cli/command/Main.kt | 39 ++ .../app/revanced/cli/command/PatchCommand.kt | 412 ++++++++++++++++++ .../utils/{signing => }/align/ZipAligner.kt | 4 +- .../{signing => }/align/zip/Extensions.kt | 2 +- .../utils/{signing => }/align/zip/ZipFile.kt | 51 ++- .../align/zip/structures/ZipEndRecord.kt | 10 +- .../align/zip/structures/ZipEntry.kt | 4 +- .../utils/signing/{Signer.kt => ApkSigner.kt} | 59 ++- .../revanced/utils/signing/SigningOptions.kt | 7 + 12 files changed, 547 insertions(+), 112 deletions(-) delete mode 100644 src/main/kotlin/app/revanced/cli/aligning/Aligning.kt create mode 100644 src/main/kotlin/app/revanced/cli/command/Main.kt create mode 100644 src/main/kotlin/app/revanced/cli/command/PatchCommand.kt rename src/main/kotlin/app/revanced/utils/{signing => }/align/ZipAligner.kt (74%) rename src/main/kotlin/app/revanced/utils/{signing => }/align/zip/Extensions.kt (96%) rename src/main/kotlin/app/revanced/utils/{signing => }/align/zip/ZipFile.kt (75%) rename src/main/kotlin/app/revanced/utils/{signing => }/align/zip/structures/ZipEndRecord.kt (89%) rename src/main/kotlin/app/revanced/utils/{signing => }/align/zip/structures/ZipEntry.kt (98%) rename src/main/kotlin/app/revanced/utils/signing/{Signer.kt => ApkSigner.kt} (68%) create mode 100644 src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt diff --git a/docs/0_prerequisites.md b/docs/0_prerequisites.md index d8654d8..ed5660f 100644 --- a/docs/0_prerequisites.md +++ b/docs/0_prerequisites.md @@ -5,7 +5,7 @@ To use ReVanced CLI, you will need to fulfil specific requirements. ## 🤝 Requirements - 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) - ReVanced Patches - ReVanced Integrations, if the patches require it diff --git a/docs/1_usage.md b/docs/1_usage.md index 932fdb2..a03abaa 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -10,7 +10,7 @@ Learn how to ReVanced CLI. 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. + If you want to install 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. ```bash adb shell su -c exit @@ -33,8 +33,7 @@ Learn how to ReVanced CLI. - ### 📃 List patches from supplied patch bundles ```bash - java -jar revanced-cli.jar \ - list-patches \ + java -jar revanced-cli.jar list-patches \ --with-packages \ --with-versions \ --with-options \ @@ -49,31 +48,31 @@ Learn how to ReVanced CLI. > **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. -- ### 💉 Use ReVanced CLI to patch an APK file but deploy without root permissions +- ### 💉 Use ReVanced CLI to patch an APK file but install without root permissions - This will deploy the patched APK file on your device by installing it. + This will install the patched APK file regularly on your device. ```bash - java -jar revanced-cli.jar \ - -a input.apk \ - -o patched-output.apk \ + java -jar revanced-cli.jar patch \ -b revanced-patches.jar \ - -d device-serial + -o patched-output.apk \ + -d device-serial \ + 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 adb install input.apk - java -jar revanced-cli.jar \ - -a input.apk \ + java -jar revanced-cli.jar patch \ -o patched-output.apk \ -b revanced-patches.jar \ - -e vanced-microg-support \ + -e some-patch \ -d 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. @@ -81,8 +80,7 @@ Learn how to ReVanced CLI. - ### 🗑️ Uninstall a patched ```bash - java -jar revanced-cli.jar \ - uninstall \ + java -jar revanced-cli.jar uninstall \ -p package-name \ device-serial ``` diff --git a/src/main/kotlin/app/revanced/cli/aligning/Aligning.kt b/src/main/kotlin/app/revanced/cli/aligning/Aligning.kt deleted file mode 100644 index d67c2ad..0000000 --- a/src/main/kotlin/app/revanced/cli/aligning/Aligning.kt +++ /dev/null @@ -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 - ) - } - } -} diff --git a/src/main/kotlin/app/revanced/cli/command/Main.kt b/src/main/kotlin/app/revanced/cli/command/Main.kt new file mode 100644 index 0000000..d33e412 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/Main.kt @@ -0,0 +1,39 @@ +package app.revanced.cli.command + +import app.revanced.cli.logging.impl.DefaultCliLogger +import app.revanced.patcher.patch.PatchClass +import picocli.CommandLine +import picocli.CommandLine.Command +import picocli.CommandLine.IVersionProvider +import java.util.* + +fun main(args: Array) { + CommandLine(Main).execute(*args) +} + +internal typealias PatchList = List + +internal val logger = DefaultCliLogger() + +object CLIVersionProvider : IVersionProvider { + override fun getVersion(): Array { + Properties().apply { + load(Main::class.java.getResourceAsStream("/app/revanced/cli/version.properties")) + }.let { + return arrayOf("ReVanced CLI v${it.getProperty("version")}") + } + } +} + +@Command( + name = "revanced-cli", + description = ["Command line application to use ReVanced"], + mixinStandardHelpOptions = true, + versionProvider = CLIVersionProvider::class, + subcommands = [ + ListPatchesCommand::class, + PatchCommand::class, + UninstallCommand::class + ] +) +internal object Main \ 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 new file mode 100644 index 0000000..279fce8 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -0,0 +1,412 @@ +package app.revanced.cli.command + +import app.revanced.cli.patcher.logging.impl.PatcherLogger +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 + + +@CommandLine.Command( + name = "patch", + description = ["Patch the supplied APK file with the supplied patches and integrations"] +) +internal object PatchCommand: Runnable { + @CommandLine.Parameters( + description = ["APK file to be patched"], + arity = "1..1" + ) + lateinit var apk: File + + @CommandLine.Option( + names = ["-b", "--bundle"], + description = ["One or more bundles of patches"], + required = true + ) + var patchBundles = emptyList() + + @CommandLine.Option( + names = ["-m", "--merge"], + description = ["One or more DEX files or containers to merge into the APK"] + ) + var integrations = listOf() + + @CommandLine.Option( + names = ["-i", "--include"], + description = ["List of patches to include"] + ) + var includedPatches = arrayOf() + + @CommandLine.Option( + names = ["-e", "--exclude"], + description = ["List of patches to exclude"] + ) + var excludedPatches = arrayOf() + + @CommandLine.Option( + names = ["--options"], + description = ["Path to patch options JSON file"], + showDefaultValue = ALWAYS + ) + 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 + + @CommandLine.Option( + names = ["--experimental"], + description = ["Ignore patches incompatibility to versions"], + showDefaultValue = ALWAYS + ) + var experimental: Boolean = false + + @CommandLine.Option( + names = ["-o", "--out"], + description = ["Path to save the patched APK file to"], + required = true + ) + lateinit var outputFilePath: File + + @CommandLine.Option( + names = ["-d", "--device-serial"], + description = ["ADB device serial to install to"], + showDefaultValue = ALWAYS + ) + var deviceSerial: String? = null + + @CommandLine.Option( + names = ["--mount"], + description = ["Install by mounting the patched package"], + showDefaultValue = ALWAYS + ) + var mount: Boolean = false + + @CommandLine.Option( + names = ["--common-name"], + description = ["The common name of the signer of the patched APK file"], + showDefaultValue = ALWAYS + + ) + var commonName = "ReVanced" + + @CommandLine.Option( + names = ["--keystore"], + description = ["Path to the keystore to sign the patched APK file with"] + ) + var keystorePath: String? = null + + @CommandLine.Option( + names = ["--password"], + description = ["The password of the keystore to sign the patched APK file with"] + ) + var password = "ReVanced" + + @CommandLine.Option( + names = ["-r", "--resource-cache"], + description = ["Path to temporary resource cache directory"], + showDefaultValue = ALWAYS + ) + var resourceCachePath = File("revanced-resource-cache") + + @CommandLine.Option( + names = ["--custom-aapt2-binary"], + description = ["Path to a custom AAPT binary to compile resources with"] + ) + var aaptBinaryPath = File("") + + @CommandLine.Option( + names = ["-p", "--purge"], + description = ["Purge the temporary resource cache directory after patching"], + showDefaultValue = ALWAYS + ) + var purge: Boolean = false + + override fun run() { + // region Prepare + + if (!apk.exists()) { + logger.error("Input file ${apk.name} does not exist") + return + } + + val adbManager = deviceSerial?.let { serial -> + if (mount) AdbManager.RootAdbManager(serial, logger) else AdbManager.UserAdbManager( + serial, + logger + ) + } + + // endregion + + // region Load patches + + logger.info("Loading patches") + + val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray()) + val integrations = integrations + + logger.info("Setting up patch options") + + optionsFile.let { + if (it.exists()) patches.setOptions(it, logger) + else Options.serialize(patches, prettyPrint = true).let(it::writeText) + } + + // endregion + + // region Patch + + val patcher = Patcher( + PatcherOptions( + apk, + resourceCachePath, + aaptBinaryPath.absolutePath, + resourceCachePath.absolutePath, + PatcherLogger + ) + ) + + val result = patcher.apply { + acceptIntegrations(integrations) + acceptPatches(filterPatchSelection(patches)) + + // Execute patches. + runBlocking { + apply(false).collect { patchResult -> + patchResult.exception?.let { + logger.error("${patchResult.patchName} failed:\n${patchResult.exception}") + } ?: 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") + outputFilePath.delete() + 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) + * - [experimental] (ignore patches incompatibility to versions) + * - package name and version of the input APK file (if [experimental] 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(" ", "-") + + /** + * Check if the patch is explicitly excluded. + * + * Cases: + * 1. -e patch.name + * 2. -i patch.name -e patch.name + */ + + /** + * Check if the patch is explicitly excluded. + * + * Cases: + * 1. -e patch.name + * 2. -i patch.name -e patch.name + */ + + val excluded = excludedPatches.contains(formattedPatchName) + if (excluded) return@patch logger.info("Excluding ${patch.patchName}") + + /** + * Check if the patch is constrained to packages. + */ + + /** + * Check if the patch is constrained to packages. + */ + + patch.compatiblePackages?.let { packages -> + packages.singleOrNull { it.name == packageName }?.let { `package` -> + /** + * Check if the package version matches. + * If experimental is true, version matching will be skipped. + */ + + /** + * Check if the package version matches. + * If experimental is true, version matching will be skipped. + */ + + val matchesVersion = experimental || `package`.versions.let { + it.isEmpty() || it.any { version -> version == packageVersion } + } + + if (!matchesVersion) return@patch logger.warn( + "${patch.patchName} is incompatible with version $packageVersion. " + + "This patch is only compatible with version " + + packages.joinToString(";") { `package` -> + "${`package`.name}: ${`package`.versions.joinToString(", ")}" + } + ) + + } ?: return@patch logger.trace( + "${patch.patchName} is incompatible with $packageName. " + + "This patch is only compatible with " + + packages.joinToString(", ") { `package` -> `package`.name } + ) + + return@let + } ?: logger.trace("$formattedPatchName: No constraint on packages.") + + /** + * Check if the patch is explicitly included. + * + * Cases: + * 1. --exclusive + * 2. --exclusive -i patch.name + */ + + /** + * Check if the patch is explicitly included. + * + * Cases: + * 1. --exclusive + * 2. --exclusive -i patch.name + */ + + val explicitlyIncluded = includedPatches.contains(formattedPatchName) + + val implicitlyIncluded = !exclusive && patch.include // Case 3. + val exclusivelyIncluded = exclusive && explicitlyIncluded // Case 2. + + val included = implicitlyIncluded || exclusivelyIncluded + if (!included) return@patch logger.info("${patch.patchName} excluded by default") // Case 1. + + logger.trace("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 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 + ) + } + + // 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) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/signing/align/ZipAligner.kt b/src/main/kotlin/app/revanced/utils/align/ZipAligner.kt similarity index 74% rename from src/main/kotlin/app/revanced/utils/signing/align/ZipAligner.kt rename to src/main/kotlin/app/revanced/utils/align/ZipAligner.kt index be40ba1..568e20b 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/ZipAligner.kt +++ b/src/main/kotlin/app/revanced/utils/align/ZipAligner.kt @@ -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 { private const val DEFAULT_ALIGNMENT = 4 diff --git a/src/main/kotlin/app/revanced/utils/signing/align/zip/Extensions.kt b/src/main/kotlin/app/revanced/utils/align/zip/Extensions.kt similarity index 96% rename from src/main/kotlin/app/revanced/utils/signing/align/zip/Extensions.kt rename to src/main/kotlin/app/revanced/utils/align/zip/Extensions.kt index 87f7db6..330c689 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/zip/Extensions.kt +++ b/src/main/kotlin/app/revanced/utils/align/zip/Extensions.kt @@ -1,4 +1,4 @@ -package app.revanced.utils.signing.align.zip +package app.revanced.utils.align.zip import java.io.DataInput import java.io.DataOutput diff --git a/src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt b/src/main/kotlin/app/revanced/utils/align/zip/ZipFile.kt similarity index 75% rename from src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt rename to src/main/kotlin/app/revanced/utils/align/zip/ZipFile.kt index e64cd1a..f961488 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/zip/ZipFile.kt +++ b/src/main/kotlin/app/revanced/utils/align/zip/ZipFile.kt @@ -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.signing.align.zip.structures.ZipEntry +import app.revanced.utils.align.zip.structures.ZipEndRecord +import app.revanced.utils.align.zip.structures.ZipEntry import java.io.Closeable import java.io.File import java.io.RandomAccessFile @@ -11,15 +11,15 @@ import java.util.zip.CRC32 import java.util.zip.Deflater class ZipFile(file: File) : Closeable { - var entries: MutableList = mutableListOf() + private var entries: MutableList = mutableListOf() private val filePointer: RandomAccessFile = RandomAccessFile(file, "rw") - private var CDNeedsRewrite = false + private var centralDirectoryNeedsRewrite = false private val compressionLevel = 5 init { - //if file isn't empty try to load entries + // If file isn't empty try to load entries. if (file.length() > 0) { val endRecord = findEndRecord() @@ -29,17 +29,17 @@ class ZipFile(file: File) : Closeable { entries = readEntries(endRecord).toMutableList() } - //seek back to start for writing + // Seek back to start for writing. filePointer.seek(0) } 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) { filePointer.seek(i) - //possible beginning of signature + // Possible beginning of signature. if (filePointer.readByte() == 0x50.toByte()) { - //seek back to get the full int + // Seek back to get the full int. filePointer.seek(i) val possibleSignature = filePointer.readUIntLE() if (possibleSignature == ZipEndRecord.ECD_SIGNATURE) { @@ -76,7 +76,7 @@ class ZipFile(file: File) : Closeable { } private fun writeCD() { - val CDStart = filePointer.channel.position().toUInt() + val centralDirectoryStartOffset = filePointer.channel.position().toUInt() entries.forEach { filePointer.channel.write(it.toCDE()) @@ -89,8 +89,8 @@ class ZipFile(file: File) : Closeable { 0u, entriesCount, entriesCount, - filePointer.channel.position().toUInt() - CDStart, - CDStart, + filePointer.channel.position().toUInt() - centralDirectoryStartOffset, + centralDirectoryStartOffset, "" ) @@ -98,7 +98,7 @@ class ZipFile(file: File) : Closeable { } private fun addEntry(entry: ZipEntry, data: ByteBuffer) { - CDNeedsRewrite = true + centralDirectoryNeedsRewrite = true entry.localHeaderOffset = filePointer.channel.position().toUInt() @@ -114,8 +114,7 @@ class ZipFile(file: File) : Closeable { compressor.finish() val uncompressedSize = data.size - val compressedData = - ByteArray(uncompressedSize) //i'm guessing compression won't make the data bigger + val compressedData = ByteArray(uncompressedSize) // I'm guessing compression won't make the data bigger. val compressedDataLength = compressor.deflate(compressedData) val compressedBuffer = @@ -126,7 +125,7 @@ class ZipFile(file: File) : Closeable { val crc = CRC32() crc.update(data) - entry.compression = 8u //deflate compression + entry.compression = 8u // Deflate compression. entry.uncompressedSize = uncompressedSize.toUInt() entry.compressedSize = compressedDataLength.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) { alignment?.let { - //calculate where data would end up + // Calculate where data would end up. val dataOffset = filePointer.filePointer + entry.LFHSize val mod = dataOffset % alignment - //wrong alignment + // Wrong alignment. if (mod != 0L) { - //add padding at end of extra field + // Add padding at end of extra field. entry.localExtraField = entry.localExtraField.copyOf((entry.localExtraField.size + (alignment - mod)).toInt()) } @@ -152,7 +151,7 @@ class ZipFile(file: File) : Closeable { addEntry(entry, data) } - fun getDataForEntry(entry: ZipEntry): ByteBuffer { + private fun getDataForEntry(entry: ZipEntry): ByteBuffer { return filePointer.channel.map( FileChannel.MapMode.READ_ONLY, 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?) { 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) addEntryCopyData(entry, data, entryAlignment(entry)) @@ -170,7 +175,7 @@ class ZipFile(file: File) : Closeable { } override fun close() { - if (CDNeedsRewrite) writeCD() + if (centralDirectoryNeedsRewrite) writeCD() filePointer.close() } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEndRecord.kt b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEndRecord.kt similarity index 89% rename from src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEndRecord.kt rename to src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEndRecord.kt index d26e551..387679e 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEndRecord.kt +++ b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEndRecord.kt @@ -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.signing.align.zip.putUShort -import app.revanced.utils.signing.align.zip.readUIntLE -import app.revanced.utils.signing.align.zip.readUShortLE +import app.revanced.utils.align.zip.putUInt +import app.revanced.utils.align.zip.putUShort +import app.revanced.utils.align.zip.readUIntLE +import app.revanced.utils.align.zip.readUShortLE import java.io.DataInput import java.nio.ByteBuffer import java.nio.ByteOrder diff --git a/src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEntry.kt b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEntry.kt similarity index 98% rename from src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEntry.kt rename to src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEntry.kt index d99a73d..316a836 100644 --- a/src/main/kotlin/app/revanced/utils/signing/align/zip/structures/ZipEntry.kt +++ b/src/main/kotlin/app/revanced/utils/align/zip/structures/ZipEntry.kt @@ -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.nio.ByteBuffer import java.nio.ByteOrder diff --git a/src/main/kotlin/app/revanced/utils/signing/Signer.kt b/src/main/kotlin/app/revanced/utils/signing/ApkSigner.kt similarity index 68% rename from src/main/kotlin/app/revanced/utils/signing/Signer.kt rename to src/main/kotlin/app/revanced/utils/signing/ApkSigner.kt index 358395a..a6bf337 100644 --- a/src/main/kotlin/app/revanced/utils/signing/Signer.kt +++ b/src/main/kotlin/app/revanced/utils/signing/ApkSigner.kt @@ -1,7 +1,6 @@ package app.revanced.utils.signing -import app.revanced.cli.command.MainCommand.logger -import app.revanced.cli.signing.SigningOptions +import app.revanced.cli.command.logger import com.android.apksig.ApkSigner import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo @@ -18,10 +17,40 @@ import java.security.* import java.security.cert.X509Certificate import java.util.* -internal class Signer( +internal class ApkSigner( private val signingOptions: SigningOptions ) { + private val signer: ApkSigner.Builder 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) { val (publicKey, privateKey) = createKey() val privateKS = KeyStore.getInstance("BKS", "BC") @@ -50,30 +79,12 @@ internal class Signer( return JcaX509CertificateConverter().getCertificate(builder.build(signer)) to pair.private } - fun signApk(input: File, output: 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) + fun signApk(input: File, output: File): File { signer.setInputApk(input) signer.setOutputApk(output) signer.build().sign() + + return output } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt b/src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt new file mode 100644 index 0000000..9ffdc6d --- /dev/null +++ b/src/main/kotlin/app/revanced/utils/signing/SigningOptions.kt @@ -0,0 +1,7 @@ +package app.revanced.utils.signing + +data class SigningOptions( + val cn: String, + val password: String, + val keyStoreFilePath: String +) \ No newline at end of file From 9edbbf31635603f89fc7bc5dcc6c023d4cdbb5a6 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 23 Aug 2023 03:35:38 +0200 Subject: [PATCH 07/14] feat: add options command --- docs/1_usage.md | 16 ++++-- .../kotlin/app/revanced/cli/command/Main.kt | 3 +- .../revanced/cli/command/OptionsCommand.kt | 50 +++++++++++++++++++ 3 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt diff --git a/docs/1_usage.md b/docs/1_usage.md index a03abaa..2bde6de 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -40,13 +40,21 @@ Learn how to ReVanced CLI. revanced-patches.jar ``` -- ### ⚙️ Supply options to patches using ReVanced CLI +- ### ⚙️ Generate options from patches using ReVanced CLI - 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. + Some patches accept options. + +- ```bash + java -jar revanced-cli.jar options \ + --overwrite \ + --update \ + revanced-patches.jar + ``` - The options file contains all options from supplied patch bundles. + > **Note**: A default `options.json` file will be automatically generated, if it does not exist + without any need of intervention. - > **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. + ```bash - ### 💉 Use ReVanced CLI to patch an APK file but install without root permissions diff --git a/src/main/kotlin/app/revanced/cli/command/Main.kt b/src/main/kotlin/app/revanced/cli/command/Main.kt index d33e412..e853138 100644 --- a/src/main/kotlin/app/revanced/cli/command/Main.kt +++ b/src/main/kotlin/app/revanced/cli/command/Main.kt @@ -33,7 +33,8 @@ object CLIVersionProvider : IVersionProvider { subcommands = [ ListPatchesCommand::class, PatchCommand::class, - UninstallCommand::class + UninstallCommand::class, + OptionsCommand::class, ] ) internal object Main \ 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 new file mode 100644 index 0000000..0d0b020 --- /dev/null +++ b/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt @@ -0,0 +1,50 @@ +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 + +@CommandLine.Command( + name = "options", + description = ["Generate options file from patches"], +) +internal object OptionsCommand : Runnable { + @CommandLine.Parameters( + description = ["Paths to patch bundles"], + arity = "1..*" + ) + lateinit var patchBundles: Array + + @CommandLine.Option( + names = ["-p", "--path"], + description = ["Path to patch options JSON file"], + showDefaultValue = ALWAYS + ) + var path: File = File("options.json") + + @CommandLine.Option( + names = ["-o", "--overwrite"], + description = ["Overwrite existing options file"], + showDefaultValue = ALWAYS + ) + 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 + + override fun run() = if (!path.exists() || overwrite) + with(PatchBundleLoader.Jar(*patchBundles)) { + if (update) setOptions(path, logger) + + Options.serialize(this, prettyPrint = true) + .let(path::writeText) + } + else logger.error("Options file already exists, use --override to override it") +} \ No newline at end of file From f8972eac3e5ee0a4a186c12cbe711925656d657b Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 23 Aug 2023 03:40:32 +0200 Subject: [PATCH 08/14] fix: use correct option name --- src/main/kotlin/app/revanced/cli/command/PatchCommand.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt index 279fce8..1298ac2 100644 --- a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -34,7 +34,7 @@ internal object PatchCommand: Runnable { lateinit var apk: File @CommandLine.Option( - names = ["-b", "--bundle"], + names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true ) From 47a20afd2de6303c4f7f39ec6d8aae4a1c95b3b6 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 23 Aug 2023 03:53:24 +0200 Subject: [PATCH 09/14] docs: improve correctness --- docs/1_usage.md | 45 ++++++++++--------- .../revanced/cli/command/UninstallCommand.kt | 2 +- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/docs/1_usage.md b/docs/1_usage.md index 2bde6de..27bd3d8 100644 --- a/docs/1_usage.md +++ b/docs/1_usage.md @@ -10,13 +10,14 @@ Learn how to ReVanced CLI. adb shell exit ``` - If you want to install 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 adb shell su -c exit ``` -2. Get the name of your device +2. Get your device serial ```bash adb devices @@ -37,22 +38,23 @@ Learn how to ReVanced CLI. --with-packages \ --with-versions \ --with-options \ - revanced-patches.jar + revanced-patches.jar [ ...] ``` - ### ⚙️ Generate options from patches using ReVanced CLI - Some patches accept options. + 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 \ - --update \ - revanced-patches.jar + revanced-patches.jar [ ...] ``` > **Note**: A default `options.json` file will be automatically generated, if it does not exist - without any need of intervention. + without any need of intervention when using the `patch` command. ```bash @@ -62,10 +64,10 @@ Learn how to ReVanced CLI. ```bash java -jar revanced-cli.jar patch \ - -b revanced-patches.jar \ - -o patched-output.apk \ - -d device-serial \ - input-apk + --patch-bundle revanced-patches.jar \ + --out output.apk \ + --device-serial \ + input.apk ``` - ### 👾 Use ReVanced CLI to patch an APK file but install with root permissions @@ -75,20 +77,23 @@ Learn how to ReVanced CLI. ```bash adb install input.apk java -jar revanced-cli.jar patch \ - -o patched-output.apk \ - -b revanced-patches.jar \ - -e some-patch \ - -d device-serial \ + --patch-bundle revanced-patches.jar \ + --include some-other-patch \ + --exclude some-patch \ + --out patched-output.apk \ + --device-serial \ --mount \ - input-apk + 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. - package + > **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, + they will be merged into the APK file automatically. - ### 🗑️ Uninstall a patched ```bash java -jar revanced-cli.jar uninstall \ - -p package-name \ - device-serial + --package-name \ + ``` diff --git a/src/main/kotlin/app/revanced/cli/command/UninstallCommand.kt b/src/main/kotlin/app/revanced/cli/command/UninstallCommand.kt index 27916de..84e673a 100644 --- a/src/main/kotlin/app/revanced/cli/command/UninstallCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/UninstallCommand.kt @@ -7,7 +7,7 @@ import picocli.CommandLine.Help.Visibility.ALWAYS @Command( name = "uninstall", - description = ["Uninstall a patched package from the devices with the supplied ADB device serials"] + description = ["Uninstall a patched APK file from the devices with the supplied ADB device serials"] ) internal object UninstallCommand : Runnable { @Parameters( From a7290353bf3def005c06e7c8997f17cf148b4d6c Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 23 Aug 2023 03:56:31 +0200 Subject: [PATCH 10/14] build: make sure to add use all necessary repositories --- build.gradle.kts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 5510647..eb96092 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,14 +12,16 @@ repositories { mavenCentral() mavenLocal() google() - maven { - url = uri("https://maven.pkg.github.com/revanced/revanced-patcher") - credentials { - username = githubUsername - password = githubPassword + 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 + } } } - maven { url = uri("https://jitpack.io") } } dependencies { From 0dcd838de3172b160e236a588446735d8716f899 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 23 Aug 2023 04:28:12 +0200 Subject: [PATCH 11/14] build: migrate dependencies to version catalogs --- build.gradle.kts | 41 +++++++++++---------------------------- gradle/libs.versions.toml | 25 ++++++++++++++++++++++++ settings.gradle.kts | 22 +++++++++++++++++++++ 3 files changed, 58 insertions(+), 30 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/build.gradle.kts b/build.gradle.kts index eb96092..0c40fa2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,44 +1,24 @@ plugins { kotlin("jvm") version "1.8.20" - id("com.github.johnrengelman.shadow") version "7.1.2" + alias(libs.plugins.shadow) } 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() - 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 - } - } - } -} - dependencies { - implementation("app.revanced:revanced-patcher:14.0.0") - implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") - implementation("info.picocli:picocli:4.7.3") - implementation("com.github.revanced:jadb:2531a28109") // Updated fork - implementation("com.android.tools.build:apksig:8.1.0") - implementation("org.bouncycastle:bcpkix-jdk15on:1.70") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.3") - testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC") + implementation(libs.revanced.patcher) + implementation(libs.kotlin.reflect) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.picocli) + implementation(libs.jadb) // Updated fork + implementation(libs.apksig) + implementation(libs.bcpkix.jdk15on) + implementation(libs.jackson.module.kotlin) + testImplementation(libs.kotlin.test) } kotlin { jvmToolchain(11) } - tasks { test { useJUnitPlatform() @@ -65,6 +45,7 @@ tasks { exclude(dependency("app.revanced:.*")) } } + // Dummy task to fix the Gradle semantic-release plugin. // Remove this if you forked it to support building only. // Tracking issue: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..7b96974 --- /dev/null +++ b/gradle/libs.versions.toml @@ -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.0.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" } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 028b7bc..9bd8dc7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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" \ No newline at end of file From a9c2a5f096627dbbf8ab1b8da26fb14529ce6bc3 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 23 Aug 2023 04:29:07 +0200 Subject: [PATCH 12/14] fix: do not use absolute path from custom AAPT2 binary option --- src/main/kotlin/app/revanced/cli/command/PatchCommand.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt index 1298ac2..14ddf82 100644 --- a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -179,7 +179,7 @@ internal object PatchCommand: Runnable { PatcherOptions( apk, resourceCachePath, - aaptBinaryPath.absolutePath, + aaptBinaryPath.path, resourceCachePath.absolutePath, PatcherLogger ) @@ -291,8 +291,8 @@ internal object PatchCommand: Runnable { if (!matchesVersion) return@patch logger.warn( "${patch.patchName} is incompatible with version $packageVersion. " + "This patch is only compatible with version " + - packages.joinToString(";") { `package` -> - "${`package`.name}: ${`package`.versions.joinToString(", ")}" + packages.joinToString(";") { pkg -> + "${pkg.name}: ${pkg.versions.joinToString(", ")}" } ) From ba758f00f4ce18791439b7e72fe1ad2e7f11f8af Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 23 Aug 2023 04:32:28 +0200 Subject: [PATCH 13/14] feat: use simpler log --- src/main/kotlin/app/revanced/cli/command/PatchCommand.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt index 14ddf82..655b26d 100644 --- a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -345,7 +345,7 @@ internal object PatchCommand: Runnable { result: PatcherResult, outputFile: File ): File { - logger.info("Aligning $name to ${outputFile.name}") + logger.info("Aligning $name") if (outputFile.exists()) outputFile.delete() From b0e748daff527ee7f417b3069882e074896fc131 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 23 Aug 2023 04:46:42 +0200 Subject: [PATCH 14/14] feat: use better logging text --- src/main/kotlin/app/revanced/cli/command/PatchCommand.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt index 655b26d..f1ccbd3 100644 --- a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -164,7 +164,7 @@ internal object PatchCommand: Runnable { val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray()) val integrations = integrations - logger.info("Setting up patch options") + logger.info("Setting patch options") optionsFile.let { if (it.exists()) patches.setOptions(it, logger)