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
+ }
+}