From 033146ef5efb4e66dcf1627fb06879db27626160 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Tue, 28 Jul 2026 11:05:17 +0530 Subject: [PATCH 1/2] Convert paths for MSYS2 and jvm.config placeholders in bin/mvn The launcher already converted POSIX paths to their native Windows form for Cygwin and MinGW (GH-7958), but a few paths still reached the JVM in a mangled or mixed form: * uname(1) reports MSYS_NT-* for the MSYS2 shell, which did not match the MINGW* pattern, so no conversion happened at all there. The MSYS2 argument conversion then rewrote the POSIX path into the mixed form D:/a/... instead of D:\a\... * ${MAVEN_PROJECTBASEDIR} placeholders in .mvn/jvm.config were expanded with the POSIX path, since JvmConfigParser was invoked before the conversion took place. The base directory is now converted upfront and both representations are kept: the POSIX one to locate jvm.config from the shell, the native one for everything handed over to the JVM. * -Dlibrary.jline.path was built by appending /lib/jline-native to the already converted MAVEN_HOME, producing C:\maven/lib/jline-native. It is now converted on its own. Values holding a path are printed with printf rather than echo, including the debug logging: several POSIX shells (dash and the other Almquist derivatives commonly installed as /bin/sh) expand backslash escapes in echo, so C:\tmp\build was emitted with a tab in place of \t. The conversion is also guarded, so a missing cygpath(1) warns and falls back to POSIX paths rather than passing empty paths to the JVM. A shell test covering all of the above is added and wired into the build through exec-maven-plugin, so mvn verify runs it. It stubs uname, cygpath and java and runs the launcher with a PATH holding nothing but those stubs, so the Windows-only code paths are exercised identically on every platform; keeping the ambient PATH would let the genuine cygpath(1) of an actual Cygwin/MSYS2 installation be picked up and the missing-cygpath fallback would never be exercised there. Related to GH-12537 --- apache-maven/pom.xml | 36 +++ apache-maven/src/assembly/maven/bin/mvn | 72 +++-- .../test/scripts/test-mvn-path-conversion.sh | 288 ++++++++++++++++++ 3 files changed, 379 insertions(+), 17 deletions(-) create mode 100644 apache-maven/src/test/scripts/test-mvn-path-conversion.sh 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..7cb6821585a6 --- /dev/null +++ b/apache-maven/src/test/scripts/test-mvn-path-conversion.sh @@ -0,0 +1,288 @@ +#!/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"` +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" From 554e692a4745570b5059842af2c7bf75d890dc68 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 28 Jul 2026 08:37:49 +0200 Subject: [PATCH 2/2] Fix #12537: normalize temp dir path in test-mvn-path-conversion.sh On macOS the $TMPDIR variable ends with a trailing slash, so mktemp -d "${TMPDIR}/mvn-path-conversion.XXXXXX" produces a path containing a double slash (e.g. .../T//mvn-path-conversion.XNBzZw). The pwd(1) calls inside the mvn launcher collapse the double slash, causing all 22 path assertions to fail on macOS CI runners. Resolve the work directory through cd/pwd right after creation so the test's reference paths stay in sync with the launcher output. Co-Authored-By: Claude Opus 4.6 --- apache-maven/src/test/scripts/test-mvn-path-conversion.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apache-maven/src/test/scripts/test-mvn-path-conversion.sh b/apache-maven/src/test/scripts/test-mvn-path-conversion.sh index 7cb6821585a6..51d520cc90ae 100644 --- a/apache-maven/src/test/scripts/test-mvn-path-conversion.sh +++ b/apache-maven/src/test/scripts/test-mvn-path-conversion.sh @@ -52,6 +52,10 @@ fi 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"