mirror of
https://github.com/revanced/revanced-cli.git
synced 2025-01-06 01:05:50 +01:00
refactor: migration to picocli
Signed-off-by: oSumAtrIX <johan.melkonyan1@web.de>
This commit is contained in:
parent
58213781e1
commit
f6d60a3460
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
110
src/main/kotlin/app/revanced/cli/MainCommand.kt
Normal file
110
src/main/kotlin/app/revanced/cli/MainCommand.kt
Normal 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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
@ -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
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.cli.utils
|
||||
package app.revanced.utils.patch
|
||||
|
||||
import java.io.File
|
||||
import java.net.URL
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.cli.utils
|
||||
package app.revanced.utils.patch
|
||||
|
||||
import app.revanced.patches.Index
|
||||
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.cli.utils.signer
|
||||
package app.revanced.utils.signing
|
||||
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
@ -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) {
|
Loadingβ¦
Reference in New Issue
Block a user