particle-os-schema/debian_package_build_system.py
2025-08-26 10:13:49 -07:00

537 lines
20 KiB
Python

#!/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()