first commit
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
import configparser
|
||||
import subprocess
|
||||
import shutil
|
||||
import argparse
|
||||
import requests
|
||||
from pathlib import Path
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
||||
from build import get_settings
|
||||
|
||||
def download_file(url, dest):
|
||||
"""下载文件到指定路径"""
|
||||
try:
|
||||
r = requests.get(url, stream=True)
|
||||
r.raise_for_status()
|
||||
with open(dest, 'wb') as f:
|
||||
for chunk in r.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"下载失败: {e}")
|
||||
return False
|
||||
|
||||
def load_config(config_path):
|
||||
"""加载配置文件"""
|
||||
return get_settings()
|
||||
config = configparser.ConfigParser()
|
||||
try:
|
||||
# 指定 UTF-8 编码读取配置文件
|
||||
config.read(config_path, encoding='utf-8')
|
||||
if not config.sections(): # 检查是否成功读取到配置项
|
||||
raise ValueError(f"配置文件 {config_path} 为空或格式错误")
|
||||
return config
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"配置文件 {config_path} 不存在")
|
||||
except UnicodeDecodeError:
|
||||
# 尝试其他常见编码
|
||||
try:
|
||||
config.read(config_path, encoding='gbk')
|
||||
if not config.sections():
|
||||
raise ValueError(f"配置文件 {config_path} 为空或格式错误")
|
||||
return config
|
||||
except Exception as e:
|
||||
raise ValueError(f"配置文件 {config_path} 编码错误: {str(e)}")
|
||||
except Exception as e:
|
||||
raise ValueError(f"加载配置文件 {config_path} 失败: {str(e)}")
|
||||
|
||||
def build_nuitka_command(config, args, app_name):
|
||||
"""构造Nuitka编译命令"""
|
||||
cmd = []
|
||||
|
||||
# 基本选项
|
||||
|
||||
if compiler := args.compiler or config.nuitka.compiler:
|
||||
cmd.append(f"--{compiler}")
|
||||
if args.show_progress or config.nuitka.show_progress:
|
||||
cmd.append("--show-progress")
|
||||
if args.standalone or config.nuitka.standalone:
|
||||
cmd.append("--standalone")
|
||||
if args.onefile or config.nuitka.onefile:
|
||||
cmd.append("--onefile")
|
||||
if config.nuitka.remove_output:
|
||||
cmd.append("--remove-output")
|
||||
if args.disable_console or config.nuitka.disable_console:
|
||||
cmd.append("--windows-disable-console")
|
||||
|
||||
# 版本信息
|
||||
if product_version := config.version.version:
|
||||
cmd.append(f"--product-version={product_version}")
|
||||
if file_version := config.version.version_str:
|
||||
cmd.append(f"--file-version={file_version}")
|
||||
if company_name := config.version.author:
|
||||
cmd.append(f"--company-name={company_name}")
|
||||
if product_name := (config.version.alias or config.version.name):
|
||||
cmd.append(f"--product-name={product_name}")
|
||||
cmd.append(f"--output-filename={app_name}")
|
||||
|
||||
# 添加新的编译选项
|
||||
if config.nuitka.get('follow_imports', True):
|
||||
cmd.append("--follow-imports")
|
||||
|
||||
if lto := config.nuitka.lto:
|
||||
cmd.append(f"--lto={lto}")
|
||||
|
||||
if jobs := args.jobs or config.nuitka.jobs:
|
||||
cmd.append(f"--jobs={jobs}")
|
||||
|
||||
# 插件处理
|
||||
if plugins := config.nuitka.enable_plugin:
|
||||
for plugin in plugins.split(','):
|
||||
cmd.append(f"--enable-plugin={plugin.strip()}")
|
||||
|
||||
# 包和模块包含
|
||||
if packages := config.nuitka.include_package:
|
||||
for package in packages.split(','):
|
||||
cmd.append(f"--include-package={package.strip()}")
|
||||
|
||||
if modules := config.nuitka.include_module:
|
||||
for module in modules.split(','):
|
||||
cmd.append(f"--include-module={module.strip()}")
|
||||
|
||||
# 不跟随的导入
|
||||
if nofollow := config.nuitka.nofollow_import_to:
|
||||
for item in nofollow.split(','):
|
||||
cmd.append(f"--nofollow-import-to={item.strip()}")
|
||||
|
||||
# 数据文件排除
|
||||
if noinclude_data := config.nuitka.noinclude_data_files:
|
||||
for pattern in noinclude_data.split(','):
|
||||
cmd.append(f"--noinclude-data-files={pattern.strip()}")
|
||||
|
||||
# Python标志
|
||||
if python_flags := config.nuitka.python_flag:
|
||||
for flag in python_flags.split(','):
|
||||
cmd.append(f"--python-flag={flag.strip()}")
|
||||
|
||||
# 图标处理
|
||||
icon_source = args.icon or config.build.icon_file
|
||||
if icon_source:
|
||||
ico_path = 'favicon.ico'
|
||||
if 'ico' not in icon_source:
|
||||
from tranform_to_ico import convert_png_to_ico
|
||||
convert_png_to_ico(icon_source, ico_path, sizes=[(256, 256)], sharpen=False)
|
||||
else:
|
||||
ico_path = icon_source
|
||||
|
||||
if ico_path.startswith(('http://', 'https://')):
|
||||
local_icon = Path("download.ico")
|
||||
if download_file(ico_path, local_icon):
|
||||
cmd.append(f"--windows-icon-from-ico={local_icon}")
|
||||
else:
|
||||
cmd.append(f"--windows-icon-from-ico={ico_path}")
|
||||
|
||||
# 额外选项
|
||||
if extra_options := config.nuitka.extra_options:
|
||||
if isinstance(extra_options, str):
|
||||
extra_options = extra_options.split(',')
|
||||
cmd.extend(extra_options)
|
||||
|
||||
# 输出目录
|
||||
output_dir = args.output or config.build.output_dir or 'dist'
|
||||
cmd.append(f"--output-dir={output_dir}")
|
||||
|
||||
python_exe = sys.executable
|
||||
cmd = [python_exe, "-m", "nuitka"] + list(set(cmd))
|
||||
|
||||
# 源文件
|
||||
source_script = args.script or config.build.source_script or 'source_script'
|
||||
cmd.append(source_script)
|
||||
|
||||
return cmd
|
||||
|
||||
def sign_executable(config, args):
|
||||
"""签名可执行文件"""
|
||||
if not args.sign:
|
||||
print("跳过签名步骤")
|
||||
return True
|
||||
|
||||
sign_tool = config.signing.sign_tool
|
||||
if not sign_tool or not Path(sign_tool).exists():
|
||||
print("警告: signtool.exe 未找到,跳过签名")
|
||||
return False
|
||||
|
||||
cert_file = args.cert or config.signing.cert_file
|
||||
if not cert_file or not Path(cert_file).exists():
|
||||
print("警告: 证书文件不存在,跳过签名")
|
||||
return False
|
||||
|
||||
cert_password = args.password or config.signing.cert_password or ''
|
||||
timestamp_server = args.timestamp or config.signing.timestamp_server or 'http://timestamp.digicert.com'
|
||||
|
||||
output_dir = args.output or config.build.output_dirt
|
||||
source_script = args.script or config.build.source_script
|
||||
exe_name = Path(source_script).stem + ".exe"
|
||||
exe_path = Path(output_dir) / exe_name
|
||||
|
||||
sign_cmd = [
|
||||
sign_tool,
|
||||
"sign",
|
||||
"/tr", timestamp_server,
|
||||
"/td", "sha256",
|
||||
"/fd", "sha256",
|
||||
"/f", cert_file,
|
||||
"/p", cert_password,
|
||||
str(exe_path),
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(sign_cmd, check=True)
|
||||
print(f"✓ 签名成功: {exe_path}")
|
||||
|
||||
# 验证签名
|
||||
verify_cmd = [sign_tool, "verify", "/pa", str(exe_path)]
|
||||
subprocess.run(verify_cmd, check=True)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"✗ 签名失败: {e}")
|
||||
return False
|
||||
|
||||
def parse_args():
|
||||
"""解析命令行参数"""
|
||||
parser = argparse.ArgumentParser(description="Python打包签名工具")
|
||||
parser.add_argument('--config', default='build_config.ini',
|
||||
help="配置文件路径 (默认: build_config.ini)")
|
||||
parser.add_argument('--script', help="覆盖配置中的源脚本")
|
||||
parser.add_argument('--output', help="覆盖输出目录")
|
||||
parser.add_argument('--icon', help="本地或在线ICO图标路径 (http/https)")
|
||||
parser.add_argument('--jobs', help="并行进程数")
|
||||
parser.add_argument('--compiler', default="mingw64", help="编译器类型")
|
||||
parser.add_argument('--show-progress', default="store_true", help="显示编译进度")
|
||||
parser.add_argument('--standalone', action='store_true', help="强制启用standalone模式")
|
||||
parser.add_argument('--onefile', action='store_true', help="强制启用单文件模式")
|
||||
parser.add_argument('--disable-console', action='store_true',
|
||||
help="禁用控制台窗口 (GUI程序)")
|
||||
parser.add_argument('--no-sign', dest='sign', action='store_false',
|
||||
help="跳过签名步骤")
|
||||
parser.add_argument('--cert', help="覆盖证书路径")
|
||||
parser.add_argument('--password', help="证书密码")
|
||||
parser.add_argument('--timestamp', help="覆盖时间戳服务器URL")
|
||||
return parser.parse_args()
|
||||
|
||||
def format_file_size(size_bytes):
|
||||
"""将文件大小转换为易读格式"""
|
||||
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||
if size_bytes < 1024.0:
|
||||
return f"{size_bytes:.2f} {unit}"
|
||||
size_bytes /= 1024.0
|
||||
return f"{size_bytes:.2f} TB"
|
||||
|
||||
def get_exe_path(config, args):
|
||||
"""获取生成的exe文件路径"""
|
||||
output_dir = args.output or config.build.output_dirt or 'dist'
|
||||
source_script = args.script or config.build.source_script
|
||||
exe_name = Path(source_script).stem + (".exe" if os.name == 'nt' else "")
|
||||
return Path(output_dir) / exe_name
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
print("=== 开始构建 ===")
|
||||
start_time = time.time()
|
||||
build_success = True
|
||||
sign_success = False
|
||||
|
||||
try:
|
||||
config = load_config(args.config)
|
||||
except FileNotFoundError as e:
|
||||
print(f"错误: {e}")
|
||||
return
|
||||
|
||||
# import json; print(json.dumps(config, indent=4, ensure_ascii=False))
|
||||
# import json; print(json.dumps(get_settings(), indent=4, ensure_ascii=False))
|
||||
# return
|
||||
|
||||
# 清理旧构建
|
||||
output_dir = args.output or config.build.output_dir or 'dist'
|
||||
if config.build.clean and Path(output_dir).exists():
|
||||
try:
|
||||
shutil.rmtree(output_dir)
|
||||
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
||||
print(f"已清理输出目录: {output_dir}")
|
||||
except Exception as e:
|
||||
print(f"清理目录时出错: {e}")
|
||||
build_success = False
|
||||
|
||||
product_name = (config.version.alias or config.version.name)
|
||||
product_version = config.version.version
|
||||
app_name = f"{product_name}{'v'+product_version if product_version else ''}"
|
||||
print(f"应用名称: {app_name}")
|
||||
build_success = True
|
||||
|
||||
if build_success:
|
||||
# 编译
|
||||
print("\n[1/3] 正在使用Nuitka编译...")
|
||||
try:
|
||||
nuitka_cmd = build_nuitka_command(config, args, app_name)
|
||||
print("执行命令:", " ".join(nuitka_cmd))
|
||||
|
||||
compile_start = time.time()
|
||||
subprocess.run(nuitka_cmd, check=True)
|
||||
print(f"编译完成,耗时: {time.time() - compile_start:.2f}秒")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"编译失败: {e}")
|
||||
build_success = False
|
||||
|
||||
if build_success and args.sign:
|
||||
# 签名
|
||||
print("\n[2/3] 正在处理签名...")
|
||||
try:
|
||||
sign_start = time.time()
|
||||
sign_success = sign_executable(config, args)
|
||||
if sign_success:
|
||||
print(f"签名完成,耗时: {time.time() - sign_start:.2f}秒")
|
||||
except Exception as e:
|
||||
print(f"签名过程中出错: {e}")
|
||||
sign_success = False
|
||||
|
||||
if build_success:
|
||||
# 压缩文件
|
||||
print("\n[3/3] 正在压缩文件...")
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
zip_path = f"{output_dir}/{app_name}.zip"
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
for file in [f'{output_dir}/{app_name}.exe', *(config.build.resource_files or [])]:
|
||||
if not file: continue
|
||||
filename = Path(file).name
|
||||
zipf.write(file, arcname=filename)
|
||||
print(f"压缩完成: {zip_path}")
|
||||
except Exception as e:
|
||||
print(f"压缩文件时出错: {e}")
|
||||
build_success = False
|
||||
|
||||
zip_app_name = f"{config.version.name}{'v'+product_version if product_version else ''}.zip"
|
||||
version_data = config.version.to_dict()
|
||||
save_url = config.build.save_url.strip('/')
|
||||
access_url = config.version.access_url.strip('/')
|
||||
version_data['url'] = access_url + '/' + zip_app_name
|
||||
|
||||
import json
|
||||
with open(f'{output_dir}/version.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(dict(
|
||||
data=version_data,
|
||||
code=200,
|
||||
success='true',
|
||||
message=f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} - {app_name} 打包完成',
|
||||
), f, ensure_ascii=False, indent=4)
|
||||
print(f"版本信息已保存到: {output_dir}/version.json")
|
||||
|
||||
if not os.system(f'curl -T {zip_path} {save_url}/{zip_app_name}'):
|
||||
print(f"上传完成: {version_data['url']}")
|
||||
os.system(f'curl -T {output_dir}/version.json {save_url}/latest')
|
||||
|
||||
# 构建总结
|
||||
print("\n=== 构建总结 ===")
|
||||
total_time = time.time() - start_time
|
||||
status = "成功" if build_success else "失败"
|
||||
|
||||
summary = [
|
||||
f"构建状态: {status}",
|
||||
f"总耗时: {total_time:.2f}秒",
|
||||
f"输出目录: {output_dir}",
|
||||
]
|
||||
|
||||
if build_success:
|
||||
exe_path = get_exe_path(config, args)
|
||||
if exe_path.exists():
|
||||
file_time = time.strftime('%Y-%m-%d %H:%M:%S',
|
||||
time.localtime(exe_path.stat().st_mtime))
|
||||
summary.extend([
|
||||
f"生成文件: {exe_path}",
|
||||
f"文件大小: {format_file_size(exe_path.stat().st_size)}",
|
||||
f"修改时间: {file_time}",
|
||||
])
|
||||
|
||||
if args.sign:
|
||||
summary.append(f"签名状态: {'成功' if sign_success else '失败'}")
|
||||
|
||||
print("\n".join(summary))
|
||||
|
||||
if not build_success:
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user