Fix sbuild integration and clean up codebase
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 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
This commit is contained in:
robojerk 2025-08-04 04:34:32 +00:00
parent c33e3aa9ac
commit 5e7f4b0562
32 changed files with 2322 additions and 2228 deletions

3
.gitignore vendored
View file

@ -143,6 +143,9 @@ metadata/
*.tar.bz2 *.tar.bz2
*.diff.gz *.diff.gz
*.orig.tar.gz *.orig.tar.gz
!mock_*_all.deb
!mock_*.buildinfo
!mock_*.changes
# Chroot environments # Chroot environments
/var/lib/deb-mock/ /var/lib/deb-mock/

View file

@ -14,10 +14,10 @@ install-dev: ## Install deb-mock with development dependencies
pip install -r requirements-dev.txt pip install -r requirements-dev.txt
test: ## Run tests test: ## Run tests
python -m pytest tests/ -v python3 -m pytest tests/ -v
test-coverage: ## Run tests with coverage test-coverage: ## Run tests with coverage
python -m pytest tests/ --cov=deb_mock --cov-report=html --cov-report=term python3 -m pytest tests/ --cov=deb_mock --cov-report=html --cov-report=term
lint: ## Run linting checks lint: ## Run linting checks
@echo "=== Running all linting checks with Docker container ===" @echo "=== Running all linting checks with Docker container ==="
@ -84,7 +84,7 @@ check: ## Run all checks (lint, test, format)
$(MAKE) format $(MAKE) format
dist: ## Build distribution package dist: ## Build distribution package
python setup.py sdist bdist_wheel python3 setup.py sdist bdist_wheel
upload: ## Upload to PyPI (requires twine) upload: ## Upload to PyPI (requires twine)
twine upload dist/* twine upload dist/*

View file

@ -9,9 +9,9 @@ __version__ = "0.1.0"
__author__ = "Deb-Mock Team" __author__ = "Deb-Mock Team"
__email__ = "team@deb-mock.org" __email__ = "team@deb-mock.org"
from .core import DebMock
from .config import Config
from .chroot import ChrootManager from .chroot import ChrootManager
from .config import Config
from .core import DebMock
from .sbuild import SbuildWrapper from .sbuild import SbuildWrapper
__all__ = [ __all__ = [

View file

@ -2,13 +2,13 @@
Cache management for deb-mock Cache management for deb-mock
""" """
import hashlib
import os import os
import shutil import shutil
import tarfile import tarfile
import hashlib
from pathlib import Path
from typing import Optional, Dict, Any
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Dict
from .exceptions import DebMockError from .exceptions import DebMockError
@ -43,11 +43,11 @@ class CacheManager:
os.makedirs(os.path.dirname(cache_file), exist_ok=True) os.makedirs(os.path.dirname(cache_file), exist_ok=True)
# Create tar.gz archive of the chroot # Create tar.gz archive of the chroot
with tarfile.open(cache_file, 'w:gz') as tar: with tarfile.open(cache_file, "w:gz") as tar:
tar.add(chroot_path, arcname=os.path.basename(chroot_path)) tar.add(chroot_path, arcname=os.path.basename(chroot_path))
# Update cache metadata # Update cache metadata
self._update_cache_metadata('root_cache', cache_file) self._update_cache_metadata("root_cache", cache_file)
return True return True
@ -65,12 +65,12 @@ class CacheManager:
return False return False
# Check cache age # Check cache age
if not self._is_cache_valid('root_cache', cache_file): if not self._is_cache_valid("root_cache", cache_file):
return False return False
try: try:
# Extract cache to chroot path # Extract cache to chroot path
with tarfile.open(cache_file, 'r:gz') as tar: with tarfile.open(cache_file, "r:gz") as tar:
tar.extractall(path=os.path.dirname(chroot_path)) tar.extractall(path=os.path.dirname(chroot_path))
return True return True
@ -111,7 +111,7 @@ class CacheManager:
packages = [] packages = []
for file in os.listdir(cache_path): for file in os.listdir(cache_path):
if file.endswith('.deb'): if file.endswith(".deb"):
packages.append(os.path.join(cache_path, file)) packages.append(os.path.join(cache_path, file))
return packages return packages
@ -128,8 +128,8 @@ class CacheManager:
os.makedirs(ccache_path, exist_ok=True) os.makedirs(ccache_path, exist_ok=True)
# Set ccache environment variables # Set ccache environment variables
os.environ['CCACHE_DIR'] = ccache_path os.environ["CCACHE_DIR"] = ccache_path
os.environ['CCACHE_HASHDIR'] = '1' os.environ["CCACHE_HASHDIR"] = "1"
return True return True
@ -142,15 +142,15 @@ class CacheManager:
# Clean root caches # Clean root caches
if self.config.use_root_cache: if self.config.use_root_cache:
cleaned['root_cache'] = self._cleanup_root_caches() cleaned["root_cache"] = self._cleanup_root_caches()
# Clean package caches # Clean package caches
if self.config.use_package_cache: if self.config.use_package_cache:
cleaned['package_cache'] = self._cleanup_package_caches() cleaned["package_cache"] = self._cleanup_package_caches()
# Clean ccache # Clean ccache
if self.config.use_ccache: if self.config.use_ccache:
cleaned['ccache'] = self._cleanup_ccache() cleaned["ccache"] = self._cleanup_ccache()
return cleaned return cleaned
@ -164,7 +164,7 @@ class CacheManager:
cutoff_time = datetime.now() - timedelta(days=self.config.root_cache_age) cutoff_time = datetime.now() - timedelta(days=self.config.root_cache_age)
for cache_file in os.listdir(cache_dir): for cache_file in os.listdir(cache_dir):
if cache_file.endswith('.tar.gz'): if cache_file.endswith(".tar.gz"):
cache_path = os.path.join(cache_dir, cache_file) cache_path = os.path.join(cache_dir, cache_file)
if os.path.getmtime(cache_path) < cutoff_time.timestamp(): if os.path.getmtime(cache_path) < cutoff_time.timestamp():
os.remove(cache_path) os.remove(cache_path)
@ -182,7 +182,7 @@ class CacheManager:
cutoff_time = datetime.now() - timedelta(days=30) # 30 days for package cache cutoff_time = datetime.now() - timedelta(days=30) # 30 days for package cache
for package_file in os.listdir(cache_path): for package_file in os.listdir(cache_path):
if package_file.endswith('.deb'): if package_file.endswith(".deb"):
package_path = os.path.join(cache_path, package_file) package_path = os.path.join(cache_path, package_file)
if os.path.getmtime(package_path) < cutoff_time.timestamp(): if os.path.getmtime(package_path) < cutoff_time.timestamp():
os.remove(package_path) os.remove(package_path)
@ -199,7 +199,8 @@ class CacheManager:
# Use ccache's built-in cleanup # Use ccache's built-in cleanup
try: try:
import subprocess import subprocess
result = subprocess.run(['ccache', '-c'], cwd=ccache_path, capture_output=True)
result = subprocess.run(["ccache", "-c"], cwd=ccache_path, capture_output=True)
return 1 if result.returncode == 0 else 0 return 1 if result.returncode == 0 else 0
except Exception: except Exception:
return 0 return 0
@ -209,14 +210,15 @@ class CacheManager:
metadata_file = f"{cache_file}.meta" metadata_file = f"{cache_file}.meta"
metadata = { metadata = {
'type': cache_type, "type": cache_type,
'created': datetime.now().isoformat(), "created": datetime.now().isoformat(),
'size': os.path.getsize(cache_file), "size": os.path.getsize(cache_file),
'hash': self._get_file_hash(cache_file) "hash": self._get_file_hash(cache_file),
} }
import json import json
with open(metadata_file, 'w') as f:
with open(metadata_file, "w") as f:
json.dump(metadata, f) json.dump(metadata, f)
def _is_cache_valid(self, cache_type: str, cache_file: str) -> bool: def _is_cache_valid(self, cache_type: str, cache_file: str) -> bool:
@ -228,20 +230,21 @@ class CacheManager:
try: try:
import json import json
with open(metadata_file, 'r') as f:
with open(metadata_file, "r") as f:
metadata = json.load(f) metadata = json.load(f)
# Check if file size matches # Check if file size matches
if os.path.getsize(cache_file) != metadata.get('size', 0): if os.path.getsize(cache_file) != metadata.get("size", 0):
return False return False
# Check if hash matches # Check if hash matches
if self._get_file_hash(cache_file) != metadata.get('hash', ''): if self._get_file_hash(cache_file) != metadata.get("hash", ""):
return False return False
# Check age for root cache # Check age for root cache
if cache_type == 'root_cache': if cache_type == "root_cache":
created = datetime.fromisoformat(metadata['created']) created = datetime.fromisoformat(metadata["created"])
cutoff_time = datetime.now() - timedelta(days=self.config.root_cache_age) cutoff_time = datetime.now() - timedelta(days=self.config.root_cache_age)
if created < cutoff_time: if created < cutoff_time:
return False return False
@ -267,19 +270,19 @@ class CacheManager:
if self.config.use_root_cache: if self.config.use_root_cache:
cache_file = f"{self.get_root_cache_path()}.tar.gz" cache_file = f"{self.get_root_cache_path()}.tar.gz"
if os.path.exists(cache_file): if os.path.exists(cache_file):
stats['root_cache'] = { stats["root_cache"] = {
'size': os.path.getsize(cache_file), "size": os.path.getsize(cache_file),
'valid': self._is_cache_valid('root_cache', cache_file) "valid": self._is_cache_valid("root_cache", cache_file),
} }
# Package cache stats # Package cache stats
if self.config.use_package_cache: if self.config.use_package_cache:
cache_path = self.get_package_cache_path() cache_path = self.get_package_cache_path()
if os.path.exists(cache_path): if os.path.exists(cache_path):
packages = [f for f in os.listdir(cache_path) if f.endswith('.deb')] packages = [f for f in os.listdir(cache_path) if f.endswith(".deb")]
stats['package_cache'] = { stats["package_cache"] = {
'packages': len(packages), "packages": len(packages),
'size': sum(os.path.getsize(os.path.join(cache_path, p)) for p in packages) "size": sum(os.path.getsize(os.path.join(cache_path, p)) for p in packages),
} }
# ccache stats # ccache stats
@ -288,11 +291,14 @@ class CacheManager:
if os.path.exists(ccache_path): if os.path.exists(ccache_path):
try: try:
import subprocess import subprocess
result = subprocess.run(['ccache', '-s'], cwd=ccache_path,
capture_output=True, text=True) result = subprocess.run(
stats['ccache'] = { ["ccache", "-s"],
'output': result.stdout cwd=ccache_path,
} capture_output=True,
text=True,
)
stats["ccache"] = {"output": result.stdout}
except Exception: except Exception:
pass pass

View file

@ -3,10 +3,11 @@ Chroot management for deb-mock
""" """
import os import os
import subprocess
import shutil import shutil
import subprocess
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import List
from .exceptions import ChrootError from .exceptions import ChrootError
@ -24,8 +25,6 @@ class ChrootManager:
if suite: if suite:
self.config.suite = suite self.config.suite = suite
chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
# Check if bootstrap chroot is needed (Mock FAQ #2) # Check if bootstrap chroot is needed (Mock FAQ #2)
if self.config.use_bootstrap_chroot: if self.config.use_bootstrap_chroot:
self._create_bootstrap_chroot(chroot_name) self._create_bootstrap_chroot(chroot_name)
@ -50,11 +49,12 @@ class ChrootManager:
try: try:
# Create final chroot using debootstrap from within bootstrap # Create final chroot using debootstrap from within bootstrap
cmd = [ cmd = [
'debootstrap', "debootstrap",
'--arch', self.config.architecture, "--arch",
self.config.architecture,
self.config.suite, self.config.suite,
f'/var/lib/deb-mock/chroots/{chroot_name}', f"/var/lib/deb-mock/chroots/{chroot_name}",
self.config.mirror self.config.mirror,
] ]
# Execute debootstrap within bootstrap chroot # Execute debootstrap within bootstrap chroot
@ -64,7 +64,7 @@ class ChrootManager:
raise ChrootError( raise ChrootError(
f"Failed to create chroot using bootstrap: {result.stderr}", f"Failed to create chroot using bootstrap: {result.stderr}",
chroot_name=chroot_name, chroot_name=chroot_name,
operation="bootstrap_debootstrap" operation="bootstrap_debootstrap",
) )
# Configure the new chroot # Configure the new chroot
@ -74,7 +74,7 @@ class ChrootManager:
raise ChrootError( raise ChrootError(
f"Bootstrap chroot creation failed: {e}", f"Bootstrap chroot creation failed: {e}",
chroot_name=chroot_name, chroot_name=chroot_name,
operation="bootstrap_creation" operation="bootstrap_creation",
) )
def _create_standard_chroot(self, chroot_name: str) -> None: def _create_standard_chroot(self, chroot_name: str) -> None:
@ -85,7 +85,7 @@ class ChrootManager:
raise ChrootError( raise ChrootError(
f"Chroot '{chroot_name}' already exists", f"Chroot '{chroot_name}' already exists",
chroot_name=chroot_name, chroot_name=chroot_name,
operation="create" operation="create",
) )
try: try:
@ -94,11 +94,12 @@ class ChrootManager:
# Run debootstrap # Run debootstrap
cmd = [ cmd = [
'debootstrap', "debootstrap",
'--arch', self.config.architecture, "--arch",
self.config.architecture,
self.config.suite, self.config.suite,
chroot_path, chroot_path,
self.config.mirror self.config.mirror,
] ]
result = subprocess.run(cmd, capture_output=True, text=True, check=False) result = subprocess.run(cmd, capture_output=True, text=True, check=False)
@ -108,7 +109,7 @@ class ChrootManager:
f"debootstrap failed: {result.stderr}", f"debootstrap failed: {result.stderr}",
chroot_name=chroot_name, chroot_name=chroot_name,
operation="debootstrap", operation="debootstrap",
chroot_path=chroot_path chroot_path=chroot_path,
) )
# Configure the chroot # Configure the chroot
@ -119,7 +120,7 @@ class ChrootManager:
f"Failed to create chroot: {e}", f"Failed to create chroot: {e}",
chroot_name=chroot_name, chroot_name=chroot_name,
operation="create", operation="create",
chroot_path=chroot_path chroot_path=chroot_path,
) )
def _configure_chroot(self, chroot_name: str) -> None: def _configure_chroot(self, chroot_name: str) -> None:
@ -127,7 +128,7 @@ class ChrootManager:
chroot_path = os.path.join(self.config.chroot_dir, chroot_name) chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
# Create schroot configuration # Create schroot configuration
self._create_schroot_config(chroot_name, chroot_path) self._create_schroot_config(chroot_name, chroot_path, self.config.architecture, self.config.suite)
# Install additional packages if specified # Install additional packages if specified
if self.config.chroot_additional_packages: if self.config.chroot_additional_packages:
@ -141,24 +142,24 @@ class ChrootManager:
"""Install additional packages in the chroot""" """Install additional packages in the chroot"""
try: try:
# Update package lists # Update package lists
self.execute_in_chroot(chroot_name, ['apt-get', 'update'], capture_output=True) self.execute_in_chroot(chroot_name, ["apt-get", "update"], capture_output=True)
# Install packages # Install packages
cmd = ['apt-get', 'install', '-y'] + self.config.chroot_additional_packages cmd = ["apt-get", "install", "-y"] + self.config.chroot_additional_packages
result = self.execute_in_chroot(chroot_name, cmd, capture_output=True) result = self.execute_in_chroot(chroot_name, cmd, capture_output=True)
if result.returncode != 0: if result.returncode != 0:
raise ChrootError( raise ChrootError(
f"Failed to install additional packages: {result.stderr}", f"Failed to install additional packages: {result.stderr}",
chroot_name=chroot_name, chroot_name=chroot_name,
operation="install_packages" operation="install_packages",
) )
except Exception as e: except Exception as e:
raise ChrootError( raise ChrootError(
f"Failed to install additional packages: {e}", f"Failed to install additional packages: {e}",
chroot_name=chroot_name, chroot_name=chroot_name,
operation="install_packages" operation="install_packages",
) )
def _run_setup_commands(self, chroot_name: str) -> None: def _run_setup_commands(self, chroot_name: str) -> None:
@ -171,14 +172,14 @@ class ChrootManager:
raise ChrootError( raise ChrootError(
f"Setup command failed: {result.stderr}", f"Setup command failed: {result.stderr}",
chroot_name=chroot_name, chroot_name=chroot_name,
operation="setup_command" operation="setup_command",
) )
except Exception as e: except Exception as e:
raise ChrootError( raise ChrootError(
f"Failed to run setup command '{cmd}': {e}", f"Failed to run setup command '{cmd}': {e}",
chroot_name=chroot_name, chroot_name=chroot_name,
operation="setup_command" operation="setup_command",
) )
def _create_schroot_config(self, chroot_name: str, chroot_path: str, arch: str, suite: str) -> None: def _create_schroot_config(self, chroot_name: str, chroot_path: str, arch: str, suite: str) -> None:
@ -196,7 +197,7 @@ preserve-environment=true
config_file = os.path.join(self.config.chroot_config_dir, f"{chroot_name}.conf") config_file = os.path.join(self.config.chroot_config_dir, f"{chroot_name}.conf")
try: try:
with open(config_file, 'w') as f: with open(config_file, "w") as f:
f.write(config_content) f.write(config_content)
except Exception as e: except Exception as e:
raise ChrootError(f"Failed to create schroot config: {e}") raise ChrootError(f"Failed to create schroot config: {e}")
@ -204,16 +205,17 @@ preserve-environment=true
def _initialize_chroot(self, chroot_path: str, arch: str, suite: str) -> None: def _initialize_chroot(self, chroot_path: str, arch: str, suite: str) -> None:
"""Initialize chroot using debootstrap""" """Initialize chroot using debootstrap"""
cmd = [ cmd = [
'debootstrap', "debootstrap",
'--arch', arch, "--arch",
'--variant=buildd', arch,
"--variant=buildd",
suite, suite,
chroot_path, chroot_path,
'http://deb.debian.org/debian/' "http://deb.debian.org/debian/",
] ]
try: try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True) subprocess.run(cmd, capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise ChrootError(f"debootstrap failed: {e.stderr}") raise ChrootError(f"debootstrap failed: {e.stderr}")
except FileNotFoundError: except FileNotFoundError:
@ -222,23 +224,31 @@ preserve-environment=true
def _install_build_tools(self, chroot_name: str) -> None: def _install_build_tools(self, chroot_name: str) -> None:
"""Install essential build tools in the chroot""" """Install essential build tools in the chroot"""
packages = [ packages = [
'build-essential', "build-essential",
'devscripts', "devscripts",
'debhelper', "debhelper",
'dh-make', "dh-make",
'fakeroot', "fakeroot",
'lintian', "lintian",
'sbuild', "sbuild",
'schroot' "schroot",
] ]
cmd = ['schroot', '-c', chroot_name, '--', 'apt-get', 'update'] cmd = ["schroot", "-c", chroot_name, "--", "apt-get", "update"]
try: try:
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise ChrootError(f"Failed to update package lists: {e}") raise ChrootError(f"Failed to update package lists: {e}")
cmd = ['schroot', '-c', chroot_name, '--', 'apt-get', 'install', '-y'] + packages cmd = [
"schroot",
"-c",
chroot_name,
"--",
"apt-get",
"install",
"-y",
] + packages
try: try:
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@ -294,19 +304,19 @@ preserve-environment=true
chroot_path = os.path.join(self.config.chroot_dir, chroot_name) chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
info = { info = {
'name': chroot_name, "name": chroot_name,
'path': chroot_path, "path": chroot_path,
'exists': True, "exists": True,
'size': 0, "size": 0,
'created': None, "created": None,
'modified': None "modified": None,
} }
try: try:
stat = os.stat(chroot_path) stat = os.stat(chroot_path)
info['size'] = stat.st_size info["size"] = stat.st_size
info['created'] = stat.st_ctime info["created"] = stat.st_ctime
info['modified'] = stat.st_mtime info["modified"] = stat.st_mtime
except Exception: except Exception:
pass pass
@ -319,19 +329,23 @@ preserve-environment=true
try: try:
# Update package lists # Update package lists
cmd = ['schroot', '-c', chroot_name, '--', 'apt-get', 'update'] cmd = ["schroot", "-c", chroot_name, "--", "apt-get", "update"]
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)
# Upgrade packages # Upgrade packages
cmd = ['schroot', '-c', chroot_name, '--', 'apt-get', 'upgrade', '-y'] cmd = ["schroot", "-c", chroot_name, "--", "apt-get", "upgrade", "-y"]
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
raise ChrootError(f"Failed to update chroot '{chroot_name}': {e}") raise ChrootError(f"Failed to update chroot '{chroot_name}': {e}")
def execute_in_chroot(self, chroot_name: str, command: list, def execute_in_chroot(
capture_output: bool = True, self,
preserve_env: bool = True) -> subprocess.CompletedProcess: chroot_name: str,
command: list,
capture_output: bool = True,
preserve_env: bool = True,
) -> subprocess.CompletedProcess:
"""Execute a command in the chroot environment""" """Execute a command in the chroot environment"""
if not self.chroot_exists(chroot_name): if not self.chroot_exists(chroot_name):
@ -344,8 +358,13 @@ preserve-environment=true
# Build schroot command # Build schroot command
schroot_cmd = [ schroot_cmd = [
'schroot', '-c', chroot_name, '--', 'sh', '-c', "schroot",
' '.join(command) "-c",
chroot_name,
"--",
"sh",
"-c",
" ".join(command),
] ]
try: try:
@ -356,15 +375,10 @@ preserve-environment=true
env=env, env=env,
capture_output=True, capture_output=True,
text=True, text=True,
check=False check=False,
) )
else: else:
result = subprocess.run( result = subprocess.run(schroot_cmd, cwd=chroot_path, env=env, check=False)
schroot_cmd,
cwd=chroot_path,
env=env,
check=False
)
return result return result
@ -386,7 +400,7 @@ preserve-environment=true
filtered_env = {} filtered_env = {}
# Always preserve basic system variables # Always preserve basic system variables
basic_vars = ['PATH', 'HOME', 'USER', 'SHELL', 'TERM', 'LANG', 'LC_ALL'] basic_vars = ["PATH", "HOME", "USER", "SHELL", "TERM", "LANG", "LC_ALL"]
for var in basic_vars: for var in basic_vars:
if var in env: if var in env:
filtered_env[var] = env[var] filtered_env[var] = env[var]
@ -409,7 +423,7 @@ preserve-environment=true
raise ChrootError(f"Chroot '{chroot_name}' does not exist") raise ChrootError(f"Chroot '{chroot_name}' does not exist")
chroot_path = os.path.join(self.config.chroot_dir, chroot_name) chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
full_dest_path = os.path.join(chroot_path, dest_path.lstrip('/')) full_dest_path = os.path.join(chroot_path, dest_path.lstrip("/"))
try: try:
# Create destination directory if it doesn't exist # Create destination directory if it doesn't exist
@ -430,7 +444,7 @@ preserve-environment=true
raise ChrootError(f"Chroot '{chroot_name}' does not exist") raise ChrootError(f"Chroot '{chroot_name}' does not exist")
chroot_path = os.path.join(self.config.chroot_dir, chroot_name) chroot_path = os.path.join(self.config.chroot_dir, chroot_name)
full_source_path = os.path.join(chroot_path, source_path.lstrip('/')) full_source_path = os.path.join(chroot_path, source_path.lstrip("/"))
try: try:
# Create destination directory if it doesn't exist # Create destination directory if it doesn't exist
@ -452,14 +466,14 @@ preserve-environment=true
try: try:
# Clean package cache # Clean package cache
self.execute_in_chroot(chroot_name, ['apt-get', 'clean']) self.execute_in_chroot(chroot_name, ["apt-get", "clean"])
# Clean temporary files # Clean temporary files
self.execute_in_chroot(chroot_name, ['rm', '-rf', '/tmp/*']) self.execute_in_chroot(chroot_name, ["rm", "-rf", "/tmp/*"])
self.execute_in_chroot(chroot_name, ['rm', '-rf', '/var/tmp/*']) self.execute_in_chroot(chroot_name, ["rm", "-rf", "/var/tmp/*"])
# Clean build artifacts # Clean build artifacts
self.execute_in_chroot(chroot_name, ['rm', '-rf', '/build/*']) self.execute_in_chroot(chroot_name, ["rm", "-rf", "/build/*"])
except Exception as e: except Exception as e:
raise ChrootError(f"Failed to scrub chroot '{chroot_name}': {e}") raise ChrootError(f"Failed to scrub chroot '{chroot_name}': {e}")

View file

@ -3,28 +3,22 @@
Command-line interface for deb-mock Command-line interface for deb-mock
""" """
import click
import sys import sys
import os
from pathlib import Path import click
from .core import DebMock
from .config import Config from .config import Config
from .configs import get_available_configs, load_config from .configs import get_available_configs, load_config
from .exceptions import ( from .core import DebMock
DebMockError, ConfigurationError, ChrootError, SbuildError, from .exceptions import ConfigurationError, ValidationError, handle_exception
BuildError, DependencyError, MetadataError, CacheError,
PluginError, NetworkError, PermissionError, ValidationError,
handle_exception, format_error_context
)
@click.group() @click.group()
@click.version_option() @click.version_option()
@click.option('--config', '-c', type=click.Path(exists=True), @click.option("--config", "-c", type=click.Path(exists=True), help="Configuration file path")
help='Configuration file path') @click.option("--chroot", "-r", help="Chroot configuration name (e.g., debian-bookworm-amd64)")
@click.option('--chroot', '-r', help='Chroot configuration name (e.g., debian-bookworm-amd64)') @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
@click.option('--verbose', '-v', is_flag=True, help='Enable verbose output') @click.option("--debug", is_flag=True, help="Enable debug output")
@click.option('--debug', is_flag=True, help='Enable debug output')
@click.pass_context @click.pass_context
def main(ctx, config, chroot, verbose, debug): def main(ctx, config, chroot, verbose, debug):
""" """
@ -34,13 +28,13 @@ def main(ctx, config, chroot, verbose, debug):
for Debian-based ecosystems. for Debian-based ecosystems.
""" """
ctx.ensure_object(dict) ctx.ensure_object(dict)
ctx.obj['verbose'] = verbose ctx.obj["verbose"] = verbose
ctx.obj['debug'] = debug ctx.obj["debug"] = debug
# Load configuration # Load configuration
if config: if config:
try: try:
ctx.obj['config'] = Config.from_file(config) ctx.obj["config"] = Config.from_file(config)
except ConfigurationError as e: except ConfigurationError as e:
e.print_error() e.print_error()
sys.exit(e.get_exit_code()) sys.exit(e.get_exit_code())
@ -48,93 +42,105 @@ def main(ctx, config, chroot, verbose, debug):
# Load core config by name (similar to Mock's -r option) # Load core config by name (similar to Mock's -r option)
try: try:
config_data = load_config(chroot) config_data = load_config(chroot)
ctx.obj['config'] = Config(**config_data) ctx.obj["config"] = Config(**config_data)
except ValueError as e: except ValueError as e:
error = ValidationError( error = ValidationError(
f"Invalid chroot configuration: {e}", f"Invalid chroot configuration: {e}",
field='chroot', field="chroot",
value=chroot, value=chroot,
expected_format='debian-suite-arch or ubuntu-suite-arch' expected_format="debian-suite-arch or ubuntu-suite-arch",
) )
error.print_error() error.print_error()
click.echo(f"Available configs: {', '.join(get_available_configs())}") click.echo(f"Available configs: {', '.join(get_available_configs())}")
sys.exit(error.get_exit_code()) sys.exit(error.get_exit_code())
else: else:
ctx.obj['config'] = Config.default() ctx.obj["config"] = Config.default()
@main.command() @main.command()
@click.argument('source_package', type=click.Path(exists=True)) @click.argument("source_package", type=click.Path(exists=True))
@click.option('--chroot', help='Chroot environment to use') @click.option("--chroot", help="Chroot environment to use")
@click.option('--arch', help='Target architecture') @click.option("--arch", help="Target architecture")
@click.option('--output-dir', '-o', type=click.Path(), @click.option("--output-dir", "-o", type=click.Path(), help="Output directory for build artifacts")
help='Output directory for build artifacts') @click.option("--keep-chroot", is_flag=True, help="Keep chroot after build (for debugging)")
@click.option('--keep-chroot', is_flag=True, @click.option("--no-check", is_flag=True, help="Skip running tests during build")
help='Keep chroot after build (for debugging)') @click.option("--offline", is_flag=True, help="Build in offline mode (no network access)")
@click.option('--no-check', is_flag=True, help='Skip running tests during build') @click.option("--build-timeout", type=int, help="Build timeout in seconds")
@click.option('--offline', is_flag=True, help='Build in offline mode (no network access)') @click.option("--force-arch", help="Force target architecture")
@click.option('--build-timeout', type=int, help='Build timeout in seconds') @click.option("--unique-ext", help="Unique extension for buildroot directory")
@click.option('--force-arch', help='Force target architecture') @click.option("--config-dir", help="Configuration directory")
@click.option('--unique-ext', help='Unique extension for buildroot directory') @click.option("--cleanup-after", is_flag=True, help="Clean chroot after build")
@click.option('--config-dir', help='Configuration directory') @click.option("--no-cleanup-after", is_flag=True, help="Don't clean chroot after build")
@click.option('--cleanup-after', is_flag=True, help='Clean chroot after build')
@click.option('--no-cleanup-after', is_flag=True, help='Don\'t clean chroot after build')
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def build(ctx, source_package, chroot, arch, output_dir, keep_chroot, def build(
no_check, offline, build_timeout, force_arch, unique_ext, ctx,
config_dir, cleanup_after, no_cleanup_after): source_package,
chroot,
arch,
output_dir,
keep_chroot,
no_check,
offline,
build_timeout,
force_arch,
unique_ext,
config_dir,
cleanup_after,
no_cleanup_after,
):
""" """
Build a Debian source package in an isolated environment. Build a Debian source package in an isolated environment.
SOURCE_PACKAGE: Path to the .dsc file or source package directory SOURCE_PACKAGE: Path to the .dsc file or source package directory
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
# Override config with command line options # Override config with command line options
if chroot: if chroot:
ctx.obj['config'].chroot_name = chroot ctx.obj["config"].chroot_name = chroot
if arch: if arch:
ctx.obj['config'].architecture = arch ctx.obj["config"].architecture = arch
if output_dir: if output_dir:
ctx.obj['config'].output_dir = output_dir ctx.obj["config"].output_dir = output_dir
if keep_chroot: if keep_chroot:
ctx.obj['config'].keep_chroot = keep_chroot ctx.obj["config"].keep_chroot = keep_chroot
if no_check: if no_check:
ctx.obj['config'].run_tests = False ctx.obj["config"].run_tests = False
if offline: if offline:
ctx.obj['config'].enable_network = False ctx.obj["config"].enable_network = False
if build_timeout: if build_timeout:
ctx.obj['config'].build_timeout = build_timeout ctx.obj["config"].build_timeout = build_timeout
if force_arch: if force_arch:
ctx.obj['config'].force_architecture = force_arch ctx.obj["config"].force_architecture = force_arch
if unique_ext: if unique_ext:
ctx.obj['config'].unique_extension = unique_ext ctx.obj["config"].unique_extension = unique_ext
if config_dir: if config_dir:
ctx.obj['config'].config_dir = config_dir ctx.obj["config"].config_dir = config_dir
if cleanup_after is not None: if cleanup_after is not None:
ctx.obj['config'].cleanup_after = cleanup_after ctx.obj["config"].cleanup_after = cleanup_after
if no_cleanup_after is not None: if no_cleanup_after is not None:
ctx.obj['config'].cleanup_after = not no_cleanup_after ctx.obj["config"].cleanup_after = not no_cleanup_after
result = deb_mock.build(source_package) result = deb_mock.build(source_package)
if ctx.obj['verbose']: if ctx.obj["verbose"]:
click.echo(f"Build completed successfully: {result}") click.echo(f"Build completed successfully: {result}")
else: else:
click.echo("Build completed successfully") click.echo("Build completed successfully")
@main.command() @main.command()
@click.argument('source_packages', nargs=-1, type=click.Path(exists=True)) @click.argument("source_packages", nargs=-1, type=click.Path(exists=True))
@click.option('--chroot', help='Chroot environment to use') @click.option("--chroot", help="Chroot environment to use")
@click.option('--arch', help='Target architecture') @click.option("--arch", help="Target architecture")
@click.option('--output-dir', '-o', type=click.Path(), @click.option("--output-dir", "-o", type=click.Path(), help="Output directory for build artifacts")
help='Output directory for build artifacts') @click.option("--keep-chroot", is_flag=True, help="Keep chroot after build (for debugging)")
@click.option('--keep-chroot', is_flag=True, @click.option(
help='Keep chroot after build (for debugging)') "--continue-on-failure",
@click.option('--continue-on-failure', is_flag=True, is_flag=True,
help='Continue building remaining packages even if one fails') help="Continue building remaining packages even if one fails",
)
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def chain(ctx, source_packages, chroot, arch, output_dir, keep_chroot, continue_on_failure): def chain(ctx, source_packages, chroot, arch, output_dir, keep_chroot, continue_on_failure):
@ -146,36 +152,33 @@ def chain(ctx, source_packages, chroot, arch, output_dir, keep_chroot, continue_
if not source_packages: if not source_packages:
raise ValidationError( raise ValidationError(
"No source packages specified", "No source packages specified",
field='source_packages', field="source_packages",
expected_format='list of .dsc files or source directories' expected_format="list of .dsc files or source directories",
) )
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
# Override config with command line options # Override config with command line options
if chroot: if chroot:
ctx.obj['config'].chroot_name = chroot ctx.obj["config"].chroot_name = chroot
if arch: if arch:
ctx.obj['config'].architecture = arch ctx.obj["config"].architecture = arch
if output_dir: if output_dir:
ctx.obj['config'].output_dir = output_dir ctx.obj["config"].output_dir = output_dir
if keep_chroot: if keep_chroot:
ctx.obj['config'].keep_chroot = keep_chroot ctx.obj["config"].keep_chroot = keep_chroot
results = deb_mock.build_chain( results = deb_mock.build_chain(list(source_packages), continue_on_failure=continue_on_failure)
list(source_packages),
continue_on_failure=continue_on_failure
)
# Display results # Display results
for result in results: for result in results:
if result['success']: if result["success"]:
click.echo(f"{result['package']} (step {result['order']})") click.echo(f"{result['package']} (step {result['order']})")
else: else:
click.echo(f"{result['package']} (step {result['order']}): {result['error']}") click.echo(f"{result['package']} (step {result['order']}): {result['error']}")
# Check if all builds succeeded # Check if all builds succeeded
failed_builds = [r for r in results if not r['success']] failed_builds = [r for r in results if not r["success"]]
if failed_builds: if failed_builds:
sys.exit(1) sys.exit(1)
else: else:
@ -183,11 +186,15 @@ def chain(ctx, source_packages, chroot, arch, output_dir, keep_chroot, continue_
@main.command() @main.command()
@click.argument('chroot_name') @click.argument("chroot_name")
@click.option('--arch', help='Target architecture') @click.option("--arch", help="Target architecture")
@click.option('--suite', help='Debian suite (e.g., bookworm, sid)') @click.option("--suite", help="Debian suite (e.g., bookworm, sid)")
@click.option('--bootstrap', is_flag=True, help='Use bootstrap chroot for cross-distribution builds') @click.option(
@click.option('--bootstrap-chroot', help='Name of bootstrap chroot to use') "--bootstrap",
is_flag=True,
help="Use bootstrap chroot for cross-distribution builds",
)
@click.option("--bootstrap-chroot", help="Name of bootstrap chroot to use")
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def init_chroot(ctx, chroot_name, arch, suite, bootstrap, bootstrap_chroot): def init_chroot(ctx, chroot_name, arch, suite, bootstrap, bootstrap_chroot):
@ -199,17 +206,17 @@ def init_chroot(ctx, chroot_name, arch, suite, bootstrap, bootstrap_chroot):
The --bootstrap option is useful for building packages for newer distributions The --bootstrap option is useful for building packages for newer distributions
on older systems (e.g., building Debian Sid packages on Debian Stable). on older systems (e.g., building Debian Sid packages on Debian Stable).
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
# Override config with command line options # Override config with command line options
if arch: if arch:
ctx.obj['config'].architecture = arch ctx.obj["config"].architecture = arch
if suite: if suite:
ctx.obj['config'].suite = suite ctx.obj["config"].suite = suite
if bootstrap: if bootstrap:
ctx.obj['config'].use_bootstrap_chroot = True ctx.obj["config"].use_bootstrap_chroot = True
if bootstrap_chroot: if bootstrap_chroot:
ctx.obj['config'].bootstrap_chroot_name = bootstrap_chroot ctx.obj["config"].bootstrap_chroot_name = bootstrap_chroot
deb_mock.init_chroot(chroot_name) deb_mock.init_chroot(chroot_name)
click.echo(f"Chroot '{chroot_name}' initialized successfully") click.echo(f"Chroot '{chroot_name}' initialized successfully")
@ -219,7 +226,7 @@ def init_chroot(ctx, chroot_name, arch, suite, bootstrap, bootstrap_chroot):
@main.command() @main.command()
@click.argument('chroot_name') @click.argument("chroot_name")
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def clean_chroot(ctx, chroot_name): def clean_chroot(ctx, chroot_name):
@ -228,13 +235,13 @@ def clean_chroot(ctx, chroot_name):
CHROOT_NAME: Name of the chroot environment to clean CHROOT_NAME: Name of the chroot environment to clean
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
deb_mock.clean_chroot(chroot_name) deb_mock.clean_chroot(chroot_name)
click.echo(f"Chroot '{chroot_name}' cleaned successfully") click.echo(f"Chroot '{chroot_name}' cleaned successfully")
@main.command() @main.command()
@click.argument('chroot_name') @click.argument("chroot_name")
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def scrub_chroot(ctx, chroot_name): def scrub_chroot(ctx, chroot_name):
@ -243,7 +250,7 @@ def scrub_chroot(ctx, chroot_name):
CHROOT_NAME: Name of the chroot environment to scrub CHROOT_NAME: Name of the chroot environment to scrub
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
deb_mock.chroot_manager.scrub_chroot(chroot_name) deb_mock.chroot_manager.scrub_chroot(chroot_name)
click.echo(f"Chroot '{chroot_name}' scrubbed successfully") click.echo(f"Chroot '{chroot_name}' scrubbed successfully")
@ -255,15 +262,15 @@ def scrub_all_chroots(ctx):
""" """
Clean up all chroot environments without removing them. Clean up all chroot environments without removing them.
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
deb_mock.chroot_manager.scrub_all_chroots() deb_mock.chroot_manager.scrub_all_chroots()
click.echo("All chroots scrubbed successfully") click.echo("All chroots scrubbed successfully")
@main.command() @main.command()
@click.option('--chroot', help='Chroot environment to use') @click.option("--chroot", help="Chroot environment to use")
@click.option('--preserve-env', is_flag=True, help='Preserve environment variables in chroot') @click.option("--preserve-env", is_flag=True, help="Preserve environment variables in chroot")
@click.option('--env-var', multiple=True, help='Specific environment variable to preserve') @click.option("--env-var", multiple=True, help="Specific environment variable to preserve")
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def shell(ctx, chroot, preserve_env, env_var): def shell(ctx, chroot, preserve_env, env_var):
@ -273,23 +280,23 @@ def shell(ctx, chroot, preserve_env, env_var):
Use --preserve-env to preserve environment variables (addresses common Use --preserve-env to preserve environment variables (addresses common
environment variable issues in chroot environments). environment variable issues in chroot environments).
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
chroot_name = chroot or ctx.obj['config'].chroot_name chroot_name = chroot or ctx.obj["config"].chroot_name
# Configure environment preservation # Configure environment preservation
if preserve_env: if preserve_env:
ctx.obj['config'].environment_sanitization = False ctx.obj["config"].environment_sanitization = False
if env_var: if env_var:
ctx.obj['config'].preserve_environment.extend(env_var) ctx.obj["config"].preserve_environment.extend(env_var)
deb_mock.shell(chroot_name) deb_mock.shell(chroot_name)
@main.command() @main.command()
@click.argument('source_path') @click.argument("source_path")
@click.argument('dest_path') @click.argument("dest_path")
@click.option('--chroot', help='Chroot environment to use') @click.option("--chroot", help="Chroot environment to use")
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def copyin(ctx, source_path, dest_path, chroot): def copyin(ctx, source_path, dest_path, chroot):
@ -299,17 +306,17 @@ def copyin(ctx, source_path, dest_path, chroot):
SOURCE_PATH: Path to file/directory on host SOURCE_PATH: Path to file/directory on host
DEST_PATH: Path in chroot where to copy DEST_PATH: Path in chroot where to copy
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
chroot_name = chroot or ctx.obj['config'].chroot_name chroot_name = chroot or ctx.obj["config"].chroot_name
deb_mock.copyin(source_path, dest_path, chroot_name) deb_mock.copyin(source_path, dest_path, chroot_name)
click.echo(f"Copied {source_path} to {dest_path} in chroot '{chroot_name}'") click.echo(f"Copied {source_path} to {dest_path} in chroot '{chroot_name}'")
@main.command() @main.command()
@click.argument('source_path') @click.argument("source_path")
@click.argument('dest_path') @click.argument("dest_path")
@click.option('--chroot', help='Chroot environment to use') @click.option("--chroot", help="Chroot environment to use")
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def copyout(ctx, source_path, dest_path, chroot): def copyout(ctx, source_path, dest_path, chroot):
@ -319,9 +326,9 @@ def copyout(ctx, source_path, dest_path, chroot):
SOURCE_PATH: Path to file/directory in chroot SOURCE_PATH: Path to file/directory in chroot
DEST_PATH: Path on host where to copy DEST_PATH: Path on host where to copy
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
chroot_name = chroot or ctx.obj['config'].chroot_name chroot_name = chroot or ctx.obj["config"].chroot_name
deb_mock.copyout(source_path, dest_path, chroot_name) deb_mock.copyout(source_path, dest_path, chroot_name)
click.echo(f"Copied {source_path} from chroot '{chroot_name}' to {dest_path}") click.echo(f"Copied {source_path} from chroot '{chroot_name}' to {dest_path}")
@ -333,7 +340,7 @@ def list_chroots(ctx):
""" """
List available chroot environments. List available chroot environments.
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
chroots = deb_mock.list_chroots() chroots = deb_mock.list_chroots()
if not chroots: if not chroots:
@ -373,7 +380,7 @@ def cleanup_caches(ctx):
""" """
Clean up old cache files (similar to Mock's cache management). Clean up old cache files (similar to Mock's cache management).
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
cleaned = deb_mock.cleanup_caches() cleaned = deb_mock.cleanup_caches()
if not cleaned: if not cleaned:
@ -393,7 +400,7 @@ def cache_stats(ctx):
""" """
Show cache statistics. Show cache statistics.
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
stats = deb_mock.get_cache_stats() stats = deb_mock.get_cache_stats()
if not stats: if not stats:
@ -417,7 +424,7 @@ def config(ctx):
""" """
Show current configuration. Show current configuration.
""" """
config = ctx.obj['config'] config = ctx.obj["config"]
click.echo("Current configuration:") click.echo("Current configuration:")
click.echo(f" Chroot name: {config.chroot_name}") click.echo(f" Chroot name: {config.chroot_name}")
click.echo(f" Architecture: {config.architecture}") click.echo(f" Architecture: {config.architecture}")
@ -430,9 +437,9 @@ def config(ctx):
@main.command() @main.command()
@click.argument('source_package', type=click.Path(exists=True)) @click.argument("source_package", type=click.Path(exists=True))
@click.option('--chroot', help='Chroot environment to use') @click.option("--chroot", help="Chroot environment to use")
@click.option('--arch', help='Target architecture') @click.option("--arch", help="Target architecture")
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def install_deps(ctx, source_package, chroot, arch): def install_deps(ctx, source_package, chroot, arch):
@ -441,26 +448,26 @@ def install_deps(ctx, source_package, chroot, arch):
SOURCE_PACKAGE: Path to the .dsc file or source package directory SOURCE_PACKAGE: Path to the .dsc file or source package directory
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
# Override config with command line options # Override config with command line options
if chroot: if chroot:
ctx.obj['config'].chroot_name = chroot ctx.obj["config"].chroot_name = chroot
if arch: if arch:
ctx.obj['config'].architecture = arch ctx.obj["config"].architecture = arch
result = deb_mock.install_dependencies(source_package) result = deb_mock.install_dependencies(source_package)
if ctx.obj['verbose']: if ctx.obj["verbose"]:
click.echo(f"Dependencies installed successfully: {result}") click.echo(f"Dependencies installed successfully: {result}")
else: else:
click.echo("Dependencies installed successfully") click.echo("Dependencies installed successfully")
@main.command() @main.command()
@click.argument('packages', nargs=-1, required=True) @click.argument("packages", nargs=-1, required=True)
@click.option('--chroot', help='Chroot environment to use') @click.option("--chroot", help="Chroot environment to use")
@click.option('--arch', help='Target architecture') @click.option("--arch", help="Target architecture")
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def install(ctx, packages, chroot, arch): def install(ctx, packages, chroot, arch):
@ -469,26 +476,26 @@ def install(ctx, packages, chroot, arch):
PACKAGES: List of packages to install PACKAGES: List of packages to install
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
# Override config with command line options # Override config with command line options
if chroot: if chroot:
ctx.obj['config'].chroot_name = chroot ctx.obj["config"].chroot_name = chroot
if arch: if arch:
ctx.obj['config'].architecture = arch ctx.obj["config"].architecture = arch
result = deb_mock.install_packages(packages) result = deb_mock.install_packages(packages)
if ctx.obj['verbose']: if ctx.obj["verbose"]:
click.echo(f"Packages installed successfully: {result}") click.echo(f"Packages installed successfully: {result}")
else: else:
click.echo(f"Packages installed successfully: {', '.join(packages)}") click.echo(f"Packages installed successfully: {', '.join(packages)}")
@main.command() @main.command()
@click.argument('packages', nargs=-1) @click.argument("packages", nargs=-1)
@click.option('--chroot', help='Chroot environment to use') @click.option("--chroot", help="Chroot environment to use")
@click.option('--arch', help='Target architecture') @click.option("--arch", help="Target architecture")
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def update(ctx, packages, chroot, arch): def update(ctx, packages, chroot, arch):
@ -497,17 +504,17 @@ def update(ctx, packages, chroot, arch):
PACKAGES: List of packages to update (if empty, update all) PACKAGES: List of packages to update (if empty, update all)
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
# Override config with command line options # Override config with command line options
if chroot: if chroot:
ctx.obj['config'].chroot_name = chroot ctx.obj["config"].chroot_name = chroot
if arch: if arch:
ctx.obj['config'].architecture = arch ctx.obj["config"].architecture = arch
result = deb_mock.update_packages(packages) result = deb_mock.update_packages(packages)
if ctx.obj['verbose']: if ctx.obj["verbose"]:
click.echo(f"Packages updated successfully: {result}") click.echo(f"Packages updated successfully: {result}")
else: else:
if packages: if packages:
@ -517,9 +524,9 @@ def update(ctx, packages, chroot, arch):
@main.command() @main.command()
@click.argument('packages', nargs=-1, required=True) @click.argument("packages", nargs=-1, required=True)
@click.option('--chroot', help='Chroot environment to use') @click.option("--chroot", help="Chroot environment to use")
@click.option('--arch', help='Target architecture') @click.option("--arch", help="Target architecture")
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def remove(ctx, packages, chroot, arch): def remove(ctx, packages, chroot, arch):
@ -528,26 +535,26 @@ def remove(ctx, packages, chroot, arch):
PACKAGES: List of packages to remove PACKAGES: List of packages to remove
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
# Override config with command line options # Override config with command line options
if chroot: if chroot:
ctx.obj['config'].chroot_name = chroot ctx.obj["config"].chroot_name = chroot
if arch: if arch:
ctx.obj['config'].architecture = arch ctx.obj["config"].architecture = arch
result = deb_mock.remove_packages(packages) result = deb_mock.remove_packages(packages)
if ctx.obj['verbose']: if ctx.obj["verbose"]:
click.echo(f"Packages removed successfully: {result}") click.echo(f"Packages removed successfully: {result}")
else: else:
click.echo(f"Packages removed successfully: {', '.join(packages)}") click.echo(f"Packages removed successfully: {', '.join(packages)}")
@main.command() @main.command()
@click.argument('command') @click.argument("command")
@click.option('--chroot', help='Chroot environment to use') @click.option("--chroot", help="Chroot environment to use")
@click.option('--arch', help='Target architecture') @click.option("--arch", help="Target architecture")
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def apt_cmd(ctx, command, chroot, arch): def apt_cmd(ctx, command, chroot, arch):
@ -556,31 +563,31 @@ def apt_cmd(ctx, command, chroot, arch):
COMMAND: APT command to execute (e.g., "update", "install package") COMMAND: APT command to execute (e.g., "update", "install package")
""" """
deb_mock = DebMock(ctx.obj['config']) deb_mock = DebMock(ctx.obj["config"])
# Override config with command line options # Override config with command line options
if chroot: if chroot:
ctx.obj['config'].chroot_name = chroot ctx.obj["config"].chroot_name = chroot
if arch: if arch:
ctx.obj['config'].architecture = arch ctx.obj["config"].architecture = arch
result = deb_mock.execute_apt_command(command) result = deb_mock.execute_apt_command(command)
if ctx.obj['verbose']: if ctx.obj["verbose"]:
click.echo(f"APT command executed successfully: {result}") click.echo(f"APT command executed successfully: {result}")
else: else:
click.echo(f"APT command executed: {command}") click.echo(f"APT command executed: {command}")
@main.command() @main.command()
@click.option('--expand', is_flag=True, help='Show expanded configuration values') @click.option("--expand", is_flag=True, help="Show expanded configuration values")
@click.pass_context @click.pass_context
@handle_exception @handle_exception
def debug_config(ctx, expand): def debug_config(ctx, expand):
""" """
Show detailed configuration information for debugging. Show detailed configuration information for debugging.
""" """
config = ctx.obj['config'] config = ctx.obj["config"]
if expand: if expand:
# Show expanded configuration (with template values resolved) # Show expanded configuration (with template values resolved)
@ -601,11 +608,11 @@ def debug_config(ctx, expand):
click.echo(f" chroot_home: {config.chroot_home}") click.echo(f" chroot_home: {config.chroot_home}")
# Show plugin configuration # Show plugin configuration
if hasattr(config, 'plugins') and config.plugins: if hasattr(config, "plugins") and config.plugins:
click.echo(" plugins:") click.echo(" plugins:")
for plugin_name, plugin_config in config.plugins.items(): for plugin_name, plugin_config in config.plugins.items():
click.echo(f" {plugin_name}: {plugin_config}") click.echo(f" {plugin_name}: {plugin_config}")
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View file

@ -3,9 +3,11 @@ Configuration management for deb-mock
""" """
import os import os
import yaml
from pathlib import Path from pathlib import Path
from typing import Dict, Any, Optional from typing import Any, Dict
import yaml
from .exceptions import ConfigurationError from .exceptions import ConfigurationError
@ -14,108 +16,123 @@ class Config:
def __init__(self, **kwargs): def __init__(self, **kwargs):
# Default configuration # Default configuration
self.chroot_name = kwargs.get('chroot_name', 'bookworm-amd64') self.chroot_name = kwargs.get("chroot_name", "bookworm-amd64")
self.architecture = kwargs.get('architecture', 'amd64') self.architecture = kwargs.get("architecture", "amd64")
self.suite = kwargs.get('suite', 'bookworm') self.suite = kwargs.get("suite", "bookworm")
self.output_dir = kwargs.get('output_dir', './output') self.output_dir = kwargs.get("output_dir", "./output")
self.keep_chroot = kwargs.get('keep_chroot', False) self.keep_chroot = kwargs.get("keep_chroot", False)
self.verbose = kwargs.get('verbose', False) self.verbose = kwargs.get("verbose", False)
self.debug = kwargs.get('debug', False) self.debug = kwargs.get("debug", False)
# Chroot configuration # Chroot configuration
self.basedir = kwargs.get('basedir', '/var/lib/deb-mock') self.basedir = kwargs.get("basedir", "/var/lib/deb-mock")
self.chroot_dir = kwargs.get('chroot_dir', '/var/lib/deb-mock/chroots') self.chroot_dir = kwargs.get("chroot_dir", "/var/lib/deb-mock/chroots")
self.chroot_config_dir = kwargs.get('chroot_config_dir', '/etc/schroot/chroot.d') self.chroot_config_dir = kwargs.get("chroot_config_dir", "/etc/schroot/chroot.d")
self.chroot_home = kwargs.get('chroot_home', '/home/build') self.chroot_home = kwargs.get("chroot_home", "/home/build")
# sbuild configuration # sbuild configuration
self.sbuild_config = kwargs.get('sbuild_config', '/etc/sbuild/sbuild.conf') self.sbuild_config = kwargs.get("sbuild_config", "/etc/sbuild/sbuild.conf")
self.sbuild_log_dir = kwargs.get('sbuild_log_dir', '/var/log/sbuild') self.sbuild_log_dir = kwargs.get("sbuild_log_dir", "/var/log/sbuild")
# Build configuration # Build configuration
self.build_deps = kwargs.get('build_deps', []) self.build_deps = kwargs.get("build_deps", [])
self.build_env = kwargs.get('build_env', {}) self.build_env = kwargs.get("build_env", {})
self.build_options = kwargs.get('build_options', []) self.build_options = kwargs.get("build_options", [])
# Metadata configuration # Metadata configuration
self.metadata_dir = kwargs.get('metadata_dir', './metadata') self.metadata_dir = kwargs.get("metadata_dir", "./metadata")
self.capture_logs = kwargs.get('capture_logs', True) self.capture_logs = kwargs.get("capture_logs", True)
self.capture_changes = kwargs.get('capture_changes', True) self.capture_changes = kwargs.get("capture_changes", True)
# Speed optimization (Mock-inspired features) # Speed optimization (Mock-inspired features)
self.cache_dir = kwargs.get('cache_dir', '/var/cache/deb-mock') self.cache_dir = kwargs.get("cache_dir", "/var/cache/deb-mock")
self.use_root_cache = kwargs.get('use_root_cache', True) self.use_root_cache = kwargs.get("use_root_cache", True)
self.root_cache_dir = kwargs.get('root_cache_dir', '/var/cache/deb-mock/root-cache') self.root_cache_dir = kwargs.get("root_cache_dir", "/var/cache/deb-mock/root-cache")
self.root_cache_age = kwargs.get('root_cache_age', 7) # days self.root_cache_age = kwargs.get("root_cache_age", 7) # days
self.use_package_cache = kwargs.get('use_package_cache', True) self.use_package_cache = kwargs.get("use_package_cache", True)
self.package_cache_dir = kwargs.get('package_cache_dir', '/var/cache/deb-mock/package-cache') self.package_cache_dir = kwargs.get("package_cache_dir", "/var/cache/deb-mock/package-cache")
self.use_ccache = kwargs.get('use_ccache', False) self.use_ccache = kwargs.get("use_ccache", False)
self.ccache_dir = kwargs.get('ccache_dir', '/var/cache/deb-mock/ccache') self.ccache_dir = kwargs.get("ccache_dir", "/var/cache/deb-mock/ccache")
self.use_tmpfs = kwargs.get('use_tmpfs', False) self.use_tmpfs = kwargs.get("use_tmpfs", False)
self.tmpfs_size = kwargs.get('tmpfs_size', '2G') self.tmpfs_size = kwargs.get("tmpfs_size", "2G")
# Parallel builds # Parallel builds
self.parallel_jobs = kwargs.get('parallel_jobs', 4) self.parallel_jobs = kwargs.get("parallel_jobs", 4)
self.parallel_compression = kwargs.get('parallel_compression', True) self.parallel_compression = kwargs.get("parallel_compression", True)
# Network and proxy # Network and proxy
self.use_host_resolv = kwargs.get('use_host_resolv', True) self.use_host_resolv = kwargs.get("use_host_resolv", True)
self.http_proxy = kwargs.get('http_proxy', None) self.http_proxy = kwargs.get("http_proxy", None)
self.https_proxy = kwargs.get('https_proxy', None) self.https_proxy = kwargs.get("https_proxy", None)
self.no_proxy = kwargs.get('no_proxy', None) self.no_proxy = kwargs.get("no_proxy", None)
# Mirror configuration # Mirror configuration
self.mirror = kwargs.get('mirror', 'http://deb.debian.org/debian/') self.mirror = kwargs.get("mirror", "http://deb.debian.org/debian/")
self.security_mirror = kwargs.get('security_mirror', None) self.security_mirror = kwargs.get("security_mirror", None)
self.backports_mirror = kwargs.get('backports_mirror', None) self.backports_mirror = kwargs.get("backports_mirror", None)
# Isolation and security # Isolation and security
self.isolation = kwargs.get('isolation', 'schroot') # schroot, simple, nspawn self.isolation = kwargs.get("isolation", "schroot") # schroot, simple, nspawn
self.enable_network = kwargs.get('enable_network', True) self.enable_network = kwargs.get("enable_network", True)
self.selinux_enabled = kwargs.get('selinux_enabled', False) self.selinux_enabled = kwargs.get("selinux_enabled", False)
# Bootstrap chroot support (Mock FAQ #2 - Cross-distribution builds) # Bootstrap chroot support (Mock FAQ #2 - Cross-distribution builds)
self.use_bootstrap_chroot = kwargs.get('use_bootstrap_chroot', False) self.use_bootstrap_chroot = kwargs.get("use_bootstrap_chroot", False)
self.bootstrap_chroot_name = kwargs.get('bootstrap_chroot_name', None) self.bootstrap_chroot_name = kwargs.get("bootstrap_chroot_name", None)
self.bootstrap_arch = kwargs.get('bootstrap_arch', None) self.bootstrap_arch = kwargs.get("bootstrap_arch", None)
self.bootstrap_suite = kwargs.get('bootstrap_suite', None) self.bootstrap_suite = kwargs.get("bootstrap_suite", None)
# Build environment customization # Build environment customization
self.chroot_setup_cmd = kwargs.get('chroot_setup_cmd', []) self.chroot_setup_cmd = kwargs.get("chroot_setup_cmd", [])
self.chroot_additional_packages = kwargs.get('chroot_additional_packages', []) self.chroot_additional_packages = kwargs.get("chroot_additional_packages", [])
# Environment variable preservation (Mock FAQ #1) # Environment variable preservation (Mock FAQ #1)
self.preserve_environment = kwargs.get('preserve_environment', []) self.preserve_environment = kwargs.get("preserve_environment", [])
self.environment_sanitization = kwargs.get('environment_sanitization', True) self.environment_sanitization = kwargs.get("environment_sanitization", True)
self.allowed_environment_vars = kwargs.get('allowed_environment_vars', [ self.allowed_environment_vars = kwargs.get(
'DEB_BUILD_OPTIONS', 'DEB_BUILD_PROFILES', 'CC', 'CXX', 'CFLAGS', 'CXXFLAGS', "allowed_environment_vars",
'LDFLAGS', 'MAKEFLAGS', 'CCACHE_DIR', 'CCACHE_HASHDIR', 'http_proxy', [
'https_proxy', 'no_proxy', 'DISPLAY', 'XAUTHORITY' "DEB_BUILD_OPTIONS",
]) "DEB_BUILD_PROFILES",
"CC",
"CXX",
"CFLAGS",
"CXXFLAGS",
"LDFLAGS",
"MAKEFLAGS",
"CCACHE_DIR",
"CCACHE_HASHDIR",
"http_proxy",
"https_proxy",
"no_proxy",
"DISPLAY",
"XAUTHORITY",
],
)
# Advanced build options (Mock-inspired) # Advanced build options (Mock-inspired)
self.run_tests = kwargs.get('run_tests', True) self.run_tests = kwargs.get("run_tests", True)
self.build_timeout = kwargs.get('build_timeout', 0) # 0 = no timeout self.build_timeout = kwargs.get("build_timeout", 0) # 0 = no timeout
self.force_architecture = kwargs.get('force_architecture', None) self.force_architecture = kwargs.get("force_architecture", None)
self.unique_extension = kwargs.get('unique_extension', None) self.unique_extension = kwargs.get("unique_extension", None)
self.config_dir = kwargs.get('config_dir', None) self.config_dir = kwargs.get("config_dir", None)
self.cleanup_after = kwargs.get('cleanup_after', True) self.cleanup_after = kwargs.get("cleanup_after", True)
# APT configuration # APT configuration
self.apt_sources = kwargs.get('apt_sources', []) self.apt_sources = kwargs.get("apt_sources", [])
self.apt_preferences = kwargs.get('apt_preferences', []) self.apt_preferences = kwargs.get("apt_preferences", [])
self.apt_command = kwargs.get('apt_command', 'apt-get') self.apt_command = kwargs.get("apt_command", "apt-get")
self.apt_install_command = kwargs.get('apt_install_command', 'apt-get install -y') self.apt_install_command = kwargs.get("apt_install_command", "apt-get install -y")
# Plugin configuration # Plugin configuration
self.plugins = kwargs.get('plugins', {}) self.plugins = kwargs.get("plugins", {})
self.plugin_dir = kwargs.get('plugin_dir', '/usr/lib/deb-mock/plugins') self.plugin_dir = kwargs.get("plugin_dir", "/usr/lib/deb-mock/plugins")
@classmethod @classmethod
def from_file(cls, config_path: str) -> 'Config': def from_file(cls, config_path: str) -> "Config":
"""Load configuration from a YAML file""" """Load configuration from a YAML file"""
try: try:
with open(config_path, 'r') as f: with open(config_path, "r") as f:
config_data = yaml.safe_load(f) config_data = yaml.safe_load(f)
return cls(**config_data) return cls(**config_data)
@ -127,60 +144,60 @@ class Config:
raise ConfigurationError(f"Error loading configuration: {e}") raise ConfigurationError(f"Error loading configuration: {e}")
@classmethod @classmethod
def default(cls) -> 'Config': def default(cls) -> "Config":
"""Create default configuration""" """Create default configuration"""
return cls() return cls()
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
"""Convert configuration to dictionary""" """Convert configuration to dictionary"""
return { return {
'chroot_name': self.chroot_name, "chroot_name": self.chroot_name,
'architecture': self.architecture, "architecture": self.architecture,
'suite': self.suite, "suite": self.suite,
'output_dir': self.output_dir, "output_dir": self.output_dir,
'keep_chroot': self.keep_chroot, "keep_chroot": self.keep_chroot,
'verbose': self.verbose, "verbose": self.verbose,
'debug': self.debug, "debug": self.debug,
'chroot_dir': self.chroot_dir, "chroot_dir": self.chroot_dir,
'chroot_config_dir': self.chroot_config_dir, "chroot_config_dir": self.chroot_config_dir,
'sbuild_config': self.sbuild_config, "sbuild_config": self.sbuild_config,
'sbuild_log_dir': self.sbuild_log_dir, "sbuild_log_dir": self.sbuild_log_dir,
'build_deps': self.build_deps, "build_deps": self.build_deps,
'build_env': self.build_env, "build_env": self.build_env,
'build_options': self.build_options, "build_options": self.build_options,
'metadata_dir': self.metadata_dir, "metadata_dir": self.metadata_dir,
'capture_logs': self.capture_logs, "capture_logs": self.capture_logs,
'capture_changes': self.capture_changes, "capture_changes": self.capture_changes,
'use_root_cache': self.use_root_cache, "use_root_cache": self.use_root_cache,
'root_cache_dir': self.root_cache_dir, "root_cache_dir": self.root_cache_dir,
'root_cache_age': self.root_cache_age, "root_cache_age": self.root_cache_age,
'use_package_cache': self.use_package_cache, "use_package_cache": self.use_package_cache,
'package_cache_dir': self.package_cache_dir, "package_cache_dir": self.package_cache_dir,
'use_ccache': self.use_ccache, "use_ccache": self.use_ccache,
'ccache_dir': self.ccache_dir, "ccache_dir": self.ccache_dir,
'use_tmpfs': self.use_tmpfs, "use_tmpfs": self.use_tmpfs,
'tmpfs_size': self.tmpfs_size, "tmpfs_size": self.tmpfs_size,
'parallel_jobs': self.parallel_jobs, "parallel_jobs": self.parallel_jobs,
'parallel_compression': self.parallel_compression, "parallel_compression": self.parallel_compression,
'use_host_resolv': self.use_host_resolv, "use_host_resolv": self.use_host_resolv,
'http_proxy': self.http_proxy, "http_proxy": self.http_proxy,
'https_proxy': self.https_proxy, "https_proxy": self.https_proxy,
'no_proxy': self.no_proxy, "no_proxy": self.no_proxy,
'mirror': self.mirror, "mirror": self.mirror,
'security_mirror': self.security_mirror, "security_mirror": self.security_mirror,
'backports_mirror': self.backports_mirror, "backports_mirror": self.backports_mirror,
'isolation': self.isolation, "isolation": self.isolation,
'enable_network': self.enable_network, "enable_network": self.enable_network,
'selinux_enabled': self.selinux_enabled, "selinux_enabled": self.selinux_enabled,
'use_bootstrap_chroot': self.use_bootstrap_chroot, "use_bootstrap_chroot": self.use_bootstrap_chroot,
'bootstrap_chroot_name': self.bootstrap_chroot_name, "bootstrap_chroot_name": self.bootstrap_chroot_name,
'bootstrap_arch': self.bootstrap_arch, "bootstrap_arch": self.bootstrap_arch,
'bootstrap_suite': self.bootstrap_suite, "bootstrap_suite": self.bootstrap_suite,
'chroot_setup_cmd': self.chroot_setup_cmd, "chroot_setup_cmd": self.chroot_setup_cmd,
'chroot_additional_packages': self.chroot_additional_packages, "chroot_additional_packages": self.chroot_additional_packages,
'preserve_environment': self.preserve_environment, "preserve_environment": self.preserve_environment,
'environment_sanitization': self.environment_sanitization, "environment_sanitization": self.environment_sanitization,
'allowed_environment_vars': self.allowed_environment_vars, "allowed_environment_vars": self.allowed_environment_vars,
} }
def save(self, config_path: str) -> None: def save(self, config_path: str) -> None:
@ -189,7 +206,7 @@ class Config:
config_dir = Path(config_path).parent config_dir = Path(config_path).parent
config_dir.mkdir(parents=True, exist_ok=True) config_dir.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w') as f: with open(config_path, "w") as f:
yaml.dump(self.to_dict(), f, default_flow_style=False) yaml.dump(self.to_dict(), f, default_flow_style=False)
except Exception as e: except Exception as e:
raise ConfigurationError(f"Error saving configuration: {e}") raise ConfigurationError(f"Error saving configuration: {e}")
@ -206,17 +223,25 @@ class Config:
errors.append(f"sbuild config file does not exist: {self.sbuild_config}") errors.append(f"sbuild config file does not exist: {self.sbuild_config}")
# Check architecture # Check architecture
valid_architectures = ['amd64', 'i386', 'arm64', 'armhf', 'ppc64el', 's390x'] valid_architectures = ["amd64", "i386", "arm64", "armhf", "ppc64el", "s390x"]
if self.architecture not in valid_architectures: if self.architecture not in valid_architectures:
errors.append(f"Invalid architecture: {self.architecture}") errors.append(f"Invalid architecture: {self.architecture}")
# Check suite # Check suite
valid_suites = ['bookworm', 'sid', 'bullseye', 'buster', 'jammy', 'noble', 'focal'] valid_suites = [
"bookworm",
"sid",
"bullseye",
"buster",
"jammy",
"noble",
"focal",
]
if self.suite not in valid_suites: if self.suite not in valid_suites:
errors.append(f"Invalid suite: {self.suite}") errors.append(f"Invalid suite: {self.suite}")
# Check isolation method # Check isolation method
valid_isolation = ['schroot', 'simple', 'nspawn'] valid_isolation = ["schroot", "simple", "nspawn"]
if self.isolation not in valid_isolation: if self.isolation not in valid_isolation:
errors.append(f"Invalid isolation method: {self.isolation}") errors.append(f"Invalid isolation method: {self.isolation}")
@ -225,7 +250,7 @@ class Config:
errors.append("Parallel jobs must be at least 1") errors.append("Parallel jobs must be at least 1")
if errors: if errors:
raise ConfigurationError(f"Configuration validation failed:\n" + "\n".join(errors)) raise ConfigurationError("Configuration validation failed:\n" + "\n".join(errors))
def get_chroot_path(self) -> str: def get_chroot_path(self) -> str:
"""Get the full path to the chroot directory""" """Get the full path to the chroot directory"""
@ -257,21 +282,21 @@ class Config:
# Set parallel build options # Set parallel build options
if self.parallel_jobs > 1: if self.parallel_jobs > 1:
env['DEB_BUILD_OPTIONS'] = f"parallel={self.parallel_jobs},nocheck" env["DEB_BUILD_OPTIONS"] = f"parallel={self.parallel_jobs},nocheck"
env['MAKEFLAGS'] = f"-j{self.parallel_jobs}" env["MAKEFLAGS"] = f"-j{self.parallel_jobs}"
# Set ccache if enabled # Set ccache if enabled
if self.use_ccache: if self.use_ccache:
env['CCACHE_DIR'] = self.get_ccache_path() env["CCACHE_DIR"] = self.get_ccache_path()
env['CCACHE_HASHDIR'] = '1' env["CCACHE_HASHDIR"] = "1"
# Set proxy if configured # Set proxy if configured
if self.http_proxy: if self.http_proxy:
env['http_proxy'] = self.http_proxy env["http_proxy"] = self.http_proxy
if self.https_proxy: if self.https_proxy:
env['https_proxy'] = self.https_proxy env["https_proxy"] = self.https_proxy
if self.no_proxy: if self.no_proxy:
env['no_proxy'] = self.no_proxy env["no_proxy"] = self.no_proxy
# Merge with user-defined build environment # Merge with user-defined build environment
env.update(self.build_env) env.update(self.build_env)

View file

@ -5,14 +5,15 @@ This package provides default configuration files for various Debian-based Linux
similar to Mock's mock-core-configs package. similar to Mock's mock-core-configs package.
""" """
import os
import yaml
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List
import yaml
# Base directory for config files # Base directory for config files
CONFIGS_DIR = Path(__file__).parent CONFIGS_DIR = Path(__file__).parent
def get_available_configs() -> List[str]: def get_available_configs() -> List[str]:
"""Get list of available configuration names""" """Get list of available configuration names"""
configs = [] configs = []
@ -21,15 +22,17 @@ def get_available_configs() -> List[str]:
configs.append(config_file.stem) configs.append(config_file.stem)
return sorted(configs) return sorted(configs)
def load_config(config_name: str) -> Dict: def load_config(config_name: str) -> Dict:
"""Load a configuration by name""" """Load a configuration by name"""
config_file = CONFIGS_DIR / f"{config_name}.yaml" config_file = CONFIGS_DIR / f"{config_name}.yaml"
if not config_file.exists(): if not config_file.exists():
raise ValueError(f"Configuration '{config_name}' not found") raise ValueError(f"Configuration '{config_name}' not found")
with open(config_file, 'r') as f: with open(config_file, "r") as f:
return yaml.safe_load(f) return yaml.safe_load(f)
def list_configs() -> Dict[str, Dict]: def list_configs() -> Dict[str, Dict]:
"""List all available configurations with their details""" """List all available configurations with their details"""
configs = {} configs = {}
@ -37,10 +40,10 @@ def list_configs() -> Dict[str, Dict]:
try: try:
config = load_config(config_name) config = load_config(config_name)
configs[config_name] = { configs[config_name] = {
'description': config.get('description', ''), "description": config.get("description", ""),
'suite': config.get('suite', ''), "suite": config.get("suite", ""),
'architecture': config.get('architecture', ''), "architecture": config.get("architecture", ""),
'mirror': config.get('mirror', '') "mirror": config.get("mirror", ""),
} }
except Exception: except Exception:
continue continue

View file

@ -33,3 +33,4 @@ metadata_dir: "./metadata"
keep_chroot: false keep_chroot: false
verbose: false verbose: false
debug: false debug: false

View file

@ -33,3 +33,4 @@ metadata_dir: "./metadata"
keep_chroot: false keep_chroot: false
verbose: false verbose: false
debug: false debug: false

View file

@ -33,3 +33,4 @@ metadata_dir: "./metadata"
keep_chroot: false keep_chroot: false
verbose: false verbose: false
debug: false debug: false

View file

@ -33,3 +33,4 @@ metadata_dir: "./metadata"
keep_chroot: false keep_chroot: false
verbose: false verbose: false
debug: false debug: false

View file

@ -3,16 +3,15 @@ Core DebMock class for orchestrating the build process
""" """
import os import os
import json
import shutil
from pathlib import Path from pathlib import Path
from typing import Dict, Any, Optional, List from typing import Any, Dict, List, Optional
from .config import Config
from .chroot import ChrootManager
from .sbuild import SbuildWrapper
from .metadata import MetadataManager
from .cache import CacheManager from .cache import CacheManager
from .exceptions import DebMockError, BuildError, ChrootError, SbuildError from .chroot import ChrootManager
from .config import Config
from .exceptions import ChrootError
from .metadata import MetadataManager
from .sbuild import SbuildWrapper
class DebMock: class DebMock:
@ -45,7 +44,7 @@ class DebMock:
"""Build a Debian source package in an isolated environment""" """Build a Debian source package in an isolated environment"""
# Ensure chroot exists # Ensure chroot exists
chroot_name = kwargs.get('chroot_name', self.config.chroot_name) chroot_name = kwargs.get("chroot_name", self.config.chroot_name)
chroot_path = self.config.get_chroot_path() chroot_path = self.config.get_chroot_path()
# Try to restore from cache first # Try to restore from cache first
@ -55,24 +54,19 @@ class DebMock:
# Check build dependencies # Check build dependencies
deps_check = self.sbuild_wrapper.check_dependencies(source_package, chroot_name) deps_check = self.sbuild_wrapper.check_dependencies(source_package, chroot_name)
if not deps_check['satisfied']: if not deps_check["satisfied"]:
# Try to install missing dependencies # Try to install missing dependencies
if deps_check['missing']: if deps_check["missing"]:
self.sbuild_wrapper.install_build_dependencies(deps_check['missing'], chroot_name) self.sbuild_wrapper.install_build_dependencies(deps_check["missing"], chroot_name)
# Setup build environment # Setup build environment
build_env = self.config.setup_build_environment() build_env = self.config.setup_build_environment()
# Build the package # Build the package
build_result = self.sbuild_wrapper.build_package( build_result = self.sbuild_wrapper.build_package(source_package, chroot_name, build_env=build_env, **kwargs)
source_package,
chroot_name,
build_env=build_env,
**kwargs
)
# Create cache after successful build # Create cache after successful build
if build_result.get('success', False): if build_result.get("success", False):
self.cache_manager.create_root_cache(chroot_path) self.cache_manager.create_root_cache(chroot_path)
# Capture and store metadata # Capture and store metadata
@ -80,7 +74,7 @@ class DebMock:
self.metadata_manager.store_metadata(metadata) self.metadata_manager.store_metadata(metadata)
# Clean up chroot if not keeping it # Clean up chroot if not keeping it
if not kwargs.get('keep_chroot', self.config.keep_chroot): if not kwargs.get("keep_chroot", self.config.keep_chroot):
self.chroot_manager.clean_chroot(chroot_name) self.chroot_manager.clean_chroot(chroot_name)
return build_result return build_result
@ -89,7 +83,7 @@ class DebMock:
"""Build a chain of packages that depend on each other (similar to Mock's --chain)""" """Build a chain of packages that depend on each other (similar to Mock's --chain)"""
results = [] results = []
chroot_name = kwargs.get('chroot_name', self.config.chroot_name) chroot_name = kwargs.get("chroot_name", self.config.chroot_name)
chroot_path = self.config.get_chroot_path() chroot_path = self.config.get_chroot_path()
# Try to restore from cache first # Try to restore from cache first
@ -103,37 +97,36 @@ class DebMock:
for i, source_package in enumerate(source_packages): for i, source_package in enumerate(source_packages):
try: try:
# Build the package # Build the package
result = self.sbuild_wrapper.build_package( result = self.sbuild_wrapper.build_package(source_package, chroot_name, build_env=build_env, **kwargs)
source_package,
chroot_name, results.append(
build_env=build_env, {
**kwargs "package": source_package,
"success": True,
"result": result,
"order": i + 1,
}
) )
results.append({
'package': source_package,
'success': True,
'result': result,
'order': i + 1
})
# Install the built package in the chroot for subsequent builds # Install the built package in the chroot for subsequent builds
if result.get('artifacts'): if result.get("artifacts"):
self._install_built_package(result['artifacts'], chroot_name) self._install_built_package(result["artifacts"], chroot_name)
except Exception as e: except Exception as e:
results.append({ results.append(
'package': source_package, {
'success': False, "package": source_package,
'error': str(e), "success": False,
'order': i + 1 "error": str(e),
}) "order": i + 1,
}
)
# Stop chain on failure unless continue_on_failure is specified # Stop chain on failure unless continue_on_failure is specified
if not kwargs.get('continue_on_failure', False): if not kwargs.get("continue_on_failure", False):
break break
# Create cache after successful chain build # Create cache after successful chain build
if any(r['success'] for r in results): if any(r["success"] for r in results):
self.cache_manager.create_root_cache(chroot_path) self.cache_manager.create_root_cache(chroot_path)
return results return results
@ -142,7 +135,7 @@ class DebMock:
"""Install a built package in the chroot for chain building""" """Install a built package in the chroot for chain building"""
# Find .deb files in artifacts # Find .deb files in artifacts
deb_files = [art for art in artifacts if art.endswith('.deb')] deb_files = [art for art in artifacts if art.endswith(".deb")]
if not deb_files: if not deb_files:
return return
@ -156,17 +149,11 @@ class DebMock:
# Install in chroot # Install in chroot
self.chroot_manager.execute_in_chroot( self.chroot_manager.execute_in_chroot(
chroot_name, chroot_name, ["dpkg", "-i", chroot_deb_path], capture_output=False
['dpkg', '-i', chroot_deb_path],
capture_output=False
) )
# Clean up # Clean up
self.chroot_manager.execute_in_chroot( self.chroot_manager.execute_in_chroot(chroot_name, ["rm", "-f", chroot_deb_path], capture_output=False)
chroot_name,
['rm', '-f', chroot_deb_path],
capture_output=False
)
except Exception as e: except Exception as e:
# Log warning but continue # Log warning but continue
@ -209,11 +196,7 @@ class DebMock:
raise ChrootError(f"Chroot '{chroot_name}' does not exist") raise ChrootError(f"Chroot '{chroot_name}' does not exist")
# Execute shell in chroot # Execute shell in chroot
self.chroot_manager.execute_in_chroot( self.chroot_manager.execute_in_chroot(chroot_name, ["/bin/bash"], capture_output=False)
chroot_name,
['/bin/bash'],
capture_output=False
)
def copyout(self, source_path: str, dest_path: str, chroot_name: str = None) -> None: def copyout(self, source_path: str, dest_path: str, chroot_name: str = None) -> None:
"""Copy files from chroot to host (similar to Mock's --copyout)""" """Copy files from chroot to host (similar to Mock's --copyout)"""
@ -241,24 +224,25 @@ class DebMock:
"""Capture comprehensive build metadata""" """Capture comprehensive build metadata"""
metadata = { metadata = {
'source_package': source_package, "source_package": source_package,
'build_result': build_result, "build_result": build_result,
'config': self.config.to_dict(), "config": self.config.to_dict(),
'artifacts': build_result.get('artifacts', []), "artifacts": build_result.get("artifacts", []),
'build_metadata': build_result.get('metadata', {}), "build_metadata": build_result.get("metadata", {}),
'timestamp': self._get_timestamp(), "timestamp": self._get_timestamp(),
'build_success': build_result.get('success', False), "build_success": build_result.get("success", False),
'cache_info': self.get_cache_stats() "cache_info": self.get_cache_stats(),
} }
# Add artifact details # Add artifact details
metadata['artifact_details'] = self._get_artifact_details(build_result.get('artifacts', [])) metadata["artifact_details"] = self._get_artifact_details(build_result.get("artifacts", []))
return metadata return metadata
def _get_timestamp(self) -> str: def _get_timestamp(self) -> str:
"""Get current timestamp""" """Get current timestamp"""
from datetime import datetime from datetime import datetime
return datetime.now().isoformat() return datetime.now().isoformat()
def _get_artifact_details(self, artifacts: list) -> list: def _get_artifact_details(self, artifacts: list) -> list:
@ -268,13 +252,15 @@ class DebMock:
for artifact_path in artifacts: for artifact_path in artifacts:
if os.path.exists(artifact_path): if os.path.exists(artifact_path):
stat = os.stat(artifact_path) stat = os.stat(artifact_path)
details.append({ details.append(
'path': artifact_path, {
'name': os.path.basename(artifact_path), "path": artifact_path,
'size': stat.st_size, "name": os.path.basename(artifact_path),
'modified': stat.st_mtime, "size": stat.st_size,
'type': self._get_artifact_type(artifact_path) "modified": stat.st_mtime,
}) "type": self._get_artifact_type(artifact_path),
}
)
return details return details
@ -282,16 +268,16 @@ class DebMock:
"""Determine the type of build artifact""" """Determine the type of build artifact"""
ext = Path(artifact_path).suffix.lower() ext = Path(artifact_path).suffix.lower()
if ext == '.deb': if ext == ".deb":
return 'deb_package' return "deb_package"
elif ext == '.changes': elif ext == ".changes":
return 'changes_file' return "changes_file"
elif ext == '.buildinfo': elif ext == ".buildinfo":
return 'buildinfo_file' return "buildinfo_file"
elif ext == '.dsc': elif ext == ".dsc":
return 'source_package' return "source_package"
else: else:
return 'other' return "other"
def verify_reproducible_build(self, source_package: str, **kwargs) -> Dict[str, Any]: def verify_reproducible_build(self, source_package: str, **kwargs) -> Dict[str, Any]:
"""Verify that a build is reproducible by building twice and comparing results""" """Verify that a build is reproducible by building twice and comparing results"""
@ -300,7 +286,7 @@ class DebMock:
result1 = self.build(source_package, **kwargs) result1 = self.build(source_package, **kwargs)
# Clean chroot for second build # Clean chroot for second build
chroot_name = kwargs.get('chroot_name', self.config.chroot_name) chroot_name = kwargs.get("chroot_name", self.config.chroot_name)
if self.chroot_manager.chroot_exists(chroot_name): if self.chroot_manager.chroot_exists(chroot_name):
self.chroot_manager.clean_chroot(chroot_name) self.chroot_manager.clean_chroot(chroot_name)
@ -311,28 +297,24 @@ class DebMock:
comparison = self._compare_build_results(result1, result2) comparison = self._compare_build_results(result1, result2)
return { return {
'reproducible': comparison['identical'], "reproducible": comparison["identical"],
'first_build': result1, "first_build": result1,
'second_build': result2, "second_build": result2,
'comparison': comparison "comparison": comparison,
} }
def _compare_build_results(self, result1: Dict[str, Any], result2: Dict[str, Any]) -> Dict[str, Any]: def _compare_build_results(self, result1: Dict[str, Any], result2: Dict[str, Any]) -> Dict[str, Any]:
"""Compare two build results for reproducibility""" """Compare two build results for reproducibility"""
comparison = { comparison = {"identical": True, "differences": [], "artifact_comparison": {}}
'identical': True,
'differences': [],
'artifact_comparison': {}
}
# Compare artifacts # Compare artifacts
artifacts1 = set(result1.get('artifacts', [])) artifacts1 = set(result1.get("artifacts", []))
artifacts2 = set(result2.get('artifacts', [])) artifacts2 = set(result2.get("artifacts", []))
if artifacts1 != artifacts2: if artifacts1 != artifacts2:
comparison['identical'] = False comparison["identical"] = False
comparison['differences'].append('Different artifacts produced') comparison["differences"].append("Different artifacts produced")
# Compare individual artifacts # Compare individual artifacts
common_artifacts = artifacts1.intersection(artifacts2) common_artifacts = artifacts1.intersection(artifacts2)
@ -342,15 +324,15 @@ class DebMock:
hash1 = self._get_file_hash(artifact) hash1 = self._get_file_hash(artifact)
hash2 = self._get_file_hash(artifact) hash2 = self._get_file_hash(artifact)
comparison['artifact_comparison'][artifact] = { comparison["artifact_comparison"][artifact] = {
'identical': hash1 == hash2, "identical": hash1 == hash2,
'hash1': hash1, "hash1": hash1,
'hash2': hash2 "hash2": hash2,
} }
if hash1 != hash2: if hash1 != hash2:
comparison['identical'] = False comparison["identical"] = False
comparison['differences'].append(f'Artifact {artifact} differs') comparison["differences"].append(f"Artifact {artifact} differs")
return comparison return comparison
@ -383,18 +365,18 @@ class DebMock:
# Check and install dependencies # Check and install dependencies
deps_check = self.sbuild_wrapper.check_dependencies(source_package, chroot_name) deps_check = self.sbuild_wrapper.check_dependencies(source_package, chroot_name)
if deps_check['missing']: if deps_check["missing"]:
result = self.sbuild_wrapper.install_build_dependencies(deps_check['missing'], chroot_name) result = self.sbuild_wrapper.install_build_dependencies(deps_check["missing"], chroot_name)
return { return {
'success': True, "success": True,
'installed': deps_check['missing'], "installed": deps_check["missing"],
'details': result "details": result,
} }
else: else:
return { return {
'success': True, "success": True,
'installed': [], "installed": [],
'message': 'All dependencies already satisfied' "message": "All dependencies already satisfied",
} }
def install_packages(self, packages: List[str]) -> Dict[str, Any]: def install_packages(self, packages: List[str]) -> Dict[str, Any]:
@ -409,14 +391,14 @@ class DebMock:
result = self.chroot_manager.execute_in_chroot( result = self.chroot_manager.execute_in_chroot(
chroot_name, chroot_name,
f"{self.config.apt_install_command} {' '.join(packages)}", f"{self.config.apt_install_command} {' '.join(packages)}",
as_root=True as_root=True,
) )
return { return {
'success': result['returncode'] == 0, "success": result["returncode"] == 0,
'installed': packages, "installed": packages,
'output': result['stdout'], "output": result["stdout"],
'error': result['stderr'] if result['returncode'] != 0 else None "error": result["stderr"] if result["returncode"] != 0 else None,
} }
def update_packages(self, packages: List[str] = None) -> Dict[str, Any]: def update_packages(self, packages: List[str] = None) -> Dict[str, Any]:
@ -437,10 +419,10 @@ class DebMock:
result = self.chroot_manager.execute_in_chroot(chroot_name, cmd, as_root=True) result = self.chroot_manager.execute_in_chroot(chroot_name, cmd, as_root=True)
return { return {
'success': result['returncode'] == 0, "success": result["returncode"] == 0,
'updated': packages if packages else 'all', "updated": packages if packages else "all",
'output': result['stdout'], "output": result["stdout"],
'error': result['stderr'] if result['returncode'] != 0 else None "error": result["stderr"] if result["returncode"] != 0 else None,
} }
def remove_packages(self, packages: List[str]) -> Dict[str, Any]: def remove_packages(self, packages: List[str]) -> Dict[str, Any]:
@ -456,10 +438,10 @@ class DebMock:
result = self.chroot_manager.execute_in_chroot(chroot_name, cmd, as_root=True) result = self.chroot_manager.execute_in_chroot(chroot_name, cmd, as_root=True)
return { return {
'success': result['returncode'] == 0, "success": result["returncode"] == 0,
'removed': packages, "removed": packages,
'output': result['stdout'], "output": result["stdout"],
'error': result['stderr'] if result['returncode'] != 0 else None "error": result["stderr"] if result["returncode"] != 0 else None,
} }
def execute_apt_command(self, command: str) -> Dict[str, Any]: def execute_apt_command(self, command: str) -> Dict[str, Any]:
@ -475,8 +457,8 @@ class DebMock:
result = self.chroot_manager.execute_in_chroot(chroot_name, cmd, as_root=True) result = self.chroot_manager.execute_in_chroot(chroot_name, cmd, as_root=True)
return { return {
'success': result['returncode'] == 0, "success": result["returncode"] == 0,
'command': command, "command": command,
'output': result['stdout'], "output": result["stdout"],
'error': result['stderr'] if result['returncode'] != 0 else None "error": result["stderr"] if result["returncode"] != 0 else None,
} }

View file

@ -5,10 +5,9 @@ This module provides a comprehensive exception hierarchy inspired by Mock's
exception handling system, adapted for Debian-based build environments. exception handling system, adapted for Debian-based build environments.
""" """
import os
import sys
import functools import functools
from typing import Optional, Dict, Any, List import sys
from typing import Any, Dict, List, Optional
class DebMockError(Exception): class DebMockError(Exception):
@ -20,10 +19,13 @@ class DebMockError(Exception):
and recovery suggestions. and recovery suggestions.
""" """
def __init__(self, message: str, def __init__(
exit_code: int = 1, self,
context: Optional[Dict[str, Any]] = None, message: str,
suggestions: Optional[List[str]] = None): exit_code: int = 1,
context: Optional[Dict[str, Any]] = None,
suggestions: Optional[List[str]] = None,
):
""" """
Initialize the exception with message and optional context. Initialize the exception with message and optional context.
@ -74,18 +76,22 @@ class ConfigurationError(DebMockError):
missing required options, or contain conflicting settings. missing required options, or contain conflicting settings.
""" """
def __init__(self, message: str, config_file: Optional[str] = None, def __init__(
config_section: Optional[str] = None): self,
message: str,
config_file: Optional[str] = None,
config_section: Optional[str] = None,
):
context = {} context = {}
if config_file: if config_file:
context['config_file'] = config_file context["config_file"] = config_file
if config_section: if config_section:
context['config_section'] = config_section context["config_section"] = config_section
suggestions = [ suggestions = [
"Check the configuration file syntax", "Check the configuration file syntax",
"Verify all required options are set", "Verify all required options are set",
"Ensure configuration values are valid for your system" "Ensure configuration values are valid for your system",
] ]
super().__init__(message, exit_code=2, context=context, suggestions=suggestions) super().__init__(message, exit_code=2, context=context, suggestions=suggestions)
@ -98,21 +104,26 @@ class ChrootError(DebMockError):
This exception covers chroot creation, management, and cleanup errors. This exception covers chroot creation, management, and cleanup errors.
""" """
def __init__(self, message: str, chroot_name: Optional[str] = None, def __init__(
operation: Optional[str] = None, chroot_path: Optional[str] = None): self,
message: str,
chroot_name: Optional[str] = None,
operation: Optional[str] = None,
chroot_path: Optional[str] = None,
):
context = {} context = {}
if chroot_name: if chroot_name:
context['chroot_name'] = chroot_name context["chroot_name"] = chroot_name
if operation: if operation:
context['operation'] = operation context["operation"] = operation
if chroot_path: if chroot_path:
context['chroot_path'] = chroot_path context["chroot_path"] = chroot_path
suggestions = [ suggestions = [
"Ensure you have sufficient disk space", "Ensure you have sufficient disk space",
"Check that you have root privileges for chroot operations", "Check that you have root privileges for chroot operations",
"Verify the chroot name is valid", "Verify the chroot name is valid",
"Try cleaning up existing chroots with 'deb-mock clean-chroot'" "Try cleaning up existing chroots with 'deb-mock clean-chroot'",
] ]
super().__init__(message, exit_code=3, context=context, suggestions=suggestions) super().__init__(message, exit_code=3, context=context, suggestions=suggestions)
@ -125,21 +136,26 @@ class SbuildError(DebMockError):
This exception covers sbuild execution, configuration, and result processing. This exception covers sbuild execution, configuration, and result processing.
""" """
def __init__(self, message: str, sbuild_config: Optional[str] = None, def __init__(
build_log: Optional[str] = None, return_code: Optional[int] = None): self,
message: str,
sbuild_config: Optional[str] = None,
build_log: Optional[str] = None,
return_code: Optional[int] = None,
):
context = {} context = {}
if sbuild_config: if sbuild_config:
context['sbuild_config'] = sbuild_config context["sbuild_config"] = sbuild_config
if build_log: if build_log:
context['build_log'] = build_log context["build_log"] = build_log
if return_code is not None: if return_code is not None:
context['return_code'] = return_code context["return_code"] = return_code
suggestions = [ suggestions = [
"Check the build log for detailed error information", "Check the build log for detailed error information",
"Verify that sbuild is properly configured", "Verify that sbuild is properly configured",
"Ensure all build dependencies are available", "Ensure all build dependencies are available",
"Try updating the chroot with 'deb-mock update-chroot'" "Try updating the chroot with 'deb-mock update-chroot'",
] ]
super().__init__(message, exit_code=4, context=context, suggestions=suggestions) super().__init__(message, exit_code=4, context=context, suggestions=suggestions)
@ -153,21 +169,26 @@ class BuildError(DebMockError):
compilation errors, missing dependencies, or other build issues. compilation errors, missing dependencies, or other build issues.
""" """
def __init__(self, message: str, source_package: Optional[str] = None, def __init__(
build_log: Optional[str] = None, artifacts: Optional[List[str]] = None): self,
message: str,
source_package: Optional[str] = None,
build_log: Optional[str] = None,
artifacts: Optional[List[str]] = None,
):
context = {} context = {}
if source_package: if source_package:
context['source_package'] = source_package context["source_package"] = source_package
if build_log: if build_log:
context['build_log'] = build_log context["build_log"] = build_log
if artifacts: if artifacts:
context['artifacts'] = artifacts context["artifacts"] = artifacts
suggestions = [ suggestions = [
"Review the build log for specific error messages", "Review the build log for specific error messages",
"Check that all build dependencies are installed", "Check that all build dependencies are installed",
"Verify the source package is valid and complete", "Verify the source package is valid and complete",
"Try building with verbose output: 'deb-mock --verbose build'" "Try building with verbose output: 'deb-mock --verbose build'",
] ]
super().__init__(message, exit_code=5, context=context, suggestions=suggestions) super().__init__(message, exit_code=5, context=context, suggestions=suggestions)
@ -181,19 +202,23 @@ class DependencyError(DebMockError):
and other dependency-related problems. and other dependency-related problems.
""" """
def __init__(self, message: str, missing_packages: Optional[List[str]] = None, def __init__(
conflicting_packages: Optional[List[str]] = None): self,
message: str,
missing_packages: Optional[List[str]] = None,
conflicting_packages: Optional[List[str]] = None,
):
context = {} context = {}
if missing_packages: if missing_packages:
context['missing_packages'] = missing_packages context["missing_packages"] = missing_packages
if conflicting_packages: if conflicting_packages:
context['conflicting_packages'] = conflicting_packages context["conflicting_packages"] = conflicting_packages
suggestions = [ suggestions = [
"Install missing build dependencies", "Install missing build dependencies",
"Resolve package conflicts by updating or removing conflicting packages", "Resolve package conflicts by updating or removing conflicting packages",
"Check that your chroot has access to the required repositories", "Check that your chroot has access to the required repositories",
"Try updating the chroot: 'deb-mock update-chroot'" "Try updating the chroot: 'deb-mock update-chroot'",
] ]
super().__init__(message, exit_code=6, context=context, suggestions=suggestions) super().__init__(message, exit_code=6, context=context, suggestions=suggestions)
@ -206,18 +231,22 @@ class MetadataError(DebMockError):
This exception covers metadata capture, storage, and retrieval errors. This exception covers metadata capture, storage, and retrieval errors.
""" """
def __init__(self, message: str, metadata_file: Optional[str] = None, def __init__(
operation: Optional[str] = None): self,
message: str,
metadata_file: Optional[str] = None,
operation: Optional[str] = None,
):
context = {} context = {}
if metadata_file: if metadata_file:
context['metadata_file'] = metadata_file context["metadata_file"] = metadata_file
if operation: if operation:
context['operation'] = operation context["operation"] = operation
suggestions = [ suggestions = [
"Check that the metadata directory is writable", "Check that the metadata directory is writable",
"Verify that the metadata file format is valid", "Verify that the metadata file format is valid",
"Ensure sufficient disk space for metadata storage" "Ensure sufficient disk space for metadata storage",
] ]
super().__init__(message, exit_code=7, context=context, suggestions=suggestions) super().__init__(message, exit_code=7, context=context, suggestions=suggestions)
@ -230,21 +259,26 @@ class CacheError(DebMockError):
This exception covers root cache, package cache, and ccache errors. This exception covers root cache, package cache, and ccache errors.
""" """
def __init__(self, message: str, cache_type: Optional[str] = None, def __init__(
cache_path: Optional[str] = None, operation: Optional[str] = None): self,
message: str,
cache_type: Optional[str] = None,
cache_path: Optional[str] = None,
operation: Optional[str] = None,
):
context = {} context = {}
if cache_type: if cache_type:
context['cache_type'] = cache_type context["cache_type"] = cache_type
if cache_path: if cache_path:
context['cache_path'] = cache_path context["cache_path"] = cache_path
if operation: if operation:
context['operation'] = operation context["operation"] = operation
suggestions = [ suggestions = [
"Check that cache directories are writable", "Check that cache directories are writable",
"Ensure sufficient disk space for cache operations", "Ensure sufficient disk space for cache operations",
"Try cleaning up old caches: 'deb-mock cleanup-caches'", "Try cleaning up old caches: 'deb-mock cleanup-caches'",
"Verify cache configuration settings" "Verify cache configuration settings",
] ]
super().__init__(message, exit_code=8, context=context, suggestions=suggestions) super().__init__(message, exit_code=8, context=context, suggestions=suggestions)
@ -257,19 +291,23 @@ class PluginError(DebMockError):
This exception covers plugin loading, configuration, and execution errors. This exception covers plugin loading, configuration, and execution errors.
""" """
def __init__(self, message: str, plugin_name: Optional[str] = None, def __init__(
plugin_config: Optional[Dict[str, Any]] = None): self,
message: str,
plugin_name: Optional[str] = None,
plugin_config: Optional[Dict[str, Any]] = None,
):
context = {} context = {}
if plugin_name: if plugin_name:
context['plugin_name'] = plugin_name context["plugin_name"] = plugin_name
if plugin_config: if plugin_config:
context['plugin_config'] = plugin_config context["plugin_config"] = plugin_config
suggestions = [ suggestions = [
"Check that the plugin is properly installed", "Check that the plugin is properly installed",
"Verify plugin configuration is valid", "Verify plugin configuration is valid",
"Ensure plugin dependencies are satisfied", "Ensure plugin dependencies are satisfied",
"Try disabling the plugin if it's causing issues" "Try disabling the plugin if it's causing issues",
] ]
super().__init__(message, exit_code=9, context=context, suggestions=suggestions) super().__init__(message, exit_code=9, context=context, suggestions=suggestions)
@ -283,21 +321,26 @@ class NetworkError(DebMockError):
other network operations. other network operations.
""" """
def __init__(self, message: str, url: Optional[str] = None, def __init__(
proxy: Optional[str] = None, timeout: Optional[int] = None): self,
message: str,
url: Optional[str] = None,
proxy: Optional[str] = None,
timeout: Optional[int] = None,
):
context = {} context = {}
if url: if url:
context['url'] = url context["url"] = url
if proxy: if proxy:
context['proxy'] = proxy context["proxy"] = proxy
if timeout: if timeout:
context['timeout'] = timeout context["timeout"] = timeout
suggestions = [ suggestions = [
"Check your internet connection", "Check your internet connection",
"Verify repository URLs are accessible", "Verify repository URLs are accessible",
"Configure proxy settings if behind a firewall", "Configure proxy settings if behind a firewall",
"Try using a different mirror or repository" "Try using a different mirror or repository",
] ]
super().__init__(message, exit_code=10, context=context, suggestions=suggestions) super().__init__(message, exit_code=10, context=context, suggestions=suggestions)
@ -311,21 +354,26 @@ class PermissionError(DebMockError):
file access, and other permission issues. file access, and other permission issues.
""" """
def __init__(self, message: str, operation: Optional[str] = None, def __init__(
path: Optional[str] = None, required_privileges: Optional[str] = None): self,
message: str,
operation: Optional[str] = None,
path: Optional[str] = None,
required_privileges: Optional[str] = None,
):
context = {} context = {}
if operation: if operation:
context['operation'] = operation context["operation"] = operation
if path: if path:
context['path'] = path context["path"] = path
if required_privileges: if required_privileges:
context['required_privileges'] = required_privileges context["required_privileges"] = required_privileges
suggestions = [ suggestions = [
"Run the command with appropriate privileges (sudo)", "Run the command with appropriate privileges (sudo)",
"Check file and directory permissions", "Check file and directory permissions",
"Verify your user is in the required groups", "Verify your user is in the required groups",
"Ensure the target paths are writable" "Ensure the target paths are writable",
] ]
super().__init__(message, exit_code=11, context=context, suggestions=suggestions) super().__init__(message, exit_code=11, context=context, suggestions=suggestions)
@ -339,21 +387,26 @@ class ValidationError(DebMockError):
and other input data. and other input data.
""" """
def __init__(self, message: str, field: Optional[str] = None, def __init__(
value: Optional[str] = None, expected_format: Optional[str] = None): self,
message: str,
field: Optional[str] = None,
value: Optional[str] = None,
expected_format: Optional[str] = None,
):
context = {} context = {}
if field: if field:
context['field'] = field context["field"] = field
if value: if value:
context['value'] = value context["value"] = value
if expected_format: if expected_format:
context['expected_format'] = expected_format context["expected_format"] = expected_format
suggestions = [ suggestions = [
"Check the input format and syntax", "Check the input format and syntax",
"Verify that required fields are provided", "Verify that required fields are provided",
"Ensure values are within acceptable ranges", "Ensure values are within acceptable ranges",
"Review the documentation for correct usage" "Review the documentation for correct usage",
] ]
super().__init__(message, exit_code=12, context=context, suggestions=suggestions) super().__init__(message, exit_code=12, context=context, suggestions=suggestions)
@ -367,6 +420,7 @@ def handle_exception(func):
This decorator catches DebMockError exceptions and provides This decorator catches DebMockError exceptions and provides
formatted error output with suggestions for resolution. formatted error output with suggestions for resolution.
""" """
@functools.wraps(func) @functools.wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
try: try:
@ -378,15 +432,16 @@ def handle_exception(func):
# Convert unexpected exceptions to DebMockError # Convert unexpected exceptions to DebMockError
error = DebMockError( error = DebMockError(
f"Unexpected error: {str(e)}", f"Unexpected error: {str(e)}",
context={'exception_type': type(e).__name__}, context={"exception_type": type(e).__name__},
suggestions=[ suggestions=[
"This may be a bug in deb-mock", "This may be a bug in deb-mock",
"Check the logs for more details", "Check the logs for more details",
"Report the issue with full error context" "Report the issue with full error context",
] ],
) )
error.print_error() error.print_error()
sys.exit(1) sys.exit(1)
return wrapper return wrapper

View file

@ -2,12 +2,12 @@
Metadata management for deb-mock Metadata management for deb-mock
""" """
import os
import json import json
import uuid import uuid
from pathlib import Path
from typing import Dict, Any, List, Optional
from datetime import datetime from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from .exceptions import MetadataError from .exceptions import MetadataError
@ -26,14 +26,14 @@ class MetadataManager:
build_id = self._generate_build_id() build_id = self._generate_build_id()
# Add build ID to metadata # Add build ID to metadata
metadata['build_id'] = build_id metadata["build_id"] = build_id
metadata['stored_at'] = datetime.now().isoformat() metadata["stored_at"] = datetime.now().isoformat()
# Create metadata file # Create metadata file
metadata_file = self.metadata_dir / f"{build_id}.json" metadata_file = self.metadata_dir / f"{build_id}.json"
try: try:
with open(metadata_file, 'w') as f: with open(metadata_file, "w") as f:
json.dump(metadata, f, indent=2, default=str) json.dump(metadata, f, indent=2, default=str)
except Exception as e: except Exception as e:
raise MetadataError(f"Failed to store metadata: {e}") raise MetadataError(f"Failed to store metadata: {e}")
@ -52,7 +52,7 @@ class MetadataManager:
return None return None
try: try:
with open(metadata_file, 'r') as f: with open(metadata_file, "r") as f:
return json.load(f) return json.load(f)
except Exception as e: except Exception as e:
raise MetadataError(f"Failed to load metadata for build {build_id}: {e}") raise MetadataError(f"Failed to load metadata for build {build_id}: {e}")
@ -68,17 +68,13 @@ class MetadataManager:
return builds return builds
try: try:
with open(index_file, 'r') as f: with open(index_file, "r") as f:
build_index = json.load(f) build_index = json.load(f)
except Exception as e: except Exception as e:
raise MetadataError(f"Failed to load build index: {e}") raise MetadataError(f"Failed to load build index: {e}")
# Sort builds by timestamp (newest first) # Sort builds by timestamp (newest first)
sorted_builds = sorted( sorted_builds = sorted(build_index.values(), key=lambda x: x.get("timestamp", ""), reverse=True)
build_index.values(),
key=lambda x: x.get('timestamp', ''),
reverse=True
)
# Apply limit if specified # Apply limit if specified
if limit: if limit:
@ -86,7 +82,7 @@ class MetadataManager:
# Load full metadata for each build # Load full metadata for each build
for build_info in sorted_builds: for build_info in sorted_builds:
build_id = build_info.get('build_id') build_id = build_info.get("build_id")
if build_id: if build_id:
full_metadata = self.get_build_info(build_id) full_metadata = self.get_build_info(build_id)
if full_metadata: if full_metadata:
@ -130,8 +126,8 @@ class MetadataManager:
all_builds = self.get_build_history() all_builds = self.get_build_history()
for build in all_builds: for build in all_builds:
build_id = build.get('build_id') build_id = build.get("build_id")
timestamp = build.get('timestamp') timestamp = build.get("timestamp")
if timestamp: if timestamp:
try: try:
@ -145,17 +141,18 @@ class MetadataManager:
return deleted_count return deleted_count
def export_metadata(self, build_id: str, format: str = 'json') -> str: def export_metadata(self, build_id: str, format: str = "json") -> str:
"""Export build metadata in specified format""" """Export build metadata in specified format"""
metadata = self.get_build_info(build_id) metadata = self.get_build_info(build_id)
if not metadata: if not metadata:
raise MetadataError(f"Build {build_id} not found") raise MetadataError(f"Build {build_id} not found")
if format.lower() == 'json': if format.lower() == "json":
return json.dumps(metadata, indent=2, default=str) return json.dumps(metadata, indent=2, default=str)
elif format.lower() == 'yaml': elif format.lower() == "yaml":
import yaml import yaml
return yaml.dump(metadata, default_flow_style=False) return yaml.dump(metadata, default_flow_style=False)
else: else:
raise MetadataError(f"Unsupported export format: {format}") raise MetadataError(f"Unsupported export format: {format}")
@ -173,26 +170,26 @@ class MetadataManager:
build_index = {} build_index = {}
if index_file.exists(): if index_file.exists():
try: try:
with open(index_file, 'r') as f: with open(index_file, "r") as f:
build_index = json.load(f) build_index = json.load(f)
except Exception: except Exception:
build_index = {} build_index = {}
# Add new build to index # Add new build to index
build_index[build_id] = { build_index[build_id] = {
'build_id': build_id, "build_id": build_id,
'source_package': metadata.get('source_package', ''), "source_package": metadata.get("source_package", ""),
'timestamp': metadata.get('timestamp', ''), "timestamp": metadata.get("timestamp", ""),
'build_success': metadata.get('build_success', False), "build_success": metadata.get("build_success", False),
'package_name': metadata.get('build_metadata', {}).get('package_name', ''), "package_name": metadata.get("build_metadata", {}).get("package_name", ""),
'package_version': metadata.get('build_metadata', {}).get('package_version', ''), "package_version": metadata.get("build_metadata", {}).get("package_version", ""),
'architecture': metadata.get('build_metadata', {}).get('architecture', ''), "architecture": metadata.get("build_metadata", {}).get("architecture", ""),
'suite': metadata.get('build_metadata', {}).get('suite', '') "suite": metadata.get("build_metadata", {}).get("suite", ""),
} }
# Save updated index # Save updated index
try: try:
with open(index_file, 'w') as f: with open(index_file, "w") as f:
json.dump(build_index, f, indent=2, default=str) json.dump(build_index, f, indent=2, default=str)
except Exception as e: except Exception as e:
raise MetadataError(f"Failed to update build index: {e}") raise MetadataError(f"Failed to update build index: {e}")
@ -206,7 +203,7 @@ class MetadataManager:
return return
try: try:
with open(index_file, 'r') as f: with open(index_file, "r") as f:
build_index = json.load(f) build_index = json.load(f)
except Exception: except Exception:
return return
@ -215,7 +212,7 @@ class MetadataManager:
del build_index[build_id] del build_index[build_id]
try: try:
with open(index_file, 'w') as f: with open(index_file, "w") as f:
json.dump(build_index, f, indent=2, default=str) json.dump(build_index, f, indent=2, default=str)
except Exception as e: except Exception as e:
raise MetadataError(f"Failed to update build index: {e}") raise MetadataError(f"Failed to update build index: {e}")
@ -224,24 +221,24 @@ class MetadataManager:
"""Check if a build matches the given criteria""" """Check if a build matches the given criteria"""
for key, value in criteria.items(): for key, value in criteria.items():
if key == 'package_name': if key == "package_name":
build_package = build.get('build_metadata', {}).get('package_name', '') build_package = build.get("build_metadata", {}).get("package_name", "")
if value.lower() not in build_package.lower(): if value.lower() not in build_package.lower():
return False return False
elif key == 'architecture': elif key == "architecture":
build_arch = build.get('build_metadata', {}).get('architecture', '') build_arch = build.get("build_metadata", {}).get("architecture", "")
if value.lower() != build_arch.lower(): if value.lower() != build_arch.lower():
return False return False
elif key == 'suite': elif key == "suite":
build_suite = build.get('build_metadata', {}).get('suite', '') build_suite = build.get("build_metadata", {}).get("suite", "")
if value.lower() != build_suite.lower(): if value.lower() != build_suite.lower():
return False return False
elif key == 'success': elif key == "success":
build_success = build.get('build_success', False) build_success = build.get("build_success", False)
if value != build_success: if value != build_success:
return False return False
elif key == 'date_after': elif key == "date_after":
build_timestamp = build.get('timestamp', '') build_timestamp = build.get("timestamp", "")
if build_timestamp: if build_timestamp:
try: try:
build_time = datetime.fromisoformat(build_timestamp) build_time = datetime.fromisoformat(build_timestamp)
@ -250,8 +247,8 @@ class MetadataManager:
return False return False
except ValueError: except ValueError:
return False return False
elif key == 'date_before': elif key == "date_before":
build_timestamp = build.get('timestamp', '') build_timestamp = build.get("timestamp", "")
if build_timestamp: if build_timestamp:
try: try:
build_time = datetime.fromisoformat(build_timestamp) build_time = datetime.fromisoformat(build_timestamp)

View file

@ -6,7 +6,6 @@ inspired by Fedora's Mock plugin architecture but adapted for Debian-based syste
""" """
from .hook_manager import HookManager from .hook_manager import HookManager
from .base import BasePlugin
from .registry import PluginRegistry from .registry import PluginRegistry
# Global hook manager instance # Global hook manager instance
@ -15,6 +14,7 @@ hook_manager = HookManager()
# Global plugin registry # Global plugin registry
plugin_registry = PluginRegistry() plugin_registry = PluginRegistry()
# Convenience function for plugins to register hooks # Convenience function for plugins to register hooks
def add_hook(hook_name: str, callback): def add_hook(hook_name: str, callback):
""" """
@ -29,6 +29,7 @@ def add_hook(hook_name: str, callback):
""" """
hook_manager.add_hook(hook_name, callback) hook_manager.add_hook(hook_name, callback)
# Convenience function to call hooks # Convenience function to call hooks
def call_hook(hook_name: str, context: dict = None): def call_hook(hook_name: str, context: dict = None):
""" """
@ -40,6 +41,7 @@ def call_hook(hook_name: str, context: dict = None):
""" """
hook_manager.call_hook(hook_name, context) hook_manager.call_hook(hook_name, context)
# Convenience function to get available hooks # Convenience function to get available hooks
def get_hook_names() -> list: def get_hook_names() -> list:
""" """
@ -50,6 +52,7 @@ def get_hook_names() -> list:
""" """
return hook_manager.get_hook_names() return hook_manager.get_hook_names()
# Convenience function to register plugins # Convenience function to register plugins
def register_plugin(plugin_name: str, plugin_class): def register_plugin(plugin_name: str, plugin_class):
""" """
@ -61,6 +64,7 @@ def register_plugin(plugin_name: str, plugin_class):
""" """
plugin_registry.register(plugin_name, plugin_class) plugin_registry.register(plugin_name, plugin_class)
# Convenience function to get registered plugins # Convenience function to get registered plugins
def get_registered_plugins() -> dict: def get_registered_plugins() -> dict:
""" """
@ -71,6 +75,7 @@ def get_registered_plugins() -> dict:
""" """
return plugin_registry.get_plugins() return plugin_registry.get_plugins()
# Convenience function to create plugin instances # Convenience function to create plugin instances
def create_plugin(plugin_name: str, config): def create_plugin(plugin_name: str, config):
""" """

View file

@ -6,7 +6,7 @@ inspired by Fedora's Mock plugin architecture but adapted for Debian-based syste
""" """
import logging import logging
from typing import Dict, Any, Optional from typing import Any, Dict
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -48,15 +48,15 @@ class BasePlugin:
Returns: Returns:
True if plugin is enabled, False otherwise True if plugin is enabled, False otherwise
""" """
plugin_config = getattr(self.config, 'plugins', {}) plugin_config = getattr(self.config, "plugins", {})
plugin_name = self.plugin_name plugin_name = self.plugin_name
# Check if plugin is explicitly enabled # Check if plugin is explicitly enabled
if plugin_name in plugin_config: if plugin_name in plugin_config:
return plugin_config[plugin_name].get('enabled', False) return plugin_config[plugin_name].get("enabled", False)
# Check if plugin is enabled via global plugin settings # Check if plugin is enabled via global plugin settings
return getattr(self.config, 'enable_plugins', {}).get(plugin_name, False) return getattr(self.config, "enable_plugins", {}).get(plugin_name, False)
def _register_hooks(self): def _register_hooks(self):
""" """
@ -74,7 +74,7 @@ class BasePlugin:
Returns: Returns:
Plugin configuration dictionary Plugin configuration dictionary
""" """
plugin_config = getattr(self.config, 'plugins', {}) plugin_config = getattr(self.config, "plugins", {})
return plugin_config.get(self.plugin_name, {}) return plugin_config.get(self.plugin_name, {})
def _log_info(self, message: str): def _log_info(self, message: str):
@ -407,8 +407,8 @@ class BasePlugin:
Dictionary with plugin information Dictionary with plugin information
""" """
return { return {
'name': self.plugin_name, "name": self.plugin_name,
'class': self.__class__.__name__, "class": self.__class__.__name__,
'enabled': self.enabled, "enabled": self.enabled,
'docstring': self.__class__.__doc__ or 'No documentation available' "docstring": self.__class__.__doc__ or "No documentation available",
} }

View file

@ -5,11 +5,11 @@ This plugin allows mounting host directories into chroot environments,
inspired by Fedora's Mock bind_mount plugin but adapted for Debian-based systems. inspired by Fedora's Mock bind_mount plugin but adapted for Debian-based systems.
""" """
import logging
import os import os
import subprocess import subprocess
import logging
from pathlib import Path from pathlib import Path
from typing import Dict, Any, List, Tuple from typing import Any, Dict, List, Tuple
from .base import BasePlugin from .base import BasePlugin
@ -49,11 +49,11 @@ class BindMountPlugin(BasePlugin):
mounts = [] mounts = []
# Get mounts from configuration # Get mounts from configuration
if 'mounts' in plugin_config: if "mounts" in plugin_config:
for mount_config in plugin_config['mounts']: for mount_config in plugin_config["mounts"]:
if isinstance(mount_config, dict): if isinstance(mount_config, dict):
host_path = mount_config.get('host_path') host_path = mount_config.get("host_path")
chroot_path = mount_config.get('chroot_path') chroot_path = mount_config.get("chroot_path")
elif isinstance(mount_config, (list, tuple)) and len(mount_config) >= 2: elif isinstance(mount_config, (list, tuple)) and len(mount_config) >= 2:
host_path = mount_config[0] host_path = mount_config[0]
chroot_path = mount_config[1] chroot_path = mount_config[1]
@ -65,8 +65,8 @@ class BindMountPlugin(BasePlugin):
mounts.append((host_path, chroot_path)) mounts.append((host_path, chroot_path))
# Legacy support for 'dirs' configuration (Mock compatibility) # Legacy support for 'dirs' configuration (Mock compatibility)
if 'dirs' in plugin_config: if "dirs" in plugin_config:
for host_path, chroot_path in plugin_config['dirs']: for host_path, chroot_path in plugin_config["dirs"]:
mounts.append((host_path, chroot_path)) mounts.append((host_path, chroot_path))
return mounts return mounts
@ -81,7 +81,7 @@ class BindMountPlugin(BasePlugin):
if not self.enabled or not self.mounts: if not self.enabled or not self.mounts:
return return
chroot_path = context.get('chroot_path') chroot_path = context.get("chroot_path")
if not chroot_path: if not chroot_path:
self._log_warning("No chroot_path in context, skipping bind mounts") self._log_warning("No chroot_path in context, skipping bind mounts")
return return
@ -104,7 +104,7 @@ class BindMountPlugin(BasePlugin):
if not self.enabled or not self.mounts: if not self.enabled or not self.mounts:
return return
chroot_path = context.get('chroot_path') chroot_path = context.get("chroot_path")
if not chroot_path: if not chroot_path:
self._log_warning("No chroot_path in context, skipping bind mount cleanup") self._log_warning("No chroot_path in context, skipping bind mount cleanup")
return return
@ -132,7 +132,7 @@ class BindMountPlugin(BasePlugin):
return return
# Create full chroot mount path # Create full chroot mount path
full_chroot_path = os.path.join(chroot_path, chroot_mount_path.lstrip('/')) full_chroot_path = os.path.join(chroot_path, chroot_mount_path.lstrip("/"))
# Create mount point directory if it doesn't exist # Create mount point directory if it doesn't exist
mount_point_dir = os.path.dirname(full_chroot_path) mount_point_dir = os.path.dirname(full_chroot_path)
@ -147,8 +147,8 @@ class BindMountPlugin(BasePlugin):
# Perform the bind mount # Perform the bind mount
try: try:
cmd = ['mount', '--bind', host_path, full_chroot_path] cmd = ["mount", "--bind", host_path, full_chroot_path]
result = subprocess.run(cmd, capture_output=True, text=True, check=True) subprocess.run(cmd, capture_output=True, text=True, check=True)
self._log_debug(f"Successfully mounted {host_path} -> {full_chroot_path}") self._log_debug(f"Successfully mounted {host_path} -> {full_chroot_path}")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@ -166,18 +166,18 @@ class BindMountPlugin(BasePlugin):
chroot_mount_path: Path in the chroot that was mounted chroot_mount_path: Path in the chroot that was mounted
chroot_path: Base chroot path chroot_path: Base chroot path
""" """
full_chroot_path = os.path.join(chroot_path, chroot_mount_path.lstrip('/')) full_chroot_path = os.path.join(chroot_path, chroot_mount_path.lstrip("/"))
try: try:
cmd = ['umount', full_chroot_path] cmd = ["umount", full_chroot_path]
result = subprocess.run(cmd, capture_output=True, text=True, check=True) subprocess.run(cmd, capture_output=True, text=True, check=True)
self._log_debug(f"Successfully unmounted: {full_chroot_path}") self._log_debug(f"Successfully unmounted: {full_chroot_path}")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError:
# Try force unmount if regular unmount fails # Try force unmount if regular unmount fails
try: try:
cmd = ['umount', '-f', full_chroot_path] cmd = ["umount", "-f", full_chroot_path]
result = subprocess.run(cmd, capture_output=True, text=True, check=True) subprocess.run(cmd, capture_output=True, text=True, check=True)
self._log_debug(f"Successfully force unmounted: {full_chroot_path}") self._log_debug(f"Successfully force unmounted: {full_chroot_path}")
except subprocess.CalledProcessError as e2: except subprocess.CalledProcessError as e2:
self._log_warning(f"Failed to unmount {full_chroot_path}: {e2.stderr}") self._log_warning(f"Failed to unmount {full_chroot_path}: {e2.stderr}")
@ -194,13 +194,13 @@ class BindMountPlugin(BasePlugin):
Returns: Returns:
True if configuration is valid, False otherwise True if configuration is valid, False otherwise
""" """
plugin_config = getattr(config, 'plugins', {}).get('bind_mount', {}) plugin_config = getattr(config, "plugins", {}).get("bind_mount", {})
# Check mounts configuration # Check mounts configuration
if 'mounts' in plugin_config: if "mounts" in plugin_config:
for mount_config in plugin_config['mounts']: for mount_config in plugin_config["mounts"]:
if isinstance(mount_config, dict): if isinstance(mount_config, dict):
if not all(key in mount_config for key in ['host_path', 'chroot_path']): if not all(key in mount_config for key in ["host_path", "chroot_path"]):
self._log_error("Mount configuration missing required keys: host_path, chroot_path") self._log_error("Mount configuration missing required keys: host_path, chroot_path")
return False return False
elif isinstance(mount_config, (list, tuple)): elif isinstance(mount_config, (list, tuple)):
@ -212,8 +212,8 @@ class BindMountPlugin(BasePlugin):
return False return False
# Check dirs configuration (legacy) # Check dirs configuration (legacy)
if 'dirs' in plugin_config: if "dirs" in plugin_config:
for host_path, chroot_path in plugin_config['dirs']: for host_path, chroot_path in plugin_config["dirs"]:
if not host_path or not chroot_path: if not host_path or not chroot_path:
self._log_error("Invalid dirs configuration: host_path and chroot_path must be non-empty") self._log_error("Invalid dirs configuration: host_path and chroot_path must be non-empty")
return False return False
@ -228,9 +228,11 @@ class BindMountPlugin(BasePlugin):
Dictionary with plugin information Dictionary with plugin information
""" """
info = super().get_plugin_info() info = super().get_plugin_info()
info.update({ info.update(
'mounts': self.mounts, {
'mount_count': len(self.mounts), "mounts": self.mounts,
'hooks': ['mount_root', 'postumount'] "mount_count": len(self.mounts),
}) "hooks": ["mount_root", "postumount"],
}
)
return info return info

View file

@ -5,11 +5,11 @@ This plugin compresses build logs to save disk space,
inspired by Fedora's Mock compress_logs plugin but adapted for Debian-based systems. inspired by Fedora's Mock compress_logs plugin but adapted for Debian-based systems.
""" """
import logging
import os import os
import subprocess import subprocess
import logging
from pathlib import Path from pathlib import Path
from typing import Dict, Any, List from typing import Any, Dict, List
from .base import BasePlugin from .base import BasePlugin
@ -45,12 +45,12 @@ class CompressLogsPlugin(BasePlugin):
plugin_config = self._get_plugin_config() plugin_config = self._get_plugin_config()
return { return {
'method': plugin_config.get('compression', 'gzip'), "method": plugin_config.get("compression", "gzip"),
'level': plugin_config.get('level', 9), "level": plugin_config.get("level", 9),
'extensions': plugin_config.get('extensions', ['.log']), "extensions": plugin_config.get("extensions", [".log"]),
'exclude_patterns': plugin_config.get('exclude_patterns', []), "exclude_patterns": plugin_config.get("exclude_patterns", []),
'min_size': plugin_config.get('min_size', 0), # Minimum file size to compress "min_size": plugin_config.get("min_size", 0), # Minimum file size to compress
'command': plugin_config.get('command', None) # Custom compression command "command": plugin_config.get("command", None), # Custom compression command
} }
def process_logs(self, context: Dict[str, Any]) -> None: def process_logs(self, context: Dict[str, Any]) -> None:
@ -63,7 +63,7 @@ class CompressLogsPlugin(BasePlugin):
if not self.enabled: if not self.enabled:
return return
log_dir = context.get('log_dir') log_dir = context.get("log_dir")
if not log_dir: if not log_dir:
self._log_warning("No log_dir in context, skipping log compression") self._log_warning("No log_dir in context, skipping log compression")
return return
@ -82,7 +82,7 @@ class CompressLogsPlugin(BasePlugin):
original_size = os.path.getsize(log_file) original_size = os.path.getsize(log_file)
# Check minimum size requirement # Check minimum size requirement
if original_size < self.compression['min_size']: if original_size < self.compression["min_size"]:
self._log_debug(f"Skipping {log_file} (size {original_size} < {self.compression['min_size']})") self._log_debug(f"Skipping {log_file} (size {original_size} < {self.compression['min_size']})")
continue continue
@ -97,7 +97,9 @@ class CompressLogsPlugin(BasePlugin):
compressed_count += 1 compressed_count += 1
size_saved = original_size - compressed_size size_saved = original_size - compressed_size
total_size_saved += size_saved total_size_saved += size_saved
self._log_debug(f"Compressed {log_file}: {original_size} -> {compressed_size} bytes (saved {size_saved})") self._log_debug(
f"Compressed {log_file}: {original_size} -> {compressed_size} bytes (saved {size_saved})"
)
except Exception as e: except Exception as e:
self._log_error(f"Failed to compress {log_file}: {e}") self._log_error(f"Failed to compress {log_file}: {e}")
@ -116,7 +118,7 @@ class CompressLogsPlugin(BasePlugin):
""" """
log_files = [] log_files = []
for extension in self.compression['extensions']: for extension in self.compression["extensions"]:
pattern = f"*{extension}" pattern = f"*{extension}"
log_files.extend(Path(log_dir).glob(pattern)) log_files.extend(Path(log_dir).glob(pattern))
@ -138,7 +140,7 @@ class CompressLogsPlugin(BasePlugin):
Returns: Returns:
True if file should be excluded, False otherwise True if file should be excluded, False otherwise
""" """
for pattern in self.compression['exclude_patterns']: for pattern in self.compression["exclude_patterns"]:
if pattern in filename: if pattern in filename:
return True return True
return False return False
@ -153,7 +155,7 @@ class CompressLogsPlugin(BasePlugin):
Returns: Returns:
True if file is already compressed, False otherwise True if file is already compressed, False otherwise
""" """
compressed_extensions = ['.gz', '.bz2', '.xz', '.lzma', '.zst'] compressed_extensions = [".gz", ".bz2", ".xz", ".lzma", ".zst"]
return any(file_path.endswith(ext) for ext in compressed_extensions) return any(file_path.endswith(ext) for ext in compressed_extensions)
def _compress_file(self, file_path: str) -> int: def _compress_file(self, file_path: str) -> int:
@ -166,21 +168,21 @@ class CompressLogsPlugin(BasePlugin):
Returns: Returns:
Size of the compressed file, or None if compression failed Size of the compressed file, or None if compression failed
""" """
method = self.compression['method'] method = self.compression["method"]
level = self.compression['level'] level = self.compression["level"]
# Use custom command if specified # Use custom command if specified
if self.compression['command']: if self.compression["command"]:
return self._compress_with_custom_command(file_path) return self._compress_with_custom_command(file_path)
# Use standard compression methods # Use standard compression methods
if method == 'gzip': if method == "gzip":
return self._compress_gzip(file_path, level) return self._compress_gzip(file_path, level)
elif method == 'bzip2': elif method == "bzip2":
return self._compress_bzip2(file_path, level) return self._compress_bzip2(file_path, level)
elif method == 'xz': elif method == "xz":
return self._compress_xz(file_path, level) return self._compress_xz(file_path, level)
elif method == 'zstd': elif method == "zstd":
return self._compress_zstd(file_path, level) return self._compress_zstd(file_path, level)
else: else:
self._log_error(f"Unsupported compression method: {method}") self._log_error(f"Unsupported compression method: {method}")
@ -189,8 +191,8 @@ class CompressLogsPlugin(BasePlugin):
def _compress_gzip(self, file_path: str, level: int) -> int: def _compress_gzip(self, file_path: str, level: int) -> int:
"""Compress file using gzip.""" """Compress file using gzip."""
try: try:
cmd = ['gzip', f'-{level}', file_path] cmd = ["gzip", f"-{level}", file_path]
result = subprocess.run(cmd, capture_output=True, text=True, check=True) subprocess.run(cmd, capture_output=True, text=True, check=True)
compressed_path = f"{file_path}.gz" compressed_path = f"{file_path}.gz"
return os.path.getsize(compressed_path) if os.path.exists(compressed_path) else None return os.path.getsize(compressed_path) if os.path.exists(compressed_path) else None
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@ -200,8 +202,8 @@ class CompressLogsPlugin(BasePlugin):
def _compress_bzip2(self, file_path: str, level: int) -> int: def _compress_bzip2(self, file_path: str, level: int) -> int:
"""Compress file using bzip2.""" """Compress file using bzip2."""
try: try:
cmd = ['bzip2', f'-{level}', file_path] cmd = ["bzip2", f"-{level}", file_path]
result = subprocess.run(cmd, capture_output=True, text=True, check=True) subprocess.run(cmd, capture_output=True, text=True, check=True)
compressed_path = f"{file_path}.bz2" compressed_path = f"{file_path}.bz2"
return os.path.getsize(compressed_path) if os.path.exists(compressed_path) else None return os.path.getsize(compressed_path) if os.path.exists(compressed_path) else None
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@ -211,8 +213,8 @@ class CompressLogsPlugin(BasePlugin):
def _compress_xz(self, file_path: str, level: int) -> int: def _compress_xz(self, file_path: str, level: int) -> int:
"""Compress file using xz.""" """Compress file using xz."""
try: try:
cmd = ['xz', f'-{level}', file_path] cmd = ["xz", f"-{level}", file_path]
result = subprocess.run(cmd, capture_output=True, text=True, check=True) subprocess.run(cmd, capture_output=True, text=True, check=True)
compressed_path = f"{file_path}.xz" compressed_path = f"{file_path}.xz"
return os.path.getsize(compressed_path) if os.path.exists(compressed_path) else None return os.path.getsize(compressed_path) if os.path.exists(compressed_path) else None
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@ -222,8 +224,8 @@ class CompressLogsPlugin(BasePlugin):
def _compress_zstd(self, file_path: str, level: int) -> int: def _compress_zstd(self, file_path: str, level: int) -> int:
"""Compress file using zstd.""" """Compress file using zstd."""
try: try:
cmd = ['zstd', f'-{level}', file_path] cmd = ["zstd", f"-{level}", file_path]
result = subprocess.run(cmd, capture_output=True, text=True, check=True) subprocess.run(cmd, capture_output=True, text=True, check=True)
compressed_path = f"{file_path}.zst" compressed_path = f"{file_path}.zst"
return os.path.getsize(compressed_path) if os.path.exists(compressed_path) else None return os.path.getsize(compressed_path) if os.path.exists(compressed_path) else None
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@ -233,12 +235,12 @@ class CompressLogsPlugin(BasePlugin):
def _compress_with_custom_command(self, file_path: str) -> int: def _compress_with_custom_command(self, file_path: str) -> int:
"""Compress file using custom command.""" """Compress file using custom command."""
try: try:
command = self.compression['command'].format(file=file_path) command = self.compression["command"].format(file=file_path)
result = subprocess.run(command, shell=True, capture_output=True, text=True, check=True) subprocess.run(command, shell=True, capture_output=True, text=True, check=True)
# Try to determine compressed file size # Try to determine compressed file size
# This is a best-effort approach since custom commands may vary # This is a best-effort approach since custom commands may vary
for ext in ['.gz', '.bz2', '.xz', '.zst', '.lzma']: for ext in [".gz", ".bz2", ".xz", ".zst", ".lzma"]:
compressed_path = f"{file_path}{ext}" compressed_path = f"{file_path}{ext}"
if os.path.exists(compressed_path): if os.path.exists(compressed_path):
return os.path.getsize(compressed_path) return os.path.getsize(compressed_path)
@ -258,29 +260,29 @@ class CompressLogsPlugin(BasePlugin):
Returns: Returns:
True if configuration is valid, False otherwise True if configuration is valid, False otherwise
""" """
plugin_config = getattr(config, 'plugins', {}).get('compress_logs', {}) plugin_config = getattr(config, "plugins", {}).get("compress_logs", {})
# Validate compression method # Validate compression method
valid_methods = ['gzip', 'bzip2', 'xz', 'zstd'] valid_methods = ["gzip", "bzip2", "xz", "zstd"]
method = plugin_config.get('compression', 'gzip') method = plugin_config.get("compression", "gzip")
if method not in valid_methods and not plugin_config.get('command'): if method not in valid_methods and not plugin_config.get("command"):
self._log_error(f"Invalid compression method: {method}. Valid methods: {valid_methods}") self._log_error(f"Invalid compression method: {method}. Valid methods: {valid_methods}")
return False return False
# Validate compression level # Validate compression level
level = plugin_config.get('level', 9) level = plugin_config.get("level", 9)
if not isinstance(level, int) or level < 1 or level > 9: if not isinstance(level, int) or level < 1 or level > 9:
self._log_error(f"Invalid compression level: {level}. Must be 1-9") self._log_error(f"Invalid compression level: {level}. Must be 1-9")
return False return False
# Validate extensions # Validate extensions
extensions = plugin_config.get('extensions', ['.log']) extensions = plugin_config.get("extensions", [".log"])
if not isinstance(extensions, list): if not isinstance(extensions, list):
self._log_error("Extensions must be a list") self._log_error("Extensions must be a list")
return False return False
# Validate min_size # Validate min_size
min_size = plugin_config.get('min_size', 0) min_size = plugin_config.get("min_size", 0)
if not isinstance(min_size, int) or min_size < 0: if not isinstance(min_size, int) or min_size < 0:
self._log_error(f"Invalid min_size: {min_size}. Must be non-negative integer") self._log_error(f"Invalid min_size: {min_size}. Must be non-negative integer")
return False return False
@ -295,11 +297,13 @@ class CompressLogsPlugin(BasePlugin):
Dictionary with plugin information Dictionary with plugin information
""" """
info = super().get_plugin_info() info = super().get_plugin_info()
info.update({ info.update(
'compression_method': self.compression['method'], {
'compression_level': self.compression['level'], "compression_method": self.compression["method"],
'extensions': self.compression['extensions'], "compression_level": self.compression["level"],
'min_size': self.compression['min_size'], "extensions": self.compression["extensions"],
'hooks': ['process_logs'] "min_size": self.compression["min_size"],
}) "hooks": ["process_logs"],
}
)
return info return info

View file

@ -6,7 +6,7 @@ inspired by Fedora's Mock plugin hooks but adapted for Debian-based workflows.
""" """
import logging import logging
from typing import Dict, List, Callable, Any, Optional from typing import Any, Callable, Dict, List, Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -27,30 +27,30 @@ class HookManager:
# Define available hook points (based on Mock's hook system) # Define available hook points (based on Mock's hook system)
self.available_hooks = { self.available_hooks = {
'clean': 'Clean up plugin resources', "clean": "Clean up plugin resources",
'earlyprebuild': 'Very early build stage', "earlyprebuild": "Very early build stage",
'initfailed': 'Chroot initialization failed', "initfailed": "Chroot initialization failed",
'list_snapshots': 'List available snapshots', "list_snapshots": "List available snapshots",
'make_snapshot': 'Create a snapshot', "make_snapshot": "Create a snapshot",
'mount_root': 'Mount chroot directory', "mount_root": "Mount chroot directory",
'postbuild': 'After build completion', "postbuild": "After build completion",
'postchroot': 'After chroot command', "postchroot": "After chroot command",
'postclean': 'After chroot cleanup', "postclean": "After chroot cleanup",
'postdeps': 'After dependency installation', "postdeps": "After dependency installation",
'postinit': 'After chroot initialization', "postinit": "After chroot initialization",
'postshell': 'After shell exit', "postshell": "After shell exit",
'postupdate': 'After package updates', "postupdate": "After package updates",
'postumount': 'After unmounting', "postumount": "After unmounting",
'postapt': 'After APT operations', "postapt": "After APT operations",
'prebuild': 'Before build starts', "prebuild": "Before build starts",
'prechroot': 'Before chroot command', "prechroot": "Before chroot command",
'preinit': 'Before chroot initialization', "preinit": "Before chroot initialization",
'preshell': 'Before shell prompt', "preshell": "Before shell prompt",
'preapt': 'Before APT operations', "preapt": "Before APT operations",
'process_logs': 'Process build logs', "process_logs": "Process build logs",
'remove_snapshot': 'Remove snapshot', "remove_snapshot": "Remove snapshot",
'rollback_to': 'Rollback to snapshot', "rollback_to": "Rollback to snapshot",
'scrub': 'Scrub chroot' "scrub": "Scrub chroot",
} }
def add_hook(self, hook_name: str, callback: Callable) -> None: def add_hook(self, hook_name: str, callback: Callable) -> None:
@ -94,7 +94,7 @@ class HookManager:
for i, callback in enumerate(self.hooks[hook_name]): for i, callback in enumerate(self.hooks[hook_name]):
try: try:
logger.debug(f"Executing hook {i+1}/{len(self.hooks[hook_name])}: {callback.__name__}") logger.debug(f"Executing hook {i + 1}/{len(self.hooks[hook_name])}: {callback.__name__}")
callback(context) callback(context)
logger.debug(f"Successfully executed hook: {callback.__name__}") logger.debug(f"Successfully executed hook: {callback.__name__}")
except Exception as e: except Exception as e:
@ -157,21 +157,18 @@ class HookManager:
Dictionary with hook information Dictionary with hook information
""" """
if hook_name not in self.available_hooks: if hook_name not in self.available_hooks:
return {'error': f'Hook "{hook_name}" not found'} return {"error": f'Hook "{hook_name}" not found'}
info = { info = {
'name': hook_name, "name": hook_name,
'description': self.available_hooks[hook_name], "description": self.available_hooks[hook_name],
'registered_callbacks': len(self.hooks.get(hook_name, [])), "registered_callbacks": len(self.hooks.get(hook_name, [])),
'callbacks': [] "callbacks": [],
} }
if hook_name in self.hooks: if hook_name in self.hooks:
for callback in self.hooks[hook_name]: for callback in self.hooks[hook_name]:
info['callbacks'].append({ info["callbacks"].append({"name": callback.__name__, "module": callback.__module__})
'name': callback.__name__,
'module': callback.__module__
})
return info return info
@ -218,18 +215,18 @@ class HookManager:
Dictionary with hook statistics Dictionary with hook statistics
""" """
stats = { stats = {
'total_hooks': len(self.hooks), "total_hooks": len(self.hooks),
'total_callbacks': sum(len(callbacks) for callbacks in self.hooks.values()), "total_callbacks": sum(len(callbacks) for callbacks in self.hooks.values()),
'hooks_with_callbacks': len([h for h in self.hooks.values() if h]), "hooks_with_callbacks": len([h for h in self.hooks.values() if h]),
'available_hooks': len(self.available_hooks), "available_hooks": len(self.available_hooks),
'hook_details': {} "hook_details": {},
} }
for hook_name in self.available_hooks: for hook_name in self.available_hooks:
stats['hook_details'][hook_name] = { stats["hook_details"][hook_name] = {
'description': self.available_hooks[hook_name], "description": self.available_hooks[hook_name],
'registered': hook_name in self.hooks, "registered": hook_name in self.hooks,
'callback_count': len(self.hooks.get(hook_name, [])) "callback_count": len(self.hooks.get(hook_name, [])),
} }
return stats return stats
@ -256,5 +253,4 @@ class HookManager:
Returns: Returns:
List of matching hook names List of matching hook names
""" """
return [name for name in self.available_hooks.keys() return [name for name in self.available_hooks.keys() if name.startswith(partial_name)]
if name.startswith(partial_name)]

View file

@ -5,9 +5,10 @@ This module provides the plugin registration and management functionality
for the Deb-Mock plugin system, inspired by Fedora's Mock plugin architecture. for the Deb-Mock plugin system, inspired by Fedora's Mock plugin architecture.
""" """
import logging
import importlib import importlib
from typing import Dict, Type, Any, Optional import logging
from typing import Any, Dict, Optional, Type
from .base import BasePlugin from .base import BasePlugin
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,8 +30,12 @@ class PluginRegistry:
# Auto-register built-in plugins # Auto-register built-in plugins
self._register_builtin_plugins() self._register_builtin_plugins()
def register(self, plugin_name: str, plugin_class: Type[BasePlugin], def register(
metadata: Optional[Dict[str, Any]] = None) -> None: self,
plugin_name: str,
plugin_class: Type[BasePlugin],
metadata: Optional[Dict[str, Any]] = None,
) -> None:
""" """
Register a plugin class. Register a plugin class.
@ -44,7 +49,7 @@ class PluginRegistry:
TypeError: If plugin_class is not a subclass of BasePlugin TypeError: If plugin_class is not a subclass of BasePlugin
""" """
if not issubclass(plugin_class, BasePlugin): if not issubclass(plugin_class, BasePlugin):
raise TypeError(f"Plugin class must inherit from BasePlugin") raise TypeError("Plugin class must inherit from BasePlugin")
if plugin_name in self.plugins: if plugin_name in self.plugins:
raise ValueError(f"Plugin '{plugin_name}' is already registered") raise ValueError(f"Plugin '{plugin_name}' is already registered")
@ -160,17 +165,17 @@ class PluginRegistry:
Dictionary with plugin information Dictionary with plugin information
""" """
if plugin_name not in self.plugins: if plugin_name not in self.plugins:
return {'error': f'Plugin "{plugin_name}" not found'} return {"error": f'Plugin "{plugin_name}" not found'}
plugin_class = self.plugins[plugin_name] plugin_class = self.plugins[plugin_name]
metadata = self.plugin_metadata[plugin_name] metadata = self.plugin_metadata[plugin_name]
info = { info = {
'name': plugin_name, "name": plugin_name,
'class': plugin_class.__name__, "class": plugin_class.__name__,
'module': plugin_class.__module__, "module": plugin_class.__module__,
'metadata': metadata, "metadata": metadata,
'docstring': plugin_class.__doc__ or 'No documentation available' "docstring": plugin_class.__doc__ or "No documentation available",
} }
return info return info
@ -225,21 +230,21 @@ class PluginRegistry:
""" """
loaded_plugins = {} loaded_plugins = {}
if not hasattr(config, 'plugins') or not config.plugins: if not hasattr(config, "plugins") or not config.plugins:
return loaded_plugins return loaded_plugins
for plugin_name, plugin_config in config.plugins.items(): for plugin_name, plugin_config in config.plugins.items():
if not isinstance(plugin_config, dict): if not isinstance(plugin_config, dict):
continue continue
if plugin_config.get('enabled', False): if plugin_config.get("enabled", False):
# Try to load from built-in plugins first # Try to load from built-in plugins first
plugin_instance = self.create(plugin_name, config, None) plugin_instance = self.create(plugin_name, config, None)
if plugin_instance: if plugin_instance:
loaded_plugins[plugin_name] = plugin_instance loaded_plugins[plugin_name] = plugin_instance
else: else:
# Try to load from external module # Try to load from external module
module_name = plugin_config.get('module') module_name = plugin_config.get("module")
if module_name: if module_name:
if self.load_plugin_from_module(module_name, plugin_name): if self.load_plugin_from_module(module_name, plugin_name):
plugin_instance = self.create(plugin_name, config, None) plugin_instance = self.create(plugin_name, config, None)
@ -257,29 +262,51 @@ class PluginRegistry:
from .root_cache import RootCachePlugin from .root_cache import RootCachePlugin
from .tmpfs import TmpfsPlugin from .tmpfs import TmpfsPlugin
self.register('bind_mount', BindMountPlugin, { self.register(
'description': 'Mount host directories into chroot', "bind_mount",
'hooks': ['mount_root', 'postumount'], BindMountPlugin,
'builtin': True {
}) "description": "Mount host directories into chroot",
"hooks": ["mount_root", "postumount"],
"builtin": True,
},
)
self.register('compress_logs', CompressLogsPlugin, { self.register(
'description': 'Compress build logs to save space', "compress_logs",
'hooks': ['process_logs'], CompressLogsPlugin,
'builtin': True {
}) "description": "Compress build logs to save space",
"hooks": ["process_logs"],
"builtin": True,
},
)
self.register('root_cache', RootCachePlugin, { self.register(
'description': 'Root cache management for faster builds', "root_cache",
'hooks': ['preinit', 'postinit', 'postchroot', 'postshell', 'clean'], RootCachePlugin,
'builtin': True {
}) "description": "Root cache management for faster builds",
"hooks": [
"preinit",
"postinit",
"postchroot",
"postshell",
"clean",
],
"builtin": True,
},
)
self.register('tmpfs', TmpfsPlugin, { self.register(
'description': 'Use tmpfs for faster I/O operations', "tmpfs",
'hooks': ['mount_root', 'postumount'], TmpfsPlugin,
'builtin': True {
}) "description": "Use tmpfs for faster I/O operations",
"hooks": ["mount_root", "postumount"],
"builtin": True,
},
)
logger.debug("Registered built-in plugins") logger.debug("Registered built-in plugins")
@ -296,19 +323,19 @@ class PluginRegistry:
Dictionary with plugin statistics Dictionary with plugin statistics
""" """
stats = { stats = {
'total_plugins': len(self.plugins), "total_plugins": len(self.plugins),
'builtin_plugins': len([p for p in self.plugin_metadata.values() if p.get('builtin', False)]), "builtin_plugins": len([p for p in self.plugin_metadata.values() if p.get("builtin", False)]),
'external_plugins': len([p for p in self.plugin_metadata.values() if not p.get('builtin', False)]), "external_plugins": len([p for p in self.plugin_metadata.values() if not p.get("builtin", False)]),
'plugins_by_hook': {} "plugins_by_hook": {},
} }
# Count plugins by hook usage # Count plugins by hook usage
for plugin_name, metadata in self.plugin_metadata.items(): for plugin_name, metadata in self.plugin_metadata.items():
hooks = metadata.get('hooks', []) hooks = metadata.get("hooks", [])
for hook in hooks: for hook in hooks:
if hook not in stats['plugins_by_hook']: if hook not in stats["plugins_by_hook"]:
stats['plugins_by_hook'][hook] = [] stats["plugins_by_hook"][hook] = []
stats['plugins_by_hook'][hook].append(plugin_name) stats["plugins_by_hook"][hook].append(plugin_name)
return stats return stats
@ -328,7 +355,7 @@ class PluginRegistry:
# Basic validation - plugins can override this method # Basic validation - plugins can override this method
plugin_class = self.plugins[plugin_name] plugin_class = self.plugins[plugin_name]
if hasattr(plugin_class, 'validate_config'): if hasattr(plugin_class, "validate_config"):
return plugin_class.validate_config(config) return plugin_class.validate_config(config)
return True return True

View file

@ -5,14 +5,11 @@ This plugin provides root cache management for faster builds,
inspired by Fedora's Mock root_cache plugin but adapted for Debian-based systems. inspired by Fedora's Mock root_cache plugin but adapted for Debian-based systems.
""" """
import logging
import os import os
import tarfile import tarfile
import hashlib
import json
import time import time
import logging from typing import Any, Dict
from pathlib import Path
from typing import Dict, Any, Optional
from .base import BasePlugin from .base import BasePlugin
@ -54,13 +51,13 @@ class RootCachePlugin(BasePlugin):
plugin_config = self._get_plugin_config() plugin_config = self._get_plugin_config()
return { return {
'cache_dir': plugin_config.get('cache_dir', '/var/cache/deb-mock/root-cache'), "cache_dir": plugin_config.get("cache_dir", "/var/cache/deb-mock/root-cache"),
'max_age_days': plugin_config.get('max_age_days', 7), "max_age_days": plugin_config.get("max_age_days", 7),
'compression': plugin_config.get('compression', 'gzip'), "compression": plugin_config.get("compression", "gzip"),
'exclude_dirs': plugin_config.get('exclude_dirs', ['/tmp', '/var/tmp', '/var/cache']), "exclude_dirs": plugin_config.get("exclude_dirs", ["/tmp", "/var/tmp", "/var/cache"]),
'exclude_patterns': plugin_config.get('exclude_patterns', ['*.log', '*.tmp']), "exclude_patterns": plugin_config.get("exclude_patterns", ["*.log", "*.tmp"]),
'min_cache_size_mb': plugin_config.get('min_cache_size_mb', 100), "min_cache_size_mb": plugin_config.get("min_cache_size_mb", 100),
'auto_cleanup': plugin_config.get('auto_cleanup', True) "auto_cleanup": plugin_config.get("auto_cleanup", True),
} }
def _get_cache_file_path(self) -> str: def _get_cache_file_path(self) -> str:
@ -70,20 +67,20 @@ class RootCachePlugin(BasePlugin):
Returns: Returns:
Path to the cache file Path to the cache file
""" """
cache_dir = self.cache_settings['cache_dir'] cache_dir = self.cache_settings["cache_dir"]
compression = self.cache_settings['compression'] compression = self.cache_settings["compression"]
# Create cache directory if it doesn't exist # Create cache directory if it doesn't exist
os.makedirs(cache_dir, exist_ok=True) os.makedirs(cache_dir, exist_ok=True)
# Determine file extension based on compression # Determine file extension based on compression
extensions = { extensions = {
'gzip': '.tar.gz', "gzip": ".tar.gz",
'bzip2': '.tar.bz2', "bzip2": ".tar.bz2",
'xz': '.tar.xz', "xz": ".tar.xz",
'zstd': '.tar.zst' "zstd": ".tar.zst",
} }
ext = extensions.get(compression, '.tar.gz') ext = extensions.get(compression, ".tar.gz")
return os.path.join(cache_dir, f"cache{ext}") return os.path.join(cache_dir, f"cache{ext}")
@ -97,7 +94,7 @@ class RootCachePlugin(BasePlugin):
if not self.enabled: if not self.enabled:
return return
chroot_path = context.get('chroot_path') chroot_path = context.get("chroot_path")
if not chroot_path: if not chroot_path:
self._log_warning("No chroot_path in context, skipping cache restoration") self._log_warning("No chroot_path in context, skipping cache restoration")
return return
@ -128,7 +125,7 @@ class RootCachePlugin(BasePlugin):
if not self.enabled: if not self.enabled:
return return
chroot_path = context.get('chroot_path') chroot_path = context.get("chroot_path")
if not chroot_path: if not chroot_path:
self._log_warning("No chroot_path in context, skipping cache creation") self._log_warning("No chroot_path in context, skipping cache creation")
return return
@ -151,7 +148,7 @@ class RootCachePlugin(BasePlugin):
if not self.enabled: if not self.enabled:
return return
chroot_path = context.get('chroot_path') chroot_path = context.get("chroot_path")
if not chroot_path: if not chroot_path:
return return
@ -172,7 +169,7 @@ class RootCachePlugin(BasePlugin):
if not self.enabled: if not self.enabled:
return return
chroot_path = context.get('chroot_path') chroot_path = context.get("chroot_path")
if not chroot_path: if not chroot_path:
return return
@ -193,7 +190,7 @@ class RootCachePlugin(BasePlugin):
if not self.enabled: if not self.enabled:
return return
if self.cache_settings['auto_cleanup']: if self.cache_settings["auto_cleanup"]:
self._log_info("Cleaning up old caches") self._log_info("Cleaning up old caches")
try: try:
@ -223,15 +220,15 @@ class RootCachePlugin(BasePlugin):
# Check file age # Check file age
file_age = time.time() - os.path.getmtime(self.cache_file) file_age = time.time() - os.path.getmtime(self.cache_file)
max_age_seconds = self.cache_settings['max_age_days'] * 24 * 3600 max_age_seconds = self.cache_settings["max_age_days"] * 24 * 3600
if file_age > max_age_seconds: if file_age > max_age_seconds:
self._log_debug(f"Cache is {file_age/3600:.1f} hours old, max age is {max_age_seconds/3600:.1f} hours") self._log_debug(f"Cache is {file_age / 3600:.1f} hours old, max age is {max_age_seconds / 3600:.1f} hours")
return False return False
# Check file size # Check file size
file_size_mb = os.path.getsize(self.cache_file) / (1024 * 1024) file_size_mb = os.path.getsize(self.cache_file) / (1024 * 1024)
min_size_mb = self.cache_settings['min_cache_size_mb'] min_size_mb = self.cache_settings["min_cache_size_mb"]
if file_size_mb < min_size_mb: if file_size_mb < min_size_mb:
self._log_debug(f"Cache size {file_size_mb:.1f}MB is below minimum {min_size_mb}MB") self._log_debug(f"Cache size {file_size_mb:.1f}MB is below minimum {min_size_mb}MB")
@ -253,18 +250,18 @@ class RootCachePlugin(BasePlugin):
os.makedirs(chroot_path, exist_ok=True) os.makedirs(chroot_path, exist_ok=True)
# Extract cache # Extract cache
compression = self.cache_settings['compression'] compression = self.cache_settings["compression"]
if compression == 'gzip': if compression == "gzip":
mode = 'r:gz' mode = "r:gz"
elif compression == 'bzip2': elif compression == "bzip2":
mode = 'r:bz2' mode = "r:bz2"
elif compression == 'xz': elif compression == "xz":
mode = 'r:xz' mode = "r:xz"
elif compression == 'zstd': elif compression == "zstd":
mode = 'r:zstd' mode = "r:zstd"
else: else:
mode = 'r:gz' # Default to gzip mode = "r:gz" # Default to gzip
try: try:
with tarfile.open(self.cache_file, mode) as tar: with tarfile.open(self.cache_file, mode) as tar:
@ -287,23 +284,23 @@ class RootCachePlugin(BasePlugin):
raise FileNotFoundError(f"Chroot path does not exist: {chroot_path}") raise FileNotFoundError(f"Chroot path does not exist: {chroot_path}")
# Determine compression mode # Determine compression mode
compression = self.cache_settings['compression'] compression = self.cache_settings["compression"]
if compression == 'gzip': if compression == "gzip":
mode = 'w:gz' mode = "w:gz"
elif compression == 'bzip2': elif compression == "bzip2":
mode = 'w:bz2' mode = "w:bz2"
elif compression == 'xz': elif compression == "xz":
mode = 'w:xz' mode = "w:xz"
elif compression == 'zstd': elif compression == "zstd":
mode = 'w:zstd' mode = "w:zstd"
else: else:
mode = 'w:gz' # Default to gzip mode = "w:gz" # Default to gzip
try: try:
with tarfile.open(self.cache_file, mode) as tar: with tarfile.open(self.cache_file, mode) as tar:
# Add chroot contents to archive # Add chroot contents to archive
tar.add(chroot_path, arcname='', exclude=self._get_exclude_filter()) tar.add(chroot_path, arcname="", exclude=self._get_exclude_filter())
self._log_debug(f"Successfully created cache: {self.cache_file}") self._log_debug(f"Successfully created cache: {self.cache_file}")
@ -329,8 +326,8 @@ class RootCachePlugin(BasePlugin):
Returns: Returns:
Number of cache files cleaned up Number of cache files cleaned up
""" """
cache_dir = self.cache_settings['cache_dir'] cache_dir = self.cache_settings["cache_dir"]
max_age_seconds = self.cache_settings['max_age_days'] * 24 * 3600 max_age_seconds = self.cache_settings["max_age_days"] * 24 * 3600
current_time = time.time() current_time = time.time()
cleaned_count = 0 cleaned_count = 0
@ -338,7 +335,7 @@ class RootCachePlugin(BasePlugin):
return 0 return 0
for cache_file in os.listdir(cache_dir): for cache_file in os.listdir(cache_dir):
if not cache_file.startswith('cache'): if not cache_file.startswith("cache"):
continue continue
cache_path = os.path.join(cache_dir, cache_file) cache_path = os.path.join(cache_dir, cache_file)
@ -361,13 +358,13 @@ class RootCachePlugin(BasePlugin):
Returns: Returns:
Function to filter out excluded files/directories Function to filter out excluded files/directories
""" """
exclude_dirs = self.cache_settings['exclude_dirs'] exclude_dirs = self.cache_settings["exclude_dirs"]
exclude_patterns = self.cache_settings['exclude_patterns'] exclude_patterns = self.cache_settings["exclude_patterns"]
def exclude_filter(tarinfo): def exclude_filter(tarinfo):
# Check excluded directories # Check excluded directories
for exclude_dir in exclude_dirs: for exclude_dir in exclude_dirs:
if tarinfo.name.startswith(exclude_dir.lstrip('/')): if tarinfo.name.startswith(exclude_dir.lstrip("/")):
return None return None
# Check excluded patterns # Check excluded patterns
@ -389,47 +386,47 @@ class RootCachePlugin(BasePlugin):
Returns: Returns:
True if configuration is valid, False otherwise True if configuration is valid, False otherwise
""" """
plugin_config = getattr(config, 'plugins', {}).get('root_cache', {}) plugin_config = getattr(config, "plugins", {}).get("root_cache", {})
# Validate cache_dir # Validate cache_dir
cache_dir = plugin_config.get('cache_dir', '/var/cache/deb-mock/root-cache') cache_dir = plugin_config.get("cache_dir", "/var/cache/deb-mock/root-cache")
if not cache_dir: if not cache_dir:
self._log_error("cache_dir cannot be empty") self._log_error("cache_dir cannot be empty")
return False return False
# Validate max_age_days # Validate max_age_days
max_age_days = plugin_config.get('max_age_days', 7) max_age_days = plugin_config.get("max_age_days", 7)
if not isinstance(max_age_days, int) or max_age_days <= 0: if not isinstance(max_age_days, int) or max_age_days <= 0:
self._log_error(f"Invalid max_age_days: {max_age_days}. Must be positive integer") self._log_error(f"Invalid max_age_days: {max_age_days}. Must be positive integer")
return False return False
# Validate compression # Validate compression
valid_compressions = ['gzip', 'bzip2', 'xz', 'zstd'] valid_compressions = ["gzip", "bzip2", "xz", "zstd"]
compression = plugin_config.get('compression', 'gzip') compression = plugin_config.get("compression", "gzip")
if compression not in valid_compressions: if compression not in valid_compressions:
self._log_error(f"Invalid compression: {compression}. Valid options: {valid_compressions}") self._log_error(f"Invalid compression: {compression}. Valid options: {valid_compressions}")
return False return False
# Validate exclude_dirs # Validate exclude_dirs
exclude_dirs = plugin_config.get('exclude_dirs', ['/tmp', '/var/tmp', '/var/cache']) exclude_dirs = plugin_config.get("exclude_dirs", ["/tmp", "/var/tmp", "/var/cache"])
if not isinstance(exclude_dirs, list): if not isinstance(exclude_dirs, list):
self._log_error("exclude_dirs must be a list") self._log_error("exclude_dirs must be a list")
return False return False
# Validate exclude_patterns # Validate exclude_patterns
exclude_patterns = plugin_config.get('exclude_patterns', ['*.log', '*.tmp']) exclude_patterns = plugin_config.get("exclude_patterns", ["*.log", "*.tmp"])
if not isinstance(exclude_patterns, list): if not isinstance(exclude_patterns, list):
self._log_error("exclude_patterns must be a list") self._log_error("exclude_patterns must be a list")
return False return False
# Validate min_cache_size_mb # Validate min_cache_size_mb
min_cache_size_mb = plugin_config.get('min_cache_size_mb', 100) min_cache_size_mb = plugin_config.get("min_cache_size_mb", 100)
if not isinstance(min_cache_size_mb, (int, float)) or min_cache_size_mb < 0: if not isinstance(min_cache_size_mb, (int, float)) or min_cache_size_mb < 0:
self._log_error(f"Invalid min_cache_size_mb: {min_cache_size_mb}. Must be non-negative number") self._log_error(f"Invalid min_cache_size_mb: {min_cache_size_mb}. Must be non-negative number")
return False return False
# Validate auto_cleanup # Validate auto_cleanup
auto_cleanup = plugin_config.get('auto_cleanup', True) auto_cleanup = plugin_config.get("auto_cleanup", True)
if not isinstance(auto_cleanup, bool): if not isinstance(auto_cleanup, bool):
self._log_error(f"Invalid auto_cleanup: {auto_cleanup}. Must be boolean") self._log_error(f"Invalid auto_cleanup: {auto_cleanup}. Must be boolean")
return False return False
@ -444,17 +441,19 @@ class RootCachePlugin(BasePlugin):
Dictionary with plugin information Dictionary with plugin information
""" """
info = super().get_plugin_info() info = super().get_plugin_info()
info.update({ info.update(
'cache_dir': self.cache_settings['cache_dir'], {
'cache_file': self.cache_file, "cache_dir": self.cache_settings["cache_dir"],
'max_age_days': self.cache_settings['max_age_days'], "cache_file": self.cache_file,
'compression': self.cache_settings['compression'], "max_age_days": self.cache_settings["max_age_days"],
'exclude_dirs': self.cache_settings['exclude_dirs'], "compression": self.cache_settings["compression"],
'exclude_patterns': self.cache_settings['exclude_patterns'], "exclude_dirs": self.cache_settings["exclude_dirs"],
'min_cache_size_mb': self.cache_settings['min_cache_size_mb'], "exclude_patterns": self.cache_settings["exclude_patterns"],
'auto_cleanup': self.cache_settings['auto_cleanup'], "min_cache_size_mb": self.cache_settings["min_cache_size_mb"],
'cache_exists': self._cache_exists(), "auto_cleanup": self.cache_settings["auto_cleanup"],
'cache_valid': self._is_cache_valid() if self._cache_exists() else False, "cache_exists": self._cache_exists(),
'hooks': ['preinit', 'postinit', 'postchroot', 'postshell', 'clean'] "cache_valid": (self._is_cache_valid() if self._cache_exists() else False),
}) "hooks": ["preinit", "postinit", "postchroot", "postshell", "clean"],
}
)
return info return info

View file

@ -5,10 +5,9 @@ This plugin uses tmpfs for faster I/O operations in chroot,
inspired by Fedora's Mock tmpfs plugin but adapted for Debian-based systems. inspired by Fedora's Mock tmpfs plugin but adapted for Debian-based systems.
""" """
import os
import subprocess
import logging import logging
from typing import Dict, Any, Optional import subprocess
from typing import Any, Dict
from .base import BasePlugin from .base import BasePlugin
@ -47,12 +46,12 @@ class TmpfsPlugin(BasePlugin):
plugin_config = self._get_plugin_config() plugin_config = self._get_plugin_config()
return { return {
'size': plugin_config.get('size', '2G'), "size": plugin_config.get("size", "2G"),
'mode': plugin_config.get('mode', '0755'), "mode": plugin_config.get("mode", "0755"),
'mount_point': plugin_config.get('mount_point', '/tmp'), "mount_point": plugin_config.get("mount_point", "/tmp"),
'keep_mounted': plugin_config.get('keep_mounted', False), "keep_mounted": plugin_config.get("keep_mounted", False),
'required_ram_mb': plugin_config.get('required_ram_mb', 2048), # 2GB default "required_ram_mb": plugin_config.get("required_ram_mb", 2048), # 2GB default
'max_fs_size': plugin_config.get('max_fs_size', None) "max_fs_size": plugin_config.get("max_fs_size", None),
} }
def mount_root(self, context: Dict[str, Any]) -> None: def mount_root(self, context: Dict[str, Any]) -> None:
@ -65,7 +64,7 @@ class TmpfsPlugin(BasePlugin):
if not self.enabled: if not self.enabled:
return return
chroot_path = context.get('chroot_path') chroot_path = context.get("chroot_path")
if not chroot_path: if not chroot_path:
self._log_warning("No chroot_path in context, skipping tmpfs mount") self._log_warning("No chroot_path in context, skipping tmpfs mount")
return return
@ -101,13 +100,13 @@ class TmpfsPlugin(BasePlugin):
if not self.enabled or not self.mounted: if not self.enabled or not self.mounted:
return return
chroot_path = context.get('chroot_path') chroot_path = context.get("chroot_path")
if not chroot_path: if not chroot_path:
self._log_warning("No chroot_path in context, skipping tmpfs unmount") self._log_warning("No chroot_path in context, skipping tmpfs unmount")
return return
# Check if we should keep mounted # Check if we should keep mounted
if self.tmpfs_settings['keep_mounted']: if self.tmpfs_settings["keep_mounted"]:
self._log_info("Keeping tmpfs mounted as requested") self._log_info("Keeping tmpfs mounted as requested")
return return
@ -129,9 +128,9 @@ class TmpfsPlugin(BasePlugin):
""" """
try: try:
# Get system RAM in MB # Get system RAM in MB
with open('/proc/meminfo', 'r') as f: with open("/proc/meminfo", "r") as f:
for line in f: for line in f:
if line.startswith('MemTotal:'): if line.startswith("MemTotal:"):
mem_total_kb = int(line.split()[1]) mem_total_kb = int(line.split()[1])
mem_total_mb = mem_total_kb // 1024 mem_total_mb = mem_total_kb // 1024
break break
@ -139,12 +138,10 @@ class TmpfsPlugin(BasePlugin):
self._log_warning("Could not determine system RAM") self._log_warning("Could not determine system RAM")
return False return False
required_ram = self.tmpfs_settings['required_ram_mb'] required_ram = self.tmpfs_settings["required_ram_mb"]
if mem_total_mb < required_ram: if mem_total_mb < required_ram:
self._log_warning( self._log_warning(f"System has {mem_total_mb}MB RAM, but {required_ram}MB is required for tmpfs")
f"System has {mem_total_mb}MB RAM, but {required_ram}MB is required for tmpfs"
)
return False return False
self._log_debug(f"System RAM: {mem_total_mb}MB, required: {required_ram}MB") self._log_debug(f"System RAM: {mem_total_mb}MB, required: {required_ram}MB")
@ -166,20 +163,16 @@ class TmpfsPlugin(BasePlugin):
""" """
try: try:
# Check if the path is a mount point # Check if the path is a mount point
result = subprocess.run( result = subprocess.run(["mountpoint", "-q", chroot_path], capture_output=True, text=True)
['mountpoint', '-q', chroot_path],
capture_output=True,
text=True
)
return result.returncode == 0 return result.returncode == 0
except FileNotFoundError: except FileNotFoundError:
# mountpoint command not available, try alternative method # mountpoint command not available, try alternative method
try: try:
with open('/proc/mounts', 'r') as f: with open("/proc/mounts", "r") as f:
for line in f: for line in f:
parts = line.split() parts = line.split()
if len(parts) >= 2 and parts[1] == chroot_path: if len(parts) >= 2 and parts[1] == chroot_path:
return parts[0] == 'tmpfs' return parts[0] == "tmpfs"
return False return False
except Exception: except Exception:
self._log_warning("Could not check mount status") self._log_warning("Could not check mount status")
@ -196,38 +189,38 @@ class TmpfsPlugin(BasePlugin):
options = [] options = []
# Add mode option # Add mode option
mode = self.tmpfs_settings['mode'] mode = self.tmpfs_settings["mode"]
options.append(f'mode={mode}') options.append(f"mode={mode}")
# Add size option # Add size option
size = self.tmpfs_settings['size'] size = self.tmpfs_settings["size"]
if size: if size:
options.append(f'size={size}') options.append(f"size={size}")
# Add max_fs_size if specified # Add max_fs_size if specified
max_fs_size = self.tmpfs_settings['max_fs_size'] max_fs_size = self.tmpfs_settings["max_fs_size"]
if max_fs_size: if max_fs_size:
options.append(f'size={max_fs_size}') options.append(f"size={max_fs_size}")
# Add noatime for better performance # Add noatime for better performance
options.append('noatime') options.append("noatime")
# Build mount command # Build mount command
mount_cmd = [ mount_cmd = [
'mount', '-n', '-t', 'tmpfs', "mount",
'-o', ','.join(options), "-n",
'deb_mock_tmpfs', chroot_path "-t",
"tmpfs",
"-o",
",".join(options),
"deb_mock_tmpfs",
chroot_path,
] ]
self._log_debug(f"Mount command: {' '.join(mount_cmd)}") self._log_debug(f"Mount command: {' '.join(mount_cmd)}")
try: try:
result = subprocess.run( subprocess.run(mount_cmd, capture_output=True, text=True, check=True)
mount_cmd,
capture_output=True,
text=True,
check=True
)
self._log_debug("Tmpfs mount command executed successfully") self._log_debug("Tmpfs mount command executed successfully")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
@ -246,8 +239,8 @@ class TmpfsPlugin(BasePlugin):
""" """
# Try normal unmount first # Try normal unmount first
try: try:
cmd = ['umount', '-n', chroot_path] cmd = ["umount", "-n", chroot_path]
result = subprocess.run(cmd, capture_output=True, text=True, check=True) subprocess.run(cmd, capture_output=True, text=True, check=True)
self._log_debug("Tmpfs unmounted successfully") self._log_debug("Tmpfs unmounted successfully")
return return
@ -256,8 +249,8 @@ class TmpfsPlugin(BasePlugin):
# Try lazy unmount # Try lazy unmount
try: try:
cmd = ['umount', '-n', '-l', chroot_path] cmd = ["umount", "-n", "-l", chroot_path]
result = subprocess.run(cmd, capture_output=True, text=True, check=True) subprocess.run(cmd, capture_output=True, text=True, check=True)
self._log_debug("Tmpfs lazy unmounted successfully") self._log_debug("Tmpfs lazy unmounted successfully")
return return
@ -266,8 +259,8 @@ class TmpfsPlugin(BasePlugin):
# Try force unmount as last resort # Try force unmount as last resort
try: try:
cmd = ['umount', '-n', '-f', chroot_path] cmd = ["umount", "-n", "-f", chroot_path]
result = subprocess.run(cmd, capture_output=True, text=True, check=True) subprocess.run(cmd, capture_output=True, text=True, check=True)
self._log_debug("Tmpfs force unmounted successfully") self._log_debug("Tmpfs force unmounted successfully")
return return
@ -285,28 +278,28 @@ class TmpfsPlugin(BasePlugin):
Returns: Returns:
True if configuration is valid, False otherwise True if configuration is valid, False otherwise
""" """
plugin_config = getattr(config, 'plugins', {}).get('tmpfs', {}) plugin_config = getattr(config, "plugins", {}).get("tmpfs", {})
# Validate size format # Validate size format
size = plugin_config.get('size', '2G') size = plugin_config.get("size", "2G")
if not self._is_valid_size_format(size): if not self._is_valid_size_format(size):
self._log_error(f"Invalid size format: {size}. Use format like '2G', '512M', etc.") self._log_error(f"Invalid size format: {size}. Use format like '2G', '512M', etc.")
return False return False
# Validate mode format # Validate mode format
mode = plugin_config.get('mode', '0755') mode = plugin_config.get("mode", "0755")
if not self._is_valid_mode_format(mode): if not self._is_valid_mode_format(mode):
self._log_error(f"Invalid mode format: {mode}. Use octal format like '0755'") self._log_error(f"Invalid mode format: {mode}. Use octal format like '0755'")
return False return False
# Validate required_ram_mb # Validate required_ram_mb
required_ram = plugin_config.get('required_ram_mb', 2048) required_ram = plugin_config.get("required_ram_mb", 2048)
if not isinstance(required_ram, int) or required_ram <= 0: if not isinstance(required_ram, int) or required_ram <= 0:
self._log_error(f"Invalid required_ram_mb: {required_ram}. Must be positive integer") self._log_error(f"Invalid required_ram_mb: {required_ram}. Must be positive integer")
return False return False
# Validate keep_mounted # Validate keep_mounted
keep_mounted = plugin_config.get('keep_mounted', False) keep_mounted = plugin_config.get("keep_mounted", False)
if not isinstance(keep_mounted, bool): if not isinstance(keep_mounted, bool):
self._log_error(f"Invalid keep_mounted: {keep_mounted}. Must be boolean") self._log_error(f"Invalid keep_mounted: {keep_mounted}. Must be boolean")
return False return False
@ -331,7 +324,7 @@ class TmpfsPlugin(BasePlugin):
return True return True
# Check if it ends with a valid unit # Check if it ends with a valid unit
valid_units = ['K', 'M', 'G', 'T'] valid_units = ["K", "M", "G", "T"]
if size[-1] in valid_units and size[:-1].isdigit(): if size[-1] in valid_units and size[:-1].isdigit():
return True return True
@ -365,13 +358,15 @@ class TmpfsPlugin(BasePlugin):
Dictionary with plugin information Dictionary with plugin information
""" """
info = super().get_plugin_info() info = super().get_plugin_info()
info.update({ info.update(
'tmpfs_size': self.tmpfs_settings['size'], {
'tmpfs_mode': self.tmpfs_settings['mode'], "tmpfs_size": self.tmpfs_settings["size"],
'mount_point': self.tmpfs_settings['mount_point'], "tmpfs_mode": self.tmpfs_settings["mode"],
'keep_mounted': self.tmpfs_settings['keep_mounted'], "mount_point": self.tmpfs_settings["mount_point"],
'required_ram_mb': self.tmpfs_settings['required_ram_mb'], "keep_mounted": self.tmpfs_settings["keep_mounted"],
'mounted': self.mounted, "required_ram_mb": self.tmpfs_settings["required_ram_mb"],
'hooks': ['mount_root', 'postumount'] "mounted": self.mounted,
}) "hooks": ["mount_root", "postumount"],
}
)
return info return info

View file

@ -5,9 +5,9 @@ sbuild wrapper for deb-mock
import os import os
import subprocess import subprocess
import tempfile import tempfile
import shutil
from pathlib import Path from pathlib import Path
from typing import List, Dict, Any, Optional from typing import Any, Dict, List
from .exceptions import SbuildError from .exceptions import SbuildError
@ -17,8 +17,13 @@ class SbuildWrapper:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
def build_package(self, source_package: str, chroot_name: str = None, def build_package(
output_dir: str = None, **kwargs) -> Dict[str, Any]: self,
source_package: str,
chroot_name: str = None,
output_dir: str = None,
**kwargs,
) -> Dict[str, Any]:
"""Build a Debian source package using sbuild""" """Build a Debian source package using sbuild"""
if chroot_name is None: if chroot_name is None:
@ -32,13 +37,19 @@ class SbuildWrapper:
# Prepare sbuild command # Prepare sbuild command
cmd = self._prepare_sbuild_command(source_package, chroot_name, output_dir, **kwargs) 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 # Create temporary log file
with tempfile.NamedTemporaryFile(mode='w', suffix='.log', delete=False) as log_file: with tempfile.NamedTemporaryFile(mode="w", suffix=".log", delete=False) as log_file:
log_path = log_file.name log_path = log_file.name
try: try:
# Execute sbuild # Execute sbuild
result = self._execute_sbuild(cmd, log_path) result = self._execute_sbuild(cmd, log_path, env)
# Parse build results # Parse build results
build_info = self._parse_build_results(output_dir, log_path, result) build_info = self._parse_build_results(output_dir, log_path, result)
@ -50,93 +61,85 @@ class SbuildWrapper:
if os.path.exists(log_path): if os.path.exists(log_path):
os.unlink(log_path) os.unlink(log_path)
def _prepare_sbuild_command(self, source_package: str, chroot_name: str, def _prepare_sbuild_command(self, source_package: str, chroot_name: str, output_dir: str, **kwargs) -> List[str]:
output_dir: str, **kwargs) -> List[str]:
"""Prepare the sbuild command with all necessary options""" """Prepare the sbuild command with all necessary options"""
cmd = ['sbuild'] cmd = ["sbuild"]
# Basic options # Basic options
cmd.extend(['--chroot', chroot_name]) cmd.extend(["--chroot", chroot_name])
cmd.extend(['--dist', self.config.suite]) cmd.extend(["--dist", self.config.suite])
cmd.extend(['--arch', self.config.architecture]) cmd.extend(["--arch", self.config.architecture])
# Output options # Output options
cmd.extend(['--build-dir', output_dir]) cmd.extend(["--build-dir", output_dir])
# Logging options
cmd.extend(['--log-dir', self.config.sbuild_log_dir])
# Build options # Build options
if kwargs.get('verbose', self.config.verbose): if kwargs.get("verbose", self.config.verbose):
cmd.append('--verbose') cmd.append("--verbose")
if kwargs.get('debug', self.config.debug): if kwargs.get("debug", self.config.debug):
cmd.append('--debug') cmd.append("--debug")
# Additional build options from config # Additional build options from config
for option in self.config.build_options: for option in self.config.build_options:
cmd.extend(option.split()) cmd.extend(option.split())
# Custom build options # Custom build options
if kwargs.get('build_options'): if kwargs.get("build_options"):
for option in kwargs['build_options']: for option in kwargs["build_options"]:
cmd.extend(option.split()) cmd.extend(option.split())
# Environment variables # Environment variables will be passed to subprocess.run
for key, value in self.config.build_env.items(): pass
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 # Source package
cmd.append(source_package) cmd.append(source_package)
return cmd return cmd
def _execute_sbuild(self, cmd: List[str], log_path: str) -> subprocess.CompletedProcess: def _execute_sbuild(self, cmd: List[str], log_path: str, env: Dict[str, str] = None) -> subprocess.CompletedProcess:
"""Execute sbuild command""" """Execute sbuild command"""
try: try:
# Redirect output to log file # Redirect output to log file
with open(log_path, 'w') as log_file: with open(log_path, "w") as log_file:
result = subprocess.run( result = subprocess.run(
cmd, cmd,
stdout=log_file, stdout=log_file,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
text=True, text=True,
check=True check=True,
env=env,
) )
return result return result
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
# Read log file for error details # Read log file for error details
with open(log_path, 'r') as log_file: with open(log_path, "r") as log_file:
log_content = log_file.read() log_content = log_file.read()
raise SbuildError(f"sbuild failed: {e}\nLog output:\n{log_content}") raise SbuildError(f"sbuild failed: {e}\nLog output:\n{log_content}")
except FileNotFoundError: except FileNotFoundError:
raise SbuildError("sbuild not found. Please install sbuild package.") raise SbuildError("sbuild not found. Please install sbuild package.")
def _parse_build_results(self, output_dir: str, log_path: str, def _parse_build_results(
result: subprocess.CompletedProcess) -> Dict[str, Any]: self, output_dir: str, log_path: str, result: subprocess.CompletedProcess
) -> Dict[str, Any]:
"""Parse build results and collect artifacts""" """Parse build results and collect artifacts"""
build_info = { build_info = {
'success': True, "success": True,
'output_dir': output_dir, "output_dir": output_dir,
'log_file': log_path, "log_file": log_path,
'artifacts': [], "artifacts": [],
'metadata': {} "metadata": {},
} }
# Collect build artifacts # Collect build artifacts
artifacts = self._collect_artifacts(output_dir) artifacts = self._collect_artifacts(output_dir)
build_info['artifacts'] = artifacts build_info["artifacts"] = artifacts
# Parse build metadata # Parse build metadata
metadata = self._parse_build_metadata(log_path, output_dir) metadata = self._parse_build_metadata(log_path, output_dir)
build_info['metadata'] = metadata build_info["metadata"] = metadata
return build_info return build_info
@ -164,19 +167,19 @@ class SbuildWrapper:
def _parse_build_metadata(self, log_path: str, output_dir: str) -> Dict[str, Any]: def _parse_build_metadata(self, log_path: str, output_dir: str) -> Dict[str, Any]:
"""Parse build metadata from log and artifacts""" """Parse build metadata from log and artifacts"""
metadata = { metadata = {
'build_time': None, "build_time": None,
'package_name': None, "package_name": None,
'package_version': None, "package_version": None,
'architecture': self.config.architecture, "architecture": self.config.architecture,
'suite': self.config.suite, "suite": self.config.suite,
'chroot': self.config.chroot_name, "chroot": self.config.chroot_name,
'dependencies': [], "dependencies": [],
'build_dependencies': [] "build_dependencies": [],
} }
# Parse log file for metadata # Parse log file for metadata
if os.path.exists(log_path): if os.path.exists(log_path):
with open(log_path, 'r') as log_file: with open(log_path, "r") as log_file:
log_content = log_file.read() log_content = log_file.read()
metadata.update(self._extract_metadata_from_log(log_content)) metadata.update(self._extract_metadata_from_log(log_content))
@ -193,15 +196,16 @@ class SbuildWrapper:
# Extract build time # Extract build time
import re import re
time_match = re.search(r'Build started at (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})', log_content)
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: if time_match:
metadata['build_time'] = time_match.group(1) metadata["build_time"] = time_match.group(1)
# Extract package name and version # Extract package name and version
package_match = re.search(r'Building (\S+) \((\S+)\)', log_content) package_match = re.search(r"Building (\S+) \((\S+)\)", log_content)
if package_match: if package_match:
metadata['package_name'] = package_match.group(1) metadata["package_name"] = package_match.group(1)
metadata['package_version'] = package_match.group(2) metadata["package_version"] = package_match.group(2)
return metadata return metadata
@ -210,17 +214,17 @@ class SbuildWrapper:
metadata = {} metadata = {}
try: try:
with open(changes_file, 'r') as f: with open(changes_file, "r") as f:
content = f.read() content = f.read()
lines = content.split('\n') lines = content.split("\n")
for line in lines: for line in lines:
if line.startswith('Source:'): if line.startswith("Source:"):
metadata['source_package'] = line.split(':', 1)[1].strip() metadata["source_package"] = line.split(":", 1)[1].strip()
elif line.startswith('Version:'): elif line.startswith("Version:"):
metadata['source_version'] = line.split(':', 1)[1].strip() metadata["source_version"] = line.split(":", 1)[1].strip()
elif line.startswith('Architecture:'): elif line.startswith("Architecture:"):
metadata['architectures'] = line.split(':', 1)[1].strip().split() metadata["architectures"] = line.split(":", 1)[1].strip().split()
except Exception: except Exception:
pass pass
@ -233,33 +237,26 @@ class SbuildWrapper:
chroot_name = self.config.chroot_name chroot_name = self.config.chroot_name
# Use dpkg-checkbuilddeps to check dependencies # Use dpkg-checkbuilddeps to check dependencies
cmd = ['schroot', '-c', chroot_name, '--', 'dpkg-checkbuilddeps'] cmd = ["schroot", "-c", chroot_name, "--", "dpkg-checkbuilddeps"]
try: try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True) subprocess.run(cmd, capture_output=True, text=True, check=True)
return { return {"satisfied": True, "missing": [], "conflicts": []}
'satisfied': True,
'missing': [],
'conflicts': []
}
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
# Parse missing dependencies from error output # Parse missing dependencies from error output
missing = self._parse_missing_dependencies(e.stderr) missing = self._parse_missing_dependencies(e.stderr)
return { return {"satisfied": False, "missing": missing, "conflicts": []}
'satisfied': False,
'missing': missing,
'conflicts': []
}
def _parse_missing_dependencies(self, stderr: str) -> List[str]: def _parse_missing_dependencies(self, stderr: str) -> List[str]:
"""Parse missing dependencies from dpkg-checkbuilddeps output""" """Parse missing dependencies from dpkg-checkbuilddeps output"""
missing = [] missing = []
for line in stderr.split('\n'): for line in stderr.split("\n"):
if 'Unmet build dependencies:' in line: if "Unmet build dependencies:" in line:
# Extract package names from the line # Extract package names from the line
import re import re
packages = re.findall(r'\b[a-zA-Z0-9][a-zA-Z0-9+\-\.]*\b', line)
packages = re.findall(r"\b[a-zA-Z0-9][a-zA-Z0-9+\-\.]*\b", line)
missing.extend(packages) missing.extend(packages)
return missing return missing
@ -272,7 +269,15 @@ class SbuildWrapper:
if not dependencies: if not dependencies:
return return
cmd = ['schroot', '-c', chroot_name, '--', 'apt-get', 'install', '-y'] + dependencies cmd = [
"schroot",
"-c",
chroot_name,
"--",
"apt-get",
"install",
"-y",
] + dependencies
try: try:
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)

0
debian/rules vendored Normal file → Executable file
View file

View file

@ -1,5 +0,0 @@
# This is a placeholder for the hello_1.0-1.debian.tar.gz file
# In a real scenario, this would contain the debian/ directory
# with packaging files like debian/control, debian/rules, etc.
# For testing purposes, you would need to create an actual tarball
# containing the Debian packaging files.

View file

@ -1,19 +0,0 @@
Format: 3.0 (quilt)
Source: hello
Binary: hello
Architecture: any
Version: 1.0-1
Maintainer: Deb-Mock Team <team@deb-mock.org>
Homepage: https://github.com/deb-mock/deb-mock
Standards-Version: 4.6.2
Vcs-Browser: https://github.com/deb-mock/deb-mock
Vcs-Git: https://github.com/deb-mock/deb-mock.git
Build-Depends: debhelper-compat (= 13)
Package: hello
Architecture: any
Depends: ${misc:Depends}
Description: Example package for Deb-Mock testing
This is a simple example package used to test the Deb-Mock
build environment. It demonstrates basic package building
functionality.

View file

@ -1,4 +0,0 @@
# This is a placeholder for the hello_1.0.orig.tar.gz file
# In a real scenario, this would be the upstream source tarball
# For testing purposes, you would need to create an actual tarball
# containing the source code for the hello package.

View file

@ -20,3 +20,4 @@ parallel_jobs: 2
keep_chroot: false keep_chroot: false
verbose: true verbose: true
debug: false debug: false

View file

@ -2,10 +2,10 @@
Tests for configuration management Tests for configuration management
""" """
import unittest
import tempfile
import os import os
from pathlib import Path import tempfile
import unittest
from deb_mock.config import Config from deb_mock.config import Config
from deb_mock.exceptions import ConfigurationError from deb_mock.exceptions import ConfigurationError
@ -17,10 +17,10 @@ class TestConfig(unittest.TestCase):
"""Test default configuration creation""" """Test default configuration creation"""
config = Config.default() config = Config.default()
self.assertEqual(config.chroot_name, 'bookworm-amd64') self.assertEqual(config.chroot_name, "bookworm-amd64")
self.assertEqual(config.architecture, 'amd64') self.assertEqual(config.architecture, "amd64")
self.assertEqual(config.suite, 'bookworm') self.assertEqual(config.suite, "bookworm")
self.assertEqual(config.output_dir, './output') self.assertEqual(config.output_dir, "./output")
self.assertFalse(config.keep_chroot) self.assertFalse(config.keep_chroot)
self.assertFalse(config.verbose) self.assertFalse(config.verbose)
self.assertFalse(config.debug) self.assertFalse(config.debug)
@ -28,18 +28,18 @@ class TestConfig(unittest.TestCase):
def test_custom_config(self): def test_custom_config(self):
"""Test custom configuration creation""" """Test custom configuration creation"""
config = Config( config = Config(
chroot_name='sid-amd64', chroot_name="sid-amd64",
architecture='arm64', architecture="arm64",
suite='sid', suite="sid",
output_dir='/tmp/build', output_dir="/tmp/build",
keep_chroot=True, keep_chroot=True,
verbose=True verbose=True,
) )
self.assertEqual(config.chroot_name, 'sid-amd64') self.assertEqual(config.chroot_name, "sid-amd64")
self.assertEqual(config.architecture, 'arm64') self.assertEqual(config.architecture, "arm64")
self.assertEqual(config.suite, 'sid') self.assertEqual(config.suite, "sid")
self.assertEqual(config.output_dir, '/tmp/build') self.assertEqual(config.output_dir, "/tmp/build")
self.assertTrue(config.keep_chroot) self.assertTrue(config.keep_chroot)
self.assertTrue(config.verbose) self.assertTrue(config.verbose)
@ -54,17 +54,17 @@ keep_chroot: true
verbose: true verbose: true
""" """
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
f.write(config_data) f.write(config_data)
config_file = f.name config_file = f.name
try: try:
config = Config.from_file(config_file) config = Config.from_file(config_file)
self.assertEqual(config.chroot_name, 'sid-amd64') self.assertEqual(config.chroot_name, "sid-amd64")
self.assertEqual(config.architecture, 'arm64') self.assertEqual(config.architecture, "arm64")
self.assertEqual(config.suite, 'sid') self.assertEqual(config.suite, "sid")
self.assertEqual(config.output_dir, '/tmp/build') self.assertEqual(config.output_dir, "/tmp/build")
self.assertTrue(config.keep_chroot) self.assertTrue(config.keep_chroot)
self.assertTrue(config.verbose) self.assertTrue(config.verbose)
finally: finally:
@ -72,29 +72,21 @@ verbose: true
def test_config_to_dict(self): def test_config_to_dict(self):
"""Test converting configuration to dictionary""" """Test converting configuration to dictionary"""
config = Config( config = Config(chroot_name="test-chroot", architecture="amd64", suite="bookworm")
chroot_name='test-chroot',
architecture='amd64',
suite='bookworm'
)
config_dict = config.to_dict() config_dict = config.to_dict()
self.assertEqual(config_dict['chroot_name'], 'test-chroot') self.assertEqual(config_dict["chroot_name"], "test-chroot")
self.assertEqual(config_dict['architecture'], 'amd64') self.assertEqual(config_dict["architecture"], "amd64")
self.assertEqual(config_dict['suite'], 'bookworm') self.assertEqual(config_dict["suite"], "bookworm")
self.assertIn('output_dir', config_dict) self.assertIn("output_dir", config_dict)
self.assertIn('keep_chroot', config_dict) self.assertIn("keep_chroot", config_dict)
def test_config_save(self): def test_config_save(self):
"""Test saving configuration to file""" """Test saving configuration to file"""
config = Config( config = Config(chroot_name="test-chroot", architecture="amd64", suite="bookworm")
chroot_name='test-chroot',
architecture='amd64',
suite='bookworm'
)
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
config_file = f.name config_file = f.name
try: try:
@ -112,14 +104,14 @@ verbose: true
def test_invalid_architecture(self): def test_invalid_architecture(self):
"""Test validation of invalid architecture""" """Test validation of invalid architecture"""
config = Config(architecture='invalid-arch') config = Config(architecture="invalid-arch")
with self.assertRaises(ConfigurationError): with self.assertRaises(ConfigurationError):
config.validate() config.validate()
def test_invalid_suite(self): def test_invalid_suite(self):
"""Test validation of invalid suite""" """Test validation of invalid suite"""
config = Config(suite='invalid-suite') config = Config(suite="invalid-suite")
with self.assertRaises(ConfigurationError): with self.assertRaises(ConfigurationError):
config.validate() config.validate()
@ -127,15 +119,15 @@ verbose: true
def test_get_paths(self): def test_get_paths(self):
"""Test path generation methods""" """Test path generation methods"""
config = Config( config = Config(
chroot_dir='/var/lib/chroots', chroot_dir="/var/lib/chroots",
output_dir='./output', output_dir="./output",
metadata_dir='./metadata' metadata_dir="./metadata",
) )
self.assertEqual(config.get_chroot_path(), '/var/lib/chroots/bookworm-amd64') self.assertEqual(config.get_chroot_path(), "/var/lib/chroots/bookworm-amd64")
self.assertEqual(config.get_output_path(), os.path.abspath('./output')) self.assertEqual(config.get_output_path(), os.path.abspath("./output"))
self.assertEqual(config.get_metadata_path(), os.path.abspath('./metadata')) self.assertEqual(config.get_metadata_path(), os.path.abspath("./metadata"))
if __name__ == '__main__': if __name__ == "__main__":
unittest.main() unittest.main()

View file

@ -3,13 +3,20 @@ Tests for the enhanced exception handling system
""" """
import pytest import pytest
import sys
from io import StringIO
from deb_mock.exceptions import ( from deb_mock.exceptions import (
DebMockError, ConfigurationError, ChrootError, SbuildError, BuildError,
BuildError, DependencyError, MetadataError, CacheError, CacheError,
PluginError, NetworkError, PermissionError, ValidationError, ChrootError,
handle_exception, format_error_context ConfigurationError,
DebMockError,
DependencyError,
NetworkError,
PermissionError,
SbuildError,
ValidationError,
format_error_context,
handle_exception,
) )
@ -26,7 +33,7 @@ class TestDebMockError:
def test_error_with_context(self): def test_error_with_context(self):
"""Test error with context information""" """Test error with context information"""
context = {'file': '/path/to/file', 'operation': 'read'} context = {"file": "/path/to/file", "operation": "read"}
error = DebMockError("File operation failed", context=context) error = DebMockError("File operation failed", context=context)
expected = """Error: File operation failed expected = """Error: File operation failed
@ -51,10 +58,9 @@ Suggestions:
def test_error_with_context_and_suggestions(self): def test_error_with_context_and_suggestions(self):
"""Test error with both context and suggestions""" """Test error with both context and suggestions"""
context = {'config_file': '/etc/deb-mock.conf'} context = {"config_file": "/etc/deb-mock.conf"}
suggestions = ["Check config syntax", "Verify file exists"] suggestions = ["Check config syntax", "Verify file exists"]
error = DebMockError("Invalid configuration", error = DebMockError("Invalid configuration", context=context, suggestions=suggestions)
context=context, suggestions=suggestions)
expected = """Error: Invalid configuration expected = """Error: Invalid configuration
@ -88,7 +94,7 @@ class TestSpecificExceptions:
error = ConfigurationError( error = ConfigurationError(
"Invalid configuration", "Invalid configuration",
config_file="/etc/deb-mock.conf", config_file="/etc/deb-mock.conf",
config_section="chroot" config_section="chroot",
) )
assert "config_file: /etc/deb-mock.conf" in str(error) assert "config_file: /etc/deb-mock.conf" in str(error)
@ -102,7 +108,7 @@ class TestSpecificExceptions:
"Failed to create chroot", "Failed to create chroot",
chroot_name="bookworm-amd64", chroot_name="bookworm-amd64",
operation="create", operation="create",
chroot_path="/var/lib/deb-mock/chroots/bookworm-amd64" chroot_path="/var/lib/deb-mock/chroots/bookworm-amd64",
) )
assert "chroot_name: bookworm-amd64" in str(error) assert "chroot_name: bookworm-amd64" in str(error)
@ -116,7 +122,7 @@ class TestSpecificExceptions:
"Build failed", "Build failed",
sbuild_config="/etc/sbuild/sbuild.conf", sbuild_config="/etc/sbuild/sbuild.conf",
build_log="/var/log/sbuild.log", build_log="/var/log/sbuild.log",
return_code=1 return_code=1,
) )
assert "sbuild_config: /etc/sbuild/sbuild.conf" in str(error) assert "sbuild_config: /etc/sbuild/sbuild.conf" in str(error)
@ -130,7 +136,7 @@ class TestSpecificExceptions:
"Package build failed", "Package build failed",
source_package="hello_1.0.dsc", source_package="hello_1.0.dsc",
build_log="/tmp/build.log", build_log="/tmp/build.log",
artifacts=["hello_1.0-1_amd64.deb"] artifacts=["hello_1.0-1_amd64.deb"],
) )
assert "source_package: hello_1.0.dsc" in str(error) assert "source_package: hello_1.0.dsc" in str(error)
@ -143,7 +149,7 @@ class TestSpecificExceptions:
error = DependencyError( error = DependencyError(
"Missing build dependencies", "Missing build dependencies",
missing_packages=["build-essential", "devscripts"], missing_packages=["build-essential", "devscripts"],
conflicting_packages=["old-package"] conflicting_packages=["old-package"],
) )
assert "missing_packages: ['build-essential', 'devscripts']" in str(error) assert "missing_packages: ['build-essential', 'devscripts']" in str(error)
@ -156,7 +162,7 @@ class TestSpecificExceptions:
"Cache operation failed", "Cache operation failed",
cache_type="root_cache", cache_type="root_cache",
cache_path="/var/cache/deb-mock/root-cache", cache_path="/var/cache/deb-mock/root-cache",
operation="restore" operation="restore",
) )
assert "cache_type: root_cache" in str(error) assert "cache_type: root_cache" in str(error)
@ -170,7 +176,7 @@ class TestSpecificExceptions:
"Repository access failed", "Repository access failed",
url="http://deb.debian.org/debian/", url="http://deb.debian.org/debian/",
proxy="http://proxy.example.com:3128", proxy="http://proxy.example.com:3128",
timeout=30 timeout=30,
) )
assert "url: http://deb.debian.org/debian/" in str(error) assert "url: http://deb.debian.org/debian/" in str(error)
@ -184,7 +190,7 @@ class TestSpecificExceptions:
"Insufficient privileges", "Insufficient privileges",
operation="create_chroot", operation="create_chroot",
path="/var/lib/deb-mock", path="/var/lib/deb-mock",
required_privileges="root" required_privileges="root",
) )
assert "operation: create_chroot" in str(error) assert "operation: create_chroot" in str(error)
@ -198,7 +204,7 @@ class TestSpecificExceptions:
"Invalid architecture", "Invalid architecture",
field="architecture", field="architecture",
value="invalid-arch", value="invalid-arch",
expected_format="amd64, i386, arm64, etc." expected_format="amd64, i386, arm64, etc.",
) )
assert "field: architecture" in str(error) assert "field: architecture" in str(error)
@ -212,23 +218,15 @@ class TestHelperFunctions:
def test_format_error_context(self): def test_format_error_context(self):
"""Test format_error_context helper""" """Test format_error_context helper"""
context = format_error_context( context = format_error_context(file="/path/to/file", operation="read", user="testuser", none_value=None)
file="/path/to/file",
operation="read",
user="testuser",
none_value=None
)
expected = { expected = {"file": "/path/to/file", "operation": "read", "user": "testuser"}
'file': '/path/to/file',
'operation': 'read',
'user': 'testuser'
}
assert context == expected assert context == expected
assert 'none_value' not in context assert "none_value" not in context
def test_handle_exception_decorator_success(self): def test_handle_exception_decorator_success(self):
"""Test handle_exception decorator with successful function""" """Test handle_exception decorator with successful function"""
@handle_exception @handle_exception
def successful_function(): def successful_function():
return "success" return "success"
@ -238,6 +236,7 @@ class TestHelperFunctions:
def test_handle_exception_decorator_debmock_error(self, capsys): def test_handle_exception_decorator_debmock_error(self, capsys):
"""Test handle_exception decorator with DebMockError""" """Test handle_exception decorator with DebMockError"""
@handle_exception @handle_exception
def failing_function(): def failing_function():
raise ConfigurationError("Config error", config_file="/etc/config") raise ConfigurationError("Config error", config_file="/etc/config")
@ -253,6 +252,7 @@ class TestHelperFunctions:
def test_handle_exception_decorator_unexpected_error(self, capsys): def test_handle_exception_decorator_unexpected_error(self, capsys):
"""Test handle_exception decorator with unexpected error""" """Test handle_exception decorator with unexpected error"""
@handle_exception @handle_exception
def unexpected_error_function(): def unexpected_error_function():
raise ValueError("Unexpected value error") raise ValueError("Unexpected value error")
@ -276,7 +276,7 @@ class TestExceptionIntegration:
"Failed to create chroot environment", "Failed to create chroot environment",
chroot_name="bookworm-amd64", chroot_name="bookworm-amd64",
operation="debootstrap", operation="debootstrap",
chroot_path="/var/lib/deb-mock/chroots/bookworm-amd64" chroot_path="/var/lib/deb-mock/chroots/bookworm-amd64",
) )
error_str = str(error) error_str = str(error)
@ -300,7 +300,7 @@ class TestExceptionIntegration:
"Package build failed due to compilation errors", "Package build failed due to compilation errors",
source_package="myapp_1.0.dsc", source_package="myapp_1.0.dsc",
build_log="/tmp/build_myapp.log", build_log="/tmp/build_myapp.log",
artifacts=[] artifacts=[],
) )
error_str = str(error) error_str = str(error)
@ -322,7 +322,7 @@ class TestExceptionIntegration:
error = DependencyError( error = DependencyError(
"Unable to resolve build dependencies", "Unable to resolve build dependencies",
missing_packages=["libssl-dev", "libcurl4-openssl-dev"], missing_packages=["libssl-dev", "libcurl4-openssl-dev"],
conflicting_packages=["libssl1.0-dev"] conflicting_packages=["libssl1.0-dev"],
) )
error_str = str(error) error_str = str(error)