diff --git a/maven-wrapper-plugin/pom.xml b/maven-wrapper-plugin/pom.xml
index 3af73fa4..034a739c 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
@@ -75,6 +75,12 @@ under the License.
plexus-io
3.6.0
+
+ org.codehaus.plexus
+ plexus-utils
+ 3.6.1
+ provided
+
org.apache.maven.resolver
maven-resolver-api
@@ -113,6 +119,11 @@ under the License.
junit-jupiter-api
test
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
org.mockito
mockito-core
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..bc768589 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
@@ -23,16 +23,24 @@
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
+import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
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.repository.metadata.Metadata;
+import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader;
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
+import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
+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;
@@ -40,6 +48,7 @@
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.archiver.UnArchiver;
import org.codehaus.plexus.components.io.fileselectors.FileSelector;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
@@ -49,6 +58,7 @@
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
+import static java.util.stream.Collectors.toList;
import static org.apache.maven.shared.utils.logging.MessageUtils.buffer;
/**
@@ -69,9 +79,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 +223,9 @@ 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");
+ String repoUrl = getRepoUrl();
+ mavenVersion =
+ resolveVersionRange(getVersion(mavenVersion, Maven.class, "org.apache.maven/maven-core"), repoUrl);
String wrapperVersion = getVersion(null, this.getClass(), "org.apache.maven.plugins/maven-wrapper-plugin");
final Artifact artifact = downloadWrapperDistribution(wrapperVersion);
@@ -219,7 +233,7 @@ public void execute() throws MojoExecutionException {
createDirectories(wrapperDir);
cleanup(wrapperDir);
unpack(artifact, baseDir);
- replaceProperties(wrapperVersion, wrapperDir);
+ replaceProperties(wrapperVersion, wrapperDir, repoUrl);
}
private String determineDistributionType(final Path wrapperDir) {
@@ -303,11 +317,11 @@ private void unpack(Artifact artifact, Path targetFolder) {
*
* @param wrapperVersion the wrapper version
* @param targetFolder the folder containing the wrapper.properties
+ * @param repoUrl the repository url
* @throws MojoExecutionException if writing fails
*/
- private void replaceProperties(String wrapperVersion, Path targetFolder) throws MojoExecutionException {
- String repoUrl = getRepoUrl();
-
+ private void replaceProperties(String wrapperVersion, Path targetFolder, String repoUrl)
+ throws MojoExecutionException {
String finalDistributionUrl;
if (distributionUrl != null && !distributionUrl.trim().isEmpty()) {
// Use custom distribution URL if provided
@@ -376,6 +390,39 @@ private String getVersion(String defaultVersion, Class> clazz, String path) {
return version;
}
+ /**
+ * Resolves the actual Maven version to download, resolving the range
+ * provided in {@link WrapperMojo#mavenVersion}
+ *
+ * @param version the provided Maven version range
+ * @param repoUrl the repository url
+ *
+ * @return the highest available version according to the provided range
+ * @throws MojoExecutionException if the provided version range is invalid
+ *
+ * @see Dependency Version Requirement Specification
+ */
+ String resolveVersionRange(String version, String repoUrl) throws MojoExecutionException {
+ try {
+ VersionRange range = VersionRange.createFromVersionSpec(version);
+ if (range.getRecommendedVersion() != null) {
+ return version;
+ }
+
+ URI uri = URI.create(repoUrl + "/org/apache/maven/apache-maven/maven-metadata.xml");
+ Metadata metadata = new MetadataXpp3Reader().read(uri.toURL().openStream());
+ List versions = metadata.getVersioning().getVersions().stream()
+ .map(DefaultArtifactVersion::new)
+ .collect(toList());
+
+ return range.matchVersion(versions).toString();
+ } catch (IOException | XmlPullParserException e) {
+ return version;
+ } catch (InvalidVersionSpecificationException e) {
+ throw new MojoExecutionException("Invalid version specification: " + version, e);
+ }
+ }
+
/**
* 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..e06686c4 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,28 +18,79 @@
*/
package org.apache.maven.plugins.wrapper;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.maven.artifact.repository.metadata.Metadata;
+import org.apache.maven.artifact.repository.metadata.Versioning;
+import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader;
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
+import org.apache.maven.artifact.versioning.VersionRange;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
+import org.mockito.MockedConstruction;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class WrapperMojoTest {
+
+ private final String repoUrl = "repoUrl";
+ private final String metadataXmlUrl = "/org/apache/maven/apache-maven/maven-metadata.xml";
+ private final String version = "version";
+
@Mock(strictness = Mock.Strictness.LENIENT)
private RepositorySystem repositorySystem;
@Mock
private RepositorySystemSession repositorySystemSession;
+ @Mock
+ private VersionRange versionRange;
+
+ @Mock
+ private ArtifactVersion artifactVersion;
+
+ @Mock
+ private URI uri;
+
+ @Mock
+ private URL url;
+
+ @Mock
+ private InputStream inputStream;
+
+ @Mock
+ private Metadata metadata;
+
+ @Mock
+ private Versioning versioning;
+
@InjectMocks
private WrapperMojo wrapperMojo;
@@ -49,6 +100,96 @@ void setupMocks() {
.then(i -> i.getArguments()[1]);
}
+ @Test
+ void resolveVersionRangeRecommendedVersion() throws InvalidVersionSpecificationException, MojoExecutionException {
+ try (MockedStatic ignored = Mockito.mockStatic()) {
+ when(VersionRange.createFromVersionSpec(version)).thenReturn(versionRange);
+ when(versionRange.getRecommendedVersion()).thenReturn(artifactVersion);
+
+ assertEquals(version, wrapperMojo.resolveVersionRange(version, repoUrl));
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("resolveVersionRangeArguments")
+ void resolveVersionRange(String version, List metadataVersions, String expectedVersion)
+ throws IOException, MojoExecutionException {
+ try (MockedStatic ignored2 = Mockito.mockStatic()) {
+ when(URI.create(repoUrl + metadataXmlUrl)).thenReturn(uri);
+ when(uri.toURL()).thenReturn(url);
+ when(url.openStream()).thenReturn(inputStream);
+
+ try (MockedConstruction ignored3 = mockConstruction(
+ (mock, context) -> when(mock.read(inputStream)).thenReturn(metadata))) {
+ when(metadata.getVersioning()).thenReturn(versioning);
+ when(versioning.getVersions()).thenReturn(metadataVersions);
+
+ assertEquals(expectedVersion, wrapperMojo.resolveVersionRange(version, repoUrl));
+ }
+ }
+ }
+
+ private static Stream resolveVersionRangeArguments() {
+ return Stream.of(
+ arguments("[1.0,2.0]", Arrays.asList("1.0.0", "1.2.3"), "1.2.3"),
+ arguments("[1.0,2.0]", Arrays.asList("1.0.0", "2.0.0"), "2.0.0"),
+ arguments("[1.0,2.0]", Arrays.asList("1.0.0", "3.0.0"), "1.0.0"),
+ arguments("[1.0,2.0-rc)", Arrays.asList("1.0.0", "2.0.0-rc-1"), "1.0.0"),
+ arguments("[1.0,2.0)", Arrays.asList("1.0.0", "2.0.0-rc-1"), "2.0.0-rc-1"),
+ arguments("[1.0,2.0]", Arrays.asList("1.0.0", "1.2.3"), "1.2.3"),
+ arguments("[1.0,2.0]", Arrays.asList("1.0.0", "1.2.3-rc-1", "1.2.3-rc-2"), "1.2.3-rc-2"),
+ arguments("[1.0,2.0]", Arrays.asList("1.0.0", "1.2.3-alpha-1", "1.2.3-rc-1"), "1.2.3-rc-1"),
+ arguments("[1.0,2.0]", Arrays.asList("1.0.0", "1.2.3-rc-1", "2.0.0"), "2.0.0"),
+ arguments("[1.0,2.0]", Arrays.asList("1.2.3-rc-1", "2.0.0-alpha-1"), "2.0.0-alpha-1"),
+ arguments("[1.0,2.0]", Arrays.asList("1.0.0", "1.2.3-rc-1", "3.0.0"), "1.2.3-rc-1"));
+ }
+
+ @Test
+ void resolveVersionRangeInvalidVersion() throws InvalidVersionSpecificationException {
+ try (MockedStatic ignored = Mockito.mockStatic()) {
+ when(VersionRange.createFromVersionSpec(version)).thenThrow(InvalidVersionSpecificationException.class);
+
+ Exception e =
+ assertThrows(MojoExecutionException.class, () -> wrapperMojo.resolveVersionRange(version, repoUrl));
+ assertEquals("Invalid version specification: " + version, e.getMessage());
+ }
+ }
+
+ @Test
+ void resolveVersionRangeIOException()
+ throws InvalidVersionSpecificationException, IOException, MojoExecutionException {
+ try (MockedStatic ignored = Mockito.mockStatic();
+ MockedStatic ignored2 = Mockito.mockStatic()) {
+ when(VersionRange.createFromVersionSpec(version)).thenReturn(versionRange);
+ when(versionRange.getRecommendedVersion()).thenReturn(null);
+
+ when(URI.create(repoUrl + metadataXmlUrl)).thenReturn(uri);
+ when(uri.toURL()).thenReturn(url);
+ when(url.openStream()).thenThrow(IOException.class);
+
+ assertEquals(version, wrapperMojo.resolveVersionRange(version, repoUrl));
+ }
+ }
+
+ @Test
+ void resolveVersionRangeXmlPullParserException()
+ throws InvalidVersionSpecificationException, IOException, MojoExecutionException {
+ try (MockedStatic ignored = Mockito.mockStatic();
+ MockedStatic ignored2 = Mockito.mockStatic()) {
+ when(VersionRange.createFromVersionSpec(version)).thenReturn(versionRange);
+ when(versionRange.getRecommendedVersion()).thenReturn(null);
+
+ when(URI.create(repoUrl + metadataXmlUrl)).thenReturn(uri);
+ when(uri.toURL()).thenReturn(url);
+ when(url.openStream()).thenReturn(inputStream);
+
+ try (MockedConstruction ignored3 = mockConstruction(
+ (mock, context) -> when(mock.read(inputStream)).thenThrow(XmlPullParserException.class))) {
+ assertEquals(version, wrapperMojo.resolveVersionRange(version, repoUrl));
+ }
+ }
+ }
+
@Test
void userSuppliedRepoUrlGetsTrailingSlashTrimmed() {
// when