Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/main/kotlin/app/morphe/cli/command/MainCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) {
Logger.setDefault()
CommandLine(MainCommand).execute(*args).let(System::exit)
val exitCode = CommandLine(MainCommand).execute(*args)
exitProcess(exitCode)
}

private object CLIVersionProvider : IVersionProvider {
Expand Down Expand Up @@ -37,6 +39,6 @@ private object CLIVersionProvider : IVersionProvider {
ListPatchesCommand::class,
ListCompatibleVersions::class,
UtilityCommand::class,
],
]
)
private object MainCommand
110 changes: 72 additions & 38 deletions src/main/kotlin/app/morphe/cli/command/PatchCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,19 @@ 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)
@CommandLine.Command(
name = "patch",
description = ["Patch an APK file."],
)
internal object PatchCommand : Runnable {
internal object PatchCommand : Callable<Int> {

private const val EXIT_CODE_SUCCESS = 0
private const val EXIT_CODE_ERROR = 1

private val logger = Logger.getLogger(this::class.java.name)

@Spec
Expand Down Expand Up @@ -264,7 +269,14 @@ internal object PatchCommand : Runnable {
)
private var striplibs: List<String> = 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 =
Expand All @@ -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. " +
Expand All @@ -303,49 +315,49 @@ internal object PatchCommand : Runnable {
)
}

return
return EXIT_CODE_ERROR
}
} else {
null
}

// 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,
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -478,26 +497,39 @@ 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 ->
Json.encodeToStream(patchingResult, outputStream)
}
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
}

/**
Expand Down Expand Up @@ -578,3 +610,5 @@ internal object PatchCommand : Runnable {
logger.info(result)
}
}

private class PatchFailedException(message: String, cause: Throwable) : Exception(message, cause)
Loading