├── .idea ├── .name ├── .gitignore ├── codeStyles │ └── codeStyleConfig.xml ├── vcs.xml ├── kotlinc.xml ├── AndroidProjectSystem.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml └── gradle.xml ├── core ├── .gitignore ├── src │ ├── test │ │ ├── resources │ │ │ └── libs │ │ │ │ ├── license.txt │ │ │ │ ├── foo.jar │ │ │ │ └── external.jar │ │ └── kotlin │ │ │ └── sh │ │ │ └── christian │ │ │ └── aaraar │ │ │ ├── model │ │ │ ├── JniTest.kt │ │ │ ├── AssetsTest.kt │ │ │ ├── LintRulesTest.kt │ │ │ ├── classeditor │ │ │ │ ├── testutil.kt │ │ │ │ ├── ClasspathTest.kt │ │ │ │ └── metadata │ │ │ │ │ └── FieldMetadataTest.kt │ │ │ ├── ProguardTest.kt │ │ │ ├── ResourcesTest.kt │ │ │ ├── AarMetadataTest.kt │ │ │ ├── LibsTest.kt │ │ │ ├── ApiJarTest.kt │ │ │ ├── ClassesTest.kt │ │ │ ├── FileSetTest.kt │ │ │ ├── AndroidManifestTest.kt │ │ │ ├── NavigationJsonTest.kt │ │ │ ├── GenericJarArchiveTest.kt │ │ │ ├── RTxtTest.kt │ │ │ └── PublicTxtTest.kt │ │ │ ├── shading │ │ │ ├── util.kt │ │ │ ├── GenericJarArchiveServiceLoaderShaderTest.kt │ │ │ ├── GenericJarArchiveResourceShaderTest.kt │ │ │ └── GenericJarArchiveNonStandardTest.kt │ │ │ └── merger │ │ │ └── impl │ │ │ ├── GenericJarArchiveMergerTest.kt │ │ │ └── NavigationJsonMergerTest.kt │ └── main │ │ └── kotlin │ │ └── sh │ │ └── christian │ │ └── aaraar │ │ ├── shading │ │ ├── impl │ │ │ ├── transform │ │ │ │ ├── ClassDelete.kt │ │ │ │ ├── ReplacePattern.kt │ │ │ │ ├── AbstractResourcePattern.kt │ │ │ │ ├── ReplacePart.kt │ │ │ │ ├── JarProcessor.kt │ │ │ │ ├── ClassRename.kt │ │ │ │ ├── ResourceRename.kt │ │ │ │ ├── AbstractPattern.kt │ │ │ │ ├── JarProcessorChain.kt │ │ │ │ ├── AbstractClassPattern.kt │ │ │ │ ├── Transformable.kt │ │ │ │ ├── PathRemapper.kt │ │ │ │ └── PackageRemapper.kt │ │ │ ├── JarArchiveShader.kt │ │ │ ├── ClassesShader.kt │ │ │ ├── LibsShader.kt │ │ │ ├── AarArchiveShader.kt │ │ │ └── GenericJarArchiveShader.kt │ │ ├── Shader.kt │ │ └── pipeline │ │ │ ├── ClassFilesProcessor.kt │ │ │ ├── ResourceFileShader.kt │ │ │ ├── ClassFileFilter.kt │ │ │ ├── ServiceLoaderShader.kt │ │ │ ├── ClassFileShader.kt │ │ │ ├── KotlinModuleFilter.kt │ │ │ ├── ServiceLoaderFilter.kt │ │ │ ├── KotlinModuleShader.kt │ │ │ └── ResourceFilter.kt │ │ ├── merger │ │ ├── Merger.kt │ │ ├── ClassesAndLibsMerger.kt │ │ ├── ArchiveMerger.kt │ │ ├── impl │ │ │ ├── NoJarArchiveMerger.kt │ │ │ ├── ProguardMerger.kt │ │ │ ├── JniMerger.kt │ │ │ ├── AssetsMerger.kt │ │ │ ├── ApiJarMerger.kt │ │ │ ├── RTxtMerger.kt │ │ │ ├── NavigationJsonMerger.kt │ │ │ ├── LintRulesMerger.kt │ │ │ ├── PublicTxtMerger.kt │ │ │ ├── FileSetMerger.kt │ │ │ ├── ClassesMerger.kt │ │ │ ├── ArtifactArchiveMerger.kt │ │ │ ├── JarArchiveMerger.kt │ │ │ ├── GenericJarArchiveMerger.kt │ │ │ └── AndroidManifestMerger.kt │ │ └── MergeRules.kt │ │ ├── model │ │ ├── classeditor │ │ │ ├── FieldReference.kt │ │ │ ├── NewParameter.kt │ │ │ ├── Parameter.kt │ │ │ ├── MutableMemberReference.kt │ │ │ ├── MemberReference.kt │ │ │ ├── ConstructorReference.kt │ │ │ ├── Signature.kt │ │ │ ├── ParameterOwner.kt │ │ │ ├── MethodReference.kt │ │ │ ├── types │ │ │ │ └── platform.kt │ │ │ ├── Classpath.kt │ │ │ ├── ClassReference.kt │ │ │ ├── metadata │ │ │ │ └── util.kt │ │ │ ├── Attribute.kt │ │ │ ├── Modifier.kt │ │ │ └── MutableFieldReference.kt │ │ ├── Jni.kt │ │ ├── Assets.kt │ │ ├── LintRules.kt │ │ ├── ApiJar.kt │ │ ├── Classes.kt │ │ ├── bytearrays.kt │ │ ├── Proguard.kt │ │ ├── Libs.kt │ │ ├── AarMetadata.kt │ │ ├── Resources.kt │ │ ├── FileSet.kt │ │ ├── ShadeConfiguration.kt │ │ ├── AndroidManifest.kt │ │ ├── NavigationJson.kt │ │ ├── RTxt.kt │ │ └── PublicTxt.kt │ │ ├── Environment.kt │ │ └── utils │ │ ├── xml.kt │ │ ├── files.kt │ │ └── AarEntries.kt ├── gradle.properties └── build.gradle.kts ├── fixtures ├── .gitignore ├── src │ ├── animal │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ ├── Animal.java │ │ │ ├── Cat.java │ │ │ └── Dog.java │ ├── service │ │ ├── resources │ │ │ ├── META-INF │ │ │ │ └── services │ │ │ │ │ └── java.nio.file.spi.CustomService │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── tracklist.txt │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ ├── CustomService.java │ │ │ ├── MyCustomService.java │ │ │ └── RealCustomService.java │ ├── foo │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── Foo.java │ ├── foo2 │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── Foo.java │ ├── ktLibrary │ │ └── kotlin │ │ │ └── sh │ │ │ └── christian │ │ │ └── mylibrary │ │ │ ├── FooInternal.kt │ │ │ ├── FooMaker.kt │ │ │ ├── Immutable.kt │ │ │ ├── Foo.kt │ │ │ ├── FooInternalMaker.kt │ │ │ ├── Name.kt │ │ │ └── Name-Maker.kt │ ├── testFixtures │ │ └── kotlin │ │ │ └── sh │ │ │ └── christian │ │ │ └── aaraar │ │ │ └── utils │ │ │ ├── classeditor.kt │ │ │ ├── metadata.kt │ │ │ ├── fileSets.kt │ │ │ ├── assertions.kt │ │ │ └── virtualFs.kt │ └── annotations │ │ └── java │ │ └── com │ │ └── example │ │ └── RegExp.java └── build.gradle.kts ├── sample-lib ├── .idea │ ├── .name │ ├── .gitignore │ ├── kotlinc.xml │ ├── vcs.xml │ ├── misc.xml │ └── gradle.xml ├── library │ ├── .gitignore │ ├── consumer-rules.pro │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── sh │ │ │ │ └── christian │ │ │ │ └── samplelib │ │ │ │ ├── FooInternal.kt │ │ │ │ ├── Foo.kt │ │ │ │ └── contexts.kt │ │ │ └── res │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── colors.xml │ └── libs │ │ └── annotations-24.1.0.jar ├── helper-library │ ├── .gitignore │ ├── src │ │ ├── main │ │ │ ├── resources │ │ │ │ ├── include-me.txt │ │ │ │ └── exclude-me.txt │ │ │ ├── AndroidManifest.xml │ │ │ ├── res │ │ │ │ └── values │ │ │ │ │ └── strings.xml │ │ │ └── java │ │ │ │ └── sh │ │ │ │ └── christian │ │ │ │ └── helperlib │ │ │ │ └── helper.kt │ │ ├── debug │ │ │ └── java │ │ │ │ └── sh │ │ │ │ └── christian │ │ │ │ └── helperlib │ │ │ │ └── debug.kt │ │ └── release │ │ │ └── java │ │ │ └── sh │ │ │ └── christian │ │ │ └── helperlib │ │ │ └── secrets.kt │ ├── libs │ │ └── slf4j-api-2.0.6.jar │ └── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── build.gradle ├── gradle.properties ├── .gitignore └── settings.gradle ├── agp-compat ├── agp7 │ ├── .gitignore │ ├── gradle.properties │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── sh │ │ └── christian │ │ └── aaraar │ │ └── gradle │ │ └── agp │ │ ├── Agp7AndroidExtension.kt │ │ ├── Agp7AndroidVariant.kt │ │ ├── Agp7AndroidPackaging.kt │ │ └── Agp7.kt ├── agp8 │ ├── .gitignore │ ├── gradle.properties │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── sh │ │ └── christian │ │ └── aaraar │ │ └── gradle │ │ └── agp │ │ ├── Agp8AndroidExtension.kt │ │ ├── Agp8AndroidVariant.kt │ │ ├── Agp8AndroidPackaging.kt │ │ └── Agp8.kt └── base │ ├── .gitignore │ ├── gradle.properties │ ├── build.gradle.kts │ └── src │ └── main │ └── kotlin │ └── sh │ └── christian │ └── aaraar │ └── gradle │ └── agp │ ├── AndroidExtension.kt │ ├── AndroidPackaging.kt │ ├── AgpCompat.kt │ └── AndroidVariant.kt ├── docs ├── CNAME ├── changelog.md ├── assets │ ├── logo.png │ └── favicons │ │ ├── favicon.ico │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── mstile-150x150.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── browserconfig.xml │ │ └── site.webmanifest ├── license.md ├── overrides │ └── main.html ├── index.md ├── installation.md └── publishing-jar.md ├── gradle-plugin ├── .gitignore ├── gradle.properties ├── src │ └── main │ │ └── kotlin │ │ └── sh │ │ └── christian │ │ └── aaraar │ │ ├── packaging │ │ ├── PackagerLogger.kt │ │ └── PackagingEnvironment.kt │ │ └── gradle │ │ ├── VariantDescriptor.kt │ │ ├── ArtifactArchiveProcessor.kt │ │ ├── PackageJarTask.kt │ │ ├── agp.kt │ │ ├── PackageAarTask.kt │ │ ├── ClassNameArtifactArchiveProcessorFactory.kt │ │ ├── ArtifactTypeDependencyRules.kt │ │ └── ApiJarProcessor.kt └── build.gradle.kts ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── libs.versions.toml ├── gradle.properties ├── .gitignore ├── RELEASING.md ├── config └── detekt │ └── detekt.yml ├── settings.gradle.kts ├── README.md ├── release.sh ├── .github └── workflows │ ├── deploy_docs.yml │ ├── publish.yml │ └── ci.yml └── mkdocs.yml /.idea/.name: -------------------------------------------------------------------------------- 1 | aaraar -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /fixtures/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /sample-lib/.idea/.name: -------------------------------------------------------------------------------- 1 | samplelib -------------------------------------------------------------------------------- /agp-compat/agp7/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /agp-compat/agp8/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /agp-compat/base/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | aaraar.christian.sh 2 | -------------------------------------------------------------------------------- /gradle-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /sample-lib/library/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /sample-lib/helper-library/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | -------------------------------------------------------------------------------- /core/src/test/resources/libs/license.txt: -------------------------------------------------------------------------------- 1 | WTFPL 2 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | /Users/chr/Documents/aaraar-plugin/CHANGELOG.md -------------------------------------------------------------------------------- /sample-lib/library/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class sh.** { *; } 2 | -------------------------------------------------------------------------------- /sample-lib/helper-library/src/main/resources/include-me.txt: -------------------------------------------------------------------------------- 1 | keep me! 2 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /sample-lib/helper-library/src/main/resources/exclude-me.txt: -------------------------------------------------------------------------------- 1 | delete me! 2 | -------------------------------------------------------------------------------- /sample-lib/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/docs/assets/logo.png -------------------------------------------------------------------------------- /sample-lib/library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /core/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=AarAar Core 2 | POM_DESCRIPTION=Tools and models for merging aar and jar archives. 3 | -------------------------------------------------------------------------------- /fixtures/src/animal/java/com/example/Animal.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | public interface Animal { 4 | } 5 | -------------------------------------------------------------------------------- /sample-lib/helper-library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/assets/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/docs/assets/favicons/favicon.ico -------------------------------------------------------------------------------- /fixtures/src/animal/java/com/example/Cat.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | public class Cat implements Animal { 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/src/animal/java/com/example/Dog.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | public class Dog implements Animal { 4 | } 5 | -------------------------------------------------------------------------------- /gradle-plugin/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=AarAar Plugin 2 | POM_DESCRIPTION=A plugin for creating a merged aar file. 3 | -------------------------------------------------------------------------------- /agp-compat/agp7/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=AarAar AGP 7 2 | POM_DESCRIPTION=Compatibility layer for interacting with AGP 7. 3 | -------------------------------------------------------------------------------- /agp-compat/agp8/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=AarAar AGP 8 2 | POM_DESCRIPTION=Compatibility layer for interacting with AGP 8. 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /agp-compat/base/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=AarAar AGP Base 2 | POM_DESCRIPTION=Base compatibility layer for interacting with AGP. 3 | -------------------------------------------------------------------------------- /core/src/test/resources/libs/foo.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/core/src/test/resources/libs/foo.jar -------------------------------------------------------------------------------- /docs/assets/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/docs/assets/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /docs/assets/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/docs/assets/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /docs/assets/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/docs/assets/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /core/src/test/resources/libs/external.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/core/src/test/resources/libs/external.jar -------------------------------------------------------------------------------- /docs/assets/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/docs/assets/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /fixtures/src/service/resources/META-INF/services/java.nio.file.spi.CustomService: -------------------------------------------------------------------------------- 1 | com.example.MyCustomService 2 | com.example.RealCustomService -------------------------------------------------------------------------------- /sample-lib/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/sample-lib/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sample-lib/helper-library/src/debug/java/sh/christian/helperlib/debug.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.helperlib 2 | 3 | val DEBUG_KEY = "debug-key" 4 | -------------------------------------------------------------------------------- /docs/assets/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/docs/assets/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /sample-lib/helper-library/src/release/java/sh/christian/helperlib/secrets.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.helperlib 2 | 3 | val RELEASE_KEY = "release-key" 4 | -------------------------------------------------------------------------------- /sample-lib/library/libs/annotations-24.1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/sample-lib/library/libs/annotations-24.1.0.jar -------------------------------------------------------------------------------- /sample-lib/helper-library/libs/slf4j-api-2.0.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/christiandeange/aaraar/HEAD/sample-lib/helper-library/libs/slf4j-api-2.0.6.jar -------------------------------------------------------------------------------- /sample-lib/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.library" version "8.0.1" apply false 3 | id "org.jetbrains.kotlin.android" version "1.8.0" apply false 4 | } 5 | -------------------------------------------------------------------------------- /fixtures/src/service/java/com/example/CustomService.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | public abstract class CustomService { 4 | public abstract void onServiceLoaded(); 5 | } 6 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=512m 2 | 3 | kotlin.code.style=official 4 | 5 | POM_GROUP_ID=sh.christian.aaraar 6 | POM_VERSION=0.1.4-SNAPSHOT 7 | -------------------------------------------------------------------------------- /sample-lib/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 2 | android.nonTransitiveRClass=true 3 | android.useAndroidX=true 4 | kotlin.code.style=official 5 | -------------------------------------------------------------------------------- /fixtures/src/foo/java/com/example/Foo.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | public class Foo { 4 | void printHello() { 5 | System.out.println("Hello, world!"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fixtures/src/foo2/java/com/example/Foo.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | public class Foo { 4 | public void printHello() { 5 | System.out.println("Hello, Foo!"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sample-lib/helper-library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | helper 3 | helper 4 | 5 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /sample-lib/library/src/main/java/sh/christian/samplelib/FooInternal.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.samplelib 2 | 3 | class FooInternal { 4 | // This class is public for reasons but ideally would be private! 5 | } 6 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/ClassDelete.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | internal class ClassDelete(pattern: String) : AbstractClassPattern(pattern) 4 | -------------------------------------------------------------------------------- /sample-lib/library/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | samplelib 3 | 4 | -------------------------------------------------------------------------------- /fixtures/src/service/java/com/example/MyCustomService.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | public class MyCustomService extends CustomService { 4 | @Override 5 | public void onServiceLoaded() { 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sample-lib/.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /fixtures/src/ktLibrary/kotlin/sh/christian/mylibrary/FooInternal.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.mylibrary 2 | 3 | internal class FooInternal { 4 | internal fun printInternal() { 5 | println("Hello, internal!") 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sample-lib/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/ReplacePattern.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | internal interface ReplacePattern { 4 | fun replace(value: String): String? 5 | } 6 | -------------------------------------------------------------------------------- /fixtures/src/ktLibrary/kotlin/sh/christian/mylibrary/FooMaker.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Foos") 2 | 3 | package sh.christian.mylibrary 4 | 5 | fun newFoo() = Foo() 6 | 7 | @JvmField 8 | val twoFoos: Array = arrayOf(Foo(), Foo()) 9 | -------------------------------------------------------------------------------- /sample-lib/helper-library/src/main/java/sh/christian/helperlib/helper.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.helperlib 2 | 3 | import sh.christian.samplelib.helper.BuildConfig 4 | 5 | val HELPER_LIB_NAME = BuildConfig.LIBRARY_PACKAGE_NAME 6 | -------------------------------------------------------------------------------- /fixtures/src/ktLibrary/kotlin/sh/christian/mylibrary/Immutable.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.mylibrary 2 | 3 | @MustBeDocumented 4 | @Retention(value = AnnotationRetention.BINARY) 5 | @Target(AnnotationTarget.CLASS) 6 | annotation class Immutable 7 | -------------------------------------------------------------------------------- /.idea/AndroidProjectSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/sh/christian/aaraar/packaging/PackagerLogger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.packaging 2 | 3 | /** 4 | * Simple logger interface for the [Packager]. 5 | */ 6 | fun interface PackagerLogger { 7 | fun info(message: String) 8 | } 9 | -------------------------------------------------------------------------------- /fixtures/src/ktLibrary/kotlin/sh/christian/mylibrary/Foo.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.mylibrary 2 | 3 | class Foo { 4 | fun print() { 5 | println("Hello, public!") 6 | } 7 | 8 | internal fun printInternal() { 9 | println("Hello, internal!") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /fixtures/src/service/java/com/example/RealCustomService.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | public class RealCustomService extends CustomService { 4 | @Override 5 | public void onServiceLoaded() { 6 | System.out.println("RealCustomService loaded"); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /sample-lib/library/src/main/java/sh/christian/samplelib/Foo.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.samplelib 2 | 3 | class Foo { 4 | fun print() { 5 | System.out.println("print!") 6 | } 7 | fun printInternal() { 8 | System.out.println("print internal!") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/sh/christian/aaraar/gradle/VariantDescriptor.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle 2 | 3 | /** 4 | * Simple definition of an Android variant. 5 | */ 6 | data class VariantDescriptor( 7 | val name: String, 8 | val buildType: String?, 9 | ) 10 | -------------------------------------------------------------------------------- /sample-lib/library/src/main/java/sh/christian/samplelib/contexts.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.samplelib 2 | 3 | import android.content.Context 4 | import io.reactivex.rxjava3.core.Single 5 | 6 | fun Context.myAppPackage(): Single { 7 | return Single.fromCallable { packageName } 8 | } 9 | -------------------------------------------------------------------------------- /agp-compat/base/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | @Suppress("DSL_SCOPE_VIOLATION") val plugins = libs.plugins 3 | 4 | alias(plugins.kotlin.jvm) 5 | `kotlin-dsl` 6 | id("aaraar-detekt") 7 | id("aaraar-publish") 8 | } 9 | 10 | dependencies { 11 | api(platform(kotlin("bom"))) 12 | } 13 | -------------------------------------------------------------------------------- /sample-lib/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jan 11 18:52:15 EST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /fixtures/src/ktLibrary/kotlin/sh/christian/mylibrary/FooInternalMaker.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("FooInternals") 2 | 3 | package sh.christian.mylibrary 4 | 5 | internal fun newFooInternal() = FooInternal() 6 | 7 | @JvmField 8 | internal val twoFooInternals: Array = arrayOf(FooInternal(), FooInternal()) 9 | -------------------------------------------------------------------------------- /fixtures/src/ktLibrary/kotlin/sh/christian/mylibrary/Name.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.mylibrary 2 | 3 | class Name(var name: String) { 4 | fun printName() { 5 | println("Name: $name") 6 | } 7 | 8 | fun updateName(newName: String) { 9 | println("Name updated: $newName") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/assets/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/AbstractResourcePattern.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | internal abstract class AbstractResourcePattern(patternText: String) : AbstractPattern() { 4 | override val regex: Regex = RegexUtils.newPattern(patternText, forClass = false) 5 | } 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /agp-compat/agp7/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | @Suppress("DSL_SCOPE_VIOLATION") val plugins = libs.plugins 3 | 4 | alias(plugins.kotlin.jvm) 5 | `kotlin-dsl` 6 | id("aaraar-detekt") 7 | id("aaraar-publish") 8 | } 9 | 10 | dependencies { 11 | api(project(":agp-compat:base")) 12 | compileOnly(libs.agp.api7) 13 | } 14 | -------------------------------------------------------------------------------- /agp-compat/agp8/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | @Suppress("DSL_SCOPE_VIOLATION") val plugins = libs.plugins 3 | 4 | alias(plugins.kotlin.jvm) 5 | `kotlin-dsl` 6 | id("aaraar-detekt") 7 | id("aaraar-publish") 8 | } 9 | 10 | dependencies { 11 | api(project(":agp-compat:base")) 12 | compileOnly(libs.agp.api8) 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/ReplacePart.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | internal sealed interface ReplacePart { 4 | data class Literal( 5 | val value: String, 6 | ) : ReplacePart 7 | 8 | data class Group( 9 | val index: Int, 10 | ) : ReplacePart 11 | } 12 | -------------------------------------------------------------------------------- /fixtures/src/ktLibrary/kotlin/sh/christian/mylibrary/Name-Maker.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.mylibrary 2 | 3 | class `Name-Maker` { 4 | fun configure(block: Name.() -> Unit): Name { 5 | val name = Name(DEFAULT_VALUE) 6 | name.apply(block) 7 | return name 8 | } 9 | 10 | companion object { 11 | val DEFAULT_VALUE = "" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | .gradle 3 | /build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | 6 | # IntelliJ IDEA 7 | .idea/modules.xml 8 | .idea/jarRepositories.xml 9 | .idea/compiler.xml 10 | .idea/uiDesigner.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | 16 | # Steve Jobs 17 | .DS_Store 18 | 19 | # Generated Output 20 | /docs/kdoc/ 21 | /site/ 22 | -------------------------------------------------------------------------------- /fixtures/src/service/resources/com/example/tracklist.txt: -------------------------------------------------------------------------------- 1 | 1. "The Fate of Ophelia" 2 | 2. "Elizabeth Taylor" 3 | 3. "Opalite" 4 | 4. "Father Figure" 5 | 5. "Eldest Daughter" 6 | 6. "Ruin the Friendship" 7 | 7. "Actually Romantic" 8 | 8. "Wish List" 9 | 9. "Wood" 10 | 10. "Cancelled!" 11 | 11. "Honey" 12 | 12. "The Life of a Showgirl" (featuring Sabrina Carpenter) 13 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/Merger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger 2 | 3 | /** 4 | * Used for implementations that merge multiple entries together into a target entry, producing a single entry. 5 | */ 6 | interface Merger { 7 | fun merge(first: T, others: List): T 8 | 9 | fun merge(first: T, other: T): T = merge(first, listOf(other)) 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/JarProcessor.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | internal interface JarProcessor { 4 | enum class Result { 5 | KEEP, 6 | DISCARD, 7 | } 8 | 9 | fun process(struct: Transformable): Result 10 | 11 | companion object { 12 | const val EXT_CLASS = ".class" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sample-lib/.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | .gradle 3 | /build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | 6 | # IntelliJ IDEA 7 | .idea/modules.xml 8 | .idea/jarRepositories.xml 9 | .idea/compiler.xml 10 | .idea/libraries/ 11 | *.iws 12 | *.iml 13 | *.ipr 14 | 15 | # Steve Jobs 16 | .DS_Store 17 | 18 | # Android 19 | /captures 20 | .externalNativeBuild 21 | .cxx 22 | local.properties 23 | -------------------------------------------------------------------------------- /docs/assets/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "theme_color": "#000000", 12 | "background_color": "#000000", 13 | "display": "standalone" 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/FieldReference.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | /** 4 | * Represents a declared field for a particular class. 5 | */ 6 | interface FieldReference : MemberReference { 7 | /** The JVM field signature. */ 8 | val signature: Signature 9 | 10 | /** The type that this field stores. */ 11 | val type: ClassReference 12 | } 13 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Production Releases 2 | 3 | 1. Make sure the `CHANGELOG.md` is updated with all the latest notable changes. 4 | 2. Run the release script. Any unsaved local changes will be ignored. 5 | ```shell 6 | ./release.sh 7 | ``` 8 | 3. Push the commits. A new release will automatically be published on Sonatype. 9 | ```shell 10 | git push && git push --tags 11 | ``` 12 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/Shader.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading 2 | 3 | import sh.christian.aaraar.model.ShadeConfiguration 4 | 5 | /** 6 | * Used for implementations that apply a [ShadeConfiguration] to a source value to produce a shaded output. 7 | */ 8 | interface Shader { 9 | fun shade( 10 | source: T, 11 | shadeConfiguration: ShadeConfiguration, 12 | ): T 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/ClassesAndLibsMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger 2 | 3 | import sh.christian.aaraar.model.Classes 4 | import sh.christian.aaraar.model.Libs 5 | 6 | /** 7 | * Used for implementations that merge `libs/` jars into a main `classes.jar` file. 8 | */ 9 | interface ClassesAndLibsMerger : Merger { 10 | fun merge(first: Classes, others: Libs): Classes 11 | } 12 | -------------------------------------------------------------------------------- /agp-compat/base/src/main/kotlin/sh/christian/aaraar/gradle/agp/AndroidExtension.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle.agp 2 | 3 | /** 4 | * A facade of some of the interactions with the `android` extension on an Android module. 5 | */ 6 | interface AndroidExtension { 7 | /** 8 | * Allows for registration of a callback to be called with build type names. 9 | */ 10 | fun onBuildTypes(callback: (String) -> Unit) 11 | } 12 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/JniTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.matchers.shouldBe 4 | import sh.christian.aaraar.utils.ktLibraryJarPath 5 | import kotlin.test.Test 6 | 7 | class JniTest { 8 | @Test 9 | fun `test equality`() { 10 | val jni1 = Jni.from(ktLibraryJarPath.parent) 11 | val jni2 = Jni.from(ktLibraryJarPath.parent) 12 | jni1 shouldBe jni2 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sample-lib/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | } 15 | } 16 | 17 | rootProject.name = "samplelib" 18 | 19 | include ":library" 20 | include ":helper-library" 21 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /agp-compat/agp7/src/main/kotlin/sh/christian/aaraar/gradle/agp/Agp7AndroidExtension.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle.agp 2 | 3 | import com.android.build.api.dsl.LibraryExtension 4 | 5 | internal class Agp7AndroidExtension( 6 | private val android: LibraryExtension, 7 | ) : AndroidExtension { 8 | override fun onBuildTypes(callback: (String) -> Unit) { 9 | return android.buildTypes.configureEach { callback(name) } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /agp-compat/agp8/src/main/kotlin/sh/christian/aaraar/gradle/agp/Agp8AndroidExtension.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle.agp 2 | 3 | import com.android.build.api.dsl.LibraryExtension 4 | 5 | internal class Agp8AndroidExtension( 6 | private val android: LibraryExtension, 7 | ) : AndroidExtension { 8 | override fun onBuildTypes(callback: (String) -> Unit) { 9 | return android.buildTypes.configureEach { callback(name) } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /sample-lib/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /sample-lib/library/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | 11 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/AssetsTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.matchers.shouldBe 4 | import sh.christian.aaraar.utils.ktLibraryJarPath 5 | import kotlin.test.Test 6 | 7 | class AssetsTest { 8 | @Test 9 | fun `test equality`() { 10 | val assets1 = Assets.from(ktLibraryJarPath.parent) 11 | val assets2 = Assets.from(ktLibraryJarPath.parent) 12 | assets1 shouldBe assets2 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/LintRulesTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.matchers.shouldBe 4 | import sh.christian.aaraar.utils.serviceJarPath 5 | import kotlin.test.Test 6 | 7 | class LintRulesTest { 8 | @Test 9 | fun `test equality`() { 10 | val lintRules1 = LintRules.from(serviceJarPath) 11 | val lintRules2 = LintRules.from(serviceJarPath) 12 | lintRules1 shouldBe lintRules2 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/NewParameter.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | /** 4 | * Represents the configuration for a new [MutableParameter] being added to a method or constructor. 5 | */ 6 | data class NewParameter( 7 | val name: String, 8 | val type: MutableClassReference, 9 | val annotations: List = emptyList(), 10 | ) { 11 | override fun toString(): String { 12 | return "$name: $type" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/ClassRename.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | internal class ClassRename( 4 | patternText: String, 5 | replaceText: String, 6 | ) : AbstractClassPattern(patternText), ReplacePattern { 7 | private val replace: List = RegexUtils.newReplace(replaceText, forClass = true) 8 | 9 | override fun replace(value: String): String? { 10 | return RegexUtils.replace(this, replace, value) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/ResourceRename.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | internal class ResourceRename( 4 | patternText: String, 5 | replaceText: String 6 | ) : AbstractResourcePattern(patternText), ReplacePattern { 7 | private val replace: List = RegexUtils.newReplace(replaceText, forClass = false) 8 | 9 | override fun replace(value: String): String? { 10 | return RegexUtils.replace(this, replace, value) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/ArchiveMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger 2 | 3 | import sh.christian.aaraar.model.ArtifactArchive 4 | 5 | /** 6 | * Interface for implementations that merge a set of [ArtifactArchive]s into a specific type of [ArtifactArchive]. 7 | * 8 | * This is useful for when the merging target has a known sealed type, but the dependencies do not. 9 | */ 10 | interface ArchiveMerger { 11 | fun merge(first: T, others: List): T 12 | } 13 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/Parameter.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | /** 4 | * Represents an argument that is part of a method or constructor signature. 5 | */ 6 | interface Parameter { 7 | /** The set of annotations applied to this parameter definition. */ 8 | val annotations: List 9 | 10 | /** The parameter name. */ 11 | val name: String 12 | 13 | /** The type that this parameter stores. */ 14 | val type: ClassReference 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/AbstractPattern.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | internal abstract class AbstractPattern { 4 | protected abstract val regex: Regex 5 | 6 | open fun matchOrNull(value: String): MatchResult? { 7 | return regex.matchEntire(value) 8 | } 9 | 10 | fun matches(value: String): Boolean { 11 | return regex.matches(value) 12 | } 13 | 14 | override fun toString(): String { 15 | return regex.pattern 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/MutableMemberReference.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | /** 4 | * Represents a declared member (ie: constructor, field, or method) for a particular class. 5 | * 6 | * This representation is mutable, to allow changing properties of the member. 7 | */ 8 | sealed class MutableMemberReference : MemberReference { 9 | abstract override var annotations: List 10 | 11 | abstract override var modifiers: Set 12 | } 13 | -------------------------------------------------------------------------------- /config/detekt/detekt.yml: -------------------------------------------------------------------------------- 1 | complexity: 2 | LongParameterList: 3 | functionThreshold: 999 4 | constructorThreshold: 999 5 | LongMethod: 6 | threshold: 100 7 | CyclomaticComplexMethod: 8 | threshold: 20 9 | TooManyFunctions: 10 | thresholdInClasses: 20 11 | 12 | naming: 13 | ConstructorParameterNaming: 14 | parameterPattern: "[a-z_][A-Za-z0-9]*" 15 | privateParameterPattern: "[a-z_][A-Za-z0-9]*" 16 | 17 | formatting: 18 | Filename: 19 | active: false 20 | Indentation: 21 | indentSize: 2 22 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/NoJarArchiveMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import sh.christian.aaraar.merger.Merger 4 | import sh.christian.aaraar.model.GenericJarArchive 5 | 6 | /** 7 | * Unsupported implementation for merging multiple `jar` files. 8 | */ 9 | object NoJarArchiveMerger : Merger { 10 | override fun merge(first: GenericJarArchive, others: List): GenericJarArchive { 11 | error("Merging JARs in this context is not supported.") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/Jni.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import java.nio.file.Path 4 | 5 | /** 6 | * Represents the compiled native files in the `jni/` folder. 7 | */ 8 | data class Jni( 9 | val files: FileSet, 10 | ) { 11 | fun writeTo(path: Path) { 12 | files.writeTo(path) 13 | } 14 | 15 | companion object { 16 | fun from(path: Path): Jni { 17 | return FileSet.fromFileTree(path) 18 | ?.let { files -> Jni(files) } 19 | ?: Jni(files = FileSet.EMPTY) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/Assets.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import java.nio.file.Path 4 | 5 | /** 6 | * Represents the contents of the `assets/` folder. 7 | */ 8 | data class Assets( 9 | val files: FileSet, 10 | ) { 11 | fun writeTo(path: Path) { 12 | files.writeTo(path) 13 | } 14 | 15 | companion object { 16 | fun from(path: Path): Assets { 17 | return FileSet.fromFileTree(path) 18 | ?.let { files -> Assets(files) } 19 | ?: Assets(files = FileSet.EMPTY) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/MemberReference.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | /** 4 | * Represents a declared member (ie: constructor, field, or method) for a particular class. 5 | */ 6 | interface MemberReference { 7 | /** The member name. */ 8 | val name: String 9 | 10 | /** The set of annotations applied to this member definition. */ 11 | val annotations: List 12 | 13 | /** The set of modifiers applied to the member definition. */ 14 | val modifiers: Set 15 | } 16 | -------------------------------------------------------------------------------- /agp-compat/base/src/main/kotlin/sh/christian/aaraar/gradle/agp/AndroidPackaging.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle.agp 2 | 3 | import org.gradle.api.provider.SetProperty 4 | 5 | interface AndroidPackaging { 6 | val jniLibs: JniLibs 7 | val resources: Resources 8 | 9 | interface JniLibs { 10 | val excludes: SetProperty 11 | val pickFirsts: SetProperty 12 | } 13 | 14 | interface Resources { 15 | val excludes: SetProperty 16 | val pickFirsts: SetProperty 17 | val merges: SetProperty 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/ProguardMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import sh.christian.aaraar.merger.Merger 4 | import sh.christian.aaraar.model.Proguard 5 | 6 | /** 7 | * Standard implementation for merging multiple proguard rule files. 8 | * 9 | * Concatenates all rule entries without any deduplication. 10 | */ 11 | class ProguardMerger : Merger { 12 | override fun merge(first: Proguard, others: List): Proguard { 13 | return Proguard(first.lines + others.flatMap { it.lines }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/ConstructorReference.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | /** 4 | * Represents a declared constructor for a particular class. 5 | */ 6 | interface ConstructorReference : MemberReference { 7 | /** The JVM constructor signature. */ 8 | val signature: Signature 9 | 10 | /** The [Parameter] arguments that this constructor must be invoked with. */ 11 | val parameters: List 12 | 13 | /** The type that is instantiated by this constructor. */ 14 | val type: ClassReference 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/JniMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import sh.christian.aaraar.merger.Merger 4 | import sh.christian.aaraar.model.FileSet 5 | import sh.christian.aaraar.model.Jni 6 | 7 | /** 8 | * Standard file-wise implementation for merging multiple `jni/` folders. 9 | */ 10 | class JniMerger( 11 | private val fileSetMerger: Merger, 12 | ) : Merger { 13 | override fun merge(first: Jni, others: List): Jni { 14 | return Jni(fileSetMerger.merge(first.files, others.map { it.files })) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/LintRules.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import java.nio.file.Path 4 | 5 | /** 6 | * Represents the set of consumer Lint rules. 7 | */ 8 | data class LintRules( 9 | val archive: GenericJarArchive, 10 | ) { 11 | fun writeTo(path: Path) { 12 | archive.writeTo(path) 13 | } 14 | 15 | companion object { 16 | fun from(path: Path): LintRules { 17 | return GenericJarArchive.from(path, keepMetaFiles = true) 18 | ?.let { archive -> LintRules(archive) } 19 | ?: LintRules(GenericJarArchive.NONE) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/AssetsMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import sh.christian.aaraar.merger.Merger 4 | import sh.christian.aaraar.model.Assets 5 | import sh.christian.aaraar.model.FileSet 6 | 7 | /** 8 | * Standard file-wise implementation for merging multiple `assets/` folders. 9 | */ 10 | class AssetsMerger( 11 | private val fileSetMerger: Merger, 12 | ) : Merger { 13 | override fun merge(first: Assets, others: List): Assets { 14 | return Assets(fileSetMerger.merge(first.files, others.map { it.files })) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/Environment.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * General properties that influence the archive merging process. 7 | * 8 | * @param androidAaptIgnore the value of the `ANDROID_AAPT_IGNORE` environment variable. 9 | * @param keepClassesMetaFiles whether `META-INF/` files should be kept in merged archive file. 10 | */ 11 | data class Environment( 12 | val androidAaptIgnore: String, 13 | val keepClassesMetaFiles: Boolean, 14 | ) : Serializable { 15 | companion object { 16 | private const val serialVersionUID = 1L 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/ApiJarMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import sh.christian.aaraar.merger.Merger 4 | import sh.christian.aaraar.model.ApiJar 5 | import sh.christian.aaraar.model.GenericJarArchive 6 | 7 | /** 8 | * Standard jar-wise implementation for merging multiple `api.jar` files. 9 | */ 10 | class ApiJarMerger( 11 | private val jarMerger: Merger, 12 | ) : Merger { 13 | override fun merge(first: ApiJar, others: List): ApiJar { 14 | return ApiJar(jarMerger.merge(first.archive, others.map { it.archive })) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/ApiJar.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import java.nio.file.Path 4 | 5 | /** 6 | * Represents the contents of the `api.jar` file, the IDE sources for an [ArtifactArchive]. 7 | */ 8 | data class ApiJar( 9 | val archive: GenericJarArchive, 10 | ) { 11 | fun writeTo(path: Path) { 12 | archive.writeTo(path) 13 | } 14 | 15 | companion object { 16 | fun from( 17 | path: Path, 18 | keepMetaFiles: Boolean, 19 | ): ApiJar { 20 | return ApiJar(GenericJarArchive.from(path, keepMetaFiles) ?: GenericJarArchive.NONE) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/RTxtMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import com.android.ide.common.symbols.SymbolTable 4 | import sh.christian.aaraar.merger.Merger 5 | import sh.christian.aaraar.model.RTxt 6 | 7 | /** 8 | * Standard implementation for merging multiple `R.txt` files. 9 | * 10 | * Concatenates all rule entries without any sorting or deduplication. 11 | */ 12 | class RTxtMerger : Merger { 13 | override fun merge(first: RTxt, others: List): RTxt { 14 | return RTxt(SymbolTable.merge(listOf(first.symbolTable) + others.map { it.symbolTable })) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | ``` 2 | Copyright 2025 Christian De Angelis 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | ``` 16 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/NavigationJsonMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import sh.christian.aaraar.merger.Merger 4 | import sh.christian.aaraar.model.NavigationJson 5 | 6 | /** 7 | * Standard implementation for merging multiple `navigation.json` files. 8 | * 9 | * Concatenates all file entries without any deduplication. 10 | */ 11 | class NavigationJsonMerger : Merger { 12 | override fun merge(first: NavigationJson, others: List): NavigationJson { 13 | return NavigationJson(first.navigationData + others.flatMap { it.navigationData }) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/Signature.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | sealed interface Signature { 4 | val memberName: String 5 | val descriptor: String 6 | } 7 | 8 | data class ConstructorSignature( 9 | override val descriptor: String 10 | ) : Signature { 11 | override val memberName: String = "" 12 | } 13 | 14 | data class MethodSignature( 15 | override val memberName: String, 16 | override val descriptor: String, 17 | ) : Signature 18 | 19 | data class FieldSignature( 20 | override val memberName: String, 21 | override val descriptor: String, 22 | ) : Signature 23 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/Classes.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import java.nio.file.Path 4 | 5 | /** 6 | * Represents the contents of the `classes.jar` file, the main runtime sources for an [ArtifactArchive]. 7 | */ 8 | data class Classes( 9 | val archive: GenericJarArchive, 10 | ) { 11 | fun writeTo(path: Path) { 12 | archive.writeTo(path) 13 | } 14 | 15 | companion object { 16 | fun from( 17 | path: Path, 18 | keepMetaFiles: Boolean, 19 | ): Classes { 20 | return Classes(GenericJarArchive.from(path, keepMetaFiles) ?: GenericJarArchive.NONE) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/LintRulesMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import sh.christian.aaraar.merger.Merger 4 | import sh.christian.aaraar.model.GenericJarArchive 5 | import sh.christian.aaraar.model.LintRules 6 | 7 | /** 8 | * Standard jar-wise implementation for merging multiple `lint.jar` files. 9 | */ 10 | class LintRulesMerger( 11 | private val jarMerger: Merger, 12 | ) : Merger { 13 | override fun merge(first: LintRules, others: List): LintRules { 14 | return LintRules(jarMerger.merge(first.archive, others.map { it.archive })) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block extrahead %} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/JarProcessorChain.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | import java.io.IOException 4 | 5 | internal class JarProcessorChain( 6 | val processors: List, 7 | ) : JarProcessor { 8 | 9 | constructor(vararg processors: JarProcessor) : this(processors.toList()) 10 | 11 | @Throws(IOException::class) 12 | override fun process(struct: Transformable): JarProcessor.Result { 13 | return if (processors.any { it.process(struct) == JarProcessor.Result.DISCARD }) { 14 | JarProcessor.Result.DISCARD 15 | } else { 16 | JarProcessor.Result.KEEP 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /fixtures/src/testFixtures/kotlin/sh/christian/aaraar/utils/classeditor.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.utils 2 | 3 | import sh.christian.aaraar.model.GenericJarArchive 4 | import sh.christian.aaraar.model.classeditor.MutableClasspath 5 | import kotlin.contracts.ExperimentalContracts 6 | import kotlin.contracts.InvocationKind.EXACTLY_ONCE 7 | import kotlin.contracts.contract 8 | 9 | @OptIn(ExperimentalContracts::class) 10 | inline fun withClasspath( 11 | jar: GenericJarArchive = GenericJarArchive.NONE, 12 | crossinline block: (MutableClasspath) -> Unit, 13 | ) { 14 | contract { 15 | callsInPlace(block, EXACTLY_ONCE) 16 | } 17 | block(MutableClasspath.from(jar)) 18 | } 19 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/classeditor/testutil.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | import io.kotest.matchers.nulls.shouldNotBeNull 4 | import kotlinx.metadata.KmClass 5 | 6 | internal val MutableClasspath.foo get() = this["sh.christian.mylibrary.Foo"] 7 | internal val MutableClasspath.fooInternal get() = this["sh.christian.mylibrary.FooInternal"] 8 | internal val MutableClasspath.immutable get() = this["sh.christian.mylibrary.Immutable"] 9 | internal val MutableClasspath.name get() = this["sh.christian.mylibrary.Name"] 10 | 11 | internal fun MutableClassReference.requireMetadata(): KmClass { 12 | return kotlinMetadata.shouldNotBeNull().kmClass 13 | } 14 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/sh/christian/aaraar/gradle/ArtifactArchiveProcessor.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle 2 | 3 | import sh.christian.aaraar.model.ArtifactArchive 4 | import java.io.Serializable 5 | 6 | /** 7 | * Observes or modifies a merged [ArtifactArchive]. This is useful for post-processing the merged archive to optionally 8 | * add, remove, modify, validate, or inspect the contents of the archive. 9 | */ 10 | fun interface ArtifactArchiveProcessor { 11 | fun process(archive: ArtifactArchive): ArtifactArchive 12 | 13 | /** Factory for creating a new [ArtifactArchiveProcessor]. */ 14 | interface Factory : Serializable { 15 | fun create(): ArtifactArchiveProcessor 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/JarArchiveShader.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl 2 | 3 | import sh.christian.aaraar.model.Classes 4 | import sh.christian.aaraar.model.JarArchive 5 | import sh.christian.aaraar.model.ShadeConfiguration 6 | import sh.christian.aaraar.shading.Shader 7 | 8 | /** 9 | * Standard implementation for shading a JAR artifact by shading the JAR directly. 10 | */ 11 | class JarArchiveShader( 12 | private val classesShader: Shader, 13 | ) : Shader { 14 | override fun shade(source: JarArchive, shadeConfiguration: ShadeConfiguration): JarArchive { 15 | return JarArchive(classesShader.shade(source.classes, shadeConfiguration)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /fixtures/src/testFixtures/kotlin/sh/christian/aaraar/utils/metadata.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.utils 2 | 3 | import io.kotest.matchers.shouldBe 4 | import kotlinx.metadata.KmClass 5 | import kotlinx.metadata.jvm.JvmMetadataVersion 6 | import kotlinx.metadata.jvm.KotlinClassMetadata 7 | 8 | infix fun KmClass.shouldBe(other: KmClass) { 9 | val class1 = KotlinClassMetadata.Class( 10 | kmClass = this, 11 | version = JvmMetadataVersion.LATEST_STABLE_SUPPORTED, 12 | flags = 0, 13 | ) 14 | 15 | val class2 = KotlinClassMetadata.Class( 16 | kmClass = other, 17 | version = JvmMetadataVersion.LATEST_STABLE_SUPPORTED, 18 | flags = 0, 19 | ) 20 | 21 | class1.write() shouldBe class2.write() 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/PublicTxtMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import com.android.ide.common.symbols.SymbolTable 4 | import sh.christian.aaraar.merger.Merger 5 | import sh.christian.aaraar.model.PublicTxt 6 | 7 | /** 8 | * Standard implementation for merging multiple `public.txt` files. 9 | * 10 | * The basis of this implementation uses the same resource table merging logic that the Android Gradle Plugin uses. 11 | */ 12 | class PublicTxtMerger : Merger { 13 | override fun merge(first: PublicTxt, others: List): PublicTxt { 14 | return PublicTxt(SymbolTable.merge(listOf(first.symbolTable) + others.map { it.symbolTable })) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "aaraar" 2 | 3 | pluginManagement { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | gradlePluginPortal() 8 | maven("https://www.jetbrains.com/intellij-repository/releases/") 9 | } 10 | } 11 | 12 | @Suppress("UnstableApiUsage") 13 | dependencyResolutionManagement { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | gradlePluginPortal() 18 | maven("https://www.jetbrains.com/intellij-repository/releases/") 19 | } 20 | } 21 | 22 | include(":agp-compat:agp7") 23 | include(":agp-compat:agp8") 24 | include(":agp-compat:base") 25 | include(":core") 26 | include(":fixtures") 27 | include(":gradle-plugin") 28 | 29 | includeBuild("build-logic") 30 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/ClassesShader.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl 2 | 3 | import sh.christian.aaraar.model.Classes 4 | import sh.christian.aaraar.model.GenericJarArchive 5 | import sh.christian.aaraar.model.ShadeConfiguration 6 | import sh.christian.aaraar.shading.Shader 7 | 8 | /** 9 | * Standard implementation for shading `classes.jar` by delegating to another JAR shader implementation. 10 | */ 11 | class ClassesShader( 12 | private val genericJarArchiveShader: Shader, 13 | ) : Shader { 14 | override fun shade(source: Classes, shadeConfiguration: ShadeConfiguration): Classes { 15 | return Classes(genericJarArchiveShader.shade(source.archive, shadeConfiguration)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/ProguardTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.matchers.shouldBe 4 | import kotlin.test.Test 5 | 6 | class ProguardTest { 7 | private val contents = """ 8 | -keep class androidx.** { *; } 9 | -keep class com.google.** { 10 | ; 11 | ; 12 | } 13 | 14 | -dontwarn com.android.** 15 | """.trimIndent() 16 | 17 | @Test 18 | fun `test toString`() { 19 | val proguard1 = Proguard(contents.lines()) 20 | proguard1.toString() shouldBe contents 21 | } 22 | 23 | @Test 24 | fun `test equality`() { 25 | val proguard1 = Proguard(contents.lines()) 26 | val proguard2 = Proguard(contents.lines()) 27 | proguard1 shouldBe proguard2 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/bytearrays.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | @Suppress("ReturnCount") 4 | internal fun contentEquals( 5 | map1: Map, 6 | map2: Map, 7 | ): Boolean { 8 | if (map1.size != map2.size) return false 9 | 10 | for ((key, value) in map1) { 11 | val otherValue = map2[key] ?: return false 12 | if (!value.contentEquals(otherValue)) return false 13 | } 14 | return true 15 | } 16 | 17 | @Suppress("MagicNumber") 18 | internal fun contentHashCode( 19 | map: Map, 20 | ): Int { 21 | var result = 1 22 | for ((key, value) in map) { 23 | result = 31 * result + key.hashCode() 24 | result = 31 * result + value.contentHashCode() 25 | } 26 | return result 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/ParameterOwner.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | import javassist.CtBehavior 4 | 5 | internal sealed interface ParameterOwner { 6 | val classpath: MutableClasspath 7 | val behavior: CtBehavior 8 | } 9 | 10 | internal data class FromConstructor( 11 | val constructor: MutableConstructorReference, 12 | ) : ParameterOwner { 13 | override val classpath: MutableClasspath = constructor.classpath 14 | override val behavior: CtBehavior = constructor._constructor 15 | } 16 | 17 | internal data class FromMethod( 18 | val method: MutableMethodReference, 19 | ) : ParameterOwner { 20 | override val classpath: MutableClasspath = method.classpath 21 | override val behavior: CtBehavior = method._method 22 | } 23 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/ResourcesTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.matchers.shouldBe 4 | import sh.christian.aaraar.utils.ktLibraryJarPath 5 | import kotlin.test.Test 6 | 7 | class ResourcesTest { 8 | @Test 9 | fun `test equality`() { 10 | val resources1 = Resources( 11 | files = FileSet.fromFileTree(ktLibraryJarPath.parent)!!, 12 | packageName = "sh.christian.example", 13 | minSdk = 21, 14 | androidAaptIgnore = "", 15 | ) 16 | val resources2 = Resources( 17 | files = FileSet.fromFileTree(ktLibraryJarPath.parent)!!, 18 | packageName = "sh.christian.example", 19 | minSdk = 21, 20 | androidAaptIgnore = "", 21 | ) 22 | resources1 shouldBe resources2 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/MethodReference.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | import sh.christian.aaraar.model.classeditor.types.voidType 4 | 5 | /** 6 | * Represents a declared method for a particular class. 7 | */ 8 | interface MethodReference : MemberReference { 9 | /** The JVM method signature. */ 10 | val signature: Signature 11 | 12 | /** The [Parameter] arguments that this constructor must be invoked with. */ 13 | val parameters: List 14 | 15 | /** The constant default value returned by this method, if defined on an annotation class. */ 16 | val defaultValue: AnnotationInstance.Value? 17 | 18 | /** The type that is returned by this method, or [voidType] if none is defined. */ 19 | val returnType: ClassReference 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/AbstractClassPattern.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | internal abstract class AbstractClassPattern(patternText: String) : AbstractPattern() { 4 | override val regex: Regex = RegexUtils.newPattern(check(patternText), forClass = true) 5 | 6 | override fun matchOrNull(value: String): MatchResult? { 7 | return if (RegexUtils.isPossibleQualifiedName(value, "/")) { 8 | super.matchOrNull(value) 9 | } else { 10 | null 11 | } 12 | } 13 | 14 | private companion object { 15 | fun check(patternText: String): String { 16 | require('/' !in patternText) { 17 | "Class patterns cannot contain slashes" 18 | } 19 | return patternText.replace('.', '/') 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/AarMetadataTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.matchers.shouldBe 4 | import kotlin.test.Test 5 | 6 | class AarMetadataTest { 7 | private val contents = """ 8 | aarFormatVersion=1.0 9 | aarMetadataVersion=1.0 10 | minCompileSdk=1 11 | minCompileSdkExtension=0 12 | minAndroidGradlePluginVersion=1.0.0 13 | coreLibraryDesugaringEnabled=false 14 | """.trimIndent() 15 | 16 | @Test 17 | fun `test toString`() { 18 | val metadata = AarMetadata(contents.lines()) 19 | metadata.toString() shouldBe contents 20 | } 21 | 22 | @Test 23 | fun `test equality`() { 24 | val metadata1 = AarMetadata(contents.lines()) 25 | val metadata2 = AarMetadata(contents.lines()) 26 | metadata1 shouldBe metadata2 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sample-lib/.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | `aaraar` is a Gradle plugin that assists in embedding other dependencies directly into your published artifact. 2 | The plugin can be applied to any module that is published as an `aar` or a `jar` file, and includes some handy features 3 | such as: 4 | 5 | - Embedding dependencies directly into your `jar` or `aar` file 6 | - Shading classes to rename or delete them 7 | - Stripping `META-INF` files 8 | 9 | `aaraar` is simple to use with the most common publishing plugins, including the 10 | [Maven Publish Plugin](https://docs.gradle.org/current/userguide/publishing_maven.html) and the 11 | [Gradle Maven Publish Plugin](https://github.com/vanniktech/gradle-maven-publish-plugin), but advanced configuration is 12 | still available for those with a more custom publishing pipeline. 13 | 14 | Visit [Installation](installation.md) to get started. 15 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/shading/util.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading 2 | 3 | import sh.christian.aaraar.model.GenericJarArchive 4 | import sh.christian.aaraar.model.ShadeConfiguration 5 | import sh.christian.aaraar.shading.impl.GenericJarArchiveShader 6 | 7 | internal fun GenericJarArchive.shaded( 8 | classRenames: Map = emptyMap(), 9 | classDeletes: Set = emptySet(), 10 | resourceRenames: Map = emptyMap(), 11 | resourceDeletes: Set = emptySet(), 12 | ): GenericJarArchive { 13 | return GenericJarArchiveShader().shade( 14 | source = this, 15 | shadeConfiguration = ShadeConfiguration( 16 | classRenames = classRenames, 17 | classDeletes = classDeletes, 18 | resourceRenames = resourceRenames, 19 | resourceDeletes = resourceDeletes, 20 | ), 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/FileSetMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import sh.christian.aaraar.merger.MergeRules 4 | import sh.christian.aaraar.merger.Merger 5 | import sh.christian.aaraar.merger.mergeContents 6 | import sh.christian.aaraar.model.FileSet 7 | import sh.christian.aaraar.model.GenericJarArchive 8 | 9 | /** 10 | * Standard file-wise implementation for merging multiple sets of files. 11 | * 12 | * If there are any duplicate file paths with differing file contents, an exception will be thrown. 13 | */ 14 | class FileSetMerger( 15 | private val jarMerger: Merger, 16 | private val mergeRules: MergeRules, 17 | ) : Merger { 18 | override fun merge(first: FileSet, others: List): FileSet { 19 | return FileSet(mergeContents(first, others, jarMerger, mergeRules)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/LibsTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.matchers.maps.shouldContainKeys 4 | import io.kotest.matchers.shouldBe 5 | import org.junit.jupiter.api.Test 6 | import sh.christian.aaraar.utils.externalLibsPath 7 | import sh.christian.aaraar.utils.shouldContainExactly 8 | 9 | class LibsTest { 10 | 11 | @Test 12 | fun `identifies jar files`() { 13 | val libs = Libs.from(externalLibsPath) 14 | libs.files.shouldContainExactly( 15 | "external.jar", 16 | "foo.jar", 17 | "license.txt", 18 | ) 19 | 20 | libs.jars().shouldContainKeys( 21 | "external.jar", 22 | "foo.jar", 23 | ) 24 | } 25 | 26 | @Test 27 | fun `test equality`() { 28 | val libs1 = Libs.from(externalLibsPath) 29 | val libs2 = Libs.from(externalLibsPath) 30 | libs1 shouldBe libs2 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/Proguard.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import java.nio.file.Files 4 | import java.nio.file.Path 5 | import java.util.stream.Collectors.toList 6 | 7 | /** 8 | * Represents the set of consumer Proguard rules. 9 | */ 10 | data class Proguard( 11 | val lines: List, 12 | ) { 13 | override fun toString(): String { 14 | return lines.joinToString(separator = "\n") 15 | } 16 | 17 | fun writeTo(path: Path) { 18 | if (lines.isEmpty()) { 19 | Files.deleteIfExists(path) 20 | } else { 21 | Files.write(path, lines) 22 | } 23 | } 24 | 25 | companion object { 26 | fun from(path: Path): Proguard { 27 | if (!Files.isRegularFile(path)) return Proguard(lines = emptyList()) 28 | 29 | val lines = Files.lines(path).collect(toList()) 30 | return Proguard(lines) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/ClassesMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import sh.christian.aaraar.merger.ClassesAndLibsMerger 4 | import sh.christian.aaraar.merger.Merger 5 | import sh.christian.aaraar.model.Classes 6 | import sh.christian.aaraar.model.GenericJarArchive 7 | import sh.christian.aaraar.model.Libs 8 | 9 | /** 10 | * Standard jar-wise implementation for merging multiple `classes.jar` files. 11 | */ 12 | class ClassesMerger( 13 | private val jarMerger: Merger, 14 | ) : ClassesAndLibsMerger { 15 | override fun merge(first: Classes, others: List): Classes { 16 | return Classes(jarMerger.merge(first.archive, others.map { it.archive })) 17 | } 18 | 19 | override fun merge(first: Classes, others: Libs): Classes { 20 | return Classes(jarMerger.merge(first.archive, others.jars().values.toList())) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/Libs.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import java.nio.file.Path 4 | 5 | /** 6 | * Represents the set of `jar` files in the `libs/` folder. 7 | */ 8 | data class Libs( 9 | val files: FileSet, 10 | ) { 11 | fun jars(): Map { 12 | return files.mapNotNull { (path, contents) -> 13 | if (path.substringAfterLast('.') == "jar") { 14 | GenericJarArchive.from(contents, keepMetaFiles = true)?.let { path to it } 15 | } else { 16 | null 17 | } 18 | }.toMap() 19 | } 20 | 21 | fun writeTo(path: Path) { 22 | files.writeTo(path) 23 | } 24 | 25 | companion object { 26 | val EMPTY = Libs(files = FileSet.EMPTY) 27 | 28 | fun from(path: Path): Libs { 29 | return FileSet.fromFileTree(path) 30 | ?.let { files -> Libs(files) } 31 | ?: EMPTY 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/ArtifactArchiveMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import sh.christian.aaraar.merger.ArchiveMerger 4 | import sh.christian.aaraar.model.AarArchive 5 | import sh.christian.aaraar.model.ArtifactArchive 6 | import sh.christian.aaraar.model.JarArchive 7 | 8 | /** 9 | * Composite archive file merger that delegates to either an `aar` file merger or a `jar` file merger. 10 | */ 11 | class ArtifactArchiveMerger( 12 | private val jarArchiveMerger: ArchiveMerger, 13 | private val aarArchiveMerger: ArchiveMerger, 14 | ) : ArchiveMerger { 15 | override fun merge(first: ArtifactArchive, others: List): ArtifactArchive { 16 | return when (first) { 17 | is JarArchive -> jarArchiveMerger.merge(first, others) 18 | is AarArchive -> aarArchiveMerger.merge(first, others) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/sh/christian/aaraar/gradle/PackageJarTask.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle 2 | 3 | import org.gradle.api.file.RegularFileProperty 4 | import org.gradle.api.tasks.CacheableTask 5 | import org.gradle.api.tasks.InputFile 6 | import org.gradle.api.tasks.OutputFile 7 | import org.gradle.api.tasks.PathSensitive 8 | import org.gradle.api.tasks.PathSensitivity 9 | import sh.christian.aaraar.Environment 10 | 11 | @CacheableTask 12 | abstract class PackageJarTask : PackageArchiveTask() { 13 | @get:InputFile 14 | @get:PathSensitive(PathSensitivity.RELATIVE) 15 | val inputJar: RegularFileProperty get() = inputArchive 16 | 17 | @get:OutputFile 18 | val outputJar: RegularFileProperty get() = outputArchive 19 | 20 | final override fun environment(): Environment { 21 | return Environment( 22 | androidAaptIgnore = "", 23 | keepClassesMetaFiles = keepMetaFiles.get(), 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/pipeline/ClassFilesProcessor.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.pipeline 2 | 3 | import sh.christian.aaraar.shading.impl.transform.JarProcessor 4 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.DISCARD 5 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.KEEP 6 | import sh.christian.aaraar.shading.impl.transform.Transformable 7 | 8 | internal class ClassFilesProcessor( 9 | private val jarProcessor: JarProcessor, 10 | ) { 11 | fun process(entries: Map): Map = buildMap { 12 | entries.forEach { (path, contents) -> 13 | val entry = Transformable( 14 | name = path, 15 | data = contents, 16 | time = 0L, 17 | ) 18 | 19 | when (jarProcessor.process(entry)) { 20 | KEEP -> put(entry.name, entry.data) 21 | DISCARD -> Unit 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/AarMetadata.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import java.nio.file.Files 4 | import java.nio.file.Path 5 | import java.util.stream.Collectors.toList 6 | 7 | /** 8 | * Represents the contents of the `META-INF/com/android/build/gradle/aar-metadata.properties` file. 9 | */ 10 | data class AarMetadata( 11 | val lines: List, 12 | ) { 13 | override fun toString(): String { 14 | return lines.joinToString(separator = "\n") 15 | } 16 | 17 | fun writeTo(path: Path) { 18 | if (lines.isEmpty()) { 19 | Files.deleteIfExists(path) 20 | } else { 21 | Files.write(path, lines) 22 | } 23 | } 24 | 25 | companion object { 26 | fun from(path: Path): AarMetadata { 27 | if (!Files.isRegularFile(path)) return AarMetadata(lines = emptyList()) 28 | 29 | val lines = Files.lines(path).collect(toList()) 30 | return AarMetadata(lines) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /fixtures/src/annotations/java/com/example/RegExp.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.intellij.lang.annotations.Language; 4 | import org.jetbrains.annotations.NonNls; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | import static java.lang.annotation.ElementType.ANNOTATION_TYPE; 12 | import static java.lang.annotation.ElementType.FIELD; 13 | import static java.lang.annotation.ElementType.LOCAL_VARIABLE; 14 | import static java.lang.annotation.ElementType.METHOD; 15 | import static java.lang.annotation.ElementType.PARAMETER; 16 | 17 | @Documented 18 | @Retention(RetentionPolicy.CLASS) 19 | @Target({METHOD, FIELD, PARAMETER, LOCAL_VARIABLE, ANNOTATION_TYPE}) 20 | @Language("RegExp") 21 | public @interface RegExp { 22 | @NonNls String prefix() default ""; 23 | 24 | @NonNls String suffix() default ""; 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/Transformable.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | internal data class Transformable( 4 | var data: ByteArray, 5 | var name: String, 6 | var time: Long, 7 | ) { 8 | override fun toString(): String { 9 | return "Transformable(name='$name', time=$time, data=ByteArray[${data.size}])" 10 | } 11 | 12 | override fun equals(other: Any?): Boolean { 13 | if (this === other) return true 14 | if (javaClass != other?.javaClass) return false 15 | 16 | other as Transformable 17 | 18 | if (time != other.time) return false 19 | if (!data.contentEquals(other.data)) return false 20 | if (name != other.name) return false 21 | 22 | return true 23 | } 24 | 25 | override fun hashCode(): Int { 26 | var result = time.hashCode() 27 | result = 31 * result + data.contentHashCode() 28 | result = 31 * result + name.hashCode() 29 | return result 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/sh/christian/aaraar/gradle/agp.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle 2 | 3 | import com.android.build.api.variant.LibraryAndroidComponentsExtension 4 | import org.gradle.api.Project 5 | import org.gradle.kotlin.dsl.getByType 6 | import sh.christian.aaraar.gradle.agp.Agp7 7 | import sh.christian.aaraar.gradle.agp.Agp8 8 | import sh.christian.aaraar.gradle.agp.AgpCompat 9 | 10 | private const val AGP_8 = 8 11 | private const val AGP_7 = 7 12 | 13 | internal val Project.agp: AgpCompat 14 | get() { 15 | val agpVersion = extensions.getByType().pluginVersion 16 | return when { 17 | agpVersion.major > AGP_8 -> Agp8(this).also { 18 | project.logger.warn("aaraar has not been tested against AGP > 8. Use at your own risk!") 19 | } 20 | agpVersion.major == AGP_8 -> Agp8(this) 21 | agpVersion.major == AGP_7 -> Agp7(this) 22 | else -> error("aaraar is not compatible with AGP < 7") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/MergeRules.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger 2 | 3 | /** 4 | * Constraints on how to handle the merging of collections of files. 5 | * 6 | * These constraints are consulted primarily when there is more than one entry for a given path. However, [excludes] are 7 | * also used to exclude any matching entry even if there are no conflicts for that path. 8 | */ 9 | data class MergeRules( 10 | /** 11 | * The pattern(s) for which the first occurrence is packaged. Ordering is determined by the order of dependencies. 12 | */ 13 | val pickFirsts: Glob, 14 | /** 15 | * The pattern(s) for which matching resources are merged into a single entry. 16 | */ 17 | val merges: Glob, 18 | /** 19 | * The excluded pattern(s). 20 | */ 21 | val excludes: Glob, 22 | ) { 23 | companion object { 24 | val None = MergeRules( 25 | pickFirsts = Glob.None, 26 | merges = Glob.None, 27 | excludes = Glob.None, 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Maven Central](https://img.shields.io/maven-central/v/sh.christian.aaraar/gradle-plugin?versionPrefix=0.1.3) ![CI](https://github.com/christiandeange/aaraar/actions/workflows/ci.yml/badge.svg) 2 | 3 | # aaraar 4 | 5 | A Gradle Plugin for creating a merged aar file. 6 | 7 | Work in progress. 8 | 9 | # Documentation 10 | 11 | See the docs at https://aaraar.christian.sh 12 | 13 | # License 14 | 15 | ``` 16 | Copyright 2025 Christian De Angelis 17 | 18 | Licensed under the Apache License, Version 2.0 (the "License"); 19 | you may not use this file except in compliance with the License. 20 | You may obtain a copy of the License at 21 | 22 | http://www.apache.org/licenses/LICENSE-2.0 23 | 24 | Unless required by applicable law or agreed to in writing, software 25 | distributed under the License is distributed on an "AS IS" BASIS, 26 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | See the License for the specific language governing permissions and 28 | limitations under the License. 29 | ``` 30 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/pipeline/ResourceFileShader.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.pipeline 2 | 3 | import sh.christian.aaraar.shading.impl.transform.JarProcessor 4 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Companion.EXT_CLASS 5 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.KEEP 6 | import sh.christian.aaraar.shading.impl.transform.PathRemapper 7 | import sh.christian.aaraar.shading.impl.transform.ResourceRename 8 | import sh.christian.aaraar.shading.impl.transform.Transformable 9 | 10 | internal class ResourceFileShader( 11 | resourceRenames: Map, 12 | ) : JarProcessor { 13 | private val pathRemapper = PathRemapper( 14 | resourceRenames.map { (pattern, result) -> ResourceRename(pattern, result) } 15 | ) 16 | 17 | override fun process(struct: Transformable): JarProcessor.Result { 18 | if (struct.name.endsWith(EXT_CLASS)) return KEEP 19 | 20 | struct.name = pathRemapper.mapType(struct.name) 21 | 22 | return KEEP 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/ApiJarTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.matchers.shouldBe 4 | import io.kotest.matchers.shouldNotBe 5 | import sh.christian.aaraar.utils.serviceJarPath 6 | import kotlin.test.Test 7 | 8 | class ApiJarTest { 9 | @Test 10 | fun `test equality with meta files`() { 11 | val apiJar1 = ApiJar.from(serviceJarPath, keepMetaFiles = true) 12 | val apiJar2 = ApiJar.from(serviceJarPath, keepMetaFiles = true) 13 | apiJar1 shouldBe apiJar2 14 | } 15 | 16 | @Test 17 | fun `test equality without meta files`() { 18 | val apiJar1 = ApiJar.from(serviceJarPath, keepMetaFiles = false) 19 | val apiJar2 = ApiJar.from(serviceJarPath, keepMetaFiles = false) 20 | apiJar1 shouldBe apiJar2 21 | } 22 | 23 | @Test 24 | fun `test equality with differing meta files`() { 25 | val apiJar1 = ApiJar.from(serviceJarPath, keepMetaFiles = true) 26 | val apiJar2 = ApiJar.from(serviceJarPath, keepMetaFiles = false) 27 | apiJar1 shouldNotBe apiJar2 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/ClassesTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.matchers.shouldBe 4 | import io.kotest.matchers.shouldNotBe 5 | import sh.christian.aaraar.utils.serviceJarPath 6 | import kotlin.test.Test 7 | 8 | class ClassesTest { 9 | @Test 10 | fun `test equality with meta files`() { 11 | val classes1 = Classes.from(serviceJarPath, keepMetaFiles = true) 12 | val classes2 = Classes.from(serviceJarPath, keepMetaFiles = true) 13 | classes1 shouldBe classes2 14 | } 15 | 16 | @Test 17 | fun `test equality without meta files`() { 18 | val classes1 = Classes.from(serviceJarPath, keepMetaFiles = false) 19 | val classes2 = Classes.from(serviceJarPath, keepMetaFiles = false) 20 | classes1 shouldBe classes2 21 | } 22 | 23 | @Test 24 | fun `test equality with differing meta files`() { 25 | val classes1 = Classes.from(serviceJarPath, keepMetaFiles = true) 26 | val classes2 = Classes.from(serviceJarPath, keepMetaFiles = false) 27 | classes1 shouldNotBe classes2 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /agp-compat/agp7/src/main/kotlin/sh/christian/aaraar/gradle/agp/Agp7AndroidVariant.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle.agp 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.LibraryVariant 5 | import org.gradle.api.Task 6 | import org.gradle.api.file.RegularFileProperty 7 | import org.gradle.api.tasks.TaskProvider 8 | 9 | internal class Agp7AndroidVariant( 10 | private val variant: LibraryVariant, 11 | ) : AndroidVariant { 12 | override val variantName: String = variant.name 13 | override val buildType: String? = variant.buildType 14 | override val namespace: String get() = variant.namespace.get() 15 | override val packaging: AndroidPackaging = Agp7AndroidPackaging(variant.packaging) 16 | 17 | override fun registerAarTransform( 18 | task: TaskProvider, 19 | inputAar: (T) -> RegularFileProperty, 20 | outputAar: (T) -> RegularFileProperty, 21 | ) { 22 | variant.artifacts 23 | .use(task) 24 | .wiredWithFiles(inputAar, outputAar) 25 | .toTransform(SingleArtifact.AAR) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /agp-compat/agp8/src/main/kotlin/sh/christian/aaraar/gradle/agp/Agp8AndroidVariant.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle.agp 2 | 3 | import com.android.build.api.artifact.SingleArtifact 4 | import com.android.build.api.variant.LibraryVariant 5 | import org.gradle.api.Task 6 | import org.gradle.api.file.RegularFileProperty 7 | import org.gradle.api.tasks.TaskProvider 8 | 9 | internal class Agp8AndroidVariant( 10 | private val variant: LibraryVariant, 11 | ) : AndroidVariant { 12 | override val variantName: String = variant.name 13 | override val buildType: String? = variant.buildType 14 | override val namespace: String get() = variant.namespace.get() 15 | override val packaging: AndroidPackaging = Agp8AndroidPackaging(variant.packaging) 16 | 17 | override fun registerAarTransform( 18 | task: TaskProvider, 19 | inputAar: (T) -> RegularFileProperty, 20 | outputAar: (T) -> RegularFileProperty, 21 | ) { 22 | variant.artifacts 23 | .use(task) 24 | .wiredWithFiles(inputAar, outputAar) 25 | .toTransform(SingleArtifact.AAR) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /fixtures/src/testFixtures/kotlin/sh/christian/aaraar/utils/fileSets.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.utils 2 | 3 | import io.kotest.matchers.maps.shouldHaveSize 4 | import io.kotest.matchers.nulls.shouldBeNull 5 | import io.kotest.matchers.nulls.shouldNotBeNull 6 | import io.kotest.matchers.shouldBe 7 | import sh.christian.aaraar.model.FileSet 8 | 9 | fun FileSet.forEntry(entry: String) = FileSetEntry(this, entry) 10 | 11 | fun FileSet.shouldContainExactly(vararg entries: String) { 12 | entries.forEach { entry -> 13 | forEntry(entry).shouldExist() 14 | } 15 | shouldHaveSize(entries.size) 16 | } 17 | 18 | data class FileSetEntry( 19 | private val fileSet: FileSet, 20 | private val name: String, 21 | ) { 22 | fun shouldExist() { 23 | fileSet[name].shouldNotBeNull() 24 | } 25 | 26 | fun shouldNotExist() { 27 | fileSet[name]?.decodeToString().shouldBeNull() 28 | } 29 | 30 | infix fun shouldHaveFileContents(contents: String) { 31 | val file = fileSet[name] 32 | file.shouldNotBeNull() 33 | file.decodeToString().normalizeWhitespace() shouldBe contents.trimIndent() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /fixtures/src/testFixtures/kotlin/sh/christian/aaraar/utils/assertions.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.utils 2 | 3 | import io.kotest.matchers.shouldBe 4 | import sh.christian.aaraar.model.AndroidManifest 5 | import sh.christian.aaraar.model.classeditor.ClassReference 6 | import java.nio.file.Files 7 | import java.nio.file.Path 8 | 9 | infix fun Path.shouldHaveContents(contents: String) { 10 | val output = Files.readString(this).normalizeWhitespace() 11 | output shouldBe contents.trimIndent() 12 | } 13 | 14 | infix fun AndroidManifest.shouldBe(contents: String) { 15 | val output = toString().normalizeWhitespace() 16 | output shouldBe contents.trimIndent() 17 | } 18 | 19 | infix fun ClassReference.shouldBeDecompiledTo(contents: String) { 20 | val output = decompile(this).normalizeWhitespace() 21 | output shouldBe contents.trimIndent() 22 | } 23 | 24 | infix fun ByteArray.shouldBeDecompiledTo(contents: String) { 25 | val output = decompile(this).normalizeWhitespace() 26 | output shouldBe contents.trimIndent() 27 | } 28 | 29 | fun String.normalizeWhitespace() = trim().replace("\t", " ").replace("\r\n", "\n") 30 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/Resources.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import java.nio.file.Path 4 | 5 | /** 6 | * Represents the contents of the `res/` folder. 7 | */ 8 | data class Resources( 9 | val files: FileSet, 10 | val packageName: String, 11 | val minSdk: Int, 12 | val androidAaptIgnore: String, 13 | ) { 14 | fun writeTo(path: Path) { 15 | files.writeTo(path) 16 | } 17 | 18 | companion object { 19 | fun from( 20 | path: Path, 21 | packageName: String, 22 | minSdk: Int, 23 | androidAaptIgnore: String, 24 | ): Resources { 25 | return FileSet.fromFileTree(path) 26 | ?.let { files -> 27 | Resources( 28 | files = files, 29 | packageName = packageName, 30 | minSdk = minSdk, 31 | androidAaptIgnore = androidAaptIgnore, 32 | ) 33 | } 34 | ?: Resources( 35 | files = FileSet.EMPTY, 36 | packageName = packageName, 37 | minSdk = minSdk, 38 | androidAaptIgnore = androidAaptIgnore, 39 | ) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/LibsShader.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl 2 | 3 | import sh.christian.aaraar.model.FileSet 4 | import sh.christian.aaraar.model.GenericJarArchive 5 | import sh.christian.aaraar.model.Libs 6 | import sh.christian.aaraar.model.ShadeConfiguration 7 | import sh.christian.aaraar.shading.Shader 8 | 9 | /** 10 | * Standard implementation for shading the `libs` folder by delegating to another JAR shader implementation. 11 | */ 12 | class LibsShader( 13 | private val genericJarArchiveShader: Shader, 14 | ) : Shader { 15 | override fun shade(source: Libs, shadeConfiguration: ShadeConfiguration): Libs { 16 | val shadedFiles = source.files.mapValues { (path, contents) -> 17 | if (path.substringAfterLast('.') == "jar") { 18 | GenericJarArchive.from(contents, keepMetaFiles = true) 19 | ?.let { genericJarArchiveShader.shade(it, shadeConfiguration) } 20 | ?.bytes() 21 | ?: contents 22 | } else { 23 | contents 24 | } 25 | } 26 | 27 | return Libs(FileSet(shadedFiles)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/JarArchiveMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import sh.christian.aaraar.merger.ArchiveMerger 4 | import sh.christian.aaraar.merger.Merger 5 | import sh.christian.aaraar.model.ArtifactArchive 6 | import sh.christian.aaraar.model.Classes 7 | import sh.christian.aaraar.model.JarArchive 8 | 9 | /** 10 | * Standard implementation for merging multiple archive dependencies into an `jar` file. 11 | */ 12 | class JarArchiveMerger( 13 | private val classesMerger: Merger, 14 | ) : ArchiveMerger { 15 | override fun merge(first: JarArchive, others: List): JarArchive { 16 | val mergedClasses = classesMerger.merge( 17 | first.classes, 18 | others.map { it.classes }, 19 | ) 20 | 21 | // Generally speaking a module producing a JAR should only have other JARs as dependencies. 22 | // However, even if this is not true (ie: somehow a JAR depends on an AAR), we will silently 23 | // throw away all the other AAR entries and publish the file merged archive as a JAR as well. 24 | return JarArchive(classes = mergedClasses) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /agp-compat/agp7/src/main/kotlin/sh/christian/aaraar/gradle/agp/Agp7AndroidPackaging.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle.agp 2 | 3 | import com.android.build.api.variant.JniLibsPackaging 4 | import com.android.build.api.variant.Packaging 5 | import com.android.build.api.variant.ResourcesPackaging 6 | import org.gradle.api.provider.SetProperty 7 | 8 | class Agp7AndroidPackaging(packaging: Packaging) : AndroidPackaging { 9 | override val jniLibs: AndroidPackaging.JniLibs = Agp7JniLibs(packaging.jniLibs) 10 | override val resources: AndroidPackaging.Resources = Agp7Resources(packaging.resources) 11 | 12 | private class Agp7JniLibs(jniLibs: JniLibsPackaging) : AndroidPackaging.JniLibs { 13 | override val pickFirsts: SetProperty = jniLibs.pickFirsts 14 | override val excludes: SetProperty = jniLibs.excludes 15 | } 16 | 17 | private class Agp7Resources(resources: ResourcesPackaging) : AndroidPackaging.Resources { 18 | override val pickFirsts: SetProperty = resources.pickFirsts 19 | override val merges: SetProperty = resources.merges 20 | override val excludes: SetProperty = resources.excludes 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /agp-compat/agp8/src/main/kotlin/sh/christian/aaraar/gradle/agp/Agp8AndroidPackaging.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle.agp 2 | 3 | import com.android.build.api.variant.JniLibsPackaging 4 | import com.android.build.api.variant.Packaging 5 | import com.android.build.api.variant.ResourcesPackaging 6 | import org.gradle.api.provider.SetProperty 7 | 8 | class Agp8AndroidPackaging(packaging: Packaging) : AndroidPackaging { 9 | override val jniLibs: AndroidPackaging.JniLibs = Agp8JniLibs(packaging.jniLibs) 10 | override val resources: AndroidPackaging.Resources = Agp8Resources(packaging.resources) 11 | 12 | private class Agp8JniLibs(jniLibs: JniLibsPackaging) : AndroidPackaging.JniLibs { 13 | override val pickFirsts: SetProperty = jniLibs.pickFirsts 14 | override val excludes: SetProperty = jniLibs.excludes 15 | } 16 | 17 | private class Agp8Resources(resources: ResourcesPackaging) : AndroidPackaging.Resources { 18 | override val pickFirsts: SetProperty = resources.pickFirsts 19 | override val merges: SetProperty = resources.merges 20 | override val excludes: SetProperty = resources.excludes 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/GenericJarArchiveMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import sh.christian.aaraar.merger.MergeRules 4 | import sh.christian.aaraar.merger.Merger 5 | import sh.christian.aaraar.merger.mergeContents 6 | import sh.christian.aaraar.model.GenericJarArchive 7 | 8 | /** 9 | * Standard jar-wise implementation for merging multiple `jar` files. 10 | * 11 | * If there are any duplicate file paths with differing file contents, the following logic will be applied: 12 | * - If the files have the same file contents, those contents are used. 13 | * - If the files are in the `META-INF/services/` subfolder, the file contents are appended. 14 | * - If the files are also `jar` files, they will recursively be merged with this same logic applied. 15 | * - Otherwise, an exception will be thrown. 16 | */ 17 | class GenericJarArchiveMerger( 18 | private val mergeRules: MergeRules, 19 | ) : Merger { 20 | override fun merge(first: GenericJarArchive, others: List): GenericJarArchive { 21 | return GenericJarArchive(mergeContents(first, others, this, mergeRules)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /agp-compat/agp7/src/main/kotlin/sh/christian/aaraar/gradle/agp/Agp7.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle.agp 2 | 3 | import com.android.build.api.attributes.BuildTypeAttr 4 | import com.android.build.api.dsl.LibraryExtension 5 | import com.android.build.api.variant.LibraryAndroidComponentsExtension 6 | import org.gradle.api.Project 7 | import org.gradle.api.attributes.AttributeContainer 8 | import org.gradle.kotlin.dsl.getByType 9 | import org.gradle.kotlin.dsl.named 10 | 11 | /** 12 | * Compatibility layer for using AGP 7 at runtime. 13 | */ 14 | class Agp7(private val project: Project) : AgpCompat { 15 | private val androidComponents = project.extensions.getByType() 16 | 17 | override val android: AndroidExtension = 18 | Agp7AndroidExtension(project.extensions.getByType()) 19 | 20 | override fun AttributeContainer.buildTypeAttribute(buildType: String) { 21 | attribute(BuildTypeAttr.ATTRIBUTE, project.objects.named(buildType)) 22 | } 23 | 24 | override fun onVariants(callback: (AndroidVariant) -> Unit) { 25 | return androidComponents.onVariants { callback(Agp7AndroidVariant(it)) } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /agp-compat/agp8/src/main/kotlin/sh/christian/aaraar/gradle/agp/Agp8.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle.agp 2 | 3 | import com.android.build.api.attributes.BuildTypeAttr 4 | import com.android.build.api.dsl.LibraryExtension 5 | import com.android.build.api.variant.LibraryAndroidComponentsExtension 6 | import org.gradle.api.Project 7 | import org.gradle.api.attributes.AttributeContainer 8 | import org.gradle.kotlin.dsl.getByType 9 | import org.gradle.kotlin.dsl.named 10 | 11 | /** 12 | * Compatibility layer for using AGP 8 at runtime. 13 | */ 14 | class Agp8(private val project: Project) : AgpCompat { 15 | private val androidComponents = project.extensions.getByType() 16 | 17 | override val android: AndroidExtension = 18 | Agp8AndroidExtension(project.extensions.getByType()) 19 | 20 | override fun AttributeContainer.buildTypeAttribute(buildType: String) { 21 | attribute(BuildTypeAttr.ATTRIBUTE, project.objects.named(buildType)) 22 | } 23 | 24 | override fun onVariants(callback: (AndroidVariant) -> Unit) { 25 | return androidComponents.onVariants { callback(Agp8AndroidVariant(it)) } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/sh/christian/aaraar/gradle/PackageAarTask.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle 2 | 3 | import org.gradle.api.file.RegularFileProperty 4 | import org.gradle.api.provider.Property 5 | import org.gradle.api.tasks.CacheableTask 6 | import org.gradle.api.tasks.Input 7 | import org.gradle.api.tasks.InputFile 8 | import org.gradle.api.tasks.Optional 9 | import org.gradle.api.tasks.OutputFile 10 | import org.gradle.api.tasks.PathSensitive 11 | import org.gradle.api.tasks.PathSensitivity 12 | import sh.christian.aaraar.Environment 13 | 14 | @CacheableTask 15 | abstract class PackageAarTask : PackageArchiveTask() { 16 | @get:InputFile 17 | @get:PathSensitive(PathSensitivity.RELATIVE) 18 | val inputAar: RegularFileProperty get() = inputArchive 19 | 20 | @get:OutputFile 21 | val outputAar: RegularFileProperty get() = outputArchive 22 | 23 | @get:Input 24 | @get:Optional 25 | abstract val androidAaptIgnore: Property 26 | 27 | final override fun environment(): Environment { 28 | return Environment( 29 | androidAaptIgnore = androidAaptIgnore.get(), 30 | keepClassesMetaFiles = keepMetaFiles.get(), 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/types/platform.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor.types 2 | 3 | import sh.christian.aaraar.model.classeditor.ClassReference 4 | import sh.christian.aaraar.model.classeditor.Classpath 5 | import sh.christian.aaraar.model.classeditor.MutableClassReference 6 | import sh.christian.aaraar.model.classeditor.MutableClasspath 7 | 8 | /** The platform [Class] type. */ 9 | val Classpath.classType: ClassReference 10 | get() = this["java.lang.Class"] 11 | 12 | /** The platform [Object] type. */ 13 | val Classpath.objectType: ClassReference 14 | get() = this["java.lang.Object"] 15 | 16 | /** The platform [String] type. */ 17 | val Classpath.stringType: ClassReference 18 | get() = this["java.lang.String"] 19 | 20 | /** The platform [Class] type. */ 21 | val MutableClasspath.classType: MutableClassReference 22 | get() = this["java.lang.Class"] 23 | 24 | /** The platform [Object] type. */ 25 | val MutableClasspath.objectType: MutableClassReference 26 | get() = this["java.lang.Object"] 27 | 28 | /** The platform [String] type. */ 29 | val MutableClasspath.stringType: MutableClassReference 30 | get() = this["java.lang.String"] 31 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/pipeline/ClassFileFilter.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.pipeline 2 | 3 | import sh.christian.aaraar.shading.impl.transform.ClassDelete 4 | import sh.christian.aaraar.shading.impl.transform.JarProcessor 5 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Companion.EXT_CLASS 6 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.DISCARD 7 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.KEEP 8 | import sh.christian.aaraar.shading.impl.transform.Transformable 9 | 10 | internal class ClassFileFilter( 11 | classDeletes: Set, 12 | ) : JarProcessor { 13 | private val classDeletePatterns = classDeletes.map { ClassDelete(it) } 14 | 15 | override fun process(struct: Transformable): JarProcessor.Result { 16 | if (classDeletePatterns.isEmpty() || !struct.name.endsWith(EXT_CLASS)) return KEEP 17 | 18 | return if (shouldDeletePath(struct.name.removeSuffix(EXT_CLASS))) { 19 | DISCARD 20 | } else { 21 | KEEP 22 | } 23 | } 24 | 25 | private fun shouldDeletePath(className: String): Boolean { 26 | return classDeletePatterns.any { it.matches(className) } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sample-lib/helper-library/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.library" 3 | id "org.jetbrains.kotlin.android" 4 | id "maven-publish" 5 | } 6 | 7 | group = "sh.christian.samplelib" 8 | version = "1.0.0" 9 | 10 | android { 11 | namespace "sh.christian.samplelib.helper" 12 | compileSdk 33 13 | 14 | defaultConfig { 15 | minSdk 24 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | } 22 | } 23 | 24 | buildFeatures { 25 | buildConfig true 26 | } 27 | 28 | packagingOptions { 29 | exclude "**/module-info.class" 30 | } 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_11 34 | targetCompatibility JavaVersion.VERSION_11 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = "11" 39 | } 40 | 41 | publishing { 42 | singleVariant("release") { 43 | withSourcesJar() 44 | } 45 | } 46 | } 47 | 48 | dependencies { 49 | api fileTree(dir: "libs", include: ["*.jar"]) 50 | implementation "androidx.core:core-ktx:1.9.0" 51 | implementation "org.jetbrains.kotlin:kotlin-stdlib:1.8.0" 52 | } 53 | 54 | afterEvaluate { 55 | publishing { 56 | publications { 57 | maven(MavenPublication) { 58 | from components.release 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/FileSetTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.assertions.withClue 4 | import io.kotest.matchers.maps.shouldBeEmpty 5 | import io.kotest.matchers.shouldBe 6 | import io.kotest.matchers.shouldNotBe 7 | import sh.christian.aaraar.utils.div 8 | import sh.christian.aaraar.utils.ktLibraryJarPath 9 | import sh.christian.aaraar.utils.withFileSystem 10 | import kotlin.test.Test 11 | 12 | class FileSetTest { 13 | 14 | @Test 15 | fun `from nonexistent file tree returns null`() { 16 | withFileSystem { 17 | (FileSet.fromFileTree(root / "Documents and Settings")) shouldBe null 18 | } 19 | } 20 | 21 | @Test 22 | fun `from empty file tree returns empty FileSet`() { 23 | withFileSystem { 24 | val fileSet = FileSet.fromFileTree(root) 25 | withClue("FileSet from root folder") { 26 | fileSet shouldNotBe null 27 | } 28 | 29 | withDirectory { 30 | fileSet!!.writeTo(root) 31 | files().shouldBeEmpty() 32 | } 33 | } 34 | } 35 | 36 | @Test 37 | fun `test equality`() { 38 | val fileSet1 = FileSet.fromFileTree(ktLibraryJarPath.parent) 39 | val fileSet2 = FileSet.fromFileTree(ktLibraryJarPath.parent) 40 | fileSet1 shouldBe fileSet2 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/sh/christian/aaraar/gradle/ClassNameArtifactArchiveProcessorFactory.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle 2 | 3 | fun artifactArchiveProcessorFromClassName(processorFactoryClass: String): ArtifactArchiveProcessor.Factory { 4 | return ClassNameArtifactArchiveProcessorFactory(processorFactoryClass) 5 | } 6 | 7 | private class ClassNameArtifactArchiveProcessorFactory( 8 | private val processorFactoryClass: String, 9 | ) : ArtifactArchiveProcessor.Factory { 10 | private val delegate: ArtifactArchiveProcessor.Factory by lazy { 11 | val apiJarProcessorType = try { 12 | Class.forName(processorFactoryClass) 13 | } catch (e: ClassNotFoundException) { 14 | throw IllegalArgumentException("Couldn't load '$processorFactoryClass' class.", e) 15 | } 16 | 17 | val constructor = try { 18 | apiJarProcessorType.getConstructor() 19 | } catch (e: NoSuchMethodException) { 20 | throw IllegalArgumentException("No public no-arg constructor on '$processorFactoryClass'.", e) 21 | } 22 | 23 | constructor.newInstance() as? ArtifactArchiveProcessor.Factory 24 | ?: throw IllegalArgumentException("$processorFactoryClass does not implement ArtifactArchiveProcessor.Factory") 25 | } 26 | 27 | override fun create(): ArtifactArchiveProcessor { 28 | return delegate.create() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/AarArchiveShader.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl 2 | 3 | import sh.christian.aaraar.model.AarArchive 4 | import sh.christian.aaraar.model.Classes 5 | import sh.christian.aaraar.model.Libs 6 | import sh.christian.aaraar.model.ShadeConfiguration 7 | import sh.christian.aaraar.shading.Shader 8 | 9 | /** 10 | * Standard implementation for shading an AAR artifact by shading `classes.jar` and all JAR files in the `libs` folder. 11 | */ 12 | class AarArchiveShader( 13 | private val classesShader: Shader, 14 | private val libsShader: Shader, 15 | ) : Shader { 16 | override fun shade(source: AarArchive, shadeConfiguration: ShadeConfiguration): AarArchive { 17 | return AarArchive( 18 | aarMetadata = source.aarMetadata, 19 | androidManifest = source.androidManifest, 20 | classes = classesShader.shade(source.classes, shadeConfiguration), 21 | resources = source.resources, 22 | rTxt = source.rTxt, 23 | publicTxt = source.publicTxt, 24 | assets = source.assets, 25 | libs = libsShader.shade(source.libs, shadeConfiguration), 26 | jni = source.jni, 27 | proguard = source.proguard, 28 | lintRules = source.lintRules, 29 | navigationJson = source.navigationJson, 30 | apiJar = source.apiJar, 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /agp-compat/base/src/main/kotlin/sh/christian/aaraar/gradle/agp/AgpCompat.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle.agp 2 | 3 | import org.gradle.api.attributes.AttributeContainer 4 | 5 | /** 6 | * Compatibility layer for interacting with the Android Gradle Plugin from the Aaraar plugin. 7 | * 8 | * We cannot predict exactly which version of AGP will be on our classpath at runtime, and we want 9 | * to be able to support at least a few different major versions at a time, some of which may be 10 | * binary-incompatible with one another and have breaking API changes. 11 | * 12 | * Because of this, we need to create a facade in front of any AGP-defined type, using our own 13 | * classes in order to create the same kind of behaviour across different AGP versions. 14 | */ 15 | interface AgpCompat { 16 | /** 17 | * Returns a shim of the `android` extension registered on an Android module. 18 | */ 19 | val android: AndroidExtension 20 | 21 | /** 22 | * Sets the build type attribute on an attributable object with the given build type name. 23 | */ 24 | fun AttributeContainer.buildTypeAttribute(buildType: String) 25 | 26 | /** 27 | * Allows for registration of a callback to be called with variant instances. 28 | * This can be used to modify compilation behaviour of a given variant at configuration time. 29 | */ 30 | fun onVariants(callback: (AndroidVariant) -> Unit) 31 | } 32 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | NEXT_SNAPSHOT_VERSION="${1-}" 6 | 7 | if [[ -z "${1}" ]]; then 8 | echo "Usage: $0 " >&2 9 | exit 1 10 | elif ! [[ "${1}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then 11 | echo "Error: '${1}' is not a valid semantic version." >&2 12 | echo "Usage: $0 " >&2 13 | exit 1 14 | fi 15 | 16 | git checkout main 17 | 18 | if [[ -n "$(git status --porcelain)" ]]; then 19 | echo "Error: Uncommitted changes present. Please re-run with no local changes." >&2 20 | exit 1 21 | fi 22 | 23 | sed -i '' "s/-SNAPSHOT//g" gradle.properties 24 | 25 | NEXT_RELEASE="$(awk -F= '/POM_VERSION/ { print $2 }' < gradle.properties)" 26 | CURRENT_RELEASE="$(awk -F\" '/id "sh.christian.aaraar"/ { print $4 }' < sample-lib/library/build.gradle)" 27 | sed -i '' "s/$CURRENT_RELEASE/$NEXT_RELEASE/g" sample-lib/library/build.gradle 28 | sed -i '' "s/$CURRENT_RELEASE/$NEXT_RELEASE/g" docs/installation.md 29 | sed -i '' "s/$CURRENT_RELEASE/$NEXT_RELEASE/g" README.md 30 | 31 | git add README.md 32 | git add gradle.properties 33 | git add docs 34 | git add sample-lib/library/build.gradle 35 | 36 | git commit -m "Releasing v$NEXT_RELEASE" 37 | git tag "v$NEXT_RELEASE" 38 | 39 | sed -i '' "s/$NEXT_RELEASE/$NEXT_SNAPSHOT_VERSION-SNAPSHOT/g" gradle.properties 40 | 41 | git add gradle.properties 42 | git commit -m "Prepare next development cycle." 43 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/pipeline/ServiceLoaderShader.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.pipeline 2 | 3 | import sh.christian.aaraar.shading.impl.transform.ClassRename 4 | import sh.christian.aaraar.shading.impl.transform.JarProcessor 5 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.KEEP 6 | import sh.christian.aaraar.shading.impl.transform.PackageRemapper 7 | import sh.christian.aaraar.shading.impl.transform.Transformable 8 | 9 | internal class ServiceLoaderShader( 10 | classRenames: Map, 11 | ) : JarProcessor { 12 | private val packageRemapper = PackageRemapper( 13 | classRenames.map { (pattern, result) -> ClassRename(pattern, result) } 14 | ) 15 | 16 | override fun process(struct: Transformable): JarProcessor.Result { 17 | if (!struct.name.startsWith("META-INF/services/")) return KEEP 18 | 19 | val originalFile = struct.data.decodeToString() 20 | 21 | struct.data = buildString { 22 | val line = StringBuilder() 23 | 24 | originalFile.forEach { c -> 25 | if (c == '\n' || c == '\r') { 26 | append(packageRemapper.mapValue(line.toString())) 27 | append(c) 28 | line.clear() 29 | } else { 30 | line.append(c) 31 | } 32 | } 33 | 34 | append(packageRemapper.mapValue(line.toString())) 35 | }.encodeToByteArray() 36 | 37 | return KEEP 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/deploy_docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | 3 | on: 4 | workflow_call: 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: write 9 | 10 | concurrency: 11 | group: "pages" 12 | cancel-in-progress: false 13 | 14 | jobs: 15 | deploy-mkdocs: 16 | runs-on: ubuntu-latest 17 | if: github.repository == 'christiandeange/aaraar' 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: gradle/actions/wrapper-validation@v3 22 | - uses: actions/setup-java@v4 23 | with: 24 | distribution: 'temurin' 25 | java-version: '17' 26 | check-latest: true 27 | - uses: actions/setup-python@v5 28 | with: 29 | python-version: 3.x 30 | 31 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 32 | - uses: actions/cache@v4 33 | with: 34 | key: mkdocs-material-${{ env.cache_id }} 35 | path: .cache 36 | restore-keys: | 37 | mkdocs-material- 38 | 39 | - name: Build API Reference 40 | run: ./gradlew dokkaHtmlCollector 41 | 42 | - name: Install MkDocs 43 | run: pip install mkdocs-material 44 | 45 | - name: Build MkDocs 46 | run: | 47 | rm docs/changelog.md 48 | cp CHANGELOG.md docs/changelog.md 49 | mkdocs build 50 | 51 | - name: Deploy MkDocs to GitHub Pages 52 | run: mkdocs gh-deploy --force 53 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/FileSet.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import sh.christian.aaraar.utils.div 4 | import sh.christian.aaraar.utils.mkdirs 5 | import java.nio.file.Files 6 | import java.nio.file.Path 7 | import kotlin.streams.asSequence 8 | 9 | /** 10 | * Represents an arbitrary set of files, indexed by their relative file path to a specified root. 11 | */ 12 | class FileSet( 13 | val indexedFiles: Map, 14 | ) : Map by indexedFiles { 15 | override fun equals(other: Any?): Boolean { 16 | if (other !is FileSet) return false 17 | return contentEquals(indexedFiles, other.indexedFiles) 18 | } 19 | 20 | override fun hashCode(): Int { 21 | return contentHashCode(indexedFiles) 22 | } 23 | 24 | fun writeTo(path: Path) { 25 | indexedFiles.forEach { (entry, bytes) -> 26 | val filePath = (path / entry).mkdirs() 27 | Files.write(filePath, bytes) 28 | } 29 | } 30 | 31 | companion object { 32 | val EMPTY = FileSet(indexedFiles = emptyMap()) 33 | 34 | fun fromFileTree(path: Path): FileSet? { 35 | if (!Files.exists(path)) return null 36 | 37 | val indexedFiles = Files.walk(path) 38 | .asSequence() 39 | .filter(Files::isRegularFile) 40 | .map { path.relativize(it).toString() to Files.readAllBytes(it) } 41 | .toMap() 42 | 43 | return FileSet(indexedFiles) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /gradle-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 2 | import org.gradle.api.tasks.testing.logging.TestLogEvent 3 | 4 | plugins { 5 | @Suppress("DSL_SCOPE_VIOLATION") val plugins = libs.plugins 6 | 7 | alias(plugins.kotlin.jvm) 8 | `java-gradle-plugin` 9 | `kotlin-dsl` 10 | id("aaraar-detekt") 11 | id("aaraar-publish") 12 | } 13 | 14 | val fixtureJars by configurations.registering 15 | 16 | dependencies { 17 | implementation(project(":core")) 18 | implementation(project(":agp-compat:agp7")) 19 | implementation(project(":agp-compat:agp8")) 20 | 21 | implementation(platform(kotlin("bom"))) 22 | compileOnly(libs.agp.api.latest) 23 | 24 | testImplementation(testFixtures(project(":fixtures"))) 25 | testImplementation(kotlin("test")) 26 | testImplementation(libs.kotest) 27 | 28 | fixtureJars(project(":fixtures", configuration = "fixtureJars")) 29 | } 30 | 31 | tasks.test { 32 | useJUnitPlatform() 33 | 34 | testLogging { 35 | exceptionFormat = TestExceptionFormat.FULL 36 | events = TestLogEvent.values().toSet() - TestLogEvent.STARTED 37 | } 38 | } 39 | 40 | kotlin { 41 | sourceSets { 42 | test { 43 | resources.srcDirs(fixtureJars) 44 | } 45 | } 46 | } 47 | 48 | gradlePlugin { 49 | plugins { 50 | create("aaraar") { 51 | id = "sh.christian.aaraar" 52 | implementationClass = "sh.christian.aaraar.gradle.AarAarPlugin" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/ShadeConfiguration.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * Defines the rules for shading, following a syntax for pattern matching classes and resources. 7 | * 8 | * The pattern will be matched against class names or resource paths, allowing for wildcards to be specified: 9 | * - `*` will match a single package component or path part. 10 | * - `**` will match against the remainder of any valid fully-qualified class name or resource path. 11 | * 12 | * For renamed classes and resources, the replacement string can reference the substrings matched by the wildcards. 13 | * A numbered reference is available for every wildcard in the pattern, starting from left to right: `@1`, `@2`, etc. 14 | * A special `@0` reference contains the entire matched class name or resource path. 15 | */ 16 | data class ShadeConfiguration( 17 | val classRenames: Map, 18 | val classDeletes: Set, 19 | val resourceRenames: Map, 20 | val resourceDeletes: Set, 21 | ) : Serializable { 22 | 23 | /** 24 | * Returns `true` if there are no rules specified, or `false` otherwise. 25 | */ 26 | fun isEmpty(): Boolean { 27 | return classRenames.isEmpty() && classDeletes.isEmpty() && resourceRenames.isEmpty() && resourceDeletes.isEmpty() 28 | } 29 | 30 | companion object { 31 | private const val serialVersionUID = 1L 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/utils/xml.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.utils 2 | 3 | import com.android.SdkConstants.XMLNS_PREFIX 4 | import com.android.utils.forEach 5 | import org.redundent.kotlin.xml.Node 6 | import org.redundent.kotlin.xml.node 7 | import org.w3c.dom.Node as W3CNode 8 | 9 | internal fun W3CNode.toNode(): Node { 10 | return node(nodeName) { 11 | copyNode(this@toNode, this) 12 | }.first(nodeName) 13 | } 14 | 15 | private fun copyNode(source: W3CNode, dest: Node) { 16 | when (source.nodeType) { 17 | W3CNode.ELEMENT_NODE -> { 18 | val cur = dest.element(source.nodeName) 19 | copyAttributes(source, cur) 20 | source.childNodes.forEach { child -> copyNode(child, cur) } 21 | } 22 | 23 | W3CNode.CDATA_SECTION_NODE -> { 24 | dest.cdata(source.nodeValue) 25 | } 26 | 27 | W3CNode.TEXT_NODE -> { 28 | dest.text(source.nodeValue) 29 | } 30 | 31 | W3CNode.COMMENT_NODE -> { 32 | dest.comment(source.nodeValue) 33 | } 34 | } 35 | } 36 | 37 | private fun copyAttributes(source: W3CNode, dest: Node) { 38 | val attributes = source.attributes 39 | if (attributes == null || attributes.length == 0) { 40 | return 41 | } 42 | 43 | attributes.forEach { 44 | if (it.nodeName.startsWith(XMLNS_PREFIX)) { 45 | dest.namespace(it.nodeName.removePrefix(XMLNS_PREFIX), it.nodeValue) 46 | } else { 47 | dest.attribute(it.nodeName, it.nodeValue) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/pipeline/ClassFileShader.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.pipeline 2 | 3 | import org.objectweb.asm.ClassReader 4 | import org.objectweb.asm.ClassWriter 5 | import org.objectweb.asm.commons.ClassRemapper 6 | import sh.christian.aaraar.shading.impl.transform.ClassRename 7 | import sh.christian.aaraar.shading.impl.transform.JarProcessor 8 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Companion.EXT_CLASS 9 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.KEEP 10 | import sh.christian.aaraar.shading.impl.transform.PackageRemapper 11 | import sh.christian.aaraar.shading.impl.transform.Transformable 12 | 13 | internal class ClassFileShader( 14 | classRenames: Map, 15 | ) : JarProcessor { 16 | private val packageRemapper = PackageRemapper( 17 | classRenames.map { (pattern, result) -> ClassRename(pattern, result) } 18 | ) 19 | 20 | override fun process(struct: Transformable): JarProcessor.Result { 21 | if (!struct.name.endsWith(EXT_CLASS)) return KEEP 22 | 23 | val classSource = ClassReader(struct.data) 24 | val classWriter = ClassWriter(classSource, 0) 25 | val visitor = ClassRemapper(classWriter, packageRemapper) 26 | 27 | classSource.accept(visitor, 0) 28 | 29 | struct.name = struct.name.removeSuffix(EXT_CLASS).let(packageRemapper::mapType) + EXT_CLASS 30 | struct.data = classWriter.toByteArray() 31 | 32 | return KEEP 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fixtures/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | @Suppress("DSL_SCOPE_VIOLATION") val plugins = libs.plugins 3 | 4 | alias(plugins.kotlin.jvm) 5 | `kotlin-dsl` 6 | `java-test-fixtures` 7 | id("aaraar-detekt") 8 | } 9 | 10 | dependencies { 11 | testFixturesApi(project(":core")) 12 | testFixturesApi(libs.agp.tools.manifestmerger) 13 | 14 | testFixturesImplementation(libs.kotest) 15 | testFixturesImplementation(libs.decompiler) 16 | testFixturesImplementation(libs.jimfs) 17 | } 18 | 19 | tasks.withType { 20 | duplicatesStrategy = DuplicatesStrategy.INCLUDE 21 | } 22 | 23 | val fixtureJarsDir = layout.buildDirectory.dir("fixture-jars") 24 | val fixtureJars by configurations.registering 25 | 26 | registerSourceSet("animal") 27 | registerSourceSet("annotations") 28 | registerSourceSet("foo") 29 | registerSourceSet("foo2") 30 | registerSourceSet("ktLibrary") 31 | registerSourceSet("service") 32 | 33 | fun registerSourceSet(name: String) { 34 | val newSourceSet = sourceSets.create(name) { 35 | java.srcDir("src/$name/java") 36 | kotlin.srcDir("src/$name/kotlin") 37 | resources.srcDir("src/$name/resources") 38 | } 39 | 40 | val newSourceSetJar = tasks.register("${name}Jar") { 41 | from(newSourceSet.output) 42 | destinationDirectory.set(fixtureJarsDir) 43 | archiveFileName.set("$name.jar") 44 | } 45 | 46 | artifacts { 47 | add(fixtureJars.name, newSourceSetJar.flatMap { it.destinationDirectory }) { 48 | builtBy(newSourceSetJar) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 4 | import org.gradle.api.tasks.testing.logging.TestLogEvent 5 | 6 | plugins { 7 | @Suppress("DSL_SCOPE_VIOLATION") val plugins = libs.plugins 8 | 9 | alias(plugins.kotlin.jvm) 10 | `kotlin-dsl` 11 | id("aaraar-detekt") 12 | id("aaraar-publish") 13 | } 14 | 15 | val fixtureJars by configurations.registering 16 | 17 | dependencies { 18 | api(libs.agp.tools.common) 19 | api(libs.kotlin.metadata) 20 | 21 | implementation(platform(kotlin("bom"))) 22 | implementation(libs.agp.layoutlib) 23 | implementation(libs.agp.tools.manifestmerger) 24 | implementation(libs.agp.tools.sdk) 25 | implementation(libs.asm) 26 | implementation(libs.gson) 27 | implementation(libs.javassist) 28 | implementation(libs.kotlinxml) 29 | 30 | testImplementation(testFixtures(project(":fixtures"))) 31 | testImplementation(kotlin("test")) 32 | testImplementation(libs.kotest) 33 | 34 | fixtureJars(project(":fixtures", configuration = "fixtureJars")) 35 | } 36 | 37 | tasks.test { 38 | useJUnitPlatform() 39 | 40 | testLogging { 41 | exceptionFormat = TestExceptionFormat.FULL 42 | events = TestLogEvent.values().toSet() - TestLogEvent.STARTED 43 | } 44 | } 45 | 46 | kotlin { 47 | sourceSets { 48 | all { 49 | languageSettings.apply { 50 | optIn("kotlin.ExperimentalStdlibApi") 51 | } 52 | } 53 | 54 | test { 55 | resources.srcDirs(fixtureJars) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/sh/christian/aaraar/gradle/ArtifactTypeDependencyRules.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle 2 | 3 | import org.gradle.api.attributes.AttributeCompatibilityRule 4 | import org.gradle.api.attributes.AttributeDisambiguationRule 5 | import org.gradle.api.attributes.CompatibilityCheckDetails 6 | import org.gradle.api.attributes.MultipleCandidatesDetails 7 | 8 | internal const val MERGEABLE_ARTIFACT_TYPE = "mergeable-artifact" 9 | internal const val MERGED_AAR_TYPE = "merged-aar" 10 | internal const val MERGED_JAR_TYPE = "merged-jar" 11 | 12 | private val MERGEABLE_TYPES = setOf(MERGED_AAR_TYPE, "aar", "android-lint-local-aar", MERGED_JAR_TYPE, "jar") 13 | 14 | internal class ArtifactTypeCompatibilityDependencyRule : AttributeCompatibilityRule { 15 | override fun execute(t: CompatibilityCheckDetails) { 16 | if (t.consumerValue == MERGEABLE_ARTIFACT_TYPE || t.consumerValue == null) { 17 | if (t.producerValue in MERGEABLE_TYPES) { 18 | t.compatible() 19 | } else if (t.consumerValue == MERGEABLE_ARTIFACT_TYPE) { 20 | t.incompatible() 21 | } 22 | } 23 | } 24 | } 25 | 26 | internal class ArtifactTypeDisambiguationDependencyRule : AttributeDisambiguationRule { 27 | override fun execute(t: MultipleCandidatesDetails) { 28 | if (t.consumerValue == MERGEABLE_ARTIFACT_TYPE || t.consumerValue == null) { 29 | MERGEABLE_TYPES.firstOrNull { it in t.candidateValues }?.let { preferredType -> 30 | t.closestMatch(preferredType) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 35 | 36 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | publish-release: 13 | runs-on: ubuntu-latest 14 | if: github.repository == 'christiandeange/aaraar' 15 | timeout-minutes: 30 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: gradle/actions/wrapper-validation@v3 20 | - uses: actions/setup-java@v4 21 | with: 22 | distribution: 'temurin' 23 | java-version: '17' 24 | check-latest: true 25 | 26 | - name: Publish Release 27 | env: 28 | SONATYPE_CENTRAL_USERNAME: ${{ secrets.SONATYPE_CENTRAL_USERNAME }} 29 | SONATYPE_CENTRAL_PASSWORD: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }} 30 | ARTIFACT_SIGNING_PRIVATE_KEY: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} 31 | ARTIFACT_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY_PASSWORD }} 32 | run: | 33 | ORG_GRADLE_PROJECT_mavenCentralUsername="$SONATYPE_CENTRAL_USERNAME" \ 34 | ORG_GRADLE_PROJECT_mavenCentralPassword="$SONATYPE_CENTRAL_PASSWORD" \ 35 | ORG_GRADLE_PROJECT_signingInMemoryKey="$ARTIFACT_SIGNING_PRIVATE_KEY" \ 36 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword="$ARTIFACT_SIGNING_PRIVATE_KEY_PASSWORD" \ 37 | ./gradlew clean publish --no-build-cache --no-daemon --stacktrace 38 | 39 | deploy-mkdocs: 40 | uses: ./.github/workflows/deploy_docs.yml 41 | secrets: inherit 42 | needs: 43 | - publish-release 44 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/merger/impl/AndroidManifestMerger.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import com.android.manifmerger.ManifestMerger2 4 | import com.android.manifmerger.MergingReport 5 | import com.android.utils.StdLogger 6 | import sh.christian.aaraar.merger.Merger 7 | import sh.christian.aaraar.model.AndroidManifest 8 | 9 | /** 10 | * Standard implementation for merging multiple `AndroidManifest.xml` files. 11 | * 12 | * The basis of this implementation uses the same manifest merging logic that the Android Gradle Plugin uses. 13 | */ 14 | class AndroidManifestMerger : Merger { 15 | override fun merge(first: AndroidManifest, others: List): AndroidManifest { 16 | val mergeReport = ManifestMerger2.newMerger( 17 | first.asTempFile(), 18 | StdLogger(StdLogger.Level.WARNING), 19 | ManifestMerger2.MergeType.APPLICATION 20 | ) 21 | .withFeatures(ManifestMerger2.Invoker.Feature.NO_PLACEHOLDER_REPLACEMENT) 22 | .withFeatures(ManifestMerger2.Invoker.Feature.REMOVE_TOOLS_DECLARATIONS) 23 | .apply { 24 | others.forEach { other -> 25 | addLibraryManifest(other.asTempFile()) 26 | } 27 | } 28 | .merge() 29 | 30 | check(mergeReport.result != MergingReport.Result.ERROR) { 31 | """ 32 | Failed to merge manifest. ${mergeReport.reportString} 33 | 34 | ${mergeReport.loggingRecords.joinToString("\n")} 35 | """.trimIndent() 36 | } 37 | 38 | return AndroidManifest(mergeReport.getMergedDocument(MergingReport.MergedManifestKind.MERGED)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/AndroidManifest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import org.redundent.kotlin.xml.Node 4 | import org.redundent.kotlin.xml.parse 5 | import java.io.File 6 | import java.io.FileOutputStream 7 | import java.io.OutputStreamWriter 8 | import java.nio.file.Files 9 | import java.nio.file.Path 10 | 11 | /** 12 | * Represents the contents of the `AndroidManifest.xml` file. 13 | */ 14 | class AndroidManifest 15 | internal constructor( 16 | private val manifestNode: Node, 17 | ) { 18 | constructor(xmlSource: String) : this(parse(xmlSource.byteInputStream())) 19 | 20 | val packageName: String by lazy { 21 | manifestNode.get("package")!! 22 | } 23 | 24 | val minSdk: Int by lazy { 25 | manifestNode.first("uses-sdk").get("android:minSdkVersion")!!.toInt() 26 | } 27 | 28 | override fun toString(): String { 29 | return manifestNode.toString() 30 | } 31 | 32 | override fun equals(other: Any?): Boolean { 33 | if (other !is AndroidManifest) return false 34 | return manifestNode == other.manifestNode 35 | } 36 | 37 | override fun hashCode(): Int { 38 | return manifestNode.hashCode() 39 | } 40 | 41 | fun writeTo(path: Path) { 42 | OutputStreamWriter(Files.newOutputStream(path)).use { 43 | manifestNode.writeTo(it) 44 | } 45 | } 46 | 47 | internal fun asTempFile(): File { 48 | val file = Files.createTempFile("AndroidManifest", ".xml").toFile() 49 | FileOutputStream(file).writer().use { 50 | manifestNode.writeTo(it) 51 | } 52 | return file 53 | } 54 | 55 | companion object { 56 | fun from(path: Path): AndroidManifest { 57 | return AndroidManifest(parse(Files.newInputStream(path))) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /agp-compat/base/src/main/kotlin/sh/christian/aaraar/gradle/agp/AndroidVariant.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle.agp 2 | 3 | import org.gradle.api.Task 4 | import org.gradle.api.file.RegularFileProperty 5 | import org.gradle.api.tasks.TaskProvider 6 | 7 | /** 8 | * A facade of some of the interactions with Android module variants. 9 | */ 10 | interface AndroidVariant { 11 | /** 12 | * The name of the variant. 13 | */ 14 | val variantName: String 15 | 16 | /** 17 | * The name of the variant's build type, if present. 18 | */ 19 | val buildType: String? 20 | 21 | /** 22 | * The namespace of the variant for generated R and BuildConfig classes. 23 | */ 24 | val namespace: String 25 | 26 | /** 27 | * The packaging options for handling resource merge conflicts from dependencies. 28 | */ 29 | val packaging: AndroidPackaging 30 | 31 | /** 32 | * Register a transformation of the AAR produced by this variant. 33 | * [inputAar] is set to the input, and the transformed AAR should be written to [outputAar]. 34 | */ 35 | fun registerAarTransform( 36 | task: TaskProvider, 37 | inputAar: (T) -> RegularFileProperty, 38 | outputAar: (T) -> RegularFileProperty, 39 | ) 40 | 41 | /** 42 | * Returns a string using an optional prefix and suffix to surround the variant name, applying the default 43 | * snake-casing formatting convention that Gradle naming often follows. 44 | */ 45 | fun name( 46 | prefix: String = "", 47 | suffix: String = "", 48 | ): String { 49 | return if (prefix.isEmpty()) { 50 | variantName + suffix 51 | } else if (prefix.last().isLetterOrDigit()) { 52 | @Suppress("DEPRECATION") 53 | prefix + variantName.capitalize() + suffix 54 | } else { 55 | prefix + variantName + suffix 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/classeditor/ClasspathTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | import io.kotest.matchers.collections.shouldBeEmpty 4 | import io.kotest.matchers.shouldBe 5 | import sh.christian.aaraar.model.GenericJarArchive 6 | import sh.christian.aaraar.utils.annotationsJarPath 7 | import sh.christian.aaraar.utils.loadJar 8 | import sh.christian.aaraar.utils.shouldContainExactly 9 | import sh.christian.aaraar.utils.withClasspath 10 | import kotlin.test.Test 11 | 12 | class ClasspathTest { 13 | @Test 14 | fun `ignores new enums`() { 15 | val jar: GenericJarArchive 16 | 17 | withClasspath { cp -> 18 | cp.addClass("com.example.MyClass") 19 | cp.addClass("com.example.MyEnum") { 20 | modifiers += Modifier.ENUM 21 | superclass = cp["java.lang.Enum"] 22 | } 23 | jar = cp.toGenericJarArchive() 24 | } 25 | 26 | jar.shouldContainExactly("com/example/MyClass.class") 27 | } 28 | 29 | @Test 30 | fun `ignores changes to existing enums`() { 31 | val oldJar = annotationsJarPath.loadJar() 32 | val newJar: GenericJarArchive 33 | 34 | withClasspath(oldJar) { cp -> 35 | val capitalizationClass = cp["org.jetbrains.annotations.Nls${'$'}Capitalization"] 36 | capitalizationClass.superclass?.qualifiedName shouldBe "java.lang.Enum" 37 | capitalizationClass.interfaces.shouldBeEmpty() 38 | 39 | capitalizationClass.interfaces += cp["java.io.Serializable"] 40 | newJar = cp.toGenericJarArchive() 41 | } 42 | 43 | withClasspath(newJar) { cp -> 44 | val capitalizationClass = cp["org.jetbrains.annotations.Nls${'$'}Capitalization"] 45 | 46 | capitalizationClass.superclass?.qualifiedName shouldBe "java.lang.Enum" 47 | capitalizationClass.interfaces.shouldBeEmpty() 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | repo_name: aaraar 2 | repo_url: https://github.com/christiandeange/aaraar 3 | site_name: aaraar 4 | site_url: https://christiandeange.github.io/aaraar/ 5 | site_author: Christian De Angelis 6 | site_description: 'A Gradle Plugin for creating a merged aar file' 7 | copyright: 'Copyright 2025 Christian De Angelis' 8 | remote_branch: gh-pages 9 | 10 | theme: 11 | name: 'material' 12 | logo: assets/logo.png 13 | custom_dir: docs/overrides 14 | icon: 15 | repo: fontawesome/brands/github 16 | features: 17 | - content.code.copy 18 | - content.tabs.link 19 | - navigation.footer 20 | - navigation.instant 21 | - toc.integrate 22 | palette: 23 | - scheme: default 24 | media: "(prefers-color-scheme: light)" 25 | primary: blue grey 26 | toggle: 27 | icon: material/brightness-5 28 | name: Switch to dark mode 29 | 30 | - scheme: slate 31 | media: "(prefers-color-scheme: dark)" 32 | primary: blue grey 33 | toggle: 34 | icon: material/brightness-3 35 | name: Switch to light mode 36 | 37 | markdown_extensions: 38 | - toc: 39 | permalink: true 40 | - pymdownx.caret 41 | - pymdownx.details 42 | - pymdownx.highlight: 43 | use_pygments: true 44 | anchor_linenums: true 45 | - pymdownx.inlinehilite 46 | - pymdownx.superfences 47 | - pymdownx.tabbed: 48 | alternate_style: true 49 | - admonition 50 | 51 | plugins: 52 | - search 53 | 54 | nav: 55 | - 'Overview': index.md 56 | - 'Usage': 57 | - 'Installation': installation.md 58 | - 'Packaging': packaging.md 59 | - 'Shading': shading.md 60 | - 'Publishing an AAR': publishing-aar.md 61 | - 'Publishing a JAR': publishing-jar.md 62 | - 'API Reference': kdoc/index.html 63 | - 'Changelog': changelog.md 64 | - 'License': license.md 65 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/Classpath.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | import sh.christian.aaraar.model.GenericJarArchive 4 | 5 | /** 6 | * Represents a set of classes that are available at runtime. 7 | * 8 | * The full set of runtime classes is likely more than what is represented here, which is why calling [get] for any 9 | * unknown classes will return a virtual class definition that can still be referenced as usual. 10 | * However, these virtual classes will be ignored when exporting the classpath via [toGenericJarArchive], and won't 11 | * include all the information (like supertypes, declared functions, etc) that the real class would. 12 | */ 13 | interface Classpath { 14 | /** The set of all input classes that will be packaged in this JAR. */ 15 | val classes: Set 16 | 17 | /** Returns the class definition for the given class, or throws if one does not exist. */ 18 | operator fun get(clazz: Class<*>): ClassReference 19 | 20 | /** 21 | * Returns the class definition for the given class name, or returns a virtual definition if one does not exist. 22 | * Virtual definitions won't include all the information (like supertypes, declared functions, etc) that the real 23 | * class would, they are simply a placeholder for referencing a type from another compilation unit. 24 | */ 25 | operator fun get(className: String): ClassReference 26 | 27 | /** Returns the class definition for the given class name, or `null` if none exists. */ 28 | fun getOrNull(className: String): ClassReference? 29 | 30 | /** Returns the classpath as a JAR representation. */ 31 | fun toGenericJarArchive(): GenericJarArchive 32 | 33 | companion object { 34 | fun from(jarArchive: GenericJarArchive): Classpath { 35 | return MutableClasspath.from(jarArchive) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/pipeline/KotlinModuleFilter.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.pipeline 2 | 3 | import kotlinx.metadata.jvm.KotlinModuleMetadata 4 | import kotlinx.metadata.jvm.UnstableMetadataApi 5 | import sh.christian.aaraar.shading.impl.transform.ClassDelete 6 | import sh.christian.aaraar.shading.impl.transform.JarProcessor 7 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.DISCARD 8 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.KEEP 9 | import sh.christian.aaraar.shading.impl.transform.Transformable 10 | 11 | @OptIn(UnstableMetadataApi::class) 12 | internal class KotlinModuleFilter( 13 | classDeletes: Set, 14 | ) : JarProcessor { 15 | private val classDeletePatterns = classDeletes.map { ClassDelete(it) } 16 | 17 | override fun process(struct: Transformable): JarProcessor.Result { 18 | if (!struct.name.endsWith(".kotlin_module") || classDeletePatterns.isEmpty()) return KEEP 19 | 20 | val metadata = KotlinModuleMetadata.read(struct.data) 21 | val kotlinModule = metadata.kmModule 22 | 23 | val packageParts = kotlinModule.packageParts.toMap() 24 | kotlinModule.packageParts.clear() 25 | kotlinModule.packageParts.putAll( 26 | packageParts.mapNotNull { (packageName, packageParts) -> 27 | val fileFacades = packageParts.fileFacades.toList() 28 | packageParts.fileFacades.clear() 29 | packageParts.fileFacades.addAll( 30 | fileFacades.filter { clazz -> classDeletePatterns.none { it.matches(clazz) } } 31 | ) 32 | 33 | (packageName to packageParts).takeIf { packageParts.fileFacades.isNotEmpty() } 34 | } 35 | ) 36 | 37 | return if (kotlinModule.packageParts.isNotEmpty()) { 38 | struct.data = metadata.write() 39 | KEEP 40 | } else { 41 | DISCARD 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags-ignore: 8 | - '**' 9 | pull_request: 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | matrix: 15 | os: [ ubuntu-latest, windows-latest ] 16 | runs-on: ${{ matrix.os }} 17 | timeout-minutes: 30 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: gradle/actions/wrapper-validation@v3 21 | - uses: actions/setup-java@v4 22 | with: 23 | distribution: 'temurin' 24 | java-version: '17' 25 | check-latest: true 26 | 27 | - name: Assemble & Test 28 | run: ./gradlew clean assemble test detekt --no-build-cache --no-daemon --stacktrace 29 | 30 | - name: Upload Test Results 31 | uses: actions/upload-artifact@v4 32 | if: ${{ failure() }} 33 | with: 34 | name: test-results 35 | path: ./**/build/reports/tests/ 36 | 37 | publish-snapshot: 38 | runs-on: ubuntu-latest 39 | if: github.repository == 'christiandeange/aaraar' && github.ref == 'refs/heads/main' 40 | timeout-minutes: 30 41 | needs: 42 | - test 43 | steps: 44 | - uses: actions/checkout@v4 45 | - uses: actions/setup-java@v4 46 | with: 47 | distribution: 'temurin' 48 | java-version: '17' 49 | check-latest: true 50 | 51 | - name: Publish Snapshot 52 | env: 53 | SONATYPE_CENTRAL_USERNAME: ${{ secrets.SONATYPE_CENTRAL_USERNAME }} 54 | SONATYPE_CENTRAL_PASSWORD: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }} 55 | run: | 56 | ORG_GRADLE_PROJECT_mavenCentralUsername="$SONATYPE_CENTRAL_USERNAME" \ 57 | ORG_GRADLE_PROJECT_mavenCentralPassword="$SONATYPE_CENTRAL_PASSWORD" \ 58 | ./gradlew clean publish --no-build-cache --no-daemon --stacktrace 59 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | The plugin only needs to be applied to modules you intend to publish as artifacts. 2 | 3 | === "Kotlin" 4 | 5 | ```kotlin 6 | // build.gradle.kts 7 | 8 | plugins { 9 | id("sh.christian.aaraar") version "0.1.3" 10 | } 11 | ``` 12 | 13 | === "Groovy" 14 | 15 | ```groovy 16 | // build.gradle 17 | 18 | plugins { 19 | id("sh.christian.aaraar") version "0.1.3" 20 | } 21 | ``` 22 | 23 | ### Android 24 | 25 | For Android modules, aaraar is configured to run automatically as part of the assemble pipeline for all variants, unless 26 | configured otherwise via the provided `aaraar` extension. It is recommended that you only enable aaraar for variant(s) 27 | you intend to publish. 28 | 29 | ```kotlin 30 | aaraar { 31 | isEnabledForVariant { variant -> 32 | variant.name == "release" 33 | } 34 | } 35 | ``` 36 | 37 | ### JVM 38 | 39 | By default, the `packageJar` task will overwrite the output of the `jar` task with the merged jar file, but this can 40 | be customized to suit your needs by changing the `PackageJar.outputJar` task output file property. 41 | 42 | === "Kotlin" 43 | 44 | ```kotlin 45 | tasks.named("packageJar") { 46 | isEnabled = providers.gradleProperty("enablePublishing").map { it.toBoolean() }.getOrElse(false) 47 | 48 | outputJar.set(project.layout.buildDirectory.file("artifact-all.jar")) 49 | } 50 | 51 | // Run via ./gradlew -PenablePublishing=true [task_name] 52 | ``` 53 | 54 | === "Groovy" 55 | 56 | ```groovy 57 | tasks.named("packageJar", PackageJarTask) { 58 | setEnabled(providers.gradleProperty("enablePublishing").map { it.toBoolean() }.getOrElse(false)) 59 | 60 | outputJar = project.layout.buildDirectory.file("artifact-all.jar") 61 | } 62 | 63 | // Run via ./gradlew -PenablePublishing=true [task_name] 64 | ``` 65 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/merger/impl/GenericJarArchiveMergerTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import io.kotest.assertions.throwables.shouldThrow 4 | import sh.christian.aaraar.merger.MergeRules 5 | import sh.christian.aaraar.utils.animalJarPath 6 | import sh.christian.aaraar.utils.foo2JarPath 7 | import sh.christian.aaraar.utils.fooJarPath 8 | import sh.christian.aaraar.utils.loadJar 9 | import sh.christian.aaraar.utils.shouldContainExactly 10 | import kotlin.test.Test 11 | 12 | class GenericJarArchiveMergerTest { 13 | 14 | private val merger = GenericJarArchiveMerger(MergeRules.None) 15 | 16 | @Test 17 | fun `nothing to merge`() { 18 | val animalClasses = animalJarPath.loadJar() 19 | 20 | merger.merge(animalClasses, emptyList()).shouldContainExactly( 21 | "com/example/Animal.class", 22 | "com/example/Cat.class", 23 | "com/example/Dog.class", 24 | ) 25 | } 26 | 27 | @Test 28 | fun `simple merge with classes`() { 29 | val animalClasses = animalJarPath.loadJar() 30 | val fooClasses = fooJarPath.loadJar() 31 | 32 | merger.merge(animalClasses, fooClasses).shouldContainExactly( 33 | "com/example/Animal.class", 34 | "com/example/Cat.class", 35 | "com/example/Dog.class", 36 | "com/example/Foo.class", 37 | ) 38 | } 39 | 40 | @Test 41 | fun `merge with self is redundant`() { 42 | val fooClasses1 = fooJarPath.loadJar() 43 | val fooClasses2 = fooJarPath.loadJar() 44 | 45 | merger.merge(fooClasses1, fooClasses2).shouldContainExactly( 46 | "com/example/Foo.class", 47 | ) 48 | } 49 | 50 | @Test 51 | fun `merge with classes with conflicting class files fails`() { 52 | val fooClasses = fooJarPath.loadJar() 53 | val foo2Classes = foo2JarPath.loadJar() 54 | 55 | shouldThrow { 56 | merger.merge(fooClasses, foo2Classes) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/AndroidManifestTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.matchers.shouldBe 4 | import sh.christian.aaraar.utils.normalizeWhitespace 5 | import kotlin.test.Test 6 | 7 | class AndroidManifestTest { 8 | 9 | @Test 10 | fun `parses package name from manifest`() { 11 | val manifest = AndroidManifest("""""") 12 | manifest.packageName shouldBe "com.library.main" 13 | } 14 | 15 | @Test 16 | fun `parses minSdkVersion from manifest`() { 17 | val manifest = AndroidManifest( 18 | """ 19 | 20 | 23 | 24 | """ 25 | ) 26 | 27 | manifest.minSdk shouldBe 21 28 | } 29 | 30 | @Test 31 | fun `test toString`() { 32 | val manifestString = 33 | """ 34 | 35 | 36 | 37 | """.trimIndent() 38 | 39 | val manifest = AndroidManifest(manifestString) 40 | manifest.toString().normalizeWhitespace() shouldBe manifestString.normalizeWhitespace() 41 | } 42 | 43 | @Test 44 | fun `test equality`() { 45 | val manifestString = 46 | """ 47 | 48 | 49 | 50 | """.trimIndent() 51 | 52 | val manifest1 = AndroidManifest(manifestString) 53 | val manifest2 = AndroidManifest(manifestString) 54 | manifest1 shouldBe manifest2 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/NavigationJson.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import com.android.manifmerger.NavigationXmlDocumentData 4 | import com.google.gson.GsonBuilder 5 | import com.google.gson.reflect.TypeToken 6 | import java.nio.file.Files 7 | import java.nio.file.Path 8 | 9 | /** 10 | * Represents the contents of the `navigation.json` file. 11 | */ 12 | class NavigationJson 13 | internal constructor( 14 | internal val navigationData: List, 15 | ) { 16 | constructor(jsonSource: String) : this(parseJson(jsonSource)) 17 | 18 | override fun toString(): String { 19 | return GSON.toJson(navigationData) 20 | } 21 | 22 | override fun equals(other: Any?): Boolean { 23 | if (other !is NavigationJson) return false 24 | return navigationData == other.navigationData 25 | } 26 | 27 | override fun hashCode(): Int { 28 | return navigationData.hashCode() 29 | } 30 | 31 | fun writeTo(path: Path) { 32 | if (navigationData.isEmpty()) { 33 | Files.deleteIfExists(path) 34 | } else { 35 | Files.writeString(path, toString()) 36 | } 37 | } 38 | 39 | companion object { 40 | private val GSON = GsonBuilder().setPrettyPrinting().create() 41 | 42 | fun from(path: Path): NavigationJson { 43 | if (!Files.isRegularFile(path)) return NavigationJson(emptyList()) 44 | 45 | val typeToken = object : TypeToken>() {}.type 46 | val navigationData = GSON.fromJson(Files.newBufferedReader(path), typeToken) as List 47 | 48 | return NavigationJson(navigationData) 49 | } 50 | 51 | private fun parseJson(json: String): List { 52 | val typeToken = object : TypeToken>() {}.type 53 | return GSON.fromJson(json, typeToken) as List 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/utils/files.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.utils 2 | 3 | import java.io.File 4 | import java.net.URI 5 | import java.nio.file.FileSystem 6 | import java.nio.file.FileSystems 7 | import java.nio.file.Files 8 | import java.nio.file.Path 9 | 10 | internal operator fun Path.div(path: String): Path { 11 | return resolve(path) 12 | } 13 | 14 | internal operator fun FileSystem.div(path: String): Path { 15 | return getPath(path) 16 | } 17 | 18 | internal fun Path.mkdirs(): Path { 19 | return apply { parent?.let(Files::createDirectories) } 20 | } 21 | 22 | internal fun Path.deleteIfExists(): Path { 23 | return apply { Files.deleteIfExists(this) } 24 | } 25 | 26 | /** 27 | * Creates a new archive file at the specified path, returning a [FileSystem] that represents the internal structure 28 | * of the archive to which files can be read, written, and deleted. 29 | */ 30 | fun Path.createArchive(block: (FileSystem) -> T): T = asArchiveFileSystem(env = mapOf("create" to true), block) 31 | 32 | /** 33 | * Opens an existing archive file at the specified path, returning a [FileSystem] that represents the internal structure 34 | * of the archive to which files can be read, written, and deleted. 35 | */ 36 | fun Path.openArchive(block: (FileSystem) -> T): T = asArchiveFileSystem(env = emptyMap(), block) 37 | 38 | private fun Path.asArchiveFileSystem( 39 | env: Map = emptyMap(), 40 | block: (FileSystem) -> T, 41 | ): T { 42 | val fileSystemUri = if (File.separatorChar == '\\') { 43 | URI.create("jar:file:/${toAbsolutePath().toString().replace("\\", "/")}") 44 | } else { 45 | URI.create("jar:file:${toAbsolutePath()}") 46 | } 47 | 48 | return runCatching { 49 | FileSystems.newFileSystem(fileSystemUri, env) 50 | }.getOrElse { e -> 51 | throw IllegalStateException("Cannot create filesystem for ${toAbsolutePath()}", e) 52 | }.use(block) 53 | } 54 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/pipeline/ServiceLoaderFilter.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.pipeline 2 | 3 | import sh.christian.aaraar.shading.impl.transform.ClassDelete 4 | import sh.christian.aaraar.shading.impl.transform.JarProcessor 5 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.DISCARD 6 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.KEEP 7 | import sh.christian.aaraar.shading.impl.transform.Transformable 8 | 9 | internal class ServiceLoaderFilter( 10 | classDeletes: Set, 11 | ) : JarProcessor { 12 | private val classDeletePatterns = classDeletes.map { ClassDelete(it) } 13 | 14 | override fun process(struct: Transformable): JarProcessor.Result { 15 | if (classDeletePatterns.isEmpty() || !struct.name.startsWith("META-INF/services/")) return KEEP 16 | val originalFile = struct.data.decodeToString() 17 | 18 | val newContents = buildString { 19 | val line = StringBuilder() 20 | 21 | originalFile.forEach { c -> 22 | if (c == '\n' || c == '\r') { 23 | val className = line.toString() 24 | if (!shouldDeleteClass(className)) { 25 | append(className) 26 | append(c) 27 | } 28 | line.clear() 29 | } else { 30 | line.append(c) 31 | } 32 | } 33 | 34 | val className = line.toString() 35 | if (!shouldDeleteClass(className)) { 36 | append(className) 37 | } 38 | } 39 | 40 | return if (newContents.isBlank()) { 41 | DISCARD 42 | } else { 43 | struct.data = newContents.encodeToByteArray() 44 | KEEP 45 | } 46 | } 47 | 48 | private fun shouldDeleteClass(className: String): Boolean { 49 | return shouldDeletePath(className.replace('.', '/')) 50 | } 51 | 52 | private fun shouldDeletePath(className: String): Boolean { 53 | return classDeletePatterns.any { it.matches(className) } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/pipeline/KotlinModuleShader.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.pipeline 2 | 3 | import kotlinx.metadata.jvm.KotlinModuleMetadata 4 | import kotlinx.metadata.jvm.UnstableMetadataApi 5 | import sh.christian.aaraar.shading.impl.transform.ClassRename 6 | import sh.christian.aaraar.shading.impl.transform.JarProcessor 7 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.KEEP 8 | import sh.christian.aaraar.shading.impl.transform.PackageRemapper 9 | import sh.christian.aaraar.shading.impl.transform.Transformable 10 | 11 | @OptIn(UnstableMetadataApi::class) 12 | internal class KotlinModuleShader( 13 | classRenames: Map, 14 | ) : JarProcessor { 15 | private val packageRemapper = PackageRemapper( 16 | classRenames.map { (pattern, result) -> ClassRename(pattern, result) } 17 | ) 18 | 19 | override fun process(struct: Transformable): JarProcessor.Result { 20 | if (!struct.name.endsWith(".kotlin_module")) return KEEP 21 | 22 | val metadata = KotlinModuleMetadata.read(struct.data) 23 | val kotlinModule = metadata.kmModule 24 | 25 | val packageParts = kotlinModule.packageParts.toMap() 26 | kotlinModule.packageParts.clear() 27 | kotlinModule.packageParts.putAll( 28 | packageParts.map { (packageName, packageParts) -> 29 | val newPackageName = packageRemapper.mapPackage(packageName) 30 | 31 | val fileFacades = packageParts.fileFacades.toList() 32 | packageParts.fileFacades.clear() 33 | packageParts.fileFacades.addAll(fileFacades.map { packageRemapper.mapType(it) }) 34 | 35 | newPackageName to packageParts 36 | } 37 | ) 38 | struct.data = metadata.write() 39 | 40 | return KEEP 41 | } 42 | 43 | private fun PackageRemapper.mapPackage(packageName: String): String { 44 | val classSuffix = ".Dummy" 45 | return mapValue(packageName + classSuffix).toString().removeSuffix(classSuffix) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/NavigationJsonTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.matchers.shouldBe 4 | import sh.christian.aaraar.utils.navigationJsonDataString 5 | import java.io.File 6 | import kotlin.test.Test 7 | 8 | class NavigationJsonTest { 9 | @Test 10 | fun `test toString`() { 11 | val json = navigationJsonDataString("nav1", "/lib1") 12 | val navigationJson = NavigationJson(json) 13 | 14 | val filePath = if (File.separatorChar == '\\') { 15 | """D:\\nav1.xml""" 16 | } else { 17 | "/nav1.xml" 18 | } 19 | 20 | navigationJson.toString() shouldBe """ 21 | [ 22 | { 23 | "name": "nav1", 24 | "navigationXmlIds": [], 25 | "deepLinks": [ 26 | { 27 | "schemes": [ 28 | "http", 29 | "https" 30 | ], 31 | "host": "www.example.com", 32 | "port": -1, 33 | "path": "/lib1", 34 | "sourceFilePosition": { 35 | "mSourceFile": { 36 | "mFilePath": "$filePath", 37 | "mDescription": "nav1" 38 | }, 39 | "mSourcePosition": { 40 | "mStartLine": 7, 41 | "mStartColumn": 4, 42 | "mStartOffset": 309, 43 | "mEndLine": 9, 44 | "mEndColumn": 37, 45 | "mEndOffset": 440 46 | } 47 | }, 48 | "isAutoVerify": false, 49 | "action": "android.intent.action.VIEW" 50 | } 51 | ] 52 | } 53 | ] 54 | """.trimIndent() 55 | } 56 | 57 | @Test 58 | fun `test equality`() { 59 | val json = navigationJsonDataString("nav1", "/lib1") 60 | 61 | val navigationJson1 = NavigationJson(json) 62 | val navigationJson2 = NavigationJson(json) 63 | navigationJson1 shouldBe navigationJson2 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/shading/GenericJarArchiveServiceLoaderShaderTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading 2 | 3 | import sh.christian.aaraar.utils.forEntry 4 | import sh.christian.aaraar.utils.loadJar 5 | import sh.christian.aaraar.utils.serviceJarPath 6 | import kotlin.test.Test 7 | 8 | class GenericJarArchiveServiceLoaderShaderTest { 9 | @Test 10 | fun `default service loader file`() { 11 | val originalClasses = serviceJarPath.loadJar() 12 | originalClasses.forEntry("META-INF/services/java.nio.file.spi.CustomService") shouldHaveFileContents """ 13 | com.example.MyCustomService 14 | com.example.RealCustomService 15 | """ 16 | } 17 | 18 | @Test 19 | fun `shading updates class references from service loader file`() { 20 | val shadedClasses = serviceJarPath.loadJar().shaded( 21 | classRenames = mapOf("com.example.MyCustomService" to "com.example.EmptyCustomService"), 22 | ) 23 | shadedClasses.forEntry("META-INF/services/java.nio.file.spi.CustomService") shouldHaveFileContents """ 24 | com.example.EmptyCustomService 25 | com.example.RealCustomService 26 | """ 27 | } 28 | 29 | @Test 30 | fun `deleting some class references from service loader file removes them`() { 31 | val shadedClasses = serviceJarPath.loadJar().shaded( 32 | classDeletes = setOf("com.example.MyCustomService"), 33 | ) 34 | shadedClasses.forEntry("META-INF/services/java.nio.file.spi.CustomService") shouldHaveFileContents """ 35 | com.example.RealCustomService 36 | """ 37 | } 38 | 39 | @Test 40 | fun `deleting all class references from service loader file removes the service loader file`() { 41 | val shadedClasses = serviceJarPath.loadJar().shaded( 42 | classDeletes = setOf( 43 | "com.example.MyCustomService", 44 | "com.example.RealCustomService", 45 | ), 46 | ) 47 | shadedClasses.forEntry("META-INF/services/java.nio.file.spi.CustomService").shouldNotExist() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/RTxt.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import com.android.ide.common.symbols.SymbolIo 4 | import com.android.ide.common.symbols.SymbolTable 5 | import java.io.StringWriter 6 | import java.nio.file.Files 7 | import java.nio.file.Path 8 | 9 | /** 10 | * Represents the contents of the `R.txt` file. 11 | */ 12 | class RTxt 13 | internal constructor( 14 | val symbolTable: SymbolTable, 15 | ) { 16 | constructor( 17 | lines: List, 18 | packageName: String, 19 | ) : this(lines.joinToString(separator = "\n"), packageName) 20 | 21 | constructor( 22 | lines: String, 23 | packageName: String, 24 | ) : this(SymbolIo.readFromAaptNoValues(lines.byteInputStream().bufferedReader(), "R.txt", packageName)) 25 | 26 | override fun toString(): String { 27 | val writer = StringWriter() 28 | writer.use { SymbolIo.writeForAar(symbolTable, it) } 29 | return writer.toString() 30 | } 31 | 32 | override fun equals(other: Any?): Boolean { 33 | if (other !is RTxt) return false 34 | return symbolTable.symbols == other.symbolTable.symbols && 35 | symbolTable.tablePackage == other.symbolTable.tablePackage 36 | } 37 | 38 | override fun hashCode(): Int { 39 | var result = 1 40 | result = 31 * result + symbolTable.symbols.hashCode() 41 | result = 31 * result + symbolTable.tablePackage.hashCode() 42 | return result 43 | } 44 | 45 | fun writeTo(path: Path) { 46 | if (symbolTable.symbols.isEmpty) { 47 | Files.deleteIfExists(path) 48 | } else { 49 | SymbolIo.writeForAar(symbolTable, path) 50 | } 51 | } 52 | 53 | companion object { 54 | fun from(path: Path, packageName: String): RTxt { 55 | if (!Files.isRegularFile(path)) return RTxt(symbolTable = SymbolTable.builder().build()) 56 | 57 | val symbolTable = SymbolIo.readFromAaptNoValues(Files.newBufferedReader(path), path.toString(), packageName) 58 | return RTxt(symbolTable) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/sh/christian/aaraar/packaging/PackagingEnvironment.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.packaging 2 | 3 | import java.io.Serializable 4 | 5 | /** 6 | * Defines the environment of all applicable packaging rules. 7 | */ 8 | data class PackagingEnvironment( 9 | /** 10 | * Packaging rules for merging JNI library folders. 11 | */ 12 | val jniLibs: JniLibs, 13 | /** 14 | * Packaging rules for merging resource files. 15 | */ 16 | val resources: Resources, 17 | ) : Serializable { 18 | 19 | data class JniLibs( 20 | /** 21 | * The excluded pattern(s). 22 | */ 23 | val excludes: Set, 24 | /** 25 | * The pattern(s) for which the first occurrence is packaged. Ordering is determined by the order of dependencies. 26 | */ 27 | val pickFirsts: Set, 28 | ) : Serializable { 29 | companion object { 30 | private const val serialVersionUID = 1L 31 | } 32 | } 33 | 34 | data class Resources( 35 | /** 36 | * The excluded pattern(s). 37 | */ 38 | val excludes: Set, 39 | /** 40 | * The pattern(s) for which the first occurrence is packaged. Ordering is determined by the order of dependencies. 41 | */ 42 | val pickFirsts: Set, 43 | /** 44 | * The pattern(s) for which matching resources are merged into a single entry. 45 | */ 46 | val merges: Set, 47 | ) : Serializable { 48 | companion object { 49 | private const val serialVersionUID = 1L 50 | } 51 | } 52 | 53 | companion object { 54 | private const val serialVersionUID = 1L 55 | 56 | /** 57 | * Sentinel value for no custom packaging rules. 58 | */ 59 | val None = PackagingEnvironment( 60 | jniLibs = JniLibs( 61 | excludes = emptySet(), 62 | pickFirsts = emptySet(), 63 | ), 64 | resources = Resources( 65 | excludes = emptySet(), 66 | pickFirsts = emptySet(), 67 | merges = emptySet(), 68 | ), 69 | ) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/PathRemapper.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | import org.objectweb.asm.commons.Remapper 4 | 5 | internal class PathRemapper( 6 | private val patterns: List, 7 | ) : Remapper() where T : AbstractPattern, T : ReplacePattern { 8 | private val typeCache: MutableMap = mutableMapOf() 9 | private val pathCache: MutableMap = mutableMapOf() 10 | private val valueCache: MutableMap = mutableMapOf() 11 | 12 | constructor(vararg patterns: T) : this(patterns.toList()) 13 | 14 | override fun map(key: String): String { 15 | return typeCache.getOrPut(key) { 16 | val mapped = replaceHelper(key) 17 | if (key == mapped) return mapped 18 | mapped 19 | } 20 | } 21 | 22 | override fun mapValue(value: Any?): Any? { 23 | return if (value is String) { 24 | valueCache.getOrPut(value) { 25 | value.let(::mapPath).let(::replaceHelper) 26 | } 27 | } else { 28 | super.mapValue(value) 29 | } 30 | } 31 | 32 | fun mapPath(path: String): String { 33 | return pathCache.getOrPut(path) { 34 | var (s, end) = if ('/' !in path) { 35 | RESOURCE_SUFFIX to path 36 | } else { 37 | path.substringBeforeLast("/") + "/$RESOURCE_SUFFIX" to path.substringAfterLast("/") 38 | } 39 | 40 | s = if (s.startsWith("/")) { 41 | // Map the path without the leading slash that makes it absolute 42 | s.substring(1).let(::replaceHelper).let { "/$it" } 43 | } else { 44 | replaceHelper(s) 45 | } 46 | 47 | if (RESOURCE_SUFFIX !in s) { 48 | path 49 | } else { 50 | s.removeSuffix(RESOURCE_SUFFIX) + end 51 | } 52 | } 53 | } 54 | 55 | private fun replaceHelper(value: String): String { 56 | return patterns.firstNotNullOfOrNull { it.replace(value) } ?: value 57 | } 58 | 59 | private companion object { 60 | const val RESOURCE_SUFFIX = "RESOURCE" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/pipeline/ResourceFilter.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.pipeline 2 | 3 | import sh.christian.aaraar.shading.impl.transform.JarProcessor 4 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Companion.EXT_CLASS 5 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.DISCARD 6 | import sh.christian.aaraar.shading.impl.transform.JarProcessor.Result.KEEP 7 | import sh.christian.aaraar.shading.impl.transform.Transformable 8 | import sh.christian.aaraar.utils.div 9 | import java.nio.file.FileSystems 10 | 11 | internal class ResourceFilter( 12 | private val resourceDeletes: Set, 13 | ) : JarProcessor { 14 | private val fs = FileSystems.getDefault() 15 | 16 | override fun process(struct: Transformable): JarProcessor.Result { 17 | val matchingRules = resourceDeletes.filter { fs.getPathMatcher("glob:$it").matches(fs / struct.name) } 18 | 19 | return when { 20 | // If there are no matching rules, keep the file. 21 | resourceDeletes.isEmpty() -> KEEP 22 | 23 | // If there are no rules at all, keep the file. 24 | matchingRules.isEmpty() -> KEEP 25 | 26 | // If the file is a class, only allow removing it if it matches a rule that explicitly ends with ".class". 27 | // This prevents accidental deletion of classes that match an overly aggressive glob pattern, especially one that 28 | // may have been configured by default from AGP. 29 | // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/internal/packaging/PackagingOptionsUtils.kt;l=1?q=PackagingOptionsUtils.kt%20%20&sq= 30 | struct.name.endsWith(EXT_CLASS) -> { 31 | if (matchingRules.any { it.endsWith(EXT_CLASS) }) { 32 | DISCARD 33 | } else { 34 | KEEP 35 | } 36 | } 37 | 38 | // Otherwise, we have at least one matching rule that applies to a resource file, so we discard it. 39 | else -> DISCARD 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/merger/impl/NavigationJsonMergerTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.merger.impl 2 | 3 | import io.kotest.matchers.collections.shouldContainExactly 4 | import sh.christian.aaraar.model.NavigationJson 5 | import sh.christian.aaraar.utils.navigationJsonData 6 | import sh.christian.aaraar.utils.navigationJsonDataString 7 | import sh.christian.aaraar.utils.withFile 8 | import sh.christian.aaraar.utils.withFileSystem 9 | import java.nio.file.Files 10 | import kotlin.test.Test 11 | 12 | class NavigationJsonMergerTest { 13 | 14 | private val merger = NavigationJsonMerger() 15 | 16 | @Test 17 | fun `parses navigation data json`() = withFile { 18 | Files.writeString(filePath, navigationJsonDataString("nav1", "/lib1")) 19 | 20 | val navigationJson = NavigationJson.from(filePath) 21 | navigationJson.navigationData shouldContainExactly listOf(navigationJsonData("nav1", "/lib1")) 22 | } 23 | 24 | @Test 25 | fun `simple merge with two navigation graphs`() = withFileSystem { 26 | val json1 = NavigationJson.from(withFile { Files.writeString(filePath, navigationJsonDataString("nav1", "/lib1")) }) 27 | val json2 = NavigationJson.from(withFile { Files.writeString(filePath, navigationJsonDataString("nav2", "/lib2")) }) 28 | 29 | val merged = merger.merge(json1, json2) 30 | 31 | merged.navigationData shouldContainExactly listOf( 32 | navigationJsonData("nav1", "/lib1"), 33 | navigationJsonData("nav2", "/lib2") 34 | ) 35 | } 36 | 37 | @Test 38 | fun `merge with two of same name keeps both`() = withFileSystem { 39 | val json1 = NavigationJson.from(withFile { Files.writeString(filePath, navigationJsonDataString("nav", "/lib1")) }) 40 | val json2 = NavigationJson.from(withFile { Files.writeString(filePath, navigationJsonDataString("nav", "/lib2")) }) 41 | 42 | val merged = merger.merge(json1, json2) 43 | 44 | merged.navigationData shouldContainExactly listOf( 45 | navigationJsonData("nav", "/lib1"), 46 | navigationJsonData("nav", "/lib2"), 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/GenericJarArchiveTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import io.kotest.assertions.withClue 4 | import io.kotest.matchers.collections.shouldContainExactly 5 | import io.kotest.matchers.shouldBe 6 | import io.kotest.matchers.shouldNotBe 7 | import sh.christian.aaraar.utils.animalJarPath 8 | import sh.christian.aaraar.utils.deleteIfExists 9 | import sh.christian.aaraar.utils.loadJar 10 | import sh.christian.aaraar.utils.serviceJarPath 11 | import sh.christian.aaraar.utils.withFile 12 | import kotlin.test.Test 13 | 14 | class GenericJarArchiveTest { 15 | 16 | @Test 17 | fun `can read from written jar`() { 18 | val originalJar = animalJarPath.loadJar() 19 | 20 | withFile { 21 | filePath.deleteIfExists() 22 | originalJar.writeTo(filePath) 23 | 24 | val rehydratedJar = filePath.loadJar() 25 | 26 | rehydratedJar.keys shouldContainExactly originalJar.keys 27 | rehydratedJar.keys.forEach { key -> 28 | withClue("Jar entry: $key") { 29 | rehydratedJar[key]!! shouldBe originalJar[key]!! 30 | } 31 | } 32 | } 33 | } 34 | 35 | @Test 36 | fun `empty jar has empty bytes`() { 37 | GenericJarArchive.NONE.bytes() shouldBe byteArrayOf() 38 | } 39 | 40 | @Test 41 | fun `test equality with meta files`() { 42 | val jar1 = GenericJarArchive.from(serviceJarPath, keepMetaFiles = true) 43 | val jar2 = GenericJarArchive.from(serviceJarPath, keepMetaFiles = true) 44 | jar1 shouldBe jar2 45 | } 46 | 47 | @Test 48 | fun `test equality without meta files`() { 49 | val jar1 = GenericJarArchive.from(serviceJarPath, keepMetaFiles = false) 50 | val jar2 = GenericJarArchive.from(serviceJarPath, keepMetaFiles = false) 51 | jar1 shouldBe jar2 52 | } 53 | 54 | @Test 55 | fun `test equality with differing meta files`() { 56 | val jar1 = GenericJarArchive.from(serviceJarPath, keepMetaFiles = true) 57 | val jar2 = GenericJarArchive.from(serviceJarPath, keepMetaFiles = false) 58 | jar1 shouldNotBe jar2 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/ClassReference.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | /** 4 | * Represents a class definition. 5 | */ 6 | interface ClassReference { 7 | /** The major version of bytecode that this class definition targets. */ 8 | val classMajorVersion: Int 9 | 10 | /** The minor version of bytecode that this class definition targets, or `0` if not set. */ 11 | val classMinorVersion: Int 12 | 13 | /** The set of modifiers applied to the class definition. */ 14 | val modifiers: Set 15 | 16 | /** This class's fully-qualified class name, including its package name and simple class name. */ 17 | val qualifiedName: String 18 | 19 | /** The declared name of this specific class. */ 20 | val simpleName: String 21 | 22 | /** The name of the package this class is defined in. */ 23 | val packageName: String 24 | 25 | /** The set of annotations applied to this class definition. */ 26 | val annotations: List 27 | 28 | /** The supertype of this class, or `null` if none defined. */ 29 | val superclass: ClassReference? 30 | 31 | /** 32 | * If this is a class, these are the set of interface types implemented by this class. 33 | * If this is an interface, these are the interfaces extended by this interface. 34 | */ 35 | val interfaces: List 36 | 37 | /** The set of constructors explicitly declared by this class. */ 38 | val constructors: List 39 | 40 | /** The set of fields explicitly declared by this class. */ 41 | val fields: List 42 | 43 | /** The set of methods explicitly declared by this class. */ 44 | val methods: List 45 | 46 | /** Returns the declared field identified by this name, or `null` if none exists. */ 47 | fun getField(name: String): FieldReference? 48 | 49 | /** Returns the declared method identified by this name, or `null` if none exists. */ 50 | fun getMethod(name: String): MethodReference? 51 | 52 | /** Returns the bytecode associated with this class definition. */ 53 | fun toBytecode(): ByteArray 54 | } 55 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp-latest = "8.5.0" 3 | agp-tools = "31.5.0" 4 | detekt = "1.23.6" 5 | dokka = "1.9.20" 6 | kotlin = "1.9.24" 7 | maven-publish = "0.34.0" 8 | 9 | [plugins] 10 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 11 | 12 | [libraries] 13 | agp-api7 = { module = "com.android.tools.build:gradle-api", version = "7.3.1" } 14 | agp-api8 = { module = "com.android.tools.build:gradle-api", version = "8.5.0" } 15 | agp-api-latest = { module = "com.android.tools.build:gradle-api", version.ref = "agp-latest" } 16 | agp-latest = { module = "com.android.tools.build:gradle", version.ref = "agp-latest" } 17 | agp-layoutlib = { module = "com.android.tools.layoutlib:layoutlib-api", version.ref = "agp-tools" } 18 | agp-tools-common = { module = "com.android.tools:common", version.ref = "agp-tools" } 19 | agp-tools-manifestmerger = { module = "com.android.tools.build:manifest-merger", version.ref = "agp-tools" } 20 | agp-tools-sdk = { module = "com.android.tools:sdk-common", version.ref = "agp-tools" } 21 | 22 | asm = { module = "org.ow2.asm:asm-commons", version = "9.4" } 23 | javassist = { module = "org.javassist:javassist", version = "3.30.2-GA" } 24 | decompiler = { module = "com.jetbrains.intellij.java:java-decompiler-engine", version = "233.14475.28" } 25 | kotlin-metadata = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version = "0.9.0" } 26 | 27 | jimfs = { module = "com.google.jimfs:jimfs", version = "1.2" } 28 | 29 | gson = { module = "com.google.code.gson:gson", version = "2.10.1" } 30 | kotlinxml = { module = "org.redundent:kotlin-xml-builder", version = "1.8.0" } 31 | 32 | dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } 33 | maven-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" } 34 | 35 | detekt-plugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } 36 | detekt-rules-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } 37 | 38 | kotest = { module = "io.kotest:kotest-assertions-core", version = "5.5.4" } 39 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/shading/GenericJarArchiveResourceShaderTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading 2 | 3 | import io.kotest.matchers.maps.shouldHaveKey 4 | import io.kotest.matchers.maps.shouldNotHaveKey 5 | import sh.christian.aaraar.utils.ktLibraryJarPath 6 | import sh.christian.aaraar.utils.loadJar 7 | import sh.christian.aaraar.utils.serviceJarPath 8 | import kotlin.test.Test 9 | 10 | class GenericJarArchiveResourceShaderTest { 11 | @Test 12 | fun `rename resource file by resource name`() { 13 | val originalClasses = serviceJarPath.loadJar() 14 | originalClasses shouldHaveKey "com/example/tracklist.txt" 15 | 16 | val shadedClasses = originalClasses.shaded(resourceRenames = mapOf("com/example/**" to "music/@0")) 17 | shadedClasses shouldHaveKey "music/com/example/tracklist.txt" 18 | } 19 | 20 | @Test 21 | fun `rename resource file by resource name does not affect class files`() { 22 | val originalClasses = serviceJarPath.loadJar() 23 | originalClasses shouldHaveKey "com/example/CustomService.class" 24 | 25 | val shadedClasses = originalClasses.shaded(resourceRenames = mapOf("com/example/**" to "music/@0")) 26 | shadedClasses shouldHaveKey "com/example/CustomService.class" 27 | shadedClasses shouldNotHaveKey "music/com/example/CustomService.class" 28 | } 29 | 30 | @Test 31 | fun `rename kotlin module by resource name`() { 32 | val originalClasses = ktLibraryJarPath.loadJar() 33 | originalClasses shouldHaveKey "META-INF/fixtures_ktLibrary.kotlin_module" 34 | 35 | val shadedClasses = originalClasses.shaded( 36 | resourceRenames = mapOf("META-INF/*.kotlin_module" to "META-INF/old/@1.kotlin_module"), 37 | ) 38 | shadedClasses shouldHaveKey "META-INF/old/fixtures_ktLibrary.kotlin_module" 39 | } 40 | 41 | @Test 42 | fun `delete by resource name`() { 43 | val originalClasses = serviceJarPath.loadJar() 44 | originalClasses shouldHaveKey "com/example/tracklist.txt" 45 | 46 | val shadedClasses = originalClasses.shaded(resourceDeletes = setOf("com/example/**")) 47 | shadedClasses shouldNotHaveKey "com/example/tracklist.txt" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /fixtures/src/testFixtures/kotlin/sh/christian/aaraar/utils/virtualFs.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.utils 2 | 3 | import com.google.common.jimfs.Configuration 4 | import com.google.common.jimfs.Jimfs 5 | import java.io.Closeable 6 | import java.nio.file.Files 7 | import java.nio.file.Path 8 | import kotlin.streams.asSequence 9 | 10 | class VirtualOutputContext : Closeable { 11 | private val fileSystem = Jimfs.newFileSystem(Configuration.unix()) 12 | 13 | val root: Path = fileSystem.rootDirectories.first() 14 | 15 | fun withFile(block: VirtualOutputFileContext.() -> Unit): Path { 16 | val tempFile = Files.createTempFile(root, "test-file", ".tmp") 17 | VirtualOutputFileContext(tempFile).apply(block) 18 | return tempFile 19 | } 20 | 21 | fun withDirectory(block: VirtualOutputDirectoryContext.() -> Unit): Path { 22 | val tempDirectory = Files.createTempDirectory(root, "test-dir") 23 | VirtualOutputDirectoryContext(tempDirectory).apply(block) 24 | return tempDirectory 25 | } 26 | 27 | override fun close() { 28 | fileSystem.close() 29 | } 30 | } 31 | 32 | class VirtualOutputFileContext(val filePath: Path) { 33 | fun bytes(): ByteArray { 34 | return Files.readAllBytes(filePath) 35 | } 36 | 37 | fun string(): String { 38 | return Files.readString(filePath) 39 | } 40 | } 41 | 42 | class VirtualOutputDirectoryContext(val root: Path) { 43 | fun filePaths(): List { 44 | return Files.walk(root) 45 | .asSequence() 46 | .filter(Files::isRegularFile) 47 | .map { it.toString() } 48 | .toList() 49 | } 50 | 51 | fun files(): Map { 52 | return Files.walk(root) 53 | .asSequence() 54 | .filter(Files::isRegularFile) 55 | .associate { root.relativize(it).toString() to Files.readString(it) } 56 | } 57 | } 58 | 59 | inline fun withFileSystem(block: VirtualOutputContext.() -> Unit) { 60 | VirtualOutputContext().use { 61 | it.block() 62 | } 63 | } 64 | 65 | fun withFile(block: VirtualOutputFileContext.() -> Unit) = withFileSystem { withFile(block) } 66 | 67 | fun withDirectory(block: VirtualOutputDirectoryContext.() -> Unit) = withFileSystem { withDirectory(block) } 68 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/PublicTxt.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import com.android.ide.common.symbols.SymbolIo 4 | import com.android.ide.common.symbols.SymbolTable 5 | import com.android.resources.ResourceType 6 | import java.nio.file.Files 7 | import java.nio.file.Path 8 | 9 | /** 10 | * Represents the contents of the `public.txt` file. 11 | */ 12 | class PublicTxt 13 | internal constructor( 14 | internal val symbolTable: SymbolTable, 15 | ) { 16 | constructor( 17 | lines: List, 18 | packageName: String, 19 | ) : this(lines.joinToString(separator = "\n"), packageName) 20 | 21 | constructor( 22 | lines: String, 23 | packageName: String, 24 | ) : this(SymbolIo.readFromPublicTxtFile(lines.byteInputStream(), "public.txt", packageName)) 25 | 26 | override fun toString(): String { 27 | return ResourceType.values() 28 | .flatMap { type -> 29 | symbolTable.getSymbolByResourceType(type) 30 | } 31 | .joinToString("\n") { symbol -> 32 | @Suppress("UsePropertyAccessSyntax") 33 | "${symbol.resourceType.getName()} ${symbol.canonicalName}" 34 | } 35 | } 36 | 37 | override fun equals(other: Any?): Boolean { 38 | if (other !is PublicTxt) return false 39 | return symbolTable.symbols == other.symbolTable.symbols && 40 | symbolTable.tablePackage == other.symbolTable.tablePackage 41 | } 42 | 43 | override fun hashCode(): Int { 44 | var result = 1 45 | result = 31 * result + symbolTable.symbols.hashCode() 46 | result = 31 * result + symbolTable.tablePackage.hashCode() 47 | return result 48 | } 49 | 50 | fun writeTo(path: Path) { 51 | if (symbolTable.symbols.isEmpty) { 52 | Files.deleteIfExists(path) 53 | } else { 54 | Files.writeString(path, toString()) 55 | } 56 | } 57 | 58 | companion object { 59 | fun from(path: Path, packageName: String): PublicTxt { 60 | if (!Files.isRegularFile(path)) return PublicTxt(symbolTable = SymbolTable.builder().build()) 61 | 62 | val symbolTable = SymbolIo.readFromPublicTxtFile(Files.newInputStream(path), path.toString(), packageName) 63 | return PublicTxt(symbolTable) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/metadata/util.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor.metadata 2 | 3 | import kotlinx.metadata.ClassName 4 | import kotlinx.metadata.KmConstructor 5 | import kotlinx.metadata.KmFunction 6 | import kotlinx.metadata.KmProperty 7 | import kotlinx.metadata.Visibility 8 | import kotlinx.metadata.jvm.fieldSignature 9 | import kotlinx.metadata.jvm.getterSignature 10 | import kotlinx.metadata.jvm.setterSignature 11 | import kotlinx.metadata.jvm.signature 12 | import sh.christian.aaraar.model.classeditor.ConstructorSignature 13 | import sh.christian.aaraar.model.classeditor.FieldSignature 14 | import sh.christian.aaraar.model.classeditor.MethodSignature 15 | import sh.christian.aaraar.model.classeditor.Modifier 16 | import sh.christian.aaraar.model.classeditor.Signature 17 | 18 | internal fun String.toClassName(): ClassName { 19 | return this.replace(".", "/") 20 | } 21 | 22 | internal fun ClassName.toQualifiedName(): String { 23 | return this.replace("/", ".") 24 | } 25 | 26 | internal fun KmConstructor.signature(): ConstructorSignature { 27 | return ConstructorSignature(signature!!.descriptor) 28 | } 29 | 30 | internal fun KmFunction.signature(): MethodSignature { 31 | return MethodSignature(name, signature!!.descriptor) 32 | } 33 | 34 | internal fun KmProperty.fieldSignature(): Signature? { 35 | return fieldSignature?.let { FieldSignature(it.name, it.descriptor) } 36 | } 37 | 38 | internal fun KmProperty.getterSignature(): Signature? { 39 | return getterSignature?.let { MethodSignature(it.name, it.descriptor) } 40 | } 41 | 42 | internal fun KmProperty.setterSignature(): Signature? { 43 | return setterSignature?.let { MethodSignature(it.name, it.descriptor) } 44 | } 45 | 46 | internal fun KmProperty.signatures(): List { 47 | return listOfNotNull(fieldSignature(), getterSignature(), setterSignature()) 48 | } 49 | 50 | internal fun Set.toVisibility(): Visibility { 51 | return when { 52 | Modifier.PUBLIC in this -> Visibility.PUBLIC 53 | Modifier.PROTECTED in this -> Visibility.PROTECTED 54 | Modifier.PRIVATE in this -> Visibility.PRIVATE 55 | // Assume internal if no visibility modifiers are present. 56 | else -> Visibility.INTERNAL 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/utils/AarEntries.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.utils 2 | 3 | import java.nio.file.FileSystem 4 | import java.nio.file.Path 5 | 6 | // https://developer.android.com/studio/projects/android-library.html#aar-contents 7 | 8 | /** 9 | * The full path to the `aar-metadata.properties` file within an AAR file. 10 | */ 11 | val FileSystem.aarMetadataProperties: Path 12 | get() = this / "META-INF" / "com" / "android" / "build" / "gradle" / "aar-metadata.properties" 13 | 14 | /** 15 | * The full path to the `AndroidManifest.xml` file within an AAR file. 16 | */ 17 | val FileSystem.androidManifestXml: Path get() = this / "AndroidManifest.xml" 18 | 19 | /** 20 | * The full path to the `classes.jar` file within an AAR file. 21 | */ 22 | val FileSystem.clasesJar: Path get() = this / "classes.jar" 23 | 24 | /** 25 | * The full path to the `res` file within an AAR file. 26 | */ 27 | val FileSystem.res: Path get() = this / "res" 28 | 29 | /** 30 | * The full path to the `R.txt` file within an AAR file. 31 | */ 32 | val FileSystem.rTxt: Path get() = this / "R.txt" 33 | 34 | /** 35 | * The full path to the `public.txt` file within an AAR file. 36 | */ 37 | val FileSystem.publicTxt: Path get() = this / "public.txt" 38 | 39 | /** 40 | * The full path to the `assets` file within an AAR file. 41 | */ 42 | val FileSystem.assets: Path get() = this / "assets" 43 | 44 | /** 45 | * The full path to the `libs` file within an AAR file. 46 | */ 47 | val FileSystem.libs: Path get() = this / "libs" 48 | 49 | /** 50 | * The full path to the `jni` file within an AAR file. 51 | */ 52 | val FileSystem.jni: Path get() = this / "jni" 53 | 54 | /** 55 | * The full path to the `proguard.txt` file within an AAR file. 56 | */ 57 | val FileSystem.proguardTxt: Path get() = this / "proguard.txt" 58 | 59 | /** 60 | * The full path to the `lint.jar` file within an AAR file. 61 | */ 62 | val FileSystem.lintJar: Path get() = this / "lint.jar" 63 | 64 | /** 65 | * The full path to the `navigation.json` file within an AAR file. 66 | */ 67 | val FileSystem.navigationJson: Path get() = this / "navigation.json" 68 | 69 | /** 70 | * The full path to the `api.jar` file within an AAR file. 71 | */ 72 | val FileSystem.apiJar: Path get() = this / "api.jar" 73 | -------------------------------------------------------------------------------- /docs/publishing-jar.md: -------------------------------------------------------------------------------- 1 | The merged jar is included in a Gradle `SoftwareComponent` that you can publish using your plugin of choice. 2 | 3 | Integrating publishing with common publishing plugins is very simple, but direct access to the generated `jar` file 4 | is also available if a custom publishing solution is needed. 5 | 6 | ???+ note "maven-publish" 7 | 8 | [https://docs.gradle.org/current/userguide/publishing_maven.html](https://docs.gradle.org/current/userguide/publishing_maven.html) 9 | 10 | === "Kotlin" 11 | 12 | ```kotlin 13 | afterEvaluate { 14 | publishing { 15 | publications { 16 | create("maven") { 17 | from(components["java"]) 18 | } 19 | } 20 | } 21 | } 22 | ``` 23 | 24 | === "Groovy" 25 | 26 | ```groovy 27 | afterEvaluate { 28 | publishing { 29 | publications { 30 | maven(MavenPublication) { 31 | from(components.java) 32 | } 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | ??? note "com.vanniktech.maven.publish" 39 | 40 | [https://github.com/vanniktech/gradle-maven-publish-plugin](https://github.com/vanniktech/gradle-maven-publish-plugin) 41 | 42 | No configuration needed! Works right out of the box. 43 | 44 | ??? note "Custom Publishing" 45 | 46 | If you have your own custom publishing step, you can reference the generated `jar` file as a property like so: 47 | 48 | === "Kotlin" 49 | 50 | ```kotlin 51 | abstract class MyCustomPublishTask { 52 | @get:InputFile 53 | abstract val inputJar: RegularFileProperty 54 | 55 | // ... 56 | } 57 | 58 | tasks.named("publish") { 59 | inputJar.set(tasks.named("packageJar").flatMap { it.outputJar }) 60 | } 61 | ``` 62 | 63 | === "Groovy" 64 | 65 | ```groovy 66 | abstract class MyCustomPublishTask { 67 | @InputFile 68 | abstract RegularFileProperty inputJar; 69 | 70 | // ... 71 | } 72 | 73 | tasks.named("publish", MyCustomPublishTask).configureEach { 74 | inputJar.set(tasks.named("packageJar", PackageJarTask).flatMap { it.outputJar }) 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/Attribute.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNCHECKED_CAST") 2 | 3 | package sh.christian.aaraar.model.classeditor 4 | 5 | import javassist.CtBehavior 6 | import javassist.CtClass 7 | import javassist.CtField 8 | import javassist.bytecode.AnnotationDefaultAttribute 9 | import javassist.bytecode.AnnotationsAttribute 10 | import javassist.bytecode.AttributeInfo 11 | import javassist.bytecode.MethodParametersAttribute 12 | import javassist.bytecode.ParameterAnnotationsAttribute 13 | 14 | internal sealed class Attribute(val key: String) { 15 | object VisibleAnnotations : 16 | Attribute(AnnotationsAttribute.visibleTag) 17 | 18 | object InvisibleAnnotations : 19 | Attribute(AnnotationsAttribute.invisibleTag) 20 | 21 | object VisibleParameterAnnotations : 22 | Attribute(ParameterAnnotationsAttribute.visibleTag) 23 | 24 | object InvisibleParameterAnnotations : 25 | Attribute(ParameterAnnotationsAttribute.invisibleTag) 26 | 27 | object AnnotationDefaultValue : 28 | Attribute(AnnotationDefaultAttribute.tag) 29 | 30 | object MethodParameters : 31 | Attribute(MethodParametersAttribute.tag) 32 | } 33 | 34 | internal fun CtClass.get(attribute: Attribute): T? { 35 | return classFile.getAttribute(attribute.key) as T? 36 | } 37 | 38 | internal fun CtClass.set( 39 | attribute: Attribute, 40 | value: T?, 41 | ) { 42 | if (value == null) classFile.removeAttribute(attribute.key) else classFile.addAttribute(value) 43 | } 44 | 45 | internal fun CtBehavior.get(attribute: Attribute): T? { 46 | return methodInfo.getAttribute(attribute.key) as T? 47 | } 48 | 49 | internal fun CtBehavior.set( 50 | attribute: Attribute, 51 | value: T?, 52 | ) { 53 | if (value == null) methodInfo.removeAttribute(attribute.key) else methodInfo.addAttribute(value) 54 | } 55 | 56 | internal fun CtField.get(attribute: Attribute): T? { 57 | return fieldInfo.getAttribute(attribute.key) as T? 58 | } 59 | 60 | internal fun CtField.set( 61 | attribute: Attribute, 62 | value: T?, 63 | ) { 64 | if (value == null) fieldInfo.removeAttribute(attribute.key) else fieldInfo.addAttribute(value) 65 | } 66 | -------------------------------------------------------------------------------- /gradle-plugin/src/main/kotlin/sh/christian/aaraar/gradle/ApiJarProcessor.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.gradle 2 | 3 | import sh.christian.aaraar.model.AarArchive 4 | import sh.christian.aaraar.model.ApiJar 5 | import sh.christian.aaraar.model.ArtifactArchive 6 | import sh.christian.aaraar.model.classeditor.MutableClasspath 7 | 8 | /** 9 | * Subclass of [ArtifactArchiveProcessor] to allow for producing an `api.jar` element inside an AAR file. 10 | * 11 | * The `api.jar` file is an optional element that contains information about the library's public API. 12 | * This file helps developers using the library understand its exposed classes, methods, and functionalities. 13 | * When this file exists in an AAR package, it will be used it as the source of truth for which members are exposed 14 | * externally by the AAR, and which members can be referenced at compile time. 15 | * 16 | * Generating a custom `api.jar` file can be used to hide certain public members from IDE autocomplete, though they 17 | * can still be referenced and invoked via reflection at runtime as per usual. 18 | * 19 | * This has no effect if applied to a module that does not produce an Android AAR file. 20 | */ 21 | interface ApiJarProcessor : ArtifactArchiveProcessor { 22 | 23 | /** Whether the processor is enabled or not. If `false`, no `api.jar` file will be produced. */ 24 | fun isEnabled(): Boolean = true 25 | 26 | /** 27 | * Provides the processor with the merged AAR file and a [MutableClasspath] from which an `api.jar` will be based on. 28 | * The classpath defaults to the public API of [AarArchive.classes], but supports adding/removing/altering classes. 29 | * 30 | * This method is only invoked if [isEnabled] is `true`. 31 | */ 32 | fun processClasspath( 33 | aarArchive: AarArchive, 34 | classpath: MutableClasspath, 35 | ) 36 | 37 | override fun process(archive: ArtifactArchive): ArtifactArchive { 38 | return when (archive) { 39 | is AarArchive -> { 40 | if (isEnabled()) { 41 | val inputApiJar = archive.classes.archive 42 | val classpath = MutableClasspath.from(inputApiJar) 43 | 44 | processClasspath(archive, classpath) 45 | 46 | val apiClasses = classpath.apply { asApiJar() }.toGenericJarArchive() 47 | archive.copy(apiJar = ApiJar(apiClasses)) 48 | } else { 49 | archive 50 | } 51 | } 52 | else -> archive 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/RTxtTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import com.android.ide.common.symbols.Symbol.Companion.createSymbol 4 | import com.android.ide.common.symbols.SymbolTable 5 | import com.android.resources.ResourceType 6 | import io.kotest.matchers.paths.shouldExist 7 | import io.kotest.matchers.paths.shouldNotExist 8 | import io.kotest.matchers.shouldBe 9 | import sh.christian.aaraar.utils.shouldHaveContents 10 | import sh.christian.aaraar.utils.withFile 11 | import kotlin.test.Test 12 | 13 | class RTxtTest { 14 | 15 | private val emptySymbolTable = SymbolTable.builder().build() 16 | 17 | private val smallSymbolTable = SymbolTable.builder() 18 | .add(createSymbol(ResourceType.STRING, "app_name", value = 100)) 19 | .build() 20 | 21 | private val largeSymbolTable = SymbolTable.builder() 22 | .add(createSymbol(ResourceType.STRING, "app_name", value = 100)) 23 | .add(createSymbol(ResourceType.STRING, "activity_name", value = 101)) 24 | .add(createSymbol(ResourceType.INTEGER, "anim_duration", value = 102)) 25 | .add(createSymbol(ResourceType.FONT, "inter", value = 103)) 26 | .add(createSymbol(ResourceType.FONT, "proxima_nova", value = 104)) 27 | .add(createSymbol(ResourceType.BOOL, "is_tablet", value = 105)) 28 | .build() 29 | 30 | @Test 31 | fun `empty symbol table does not write R txt file`() = withFile { 32 | val rTxt = RTxt(symbolTable = emptySymbolTable) 33 | 34 | rTxt.writeTo(filePath) 35 | filePath.shouldNotExist() 36 | } 37 | 38 | @Test 39 | fun `symbol table with at least one symbol writes R txt file`() = withFile { 40 | val rTxt = RTxt(symbolTable = smallSymbolTable) 41 | 42 | rTxt.writeTo(filePath) 43 | filePath.shouldExist() 44 | filePath shouldHaveContents """ 45 | int string app_name 0x64 46 | """ 47 | } 48 | 49 | @Test 50 | fun `test toString`() { 51 | val rTxt = RTxt(symbolTable = largeSymbolTable) 52 | rTxt.toString() shouldBe """ 53 | int bool is_tablet 0x69 54 | int font inter 0x67 55 | int font proxima_nova 0x68 56 | int integer anim_duration 0x66 57 | int string activity_name 0x65 58 | int string app_name 0x64 59 | 60 | """.trimIndent() 61 | } 62 | 63 | @Test 64 | fun `test equality`() { 65 | val rTxt1 = RTxt(symbolTable = largeSymbolTable) 66 | val rTxt2 = RTxt(symbolTable = largeSymbolTable) 67 | rTxt1 shouldBe rTxt2 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/GenericJarArchiveShader.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl 2 | 3 | import sh.christian.aaraar.model.GenericJarArchive 4 | import sh.christian.aaraar.model.ShadeConfiguration 5 | import sh.christian.aaraar.shading.Shader 6 | import sh.christian.aaraar.shading.impl.transform.JarProcessorChain 7 | import sh.christian.aaraar.shading.pipeline.ClassFileFilter 8 | import sh.christian.aaraar.shading.pipeline.ClassFileShader 9 | import sh.christian.aaraar.shading.pipeline.ClassFilesProcessor 10 | import sh.christian.aaraar.shading.pipeline.KotlinModuleFilter 11 | import sh.christian.aaraar.shading.pipeline.KotlinModuleShader 12 | import sh.christian.aaraar.shading.pipeline.ResourceFileShader 13 | import sh.christian.aaraar.shading.pipeline.ResourceFilter 14 | import sh.christian.aaraar.shading.pipeline.ServiceLoaderFilter 15 | import sh.christian.aaraar.shading.pipeline.ServiceLoaderShader 16 | 17 | /** 18 | * Standard implementation for shading a JAR file by applying rules from the [ShadeConfiguration] in this order: 19 | * - Remove class files matching [`classDeletes`][ShadeConfiguration.classDeletes]. 20 | * - Remove resource files matching [`resourceExclusions`][ShadeConfiguration.resourceDeletes]. 21 | * - Rename class files and class references matching [`classRenames`][ShadeConfiguration.classRenames]. 22 | * - Rename resource files matching [`resourceRenames`][ShadeConfiguration.resourceRenames]. 23 | * 24 | * This ordering is important since class files are removed based on their _original_ name, not their shaded name. 25 | */ 26 | class GenericJarArchiveShader : Shader { 27 | override fun shade(source: GenericJarArchive, shadeConfiguration: ShadeConfiguration): GenericJarArchive { 28 | val processor = JarProcessorChain( 29 | ResourceFilter(shadeConfiguration.resourceDeletes), 30 | ClassFileFilter(shadeConfiguration.classDeletes), 31 | ClassFileShader(shadeConfiguration.classRenames), 32 | ResourceFileShader(shadeConfiguration.resourceRenames), 33 | ServiceLoaderFilter(shadeConfiguration.classDeletes), 34 | ServiceLoaderShader(shadeConfiguration.classRenames), 35 | KotlinModuleFilter(shadeConfiguration.classDeletes), 36 | KotlinModuleShader(shadeConfiguration.classRenames), 37 | ) 38 | 39 | val newArchiveEntries = ClassFilesProcessor(processor).process(source) 40 | 41 | return GenericJarArchive(newArchiveEntries) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/PublicTxtTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model 2 | 3 | import com.android.ide.common.symbols.Symbol.Companion.createSymbol 4 | import com.android.ide.common.symbols.SymbolTable 5 | import com.android.resources.ResourceType 6 | import io.kotest.matchers.paths.shouldExist 7 | import io.kotest.matchers.paths.shouldNotExist 8 | import io.kotest.matchers.shouldBe 9 | import sh.christian.aaraar.utils.shouldHaveContents 10 | import sh.christian.aaraar.utils.withFile 11 | import kotlin.test.Test 12 | 13 | class PublicTxtTest { 14 | 15 | private val emptySymbolTable = SymbolTable.builder().build() 16 | 17 | private val smallSymbolTable = SymbolTable.builder() 18 | .add(createSymbol(ResourceType.STRING, "app_name", value = 100)) 19 | .build() 20 | 21 | private val largeSymbolTable = SymbolTable.builder() 22 | .add(createSymbol(ResourceType.STRING, "app_name", value = 100)) 23 | .add(createSymbol(ResourceType.STRING, "activity_name", value = 101)) 24 | .add(createSymbol(ResourceType.INTEGER, "anim_duration", value = 102)) 25 | .add(createSymbol(ResourceType.FONT, "inter", value = 103)) 26 | .add(createSymbol(ResourceType.FONT, "proxima_nova", value = 104)) 27 | .add(createSymbol(ResourceType.BOOL, "is_tablet", value = 105)) 28 | .build() 29 | 30 | @Test 31 | fun `empty symbol table does not write public txt file`() = withFile { 32 | val publicTxt = PublicTxt(symbolTable = emptySymbolTable) 33 | 34 | publicTxt.writeTo(filePath) 35 | filePath.shouldNotExist() 36 | } 37 | 38 | @Test 39 | fun `symbol table with at least one symbol writes public txt file`() = withFile { 40 | val publicTxt = PublicTxt(symbolTable = smallSymbolTable) 41 | 42 | publicTxt.writeTo(filePath) 43 | filePath.shouldExist() 44 | filePath shouldHaveContents """ 45 | string app_name 46 | """ 47 | } 48 | 49 | @Test 50 | fun `test toString`() { 51 | val publicTxt = PublicTxt(symbolTable = largeSymbolTable) 52 | publicTxt.toString() shouldBe """ 53 | bool is_tablet 54 | font inter 55 | font proxima_nova 56 | integer anim_duration 57 | string activity_name 58 | string app_name 59 | """.trimIndent() 60 | } 61 | 62 | @Test 63 | fun `test equality`() { 64 | val publicTxt1 = PublicTxt(symbolTable = largeSymbolTable) 65 | val publicTxt2 = PublicTxt(symbolTable = largeSymbolTable) 66 | publicTxt1 shouldBe publicTxt2 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/model/classeditor/metadata/FieldMetadataTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor.metadata 2 | 3 | import io.kotest.matchers.collections.shouldBeEmpty 4 | import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder 5 | import io.kotest.matchers.nulls.shouldBeNull 6 | import io.kotest.matchers.nulls.shouldNotBeNull 7 | import io.kotest.matchers.shouldBe 8 | import sh.christian.aaraar.model.classeditor.name 9 | import sh.christian.aaraar.model.classeditor.requireMetadata 10 | import sh.christian.aaraar.model.classeditor.types.objectType 11 | import sh.christian.aaraar.model.classeditor.types.stringType 12 | import sh.christian.aaraar.utils.ktLibraryJarPath 13 | import sh.christian.aaraar.utils.loadJar 14 | import sh.christian.aaraar.utils.withClasspath 15 | import kotlin.test.Test 16 | 17 | class FieldMetadataTest { 18 | 19 | @Test 20 | fun `remove field`() = withClasspath(ktLibraryJarPath.loadJar()) { cp -> 21 | cp.name.fields.map { it.name }.shouldContainExactlyInAnyOrder("name") 22 | cp.name.requireMetadata().properties.map { it.name }.shouldContainExactlyInAnyOrder("name") 23 | 24 | cp.name.fields = emptyList() 25 | cp.name.methods = cp.name.methods.filter { it.name != "getName" && it.name != "setName" } 26 | cp.name.finalizeClass() 27 | 28 | cp.name.requireMetadata().properties.shouldBeEmpty() 29 | } 30 | 31 | @Test 32 | fun `set field name`() = withClasspath(ktLibraryJarPath.loadJar()) { cp -> 33 | cp.name.getField("name").shouldNotBeNull() 34 | cp.name.requireMetadata().properties.map { it.name }.shouldContainExactlyInAnyOrder("name") 35 | 36 | cp.name.getField("name")!!.name = "myName" 37 | cp.name.finalizeClass() 38 | 39 | cp.name.getField("name").shouldBeNull() 40 | cp.name.getField("myName").shouldNotBeNull() 41 | cp.name.requireMetadata().properties.map { it.name }.shouldContainExactlyInAnyOrder("myName") 42 | } 43 | 44 | @Test 45 | fun `set field type`() = withClasspath(ktLibraryJarPath.loadJar()) { cp -> 46 | cp.name.fields.single().type shouldBe cp.stringType 47 | cp.name.requireMetadata().properties.single().returnType.classifier shouldBe cp.kmClassifier("kotlin.String") 48 | 49 | cp.name.fields.single().type = cp.objectType 50 | cp.name.finalizeClass() 51 | 52 | cp.name.fields.single().type shouldBe cp.objectType 53 | cp.name.requireMetadata().properties.single().returnType.classifier shouldBe cp.kmClassifier("kotlin.Any") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/Modifier.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | import javassist.Modifier as JModifier 4 | 5 | /** 6 | * A modifier keyword that may be applied to a class, member, or parameter. 7 | */ 8 | enum class Modifier { 9 | PUBLIC, 10 | PROTECTED, 11 | PRIVATE, 12 | ABSTRACT, 13 | STATIC, 14 | FINAL, 15 | TRANSIENT, 16 | VOLATILE, 17 | SYNCHRONIZED, 18 | NATIVE, 19 | STRICTFP, 20 | VARARG, 21 | ANNOTATION, 22 | INTERFACE, 23 | ENUM, 24 | ; 25 | 26 | override fun toString(): String { 27 | return name.lowercase() 28 | } 29 | 30 | internal companion object { 31 | fun fromModifiers(modifiers: Int): Set { 32 | return setOfNotNull( 33 | PUBLIC.takeIf { JModifier.isPublic(modifiers) }, 34 | PROTECTED.takeIf { JModifier.isProtected(modifiers) }, 35 | PRIVATE.takeIf { JModifier.isPrivate(modifiers) }, 36 | ABSTRACT.takeIf { JModifier.isAbstract(modifiers) }, 37 | STATIC.takeIf { JModifier.isStatic(modifiers) }, 38 | FINAL.takeIf { JModifier.isFinal(modifiers) }, 39 | TRANSIENT.takeIf { JModifier.isTransient(modifiers) }, 40 | VOLATILE.takeIf { JModifier.isVolatile(modifiers) }, 41 | SYNCHRONIZED.takeIf { JModifier.isSynchronized(modifiers) }, 42 | NATIVE.takeIf { JModifier.isNative(modifiers) }, 43 | STRICTFP.takeIf { JModifier.isStrict(modifiers) }, 44 | VARARG.takeIf { JModifier.isVarArgs(modifiers) }, 45 | ANNOTATION.takeIf { JModifier.isAnnotation(modifiers) }, 46 | INTERFACE.takeIf { JModifier.isInterface(modifiers) }, 47 | ENUM.takeIf { JModifier.isEnum(modifiers) }, 48 | ) 49 | } 50 | 51 | internal fun Set.toModifiers(): Int { 52 | return map { 53 | when (it) { 54 | PUBLIC -> JModifier.PUBLIC 55 | PROTECTED -> JModifier.PROTECTED 56 | PRIVATE -> JModifier.PRIVATE 57 | ABSTRACT -> JModifier.ABSTRACT 58 | STATIC -> JModifier.STATIC 59 | FINAL -> JModifier.FINAL 60 | TRANSIENT -> JModifier.TRANSIENT 61 | VOLATILE -> JModifier.VOLATILE 62 | SYNCHRONIZED -> JModifier.SYNCHRONIZED 63 | NATIVE -> JModifier.NATIVE 64 | STRICTFP -> JModifier.STRICT 65 | VARARG -> JModifier.VARARGS 66 | ANNOTATION -> JModifier.ANNOTATION 67 | INTERFACE -> JModifier.INTERFACE 68 | ENUM -> JModifier.ENUM 69 | } 70 | }.fold(0, Int::or) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /core/src/test/kotlin/sh/christian/aaraar/shading/GenericJarArchiveNonStandardTest.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading 2 | 3 | import io.kotest.matchers.collections.shouldHaveSize 4 | import io.kotest.matchers.maps.shouldHaveKey 5 | import io.kotest.matchers.shouldBe 6 | import org.junit.jupiter.api.Test 7 | import sh.christian.aaraar.utils.ktLibraryJarPath 8 | import sh.christian.aaraar.utils.loadJar 9 | import sh.christian.aaraar.utils.withClasspath 10 | 11 | class GenericJarArchiveNonStandardTest { 12 | @Test 13 | fun `no shading`() { 14 | val originalClasses = ktLibraryJarPath.loadJar() 15 | originalClasses shouldHaveKey "sh/christian/mylibrary/Name-Maker.class" 16 | originalClasses shouldHaveKey "sh/christian/mylibrary/Name-Maker${'$'}Companion.class" 17 | } 18 | 19 | @Test 20 | fun `shade by class name with classes with non-java characters`() { 21 | val shadedClasses = ktLibraryJarPath.loadJar().shaded( 22 | classRenames = mapOf("sh.christian.mylibrary.Name-Maker" to "sh.christian.mylibrary.NameMaker"), 23 | ) 24 | shadedClasses shouldHaveKey "sh/christian/mylibrary/NameMaker.class" 25 | shadedClasses shouldHaveKey "sh/christian/mylibrary/Name-Maker${'$'}Companion.class" 26 | } 27 | 28 | @Test 29 | fun `shade by class name with inner classes with classes with non-java characters`() { 30 | val shadedClasses = ktLibraryJarPath.loadJar().shaded( 31 | classRenames = mapOf("sh.christian.mylibrary.Name-Maker**" to "sh.christian.mylibrary.NameMaker@1"), 32 | ) 33 | shadedClasses shouldHaveKey "sh/christian/mylibrary/NameMaker.class" 34 | shadedClasses shouldHaveKey "sh/christian/mylibrary/NameMaker${'$'}Companion.class" 35 | } 36 | 37 | @Test 38 | fun `shade by package name with classes with non-java characters`() { 39 | val shadedClasses = ktLibraryJarPath.loadJar().shaded( 40 | classRenames = mapOf("sh.christian.mylibrary.**" to "sh.christian.lib.@1"), 41 | ) 42 | shadedClasses shouldHaveKey "sh/christian/lib/Name-Maker.class" 43 | shadedClasses shouldHaveKey "sh/christian/lib/Name-Maker${'$'}Companion.class" 44 | } 45 | 46 | @Test 47 | fun `shade by package name with arrays`() { 48 | val shadedClasses = ktLibraryJarPath.loadJar().shaded( 49 | classRenames = mapOf("sh.christian.mylibrary.**" to "sh.christian.lib.@1"), 50 | ) 51 | 52 | withClasspath(shadedClasses) { cp -> 53 | val fields = cp["sh.christian.lib.Foos"].fields 54 | 55 | fields shouldHaveSize 1 56 | fields[0].name shouldBe "twoFoos" 57 | fields[0].type.qualifiedName shouldBe "sh.christian.lib.Foo[]" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/model/classeditor/MutableFieldReference.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.model.classeditor 2 | 3 | import javassist.CtField 4 | import javassist.bytecode.ConstantAttribute 5 | import kotlinx.metadata.KmProperty 6 | import kotlinx.metadata.visibility 7 | import sh.christian.aaraar.model.classeditor.Modifier.Companion.toModifiers 8 | import sh.christian.aaraar.model.classeditor.metadata.fieldSignature 9 | import sh.christian.aaraar.model.classeditor.metadata.toVisibility 10 | 11 | /** 12 | * Represents a declared field for a particular class. 13 | * 14 | * This representation is mutable, to allow changing properties of the field. 15 | */ 16 | class MutableFieldReference 17 | internal constructor( 18 | internal val classpath: MutableClasspath, 19 | internal val _field: CtField, 20 | ) : MutableMemberReference(), FieldReference { 21 | override val signature: Signature 22 | get() = FieldSignature(_field.name, _field.fieldInfo.descriptor) 23 | 24 | val propertyMetadata: KmProperty? = 25 | classpath[_field.declaringClass].kotlinMetadata?.kmClass?.properties 26 | ?.firstOrNull { it.fieldSignature() == signature } 27 | 28 | override var modifiers: Set 29 | get() = Modifier.fromModifiers(_field.modifiers) 30 | set(value) { 31 | _field.modifiers = value.toModifiers() 32 | propertyMetadata?.visibility = value.toVisibility() 33 | } 34 | 35 | override var name: String 36 | get() = _field.name 37 | set(value) { 38 | _field.name = value 39 | propertyMetadata?.name = value 40 | } 41 | 42 | override var annotations: List by ::fieldAnnotations 43 | 44 | override var type: MutableClassReference 45 | get() = classpath[_field.type] 46 | set(value) { 47 | _field.type = value._class 48 | propertyMetadata?.returnType?.classifier = classpath.kmClassifier(value.qualifiedName) 49 | } 50 | 51 | /** Removes bytecode stored to describe this field's constant value, if it is a `static final` field. */ 52 | fun removeConstantInitializer() { 53 | _field.fieldInfo.removeAttribute(ConstantAttribute.tag) 54 | } 55 | 56 | override fun equals(other: Any?): Boolean { 57 | if (other !is MutableFieldReference) return false 58 | return _field == other._field 59 | } 60 | 61 | override fun hashCode(): Int { 62 | var result = name.hashCode() 63 | result = 31 * result + annotations.hashCode() 64 | result = 31 * result + type.hashCode() 65 | return result 66 | } 67 | 68 | override fun toString(): String { 69 | val valOrVar = if (Modifier.FINAL in modifiers) "val" else "var" 70 | return "$valOrVar ${_field.declaringClass.name}.$name: $type" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /core/src/main/kotlin/sh/christian/aaraar/shading/impl/transform/PackageRemapper.kt: -------------------------------------------------------------------------------- 1 | package sh.christian.aaraar.shading.impl.transform 2 | 3 | import org.objectweb.asm.commons.Remapper 4 | 5 | internal class PackageRemapper( 6 | private val patterns: List, 7 | ) : Remapper() where T : AbstractPattern, T : ReplacePattern { 8 | private val typeCache: MutableMap = mutableMapOf() 9 | private val pathCache: MutableMap = mutableMapOf() 10 | private val valueCache: MutableMap = mutableMapOf() 11 | 12 | constructor(vararg patterns: T) : this(patterns.toList()) 13 | 14 | override fun map(key: String): String { 15 | return typeCache.getOrPut(key) { 16 | val mapped = replaceHelper(key) 17 | if (key == mapped) return mapped 18 | mapped 19 | } 20 | } 21 | 22 | override fun mapValue(value: Any?): Any? { 23 | return if (value is String) { 24 | valueCache.getOrPut(value) { 25 | if (isClassArrayDescriptor(value)) { 26 | value.replace('.', '/').let(::mapDesc).replace('/', '.') 27 | } else { 28 | var s = mapPath(value) 29 | if (s == value) { 30 | val hasDot = '.' in s 31 | val hasSlash = '/' in s 32 | if (!hasDot || !hasSlash) { 33 | s = if (hasDot) { 34 | s.replace('.', '/').let(::replaceHelper).replace('/', '.') 35 | } else { 36 | replaceHelper(s) 37 | } 38 | } 39 | } 40 | s 41 | } 42 | } 43 | } else { 44 | super.mapValue(value) 45 | } 46 | } 47 | 48 | fun mapPath(path: String): String { 49 | return pathCache.getOrPut(path) { 50 | var (s, end) = if ('/' !in path) { 51 | RESOURCE_SUFFIX to path 52 | } else { 53 | path.substringBeforeLast("/") + "/$RESOURCE_SUFFIX" to path.substringAfterLast("/") 54 | } 55 | 56 | s = if (s.startsWith("/")) { 57 | // Map the path without the leading slash that makes it absolute 58 | s.substring(1).let(::replaceHelper).let { "/$it" } 59 | } else { 60 | replaceHelper(s) 61 | } 62 | 63 | if (RESOURCE_SUFFIX !in s) { 64 | path 65 | } else { 66 | s.removeSuffix(RESOURCE_SUFFIX) + end 67 | } 68 | } 69 | } 70 | 71 | private fun replaceHelper(value: String): String { 72 | return patterns.firstNotNullOfOrNull { it.replace(value) } ?: value 73 | } 74 | 75 | private fun isClassArrayDescriptor(desc: String): Boolean { 76 | return desc.startsWith("[L") && desc.endsWith(";") 77 | } 78 | 79 | private companion object { 80 | const val RESOURCE_SUFFIX = "RESOURCE" 81 | } 82 | } 83 | --------------------------------------------------------------------------------