Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iterated DSL #16

Merged
merged 5 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,26 @@ plugins {
id("io.github.gmazzo.importclasses") version "<latest>"
}

importClasses {
repackageTo = "org.test.imported"
keep("org.apache.commons.lang3.StringUtils")
}

dependencies {
importClasses("org.apache.commons:commons-lang3:3.14.0")
}
```
And then use the new `importClasses` DSL on the target `SourceSet` you want the classes to be imported:
To configure the dependencies to import, a `importClasses` configuration will be created as stated above.
Also a companion `importClassesLibrary` configuration will be created, which will be mapped to [Proguard's `-libraryjars` option](https://www.guardsquare.com/manual/configuration/usage#libraryjars).

By default, the plugin will bind with the `main` SourceSet, this can be changed by setting the `sourceSet` property:
```kotlin
sourceSets.main {
importClasses("org.apache.commons:commons-lang3:3.14.0") {
repackageTo = "org.test.imported"
keep("org.apache.commons.lang3.StringUtils")
}
importClasses {
sourceSet = sourceSets.test
}
```

Then the `main` SourceSet will have the class `org.apache.commons.lang3.StringUtils` from (`org.apache.commons:commons-lang3:3.14.0`)
Then the SourceSet will have the class `org.apache.commons.lang3.StringUtils` from (`org.apache.commons:commons-lang3:3.14.0`)
imported and repackaged as `org.test.imported.StringUtils`.
```java
package org.test;
Expand All @@ -46,3 +54,22 @@ public class Foo {
> This plugin uses Gradle's [Artifact Transform](https://docs.gradle.org/current/userguide/artifact_transforms.html)
> by running [`Proguard`](https://www.guardsquare.com/manual/home) on the target dependency.
> You can pass any Proguard option to it inside `importClasses`'s configuration block by calling `option(<rule>)`

## Having multiples `importClasses` instances
You can configure multiple (and isolated) `importClasses` trough the DSL:
```kotlin
importClasses {
specs {
create("another") {
sourceSet = sourceSets.main // to consume it in the `main` source set
repackageTo = "org.foo.another.imported"
keep("org.foo.AnotherClass")
}
}
}

dependencies {
importClassesAnother("org.foo:foo:1.0.0")
}
```
Same as the default configuration, `importClassesAnother` and `importClassesAnotherLibrary` configurations will be created for the `another` spec.
13 changes: 6 additions & 7 deletions demo-groovy/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@ plugins {

java.toolchain.languageVersion = JavaLanguageVersion.of(libs.versions.java.get())

sourceSets.main {
importClasses(libs.demo.commons.lang3) {
repackageTo = "io.github.gmazzo.importclasses.demo.imported"
keep("org.apache.commons.lang3.StringUtils")

include("**.class")
}
importClasses {
repackageTo = "io.github.gmazzo.importclasses.demo.imported"
keep("org.apache.commons.lang3.StringUtils")
include("**.class")
}

dependencies {
importClasses(libs.demo.commons.lang3)

implementation(libs.demo.groovy)

testImplementation(platform(libs.junit.bom))
Expand Down
13 changes: 6 additions & 7 deletions demo-kts/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ plugins {

java.toolchain.languageVersion = JavaLanguageVersion.of(libs.versions.java.get())

sourceSets.main {
importClasses(libs.demo.commons.lang3) {
repackageTo = "io.github.gmazzo.importclasses.demo.imported"
keep("org.apache.commons.lang3.StringUtils")

include("**.class")
}
importClasses {
repackageTo = "io.github.gmazzo.importclasses.demo.imported"
keep("org.apache.commons.lang3.StringUtils")
include("**.class")
}

dependencies {
importClasses(libs.demo.commons.lang3)

testImplementation(platform(libs.junit.bom))
testImplementation(libs.junit.params)
}
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ junit-params = { module = "org.junit.jupiter:junit-jupiter-params" }
proguard = { module = "com.guardsquare:proguard-base", version = "7.6.1" }

[plugins]
buildConfig = { id = "com.github.gmazzo.buildconfig", version = "5.5.1" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-samReceiver = { id = "org.jetbrains.kotlin.plugin.sam.with.receiver", version.ref = "kotlin" }
gradle-pluginPublish = { id = "com.gradle.plugin-publish", version = "1.3.0" }
Expand Down
12 changes: 10 additions & 2 deletions plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
plugins {
alias(libs.plugins.buildConfig)
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.samReceiver)
alias(libs.plugins.gradle.pluginPublish)
Expand All @@ -13,6 +14,7 @@ version = providers
.standardOutput.asText.get().trim().removePrefix("v")

java.toolchain.languageVersion.set(JavaLanguageVersion.of(libs.versions.java.get()))
kotlin.compilerOptions.freeCompilerArgs.add("-Xjvm-default=all")
samWithReceiver.annotation(HasImplicitReceiver::class.qualifiedName!!)

gradlePlugin {
Expand All @@ -30,16 +32,22 @@ gradlePlugin {
}
}

buildConfig {
packageName = "io.github.gmazzo.importclasses"
buildConfigField("PROGUARD_DEFAULT_DEPENDENCY", libs.proguard.map { "${it.group}:${it.name}:${it.version}" })
}

dependencies {
fun plugin(plugin: Provider<PluginDependency>) =
plugin.map { "${it.pluginId}:${it.pluginId}.gradle.plugin:${it.version}" }

compileOnly(gradleKotlinDsl())
compileOnly(libs.proguard)

testImplementation(gradleKotlinDsl())
testImplementation(gradleTestKit())
testImplementation(libs.proguard)
testImplementation(plugin(libs.plugins.kotlin.jvm))

implementation(libs.proguard)
}

testing.suites.withType<JvmTestSuite> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package io.github.gmazzo.importclasses

import org.gradle.api.Action
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property

interface ImportClassesExtension {
interface ImportClassesExtension : ImportClassesSpec {

operator fun invoke(dependency: Any, vararg moreDependencies: Any, configure: Action<ImportClassesSpec>)
val proguardMainClass : Property<String>

val proguardJvmArgs : ListProperty<String>

val specs: NamedDomainObjectContainer<ImportClassesSpec>

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,192 +2,18 @@

package io.github.gmazzo.importclasses

import groovy.lang.Closure
import groovy.lang.MissingMethodException
import io.github.gmazzo.importclasses.ImportClassesPlugin.Companion.EXTENSION_NAME
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.Dependency
import org.gradle.api.attributes.Category.CATEGORY_ATTRIBUTE
import org.gradle.api.attributes.Category.LIBRARY
import org.gradle.api.attributes.LibraryElements
import org.gradle.api.attributes.LibraryElements.CLASSES
import org.gradle.api.attributes.LibraryElements.JAR
import org.gradle.api.attributes.LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE
import org.gradle.api.attributes.LibraryElements.RESOURCES
import org.gradle.api.attributes.Usage.JAVA_RUNTIME
import org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderConvertible
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.Sync
import org.gradle.kotlin.dsl.named
import org.gradle.kotlin.dsl.newInstance
import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.registerTransform
import org.gradle.util.internal.ConfigureUtil
import proguard.ConfigurationConstants.DONT_NOTE_OPTION
import proguard.ConfigurationConstants.DONT_WARN_OPTION
import proguard.ConfigurationConstants.IGNORE_WARNINGS_OPTION
import java.util.*
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.model.ObjectFactory
import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME
import org.gradle.kotlin.dsl.domainObjectContainer
import javax.inject.Inject

internal abstract class ImportClassesExtensionImpl @Inject constructor(
private val project: Project,
private val sourceSet: SourceSet,
) : ImportClassesExtension {
internal abstract class ImportClassesExtensionImpl private constructor(
override val specs: NamedDomainObjectContainer<ImportClassesSpec>,
) : ImportClassesExtension,
ImportClassesSpec by specs.create(MAIN_SOURCE_SET_NAME) {

override fun invoke(
dependency: Any,
vararg moreDependencies: Any,
configure: Action<ImportClassesSpec>,
): Unit = with(project) {

val deps = (sequenceOf(dependency) + moreDependencies)
.map {
when (it) {
is Provider<*> -> it.get()
is ProviderConvertible<*> -> it.asProvider().get()
else -> it
}
}
.map(project.dependencies::create)
.toSortedSet(compareBy { it.discriminatorPart })

val disambiguator = computeDisambiguator(deps)
val discriminator = "imported-$disambiguator"
val jarElements: LibraryElements = objects.named("$JAR+$discriminator")
val classesElements: LibraryElements = objects.named("$CLASSES+$discriminator")
val resourcesElements: LibraryElements = objects.named("$RESOURCES+$discriminator")

fun Configuration.configureAttrs() = attributes {
attribute(USAGE_ATTRIBUTE, objects.named(JAVA_RUNTIME))
attribute(CATEGORY_ATTRIBUTE, objects.named(LIBRARY))
attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(JAR))
}

val config = configurations.maybeCreate(discriminator).apply {
isCanBeResolved = true
isCanBeConsumed = false
isVisible = false
configureAttrs()
dependencies.addAll(deps)
}

val spec = objects.newInstance<ImportClassesSpecImpl>().apply {

keepsAndRenames
.finalizeValueOnRead()

repackageTo
.finalizeValueOnRead()

filters
.finalizeValueOnRead()

extraOptions
.apply {
if (!logger.isDebugEnabled) addAll(
DONT_NOTE_OPTION,
if (logger.isInfoEnabled) IGNORE_WARNINGS_OPTION else DONT_WARN_OPTION
)
}
.finalizeValueOnRead()

libraries
.finalizeValueOnRead()

// excludes by default all known resources related to the module build process
exclude(
"META-INF/LICENSE.txt",
"META-INF/MANIFEST.MF",
"META-INF/*.kotlin_module",
"META-INF/*.SF",
"META-INF/*.DSA",
"META-INF/*.RSA",
"META-INF/maven/**",
"META-INF/versions/*/module-info.class"
)

configure.execute(this)

check(keepsAndRenames.get().isNotEmpty()) { "Must call `keep(<classname>)` at least once" }
}

dependencies.registerTransform(ImportClassesTransform::class) {
val libraries = configurations
.detachedConfiguration(*spec.libraries.get().map(project.dependencies::create).toTypedArray())
.configureAttrs()

from.attribute(LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(JAR))
to.attribute(LIBRARY_ELEMENTS_ATTRIBUTE, jarElements)
parameters.inJARs.from(config)
parameters.libraryJARs.from(libraries)
parameters.keepsAndRenames.value(spec.keepsAndRenames)
parameters.repackageName.value(spec.repackageTo)
parameters.filters.value(spec.filters)
parameters.extraOptions.value(spec.extraOptions)
}

dependencies.registerTransform(ExtractJARTransform::class) {
from.attributes.attribute(LIBRARY_ELEMENTS_ATTRIBUTE, jarElements)
to.attributes.attribute(LIBRARY_ELEMENTS_ATTRIBUTE, classesElements)
parameters.forResources = false
}

dependencies.registerTransform(ExtractJARTransform::class) {
from.attributes.attribute(LIBRARY_ELEMENTS_ATTRIBUTE, jarElements)
to.attributes.attribute(LIBRARY_ELEMENTS_ATTRIBUTE, resourcesElements)
parameters.forResources = true
}

fun extractedFiles(elements: LibraryElements) = config.incoming
.artifactView { attributes.attribute(LIBRARY_ELEMENTS_ATTRIBUTE, elements) }
.files

// a task is required when dependencies are generated by tasks of the build, since it's exposed as an outgoing variant artifact
val extractClasses =
tasks.register<Sync>("extract${disambiguator.replaceFirstChar { it.uppercase() }}ImportedClasses") {
from(extractedFiles(classesElements))
into(layout.buildDirectory.dir("imported-classes/$disambiguator"))
duplicatesStrategy = DuplicatesStrategy.WARN
}

dependencies.add(sourceSet.compileOnlyConfigurationName, extractedFiles(jarElements))
(sourceSet.output.classesDirs as ConfigurableFileCollection).from(extractClasses)
sourceSet.resources.srcDir(extractedFiles(resourcesElements))
}

/**
* For Groovy support
*/
@Suppress("unused")
fun call(vararg args: Any) {
fun missingMethod(): Nothing = throw MissingMethodException(EXTENSION_NAME, SourceSet::class.java, args)

if (args.size < 2) missingMethod()
val configure = args.last() as? Closure<*> ?: missingMethod()

invoke(
dependency = args[0],
moreDependencies = args.drop(1).dropLast(1).toTypedArray(),
configure = ConfigureUtil.configureUsing(configure)
)
}

private val Dependency.discriminatorPart
get() = "$group-$name"

private fun computeDisambiguator(dependencies: SortedSet<Dependency>) = buildString {
check(dependencies.isNotEmpty()) { "At least one dependency is required" }
append(dependencies.first().discriminatorPart)
if (dependencies.size > 1) {
append('-')
append(dependencies.drop(1).map { it.discriminatorPart }.hashCode().toHexString())
}
}
@Inject
constructor(objects: ObjectFactory) : this(objects.domainObjectContainer(ImportClassesSpec::class))

}
Loading
Loading