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