Reputation: 1309
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.
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?
Upvotes: 37
Views: 38161
Reputation: 67
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)
Upvotes: 0
Reputation: 11
I solved this problem by removing the config file (config.py for me) from the project folder before running PyInstaller.
Upvotes: 0
Reputation: 4289
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 readconfig.json
directly.config.json
may be read by any of the modules used byapp.py
and the answer would still help.
app.py
config.json
When the application is running, files are copied to a temporary location on your Windows system C:\Users\<You>\AppData\Local\Temp\MEIxxx
. 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.
.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.
Finally, we can run the .spec
file to build the installer,
pyinstaller --clean appexe.spec
The resulting installer should now run without any FileNotFoundError
s.
Upvotes: 2
Reputation: 11
I tried many approaches, and this one worked for me:
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.
Inside your script, load that settings file this way:
filename = "settings.json"
contents = open(filename).read()
config = eval(contents)
setting1 = config['setting1']
Run PyInstaller or auto-py-to-exe (I tried both, and all works)
Place your settings.json file in the same folder where your .exe file is located.
Run it and it will take settings from that file.
Upvotes: 1
Reputation: 484
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())
os.getcwd()
/relative paths don't workThe 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.
Upvotes: 8
Reputation: 19112
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.
Upvotes: 3
Reputation: 1309
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.
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.
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.
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.
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.
Upvotes: 39