Agrupando arquivos de dados com PyInstaller (--onefile)

107

Estou tentando construir um EXE de um arquivo com PyInstaller que deve incluir uma imagem e um ícone. Eu não posso por minha vida fazê-lo funcionar --onefile.

Se eu fizer --onedirisso, tudo funcionará muito bem. Quando eu uso --onefile, ele não consegue encontrar os arquivos adicionais referenciados (ao executar o EXE compilado). Ele encontra as DLLs e tudo o mais em ordem, mas não as duas imagens.

Eu olhei no diretório temporário gerado ao executar o EXE ( \Temp\_MEI95642\por exemplo) e os arquivos estão realmente lá. Quando eu solto o EXE nesse diretório temporário, ele os encontra. Muito desconcertante.

Isso é o que eu adicionei ao .specarquivo

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

Devo acrescentar que tentei não colocá-los em subpastas também, não fez diferença.

Editar: a resposta mais recente marcada como correta devido à atualização do PyInstaller.

tubarão arbóreo
fonte
11
muito obrigado! a linha aqui ( a.datas += ...) realmente me ajudou agora. a documentação do pyinstaller fala sobre o uso, COLLECTmas não consegue colocar os arquivos no binário ao usar--onefile
Igor Serebryany
@IgorSerebryany: Destacado! Eu simplesmente tive exatamente o mesmo problema.
Hubro
Meu .exe trava quando clico na barra de menu se eu o usava
iacopo
1
Leve em consideração que a pasta temporária desaparece quando a execução termina, então para verificar o que há dentro dela, coloque uma lista de sys._MEIPASS em seu programa principal
hithwen
Existe também uma maneira de usar a sintaxe Tree () que pareço ter visto por aí?
fatuhoku

Respostas:

152

As versões mais recentes do PyInstaller não definem mais a envvariável, portanto, a excelente resposta de Shish não funcionará. Agora o caminho é definido 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
fonte
10
Estou usando o pyinstaller 2 com python 2.7 e não tenho _MEIPASS2no envs, mas sys._MEIPASSfunciona bem, então +1. Eu sugiro:path = getattr(sys, '_MEIPASS', os.getcwd())
kbec
2
+1. Obrigado por isso. Tentando usar a variável de ambiente _MEIPASS2 por um tempo agora, evidentemente eu escrevi meu código quando ainda era a variável de ambiente, porque me lembro dele funcionando. De repente, ele parou, quando eu recompilei recentemente.
Favil Orbedios de
8
Eu recomendo pegar o em AttributeErrorvez do mais geral Exception. Eu tive problemas em que estava faltando a importação do sistema e o caminho silenciosamente padronizado os.path.abspath.
Aaron
Você pode ajudar a resolver meu problema .
Phillip
1
Usar o diretório de trabalho atual no caso em que _MEIPASS não está definido é errado. Veja minha resposta .
Jonathon Reinhart
51

pyinstaller descompacta seus dados em uma pasta temporária e armazena esse caminho de diretório na _MEIPASS2variável de ambiente. Para obter o _MEIPASS2dir no modo compactado e usar o diretório local no modo descompactado (desenvolvimento), eu uso o seguinte:

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

Resultado:

# 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
fonte
10
Como, quando e onde usar isso?
gseattle
4
Você deve usar este script no arquivo .py que está tentando compilar com o PyInstaller. Não coloque este trecho de código no arquivo .spec, isso não funcionará. Acesse seus arquivos substituindo o caminho pelo qual você normalmente digitaria resource_path("file_to_be_accessed.mp3"). Tenha cuidado ao usar max 'answer para a versão atual do PyInstaller.
Exeleration-G
1
Esta resposta é específica para usar a --one-fileopção?
fatuhoku
Você pode ajudar a resolver meu problema .
Phillip
Usar o diretório de trabalho atual no caso em que _MEIPASS não está definido é errado. Veja minha resposta .
Jonathon Reinhart
30

Todas as outras respostas usam o diretório de trabalho atual no caso em que o aplicativo não é PyInstalled (ou sys._MEIPASSseja, não está definido). Isso está errado, pois impede que você execute seu aplicativo a partir de um diretório diferente daquele onde está seu script.

Uma solução melhor:

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
fonte
Obrigado Jon, sua função me ajudou hoje a resolver um problema. Eu apenas tenho uma pergunta. Por que alguns escrevem _MEIPASS2 em vez de _MEIPASS? Quando tentei adicionar o 2 antes, não funcionou.
Ahmed AbdelKhalek
Eu acho que os.env['_MEIPASS2'](usando o ambiente do sistema operacional) era um método antigo de obter o diretório. AFAIK sys._MEIPASSé o único método correto agora.
Jonathon Reinhart
Olá, sua solução funciona bem quando resource_path()está no script principal. Existe uma maneira de fazer funcionar quando é escrito em um módulo? A partir de agora, ele tentará obter recursos na pasta onde está o módulo, não na principal.
Guimoute
1
@Guimoute parece que o código nesta resposta está funcionando conforme projetado: ele localiza os recursos para o módulo que os fornece porque usa __file__. Se você quiser que um módulo seja capaz de acessar recursos fora de seu próprio escopo, você terá que implementar que o código de importação forneça um caminho para eles para seu módulo usar, por exemplo, pelo módulo fornecendo uma chamada "set_resource_location ()" que o chamadas do importador - não pense que isso é diferente de como você deveria trabalhar quando não está usando o pyinstaller.
barny
@barny Obrigado pela dica! Vou tentar algo nesse sentido.
Guimoute
14

Talvez eu tenha perdido uma etapa ou feito algo errado, mas os métodos acima não agrupam os arquivos de dados com o PyInstaller em um arquivo exe. Deixe-me compartilhar os passos que fiz.

  1. etapa: Grave um dos métodos acima em seu arquivo py com a importação dos módulos sys e os. Eu tentei os dois. O último é:

    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. etapa: Grave, pyi-makespec file.py , no console, para criar um arquivo file.spec.

  3. passo: Abra, file.spec com Notepad ++ para adicionar os arquivos de dados como abaixo:

    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. etapa: Eu segui as etapas acima e salvei o arquivo de especificações. Por fim, abra o console e escreva, pyinstaller file.spec (no meu caso, arquivo = Converter-GUI).

Conclusão: ainda há mais de um arquivo na pasta dist.

Observação: estou usando Python 3.5.

EDIT: Finalmente funciona com o método de Jonathan Reinhart.

  1. etapa: Adicione os códigos abaixo ao seu arquivo python com a importação de 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. etapa: Chame a função acima adicionando o caminho do seu arquivo:

    image_path = resource_path("Converter-GUI.ico")
  3. etapa: Escreva a variável acima que chama a função para onde seus códigos precisam do caminho. No meu caso é:

        self.window.iconbitmap(image_path)
  4. passo: Abra o console no mesmo diretório do seu arquivo python, escreva os códigos como abaixo:

        pyinstaller --onefile your_file.py
  5. etapa: Abra o arquivo .spec do arquivo python e anexe o array a.datas e adicione o ícone à classe exe, que foi fornecida acima antes da edição na 3ª etapa.
  6. passo: Salve e saia do arquivo de caminho. Vá para a pasta que inclui o arquivo spec e py. Abra novamente a janela do console e digite o comando abaixo:

        pyinstaller your_file.spec

Após a etapa 6., seu único arquivo está pronto para uso.

dildeolupbiten
fonte
Obrigado @dilde! Não que a a.datasmatriz da etapa 5 use tuplas de strings, consulte pythonhosted.org/PyInstaller/spec-files.html#adding-data-files para referência.
Ian Campbell,
Desculpe, não consigo entender uma parte da sua mensagem tão bem, eu gostaria, por causa do meu inglês fraco. Essa parte é "Não que o a.datas ...." . Você queria escrever 'Observe que o a.datas ... " Há algo de errado com o que escrevi sobre adicionar strings ao array a.datas?
dildeolupbiten
Ops! Deve ser Observe que ... sim, desculpe o erro de digitação. Seus passos funcionaram muito bem para mim :), não consegui encontrar essas informações na documentação deles ou em qualquer outro lugar.
Ian Campbell,
8

Em vez de reescrever todo o meu path code conforme sugerido, alterei o diretório de trabalho:

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

Basta adicionar essas duas linhas no início de seu código, você pode deixar o resto como está.

Bobsleigh
fonte
2
Não. Bons aplicativos raramente devem mudar o diretório de trabalho. Existem maneiras melhores.
Jonathon Reinhart
3

Ligeira modificação na resposta aceita.

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
fonte
Usar o diretório de trabalho atual no caso em que _MEIPASS não está definido é errado. Veja minha resposta .
Jonathon Reinhart
1

A reclamação / pergunta mais comum que vi no PyInstaller é "meu código não consegue encontrar um arquivo de dados que definitivamente incluí no pacote, onde está?", E não é fácil ver qual / onde seu código está procurando porque o código extraído está em um local temporário e é removido ao sair. Adicione este pedaço de código para ver o que está incluído em seu arquivo on-line e onde ele está, usando @Jonathon Reinhartresource_path()

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

Achei as respostas existentes confusas e levei muito tempo para descobrir onde estava o problema. Aqui está uma compilação de tudo que encontrei.

Quando executo meu aplicativo, recebo um erro Failed to execute script foo(se foo.pyfor o arquivo principal). Para solucionar isso, não execute o PyInstaller com --noconsole(ou edite main.specpara alterar console=False=> console=True). Com isso, execute o executável a partir de uma linha de comando e você verá a falha.

A primeira coisa a verificar é se ele está empacotando seus arquivos extras corretamente. Você deve adicionar tuplas, ('x', 'x')se quiser que a pasta xseja incluída.

Depois que ele travar, não clique em OK. Se você estiver no Windows, poderá usar Pesquisar tudo . Procure um de seus arquivos (por exemplo sword.png). Você deve encontrar o caminho temporário onde descompactou os arquivos (por exemplo. C:\Users\ashes999\AppData\Local\Temp\_MEI157682\images\sword.png). Você pode navegar neste diretório e certificar-se de que incluiu tudo. Se você não conseguir encontrar desta forma, procure algo como main.exe.manifest(Windows) ou python35.dll(se estiver usando Python 3.5).

Se o instalador incluir tudo, o próximo problema provável é a E / S do arquivo: seu código Python está procurando no diretório do executável, em vez do diretório temporário, por arquivos.

Para consertar isso, qualquer uma das respostas nesta pergunta funciona. Pessoalmente, descobri que uma mistura de todos eles funcionam: mude o diretório condicionalmente a primeira coisa em seu arquivo de ponto de entrada principal e todo o resto funcionará como está:

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

cinzas999
fonte
1
Desculpe. Mas mudar de diretório nunca é a resposta certa. Se um usuário fornecer um caminho para o aplicativo, ele espera que seja relativo ao diretório atual. Se você mudar isso, ficará errado.
Jonathon Reinhart
0

Se você ainda está tentando colocar arquivos relativos ao seu executável em vez de no diretório temporário, você precisa copiá-lo você mesmo. Foi assim que acabei fazendo.

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

Você adiciona uma etapa no arquivo spec que faz uma cópia do sistema de arquivos para a variável DISTPATH.

Espero que ajude.

Phyatt
fonte
0

Tenho lidado com esse problema há muito (bem, muito tempo) tempo. Procurei quase todas as fontes, mas as coisas não estavam entrando em um padrão na minha cabeça.

Finalmente, acho que descobri as etapas exatas a seguir e gostaria de compartilhar.

Note que minha resposta usa informações sobre as respostas de outras pessoas sobre esta questão.

Como criar um executável autônomo de um projeto Python.

Suponha que temos um project_folder e a árvore de arquivos é a seguinte:

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

Em primeiro lugar, digamos que você tenha definido seus caminhos para sound/e img/pastas em variáveis sound_dire da img_dirseguinte forma:

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

Você deve alterá-los da seguinte forma:

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

Onde, resource_path()é definido no início do seu script 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)

Ative o env virtual se estiver usando um venv,

Instale pyinstaller se você fez não ainda, por: pip3 install pyinstaller.

Executar: pyi-makespec --onefile main.pypara criar o arquivo de especificações para o processo de compilação e construção.

Isso mudará a hierarquia do arquivo para:

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

Aberto (com um edior) main.spec:

No topo, insira:

added_files = [

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

]

Em seguida, altere a linha de datas=[],paradatas=added_files,

Para mais detalhes sobre as operações realizadas, main.specconsulte aqui.

Corre pyinstaller --onefile main.spec

E isso é tudo, você pode executar mainem project_folder/distqualquer lugar, sem ter qualquer outra coisa em sua pasta. Você pode distribuir apenas esse mainarquivo. É agora, um verdadeiro autônomo .

muyustan
fonte
@JonathonReinhart se você ainda estiver por aí, gostaria de informar que usei sua função em minha resposta.
muyustan
0

Usando a excelente resposta de Max e Este post sobre adicionar arquivos de dados extras como imagens ou som e minha própria pesquisa / teste, descobri o que acredito ser a maneira mais fácil de adicionar esses arquivos.

Se você gostaria de ver um exemplo ao vivo, meu repositório está aqui no GitHub.

Nota: isso é para compilar usando o comando --onefileou -Fcom pyinstaller.

Meu ambiente é o seguinte.


Resolvendo o problema em 2 etapas

Para resolver o problema, precisamos informar especificamente ao Pyinstaller que temos arquivos extras que precisam ser "agrupados" com o aplicativo.

Também precisamos usar um caminho 'relativo' , para que o aplicativo possa ser executado corretamente quando estiver sendo executado como um script Python ou um EXE congelado.

Com isso dito, precisamos de uma função que nos permita ter caminhos relativos. Usando a função que Max Postou , podemos resolver facilmente o caminho 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)

Usaríamos a função acima assim, para que o ícone do aplicativo apareça quando o aplicativo estiver sendo executado como um Script OU EXE congelado.

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

A próxima etapa é que precisamos instruir o Pyinstaller sobre onde encontrar os arquivos extras durante a compilação para que, quando o aplicativo for executado, eles sejam criados no diretório temporário.

Podemos resolver esse problema de duas maneiras, conforme mostrado na documentação , mas eu pessoalmente prefiro gerenciar meu próprio arquivo .spec, então é assim que faremos.

Primeiro, você já deve ter um arquivo .spec. No meu caso, consegui criar o que precisava executando pyinstallercom args extras. Você pode encontrar args extras aqui . Por causa disso, meu arquivo de especificações pode parecer um pouco diferente do seu, mas estou postando tudo para referência depois de explicar as partes importantes.

add_files é essencialmente uma lista contendo tuplas, no meu caso eu só quero adicionar uma ÚNICA imagem, mas você pode adicionar vários ico's, png's ou jpg's usando('app/img/*.ico', 'app/img')Você também pode criar outra tuplaadded_files = [ (), (), ()]para ter várias importações

A primeira parte da tupla define que arquivo ou que tipo de arquivo você gostaria de adicionar, bem como onde encontrá-los. Pense nisso como CTRL + C

A segunda parte da tupla diz ao Pyinstaller para tornar o caminho 'app / img /' e colocar os arquivos nesse diretório RELATIVO a qualquer diretório temporário criado quando você executa o .exe. Pense nisso como CTRL + V

Ema = Analysis([main... , eu configureidatas=added_files, originalmente costumava ser,datas=[]mas queremos que a lista de importações seja, bem, importada, então passamos nossas importações personalizadas.

Você não precisa fazer isso, a menos que queira um ícone específico para o EXE; na parte inferior do arquivo de especificações, estou dizendo ao Pyinstaller para definir o ícone do meu aplicativo para o exe com a opção 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')

Compilando para EXE

Sou muito preguiçoso; Não gosto de digitar coisas mais do que preciso. Criei um arquivo .bat no qual posso apenas clicar. Você não precisa fazer isso, este código será executado em um shell de prompt de comando sem ele.

Uma vez que o arquivo .spec contém todas as nossas configurações de compilação e args (opções também), só temos que fornecer esse arquivo .spec para o Pyinstaller.

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

Outra solução é fazer um gancho de tempo de execução, que irá copiar (ou mover) seus dados (arquivos / pastas) para o diretório no qual o executável está armazenado. O gancho é um arquivo python simples que pode fazer quase tudo, logo antes da execução do seu aplicativo. Para configurá-lo, você deve usar a --runtime-hook=my_hook.pyopção de pyinstaller. Portanto, caso seus dados sejam uma pasta de imagens , você deve executar o comando:

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

O cp_images_hook.py poderia ser algo assim:

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 execução, a pasta de imagens é movida para o diretório atual (da pasta _MEIPASS), para que o executável sempre tenha acesso a ela. Dessa forma, não há necessidade de modificar o código do seu projeto.

Segunda Solução

Você pode aproveitar as vantagens do mecanismo runtime-hook e alterar o diretório atual, o que não é uma boa prática de acordo com alguns desenvolvedores, mas funciona bem.

O código do gancho pode ser encontrado abaixo:

import sys
import os

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