24

Situation

I'm using Pyinstaller on Windows to make an .exe file of my project.

I would like to use --onefile option to have a clean result and an easy to distribute file/program.

My program use a config.ini file for storing config options. This file could be customized by users.

Problem

Using --onefile option Pyinstaller put all declared "data-file" inside the single .exe file file.

I've seen this request but it give istructions to add a bundle file inside the onefile and not outside, at the same level of the .exe and in the same dist directory.

At some point I've thought to use a shutil.copy command inside .spec file to copy this file... but I think to be in the wrong way.

Can some one help me? I'll appreciate it :-)

Stefano Giraldi
  • 1,031
  • 1
  • 10
  • 22
  • 1
    I have the exact same problem. – Ricardo Duarte Dec 18 '17 at 12:06
  • I hope to get some advice to understand if it's possible get this result directly with Pyinstaller. At the moment I've seen a lot of questions about this behavior of - - onefile option without finding the solution. – Stefano Giraldi Dec 18 '17 at 12:21
  • Hi @RicardoDuarte I've found an automatic way to add external data files to PyInstaller --onefile command. Take a look to the answer and let me know if it works also for you. – Stefano Giraldi Dec 19 '17 at 14:21
  • You can follow the instructions of a simple solution below: https://stackoverflow.com/a/63069583/5068961 – tsahmatsis Jul 24 '20 at 08:37

2 Answers2

30

A repository on Github helped me to find a solution to my question.

I've used shutil module and .spec file to add extra data files (in my case a config-sample.ini file) to dist folder using Pyinstaller --onefile option.

Make a .spec file for pyinstaller

First of all I've create a makespec file with the options I need:

$ pyi-makespec --onefile --windowed --name exefilename scriptname.py

This comand create an exefilename.spec file to use with Pyinstaller

Modify exefilename.spec adding shutil.copyfile

Now I've edited the exefilename.spec adding at the end of the file the following code.

import shutil
shutil.copyfile('config-sample.ini', '{0}/config-sample.ini'.format(DISTPATH))
shutil.copyfile('whateveryouwant.ext', '{0}/whateveryouwant.ext'.format(DISTPATH))

This code copy the data files needed at the end of compile process. You could use all the methods available in shutil package.

Run PyInstaller

The final step is to run compile process

pyinstaller --clean exefilename.spec

The result is that in the dist folder you should have the compiled .exe file together with the data files copied.

Consideration

In the official documentation of Pyinstaller I didn't found an option to get this result. I think it could be considered as a workaround... that works.

Community
  • 1
  • 1
Stefano Giraldi
  • 1,031
  • 1
  • 10
  • 22
  • Beautiful. Thank you for posting. – phyatt Dec 19 '19 at 19:08
  • 1
    I know this is an old answer, but I'm still searching for a solution to this question. This answer has a lot of upvotes so I think I'm missing something. This copies the config file to the same directory as the executable, but doesn't appear to have any way of giving the path of the config file to the exe at runtime. That problem is solved in this answer https://stackoverflow.com/questions/60937345/how-to-set-up-relative-paths-to-make-a-portable-exe-build-in-pyinstaller-with-p but doesn't solve the problem of having the file in the same dir as the onefile exe – SimonN Jul 19 '21 at 07:54
1

My solution is similar to @Stefano-Giraldi 's excellent solution. I was getting permission denied when passing directories to the shutil.copyfile.

I ended up using shutil.copytree:

import sys, os, shutil

site_packages = os.path.join(os.path.dirname(sys.executable), "Lib", "site-packages")
added_files = [
                (os.path.join(site_packages, 'dash_html_components'), 'dash_html_components'),
                (os.path.join(site_packages, 'dash_core_components'), 'dash_core_components'),
                (os.path.join(site_packages, 'plotly'), 'plotly'),
                (os.path.join(site_packages, 'scipy', '.libs', '*.dll'), '.')
                ]
working_dir_files = [
                ('assets', 'assets'),
                ('csv', 'csv')
                ]

print('ADDED FILES: (will show up in sys._MEIPASS)')
print(added_files)
print('Copying files to the dist folder')

print(os.getcwd())
for tup in working_dir_files:
        print(tup)
        to_path = os.path.join(DISTPATH, tup[1])
        if os.path.exists(to_path):
                if os.path.isdir(to_path):
                        shutil.rmtree(to_path)
                else:
                        os.remove(to_path)
        if os.path.isdir(tup[0]):
                shutil.copytree(tup[0], to_path )
        else:
                shutil.copyfile(tup[0], to_path )

#### ... Rest of spec file
a = Analysis(['myapp.py'],
             pathex=['.', os.path.join(site_packages, 'scipy', '.libs')],
             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='myapp',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=True )

This avoids the _MEI folder and keeps it from copying config files that you want in your dist folder and not in a temp folder.

Hope that helps.

phyatt
  • 17,674
  • 5
  • 55
  • 73