diff --git a/src/main/kotlin/app/morphe/cli/command/MainCommand.kt b/src/main/kotlin/app/morphe/cli/command/MainCommand.kt index d8eff33..01f481c 100644 --- a/src/main/kotlin/app/morphe/cli/command/MainCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/MainCommand.kt @@ -5,11 +5,13 @@ import app.morphe.library.logging.Logger import picocli.CommandLine import picocli.CommandLine.Command import picocli.CommandLine.IVersionProvider -import java.util.* +import java.util.Properties +import kotlin.system.exitProcess fun main(args: Array) { Logger.setDefault() - CommandLine(MainCommand).execute(*args).let(System::exit) + val exitCode = CommandLine(MainCommand).execute(*args) + exitProcess(exitCode) } private object CLIVersionProvider : IVersionProvider { @@ -37,6 +39,6 @@ private object CLIVersionProvider : IVersionProvider { ListPatchesCommand::class, ListCompatibleVersions::class, UtilityCommand::class, - ], + ] ) private object MainCommand diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index bf88cd4..2987f03 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -28,6 +28,7 @@ import picocli.CommandLine.Spec import java.io.File import java.io.PrintWriter import java.io.StringWriter +import java.util.concurrent.Callable import java.util.logging.Logger @OptIn(ExperimentalSerializationApi::class) @@ -35,7 +36,11 @@ import java.util.logging.Logger name = "patch", description = ["Patch an APK file."], ) -internal object PatchCommand : Runnable { +internal object PatchCommand : Callable { + + private const val EXIT_CODE_SUCCESS = 0 + private const val EXIT_CODE_ERROR = 1 + private val logger = Logger.getLogger(this::class.java.name) @Spec @@ -264,7 +269,14 @@ internal object PatchCommand : Runnable { ) private var striplibs: List = emptyList() - override fun run() { + @CommandLine.Option( + names = ["--continue-on-error"], + description = ["Continue patching even if a patch fails. By default, patching stops on the first error."], + showDefaultValue = ALWAYS, + ) + private var continueOnError: Boolean = false + + override fun call(): Int { // region Setup val outputFilePath = @@ -290,7 +302,7 @@ internal object PatchCommand : Runnable { } else { AdbInstaller(deviceSerial) } - } catch (e: DeviceNotFoundException) { + } catch (_: DeviceNotFoundException) { if (deviceSerial?.isNotEmpty() == true) { logger.severe( "Device with serial $deviceSerial not found to install to. " + @@ -303,7 +315,7 @@ internal object PatchCommand : Runnable { ) } - return + return EXIT_CODE_ERROR } } else { null @@ -311,41 +323,41 @@ internal object PatchCommand : Runnable { // endregion - // region Load patches + val patchingResult = PatchingResult() + var mergedApkToCleanup: File? = null - logger.info("Loading patches") + try { + // region Load patches - val patches = loadPatchesFromJar(patchesFiles) + logger.info("Loading patches") - // endregion + val patches = loadPatchesFromJar(patchesFiles) - val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher") + // endregion - // Checking if the file is in apkm format (like reddit) - var mergedApkToCleanup: File? = null - val inputApk = if (apk.extension.equals("apkm", ignoreCase = true)) { - logger.info("Merging APKM bundle") + val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher") - // Save merged APK to output directory (will be cleaned up after patching) - val outputApk = outputFilePath.parentFile.resolve("${apk.nameWithoutExtension}-merged.apk") + // Checking if the file is in apkm format (like reddit) + val inputApk = if (apk.extension.equals("apkm", ignoreCase = true)) { + logger.info("Merging APKM bundle") - // Use APKEditor's Merger directly (handles extraction and merging) - val mergerOptions = MergerOptions().apply { - inputFile = apk // Original APKM file - outputFile = outputApk - cleanMeta = true - } - Merger(mergerOptions).run() + // Save merged APK to output directory (will be cleaned up after patching) + val outputApk = outputFilePath.parentFile.resolve("${apk.nameWithoutExtension}-merged.apk") - mergedApkToCleanup = outputApk - outputApk - } else { - apk - } + // Use APKEditor's Merger directly (handles extraction and merging) + val mergerOptions = MergerOptions().apply { + inputFile = apk // Original APKM file + outputFile = outputApk + cleanMeta = true + } + Merger(mergerOptions).run() - val patchingResult = PatchingResult() + mergedApkToCleanup = outputApk + outputApk + } else { + apk + } - try { val (packageName, patcherResult) = Patcher( PatcherConfig( inputApk, @@ -393,6 +405,13 @@ internal object PatchCommand : Runnable { ) ) patchingResult.success = false + + if (!continueOnError) { + throw PatchFailedException( + "\"${patchResult.patch}\" failed", + exception + ) + } } } ?: patchResult.patch.let { patchingResult.appliedPatches.add(patchResult.patch.toSerializablePatch()) @@ -478,6 +497,17 @@ internal object PatchCommand : Runnable { } // endregion + } catch (e: PatchFailedException) { + logger.severe("Patching aborted: ${e.message}") + logger.info( + "Use --continue-on-error to skip failed patches and continue patching" + ) + return EXIT_CODE_ERROR + } catch (e: Exception) { + // Should never happen. + logger.severe("An unexpected error occurred: ${e.message}") + e.printStackTrace() + return EXIT_CODE_ERROR } finally { patchingResultOutputFilePath?.let { outputFile -> outputFile.outputStream().use { outputStream -> @@ -485,19 +515,21 @@ internal object PatchCommand : Runnable { } logger.info("Patching result saved to $outputFile") } - } - if (purge) { - logger.info("Purging temporary files") - purge(temporaryFilesPath) - } + if (purge) { + logger.info("Purging temporary files") + purge(temporaryFilesPath) + } - // Clean up merged APK if we created one from APKM - mergedApkToCleanup?.let { - if (!it.delete()) { - logger.warning("Could not clean up merged APK: ${it.path}") + // Clean up merged APK if we created one from APKM + mergedApkToCleanup?.let { + if (!it.delete()) { + logger.warning("Could not clean up merged APK: ${it.path}") + } } } + + return EXIT_CODE_SUCCESS } /** @@ -578,3 +610,5 @@ internal object PatchCommand : Runnable { logger.info(result) } } + +private class PatchFailedException(message: String, cause: Throwable) : Exception(message, cause)