The current Bootstrapper class in src/fromager/bootstrapper.py violates the Single Responsibility Principle by combining 7 distinct responsibilities into a single 1500+ line class. This document outlines a decomposition strategy to improve maintainability, testability, and code organization.
- Monolithic Design: Single class handling version resolution, caching, builds, dependencies, error handling, and state tracking
- Testing Complexity: Difficult to unit test individual concerns in isolation
- Tight Coupling: Changes in one area (e.g., caching) can affect unrelated areas (e.g., version resolution)
- Poor Reusability: Individual components cannot be reused independently
- Maintenance Burden: Large class makes it difficult to understand and modify behavior
- Resolving package versions from PyPI, git URLs, and cached graphs
- Handling git repository cloning and version extraction
- Managing resolution caching and constraint satisfaction
- Extracting build dependencies (build-system, build-backend, build-sdist)
- Extracting install dependencies from wheels and sdists
- Managing dependency installation in build environments
- Downloading source archives and git repositories
- Unpacking and preparing source code for building
- Managing working directories and source preparation
- Coordinating wheel and sdist builds
- Managing build environments and build processes
- Handling cached wheel usage and build fallbacks
- Finding cached wheels across multiple locations
- Managing local, download, and remote cache hierarchies
- Validating build tags and extracting metadata
- Recording test mode failures with categorization
- Managing fallback strategies for failed builds
- Generating comprehensive failure reports
- Tracking seen requirements and build orders
- Managing dependency graph updates
- Maintaining progress and completion state
Responsibility: Resolve package versions from all sources
Public Interface:
class PackageVersionResolver:
def resolve_version(self, req: Requirement, req_type: RequirementType) -> tuple[str, Version]
def clear_resolution_cache(self) -> None
def get_cached_resolution(self, req: Requirement) -> tuple[str, Version] | NoneKey Methods:
resolve_source_version()- Handle PyPI and git URL resolutionresolve_prebuilt_version()- Handle wheel resolution from serversresolve_version_from_git_url()- Git cloning and version extractionget_version_from_package_metadata()- Extract version from package metadataresolve_from_previous_graph()- Use cached resolutions from dependency graphresolve_from_version_source()- Resolve using provided version candidates
Dependencies: WorkContext, DependencyGraph, sources module, wheels module, resolver module
Responsibility: Extract and manage all types of dependencies
Public Interface:
class PackageDependencyExtractor:
def get_build_dependencies(self, req: Requirement, sdist_root_dir: Path, build_env: BuildEnvironment) -> set[Requirement]
def get_install_dependencies(self, req: Requirement, resolved_version: Version, wheel_filename: Path | None, sdist_filename: Path | None, sdist_root_dir: Path | None, build_env: BuildEnvironment | None, unpack_dir: Path | None) -> list[Requirement]Key Methods:
get_build_system_dependencies()- Extract build system requirementsget_build_backend_dependencies()- Extract build backend requirementsget_build_sdist_dependencies()- Extract build sdist requirementsextract_from_wheel()- Extract install dependencies from wheelsextract_from_sdist()- Extract install dependencies from source
Dependencies: WorkContext, dependencies module, BuildEnvironment
Responsibility: Handle source code operations
Public Interface:
class PackageSourceManager:
def download_source(self, req: Requirement, resolved_version: Version, source_url: str) -> Path
def prepare_source(self, req: Requirement, source_filename: Path, version: Version) -> Path
def download_git_source(self, req: Requirement, url_to_clone: str, destination_dir: Path, ref: str | None) -> NoneKey Methods:
download_source_archive()- Download source archives from URLsprepare_source_directory()- Unpack and prepare source for buildingclone_git_repository()- Clone git repositories with specific refsget_source_type()- Determine source type (git, archive, etc.)create_unpack_directory()- Create working directories for source
Dependencies: WorkContext, sources module
Responsibility: Coordinate builds and manage build environments
Public Interface:
class PackageBuildOrchestrator:
def build_package(self, req: Requirement, resolved_version: Version, sdist_root_dir: Path, build_sdist_only: bool, cached_wheel_filename: Path | None) -> tuple[Path | None, Path | None]
def create_build_environment(self, req: Requirement, resolved_version: Version, parent_dir: Path) -> BuildEnvironmentKey Methods:
build_wheel()- Build wheels from prepared sourcebuild_sdist()- Build source distributionsuse_cached_wheel()- Use existing cached wheelsupdate_wheel_mirror()- Update local wheel mirrorsclean_build_directories()- Clean up build artifacts
Dependencies: WorkContext, build_environment module, wheels module, server module
Responsibility: Handle all caching operations
Public Interface:
class PackageCacheManager:
def find_cached_wheel(self, req: Requirement, resolved_version: Version) -> tuple[Path | None, Path | None]
def download_prebuilt_wheel(self, req: Requirement, req_type: RequirementType, resolved_version: Version, wheel_url: str) -> tuple[Path, Path]Key Methods:
look_for_existing_wheel()- Search local directories for cached wheelsdownload_wheel_from_cache_server()- Download from remote cache serversvalidate_build_tag()- Ensure build tags match expectationsunpack_metadata_from_wheel()- Extract metadata and requirementsextract_build_requirements_from_wheel()- Extract fromager build requirements
Dependencies: WorkContext, wheels module, finders module, server module
Responsibility: Handle test mode failures and fallback strategies
Public Interface:
class TestModeErrorHandler:
def record_failure(self, req: Requirement, version: str | None, err: Exception, failure_type: FailureType, log_level: Literal["error", "warning"] = "error") -> None
def handle_build_failure(self, req: Requirement, resolved_version: Version, req_type: RequirementType, build_error: Exception) -> SourceBuildResult | None
def generate_failure_report(self) -> dict[str, list[FailureRecord]]
def write_failure_report_to_file(self, work_dir: Path) -> PathKey Methods:
attempt_prebuilt_fallback()- Try prebuilt wheels when source builds faillog_failure_message()- Log formatted failure messages with contextget_failed_packages()- Get list of all recorded failuresclear_failures()- Reset failure tracking statehas_failures()- Check if any failures have been recorded
Dependencies: PackageVersionResolver, PackageCacheManager (for fallback)
Responsibility: Track bootstrap state and progress
Public Interface:
class BootstrapStateTracker:
def mark_as_seen(self, req: Requirement, version: Version, sdist_only: bool = False) -> None
def has_been_seen(self, req: Requirement, version: Version, sdist_only: bool = False) -> bool
def add_to_build_order(self, req: Requirement, version: Version, source_url: str, source_type: SourceType, prebuilt: bool = False, constraint: Requirement | None = None) -> None
def get_build_order(self) -> list[dict[str, Any]]
def write_build_order_to_file(self) -> NoneKey Methods:
create_resolved_key()- Generate unique keys for requirement trackingis_already_in_build_order()- Check if package already in build queueadd_to_dependency_graph()- Update dependency graph with new edgessort_requirements()- Sort requirements for deterministic processing
Dependencies: WorkContext, DependencyGraph
Responsibility: Orchestrate the bootstrap process using service classes
Public Interface:
class PackageBootstrapper:
def resolve_and_add_top_level(self, req: Requirement) -> tuple[str, Version] | None
def bootstrap(self, req: Requirement, req_type: RequirementType) -> None
def finalize(self) -> intKey Methods:
bootstrap_implementation()- Core bootstrap logic using serviceshandle_build_requirements()- Process build dependencies recursivelytrack_dependency_chain()- Context manager for dependency trackingprocessing_build_requirement()- Determine if processing build deps
Dependencies: All service classes listed above
Risk Level: Low - These classes encapsulate well-defined functionality
-
PackageVersionResolver - Extract version resolution logic
- Move methods:
resolve_version(),_resolve_source_with_history(), etc. - Minimal external dependencies
- Easy to unit test in isolation
- Move methods:
-
PackageCacheManager - Extract caching operations
- Move methods:
_find_cached_wheel(),_look_for_existing_wheel(), etc. - Clear separation from other concerns
- Straightforward to test with mock file systems
- Move methods:
-
PackageSourceManager - Extract source management
- Move methods:
_download_source(),_prepare_source(), etc. - Well-defined input/output contracts
- Can be tested with temporary directories
- Move methods:
-
TestModeErrorHandler - Extract error handling/reporting
- Move methods:
_record_test_mode_failure(),_handle_test_mode_failure() - Pure data management with clear interfaces
- Easy to test error scenarios
- Move methods:
Risk Level: Medium - These classes have more complex interactions
-
PackageDependencyExtractor - Extract dependency management
- Move methods:
_prepare_build_dependencies(),_get_install_dependencies() - Requires coordination with build environments
- Needs careful testing of dependency resolution
- Move methods:
-
PackageBuildOrchestrator - Extract build coordination
- Move methods:
_do_build(),_build_wheel(),_build_sdist() - Complex interactions with build environments
- Requires integration testing with actual builds
- Move methods:
-
BootstrapStateTracker - Extract state management
- Move methods:
_mark_as_seen(),_add_to_build_order(), etc. - Manages critical bootstrap state
- Needs careful testing of state consistency
- Move methods:
Risk Level: Higher - Changes main orchestration logic
-
PackageBootstrapper Orchestrator - Refactor main class
- Rewrite
bootstrap()and_bootstrap_impl()to use services - Update dependency injection and service coordination
- Comprehensive integration testing required
- Rewrite
-
Update Imports and Callers - System-wide changes
- Update all imports of
Bootstrapperclass - Ensure backward compatibility during transition
- Update command-line interface and entry points
- Update all imports of
-
Add Integration Tests - Verify end-to-end behavior
- Test complete bootstrap workflows
- Verify test mode functionality
- Performance regression testing
- Unit Tests for Each Service: Write comprehensive unit tests for each extracted class before integration
- Mock Dependencies: Use dependency injection and mocking to isolate components
- File-Scoped Testing: Run
hatch run test:test tests/test_<module>.pyafter each extraction - Integration Testing: Maintain end-to-end tests to verify overall behavior
- Gradual Migration: Keep original
Bootstrapperclass during transition - Facade Pattern: Provide compatibility wrapper if needed
- Feature Flags: Use configuration to enable new implementation gradually
- Rollback Plan: Maintain ability to revert to original implementation
- Code Reviews: Each extracted class requires thorough review
- Type Checking: Run
hatch run mypy:checkon all new classes - Lint Checks: Ensure
hatch run lint:fixpasses on all new code - Performance Testing: Verify no performance regressions
- Single Responsibility: Each class has one clear purpose
- Easier Debugging: Problems isolated to specific components
- Simpler Code Reviews: Smaller, focused changes
- Unit Testing: Each component can be tested independently
- Mock Dependencies: Clear interfaces enable effective mocking
- Faster Test Cycles: Test only relevant components during development
- Component Extraction: Services can be reused in other contexts
- Dependency Injection: Easy to substitute implementations
- Plugin Architecture: Services can be extended or replaced
- Reduced Complexity: Smaller classes are easier to understand
- Better Encapsulation: Clear boundaries between concerns
- Improved Documentation: Each class has focused documentation
- Create service class skeletons
- Set up dependency injection framework
- Write initial unit tests
- Extract
PackageVersionResolver - Extract
PackageCacheManager - Extract
PackageSourceManager - Extract
TestModeErrorHandler
- Extract
PackageDependencyExtractor - Extract
PackageBuildOrchestrator - Extract
BootstrapStateTracker
- Refactor
PackageBootstrapperorchestrator - Update all imports and callers
- Comprehensive integration testing
- Performance testing and optimization
- Documentation updates
- Remove deprecated code
- Functional Equivalence: All existing functionality preserved
- Test Coverage: Maintain or improve test coverage
- Performance: No significant performance degradation
- Code Quality: Improved maintainability metrics
- Documentation: Clear documentation for each service class
This refactoring will transform a monolithic 1500+ line class into 8 focused, testable, and maintainable components. The gradual migration strategy minimizes risk while delivering immediate benefits in code organization and testability.
The investment in proper decomposition will pay dividends in reduced maintenance burden, faster feature development, and improved code quality for the Fromager project.