Last active
February 11, 2025 17:20
-
-
Save meyt/4d5421501f80fd829b53abb9d9ce9c21 to your computer and use it in GitHub Desktop.
AppImage installer (link to PATH, create desktop file and icons)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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