You've already forked revanced-patcher
mirror of
https://github.com/revanced/revanced-patcher
synced 2025-09-10 05:30:49 +02:00
Compare commits
9 Commits
v15.0.0-de
...
arsclib-re
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c6fdf97794 | ||
![]() |
c52f0b80f2 | ||
![]() |
4b5e25b29c | ||
![]() |
c752a3c596 | ||
![]() |
740911a2a3 | ||
![]() |
242d805c6c | ||
![]() |
c543fdc18b | ||
![]() |
d48a8e697f | ||
![]() |
8749a61d39 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: ./gradlew build clean --no-daemon
|
run: ./gradlew clean --no-daemon
|
||||||
- name: Setup semantic-release
|
- name: Setup semantic-release
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: Release
|
- name: Release
|
||||||
|
@@ -7,13 +7,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
[
|
"@semantic-release/commit-analyzer",
|
||||||
"@semantic-release/commit-analyzer", {
|
|
||||||
"releaseRules": [
|
|
||||||
{ "type": "build", "scope": "Needs bump", "release": "patch" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@semantic-release/release-notes-generator",
|
"@semantic-release/release-notes-generator",
|
||||||
"@semantic-release/changelog",
|
"@semantic-release/changelog",
|
||||||
"gradle-semantic-release-plugin",
|
"gradle-semantic-release-plugin",
|
||||||
|
291
CHANGELOG.md
291
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
42
arsclib-utils/.gitignore
vendored
Normal file
42
arsclib-utils/.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea/modules.xml
|
||||||
|
.idea/jarRepositories.xml
|
||||||
|
.idea/compiler.xml
|
||||||
|
.idea/libraries/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
|
||||||
|
### Eclipse ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
bin/
|
||||||
|
!**/src/main/**/bin/
|
||||||
|
!**/src/test/**/bin/
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
### Mac OS ###
|
||||||
|
.DS_Store
|
18
arsclib-utils/build.gradle.kts
Normal file
18
arsclib-utils/build.gradle.kts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
`maven-publish`
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "app.revanced"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("io.github.reandroid:ARSCLib:1.1.7")
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
withSourcesJar()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(11)
|
||||||
|
}
|
@@ -0,0 +1,72 @@
|
|||||||
|
package app.revanced.arsc
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception thrown when there is an error with APK resources.
|
||||||
|
*
|
||||||
|
* @param message The exception message.
|
||||||
|
* @param throwable The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
sealed class ApkResourceException(message: String, throwable: Throwable? = null) : Exception(message, throwable) {
|
||||||
|
/**
|
||||||
|
* An exception when locking resources.
|
||||||
|
*
|
||||||
|
* @param message The exception message.
|
||||||
|
* @param throwable The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
class Locked(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception when writing resources.
|
||||||
|
*
|
||||||
|
* @param message The exception message.
|
||||||
|
* @param throwable The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
class Write(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception when reading resources.
|
||||||
|
*
|
||||||
|
* @param message The exception message.
|
||||||
|
* @param throwable The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
class Read(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
|
||||||
|
/**
|
||||||
|
* An exception when decoding resources.
|
||||||
|
*
|
||||||
|
* @param message The exception message.
|
||||||
|
* @param throwable The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
class Decode(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception when encoding resources.
|
||||||
|
*
|
||||||
|
* @param message The exception message.
|
||||||
|
* @param throwable The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
class Encode(message: String, throwable: Throwable? = null) : ApkResourceException(message, throwable)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception thrown when a reference could not be resolved.
|
||||||
|
*
|
||||||
|
* @param reference The invalid reference.
|
||||||
|
* @param throwable The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
class InvalidReference(reference: String, throwable: Throwable? = null) :
|
||||||
|
ApkResourceException("Failed to resolve: $reference", throwable) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception thrown when a reference could not be resolved.
|
||||||
|
*
|
||||||
|
* @param type The type of the reference.
|
||||||
|
* @param name The name of the reference.
|
||||||
|
* @param throwable The corresponding [Throwable].
|
||||||
|
*/
|
||||||
|
constructor(type: String, name: String, throwable: Throwable? = null) : this("@$type/$name", throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An exception thrown when the Apk file not have a resource table, but was expected to have one.
|
||||||
|
*/
|
||||||
|
class MissingResourceTable : ApkResourceException("Apk does not have a resource table.")
|
||||||
|
}
|
@@ -0,0 +1,28 @@
|
|||||||
|
@file:Suppress("MemberVisibilityCanBePrivate")
|
||||||
|
|
||||||
|
package app.revanced.arsc.archive
|
||||||
|
|
||||||
|
import app.revanced.arsc.resource.ResourceContainer
|
||||||
|
import com.reandroid.apk.ApkModule
|
||||||
|
import com.reandroid.apk.DexFileInputSource
|
||||||
|
import com.reandroid.archive.InputSource
|
||||||
|
import java.io.File
|
||||||
|
import java.io.Flushable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class for reading/writing files in an [ApkModule].
|
||||||
|
*
|
||||||
|
* @param module The [ApkModule] to operate on.
|
||||||
|
*/
|
||||||
|
class Archive(internal val module: ApkModule) : Flushable {
|
||||||
|
val mainPackageResources = ResourceContainer(this, module.tableBlock)
|
||||||
|
|
||||||
|
fun save(output: File) {
|
||||||
|
flush()
|
||||||
|
module.writeApk(output)
|
||||||
|
}
|
||||||
|
fun readDexFiles(): MutableList<DexFileInputSource> = module.listDexFiles()
|
||||||
|
fun write(inputSource: InputSource) = module.apkArchive.add(inputSource) // Overwrites existing files.
|
||||||
|
fun read(name: String): InputSource? = module.apkArchive.getInputSource(name)
|
||||||
|
override fun flush() = mainPackageResources.flush()
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package app.revanced.arsc.logging
|
||||||
|
interface Logger {
|
||||||
|
fun error(msg: String)
|
||||||
|
fun warn(msg: String)
|
||||||
|
fun info(msg: String)
|
||||||
|
fun trace(msg: String)
|
||||||
|
}
|
@@ -0,0 +1,166 @@
|
|||||||
|
package app.revanced.arsc.resource
|
||||||
|
|
||||||
|
import app.revanced.arsc.ApkResourceException
|
||||||
|
import com.reandroid.arsc.coder.EncodeResult
|
||||||
|
import com.reandroid.arsc.coder.ValueDecoder
|
||||||
|
import com.reandroid.arsc.value.Entry
|
||||||
|
import com.reandroid.arsc.value.ValueType
|
||||||
|
import com.reandroid.arsc.value.array.ArrayBag
|
||||||
|
import com.reandroid.arsc.value.array.ArrayBagItem
|
||||||
|
import com.reandroid.arsc.value.plurals.PluralsBag
|
||||||
|
import com.reandroid.arsc.value.plurals.PluralsBagItem
|
||||||
|
import com.reandroid.arsc.value.plurals.PluralsQuantity
|
||||||
|
import com.reandroid.arsc.value.style.StyleBag
|
||||||
|
import com.reandroid.arsc.value.style.StyleBagItem
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A resource value.
|
||||||
|
*/
|
||||||
|
sealed class Resource {
|
||||||
|
internal abstract fun write(entry: Entry, resources: ResourceContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val Resource.isComplex get() = when (this) {
|
||||||
|
is Scalar -> false
|
||||||
|
is Complex -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple resource.
|
||||||
|
*/
|
||||||
|
open class Scalar internal constructor(private val valueType: ValueType, private val value: Int) : Resource() {
|
||||||
|
protected open fun data(resources: ResourceContainer) = value
|
||||||
|
|
||||||
|
override fun write(entry: Entry, resources: ResourceContainer) {
|
||||||
|
entry.setValueAsRaw(valueType, data(resources))
|
||||||
|
}
|
||||||
|
|
||||||
|
internal open fun toArrayItem(resources: ResourceContainer) = ArrayBagItem.create(valueType, data(resources))
|
||||||
|
internal open fun toStyleItem(resources: ResourceContainer) = StyleBagItem.create(valueType, data(resources))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A marker class for complex resources.
|
||||||
|
*/
|
||||||
|
sealed class Complex : Resource()
|
||||||
|
|
||||||
|
private fun encoded(encodeResult: EncodeResult?) = encodeResult?.let { Scalar(it.valueType, it.value) }
|
||||||
|
?: throw ApkResourceException.Encode("Failed to encode value")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a color.
|
||||||
|
*
|
||||||
|
* @param hex The hex value of the color.
|
||||||
|
* @return The encoded [Resource].
|
||||||
|
*/
|
||||||
|
fun color(hex: String) = encoded(ValueDecoder.encodeColor(hex))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a dimension or fraction.
|
||||||
|
*
|
||||||
|
* @param value The dimension value such as 24dp.
|
||||||
|
* @return The encoded [Resource].
|
||||||
|
*/
|
||||||
|
fun dimension(value: String) = encoded(ValueDecoder.encodeDimensionOrFraction(value))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a boolean resource.
|
||||||
|
*
|
||||||
|
* @param value The boolean.
|
||||||
|
* @return The encoded [Resource].
|
||||||
|
*/
|
||||||
|
fun boolean(value: Boolean) = Scalar(ValueType.INT_BOOLEAN, if (value) -Int.MAX_VALUE else 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a float.
|
||||||
|
*
|
||||||
|
* @param n The number to encode.
|
||||||
|
* @return The encoded [Resource].
|
||||||
|
*/
|
||||||
|
fun float(n: Float) = Scalar(ValueType.FLOAT, n.toBits())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an integer [Resource].
|
||||||
|
*
|
||||||
|
* @param n The number to encode.
|
||||||
|
* @return The integer [Resource].
|
||||||
|
*/
|
||||||
|
fun integer(n: Int) = Scalar(ValueType.INT_DEC, n)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a reference [Resource].
|
||||||
|
*
|
||||||
|
* @param resourceId The target resource.
|
||||||
|
* @return The reference resource.
|
||||||
|
*/
|
||||||
|
fun reference(resourceId: Int) = Scalar(ValueType.REFERENCE, resourceId)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve and create a reference [Resource].
|
||||||
|
*
|
||||||
|
* @see reference
|
||||||
|
* @param ref The reference string to resolve.
|
||||||
|
* @param resourceTable The resource table to resolve the reference with.
|
||||||
|
* @return The reference resource.
|
||||||
|
*/
|
||||||
|
fun reference(resourceTable: ResourceTable, ref: String) = reference(resourceTable.resolve(ref))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array [Resource].
|
||||||
|
*
|
||||||
|
* @param elements The elements of the array.
|
||||||
|
*/
|
||||||
|
class Array(private val elements: Collection<Scalar>) : Complex() {
|
||||||
|
override fun write(entry: Entry, resources: ResourceContainer) {
|
||||||
|
ArrayBag.create(entry).addAll(elements.map { it.toArrayItem(resources) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A style resource.
|
||||||
|
*
|
||||||
|
* @param elements The attributes to override.
|
||||||
|
* @param parent A reference to the parent style.
|
||||||
|
*/
|
||||||
|
class Style(private val elements: Map<String, Scalar>, private val parent: String? = null) : Complex() {
|
||||||
|
override fun write(entry: Entry, resources: ResourceContainer) {
|
||||||
|
val resTable = resources.resourceTable
|
||||||
|
val style = StyleBag.create(entry)
|
||||||
|
parent?.let {
|
||||||
|
style.parentId = resTable.resolve(parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
style.putAll(
|
||||||
|
elements.asIterable().associate {
|
||||||
|
StyleBag.resolve(resTable.encodeMaterials, it.key) to it.value.toStyleItem(resources)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A quantity string [Resource].
|
||||||
|
*
|
||||||
|
* @param elements A map of the quantity to the corresponding string.
|
||||||
|
*/
|
||||||
|
class Plurals(private val elements: Map<String, String>) : Complex() {
|
||||||
|
override fun write(entry: Entry, resources: ResourceContainer) {
|
||||||
|
val plurals = PluralsBag.create(entry)
|
||||||
|
|
||||||
|
plurals.putAll(elements.asIterable().associate { (k, v) ->
|
||||||
|
PluralsQuantity.value(k) to PluralsBagItem.string(resources.getOrCreateString(v))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string [Resource].
|
||||||
|
*
|
||||||
|
* @param value The string value.
|
||||||
|
*/
|
||||||
|
class StringResource(val value: String) : Scalar(ValueType.STRING, 0) {
|
||||||
|
private fun tableString(resources: ResourceContainer) = resources.getOrCreateString(value)
|
||||||
|
|
||||||
|
override fun data(resources: ResourceContainer) = tableString(resources).index
|
||||||
|
override fun toArrayItem(resources: ResourceContainer) = ArrayBagItem.string(tableString(resources))
|
||||||
|
override fun toStyleItem(resources: ResourceContainer) = StyleBagItem.string(tableString(resources))
|
||||||
|
}
|
@@ -0,0 +1,167 @@
|
|||||||
|
package app.revanced.arsc.resource
|
||||||
|
|
||||||
|
import app.revanced.arsc.ApkResourceException
|
||||||
|
import app.revanced.arsc.archive.Archive
|
||||||
|
import com.reandroid.apk.xmlencoder.EncodeUtil
|
||||||
|
import com.reandroid.arsc.chunk.TableBlock
|
||||||
|
import com.reandroid.arsc.chunk.xml.ResXmlDocument
|
||||||
|
import com.reandroid.arsc.value.Entry
|
||||||
|
import com.reandroid.arsc.value.ResConfig
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.File
|
||||||
|
import java.io.Flushable
|
||||||
|
|
||||||
|
class ResourceContainer(private val archive: Archive, internal val tableBlock: TableBlock) : Flushable {
|
||||||
|
private val packageBlock = tableBlock.pickOne() // Pick the main package block.
|
||||||
|
internal lateinit var resourceTable: ResourceTable // TODO: Set this.
|
||||||
|
|
||||||
|
private val lockedResourceFileNames = mutableSetOf<String>()
|
||||||
|
|
||||||
|
private fun lock(resourceFile: ResourceFile) {
|
||||||
|
if (resourceFile.name in lockedResourceFileNames) {
|
||||||
|
throw ApkResourceException.Locked("Resource file ${resourceFile.name} is already locked.")
|
||||||
|
}
|
||||||
|
|
||||||
|
lockedResourceFileNames.add(resourceFile.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unlock(resourceFile: ResourceFile) {
|
||||||
|
lockedResourceFileNames.remove(resourceFile.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun <T : ResourceFile> openResource(name: String): ResourceFileEditor<T> {
|
||||||
|
val inputSource = archive.read(name)
|
||||||
|
?: throw ApkResourceException.Read("Resource file $name not found.")
|
||||||
|
|
||||||
|
val resourceFile = when {
|
||||||
|
ResXmlDocument.isResXmlBlock(inputSource.openStream()) -> {
|
||||||
|
val xmlDocument = archive.module
|
||||||
|
.loadResXmlDocument(inputSource)
|
||||||
|
.decodeToXml(resourceTable.entryStore, packageBlock.id)
|
||||||
|
|
||||||
|
ResourceFile.XmlResourceFile(name, xmlDocument)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val bytes = inputSource.openStream().use { it.readAllBytes() }
|
||||||
|
|
||||||
|
ResourceFile.BinaryResourceFile(name, bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return ResourceFileEditor(resourceFile as T).also {
|
||||||
|
lockedResourceFileNames.add(name)
|
||||||
|
}
|
||||||
|
} catch (e: ClassCastException) {
|
||||||
|
throw ApkResourceException.Decode("Resource file $name is not ${resourceFile::class}.", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ResourceFileEditor<T : ResourceFile> internal constructor(
|
||||||
|
private val resourceFile: T,
|
||||||
|
) : Closeable {
|
||||||
|
fun use(block: (T) -> Unit) = block(resourceFile)
|
||||||
|
override fun close() {
|
||||||
|
lockedResourceFileNames.remove(resourceFile.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun flush() {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a resource file, creating it if the file does not exist.
|
||||||
|
*
|
||||||
|
* @param path The resource file path.
|
||||||
|
* @return The corresponding [ResourceFiles],
|
||||||
|
*/
|
||||||
|
fun openFile(path: String) = ResourceFiles(createHandle(path), archive)
|
||||||
|
|
||||||
|
private fun getPackageBlock() = packageBlock ?: throw ApkResourceException.MissingResourceTable
|
||||||
|
|
||||||
|
internal fun getOrCreateString(value: String) =
|
||||||
|
tableBlock?.stringPool?.getOrCreate(value) ?: throw ApkResourceException.MissingResourceTable
|
||||||
|
|
||||||
|
private fun Entry.set(resource: Resource) {
|
||||||
|
val existingEntryNameReference = specReference
|
||||||
|
|
||||||
|
// Sets this.specReference if the entry is not yet initialized.
|
||||||
|
// Sets this.specReference to 0 if the resource type of the existing entry changes.
|
||||||
|
ensureComplex(resource.isComplex)
|
||||||
|
|
||||||
|
if (existingEntryNameReference != 0) {
|
||||||
|
// Preserve the entry name by restoring the previous spec block reference (if present).
|
||||||
|
specReference = existingEntryNameReference
|
||||||
|
}
|
||||||
|
|
||||||
|
resource.write(this, this@ResourceContainer)
|
||||||
|
resourceTable.registerChanged(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve an [Entry] from the resource table.
|
||||||
|
*
|
||||||
|
* @param type The resource type.
|
||||||
|
* @param name The resource name.
|
||||||
|
* @param qualifiers The variant to use.
|
||||||
|
*/
|
||||||
|
private fun getEntry(type: String, name: String, qualifiers: String?): Entry? {
|
||||||
|
val resourceId = try {
|
||||||
|
resourceTable.resolve("@$type/$name")
|
||||||
|
} catch (_: ApkResourceException.InvalidReference) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val config = ResConfig.parse(qualifiers)
|
||||||
|
return tableBlock?.resolveReference(resourceId)?.singleOrNull { it.resConfig == config }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a [ResourceFiles.Handle] that can be used to open a [ResourceFiles].
|
||||||
|
* This may involve looking it up in the resource table to find the actual location in the archive.
|
||||||
|
*
|
||||||
|
* @param path The path of the resource.
|
||||||
|
*/
|
||||||
|
private fun createHandle(path: String): ResourceFiles.Handle {
|
||||||
|
if (path.startsWith("res/values")) throw ApkResourceException.Decode("Decoding the resource table as a file is not supported")
|
||||||
|
|
||||||
|
var onClose = {}
|
||||||
|
var archivePath = path
|
||||||
|
|
||||||
|
if (tableBlock != null && path.startsWith("res/") && path.count { it == '/' } == 2) {
|
||||||
|
val file = File(path)
|
||||||
|
|
||||||
|
val qualifiers = EncodeUtil.getQualifiersFromResFile(file)
|
||||||
|
val type = EncodeUtil.getTypeNameFromResFile(file)
|
||||||
|
val name = file.nameWithoutExtension
|
||||||
|
|
||||||
|
// The resource file names that the app developers used may have been minified, so we have to resolve it with the resource table.
|
||||||
|
// Example: res/drawable-hdpi/icon.png -> res/4a.png
|
||||||
|
getEntry(type, name, qualifiers)?.resValue?.valueAsString?.let {
|
||||||
|
archivePath = it
|
||||||
|
} ?: run {
|
||||||
|
// An entry for this specific resource file was not found in the resource table, so we have to register it after we save.
|
||||||
|
onClose = { setResource(type, name, StringResource(archivePath), qualifiers) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResourceFiles.Handle(path, archivePath, onClose)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setResource(type: String, entryName: String, resource: Resource, qualifiers: String? = null) =
|
||||||
|
getPackageBlock().getOrCreate(qualifiers, type, entryName).also { it.set(resource) }.resourceId
|
||||||
|
|
||||||
|
fun setResources(type: String, resources: Map<String, Resource>, configuration: String? = null) {
|
||||||
|
getPackageBlock().getOrCreateSpecTypePair(type).getOrCreateTypeBlock(configuration).apply {
|
||||||
|
resources.forEach { (entryName, resource) -> getOrCreateEntry(entryName).set(resource) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun flush() {
|
||||||
|
packageBlock?.name = archive
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,91 @@
|
|||||||
|
package app.revanced.arsc.resource
|
||||||
|
|
||||||
|
import app.revanced.arsc.ApkResourceException
|
||||||
|
import app.revanced.arsc.archive.Archive
|
||||||
|
import com.reandroid.archive.InputSource
|
||||||
|
import com.reandroid.xml.XMLDocument
|
||||||
|
import com.reandroid.xml.XMLException
|
||||||
|
import java.io.*
|
||||||
|
|
||||||
|
|
||||||
|
abstract class ResourceFile(val name: String) {
|
||||||
|
internal var realName: String? = null
|
||||||
|
|
||||||
|
class XmlResourceFile(name: String, val document: XMLDocument) : ResourceFile(name)
|
||||||
|
class BinaryResourceFile(name: String, var bytes: ByteArray) : ResourceFile(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceFiles private constructor(
|
||||||
|
) : Closeable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate a [ResourceFiles].
|
||||||
|
*
|
||||||
|
* @param handle The [Handle] associated with this file.
|
||||||
|
* @param archive The [Archive] that the file resides in.
|
||||||
|
*/
|
||||||
|
internal constructor(handle: Handle, archive: Archive) : this(
|
||||||
|
handle,
|
||||||
|
archive,
|
||||||
|
try {
|
||||||
|
archive.read(handle.archivePath)
|
||||||
|
} catch (e: XMLException) {
|
||||||
|
throw ApkResourceException.Decode("Failed to decode XML while reading ${handle.virtualPath}", e)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw ApkResourceException.Decode("Could not read ${handle.virtualPath}", e)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_BUFFER_SIZE = 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
var contents = readResult?.data ?: ByteArray(0)
|
||||||
|
set(value) {
|
||||||
|
pendingWrite = true
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
val exists = readResult != null
|
||||||
|
|
||||||
|
override fun toString() = handle.virtualPath
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
if (pendingWrite) {
|
||||||
|
val path = handle.archivePath
|
||||||
|
|
||||||
|
if (isXmlResource) archive.writeXml(
|
||||||
|
path,
|
||||||
|
try {
|
||||||
|
XMLDocument.load(inputStream())
|
||||||
|
} catch (e: XMLException) {
|
||||||
|
throw ApkResourceException.Encode("Failed to parse XML while writing ${handle.virtualPath}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
) else archive.writeRaw(path, contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle.onClose()
|
||||||
|
|
||||||
|
|
||||||
|
archive.unlock(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun inputStream(): InputStream = ByteArrayInputStream(contents)
|
||||||
|
|
||||||
|
fun outputStream(bufferSize: Int = DEFAULT_BUFFER_SIZE): OutputStream =
|
||||||
|
object : ByteArrayOutputStream(bufferSize) {
|
||||||
|
override fun close() {
|
||||||
|
this@ResourceFiles.contents = if (buf.size > count) buf.copyOf(count) else buf
|
||||||
|
super.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param virtualPath The resource file path. Example: /res/drawable-hdpi/icon.png.
|
||||||
|
* @param archivePath The actual file path in the archive. Example: res/4a.png.
|
||||||
|
* @param onClose An action to perform when the file associated with this handle is closed
|
||||||
|
*/
|
||||||
|
internal data class Handle(val virtualPath: String, val archivePath: String, val onClose: () -> Unit)
|
||||||
|
}
|
@@ -0,0 +1,100 @@
|
|||||||
|
package app.revanced.arsc.resource
|
||||||
|
|
||||||
|
import app.revanced.arsc.ApkResourceException
|
||||||
|
import com.reandroid.apk.xmlencoder.EncodeException
|
||||||
|
import com.reandroid.apk.xmlencoder.EncodeMaterials
|
||||||
|
import com.reandroid.arsc.util.FrameworkTable
|
||||||
|
import com.reandroid.arsc.value.Entry
|
||||||
|
import com.reandroid.common.TableEntryStore
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A high-level API for resolving resources in the resource table, which spans the entire ApkBundle.
|
||||||
|
*/
|
||||||
|
class ResourceTable(base: ResourceContainer, all: Sequence<ResourceContainer>) {
|
||||||
|
private val packageName = base.tableBlock!!.name
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [TableEntryStore] used to decode XML.
|
||||||
|
*/
|
||||||
|
internal val entryStore = TableEntryStore()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [EncodeMaterials] to use for resolving resources and encoding XML.
|
||||||
|
*/
|
||||||
|
internal val encodeMaterials: EncodeMaterials = object : EncodeMaterials() {
|
||||||
|
/*
|
||||||
|
Our implementation is more efficient because it does not have to loop through every single entry group
|
||||||
|
when the resource id cannot be found in the TableIdentifier, which does not update when you create a new resource.
|
||||||
|
It also looks at the entire table instead of just the current package.
|
||||||
|
*/
|
||||||
|
override fun resolveLocalResourceId(type: String, name: String) = resolveLocal(type, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The resource mappings which are generated when the ApkBundle is created.
|
||||||
|
*/
|
||||||
|
private val tableIdentifier = encodeMaterials.tableIdentifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A table of all the resources that have been changed or added.
|
||||||
|
*/
|
||||||
|
private val modifiedResources = HashMap<String, HashMap<String, Int>>()
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a resource id for the specified resource.
|
||||||
|
* Cannot resolve resources from the android framework.
|
||||||
|
*
|
||||||
|
* @param type The type of the resource.
|
||||||
|
* @param name The name of the resource.
|
||||||
|
* @return The id of the resource.
|
||||||
|
*/
|
||||||
|
fun resolveLocal(type: String, name: String) =
|
||||||
|
modifiedResources[type]?.get(name)
|
||||||
|
?: tableIdentifier.get(packageName, type, name)?.resourceId
|
||||||
|
?: throw ApkResourceException.InvalidReference(
|
||||||
|
type,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a resource id for the specified resource.
|
||||||
|
*
|
||||||
|
* @param reference The resource reference string.
|
||||||
|
* @return The id of the resource.
|
||||||
|
*/
|
||||||
|
fun resolve(reference: String) = try {
|
||||||
|
encodeMaterials.resolveReference(reference)
|
||||||
|
} catch (e: EncodeException) {
|
||||||
|
throw ApkResourceException.InvalidReference(reference, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the [ResourceTable] that an [Entry] has been created or modified.
|
||||||
|
*/
|
||||||
|
internal fun registerChanged(entry: Entry) {
|
||||||
|
modifiedResources.getOrPut(entry.typeName, ::HashMap)[entry.name] = entry.resourceId
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
all.forEach {
|
||||||
|
it.tableBlock?.let { table ->
|
||||||
|
entryStore.add(table)
|
||||||
|
tableIdentifier.load(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.resourceTable = this
|
||||||
|
}
|
||||||
|
|
||||||
|
base.also {
|
||||||
|
encodeMaterials.currentPackage = it.tableBlock
|
||||||
|
|
||||||
|
it.tableBlock!!.frameWorks.forEach { fw ->
|
||||||
|
if (fw is FrameworkTable) {
|
||||||
|
entryStore.add(fw)
|
||||||
|
encodeMaterials.addFramework(fw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,56 @@
|
|||||||
|
package app.revanced.arsc.xml
|
||||||
|
|
||||||
|
import app.revanced.arsc.resource.ResourceContainer
|
||||||
|
import app.revanced.arsc.resource.boolean
|
||||||
|
import com.reandroid.apk.xmlencoder.EncodeException
|
||||||
|
import com.reandroid.apk.xmlencoder.XMLEncodeSource
|
||||||
|
import com.reandroid.arsc.chunk.xml.ResXmlDocument
|
||||||
|
import com.reandroid.xml.XMLDocument
|
||||||
|
import com.reandroid.xml.XMLElement
|
||||||
|
import com.reandroid.xml.source.XMLDocumentSource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archive input source to lazily encode an [XMLDocument] after it has been modified.
|
||||||
|
*
|
||||||
|
* @param name The file name of this input source.
|
||||||
|
* @param document The [XMLDocument] to encode.
|
||||||
|
* @param resources The [ResourceContainer] to use for encoding.
|
||||||
|
*/
|
||||||
|
internal class LazyXMLEncodeSource(
|
||||||
|
name: String,
|
||||||
|
val document: XMLDocument,
|
||||||
|
private val resources: ResourceContainer
|
||||||
|
) : XMLEncodeSource(resources.resourceTable.encodeMaterials, XMLDocumentSource(name, document)) {
|
||||||
|
private var encoded = false
|
||||||
|
|
||||||
|
override fun getResXmlBlock(): ResXmlDocument {
|
||||||
|
if (encoded) return super.getResXmlBlock()
|
||||||
|
|
||||||
|
XMLEncodeSource(resources.resourceTable.encodeMaterials, XMLDocumentSource(name, document))
|
||||||
|
|
||||||
|
fun XMLElement.registerIds() {
|
||||||
|
listAttributes().forEach { attr ->
|
||||||
|
if (!attr.value.startsWith("@+id/")) return@forEach
|
||||||
|
|
||||||
|
val name = attr.value.split('/').last()
|
||||||
|
resources.setResource("id", name, boolean(false))
|
||||||
|
attr.value = "@id/$name"
|
||||||
|
}
|
||||||
|
|
||||||
|
listChildElements().forEach { it.registerIds() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle all @+id/id_name references in the document.
|
||||||
|
document.documentElement.registerIds()
|
||||||
|
|
||||||
|
encoded = true
|
||||||
|
|
||||||
|
// This will call XMLEncodeSource.getResXmlBlock(),
|
||||||
|
// which will encode the document if it has not already been encoded.
|
||||||
|
try {
|
||||||
|
return super.getResXmlBlock()
|
||||||
|
} catch (e: EncodeException) {
|
||||||
|
throw EncodeException("Failed to encode $name", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,3 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.9.0" apply false
|
kotlin("jvm") version "1.8.20" apply false
|
||||||
alias(libs.plugins.binary.compatibility.validator)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
allprojects {
|
|
||||||
apply(plugin = "maven-publish")
|
|
||||||
|
|
||||||
group = "app.revanced"
|
|
||||||
}
|
|
@@ -1,4 +1,4 @@
|
|||||||
org.gradle.parallel = true
|
org.gradle.parallel=true
|
||||||
org.gradle.caching = true
|
org.gradle.caching=true
|
||||||
kotlin.code.style = official
|
kotlin.code.style = official
|
||||||
version = 15.0.0-dev.2
|
version = 11.0.4
|
||||||
|
@@ -1,31 +0,0 @@
|
|||||||
[versions]
|
|
||||||
android = "4.1.1.4"
|
|
||||||
kotlin-reflect = "1.9.0"
|
|
||||||
apktool-lib = "2.8.2-6"
|
|
||||||
kotlin-test = "1.8.20-RC"
|
|
||||||
kotlinx-coroutines-core = "1.7.1"
|
|
||||||
multidexlib2 = "3.0.3.r2"
|
|
||||||
smali = "3.0.3"
|
|
||||||
symbol-processing-api = "1.9.0-1.0.11"
|
|
||||||
xpp3 = "1.1.4c"
|
|
||||||
binary-compatibility-validator = "0.13.2"
|
|
||||||
kotlin-compile-testing-ksp = "1.5.0"
|
|
||||||
kotlinpoet-ksp = "1.14.2"
|
|
||||||
ksp = "1.9.0-1.0.11"
|
|
||||||
|
|
||||||
[libraries]
|
|
||||||
android = { module = "com.google.android:android", version.ref = "android" }
|
|
||||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin-reflect" }
|
|
||||||
apktool-lib = { module = "app.revanced:apktool-lib", version.ref = "apktool-lib" }
|
|
||||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin-test" }
|
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines-core" }
|
|
||||||
multidexlib2 = { module = "app.revanced:multidexlib2", version.ref = "multidexlib2" }
|
|
||||||
smali = { module = "com.android.tools.smali:smali", version.ref = "smali" }
|
|
||||||
symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "symbol-processing-api" }
|
|
||||||
xpp3 = { module = "xpp3:xpp3", version.ref = "xpp3" }
|
|
||||||
kotlin-compile-testing = { module = "com.github.tschuchortdev:kotlin-compile-testing-ksp", version.ref = "kotlin-compile-testing-ksp" }
|
|
||||||
kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet-ksp" }
|
|
||||||
|
|
||||||
[plugins]
|
|
||||||
binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" }
|
|
||||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
|
@@ -1,25 +0,0 @@
|
|||||||
public abstract interface annotation class app/revanced/patcher/patch/annotations/CompatiblePackage : java/lang/annotation/Annotation {
|
|
||||||
public abstract fun name ()Ljava/lang/String;
|
|
||||||
public abstract fun versions ()[Ljava/lang/String;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract interface annotation class app/revanced/patcher/patch/annotations/Patch : java/lang/annotation/Annotation {
|
|
||||||
public abstract fun compatiblePackages ()[Lapp/revanced/patcher/patch/annotations/CompatiblePackage;
|
|
||||||
public abstract fun dependencies ()[Ljava/lang/Class;
|
|
||||||
public abstract fun description ()Ljava/lang/String;
|
|
||||||
public abstract fun name ()Ljava/lang/String;
|
|
||||||
public abstract fun requiresIntegrations ()Z
|
|
||||||
public abstract fun use ()Z
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class app/revanced/patcher/patch/annotations/processor/PatchProcessor : com/google/devtools/ksp/processing/SymbolProcessor {
|
|
||||||
public fun <init> (Lcom/google/devtools/ksp/processing/CodeGenerator;Lcom/google/devtools/ksp/processing/KSPLogger;)V
|
|
||||||
public fun process (Lcom/google/devtools/ksp/processing/Resolver;)Ljava/util/List;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final class app/revanced/patcher/patch/annotations/processor/PatchProcessorProvider : com/google/devtools/ksp/processing/SymbolProcessorProvider {
|
|
||||||
public fun <init> ()V
|
|
||||||
public fun create (Lcom/google/devtools/ksp/processing/SymbolProcessorEnvironment;)Lapp/revanced/patcher/patch/annotations/processor/PatchProcessor;
|
|
||||||
public synthetic fun create (Lcom/google/devtools/ksp/processing/SymbolProcessorEnvironment;)Lcom/google/devtools/ksp/processing/SymbolProcessor;
|
|
||||||
}
|
|
||||||
|
|
@@ -1,74 +0,0 @@
|
|||||||
plugins {
|
|
||||||
kotlin("jvm") version "1.9.0"
|
|
||||||
alias(libs.plugins.ksp)
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(libs.symbol.processing.api)
|
|
||||||
implementation(libs.kotlinpoet.ksp)
|
|
||||||
implementation(project(":revanced-patcher"))
|
|
||||||
|
|
||||||
testImplementation(libs.kotlin.test)
|
|
||||||
testImplementation(libs.kotlin.compile.testing)
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks {
|
|
||||||
test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
testLogging {
|
|
||||||
events("PASSED", "SKIPPED", "FAILED")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlin { jvmToolchain(11) }
|
|
||||||
|
|
||||||
java {
|
|
||||||
withSourcesJar()
|
|
||||||
}
|
|
||||||
|
|
||||||
publishing {
|
|
||||||
repositories {
|
|
||||||
mavenLocal()
|
|
||||||
maven {
|
|
||||||
name = "GitHubPackages"
|
|
||||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
|
||||||
credentials {
|
|
||||||
username = System.getenv("GITHUB_ACTOR")
|
|
||||||
password = System.getenv("GITHUB_TOKEN")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
publications {
|
|
||||||
create<MavenPublication>("gpr") {
|
|
||||||
from(components["java"])
|
|
||||||
|
|
||||||
version = project.version.toString()
|
|
||||||
|
|
||||||
pom {
|
|
||||||
name = "ReVanced Patch annotations processor"
|
|
||||||
description = "Annotation processor for patches."
|
|
||||||
url = "https://revanced.app"
|
|
||||||
|
|
||||||
licenses {
|
|
||||||
license {
|
|
||||||
name = "GNU General Public License v3.0"
|
|
||||||
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
developers {
|
|
||||||
developer {
|
|
||||||
id = "ReVanced"
|
|
||||||
name = "ReVanced"
|
|
||||||
email = "contact@revanced.app"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scm {
|
|
||||||
connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
|
|
||||||
developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
|
|
||||||
url = "https://github.com/revanced/revanced-patcher"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,2 +0,0 @@
|
|||||||
rootProject.name = "revanced-patch-annotations-processor"
|
|
||||||
|
|
@@ -1,38 +0,0 @@
|
|||||||
package app.revanced.patcher.patch.annotations
|
|
||||||
|
|
||||||
import java.lang.annotation.Inherited
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Annotation for [app.revanced.patcher.patch.Patch] classes.
|
|
||||||
*
|
|
||||||
* @param name The name of the patch. If empty, the patch will be unnamed.
|
|
||||||
* @param description The description of the patch. If empty, no description will be used.
|
|
||||||
* @param dependencies The patches this patch depends on.
|
|
||||||
* @param compatiblePackages The packages this patch is compatible with.
|
|
||||||
* @param use Whether this patch should be used.
|
|
||||||
* @param requiresIntegrations Whether this patch requires integrations.
|
|
||||||
*/
|
|
||||||
@Retention(AnnotationRetention.SOURCE)
|
|
||||||
@Target(AnnotationTarget.CLASS)
|
|
||||||
@Inherited
|
|
||||||
annotation class Patch(
|
|
||||||
val name: String = "",
|
|
||||||
val description: String = "",
|
|
||||||
val dependencies: Array<KClass<out app.revanced.patcher.patch.Patch<*>>> = [],
|
|
||||||
val compatiblePackages: Array<CompatiblePackage> = [],
|
|
||||||
val use: Boolean = true,
|
|
||||||
// TODO: Remove this property, once integrations are coupled with patches.
|
|
||||||
val requiresIntegrations: Boolean = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A package that a [app.revanced.patcher.patch.Patch] is compatible with.
|
|
||||||
*
|
|
||||||
* @param name The name of the package.
|
|
||||||
* @param versions The versions of the package.
|
|
||||||
*/
|
|
||||||
annotation class CompatiblePackage(
|
|
||||||
val name: String,
|
|
||||||
val versions: Array<String> = [],
|
|
||||||
)
|
|
@@ -1,205 +0,0 @@
|
|||||||
package app.revanced.patcher.patch.annotations.processor
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.BytecodeContext
|
|
||||||
import app.revanced.patcher.data.ResourceContext
|
|
||||||
import app.revanced.patcher.patch.BytecodePatch
|
|
||||||
import app.revanced.patcher.patch.ResourcePatch
|
|
||||||
import app.revanced.patcher.patch.annotations.Patch
|
|
||||||
import com.google.devtools.ksp.processing.*
|
|
||||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
|
||||||
import com.google.devtools.ksp.symbol.KSAnnotation
|
|
||||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
|
||||||
import com.google.devtools.ksp.symbol.KSType
|
|
||||||
import com.google.devtools.ksp.validate
|
|
||||||
import com.squareup.kotlinpoet.*
|
|
||||||
import com.squareup.kotlinpoet.ksp.toClassName
|
|
||||||
import com.squareup.kotlinpoet.ksp.writeTo
|
|
||||||
import kotlin.reflect.KClass
|
|
||||||
|
|
||||||
class PatchProcessor(
|
|
||||||
private val codeGenerator: CodeGenerator,
|
|
||||||
private val logger: KSPLogger
|
|
||||||
) : SymbolProcessor {
|
|
||||||
|
|
||||||
private fun KSAnnotated.isSubclassOf(cls: KClass<*>): Boolean {
|
|
||||||
if (this !is KSClassDeclaration) return false
|
|
||||||
|
|
||||||
if (qualifiedName?.asString() == cls.qualifiedName) return true
|
|
||||||
|
|
||||||
return superTypes.any { it.resolve().declaration.isSubclassOf(cls) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
|
||||||
val executablePatches = buildMap {
|
|
||||||
resolver.getSymbolsWithAnnotation(Patch::class.qualifiedName!!).filter {
|
|
||||||
// Do not check here if Patch is super of the class, because it is expensive.
|
|
||||||
// Check it later when processing.
|
|
||||||
it.validate() && it.isSubclassOf(app.revanced.patcher.patch.Patch::class)
|
|
||||||
}.map {
|
|
||||||
it as KSClassDeclaration
|
|
||||||
}.forEach { patchDeclaration ->
|
|
||||||
patchDeclaration.annotations.find {
|
|
||||||
it.annotationType.resolve().declaration.qualifiedName!!.asString() == Patch::class.qualifiedName!!
|
|
||||||
}?.let { annotation ->
|
|
||||||
fun KSAnnotation.property(name: String) =
|
|
||||||
arguments.find { it.name!!.asString() == name }?.value!!
|
|
||||||
|
|
||||||
val name =
|
|
||||||
annotation.property("name").toString().ifEmpty { null }
|
|
||||||
|
|
||||||
val description =
|
|
||||||
annotation.property("description").toString().ifEmpty { null }
|
|
||||||
|
|
||||||
val dependencies =
|
|
||||||
(annotation.property("dependencies") as List<KSType>).ifEmpty { null }
|
|
||||||
|
|
||||||
val compatiblePackages =
|
|
||||||
(annotation.property("compatiblePackages") as List<KSAnnotation>).ifEmpty { null }
|
|
||||||
|
|
||||||
val use =
|
|
||||||
annotation.property("use") as Boolean
|
|
||||||
|
|
||||||
val requiresIntegrations =
|
|
||||||
annotation.property("requiresIntegrations") as Boolean
|
|
||||||
|
|
||||||
// Data class for KotlinPoet
|
|
||||||
data class PatchData(
|
|
||||||
val name: String?,
|
|
||||||
val description: String?,
|
|
||||||
val dependencies: List<ClassName>?,
|
|
||||||
val compatiblePackages: List<CodeBlock>?,
|
|
||||||
val use: Boolean,
|
|
||||||
val requiresIntegrations: Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
this[patchDeclaration] = PatchData(
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
dependencies?.map { dependency -> dependency.toClassName() },
|
|
||||||
compatiblePackages?.map {
|
|
||||||
val packageName = it.property("name")
|
|
||||||
val packageVersions = (it.property("versions") as List<String>)
|
|
||||||
.joinToString(", ") { version -> "\"$version\"" }
|
|
||||||
|
|
||||||
CodeBlock.of(
|
|
||||||
"%T(%S, setOf(%L))",
|
|
||||||
app.revanced.patcher.patch.Patch.CompatiblePackage::class,
|
|
||||||
packageName,
|
|
||||||
packageVersions
|
|
||||||
)
|
|
||||||
},
|
|
||||||
use,
|
|
||||||
requiresIntegrations
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a patch depends on another, that is annotated, the dependency should be replaced with the generated patch,
|
|
||||||
// because the generated patch has all the necessary properties to invoke the super constructor,
|
|
||||||
// unlike the annotated patch.
|
|
||||||
val dependencyResolutionMap = buildMap {
|
|
||||||
executablePatches.values.filter { it.dependencies != null }.flatMap {
|
|
||||||
it.dependencies!!
|
|
||||||
}.distinct().forEach { dependency ->
|
|
||||||
executablePatches.keys.find { it.qualifiedName?.asString() == dependency.toString() }
|
|
||||||
?.let { patch ->
|
|
||||||
this[dependency] = ClassName(
|
|
||||||
patch.packageName.asString(),
|
|
||||||
patch.simpleName.asString() + "Generated"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
executablePatches.forEach { (patchDeclaration, patchAnnotation) ->
|
|
||||||
val isBytecodePatch = patchDeclaration.isSubclassOf(BytecodePatch::class)
|
|
||||||
|
|
||||||
val superClass = if (isBytecodePatch) {
|
|
||||||
BytecodePatch::class
|
|
||||||
} else {
|
|
||||||
ResourcePatch::class
|
|
||||||
}
|
|
||||||
|
|
||||||
val contextClass = if (isBytecodePatch) {
|
|
||||||
BytecodeContext::class
|
|
||||||
} else {
|
|
||||||
ResourceContext::class
|
|
||||||
}
|
|
||||||
|
|
||||||
val generatedPatchClassName = ClassName(
|
|
||||||
patchDeclaration.packageName.asString(),
|
|
||||||
patchDeclaration.simpleName.asString() + "Generated"
|
|
||||||
)
|
|
||||||
|
|
||||||
FileSpec.builder(generatedPatchClassName)
|
|
||||||
.addType(
|
|
||||||
TypeSpec.objectBuilder(generatedPatchClassName)
|
|
||||||
.superclass(superClass).apply {
|
|
||||||
patchAnnotation.name?.let { name ->
|
|
||||||
addSuperclassConstructorParameter("name = %S", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
patchAnnotation.description?.let { description ->
|
|
||||||
addSuperclassConstructorParameter("description = %S", description)
|
|
||||||
}
|
|
||||||
|
|
||||||
patchAnnotation.compatiblePackages?.let { compatiblePackages ->
|
|
||||||
addSuperclassConstructorParameter(
|
|
||||||
"compatiblePackages = setOf(%L)",
|
|
||||||
compatiblePackages.joinToString(", ")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
patchAnnotation.dependencies?.let { dependencies ->
|
|
||||||
addSuperclassConstructorParameter(
|
|
||||||
"dependencies = setOf(%L)",
|
|
||||||
buildList {
|
|
||||||
addAll(dependencies)
|
|
||||||
// Also add the source class of the generated class so that it is also executed.
|
|
||||||
add(patchDeclaration.toClassName())
|
|
||||||
}.joinToString(", ") { dependency ->
|
|
||||||
"${(dependencyResolutionMap[dependency] ?: dependency)}::class"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
addSuperclassConstructorParameter(
|
|
||||||
"use = %L", patchAnnotation.use
|
|
||||||
)
|
|
||||||
|
|
||||||
addSuperclassConstructorParameter(
|
|
||||||
"requiresIntegrations = %L",
|
|
||||||
patchAnnotation.requiresIntegrations
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.addFunction(
|
|
||||||
FunSpec.builder("execute")
|
|
||||||
.addModifiers(KModifier.OVERRIDE)
|
|
||||||
.addParameter("context", contextClass)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.addInitializerBlock(
|
|
||||||
CodeBlock.builder()
|
|
||||||
.add(
|
|
||||||
"%T.options.forEach { (key, option) ->",
|
|
||||||
patchDeclaration.toClassName()
|
|
||||||
)
|
|
||||||
.addStatement(
|
|
||||||
"options.register(option)"
|
|
||||||
)
|
|
||||||
.add(
|
|
||||||
"}"
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
).build().writeTo(
|
|
||||||
codeGenerator,
|
|
||||||
Dependencies(false, patchDeclaration.containingFile!!)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
package app.revanced.patcher.patch.annotations.processor
|
|
||||||
|
|
||||||
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
|
|
||||||
import com.google.devtools.ksp.processing.SymbolProcessorProvider
|
|
||||||
|
|
||||||
class PatchProcessorProvider : SymbolProcessorProvider {
|
|
||||||
override fun create(environment: SymbolProcessorEnvironment) =
|
|
||||||
PatchProcessor(environment.codeGenerator, environment.logger)
|
|
||||||
}
|
|
@@ -1 +0,0 @@
|
|||||||
app.revanced.patcher.patch.annotations.processor.PatchProcessorProvider
|
|
@@ -1,130 +0,0 @@
|
|||||||
package app.revanced.patcher.patch.annotations.processor
|
|
||||||
|
|
||||||
import app.revanced.patcher.patch.Patch
|
|
||||||
import com.tschuchort.compiletesting.KotlinCompilation
|
|
||||||
import com.tschuchort.compiletesting.SourceFile
|
|
||||||
import com.tschuchort.compiletesting.kspWithCompilation
|
|
||||||
import com.tschuchort.compiletesting.symbolProcessorProviders
|
|
||||||
import kotlin.test.Test
|
|
||||||
import kotlin.test.assertEquals
|
|
||||||
import kotlin.test.assertNull
|
|
||||||
|
|
||||||
class TestPatchAnnotationProcessor {
|
|
||||||
// region Processing
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testProcessing() = assertEquals(
|
|
||||||
"Processable patch", compile(
|
|
||||||
getSourceFile(
|
|
||||||
"processing", "ProcessablePatch"
|
|
||||||
)
|
|
||||||
).loadPatch("$SAMPLE_PACKAGE.processing.ProcessablePatchGenerated").name
|
|
||||||
)
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Dependencies
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testDependencies() {
|
|
||||||
compile(
|
|
||||||
getSourceFile(
|
|
||||||
"dependencies", "DependentPatch"
|
|
||||||
), getSourceFile(
|
|
||||||
"dependencies", "DependencyPatch"
|
|
||||||
)
|
|
||||||
).let { result ->
|
|
||||||
result.loadPatch("$SAMPLE_PACKAGE.dependencies.DependentPatchGenerated").let {
|
|
||||||
// Dependency as well as the source class of the generated class.
|
|
||||||
assertEquals(
|
|
||||||
2,
|
|
||||||
it.dependencies!!.size
|
|
||||||
)
|
|
||||||
|
|
||||||
// The last dependency is always the source class of the generated class to respect
|
|
||||||
// order of dependencies.
|
|
||||||
assertEquals(
|
|
||||||
result.loadPatch("$SAMPLE_PACKAGE.dependencies.DependentPatch")::class,
|
|
||||||
it.dependencies!!.last()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Options
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testOptions() {
|
|
||||||
val patch = compile(
|
|
||||||
getSourceFile(
|
|
||||||
"options", "OptionsPatch"
|
|
||||||
)
|
|
||||||
).loadPatch("$SAMPLE_PACKAGE.options.OptionsPatchGenerated")
|
|
||||||
|
|
||||||
assert(patch.options.isNotEmpty())
|
|
||||||
assertEquals(patch.options["print"].title, "Print message")
|
|
||||||
}
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region Limitations
|
|
||||||
@Test
|
|
||||||
fun failingManualDependency() = assertNull(
|
|
||||||
compile(
|
|
||||||
getSourceFile(
|
|
||||||
"limitations/manualdependency", "DependentPatch"
|
|
||||||
), getSourceFile(
|
|
||||||
"limitations/manualdependency", "DependencyPatch"
|
|
||||||
)
|
|
||||||
).loadPatch("$SAMPLE_PACKAGE.limitations.manualdependency.DependentPatchGenerated").dependencies
|
|
||||||
)
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
private companion object Utils {
|
|
||||||
const val SAMPLE_PACKAGE = "app.revanced.patcher.patch.annotations.processor.samples"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a source file from the given sample and class name.
|
|
||||||
*
|
|
||||||
* @param sample The sample to get the source file from.
|
|
||||||
* @param className The name of the class to get the source file from.
|
|
||||||
* @return The source file.
|
|
||||||
*/
|
|
||||||
fun getSourceFile(sample: String, className: String) = SourceFile.kotlin(
|
|
||||||
"$className.kt", TestPatchAnnotationProcessor::class.java.classLoader.getResourceAsStream(
|
|
||||||
"app/revanced/patcher/patch/annotations/processor/samples/$sample/$className.kt"
|
|
||||||
)?.readAllBytes()?.toString(Charsets.UTF_8) ?: error("Could not find resource $className")
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compile the given source files and return the result.
|
|
||||||
*
|
|
||||||
* @param sourceFiles The source files to compile.
|
|
||||||
* @return The result of the compilation.
|
|
||||||
*/
|
|
||||||
fun compile(vararg sourceFiles: SourceFile) = KotlinCompilation().apply {
|
|
||||||
sources = sourceFiles.asList()
|
|
||||||
|
|
||||||
symbolProcessorProviders = listOf(PatchProcessorProvider())
|
|
||||||
|
|
||||||
// Required until https://github.com/tschuchortdev/kotlin-compile-testing/issues/312 closed.
|
|
||||||
kspWithCompilation = true
|
|
||||||
|
|
||||||
inheritClassPath = true
|
|
||||||
messageOutputStream = System.out
|
|
||||||
}.compile().also { result ->
|
|
||||||
assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Class loading
|
|
||||||
|
|
||||||
fun KotlinCompilation.Result.loadPatch(name: String) = classLoader.loadClass(name).loadPatch()
|
|
||||||
|
|
||||||
fun Class<*>.loadPatch() = this.getField("INSTANCE").get(null) as Patch<*>
|
|
||||||
|
|
||||||
// endregion
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
package app.revanced.patcher.patch.annotations.processor.samples.dependencies
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.ResourceContext
|
|
||||||
import app.revanced.patcher.patch.ResourcePatch
|
|
||||||
import app.revanced.patcher.patch.annotations.Patch
|
|
||||||
|
|
||||||
@Patch(name = "Dependency patch")
|
|
||||||
object DependencyPatch : ResourcePatch() {
|
|
||||||
override fun execute(context: ResourceContext) {}
|
|
||||||
}
|
|
@@ -1,12 +0,0 @@
|
|||||||
package app.revanced.patcher.patch.annotations.processor.samples.dependencies
|
|
||||||
import app.revanced.patcher.data.BytecodeContext
|
|
||||||
import app.revanced.patcher.patch.BytecodePatch
|
|
||||||
import app.revanced.patcher.patch.annotations.Patch
|
|
||||||
|
|
||||||
@Patch(
|
|
||||||
name = "Dependent patch",
|
|
||||||
dependencies = [DependencyPatch::class],
|
|
||||||
)
|
|
||||||
object DependentPatch : BytecodePatch() {
|
|
||||||
override fun execute(context: BytecodeContext) {}
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
package app.revanced.patcher.patch.annotations.processor.samples.limitations.manualdependency
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.ResourceContext
|
|
||||||
import app.revanced.patcher.patch.ResourcePatch
|
|
||||||
import app.revanced.patcher.patch.annotations.Patch
|
|
||||||
|
|
||||||
@Patch(name = "Dependency patch")
|
|
||||||
object DependencyPatch : ResourcePatch() {
|
|
||||||
override fun execute(context: ResourceContext) { }
|
|
||||||
}
|
|
@@ -1,17 +0,0 @@
|
|||||||
package app.revanced.patcher.patch.annotations.processor.samples.limitations.manualdependency
|
|
||||||
import app.revanced.patcher.data.BytecodeContext
|
|
||||||
import app.revanced.patcher.patch.BytecodePatch
|
|
||||||
import app.revanced.patcher.patch.annotations.Patch
|
|
||||||
|
|
||||||
@Patch(name = "Dependent patch")
|
|
||||||
object DependentPatch : BytecodePatch(
|
|
||||||
// Dependency will not be executed correctly if it is manually specified.
|
|
||||||
// The reason for this is that the dependency patch is annotated too,
|
|
||||||
// so the processor will generate a new patch class for it embedding the annotated information.
|
|
||||||
// Because the dependency is manually specified,
|
|
||||||
// the processor will not be able to change this dependency to the generated class,
|
|
||||||
// which means that the dependency will lose the annotated information.
|
|
||||||
dependencies = setOf(DependencyPatch::class)
|
|
||||||
) {
|
|
||||||
override fun execute(context: BytecodeContext) {}
|
|
||||||
}
|
|
@@ -1,19 +0,0 @@
|
|||||||
package app.revanced.patcher.patch.annotations.processor.samples.options
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.ResourceContext
|
|
||||||
import app.revanced.patcher.patch.ResourcePatch
|
|
||||||
import app.revanced.patcher.patch.annotations.Patch
|
|
||||||
import app.revanced.patcher.patch.options.types.StringPatchOption.Companion.stringPatchOption
|
|
||||||
|
|
||||||
@Patch(name = "Options patch")
|
|
||||||
object OptionsPatch : ResourcePatch() {
|
|
||||||
override fun execute(context: ResourceContext) {}
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
private val printOption by stringPatchOption(
|
|
||||||
"print",
|
|
||||||
null,
|
|
||||||
"Print message",
|
|
||||||
"The message to print."
|
|
||||||
)
|
|
||||||
}
|
|
@@ -1,10 +0,0 @@
|
|||||||
package app.revanced.patcher.patch.annotations.processor.samples.processing
|
|
||||||
|
|
||||||
import app.revanced.patcher.data.BytecodeContext
|
|
||||||
import app.revanced.patcher.patch.BytecodePatch
|
|
||||||
import app.revanced.patcher.patch.annotations.Patch
|
|
||||||
|
|
||||||
@Patch("Processable patch")
|
|
||||||
object ProcessablePatch : BytecodePatch() {
|
|
||||||
override fun execute(context: BytecodeContext) {}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,22 @@
|
|||||||
plugins {
|
plugins {
|
||||||
kotlin("jvm") version "1.9.0"
|
kotlin("jvm")
|
||||||
|
`maven-publish`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
group = "app.revanced"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation("xpp3:xpp3:1.1.4c")
|
||||||
implementation(libs.xpp3)
|
implementation("app.revanced:smali:2.5.3-a3836654")
|
||||||
implementation(libs.smali)
|
implementation("app.revanced:multidexlib2:2.5.3-a3836654")
|
||||||
implementation(libs.multidexlib2)
|
implementation("io.github.reandroid:ARSCLib:1.1.7")
|
||||||
implementation(libs.apktool.lib)
|
implementation(project(":arsclib-utils"))
|
||||||
implementation(libs.kotlin.reflect)
|
|
||||||
|
|
||||||
compileOnly(libs.android)
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
|
||||||
|
implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.20-RC")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.20-RC")
|
||||||
|
|
||||||
testImplementation(project(":revanced-patch-annotations-processor"))
|
compileOnly("com.google.android:android:4.1.1.4")
|
||||||
testImplementation(libs.kotlin.test)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks {
|
tasks {
|
||||||
@@ -23,60 +26,36 @@ tasks {
|
|||||||
events("PASSED", "SKIPPED", "FAILED")
|
events("PASSED", "SKIPPED", "FAILED")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processResources {
|
processResources {
|
||||||
expand("projectVersion" to project.version)
|
expand("projectVersion" to project.version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlin { jvmToolchain(11) }
|
|
||||||
|
|
||||||
java {
|
java {
|
||||||
withSourcesJar()
|
withSourcesJar()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(11)
|
||||||
|
}
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
repositories {
|
repositories {
|
||||||
mavenLocal()
|
if (System.getenv("GITHUB_ACTOR") != null)
|
||||||
maven {
|
maven {
|
||||||
name = "GitHubPackages"
|
name = "GitHubPackages"
|
||||||
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
url = uri("https://maven.pkg.github.com/revanced/revanced-patcher")
|
||||||
credentials {
|
credentials {
|
||||||
username = System.getenv("GITHUB_ACTOR")
|
username = System.getenv("GITHUB_ACTOR")
|
||||||
password = System.getenv("GITHUB_TOKEN")
|
password = System.getenv("GITHUB_TOKEN")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
mavenLocal()
|
||||||
}
|
}
|
||||||
publications {
|
publications {
|
||||||
create<MavenPublication>("gpr") {
|
register<MavenPublication>("gpr") {
|
||||||
from(components["java"])
|
from(components["java"])
|
||||||
|
|
||||||
version = project.version.toString()
|
|
||||||
|
|
||||||
pom {
|
|
||||||
name = "ReVanced Patcher"
|
|
||||||
description = "Patcher used by ReVanced."
|
|
||||||
url = "https://revanced.app"
|
|
||||||
|
|
||||||
licenses {
|
|
||||||
license {
|
|
||||||
name = "GNU General Public License v3.0"
|
|
||||||
url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
developers {
|
|
||||||
developer {
|
|
||||||
id = "ReVanced"
|
|
||||||
name = "ReVanced"
|
|
||||||
email = "contact@revanced.app"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scm {
|
|
||||||
connection = "scm:git:git://github.com/revanced/revanced-patcher.git"
|
|
||||||
developerConnection = "scm:git:git@github.com:revanced/revanced-patcher.git"
|
|
||||||
url = "https://github.com/revanced/revanced-patcher"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user