#Requires -Version 5.1 <# .SYNOPSIS 将 JAR 包转换为 Windows EXE 可执行文件(基于 Launch4j) .DESCRIPTION 自动下载 Launch4j(首次运行),生成配置文件并调用 Launch4j 生成 EXE。 EXE 为启动器,运行时会调用本机已安装的 JRE 执行 JAR。 .PARAMETER JarPath 输入的 JAR 文件路径(必填) .PARAMETER OutputExe 输出的 EXE 文件路径。默认与 JAR 同目录、同名 .exe .PARAMETER MainClass 主类全名,例如 com.example.Main。JAR 已含 Main-Class 时可省略 .PARAMETER AppTitle 应用标题,用于错误提示窗口 .PARAMETER MinJreVersion 最低 JRE 版本,默认 1.8.0 .PARAMETER IconPath 可选的应用图标 (.ico) .PARAMETER HeaderType gui = 无控制台窗口;console = 显示控制台。默认 gui .PARAMETER DontWrapJar 为 true 时 EXE 与 JAR 分离(JAR 不嵌入 EXE),默认 false(嵌入 JAR) .PARAMETER Launch4jDir Launch4j 安装目录,默认 tools/jar2exe/launch4j .EXAMPLE .\jar2exe.ps1 -JarPath "..\..\dist\mesclient.jar" -MainClass "com.mes.ui.MesClient" .EXAMPLE .\jar2exe.ps1 -JarPath "app.jar" -OutputExe "app.exe" -MainClass "com.example.App" -IconPath "app.ico" #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$JarPath, [string]$OutputExe = "", [string]$MainClass = "", [string]$AppTitle = "Java Application", [string]$MinJreVersion = "1.8.0", [string]$IconPath = "", [ValidateSet("gui", "console")] [string]$HeaderType = "gui", [bool]$DontWrapJar = $false, [string]$Launch4jDir = "" ) $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 function Write-Step([string]$Message) { Write-Host "[jar2exe] $Message" -ForegroundColor Cyan } function Write-Ok([string]$Message) { Write-Host "[jar2exe] $Message" -ForegroundColor Green } function Write-Err([string]$Message) { Write-Host "[jar2exe] $Message" -ForegroundColor Red } function Resolve-FullPath([string]$Path) { if ([string]::IsNullOrWhiteSpace($Path)) { return "" } return (Resolve-Path -LiteralPath $Path).Path } function Ensure-Launch4j([string]$Dir) { $launch4jExe = Join-Path $Dir "launch4j.exe" if (Test-Path -LiteralPath $launch4jExe) { return $launch4jExe } Write-Step "未检测到 Launch4j,正在下载..." $zipUrl = "https://downloads.sourceforge.net/project/launch4j/launch4j-3/3.50/launch4j-3.50-win32.zip" $zipPath = Join-Path $env:TEMP "launch4j-3.50-win32.zip" try { Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing } catch { throw "下载 Launch4j 失败: $($_.Exception.Message)`n请手动下载并解压到: $Dir`n下载地址: $zipUrl" } if (-not (Test-Path -LiteralPath $Dir)) { New-Item -ItemType Directory -Path $Dir -Force | Out-Null } Expand-Archive -LiteralPath $zipPath -DestinationPath $Dir -Force $nestedDir = Join-Path $Dir "launch4j" if (Test-Path -LiteralPath (Join-Path $nestedDir "launch4j.exe")) { Get-ChildItem -LiteralPath $nestedDir | ForEach-Object { $target = Join-Path $Dir $_.Name if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target -Recurse -Force } Move-Item -LiteralPath $_.FullName -Destination $target -Force } Remove-Item -LiteralPath $nestedDir -Recurse -Force -ErrorAction SilentlyContinue } if (-not (Test-Path -LiteralPath $launch4jExe)) { throw "Launch4j 解压后未找到 launch4j.exe,请检查目录: $Dir" } Write-Ok "Launch4j 已就绪: $launch4jExe" return $launch4jExe } function Get-JarMainClass([string]$JarFile) { $jarTool = Join-Path $env:JAVA_HOME "bin\jar.exe" if (-not (Test-Path -LiteralPath $jarTool)) { $jarTool = "jar" } $manifest = & $jarTool xf $JarFile META-INF/MANIFEST.MF 2>$null $manifestPath = Join-Path (Get-Location) "META-INF\MANIFEST.MF" if (-not (Test-Path -LiteralPath $manifestPath)) { return "" } $content = Get-Content -LiteralPath $manifestPath -Encoding UTF8 Remove-Item -LiteralPath "META-INF" -Recurse -Force -ErrorAction SilentlyContinue foreach ($line in $content) { if ($line -match '^Main-Class:\s*(.+)$') { return $Matches[1].Trim() } } return "" } function New-Launch4jConfig { param( [string]$ConfigPath, [string]$JarFile, [string]$ExeFile, [string]$MainClassName, [string]$Title, [string]$JreMin, [string]$Icon, [string]$Header, [bool]$SeparateJar ) $jarAbs = $JarFile.Replace("\", "/") $exeAbs = $ExeFile.Replace("\", "/") $iconXml = "" if (-not [string]::IsNullOrWhiteSpace($Icon)) { $iconAbs = $Icon.Replace("\", "/") $iconXml = " $iconAbs" } $xml = @" $($SeparateJar.ToString().ToLower()) $Header $jarAbs $exeAbs $Title . normal https://www.java.com/download/ false false $iconXml false false $JreMin preferJre 64/32 "@ $utf8NoBom = New-Object System.Text.UTF8Encoding $false [System.IO.File]::WriteAllText($ConfigPath, $xml, $utf8NoBom) } # ---------- 主流程 ---------- $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path if ([string]::IsNullOrWhiteSpace($Launch4jDir)) { $Launch4jDir = Join-Path $scriptDir "launch4j" } if (-not (Test-Path -LiteralPath $JarPath)) { Write-Err "找不到 JAR 文件: $JarPath" exit 1 } $jarFull = Resolve-FullPath $JarPath if ([string]::IsNullOrWhiteSpace($OutputExe)) { $OutputExe = [System.IO.Path]::ChangeExtension($jarFull, ".exe") } else { $parent = Split-Path -Parent $OutputExe if (-not [string]::IsNullOrWhiteSpace($parent) -and -not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } if (Test-Path -LiteralPath $OutputExe) { $OutputExe = (Resolve-Path -LiteralPath $OutputExe).Path } else { $OutputExe = [System.IO.Path]::GetFullPath($OutputExe) } } if ([string]::IsNullOrWhiteSpace($MainClass)) { Push-Location (Split-Path -Parent $jarFull) try { $MainClass = Get-JarMainClass -JarFile $jarFull } finally { Pop-Location } if ([string]::IsNullOrWhiteSpace($MainClass)) { Write-Err "无法从 JAR 读取 Main-Class,请通过 -MainClass 参数指定主类" exit 1 } Write-Step "从 JAR 读取主类: $MainClass" } if (-not [string]::IsNullOrWhiteSpace($IconPath)) { if (-not (Test-Path -LiteralPath $IconPath)) { Write-Err "找不到图标文件: $IconPath" exit 1 } $IconPath = Resolve-FullPath $IconPath } $exeDir = Split-Path -Parent $OutputExe if (-not [string]::IsNullOrWhiteSpace($exeDir) -and -not (Test-Path -LiteralPath $exeDir)) { New-Item -ItemType Directory -Path $exeDir -Force | Out-Null } Write-Step "JAR: $jarFull" Write-Step "EXE: $OutputExe" Write-Step "主类: $MainClass" $launch4jExe = Ensure-Launch4j -Dir $Launch4jDir $configPath = Join-Path $exeDir "launch4j-config.xml" New-Launch4jConfig -ConfigPath $configPath ` -JarFile $jarFull ` -ExeFile $OutputExe ` -MainClassName $MainClass ` -Title $AppTitle ` -JreMin $MinJreVersion ` -Icon $IconPath ` -Header $HeaderType ` -SeparateJar $DontWrapJar Write-Step "正在生成 EXE..." $process = Start-Process -FilePath $launch4jExe -ArgumentList "`"$configPath`"" -Wait -PassThru -NoNewWindow if ($process.ExitCode -ne 0) { Write-Err "Launch4j 执行失败,退出码: $($process.ExitCode)" Write-Err "配置文件: $configPath" exit $process.ExitCode } if (-not (Test-Path -LiteralPath $OutputExe)) { Write-Err "EXE 未生成,请检查 Launch4j 日志" exit 1 } Write-Ok "转换完成!" Write-Ok "EXE 文件: $OutputExe" Write-Host "" Write-Host "说明:" -ForegroundColor Yellow Write-Host " - EXE requires JRE $MinJreVersion or higher on target machine" Write-Host " - If using -DontWrapJar, distribute JAR and EXE together" Write-Host " - For bundled JRE, configure bundled JRE path in Launch4j and regenerate" exit 0