dotfiles/setup_dotfiles.py
Trysdyn Black 508fad2e2c Update supporting script
- Update README to properly reflect py3.9 requirement
- Add support for `--force` that's been advertised forever
2024-10-10 15:38:25 -07:00

91 lines
3.2 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
import sys
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
# This is the only function that bumps our requirement to py3.9, lol
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, skip this file
else:
print(f"{destination!s} exists as a file and --force not provided")
return False
# 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__":
force = "--force" in sys.argv
if force:
print("--force enabled, will remove files to create symlinks")
install_dotfiles(SOURCEDIR, HOMEDIR, EXCLUSIONS, force=force)