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
Install with pipx
pipx install jpsapp
jpsapp --help
The Series
- This article: build the app locally with build
- 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.
- Add the universal portable application build to the GitHub Release Action using PyInstaller
- Add a Windows MSI installer build to the GitHub Release Action using WiX v4
- Add Linux .deb and .rpm installer builds to the GitHub Release Action using fpm
- Deploy to the Microsoft Store and
winget
- Deploy to the Mac App Store
- 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:
- no argument: print "Hello, World!"
-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-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.
__main__.py
: support running as a module -python -m jpsapp
main.py
: the app described above
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 bothpython
andpython3
call the Python you want, none of thispy
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
.
- If
. ./envr.ps1
: activate the development environmentpip 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
- \^ "Portable application". Wikipedia.com. Retrieved 2024-03-11.
- \^ "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