diff --git a/dev_env_sync/core/platform.py b/dev_env_sync/core/platform.py new file mode 100644 index 0000000..d591012 --- /dev/null +++ b/dev_env_sync/core/platform.py @@ -0,0 +1,233 @@ +"""Platform detection for Linux, macOS, and WSL environments.""" + +import os +import platform +import re +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Optional + + +class Platform(Enum): + """Supported platforms.""" + LINUX = "linux" + MACOS = "macos" + WSL = "wsl" + UNKNOWN = "unknown" + + +@dataclass +class PlatformInfo: + """Information about the current platform.""" + system: str + release: str + version: str + machine: str + platform: Platform + is_wsl: bool + wsl_version: Optional[str] = None + + +class PlatformNotSupported(Exception): + """Raised when the platform is not supported.""" + pass + + +class PlatformDetector: + """Detects and provides information about the current platform.""" + + WSL_PATTERN = re.compile(r'(microsoft|wsl|WSL)', re.IGNORECASE) + + @staticmethod + def detect() -> PlatformInfo: + """Detect the current platform and return detailed information.""" + system = platform.system() + release = platform.release() + version = platform.version() + machine = platform.machine() + + is_wsl, wsl_version = PlatformDetector._detect_wsl() + + platform_enum = Platform.UNKNOWN + + if system == "Darwin": + platform_enum = Platform.MACOS + elif system == "Linux": + if is_wsl: + platform_enum = Platform.WSL + else: + platform_enum = Platform.LINUX + + return PlatformInfo( + system=system, + release=release, + version=version, + machine=machine, + platform=platform_enum, + is_wsl=is_wsl, + wsl_version=wsl_version, + ) + + @staticmethod + def _detect_wsl() -> tuple[bool, Optional[str]]: + """Detect if running in WSL and return version info.""" + try: + os_release_path = Path("/proc/sys/kernel/osrelease") + if os_release_path.exists(): + content = os_release_path.read_text().strip() + if PlatformDetector.WSL_PATTERN.search(content): + wsl_version = content + return True, wsl_version + except (IOError, PermissionError): + pass + + try: + version_path = Path("/proc/version") + if version_path.exists(): + content = version_path.read_text().strip() + if PlatformDetector.WSL_PATTERN.search(content): + return True, content + except (IOError, PermissionError): + pass + + if "microsoft" in platform.release().lower(): + return True, platform.release() + + return False, None + + @staticmethod + def is_linux() -> bool: + """Check if running on Linux.""" + return platform.system() == "Linux" and not PlatformDetector._detect_wsl()[0] + + @staticmethod + def is_macos() -> bool: + """Check if running on macOS.""" + return platform.system() == "Darwin" + + @staticmethod + def is_wsl() -> bool: + """Check if running on WSL.""" + return PlatformDetector._detect_wsl()[0] + + @staticmethod + def is_supported() -> bool: + """Check if the current platform is supported.""" + return PlatformDetector.detect().platform != Platform.UNKNOWN + + @staticmethod + def get_shell_config_dir() -> Path: + """Get the platform-specific shell configuration directory.""" + info = PlatformDetector.detect() + + if info.platform == Platform.MACOS: + return Path.home() / ".config" + elif info.platform in (Platform.LINUX, Platform.WSL): + xdg_config = os.environ.get("XDG_CONFIG_HOME") + if xdg_config: + return Path(xdg_config) + return Path.home() / ".config" + else: + return Path.home() + + @staticmethod + def get_home_dir() -> Path: + """Get the user's home directory.""" + return Path.home() + + @staticmethod + def get_backup_dir() -> Path: + """Get the default backup directory based on platform.""" + return PlatformDetector.get_home_dir() / ".dev-env-sync-backups" + + @staticmethod + def get_editor_config_dir(editor: str) -> Path: + """Get the platform-specific config directory for an editor.""" + info = PlatformDetector.detect() + home = Path.home() + + if editor == "vscode": + if info.platform == Platform.MACOS: + return home / "Library/Application Support/Code/User" + else: + xdg_config = os.environ.get("XDG_CONFIG_HOME") + if xdg_config: + return Path(xdg_config) / "Code/User" + return home / ".config/Code/User" + + elif editor == "neovim": + if info.platform == Platform.MACOS: + return home / ".config/nvim" + else: + xdg_config = os.environ.get("XDG_CONFIG_HOME") + if xdg_config: + return Path(xdg_config) / "nvim" + return home / ".config/nvim" + + elif editor == "vim": + if info.platform == Platform.MACOS: + return home + else: + return home + + else: + return PlatformDetector.get_shell_config_dir() + + @staticmethod + def get_package_manager() -> str: + """Get the appropriate package manager for the platform.""" + info = PlatformDetector.detect() + + if info.platform == Platform.MACOS: + return "brew" + elif info.platform == Platform.WSL: + if PlatformDetector._has_command("apt"): + return "apt" + elif PlatformDetector._has_command("dnf"): + return "dnf" + elif PlatformDetector._has_command("yum"): + return "yum" + elif info.platform == Platform.LINUX: + if PlatformDetector._has_command("apt"): + return "apt" + elif PlatformDetector._has_command("dnf"): + return "dnf" + elif PlatformDetector._has_command("yum"): + return "yum" + elif PlatformDetector._has_command("pacman"): + return "pacman" + + return "unknown" + + @staticmethod + def _has_command(cmd: str) -> bool: + """Check if a command is available.""" + import shutil + return shutil.which(cmd) is not None + + @staticmethod + def normalize_path(path: str) -> Path: + """Normalize a path for the current platform.""" + path_obj = Path(path).expanduser() + + if not path_obj.is_absolute(): + if path.startswith("~"): + path_obj = Path.home() / path[2:] + else: + path_obj = Path.cwd() / path_obj + + return path_obj + + @staticmethod + def get_path_separator() -> str: + """Get the path separator for the current platform.""" + return "/" if platform.system() != "Windows" else "\\" + + @staticmethod + def get_line_ending() -> str: + """Get the appropriate line ending for the current platform.""" + info = PlatformDetector.detect() + if info.platform == Platform.MACOS and platform.mac_ver()[0].startswith("10"): + return "\r" + return "\n"