Files
ykxmz/_build/bump_version_generic.py
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

207 lines
7.9 KiB
Python
Raw Permalink 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 -*-
r"""通用版本号升级(1.5.0 -> 1.6.0)。
只改**用户能看到版本号的地方**,不动那些当数据用的 "1.4.0/1.5.0":
* HTTP User-Agent "LanRemote/1.4.0" —— 协议标识,不是产品版本,
而且 20 多个测试在断言它,动它要同步改一大片
* _tests 里当测试数据用的版本串
* _build 下的一次性脚本(历史记录)
改这些:
android/AndroidManifest.xml versionCode / versionName
android/build_apk.ps1 APK 文件名 + manifest 参数
_build/package_remote.py 三个产物名
windows/version_info.txt EXE 的文件版本
windows/lanremote/__init__.py __version__
docs/*.txt 三份文档
relay/relay.py VERSION
_tests/test_package_remote.py 断言成品名
_tests/test_apk_contents.py 断言 APK 名
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
OLD = sys.argv[1] if len(sys.argv) > 1 else "1.5.0"
NEW = sys.argv[2] if len(sys.argv) > 2 else "1.6.0"
OLD_CODE = str(int(OLD.split(".")[0]) * 100 + int(OLD.split(".")[1]) + 1)
NEW_CODE = str(int(NEW.split(".")[0]) * 100 + int(NEW.split(".")[1]) + 1)
def swap(path: Path, pairs, note: str) -> bool:
if not path.is_file():
print(f" [跳过] {path.name} 不存在")
return False
raw = path.read_bytes()
had_bom = raw.startswith(b"\xef\xbb\xbf")
text = raw.decode("utf-8-sig")
original = text
for old, new in pairs:
text = text.replace(old, new)
if text == original:
print(f" [跳过] {path.name}: 没有 {OLD}")
return False
path.write_bytes((b"\xef\xbb\xbf" if had_bom else b"")
+ text.replace("\r\n", "\n").encode("utf-8"))
print(f" {path.name}: {note}")
return True
def main() -> int:
print(f" 版本号 {OLD} -> {NEW}(versionCode {OLD_CODE} -> {NEW_CODE})")
print()
changed = 0
# ---- Android manifest ----
manifest = ROOT / "android" / "AndroidManifest.xml"
if swap(manifest,
[(f'android:versionCode="{OLD_CODE}"',
f'android:versionCode="{NEW_CODE}"'),
(f'android:versionName="{OLD}"',
f'android:versionName="{NEW}"')],
f"versionCode={NEW_CODE} versionName={NEW}"):
changed += 1
# ---- APK 构建脚本 ----
apk_ps1 = ROOT / "android" / "build_apk.ps1"
if swap(apk_ps1,
[(f"ykxmz-watch-{OLD}.apk", f"ykxmz-watch-{NEW}.apk"),
(f"--version-code {OLD_CODE}", f"--version-code {NEW_CODE}"),
(f'--version-name "{OLD}"', f'--version-name "{NEW}"')],
"APK 名 + 版本"):
changed += 1
# ---- 打包脚本 ----
if swap(ROOT / "_build" / "package_remote.py",
[(f'VERSION = "{OLD}"', f'VERSION = "{NEW}"')],
f"VERSION={NEW}"):
changed += 1
# ---- Windows EXE 版本信息 ----
# ⚠ 原来用的是 f'"{OLD}.0"',一旦 OLD 和文件里对不上就静默跳过
# (version_info.txt 就是这么从 1.7.1 一直漏到现在的)。
# 改成正则匹配任意四段版本。
import re as _re
_vi = ROOT / "windows" / "version_info.txt"
if _vi.is_file():
_raw = _vi.read_bytes()
_bom = _raw.startswith(b"\xef\xbb\xbf")
_txt = _raw.decode("utf-8-sig")
_new = _re.sub(
r'(StringStruct\("(?:File|Product)Version",\\s*")\\d+\\.\\d+\\.\\d+\\.\\d+("\))',
r'\\g<1>' + NEW + r'.0\\g<2>', _txt)
if _new != _txt:
_vi.write_bytes((b"\xef\xbb\xbf" if _bom else b"")
+ _new.replace("\\r\\n", "\\n").encode("utf-8"))
print(f" version_info.txt: EXE 文件版本 {NEW}.0")
changed += 1
else:
print(" [跳过] version_info.txt")
if False:
changed += 1
# ---- __init__ ----
if swap(ROOT / "windows" / "lanremote" / "__init__.py",
[(f'"{OLD}"', f'"{NEW}"')],
f"__version__={NEW}"):
changed += 1
# ---- 中转 VERSION ----
if swap(ROOT / "relay" / "relay.py",
[(f'VERSION = "{OLD}"', f'VERSION = "{NEW}"')],
f"VERSION={NEW}"):
changed += 1
# ---- 安装包版本 ----
# ⚠ 原来漏了这三处,导致 version_info.txt 从 1.7.1 起就一直没跟上
# (用户在"文件属性 -> 详细信息"里看到一个错三个版本的号)。
if swap(ROOT / "installer" / "ykxmz.iss",
[(f'#define MyAppVersion "{OLD}"',
f'#define MyAppVersion "{NEW}"'),
(f'ykxmz-setup-{OLD}.exe', f'ykxmz-setup-{NEW}.exe')],
f"安装包版本 {NEW}"):
changed += 1
# ---- 安装许可页上的版本 ----
if swap(ROOT / "installer" / "license-combined.txt",
[(f"版本:{OLD}", f"版本:{NEW}")],
f"许可页版本 {NEW}"):
changed += 1
# ---- 三份文档 ----
for path in sorted((ROOT / "docs").glob("*.txt")):
raw = path.read_bytes()
had_bom = raw.startswith(b"\xef\xbb\xbf")
text = raw.decode("utf-8-sig")
new_text = text.replace(OLD, NEW)
if new_text != text:
path.write_bytes((b"\xef\xbb\xbf" if had_bom else b"")
+ new_text.replace("\r\n", "\n").encode("utf-8"))
print(f" docs/{path.name}")
changed += 1
# ---- README 里的成品文件名 ----
#
# README 里有 8 处写着 `ykxmz-setup-1.8.2.exe` 这类成品名。
# 以前没管它,结果一直停在 1.8.2 —— 用户照着 README 去 releases
# 找 1.8.2 的包,当然找不到(最新已经是 2.x 了)。
#
# 这里用"任意数字版本号 -> 新版本"的正则,而不是精确替换 OLD:
# README 可能因为漏过几次 bump 而停在更老的版本上,
# 精确匹配 OLD 那时候就换不掉了。
readme = ROOT / "README.md"
if readme.is_file():
text = readme.read_text(encoding="utf-8")
new_text = re.sub(
r"(ykxmz-(?:setup|server|watch|agent)-)\d+\.\d+\.\d+",
lambda m: m.group(1) + NEW, text)
if new_text != text:
readme.write_text(new_text, encoding="utf-8", newline="\n")
print(" README.md")
changed += 1
# ---- 会断言成品名的测试 ----
# 注意 test_installer_cycle.py 也曾经写死过成品名(ykxmz-setup-1.8.2.exe),
# 结果它去装一个不存在的包 -> 静默失败 -> 报出一堆莫名其妙的
# "配对码实际是 xxx" "版本显示 v1.8.2"。那个文件现在改成自动找
# dist 下最新的安装包了,不再需要这里维护。
for name in ("test_package_remote.py", "test_apk_contents.py"):
path = ROOT / "_tests" / name
if not path.is_file():
continue
text = path.read_text(encoding="utf-8")
new_text = (text.replace(f"ykxmz-agent-{OLD}", f"ykxmz-agent-{NEW}")
.replace(f"ykxmz-server-{OLD}", f"ykxmz-server-{NEW}")
.replace(f"ykxmz-watch-{OLD}", f"ykxmz-watch-{NEW}"))
if new_text != text:
path.write_text(new_text, encoding="utf-8", newline="\n")
print(f" _tests/{name}")
changed += 1
print()
print(f" 共改了 {changed} 处")
print(f" 注意:HTTP UA 仍然是 LanRemote/1.4.0(协议标识,不是产品版本)")
# 复核
print()
print(" 复核:")
checks = [
(manifest, f'versionName="{NEW}"'),
(apk_ps1, f"ykxmz-watch-{NEW}.apk"),
(ROOT / "_build" / "package_remote.py", f'VERSION = "{NEW}"'),
]
for path, needle in checks:
ok = needle in path.read_text(encoding="utf-8-sig")
print(f" {path.name}: {'✓' if ok else '✗'} {needle}")
return 0
if __name__ == "__main__":
sys.exit(main())