refactor: Move ReVanced Library subproject to another repository

This commit removes the subproject ReVanced Library and moves it to another repository. A monorepo turned out to be difficult to work with.
This commit is contained in:
oSumAtrIX
2023-09-23 18:15:10 +02:00
parent 84b24f1b6f
commit 995f2ec99b
31 changed files with 73 additions and 1692 deletions

View File

@@ -0,0 +1,99 @@
package app.revanced.cli.command
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.options.PatchOption
import picocli.CommandLine.*
import picocli.CommandLine.Help.Visibility.ALWAYS
import java.io.File
import java.util.logging.Logger
@Command(name = "list-patches", description = ["List patches from supplied patch bundles"])
internal object ListPatchesCommand : Runnable {
private val logger = Logger.getLogger(ListPatchesCommand::class.java.name)
@Parameters(
description = ["Paths to patch bundles"], arity = "1..*"
)
private lateinit var patchBundles: Array<File>
@Option(
names = ["-d", "--with-descriptions"], description = ["List their descriptions"], showDefaultValue = ALWAYS
)
private var withDescriptions: Boolean = true
@Option(
names = ["-p", "--with-packages"],
description = ["List the packages the patches are compatible with"],
showDefaultValue = ALWAYS
)
private var withPackages: Boolean = false
@Option(
names = ["-v", "--with-versions"],
description = ["List the versions of the apps the patches are compatible with"],
showDefaultValue = ALWAYS
)
private var withVersions: Boolean = false
@Option(
names = ["-o", "--with-options"], description = ["List the options of the patches"], showDefaultValue = ALWAYS
)
private var withOptions: Boolean = false
@Option(
names = ["-f", "--filter-package-name"], description = ["Filter patches by package name"]
)
private var packageName: String? = null
override fun run() {
fun Patch.CompatiblePackage.buildString() = buildString {
if (withVersions && versions != null) {
appendLine("Package name: $name")
appendLine("Compatible versions:")
append(versions!!.joinToString("\n") { version -> version }.prependIndent("\t"))
} else append("Package name: $name")
}
fun PatchOption<*>.buildString() = buildString {
appendLine("Title: $title")
appendLine("Description: $description")
value?.let {
appendLine("Key: $key")
append("Value: $it")
} ?: append("Key: $key")
}
fun Patch<*>.buildString() = buildString {
append("Name: $name")
if (withDescriptions) append("\nDescription: $description")
if (withOptions && options.isNotEmpty()) {
appendLine("\nOptions:")
append(
options.values.joinToString("\n\n") { option ->
option.buildString()
}.prependIndent("\t")
)
}
if (withPackages && compatiblePackages != null) {
appendLine("\nCompatible packages:")
append(
compatiblePackages!!.joinToString("\n") { it.buildString() }.prependIndent("\t")
)
}
}
fun Patch<*>.anyPackageName(name: String) = compatiblePackages?.any { it.name == name } == true
val patches = PatchBundleLoader.Jar(*patchBundles)
val filtered = packageName?.let { patches.filter { patch -> patch.anyPackageName(it) } } ?: patches
if (filtered.isNotEmpty()) logger.info(filtered.joinToString("\n\n") { it.buildString() })
}
}

View File

@@ -0,0 +1,39 @@
package app.revanced.cli.command
import app.revanced.cli.command.utility.UtilityCommand
import app.revanced.library.logging.Logger
import picocli.CommandLine
import picocli.CommandLine.Command
import picocli.CommandLine.IVersionProvider
import java.util.*
fun main(args: Array<String>) {
Logger.setDefault()
CommandLine(MainCommand).execute(*args)
}
private object CLIVersionProvider : IVersionProvider {
override fun getVersion() = arrayOf(
MainCommand::class.java.getResourceAsStream(
"/app/revanced/cli/version.properties"
)?.use { stream ->
Properties().apply { load(stream) }.let {
"ReVanced CLI v${it.getProperty("version")}"
}
} ?: "ReVanced CLI")
}
@Command(
name = "revanced-cli",
description = ["Command line application to use ReVanced"],
mixinStandardHelpOptions = true,
versionProvider = CLIVersionProvider::class,
subcommands = [
ListPatchesCommand::class,
PatchCommand::class,
OptionsCommand::class,
UtilityCommand::class,
]
)
private object MainCommand

View File

@@ -0,0 +1,53 @@
package app.revanced.cli.command
import app.revanced.library.Options
import app.revanced.library.Options.setOptions
import app.revanced.patcher.PatchBundleLoader
import picocli.CommandLine
import picocli.CommandLine.Help.Visibility.ALWAYS
import java.io.File
import java.util.logging.Logger
@CommandLine.Command(
name = "options",
description = ["Generate options file from patches"],
)
internal object OptionsCommand : Runnable {
private val logger = Logger.getLogger(OptionsCommand::class.java.name)
@CommandLine.Parameters(
description = ["Paths to patch bundles"], arity = "1..*"
)
private lateinit var patchBundles: Array<File>
@CommandLine.Option(
names = ["-p", "--path"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS
)
private var filePath: File = File("options.json")
@CommandLine.Option(
names = ["-o", "--overwrite"], description = ["Overwrite existing options file"], showDefaultValue = ALWAYS
)
private var overwrite: Boolean = false
@CommandLine.Option(
names = ["-u", "--update"],
description = ["Update existing options by adding missing and removing non-existent options"],
showDefaultValue = ALWAYS
)
private var update: Boolean = false
override fun run() = try {
PatchBundleLoader.Jar(*patchBundles).let { patches ->
if (!filePath.exists() || overwrite) {
if (update && filePath.exists()) patches.setOptions(filePath)
Options.serialize(patches, prettyPrint = true).let(filePath::writeText)
} else throw OptionsFileAlreadyExistsException()
}
} catch (ex: OptionsFileAlreadyExistsException) {
logger.severe("Options file already exists, use --overwrite to override it")
}
class OptionsFileAlreadyExistsException : Exception()
}

View File

@@ -0,0 +1,331 @@
package app.revanced.cli.command
import app.revanced.library.ApkUtils
import app.revanced.library.Options
import app.revanced.library.Options.setOptions
import app.revanced.library.adb.AdbManager
import app.revanced.patcher.PatchBundleLoader
import app.revanced.patcher.PatchSet
import app.revanced.patcher.Patcher
import app.revanced.patcher.PatcherOptions
import kotlinx.coroutines.runBlocking
import picocli.CommandLine
import picocli.CommandLine.Help.Visibility.ALWAYS
import picocli.CommandLine.Model.CommandSpec
import picocli.CommandLine.Spec
import java.io.File
import java.io.PrintWriter
import java.io.StringWriter
import java.util.logging.Logger
@CommandLine.Command(
name = "patch", description = ["Patch an APK file"]
)
internal object PatchCommand : Runnable {
private val logger = Logger.getLogger(PatchCommand::class.java.name)
@Spec
lateinit var spec: CommandSpec // injected by picocli
private lateinit var apk: File
private var integrations = listOf<File>()
private var patchBundles = emptyList<File>()
@CommandLine.Option(
names = ["-i", "--include"], description = ["List of patches to include"]
)
private var includedPatches = arrayOf<String>()
@CommandLine.Option(
names = ["-e", "--exclude"], description = ["List of patches to exclude"]
)
private var excludedPatches = arrayOf<String>()
@CommandLine.Option(
names = ["--options"], description = ["Path to patch options JSON file"], showDefaultValue = ALWAYS
)
private var optionsFile: File = File("options.json")
@CommandLine.Option(
names = ["--exclusive"],
description = ["Only include patches that are explicitly specified to be included"],
showDefaultValue = ALWAYS
)
private var exclusive = false
@CommandLine.Option(
names = ["-f","--force"],
description = ["Force inclusion of patches that are incompatible with the supplied APK file's version"],
showDefaultValue = ALWAYS
)
private var force: Boolean = false
@CommandLine.Option(
names = ["-o", "--out"], description = ["Path to save the patched APK file to"], required = true
)
private lateinit var outputFilePath: File
@CommandLine.Option(
names = ["-d", "--device-serial"], description = ["ADB device serial to install to"], showDefaultValue = ALWAYS
)
private var deviceSerial: String? = null
@CommandLine.Option(
names = ["--mount"], description = ["Install by mounting the patched APK file"], showDefaultValue = ALWAYS
)
private var mount: Boolean = false
@CommandLine.Option(
names = ["--keystore"], description = ["Path to the keystore to sign the patched APK file with"],
)
private var keystoreFilePath: File? = null
// key store password
@CommandLine.Option(
names = ["--keystore-password"],
description = ["The password of the keystore to sign the patched APK file with"],
)
private var keyStorePassword: String? = null // Empty password by default
@CommandLine.Option(
names = ["--alias"], description = ["The alias of the key from the keystore to sign the patched APK file with"],
showDefaultValue = ALWAYS
)
private var alias = "ReVanced Key"
@CommandLine.Option(
names = ["--keystore-entry-password"],
description = ["The password of the entry from the keystore for the key to sign the patched APK file with"]
)
private var password = "" // Empty password by default
@CommandLine.Option(
names = ["--signer"], description = ["The name of the signer to sign the patched APK file with"],
showDefaultValue = ALWAYS
)
private var signer = "ReVanced"
@CommandLine.Option(
names = ["-r", "--resource-cache"],
description = ["Path to temporary resource cache directory"],
showDefaultValue = ALWAYS
)
private var resourceCachePath = File("revanced-resource-cache")
private var aaptBinaryPath: File? = null
@CommandLine.Option(
names = ["-p", "--purge"],
description = ["Purge the temporary resource cache directory after patching"],
showDefaultValue = ALWAYS
)
private var purge: Boolean = false
@CommandLine.Option(
names = ["-w", "--warn"],
description = ["Warn if a patch can not be found in the supplied patch bundles"],
showDefaultValue = ALWAYS
)
private var warn: Boolean = false
@CommandLine.Parameters(
description = ["APK file to be patched"], arity = "1..1"
)
@Suppress("unused")
private fun setApk(apk: File) {
if (!apk.exists()) throw CommandLine.ParameterException(
spec.commandLine(),
"APK file ${apk.name} does not exist"
)
this.apk = apk
}
@CommandLine.Option(
names = ["-m", "--merge"], description = ["One or more DEX files or containers to merge into the APK"]
)
@Suppress("unused")
private fun setIntegrations(integrations: Array<File>) {
integrations.firstOrNull { !it.exists() }?.let {
throw CommandLine.ParameterException(spec.commandLine(), "Integrations file ${it.name} does not exist")
}
this.integrations += integrations
}
@CommandLine.Option(
names = ["-b", "--patch-bundle"], description = ["One or more bundles of patches"], required = true
)
@Suppress("unused")
private fun setPatchBundles(patchBundles: Array<File>) {
patchBundles.firstOrNull { !it.exists() }?.let {
throw CommandLine.ParameterException(spec.commandLine(), "Patch bundle ${it.name} does not exist")
}
this.patchBundles = patchBundles.toList()
}
@CommandLine.Option(
names = ["--custom-aapt2-binary"], description = ["Path to a custom AAPT binary to compile resources with"]
)
@Suppress("unused")
private fun setAaptBinaryPath(aaptBinaryPath: File) {
if (!aaptBinaryPath.exists()) throw CommandLine.ParameterException(
spec.commandLine(),
"AAPT binary ${aaptBinaryPath.name} does not exist"
)
this.aaptBinaryPath = aaptBinaryPath
}
override fun run() {
val adbManager = deviceSerial?.let { serial -> AdbManager.getAdbManager(serial, mount) }
// region Load patches
logger.info("Loading patches")
val patches = PatchBundleLoader.Jar(*patchBundles.toTypedArray())
// Warn if a patch can not be found in the supplied patch bundles.
if (warn) patches.map { it.name }.toHashSet().let { availableNames ->
arrayOf(*includedPatches, *excludedPatches).filter { name ->
!availableNames.contains(name)
}
}.let { unknownPatches ->
if (unknownPatches.isEmpty()) return@let
logger.warning("Unknown input of patches:\n${unknownPatches.joinToString("\n")}")
}
logger.info("Setting patch options")
optionsFile.let {
if (it.exists()) patches.setOptions(it)
else Options.serialize(patches, prettyPrint = true).let(it::writeText)
}
// endregion
Patcher(
PatcherOptions(
apk,
resourceCachePath,
aaptBinaryPath?.path,
resourceCachePath.absolutePath,
)
).use { patcher ->
// region Patch
val patcherResult = patcher.apply {
acceptIntegrations(integrations)
acceptPatches(filterPatchSelection(patches))
// Execute patches.
runBlocking {
apply(false).collect { patchResult ->
patchResult.exception?.let {
StringWriter().use { writer ->
it.printStackTrace(PrintWriter(writer))
logger.severe("${patchResult.patch.name} failed:\n$writer")
}
} ?: logger.info("${patchResult.patch.name} succeeded")
}
}
}.get()
// endregion
// region Save
val tempFile = resourceCachePath.resolve(apk.name).apply {
ApkUtils.copyAligned(apk, this, patcherResult)
}
val keystoreFilePath = keystoreFilePath ?: outputFilePath.absoluteFile.parentFile
.resolve("${outputFilePath.nameWithoutExtension}.keystore")
if (!mount) ApkUtils.sign(
tempFile,
outputFilePath,
ApkUtils.SigningOptions(
keystoreFilePath,
keyStorePassword,
alias,
password,
signer
)
)
// endregion
// region Install
adbManager?.install(AdbManager.Apk(outputFilePath, patcher.context.packageMetadata.packageName))
// endregion
}
if (purge) {
logger.info("Purging temporary files")
purge(resourceCachePath)
}
}
/**
* Filter the patches to be added to the patcher. The filter is based on the following:
*
* @param patches The patches to filter.
* @return The filtered patches.
*/
private fun Patcher.filterPatchSelection(patches: PatchSet) = buildList {
val packageName = context.packageMetadata.packageName
val packageVersion = context.packageMetadata.packageVersion
patches.forEach patch@{ patch ->
val patchName = patch.name!!
val explicitlyExcluded = excludedPatches.contains(patchName)
if (explicitlyExcluded) return@patch logger.info("Excluding $patchName")
// Make sure the patch is compatible with the supplied APK files package name and version.
patch.compatiblePackages?.let { packages ->
packages.singleOrNull { it.name == packageName }?.let { `package` ->
val matchesVersion = force || `package`.versions?.let {
it.any { version -> version == packageVersion }
} ?: true
if (!matchesVersion) return@patch logger.warning(
"$patchName is incompatible with version $packageVersion. "
+ "This patch is only compatible with version "
+ packages.joinToString(";") { pkg ->
"${pkg.name}: ${pkg.versions!!.joinToString(", ")}"
}
)
} ?: return@patch logger.fine(
"$patchName is incompatible with $packageName. "
+ "This patch is only compatible with "
+ packages.joinToString(", ") { `package` -> `package`.name })
return@let
} ?: logger.fine("$patchName has no constraint on packages.")
// If the patch is implicitly used, it will be only included if [exclusive] is false.
val implicitlyIncluded = !exclusive && patch.use
// If the patch is explicitly used, it will be included even if [exclusive] is false.
val explicitlyIncluded = includedPatches.contains(patchName)
val included = implicitlyIncluded || explicitlyIncluded
if (!included) return@patch logger.info("$patchName excluded") // Case 1.
logger.fine("Adding $patchName")
add(patch)
}
}
private fun purge(resourceCachePath: File) {
val result = if (resourceCachePath.deleteRecursively()) "Purged resource cache directory"
else "Failed to purge resource cache directory"
logger.info(result)
}
}

View File

@@ -0,0 +1,38 @@
package app.revanced.cli.command.utility
import app.revanced.library.adb.AdbManager
import picocli.CommandLine.*
import java.io.File
import java.util.logging.Logger
@Command(
name = "install", description = ["Install an APK file to devices with the supplied ADB device serials"]
)
internal object InstallCommand : Runnable {
private val logger = Logger.getLogger(InstallCommand::class.java.name)
@Parameters(
description = ["ADB device serials"], arity = "1..*"
)
private lateinit var deviceSerials: Array<String>
@Option(
names = ["-a", "--apk"], description = ["APK file to be installed"], required = true
)
private lateinit var apk: File
@Option(
names = ["-m", "--mount"],
description = ["Mount the supplied APK file over the app with the supplied package name"],
)
private var packageName: String? = null
override fun run() = deviceSerials.forEach { deviceSerial ->
try {
AdbManager.getAdbManager(deviceSerial, packageName != null).install(AdbManager.Apk(apk, packageName))
} catch (e: AdbManager.DeviceNotFoundException) {
logger.severe(e.toString())
}
}
}

View File

@@ -0,0 +1,36 @@
package app.revanced.cli.command.utility
import app.revanced.library.adb.AdbManager
import picocli.CommandLine.*
import picocli.CommandLine.Help.Visibility.ALWAYS
import java.util.logging.Logger
@Command(
name = "uninstall",
description = ["Uninstall a patched app from the devices with the supplied ADB device serials"]
)
internal object UninstallCommand : Runnable {
private val logger = Logger.getLogger(UninstallCommand::class.java.name)
@Parameters(description = ["ADB device serials"], arity = "1..*")
private lateinit var deviceSerials: Array<String>
@Option(names = ["-p", "--package-name"], description = ["Package name of the app to uninstall"], required = true)
private lateinit var packageName: String
@Option(
names = ["-u", "--unmount"],
description = ["Uninstall by unmounting the patched APK file"],
showDefaultValue = ALWAYS
)
private var unmount: Boolean = false
override fun run() = deviceSerials.forEach { deviceSerial ->
try {
AdbManager.getAdbManager(deviceSerial, unmount).uninstall(packageName)
} catch (e: AdbManager.DeviceNotFoundException) {
logger.severe(e.toString())
}
}
}

View File

@@ -0,0 +1,10 @@
package app.revanced.cli.command.utility
import picocli.CommandLine
@CommandLine.Command(
name = "utility",
description = ["Commands for utility purposes"],
subcommands = [InstallCommand::class, UninstallCommand::class],
)
internal object UtilityCommand

View File

@@ -0,0 +1 @@
version=${projectVersion}