diff --git a/gradle-idea-language-injector/build.gradle b/gradle-idea-language-injector/build.gradle index 08c5e33..09474cf 100644 --- a/gradle-idea-language-injector/build.gradle +++ b/gradle-idea-language-injector/build.gradle @@ -18,6 +18,8 @@ dependencies { testImplementation 'com.netflix.nebula:nebula-test' testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' + + gradlePluginForTesting 'com.palantir.gradle.consistentversions:gradle-consistent-versions' } tasks.withType(Test) { diff --git a/gradle-idea-language-injector/src/main/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorProjectPlugin.java b/gradle-idea-language-injector/src/main/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorProjectPlugin.java index 41cee2b..aa26657 100644 --- a/gradle-idea-language-injector/src/main/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorProjectPlugin.java +++ b/gradle-idea-language-injector/src/main/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorProjectPlugin.java @@ -15,48 +15,42 @@ */ package com.palantir.gradle.idealanguageinjector; -import java.io.File; -import java.util.Set; -import java.util.stream.Collectors; +import javax.inject.Inject; import org.gradle.api.Plugin; import org.gradle.api.Project; -import org.gradle.api.artifacts.type.ArtifactTypeDefinition; +import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.attributes.Usage; +import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.tasks.SourceSetContainer; public abstract class IdeaLanguageInjectorProjectPlugin implements Plugin { - static final String OUTGOING_USAGE = "idea-language-injector-jars"; + static final String OUTGOING_USAGE = "idea-language-injector-outgoing"; + + @Inject + protected abstract ObjectFactory getObjects(); + + @Inject + protected abstract SourceSetContainer getSourceSets(); + + @Inject + protected abstract ConfigurationContainer getConfigurations(); @Override public final void apply(Project project) { project.getPlugins().withType(JavaPlugin.class, _javaPlugin -> { - createOutgoingConfiguration(project); - }); - } + getConfigurations().consumable("idea-language-injector-outgoing", conf -> { + conf.attributes(attrs -> { + attrs.attribute(Usage.USAGE_ATTRIBUTE, getObjects().named(Usage.class, OUTGOING_USAGE)); + }); - private static void createOutgoingConfiguration(Project project) { - project.getConfigurations().consumable("idea-language-injector-outgoing", outgoing -> { - outgoing.attributes(attrs -> { - attrs.attribute(Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, OUTGOING_USAGE)); + getSourceSets() + .all(sourceSet -> getConfigurations() + .getByName(sourceSet.getCompileClasspathConfigurationName()) + .getExtendsFrom() + .forEach(conf::extendsFrom)); }); - - outgoing.getOutgoing().artifacts(project.provider(() -> sourceSetArtifacts(project))); }); } - - private static Set sourceSetArtifacts(Project project) { - return project.getExtensions().getByType(SourceSetContainer.class).stream() - .flatMap(sourceSet -> project - .getConfigurations() - .getByName(sourceSet.getCompileClasspathConfigurationName()) - .getIncoming() - .artifactView(view -> view.attributes(attrs -> attrs.attribute( - ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.JAR_TYPE))) - .getFiles() - .getFiles() - .stream()) - .collect(Collectors.toSet()); - } } diff --git a/gradle-idea-language-injector/src/main/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorRootPlugin.java b/gradle-idea-language-injector/src/main/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorRootPlugin.java index e0d69d2..9d421c3 100644 --- a/gradle-idea-language-injector/src/main/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorRootPlugin.java +++ b/gradle-idea-language-injector/src/main/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorRootPlugin.java @@ -21,21 +21,38 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import javax.inject.Inject; import org.gradle.StartParameter; import org.gradle.api.GradleException; import org.gradle.api.NamedDomainObjectProvider; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.artifacts.DependencyScopeConfiguration; +import org.gradle.api.artifacts.dsl.DependencyHandler; import org.gradle.api.artifacts.type.ArtifactTypeDefinition; import org.gradle.api.attributes.Usage; +import org.gradle.api.model.ObjectFactory; import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.provider.ProviderFactory; import org.gradle.api.tasks.TaskProvider; public abstract class IdeaLanguageInjectorRootPlugin implements Plugin { static final String CONVERTED_TO_XML = "idea-language-injector-jars"; + @Inject + protected abstract ObjectFactory getObjects(); + + @Inject + protected abstract ProviderFactory getProviders(); + + @Inject + protected abstract DependencyHandler getDependencies(); + + @Inject + protected abstract ConfigurationContainer getConfigurations(); + @Override public final void apply(Project rootProject) { if (rootProject != rootProject.getRootProject()) { @@ -43,10 +60,11 @@ public final void apply(Project rootProject) { "The com.palantir.idea-language-injector plugin must be applied on the root project"); } - registerTransform(rootProject); + registerTransform(); + registerUsageCompatibilityRule(); NamedDomainObjectProvider subprojectDependencies = - rootProject.getConfigurations().dependencyScope("idea-language-injector-subprojects"); + getConfigurations().dependencyScope("idea-language-injector-subprojects"); // to make this plugin isolated projects compatible instead of applying the project plugin here apply via a // settings plugin @@ -59,31 +77,32 @@ public final void apply(Project rootProject) { subprojectDependencies.configure(subprojectDeps -> { subprojectDeps .getDependencies() - .addAllLater(rootProject.provider(() -> rootProject.getAllprojects().stream() - .map(subproject -> rootProject - .getDependencies() - .project(Map.of( - "path", subproject.getIsolated().getPath()))) - .toList())); + .addAllLater(getProviders() + .provider(() -> rootProject.getAllprojects().stream() + .map(subproject -> getDependencies() + .project(Map.of( + "path", + subproject.getIsolated().getPath()))) + .toList())); }); TaskProvider update = rootProject .getTasks() .register("updateIntelliLangXml", UpdateIntelliLang.class, task -> { task.getArtifactFiles() - .from(rootProject - .getConfigurations() + .from(getConfigurations() .resolvable("collected-idea-language-injector-outgoing", conf -> { conf.extendsFrom(subprojectDependencies.get()); - conf.setTransitive(false); conf.attributes(attrs -> { attrs.attribute( Usage.USAGE_ATTRIBUTE, - rootProject - .getObjects() + getObjects() .named( Usage.class, IdeaLanguageInjectorProjectPlugin.OUTGOING_USAGE)); + attrs.attribute( + ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, + ArtifactTypeDefinition.JAR_TYPE); }); }) .map(resolvable -> resolvable @@ -106,8 +125,16 @@ public final void apply(Project rootProject) { } } - private static void registerTransform(Project rootProject) { - rootProject.getDependencies().registerTransform(AnnotationScanTransform.class, spec -> { + private void registerUsageCompatibilityRule() { + getDependencies() + .getAttributesSchema() + .attribute(Usage.USAGE_ATTRIBUTE) + .getCompatibilityRules() + .add(IdeaLanguageInjectorUsageCompatibilityRule.class); + } + + private void registerTransform() { + getDependencies().registerTransform(AnnotationScanTransform.class, spec -> { spec.getFrom().attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, ArtifactTypeDefinition.JAR_TYPE); spec.getTo().attribute(ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE, CONVERTED_TO_XML); }); diff --git a/gradle-idea-language-injector/src/main/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorUsageCompatibilityRule.java b/gradle-idea-language-injector/src/main/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorUsageCompatibilityRule.java new file mode 100644 index 0000000..f04f42a --- /dev/null +++ b/gradle-idea-language-injector/src/main/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorUsageCompatibilityRule.java @@ -0,0 +1,46 @@ +/* + * (c) Copyright 2025 Palantir Technologies Inc. All rights reserved. + * + * 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 com.palantir.gradle.idealanguageinjector; + +import java.util.Optional; +import org.gradle.api.attributes.AttributeCompatibilityRule; +import org.gradle.api.attributes.CompatibilityCheckDetails; +import org.gradle.api.attributes.Usage; + +/** + * Allows configurations requesting {@link IdeaLanguageInjectorProjectPlugin#USAGE_NAME} usage to resolve + * transitive dependencies that only offer {@link Usage#JAVA_API}. The reverse is not true: consumers + * requesting {@code JAVA_API} will never match variants offering our custom usage. + */ +public abstract class IdeaLanguageInjectorUsageCompatibilityRule implements AttributeCompatibilityRule { + + @Override + public final void execute(CompatibilityCheckDetails details) { + boolean consumerMatches = Optional.ofNullable(details.getConsumerValue()) + .map(Usage::getName) + .map(IdeaLanguageInjectorProjectPlugin.OUTGOING_USAGE::equals) + .orElse(false); + + boolean producerMatches = Optional.ofNullable(details.getProducerValue()) + .map(Usage::getName) + .map(Usage.JAVA_API::equals) + .orElse(false); + + if (consumerMatches && producerMatches) { + details.compatible(); + } + } +} diff --git a/gradle-idea-language-injector/src/test/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorTest.java b/gradle-idea-language-injector/src/test/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorTest.java index 5429d16..4312c35 100644 --- a/gradle-idea-language-injector/src/test/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorTest.java +++ b/gradle-idea-language-injector/src/test/java/com/palantir/gradle/idealanguageinjector/IdeaLanguageInjectorTest.java @@ -330,4 +330,13 @@ void deduplicates_same_library_from_multiple_subprojects( assertThat(actual).as("IntelliLang.xml should match expected").isEqualTo(expected); } + + @Test + void compatible_with_consistent_versions(GradleInvoker gradle, RootProject rootProject) { + rootProject.buildGradle().plugins().add("com.palantir.consistent-versions"); + rootProject.file("versions.props").createEmpty(); + rootProject.file("versions.lock").createEmpty(); + + gradle.withArgs("compileJava").buildsSuccessfully(); + } } diff --git a/gradle/gradle-test-versions.yml b/gradle/gradle-test-versions.yml index 83ef2a9..3f6519d 100644 --- a/gradle/gradle-test-versions.yml +++ b/gradle/gradle-test-versions.yml @@ -1,4 +1,4 @@ major-versions: 8: 8.14.3 - 9: 9.3.1 + 9: 9.4.0 extra-versions: [] diff --git a/versions.lock b/versions.lock index cc55966..2e186c2 100644 --- a/versions.lock +++ b/versions.lock @@ -6,11 +6,11 @@ com.fasterxml.jackson.core:jackson-core:2.21.1 (4 constraints: 8856df02) com.fasterxml.jackson.core:jackson-databind:2.21.1 (4 constraints: 3a49683c) -com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.21.1 (1 constraints: 3805373b) +com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.21.1 (3 constraints: c53bd3d9) -com.fasterxml.jackson.datatype:jackson-datatype-guava:2.21.1 (1 constraints: 3805373b) +com.fasterxml.jackson.datatype:jackson-datatype-guava:2.21.1 (3 constraints: c53bd3d9) -com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.21.1 (1 constraints: 3805373b) +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.21.1 (2 constraints: dc1fb7de) com.fasterxml.woodstox:woodstox-core:7.1.1 (1 constraints: 3d174926) @@ -18,7 +18,7 @@ com.google.errorprone:error_prone_annotations:2.41.0 (1 constraints: 490a3ebf) com.google.guava:failureaccess:1.0.3 (1 constraints: 160ae3b4) -com.google.guava:guava:33.5.0-jre (3 constraints: 224b6a38) +com.google.guava:guava:33.5.0-jre (4 constraints: 7e684aa7) com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava (1 constraints: bd17c918) @@ -42,6 +42,12 @@ cglib:cglib-nodep:3.2.2 (1 constraints: 490ded24) com.netflix.nebula:nebula-test:10.6.2 (2 constraints: d61de72d) +com.palantir.gradle.consistentversions:gradle-consistent-versions:3.14.0 (1 constraints: 3a05413b) + +com.palantir.gradle.failure-reports:gradle-failure-reports-exceptions:1.2.0 (1 constraints: b71b1c46) + +com.palantir.gradle.idea-configuration:gradle-idea-configuration:0.7.0 (1 constraints: bb1b2646) + com.palantir.gradle.plugintesting:configuration-cache-spec:0.64.0 (1 constraints: 3c05433b) com.palantir.gradle.plugintesting:discover-tests-cli:0.64.0 (1 constraints: 3c05433b) @@ -58,6 +64,8 @@ junit:junit:4.13.2 (3 constraints: 052421c6) net.bytebuddy:byte-buddy:1.18.3 (1 constraints: 530b55df) +one.util:streamex:0.8.4 (1 constraints: c01b2d46) + org.apiguardian:apiguardian-api:1.1.2 (7 constraints: cc761de9) org.assertj:assertj-core:3.27.7 (2 constraints: 221f6290) diff --git a/versions.props b/versions.props index 7beadaa..070c8d9 100644 --- a/versions.props +++ b/versions.props @@ -8,3 +8,4 @@ org.junit.platform:* = 6.0.3 com.fasterxml.jackson.*:* = 2.21.1 org.ow2.asm:asm = 9.9.1 org.immutables:value = 2.12.1 +com.palantir.gradle.consistentversions:gradle-consistent-versions = 3.14.0