Skip to content

wheel_builder

๐Ÿค– AI-Generated Content

This documentation was generated with AI assistance and is still being audited. Some, or potentially a lot, of this information may be inaccurate. Learn more.

flavor.packaging.python.wheel_builder

Wheel building and dependency resolution for FlavorPack packaging.

This module provides wheel building with complex dependency resolution logic, combining UV performance where appropriate with PyPA pip compatibility.

Classes

WheelBuilder

WheelBuilder(python_version: str = '3.11')

Wheel builder with sophisticated dependency resolution.

Combines the speed of UV with the reliability of PyPA pip for complex Python package building scenarios.

This class handles: - Source package wheel building - Complex dependency resolution - Cross-platform wheel selection - Proper manylinux compatibility

Initialize the wheel builder.

Parameters:

Name Type Description Default
python_version str

Target Python version for wheel building

'3.11'
Source code in flavor/packaging/python/wheel_builder.py
def __init__(self, python_version: str = "3.11") -> None:
    """
    Initialize the wheel builder.

    Args:
        python_version: Target Python version for wheel building
    """
    self.python_version = python_version

    # Initialize managers
    self.pypapip = PyPaPipManager(python_version=python_version)
    self.uv = UVManager()  # UV manager for performance where appropriate

    logger.debug(f"Initialized WheelBuilder for Python {python_version}")
Functions
build_and_resolve_project
build_and_resolve_project(
    python_exe: Path,
    project_dir: Path,
    build_dir: Path,
    requirements_file: Path | None = None,
    extra_packages: list[str] | None = None,
) -> dict[str, Any]

Complete wheel building and dependency resolution for a project.

CRITICAL: The PROJECT wheel is ALWAYS built from LOCAL SOURCE. Runtime dependencies are resolved and downloaded from PyPI as normal. This ensures the packaged project is never downloaded from PyPI.

Parameters:

Name Type Description Default
python_exe Path

Python executable to use

required
project_dir Path

Project source directory

required
build_dir Path

Directory for build artifacts

required
requirements_file Path | None

Optional requirements file

None
extra_packages list[str] | None

Additional packages to include

None

Returns:

Type Description
dict[str, Any]

Dictionary with build information and file paths

Source code in flavor/packaging/python/wheel_builder.py
def build_and_resolve_project(
    self,
    python_exe: Path,
    project_dir: Path,
    build_dir: Path,
    requirements_file: Path | None = None,
    extra_packages: list[str] | None = None,
) -> dict[str, Any]:
    """
    Complete wheel building and dependency resolution for a project.

    CRITICAL: The PROJECT wheel is ALWAYS built from LOCAL SOURCE.
    Runtime dependencies are resolved and downloaded from PyPI as normal.
    This ensures the packaged project is never downloaded from PyPI.

    Args:
        python_exe: Python executable to use
        project_dir: Project source directory
        build_dir: Directory for build artifacts
        requirements_file: Optional requirements file
        extra_packages: Additional packages to include

    Returns:
        Dictionary with build information and file paths
    """
    logger.info("(PROJECT from LOCAL SOURCE, dependencies from PyPI)")

    # Create build directories
    wheel_dir = build_dir / "wheels"
    deps_dir = build_dir / "deps"
    ensure_dir(wheel_dir)
    ensure_dir(deps_dir)

    # Build main project wheel FROM LOCAL SOURCE (never from PyPI)
    # Phase 39: Use no isolation to avoid DNS/network issues in CI (setuptools is now a runtime dep)
    logger.info("๐Ÿ”จ Building PROJECT wheel from LOCAL SOURCE")
    project_wheel = self.build_wheel_from_source(python_exe, project_dir, wheel_dir, use_isolation=False)

    # Extract project dependencies from pyproject.toml
    project_dependencies = []
    pyproject_path = project_dir / "pyproject.toml"
    if pyproject_path.exists() and not requirements_file:
        import tomllib

        try:
            with pyproject_path.open("rb") as f:
                pyproject_data = tomllib.load(f)
            project_dependencies = pyproject_data.get("project", {}).get("dependencies", [])
            if project_dependencies:
                logger.info(
                    "๐Ÿ“ฆ Found project dependencies in pyproject.toml",
                    count=len(project_dependencies),
                )
                logger.debug("Project dependencies", deps=project_dependencies)
        except Exception as e:
            logger.warning(f"Could not extract dependencies from pyproject.toml: {e}")

    # Combine all packages to resolve
    all_packages = list(extra_packages or [])
    if project_dependencies:
        all_packages.extend(project_dependencies)

    # Resolve and download dependency wheels from PyPI
    dependency_wheels = []
    if requirements_file or all_packages:
        logger.info(
            f"๐ŸŒ Resolving {len(all_packages)} runtime dependencies from PyPI "
            "(only runtime deps, not the project itself)"
        )
        locked_requirements = self.resolve_dependencies(
            python_exe=python_exe,
            requirements_file=requirements_file,
            packages=all_packages if all_packages else None,
            output_dir=deps_dir,
        )

        # Download dependency wheels
        dependency_wheels = self.download_wheels_for_resolved_deps(
            python_exe, locked_requirements, wheel_dir
        )
    else:
        locked_requirements = None

    build_info = {
        "project_wheel": project_wheel,
        "dependency_wheels": dependency_wheels,
        "locked_requirements": locked_requirements,
        "wheel_dir": wheel_dir,
        "total_wheels": len(dependency_wheels) + 1,  # +1 for project wheel
    }

    logger.info("(project from local source + dependencies from PyPI)")
    return build_info
build_wheel_from_source
build_wheel_from_source(
    python_exe: Path,
    source_path: Path,
    wheel_dir: Path,
    use_isolation: bool = True,
    build_options: dict[str, Any] | None = None,
) -> Path

Build wheel from Python source package.

Parameters:

Name Type Description Default
python_exe Path

Python executable to use

required
source_path Path

Path to source directory

required
wheel_dir Path

Directory to place built wheel

required
use_isolation bool

Whether to use build isolation

True
build_options dict[str, Any] | None

Additional build options

None

Returns:

Type Description
Path

Path to the built wheel file

Source code in flavor/packaging/python/wheel_builder.py
def build_wheel_from_source(
    self,
    python_exe: Path,
    source_path: Path,
    wheel_dir: Path,
    use_isolation: bool = True,
    build_options: dict[str, Any] | None = None,
) -> Path:
    """
    Build wheel from Python source package.

    Args:
        python_exe: Python executable to use
        source_path: Path to source directory
        wheel_dir: Directory to place built wheel
        use_isolation: Whether to use build isolation
        build_options: Additional build options

    Returns:
        Path to the built wheel file
    """

    # Use PyPA pip for wheel building (more reliable than UV for complex builds)
    wheel_cmd = self.pypapip._get_pypapip_wheel_cmd(
        python_exe=python_exe,
        wheel_dir=wheel_dir,
        source=source_path,
        no_deps=True,  # We handle deps separately
    )

    # Add build isolation flag if requested
    if not use_isolation:
        wheel_cmd.append("--no-build-isolation")

    # Add any custom build options
    if build_options:
        for option, value in build_options.items():
            if value is True:
                wheel_cmd.append(f"--{option}")
            elif value is not False and value is not None:
                wheel_cmd.extend([f"--{option}", str(value)])

    logger.debug("๐Ÿ’ป Building wheel", command=" ".join(wheel_cmd))
    result = run(wheel_cmd, check=True, capture_output=True)

    # Find the built wheel
    built_wheel = self._find_built_wheel(wheel_dir, source_path.name)

    if result.stdout:
        # Look for wheel filename in output
        for line in result.stdout.strip().split("\n"):
            if ".whl" in line:
                break

    return built_wheel
download_wheels_for_resolved_deps
download_wheels_for_resolved_deps(
    python_exe: Path,
    requirements_file: Path,
    wheel_dir: Path,
    use_uv_for_download: bool = False,
) -> list[Path]

Download wheels for resolved dependencies.

Parameters:

Name Type Description Default
python_exe Path

Python executable to use

required
requirements_file Path

Locked requirements file

required
wheel_dir Path

Directory to download wheels to

required
use_uv_for_download bool

Whether to use UV for downloading

False

Returns:

Type Description
list[Path]

List of downloaded wheel file paths

Source code in flavor/packaging/python/wheel_builder.py
def download_wheels_for_resolved_deps(
    self,
    python_exe: Path,
    requirements_file: Path,
    wheel_dir: Path,
    use_uv_for_download: bool = False,
) -> list[Path]:
    """
    Download wheels for resolved dependencies.

    Args:
        python_exe: Python executable to use
        requirements_file: Locked requirements file
        wheel_dir: Directory to download wheels to
        use_uv_for_download: Whether to use UV for downloading

    Returns:
        List of downloaded wheel file paths
    """
    logger.info("๐ŸŒ๐Ÿ“ฅ Downloading wheels for resolved dependencies")

    ensure_dir(wheel_dir)

    # Always use PyPA pip for wheel downloads to ensure manylinux compatibility
    # UV pip doesn't handle platform tags as reliably
    logger.debug("Using PyPA pip for reliable wheel downloads")

    try:
        self.pypapip.download_wheels_from_requirements(python_exe, requirements_file, wheel_dir)
    except RuntimeError as e:
        logger.error(f"โŒ Failed to download dependencies: {e}")
        raise

    # Return list of downloaded wheels
    wheel_files = list(wheel_dir.glob("*.whl"))

    # Validate we got at least some wheels
    if not wheel_files:
        error_msg = "No wheel files were downloaded - package would be broken"
        logger.error(error_msg)
        raise RuntimeError(error_msg)

    return wheel_files
resolve_dependencies
resolve_dependencies(
    python_exe: Path,
    requirements_file: Path | None = None,
    packages: list[str] | None = None,
    output_dir: Path | None = None,
    use_uv_for_resolution: bool = True,
) -> Path

Resolve dependencies and create a locked requirements file.

Parameters:

Name Type Description Default
python_exe Path

Python executable to use

required
requirements_file Path | None

Input requirements file

None
packages list[str] | None

List of packages to resolve

None
output_dir Path | None

Directory for output files

None
use_uv_for_resolution bool

Whether to use UV for fast resolution

True

Returns:

Type Description
Path

Path to locked requirements file

Source code in flavor/packaging/python/wheel_builder.py
def resolve_dependencies(
    self,
    python_exe: Path,
    requirements_file: Path | None = None,
    packages: list[str] | None = None,
    output_dir: Path | None = None,
    use_uv_for_resolution: bool = True,
) -> Path:
    """
    Resolve dependencies and create a locked requirements file.

    Args:
        python_exe: Python executable to use
        requirements_file: Input requirements file
        packages: List of packages to resolve
        output_dir: Directory for output files
        use_uv_for_resolution: Whether to use UV for fast resolution

    Returns:
        Path to locked requirements file
    """
    logger.info("๐Ÿ”๐Ÿ“ Resolving dependencies")

    if output_dir is None:
        output_dir = Path(tempfile.mkdtemp())

    # Create input requirements file if packages provided
    if packages and not requirements_file:
        requirements_file = output_dir / "requirements.in"
        with requirements_file.open("w") as f:
            for package in packages:
                f.write(f"{package}\n")

    if not requirements_file:
        raise ValueError("Either requirements_file or packages must be provided")

    # Create locked requirements file
    locked_requirements = output_dir / "requirements.txt"

    if use_uv_for_resolution:
        try:
            # Try UV pip-compile for speed
            logger.debug("Attempting UV pip-compile for fast resolution")
            self.uv.compile_requirements(requirements_file, locked_requirements, self.python_version)
            return locked_requirements
        except Exception as e:
            logger.warning(f"UV resolution failed, falling back to pip-tools: {e}")

    # Fallback to pip-tools approach
    logger.debug("Using pip-tools for dependency resolution")
    self._resolve_with_pip_tools(python_exe, requirements_file, locked_requirements)

    return locked_requirements