#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