Skip to content
Open
76 changes: 57 additions & 19 deletions src/main/java/com/palantir/gradle/versions/VersionsLockPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,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;
Expand All @@ -96,6 +97,7 @@
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.publish.Publication;
import org.gradle.api.publish.PublishingExtension;
import org.gradle.api.publish.ivy.IvyPublication;
Expand Down Expand Up @@ -133,6 +135,7 @@ public abstract class VersionsLockPlugin implements Plugin<Project> {
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";

public enum GcvUsage implements Named {
/**
Expand Down Expand Up @@ -917,31 +920,29 @@ private static void configureAllProjectsUsingConstraints(
Map<Project, LockedConfigurations> lockedConfigurations,
ProjectDependency locksDependency) {

List<DependencyConstraint> publishableConstraints = constructPublishableConstraintsFromLockFile(
List<DependencyConstraint> lockFileConstraints = constructPublishableConstraintsFromLockFile(
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<DependencyConstraint> localProjectConstraints = constructPublishableConstraintsFromLocalProjects(
subproject, rootProject.getDependencies().getConstraints()::create);
ImmutableList<DependencyConstraint> publishableConstraintsForSubproject =
ImmutableList.<DependencyConstraint>builder()
.addAll(localProjectConstraints)
.addAll(publishableConstraints)
.build();
configureUsingConstraints(
subproject,
locksDependency,
publishableConstraintsForSubproject,
lockFileConstraints,
localProjectConstraints,
lockedConfigurations.get(subproject));
});
}

private static void configureUsingConstraints(
Project subproject,
ProjectDependency locksDependency,
List<DependencyConstraint> publishableConstraints,
List<DependencyConstraint> lockFileConstraints,
List<DependencyConstraint> localProjectConstraints,
LockedConfigurations lockedConfigurations) {

@SuppressWarnings("for-rollout:ConfigurationAvoidanceRegistration")
Configuration locksConfiguration = subproject
.getConfigurations()
Expand All @@ -959,29 +960,61 @@ private static void configureUsingConstraints(
VersionsLockPlugin.ensureNoFailOnVersionConflict(conf);
});

NamedDomainObjectProvider<Configuration> publishConstraints = subproject
.getConfigurations()
.register("gcvPublishConstraints", conf -> {
conf.setDescription("Publishable constraints from the GCV versions.lock file");
conf.setCanBeResolved(false);
conf.setCanBeConsumed(false);
conf.getDependencyConstraints().addAll(publishableConstraints);
});

// Enrich the configurations being published as part of the java component (components.java)
// with constraints generated from the lock file.
subproject.getPluginManager().withPlugin("java", _plugin -> {
NamedDomainObjectProvider<Configuration> publishConstraints = subproject
.getConfigurations()
.register("gcvPublishConstraints", conf -> {
conf.setDescription("Publishable constraints from the GCV versions.lock file");
conf.setCanBeResolved(false);
conf.setCanBeConsumed(false);

conf.getDependencyConstraints().addAll(localProjectConstraints);

if (filterLockFileConstraints(subproject)) {
conf.getDependencyConstraints()
.addAllLater(filterConstraintsByUsage(subproject, lockFileConstraints));
} else {
conf.getDependencyConstraints().addAll(lockFileConstraints);
}
});

subproject
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to make the default behaviour not publishing the dep constraints at all?

Copy link

@kelvinou01 kelvinou01 Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep sounds good to me, as the deps dumped into <dependencyManagement> is noise anyways, so not publishing any dep constraints to GMM is equivalent to the status quo. Looking for a second opinion before I commit this though (as this is not my PR)

.getConfigurations()
.named(JavaPlugin.API_ELEMENTS_CONFIGURATION_NAME)
.configure(conf -> conf.extendsFrom(publishConstraints.get()));

subproject
.getConfigurations()
.named(JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME)
.configure(conf -> conf.extendsFrom(publishConstraints.get()));
});
}

private static Provider<Collection<DependencyConstraint>> filterConstraintsByUsage(
Project project, List<DependencyConstraint> constraints) {

NamedDomainObjectProvider<Configuration> compileClasspath =
project.getConfigurations().named(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME);
NamedDomainObjectProvider<Configuration> runtimeClasspath =
project.getConfigurations().named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);

return compileClasspath.zip(runtimeClasspath, (compile, runtime) -> {
Set<ModuleIdentifier> usedModules = new HashSet<>();

Stream.of(compile, runtime).forEach(config -> {
config.getIncoming().getResolutionResult().getAllComponents().stream()
.map(ResolvedComponentResult::getModuleVersion)
.filter(Objects::nonNull)
.map(ModuleVersionIdentifier::getModule)
.forEach(usedModules::add);
});

return constraints.stream()
.filter(c -> usedModules.contains(c.getModule()))
.collect(Collectors.toList());
});
}

private static LockedConfigurations computeConfigurationsToLock(Project project, VersionsLockExtension ext) {
Preconditions.checkState(
project.getState().getExecuted(),
Expand Down Expand Up @@ -1107,6 +1140,11 @@ private static boolean publishLocalConstraints(Project project) {
&& "true".equals(project.property(PUBLISH_LOCAL_CONSTRAINTS_PROPERTY));
}

private static boolean filterLockFileConstraints(Project project) {
return project.hasProperty(FILTER_LOCK_FILE_CONSTRAINTS)
&& "true".equals(project.property(FILTER_LOCK_FILE_CONSTRAINTS));
}

private static boolean isJavaLibrary(Project project) {
if (project.getPluginManager().hasPlugin("nebula.maven-publish")) {
// 'nebula.maven-publish' creates publications lazily which causes inconsistencies based
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -922,10 +922,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 << """
Expand Down Expand Up @@ -1025,7 +1133,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 local 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)
}
Comment on lines +1107 to +1111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is never going to be true, minimum version is gradle 7.6.4

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, this was a copy pasted test

Copy link

@kelvinou01 kelvinou01 Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should i remove all checks for 6.0?


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

Expand Down