diff --git a/.releaserc b/.releaserc index 85c3f702..a62c7738 100644 --- a/.releaserc +++ b/.releaserc @@ -31,7 +31,7 @@ { "assets": [ { - "path": "build/libs/*all.jar" + "path": "revanced-cli/build/libs/*all.jar" } ], successComment: false diff --git a/build.gradle.kts b/build.gradle.kts index 9c6b3357..f03e3dd4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,57 +1,7 @@ plugins { - kotlin("jvm") version "1.8.20" - alias(libs.plugins.shadow) + kotlin("jvm") version "1.9.0" apply false } -group = "app.revanced" - -dependencies { - 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() - testLogging { - events("PASSED", "SKIPPED", "FAILED") - } - } - - processResources { - expand("projectVersion" to project.version) - } - - shadowJar { - manifest { - attributes("Main-Class" to "app.revanced.cli.command.MainCommandKt") - } - minimize { - exclude(dependency("org.jetbrains.kotlin:.*")) - exclude(dependency("org.bouncycastle:.*")) - exclude(dependency("app.revanced:.*")) - } - } - - build { - dependsOn(shadowJar) - } - - // 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) - } -} +allprojects { + group = "app.revanced" +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2f3c07d6..39adf63e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,9 +6,10 @@ 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" +kotlinx-coroutines-core = "1.7.3" picocli = "4.7.3" -revanced-patcher = "15.0.0-dev.2" +revanced-patcher = "15.0.0" +binary-compatibility-validator = "0.13.2" [libraries] apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" } @@ -23,3 +24,4 @@ revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "re [plugins] shadow = { id = "com.github.johnrengelman.shadow", version.ref = "shadow" } +binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } diff --git a/revanced-cli/build.gradle.kts b/revanced-cli/build.gradle.kts new file mode 100644 index 00000000..eaeae602 --- /dev/null +++ b/revanced-cli/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + kotlin("jvm") version "1.9.0" + alias(libs.plugins.shadow) +} + +dependencies { + implementation(project(":revanced-lib")) + implementation(libs.revanced.patcher) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.picocli) + + testImplementation(libs.kotlin.test) +} + +kotlin { jvmToolchain(11) } + +tasks { + test { + useJUnitPlatform() + testLogging { + events("PASSED", "SKIPPED", "FAILED") + } + } + + processResources { + expand("projectVersion" to project.version) + } + + shadowJar { + manifest { + attributes("Main-Class" to "app.revanced.cli.command.MainCommandKt") + } + minimize { + exclude(dependency("org.jetbrains.kotlin:.*")) + exclude(dependency("org.bouncycastle:.*")) + exclude(dependency("app.revanced:.*")) + } + } + + build { + dependsOn(shadowJar) + } + + // 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) + } +} diff --git a/revanced-cli/settings.gradle.kts b/revanced-cli/settings.gradle.kts new file mode 100644 index 00000000..028b7bca --- /dev/null +++ b/revanced-cli/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "revanced-cli" \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt b/revanced-cli/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt similarity index 100% rename from src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt rename to revanced-cli/src/main/kotlin/app/revanced/cli/command/ListPatchesCommand.kt diff --git a/revanced-cli/src/main/kotlin/app/revanced/cli/command/MainCommand.kt b/revanced-cli/src/main/kotlin/app/revanced/cli/command/MainCommand.kt new file mode 100644 index 00000000..e1e1e032 --- /dev/null +++ b/revanced-cli/src/main/kotlin/app/revanced/cli/command/MainCommand.kt @@ -0,0 +1,39 @@ +package app.revanced.cli.command + +import app.revanced.cli.command.utility.UtilityCommand +import app.revanced.lib.logging.Logger +import picocli.CommandLine +import picocli.CommandLine.Command +import picocli.CommandLine.IVersionProvider +import java.util.* + + +fun main(args: Array) { + Logger.setDefault() + CommandLine(MainCommand).execute(*args) +} + +private object CLIVersionProvider : IVersionProvider { + override fun getVersion() = arrayOf( + MainCommand::class.java.getResourceAsStream( + "/app/revanced/cli/version.properties" + )?.use { stream -> + Properties().apply { load(stream) }.let { + "ReVanced CLI v${it.getProperty("version")}" + } + } ?: "ReVanced CLI") +} + +@Command( + name = "revanced-cli", + description = ["Command line application to use ReVanced"], + mixinStandardHelpOptions = true, + versionProvider = CLIVersionProvider::class, + subcommands = [ + ListPatchesCommand::class, + PatchCommand::class, + OptionsCommand::class, + UtilityCommand::class, + ] +) +private object MainCommand \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt b/revanced-cli/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt similarity index 65% rename from src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt rename to revanced-cli/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt index f81c273b..ceedbb0e 100644 --- a/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt +++ b/revanced-cli/src/main/kotlin/app/revanced/cli/command/OptionsCommand.kt @@ -1,8 +1,8 @@ package app.revanced.cli.command +import app.revanced.lib.Options +import app.revanced.lib.Options.setOptions 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 @@ -37,10 +37,17 @@ internal object OptionsCommand : Runnable { ) private var update: Boolean = false - override fun run() = if (!filePath.exists() || overwrite) with(PatchBundleLoader.Jar(*patchBundles)) { - if (update && filePath.exists()) setOptions(filePath) + override fun run() = try { + PatchBundleLoader.Jar(*patchBundles).let { patches -> + if (!filePath.exists() || overwrite) { + if (update && filePath.exists()) patches.setOptions(filePath) - Options.serialize(this, prettyPrint = true).let(filePath::writeText) + Options.serialize(patches, prettyPrint = true).let(filePath::writeText) + } else throw OptionsFileAlreadyExistsException() + } + } catch (ex: OptionsFileAlreadyExistsException) { + logger.severe("Options file already exists, use --overwrite to override it") } - else logger.severe("Options file already exists, use --overwrite to override it") + + class OptionsFileAlreadyExistsException : Exception() } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt b/revanced-cli/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt similarity index 66% rename from src/main/kotlin/app/revanced/cli/command/PatchCommand.kt rename to revanced-cli/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt index b39b048e..97dc6a13 100644 --- a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt +++ b/revanced-cli/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -1,17 +1,19 @@ package app.revanced.cli.command -import app.revanced.patcher.* -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 app.revanced.lib.ApkUtils +import app.revanced.lib.Options +import app.revanced.lib.Options.setOptions +import app.revanced.lib.adb.AdbManager +import app.revanced.lib.signing.SigningOptions +import app.revanced.patcher.PatchBundleLoader +import app.revanced.patcher.PatchSet +import app.revanced.patcher.Patcher +import app.revanced.patcher.PatcherOptions import kotlinx.coroutines.runBlocking import picocli.CommandLine import picocli.CommandLine.Help.Visibility.ALWAYS +import picocli.CommandLine.Model.CommandSpec +import picocli.CommandLine.Spec import java.io.File import java.io.PrintWriter import java.io.StringWriter @@ -24,21 +26,15 @@ import java.util.logging.Logger internal object PatchCommand : Runnable { private val logger = Logger.getLogger(PatchCommand::class.java.name) - @CommandLine.Parameters( - description = ["APK file to be patched"], arity = "1..1" - ) - private lateinit var apk: File + @Spec + lateinit var spec: CommandSpec // injected by picocli - @CommandLine.Option( - names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true - ) - private var patchBundles = emptyList() + private lateinit var apk: File - @CommandLine.Option( - names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"] - ) private var integrations = listOf() + private var patchBundles = emptyList() + @CommandLine.Option( names = ["-i", "--include"], description = ["List of patches to include"] ) @@ -94,7 +90,7 @@ internal object PatchCommand : Runnable { @CommandLine.Option( names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"] ) - private var keystorePath: String? = null + private var keystoreFilePath: File? = null @CommandLine.Option( names = ["--password"], description = ["The password of the keystore to sign the patched APK file with"] @@ -108,10 +104,7 @@ internal object PatchCommand : Runnable { ) private var resourceCachePath = File("revanced-resource-cache") - @CommandLine.Option( - names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"] - ) - private var aaptBinaryPath = File("") + private var aaptBinaryPath: File? = null @CommandLine.Option( names = ["-p", "--purge"], @@ -120,36 +113,60 @@ internal object PatchCommand : Runnable { ) private var purge: Boolean = false - override fun run() { - // region Prepare + @CommandLine.Parameters( + description = ["APK file to be patched"], arity = "1..1" + ) + @Suppress("unused") + private fun setApk(apk: File) { + if (!apk.exists()) throw CommandLine.ParameterException( + spec.commandLine(), + "APK file ${apk.name} does not exist" + ) + this.apk = apk + } - if (!apk.exists()) { - logger.severe("APK file ${apk.name} does not exist") - return + @CommandLine.Option( + names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"] + ) + @Suppress("unused") + private fun setIntegrations(integrations: Array) { + integrations.firstOrNull { !it.exists() }?.let { + throw CommandLine.ParameterException(spec.commandLine(), "Integrations file ${it.name} does not exist") } + this.integrations += integrations + } - integrations.filter { !it.exists() }.let { - if (it.isEmpty()) return@let - - it.forEach { integration -> - logger.severe("Integration file ${integration.name} does not exist") - } - return + @CommandLine.Option( + names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true + ) + @Suppress("unused") + private fun setPatchBundles(patchBundles: Array) { + patchBundles.firstOrNull { !it.exists() }?.let { + throw CommandLine.ParameterException(spec.commandLine(), "Patch bundle ${it.name} does not exist") } + this.patchBundles = patchBundles.toList() + } - val adbManager = deviceSerial?.let { serial -> - if (mount) AdbManager.RootAdbManager(serial) - else AdbManager.UserAdbManager(serial) - } + @CommandLine.Option( + names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"] + ) + @Suppress("unused") + private fun setAaptBinaryPath(aaptBinaryPath: File) { + if (!aaptBinaryPath.exists()) throw CommandLine.ParameterException( + spec.commandLine(), + "AAPT binary ${aaptBinaryPath.name} does not exist" + ) + this.aaptBinaryPath = aaptBinaryPath + } - // endregion + override fun run() { + val adbManager = deviceSerial?.let { serial -> AdbManager.getAdbManager(serial, mount) } // region Load patches logger.info("Loading patches") val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray()) - val integrations = integrations logger.info("Setting patch options") @@ -160,57 +177,63 @@ internal object PatchCommand : Runnable { // endregion - // region Patch - - val patcher = Patcher( + Patcher( PatcherOptions( apk, resourceCachePath, - aaptBinaryPath.path, + aaptBinaryPath?.path, resourceCachePath.absolutePath, ) - ) - - val result = patcher.apply { - acceptIntegrations(integrations) - acceptPatches(filterPatchSelection(patches)) - - // Execute patches. - runBlocking { - apply(false).collect { patchResult -> - patchResult.exception?.let { - StringWriter().use { writer -> - it.printStackTrace(PrintWriter(writer)) - logger.severe("${patchResult.patch.name} failed:\n$writer") - } - } ?: logger.info("${patchResult.patch.name} succeeded") + ).use { patcher -> + // region Patch + + val patcherResult = patcher.apply { + acceptIntegrations(integrations) + acceptPatches(filterPatchSelection(patches)) + + // Execute patches. + runBlocking { + apply(false).collect { patchResult -> + patchResult.exception?.let { + StringWriter().use { writer -> + it.printStackTrace(PrintWriter(writer)) + logger.severe("${patchResult.patch.name} failed:\n$writer") + } + } ?: logger.info("${patchResult.patch.name} succeeded") + } } - } - }.get() - - patcher.close() - - // endregion + }.get() + + // endregion + + // region Save + + val tempFile = resourceCachePath.resolve(apk.name) + ApkUtils.copyAligned(apk, tempFile, patcherResult) + if (!mount) ApkUtils.sign( + tempFile, + outputFilePath, + SigningOptions( + commonName, + password, + keystoreFilePath ?: outputFilePath.absoluteFile.parentFile + .resolve("${outputFilePath.nameWithoutExtension}.keystore"), + ) + ) - // region Finish + // endregion - val alignAndSignedFile = sign( - apk.newAlignedFile( - result, resourceCachePath.resolve("${outputFilePath.nameWithoutExtension}_aligned.apk") - ) - ) + // region Install - logger.info("Copying to ${outputFilePath.name}") - alignAndSignedFile.copyTo(outputFilePath, overwrite = true) + adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName)) - adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName)) + // endregion + } if (purge) { logger.info("Purging temporary files") purge(resourceCachePath) } - - // endregion } @@ -279,64 +302,6 @@ internal object PatchCommand : Runnable { } } - /** - * 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") - - 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" diff --git a/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt b/revanced-cli/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt similarity index 66% rename from src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt rename to revanced-cli/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt index 593680d3..680ed20f 100644 --- a/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt +++ b/revanced-cli/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt @@ -1,6 +1,6 @@ package app.revanced.cli.command.utility -import app.revanced.utils.adb.AdbManager +import app.revanced.lib.adb.AdbManager import picocli.CommandLine.* import java.io.File import java.util.logging.Logger @@ -28,15 +28,11 @@ internal object InstallCommand : Runnable { ) private var packageName: String? = null - override fun run() = try { - deviceSerials.forEach { deviceSerial -> - if (packageName != null) { - AdbManager.RootAdbManager(deviceSerial) - } else { - AdbManager.UserAdbManager(deviceSerial) - }.install(AdbManager.Apk(apk, packageName)) + override fun run() = deviceSerials.forEach { deviceSerial -> + try { + AdbManager.getAdbManager(deviceSerial, packageName != null).install(AdbManager.Apk(apk, packageName)) + } catch (e: AdbManager.DeviceNotFoundException) { + logger.severe(e.toString()) } - } catch (e: AdbManager.DeviceNotFoundException) { - logger.severe(e.toString()) } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt b/revanced-cli/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt similarity index 69% rename from src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt rename to revanced-cli/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt index e17bec74..52463f47 100644 --- a/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt +++ b/revanced-cli/src/main/kotlin/app/revanced/cli/command/utility/UninstallCommand.kt @@ -1,6 +1,6 @@ package app.revanced.cli.command.utility -import app.revanced.utils.adb.AdbManager +import app.revanced.lib.adb.AdbManager import picocli.CommandLine.* import picocli.CommandLine.Help.Visibility.ALWAYS import java.util.logging.Logger @@ -26,15 +26,11 @@ internal object UninstallCommand : Runnable { ) private var unmount: Boolean = false - override fun run() = try { - deviceSerials.forEach { deviceSerial -> - if (unmount) { - AdbManager.RootAdbManager(deviceSerial) - } else { - AdbManager.UserAdbManager(deviceSerial) - }.uninstall(packageName) + override fun run() = deviceSerials.forEach { deviceSerial -> + try { + AdbManager.getAdbManager(deviceSerial, unmount).uninstall(packageName) + } catch (e: AdbManager.DeviceNotFoundException) { + logger.severe(e.toString()) } - } catch (e: AdbManager.DeviceNotFoundException) { - logger.severe(e.toString()) } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/cli/command/utility/UtilityCommand.kt b/revanced-cli/src/main/kotlin/app/revanced/cli/command/utility/UtilityCommand.kt similarity index 100% rename from src/main/kotlin/app/revanced/cli/command/utility/UtilityCommand.kt rename to revanced-cli/src/main/kotlin/app/revanced/cli/command/utility/UtilityCommand.kt diff --git a/src/main/resources/app/revanced/cli/version.properties b/revanced-cli/src/main/resources/app/revanced/cli/version.properties similarity index 100% rename from src/main/resources/app/revanced/cli/version.properties rename to revanced-cli/src/main/resources/app/revanced/cli/version.properties diff --git a/revanced-lib/api/revanced-lib.api b/revanced-lib/api/revanced-lib.api new file mode 100644 index 00000000..dd646c9f --- /dev/null +++ b/revanced-lib/api/revanced-lib.api @@ -0,0 +1,118 @@ +public final class app/revanced/lib/ApkUtils { + public static final field INSTANCE Lapp/revanced/lib/ApkUtils; + public final fun copyAligned (Ljava/io/File;Ljava/io/File;Lapp/revanced/patcher/PatcherResult;)V + public final fun sign (Ljava/io/File;Ljava/io/File;Lapp/revanced/lib/signing/SigningOptions;)V +} + +public final class app/revanced/lib/Options { + public static final field INSTANCE Lapp/revanced/lib/Options; + public final fun deserialize (Ljava/lang/String;)[Lapp/revanced/lib/Options$Patch; + public final fun serialize (Ljava/util/Set;Z)Ljava/lang/String; + public static synthetic fun serialize$default (Lapp/revanced/lib/Options;Ljava/util/Set;ZILjava/lang/Object;)Ljava/lang/String; + public final fun setOptions (Ljava/util/Set;Ljava/io/File;)V + public final fun setOptions (Ljava/util/Set;Ljava/lang/String;)V +} + +public final class app/revanced/lib/Options$Patch { + public final fun getOptions ()Ljava/util/List; + public final fun getPatchName ()Ljava/lang/String; +} + +public final class app/revanced/lib/Options$Patch$Option { + public final fun getKey ()Ljava/lang/String; + public final fun getValue ()Ljava/lang/Object; +} + +public abstract class app/revanced/lib/adb/AdbManager { + public static final field Companion Lapp/revanced/lib/adb/AdbManager$Companion; + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + protected final fun getDevice ()Lse/vidstige/jadb/JadbDevice; + protected final fun getLogger ()Ljava/util/logging/Logger; + public fun install (Lapp/revanced/lib/adb/AdbManager$Apk;)V + public fun uninstall (Ljava/lang/String;)V +} + +public final class app/revanced/lib/adb/AdbManager$Apk { + public fun (Ljava/io/File;Ljava/lang/String;)V + public synthetic fun (Ljava/io/File;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getFile ()Ljava/io/File; + public final fun getPackageName ()Ljava/lang/String; +} + +public final class app/revanced/lib/adb/AdbManager$Companion { + public final fun getAdbManager (Ljava/lang/String;Z)Lapp/revanced/lib/adb/AdbManager; + public static synthetic fun getAdbManager$default (Lapp/revanced/lib/adb/AdbManager$Companion;Ljava/lang/String;ZILjava/lang/Object;)Lapp/revanced/lib/adb/AdbManager; +} + +public final class app/revanced/lib/adb/AdbManager$DeviceNotFoundException : java/lang/Exception { +} + +public final class app/revanced/lib/adb/AdbManager$FailedToFindInstalledPackageException : java/lang/Exception { +} + +public final class app/revanced/lib/adb/AdbManager$PackageNameRequiredException : java/lang/Exception { +} + +public final class app/revanced/lib/adb/AdbManager$RootAdbManager : app/revanced/lib/adb/AdbManager { + public static final field Utils Lapp/revanced/lib/adb/AdbManager$RootAdbManager$Utils; + public fun install (Lapp/revanced/lib/adb/AdbManager$Apk;)V + public fun uninstall (Ljava/lang/String;)V +} + +public final class app/revanced/lib/adb/AdbManager$RootAdbManager$Utils { +} + +public final class app/revanced/lib/adb/AdbManager$UserAdbManager : app/revanced/lib/adb/AdbManager { + public fun install (Lapp/revanced/lib/adb/AdbManager$Apk;)V + public fun uninstall (Ljava/lang/String;)V +} + +public final class app/revanced/lib/logging/Logger { + public static final field INSTANCE Lapp/revanced/lib/logging/Logger; + public final fun addHandler (Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V + public final fun removeAllHandlers ()V + public final fun setDefault ()V + public final fun setFormat (Ljava/lang/String;)V + public static synthetic fun setFormat$default (Lapp/revanced/lib/logging/Logger;Ljava/lang/String;ILjava/lang/Object;)V +} + +public final class app/revanced/lib/signing/ApkSigner { + public fun (Lapp/revanced/lib/signing/SigningOptions;)V + public final fun signApk (Ljava/io/File;Ljava/io/File;)V +} + +public final class app/revanced/lib/signing/SigningOptions { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/io/File;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/io/File; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/io/File;)Lapp/revanced/lib/signing/SigningOptions; + public static synthetic fun copy$default (Lapp/revanced/lib/signing/SigningOptions;Ljava/lang/String;Ljava/lang/String;Ljava/io/File;ILjava/lang/Object;)Lapp/revanced/lib/signing/SigningOptions; + public fun equals (Ljava/lang/Object;)Z + public final fun getCommonName ()Ljava/lang/String; + public final fun getKeyStoreOutputFilePath ()Ljava/io/File; + public final fun getPassword ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class app/revanced/lib/zip/ZipFile : java/io/Closeable { + public static final field ApkZipFile Lapp/revanced/lib/zip/ZipFile$ApkZipFile; + public fun (Ljava/io/File;)V + public final fun addEntryCompressData (Lapp/revanced/lib/zip/structures/ZipEntry;[B)V + public fun close ()V + public final fun copyEntriesFromFileAligned (Lapp/revanced/lib/zip/ZipFile;Lkotlin/jvm/functions/Function1;)V +} + +public final class app/revanced/lib/zip/ZipFile$ApkZipFile { + public final fun getApkZipEntryAlignment ()Lkotlin/jvm/functions/Function1; +} + +public final class app/revanced/lib/zip/structures/ZipEntry { + public static final field Companion Lapp/revanced/lib/zip/structures/ZipEntry$Companion; + public fun (Ljava/lang/String;)V +} + +public final class app/revanced/lib/zip/structures/ZipEntry$Companion { +} + diff --git a/revanced-lib/build.gradle.kts b/revanced-lib/build.gradle.kts new file mode 100644 index 00000000..ea0a0b0b --- /dev/null +++ b/revanced-lib/build.gradle.kts @@ -0,0 +1,78 @@ +plugins { + kotlin("jvm") version "1.9.0" + alias(libs.plugins.binary.compatibility.validator) + `maven-publish` +} + +dependencies { + implementation(libs.revanced.patcher) + implementation(libs.kotlin.reflect) + implementation(libs.jadb) // Updated fork + implementation(libs.apksig) + implementation(libs.bcpkix.jdk15on) + implementation(libs.jackson.module.kotlin) + + testImplementation(libs.revanced.patcher) + testImplementation(libs.kotlin.test) +} + +tasks { + test { + useJUnitPlatform() + testLogging { + events("PASSED", "SKIPPED", "FAILED") + } + } +} + +kotlin { jvmToolchain(11) } + +java { + withSourcesJar() +} + +publishing { + repositories { + mavenLocal() + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/revanced/revanced-cli") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } + publications { + create("gpr") { + from(components["java"]) + + version = project.version.toString() + + pom { + name = "ReVanced Library" + description = "Library containing common utilities for ReVanced" + url = "https://revanced.app" + + licenses { + license { + name = "GNU General Public License v3.0" + url = "https://www.gnu.org/licenses/gpl-3.0.en.html" + } + } + developers { + developer { + id = "ReVanced" + name = "ReVanced" + email = "contact@revanced.app" + } + } + scm { + connection = "scm:git:git://github.com/revanced/revanced-cli.git" + developerConnection = "scm:git:git@github.com:revanced/revanced-cli.git" + url = "https://github.com/revanced/revanced-cli" + } + } + } + } +} \ No newline at end of file diff --git a/revanced-lib/settings.gradle.kts b/revanced-lib/settings.gradle.kts new file mode 100644 index 00000000..f1bcd729 --- /dev/null +++ b/revanced-lib/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "revanced-lib" \ No newline at end of file diff --git a/revanced-lib/src/main/kotlin/app/revanced/lib/ApkUtils.kt b/revanced-lib/src/main/kotlin/app/revanced/lib/ApkUtils.kt new file mode 100644 index 00000000..9a8ebcc4 --- /dev/null +++ b/revanced-lib/src/main/kotlin/app/revanced/lib/ApkUtils.kt @@ -0,0 +1,67 @@ +package app.revanced.lib + +import app.revanced.lib.signing.ApkSigner +import app.revanced.lib.signing.SigningOptions +import app.revanced.lib.zip.ZipFile +import app.revanced.lib.zip.structures.ZipEntry +import app.revanced.patcher.PatcherResult +import java.io.File +import java.util.logging.Logger +import kotlin.io.path.deleteIfExists + +@Suppress("MemberVisibilityCanBePrivate", "unused") +object ApkUtils { + private val logger = Logger.getLogger(ApkUtils::class.java.name) + + /** + * Creates a new apk from [apkFile] and [patchedEntriesSource] and writes it to [outputFile]. + * + * @param apkFile The apk to copy entries from. + * @param outputFile The apk to write the new entries to. + * @param patchedEntriesSource The result of the patcher to add the patched dex files and resources. + */ + fun copyAligned(apkFile: File, outputFile: File, patchedEntriesSource: PatcherResult) { + logger.info("Aligning ${apkFile.name}") + + outputFile.toPath().deleteIfExists() + + ZipFile(outputFile).use { file -> + patchedEntriesSource.dexFiles.forEach { + file.addEntryCompressData( + ZipEntry(it.name), it.stream.readBytes() + ) + } + + patchedEntriesSource.resourceFile?.let { + file.copyEntriesFromFileAligned( + ZipFile(it), ZipFile.apkZipEntryAlignment + ) + } + + // TODO: Do not compress result.doNotCompress + + // TODO: Fix copying resources that are not needed anymore. + file.copyEntriesFromFileAligned( + ZipFile(apkFile), ZipFile.apkZipEntryAlignment + ) + } + } + + + /** + * Signs the apk at [apk] and writes it to [output]. + * + * @param apk The apk to sign. + * @param output The apk to write the signed apk to. + * @param signingOptions The options to use for signing. + */ + fun sign( + apk: File, + output: File, + signingOptions: SigningOptions, + ) { + logger.info("Signing ${apk.name}") + + ApkSigner(signingOptions).signApk(apk, output) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/utils/Options.kt b/revanced-lib/src/main/kotlin/app/revanced/lib/Options.kt similarity index 85% rename from src/main/kotlin/app/revanced/utils/Options.kt rename to revanced-lib/src/main/kotlin/app/revanced/lib/Options.kt index 7891d4ab..3bef8529 100644 --- a/src/main/kotlin/app/revanced/utils/Options.kt +++ b/revanced-lib/src/main/kotlin/app/revanced/lib/Options.kt @@ -1,15 +1,19 @@ -package app.revanced.utils +@file:Suppress("MemberVisibilityCanBePrivate") +package app.revanced.lib + +import app.revanced.lib.Options.Patch.Option +import app.revanced.patcher.PatchClass import app.revanced.patcher.PatchSet import app.revanced.patcher.patch.options.PatchOptionException -import app.revanced.utils.Options.PatchOption.Option import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import java.io.File import java.util.logging.Logger +private typealias PatchList = List -internal object Options { +object Options { private val logger = Logger.getLogger(Options::class.java.name) private var mapper = jacksonObjectMapper() @@ -24,7 +28,7 @@ internal object Options { fun serialize(patches: PatchSet, prettyPrint: Boolean = false): String = patches .filter { it.options.any() } .map { patch -> - PatchOption( + Patch( patch.name!!, patch.options.values.map { option -> Option(option.key, option.value) } ) @@ -42,12 +46,11 @@ internal object Options { * Deserializes the options for the patches in the list. * * @param json The JSON string containing the options. - * @return The list of [PatchOption]s. - * @see PatchOption + * @return The list of [Patch]s. + * @see Patch * @see PatchList */ - @Suppress("MemberVisibilityCanBePrivate") - fun deserialize(json: String): Array = mapper.readValue(json, Array::class.java) + fun deserialize(json: String): Array = mapper.readValue(json, Array::class.java) /** * Sets the options for the patches in the list. @@ -88,7 +91,7 @@ internal object Options { * @property patchName The name of the patch. * @property options The [Option]s for the patch. */ - internal data class PatchOption( + class Patch internal constructor( val patchName: String, val options: List