├── .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 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
5 |
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 |
5 |
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 |
4 |
5 |
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 |
5 |
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 |
8 |
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 |  
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
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 |
--------------------------------------------------------------------------------