diff --git a/src/main/java/com/palantir/gradle/versions/VersionsLockPlugin.java b/src/main/java/com/palantir/gradle/versions/VersionsLockPlugin.java index 3c122ae62..8b6c137ae 100644 --- a/src/main/java/com/palantir/gradle/versions/VersionsLockPlugin.java +++ b/src/main/java/com/palantir/gradle/versions/VersionsLockPlugin.java @@ -73,6 +73,7 @@ import org.gradle.api.artifacts.DependencySet; import org.gradle.api.artifacts.ExternalModuleDependency; import org.gradle.api.artifacts.ModuleIdentifier; +import org.gradle.api.artifacts.ModuleVersionIdentifier; import org.gradle.api.artifacts.ProjectDependency; import org.gradle.api.artifacts.VersionConstraint; import org.gradle.api.artifacts.component.ComponentSelector; @@ -133,6 +134,7 @@ public abstract class VersionsLockPlugin implements Plugin { new TaskNameMatcher(WRITE_VERSIONS_LOCKS_TASK); private static final String PUBLISH_LOCAL_CONSTRAINTS_PROPERTY = "com.palantir.gradle.versions.publishLocalConstraints"; + private static final String FILTER_LOCK_FILE_CONSTRAINTS = "com.palantir.gradle.versions.filterLockFileConstraints"; private static final String GCV_PUBLISH_CONSTRAINTS_CONFIGURATION_NAME = "gcvPublishConstraints"; public enum GcvUsage implements Named { @@ -954,22 +956,18 @@ private static void configureAllProjectsUsingConstraints( Map lockedConfigurations, ProjectDependency locksDependency) { - List publishableConstraints = constructPublishableConstraintsFromLockFile( + List lockFileConstraints = constructConstraintsFromLockfile( rootProject, gradleLockfile, rootProject.getDependencies().getConstraints()::create); rootProject.allprojects(subproject -> { // Avoid including the current project as a constraint -- it must already be present to provide constraints List localProjectConstraints = constructPublishableConstraintsFromLocalProjects( subproject, rootProject.getDependencies().getConstraints()::create); - ImmutableList publishableConstraintsForSubproject = - ImmutableList.builder() - .addAll(localProjectConstraints) - .addAll(publishableConstraints) - .build(); configureUsingConstraints( subproject, locksDependency, - publishableConstraintsForSubproject, + lockFileConstraints, + localProjectConstraints, lockedConfigurations.get(subproject)); }); } @@ -977,7 +975,8 @@ private static void configureAllProjectsUsingConstraints( private static void configureUsingConstraints( Project subproject, ProjectDependency locksDependency, - List publishableConstraints, + List lockFileConstraints, + List localProjectConstraints, LockedConfigurations lockedConfigurations) { subproject.getConfigurations().named(LOCK_CONSTRAINTS_CONFIGURATION_NAME, conf -> conf.getDependencies() .add(locksDependency)); @@ -986,11 +985,46 @@ private static void configureUsingConstraints( log.info("Configuring locks for {}. Locked configurations: {}", subproject.getPath(), configurationsToLock); configurationsToLock.forEach(VersionsLockPlugin::ensureNoFailOnVersionConflict); - subproject.getConfigurations().named(GCV_PUBLISH_CONSTRAINTS_CONFIGURATION_NAME, conf -> { - conf.getDependencyConstraints().addAll(publishableConstraints); + subproject.getPluginManager().withPlugin("java", _plugin -> { + subproject.getConfigurations().named(GCV_PUBLISH_CONSTRAINTS_CONFIGURATION_NAME, conf -> { + conf.getDependencyConstraints().addAll(localProjectConstraints); + conf.getDependencyConstraints() + .addAllLater(maybeFilterConstraintsByUsage(subproject, lockFileConstraints)); + }); + }); + } + + private static Provider> maybeFilterConstraintsByUsage( + Project project, List constraints) { + + NamedDomainObjectProvider compileClasspath = + project.getConfigurations().named(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME); + NamedDomainObjectProvider runtimeClasspath = + project.getConfigurations().named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + + return compileClasspath.zip(runtimeClasspath, (compile, runtime) -> { + if (!filterLockFileConstraints(project)) { + return constraints; + } + + Set usedModules = Stream.of(compile, runtime) + .flatMap(config -> config.getIncoming().getResolutionResult().getAllComponents().stream()) + .map(ResolvedComponentResult::getModuleVersion) + .filter(Objects::nonNull) + .map(ModuleVersionIdentifier::getModule) + .collect(Collectors.toSet()); + + return constraints.stream() + .filter(constraint -> usedModules.contains(constraint.getModule())) + .collect(Collectors.toList()); }); } + private static boolean filterLockFileConstraints(Project project) { + return project.hasProperty(FILTER_LOCK_FILE_CONSTRAINTS) + && "true".equals(project.property(FILTER_LOCK_FILE_CONSTRAINTS)); + } + private static LockedConfigurations computeConfigurationsToLock(Project project, VersionsLockExtension ext) { Preconditions.checkState( project.getState().getExecuted(), @@ -1077,7 +1111,7 @@ private static List constructConstraintsFromLockFile( .collect(Collectors.toList()); } - private static List constructPublishableConstraintsFromLockFile( + private static List constructConstraintsFromLockfile( Project rootProject, Path gradleLockfile, DependencyConstraintCreator constraintCreator) { LockState lockState = new ConflictSafeLockFile(gradleLockfile).readLocks(); // We only publish the production locks. diff --git a/src/test/groovy/com/palantir/gradle/versions/VersionsLockPluginIntegrationSpec.groovy b/src/test/groovy/com/palantir/gradle/versions/VersionsLockPluginIntegrationSpec.groovy index 8744d10c4..e2598d1da 100644 --- a/src/test/groovy/com/palantir/gradle/versions/VersionsLockPluginIntegrationSpec.groovy +++ b/src/test/groovy/com/palantir/gradle/versions/VersionsLockPluginIntegrationSpec.groovy @@ -856,10 +856,118 @@ class VersionsLockPluginIntegrationSpec extends IntegrationSpec { gradleVersionNumber << GRADLE_VERSIONS } - def "#gradleVersionNumber: published constraints are derived from lock file (with local constraints)"() { + def "#gradleVersionNumber: published constraints are derived from lock file (with local constraints and with filtering)"() { setup: // Test with local constraints enabled - file('gradle.properties') << 'com.palantir.gradle.versions.publishLocalConstraints = true' + file('gradle.properties') << ''' + com.palantir.gradle.versions.publishLocalConstraints = true + com.palantir.gradle.versions.filterLockFileConstraints = true + ''' + gradleVersion = gradleVersionNumber + + buildFile << """ + allprojects { + apply plugin: 'java' + } + """.stripIndent(true) + + String publish = """ + apply plugin: 'maven-publish' + group = 'com.palantir.published-constraints' + version = '1.2.3' + publishing.publications { + maven(MavenPublication) { + from components.java + } + } + """.stripIndent(true) + + addSubproject('foo', """ + $publish + dependencies { + implementation 'ch.qos.logback:logback-classic:1.2.3' + } + """.stripIndent(true)) + + addSubproject('bar', """ + $publish + dependencies { + implementation 'junit:junit:4.10' + } + """.stripIndent(true)) + + if (GradleVersion.version(gradleVersionNumber) < GradleVersion.version("6.0")) { + settingsFile << """ + enableFeaturePreview('GRADLE_METADATA') + """.stripIndent(true) + } + + runTasks('--write-locks') + + when: + runTasks('generatePomFileForMavenPublication', 'generateMetadataFileForMavenPublication') + + def junitDep = new MetadataFile.Dependency( + group: 'junit', + module: 'junit', + version: [requires: '4.10']) + def logbackDep = new MetadataFile.Dependency( + group: 'ch.qos.logback', + module: 'logback-classic', + version: [requires: '1.2.3']) + def slf4jDep = new MetadataFile.Dependency( + group: 'org.slf4j', + module: 'slf4j-api', + version: [requires: '1.7.25']) + def fooDep = new MetadataFile.Dependency( + group: 'com.palantir.published-constraints', + module: 'foo', + version: [requires: '1.2.3']) + def barDep = new MetadataFile.Dependency( + group: 'com.palantir.published-constraints', + module: 'bar', + version: [requires: '1.2.3']) + + then: "foo's metadata file has the right dependency constraints" + def fooMetadataFilename = new File(projectDir, "foo/build/publications/maven/module.json") + def fooMetadata = new ObjectMapper().readValue(fooMetadataFilename, MetadataFile) + + fooMetadata.variants == [ + new MetadataFile.Variant( + name: 'runtimeElements', + dependencies: [logbackDep], + dependencyConstraints: [barDep, logbackDep, slf4jDep]), + new MetadataFile.Variant( + name: 'apiElements', + dependencies: null, + dependencyConstraints: [barDep, logbackDep, slf4jDep]) + ] as Set + + and: "bar's metadata file has the right dependency constraints" + def barMetadataFilename = new File(projectDir, "bar/build/publications/maven/module.json") + def barMetadata = new ObjectMapper().readValue(barMetadataFilename, MetadataFile) + + barMetadata.variants == [ + new MetadataFile.Variant( + name: 'runtimeElements', + dependencies: [junitDep], + dependencyConstraints: [fooDep, junitDep]), + new MetadataFile.Variant( + name: 'apiElements', + dependencies: null, + dependencyConstraints: [fooDep, junitDep]), + ] as Set + + where: + gradleVersionNumber << GRADLE_VERSIONS + } + + def "#gradleVersionNumber: published constraints are derived from lock file (with local constraints and without filtering)"() { + setup: + // Test with local constraints enabled + file('gradle.properties') << ''' + com.palantir.gradle.versions.publishLocalConstraints = true + ''' gradleVersion = gradleVersionNumber buildFile << """ @@ -959,7 +1067,102 @@ class VersionsLockPluginIntegrationSpec extends IntegrationSpec { gradleVersionNumber << GRADLE_VERSIONS } - def "#gradleVersionNumber: published constraints are derived from lock file (without local constraints)"() { + def "#gradleVersionNumber: published constraints are derived from lock file (without local constraints and with filtering)"() { + setup: + // Test with filtering of lock file constraints enabled + file('gradle.properties') << 'com.palantir.gradle.versions.filterLockFileConstraints = true' + gradleVersion = gradleVersionNumber + + buildFile << """ + allprojects { + apply plugin: 'java' + } + """.stripIndent(true) + + String publish = """ + apply plugin: 'maven-publish' + group = 'com.palantir.published-constraints' + version = '1.2.3' + publishing.publications { + maven(MavenPublication) { + from components.java + } + } + """.stripIndent(true) + + addSubproject('foo', """ + $publish + dependencies { + implementation 'ch.qos.logback:logback-classic:1.2.3' + } + """.stripIndent(true)) + + addSubproject('bar', """ + $publish + dependencies { + implementation 'junit:junit:4.10' + } + """.stripIndent(true)) + + if (GradleVersion.version(gradleVersionNumber) < GradleVersion.version("6.0")) { + settingsFile << """ + enableFeaturePreview('GRADLE_METADATA') + """.stripIndent(true) + } + + runTasks('--write-locks') + + when: + runTasks('generatePomFileForMavenPublication', 'generateMetadataFileForMavenPublication') + + def junitDep = new MetadataFile.Dependency( + group: 'junit', + module: 'junit', + version: [requires: '4.10']) + def logbackDep = new MetadataFile.Dependency( + group: 'ch.qos.logback', + module: 'logback-classic', + version: [requires: '1.2.3']) + def slf4jDep = new MetadataFile.Dependency( + group: 'org.slf4j', + module: 'slf4j-api', + version: [requires: '1.7.25']) + + then: "foo's metadata file has the right dependency constraints" + def fooMetadataFilename = new File(projectDir, "foo/build/publications/maven/module.json") + def fooMetadata = new ObjectMapper().readValue(fooMetadataFilename, MetadataFile) + + fooMetadata.variants == [ + new MetadataFile.Variant( + name: 'runtimeElements', + dependencies: [logbackDep], + dependencyConstraints: [logbackDep, slf4jDep]), + new MetadataFile.Variant( + name: 'apiElements', + dependencies: null, + dependencyConstraints: [logbackDep, slf4jDep]) + ] as Set + + and: "bar's metadata file has the right dependency constraints" + def barMetadataFilename = new File(projectDir, "bar/build/publications/maven/module.json") + def barMetadata = new ObjectMapper().readValue(barMetadataFilename, MetadataFile) + + barMetadata.variants == [ + new MetadataFile.Variant( + name: 'runtimeElements', + dependencies: [junitDep], + dependencyConstraints: [junitDep]), + new MetadataFile.Variant( + name: 'apiElements', + dependencies: null, + dependencyConstraints: [junitDep]), + ] as Set + + where: + gradleVersionNumber << GRADLE_VERSIONS + } + + def "#gradleVersionNumber: published constraints are derived from lock file (without local constraints and without filtering)"() { setup: gradleVersion = gradleVersionNumber