docs: 新增 Windows 移植交接文档
docs/WINDOWS_PORT.md 覆盖: - 逐文件可移植性审计(核心逻辑层约 90% 可直接复用) - 核心难点:iOS 17+ 的 RSD 隧道无法在 Windows 复用(macOS 实现依赖 XPC + ObjC runtime 私有框架),改用 pymobiledevice3 的 userspace 隧道 - 已实测确认的命令与 API(--userspace 挂在 lockdown start-tunnel / mounter auto-mount 上,remote start-tunnel 不支持;类名是 UserspaceRsdTunnel) - 窗口启动、数据目录、打包与分发(DMG → Inno Setup)、图标(.icns → .ico) - macOS 侧踩过的坑:镜像挂载用 Python API、必须打包包元数据、不要把 distutils 放进 excludes、AlreadyMountedError 当成功处理 - 设计约束(DVT 长连接、只能改经纬度、last_point 与 last_reported 分离等) - 验收基线(复用现有测试套件)与建议的工作顺序
This commit is contained in:
commit
85e9a53750
65 files changed
+18603
No files matched your search
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
"""把打包好的 .app 制作成可分发的 DMG 安装包。
|
||||
|
||||
产物:dist/虚拟定位轨迹-1.0.0.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.0.0"
|
||||
APP = ROOT / "dist" / f"{APP_NAME}.app"
|
||||
DMG = ROOT / "dist" / f"{APP_NAME}-{VERSION}.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())
|
||||
Reference in new issue
Block a user