Files
ios-location-track/tools/make_dmg.py
T
jbjf6666 c60e4b5cd4 chore(release): v1.1.0 — 版本号 1.0.0 → 1.1.0,README 双语化,构建脚本编码自检
版本号 1.1.0(Windows 支持是新功能)
- core/protocol.py APP_VERSION、installer/version_info.txt、installer/iosloc.iss
  MyAppVersion、Location.spec(mac BUNDLE)、tools/make_dmg.py VERSION 全部同步到 1.1.0
- 产物命名规范:Windows 用 _win、macOS 用 _mac 后缀,Release 页面一眼分清平台
  · iOSLocationTrack-1.1.0_win-setup.exe
  · iOSLocationTrack-1.1.0_win-portable.zip
  · iOSLocationTrack-1.1.0_mac.dmg

README 双语化(默认中文)
- docs/README-WINDOWS.en.md 新增(Windows 文档全文英文版)
- docs/README-WINDOWS.md 顶部加 [中文] | [English] 语言切换
- README.md / README.en.md 的下载章节改成平台表格,说明 _win / _mac 命名

构建脚本编码自检
- scripts/build.ps1 新增第 0.5 步:检查 scripts/*.ps1 是否为 UTF-8 with BOM,
  缺失则自动补回。Windows PowerShell 5.1 没有 BOM 时会按 ANSI(GBK) 解析,
  中文乱码且直接语法报错(改个注释就构建不了,本项目实际踩过两次)
- 本次修复:build.ps1 的 BOM 在编辑后被工具静默丢弃,已补回

验收(Windows 11 x64 / iPhone 11 iOS 26.6.1)
- selftest 83/83、drift_sim 40/40、offline_tiles 13/13
- 打包版 exe 清单含 requireAdministrator,FileVersion 1.1.0.0
2026-10-16 01:51:50 +08:00

224 lines
9.0 KiB
Python

#!/usr/bin/env python3
"""把打包好的 .app 制作成可分发的 DMG 安装包。
产物:dist/iOSLocationTrack-1.1.0_mac.dmg
内容:应用本身 + Applications 快捷方式(拖进去即安装)+ 中文安装说明
特点:
- UDZO 压缩(zlib),体积最小且兼容性好
- 固定卷名、固定图标位置(打开 DMG 后两个图标位置整齐)
- 自定义背景图(程序化生成,无需设计稿)
- 生成后自动校验挂载与内容
用法:./.venv/bin/python tools/make_dmg.py
"""
from __future__ import annotations
import json
import os
import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
APP_NAME = "虚拟定位轨迹"
VERSION = "1.1.0"
APP = ROOT / "dist" / f"{APP_NAME}.app"
# 产物名带 _mac 后缀,与 Windows 的 _win 对应,Release 页面上一眼能分清平台。
# 用 ASCII 文件名:GitHub Release 会把非 ASCII 字符剥掉(v1.0.0 首次上传时踩过)。
DMG = ROOT / "dist" / f"iOSLocationTrack-{VERSION}_mac.dmg"
VOLUME = APP_NAME
BG_WIDTH, BG_HEIGHT = 600, 400
def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
return subprocess.run(cmd, capture_output=True, text=True, **kwargs)
def make_background(path: Path) -> None:
"""程序化生成 DMG 背景图(深色,与应用图标风格一致)。"""
try:
from PIL import Image, ImageDraw
except ImportError:
return
img = Image.new("RGB", (BG_WIDTH, BG_HEIGHT), (24, 30, 42))
draw = ImageDraw.Draw(img)
# 顶部渐变更深一点,营造层次
for y in range(BG_HEIGHT // 2):
t = y / (BG_HEIGHT / 2)
color = (int(24 + 10 * t), int(30 + 12 * t), int(42 + 16 * t))
draw.line([(0, y), (BG_WIDTH, y)], fill=color)
# 一条虚线轨迹指向右侧(呼应应用图标)
for x in range(70, 520, 18):
draw.line([(x, 300), (x + 9, 300)], fill=(70, 90, 120), width=3)
# 中间箭头提示"拖过去"
draw.line([(250, 190), (330, 190)], fill=(120, 150, 190), width=4)
draw.polygon([(330, 178), (352, 190), (330, 202)], fill=(120, 150, 190))
img.save(path)
def main() -> int:
if not APP.exists():
print(f"未找到 {APP},请先运行 ./scripts/build_app.sh")
return 1
py = sys.executable
print("\033[1m制作 DMG 安装包\033[0m")
print(f" 应用:{APP}")
app_size = sum(f.stat().st_size for f in APP.rglob("*") if f.is_file()) / 1048576
print(f" 应用体积:{app_size:.1f} MB")
# 注意:/tmp 在系统只读卷上(可用空间很小),而 UDRW 中间镜像需要约 2 倍应用体积,
# 因此临时目录必须放在数据卷(用户目录)下。
staging_root = Path.home() / "Library" / "Caches" / "iosloc-dmg"
staging_root.mkdir(parents=True, exist_ok=True)
staging = Path(tempfile.mkdtemp(prefix="build-", dir=str(staging_root)))
try:
# 1) 准备素材
print("\n1) 准备 DMG 内容")
target_app = staging / f"{APP_NAME}.app"
shutil.copytree(APP, target_app, symlinks=True)
# Applications 快捷方式:用户把 app 拖进去即完成安装
os.symlink("/Applications", staging / "Applications")
bg = staging / ".background.png"
make_background(bg)
readme = staging / "安装说明.txt"
readme.write_text(
f"""{APP_NAME} v{VERSION}
========================================
安装:
把「{APP_NAME}.app」拖到右侧的 Applications 文件夹即可。
首次打开:
本工具未做 Apple 公证,若提示"来自身份不明的开发者",
请在应用上点右键 → 打开 → 再点"打开"。
首次运行还会请求"控制 Chrome/Edge"权限(用于以应用窗口方式打开界面)。
需要什么:
· macOS 12 或更高(Apple Silicon / M 系列芯片)
· 一根数据线 + 一台 iOS 设备(iOS 17 及以上需开启开发者模式并挂载开发者镜像)
· 不需要安装 Python 或任何依赖,全部已打包在应用内
数据位置:
~/Library/Application Support/iOSLocationTrack/
(配置与地图瓦片缓存;删除该目录即可重置)
完整使用说明见应用内的 README,或访问项目目录下的 README.md
""",
encoding="utf-8",
)
print(f" 已放入:应用 + Applications 快捷方式 + 安装说明 + 背景图")
# 2) 生成可读写 DMG 以便布局图标
temp_dmg = staging / "temp.dmg"
print("\n2) 生成磁盘映像并设置窗口布局")
result = run([
"hdiutil", "create", "-srcfolder", str(staging), "-volname", VOLUME,
"-fs", "HFS+", "-format", "UDRW", "-ov", str(temp_dmg),
])
if result.returncode != 0:
print(" hdiutil create 失败:", result.stderr[-500:])
return 1
mount_point = Path(f"/Volumes/{VOLUME}")
if mount_point.exists():
run(["hdiutil", "detach", str(mount_point), "-force"])
result = run(["hdiutil", "attach", str(temp_dmg), "-readwrite", "-noverify", "-noautoopen"])
if result.returncode != 0:
print(" 挂载失败:", result.stderr[-400:])
return 1
time.sleep(2)
# 用 AppleScript 设置 Finder 视图(图标位置 / 背景 / 大小)
script = f'''
tell application "Finder"
tell disk "{VOLUME}"
open
set current view of container window to icon view
set toolbar visible of container window to false
set statusbar visible of container window to false
set the bounds of container window to {{200, 150, 800, 550}}
set viewOptions to the icon view options of container window
set arrangement of viewOptions to not arranged
set icon size of viewOptions to 110
try
set background picture of viewOptions to file ".background.png"
end try
set position of item "{APP_NAME}.app" of container window to {{150, 190}}
set position of item "Applications" of container window to {{450, 190}}
set position of item "安装说明.txt" of container window to {{300, 320}}
update without registering applications
delay 1
close
end tell
end tell
'''
result = run(["osascript", "-e", script])
if result.returncode != 0:
print(" 布局脚本部分失败(不影响使用):", result.stderr.strip()[:200])
# 收尾:确保 Finder 写入的 .DS_Store 落盘
run(["sync"])
time.sleep(1)
detach = run(["hdiutil", "detach", str(mount_point)])
if detach.returncode != 0:
run(["hdiutil", "detach", str(mount_point), "-force"])
time.sleep(1)
# 3) 压缩成最终 DMG
print("\n3) 压缩为最终 DMG")
if DMG.exists():
DMG.unlink()
result = run([
"hdiutil", "convert", str(temp_dmg), "-format", "UDZO",
"-imagekey", "zlib-level=9", "-o", str(DMG),
])
if result.returncode != 0:
print(" 压缩失败:", result.stderr[-400:])
return 1
finally:
shutil.rmtree(staging, ignore_errors=True)
try:
shutil.rmtree(staging_root, ignore_errors=True)
except Exception:
pass
# 4) 校验
print("\n4) 校验 DMG")
size_mb = DMG.stat().st_size / 1048576
print(f" 文件:{DMG}")
print(f" 体积:{size_mb:.1f} MB")
verify_mount = Path(f"/Volumes/{VOLUME}")
result = run(["hdiutil", "attach", str(DMG), "-nobrowse", "-readonly", "-noautoopen"])
ok_mount = result.returncode == 0
print(f" 挂载:{'成功' if ok_mount else '失败 ' + result.stderr[-200:]}")
if ok_mount:
try:
time.sleep(2)
contents = sorted(p.name for p in verify_mount.iterdir())
print(f" 内容:{contents}")
has_app = (verify_mount / f"{APP_NAME}.app").exists()
has_link = (verify_mount / "Applications").is_symlink()
has_readme = (verify_mount / "安装说明.txt").exists()
binary = verify_mount / f"{APP_NAME}.app" / "Contents" / "MacOS" / "iOSLocationTrack"
print(f" 应用存在:{has_app} | Applications 快捷方式:{has_link} | 安装说明:{has_readme}")
if binary.exists():
arch = run(["lipo", "-archs", str(binary)]).stdout.strip()
print(f" 可执行架构:{arch}")
# 校验签名/完整性(未签名时的常规检查)
quarantine = run(["xattr", "-l", str(verify_mount / f"{APP_NAME}.app")]).stdout
print(f" 扩展属性:{'有' if quarantine.strip() else '无'}")
finally:
run(["hdiutil", "detach", str(verify_mount), "-force"])
print(f"\n\033[32m完成:{DMG}\033[0m")
return 0
if __name__ == "__main__":
raise SystemExit(main())