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!