<# .SYNOPSIS 在 Windows 上把 Parking Simulator 打包成免安装的 app-image(含 zip)。 .DESCRIPTION 产物路径:dist\win\ParkingSimulator\ (目录形式,双击 ParkingSimulator.exe 直接运行) dist\win\ParkingSimulator-win-x64.zip(同上目录的 zip 压缩包,便于分发) 依赖: - JDK 25(含 jlink / jpackage / javac / jar) - Maven 3.9+ 进阶:如果想生成 .exe 安装向导,请先安装 WiX Toolset 3.x,再放开文件末尾被注释的那段。 .EXAMPLE PS> .\build-win.ps1 #> $ErrorActionPreference = "Stop" $OriginalLocation = Get-Location try { Set-Location (Join-Path $PSScriptRoot "..") $AppName = "ParkingSimulator" $AppVersion = "1.0.0" $AppVendor = "Fujica" $MainModule = "com.fujica.parkingtool" $MainClass = "com.fujica.parkingtool.App" $OutDir = "dist\win" $RuntimeDir = "target\runtime-win" function Require-Cmd([string]$Name) { if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { throw "缺少命令:$Name" } } Require-Cmd mvn Require-Cmd jlink Require-Cmd jpackage Require-Cmd javac Require-Cmd jar # 校验当前会话用的是 JDK 25,避免误用低版本 javac 编译失败 # 报错样例:Fatal error compiling: 无效的标记: --module-path $JavacVer = ((& javac -version 2>&1) | Out-String).Trim() if ($JavacVer -notmatch 'javac\s+25(\.|$)') { $msg = "当前 javac 不是 JDK 25(实际:$JavacVer)。`r`n" + "请将 JAVA_HOME 指向 JDK 25 后再次运行:`r`n" + " `$env:JAVA_HOME = 'C:\Program Files\Eclipse Adoptium\jdk-25.x.x-hotspot'`r`n" + " `$env:Path = `"`$env:JAVA_HOME\bin;`$env:Path`"`r`n" + "JDK 25 下载:https://adoptium.net/zh-CN/temurin/releases" throw $msg } Write-Host " javac: $JavacVer" Write-Host "==> [1/5] Maven 构建 + 拉取依赖到 target\modules" & mvn -q -DskipTests clean package if ($LASTEXITCODE -ne 0) { throw "Maven 构建失败" } $ModulePath = "target\modules" # 防御性清理:删掉 OpenJFX 偶尔会带过来的 < 1KB 空 stub jar Get-ChildItem -Path $ModulePath -Filter "*.jar" | Where-Object { $_.Length -lt 1024 } | ForEach-Object { Write-Host " 清理空 stub: $($_.Name)" Remove-Item -Force $_.FullName } # --------------------------------------------------------------------------- # 给 Paho v3(automatic module)补一份真正的 module-info,否则 jlink 拒绝它。 # 必须包含: # - uses org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory # - provides ... TCP / SSL / WebSocket / WebSocketSecure 4 个工厂 # 否则运行期会抛 ServiceConfigurationError: module ... does not declare `uses` # --------------------------------------------------------------------------- Write-Host "==> [2/5] 给 Paho jar 注入 module-info" $PahoJarFile = Get-ChildItem -Path $ModulePath -Filter "org.eclipse.paho.client.mqttv3-*.jar" | Select-Object -First 1 if (-not $PahoJarFile) { throw "找不到 Paho mqttv3 jar 在 $ModulePath\" } $PahoJar = $PahoJarFile.FullName # 绝对路径,子目录内仍可引用 $PatchTmp = Join-Path $env:TEMP ("paho-modinfo-" + [System.IO.Path]::GetRandomFileName()) New-Item -ItemType Directory -Path $PatchTmp | Out-Null try { $ExpDir = Join-Path $PatchTmp "exploded" New-Item -ItemType Directory -Path $ExpDir | Out-Null Push-Location $ExpDir & jar xf $PahoJar $JarExtract = $LASTEXITCODE Pop-Location if ($JarExtract -ne 0) { throw "解压 Paho jar 失败" } $ModuleInfo = @" module org.eclipse.paho.client.mqttv3 { requires transitive java.logging; requires transitive java.prefs; exports org.eclipse.paho.client.mqttv3; exports org.eclipse.paho.client.mqttv3.internal; exports org.eclipse.paho.client.mqttv3.internal.security; exports org.eclipse.paho.client.mqttv3.internal.websocket; exports org.eclipse.paho.client.mqttv3.internal.wire; exports org.eclipse.paho.client.mqttv3.logging; exports org.eclipse.paho.client.mqttv3.persist; exports org.eclipse.paho.client.mqttv3.spi; exports org.eclipse.paho.client.mqttv3.util; uses org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory; provides org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory with org.eclipse.paho.client.mqttv3.internal.TCPNetworkModuleFactory, org.eclipse.paho.client.mqttv3.internal.SSLNetworkModuleFactory, org.eclipse.paho.client.mqttv3.internal.websocket.WebSocketNetworkModuleFactory, org.eclipse.paho.client.mqttv3.internal.websocket.WebSocketSecureNetworkModuleFactory; } "@ $ModuleInfoFile = Join-Path $PatchTmp "module-info.java" Set-Content -Path $ModuleInfoFile -Value $ModuleInfo -Encoding ASCII & javac --patch-module "org.eclipse.paho.client.mqttv3=$ExpDir" -d $PatchTmp $ModuleInfoFile if ($LASTEXITCODE -ne 0) { throw "javac 生成 module-info.class 失败" } Push-Location $PatchTmp & jar uf $PahoJar "module-info.class" $JarUpdate = $LASTEXITCODE Pop-Location if ($JarUpdate -ne 0) { throw "回写 Paho jar 失败" } Write-Host (" Paho 已注入 module-info → " + $PahoJarFile.Name) } finally { Remove-Item -Recurse -Force $PatchTmp -ErrorAction SilentlyContinue } # 自动发现需要进入 runtime 的模块集合 $RuntimeModules = @( $MainModule, "javafx.base", "javafx.graphics", "javafx.controls", "javafx.swing", "org.eclipse.paho.client.mqttv3", "jdk.localedata", "jdk.crypto.ec", "jdk.crypto.cryptoki" ) -join "," Write-Host "==> [3/5] jlink 生成最小运行时 → $RuntimeDir" if (Test-Path $RuntimeDir) { Remove-Item -Recurse -Force $RuntimeDir } & jlink ` --module-path "$env:JAVA_HOME\jmods;$ModulePath" ` --add-modules $RuntimeModules ` --no-header-files ` --no-man-pages ` --strip-debug ` --compress=zip-9 ` --ignore-signing-information ` --output $RuntimeDir if ($LASTEXITCODE -ne 0) { throw "jlink 失败" } Write-Host "==> [4/5] jpackage 生成 app-image → $OutDir" if (Test-Path $OutDir) { Remove-Item -Recurse -Force $OutDir } New-Item -ItemType Directory -Path $OutDir | Out-Null # --------------------------------------------------------------------------- # 准备 Windows 图标: # - 期望 src\main\resources\icon.ico(多分辨率:16 / 32 / 48 / 256) # - 如果只有 icon.png,建议先用 ImageMagick 等工具转换: # magick icon.png -define icon:auto-resize=256,128,64,48,32,16 icon.ico # --------------------------------------------------------------------------- $IconArgs = @() if (Test-Path "src\main\resources\icon.ico") { $IconArgs = @("--icon", "src\main\resources\icon.ico") Write-Host " 使用图标:src\main\resources\icon.ico" } elseif (Test-Path "src\main\resources\com\fujica\parkingtool\icon.ico") { $IconArgs = @("--icon", "src\main\resources\com\fujica\parkingtool\icon.ico") Write-Host " 使用图标:src\main\resources\com\fujica\parkingtool\icon.ico" } elseif (Test-Path "src\main\resources\icon.png") { Write-Host " ⚠️ Windows 需要 .ico 格式,未找到 icon.ico,本次将使用默认图标" Write-Host " 可用 ImageMagick 生成:magick icon.png -define icon:auto-resize=256,128,64,48,32,16 icon.ico" } $JpackageArgs = @( "--type", "app-image", "--name", $AppName, "--app-version", $AppVersion, "--vendor", $AppVendor, "--module-path", $ModulePath, "--module", "$MainModule/$MainClass", "--runtime-image", $RuntimeDir, "--dest", $OutDir, "--java-options", "-Xms128m", "--java-options", "-Xmx1024m", "--java-options", "-Dprism.lcdtext=false", "--java-options", "--enable-native-access=javafx.graphics" ) + $IconArgs & jpackage @JpackageArgs if ($LASTEXITCODE -ne 0) { throw "jpackage 失败" } Write-Host "==> [5/5] 压缩成 zip 便于分发" $ZipName = "$AppName-win-x64.zip" $ZipPath = Join-Path $OutDir $ZipName if (Test-Path $ZipPath) { Remove-Item -Force $ZipPath } Compress-Archive -Path (Join-Path $OutDir $AppName) -DestinationPath $ZipPath -CompressionLevel Optimal $AppDir = Join-Path $OutDir $AppName $AppExe = Join-Path $AppDir "$AppName.exe" $AppSize = "{0:N1} MB" -f ((Get-ChildItem -Recurse $AppDir | Measure-Object -Property Length -Sum).Sum / 1MB) $ZipSize = "{0:N1} MB" -f ((Get-Item $ZipPath).Length / 1MB) Write-Host "" Write-Host "✅ 完成" Write-Host " app: $AppDir ($AppSize)" Write-Host " exe: $AppExe" Write-Host " zip: $ZipPath ($ZipSize)" Write-Host " 解压 zip 后双击 $AppName.exe 即可运行,无需安装 JDK。" # === 可选:生成 .exe 安装向导(需要预装 WiX Toolset 3.x,并把 candle/light 加到 PATH) === # $InstallerArgs = @( # "--type", "exe", # "--name", $AppName, # "--app-version", $AppVersion, # "--vendor", $AppVendor, # "--module-path", $ModulePath, # "--module", "$MainModule/$MainClass", # "--runtime-image", $RuntimeDir, # "--dest", $OutDir, # "--win-dir-chooser", # "--win-shortcut", # "--win-menu", # "--win-menu-group", $AppVendor # ) + $IconArgs # & jpackage @InstallerArgs } finally { Set-Location $OriginalLocation }