Bundling macOS Applications with Platypus

So I know that Apple is pushing all app developers to the App Store, but I’ve recently been playing around with Platypus for the Connected Learning Initiative project that I’m working on. I was curious how to bundle together a macOS application, but didn’t want to go through XCode or the Apple Developer Program.

Platypus is pretty nifty because it organizes all of the files and scripts for you, into the appropriate .app folder structure. There were a couple of things that I couldn’t find in the documentation, however, that I wound up having to figure out:

Our bundle actually consists of running two executables on program launch (one that acts as an assessment engine, another as an ePub reader). However, you can only launch one script when you open the app. I wound up creating an AppleScript to open up two instances of Terminal and running one executable per instance. And apparently spaces in Terminal have to be double-forward-slash-escaped instead of single (i.e. \\ instead of the typical command line \):

#!/bin/bash
EPUB_READER_SCRIPT=`pwd`/epub_reader_executable
EPUB_READER_SCRIPT=${EPUB_READER_SCRIPT// /\\\\ }
echo Running ePub reader from $EPUB_READER_SCRIPT
osascript -e "tell app \"Terminal\" to do script \"$EPUB_READER_SCRIPT\""
sleep 3

ASSESSMENT_ENGINE_SCRIPT=`pwd`/assessment_engine_executable
ASSESSMENT_ENGINE_SCRIPT=${ASSESSMENT_ENGINE_SCRIPT// /\\\\ }
echo Running assessment engine from $ASSESSMENT_ENGINE_SCRIPT
osascript -e "tell app \"Terminal\" to do script \"$ASSESSMENT_ENGINE_SCRIPT\""
sleep 3

/usr/bin/open -a "/Applications/Google Chrome.app" 'https://localhost:8888/'

One other challenge was finding bundled data files. I was including static files, ePubs, and JSON data files in the application bundle, since it is meant to be a standalone LMS / learning application, and I couldn’t figure out why my scripts couldn’t find the data. The Platypus documents indicate scripts in the app bundle can reference other files in the bundle relative to themselves. The directory structure inside of /Content/Resources was:

main_script.py
database.sqlite3
data_files/
    foo.json

However, because I was launching new Terminal instances, the Python scripts inside of your .app bundle had the current working directory set to the user’s home directory (Terminal default). For example, in my main_script.py file, os.getcwd() would return the user’s home directory (/Users/) instead of the app’s /Content/Resources directory.

I wound up having to do something like this in my main_script.py:

import os

ABS_PATH = os.path.dirname(os.path.abspath(__file__))
os.chdir(ABS_PATH)

in order to find and read files relative to my main script.

Tornado and Django — serving static content

I recently inherited a project for CLIx, a Django app running off of a Tornado WSGI server. Everything seemed to run fine, until we started getting reports that video and audio files were not playing correctly. It turns out that the original app was using Django to serve static files — not quite recommended for production use. This worked fine for small files like .html and .vtt, but larger files would not stream (.mp4, .mp3). You could not seek videos, and pausing / waiting / playing again would cause the video to re-play from the beginning.

In the Chrome dev tools, we could see that only part of the files were loading, but nothing else. So I decided to make Tornado serve the static content … not a lot of documentation about doing this. Luckily the Tornado-Django example application gives us a hint:

wsgi_app = tornado.wsgi.WSGIContainer(django.core.handlers.wsgi.WSGIHandler())
tornado_app = tornado.web.Application([
('/hello-tornado', HelloHandler),
('.*', tornado.web.FallbackHandler, dict(fallback=wsgi_app)),
])
server = tornado.httpserver.HTTPServer(tornado_app)

 

And a bunch of StackOverflow questions show how to configure Tornado URLs with the static URL handler:

handlers = [
  (r'/favicon.ico', tornado.web.StaticFileHandler, {'path': favicon_path}),
  (r'/static/(.*)', tornado.web.StaticFileHandler, {'path': static_path}), 
  (r'/', WebHandler)
]

Combining these two, you can make a Tornado WSGI server handle static files for a Django app!

sgi_app = tornado.wsgi.WSGIContainer(DJANGO_WSGI_APP)
tornado_app = tornado.web.Application([
  (r'/static/(.*)', tornado.web.StaticFileHandler, {'path': STATIC_URL}), 
  (r'/media/(.*)', tornado.web.StaticFileHandler, {'path': MEDIA_URL}), 
  (r'.*', tornado.web.FallbackHandler, dict(fallback=wsgi_app)),
])
server = tornado.httpserver.HTTPServer(tornado_app)

Web.py and PyInstaller: issues with HTTPS on 32-bit Windows machines

I recently had a project where I had to bundle a Windows app, written in Python 2.7. I chose to use PyInstaller 3.2, since it was being used for other parts of our project as well.

The app had to be light-weight and compatible with older, 32-bit Windows machines, since it was going to be deployed on 10+ year old hardware in rural areas (running 32-bit Windows XP). The app basically provided a local, RESTful assessment engine, allowing students to interactively answer various types of questions (Multiple Choice, Fill-in-the-Blank, etc.) using the open source Open Embedded Assessments tool. To make it as light-weight as possible, I had decided to use the Web.py server.

While the app bundled fine on a 64-bit Windows 10 VM, I kept running into an error on a 32-bit Windows 10 VM:

    You must install pyOpenSSL to use HTTPS.

The app ran fine from the command prompt, and using a Python shell, I could import OpenSSL manually, so I was stumped. I finally dug into the PyInstaller warnings file and realized that Web.py was masking a common issue with _cffi_backend not being declared as a hidden import. Adding that to my spec file solved it, and I could cleanly build my app in 32-bit Windows.

I’m not entirely sure why the same spec file (without the _cffi_backend hidden import) worked fine in 64-bits, but that’s another mystery for another day.