mirror of
https://github.com/ItsDrike/dotfiles.git
synced 2024-12-25 20:54:34 +00:00
Compltete source-code rewrite
This commit is contained in:
parent
9f688a0ebf
commit
7a331619e0
|
@ -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
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
pyyaml==5.4.1
|
||||
pyyaml
|
||||
psutil
|
||||
colorama
|
||||
pyinquirer
|
||||
|
|
|
@ -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()
|
||||
|
|
130
src/arch_install.py
Normal file
130
src/arch_install.py
Normal 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
12
src/chroot_install.py
Normal 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()
|
|
@ -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
111
src/dotfiles_install.py
Normal 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()
|
|
@ -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__":
|
|
@ -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")
|
|
@ -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
|
||||
|
|
|
@ -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
95
src/util/internet.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
19
tox.ini
Normal 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
|
Loading…
Reference in a new issue