| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- #!/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"
- 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[@]}"
|