build-mac.sh 7.6 KB

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