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 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.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.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, PackageError, PackageInstaller
|
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()]
|
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:
|
class Command:
|
||||||
args: Namespace
|
args: Namespace
|
||||||
|
|
||||||
@@ -43,6 +64,7 @@ class Command:
|
|||||||
except PackageError as e:
|
except PackageError as e:
|
||||||
fatal(e)
|
fatal(e)
|
||||||
run_hooks(manifest, "post_package")
|
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)
|
deployed = self.deploy_configs(source, manifest)
|
||||||
run_hooks(manifest, "post_install")
|
run_hooks(manifest, "post_install")
|
||||||
|
|
||||||
@@ -171,6 +193,42 @@ class Command:
|
|||||||
|
|
||||||
return installer, packages, local_packages
|
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:
|
def migrate_legacy(self, installer: PackageInstaller, legacy_dir: Path | None) -> None:
|
||||||
"""Clean up a previous install.fish setup (repo, symlinks and metapackage)."""
|
"""Clean up a previous install.fish setup (repo, symlinks and metapackage)."""
|
||||||
|
|
||||||
@@ -186,6 +244,7 @@ class Command:
|
|||||||
|
|
||||||
deployer = Deployer()
|
deployer = Deployer()
|
||||||
try:
|
try:
|
||||||
|
self.deref_backup_syms(legacy_dir)
|
||||||
for path in to_delete:
|
for path in to_delete:
|
||||||
deployer.remove(path)
|
deployer.remove(path)
|
||||||
info(f"Deleted {path}")
|
info(f"Deleted {path}")
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ def _find_legacy_repo(path: Path) -> Path | None:
|
|||||||
return path
|
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:
|
def detect_legacy_repo() -> Path | None:
|
||||||
for conf in _confs:
|
for conf in _confs:
|
||||||
path = config_dir / conf
|
path = config_dir / conf
|
||||||
@@ -59,25 +63,32 @@ def detect_legacy_repo() -> Path | None:
|
|||||||
return _find_legacy_repo(data_dir / "caelestia")
|
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]:
|
def legacy_to_delete(legacy_dir: Path | None) -> list[Path]:
|
||||||
if not legacy_dir:
|
if not legacy_dir:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
to_delete = []
|
return [*legacy_symlinks(legacy_dir), legacy_dir]
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user