From 7f8d55a112dbc3481d847a4b900f104f49b1bac1 Mon Sep 17 00:00:00 2001 From: Giulio Longfils Date: Thu, 30 Jul 2026 21:00:08 +0200 Subject: [PATCH] feat: allowing automatic Maven version resolution via version range. For instance, it's now possible to run with `-Dmaven=[3.0,4.0\)` to let the plugin resolve the highest available version in that range. --- maven-wrapper-plugin/pom.xml | 8 +- .../it/projects/mavenversion_range/pom.xml | 66 +++++++++++ .../mavenversion_range/test.properties | 18 +++ .../projects/mavenversion_range/verify.groovy | 39 +++++++ .../maven/plugins/wrapper/WrapperMojo.java | 42 ++++++- .../src/site/markdown/usage.md | 8 +- .../plugins/wrapper/WrapperMojoTest.java | 107 ++++++++++++++++++ 7 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 maven-wrapper-plugin/src/it/projects/mavenversion_range/pom.xml create mode 100644 maven-wrapper-plugin/src/it/projects/mavenversion_range/test.properties create mode 100644 maven-wrapper-plugin/src/it/projects/mavenversion_range/verify.groovy 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