Объединение файлов данных с помощью PyInstaller (--onefile)

Я пытаюсь создать один файл EXE с PyInstaller, который должен включать изображение и значок. Я не могу на всю жизнь заставить его работать с --onefile,

Если я сделаю --onedir это работает все работает очень хорошо. Когда я использую --onefile, он не может найти указанные дополнительные файлы (при запуске скомпилированного EXE). Он находит библиотеки DLL и все остальное в порядке, но не два изображения.

Я посмотрел в temp-dir, сгенерированном при запуске EXE (\Temp\_MEI95642\ например) и файлы там действительно есть. Когда я сбрасываю EXE в этот временный каталог, он находит их. Очень сбивает с толку.

Это то, что я добавил к .spec файл

a.datas += [('images/icon.ico', 'D:\\[workspace]\\App\\src\\images\\icon.ico',  'DATA'),
('images/loaderani.gif','D:\\[workspace]\\App\\src\\images\\loaderani.gif','DATA')]     

Я должен добавить, что я старался не помещать их в подпапки, но это не имело значения.

Изменить: новый ответ помечен как правильный из-за обновления PyInstaller.

15 ответов

Решение

Более новые версии PyInstaller не устанавливают env переменная больше, так что отличный ответ Шиш не будет работать. Теперь путь устанавливается как sys._MEIPASS:

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

pyinstaller распаковывает ваши данные во временную папку и сохраняет этот путь к каталогу в _MEIPASS2 переменная окружения. Чтобы получить _MEIPASS2 DIR в упакованном режиме и использовать локальный каталог в распакованном режиме (разработки), я использую это:

def resource_path(relative):
    return os.path.join(
        os.environ.get(
            "_MEIPASS2",
            os.path.abspath(".")
        ),
        relative
    )

Выход:

# in development
>>> resource_path("app_icon.ico")
"/home/shish/src/my_app/app_icon.ico"

# in production
>>> resource_path("app_icon.ico")
"/tmp/_MEI34121/app_icon.ico"

Все остальные ответы используют текущий рабочий каталог в случае, когда приложение не установлено PyInstalled (т.е. sys._MEIPASS не установлено). Это неправильно, так как не позволяет запускать ваше приложение из каталога, отличного от того, в котором находится ваш скрипт.

Лучшее решение:

import sys
import os

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)

Давно (ну очень давно) занимаюсь этим вопросом. Я искал почти все источники, но все не складывалось в моей голове.

Наконец, я думаю, что выяснил точные шаги, которым нужно следовать, и хотел бы поделиться.

Обратите внимание, что в моем ответе используется информация об ответах других на этот вопрос.

Как создать автономный исполняемый файл проекта Python.

Предположим, у нас есть project_folder, а дерево файлов выглядит следующим образом:

project_folder/
    main.py
    xxx.py # modules
    xxx.py # modules
    sound/ # directory containing the sound files
    img/ # directory containing the image files
    venv/ # if using a venv

Прежде всего, предположим, что вы определили свой путь к sound/ а также img/ папки в переменные sound_dir а также img_dir следующее:

img_dir = os.path.join(os.path.dirname(__file__), "img")
sound_dir = os.path.join(os.path.dirname(__file__), "sound")

Вы должны изменить их следующим образом:

img_dir = resource_path("img")
sound_dir = resource_path("sound")

Где, resource_path() определяется в верхней части вашего скрипта как:

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)

Активируйте виртуальный env при использовании venv,

Установите pyinstaller, если вы еще этого не сделали: pip3 install pyinstaller.

Пробег: pyi-makespec --onefile main.py чтобы создать файл спецификации для процесса компиляции и сборки.

Это изменит иерархию файлов на:

project_folder/
    main.py
    xxx.py # modules
    xxx.py # modules
    sound/ # directory containing the sound files
    img/ # directory containing the image files
    venv/ # if using a venv
    main.spec

Открытый (с редактором) main.spec:

Вверху вставьте:

added_files = [

("sound", "sound"),
("img", "img")

]

Затем измените строку datas=[], к datas=added_files,

Подробнее об операциях, выполненных на main.specсм. здесь.

Пробег pyinstaller --onefile main.spec

И это все, ты можешь бежать main в project_folder/distоткуда угодно, не имея ничего другого в своей папке. Вы можете распространять только тоmainфайл. Теперь это настоящая автономная версия.

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

  1. шаг: запишите один из вышеперечисленных методов в ваш файл py с импортом модулей sys и os. Я попробовал их обоих. Последний из них:

    def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
        base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
        return os.path.join(base_path, relative_path)
    
  2. шаг: напишите pyi-makepec file.py в консоль, чтобы создать файл file.spec.

  3. шаг: Откройте файл file.spec с помощью Notepad++, чтобы добавить файлы данных, как показано ниже:

    a = Analysis(['C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.py'],
                 pathex=['C:\\Users\\TCK\\Desktop\\Projeler'],
                 binaries=[],
                 datas=[],
                 hiddenimports=[],
                 hookspath=[],
                 runtime_hooks=[],
                 excludes=[],
                 win_no_prefer_redirects=False,
                 win_private_assemblies=False,
                 cipher=block_cipher)
    #Add the file like the below example
    a.datas += [('Converter-GUI.ico', 'C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.ico', 'DATA')]
    pyz = PYZ(a.pure, a.zipped_data,
         cipher=block_cipher)
    exe = EXE(pyz,
              a.scripts,
              exclude_binaries=True,
              name='Converter-GUI',
              debug=False,
              strip=False,
              upx=True,
              #Turn the console option False if you don't want to see the console while executing the program.
              console=False,
              #Add an icon to the program.
              icon='C:\\Users\\TCK\\Desktop\\Projeler\\Converter-GUI.ico')
    
    coll = COLLECT(exe,
                   a.binaries,
                   a.zipfiles,
                   a.datas,
                   strip=False,
                   upx=True,
                   name='Converter-GUI')
    
  4. шаг: я следовал за вышеупомянутыми шагами, затем сохранил файл спецификации. Наконец-то открыли консоль и напишем, pyinstaller file.spec (в моем случае file=Converter-GUI).

Вывод: в папке dist еще несколько файлов.

Примечание: я использую Python 3.5.

РЕДАКТИРОВАТЬ: Наконец, это работает с методом Джонатана Рейнхарта.

  1. шаг: добавьте приведенные ниже коды в ваш файл python с импортом sys и os.

    def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
        base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
        return os.path.join(base_path, relative_path)
    
  2. шаг: вызов вышеупомянутой функции с добавлением пути вашего файла:

    image_path = resource_path("Converter-GUI.ico")
    
  3. шаг: напишите вышеупомянутую переменную, которая вызывает функцию, где ваши коды нуждаются в пути. В моем случае это:

        self.window.iconbitmap(image_path)
    
  4. шаг: откройте консоль в том же каталоге вашего файла Python, напишите коды, как показано ниже:

        pyinstaller --onefile your_file.py
    
  5. шаг: откройте файл.spec файла python, добавьте массив a.datas и добавьте значок в класс exe, который был указан выше до редактирования на третьем шаге.
  6. шаг: сохранить и выйти из файла пути. Перейдите в вашу папку, в которой есть файл спецификации и py. Снова откройте окно консоли и введите следующую команду:

        pyinstaller your_file.spec
    

После 6. шага ваш единственный файл готов к использованию.

Используя отличный ответ от Макса и этот пост о добавлении дополнительных файлов данных, таких как изображения или звук, а также мое собственное исследование / тестирование, я выяснил, что, по моему мнению, является самым простым способом добавления таких файлов.

Если вы хотите увидеть живой пример, мой репозиторий находится здесь, на GitHub.

Примечание: это для компиляции с использованием --onefile или -F команда с pyinstaller.

Моя среда выглядит следующим образом.


Решение проблемы за 2 шага

Чтобы решить эту проблему, нам нужно специально сообщить Pyinstaller, что у нас есть дополнительные файлы, которые необходимо "связать" с приложением.

Нам также необходимо использовать "относительный" путь, чтобы приложение могло работать правильно, когда оно выполняется как скрипт Python или замороженный EXE.

С учетом сказанного нам нужна функция, которая позволяет нам иметь относительные пути. Используя функцию, которую опубликовал Макс, мы можем легко решить относительный путь.

def img_resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    try:
        # PyInstaller creates a temp folder and stores path in _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")

    return os.path.join(base_path, relative_path)

Мы бы использовали указанную выше функцию таким образом, чтобы значок приложения отображался, когда приложение работает как скрипт ИЛИ замороженный EXE.

icon_path = img_resource_path("app/img/app_icon.ico")
root.wm_iconbitmap(icon_path)

Следующим шагом является то, что нам нужно указать Pyinstaller, где найти дополнительные файлы при его компиляции, чтобы при запуске приложения они создавались во временном каталоге.

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

Во-первых, у вас уже должен быть файл.spec. В моем случае я смог создать то, что мне нужно, запустивpyinstallerс дополнительными аргументами вы можете найти дополнительные аргументы здесь. Из-за этого мой файл спецификаций может немного отличаться от вашего, но я публикую его для справки после того, как объясню важные моменты.

added_files - это, по сути, список, содержащий кортежи, в моем случае я хочу добавить только ОДНО изображение, но вы можете добавить несколько ico, png или jpg, используя('app/img/*.ico', 'app/img') Вы также можете создать другой кортеж, напримерadded_files = [ (), (), ()] иметь множественный импорт

Первая часть кортежа определяет, какой файл или какой тип файла вы хотите добавить, а также где их найти. Думайте об этом как о CTRL+C

Вторая часть кортежа сообщает Pyinstaller, что нужно сделать путь app/img/ и поместить файлы в этот каталог ОТНОСИТЕЛЬНО к любому временному каталогу, который создается при запуске.exe. Думайте об этом как о CTRL+V

Под a = Analysis([main..., Я установил datas=added_files, изначально это было datas=[] но мы хотим, чтобы список импорта был импортирован, поэтому мы передаем наши пользовательские импорты.

Вам не нужно этого делать, если вам не нужен конкретный значок для EXE, в нижней части файла спецификации я говорю Pyinstaller, чтобы установить значок моего приложения для exe с опцией icon='app\\img\\app_icon.ico'.

added_files = [
    ('app/img/app_icon.ico','app/img/')
]
a = Analysis(['main.py'],
             pathex=['D:\\Github Repos\\Processes-Killer\\Process Killer'],
             binaries=[],
             datas=added_files,
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='Process Killer',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=True , uac_admin=True, icon='app\\img\\app_icon.ico')

Компиляция в EXE

Я очень ленив; Я не люблю печатать больше, чем должен. Я создал файл.bat, по которому могу просто щелкнуть. Вам не обязательно этого делать, этот код будет отлично работать и без него в командной строке.

Поскольку файл.spec содержит все наши параметры компиляции и аргументы (также известные как параметры), нам просто нужно передать этот файл.spec в Pyinstaller.

pyinstaller.exe "Process Killer.spec"

Вместо того, чтобы переписать весь мой путь, как предложено, я изменил рабочий каталог:

if getattr(sys, 'frozen', False):
    os.chdir(sys._MEIPASS)

Просто добавьте эти две строки в начале вашего кода, а остальные оставьте как есть.

Небольшое изменение принятого ответа.

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative_path)

    return os.path.join(os.path.abspath("."), relative_path)

Другое решение - создать обработчик времени выполнения, который будет копировать (или перемещать) ваши данные (файлы / папки) в каталог, в котором хранится исполняемый файл. Хук - это простой файл Python, который может делать почти все, непосредственно перед запуском вашего приложения. Чтобы установить его, вы должны использовать--runtime-hook=my_hook.pyвариант pyinstaller. Итак, если ваши данные представляют собой папку с изображениями, вы должны выполнить команду:

pyinstaller.py --onefile -F --add-data=images;images --runtime-hook=cp_images_hook.py main.py

Cp_images_hook.py может быть примерно таким:

import sys
import os
import shutil

path = getattr(sys, '_MEIPASS', os.getcwd())

full_path = path+"\\images"
try:
    shutil.move(full_path, ".\\images")
except:
    print("Cannot create 'images' folder. Already exists.")

Перед каждым выполнением папка изображений перемещается в текущий каталог (из папки _MEIPASS), поэтому исполняемый файл всегда будет иметь к нему доступ. Таким образом, нет необходимости изменять код вашего проекта.

Второе решение

Вы можете воспользоваться механизмом runtime-hook и изменить текущий каталог, что, по мнению некоторых разработчиков, не является хорошей практикой, но работает нормально.

Код перехвата можно найти ниже:

import sys
import os

path = getattr(sys, '_MEIPASS', os.getcwd())   
os.chdir(path)

Я использую это на основе максимального решения

def getPath(filename):
    import os
    import sys
    from os import chdir
    from os.path import join
    from os.path import dirname
    from os import environ
    
    if hasattr(sys, '_MEIPASS'):
        # PyInstaller >= 1.6
        chdir(sys._MEIPASS)
        filename = join(sys._MEIPASS, filename)
    elif '_MEIPASS2' in environ:
        # PyInstaller < 1.6 (tested on 1.5 only)
        chdir(environ['_MEIPASS2'])
        filename = join(environ['_MEIPASS2'], filename)
    else:
        chdir(dirname(sys.argv[0]))
        filename = join(dirname(sys.argv[0]), filename)
        
    return filename

В КОНЦЕ КОНЦОВ ! на основе решения Луки

Мне удалось окончательно выполнить файлы внутри включенного каталога в --onefile pyinstaller.

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

Второе, что вам нужно, это добавить каталог/файлы, которые вы хотите, внутри pyinstaller. Просто используйте такие аргументы, как:

      --add-data "yourDir/And/OrFile;yourDir"

Для тех, кто все еще ищет более свежий ответ, вот вам:

В документации есть раздел о доступе к добавленным файлам данных .
Вот его краткий и сладкий вид.


Ты захочешь import pkgutilи найдите, в какую папку вы добавили файл данных; то есть вторая строка в кортеже, который был добавлен в файл спецификации:

      datas = [("path/to/mypackage/data_file.txt", "path/to/mypackage")]

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

Файловая структура:

      mypackage
      __init__.py  # This is a MUST in order for the package to be registered
      data_file.txt  # The data file you've added

data_file.txt

      Hello world!

main.py

      import pkgutil

file = pkgutil.get_data("mypackage", "data_file.txt")
contents = file.decode("utf-8")
print(contents)  # Hello world!

Рекомендации:

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

/questions/47445971/dobavit-konfiguratsionnyij-fajl-vne-pyinstaller-onefile-exe-v-katalog-dist/55273639#55273639

Вы добавляете в файл спецификации шаг, который копирует файловую систему в переменную DISTPATH.

Надеюсь, это поможет.

Добавьте этот фрагмент кода, чтобы увидеть, что включено в ваш файл, используя @Jonathon Reinhart's resource_path()

for root, dirs, files in os.walk(resource_path("")):
    print(root)
    for file in files:
        print( "  ",file)

Я нашел, что существующие ответы сбивают с толку, и потребовалось много времени, чтобы понять, в чем проблема. Вот подборка всего, что я нашел.

Когда я запускаю свое приложение, я получаю сообщение об ошибке Failed to execute script foo (если foo.py это основной файл). Чтобы устранить это, не запускайте PyInstaller с --noconsole (или редактировать main.spec изменить console=False => console=True). При этом запустите исполняемый файл из командной строки, и вы увидите ошибку.

Первое, что нужно проверить, это правильно ли упаковывать ваши дополнительные файлы. Вы должны добавить кортежи как ('x', 'x') если вы хотите папку x быть включенным.

После сбоя не нажимайте кнопку ОК. Если вы работаете в Windows, вы можете использовать Поиск по всему. Найдите один из ваших файлов (например, sword.png). Вы должны найти временный путь, по которому распакованы файлы (например. C:\Users\ashes999\AppData\Local\Temp\_MEI157682\images\sword.png). Вы можете просмотреть этот каталог и убедиться, что он включает в себя все. Если вы не можете найти его таким образом, ищите что-то вроде main.exe.manifest (Windows) или python35.dll (если вы используете Python 3.5).

Если установщик включает все, следующая вероятная проблема - это файловый ввод / вывод: ваш код Python ищет файлы в каталоге исполняемого файла, а не во временном каталоге.

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

if hasattr(sys, '_MEIPASS'): os.chdir(sys._MEIPASS)

Другие вопросы по тегам