Google Summer of Code 2016 Blogpost 9 - Deploying a Kivy Application with PyInstaller for Mac OSX with Travis CI to Github
In this sprint for the kniteditor library we focused on automatic deployment for Windows and Mac. The idea: whenever a tag is pushed to Github, a new travis build is triggered. The new built app is uploaded to Github as an “.dmg” file.
Travis
Travis is configured with the “.travis.yml” file which you can see here:
language: python
# see https://docs.travis-ci.com/user/multi-os/
matrix:
include:
- os: linux
python: 3.4
- os: osx
language: generic
allow_failures:
- os: osx
install:
- if [ "$TRAVIS_OS_NAME" == "osx" ] ; then mac-build/install.sh ; fi
script:
- if [ "$TRAVIS_OS_NAME" == "osx" ] ; then mac-build/test.sh ; fi
before_deploy:
- if [ "$TRAVIS_OS_NAME" == "osx" ] ; then cp mac-build/dist/KnitEditor.dmg /Users/travis/KnitEditor.dmg ; fi
deploy:
# see https://docs.travis-ci.com/user/deployment/releases/
- provider: releases
api_key:
secure: v18ZcrXkIMgyb7mIrKWJYCXMpBmIGYWXhKul4/PL/TVpxtg2f/zfg08qHju7mWnAZYApjTV/EjOwWCtqn/hm2CfPFo=
file: /Users/travis/KnitEditor.dmg
on:
tags: true
condition: "\"$TRAVIS_OS_NAME\" == \"osx\""
repo: AllYarnsAreBeautiful/kniteditor
Note that it builds both Linux and OSX. Thus, for each step one must distinguish. Here, only the OSX parts are shown. These steps are executed:
- Installation. The app and dmg files are built.
- Testing. The tests are shipped with the app in our case. This allows us to execute them at many more locations – where the user is.
- Before Deploy. Somehow Travis did not manage to upload from the original location. Maybe it was a bug. Thus, a absolute path was created for the use in (4).
- Deployment to github. In this case we use an API key. One could also use a password.
Installation:
#!/bin/bash
#
# execute with --user to pip install in the user home
#
set -e
HERE="`dirname \"$0\"`"
USER="$1"
cd "$HERE"
brew update
echo "# install python3"
brew install python3
echo -n "Python version: "
python3 --version
python3 -m pip install --upgrade pip
echo "# install pygame"
python3 -m pip uninstall -y pygame || true
# locally compiled pygame version
# see https://bitbucket.org/pygame/pygame/issues/82/homebrew-on-leopard-fails-to-install#comment-636765
brew install sdl sdl_image sdl_mixer sdl_ttf portmidi
brew install mercurial || true
python3 -m pip install $USER hg+http://bitbucket.org/pygame/pygame
echo "# install kivy dependencies"
brew install sdl2 sdl2_image sdl2_ttf sdl2_mixer gstreamer
echo "# install requirements"
python3 -m pip install $USER -I Cython==0.23 \
--install-option="--no-cython-compile"
USE_OSX_FRAMEWORKS=0 python3 -m pip install $USER kivy
python3 -m pip uninstall -y Cython==0.23
python3 -m pip install $USER -r ../requirements.txt
python3 -m pip install $USER -r ../test-requirements.txt
python3 -m pip install $USER PyInstaller
./build.sh $USER
The first step is to update brew. It cost me 4 hours to find this bug, 2 hours to work around it before. If brew is not updated, Python 3.4 is installed instead of Python 3.5.
Then, Python, Pygame as the window provider for Kivy is installed, and the other requirements. It goes on with the build step. While installation is executed once on a personal Mac, the build step is executed several times, when the source code is changed.
#!/bin/bash
#
# execute with --user to make pip install in the user home
#
set -e
HERE="`dirname \"$0\"`"
USER="$1"
cd "$HERE"
(
cd ..
echo "# removing old installation of kniteditor"
python3 -m pip uninstall -y kniteditor || true
echo "# build the distribution"
python3 -m pip install $USER wheel
python3 setup.py sdist --formats=zip
python3 setup.py bdist_wheel
python3 -m pip uninstall -y wheel
echo "# install the current version from the build"
python3 -m pip install $USER --upgrade dist/kniteditor-`linux-build/package_version`.zip
echo "# install test requirements"
python3 -m pip install $USER --upgrade -r test-requirements.txt
)
echo "# build the app"
# see https://pythonhosted.org/PyInstaller/usage.html
python3 -m PyInstaller -d -y KnitEditor.spec
echo "# create the .dmg file"
# see http://stackoverflow.com/a/367826/1320237
KNITEDITOR_DMG="`pwd`/dist/KnitEditor.dmg"
rm -f "$KNITEDITOR_DMG"
hdiutil create -srcfolder dist/KnitEditor.app "$KNITEDITOR_DMG"
echo "The installer can be found in \"$KNITEDITOR_DMG\"."
In the first steps we install the kniteditor from the built “sdist” zip file. This way we can uninstall it with pip. Also, the dependencies are installed. Then, PyInstaller is invoked with a spec and then the .dmg file is created.
The spec looks like this:
# -*- mode: python -*-
import sys
site_packages = [path for path in sys.path if path.rstrip("/\\").endswith('site-packages')]
print("site_packages:", site_packages)
from kivy.tools.packaging.pyinstaller_hooks import get_deps_all, \
hookspath, runtime_hooks
block_cipher = None
added_files = [(site_packages_, ".") for site_packages_ in site_packages]
kwargs = get_deps_all()
kwargs["datas"] = added_files
kwargs["hiddenimports"] += ['queue', 'unittest', 'unittest.mock']
a = Analysis(['_KnitEditor.py'],
pathex=[],
binaries=None,
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
hookspath=hookspath(),
runtime_hooks=runtime_hooks(),
**kwargs)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
exclude_binaries=True,
name='KnitEditorX',
debug=False,
strip=False,
upx=True,
console=True )
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
name='KnitEditor')
app = BUNDLE(coll,
name='KnitEditor.app',
icon=None,
bundle_identifier="com.ayab-knitting.KnitEditor")
Note, that all files in all site-packages are included. This way, we do not need to cope with missing modules. Also, there are three different names for
- the entry script “_KnitEditor.py”
- the executable “KnitEditorX”
- the library “kniteditor”
While on the command line, OSX is case sensitive, it is not sensitive on the file system. Thus, if one of the names is the same, we can get errors durig the PyInstaller build.
Lessons learned
Do “brew update” on travis.
Use absolute paths for deployment on Mac OSX travis.
Never use the same names in PyInstaller for the main script, a library and the executable. Otherwise you get a “not a directory” or “not a file” error.
Travis OSX build max out from time to time. It is much faster to have a Mac computer there, to create the scripts.
— Nicco Kunzmann