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()