build-mac.sh 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. #!/usr/bin/env bash
  2. #
  3. # 在 macOS 上把 Parking Simulator 打包成可双击运行的 .app(免装 JDK)。
  4. # 产物路径:dist/mac/ParkingSimulator.app
  5. #
  6. # 依赖:
  7. # - JDK 25(含 jlink / jpackage)
  8. # - Maven 3.9+
  9. #
  10. # 进阶:取消最后一段注释可一并生成 .dmg 安装包。
  11. set -euo pipefail
  12. cd "$(dirname "$0")/.."
  13. APP_NAME="ParkingSimulator"
  14. APP_VERSION="1.0.0"
  15. APP_VENDOR="Fujica"
  16. MAIN_MODULE="com.fujica.parkingtool"
  17. MAIN_CLASS="com.fujica.parkingtool.App"
  18. OUT_DIR="dist/mac"
  19. RUNTIME_DIR="target/runtime-mac"
  20. # ---------------------------------------------------------------------------
  21. # 强制把当前会话锁定到 JDK 25,避免 Maven 走到系统默认的 JDK 8 上
  22. # 报错样例:Fatal error compiling: 无效的标记: --module-path(< JDK 9 的 javac)
  23. # ---------------------------------------------------------------------------
  24. detect_jdk25() {
  25. # 1) Apple 的 java_home 工具
  26. local home
  27. if home=$(/usr/libexec/java_home -v 25 2>/dev/null) && [[ -n "${home}" ]]; then
  28. echo "${home}"; return 0
  29. fi
  30. # 2) Fallback:扫 /Library/Java/JavaVirtualMachines/ 与 ~/Library/Java/JavaVirtualMachines/
  31. local cand
  32. for base in /Library/Java/JavaVirtualMachines "${HOME}/Library/Java/JavaVirtualMachines"; do
  33. for cand in "${base}"/jdk-25*.jdk "${base}"/temurin-25*.jdk "${base}"/zulu-25*.jdk; do
  34. if [[ -x "${cand}/Contents/Home/bin/javac" ]]; then
  35. echo "${cand}/Contents/Home"; return 0
  36. fi
  37. done
  38. done
  39. return 1
  40. }
  41. if ! JAVA_HOME=$(detect_jdk25); then
  42. echo "❌ 未找到 JDK 25,请先安装:" >&2
  43. echo " brew install --cask temurin@25" >&2
  44. echo " 或从 https://adoptium.net/zh-CN/temurin/releases 下载 25 LTS。" >&2
  45. exit 1
  46. fi
  47. export JAVA_HOME
  48. export PATH="${JAVA_HOME}/bin:${PATH}"
  49. echo " 使用 JDK:${JAVA_HOME}"
  50. javac -version
  51. require() {
  52. command -v "$1" >/dev/null 2>&1 || {
  53. echo "❌ 缺少命令:$1" >&2
  54. exit 1
  55. }
  56. }
  57. require mvn
  58. require jlink
  59. require jpackage
  60. require javac
  61. require jar
  62. echo "==> [1/5] Maven 构建 + 拉取依赖到 target/modules"
  63. mvn -q -DskipTests clean package
  64. MODULE_PATH="target/modules"
  65. # 防御性清理:删掉 OpenJFX 偶尔会带过来的 < 1KB 空 stub jar,避免污染 module-path
  66. find "${MODULE_PATH}" -name '*.jar' -size -1k -print -delete || true
  67. # ---------------------------------------------------------------------------
  68. # 给 Paho v3(automatic module)补一份真正的 module-info,否则 jlink 拒绝它。
  69. # 关键点:必须包含
  70. # - uses org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory
  71. # - provides ...with TCP / SSL / WebSocket / WebSocketSecure 4 个工厂
  72. # 否则运行期会抛 ServiceConfigurationError: module ... does not declare `uses`
  73. # ---------------------------------------------------------------------------
  74. echo "==> [2/5] 给 Paho jar 注入 module-info"
  75. PAHO_JAR=$(ls "${MODULE_PATH}"/org.eclipse.paho.client.mqttv3-*.jar 2>/dev/null | head -n 1)
  76. if [[ -z "${PAHO_JAR}" || ! -f "${PAHO_JAR}" ]]; then
  77. echo "❌ 找不到 Paho mqttv3 jar 在 ${MODULE_PATH}/" >&2
  78. exit 1
  79. fi
  80. # 子 shell cd 之后还要能引用,转绝对路径
  81. PAHO_JAR=$(cd "$(dirname "${PAHO_JAR}")" && pwd)/$(basename "${PAHO_JAR}")
  82. PATCH_TMP=$(mktemp -d -t paho-modinfo.XXXXXX)
  83. trap 'rm -rf "${PATCH_TMP}"' EXIT
  84. EXP_DIR="${PATCH_TMP}/exploded"
  85. mkdir -p "${EXP_DIR}"
  86. ( cd "${EXP_DIR}" && jar xf "${PAHO_JAR}" )
  87. cat > "${PATCH_TMP}/module-info.java" <<'PAHO_MOD_INFO'
  88. module org.eclipse.paho.client.mqttv3 {
  89. requires transitive java.logging;
  90. requires transitive java.prefs;
  91. exports org.eclipse.paho.client.mqttv3;
  92. exports org.eclipse.paho.client.mqttv3.internal;
  93. exports org.eclipse.paho.client.mqttv3.internal.security;
  94. exports org.eclipse.paho.client.mqttv3.internal.websocket;
  95. exports org.eclipse.paho.client.mqttv3.internal.wire;
  96. exports org.eclipse.paho.client.mqttv3.logging;
  97. exports org.eclipse.paho.client.mqttv3.persist;
  98. exports org.eclipse.paho.client.mqttv3.spi;
  99. exports org.eclipse.paho.client.mqttv3.util;
  100. uses org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory;
  101. provides org.eclipse.paho.client.mqttv3.spi.NetworkModuleFactory with
  102. org.eclipse.paho.client.mqttv3.internal.TCPNetworkModuleFactory,
  103. org.eclipse.paho.client.mqttv3.internal.SSLNetworkModuleFactory,
  104. org.eclipse.paho.client.mqttv3.internal.websocket.WebSocketNetworkModuleFactory,
  105. org.eclipse.paho.client.mqttv3.internal.websocket.WebSocketSecureNetworkModuleFactory;
  106. }
  107. PAHO_MOD_INFO
  108. # 用解压后的目录做 patch,避免 javac 在 jar 上看不到内嵌包
  109. javac --patch-module org.eclipse.paho.client.mqttv3="${EXP_DIR}" \
  110. -d "${PATCH_TMP}" "${PATCH_TMP}/module-info.java"
  111. # 注入进 jar;如有旧的同名条目则覆盖
  112. ( cd "${PATCH_TMP}" && jar uf "${PAHO_JAR}" module-info.class )
  113. echo " Paho 已注入 module-info → $(basename "${PAHO_JAR}")"
  114. # 自动发现需要进入 runtime 的模块集合
  115. 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"
  116. echo "==> [3/5] jlink 生成最小运行时 → ${RUNTIME_DIR}"
  117. rm -rf "${RUNTIME_DIR}"
  118. jlink \
  119. --module-path "${JAVA_HOME}/jmods:${MODULE_PATH}" \
  120. --add-modules "${RUNTIME_MODULES}" \
  121. --no-header-files \
  122. --no-man-pages \
  123. --strip-debug \
  124. --compress=zip-9 \
  125. --ignore-signing-information \
  126. --output "${RUNTIME_DIR}"
  127. echo "==> [4/5] jpackage 生成 .app → ${OUT_DIR}"
  128. rm -rf "${OUT_DIR}"
  129. mkdir -p "${OUT_DIR}"
  130. # ---------------------------------------------------------------------------
  131. # 自动准备 Dock 图标:
  132. # 1) 优先使用 src/main/resources/icon.icns(如果用户已自备多分辨率图标)
  133. # 2) 否则从 src/main/resources/icon.png 现转:
  134. # - 用 sips 生成 16 / 32 / 64 / 128 / 256 / 512 / 1024 + @2x 共 10 张
  135. # - 用 iconutil 打包成 icon.icns
  136. # 建议源图 ≥ 1024×1024;尺寸过小会被强行放大、Dock 上糊
  137. # ---------------------------------------------------------------------------
  138. ICON_ARG=()
  139. if [[ -f "src/main/resources/icon.icns" ]]; then
  140. ICON_ARG+=(--icon "src/main/resources/icon.icns")
  141. echo " 使用图标:src/main/resources/icon.icns"
  142. elif [[ -f "src/main/resources/com/fujica/parkingtool/icon.icns" ]]; then
  143. ICON_ARG+=(--icon "src/main/resources/com/fujica/parkingtool/icon.icns")
  144. echo " 使用图标:src/main/resources/com/fujica/parkingtool/icon.icns"
  145. elif [[ -f "src/main/resources/icon.png" ]]; then
  146. SRC_PNG="src/main/resources/icon.png"
  147. ICONSET="target/icon.iconset"
  148. OUT_ICNS="target/icon.icns"
  149. rm -rf "${ICONSET}"
  150. mkdir -p "${ICONSET}"
  151. # 注意:尺寸 = base × scale;命名规则参考 iconutil 文档
  152. declare -a SIZES=(16 32 32 64 128 256 256 512 512 1024)
  153. declare -a NAMES=(
  154. "icon_16x16.png" "icon_16x16@2x.png"
  155. "icon_32x32.png" "icon_32x32@2x.png"
  156. "icon_128x128.png" "icon_128x128@2x.png"
  157. "icon_256x256.png" "icon_256x256@2x.png"
  158. "icon_512x512.png" "icon_512x512@2x.png"
  159. )
  160. for i in "${!SIZES[@]}"; do
  161. sips -z "${SIZES[$i]}" "${SIZES[$i]}" "${SRC_PNG}" --out "${ICONSET}/${NAMES[$i]}" >/dev/null
  162. done
  163. rm -f "${OUT_ICNS}"
  164. iconutil --convert icns --output "${OUT_ICNS}" "${ICONSET}"
  165. ICON_ARG+=(--icon "${OUT_ICNS}")
  166. echo " 已从 ${SRC_PNG} 自动生成 → ${OUT_ICNS}"
  167. SRC_W=$(sips -g pixelWidth "${SRC_PNG}" | awk '/pixelWidth/ {print $2}')
  168. if [[ -n "${SRC_W}" && "${SRC_W}" -lt 512 ]]; then
  169. echo " ⚠️ 源图仅 ${SRC_W}px,Dock 会被放大显示,建议提供 ≥1024×1024 的 icon.png"
  170. fi
  171. fi
  172. jpackage \
  173. --type app-image \
  174. --name "${APP_NAME}" \
  175. --app-version "${APP_VERSION}" \
  176. --vendor "${APP_VENDOR}" \
  177. --module-path "${MODULE_PATH}" \
  178. --module "${MAIN_MODULE}/${MAIN_CLASS}" \
  179. --runtime-image "${RUNTIME_DIR}" \
  180. --dest "${OUT_DIR}" \
  181. --java-options "-Xms128m" \
  182. --java-options "-Xmx1024m" \
  183. --java-options "-Dprism.lcdtext=false" \
  184. --java-options "--enable-native-access=javafx.graphics" \
  185. ${ICON_ARG[@]+"${ICON_ARG[@]}"}
  186. echo "==> [5/5] 压缩成 zip 便于分发"
  187. ARCH=$(uname -m)
  188. ZIP_NAME="${APP_NAME}-mac-${ARCH}.zip"
  189. ( cd "${OUT_DIR}" && rm -f "${ZIP_NAME}" && ditto -ck --rsrc --keepParent "${APP_NAME}.app" "${ZIP_NAME}" )
  190. echo ""
  191. echo "✅ 完成"
  192. du -sh "${OUT_DIR}/${APP_NAME}.app" || true
  193. du -sh "${OUT_DIR}/${ZIP_NAME}" || true
  194. echo " .app: ${OUT_DIR}/${APP_NAME}.app"
  195. echo " .zip: ${OUT_DIR}/${ZIP_NAME}"
  196. echo " 双击 .app 运行即可,无需安装 JDK。"
  197. # === 可选:生成 .dmg 安装镜像 ===
  198. # jpackage \
  199. # --type dmg \
  200. # --name "${APP_NAME}" \
  201. # --app-version "${APP_VERSION}" \
  202. # --vendor "${APP_VENDOR}" \
  203. # --module-path "${MODULE_PATH}" \
  204. # --module "${MAIN_MODULE}/${MAIN_CLASS}" \
  205. # --runtime-image "${RUNTIME_DIR}" \
  206. # --dest "${OUT_DIR}" \
  207. # "${ICON_ARG[@]}"