diff --git a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt index 00265405..ace9d1bd 100644 --- a/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/PatchCommand.kt @@ -3,8 +3,10 @@ package app.revanced.cli.command import app.revanced.cli.command.PatchesFileInput.Companion.loadPatches import app.revanced.library.ApkUtils import app.revanced.library.ApkUtils.applyTo +import app.revanced.library.ApkUtils.applyToSplits import app.revanced.library.installation.installer.* import app.revanced.library.setOptions +import app.revanced.patcher.Apk import app.revanced.patcher.patch.Patch import app.revanced.patcher.patcher import kotlinx.coroutines.runBlocking @@ -220,6 +222,26 @@ internal object PatchCommand : Callable { // region Resource compilation + @CommandLine.Option( + names = ["--splits"], + description = ["Paths to split APK files, keyed by split name (e.g. --splits split_config.arm64_v8a=split_arm64.apk)."], + ) + @Suppress("unused") + private fun setSplitApkFiles(splitApkFiles: Map) { + splitApkFiles.forEach { (splitName, splitFile) -> + if (!splitFile.exists()) { + throw CommandLine.ParameterException( + spec.commandLine(), + "Split APK file for $splitName does not exist: ${splitFile.path}", + ) + } + } + + this.splitApkFiles = splitApkFiles.toMutableMap() + } + + private var splitApkFiles = mutableMapOf() + @CommandLine.Option( names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with."], @@ -312,11 +334,23 @@ internal object PatchCommand : Callable { // endregion val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher") + val signingDetails = ApkUtils.KeyStoreDetails( + keystoreFilePath, + signing?.keystorePassword, + signing?.keystoreEntryAlias ?: "ReVanced Key", + signing?.keystoreEntryPassword ?: "", + ) + + val apkInput: Apk = if (splitApkFiles.isNotEmpty()) { + Apk.Split(apk, splitApkFiles) + } else { + Apk.Single(apk) + } lateinit var packageName: String val patch = patcher( - apk, + apkInput, patcherTemporaryFilesPath, aaptBinaryPath, patcherTemporaryFilesPath.absolutePath, @@ -353,6 +387,22 @@ internal object PatchCommand : Callable { // region Save. + val splitOutputFiles = + if (splitApkFiles.isNotEmpty()) { + val splitOutputDir = outputFilePath.parentFile.resolve("splits") + splitOutputDir.mkdirs() + + splitApkFiles.mapValues { (name, file) -> + file.copyTo(splitOutputDir.resolve(splitOutputFileName(name)), overwrite = true) + }.also { copiedSplits -> + if (patchesResult.splitResources.isNotEmpty()) { + patchesResult.applyToSplits(copiedSplits) + } + } + } else { + emptyMap() + } + apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).let { patchesResult.applyTo(it) @@ -361,12 +411,13 @@ internal object PatchCommand : Callable { it, outputFilePath, signing?.signer ?: "ReVanced", - ApkUtils.KeyStoreDetails( - keystoreFilePath, - signing?.keystorePassword, - signing?.keystoreEntryAlias ?: "ReVanced Key", - signing?.keystoreEntryPassword ?: "", - ), + signingDetails, + ) + + ApkUtils.signApks( + splitOutputFiles.values, + signing?.signer ?: "ReVanced", + signingDetails, ) } else { it.copyTo(outputFilePath, overwrite = true) @@ -375,6 +426,11 @@ internal object PatchCommand : Callable { logger.info("Saved to $outputFilePath") + if (splitOutputFiles.isNotEmpty()) { + val splitOutputDir = splitOutputFiles.values.first().parentFile + logger.info("Saved patched splits to $splitOutputDir") + } + // endregion // region Install. @@ -382,7 +438,7 @@ internal object PatchCommand : Callable { installation?.deviceSerial?.let { runBlocking { when (val result = - installer!!.install(Installer.Apk(outputFilePath, packageName))) { + installer!!.install(Installer.Apk(outputFilePath, packageName, splitOutputFiles))) { RootInstallerResult.FAILURE -> logger.severe("Failed to mount the patched APK file") is AdbInstallerResult.Failure -> logger.severe(result.exception.toString()) else -> logger.info("Installed the patched APK file") @@ -477,4 +533,7 @@ internal object PatchCommand : Callable { } logger.info(result) } + + private fun splitOutputFileName(splitName: String) = + if (splitName.endsWith(".apk")) splitName else "$splitName.apk" } diff --git a/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt b/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt index 7ecddeed..55d6b090 100644 --- a/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt +++ b/src/main/kotlin/app/revanced/cli/command/utility/InstallCommand.kt @@ -4,8 +4,10 @@ import app.revanced.library.installation.installer.* import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking +import picocli.CommandLine import picocli.CommandLine.* import java.io.File +import picocli.CommandLine.ParameterException import java.util.logging.Logger @Command( @@ -34,6 +36,26 @@ internal object InstallCommand : Runnable { ) private var packageName: String? = null + @Option( + names = ["--splits"], + description = ["Paths to split APK files, keyed by split name (e.g. --splits split_config.arm64_v8a=split_arm64.apk)."], + ) + @Suppress("unused") + private fun setSplitApkFiles(splitApkFiles: Map) { + splitApkFiles.forEach { (splitName, splitFile) -> + if (!splitFile.exists()) { + throw ParameterException( + CommandLine(this), + "Split APK file for $splitName does not exist: ${splitFile.path}", + ) + } + } + + this.splitApkFiles = splitApkFiles.toMutableMap() + } + + private var splitApkFiles = mutableMapOf() + override fun run() { suspend fun install(deviceSerial: String? = null) { val result = try { @@ -41,7 +63,7 @@ internal object InstallCommand : Runnable { AdbRootInstaller(deviceSerial) } else { AdbInstaller(deviceSerial) - }.install(Installer.Apk(apk, packageName)) + }.install(Installer.Apk(apk, packageName, splitApkFiles)) } catch (e: Exception) { logger.severe(e.toString()) }