refactor: migration to picocli

Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
oSumAtrIX 2022-05-01 02:07:25 +02:00
parent 58213781e1
commit f6d60a3460
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
11 changed files with 123 additions and 384 deletions

View File

@ -20,35 +20,26 @@ repositories {
}
}
val patchesDependency = "app.revanced:revanced-patches:1.0.0-dev.4"
val patchesDependency = "app.revanced:revanced-patches:+"
dependencies {
implementation(kotlin("stdlib"))
implementation("org.jetbrains.kotlinx:kotlinx-cli:0.3.4")
implementation("app.revanced:revanced-patcher:1.0.0-dev.8")
implementation("app.revanced:revanced-patcher:+")
implementation(patchesDependency)
implementation("com.google.code.gson:gson:2.9.0")
implementation("me.tongfei:progressbar:0.9.3")
implementation("com.github.li-wjohnson:jadb:master-SNAPSHOT") // using a fork instead.
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
implementation("info.picocli:picocli:+")
implementation("org.bouncycastle:bcpkix-jdk15on:+")
}
val cliMainClass = "app.revanced.cli.Main"
tasks {
build {
dependsOn(shadowJar)
}
shadowJar {
dependencies {
// This makes sure we link to the library, but don't include it.
// So, a "runtime only" dependency.
exclude(dependency(patchesDependency))
}
manifest {
attributes("Main-Class" to cliMainClass)
attributes("Main-Class" to "app.revanced.cli.Main")
attributes("Implementation-Title" to project.name)
attributes("Implementation-Version" to project.version)
}

View File

@ -1,178 +0,0 @@
package app.revanced.cli
import app.revanced.cli.runner.AdbRunner
import app.revanced.cli.utils.PatchLoader
import app.revanced.cli.utils.Patches
import app.revanced.cli.utils.Preconditions
import app.revanced.patcher.Patcher
import app.revanced.patcher.patch.PatchMetadata
import app.revanced.patcher.patch.PatchResult
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.default
import kotlinx.cli.required
import me.tongfei.progressbar.ProgressBarBuilder
import me.tongfei.progressbar.ProgressBarStyle
import java.io.File
import java.nio.file.Files
private const val CLI_NAME = "ReVanced CLI"
private val CLI_VERSION = Main::class.java.`package`.implementationVersion ?: "0.0.0-unknown"
class Main {
companion object {
private fun runCLI(
inApk: String,
inPatches: String,
inIntegrations: String?,
inOutput: String,
inRunOnAdb: String?,
hideResults: Boolean,
noLogging: Boolean,
) {
val bar = ProgressBarBuilder()
.setTaskName("Working..")
.setUpdateIntervalMillis(25)
.continuousUpdate()
.setStyle(ProgressBarStyle.ASCII)
.build()
.maxHint(1)
.setExtraMessage("Initializing")
val apk = Preconditions.isFile(inApk)
val patchesFile = Preconditions.isFile(inPatches)
val output = Preconditions.isDirectory(inOutput)
bar.step()
val patcher = Patcher(apk)
inIntegrations?.let {
bar.reset().maxHint(1)
.extraMessage = "Merging integrations"
val integrations = Preconditions.isFile(it)
patcher.addFiles(listOf(integrations))
bar.step()
}
bar.reset().maxHint(1)
.extraMessage = "Loading patches"
PatchLoader.injectPatches(patchesFile)
val patches = Patches.loadPatches().map { it() }
patcher.addPatches(patches)
bar.step()
bar.reset().maxHint(1)
.extraMessage = "Resolving signatures"
patcher.resolveSignatures()
bar.step()
val szPatches = patches.size.toLong()
bar.reset().maxHint(szPatches)
.extraMessage = "Applying patches"
val results = patcher.applyPatches {
bar.step().extraMessage = "Applying $it"
}
bar.reset().maxHint(-1)
.extraMessage = "Generating dex files"
val dexFiles = patcher.save()
val szDexFiles = dexFiles.size.toLong()
bar.reset().maxHint(szDexFiles)
.extraMessage = "Saving dex files"
dexFiles.forEach { (dexName, dexData) ->
Files.write(File(output, dexName).toPath(), dexData.data)
bar.step()
}
bar.stepTo(szDexFiles)
bar.close()
inRunOnAdb?.let { device ->
AdbRunner.runApk(
apk,
dexFiles,
output,
device,
noLogging
)
}
println("All done!")
if (!hideResults) {
printResults(results)
}
}
private fun printResults(results: Map<PatchMetadata, Result<PatchResult>>) {
for ((metadata, result) in results) {
if (result.isSuccess) {
println("${metadata.shortName} was applied successfully!")
} else {
println("${metadata.shortName} failed to apply! Cause:")
result.exceptionOrNull()!!.printStackTrace()
}
}
}
@JvmStatic
fun main(args: Array<String>) {
println("$CLI_NAME version $CLI_VERSION")
val parser = ArgParser(CLI_NAME)
// TODO: add some kind of incremental building, so merging integrations can be skipped.
// this can be achieved manually, but doing it automatically is better.
val apk by parser.option(
ArgType.String,
fullName = "apk",
shortName = "a",
description = "APK file"
).required()
val patches by parser.option(
ArgType.String,
fullName = "patches",
shortName = "p",
description = "Patches JAR file"
).required()
val integrations by parser.option(
ArgType.String,
fullName = "integrations",
shortName = "i",
description = "Integrations APK file"
)
val output by parser.option(
ArgType.String,
fullName = "output",
shortName = "o",
description = "Output directory"
).required()
val runOnAdb by parser.option(
ArgType.String,
fullName = "run-on",
description = "After the CLI is done building, which ADB device should it run on?"
)
// TODO: package name
val hideResults by parser.option(
ArgType.Boolean,
fullName = "hide-results",
description = "Don't print the patch results."
).default(false)
val noLogging by parser.option(
ArgType.Boolean,
fullName = "no-logging",
description = "Don't print the output of the application when used in combination with \"run-on\"."
).default(false)
parser.parse(args)
runCLI(
apk,
patches,
integrations,
output,
runOnAdb,
hideResults,
noLogging,
)
}
}
}

View File

@ -0,0 +1,110 @@
package app.revanced.cli
import app.revanced.cli.MainCommand.excludedPatches
import app.revanced.cli.MainCommand.patchBundles
import app.revanced.patcher.Patcher
import app.revanced.patcher.patch.Patch
import app.revanced.utils.dex.DexReplacer
import app.revanced.utils.patch.PatchLoader
import app.revanced.utils.patch.Patches
import app.revanced.utils.signing.Signer
import picocli.CommandLine
import picocli.CommandLine.*
import java.io.File
@Command(
name = "ReVanced-CLI",
version = ["1.0.0"],
mixinStandardHelpOptions = true
)
object MainCommand : Runnable {
@Option(names = ["-p", "--patches"], description = ["One or more bundles of patches"])
var patchBundles = arrayOf<File>()
@Parameters(paramLabel = "EXCLUDE", description = ["Which patches to exclude"])
var excludedPatches = arrayOf<String>()
@Option(names = ["-l", "--list"], description = ["List patches only"])
var listOnly: Boolean = false
@Option(names = ["-m", "--merge"], description = ["One or more dex file containers to merge"])
var mergeFiles = listOf<File>()
@Option(names = ["-a", "--apk"], description = ["Input file to be patched"], required = true)
lateinit var inputFile: File
@Option(names = ["-o", "--out"], description = ["Output file path"], required = true)
lateinit var outputPath: String
override fun run() {
if (listOnly) {
patchBundles.forEach {
PatchLoader.injectPatches(it)
Patches.loadPatches().forEach {
println(it().metadata)
}
}
return
}
val patcher = Patcher(inputFile)
// merge files like necessary integrations
patcher.addFiles(mergeFiles)
// add patches, but filter incompatible or excluded patches
patcher.addPatchesFiltered()
// apply patches
for (patchResult in patcher.applyPatches {
println("Applying: $it")
}) {
println(patchResult)
}
// write output file
val outFile = File(outputPath)
inputFile.copyTo(outFile)
DexReplacer.replaceDex(outFile, patcher.save())
// sign the apk file
Signer.signApk(outFile)
}
}
private fun Patcher.addPatchesFiltered() {
// TODO: get package metadata (outside of this method) for apk file which needs to be patched
val packageName = "com.example.exampleApp"
val packageVersion = "1.2.3"
patchBundles.forEach { bundle ->
PatchLoader.injectPatches(bundle)
val includedPatches = mutableListOf<Patch>()
Patches.loadPatches().forEach patch@{
val patch = it()
// TODO: filter out incompatible patches with package metadata
val filterOutPatches = true
if (filterOutPatches &&
!patch.metadata.compatiblePackages.any { packageMetadata ->
packageMetadata.name == packageName &&
packageMetadata.versions.any {
it == packageVersion
}
}
) {
// TODO: report to stdout
return@patch
}
if (excludedPatches.contains(patch.metadata.shortName)) {
// TODO: report to stdout
return@patch
}
includedPatches.add(patch)
}
this.addPatches(includedPatches)
}
}
fun main(args: Array<String>) {
CommandLine(MainCommand).execute(*args)
}

View File

@ -1,160 +0,0 @@
package app.revanced.cli.runner
import app.revanced.cli.utils.DexReplacer
import app.revanced.cli.utils.Scripts
import app.revanced.cli.utils.signer.Signer
import me.tongfei.progressbar.ProgressBar
import me.tongfei.progressbar.ProgressBarBuilder
import me.tongfei.progressbar.ProgressBarStyle
import org.jf.dexlib2.writer.io.MemoryDataStore
import se.vidstige.jadb.JadbConnection
import se.vidstige.jadb.JadbDevice
import se.vidstige.jadb.RemoteFile
import se.vidstige.jadb.ShellProcessBuilder
import java.io.File
import java.util.concurrent.Executors
object AdbRunner {
fun runApk(
apk: File,
dexFiles: Map<String, MemoryDataStore>,
outputDir: File,
deviceName: String,
noLogging: Boolean
) {
lateinit var dvc: JadbDevice
pbar("Initializing").use { bar ->
dvc = JadbConnection().findDevice(deviceName)
?: throw IllegalArgumentException("No such device with name $deviceName")
if (!dvc.hasSu())
throw IllegalArgumentException("Device $deviceName is not rooted or does not have su")
bar.step()
}
lateinit var tmpFile: File // we need this file at the end to clean up.
pbar("Generating APK file", 3).use { bar ->
bar.step().extraMessage = "Creating APK file"
tmpFile = File(outputDir, "revanced.apk")
apk.copyTo(tmpFile, true)
bar.step().extraMessage = "Replacing dex files"
DexReplacer.replaceDex(tmpFile, dexFiles)
bar.step().extraMessage = "Signing APK file"
try {
Signer.signApk(tmpFile)
} catch (e: SecurityException) {
throw IllegalStateException(
"A security exception occurred when signing the APK! " +
"If it has anything to with \"cannot authenticate\" then please make sure " +
"you are using Zulu or OpenJDK as they do work when using the adb runner.",
e
)
}
}
pbar("Running application", 6, false).use { bar ->
bar.step().extraMessage = "Pushing mount scripts"
dvc.push(Scripts.MOUNT_SCRIPT, RemoteFile(Scripts.SCRIPT_PATH))
dvc.cmd(Scripts.CREATE_DIR_COMMAND).assertZero()
dvc.cmd(Scripts.MV_MOUNT_COMMAND).assertZero()
dvc.cmd(Scripts.CHMOD_MOUNT_COMMAND).assertZero()
bar.step().extraMessage = "Pushing APK file"
dvc.push(tmpFile, RemoteFile(Scripts.APK_PATH))
bar.step().extraMessage = "Mounting APK file"
dvc.cmd(Scripts.STOP_APP_COMMAND).startAndWait()
dvc.cmd(Scripts.START_MOUNT_COMMAND).assertZero()
bar.step().extraMessage = "Starting APK file"
dvc.cmd(Scripts.START_APP_COMMAND).assertZero()
bar.step().setExtraMessage("Debugging APK file").refresh()
println("\nWaiting until app is closed.")
val executor = Executors.newSingleThreadExecutor()
val pipe = if (noLogging) {
ProcessBuilder.Redirect.PIPE
} else {
ProcessBuilder.Redirect.INHERIT
}
val p = dvc.cmd(Scripts.LOGCAT_COMMAND)
.redirectOutput(pipe)
.redirectError(pipe)
.useExecutor(executor)
.start()
Thread.sleep(250) // give the app some time to start up.
while (true) {
try {
while (dvc.cmd(Scripts.PIDOF_APP_COMMAND).startAndWait() == 0) {
Thread.sleep(250)
}
break
} catch (e: Exception) {
throw RuntimeException("An error occurred while monitoring state of app", e)
}
}
println("App closed, continuing.")
p.destroy()
executor.shutdown()
bar.step().extraMessage = "Unmounting APK file"
var exitCode: Int
do {
exitCode = dvc.cmd(Scripts.UNMOUNT_COMMAND).startAndWait()
} while (exitCode != 0)
}
}
}
private fun JadbDevice.push(s: String, remoteFile: RemoteFile) =
this.push(s.byteInputStream(), System.currentTimeMillis(), 644, remoteFile)
private fun JadbConnection.findDevice(device: String): JadbDevice? {
return devices.find { it.serial == device }
}
private fun JadbDevice.cmd(s: String): ShellProcessBuilder {
val args = s.split(" ") as ArrayList<String>
val cmd = args.removeFirst()
return shellProcessBuilder(cmd, *args.toTypedArray())
}
private fun JadbDevice.hasSu(): Boolean {
return cmd("su -h").startAndWait() == 0
}
private fun ShellProcessBuilder.startAndWait(): Int {
return start().waitFor()
}
private fun ShellProcessBuilder.assertZero() {
if (startAndWait() != 0) {
val cmd = getcmd()
throw IllegalStateException("ADB returned non-zero status code for command: $cmd")
}
}
private fun pbar(task: String, steps: Long = 1, update: Boolean = true): ProgressBar {
val b = ProgressBarBuilder().setTaskName(task)
if (update) b
.setUpdateIntervalMillis(250)
.continuousUpdate()
return b
.setStyle(ProgressBarStyle.ASCII)
.build()
.maxHint(steps + 1)
}
private fun ProgressBar.use(block: (ProgressBar) -> Unit) {
block(this)
stepTo(max) // step to 100%
extraMessage = "" // clear extra message
close()
}
private fun ShellProcessBuilder.getcmd(): String {
val f = this::class.java.getDeclaredField("command")
f.isAccessible = true
return f.get(this) as String
}

View File

@ -1,24 +0,0 @@
package app.revanced.cli.utils
import java.io.File
import java.io.FileNotFoundException
class Preconditions {
companion object {
fun isFile(path: String): File {
val f = File(path)
if (!f.exists()) {
throw FileNotFoundException(f.toString())
}
return f
}
fun isDirectory(path: String): File {
val f = isFile(path)
if (!f.isDirectory) {
throw IllegalArgumentException("$f is not a directory")
}
return f
}
}
}

View File

@ -1,9 +1,9 @@
package app.revanced.cli.utils
package app.revanced.utils
// TODO: make this a class with PACKAGE_NAME as argument, then use that everywhere.
// make sure to remove the "const" from all the vals, they won't compile obviously.
object Scripts {
private const val PACKAGE_NAME = "com.google.android.youtube"
private const val PACKAGE_NAME = "com.google.android.apps.youtube.music"
private const val DATA_PATH = "/data/adb/ReVanced"
const val APK_PATH = "/sdcard/base.apk"
const val SCRIPT_PATH = "/sdcard/mount.sh"

View File

@ -1,4 +1,4 @@
package app.revanced.cli.utils
package app.revanced.utils.dex
import lanchon.multidexlib2.BasicDexFileNamer
import org.jf.dexlib2.writer.io.MemoryDataStore

View File

@ -1,4 +1,4 @@
package app.revanced.cli.utils
package app.revanced.utils.patch
import java.io.File
import java.net.URL

View File

@ -1,4 +1,4 @@
package app.revanced.cli.utils
package app.revanced.utils.patch
import app.revanced.patches.Index

View File

@ -1,4 +1,4 @@
package app.revanced.cli.utils.signer
package app.revanced.utils.signing
import java.security.PrivateKey
import java.security.cert.X509Certificate

View File

@ -3,7 +3,7 @@
* Licensed under the Open Software License version 3.0
*/
package app.revanced.cli.utils.signer
package app.revanced.utils.signing
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
@ -41,7 +41,7 @@ val PASSWORD = "revanced".toCharArray() // TODO: make it secure; random password
/**
* APK Signer.
* @author Aliucord authors
* @author ReVanced Team
* @author ReVanced team
*/
object Signer {
private fun newKeystore(out: File) {