#!/usr/bin/env python3 """ Alternative Debian Atomic Solution Uses debootstrap + ostree instead of apt-ostree to create Debian Atomic systems """ 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 + ostree""" def __init__(self, work_dir: str = None): 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" 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 debootstrap""" try: print(f"Creating rootfs for variant: {variant}") self.rootfs_dir.mkdir(parents=True, exist_ok=True) # Get package list based on variant packages = self.get_packages_for_variant(variant) # Create minimal rootfs with debootstrap 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 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: print("Usage: python3 create-debian-atomic.py [work_dir]") print("Variants:", ", ".join(DebianAtomicBuilder().list_variants())) sys.exit(1) variant = sys.argv[1] work_dir = sys.argv[2] if len(sys.argv) > 2 else None builder = DebianAtomicBuilder() 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: 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()