mirror of
https://github.com/caelestia-dots/cli.git
synced 2026-06-19 07:20:01 -05:00
fix: deref legacy symlinks (#127)
* fix: deref legacy syms before deploy Deploy will completely overwrite symlinked dirs, so you'd lose all extra content inside them Also deref syms in config backup before deleting legacy dir * fix: restore link if deref fails
This commit is contained in:
@@ -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}")
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user