Skip to content

Commit

Permalink
[gui] Add draft installer window
Browse files Browse the repository at this point in the history
  • Loading branch information
kuba2k2 committed Dec 14, 2023
1 parent 7344e22 commit 6f8b5af
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 34 deletions.
10 changes: 8 additions & 2 deletions ltchiptool/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@


def cli():
from .__main__ import cli
import sys

cli()
from .__main__ import cli, install_cli

if len(sys.argv) > 1 and sys.argv[1] == "install":
sys.argv.pop(1)
install_cli()
else:
cli()
41 changes: 34 additions & 7 deletions ltchiptool/gui/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

import sys
from logging import INFO, NOTSET, error, exception
from pathlib import Path

import click

from ltchiptool.util.logging import LoggingHandler
from ltchiptool.util.ltim import LTIM


def gui_entrypoint(*args, **kwargs):
def gui_entrypoint(install: bool, *args, **kwargs):
if sys.version_info < (3, 10, 0):
error("ltchiptool GUI requires Python 3.10 or newer")
exit(1)
Expand All @@ -23,13 +24,20 @@ def gui_entrypoint(*args, **kwargs):

app = wx.App()
try:
from .main import MainFrame

if LoggingHandler.get().level == NOTSET:
LoggingHandler.get().level = INFO
frm = MainFrame(None, title=f"ltchiptool {LTIM.get_version_full()}")
frm.init_params = kwargs
frm.Show()

if not install:
from .main import MainFrame

frm = MainFrame(None, title=f"ltchiptool {LTIM.get_version_full()}")
frm.init_params = kwargs
frm.Show()
else:
from .install import InstallFrame

frm = InstallFrame(install_kwargs=kwargs, parent=None)
frm.Show()
app.MainLoop()
except Exception as e:
LoggingHandler.get().exception_hook = None
Expand All @@ -47,7 +55,26 @@ def gui_entrypoint(*args, **kwargs):
@click.argument("FILE", type=str, required=False)
def cli(*args, **kwargs):
try:
gui_entrypoint(*args, **kwargs)
gui_entrypoint(install=False, *args, **kwargs)
except Exception as e:
exception(None, exc_info=e)
exit(1)


@click.command(help="Start the installer")
@click.argument(
"out_path",
type=click.Path(
file_okay=False,
dir_okay=True,
writable=True,
resolve_path=True,
path_type=Path,
),
)
def install_cli(*args, **kwargs):
try:
gui_entrypoint(install=True, *args, **kwargs)
except Exception as e:
exception(None, exc_info=e)
exit(1)
Expand Down
58 changes: 58 additions & 0 deletions ltchiptool/gui/install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright (c) Kuba Szczodrzyński 2023-12-14.

from logging import INFO

import wx
import wx.xrc

from ltchiptool.util.logging import LoggingHandler
from ltchiptool.util.ltim import LTIM

from .base.window import BaseWindow
from .panels.log import LogPanel
from .utils import load_xrc_file
from .work.install import InstallThread


# noinspection PyPep8Naming
class InstallFrame(wx.Frame, BaseWindow):
def __init__(self, install_kwargs: dict, *args, **kw):
super().__init__(*args, **kw)

xrc = LTIM.get().get_gui_resource("ltchiptool.xrc")
icon = LTIM.get().get_gui_resource("ltchiptool.ico")
self.Xrc = load_xrc_file(xrc)

LoggingHandler.get().level = INFO
LoggingHandler.get().exception_hook = self.ShowExceptionMessage

self.Log = LogPanel(parent=self, frame=self)
# noinspection PyTypeChecker
self.Log.OnDonateClose(None)

self.install_kwargs = install_kwargs

self.Bind(wx.EVT_SHOW, self.OnShow)
self.Bind(wx.EVT_CLOSE, self.OnClose)

self.SetTitle("Installing ltchiptool")
self.SetIcon(wx.Icon(str(icon), wx.BITMAP_TYPE_ICO))
self.SetSize((1000, 400))
self.SetMinSize((600, 200))
self.Center()

@staticmethod
def ShowExceptionMessage(e, msg):
wx.MessageBox(
message="Installation failed!\n\nRefer to the log window for details.",
caption="Error",
style=wx.ICON_ERROR,
)

def OnShow(self, *_):
self.InitWindow(self)
self.StartWork(InstallThread(**self.install_kwargs))

def OnClose(self, *_):
self.StopWork(InstallThread)
self.Destroy()
10 changes: 6 additions & 4 deletions ltchiptool/gui/panels/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,13 @@ def SetSettings(
handler.full_traceback = full_traceback
LoggingStreamHook.set_registered(Serial, registered=dump_serial)

if donate_closed:
# noinspection PyTypeChecker
self.OnDonateClose(None)

menu_bar: wx.MenuBar = self.TopLevelParent.MenuBar
if not menu_bar:
return
menu: wx.Menu = menu_bar.GetMenu(menu_bar.FindMenu("Logging"))
if not menu:
warning(f"Couldn't find Logging menu")
Expand All @@ -200,10 +206,6 @@ def SetSettings(
case _ if item.GetItemLabel() == level_name:
item.Check()

if donate_closed:
# noinspection PyTypeChecker
self.OnDonateClose(None)

@on_event
def OnIdle(self):
while True:
Expand Down
11 changes: 5 additions & 6 deletions ltchiptool/gui/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright (c) Kuba Szczodrzyński 2023-1-3.

from os.path import join
from pathlib import Path
from typing import Callable

import wx
Expand Down Expand Up @@ -40,12 +40,11 @@ def int_or_zero(value: str) -> int:
return 0


def load_xrc_file(*path: str) -> wx.xrc.XmlResource:
xrc = join(*path)
def load_xrc_file(*path: str | Path) -> wx.xrc.XmlResource:
xrc = Path(*path)
try:
with open(xrc, "r") as f:
xrc_str = f.read()
xrc_str = xrc_str.replace("<object>", '<object class="notebookpage">')
xrc_str = xrc.read_text()
xrc_str = xrc_str.replace("<object>", '<object class="notebookpage">')
res = wx.xrc.XmlResource()
res.LoadFromBuffer(xrc_str.encode())
return res
Expand Down
14 changes: 14 additions & 0 deletions ltchiptool/gui/work/install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (c) Kuba Szczodrzyński 2023-12-14.

from ltchiptool.util.ltim import LTIM

from .base import BaseThread


class InstallThread(BaseThread):
def __init__(self, **kwargs):
super().__init__()
self.kwargs = kwargs

def run_impl(self):
LTIM.get().install(**self.kwargs)
47 changes: 32 additions & 15 deletions ltchiptool/util/ltim.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from os.path import expandvars
from pathlib import Path
from subprocess import PIPE, Popen
from typing import Optional, Tuple
from typing import Callable, Optional, Tuple
from zipfile import ZipFile

import requests
Expand All @@ -33,6 +33,7 @@ class LTIM:
"""ltchiptool installation manager"""

INSTANCE: "LTIM" = None
on_message: Callable[[str], None] = None

@staticmethod
def get() -> "LTIM":
Expand All @@ -41,9 +42,19 @@ def get() -> "LTIM":
LTIM.INSTANCE = LTIM()
return LTIM.INSTANCE

@staticmethod
def get_resource(name: str) -> Path:
pass
@property
def is_bundled(self) -> bool:
return getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS")

def get_resource(self, name: str) -> Path:
if self.is_bundled:
return Path(sys._MEIPASS) / name
return Path(__file__).parents[2] / name

def get_gui_resource(self, name: str) -> Path:
if self.is_bundled:
return Path(sys._MEIPASS) / name
return Path(__file__).parents[1] / "gui" / name

@staticmethod
@lru_cache
Expand All @@ -60,20 +71,25 @@ def get_version_full() -> Optional[str]:
tool_version += " (dev)"
return tool_version

def message(self, msg: str) -> None:
info(msg)
if self.on_message:
self.on_message(msg)

def install(self, out_path: Path) -> None:
out_path = out_path.expanduser().resolve()
out_path.mkdir(parents=True, exist_ok=True)

python_path, pythonw_path = self._install_python_windows(out_path)

info("Downloading get-pip.py...")
self.on_message("Downloading get-pip.py...")
get_pip_path = out_path / "get-pip.py"
with requests.get(PYTHON_GET_PIP) as r:
get_pip_path.write_bytes(r.content)

opts = ["--prefer-binary", "--no-warn-script-location"]

info("Installing pip...")
self.on_message("Installing pip...")
return_code = run_subprocess(
python_path,
get_pip_path,
Expand All @@ -83,7 +99,7 @@ def install(self, out_path: Path) -> None:
if return_code != 0:
raise RuntimeError(f"{get_pip_path.name} returned {return_code}")

info("Checking pip installation...")
self.on_message("Checking pip installation...")
return_code = run_subprocess(
python_path,
"-m",
Expand All @@ -94,7 +110,7 @@ def install(self, out_path: Path) -> None:
if return_code != 0:
raise RuntimeError(f"pip --version returned {return_code}")

info("Installing ltchiptool with GUI extras...")
self.on_message("Installing ltchiptool with GUI extras...")
return_code = run_subprocess(
python_path,
"-m",
Expand All @@ -107,11 +123,10 @@ def install(self, out_path: Path) -> None:
if return_code != 0:
raise RuntimeError(f"pip install returned {return_code}")

@staticmethod
def _install_python_windows(out_path: Path) -> Tuple[Path, Path]:
def _install_python_windows(self, out_path: Path) -> Tuple[Path, Path]:
version_spec = SimpleSpec("~3.11")

info("Checking the latest Python version...")
self.on_message("Checking the latest Python version...")
with requests.get(PYTHON_RELEASES) as r:
releases = r.json()
releases_map = [
Expand All @@ -132,7 +147,7 @@ def _install_python_windows(out_path: Path) -> Tuple[Path, Path]:
if part.isnumeric()
)

info(f"Will install Python {latest_version}")
self.on_message(f"Will install Python {latest_version}")
with requests.get(PYTHON_RELEASE_FILE_FMT % latest_release_id) as r:
release_files = r.json()
for release_file in release_files:
Expand All @@ -145,12 +160,14 @@ def _install_python_windows(out_path: Path) -> Tuple[Path, Path]:
else:
raise RuntimeError("Couldn't find embeddable package URL")

info(f"Downloading and extracting '{release_url}' to '{out_path}'...")
self.on_message(
f"Downloading and extracting '{release_url}' to '{out_path}'..."
)
with requests.get(release_url) as r:
with ZipFile(BytesIO(r.content)) as z:
z.extractall(out_path)

info("Checking installed executable...")
self.on_message("Checking installed executable...")
python_path = out_path / "python.exe"
pythonw_path = out_path / "pythonw.exe"
p = Popen(
Expand All @@ -162,7 +179,7 @@ def _install_python_windows(out_path: Path) -> Tuple[Path, Path]:
raise RuntimeError(f"{python_path.name} returned {p.returncode}")
version_tuple = version_name.decode().partition(" ")[2].split(".")

info("Enabling site-packages...")
self.on_message("Enabling site-packages...")
pth_path = out_path / ("python%s%s._pth" % tuple(version_tuple[:2]))
if not pth_path.is_file():
raise RuntimeError(f"Extraction failed, {pth_path.name} is not a file")
Expand Down

0 comments on commit 6f8b5af

Please sign in to comment.