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 + *.java dos 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/mvn b/apache-maven/src/assembly/maven/bin/mvn index 8559d47af557..1a8e6a2fdccc 100755 --- a/apache-maven/src/assembly/maven/bin/mvn +++ b/apache-maven/src/assembly/maven/bin/mvn @@ -166,30 +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/#.*$//' | \ - # Replace LF with NUL for xargs - tr '\n' '\0' | \ - # Split into words and process each argument - # Use -0 with NUL to avoid special behaviour on quotes - xargs -n 1 -0 | \ - 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") - - echo "$arg" - 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 @@ -239,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 \ @@ -251,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 a3e8600df3d1..f25f85858f7a 100644 --- a/apache-maven/src/assembly/maven/bin/mvn.cmd +++ b/apache-maven/src/assembly/maven/bin/mvn.cmd @@ -177,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 JVM_CONFIG_MAVEN_OPTS=%JVM_CONFIG_MAVEN_OPTS% :endReadJvmConfig @@ -251,6 +270,11 @@ 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% ^ @@ -286,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/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh10937QuotedPipesInMavenOptsTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh10937QuotedPipesInMavenOptsTest.java index 9d954afb4f35..0765147c8011 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh10937QuotedPipesInMavenOptsTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh10937QuotedPipesInMavenOptsTest.java @@ -42,6 +42,8 @@ void testIt() throws Exception { Verifier verifier = newVerifier(basedir.toString()); verifier.setEnvironmentVariable("MAVEN_OPTS", "-Dprop.maven-opts=\"foo|bar\""); + // Enable debug logging for launcher script to diagnose jvm.config parsing issues + verifier.setEnvironmentVariable("MAVEN_DEBUG_SCRIPT", "1"); verifier.addCliArguments("validate"); verifier.execute(); verifier.verifyErrorFreeLog(); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11363PipeSymbolsInJvmConfigTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11363PipeSymbolsInJvmConfigTest.java new file mode 100644 index 000000000000..4603bf09cf9c --- /dev/null +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11363PipeSymbolsInJvmConfigTest.java @@ -0,0 +1,55 @@ +/* + * 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.it; + +import java.nio.file.Path; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test set for gh-11363: + * Verify that pipe symbols in .mvn/jvm.config are properly handled and don't cause shell command parsing errors. + */ +public class MavenITgh11363PipeSymbolsInJvmConfigTest extends AbstractMavenIntegrationTestCase { + + /** + * Verify that pipe symbols in .mvn/jvm.config are properly handled + */ + @Test + void testPipeSymbolsInJvmConfig() throws Exception { + Path basedir = extractResources("/gh-11363-pipe-symbols-jvm-config") + .getAbsoluteFile() + .toPath(); + + Verifier verifier = newVerifier(basedir.toString()); + verifier.setForkJvm(true); // Use forked JVM to test .mvn/jvm.config processing + // Enable debug logging for launcher script to diagnose jvm.config parsing issues + verifier.setEnvironmentVariable("MAVEN_DEBUG_SCRIPT", "1"); + verifier.addCliArguments("validate"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + Properties props = verifier.loadProperties("target/pom.properties"); + assertEquals("de|*.de|my.company.mirror.de", props.getProperty("project.properties.pom.prop.nonProxyHosts")); + assertEquals("value|with|pipes", props.getProperty("project.properties.pom.prop.with.pipes")); + } +} diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11485AtSignInJvmConfigTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11485AtSignInJvmConfigTest.java new file mode 100644 index 000000000000..c23128946ea7 --- /dev/null +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11485AtSignInJvmConfigTest.java @@ -0,0 +1,88 @@ +/* + * 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.it; + +import java.io.File; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test set for GH-11485: + * Verify that @ character in .mvn/jvm.config values is handled correctly. + * This is important for Jenkins workspaces like workspace/project_PR-350@2 + */ +public class MavenITgh11485AtSignInJvmConfigTest extends AbstractMavenIntegrationTestCase { + + @Test + public void testAtSignInJvmConfig() throws Exception { + File testDir = extractResources("/gh-11485-at-sign"); + + Verifier verifier = newVerifier(testDir.getAbsolutePath()); + verifier.addCliArgument( + "-Dexpression.outputFile=" + new File(testDir, "target/pom.properties").getAbsolutePath()); + verifier.setForkJvm(true); // custom .mvn/jvm.config + // Enable debug logging for launcher script to diagnose jvm.config parsing issues + verifier.setEnvironmentVariable("MAVEN_DEBUG_SCRIPT", "1"); + verifier.addCliArgument("validate"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + Properties props = verifier.loadProperties("target/pom.properties"); + String expectedPath = testDir.getAbsolutePath().replace('\\', '/'); + assertEquals( + expectedPath + "/workspace@2/test", + props.getProperty("project.properties.pathWithAtProp").replace('\\', '/'), + "Path with @ character should be preserved"); + assertEquals( + "value@test", + props.getProperty("project.properties.propWithAtProp"), + "Property value with @ character should be preserved"); + } + + @Test + public void testAtSignInCommandLineProperty() throws Exception { + File testDir = extractResources("/gh-11485-at-sign"); + + Verifier verifier = newVerifier(testDir.getAbsolutePath()); + verifier.addCliArgument( + "-Dexpression.outputFile=" + new File(testDir, "target/pom.properties").getAbsolutePath()); + verifier.setForkJvm(true); // custom .mvn/jvm.config + // Pass a path with @ character via command line (simulating Jenkins workspace) + String jenkinsPath = testDir.getAbsolutePath().replace('\\', '/') + "/jenkins.workspace/proj@2"; + verifier.addCliArgument("-Dcmdline.path=" + jenkinsPath); + verifier.addCliArgument("-Dcmdline.value=test@value"); + verifier.addCliArgument("validate"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + Properties props = verifier.loadProperties("target/pom.properties"); + assertEquals( + jenkinsPath, + props.getProperty("project.properties.cmdlinePath").replace('\\', '/'), + "Command-line path with @ character should be preserved"); + assertEquals( + "test@value", + props.getProperty("project.properties.cmdlineValue"), + "Command-line value with @ character should be preserved"); + } +} + diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4559SpacesInJvmOptsTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4559SpacesInJvmOptsTest.java index e7279842264d..78799608bdef 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4559SpacesInJvmOptsTest.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng4559SpacesInJvmOptsTest.java @@ -42,6 +42,8 @@ void testIt() throws Exception { Verifier verifier = newVerifier(basedir.toString()); verifier.setEnvironmentVariable("MAVEN_OPTS", "-Dprop.maven-opts=\"foo bar\""); + // Enable debug logging for launcher script to diagnose jvm.config parsing issues + verifier.setEnvironmentVariable("MAVEN_DEBUG_SCRIPT", "1"); verifier.addCliArguments("validate"); verifier.execute(); verifier.verifyErrorFreeLog(); diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6255FixConcatLines.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6255FixConcatLines.java index 687aa9a4dc7f..6ac39e4c8956 100644 --- a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6255FixConcatLines.java +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng6255FixConcatLines.java @@ -46,7 +46,7 @@ class MavenITmng6255FixConcatLines extends AbstractMavenIntegrationTestCase { @Test @Disabled void testJvmConfigFileCR() throws Exception { - runWithLineEndings("\r"); + runWithLineEndings("\r", "cr"); } /** @@ -56,7 +56,7 @@ void testJvmConfigFileCR() throws Exception { */ @Test void testJvmConfigFileLF() throws Exception { - runWithLineEndings("\n"); + runWithLineEndings("\n", "lf"); } /** @@ -66,10 +66,10 @@ void testJvmConfigFileLF() throws Exception { */ @Test void testJvmConfigFileCRLF() throws Exception { - runWithLineEndings("\r\n"); + runWithLineEndings("\r\n", "crlf"); } - protected void runWithLineEndings(String lineEndings) throws Exception { + protected void runWithLineEndings(String lineEndings, String test) throws Exception { File baseDir = extractResources("/mng-6255"); File mvnDir = new File(baseDir, ".mvn"); @@ -77,14 +77,16 @@ protected void runWithLineEndings(String lineEndings) throws Exception { createJvmConfigFile(jvmConfig, lineEndings, "-Djvm.config=ok", "-Xms256m", "-Xmx512m"); Verifier verifier = newVerifier(baseDir.getAbsolutePath()); + // Use different log file for each test to avoid overwriting + verifier.setLogFileName("log-" + test + ".txt"); verifier.addCliArgument( - "-Dexpression.outputFile=" + new File(baseDir, "expression.properties").getAbsolutePath()); + "-Dexpression.outputFile=" + new File(baseDir, "expression-" + test + ".properties").getAbsolutePath()); verifier.setForkJvm(true); // custom .mvn/jvm.config verifier.addCliArgument("validate"); verifier.execute(); verifier.verifyErrorFreeLog(); - Properties props = verifier.loadProperties("expression.properties"); + Properties props = verifier.loadProperties("expression-" + test + ".properties"); assertEquals("ok", props.getProperty("project.properties.jvm-config")); } diff --git a/its/core-it-suite/src/test/resources/gh-11363-pipe-symbols-jvm-config/.mvn/jvm.config b/its/core-it-suite/src/test/resources/gh-11363-pipe-symbols-jvm-config/.mvn/jvm.config new file mode 100644 index 000000000000..fa129e3da219 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11363-pipe-symbols-jvm-config/.mvn/jvm.config @@ -0,0 +1,3 @@ +# Test for MNG-11363: Maven 4 fails to parse pipe symbols in .mvn/jvm.config +-Dhttp.nonProxyHosts=de|*.de|my.company.mirror.de +-Dprop.with.pipes="value|with|pipes" diff --git a/its/core-it-suite/src/test/resources/gh-11363-pipe-symbols-jvm-config/pom.xml b/its/core-it-suite/src/test/resources/gh-11363-pipe-symbols-jvm-config/pom.xml new file mode 100644 index 000000000000..52f90ad94181 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11363-pipe-symbols-jvm-config/pom.xml @@ -0,0 +1,59 @@ + + + + 4.0.0 + + org.apache.maven.its.mng11363 + test + 1.0 + + Maven Integration Test :: MNG-11363 + Verify that JVM args can contain pipe symbols in .mvn/jvm.config. + + + ${http.nonProxyHosts} + ${prop.with.pipes} + + + + + + org.apache.maven.its.plugins + maven-it-plugin-expression + 2.1-SNAPSHOT + + + test + + eval + + validate + + target/pom.properties + + project/properties + + + + + + + + diff --git a/its/core-it-suite/src/test/resources/gh-11485-at-sign/.mvn/jvm.config b/its/core-it-suite/src/test/resources/gh-11485-at-sign/.mvn/jvm.config new file mode 100644 index 000000000000..ec92d7c5f558 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11485-at-sign/.mvn/jvm.config @@ -0,0 +1,3 @@ +-Dpath.with.at=${MAVEN_PROJECTBASEDIR}/workspace@2/test +-Dprop.with.at=value@test + diff --git a/its/core-it-suite/src/test/resources/gh-11485-at-sign/pom.xml b/its/core-it-suite/src/test/resources/gh-11485-at-sign/pom.xml new file mode 100644 index 000000000000..9fdbc2444b6e --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11485-at-sign/pom.xml @@ -0,0 +1,70 @@ + + + + 4.0.0 + + org.apache.maven.its.gh11485 + test + 1.0 + pom + + Test @ character in jvm.config + + Verify that @ character in jvm.config values is handled correctly. + This is important for Jenkins workspaces like workspace/project_PR-350@2 + + + + ${path.with.at} + ${prop.with.at} + ${cmdline.path} + ${cmdline.value} + + + + + + org.apache.maven.its.plugins + maven-it-plugin-expression + 2.1-SNAPSHOT + + + validate + + eval + + + target/pom.properties + + project/properties/pathWithAtProp + project/properties/propWithAtProp + project/properties/cmdlinePath + project/properties/cmdlineValue + + + + + + + + + diff --git a/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/Verifier.java b/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/Verifier.java index 1904d9715208..d8c678058038 100644 --- a/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/Verifier.java +++ b/its/core-it-support/maven-it-helper/src/main/java/org/apache/maven/it/Verifier.java @@ -296,6 +296,30 @@ public void execute() throws VerificationException { System.err.println("Warning: Could not prepend command line to log file: " + e.getMessage()); } } + + // Save stdout/stderr to files if not empty (captures shell script debug output) + if (logFileName != null) { + String logBaseName = logFileName.endsWith(".txt") + ? logFileName.substring(0, logFileName.length() - 4) + : logFileName; + if (stdout.size() > 0) { + try { + Path stdoutFile = basedir.resolve(logBaseName + "-stdout.txt"); + Files.writeString(stdoutFile, stdout.toString(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + } catch (IOException e) { + System.err.println("Warning: Could not write stdout file: " + e.getMessage()); + } + } + if (stderr.size() > 0) { + try { + Path stderrFile = basedir.resolve(logBaseName + "-stderr.txt"); + Files.writeString(stderrFile, stderr.toString(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + } catch (IOException e) { + System.err.println("Warning: Could not write stderr file: " + e.getMessage()); + } + } + } + if (ret > 0) { String dump; try { @@ -478,15 +502,18 @@ private String formatCommandLine(ExecutorRequest request, ExecutorHelper.Mode mo } } - // Add environment variables that would be set + // Add environment variables that would be set (excluding MAVEN_OPTS which is handled separately) if (request.environmentVariables().isPresent() && !request.environmentVariables().get().isEmpty()) { cmdLine.append("\n# Environment variables:"); for (Map.Entry entry : request.environmentVariables().get().entrySet()) { - cmdLine.append("\n# ").append(entry.getKey()).append("=").append(entry.getValue()); + if (!"MAVEN_OPTS".equals(entry.getKey())) { + cmdLine.append("\n# ").append(entry.getKey()).append("=").append(entry.getValue()); + } } } - // Add JVM arguments that would be set via MAVEN_OPTS + // Compute the final MAVEN_OPTS value (combining env var + jvmArgs) + // This matches what ForkedMavenExecutor does List jvmArgs = new ArrayList<>(); if (!request.userHomeDirectory().equals(ExecutorRequest.getCanonicalPath(Paths.get(System.getProperty("user.home"))))) { jvmArgs.add("-Duser.home=" + request.userHomeDirectory().toString()); @@ -500,8 +527,23 @@ private String formatCommandLine(ExecutorRequest request, ExecutorHelper.Mode mo .toList()); } + // Build the final MAVEN_OPTS value + StringBuilder mavenOpts = new StringBuilder(); + if (request.environmentVariables().isPresent()) { + String existingMavenOpts = request.environmentVariables().get().get("MAVEN_OPTS"); + if (existingMavenOpts != null && !existingMavenOpts.isEmpty()) { + mavenOpts.append(existingMavenOpts); + } + } if (!jvmArgs.isEmpty()) { - cmdLine.append("\n# MAVEN_OPTS=").append(String.join(" ", jvmArgs)); + if (mavenOpts.length() > 0) { + mavenOpts.append(" "); + } + mavenOpts.append(String.join(" ", jvmArgs)); + } + + if (mavenOpts.length() > 0) { + cmdLine.append("\n# MAVEN_OPTS=").append(mavenOpts.toString()); } if (request.skipMavenRc()) {