initial commit
This commit is contained in:
commit
74fe9143d9
43 changed files with 10069 additions and 0 deletions
280
deb_mock/sbuild.py
Normal file
280
deb_mock/sbuild.py
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
"""
|
||||
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}")
|
||||
Loading…
Add table
Add a link
Reference in a new issue