From 2296f5449e24640e45016152b1024d786071715f Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Sun, 8 Jan 2023 03:09:04 -0500 Subject: [PATCH] (installer) initial work on the installer --- installer/__init__.py | 0 installer/installer.py | 116 +++++++++++++++++++++++++++++++++++++++++ installer/main.py | 14 +++++ installer/messages.py | 88 +++++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+) create mode 100644 installer/__init__.py create mode 100644 installer/installer.py create mode 100644 installer/main.py create mode 100644 installer/messages.py diff --git a/installer/__init__.py b/installer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/installer/installer.py b/installer/installer.py new file mode 100644 index 0000000000..75497b7f38 --- /dev/null +++ b/installer/installer.py @@ -0,0 +1,116 @@ +""" +InvokeAI installer script +""" + +import subprocess +import sys +import venv +from pathlib import Path +from tempfile import TemporaryDirectory + +SUPPORTED_PYTHON = ">=3.9.0,<3.11" +INSTALLER_REQS = ["rich", "semver"] +PLATFORM = sys.platform + +### Feature flags +# Place the virtualenv inside the runtime dir +# (default for 2.2.5) == True +VENV_IN_RUNTIME = True + +# Install the wheel from pypi +# (default for 2.2.5) == False +USE_WHEEL = False + + +class Installer: + """ + Deploys an InvokeAI installation into a given path + """ + + def __init__(self) -> None: + self.reqs = INSTALLER_REQS + self.preflight() + self.bootstrap() + + def preflight(self) -> None: + """ + Preflight checks + """ + + # TODO + # verify python version + # on macOS verify XCode tools are present + # verify libmesa, libglx on linux + # check that the system arch is not i386 (?) + # check that the system has a GPU, and the type of GPU + + pass + + def inst_venv(self) -> TemporaryDirectory: + """ + Creates a temporary virtual environment for the installer itself + + :return: path to the created virtual environment directory + :rtype: TemporaryDirectory + """ + + venv_dir = TemporaryDirectory(prefix="invokeai-installer-") + venv.create(venv_dir.name, with_pip=True) + sys.path.append(f"{venv_dir.name}/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages") + + self.venv_dir = venv_dir + return venv_dir + + def bootstrap(self, verbose: bool = False) -> TemporaryDirectory: + """ + Bootstrap the installer venv with packages required at install time + + :return: path to the virtual environment directory that was bootstrapped + :rtype: TemporaryDirectory + """ + + print("Initializing the installer, please wait...") + + venv_dir = self.inst_venv() + pip = str(Path(venv_dir.name).absolute() / "bin/pip") + + cmd = [pip, "install", "--require-virtualenv"] + cmd.extend(self.reqs) + + try: + res = subprocess.check_output(cmd).decode() + if verbose: + print(res) + return venv_dir + except subprocess.CalledProcessError as e: + print(e) + + def install(self, path: str = "~/invokeai", version: str = "latest") -> None: + """ + Install the InvokeAI application into the given runtime path + + :param path: Destination path for the installation + :type path: str + :param version: InvokeAI version to install + :type version: str + """ + + from messages import dest_path, welcome + + welcome() + self.dest = dest_path(path) + + def application_venv(): + """ + Create a virtualenv for the InvokeAI installation + """ + pass + + +class InvokeAiDeployment: + """ + Manages an installed instance of InvokeAI + """ + + def __init__(self, path) -> None: + pass diff --git a/installer/main.py b/installer/main.py new file mode 100644 index 0000000000..53b41fd28d --- /dev/null +++ b/installer/main.py @@ -0,0 +1,14 @@ +""" +InvokeAI Installer +""" + +from installer import Installer + +if __name__ == "__main__": + inst = Installer() + try: + inst.install() + except KeyboardInterrupt as exc: + print("\n") + print("Ctrl-C pressed. Aborting.") + print("See you again soon!") diff --git a/installer/messages.py b/installer/messages.py new file mode 100644 index 0000000000..74f4b4afbe --- /dev/null +++ b/installer/messages.py @@ -0,0 +1,88 @@ +""" +Installer user interaction +""" + +import platform +from pathlib import Path +from tkinter.filedialog import askdirectory + +from rich import box, print +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Confirm +from rich.style import Style +from rich.text import Text + +console = Console(width=80) + +OS = platform.uname().system +ARCH = platform.uname().machine + + +def welcome(): + console.rule() + print( + Panel( + title="[bold wheat1]Welcome to the InvokeAI Installer!", + renderable=Text( + "Some of the installation steps take a long time to run. Please be patient. If the script appears to hang for more than 10 minutes, please interrupt with control-C and retry.", + justify="center", + ), + box=box.DOUBLE, + width=80, + expand=False, + padding=(1, 2), + style=Style(bgcolor="grey23", color="orange1"), + subtitle=f"[wheat1] Installing for [bold]{OS}-{ARCH}", + ) + ) + console.line() + + +def dest_path(init_path=None) -> Path: + """ + Prompt the user for the destination path and create the path + + :param init_path: a filesystem path, defaults to None + :type init_path: str, optional + :return: absolute path to the created installation directory + :rtype: Path + """ + + dest = init_path + dest_confirmed = False + + while not dest_confirmed: + console.line() + if dest is not None: + dest = Path(dest).expanduser().resolve() + print(f"InvokeAI will be installed at {dest}") + dest_confirmed = Confirm.ask(f"Continue?") + if not dest_confirmed: + print(f"Please select the destination directory for the installation") + resp = askdirectory(initialdir=dest) + if resp == (): + continue + dest = Path(resp).expanduser().resolve() + if dest.exists(): + print(f":exclamation: Directory {dest} already exists.") + dest_confirmed = Confirm.ask( + ":question: Are you sure you want to (re)install in this location?", default="y" + ) + + try: + dest.mkdir(exist_ok=True, parents=True) + return dest + except PermissionError as exc: + print( + f"Failed to create directory {dest} due to insufficient permissions", + style=Style(color="red"), + highlight=True, + ) + except OSError as exc: + console.print_exception(exc) + + if Confirm.ask("Would you like to try again?"): + dest_path(init_path) + else: + console.rule("Goodbye!")