feat: add migration step to install cmd (#126)

* feat: add migration step to install cmd

* fix: set packages to explicitly installed

* fix: legacy remote check

Oops

* fix: generator

* fix: better legacy detection

* fix: run legacy detection before deployment

Also fix unlink on dir

* fix: legacy file check issue

* fix: handle no legacy

* fix: make sure not to go past home when looking for repo

* fix: wrong dir for default legacy path

* fix: catch errors with deleting legacy install
This commit is contained in:
2 * r + 2 * t
2026-06-18 21:40:05 +10:00
committed by GitHub
parent 7ff0913826
commit f53b3d036f
3 changed files with 145 additions and 7 deletions
+35 -4
View File
@@ -1,9 +1,11 @@
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.legacy import LEGACY_META_PKG, detect_legacy_repo, legacy_to_delete
from caelestia.utils.dots.manifest import ComponentError, Manifest, ManifestError
from caelestia.utils.dots.misc import build_local_packages, run_hooks
from caelestia.utils.dots.packages import DEFAULT_AUR_HELPER, PackageInstaller
@@ -34,14 +36,15 @@ class Command:
self.print_greeting()
self.create_backup()
legacy_dir = detect_legacy_repo() # Detect legacy repo first cause deploy overwrites legacy syms
source, tip, manifest = self.fetch_manifest()
deployed = self.deploy_configs(source, manifest)
helper, packages, local_packages = self.install_packages(source, manifest)
installer, packages, local_packages = self.install_packages(source, manifest)
run_hooks(manifest, "post_install")
DotsState(
aur_helper=helper,
aur_helper=getattr(installer, "helper", DEFAULT_AUR_HELPER),
applied_rev=tip,
enabled_components=manifest.enabled_components,
packages=packages,
@@ -49,6 +52,7 @@ class Command:
deployed_files=deployed,
).save()
self.migrate_legacy(installer, legacy_dir)
self.print_done()
def print_greeting(self) -> None:
@@ -144,7 +148,9 @@ class Command:
return deployer.deployed_files
def install_packages(self, source: DotsSource, manifest: Manifest) -> tuple[str, list[str], dict[str, list[str]]]:
def install_packages(
self, source: DotsSource, manifest: Manifest
) -> tuple[PackageInstaller, list[str], dict[str, list[str]]]:
installer = PackageInstaller.get(self.args.aur_helper, self.args.noconfirm)
packages = manifest.enabled_packages()
@@ -160,7 +166,32 @@ class Command:
log("Building local packages...")
local_packages = build_local_packages(installer, source, local_dirs)
return getattr(installer, "helper", DEFAULT_AUR_HELPER), packages, local_packages
return installer, packages, local_packages
def migrate_legacy(self, installer: PackageInstaller, legacy_dir: Path | None) -> None:
"""Clean up a previous install.fish setup (repo, symlinks and metapackage)."""
to_delete = legacy_to_delete(legacy_dir)
meta_installed = installer.is_installed(LEGACY_META_PKG)
if not to_delete and not meta_installed:
return
print()
log("Found a legacy Caelestia installation...")
if not confirm("Clear legacy installation?"):
return
deployer = Deployer()
try:
for path in to_delete:
deployer.remove(path)
info(f"Deleted {path}")
if meta_installed:
log("Removing legacy meta package...")
installer.remove([LEGACY_META_PKG])
except (OSError, subprocess.CalledProcessError) as e:
warn(f"could not fully clear the legacy installation: {e}")
def print_done(self) -> None:
print()
+83
View File
@@ -0,0 +1,83 @@
import subprocess
from pathlib import Path
from caelestia.utils.paths import config_dir, data_dir
LEGACY_META_PKG = "caelestia-meta"
_confs = [
"hypr",
"starship.toml",
"foot",
"fish",
"fastfetch",
"uwsm",
"btop",
"spicetify",
"Code/User/settings.json",
"VSCodium/User/settings.json",
"Code/User/keybindings.json",
"VSCodium/User/keybindings.json",
"code-flags.conf",
"codium-flags.conf",
]
def _find_legacy_repo(path: Path) -> Path | None:
try:
remote = subprocess.check_output(["git", "-C", path, "remote", "get-url", "origin"], text=True)
except subprocess.CalledProcessError:
return
# Check remote
if remote.strip() != "https://github.com/caelestia-dots/caelestia.git":
return
# Ignore anything outside home
if Path.home() not in path.parents:
return
# Walk up parents (capped at home) to find the repo root
while path != Path.home() and not (path / ".git").is_dir():
path = path.parent
# Only return path if didn't hit home (we really don't want to nuke home)
if path != Path.home():
return path
def detect_legacy_repo() -> Path | None:
for conf in _confs:
path = config_dir / conf
if not path.is_symlink():
continue
legacy_dir = _find_legacy_repo(path.resolve())
if legacy_dir:
return legacy_dir
return _find_legacy_repo(data_dir / "caelestia")
def legacy_to_delete(legacy_dir: Path | None) -> list[Path]:
if not legacy_dir:
return []
to_delete = []
for conf in _confs:
path = config_dir / conf
if path.is_symlink() and legacy_dir in path.resolve().parents:
to_delete.append(path)
others = [
*(Path.home() / ".zen").glob("*/chrome/userChrome.css"),
Path.home() / ".local/lib/caelestia/caelestiafox",
]
for path in others:
if path.is_symlink() and legacy_dir in path.resolve().parents:
to_delete.append(path)
to_delete.append(legacy_dir)
return to_delete
+27 -3
View File
@@ -73,6 +73,9 @@ class PackageInstaller(ABC):
def build_install(self, directory: Path) -> list[str]:
"""Build and install the PKGBUILD in `directory`, returning the installed package names."""
@abstractmethod
def is_installed(self, package: str) -> bool: ...
@abstractmethod
def system_update(self) -> None: ...
@@ -92,6 +95,9 @@ class NoopInstaller(PackageInstaller):
info(f"Skipping local package build (not on Arch): {directory}")
return []
def is_installed(self, package: str) -> bool:
return False
def system_update(self) -> None:
info("Skipping system update (not on Arch)")
@@ -101,10 +107,18 @@ class ArchInstaller(PackageInstaller):
self.helper = helper
self.flags = ["--noconfirm"] if noconfirm else []
def install(self, packages: list[str], extra_flags: list[str] | None = None) -> None:
def install(self, packages: list[str], explicit: bool = True) -> None:
if not packages:
return
subprocess.run([self.helper, "-S", "--needed", *self.flags, *(extra_flags or []), *packages], check=True)
cmd = [self.helper, "-S", "--needed", *self.flags]
if not explicit:
cmd.append("--asdeps") # Set install reason to dep (does not affect already installed packages)
subprocess.run(cmd + packages, check=True)
# Force install reason to explicit install
if explicit:
subprocess.run([self.helper, "-D", "--asexplicit", *self.flags, *packages], check=True)
def remove(self, packages: list[str]) -> None:
if not packages:
@@ -126,7 +140,7 @@ class ArchInstaller(PackageInstaller):
elif key == "depends":
depends.append(value.strip())
self.install(depends, extra_flags=["--asdeps"])
self.install(depends, explicit=False)
# Stop makepkg from resetting sudo
env = {**os.environ, "PACMAN_AUTH": "sudo"}
@@ -135,5 +149,15 @@ class ArchInstaller(PackageInstaller):
return names
def is_installed(self, package: str) -> bool:
return (
subprocess.run(
["pacman", "-Q", package],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
== 0
)
def system_update(self) -> None:
subprocess.run([self.helper, "-Syu", *self.flags], check=True)