Files
ykxmz/_tests/test_apk_contents.py
T
xmz7891 be59a20036 2.1.5:修「Windows 7 装完报 dll 缺失」
用户报:学校的电脑是 Windows 7,装完提示缺 DLL。

== 根因不是漏打包,是 Python 版本越界 ==

    Python 3.8    -> 最后一个支持 Windows 7 的版本
    Python 3.9+   -> 只支持 Windows 8 及以上

Python 3.9 起用到了 api-ms-win-core-path-l1-1-0.dll 里的
PathCchCanonicalizeEx,而这个 DLL **Windows 7 上根本不存在**,
也没法用 api-set 转发(那套机制只在 Win8+ 生效)。

所以之前用 3.11 打出来的包,在 Win7 上必然报缺 DLL ——
**那个 DLL 不是"忘了放进包",是"放进去也没用"。**

顺带说明:包里那 47 个 api-ms-win-core-*.dll 转发库正是
Python 为兼容 Win7 才带上的 —— 上游本来就考虑过 Win7,
只是 3.11 这个版本号越界了。

== 改法 ==

1. **改用 Python 3.8 打包,而且选 32 位**
   32 位在 Win7 的 32 位和 64 位系统上都能跑,学校不用分两种发。
   (64 位的包在 32 位 Windows 上直接起不来。)

   依赖固定到 3.8 能用的版本:
       Pillow==9.5.0(最后一个支持 3.8 的)
       pyinstaller 6.x(保持 _internal 布局,和安装包/测试/卸载逻辑一致;
                       5.x 会把依赖摊在根目录,导致"_internal 在"那一项失败)

2. **打包脚本主动拦**
   windows\build_exe.ps1 打包前先校验 Python:
     * 找不到 3.8 -> 报错并给出安装命令
     * 版本不是 3.8 -> 直接 throw,而不是默默打一个别人装不上的包
     * 不是 32 位 -> 警告
   Python 从 YKXMZ_PY 环境变量或常见路径里找,不写死。

3. **新增 Win7 兼容校验** _build\check_win7_compat.py
   盯三件事:内嵌是不是 3.8 的运行库、**有没有**那个要命的 DLL、
   exe 是不是 32 位。打包时和回归套件(3a9 关)里都会跑,
   不过就拒绝出包。

== 顺手修的 ==

_build/test_installer_cycle.py 里写死了 ykxmz-setup-1.8.2.exe,
于是一直在**装一个不存在的旧包** -> 静默失败 -> 报出一堆
莫名其妙的失败("配对码实际是 xxx"、"版本显示 v1.8.2")。
改成自动找 dist 下版本号最新的包(按数字段排序,不能按字符串,
否则 1.9.0 会排到 2.1.4 前面)。

== 验证(用户要求:自己测一遍,没问题再上传)==

    全量回归 42 个阶段全 PASS(含新的 "Windows 7 兼容" 关)
    安装包完整周期 20/20
    Win7 校验:python38.dll / 无 api-ms-win-core-path / 32 位
    实际启动打包的 EXE:进程活着、TCP 监听、UDP 自动发现在听、
                        连接能建立(日志里有"设备接入")
    产物扫描:三件全部干净(无旧密码、无服务器 IP)
    提交前泄漏闸门通过

安装包体积 14.4 MB -> 9.0 MB。
2026-10-09 13:16:31 +08:00

148 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""检查手表 APK 是否真的把「遥控诊断」合并进去了。
合并前是两个 APK(手表端 + 独立的遥控诊断 App)。
合并后只需要一个:主程序里有「设备诊断」按钮。
这里从 APK 的 dex 和清单里核实,而不是只信源码。
"""
from __future__ import annotations
import re
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# 别写死版本号 —— 一升级就坏(这坑踩过好几回了)。
# 直接扫 dist 里最新的手表 APK。
def _latest_apk() -> Path:
candidates = sorted(
(ROOT / "dist").glob("*watch*.apk"),
key=lambda p: p.stat().st_mtime, reverse=True)
return candidates[0] if candidates else \
ROOT / "dist" / "ykxmz-watch-2.1.5.apk"
APK = _latest_apk()
DIAG_APK = ROOT / "dist" / "LanRemote-Diag-1.1.0.apk"
AAPT = (Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain")
/ "android-sdk" / "build-tools" / "34.0.0" / "aapt.exe")
PASSED = 0
FAILED = 0
def check(name: str, ok: bool, detail: str = "") -> None:
global PASSED, FAILED
if ok:
PASSED += 1
print(f" [PASS] {name}")
else:
FAILED += 1
print(f" [FAIL] {name} {detail}")
def main() -> int:
print("=" * 68)
print("一、手表 APK 里有没有内置诊断")
print("=" * 68)
if not APK.is_file():
print(f" 找不到 {APK}")
print(" 请先运行 android\\build_apk.ps1")
return 1
with zipfile.ZipFile(APK) as archive:
dex = archive.read("classes.dex")
names = {item.decode() for item in
re.findall(rb"Lcom/lanremote/[A-Za-z0-9_/]+;", dex)}
print(f" dex 中的类: {len(names)} 个")
check("APK 里有 Diagnostics 类(诊断逻辑已内置)",
"Lcom/lanremote/watch/Diagnostics;" in names,
str(sorted(names)))
check("APK 里有 DiscoveryCore(诊断与主程序共用同一份发现实现)",
"Lcom/lanremote/watch/DiscoveryCore;" in names)
check("APK 里有 MainActivity", "Lcom/lanremote/watch/MainActivity;" in names)
# 注意:不能用 "diag" 做子串判断 —— Diagnostics 里也含 diag。
# 要查的是旧的独立包名 com/lanremote/diag/。
legacy = [n for n in names if n.startswith("Lcom/lanremote/diag/")]
check("不含旧的独立诊断包 com.lanremote.diag",
not legacy, str(legacy))
print()
print("=" * 68)
print("二、清单权限(诊断读 WiFi 名称需要定位权限)")
print("=" * 68)
# aapt 不认中文路径,先拷到纯 ASCII 路径
with tempfile.TemporaryDirectory() as tmp:
simple = Path(tmp) / "watch.apk"
simple.write_bytes(APK.read_bytes())
try:
proc = subprocess.run(
[str(AAPT), "dump", "badging", str(simple)],
capture_output=True, timeout=60)
badging = proc.stdout.decode("utf-8", errors="replace")
except Exception as exc: # noqa: BLE001
badging = ""
print(f" aapt 调用失败: {exc}")
if badging:
for permission, why in (
("android.permission.ACCESS_FINE_LOCATION", "读 WiFi 名称"),
("android.permission.INTERNET", "网络通信"),
("android.permission.CHANGE_WIFI_MULTICAST_STATE", "UDP 广播"),
):
check(f"声明了 {permission.split('.')[-1]}({why})",
permission in badging)
check("没有申请定位以外的敏感权限",
"READ_CONTACTS" not in badging
and "READ_SMS" not in badging
and "CAMERA" not in badging
and "RECORD_AUDIO" not in badging)
else:
check("能读取 APK 清单", False, "aapt 没有输出")
print()
print("=" * 68)
print("三、构建脚本不再依赖独立诊断 APK")
print("=" * 68)
package = (ROOT / "_build" / "package.py").read_text(encoding="utf-8")
check("package.py 不再要求 Diag APK",
"DIAG_APK" not in package, "仍引用 DIAG_APK")
check("package.py 说明里写明诊断已合并",
"合并" in package)
src = (ROOT / "android" / "src" / "com" / "lanremote" / "watch"
/ "MainActivity.java").read_text(encoding="utf-8")
# 用户要求:"安卓端中转设置下面,把那个设备诊断给删了,
# 换成输入配对码,输入对应配对码就可以连接对应电脑"
# 所以这里反过来断言:不该再有设备诊断入口,而是配对码输入。
import re as _re
code = _re.sub(r"//[^\n]*", "", src)
check("主界面不再有「设备诊断」入口", "设备诊断" not in code)
check("主界面不再调用 Diagnostics.build",
"Diagnostics.build(" not in code)
check("主界面不再调用 Diagnostics.manualTest",
"Diagnostics.manualTest(" not in code)
check("换成了「输入配对码连接」入口",
'bigButton("输入配对码连接"' in code)
check("配对码连接走中转(connectByCode)",
"private void connectByCode(" in code)
print()
print("=" * 68)
print(f"通过 {PASSED} 项,失败 {FAILED} 项")
print("=" * 68)
return 1 if FAILED else 0
if __name__ == "__main__":
sys.exit(main())