feat(windows): 新增 Windows 移植(免管理员隧道 + 自动装 Apple 驱动 + 环境自检)
按 docs/WINDOWS_PORT.md 完成 Windows 移植,并修正该文档中若干与实测不符之处。
macOS 构建链完整保留,单一代码库双平台构建。
核心:iOS 17+ RSD 隧道在 Windows 上换了实现方式
- macOS 借 Apple remoted(--native,内核可路由),Windows 改走 UserspaceRsdTunnel
(纯 Python PyTCP 用户态协议栈)
- 实测:不需要管理员权限、不需要 Wintun 驱动(pytun_pmd3 自带 wintun.dll)、
建立耗时约 0.3 秒
- 关键坑:用户态隧道的设备地址**只在本进程可达**。拿 (host, port) 另建 RSD 会得到
OSError [WinError 1231] 网络位置不可达。必须把 aopen() 返回的 rsd 对象本身注入
DVT 层(RealLocationProvider 早有 rsd= 注入口),因此 Windows 不能沿用
「隧道助手子进程 + 解析 HOST PORT」那套架构
- 修正 WINDOWS_PORT.md 的错误:类名是 UserspaceRsdTunnel 不是 UserspaceTunnel;
remote start-tunnel 没有 --userspace;Wintun 不是必需项
新增
- core/platform.py 平台抽象层(数据目录/浏览器探测/进程管理/提权/信号)
- core/apple_driver.py Apple Mobile Device Support 检测与自动安装
(服务启动 → 独立 AMDS MSI → 完整 iTunes 包 → winget 逐级降级)
+ 开发者镜像下载
- core/setup_window.py 启动时的「环境准备」窗口:逐项列出依赖,缺失项右侧可点安装,
必需项全部就绪后「进入程序」才可点击
- scripts/run.ps1, build.ps1, installer/iosloc.iss, installer/version_info.txt
- tools/make_ico.py(由已有 PNG 图标集生成多尺寸 .ico)
- tools/probe_tunnel_win.py(隧道可行性诊断)
- tests/win_e2e.py(7 项)、tests/win_app_e2e.py(9 项)真机验收
- docs/README-WINDOWS.md
- .gitattributes(避免 Windows 写出 CRLF 的 .sh 把 macOS 构建搞坏)
修改
- core/tunnel.py Windows 走进程内 userspace 隧道;macOS 路径不变
- core/server.py TunnelManager 注入 runner;provider_for 注入 rsd 对象;
资源目录与数据目录分离;日志落盘
- core/location_provider.py 平台化错误提示;新增保活(隧道静默断开会让设备
偷偷恢复真实定位,停止播放时每 15s 重发)
- main.py 「环境准备」窗口、Apple 驱动后台检查、
--check-driver / --install-driver、跨平台浏览器与信号
- Location.spec 平台分支;Windows 用 .ico + uac_admin=True(启动即请求管理员)
+ version_info.txt;单一 spec 不分叉
- tests/drift_sim.py / offline_tiles.py 去掉硬编码 /tmp,改用 tempfile.gettempdir()
打包
- exe 清单写入 requireAdministrator(用户要求:启动时直接请求管理员权限)
- 绿色版 dist\iOSLocationTrack\iOSLocationTrack.exe
- 安装包 dist\installer\iOSLocationTrack-1.0.0-setup.exe(Inno Setup)
实测(Windows 11 x64 / Python 3.12.14 / pymobiledevice3 11.31.0 / iPhone 11 iOS 26.6.1)
- 隧道→镜像→DVT→下发定位→恢复真实定位 全链路跑通,连接 0.8s
- selftest 83/83、drift_sim 40/40、offline_tiles 13/13
- win_e2e 7/7、win_app_e2e 9/9
- 打包版 exe:发现设备 → 0.3s 隧道 → DVT → 两次下发 → 恢复真实定位 → 干净退出
This commit is contained in:
1 parent
85e9a53750
commit
4d55e2415f
26 files changed
+4515
-306
No files matched your search
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env python3
|
||||
"""由 macOS 侧已有的 PNG 图标集生成 Windows 用的多尺寸 .ico。
|
||||
|
||||
macOS 用 ``iconutil`` 产出 .icns;Windows 需要 .ico,用 Pillow 直接合成即可。
|
||||
输入:resources/AppIcon.iconset/ 下的 PNG(16/32/64/128/256/512/1024)
|
||||
输出:resources/AppIcon.ico
|
||||
|
||||
用法:
|
||||
.venv\\Scripts\\python.exe tools\\make_ico.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
ICONSET = ROOT / "resources" / "AppIcon.iconset"
|
||||
OUTPUT = ROOT / "resources" / "AppIcon.ico"
|
||||
PREVIEW = ROOT / "resources" / "AppIcon-256.png"
|
||||
|
||||
#: Windows 资源管理器/任务栏实际会用到的尺寸
|
||||
SIZES = [16, 24, 32, 48, 64, 128, 256]
|
||||
|
||||
#: 每个目标尺寸优先使用的源文件(按分辨率从高到低找,避免放大糊掉)
|
||||
CANDIDATES = {
|
||||
16: ["icon_16x16.png", "icon_16x16@2x.png", "icon_32x32.png"],
|
||||
24: ["icon_32x32.png", "icon_16x16@2x.png"],
|
||||
32: ["icon_32x32.png", "icon_32x32@2x.png", "icon_64x64.png"],
|
||||
48: ["icon_128x128.png", "icon_32x32@2x.png"],
|
||||
64: ["icon_64x64.png", "icon_32x32@2x.png", "icon_128x128.png"],
|
||||
128: ["icon_128x128.png", "icon_128x128@2x.png", "icon_256x256.png"],
|
||||
256: ["icon_256x256.png", "icon_256x256@2x.png", "icon_512x512.png"],
|
||||
}
|
||||
|
||||
#: 兜底:任意大图,用 Pillow 缩放
|
||||
FALLBACKS = ["icon_512x512@2x.png", "icon_512x512.png", "icon_256x256@2x.png",
|
||||
"icon_256x256.png", "icon_128x128@2x.png", "icon_128x128.png"]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
print("需要 Pillow:.venv\\Scripts\\python.exe -m pip install pillow")
|
||||
return 1
|
||||
|
||||
if not ICONSET.is_dir():
|
||||
print(f"找不到图标源目录:{ICONSET}")
|
||||
return 1
|
||||
|
||||
available = {p.name: p for p in ICONSET.glob("*.png")}
|
||||
if not available:
|
||||
print(f"{ICONSET} 下没有 PNG")
|
||||
return 1
|
||||
|
||||
fallback = next((ICONSET / n for n in FALLBACKS if n in available), None)
|
||||
if fallback is None:
|
||||
fallback = sorted(available.values(), key=lambda p: p.stat().st_size)[-1]
|
||||
|
||||
images = []
|
||||
for size in SIZES:
|
||||
chosen = next((ICONSET / n for n in CANDIDATES[size] if n in available), None)
|
||||
source = chosen or fallback
|
||||
try:
|
||||
with Image.open(source) as img:
|
||||
img = img.convert("RGBA")
|
||||
if img.size != (size, size):
|
||||
img = img.resize((size, size), Image.LANCZOS)
|
||||
images.append(img.copy())
|
||||
print(f" {size:>3}x{size:<3} ← {source.name}")
|
||||
except Exception as exc:
|
||||
print(f" {size:>3}x{size:<3} 失败:{exc}")
|
||||
|
||||
if not images:
|
||||
print("没有生成任何尺寸")
|
||||
return 1
|
||||
|
||||
# 以最大的一张作为基准,sizes= 里附上其余尺寸
|
||||
largest = max(images, key=lambda i: i.size[0])
|
||||
largest.save(
|
||||
OUTPUT,
|
||||
format="ICO",
|
||||
sizes=[img.size for img in images],
|
||||
append_images=[img for img in images if img is not largest],
|
||||
)
|
||||
print(f"\n已生成:{OUTPUT}({OUTPUT.stat().st_size/1024:.1f} KB,{len(images)} 个尺寸)")
|
||||
|
||||
# 顺便导出一张 PNG 供 Inno Setup 的向导图/文档使用
|
||||
largest.resize((256, 256), Image.LANCZOS).save(PREVIEW)
|
||||
print(f"已生成:{PREVIEW}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user