Compltete source-code rewrite

This commit is contained in:
ItsDrike 2021-04-12 18:06:50 +02:00
parent 9f688a0ebf
commit 7a331619e0
No known key found for this signature in database
GPG key ID: 252D306F545351FC
15 changed files with 510 additions and 308 deletions

View file

@ -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 ## Set keyboard layout
Default layout will be US Default layout will be US
`ls /usr/share/kbd/keymaps/**/*.map.gz` <- list keymaps `ls /usr/share/kbd/keymaps/**/*.map.gz` <- list keymaps
@ -43,6 +50,7 @@ First select mirrors (`/etc/pacman.d/mirrorlist`)
## Chroot ## Chroot
`arch-chroot /mnt` `arch-chroot /mnt`
# Proceed from this line, if you're reading after chrooting
## Set time zone ## Set time zone
`ln -sf /usr/share/zoneinfo/Region/Ciry /etc/localtime` <- specify timezone `ln -sf /usr/share/zoneinfo/Region/Ciry /etc/localtime` <- specify timezone

View file

@ -1 +1,4 @@
pyyaml==5.4.1 pyyaml
psutil
colorama
pyinquirer

View file

@ -1,22 +1,26 @@
import os import sys
from src.packages import install_packages import colorama
from src.dotfiles import install_dotfiles import inquirer.shortcuts
from src.util.user import Input, Print
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 inquirer.shortcuts.confirm("Do you wish to perform Arch install? (Directly from live ISO)"):
if os.geteuid() == 0: run_cmd("clear")
Print.err("You can't to run this program as root user") print(f"{colorama.Fore.BLUE}Running Arch Installation")
return 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`)?"): if inquirer.shortcuts.confirm("Do you wish to perform package installation (from packages.yaml)"):
install_packages() 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 install dotfiles? (from home and root folders)"):
try: install_dotfiles()
main()
except KeyboardInterrupt:
Print.err("User cancelled (KeyboardInterrupt)")

130
src/arch_install.py Normal file
View file

@ -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()

12
src/chroot_install.py Normal file
View file

@ -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()

View file

@ -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()

111
src/dotfiles_install.py Normal file
View file

@ -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()

View file

@ -1,9 +1,13 @@
import typing as t import typing as t
import colorama
import inquirer.shortcuts
import yaml import yaml
from src.util.package import Package, PackageAlreadyInstalled, InvalidPackage from src.util.command import run_root_cmd
from src.util import install from src.util.package import InvalidPackage, Package, PackageAlreadyInstalled
from src.util.user import Print, Input
colorama.init(autoreset=True)
def obtain_packages() -> t.List[Package]: def obtain_packages() -> t.List[Package]:
@ -16,24 +20,25 @@ def obtain_packages() -> t.List[Package]:
packages = [] packages = []
packages += Package.safe_load(pacman_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(git_packages, git=True)
packages += Package.safe_load(aur_packages, aur=True)
return packages return packages
def install_packages() -> None: def install_packages() -> None:
packages = obtain_packages() packages = obtain_packages()
if Input.yes_no("Do you wish to perform system upgrade first? (Recommended)"): if inquirer.shortcuts.confirm("Do you wish to perform system upgrade first? (Recommended)", default=True):
install.upgrade_pacman() run_root_cmd("pacman -Syu")
for package in packages: for package in packages:
try: try:
Print.action(f"Installing {package}") print(f"{colorama.Fore.CYAN}Installing {package}")
package.install() package.install()
except PackageAlreadyInstalled: 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: except InvalidPackage as e:
Print.warning(str(e)) print(f"{colorama.Fore.RED}{str(e)}")
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -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")

View file

@ -1,27 +1,47 @@
import os
import subprocess import subprocess
import colorama
import inquirer.shortcuts
def execute(command) -> None: DEBUG = True
"""Execute bash `command`, return the returncode (int)"""
command = command.split(" ")
subprocess.call(command)
def get_output(command) -> str: def debug_confirm_run(cmd):
"""Get standard output of `command`""" if DEBUG:
command = command.split(" ") cnfrm = inquirer.shortcuts.confirm(
return subprocess.run( f"{colorama.Fore.BLUE}[DEBUG] Running command: "
command, f"{colorama.Fore.YELLOW}{cmd}{colorama.Fore.RESET}"
stderr=subprocess.STDOUT, )
stdout=subprocess.PIPE return cnfrm
).stdout.decode("utf-8") else:
return True
def get_return_code(command) -> int: def run_root_cmd(cmd: str, enable_debug: bool = True) -> subprocess.CompletedProcess:
"""get return code of `command` (int)""" """Run command as root"""
command = command.split(" ") if os.geteuid() != 0:
return subprocess.run( return run_cmd(f"sudo {cmd}", enable_debug=enable_debug)
command, else:
stderr=subprocess.STDOUT, return run_cmd(cmd, enable_debug=enable_debug)
stdout=subprocess.PIPE
).returncode
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

View file

@ -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}`")

95
src/util/internet.py Normal file
View file

@ -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

View file

@ -1,7 +1,13 @@
import os
import typing as t import typing as t
from pathlib import Path
from src.util import install import colorama
from src.util.user import Print import inquirer.shortcuts
from src.util.command import run_cmd, run_root_cmd
colorama.init(autoreset=True)
class InvalidPackage(Exception): class InvalidPackage(Exception):
@ -12,6 +18,48 @@ class PackageAlreadyInstalled(Exception):
pass 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: class Package:
def __init__(self, name: str, aur: bool = False, git: bool = False): def __init__(self, name: str, aur: bool = False, git: bool = False):
self.name = name self.name = name
@ -32,17 +80,17 @@ class Package:
self.git_url = f"https://github.com/{self.name}" self.git_url = f"https://github.com/{self.name}"
def install(self) -> None: 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") raise PackageAlreadyInstalled(f"Package {self} is already installed")
if self.aur: 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") 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: elif self.git:
install.git_install(self.git_url) git_install(self.git_url)
else: else:
install.pacman_install(self.name) pacman_install(self.name)
def __repr__(self) -> str: def __repr__(self) -> str:
if self.git: if self.git:
@ -60,6 +108,6 @@ class Package:
try: try:
loaded_packages.append(cls(package, aur=aur, git=git)) loaded_packages.append(cls(package, aur=aur, git=git))
except InvalidPackage as e: except InvalidPackage as e:
Print.warning(str(e)) print(f"{colorama.Fore.RED}{str(e)}")
return loaded_packages return loaded_packages

View file

@ -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(" >>")

19
tox.ini Normal file
View file

@ -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