"""UV-based package installer for custom environments.""" import tempfile import shutil from typing import List from pathlib import Path class UVInstaller: """Manages UV-based package installation in containers.""" def __init__(self, cache_path: Path): """ Initialize UV installer. Args: cache_path: Path to UV cache directory """ self.cache_path = Path(cache_path) self._ensure_cache_dir() def generate_requirements(self, packages: List[str]) -> str: """ Generate requirements.txt content. One package per line with version specifiers preserved. Args: packages: List of package specifications Returns: requirements.txt content """ if not packages: return "" return '\n'.join(packages) def generate_containerfile( self, base_image: str, packages: List[str], python_version: str = "3.11" ) -> str: """ Generate Containerfile for building custom environment. Containerfile structure optimizes layer caching: 1. Base image 2. Install UV (cached layer) 3. Create non-root user 4. Copy requirements.txt (cache-friendly) 5. Install packages with UV 6. Set working directory Args: base_image: Base container image packages: List of package specifications python_version: Python version (for reference) Returns: Containerfile content """ has_packages = bool(packages) containerfile = f"""FROM {base_image} # Install UV for fast package installation RUN pip install --no-cache-dir uv # Create non-root user for security RUN useradd -m -u 1000 -s /bin/bash forge && \\ mkdir -p /home/forge/.cache/uv && \\ chown -R forge:forge /home/forge # Switch to non-root user USER forge WORKDIR /home/forge # Copy requirements for layer caching COPY --chown=forge:forge requirements.txt /home/forge/requirements.txt """ if has_packages: containerfile += """# Install packages with UV RUN uv pip install --system -r requirements.txt """ containerfile += """# Set working directory WORKDIR /home/forge/workspace # Default command CMD ["/bin/bash"] """ return containerfile def create_build_context( self, base_image: str, packages: List[str], python_version: str = "3.11" ) -> Path: """ Create temporary build context directory. Contains: - Containerfile - requirements.txt Args: base_image: Base container image packages: List of package specifications python_version: Python version Returns: Path to build context directory (caller must cleanup) """ # Create temporary directory context_dir = Path(tempfile.mkdtemp(prefix="mcp-forge-build-")) try: # Generate Containerfile containerfile_content = self.generate_containerfile( base_image, packages, python_version ) containerfile_path = context_dir / "Containerfile" containerfile_path.write_text(containerfile_content) # Generate requirements.txt requirements_content = self.generate_requirements(packages) requirements_path = context_dir / "requirements.txt" requirements_path.write_text(requirements_content) return context_dir except Exception: # Cleanup on error shutil.rmtree(context_dir, ignore_errors=True) raise def get_cache_volume_mount(self) -> dict: """ Get volume mount configuration for UV cache. Returns: Volume mount dict for Podman """ return { "bind": "/home/forge/.cache/uv", "mode": "rw" } def _ensure_cache_dir(self) -> None: """Ensure UV cache directory exists.""" self.cache_path.mkdir(parents=True, exist_ok=True)