diff --git a/apache-maven/pom.xml b/apache-maven/pom.xml
index c8e8eb213899..13d809a7b4cf 100644
--- a/apache-maven/pom.xml
+++ b/apache-maven/pom.xml
@@ -455,5 +455,41 @@ under the License.
${project.artifactId}
+
+
+ shell-script-tests
+
+
+ unix
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.6.3
+
+
+ test-mvn-path-conversion
+
+ exec
+
+ test
+
+ ${skipTests}
+ sh
+
+ ${project.basedir}/src/test/scripts/test-mvn-path-conversion.sh
+
+
+
+
+
+
+
+
diff --git a/apache-maven/src/assembly/maven/bin/mvn b/apache-maven/src/assembly/maven/bin/mvn
index 85a9a9880535..ea5b284fbc13 100755
--- a/apache-maven/src/assembly/maven/bin/mvn
+++ b/apache-maven/src/assembly/maven/bin/mvn
@@ -46,14 +46,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"
@@ -94,7 +117,7 @@ if [ -n "$JAVA_HOME" ] ; then
if [ ! -x "$JAVACMD" ] ; then
echo "The JAVA_HOME environment variable is not defined correctly, so Apache Maven cannot be started." >&2
- echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" does not exist." >&2
+ printf 'JAVA_HOME is set to "%s", but "$JAVA_HOME/bin/java" does not exist.\n' "$JAVA_HOME" >&2
exit 1
fi
fi
@@ -131,7 +154,7 @@ find_maven_basedir() {
fi
wdir=`cd "$wdir/.."; pwd`
done
- echo "$basedir"
+ printf '%s\n' "$basedir"
)
}
@@ -161,7 +184,7 @@ find_file_argument_basedir() {
found_file_switch=1
fi
done
- echo "$basedir"
+ printf '%s\n' "$basedir"
)
}
@@ -177,9 +200,10 @@ concat_lines() {
# 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
+ printf '[DEBUG] Found jvm.config file at: %s\n' "$1" >&2
+ printf '[DEBUG] Running JvmConfigParser with Java: %s\n' "$JAVACMD" >&2
+ printf '[DEBUG] Parser arguments: %s %s %s\n' \
+ "$MAVEN_HOME/bin/JvmConfigParser.java" "$1" "$MAVEN_PROJECTBASEDIR_NATIVE" >&2
fi
# Verify Java is available
@@ -190,30 +214,40 @@ concat_lines() {
# 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_output=$("$JAVACMD" "$MAVEN_HOME/bin/JvmConfigParser.java" "$1" "$MAVEN_PROJECTBASEDIR_NATIVE" 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
+ printf '[DEBUG] JvmConfigParser output: %s\n' "$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
+ printf ' jvm.config path: %s\n' "$1" >&2
+ printf ' Maven basedir: %s\n' "$MAVEN_PROJECTBASEDIR" >&2
+ printf ' Java command: %s\n' "$JAVACMD" >&2
echo " Parser output:" >&2
- echo "$parser_output" | sed 's/^/ /' >&2
+ printf '%s\n' "$parser_output" | sed 's/^/ /' >&2
exit 1
fi
- echo "$parser_output"
+ printf '%s\n' "$parser_output"
fi
}
MAVEN_PROJECTBASEDIR="`find_maven_basedir "$@"`"
+
+# Under Cygwin/MinGW the POSIX form of the project base directory is required by
+# the shell (to locate .mvn/jvm.config), while the JVM and everything it hands
+# the value over to (plugins, jvm.config placeholders) expects the native
+# Windows form. Compute the latter upfront and keep both around.
+MAVEN_PROJECTBASEDIR_NATIVE="$MAVEN_PROJECTBASEDIR"
+if $cygwin || $mingw ; then
+ MAVEN_PROJECTBASEDIR_NATIVE=`cygpath --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
# 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
@@ -224,10 +258,11 @@ if [ -n "$_jvm_config" ]; then
fi
fi
if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
- echo "[DEBUG] Final MAVEN_OPTS: $MAVEN_OPTS" >&2
+ printf '[DEBUG] Final MAVEN_OPTS: %s\n' "$MAVEN_OPTS" >&2
fi
LAUNCHER_JAR=`echo "$MAVEN_HOME"/boot/plexus-classworlds-*.jar`
LAUNCHER_CLASS=org.codehaus.plexus.classworlds.launcher.Launcher
+JLINE_NATIVE_PATH="$MAVEN_HOME/lib/jline-native"
# For Cygwin and MinGW, switch paths to Windows format before running java(1) command
if $cygwin || $mingw ; then
@@ -236,8 +271,11 @@ if $cygwin || $mingw ; then
LAUNCHER_JAR=`cygpath --windows "$LAUNCHER_JAR"`
CLASSWORLDS_CONF=`cygpath --windows "$CLASSWORLDS_CONF"`
MAVEN_HOME=`cygpath --windows "$MAVEN_HOME"`
- MAVEN_PROJECTBASEDIR=`cygpath --windows "$MAVEN_PROJECTBASEDIR"`
+ # 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
+MAVEN_PROJECTBASEDIR="$MAVEN_PROJECTBASEDIR_NATIVE"
handle_args() {
while [ $# -gt 0 ]; do
@@ -284,7 +322,7 @@ cmd="\"$JAVACMD\" \
\"-Dclassworlds.conf=$CLASSWORLDS_CONF\" \
\"-Dmaven.home=$MAVEN_HOME\" \
\"-Dmaven.mainClass=$MAVEN_MAIN_CLASS\" \
- \"-Dlibrary.jline.path=${MAVEN_HOME}/lib/jline-native\" \
+ \"-Dlibrary.jline.path=$JLINE_NATIVE_PATH\" \
\"-Dmaven.multiModuleProjectDirectory=$MAVEN_PROJECTBASEDIR\" \
$LAUNCHER_CLASS"
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 000000000000..51d520cc90ae
--- /dev/null
+++ b/apache-maven/src/test/scripts/test-mvn-path-conversion.sh
@@ -0,0 +1,292 @@
+#!/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/../../assembly/maven/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" "$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
+
+# 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:}" '\' '/'`" ;;
+ *) printf '%s\n' "$path" ;;
+esac
+STUB
+
+# java(1) stub: answers version probes, emulates JvmConfigParser 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
+
+case "$1" in
+ *JvmConfigParser.java)
+ # Mimics bin/JvmConfigParser: strip comments, expand the base directory
+ # placeholders and print the arguments quoted.
+ expand() {
+ 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"
+ }
+
+ basedir="$3"
+ set -f
+ while IFS= read -r line || [ -n "$line" ]; do
+ line="${line%%#*}"
+ line=`expand "$line" '${MAVEN_PROJECTBASEDIR}' "$basedir"`
+ line=`expand "$line" '$MAVEN_PROJECTBASEDIR' "$basedir"`
+ for token in $line; do
+ printf '"%s" ' "$token"
+ done
+ done < "$2"
+ printf '\n'
+ 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" \
+ "$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; do
+ cp "$stub_dir/$stub" "$nocygpath_dir/$stub"
+ chmod +x "$nocygpath_dir/$stub"
+done
+
+# A jvm.config referencing the project base directory, to assert that the value
+# handed over to the JVM configuration is converted as well.
+echo '-Xmx512m -Dtest.basedir=${MAVEN_PROJECTBASEDIR}' > "$project_dir/.mvn/jvm.config"
+
+failures=0
+
+# run_mvn
+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 )
+}
+
+# run_mvn_debug
+# Same, but with the script debug logging on and stderr captured instead.
+run_mvn_debug() {
+ ( cd "$project_dir/module" &&
+ FAKE_UNAME="$1" PATH="$2" JAVA_HOME= MAVEN_SKIP_RC=1 MAVEN_DEBUG_SCRIPT=1 \
+ "$sh_bin" "$maven_home/bin/mvn" verify 2>&1 >/dev/null )
+}
+
+# to_windows
+to_windows() {
+ text="$1"; out=""
+ while :; do
+ case "$text" in
+ */*)
+ out="$out${text%%/*}\\"
+ text="${text#*/}"
+ ;;
+ *) break ;;
+ esac
+ done
+ printf 'C:%s' "$out$text"
+}
+
+# contains
+# 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
+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
+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: jvm.config placeholder stays a POSIX path" "$output" \
+ "[-Dtest.basedir=$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: jvm.config placeholder is a native Windows path" "$output" \
+ "[-Dtest.basedir=`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=]"
+
+# The debug log must reproduce the Windows paths verbatim. echo expands
+# backslash escapes in dash and friends, turning C:\tmp into C:mp.
+debug_output=`run_mvn_debug MSYS_NT-10.0 "$stub_dir"`
+windows_basedir=`to_windows "$project_dir"`
+
+assert_contains "debug log: parser arguments keep the native base directory" \
+ "$debug_output" "$windows_basedir"
+assert_contains "debug log: MAVEN_OPTS keeps the native base directory" \
+ "$debug_output" "[DEBUG] Final MAVEN_OPTS:"
+assert_not_contains "debug log: no backslash escape was expanded" \
+ "$debug_output" "`printf 'C:\t'`"
+
+if [ "$failures" -ne 0 ]; then
+ printf '%s assertion(s) failed\n' "$failures"
+ exit 1
+fi
+
+echo "All assertions passed"