524 lines
14 KiB
Python
524 lines
14 KiB
Python
|
#!/usr/bin/env python3
|
|||
|
# -*- coding: utf-8 -*-
|
|||
|
"""
|
|||
|
后端应用程序打包脚本
|
|||
|
使用PyInstaller将Flask应用程序打包成独立的exe文件
|
|||
|
"""
|
|||
|
|
|||
|
import os
|
|||
|
import sys
|
|||
|
import shutil
|
|||
|
import subprocess
|
|||
|
from pathlib import Path
|
|||
|
|
|||
|
def create_spec_file():
|
|||
|
"""创建PyInstaller spec文件"""
|
|||
|
spec_content = '''
|
|||
|
# -*- mode: python ; coding: utf-8 -*-
|
|||
|
|
|||
|
block_cipher = None
|
|||
|
|
|||
|
a = Analysis(
|
|||
|
['main_exe.py'],
|
|||
|
pathex=[],
|
|||
|
binaries=[
|
|||
|
('dll/femtobolt/bin/k4a.dll', '.'), # K4A动态库
|
|||
|
('dll/femtobolt/bin/k4arecord.dll', '.'), # K4A录制库
|
|||
|
('dll/femtobolt/bin/depthengine_2_0.dll', '.'), # 深度引擎
|
|||
|
('dll/femtobolt/bin/OrbbecSDK.dll', '.'), # Orbbec SDK
|
|||
|
('dll/femtobolt/bin/ob_usb.dll', '.'), # Orbbec USB库
|
|||
|
('dll/femtobolt/bin/live555.dll', '.'), # Live555库
|
|||
|
('dll/smitsense/SMiTSenseUsb-F3.0.dll', '.'), # SMiTSense传感器库
|
|||
|
('dll/femtobolt/bin/OrbbecSDKConfig_v1.0.xml', '.'), # Orbbec配置文件
|
|||
|
],
|
|||
|
datas=[
|
|||
|
('..\\config.ini', '.'), # 配置文件
|
|||
|
('..\\config.json', '.'), # JSON配置文件
|
|||
|
('tests', 'tests'), # 测试文件夹
|
|||
|
('data', 'data'), # 数据文件夹
|
|||
|
],
|
|||
|
hiddenimports=[
|
|||
|
'flask',
|
|||
|
'flask_socketio',
|
|||
|
'flask_cors',
|
|||
|
'cv2',
|
|||
|
'numpy',
|
|||
|
'pandas',
|
|||
|
'scipy',
|
|||
|
'matplotlib',
|
|||
|
'seaborn',
|
|||
|
'sklearn',
|
|||
|
'PIL',
|
|||
|
'reportlab',
|
|||
|
'sqlite3',
|
|||
|
'configparser',
|
|||
|
'logging',
|
|||
|
'threading',
|
|||
|
'queue',
|
|||
|
'base64',
|
|||
|
'psutil',
|
|||
|
'pykinect_azure',
|
|||
|
'pyserial',
|
|||
|
'requests',
|
|||
|
'yaml',
|
|||
|
'click',
|
|||
|
'colorama',
|
|||
|
'tqdm',
|
|||
|
'database',
|
|||
|
'device_manager',
|
|||
|
'utils',
|
|||
|
],
|
|||
|
hookspath=[],
|
|||
|
hooksconfig={},
|
|||
|
runtime_hooks=[],
|
|||
|
excludes=[],
|
|||
|
win_no_prefer_redirects=False,
|
|||
|
win_private_assemblies=False,
|
|||
|
cipher=block_cipher,
|
|||
|
noarchive=False,
|
|||
|
)
|
|||
|
|
|||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
|||
|
|
|||
|
exe = EXE(
|
|||
|
pyz,
|
|||
|
a.scripts,
|
|||
|
a.binaries,
|
|||
|
a.zipfiles,
|
|||
|
a.datas,
|
|||
|
[],
|
|||
|
name='BodyBalanceBackend',
|
|||
|
debug=False,
|
|||
|
bootloader_ignore_signals=False,
|
|||
|
strip=False,
|
|||
|
upx=True,
|
|||
|
upx_exclude=[],
|
|||
|
runtime_tmpdir=None,
|
|||
|
console=True,
|
|||
|
disable_windowed_traceback=False,
|
|||
|
argv_emulation=False,
|
|||
|
target_arch=None,
|
|||
|
codesign_identity=None,
|
|||
|
entitlements_file=None,
|
|||
|
icon=None, # 图标文件不存在,暂时禁用
|
|||
|
)
|
|||
|
'''
|
|||
|
|
|||
|
with open('backend.spec', 'w', encoding='utf-8') as f:
|
|||
|
f.write(spec_content)
|
|||
|
print("✓ 已创建 backend.spec 文件")
|
|||
|
|
|||
|
def create_debug_spec():
|
|||
|
"""创建诊断脚本的spec文件"""
|
|||
|
import subprocess
|
|||
|
|
|||
|
spec_content = f'''# -*- mode: python ; coding: utf-8 -*-
|
|||
|
|
|||
|
block_cipher = None
|
|||
|
|
|||
|
a = Analysis(
|
|||
|
['test_socketio_debug.py'],
|
|||
|
pathex=['{os.getcwd()}'],
|
|||
|
binaries=[],
|
|||
|
datas=[],
|
|||
|
hiddenimports=[
|
|||
|
'flask',
|
|||
|
'flask_socketio',
|
|||
|
'eventlet',
|
|||
|
'threading',
|
|||
|
'engineio.async_drivers.threading',
|
|||
|
'engineio.async_drivers.eventlet',
|
|||
|
'engineio.async_eventlet',
|
|||
|
'socketio.async_eventlet',
|
|||
|
'greenlet',
|
|||
|
],
|
|||
|
hookspath=[],
|
|||
|
hooksconfig={{}},
|
|||
|
runtime_hooks=[],
|
|||
|
excludes=[],
|
|||
|
win_no_prefer_redirects=False,
|
|||
|
win_private_assemblies=False,
|
|||
|
cipher=block_cipher,
|
|||
|
noarchive=False,
|
|||
|
)
|
|||
|
|
|||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
|||
|
|
|||
|
exe = EXE(
|
|||
|
pyz,
|
|||
|
a.scripts,
|
|||
|
a.binaries,
|
|||
|
a.zipfiles,
|
|||
|
a.datas,
|
|||
|
[],
|
|||
|
name='test_socketio_debug',
|
|||
|
debug=False,
|
|||
|
bootloader_ignore_signals=False,
|
|||
|
strip=False,
|
|||
|
upx=True,
|
|||
|
upx_exclude=[],
|
|||
|
runtime_tmpdir=None,
|
|||
|
console=True,
|
|||
|
disable_windowed_traceback=False,
|
|||
|
argv_emulation=False,
|
|||
|
target_arch=None,
|
|||
|
codesign_identity=None,
|
|||
|
entitlements_file=None,
|
|||
|
)
|
|||
|
'''
|
|||
|
|
|||
|
with open('debug.spec', 'w', encoding='utf-8') as f:
|
|||
|
f.write(spec_content)
|
|||
|
print("✓ 已创建 debug.spec 文件")
|
|||
|
|
|||
|
# 直接构建诊断exe
|
|||
|
print("开始构建诊断exe文件...")
|
|||
|
result = subprocess.run([sys.executable, '-m', 'PyInstaller', 'debug.spec'],
|
|||
|
capture_output=True, text=True, encoding='utf-8')
|
|||
|
|
|||
|
if result.returncode == 0:
|
|||
|
print("✓ 诊断exe构建成功!")
|
|||
|
print("运行诊断: dist/test_socketio_debug.exe")
|
|||
|
else:
|
|||
|
print(f"❌ 诊断exe构建失败: {result.stderr}")
|
|||
|
|
|||
|
def create_minimal_spec():
|
|||
|
"""创建最小化测试脚本的spec文件"""
|
|||
|
print("创建最小化测试脚本的spec文件...")
|
|||
|
|
|||
|
spec_content = '''# -*- mode: python ; coding: utf-8 -*-
|
|||
|
|
|||
|
block_cipher = None
|
|||
|
|
|||
|
a = Analysis(
|
|||
|
['test_socketio_minimal.py'],
|
|||
|
pathex=[],
|
|||
|
binaries=[],
|
|||
|
datas=[],
|
|||
|
hiddenimports=[
|
|||
|
'flask',
|
|||
|
'flask_socketio',
|
|||
|
'eventlet',
|
|||
|
'eventlet.hubs',
|
|||
|
'eventlet.hubs.epolls',
|
|||
|
'eventlet.hubs.kqueue',
|
|||
|
'eventlet.hubs.selects',
|
|||
|
'threading',
|
|||
|
'engineio',
|
|||
|
'engineio.async_drivers',
|
|||
|
'engineio.async_drivers.threading',
|
|||
|
'engineio.async_drivers.eventlet',
|
|||
|
'engineio.async_drivers.gevent',
|
|||
|
'socketio',
|
|||
|
'socketio.namespace'
|
|||
|
],
|
|||
|
hookspath=[],
|
|||
|
hooksconfig={},
|
|||
|
runtime_hooks=[],
|
|||
|
excludes=[],
|
|||
|
win_no_prefer_redirects=False,
|
|||
|
win_private_assemblies=False,
|
|||
|
cipher=block_cipher,
|
|||
|
noarchive=False,
|
|||
|
)
|
|||
|
|
|||
|
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
|||
|
|
|||
|
exe = EXE(
|
|||
|
pyz,
|
|||
|
a.scripts,
|
|||
|
a.binaries,
|
|||
|
a.zipfiles,
|
|||
|
a.datas,
|
|||
|
[],
|
|||
|
name='test_socketio_minimal',
|
|||
|
debug=False,
|
|||
|
bootloader_ignore_signals=False,
|
|||
|
strip=False,
|
|||
|
upx=True,
|
|||
|
upx_exclude=[],
|
|||
|
runtime_tmpdir=None,
|
|||
|
console=True,
|
|||
|
disable_windowed_traceback=False,
|
|||
|
argv_emulation=False,
|
|||
|
target_arch=None,
|
|||
|
codesign_identity=None,
|
|||
|
entitlements_file=None,
|
|||
|
)
|
|||
|
'''
|
|||
|
|
|||
|
with open('minimal.spec', 'w', encoding='utf-8') as f:
|
|||
|
f.write(spec_content)
|
|||
|
|
|||
|
print("✓ 已创建 minimal.spec 文件")
|
|||
|
|
|||
|
# 直接构建
|
|||
|
print("开始构建最小化测试exe...")
|
|||
|
result = subprocess.run([sys.executable, '-m', 'PyInstaller', 'minimal.spec', '--clean'],
|
|||
|
capture_output=True, text=True, encoding='utf-8')
|
|||
|
if result.returncode == 0:
|
|||
|
print("✓ 最小化测试exe构建完成: dist/test_socketio_minimal.exe")
|
|||
|
else:
|
|||
|
print(f"❌ 最小化测试exe构建失败: {result.stderr}")
|
|||
|
|
|||
|
def create_main_entry():
|
|||
|
"""创建主入口文件"""
|
|||
|
main_content = '''
|
|||
|
#!/usr/bin/env python3
|
|||
|
# -*- coding: utf-8 -*-
|
|||
|
"""
|
|||
|
身体平衡评估系统 - 后端服务入口
|
|||
|
独立运行的exe版本
|
|||
|
"""
|
|||
|
|
|||
|
import os
|
|||
|
import sys
|
|||
|
import logging
|
|||
|
from pathlib import Path
|
|||
|
|
|||
|
# 设置工作目录
|
|||
|
if getattr(sys, 'frozen', False):
|
|||
|
# 如果是打包后的exe
|
|||
|
application_path = os.path.dirname(sys.executable)
|
|||
|
else:
|
|||
|
# 如果是开发环境
|
|||
|
application_path = os.path.dirname(os.path.abspath(__file__))
|
|||
|
|
|||
|
os.chdir(application_path)
|
|||
|
sys.path.insert(0, application_path)
|
|||
|
|
|||
|
# 创建必要的目录
|
|||
|
os.makedirs('logs', exist_ok=True)
|
|||
|
os.makedirs('data', exist_ok=True)
|
|||
|
|
|||
|
# 配置日志
|
|||
|
logging.basicConfig(
|
|||
|
level=logging.INFO,
|
|||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|||
|
handlers=[
|
|||
|
logging.FileHandler('logs/backend.log', encoding='utf-8'),
|
|||
|
logging.StreamHandler()
|
|||
|
]
|
|||
|
)
|
|||
|
|
|||
|
logger = logging.getLogger(__name__)
|
|||
|
|
|||
|
def main():
|
|||
|
"""主函数"""
|
|||
|
try:
|
|||
|
logger.info("启动身体平衡评估系统后端服务...")
|
|||
|
logger.info(f"工作目录: {os.getcwd()}")
|
|||
|
|
|||
|
# 导入并启动Flask应用
|
|||
|
from app import app, socketio, init_app
|
|||
|
|
|||
|
# 初始化应用
|
|||
|
init_app()
|
|||
|
|
|||
|
# 启动服务器
|
|||
|
logger.info("后端服务器启动在 http://localhost:5000")
|
|||
|
socketio.run(
|
|||
|
app,
|
|||
|
host='0.0.0.0',
|
|||
|
port=5000,
|
|||
|
debug=False
|
|||
|
)
|
|||
|
|
|||
|
except KeyboardInterrupt:
|
|||
|
logger.info("用户中断,正在关闭服务器...")
|
|||
|
except Exception as e:
|
|||
|
logger.error(f"启动失败: {e}")
|
|||
|
input("按回车键退出...")
|
|||
|
sys.exit(1)
|
|||
|
|
|||
|
if __name__ == '__main__':
|
|||
|
main()
|
|||
|
'''
|
|||
|
|
|||
|
with open('main_exe.py', 'w', encoding='utf-8') as f:
|
|||
|
f.write(main_content)
|
|||
|
print("✓ 已创建 main_exe.py 入口文件")
|
|||
|
|
|||
|
def check_dependencies():
|
|||
|
"""检查必需的依赖模块"""
|
|||
|
required_modules = [
|
|||
|
'flask', 'flask_cors', 'flask_socketio', 'numpy', 'pandas',
|
|||
|
'cv2', 'matplotlib', 'seaborn', 'PIL', 'reportlab',
|
|||
|
'serial', 'requests', 'sqlite3', 'configparser', 'scipy', 'eventlet'
|
|||
|
]
|
|||
|
|
|||
|
missing_modules = []
|
|||
|
for module in required_modules:
|
|||
|
try:
|
|||
|
__import__(module)
|
|||
|
print(f"✓ {module}")
|
|||
|
except ImportError:
|
|||
|
missing_modules.append(module)
|
|||
|
print(f"❌ {module} - 未安装")
|
|||
|
|
|||
|
if missing_modules:
|
|||
|
print(f"\n警告: 发现 {len(missing_modules)} 个缺失的依赖模块")
|
|||
|
print("建议运行: pip install -r requirements.txt")
|
|||
|
return False
|
|||
|
|
|||
|
print("✓ 所有必需依赖模块检查通过")
|
|||
|
return True
|
|||
|
|
|||
|
def build_exe():
|
|||
|
"""构建exe文件"""
|
|||
|
print("开始构建exe文件...")
|
|||
|
|
|||
|
# 检查依赖模块
|
|||
|
print("\n检查依赖模块...")
|
|||
|
if not check_dependencies():
|
|||
|
print("\n❌ 依赖检查失败,请先安装缺失的模块")
|
|||
|
return False
|
|||
|
|
|||
|
# 检查PyInstaller是否安装
|
|||
|
try:
|
|||
|
import PyInstaller
|
|||
|
print(f"\n✓ PyInstaller版本: {PyInstaller.__version__}")
|
|||
|
except ImportError:
|
|||
|
print("❌ PyInstaller未安装,正在安装...")
|
|||
|
os.system('pip install pyinstaller')
|
|||
|
|
|||
|
# 清理之前的构建
|
|||
|
if os.path.exists('build'):
|
|||
|
shutil.rmtree('build')
|
|||
|
print("✓ 清理build目录")
|
|||
|
|
|||
|
if os.path.exists('dist'):
|
|||
|
shutil.rmtree('dist')
|
|||
|
print("✓ 清理dist目录")
|
|||
|
|
|||
|
# 使用spec文件构建
|
|||
|
cmd = 'python -m PyInstaller backend.spec --clean --noconfirm'
|
|||
|
print(f"执行命令: {cmd}")
|
|||
|
result = os.system(cmd)
|
|||
|
|
|||
|
if result == 0:
|
|||
|
print("\n✓ 构建成功!")
|
|||
|
print("exe文件位置: dist/BodyBalanceBackend.exe")
|
|||
|
|
|||
|
# 复制额外的文件
|
|||
|
dist_path = Path('dist')
|
|||
|
if dist_path.exists():
|
|||
|
# 复制配置文件
|
|||
|
config_files = ['../config.ini', '../config.json']
|
|||
|
for config_file in config_files:
|
|||
|
if os.path.exists(config_file):
|
|||
|
shutil.copy2(config_file, dist_path)
|
|||
|
print(f"✓ 复制配置文件: {config_file}")
|
|||
|
|
|||
|
# 创建启动脚本
|
|||
|
start_script = dist_path / 'start_backend.bat'
|
|||
|
with open(start_script, 'w', encoding='utf-8') as f:
|
|||
|
f.write('@echo off\n')
|
|||
|
f.write('echo 启动身体平衡评估系统后端服务...\n')
|
|||
|
f.write('BodyBalanceBackend.exe\n')
|
|||
|
f.write('pause\n')
|
|||
|
print("✓ 创建启动脚本: start_backend.bat")
|
|||
|
|
|||
|
print("\n🎉 打包完成!")
|
|||
|
print("运行方式:")
|
|||
|
print("1. 直接运行: dist/BodyBalanceBackend.exe")
|
|||
|
print("2. 使用脚本: dist/start_backend.bat")
|
|||
|
else:
|
|||
|
print("❌ 构建失败")
|
|||
|
return False
|
|||
|
|
|||
|
return True
|
|||
|
|
|||
|
def check_required_files():
|
|||
|
"""检查必需的文件是否存在"""
|
|||
|
required_files = {
|
|||
|
'app.py': '主应用文件',
|
|||
|
'database.py': '数据库模块',
|
|||
|
'device_manager.py': '设备管理模块',
|
|||
|
'utils.py': '工具模块',
|
|||
|
'../config.ini': '配置文件',
|
|||
|
'../config.json': 'JSON配置文件'
|
|||
|
}
|
|||
|
|
|||
|
required_dlls = {
|
|||
|
'dll/femtobolt/bin/k4a.dll': 'K4A动态库',
|
|||
|
'dll/femtobolt/bin/k4arecord.dll': 'K4A录制库',
|
|||
|
'dll/femtobolt/bin/depthengine_2_0.dll': '深度引擎',
|
|||
|
'dll/femtobolt/bin/OrbbecSDK.dll': 'Orbbec SDK',
|
|||
|
'dll/femtobolt/bin/ob_usb.dll': 'Orbbec USB库',
|
|||
|
'dll/femtobolt/bin/live555.dll': 'Live555库',
|
|||
|
'dll/smitsense/SMiTSenseUsb-F3.0.dll': 'SMiTSense传感器库',
|
|||
|
'dll/femtobolt/bin/OrbbecSDKConfig_v1.0.xml': 'Orbbec配置文件'
|
|||
|
}
|
|||
|
|
|||
|
missing_files = []
|
|||
|
|
|||
|
print("检查必需文件...")
|
|||
|
for file_path, description in required_files.items():
|
|||
|
if os.path.exists(file_path):
|
|||
|
print(f"✓ {description}: {file_path}")
|
|||
|
else:
|
|||
|
missing_files.append((file_path, description))
|
|||
|
print(f"❌ {description}: {file_path} - 文件不存在")
|
|||
|
|
|||
|
print("\n检查DLL文件...")
|
|||
|
for dll_path, description in required_dlls.items():
|
|||
|
if os.path.exists(dll_path):
|
|||
|
print(f"✓ {description}: {dll_path}")
|
|||
|
else:
|
|||
|
missing_files.append((dll_path, description))
|
|||
|
print(f"❌ {description}: {dll_path} - 文件不存在")
|
|||
|
|
|||
|
if missing_files:
|
|||
|
print(f"\n警告: 发现 {len(missing_files)} 个缺失文件")
|
|||
|
print("缺失的文件可能导致打包失败或运行时错误")
|
|||
|
return False
|
|||
|
|
|||
|
print("\n✓ 所有必需文件检查通过")
|
|||
|
return True
|
|||
|
|
|||
|
def main():
|
|||
|
"""主函数"""
|
|||
|
print("=" * 50)
|
|||
|
print("身体平衡评估系统 - 后端打包工具")
|
|||
|
print("=" * 50)
|
|||
|
print()
|
|||
|
|
|||
|
# 检查当前目录
|
|||
|
if not os.path.exists('app.py'):
|
|||
|
print("❌ 请在backend目录下运行此脚本")
|
|||
|
return
|
|||
|
|
|||
|
# 检查必需文件
|
|||
|
print("\n检查必需文件...")
|
|||
|
if not check_required_files():
|
|||
|
print("\n⚠️ 文件检查发现问题,但将继续执行打包")
|
|||
|
print("如果打包失败,请检查上述缺失的文件")
|
|||
|
input("按回车键继续...")
|
|||
|
|
|||
|
try:
|
|||
|
# 创建配置文件
|
|||
|
create_spec_file()
|
|||
|
# create_main_entry() # 注释掉以保留现有的main_exe.py修改
|
|||
|
|
|||
|
# 临时:为诊断脚本创建spec文件
|
|||
|
if len(sys.argv) > 1 and sys.argv[1] == 'debug':
|
|||
|
create_debug_spec()
|
|||
|
return
|
|||
|
elif len(sys.argv) > 1 and sys.argv[1] == 'minimal':
|
|||
|
create_minimal_spec()
|
|||
|
return
|
|||
|
|
|||
|
# 构建exe
|
|||
|
if build_exe():
|
|||
|
print("\n✅ 所有操作完成!")
|
|||
|
else:
|
|||
|
print("\n❌ 构建过程中出现错误")
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
print(f"❌ 错误: {e}")
|
|||
|
|
|||
|
input("\n按回车键退出...")
|
|||
|
|
|||
|
if __name__ == '__main__':
|
|||
|
main()
|