537 lines
20 KiB
Python
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()
|