Packaging and Distributing a Kivy application on Linux

The Kivy library can be used to create cross-platform desktop and mobile apps that can be distributed on Linux, OS X, Windows, iOS, and Android. Packaging Kivy apps on Linux is not well-supported. Here is how to get it to work:

1. Use a Vagrantfile to bootstrap a development environment

There are a lot of dependencies for kivy development, and a lot of different ways to install them. In order to help you get started, I made a Vagrantfile to set up a Kivy dev environment.

The Vagrantfile installs python, kivy, and pyinstaller in an Ubuntu VM, and then packages a kivy example app into a .deb. You end up with a double-clickable application that works like a regular linux desktop app.

The Vagrantfile and instructions on how to use it are here: https://github.com/rajbot/kivy_pyinstaller_linux_example


2. Install Kivy using pip, and not from the provided PPA

If you don't use the Vagrantfile above and want to install Kivy yourself, do not install it using the PPA. This is because we are going to use PyInstaller to create a linux executable, and we will need the PyInstaller hooks from the kivy.tools package, but the PPA does not include kivy.tools.

This bash script will show you how to install Kivy in a virtualenv so that you can use the PyInstaller hooks.


3. Strip system libraries from the pyinstaller executable to ensure your app is relocatable

If you follow the setup instructions above, you will end up an executable that works only on the machine it was built. If you try to copy it to another linux box, kivy will often segfault on startup with this error message:

Fatal Python Error: (pygame parachute) Segmentation Fault

To ensure the executable can run on as many different flavors of linux as possible, we are going to strip out all binaries provided by system packages. We will distribute the application in a .deb file and and use .deb dependencies to ensure required libraries are installed on the target machine.

Here is a copy of a PyInstaller .spec file that installs Kivy hooks and strips out all binaries that dpkg -S finds in a system-installed library:

# -*- mode: python -*-
from kivy.tools.packaging.pyinstaller_hooks import install_hooks
install_hooks(globals())

def filter_binaries(all_binaries):
    '''Exclude binaries provided by system packages, and rely on .deb dependencies
    to ensure these binaries are available on the target machine.

    We need to remove OpenGL-related libraries so we can distribute the executable
    to other linux machines that might have different graphics hardware. If you
    bundle system libraries, your application might crash when run on a different
    machine with the following error during kivy startup:

    Fatal Python Error: (pygame parachute) Segmentation Fault

    If we strip all libraries, then PIL might not be able to find the correct _imaging
    module, even if the `python-image` package has been installed on the system. The
    easy way to fix this is to not filter binaries from the python-imaging package.

    We will strip out all binaries, except libpython2.7, which is required for the
    pyinstaller-frozen executable to work, and any of the python-* packages.
    '''

    print 'Excluding system libraries'
    import subprocess
    excluded_pkgs  = set()
    excluded_files = set()
    whitelist_prefixes = ('libpython2.7', 'python-')
    binaries = []

    for b in all_binaries:
        try:
            output = subprocess.check_output(['dpkg', '-S', b[1]], stderr=open('/dev/null'))
            p, path = output.split(':', 2)
            if not p.startswith(whitelist_prefixes):
                excluded_pkgs.add(p)
                excluded_files.add(b[0])
                print ' excluding {f} from package {p}'.format(f=b[0], p=p)
        except Exception:
            pass

    print 'Your exe will depend on the following packages:'
    print excluded_pkgs

    inc_libs = set(['libpython2.7.so.1.0'])
    binaries = [x for x in all_binaries if x[0] not in excluded_files]
    return binaries


a = Analysis(['scribe.py'],
             pathex=['.'],
             hiddenimports=[],
            )
pyz = PYZ(a.pure)

binaries = filter_binaries(a.binaries)

exe = EXE(pyz,
          [('scribe.kv', 'scribe.kv', 'DATA')],
          a.scripts,
          binaries, #a.binaries,
          a.zipfiles,
          a.datas,
          name='ia-scribe',
          debug=False,
          strip=None,
          upx=True,
          console=False )


4. Package your executable in a .deb file

Once you make an executable, you can give it a nice icon and Ubuntu .desktop file and package it in a .deb for distribution. Steps to create the .deb can be found in bootstrap.sh, from step #1 above. At minimum, your .deb package should:

  • Install your application in a binary directory, such as /usr/local/bin/my-app
  • Install an icon in /usr/share/pixmaps/my-app.png
  • Install a .desktop file in /usr/share/applications/my-app.desktop

Debian version numbers are in the form {major}.{minor}-{patchlevel}. To make the .deb, first create the directory structure below:

- my-app_1.0-1
    - DEBIAN
        - control
    - usr
        - local
            - bin
                - my-app
        - share
            - applications
                - my-app.desktop
            - pixmaps
                - my-app.png

Now you can package your app by typing dpkg-deb --build myapp_1.0-0. You can then install the resulting package by typing sudo dpkg -i myapp_1.0-0.deb.

The DEBIAN/control file should look like this:

Source: my-app
Priority: extra
Maintainer: raj <raj@unknown>
Build-Depends: debhelper (>= 8.0.0)
Standards-Version: 3.9.2
Package: my-app
Version: 1.0-0
Architecture: i386
Description: Should description
 Long description string (starts with a whitespace)

To give your executable a first-class Ubuntu application, you will need to create a .desktop file, which will tell Ubuntu about its icon, version, and name. my-app.desktop should look like this:

[Desktop Entry]
Version=1.0
Name=My Application
Comment=Example App
Exec=/usr/local/bin/my-app
Icon=my-app
Type=Application
Categories=Utility;Application;

Note that Version above refers to the version of the .desktop format (and not the version of the app), and should always be "1.0". The Icon entry does not need a full path or extension. Ubuntu will look for your icon in /usr/share/pixmaps.


5. Set up a signed trivial APT repository to distribute your .deb

To distribute your .deb file to end users, you will want to set up an APT repository, which must be signed with a GPG key if you want to allow for programatic installation or automatic updates.

Instructions for setting up a signed trivial repo are provided here.

blogroll

social