#!/usr/bin/env python # -*- coding: utf-8 -*- import warnings import os import platform import subprocess import time import threading import json import sys from pathlib import Path # Ignore specific SyntaxWarning warnings.filterwarnings("ignore", category=SyntaxWarning, module="DrissionPage") CURSOR_LOGO = """ ██████╗██╗ ██╗██████╗ ███████╗ ██████╗ ██████╗ ██╔════╝██║ ██║██╔══██╗██╔════╝██╔═══██╗██╔══██╗ ██║ ██║ ██║██████╔╝███████╗██║ ██║██████╔╝ ██║ ██║ ██║██╔══██╗╚════██║██║ ██║██╔══██╗ ╚██████╗╚██████╔╝██║ ██║███████║╚██████╔╝██║ ██║ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ """ class LoadingAnimation: def __init__(self): self.is_running = False self.animation_thread = None def start(self, message="Building"): self.is_running = True self.animation_thread = threading.Thread(target=self._animate, args=(message,)) self.animation_thread.start() def stop(self): self.is_running = False if self.animation_thread: self.animation_thread.join() print("\r" + " " * 70 + "\r", end="", flush=True) # Clear the line def _animate(self, message): animation = "|/-\\" idx = 0 while self.is_running: print(f"\r{message} {animation[idx % len(animation)]}", end="", flush=True) idx += 1 time.sleep(0.1) def print_logo(): print("\033[96m" + CURSOR_LOGO + "\033[0m") print("\033[93m" + "Building Cursor Keep Alive...".center(56) + "\033[0m\n") def progress_bar(progress, total, prefix="", length=50): filled = int(length * progress // total) bar = "█" * filled + "░" * (length - filled) percent = f"{100 * progress / total:.1f}" print(f"\r{prefix} |{bar}| {percent}% Complete", end="", flush=True) if progress == total: print() def simulate_progress(message, duration=1.0, steps=20): print(f"\033[94m{message}\033[0m") for i in range(steps + 1): time.sleep(duration / steps) progress_bar(i, steps, prefix="Progress:", length=40) def filter_output(output): """ImportantMessage""" if not output: return "" important_lines = [] for line in output.split("\n"): # Only keep lines containing specific keywords if any( keyword in line.lower() for keyword in ["error:", "failed:", "completed", "directory:"] ): important_lines.append(line) return "\n".join(important_lines) def increment_version(): """增加构建版本号""" version_file = Path("version.json") if version_file.exists(): with open(version_file, "r") as f: version_data = json.load(f) # 增加构建号 version_data["build"] += 1 # 更新版本号的最后一位 version_parts = version_data["version"].split(".") version_parts[-1] = str(version_data["build"]) version_data["version"] = ".".join(version_parts) # 保存更新后的版本信息 with open(version_file, "w") as f: json.dump(version_data, f, indent=4) return version_data else: print("错误:未找到 version.json 文件") sys.exit(1) def create_icns(): """将 SVG 转换为 ICNS 格式""" if not Path("icons/logo.svg").exists(): print("错误:未找到 logo.svg 文件") sys.exit(1) # 创建临时目录 os.makedirs("icons/tmp.iconset", exist_ok=True) # 转换 SVG 到 PNG sizes = [16, 32, 64, 128, 256, 512, 1024] for size in sizes: # 普通分辨率 os.system(f"rsvg-convert -w {size} -h {size} icons/logo.svg > icons/tmp.iconset/icon_{size}x{size}.png") # 高分辨率(@2x) if size <= 512: os.system(f"rsvg-convert -w {size*2} -h {size*2} icons/logo.svg > icons/tmp.iconset/icon_{size}x{size}@2x.png") # 生成 icns 文件 os.system("iconutil -c icns icons/tmp.iconset -o icons/logo.icns") # 清理临时文件 os.system("rm -rf icons/tmp.iconset") def update_spec(version_data): """更新 spec 文件中的版本信息""" spec_content = f'''# -*- mode: python ; coding: utf-8 -*- block_cipher = None a = Analysis( ['cursor_gui.py'], pathex=[], binaries=[], datas=[], hiddenimports=[], 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, [], exclude_binaries=True, name='听泉Cursor助手', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, console=False, disable_windowed_traceback=False, argv_emulation=True, target_arch=None, codesign_identity=None, entitlements_file=None, icon='icons/logo.icns', ) coll = COLLECT( exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, upx_exclude=[], name='听泉Cursor助手', ) app = BUNDLE( coll, name='听泉Cursor助手.app', icon='icons/logo.icns', bundle_identifier='com.tingquan.cursor', info_plist={{ 'CFBundleShortVersionString': '{version_data["version"]}', 'CFBundleVersion': '{version_data["build"]}', 'NSHighResolutionCapable': 'True', 'LSMinimumSystemVersion': '10.13.0', 'CFBundleName': '听泉Cursor助手', 'CFBundleDisplayName': '听泉Cursor助手', }}, )''' with open("cursor_app.spec", "w") as f: f.write(spec_content) def build_app(): """构建应用程序""" # 增加版本号 version_data = increment_version() # 创建图标 create_icns() # 更新 spec 文件 update_spec(version_data) # 更新主程序中的版本号 update_main_version(version_data["version"]) # 构建应用 os.system("pyinstaller cursor_app.spec") # 创建压缩包 os.system(f'cd dist && zip -r "听泉Cursor助手_v{version_data["version"]}.zip" "听泉Cursor助手.app"') print(f"\n构建完成!版本号:v{version_data['version']}") print(f"应用程序位置:dist/听泉Cursor助手.app") print(f"压缩包位置:dist/听泉Cursor助手_v{version_data['version']}.zip") def update_main_version(version): """更新主程序中的版本号""" with open("cursor_gui.py", "r") as f: content = f.read() # 替换版本号 content = content.replace( 'self.setWindowTitle("Cursor账号管理器 v3.5.3")', f'self.setWindowTitle("听泉Cursor助手 v{version}")' ) with open("cursor_gui.py", "w") as f: f.write(content) def build(): # Clear screen os.system("cls" if platform.system().lower() == "windows" else "clear") # Print logo print_logo() system = platform.system().lower() spec_file = os.path.join("CursorKeepAlive.spec") # if system not in ["darwin", "windows"]: # print(f"\033[91mUnsupported operating system: {system}\033[0m") # return output_dir = f"dist/{system if system != 'darwin' else 'mac'}" # Create output directory os.makedirs(output_dir, exist_ok=True) simulate_progress("Creating output directory...", 0.5) # Run PyInstaller with loading animation pyinstaller_command = [ "pyinstaller", spec_file, "--distpath", output_dir, "--workpath", f"build/{system}", "--noconfirm", ] loading = LoadingAnimation() try: simulate_progress("Running PyInstaller...", 2.0) loading.start("Building in progress") result = subprocess.run( pyinstaller_command, check=True, capture_output=True, text=True ) loading.stop() if result.stderr: filtered_errors = [ line for line in result.stderr.split("\n") if any( keyword in line.lower() for keyword in ["error:", "failed:", "completed", "directory:"] ) ] if filtered_errors: print("\033[93mBuild Warnings/Errors:\033[0m") print("\n".join(filtered_errors)) except subprocess.CalledProcessError as e: loading.stop() print(f"\033[91mBuild failed with error code {e.returncode}\033[0m") if e.stderr: print("\033[91mError Details:\033[0m") print(e.stderr) return except FileNotFoundError: loading.stop() print( "\033[91mError: Please ensure PyInstaller is installed (pip install pyinstaller)\033[0m" ) return except KeyboardInterrupt: loading.stop() print("\n\033[91mBuild cancelled by user\033[0m") return finally: loading.stop() # Copy config file if os.path.exists("config.ini.example"): simulate_progress("Copying configuration file...", 0.5) if system == "windows": subprocess.run( ["copy", "config.ini.example", f"{output_dir}\\config.ini"], shell=True ) else: subprocess.run(["cp", "config.ini.example", f"{output_dir}/config.ini"]) # Copy .env.example file if os.path.exists(".env.example"): simulate_progress("Copying environment file...", 0.5) if system == "windows": subprocess.run(["copy", ".env.example", f"{output_dir}\\.env"], shell=True) else: subprocess.run(["cp", ".env.example", f"{output_dir}/.env"]) print( f"\n\033[92mBuild completed successfully! Output directory: {output_dir}\033[0m" ) if __name__ == "__main__": build_app()