#!/usr/bin/env bash # # 在 macOS 上把 Parking Simulator 打包成可双击运行的 .app(免装 JDK)。 # 产物路径:dist/mac/ParkingSimulator.app # # 依赖: # - JDK 25(含 jlink / jpackage) # - Maven 3.9+ # # 进阶:取消最后一段注释可一并生成 .dmg 安装包。 set -euo pipefail cd "$(dirname "$0")/.." APP_NAME="ParkingSimulator" APP_VERSION="1.0.0" APP_VENDOR="Fujica" MAIN_MODULE="com.fujica.parkingtool" MAIN_CLASS="com.fujica.parkingtool.App" OUT_DIR="dist/mac" RUNTIME_DIR="target/runtime-mac" # --------------------------------------------------------------------------- # 强制把当前会话锁定到 JDK 25,避免 Maven 走到系统默认的 JDK 8 上 # 报错样例:Fatal error compiling: 无效的标记: --module-path(< JDK 9 的 javac) # --------------------------------------------------------------------------- detect_jdk25() { # 1) Apple 的 java_home 工具 local home if home=$(/usr/libexec/java_home -v 25 2>/dev/null) && [[ -n "${home}" ]]; then echo "${home}"; return 0 fi # 2) Fallback:扫 /Library/Java/JavaVirtualMachines/ 与 ~/Library/Java/JavaVirtualMachines/ local cand for base in /Library/Java/JavaVirtualMachines "${HOME}/Library/Java/JavaVirtualMachines"; do for cand in "${base}"/jdk-25*.jdk "${base}"/temurin-25*.jdk "${base}"/zulu-25*.jdk; do if [[ -x "${cand}/Contents/Home/bin/javac" ]]; then echo "${cand}/Contents/Home"; return 0 fi done done return 1 } if ! JAVA_HOME=$(detect_jdk25); then echo "❌ 未找到 JDK 25,请先安装:" >&2 echo " brew install --cask temurin@25" >&2 echo " 或从 https://adoptium.net/zh-CN/temurin/releases 下载 25 LTS。" >&2 exit 1 fi export JAVA_HOME export PATH="${JAVA_HOME}/bin:${PATH}" echo " 使用 JDK:${JAVA_HOME}" javac -version require() { command -v "$1" >/dev/null 2>&1 || { echo "❌ 缺少命令:$1" >&2 exit 1 } } require mvn require jlink require jpackage require javac require jar echo "==> [1/5] Maven 构建 + 拉取依赖到 target/modules" mvn -q -DskipTests clean package MODULE_PATH="target/modules" # 防御性清理:删掉 OpenJFX 偶尔会带过来的 < 1KB 空 stub jar,避免污染 module-path find "${MODULE_PATH}" -name '*.jar' -size -1k -print -delete || true # --------------------------------------------------------------------------- # 给 Paho v3(automatic module)补一份真正的 module-info,否则 jlink 拒绝它。 # 关键点:必须包含 # - uses org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory # - provides ...with TCP / SSL / WebSocket / WebSocketSecure 4 个工厂 # 否则运行期会抛 ServiceConfigurationError: module ... does not declare `uses` # --------------------------------------------------------------------------- echo "==> [2/5] 给 Paho jar 注入 module-info" PAHO_JAR=$(ls "${MODULE_PATH}"/org.eclipse.paho.client.mqttv3-*.jar 2>/dev/null | head -n 1) if [[ -z "${PAHO_JAR}" || ! -f "${PAHO_JAR}" ]]; then echo "❌ 找不到 Paho mqttv3 jar 在 ${MODULE_PATH}/" >&2 exit 1 fi # 子 shell cd 之后还要能引用,转绝对路径 PAHO_JAR=$(cd "$(dirname "${PAHO_JAR}")" && pwd)/$(basename "${PAHO_JAR}") PATCH_TMP=$(mktemp -d -t paho-modinfo.XXXXXX) trap 'rm -rf "${PATCH_TMP}"' EXIT EXP_DIR="${PATCH_TMP}/exploded" mkdir -p "${EXP_DIR}" ( cd "${EXP_DIR}" && jar xf "${PAHO_JAR}" ) cat > "${PATCH_TMP}/module-info.java" <<'PAHO_MOD_INFO' 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; } PAHO_MOD_INFO # 用解压后的目录做 patch,避免 javac 在 jar 上看不到内嵌包 javac --patch-module org.eclipse.paho.client.mqttv3="${EXP_DIR}" \ -d "${PATCH_TMP}" "${PATCH_TMP}/module-info.java" # 注入进 jar;如有旧的同名条目则覆盖 ( cd "${PATCH_TMP}" && jar uf "${PAHO_JAR}" module-info.class ) echo " Paho 已注入 module-info → $(basename "${PAHO_JAR}")" # 自动发现需要进入 runtime 的模块集合 RUNTIME_MODULES="${MAIN_MODULE},javafx.base,javafx.graphics,javafx.controls,javafx.swing,org.eclipse.paho.client.mqttv3,jdk.localedata,jdk.crypto.ec,jdk.crypto.cryptoki" echo "==> [3/5] jlink 生成最小运行时 → ${RUNTIME_DIR}" rm -rf "${RUNTIME_DIR}" jlink \ --module-path "${JAVA_HOME}/jmods:${MODULE_PATH}" \ --add-modules "${RUNTIME_MODULES}" \ --no-header-files \ --no-man-pages \ --strip-debug \ --compress=zip-9 \ --ignore-signing-information \ --output "${RUNTIME_DIR}" echo "==> [4/5] jpackage 生成 .app → ${OUT_DIR}" rm -rf "${OUT_DIR}" mkdir -p "${OUT_DIR}" # --------------------------------------------------------------------------- # 自动准备 Dock 图标: # 1) 优先使用 src/main/resources/icon.icns(如果用户已自备多分辨率图标) # 2) 否则从 src/main/resources/icon.png 现转: # - 用 sips 生成 16 / 32 / 64 / 128 / 256 / 512 / 1024 + @2x 共 10 张 # - 用 iconutil 打包成 icon.icns # 建议源图 ≥ 1024×1024;尺寸过小会被强行放大、Dock 上糊 # --------------------------------------------------------------------------- ICON_ARG=() if [[ -f "src/main/resources/icon.icns" ]]; then ICON_ARG+=(--icon "src/main/resources/icon.icns") echo " 使用图标:src/main/resources/icon.icns" elif [[ -f "src/main/resources/com/fujica/parkingtool/icon.icns" ]]; then ICON_ARG+=(--icon "src/main/resources/com/fujica/parkingtool/icon.icns") echo " 使用图标:src/main/resources/com/fujica/parkingtool/icon.icns" elif [[ -f "src/main/resources/icon.png" ]]; then SRC_PNG="src/main/resources/icon.png" ICONSET="target/icon.iconset" OUT_ICNS="target/icon.icns" rm -rf "${ICONSET}" mkdir -p "${ICONSET}" # 注意:尺寸 = base × scale;命名规则参考 iconutil 文档 declare -a SIZES=(16 32 32 64 128 256 256 512 512 1024) declare -a NAMES=( "icon_16x16.png" "icon_16x16@2x.png" "icon_32x32.png" "icon_32x32@2x.png" "icon_128x128.png" "icon_128x128@2x.png" "icon_256x256.png" "icon_256x256@2x.png" "icon_512x512.png" "icon_512x512@2x.png" ) for i in "${!SIZES[@]}"; do sips -z "${SIZES[$i]}" "${SIZES[$i]}" "${SRC_PNG}" --out "${ICONSET}/${NAMES[$i]}" >/dev/null done rm -f "${OUT_ICNS}" iconutil --convert icns --output "${OUT_ICNS}" "${ICONSET}" ICON_ARG+=(--icon "${OUT_ICNS}") echo " 已从 ${SRC_PNG} 自动生成 → ${OUT_ICNS}" SRC_W=$(sips -g pixelWidth "${SRC_PNG}" | awk '/pixelWidth/ {print $2}') if [[ -n "${SRC_W}" && "${SRC_W}" -lt 512 ]]; then echo " ⚠️ 源图仅 ${SRC_W}px,Dock 会被放大显示,建议提供 ≥1024×1024 的 icon.png" fi fi jpackage \ --type app-image \ --name "${APP_NAME}" \ --app-version "${APP_VERSION}" \ --vendor "${APP_VENDOR}" \ --module-path "${MODULE_PATH}" \ --module "${MAIN_MODULE}/${MAIN_CLASS}" \ --runtime-image "${RUNTIME_DIR}" \ --dest "${OUT_DIR}" \ --java-options "-Xms128m" \ --java-options "-Xmx1024m" \ --java-options "-Dprism.lcdtext=false" \ --java-options "--enable-native-access=javafx.graphics" \ ${ICON_ARG[@]+"${ICON_ARG[@]}"} echo "==> [5/5] 压缩成 zip 便于分发" ARCH=$(uname -m) ZIP_NAME="${APP_NAME}-mac-${ARCH}.zip" ( cd "${OUT_DIR}" && rm -f "${ZIP_NAME}" && ditto -ck --rsrc --keepParent "${APP_NAME}.app" "${ZIP_NAME}" ) echo "" echo "✅ 完成" du -sh "${OUT_DIR}/${APP_NAME}.app" || true du -sh "${OUT_DIR}/${ZIP_NAME}" || true echo " .app: ${OUT_DIR}/${APP_NAME}.app" echo " .zip: ${OUT_DIR}/${ZIP_NAME}" echo " 双击 .app 运行即可,无需安装 JDK。" # === 可选:生成 .dmg 安装镜像 === # jpackage \ # --type dmg \ # --name "${APP_NAME}" \ # --app-version "${APP_VERSION}" \ # --vendor "${APP_VENDOR}" \ # --module-path "${MODULE_PATH}" \ # --module "${MAIN_MODULE}/${MAIN_CLASS}" \ # --runtime-image "${RUNTIME_DIR}" \ # --dest "${OUT_DIR}" \ # "${ICON_ARG[@]}"