deb-mock/deb_mock/chroot.py
robojerk 5e7f4b0562
Some checks failed
Build Deb-Mock Package / build (push) Successful in 55s
Lint Code / Lint All Code (push) Failing after 3s
Test Deb-Mock Build / test (push) Failing after 53s
Fix sbuild integration and clean up codebase
- Fix environment variable handling in sbuild wrapper
- Remove unsupported --log-dir and --env options from sbuild command
- Clean up unused imports and fix linting issues
- Organize examples directory with official Debian hello package
- Fix YAML formatting (trailing spaces, newlines)
- Remove placeholder example files
- All tests passing (30/30)
- Successfully tested build with official Debian hello package
2025-08-04 04:34:32 +00:00

489 lines
17 KiB
Python

"""
Chroot management for deb-mock
"""
import os
import shutil
import subprocess
from pathlib import Path
from typing import List
from .exceptions import ChrootError
class ChrootManager:
"""Manages chroot environments for deb-mock"""
def __init__(self, config):
self.config = config
def create_chroot(self, chroot_name: str, arch: str = None, suite: str = None) -> None:
"""Create a new chroot environment"""
if arch:
self.config.architecture = arch
if suite:
self.config.suite = suite
# Check if bootstrap chroot is needed (Mock FAQ #2)
if self.config.use_bootstrap_chroot:
self._create_bootstrap_chroot(chroot_name)
else:
self._create_standard_chroot(chroot_name)
def _create_bootstrap_chroot(self, chroot_name: str) -> None:
"""
Create a bootstrap chroot for cross-distribution builds.
This addresses Mock FAQ #2 about building packages for newer distributions
on older systems (e.g., building Debian Sid packages on Debian Stable).
"""
bootstrap_name = self.config.bootstrap_chroot_name or f"{chroot_name}-bootstrap"
bootstrap_path = os.path.join(self.config.chroot_dir, bootstrap_name)
# Create minimal bootstrap chroot first
if not os.path.exists(bootstrap_path):
self._create_standard_chroot(bootstrap_name)
# Use bootstrap chroot to create the final chroot
try:
# Create final chroot using debootstrap from within bootstrap
cmd = [
"debootstrap",
"--arch",
self.config.architecture,
self.config.suite,
f"/var/lib/deb-mock/chroots/{chroot_name}",
self.config.mirror,
]
# Execute debootstrap within bootstrap chroot
result = self.execute_in_chroot(bootstrap_name, cmd, capture_output=True)
if result.returncode != 0:
raise ChrootError(
f"Failed to create chroot using bootstrap: {result.stderr}",
chroot_name=chroot_name,
operation="bootstrap_debootstrap",
)
# Configure the new chroot
self._configure_chroot(chroot_name)
except Exception as e:
raise ChrootError(
f"Bootstrap chroot creation failed: {e}",
chroot_name=chroot_name,
operation="bootstrap_creation",
)
def _create_standard_chroot(self, chroot_name: str) -> None:
"""Create a standard chroot using debootstrap"""
chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
if os.path.exists(chroot_path):
raise ChrootError(
f"Chroot '{chroot_name}' already exists",
chroot_name=chroot_name,
operation="create",
)
try:
# Create chroot directory
os.makedirs(chroot_path, exist_ok=True)
# Run debootstrap
cmd = [
"debootstrap",
"--arch",
self.config.architecture,
self.config.suite,
chroot_path,
self.config.mirror,
]
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
if result.returncode != 0:
raise ChrootError(
f"debootstrap failed: {result.stderr}",
chroot_name=chroot_name,
operation="debootstrap",
chroot_path=chroot_path,
)
# Configure the chroot
self._configure_chroot(chroot_name)
except subprocess.CalledProcessError as e:
raise ChrootError(
f"Failed to create chroot: {e}",
chroot_name=chroot_name,
operation="create",
chroot_path=chroot_path,
)
def _configure_chroot(self, chroot_name: str) -> None:
"""Configure a newly created chroot"""
chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
# Create schroot configuration
self._create_schroot_config(chroot_name, chroot_path, self.config.architecture, self.config.suite)
# Install additional packages if specified
if self.config.chroot_additional_packages:
self._install_additional_packages(chroot_name)
# Run setup commands if specified
if self.config.chroot_setup_cmd:
self._run_setup_commands(chroot_name)
def _install_additional_packages(self, chroot_name: str) -> None:
"""Install additional packages in the chroot"""
try:
# Update package lists
self.execute_in_chroot(chroot_name, ["apt-get", "update"], capture_output=True)
# Install packages
cmd = ["apt-get", "install", "-y"] + self.config.chroot_additional_packages
result = self.execute_in_chroot(chroot_name, cmd, capture_output=True)
if result.returncode != 0:
raise ChrootError(
f"Failed to install additional packages: {result.stderr}",
chroot_name=chroot_name,
operation="install_packages",
)
except Exception as e:
raise ChrootError(
f"Failed to install additional packages: {e}",
chroot_name=chroot_name,
operation="install_packages",
)
def _run_setup_commands(self, chroot_name: str) -> None:
"""Run setup commands in the chroot"""
for cmd in self.config.chroot_setup_cmd:
try:
result = self.execute_in_chroot(chroot_name, cmd.split(), capture_output=True)
if result.returncode != 0:
raise ChrootError(
f"Setup command failed: {result.stderr}",
chroot_name=chroot_name,
operation="setup_command",
)
except Exception as e:
raise ChrootError(
f"Failed to run setup command '{cmd}': {e}",
chroot_name=chroot_name,
operation="setup_command",
)
def _create_schroot_config(self, chroot_name: str, chroot_path: str, arch: str, suite: str) -> None:
"""Create schroot configuration file"""
config_content = f"""[{chroot_name}]
description=Deb-Mock chroot for {suite} {arch}
directory={chroot_path}
root-users=root
users=root
type=directory
profile=desktop
preserve-environment=true
"""
config_file = os.path.join(self.config.chroot_config_dir, f"{chroot_name}.conf")
try:
with open(config_file, "w") as f:
f.write(config_content)
except Exception as e:
raise ChrootError(f"Failed to create schroot config: {e}")
def _initialize_chroot(self, chroot_path: str, arch: str, suite: str) -> None:
"""Initialize chroot using debootstrap"""
cmd = [
"debootstrap",
"--arch",
arch,
"--variant=buildd",
suite,
chroot_path,
"http://deb.debian.org/debian/",
]
try:
subprocess.run(cmd, capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as e:
raise ChrootError(f"debootstrap failed: {e.stderr}")
except FileNotFoundError:
raise ChrootError("debootstrap not found. Please install debootstrap package.")
def _install_build_tools(self, chroot_name: str) -> None:
"""Install essential build tools in the chroot"""
packages = [
"build-essential",
"devscripts",
"debhelper",
"dh-make",
"fakeroot",
"lintian",
"sbuild",
"schroot",
]
cmd = ["schroot", "-c", chroot_name, "--", "apt-get", "update"]
try:
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e:
raise ChrootError(f"Failed to update package lists: {e}")
cmd = [
"schroot",
"-c",
chroot_name,
"--",
"apt-get",
"install",
"-y",
] + packages
try:
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e:
raise ChrootError(f"Failed to install build tools: {e}")
def clean_chroot(self, chroot_name: str) -> None:
"""Clean up a chroot environment"""
chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
config_file = os.path.join(self.config.chroot_config_dir, f"{chroot_name}.conf")
try:
# Remove schroot configuration
if os.path.exists(config_file):
os.remove(config_file)
# Remove chroot directory
if os.path.exists(chroot_path):
shutil.rmtree(chroot_path)
except Exception as e:
raise ChrootError(f"Failed to clean chroot '{chroot_name}': {e}")
def list_chroots(self) -> List[str]:
"""List available chroot environments"""
chroots = []
try:
# List chroot configurations
for config_file in Path(self.config.chroot_config_dir).glob("*.conf"):
chroot_name = config_file.stem
chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
if os.path.exists(chroot_path):
chroots.append(chroot_name)
except Exception as e:
raise ChrootError(f"Failed to list chroots: {e}")
return chroots
def chroot_exists(self, chroot_name: str) -> bool:
"""Check if a chroot environment exists"""
chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
config_file = os.path.join(self.config.chroot_config_dir, f"{chroot_name}.conf")
return os.path.exists(chroot_path) and os.path.exists(config_file)
def get_chroot_info(self, chroot_name: str) -> dict:
"""Get information about a chroot environment"""
if not self.chroot_exists(chroot_name):
raise ChrootError(f"Chroot '{chroot_name}' does not exist")
chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
info = {
"name": chroot_name,
"path": chroot_path,
"exists": True,
"size": 0,
"created": None,
"modified": None,
}
try:
stat = os.stat(chroot_path)
info["size"] = stat.st_size
info["created"] = stat.st_ctime
info["modified"] = stat.st_mtime
except Exception:
pass
return info
def update_chroot(self, chroot_name: str) -> None:
"""Update packages in a chroot environment"""
if not self.chroot_exists(chroot_name):
raise ChrootError(f"Chroot '{chroot_name}' does not exist")
try:
# Update package lists
cmd = ["schroot", "-c", chroot_name, "--", "apt-get", "update"]
subprocess.run(cmd, check=True)
# Upgrade packages
cmd = ["schroot", "-c", chroot_name, "--", "apt-get", "upgrade", "-y"]
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e:
raise ChrootError(f"Failed to update chroot '{chroot_name}': {e}")
def execute_in_chroot(
self,
chroot_name: str,
command: list,
capture_output: bool = True,
preserve_env: bool = True,
) -> subprocess.CompletedProcess:
"""Execute a command in the chroot environment"""
if not self.chroot_exists(chroot_name):
raise ChrootError(f"Chroot '{chroot_name}' does not exist")
chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
# Prepare environment variables (Mock FAQ #1 - Environment preservation)
env = self._prepare_chroot_environment(preserve_env)
# Build schroot command
schroot_cmd = [
"schroot",
"-c",
chroot_name,
"--",
"sh",
"-c",
" ".join(command),
]
try:
if capture_output:
result = subprocess.run(
schroot_cmd,
cwd=chroot_path,
env=env,
capture_output=True,
text=True,
check=False,
)
else:
result = subprocess.run(schroot_cmd, cwd=chroot_path, env=env, check=False)
return result
except subprocess.CalledProcessError as e:
raise ChrootError(f"Command failed in chroot: {e}")
def _prepare_chroot_environment(self, preserve_env: bool = True) -> dict:
"""
Prepare environment variables for chroot execution.
This addresses Mock FAQ #1 about environment variable preservation.
"""
env = os.environ.copy()
if not preserve_env or not self.config.environment_sanitization:
return env
# Filter environment variables based on allowed list
filtered_env = {}
# Always preserve basic system variables
basic_vars = ["PATH", "HOME", "USER", "SHELL", "TERM", "LANG", "LC_ALL"]
for var in basic_vars:
if var in env:
filtered_env[var] = env[var]
# Preserve allowed build-related variables
for var in self.config.allowed_environment_vars:
if var in env:
filtered_env[var] = env[var]
# Preserve user-specified variables
for var in self.config.preserve_environment:
if var in env:
filtered_env[var] = env[var]
return filtered_env
def copy_to_chroot(self, source_path: str, dest_path: str, chroot_name: str) -> None:
"""Copy files from host to chroot (similar to Mock's --copyin)"""
if not self.chroot_exists(chroot_name):
raise ChrootError(f"Chroot '{chroot_name}' does not exist")
chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
full_dest_path = os.path.join(chroot_path, dest_path.lstrip("/"))
try:
# Create destination directory if it doesn't exist
os.makedirs(os.path.dirname(full_dest_path), exist_ok=True)
# Copy file or directory
if os.path.isdir(source_path):
shutil.copytree(source_path, full_dest_path, dirs_exist_ok=True)
else:
shutil.copy2(source_path, full_dest_path)
except Exception as e:
raise ChrootError(f"Failed to copy {source_path} to chroot: {e}")
def copy_from_chroot(self, source_path: str, dest_path: str, chroot_name: str) -> None:
"""Copy files from chroot to host (similar to Mock's --copyout)"""
if not self.chroot_exists(chroot_name):
raise ChrootError(f"Chroot '{chroot_name}' does not exist")
chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
full_source_path = os.path.join(chroot_path, source_path.lstrip("/"))
try:
# Create destination directory if it doesn't exist
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
# Copy file or directory
if os.path.isdir(full_source_path):
shutil.copytree(full_source_path, dest_path, dirs_exist_ok=True)
else:
shutil.copy2(full_source_path, dest_path)
except Exception as e:
raise ChrootError(f"Failed to copy {source_path} from chroot: {e}")
def scrub_chroot(self, chroot_name: str) -> None:
"""Clean up chroot without removing it (similar to Mock's --scrub)"""
if not self.chroot_exists(chroot_name):
raise ChrootError(f"Chroot '{chroot_name}' does not exist")
try:
# Clean package cache
self.execute_in_chroot(chroot_name, ["apt-get", "clean"])
# Clean temporary files
self.execute_in_chroot(chroot_name, ["rm", "-rf", "/tmp/*"])
self.execute_in_chroot(chroot_name, ["rm", "-rf", "/var/tmp/*"])
# Clean build artifacts
self.execute_in_chroot(chroot_name, ["rm", "-rf", "/build/*"])
except Exception as e:
raise ChrootError(f"Failed to scrub chroot '{chroot_name}': {e}")
def scrub_all_chroots(self) -> None:
"""Clean up all chroots (similar to Mock's --scrub-all-chroots)"""
chroots = self.list_chroots()
for chroot_name in chroots:
try:
self.scrub_chroot(chroot_name)
except Exception as e:
print(f"Warning: Failed to scrub chroot '{chroot_name}': {e}")