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

windows cli installer #45

Merged
merged 6 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
16 changes: 15 additions & 1 deletion docs/.includes/installer_cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,21 @@

=== "Windows"

CLI installers on Windows are currently not supported.
Windows installers are created using
[NSIS](https://nsis.sourceforge.io/Main_Page).
You must ensure that NSIS is installed and available on the system path.
The installer is an executable in `target/release/projectname-v1.2.3-win.exe`
that can be run by double-clicking it.

The installer will ask the user for the target directory.
It will then copy the binary to the target directory and create an uninstaller.

When using the uninstaller that is created with NSIS, all PyApp data from this project will be removed as well in order to provide the user with a clean uninstallation.

!!! warning
The installer will not add the install directory to the `PATH` variable.
You or the user must do this manually.
This is also stated on the last page of the installer.

=== "macOS"

Expand Down
24 changes: 18 additions & 6 deletions src/box/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
"""CLI for box-packager."""

from pathlib import Path

import rich_click as click

import box
from box.cleaner import CleanProject
from box.config import uninitialize
from box.initialization import InitializeProject
Expand Down Expand Up @@ -146,12 +151,19 @@ def installer(verbose):
ut.check_boxproject()
my_installer = CreateInstaller(verbose=verbose)
my_installer.create_installer()
if (inst_name := my_installer.installer_name) is not None:
fmt.success(
f"Installer successfully created.\n"
f"You can find the installer file {inst_name} "
f"in the `target/release` folder."
)
inst_name = my_installer.installer_name
if inst_name is not None:
if Path(box.RELEASE_DIR_NAME).joinpath(inst_name).exists():
fmt.success(
f"Installer successfully created.\n"
f"You can find the installer file {inst_name} "
f"in the `target/release` folder."
)
else:
raise click.ClickException(
"Installer was not created. "
"Run with `box installer -v` to get verbose feedback."
)


@cli.command(name="clean")
Expand Down
53 changes: 41 additions & 12 deletions src/box/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ def create_installer(self):
self.linux_cli()
elif self._os == "Linux" and self._mode == "GUI":
self.linux_gui()
# elif self._os == "Windows" and self._mode == "CLI":
# self.unsupported_os_or_mode()
elif self._os == "Windows" and self._mode == "CLI":
self.windows_cli()
elif self._os == "Windows" and self._mode == "GUI":
self.windows_gui()
else:
Expand Down Expand Up @@ -89,22 +89,20 @@ def linux_cli(self) -> None:

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

bash_part = linux_gui.create_bash_installer(name_pkg, version, icon_name)
bash_part = linux_gui.create_bash_installer(name, 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"
)
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 @@ -121,28 +119,59 @@ def linux_gui(self) -> None:
def unsupported_os_or_mode(self):
"""Print a message for unsupported OS or mode."""
fmt.warning(
f"Creating an installer for a {self._mode} is \
currently not supported on {self._os}."
f"Creating an installer for a {self._mode} is "
f"currently not supported on {self._os}."
)

def windows_cli(self):
"""Create a Windows CLI installer."""
self._check_makensis()

from box.installer_utils.windows_hlp import nsis_cli_script

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

installer_name = f"{name}-v{version}-win.exe"

with ut.set_dir(RELEASE_DIR_NAME):
nsis_script_name = Path("make_installer.nsi")
with open(nsis_script_name, "w") as f:
f.write(
nsis_cli_script(
name,
installer_name,
self._config.author,
self._config.version,
self._release_file,
)
)

# make the installer
subprocess.run(["makensis", nsis_script_name], **self.subp_kwargs)

nsis_script_name.unlink()

self._installer_name = installer_name

def windows_gui(self):
"""Create a Windows GUI installer."""
self._check_makensis()

from box.installer_utils.windows_hlp import nsis_gui_script

name_pkg = self._config.name_pkg
name = self._config.name
version = self._config.version
icon = get_icon("ico")

installer_name = f"{name_pkg}-v{version}-win.exe"
installer_name = f"{name}-v{version}-win.exe"

with ut.set_dir(RELEASE_DIR_NAME):
nsis_script_name = Path("make_installer.nsi")
with open(nsis_script_name, "w") as f:
f.write(
nsis_gui_script(
name_pkg,
name,
installer_name,
self._config.author,
self._config.version,
Expand Down
120 changes: 120 additions & 0 deletions src/box/installer_utils/windows_hlp.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,126 @@
from pathlib import Path


def nsis_cli_script(
project_name: str, installer_name: str, author: str, version: str, binary_path: Path
):
"""Create NSIS script for CLI installer.

:param project_name: Name of the project
:param installer_name: Name of the installer to be produced by NSIS
:param author: Author of the project
:param version: Version of the project
:param binary_path: Path to the binary to be installed
"""
return rf"""; NSIS script to create installer for {project_name}

;--------------------------------
;Include Modern UI

!include "MUI2.nsh"

;--------------------------------
;General

;Name and file
Name "{project_name}"
OutFile "{installer_name}"
Unicode True

;Default installation folder
InstallDir "$LOCALAPPDATA\{project_name}"

;Get installation folder from registry if available
InstallDirRegKey HKCU "Software\{project_name}" ""

;Request application privileges
RequestExecutionLevel user


;--------------------------------
;Interface Settings

!define MUI_ABORTWARNING

;--------------------------------
;Pages

!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_DIRECTORY

!insertmacro MUI_PAGE_INSTFILES

!define MUI_FINISHPAGE_TITLE "Successfully installed {project_name}"
!define MUI_FINISHPAGE_TITLE_3LINES
!define MUI_FINISHPAGE_TEXT "Your command line interface CLI has been successfully installed. However, the installer did not modify your PATH variable.$\r$\nPlease add the installation folder$\r$\n$INSTDIR$\r$\nto your PATH variable manually."
!insertmacro MUI_PAGE_FINISH

!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES

;--------------------------------
;Languages

!insertmacro MUI_LANGUAGE "English"

;--------------------------------
;Installer Sections

Section "{project_name}" SecInst

SetOutPath "$INSTDIR"

; Files for {project_name}
File "{binary_path.name}"

;Store installation folder
WriteRegStr HKCU "Software\{project_name}" "" $INSTDIR

;Create uninstaller
WriteUninstaller "$INSTDIR\Uninstall-{project_name}.exe"

; Add uninstaller key information for add/remove software entry
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\{project_name}" "DisplayName" "{project_name}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\{project_name}" "UninstallString" "$INSTDIR\Uninstall-{project_name}.exe"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\{project_name}" "Publisher" "{author}"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\{project_name}" "DisplayVersion" "{version}"

SectionEnd

;--------------------------------
;Descriptions

;Language strings
LangString DESC_SecInst ${{LANG_ENGLISH}} "Selection."

;Assign language strings to sections
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${{SecInst}} $(DESC_SecInst)
!insertmacro MUI_FUNCTION_DESCRIPTION_END

;--------------------------------
;Uninstaller Section

Section "Uninstall"


; Delete {project_name} folder
Delete "$INSTDIR\{binary_path.name}"
Delete "$INSTDIR\Uninstall-{project_name}.exe"

RMDir "$INSTDIR"

; Delete PyApp virtual environment
RMDir /r "$LOCALAPPDATA\pyapp\data\{project_name}"

; Delete registry key
DeleteRegKey HKCU "Software\{project_name}"
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\{project_name}"

SectionEnd
"""


def nsis_gui_script(
project_name: str,
installer_name: str,
Expand Down
14 changes: 12 additions & 2 deletions src/box/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from contextlib import contextmanager
import os
from pathlib import Path
import subprocess

from rich_click import ClickException

Expand All @@ -11,7 +12,6 @@
# available app entry types that are used in box
PYAPP_APP_ENTRY_TYPES = ["spec", "module", "script", "notebook"]


# supported Python versions. Default will be set to last entry (latest).
PYAPP_PYTHON_VERSIONS = (
"pypy2.7",
Expand Down Expand Up @@ -45,7 +45,17 @@ def cmd_python() -> str:

:return: Command to run Python
"""
return "py" if is_windows() else "python"
if is_windows():
try:
subprocess.run(
["py", "--version"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return "py"
except FileNotFoundError:
pass
return "python"


def is_windows() -> bool:
Expand Down
Loading
Loading