Skip to content

gh-106111: Remove zipapp documentation on creating a Windows executable #106112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 26, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 9 additions & 106 deletions Doc/library/zipapp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -303,115 +303,18 @@ the Python interpreter registers the ``.pyz`` and ``.pyzw`` file extensions
when installed.


Making a Windows executable
~~~~~~~~~~~~~~~~~~~~~~~~~~~

On Windows, registration of the ``.pyz`` extension is optional, and
furthermore, there are certain places that don't recognise registered
extensions "transparently" (the simplest example is that
``subprocess.run(['myapp'])`` won't find your application - you need to
explicitly specify the extension).

On Windows, therefore, it is often preferable to create an executable from the
zipapp. This is relatively easy, although it does require a C compiler. The
basic approach relies on the fact that zipfiles can have arbitrary data
prepended, and Windows exe files can have arbitrary data appended. So by
creating a suitable launcher and tacking the ``.pyz`` file onto the end of it,
you end up with a single-file executable that runs your application.

A suitable launcher can be as simple as the following::

#define Py_LIMITED_API 1
#include "Python.h"

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#ifdef WINDOWS
int WINAPI wWinMain(
HINSTANCE hInstance, /* handle to current instance */
HINSTANCE hPrevInstance, /* handle to previous instance */
LPWSTR lpCmdLine, /* pointer to command line */
int nCmdShow /* show state of window */
)
#else
int wmain()
#endif
{
wchar_t **myargv = _alloca((__argc + 1) * sizeof(wchar_t*));
myargv[0] = __wargv[0];
memcpy(myargv + 1, __wargv, __argc * sizeof(wchar_t *));
return Py_Main(__argc+1, myargv);
}

If you define the ``WINDOWS`` preprocessor symbol, this will generate a
GUI executable, and without it, a console executable.

To compile the executable, you can either just use the standard MSVC
command line tools, or you can take advantage of the fact that distutils
knows how to compile Python source::

>>> from distutils.ccompiler import new_compiler
>>> import distutils.sysconfig
>>> import sys
>>> import os
>>> from pathlib import Path

>>> def compile(src):
>>> src = Path(src)
>>> cc = new_compiler()
>>> exe = src.stem
>>> cc.add_include_dir(distutils.sysconfig.get_python_inc())
>>> cc.add_library_dir(os.path.join(sys.base_exec_prefix, 'libs'))
>>> # First the CLI executable
>>> objs = cc.compile([str(src)])
>>> cc.link_executable(objs, exe)
>>> # Now the GUI executable
>>> cc.define_macro('WINDOWS')
>>> objs = cc.compile([str(src)])
>>> cc.link_executable(objs, exe + 'w')

>>> if __name__ == "__main__":
>>> compile("zastub.c")

The resulting launcher uses the "Limited ABI", so it will run unchanged with
any version of Python 3.x. All it needs is for Python (``python3.dll``) to be
on the user's ``PATH``.

For a fully standalone distribution, you can distribute the launcher with your
application appended, bundled with the Python "embedded" distribution. This
will run on any PC with the appropriate architecture (32 bit or 64 bit).


Caveats
~~~~~~~

There are some limitations to the process of bundling your application into
a single file. In most, if not all, cases they can be addressed without
needing major changes to your application.

1. If your application depends on a package that includes a C extension, that
package cannot be run from a zip file (this is an OS limitation, as executable
code must be present in the filesystem for the OS loader to load it). In this
case, you can exclude that dependency from the zipfile, and either require
your users to have it installed, or ship it alongside your zipfile and add code
to your ``__main__.py`` to include the directory containing the unzipped
module in ``sys.path``. In this case, you will need to make sure to ship
appropriate binaries for your target architecture(s) (and potentially pick the
correct version to add to ``sys.path`` at runtime, based on the user's machine).

2. If you are shipping a Windows executable as described above, you either need to
ensure that your users have ``python3.dll`` on their PATH (which is not the
default behaviour of the installer) or you should bundle your application with
the embedded distribution.

3. The suggested launcher above uses the Python embedding API. This means that in
your application, ``sys.executable`` will be your application, and *not* a
conventional Python interpreter. Your code and its dependencies need to be
prepared for this possibility. For example, if your application uses the
:mod:`multiprocessing` module, it will need to call
:func:`multiprocessing.set_executable` to let the module know where to find the
standard Python interpreter.
If your application depends on a package that includes a C extension, that
package cannot be run from a zip file (this is an OS limitation, as executable
code must be present in the filesystem for the OS loader to load it). In this
case, you can exclude that dependency from the zipfile, and either require
your users to have it installed, or ship it alongside your zipfile and add code
to your ``__main__.py`` to include the directory containing the unzipped
module in ``sys.path``. In this case, you will need to make sure to ship
appropriate binaries for your target architecture(s) (and potentially pick the
correct version to add to ``sys.path`` at runtime, based on the user's machine).


The Python Zip Application Archive Format
Expand Down