Skip to content

Commit

Permalink
Setup and test DependenciesExtensions and FunctionExtractor using Dep…
Browse files Browse the repository at this point in the history
…endencyCollector (gradle#28298)
  • Loading branch information
bot-gradle authored Mar 1, 2024
2 parents 7bc4e14 + cd8d107 commit cebf576
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.gradle.internal.declarativedsl.project

import org.gradle.integtests.fixtures.AbstractIntegrationSpec

class DeclarativeDSLCustomDependenciesExtensionsSpec extends AbstractIntegrationSpec {
def 'can configure an extension using DependencyCollector in declarative DSL'() {
given: "a plugin that creates a custom extension using a DependencyCollector"
file("buildSrc/src/main/java/com/example/restricted/DependenciesExtension.java") << defineDependenciesExtension()
file("buildSrc/src/main/java/com/example/restricted/LibraryExtension.java") << defineLibraryExtension()
file("buildSrc/src/main/java/com/example/restricted/RestrictedPlugin.java") << """
package com.example.restricted;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.DependencyScopeConfiguration;
public class RestrictedPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// no plugin application, must create configurations manually
DependencyScopeConfiguration api = project.getConfigurations().dependencyScope("api").get();
DependencyScopeConfiguration implementation = project.getConfigurations().dependencyScope("implementation").get();
// create and wire the custom dependencies extension's dependencies to these global configurations
LibraryExtension restricted = project.getExtensions().create("library", LibraryExtension.class);
api.fromDependencyCollector(restricted.getDependencies().getApi());
implementation.fromDependencyCollector(restricted.getDependencies().getImplementation());
}
}
"""
file("buildSrc/build.gradle") << defineRestrictedPluginBuild()

and: "a build script that adds dependencies using the custom extension"
file("build.gradle.something") << defineDeclarativeDSLBuildScript()
file("settings.gradle") << defineSettings()

expect: "a dependency has been added to the api configuration"
succeeds("dependencies", "--configuration", "api")
outputContains("com.google.guava:guava:30.1.1-jre")

and: "a dependency has been added to the implementation configuration"
succeeds("dependencies", "--configuration", "implementation")
outputContains("org.apache.commons:commons-lang3:3.12.0")
}

def 'can configure an extension using DependencyCollector in declarative DSL and build a java plugin'() {
given: "a plugin that creates a custom extension using a DependencyCollector and applies the java library plugin"
file("buildSrc/src/main/java/com/example/restricted/DependenciesExtension.java") << defineDependenciesExtension()
file("buildSrc/src/main/java/com/example/restricted/LibraryExtension.java") << defineLibraryExtension()
file("buildSrc/src/main/java/com/example/restricted/RestrictedPlugin.java") << """
package com.example.restricted;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaLibraryPlugin;
public class RestrictedPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
// api and implementation configurations created by plugin
project.getPluginManager().apply(JavaLibraryPlugin.class);
// create and wire the custom dependencies extension's dependencies to the global configurations created by the plugin
LibraryExtension restricted = project.getExtensions().create("library", LibraryExtension.class);
project.getConfigurations().getByName("api").fromDependencyCollector(restricted.getDependencies().getApi());
project.getConfigurations().getByName("implementation").fromDependencyCollector(restricted.getDependencies().getImplementation());
}
}
"""
file("buildSrc/build.gradle") << defineRestrictedPluginBuild()

and: "a build script that adds dependencies using the custom extension, and defines a source file requiring the dependencies to compile"
file("src/main/java/com/example/Lib.java") << defineExampleJavaClass()
file("build.gradle.something") << defineDeclarativeDSLBuildScript()
file("settings.gradle") << defineSettings()

expect: "the library can be built successfully"
succeeds("build")
file("build/libs/example.jar").exists()
}

private String defineDependenciesExtension() {
return """
package com.example.restricted;
import org.gradle.api.artifacts.dsl.DependencyCollector;
import org.gradle.api.artifacts.dsl.GradleDependencies;
import org.gradle.api.plugins.jvm.PlatformDependencyModifiers;
import org.gradle.api.plugins.jvm.TestFixturesDependencyModifiers;
import org.gradle.declarative.dsl.model.annotations.Restricted;
@Restricted
public interface DependenciesExtension extends PlatformDependencyModifiers, TestFixturesDependencyModifiers, GradleDependencies {
DependencyCollector getApi();
DependencyCollector getImplementation();
}
"""
}

private String defineLibraryExtension() {
return """
package com.example.restricted;
import org.gradle.api.Action;
import org.gradle.api.model.ObjectFactory;
import org.gradle.declarative.dsl.model.annotations.Configuring;
import org.gradle.declarative.dsl.model.annotations.Restricted;
import javax.inject.Inject;
@Restricted
public abstract class LibraryExtension {
private final DependenciesExtension dependencies;
@Inject
public LibraryExtension(ObjectFactory objectFactory) {
this.dependencies = objectFactory.newInstance(DependenciesExtension.class);
}
public DependenciesExtension getDependencies() {
return dependencies;
}
@Configuring
public void dependencies(Action<? super DependenciesExtension> configure) {
configure.execute(dependencies);
}
}
"""
}

private String defineRestrictedPluginBuild() {
return """
plugins {
id('java-gradle-plugin')
}
gradlePlugin {
plugins {
create("restrictedPlugin") {
id = "com.example.restricted"
implementationClass = "com.example.restricted.RestrictedPlugin"
}
}
}
"""
}

private String defineExampleJavaClass() {
return """
package com.example;
import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang3.StringUtils;
public class Lib {
public static ImmutableSet<String> getPeople() {
return ImmutableSet.of(capitalize("adam johnson"), capitalize("bob smith"), capitalize("carl jones"));
}
private static String capitalize(String input) {
return StringUtils.capitalize(input);
}
}
"""
}

private String defineDeclarativeDSLBuildScript() {
return """
plugins {
id("com.example.restricted")
}
library {
dependencies {
api("com.google.guava:guava:30.1.1-jre")
implementation("org.apache.commons:commons-lang3:3.12.0")
}
}
"""
}

private String defineSettings() {
return """
dependencyResolutionManagement {
repositories {
mavenCentral()
}
}
rootProject.name = 'example'
"""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,25 @@ import org.gradle.internal.declarativedsl.schemaBuilder.FunctionExtractor
import org.gradle.internal.declarativedsl.schemaBuilder.toDataTypeRef
import org.gradle.api.Project
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.artifacts.dsl.DependencyCollector
import org.gradle.api.artifacts.dsl.DependencyHandler
import org.gradle.internal.declarativedsl.analysis.FunctionSemantics.ConfigureSemantics.ConfigureBlockRequirement.NOT_ALLOWED
import org.gradle.internal.declarativedsl.evaluationSchema.EvaluationSchemaComponent
import java.lang.reflect.Type
import java.util.Locale
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.full.functions
import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.memberFunctions
import kotlin.reflect.javaType


/**
* Introduces functions for registering project dependencies, such as `implementation(...)`, as member functions of:
* * [RestrictedDependenciesHandler] in the schema,
* * [DependencyHandler] when resolved at runtime.
* * Any type with getters returning [DependencyCollector] in the schema.
*
* Inspects the configurations available in the given project to build the functions.
*/
Expand All @@ -54,11 +61,13 @@ class DependencyConfigurationsComponent(
val configurations = DependencyConfigurations(project.configurations.names.toList())

override fun functionExtractors(): List<FunctionExtractor> = listOf(
DependencyFunctionsExtractor(configurations)
DependencyFunctionsExtractor(configurations),
ImplicitDependencyCollectorFunctionExtractor(configurations)
)

override fun runtimeFunctionResolvers(): List<RuntimeFunctionResolver> = listOf(
RuntimeDependencyFunctionResolver(configurations)
RuntimeDependencyFunctionResolver(configurations),
ImplicitDependencyCollectorFunctionResolver(configurations)
)
}

Expand Down Expand Up @@ -89,6 +98,27 @@ class DependencyFunctionsExtractor(val configurations: DependencyConfigurations)
}


private
class ImplicitDependencyCollectorFunctionExtractor(val configurations: DependencyConfigurations) : FunctionExtractor {
override fun memberFunctions(kClass: KClass<*>, preIndex: DataSchemaBuilder.PreIndex): Iterable<SchemaMemberFunction> = kClass.memberFunctions
.filter { function -> hasDependencyCollectorGetterSignature(function) }
.map { function -> function.name.removePrefix("get").replaceFirstChar { it.lowercase(Locale.getDefault()) } }
.filter { confName -> confName in configurations.configurationNames }
.map { confName ->
DataMemberFunction(
kClass.toDataTypeRef(),
confName,
listOf(DataParameter("dependency", String::class.toDataTypeRef(), false, ParameterSemantics.Unknown)),
false,
FunctionSemantics.AddAndConfigure(kClass.toDataTypeRef(), NOT_ALLOWED)
)
}

override fun constructors(kClass: KClass<*>, preIndex: DataSchemaBuilder.PreIndex): Iterable<DataConstructor> = emptyList()
override fun topLevelFunction(function: KFunction<*>, preIndex: DataSchemaBuilder.PreIndex): DataTopLevelFunction? = null
}


private
class RuntimeDependencyFunctionResolver(configurations: DependencyConfigurations) : RuntimeFunctionResolver {
private
Expand All @@ -103,6 +133,48 @@ class RuntimeDependencyFunctionResolver(configurations: DependencyConfigurations
}
})
}

return RuntimeFunctionResolver.Resolution.Unresolved
}
}


private
class ImplicitDependencyCollectorFunctionResolver(configurations: DependencyConfigurations) : RuntimeFunctionResolver {
private
val configurationNames = configurations.configurationNames.toSet()

override fun resolve(receiverClass: KClass<*>, name: String, parameterValueBinding: ParameterValueBinding): RuntimeFunctionResolver.Resolution {
if (name in configurationNames) {
val getterFunction = getDependencyCollectorGetter(receiverClass, name)
if (getterFunction != null) {
return RuntimeFunctionResolver.Resolution.Resolved(object : RestrictedRuntimeFunction {
override fun callBy(receiver: Any, binding: Map<DataParameter, Any?>, hasLambda: Boolean): RestrictedRuntimeFunction.InvocationResult {
val dependencyNotation = binding.values.single().toString()
val collector: DependencyCollector = getterFunction.call(receiver) as DependencyCollector
collector.add(dependencyNotation)
return RestrictedRuntimeFunction.InvocationResult(Unit, null)
}
})
}
}
return RuntimeFunctionResolver.Resolution.Unresolved
}

private
fun getDependencyCollectorGetter(receiverClass: KClass<*>, configurationName: String): KFunction<*>? = receiverClass.functions
.filter { hasDependencyCollectorGetterSignature(it) }
.firstOrNull { function -> function.name == "get${configurationName.replaceFirstChar { it.uppercase(Locale.getDefault()) }}" }
}


@OptIn(ExperimentalStdlibApi::class) // For javaType
private
fun hasDependencyCollectorGetterSignature(function: KFunction<*>): Boolean {
val returnType: Type = try {
function.returnType.javaType
} catch (e: Throwable) { // Sometimes reflection fails with an error when the return type is unusual, if it failed then it's not a getter of interest
Void::class.java
}
return function.name.startsWith("get") && returnType == DependencyCollector::class.java && function.parameters.size == 1
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class AllDistributionIntegrationSpec extends DistributionIntegrationSpec {

@Override
int getMaxDistributionSizeBytes() {
return 218 * 1024 * 1024
return 220 * 1024 * 1024
}

@Requires(UnitTestPreconditions.StableGroovy) // cannot link to public javadocs of Groovy snapshots like https://docs.groovy-lang.org/docs/groovy-4.0.5-SNAPSHOT/html/gapi/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class DistributionIntegritySpec extends DistributionIntegrationSpec {

@Override
int getMaxDistributionSizeBytes() {
return 130 * 1024 * 1024
return 131 * 1024 * 1024
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class DocsDistributionIntegrationSpec extends DistributionIntegrationSpec {

@Override
int getMaxDistributionSizeBytes() {
return 82 * 1024 * 1024
return 84 * 1024 * 1024
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class SrcDistributionIntegrationSpec extends DistributionIntegrationSpec {

@Override
int getMaxDistributionSizeBytes() {
return 60 * 1024 * 1024
return 62 * 1024 * 1024
}

@Override
Expand Down

0 comments on commit cebf576

Please sign in to comment.