#!/usr/bin/env python3 """ Alternative Debian Atomic Solution Uses debootstrap/mmdebstrap + ostree instead of apt-ostree to create Debian Atomic systems Features: - Automatic detection of mmdebstrap (preferred) or debootstrap (fallback) - mmdebstrap is faster and more reliable than debootstrap - Automatic installation of mmdebstrap if not available - Support for all major Debian desktop environments - OSTree integration for atomic system management Usage: python3 create-debian-atomic.py [work_dir] [options] Variants: minimal, gnome, plasma, cosmic, sway, budgie Options: --use-debootstrap, --help, -h """ import os import sys import subprocess import tempfile import shutil import json import yaml from pathlib import Path from typing import List, Dict, Any class DebianAtomicBuilder: """Build Debian Atomic systems using debootstrap/mmdebstrap + ostree""" def __init__(self, work_dir: str = None, use_mmdebstrap: bool = True): self.work_dir = Path(work_dir) if work_dir else Path(tempfile.mkdtemp(prefix="debian-atomic-")) self.repo_dir = self.work_dir / "repo" self.rootfs_dir = self.work_dir / "rootfs" self.use_mmdebstrap = use_mmdebstrap # Check which bootstrap tool is available self.bootstrap_tool = self._detect_bootstrap_tool() def _detect_bootstrap_tool(self) -> str: """Detect which bootstrap tool is available and preferred""" if self.use_mmdebstrap and self._check_tool_available("mmdebstrap"): print("Using mmdebstrap (faster and more reliable)") return "mmdebstrap" elif self._check_tool_available("debootstrap"): print("Using debootstrap (fallback option)") return "debootstrap" else: print("Warning: No bootstrap tool found, attempting to install mmdebstrap") if self._install_mmdebstrap(): return "mmdebstrap" else: raise RuntimeError("No bootstrap tool available and could not install mmdebstrap") def _check_tool_available(self, tool: str) -> bool: """Check if a tool is available in PATH""" try: subprocess.run([tool, "--version"], capture_output=True, check=True) return True except (subprocess.CalledProcessError, FileNotFoundError): return False def _install_mmdebstrap(self) -> bool: """Attempt to install mmdebstrap if not available""" try: print("Attempting to install mmdebstrap...") # Try different package managers install_cmds = [ ["apt-get", "update", "-y"], ["apt-get", "install", "-y", "mmdebstrap"] ] for cmd in install_cmds: result = subprocess.run(cmd, capture_output=True, text=True, check=True) print(f"Successfully ran: {' '.join(cmd)}") return self._check_tool_available("mmdebstrap") except subprocess.CalledProcessError as e: print(f"Failed to install mmdebstrap: {e}") return False def create_ostree_repo(self) -> bool: """Create and initialize OSTree repository""" try: print(f"Creating OSTree repository at {self.repo_dir}") self.repo_dir.mkdir(parents=True, exist_ok=True) # Initialize OSTree repository result = subprocess.run([ "ostree", "init", "--repo", str(self.repo_dir), "--mode=bare" ], capture_output=True, text=True, check=True) print("OSTree repository initialized successfully") return True except subprocess.CalledProcessError as e: print(f"Failed to initialize OSTree repository: {e}") print(f"stdout: {e.stdout}") print(f"stderr: {e.stderr}") return False except Exception as e: print(f"Error creating OSTree repository: {e}") return False def create_rootfs(self, variant: str = "minimal") -> bool: """Create rootfs using detected bootstrap tool""" try: print(f"Creating rootfs for variant: {variant} using {self.bootstrap_tool}") self.rootfs_dir.mkdir(parents=True, exist_ok=True) # Get package list based on variant packages = self.get_packages_for_variant(variant) if self.bootstrap_tool == "mmdebstrap": # Use mmdebstrap (faster and more reliable) cmd = [ "mmdebstrap", "--variant=minbase", "--include=" + ",".join(packages), "--aptopt=Acquire::Check-Valid-Until=false", # Handle expired Release files "--aptopt=Acquire::Check-Date=false", # Disable date checking "trixie", str(self.rootfs_dir), "http://deb.debian.org/debian" ] else: # Use debootstrap (fallback) cmd = [ "debootstrap", "--variant=minbase", "--include=" + ",".join(packages), "trixie", str(self.rootfs_dir), "http://deb.debian.org/debian" ] print(f"Running: {' '.join(cmd)}") result = subprocess.run(cmd, capture_output=True, text=True, check=True) print("Rootfs created successfully") return True except subprocess.CalledProcessError as e: print(f"Failed to create rootfs: {e}") print(f"stdout: {e.stdout}") print(f"stderr: {e.stderr}") return False except Exception as e: print(f"Error creating rootfs: {e}") return False def get_packages_for_variant(self, variant: str) -> List[str]: """Get package list for a specific variant""" # Start with minimal set of packages that definitely exist base_packages = [ "systemd", "dbus", "sudo", "bash", "coreutils", "util-linux", "procps" ] if variant == "minimal": return base_packages + [ "less", "vim-tiny", "wget", "curl", "ca-certificates", "gnupg", "iproute2", "net-tools", "openssh-client", "openssh-server", "htop", "rsync", "tar", "gzip", "unzip" ] elif variant == "gnome": return base_packages + [ "gnome-shell", "gnome-session", "gnome-terminal", "gnome-control-center", "gnome-settings-daemon", "gnome-backgrounds", "gnome-themes-extra", "adwaita-icon-theme", "gdm3", "gnome-initial-setup", "nautilus", "gnome-software", "gnome-tweaks" ] elif variant == "plasma": return base_packages + [ "plasma-desktop", "plasma-workspace", "plasma-nm", "plasma-pa", "kde-config", "kde-runtime", "kde-standard", "sddm", "kwin-x11", "dolphin", "konsole", "kate", "kdeconnect", "plasma-browser-integration" ] elif variant == "cosmic": return base_packages + [ "pop-desktop", "pop-shell", "gnome-shell", "gnome-session", "gnome-terminal", "gnome-control-center", "gnome-settings-daemon", "adwaita-icon-theme", "gdm3", "nautilus", "gnome-software", "pop-gtk-theme" ] elif variant == "sway": return base_packages + [ "sway", "swaybg", "swayidle", "swaylock", "waybar", "wofi", "foot", "grim", "slurp", "wl-clipboard", "mako", "swaymsg", "sway-input" ] elif variant == "budgie": return base_packages + [ "budgie-desktop", "budgie-desktop-view", "budgie-panel", "budgie-menu", "budgie-run-dialog", "budgie-screenshot", "budgie-session", "gnome-session", "gdm3", "adwaita-icon-theme" ] else: return base_packages def create_ostree_commit(self, variant: str, ref: str) -> str: """Create OSTree commit from rootfs""" try: print(f"Creating OSTree commit for variant: {variant}") # Create commit cmd = [ "ostree", "commit", "--repo", str(self.repo_dir), "--branch", ref, "--tree", f"dir={self.rootfs_dir}", f"--subject=Debian Atomic {variant} variant", f"--body=Debian Trixie Atomic system with {variant} desktop" ] print(f"Running: {' '.join(cmd)}") result = subprocess.run(cmd, capture_output=True, text=True, check=True) # Extract commit hash from output commit_hash = result.stdout.strip() print(f"OSTree commit created: {commit_hash}") return commit_hash except subprocess.CalledProcessError as e: print(f"Failed to create OSTree commit: {e}") print(f"stdout: {e.stdout}") print(f"stderr: {e.stderr}") return None except Exception as e: print(f"Error creating OSTree commit: {e}") return None def post_process_rootfs(self, variant: str) -> bool: """Post-process rootfs after debootstrap""" try: print(f"Post-processing rootfs for {variant} variant") # Create essential directories essential_dirs = [ "/etc/apt-ostree", "/var/lib/apt-ostree", "/usr/lib/bootc", "/root/.ssh", "/etc/systemd/system", "/etc/systemd/user" ] for dir_path in essential_dirs: full_path = self.rootfs_dir / dir_path.lstrip("/") full_path.mkdir(parents=True, exist_ok=True) # Set up basic systemd services if variant in ["gnome", "plasma", "cosmic", "budgie"]: # Enable display manager if variant == "plasma": sddm_service = self.rootfs_dir / "etc/systemd/system/display-manager.service" sddm_service.write_text("[Unit]\nDescription=SDDM Display Manager\n\n[Service]\nExecStart=/usr/bin/sddm\nRestart=always\n\n[Install]\nWantedBy=graphical.target\n") elif variant in ["gnome", "cosmic", "budgie"]: gdm_service = self.rootfs_dir / "etc/systemd/system/display-manager.service" gdm_service.write_text("[Unit]\nDescription=GDM Display Manager\n\n[Service]\nExecStart=/usr/sbin/gdm3\nRestart=always\n\n[Install]\nWantedBy=graphical.target\n") # Enable SSH service ssh_service = self.rootfs_dir / "etc/systemd/system/sshd.service" ssh_service.write_text("[Unit]\nDescription=OpenSSH Server\n\n[Service]\nExecStart=/usr/sbin/sshd -D\nRestart=always\n\n[Install]\nWantedBy=multi-user.target\n") print("Rootfs post-processing completed") return True except Exception as e: print(f"Error during post-processing: {e}") return False def build_variant(self, variant: str) -> bool: """Build a complete Debian Atomic variant""" try: print(f"Building Debian Atomic variant: {variant}") # Create OSTree repository if not self.create_ostree_repo(): return False # Create rootfs if not self.create_rootfs(variant): return False # Post-process rootfs if not self.post_process_rootfs(variant): return False # Create OSTree commit ref = f"debian/14/x86_64/{variant}" commit_hash = self.create_ostree_commit(variant, ref) if not commit_hash: return False print(f"Successfully built {variant} variant") print(f"Repository: {self.repo_dir}") print(f"Reference: {ref}") print(f"Commit: {commit_hash}") return True except Exception as e: print(f"Error building variant {variant}: {e}") return False def list_variants(self) -> List[str]: """List all supported variants""" return ["minimal", "gnome", "plasma", "cosmic", "sway", "budgie"] def get_bootstrap_info(self) -> Dict[str, Any]: """Get information about the bootstrap tool being used""" return { "tool": self.bootstrap_tool, "preferred": self.use_mmdebstrap, "available": self._check_tool_available(self.bootstrap_tool), "version": self._get_tool_version() } def _get_tool_version(self) -> str: """Get version of the bootstrap tool""" try: result = subprocess.run([self.bootstrap_tool, "--version"], capture_output=True, text=True, check=True) return result.stdout.strip().split('\n')[0] except: return "Unknown" def cleanup(self): """Clean up temporary files""" try: if self.work_dir.exists(): shutil.rmtree(self.work_dir) print(f"Cleaned up {self.work_dir}") except Exception as e: print(f"Error during cleanup: {e}") def main(): """Main function""" if len(sys.argv) < 2 or "--help" in sys.argv or "-h" in sys.argv: print("Alternative Debian Atomic Builder") print("=================================") print("Usage: python3 create-debian-atomic.py [work_dir] [options]") print("\nVariants:") for variant in DebianAtomicBuilder().list_variants(): print(f" {variant}") print("\nOptions:") print(" --use-debootstrap Force use of debootstrap instead of mmdebstrap") print(" --help, -h Show this help message") print("\nExamples:") print(" python3 create-debian-atomic.py minimal") print(" python3 create-debian-atomic.py gnome /tmp/my-work-dir") print(" python3 create-debian-atomic.py plasma --use-debootstrap") sys.exit(0) variant = sys.argv[1] work_dir = None use_mmdebstrap = True # Parse arguments for i, arg in enumerate(sys.argv[2:], 2): if arg == "--use-debootstrap": use_mmdebstrap = False elif arg in ["--help", "-h"]: continue # Already handled above elif not arg.startswith("--"): work_dir = arg break builder = DebianAtomicBuilder(work_dir=work_dir, use_mmdebstrap=use_mmdebstrap) supported_variants = builder.list_variants() if variant not in supported_variants: print(f"Unknown variant: {variant}") print(f"Supported variants: {', '.join(supported_variants)}") sys.exit(1) try: # Show bootstrap tool information bootstrap_info = builder.get_bootstrap_info() print(f"\n🔧 Bootstrap Tool Information:") print(f" Tool: {bootstrap_info['tool']}") print(f" Version: {bootstrap_info['version']}") print(f" Preferred: {'mmdebstrap' if bootstrap_info['preferred'] else 'debootstrap'}") success = builder.build_variant(variant) if success: print(f"\n✅ Successfully built Debian Atomic {variant} variant") print(f"Repository location: {builder.repo_dir}") print(f"To deploy: ostree admin deploy --os {variant} {builder.repo_dir}") print(f"To list refs: ostree refs --repo={builder.repo_dir}") else: print(f"\n❌ Failed to build Debian Atomic {variant} variant") sys.exit(1) finally: if not work_dir: # Only cleanup if we created a temp directory builder.cleanup() if __name__ == "__main__": main()