Skip to content

Commit 6c191d0

Browse files
authored
[maven-4.0.x] Fix special characters in .mvn/jvm.config (fix #11363, #11485 and #11486) (#11365) (#11537)
Replace shell-based jvm.config parsing with a Java-based parser to fix issues with special characters (pipes, @, quotes) that cause shell command errors. Problems fixed: - MNG-11363: Pipe symbols (|) in jvm.config cause shell parsing errors - GH-11485: @ character in paths (common in Jenkins workspaces like project_PR-350@2) causes sed failures - MNG-11486: POSIX compliance issues with xargs -0 on AIX, FreeBSD, etc. Solution: Add JvmConfigParser.java that runs via Java source-launch mode (JDK 11+) to parse jvm.config files. This avoids all shell parsing complexities and works consistently across Unix and Windows platforms. Changes: - Add apache-maven/bin/JvmConfigParser.java: Java parser that handles quoted arguments, comments, line continuations, and ${MAVEN_PROJECTBASEDIR} substitution - Update mvn (Unix): Use JvmConfigParser instead of tr/sed/xargs pipeline - Update mvn.cmd (Windows): Use JvmConfigParser with direct file output to avoid Windows file locking issues with shell redirection - Add MAVEN_DEBUG_SCRIPT environment variable for debug logging in both scripts to aid troubleshooting - Add integration tests for pipe symbols and @ character handling - Improve Verifier to save stdout/stderr to separate files for debugging The parser outputs arguments as quoted strings, preserving special characters that would otherwise be interpreted by the shell. (cherry picked from commit da5f27e)
1 parent dcb335e commit 6c191d0

File tree

14 files changed

+614
-57
lines changed

14 files changed

+614
-57
lines changed

apache-maven/src/assembly/component.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ under the License.
6868
<includes>
6969
<include>*.cmd</include>
7070
<include>*.conf</include>
71+
<include>*.java</include>
7172
</includes>
7273
<lineEnding>dos</lineEnding>
7374
</fileSet>
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import java.io.IOException;
21+
import java.io.Writer;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.nio.file.Paths;
26+
import java.util.ArrayList;
27+
import java.util.List;
28+
29+
/**
30+
* Parses .mvn/jvm.config file for Windows batch/Unix shell scripts.
31+
* This avoids the complexity of parsing special characters (pipes, quotes, etc.) in scripts.
32+
*
33+
* Usage: java JvmConfigParser.java <jvm.config-path> <maven-project-basedir> [output-file]
34+
*
35+
* If output-file is provided, writes result to that file (avoids Windows file locking issues).
36+
* Otherwise, outputs to stdout.
37+
*
38+
* Outputs: Single line with space-separated quoted arguments (safe for batch scripts)
39+
*/
40+
public class JvmConfigParser {
41+
public static void main(String[] args) {
42+
if (args.length < 2 || args.length > 3) {
43+
System.err.println("Usage: java JvmConfigParser.java <jvm.config-path> <maven-project-basedir> [output-file]");
44+
System.exit(1);
45+
}
46+
47+
Path jvmConfigPath = Paths.get(args[0]);
48+
String mavenProjectBasedir = args[1];
49+
Path outputFile = args.length == 3 ? Paths.get(args[2]) : null;
50+
51+
if (!Files.exists(jvmConfigPath)) {
52+
// No jvm.config file - output nothing (create empty file if output specified)
53+
if (outputFile != null) {
54+
try {
55+
Files.writeString(outputFile, "", StandardCharsets.UTF_8);
56+
} catch (IOException e) {
57+
System.err.println("ERROR: Failed to write output file: " + e.getMessage());
58+
System.err.flush();
59+
System.exit(1);
60+
}
61+
}
62+
return;
63+
}
64+
65+
try {
66+
String result = parseJvmConfig(jvmConfigPath, mavenProjectBasedir);
67+
if (outputFile != null) {
68+
// Write directly to file - this ensures proper file handle cleanup on Windows
69+
// Add newline at end for Windows 'for /f' command compatibility
70+
try (Writer writer = Files.newBufferedWriter(outputFile, StandardCharsets.UTF_8)) {
71+
writer.write(result);
72+
if (!result.isEmpty()) {
73+
writer.write(System.lineSeparator());
74+
}
75+
}
76+
} else {
77+
System.out.print(result);
78+
System.out.flush();
79+
}
80+
} catch (IOException e) {
81+
// If jvm.config exists but can't be read, this is a configuration error
82+
// Print clear error and exit with error code to prevent Maven from running
83+
System.err.println("ERROR: Failed to read .mvn/jvm.config: " + e.getMessage());
84+
System.err.println("Please check file permissions and syntax.");
85+
System.err.flush();
86+
System.exit(1);
87+
}
88+
}
89+
90+
/**
91+
* Parse jvm.config file and return formatted arguments.
92+
* Package-private for testing.
93+
*/
94+
static String parseJvmConfig(Path jvmConfigPath, String mavenProjectBasedir) throws IOException {
95+
StringBuilder result = new StringBuilder();
96+
97+
for (String line : Files.readAllLines(jvmConfigPath, StandardCharsets.UTF_8)) {
98+
line = processLine(line, mavenProjectBasedir);
99+
if (line.isEmpty()) {
100+
continue;
101+
}
102+
103+
List<String> parsed = parseArguments(line);
104+
appendQuotedArguments(result, parsed);
105+
}
106+
107+
return result.toString();
108+
}
109+
110+
/**
111+
* Process a single line: remove comments, trim whitespace, and replace placeholders.
112+
*/
113+
private static String processLine(String line, String mavenProjectBasedir) {
114+
// Remove comments
115+
int commentIndex = line.indexOf('#');
116+
if (commentIndex >= 0) {
117+
line = line.substring(0, commentIndex);
118+
}
119+
120+
// Trim whitespace
121+
line = line.trim();
122+
123+
// Replace MAVEN_PROJECTBASEDIR placeholders
124+
line = line.replace("${MAVEN_PROJECTBASEDIR}", mavenProjectBasedir);
125+
line = line.replace("$MAVEN_PROJECTBASEDIR", mavenProjectBasedir);
126+
127+
return line;
128+
}
129+
130+
/**
131+
* Append parsed arguments as quoted strings to the result builder.
132+
*/
133+
private static void appendQuotedArguments(StringBuilder result, List<String> args) {
134+
for (String arg : args) {
135+
if (result.length() > 0) {
136+
result.append(' ');
137+
}
138+
result.append('"').append(arg).append('"');
139+
}
140+
}
141+
142+
/**
143+
* Parse a line into individual arguments, respecting quoted strings.
144+
* Quotes are stripped from the arguments.
145+
*/
146+
private static List<String> parseArguments(String line) {
147+
List<String> args = new ArrayList<>();
148+
StringBuilder current = new StringBuilder();
149+
boolean inDoubleQuotes = false;
150+
boolean inSingleQuotes = false;
151+
152+
for (int i = 0; i < line.length(); i++) {
153+
char c = line.charAt(i);
154+
155+
if (c == '"' && !inSingleQuotes) {
156+
inDoubleQuotes = !inDoubleQuotes;
157+
} else if (c == '\'' && !inDoubleQuotes) {
158+
inSingleQuotes = !inSingleQuotes;
159+
} else if (c == ' ' && !inDoubleQuotes && !inSingleQuotes) {
160+
// Space outside quotes - end of argument
161+
if (current.length() > 0) {
162+
args.add(current.toString());
163+
current.setLength(0);
164+
}
165+
} else {
166+
current.append(c);
167+
}
168+
}
169+
170+
// Add last argument
171+
if (current.length() > 0) {
172+
args.add(current.toString());
173+
}
174+
175+
return args;
176+
}
177+
}

apache-maven/src/assembly/maven/bin/mvn

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -166,30 +166,66 @@ find_file_argument_basedir() {
166166
}
167167

168168
# concatenates all lines of a file and replaces variables
169+
# Uses Java-based parser to handle all special characters correctly
170+
# This avoids shell parsing issues with pipes, quotes, @, and other special characters
171+
# and ensures POSIX compliance (no xargs -0, awk, or complex sed needed)
172+
# Set MAVEN_DEBUG_SCRIPT=1 to enable debug logging
169173
concat_lines() {
170174
if [ -f "$1" ]; then
171-
# First convert all CR to LF using tr
172-
tr '\r' '\n' < "$1" | \
173-
sed -e '/^$/d' -e 's/#.*$//' | \
174-
# Replace LF with NUL for xargs
175-
tr '\n' '\0' | \
176-
# Split into words and process each argument
177-
# Use -0 with NUL to avoid special behaviour on quotes
178-
xargs -n 1 -0 | \
179-
while read -r arg; do
180-
# Replace variables first
181-
arg=$(echo "$arg" | sed \
182-
-e "s@\${MAVEN_PROJECTBASEDIR}@$MAVEN_PROJECTBASEDIR@g" \
183-
-e "s@\$MAVEN_PROJECTBASEDIR@$MAVEN_PROJECTBASEDIR@g")
184-
185-
echo "$arg"
186-
done | \
187-
tr '\n' ' '
175+
# Use Java source-launch mode (JDK 11+) to run JvmConfigParser directly
176+
# This avoids the need for compilation and temporary directories
177+
178+
# Debug logging
179+
if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
180+
echo "[DEBUG] Found jvm.config file at: $1" >&2
181+
echo "[DEBUG] Running JvmConfigParser with Java: $JAVACMD" >&2
182+
echo "[DEBUG] Parser arguments: $MAVEN_HOME/bin/JvmConfigParser.java $1 $MAVEN_PROJECTBASEDIR" >&2
183+
fi
184+
185+
# Verify Java is available
186+
"$JAVACMD" -version >/dev/null 2>&1 || {
187+
echo "Error: Java not found. Please set JAVA_HOME." >&2
188+
return 1
189+
}
190+
191+
# Run the parser using source-launch mode
192+
# Capture both stdout and stderr for comprehensive error reporting
193+
parser_output=$("$JAVACMD" "$MAVEN_HOME/bin/JvmConfigParser.java" "$1" "$MAVEN_PROJECTBASEDIR" 2>&1)
194+
parser_exit=$?
195+
196+
if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
197+
echo "[DEBUG] JvmConfigParser exit code: $parser_exit" >&2
198+
echo "[DEBUG] JvmConfigParser output: $parser_output" >&2
199+
fi
200+
201+
if [ $parser_exit -ne 0 ]; then
202+
# Parser failed - print comprehensive error information
203+
echo "ERROR: JvmConfigParser failed with exit code $parser_exit" >&2
204+
echo " jvm.config path: $1" >&2
205+
echo " Maven basedir: $MAVEN_PROJECTBASEDIR" >&2
206+
echo " Java command: $JAVACMD" >&2
207+
echo " Parser output:" >&2
208+
echo "$parser_output" | sed 's/^/ /' >&2
209+
exit 1
210+
fi
211+
212+
echo "$parser_output"
188213
fi
189214
}
190215

191216
MAVEN_PROJECTBASEDIR="`find_maven_basedir "$@"`"
192-
MAVEN_OPTS="$MAVEN_OPTS `concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config"`"
217+
# Read JVM config and append to MAVEN_OPTS, preserving special characters
218+
_jvm_config="`concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config"`"
219+
if [ -n "$_jvm_config" ]; then
220+
if [ -n "$MAVEN_OPTS" ]; then
221+
MAVEN_OPTS="$MAVEN_OPTS $_jvm_config"
222+
else
223+
MAVEN_OPTS="$_jvm_config"
224+
fi
225+
fi
226+
if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
227+
echo "[DEBUG] Final MAVEN_OPTS: $MAVEN_OPTS" >&2
228+
fi
193229
LAUNCHER_JAR=`echo "$MAVEN_HOME"/boot/plexus-classworlds-*.jar`
194230
LAUNCHER_CLASS=org.codehaus.plexus.classworlds.launcher.Launcher
195231

@@ -239,6 +275,7 @@ handle_args() {
239275
handle_args "$@"
240276
MAVEN_MAIN_CLASS=${MAVEN_MAIN_CLASS:=org.apache.maven.cling.MavenCling}
241277

278+
# Build command string for eval
242279
cmd="\"$JAVACMD\" \
243280
$MAVEN_OPTS \
244281
$MAVEN_DEBUG_OPTS \
@@ -251,13 +288,15 @@ cmd="\"$JAVACMD\" \
251288
\"-Dmaven.multiModuleProjectDirectory=$MAVEN_PROJECTBASEDIR\" \
252289
$LAUNCHER_CLASS \
253290
$MAVEN_ARGS"
291+
254292
# Add remaining arguments with proper quoting
255293
for arg in "$@"; do
256294
cmd="$cmd \"$arg\""
257295
done
258296

259-
# Debug: print the command that will be executed
260-
#echo "About to execute:"
261-
#echo "$cmd"
297+
if [ -n "$MAVEN_DEBUG_SCRIPT" ]; then
298+
echo "[DEBUG] Launching JVM with command:" >&2
299+
echo "[DEBUG] $cmd" >&2
300+
fi
262301

263302
eval exec "$cmd"

0 commit comments

Comments
 (0)