Files
caelestia-cli/src/caelestia/subcommands/install.py
T
2026-06-15 22:57:16 +10:00

172 lines
6.3 KiB
Python

import os
import shutil
import subprocess
import textwrap
from argparse import Namespace
from pathlib import Path
from caelestia.utils.dots.deployer import Deployer
from caelestia.utils.dots.manifest import ComponentError, Manifest, ManifestError, expand, expand_dests
from caelestia.utils.dots.packages import DEFAULT_AUR_HELPER, PackageInstaller
from caelestia.utils.dots.source import DotsSource, SourceError
from caelestia.utils.dots.state import DotsState
from caelestia.utils.io import confirm, disable_input, fatal, info, log, pause, warn
from caelestia.utils.paths import (
config_backup_dir,
config_dir,
dots_dir,
)
def _parse_list_arg(value: str | None) -> list[str] | None:
if value is None:
return None
return [item.strip() for item in value.split(",") if item.strip()]
class Command:
args: Namespace
def __init__(self, args: Namespace) -> None:
self.args = args
def run(self) -> None:
if self.args.noconfirm:
disable_input()
self.print_greeting()
self.create_backup()
source, tip, manifest = self.fetch_manifest()
self.deploy_configs(source, manifest)
helper, packages, local_packages = self.install_packages(source, manifest)
self.run_hooks(manifest)
DotsState(
aur_helper=helper,
applied_rev=tip,
enabled_components=manifest.enabled_components,
packages=packages,
local_packages=local_packages,
).save()
info("Done!")
def print_greeting(self) -> None:
print(
"\033[38;2;150;241;241m" # Caelestia colour
+ textwrap.dedent(
r"""
╭─────────────────────────────────────────────────╮
│ ______ __ __ _ │
│ / ____/___ ____ / /__ _____/ /_(_)___ _ │
│ / / / __ `/ _ \/ / _ \/ ___/ __/ / __ `/ │
│ / /___/ /_/ / __/ / __(__ ) /_/ / /_/ / │
\____/\__,_/\___/_/\___/____/\__/_/\__,_/ │
│ │
╰─────────────────────────────────────────────────╯
"""
)
+ "\033[0m"
)
info("Welcome to the Caelestia dotfiles installer!")
info("Here's a quick overview on what this command is going to do:")
info(" - Install dependencies")
info(" - Install config files")
info("The installer does NOT set up hardware/system level configs (e.g. drivers). Please do this yourself.")
pause()
def create_backup(self) -> None:
if config_dir.exists():
if not confirm("Back up the config directory?", default=True):
return
log(f"Creating a backup of {config_dir}...")
if config_backup_dir.exists():
if not confirm("A backup already exists, overwrite?", default=False):
info("Not creating backup.")
return
log("Deleting old backup...")
shutil.rmtree(config_backup_dir)
shutil.copytree(config_dir, config_backup_dir, symlinks=True)
info(f"Created backup at {config_backup_dir}")
def fetch_manifest(self) -> tuple[DotsSource, str, Manifest]:
log("Fetching dots repo...")
source = DotsSource()
try:
source.ensure()
tip = source.checkout_tip()
except SourceError as e:
fatal(e)
try:
manifest = source.manifest_at(tip)
manifest.resolve_components(
enable=_parse_list_arg(self.args.enable_components),
disable=_parse_list_arg(self.args.disable_components),
)
except (SourceError, ManifestError, ComponentError) as e:
fatal(e)
names = ", ".join(manifest.enabled_components) or "none"
info(f"Enabled components: {names}")
return source, tip, manifest
def deploy_configs(self, source: DotsSource, manifest: Manifest) -> None:
log("Installing configs...")
deployer = Deployer()
for entry in manifest.enabled_entries():
src = source.working_path(expand(entry.src))
if not src.exists():
warn(f"missing in source, skipping: {entry.src}")
continue
dests = expand_dests(entry.dest)
if not dests:
warn(f"dest glob matched nothing, skipping: {entry.dest}")
continue
for dest in dests:
deployer.place(src, Path(dest))
info(f"{entry.src} -> {dest}")
def install_packages(self, source: DotsSource, manifest: Manifest) -> tuple[str, list[str], dict[str, list[str]]]:
installer = PackageInstaller.get(self.args.aur_helper, self.args.noconfirm)
packages = manifest.enabled_packages()
if packages:
log("Installing packages...")
installer.install(packages)
local_packages = {}
local_dirs = manifest.enabled_local_packages()
if local_dirs:
log("Building local packages...")
for path in local_dirs:
directory = source.working_path(path)
if not directory.is_dir():
warn(f"missing in repo, skipping: {path}")
continue
log(f"Building {path}...")
local_packages[path] = installer.build_install(directory)
return getattr(installer, "helper", DEFAULT_AUR_HELPER), packages, local_packages
def run_hooks(self, manifest: Manifest) -> None:
hooks = manifest.enabled_hooks("post_install")
if not hooks:
return
log("Running post-install hooks...")
env = {**os.environ, "CAELESTIA_DOTS": str(dots_dir)}
for hook in hooks:
log(f"Running hook: {hook}")
result = subprocess.run(hook, shell=True, env=env)
if result.returncode != 0:
warn(f"hook exited with {result.returncode}")