Building a Universally Portable Python App

JP HutchinsJP Hutchins
6 min read

Welcome to the first article of a series about deploying a universally portable Python application.

What is a "Universally Portable" app?

A portable, or standalone, application is one that has no install-time or run-time dependencies other than the operating system.1 It is common to see this kind of application distributed as a compressed archive, such as a .zip or .tar.gz, or as an image, like .bin or .dmg.

A universal application is one that can run on all operating systems and architectures. Here, we use "universal" loosely to mean the three personal computer operating systems that make up over 90% of global market share: Windows (72.13%), MacOS (15.46%), and Linux (4.03%).2

Windows and Linux builds will target the amd64 (x86-64) architecture and MacOS will target Arm64 ("Apple Silicon" M-series Macs). Arm, aarch64 or arm32, builds for Linux would be possible locally but are not available in a GitHub Workflow, yet.

You can test the output of this tutorial by installing the example application, jpsapp, yourself.

Use portable ZIPs or OS installers

GitHub: jpsapp releases page

Install with pipx

pipx install jpsapp
jpsapp --help

The Series

  1. This article: build the app locally with build
  2. Use a GitHub Release Action to automate distribution to PyPI, the Python Package Index so that Python users can install your app with pip and pipx.
  3. Add the universal portable application build to the GitHub Release Action using PyInstaller
  4. Add a Windows MSI installer build to the GitHub Release Action using WiX v4
  5. Add Linux .deb and .rpm installer builds to the GitHub Release Action using fpm
  6. Deploy to the Microsoft Store and winget
  7. Deploy to the Mac App Store
  8. Deploy to the Debian Archive

The App

This article will focus on the application itself and the tooling to support it.

The app is a command line interface (CLI) that uses the built in argparse module and takes one of three actions:

  1. no argument: print "Hello, World!"
  2. -i or --input: print "Hello, World!", then "Press any key to exit...". This is used to create a double-clickable version of the application for Windows users
  3. -v or --version argument: print package version and exit

Take a look at the source code.

The Repo

The repository, python-distribution-example, can be cloned to your Windows, Linux, or MacOS environment.

The following are excerpts and explanations of the files that are relevant to running the app locally.

Note: tooling and dependencies are intentionally kept to a minimum in this example repository. A more complicated app that has more dependencies will benefit from the usage of tools that help to resolve dependencies and manage environments. Unfortunately, there is no easy recommendation to make.

I suggest reading Anna-Lena Popkes' article, An unbiased evaluation of environment management and packaging tools, to help you to form an opinion about what tooling is best for your application.

This repository demonstrates a highly compatible pyproject.toml that readers can easily adapt to their choice of tooling.

jpsapp/

This is the Python module itself and contains all of the source code for the application.

envr-default

This file defines the shell environment for common shells like bash, zsh, and PowerShell on Windows, MacOS, and Linux. The environment is activated by calling . ./envr.ps1

[PROJECT_OPTIONS]
PROJECT_NAME=jpsapp
PYTHON_VENV=.venv

pyproject.toml

PEP 621 introduced the pyproject.toml standard for declaring common metadata, replacing the need for requirements.txt and most other configuration files.

[build-system]
requires = [
    "setuptools>=70.0",
]
build-backend = "setuptools.build_meta"

[project]
name = "jpsapp"
version = "1.1.6"
description = "An example of Python application distribution."
authors = [
    { name = "JP Hutchins", email = "jphutchins@gmail.com" },
]
readme = "README.md"
license = { file = "LICENSE" }
requires-python = ">=3.8"
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "Topic :: Software Development :: Build Tools",
]

dependencies = [
    # Add your project dependencies here
]

[project.optional-dependencies]
dev = [
    "build>=1.2.1,<2",
    "pyinstaller>=6.4.0,<7",
    "pyinstaller-versionfile>=2.1.1,<3",
]

[project.scripts]
jpsapp = "jpsapp.main:app"

[project.urls]
Homepage = "https://dev.to/jphutchins/building-a-universally-portable-python-app-2gng"
Repository = "https://github.com/JPhutchins/python-distribution-example.git"

[tool.setuptools]
packages = ["jpsapp"]
include-package-data = true

For a detailed explanation of the pyproject.toml, refer to the Python Packaging User Guide. Here are a few interesting features of our example configuration.

version = "1.1.6" will version the python package and the eventual app. This line in the configuration is the Single Source Of Truth for the version. There are many tools available to establish a Git tag as the SSOT, if you prefer.

packages = ["jpsapp"] declares that jpsapp is the only module we are packaging. This allows more Python modules to be added to the root of the repository, such as the tooling in the distrubution folder, that we wouldn't want to package for distribution.

jpsapp = "jpsapp.main:app" declares that the command jpsapp will execute the app function from jpsapp.main.

Dependencies

Python

If you have Python >=3.8 go ahead and use that. If not, install the most recent Python release for your system. There are many ways to do so, but I'll briefly offer my opinion:

  • Windows: use the Microsoft Store or winget and take advantage of "App Execution Aliases". Whatever you do, make sure that both python and python3 call the Python you want, none of this py nonsense!
  • Linux: use your package manager, and maybe deadsnakes if you're on Ubuntu since they don't keep their Python packages current.

Build the App

Now that you have cloned the repository and installed the dependencies, you can build and run the application.

  • python3 -m venv .venv: on this first run it will create the venv at .venv
    • If python3 is not an alias to the version of Python 3 you'd like to use then update the command accordingly, e.g. python -m venv .venv.
  • . ./envr.ps1: activate the development environment
  • pip install --require-virtualenv -e .[dev]: install the development dependencies

And that's it! jpsapp should print "Hello, World!". Keep in mind that you can get the same execution with python -m jpsapp, python -m jpsapp.main, or python jpsapp/main.py, etc.

To build the Python package distributions, simply run python -m build. The Python .whl and .tar.gz packages will be built at dist/, e.g. dist/jpsapp-1.0.0.tar.gz.

In the next part of this series, we will use a GitHub Workflow to release the package distribution to the PyPI so that other users can install your app with pipx!

Footnotes

  1. \^ "Portable application". Wikipedia.com. Retrieved 2024-03-11.
  2. \^ "OS Market Share". GS.Statcounter.com. Retrieved 2024-03-11.

Change History

  • 2024-04-14: change myapp -> yourapp
  • 2024-04-18: change yourapp -> jpsapp
  • 2024-06-08: remove poetry; update pyproject.toml; update steps
0
Subscribe to my newsletter

Read articles from JP Hutchins directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

JP Hutchins
JP Hutchins