Skip to content

Instantly share code, notes, and snippets.

@mnesarco
Created December 27, 2020 13:25
Show Gist options
  • Select an option

  • Save mnesarco/baa2d49d9ec1b1edb41be7202cac352d to your computer and use it in GitHub Desktop.

Select an option

Save mnesarco/baa2d49d9ec1b1edb41be7202cac352d to your computer and use it in GitHub Desktop.

Revisions

  1. mnesarco created this gist Dec 27, 2020.
    200 changes: 200 additions & 0 deletions build-installer.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,200 @@
    #!/usr/bin/python3
    #
    # Copyright 2020 Frank David Martinez M. (mnesarco at github)
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    # http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    #

    # +------------------------------------------------------------------------+
    # | AppImage Auto Installer Builder |
    # +------------------------------------------------------------------------+
    # | |
    # | Converts an AppImage into a POSIX shell script that will |
    # | deploy the AppImage to $HOME/.appimage and create valid icon |
    # | and desktop launcher in $HOME/.local/shared/... |
    # | |
    # | usage: build-installer.py [-h] --appimage IMAGE [--exe-name EXE] |
    # | [--app-name NAME] [--app-category CATEGORY] |
    # | [--icon ICON] [--appimage-version VERSION] |
    # | |
    # | AppImage Installer Builder |
    # | |
    # | arguments: |
    # | -h, --help show this help message and exit |
    # | --appimage IMAGE, -a IMAGE |
    # | AppImage file |
    # | --exe-name EXE, -e EXE |
    # | Final name of the installed executable |
    # | --app-name NAME, -n NAME |
    # | Final name of the application |
    # | --app-category CATEGORY, -c CATEGORY |
    # | Application's category |
    # | --icon ICON, -i ICON Icon file path |
    # | --appimage-version VERSION, --iv VERSION |
    # | AppImage version |
    # | |
    # | Result: |
    # | The result is a shell script named ${EXE}-installer.sh ready to be |
    # | distributed. The final user downloads the installer and execute it. |
    # | after that, the AppImage is installed and have a launcher. |
    # +------------------------------------------------------------------------+

    import sys, argparse, os, re, stat

    # -------------------------
    # CLI Arguments
    # -------------------------

    parser = argparse.ArgumentParser(description='AppImage Installer Builder')
    parser.add_argument('--appimage', '-a', help='AppImage file', required=True, dest="image")
    parser.add_argument('--exe-name', '-e', help='Final name of the installed executable', dest="exe")
    parser.add_argument('--app-name', '-n', help='Final name of the application', dest="name")
    parser.add_argument('--app-category', '-c', help='Application\'s category', dest="category")
    parser.add_argument('--icon', '-i', help='Icon file path', dest="icon")
    parser.add_argument('--appimage-version', '--iv', help='AppImage version', dest="version")
    args = parser.parse_args()

    if not os.path.exists(args.image):
    print("File {0} does not exists".format(args.image))
    exit(1)

    if not args.exe:
    args.exe = re.compile(r'^[^\-]+').search(args.image).group()

    if not args.name:
    args.name = args.exe

    if not args.category:
    args.category = "Utility"

    if not args.version:
    ver = re.compile(r'^[^\-]+-([^\-]+)').search(args.image)
    if ver:
    args.version = ver.group(1)
    else:
    args.version = '1'

    if not args.icon:
    args.icon = 'icon.svg'
    if not os.path.exists(args.icon):
    args.icon = 'icon.png'

    if not os.path.exists(args.icon):
    print("File {0} does not exists".format(args.icon))
    exit(1)

    if args.icon.lower().endswith('.svg'):
    icon_type = 'scalable'
    else:
    icon_type = '128x128'

    icon_name = args.exe.lower()

    # -------------------------
    # XDG Desktop file
    # -------------------------

    desktop="""
    [Desktop Entry]
    Name={name}
    Exec=$HOME/.appimage/{exe} %f
    Icon={icon}
    Type=Application
    Categories={cat};
    X-AppImage-Integrate=true
    Name[en_US]={exe}.desktop
    X-AppImage-Version={ver}
    TryExec=$HOME/.appimage/{exe}
    Terminal=false
    Comment=
    """.format(name=args.name, exe=args.exe, icon=icon_name, cat=args.category, ver=args.version)

    # -------------------------
    # File offsets
    # -------------------------

    icon_size = os.stat(args.icon).st_size
    image_size = os.stat(args.image).st_size
    icon_start = 1024
    image_start = icon_start + icon_size

    # -------------------------
    # File paths
    # -------------------------

    icon_dir="$HOME/.local/share/icons/hicolor/{0}/apps".format(icon_type)
    icon_file="{0}/{1}.{2}".format(icon_dir, icon_name, 'svg' if icon_type == 'scalable' else 'png')

    desktop_dir="$HOME/.local/share/applications"
    desktop_file="{0}/{1}.desktop".format(desktop_dir, args.exe)

    image_dir="$HOME/.appimage"
    image_file="{0}/{1}".format(image_dir, args.exe)

    # -------------------------
    # Shell script
    # -------------------------

    script = """#!/bin/bash
    cat > _tmp_desktop <<EOM{desktop}
    EOM
    mkdir -p "{icon_dir}"
    dd if="$0" of={icon_file} bs=1 skip={icon_start} count={icon_size}
    mkdir -p "{desktop_dir}"
    mv _tmp_desktop {desktop_file}
    chmod +x {desktop_file}
    gio set {desktop_file} "metadata::trusted" yes
    mkdir -p "{image_dir}"
    tail -c{image_size} "$0" > {image_file}
    chmod +x {image_file}
    ln -sf {desktop_file}
    exit 0
    """.format(
    desktop=desktop,
    icon_dir=icon_dir,
    icon_file=icon_file,
    icon_start=icon_start,
    icon_size=icon_size,
    desktop_dir=desktop_dir,
    desktop_file=desktop_file,
    image_dir=image_dir,
    image_file=image_file,
    image_start=image_start,
    image_size=image_size,
    exe=args.exe)

    # -------------------------
    # Pad alignment
    # -------------------------

    pad = icon_start - len(script.encode('ascii'))
    script += "#" * pad

    # -------------------------
    # Write Installer
    # -------------------------

    installer = "{exe}-installer.sh".format(exe=args.exe)
    with open(installer, 'wb') as f:
    f.write(script.encode('ascii'))
    with open(args.icon, 'rb') as icon_f:
    f.write(icon_f.read())
    with open(args.image, 'rb') as image_f:
    while True:
    dat = image_f.read(8192)
    if not dat:
    break
    f.write(dat)

    st = os.stat(installer)
    os.chmod(installer, st.st_mode | stat.S_IEXEC)