Files
ykxmz/windows/build_exe.ps1
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

157 lines
6.7 KiB
PowerShell
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.
# 把 Windows 被控端打包成「多文件 EXE」(PyInstaller --onedir)
# 用法: powershell -ExecutionPolicy Bypass -File windows\build_exe.ps1
#
# 产物: dist_exe\ykxmz\
# ├─ ykxmz.exe <- 双击运行
# └─ _internal\ <- 依赖
#
# 刻意使用 --onedir 而不是 --onefile:
# * 启动不解压临时文件,秒开(双击就弹窗口)
# * 依赖可见、可查杀、可替换,方便做安装包
#
# 注意:本版本**不再使用系统托盘**(pystray 已移除),
# 关窗后进程继续运行,靠单实例机制再次双击找回窗口。
#
# ============================================================================
# ⚠⚠ 必须用 Python 3.8 打包 —— 这是学校电脑能用的前提 ⚠⚠
# ============================================================================
#
# 用户的学校电脑是 **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 打出来的 exe 在 Win7 上必然报缺 DLL。
# 那个 DLL 不是"忘了放进包",是"放进去也没用"——只能在 Win8+ 跑。
#
# 这里选 **32 位** 3.8,因为 32 位在 Win7 的 32 位和 64 位系统上**都能跑**,
# 一个包打天下,学校不用分两种发。
# (64 位的包在 32 位 Windows 上直接跑不起来。)
#
# 脚本会**主动校验**:拿到的 Python 不是 3.8 就直接报错退出,
# 而不是默默打出一个人家装不上的包。
# ============================================================================
$ErrorActionPreference = "Continue"
$windows = $PSScriptRoot # LanRemote/windows
$root = Split-Path -Parent $windows # LanRemote
# ---------------------------------------------------------------- 选 Python
Write-Host "=== 0/4 选择 Python(必须是 3.8,为了 Windows 7)===" -ForegroundColor Cyan
function Find-Py38 {
# 1) 环境变量指定
if ($env:YKXMZ_PY -and (Test-Path $env:YKXMZ_PY)) { return $env:YKXMZ_PY }
# 2) 常见安装位置
$cands = @(
"C:\ykxmz-py38\python.exe",
"C:\Python38\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python38\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python38-32\python.exe",
"C:\Program Files (x86)\Python38-32\python.exe"
)
foreach ($c in $cands) { if (Test-Path $c) { return $c } }
# 3) py launcher
try {
$p = & py -3.8 -c "import sys;print(sys.executable)" 2>$null
if ($LASTEXITCODE -eq 0 -and $p -and (Test-Path $p.Trim())) {
return $p.Trim()
}
} catch { }
return $null
}
$py = Find-Py38
if (-not $py) {
Write-Host @"
✗ 找不到 Python 3.8。
这个项目**必须**用 3.8 打包,因为学校电脑是 Windows 7,
而 Python 3.9 及以上在 Win7 上会因为缺
api-ms-win-core-path-l1-1-0.dll 直接启动失败。
装一个(32 位,Win7 32/64 位都能用):
https://www.python.org/ftp/python/3.8.10/python-3.8.10.exe
静默安装到 C:\ykxmz-py38 :
python-3.8.10.exe /quiet InstallAllUsers=0 TargetDir=C:\ykxmz-py38 ``
Include_pip=1 Include_tcltk=1 Include_test=0 Include_launcher=0
再装依赖:
C:\ykxmz-py38\python.exe -m pip install Pillow==9.5.0 pyinstaller==5.13.2
或者设环境变量 YKXMZ_PY 指向已有的 3.8。
"@ -ForegroundColor Red
throw "需要 Python 3.8"
}
$ver = & $py -c "import sys;print('%d.%d'%sys.version_info[:2])"
$bits = & $py -c "import struct;print(struct.calcsize('P')*8)"
Write-Host " Python: $py"
Write-Host " 版本: $ver ($bits 位)"
if ($ver -ne "3.8") {
throw "打包必须用 Python 3.8(当前 $ver)。用 3.9+ 打出来的包在 Windows 7 上会因为缺 api-ms-win-core-path-l1-1-0.dll 启动失败。"
}
if ($bits -ne "32") {
Write-Host " ⚠ 不是 32 位 —— 64 位的包在 32 位 Windows(不少学校机器是)上跑不起来。" -ForegroundColor Yellow
Write-Host " 建议换成 32 位的 3.8。继续打包……" -ForegroundColor Yellow
}
Write-Host "`n=== 1/4 生成图标 ===" -ForegroundColor Cyan
& $py (Join-Path $root "_build\make_ico.py")
if ($LASTEXITCODE -ne 0) { throw "生成图标失败" }
$ico = Join-Path $windows "app.ico"
Write-Host "`n=== 2/4 PyInstaller 打包(--onedir 多文件)===" -ForegroundColor Cyan
# ⚠ lanremote.ui 必须显式 --hidden-import(见下面那行)。
# 它是"设置窗口"(tkinter)的实现,而静默那条路不需要它,
# PyInstaller 静态分析就找不到 —— 于是双击 exe 想弹设置窗口时
# 会 ModuleNotFoundError: No module named 'lanremote.ui',
# 进程直接退出,用户看到的就是"双击没反应"。
# (PS 的续行符 ` 后面不能再跟 # 注释,所以说明写在这里。)
& $py -m PyInstaller --noconfirm --clean --onedir --windowed `
--name "ykxmz" `
--icon "$ico" `
--paths "$windows" `
--hidden-import "PIL._tkinter_finder" `
--hidden-import "lanremote.netcheck" `
--hidden-import "lanremote.features" `
--hidden-import "lanremote.headless" `
--hidden-import "lanremote.ui" `
--version-file (Join-Path $windows "version_info.txt") `
--noupx `
--exclude-module "pystray" `
--exclude-module "numpy" `
--exclude-module "psutil" `
--distpath (Join-Path $root "dist_exe") `
--workpath (Join-Path $root "_build\pyinstaller_work") `
--specpath (Join-Path $root "_build") `
(Join-Path $windows "run_agent.py") 2>&1 | Select-Object -Last 4
$target = Join-Path $root "dist_exe\ykxmz\ykxmz.exe"
if (-not (Test-Path $target)) { throw "打包失败:未生成 $target" }
# ---------------------------------------------------------------- Win7 校验
Write-Host "`n=== 3/4 Windows 7 兼容性校验 ===" -ForegroundColor Cyan
& $py (Join-Path $root "_build\check_win7_compat.py")
if ($LASTEXITCODE -ne 0) {
throw "Win7 兼容性校验没过 —— 这个包学校电脑装不上,不该发布。"
}
Write-Host "`n=== 4/4 产物 ===" -ForegroundColor Cyan
Write-Host "目录: $(Split-Path -Parent $target)"
Write-Host ("exe : {0:N1} MB" -f ((Get-Item $target).Length / 1MB))
$all = Get-ChildItem (Split-Path -Parent $target) -Recurse -File
Write-Host ("总计: {0} 个文件, {1:N1} MB" -f $all.Count, (($all | Measure-Object Length -Sum).Sum / 1MB))
Write-Host "`n完成(Python $ver $bits 位,可在 Windows 7 及以上运行)" -ForegroundColor Green