11package app.morphe.cli.command
22
3+ import app.morphe.cli.command.model.FailedPatch
4+ import app.morphe.cli.command.model.PatchingResult
5+ import app.morphe.cli.command.model.PatchingStep
6+ import app.morphe.cli.command.model.PatchingStepResult
7+ import app.morphe.cli.command.model.addStepResult
8+ import app.morphe.cli.command.model.toSerializablePatch
39import app.morphe.library.ApkUtils
410import app.morphe.library.ApkUtils.applyTo
511import app.morphe.library.installation.installer.*
@@ -9,6 +15,9 @@ import app.morphe.patcher.PatcherConfig
915import app.morphe.patcher.patch.Patch
1016import app.morphe.patcher.patch.loadPatchesFromJar
1117import kotlinx.coroutines.runBlocking
18+ import kotlinx.serialization.ExperimentalSerializationApi
19+ import kotlinx.serialization.json.Json
20+ import kotlinx.serialization.json.encodeToStream
1221import picocli.CommandLine
1322import picocli.CommandLine.ArgGroup
1423import picocli.CommandLine.Help.Visibility.ALWAYS
@@ -19,6 +28,7 @@ import java.io.PrintWriter
1928import java.io.StringWriter
2029import java.util.logging.Logger
2130
31+ @OptIn(ExperimentalSerializationApi ::class )
2232@CommandLine.Command (
2333 name = " patch" ,
2434 description = [" Patch an APK file." ],
@@ -115,6 +125,17 @@ internal object PatchCommand : Runnable {
115125 this .outputFilePath = outputFilePath?.absoluteFile
116126 }
117127
128+ private var patchingResultOutputFilePath: File ? = null
129+
130+ @CommandLine.Option (
131+ names = [" -r" , " --result-file" ],
132+ description = [" Path to save the patching result file to" ],
133+ )
134+ @Suppress(" unused" )
135+ private fun setPatchingResultOutputFilePath (outputFilePath : File ? ) {
136+ this .patchingResultOutputFilePath = outputFilePath?.absoluteFile
137+ }
138+
118139 @CommandLine.Option (
119140 names = [" -i" , " --install" ],
120141 description = [" Serial of the ADB device to install to. If not specified, the first connected device will be used." ],
@@ -233,7 +254,7 @@ internal object PatchCommand : Runnable {
233254 description = [" Disable signing of the final apk." ],
234255 )
235256 private var unsigned: Boolean = false
236-
257+
237258 override fun run () {
238259 // region Setup
239260
@@ -291,88 +312,138 @@ internal object PatchCommand : Runnable {
291312
292313 val patcherTemporaryFilesPath = temporaryFilesPath.resolve(" patcher" )
293314
294- val (packageName, patcherResult) = Patcher (
295- PatcherConfig (
296- apk,
297- patcherTemporaryFilesPath,
298- aaptBinaryPath?.path,
299- patcherTemporaryFilesPath.absolutePath,
300- ),
301- ).use { patcher ->
302- val packageName = patcher.context.packageMetadata.packageName
303- val packageVersion = patcher.context.packageMetadata.packageVersion
304-
305- val filteredPatches = patches.filterPatchSelection(packageName, packageVersion)
306-
307- logger.info(" Setting patch options" )
308-
309- val patchesList = patches.toList()
310- selection.filter { it.enabled != null }.associate {
311- val enabledSelection = it.enabled!!
312-
313- (enabledSelection.selector.name ? : patchesList[enabledSelection.selector.index!! ].name!! ) to
314- enabledSelection.options
315- }.let (filteredPatches::setOptions)
316-
317- patcher + = filteredPatches
315+ val patchingResult = PatchingResult ()
316+
317+ try {
318+ val (packageName, patcherResult) = Patcher (
319+ PatcherConfig (
320+ apk,
321+ patcherTemporaryFilesPath,
322+ aaptBinaryPath?.path,
323+ patcherTemporaryFilesPath.absolutePath,
324+ ),
325+ ).use { patcher ->
326+ val packageName = patcher.context.packageMetadata.packageName
327+ val packageVersion = patcher.context.packageMetadata.packageVersion
328+
329+ patchingResult.packageName = packageName
330+ patchingResult.packageVersion = packageVersion
331+
332+ val filteredPatches = patches.filterPatchSelection(packageName, packageVersion)
333+
334+ logger.info(" Setting patch options" )
335+
336+ val patchesList = patches.toList()
337+ selection.filter { it.enabled != null }.associate {
338+ val enabledSelection = it.enabled!!
339+
340+ (enabledSelection.selector.name ? : patchesList[enabledSelection.selector.index!! ].name!! ) to
341+ enabledSelection.options
342+ }.let (filteredPatches::setOptions)
343+
344+ patcher + = filteredPatches
345+
346+ // Execute patches.
347+ patchingResult.addStepResult(
348+ PatchingStep .PATCHING ,
349+ {
350+ runBlocking {
351+ patcher().collect { patchResult ->
352+ patchResult.exception?.let { exception ->
353+ StringWriter ().use { writer ->
354+ exception.printStackTrace(PrintWriter (writer))
355+
356+ logger.severe(" \" ${patchResult.patch} \" failed:\n $writer " )
357+
358+ patchingResult.failedPatches.add(
359+ FailedPatch (
360+ patchResult.patch.toSerializablePatch(),
361+ writer.toString()
362+ )
363+ )
364+ patchingResult.success = false
365+ }
366+ } ? : patchResult.patch.let {
367+ patchingResult.appliedPatches.add(patchResult.patch.toSerializablePatch())
368+ logger.info(" \" ${patchResult.patch} \" succeeded" )
369+ }
370+ }
371+ }
372+ }
373+ )
318374
319- // Execute patches.
320- runBlocking {
321- patcher().collect { patchResult ->
322- val exception = patchResult.exception
323- ? : return @collect logger.info(" \" ${patchResult.patch} \" succeeded" )
375+ patcher.context.packageMetadata.packageName to patcher.get()
376+ }
324377
325- StringWriter ().use { writer ->
326- exception.printStackTrace(PrintWriter (writer))
378+ // region Save.
327379
328- logger.severe(" \" ${patchResult.patch} \" failed:\n $writer " )
380+ apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true ).apply {
381+ patchingResult.addStepResult(
382+ PatchingStep .REBUILDING ,
383+ {
384+ patcherResult.applyTo(this )
329385 }
386+ )
387+ }.let { patchedApkFile ->
388+ if (! mount && ! unsigned) {
389+ patchingResult.addStepResult(
390+ PatchingStep .SIGNING ,
391+ {
392+ ApkUtils .signApk(
393+ patchedApkFile,
394+ outputFilePath,
395+ signer,
396+ ApkUtils .KeyStoreDetails (
397+ keystoreFilePath,
398+ keyStorePassword,
399+ keyStoreEntryAlias,
400+ keyStoreEntryPassword,
401+ ),
402+ )
403+ }
404+ )
405+ } else {
406+ patchedApkFile.copyTo(outputFilePath, overwrite = true )
330407 }
331408 }
332-
333- patcher.context.packageMetadata.packageName to patcher.get()
334- }
335-
336- // region Save.
337-
338- apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true ).apply {
339- patcherResult.applyTo(this )
340- }.let { patchedApkFile ->
341- if (! mount && ! unsigned) {
342- ApkUtils .signApk(
343- patchedApkFile,
344- outputFilePath,
345- signer,
346- ApkUtils .KeyStoreDetails (
347- keystoreFilePath,
348- keyStorePassword,
349- keyStoreEntryAlias,
350- keyStoreEntryPassword,
351- ),
409+ logger.info(" Saved to $outputFilePath " )
410+
411+ // endregion
412+
413+ // region Install.
414+
415+ deviceSerial?.let {
416+ patchingResult.addStepResult(
417+ PatchingStep .INSTALLING ,
418+ {
419+ runBlocking {
420+ val result = installer!! .install(Installer .Apk (outputFilePath, packageName))
421+ when (result) {
422+ RootInstallerResult .FAILURE -> {
423+ logger.severe(" Failed to mount the patched APK file" )
424+ throw IllegalStateException (" Failed to mount the patched APK file" )
425+ }
426+ is AdbInstallerResult .Failure -> {
427+ logger.severe(result.exception.toString())
428+ throw result.exception
429+ }
430+ else -> logger.info(" Installed the patched APK file" )
431+ }
432+ }
433+ }
352434 )
353- } else {
354- patchedApkFile.copyTo(outputFilePath, overwrite = true )
355435 }
356- }
357-
358- logger.info(" Saved to $outputFilePath " )
359-
360- // endregion
361436
362- // region Install.
363-
364- deviceSerial?.let {
365- runBlocking {
366- when (val result = installer!! .install(Installer .Apk (outputFilePath, packageName))) {
367- RootInstallerResult .FAILURE -> logger.severe(" Failed to mount the patched APK file" )
368- is AdbInstallerResult .Failure -> logger.severe(result.exception.toString())
369- else -> logger.info(" Installed the patched APK file" )
437+ // endregion
438+ } finally {
439+ patchingResultOutputFilePath?.let { outputFile ->
440+ outputFile.outputStream().use { outputStream ->
441+ Json .encodeToStream(patchingResult, outputStream)
370442 }
443+ logger.info(" Patching result saved to $outputFile " )
371444 }
372445 }
373446
374- // endregion
375-
376447 if (purge) {
377448 logger.info(" Purging temporary files" )
378449 purge(temporaryFilesPath)
0 commit comments