Add configuration file outside PyInstaller –onefile EXE file into dist directory

Question:

Situation

I’m using PyInstaller on Windows to make an EXE file of my project.

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

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

Problem

Using --onefile option, PyInstaller puts all declared "data-file"s inside the single .exe file.

I’ve seen this request, but it gives 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.

How can I fix this?

Asked By: Stefano Giraldi

||

Answers:

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

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

Make a .spec file for PyInstaller

First of all, I’ve created a makespec file with the options I needed:

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

This command creates an exefilename.spec file to use with PyInstaller.

Modify exefilename.spec, adding shutil.copyfile

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

import shutil

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

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

Run PyInstaller

The final step is to run the 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 find an option to get this result. I think it could be considered as a workaround… that works.

Answered By: Stefano Giraldi

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)

#### ... The rest of the 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 configuration files that you want in your dist folder and not in a temporary folder.

Answered By: phyatt

Here is how to access files that are on the same level as the output file. The trick is that the sys.executable is where the one-file .exe is located. So simply this does the trick:

import sys
import os.path
CWD = os.path.abspath(os.path.dirname(sys.executable))

Use it e.g. with

with open(os.path.join(CWD, "config.ini")) as config_file:
    print(config_file.read())

The reason why os.getcwd()/relative paths don’t work

The executable is just an executable archive that is extracted on execution to a temporary directory, where the .pyc files are executed. So when you call os.getcwd() instead of the path to the executable, you get the path to the temporary folder.

Answered By: GuiTaek

I tried many approaches, and this one worked for me:

  1. Do not use the .py extension for the configuration file. Use JSON instead. JSON is not good because you can’t write comments in it, but if you want to make an EXE file you have to use it unfortunately.

  2. Inside your script, load that settings file this way:

    filename = "settings.json"
    contents = open(filename).read()
    config = eval(contents)
    setting1 = config['setting1']
    
  3. Run PyInstaller or auto-py-to-exe (I tried both, and all works)

  4. Place your settings.json file in the same folder where your .exe file is located.

  5. Run it and it will take settings from that file.

Answered By: Dmitry S

If any of your Python scripts uses external files (JSON, text or any configuration files) and you wish to include those files in the executable, follow these steps on a Windows system.

Considering that there’s a Python script app.py and it reads a JSON file config.json, our goal is to add the config.json file in some suitable directory where it can be accessed by the application while it is running (as an .exe).

This answer is applicable even if app.py does not read config.json directly. config.json may be read by any of the modules used by app.py and the answer would still help.

app.py
config.json

Step 1: Resolving the path of the JSON file while the application is running

When the application is running, files are copied to a temporary location on your Windows system C:Users<You>AppDataLocalTempMEIxxx. So, app.py needs to read the JSON file from this temporary location. But how would the app know at runtime in which directory it has to search for the files? From this excellent answer, in app.py, we’ll have a function,

def resolve_path(path):
    if getattr(sys, "frozen", False):
        # If the 'frozen' flag is set, we are in bundled-app mode!
        resolved_path = os.path.abspath(os.path.join(sys._MEIPASS, path))
    else:
        # Normal development mode. Use os.getcwd() or __file__ as appropriate in your case...
        resolved_path = os.path.abspath(os.path.join(os.getcwd(), path))

    return resolved_path

# While reading the JSON file
with open(resolve_path("config.json"), "r") as jsonfile:
   # Access the contents here

Now app.py knows where to look for the file. Before that, we need to instruct PyInstaller to copy config.json into that temporary files directory.

Note, you need to use the resolve_path wherever you are using a relative path in your Python Scripts.

Step 2: Make and edit the .spec file to copy config.json

As we wish to create an executable of app.py, we’ll first create a .spec file for it, (referring to @Stefano Giraldi’s answer)

pyi-makespec --onefile --windowed --name appexe app.py

Open the resulting appexe.spec file, and you’ll notice these contents,

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None
a = Analysis(
    ...
    datas=[],
    ...
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    ...
)

Create a new list files and pass it to the datas argument,

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

files = [
    ( 'config.json' , '.' )
]

a = Analysis(
    ...
    datas=files,
    ...
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
    ...
)

The tuple ('config.json', '.') denotes the source and destination paths of the file. The destination path is relative to the temporary file directory.

Step 3: Build the executable

Finally, we can run the .spec file to build the installer,

pyinstaller --clean appexe.spec

The resulting installer should now run without any FileNotFoundErrors.

Answered By: Shubham Panchal

I solved this problem by removing the config file (config.py for me) from the project folder before running PyInstaller.

Answered By: LsAndy

There is a solution, but it is not the best:

config_str = """
some configuration code string
"""
with open('path_to_somewhere\config.ini', 'w', encoding="gbk") as writer:
    writer.write(config_str)
Answered By: qihuan wu
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.