Ship AppImage with bundled GTK runtime and automated releases
AppImage / build (push) Has been cancelled
AppImage / release (push) Has been cancelled
Tests / unit (de, 3.10) (push) Has been cancelled
Tests / unit (de, 3.14) (push) Has been cancelled
Tests / unit (en, 3.10) (push) Has been cancelled
Tests / unit (en, 3.14) (push) Has been cancelled

This commit is contained in:
Justin
2026-09-05 21:32:30 +02:00
parent dd0809e877
commit 3de4ca48e0
25 changed files with 690 additions and 32 deletions
+124
View File
@@ -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('<fontconfig>',
'<fontconfig>\n <dir prefix="relative">../../usr/share/fonts</dir>'))
# License texts accompany the binary; complete corresponding Ubuntu source
# archives are downloaded separately by build.sh and attached to the release.
copy('/usr/share/common-licenses')
for package in sorted(included):
doc = Path('/usr/share/doc') / package.split(':')[0] / 'copyright'
if not doc.is_file():
raise RuntimeError(f'No copyright file for {package}')
copy(doc, 'usr/share/doc/btd700-bundled/' + package.replace(':', '_') + '.copyright')
manifest = [packages[name] for name in sorted(included)]
(APPDIR / 'usr/share/doc/btd700-bundled/packages.json').write_text(json.dumps(manifest, indent=2) + '\n')
(ROOT / 'build/appimage/packages.json').write_text(json.dumps(manifest, indent=2) + '\n')
sources = sorted({f"{p['source']}={p['source_version']}" for p in manifest})
(ROOT / 'build/appimage/sources.txt').write_text('\n'.join(sources) + '\n')
# Do not ship bytecode, Python tests, or compilation-only metadata.
for path in list(APPDIR.rglob('__pycache__')):
shutil.rmtree(path)
for path in [APPDIR / 'usr/lib/python3.12/test', APPDIR / 'usr/lib/python3.12/config-3.12-x86_64-linux-gnu']:
if path.exists():
shutil.rmtree(path)
print(f'Assembled {APPDIR}: {len(manifest)} binary packages, {len(sources)} source packages')