You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题分析与修复

核心问题原因

  1. Git命令参数错误:git pull无需传入仓库URL,正确用法是拉取指定远程分支,当前命令会导致Git执行异常,无法同步代码。
  2. 手动修改版本文件冲突:先手动覆盖version_info.txt再执行git pull,可能导致Git版本文件冲突,或跳过代码同步逻辑。
  3. 未指定工作目录:如果应用运行时的工作目录不是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分发更新:

流程步骤

  1. 发布新版本:每次更新代码后,打包生成exe,在GitHub仓库创建Release,上传exe和版本说明。
  2. 版本检测:通过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"]
    
  3. 下载更新:如果版本不一致,下载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"
    
  4. 替换并重启:通过重启脚本替换当前exe(需先关闭原进程),再启动新exe。

优势

  • 无需用户安装Git,依赖更少
  • 直接分发打包好的exe,避免代码暴露
  • 版本管理更规范,支持回滚和Release说明

内容的提问来源于stack exchange,提问作者LeeKLikesToCode

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 12:24:50