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

    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():

    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:

    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():

    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

    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():

    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:

# 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:

# 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:

# 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:

# 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:

# 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

  • Architecture - Understanding the plugin architecture

  • Testing - Testing guidelines

  • Existing plugin implementations in src/dist_git_manager/version_sources/ and src/dist_git_manager/spec_managers/