mirror of
https://github.com/ItsDrike/dotfiles.git
synced 2024-12-26 13:14:35 +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
|
## 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
|
||||||
|
|
|
@ -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
|
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
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 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__":
|
|
@ -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 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
|
||||||
|
|
|
@ -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
|
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
|
||||||
|
|
|
@ -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