From 7a331619e01d8c54fc49d7d30966d752a7ef1789 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Mon, 12 Apr 2021 18:06:50 +0200 Subject: [PATCH] Compltete source-code rewrite --- arch-install-checklist.md | 8 ++ requirements.txt | 5 +- src/__main__.py | 38 +++---- src/arch_install.py | 130 ++++++++++++++++++++++++ src/chroot_install.py | 12 +++ src/dotfiles.py | 95 ----------------- src/dotfiles_install.py | 111 ++++++++++++++++++++ src/{packages.py => package_install.py} | 23 +++-- src/util/color.py | 22 ---- src/util/command.py | 60 +++++++---- src/util/install.py | 53 ---------- src/util/internet.py | 95 +++++++++++++++++ src/util/package.py | 64 ++++++++++-- src/util/user.py | 83 --------------- tox.ini | 19 ++++ 15 files changed, 510 insertions(+), 308 deletions(-) create mode 100644 src/arch_install.py create mode 100644 src/chroot_install.py delete mode 100644 src/dotfiles.py create mode 100644 src/dotfiles_install.py rename src/{packages.py => package_install.py} (57%) delete mode 100644 src/util/color.py delete mode 100644 src/util/install.py create mode 100644 src/util/internet.py delete mode 100644 src/util/user.py create mode 100644 tox.ini diff --git a/arch-install-checklist.md b/arch-install-checklist.md index 9aa98e1..df45aa3 100644 --- a/arch-install-checklist.md +++ b/arch-install-checklist.md @@ -1,3 +1,10 @@ +# Arch installation checklist +This file contains simplified instructions for Arch Linux installation +Following these should lead to a full installation of barebone Arch system, +with grub bootloader and a priviledged sudoer user. + +Note: Running the script can automated many of these points, if you are comming +here after running the script, look for a line saying: **Proceed from this line, if you're reading after chrooting** ## Set keyboard layout Default layout will be US `ls /usr/share/kbd/keymaps/**/*.map.gz` <- list keymaps @@ -43,6 +50,7 @@ First select mirrors (`/etc/pacman.d/mirrorlist`) ## Chroot `arch-chroot /mnt` +# Proceed from this line, if you're reading after chrooting ## Set time zone `ln -sf /usr/share/zoneinfo/Region/Ciry /etc/localtime` <- specify timezone diff --git a/requirements.txt b/requirements.txt index b68ec5d..b53f699 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,4 @@ -pyyaml==5.4.1 +pyyaml +psutil +colorama +pyinquirer diff --git a/src/__main__.py b/src/__main__.py index 163df83..be10304 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,22 +1,26 @@ -import os +import sys -from src.packages import install_packages -from src.dotfiles import install_dotfiles -from src.util.user import Input, Print +import colorama +import inquirer.shortcuts + +from src.arch_install import install_arch +from src.dotfiles_install import install_dotfiles +from src.package_install import install_packages +from src.util.command import run_cmd + +colorama.init(autoreset=True) -def main(): - if os.geteuid() == 0: - Print.err("You can't to run this program as root user") - return +if inquirer.shortcuts.confirm("Do you wish to perform Arch install? (Directly from live ISO)"): + run_cmd("clear") + print(f"{colorama.Fore.BLUE}Running Arch Installation") + root_mountpoint = install_arch() + print(f"{colorama.Fore.GREEN}Arch installation complete") + print(f"{colorama.Fore.CYAN}To install packages and dotfiles, move this whole directory to the new installation and run it from there.") + sys.exit() - if Input.yes_no("Do you wish to perform package install (from `packages.yaml`)?"): - install_packages() - if Input.yes_no("Do you wish to install dotfiles (sync `home/` and `root/` directories accordingly)? You can make a backup."): - install_dotfiles() +if inquirer.shortcuts.confirm("Do you wish to perform package installation (from packages.yaml)"): + install_packages() - -try: - main() -except KeyboardInterrupt: - Print.err("User cancelled (KeyboardInterrupt)") +if inquirer.shortcuts.confirm("Do you wish to install dotfiles? (from home and root folders)"): + install_dotfiles() diff --git a/src/arch_install.py b/src/arch_install.py new file mode 100644 index 0000000..ba0a808 --- /dev/null +++ b/src/arch_install.py @@ -0,0 +1,130 @@ +from pathlib import Path +from typing import Optional + +import colorama +import inquirer.shortcuts + +from src.util.command import run_cmd, run_root_cmd +from src.util.internet import connect_internet + +colorama.init(autoreset=True) + + +def mount_partition(partition_path: Path, mount_path: Optional[Path] = None) -> Path: + """ + Mount given `partition_path` to `mount_path`. + If `mount_path` wasn't provided, ask user for it. + + After mounting, mount_path will be returned + """ + if not mount_path: + mount_path = Path(inquirer.shortcuts.path(f"Specify mountpoint for {partition_path}", default="/mnt")) + + if not mount_path.exists(): + run_root_cmd(f"mkdir -p {mount_path}") + + run_root_cmd(f"mount {partition_path} {mount_path}") + return mount_path + + +def partition_disk() -> Path: + """Create all necessary partitions and return root mountpoint""" + uefi = Path("/sys/firmware/efi/efivars").is_dir() + + # Let user make partitions in shell environment + partitions_made = inquirer.shortcuts.confirm("Do you already have partitions pre-made?") + if not partitions_made: + print( + f"{colorama.Fore.CYAN}Dropping to shell environment, create your partitions here." + " When you are done, use `exit` to return\n" + f"{colorama.Style.DIM}This is {'UEFI' if uefi else 'LEGACY (BIOS)'} system\n" + ) + run_cmd("exec $SHELL") + + # Obtain partitions from user and mount them + root_part = Path(inquirer.shortcuts.path("Specify the root partition (/dev/sdXY)", exists=True)) + if inquirer.shortcuts.confirm(f"Do you wish to make EXT4 filesystem on {root_part}?", default=True): + run_root_cmd(f"mkfs.ext4 {root_part}") + root_mountpoint = mount_partition(root_part) + + if inquirer.shortcuts.confirm("Do you have an EFI partition?", default=uefi): + if not uefi: + print( + f"{colorama.Fore.RED}Warning: Adding EFI partition from non-uefi system isn't adviced.\n" + "While this process won't directly fail, you won't be able to install a bootloader from " + "this computer. You can proceed, but you will have to use another computer to install the bootloader." + ) + efi_part = Path(inquirer.shortcuts.path("Specify EFI partition (/dev/sdXY)", exists=True)) + if inquirer.shortcuts.confirm(f"Do you wish to make FAT32 filesystem on {efi_part}?", default=True): + run_root_cmd(f"mkfs.fat -F32 {efi_part}") + mount_partition(efi_part, Path(root_mountpoint, "/boot")) + elif uefi: + print( + f"{colorama.Fore.RED}Proceeding without EFI partition on UEFI system is not adviced, " + "unless you want to run this OS with other UEFI capable system" + ) + + if inquirer.shortcuts.confirm("Do you have a swap partition?"): + swap_part = Path(inquirer.shortcuts.path("Specify the swap partition (/dev/sdXY)", exists=True)) + if inquirer.shortcuts.confirm(f"Do you wish to make swap system on {root_part}?", default=True): + run_root_cmd(f"mkswap {swap_part}") + if inquirer.shortcuts.confirm("Do you wish to turn on swap?", default=True): + run_root_cmd(f"swapon {swap_part}") + + while inquirer.shortcuts.confirm("Do you have any other partition?"): + part_path = Path(inquirer.shortcuts.path("Specify partition path (/dev/sdXY)", exists=True)) + if inquirer.shortcuts.confirm("Do you wish to format this partition?"): + print(f"{colorama.Fore.CYAN}Dropping to shell, format the partition here and type `exit` to return") + run_cmd("exec $SHELL") + mount_partition(part_path) + + print(f"{colorama.Fore.LIGHTCYAN_EX}Printing disk report (with lsblk)") + run_root_cmd("lsblk") + if inquirer.shortcuts.confirm("Do you want to drop to shell and make some further adjustments?"): + print(f"{colorama.Fore.CYAN}After you are done, return by typing `exit`") + run_cmd("exec $SHELL") + + print(f"{colorama.Fore.GREEN}Partitioning complete") + return root_mountpoint + + +def run_pacstrap(root_mountpoint: Path): + mirror_setup = inquirer.shortcuts.confirm("Do you wish to setup your mirrors (This is necessary for fast downloads)?", default=True) + if mirror_setup: + print( + f"{colorama.Fore.CYAN}Dropping to shell environment, setup your mirrors from here." + " When you are done, use `exit` to return\n" + f"{colorama.Style.DIM}Mirrors are located in `/etc/pacman.d/mirrorlist`\n" + ) + run_cmd("exec $SHELL") + + extra_pkgs = inquirer.shortcuts.checkbox( + "You can choose to install additional packages with pacstrap here (select with space)", + choices=["NetworkManager", "base-devel", "vim", "nano"] + ) + run_root_cmd(f"pacstrap {root_mountpoint} base linux linux-firmware {' '.join(extra_pkgs)}") + + if inquirer.shortcuts.confirm("Do you wish to make some further adjustments and drop to shell?"): + print(f"{colorama.Fore.CYAN}When you are done, use `exit` to return") + run_cmd("exec $SHELL") + + +def install_arch(): + """Perform full Arch installation and return mountpoint and default user""" + connect_internet() + + run_root_cmd("timedatectl set-ntp true") + root_mountpoint = partition_disk() + run_pacstrap(root_mountpoint) + print(f"{colorama.Fore.CYAN}Generating fstab") + run_root_cmd(f"genfstab -U {root_mountpoint} >> {root_mountpoint}/etc/fstab") + print( + f"\n{colorama.Fore.GREEN}Core installation complete.\n" + f"{colorama.Fore.YELLOW}Instalation within chroot environment is not possible from this script, " + "run chroot_install.py within chroot environment.\n" + f"{colorama.Fore.LIGHTBLUE_EX}Execute: {colorama.Style.BRIGHT}`arch-chroot /mnt`" + ) + + +if __name__ == "__main__": + install_arch() diff --git a/src/chroot_install.py b/src/chroot_install.py new file mode 100644 index 0000000..8785ad8 --- /dev/null +++ b/src/chroot_install.py @@ -0,0 +1,12 @@ +import colorama + +colorama.init(autoreset=True) + + +def install_chroot(): + print(f"{colorama.Fore.RED}Sorry, chroot installation file is still WIP, for now, proceed manually.") + print(f"{colorama.Fore.LIGHTCYAN_EX}You can use `arch-install-checklist.md` file which containes detailed installation steps.") + + +if __name__ == "__main__": + install_chroot() diff --git a/src/dotfiles.py b/src/dotfiles.py deleted file mode 100644 index c486c1a..0000000 --- a/src/dotfiles.py +++ /dev/null @@ -1,95 +0,0 @@ -import os -import shutil -import time -from datetime import datetime -from contextlib import suppress - -from src.util import command -from src.util.user import Print, Input - - -def _backup_file(backup_dir: str, subdir: str, file: str) -> None: - """ - Backup given `file` in given `subdir` from system - """ - system_subdir = subdir.replace("home", f"{os.environ['HOME']}/").replace("root", "/") - system_path = os.path.join(system_subdir, file) - - # Only backup files which exists in origin destination (real system destination) - if os.path.exists(system_path): - backup_subdir = os.path.join(backup_dir, subdir) - backup_path = os.path.join(backup_subdir, file) - # Ensure directory existence in the new backup directory - with suppress(FileExistsError): - os.makedirs(backup_subdir) - - if file != "placeholder": - Print.comment(f"Backing up {system_path}") - shutil.copyfile(system_path, backup_path) - - -def make_backup() -> None: - """ - Find all files which will be replaced and back them up. - Files which doesn't exist in the real system destination - will be ignored. - """ - Print.action("Creating current dotfiles backup") - time = str(datetime.now()).replace(" ", "--") - backup_dir = os.path.join(os.getcwd(), "backup", time) - os.makedirs(backup_dir) - - # backup files in `home` directory - for subdir, _, files in os.walk("home"): - for file in files: - _backup_file(backup_dir, subdir, file) - - # backup files in `root` directory - for subdir, _, files in os.walk("root"): - for file in files: - _backup_file(backup_dir, subdir, file) - - Print.action("Backup complete") - - -def _overwrite_dotfile(subdir: str, dotfile: str) -> None: - """Overwrite given `dotfile` in `subdir` from system with the local one""" - local_path = os.path.join(subdir, dotfile) - system_subdir = subdir.replace("home", f"{os.environ['HOME']}").replace("root", "/") - system_path = os.path.join(system_subdir, dotfile) - - # Ensure existence of system directory - with suppress(FileExistsError): - os.makedirs(system_subdir) - - if dotfile != "placeholder": - Print.comment(f"Overwriting {system_path}") - # Use sudo to avoid PermissionError - command.execute(f"sudo cp {local_path} {system_path}") - - -def overwrite_dotfiles() -> None: - # overwrite system dotfiles with local in `home` - for subdir, _, files in os.walk("home"): - for file in files: - _overwrite_dotfile(subdir, file) - # overwrite system dotfiles with local in `root` - for subdir, _, files in os.walk("root"): - for file in files: - _overwrite_dotfile(subdir, file) - - -def install_dotfiles() -> None: - if Input.yes_no("Do you want to backup current dotfiles? (Recommended)"): - make_backup() - - Print.action("Installing dotfiles (this will overwrite your original files)") - time.sleep(2) - - overwrite_dotfiles() - - Print.action("Dotfile installation complete, make sure to adjust the dotfiles to your liking.") - - -if __name__ == "__main__": - install_dotfiles() diff --git a/src/dotfiles_install.py b/src/dotfiles_install.py new file mode 100644 index 0000000..4c25825 --- /dev/null +++ b/src/dotfiles_install.py @@ -0,0 +1,111 @@ +from datetime import datetime +from pathlib import Path + +import colorama +import inquirer.shortcuts + +from src.util.command import run_root_cmd + +colorama.init(autoreset=True) + + +def _find_all_files(path: Path): + for subpath in path.iterdir(): + if subpath.is_dir(): + yield from _find_all_files(subpath) + else: + yield subpath + + +def _walk_dotfiles(): + """ + Walk through every stored file in repositorie's dotfiles, + start by going through `home` specific files, and continue + with `root` dotfiles. + """ + yield from _find_all_files(Path.cwd().joinpath("home")) + yield from _find_all_files(Path.cwd().joinpath("root")) + + +def _dotfile_to_system(dotfile_path: Path) -> Path: + """Convert dotfile path to corresponding path on real system""" + base_dir = str(Path.cwd()) + + if base_dir + "/home/" in str(dotfile_path): + rel_path = str(dotfile_path).replace(base_dir + "/home/", "") + return Path.home().joinpath(rel_path) + elif base_dir + "/root/" in str(dotfile_path): + rel_path = str(dotfile_path).replace(base_dir + "/root/", "") + return Path("/", rel_path) + else: + raise ValueError(f"Given path is not a valid dotfile path ({dotfile_path})") + + +def make_backup() -> None: + """ + Find all files which will be replaced and back them up. + Files which doesn't exist in the real system destination + will be ignored. + """ + print(f"{colorama.Fore.LIGHTYELLOW_EX}Creating current dotfiles backup") + time = str(datetime.now()).replace(" ", "--") + backup_dir = Path.joinpath(Path.cwd(), "backup", time) + backup_dir.mkdir(parents=True, exist_ok=True) + + for dotfile_path in _walk_dotfiles(): + real_path = _dotfile_to_system(dotfile_path) + if not real_path.exists(): + continue + + rel_path = str(dotfile_path).replace(str(Path.cwd()) + "/", "") + backup_path = backup_dir.joinpath(rel_path) + + # Ensure backup directory existence + if real_path.is_dir(): + backup_path.mkdir(parents=True, exist_ok=True) + else: + backup_path.parent.mkdir(parents=True, exist_ok=True) + + print(f"{colorama.Style.DIM}Backing up{real_path}") + run_root_cmd(f"cp '{real_path}' '{backup_path}'", enable_debug=False) + + print(f"{colorama.Fore.LIGHTYELLOW_EX}Backup complete") + + +def overwrite_dotfiles() -> None: + for dotfile_path in _walk_dotfiles(): + real_path = _dotfile_to_system(dotfile_path) + # Ensure existence of system directory + if dotfile_path.is_dir(): + real_path.mkdir(parents=True, exist_ok=True) + else: + dotfile_path.parent.mkdir(parents=True, exist_ok=True) + + # If we encounter placeholder file, making folder is suffictient + # don't proceed with copying it to avoid clutterring the original system + # with empty placeholder files, these files are here only for git to + # recognize that directory + if str(dotfile_path).endswith("placeholder"): + continue + + print(f"{colorama.Style.DIM}Overwriting {real_path}") + run_root_cmd(f"cp '{dotfile_path}' '{real_path}'") + + +def install_dotfiles() -> None: + if inquirer.shortcuts.confirm("Do you want to backup current dotfiles? (Recommended)", default=True): + make_backup() + + print(f"{colorama.Fore.CYAN}Proceeding with dotfiles installation (this will overwrite your original files)") + if inquirer.shortcuts.confirm( + "Have you adjusted all dotfiles to your liking? " + f"{colorama.Fore.RED}(proceeding without checking the dotfiles first isn't adviced){colorama.Fore.RESET}" + ): + overwrite_dotfiles() + print(f"{colorama.Fore.LIGHTYELLOW_EX}Dotfile installation complete, make sure to adjust the dotfiles to your liking.") + else: + print(f"{colorama.Fore.RED}Aborted...") + + +if __name__ == "__main__": + install_dotfiles() diff --git a/src/packages.py b/src/package_install.py similarity index 57% rename from src/packages.py rename to src/package_install.py index 15af69d..6c7b74d 100644 --- a/src/packages.py +++ b/src/package_install.py @@ -1,9 +1,13 @@ import typing as t + +import colorama +import inquirer.shortcuts import yaml -from src.util.package import Package, PackageAlreadyInstalled, InvalidPackage -from src.util import install -from src.util.user import Print, Input +from src.util.command import run_root_cmd +from src.util.package import InvalidPackage, Package, PackageAlreadyInstalled + +colorama.init(autoreset=True) def obtain_packages() -> t.List[Package]: @@ -16,24 +20,25 @@ def obtain_packages() -> t.List[Package]: packages = [] packages += Package.safe_load(pacman_packages) - packages += Package.safe_load(aur_packages, aur=True) packages += Package.safe_load(git_packages, git=True) + packages += Package.safe_load(aur_packages, aur=True) return packages def install_packages() -> None: packages = obtain_packages() - if Input.yes_no("Do you wish to perform system upgrade first? (Recommended)"): - install.upgrade_pacman() + if inquirer.shortcuts.confirm("Do you wish to perform system upgrade first? (Recommended)", default=True): + run_root_cmd("pacman -Syu") + for package in packages: try: - Print.action(f"Installing {package}") + print(f"{colorama.Fore.CYAN}Installing {package}") package.install() except PackageAlreadyInstalled: - Print.cancel(f"Package {package} is already installed.") + print(f"{colorama.Style.DIM}Package {package} is already installed, skipping") except InvalidPackage as e: - Print.warning(str(e)) + print(f"{colorama.Fore.RED}{str(e)}") if __name__ == "__main__": diff --git a/src/util/color.py b/src/util/color.py deleted file mode 100644 index 3eaad11..0000000 --- a/src/util/color.py +++ /dev/null @@ -1,22 +0,0 @@ -from src.util import command - - -def get_256_color(color): - """Get color using tput (256-colors base)""" - return command.get_output(f"tput setaf {color}") - - -def get_special_color(index): - """Get special colors using tput (like bold, etc..)""" - return command.get_output(f"tput {index}") - - -RED = get_256_color(196) -BLUE = get_256_color(51) -GREEN = get_256_color(46) -YELLOW = get_256_color(226) -GOLD = get_256_color(214) -GREY = get_256_color(238) - -RESET = get_special_color("sgr0") -BOLD = get_special_color("bold") diff --git a/src/util/command.py b/src/util/command.py index 550414a..55c84bc 100644 --- a/src/util/command.py +++ b/src/util/command.py @@ -1,27 +1,47 @@ +import os import subprocess +import colorama +import inquirer.shortcuts -def execute(command) -> None: - """Execute bash `command`, return the returncode (int)""" - command = command.split(" ") - subprocess.call(command) +DEBUG = True -def get_output(command) -> str: - """Get standard output of `command`""" - command = command.split(" ") - return subprocess.run( - command, - stderr=subprocess.STDOUT, - stdout=subprocess.PIPE - ).stdout.decode("utf-8") +def debug_confirm_run(cmd): + if DEBUG: + cnfrm = inquirer.shortcuts.confirm( + f"{colorama.Fore.BLUE}[DEBUG] Running command: " + f"{colorama.Fore.YELLOW}{cmd}{colorama.Fore.RESET}" + ) + return cnfrm + else: + return True -def get_return_code(command) -> int: - """get return code of `command` (int)""" - command = command.split(" ") - return subprocess.run( - command, - stderr=subprocess.STDOUT, - stdout=subprocess.PIPE - ).returncode +def run_root_cmd(cmd: str, enable_debug: bool = True) -> subprocess.CompletedProcess: + """Run command as root""" + if os.geteuid() != 0: + return run_cmd(f"sudo {cmd}", enable_debug=enable_debug) + else: + return run_cmd(cmd, enable_debug=enable_debug) + + +def run_cmd(cmd: str, capture_out: bool = False, enable_debug: bool = True) -> subprocess.CompletedProcess: + """Run given command""" + args = {} + if capture_out: + args.update({"stdout": subprocess.PIPE, "stderr": subprocess.STDOUT}) + + if not enable_debug or debug_confirm_run(cmd): + return subprocess.run(cmd, shell=True, **args) + else: + # If debug confirm wasn't confirmed, return code 1 (error) + return subprocess.CompletedProcess(cmd, returncode=1) + + +def command_exists(cmd) -> bool: + """Check if given command can be executed""" + parts = cmd.split() + executable = parts[0] if parts[0] != "sudo" else parts[1] + proc = run_cmd(f"which {executable}", capture_out=True) + return proc.returncode != 1 diff --git a/src/util/install.py b/src/util/install.py deleted file mode 100644 index 1975010..0000000 --- a/src/util/install.py +++ /dev/null @@ -1,53 +0,0 @@ -import shutil -import os - -from src.util import command -from src.util.user import Print, Input - - -def is_installed(package: str) -> bool: - """Check if the package is already installed in the system""" - return_code = command.get_return_code(f"pacman -Qi {package}") - return return_code != 1 - - -def upgrade_pacman() -> None: - """Run full sync, refresh the package database and upgrade.""" - command.execute("sudo pacman -Syu") - - -def pacman_install(package: str) -> None: - """Install given `package`""" - command.execute(f"sudo pacman -S {package}") - - -def yay_install(package: str) -> None: - """Install give package via `yay` (from AUR)""" - command.execute(f"yay -S {package}") - - -def git_install(url: str) -> None: - """Clone a git repository with given `url`""" - dir_name = url.split("/")[-1].replace(".git", "") - if os.path.exists(dir_name): - Print.cancel(f"Git repository {dir_name} already exists") - - ret_code = command.get_return_code(f"git clone {url}") - if ret_code == 128: - Print.warning(f"Unable to install git repository {url}") - return - - if not os.path.exists(f"{dir_name}/PKGBUILD"): - Print.comment(f"Git repository {dir_name} doesn't contain PKGBUILD, only downloaded.") - return - - if Input.yes_no("Do you wish to run makepkg on the downloaded git repository?"): - cwd = os.getcwd() - os.chdir(dir_name) - command.execute("makepkg -si") - os.chdir(cwd) - shutil.rmtree(dir_name) - else: - os.makedirs("download") - command.execute(f"mv {dir_name} download/") - Print.action(f"Your git repository was cloned into `download/{dir_name}`") diff --git a/src/util/internet.py b/src/util/internet.py new file mode 100644 index 0000000..9699f53 --- /dev/null +++ b/src/util/internet.py @@ -0,0 +1,95 @@ +import sys +import time +import urllib.request +from urllib.error import URLError + +import colorama +import inquirer.shortcuts + +from src.util.command import command_exists, run_cmd, run_root_cmd + + +def _connect_wifi() -> bool: + """ + Attempt to connect to internet using WiFI. + + This uses `nmtui` with fallback to `iwctl`, if none + of these tools are aviable, either quit or return False, + if the tool was executed properly, return True + + Note: True doesn't mean we connected, just that the tool was ran, + it is up to user to use that tool properly and make the connection. + """ + if command_exists("nmtui"): + run_root_cmd("nmtui") + elif command_exists("iwctl"): + run_root_cmd("iwctl") + else: + print( + f"{colorama.Fore.RED}{colorama.Style.BRIGHT}ERROR: " + "WiFi connection tool not found: `nmtui`/`iwctl`, please use Ethernet instead.\n" + "Alternatively, connect manually outside of this script and re-run it." + ) + opt = inquirer.shortcuts.list_input( + "How do you wish to proceed?", + choices=["Quit and connect manually", "Proceed with Ethernet"] + ) + if opt == "Quit and connect manually": + sys.exit() + else: + return False + return True + + +def _connect_ethernet(max_wait_time: int = 20, iteration_time: int = 1) -> bool: + """ + Attempt to connect to internet using Ethernet. + + This will simply wait for the user to plug in the ethernet cable, + once that happens loop is interrupted and True is returned. In case + it takes over the `max_wait_time`, loop ends and False is returned. + + `iteration_time` is the time of each loop iteration, after whcih we + check if connection is valid, if not, we continue iterating. + """ + print(f"{colorama.Style.DIM}Please plug in the Ethernet cable, waiting 20s") + time_elapsed = 0 + while not check_connection() and time_elapsed < max_wait_time: + time.sleep(iteration_time) + time_elapsed += iteration_time + + if time_elapsed >= max_wait_time: + # We stopped because max wait time was crossed + return False + else: + # We stopped because connection to internet was successful + return True + + +def connect_internet(): + wifi_possible = True + + while not check_connection(): + run_cmd("clear") + print(f"{colorama.Fore.RED}Internet connection unaviable") + if wifi_possible: + connect_opt = inquirer.shortcuts.list_input("How do you wish to connect to internet?", choices=["Wi-Fi", "Ethernet"]) + else: + connect_opt = "Ethernet" + + if connect_opt == "Wi-Fi": + wifi_possible = _connect_wifi() + else: + if _connect_ethernet(): + break + + print(f"{colorama.Fore.GREEN}Internet connection successful") + + +def check_connection(host="https://google.com") -> bool: + """Check if system is connected to the internet""" + try: + urllib.request.urlopen(host) + return True + except URLError: + return False diff --git a/src/util/package.py b/src/util/package.py index ab76c12..4472b3d 100644 --- a/src/util/package.py +++ b/src/util/package.py @@ -1,7 +1,13 @@ +import os import typing as t +from pathlib import Path -from src.util import install -from src.util.user import Print +import colorama +import inquirer.shortcuts + +from src.util.command import run_cmd, run_root_cmd + +colorama.init(autoreset=True) class InvalidPackage(Exception): @@ -12,6 +18,48 @@ class PackageAlreadyInstalled(Exception): pass +def is_installed(pkg: str) -> bool: + """Check if the package is already installed in the system""" + return run_cmd(f"pacman -Qi {pkg}", capture_out=True).returncode != 1 + + +def pacman_install(package: str) -> None: + """Install given `package`""" + run_root_cmd(f"pacman -S {package}") + + +def yay_install(package: str) -> None: + """Install give package via `yay` (from AUR)""" + run_cmd(f"yay -S {package}") + + +def git_install(url: str) -> None: + """Clone a git repository with given `url`""" + dir_name = Path(url.split("/")[-1].replace(".git", "")) + if dir_name.exists(): + print(f"{colorama.Style.DIM}Git repository {dir_name} already exists") + + ret_code = run_cmd(f"git clone {url}").returncode + if ret_code == 128: + print(f"{colorama.Fore.RED}Unable to install git repository {url}") + return + + if not Path(dir_name, "PKGBUILD").exists: + print(f"{colorama.Fore.YELLOW}Git repository {dir_name} doesn't contain PKGBUILD, only downloaded.") + return + + if inquirer.shortcuts.confirm("Do you wish to run makepkg on the downloaded git repository?"): + cwd = os.getcwd() + os.chdir(dir_name) + run_cmd("makepkg -si") + os.chdir(cwd) + run_cmd(f"rm -rf {dir_name}") + else: + os.makedirs("download") + run_cmd(f"mv {dir_name} download/") + print(f"{colorama.Style.DIM}Your git repository was cloned into `download/{dir_name}`") + + class Package: def __init__(self, name: str, aur: bool = False, git: bool = False): self.name = name @@ -32,17 +80,17 @@ class Package: self.git_url = f"https://github.com/{self.name}" def install(self) -> None: - if not self.git and install.is_installed(self.name): + if not self.git and is_installed(self.name): raise PackageAlreadyInstalled(f"Package {self} is already installed") if self.aur: - if not install.is_installed("yay"): + if not is_installed("yay"): raise InvalidPackage(f"Package {self} can't be installed (missing `yay` - AUR installation software), alternatively, you can use git") - install.yay_install(self.name) + yay_install(self.name) elif self.git: - install.git_install(self.git_url) + git_install(self.git_url) else: - install.pacman_install(self.name) + pacman_install(self.name) def __repr__(self) -> str: if self.git: @@ -60,6 +108,6 @@ class Package: try: loaded_packages.append(cls(package, aur=aur, git=git)) except InvalidPackage as e: - Print.warning(str(e)) + print(f"{colorama.Fore.RED}{str(e)}") return loaded_packages diff --git a/src/util/user.py b/src/util/user.py deleted file mode 100644 index a1d577b..0000000 --- a/src/util/user.py +++ /dev/null @@ -1,83 +0,0 @@ -import typing as t - -from src.util import color - - -class Print: - @staticmethod - def question(question: str, options: t.Optional[list] = None) -> None: - """Print syntax for question with optional `options` to it""" - text = [f"{color.GREEN} // {question}{color.RESET}"] - if options: - for option in options: - text.append(f"{color.GREEN} # {option}{color.RESET}") - print("\n".join(text)) - - @staticmethod - def action(action: str) -> None: - """Print syntax for action""" - print(f"{color.GOLD} >> {action}{color.RESET}") - - @staticmethod - def err(text: str) -> None: - """Print syntax for error""" - print(f"\n{color.RED} !! {text}{color.RESET}") - - @staticmethod - def cancel(text: str) -> None: - """Print syntax for cancellation""" - print(f"{color.GREY} >> {text}{color.RESET}") - - @staticmethod - def comment(text: str) -> None: - """Print syntax for comments""" - print(f"{color.GREY} // {text}{color.RESET}") - - @staticmethod - def warning(text: str) -> None: - """Print syntax for warnings""" - print(f"{color.YELLOW} ** {text}{color.RESET}") - - -class Input: - @staticmethod - def yes_no(question: str) -> bool: - """Ask a yes/no `question`, return True(y)/False(n)""" - while True: - Print.question(question) - ans = input(" Y/N: ").lower() - if ans == "y" or ans == "": - return True - elif ans == "n": - return False - else: - Print.err("Invalid option (Y/N)") - - @staticmethod - def multiple(question: str, options: t.List[str], abort: bool = False) -> int: - """ - Ask `question` with multiple `options` - Return index from options list as chosen option - - You can also specify `abort` which will add option `-1` for No/Cancel - """ - num_options = [f"{index + 1}: {option}" for index, option in enumerate(options)] - if abort: - num_options.append("-1: No/Cancel") - while True: - Print.question(question, num_options) - try: - ans = int(input(f" Choice (1-{len(options)}{'/-1' if abort else None}): ")) - except TypeError: - Print.err(f"Invalid option, must be a number between 1-{len(options)}") - continue - - if ans in range(len(options) + 1): - return ans - 1 - else: - Print.err(f"Invalid option, outside of the number range 1-{len(options)}") - - @staticmethod - def text(text: str) -> str: - Print.question(text) - return input(" >>") diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6b1bd9f --- /dev/null +++ b/tox.ini @@ -0,0 +1,19 @@ +[flake8] +max_line_length=150 +import-order-style=pycharm +application_import_names=src +exclude= + .venv/**, + .git/** +ignore= + # Ignore missing return type annotations for special methods + ANN204 + # Ignore missing type annotations + ANN101 # Init + ANN102 # cls + ANN002, # *Args + ANN003, # **Kwargs + # Allow lambdas + E731 + # Allow markdown inline HTML + MD033