@@ -28,14 +28,19 @@ import picocli.CommandLine.Spec
2828import java.io.File
2929import java.io.PrintWriter
3030import java.io.StringWriter
31+ import java.util.concurrent.Callable
3132import 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