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
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
# [1.14.0-dev.3](https://github.com/MorpheApp/morphe-desktop/compare/v1.14.0-dev.2...v1.14.0-dev.3) (2026-08-25)


### Features

* Add patch context `fileWorkspace` ([#264](https://github.com/MorpheApp/morphe-desktop/issues/264)) ([1766a64](https://github.com/MorpheApp/morphe-desktop/commit/1766a640620304f85474798f8db32693115b617a))

# [1.14.0-dev.2](https://github.com/MorpheApp/morphe-desktop/compare/v1.14.0-dev.1...v1.14.0-dev.2) (2026-08-24)


### Bug Fixes

* Update to latest Patcher ([491535c](https://github.com/MorpheApp/morphe-desktop/commit/491535c40d130a216a49f4a89262b0f94dd46616))

# [1.14.0-dev.1](https://github.com/MorpheApp/morphe-desktop/compare/v1.13.3-dev.1...v1.14.0-dev.1) (2026-08-24)


### Features

* Show version codes in `list-versions` and `list-patches` ([#262](https://github.com/MorpheApp/morphe-desktop/issues/262)) ([f19352a](https://github.com/MorpheApp/morphe-desktop/commit/f19352a982ae6287b99085835cc1772e36cee164))

## [1.13.3-dev.1](https://github.com/MorpheApp/morphe-desktop/compare/v1.13.2...v1.13.3-dev.1) (2026-08-23)


### Bug Fixes

* Update to latest Patcher ([b1bfa5c](https://github.com/MorpheApp/morphe-desktop/commit/b1bfa5ce690837154c5d81be5dd1bb033b19e971))

## [1.13.2](https://github.com/MorpheApp/morphe-desktop/compare/v1.13.1...v1.13.2) (2026-08-22)


Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
org.gradle.parallel = true
org.gradle.caching = true
kotlin.code.style = official
version = 1.13.2
version = 1.14.0-dev.3
# Compose Desktop (5 OS targets) + Kotlin/Compose codegen need a bit more heap than
# the Gradle/Kotlin defaults (which intermittently OOM with "GC overhead limit
# exceeded" during codegen), but not so much it swaps alongside the IDE. 2g each
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ kotlin = "2.4.10"
# CLI
picocli = "4.7.7"
arsclib = "a28c6fb2a7"
morphe-patcher = "1.10.0"
morphe-patcher = "1.11.0"
morphe-library = "1.4.0"
jadb = "1.2.3"

Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
4 changes: 2 additions & 2 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=9c0f7faeeb306cb14e4279a3e084ca6b596894089a0638e68a07c945a32c9e14
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
distributionSha256Sum=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/app/morphe/desktop/command/CommandUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import java.io.File

internal fun checkFileExistsOrIsUrl(files: Set<File>, spec: CommandSpec) : Set<File> {
files.firstOrNull {
!it.exists() && !it.toString().let {
it.startsWith("http:/") || it.startsWith("https:/")
!it.exists() && !it.toString().let { url ->
url.startsWith("http:/") || url.startsWith("https:/")
}
}?.let {
throw CommandLine.ParameterException(spec.commandLine(), "${it.name} can not be found")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package app.morphe.desktop.command
import app.morphe.desktop.command.CliHttpClient
import app.morphe.engine.VersionMap
import app.morphe.engine.mostCommonCompatibleVersions
import app.morphe.engine.versionCodesFor
import app.morphe.patcher.patch.loadPatchesFromJar
import picocli.CommandLine
import picocli.CommandLine.Command
Expand Down Expand Up @@ -71,24 +72,6 @@ internal class ListCompatibleVersions : Runnable {
private lateinit var spec: CommandSpec

override fun run() {
fun VersionMap.buildVersionsString(): String {
if (isEmpty()) return "Any"

fun buildPatchesCountString(count: Int) = if (count == 1) "1 patch" else "$count patches"

return entries.joinToString("\n") { (version, count) ->
"$version (${buildPatchesCountString(count)})"
}
}

fun buildString(entry: Map.Entry<String, VersionMap>) =
buildString {
val (name, versions) = entry
appendLine("Package name: $name")
appendLine("Most common compatible versions:")
appendLine(versions.buildVersionsString().prependIndent("\t"))
}

try {
patchesFiles = PatchFileResolver.resolve(
patchesFiles,
Expand All @@ -104,6 +87,31 @@ internal class ListCompatibleVersions : Runnable {

val patches = loadPatchesFromJar(patchesFiles)

fun getVersionCodesString(pkgName: String, versionName: String): String {
return patches.versionCodesFor(pkgName, versionName)?.let { codes ->
" [versionCodes: " + codes.entries.joinToString(", ") { "${it.key.name}=${it.value}" } + "]"
} ?: ""
}

fun VersionMap.buildVersionsString(packageName: String): String {
if (isEmpty()) return "Any"

fun buildPatchesCountString(count: Int) = if (count == 1) "1 patch" else "$count patches"

return entries.joinToString("\n") { (version, count) ->
val versionCodesStr = getVersionCodesString(packageName, version)
"$version$versionCodesStr (${buildPatchesCountString(count)})"
}
}

fun buildString(entry: Map.Entry<String, VersionMap>) =
buildString {
val (name, versions) = entry
appendLine("Package name: $name")
appendLine("Most common compatible versions:")
appendLine(versions.buildVersionsString(name).prependIndent("\t"))
}

patches.mostCommonCompatibleVersions(
packageNames,
countUnusedPatches,
Expand Down
27 changes: 22 additions & 5 deletions src/main/kotlin/app/morphe/desktop/command/ListPatchesCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package app.morphe.desktop.command
import app.morphe.desktop.command.CliHttpClient
import app.morphe.engine.compatibleVersionsForDisplay
import app.morphe.engine.isCompatibleWith
import app.morphe.engine.versionCodesFor
import app.morphe.patcher.patch.Patch
import app.morphe.patcher.patch.loadPatchesFromJar
import picocli.CommandLine
Expand Down Expand Up @@ -133,6 +134,12 @@ internal object ListPatchesCommand : Runnable {
append("\nType: $type")
}

fun getVersionCodesString(patch: Patch<*>, pkgName: String, versionName: String): String {
return patch.versionCodesFor(pkgName, versionName)?.let { codes ->
codes.entries.joinToString(", ") { "${it.key.name}=${it.value}" }
} ?: ""
}

fun IndexedValue<Patch<*>>.buildString() =
let { (index, patch) ->
buildString {
Expand Down Expand Up @@ -162,14 +169,24 @@ internal object ListPatchesCommand : Runnable {
buildString {
val displayName = name ?: "(universal)"
if (withVersions && versions.isNotEmpty()) {
appendLine("Package name: $displayName")
appendLine("Compatible versions:")
append(versions.joinToString("\n").prependIndent("\t"))
appendLine("\tPackage name: $displayName")
appendLine("\tCompatible versions:")
append(versions.joinToString("\n").prependIndent("\t\t"))

val codesList = versions.mapNotNull { v ->
val codes = getVersionCodesString(patch, displayName, v)
if (codes.isNotEmpty()) "$v: $codes" else null
}

if (codesList.isNotEmpty()) {
append("\n\tVersion codes:\n")
append(codesList.joinToString("\n").prependIndent("\t\t"))
}
} else {
append("Package name: $displayName")
append("\tPackage name: $displayName")
}
}
}.prependIndent("\t"),
},
)
}
}
Expand Down
126 changes: 56 additions & 70 deletions src/main/kotlin/app/morphe/desktop/command/PatchCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -529,12 +529,11 @@ internal object PatchCommand : Callable<Int> {
}
} else {
logger.info("Options file ${file.path} does not exist, generating with defaults")
val freshBundles = patchSnapshots
val json = Json { prettyPrint = true }
file.absoluteFile.parentFile?.mkdirs()
file.writeText(json.encodeToString(freshBundles))
file.writeText(json.encodeToString(patchSnapshots))
logger.info("Generated options file at ${file.path}")
loadedBundles.zip(freshBundles).associate { (lb, b) ->
loadedBundles.zip(patchSnapshots).associate { (lb, b) ->
lb.sourceFile to b
}
}
Expand Down Expand Up @@ -782,41 +781,38 @@ internal object PatchCommand : Callable<Int> {
// stacktrace on failure since there's no "View details" UI
// in a terminal.
logger.info("Applying ${finalPatches.size} patches...")
patchingResult.addStepResult(
PatchingStep.PATCHING,
{
runBlocking {
patcher().collect { patchResult ->
val patchName = patchResult.patch.name ?: "Unknown"
patchResult.exception?.let { exception ->
StringWriter().use { writer ->
exception.printStackTrace(PrintWriter(writer))

logger.severe("FAILED: $patchName\n$writer")

patchingResult.failedPatches.add(
FailedPatch(
patchResult.patch.toSerializablePatch(),
writer.toString()
)
patchingResult.addStepResult(PatchingStep.PATCHING) {
runBlocking {
patcher().collect { patchResult ->
val patchName = patchResult.patch.name ?: "Unknown"
patchResult.exception?.let { exception ->
StringWriter().use { writer ->
exception.printStackTrace(PrintWriter(writer))

logger.severe("FAILED: $patchName\n$writer")

patchingResult.failedPatches.add(
FailedPatch(
patchResult.patch.toSerializablePatch(),
writer.toString()
)
)

if (!continueOnError) {
patchingResult.success = false
throw PatchFailedException(
"FAILED: $patchName",
exception
)
}
if (!continueOnError) {
patchingResult.success = false
throw PatchFailedException(
"FAILED: $patchName",
exception
)
}
} ?: run {
patchingResult.appliedPatches.add(patchResult.patch.toSerializablePatch())
logger.info("Applied: $patchName")
}
} ?: run {
patchingResult.appliedPatches.add(patchResult.patch.toSerializablePatch())
logger.info("Applied: $patchName")
}
}
}
)
}

// patches lives in the outer try scope (needed for patchesSnapshot and options
// file generation before the Patcher block). Clear it explicitly now — after
Expand All @@ -830,32 +826,26 @@ internal object PatchCommand : Callable<Int> {
// region Save.

inputApk.copyTo(patcherTemporaryFilesPath.resolve(inputApk.name), overwrite = true).apply {
patchingResult.addStepResult(
PatchingStep.REBUILDING,
{
patcherResult.applyTo(this)
}
)
patchingResult.addStepResult(PatchingStep.REBUILDING) {
patcherResult.applyTo(this)
}
}.let { patchedApkFile ->
if (!mount && !unsigned) {
patchingResult.addStepResult(
PatchingStep.SIGNING,
{
signWithLegacyFallback(
primary = ApkUtils.KeyStoreDetails(
keystoreFilePath,
keyStorePassword,
keyStoreEntryAlias,
keyStoreEntryPassword,
),
allowLegacyFallback = keyStoreEntryAlias == DEFAULT_KEYSTORE_ALIAS &&
keyStoreEntryPassword == DEFAULT_KEYSTORE_PASSWORD,
logger = logger,
) { details ->
ApkUtils.signApk(patchedApkFile, outputFilePath, signer, details)
}
patchingResult.addStepResult(PatchingStep.SIGNING) {
signWithLegacyFallback(
primary = ApkUtils.KeyStoreDetails(
keystoreFilePath,
keyStorePassword,
keyStoreEntryAlias,
keyStoreEntryPassword,
),
allowLegacyFallback = keyStoreEntryAlias == DEFAULT_KEYSTORE_ALIAS &&
keyStoreEntryPassword == DEFAULT_KEYSTORE_PASSWORD,
logger = logger,
) { details ->
ApkUtils.signApk(patchedApkFile, outputFilePath, signer, details)
}
)
}
} else {
patchedApkFile.copyTo(outputFilePath, overwrite = true)
}
Expand All @@ -869,25 +859,21 @@ internal object PatchCommand : Callable<Int> {
// region Install.

deviceSerial?.let {
patchingResult.addStepResult(
PatchingStep.INSTALLING,
{
runBlocking {
val result = installer!!.install(Installer.Apk(outputFilePath, packageName))
when (result) {
RootInstallerResult.FAILURE -> {
logger.severe("Failed to mount the patched APK file")
throw IllegalStateException("Failed to mount the patched APK file")
}
is AdbInstallerResult.Failure -> {
logger.severe(result.exception.toString())
throw result.exception
}
else -> logger.info("Installed the patched APK file")
patchingResult.addStepResult(PatchingStep.INSTALLING) {
runBlocking {
when (val result = installer!!.install(Installer.Apk(outputFilePath, packageName))) {
RootInstallerResult.FAILURE -> {
logger.severe("Failed to mount the patched APK file")
throw IllegalStateException("Failed to mount the patched APK file")
}
is AdbInstallerResult.Failure -> {
logger.severe(result.exception.toString())
throw result.exception
}
else -> logger.info("Installed the patched APK file")
}
}
)
}
}

// endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ object PatchFileResolver {
// morphe.software/add-source links, bare owner/repo.

val parsed = RemotePatchSourceFactory.parse(url)
?: throw IllegalArgumentException("Unrecognized patch URL: \$url")
?: throw IllegalArgumentException("Unrecognized patch URL: $url")

val source = parsed.instantiate(httpClient)

Expand Down
Loading