""" sbuild wrapper for deb-mock """ import os import subprocess import tempfile import shutil from pathlib import Path from typing import List, Dict, Any, Optional from .exceptions import SbuildError class SbuildWrapper: """Wrapper around sbuild for standardized package building""" def __init__(self, config): self.config = config def build_package(self, source_package: str, chroot_name: str = None, output_dir: str = None, **kwargs) -> Dict[str, Any]: """Build a Debian source package using sbuild""" if chroot_name is None: chroot_name = self.config.chroot_name if output_dir is None: output_dir = self.config.get_output_path() # Ensure output directory exists os.makedirs(output_dir, exist_ok=True) # Prepare sbuild command cmd = self._prepare_sbuild_command(source_package, chroot_name, output_dir, **kwargs) # Create temporary log file with tempfile.NamedTemporaryFile(mode='w', suffix='.log', delete=False) as log_file: log_path = log_file.name try: # Execute sbuild result = self._execute_sbuild(cmd, log_path) # Parse build results build_info = self._parse_build_results(output_dir, log_path, result) return build_info finally: # Clean up temporary log file if os.path.exists(log_path): os.unlink(log_path) def _prepare_sbuild_command(self, source_package: str, chroot_name: str, output_dir: str, **kwargs) -> List[str]: """Prepare the sbuild command with all necessary options""" cmd = ['sbuild'] # Basic options cmd.extend(['--chroot', chroot_name]) cmd.extend(['--dist', self.config.suite]) cmd.extend(['--arch', self.config.architecture]) # Output options cmd.extend(['--build-dir', output_dir]) # Logging options cmd.extend(['--log-dir', self.config.sbuild_log_dir]) # Build options if kwargs.get('verbose', self.config.verbose): cmd.append('--verbose') if kwargs.get('debug', self.config.debug): cmd.append('--debug') # Additional build options from config for option in self.config.build_options: cmd.extend(option.split()) # Custom build options if kwargs.get('build_options'): for option in kwargs['build_options']: cmd.extend(option.split()) # Environment variables for key, value in self.config.build_env.items(): cmd.extend(['--env', f'{key}={value}']) # Custom environment variables if kwargs.get('build_env'): for key, value in kwargs['build_env'].items(): cmd.extend(['--env', f'{key}={value}']) # Source package cmd.append(source_package) return cmd def _execute_sbuild(self, cmd: List[str], log_path: str) -> subprocess.CompletedProcess: """Execute sbuild command""" try: # Redirect output to log file with open(log_path, 'w') as log_file: result = subprocess.run( cmd, stdout=log_file, stderr=subprocess.STDOUT, text=True, check=True ) return result except subprocess.CalledProcessError as e: # Read log file for error details with open(log_path, 'r') as log_file: log_content = log_file.read() raise SbuildError(f"sbuild failed: {e}\nLog output:\n{log_content}") except FileNotFoundError: raise SbuildError("sbuild not found. Please install sbuild package.") def _parse_build_results(self, output_dir: str, log_path: str, result: subprocess.CompletedProcess) -> Dict[str, Any]: """Parse build results and collect artifacts""" build_info = { 'success': True, 'output_dir': output_dir, 'log_file': log_path, 'artifacts': [], 'metadata': {} } # Collect build artifacts artifacts = self._collect_artifacts(output_dir) build_info['artifacts'] = artifacts # Parse build metadata metadata = self._parse_build_metadata(log_path, output_dir) build_info['metadata'] = metadata return build_info def _collect_artifacts(self, output_dir: str) -> List[str]: """Collect build artifacts from output directory""" artifacts = [] if not os.path.exists(output_dir): return artifacts # Look for .deb files for deb_file in Path(output_dir).glob("*.deb"): artifacts.append(str(deb_file)) # Look for .changes files for changes_file in Path(output_dir).glob("*.changes"): artifacts.append(str(changes_file)) # Look for .buildinfo files for buildinfo_file in Path(output_dir).glob("*.buildinfo"): artifacts.append(str(buildinfo_file)) return artifacts def _parse_build_metadata(self, log_path: str, output_dir: str) -> Dict[str, Any]: """Parse build metadata from log and artifacts""" metadata = { 'build_time': None, 'package_name': None, 'package_version': None, 'architecture': self.config.architecture, 'suite': self.config.suite, 'chroot': self.config.chroot_name, 'dependencies': [], 'build_dependencies': [] } # Parse log file for metadata if os.path.exists(log_path): with open(log_path, 'r') as log_file: log_content = log_file.read() metadata.update(self._extract_metadata_from_log(log_content)) # Parse .changes file for additional metadata changes_files = list(Path(output_dir).glob("*.changes")) if changes_files: metadata.update(self._parse_changes_file(changes_files[0])) return metadata def _extract_metadata_from_log(self, log_content: str) -> Dict[str, Any]: """Extract metadata from sbuild log content""" metadata = {} # Extract build time import re time_match = re.search(r'Build started at (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', log_content) if time_match: metadata['build_time'] = time_match.group(1) # Extract package name and version package_match = re.search(r'Building (\S+) \((\S+)\)', log_content) if package_match: metadata['package_name'] = package_match.group(1) metadata['package_version'] = package_match.group(2) return metadata def _parse_changes_file(self, changes_file: Path) -> Dict[str, Any]: """Parse .changes file for metadata""" metadata = {} try: with open(changes_file, 'r') as f: content = f.read() lines = content.split('\n') for line in lines: if line.startswith('Source:'): metadata['source_package'] = line.split(':', 1)[1].strip() elif line.startswith('Version:'): metadata['source_version'] = line.split(':', 1)[1].strip() elif line.startswith('Architecture:'): metadata['architectures'] = line.split(':', 1)[1].strip().split() except Exception: pass return metadata def check_dependencies(self, source_package: str, chroot_name: str = None) -> Dict[str, Any]: """Check build dependencies for a source package""" if chroot_name is None: chroot_name = self.config.chroot_name # Use dpkg-checkbuilddeps to check dependencies cmd = ['schroot', '-c', chroot_name, '--', 'dpkg-checkbuilddeps'] try: result = subprocess.run(cmd, capture_output=True, text=True, check=True) return { 'satisfied': True, 'missing': [], 'conflicts': [] } except subprocess.CalledProcessError as e: # Parse missing dependencies from error output missing = self._parse_missing_dependencies(e.stderr) return { 'satisfied': False, 'missing': missing, 'conflicts': [] } def _parse_missing_dependencies(self, stderr: str) -> List[str]: """Parse missing dependencies from dpkg-checkbuilddeps output""" missing = [] for line in stderr.split('\n'): if 'Unmet build dependencies:' in line: # Extract package names from the line import re packages = re.findall(r'\b[a-zA-Z0-9][a-zA-Z0-9+\-\.]*\b', line) missing.extend(packages) return missing def install_build_dependencies(self, dependencies: List[str], chroot_name: str = None) -> None: """Install build dependencies in the chroot""" if chroot_name is None: chroot_name = self.config.chroot_name if not dependencies: return cmd = ['schroot', '-c', chroot_name, '--', 'apt-get', 'install', '-y'] + dependencies try: subprocess.run(cmd, check=True) except subprocess.CalledProcessError as e: raise SbuildError(f"Failed to install build dependencies: {e}")