Skip to content

Commit 53b7a97

Browse files
authored
chore: Merge branch dev to main
2 parents 9cf9326 + 60e7f26 commit 53b7a97

3 files changed

Lines changed: 109 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
# 1.0.0-dev.1 (2026-01-11)
2+
3+
4+
### Bug Fixes
5+
6+
* Migrate to new package, use maven patcher dependency ([0b50811](https://github.com/AzyrRuthless/morphe-cli/commit/0b50811a5a2a0a574538b2fbffe027c04613db6c))
7+
* Use patcher / library 1.0.1 release ([0ca02e9](https://github.com/AzyrRuthless/morphe-cli/commit/0ca02e920b9e3189ac2b3a18587f0fb9efd0de4e))
8+
* Use repo library release ([0e4fae1](https://github.com/AzyrRuthless/morphe-cli/commit/0e4fae1b24bb21baa5004126df364b736d84270b))
9+
10+
11+
### Features
12+
13+
* add command option `unsigned` ([#20](https://github.com/AzyrRuthless/morphe-cli/issues/20)) ([4505091](https://github.com/AzyrRuthless/morphe-cli/commit/4505091624b854706b39656bae642e5dc1132dcd))
14+
* Initial release ([874ecbc](https://github.com/AzyrRuthless/morphe-cli/commit/874ecbce34d6499882f7a5cf648bd828c823935a))
15+
116
# [1.1.0](https://github.com/MorpheApp/morphe-cli/compare/v1.0.0...v1.1.0) (2026-01-10)
217

318

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
org.gradle.parallel = true
22
org.gradle.caching = true
33
kotlin.code.style = official
4-
version = 1.1.0
4+
version = 1.0.0-dev.1

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

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,15 @@ import picocli.CommandLine.Help.Visibility.ALWAYS
1515
import picocli.CommandLine.Model.CommandSpec
1616
import picocli.CommandLine.Spec
1717
import java.io.File
18+
import java.io.FileOutputStream
1819
import java.io.PrintWriter
1920
import java.io.StringWriter
21+
import java.nio.file.Files
22+
import java.nio.file.StandardCopyOption
2023
import java.util.logging.Logger
24+
import java.util.zip.ZipEntry
25+
import java.util.zip.ZipFile
26+
import java.util.zip.ZipOutputStream
2127

2228
@CommandLine.Command(
2329
name = "patch",
@@ -174,13 +180,32 @@ internal object PatchCommand : Runnable {
174180

175181
private var aaptBinaryPath: File? = null
176182

183+
@CommandLine.Option(
184+
names = ["--custom-zipalign-binary"],
185+
description = ["Path to a custom zipalign binary."],
186+
)
187+
private var zipalignBinaryPath: File? = null
188+
177189
@CommandLine.Option(
178190
names = ["--purge"],
179191
description = ["Purge temporary files directory after patching."],
180192
showDefaultValue = ALWAYS,
181193
)
182194
private var purge: Boolean = false
183195

196+
@CommandLine.Option(
197+
names = ["--rip-lib"],
198+
description = ["Remove native libraries for specific architectures (e.g., x86, x86_64)."],
199+
)
200+
private var ripLibs: List<String> = emptyList()
201+
202+
@CommandLine.Option(
203+
names = ["--unsigned"],
204+
description = ["Don't sign the patched APK file."],
205+
showDefaultValue = ALWAYS,
206+
)
207+
private var unsigned: Boolean = false
208+
184209
@CommandLine.Parameters(
185210
description = ["APK file to patch."],
186211
arity = "1",
@@ -228,12 +253,6 @@ internal object PatchCommand : Runnable {
228253
this.aaptBinaryPath = aaptBinaryPath
229254
}
230255

231-
@CommandLine.Option(
232-
names = ["--unsigned"],
233-
description = ["Disable signing of the final apk."],
234-
)
235-
private var unsigned: Boolean = false
236-
237256
override fun run() {
238257
// region Setup
239258

@@ -338,6 +357,36 @@ internal object PatchCommand : Runnable {
338357
apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true).apply {
339358
patcherResult.applyTo(this)
340359
}.let { patchedApkFile ->
360+
361+
if (ripLibs.isNotEmpty()) {
362+
ripLibraries(patchedApkFile, ripLibs)
363+
}
364+
365+
// zipalign logic start
366+
if (!mount) {
367+
logger.info("Running zipalign")
368+
val alignedApk = File.createTempFile("aligned", ".apk", temporaryFilesPath)
369+
val zipalign = zipalignBinaryPath?.absolutePath ?: "zipalign"
370+
371+
val process = ProcessBuilder(
372+
zipalign, "-p", "-f", "4",
373+
patchedApkFile.absolutePath,
374+
alignedApk.absolutePath
375+
).redirectErrorStream(true).start()
376+
377+
// Consume output to prevent buffer blocking and for debug logging
378+
val output = process.inputStream.bufferedReader().use { it.readText() }
379+
val exitCode = process.waitFor()
380+
381+
if (exitCode != 0) {
382+
throw RuntimeException("zipalign failed (exit code $exitCode):\n$output")
383+
}
384+
385+
Files.move(alignedApk.toPath(), patchedApkFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
386+
logger.info("zipalign completed")
387+
}
388+
// zipalign logic end
389+
341390
if (!mount && !unsigned) {
342391
ApkUtils.signApk(
343392
patchedApkFile,
@@ -456,4 +505,42 @@ internal object PatchCommand : Runnable {
456505
}
457506
logger.info(result)
458507
}
508+
509+
private fun ripLibraries(apkFile: File, archsToRemove: List<String>) {
510+
if (archsToRemove.isEmpty()) return
511+
512+
logger.info("Ripping libraries for architectures: $archsToRemove")
513+
514+
val tempFile = File.createTempFile("temp-rip", ".apk", apkFile.parentFile)
515+
516+
ZipFile(apkFile).use { zip ->
517+
ZipOutputStream(FileOutputStream(tempFile)).use { out ->
518+
val entries = zip.entries()
519+
while (entries.hasMoreElements()) {
520+
val entry = entries.nextElement()
521+
var shouldRemove = false
522+
523+
for (arch in archsToRemove) {
524+
if (entry.name.startsWith("lib/$arch/")) {
525+
shouldRemove = true
526+
break
527+
}
528+
}
529+
530+
if (!shouldRemove) {
531+
out.putNextEntry(ZipEntry(entry.name))
532+
zip.getInputStream(entry).copyTo(out)
533+
out.closeEntry()
534+
}
535+
}
536+
}
537+
}
538+
539+
if (apkFile.delete()) {
540+
tempFile.renameTo(apkFile)
541+
} else {
542+
tempFile.copyTo(apkFile, overwrite = true)
543+
tempFile.delete()
544+
}
545+
}
459546
}

0 commit comments

Comments
 (0)