Source code for arbdmodel.binary_manager

# -*- coding: utf-8 -*-

import os
import platform
import shutil
from pathlib import Path
from .logger import logger, get_resource_path

# Default binary names by platform
DEFAULT_BINARIES = {
    "arbd": {
        "windows": "arbd.exe",
        "linux": "arbd",
        "darwin": "arbd",  # macOS uses same binary name as Linux
    },
    "namd": {
        "windows": "namd2.exe",
        "linux": "namd2",
        "darwin": "namd2",  # macOS uses same binary name as Linux
    },
    "vmd": {
        "windows": "vmd.exe",
        "linux": "vmd",
        "darwin": "vmd",  # macOS uses same binary name as Linux
    },
    "hydropro": {
        "windows": "hydropro10-msd.exe",
        "linux": "hydropro10-lnx.exe",
        "darwin": "hydropro10-lnx.exe",  # HydroPro doesn't have a macOS-specific version, use Linux version
    },
    "apbs": {
        "windows": "apbs.exe",
        "linux": "apbs",
        "darwin": "apbs",  # macOS uses same binary name as Linux
    },
    "gmsh": {
        "windows": "gmsh.exe",
        "linux": "gmsh",
        "darwin": "gmsh",  # macOS uses same binary name as Linux
    },
}

# Resource directories where binaries might be bundled with the package
RESOURCE_DIRS = {
    "hydropro": "hydropro10",
}

# Singleton for binary path management
class _BinaryManager:
    def __init__(self):
        # Initialize empty paths dictionary
        self._binary_paths = {}
        
        # Determine platform
        self._platform = platform.system().lower()
        if self._platform not in ("windows", "linux", "darwin"):
            logger.warning(f"Unsupported platform: {self._platform}, using linux defaults")
            self._platform = "linux"
        
        # For binary path purposes, treat macOS (darwin) similar to Linux for many tools
        # that don't have macOS-specific versions

    def set_binary_path(self, binary_name, path):
        """
        Set the path for a specific binary.
        
        Args:
            binary_name: The name of the binary (e.g., 'arbd', 'hydropro')
            path: The full path to the binary
        """
        if path is None:
            if binary_name in self._binary_paths:
                del self._binary_paths[binary_name]
            return
            
        path = Path(path)
        self._binary_paths[binary_name.lower()] = path
    
    def get_binary_path(self, binary_name):
        """
        Get the path for a specific binary, searching if not explicitly set.
        
        Args:
            binary_name: The name of the binary (e.g., 'arbd', 'hydropro')
            
        Returns:
            Path to the binary if found, None otherwise
        """
        binary_name = binary_name.lower()
        
        # 1. Return already configured path if available
        if binary_name in self._binary_paths:
            return self._binary_paths[binary_name]
        
        # 2. Search for the binary
        binary_path = self._find_binary(binary_name)
        if binary_path:
            self._binary_paths[binary_name] = binary_path
            return binary_path
            
        # 3. Not found
        return None
    
    def _find_binary(self, binary_name):
        """
        Search for a binary in standard locations.
        
        Search order:
        1. System PATH
        2. Package resource directories
        3. Current directory
        
        Args:
            binary_name: The name of the binary to find
            
        Returns:
            Path to the binary if found, None otherwise
        """
        # Get the default binary name for the current platform
        if binary_name in DEFAULT_BINARIES:
            binary_filename = DEFAULT_BINARIES[binary_name][self._platform]
        else:
            # If not in our defaults, use the name as is
            binary_filename = binary_name
        
        # 1. Check in system PATH
        system_path = shutil.which(binary_filename)
        if system_path:
            return Path(system_path)
        
        # 2. Check in package resource directories
        if binary_name in RESOURCE_DIRS:
            resource_dir = get_resource_path(RESOURCE_DIRS[binary_name])
            resource_path = resource_dir / binary_filename
            if resource_path.exists():
                # Make binary executable if needed (Unix only)
                if self._platform != "windows" and not os.access(resource_path, os.X_OK):
                    os.chmod(resource_path, 0o755)
                return resource_path
        
        # 3. Check in current directory
        local_path = Path.cwd() / binary_filename
        if local_path.exists():
            return local_path
        
        # Not found
        return None
    
    def get_all_paths(self):
        """
        Get all configured binary paths.
        
        Returns:
            Dictionary of binary name to path
        """
        return self._binary_paths.copy()
    
    def export_to_dict(self):
        """
        Export all binary paths as a dictionary of strings.
        
        Returns:
            Dictionary of binary name to string path
        """
        return {name: str(path) for name, path in self._binary_paths.items()}
    
    def import_from_dict(self, paths_dict):
        """
        Import binary paths from a dictionary.
        
        Args:
            paths_dict: Dictionary of binary name to path
        """
        for name, path in paths_dict.items():
            if path:  # Skip empty paths
                self.set_binary_path(name, path)
                
    def is_binary_essential(self, binary_name):
        """
        Check if a binary is considered essential for core functionality.
        
        Args:
            binary_name: The name of the binary to check
            
        Returns:
            True if the binary is essential, False otherwise
        """
        essential_binaries = ["arbd"]  # These are required for core functionality
        return binary_name.lower() in essential_binaries

# Create the singleton instance
BinaryManager = _BinaryManager()

# Initialize binary paths from environment variables
[docs] def initialize_binary_paths(): """ Initialize binary paths from environment variables. This function should be called during package initialization. If users don't wish to call this function, they can set the paths manually. Example: >>> arbd_path = BinaryManager.get_binary_path("arbd") >>> print(f"ARBD binary path: {arbd_path}") >>> BinaryManager.set_binary_path("hydropro", "/path/to/hydropro") """ # Check environment variables for binary paths for binary in DEFAULT_BINARIES: env_var = f"{binary.upper()}_PATH" if env_var in os.environ: BinaryManager.set_binary_path(binary, os.environ[env_var]) # Try to load from configuration file try: import json config_path = Path.home() / ".config" / "arbd" / "binaries.json" if config_path.exists(): with open(config_path) as f: paths_dict = json.load(f) BinaryManager.import_from_dict(paths_dict) except Exception as e: # Silently continue with environment or defaults if config loading fails pass
# Initialize binary paths on module import initialize_binary_paths() """ Usage example: arbd_path = BinaryManager.get_binary_path("arbd") print(f"ARBD binary path: {arbd_path}") # Set binary path BinaryManager.set_binary_path("hydropro", "/path/to/hydropro") """