Raggruppare file di dati con PyInstaller (--onefile)


107

Sto cercando di creare un file EXE con un solo file con PyInstaller che deve includere un'immagine e un'icona. Non posso per la vita di me farlo funzionare con --onefile.

Se lo faccio --onedirfunziona tutto funziona molto bene. Quando lo uso --onefile, non riesce a trovare i file aggiuntivi referenziati (quando si esegue l'EXE compilato). Trova le DLL e tutto il resto a posto, ma non le due immagini.

Ho guardato nella \Temp\_MEI95642\directory temporanea generata durante l'esecuzione dell'EXE ( ad esempio) e i file sono effettivamente lì. Quando lascio cadere l'EXE in quella directory temporanea, li trova. Molto sconcertante.

Questo è ciò che ho aggiunto al .specfile

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

Aggiungo che ho provato a non metterli anche in sottocartelle, non ha fatto differenza.

Modifica: la risposta più recente contrassegnata come corretta a causa dell'aggiornamento di PyInstaller.


11
grazie mille! la linea qui ( a.datas += ...) mi ha davvero aiutato proprio ora. la documentazione di pyinstaller parla dell'uso COLLECTma non riesce a mettere i file nel binario durante l'utilizzo--onefile
Igor Serebryany

@IgorSerebryany: Seconded! Ho appena avuto lo stesso identico problema.
Hubro

Il mio .exe si arresta in modo
anomalo

1
Tieni presente che la cartella temp scompare al termine dell'esecuzione, quindi per controllare cosa c'è dentro metti un listdir di sys._MEIPASS nel tuo programma principale
hithwen

C'è anche un modo per usare la sintassi Tree () che mi sembra di aver visto in giro?
fatuhoku

Risposte:


152

Le versioni più recenti di PyInstaller non impostano più la envvariabile, quindi l'eccellente risposta di Shish non funzionerà. Ora il percorso viene impostato come 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)

10
Sto usando pyinstaller 2 con python 2.7 e non ho _MEIPASS2envs, ma sys._MEIPASSfunziona bene, quindi +1. Suggerisco:path = getattr(sys, '_MEIPASS', os.getcwd())
kbec

2
+1. Grazie per quello. Cercando di usare la variabile d'ambiente _MEIPASS2 per un po 'di tempo, evidentemente ho scritto il mio codice quando era ancora la variabile d'ambiente, perché ricordo che funzionava. All'improvviso, si è fermato, quando l'ho ricompilato di recente.
Favil Orbedios

8
Consiglierei di prendere il AttributeErrorinvece del più generale Exception. Mi sono imbattuto in problemi in cui mi mancava l'importazione di sys e il percorso impostato silenziosamente per impostazione predefinita os.path.abspath.
Aaron

Potresti essere in grado di aiutare a risolvere il mio problema .
Phillip

1
L'uso della directory di lavoro corrente nel caso in cui _MEIPASS non è impostato è sbagliato. Vedi la mia risposta .
Jonathon Reinhart

51

pyinstaller decomprime i dati in una cartella temporanea e memorizza questo percorso di directory nella _MEIPASS2variabile di ambiente. Per ottenere la _MEIPASS2directory in modalità compressa e utilizzare la directory locale in modalità decompressa (sviluppo), utilizzo questo:

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

Produzione:

# 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"

10
Come, quando e dove lo si usa?
gseattle

4
Dovresti usare questo script nel file .py che stai cercando di compilare con PyInstaller. Non inserire questo frammento di codice nel file .spec, non funzionerà. Accedi ai tuoi file sostituendo il percorso con cui digiti normalmente resource_path("file_to_be_accessed.mp3"). Diffidare di utilizzare max 'answer per la versione corrente di PyInstaller.
Exeleration-G

1
Questa risposta è specifica per l'utilizzo --one-filedell'opzione?
fatuhoku

Potresti essere in grado di aiutare a risolvere il mio problema .
Phillip

L'uso della directory di lavoro corrente nel caso in cui _MEIPASS non è impostato è sbagliato. Vedi la mia risposta .
Jonathon Reinhart

30

Tutte le altre risposte utilizzano la directory di lavoro corrente nel caso in cui l'applicazione non è PyInstalled (cioè sys._MEIPASSnon è impostata). È sbagliato, in quanto ti impedisce di eseguire l'applicazione da una directory diversa da quella in cui si trova lo script.

Una soluzione migliore:

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)

Grazie Jon, la tua funzione mi ha aiutato oggi a risolvere un problema. Ho solo una domanda. Perché alcuni scrivono _MEIPASS2 invece di _MEIPASS? Quando ho provato ad aggiungere il 2 prima, non ha funzionato.
Ahmed AbdelKhalek

Penso che os.env['_MEIPASS2'](utilizzando l'ambiente del sistema operativo) fosse un vecchio metodo per ottenere la directory. AFAIK sys._MEIPASSè l'unico metodo corretto ora.
Jonathon Reinhart

Ciao, la tua soluzione funziona bene quando resource_path()è nello script principale. C'è un modo per farlo funzionare quando invece è scritto in un modulo? A partire da ora, tenterà di ottenere risorse nella cartella in cui si trova il modulo, non in quella principale.
Guimoute

1
@Guimoute sembra che il codice in questa risposta funzioni come previsto: localizza le risorse nel modulo che le fornisce perché utilizza __file__. Se vuoi che un modulo sia in grado di accedere a risorse al di fuori del proprio ambito, dovrai implementare che il codice di importazione fornisca un percorso per loro che il tuo modulo può utilizzare, ad esempio dal modulo che fornisce una chiamata "set_resource_location ()" che il chiamate importatore - non pensare che questo sia diverso da come dovresti lavorare quando non usi pyinstaller.
barny

@barny Grazie per il suggerimento! Farò provare qualcosa in questo senso.
Guimoute

14

Forse ho perso un passaggio o ho fatto qualcosa di sbagliato, ma i metodi sopra non hanno raggruppato i file di dati con PyInstaller in un file exe. Consentitemi di condividere i passaggi che ho fatto.

  1. passo: scrivi uno dei metodi precedenti nel tuo file py importando i moduli sys e os. Li ho provati entrambi. L'ultimo è:

    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. passo: scrivi, pyi-fapec file.py , sulla console, per creare un file file.spec.

  3. passo: Apri, file.spec con Notepad ++ per aggiungere i file di dati come di seguito:

    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. passaggio: ho seguito i passaggi precedenti, quindi ho salvato il file delle specifiche. Alla fine apri la console e scrivi, pyinstaller file.spec (nel mio caso, file = Converter-GUI).

Conclusione: c'è ancora più di un file nella cartella dist.

Nota: sto usando Python 3.5.

EDIT: Finalmente funziona con il metodo di Jonathan Reinhart.

  1. passo: Aggiungi i seguenti codici al tuo file python importando sys e 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. passo: chiama la funzione sopra aggiungendo il percorso del tuo file:

    image_path = resource_path("Converter-GUI.ico")
  3. passo: scrivi la variabile sopra che chiama la funzione dove i tuoi codici hanno bisogno del percorso. Nel mio caso è:

        self.window.iconbitmap(image_path)
  4. passo: Apri la console nella stessa directory del tuo file python, scrivi i codici come di seguito:

        pyinstaller --onefile your_file.py
  5. passo: Apri il file .spec del file python e aggiungi l'array a.datas e aggiungi l'icona alla classe exe, che è stata data sopra prima della modifica nel 3 ° passaggio.
  6. passo: salva ed esci dal file del percorso. Vai alla tua cartella che include il file spec e py. Apri di nuovo la finestra della console e digita il comando seguente:

        pyinstaller your_file.spec

Dopo il 6. passaggio, il tuo unico file è pronto per l'uso.


Grazie @dilde! Non che l' a.datasarray del passaggio 5 richieda tuple di stringhe, vedere pythonhosted.org/PyInstaller/spec-files.html#adding-data-files per riferimento.
Ian Campbell

Mi spiace, non riesco a capire nemmeno una parte del tuo messaggio a causa del mio inglese debole. Quella parte è "Non che a.datas ...." , Volevi scrivere "Nota che a.datas ..." C'è qualcosa di sbagliato in quello che ho scritto sull'aggiunta di stringhe a un array.datas?
dildeolupbiten

Oops! Dovrebbe essere Nota che ... scusa per l'errore di battitura. I tuoi passaggi hanno funzionato alla grande per me :), non sono riuscito a trovare queste informazioni nella loro documentazione o altrove.
Ian Campbell

8

Invece per riscrivere tutto il mio codice di percorso come suggerito, ho cambiato la directory di lavoro:

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

Basta aggiungere quelle due righe all'inizio del codice, puoi lasciare il resto così com'è.


2
No. Le buone applicazioni raramente dovrebbero cambiare la directory di lavoro. Ci sono modi migliori.
Jonathon Reinhart

3

Leggera modifica alla risposta accettata.

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)

L'uso della directory di lavoro corrente nel caso in cui _MEIPASS non è impostato è sbagliato. Vedi la mia risposta .
Jonathon Reinhart

1

Il reclamo / domanda più comune che ho visto rispetto a PyInstaller è "il mio codice non riesce a trovare un file di dati che ho sicuramente incluso nel pacchetto, dove si trova?", E non è facile vedere cosa / dove è il tuo codice sta cercando perché il codice estratto si trova in una posizione temporanea e viene rimosso quando esce. Aggiungi questo bit di codice per vedere cosa è incluso nel tuo onefile e dove si trova, usando @Jonathon Reinhart'sresource_path()

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

0

Ho trovato le risposte esistenti confuse e ho impiegato molto tempo per capire dove fosse il problema. Ecco una raccolta di tutto ciò che ho trovato.

Quando eseguo la mia app, ricevo un errore Failed to execute script foo(se foo.pyè il file principale). Per risolvere questo problema, non eseguire PyInstaller con --noconsole(o modificare main.specper modificare console=False=> console=True). Con questo, esegui l'eseguibile da una riga di comando e vedrai l'errore.

La prima cosa da controllare è che impacchetta correttamente i tuoi file extra. Dovresti aggiungere tuple come ('x', 'x')se vuoi che la cartella xsia inclusa.

Dopo l'arresto anomalo, non fare clic su OK. Se sei su Windows, puoi utilizzare Cerca in tutto . Cerca uno dei tuoi file (ad es. sword.png). Dovresti trovare il percorso temporaneo in cui ha decompresso i file (es. C:\Users\ashes999\AppData\Local\Temp\_MEI157682\images\sword.png). Puoi sfogliare questa directory e assicurarti che includa tutto. Se non riesci a trovarlo in questo modo, cerca qualcosa come main.exe.manifest(Windows) opython35.dll (se stai usando Python 3.5).

Se il programma di installazione include tutto, il prossimo probabile problema è l'I / O del file: il codice Python sta cercando i file nella directory dell'eseguibile, invece che nella directory temporanea.

Per risolvere questo problema, qualsiasi risposta a questa domanda funziona. Personalmente, ho trovato una combinazione di tutti loro che funzionano: cambia la directory in modo condizionale per prima cosa nel tuo file del punto di ingresso principale e tutto il resto funziona così com'è:

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


1
Scusate. Ma cambiare directory non è mai la risposta giusta. Se un utente fornisce un percorso all'applicazione, si aspetta che sia relativo alla directory corrente. Se lo cambi, sarà sbagliato.
Jonathon Reinhart

0

Se stai ancora cercando di inserire file relativi al tuo eseguibile invece che nella directory temporanea, devi copiarlo da solo. È così che ho finito per farlo.

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

Si aggiunge un passaggio nel file spec che esegue una copia del filesystem nella variabile DISTPATH.

Spero che aiuti.


0

Mi occupo di questo problema da molto (beh, molto tempo). Ho cercato quasi tutte le fonti, ma le cose non stavano entrando in uno schema nella mia testa.

Infine, penso di aver capito i passaggi esatti da seguire, volevo condividere.

Nota che la mia risposta utilizza informazioni sulle risposte di altri su questa domanda.

Come creare un eseguibile autonomo di un progetto Python.

Supponiamo di avere un project_folder e l'albero dei file è il seguente:

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

Prima di tutto, supponiamo di aver definito i percorsi sound/e le img/cartelle in variabili sound_dire img_dircome segue:

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

Devi cambiarli, come segue:

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

Dove, resource_path()è definito nella parte superiore dello script come:

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)

Attivare l'env virtuale se si utilizza un venv,

Installare pyinstaller se l'avete fatto non ancora, da: pip3 install pyinstaller.

Esegui: pyi-makespec --onefile main.pyper creare il file delle specifiche per il processo di compilazione e generazione.

Questo cambierà la gerarchia dei file in:

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

Aperto (con un edior) main.spec:

In cima, inserisci:

added_files = [

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

]

Quindi, cambia la linea di datas=[],indatas=added_files,

Per i dettagli delle operazioni effettuate main.specvedere qui.

Correre pyinstaller --onefile main.spec

E questo è tutto, è possibile eseguire mainin project_folder/distda qualsiasi luogo, senza avere niente altro nella sua cartella. Puoi distribuire solo quel mainfile. Ora è un vero standalone .


@JonathonReinhart se sei ancora in giro, volevo informarti che ho usato la tua funzione nella mia risposta.
muyustan

0

Usando l'eccellente risposta di Max e questo post sull'aggiunta di file di dati extra come immagini o suoni e la mia ricerca / test, ho capito quello che credo sia il modo più semplice per aggiungere tali file.

Se desideri vedere un esempio dal vivo, il mio repository è qui su GitHub.

Nota: serve per la compilazione utilizzando il comando --onefileo -Fcon pyinstaller.

Il mio ambiente è il seguente.


Risolvere il problema in 2 passaggi

Per risolvere il problema dobbiamo dire specificamente a Pyinstaller che abbiamo file extra che devono essere "raggruppati" con l'applicazione.

È inoltre necessario utilizzare un percorso "relativo" , in modo che l'applicazione possa essere eseguita correttamente quando viene eseguita come script Python o EXE congelato.

Detto questo, abbiamo bisogno di una funzione che ci consenta di avere percorsi relativi. Utilizzando la funzione che Max Posted possiamo risolvere facilmente il percorso relativo.

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)

Useremo la funzione sopra in questo modo in modo che l'icona dell'applicazione venga visualizzata quando l'app è in esecuzione come script o come EXE congelato.

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

Il passo successivo è che dobbiamo istruire Pyinstaller su dove trovare i file extra durante la compilazione in modo che quando l'applicazione viene eseguita, vengano creati nella directory temporanea.

Possiamo risolvere questo problema in due modi, come mostrato nella documentazione , ma personalmente preferisco gestire il mio file .spec, quindi è così che lo faremo.

Innanzitutto, devi già avere un file .spec. Nel mio caso, sono stato in grado di creare ciò di cui avevo bisogno eseguendo pyinstallerargomenti extra, puoi trovare argomenti extra qui . Per questo motivo, il mio file delle specifiche potrebbe sembrare leggermente diverso dal tuo, ma lo pubblicherò tutto come riferimento dopo aver spiegato le parti importanti.

added_files è essenzialmente una lista contenente Tuple, nel mio caso voglio solo aggiungere una SINGOLA immagine, ma puoi aggiungere più ico, png o jpg usando('app/img/*.ico', 'app/img')Puoi anche creare un'altra tupla in questo modoadded_files = [ (), (), ()]per avere più importazioni

La prima parte della tupla definisce quale file o quale tipo di file si desidera aggiungere e dove trovarli. Pensa a questo come CTRL + C

La seconda parte della tupla dice a Pyinstaller di creare il percorso "app / img /" e di posizionare i file in quella directory RELATIVI a qualsiasi directory temporanea creata quando si esegue l'exe. Pensa a questo come CTRL + V

Sottoa = Analysis([main... , ho impostatodatas=added_files, originariamente eradatas=[] ma vogliamo che l'elenco delle importazioni sia, beh, importato, quindi passiamo alle nostre importazioni personalizzate.

Non è necessario farlo a meno che non si desideri un'icona specifica per l'EXE, in fondo al file delle specifiche sto dicendo a Pyinstaller di impostare l'icona della mia applicazione per l'exe con l'opzione 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')

Compilazione in EXE

Sono molto pigro; Non mi piace scrivere cose più di quanto devo. Ho creato un file .bat su cui posso semplicemente fare clic. Non devi farlo, questo codice verrà eseguito in una shell del prompt dei comandi senza di essa.

Dato che il file .spec contiene tutte le nostre impostazioni e argomenti di compilazione (ovvero opzioni), dobbiamo solo dare quel file .spec a Pyinstaller.

pyinstaller.exe "Process Killer.spec"

0

Un'altra soluzione è creare un hook di runtime, che copierà (o sposterà) i tuoi dati (file / cartelle) nella directory in cui è archiviato l'eseguibile. L'hook è un semplice file python che può fare quasi tutto, appena prima dell'esecuzione della tua app. Per impostarlo, dovresti usare l' --runtime-hook=my_hook.pyopzione di pyinstaller. Quindi, nel caso in cui i tuoi dati siano una cartella di immagini , dovresti eseguire il comando:

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

Cp_images_hook.py potrebbe essere qualcosa del genere:

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

Prima di ogni esecuzione la cartella delle immagini viene spostata nella directory corrente (dalla cartella _MEIPASS), quindi l'eseguibile avrà sempre accesso ad essa. In questo modo non è necessario modificare il codice del progetto.

Seconda soluzione

Puoi sfruttare il meccanismo di runtime-hook e cambiare la directory corrente, che non è una buona pratica secondo alcuni sviluppatori, ma funziona bene.

Il codice hook può essere trovato di seguito:

import sys
import os

path = getattr(sys, '_MEIPASS', os.getcwd())   
os.chdir(path)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.