DependencyVersionsCheckMojo.java
/*
* 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 org.basepom.mojo.dvc.mojo;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Maps;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.shared.utils.logging.MessageBuilder;
import org.apache.maven.shared.utils.logging.MessageUtils;
import org.basepom.mojo.dvc.AbstractDependencyVersionsMojo;
import org.basepom.mojo.dvc.QualifiedName;
import org.basepom.mojo.dvc.dependency.DependencyMap;
import org.basepom.mojo.dvc.strategy.Strategy;
import org.basepom.mojo.dvc.version.VersionResolutionCollection;
import org.basepom.mojo.dvc.version.VersionResolutionElement;
import org.eclipse.aether.graph.DependencyNode;
import org.eclipse.aether.version.Version;
import java.util.function.Function;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
/**
* Resolves all dependencies of a project and reports version conflicts.
*/
@Mojo(name = "check", requiresProject = true, threadSafe = true, defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.NONE)
public class DependencyVersionsCheckMojo
extends AbstractDependencyVersionsMojo
{
/**
* List only dependencies in conflict or all dependencies.
*
* @since 3.0.0
*/
@Parameter(defaultValue = "true", property = "dvc.conflicts-only")
public boolean conflictsOnly = true;
/**
* Fail the build if a conflict is detected. Any conflict (direct and transitive) will cause a failure.
*
* @since 3.0.0
*/
@Parameter(defaultValue = "false", alias = "failBuildInCaseOfConflict", property = "dvc.conflicts-fail-build")
protected boolean conflictsFailBuild = false;
/**
* Fail the build only if a version conflict involves one or more direct dependencies. Direct dependency versions are controlled
* by the project itself so any conflict here can be fixed by changing the version in the project.
* <br>
* It is strongly recommended to review and fix these conflicts.
*
* @since 3.0.0
*/
@Parameter(defaultValue = "false", property = "dvc.direct-conflicts-fail-build")
protected boolean directConflictsFailBuild = false;
protected void doExecute(final ImmutableSetMultimap<QualifiedName, VersionResolutionCollection> resolutionMap, final DependencyMap rootDependencyMap)
throws Exception
{
// filter out what to display.
final var filteredMap = ImmutableMap.copyOf(Maps.filterValues(
resolutionMap.asMap(),
v -> {
// report if no condition is set.
boolean report = true;
if (conflictsOnly) {
// do not report if conflicts are requested but none exists
report &= v.stream().anyMatch(VersionResolutionCollection::hasConflict);
}
if (directOnly) {
// do not report if only directs are requested but it is not direct
report &= v.stream().anyMatch(VersionResolutionCollection::hasDirectDependencies);
}
if (managedOnly) {
report &= v.stream().anyMatch(VersionResolutionCollection::hasManagedDependencies);
}
return report;
}));
LOG.report(quiet, "Checking %s%s dependencies%s for '%s' scope%s",
(directOnly ? "direct" : "all"),
(managedOnly ? ", managed" : ""),
(deepScan ? " using deep scan" : ""),
scope,
(conflictsOnly ? ", reporting only conflicts" : ""));
if (filteredMap.isEmpty()) {
return;
}
final var rootDependencies = rootDependencyMap.getAllDependencies();
boolean directConflicts = false;
boolean transitiveConflicts = false;
for (final var entry : filteredMap.entrySet()) {
final var versionMap = entry.getValue().stream()
.collect(toImmutableSetMultimap(VersionResolutionCollection::getExpectedVersion, Function.identity()));
boolean willWarn = false;
boolean willFail = false;
final boolean isDirect = entry.getValue().stream().anyMatch(VersionResolutionCollection::hasDirectDependencies);
final QualifiedName dependencyName = entry.getKey();
final DependencyNode currentDependency = rootDependencies.get(dependencyName);
assert currentDependency != null;
final boolean isManaged = (currentDependency.getManagedBits() & DependencyNode.MANAGED_VERSION) != 0;
final Version dependencyVersion = currentDependency.getVersion();
checkState(dependencyVersion != null, "Dependency Version for %s is null! File a bug!", currentDependency);
final ComparableVersion resolvedVersion = new ComparableVersion(dependencyVersion.toString());
final Strategy strategy = strategyCache.forQualifiedName(dependencyName);
final MessageBuilder mb = MessageUtils.buffer();
mb.strong(dependencyName.getShortName())
.a(": ")
.strong(resolvedVersion)
.format(" (%s%s) - scope: %s - strategy: %s",
isDirect ? "direct" : "transitive",
isManaged ? ", managed" : "",
currentDependency.getDependency().getScope(),
strategy.getName()
)
.newline();
final int versionPadding = versionMap.keySet().stream().map(v -> v.toString().length()).reduce(0, Math::max);
for (final var versionEntry : versionMap.asMap().entrySet()) {
final boolean hasConflictVersion = versionEntry.getValue().stream().anyMatch(VersionResolutionCollection::hasConflict);
final boolean perfectMatch = versionEntry.getValue().stream().anyMatch(v -> v.isMatchFor(resolvedVersion));
final String paddedVersion = Strings.padEnd(versionEntry.getKey().toString(), versionPadding + 1, ' ');
mb.a(" ");
if (hasConflictVersion) {
mb.failure(paddedVersion);
}
else if (perfectMatch) {
mb.success(paddedVersion);
}
else {
mb.a(paddedVersion);
}
mb.a("expected by ");
for (var it = versionEntry.getValue().stream()
.flatMap(v -> v.getRequestingDependencies().stream())
.iterator(); it.hasNext(); ) {
final VersionResolutionElement versionResolutionElement = it.next();
final String name = versionResolutionElement.getRequestingDependency().getShortName();
if (versionResolutionElement.isDirectDependency()) {
mb.strong("*" + name + "*");
}
else {
mb.a(name);
}
if (it.hasNext()) {
mb.a(", ");
}
}
mb.newline();
if (hasConflictVersion) {
willWarn = true;
willFail |= conflictsFailBuild; // any conflict fails build.
willFail |= isDirect && directConflictsFailBuild;
directConflicts |= isDirect; // any direct dependency in conflict
transitiveConflicts |= !isDirect; // any transitive dependency in conflict
}
}
if (willFail) {
LOG.error("%s", mb);
}
else if (willWarn) {
LOG.warn("%s", mb);
}
else {
LOG.info("%s", mb);
}
}
if (directConflicts && (conflictsFailBuild || directConflictsFailBuild)) {
throw new MojoFailureException("Version conflict in direct dependencies detected!");
}
if (transitiveConflicts && conflictsFailBuild) {
throw new MojoFailureException("Version conflict in transitive dependencies detected!");
}
}
}