Again, this is probably just my OCD playing up, but I want to present a clean directory to my end users. Instead of this:
I want something more like this:
where 'lib' contains all the files and directories from the first screenshot that my app depends on. Unfortunately, there isn’t an easy way to get PyInstaller to do this. You can get most of the way there with only a little messing about though.
So the general idea is that we have a ‘lib’ directory (or whatever you want to call it) that contains the dependencies but that doesn’t contain the executable. If you just create one and move all the dependencies into it you’ll get errors at runtime because the dependencies can’t be found – nothing knows about your new lib directory. So the main thing you need to do is add your lib directory to the sys.path. You can’t do this in your main application module though because by the time that is run it is too late – the bootstrapping code will import stuff before your main script is run. So the way to do it is with a runtime hook. This is just a Python module; call it whatever you want and put something like the following in it:
import sysTo have PyInstaller use this you can just pass the path to it as an argument to pyinstaller (alternatively you can specify it in your .spec file):
import os
sys.path.append(os.path.join(os.path.dirname(sys.argv[0]), "lib"))
pyinstaller --onedir --windowed --runtime-hook=".\_pyinstaller_hooks\runtime_hooks\use_lib.py" …
Happily, this gets us most of the way there. Playing with this, by manually moving the built files about, I find that I can move everything into the lib directory except python27.dll (which makes sense), the ‘eggs’ directory and the PySide .pyd files. If you’re not using PySide then that last obviously won’t be an issue, so with a bit of luck just by amending the path in the runtime hook you should be able to make your ‘one dir’ distribution look like this:
which I’m sure you’ll agree is a lot nicer than the default. You’ll need to experiment to see whether the dependencies in your application can tolerate being moved into a separate directory from the executable; as noted PySide doesn’t like it (and I can’t find an easy way to fix this) but other things like NumPy seem to be perfectly happy. In case it’s not obvious, since PyInstaller isn’t set up to build this way out of the box you’ll need to move the files yourself – I just use a batch file that firstly calls pyinstaller to build the ‘one dir’ and then creates the lib directory and moves the relevant stuff into it. Finally, I’ve only tried this on Windows.
Update 15th August 2014
Here's the contents of my .spec file, as requested by Rishi Sharma:
# -*- mode: python -*-
a = Analysis(['..\\src\\my_app.py'],
pathex=['C:\\dev\\python_projects\\my_app\\dist'],
hiddenimports=[],
hookspath=['.\\_pyinstaller_hooks\\additional_hooks'],
runtime_hooks=['.\\_pyinstaller_hooks\\runtime_hooks\\use_lib.py'])
pyz = PYZ(a.pure)
exe = EXE(pyz,
a.scripts,
exclude_binaries=True,
name='my_app.exe',
debug=False,
strip=None,
upx=True,
console=True )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=None,
upx=True,
name='my_app')
Note that this is just what pyinstaller automatically generates when I call it with the arguments shown above - I haven't tweaked it. The part highlighted shows how to set the hook and runtime hook paths - obviously you'll need to adjust it for your own paths. Hope this helps.
Would you mind posting an example of a spec file? I'd like to understand how to include what you specified without explicitly using the runtime-hook flag (for older versions of pyinstaller).
ReplyDeleteHi Rishi, I've updated the post with the contents of the auto-generated .spec file - hope this is useful.
DeleteJust want to say this helps a lot
ReplyDelete