Desktop App Source Updater
desktop_app_source_updater lets a packaged Python desktop app update its own
source code from a GitHub Release, so a one-line fix doesn't mean rebuilding and
redistributing the whole app. It was built for apps frozen with PyInstaller but
depends on nothing from PyInstaller — any Python desktop app that ships a stable
launcher beside plain, updateable source files can use it.
Content Overview
- Installation
- Usage
- Real-World Examples
- Common Questions
- Configuration
- Build and Publish an Update
- Update Scope and Safety
- Reference — release checks, multiple baselines, config merging, verification, troubleshooting, development
- Adopting This in an App
- Citation
- Acknowledgment
- License
Installation
Python 3.10 or newer. No runtime dependencies outside the standard library.
python -m pip install desktop-app-source-updater
Pin an exact version in the app's dependency file — UpdateConfig's field order
is part of the public surface, and a launcher frozen into a packaged app cannot
be corrected afterward:
desktop-app-source-updater==0.3.0
# or pin a commit or tag instead of a release:
desktop-app-source-updater @ git+https://github.com/yzhaoinuw/desktop_app_source_updater.git@v0.3.0
Bundle this dependency into the app's next full packaged release. End-user machines need neither Git nor a clone of this repository.
Usage
Call the updater from the stable Python launcher that users run, before the launcher imports the app's updateable source package:
from pathlib import Path
import os
import sys
from desktop_app_source_updater import (
UpdateConfig,
format_update_message,
run_startup_update,
)
APP_ROOT = Path(__file__).resolve().parent
USER_STATE_ROOT = Path(os.environ.get("LOCALAPPDATA") or (Path.home() / ".cache"))
if str(APP_ROOT) not in sys.path:
sys.path.insert(0, str(APP_ROOT))
def show_update_available(installed_version, target_version):
print(
f"[startup-update] updating from {installed_version} to {target_version}",
flush=True,
)
update_config = UpdateConfig(
app_name="my_app",
app_root=APP_ROOT,
installed_version_file="my_app_src/__init__.py",
latest_release_url="https://github.com/me/my_app/releases/latest",
asset_prefix="my_app_update_",
allowed_payload_paths=("my_app_src/",),
check_state_file=USER_STATE_ROOT / "my_app" / "update-check.json",
skip_update_env="MY_APP_SKIP_UPDATE",
update_zip_url_env="MY_APP_UPDATE_ZIP_URL",
timeout_env="MY_APP_UPDATE_TIMEOUT_SECONDS",
force_check_env="MY_APP_FORCE_UPDATE_CHECK",
on_update_available=show_update_available,
)
result = run_startup_update(
update_config,
force_check="--check-update" in sys.argv,
)
message = format_update_message(result)
if message:
print(f"[startup-update] {message}", flush=True)
from my_app_src.app import main
main()
Replace the example names and paths with the adopting app's own. What matters is the order: run the update, display any nonempty message, then import the app runtime.
The first release that adds this dependency must be a full packaged release. Later compatible releases can then ship small source-update assets.
Real-World Examples
These maintained desktop apps use this package in their startup launchers:
sleep_scoringapplies lightweight release updates to itsapp_src/runtime before launching the packaged sleep-scoring application.fp_analysisupdates itsfp_analysis_app/runtime while enforcing additional app-specific boundaries for local data and generated assets.
Common Questions
How do I update a PyInstaller app without rebuilding it?
Ship the app's Python source as plain files beside the frozen launcher instead of inside the bundle, and call this package from that launcher before the source is imported. It downloads a zip of only the changed files from your GitHub Release and swaps them in, so a fix reaches users without a new build or installer. Changes to dependencies, packaging, or the interpreter still need a full packaged release — see Update Scope and Safety.
Can I keep my .py files outside the frozen bundle and update only those?
Yes; that arrangement is what this package assumes. The frozen bundle holds the
interpreter and the compiled dependencies and is never touched, while
allowed_payload_paths names the source directories an update may change.
Anything outside them is refused.
Do I need code signing or an update server?
Neither. Updates are ordinary GitHub Release assets fetched over HTTPS from your own repository, so there is no server to run and no signing keys to generate or protect. The trade-off is that the trust model is your GitHub account and TLS rather than a signature over the payload.
How does this compare to PyUpdater and tufup?
Both replace the whole frozen bundle. PyUpdater, long the standard for PyInstaller apps, is archived and unmaintained; its successor tufup is actively maintained and ships full or patched bundle archives signed with The Update Framework.
This package fills a different niche. It updates only the app's own Python source and leaves the frozen bundle — the interpreter and every compiled dependency — untouched, so an update is a zip of changed files rather than a copy of the application, and applying one needs nothing outside the standard library. In exchange there is no cryptographic signing, and dependency or packaging changes still require a full packaged release. If your threat model requires signed updates, use tufup.
Configuration
The values most integrations set:
| Field | Purpose |
|---|---|
app_name |
Stable application name recorded in the update manifest. |
app_root |
Installed directory containing the launcher and app source. |
installed_version_file or installed_version |
Current app version used for compatibility checks. |
latest_release_url |
The adopting app's GitHub URL ending in /releases/latest. |
asset_prefix |
Prefix for source-update assets, such as my_app_update_. |
allowed_payload_paths |
Source directories that an update may change. |
check_state_file |
App-specific, per-user JSON file for durable check throttling. Use an absolute path; relative paths resolve under app_root. |
The default version-file pattern reads a simple assignment such as
VERSION = "1.2.3".
Optional environment-variable fields make development and support easier:
| Field | Typical use |
|---|---|
skip_update_env |
Disable startup updates temporarily. |
update_zip_url_env |
Test a local zip or prerelease asset directly. |
timeout_env |
Override the network timeout. |
force_check_env |
Bypass the normal check interval. |
Build and Publish an Update
Run the asset builder from the adopting app repository:
python -m desktop_app_source_updater.build_update_asset `
--app-name my_app `
--runtime-path my_app_src `
--from-ref v1.2.0 `
--from-ref v1.2.1 `
--from-ref v1.2.2 `
--to-ref v1.2.3 `
--version v1.2.3 `
--version-file my_app_src/__init__.py `
--asset-prefix my_app_update_
Use one --from-ref for every prior release that may update directly to the new
version. The builder validates the changed paths and refuses to create a
source-only asset when the release requires a full packaged update.
Attach the generated zip to the app's GitHub Release under exactly
<asset_prefix><release-tag>.zip — my_app_update_v1.2.3.zip for the example
above. The manifest version and the release tag must agree, with a leading v
treated as equivalent; pass --version or --output when the version file
stores 1.2.3 and the tag is v1.2.3.
Update Scope and Safety
Source-update assets are for changes to approved runtime source files. Publish a full packaged release when a change affects dependencies, packaging, build files, environments, data, file deletions, or file renames.
Before applying an asset, the updater checks every listed file against the baselines recorded in the manifest. If any file has unknown bytes, the entire asset is skipped and nothing changes; this protects local patches and prevents partial updates across mutually dependent modules. Files not listed in the asset are never inspected or changed. The one exception to whole-file baseline replacement is the declared config file described under User-Editable Python Configuration.
Updates are prepared before mutation and applied through a backup-and-rollback
transaction. The updater also blocks payload paths outside
allowed_payload_paths and rejects dependency, packaging, build, cache, archive,
and local-data paths.
Reference
Details worth looking up once an integration is working.
Release Checks
With latest_release_url, the updater discovers the newest tag through GitHub's
ordinary /releases/latest redirect, compares it with the installed version, and
checks for the deterministic asset filename before announcing or downloading an
update. A release without that asset is treated as up to date.
Successful checks are cached for check_interval_seconds, which defaults to 24
hours. Failed checks retry after failure_retry_seconds, which defaults to 1
hour. HTTP 403 and 429 backoff is persisted and takes precedence. Direct zip
overrides bypass discovery and its cache.
release_api_url and release_api_env remain available for existing adopters,
but new integrations should use latest_release_url to avoid unauthenticated
GitHub REST API rate limits.
Multiple Installed Baselines
Use repeatable --installed-baseline-manifest arguments when installations
reporting the same version can legitimately contain different bytes, such as LF
Git blobs and CRLF files from a Windows package:
python -m desktop_app_source_updater.build_update_asset `
--app-name my_app `
--runtime-path my_app_src `
--from-ref v1.2.2 `
--installed-baseline-manifest release_baselines/v1.2.2-windows.json `
--to-ref v1.2.3 `
--version v1.2.3 `
--version-file my_app_src/__init__.py `
--asset-prefix my_app_update_
Each manifest names a version already supplied through --from-ref and maps every
changed runtime path to its installed SHA-256. Use null only when the file was
absent:
{
"version": "v1.2.2",
"files": {
"my_app_src/app.py": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"my_app_src/new_module.py": null
}
}
The builder deduplicates equivalent hashes, preserves version-specific baselines where needed, and refuses combinations the manifest schema cannot represent safely.
User-Editable Python Configuration
A schema-2 asset can merge one explicitly declared Python config file while ordinary source files continue to use whole-file hash verification:
python -m desktop_app_source_updater.build_update_asset `
--app-name my_app `
--runtime-path my_app_src `
--from-ref v1.2.2 `
--to-ref v1.2.3 `
--version v1.2.3 `
--version-file my_app_src/__init__.py `
--python-config-merge my_app_src/config.py `
--editable-assignment SLEEP_SCORING_MODEL `
--editable-assignment WINDOW_CONFIG
The downloaded file remains the template. For allowlisted assignments, values already present in the installed file are preserved, missing values receive the downloaded defaults, and literal dictionaries merge recursively. Imports, functions, comments, ordering, removed settings, and undeclared assignments come from the downloaded file.
Both files are parsed without being imported or executed. Invalid Python, duplicate or unsupported assignments, and nonliteral editable values fail the entire update before mutation.
The adopting app must first ship a full release containing a schema-2-compatible updater; older packaged updaters reject schema-2 assets.
Verifying an Integration
Before shipping, confirm that:
- a clean compatible installation updates successfully;
- a skipped-release installation updates when all required
--from-refvalues are included; - an unknown local edit skips the asset without changing any payload file;
- dependency or packaging changes make the builder refuse a source-only asset;
- the configured skip variable bypasses the update;
- a second ordinary launch inside the check interval makes no network request;
- a forced check bypasses the interval;
- current and newer installed versions never fetch the update zip;
- the app still launches when GitHub is unreachable.
Point update_zip_url_env at a generated zip to exercise the apply path locally
without publishing a GitHub Release.
Troubleshooting
format_update_message(result) is empty for quiet outcomes such as disabled,
throttled, or already up to date. For other outcomes, inspect result.status and
result.reason:
| Status | Meaning |
|---|---|
updated |
The source update was applied. |
skipped |
Local state did not match a safe update baseline. |
blocked |
The asset was incompatible or contained disallowed changes. |
failed |
Discovery, download, validation, callback, or application failed. |
The result also exposes installed_version and target_version, and labels
discovery and asset-download errors separately.
For debugging:
- enable
skip_update_envto bypass startup updates; - call
run_startup_update(update_config, force_check=True)for an immediate remote check; - use
force_check_envfor the same behavior without a code change; - use
update_zip_url_envto test a local zip or explicit asset URL.
Development
Use Python 3.10 or newer:
python -m unittest discover -s tests
python -m compileall -q desktop_app_source_updater
python -m desktop_app_source_updater.build_update_asset --help
The public API is exported from desktop_app_source_updater/__init__.py; the
runtime implementation is in desktop_app_source_updater/core.py.
Adopting This in an App
The integration is mechanical enough that a capable coding agent can do it from a short instruction. Paste this into the adopting app's repository:
Adopt desktop-app-source-updater in this app. Add it as a pinned dependency, then
call run_startup_update(UpdateConfig(...)) from the stable launcher, before the
app's own source package is imported.
Read this repo to find the launcher, the updateable source package, and the
version file. Bundle the updater in a full packaged release before publishing any
source-update asset.
Citation
If you use this package in research, use GitHub's Cite this repository button or the CITATION.cff file to obtain an APA or BibTeX entry.
Each release is archived on Zenodo. Cite the concept DOI 10.5281/zenodo.21763329, which resolves to the newest release; use a release's own DOI only when you need to pin the exact version you ran.
Acknowledgment
This package was extracted from software developed for research supported in part by the BRAIN Initiative of the US National Institutes of Health (U19NS128613).
License
Released under the MIT License — see
LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file desktop_app_source_updater-0.3.0.tar.gz.
File metadata
- Download URL: desktop_app_source_updater-0.3.0.tar.gz
- Upload date:
- Size: 40.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9c3c3890784f23a29ce669371ca14977cfcd7de36b7e50a63b15e8c09de7220f
|
|
| MD5 |
68ceeae2abce419ebfa3d6d32680ca27
|
|
| BLAKE2b-256 |
42daeddba7344621161cb3d665e716eb16e47cf1d5371641659ebe8f6a4606f1
|
File details
Details for the file desktop_app_source_updater-0.3.0-py3-none-any.whl.
File metadata
- Download URL: desktop_app_source_updater-0.3.0-py3-none-any.whl
- Upload date:
- Size: 26.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
265bc291a2f43b8da5bad5216cdef863f5635de2356476c56893cbe82fd7f271
|
|
| MD5 |
92df04c6985bd083b4b3ac02f3fe1c38
|
|
| BLAKE2b-256 |
a79fcfab8d79ed46acc9005b95a900a129e49b7a5f8c00d21a0c7f4f8ccec795
|