dotfiles/setup_dotfiles.py
Trysdyn Black 91b6e2377f Update setup script using new lint rules
No functional changes, I just wanted to tidy things up.
2023-12-28 18:01:11 -08:00

84 lines
3 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Dotfile setup utility.
Syncronizes dotfiles from a source dir that mimics a homedir tree to a homedir,
removing old symlinks as necessary as well as optionally files.
"""
import inspect
import os # Needed for os.walk til Py3.12
from pathlib import Path
FORCE = False
HOMEDIR = Path("~").expanduser()
SOURCEDIR = Path(str(inspect.getsourcefile(lambda: False))).parent
EXCLUSIONS = [".git", "setup_dotfiles.py", "README.md"]
def check_and_remove(source: Path, destination: Path, *, force: bool = False) -> bool:
"""
Check if a file exists and remove it if appropriate.
If it's a symlink we check it points the right place and remove it if not.
If it's a file, we remove it if --force is provided, or error if not.
If we need to re-deploy the symlink, return True, else return False.
"""
Path(destination).parent.mkdir(parents=True, exist_ok=True)
# Destination exists as a symlink
if Path(destination).is_symlink():
# Symlink is correct, return
if Path(destination).readlink() == source:
return False
# Symlink is incorrect, remove it
Path(destination).unlink()
# Destination exists as a file
elif Path(destination).exists():
# Force mode enabled, remove it (Data loss risk!!)
if force:
Path(destination).unlink()
# Force mode disabled, error out
else:
msg = f"{destination!s} exists as a file and --force not provided"
raise OSError(msg)
# File doesn't exist or undefined "thing" happened, return True to try to
# write, and if something goes wrong, we'll see the OSError
return True
def install_dotfiles(source_dir: Path, dest_dir: Path, exclusions: list, *, force: bool = False) -> None:
"""
Iterate files in SOURCEDIR and create symlinks to them in HOMEDIR.
We also exclude files noted in exclusions, and will remove files in the way
if "force" is true.
"""
for root, dir_names, file_names in os.walk(source_dir):
# Remove exclusions from the walk list so we don't touch them
for exclusion in exclusions:
if exclusion in dir_names:
dir_names.remove(exclusion)
if exclusion in file_names:
file_names.remove(exclusion)
for file_name in file_names:
# Get the full path for the source file
source = Path(root) / file_name
# Get the full path for where the symlink should go
# We remove the source_dir to get a dotfile dir relative path
destination = dest_dir / source.relative_to(source_dir)
# Check for and remove the destination file if it exists
# If a removal occurs, catch True and deploy the symlink
if check_and_remove(source, destination, force=force):
print(f"Symlinking {source} => {destination}")
os.symlink(source, destination)
if __name__ == "__main__":
install_dotfiles(SOURCEDIR, HOMEDIR, EXCLUSIONS, force=False)