diff options
author | Elizabeth <lizhunt@amazon.com> | 2025-05-28 18:19:25 -0700 |
---|---|---|
committer | Elizabeth <lizhunt@amazon.com> | 2025-05-28 18:48:35 -0700 |
commit | 2a0f7892fd3d00daa62a24359d2bae186092916f (patch) | |
tree | 04a8c5c6ba697440b3af812879905587bc97a2ae | |
parent | 1c2bd3faf3a1f0cd456a25b19ec404e1f79518c3 (diff) | |
download | dotfiles-2a0f7892fd3d00daa62a24359d2bae186092916f.tar.gz dotfiles-2a0f7892fd3d00daa62a24359d2bae186092916f.zip |
Fix that AI shit
-rw-r--r-- | .gitignore | 1 | ||||
-rwxr-xr-x[-rw-r--r--] | dots.py | 363 | ||||
-rwxr-xr-x | dots.sh | 12 |
3 files changed, 205 insertions, 171 deletions
@@ -1 +1,2 @@ .compiled_dotfiles/ +.venv @@ -1,190 +1,235 @@ #!/usr/bin/env python3 +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional import argparse import json +import logging import os -import shutil import subprocess import sys -from pathlib import Path +import shutil +from concurrent.futures import Executor, ThreadPoolExecutor, as_completed import jinja2 -def get_platform(): - """Get the platform using the platform.sh script""" - try: - script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "home/scripts/platform.sh") - result = subprocess.run([script_path], capture_output=True, text=True, check=True) - return result.stdout.strip() - except subprocess.CalledProcessError as e: - print(f"Error getting platform: {e}") + +@dataclass(frozen=True) +class Config: + SCRIPT_DIR: Path = Path(__file__).parent / "home/scripts" + COMPILED_DIR: Path = Path(".compiled_dotfiles") + CONTEXTS: Path = Path("context.json") + DEFAULT_TARGET_DIR: Path = Path.home() + DEFAULT_SOURCE_DIR: Path = Path(".") + MAX_WORKERS: int = (os.cpu_count() or 1) * 2 + + +@dataclass(frozen=True) +class Environment: + platform: str + device_name: str + context: Dict + verbose: bool + logger: logging.Logger + + +def main() -> None: + args = parse_arguments() + env = initialize_environment(args.verbose) + + if not (args.compile or args.stow or args.clean): + env.logger.error("At least one of --compile, --stow, or --clean must be specified") sys.exit(1) -def get_device_name(): - """Get the device name using the system_name.sh script""" + if args.compile: + env.logger.debug(f"Compiling {args.source} -> {args.comp}") + if not compile_dotfiles(args.source, args.comp, env): + env.logger.error("Failed to compile dotfiles") + sys.exit(1) + + if args.stow: + env.logger.debug(f"Stowing {(args.target)}") + if not stow_dotfiles(args.comp, args.target, env, clean=False): + env.logger.error("Failed to stow dotfiles") + sys.exit(1) + + if args.clean: + env.logger.debug(f"Cleaning dotfiles from {(args.target)}") + if not stow_dotfiles(args.comp, args.target, env, clean=True): + env.logger.error("Failed to clean dotfiles") + sys.exit(1) + + env.logger.info("Done!") + sys.exit(0) + + +def initialize_environment(verbose: bool) -> Environment: + logger = setup_logging(verbose) + platform = get_platform() + device_name = get_device_name() + context = { + **load_context(platform, device_name, Config.CONTEXTS), + "platform": platform, + "device_name": device_name, + } + + return Environment(platform, device_name, context, verbose, logger) + + +def copy_with_templates_rendered(executor: Executor, source: Path, destination: Path, env: Environment) -> bool: + shutil.copytree(source, destination, dirs_exist_ok=True) + env.logger.debug(f"copytree {source} -> {destination}") + + templates = [t for t in destination.glob("**/*.j2") if t.is_file()] + if not templates: + env.logger.debug(f"{source} has no templates") + return True + + for template in templates: + env.logger.debug(f"submitting work to render {source} . {templates}") + executor.submit(replace_with_rendered_template, template, env) + + return True + +def replace_with_rendered_template(template: Path, env: Environment) -> bool: + env.logger.debug(f"rendering {template}") + jinja_env = jinja2.Environment( + undefined=jinja2.StrictUndefined, trim_blocks=True, lstrip_blocks=True + ) + with open(template, "w") as t: + t.write(render(template, jinja_env, env) or "") + env.logger.debug(f"stripping template suffix of {template}") + template.rename(template.with_suffix("")) + return True + + + +def compile_dotfiles(source_dir: Path, target_dir: Path, env: Environment): + target_dir.mkdir(exist_ok=True, parents=True) + + with ThreadPoolExecutor(max_workers=Config.MAX_WORKERS) as executor: + dotfile_directories = [ + dir for dir in source_dir.iterdir() if dir.is_dir() and "." not in dir.name + ] + env.logger.debug(f"compiling dotfile directories {dotfile_directories}") + futures = [ + executor.submit( + copy_with_templates_rendered, executor, dotfiles, target_dir / dotfiles.name, env + ) + for dotfiles in dotfile_directories + ] + env.logger.debug(f"constructed {len(futures)} tasks") + return sum(1 for future in as_completed(futures) if future.result()) == len( + futures + ) + return False + + +def render( + source: Path, + jinja_env: jinja2.Environment, + env: Environment, +) -> Optional[str]: try: - script_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "home/scripts/system_name.sh") - result = subprocess.run([script_path], capture_output=True, text=True, check=True) - return result.stdout.strip() - except subprocess.CalledProcessError as e: - print(f"Error getting device name: {e}") - sys.exit(1) + env.logger.debug(f"Reading {source}") + with open(source, "r") as f: + template_content = f.read() + env.logger.debug(f"Compiling template {source}") + template = jinja_env.from_string(template_content) + return template.render(**env.context) + except Exception as e: + env.logger.error(f"Error rendering template {source}: {e}") + return None + + +def stow_package_task( + stow_dir: Path, package: Path, target_dir: Path, env: Environment +) -> bool: + try: + return True + except subprocess.SubprocessError as e: + env.logger.error(f"Error stowing package {package.name}: {e}") + return False + + +def stow_dotfiles(source_dir: Path, target_dir: Path, env: Environment, clean: bool = False) -> bool: + if not stow_installed(): + return False -def load_context(platform, device_name): - """Load the appropriate context from contexts.json""" + packages = [d for d in source_dir.iterdir() if d.is_dir()] + stow_op = ["-D"] if clean else ["--no-folding"] + + for pkg in packages: + run_shell_command(["stow", *stow_op, pkg.name]) + return True + + +def stow_installed() -> bool: + return "" != run_shell_command(["stow", "--version"]) + + +def run_shell_command(cmd: List[str]) -> str: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return result.stdout.strip() + + +def get_platform() -> str: + return run_shell_command([str(Config.SCRIPT_DIR / "platform.sh")]) + + +def get_device_name() -> str: + return run_shell_command([str(Config.SCRIPT_DIR / "system_name.sh")]) + + +def load_context(platform: str, device_name: str, context_file: Path) -> Dict: try: - with open('contexts.json', 'r') as f: + with open(context_file) as f: contexts = json.load(f) if platform not in contexts: - print(f"Warning: Platform '{platform}' not found in contexts.json") - return {} - - if device_name not in contexts[platform]: - print(f"Warning: Device '{device_name}' not found for platform '{platform}' in contexts.json") - # Try to use 'default' for the platform if device not found - if 'default' in contexts[platform]: - print(f"Using default context for platform '{platform}'") - return contexts[platform]['default'] return {} - return contexts[platform][device_name] - except FileNotFoundError: - print("Warning: contexts.json not found") + return contexts[platform].get( + device_name, contexts[platform].get("default", {}) + ) + except (FileNotFoundError, json.JSONDecodeError): return {} - except json.JSONDecodeError as e: - print(f"Error parsing contexts.json: {e}") - sys.exit(1) - -def compile_dotfiles(source_dir, target_dir, context): - """Compile dotfiles, processing Jinja templates with context""" - # Create jinja environment - env = jinja2.Environment( - undefined=jinja2.StrictUndefined, - trim_blocks=True, - lstrip_blocks=True - ) - # Ensure target directory exists - target_path = Path(target_dir) - target_path.mkdir(exist_ok=True, parents=True) - - # Process each file in source directory - for root, dirs, files in os.walk(source_dir): - # Skip .git directories - if '.git' in dirs: - dirs.remove('.git') - - # Create relative path from source_dir - rel_path = os.path.relpath(root, source_dir) - if rel_path == '.': - rel_path = '' - - # Create target directory - if rel_path: - target_subdir = target_path / rel_path - target_subdir.mkdir(exist_ok=True, parents=True) - else: - target_subdir = target_path - - for file in files: - source_file = os.path.join(root, file) - - # Determine target filename (remove .j2 extension for templates) - target_file_name = file[:-3] if file.endswith('.j2') else file - target_file = target_subdir / target_file_name - - print(f"Processing: {source_file} -> {target_file}") - - if file.endswith('.j2'): - # Render Jinja2 template - try: - with open(source_file, 'r') as f: - template_content = f.read() - - # Use the environment to create templates - template = env.from_string(template_content) - rendered_content = template.render(**context) - - # Write rendered content to target file - with open(target_file, 'w') as f: - f.write(rendered_content) - - # Make executable if source is executable - if os.access(source_file, os.X_OK): - os.chmod(target_file, 0o755) - - except Exception as e: - print(f"Error rendering template {source_file}: {e}") - else: - # Copy file as-is - shutil.copy2(source_file, target_file) - -def stow_dotfiles(dotfiles_dir, target_dir=None): - """Use GNU Stow to symlink the compiled dotfiles""" - if target_dir is None: - target_dir = os.path.expanduser("~") - - # Check if stow is installed - try: - subprocess.run(["stow", "--version"], capture_output=True, check=True) - except (subprocess.CalledProcessError, FileNotFoundError): - print("Error: GNU Stow not found. Please install it before using --stow.") - sys.exit(1) - # Get list of directories in dotfiles_dir (each is a stow package) - packages = [d for d in os.listdir(dotfiles_dir) if os.path.isdir(os.path.join(dotfiles_dir, d))] - - for package in packages: - print(f"Stowing package: {package}") - try: - # Use --adopt to replace existing files - # Use --no-folding to enable leaf mode (each file individually linked) - subprocess.run([ - "stow", - "--dir=" + dotfiles_dir, - "--target=" + target_dir, - "--adopt", - "--no-folding", - package - ], check=True) - print(f"Successfully stowed {package}") - except subprocess.CalledProcessError as e: - print(f"Error stowing {package}: {e}") - -def main(): +def parse_arguments() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Dotfiles manager") parser.add_argument("--compile", action="store_true", help="Compile dotfiles") parser.add_argument("--stow", action="store_true", help="Stow compiled dotfiles") - parser.add_argument("--target", help="Target directory for stow (default: $HOME)") - args = parser.parse_args() - - if not args.compile and not args.stow: - parser.print_help() - sys.exit(1) - - platform = get_platform() - device_name = get_device_name() - print(f"Platform: {platform}, Device: {device_name}") - - context = load_context(platform, device_name) - print(f"Loaded context: {context}") - - # Add platform and device_name to context - context['platform'] = platform - context['device_name'] = device_name + parser.add_argument("--clean", action="store_true", help="Clean (remove) stowed dotfiles") + parser.add_argument( + "--source", + help=f"Target directory for dotfiles (default: {Config.DEFAULT_SOURCE_DIR})", + default=Config.DEFAULT_SOURCE_DIR, + ) + parser.add_argument( + "--comp", + help=f"Target directory for compiled templates (default: {Config.COMPILED_DIR})", + default=Config.COMPILED_DIR, + ) + parser.add_argument( + "--target", + help=f"Target directory for stow (default: {Config.DEFAULT_TARGET_DIR})", + default=Config.DEFAULT_TARGET_DIR, + ) + parser.add_argument( + "--verbose", "-v", action="store_true", help="Enable verbose logging" + ) + return parser.parse_args() - compiled_dir = ".compiled_dotfiles" - if args.compile: - print(f"Compiling dotfiles from 'dotfiles' to '{compiled_dir}'...") - compile_dotfiles("dotfiles", compiled_dir, context) - print("Compilation complete.") +def setup_logging(verbose: bool) -> logging.Logger: + logging.basicConfig( + level=logging.DEBUG if verbose else logging.INFO, + format="%(asctime)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + return logging.getLogger("dotfiles") - if args.stow: - target_dir = args.target if args.target else None - print(f"Stowing dotfiles from '{compiled_dir}'...") - stow_dotfiles(compiled_dir, target_dir) - print("Stowing complete.") if __name__ == "__main__": main() diff --git a/dots.sh b/dots.sh deleted file mode 100755 index 38b4923..0000000 --- a/dots.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -# usage: ./dots.sh [update|clean]? - -OP="${1:-'update'}" - -STOW_OP="--no-folding" -if [ "$OP" = "clean" ]; then STOW_OP="-D"; fi - -find . -type d -mindepth 1 -maxdepth 1 -not -path '*/.*' \ - | sed "s/^\.\///" \ - | while read dir; do stow "$STOW_OP" "$dir"; done |