#!/usr/bin/env python3 """ Debian Package Build System Integration This module integrates with Debian package building tools like sbuild and pbuilder, providing package building, validation, and testing capabilities. """ import json import os import subprocess import tempfile import shutil from typing import Dict, List, Optional, Any, Tuple from dataclasses import dataclass, asdict from pathlib import Path import logging from datetime import datetime @dataclass class BuildEnvironment: """Represents a Debian build environment""" name: str suite: str architecture: str mirror: str components: List[str] extra_repositories: List[str] build_dependencies: List[str] enabled: bool = True @dataclass class BuildResult: """Represents the result of a package build""" package_name: str version: str architecture: str suite: str build_status: str build_log: str artifacts: List[str] build_time: float dependencies_resolved: bool tests_passed: bool timestamp: datetime class DebianPackageBuildSystem: """Integrates with Debian package building tools""" def __init__(self, config_dir: str = "./config/build-system"): self.config_dir = Path(config_dir) self.config_dir.mkdir(parents=True, exist_ok=True) self.build_logs_dir = self.config_dir / "build-logs" self.build_logs_dir.mkdir(exist_ok=True) self.artifacts_dir = self.config_dir / "artifacts" self.artifacts_dir.mkdir(exist_ok=True) self.logger = self._setup_logging() self._load_configuration() def _setup_logging(self) -> logging.Logger: """Setup logging for build system""" logger = logging.getLogger('debian-build-system') logger.setLevel(logging.INFO) if not logger.handlers: handler = logging.FileHandler(self.config_dir / "build-system.log") formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) handler.setFormatter(formatter) logger.addHandler(handler) return logger def _load_configuration(self): """Load build system configuration""" config_file = self.config_dir / "build-environments.json" if config_file.exists(): with open(config_file, 'r') as f: self.build_environments = json.load(f) else: self.build_environments = self._get_default_environments() self._save_configuration() def _get_default_environments(self) -> Dict[str, Any]: """Get default build environment configurations""" return { "environments": [ { "name": "bookworm-amd64", "suite": "bookworm", "architecture": "amd64", "mirror": "http://deb.debian.org/debian", "components": ["main", "contrib", "non-free-firmware"], "extra_repositories": [], "build_dependencies": ["build-essential", "devscripts", "debhelper"], "enabled": True }, { "name": "sid-amd64", "suite": "sid", "architecture": "amd64", "mirror": "http://deb.debian.org/debian", "components": ["main", "contrib", "non-free-firmware"], "extra_repositories": [], "build_dependencies": ["build-essential", "devscripts", "debhelper"], "enabled": True } ] } def _save_configuration(self): """Save build system configuration""" config_file = self.config_dir / "build-environments.json" with open(config_file, 'w') as f: json.dump(self.build_environments, f, indent=2) def check_build_tools(self) -> Dict[str, bool]: """Check availability of Debian build tools""" tools = { 'sbuild': self._check_command('sbuild'), 'pbuilder': self._check_command('pbuilder'), 'dpkg-buildpackage': self._check_command('dpkg-buildpackage'), 'debuild': self._check_command('debuild'), 'apt-get': self._check_command('apt-get'), 'schroot': self._check_command('schroot') } return tools def _check_command(self, command: str) -> bool: """Check if a command is available""" try: result = subprocess.run( ['which', command], capture_output=True, text=True ) return result.returncode == 0 except Exception: return False def setup_build_environment(self, environment_name: str) -> bool: """Setup a build environment using sbuild or pbuilder""" try: env_config = self._get_environment_config(environment_name) if not env_config: self.logger.error(f"Environment {environment_name} not found") return False # Check if environment already exists if self._environment_exists(environment_name): self.logger.info(f"Environment {environment_name} already exists") return True # Create environment if self._create_sbuild_environment(env_config): self.logger.info(f"Successfully created sbuild environment {environment_name}") return True elif self._create_pbuilder_environment(env_config): self.logger.info(f"Successfully created pbuilder environment {environment_name}") return True else: self.logger.error(f"Failed to create build environment {environment_name}") return False except Exception as e: self.logger.error(f"Environment setup failed: {e}") return False def _get_environment_config(self, name: str) -> Optional[Dict[str, Any]]: """Get environment configuration by name""" for env in self.build_environments["environments"]: if env["name"] == name: return env return None def _environment_exists(self, name: str) -> bool: """Check if build environment exists""" # Check sbuild try: result = subprocess.run( ['schroot', '-l'], capture_output=True, text=True ) return name in result.stdout except Exception: pass # Check pbuilder pbuilder_dir = Path(f"/var/cache/pbuilder/{name}") return pbuilder_dir.exists() def _create_sbuild_environment(self, config: Dict[str, Any]) -> bool: """Create sbuild environment""" try: # Create schroot configuration schroot_conf = f""" [{config['name']}] description=Debian {config['suite']} {config['architecture']} build environment directory=/var/chroot/{config['name']} root-users=root users=buildd type=directory profile=sbuild """ schroot_conf_file = Path(f"/etc/schroot/chroot.d/{config['name']}") schroot_conf_file.write_text(schroot_conf) # Create chroot directory chroot_dir = Path(f"/var/chroot/{config['name']}") chroot_dir.mkdir(parents=True, exist_ok=True) # Bootstrap the chroot cmd = [ 'debootstrap', '--arch', config['architecture'], '--variant=buildd', config['suite'], str(chroot_dir), config['mirror'] ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: self.logger.error(f"Debootstrap failed: {result.stderr}") return False # Install build dependencies self._install_build_dependencies(config, chroot_dir) return True except Exception as e: self.logger.error(f"Sbuild environment creation failed: {e}") return False def _create_pbuilder_environment(self, config: Dict[str, Any]) -> bool: """Create pbuilder environment""" try: # Create pbuilder configuration pbuilder_conf = f""" DISTRIBUTION={config['suite']} ARCHITECTURE={config['architecture']} MIRRORSITE={config['mirror']} COMPONENTS="{' '.join(config['components'])}" OTHERMIRROR="{' '.join(config['extra_repositories'])}" BUILDRESULT=/var/cache/pbuilder/result APTCACHEHARDLINK=yes USEPROC=yes USEDEVPTS=yes USEDEVFS=no BUILDPLACE=/var/cache/pbuilder/build """ pbuilder_conf_file = Path(f"/etc/pbuilderrc-{config['name']}") pbuilder_conf_file.write_text(pbuilder_conf) # Create pbuilder environment cmd = [ 'pbuilder', '--create', '--configfile', str(pbuilder_conf_file), '--basetgz', f"/var/cache/pbuilder/{config['name']}-base.tgz" ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: self.logger.error(f"Pbuilder creation failed: {result.stderr}") return False return True except Exception as e: self.logger.error(f"Pbuilder environment creation failed: {e}") return False def _install_build_dependencies(self, config: Dict[str, Any], chroot_dir: Path): """Install build dependencies in chroot""" try: # Mount proc and dev subprocess.run(['mount', '--bind', '/proc', f"{chroot_dir}/proc"]) subprocess.run(['mount', '--bind', '/dev', f"{chroot_dir}/dev"]) # Update package lists cmd = ['chroot', str(chroot_dir), 'apt-get', 'update'] subprocess.run(cmd, capture_output=True, text=True) # Install build dependencies if config['build_dependencies']: cmd = ['chroot', str(chroot_dir), 'apt-get', 'install', '-y'] + config['build_dependencies'] subprocess.run(cmd, capture_output=True, text=True) # Unmount subprocess.run(['umount', f"{chroot_dir}/proc"]) subprocess.run(['umount', f"{chroot_dir}/dev"]) except Exception as e: self.logger.error(f"Failed to install build dependencies: {e}") def build_package(self, package_source: str, environment_name: str, build_options: Dict[str, Any] = None) -> BuildResult: """Build a Debian package""" start_time = datetime.now() try: env_config = self._get_environment_config(environment_name) if not env_config: raise ValueError(f"Environment {environment_name} not found") # Setup build environment if needed if not self._environment_exists(environment_name): if not self.setup_build_environment(environment_name): raise RuntimeError(f"Failed to setup build environment {environment_name}") # Build package build_log = self._build_package_internal(package_source, env_config, build_options) # Collect artifacts artifacts = self._collect_build_artifacts(package_source, environment_name) # Run tests if available tests_passed = self._run_package_tests(package_source, environment_name) build_time = (datetime.now() - start_time).total_seconds() return BuildResult( package_name=self._extract_package_name(package_source), version=self._extract_package_version(package_source), architecture=env_config['architecture'], suite=env_config['suite'], build_status='success', build_log=build_log, artifacts=artifacts, build_time=build_time, dependencies_resolved=True, tests_passed=tests_passed, timestamp=datetime.now() ) except Exception as e: self.logger.error(f"Package build failed: {e}") build_time = (datetime.now() - start_time).total_seconds() return BuildResult( package_name=self._extract_package_name(package_source), version='unknown', architecture='unknown', suite='unknown', build_status='failed', build_log=str(e), artifacts=[], build_time=build_time, dependencies_resolved=False, tests_passed=False, timestamp=datetime.now() ) def _build_package_internal(self, package_source: str, env_config: Dict[str, Any], build_options: Dict[str, Any]) -> str: """Internal package building logic""" try: # Try sbuild first if self._check_command('sbuild'): return self._build_with_sbuild(package_source, env_config, build_options) # Fall back to pbuilder elif self._check_command('pbuilder'): return self._build_with_pbuilder(package_source, env_config, build_options) else: raise RuntimeError("No build tools available") except Exception as e: self.logger.error(f"Build failed: {e}") raise def _build_with_sbuild(self, package_source: str, env_config: Dict[str, Any], build_options: Dict[str, Any]) -> str: """Build package using sbuild""" try: cmd = [ 'sbuild', '--dist', env_config['suite'], '--arch', env_config['architecture'], '--chroot', env_config['name'] ] if build_options and build_options.get('verbose'): cmd.append('--verbose') cmd.append(package_source) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"Sbuild failed: {result.stderr}") return result.stdout except Exception as e: self.logger.error(f"Sbuild build failed: {e}") raise def _build_with_pbuilder(self, package_source: str, env_config: Dict[str, Any], build_options: Dict[str, Any]) -> str: """Build package using pbuilder""" try: cmd = [ 'pbuilder', '--build', '--configfile', f"/etc/pbuilderrc-{env_config['name']}", '--basetgz', f"/var/cache/pbuilder/{env_config['name']}-base.tgz", '--buildresult', f"/var/cache/pbuilder/result" ] if build_options and build_options.get('verbose'): cmd.append('--verbose') cmd.append(package_source) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"Pbuilder failed: {result.stderr}") return result.stdout except Exception as e: self.logger.error(f"Pbuilder build failed: {e}") raise def _collect_build_artifacts(self, package_source: str, environment_name: str) -> List[str]: """Collect build artifacts""" artifacts = [] try: # Look for .deb files package_name = self._extract_package_name(package_source) deb_files = list(Path('.').glob(f"{package_name}*.deb")) artifacts.extend([str(f) for f in deb_files]) # Look for source packages dsc_files = list(Path('.').glob(f"{package_name}*.dsc")) artifacts.extend([str(f) for f in dsc_files]) # Look for build logs log_files = list(Path('.').glob(f"{package_name}*.build")) artifacts.extend([str(f) for f in log_files]) except Exception as e: self.logger.error(f"Failed to collect artifacts: {e}") return artifacts def _run_package_tests(self, package_source: str, environment_name: str) -> bool: """Run package tests if available""" try: # Look for test suite test_dir = Path(package_source) / "debian" / "tests" if not test_dir.exists(): return True # No tests to run # Run tests using autopkgtest if available if self._check_command('autopkgtest'): cmd = ['autopkgtest', package_source, '--', 'schroot', environment_name] result = subprocess.run(cmd, capture_output=True, text=True) return result.returncode == 0 return True except Exception as e: self.logger.error(f"Test execution failed: {e}") return False def _extract_package_name(self, package_source: str) -> str: """Extract package name from source""" try: # Try to read debian/control control_file = Path(package_source) / "debian" / "control" if control_file.exists(): with open(control_file, 'r') as f: for line in f: if line.startswith('Package:'): return line.split(':', 1)[1].strip() # Fallback to directory name return Path(package_source).name except Exception: return Path(package_source).name def _extract_package_version(self, package_source: str) -> str: """Extract package version from source""" try: # Try to read debian/changelog changelog_file = Path(package_source) / "debian" / "changelog" if changelog_file.exists(): with open(changelog_file, 'r') as f: first_line = f.readline().strip() if '(' in first_line and ')' in first_line: version_part = first_line.split('(')[1].split(')')[0] return version_part return 'unknown' except Exception: return 'unknown' def list_build_environments(self) -> List[Dict[str, Any]]: """List available build environments""" return self.build_environments["environments"] def get_build_environment(self, name: str) -> Optional[Dict[str, Any]]: """Get build environment configuration""" return self._get_environment_config(name) def main(): """Test build system integration""" build_system = DebianPackageBuildSystem() # Check available tools tools = build_system.check_build_tools() print("Available build tools:") for tool, available in tools.items(): status = "✓" if available else "✗" print(f" {status} {tool}") # List build environments environments = build_system.list_build_environments() print(f"\nBuild environments: {len(environments)}") for env in environments: print(f" - {env['name']}: {env['suite']}/{env['architecture']}") if __name__ == "__main__": main()