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