Files
nl-tools/build_app.py
2025-12-11 09:47:00 +08:00

392 lines
12 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.
"""
奶酪云工具箱 - 打包脚本
支持 Windows (7/10/11) 32位/64位, macOS 打包
使用方法:
python build_app.py # 打包当前平台
python build_app.py --all # 尝试打包所有平台(需要对应环境)
python build_app.py --platform win64 # 指定平台
"""
import os
import sys
import shutil
import platform
import subprocess
from pathlib import Path
# 项目配置
APP_NAME = "奶酪云工具箱"
APP_NAME_EN = "CheeseCloudTools"
VERSION = "1.0.0"
AUTHOR = "奶酪源码"
DESCRIPTION = "多功能图片/PDF/Excel处理工具"
# 路径配置
PROJECT_ROOT = Path(__file__).parent
DIST_DIR = PROJECT_ROOT / "dist"
BUILD_DIR = PROJECT_ROOT / "build"
ICON_SOURCE = PROJECT_ROOT / "image" / "生成奶酪商城官方店介绍.png"
ICON_DIR = PROJECT_ROOT / "build_icons"
# 需要包含的数据文件
DATA_FILES = [
("resources", "resources"),
("image", "image"),
]
def create_icon_dir():
"""创建图标目录"""
ICON_DIR.mkdir(exist_ok=True)
def convert_png_to_ico(png_path: Path, ico_path: Path, sizes=None):
"""将PNG转换为ICO格式Windows图标"""
if sizes is None:
sizes = [(256, 256), (128, 128), (64, 64), (48, 48), (32, 32), (16, 16)]
try:
from PIL import Image
img = Image.open(png_path)
# 创建不同尺寸的图标
icon_images = []
for size in sizes:
resized = img.copy()
resized.thumbnail(size, Image.Resampling.LANCZOS)
# 确保尺寸正确
if resized.size != size:
new_img = Image.new('RGBA', size, (0, 0, 0, 0))
offset = ((size[0] - resized.size[0]) // 2,
(size[1] - resized.size[1]) // 2)
new_img.paste(resized, offset)
resized = new_img
icon_images.append(resized)
# 保存为ICO
icon_images[0].save(
ico_path,
format='ICO',
sizes=[(img.size[0], img.size[1]) for img in icon_images]
)
print(f"✅ 已创建 Windows 图标: {ico_path}")
return True
except Exception as e:
print(f"❌ 创建 ICO 失败: {e}")
return False
def convert_png_to_icns(png_path: Path, icns_path: Path):
"""将PNG转换为ICNS格式macOS图标"""
try:
from PIL import Image
img = Image.open(png_path)
# macOS 需要特定尺寸
sizes = [16, 32, 64, 128, 256, 512, 1024]
# 创建临时 iconset 目录
iconset_dir = icns_path.parent / f"{icns_path.stem}.iconset"
iconset_dir.mkdir(exist_ok=True)
for size in sizes:
# 标准分辨率
resized = img.copy()
resized.thumbnail((size, size), Image.Resampling.LANCZOS)
resized.save(iconset_dir / f"icon_{size}x{size}.png")
# 2x 分辨率 (Retina)
if size <= 512:
resized_2x = img.copy()
resized_2x.thumbnail((size * 2, size * 2), Image.Resampling.LANCZOS)
resized_2x.save(iconset_dir / f"icon_{size}x{size}@2x.png")
# 使用 iconutil 转换仅macOS可用
if platform.system() == "Darwin":
subprocess.run(["iconutil", "-c", "icns", str(iconset_dir), "-o", str(icns_path)])
print(f"✅ 已创建 macOS 图标: {icns_path}")
else:
# 在非macOS上复制PNG作为替代
shutil.copy(png_path, icns_path.with_suffix('.png'))
print(f"⚠️ 非macOS环境已复制PNG: {icns_path.with_suffix('.png')}")
# 清理临时目录
shutil.rmtree(iconset_dir, ignore_errors=True)
return True
except Exception as e:
print(f"❌ 创建 ICNS 失败: {e}")
return False
def prepare_icons():
"""准备各平台图标"""
create_icon_dir()
ico_path = ICON_DIR / "app.ico"
icns_path = ICON_DIR / "app.icns"
if ICON_SOURCE.exists():
convert_png_to_ico(ICON_SOURCE, ico_path)
convert_png_to_icns(ICON_SOURCE, icns_path)
else:
print(f"⚠️ 图标源文件不存在: {ICON_SOURCE}")
return ico_path, icns_path
def get_pyinstaller_args(target_platform: str, ico_path: Path, icns_path: Path):
"""获取 PyInstaller 参数"""
# 基础参数
args = [
"pyinstaller",
"--noconfirm",
"--clean",
"--name", f"{APP_NAME_EN}",
"--windowed", # GUI应用不显示控制台
"--onedir", # 打包为目录(更稳定)
]
# 添加数据文件
for src, dst in DATA_FILES:
src_path = PROJECT_ROOT / src
if src_path.exists():
args.extend(["--add-data", f"{src_path}{os.pathsep}{dst}"])
# 隐藏导入
hidden_imports = [
"PySide6.QtSvg",
"PySide6.QtSvgWidgets",
"PIL",
"PIL.Image",
"fitz",
"pandas",
"openpyxl",
"matplotlib",
"matplotlib.backends.backend_qtagg",
]
for hi in hidden_imports:
args.extend(["--hidden-import", hi])
# 平台特定参数
if target_platform.startswith("win"):
if ico_path.exists():
args.extend(["--icon", str(ico_path)])
# Windows 版本信息
args.extend([
"--version-file", str(PROJECT_ROOT / "version_info.txt"),
])
elif target_platform.startswith("mac"):
if icns_path.exists():
args.extend(["--icon", str(icns_path)])
# macOS bundle 标识符
args.extend([
"--osx-bundle-identifier", "com.naiyuanma.cheesetools",
])
# 输出目录
output_dir = DIST_DIR / target_platform
args.extend(["--distpath", str(output_dir)])
args.extend(["--workpath", str(BUILD_DIR / target_platform)])
args.extend(["--specpath", str(BUILD_DIR)])
# 主入口文件
args.append(str(PROJECT_ROOT / "main.py"))
return args
def create_version_info():
"""创建 Windows 版本信息文件"""
version_parts = VERSION.split(".")
while len(version_parts) < 4:
version_parts.append("0")
version_tuple = ", ".join(version_parts)
content = f'''# UTF-8
VSVersionInfo(
ffi=FixedFileInfo(
filevers=({version_tuple}),
prodvers=({version_tuple}),
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
u'080404b0',
[
StringStruct(u'CompanyName', u'{AUTHOR}'),
StringStruct(u'FileDescription', u'{DESCRIPTION}'),
StringStruct(u'FileVersion', u'{VERSION}'),
StringStruct(u'InternalName', u'{APP_NAME_EN}'),
StringStruct(u'LegalCopyright', u'Copyright (C) 2024 {AUTHOR}'),
StringStruct(u'OriginalFilename', u'{APP_NAME_EN}.exe'),
StringStruct(u'ProductName', u'{APP_NAME}'),
StringStruct(u'ProductVersion', u'{VERSION}'),
]
)
]
),
VarFileInfo([VarStruct(u'Translation', [2052, 1200])])
]
)
'''
version_file = PROJECT_ROOT / "version_info.txt"
with open(version_file, "w", encoding="utf-8") as f:
f.write(content)
print(f"✅ 已创建版本信息文件: {version_file}")
def build_for_platform(target_platform: str):
"""为指定平台打包"""
print(f"\n{'='*50}")
print(f"🔨 开始打包: {target_platform}")
print(f"{'='*50}")
# 准备图标
ico_path, icns_path = prepare_icons()
# 创建版本信息Windows
if target_platform.startswith("win"):
create_version_info()
# 获取 PyInstaller 参数
args = get_pyinstaller_args(target_platform, ico_path, icns_path)
print(f"📦 执行命令: {' '.join(args)}")
# 执行打包
try:
result = subprocess.run(args, check=True)
print(f"\n{target_platform} 打包成功!")
print(f"📁 输出目录: {DIST_DIR / target_platform}")
return True
except subprocess.CalledProcessError as e:
print(f"\n{target_platform} 打包失败: {e}")
return False
def get_current_platform():
"""获取当前平台标识"""
system = platform.system().lower()
machine = platform.machine().lower()
if system == "windows":
if machine in ["amd64", "x86_64"]:
return "win64"
else:
return "win32"
elif system == "darwin":
if machine == "arm64":
return "mac_arm64"
else:
return "mac_x64"
elif system == "linux":
if machine in ["amd64", "x86_64"]:
return "linux64"
else:
return "linux32"
return "unknown"
def clean_build():
"""清理构建目录"""
print("🧹 清理构建目录...")
if BUILD_DIR.exists():
shutil.rmtree(BUILD_DIR)
if ICON_DIR.exists():
shutil.rmtree(ICON_DIR)
print("✅ 清理完成")
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description="奶酪云工具箱打包脚本")
parser.add_argument("--platform", "-p",
choices=["win32", "win64", "mac_x64", "mac_arm64", "current"],
default="current",
help="目标平台")
parser.add_argument("--all", "-a", action="store_true",
help="打包所有平台(需要对应环境)")
parser.add_argument("--clean", "-c", action="store_true",
help="清理构建目录")
args = parser.parse_args()
print(f"""
╔══════════════════════════════════════════════╗
║ 奶酪云工具箱 - 打包工具 ║
║ 版本: {VERSION}
╚══════════════════════════════════════════════╝
""")
if args.clean:
clean_build()
return
# 检查 PyInstaller
try:
import PyInstaller
print(f"✅ PyInstaller 版本: {PyInstaller.__version__}")
except ImportError:
print("❌ 未安装 PyInstaller正在安装...")
subprocess.run([sys.executable, "-m", "pip", "install", "pyinstaller"])
# 确定要打包的平台
if args.all:
# 打包当前系统支持的所有架构
system = platform.system().lower()
if system == "windows":
# Windows 可以同时打包 32位和64位如果有对应Python
platforms = ["win64"] # 默认打包64位
print(f"📋 Windows 系统将打包: {platforms}")
print(f"⚠️ 32位版本需要在32位Python环境中单独打包")
elif system == "darwin":
# macOS
machine = platform.machine().lower()
if machine == "arm64":
platforms = ["mac_arm64"]
else:
platforms = ["mac_x64"]
print(f"📋 macOS 系统将打包: {platforms}")
else:
platforms = [get_current_platform()]
print(f"📋 当前平台: {platforms}")
elif args.platform == "current":
platforms = [get_current_platform()]
else:
platforms = [args.platform]
# 创建输出目录
DIST_DIR.mkdir(exist_ok=True)
BUILD_DIR.mkdir(exist_ok=True)
# 打包
success_count = 0
for plat in platforms:
if build_for_platform(plat):
success_count += 1
print(f"\n{'='*50}")
print(f"📊 打包完成: {success_count}/{len(platforms)} 成功")
print(f"📁 输出目录: {DIST_DIR}")
print(f"{'='*50}")
if __name__ == "__main__":
main()