diff --git a/.github/ci-extensions.xml b/.github/ci-extensions.xml
index 5c6a0b437805..17fa89e437f9 100644
--- a/.github/ci-extensions.xml
+++ b/.github/ci-extensions.xml
@@ -20,7 +20,7 @@ under the License.
eu.maveniverse.maven.mimir
- extension
- 0.7.8
+ extension3
+ ${env.MIMIR_VERSION}
\ No newline at end of file
diff --git a/.github/ci-mimir-daemon.properties b/.github/ci-mimir-daemon.properties
index 86a84b6ac58d..3de619d76933 100644
--- a/.github/ci-mimir-daemon.properties
+++ b/.github/ci-mimir-daemon.properties
@@ -15,7 +15,9 @@
# limitations under the License.
#
-# Mimir Daemon properties
+# Mimir Daemon config properties
-# Disable JGroups; we don't want/use LAN cache sharing
-mimir.jgroups.enabled=false
\ No newline at end of file
+# Pre-seed itself
+mimir.daemon.preSeedItself=true
+mimir.file.exclusiveAccess=true
+mimir.file.cachePurge=ON_BEGIN
diff --git a/.github/ci-mimir-session.properties b/.github/ci-mimir-session.properties
new file mode 100644
index 000000000000..5f7f9e54d2a4
--- /dev/null
+++ b/.github/ci-mimir-session.properties
@@ -0,0 +1,21 @@
+#
+# 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.
+#
+
+# Mimir Session config properties
+
+# do not waste time on this; we maintain the version
+mimir.daemon.autoupdate=false
\ No newline at end of file
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 0915790993f3..5efec65abb56 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -19,47 +19,66 @@ name: Java CI
on:
push:
- branches: [ master ]
+ branches: [ maven-4.0.x ]
pull_request:
- branches: [ master ]
+ branches: [ maven-4.0.x ]
+
+# allow single build per branch or PR
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
# clear all permissions for GITHUB_TOKEN
permissions: {}
+env:
+ MIMIR_VERSION: 0.10.6
+ MIMIR_BASEDIR: ~/.mimir
+ MIMIR_LOCAL: ~/.mimir/local
+ MAVEN_OPTS: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./target/java_heapdump.hprof
+
jobs:
initial-build:
runs-on: ubuntu-latest
steps:
- name: Set up JDK
- uses: actions/setup-java@v4
+ uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
with:
java-version: 17
distribution: 'temurin'
- name: Checkout maven
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- - name: Prepare Mimir
+ - name: Prepare Mimir for Maven 3.x
shell: bash
run: |
- mkdir -p ~/.mimir
- cp .github/ci-extensions.xml ~/.m2/extensions.xml
- cp .github/ci-mimir-daemon.properties ~/.mimir/daemon.properties
+ mkdir -p ${{ env.MIMIR_BASEDIR }}
+ cp .github/ci-mimir-session.properties ${{ env.MIMIR_BASEDIR }}/session.properties
+ cp .github/ci-mimir-daemon.properties ${{ env.MIMIR_BASEDIR }}/daemon.properties
+ cp .github/ci-extensions.xml .mvn/extensions.xml
- - name: Handle Mimir caches
- uses: actions/cache@v4
+ - name: Restore Mimir caches
+ uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
- path: ~/.mimir/local
- key: mimir-${{ runner.os }}-initial-${{ hashFiles('**/pom.xml') }}
+ path: ${{ env.MIMIR_LOCAL }}
+ key: mvn40-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
- mimir-${{ runner.os }}-initial-
- mimir-${{ runner.os }}-
+ mvn40-${{ runner.os }}-
+ mvn40-
- name: Set up Maven
shell: bash
- run: mvn --errors --batch-mode --show-version org.apache.maven.plugins:maven-wrapper-plugin:3.3.2:wrapper "-Dmaven=4.0.0-rc-3"
+ run: mvn --errors --batch-mode --show-version org.apache.maven.plugins:maven-wrapper-plugin:3.3.4:wrapper "-Dmaven=4.0.0-rc-4"
+
+ - name: Prepare Mimir for Maven 4.x
+ shell: bash
+ run: |
+ rm .mvn/extensions.xml
+ mkdir -p ~/.m2
+ cp .github/ci-extensions.xml ~/.m2/extensions.xml
- name: Build Maven distributions
shell: bash
@@ -69,14 +88,42 @@ jobs:
shell: bash
run: ls -la apache-maven/target
+ - name: Upload Mimir caches
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ if: ${{ !cancelled() && !failure() }}
+ with:
+ name: cache-${{ runner.os }}-initial
+ retention-days: 1
+ path: ${{ env.MIMIR_LOCAL }}
+
- name: Upload Maven distributions
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: maven-distributions
path: |
apache-maven/target/apache-maven*.zip
apache-maven/target/apache-maven*.tar.gz
+ - name: Upload test artifacts
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ if: ${{ failure() || cancelled() }}
+ with:
+ name: initial-logs
+ retention-days: 1
+ path: |
+ **/target/surefire-reports/*
+ **/target/java_heapdump.hprof
+
+ - name: Upload Mimir logs
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ if: always()
+ with:
+ name: initial-mimir-logs
+ include-hidden-files: true
+ retention-days: 1
+ path: |
+ ~/.mimir/*.log
+
full-build:
needs: initial-build
runs-on: ${{ matrix.os }}
@@ -84,10 +131,10 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- java: ['17', '21', '24']
+ java: ['17', '21', '25']
steps:
- name: Set up JDK ${{ matrix.java }}
- uses: actions/setup-java@v4
+ uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
@@ -105,29 +152,30 @@ jobs:
run: choco install graphviz
- name: Checkout maven
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- - name: Prepare Mimir
+ - name: Prepare Mimir for Maven 4.x
shell: bash
run: |
+ mkdir -p ${{ env.MIMIR_BASEDIR }}
+ cp .github/ci-mimir-session.properties ${{ env.MIMIR_BASEDIR }}/session.properties
+ cp .github/ci-mimir-daemon.properties ${{ env.MIMIR_BASEDIR }}/daemon.properties
mkdir -p ~/.m2
- mkdir -p ~/.mimir
cp .github/ci-extensions.xml ~/.m2/extensions.xml
- cp .github/ci-mimir-daemon.properties ~/.mimir/daemon.properties
- - name: Handle Mimir caches
- uses: actions/cache@v4
+ - name: Restore Mimir caches
+ uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
- path: ~/.mimir/local
- key: mimir-${{ runner.os }}-full-${{ hashFiles('**/pom.xml') }}
+ path: ${{ env.MIMIR_LOCAL }}
+ key: mvn40-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
- mimir-${{ runner.os }}-full-
- mimir-${{ runner.os }}-
+ mvn40-${{ runner.os }}-
+ mvn40-
- name: Download Maven distribution
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: maven-distributions
path: maven-dist
@@ -153,10 +201,6 @@ jobs:
echo "MAVEN_HOME=$PWD/maven-local" >> $GITHUB_ENV
echo "$PWD/maven-local/bin" >> $GITHUB_PATH
- - name: Show IP
- shell: bash
- run: curl --silent https://api.ipify.org
-
- name: Build with downloaded Maven
shell: bash
run: mvn verify -Papache-release -Dgpg.skip=true -e -B -V
@@ -165,12 +209,33 @@ jobs:
shell: bash
run: mvn site -e -B -V -Preporting
+ - name: Upload Mimir caches
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ if: ${{ !cancelled() && !failure() }}
+ with:
+ name: cache-${{ runner.os }}-full-build-${{ matrix.java }}
+ retention-days: 1
+ path: ${{ env.MIMIR_LOCAL }}
+
- name: Upload test artifacts
- uses: actions/upload-artifact@v4
- if: failure()
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ if: failure() || cancelled()
with:
- name: ${{ github.run_number }}-full-build-artifact-${{ runner.os }}-${{ matrix.java }}
- path: '**/target/surefire-reports/*'
+ name: full-build-logs-${{ runner.os }}-${{ matrix.java }}
+ retention-days: 1
+ path: |
+ **/target/surefire-reports/*
+ **/target/java_heapdump.hprof
+
+ - name: Upload Mimir logs
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ if: always()
+ with:
+ name: full-build-mimir-logs-${{ runner.os }}-${{ matrix.java }}
+ include-hidden-files: true
+ retention-days: 1
+ path: |
+ ~/.mimir/*.log
integration-tests:
needs: initial-build
@@ -179,38 +244,39 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
- java: ['17', '21', '24']
+ java: ['17', '21', '25']
steps:
- name: Set up JDK ${{ matrix.java }}
- uses: actions/setup-java@v4
+ uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
- name: Checkout maven
- uses: actions/checkout@v4
+ uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- - name: Prepare Mimir
+ - name: Prepare Mimir for Maven 4.x
shell: bash
run: |
+ mkdir -p ${{ env.MIMIR_BASEDIR }}
+ cp .github/ci-mimir-session.properties ${{ env.MIMIR_BASEDIR }}/session.properties
+ cp .github/ci-mimir-daemon.properties ${{ env.MIMIR_BASEDIR }}/daemon.properties
mkdir -p ~/.m2
- mkdir -p ~/.mimir
cp .github/ci-extensions.xml ~/.m2/extensions.xml
- cp .github/ci-mimir-daemon.properties ~/.mimir/daemon.properties
- - name: Handle Mimir caches
- uses: actions/cache@v4
+ - name: Restore Mimir caches
+ uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
- path: ~/.mimir/local
- key: mimir-${{ runner.os }}-its-${{ hashFiles('**/pom.xml') }}
+ path: ${{ env.MIMIR_LOCAL }}
+ key: mvn40-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
- mimir-${{ runner.os }}-its-
- mimir-${{ runner.os }}-
+ mvn40-${{ runner.os }}-
+ mvn40-
- name: Download Maven distribution
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: maven-distributions
path: maven-dist
@@ -236,17 +302,59 @@ jobs:
echo "MAVEN_HOME=$PWD/maven-local" >> $GITHUB_ENV
echo "$PWD/maven-local/bin" >> $GITHUB_PATH
- - name: Show IP
- shell: bash
- run: curl --silent https://api.ipify.org
-
- name: Build Maven and ITs and run them
shell: bash
run: mvn install -e -B -V -Prun-its,mimir
+ - name: Upload Mimir caches
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ if: ${{ !cancelled() && !failure() }}
+ with:
+ name: cache-${{ runner.os }}-integration-tests-${{ matrix.java }}
+ retention-days: 1
+ path: ${{ env.MIMIR_LOCAL }}
+
- name: Upload test artifacts
- uses: actions/upload-artifact@v4
- if: failure()
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ if: ${{ failure() || cancelled() }}
+ with:
+ name: integration-test-logs-${{ runner.os }}-${{ matrix.java }}
+ retention-days: 1
+ path: |
+ **/target/surefire-reports/*
+ **/target/failsafe-reports/*
+ ./its/core-it-suite/target/test-classes/**
+ **/target/java_heapdump.hprof
+
+ - name: Upload Mimir logs
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
+ if: always()
+ with:
+ name: integration-test-mimir-logs-${{ runner.os }}-${{ matrix.java }}
+ include-hidden-files: true
+ retention-days: 1
+ path: |
+ ~/.mimir/*.log
+
+ consolidate-caches:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ needs:
+ - full-build
+ - integration-tests
+ steps:
+ - name: Download Caches
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
+ with:
+ merge-multiple: true
+ pattern: 'cache-${{ runner.os }}*'
+ path: ${{ env.MIMIR_LOCAL }}
+ - name: Publish cache
+ uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
+ if: ${{ github.event_name != 'pull_request' && !cancelled() && !failure() }}
with:
- name: ${{ github.run_number }}-integration-test-artifact-${{ runner.os }}-${{ matrix.java }}
- path: ./its/core-it-suite/target/test-classes/
+ path: ${{ env.MIMIR_LOCAL }}
+ key: mvn40-${{ runner.os }}-${{ github.run_id }}
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
index 96eaa60a0f66..0af2d2b844c3 100644
--- a/.github/workflows/release-drafter.yml
+++ b/.github/workflows/release-drafter.yml
@@ -19,9 +19,11 @@ name: Release Drafter
on:
push:
branches:
- - master
+ - maven-4.0.x
workflow_dispatch:
jobs:
update_release_draft:
uses: apache/maven-gh-actions-shared/.github/workflows/release-drafter.yml@v4
+ with:
+ commits-since: '2025-06-18T10:29:46Z' # date of first commit on maven-4.0.x branch, bed0f8174bf728978f86fac533aa38a9511f3872
diff --git a/.idea/icon.png b/.idea/icon.png
index 55035f1855aa..e7632a98780c 100644
Binary files a/.idea/icon.png and b/.idea/icon.png differ
diff --git a/.mvn/maven.config b/.mvn/maven.config
new file mode 100644
index 000000000000..7633128e39c7
--- /dev/null
+++ b/.mvn/maven.config
@@ -0,0 +1,3 @@
+# A hack to pass on this property for Maven 3 as well; Maven 4 supports this property out of the box
+-DsessionRootDirectory=${session.rootDirectory}
+
diff --git a/Jenkinsfile b/Jenkinsfile
index 87c141ea3c27..a1bc86896e5d 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -6,45 +6,23 @@ pipeline {
options {
skipDefaultCheckout()
durabilityHint('PERFORMANCE_OPTIMIZED')
- //buildDiscarder logRotator( numToKeepStr: '60' )
disableRestartFromStage()
}
stages {
- stage("Parallel Stage") {
- parallel {
-
- stage("Build / Test - JDK17") {
- agent { node { label 'ubuntu' } }
- steps {
- timeout(time: 210, unit: 'MINUTES') {
- checkout scm
- mavenBuild("jdk_17_latest", "-Djacoco.skip=true")
- script {
- properties([buildDiscarder(logRotator(artifactNumToKeepStr: '5', numToKeepStr: env.BRANCH_NAME == 'master' ? '30' : '5'))])
- if (env.BRANCH_NAME == 'master') {
- withEnv(["JAVA_HOME=${tool "jdk_17_latest"}",
- "PATH+MAVEN=${ tool "jdk_17_latest" }/bin:${tool "maven_3_latest"}/bin",
- "MAVEN_OPTS=-Xms4G -Xmx4G -Djava.awt.headless=true"]) {
- sh "mvn clean deploy -DdeployAtEnd=true -B"
- }
- }
- }
+ stage("Build / Test - JDK17") {
+ agent { node { label 'ubuntu' } }
+ steps {
+ timeout(time: 210, unit: 'MINUTES') {
+ checkout scm
+ mavenBuild("jdk_17_latest", "")
+ script {
+ properties([buildDiscarder(logRotator(artifactNumToKeepStr: '5', numToKeepStr: isDeployedBranch() ? '30' : '5'))])
+ if (isDeployedBranch()) {
+ withEnv(["JAVA_HOME=${tool "jdk_17_latest"}",
+ "PATH+MAVEN=${ tool "jdk_17_latest" }/bin:${tool "maven_3_latest"}/bin",
+ "MAVEN_OPTS=-Xms4G -Xmx4G -Djava.awt.headless=true"]) {
+ sh "mvn clean deploy -DdeployAtEnd=true -B"
}
- }
- }
-
- stage("Build / Test - JDK21") {
- agent { node { label 'ubuntu' } }
- steps {
- timeout(time: 210, unit: 'MINUTES') {
- checkout scm
- // jacoco is definitely too slow
- mavenBuild("jdk_21_latest", "") // "-Pjacoco jacoco-aggregator:report-aggregate-all"
- // recordIssues id: "analysis-jdk17", name: "Static Analysis jdk17", aggregatingResults: true, enabledForFailure: true,
- // tools: [mavenConsole(), java(), checkStyle(), errorProne(), spotBugs(), javaDoc()],
- // skipPublishingChecks: true, skipBlames: true
- // recordCoverage id: "coverage-jdk21", name: "Coverage jdk21", tools: [[parser: 'JACOCO',pattern: 'target/site/jacoco-aggregate/jacoco.xml']],
- // sourceCodeRetention: 'MODIFIED', sourceDirectories: [[path: 'src/main/java']]
}
}
}
@@ -53,8 +31,13 @@ pipeline {
}
}
+boolean isDeployedBranch() {
+ return env.BRANCH_NAME == 'master' || env.BRANCH_NAME == 'maven-4.0.x' || env.BRANCH_NAME == 'maven-3.9.x'
+}
+
/**
* To other developers, if you are using this method above, please use the following syntax.
+ * By default this method does NOT execute ITs anymore, just "install".
*
* mavenBuild("", ""
*
@@ -65,16 +48,11 @@ def mavenBuild(jdk, extraArgs) {
script {
try {
withEnv(["JAVA_HOME=${tool "$jdk"}",
- "PATH+MAVEN=${ tool "$jdk" }/bin:${tool "maven_3_latest"}/bin",
+ "PATH+MAVEN=${tool "$jdk"}/bin:${tool "maven_3_latest"}/bin",
"MAVEN_OPTS=-Xms4G -Xmx4G -Djava.awt.headless=true"]) {
- sh "mvn --errors --batch-mode --show-version org.apache.maven.plugins:maven-wrapper-plugin:3.3.2:wrapper -Dmaven=3.9.9"
- sh "./mvnw clean install -B -U -e -DskipTests -PversionlessMavenDist -V -DdistributionTargetDir=${env.WORKSPACE}/.apache-maven-master"
- // we use two steps so that we can cache artifacts downloaded from Maven Central repository
- // without installing any local artifacts to not pollute the cache
- sh "echo package Its"
- sh "./mvnw package -DskipTests -e -B -V -Prun-its -Dmaven.repo.local=${env.WORKSPACE}/.repository/cached"
+ sh "mvn --errors --batch-mode --show-version org.apache.maven.plugins:maven-wrapper-plugin:3.3.2:wrapper -Dmaven=3.9.10"
sh "echo run Its"
- sh "./mvnw install -Pci $extraArgs -Dmaven.home=${env.WORKSPACE}/.apache-maven-master -e -B -V -Prun-its -Dmaven.repo.local=${env.WORKSPACE}/.repository/local -Dmaven.repo.local.tail=${env.WORKSPACE}/.repository/cached"
+ sh "./mvnw -e -B -V install $extraArgs"
}
}
finally {
@@ -82,4 +60,3 @@ def mavenBuild(jdk, extraArgs) {
}
}
}
-// vim: et:ts=2:sw=2:ft=groovy
diff --git a/README.md b/README.md
index 65390379828a..a60d44dde486 100644
--- a/README.md
+++ b/README.md
@@ -18,11 +18,12 @@ Apache Maven
============
[][license]
-[](https://search.maven.org/artifact/org.apache.maven/apache-maven)
-[](https://search.maven.org/artifact/org.apache.maven/apache-maven)
[](https://github.com/jvm-repo-rebuild/reproducible-central/blob/master/content/org/apache/maven/maven/README.md)
-[][build]
-[][test-results]
+- [master](https://github.com/apache/maven) = 4.1.x: [](https://central.sonatype.com/artifact/org.apache.maven/apache-maven)
+- [4.0.x](https://github.com/apache/maven/tree/maven-4.0.x): [](https://central.sonatype.com/artifact/org.apache.maven/apache-maven)
+[][build]
+[][test-results]
+- [3.9.x](https://github.com/apache/maven/tree/maven-3.9.x): [](https://central.sonatype.com/artifact/org.apache.maven/apache-maven)
Apache Maven is a software project management and comprehension tool. Based on
@@ -75,10 +76,10 @@ If you want to bootstrap Maven, you'll need:
[home]: https://maven.apache.org/
[license]: https://www.apache.org/licenses/LICENSE-2.0
-[build]: https://ci-maven.apache.org/job/Maven/job/maven-box/job/maven/job/master/
-[test-results]: https://ci-maven.apache.org/job/Maven/job/maven-box/job/maven/job/master/lastCompletedBuild/testReport/
-[build-status]: https://img.shields.io/jenkins/s/https/ci-maven.apache.org/job/Maven/job/maven-box/job/maven/job/master.svg?
-[build-tests]: https://img.shields.io/jenkins/t/https/ci-maven.apache.org/job/Maven/job/maven-box/job/maven/job/master.svg?
+[build]: https://ci-maven.apache.org/job/Maven/job/maven-box/job/maven/job/maven-4.0.x/
+[test-results]: https://ci-maven.apache.org/job/Maven/job/maven-box/job/maven/job/maven-4.0.x/lastCompletedBuild/testReport/
+[build-status]: https://img.shields.io/jenkins/s/https/ci-maven.apache.org/job/Maven/job/maven-box/job/maven/job/maven-4.0.x.svg?
+[build-tests]: https://img.shields.io/jenkins/t/https/ci-maven.apache.org/job/Maven/job/maven-box/job/maven/job/maven-4.0.x.svg?
[maven-home]: https://maven.apache.org/
[maven-download]: https://maven.apache.org/download.cgi
[users-list]: https://maven.apache.org/mailing-lists.html
diff --git a/apache-maven/pom.xml b/apache-maven/pom.xml
index acd7e41673bd..931760730dbb 100644
--- a/apache-maven/pom.xml
+++ b/apache-maven/pom.xml
@@ -23,7 +23,7 @@ under the License.
org.apache.mavenmaven
- 4.0.0-rc-4-SNAPSHOT
+ 4.0.0-SNAPSHOTapache-maven
@@ -138,19 +138,6 @@ under the License.
org.ow2.asmasm
-
-
- org.apache.maven.resolver
- maven-resolver-tools
- ${resolverVersion}
- test
-
-
- org.slf4j
- slf4j-nop
-
-
-
@@ -234,7 +221,7 @@ under the License.
eu.maveniverse.maven.pluginsbom-builder3
- 1.1.1
+ 1.3.2skinny-bom
@@ -270,38 +257,6 @@ under the License.
-
- org.codehaus.mojo
- exec-maven-plugin
- 3.5.1
-
-
- render-configuration-page
-
- java
-
- verify
-
- test
-
- ${basedir}/src/test/resources
-
- org.eclipse.aether.tools.CollectConfiguration
-
- --mode=maven
-
- --templates=maven-configuration.md,configuration.properties,configuration.yaml
- ${basedir}/..
- ${basedir}/../src/site/markdown/
-
-
-
-
-
diff --git a/apache-maven/src/assembly/component.xml b/apache-maven/src/assembly/component.xml
index 4d75c9a38ca8..5f55a310c8bd 100644
--- a/apache-maven/src/assembly/component.xml
+++ b/apache-maven/src/assembly/component.xml
@@ -68,6 +68,7 @@ under the License.
*.cmd*.conf
+ *.javados
diff --git a/apache-maven/src/assembly/maven/bin/JvmConfigParser.java b/apache-maven/src/assembly/maven/bin/JvmConfigParser.java
new file mode 100644
index 000000000000..41b87569dca1
--- /dev/null
+++ b/apache-maven/src/assembly/maven/bin/JvmConfigParser.java
@@ -0,0 +1,177 @@
+/*
+ * 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.io.IOException;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Parses .mvn/jvm.config file for Windows batch/Unix shell scripts.
+ * This avoids the complexity of parsing special characters (pipes, quotes, etc.) in scripts.
+ *
+ * Usage: java JvmConfigParser.java [output-file]
+ *
+ * If output-file is provided, writes result to that file (avoids Windows file locking issues).
+ * Otherwise, outputs to stdout.
+ *
+ * Outputs: Single line with space-separated quoted arguments (safe for batch scripts)
+ */
+public class JvmConfigParser {
+ public static void main(String[] args) {
+ if (args.length < 2 || args.length > 3) {
+ System.err.println("Usage: java JvmConfigParser.java [output-file]");
+ System.exit(1);
+ }
+
+ Path jvmConfigPath = Paths.get(args[0]);
+ String mavenProjectBasedir = args[1];
+ Path outputFile = args.length == 3 ? Paths.get(args[2]) : null;
+
+ if (!Files.exists(jvmConfigPath)) {
+ // No jvm.config file - output nothing (create empty file if output specified)
+ if (outputFile != null) {
+ try {
+ Files.writeString(outputFile, "", StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ System.err.println("ERROR: Failed to write output file: " + e.getMessage());
+ System.err.flush();
+ System.exit(1);
+ }
+ }
+ return;
+ }
+
+ try {
+ String result = parseJvmConfig(jvmConfigPath, mavenProjectBasedir);
+ if (outputFile != null) {
+ // Write directly to file - this ensures proper file handle cleanup on Windows
+ // Add newline at end for Windows 'for /f' command compatibility
+ try (Writer writer = Files.newBufferedWriter(outputFile, StandardCharsets.UTF_8)) {
+ writer.write(result);
+ if (!result.isEmpty()) {
+ writer.write(System.lineSeparator());
+ }
+ }
+ } else {
+ System.out.print(result);
+ System.out.flush();
+ }
+ } catch (IOException e) {
+ // If jvm.config exists but can't be read, this is a configuration error
+ // Print clear error and exit with error code to prevent Maven from running
+ System.err.println("ERROR: Failed to read .mvn/jvm.config: " + e.getMessage());
+ System.err.println("Please check file permissions and syntax.");
+ System.err.flush();
+ System.exit(1);
+ }
+ }
+
+ /**
+ * Parse jvm.config file and return formatted arguments.
+ * Package-private for testing.
+ */
+ static String parseJvmConfig(Path jvmConfigPath, String mavenProjectBasedir) throws IOException {
+ StringBuilder result = new StringBuilder();
+
+ for (String line : Files.readAllLines(jvmConfigPath, StandardCharsets.UTF_8)) {
+ line = processLine(line, mavenProjectBasedir);
+ if (line.isEmpty()) {
+ continue;
+ }
+
+ List parsed = parseArguments(line);
+ appendQuotedArguments(result, parsed);
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * Process a single line: remove comments, trim whitespace, and replace placeholders.
+ */
+ private static String processLine(String line, String mavenProjectBasedir) {
+ // Remove comments
+ int commentIndex = line.indexOf('#');
+ if (commentIndex >= 0) {
+ line = line.substring(0, commentIndex);
+ }
+
+ // Trim whitespace
+ line = line.trim();
+
+ // Replace MAVEN_PROJECTBASEDIR placeholders
+ line = line.replace("${MAVEN_PROJECTBASEDIR}", mavenProjectBasedir);
+ line = line.replace("$MAVEN_PROJECTBASEDIR", mavenProjectBasedir);
+
+ return line;
+ }
+
+ /**
+ * Append parsed arguments as quoted strings to the result builder.
+ */
+ private static void appendQuotedArguments(StringBuilder result, List args) {
+ for (String arg : args) {
+ if (result.length() > 0) {
+ result.append(' ');
+ }
+ result.append('"').append(arg).append('"');
+ }
+ }
+
+ /**
+ * Parse a line into individual arguments, respecting quoted strings.
+ * Quotes are stripped from the arguments.
+ */
+ private static List parseArguments(String line) {
+ List args = new ArrayList<>();
+ StringBuilder current = new StringBuilder();
+ boolean inDoubleQuotes = false;
+ boolean inSingleQuotes = false;
+
+ for (int i = 0; i < line.length(); i++) {
+ char c = line.charAt(i);
+
+ if (c == '"' && !inSingleQuotes) {
+ inDoubleQuotes = !inDoubleQuotes;
+ } else if (c == '\'' && !inDoubleQuotes) {
+ inSingleQuotes = !inSingleQuotes;
+ } else if (c == ' ' && !inDoubleQuotes && !inSingleQuotes) {
+ // Space outside quotes - end of argument
+ if (current.length() > 0) {
+ args.add(current.toString());
+ current.setLength(0);
+ }
+ } else {
+ current.append(c);
+ }
+ }
+
+ // Add last argument
+ if (current.length() > 0) {
+ args.add(current.toString());
+ }
+
+ return args;
+ }
+}
\ No newline at end of file
diff --git a/apache-maven/src/assembly/maven/bin/m2.conf b/apache-maven/src/assembly/maven/bin/m2.conf
index b1df7f0934b0..b91431dea5f9 100644
--- a/apache-maven/src/assembly/maven/bin/m2.conf
+++ b/apache-maven/src/assembly/maven/bin/m2.conf
@@ -16,6 +16,8 @@
# specific language governing permissions and limitations
# under the License.
+set maven.mainClass default org.apache.maven.cling.MavenCling
+
main is ${maven.mainClass} from plexus.core
set maven.conf default ${maven.home}/conf
diff --git a/apache-maven/src/assembly/maven/bin/mvn b/apache-maven/src/assembly/maven/bin/mvn
index 59cb66a9cc07..1a8e6a2fdccc 100755
--- a/apache-maven/src/assembly/maven/bin/mvn
+++ b/apache-maven/src/assembly/maven/bin/mvn
@@ -166,32 +166,66 @@ find_file_argument_basedir() {
}
# concatenates all lines of a file and replaces variables
+# Uses Java-based parser to handle all special characters correctly
+# This avoids shell parsing issues with pipes, quotes, @, and other special characters
+# and ensures POSIX compliance (no xargs -0, awk, or complex sed needed)
+# Set MAVEN_DEBUG_SCRIPT=1 to enable debug logging
concat_lines() {
if [ -f "$1" ]; then
- # First convert all CR to LF using tr
- tr '\r' '\n' < "$1" | \
- sed -e '/^$/d' -e 's/#.*$//' | \
- # Split into words and process each argument
- xargs -n 1 | \
- while read -r arg; do
- # Replace variables first
- arg=$(echo "$arg" | sed \
- -e "s@\${MAVEN_PROJECTBASEDIR}@$MAVEN_PROJECTBASEDIR@g" \
- -e "s@\$MAVEN_PROJECTBASEDIR@$MAVEN_PROJECTBASEDIR@g")
-
- # Add quotes only if argument contains spaces and isn't already quoted
- if echo "$arg" | grep -q " " && ! echo "$arg" | grep -q "^\".*\"$"; then
- echo "\"$arg\""
- else
- echo "$arg"
- fi
- done | \
- tr '\n' ' '
+ # Use Java source-launch mode (JDK 11+) to run JvmConfigParser directly
+ # This avoids the need for compilation and temporary directories
+
+ # Debug logging
+ if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
+ echo "[DEBUG] Found jvm.config file at: $1" >&2
+ echo "[DEBUG] Running JvmConfigParser with Java: $JAVACMD" >&2
+ echo "[DEBUG] Parser arguments: $MAVEN_HOME/bin/JvmConfigParser.java $1 $MAVEN_PROJECTBASEDIR" >&2
+ fi
+
+ # Verify Java is available
+ "$JAVACMD" -version >/dev/null 2>&1 || {
+ echo "Error: Java not found. Please set JAVA_HOME." >&2
+ return 1
+ }
+
+ # Run the parser using source-launch mode
+ # Capture both stdout and stderr for comprehensive error reporting
+ parser_output=$("$JAVACMD" "$MAVEN_HOME/bin/JvmConfigParser.java" "$1" "$MAVEN_PROJECTBASEDIR" 2>&1)
+ parser_exit=$?
+
+ if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
+ echo "[DEBUG] JvmConfigParser exit code: $parser_exit" >&2
+ echo "[DEBUG] JvmConfigParser output: $parser_output" >&2
+ fi
+
+ if [ $parser_exit -ne 0 ]; then
+ # Parser failed - print comprehensive error information
+ echo "ERROR: JvmConfigParser failed with exit code $parser_exit" >&2
+ echo " jvm.config path: $1" >&2
+ echo " Maven basedir: $MAVEN_PROJECTBASEDIR" >&2
+ echo " Java command: $JAVACMD" >&2
+ echo " Parser output:" >&2
+ echo "$parser_output" | sed 's/^/ /' >&2
+ exit 1
+ fi
+
+ echo "$parser_output"
fi
}
MAVEN_PROJECTBASEDIR="`find_maven_basedir "$@"`"
-MAVEN_OPTS="$MAVEN_OPTS `concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config"`"
+# Read JVM config and append to MAVEN_OPTS, preserving special characters
+_jvm_config="`concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config"`"
+if [ -n "$_jvm_config" ]; then
+ if [ -n "$MAVEN_OPTS" ]; then
+ MAVEN_OPTS="$MAVEN_OPTS $_jvm_config"
+ else
+ MAVEN_OPTS="$_jvm_config"
+ fi
+fi
+if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
+ echo "[DEBUG] Final MAVEN_OPTS: $MAVEN_OPTS" >&2
+fi
LAUNCHER_JAR=`echo "$MAVEN_HOME"/boot/plexus-classworlds-*.jar`
LAUNCHER_CLASS=org.codehaus.plexus.classworlds.launcher.Launcher
@@ -241,6 +275,7 @@ handle_args() {
handle_args "$@"
MAVEN_MAIN_CLASS=${MAVEN_MAIN_CLASS:=org.apache.maven.cling.MavenCling}
+# Build command string for eval
cmd="\"$JAVACMD\" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
@@ -253,13 +288,15 @@ cmd="\"$JAVACMD\" \
\"-Dmaven.multiModuleProjectDirectory=$MAVEN_PROJECTBASEDIR\" \
$LAUNCHER_CLASS \
$MAVEN_ARGS"
+
# Add remaining arguments with proper quoting
for arg in "$@"; do
cmd="$cmd \"$arg\""
done
-# Debug: print the command that will be executed
-#echo "About to execute:"
-#echo "$cmd"
+if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
+ echo "[DEBUG] Launching JVM with command:" >&2
+ echo "[DEBUG] $cmd" >&2
+fi
eval exec "$cmd"
diff --git a/apache-maven/src/assembly/maven/bin/mvn.cmd b/apache-maven/src/assembly/maven/bin/mvn.cmd
index 1d50c0ec323a..f25f85858f7a 100644
--- a/apache-maven/src/assembly/maven/bin/mvn.cmd
+++ b/apache-maven/src/assembly/maven/bin/mvn.cmd
@@ -35,6 +35,10 @@ title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%"=="on" echo %MAVEN_BATCH_ECHO%
+@REM Clear/define a variable for any options to be inserted via script
+@REM We want to avoid trying to parse the external MAVEN_OPTS variable
+SET INTERNAL_MAVEN_OPTS=
+
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%"=="" goto skipRc
if exist "%PROGRAMDATA%\mavenrc.cmd" call "%PROGRAMDATA%\mavenrc.cmd" %*
@@ -173,38 +177,57 @@ cd /d "%EXEC_DIR%"
:endDetectBaseDir
+rem Initialize JVM_CONFIG_MAVEN_OPTS to empty to avoid inheriting from environment
+set JVM_CONFIG_MAVEN_OPTS=
+
if not exist "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadJvmConfig
-@setlocal EnableExtensions EnableDelayedExpansion
-set JVM_CONFIG_MAVEN_OPTS=
-for /F "usebackq tokens=* delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do (
- set "line=%%a"
-
- rem Skip empty lines and full-line comments
- echo !line! | findstr /b /r /c:"[ ]*#" >nul
- if errorlevel 1 (
- rem Handle end-of-line comments by taking everything before #
- for /f "tokens=1* delims=#" %%i in ("!line!") do set "line=%%i"
-
- rem Trim leading/trailing spaces while preserving spaces in quotes
- set "trimmed=!line!"
- for /f "tokens=* delims= " %%i in ("!trimmed!") do set "trimmed=%%i"
- for /l %%i in (1,1,100) do if "!trimmed:~-1!"==" " set "trimmed=!trimmed:~0,-1!"
-
- rem Replace MAVEN_PROJECTBASEDIR placeholders
- set "trimmed=!trimmed:${MAVEN_PROJECTBASEDIR}=%MAVEN_PROJECTBASEDIR%!"
- set "trimmed=!trimmed:$MAVEN_PROJECTBASEDIR=%MAVEN_PROJECTBASEDIR%!"
-
- if not "!trimmed!"=="" (
- if "!JVM_CONFIG_MAVEN_OPTS!"=="" (
- set "JVM_CONFIG_MAVEN_OPTS=!trimmed!"
- ) else (
- set "JVM_CONFIG_MAVEN_OPTS=!JVM_CONFIG_MAVEN_OPTS! !trimmed!"
- )
- )
- )
+rem Use Java source-launch mode (JDK 11+) to parse jvm.config
+rem This avoids batch script parsing issues with special characters (pipes, quotes, @, etc.)
+rem Use temp file approach with cmd /c to ensure proper file handle release
+
+set "JVM_CONFIG_TEMP=%TEMP%\mvn-jvm-config-%RANDOM%-%RANDOM%.txt"
+
+rem Debug logging (set MAVEN_DEBUG_SCRIPT=1 to enable)
+if defined MAVEN_DEBUG_SCRIPT (
+ echo [DEBUG] Found .mvn\jvm.config file at: %MAVEN_PROJECTBASEDIR%\.mvn\jvm.config
+ echo [DEBUG] Using temp file: %JVM_CONFIG_TEMP%
+ echo [DEBUG] Running JvmConfigParser with Java: %JAVACMD%
+ echo [DEBUG] Parser arguments: "%MAVEN_HOME%\bin\JvmConfigParser.java" "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" "%MAVEN_PROJECTBASEDIR%" "%JVM_CONFIG_TEMP%"
+)
+
+rem Run parser with output file as third argument - Java writes directly to file to avoid Windows file locking issues
+"%JAVACMD%" "%MAVEN_HOME%\bin\JvmConfigParser.java" "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" "%MAVEN_PROJECTBASEDIR%" "%JVM_CONFIG_TEMP%"
+set JVM_CONFIG_EXIT=%ERRORLEVEL%
+
+if defined MAVEN_DEBUG_SCRIPT (
+ echo [DEBUG] JvmConfigParser exit code: %JVM_CONFIG_EXIT%
+)
+
+rem Check if parser failed
+if %JVM_CONFIG_EXIT% neq 0 (
+ echo ERROR: Failed to parse .mvn/jvm.config file 1>&2
+ echo jvm.config path: %MAVEN_PROJECTBASEDIR%\.mvn\jvm.config 1>&2
+ echo Java command: %JAVACMD% 1>&2
+ if exist "%JVM_CONFIG_TEMP%" (
+ del "%JVM_CONFIG_TEMP%" 2>nul
+ )
+ exit /b 1
+)
+
+rem Read the output file
+if exist "%JVM_CONFIG_TEMP%" (
+ if defined MAVEN_DEBUG_SCRIPT (
+ echo [DEBUG] Temp file contents:
+ type "%JVM_CONFIG_TEMP%"
+ )
+ for /f "usebackq tokens=*" %%i in ("%JVM_CONFIG_TEMP%") do set "JVM_CONFIG_MAVEN_OPTS=%%i"
+ del "%JVM_CONFIG_TEMP%" 2>nul
+)
+
+if defined MAVEN_DEBUG_SCRIPT (
+ echo [DEBUG] Final JVM_CONFIG_MAVEN_OPTS: %JVM_CONFIG_MAVEN_OPTS%
)
-@endlocal & set "MAVEN_OPTS=%MAVEN_OPTS% %JVM_CONFIG_MAVEN_OPTS%"
:endReadJvmConfig
@@ -224,7 +247,7 @@ if "%~1"=="--debug" (
echo Error: Unable to autodetect the YJP library location. Please set YJPLIB variable >&2
exit /b 1
)
- set "MAVEN_OPTS=-agentpath:%YJPLIB%=onexit=snapshot,onexit=memory,tracing,onlylocal %MAVEN_OPTS%"
+ set "INTERNAL_MAVEN_OPTS=-agentpath:%YJPLIB%=onexit=snapshot,onexit=memory,tracing,onlylocal %INTERNAL_MAVEN_OPTS%"
) else if "%~1"=="--enc" (
set "MAVEN_MAIN_CLASS=org.apache.maven.cling.MavenEncCling"
) else if "%~1"=="--shell" (
@@ -247,8 +270,15 @@ for %%i in ("%MAVEN_HOME%"\boot\plexus-classworlds-*) do set LAUNCHER_JAR="%%i"
set LAUNCHER_CLASS=org.codehaus.plexus.classworlds.launcher.Launcher
if "%MAVEN_MAIN_CLASS%"=="" @set MAVEN_MAIN_CLASS=org.apache.maven.cling.MavenCling
+if defined MAVEN_DEBUG_SCRIPT (
+ echo [DEBUG] Launching JVM with command:
+ echo [DEBUG] "%JAVACMD%" %INTERNAL_MAVEN_OPTS% %MAVEN_OPTS% %JVM_CONFIG_MAVEN_OPTS% %MAVEN_DEBUG_OPTS% --enable-native-access=ALL-UNNAMED -classpath %LAUNCHER_JAR% "-Dclassworlds.conf=%CLASSWORLDS_CONF%" "-Dmaven.home=%MAVEN_HOME%" "-Dmaven.mainClass=%MAVEN_MAIN_CLASS%" "-Dlibrary.jline.path=%MAVEN_HOME%\lib\jline-native" "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %LAUNCHER_CLASS% %MAVEN_ARGS% %*
+)
+
"%JAVACMD%" ^
+ %INTERNAL_MAVEN_OPTS% ^
%MAVEN_OPTS% ^
+ %JVM_CONFIG_MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
--enable-native-access=ALL-UNNAMED ^
-classpath %LAUNCHER_JAR% ^
@@ -280,4 +310,4 @@ if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
-exit /b %ERROR_CODE%
+exit /b %ERROR_CODE%
\ No newline at end of file
diff --git a/apache-maven/src/assembly/maven/conf/maven.properties b/apache-maven/src/assembly/maven/conf/maven-system.properties
similarity index 91%
rename from apache-maven/src/assembly/maven/conf/maven.properties
rename to apache-maven/src/assembly/maven/conf/maven-system.properties
index 1e53fa5df399..49466972a817 100644
--- a/apache-maven/src/assembly/maven/conf/maven.properties
+++ b/apache-maven/src/assembly/maven/conf/maven-system.properties
@@ -18,10 +18,10 @@
#
#
-# Maven user properties
+# Maven system properties
#
# The properties defined in this file will be made available through
-# user properties at the very beginning of Maven's boot process.
+# system properties at the very beginning of Maven's boot process.
#
maven.installation.conf = ${maven.home}/conf
@@ -31,8 +31,8 @@ maven.project.conf = ${session.rootDirectory}/.mvn
# Comma-separated list of files to include.
# Each item may be enclosed in quotes to gracefully include spaces. Items are trimmed before being loaded.
# If the first character of an item is a question mark, the load will silently fail if the file does not exist.
-${includes} = ?"${maven.user.conf}/maven.properties", \
- ?"${maven.project.conf}/maven.properties"
+${includes} = ?"${maven.user.conf}/maven-system.properties", \
+ ?"${maven.project.conf}/maven-system.properties"
#
# Settings
diff --git a/apache-maven/src/assembly/maven/conf/maven-user.properties b/apache-maven/src/assembly/maven/conf/maven-user.properties
new file mode 100644
index 000000000000..b218c3d13801
--- /dev/null
+++ b/apache-maven/src/assembly/maven/conf/maven-user.properties
@@ -0,0 +1,34 @@
+#
+# 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 user properties
+#
+# The properties defined in this file will be made available through
+# user properties at the very beginning of Maven's boot process.
+#
+
+# Comma-separated list of files to include.
+# Each item may be enclosed in quotes to gracefully include spaces. Items are trimmed before being loaded.
+# If the first character of an item is a question mark, the load will silently fail if the file does not exist.
+# Note: the maven.properties will be removed in Maven 4.1.0 and only maven-user.properties will be loaded!
+${includes} = ?"${maven.user.conf}/maven-user.properties", \
+ ?"${maven.user.conf}/maven.properties", \
+ ?"${maven.project.conf}/maven-user.properties", \
+ ?"${maven.project.conf}/maven.properties",
diff --git a/api/maven-api-annotations/pom.xml b/api/maven-api-annotations/pom.xml
index b919d04268e6..40872a8f74bb 100644
--- a/api/maven-api-annotations/pom.xml
+++ b/api/maven-api-annotations/pom.xml
@@ -23,7 +23,7 @@
org.apache.mavenmaven-api
- 4.0.0-rc-4-SNAPSHOT
+ 4.0.0-SNAPSHOTmaven-api-annotations
diff --git a/api/maven-api-annotations/src/main/java/org/apache/maven/api/annotations/Config.java b/api/maven-api-annotations/src/main/java/org/apache/maven/api/annotations/Config.java
index 493b6ceebf35..6fe290478702 100644
--- a/api/maven-api-annotations/src/main/java/org/apache/maven/api/annotations/Config.java
+++ b/api/maven-api-annotations/src/main/java/org/apache/maven/api/annotations/Config.java
@@ -80,14 +80,14 @@
enum Source {
/**
* Maven system properties. These properties are evaluated very early during the boot process,
- * typically set by Maven itself and flagged as readOnly=true. System properties are initialized
- * before the build starts and are available throughout the entire Maven execution. They are used
- * for core Maven functionality that needs to be established at startup.
+ * typically set by Maven itself and flagged as readOnly=true or by users via maven-system.properties files.
+ * System properties are initialized before the build starts and are available throughout the entire Maven
+ * execution. They are used for core Maven functionality that needs to be established at startup.
*/
SYSTEM_PROPERTIES,
/**
* Maven user properties. These are properties that users configure through various means such as
- * maven.properties files, maven.config files, command line parameters (-D flags), settings.xml,
+ * maven-user.properties files, maven.config files, command line parameters (-D flags), settings.xml,
* or environment variables. They are evaluated during the build process and represent the primary
* way for users to customize Maven's behavior at runtime.
*/
diff --git a/api/maven-api-cli/pom.xml b/api/maven-api-cli/pom.xml
index 92517b0c99c8..509f0dad6bd7 100644
--- a/api/maven-api-cli/pom.xml
+++ b/api/maven-api-cli/pom.xml
@@ -23,7 +23,7 @@
org.apache.mavenmaven-api
- 4.0.0-rc-4-SNAPSHOT
+ 4.0.0-SNAPSHOTmaven-api-cli
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerException.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerException.java
index d1a479b71127..4fbc41c0e072 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerException.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerException.java
@@ -58,7 +58,7 @@ public static final class ExitException extends InvokerException {
private final int exitCode;
public ExitException(int exitCode) {
- super("EXIT");
+ super("EXIT=" + exitCode);
this.exitCode = exitCode;
}
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Logger.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Logger.java
index cd9aaff994e0..7d5d2aebb581 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Logger.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/Logger.java
@@ -148,7 +148,10 @@ default void error(@Nonnull String message, @Nullable Throwable error) {
* @param message The logging message, never {@code null}.
* @param error The error, if applicable.
*/
- record Entry(@Nonnull Level level, @Nonnull String message, @Nullable Throwable error) {}
+ record Entry(
+ @Nonnull Level level,
+ @Nonnull String message,
+ @Nullable Throwable error) {}
/**
* If this is an accumulating log, it will "drain" this instance. It returns the accumulated log entries, and
diff --git a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnup/UpgradeOptions.java b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnup/UpgradeOptions.java
index 47d8a67cbec1..96aa7a99fb5a 100644
--- a/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnup/UpgradeOptions.java
+++ b/api/maven-api-cli/src/main/java/org/apache/maven/api/cli/mvnup/UpgradeOptions.java
@@ -33,20 +33,6 @@
*/
@Experimental
public interface UpgradeOptions extends Options {
- /**
- * Should the operation be forced (ie overwrite existing files, if any).
- *
- * @return an {@link Optional} containing the boolean value {@code true} if specified, or empty
- */
- Optional force();
-
- /**
- * Should imply "yes" to all questions.
- *
- * @return an {@link Optional} containing the boolean value {@code true} if specified, or empty
- */
- Optional yes();
-
/**
* Returns the list of upgrade goals to be executed.
* These goals can include operations like "check", "dependencies", "plugins", etc.
diff --git a/api/maven-api-cli/src/site/apt/index.apt b/api/maven-api-cli/src/site/apt/index.apt
new file mode 100644
index 000000000000..8d901b352531
--- /dev/null
+++ b/api/maven-api-cli/src/site/apt/index.apt
@@ -0,0 +1,41 @@
+~~ 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.
+
+ -----
+ Introduction
+ -----
+ Hervé Boutemy
+ -----
+ 2025-11-16
+ -----
+
+Maven 4 API - CLI
+
+ This is the {{{./apidocs/org/apache/maven/api/cli/package-summary.html}API}} for Maven's command-line interface and
+ tools:
+
+ * <<<{{{./apidocs/org/apache/maven/api/cli/mvn/package-summary.html}mvn}}>>>, the Maven build tool,
+
+ * <<<{{{./apidocs/org/apache/maven/api/cli/mvnenc/package-summary.html}mvnenc}}>>>, the Maven Password Encryption tool,
+
+ * <<<{{{./apidocs/org/apache/maven/api/cli/mvnsh/package-summary.html}mvnsh}}>>>, the Maven Shell tool,
+
+ * <<<{{{./apidocs/org/apache/maven/api/cli/mvnup/package-summary.html}mvnup}}>>>, the Maven Upgrade tool.
+
+ This API also defines {{{./core-extensions.html}Core Extensions model}} for <<<.mvn/extensions.xml>>>.
+
+ See also associated {{{../../impl/maven-cli/index.html}implementation}}.
\ No newline at end of file
diff --git a/api/maven-api-cli/src/site/site.xml b/api/maven-api-cli/src/site/site.xml
new file mode 100644
index 000000000000..6a599fce2b80
--- /dev/null
+++ b/api/maven-api-cli/src/site/site.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+ ${project.scm.url}
+
+
+
+
+
+
+
+
diff --git a/api/maven-api-core/pom.xml b/api/maven-api-core/pom.xml
index 72919d8e327d..104d6375b7a0 100644
--- a/api/maven-api-core/pom.xml
+++ b/api/maven-api-core/pom.xml
@@ -23,7 +23,7 @@
org.apache.mavenmaven-api
- 4.0.0-rc-4-SNAPSHOT
+ 4.0.0-SNAPSHOTmaven-api-core
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
index 745e2c54146b..156366b6e328 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java
@@ -462,6 +462,18 @@ public final class Constants {
@Config(type = "java.lang.Boolean", defaultValue = "true")
public static final String MAVEN_CONSUMER_POM = "maven.consumer.pom";
+ /**
+ * User property for controlling consumer POM flattening behavior.
+ * When set to true (default), consumer POMs are flattened by removing
+ * dependency management and keeping only direct dependencies with transitive scopes.
+ * When set to false, consumer POMs preserve dependency management
+ * like parent POMs, allowing dependency management to be inherited by consumers.
+ *
+ * @since 4.1.0
+ */
+ @Config(type = "java.lang.Boolean", defaultValue = "false")
+ public static final String MAVEN_CONSUMER_POM_FLATTEN = "maven.consumer.pom.flatten";
+
/**
* User property for controlling "maven personality". If activated Maven will behave
* as previous major version, Maven 3.
@@ -510,6 +522,35 @@ public final class Constants {
@Config(type = "java.lang.Integer", defaultValue = "100")
public static final String MAVEN_BUILDER_MAX_PROBLEMS = "maven.builder.maxProblems";
+ /**
+ * Configuration property for version range resolution used metadata "nature".
+ * It may contain following string values:
+ *
+ *
"auto" - decision done based on range being resolver: if any boundary is snapshot, use "release_or_snapshot", otherwise "release"
+ *
"release_or_snapshot" - the default
+ *
"release" - query only release repositories to discover versions
+ *
"snapshot" - query only snapshot repositories to discover versions
+ *
+ * Default (when unset) is using request carried nature. Hence, this configuration really makes sense with value
+ * {@code "auto"}, while ideally callers needs update and use newly added method on version range request to
+ * express preference.
+ *
+ * @since 4.0.0
+ */
+ @Config(defaultValue = "release_or_snapshot")
+ public static final String MAVEN_VERSION_RANGE_RESOLVER_NATURE_OVERRIDE =
+ "maven.versionRangeResolver.natureOverride";
+
+ /**
+ * Comma-separated list of XML contexts/fields to intern during POM parsing for memory optimization.
+ * When not specified, a default set of commonly repeated contexts will be used.
+ * Example: "groupId,artifactId,version,scope,type"
+ *
+ * @since 4.0.0
+ */
+ @Config
+ public static final String MAVEN_MODEL_BUILDER_INTERNS = "maven.modelBuilder.interns";
+
/**
* All system properties used by Maven Logger start with this prefix.
*
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/JavaPathType.java b/api/maven-api-core/src/main/java/org/apache/maven/api/JavaPathType.java
index 7c11ec36daca..7730c1968155 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/JavaPathType.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/JavaPathType.java
@@ -281,7 +281,7 @@ final String[] format(String moduleName, Iterable extends Path> paths) {
*/
@Override
public String toString() {
- return "PathType[" + id() + "]";
+ return "PathType[" + id() + ']';
}
/**
@@ -325,7 +325,7 @@ public JavaPathType rawType() {
*/
@Override
public String id() {
- return JavaPathType.this.name() + ":" + moduleName;
+ return JavaPathType.this.name() + ':' + moduleName;
}
/**
@@ -376,6 +376,25 @@ public String[] option(Iterable extends Path> paths) {
return format(moduleName, paths);
}
+ /**
+ * {@return a hash code value based on the raw type and module name}.
+ */
+ @Override
+ public int hashCode() {
+ return rawType().hashCode() + 17 * moduleName.hashCode();
+ }
+
+ /**
+ * {@return whether the given object represents the same type of path as this object}.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof Modular m) {
+ return rawType() == m.rawType() && moduleName.equals(m.moduleName);
+ }
+ return false;
+ }
+
/**
* Returns the programmatic name of this path type, including the module to patch.
* For example, if this type was created by {@code JavaPathType.patchModule("foo.bar")},
@@ -386,7 +405,7 @@ public String[] option(Iterable extends Path> paths) {
@Nonnull
@Override
public String toString() {
- return "PathType[" + id() + "]";
+ return "PathType[" + id() + ']';
}
}
}
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Project.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Project.java
index 8e989ad4ae98..22115c357234 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/Project.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Project.java
@@ -24,6 +24,7 @@
import org.apache.maven.api.annotations.Experimental;
import org.apache.maven.api.annotations.Nonnull;
+import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.model.Build;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Profile;
@@ -172,6 +173,125 @@ default Build getBuild() {
@Nonnull
Path getBasedir();
+ /**
+ * {@return the absolute path to the directory where files generated by the build are placed}
+ *
+ * Purpose: This method provides the base output directory for a given scope,
+ * which serves as the destination for compiled classes, processed resources, and other generated files.
+ * The returned path is always absolute.
+ *
+ *
+ * Scope-based Directory Resolution:
+ *
+ *
+ *
Output Directory by Scope
+ *
+ *
+ *
Scope Parameter
+ *
Build Configuration
+ *
Typical Path
+ *
Contents
+ *
+ *
+ *
+ *
+ *
{@link ProjectScope#MAIN}
+ *
{@code build.getOutputDirectory()}
+ *
{@code target/classes}
+ *
Compiled application classes and processed main resources
+ *
+ *
+ *
{@link ProjectScope#TEST}
+ *
{@code build.getTestOutputDirectory()}
+ *
{@code target/test-classes}
+ *
Compiled test classes and processed test resources
+ *
+ *
+ *
{@code null} or other
+ *
{@code build.getDirectory()}
+ *
{@code target}
+ *
Parent directory for all build outputs
+ *
+ *
+ *
+ *
+ * Role in {@link SourceRoot} Path Resolution:
+ *
+ *
+ * This method is the foundation for {@link SourceRoot#targetPath(Project)} path resolution.
+ * When a {@link SourceRoot} has a relative {@code targetPath}, it is resolved against the
+ * output directory returned by this method for the source root's scope. This ensures that:
+ *
+ *
+ *
Main resources with {@code targetPath="META-INF"} are copied to {@code target/classes/META-INF}
+ *
Test resources with {@code targetPath="test-data"} are copied to {@code target/test-classes/test-data}
+ *
Resources without an explicit {@code targetPath} are copied to the root of the output directory
+ *
+ *
+ * Maven 3 Compatibility:
+ *
+ *
+ * This behavior maintains the Maven 3.x semantic where resource {@code targetPath} elements
+ * are resolved relative to the appropriate output directory ({@code project.build.outputDirectory}
+ * or {@code project.build.testOutputDirectory}), not the project base directory.
+ *
+ *
+ * In Maven 3, when a resource configuration specifies:
+ *
+ * The maven-resources-plugin resolves {@code targetPath} as:
+ * {@code project.build.outputDirectory + "/" + targetPath}, which results in
+ * {@code target/classes/META-INF/resources}. This method provides the same base directory
+ * ({@code target/classes}) for Maven 4 API consumers.
+ *
+ *
+ * @param scope the scope of the generated files for which to get the directory, or {@code null} for the build directory
+ * @return the absolute path to the output directory for the given scope
+ *
+ * @see SourceRoot#targetPath(Project)
+ * @see SourceRoot#targetPath()
+ * @see Build#getOutputDirectory()
+ * @see Build#getTestOutputDirectory()
+ * @see Build#getDirectory()
+ */
+ @Nonnull
+ default Path getOutputDirectory(@Nullable ProjectScope scope) {
+ String dir;
+ Build build = getBuild();
+ if (scope == ProjectScope.MAIN) {
+ dir = build.getOutputDirectory();
+ } else if (scope == ProjectScope.TEST) {
+ dir = build.getTestOutputDirectory();
+ } else {
+ dir = build.getDirectory();
+ }
+ return getBasedir().resolve(dir);
+ }
+
/**
* {@return the project direct dependencies (directly specified or inherited)}.
*/
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/ProtoSession.java b/api/maven-api-core/src/main/java/org/apache/maven/api/ProtoSession.java
index 4b986da8d356..41300d074e63 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/ProtoSession.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/ProtoSession.java
@@ -58,6 +58,12 @@ public interface ProtoSession {
@Nonnull
Map getSystemProperties();
+ /**
+ * Returns the properly overlaid map of properties: system + user.
+ */
+ @Nonnull
+ Map getEffectiveProperties();
+
/**
* Returns the start time of the session.
*
@@ -163,6 +169,7 @@ public ProtoSession build() {
private static class Impl implements ProtoSession {
private final Map userProperties;
private final Map systemProperties;
+ private final Map effectiveProperties;
private final Instant startTime;
private final Path topDirectory;
private final Path rootDirectory;
@@ -173,8 +180,11 @@ private Impl(
Instant startTime,
Path topDirectory,
Path rootDirectory) {
- this.userProperties = requireNonNull(userProperties);
- this.systemProperties = requireNonNull(systemProperties);
+ this.userProperties = Map.copyOf(userProperties);
+ this.systemProperties = Map.copyOf(systemProperties);
+ Map cp = new HashMap<>(systemProperties);
+ cp.putAll(userProperties);
+ this.effectiveProperties = Map.copyOf(cp);
this.startTime = requireNonNull(startTime);
this.topDirectory = requireNonNull(topDirectory);
this.rootDirectory = rootDirectory;
@@ -190,6 +200,11 @@ public Map getSystemProperties() {
return systemProperties;
}
+ @Override
+ public Map getEffectiveProperties() {
+ return effectiveProperties;
+ }
+
@Override
public Instant getStartTime() {
return startTime;
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java b/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java
index 3f43210dc0f2..8ef3802062ea 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/Session.java
@@ -93,6 +93,15 @@ public interface Session extends ProtoSession {
@Nonnull
SessionData getData();
+ /**
+ * Default implementation at {@link ProtoSession} level, as the notion of project
+ * does not exist there.
+ */
+ @Nonnull
+ default Map getEffectiveProperties() {
+ return getEffectiveProperties(null);
+ }
+
/**
* Each invocation computes a new map of effective properties. To be used in interpolation.
*
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/SourceRoot.java b/api/maven-api-core/src/main/java/org/apache/maven/api/SourceRoot.java
index c60f6befda85..ce2d1aa4ee72 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/SourceRoot.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/SourceRoot.java
@@ -24,6 +24,8 @@
import java.util.List;
import java.util.Optional;
+import org.apache.maven.api.annotations.Nonnull;
+
/**
* A root directory of source files.
* The sources may be Java main classes, test classes, resources or anything else identified by the scope.
@@ -35,7 +37,7 @@
*/
public interface SourceRoot {
/**
- * {@return the root directory where the sources are stored}.
+ * {@return the root directory where the sources are stored}
* The path is relative to the POM file.
*
*
Default implementation
@@ -62,7 +64,7 @@ default Path directory() {
}
/**
- * {@return the list of patterns for the files to include}.
+ * {@return the list of patterns for the files to include}
* The path separator is {@code /} on all platforms, including Windows.
* The prefix before the {@code :} character, if present and longer than 1 character, is the syntax.
* If no syntax is specified, or if its length is 1 character (interpreted as a Windows drive),
@@ -79,7 +81,7 @@ default List includes() {
}
/**
- * {@return the list of patterns for the files to exclude}.
+ * {@return the list of patterns for the files to exclude}
* The exclusions are applied after the inclusions.
* The default implementation returns an empty list.
*/
@@ -88,7 +90,7 @@ default List excludes() {
}
/**
- * {@return a matcher combining the include and exclude patterns}.
+ * {@return a matcher combining the include and exclude patterns}
* If the user did not specify any includes, the given {@code defaultIncludes} are used.
* These defaults depend on the plugin.
* For example, the default include of the Java compiler plugin is "**/*.java".
@@ -104,7 +106,7 @@ default List excludes() {
PathMatcher matcher(Collection defaultIncludes, boolean useDefaultExcludes);
/**
- * {@return in which context the source files will be used}.
+ * {@return in which context the source files will be used}
* Not to be confused with dependency scope.
* The default value is {@code "main"}.
*
@@ -115,7 +117,7 @@ default ProjectScope scope() {
}
/**
- * {@return the language of the source files}.
+ * {@return the language of the source files}
* The default value is {@code "java"}.
*
* @see Language#JAVA_FAMILY
@@ -125,7 +127,7 @@ default Language language() {
}
/**
- * {@return the name of the Java module (or other language-specific module) which is built by the sources}.
+ * {@return the name of the Java module (or other language-specific module) which is built by the sources}
* The default value is empty.
*/
default Optional module() {
@@ -133,7 +135,7 @@ default Optional module() {
}
/**
- * {@return the version of the platform where the code will be executed}.
+ * {@return the version of the platform where the code will be executed}
* In a Java environment, this is the value of the {@code --release} compiler option.
* The default value is empty.
*/
@@ -142,18 +144,174 @@ default Optional targetVersion() {
}
/**
- * {@return an explicit target path, overriding the default value}.
- * When a target path is explicitly specified, the values of the {@link #module()} and {@link #targetVersion()}
- * elements are not used for inferring the path (they are still used as compiler options however).
- * It means that for scripts and resources, the files below the path specified by {@link #directory()}
+ * {@return an explicit target path, overriding the default value}
+ *
+ * Important: This method returns the target path as specified in the configuration,
+ * which may be relative or absolute. It does not perform any path resolution.
+ * For the fully resolved absolute path, use {@link #targetPath(Project)} instead.
+ *
+ *
+ * Return Value Semantics:
+ *
+ *
+ *
Empty Optional - No explicit target path was specified. Files should be copied
+ * to the root of the output directory (see {@link Project#getOutputDirectory(ProjectScope)}).
+ *
Relative Path (e.g., {@code Path.of("META-INF/resources")}) - The path is
+ * intended to be resolved relative to the output directory for this source root's {@link #scope()}.
+ *
+ *
For {@link ProjectScope#MAIN}: relative to {@code target/classes}
+ *
For {@link ProjectScope#TEST}: relative to {@code target/test-classes}
+ *
+ * The actual resolution is performed by {@link #targetPath(Project)}.
+ *
Absolute Path (e.g., {@code Path.of("/tmp/custom")}) - The path is used as-is
+ * without any resolution. Files will be copied to this exact location.
+ *
+ *
+ * Maven 3 Compatibility: This behavior maintains compatibility with Maven 3.x,
+ * where resource {@code targetPath} elements were always interpreted as relative to the output directory
+ * ({@code project.build.outputDirectory} or {@code project.build.testOutputDirectory}),
+ * not the project base directory. Maven 3 plugins (like maven-resources-plugin) expect to receive
+ * the relative path and perform the resolution themselves.
+ *
+ *
+ * Effect on Module and Target Version:
+ * When a target path is explicitly specified, the values of {@link #module()} and {@link #targetVersion()}
+ * are not used for inferring the output path (they are still used as compiler options however).
+ * This means that for scripts and resources, the files below the path specified by {@link #directory()}
* are copied to the path specified by {@code targetPath()} with the exact same directory structure.
+ *
+ *
+ * Usage Guidance:
+ *
+ *
+ *
For Maven 4 API consumers: Use {@link #targetPath(Project)} to get the
+ * fully resolved absolute path where files should be copied.
+ *
For Maven 3 compatibility layer: Use this method to get the path as specified
+ * in the configuration, which can then be passed to legacy plugins that expect to perform
+ * their own resolution.
+ *
For implementers: Store the path exactly as provided in the configuration.
+ * Do not resolve relative paths at storage time.
+ *
+ *
+ * @see #targetPath(Project)
+ * @see Project#getOutputDirectory(ProjectScope)
*/
default Optional targetPath() {
return Optional.empty();
}
/**
- * {@return whether resources are filtered to replace tokens with parameterized values}.
+ * {@return the fully resolved absolute target path where files should be copied}
+ *
+ * Purpose: This method performs the complete path resolution logic, converting
+ * the potentially relative {@link #targetPath()} into an absolute filesystem path. This is the
+ * method that Maven 4 API consumers should use when they need to know the actual destination
+ * directory for copying files.
+ *
+ *
+ * Resolution Algorithm:
+ *
+ *
+ *
Obtain the {@linkplain #targetPath() configured target path} (which may be empty, relative, or absolute)
+ *
If the configured target path is absolute (e.g., {@code /tmp/custom}):
+ *
Return it unchanged (no resolution needed)
+ *
Otherwise, get the output directory for this source root's {@link #scope()} by calling
+ * {@code project.getOutputDirectory(scope())}:
+ *
+ *
For {@link ProjectScope#MAIN}: typically {@code /path/to/project/target/classes}
+ *
For {@link ProjectScope#TEST}: typically {@code /path/to/project/target/test-classes}
+ *
+ *
If the configured target path is empty:
+ *
Return the output directory as-is
+ *
If the configured target path is relative (e.g., {@code META-INF/resources}):
+ *
Resolve it against the output directory using {@code outputDirectory.resolve(targetPath)}
+ *
+ *
+ * Concrete Examples:
+ *
+ *
+ * Given a project at {@code /home/user/myproject} with {@link ProjectScope#MAIN}:
+ *
+ * This method is the resolution counterpart to {@link #targetPath()}, which is the
+ * storage method. While {@code targetPath()} returns the path as configured (potentially relative),
+ * this method returns the absolute path where files will actually be written. The separation allows:
+ *
+ *
+ *
Maven 4 API consumers to get absolute paths via this method
+ *
Maven 3 compatibility layer to get relative paths via {@code targetPath()} for legacy plugins
+ *
Implementations to store paths without premature resolution
+ *
+ *
+ * Implementation Note: The default implementation is equivalent to:
+ *
+ *
+ * @param project the project to use for obtaining the output directory
+ * @return the absolute path where files from {@link #directory()} should be copied
+ *
+ * @see #targetPath()
+ * @see Project#getOutputDirectory(ProjectScope)
+ */
+ @Nonnull
+ default Path targetPath(@Nonnull Project project) {
+ Optional targetPath = targetPath();
+ // The test for `isAbsolute()` is a small optimization for avoiding the call to `getOutputDirectory(…)`.
+ return targetPath.filter(Path::isAbsolute).orElseGet(() -> {
+ Path base = project.getOutputDirectory(scope());
+ return targetPath.map(base::resolve).orElse(base);
+ });
+ }
+
+ /**
+ * {@return whether resources are filtered to replace tokens with parameterized values}
* The default value is {@code false}.
*/
default boolean stringFiltering() {
@@ -161,7 +319,7 @@ default boolean stringFiltering() {
}
/**
- * {@return whether the directory described by this source element should be included in the build}.
+ * {@return whether the directory described by this source element should be included in the build}
* The default value is {@code true}.
*/
default boolean enabled() {
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/feature/Features.java b/api/maven-api-core/src/main/java/org/apache/maven/api/feature/Features.java
index 91f6b9f3503b..8ab5a2006781 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/feature/Features.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/feature/Features.java
@@ -48,6 +48,13 @@ public static boolean consumerPom(@Nullable Map userProperties) {
return doGet(userProperties, Constants.MAVEN_CONSUMER_POM, !mavenMaven3Personality(userProperties));
}
+ /**
+ * Check if consumer POM flattening is enabled.
+ */
+ public static boolean consumerPomFlatten(@Nullable Map userProperties) {
+ return doGet(userProperties, Constants.MAVEN_CONSUMER_POM_FLATTEN, false);
+ }
+
private static boolean doGet(Properties userProperties, String key, boolean def) {
return doGet(userProperties != null ? userProperties.get(key) : null, def);
}
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/package-info.java b/api/maven-api-core/src/main/java/org/apache/maven/api/package-info.java
index 7fd2d60b9591..35f25fda6e25 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/package-info.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/package-info.java
@@ -119,9 +119,9 @@
*
*
Project aggregation allows building several projects together. This is only
* for projects that are built, hence available on the file system. One project,
- * called the aggregator project lists one or more modules
+ * called the aggregator project lists one or more sub-projects
* which are relative pointers on the file system to other projects. This is done using
- * the {@code /project/modules/module} elements of the POM in the aggregator project.
+ * the {@code /project/subprojects/subproject} elements of the POM in the aggregator project.
* Note that the aggregator project is required to have a {@code pom} packaging.
*
*
Project inheritance defines a parent-child relationship between projects.
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/package-info.java b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/package-info.java
index 14d2c7a2ef1c..28a7936fed4c 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/package-info.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/plugin/annotations/package-info.java
@@ -18,6 +18,10 @@
*/
/**
- * Maven Plugin Annotations.
+ * Provides annotations for Maven plugin development, including mojo configuration,
+ * parameter definitions, and lifecycle bindings. These annotations are used to
+ * generate plugin descriptors and configure plugin behavior.
+ *
+ * @since 4.0.0
*/
package org.apache.maven.api.plugin.annotations;
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverRequest.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverRequest.java
index fb012fab30df..7e832a95e41f 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverRequest.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ArtifactResolverRequest.java
@@ -40,14 +40,11 @@
*/
@Experimental
@Immutable
-public interface ArtifactResolverRequest extends Request {
+public interface ArtifactResolverRequest extends RepositoryAwareRequest {
@Nonnull
Collection extends ArtifactCoordinates> getCoordinates();
- @Nullable
- List getRepositories();
-
@Nonnull
static ArtifactResolverRequestBuilder builder() {
return new ArtifactResolverRequestBuilder();
@@ -127,7 +124,7 @@ private static class DefaultArtifactResolverRequest extends BaseRequest
@Nonnull List repositories) {
super(session, trace);
this.coordinates = List.copyOf(requireNonNull(coordinates, "coordinates cannot be null"));
- this.repositories = repositories;
+ this.repositories = validate(repositories);
}
@Nonnull
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/DependencyResolverRequest.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/DependencyResolverRequest.java
index f419d7ff60a7..5be250824d75 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/DependencyResolverRequest.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/DependencyResolverRequest.java
@@ -34,6 +34,8 @@
import org.apache.maven.api.Project;
import org.apache.maven.api.RemoteRepository;
import org.apache.maven.api.Session;
+import org.apache.maven.api.SourceRoot;
+import org.apache.maven.api.Version;
import org.apache.maven.api.annotations.Experimental;
import org.apache.maven.api.annotations.Immutable;
import org.apache.maven.api.annotations.Nonnull;
@@ -53,7 +55,7 @@
*/
@Experimental
@Immutable
-public interface DependencyResolverRequest extends Request {
+public interface DependencyResolverRequest extends RepositoryAwareRequest {
enum RequestType {
COLLECT,
@@ -95,8 +97,27 @@ enum RequestType {
@Nullable
Predicate getPathTypeFilter();
+ /**
+ * Returns the version of the platform where the code will be executed.
+ * It should be the highest value of the {@code } elements
+ * inside the {@code } elements of a POM file.
+ *
+ *
Application to Java
+ * In the context of a Java project, this is the value given to the {@code --release} compiler option.
+ * This value can determine whether a dependency will be placed on the class-path or on the module-path.
+ * For example, if the {@code module-info.class} entry of a JAR file exists only in the
+ * {@code META-INF/versions/17/} sub-directory, then the default location of that dependency will be
+ * the module-path only if the {@code --release} option is equal or greater than 17.
+ *
+ *
If this value is not provided, then the default value in the context of Java projects
+ * is the Java version on which Maven is running, as given by {@link Runtime#version()}.
+ *
+ * @return version of the platform where the code will be executed, or {@code null} for default
+ *
+ * @see SourceRoot#targetVersion()
+ */
@Nullable
- List getRepositories();
+ Version getTargetVersion();
@Nonnull
static DependencyResolverRequestBuilder builder() {
@@ -181,6 +202,7 @@ class DependencyResolverRequestBuilder {
boolean verbose;
PathScope pathScope;
Predicate pathTypeFilter;
+ Version targetVersion;
List repositories;
DependencyResolverRequestBuilder() {}
@@ -345,6 +367,18 @@ public DependencyResolverRequestBuilder pathTypeFilter(@Nonnull Collection ext
return pathTypeFilter(desiredTypes::contains);
}
+ /**
+ * Sets the version of the platform where the code will be executed.
+ *
+ * @param target version of the platform where the code will be executed, or {@code null} for the default
+ * @return {@code this} for method call chaining
+ */
+ @Nonnull
+ public DependencyResolverRequestBuilder targetVersion(@Nullable Version target) {
+ targetVersion = target;
+ return this;
+ }
+
@Nonnull
public DependencyResolverRequestBuilder repositories(@Nonnull List repositories) {
this.repositories = repositories;
@@ -365,6 +399,7 @@ public DependencyResolverRequest build() {
verbose,
pathScope,
pathTypeFilter,
+ targetVersion,
repositories);
}
@@ -404,6 +439,7 @@ public String toString() {
private final boolean verbose;
private final PathScope pathScope;
private final Predicate pathTypeFilter;
+ private final Version targetVersion;
private final List repositories;
/**
@@ -426,6 +462,7 @@ public String toString() {
boolean verbose,
@Nullable PathScope pathScope,
@Nullable Predicate pathTypeFilter,
+ @Nullable Version targetVersion,
@Nullable List repositories) {
super(session, trace);
this.requestType = requireNonNull(requestType, "requestType cannot be null");
@@ -438,7 +475,8 @@ public String toString() {
this.verbose = verbose;
this.pathScope = requireNonNull(pathScope, "pathScope cannot be null");
this.pathTypeFilter = (pathTypeFilter != null) ? pathTypeFilter : DEFAULT_FILTER;
- this.repositories = repositories;
+ this.targetVersion = targetVersion;
+ this.repositories = validate(repositories);
if (verbose && requestType != RequestType.COLLECT) {
throw new IllegalArgumentException("verbose cannot only be true when collecting dependencies");
}
@@ -495,6 +533,11 @@ public Predicate getPathTypeFilter() {
return pathTypeFilter;
}
+ @Override
+ public Version getTargetVersion() {
+ return targetVersion;
+ }
+
@Override
public List getRepositories() {
return repositories;
@@ -512,6 +555,7 @@ public boolean equals(Object o) {
&& Objects.equals(managedDependencies, that.managedDependencies)
&& Objects.equals(pathScope, that.pathScope)
&& Objects.equals(pathTypeFilter, that.pathTypeFilter)
+ && Objects.equals(targetVersion, that.targetVersion)
&& Objects.equals(repositories, that.repositories);
}
@@ -527,6 +571,7 @@ public int hashCode() {
verbose,
pathScope,
pathTypeFilter,
+ targetVersion,
repositories);
}
@@ -541,7 +586,8 @@ public String toString() {
+ managedDependencies + ", verbose="
+ verbose + ", pathScope="
+ pathScope + ", pathTypeFilter="
- + pathTypeFilter + ", repositories="
+ + pathTypeFilter + ", targetVersion="
+ + targetVersion + ", repositories="
+ repositories + ']';
}
}
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderRequest.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderRequest.java
index 14141a6d0c6c..826ffe8fc4c5 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderRequest.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderRequest.java
@@ -43,7 +43,7 @@
*/
@Experimental
@Immutable
-public interface ModelBuilderRequest extends Request {
+public interface ModelBuilderRequest extends RepositoryAwareRequest {
/**
* The possible request types for building a model.
@@ -133,9 +133,6 @@ enum RepositoryMerging {
@Nonnull
RepositoryMerging getRepositoryMerging();
- @Nullable
- List getRepositories();
-
@Nullable
ModelTransformer getLifecycleBindingsInjector();
@@ -338,7 +335,7 @@ private static class DefaultModelBuilderRequest extends BaseRequest imp
systemProperties != null ? Map.copyOf(systemProperties) : session.getSystemProperties();
this.userProperties = userProperties != null ? Map.copyOf(userProperties) : session.getUserProperties();
this.repositoryMerging = repositoryMerging;
- this.repositories = repositories != null ? List.copyOf(repositories) : null;
+ this.repositories = repositories != null ? List.copyOf(validate(repositories)) : null;
this.lifecycleBindingsInjector = lifecycleBindingsInjector;
}
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderResult.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderResult.java
index 4b15818cf033..854f8dcc01d9 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderResult.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelBuilderResult.java
@@ -19,6 +19,7 @@
package org.apache.maven.api.services;
import java.util.List;
+import java.util.Map;
import org.apache.maven.api.annotations.Experimental;
import org.apache.maven.api.annotations.Nonnull;
@@ -81,6 +82,27 @@ public interface ModelBuilderResult extends Result {
@Nonnull
List getActivePomProfiles();
+ /**
+ * Gets the profiles that were active during model building for a specific model in the hierarchy.
+ * This allows tracking which profiles came from which model (parent vs child).
+ *
+ * @param modelId The identifier of the model (groupId:artifactId:version) or empty string for the super POM.
+ * @return The active profiles for the specified model or an empty list if the model has no active profiles.
+ * @since 4.0.0
+ */
+ @Nonnull
+ List getActivePomProfiles(String modelId);
+
+ /**
+ * Gets a map of all active POM profiles organized by model ID.
+ * The map keys are model IDs (groupId:artifactId:version) and values are lists of active profiles for each model.
+ *
+ * @return A map of model IDs to their active profiles, never {@code null}.
+ * @since 4.0.0
+ */
+ @Nonnull
+ Map> getActivePomProfilesByModel();
+
/**
* Gets the external profiles that were active during model building. External profiles are those that were
* contributed by {@link ModelBuilderRequest#getProfiles()}.
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/PathMatcherFactory.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/PathMatcherFactory.java
new file mode 100644
index 000000000000..9f83e2e0f8bf
--- /dev/null
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/PathMatcherFactory.java
@@ -0,0 +1,163 @@
+/*
+ * 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.
+ */
+package org.apache.maven.api.services;
+
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.Collection;
+import java.util.Objects;
+
+import org.apache.maven.api.Service;
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.annotations.Nonnull;
+
+/**
+ * Service for creating {@link PathMatcher} objects that can be used to filter files
+ * based on include/exclude patterns. This service provides a clean API for plugins
+ * to create path matchers without directly depending on implementation classes.
+ *
+ * The path matchers created by this service support Maven's traditional include/exclude
+ * pattern syntax, which is compatible with the behavior of Maven 3 plugins like
+ * maven-compiler-plugin and maven-clean-plugin.
+ *
+ * Pattern syntax supports:
+ *
+ *
Standard glob patterns with {@code *}, {@code ?}, and {@code **} wildcards
+ *
Explicit syntax prefixes like {@code "glob:"} or {@code "regex:"}
+ *
Maven 3 compatible behavior for patterns without explicit syntax
+ *
Default exclusion patterns for SCM files when requested
+ *
+ *
+ * @since 4.0.0
+ * @see PathMatcher
+ */
+@Experimental
+public interface PathMatcherFactory extends Service {
+
+ /**
+ * Creates a path matcher for filtering files based on include and exclude patterns.
+ *
+ * The pathnames used for matching will be relative to the specified base directory
+ * and use {@code '/'} as separator, regardless of the hosting operating system.
+ *
+ * @param baseDirectory the base directory for relativizing paths during matching
+ * @param includes the patterns of files to include, or null/empty for including all files
+ * @param excludes the patterns of files to exclude, or null/empty for no exclusion
+ * @param useDefaultExcludes whether to augment excludes with default SCM exclusion patterns
+ * @return a PathMatcher that can be used to test if paths should be included
+ * @throws NullPointerException if baseDirectory is null
+ */
+ @Nonnull
+ PathMatcher createPathMatcher(
+ @Nonnull Path baseDirectory,
+ Collection includes,
+ Collection excludes,
+ boolean useDefaultExcludes);
+
+ /**
+ * Creates a path matcher for filtering files based on include and exclude patterns,
+ * without using default exclusion patterns.
+ *
+ * This is equivalent to calling {@link #createPathMatcher(Path, Collection, Collection, boolean)}
+ * with {@code useDefaultExcludes = false}.
+ *
+ * @param baseDirectory the base directory for relativizing paths during matching
+ * @param includes the patterns of files to include, or null/empty for including all files
+ * @param excludes the patterns of files to exclude, or null/empty for no exclusion
+ * @return a PathMatcher that can be used to test if paths should be included
+ * @throws NullPointerException if baseDirectory is null
+ */
+ @Nonnull
+ default PathMatcher createPathMatcher(
+ @Nonnull Path baseDirectory, Collection includes, Collection excludes) {
+ return createPathMatcher(baseDirectory, includes, excludes, false);
+ }
+
+ /**
+ * Creates a path matcher that includes all files except those matching the exclude patterns.
+ *
+ * This is equivalent to calling {@link #createPathMatcher(Path, Collection, Collection, boolean)}
+ * with {@code includes = null}.
+ *
+ * @param baseDirectory the base directory for relativizing paths during matching
+ * @param excludes the patterns of files to exclude, or null/empty for no exclusion
+ * @param useDefaultExcludes whether to augment excludes with default SCM exclusion patterns
+ * @return a PathMatcher that can be used to test if paths should be included
+ * @throws NullPointerException if baseDirectory is null
+ */
+ @Nonnull
+ default PathMatcher createExcludeOnlyMatcher(
+ @Nonnull Path baseDirectory, Collection excludes, boolean useDefaultExcludes) {
+ return createPathMatcher(baseDirectory, null, excludes, useDefaultExcludes);
+ }
+
+ /**
+ * Creates a path matcher that only includes files matching the include patterns.
+ *
+ * This is equivalent to calling {@link #createPathMatcher(Path, Collection, Collection, boolean)}
+ * with {@code excludes = null} and {@code useDefaultExcludes = false}.
+ *
+ * @param baseDirectory the base directory for relativizing paths during matching
+ * @param includes the patterns of files to include, or null/empty for including all files
+ * @return a PathMatcher that can be used to test if paths should be included
+ * @throws NullPointerException if baseDirectory is null
+ */
+ @Nonnull
+ default PathMatcher createIncludeOnlyMatcher(@Nonnull Path baseDirectory, Collection includes) {
+ return createPathMatcher(baseDirectory, includes, null, false);
+ }
+
+ /**
+ * Returns a filter for directories that may contain paths accepted by the given matcher.
+ * The given path matcher should be an instance created by this service.
+ * The path matcher returned by this method expects directory paths.
+ * If that matcher returns {@code false}, then the directory will definitively not contain
+ * the paths selected by the matcher given in argument to this method.
+ * In such case, the whole directory and all its sub-directories can be skipped.
+ * In case of doubt, or if the matcher given in argument is not recognized by this method,
+ * then the matcher returned by this method will return {@code true}.
+ *
+ * @param fileMatcher a matcher created by one of the other methods of this interface
+ * @return filter for directories that may contain the selected files
+ * @throws NullPointerException if fileMatcher is null
+ */
+ @Nonnull
+ PathMatcher deriveDirectoryMatcher(@Nonnull PathMatcher fileMatcher);
+
+ /**
+ * Returns the path matcher that unconditionally returns {@code true} for all files.
+ * It should be the matcher returned by the other methods of this interface when the
+ * given patterns match all files.
+ *
+ * @return path matcher that unconditionally returns {@code true} for all files
+ */
+ @Nonnull
+ PathMatcher includesAll();
+
+ /**
+ * {@return whether the given matcher includes all files}.
+ * This method may conservatively returns {@code false} if case of doubt.
+ * A return value of {@code true} means that the pattern is certain to match all files.
+ *
+ * @param matcher the matcher to test
+ */
+ default boolean isIncludesAll(@Nonnull PathMatcher matcher) {
+ return Objects.requireNonNull(matcher) == includesAll();
+ }
+}
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ProjectBuilderRequest.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ProjectBuilderRequest.java
index 82129b4f1b69..307ee1955947 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/ProjectBuilderRequest.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/ProjectBuilderRequest.java
@@ -43,7 +43,7 @@
*/
@Experimental
@Immutable
-public interface ProjectBuilderRequest extends Request {
+public interface ProjectBuilderRequest extends RepositoryAwareRequest {
/**
* Gets the path to the project to build.
@@ -265,7 +265,7 @@ private static class DefaultProjectBuilderRequest extends BaseRequest
this.allowStubModel = allowStubModel;
this.recursive = recursive;
this.processPlugins = processPlugins;
- this.repositories = repositories;
+ this.repositories = validate(repositories);
}
@Nonnull
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/RepositoryAwareRequest.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/RepositoryAwareRequest.java
new file mode 100644
index 000000000000..f948ecdea460
--- /dev/null
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/RepositoryAwareRequest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+package org.apache.maven.api.services;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.maven.api.RemoteRepository;
+import org.apache.maven.api.Session;
+import org.apache.maven.api.annotations.Experimental;
+import org.apache.maven.api.annotations.Immutable;
+import org.apache.maven.api.annotations.Nullable;
+
+/**
+ * Base interface for service requests that involve remote repository operations.
+ * This interface provides common functionality for requests that need to specify
+ * and validate remote repositories for artifact resolution, dependency collection,
+ * model building, and other Maven operations.
+ *
+ *
Implementations of this interface can specify a list of remote repositories
+ * to be used during the operation. If no repositories are specified (null),
+ * the session's default remote repositories will be used. The repositories
+ * are validated to ensure they don't contain duplicates or null entries.
+ *
+ *
Remote repositories are used for:
+ *
+ *
Resolving artifacts and their metadata
+ *
Downloading parent POMs and dependency POMs
+ *
Retrieving version information and ranges
+ *
Accessing plugin artifacts and their dependencies
+ *
+ *
+ *
Repository validation ensures data integrity by:
+ *
+ *
Preventing duplicate repositories that could cause confusion
+ *
Rejecting null repository entries that would cause failures
+ *
Maintaining consistent repository ordering for reproducible builds
+ *
+ *
+ * @since 4.0.0
+ * @see RemoteRepository
+ * @see Session#getRemoteRepositories()
+ */
+@Experimental
+@Immutable
+public interface RepositoryAwareRequest extends Request {
+
+ /**
+ * Returns the list of remote repositories to be used for this request.
+ *
+ *
If this method returns {@code null}, the session's default remote repositories
+ * will be used. If a non-null list is returned, it will be used instead of the
+ * session's repositories, allowing for request-specific repository configuration.
+ *
+ *
The returned list should not contain duplicate repositories (based on their
+ * equality) or null entries, as these will cause validation failures when the
+ * request is processed.
+ *
+ * @return the list of remote repositories to use, or {@code null} to use session defaults
+ * @see Session#getRemoteRepositories()
+ */
+ @Nullable
+ List getRepositories();
+
+ /**
+ * Validates a list of remote repositories to ensure data integrity.
+ *
+ *
This method performs the following validations:
+ *
+ *
Allows null input (returns null)
+ *
Ensures no duplicate repositories exist in the list
+ *
Ensures no null repository entries exist in the list
+ *
+ *
+ *
Duplicate detection is based on the {@code RemoteRepository#equals(Object)}
+ * method, which typically compares repository IDs and URLs.
+ *
+ * @param repositories the list of repositories to validate, may be {@code null}
+ * @return the same list if validation passes, or {@code null} if input was {@code null}
+ * @throws IllegalArgumentException if the list contains duplicate repositories
+ * @throws IllegalArgumentException if the list contains null repository entries
+ */
+ default List validate(List repositories) {
+ if (repositories == null) {
+ return null;
+ }
+ HashSet set = new HashSet<>(repositories);
+ if (repositories.size() != set.size()) {
+ throw new IllegalArgumentException(
+ "Repository list contains duplicate entries. Each repository must be unique based on its ID and URL. "
+ + "Found " + repositories.size() + " repositories but only " + set.size()
+ + " unique entries.");
+ }
+ if (repositories.stream().anyMatch(Objects::isNull)) {
+ throw new IllegalArgumentException(
+ "Repository list contains null entries. All repository entries must be non-null RemoteRepository instances.");
+ }
+ return repositories;
+ }
+}
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/RequestTrace.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/RequestTrace.java
index 6dafc3aeaf57..ac67cb64509e 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/RequestTrace.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/RequestTrace.java
@@ -50,7 +50,10 @@
* object being processed or any application-specific state information. May be null if no
* additional data is needed.
*/
-public record RequestTrace(@Nullable String context, @Nullable RequestTrace parent, @Nullable Object data) {
+public record RequestTrace(
+ @Nullable String context,
+ @Nullable RequestTrace parent,
+ @Nullable Object data) {
public static final String CONTEXT_PLUGIN = "plugin";
public static final String CONTEXT_PROJECT = "project";
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/VersionRangeResolverRequest.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/VersionRangeResolverRequest.java
index 52abe9e89a49..50de8e9a804f 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/VersionRangeResolverRequest.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/VersionRangeResolverRequest.java
@@ -32,86 +32,210 @@
import static java.util.Objects.requireNonNull;
/**
+ * A request to resolve a version range to a list of matching versions.
+ * This request is used by {@link VersionRangeResolver} to expand version ranges
+ * (e.g., "[3.8,4.0)") into concrete versions available in the configured repositories.
*
* @since 4.0.0
*/
@Experimental
-public interface VersionRangeResolverRequest extends Request {
+public interface VersionRangeResolverRequest extends RepositoryAwareRequest {
+ /**
+ * Specifies which type of repositories to query when resolving version ranges.
+ * This controls whether to search in release repositories, snapshot repositories, or both.
+ *
+ * @since 4.0.0
+ */
+ enum Nature {
+ /**
+ * Query only release repositories to discover versions.
+ */
+ RELEASE,
+ /**
+ * Query only snapshot repositories to discover versions.
+ */
+ SNAPSHOT,
+ /**
+ * Query both release and snapshot repositories to discover versions.
+ * This is the default behavior.
+ */
+ RELEASE_OR_SNAPSHOT
+ }
+
+ /**
+ * Gets the artifact coordinates whose version range should be resolved.
+ * The coordinates may contain a version range (e.g., "[1.0,2.0)") or a single version.
+ *
+ * @return the artifact coordinates, never {@code null}
+ */
@Nonnull
ArtifactCoordinates getArtifactCoordinates();
- @Nullable
- List getRepositories();
+ /**
+ * Gets the nature of repositories to query when resolving the version range.
+ * This determines whether to search in release repositories, snapshot repositories, or both.
+ *
+ * @return the repository nature, never {@code null}
+ */
+ @Nonnull
+ Nature getNature();
+ /**
+ * Creates a version range resolver request using the session's repositories.
+ *
+ * @param session the session to use, must not be {@code null}
+ * @param artifactCoordinates the artifact coordinates whose version range should be resolved, must not be {@code null}
+ * @return the version range resolver request, never {@code null}
+ */
@Nonnull
static VersionRangeResolverRequest build(
@Nonnull Session session, @Nonnull ArtifactCoordinates artifactCoordinates) {
- return build(session, artifactCoordinates, null);
+ return build(session, artifactCoordinates, null, null);
}
+ /**
+ * Creates a version range resolver request.
+ *
+ * @param session the session to use, must not be {@code null}
+ * @param artifactCoordinates the artifact coordinates whose version range should be resolved, must not be {@code null}
+ * @param repositories the repositories to use, or {@code null} to use the session's repositories
+ * @return the version range resolver request, never {@code null}
+ */
@Nonnull
static VersionRangeResolverRequest build(
@Nonnull Session session,
@Nonnull ArtifactCoordinates artifactCoordinates,
@Nullable List repositories) {
+ return build(session, artifactCoordinates, repositories, null);
+ }
+
+ /**
+ * Creates a version range resolver request.
+ *
+ * @param session the session to use, must not be {@code null}
+ * @param artifactCoordinates the artifact coordinates whose version range should be resolved, must not be {@code null}
+ * @param repositories the repositories to use, or {@code null} to use the session's repositories
+ * @param nature the nature of repositories to query when resolving the version range, or {@code null} to use the default
+ * @return the version range resolver request, never {@code null}
+ */
+ @Nonnull
+ static VersionRangeResolverRequest build(
+ @Nonnull Session session,
+ @Nonnull ArtifactCoordinates artifactCoordinates,
+ @Nullable List repositories,
+ @Nullable Nature nature) {
return builder()
.session(requireNonNull(session, "session cannot be null"))
.artifactCoordinates(requireNonNull(artifactCoordinates, "artifactCoordinates cannot be null"))
.repositories(repositories)
+ .nature(nature)
.build();
}
+ /**
+ * Creates a new builder for version range resolver requests.
+ *
+ * @return a new builder, never {@code null}
+ */
@Nonnull
static VersionResolverRequestBuilder builder() {
return new VersionResolverRequestBuilder();
}
+ /**
+ * Builder for {@link VersionRangeResolverRequest}.
+ */
@NotThreadSafe
class VersionResolverRequestBuilder {
Session session;
RequestTrace trace;
ArtifactCoordinates artifactCoordinates;
List repositories;
+ Nature nature = Nature.RELEASE_OR_SNAPSHOT;
+ /**
+ * Sets the session to use for the request.
+ *
+ * @param session the session, must not be {@code null}
+ * @return this builder, never {@code null}
+ */
public VersionResolverRequestBuilder session(Session session) {
this.session = session;
return this;
}
+ /**
+ * Sets the request trace for debugging and diagnostics.
+ *
+ * @param trace the request trace, may be {@code null}
+ * @return this builder, never {@code null}
+ */
public VersionResolverRequestBuilder trace(RequestTrace trace) {
this.trace = trace;
return this;
}
+ /**
+ * Sets the artifact coordinates whose version range should be resolved.
+ *
+ * @param artifactCoordinates the artifact coordinates, must not be {@code null}
+ * @return this builder, never {@code null}
+ */
public VersionResolverRequestBuilder artifactCoordinates(ArtifactCoordinates artifactCoordinates) {
this.artifactCoordinates = artifactCoordinates;
return this;
}
+ /**
+ * Sets the nature of repositories to query when resolving the version range.
+ * If {@code null} is provided, defaults to {@link Nature#RELEASE_OR_SNAPSHOT}.
+ *
+ * @param nature the repository nature, or {@code null} to use the default
+ * @return this builder, never {@code null}
+ */
+ public VersionResolverRequestBuilder nature(Nature nature) {
+ this.nature = Objects.requireNonNullElse(nature, Nature.RELEASE_OR_SNAPSHOT);
+ return this;
+ }
+
+ /**
+ * Sets the repositories to use for resolving the version range.
+ *
+ * @param repositories the repositories, or {@code null} to use the session's repositories
+ * @return this builder, never {@code null}
+ */
public VersionResolverRequestBuilder repositories(List repositories) {
this.repositories = repositories;
return this;
}
+ /**
+ * Builds the version range resolver request.
+ *
+ * @return the version range resolver request, never {@code null}
+ */
public VersionRangeResolverRequest build() {
- return new DefaultVersionResolverRequest(session, trace, artifactCoordinates, repositories);
+ return new DefaultVersionResolverRequest(session, trace, artifactCoordinates, repositories, nature);
}
private static class DefaultVersionResolverRequest extends BaseRequest
implements VersionRangeResolverRequest {
private final ArtifactCoordinates artifactCoordinates;
private final List repositories;
+ private final Nature nature;
@SuppressWarnings("checkstyle:ParameterNumber")
DefaultVersionResolverRequest(
@Nonnull Session session,
@Nullable RequestTrace trace,
@Nonnull ArtifactCoordinates artifactCoordinates,
- @Nullable List repositories) {
+ @Nullable List repositories,
+ @Nonnull Nature nature) {
super(session, trace);
- this.artifactCoordinates = artifactCoordinates;
- this.repositories = repositories;
+ this.artifactCoordinates = requireNonNull(artifactCoordinates);
+ this.repositories = validate(repositories);
+ this.nature = requireNonNull(nature);
}
@Nonnull
@@ -126,23 +250,31 @@ public List getRepositories() {
return repositories;
}
+ @Nonnull
+ @Override
+ public Nature getNature() {
+ return nature;
+ }
+
@Override
public boolean equals(Object o) {
return o instanceof DefaultVersionResolverRequest that
&& Objects.equals(artifactCoordinates, that.artifactCoordinates)
- && Objects.equals(repositories, that.repositories);
+ && Objects.equals(repositories, that.repositories)
+ && nature == that.nature;
}
@Override
public int hashCode() {
- return Objects.hash(artifactCoordinates, repositories);
+ return Objects.hash(artifactCoordinates, repositories, nature);
}
@Override
public String toString() {
return "VersionResolverRequest[" + "artifactCoordinates="
+ artifactCoordinates + ", repositories="
- + repositories + ']';
+ + repositories + ", nature="
+ + nature + ']';
}
}
}
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/VersionResolverRequest.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/VersionResolverRequest.java
index c8dee58a8fcf..b510dcc2de17 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/VersionResolverRequest.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/VersionResolverRequest.java
@@ -36,14 +36,11 @@
* @since 4.0.0
*/
@Experimental
-public interface VersionResolverRequest extends Request {
+public interface VersionResolverRequest extends RepositoryAwareRequest {
@Nonnull
ArtifactCoordinates getArtifactCoordinates();
- @Nullable
- List getRepositories();
-
@Nonnull
static VersionResolverRequest build(@Nonnull Session session, @Nonnull ArtifactCoordinates artifactCoordinates) {
return builder()
@@ -113,7 +110,7 @@ private static class DefaultVersionResolverRequest extends BaseRequest
@Nullable List repositories) {
super(session, trace);
this.artifactCoordinates = artifactCoordinates;
- this.repositories = repositories;
+ this.repositories = validate(repositories);
}
@Nonnull
diff --git a/api/maven-api-core/src/main/java/org/apache/maven/api/services/xml/XmlReaderRequest.java b/api/maven-api-core/src/main/java/org/apache/maven/api/services/xml/XmlReaderRequest.java
index d6fc50e911ad..41733eb08bf3 100644
--- a/api/maven-api-core/src/main/java/org/apache/maven/api/services/xml/XmlReaderRequest.java
+++ b/api/maven-api-core/src/main/java/org/apache/maven/api/services/xml/XmlReaderRequest.java
@@ -208,7 +208,7 @@ public Path getRootDirectory() {
@Override
public URL getURL() {
- return null;
+ return url;
}
@Override
diff --git a/api/maven-api-core/src/test/java/org/apache/maven/api/SourceRootTest.java b/api/maven-api-core/src/test/java/org/apache/maven/api/SourceRootTest.java
new file mode 100644
index 000000000000..a316550aee89
--- /dev/null
+++ b/api/maven-api-core/src/test/java/org/apache/maven/api/SourceRootTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+package org.apache.maven.api;
+
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.util.Collection;
+import java.util.Optional;
+
+import org.apache.maven.api.model.Build;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SourceRootTest implements SourceRoot {
+ private ProjectScope scope;
+
+ private Language language;
+
+ private String moduleName;
+
+ @Override
+ public ProjectScope scope() {
+ return (scope != null) ? scope : SourceRoot.super.scope();
+ }
+
+ @Override
+ public Language language() {
+ return (language != null) ? language : SourceRoot.super.language();
+ }
+
+ @Override
+ public Optional module() {
+ return Optional.ofNullable(moduleName);
+ }
+
+ @Override
+ public PathMatcher matcher(Collection defaultIncludes, boolean useDefaultExcludes) {
+ return null; // Not used for this test.
+ }
+
+ @Test
+ void testDirectory() {
+ assertEquals(Path.of("src", "main", "java"), directory());
+
+ scope = ProjectScope.TEST;
+ assertEquals(Path.of("src", "test", "java"), directory());
+
+ moduleName = "org.foo";
+ assertEquals(Path.of("src", "org.foo", "test", "java"), directory());
+ }
+
+ @Test
+ void testTargetPath() {
+ Build build = mock(Build.class);
+ when(build.getDirectory()).thenReturn("target");
+ when(build.getOutputDirectory()).thenReturn("target/classes");
+ when(build.getTestOutputDirectory()).thenReturn("target/test-classes");
+
+ Project project = mock(Project.class);
+ when(project.getBuild()).thenReturn(build);
+ when(project.getBasedir()).thenReturn(Path.of("myproject"));
+ when(project.getOutputDirectory(any(ProjectScope.class))).thenCallRealMethod();
+
+ assertEquals(Path.of("myproject", "target", "classes"), targetPath(project));
+
+ scope = ProjectScope.TEST;
+ assertEquals(Path.of("myproject", "target", "test-classes"), targetPath(project));
+ }
+}
diff --git a/api/maven-api-di/pom.xml b/api/maven-api-di/pom.xml
index 984a496d0d90..c2cc62e36f48 100644
--- a/api/maven-api-di/pom.xml
+++ b/api/maven-api-di/pom.xml
@@ -23,7 +23,7 @@
org.apache.mavenmaven-api
- 4.0.0-rc-4-SNAPSHOT
+ 4.0.0-SNAPSHOTmaven-api-di
diff --git a/api/maven-api-metadata/pom.xml b/api/maven-api-metadata/pom.xml
index 0db48a3692c3..174816e59ecf 100644
--- a/api/maven-api-metadata/pom.xml
+++ b/api/maven-api-metadata/pom.xml
@@ -23,7 +23,7 @@ under the License.
org.apache.mavenmaven-api
- 4.0.0-rc-4-SNAPSHOT
+ 4.0.0-SNAPSHOTmaven-api-metadata
diff --git a/api/maven-api-model/pom.xml b/api/maven-api-model/pom.xml
index 75534c1c597e..37e0555cbe46 100644
--- a/api/maven-api-model/pom.xml
+++ b/api/maven-api-model/pom.xml
@@ -23,7 +23,7 @@ under the License.
org.apache.mavenmaven-api
- 4.0.0-rc-4-SNAPSHOT
+ 4.0.0-SNAPSHOTmaven-api-model
diff --git a/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocation.java b/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocation.java
index 2e65dea793fd..9ecfb400ff9c 100644
--- a/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocation.java
+++ b/api/maven-api-model/src/main/java/org/apache/maven/api/model/InputLocation.java
@@ -23,6 +23,7 @@
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
+import java.util.Objects;
/**
* Represents the location of an element within a model source file.
@@ -30,16 +31,28 @@
* This class tracks the line and column numbers of elements in source files like POM files.
* It's used for error reporting and debugging to help identify where specific model elements
* are defined in the source files.
+ *
+ * Note: Starting with Maven 4.0.0, it is recommended to use the static factory methods
+ * {@code of(...)} instead of constructors. The constructors are deprecated and will be
+ * removed in a future version.
*
* @since 4.0.0
*/
-public class InputLocation implements Serializable, InputLocationTracker {
+public final class InputLocation implements Serializable, InputLocationTracker {
private final int lineNumber;
private final int columnNumber;
private final InputSource source;
private final Map