为基于customtkinter的Python GUI应用实现GitHub自动更新功能
Python GUI应用基于GitHub Releases的自动更新方案(适配CustomTkinter)
针对托管在GitHub Releases的CustomTkinter GUI应用,这里提供一套简洁的Python风格自动更新实现,支持点击按钮触发检测、下载和安装流程,分源码分发版和打包分发版两种场景。
核心步骤概览
- 调用GitHub API检测最新版本
- 对比本地版本与最新版本
- 下载更新资源
- 替换应用文件(或通过辅助脚本完成打包版更新)
1. 版本检测实现
使用GitHub REST API获取最新Release的版本号,结合packaging库做版本号对比:
import requests import packaging.version import sys # 配置项,根据你的仓库修改 CURRENT_VERSION = "1.0.0" REPO_OWNER = "your-github-username" REPO_NAME = "your-repo-name" def check_for_update(): """检查是否有可用更新,返回最新Release数据和版本号""" try: # 调用GitHub最新Release API resp = requests.get(f"https://api.github.com/repos/{REPO_OWNER}/{REPO_NAME}/releases/latest") resp.raise_for_status() release_data = resp.json() # 处理版本号(假设tag为vX.X.X格式,去掉前缀v) latest_version = release_data["tag_name"].lstrip("v") # 版本号比较 if packaging.version.parse(latest_version) > packaging.version.parse(CURRENT_VERSION): return release_data, latest_version return None, None except Exception as e: print(f"更新检测失败: {str(e)}") return None, None
2. 下载更新资源
根据分发类型下载对应的更新包:
源码版(直接下载源码Zip)
import zipfile import os from pathlib import Path def download_source_update(release_data, temp_dir="./update_temp"): """下载并解压源码更新包,返回解压后的源码目录""" os.makedirs(temp_dir, exist_ok=True) zip_path = Path(temp_dir) / "update.zip" # 下载源码Zip包 with requests.get(release_data["zipball_url"], stream=True) as r: r.raise_for_status() with open(zip_path, "wb") as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) # 解压Zip with zipfile.ZipFile(zip_path, "r") as zip_ref: zip_ref.extractall(temp_dir) # 返回解压后的根目录(GitHub Zip包会带仓库名+commit前缀) return Path(temp_dir) / next(os.scandir(temp_dir)).name
打包版(下载对应平台的可执行文件)
def download_packaged_update(release_data, target_platform="windows", temp_dir="./update_temp"): """下载打包后的可执行文件(如exe/dmg),返回文件路径""" os.makedirs(temp_dir, exist_ok=True) # 匹配对应平台的Asset(需要你在Release中按规范命名,比如app-win.exe) asset_url = None asset_name = None for asset in release_data["assets"]: if target_platform in asset["name"].lower(): asset_url = asset["browser_download_url"] asset_name = asset["name"] break if not asset_url: raise ValueError(f"未找到{target_platform}平台的更新包") asset_path = Path(temp_dir) / asset_name with requests.get(asset_url, stream=True) as r: r.raise_for_status() with open(asset_path, "wb") as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) return asset_path
3. 应用更新逻辑
源码版:直接替换文件
import shutil def apply_source_update(update_dir, app_root="./"): """将更新源码复制到应用目录""" app_root = Path(app_root) # 递归复制所有文件 for src_path in update_dir.rglob("*"): rel_path = src_path.relative_to(update_dir) dest_path = app_root / rel_path if src_path.is_dir(): dest_path.mkdir(exist_ok=True) else: dest_path.write_bytes(src_path.read_bytes()) # 清理临时文件 shutil.rmtree(update_dir.parent) print("更新完成,请重启应用")
打包版:通过辅助脚本完成更新
由于运行中的可执行文件无法自我替换,需要编写一个独立的更新脚本:
辅助更新脚本(update_script.py)
import shutil import os import sys import time def update_app(new_exe_path, target_exe_path): # 等待主应用完全退出 time.sleep(2) # 替换目标可执行文件 shutil.copy(new_exe_path, target_exe_path) # 启动更新后的应用 os.startfile(target_exe_path) # 清理临时文件和自身 shutil.rmtree(Path(new_exe_path).parent) os.remove(sys.argv[0]) if __name__ == "__main__": if len(sys.argv) != 3: sys.exit(1) update_app(sys.argv[1], sys.argv[2])
主应用中触发打包版更新
def apply_packaged_update(release_data): """触发打包版应用更新""" try: # 下载更新包 updated_exe = download_packaged_update(release_data) # 获取当前应用的可执行文件路径 current_exe = Path(sys.executable).resolve() if getattr(sys, 'frozen', False) else Path(__file__).resolve() # 启动更新脚本并退出主应用 import subprocess subprocess.Popen([sys.executable, "./update_script.py", str(updated_exe), str(current_exe)]) sys.exit() except Exception as e: print(f"更新失败: {str(e)}")
4. 集成到CustomTkinter GUI
将更新逻辑绑定到按钮,添加状态提示:
import customtkinter as ctk class UpdateEnabledApp(ctk.CTk): def __init__(self): super().__init__() self.title("带自动更新的GUI应用") self.geometry("400x200") # 状态标签 self.status_label = ctk.CTkLabel(self, text="") self.status_label.pack(pady=20) # 更新按钮 self.update_btn = ctk.CTkButton( self, text="检查并更新", command=self.trigger_update_flow ) self.update_btn.pack(pady=10) def trigger_update_flow(self): self.update_btn.configure(state="disabled") self.status_label.configure(text="正在检查更新...") self.update() # 强制刷新GUI # 检测更新 release_data, latest_version = check_for_update() if not release_data: self.status_label.configure(text="当前已是最新版本") self.update_btn.configure(state="normal") return self.status_label.configure(text=f"发现新版本 {latest_version},正在下载...") self.update() # 根据你的分发类型选择对应逻辑 # 源码版: update_dir = download_source_update(release_data) apply_source_update(update_dir) self.status_label.configure(text="更新完成,请重启应用") # 打包版: # apply_packaged_update(release_data) # self.status_label.configure(text="正在更新,请稍候...") self.update_btn.configure(state="normal") if __name__ == "__main__": app = UpdateEnabledApp() app.mainloop()
关键注意事项
- 依赖安装:需要提前安装
requests和packaging:pip install requests packaging - 权限问题:更新时需要应用目录的写入权限,Windows打包版可能需要管理员权限
- 版本规范:GitHub Release的Tag需遵循语义化版本(如v1.0.0),确保版本对比准确
- 备份机制:建议在更新前备份当前应用文件,避免更新失败导致应用损坏
- 错误处理:实际部署时需补充更多异常捕获,给用户清晰的错误提示
内容的提问来源于stack exchange,提问作者Ilya Chernyakov
相关产品推荐
相关产品推荐

