Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

installer cleanup #46

Merged
merged 7 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/integration_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
cd qtcowsay
box init -q -b build
box package
box installer
- name: Checkout cowsay-python
uses: actions/checkout@v4
with:
Expand All @@ -47,3 +48,4 @@ jobs:
git checkout 3db622cefd8b11620ece7386d4151b5e734b078b
box init -q -b build
box package
box installer
4 changes: 3 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
- Released binary is now named after the project name, not after the python package name
- Improvements to packaging: If PyApp fails and no binary exists, are more useful error message is provided.
- Add command `box installer` to create an installer for the packaged program.
- CLI on Linux: Install via a `bash` script with embedded binary.
- GUI on Linux: Install via a `bash` script with embedded binary and icon.
- CLI on Windows: Installer created using [NSIS](https://nsis.sourceforge.io/Main_Page).
- GUI on Windows: Installer created using [NSIS](https://nsis.sourceforge.io/Main_Page).
- Improvements to packaging: If PyApp fails and no binary exists, are more useful error message is provided.

## v0.1.0

Expand Down
17 changes: 9 additions & 8 deletions src/box/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
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
import box.utils as ut
Expand Down Expand Up @@ -64,18 +63,18 @@ def create_installer(self):

def linux_cli(self) -> None:
"""Create a Linux CLI installer."""
name_pkg = self._config.name_pkg
from box.installer_utils.linux_hlp import create_bash_installer_cli

name = self._config.name
version = self._config.version

bash_part = linux_cli.create_bash_installer(name_pkg, version)
bash_part = create_bash_installer_cli(name, version)

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

# Write the installer file
installer_file = Path(RELEASE_DIR_NAME).joinpath(
f"{name_pkg}-v{version}-linux.sh"
)
installer_file = Path(RELEASE_DIR_NAME).joinpath(f"{name}-v{version}-linux.sh")
with open(installer_file, "wb") as f:
f.write(bash_part.encode("utf-8"))
f.write(binary_part)
Expand All @@ -89,12 +88,14 @@ def linux_cli(self) -> None:

def linux_gui(self) -> None:
"""Create a Linux GUI installer."""
from box.installer_utils.linux_hlp import create_bash_installer_gui

name = self._config.name
version = self._config.version
icon = get_icon()
icon_name = icon.name

bash_part = linux_gui.create_bash_installer(name, version, icon_name)
bash_part = create_bash_installer_gui(name, version, icon_name)

with open(self._release_file, "rb") as f:
binary_part = f.read()
Expand Down Expand Up @@ -208,7 +209,7 @@ def _check_release(self) -> Path:

:return: Path to the release.
"""
release_file = Path(RELEASE_DIR_NAME).joinpath(self._config.name_pkg)
release_file = Path(RELEASE_DIR_NAME).joinpath(self._config.name)

if sys.platform == "win32":
release_file = release_file.with_suffix(".exe")
Expand Down
59 changes: 0 additions & 59 deletions src/box/installer_utils/linux_cli.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,65 @@
# Helper functions to create a linux GUI installer.


def create_bash_installer(name_pkg, version, icon_name) -> str:
def create_bash_installer_cli(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__
"""


def create_bash_installer_gui(name_pkg, version, icon_name) -> str:
"""Create a bash installer for a GUI application.

:param name_pkg: The name of the program.
Expand Down
6 changes: 3 additions & 3 deletions src/box/packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,9 +243,9 @@ def _package_pyapp(self):
"No binary created. Please check build process with `box package -v`."
)

self._binary_name = self._release_dir.joinpath(
self.config.name_pkg
).with_suffix(binary_path.suffix)
self._binary_name = self._release_dir.joinpath(self.config.name).with_suffix(
binary_path.suffix
)
shutil.move(binary_path, self._binary_name)

def _set_env(self):
Expand Down
33 changes: 20 additions & 13 deletions tests/cli/test_cli_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@
from box import config


def setup_mock_target_binary(path: Path) -> str:
"""Set up a mock binary in the target/release folder of the given path."""
def setup_mock_target_binary(path: Path, release_name: str) -> str:
"""Set up a mock binary in the target/release folder of the given path.

:param path: The path to the project.
:param release_name: The name of the release.
"""
target_dir = path.joinpath("target/release")
target_dir.mkdir(parents=True)
target_file = target_dir.joinpath(path.name.lower())
target_file = target_dir.joinpath(release_name)
if sys.platform == "win32":
target_file = target_file.with_suffix(".exe")
target_file_content = "This is the content of the mock binary file..."
Expand Down Expand Up @@ -56,30 +60,31 @@ def test_installer_no_binary(rye_project, platform, mocker):
@pytest.mark.skipif("sys.platform == 'win32'", reason="Not supported on Windows")
def test_installer_cli_linux(rye_project):
"""Create installer for linux CLI."""
installer_fname_exp = f"{rye_project.name}-v0.1.0-linux.sh"
target_file_content = setup_mock_target_binary(rye_project)
conf = config.PyProjectParser()
installer_fname_exp = f"{conf.name}-v0.1.0-linux.sh"
target_file_content = setup_mock_target_binary(rye_project, conf.name)

# run the CLI
runner = CliRunner()
result = runner.invoke(cli, ["installer"])

assert result.exit_code == 0
# assert result.exit_code == 0

# assert the installer file was created
installer_file = rye_project.joinpath(f"target/release/{installer_fname_exp}")
assert installer_file.name in result.output

assert installer_file.exists()
assert target_file_content in installer_file.read_text()
assert os.stat(installer_file).st_mode & stat.S_IXUSR != 0

assert installer_file.name in result.output


@pytest.mark.skipif("sys.platform == 'win32'", reason="Not supported on Windows")
def test_installer_gui_linux(rye_project):
"""Create installer for linux GUI."""
conf = config.PyProjectParser()
installer_fname_exp = f"{conf.name}-v0.1.0-linux.sh"
target_file_content = setup_mock_target_binary(rye_project)
target_file_content = setup_mock_target_binary(rye_project, conf.name)
icon_file_content = setup_mock_icon(rye_project)

# make it a GUI project
Expand Down Expand Up @@ -123,7 +128,7 @@ def test_installer_cli_windows(rye_project, mocker, verbose):

conf = config.PyProjectParser()
installer_fname_exp = f"{conf.name}-v0.1.0-win.exe"
_ = setup_mock_target_binary(rye_project)
_ = setup_mock_target_binary(rye_project, conf.name)
# create the installer binary
installer_binary = rye_project.joinpath(f"target/release/{installer_fname_exp}")
installer_binary.touch()
Expand Down Expand Up @@ -151,7 +156,8 @@ def test_installer_cli_windows_not_created(rye_project, mocker):
mocker.patch("sys.platform", "win32")
mocker.patch("subprocess.run")

_ = setup_mock_target_binary(rye_project)
conf = config.PyProjectParser()
_ = setup_mock_target_binary(rye_project, conf.name)

runner = CliRunner()
result = runner.invoke(cli, ["installer"])
Expand All @@ -174,7 +180,7 @@ def test_installer_gui_windows(rye_project, mocker, verbose):

conf = config.PyProjectParser()
installer_fname_exp = f"{conf.name}-v0.1.0-win.exe"
_ = setup_mock_target_binary(rye_project)
_ = setup_mock_target_binary(rye_project, conf.name)
_ = setup_mock_icon(rye_project, ico=True)
# create the installer binary
installer_binary = rye_project.joinpath(f"target/release/{installer_fname_exp}")
Expand Down Expand Up @@ -211,7 +217,8 @@ def test_not_implemented_installers(rye_project, mocker, platform):
if platform == "darwin":
os_exp = "macOS"

_ = setup_mock_target_binary(rye_project)
conf = config.PyProjectParser()
_ = setup_mock_target_binary(rye_project, conf.name)

runner = CliRunner()
result = runner.invoke(cli, ["installer"])
Expand Down
5 changes: 2 additions & 3 deletions tests/unit/test_packager.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,8 @@ def test_package_pyapp_cargo_and_move(rye_project, mocker, binary_extensions):
stdout=sp_devnull_mock,
stderr=sp_devnull_mock,
)
exp_binary = rye_project.joinpath(
f"target/release/{rye_project.name}{binary_extensions}"
)
conf = PyProjectParser()
exp_binary = rye_project.joinpath(f"target/release/{conf.name}{binary_extensions}")
assert exp_binary.is_file()
assert exp_binary.read_text() == "not really a binary"

Expand Down
Loading