From eb9a65ac2923c72e694404f400b1a293e07d78eb Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Sun, 26 Jul 2026 01:14:50 +0200 Subject: [PATCH] [MNG-7541] Add Maven 4 PowerShell launcher Add cross-platform PowerShell entry points with Maven 4 launcher parity, argument-safe configuration handling, wrapper modes, packaged-launcher tests, and CI coverage for PowerShell 7 and Windows PowerShell 5.1. --- .github/workflows/maven.yml | 10 + apache-maven/src/assembly/component.xml | 1 + apache-maven/src/assembly/maven/bin/mvn.ps1 | 359 ++++++++++++++++++ .../src/assembly/maven/bin/mvnDebug.ps1 | 22 ++ .../src/assembly/maven/bin/mvnenc.ps1 | 21 + apache-maven/src/assembly/maven/bin/mvnsh.ps1 | 21 + apache-maven/src/assembly/maven/bin/mvnup.ps1 | 21 + .../src/assembly/maven/bin/mvnyjp.ps1 | 22 ++ .../test/powershell/MavenLauncher.Tests.ps1 | 266 +++++++++++++ 9 files changed, 743 insertions(+) create mode 100644 apache-maven/src/assembly/maven/bin/mvn.ps1 create mode 100644 apache-maven/src/assembly/maven/bin/mvnDebug.ps1 create mode 100644 apache-maven/src/assembly/maven/bin/mvnenc.ps1 create mode 100644 apache-maven/src/assembly/maven/bin/mvnsh.ps1 create mode 100644 apache-maven/src/assembly/maven/bin/mvnup.ps1 create mode 100644 apache-maven/src/assembly/maven/bin/mvnyjp.ps1 create mode 100644 apache-maven/src/test/powershell/MavenLauncher.Tests.ps1 diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 675dbc4394d8..e1ab84b59238 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -212,6 +212,16 @@ jobs: echo "MAVEN_HOME=$PWD/maven-local" >> $GITHUB_ENV echo "$PWD/maven-local/bin" >> $GITHUB_PATH + - name: Test PowerShell 7 launcher + if: matrix.java == '17' + shell: pwsh + run: ./apache-maven/src/test/powershell/MavenLauncher.Tests.ps1 -MavenHome "$env:MAVEN_HOME" + + - name: Test Windows PowerShell 5.1 launcher + if: runner.os == 'Windows' && matrix.java == '17' + shell: powershell + run: ./apache-maven/src/test/powershell/MavenLauncher.Tests.ps1 -MavenHome "$env:MAVEN_HOME" + - name: Build with downloaded Maven shell: bash run: mvn verify -Papache-release -Dgpg.skip=true -e -B -V diff --git a/apache-maven/src/assembly/component.xml b/apache-maven/src/assembly/component.xml index 5f55a310c8bd..e63fb186d3f2 100644 --- a/apache-maven/src/assembly/component.xml +++ b/apache-maven/src/assembly/component.xml @@ -69,6 +69,7 @@ under the License. *.cmd *.conf *.java + *.ps1 dos diff --git a/apache-maven/src/assembly/maven/bin/mvn.ps1 b/apache-maven/src/assembly/maven/bin/mvn.ps1 new file mode 100644 index 000000000000..800c1b714917 --- /dev/null +++ b/apache-maven/src/assembly/maven/bin/mvn.ps1 @@ -0,0 +1,359 @@ +<# +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. +#> + +<#----------------------------------------------------------------------------- +Apache Maven Startup Script + +Environment Variable Prerequisites + + JAVA_HOME (Optional) Points to a Java installation. + MAVEN_ARGS (Optional) Arguments passed to Maven before CLI arguments. + MAVEN_OPTS (Optional) Java runtime options used when Maven is executed. + MAVEN_SKIP_RC (Optional) Flag to disable loading of mavenrc files. + MAVEN_DEBUG_OPTS (Optional) Specify the debug options used by --debug. + MAVEN_DEBUG_ADDRESS (Optional) Debug address. Default is localhost:8000. +-----------------------------------------------------------------------------#> + +$script:IsWindowsPlatform = $env:OS -eq "Windows_NT" -or $PSVersionTable.PSEdition -eq "Desktop" + +function Write-MavenError { + param([string] $Message) + + [Console]::Error.WriteLine($Message) +} + +function Write-MavenDebug { + param([string] $Message) + + if ($env:MAVEN_DEBUG_SCRIPT) { + [Console]::Error.WriteLine("[DEBUG] $Message") + } +} + +function Import-MavenRc { + if ($env:MAVEN_SKIP_RC) { + return + } + + [string[]] $rcFiles = @() + if ($script:IsWindowsPlatform) { + if ($env:PROGRAMDATA) { + $rcFiles += Join-Path $env:PROGRAMDATA "mavenrc.ps1" + } + if ($env:USERPROFILE) { + $rcFiles += Join-Path $env:USERPROFILE "mavenrc.ps1" + } + } + else { + $rcFiles += "/usr/local/etc/mavenrc.ps1" + $rcFiles += "/etc/mavenrc.ps1" + if ($env:HOME) { + $rcFiles += Join-Path $env:HOME ".mavenrc.ps1" + } + } + + foreach ($rcFile in $rcFiles) { + if (Test-Path -LiteralPath $rcFile -PathType Leaf) { + Write-MavenDebug "Loading Maven RC file: $rcFile" + . $rcFile + } + } +} + +function ConvertFrom-MavenOptionString { + param([AllowEmptyString()][string] $Value) + + $result = New-Object "System.Collections.Generic.List[string]" + if ([string]::IsNullOrWhiteSpace($Value)) { + return $result.ToArray() + } + + $current = New-Object System.Text.StringBuilder + $inDoubleQuotes = $false + $inSingleQuotes = $false + + foreach ($character in $Value.ToCharArray()) { + if ($character -eq '"' -and -not $inSingleQuotes) { + $inDoubleQuotes = -not $inDoubleQuotes + } + elseif ($character -eq "'" -and -not $inDoubleQuotes) { + $inSingleQuotes = -not $inSingleQuotes + } + elseif ([char]::IsWhiteSpace($character) -and -not $inDoubleQuotes -and -not $inSingleQuotes) { + if ($current.Length -gt 0) { + $result.Add($current.ToString()) + [void] $current.Clear() + } + } + else { + [void] $current.Append($character) + } + } + + if ($current.Length -gt 0) { + $result.Add($current.ToString()) + } + + return $result.ToArray() +} + +function Get-MavenJavaCommand { + $javaExecutable = if ($script:IsWindowsPlatform) { "java.exe" } else { "java" } + + if ($env:JAVA_HOME) { + $javaCommand = Join-Path (Join-Path $env:JAVA_HOME "bin") $javaExecutable + if (-not (Test-Path -LiteralPath $javaCommand -PathType Leaf)) { + Write-MavenError "The JAVA_HOME environment variable is not defined correctly, so Apache Maven cannot be started." + Write-MavenError "JAVA_HOME is set to `"$env:JAVA_HOME`", but `"$javaCommand`" does not exist." + throw "Java executable not found" + } + return (Get-Item -LiteralPath $javaCommand).FullName + } + + $java = Get-Command $javaExecutable -CommandType Application -ErrorAction SilentlyContinue | + Select-Object -First 1 + if (-not $java) { + Write-MavenError "The java command does not exist in PATH nor is JAVA_HOME set, so Apache Maven cannot be started." + throw "Java executable not found" + } + + return $java.Source +} + +function Test-MavenJavaVersion { + param([string] $JavaCommand) + + & $JavaCommand --enable-native-access=ALL-UNNAMED -version *> $null + if ($LASTEXITCODE -ne 0) { + Write-MavenError "Error: Apache Maven 4.x requires Java 17 or newer to run." + & $JavaCommand -version + Write-MavenError "Please upgrade your Java installation or set JAVA_HOME to point to a compatible JDK." + throw "Unsupported Java version" + } +} + +function Get-MavenHome { + return (Get-Item -LiteralPath (Join-Path $PSScriptRoot "..")).FullName +} + +function Get-FileArgumentBaseDirectory { + param([string[]] $Arguments) + + $baseDirectory = (Get-Location).ProviderPath + for ($index = 0; $index -lt $Arguments.Count; $index++) { + if ($Arguments[$index] -ne "-f" -and $Arguments[$index] -ne "--file") { + continue + } + + if ($index + 1 -ge $Arguments.Count) { + return $baseDirectory + } + + $fileArgument = $Arguments[$index + 1] + if (-not (Test-Path -LiteralPath $fileArgument)) { + Write-MavenError "POM file $fileArgument specified with the -f/--file command line argument does not exist" + throw "POM file not found" + } + + $item = Get-Item -LiteralPath $fileArgument + if ($item.PSIsContainer) { + return $item.FullName + } + + if (-not $item.Directory) { + Write-MavenError "Directory extracted from the -f/--file command-line argument $fileArgument does not exist" + throw "POM directory not found" + } + return $item.Directory.FullName + } + + return $baseDirectory +} + +function Get-MavenProjectBaseDirectory { + param([string[]] $Arguments) + + $baseDirectory = Get-FileArgumentBaseDirectory -Arguments $Arguments + $searchDirectory = Get-Item -LiteralPath $baseDirectory + while ($searchDirectory) { + if (Test-Path -LiteralPath (Join-Path $searchDirectory.FullName ".mvn") -PathType Container) { + return $searchDirectory.FullName + } + + $parent = $searchDirectory.Parent + if (-not $parent -or $parent.FullName -eq $searchDirectory.FullName) { + break + } + $searchDirectory = $parent + } + + return (Get-Item -LiteralPath $baseDirectory).FullName +} + +function Get-MavenLauncherJar { + param([string] $MavenHome) + + $launcherJars = @(Get-ChildItem -LiteralPath (Join-Path $MavenHome "boot") -Filter "plexus-classworlds-*.jar") + if ($launcherJars.Count -ne 1) { + Write-MavenError "Expected one plexus-classworlds launcher JAR in `"$MavenHome`", found $($launcherJars.Count)." + throw "Maven launcher JAR not found" + } + + return $launcherJars[0].FullName +} + +function Get-MavenJvmConfigArguments { + param( + [string] $JavaCommand, + [string] $MavenHome, + [string] $ProjectBaseDirectory + ) + + $jvmConfig = Join-Path (Join-Path $ProjectBaseDirectory ".mvn") "jvm.config" + if (-not (Test-Path -LiteralPath $jvmConfig -PathType Leaf)) { + return @() + } + + $parser = Join-Path (Join-Path $MavenHome "bin") "JvmConfigParser.java" + Write-MavenDebug "Found jvm.config file at: $jvmConfig" + Write-MavenDebug "Running JvmConfigParser with Java: $JavaCommand" + + $parserOutput = @(& $JavaCommand $parser $jvmConfig $ProjectBaseDirectory 2>&1) + $parserExitCode = $LASTEXITCODE + Write-MavenDebug "JvmConfigParser exit code: $parserExitCode" + if ($parserExitCode -ne 0) { + Write-MavenError "ERROR: JvmConfigParser failed with exit code $parserExitCode" + Write-MavenError " jvm.config path: $jvmConfig" + Write-MavenError " Maven basedir: $ProjectBaseDirectory" + Write-MavenError " Java command: $JavaCommand" + foreach ($line in $parserOutput) { + Write-MavenError " $line" + } + throw "JVM configuration parsing failed" + } + + $joinedOutput = ($parserOutput | ForEach-Object { "$_" }) -join [Environment]::NewLine + return @(ConvertFrom-MavenOptionString -Value $joinedOutput) +} + +function Format-MavenDebugArgument { + param([string] $Argument) + + if ($Argument -match '[\s"]') { + return '"' + $Argument.Replace('"', '\"') + '"' + } + return $Argument +} + +function Invoke-MavenLauncher { + param([string[]] $Arguments) + + Import-MavenRc + + $mavenHome = Get-MavenHome + $javaCommand = Get-MavenJavaCommand + Test-MavenJavaVersion -JavaCommand $javaCommand + + $projectBaseDirectory = Get-MavenProjectBaseDirectory -Arguments $Arguments + $launcherJar = Get-MavenLauncherJar -MavenHome $mavenHome + $classworldsConfig = Join-Path (Join-Path $mavenHome "bin") "m2.conf" + $jlineNativeDirectory = Join-Path (Join-Path $mavenHome "lib") "jline-native" + + [string[]] $internalMavenOptions = @() + [string[]] $mavenOptions = @(ConvertFrom-MavenOptionString -Value $env:MAVEN_OPTS) + [string[]] $jvmConfigOptions = @(Get-MavenJvmConfigArguments ` + -JavaCommand $javaCommand ` + -MavenHome $mavenHome ` + -ProjectBaseDirectory $projectBaseDirectory) + [string[]] $debugOptions = @(ConvertFrom-MavenOptionString -Value $env:MAVEN_DEBUG_OPTS) + $mainClass = "org.apache.maven.cling.MavenCling" + + foreach ($argument in $Arguments) { + switch ($argument) { + "--debug" { + if ($debugOptions.Count -eq 0) { + $debugAddress = if ($env:MAVEN_DEBUG_ADDRESS) { $env:MAVEN_DEBUG_ADDRESS } else { "localhost:8000" } + $debugOptions = @("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=$debugAddress") + } + else { + Write-Output "Ignoring --debug option as MAVEN_DEBUG_OPTS is already set" + } + } + "--yjp" { + if (-not $env:YJPLIB -or -not (Test-Path -LiteralPath $env:YJPLIB -PathType Leaf)) { + Write-MavenError "Error: Unable to autodetect the YJP library location. Please set YJPLIB variable" + throw "YourKit library not found" + } + $internalMavenOptions += "-agentpath:$env:YJPLIB=onexit=snapshot,onexit=memory,tracing,onlylocal" + } + "--enc" { + $mainClass = "org.apache.maven.cling.MavenEncCling" + } + "--shell" { + $mainClass = "org.apache.maven.cling.MavenShellCling" + } + "--up" { + $mainClass = "org.apache.maven.cling.MavenUpCling" + } + } + } + + [string[]] $javaArguments = @() + $javaArguments += $internalMavenOptions + $javaArguments += $mavenOptions + $javaArguments += $jvmConfigOptions + $javaArguments += $debugOptions + $javaArguments += @( + "--enable-native-access=ALL-UNNAMED" + "-classpath" + $launcherJar + "-Dclassworlds.conf=$classworldsConfig" + "-Dmaven.home=$mavenHome" + "-Dmaven.mainClass=$mainClass" + "-Dlibrary.jline.path=$jlineNativeDirectory" + "-Dmaven.multiModuleProjectDirectory=$projectBaseDirectory" + "org.codehaus.plexus.classworlds.launcher.Launcher" + ) + if ($mainClass -eq "org.apache.maven.cling.MavenCling") { + $javaArguments += @(ConvertFrom-MavenOptionString -Value $env:MAVEN_ARGS) + } + $javaArguments += $Arguments + + if ($env:MAVEN_DEBUG_SCRIPT) { + $debugCommand = (@($javaCommand) + $javaArguments | + ForEach-Object { Format-MavenDebugArgument -Argument $_ }) -join " " + Write-MavenDebug "Launching JVM with command:" + Write-MavenDebug " $debugCommand" + } + + & $javaCommand @javaArguments + $script:MavenProcessExitCode = $LASTEXITCODE +} + +try { + Invoke-MavenLauncher -Arguments $args + exit $script:MavenProcessExitCode +} +catch { + if ($_.Exception.Message -and + $_.Exception.Message -notmatch "^(Java executable|Unsupported Java|POM|Maven launcher|JVM configuration|YourKit)") { + Write-MavenError $_.Exception.Message + } + exit 1 +} diff --git a/apache-maven/src/assembly/maven/bin/mvnDebug.ps1 b/apache-maven/src/assembly/maven/bin/mvnDebug.ps1 new file mode 100644 index 000000000000..2e60231717b4 --- /dev/null +++ b/apache-maven/src/assembly/maven/bin/mvnDebug.ps1 @@ -0,0 +1,22 @@ +<# +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. +#> + +Write-Output "This script is deprecated for removal, please use 'mvn --debug' instead" +& (Join-Path $PSScriptRoot "mvn.ps1") "--debug" @args +exit $LASTEXITCODE diff --git a/apache-maven/src/assembly/maven/bin/mvnenc.ps1 b/apache-maven/src/assembly/maven/bin/mvnenc.ps1 new file mode 100644 index 000000000000..d3538b1889b0 --- /dev/null +++ b/apache-maven/src/assembly/maven/bin/mvnenc.ps1 @@ -0,0 +1,21 @@ +<# +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. +#> + +& (Join-Path $PSScriptRoot "mvn.ps1") "--enc" @args +exit $LASTEXITCODE diff --git a/apache-maven/src/assembly/maven/bin/mvnsh.ps1 b/apache-maven/src/assembly/maven/bin/mvnsh.ps1 new file mode 100644 index 000000000000..dbdae63b97cd --- /dev/null +++ b/apache-maven/src/assembly/maven/bin/mvnsh.ps1 @@ -0,0 +1,21 @@ +<# +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. +#> + +& (Join-Path $PSScriptRoot "mvn.ps1") "--shell" @args +exit $LASTEXITCODE diff --git a/apache-maven/src/assembly/maven/bin/mvnup.ps1 b/apache-maven/src/assembly/maven/bin/mvnup.ps1 new file mode 100644 index 000000000000..2164359e2f74 --- /dev/null +++ b/apache-maven/src/assembly/maven/bin/mvnup.ps1 @@ -0,0 +1,21 @@ +<# +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. +#> + +& (Join-Path $PSScriptRoot "mvn.ps1") "--up" @args +exit $LASTEXITCODE diff --git a/apache-maven/src/assembly/maven/bin/mvnyjp.ps1 b/apache-maven/src/assembly/maven/bin/mvnyjp.ps1 new file mode 100644 index 000000000000..bded679a4034 --- /dev/null +++ b/apache-maven/src/assembly/maven/bin/mvnyjp.ps1 @@ -0,0 +1,22 @@ +<# +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. +#> + +Write-Output "This script is deprecated for removal, please use 'mvn --yjp' instead" +& (Join-Path $PSScriptRoot "mvn.ps1") "--yjp" @args +exit $LASTEXITCODE diff --git a/apache-maven/src/test/powershell/MavenLauncher.Tests.ps1 b/apache-maven/src/test/powershell/MavenLauncher.Tests.ps1 new file mode 100644 index 000000000000..cb67c24e1712 --- /dev/null +++ b/apache-maven/src/test/powershell/MavenLauncher.Tests.ps1 @@ -0,0 +1,266 @@ +<# +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. +#> + +param( + [Parameter(Mandatory = $true)] + [string] $MavenHome +) + +$ErrorActionPreference = "Stop" +$MavenHome = (Get-Item -LiteralPath $MavenHome).FullName +$binDirectory = Join-Path $MavenHome "bin" +$temporaryRoot = Join-Path ([IO.Path]::GetTempPath()) ("maven-powershell-tests-" + [guid]::NewGuid().ToString("N")) + +function Assert-Equal { + param( + $Expected, + $Actual, + [string] $Message + ) + + if ($Expected -ne $Actual) { + throw "$Message Expected: <$Expected>; actual: <$Actual>." + } +} + +function Assert-Contains { + param( + [string] $Value, + [string] $Expected, + [string] $Message + ) + + if (-not $Value.Contains($Expected)) { + throw "$Message Missing text: <$Expected>. Output:`n$Value" + } +} + +function Assert-NotContains { + param( + [string] $Value, + [string] $Unexpected, + [string] $Message + ) + + if ($Value.Contains($Unexpected)) { + throw "$Message Unexpected text: <$Unexpected>. Output:`n$Value" + } +} + +function Invoke-Launcher { + param( + [string] $ScriptName, + [string[]] $Arguments + ) + + $scriptPath = Join-Path $binDirectory $ScriptName + $originalErrorWriter = [Console]::Error + $capturedErrorWriter = New-Object IO.StringWriter + [Console]::SetError($capturedErrorWriter) + try { + $output = @(& $scriptPath @Arguments 2>&1 | ForEach-Object { "$_" }) + $exitCode = $LASTEXITCODE + } + finally { + [Console]::SetError($originalErrorWriter) + } + + $capturedError = $capturedErrorWriter.ToString().TrimEnd() + if ($capturedError) { + $output += $capturedError + } + return [pscustomobject]@{ + ExitCode = $exitCode + Output = $output -join [Environment]::NewLine + } +} + +function Test-ScriptSyntax { + $failed = $false + Get-ChildItem -LiteralPath $binDirectory -Filter "*.ps1" | ForEach-Object { + $tokens = $null + $parseErrors = $null + [System.Management.Automation.Language.Parser]::ParseFile( + $_.FullName, + [ref] $tokens, + [ref] $parseErrors + ) > $null + + foreach ($parseError in $parseErrors) { + $failed = $true + Write-Error "$($_.Name):$($parseError.Extent.StartLineNumber):$($parseError.Extent.StartColumnNumber): $($parseError.Message)" + } + } + + if ($failed) { + throw "One or more PowerShell launcher scripts have syntax errors." + } +} + +$environmentNames = @( + "HOME", + "MAVEN_ARGS", + "MAVEN_DEBUG_ADDRESS", + "MAVEN_DEBUG_OPTS", + "MAVEN_DEBUG_SCRIPT", + "MAVEN_OPTS", + "MAVEN_SKIP_RC", + "PROGRAMDATA", + "USERPROFILE", + "YJPLIB" +) +$savedEnvironment = @{} +foreach ($name in $environmentNames) { + $savedEnvironment[$name] = [Environment]::GetEnvironmentVariable($name) +} + +try { + New-Item -ItemType Directory -Path $temporaryRoot > $null + $projectDirectory = Join-Path $temporaryRoot "project with spaces" + $nestedDirectory = Join-Path $projectDirectory "nested" + $mavenDirectory = Join-Path $projectDirectory ".mvn" + New-Item -ItemType Directory -Path $nestedDirectory > $null + New-Item -ItemType Directory -Path $mavenDirectory > $null + + [IO.File]::WriteAllText( + (Join-Path $projectDirectory "pom.xml"), + "4.0.0testlauncher1" + ) + [IO.File]::WriteAllLines( + (Join-Path $mavenDirectory "jvm.config"), + @( + "-Dps.jvm=`"jvm value`"", + "-Dps.pipe=`"foo|bar`"", + "-Dps.at=@literal" + ) + ) + + foreach ($name in $environmentNames) { + [Environment]::SetEnvironmentVariable($name, $null) + } + + Test-ScriptSyntax + Write-Output "[PASS] launcher scripts parse" + + $version = Invoke-Launcher -ScriptName "mvn.ps1" -Arguments @("--version") + Assert-Equal 0 $version.ExitCode "mvn.ps1 --version should succeed." + Assert-Contains $version.Output "Apache Maven" "Version output should identify Maven." + Write-Output "[PASS] core launcher executes Maven" + + $invalid = Invoke-Launcher -ScriptName "mvn.ps1" -Arguments @("--not-a-real-option") + if ($invalid.ExitCode -eq 0) { + throw "Invalid Maven arguments should return a nonzero exit code." + } + Write-Output "[PASS] launcher propagates Maven failures" + + $env:MAVEN_DEBUG_SCRIPT = "1" + $env:MAVEN_OPTS = "-Dps.maven=`"maven value`" -Dps.ampersand=`"foo&bar`"" + $env:MAVEN_ARGS = "-Dps.maven.args=`"argument value`"" + Push-Location $nestedDirectory + try { + $quoted = Invoke-Launcher -ScriptName "mvn.ps1" -Arguments @("--version") + } + finally { + Pop-Location + } + Assert-Equal 0 $quoted.ExitCode "Launcher with quoted options should succeed." + Assert-Contains $quoted.Output '"-Dps.maven=maven value"' "MAVEN_OPTS should preserve spaces." + Assert-Contains $quoted.Output '"-Dps.jvm=jvm value"' "jvm.config should preserve spaces." + Assert-Contains $quoted.Output "-Dps.pipe=foo|bar" "jvm.config should preserve pipe characters." + Assert-Contains $quoted.Output "-Dps.at=@literal" "jvm.config should preserve at signs." + Assert-Contains $quoted.Output '"-Dps.maven.args=argument value"' "MAVEN_ARGS should preserve spaces." + Assert-Contains $quoted.Output "-Dmaven.multiModuleProjectDirectory=$projectDirectory" "The nearest .mvn directory should be used." + Write-Output "[PASS] options, jvm.config, and project discovery preserve arguments" + + $fileResult = Invoke-Launcher -ScriptName "mvn.ps1" -Arguments @( + "-f", + (Join-Path $projectDirectory "pom.xml"), + "--version" + ) + Assert-Equal 0 $fileResult.ExitCode "-f with an existing POM should succeed." + Assert-Contains $fileResult.Output "-Dmaven.multiModuleProjectDirectory=$projectDirectory" "-f should select the POM directory." + + $missingResult = Invoke-Launcher -ScriptName "mvn.ps1" -Arguments @( + "-f", + (Join-Path $temporaryRoot "missing.xml"), + "--version" + ) + Assert-Equal 1 $missingResult.ExitCode "-f with a missing POM should fail." + Write-Output "[PASS] -f file selection and validation work" + + $env:MAVEN_DEBUG_OPTS = "-Dps.debug=true" + $debugResult = Invoke-Launcher -ScriptName "mvnDebug.ps1" -Arguments @("--version") + Assert-Equal 0 $debugResult.ExitCode "mvnDebug.ps1 should delegate successfully." + Assert-Contains $debugResult.Output "deprecated for removal" "The debug wrapper should report deprecation." + Write-Output "[PASS] debug wrapper delegates and preserves custom debug options" + + $env:MAVEN_ARGS = "-Dshould.not.reach.specialized.launchers=true" + $modeTests = @( + @("mvnenc.ps1", "org.apache.maven.cling.MavenEncCling"), + @("mvnsh.ps1", "org.apache.maven.cling.MavenShellCling"), + @("mvnup.ps1", "org.apache.maven.cling.MavenUpCling") + ) + foreach ($modeTest in $modeTests) { + $modeResult = Invoke-Launcher -ScriptName $modeTest[0] -Arguments @("--help") + Assert-Equal 0 $modeResult.ExitCode "$($modeTest[0]) --help should succeed." + Assert-Contains $modeResult.Output "-Dmaven.mainClass=$($modeTest[1])" "$($modeTest[0]) should select its MavenCling class." + Assert-NotContains $modeResult.Output "-Dshould.not.reach.specialized.launchers=true" "MAVEN_ARGS should not reach specialized launchers." + } + Write-Output "[PASS] specialized wrappers select the correct MavenCling classes" + + $env:YJPLIB = $null + $yjpResult = Invoke-Launcher -ScriptName "mvnyjp.ps1" -Arguments @("--version") + Assert-Equal 1 $yjpResult.ExitCode "mvnyjp.ps1 should fail when YJPLIB is unavailable." + Assert-Contains $yjpResult.Output "Please set YJPLIB" "YourKit failure should explain the required variable." + Write-Output "[PASS] YourKit validation fails clearly" + + $testHome = Join-Path $temporaryRoot "test home" + New-Item -ItemType Directory -Path $testHome > $null + if ($env:OS -eq "Windows_NT" -or $PSVersionTable.PSEdition -eq "Desktop") { + $env:PROGRAMDATA = Join-Path $temporaryRoot "program data" + $env:USERPROFILE = $testHome + New-Item -ItemType Directory -Path $env:PROGRAMDATA > $null + $rcFile = Join-Path $testHome "mavenrc.ps1" + } + else { + $env:HOME = $testHome + $rcFile = Join-Path $testHome ".mavenrc.ps1" + } + [IO.File]::WriteAllText($rcFile, '$env:MAVEN_OPTS = "-Dps.rc=loaded"') + $env:MAVEN_OPTS = $null + $env:MAVEN_SKIP_RC = $null + $rcResult = Invoke-Launcher -ScriptName "mvn.ps1" -Arguments @("--version") + Assert-Equal 0 $rcResult.ExitCode "A user Maven RC script should load successfully." + Assert-Contains $rcResult.Output "-Dps.rc=loaded" "The user Maven RC script should affect launcher options." + + $env:MAVEN_OPTS = $null + $env:MAVEN_SKIP_RC = "1" + $skipRcResult = Invoke-Launcher -ScriptName "mvn.ps1" -Arguments @("--version") + Assert-Equal 0 $skipRcResult.ExitCode "MAVEN_SKIP_RC should not prevent Maven startup." + Assert-NotContains $skipRcResult.Output "-Dps.rc=loaded" "MAVEN_SKIP_RC should suppress RC scripts." + Write-Output "[PASS] user RC loading and MAVEN_SKIP_RC work" +} +finally { + foreach ($name in $environmentNames) { + [Environment]::SetEnvironmentVariable($name, $savedEnvironment[$name]) + } + if (Test-Path -LiteralPath $temporaryRoot) { + Remove-Item -LiteralPath $temporaryRoot -Recurse -Force + } +}