3

我的问题本质上是,“我应该如何构建我冻结的、部署的基于 Python 的 Windows 应用程序的文件和文件夹。” 为了了解我的情况,这里有一些背景:

我正在为我的工作场所使用 Python 2.7 构建一个桌面应用程序。它是基于 PyQt 构建的基于 GUI 的应用程序。我正在使用Esky构建应用程序,它是一个跨平台的冻结和更新框架。Esky 基本上包装/调用 py2exe、py2app、bb_freeze 或您安装的适合当前平台的任何工具。Esky 创建了一个如下所示的压缩包:

prog.exe                     - esky bootstrapping executable
appdata/                     - container for all the esky magic
  appname-X.Y.platform/      - specific version of the application
    prog.exe                 - executable(s) as produced by freezer module
    library.zip              - pure-python frozen modules
    pythonXY.dll             - python DLL
    esky-files/              - esky control files
      bootstrap/             - files not yet moved into bootstrapping env
      bootstrap-manifest.txt - list of files expected in bootstrap env
      lockfile.txt           - lockfile to block removal of in-use versions
      ...other deps...
  updates/                   - work area for fetching/unpacking updates

然后可以将这些压缩包放置在 Esky 查找更新的文件服务器上。提供了许多方法来管理更新,包括一个非常简单的 auto_update()。当发生更新时,appname-XYplatform 文件夹本质上会被下一个版本文件夹替换……所以 myApp.0.1.win32 文件夹被替换为 myApp.0.2.win32 文件夹。

您应该知道的背景的另一个方面是我正在将应用程序分发给我的同事,他们没有安装 Python。我不是在分发 Python 包或库,而是在部署一个桌面应用程序(我的同事并不特别关心它是用什么写的,只是它可以工作)。我已经构建了一个 Inno 安装程序,它可以安装应用程序、提供卸载程序和各种快捷方式。因为团队中的每个人基本上都拥有相同的 Windows 7 64 位环境,所以我只为那个平台构建是相当安全的。

所以,回到结构问题。我已经阅读了为项目框架推荐某种格式的指南,例如Learn Python the Hard Way、Exercise 46Hitchhiker's Guide to Packaging。然而,这些指南面向 Python 包开发人员,而不是编译应用程序开发人员。

我也遇到了 Esky 的 appname-XYplatform 文件夹的问题,因为每次更新程序时它都会更改名称(以反映版本号)。因为我希望开始菜单中的一些快捷方式始终引用文档、更改日志等,所以我让安装程序将其中一些文件放在 appdata 文件夹下。当程序更新时,我有一些代码来检查那些我想在外部“可见”的文件的较新版本,并将较新版本从 appname-XYplatform 文件夹中复制出来,并覆盖 appdata 文件夹中的副本。然后我还需要一种存储持久用户设置的方法,因此程序会生成并使用 appdata\settings 文件夹(否则每次更新都会擦除设置)。

我应该继续让应用程序在更新后将新文件推送到 appdata 文件夹吗?我是否应该构建自己的文档、示例、设置等结构,并让程序在必要时用更新的文件填充这些文件夹?我是否应该尝试改变或更好地利用 Esky 的行为以更好地适应我的使用?也许我应该将我的应用程序重新设计为可作为 Python 包和最终用户应用程序进行分发?

这个问题与关于 Esky 的静态文件的问题关于 Python 部署的应用程序结构的问题以及关于 Python 项目结构的许多通用问题有关,这些问题并未专门解决使用 Esky 的问题。一些讨论 Esky 的视频也可以在这里这里找到

我正在寻求有关应对这些挑战的“最佳实践”方法的建议。如果这不符合 StackOverflow 问题格式,我很乐意尝试改写或缩小问题的重点。

4

1 回答 1

2

因此,尽管事实上 Esky 的自动更新每次更新都会更改我的应用程序文件夹的名称,但这是我对提到的关于在静态位置使文件可用于快捷方式的问题的解决方案。下面的函数在 QMainWindow 的类定义中。

如果您的应用程序不使用日志记录模块,则可以将日志记录语句替换为打印语句,但我强烈建议使用日志记录,尤其是在部署这样的独立应用程序时。

import os
import shutil
import logging

def push_updated_files(self):
    """
    Manually push auto-updated files from the application folder up to the appdata folder
    This enables shortcuts and other features on the computer to point to these files since the application
      directory name changes with each update.
    """
    logger = logging.getLogger(__name__)

    #Verify whether running script or frozen/deployed application
    if getattr(sys, 'frozen', False):
        logger.info("Verifying Application Integrity...")

        #Files which should by copied to appdata directory to be easily referenced by shortcuts, etc.
        data_files = ['main.ico',
                      'uninstall.ico',
                      'ReadMe.txt',
                      'changelog.txt',
                      'WhatsNew.txt',
                      'copyright.txt',
                      'Documentation.pdf']

        logger.debug("  App Path: {0}".format(self._app_path))

        #Get application top directory
        logger.debug("  AppData Directory: {0}".format(self._appdata_path))

        #Get application internal file path
        for f in data_files:
            a_file = f
            int_path = os.path.join(self._app_path, a_file)
            logger.debug("  Internal File Path: {0}".format(int_path))

            #Get file's creation time
            mtime_int = os.stat(int_path).st_mtime
            logger.debug("  Internal File Modified Time: {0}".format(time.ctime(mtime_int)))

            #Get external file path
            ext_path = os.path.join(self._appdata_path, a_file)
            if os.path.exists(ext_path):
                mtime_ext = os.stat(ext_path).st_mtime
                logger.debug("  External File Modified Time: {0}".format(time.ctime(mtime_ext)))

                if mtime_int > mtime_ext:
                    logger.debug("  Replacing external file with new file...")
                    try:
                        os.remove(ext_path)
                        shutil.copy(int_path, ext_path)
                    except Exception, e:
                        logger.error("  Failed to replace the external file...", exc_info=True)
                else:
                    logger.debug("  External file is newer than internal file - all is well.")
            else:
                logger.debug("  Copying file to appdata to be externally accessible")
                shutil.copy(int_path, ext_path)

同样与此相关的是,在处理用户设置(当前只是一个用于填充最近文件列表的 history.txt 文件)时,我在 appdata 下但在应用程序文件夹之外有一个设置文件夹,这样设置就不会在每次更新时丢失。我可能会为文档和图标制作类似的文件夹。

于 2013-12-20T21:31:26.093 回答