Как настроить относительные пути для создания переносимой сборки .exe в PyInstaller с Python 3? - PullRequest
0 голосов
/ 30 марта 2020

Я посмотрел везде и не получил однозначного ответа на довольно тривиальный вопрос.

У меня есть проект Python в PyCharm Windows 7, который содержит несколько файлов .py (которые связаны через "from %package_name%.%script_name% import %class_name%") и папку внутри проекта с двумя простыми текстовыми файлами. Я установил PyInstaller 3.6 в venv проекта и использую его как внешний инструмент, который указывает на файл .spec. Все идет нормально. Файл .spec выглядит следующим образом:

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['C:\\Users\\%username%\\PycharmProjects\\%project_folder%\\%project_folder%\\main.py'],
             pathex=['C:\\Users\\%username%\\PycharmProjects\\%project_folder%\\%project_folder%'],
             binaries=[],
             datas=[('txt_files\\file1.txt', '.'), ('txt_files\\file2.txt', '.')],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
a.datas += [
    ("C:\\Users\\%username%\\PycharmProjects\\%project_folder%\\%project_folder%\\txt_files\\file1.txt","txt_files\\file1.txt","DATA"),
    ("C:\\Users\\%username%\\PycharmProjects\\%project_folder%\\%project_folder%\\txt_files\\file2.txt","txt_files\\file2.txt","DATA"),
]
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          [],
          exclude_binaries=True,
          name='%project_name%',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=True )
coll = COLLECT(exe,
               a.binaries,
               a.zipfiles,
               a.datas,
               strip=False,
               upx=True,
               upx_exclude=[],
               name='%project_name%')

Проблема заключается в том, что если я жестко закодирую абсолютные пути к связанным файлам .txt в самих сценариях, приложение компилируется и не имеет ошибок во время выполнения. Однако, если я использую относительные пути внутри скриптов, приложение компилируется, но выдает ошибку времени выполнения, что файл .txt (т.е. file1.txt) не найден ВНУТРИ /build (или /dist, я может быть неправильно) каталог (который, очевидно, не существует).

Конечно, жесткое кодирование абсолютных путей - плохая практика, особенно когда речь идет не только о переносимости на другую машину, но и о том, что приложение кроссплатформенное. Я знаю, что процесс сборки может зависеть от sys._MEIPASS, но я не знаю точно, как использовать его в моем контексте.

В каком скрипте (main, .spec или другом?) Я должен поместить часть, которая получает абсолютный путь к пакетному файлу, используя sys._MEIPASS? И как должна выглядеть эта часть кода на Python 3.7? Я видел разные ответы (например, этот ) и уже пробовал их, но ни один из них не помог в моем случае.

1 Ответ

1 голос
/ 31 марта 2020

Использование --onefile объединяет все данные вместе в файл .exe.

При запуске файла эти файлы «распаковываются» во временную папку. На Windows это обычно C:\Users\<You>\AppData\Local\Temp\MEIxxx.

Итак, когда вы разрабатываете свой скрипт, файлы данных (ваши текстовые файлы в этом примере) будут расположены по адресу

C:\\Users\\%username%\\PycharmProjects\\%project_folder%\\%project_folder%\txt_files\

но при компиляции приложения они будут извлечены во временный каталог, упомянутый выше. Таким образом, вам нужен способ сообщить сценарию, разрабатываете ли вы его или он был скомпилирован. Здесь вы можете использовать флаг 'замороженного' (см. Документы здесь)

Подход, который я использовал ранее, заключается в создании вспомогательной функции, подобной этой

def resolve_path(path):
    if getattr(sys, "frozen", False):
        # If the 'frozen' flag is set, we are in bundled-app mode!
        resolved_path = os.path.abspath(os.path.join(sys._MEIPASS, path))
    else:
        # Normal development mode. Use os.getcwd() or __file__ as appropriate in your case...
        resolved_path = os.path.abspath(os.path.join(os.getcwd(), path))

    return resolved_path

Затем, когда вы захотите использовать путь в вашем скрипте, например, для доступа к вашим текстовым файлам, вы можете сделать

with open(resolve_path("txt_files/file1.txt"), "r") as txt:
    ...

, который должен определить правильный путь, в каком бы режиме вы ни находились.

Примечание к вашему .spe c файлу

  1. Вам не нужно указывать все текстовые файлы по отдельности. Вы можете, конечно, и у вас может быть веская причина для этого, и это хорошо. Но вы можете сделать

datas=[('txt_files', '.')]

, который поместит содержимое каталога txt_files в root вашего пакета. Однако будьте осторожны с этим, поскольку теперь пути к вашим текстовым файлам будут <dev directory>\txt_files\file1.txt, но в прилагаемом приложении они будут <MEIPASS directory>\file1.txt. Возможно, вы захотите сохранить «относительную» часть пути тем же, выполнив

datas=[('txt_files', 'txt_files')]

, который будет отражать файловую структуру между вашей папкой разработки и приложенным приложением.

Также подумайте, что если вы строите с файлом spe c, удалите часть COLLECT, чтобы получить исполняемый файл в комплекте с одним файлом.
...