From cd7e88a13ec8f7c87ac87ae85243d972b322c9cd Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Fri, 12 Apr 2024 10:07:34 +0200 Subject: [PATCH] [MRESOLVER-518] Version selector improvements (#450) Version selector improvements: * introduce strategy how to select "winner" (so far it was cemented "nearest") and optionally perform some enforcements * supplier retains same behaviour as before Big fat note: this is NOT to alter Maven4 behaviour, but to make Resolver 2 more versatile for example for "diagnostic purposes" (while Maven may still benefit from these changes as well, by having some "strict version strategy" mode for example). --- https://issues.apache.org/jira/browse/MRESOLVER-518 --- .../UnsolvableVersionConflictException.java | 20 +- .../GetDependencyHierarchyWithConflicts.java | 143 ++++++++ .../internal/impl/scope/ScopeManagerImpl.java | 4 +- .../supplier/SessionBuilderSupplier.java | 5 +- .../ConfigurableVersionSelector.java | 316 ++++++++++++++++++ .../transformer/NearestVersionSelector.java | 3 + .../ConfigurableVersionSelectorTest.java | 233 +++++++++++++ .../range-major-backtracking.txt | 10 + .../sibling-major-versions.txt | 7 + 9 files changed, 736 insertions(+), 5 deletions(-) create mode 100644 maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchyWithConflicts.java create mode 100644 maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConfigurableVersionSelector.java create mode 100644 maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConfigurableVersionSelectorTest.java create mode 100644 maven-resolver-util/src/test/resources/transformer/version-resolver/range-major-backtracking.txt create mode 100644 maven-resolver-util/src/test/resources/transformer/version-resolver/sibling-major-versions.txt diff --git a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/UnsolvableVersionConflictException.java b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/UnsolvableVersionConflictException.java index 445f9339a..10d26a25a 100644 --- a/maven-resolver-api/src/main/java/org/eclipse/aether/collection/UnsolvableVersionConflictException.java +++ b/maven-resolver-api/src/main/java/org/eclipse/aether/collection/UnsolvableVersionConflictException.java @@ -29,6 +29,8 @@ import org.eclipse.aether.graph.DependencyNode; import org.eclipse.aether.version.VersionConstraint; +import static java.util.Objects.requireNonNull; + /** * Thrown in case of an unsolvable conflict between different version constraints for a dependency. */ @@ -42,9 +44,25 @@ public class UnsolvableVersionConflictException extends RepositoryException { * Creates a new exception with the specified paths to conflicting nodes in the dependency graph. * * @param paths The paths to the dependency nodes that participate in the version conflict, may be {@code null}. + * @deprecated Use {@link #UnsolvableVersionConflictException(String, Collection)} instead. */ + @Deprecated public UnsolvableVersionConflictException(Collection> paths) { - super("Could not resolve version conflict among " + toPaths(paths)); + this("Unsolvable hard constraint combination", paths); + } + + /** + * Creates a new exception with the specified paths to conflicting nodes in the dependency graph. + * + * @param message The strategy that throw the bucket in, must not be {@code null}. Should provide concise message + * why this exception was thrown. + * @param paths The paths to the dependency nodes that participate in the version conflict, may be {@code null}. + * + * @since 2.0.0 + */ + public UnsolvableVersionConflictException( + String message, Collection> paths) { + super(requireNonNull(message, "message") + "; Could not resolve version conflict among " + toPaths(paths)); if (paths == null) { this.paths = Collections.emptyList(); this.versions = Collections.emptyList(); diff --git a/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchyWithConflicts.java b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchyWithConflicts.java new file mode 100644 index 000000000..aacf00bb3 --- /dev/null +++ b/maven-resolver-demos/maven-resolver-demo-snippets/src/main/java/org/apache/maven/resolver/examples/GetDependencyHierarchyWithConflicts.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.maven.resolver.examples; + +import org.apache.maven.resolver.examples.util.Booter; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession.CloseableSession; +import org.eclipse.aether.RepositorySystemSession.SessionBuilder; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.collection.UnsolvableVersionConflictException; +import org.eclipse.aether.resolution.ArtifactDescriptorRequest; +import org.eclipse.aether.resolution.ArtifactDescriptorResult; +import org.eclipse.aether.util.graph.manager.DependencyManagerUtils; +import org.eclipse.aether.util.graph.transformer.ChainedDependencyGraphTransformer; +import org.eclipse.aether.util.graph.transformer.ConfigurableVersionSelector; +import org.eclipse.aether.util.graph.transformer.ConflictResolver; +import org.eclipse.aether.util.graph.transformer.JavaDependencyContextRefiner; +import org.eclipse.aether.util.graph.transformer.JavaScopeDeriver; +import org.eclipse.aether.util.graph.transformer.JavaScopeSelector; +import org.eclipse.aether.util.graph.transformer.SimpleOptionalitySelector; + +/** + * Visualizes the transitive dependencies of an artifact similar to m2e's dependency hierarchy view. + */ +public class GetDependencyHierarchyWithConflicts { + + /** + * Main. + * @param args + * @throws Exception + */ + public static void main(String[] args) throws Exception { + System.out.println("------------------------------------------------------------"); + System.out.println(GetDependencyHierarchyWithConflicts.class.getSimpleName()); + + // incompatible versions: two incompatible versions present in graph + try (RepositorySystem system = Booter.newRepositorySystem(Booter.selectFactory(args))) { + SessionBuilder sessionBuilder = Booter.newRepositorySystemSession(system); + sessionBuilder.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, true); + sessionBuilder.setConfigProperty(DependencyManagerUtils.CONFIG_PROP_VERBOSE, true); + try (CloseableSession session = sessionBuilder + .setDependencyGraphTransformer(new ChainedDependencyGraphTransformer( + new ConflictResolver( + new ConfigurableVersionSelector( + new ConfigurableVersionSelector.MajorVersionConvergence( + new ConfigurableVersionSelector.Nearest())), + new JavaScopeSelector(), + new SimpleOptionalitySelector(), + new JavaScopeDeriver()), + new JavaDependencyContextRefiner())) + .build()) { + Artifact artifact = new DefaultArtifact("org.apache.maven.shared:maven-dependency-tree:3.0.1"); + + ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest(); + descriptorRequest.setArtifact(artifact); + descriptorRequest.setRepositories(Booter.newRepositories(system, session)); + ArtifactDescriptorResult descriptorResult = system.readArtifactDescriptor(session, descriptorRequest); + + CollectRequest collectRequest = new CollectRequest(); + collectRequest.setRootArtifact(descriptorResult.getArtifact()); + collectRequest.setDependencies(descriptorResult.getDependencies()); + collectRequest.setManagedDependencies(descriptorResult.getManagedDependencies()); + collectRequest.setRepositories(descriptorRequest.getRepositories()); + + system.collectDependencies(session, collectRequest); + throw new IllegalStateException("should fail"); + } + } catch (Exception e) { + e.printStackTrace(); + if (e.getCause() instanceof UnsolvableVersionConflictException) { + String cause = e.getCause().getMessage(); + if (!cause.contains( + "Incompatible versions for org.apache.maven:maven-core, incompatible versions: [2.0], all versions [2.0, 3.0.4]")) { + throw new IllegalStateException("should fail due incompatible versions"); + } + } else { + throw new IllegalStateException("should fail due incompatible versions"); + } + } + + // dependency divergence: multiple versions of same GA present in graph + try (RepositorySystem system = Booter.newRepositorySystem(Booter.selectFactory(args))) { + SessionBuilder sessionBuilder = Booter.newRepositorySystemSession(system); + sessionBuilder.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, true); + sessionBuilder.setConfigProperty(DependencyManagerUtils.CONFIG_PROP_VERBOSE, true); + try (CloseableSession session = sessionBuilder + .setDependencyGraphTransformer(new ChainedDependencyGraphTransformer( + new ConflictResolver( + new ConfigurableVersionSelector(new ConfigurableVersionSelector.VersionConvergence( + new ConfigurableVersionSelector.Nearest())), + new JavaScopeSelector(), + new SimpleOptionalitySelector(), + new JavaScopeDeriver()), + new JavaDependencyContextRefiner())) + .build()) { + Artifact artifact = new DefaultArtifact("org.apache.maven.shared:maven-dependency-tree:3.1.0"); + + ArtifactDescriptorRequest descriptorRequest = new ArtifactDescriptorRequest(); + descriptorRequest.setArtifact(artifact); + descriptorRequest.setRepositories(Booter.newRepositories(system, session)); + ArtifactDescriptorResult descriptorResult = system.readArtifactDescriptor(session, descriptorRequest); + + CollectRequest collectRequest = new CollectRequest(); + collectRequest.setRootArtifact(descriptorResult.getArtifact()); + collectRequest.setDependencies(descriptorResult.getDependencies()); + collectRequest.setManagedDependencies(descriptorResult.getManagedDependencies()); + collectRequest.setRepositories(descriptorRequest.getRepositories()); + + system.collectDependencies(session, collectRequest); + throw new IllegalStateException("should fail"); + } + } catch (Exception e) { + e.printStackTrace(); + if (e.getCause() instanceof UnsolvableVersionConflictException) { + String cause = e.getCause().getMessage(); + if (!cause.contains( + "Convergence violated for org.codehaus.plexus:plexus-utils, versions present: [2.1, 1.5.5, 2.0.6]")) { + throw new IllegalStateException("should fail due convergence violation"); + } + } else { + throw new IllegalStateException("should fail due convergence violation"); + } + } + } +} diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/ScopeManagerImpl.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/ScopeManagerImpl.java index 485b7aac3..eec86246c 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/ScopeManagerImpl.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/scope/ScopeManagerImpl.java @@ -49,8 +49,8 @@ import org.eclipse.aether.util.graph.selector.AndDependencySelector; import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector; import org.eclipse.aether.util.graph.transformer.ChainedDependencyGraphTransformer; +import org.eclipse.aether.util.graph.transformer.ConfigurableVersionSelector; import org.eclipse.aether.util.graph.transformer.ConflictResolver; -import org.eclipse.aether.util.graph.transformer.NearestVersionSelector; import org.eclipse.aether.util.graph.transformer.SimpleOptionalitySelector; import org.eclipse.aether.util.graph.visitor.CloningDependencyVisitor; import org.eclipse.aether.util.graph.visitor.FilteringDependencyVisitor; @@ -157,7 +157,7 @@ public DependencySelector getDependencySelector(ResolutionScope resolutionScope) public DependencyGraphTransformer getDependencyGraphTransformer(ResolutionScope resolutionScope) { return new ChainedDependencyGraphTransformer( new ConflictResolver( - new NearestVersionSelector(), new ManagedScopeSelector(this), + new ConfigurableVersionSelector(), new ManagedScopeSelector(this), new SimpleOptionalitySelector(), new ManagedScopeDeriver(this)), new ManagedDependencyContextRefiner(this)); } diff --git a/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/SessionBuilderSupplier.java b/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/SessionBuilderSupplier.java index 89fb5f3ca..882edef62 100644 --- a/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/SessionBuilderSupplier.java +++ b/maven-resolver-supplier-mvn3/src/main/java/org/eclipse/aether/supplier/SessionBuilderSupplier.java @@ -39,11 +39,11 @@ import org.eclipse.aether.util.graph.selector.AndDependencySelector; import org.eclipse.aether.util.graph.selector.ExclusionDependencySelector; import org.eclipse.aether.util.graph.transformer.ChainedDependencyGraphTransformer; +import org.eclipse.aether.util.graph.transformer.ConfigurableVersionSelector; import org.eclipse.aether.util.graph.transformer.ConflictResolver; import org.eclipse.aether.util.graph.transformer.JavaDependencyContextRefiner; import org.eclipse.aether.util.graph.transformer.JavaScopeDeriver; import org.eclipse.aether.util.graph.transformer.JavaScopeSelector; -import org.eclipse.aether.util.graph.transformer.NearestVersionSelector; import org.eclipse.aether.util.graph.transformer.SimpleOptionalitySelector; import org.eclipse.aether.util.graph.traverser.FatArtifactTraverser; import org.eclipse.aether.util.repository.SimpleArtifactDescriptorPolicy; @@ -69,6 +69,7 @@ public SessionBuilderSupplier(RepositorySystem repositorySystem) { } protected void configureSessionBuilder(SessionBuilder session) { + session.setSystemProperties(System.getProperties()); session.setDependencyTraverser(getDependencyTraverser()); session.setDependencyManager(getDependencyManager()); session.setDependencySelector(getDependencySelector()); @@ -95,7 +96,7 @@ protected DependencySelector getDependencySelector() { protected DependencyGraphTransformer getDependencyGraphTransformer() { return new ChainedDependencyGraphTransformer( new ConflictResolver( - new NearestVersionSelector(), new JavaScopeSelector(), + new ConfigurableVersionSelector(), new JavaScopeSelector(), new SimpleOptionalitySelector(), new JavaScopeDeriver()), new JavaDependencyContextRefiner()); } diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConfigurableVersionSelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConfigurableVersionSelector.java new file mode 100644 index 000000000..9ad0e0551 --- /dev/null +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/ConfigurableVersionSelector.java @@ -0,0 +1,316 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.eclipse.aether.util.graph.transformer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.aether.RepositoryException; +import org.eclipse.aether.collection.UnsolvableVersionConflictException; +import org.eclipse.aether.graph.DependencyFilter; +import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictContext; +import org.eclipse.aether.util.graph.transformer.ConflictResolver.ConflictItem; +import org.eclipse.aether.util.graph.transformer.ConflictResolver.VersionSelector; +import org.eclipse.aether.util.graph.visitor.PathRecordingDependencyVisitor; +import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor; +import org.eclipse.aether.version.Version; +import org.eclipse.aether.version.VersionConstraint; + +import static java.util.Objects.requireNonNull; + +/** + * A configurable version selector for use with {@link ConflictResolver} that resolves version conflicts using a + * specified strategy. If there is no single node that satisfies all encountered version ranges, the selector will fail. + * Based on configuration, this selector may fail for other reasons as well. + * + * @since 2.0.0 + */ +public class ConfigurableVersionSelector extends VersionSelector { + /** + * The strategy how "winner" is being selected. + */ + public interface SelectionStrategy { + /** + * Invoked for every "candidate" when winner is already set (very first candidate is set as winner). + *

+ * This method should determine is candidate "better" or not and should replace current winner. This method + * is invoked whenever {@code candidate} is "considered" (fits any constraint in effect, if any). + */ + boolean isBetter(ConflictItem candidate, ConflictItem winner); + /** + * Method invoked at version selection end, just before version selector returns. Note: {@code winner} may + * be {@code null}, while the rest of parameters cannot. The parameter {@code candidates} contains all the + * "considered candidates", dependencies that fulfil all constraints, if present. The {@code context} on the + * other hand contains all items participating in conflict. + *

+ * This method by default just returns the passed in {@code winner}, but can do much more. + */ + default ConflictItem winnerSelected( + ConflictItem winner, Collection candidates, ConflictContext context) + throws UnsolvableVersionConflictException { + return winner; + } + } + /** + * The strategy of winner selection, never {@code null}. + */ + protected final SelectionStrategy selectionStrategy; + + /** + * Creates a new instance of this version selector that works "as Maven did so far". + */ + public ConfigurableVersionSelector() { + this(new Nearest()); + } + + /** + * Creates a new instance of this version selector. + * + * @param selectionStrategy The winner selection strategy, must not be {@code null}. Maven3 + * used {@link Nearest} strategy. + */ + public ConfigurableVersionSelector(SelectionStrategy selectionStrategy) { + this.selectionStrategy = requireNonNull(selectionStrategy, "selectionStrategy"); + } + + @Override + public void selectVersion(ConflictContext context) throws RepositoryException { + ConflictGroup group = new ConflictGroup(); + for (ConflictItem candidate : context.getItems()) { + DependencyNode node = candidate.getNode(); + VersionConstraint constraint = node.getVersionConstraint(); + + boolean backtrack = false; + boolean hardConstraint = constraint.getRange() != null; + + if (hardConstraint) { + if (group.constraints.add(constraint)) { + if (group.winner != null + && !constraint.containsVersion( + group.winner.getNode().getVersion())) { + backtrack = true; + } + } + } + + if (isAcceptableByConstraints(group, node.getVersion())) { + group.candidates.add(candidate); + + if (backtrack) { + backtrack(group, context); + } else if (group.winner == null || selectionStrategy.isBetter(candidate, group.winner)) { + group.winner = candidate; + } + } else if (backtrack) { + backtrack(group, context); + } + } + context.setWinner(selectionStrategy.winnerSelected(group.winner, group.candidates, context)); + } + + protected void backtrack(ConflictGroup group, ConflictContext context) throws UnsolvableVersionConflictException { + group.winner = null; + + for (Iterator it = group.candidates.iterator(); it.hasNext(); ) { + ConflictItem candidate = it.next(); + + if (!isAcceptableByConstraints(group, candidate.getNode().getVersion())) { + it.remove(); + } else if (group.winner == null || selectionStrategy.isBetter(candidate, group.winner)) { + group.winner = candidate; + } + } + + if (group.winner == null) { + throw newFailure("Unsolvable hard constraint combination", context); + } + } + + protected boolean isAcceptableByConstraints(ConflictGroup group, Version version) { + for (VersionConstraint constraint : group.constraints) { + if (!constraint.containsVersion(version)) { + return false; + } + } + return true; + } + + /** + * Helper method to create failure, creates instance of {@link UnsolvableVersionConflictException}. + */ + public static UnsolvableVersionConflictException newFailure(String message, ConflictContext context) { + DependencyFilter filter = (node, parents) -> { + requireNonNull(node, "node cannot be null"); + requireNonNull(parents, "parents cannot be null"); + return context.isIncluded(node); + }; + PathRecordingDependencyVisitor visitor = new PathRecordingDependencyVisitor(filter); + context.getRoot().accept(new TreeDependencyVisitor(visitor)); + return new UnsolvableVersionConflictException(message, visitor.getPaths()); + } + + protected static class ConflictGroup { + + final Collection constraints; + + final Collection candidates; + + ConflictItem winner; + + ConflictGroup() { + constraints = new HashSet<>(); + candidates = new ArrayList<>(64); + } + + @Override + public String toString() { + return String.valueOf(winner); + } + } + + /** + * Selection strategy that selects "nearest" (to the root) version. + *

+ * This is the "classic" Maven strategy. + */ + public static class Nearest implements SelectionStrategy { + @Override + public boolean isBetter(ConflictItem candidate, ConflictItem winner) { + if (candidate.isSibling(winner)) { + return candidate + .getNode() + .getVersion() + .compareTo(winner.getNode().getVersion()) + > 0; + } else { + return candidate.getDepth() < winner.getDepth(); + } + } + } + + /** + * Selection strategy that selects "highest" version. + */ + public static class Highest implements SelectionStrategy { + @Override + public boolean isBetter(ConflictItem candidate, ConflictItem winner) { + return candidate.getNode().getVersion().compareTo(winner.getNode().getVersion()) > 0; + } + } + + /** + * Example selection strategy (used in tests and demos), is not recommended to be used in production. + *

+ * Selection strategy that delegates to another selection strategy, and at the end enforces dependency convergence + * among candidates. + */ + public static class VersionConvergence implements SelectionStrategy { + private final SelectionStrategy delegate; + + public VersionConvergence(SelectionStrategy delegate) { + this.delegate = requireNonNull(delegate, "delegate"); + } + + @Override + public boolean isBetter(ConflictItem candidate, ConflictItem winner) { + return delegate.isBetter(candidate, winner); + } + + @Override + public ConflictItem winnerSelected( + ConflictItem winner, Collection candidates, ConflictContext context) + throws UnsolvableVersionConflictException { + if (winner != null && winner.getNode().getVersionConstraint().getRange() == null) { + Set versions = candidates.stream() + .map(c -> c.getDependency().getArtifact().getVersion()) + .collect(Collectors.toSet()); + if (versions.size() > 1) { + throw newFailure( + "Convergence violated for " + + winner.getDependency().getArtifact().getGroupId() + ":" + + winner.getDependency().getArtifact().getArtifactId() + ", versions present: " + + versions, + context); + } + } + return winner; + } + } + + /** + * Example selection strategy (used in tests and demos), is not recommended to be used in production. + *

+ * Selection strategy that delegates to another selection strategy, and at end enforces aligned "major versions" + * among candidates. + */ + public static class MajorVersionConvergence implements SelectionStrategy { + private final SelectionStrategy delegate; + + public MajorVersionConvergence(SelectionStrategy delegate) { + this.delegate = requireNonNull(delegate, "delegate"); + } + + @Override + public boolean isBetter(ConflictItem candidate, ConflictItem winner) { + return delegate.isBetter(candidate, winner); + } + + @Override + public ConflictItem winnerSelected( + ConflictItem winner, Collection candidates, ConflictContext context) + throws UnsolvableVersionConflictException { + if (winner != null && !candidates.isEmpty()) { + Set incompatibleVersions = candidates.stream() + .filter(c -> !sameMajor(c, winner)) + .map(c -> c.getDependency().getArtifact().getVersion()) + .collect(Collectors.toSet()); + if (!incompatibleVersions.isEmpty()) { + Set allVersions = candidates.stream() + .map(c -> c.getDependency().getArtifact().getVersion()) + .collect(Collectors.toSet()); + throw newFailure( + "Incompatible versions for " + + winner.getDependency().getArtifact().getGroupId() + ":" + + winner.getDependency().getArtifact().getArtifactId() + ", incompatible versions: " + + incompatibleVersions + ", all versions " + allVersions, + context); + } + } + return winner; + } + + private boolean sameMajor(ConflictItem candidate, ConflictItem winner) { + String candidateVersion = candidate.getDependency().getArtifact().getVersion(); + String winnerVersion = winner.getDependency().getArtifact().getVersion(); + // for now a naive check: major versions should be same + if (candidateVersion.contains(".") && winnerVersion.contains(".")) { + String candidateMajor = candidateVersion.substring(0, candidateVersion.indexOf('.')); + String winnerMajor = winnerVersion.substring(0, winnerVersion.indexOf('.')); + return Objects.equals(candidateMajor, winnerMajor); + } + return true; // cannot determine, so just leave it + } + } +} diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/NearestVersionSelector.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/NearestVersionSelector.java index 1bca82400..32b862dc9 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/NearestVersionSelector.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/NearestVersionSelector.java @@ -40,7 +40,10 @@ /** * A version selector for use with {@link ConflictResolver} that resolves version conflicts using a nearest-wins * strategy. If there is no single node that satisfies all encountered version ranges, the selector will fail. + * + * @deprecated Use {@link ConfigurableVersionSelector} instead. */ +@Deprecated public final class NearestVersionSelector extends VersionSelector { /** diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConfigurableVersionSelectorTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConfigurableVersionSelectorTest.java new file mode 100644 index 000000000..0e33d9fd9 --- /dev/null +++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/graph/transformer/ConfigurableVersionSelectorTest.java @@ -0,0 +1,233 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.eclipse.aether.util.graph.transformer; + +import java.util.List; + +import org.eclipse.aether.collection.UnsolvableVersionConflictException; +import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.internal.test.util.DependencyGraphParser; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + */ +public class ConfigurableVersionSelectorTest extends AbstractDependencyGraphTransformerTest { + @Override + protected ConflictResolver newTransformer() { + return new ConflictResolver( + new ConfigurableVersionSelector(new ConfigurableVersionSelector.MajorVersionConvergence( + new ConfigurableVersionSelector.Nearest())), + new JavaScopeSelector(), + new SimpleOptionalitySelector(), + new JavaScopeDeriver()); + } + + @Override + protected DependencyGraphParser newParser() { + return new DependencyGraphParser("transformer/version-resolver/"); + } + + @Test + void testSelectHighestVersionFromMultipleVersionsAtSameLevel() throws Exception { + DependencyNode root = parseResource("sibling-versions.txt"); + assertSame(root, transform(root)); + + assertEquals(1, root.getChildren().size()); + assertEquals("3", root.getChildren().get(0).getArtifact().getVersion()); + } + + @Test + void testSelectHighestVersionFromMultipleVersionsAtSameLevelIncompatibleMajors() { + assertThrows(UnsolvableVersionConflictException.class, () -> { + DependencyNode root = parseResource("sibling-major-versions.txt"); + transform(root); + }); + } + + @Test + void testSelectedVersionAtDeeperLevelThanOriginallySeen() throws Exception { + DependencyNode root = parseResource("nearest-underneath-loser-a.txt"); + + assertSame(root, transform(root)); + + List trail = find(root, "j"); + assertEquals(5, trail.size()); + } + + @Test + void testNearestDirtyVersionUnderneathRemovedNode() throws Exception { + DependencyNode root = parseResource("nearest-underneath-loser-b.txt"); + + assertSame(root, transform(root)); + + List trail = find(root, "j"); + assertEquals(5, trail.size()); + } + + @Test + void testViolationOfHardConstraintFallsBackToNearestSeenNotFirstSeenIncompatibleMajors() throws Exception { + assertThrows(UnsolvableVersionConflictException.class, () -> { + DependencyNode root = parseResource("range-major-backtracking.txt"); + transform(root); + }); + } + + @Test + void testViolationOfHardConstraintFallsBackToNearestSeenNotFirstSeen() throws Exception { + DependencyNode root = parseResource("range-backtracking.txt"); + + assertSame(root, transform(root)); + + List trail = find(root, "x"); + assertEquals(3, trail.size()); + assertEquals("2", trail.get(0).getArtifact().getVersion()); + } + + @Test + void testCyclicConflictIdGraph() throws Exception { + DependencyNode root = parseResource("conflict-id-cycle.txt"); + + assertSame(root, transform(root)); + + assertEquals(2, root.getChildren().size()); + assertEquals("a", root.getChildren().get(0).getArtifact().getArtifactId()); + assertEquals("b", root.getChildren().get(1).getArtifact().getArtifactId()); + assertTrue(root.getChildren().get(0).getChildren().isEmpty()); + assertTrue(root.getChildren().get(1).getChildren().isEmpty()); + } + + @Test + void testUnsolvableRangeConflictBetweenHardConstraints() { + assertThrows(UnsolvableVersionConflictException.class, () -> { + DependencyNode root = parseResource("unsolvable.txt"); + transform(root); + }); + } + + @Test + void testUnsolvableRangeConflictWithUnrelatedCycle() throws Exception { + assertThrows(UnsolvableVersionConflictException.class, () -> { + DependencyNode root = parseResource("unsolvable-with-cycle.txt"); + assertSame(root, transform(root)); + }); + } + + @Test + void testSolvableConflictBetweenHardConstraints() throws Exception { + DependencyNode root = parseResource("ranges.txt"); + + assertSame(root, transform(root)); + } + + @Test + void testConflictGroupCompletelyDroppedFromResolvedTree() throws Exception { + DependencyNode root = parseResource("dead-conflict-group.txt"); + + assertSame(root, transform(root)); + + assertEquals(2, root.getChildren().size()); + assertEquals("a", root.getChildren().get(0).getArtifact().getArtifactId()); + assertEquals("b", root.getChildren().get(1).getArtifact().getArtifactId()); + assertTrue(root.getChildren().get(0).getChildren().isEmpty()); + assertTrue(root.getChildren().get(1).getChildren().isEmpty()); + } + + @Test + void testNearestSoftVersionPrunedByFartherRange() throws Exception { + DependencyNode root = parseResource("soft-vs-range.txt"); + + assertSame(root, transform(root)); + + assertEquals(2, root.getChildren().size()); + assertEquals("a", root.getChildren().get(0).getArtifact().getArtifactId()); + assertEquals(0, root.getChildren().get(0).getChildren().size()); + assertEquals("b", root.getChildren().get(1).getArtifact().getArtifactId()); + assertEquals(1, root.getChildren().get(1).getChildren().size()); + } + + @Test + void testCyclicGraph() throws Exception { + DependencyNode root = parseResource("cycle.txt"); + + assertSame(root, transform(root)); + + assertEquals(2, root.getChildren().size()); + assertEquals(1, root.getChildren().get(0).getChildren().size()); + assertEquals( + 0, root.getChildren().get(0).getChildren().get(0).getChildren().size()); + assertEquals(0, root.getChildren().get(1).getChildren().size()); + } + + @Test + void testLoop() throws Exception { + DependencyNode root = parseResource("loop.txt"); + + assertSame(root, transform(root)); + + assertEquals(0, root.getChildren().size()); + } + + @Test + void testOverlappingCycles() throws Exception { + DependencyNode root = parseResource("overlapping-cycles.txt"); + + assertSame(root, transform(root)); + + assertEquals(2, root.getChildren().size()); + } + + @Test + void testScopeDerivationAndConflictResolutionCantHappenForAllNodesBeforeVersionSelection() throws Exception { + DependencyNode root = parseResource("scope-vs-version.txt"); + + assertSame(root, transform(root)); + + DependencyNode[] nodes = find(root, "y").toArray(new DependencyNode[0]); + assertEquals(3, nodes.length); + assertEquals("test", nodes[1].getDependency().getScope()); + assertEquals("test", nodes[0].getDependency().getScope()); + } + + @Test + void testVerboseMode() throws Exception { + DependencyNode root = parseResource("verbose.txt"); + + session.setConfigProperty(ConflictResolver.CONFIG_PROP_VERBOSE, Boolean.TRUE); + assertSame(root, transform(root)); + + assertEquals(2, root.getChildren().size()); + assertEquals(1, root.getChildren().get(0).getChildren().size()); + DependencyNode winner = root.getChildren().get(0).getChildren().get(0); + assertEquals("test", winner.getDependency().getScope()); + assertEquals("compile", winner.getData().get(ConflictResolver.NODE_DATA_ORIGINAL_SCOPE)); + assertEquals(false, winner.getData().get(ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY)); + assertEquals(1, root.getChildren().get(1).getChildren().size()); + DependencyNode loser = root.getChildren().get(1).getChildren().get(0); + assertEquals("test", loser.getDependency().getScope()); + assertEquals(0, loser.getChildren().size()); + assertSame(winner, loser.getData().get(ConflictResolver.NODE_DATA_WINNER)); + assertEquals("compile", loser.getData().get(ConflictResolver.NODE_DATA_ORIGINAL_SCOPE)); + assertEquals(false, loser.getData().get(ConflictResolver.NODE_DATA_ORIGINAL_OPTIONALITY)); + } +} diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/range-major-backtracking.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/range-major-backtracking.txt new file mode 100644 index 000000000..cf201232d --- /dev/null +++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/range-major-backtracking.txt @@ -0,0 +1,10 @@ +(null) ++- test:x:1.0 ++- test:a:1 +| \- test:b:1 +| \- test:x:3.0 ++- test:c:1 +| \- test:x:2.0 +\- test:d:1 + \- test:e:1 + \- test:x:2[2,) # forces rejection of x:1, should fallback to nearest and not first-seen, i.e. x:2 and not x:3 diff --git a/maven-resolver-util/src/test/resources/transformer/version-resolver/sibling-major-versions.txt b/maven-resolver-util/src/test/resources/transformer/version-resolver/sibling-major-versions.txt new file mode 100644 index 000000000..a32ea844b --- /dev/null +++ b/maven-resolver-util/src/test/resources/transformer/version-resolver/sibling-major-versions.txt @@ -0,0 +1,7 @@ +# multiple versions of the same GA beneath the same parent as seen after expansion of version ranges +# versions neither in ascending nor descending order + +test:root:1 ++- test:a:1.0 ++- test:a:3.0 +\- test:a:2.0