diff --git a/maven-wrapper-plugin/pom.xml b/maven-wrapper-plugin/pom.xml
index 3af73fa4..d0f096fc 100644
--- a/maven-wrapper-plugin/pom.xml
+++ b/maven-wrapper-plugin/pom.xml
@@ -38,7 +38,7 @@ under the License.
- 4.11.0
+ 5.23.0
@@ -125,6 +125,12 @@ under the License.
${version.mockito}
test
+
+ org.codehaus.plexus
+ plexus-utils
+ 3.6.1
+ test
+
diff --git a/maven-wrapper-plugin/src/it/projects/mavenversion_range/pom.xml b/maven-wrapper-plugin/src/it/projects/mavenversion_range/pom.xml
new file mode 100644
index 00000000..e248ea57
--- /dev/null
+++ b/maven-wrapper-plugin/src/it/projects/mavenversion_range/pom.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.plugins.it.wrapper
+ mavenversion_range
+ 1.0.0-SNAPSHOT
+ pom
+
+
+
+
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ @version.exec-maven-plugin@
+
+ mvnw${cmd}
+
+ -v
+
+
+ true
+
+
+
+
+
+
+
+
+
+ windows
+
+ windows
+
+
+ .cmd
+
+
+
+
diff --git a/maven-wrapper-plugin/src/it/projects/mavenversion_range/test.properties b/maven-wrapper-plugin/src/it/projects/mavenversion_range/test.properties
new file mode 100644
index 00000000..94c52e67
--- /dev/null
+++ b/maven-wrapper-plugin/src/it/projects/mavenversion_range/test.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.
+
+maven=[3.0,3.10-rc\)
diff --git a/maven-wrapper-plugin/src/it/projects/mavenversion_range/verify.groovy b/maven-wrapper-plugin/src/it/projects/mavenversion_range/verify.groovy
new file mode 100644
index 00000000..68718008
--- /dev/null
+++ b/maven-wrapper-plugin/src/it/projects/mavenversion_range/verify.groovy
@@ -0,0 +1,39 @@
+
+/*
+ * 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.
+ */
+
+assert new File(basedir,'mvnw').exists()
+assert new File(basedir,'mvnw.cmd').exists()
+assert !(new File( basedir, 'mvnwDebug' ).exists())
+assert !(new File( basedir, 'mvnwDebug.cmd' ).exists())
+
+def wrapperProperties = new File(basedir,'.mvn/wrapper/maven-wrapper.properties')
+assert wrapperProperties.exists()
+
+Properties props = new Properties()
+wrapperProperties.withInputStream {
+ props.load(it)
+}
+
+// Assert that distribution URL was updated to the highest version automatically resolved
+// in the specified range: [3.0,3.10-rc)
+assert props.distributionUrl.endsWith("/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip")
+
+def log = new File(basedir, 'build.log').text
+assert log.contains('Apache Maven 3.9.16')
diff --git a/maven-wrapper-plugin/src/main/java/org/apache/maven/plugins/wrapper/WrapperMojo.java b/maven-wrapper-plugin/src/main/java/org/apache/maven/plugins/wrapper/WrapperMojo.java
index b38a0e45..e7417b50 100644
--- a/maven-wrapper-plugin/src/main/java/org/apache/maven/plugins/wrapper/WrapperMojo.java
+++ b/maven-wrapper-plugin/src/main/java/org/apache/maven/plugins/wrapper/WrapperMojo.java
@@ -29,10 +29,12 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.maven.Maven;
+import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
@@ -48,7 +50,11 @@
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResolutionException;
+import org.eclipse.aether.version.Version;
+import static java.util.Comparator.reverseOrder;
import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
/**
@@ -69,9 +75,11 @@ public class WrapperMojo extends AbstractMojo {
/**
* The version of Maven to require, default value is the Runtime version of Maven.
- * Can be any valid release above 2.0.9
+ * Can be any valid release above 2.0.9.
+ * Automatic version resolution is supported via {@link VersionRange} such as {@code [3.0,4.0-alpha)}.
*
* @since 3.0.0
+ * @see Dependency Version Requirement Specification
*/
@Parameter(property = "maven")
private String mavenVersion;
@@ -211,7 +219,7 @@ public void execute() throws MojoExecutionException {
+ " cannot work with mvnd, please set type to '" + TYPE_ONLY_SCRIPT + "'.");
}
- mavenVersion = getVersion(mavenVersion, Maven.class, "org.apache.maven/maven-core");
+ mavenVersion = resolveMavenVersion(getVersion(mavenVersion, Maven.class, "org.apache.maven/maven-core"));
String wrapperVersion = getVersion(null, this.getClass(), "org.apache.maven.plugins/maven-wrapper-plugin");
final Artifact artifact = downloadWrapperDistribution(wrapperVersion);
@@ -376,6 +384,36 @@ private String getVersion(String defaultVersion, Class> clazz, String path) {
return version;
}
+ /**
+ * Resolves the actual Maven version to download, given the range in {@link WrapperMojo#mavenVersion}.
+ * If the requested range could not be parsed, the version provided as input is returned.
+ *
+ * @param version the Maven version range
+ *
+ * @return the highest release version according to the provided range
+ * @see Dependency Version Requirement Specification
+ */
+ String resolveMavenVersion(String version) {
+ try {
+ Artifact artifact = new DefaultArtifact("org.apache.maven:apache-maven:" + version);
+ VersionRangeRequest request = new VersionRangeRequest(
+ artifact, session.getCurrentProject().getRemotePluginRepositories(), "wrapper");
+
+ List versions = repositorySystem
+ .resolveVersionRange(repositorySystemSession, request)
+ .getVersions();
+ versions.sort(reverseOrder());
+
+ return versions.stream()
+ .map(Version::toString)
+ .filter(v -> !artifact.setVersion(v).isSnapshot())
+ .findFirst()
+ .orElse(version);
+ } catch (VersionRangeResolutionException e) {
+ return version;
+ }
+ }
+
/**
* Determine the repository URL to download Wrapper and Maven from.
*/
diff --git a/maven-wrapper-plugin/src/site/markdown/usage.md b/maven-wrapper-plugin/src/site/markdown/usage.md
index 4bba7167..acfb9d0a 100644
--- a/maven-wrapper-plugin/src/site/markdown/usage.md
+++ b/maven-wrapper-plugin/src/site/markdown/usage.md
@@ -55,8 +55,12 @@ You can still use `-Dtype={type}` to change the distribution type for an existin
Maven Version
-------------
By default the plugin will assume the same version as the Maven runtime (calling `mvn -v`). But you can pick a different version.
-You can call `mvn wrapper:wrapper -Dmaven=x`, where `x` is any valid Apache Maven Release (see [Maven Central](https://central.sonatype.com/artifact/org.apache.maven/apache-maven/versions)).
-Another option is to adjust the `distributionUrl` in `.mvn/wrapper/maven-wrapper.properties`.
+You can call `mvn wrapper:wrapper -Dmaven=x`, where `x` is any valid Apache Maven Release (see [Maven Central](https://central.sonatype.com/artifact/org.apache.maven/apache-maven/versions))
+or a version range (see [Dependency Version Requirement Specification](https://maven.apache.org/pom.html#Dependency_Version_Requirement_Specification)).
+In the latter case, mind that round brackets must be escaped with a backslash, as in `-Dmaven=[3.0,4.0\)`,
+or the whole argument must be quoted: `-Dmaven="[3.0,4.0)"`.
+
+Another option is to adjust the `distributionUrl` in `.mvn/wrapper/maven-wrapper.properties` with the specific version.
Debugging
---------
diff --git a/maven-wrapper-plugin/src/test/java/org/apache/maven/plugins/wrapper/WrapperMojoTest.java b/maven-wrapper-plugin/src/test/java/org/apache/maven/plugins/wrapper/WrapperMojoTest.java
index 303d508c..4bd38809 100644
--- a/maven-wrapper-plugin/src/test/java/org/apache/maven/plugins/wrapper/WrapperMojoTest.java
+++ b/maven-wrapper-plugin/src/test/java/org/apache/maven/plugins/wrapper/WrapperMojoTest.java
@@ -18,18 +18,34 @@
*/
package org.apache.maven.plugins.wrapper;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.maven.execution.MavenSession;
+import org.apache.maven.project.MavenProject;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.repository.RemoteRepository;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResolutionException;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.eclipse.aether.version.Version;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
+import org.mockito.MockedConstruction;
import org.mockito.junit.jupiter.MockitoExtension;
+import static java.util.Comparator.reverseOrder;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -40,6 +56,27 @@ class WrapperMojoTest {
@Mock
private RepositorySystemSession repositorySystemSession;
+ @Mock
+ private MavenSession session;
+
+ @Mock
+ private MavenProject project;
+
+ @Mock
+ private List remoteRepositories;
+
+ @Mock
+ private VersionRangeResult result;
+
+ @Mock
+ private Version version1;
+
+ @Mock
+ private Version version2;
+
+ @Mock
+ private List versions;
+
@InjectMocks
private WrapperMojo wrapperMojo;
@@ -49,6 +86,76 @@ void setupMocks() {
.then(i -> i.getArguments()[1]);
}
+ @Test
+ void resolveMavenVersion() {
+ String versionRange = "versionRange";
+ String resolvedVersion1 = "resolvedVersion1";
+ String resolvedVersion2 = "resolvedVersion2";
+
+ try (MockedConstruction artifactMockedConstruction = mockConstruction((artifact, context) -> {
+ assertEquals(1, context.arguments().size());
+ assertEquals(
+ "org.apache.maven:apache-maven:" + versionRange,
+ context.arguments().get(0));
+
+ when(artifact.setVersion(resolvedVersion1)).thenReturn(artifact);
+ when(artifact.setVersion(resolvedVersion2)).thenReturn(artifact);
+ when(artifact.isSnapshot()).thenReturn(true, false);
+ });
+ MockedConstruction ignored = mockConstruction((request, context) -> {
+ assertEquals(3, context.arguments().size());
+ assertEquals(
+ artifactMockedConstruction.constructed().get(0),
+ context.arguments().get(0));
+ assertEquals(remoteRepositories, context.arguments().get(1));
+ assertEquals("wrapper", context.arguments().get(2));
+
+ when(repositorySystem.resolveVersionRange(repositorySystemSession, request))
+ .thenReturn(result);
+ })) {
+ when(session.getCurrentProject()).thenReturn(project);
+ when(project.getRemotePluginRepositories()).thenReturn(remoteRepositories);
+ when(result.getVersions()).thenReturn(versions);
+ when(versions.stream()).thenReturn(Stream.of(version1, version2));
+ when(version1.toString()).thenReturn(resolvedVersion1);
+ when(version2.toString()).thenReturn(resolvedVersion2);
+
+ assertEquals(resolvedVersion2, wrapperMojo.resolveMavenVersion(versionRange));
+
+ verify(versions).sort(reverseOrder());
+ }
+ }
+
+ @Test
+ void resolveMavenVersionException() {
+ String versionRange = "versionRange";
+
+ try (MockedConstruction artifactMockedConstruction = mockConstruction((artifact, context) -> {
+ assertEquals(1, context.arguments().size());
+ assertEquals(
+ "org.apache.maven:apache-maven:" + versionRange,
+ context.arguments().get(0));
+
+ verifyNoInteractions(artifact);
+ });
+ MockedConstruction ignored = mockConstruction((request, context) -> {
+ assertEquals(3, context.arguments().size());
+ assertEquals(
+ artifactMockedConstruction.constructed().get(0),
+ context.arguments().get(0));
+ assertEquals(remoteRepositories, context.arguments().get(1));
+ assertEquals("wrapper", context.arguments().get(2));
+
+ when(repositorySystem.resolveVersionRange(repositorySystemSession, request))
+ .thenThrow(VersionRangeResolutionException.class);
+ })) {
+ when(session.getCurrentProject()).thenReturn(project);
+ when(project.getRemotePluginRepositories()).thenReturn(remoteRepositories);
+
+ assertEquals(versionRange, wrapperMojo.resolveMavenVersion(versionRange));
+ }
+ }
+
@Test
void userSuppliedRepoUrlGetsTrailingSlashTrimmed() {
// when