diff --git a/src/caelestia/subcommands/install.py b/src/caelestia/subcommands/install.py index da3e37d..bc337e8 100644 --- a/src/caelestia/subcommands/install.py +++ b/src/caelestia/subcommands/install.py @@ -4,7 +4,13 @@ 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.legacy import ( + LEGACY_META_PKG, + detect_legacy_repo, + legacy_config_symlinks, + legacy_symlinks, + 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, PackageError, PackageInstaller @@ -23,6 +29,21 @@ def _parse_list_arg(value: str | None) -> list[str] | None: return [item.strip() for item in value.split(",") if item.strip()] +def _deref_symlink(link: Path, target: Path) -> None: + """Replace symlink `link` with a real copy of `target`'s content.""" + + bak = link.rename(link.parent / f"{link.name}.bak") + try: + if target.is_dir(): + shutil.copytree(target, link, symlinks=True) + else: + shutil.copy2(target, link) + except OSError: + bak.rename(link) + raise + bak.unlink() + + class Command: args: Namespace @@ -43,6 +64,7 @@ class Command: except PackageError as e: fatal(e) run_hooks(manifest, "post_package") + self.dereference_legacy(legacy_dir) # Copy legacy content into place before deploy overwrites the symlinks deployed = self.deploy_configs(source, manifest) run_hooks(manifest, "post_install") @@ -171,6 +193,42 @@ class Command: return installer, packages, local_packages + def dereference_legacy(self, legacy_dir: Path | None) -> None: + """Replace legacy symlinks with real copies of their targets.""" + + symlinks = legacy_symlinks(legacy_dir) + if not symlinks: + return + + print() + log("Preserving content from legacy symlinks...") + for path in symlinks: + target = path.resolve() + if not target.exists(): + continue + + try: + _deref_symlink(path, target) + info(f"Copied {target} -> {path}") + except OSError as e: + warn(f"failed to preserve {path}: {e}") + + def deref_backup_syms(self, legacy_dir: Path | None) -> None: + """Deref the backup's legacy symlinks before the repo is cleared, so the backup keeps real content.""" + + if not config_backup_dir.is_dir(): + return + + for link in legacy_config_symlinks(config_backup_dir, legacy_dir): + target = link.resolve() + if not target.exists(): + continue + + try: + _deref_symlink(link, target) + except OSError as e: + warn(f"failed to preserve {link} in backup: {e}") + def migrate_legacy(self, installer: PackageInstaller, legacy_dir: Path | None) -> None: """Clean up a previous install.fish setup (repo, symlinks and metapackage).""" @@ -186,6 +244,7 @@ class Command: deployer = Deployer() try: + self.deref_backup_syms(legacy_dir) for path in to_delete: deployer.remove(path) info(f"Deleted {path}") diff --git a/src/caelestia/utils/dots/legacy.py b/src/caelestia/utils/dots/legacy.py index a17fdf7..b706d14 100644 --- a/src/caelestia/utils/dots/legacy.py +++ b/src/caelestia/utils/dots/legacy.py @@ -46,6 +46,10 @@ def _find_legacy_repo(path: Path) -> Path | None: return path +def _filter_candidates(candidates: list[Path], legacy_dir: Path) -> list[Path]: + return [path for path in candidates if path.is_symlink() and legacy_dir in path.resolve().parents] + + def detect_legacy_repo() -> Path | None: for conf in _confs: path = config_dir / conf @@ -59,25 +63,32 @@ def detect_legacy_repo() -> Path | None: return _find_legacy_repo(data_dir / "caelestia") +def legacy_config_symlinks(base: Path, legacy_dir: Path | None) -> list[Path]: + """Config-relative links install.fish created, resolved under `base` (the live config or a backup of it).""" + + if not legacy_dir: + return [] + + candidates = [base / conf for conf in _confs] + return _filter_candidates(candidates, legacy_dir) + + +def legacy_symlinks(legacy_dir: Path | None) -> list[Path]: + """All paths symlinked into the legacy repo (the links install.fish created).""" + + if not legacy_dir: + return [] + + extras = [ + *(Path.home() / ".zen").glob("*/chrome/userChrome.css"), + Path.home() / ".local/lib/caelestia/caelestiafox", + ] + + return [*legacy_config_symlinks(config_dir, legacy_dir), *_filter_candidates(extras, legacy_dir)] + + 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 + return [*legacy_symlinks(legacy_dir), legacy_dir]