Skip to content

Commit 8583ff0

Browse files
Merge pull request #34 from jonathanrocher/feat/pyinstaller-distribution
PyInstaller distributions
2 parents a33ad38 + 9861f77 commit 8583ff0

File tree

9 files changed

+456
-0
lines changed

9 files changed

+456
-0
lines changed
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
# Distributable Applications
2+
3+
What you have now is a working application on your computer. The problem that
4+
you face is how to distribute this application to your colleagues so that they
5+
can also use your work.
6+
7+
If they are comfortable with Python and tools like `git` and `pip` you may be
8+
able to just give a list of commands to download the code, install an
9+
environment and run the application.
10+
11+
But a major advantage of GUI applications is that they are intended to be
12+
accessible by users who are not as knowledgeable about coding. For this reason
13+
it's desirable to be able to provide them with simple tools which either
14+
install an application and its environment, or which look and behave like
15+
ordinary applications on their operating system.
16+
17+
A number of solutions are available for this sort of operation, including
18+
some commercial solutions from Anaconda and Enthought. Tools which you might
19+
consider using include:
20+
21+
- [PyOxidizer](https://pyoxidizer.readthedocs.io/en/stable/), which
22+
is comprehensive but complex and written in Rust.
23+
- [Py2App](https://py2app.readthedocs.io/en/latest/), which is aimed at
24+
MacOS applications specifically
25+
- [BeeWare](https://beeware.org/)'s [Briefcase](https://briefcase.readthedocs.io/en/latest/)
26+
which is new and somewhat incomplete, but aims to be deployable on all
27+
platforms including iOS and Android.
28+
29+
In this section we will use [PyInstaller](https://pyinstaller.org/en/stable/)
30+
because it works on all major desktop platforms and is fairly mature.
31+
32+
## PyInstaller Basics
33+
34+
At its simplest, using PyInstaller is just a matter of installing pyinstaller
35+
with `pip`:
36+
```
37+
pip install -U pyinstaller
38+
```
39+
changing to the directory of your program, and running
40+
```
41+
pysinstaller my_script.py
42+
```
43+
PyInstaller will create an application file (.exe if on Windows, executable if
44+
on POSIX systems) and place it in a `dist/` folder
45+
next to your application. You can run this executable as a command from the
46+
command-line, or by finding the icon in your OS file browser and opening it
47+
that way.
48+
49+
PyInstaller tries to analyse your code and only include modules that it knows
50+
that your application will use, to make the resulting application file as small
51+
as possible. This is magical, and as with all magical things there are a lot
52+
of options and things to tweak to make sure that the magic works.
53+
54+
### OS and Environment
55+
56+
PyInstaller must be run in a working Python environment containing your
57+
application code and dependencies. This unfortunately means that you can't
58+
build for a different OS or CPU architecture; indeed in some cases you may
59+
not be able to build an application for an earlier version of the OS you use.
60+
If you have users on many different systems, you may need to have a collection
61+
of virtual machines (or even physical machines) configured appropriately for
62+
performing the builds.
63+
64+
PyInstaller has some additional options for Windows and MacOS to help support
65+
the particular idiosyncracies of each.
66+
67+
### Single Directory vs. Single File
68+
69+
PyInstaller gives you a choice between building an application as an
70+
executable plus auxiliary files as a single directory, or as a single
71+
executable file. While the single file is nicer, getting it to work can be
72+
more difficult. For simplicity, we'll use the default single directory
73+
approach for this tutorial. You can use zip or a similar utility to bundle
74+
this for easier distribution: most users are comfortable with unzipping
75+
a bundle that they have been given before running it.
76+
77+
### Windowed vs. Console
78+
79+
On Windows and MacOS, PyInstaller needs to be told whether the application
80+
needs a text-based terminal window available for input or displaying things
81+
to the user (even if it opens a GUI) or a purely windowed application.
82+
These are controlled by the `--console` and `--windowed` command-line options.
83+
84+
## Common Problems
85+
86+
Because Python is a very dynamic language, it can be difficult or impossible
87+
for PyInstaller to work out exactly what it needs to include in the
88+
application bundle.
89+
90+
### Data Files
91+
92+
It's particularly common for GUI applications to have additional files that
93+
they need to operate. The most common are image files for icons and logos,
94+
but can include things like HTML files containing documentation, data files,
95+
or trained machine learning model files.
96+
97+
PyInstaller has no way of knowing if any non-Python files are needed, and so
98+
it needs to be told. This includes any such files needed by libraries that
99+
your application uses.
100+
101+
### Dynamic Imports
102+
103+
Python is a very dynamic language and can import modules by methods other than
104+
the standard `import` statement. This is commonly used by libraries that need
105+
to decide what code to use based on the environment that they find themselves
106+
in. For example libraries like Pyface, TraitsUI and Matplotlib will look at
107+
environment variables and/or try importing GUI library dependencies like Qt to
108+
determine which one they should use. This is very convenient for people who
109+
are writing and distributing scripts, but it makes it difficult for
110+
PyInstaller to perform its import analysis.
111+
112+
### Entry Points
113+
114+
Related to dynamic importing, many Python libraries use "entry points" to
115+
advertise capabilities to other code. You most likely have seen this in a
116+
package setup file where you may have seen or used lines like:
117+
```
118+
entry_points={
119+
'console_scripts': [
120+
'my_script = my_package.my_script:main',
121+
],
122+
}
123+
```
124+
which advertise that the package has a command-line script `my_script` that
125+
can be run from the `main` function in `my_package.my_script` and Python tools
126+
will ensure that these are made available when you install them into a Python
127+
environment. However they are a much more general mechanism which can be used
128+
for building general "plugin" capabilities for Python libraries.
129+
130+
Amir Rachum has a [good blog post](https://amir.rachum.com/blog/2017/07/28/python-entry-points/)
131+
from a few years back that explains why you might use or care about entry
132+
points.
133+
134+
Entry points used to be part of the `setuptools` library, but since Python 3.8
135+
they are now available via the
136+
[imporlib.metadata](https://docs.python.org/3/library/importlib.metadata.html)
137+
standard library module.
138+
139+
Again, the problem for PyInstaller is that it can't detect use of code that
140+
comes from entry points, but in addition PyInstaller doesn't expose the entry
141+
points for libraries that it wraps by default. So if you include code that
142+
expects to load capabilities via this mechanism they will fail even if the
143+
required code is packaged unless you tell PyInstaller about the entry points.
144+
145+
## Spec Files and Hook Files
146+
147+
While many of these problems can be corrected via the use of appropriate
148+
command-line options, once you have any level of complexity you will want to
149+
be putting these into something more repeatable and editable. PyInstaller
150+
has two mechanisms for this:
151+
152+
- `.spec` files, which are Python files which hold the build instructions for
153+
an application as a whole
154+
- `hook-` files, which are Python files which hold information about a
155+
particular package and how it should work with PyInstaller
156+
157+
### Spec Files
158+
159+
When you run `pysinstaller my_script.py`, PyInstaller first creates a
160+
corresponding `my_script.spec` file using the command-line options passed in
161+
and runs the code in that file. You can also create a `.spec` file using the
162+
`pyi-makespec` commands. Once you have a `.spec` file, you can instead run
163+
```
164+
pyinstaller my_spec.spec
165+
```
166+
and it will use the options specified there. If you used the `.spec` file
167+
created automatically, it is probably a good idea to rename it so it doesn't
168+
accidentally get overwritten if you run `pysinstaller my_script.py` again.
169+
170+
The contents of the file might look something like this (depending on the
171+
options used to generate it):
172+
```
173+
block_cipher = None
174+
a = Analysis(
175+
['my_script.py'],
176+
pathex=['/Developer/PItests/minimal'],
177+
binaries=None,
178+
datas=None,
179+
hiddenimports=[],
180+
hookspath=None,
181+
runtime_hooks=None,
182+
excludes=None,
183+
cipher=block_cipher,
184+
)
185+
pyz = PYZ(
186+
a.pure,
187+
a.zipped_data,
188+
cipher=block_cipher,
189+
)
190+
exe = EXE(pyz,... )
191+
coll = COLLECT(...)
192+
```
193+
Most of this you can ignore for simple usage, but there are a few things that
194+
you can use to fix the problems listed above:
195+
196+
- adding data files: the `datas` argument to the `Analysis` function expects
197+
a list of `(source, dest)` tuples that tell PyInstaller to include the
198+
file(s) at the `source` path in your code in the directory specified by
199+
`dest` in your application. This understands basic "glob"-style wildcards.
200+
201+
Example: `datas=[("docs/build/html", "documentation"), ("*.txt", ".")]`
202+
would:
203+
204+
- take the `html` folder as typically built by Sphinx and add it into your
205+
application distribution as a subdirectory called "documentation".
206+
207+
- take all files with the `.txt` suffix in the main script directory and add
208+
them to the top-level application directory alongside the executable.
209+
210+
- adding dynamic imports: the `hiddenimports` argument expects a list of
211+
additional module names to be added to the modules packaged into the
212+
application.
213+
214+
Example: `hiddenimports=["my_package.editors.qt"]` would add the
215+
`my_package.editors.qt` module and everything that imports to the packaged
216+
set of modules.
217+
218+
- adding binary dependencies: PyInstaller is usually fairly good about
219+
detecting and adding Python C extensions, but if it fails to correctly find
220+
the extension (or its dependencies) you can use the `binaries` argument
221+
to supply a list of DLLs or folders containing DLLs (or the OS-specific
222+
equivalents) that you want added to the application. These are specified
223+
as `(source, dest)` pairs as for data files.
224+
225+
- specifying a list of additional places to look for hook files with the
226+
`hookspath` argument.
227+
228+
Manually listing all of these files can be tedious, and so there are several
229+
utility methods that are available to help populate these lists.
230+
231+
### Hook files
232+
233+
Hook files are similar to `.spec` files, but are instead Python files that
234+
have a name in the pattern `hook-package.name.py` and which are expected to
235+
provide particular information that `package.name` needs to correctly work in
236+
a PyInstaller application. The idea is that these can be defined once for a
237+
given library and then shared by all the applications which use this library.
238+
239+
PyInstaller comes with built-in support for some common libraries which
240+
require additional support, such as Matplotlib: you should not need to do
241+
any additional work to build an application which used matplotlib in a
242+
standard way, for example. Additionally the
243+
[PyInstaller hooks repository](https://github.com/pyinstaller/pyinstaller-hooks-contrib)
244+
has additional community-contributed hook files for popular packages, which
245+
are `pip`-installable as `pyinstaller-hooks-contrib`.
246+
247+
However, if you are the author of, or want to use, a library that needs
248+
additional support, then writing a hookfile may be easier than adding things
249+
to a `.spec` file.
250+
251+
Hook files are expected to populate certain global variables in the module
252+
with appropriate values:
253+
254+
- `datas`: a list of data files in `(source, dest)` form as described above.
255+
256+
- `hiddenimports`: a list of additional modules to include in the package.
257+
258+
- `binaries`: a list of binary files to include in `(source, dest)` form as
259+
described above.
260+
261+
### `PyInstaller.utils.hooks`
262+
263+
Specifying all of the extra files to include can be tedious and error-prone,
264+
particularly as a library or application change over time. PyInstaller has
265+
some utility files which make it easier to write `.spec` and hook files. Most
266+
of these can be found in the `PyInstaller.utils.hooks` module, which can be
267+
imported in a `.spec` or hook file as you would for a normal Python file.
268+
269+
- `collect_data_files`: In the simplest form, you pass this the name of a
270+
Python package and it will generate a list of `(source, dest)` paths for all
271+
non-Python files in the package, suitable for inclusion in a `datas` list.
272+
273+
- `collect_submodules`: This will create a list of all submodules of a given
274+
module in a form suitable for use with the `hiddenimports` list. Submodules
275+
will be included whether or not they are imported.
276+
277+
- `collect_entry_point`: This inspects the given entry point and returns a
278+
list of `datas` and a list of `hiddenimports` that are needed to support the
279+
use of that entry point.
280+
281+
- `collect_dynamic_libs`: This finds all DLLs included in a given package.
282+
283+
## Grace Notes
284+
285+
By default on Windows and Mac OS the application's desktop icon will be the
286+
default PyInstaller icon. While this is fine in development, it is useful for
287+
users to have a distinct icon on the desktop. This can be provided using the
288+
`--icon` command-line option. It can also be specified in a `.spec` file.
289+
290+
For MacOS in particular you can include additional information for the
291+
`.plist` file inside a Mac app bundle.
292+
293+
## Distributing ETS Applications
294+
295+
The ETS libraries require the use of a number of these tools to build
296+
applications with PyInstaller.
297+
298+
- many of the libraries have data files,
299+
300+
- dynamic importing is used, particularly to choose between Qt and Wx backend
301+
implementations,
302+
303+
- availability of toolkits and other functionality are advertised via entry
304+
points.
305+
306+
The easiest way to overcome these is to simply use the `collect_...` methods
307+
to gather everything from the top-level package. For example, the following
308+
code will gather everything that is needed for TraitsUI:
309+
```
310+
from PyInstaller.utils.hooks import (
311+
collect_data_files, collect_entry_point, collect_submodules
312+
)
313+
314+
data, hiddenimports = collect_entry_point("traitsui.toolkits")
315+
data += collect_data_files("traitsui")
316+
hiddenimports += collect_submodules("traitsui")
317+
```
318+
319+
This will include code that your application does not use, but it is unlikely
320+
that the extra size of the resulting application from this will pose a
321+
problem.
322+
323+
This directory includes hook files suitable for use with the core ETS
324+
libraries.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
This software is OSI Certified Open Source Software.
2+
OSI Certified is a certification mark of the Open Source Initiative.
3+
4+
Copyright (c) 2022, Enthought, Inc.
5+
All rights reserved.
6+
7+
Redistribution and use in source and binary forms, with or without
8+
modification, are permitted provided that the following conditions are met:
9+
10+
* Redistributions of source code must retain the above copyright notice, this
11+
list of conditions and the following disclaimer.
12+
* Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
* Neither the name of Enthought, Inc. nor the names of its contributors may
16+
be used to endorse or promote products derived from this software without
17+
specific prior written permission.
18+
19+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# (C) Copyright 2022 Enthought, Inc., Austin, TX
2+
# All rights reserved.
3+
#
4+
# This software is provided without warranty under the terms of the BSD
5+
# license included in LICENSE_Enthought.txt and may be redistributed only
6+
# under the conditions described in the aforementioned license. The license
7+
# is also available online at http://www.enthought.com/licenses/BSD.txt
8+
#
9+
# Thanks for using Enthought open source!
10+
11+
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
12+
13+
data = collect_data_files("apptools")
14+
hiddenimports = collect_submodules("apptools")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# (C) Copyright 2022 Enthought, Inc., Austin, TX
2+
# All rights reserved.
3+
#
4+
# This software is provided without warranty under the terms of the BSD
5+
# license included in LICENSE_Enthought.txt and may be redistributed only
6+
# under the conditions described in the aforementioned license. The license
7+
# is also available online at http://www.enthought.com/licenses/BSD.txt
8+
#
9+
# Thanks for using Enthought open source!
10+
11+
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
12+
13+
data = collect_data_files("enable")
14+
hiddenimports = collect_submodules("enable")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# (C) Copyright 2022 Enthought, Inc., Austin, TX
2+
# All rights reserved.
3+
#
4+
# This software is provided without warranty under the terms of the BSD
5+
# license included in LICENSE_Enthought.txt and may be redistributed only
6+
# under the conditions described in the aforementioned license. The license
7+
# is also available online at http://www.enthought.com/licenses/BSD.txt
8+
#
9+
# Thanks for using Enthought open source!
10+
11+
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
12+
13+
data = collect_data_files("envisage")
14+
hiddenimports = collect_submodules("envisage")

0 commit comments

Comments
 (0)