build.bat参数解析兼容PowerShell+gitignore修正保留构建脚本

This commit is contained in:
youpaishidifu
2026-08-03 11:03:57 +08:00
parent fa67eb407e
commit 686aae7e53
8 changed files with 577 additions and 1 deletions
+117
View File
@@ -0,0 +1,117 @@
@echo off
setlocal enabledelayedexpansion
REM === WindowMsgBot 构建脚本 ===
REM 自动化打包和签名流程
REM 用法: build.bat /skip-cert /skip-sign
REM build.bat --skip-cert --skip-sign (PowerShell兼容)
REM build.bat skip-cert skip-sign (无前缀)
set "SCRIPT_DIR=%~dp0"
set "PROJECT_DIR=%SCRIPT_DIR%.."
set "PYTHON=%PROJECT_DIR%\.venv311\Scripts\python.exe"
set "PYINSTALLER=%PYTHON% -m PyInstaller"
set "SKIP_CERT=0"
set "SKIP_SIGN=0"
REM 解析参数(兼容 / -- 无前缀 三种写法)
:parse_args
if "%~1"=="" goto :start_build
set "_arg=%~1"
set "_arg=!_arg:/=!"
set "_arg=!_arg:--=!"
if "!_arg!"=="skip-cert" set "SKIP_CERT=1"
if "!_arg!"=="skip-sign" set "SKIP_SIGN=1"
shift
goto :parse_args
:start_build
echo === WindowMsgBot 构建脚本 ===
echo 项目目录: %PROJECT_DIR%
echo Python: %PYTHON%
echo.
REM 检查Python
if not exist "%PYTHON%" (
echo 错误: Python不存在: %PYTHON%
echo 请先创建虚拟环境
pause
exit /b 1
)
REM 检查PyInstaller
"%PYTHON%" -c "import PyInstaller" >nul 2>&1
if %errorlevel% neq 0 (
echo 安装PyInstaller...
"%PYTHON%" -m pip install pyinstaller
)
REM 1. 生成证书(可选)
if "%SKIP_CERT%"=="0" (
echo 1. 生成自签名证书...
if exist "%SCRIPT_DIR%code_signing_cert.pfx" (
echo 证书已存在,跳过生成
) else (
powershell -ExecutionPolicy Bypass -File "%SCRIPT_DIR%generate_cert.ps1"
if %errorlevel% neq 0 (
echo ⚠ 证书生成失败,继续构建...
)
)
) else (
echo 1. 跳过证书生成
)
REM 2. 清理旧构建(只清理PyInstaller输出,保留build目录里的spec和hooks)
echo.
echo 2. 清理旧构建文件...
if exist "%PROJECT_DIR%\build\build" rmdir /s /q "%PROJECT_DIR%\build\build"
if exist "%PROJECT_DIR%\dist" rmdir /s /q "%PROJECT_DIR%\dist"
REM 3. PyInstaller打包
echo.
echo 3. PyInstaller打包...
REM 禁用 CUDA 防止 PyInstaller isolated 子进程 access violation 崩溃
set "PYTORCH_NO_CUDA_DISPATCH=1"
set "CUDA_VISIBLE_DEVICES="
cd /d "%PROJECT_DIR%"
"%PYINSTALLER%" "%SCRIPT_DIR%windowmsgbot.spec" --noconfirm --clean
if %errorlevel% neq 0 (
echo 错误: PyInstaller打包失败!
pause
exit /b 1
)
echo ✓ 打包完成
REM 4. 签名(可选)
if "%SKIP_SIGN%"=="0" (
echo.
echo 4. 签名exe文件...
if exist "%SCRIPT_DIR%code_signing_cert.pfx" (
call "%SCRIPT_DIR%sign.bat"
) else (
echo ⚠ 证书不存在,跳过签名
)
) else (
echo.
echo 4. 跳过签名
)
REM 5. 输出结果
echo.
echo 5. 构建结果...
if exist "%PROJECT_DIR%\dist\WindowMsgBot\WindowMsgBot.exe" (
echo ✓ 可执行文件: %PROJECT_DIR%\dist\WindowMsgBot\WindowMsgBot.exe
for %%A in ("%PROJECT_DIR%\dist\WindowMsgBot\WindowMsgBot.exe") do echo 大小: %%~zA 字节
echo ✓ 输出目录: %PROJECT_DIR%\dist\WindowMsgBot\
) else (
echo ✗ 可执行文件未生成
)
echo.
echo === 构建完成 ===
echo 输出目录: %PROJECT_DIR%\dist
echo.
pause
+105
View File
@@ -0,0 +1,105 @@
# 生成自签名代码签名证书 - 降低360敏感度
# 使用方法: 以管理员身份运行 PowerShell, 执行 .\generate_cert.ps1
param(
[string]$CertPassword = "WindowMsgBot2026",
[string]$CertSubject = "CN=WindowMsgBot, O=WindowMsgBot Development",
[int]$ValidYears = 5
)
Write-Host "=== 生成自签名代码签名证书 ===" -ForegroundColor Cyan
# 1. 生成证书
Write-Host "1. 生成自签名证书..." -ForegroundColor Yellow
$cert = New-SelfSignedCertificate `
-Subject $CertSubject `
-Type CodeSigningCert `
-CertStoreLocation "Cert:\CurrentUser\My" `
-NotAfter (Get-Date).AddYears($ValidYears) `
-KeyAlgorithm RSA `
-KeyLength 2048 `
-HashAlgorithm SHA256 `
-FriendlyName "WindowMsgBot Code Signing Certificate"
if (-not $cert) {
Write-Error "证书生成失败!"
exit 1
}
Write-Host " 证书指纹: $($cert.Thumbprint)" -ForegroundColor Green
# 2. 导出PFX文件
Write-Host "2. 导出PFX文件..." -ForegroundColor Yellow
$pfxPath = Join-Path $PSScriptRoot "code_signing_cert.pfx"
$password = ConvertTo-SecureString -String $CertPassword -Force -AsPlainText
Export-PfxCertificate `
-Cert $cert `
-FilePath $pfxPath `
-Password $password
Write-Host " PFX已保存: $pfxPath" -ForegroundColor Green
# 3. 导出CER文件(公钥)
Write-Host "3. 导出CER公钥文件..." -ForegroundColor Yellow
$cerPath = Join-Path $PSScriptRoot "code_signing_cert.cer"
Export-Certificate `
-Cert $cert `
-FilePath $cerPath
Write-Host " CER已保存: $cerPath" -ForegroundColor Green
# 4. 安装到受信任的根证书颁发机构
Write-Host "4. 安装到受信任的根证书颁发机构..." -ForegroundColor Yellow
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(
"Root", "LocalMachine")
$store.Open("ReadWrite")
$store.Add($cert)
$store.Close()
Write-Host " 证书已安装到受信任的根证书存储" -ForegroundColor Green
# 5. 验证安装
Write-Host "5. 验证证书安装..." -ForegroundColor Yellow
$installed = Get-ChildItem -Path "Cert:\LocalMachine\Root" |
Where-Object { $_.Thumbprint -eq $cert.Thumbprint }
if ($installed) {
Write-Host " ✓ 证书已成功安装到受信任存储" -ForegroundColor Green
} else {
Write-Host " ⚠ 证书可能未安装到受信任存储" -ForegroundColor Yellow
}
# 6. 输出证书信息
Write-Host "`n=== 证书信息 ===" -ForegroundColor Cyan
Write-Host "主题: $($cert.Subject)"
Write-Host "指纹: $($cert.Thumbprint)"
Write-Host "有效期: $($cert.NotBefore) 至 $($cert.NotAfter)"
Write-Host "用途: $($cert.EnhancedKeyUsageList.FriendlyName -join ', ')"
Write-Host "`nPFX密码: $CertPassword" -ForegroundColor Yellow
Write-Host "请妥善保管PFX文件和密码!" -ForegroundColor Red
# 7. 创建证书信息文件
Write-Host "7. 创建证书信息文件..." -ForegroundColor Yellow
$infoPath = Join-Path $PSScriptRoot "cert_info.txt"
@"
证书信息
========
主题: $($cert.Subject)
指纹: $($cert.Thumbprint)
有效期: $($cert.NotBefore) 至 $($cert.NotAfter)
PFX文件: code_signing_cert.pfx
CER文件: code_signing_cert.cer
PFX密码: $CertPassword
使用说明
========
1. PFX文件用于signtool签名
2. CER文件可分发给其他人验证签名
3. 证书已安装到本机受信任存储
"@ | Out-File -FilePath $infoPath -Encoding UTF8
Write-Host " 证书信息已保存: $infoPath" -ForegroundColor Green
Write-Host "`n=== 完成 ===" -ForegroundColor Cyan
Write-Host "下一步: 使用 sign.bat 脚本签名exe文件" -ForegroundColor Yellow
+20
View File
@@ -0,0 +1,20 @@
"""PyInstaller hook: 避免 easyocr 默认 hook 触发 collect_submodules('torch') 崩溃。
PyInstaller 默认 hook-easyocr.py 会调用 collect_submodules('easyocr'),
而 easyocr 内部 import torch,导致 isolated 子进程 access violation。
本 hook 只列出 easyocr 实际需要的子模块,避开 torch 引用。
"""
hiddenimports = [
'easyocr',
'easyocr.easyocr',
'easyocr.config',
'easyocr.detection',
'easyocr.recognition',
'easyocr.utils',
'easyocr.craft',
'easyocr.craft_utils',
'easyocr.imgproc',
'easyocr.dataset',
'easyocr.model',
'easyocr.model.vgg_model',
]
+41
View File
@@ -0,0 +1,41 @@
"""PyInstaller hook: 避免 collect_submodules('torch') 在 isolated 子进程中 access violation 崩溃。
PyInstaller 默认 hook-torch.py 调用 collect_submodules('torch') 遍历全部子模块,
在 torch 2.5+ 的 isolated 子进程中会触发 0xC0000005 access violation。
本 hook 直接列出实际需要的 torch 子模块,完全避开 collect_submodules。
"""
# 只列出 bot 实际引用的 torch 子模块(通过 import 分析确认)
hiddenimports = [
'torch',
'torch.nn',
'torch.nn.functional',
'torch.nn.modules',
'torch.nn.modules.activation',
'torch.nn.modules.conv',
'torch.nn.modules.linear',
'torch.nn.modules.module',
'torch.nn.modules.normalization',
'torch.nn.modules.padding',
'torch.nn.modules.pooling',
'torch.nn.modules.rnn',
'torch.nn.modules.sparse',
'torch.nn.modules.dropout',
'torch.nn.modules.batchnorm',
'torch.nn.modules.loss',
'torch.nn.modules.container',
'torch.nn.modules.flatten',
'torch.nn.modules.pixelshuffle',
'torch.nn.modules.upsampling',
'torch.nn.modules.channelshuffle',
'torch.cuda',
'torch.cuda.amp',
'torch.tensor',
'torch._C',
'torch._utils',
'torch.storage',
'torch.serialization',
'torch.types',
'torch.classes',
'torch.backends',
'torch.backends.cudnn',
]
+119
View File
@@ -0,0 +1,119 @@
@echo off
setlocal enabledelayedexpansion
REM === WindowMsgBot 签名脚本 ===
REM 使用自签名证书签名exe文件,降低360敏感度
REM 用法: sign.bat [exe路径]
set "SCRIPT_DIR=%~dp0"
set "PROJECT_DIR=%SCRIPT_DIR%.."
set "CERT_PFX=%SCRIPT_DIR%code_signing_cert.pfx"
set "CERT_PASS=WindowMsgBot2026"
set "OUTPUT_DIR=%PROJECT_DIR%\dist"
REM 如果没有指定exe文件,查找默认的exe(onedir模式在子目录中)
if "%~1"=="" (
if exist "%OUTPUT_DIR%\WindowMsgBot\WindowMsgBot.exe" (
set "EXE_PATH=%OUTPUT_DIR%\WindowMsgBot\WindowMsgBot.exe"
) else (
set "EXE_PATH=%OUTPUT_DIR%\WindowMsgBot.exe"
)
) else (
set "EXE_PATH=%~1"
)
echo === WindowMsgBot 代码签名 ===
echo.
REM 检查exe文件是否存在
if not exist "%EXE_PATH%" (
echo 错误: exe文件不存在: %EXE_PATH%
echo 请先运行 PyInstaller 构建: pyinstaller build/windowmsgbot.spec
pause
exit /b 1
)
REM 检查证书文件是否存在
if not exist "%CERT_PFX%" (
echo 错误: 证书文件不存在: %CERT_PFX%
echo 请先运行证书生成脚本: powershell -ExecutionPolicy Bypass -File build\generate_cert.ps1
pause
exit /b 1
)
REM 查找signtool.exe
set "SIGNTOOL="
for %%i in (
"%ProgramFiles%\Windows Kits\10\bin\x64\signtool.exe"
"%ProgramFiles(x86)%\Windows Kits\10\bin\x64\signtool.exe"
"%ProgramFiles%\Windows Kits\10\bin\x86\signtool.exe"
"%ProgramFiles(x86)%\Windows Kits\10\bin\x86\signtool.exe"
"%ProgramFiles%\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.8 Tools\signtool.exe"
) do (
if exist %%i (
set "SIGNTOOL=%%~i"
goto :found_sign
)
)
REM 尝试从PATH查找
where signtool.exe >nul 2>&1
if %errorlevel% equ 0 (
set "SIGNTOOL=signtool.exe"
goto :found_sign
)
echo 错误: 未找到 signtool.exe
echo 请安装 Windows SDK 或 Windows Kits
echo 下载地址: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/
pause
exit /b 1
:found_sign
echo 使用signtool: %SIGNTOOL%
echo.
REM 1. 签名exe
echo 1. 签名exe文件...
"%SIGNTOOL%" sign /f "%CERT_PFX%" /p "%CERT_PASS%" /tr http://timestamp.digicert.com /td sha256 /fd sha256 "%EXE_PATH%"
if %errorlevel% neq 0 (
echo 错误: 签名失败!
pause
exit /b 1
)
echo ✓ 签名成功
REM 2. 验证签名
echo.
echo 2. 验证签名...
"%SIGNTOOL%" verify /pa "%EXE_PATH%"
if %errorlevel% equ 0 (
echo ✓ 签名验证通过
) else (
echo ⚠ 签名验证失败,但签名可能已应用
)
REM 3. 显示签名信息
echo.
echo 3. 签名信息...
"%SIGNTOOL%" verify /v "%EXE_PATH%" 2>nul | findstr /i "Signing"
echo.
REM 4. 复制签名后的exe到项目根目录
echo 4. 复制签名后的exe...
copy "%EXE_PATH%" "%PROJECT_DIR%\WindowMsgBot_signed.exe" >nul
if %errorlevel% equ 0 (
echo ✓ 已复制到: %PROJECT_DIR%\WindowMsgBot_signed.exe
) else (
echo ⚠ 复制失败
)
echo.
echo === 完成 ===
echo 签名后的exe: %EXE_PATH%
echo 备份副本: %PROJECT_DIR%\WindowMsgBot_signed.exe
echo.
echo 注意: 自签名证书需要在目标机器上安装证书才能验证签名
echo 证书文件: %CERT_PFX%
echo.
pause
+172
View File
@@ -0,0 +1,172 @@
# -*- mode: python ; coding: utf-8 -*-
# WindowMsgBot PyInstaller spec文件
# 用法: pyinstaller build/windowmsgbot.spec
import os
import sys
from PyInstaller.utils.hooks import collect_submodules, collect_data_files
# 项目根目录
PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(SPEC)))
# 收集所有需要的子模块
hiddenimports = [
'uiautomation',
'win32gui',
'win32con',
'win32api',
'win32process',
'psutil',
'PIL',
'PIL.Image',
'PIL.ImageGrab',
'PIL.ImageEnhance',
'PIL.ImageFilter',
'easyocr',
'numpy',
'torch',
'torchvision',
'sklearn',
'sklearn.feature_extraction.text',
'sklearn.metrics.pairwise',
'jieba',
'customtkinter',
'comtypes',
'comtypes.client',
'win32com',
'win32com.client',
'pythoncom',
'pywintypes',
'ctypes',
'ctypes.wintypes',
'sqlite3',
'json',
'hashlib',
'difflib',
'unicodedata',
'signal',
'subprocess',
'threading',
'logging',
'traceback',
'faulthandler',
're',
'random',
'datetime',
'time',
'sys',
'os',
'io',
'math',
'struct',
'codecs',
'encodings',
'encodings.utf_8',
'encodings.gbk',
'encodings.gb2312',
'encodings.gb18030',
]
# 数据文件
datas = [
# 知识库目录
(os.path.join(PROJECT_ROOT, '知识库'), '知识库'),
# 偏旁库
(os.path.join(PROJECT_ROOT, 'unicode_radicals.md'), '.'),
# 配置文件
(os.path.join(PROJECT_ROOT, 'config.json'), '.'),
# EasyOCR worker脚本(作为数据文件,运行时提取)
(os.path.join(PROJECT_ROOT, 'easyocr_worker.py'), '.'),
# 其他必要文件
(os.path.join(PROJECT_ROOT, 'auto_reply.py'), '.'),
(os.path.join(PROJECT_ROOT, 'knowledge_base.py'), '.'),
(os.path.join(PROJECT_ROOT, 'config.py'), '.'),
(os.path.join(PROJECT_ROOT, 'gui.py'), '.'),
]
# 收集easyocr数据文件
easyocr_datas = collect_data_files('easyocr')
datas.extend(easyocr_datas)
# 分析
a = Analysis(
[os.path.join(PROJECT_ROOT, 'main.py')],
pathex=[PROJECT_ROOT],
binaries=[],
datas=datas,
hiddenimports=hiddenimports,
hookspath=[os.path.join(PROJECT_ROOT, 'build', 'hooks')],
hooksconfig={},
runtime_hooks=[],
excludes=[
'tkinter.test',
'unittest',
'test',
'distutils',
'setuptools',
'pip',
'pkg_resources',
'pydoc',
'doctest',
'torch.autograd',
'torch.testing',
'torch.distributed',
'torch.fx',
'torch.jit',
'torch.onnx',
'torch.profiler',
'torch.sparse',
'torch.utils.benchmark',
'torch.utils.bottleneck',
'torch.utils.checkpoint',
'torch.utils.collect_env',
'torch.utils.cpp_extension',
'torch.utils.data',
'torch.utils.deterministic',
'torch.utils.dlpack',
'torch.utils.file_baton',
'torch.utils.hipify',
'torch.utils.jit',
'torch.utils.mobile_optimizer',
'torch.utils.model_zoo',
'torch.utils.show_pickle',
'torch.utils.tensorboard',
'torch.utils.throughput_benchmark',
],
noarchive=False,
)
# 过滤掉不必要的大文件
a.datas = [d for d in a.datas if not d[0].endswith(('.pyc', '.pyo'))]
# PYZ - Python字节码
pyz = PYZ(a.pure)
# EXE - 主程序(onedir模式:只放脚本+PYZ,不嵌入binaries/data)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True, # onedir关键:不嵌入二进制,交给COLLECT
name='WindowMsgBot',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False, # UPX对大型DLL(torch/scipy)常出问题,关掉
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
# COLLECT - 收集所有文件到输出目录
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=False,
name='WindowMsgBot',
)