438 lines
16 KiB
Python
438 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Debian Variants and Flavors Manager
|
|
|
|
This module manages Debian variants (stable, testing, unstable, backports)
|
|
and desktop environment flavors (GNOME, KDE, XFCE, MATE).
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
from typing import Dict, List, Optional, Any, Set
|
|
from dataclasses import dataclass, asdict
|
|
from pathlib import Path
|
|
import urllib.request
|
|
import urllib.parse
|
|
from datetime import datetime, timedelta
|
|
|
|
@dataclass
|
|
class DebianVariant:
|
|
"""Represents a Debian variant"""
|
|
name: str
|
|
codename: str
|
|
version: str
|
|
status: str # stable, testing, unstable
|
|
release_date: Optional[datetime]
|
|
end_of_life: Optional[datetime]
|
|
architectures: List[str]
|
|
mirrors: List[str]
|
|
security_support: bool
|
|
updates_support: bool
|
|
backports_support: bool
|
|
|
|
@dataclass
|
|
class DesktopFlavor:
|
|
"""Represents a desktop environment flavor"""
|
|
name: str
|
|
display_name: str
|
|
description: str
|
|
packages: List[str]
|
|
dependencies: List[str]
|
|
variants: List[str] # Which Debian variants support this flavor
|
|
enabled: bool = True
|
|
priority: int = 500
|
|
|
|
class DebianVariantsManager:
|
|
"""Manages Debian variants and desktop flavors"""
|
|
|
|
def __init__(self, config_dir: str = "./config/variants"):
|
|
self.config_dir = Path(config_dir)
|
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
self.variants_file = self.config_dir / "variants.json"
|
|
self.flavors_file = self.config_dir / "flavors.json"
|
|
self._load_configuration()
|
|
|
|
def _load_configuration(self):
|
|
"""Load variants and flavors configuration"""
|
|
# Load variants
|
|
if self.variants_file.exists():
|
|
with open(self.variants_file, 'r') as f:
|
|
self.variants = json.load(f)
|
|
else:
|
|
self.variants = self._get_default_variants()
|
|
self._save_variants()
|
|
|
|
# Load flavors
|
|
if self.flavors_file.exists():
|
|
with open(self.flavors_file, 'r') as f:
|
|
self.flavors = json.load(f)
|
|
else:
|
|
self.flavors = self._get_default_flavors()
|
|
self._save_flavors()
|
|
|
|
def _get_default_variants(self) -> Dict[str, Any]:
|
|
"""Get default Debian variants configuration"""
|
|
return {
|
|
"variants": [
|
|
{
|
|
"name": "bookworm",
|
|
"codename": "Bookworm",
|
|
"version": "12",
|
|
"status": "stable",
|
|
"release_date": "2023-06-10T00:00:00",
|
|
"end_of_life": "2026-06-10T00:00:00",
|
|
"architectures": ["amd64", "arm64", "armel", "armhf", "i386", "mips64el", "mipsel", "ppc64el", "s390x"],
|
|
"mirrors": [
|
|
"http://deb.debian.org/debian",
|
|
"http://security.debian.org/debian-security"
|
|
],
|
|
"security_support": True,
|
|
"updates_support": True,
|
|
"backports_support": True
|
|
},
|
|
{
|
|
"name": "sid",
|
|
"codename": "Sid",
|
|
"version": "unstable",
|
|
"status": "unstable",
|
|
"release_date": None,
|
|
"end_of_life": None,
|
|
"architectures": ["amd64", "arm64", "armel", "armhf", "i386", "mips64el", "mipsel", "ppc64el", "s390x"],
|
|
"mirrors": [
|
|
"http://deb.debian.org/debian"
|
|
],
|
|
"security_support": False,
|
|
"updates_support": False,
|
|
"backports_support": False
|
|
},
|
|
{
|
|
"name": "testing",
|
|
"codename": "Trixie",
|
|
"version": "13",
|
|
"status": "testing",
|
|
"release_date": None,
|
|
"end_of_life": None,
|
|
"architectures": ["amd64", "arm64", "armel", "armhf", "i386", "mips64el", "mipsel", "ppc64el", "s390x"],
|
|
"mirrors": [
|
|
"http://deb.debian.org/debian"
|
|
],
|
|
"security_support": False,
|
|
"updates_support": False,
|
|
"backports_support": False
|
|
}
|
|
]
|
|
}
|
|
|
|
def _get_default_flavors(self) -> Dict[str, Any]:
|
|
"""Get default desktop flavors configuration"""
|
|
return {
|
|
"flavors": [
|
|
{
|
|
"name": "gnome",
|
|
"display_name": "GNOME",
|
|
"description": "Modern, intuitive desktop environment",
|
|
"packages": ["task-gnome-desktop", "gnome-core"],
|
|
"dependencies": ["gnome-session", "gnome-shell", "gdm3"],
|
|
"variants": ["bookworm", "sid", "testing"],
|
|
"enabled": True,
|
|
"priority": 100
|
|
},
|
|
{
|
|
"name": "kde",
|
|
"display_name": "KDE Plasma",
|
|
"description": "Feature-rich, customizable desktop",
|
|
"packages": ["task-kde-desktop", "plasma-desktop"],
|
|
"dependencies": ["kde-plasma-desktop", "sddm"],
|
|
"variants": ["bookworm", "sid", "testing"],
|
|
"enabled": True,
|
|
"priority": 200
|
|
},
|
|
{
|
|
"name": "xfce",
|
|
"display_name": "Xfce",
|
|
"description": "Lightweight, fast desktop environment",
|
|
"packages": ["task-xfce-desktop", "xfce4"],
|
|
"dependencies": ["xfce4-session", "lightdm"],
|
|
"variants": ["bookworm", "sid", "testing"],
|
|
"enabled": True,
|
|
"priority": 300
|
|
},
|
|
{
|
|
"name": "mate",
|
|
"display_name": "MATE",
|
|
"description": "Traditional GNOME 2 desktop",
|
|
"packages": ["task-mate-desktop", "mate-desktop"],
|
|
"dependencies": ["mate-session-manager", "lightdm"],
|
|
"variants": ["bookworm", "sid", "testing"],
|
|
"enabled": True,
|
|
"priority": 400
|
|
},
|
|
{
|
|
"name": "minimal",
|
|
"display_name": "Minimal",
|
|
"description": "Minimal system without desktop",
|
|
"packages": [],
|
|
"dependencies": [],
|
|
"variants": ["bookworm", "sid", "testing"],
|
|
"enabled": True,
|
|
"priority": 500
|
|
}
|
|
]
|
|
}
|
|
|
|
def _save_variants(self):
|
|
"""Save variants configuration"""
|
|
with open(self.variants_file, 'w') as f:
|
|
json.dump(self.variants, f, indent=2)
|
|
|
|
def _save_flavors(self):
|
|
"""Save flavors configuration"""
|
|
with open(self.flavors_file, 'w') as f:
|
|
json.dump(self.flavors, f, indent=2)
|
|
|
|
def get_variant(self, name: str) -> Optional[Dict[str, Any]]:
|
|
"""Get variant configuration by name"""
|
|
for variant in self.variants["variants"]:
|
|
if variant["name"] == name:
|
|
return variant
|
|
return None
|
|
|
|
def get_flavor(self, name: str) -> Optional[Dict[str, Any]]:
|
|
"""Get flavor configuration by name"""
|
|
for flavor in self.flavors["flavors"]:
|
|
if flavor["name"] == name:
|
|
return flavor
|
|
return None
|
|
|
|
def list_variants(self, status: str = None) -> List[Dict[str, Any]]:
|
|
"""List variants, optionally filtered by status"""
|
|
if status:
|
|
return [v for v in self.variants["variants"] if v["status"] == status]
|
|
return self.variants["variants"]
|
|
|
|
def list_flavors(self, variant: str = None) -> List[Dict[str, Any]]:
|
|
"""List flavors, optionally filtered by variant support"""
|
|
if variant:
|
|
return [f for f in self.flavors["flavors"] if variant in f["variants"]]
|
|
return self.flavors["flavors"]
|
|
|
|
def add_variant(self, variant: DebianVariant) -> bool:
|
|
"""Add a new Debian variant"""
|
|
try:
|
|
# Check if variant already exists
|
|
for existing_variant in self.variants["variants"]:
|
|
if existing_variant["name"] == variant.name:
|
|
print(f"Variant {variant.name} already exists")
|
|
return False
|
|
|
|
# Add new variant
|
|
self.variants["variants"].append(asdict(variant))
|
|
self._save_variants()
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"Failed to add variant: {e}")
|
|
return False
|
|
|
|
def add_flavor(self, flavor: DesktopFlavor) -> bool:
|
|
"""Add a new desktop flavor"""
|
|
try:
|
|
# Check if flavor already exists
|
|
for existing_flavor in self.flavors["flavors"]:
|
|
if existing_flavor["name"] == flavor.name:
|
|
print(f"Flavor {flavor.name} already exists")
|
|
return False
|
|
|
|
# Add new flavor
|
|
self.flavors["flavors"].append(asdict(flavor))
|
|
self._save_flavors()
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"Failed to add flavor: {e}")
|
|
return False
|
|
|
|
def update_variant(self, name: str, **kwargs) -> bool:
|
|
"""Update variant configuration"""
|
|
try:
|
|
for variant in self.variants["variants"]:
|
|
if variant["name"] == name:
|
|
for key, value in kwargs.items():
|
|
if key in variant:
|
|
variant[key] = value
|
|
self._save_variants()
|
|
return True
|
|
|
|
print(f"Variant {name} not found")
|
|
return False
|
|
|
|
except Exception as e:
|
|
print(f"Failed to update variant: {e}")
|
|
return False
|
|
|
|
def update_flavor(self, name: str, **kwargs) -> bool:
|
|
"""Update flavor configuration"""
|
|
try:
|
|
for flavor in self.flavors["flavors"]:
|
|
if flavor["name"] == name:
|
|
for key, value in kwargs.items():
|
|
if key in flavor:
|
|
flavor[key] = value
|
|
self._save_flavors()
|
|
return True
|
|
|
|
print(f"Flavor {name} not found")
|
|
return False
|
|
|
|
except Exception as e:
|
|
print(f"Failed to update flavor: {e}")
|
|
return False
|
|
|
|
def get_variant_mirrors(self, variant_name: str) -> List[str]:
|
|
"""Get mirrors for a specific variant"""
|
|
variant = self.get_variant(variant_name)
|
|
if variant:
|
|
return variant.get("mirrors", [])
|
|
return []
|
|
|
|
def get_variant_architectures(self, variant_name: str) -> List[str]:
|
|
"""Get supported architectures for a variant"""
|
|
variant = self.get_variant(variant_name)
|
|
if variant:
|
|
return variant.get("architectures", [])
|
|
return []
|
|
|
|
def get_flavor_packages(self, flavor_name: str, variant_name: str = None) -> List[str]:
|
|
"""Get packages for a flavor, optionally filtered by variant"""
|
|
flavor = self.get_flavor(flavor_name)
|
|
if not flavor:
|
|
return []
|
|
|
|
packages = flavor.get("packages", [])
|
|
dependencies = flavor.get("dependencies", [])
|
|
|
|
# Filter by variant if specified
|
|
if variant_name and variant_name not in flavor.get("variants", []):
|
|
return []
|
|
|
|
return packages + dependencies
|
|
|
|
def create_variant_sources_list(self, variant_name: str, architecture: str = "amd64") -> str:
|
|
"""Create sources.list content for a variant"""
|
|
variant = self.get_variant(variant_name)
|
|
if not variant:
|
|
return ""
|
|
|
|
sources_list = []
|
|
|
|
# Main repository
|
|
for mirror in variant.get("mirrors", []):
|
|
if "security.debian.org" in mirror:
|
|
continue # Security handled separately
|
|
|
|
sources_list.append(f"deb {mirror} {variant_name} main contrib non-free-firmware")
|
|
|
|
# Updates repository
|
|
if variant.get("updates_support", False):
|
|
sources_list.append(f"deb {mirror} {variant_name}-updates main contrib non-free-firmware")
|
|
|
|
# Backports repository
|
|
if variant.get("backports_support", False):
|
|
sources_list.append(f"deb {mirror} {variant_name}-backports main contrib non-free-firmware")
|
|
|
|
# Security repository
|
|
if variant.get("security_support", False):
|
|
security_mirrors = [m for m in variant.get("mirrors", []) if "security.debian.org" in m]
|
|
for mirror in security_mirrors:
|
|
sources_list.append(f"deb {mirror} {variant_name}-security main contrib non-free-firmware")
|
|
|
|
return "\n".join(sources_list)
|
|
|
|
def get_variant_status_summary(self) -> Dict[str, Any]:
|
|
"""Get summary of variant statuses"""
|
|
summary = {
|
|
"stable": [],
|
|
"testing": [],
|
|
"unstable": [],
|
|
"total": len(self.variants["variants"])
|
|
}
|
|
|
|
for variant in self.variants["variants"]:
|
|
status = variant.get("status", "unknown")
|
|
if status in summary:
|
|
summary[status].append(variant["name"])
|
|
|
|
return summary
|
|
|
|
def get_flavor_compatibility_matrix(self) -> Dict[str, List[str]]:
|
|
"""Get compatibility matrix between variants and flavors"""
|
|
matrix = {}
|
|
|
|
for variant in self.variants["variants"]:
|
|
variant_name = variant["name"]
|
|
matrix[variant_name] = []
|
|
|
|
for flavor in self.flavors["flavors"]:
|
|
if variant_name in flavor.get("variants", []):
|
|
matrix[variant_name].append(flavor["name"])
|
|
|
|
return matrix
|
|
|
|
def validate_variant_flavor_combination(self, variant_name: str, flavor_name: str) -> bool:
|
|
"""Validate if a variant and flavor combination is supported"""
|
|
variant = self.get_variant(variant_name)
|
|
flavor = self.get_flavor(flavor_name)
|
|
|
|
if not variant or not flavor:
|
|
return False
|
|
|
|
return variant_name in flavor.get("variants", [])
|
|
|
|
def get_recommended_flavor_for_variant(self, variant_name: str) -> Optional[str]:
|
|
"""Get recommended flavor for a variant based on priority"""
|
|
compatible_flavors = []
|
|
|
|
for flavor in self.flavors["flavors"]:
|
|
if variant_name in flavor.get("variants", []) and flavor.get("enabled", True):
|
|
compatible_flavors.append((flavor["name"], flavor.get("priority", 500)))
|
|
|
|
if compatible_flavors:
|
|
# Sort by priority (lower is higher priority)
|
|
compatible_flavors.sort(key=lambda x: x[1])
|
|
return compatible_flavors[0][0]
|
|
|
|
return None
|
|
|
|
def main():
|
|
"""Test variants and flavors management"""
|
|
manager = DebianVariantsManager()
|
|
|
|
# List variants
|
|
print("Debian Variants:")
|
|
variants = manager.list_variants()
|
|
for variant in variants:
|
|
print(f" - {variant['name']} ({variant['codename']}): {variant['status']}")
|
|
|
|
# List flavors
|
|
print("\nDesktop Flavors:")
|
|
flavors = manager.list_flavors()
|
|
for flavor in flavors:
|
|
print(f" - {flavor['display_name']}: {flavor['description']}")
|
|
|
|
# Show compatibility matrix
|
|
print("\nVariant-Flavor Compatibility:")
|
|
matrix = manager.get_flavor_compatibility_matrix()
|
|
for variant, supported_flavors in matrix.items():
|
|
print(f" {variant}: {', '.join(supported_flavors)}")
|
|
|
|
# Show variant status summary
|
|
print("\nVariant Status Summary:")
|
|
summary = manager.get_variant_status_summary()
|
|
for status, variant_list in summary.items():
|
|
if status != "total":
|
|
print(f" {status}: {', '.join(variant_list)}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|