PyQt5桌面应用GitHub版本自动更新故障排查与方案咨询
PyQt5桌面应用GitHub更新工作流问题解决与优化方案
背景与需求
开发了一款Windows平台下基于PyQt5的桌面应用(打包为.exe)供内部分析师使用,期望搭建基于GitHub的自动更新工作流:
- 开发者修改应用代码后推送到GitHub仓库
- 用户运行.exe时,应用自动检测新版本,弹出提示询问是否更新并重启
目前已实现:将develop分支合并到master分支时,更新my_qt_app.py并递增version_info.txt的补丁版本号;本地运行my_qt_app.py时能正确检测到远程master分支的版本更新。但执行应用内的git pull命令后,仅version_info.txt被更新,my_qt_app.py代码未同步,无法实现预期的代码更新效果。目标是实现类似rpcs3模拟器的更新流程:版本检测→用户确认→自动更新→重启应用。
当前依赖包
pyqt5 requests
现有问题代码
import requests, sys, subprocess import platform from PyQt5.QtCore import QTimer, Qt, QSettings from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QMessageBox # Replace with your GitHub username and repository name GITHUB_USERNAME = "malikcodergreen" REPOSITORY_NAME = "version_updater_app" # URL for the version info file VERSION_INFO_URL = f"https://raw.githubusercontent.com/{GITHUB_USERNAME}/{REPOSITORY_NAME}/master/version_info.txt" class HelloWorld(QWidget): def check_for_updates(self): print(f"Checking for updates") try: response = requests.get(VERSION_INFO_URL) latest_version = response.text.strip() # Replace with your logic to get current version (e.g., from a file) version_file = "version_info.txt" try: with open(version_file, "r") as file: current_version = file.readline().strip() except IOError: raise IOError(f"Error opening version file: {version_file}") # Display an error message to the user if latest_version != current_version: message_box = QMessageBox() message_box.setWindowTitle("Update Available") message_box.setText(f"A new version ({latest_version}) is available! You're currently on version {current_version}.") message_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) try: # Replace with your Git repository URL repo_url = f"https://github.com/{GITHUB_USERNAME}/{REPOSITORY_NAME}.git" button_pressed = message_box.exec_() if button_pressed == QMessageBox.Yes: # update the existing version file try: with open(version_file, "w") as file: file.write(latest_version) except IOError: raise IOError(f"Error writing to version file: {version_file}") subprocess.run(["git", "pull", repo_url]) # Option 1: Restart using a separate script (recommended) self.close() # Create a separate script (e.g., restart.bat or restart.sh) to relaunch the application if platform.system() == "Windows": subprocess.Popen(["restart.bat"]) # Windows example else: subprocess.Popen(["restart.sh"]) # Linux/macOS example print("Update pulled successfully. Restarting application...") elif button_pressed == QMessageBox.No: print("User declined update") message_box.done(1) self.update_timer.stop() except subprocess.CalledProcessError as e: print(e.output) # Add an optional "Remind Later" button (custom button) # remind_later_button = message_box.addButton("Remind Later", QMessageBox.NoRole) # You can connect a signal to this button for later reminder functionality except requests.exceptions.RequestException as e: print(f"Error checking for updates: {e}") def __init__(self): super().__init__() # Set window title self.setWindowTitle("MY AWESOME NEW TITLE!") # Create a label widget self.label = QLabel("HELLO FROM BOOZ's WORLD", self) # Center the label in the window self.label.setAlignment(Qt.AlignCenter) # Set the window size self.resize(400, 300) self.skip_update = False # Schedule update check every minute self.update_timer = QTimer(self) self.update_timer.setInterval(600) # Milliseconds in a minute self.update_timer.timeout.connect(self.check_for_updates) self.update_timer.start() # Show the widget (make the window visible) self.show() if __name__ == "__main__": app = QApplication([]) window = HelloWorld() sys.exit(app.exec_())
问题分析与修复
核心问题原因
- Git命令参数错误:
git pull无需传入仓库URL,正确用法是拉取指定远程分支,当前命令会导致Git执行异常,无法同步代码。 - 手动修改版本文件冲突:先手动覆盖
version_info.txt再执行git pull,可能导致Git版本文件冲突,或跳过代码同步逻辑。 - 未指定工作目录:如果应用运行时的工作目录不是Git仓库根目录,
git pull无法找到仓库,自然无法更新代码。
修复后的代码
import requests, sys, subprocess, os import platform from PyQt5.QtCore import QTimer, Qt, QSettings from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QMessageBox GITHUB_USERNAME = "malikcodergreen" REPOSITORY_NAME = "version_updater_app" VERSION_INFO_URL = f"https://raw.githubusercontent.com/{GITHUB_USERNAME}/{REPOSITORY_NAME}/master/version_info.txt" # 指定Git仓库根目录(打包后可通过sys.executable获取应用所在目录) REPO_ROOT = os.path.dirname(os.path.abspath(__file__)) class HelloWorld(QWidget): def check_for_updates(self): print("Checking for updates") try: response = requests.get(VERSION_INFO_URL) response.raise_for_status() latest_version = response.text.strip() version_file = os.path.join(REPO_ROOT, "version_info.txt") try: with open(version_file, "r") as file: current_version = file.readline().strip() except IOError as e: print(f"Error opening version file: {e}") return if latest_version != current_version: message_box = QMessageBox() message_box.setWindowTitle("Update Available") message_box.setText(f"新版本 {latest_version} 已发布!当前版本为 {current_version}。") message_box.setStandardButtons(QMessageBox.Yes | QMessageBox.No) button_pressed = message_box.exec_() if button_pressed == QMessageBox.Yes: try: # 执行Git拉取,指定仓库目录和分支 subprocess.run( ["git", "pull", "origin", "master"], cwd=REPO_ROOT, check=True, capture_output=True, text=True ) print("代码与版本文件已同步") # 关闭当前应用并启动重启脚本 self.close() restart_script = os.path.join(REPO_ROOT, "restart.bat") if platform.system() == "Windows": subprocess.Popen([restart_script], shell=True) print("正在重启应用...") except subprocess.CalledProcessError as e: print(f"Git拉取失败: {e.stderr}") QMessageBox.critical(self, "更新失败", f"同步代码时出错:{e.stderr}") elif button_pressed == QMessageBox.No: print("用户拒绝更新") self.update_timer.stop() except requests.exceptions.RequestException as e: print(f"检查更新失败: {e}") def __init__(self): super().__init__() self.setWindowTitle("MY AWESOME NEW TITLE!") self.label = QLabel("HELLO FROM BOOZ's WORLD", self) self.label.setAlignment(Qt.AlignCenter) self.resize(400, 300) self.update_timer = QTimer(self) self.update_timer.setInterval(60000) # 改为60秒,原600毫秒过于频繁 self.update_timer.timeout.connect(self.check_for_updates) self.update_timer.start() self.show() if __name__ == "__main__": app = QApplication([]) window = HelloWorld() sys.exit(app.exec_())
配套重启脚本(restart.bat)
@echo off :: 等待2秒确保原进程关闭 timeout /t 2 /nobreak > nul :: 启动应用(如果是打包后的exe,替换为实际文件名) start "" "python" "my_qt_app.py" :: 如果是exe文件,使用:start "" "my_qt_app.exe" exit
更优更新方案(基于GitHub Releases)
使用Git拉取代码的方案依赖用户机器安装Git,且打包为单文件exe时仓库结构易被破坏。更可靠的方案是通过GitHub Releases分发更新:
流程步骤
- 发布新版本:每次更新代码后,打包生成exe,在GitHub仓库创建Release,上传exe和版本说明。
- 版本检测:通过GitHub API获取最新Release的版本号:
def get_latest_release_version(): url = f"https://api.github.com/repos/{GITHUB_USERNAME}/{REPOSITORY_NAME}/releases/latest" response = requests.get(url) response.raise_for_status() return response.json()["tag_name"] - 下载更新:如果版本不一致,下载Release中的exe资产:
def download_latest_exe(): url = f"https://api.github.com/repos/{GITHUB_USERNAME}/{REPOSITORY_NAME}/releases/latest" response = requests.get(url) response.raise_for_status() assets = response.json()["assets"] exe_asset = next(a for a in assets if a["name"].endswith(".exe")) download_url = exe_asset["browser_download_url"] with open("temp_update.exe", "wb") as f: f.write(requests.get(download_url).content) return "temp_update.exe" - 替换并重启:通过重启脚本替换当前exe(需先关闭原进程),再启动新exe。
优势
- 无需用户安装Git,依赖更少
- 直接分发打包好的exe,避免代码暴露
- 版本管理更规范,支持回滚和Release说明
内容的提问来源于stack exchange,提问作者LeeKLikesToCode
相关产品推荐
相关产品推荐

