diff --git a/platforms/core-configuration/declarative-dsl-provider/src/integTest/groovy/org/gradle/internal/declarativedsl/project/DeclarativeDSLCustomDependenciesExtensionsSpec.groovy b/platforms/core-configuration/declarative-dsl-provider/src/integTest/groovy/org/gradle/internal/declarativedsl/project/DeclarativeDSLCustomDependenciesExtensionsSpec.groovy new file mode 100644 index 000000000000..e0d13d387ce8 --- /dev/null +++ b/platforms/core-configuration/declarative-dsl-provider/src/integTest/groovy/org/gradle/internal/declarativedsl/project/DeclarativeDSLCustomDependenciesExtensionsSpec.groovy @@ -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 { + @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 { + @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 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 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' + """ + } +} diff --git a/platforms/core-configuration/declarative-dsl-provider/src/main/kotlin/org/gradle/internal/declarativedsl/project/dependencyConfigurationSchema.kt b/platforms/core-configuration/declarative-dsl-provider/src/main/kotlin/org/gradle/internal/declarativedsl/project/dependencyConfigurationSchema.kt index 2db99f5129f2..e0a310370f50 100644 --- a/platforms/core-configuration/declarative-dsl-provider/src/main/kotlin/org/gradle/internal/declarativedsl/project/dependencyConfigurationSchema.kt +++ b/platforms/core-configuration/declarative-dsl-provider/src/main/kotlin/org/gradle/internal/declarativedsl/project/dependencyConfigurationSchema.kt @@ -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. */ @@ -54,11 +61,13 @@ class DependencyConfigurationsComponent( val configurations = DependencyConfigurations(project.configurations.names.toList()) override fun functionExtractors(): List = listOf( - DependencyFunctionsExtractor(configurations) + DependencyFunctionsExtractor(configurations), + ImplicitDependencyCollectorFunctionExtractor(configurations) ) override fun runtimeFunctionResolvers(): List = listOf( - RuntimeDependencyFunctionResolver(configurations) + RuntimeDependencyFunctionResolver(configurations), + ImplicitDependencyCollectorFunctionResolver(configurations) ) } @@ -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 = 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 = emptyList() + override fun topLevelFunction(function: KFunction<*>, preIndex: DataSchemaBuilder.PreIndex): DataTopLevelFunction? = null +} + + private class RuntimeDependencyFunctionResolver(configurations: DependencyConfigurations) : RuntimeFunctionResolver { private @@ -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, 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 +} diff --git a/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/AllDistributionIntegrationSpec.groovy b/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/AllDistributionIntegrationSpec.groovy index 59d7ab9fb583..f5c284ee8c4d 100644 --- a/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/AllDistributionIntegrationSpec.groovy +++ b/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/AllDistributionIntegrationSpec.groovy @@ -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/ diff --git a/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/DistributionIntegritySpec.groovy b/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/DistributionIntegritySpec.groovy index 0742c41226e9..4a8cd30ff94c 100644 --- a/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/DistributionIntegritySpec.groovy +++ b/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/DistributionIntegritySpec.groovy @@ -35,7 +35,7 @@ class DistributionIntegritySpec extends DistributionIntegrationSpec { @Override int getMaxDistributionSizeBytes() { - return 130 * 1024 * 1024 + return 131 * 1024 * 1024 } /** diff --git a/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/DocsDistributionIntegrationSpec.groovy b/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/DocsDistributionIntegrationSpec.groovy index 7028ee26c9cc..db1f178d45a2 100644 --- a/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/DocsDistributionIntegrationSpec.groovy +++ b/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/DocsDistributionIntegrationSpec.groovy @@ -32,7 +32,7 @@ class DocsDistributionIntegrationSpec extends DistributionIntegrationSpec { @Override int getMaxDistributionSizeBytes() { - return 82 * 1024 * 1024 + return 84 * 1024 * 1024 } @Override diff --git a/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/SrcDistributionIntegrationSpec.groovy b/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/SrcDistributionIntegrationSpec.groovy index baf96ee18e93..b3a13301b4fd 100644 --- a/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/SrcDistributionIntegrationSpec.groovy +++ b/subprojects/distributions-integ-tests/src/integTest/groovy/org/gradle/SrcDistributionIntegrationSpec.groovy @@ -38,7 +38,7 @@ class SrcDistributionIntegrationSpec extends DistributionIntegrationSpec { @Override int getMaxDistributionSizeBytes() { - return 60 * 1024 * 1024 + return 62 * 1024 * 1024 } @Override