@@ -15,9 +15,15 @@ import picocli.CommandLine.Help.Visibility.ALWAYS
1515import picocli.CommandLine.Model.CommandSpec
1616import picocli.CommandLine.Spec
1717import java.io.File
18+ import java.io.FileOutputStream
1819import java.io.PrintWriter
1920import java.io.StringWriter
21+ import java.nio.file.Files
22+ import java.nio.file.StandardCopyOption
2023import 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