Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enforce lock constraints using shared platform #557

Merged
merged 15 commits into from
Aug 27, 2020
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog/@unreleased/pr-557.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type: improvement
improvement:
description: Manage lock file constraints using a single gradle "platform", reducing
the number of constraints that have to be created.
links:
- https://github.com/palantir/gradle-consistent-versions/pull/557
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.gradle.api.Project;

public class ConsistentVersionsPlugin implements Plugin<Project> {
static final String CONSISTENT_VERSIONS_USAGE = "consistent-versions-usage";

@Override
public final void apply(Project project) {
if (!project.getRootProject().equals(project)) {
Expand Down
63 changes: 55 additions & 8 deletions src/main/java/com/palantir/gradle/versions/VersionsLockPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@
import org.gradle.api.artifacts.result.ResolvedComponentResult;
import org.gradle.api.artifacts.result.UnresolvedDependencyResult;
import org.gradle.api.attributes.Attribute;
import org.gradle.api.attributes.AttributeCompatibilityRule;
import org.gradle.api.attributes.AttributesSchema;
import org.gradle.api.attributes.CompatibilityCheckDetails;
import org.gradle.api.attributes.Usage;
import org.gradle.api.invocation.Gradle;
import org.gradle.api.logging.Logger;
Expand Down Expand Up @@ -115,6 +117,7 @@ public class VersionsLockPlugin implements Plugin<Project> {

private static final Attribute<GcvUsage> GCV_USAGE_ATTRIBUTE =
Attribute.of("com.palantir.consistent-versions.usage", GcvUsage.class);
private static final String GCV_LOCKS_CAPABILITY = "gcv:locks:0";

public enum GcvUsage implements Named {
/**
Expand Down Expand Up @@ -180,7 +183,7 @@ public String getName() {
@Inject
public VersionsLockPlugin(Gradle gradle, ObjectFactory objectFactory) {
showStacktrace = gradle.getStartParameter().getShowStacktrace();
internalUsage = objectFactory.named(Usage.class, "consistent-versions-usage");
internalUsage = objectFactory.named(Usage.class, ConsistentVersionsPlugin.CONSISTENT_VERSIONS_USAGE);
}

static Path getRootLockFile(Project project) {
Expand All @@ -196,6 +199,9 @@ public final void apply(Project project) {
AttributesSchema attributesSchema = p.getDependencies().getAttributesSchema();
attributesSchema.attribute(GCV_SCOPE_ATTRIBUTE);
attributesSchema.attribute(GCV_USAGE_ATTRIBUTE);
attributesSchema.attribute(Usage.USAGE_ATTRIBUTE, strategy -> {
strategy.getCompatibilityRules().add(EverythingIsCompatibleWithConsistentVersionsUsage.class);
});
});

Configuration unifiedClasspath = project.getConfigurations()
Expand All @@ -219,6 +225,23 @@ public final void apply(Project project) {
// (but that's internal)
project.getPluginManager().apply("java-base");

// Create "platform" configuration in root project, which will hold the strictConstraints
NamedDomainObjectProvider<Configuration> gcvLocksConfiguration = project.getConfigurations()
.register("gcvLocks", conf -> {
conf.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, internalUsage);
conf.getOutgoing().capability(GCV_LOCKS_CAPABILITY);
conf.setCanBeResolved(false);
conf.setVisible(false);
});

ProjectDependency locksDependency =
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we create a self dependency?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's not actually "self" but pointing to a particular configuration (indirectly).
This is to pick up the locks configuration which is defined in the root project.

(ProjectDependency) project.getDependencies().create(project);
locksDependency.capabilities(moduleDependencyCapabilitiesHandler ->
Copy link
Contributor

Choose a reason for hiding this comment

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

is this necessary given that we already have configured the configuration?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes - we will resolve multiple variants of the same project (root project) so if they don't have different (i.e. explicit and different) capabilities, they will conflict.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh right and you can't depend on an explicit capability just with attributes, you need to specify the capability.

If anything, we might not need to specify the attribute directly, but it's a bit more clear this way I think.

moduleDependencyCapabilitiesHandler.requireCapabilities(GCV_LOCKS_CAPABILITY));
locksDependency.attributes(attrs -> {
attrs.attribute(Usage.USAGE_ATTRIBUTE, internalUsage);
});

// afterEvaluate is necessary to ensure all projects' dependencies have been configured, because we
// need to copy them eagerly before we add the constraints from the lock file.
//
Expand Down Expand Up @@ -265,7 +288,14 @@ public final void apply(Project project) {
}
}

configureAllProjectsUsingConstraints(project, rootLockfile, lockedConfigurations);
// Wire up the locks from the lock file into the strict locks platform.
gcvLocksConfiguration.configure(conf -> {
conf.getDependencyConstraints()
.addAll(constructConstraintsFromLockFile(
rootLockfile, project.getDependencies().getConstraints()));
});

configureAllProjectsUsingConstraints(project, rootLockfile, lockedConfigurations, locksDependency);
});

TaskProvider<?> verifyLocks = project.getTasks().register("verifyLocks", VerifyLocksTask.class, task -> {
Expand All @@ -281,6 +311,20 @@ public final void apply(Project project) {
});
}

static class EverythingIsCompatibleWithConsistentVersionsUsage implements AttributeCompatibilityRule<Usage> {
@Override
public void execute(CompatibilityCheckDetails<Usage> details) {
if (ConsistentVersionsPlugin.CONSISTENT_VERSIONS_USAGE.equals(
details.getProducerValue().getName())
// This shouldn't be necessary, because we never resolve configurations with this usage.
// However, 5.3 tests fail without it
|| ConsistentVersionsPlugin.CONSISTENT_VERSIONS_USAGE.equals(
details.getConsumerValue().getName())) {
details.compatible();
}
}
}

static boolean isIgnoreLockFile(Project project) {
return project.hasProperty("ignoreLockFile");
}
Expand Down Expand Up @@ -795,18 +839,21 @@ private String formatUnresolvedDependencyResult(UnresolvedDependencyResult resul
}

private static void configureAllProjectsUsingConstraints(
Project rootProject, Path gradleLockfile, Map<Project, LockedConfigurations> lockedConfigurations) {
List<DependencyConstraint> strictConstraints = constructConstraintsFromLockFile(
gradleLockfile, rootProject.getDependencies().getConstraints());
Project rootProject,
Path gradleLockfile,
Map<Project, LockedConfigurations> lockedConfigurations,
ProjectDependency locksDependency) {

List<DependencyConstraint> publishableConstraints = constructPublishableConstraintsFromLockFile(
gradleLockfile, rootProject.getDependencies().getConstraints());

rootProject.allprojects(subproject -> configureUsingConstraints(
subproject, strictConstraints, publishableConstraints, lockedConfigurations.get(subproject)));
subproject, locksDependency, publishableConstraints, lockedConfigurations.get(subproject)));
}

private static void configureUsingConstraints(
Project subproject,
List<DependencyConstraint> lockConstraints,
ProjectDependency locksDependency,
List<DependencyConstraint> publishableConstraints,
LockedConfigurations lockedConfigurations) {
Configuration locksConfiguration = subproject
Expand All @@ -815,7 +862,7 @@ private static void configureUsingConstraints(
locksConf.setVisible(false);
locksConf.setCanBeConsumed(false);
locksConf.setCanBeResolved(false);
lockConstraints.forEach(locksConf.getDependencyConstraints()::add);
locksConf.getDependencies().add(locksDependency);
});

Set<Configuration> configurationsToLock = lockedConfigurations.allConfigurations();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
import org.gradle.api.artifacts.DependencySet;
import org.gradle.api.artifacts.ExternalDependency;
import org.gradle.api.artifacts.ModuleDependency;
import org.gradle.api.artifacts.ProjectDependency;
import org.gradle.api.artifacts.dsl.DependencyConstraintHandler;
import org.gradle.api.attributes.Usage;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.JavaPlugin;
Expand All @@ -58,6 +60,17 @@ public class VersionsPropsPlugin implements Plugin<Project> {
@Override
public final void apply(Project project) {
checkPreconditions();

// Shared across root project / other project
// This must be usable during VersionsLockPlugin's resolution of unifiedClasspath, so the usage
// must be 'compatible with' (or the same as) the one for the VersionsLockPlugin's own configurations.
Usage gcvVersionsPropsUsage =
project.getObjects().named(Usage.class, ConsistentVersionsPlugin.CONSISTENT_VERSIONS_USAGE);
String gcvVersionsPropsCapability = "gcv:versions-props:0";

VersionsProps versionsProps = loadVersionsProps(
project.getRootProject().file("versions.props").toPath());

if (project.getRootProject().equals(project)) {
applyToRootProject(project);

Expand All @@ -71,29 +84,39 @@ public final void apply(Project project) {
.set(project.getLayout().getProjectDirectory().file("versions.props"));
});
project.getTasks().named("check").configure(task -> task.dependsOn(checkNoUnusedConstraints));

// Create "platform" configuration in root project, which will hold the versions props constraints
project.getConfigurations().register("gcvVersionsPropsConstraints", conf -> {
conf.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, gcvVersionsPropsUsage);
conf.getOutgoing().capability(gcvVersionsPropsCapability);
conf.setCanBeResolved(false);
conf.setCanBeConsumed(true);
conf.setVisible(false);

// Note: don't add constraints to the ConstraintHandler, only call `create` / `platform` on it.
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean here? could you explain why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was there before and mostly just meant the function accepting the ConstraintHandler doesn't call add on it, only create.
It's a bit confusing though so I'm gonna instead make up an interface that can only create, and pass that in.

addVersionsPropsConstraints(project.getDependencies().getConstraints(), conf, versionsProps);
});
}

VersionRecommendationsExtension extension =
project.getRootProject().getExtensions().getByType(VersionRecommendationsExtension.class);

VersionsProps versionsProps = loadVersionsProps(
project.getRootProject().file("versions.props").toPath());

NamedDomainObjectProvider<Configuration> rootConfiguration = project.getConfigurations()
.register(ROOT_CONFIGURATION_NAME, conf -> {
conf.setCanBeResolved(false);
conf.setCanBeConsumed(false);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

not setting this was an oversight, now fixed

conf.setVisible(false);

// Wire in the constraints from the main configuration.
conf.getDependencies()
.add(createDepOnRootConstraintsConfiguration(
project, gcvVersionsPropsUsage, gcvVersionsPropsCapability));
});

project.getConfigurations().configureEach(conf -> {
setupConfiguration(project, extension, rootConfiguration.get(), versionsProps, conf);
});

// Note: don't add constraints to this, only call `create` / `platform` on it.
DependencyConstraintHandler constraintHandler =
project.getDependencies().getConstraints();
rootConfiguration.configure(conf -> addVersionsPropsConstraints(constraintHandler, conf, versionsProps));

log.info("Configuring rules to assign *-constraints to platforms in {}", project);
project.getDependencies()
.getComponents()
Expand All @@ -103,10 +126,20 @@ public final void apply(Project project) {
configureResolvedVersionsWithVersionMapping(project);
}

private static ProjectDependency createDepOnRootConstraintsConfiguration(
Project project, Usage usage, String capability) {
ProjectDependency projectDep =
((ProjectDependency) project.getDependencies().create(project.getRootProject()));
projectDep.capabilities(capabilities -> capabilities.requireCapability(capability));
projectDep.attributes(attrs -> attrs.attribute(Usage.USAGE_ATTRIBUTE, usage));
return projectDep;
}

private static void applyToRootProject(Project project) {
project.getPluginManager().apply(LifecycleBasePlugin.class);
project.getExtensions()
.create(VersionRecommendationsExtension.EXTENSION, VersionRecommendationsExtension.class, project);

project.subprojects(subproject -> subproject.getPluginManager().apply(VersionsPropsPlugin.class));
}

Expand Down