Initial commit

This commit is contained in:
robojerk 2025-08-11 08:59:41 -07:00
commit 3326d796f0
87 changed files with 15792 additions and 0 deletions

View file

@ -0,0 +1,4 @@
# apt-stage package
from .apt_stage import AptStage
__all__ = ['AptStage']

View file

@ -0,0 +1,326 @@
#!/usr/bin/env python3
"""
Debian APT Stage for osbuild
This stage handles Debian package installation using apt/dpkg within an osbuild chroot.
It replaces the DNF stage used in Fedora/RHEL systems.
Author: Debian bootc-image-builder team
License: Same as original bootc-image-builder
"""
import os
import subprocess
import tempfile
import json
from typing import Dict, List, Optional, Any
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class AptStage:
"""
osbuild stage for Debian package management using apt/dpkg.
This stage handles:
- Repository configuration
- Package dependency resolution
- Package installation
- Cache management
- OSTree integration considerations
"""
def __init__(self, options: Dict[str, Any]):
"""
Initialize the APT stage with configuration options.
Args:
options: Dictionary containing stage configuration
- packages: List of packages to install
- repos: List of repository configurations
- release: Debian release (e.g., 'trixie', 'bookworm')
- arch: Target architecture (e.g., 'amd64')
- exclude_packages: List of packages to exclude
- install_weak_deps: Whether to install weak dependencies
"""
self.options = options
self.packages = options.get('packages', [])
self.repos = options.get('repos', [])
self.release = options.get('release', 'trixie')
self.arch = options.get('arch', 'amd64')
self.exclude_packages = options.get('exclude_packages', [])
self.install_weak_deps = options.get('install_weak_deps', True)
# Validate required options
if not self.packages:
raise ValueError("No packages specified for installation")
logger.info(f"APT Stage initialized for {self.release} ({self.arch})")
logger.info(f"Packages to install: {self.packages}")
def run(self, context) -> None:
"""
Execute the APT stage within the osbuild context.
Args:
context: osbuild context providing chroot access
"""
logger.info("Starting APT stage execution")
try:
# Step 1: Set up APT configuration in chroot
self._setup_apt_config(context)
# Step 2: Configure repositories
self._configure_repositories(context)
# Step 3: Update package lists
self._update_package_lists(context)
# Step 4: Install packages with dependency resolution
self._install_packages(context)
# Step 5: Clean up APT cache
self._cleanup_cache(context)
logger.info("APT stage completed successfully")
except Exception as e:
logger.error(f"APT stage failed: {e}")
raise
def _setup_apt_config(self, context) -> None:
"""
Set up APT configuration in the chroot environment.
Args:
context: osbuild context
"""
logger.info("Setting up APT configuration")
# Create /etc/apt/apt.conf.d/ directory if it doesn't exist
apt_conf_dir = os.path.join(context.root, "etc", "apt", "apt.conf.d")
os.makedirs(apt_conf_dir, exist_ok=True)
# Configure APT for chroot environment
apt_config = [
'Acquire::Check-Valid-Until "false";',
'Acquire::Languages "none";',
'Acquire::GzipIndexes "true";',
'Acquire::CompressionTypes::Order:: "gz";',
'Dpkg::Options::="--force-confdef";',
'Dpkg::Options::="--force-confold";',
'Dpkg::Use-Pty "false";',
'Dpkg::Progress-Fancy "0";',
]
# Write APT configuration
config_file = os.path.join(apt_conf_dir, "99osbuild")
with open(config_file, 'w') as f:
f.write('\n'.join(apt_config))
# Set proper permissions
os.chmod(config_file, 0o644)
logger.info("APT configuration set up successfully")
def _configure_repositories(self, context) -> None:
"""
Configure APT repositories in the chroot.
Args:
context: osbuild context
"""
logger.info("Configuring APT repositories")
# Create /etc/apt/sources.list.d/ directory
sources_dir = os.path.join(context.root, "etc", "apt", "sources.list.d")
os.makedirs(sources_dir, exist_ok=True)
# Default Debian repositories if none specified
if not self.repos:
self.repos = [
{
"name": "debian",
"url": f"http://deb.debian.org/debian",
"suite": self.release,
"components": ["main", "contrib", "non-free"]
},
{
"name": "debian-security",
"url": f"http://deb.debian.org/debian-security",
"suite": f"{self.release}-security",
"components": ["main", "contrib", "non-free"]
},
{
"name": "debian-updates",
"url": f"http://deb.debian.org/debian",
"suite": f"{self.release}-updates",
"components": ["main", "contrib", "non-free"]
}
]
# Write repository configurations
for repo in self.repos:
repo_name = repo.get("name", "debian")
repo_url = repo.get("url", "http://deb.debian.org/debian")
repo_suite = repo.get("suite", self.release)
repo_components = repo.get("components", ["main"])
# Create sources.list entry
sources_entry = f"deb {repo_url} {repo_suite} {' '.join(repo_components)}\n"
# Write to sources.list.d file
sources_file = os.path.join(sources_dir, f"{repo_name}.list")
with open(sources_file, 'w') as f:
f.write(sources_entry)
# Set proper permissions
os.chmod(sources_file, 0o644)
logger.info(f"Configured {len(self.repos)} repositories")
def _update_package_lists(self, context) -> None:
"""
Update package lists in the chroot.
Args:
context: osbuild context
"""
logger.info("Updating package lists")
# Run apt-get update in chroot
cmd = ["apt-get", "update"]
result = context.run(cmd)
if result.returncode != 0:
raise RuntimeError(f"Failed to update package lists: {result.stderr}")
logger.info("Package lists updated successfully")
def _install_packages(self, context) -> None:
"""
Install packages with dependency resolution.
Args:
context: osbuild context
"""
logger.info(f"Installing {len(self.packages)} packages")
# Build apt-get install command
cmd = ["apt-get", "install", "-y", "--no-install-recommends"]
# Add architecture specification if needed
if self.arch != 'amd64':
cmd.extend(["-o", f"APT::Architecture={self.arch}"])
# Add package list
cmd.extend(self.packages)
# Run package installation
result = context.run(cmd)
if result.returncode != 0:
# Log detailed error information
logger.error(f"Package installation failed: {result.stderr}")
logger.error(f"Command executed: {' '.join(cmd)}")
# Try to get more detailed error information
self._log_apt_errors(context)
raise RuntimeError("Package installation failed")
logger.info("Package installation completed successfully")
def _cleanup_cache(self, context) -> None:
"""
Clean up APT cache to reduce image size.
Args:
context: osbuild context
"""
logger.info("Cleaning up APT cache")
# Clean package cache
cmd = ["apt-get", "clean"]
result = context.run(cmd)
if result.returncode != 0:
logger.warning(f"Cache cleanup failed: {result.stderr}")
else:
logger.info("APT cache cleaned successfully")
# Remove package lists
cmd = ["rm", "-rf", "/var/lib/apt/lists/*"]
result = context.run(cmd)
if result.returncode != 0:
logger.warning(f"Package list cleanup failed: {result.stderr}")
else:
logger.info("Package lists removed successfully")
def _log_apt_errors(self, context) -> None:
"""
Log detailed APT error information for debugging.
Args:
context: osbuild context
"""
logger.info("Collecting detailed APT error information")
# Check APT status
cmd = ["apt-get", "check"]
result = context.run(cmd)
logger.info(f"APT check result: {result.stdout}")
# Check for broken packages
cmd = ["dpkg", "--audit"]
result = context.run(cmd)
if result.stdout:
logger.error(f"Broken packages detected: {result.stdout}")
# Check package status
cmd = ["dpkg", "-l"]
result = context.run(cmd)
logger.debug(f"Package status: {result.stdout}")
def main():
"""
Main entry point for the APT stage.
This function is called by osbuild when executing the stage.
"""
import sys
# Read options from stdin (osbuild passes options as JSON)
options = json.load(sys.stdin)
# Create and run the stage
stage = AptStage(options)
# Note: In a real osbuild stage, the context would be provided by osbuild
# For now, this is a placeholder for testing
class MockContext:
def __init__(self, root):
self.root = root
def run(self, cmd):
# Mock implementation for testing
logger.info(f"Would run: {' '.join(cmd)}")
return type('Result', (), {'returncode': 0, 'stdout': '', 'stderr': ''})()
# For testing purposes
if len(sys.argv) > 1 and sys.argv[1] == '--test':
context = MockContext('/tmp/test-chroot')
stage.run(context)
else:
# In real osbuild environment, context would be provided
raise NotImplementedError("This stage must be run within osbuild")
if __name__ == "__main__":
main()