001/* 002 * Licensed under the Apache License, Version 2.0 (the "License"); 003 * you may not use this file except in compliance with the License. 004 * You may obtain a copy of the License at 005 * 006 * http://www.apache.org/licenses/LICENSE-2.0 007 * 008 * Unless required by applicable law or agreed to in writing, software 009 * distributed under the License is distributed on an "AS IS" BASIS, 010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 011 * See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014 015package org.basepom.mojo.dvc.mojo; 016 017import static com.google.common.base.Preconditions.checkState; 018import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap; 019 020import org.basepom.mojo.dvc.AbstractDependencyVersionsMojo; 021import org.basepom.mojo.dvc.QualifiedName; 022import org.basepom.mojo.dvc.dependency.DependencyMap; 023import org.basepom.mojo.dvc.strategy.Strategy; 024import org.basepom.mojo.dvc.version.VersionResolutionCollection; 025import org.basepom.mojo.dvc.version.VersionResolutionElement; 026 027import java.util.function.Function; 028 029import com.google.common.base.Strings; 030import com.google.common.collect.ImmutableMap; 031import com.google.common.collect.ImmutableSetMultimap; 032import com.google.common.collect.Maps; 033import org.apache.maven.artifact.versioning.ComparableVersion; 034import org.apache.maven.plugin.MojoFailureException; 035import org.apache.maven.plugins.annotations.LifecyclePhase; 036import org.apache.maven.plugins.annotations.Mojo; 037import org.apache.maven.plugins.annotations.Parameter; 038import org.apache.maven.plugins.annotations.ResolutionScope; 039import org.apache.maven.shared.utils.logging.MessageBuilder; 040import org.apache.maven.shared.utils.logging.MessageUtils; 041import org.eclipse.aether.graph.DependencyNode; 042import org.eclipse.aether.version.Version; 043 044/** 045 * Resolves all dependencies of a project and reports version conflicts. 046 */ 047@Mojo(name = "check", requiresProject = true, threadSafe = true, defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.NONE) 048public class DependencyVersionsCheckMojo 049 extends AbstractDependencyVersionsMojo { 050 051 /** 052 * List only dependencies in conflict or all dependencies. 053 * 054 * @since 3.0.0 055 */ 056 @Parameter(defaultValue = "true", property = "dvc.conflicts-only") 057 public boolean conflictsOnly = true; 058 059 /** 060 * Fail the build if a conflict is detected. Any conflict (direct and transitive) will cause a failure. 061 * 062 * @since 3.0.0 063 */ 064 @Parameter(defaultValue = "false", alias = "failBuildInCaseOfConflict", property = "dvc.conflicts-fail-build") 065 protected boolean conflictsFailBuild = false; 066 067 /** 068 * Fail the build only if a version conflict involves one or more direct dependencies. Direct dependency versions are controlled by the project itself so 069 * any conflict here can be fixed by changing the version in the project. 070 * <br> 071 * It is strongly recommended to review and fix these conflicts. 072 * 073 * @since 3.0.0 074 */ 075 @Parameter(defaultValue = "false", property = "dvc.direct-conflicts-fail-build") 076 protected boolean directConflictsFailBuild = false; 077 078 @Override 079 protected void doExecute(final ImmutableSetMultimap<QualifiedName, VersionResolutionCollection> resolutionMap, final DependencyMap rootDependencyMap) 080 throws Exception { 081 // filter out what to display. 082 final var filteredMap = ImmutableMap.copyOf(Maps.filterValues( 083 resolutionMap.asMap(), 084 v -> { 085 // report if no condition is set. 086 boolean report = true; 087 088 if (conflictsOnly) { 089 // do not report if conflicts are requested but none exists 090 report &= v.stream().anyMatch(VersionResolutionCollection::hasConflict); 091 } 092 if (directOnly) { 093 // do not report if only directs are requested but it is not direct 094 report &= v.stream().anyMatch(VersionResolutionCollection::hasDirectDependencies); 095 } 096 097 if (managedOnly) { 098 report &= v.stream().anyMatch(VersionResolutionCollection::hasManagedDependencies); 099 } 100 101 return report; 102 })); 103 104 log.report(quiet, "Checking %s%s dependencies%s for '%s' scope%s", 105 (directOnly ? "direct" : "all"), 106 (managedOnly ? ", managed" : ""), 107 (deepScan ? " using deep scan" : ""), 108 scope, 109 (conflictsOnly ? ", reporting only conflicts" : "")); 110 111 if (filteredMap.isEmpty()) { 112 return; 113 } 114 115 final var rootDependencies = rootDependencyMap.getAllDependencies(); 116 117 boolean directConflicts = false; 118 boolean transitiveConflicts = false; 119 120 for (final var entry : filteredMap.entrySet()) { 121 final var versionMap = entry.getValue().stream() 122 .collect(toImmutableSetMultimap(VersionResolutionCollection::getExpectedVersion, Function.identity())); 123 124 boolean willWarn = false; 125 boolean willFail = false; 126 127 final boolean isDirect = entry.getValue().stream().anyMatch(VersionResolutionCollection::hasDirectDependencies); 128 final QualifiedName dependencyName = entry.getKey(); 129 final DependencyNode currentDependency = rootDependencies.get(dependencyName); 130 assert currentDependency != null; 131 132 final boolean isManaged = (currentDependency.getManagedBits() & DependencyNode.MANAGED_VERSION) != 0; 133 134 final Version dependencyVersion = currentDependency.getVersion(); 135 checkState(dependencyVersion != null, "Dependency Version for %s is null! File a bug!", currentDependency); 136 final ComparableVersion resolvedVersion = new ComparableVersion(dependencyVersion.toString()); 137 138 final Strategy strategy = strategyCache.forQualifiedName(dependencyName); 139 140 final MessageBuilder mb = MessageUtils.buffer(); 141 142 mb.strong(dependencyName.getShortName()) 143 .a(": ") 144 .strong(resolvedVersion) 145 .format(" (%s%s) - scope: %s - strategy: %s", 146 isDirect ? "direct" : "transitive", 147 isManaged ? ", managed" : "", 148 currentDependency.getDependency().getScope(), 149 strategy.getName() 150 ) 151 .newline(); 152 153 final int versionPadding = versionMap.keySet().stream().map(v -> v.toString().length()).reduce(0, Math::max); 154 for (final var versionEntry : versionMap.asMap().entrySet()) { 155 final boolean hasConflictVersion = versionEntry.getValue().stream().anyMatch(VersionResolutionCollection::hasConflict); 156 final boolean perfectMatch = versionEntry.getValue().stream().anyMatch(v -> v.isMatchFor(resolvedVersion)); 157 final String paddedVersion = Strings.padEnd(versionEntry.getKey().toString(), versionPadding + 1, ' '); 158 159 mb.a(" "); 160 161 if (hasConflictVersion) { 162 mb.failure(paddedVersion); 163 } else if (perfectMatch) { 164 mb.success(paddedVersion); 165 } else { 166 mb.a(paddedVersion); 167 } 168 169 mb.a("expected by "); 170 171 for (var it = versionEntry.getValue().stream() 172 .flatMap(v -> v.getRequestingDependencies().stream()) 173 .iterator(); it.hasNext(); ) { 174 final VersionResolutionElement versionResolutionElement = it.next(); 175 final String name = versionResolutionElement.getRequestingDependency().getShortName(); 176 177 if (versionResolutionElement.isDirectDependency()) { 178 mb.strong("*" + name + "*"); 179 } else { 180 mb.a(name); 181 } 182 if (it.hasNext()) { 183 mb.a(", "); 184 } 185 } 186 187 mb.newline(); 188 189 if (hasConflictVersion) { 190 willWarn = true; 191 willFail |= conflictsFailBuild; // any conflict fails build. 192 willFail |= isDirect && directConflictsFailBuild; 193 194 directConflicts |= isDirect; // any direct dependency in conflict 195 transitiveConflicts |= !isDirect; // any transitive dependency in conflict 196 } 197 } 198 199 if (willFail) { 200 log.error("%s", mb); 201 } else if (willWarn) { 202 log.warn("%s", mb); 203 } else { 204 log.info("%s", mb); 205 } 206 } 207 208 if (directConflicts && (conflictsFailBuild || directConflictsFailBuild)) { 209 throw new MojoFailureException("Version conflict in direct dependencies detected!"); 210 } 211 212 if (transitiveConflicts && conflictsFailBuild) { 213 throw new MojoFailureException("Version conflict in transitive dependencies detected!"); 214 } 215 } 216}