feat: add install command

Not wired yet
This commit is contained in:
2 * r + 2 * t
2026-06-14 21:05:56 +10:00
parent 393dbf6363
commit 36a6029a2c
7 changed files with 707 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
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 Manifest, ManifestError, expand, expand_dests
from caelestia.utils.dots.packages import 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 ManifestError 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", ""), 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}")