- Fixed all linting issues (unused imports, whitespace, f-string issues) - Implemented robust sbuild integration with proper environment handling - Added fallback directory creation for output and metadata paths - Fixed test dependencies in debian/control (python3-pytest, python3-yaml) - Corrected package naming and entry points in setup.py and debian/rules - Successfully built and tested both simple (hello) and complex (wget) packages - Verified mock CLI works correctly with pipx installation - Added comprehensive test suite with 30 passing tests - Implemented proper chroot management and sbuild integration Key achievements: - Mock can build itself (self-hosting capability) - Successfully built hello package (3.1KB .deb) - Successfully built wget package (936KB .deb) with complex dependencies - All packages install and function correctly - Ready for real-world Debian package building This completes the adaptation of Fedora's Mock to Debian with full functionality.
290 lines
9.5 KiB
Python
290 lines
9.5 KiB
Python
"""
|
|
sbuild wrapper for deb-mock
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List
|
|
|
|
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
|
|
try:
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
except Exception as e:
|
|
# If we can't create the directory, use a fallback
|
|
output_dir = os.path.join(tempfile.gettempdir(), "deb-mock-output")
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# Prepare sbuild command
|
|
cmd = self._prepare_sbuild_command(source_package, chroot_name, output_dir, **kwargs)
|
|
|
|
# Prepare environment variables
|
|
env = os.environ.copy()
|
|
if kwargs.get("build_env"):
|
|
env.update(kwargs["build_env"])
|
|
env.update(self.config.build_env)
|
|
|
|
# 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, env)
|
|
|
|
# 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])
|
|
|
|
# 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 will be passed to subprocess.run
|
|
pass
|
|
|
|
# Source package
|
|
cmd.append(source_package)
|
|
|
|
return cmd
|
|
|
|
def _execute_sbuild(self, cmd: List[str], log_path: str, env: Dict[str, str] = None) -> 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,
|
|
env=env,
|
|
)
|
|
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:
|
|
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}")
|