From 3de4ca48e0b058ecb578fdf0f345b27f0622217d Mon Sep 17 00:00:00 2001
From: Justin <39440218+justin-eckenweber@users.noreply.github.com>
Date: Sat, 5 Sep 2026 21:32:30 +0200
Subject: [PATCH] Ship AppImage with bundled GTK runtime and automated releases
---
.dockerignore | 2 +
.github/workflows/appimage.yml | 76 +++++++++++++
CHANGELOG.md | 10 ++
MANIFEST.in | 1 +
README.de.md | 33 ++++--
README.md | 41 +++++--
THIRD_PARTY_NOTICES.md | 17 ++-
btd700/i18n.py | 4 +
btd700/integration.py | 27 ++++-
docs/APPIMAGE.md | 90 ++++++++++++++++
docs/VALIDATION.md | 20 +++-
install.py | 11 +-
packaging/appimage/AppRun | 10 ++
packaging/appimage/Containerfile | 14 +++
packaging/appimage/RELEASE_NOTES.md | 23 ++++
packaging/appimage/TestContainerfile | 7 ++
packaging/appimage/assemble.py | 124 ++++++++++++++++++++++
packaging/appimage/btd700-control.desktop | 11 ++
packaging/appimage/build.sh | 59 ++++++++++
packaging/appimage/check.sh | 46 ++++++++
packaging/appimage/environment.sh | 14 +++
pyproject.toml | 2 +-
tests/test_integration.py | 36 ++++++-
tools/check_gui.py | 21 +++-
tools/tray_test_host.py | 23 ++++
25 files changed, 690 insertions(+), 32 deletions(-)
create mode 100644 .dockerignore
create mode 100644 .github/workflows/appimage.yml
create mode 100644 docs/APPIMAGE.md
create mode 100644 packaging/appimage/AppRun
create mode 100644 packaging/appimage/Containerfile
create mode 100644 packaging/appimage/RELEASE_NOTES.md
create mode 100644 packaging/appimage/TestContainerfile
create mode 100644 packaging/appimage/assemble.py
create mode 100644 packaging/appimage/btd700-control.desktop
create mode 100644 packaging/appimage/build.sh
create mode 100644 packaging/appimage/check.sh
create mode 100644 packaging/appimage/environment.sh
create mode 100644 tools/tray_test_host.py
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..5c35550
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,2 @@
+# The Containerfile only installs Ubuntu packages; source is mounted at runtime.
+**
diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml
new file mode 100644
index 0000000..478c5cc
--- /dev/null
+++ b/.github/workflows/appimage.yml
@@ -0,0 +1,76 @@
+name: AppImage
+
+on:
+ push:
+ tags: ['v*']
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-24.04
+ timeout-minutes: 35
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ persist-credentials: false
+ - name: Check release version
+ if: startsWith(github.ref, 'refs/tags/')
+ env:
+ RELEASE_TAG: ${{ github.ref_name }}
+ run: |
+ python3 - <<'PY'
+ import os, tomllib
+ version = tomllib.load(open('pyproject.toml', 'rb'))['project']['version']
+ assert os.environ['RELEASE_TAG'] == 'v' + version, 'Tag must match pyproject.toml'
+ PY
+ - name: Build isolated Ubuntu runtime and source archives
+ run: |
+ docker build -t btd700-appimage-builder -f packaging/appimage/Containerfile .
+ docker run --rm -v "$PWD:/src" btd700-appimage-builder
+ - name: Verify unit tests
+ run: |
+ docker run --rm -v "$PWD:/src" btd700-appimage-builder bash -c '
+ BTD700_LANGUAGE=en python3 -m unittest discover -s tests -v &&
+ BTD700_LANGUAGE=de python3 -m unittest discover -s tests -v'
+ - name: Verify packaged English/German GUI and tray without host Python or GTK
+ run: |
+ docker build -t btd700-appimage-test -f packaging/appimage/TestContainerfile .
+ docker run --rm -v "$PWD:/src" btd700-appimage-test
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
+ if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
+ with:
+ name: appimage-x86_64
+ path: dist/*
+ compression-level: 0
+ if-no-files-found: error
+ retention-days: 1
+
+ release:
+ if: startsWith(github.ref, 'refs/tags/')
+ needs: build
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ with:
+ persist-credentials: false
+ - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
+ with:
+ name: appimage-x86_64
+ path: dist
+ - name: Publish verified release assets
+ env:
+ GH_TOKEN: ${{ github.token }}
+ GH_REPO: ${{ github.repository }}
+ RELEASE_TAG: ${{ github.ref_name }}
+ run: |
+ (cd dist && sha256sum --check SHA256SUMS)
+ gh release create "$RELEASE_TAG" dist/* --draft --verify-tag \
+ --title "BTD 700 Control $RELEASE_TAG" \
+ --notes-file packaging/appimage/RELEASE_NOTES.md
+ gh release edit "$RELEASE_TAG" --draft=false --latest
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 34b1fe4..c7c1603 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Changelog
+## 0.3.0 — 2026-09-05
+
+- Downloadable x86-64 AppImage for glibc 2.39+ desktops, including Python, GTK,
+ libadwaita and the tray library; no Python package installation needed.
+- AppImage-aware application-menu installation and autostart using its permanent
+ file path, including a persistent icon and support for extracted AppDirs.
+- Container build and GitHub release workflow, checksums, bundled license notices
+ and corresponding dependency source archives.
+- English/German packaged GUI and D-Bus tray verification with simulated hardware.
+
## 0.2.0 — 2026-09-05
- English and German app, tray, CLI help and error messages, selected by system
diff --git a/MANIFEST.in b/MANIFEST.in
index 3081f93..efc270d 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,7 @@
include README.md README.de.md LICENSE THIRD_PARTY_NOTICES.md CONTRIBUTING.md CHANGELOG.md
include run.sh install.py
recursive-include packaging *.svg *.rules
+recursive-include packaging/appimage *.sh *.py *.desktop *.md Containerfile TestContainerfile AppRun
recursive-include docs *.md *.png
recursive-include tools *.py
recursive-include tests *.py
diff --git a/README.de.md b/README.de.md
index a1b3c4e..067aa95 100644
--- a/README.de.md
+++ b/README.de.md
@@ -1,6 +1,6 @@
# BTD 700 Control für Linux
-[English](README.md) · [Quellcode v0.2.0 herunterladen](https://github.com/justin-eckenweber/btd700linux/archive/refs/tags/v0.2.0.zip) · [Fehler melden](https://github.com/justin-eckenweber/btd700linux/issues)
+[English](README.md) · [AppImage herunterladen](https://github.com/justin-eckenweber/btd700linux/releases/latest) · [Fehler melden](https://github.com/justin-eckenweber/btd700linux/issues)
Native GTK-4-App zur Steuerung des **Sennheiser BTD 700**, mit einem Menü im
Infobereich von GNOME/KDE. Eigenständige Implementierung des HID-Steuerprotokolls,
@@ -29,16 +29,37 @@ Die Bilder zeigen die englische Oberfläche; Deutsch ist ebenfalls enthalten.
Install · - Download source v0.2.0 · + Download AppImage · Deutsche Anleitung · Report an issue
@@ -65,11 +65,33 @@ window quits the app instead. ## Install -Version **0.2.0** is distributed as source. You do not need to compile the app or -install Python packages with pip. There is currently no AppImage, Flatpak or -self-contained binary download. +### AppImage (x86-64) -### 1. Install the system libraries +[Download the AppImage from GitHub Releases](https://github.com/justin-eckenweber/btd700linux/releases/latest). +It includes Python, GTK, libadwaita and the tray library. Requires **glibc 2.39+** +(for example Ubuntu 24.04 or newer, or the tested Bazzite 44 desktop). + +```bash +chmod +x BTD_700_Control-0.3.0-x86_64.AppImage +./BTD_700_Control-0.3.0-x86_64.AppImage +``` + +Put it in a permanent folder, then optionally run it with `--install-desktop` to +add an application-menu entry. Enable **Start at login** inside the app for tray +autostart. Use `--remove-desktop` to remove both entries. Keep the AppImage at the +same path; after moving it, recreate the menu entry and toggle autostart off/on. + +If FUSE is unavailable, start with +`APPIMAGE_EXTRACT_AND_RUN=1 ./BTD_700_Control-0.3.0-x86_64.AppImage`. +See the [AppImage guide](docs/APPIMAGE.md) for permanent extraction, build instructions, +checksums and dependency sources. USB permissions and the desktop's tray host are +still required; GNOME needs a StatusNotifier/AppIndicator extension. + +### Run from source + +You can also run the Python source directly, without compiling or using pip. + +#### 1. Install the system libraries Requirements: **Python 3.10+**, PyGObject, **GTK 4.10+**, **libadwaita 1.5+** and libdbusmenu with GObject introspection. A graphical desktop session is needed for @@ -99,7 +121,7 @@ KDE Plasma provides a tray host. Other desktops may work but have not been teste Package references: [Fedora libdbusmenu](https://packages.fedoraproject.org/pkgs/libdbusmenu/libdbusmenu/), [Ubuntu introspection package](https://packages.ubuntu.com/noble/gir1.2-dbusmenu-glib-0.4). -### 2. Download and run +#### 2. Download and run ```bash git clone https://github.com/justin-eckenweber/btd700linux.git @@ -107,7 +129,7 @@ cd btd700linux ./run.sh ``` -Or [download the v0.2.0 source ZIP](https://github.com/justin-eckenweber/btd700linux/archive/refs/tags/v0.2.0.zip), +Or [download the v0.3.0 source ZIP](https://github.com/justin-eckenweber/btd700linux/archive/refs/tags/v0.3.0.zip), extract it, and run `bash run.sh` inside the extracted folder. Want to explore without touching any hardware? @@ -116,7 +138,7 @@ Want to explore without touching any hardware? ./run.sh --demo ``` -### 3. Add it to your application menu +#### 3. Add it to your application menu ```bash python3 install.py @@ -133,7 +155,8 @@ python3 install.py --uninstall # Remove launcher and autostart entry ### USB permissions -If the app reports that USB access is denied: +If the app reports that USB access is denied, use the rule from the source checkout +or download `70-btd700-control.rules` from the same AppImage release (adjust its path below): ```bash sudo install -m 0644 packaging/70-btd700-control.rules /etc/udev/rules.d/70-btd700-control.rules diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index cf025b4..4c1c81d 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -3,8 +3,21 @@ The MIT license in this repository covers this project's own implementation, documentation and artwork. It does not relicense third-party software or trademarks. -- Python, PyGObject, GTK, libadwaita and libdbusmenu are provided by your system, - not bundled here. Their respective licenses continue to apply. +- When running from source, Python, PyGObject, GTK, libadwaita and libdbusmenu are + provided by your system. The AppImage bundles these libraries and their runtime + dependencies from Ubuntu 24.04. Their respective licenses continue to apply. +- AppImage license/copyright texts and the package manifest are included under + `usr/share/doc/btd700-bundled/`; shared license texts are under + `usr/share/common-licenses/`. Extract with `--appimage-extract` to inspect them. + Exact corresponding Ubuntu source archives, package versions and build + instructions are downloadable beside each AppImage as + `*-dependency-sources.tar.gz`. These dependencies are not relicensed under MIT. +- The AppImage type2 runtime is MIT-licensed and statically includes musl (MIT), + libfuse (LGPL 2.1), squashfuse (BSD), zstd (BSD) and zlib (zlib license). + Runtime notices are included in the image; runtime sources, build scripts, + libfuse sources/patches and squashfuse sources accompany the dependency archive. + See [AppImage build documentation](docs/APPIMAGE.md) for replacing libraries and + rebuilding the runtime or app. - The control protocol was reconstructed by examining the official Windows Sennheiser Dongle Control 1.0.5 application. The vendor application, decompiled sources, firmware and vendor graphics are not included. Provenance and references diff --git a/btd700/i18n.py b/btd700/i18n.py index c26b776..ae17585 100644 --- a/btd700/i18n.py +++ b/btd700/i18n.py @@ -32,6 +32,10 @@ def tr(message): # German source messages are retained for compatibility with the first release. # Keep placeholders identical in both languages; never translate protocol values. ENGLISH = { + 'Menüeintrag und Autostart entfernt. Die Programmdateien bleiben erhalten.': + 'Removed the launcher and start-at-login entry. The application files are unchanged.', + 'Im Anwendungsmenü „BTD 700 Control“ öffnen. Die AppImage-Datei muss an diesem Ort bleiben.': + 'Open BTD 700 Control from the application menu. Keep the AppImage file at this location.', 'Menüeintrag und Autostart entfernt. Der Projektordner bleibt erhalten.': 'Removed the launcher and start-at-login entry. The project folder is unchanged.', 'Status aktualisieren': 'Refresh status', 'App vollständig beenden': 'Quit application', diff --git a/btd700/integration.py b/btd700/integration.py index a75d411..e9bc235 100644 --- a/btd700/integration.py +++ b/btd700/integration.py @@ -1,6 +1,7 @@ """User-scoped desktop integration.""" from pathlib import Path import os +import shutil import sys ROOT = Path(__file__).resolve().parent.parent @@ -8,10 +9,17 @@ APP_ID = 'io.github.btd700linux.Control' def desktop_entry(*, background=False): + appdir = os.environ.get('APPDIR') + appimage = os.environ.get('APPIMAGE') if appdir else None checkout = (ROOT / 'run.sh').is_file() - path = str(ROOT / 'run.sh') if checkout else sys.executable - module_args = '' if checkout else ' -m btd700' - icon = str(ROOT / 'packaging/btd700-control.svg') if checkout else 'audio-headphones' + path = appimage or (str(Path(appdir) / 'AppRun') if appdir else None) or (str(ROOT / 'run.sh') if checkout else sys.executable) + module_args = '' if appdir or checkout else ' -m btd700' + # The pinned type2 runtime removes its CLI flag before launching AppRun; + # its extraction directory also identifies subsequent launches in this mode. + if appimage and ('APPIMAGE_EXTRACT_AND_RUN' in os.environ or Path(appdir).name.startswith('appimage_extracted_')): + module_args = ' --appimage-extract-and-run' + icon = (str(installed_icon()) if appimage else str(Path(appdir) / 'btd700-control.svg') if appdir + else str(ROOT / 'packaging/btd700-control.svg') if checkout else 'audio-headphones') # Desktop Entry Exec quoting is not shell quoting. escaped = path.replace('\\', '\\\\').replace('"', '\\"').replace('`', '\\`').replace('$', '\\$').replace('%', '%%') return ('[Desktop Entry]\nType=Application\nName=BTD 700 Control\n' @@ -29,9 +37,22 @@ def autostart_path(): return base / 'autostart' / f'{APP_ID}.desktop' +def installed_icon(): + base = Path(os.environ.get('XDG_DATA_HOME', Path.home() / '.local/share')) + return base / 'icons/hicolor/scalable/apps' / f'{APP_ID}.svg' + + +def install_appimage_icon(): + if os.environ.get('APPIMAGE') and os.environ.get('APPDIR'): + path = installed_icon() + path.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(Path(os.environ['APPDIR']) / 'btd700-control.svg', path) + + def set_autostart(enabled): path = autostart_path() if enabled: + install_appimage_icon() path.parent.mkdir(parents=True, exist_ok=True) path.write_text(desktop_entry(background=True)) else: diff --git a/docs/APPIMAGE.md b/docs/APPIMAGE.md new file mode 100644 index 0000000..150e90f --- /dev/null +++ b/docs/APPIMAGE.md @@ -0,0 +1,90 @@ +# AppImage packaging + +The x86-64 AppImage bundles Python 3.12, PyGObject, GTK 4, libadwaita, +libdbusmenu, icons and fallback fonts from Ubuntu 24.04. It requires a Linux +host with **glibc 2.39 or newer** and a graphical Wayland or X11 session. +It does not bundle glibc, audio drivers or firmware. ARM and older glibc +distributions are not supported by this binary; running from source is another option. + +## Download and use + +Download the `.AppImage` from [GitHub Releases](https://github.com/justin-eckenweber/btd700linux/releases/latest), +make it executable, and open it. All regular CLI arguments also work. + +```bash +chmod +x BTD_700_Control-*-x86_64.AppImage +./BTD_700_Control-0.3.0-x86_64.AppImage +./BTD_700_Control-0.3.0-x86_64.AppImage --demo +``` + +Put the file at its permanent location before adding a menu entry: + +```bash +./BTD_700_Control-0.3.0-x86_64.AppImage --install-desktop +``` + +The menu entry and **Start at login** use the original AppImage path, not its +temporary mount. Moving or renaming the file requires installing the menu entry +again and toggling Start at login off/on. `--remove-desktop` removes this app's +menu entry, autostart entry and installed icon. It leaves the AppImage in place. + +If FUSE is unavailable, use the runtime's extraction mode: + +```bash +APPIMAGE_EXTRACT_AND_RUN=1 ./BTD_700_Control-0.3.0-x86_64.AppImage +``` + +When installing a menu entry or enabling autostart from this mode, the launcher +preserves it using the runtime's `--appimage-extract-and-run` argument. + +For a permanent launcher on such a system, extract once with `--appimage-extract`, +keep the resulting `squashfs-root` directory, and run its `AppRun`. The launcher +created from that extracted copy uses its `AppRun` wrapper. Remove its +desktop/autostart entries before deleting the directory. + +GNOME still needs a working StatusNotifier/AppIndicator extension for a tray. +USB access uses the host's `/dev/hidraw` permissions. If access is denied, download +`70-btd700-control.rules` from the same release and install it as described in the +[README](../README.md#usb-permissions). The app never changes system permissions itself. + +## Build + +Run from the repository root using rootless Podman (Docker can also build the +same Containerfile): + +```bash +podman build -t btd700-appimage-builder -f packaging/appimage/Containerfile . +podman run --rm -v "$PWD:/src:z" btd700-appimage-builder +``` + +Verify the resulting file on a minimal test host without Python, GTK, libadwaita +or libdbusmenu installed (CLI, menu installation/removal, both GUI languages, +real D-Bus tray events against a simulated device, and extraction-mode startup): + +```bash +podman build -t btd700-appimage-test -f packaging/appimage/TestContainerfile . +podman run --rm -v "$PWD:/src:z" btd700-appimage-test +``` + +The output goes to `dist/`. Ubuntu source repositories are enabled in the builder. +`assemble.py` copies runtime files and follows their ELF dependencies, retaining +package versions and license texts. The build downloads the exact corresponding +Ubuntu source archives and publishes them in `*-dependency-sources.tar.gz`. +`*-packages.json` lists the binary and source versions. The container receives +current Ubuntu 24.04 security updates, so two builds at different times need not +be byte-identical. No host packages are installed by this process. + +Appimagetool 1.9.1 and type2-runtime 20251108 are checksum-pinned. The dependency +source archive also includes the runtime source, its build scripts and patches, +and the libfuse 3.15.0 and squashfuse 0.5.2 sources used by that runtime. To rebuild +the runtime, follow `BUILD.md` and `scripts/docker/` in that source archive; +the Dockerfile uses Alpine 3.21 and installs its build dependencies. The GNU LGPL +libraries in the AppDir remain dynamically linked and replaceable after extracting +the image. Repack a modified AppDir using appimagetool with `--runtime-file`. +For Ubuntu libraries, unpack their `.dsc` files with `dpkg-source -x`, install +the package's build dependencies and build with `dpkg-buildpackage` in Ubuntu 24.04. +For app modifications, rebuild from this project's tagged source and Containerfile. + +The release contains `SHA256SUMS` for the binary, dependency sources, package manifest +and USB rule. The project itself remains MIT-licensed; dependencies retain their +own licenses. See [third-party notices](../THIRD_PARTY_NOTICES.md). diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index 3c649f7..9ebaced 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -38,14 +38,30 @@ Connected BTD 700: USB `3542:3001`, firmware 3.11.0. - Extraction helper output matched the SHA256 of the original assembly used for protocol research. No vendor binaries were added to the distribution. -The CI workflow runs the unit tests on Python 3.10 and 3.14, in both languages. +## AppImage 0.3.0 + +- Built an x86-64 image from Ubuntu 24.04 runtime packages, with bundled Python + 3.12, GTK, libadwaita, libdbusmenu, SVG loading, MIME data, icons and fonts. +- Normal FUSE startup and `APPIMAGE_EXTRACT_AND_RUN=1` startup both displayed a + working demo window and registered the tray on Bazzite/GNOME. +- The bundled runtime passed the English and German form and D-Bus menu checks. +- The AppImage read the real BTD 700 status successfully: firmware 3.11.0, + Gaming, aptX Adaptive, 24-bit / 48 kHz, audio playing. No device settings changed. +- Menu installation/removal uses isolated temporary XDG directories. Autostart + tests cover persistent AppImage paths/icons, extracted AppDirs and no-FUSE mode. +- A minimal Ubuntu test container without Python, GTK, libadwaita or libdbusmenu + exercises the packaged CLI, SVG decoding, both GUI languages and D-Bus tray + controls with simulated hardware. The release workflow repeats these checks. + +The unit-test CI workflow runs on Python 3.10 and 3.14, in both languages. It does not run real hardware tests or claim that a particular receiver works. ## Not yet systematically verified - Hardware writes across different firmware revisions and all individual commands. - Real Auracast receiver compatibility, audio quality and reconnect behavior. -- Other distributions/desktops beyond the tested Bazzite/GNOME setup. +- Interactive desktops other than Bazzite/GNOME; Ubuntu GUI checks use Xvfb and + a private test tray watcher, not a full GNOME or KDE desktop. `tools/check_hardware.py --run` has not been executed. It deliberately interrupts audio, changes settings including the broadcast password, and attempts to restore diff --git a/install.py b/install.py index 89185e3..4b14b4b 100644 --- a/install.py +++ b/install.py @@ -4,7 +4,7 @@ import argparse import os from pathlib import Path from btd700.i18n import tr -from btd700.integration import APP_ID, autostart_path, desktop_entry +from btd700.integration import APP_ID, autostart_path, desktop_entry, install_appimage_icon, installed_icon def main(): @@ -16,12 +16,17 @@ def main(): if args.uninstall: destination.unlink(missing_ok=True) autostart_path().unlink(missing_ok=True) - print(tr('Menüeintrag und Autostart entfernt. Der Projektordner bleibt erhalten.')) + installed_icon().unlink(missing_ok=True) + print(tr('Menüeintrag und Autostart entfernt. Die Programmdateien bleiben erhalten.')) return destination.parent.mkdir(parents=True, exist_ok=True) + install_appimage_icon() destination.write_text(desktop_entry()) print(tr('Installiert: {path}').format(path=destination)) - print(tr('Im Anwendungsmenü „BTD 700 Control“ öffnen. Der Projektordner muss erhalten bleiben.')) + if os.environ.get('APPIMAGE') and os.environ.get('APPDIR'): + print(tr('Im Anwendungsmenü „BTD 700 Control“ öffnen. Die AppImage-Datei muss an diesem Ort bleiben.')) + else: + print(tr('Im Anwendungsmenü „BTD 700 Control“ öffnen. Der Projektordner muss erhalten bleiben.')) if __name__ == '__main__': diff --git a/packaging/appimage/AppRun b/packaging/appimage/AppRun new file mode 100644 index 0000000..52a90a2 --- /dev/null +++ b/packaging/appimage/AppRun @@ -0,0 +1,10 @@ +#!/bin/sh +set -eu +APPDIR=${APPDIR:-$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)} +export APPDIR +. "$APPDIR/usr/share/btd700-control/appimage-environment.sh" +case "${1:-}" in + --install-desktop) exec "$APPDIR/usr/bin/python3.12" "$APPDIR/usr/share/btd700-control/install.py" ;; + --remove-desktop) exec "$APPDIR/usr/bin/python3.12" "$APPDIR/usr/share/btd700-control/install.py" --uninstall ;; +esac +exec "$APPDIR/usr/bin/python3.12" -m btd700 "$@" diff --git a/packaging/appimage/Containerfile b/packaging/appimage/Containerfile new file mode 100644 index 0000000..90525b6 --- /dev/null +++ b/packaging/appimage/Containerfile @@ -0,0 +1,14 @@ +FROM docker.io/library/ubuntu:24.04 +ENV DEBIAN_FRONTEND=noninteractive +RUN sed -i 's/Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources \ + && apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y --no-install-recommends \ + python3 python3-gi python3-gi-cairo gir1.2-gtk-4.0 gir1.2-adw-1 \ + gir1.2-dbusmenu-glib-0.4 adwaita-icon-theme fonts-dejavu-core \ + ca-certificates curl squashfs-tools desktop-file-utils binutils libglib2.0-bin \ + xvfb xauth dbus-x11 \ + && rm -rf /var/cache/apt/archives/*.deb +RUN apt-get install -y --no-install-recommends librsvg2-common xz-utils file +WORKDIR /src +CMD ["bash", "packaging/appimage/build.sh"] diff --git a/packaging/appimage/RELEASE_NOTES.md b/packaging/appimage/RELEASE_NOTES.md new file mode 100644 index 0000000..78a01bf --- /dev/null +++ b/packaging/appimage/RELEASE_NOTES.md @@ -0,0 +1,23 @@ +Download **BTD_700_Control-0.3.0-x86_64.AppImage**, make it executable and open it. +Python, GTK, libadwaita and the tray library are included. Requires **x86-64 Linux +with glibc 2.39+** (such as Ubuntu 24.04 or newer). English and German are supported. + +```bash +chmod +x BTD_700_Control-0.3.0-x86_64.AppImage +./BTD_700_Control-0.3.0-x86_64.AppImage +``` + +Optional `--install-desktop` adds an application-menu entry. Keep the file at that +location; enable Start at login in the app for tray autostart. `--remove-desktop` +removes both entries. If FUSE is unavailable, prefix the launch command with +`APPIMAGE_EXTRACT_AND_RUN=1`. + +GNOME still needs a working AppIndicator/StatusNotifier extension. If USB access +is denied, install the attached udev rule following the README instructions. +SHA256SUMS covers the binary, package manifest, USB rule and dependency source archive. +The large `dependency-sources.tar.gz` is for reviewing/rebuilding libraries and is +not needed to run the app. GitHub's Source code archives contain the app itself. + +**Independent, AI-developed (OpenAI Codex), experimental software. No warranty or +guarantee of compatibility.** Project code is MIT-licensed; bundled dependencies +retain their own licenses. Controls only: no firmware updates, downloads or update APIs. diff --git a/packaging/appimage/TestContainerfile b/packaging/appimage/TestContainerfile new file mode 100644 index 0000000..7b01849 --- /dev/null +++ b/packaging/appimage/TestContainerfile @@ -0,0 +1,7 @@ +# Deliberately no Python, GTK, libadwaita or libdbusmenu installed on this host. +FROM docker.io/library/ubuntu:24.04 +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install -y --no-install-recommends \ + xvfb xauth dbus-x11 desktop-file-utils +WORKDIR /src +CMD ["dbus-run-session", "--", "xvfb-run", "-a", "bash", "packaging/appimage/check.sh"] diff --git a/packaging/appimage/assemble.py b/packaging/appimage/assemble.py new file mode 100644 index 0000000..ac8a563 --- /dev/null +++ b/packaging/appimage/assemble.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Bundle Ubuntu runtime files and record every contributing binary/source package.""" +import json +from pathlib import Path +import re +import shutil +import subprocess + +ROOT = Path(__file__).resolve().parents[2] +APPDIR = ROOT / 'build/appimage/BTD_700_Control.AppDir' +if APPDIR.exists(): + shutil.rmtree(APPDIR) +APPDIR.mkdir(parents=True) + + +def output(*args): + return subprocess.check_output(args, text=True) + + +packages = {} +owners = {} +for line in output('dpkg-query', '-W', '-f=${binary:Package}\t${Version}\t${source:Package}\t${source:Version}\n').splitlines(): + name, version, source, source_version = line.split('\t') + packages[name] = dict(package=name, version=version, source=source, source_version=source_version) + for path in output('dpkg-query', '-L', name).splitlines(): + owners[path] = name + if Path(path).is_file(): + owners[str(Path(path).resolve())] = name +included = set() + + +def copy(path, destination=None): + path = Path(path) + if path.name in {'__pycache__', 'icon-theme.cache', 'gschemas.compiled'} or path.suffix == '.pyc': + return + if path.is_dir(): + for child in sorted(path.iterdir()): + copy(child, Path(destination) / child.name if destination else None) + elif path.is_file(): + target = APPDIR / (str(destination) if destination else str(path).lstrip('/')) + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, target) + owner = owners.get(str(path)) or owners.get(str(path.resolve())) + if not owner and path.is_relative_to('/usr/share/mime'): + owner = owners['/usr/share/mime/packages/freedesktop.org.xml'] + if owner: + included.add(owner) + elif not path.is_relative_to(ROOT): + raise RuntimeError(f'No package provenance for {path}') + + +for path in ['/usr/bin/python3.12', '/usr/lib/python3.12', + '/usr/lib/python3/dist-packages/gi', '/usr/lib/python3/dist-packages/cairo', + '/usr/lib/x86_64-linux-gnu/girepository-1.0', + '/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders', + '/usr/share/glib-2.0/schemas', '/usr/share/icons/Adwaita', + '/usr/share/icons/hicolor', '/usr/share/fonts/truetype/dejavu', + '/usr/share/mime', + '/etc/fonts', '/usr/share/fontconfig']: + copy(path) +# Include GTK/libadwaita's German translations as well as the app's own catalog. +for path in Path('/usr/share/locale').glob('de*/LC_MESSAGES/*.mo'): + if path.name.startswith(('gtk40', 'libadwaita', 'glib20', 'gdk-pixbuf')): + copy(path) +roots = list(APPDIR.rglob('*.so')) + [APPDIR / 'usr/bin/python3.12'] +roots += [Path('/usr/lib/x86_64-linux-gnu') / name for name in + ['libgtk-4.so.1', 'libadwaita-1.so.0', 'libdbusmenu-glib.so.4']] +# ldd includes the transitive ELF dependencies. glibc and the ELF loader belong +# to the host; Ubuntu 24.04 (glibc 2.39) is the documented compatibility floor. +host_libraries = re.compile(r'^(ld-linux.*|lib(c|m|dl|rt|pthread|resolv|util|nss_.*)\.so\..*)$') +libraries = set(roots[-3:]) +for path in roots: + for line in output('ldd', str(path)).splitlines(): + if 'not found' in line: + raise RuntimeError(f'Missing dependency of {path}: {line}') + match = re.search(r'=> (/\S+)', line) + if match and not host_libraries.match(Path(match[1]).name): + libraries.add(Path(match[1])) +for path in sorted(libraries): + copy(path, 'usr/lib/x86_64-linux-gnu/' + path.name) + +copy(ROOT / 'btd700', 'usr/share/btd700-control/btd700') +for name in ['install.py', 'LICENSE', 'THIRD_PARTY_NOTICES.md']: + copy(ROOT / name, 'usr/share/btd700-control/' + name) +copy(ROOT / 'packaging/btd700-control.svg', 'btd700-control.svg') +copy(ROOT / 'packaging/btd700-control.svg', 'usr/share/icons/hicolor/scalable/apps/btd700-control.svg') +copy(ROOT / 'packaging/70-btd700-control.rules', 'usr/share/btd700-control/packaging/70-btd700-control.rules') +copy(ROOT / 'packaging/appimage/AppRun', 'AppRun') +copy(ROOT / 'packaging/appimage/environment.sh', 'usr/share/btd700-control/appimage-environment.sh') +(APPDIR / 'AppRun').chmod(0o755) +copy(ROOT / 'packaging/appimage/btd700-control.desktop', 'btd700-control.desktop') +(APPDIR / '.DirIcon').symlink_to('btd700-control.svg') +subprocess.run(['desktop-file-validate', str(APPDIR / 'btd700-control.desktop')], check=True) +subprocess.run(['glib-compile-schemas', str(APPDIR / 'usr/share/glib-2.0/schemas')], check=True) +cache = output('/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/gdk-pixbuf-query-loaders') +# A bare module name is resolved by dlopen through our LD_LIBRARY_PATH. This +# avoids storing a container or temporary AppImage mount path in the cache. +cache = re.sub(r'^"[^"\n]*/([^/"\n]+\.so)"$', r'"\1"', cache, flags=re.MULTILINE) +(APPDIR / 'usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders.cache').write_text(cache) +# Make the bundled fonts available without relying on a host font installation. +font_config = APPDIR / 'etc/fonts/fonts.conf' +font_config.write_text(font_config.read_text().replace('