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

如何为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()

关键修改说明

  1. 目标文件改为exe:不再下载.py文件,直接下载GitHub Release中上传的最新exe资产
  2. 版本号对比优化:解析版本号为整数元组,避免字符串对比的逻辑错误
  3. 解决文件占用:通过辅助脚本等待原程序退出后再替换exe,最后重启应用
  4. 流式下载:大文件下载采用分块写入,降低内存占用

打包与发布流程

  1. 修改应用代码后,用PyInstaller重新打包成单文件exe:pyinstaller --onefile your_script.py
  2. 在GitHub仓库创建新Release,标记对应版本号(如v0.2.0),上传新打包的exe文件
  3. 用户启动旧版本exe时,程序会自动检测新版本、下载并替换旧exe,重启后即可运行更新后的逻辑

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 14:23:02