diff --git a/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/BanDynamicVersions.java b/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/BanDynamicVersions.java new file mode 100644 index 00000000..c3a5cc07 --- /dev/null +++ b/enforcer-rules/src/main/java/org/apache/maven/plugins/enforcer/BanDynamicVersions.java @@ -0,0 +1,382 @@ +package org.apache.maven.plugins.enforcer; + +/* + * 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. + */ + +import java.text.ChoiceFormat; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.maven.RepositoryUtils; +import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; +import org.apache.maven.enforcer.rule.api.EnforcerRuleException; +import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper; +import org.apache.maven.plugin.logging.Log; +import org.apache.maven.plugins.enforcer.utils.ArtifactMatcher; +import org.apache.maven.project.MavenProject; +import org.apache.maven.shared.utils.logging.MessageBuilder; +import org.apache.maven.shared.utils.logging.MessageUtils; +import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException; +import org.codehaus.plexus.component.repository.exception.ComponentLookupException; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.collection.CollectRequest; +import org.eclipse.aether.collection.CollectResult; +import org.eclipse.aether.collection.DependencyCollectionException; +import org.eclipse.aether.collection.DependencySelector; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.graph.DependencyNode; +import org.eclipse.aether.graph.DependencyVisitor; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.util.graph.selector.AndDependencySelector; +import org.eclipse.aether.util.graph.selector.OptionalDependencySelector; +import org.eclipse.aether.util.graph.selector.ScopeDependencySelector; +import org.eclipse.aether.util.graph.visitor.TreeDependencyVisitor; +import org.eclipse.aether.version.VersionConstraint; + +/** + * This rule bans dependencies having a version which requires resolution (i.e. dynamic versions which might change with + * each build). Dynamic versions are either + * + * + * @since 3.2.0 + */ +public class BanDynamicVersions + extends AbstractNonCacheableEnforcerRule +{ + + private static final String RELEASE = "RELEASE"; + + private static final String LATEST = "LATEST"; + + private static final String SNAPSHOT_SUFFIX = "-SNAPSHOT"; + + /** + * {@code true} if versions ending with {@code -SNAPSHOT} should be allowed + */ + private boolean allowSnapshots; + + /** + * {@code true} if versions using {@code LATEST} should be allowed + */ + private boolean allowLatest; + + /** + * {@code true} if versions using {@code RELEASE} should be allowed + */ + private boolean allowRelease; + + /** + * {@code true} if version ranges should be allowed + */ + private boolean allowRanges; + + /** + * {@code true} if ranges having the same upper and lower bound like {@code [1.0]} should be allowed. + * Only applicable if {@link #allowRanges} is not set to {@code true}. + */ + private boolean allowRangesWithIdenticalBounds; + + /** + * {@code true} if optional dependencies should not be checked + */ + private boolean excludeOptionals; + + /** + * the scopes of dependencies which should be excluded from this rule + */ + private String[] excludedScopes; + + /** + * Specify the ignored dependencies. This can be a list of artifacts in the format + * groupId[:artifactId[:version[:type[:scope:[classifier]]]]]. + * Any of the sections can be a wildcard by using '*' (e.g. {@code group:*:1.0}). + *
+ * Any of the ignored dependencies may have dynamic versions. + * + * @see {@link #setIgnores(List)} + */ + private List ignores = null; + + public void setIgnores( List ignores ) + { + this.ignores = ignores; + } + + public void setAllowSnapshots( boolean allowSnapshots ) + { + this.allowSnapshots = allowSnapshots; + } + + public void setAllowLatest( boolean allowLatest ) + { + this.allowLatest = allowLatest; + } + + public void setAllowRelease( boolean allowRelease ) + { + this.allowRelease = allowRelease; + } + + public void setAllowRanges( boolean allowRanges ) + { + this.allowRanges = allowRanges; + } + + public void setExcludeOptionals( boolean excludeOptionals ) + { + this.excludeOptionals = excludeOptionals; + } + + public void setExcludedScopes( String[] excludedScopes ) + { + this.excludedScopes = excludedScopes; + } + + private final class BannedDynamicVersionCollector + implements DependencyVisitor + { + + private final Log log; + + private final Deque nodeStack; // all intermediate nodes (without the root node) + + private boolean isRoot = true; + + private int numViolations; + + private final Predicate predicate; + + public int getNumViolations() + { + return numViolations; + } + + BannedDynamicVersionCollector( Log log, Predicate predicate ) + { + this.log = log; + nodeStack = new ArrayDeque<>(); + this.predicate = predicate; + this.isRoot = true; + numViolations = 0; + } + + private boolean isBannedDynamicVersion( VersionConstraint versionConstraint ) + { + if ( versionConstraint.getVersion() != null ) + { + if ( versionConstraint.getVersion().toString().equals( LATEST ) ) + { + return !allowLatest; + } + else if ( versionConstraint.getVersion().toString().equals( RELEASE ) ) + { + return !allowRelease; + } + else if ( versionConstraint.getVersion().toString().endsWith( SNAPSHOT_SUFFIX ) ) + { + return !allowSnapshots; + } + } + else if ( versionConstraint.getRange() != null ) + { + if ( allowRangesWithIdenticalBounds + && Objects.equals( versionConstraint.getRange().getLowerBound(), + versionConstraint.getRange().getUpperBound() ) ) + { + return false; + } + return !allowRanges; + } + else + { + log.warn( "Unexpected version constraint found: " + versionConstraint ); + } + return false; + + } + + @Override + public boolean visitEnter( DependencyNode node ) + { + if ( isRoot ) + { + isRoot = false; + } + else + { + log.debug( "Found node " + node + " with version constraint " + node.getVersionConstraint() ); + if ( predicate.test( node ) && isBannedDynamicVersion( node.getVersionConstraint() ) ) + { + MessageBuilder msgBuilder = MessageUtils.buffer(); + log.warn( msgBuilder.a( "Dependency " ) + .strong( node.getDependency() ) + .mojo( dumpIntermediatePath( nodeStack ) ) + .a( " is referenced with a banned dynamic version " + node.getVersionConstraint() ) + .toString() ); + numViolations++; + return false; + } + nodeStack.addLast( node ); + } + return true; + } + + @Override + public boolean visitLeave( DependencyNode node ) + { + if ( !nodeStack.isEmpty() ) + { + nodeStack.removeLast(); + } + return true; + } + } + + @SuppressWarnings( "unchecked" ) + @Override + public void execute( EnforcerRuleHelper helper ) + throws EnforcerRuleException + { + MavenProject project; + DefaultRepositorySystemSession newRepoSession; + RepositorySystem repoSystem; + List remoteRepositories; + try + { + project = (MavenProject) Objects.requireNonNull( helper.evaluate( "${project}" ), "${project} is null" ); + RepositorySystemSession repoSession = + (RepositorySystemSession) Objects.requireNonNull( helper.evaluate( "${repositorySystemSession}" ), + "${repositorySystemSession} is null" ); + // get a new session to be able to tweak the dependency selector + newRepoSession = new DefaultRepositorySystemSession( repoSession ); + remoteRepositories = (List) helper.evaluate( "${project.remoteProjectRepositories}" ); + repoSystem = helper.getComponent( RepositorySystem.class ); + } + catch ( ExpressionEvaluationException eee ) + { + throw new EnforcerRuleException( "Cannot resolve expression", eee ); + } + catch ( ComponentLookupException cle ) + { + throw new EnforcerRuleException( "Unable to retrieve component RepositorySystem", cle ); + } + Log log = helper.getLog(); + + Collection depSelectors = new ArrayList<>(); + depSelectors.add( new ScopeDependencySelector( excludedScopes ) ); + if ( excludeOptionals ) + { + depSelectors.add( new OptionalDependencySelector() ); + } + newRepoSession.setDependencySelector( new AndDependencySelector( depSelectors ) ); + + Dependency rootDependency = RepositoryUtils.toDependency( project.getArtifact(), null ); + try + { + // use root dependency with unresolved direct dependencies + int numViolations = emitDependenciesWithBannedDynamicVersions( rootDependency, repoSystem, newRepoSession, + remoteRepositories, log ); + if ( numViolations > 0 ) + { + ChoiceFormat dependenciesFormat = new ChoiceFormat( "1#dependency|1 path ) + { + if ( path.isEmpty() ) + { + return ""; + } + return " via " + path.stream().map( n -> n.getArtifact().toString() ).collect( Collectors.joining( " -> " ) ); + } + + private static final class ExcludeArtifactPatternsPredicate + implements Predicate + { + + private final ArtifactMatcher artifactMatcher; + + ExcludeArtifactPatternsPredicate( List excludes ) + { + this.artifactMatcher = new ArtifactMatcher( excludes, Collections.emptyList() ); + } + + @Override + public boolean test( DependencyNode depNode ) + { + try + { + return artifactMatcher.match( RepositoryUtils.toArtifact( depNode.getArtifact() ) ); + } + catch ( InvalidVersionSpecificationException e ) + { + throw new IllegalArgumentException( "Invalid version found for dependency node " + depNode, e ); + } + } + + } + + protected int emitDependenciesWithBannedDynamicVersions( org.eclipse.aether.graph.Dependency rootDependency, + RepositorySystem repoSystem, + RepositorySystemSession repoSession, + List remoteRepositories, Log log ) + throws DependencyCollectionException + { + CollectRequest collectRequest = new CollectRequest( rootDependency, remoteRepositories ); + CollectResult collectResult = repoSystem.collectDependencies( repoSession, collectRequest ); + Predicate predicate; + if ( ignores != null && !ignores.isEmpty() ) + { + predicate = new ExcludeArtifactPatternsPredicate( ignores ); + } + else + { + predicate = d -> true; + } + BannedDynamicVersionCollector bannedDynamicVersionCollector = + new BannedDynamicVersionCollector( log, predicate ); + DependencyVisitor depVisitor = new TreeDependencyVisitor( bannedDynamicVersionCollector ); + collectResult.getRoot().accept( depVisitor ); + return bannedDynamicVersionCollector.getNumViolations(); + } + +} diff --git a/enforcer-rules/src/site/apt/banDynamicVersions.apt.vm b/enforcer-rules/src/site/apt/banDynamicVersions.apt.vm new file mode 100644 index 00000000..806bf273 --- /dev/null +++ b/enforcer-rules/src/site/apt/banDynamicVersions.apt.vm @@ -0,0 +1,111 @@ +~~ 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. + + ------ + Ban Dynamic Versions + ------ + Konrad Windszus + ------ + 2022-10-13 + ------ + +Ban Dynamic Versions + + This rule bans dependencies having versions which require resolving (i.e. dynamic versions which might change with each build and require + lookup of {{{https://maven.apache.org/ref/3-LATEST/maven-repository-metadata/repository-metadata.html}repositoy metadata}}). Dynamic versions are either + + * {{{https://maven.apache.org/pom.html#Dependency_Version_Requirement_Specification}version ranges}}, i.e. all version strings starting with either <<<[>>> or <<<(>>>, + + * the special placeholders <<>>/<<>> or + + * versions ending with <<<-SNAPSHOT>>>. + + [] + + The following parameters are supported by this rule: + + * <> - if <<>> dependencies with versions ending with <<<-SNAPSHOT>>> will not be banned. Default is <<>>. + + * <> - if <<>> dependencies with version placeholder <<>> will not be banned. Default is <<>>. + + * <> - if <<>> dependencies with versions placeholder <<>> will not be banned. Default is <<>>. + + * <> - if <<>> versions ending with <<<-SNAPSHOT>>> will not be banned. Default is <<>>. + + * <> - if <<>> ranges having a range with same upper and lower bound (always inclusive) will not be banned (although they require resolving). + + * <> - if <<>> optional dependencies won't be checked. Default is <<>>. + + * <> - the list of scopes to exclude. By default no scopes are excluded. + + * <> - a list of dependencies to ignore. The format is <<>> where <<>>, <<>>, <<>>, <<>> and <<>> are optional (but require all previous parts). Wildcards may be used to replace an entire or just parts of a section. + Examples: + + * <<>> + + * <<>> + + * <<>> + + * <<>> + + * <<<*:*:*:jar:compile:tests>>> + + * <<>> + + [] + + * <> - an optional message to the user if the rule fails. + + [] + + + Sample Plugin Configuration: + ++---+ + + [...] + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${project.version} + + + ban-dynamic-versions + + enforce + + + + + + org.apache.maven + + true + + + + + + + + + [...] + ++---+ diff --git a/enforcer-rules/src/site/apt/index.apt b/enforcer-rules/src/site/apt/index.apt index dfd8dece..ac384bac 100644 --- a/enforcer-rules/src/site/apt/index.apt +++ b/enforcer-rules/src/site/apt/index.apt @@ -37,6 +37,8 @@ Built-In Rules * {{{./banDuplicatePomDependencyVersions.html}banDuplicatePomDependencyVersions}} - enforces that the project doesn't have duplicate declared dependencies. + * {{{./banDynamicVersions.html}banDynamicVersions}} - bans all dependencies requiring version resolution at build time (i.e. version ranges, placeholders <<>>/<<>> or SNAPSHOT versions). + * {{{./bannedDependencies.html}bannedDependencies}} - enforces that excluded dependencies aren't included. * {{{./bannedPlugins.html}bannedPlugins}} - enforces that specific plugins aren't included in the build. diff --git a/maven-enforcer-plugin/src/it/mrm/repository/menforcer427-1.0.pom b/maven-enforcer-plugin/src/it/mrm/repository/menforcer427-1.0.pom new file mode 100644 index 00000000..46e07d99 --- /dev/null +++ b/maven-enforcer-plugin/src/it/mrm/repository/menforcer427-1.0.pom @@ -0,0 +1,39 @@ + + + + 4.0.0 + org.apache.maven.plugins.enforcer.its + menforcer427 + 1.0 + + + + org.apache.maven.plugins.enforcer.its + menforcer427-a + [1.0,2) + + + org.apache.maven.plugins.enforcer.its + menforcer427-b + [1.0,2) + + + \ No newline at end of file diff --git a/maven-enforcer-plugin/src/it/mrm/repository/menforcer427_a-1.0.pom b/maven-enforcer-plugin/src/it/mrm/repository/menforcer427_a-1.0.pom new file mode 100644 index 00000000..01bf52a8 --- /dev/null +++ b/maven-enforcer-plugin/src/it/mrm/repository/menforcer427_a-1.0.pom @@ -0,0 +1,26 @@ + + + + 4.0.0 + org.apache.maven.plugins.enforcer.its + menforcer427-a + 1.0 + \ No newline at end of file diff --git a/maven-enforcer-plugin/src/it/mrm/repository/menforcer427_b-1.0.pom b/maven-enforcer-plugin/src/it/mrm/repository/menforcer427_b-1.0.pom new file mode 100644 index 00000000..10f018b6 --- /dev/null +++ b/maven-enforcer-plugin/src/it/mrm/repository/menforcer427_b-1.0.pom @@ -0,0 +1,26 @@ + + + + 4.0.0 + org.apache.maven.plugins.enforcer.its + menforcer427-b + 1.0 + \ No newline at end of file diff --git a/maven-enforcer-plugin/src/it/projects/ban-dynamic-versions/invoker.properties b/maven-enforcer-plugin/src/it/projects/ban-dynamic-versions/invoker.properties new file mode 100644 index 00000000..58b6526e --- /dev/null +++ b/maven-enforcer-plugin/src/it/projects/ban-dynamic-versions/invoker.properties @@ -0,0 +1,18 @@ +# 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. + +invoker.buildResult = failure diff --git a/maven-enforcer-plugin/src/it/projects/ban-dynamic-versions/pom.xml b/maven-enforcer-plugin/src/it/projects/ban-dynamic-versions/pom.xml new file mode 100644 index 00000000..71afef54 --- /dev/null +++ b/maven-enforcer-plugin/src/it/projects/ban-dynamic-versions/pom.xml @@ -0,0 +1,114 @@ + + + + + + 4.0.0 + + org.apache.maven.its.enforcer + ban-dynamic-versions-test + 1.0 + + + + + org.apache.maven.plugins + maven-enforcer-plugin + @project.version@ + + + test + + enforce + + + + + + test + + true + + + + + + + + + + + + + org.apache.maven.plugins.enforcer.its + menforcer138_archiver + + 2.1.1 + + + org.apache.maven.plugins.enforcer.its + menforcer138_utils + [1.0,5] + test + + + org.apache.maven.plugins.enforcer.its + menforcer427-b + 1.0 + + + + + + + org.apache.maven.plugins.enforcer.its + menforcer138_archiver + [1.3,2.1.1] + + + org.apache.maven.plugins.enforcer.its + menforcer138_utils + + + + org.apache.maven.plugins.enforcer.its + menforcer138_io + LATEST + + + org.apache.maven.plugins.enforcer.its + menforcer134_model + 1.0-SNAPSHOT + + + + org.apache.maven.plugins.enforcer.its + menforcer427 + 1.0 + + + org.apache.maven.plugins.enforcer.its + menforcer192-a + + [1.0] + + + + diff --git a/maven-enforcer-plugin/src/it/projects/ban-dynamic-versions/verify.groovy b/maven-enforcer-plugin/src/it/projects/ban-dynamic-versions/verify.groovy new file mode 100644 index 00000000..c26be3e4 --- /dev/null +++ b/maven-enforcer-plugin/src/it/projects/ban-dynamic-versions/verify.groovy @@ -0,0 +1,25 @@ +/* + * 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. + */ +File buildLog = new File( basedir, 'build.log' ) +assert buildLog.text.contains( '[WARNING] Dependency org.apache.maven.plugins.enforcer.its:menforcer138_archiver:jar:2.1.1 (compile) is referenced with a banned dynamic version [1.3,2.1.1]' ) +assert buildLog.text.contains( '[WARNING] Dependency org.apache.maven.plugins.enforcer.its:menforcer138_io:jar:LATEST (compile) is referenced with a banned dynamic version LATEST' ) +assert buildLog.text.contains( '[WARNING] Dependency org.apache.maven.plugins.enforcer.its:menforcer134_model:jar:1.0-SNAPSHOT (compile) is referenced with a banned dynamic version 1.0-SNAPSHOT' ) +assert buildLog.text.contains( '[WARNING] Dependency org.apache.maven.plugins.enforcer.its:menforcer427-a:jar:1.0 (compile) via org.apache.maven.plugins.enforcer.its:menforcer427:jar:1.0 is referenced with a banned dynamic version [1.0,2)' ) +assert buildLog.text.contains( '[ERROR] Rule 0: org.apache.maven.plugins.enforcer.BanDynamicVersions failed with message' ) +assert buildLog.text.contains( 'Found 4 dependencies with dynamic versions. Look at the warnings emitted above for the details.' )