How to Compile Multiple Python Files into Single .Exe File Using Pyinstaller

How to compile multiple python files into single .exe file using pyinstaller

The best way is to use the array datas

For example like this:

a = Analysis(['.\\main.py'],
pathex=['.'],
binaries=None,
datas=[ ('.\\Ressources\\i18n', 'i18n'),
('.\\Ressources\\file1.py', '.')],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)

Note: Make sure to put it in the right relative path so your program will be able to access it

Edit: Given your error message, the problem is not in packaging with PyInstaller but in os.system command.

os.system is equivalent to opening a DOS command window and typping your command python_file.py

To access your python files, you have to know:

  • PyInstaller unpack your packed files in a temporary folder that you can access in sys._MEIPASS (only works from the .exe)
  • os.system can be used to launch python given the complete path to the file like this:
    os.system("python " + os.path.join(sys._MEIPASS, "python_file.py"))

    But be carefull, this will only work if python is installed on the system (and included in syspath) and from the exe. Executing directly your python file will send exception.

How to compile multiple subprocess python files into single .exe file using pyinstaller

Assuming you can't restructure your app so this isn't necessary (e.g., by using multiprocessing instead of subprocess), there are three solutions:

  • Ensure that the .exe contains the scripts as an (executable) zipfile—or just use pkg_resources—and copy the script out to a temporary directory so you can run it from there.
  • Write a multi-entrypoint wrapper script that can be run as your main program, and also run as each script—because, while you can't run a script out of the packed exe, you can import a module out of it.
  • Using pkg_resources again, write a wrapper that runs the script by loading it as a string and running it with exec instead.

The second one is probably the cleanest, but it is a bit of work. And, while we could rely on setuptools entrypoints to some of the work, trying to explain how to do this is much harder than explaining how to do it manually,1 so I'm going to do the latter.


Let's say your code looked like this:

# main.py
import subprocess
import sys
spam, eggs = sys.argv[1], sys.argv[2]
subprocess.run([sys.executable, 'vikings.py', spam])
subprocess.run([sys.executable, 'waitress.py', spam, eggs])

# vikings.py
import sys
print(' '.join(['spam'] * int(sys.argv[1])))

# waitress.py
import sys
import time
spam, eggs = int(sys.argv[1]), int(sys.argv[2]))
if eggs > spam:
print("You can't have more eggs than spam!")
sys.exit(2)
print("Frying...")
time.sleep(2)
raise Exception("This sketch is getting too silly!")

So, you run it like this:

$ python3 main.py 3 4
spam spam spam
You can't have more eggs than spam!

We want to reorganize it so there's a script that looks at the command-line arguments to decide what to import. Here's the smallest change to do that:

# main.py
import subprocess
import sys
if sys.argv[1][:2] == '--':
script = sys.argv[1][2:]
if script == 'vikings':
import vikings
vikings.run(*sys.argv[2:])
elif script == 'waitress':
import waitress
waitress.run(*sys.argv[2:])
else:
raise Exception(f'Unknown script {script}')
else:
spam, eggs = sys.argv[1], sys.argv[2]
subprocess.run([sys.executable, __file__, '--vikings', spam])
subprocess.run([sys.executable, __file__, '--waitress', spam, eggs])

# vikings.py
def run(spam):
print(' '.join(['spam'] * int(spam)))

# waitress.py
import sys
import time
def run(spam, eggs):
spam, eggs = int(spam), int(eggs)
if eggs > spam:
print("You can't have more eggs than spam!")
sys.exit(2)
print("Frying...")
time.sleep(2)
raise Exception("This sketch is getting too silly!")

And now:

$ python3 main.py 3 4
spam spam spam
You can't have more eggs than spam!

A few changes you might want to consider in real life:

  • DRY: We have the same three lines of code copied and pasted for each script, and we have to type each script name three times. You can just use something like __import__(sys.argv[1][2:]).run(sys.argv[2:]) with appropriate error handling.
  • Use argparse instead of this hacky special casing for the first argument. If you're already sending non-trivial arguments to the scripts, you're probably already using argparse or an alternative anyway.
  • Add an if __name__ == '__main__': block to each script that just calls run(sys.argv[1:]), so that during development you can still run the scripts directly to test them.

I didn't do any of these because they'd obscure the idea for this trivial example.


1 The documentation is great as a refresher if you've already done it, but as a tutorial and explanatory rationale, not so much. And trying to write the tutorial that the brilliant PyPA guys haven't been able to come up with for years… that's probably beyond the scope of an SO answer.

Convert multiple .py file to .exe

The concept used to execute (dummy_script.py) in (GUI.py) is,

p = QProcess()
p.start("python3", ['dummy_script.py'])

PyInstaller is pretty good at finding and bundling dependencies, but it won't be able to figure out that GUI.py needs dummy_script.py if you run it this way.

A better approach would be to import the code you need and use it directly, e.g something like

from dummy_script import some_function


some_function()
Other options

If you simply modify GUI.py like this, PyInstaller should find dummy_script.py on its own.

If that is not practical for some reason, you should be able to declare it as a hidden import using a spec file. You may have a spec file from an earlier build, but if you need to create a new one you can do that with something like this:

pyi-makespec GUI.py

Then edit the spec file to add dummy_script to the list of hidden imports:

a = Analysis(['GUI.py'],
# ...
hiddenimports=['dummy_script'],
# ...,
)

Then build again from the modified spec file:

pyinstaller foo.spec

That may not work either, since you still aren't importing the other module. In that case you may need to declare it as a data file instead.



Related Topics



Leave a reply



Submit