Skip to content

Commit

Permalink
linux gui installer (#41)
Browse files Browse the repository at this point in the history
* refractor bash script writing into `installer_utils` folder

* Fix packager for GUIs, add installer for GUIs on Linux

* update docs for linux gui installer
  • Loading branch information
trappitsch committed Apr 10, 2024
1 parent fb45eb0 commit ac12e5f
Show file tree
Hide file tree
Showing 15 changed files with 434 additions and 57 deletions.
2 changes: 1 addition & 1 deletion docs/.includes/installer_cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
It can be run - like any other bash script - as following:

```
./projectname-v1.2.3-installer.sh
./projectname-v1.2.3-linux.sh
```

The installer will ask the user for the target directory (defaults to `/usr/local/bin`)
Expand Down
35 changes: 34 additions & 1 deletion docs/.includes/installer_gui.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,39 @@
=== "Linux"

GUI installers on Linux are currently not supported.
The created installer is an executable bash script
that contains bash installation instructions,
the binary program,
and the program icon.
It can be run - like any other bash script - as following:

```
./projectname-v1.2.3-linux.sh
```

The installer will ask the user for the target directory
(defaults to `$HOME/.local/share/projectname`)
and the path to copy the `.desktop` file to
(defaults to `$HOME/.local/share/applications`).

The installer will copy the binary and the icon to the target directory,
will create an uninstaller bash script in the target directory,
and create a `.desktop` file in the specified path.
It will also make the copied binary, desktop file, and uninstaller executable.

The installer script has a few checks to ensure that the installation is successful:

- It checks if the target directory exists.
If it exists, the script will check with the user if it should continue,
otherwise it will create the folder.
- It checks if the target and desktop file directories are ritable, if not, tells the user to run the installer with `sudo`.
- It checks if the files already exists in the target directory, if so asks the user if it should overwrite it or not and proceeds accordingly.

The binary itself is included in the installer script below the line marked with
`#__PROGRAM_BINARY__` and before the line marked with `#__ICON_BINARY__`.
The icon itself is included in the installer script below the line marked with
`#__ICON_BINARY__`.
The uninstaller is created from within the bash script itself.


=== "Windows"

Expand Down
9 changes: 9 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,15 @@ in the `target/release` folder.

### GUIs

In order to package a GUI,
you should have an icon file in the `assets` folder in your project.
For Linux, the icon file should be a `svg`, `png`, `jpg`, or `jpeg` file.
For Windows, the icon file should be a `ico` file.
In either case, the icon file(s) must be named `icon.<ext>`,
where `<ext>` is the file extension.
If multiple icons are available,
order of preference is `svg`, `png`, `jpg`, `jpeg`.

{% include-markdown ".includes/installer_gui.md" %}

## Cleaning your project
Expand Down
120 changes: 71 additions & 49 deletions src/box/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import rich_click as click

from box import RELEASE_DIR_NAME
from box.installer_utils import linux_cli, linux_gui
from box.config import PyProjectParser
import box.formatters as fmt

Expand Down Expand Up @@ -34,6 +35,8 @@ def __init__(self):

if self._os == "Linux" and self._mode == "CLI":
self.linux_cli()
elif self._os == "Linux" and self._mode == "GUI":
self.linux_gui()
else:
self.unsupported_os_or_mode()

Expand All @@ -47,55 +50,8 @@ def linux_cli(self) -> None:
name_pkg = self._config.name_pkg
version = self._config.version

# Bash part of the release file
bash_part = rf"""#!/bin/bash
# This is a generated installer for {name_pkg} v{version}
# Default installation name and folder
INSTALL_NAME={name_pkg}
INSTALL_DIR=/usr/local/bin
# Check if user has a better path:
read -p "Enter the installation path (default: $INSTALL_DIR): " USER_INSTALL_DIR
if [ ! -z "$USER_INSTALL_DIR" ]; then
INSTALL_DIR=$USER_INSTALL_DIR
fi
# Check if installation folder exists
if [ ! -d "$INSTALL_DIR" ]; then
echo "Error: Installation folder does not exist."
exit 1
fi
# Check if installation folder requires root access
if [ ! -w "$INSTALL_DIR" ]; then
echo "Error: Installation folder requires root access. Please run with sudo."
exit 1
fi
INSTALL_FILE=$INSTALL_DIR/$INSTALL_NAME
# check if installation file already exist and if it does, ask if overwrite is ok
if [ -f "$INSTALL_FILE" ]; then
read -p "File already exists. Overwrite? (y/n): " OVERWRITE
if [ "$OVERWRITE" != "y" ]; then
echo "Installation aborted."
exit 1
fi
fi
if ! [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then\
echo "$INSTALL_DIR is not on your PATH. Please add it."
fi
sed -e '1,/^#__PROGRAM_BINARY__$/d' "$0" > $INSTALL_FILE
chmod +x $INSTALL_FILE
echo "Successfully installed $INSTALL_NAME to $INSTALL_DIR"
exit 0
#__PROGRAM_BINARY__
"""
bash_part = linux_cli.create_bash_installer(name_pkg, version)

with open(self.release_file, "rb") as f:
binary_part = f.read()

Expand All @@ -114,6 +70,37 @@ def linux_cli(self) -> None:
mode |= (mode & 0o444) >> 2
os.chmod(installer_file, mode)

def linux_gui(self) -> None:
"""Create a Linux GUI installer."""
name_pkg = self._config.name_pkg
version = self._config.version
icon = get_icon()
icon_name = icon.name

bash_part = linux_gui.create_bash_installer(name_pkg, version, icon_name)

with open(self.release_file, "rb") as f:
binary_part = f.read()

with open(icon, "rb") as f:
icon_part = f.read()

installer_file = Path(RELEASE_DIR_NAME).joinpath(
f"{name_pkg}-v{version}-linux.sh"
)
with open(installer_file, "wb") as f:
f.write(bash_part.encode("utf-8"))
f.write(binary_part)
f.write(b"\n#__ICON_BINARY__\n")
f.write(icon_part)

self._installer_name = installer_file.name

# make installer executable
mode = os.stat(installer_file).st_mode
mode |= (mode & 0o444) >> 2
os.chmod(installer_file, mode)

def unsupported_os_or_mode(self):
"""Print a message for unsupported OS or mode."""
fmt.warning(
Expand All @@ -130,3 +117,38 @@ def _check_release(self) -> Path:
if not release_file.exists():
raise click.ClickException("No release found. Run `box package` first.")
return release_file


def get_icon(suffix: str = None) -> Path:
"""Return the icon file path.
If no suffix is provided, the following priorites will be returned (depending
on file availability):
- icon.svg
- icon.png
- icon.jpg
- icon.jpeg
Note: Windows `.ico` files must be called out explicitly.
:param suffix: The suffix of the icon file.
:return: The path to the icon file.
:raises ClickException: If no icon file is found.
"""
icon_file = Path.cwd().joinpath("assets/icon")

suffixes = ["svg", "png", "jpg", "jpeg"]
if suffix:
suffixes = [suffix] # overwrite exisiting and only check this.

for suffix in suffixes:
icon_file = icon_file.with_suffix(f".{suffix}")
if icon_file.exists():
return icon_file

raise click.ClickException(
f"No icon file found. Please provide an icon file. "
f"Valid formats are {', '.join(suffixes)}."
)
File renamed without changes.
59 changes: 59 additions & 0 deletions src/box/installer_utils/linux_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Helper functions to create a linux CLI installer.


def create_bash_installer(name_pkg, version) -> str:
"""Create a bash installer for a CLI application.
:param name_pkg: The name of the program.
:param version: The version of the program.
:return: The bash installer content.
"""
return rf"""#!/bin/bash
# This is a generated installer for {name_pkg} v{version}
# Default installation name and folder
INSTALL_NAME={name_pkg}
INSTALL_DIR=/usr/local/bin
# Check if user has a better path:
read -p "Enter the installation path (default: $INSTALL_DIR): " USER_INSTALL_DIR
if [ ! -z "$USER_INSTALL_DIR" ]; then
INSTALL_DIR=$USER_INSTALL_DIR
fi
# Check if installation folder exists
if [ ! -d "$INSTALL_DIR" ]; then
echo "Error: Installation folder does not exist."
exit 1
fi
# Check if installation folder requires root access
if [ ! -w "$INSTALL_DIR" ]; then
echo "Error: Installation folder requires root access. Please run with sudo."
exit 1
fi
INSTALL_FILE=$INSTALL_DIR/$INSTALL_NAME
# check if installation file already exist and if it does, ask if overwrite is ok
if [ -f "$INSTALL_FILE" ]; then
read -p "File already exists. Overwrite? (y/n): " OVERWRITE
if [ "$OVERWRITE" != "y" ]; then
echo "Installation aborted."
exit 1
fi
fi
if ! [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then\
echo "$INSTALL_DIR is not on your PATH. Please add it."
fi
sed -e '1,/^#__PROGRAM_BINARY__$/d' "$0" > $INSTALL_FILE
chmod +x $INSTALL_FILE
echo "Successfully installed $INSTALL_NAME to $INSTALL_DIR"
exit 0
#__PROGRAM_BINARY__
"""

0 comments on commit ac12e5f

Please sign in to comment.