jar2exe.ps1 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. #Requires -Version 5.1
  2. <#
  3. .SYNOPSIS
  4. 将 JAR 包转换为 Windows EXE 可执行文件(基于 Launch4j)
  5. .DESCRIPTION
  6. 自动下载 Launch4j(首次运行),生成配置文件并调用 Launch4j 生成 EXE。
  7. EXE 为启动器,运行时会调用本机已安装的 JRE 执行 JAR。
  8. .PARAMETER JarPath
  9. 输入的 JAR 文件路径(必填)
  10. .PARAMETER OutputExe
  11. 输出的 EXE 文件路径。默认与 JAR 同目录、同名 .exe
  12. .PARAMETER MainClass
  13. 主类全名,例如 com.example.Main。JAR 已含 Main-Class 时可省略
  14. .PARAMETER AppTitle
  15. 应用标题,用于错误提示窗口
  16. .PARAMETER MinJreVersion
  17. 最低 JRE 版本,默认 1.8.0
  18. .PARAMETER IconPath
  19. 可选的应用图标 (.ico)
  20. .PARAMETER HeaderType
  21. gui = 无控制台窗口;console = 显示控制台。默认 gui
  22. .PARAMETER DontWrapJar
  23. 为 true 时 EXE 与 JAR 分离(JAR 不嵌入 EXE),默认 false(嵌入 JAR)
  24. .PARAMETER Launch4jDir
  25. Launch4j 安装目录,默认 tools/jar2exe/launch4j
  26. .EXAMPLE
  27. .\jar2exe.ps1 -JarPath "..\..\dist\mesclient.jar" -MainClass "com.mes.ui.MesClient"
  28. .EXAMPLE
  29. .\jar2exe.ps1 -JarPath "app.jar" -OutputExe "app.exe" -MainClass "com.example.App" -IconPath "app.ico"
  30. #>
  31. [CmdletBinding()]
  32. param(
  33. [Parameter(Mandatory = $true)]
  34. [string]$JarPath,
  35. [string]$OutputExe = "",
  36. [string]$MainClass = "",
  37. [string]$AppTitle = "Java Application",
  38. [string]$MinJreVersion = "1.8.0",
  39. [string]$IconPath = "",
  40. [ValidateSet("gui", "console")]
  41. [string]$HeaderType = "gui",
  42. [bool]$DontWrapJar = $false,
  43. [string]$Launch4jDir = ""
  44. )
  45. $ErrorActionPreference = "Stop"
  46. [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
  47. function Write-Step([string]$Message) {
  48. Write-Host "[jar2exe] $Message" -ForegroundColor Cyan
  49. }
  50. function Write-Ok([string]$Message) {
  51. Write-Host "[jar2exe] $Message" -ForegroundColor Green
  52. }
  53. function Write-Err([string]$Message) {
  54. Write-Host "[jar2exe] $Message" -ForegroundColor Red
  55. }
  56. function Resolve-FullPath([string]$Path) {
  57. if ([string]::IsNullOrWhiteSpace($Path)) {
  58. return ""
  59. }
  60. return (Resolve-Path -LiteralPath $Path).Path
  61. }
  62. function Ensure-Launch4j([string]$Dir) {
  63. $launch4jExe = Join-Path $Dir "launch4j.exe"
  64. if (Test-Path -LiteralPath $launch4jExe) {
  65. return $launch4jExe
  66. }
  67. Write-Step "未检测到 Launch4j,正在下载..."
  68. $zipUrl = "https://downloads.sourceforge.net/project/launch4j/launch4j-3/3.50/launch4j-3.50-win32.zip"
  69. $zipPath = Join-Path $env:TEMP "launch4j-3.50-win32.zip"
  70. try {
  71. Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing
  72. }
  73. catch {
  74. throw "下载 Launch4j 失败: $($_.Exception.Message)`n请手动下载并解压到: $Dir`n下载地址: $zipUrl"
  75. }
  76. if (-not (Test-Path -LiteralPath $Dir)) {
  77. New-Item -ItemType Directory -Path $Dir -Force | Out-Null
  78. }
  79. Expand-Archive -LiteralPath $zipPath -DestinationPath $Dir -Force
  80. $nestedDir = Join-Path $Dir "launch4j"
  81. if (Test-Path -LiteralPath (Join-Path $nestedDir "launch4j.exe")) {
  82. Get-ChildItem -LiteralPath $nestedDir | ForEach-Object {
  83. $target = Join-Path $Dir $_.Name
  84. if (Test-Path -LiteralPath $target) {
  85. Remove-Item -LiteralPath $target -Recurse -Force
  86. }
  87. Move-Item -LiteralPath $_.FullName -Destination $target -Force
  88. }
  89. Remove-Item -LiteralPath $nestedDir -Recurse -Force -ErrorAction SilentlyContinue
  90. }
  91. if (-not (Test-Path -LiteralPath $launch4jExe)) {
  92. throw "Launch4j 解压后未找到 launch4j.exe,请检查目录: $Dir"
  93. }
  94. Write-Ok "Launch4j 已就绪: $launch4jExe"
  95. return $launch4jExe
  96. }
  97. function Get-JarMainClass([string]$JarFile) {
  98. $jarTool = Join-Path $env:JAVA_HOME "bin\jar.exe"
  99. if (-not (Test-Path -LiteralPath $jarTool)) {
  100. $jarTool = "jar"
  101. }
  102. $manifest = & $jarTool xf $JarFile META-INF/MANIFEST.MF 2>$null
  103. $manifestPath = Join-Path (Get-Location) "META-INF\MANIFEST.MF"
  104. if (-not (Test-Path -LiteralPath $manifestPath)) {
  105. return ""
  106. }
  107. $content = Get-Content -LiteralPath $manifestPath -Encoding UTF8
  108. Remove-Item -LiteralPath "META-INF" -Recurse -Force -ErrorAction SilentlyContinue
  109. foreach ($line in $content) {
  110. if ($line -match '^Main-Class:\s*(.+)$') {
  111. return $Matches[1].Trim()
  112. }
  113. }
  114. return ""
  115. }
  116. function New-Launch4jConfig {
  117. param(
  118. [string]$ConfigPath,
  119. [string]$JarFile,
  120. [string]$ExeFile,
  121. [string]$MainClassName,
  122. [string]$Title,
  123. [string]$JreMin,
  124. [string]$Icon,
  125. [string]$Header,
  126. [bool]$SeparateJar
  127. )
  128. $jarAbs = $JarFile.Replace("\", "/")
  129. $exeAbs = $ExeFile.Replace("\", "/")
  130. $iconXml = ""
  131. if (-not [string]::IsNullOrWhiteSpace($Icon)) {
  132. $iconAbs = $Icon.Replace("\", "/")
  133. $iconXml = " <icon>$iconAbs</icon>"
  134. }
  135. $xml = @"
  136. <?xml version="1.0" encoding="UTF-8"?>
  137. <launch4jConfig>
  138. <dontWrapJar>$($SeparateJar.ToString().ToLower())</dontWrapJar>
  139. <headerType>$Header</headerType>
  140. <jar>$jarAbs</jar>
  141. <outfile>$exeAbs</outfile>
  142. <errTitle>$Title</errTitle>
  143. <cmdLine></cmdLine>
  144. <chdir>.</chdir>
  145. <priority>normal</priority>
  146. <downloadUrl>https://www.java.com/download/</downloadUrl>
  147. <supportUrl></supportUrl>
  148. <stayAlive>false</stayAlive>
  149. <restartOnCrash>false</restartOnCrash>
  150. <manifest></manifest>
  151. $iconXml
  152. <jre>
  153. <path></path>
  154. <bundledJre64Bit>false</bundledJre64Bit>
  155. <bundledJreAsFallback>false</bundledJreAsFallback>
  156. <minVersion>$JreMin</minVersion>
  157. <maxVersion></maxVersion>
  158. <jdkPreference>preferJre</jdkPreference>
  159. <runtimeBits>64/32</runtimeBits>
  160. </jre>
  161. </launch4jConfig>
  162. "@
  163. $utf8NoBom = New-Object System.Text.UTF8Encoding $false
  164. [System.IO.File]::WriteAllText($ConfigPath, $xml, $utf8NoBom)
  165. }
  166. # ---------- 主流程 ----------
  167. $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
  168. if ([string]::IsNullOrWhiteSpace($Launch4jDir)) {
  169. $Launch4jDir = Join-Path $scriptDir "launch4j"
  170. }
  171. if (-not (Test-Path -LiteralPath $JarPath)) {
  172. Write-Err "找不到 JAR 文件: $JarPath"
  173. exit 1
  174. }
  175. $jarFull = Resolve-FullPath $JarPath
  176. if ([string]::IsNullOrWhiteSpace($OutputExe)) {
  177. $OutputExe = [System.IO.Path]::ChangeExtension($jarFull, ".exe")
  178. }
  179. else {
  180. $parent = Split-Path -Parent $OutputExe
  181. if (-not [string]::IsNullOrWhiteSpace($parent) -and -not (Test-Path -LiteralPath $parent)) {
  182. New-Item -ItemType Directory -Path $parent -Force | Out-Null
  183. }
  184. if (Test-Path -LiteralPath $OutputExe) {
  185. $OutputExe = (Resolve-Path -LiteralPath $OutputExe).Path
  186. }
  187. else {
  188. $OutputExe = [System.IO.Path]::GetFullPath($OutputExe)
  189. }
  190. }
  191. if ([string]::IsNullOrWhiteSpace($MainClass)) {
  192. Push-Location (Split-Path -Parent $jarFull)
  193. try {
  194. $MainClass = Get-JarMainClass -JarFile $jarFull
  195. }
  196. finally {
  197. Pop-Location
  198. }
  199. if ([string]::IsNullOrWhiteSpace($MainClass)) {
  200. Write-Err "无法从 JAR 读取 Main-Class,请通过 -MainClass 参数指定主类"
  201. exit 1
  202. }
  203. Write-Step "从 JAR 读取主类: $MainClass"
  204. }
  205. if (-not [string]::IsNullOrWhiteSpace($IconPath)) {
  206. if (-not (Test-Path -LiteralPath $IconPath)) {
  207. Write-Err "找不到图标文件: $IconPath"
  208. exit 1
  209. }
  210. $IconPath = Resolve-FullPath $IconPath
  211. }
  212. $exeDir = Split-Path -Parent $OutputExe
  213. if (-not [string]::IsNullOrWhiteSpace($exeDir) -and -not (Test-Path -LiteralPath $exeDir)) {
  214. New-Item -ItemType Directory -Path $exeDir -Force | Out-Null
  215. }
  216. Write-Step "JAR: $jarFull"
  217. Write-Step "EXE: $OutputExe"
  218. Write-Step "主类: $MainClass"
  219. $launch4jExe = Ensure-Launch4j -Dir $Launch4jDir
  220. $configPath = Join-Path $exeDir "launch4j-config.xml"
  221. New-Launch4jConfig -ConfigPath $configPath `
  222. -JarFile $jarFull `
  223. -ExeFile $OutputExe `
  224. -MainClassName $MainClass `
  225. -Title $AppTitle `
  226. -JreMin $MinJreVersion `
  227. -Icon $IconPath `
  228. -Header $HeaderType `
  229. -SeparateJar $DontWrapJar
  230. Write-Step "正在生成 EXE..."
  231. $process = Start-Process -FilePath $launch4jExe -ArgumentList "`"$configPath`"" -Wait -PassThru -NoNewWindow
  232. if ($process.ExitCode -ne 0) {
  233. Write-Err "Launch4j 执行失败,退出码: $($process.ExitCode)"
  234. Write-Err "配置文件: $configPath"
  235. exit $process.ExitCode
  236. }
  237. if (-not (Test-Path -LiteralPath $OutputExe)) {
  238. Write-Err "EXE 未生成,请检查 Launch4j 日志"
  239. exit 1
  240. }
  241. Write-Ok "转换完成!"
  242. Write-Ok "EXE 文件: $OutputExe"
  243. Write-Host ""
  244. Write-Host "说明:" -ForegroundColor Yellow
  245. Write-Host " - EXE requires JRE $MinJreVersion or higher on target machine"
  246. Write-Host " - If using -DontWrapJar, distribute JAR and EXE together"
  247. Write-Host " - For bundled JRE, configure bundled JRE path in Launch4j and regenerate"
  248. exit 0