A simple software updater for stand-alone Python applications.
Here's what a basic update-cycle for a self-updating application could look like:
The principle is relatively simple, as long as you don't consider the security risks involved.
Unfortunately, in the real world, security cannot be neglected: You don't want to install untrusted software on your system.
So, how can we make sure our update-cycle is secure? This is where things get quite complicated.
Luckily, python-tuf, the reference implementation for TUF (The Update Framework) takes care of this complexity. If used properly, TUF ensures a high level of security for your update system.
That's why the tufup
package is built on top of python-tuf
.
It is highly advisable to read the TUF documentation before proceeding.
The easiest way to understand how tufup
works is by example. A minimal example of an application that uses tufup
can be found in the companion repository:
tufup-example
The example repository shows how to integrate the tufup
client into your app, and it shows you how to set up a tufup
update-repository.
NOTE: Although the tufup-example repository uses PyInstaller to bundle an application,
tufup
can be used with any type of application bundle, even plain python scripts.
If you have questions about tufup
, or need help getting started, please start a new Q&A discussion, or post a question on Stack Overflow.
If you encounter bugs or other problems that are likely to affect other users, please create a new issue here.
The tufup
package was inspired by PyUpdater, and uses a general approach to updating that is directly based on PyUpdater's implementation.
NOTE:
tufup
is completely independent of PyUpdater. In fact,tufup
was created as a replacement for PyUpdater, given the fact that PyUpdater has been archived and is no longer maintained.
However, whereas PyUpdater implements a custom security mechanism to ensure authenticity (and integrity) of downloaded update files, tufup
is built on top of the security mechanisms implemented in the python-tuf package, a.k.a. tuf
.
By entrusting the design of security measures to the security professionals, tufup
can focus on high-level tools.
Although tuf
supports highly complex security infrastructures, see e.g. PEP458, it also offers sufficient flexibility to allow application developers to tailor the security level to their use case.
For details and best practices, refer to the tuf docs.
Based on the intended use, the tufup
package supports only the top-level roles offered by tuf
. At this time we do not support delegations.
NOTE: Whereas PyUpdater is tightly integrated with PyInstaller,
tufup
is completely independent of the type of packaging solution. At its core,tufup
simply moves bundles of files from A to B, securely, regardless of how these bundles were created. A bundle may consist of a simple python script, a PyInstaller bundle, a PEX package, or any other collection of files and folders.
Borrowing TUF terminology, we distinguish between a repo-side (repository) and a client-side (application).
Below you'll find a list of the basic steps that occur in an application update cycle.
Steps covered by tufup
are highlighted.
On the repo-side, the app developer
On the client-side, the application
The tuf
package is used under the hood to check for updates and download update files in a secure manner, so tufup
can safely apply the update.
See the tuf docs for more information.
Internally, tufup
works with archives (gzipped bundles of files and folders) and patches (binary differences between subsequent archives).
Each archive, except the first one, has a corresponding patch file.
Archive filenames and patch filenames follow the pattern
<name>-<version><suffix>
where name
is a short string that may only contain alphanumeric characters, underscores, and hyphens, version
is a version string according to the PEP440 specification, and suffix
is either '.tar.gz'
or '.patch'
.
BEWARE: whitespace is NOT allowed in the filename.
Patches are typically smaller than archives, so the tufup client will always attempt to update using one or more patches. However, if the total amount of patch data is greater than the desired full archive file, a full update will be performed.
If this sounds confusing, don't worry: it is all handled internally.
When a new release of your application is ready, the following steps need to be taken to enable clients to update to that new release:
tuf
metadata.tuf
metadata files.The tufup.repo
module and the tufup
CLI provide convenient ways to streamline steps 1 to 4, based on the tuf
basic repo example.
Step 5 is not covered by tufup
, as it depends on the implementation.
The signed metadata and hashes ensure both authenticity and integrity of the update files (see tuf docs).
In order to sign the metadata, we need access to the private key files for the applicable tuf
roles.
By default, updates are applied by copying all files and folders from the latest archive to the current app installation directory.
Here's what happens during the update process:
The default install script accepts an optional purge_dst_dir
argument, which will cause ALL files and folders to be deleted from the app installation directory, before moving the new files into place.
This is a convenient way to remove any stale files and folders from the app installation directory.
WARNING: The
purge_dst_dir
option should only be used if the app is properly installed in its own separate directory. If this is not the case, for example if the app is running from the WindowsDesktop
directory, any unrelated files or folders in this directory will also be deleted!
When adding an application bundle to your tufup
repository, you need to specify an app version string.
This version string is used in the archive filename, and must be PEP440 compliant (internally we use packaging.version.Version
).
The tufup
client inspects these version strings to determine if updates are available.
By default, when the tufup
client looks for updates, it only includes final releases.
Pre-releases are filtered out, unless you explicitly specify a "pre-release" channel.
Refer to the Client.check_for_updates()
method for details:
If
pre
is specified, pre-releases are included, down to the specified level. Pre-release identifiers follow the PEP440 specification, i.e.'a'
,'b'
, or'rc'
, for alpha, beta, and release candidate, respectively.
For example, suppose your latest final-release is 1.3.0
, and your latest pre-release is 2.0.0a3
.
An app in the field still has old version 1.0.0
.
If this app checks either the default channel, the release-candidate ('rc'
) channel, or the beta ('b'
) channel, it finds version 1.3.0
available.
If the app checks the alpha channel ('a'
), it finds 2.0.0a3
.
Just to be clear: tufup
assumes a typical linear release history without branching, so
0.0 < 0.1a < 0.1b < 0.1rc < 0.1rc1 < 0.1 < ...
Here's one way to migrate from another update framework, such as pyupdater
, to tufup
:
tufup
to your main application environment as a core dependency, and move pyupdater
from core dependencies to development dependencies.pyupdater
client code (and configuration) in your application by the tufup
client.tufup
repository, so the root metadata file root.json
exists..spec
file (from PyUpdater) to ensure that the root.json
file is included in your package.pyupdater
, and deploy to your server, as usual.
This ensures that your pyupdater
clients currently in the field will be able to update to the new tufup
client.tufup
.pyupdater
version to the new tufup
version, extract the latest PyUpdater archive and add the resulting bundle to the tufup
repository. tufup
repository. pyupdater
repository in place as long as necessary to allow all clients to update.tufup
, as described elsewhere in this document.The tufup.client
tools are aimed primarily at Windows and macOS applications, whereas the tufup.repo
tools are platform independent, as tufup.repo
is just a thin layer on top of python-tuf
.
Although tufup.client
could also be used for Linux applications, those are probably better off using native packaging solutions, or solutions such as Flatpak or Snapcraft.
Read the Python packaging overview for more information.
Platform dependence for tufup.client
is related to file handling and process handling during the installation procedure, as can be seen in tufup.utils.platform_specific.
A custom, platform dependent, installation procedure can be specified via the optional install
argument for the Client.update()
method.