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:
jbjf6666 committed 2026-10-16 01:05:09 +08:00
commit 85e9a53750
65 files changed
+18603

No files matched your search

+221
View File
@@ -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())