jar2exe.ps1 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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. [string]$FileEncoding = "GBK"
  128. )
  129. $jarAbs = $JarFile.Replace("\", "/")
  130. $exeAbs = $ExeFile.Replace("\", "/")
  131. $iconXml = ""
  132. if (-not [string]::IsNullOrWhiteSpace($Icon)) {
  133. $iconAbs = $Icon.Replace("\", "/")
  134. $iconXml = " <icon>$iconAbs</icon>"
  135. }
  136. $encodingOpts = ""
  137. if (-not [string]::IsNullOrWhiteSpace($FileEncoding) -and $FileEncoding -ne "system") {
  138. $encodingOpts = @"
  139. <opt>-Dfile.encoding=$FileEncoding</opt>
  140. <opt>-Dsun.jnu.encoding=$FileEncoding</opt>
  141. <opt>-Dclient.encoding.override=$FileEncoding</opt>
  142. "@
  143. }
  144. $xml = @"
  145. <?xml version="1.0" encoding="UTF-8"?>
  146. <launch4jConfig>
  147. <dontWrapJar>$($SeparateJar.ToString().ToLower())</dontWrapJar>
  148. <headerType>$Header</headerType>
  149. <jar>$jarAbs</jar>
  150. <outfile>$exeAbs</outfile>
  151. <errTitle>$Title</errTitle>
  152. <cmdLine></cmdLine>
  153. <chdir>.</chdir>
  154. <priority>normal</priority>
  155. <downloadUrl>https://www.java.com/download/</downloadUrl>
  156. <supportUrl></supportUrl>
  157. <stayAlive>false</stayAlive>
  158. <restartOnCrash>false</restartOnCrash>
  159. <manifest></manifest>
  160. $iconXml
  161. <jre>
  162. <path></path>
  163. <bundledJre64Bit>false</bundledJre64Bit>
  164. <bundledJreAsFallback>false</bundledJreAsFallback>
  165. <minVersion>$JreMin</minVersion>
  166. <maxVersion></maxVersion>
  167. <jdkPreference>preferJre</jdkPreference>
  168. <runtimeBits>64/32</runtimeBits>
  169. $encodingOpts
  170. </jre>
  171. </launch4jConfig>
  172. "@
  173. $utf8NoBom = New-Object System.Text.UTF8Encoding $false
  174. [System.IO.File]::WriteAllText($ConfigPath, $xml, $utf8NoBom)
  175. }
  176. # ---------- 主流程 ----------
  177. $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
  178. if ([string]::IsNullOrWhiteSpace($Launch4jDir)) {
  179. $Launch4jDir = Join-Path $scriptDir "launch4j"
  180. }
  181. if (-not (Test-Path -LiteralPath $JarPath)) {
  182. Write-Err "找不到 JAR 文件: $JarPath"
  183. exit 1
  184. }
  185. $jarFull = Resolve-FullPath $JarPath
  186. if ([string]::IsNullOrWhiteSpace($OutputExe)) {
  187. $OutputExe = [System.IO.Path]::ChangeExtension($jarFull, ".exe")
  188. }
  189. else {
  190. $parent = Split-Path -Parent $OutputExe
  191. if (-not [string]::IsNullOrWhiteSpace($parent) -and -not (Test-Path -LiteralPath $parent)) {
  192. New-Item -ItemType Directory -Path $parent -Force | Out-Null
  193. }
  194. if (Test-Path -LiteralPath $OutputExe) {
  195. $OutputExe = (Resolve-Path -LiteralPath $OutputExe).Path
  196. }
  197. else {
  198. $OutputExe = [System.IO.Path]::GetFullPath($OutputExe)
  199. }
  200. }
  201. if ([string]::IsNullOrWhiteSpace($MainClass)) {
  202. Push-Location (Split-Path -Parent $jarFull)
  203. try {
  204. $MainClass = Get-JarMainClass -JarFile $jarFull
  205. }
  206. finally {
  207. Pop-Location
  208. }
  209. if ([string]::IsNullOrWhiteSpace($MainClass)) {
  210. Write-Err "无法从 JAR 读取 Main-Class,请通过 -MainClass 参数指定主类"
  211. exit 1
  212. }
  213. Write-Step "从 JAR 读取主类: $MainClass"
  214. }
  215. if (-not [string]::IsNullOrWhiteSpace($IconPath)) {
  216. if (-not (Test-Path -LiteralPath $IconPath)) {
  217. Write-Err "找不到图标文件: $IconPath"
  218. exit 1
  219. }
  220. $IconPath = Resolve-FullPath $IconPath
  221. }
  222. $exeDir = Split-Path -Parent $OutputExe
  223. if (-not [string]::IsNullOrWhiteSpace($exeDir) -and -not (Test-Path -LiteralPath $exeDir)) {
  224. New-Item -ItemType Directory -Path $exeDir -Force | Out-Null
  225. }
  226. Write-Step "JAR: $jarFull"
  227. Write-Step "EXE: $OutputExe"
  228. Write-Step "主类: $MainClass"
  229. $launch4jExe = Ensure-Launch4j -Dir $Launch4jDir
  230. $configPath = Join-Path $exeDir "launch4j-config.xml"
  231. New-Launch4jConfig -ConfigPath $configPath `
  232. -JarFile $jarFull `
  233. -ExeFile $OutputExe `
  234. -MainClassName $MainClass `
  235. -Title $AppTitle `
  236. -JreMin $MinJreVersion `
  237. -Icon $IconPath `
  238. -Header $HeaderType `
  239. -SeparateJar $DontWrapJar `
  240. -FileEncoding "GBK"
  241. Write-Step "正在生成 EXE..."
  242. $process = Start-Process -FilePath $launch4jExe -ArgumentList "`"$configPath`"" -Wait -PassThru -NoNewWindow
  243. if ($process.ExitCode -ne 0) {
  244. Write-Err "Launch4j 执行失败,退出码: $($process.ExitCode)"
  245. Write-Err "配置文件: $configPath"
  246. exit $process.ExitCode
  247. }
  248. if (-not (Test-Path -LiteralPath $OutputExe)) {
  249. Write-Err "EXE 未生成,请检查 Launch4j 日志"
  250. exit 1
  251. }
  252. $iniPath = [System.IO.Path]::ChangeExtension($OutputExe, ".l4j.ini")
  253. @"
  254. # Auto generated by jar2exe - do not delete
  255. -Dfile.encoding=GBK
  256. -Dsun.jnu.encoding=GBK
  257. -Dclient.encoding.override=GBK
  258. -Duser.language=zh
  259. -Duser.country=CN
  260. "@ | Set-Content -LiteralPath $iniPath -Encoding Ascii
  261. Write-Ok "编码配置: $iniPath"
  262. Write-Ok "转换完成!"
  263. Write-Ok "EXE 文件: $OutputExe"
  264. Write-Host ""
  265. Write-Host "说明:" -ForegroundColor Yellow
  266. Write-Host " - EXE requires JRE $MinJreVersion or higher on target machine"
  267. Write-Host " - Keep the .l4j.ini file next to the EXE when distributing"
  268. Write-Host " - If using -DontWrapJar, distribute JAR and EXE together"
  269. Write-Host " - For bundled JRE, configure bundled JRE path in Launch4j and regenerate"
  270. exit 0