如何为PyInstaller打包的单文件.exe应用实现更新功能
PyInstaller单文件exe应用的自动更新方案
我用Selenium开发了一款Python应用,通过PyInstaller打包成单文件.exe分发给用户。现在要添加新功能,但已安装的用户无法获取更新,请问怎么给用户提供更新?需要修改哪些代码来实现更新机制?
我用ChatGPT写了一段代码,但它只能把.py文件下载到应用目录,达不到预期的更新效果——比如原程序执行print("Hello World")输出Hello World,我希望更新后改成执行print("Hello New World")输出Hello New World。
尝试的代码
import requests import os import sys import zipfile import tempfile import shutil def check_for_updates(current_version, repo_owner, repo_name): releases_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest" print(releases_url) try: response = requests.get(releases_url) release_info = response.json() latest_version = release_info["tag_name"] return latest_version except requests.RequestException as e: print(f"Error checking for updates: {e}") return None def is_update_available(current_version, latest_version): return current_version < latest_version def download_update(repo_owner, repo_name): releases_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest" try: response = requests.get(releases_url) release_info = response.json() assets = release_info["assets"] # Assuming the first asset is a zip file update_url = assets[0]["browser_download_url"] print(update_url) with tempfile.TemporaryDirectory() as temp_dir: zip_path = os.path.join(temp_dir, "file1.zip") with open(zip_path, "wb") as zip_file: zip_file.write(requests.get(update_url).content) extract_path = os.path.join(temp_dir, "extracted_update") print("Extract Path: ", extract_path) with zipfile.ZipFile(zip_path, "r") as zip_ref: zip_ref.extractall(extract_path) # Replace old files with the updated ones update_files(extract_path) except requests.RequestException as e: print(f"Error downloading update: {e}") def update_files(extract_path): # Replace old files with the updated ones app_dir = os.path.dirname(os.path.realpath(sys.argv[0])) print(app_dir) for root, dirs, files in os.walk(extract_path): for file in files: src_path = os.path.join(root, file) relative_path = os.path.relpath(src_path, extract_path) dest_path = os.path.join(app_dir, relative_path) # Ensure the destination directory exists os.makedirs(os.path.dirname(dest_path), exist_ok=True) # Copy the file to the destination shutil.copy2(src_path, dest_path) def main(): current_version = "v0.1.0" latest_version = check_for_updates(current_version, "******************", "Updating") if latest_version and is_update_available(current_version, latest_version): print("Update available. Downloading...") download_update("******************", "Updating") print("Update complete. Please restart the application.") else: print("Your application is up to date.") if __name__ == "__main__": main()
问题分析
当前代码的核心缺陷是:你打包的是单文件exe,PyInstaller单文件模式会把所有代码、依赖打包成一个独立的exe,运行时会自动解压到系统临时目录执行,而非读取本地的.py文件。所以下载.py文件到应用目录完全不会影响已打包exe的运行逻辑。
解决方案:替换单文件exe的更新机制
单文件exe的更新本质是替换旧的exe文件,但要解决「运行中的exe无法被直接替换」的问题,以下是修改后的完整实现:
修改后的代码
import requests import os import sys import tempfile import subprocess import time def check_for_updates(current_version, repo_owner, repo_name): releases_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest" try: response = requests.get(releases_url) response.raise_for_status() release_info = response.json() latest_version = release_info["tag_name"] # 获取Release中的exe资产链接 exe_asset = next((asset for asset in release_info["assets"] if asset["name"].endswith(".exe")), None) return latest_version, exe_asset["browser_download_url"] if exe_asset else None except requests.RequestException as e: print(f"检查更新失败: {e}") return None, None def is_update_available(current_version, latest_version): # 修复版本号对比逻辑(避免v0.10.0被误认为小于v0.2.0) def parse_version(v): return tuple(map(int, v.lstrip('v').split('.'))) return parse_version(current_version) < parse_version(latest_version) def download_update(update_url): try: # 流式下载最新exe到临时文件 with tempfile.NamedTemporaryFile(delete=False, suffix=".exe") as temp_exe: response = requests.get(update_url, stream=True) response.raise_for_status() for chunk in response.iter_content(chunk_size=8192): temp_exe.write(chunk) return temp_exe.name except requests.RequestException as e: print(f"下载更新失败: {e}") return None def replace_and_restart(old_exe_path, new_exe_path): # 生成辅助替换脚本,解决exe文件占用问题 script_content = f""" import os import shutil import time import sys old_exe = r"{old_exe_path}" new_exe = r"{new_exe_path}" # 等待原程序完全退出 time.sleep(2) try: # 替换旧exe shutil.move(new_exe, old_exe) # 重启应用 subprocess.Popen([old_exe]) except Exception as e: print(f"更新替换失败: {e}") input("按回车键退出...") """ with tempfile.NamedTemporaryFile(delete=False, suffix=".py", encoding="utf-8") as script_file: script_file.write(script_content) # 启动替换脚本并退出原程序 subprocess.Popen([sys.executable, script_file.name]) sys.exit(0) def main(): current_version = "v0.1.0" repo_owner = "你的GitHub用户名" repo_name = "你的仓库名" latest_version, update_url = check_for_updates(current_version, repo_owner, repo_name) if latest_version and is_update_available(current_version, latest_version) and update_url: print(f"发现新版本 {latest_version},开始下载...") new_exe_path = download_update(update_url) if new_exe_path: print("下载完成,准备更新...") old_exe_path = os.path.realpath(sys.argv[0]) replace_and_restart(old_exe_path, new_exe_path) else: print("更新下载失败") else: print("当前已是最新版本") # 原应用核心逻辑(更新时修改此处代码后重新打包) print("Hello World") if __name__ == "__main__": main()
关键修改说明
- 目标文件改为exe:不再下载.py文件,直接下载GitHub Release中上传的最新exe资产
- 版本号对比优化:解析版本号为整数元组,避免字符串对比的逻辑错误
- 解决文件占用:通过辅助脚本等待原程序退出后再替换exe,最后重启应用
- 流式下载:大文件下载采用分块写入,降低内存占用
打包与发布流程
- 修改应用代码后,用PyInstaller重新打包成单文件exe:
pyinstaller --onefile your_script.py - 在GitHub仓库创建新Release,标记对应版本号(如v0.2.0),上传新打包的exe文件
- 用户启动旧版本exe时,程序会自动检测新版本、下载并替换旧exe,重启后即可运行更新后的逻辑
内容的提问来源于stack exchange,提问作者Panos Chatzileontiadis
相关产品推荐
相关产品推荐

