Agrupar archivos de datos con PyInstaller (--onefile)

107

Estoy tratando de crear un archivo EXE de un solo archivo con PyInstaller que debe incluir una imagen y un icono. No puedo por mi vida hacer que funcione --onefile.

Si lo hago --onedir, todo funciona muy bien. Cuando lo uso --onefile, no puede encontrar los archivos adicionales a los que se hace referencia (al ejecutar el EXE compilado). Encuentra las DLL y todo lo demás bien, pero no las dos imágenes.

He mirado en el directorio temporal generado al ejecutar el EXE ( \Temp\_MEI95642\por ejemplo) y los archivos están ahí. Cuando coloco el EXE en ese directorio temporal, los encuentra. Muy desconcertante.

Esto es lo que agregué al .specarchivo

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

Debo agregar que he intentado no ponerlos en subcarpetas también, no marcó la diferencia.

Editar: la respuesta más reciente marcada como correcta debido a la actualización de PyInstaller.

tiburón arborícola
fuente
11
¡Muchas gracias! la línea aquí ( a.datas += ...) realmente me ayudó en este momento. la documentación de pyinstaller habla sobre el uso, COLLECTpero eso no logra poner archivos en el binario cuando se usa--onefile
Igor Serebryany
@IgorSerebryany: ¡Secundado! Simplemente tuve exactamente el mismo problema.
Hubro
Mi .exe se bloquea cuando hago clic en la barra de menú si lo usé
iacopo
1
Tenga en cuenta que la carpeta temporal desaparece cuando finaliza la ejecución, por lo tanto, para verificar qué hay dentro, coloque un directorio de lista de sys._MEIPASS en la principal de
2013
¿Hay también una forma de usar la sintaxis Tree () que parece haber visto en el lugar?
fatuhoku

Respuestas:

152

Las versiones más nuevas de PyInstaller ya no establecen la envvariable, por lo que la excelente respuesta de Shish no funcionará. Ahora la ruta se establece como 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)
max
fuente
10
Estoy usando pyinstaller 2 con python 2.7 y no tengo _MEIPASS2envs, pero sys._MEIPASSfunciona bien, así que +1. Sugiero:path = getattr(sys, '_MEIPASS', os.getcwd())
kbec
2
+1. Gracias por eso. Intentando usar la variable de entorno _MEIPASS2 por un tiempo, evidentemente escribí mi código cuando todavía era la variable de entorno, porque recuerdo que funcionaba. De repente, se detuvo, cuando volví a compilar recientemente.
Favil Orbedios
8
Recomendaría capturar el en AttributeErrorlugar del más general Exception. Me encontré con problemas en los que me faltaba la importación del sistema y la ruta se predeterminaba silenciosamente os.path.abspath.
Aaron
Es posible que pueda ayudarme a resolver mi problema .
Phillip
1
Usar el directorio de trabajo actual en el caso de que _MEIPASS no esté configurado es incorrecto. Mira mi respuesta .
Jonathon Reinhart
51

pyinstaller descomprime sus datos en una carpeta temporal y almacena esta ruta de directorio en la _MEIPASS2variable de entorno. Para obtener el _MEIPASS2directorio en modo empaquetado y usar el directorio local en modo desempaquetado (desarrollo), uso esto:

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

Salida:

# 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"
Shish
fuente
10
¿Cómo, cuándo y dónde se usaría eso?
gseattle
4
Debe usar este script en el archivo .py que está intentando compilar con PyInstaller. No coloque este fragmento de código en el archivo .spec, eso no funcionará. Acceda a sus archivos sustituyendo la ruta por la que normalmente escribiría resource_path("file_to_be_accessed.mp3"). Tenga cuidado de que debería usar max 'answer para la versión actual de PyInstaller.
Exeleration-G
1
¿Esta respuesta es específica para usar la --one-fileopción?
fatuhoku
Es posible que pueda ayudarme a resolver mi problema .
Phillip
Usar el directorio de trabajo actual en el caso de que _MEIPASS no esté configurado es incorrecto. Mira mi respuesta .
Jonathon Reinhart
30

Todas las otras respuestas usan el directorio de trabajo actual en el caso de que la aplicación no esté PyInstalled ( sys._MEIPASSes decir, no esté configurada). Eso es incorrecto, ya que le impide ejecutar su aplicación desde un directorio que no sea aquel donde está su script.

Una mejor solucion:

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)
Jonathon Reinhart
fuente
Gracias Jon, tu función me ayudó hoy a resolver un problema. Solo tengo una pregunta. ¿Por qué algunos escriben _MEIPASS2 en lugar de _MEIPASS? Cuando intenté agregar el 2 antes, no funcionó.
Ahmed AbdelKhalek
Creo que os.env['_MEIPASS2'](usar el entorno del sistema operativo) era un método antiguo para obtener el directorio. AFAIK sys._MEIPASSes el único método correcto ahora.
Jonathon Reinhart
Hola, tu solución funciona bien cuando resource_path()está en el script principal. ¿Hay alguna forma de hacer que funcione cuando está escrito en un módulo? A partir de ahora, intentará obtener recursos en la carpeta donde está el módulo, no en la principal.
Guimoute
1
@Guimoute parece que el código en esta respuesta funciona según lo diseñado: localiza los recursos en el módulo que lo proporciona porque usa __file__. Si desea que un módulo pueda acceder a recursos fuera de su propio alcance, tendrá que implementar que el código de importación proporcione una ruta para que su módulo lo use, por ejemplo, mediante el módulo que proporciona una llamada "set_resource_location ()" que el llamadas de importador: no crea que esto sea diferente de cómo debería trabajar cuando no usa pyinstaller.
barny
@barny ¡Gracias por la pista! Intentaré algo en ese sentido.
Guimoute
14

Quizás me perdí un paso o hice algo mal, pero los métodos anteriores no agruparon archivos de datos con PyInstaller en un archivo exe. Permítanme compartirles los pasos que he realizado.

  1. paso: Escriba uno de los métodos anteriores en su archivo py con la importación de módulos sys y os. Probé los dos. El ultimo es:

    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. paso: Escriba, pyi-makespec file.py , en la consola, para crear un archivo file.spec.

  3. paso: Abra, file.spec con Notepad ++ para agregar los archivos de datos como se muestra a continuación:

    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. paso: Seguí los pasos anteriores y luego guardé el archivo de especificaciones. Por fin abrí la consola y escribí, pyinstaller file.spec (en mi caso, file = Converter-GUI).

Conclusión: todavía hay más de un archivo en la carpeta dist.

Nota: estoy usando Python 3.5.

EDITAR: Finalmente funciona con el método de Jonathan Reinhart.

  1. paso: agregue los siguientes códigos a su archivo de Python con la importación de sys y 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. paso: Llame a la función anterior agregando la ruta de su archivo:

    image_path = resource_path("Converter-GUI.ico")
  3. paso: Escriba la variable anterior que llama a la función donde sus códigos necesitan la ruta. En mi caso es:

        self.window.iconbitmap(image_path)
  4. paso: Abra la consola en el mismo directorio de su archivo python, escriba los códigos como se muestra a continuación:

        pyinstaller --onefile your_file.py
  5. paso: Abra el archivo .spec del archivo python y agregue la matriz a.datas y agregue el ícono a la clase exe, que se proporcionó anteriormente antes de la edición en el tercer paso.
  6. paso: guarde y salga del archivo de ruta. Vaya a su carpeta que incluye el archivo de especificaciones y py. Abra de nuevo la ventana de la consola y escriba el siguiente comando:

        pyinstaller your_file.spec

Después del 6. paso, su archivo está listo para usar.

dildeolupbiten
fuente
¡Gracias @dilde! No es que la a.datasmatriz del paso 5 tome tuplas de cadenas, consulte pythonhosted.org/PyInstaller/spec-files.html#adding-data-files como referencia.
Ian Campbell
Lo siento, no puedo entender una parte de tu mensaje tan bien como quisiera debido a mi inglés débil. Esa parte es "No es que los a.datas ...." , ¿Querías escribir 'Tenga en cuenta que los a.datas ... " ¿Hay algún problema con lo que escribí acerca de agregar cadenas a una matriz de a.datas?
dildeolupbiten
¡Ups! Debe tener en cuenta que ... sí , perdón por el error tipográfico. Tus pasos funcionaron muy bien para mí :), no pude encontrar esta información en su documentación ni en ningún otro lugar.
Ian Campbell
8

En lugar de reescribir todo mi código de ruta como se sugirió, cambié el directorio de trabajo:

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

Simplemente agregue esas dos líneas al comienzo de su código, puede dejar el resto como está.

Bobsleigh
fuente
2
No. Las buenas aplicaciones rara vez deberían cambiar el directorio de trabajo. Hay mejores formas.
Jonathon Reinhart
3

Ligera modificación de la respuesta aceptada.

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)
Krishna Balan
fuente
Usar el directorio de trabajo actual en el caso de que _MEIPASS no esté configurado es incorrecto. Mira mi respuesta .
Jonathon Reinhart
1

La queja / pregunta más común que he visto en PyInstaller es "mi código no puede encontrar un archivo de datos que definitivamente incluí en el paquete, ¿dónde está?", Y no es fácil ver qué / dónde su código está buscando porque el código extraído está en una ubicación temporal y se elimina cuando sale. Agregue este fragmento de código para ver qué se incluye en su archivo único y dónde está, usando @Jonathon Reinhart'sresource_path()

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

Encontré las respuestas existentes confusas y me tomó mucho tiempo averiguar dónde está el problema. Aquí hay una recopilación de todo lo que encontré.

Cuando ejecuto mi aplicación, aparece un error Failed to execute script foo(si foo.pyes el archivo principal). Para solucionar este problema, no ejecute PyInstaller con --noconsole(o edite main.specpara cambiar console=False=> console=True). Con esto, ejecute el ejecutable desde una línea de comandos y verá el error.

Lo primero que debe verificar es que está empaquetando correctamente sus archivos adicionales. Debe agregar tuplas como ('x', 'x')si desea xque se incluya la carpeta .

Después de que se bloquee, no haga clic en Aceptar. Si está en Windows, puede usar Buscar todo . Busque uno de sus archivos (p. Ej. sword.png). Debería encontrar la ruta temporal donde descomprimió los archivos (por ejemplo C:\Users\ashes999\AppData\Local\Temp\_MEI157682\images\sword.png). Puede navegar por este directorio y asegurarse de que incluye todo. Si no puede encontrarlo de esta manera, busque algo como main.exe.manifest(Windows) o python35.dll(si está usando Python 3.5).

Si el instalador incluye todo, el siguiente problema probable es la E / S de archivos: su código Python busca archivos en el directorio del ejecutable, en lugar del directorio temporal.

Para solucionarlo, cualquiera de las respuestas a esta pregunta funciona. Personalmente, encontré una combinación de todos ellos para funcionar: cambie el directorio condicionalmente primero en su archivo de punto de entrada principal, y todo lo demás funciona como está:

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

cenizas999
fuente
1
Lo siento. Pero cambiar de directorio nunca es la respuesta correcta. Si un usuario proporciona una ruta a la aplicación, espera que sea relativa al directorio actual. Si cambia eso, estará mal.
Jonathon Reinhart
0

Si todavía está intentando colocar archivos relacionados con su ejecutable en lugar de en el directorio temporal, debe copiarlo usted mismo. Así es como terminé haciéndolo.

https://stackoverflow.com/a/59415662/999943

Agrega un paso en el archivo de especificaciones que hace una copia del sistema de archivos a la variable DISTPATH.

Espero que ayude.

phyatt
fuente
0

He estado tratando con este problema desde hace mucho (bueno, muy largo tiempo). He buscado en casi todas las fuentes, pero las cosas no estaban siguiendo un patrón en mi cabeza.

Finalmente, creo que he descubierto los pasos exactos a seguir, quería compartirlos.

Tenga en cuenta que mi respuesta utiliza información sobre las respuestas de otros en esta pregunta.

Cómo crear un ejecutable independiente de un proyecto de Python.

Supongamos que tenemos un project_folder y el árbol de archivos es el siguiente:

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

En primer lugar, digamos que ha definido sus rutas sound/y img/carpetas en variables sound_diry de la img_dirsiguiente manera:

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

Tienes que cambiarlos, de la siguiente manera:

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

Donde, resource_path()se define en la parte superior de su secuencia de comandos como:

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)

Active env virtual si usa un venv,

Instalar PyInstaller si lo hizo, no todavía, por: pip3 install pyinstaller.

Ejecutar: pyi-makespec --onefile main.pypara crear el archivo de especificaciones para el proceso de compilación y construcción.

Esto cambiará la jerarquía de archivos a:

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

Abierto (con un edior) main.spec:

En la parte superior, inserte:

added_files = [

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

]

Luego, cambie la línea de datas=[],adatas=added_files,

Para conocer los detalles de las operaciones realizadas, main.specconsulte aquí.

correr pyinstaller --onefile main.spec

Y eso es todo, puede ejecutar mainen project_folder/distdesde cualquier lugar, sin tener otra cosa en su carpeta. Solo puede distribuir ese mainarchivo. Ahora es un verdadero independiente .

muyustan
fuente
@JonathonReinhart, si todavía estás cerca, quería informarte que usé tu función en mi respuesta.
muyustan
0

Usando la excelente respuesta de Max y esta publicación sobre agregar archivos de datos adicionales como imágenes o sonido y mi propia investigación / prueba, descubrí lo que creo que es la forma más fácil de agregar dichos archivos.

Si desea ver un ejemplo en vivo, mi repositorio está aquí en GitHub.

Nota: esto es para compilar usando el comando --onefileo -Fcon pyinstaller.

Mi entorno es el siguiente.


Resolviendo el problema en 2 pasos

Para resolver el problema, necesitamos decirle específicamente a Pyinstaller que tenemos archivos adicionales que necesitan ser "empaquetados" con la aplicación.

También necesitamos usar una ruta 'relativa' , para que la aplicación pueda ejecutarse correctamente cuando se ejecuta como un script de Python o un EXE congelado.

Dicho esto, necesitamos una función que nos permita tener rutas relativas. Usando la función que Max Posted podemos resolver fácilmente la ruta relativa.

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)

Usaríamos la función anterior de esta manera para que el ícono de la aplicación aparezca cuando la aplicación se esté ejecutando como Script O Frozen EXE.

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

El siguiente paso es que necesitamos indicarle a Pyinstaller dónde encontrar los archivos adicionales cuando se está compilando para que cuando se ejecute la aplicación, se creen en el directorio temporal.

Podemos resolver este problema de dos maneras, como se muestra en la documentación , pero personalmente prefiero administrar mi propio archivo .spec, así es como lo haremos.

Primero, ya debe tener un archivo .spec. En mi caso, pude crear lo que necesitaba ejecutando pyinstallercon argumentos adicionales, puede encontrar argumentos adicionales aquí . Debido a esto, mi archivo de especificaciones puede verse un poco diferente al suyo, pero lo publicaré como referencia después de explicar los puntos importantes.

added_files es esencialmente una lista que contiene tuplas, en mi caso solo quiero agregar una imagen ÚNICA, pero puede agregar múltiples ico, png o jpg usando('app/img/*.ico', 'app/img')También puede crear otra tupla comoadded_files = [ (), (), ()]para tener múltiples importaciones

La primera parte de la tupla define qué archivo o qué tipo de archivo le gustaría agregar, así como dónde encontrarlos. Piense en esto como CTRL + C

La segunda parte de la tupla le dice a Pyinstaller que haga la ruta 'app / img /' y coloque los archivos en ese directorio RELATIVOS a cualquier directorio temporal que se cree cuando ejecuta el .exe. Piense en esto como CTRL + V

Debajoa = Analysis([main... , configurédatas=added_files, originalmente solía ser,datas=[]pero queremos que la lista de importaciones sea, bueno, importada, así que pasamos nuestras importaciones personalizadas.

No necesita hacer esto a menos que desee un ícono específico para el EXE, en la parte inferior del archivo de especificaciones le digo a Pyinstaller que configure el ícono de mi aplicación para el exe con la opción 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')

Compilar a EXE

Soy muy flojo; No me gusta escribir cosas más de lo necesario. He creado un archivo .bat en el que puedo hacer clic. No tiene que hacer esto, este código se ejecutará en un shell del símbolo del sistema perfectamente sin él.

Dado que el archivo .spec contiene todas nuestras configuraciones y argumentos de compilación (también conocidos como opciones), solo tenemos que darle ese archivo .spec a Pyinstaller.

pyinstaller.exe "Process Killer.spec"
Jtonna
fuente
0

Otra solución es crear un enlace de tiempo de ejecución, que copiará (o moverá) sus datos (archivos / carpetas) al directorio en el que está almacenado el ejecutable. El gancho es un archivo Python simple que puede hacer casi cualquier cosa, justo antes de la ejecución de su aplicación. Para configurarlo, debe usar la --runtime-hook=my_hook.pyopción de pyinstaller. Entonces, en caso de que sus datos sean una carpeta de imágenes , debe ejecutar el comando:

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

El cp_images_hook.py podría ser algo como esto:

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.")

Antes de cada ejecución, la carpeta de imágenes se mueve al directorio actual (desde la carpeta _MEIPASS), por lo que el ejecutable siempre tendrá acceso a ella. De esa forma, no es necesario modificar el código de su proyecto.

Segunda solución

Puede aprovechar el mecanismo de enlace en tiempo de ejecución y cambiar el directorio actual, lo que no es una buena práctica según algunos desarrolladores, pero funciona bien.

El código de gancho se puede encontrar a continuación:

import sys
import os

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