Skip to content

Instantly share code, notes, and snippets.

@meyt
Last active February 11, 2025 17:20
Show Gist options
  • Save meyt/4d5421501f80fd829b53abb9d9ce9c21 to your computer and use it in GitHub Desktop.
Save meyt/4d5421501f80fd829b53abb9d9ce9c21 to your computer and use it in GitHub Desktop.
AppImage installer (link to PATH, create desktop file and icons)
#!/usr/bin/env python3
#
# Requirements:
# sudo apt install -y binutils xdg-utils desktop-file-utils squashfuse
#
# Usage:
# python3 install_appimage.py app1.appimage app2.appimage ...
import os
import shutil
import subprocess
import sys
from pathlib import Path
APPS_DIR = os.path.expanduser("~/.local/share/applications")
ICONS_DIR = os.path.expanduser("~/.local/share/icons")
class DesktopEntry:
def __init__(self, file_path=None):
self.entries = {}
if file_path:
self.load(file_path)
def load(self, file_path):
"""Load a .desktop file."""
with open(file_path, "r", encoding="utf-8") as file:
current_section = None
for line in file:
line = line.strip()
# Skip comments and empty lines
if not line or line.startswith("#"):
continue
# Handle section headers
if line.startswith("[") and line.endswith("]"):
current_section = line[1:-1]
self.entries[current_section] = {}
# Handle key-value pairs
elif "=" in line:
key, value = line.split("=", 1)
key = key.strip()
value = value.strip()
if current_section:
self.entries[current_section][key] = value
def dump(self, file_path):
"""Dump the current entries to a .desktop file."""
with open(file_path, "w", encoding="utf-8") as file:
for section, options in self.entries.items():
file.write(f"[{section}]\n")
for key, value in options.items():
file.write(f"{key}={value}\n")
file.write("\n")
def get(self, section, key, default=None):
"""Get a value from a specific section."""
return self.entries.get(section, {}).get(key, default)
def set(self, section, key, value):
"""Set a value in a specific section."""
if section not in self.entries:
self.entries[section] = {}
self.entries[section][key] = value
def __str__(self):
"""Return the .desktop file as a string."""
result = []
for section, options in self.entries.items():
result.append(f"[{section}]")
for key, value in options.items():
result.append(f"{key}={value}")
result.append("")
return "\n".join(result)
def cmd(cmd, check=True):
"""Run a shell command and return its output."""
result = subprocess.run(
cmd, shell=True, check=check, text=True, capture_output=True
)
return result.stdout.strip()
def readelf(path):
elf_info = {}
lines = cmd(f"readelf -h {path}").strip().split("\n")
for line in lines:
if not line.strip():
continue
if line.startswith("Magic"):
key, value = line.split(":", 1)
elf_info[key.strip()] = value.strip()
continue
key_value = line.split(":", 1)
if len(key_value) == 2:
key, value = key_value
elf_info[key.strip()] = value.strip()
return elf_info
def install(appimage):
img = Path(appimage).resolve()
# img_filename = img.name
img_name = img.stem
mountpoint = Path(f"/tmp/appimg-{img_name}")
print(f"> Installing '{img}'")
# Calculate squashfs offset
elf = readelf(img)
sec_start = int(elf["Start of section headers"].split()[0])
sec_size = int(elf["Size of section headers"].split()[0])
sec_no = int(elf["Number of section headers"].split()[0])
img_offset = sec_start + sec_size * sec_no
# Mount the image
print(f"> Mount the image on '{mountpoint}'")
if not mountpoint.exists():
mountpoint.mkdir()
elif os.path.ismount(mountpoint):
cmd(f"umount {mountpoint}")
cmd(f"squashfuse -o offset={img_offset} {img} {mountpoint}")
# Find the .desktop file
img_desktop = next(mountpoint.glob("*.desktop"), None)
if not img_desktop:
print("Error: No .desktop file found in the AppImage.")
sys.exit(1)
img_desktop_filename = img_desktop.name
# Extract Exec command from .desktop file
desktop_content = DesktopEntry(img_desktop)
desktop_entry = desktop_content.entries.get("Desktop Entry")
icon_name = None
if desktop_entry:
icon_name = desktop_entry["Icon"]
desktop_entry["Exec"] = appimage
desktop_file = Path(APPS_DIR) / f"appimage-{img_desktop_filename}"
desktop_content.dump(desktop_file)
cmd(f"update-desktop-database {APPS_DIR}", check=False)
print(f"> Install desktop file on '{desktop_file}'")
# Copy icons
if icon_name:
print("> Install icons")
# https://specifications.freedesktop.org/icon-theme-spec/latest/
for icon_ext in ("svg", "png", "xpm"):
img_icon = mountpoint / f"{icon_name}.{icon_ext}"
if not img_icon.exists():
continue
img_icon_dest = Path(ICONS_DIR) / f"{icon_name}.{icon_ext}"
shutil.copy(img_icon, img_icon_dest)
print(f"> Icon '{img_icon_dest}' installed")
# Unmount and clean up
print(f"> Unmount '{mountpoint}'")
cmd(f"umount {mountpoint}")
mountpoint.rmdir()
print("> The AppImage installed successfully.")
def main(appimages):
for img in appimages:
install(img)
if __name__ == "__main__":
if len(sys.argv) < 2:
print(
f"Usage: {sys.argv[0]} /path/to/appimage1 /path/to/appimage2 ..."
)
sys.exit(1)
main(sys.argv[1:])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment