@@ -15,9 +15,13 @@ 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
2021import java.util.logging.Logger
22+ import java.util.zip.ZipEntry
23+ import java.util.zip.ZipFile
24+ import java.util.zip.ZipOutputStream
2125
2226@CommandLine.Command (
2327 name = " patch" ,
@@ -181,6 +185,19 @@ internal object PatchCommand : Runnable {
181185 )
182186 private var purge: Boolean = false
183187
188+ @CommandLine.Option (
189+ names = [" --rip-lib" ],
190+ description = [" Remove native libraries for specific architectures (e.g., x86, x86_64)." ],
191+ )
192+ private var ripLibs: List <String > = emptyList()
193+
194+ @CommandLine.Option (
195+ names = [" --unsigned" ],
196+ description = [" Don't sign the patched APK file." ],
197+ showDefaultValue = ALWAYS ,
198+ )
199+ private var unsigned: Boolean = false
200+
184201 @CommandLine.Parameters (
185202 description = [" APK file to patch." ],
186203 arity = " 1" ,
@@ -228,12 +245,6 @@ internal object PatchCommand : Runnable {
228245 this .aaptBinaryPath = aaptBinaryPath
229246 }
230247
231- @CommandLine.Option (
232- names = [" --unsigned" ],
233- description = [" Disable signing of the final apk." ],
234- )
235- private var unsigned: Boolean = false
236-
237248 override fun run () {
238249 // region Setup
239250
@@ -338,6 +349,37 @@ internal object PatchCommand : Runnable {
338349 apk.copyTo(temporaryFilesPath.resolve(apk.name), overwrite = true ).apply {
339350 patcherResult.applyTo(this )
340351 }.let { patchedApkFile ->
352+
353+ if (ripLibs.isNotEmpty()) {
354+ ripLibraries(patchedApkFile, ripLibs)
355+ }
356+
357+ // zipalign logic start
358+ if (! mount) {
359+ try {
360+ logger.info(" Running zipalign..." )
361+ val alignedApk = File .createTempFile(" aligned" , " .apk" , temporaryFilesPath)
362+ val pb = ProcessBuilder (
363+ " zipalign" , " -p" , " -f" , " 4" ,
364+ patchedApkFile.absolutePath,
365+ alignedApk.absolutePath
366+ )
367+ val process = pb.start()
368+ if (process.waitFor() == 0 ) {
369+ if (patchedApkFile.delete()) {
370+ alignedApk.renameTo(patchedApkFile)
371+ logger.info(" zipalign completed" )
372+ }
373+ } else {
374+ logger.warning(" zipalign failed (exit code ${process.exitValue()} )" )
375+ alignedApk.delete()
376+ }
377+ } catch (e: Exception ) {
378+ logger.warning(" Failed to run zipalign: ${e.message} . Ensure it is in PATH." )
379+ }
380+ }
381+ // zipalign logic end
382+
341383 if (! mount && ! unsigned) {
342384 ApkUtils .signApk(
343385 patchedApkFile,
@@ -456,4 +498,42 @@ internal object PatchCommand : Runnable {
456498 }
457499 logger.info(result)
458500 }
501+
502+ private fun ripLibraries (apkFile : File , archsToRemove : List <String >) {
503+ if (archsToRemove.isEmpty()) return
504+
505+ logger.info(" Ripping libraries for architectures: $archsToRemove " )
506+
507+ val tempFile = File .createTempFile(" temp-rip" , " .apk" , apkFile.parentFile)
508+
509+ ZipFile (apkFile).use { zip ->
510+ ZipOutputStream (FileOutputStream (tempFile)).use { out ->
511+ val entries = zip.entries()
512+ while (entries.hasMoreElements()) {
513+ val entry = entries.nextElement()
514+ var shouldRemove = false
515+
516+ for (arch in archsToRemove) {
517+ if (entry.name.startsWith(" lib/$arch /" )) {
518+ shouldRemove = true
519+ break
520+ }
521+ }
522+
523+ if (! shouldRemove) {
524+ out .putNextEntry(ZipEntry (entry.name))
525+ zip.getInputStream(entry).copyTo(out )
526+ out .closeEntry()
527+ }
528+ }
529+ }
530+ }
531+
532+ if (apkFile.delete()) {
533+ tempFile.renameTo(apkFile)
534+ } else {
535+ tempFile.copyTo(apkFile, overwrite = true )
536+ tempFile.delete()
537+ }
538+ }
459539}
0 commit comments