Let’s assume you have a desktop application built with Python. It could be a traditional GUI app built with PyQT/wxPython/Kivy or any other GUI framework. Or it could be a web server that serves a browser based HTML GUI for the user. Either way, you have “frozen” the app using cx_freeze, py2app/py2exe or pyinstaller and now you want to add “auto update” to the app, so when there’s a new version of the application is available, the app can download and install the update, automatically. For this particular task, I found esky to be a good viable option. In this article, I am going to demonstrate how we can use esky to deliver updates to our apps.
Quick Introduction to Esky
If we want to use Esky to deliver updates, we need to freeze the app first. But this time, we will ask Esky to freeze the app for us, using our freezer of choice. For example, if we used py2app before, we will still use py2app but instead of directly using it, we will pass it to Esky and Esky will use the py2app to freeze the app for us. This step is necessary so that Esky can inject the necessary parts to handle updates/patches and install them gracefully.
For the apps to locate and download the updates, we need to serve the updates from a location on the internet/local network. Esky produces a zip archive. We can directly put it on our webserver. The apps we freeze needs to know the URL of the webserver and must have access to it.
On the other hand, inside our app, we need to write some codes which will scan the URL of the above mentioned webserver, find any newer updates and install them. Esky provides nice APIs to do these.
So now that we know the steps to follow, let’s start.
Creating a setup
file
If you have frozen an app before, you probably know what a setup file is and how to write one. Here’s a sample that uses py2app to freeze an app:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import sys from esky import bdist_esky from distutils.core import setup PY2APP_OPTIONS = {"includes": ['ssl', 'sip', 'PyQt4']} DATA_FILES = ['my_pyqt_ui_file.ui'] # Using py2app setup( name="My Awesome App", version="0.1", scripts=["main.py"], data_files=DATA_FILES, options={"bdist_esky": { "freezer_module": "py2app", "freezer_options": PY2APP_OPTIONS, }} ) |
Now we can generate the frozen app using:
1 |
python setup.py bdist_esky |
This should generate a zip archive in the dist
directory.
Hosting the app
Collect the zip file from the dist
directory and put it somewhere accessible on the internet. For local testing, you can probably use Python’s built in HTTP server to distribute it.
Finding, Downloading and Installing Updates
Now we will see the client side code that we need to write to locate and install the updates.
Here’s some codes taken from a PyQT app. The find_esky_update
method is part of a QMainWindow
class. It is called inside the onQApplicationStarted
method. So it checks the update as soon as the application starts.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def find_esky_update(self): if getattr(sys, "frozen", False): updater = esky.Esky(sys.executable, "http://localhost:8000") if updater.find_update(): reply = QtGui. \ QMessageBox \ .question(self, 'Update', "New update available! Do you want to update?", QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No) if reply == QtGui.QMessageBox.Yes: updater.auto_update(self.handle_esky_status) else: print("No new updates found!") else: print ("App is not frozen!") |
We first check if the app is frozen. If it’s not, then there’s no way we can install updates. sys.frozen
will contain information about the app if it’s frozen. Otherwise it will not be available. So we first ensure that it is indeed a frozen app.
Then we create an Esky app instance by providing it the URL of our webserver (where the updates are available). We only pass the root URL (without the zip file name). The find_update()
method on the Esky app will find newer update and return some information if a new update is available. Otherwise it will be falsy.
If an update is available, we ask our user if s/he wants to update. Here we used QMessageBox
for that. If they agree, we call the auto_update
method with a callback. We will see the callback soon. The auto_update
downloads the update and installs it. The callback we pass – it gets called every time something happens during the process. It can be a good way to display download progress using this callback.
Let’s see our example code here:
1 2 3 4 5 6 7 8 9 10 |
def handle_esky_status(self, message): progress = None if message['status'] == 'downloading': progress = int((float(message['received']) / float(message['size'])) * 100) elif message['status'] == 'ready': progress = 100 if progress is not None: print(progress) #self.progressBar.setValue(progress) |
As you can see from the code, the callback gets a dictionary which has a key status
and if it is “downloading”, we also have the amount of data we have received so far and the total size. We can use this to calculate the progress and print it. We can also display a nice progress bar if we wish.
So basically, this is all we need to find and install updates.
Rolling a new update
We have learned to use Esky, we have seen how to add auto update to our app. Now it’s time to build a new update. That is easy, we go back to the setup.py
file we defined earlier. We had version="0.1",
inside the setup()
function. We need to bump it. So let’s make it 0.2
and build it. We will get a new zip file (the file contains the version if you notice carefully). Drop it on the webserver (the URL where we put our app). Run an older copy of the app (which includes the update checking codes described above). It should ask you for an update 🙂
Please note, you need to call the find_esky_update()
method for the prompt to trigger. As I mentioned above, I run it in onQApplicationStarted
method for PyQt. You need to find the appropriate place to call it from in your application.
Further Reading
You can find a nice tutorial with step by step instructions and code samples here: https://github.com/cloudmatrix/esky/tree/master/tutorial