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 shutil
import subprocess
import textwrap import textwrap
from argparse import Namespace from argparse import Namespace
from pathlib import Path from pathlib import Path
from caelestia.utils.dots.deployer import Deployer 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.manifest import ComponentError, Manifest, ManifestError
from caelestia.utils.dots.misc import build_local_packages, run_hooks from caelestia.utils.dots.misc import build_local_packages, run_hooks
from caelestia.utils.dots.packages import DEFAULT_AUR_HELPER, PackageInstaller from caelestia.utils.dots.packages import DEFAULT_AUR_HELPER, PackageInstaller
@@ -34,14 +36,15 @@ class Command:
self.print_greeting() self.print_greeting()
self.create_backup() self.create_backup()
legacy_dir = detect_legacy_repo() # Detect legacy repo first cause deploy overwrites legacy syms
source, tip, manifest = self.fetch_manifest() source, tip, manifest = self.fetch_manifest()
deployed = self.deploy_configs(source, 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") run_hooks(manifest, "post_install")
DotsState( DotsState(
aur_helper=helper, aur_helper=getattr(installer, "helper", DEFAULT_AUR_HELPER),
applied_rev=tip, applied_rev=tip,
enabled_components=manifest.enabled_components, enabled_components=manifest.enabled_components,
packages=packages, packages=packages,
@@ -49,6 +52,7 @@ class Command:
deployed_files=deployed, deployed_files=deployed,
).save() ).save()
self.migrate_legacy(installer, legacy_dir)
self.print_done() self.print_done()
def print_greeting(self) -> None: def print_greeting(self) -> None:
@@ -144,7 +148,9 @@ class Command:
return deployer.deployed_files 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) installer = PackageInstaller.get(self.args.aur_helper, self.args.noconfirm)
packages = manifest.enabled_packages() packages = manifest.enabled_packages()
@@ -160,7 +166,32 @@ class Command:
log("Building local packages...") log("Building local packages...")
local_packages = build_local_packages(installer, source, local_dirs) 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: def print_done(self) -> None:
print() 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]: def build_install(self, directory: Path) -> list[str]:
"""Build and install the PKGBUILD in `directory`, returning the installed package names.""" """Build and install the PKGBUILD in `directory`, returning the installed package names."""
@abstractmethod
def is_installed(self, package: str) -> bool: ...
@abstractmethod @abstractmethod
def system_update(self) -> None: ... def system_update(self) -> None: ...
@@ -92,6 +95,9 @@ class NoopInstaller(PackageInstaller):
info(f"Skipping local package build (not on Arch): {directory}") info(f"Skipping local package build (not on Arch): {directory}")
return [] return []
def is_installed(self, package: str) -> bool:
return False
def system_update(self) -> None: def system_update(self) -> None:
info("Skipping system update (not on Arch)") info("Skipping system update (not on Arch)")
@@ -101,10 +107,18 @@ class ArchInstaller(PackageInstaller):
self.helper = helper self.helper = helper
self.flags = ["--noconfirm"] if noconfirm else [] 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: if not packages:
return 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: def remove(self, packages: list[str]) -> None:
if not packages: if not packages:
@@ -126,7 +140,7 @@ class ArchInstaller(PackageInstaller):
elif key == "depends": elif key == "depends":
depends.append(value.strip()) depends.append(value.strip())
self.install(depends, extra_flags=["--asdeps"]) self.install(depends, explicit=False)
# Stop makepkg from resetting sudo # Stop makepkg from resetting sudo
env = {**os.environ, "PACMAN_AUTH": "sudo"} env = {**os.environ, "PACMAN_AUTH": "sudo"}
@@ -135,5 +149,15 @@ class ArchInstaller(PackageInstaller):
return names 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: def system_update(self) -> None:
subprocess.run([self.helper, "-Syu", *self.flags], check=True) subprocess.run([self.helper, "-Syu", *self.flags], check=True)