Skip to content

Commit 255646b

Browse files
feat: Add --continue-on-error argument, return non zero exit code if patching fails (MorpheApp#47)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
1 parent 82ac1b5 commit 255646b

2 files changed

Lines changed: 77 additions & 41 deletions

File tree

src/main/kotlin/app/morphe/cli/command/MainCommand.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import app.morphe.library.logging.Logger
55
import picocli.CommandLine
66
import picocli.CommandLine.Command
77
import picocli.CommandLine.IVersionProvider
8-
import java.util.*
8+
import java.util.Properties
9+
import kotlin.system.exitProcess
910

1011
fun main(args: Array<String>) {
1112
Logger.setDefault()
12-
CommandLine(MainCommand).execute(*args).let(System::exit)
13+
val exitCode = CommandLine(MainCommand).execute(*args)
14+
exitProcess(exitCode)
1315
}
1416

1517
private object CLIVersionProvider : IVersionProvider {
@@ -37,6 +39,6 @@ private object CLIVersionProvider : IVersionProvider {
3739
ListPatchesCommand::class,
3840
ListCompatibleVersions::class,
3941
UtilityCommand::class,
40-
],
42+
]
4143
)
4244
private object MainCommand

src/main/kotlin/app/morphe/cli/command/PatchCommand.kt

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,19 @@ import picocli.CommandLine.Spec
2828
import java.io.File
2929
import java.io.PrintWriter
3030
import java.io.StringWriter
31+
import java.util.concurrent.Callable
3132
import java.util.logging.Logger
3233

3334
@OptIn(ExperimentalSerializationApi::class)
3435
@CommandLine.Command(
3536
name = "patch",
3637
description = ["Patch an APK file."],
3738
)
38-
internal object PatchCommand : Runnable {
39+
internal object PatchCommand : Callable<Int> {
40+
41+
private const val EXIT_CODE_SUCCESS = 0
42+
private const val EXIT_CODE_ERROR = 1
43+
3944
private val logger = Logger.getLogger(this::class.java.name)
4045

4146
@Spec
@@ -264,7 +269,14 @@ internal object PatchCommand : Runnable {
264269
)
265270
private var striplibs: List<String> = emptyList()
266271

267-
override fun run() {
272+
@CommandLine.Option(
273+
names = ["--continue-on-error"],
274+
description = ["Continue patching even if a patch fails. By default, patching stops on the first error."],
275+
showDefaultValue = ALWAYS,
276+
)
277+
private var continueOnError: Boolean = false
278+
279+
override fun call(): Int {
268280
// region Setup
269281

270282
val outputFilePath =
@@ -290,7 +302,7 @@ internal object PatchCommand : Runnable {
290302
} else {
291303
AdbInstaller(deviceSerial)
292304
}
293-
} catch (e: DeviceNotFoundException) {
305+
} catch (_: DeviceNotFoundException) {
294306
if (deviceSerial?.isNotEmpty() == true) {
295307
logger.severe(
296308
"Device with serial $deviceSerial not found to install to. " +
@@ -303,49 +315,49 @@ internal object PatchCommand : Runnable {
303315
)
304316
}
305317

306-
return
318+
return EXIT_CODE_ERROR
307319
}
308320
} else {
309321
null
310322
}
311323

312324
// endregion
313325

314-
// region Load patches
326+
val patchingResult = PatchingResult()
327+
var mergedApkToCleanup: File? = null
315328

316-
logger.info("Loading patches")
329+
try {
330+
// region Load patches
317331

318-
val patches = loadPatchesFromJar(patchesFiles)
332+
logger.info("Loading patches")
319333

320-
// endregion
334+
val patches = loadPatchesFromJar(patchesFiles)
321335

322-
val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher")
336+
// endregion
323337

324-
// Checking if the file is in apkm format (like reddit)
325-
var mergedApkToCleanup: File? = null
326-
val inputApk = if (apk.extension.equals("apkm", ignoreCase = true)) {
327-
logger.info("Merging APKM bundle")
338+
val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher")
328339

329-
// Save merged APK to output directory (will be cleaned up after patching)
330-
val outputApk = outputFilePath.parentFile.resolve("${apk.nameWithoutExtension}-merged.apk")
340+
// Checking if the file is in apkm format (like reddit)
341+
val inputApk = if (apk.extension.equals("apkm", ignoreCase = true)) {
342+
logger.info("Merging APKM bundle")
331343

332-
// Use APKEditor's Merger directly (handles extraction and merging)
333-
val mergerOptions = MergerOptions().apply {
334-
inputFile = apk // Original APKM file
335-
outputFile = outputApk
336-
cleanMeta = true
337-
}
338-
Merger(mergerOptions).run()
344+
// Save merged APK to output directory (will be cleaned up after patching)
345+
val outputApk = outputFilePath.parentFile.resolve("${apk.nameWithoutExtension}-merged.apk")
339346

340-
mergedApkToCleanup = outputApk
341-
outputApk
342-
} else {
343-
apk
344-
}
347+
// Use APKEditor's Merger directly (handles extraction and merging)
348+
val mergerOptions = MergerOptions().apply {
349+
inputFile = apk // Original APKM file
350+
outputFile = outputApk
351+
cleanMeta = true
352+
}
353+
Merger(mergerOptions).run()
345354

346-
val patchingResult = PatchingResult()
355+
mergedApkToCleanup = outputApk
356+
outputApk
357+
} else {
358+
apk
359+
}
347360

348-
try {
349361
val (packageName, patcherResult) = Patcher(
350362
PatcherConfig(
351363
inputApk,
@@ -393,6 +405,13 @@ internal object PatchCommand : Runnable {
393405
)
394406
)
395407
patchingResult.success = false
408+
409+
if (!continueOnError) {
410+
throw PatchFailedException(
411+
"\"${patchResult.patch}\" failed",
412+
exception
413+
)
414+
}
396415
}
397416
} ?: patchResult.patch.let {
398417
patchingResult.appliedPatches.add(patchResult.patch.toSerializablePatch())
@@ -478,26 +497,39 @@ internal object PatchCommand : Runnable {
478497
}
479498

480499
// endregion
500+
} catch (e: PatchFailedException) {
501+
logger.severe("Patching aborted: ${e.message}")
502+
logger.info(
503+
"Use --continue-on-error to skip failed patches and continue patching"
504+
)
505+
return EXIT_CODE_ERROR
506+
} catch (e: Exception) {
507+
// Should never happen.
508+
logger.severe("An unexpected error occurred: ${e.message}")
509+
e.printStackTrace()
510+
return EXIT_CODE_ERROR
481511
} finally {
482512
patchingResultOutputFilePath?.let { outputFile ->
483513
outputFile.outputStream().use { outputStream ->
484514
Json.encodeToStream(patchingResult, outputStream)
485515
}
486516
logger.info("Patching result saved to $outputFile")
487517
}
488-
}
489518

490-
if (purge) {
491-
logger.info("Purging temporary files")
492-
purge(temporaryFilesPath)
493-
}
519+
if (purge) {
520+
logger.info("Purging temporary files")
521+
purge(temporaryFilesPath)
522+
}
494523

495-
// Clean up merged APK if we created one from APKM
496-
mergedApkToCleanup?.let {
497-
if (!it.delete()) {
498-
logger.warning("Could not clean up merged APK: ${it.path}")
524+
// Clean up merged APK if we created one from APKM
525+
mergedApkToCleanup?.let {
526+
if (!it.delete()) {
527+
logger.warning("Could not clean up merged APK: ${it.path}")
528+
}
499529
}
500530
}
531+
532+
return EXIT_CODE_SUCCESS
501533
}
502534

503535
/**
@@ -578,3 +610,5 @@ internal object PatchCommand : Runnable {
578610
logger.info(result)
579611
}
580612
}
613+
614+
private class PatchFailedException(message: String, cause: Throwable) : Exception(message, cause)

0 commit comments

Comments
 (0)