You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/07/28 19:48:52 UTC

(maven) branch maven-3.10.x updated: [3.10.x] Convert paths for MSYS2 and jvm.config placeholders in bin/mvn (#12567)

This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch maven-3.10.x
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/maven-3.10.x by this push:
     new 9a6120ee84 [3.10.x] Convert paths for MSYS2 and jvm.config placeholders in bin/mvn (#12567)
9a6120ee84 is described below

commit 9a6120ee84ba05cadc6b07f6ea9a0075386d02c5
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Tue Jul 28 21:48:24 2026 +0200

    [3.10.x] Convert paths for MSYS2 and jvm.config placeholders in bin/mvn (#12567)
    
    Backport of #12546 to the 3.10.x branch. Adapted for the different script
    location (src/bin/mvn vs src/assembly/maven/bin/mvn) and the simpler
    jvm.config handling (no JvmConfigParser on this branch).
    
    Key changes:
    - Detect MSYS2 (MSYS_NT-*) alongside MinGW in uname matching
    - Guard against missing cygpath(1) — warn and fall back to POSIX paths
    - Convert all paths (MAVEN_HOME, CLASSWORLDS_JAR, MAVEN_PROJECTBASEDIR,
      jline-native) for both Cygwin and MinGW/MSYS2, not just Cygwin
    - Convert jline-native path separately to avoid mixed separators
    - Use printf instead of echo for values holding paths (avoid backslash
      expansion in dash/ash)
    - Add shell test covering Cygwin, MinGW, MSYS2, and missing-cygpath
      fallback, wired into the build via exec-maven-plugin
    
    Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
 apache-maven/pom.xml                               |  36 +++
 apache-maven/src/bin/mvn                           |  53 +++--
 .../src/test/scripts/test-mvn-path-conversion.sh   | 250 +++++++++++++++++++++
 3 files changed, 326 insertions(+), 13 deletions(-)

diff --git a/apache-maven/pom.xml b/apache-maven/pom.xml
index 777ea58b95..9399fd4a6a 100644
--- a/apache-maven/pom.xml
+++ b/apache-maven/pom.xml
@@ -357,5 +357,41 @@ under the License.
         <finalName>${project.artifactId}</finalName>
       </build>
     </profile>
+    <!-- Runs the bin/mvn path conversion test as part of the build. The test is
+         a POSIX shell script driving the launcher, so it only applies to the
+         platforms that use bin/mvn; Windows ships and runs bin/mvn.cmd. -->
+    <profile>
+      <id>shell-script-tests</id>
+      <activation>
+        <os>
+          <family>unix</family>
+        </os>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>exec-maven-plugin</artifactId>
+            <version>3.6.3</version>
+            <executions>
+              <execution>
+                <id>test-mvn-path-conversion</id>
+                <goals>
+                  <goal>exec</goal>
+                </goals>
+                <phase>test</phase>
+                <configuration>
+                  <skip>${skipTests}</skip>
+                  <executable>sh</executable>
+                  <arguments>
+                    <argument>${project.basedir}/src/test/scripts/test-mvn-path-conversion.sh</argument>
+                  </arguments>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
   </profiles>
 </project>
diff --git a/apache-maven/src/bin/mvn b/apache-maven/src/bin/mvn
index 2b3dc375fc..0b53cde28d 100755
--- a/apache-maven/src/bin/mvn
+++ b/apache-maven/src/bin/mvn
@@ -44,14 +44,37 @@ if [ -z "$MAVEN_SKIP_RC" ] ; then
 
 fi
 
+# Note on echo vs printf: several POSIX shells (dash and the other Almquist
+# derivatives commonly installed as /bin/sh) expand backslash escapes in echo.
+# Windows paths are full of backslashes, so C:\tmp\build would be printed with a
+# tab in place of \t. Every value that may hold a path is therefore emitted with
+# printf '%s' rather than echo; plain literals may still use echo.
+
 # OS specific support. $var _must_ be set to either true or false.
+# MINGW* matches the MinGW32/MinGW64 shells (e.g. Git Bash, the shell used by
+# "shell: bash" steps of GitHub Actions Windows runners), MSYS* the MSYS2 shell.
+# All of them run on top of a POSIX emulation layer that mangles POSIX paths
+# passed to native (Windows) processes such as the JVM, so paths have to be
+# converted to their native Windows form explicitly.
 cygwin=false;
 mingw=false;
 case "`uname`" in
   CYGWIN*) cygwin=true;;
-  MINGW*) mingw=true;;
+  MINGW*|MSYS*) mingw=true;;
 esac
 
+# Cygwin and MSYS2 both ship cygpath(1), but bail out of the path conversion if
+# it is unexpectedly missing: substituting empty paths would break the launch in
+# a way that is much harder to diagnose than keeping the POSIX paths.
+if $cygwin || $mingw ; then
+  if ! ( \unset -f command; \command -v cygpath ) > /dev/null 2>&1 ; then
+    echo "Warning: cygpath was not found on the PATH, Apache Maven cannot convert paths to the Windows format." >&2
+    echo "Some plugins may misbehave when they receive POSIX paths." >&2
+    cygwin=false
+    mingw=false
+  fi
+fi
+
 ## resolve links - $0 may be a link to Maven's home
 PRG="$0"
 
@@ -109,16 +132,20 @@ fi
 CLASSWORLDS_JAR=`echo "${MAVEN_HOME}"/boot/plexus-classworlds-*.jar`
 CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
 
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
+# For Cygwin and MinGW, switch paths to Windows format before running java(1)
+JLINE_NATIVE_PATH="$MAVEN_HOME/lib/jline-native"
+if $cygwin || $mingw ; then
   [ -n "$MAVEN_HOME" ] &&
-    MAVEN_HOME=`cygpath --path --windows "$MAVEN_HOME"`
+    MAVEN_HOME=`cygpath --windows "$MAVEN_HOME"`
   [ -n "$JAVA_HOME" ] &&
-    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+    JAVA_HOME=`cygpath --windows "$JAVA_HOME"`
   [ -n "$CLASSPATH" ] &&
     CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
   [ -n "$CLASSWORLDS_JAR" ] &&
-    CLASSWORLDS_JAR=`cygpath --path --windows "$CLASSWORLDS_JAR"`
+    CLASSWORLDS_JAR=`cygpath --windows "$CLASSWORLDS_JAR"`
+  # converted separately, appending to the already converted MAVEN_HOME would
+  # produce a path mixing both separators, e.g. C:\maven/lib/jline-native
+  JLINE_NATIVE_PATH=`cygpath --windows "$JLINE_NATIVE_PATH"`
 fi
 
 # traverses directory structure from process work directory to filesystem root
@@ -138,7 +165,7 @@ find_maven_basedir() {
     fi
     wdir=`cd "$wdir/.."; pwd`
   done
-  echo "${basedir}"
+  printf '%s\n' "${basedir}"
 )
 }
 
@@ -168,7 +195,7 @@ find_file_argument_basedir() {
       found_file_switch=1
     fi
   done
-  echo "${basedir}"
+  printf '%s\n' "${basedir}"
 )
 }
 
@@ -182,11 +209,11 @@ concat_lines() {
 MAVEN_PROJECTBASEDIR="${MAVEN_BASEDIR:-`find_maven_basedir "$@"`}"
 MAVEN_OPTS="`concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config"` $MAVEN_OPTS"
 
-# For Cygwin, switch project base directory path to Windows format before
-# executing Maven otherwise this will cause Maven not to consider it.
-if $cygwin ; then
+# For Cygwin and MinGW, switch project base directory path to Windows format
+# before executing Maven otherwise this will cause Maven not to consider it.
+if $cygwin || $mingw ; then
   [ -n "$MAVEN_PROJECTBASEDIR" ] &&
-  MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+  MAVEN_PROJECTBASEDIR=`cygpath --windows "$MAVEN_PROJECTBASEDIR"`
 fi
 
 export MAVEN_PROJECTBASEDIR
@@ -211,7 +238,7 @@ exec "$JAVACMD" \
   -classpath "${CLASSWORLDS_JAR}" \
   "-Dclassworlds.conf=${MAVEN_HOME}/bin/m2.conf" \
   "-Dmaven.home=${MAVEN_HOME}" \
-  "-Dlibrary.jline.path=${MAVEN_HOME}/lib/jline-native" \
+  "-Dlibrary.jline.path=$JLINE_NATIVE_PATH" \
   "-Dorg.jline.terminal.exec.redirectPipeCreationMode=native,reflection" \
   "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
   ${CLASSWORLDS_LAUNCHER} ${MAVEN_ARGS} "$@"
diff --git a/apache-maven/src/test/scripts/test-mvn-path-conversion.sh b/apache-maven/src/test/scripts/test-mvn-path-conversion.sh
new file mode 100644
index 0000000000..3cbc74d8e4
--- /dev/null
+++ b/apache-maven/src/test/scripts/test-mvn-path-conversion.sh
@@ -0,0 +1,250 @@
+#!/bin/sh
+
+# 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.
+
+# -----------------------------------------------------------------------------
+# Tests the Cygwin/MinGW/MSYS2 path conversion performed by bin/mvn.
+#
+# The test runs on any POSIX platform: a throw-away Maven home is populated with
+# the real bin/mvn, and uname(1), cygpath(1) and java(1) are stubbed out so that
+# the Windows-only code paths can be exercised and the resulting JVM command
+# line can be asserted upon.
+#
+# The launcher is always run with a PATH holding nothing but those stubs. That
+# keeps the outcome identical on every platform, and it is what makes the
+# missing-cygpath case meaningful: were the real PATH kept, the genuine
+# cygpath(1) of an actual Cygwin/MSYS2 installation would be picked up and the
+# fallback would never be exercised. Consequently the stubs may only rely on
+# shell built-ins, no external command is reachable from them.
+#
+# Usage: sh apache-maven/src/test/scripts/test-mvn-path-conversion.sh
+#
+# Exits with 0 when all assertions pass, 1 otherwise, so it can be wired into a
+# CI job or a git hook as-is.
+# -----------------------------------------------------------------------------
+
+set -e
+
+script_dir=`cd "\`dirname "$0"\`" && pwd`
+mvn_script="$script_dir/../../bin/mvn"
+
+if [ ! -f "$mvn_script" ]; then
+  echo "Cannot locate the mvn script at $mvn_script" >&2
+  exit 1
+fi
+
+# Absolute path to the shell: it has to be invoked while PATH holds the stubs only.
+sh_bin=`unset -f command; command -v sh`
+
+work_dir=`mktemp -d "${TMPDIR:-/tmp}/mvn-path-conversion.XXXXXX"`
+# Normalize the path: on macOS $TMPDIR ends with '/', producing a double
+# slash that pwd(1) inside the launcher will collapse.  Resolving through
+# cd/pwd keeps the test's reference paths in sync with the launcher output.
+work_dir=`cd "$work_dir" && pwd`
+trap 'rm -rf "$work_dir"' EXIT INT TERM
+
+stub_dir="$work_dir/stubs"
+maven_home="$work_dir/maven-home"
+project_dir="$work_dir/project"
+
+mkdir -p "$stub_dir" "$maven_home/bin" "$maven_home/boot" "$maven_home/lib/jline-native" \
+  "$project_dir/.mvn" "$project_dir/module"
+
+cp "$mvn_script" "$maven_home/bin/mvn"
+touch "$maven_home/bin/m2.conf" "$maven_home/boot/plexus-classworlds-9.9.9.jar"
+
+# uname(1) stub: the emulated OS is taken from the FAKE_UNAME variable.
+cat > "$stub_dir/uname" <<'STUB'
+#!/bin/sh
+echo "${FAKE_UNAME:-Linux}"
+STUB
+
+# dirname(1) stub: bin/mvn needs it to locate the Maven home.
+cat > "$stub_dir/dirname" <<'STUB'
+#!/bin/sh
+path="$1"
+case "$path" in
+  */*)
+    path="${path%/*}"
+    [ -n "$path" ] || path="/"
+    ;;
+  *) path="." ;;
+esac
+printf '%s\n' "$path"
+STUB
+
+# tr(1) stub: concat_lines uses tr -s '\r\n' '  ' to flatten jvm.config.
+cat > "$stub_dir/tr" <<'STUB'
+#!/bin/sh
+# Minimal tr that only handles the concat_lines usage: tr -s '\r\n' '  '
+# Reads stdin and replaces \r and \n with spaces, squeezing repeats.
+while IFS= read -r line || [ -n "$line" ]; do
+  printf '%s ' "$line"
+done
+STUB
+
+# cygpath(1) stub: --windows maps /foo/bar to C:\foo\bar, --unix does the reverse.
+cat > "$stub_dir/cygpath" <<'STUB'
+#!/bin/sh
+for arg in "$@"; do path="$arg"; done
+
+replace() {
+  text="$1"; needle="$2"; value="$3"; out=""
+  while :; do
+    case "$text" in
+      *"$needle"*)
+        out="$out${text%%"$needle"*}$value"
+        text="${text#*"$needle"}"
+        ;;
+      *) break ;;
+    esac
+  done
+  printf '%s' "$out$text"
+}
+
+case " $* " in
+  *" --windows "*) printf 'C:%s\n' "`replace "$path" '/' '\'`" ;;
+  *" --unix "*) printf '%s\n' "`replace "${path#C:}" '\' '/'`" ;;
+  *" --path "*)
+    # --path with --windows
+    case " $* " in
+      *" --windows "*) printf 'C:%s\n' "`replace "$path" '/' '\'`" ;;
+      *" --unix "*) printf '%s\n' "`replace "${path#C:}" '\' '/'`" ;;
+      *) printf '%s\n' "$path" ;;
+    esac ;;
+  *) printf '%s\n' "$path" ;;
+esac
+STUB
+
+# java(1) stub: answers version probes and otherwise echoes the arguments it was
+# invoked with, one bracketed token per argument.
+cat > "$stub_dir/java" <<'STUB'
+#!/bin/sh
+case " $* " in
+  *" -version "*) exit 0 ;;
+esac
+
+for arg in "$@"; do printf '[%s]' "$arg"; done
+printf '\n'
+STUB
+
+chmod +x "$stub_dir/uname" "$stub_dir/dirname" "$stub_dir/cygpath" "$stub_dir/java" \
+  "$stub_dir/tr" "$maven_home/bin/mvn"
+
+# The same stubs, without cygpath, to exercise the fallback.
+nocygpath_dir="$work_dir/stubs-without-cygpath"
+mkdir -p "$nocygpath_dir"
+for stub in uname dirname java tr; do
+  cp "$stub_dir/$stub" "$nocygpath_dir/$stub"
+  chmod +x "$nocygpath_dir/$stub"
+done
+
+failures=0
+
+# run_mvn <uname-output> <stub-dir>
+run_mvn() {
+  ( cd "$project_dir/module" &&
+    FAKE_UNAME="$1" PATH="$2" JAVA_HOME= MAVEN_SKIP_RC=1 \
+      "$sh_bin" "$maven_home/bin/mvn" verify 2>/dev/null )
+}
+
+# to_windows <posix-path>
+to_windows() {
+  text="$1"; out=""
+  while :; do
+    case "$text" in
+      */*)
+        out="$out${text%%/*}\\"
+        text="${text#*/}"
+        ;;
+      *) break ;;
+    esac
+  done
+  printf 'C:%s' "$out$text"
+}
+
+# contains <haystack> <needle>
+# Plain substring check. A case/glob comparison cannot be used, the haystack
+# holds Windows paths and backslashes are escape characters in glob patterns.
+contains() {
+  case "$1" in
+    *"$2"*) return 0 ;;
+    *) return 1 ;;
+  esac
+}
+
+# assert_contains <description> <haystack> <needle>
+assert_contains() {
+  if contains "$2" "$3"; then
+    printf 'ok - %s\n' "$1"
+  else
+    printf 'FAILED - %s\n' "$1"
+    printf '  expected to contain: %s\n' "$3"
+    printf '  actual: %s\n' "$2"
+    failures=`expr $failures + 1`
+  fi
+}
+
+# assert_not_contains <description> <haystack> <needle>
+assert_not_contains() {
+  if contains "$2" "$3"; then
+    printf 'FAILED - %s\n' "$1"
+    printf '  expected NOT to contain: %s\n' "$3"
+    printf '  actual: %s\n' "$2"
+    failures=`expr $failures + 1`
+  else
+    printf 'ok - %s\n' "$1"
+  fi
+}
+
+# POSIX platforms must be left untouched.
+output=`run_mvn Linux "$stub_dir"`
+assert_contains "POSIX: multiModuleProjectDirectory stays a POSIX path" "$output" \
+  "[-Dmaven.multiModuleProjectDirectory=$project_dir]"
+assert_contains "POSIX: maven.home stays a POSIX path" "$output" \
+  "[-Dmaven.home=$maven_home]"
+
+# Every Windows POSIX emulation layer must receive native Windows paths.
+for os in CYGWIN_NT-10.0 MINGW32_NT-6.2 MINGW64_NT-10.0 MSYS_NT-10.0; do
+  output=`run_mvn "$os" "$stub_dir"`
+
+  assert_contains "$os: multiModuleProjectDirectory is a native Windows path" "$output" \
+    "[-Dmaven.multiModuleProjectDirectory=`to_windows "$project_dir"`]"
+  assert_contains "$os: maven.home is a native Windows path" "$output" \
+    "[-Dmaven.home=`to_windows "$maven_home"`]"
+  assert_contains "$os: library.jline.path is a native Windows path" "$output" \
+    "[-Dlibrary.jline.path=`to_windows "$maven_home/lib/jline-native"`]"
+  assert_not_contains "$os: no path mixes both separators" "$output" "\\/"
+done
+
+# Without cygpath the launcher must still work, using unconverted paths, rather
+# than passing empty paths to the JVM.
+output=`run_mvn MINGW64_NT-10.0 "$nocygpath_dir"`
+assert_contains "missing cygpath: multiModuleProjectDirectory falls back to a POSIX path" "$output" \
+  "[-Dmaven.multiModuleProjectDirectory=$project_dir]"
+assert_contains "missing cygpath: maven.home falls back to a POSIX path" "$output" \
+  "[-Dmaven.home=$maven_home]"
+assert_not_contains "missing cygpath: no empty maven.home is passed" "$output" \
+  "[-Dmaven.home=]"
+
+if [ "$failures" -ne 0 ]; then
+  printf '%s assertion(s) failed\n' "$failures"
+  exit 1
+fi
+
+echo "All assertions passed"