SoFunction
Updated on 2025-05-07

Implementation code for automatic update of PyQt5 program

1. Background

The developed QT tools need to be used between different departments, and the iteration of the tool version requires packaging -> compression -> uploading to the shared directory -> downloading and decompressing. The collaboration is very unpleasant. Moreover, PyQt5 does not have its own update framework like native Qt, so it needs to implement a set of update logic by itself.

2. Core issues that need attention

  • Qt application needs to be run before it can have an interactive interface, that is, it is preferred to execute it.app = QApplication()
  • When updating, the modified modules need to be replaced, but when the Qt program is running, some modules are occupied and cannot be replaced and deleted, so they need to be replaced after the Qt program is stopped.

Ideas for realization

  • Another monitoring program is launched to be responsible for updates.
  • By writing and executing .bat files, it is responsible for a series of operations such as updating, replacing, and restarting after the Qt program is stopped. Obviously, the second method looks simpler and faster.

4. Specific implementation

# 
import argparse  
import os  
import sys  
  
from  import QApplication, QMessageBox  
from packaging import version  
  
from main import MainForm  
from updates_scripts.updater import update_main  
  
  
def check_is_need_update():  
    """Check if the application needs update"""  
    try:  
        remote_path = r'\\10.10.10.10' # I am a shared file server here, and I replace it myself according to my needs
        # Read the local version        with open('', 'r', encoding='utf-8') as f:  
            local_version = ().strip()  

        # traverse remote folders to find the latest version        max_version = ('0')  
        update_path = ''  

        for root, dirs, files in (remote_path):  
            for file in files:  
                if ('DebugTool_V') and ('.zip'):  
                    remote_version = file[('_V') + 2:('.')]  
                    try:  
                        remote_ver = (remote_version)  
                        if remote_ver > max_version:  
                            max_version = remote_ver  
                            update_path = (root, file)  
                    except :  
                        continue  

        # Compare version        local_ver = (local_version)  
        if max_version > local_ver:  
            # Qt dialog must be displayed in the main thread            reply = (  
            None,  
            "Update Prompt",  
            f"Discover a new version {max_version} (Current version {local_version}),Whether to update?",  
             | ,  
              
            )  

            if reply == :  
                print("User Select Update")  
                root_path = (([0]))  
                return {  
                'update': True,  
                'root_dir': root_path,  
                'version': f"DebugTool_V{str(max_version)}",  
                'remote_file': update_path  
                }  
            else:  
                print("User Cancel Update")  
       return {'update': False}  

    except Exception as e:  
        print(f"An error occurred while checking for updates: {str(e)}")  
        return {'update': False, 'error': str(e)}  
  
  
if __name__ == "__main__":  
# Initialize the Qt applicationapp = QApplication()  
parser = ()  
parser.add_argument('--check', action='store_true', help='Does it need to be checked for updates')  
args = parser.parse_args()  
# Check for updatesif not :  
    update_info = check_is_need_update()  

    if not  and update_info.get('update'):  
    # Execute update logic    print(f"Ready to update to: {update_info['version']}")  
    # Add your update logic here    if update_main(root_dir=update_info['root_dir'], version=update_info['version'],  
        remote_file=update_info['remote_file']):  
        root_path = update_info['root_dir']  
        bat_content = f"""@echo off
         REM Wait for the main program to exit
         :loop
         tasklist /FI "IMAGENAME eq " 2>NUL | find /I "" >NUL  
        if "%ERRORLEVEL%"=="0" (  
            timeout /t 1 /nobreak >NUL  
            goto loop  
        )  

        REM Delete old files  
        del /F /Q "{root_path}\*.*"  
        for /D %%p in ("{root_path}\*") do (  
            if /I not "%%~nxp"=="DebugTool" (  
                if /I not "%%~nxp"=="plans" (  
                    if /I not "%%~nxp"=="Logs" (  
                        if /I not "%%~nxp"=="results" (  
                            rmdir "%%p" /s /q  
                        )  
                    )  
                )  
            )  
        )  

        REM Copy the new version of the file(userobocopyAchieve reliable copying)  
        robocopy {(root_path, 'DebugTool')} {root_path} /E  

        REM Start a new version  
        start {root_path}\DebugTool\ --check  

        REM Delete temporary files  
        del "%~f0"  
        """  
        # Write batch scripts to temporary files        bat_path = (root_path, 'DebugTool', "")  
        with open(bat_path, 'w') as f:  
            (bat_content)  
        # Start the batch script        (bat_path)  

        # Exit the current program        ().quit()  
    else:  
        # An update failure prompt box pops up        (None, "Update failed", "Update failed, please check or reopen and select not to update")  
        (0)  
else:  
    # Start the application normally    win = MainForm()  
    ()  
    (app.exec_())
# -*- coding: utf-8 -*-  
"""  
Time : 2025/5/6 20:50  
Author :   
"""  
# -*- coding: utf-8 -*-  
"""  
Time : 2025/5/6 9:39  
Author :   
"""  
import json  
import os  
import shutil  
import sys  
import zipfile  
from datetime import datetime  
  
from  import QSettings, Qt  
from  import (QApplication, QLabel, QMessageBox, QProgressDialog)  
  
  
def get_resource_path(relative_path):  
    """Absolute path to obtain resources, compatible with development mode and post-package mode"""  
    if hasattr(sys, '_MEIPASS'):  
    return (sys._MEIPASS, relative_path)  
    return (("."), relative_path)  
  
  
class UpdateConfig:  
    """Update system configuration"""  

    def __init__(self):  
         = QSettings("DebugTool", "DebugTool")  

        # Modify to shared folder path         = {  
        'update_url': r"\\10.10.10.10",  
        'auto_check': ("auto_update", True, bool),  
        'allow_incremental': True,  
        'max_rollback_versions': 3,  
        'app_name': 'DebugTool',  
        'main_executable': '' if  == 'win32' else 'DebugTool'  
        }  

    def __getitem__(self, key):  
        return [key]  

    def set_auto_update(self, enabled):  
        ['auto_check'] = enabled  
        ("auto_update", enabled)  
  
  
class SharedFileDownloader:  
    """Downloader for downloading files from Windows shared folders"""  

    def __init__(self, config, parent=None):  
         = config  
         = parent  
        self.temp_dir = (("~"), "temp", f"{['app_name']}_updates")  
        (self.temp_dir, exist_ok=True)  

    def download_update(self, update_info, progress_callback=None):  
        """Download update package from shared folder"""  
        try:  
            if update_info['update_type'] == 'incremental':  
                # Incremental update                return self.download_incremental(update_info, progress_callback)  
            else:  
                # Full update                return self.download_full(update_info, progress_callback)  
        except Exception as e:  
            raise Exception(f"Failed to download from shared folder: {str(e)}")  

    def download_full(self, update_info, progress_callback):  
        """Download the full update package"""  
        local_file = (self.temp_dir, f"{update_info['version']}.zip")  

        self._copy_from_share(update_info['remote_file'], local_file, progress_callback)  
        return local_file  

    def download_incremental(self, update_info, progress_callback):  
        """Download incremental update package"""  
        incremental = update_info['incremental']  
        remote_file = f"incremental/{incremental['file']}"  
        local_file = (self.temp_dir, f"{update_info['version']}.zip")  

        self._copy_from_share(remote_file, local_file, progress_callback)  

        # Download the list of differences files        remote_manifest = f"incremental/{incremental['manifest']}"  
        local_manifest = (self.temp_dir, f"inc_{update_info['version']}.json")  
        self._copy_from_share(remote_manifest, local_manifest, None)  

        return local_file  

    def _copy_from_share(self, remote_path, local_path, progress_callback):  
        """Copy files from shared folders"""  
        # Construct a complete shared path        source_path = remote_path  

        # Make sure to use UNC path format        if not source_path.startswith('\\\\'):  
            source_path = '\\\\' + source_path.replace('/', '\\')  

        # Standardized paths        source_path = (source_path)  
        local_path = (local_path)  

        # Make sure the local directory exists        ((local_path), exist_ok=True)  

        if progress_callback:  
            progress_callback(0, f"Prepare to share folders {source_path} copy...")  
            ()  

        try:  
            # Get the file size for progress display            total_size = (source_path)  

            # Simulate progress update (file copying is an atomic operation)            if progress_callback:  
                progress_callback(30, "Copying files from shared folder...")  
                ()  

            # Execute file copying            shutil.copy2(source_path, local_path)  

            # Verify file            if (local_path) != total_size:  
                raise Exception("File size does not match, copying may be incomplete")  

            if progress_callback:  
                progress_callback(100, "File copy is complete!")  
        except Exception as e:  
        # Clean up files that may have been partially copied        if (local_path):  
            try:  
                (local_path)  
            except:  
                pass  
                raise e  
  
  
class UpdateApplier:  
"""Update the App"""  
  
def __init__(self, config, backup_dir):  
     = config  
    self.backup_dir = backup_dir  
    (self.backup_dir, exist_ok=True)  
  
def apply_update(self, update_file, update_info, progress_callback=None):  
    """App Update"""  
    try:  
        if progress_callback:  
            progress_callback(10, "Start apply update...")  

        if update_info['update_type'] == 'incremental':  
            self._apply_incremental_update(update_file, update_info)  
        else:  
            self._apply_full_update(update_file, update_info['root_dir'])  

        if progress_callback:  
            progress_callback(90, "Updated version information...")  

        self._update_version_file(update_info['version'], update_info['root_dir'])  

        if progress_callback:  
            progress_callback(100, "Update is complete!")  

        return True  
    except Exception as e:  
        if progress_callback:  
            progress_callback(0, f"Update failed: {str(e)}")  
            raise Exception(f"Update failed: {str(e)}")  
  
def _create_backup(self):  
    """Create a backup of the current version"""  
    timestamp = ().strftime("%Y%m%d_%H%M%S")  
    backup_path = (self.backup_dir, f"backup_{timestamp}")  
    (backup_path, exist_ok=True)  

    # Back up the entire application directory    app_dir = r'xxx'  
    for item in (app_dir):  
        src = (app_dir, item)  
        if (src):  
            shutil.copy2(src, (backup_path, item))  
        elif (src) and not ('_'):  
            (src, (backup_path, item))  

    return backup_path  
  
def _apply_full_update(self, update_file, root_dir):  
    """Apply full update"""  

    # Unzip the update package    with (update_file, 'r') as zip_ref:  
        zip_ref.extractall(root_dir)  

    # Unzip to temporary directory    temp_dir = ((update_file), "temp_full_update")  
    with (update_file, 'r') as zip_ref:  
        zip_ref.extractall(temp_dir)  

    # Delete the unzipped directory    (((update_file)))  
    (self.backup_dir)  

def _apply_incremental_update(self, update_file, update_info):  
    """Apply incremental update"""  
    root_dir = update_info['root_dir']  

    # parse the list of differential files    manifest_file = (  
    (update_file),  
    f"inc_{update_info['version']}.json"  
    )  

    with open(manifest_file, 'r') as f:  
        manifest = (f)  

    # Unzip incremental package    temp_dir = ((update_file), "temp_inc_update")  
    with (update_file, 'r') as zip_ref:  
        zip_ref.extractall(temp_dir)  

    # App update    for action in manifest['actions']:  
        src = (temp_dir, action['path'])  
        dst = (root_dir, action['path'])  

        if action['type'] == 'add' or action['type'] == 'modify':  
            ((dst), exist_ok=True)  
            if (dst):  
                (dst)  
            (src, dst)  
        elif action['type'] == 'delete':  
            if (dst):  
                if (dst):  
                    (dst)  
                else:  
                    (dst)  
  
def _update_version_file(self, new_version, root_dir):  
    """Update version number file"""  
    version_file = (root_dir, '')  
    with open(version_file, 'w') as f:  
        (new_version)  

def _rollback_update(self, backup_path):  
    """Rollback to backup version"""  
    if not (backup_path):  
        return False  

    app_dir = r'xxx'  

    # Restore backup    for item in (backup_path):  
        src = (backup_path, item)  
        dst = (app_dir, item)  

        if (dst):  
            if (dst):  
                (dst)  
            else:  
                (dst)  

        if (src):  
            shutil.copy2(src, dst)  
        else:  
            (src, dst)  

    return True  
  
  
class UpdateProgressDialog(QProgressDialog):  
    """Update Progress Dialog"""  
  
def __init__(self, parent=None):  
    super().__init__("", "Cancel", 0, 100, parent)  
    ("Updating")  
    ()  
    (500, 150)  

    self.detail_label = QLabel()  
    (self.detail_label)  
  
def update_progress(self, value, message):  
    (value)  
    self.detail_label.setText(message)  
    ()  
  
  
def update_main(root_dir: str, version: str, remote_file: str):  
    try:  
    config = UpdateConfig()  

    update_info = {  
        "version": version,  
        "update_type": "full",  
        "changelog": "Fixed some bugs",  
        'root_dir': root_dir,  
        'remote_file': remote_file  
    }  

    progress = UpdateProgressDialog()  
    downloader = SharedFileDownloader(config)  
    applier = UpdateApplier(config, (("~"), f"{config['app_name']}_backups"))  
    # Download update    update_file = downloader.download_update(  
        update_info,  
        progress.update_progress  
    )  

    # App update    if ():  
        (1)  

    success = applier.apply_update(  
        update_file,  
        update_info,  
        progress.update_progress  
    )  

    if success:  
        (  
        None,  
        "Update Completed",  
        "The update has been installed successfully. After clicking OK, the application will be restarted soon (within 10s)."  
        )  
        return True  
    return True  
except:  
    return False

5. Summary

This is the article about the implementation code of the automatic update of PyQt5 program. For more related content of automatic update of PyQt5 program, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!