Skip to content

Commit a48af1a

Browse files
committed
Another round of potential fixes
1 parent 72db909 commit a48af1a

3 files changed

Lines changed: 191 additions & 118 deletions

File tree

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

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package app.morphe.cli.command
22

3-
import app.morphe.cli.command.model.PatchOptionsFile
4-
import app.morphe.cli.command.model.mergeWithOptionsFile
5-
import app.morphe.cli.command.model.toPatchOptionsFile
3+
import app.morphe.cli.command.model.PatchBundle
4+
import app.morphe.cli.command.model.findMatchingBundle
5+
import app.morphe.cli.command.model.mergeWithBundle
6+
import app.morphe.cli.command.model.withUpdatedBundle
67
import app.morphe.patcher.patch.loadPatchesFromJar
78
import kotlinx.serialization.json.Json
89
import picocli.CommandLine
@@ -69,37 +70,40 @@ internal object OptionsCommand : Callable<Int> {
6970
}.toSet()
7071
} ?: patches
7172

72-
val sourcePatchesName = patchesFiles.joinToString(", ") { it.name }
73-
74-
// Merge with existing file if it exists, otherwise create fresh
75-
val existing = if (outputFile.exists()) {
73+
// Read existing bundles list if the file already exists
74+
val existingBundles: List<PatchBundle>? = if (outputFile.exists()) {
7675
try {
77-
Json.decodeFromString<PatchOptionsFile>(outputFile.readText())
76+
Json.decodeFromString<List<PatchBundle>>(outputFile.readText())
7877
} catch (e: Exception) {
7978
logger.warning("Could not parse existing file, creating fresh: ${e.message}")
8079
null
8180
}
8281
} else null
8382

84-
val patchOptionsFile = filtered.mergeWithOptionsFile(
85-
existing = existing,
86-
sourcePatches = sourcePatchesName,
83+
// Find the bundle matching the current .mpp file(s), merge with it (or create fresh)
84+
val existingBundle = existingBundles?.findMatchingBundle(patchesFiles)
85+
val updatedBundle = filtered.mergeWithBundle(
86+
existing = existingBundle,
87+
sourceFiles = patchesFiles,
8788
)
88-
val jsonString = json.encodeToString(patchOptionsFile)
89+
90+
// Replace the matching entry in the list (or start a new list)
91+
val updatedBundles = existingBundles?.withUpdatedBundle(updatedBundle)
92+
?: listOf(updatedBundle)
8993

9094
outputFile.absoluteFile.parentFile?.mkdirs()
91-
outputFile.writeText(jsonString)
95+
outputFile.writeText(json.encodeToString(updatedBundles))
9296

93-
if (existing != null) {
94-
val existingNames = existing.patches.keys.map { it.lowercase() }.toSet()
95-
val newNames = patchOptionsFile.patches.keys.map { it.lowercase() }.toSet()
97+
if (existingBundle != null) {
98+
val existingNames = existingBundle.patches.keys.map { it.lowercase() }.toSet()
99+
val newNames = updatedBundle.patches.keys.map { it.lowercase() }.toSet()
96100
val added = newNames - existingNames
97101
val removed = existingNames - newNames
98102
val kept = newNames.intersect(existingNames)
99-
logger.info("Updated existing options file at ${outputFile.path}")
103+
logger.info("Updated bundle in options file at ${outputFile.path}")
100104
logger.info(" ${kept.size} patch(es) preserved, ${added.size} added, ${removed.size} removed")
101105
} else {
102-
logger.info("Created new options file at ${outputFile.path} with ${patchOptionsFile.patches.size} patches")
106+
logger.info("Created new bundle in options file at ${outputFile.path} with ${updatedBundle.patches.size} patches")
103107
}
104108

105109
EXIT_CODE_SUCCESS

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

Lines changed: 62 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
package app.morphe.cli.command
22

33
import app.morphe.cli.command.model.FailedPatch
4-
import app.morphe.cli.command.model.PatchEntry
5-
import app.morphe.cli.command.model.PatchOptionsFile
4+
import app.morphe.cli.command.model.PatchBundle
65
import app.morphe.cli.command.model.PatchingResult
76
import app.morphe.cli.command.model.PatchingStep
87
import app.morphe.cli.command.model.addStepResult
9-
import app.morphe.cli.command.model.PatchSerializer
108
import app.morphe.cli.command.model.deserializeOptionValue
9+
import app.morphe.cli.command.model.findMatchingBundle
1110
import app.morphe.cli.command.model.mergeWith
12-
import app.morphe.cli.command.model.mergeWithOptionsFile
13-
import app.morphe.cli.command.model.toPatchOptionsFile
11+
import app.morphe.cli.command.model.toPatchBundle
1412
import app.morphe.cli.command.model.toSerializablePatch
13+
import app.morphe.cli.command.model.withUpdatedBundle
1514
import app.morphe.gui.util.ApkLibraryStripper
1615
import app.morphe.library.ApkUtils
1716
import app.morphe.library.ApkUtils.applyTo
@@ -352,45 +351,43 @@ internal object PatchCommand : Callable<Int> {
352351
var mergedApkToCleanup: File? = null
353352

354353
// Lightweight snapshot of patch metadata for use in finally block (auto-update).
354+
// Lightweight snapshot of current bundle metadata for use in finally block (auto-update).
355355
// The heavy Patch objects hold DEX classloaders and must not leak into finally.
356-
var patchesSnapshot: PatchOptionsFile? = null
356+
var patchesSnapshot: PatchBundle? = null
357357

358358
try {
359359
logger.info("Loading patches")
360-
val patches = loadPatchesFromJar(patchesFiles)
361-
patchesSnapshot = patches.toPatchOptionsFile(
362-
sourcePatches = patchesFiles.joinToString(", ") { it.name }
363-
)
360+
val patches: MutableSet<Patch<*>> = loadPatchesFromJar(patchesFiles).toMutableSet()
361+
patchesSnapshot = patches.toPatchBundle(sourceFiles = patchesFiles)
364362

365363
// region Parse options JSON
366364

367-
val patchOptionsFile = optionsFilePath?.let { file ->
365+
val patchOptionsBundle: PatchBundle? = optionsFilePath?.let { file ->
368366
if (file.exists()) {
369367
logger.info("Reading options from ${file.path}")
370-
Json.decodeFromString<PatchOptionsFile>(file.readText())
368+
val bundles = Json.decodeFromString<List<PatchBundle>>(file.readText())
369+
bundles.findMatchingBundle(patchesFiles)
371370
} else {
372371
logger.info("Options file ${file.path} does not exist, generating with defaults")
373-
val generated = patches.toPatchOptionsFile(
374-
sourcePatches = patchesFiles.joinToString(", ") { it.name }
375-
)
372+
val bundle = patches.toPatchBundle(sourceFiles = patchesFiles)
376373
val json = Json { prettyPrint = true }
377374
file.absoluteFile.parentFile?.mkdirs()
378-
file.writeText(json.encodeToString(generated))
375+
file.writeText(json.encodeToString(listOf(bundle)))
379376
logger.info("Generated options file at ${file.path}")
380-
generated
377+
bundle
381378
}
382379
}
383380

384381
// Build enable/disable sets from JSON (lowercase for case-insensitive matching)
385-
val jsonEnabledPatches = patchOptionsFile?.patches
382+
val jsonEnabledPatches = patchOptionsBundle?.patches
386383
?.filter { (_, entry) -> entry.enabled }
387384
?.keys?.map { it.lowercase() }?.toSet() ?: emptySet()
388-
val jsonDisabledPatches = patchOptionsFile?.patches
385+
val jsonDisabledPatches = patchOptionsBundle?.patches
389386
?.filter { (_, entry) -> !entry.enabled }
390387
?.keys?.map { it.lowercase() }?.toSet() ?: emptySet()
391388

392389
// Build options map from JSON, deserializing values using each patch's option types
393-
val jsonOptionsMap: Map<String, Map<String, Any?>> = patchOptionsFile?.patches
390+
val jsonOptionsMap: Map<String, Map<String, Any?>> = patchOptionsBundle?.patches
394391
?.mapNotNull { (patchName, entry) ->
395392
if (entry.options.isEmpty()) return@mapNotNull null
396393
val patch = patches.firstOrNull { it.name.equals(patchName, ignoreCase = true) }
@@ -439,7 +436,7 @@ internal object PatchCommand : Callable<Int> {
439436
inputApk,
440437
patcherTemporaryFilesPath,
441438
aaptBinaryPath?.path,
442-
patcherTemporaryFilesPath.absolutePath,
439+
patcherTemporaryFilesPath.absolutePath
443440
),
444441
).use { patcher ->
445442
val packageName = patcher.context.packageMetadata.packageName
@@ -449,15 +446,15 @@ internal object PatchCommand : Callable<Int> {
449446
patchingResult.packageVersion = packageVersion
450447

451448
// Warn if options file is out of date (only for patches compatible with this app)
452-
if (patchOptionsFile != null && optionsFilePath?.exists() == true && !updateOptions) {
449+
if (patchOptionsBundle != null && optionsFilePath?.exists() == true && !updateOptions) {
453450
val compatiblePatchNames = patches
454451
.filter { patch ->
455452
patch.compatiblePackages == null ||
456453
patch.compatiblePackages!!.any { (name, _) -> name == packageName }
457454
}
458455
.mapNotNull { it.name?.lowercase() }
459456
.toSet()
460-
val jsonPatchNames = patchOptionsFile.patches.keys.map { it.lowercase() }.toSet()
457+
val jsonPatchNames = patchOptionsBundle.patches.keys.map { it.lowercase() }.toSet()
461458

462459
val newPatches = compatiblePatchNames - jsonPatchNames
463460
val removedPatches = jsonPatchNames - compatiblePatchNames
@@ -466,7 +463,7 @@ internal object PatchCommand : Callable<Int> {
466463
var patchesWithNewOptions = 0
467464
for ((patchName, snapshotEntry) in patchesSnapshot.patches) {
468465
if (patchName.lowercase() !in compatiblePatchNames) continue
469-
val jsonEntry = patchOptionsFile.patches.entries
466+
val jsonEntry = patchOptionsBundle.patches.entries
470467
.firstOrNull { it.key.equals(patchName, ignoreCase = true) }?.value
471468
?: continue
472469
val newOptionKeys = snapshotEntry.options.keys - jsonEntry.options.keys
@@ -488,46 +485,44 @@ internal object PatchCommand : Callable<Int> {
488485
}
489486
}
490487

491-
val filteredPatches = patches.filterPatchSelection(
488+
logger.info("Setting patch options")
489+
490+
// Scope filteredPatches inside let so it goes out of scope immediately after
491+
// patcher += filteredPatches, matching the pattern from PR #54.
492+
patches.filterPatchSelection(
492493
packageName,
493494
packageVersion,
494495
jsonEnabledPatches,
495496
jsonDisabledPatches,
496-
)
497+
).let { filteredPatches ->
498+
val patchesList = patches.toList()
499+
val cliOptionsMap = selection.filter { it.enabled != null }.associate {
500+
val enabledSelection = it.enabled!!
497501

498-
logger.info("Setting patch options")
499-
500-
// Build CLI options map (CLI flags take precedence over JSON)
501-
val patchesList = patches.toList()
502-
val cliOptionsMap = selection.filter { it.enabled != null }.associate {
503-
val enabledSelection = it.enabled!!
504-
505-
val resolvedName = enabledSelection.selector.name?.let { userInput ->
506-
patchesList.firstOrNull { it.name.equals(userInput, ignoreCase = true) }?.name ?: userInput
507-
} ?: patchesList[enabledSelection.selector.index!!].name!!
502+
val resolvedName = enabledSelection.selector.name?.let { userInput ->
503+
patchesList.firstOrNull { it.name.equals(userInput, ignoreCase = true) }?.name ?: userInput
504+
} ?: patchesList[enabledSelection.selector.index!!].name!!
508505

509-
resolvedName to enabledSelection.options
510-
}
506+
resolvedName to enabledSelection.options
507+
}
511508

512-
// Merge: JSON options as base, CLI options override
513-
val mergedOptionsMap = (jsonOptionsMap.keys + cliOptionsMap.keys).associateWith { patchName ->
514-
val jsonOpts = jsonOptionsMap[patchName] ?: emptyMap()
515-
val cliOpts = cliOptionsMap[patchName] ?: emptyMap()
509+
(jsonOptionsMap.keys + cliOptionsMap.keys).associateWith { patchName ->
510+
val jsonOpts = jsonOptionsMap[patchName] ?: emptyMap()
511+
val cliOpts = cliOptionsMap[patchName] ?: emptyMap()
516512

517-
// Log when CLI options override JSON values
518-
for ((key, cliValue) in cliOpts) {
519-
val jsonValue = jsonOpts[key]
520-
if (jsonValue != null && jsonValue != cliValue) {
521-
logger.info("CLI option overrides JSON for \"$patchName\" -> \"$key\": $jsonValue -> $cliValue")
513+
// Log when CLI options override JSON values
514+
for ((key, cliValue) in cliOpts) {
515+
val jsonValue = jsonOpts[key]
516+
if (jsonValue != null && jsonValue != cliValue) {
517+
logger.info("CLI option overrides JSON for \"$patchName\" -> \"$key\": $jsonValue -> $cliValue")
518+
}
522519
}
523-
}
524520

525-
jsonOpts + cliOpts // CLI entries override JSON entries for same key
526-
}
521+
jsonOpts + cliOpts // CLI entries override JSON entries for same key
522+
}.let(filteredPatches::setOptions)
527523

528-
mergedOptionsMap.let(filteredPatches::setOptions)
529-
530-
patcher += filteredPatches
524+
patcher += filteredPatches
525+
} // filteredPatches and patchesList go out of scope here
531526

532527
// Execute patches.
533528
patchingResult.addStepResult(
@@ -565,6 +560,12 @@ internal object PatchCommand : Callable<Int> {
565560
}
566561
)
567562

563+
// patches lives in the outer try scope (needed for patchesSnapshot and options
564+
// file generation before the Patcher block). Clear it explicitly now — after
565+
// patcher() finishes and before patcher.get() — so the JVM can GC the DEX
566+
// classloaders before the most memory-intensive step.
567+
patches.clear()
568+
568569
patcher.context.packageMetadata.packageName to patcher.get()
569570
}
570571

@@ -663,20 +664,17 @@ internal object PatchCommand : Callable<Int> {
663664
val snapshot = patchesSnapshot
664665
if (optionsFilePath != null && updateOptions && snapshot != null) {
665666
try {
666-
val currentOptionsFile = optionsFilePath!!.let { file ->
667+
val existingBundles = optionsFilePath!!.let { file ->
667668
if (file.exists()) {
668-
Json.decodeFromString<PatchOptionsFile>(file.readText())
669-
} else {
670-
null
671-
}
669+
try { Json.decodeFromString<List<PatchBundle>>(file.readText()) }
670+
catch (e: Exception) { emptyList() }
671+
} else emptyList()
672672
}
673-
674-
val updatedFile = snapshot.mergeWith(
675-
existing = currentOptionsFile,
676-
sourcePatches = patchesFiles.joinToString(", ") { it.name },
677-
)
673+
val existingBundle = existingBundles.findMatchingBundle(patchesFiles)
674+
val updatedBundle = snapshot.mergeWith(existingBundle)
675+
val updatedBundles = existingBundles.withUpdatedBundle(updatedBundle)
678676
val json = Json { prettyPrint = true }
679-
optionsFilePath!!.writeText(json.encodeToString(updatedFile))
677+
optionsFilePath!!.writeText(json.encodeToString(updatedBundles))
680678
logger.info("Updated options file ${optionsFilePath!!.path}")
681679
} catch (e: Exception) {
682680
logger.warning("Failed to update options file: ${e.message}")

0 commit comments

Comments
 (0)