Adding New Plugins ================== The plugin architecture makes it easy to add support for new version sources and spec managers. Adding a Spec Manager ---------------------- To add a new spec manager (e.g., for a new language ecosystem): 1. **Create the spec manager file** Create ``src/dist_git_manager/spec_managers/new_manager.py`` 2. **Inherit from SpecManager base class** .. code-block:: python from pathlib import Path from typing import Optional import subprocess from dist_git_manager.spec_managers.base import SpecManager from dist_git_manager.exceptions import SpecManagerError from dist_git_manager.logger import get_logger class NewSpecManager(SpecManager): """Spec manager for XYZ packages using xyz2rpm.""" def __init__(self, dry_run: bool = False): super().__init__(dry_run) self.logger = get_logger() def update_spec( self, repo_path: Path, branch: str, package_name: str, version: str, dry_run: bool, ) -> None: """Generate spec file using xyz2rpm.""" spec_path = self.find_spec_file(repo_path, package_name) # Call the tool cmd = ["xyz2rpm", package_name, "--version", version] if dry_run: self.logger.info(f"[DRY-RUN] Would run: {' '.join(cmd)}") return try: result = subprocess.run( cmd, cwd=repo_path, capture_output=True, text=True, check=True, ) self.logger.info(f"Generated spec for {package_name} {version}") except subprocess.CalledProcessError as e: raise SpecManagerError(f"xyz2rpm failed: {e.stderr}") def parse_version(self, spec_path: Path) -> Optional[str]: """Extract version from spec file using rpmspec.""" return self._parse_version_rpmspec(spec_path) def find_spec_file( self, repo_path: Path, package_name: str ) -> Optional[Path]: """Find spec file in repository.""" # Standard locations candidates = [ repo_path / f"{package_name}.spec", repo_path / f"xyz-{package_name}.spec", # Language prefix repo_path / "SPECS" / f"{package_name}.spec", ] for path in candidates: if path.exists(): return path return None def ensure_prerequisites( self, repo_path: Path, project: dict ) -> None: """Ensure necessary files exist (if needed).""" # Some spec managers need config files, others don't pass 3. **Register in DistGitManager** Edit ``src/dist_git_manager/core/manager.py`` and add to ``_get_spec_manager()``: .. code-block:: python from dist_git_manager.spec_managers.new_manager import NewSpecManager def _get_spec_manager(self, project: Dict[str, Any]) -> SpecManager: # ... existing code ... elif manager_type == "xyz2rpm": return NewSpecManager(dry_run=self.dry_run) else: raise ConfigError(f"Unknown spec manager type: {manager_type}") 4. **Add to CLI choices** Edit ``src/dist_git_manager/cli.py`` and add to spec manager choices: .. code-block:: python cli_group.add_argument( "--spec-manager", choices=["packit", "rust2rpm", "pyp2spec", "xyz2rpm"], # Add here help="Spec manager type (required for CLI-only mode)", ) 5. **Update list-plugins command** Edit ``src/dist_git_manager/cli.py`` in ``list_plugins()``: .. code-block:: python def list_plugins() -> None: print("Available spec managers:") print(" - packit : Packit (updates existing specs)") print(" - rust2rpm : rust2rpm (generates specs from crates.io)") print(" - pyp2spec : pyp2spec (generates specs from PyPI)") print(" - xyz2rpm : xyz2rpm (generates specs from XYZ registry)") 6. **Add tests** Create ``tests/unit/test_new_manager.py`` following the pattern of existing spec manager tests. Adding a Version Source ------------------------ To add a new version source (e.g., for a new package registry): 1. **Create the version source file** Create ``src/dist_git_manager/version_sources/new_source.py`` 2. **Inherit from VersionSource base class** .. code-block:: python from typing import List, Optional import requests from dist_git_manager.version_sources.base import VersionSource from dist_git_manager.exceptions import VersionSourceError from dist_git_manager.logger import get_logger class NewVersionSource(VersionSource): """Fetch versions from XYZ package registry.""" def __init__(self, api_url: str = "https://api.xyz-registry.org"): super().__init__() self.logger = get_logger() self.api_url = api_url def get_latest_version( self, identifier: str, release_type: str = "semver" ) -> Optional[str]: """Get latest version for a package.""" try: versions = self._fetch_all_versions(identifier) except Exception as e: raise VersionSourceError(f"Failed to fetch versions: {e}") if not versions: return None # Filter and sort based on release_type if release_type == "semver": # Return latest stable semantic version return self._get_latest_semver(versions) else: # Return most recent version return versions[0] if versions else None def _fetch_all_versions(self, package_name: str) -> List[str]: """Fetch all versions from API.""" url = f"{self.api_url}/packages/{package_name}/versions" try: response = requests.get(url, timeout=30) response.raise_for_status() data = response.json() # Extract versions from response versions = data.get("versions", []) return versions except requests.RequestException as e: raise VersionSourceError(f"API request failed: {e}") 3. **Register in DistGitManager** Edit ``src/dist_git_manager/core/manager.py`` and add to ``_get_version_source()``: .. code-block:: python from dist_git_manager.version_sources.new_source import NewVersionSource def _get_version_source(self, project: Dict[str, Any]) -> VersionSource: # ... existing code ... elif source_type == "xyz_registry": return NewVersionSource() else: raise ConfigError(f"Unknown version source type: {source_type}") 4. **Add to CLI choices and documentation** (same pattern as spec manager) 5. **Add tests** (same pattern as spec manager) Fedora Spec Generators ----------------------- Here are the additional Fedora spec generators that could be added: R Packages (r2spec) ~~~~~~~~~~~~~~~~~~~ **Tool:** https://pagure.io/r2spec **Version Source:** CRAN (Comprehensive R Archive Network) **API:** https://cran.r-project.org/web/packages/{package}/index.html **Implementation:** .. code-block:: python # Version Source class CRANVersionSource(VersionSource): def get_latest_version(self, identifier: str, release_type: str) -> str: url = f"https://cran.r-project.org/web/packages/{identifier}/index.html" # Parse HTML to extract version # Or use: https://crandb.r-pkg.org/{identifier} # Spec Manager class R2SpecManager(SpecManager): def update_spec(self, repo_path, branch, package_name, version, dry_run): cmd = ["r2spec", "update", package_name, version] # Run r2spec command Go Packages (go2rpm) ~~~~~~~~~~~~~~~~~~~~ **Tool:** https://gitlab.com/fedora/sigs/go/go2rpm **Version Source:** GitHub releases or Go package proxy **API:** https://proxy.golang.org/{module}/@v/list **Implementation:** .. code-block:: python # Version Source class GoProxyVersionSource(VersionSource): def get_latest_version(self, identifier: str, release_type: str) -> str: # identifier is module path like "github.com/user/repo" url = f"https://proxy.golang.org/{identifier}/@v/list" # Fetch and parse version list # Spec Manager class Go2RpmManager(SpecManager): def update_spec(self, repo_path, branch, package_name, version, dry_run): cmd = ["go2rpm", "convert", "--version", version, package_name] # Run go2rpm command Octave Packages (oct2spec) ~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Tool:** https://pagure.io/oct2spec **Version Source:** Octave Forge **API:** https://octave.sourceforge.io/packages.php **Implementation:** .. code-block:: python # Version Source class OctaveForgeVersionSource(VersionSource): def get_latest_version(self, identifier: str, release_type: str) -> str: url = f"https://octave.sourceforge.io/{identifier}/index.html" # Parse package page for version # Spec Manager class Oct2SpecManager(SpecManager): def update_spec(self, repo_path, branch, package_name, version, dry_run): cmd = ["oct2spec", package_name, version] # Run oct2spec command Ruby Gems (gem2rpm) ~~~~~~~~~~~~~~~~~~~ **Tool:** https://github.com/fedora-ruby/gem2rpm **Version Source:** RubyGems.org **API:** https://rubygems.org/api/v1/versions/{gem}.json **Implementation:** .. code-block:: python # Version Source class RubyGemsVersionSource(VersionSource): def get_latest_version(self, identifier: str, release_type: str) -> str: url = f"https://rubygems.org/api/v1/versions/{identifier}.json" response = requests.get(url) versions = response.json() # Find latest stable version # Spec Manager class Gem2RpmManager(SpecManager): def update_spec(self, repo_path, branch, package_name, version, dry_run): cmd = ["gem2rpm", f"{package_name}-{version}.gem", "--output", f"{package_name}.spec"] # Run gem2rpm command Quick Start: Adding R Package Support -------------------------------------- Would you like me to implement R package support (CRAN + r2spec) as a working example? The steps would be: 1. Create ``src/dist_git_manager/version_sources/cran.py`` 2. Create ``src/dist_git_manager/spec_managers/r2spec.py`` 3. Register both in ``core/manager.py`` 4. Add to CLI choices 5. Create tests 6. Update documentation This would demonstrate the complete process and make it easy to add the others following the same pattern. Testing New Plugins -------------------- When developing a new plugin: 1. **Unit tests**: Mock all external dependencies (HTTP, subprocess) 2. **Integration tests**: Test with real git repos in tempdir 3. **Manual testing**: Use ``--dry-run`` first Example test structure: .. code-block:: python # tests/unit/test_new_source.py import responses import pytest from dist_git_manager.version_sources.new_source import NewVersionSource @responses.activate def test_fetch_version(): responses.add( responses.GET, "https://api.xyz-registry.org/packages/test/versions", json={"versions": ["1.0.0", "2.0.0"]}, status=200, ) source = NewVersionSource() version = source.get_latest_version("test", "semver") assert version == "2.0.0" See Also -------- - :doc:`architecture` - Understanding the plugin architecture - :doc:`testing` - Testing guidelines - Existing plugin implementations in ``src/dist_git_manager/version_sources/`` and ``src/dist_git_manager/spec_managers/``