did stuff
This commit is contained in:
parent
0ee5aa76d9
commit
7572de6f46
5 changed files with 974 additions and 0 deletions
142
.forgejo/workflows/ci.yml
Normal file
142
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
---
|
||||||
|
name: Blue Build Modules CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEBIAN_FRONTEND: noninteractive
|
||||||
|
PYTHON_VERSION: "3.11"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-package:
|
||||||
|
name: Build and Package Python Modules
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: python:3.11-bullseye
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python environment
|
||||||
|
run: |
|
||||||
|
python3 --version
|
||||||
|
pip3 --version
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
devscripts \
|
||||||
|
debhelper \
|
||||||
|
python3-dev \
|
||||||
|
python3-setuptools \
|
||||||
|
python3-pip \
|
||||||
|
python3-wheel \
|
||||||
|
git \
|
||||||
|
ca-certificates
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
pip3 install --upgrade pip setuptools wheel
|
||||||
|
if [ -f requirements.txt ]; then
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create debian directory
|
||||||
|
run: |
|
||||||
|
mkdir -p debian
|
||||||
|
cat > debian/control << EOF
|
||||||
|
Source: blue-build-modules
|
||||||
|
Section: python
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: Blue Build Team <team@blue-build.org>
|
||||||
|
Build-Depends: debhelper (>= 13), python3-dev, python3-setuptools, python3-pip, python3-wheel, git, ca-certificates
|
||||||
|
Standards-Version: 4.6.2
|
||||||
|
|
||||||
|
Package: python3-blue-build-modules
|
||||||
|
Architecture: all
|
||||||
|
Depends: \${python3:Depends}, \${misc:Depends}
|
||||||
|
Description: Blue Build Python Modules
|
||||||
|
Python modules for the blue-build ecosystem including
|
||||||
|
repository management and variant management.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > debian/rules << EOF
|
||||||
|
#!/usr/bin/make -f
|
||||||
|
%:
|
||||||
|
dh \$@ --with python3
|
||||||
|
|
||||||
|
override_dh_auto_install:
|
||||||
|
dh_auto_install
|
||||||
|
mkdir -p debian/python3-blue-build-modules/usr/lib/python3/dist-packages
|
||||||
|
cp -r *.py debian/python3-blue-build-modules/usr/lib/python3/dist-packages/
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > debian/changelog << EOF
|
||||||
|
blue-build-modules (1.0.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Initial release
|
||||||
|
* Blue Build Python modules implementation
|
||||||
|
|
||||||
|
-- Blue Build Team <team@blue-build.org> $(date -R)
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat > debian/compat << EOF
|
||||||
|
13
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x debian/rules
|
||||||
|
|
||||||
|
- name: Build Debian package
|
||||||
|
run: |
|
||||||
|
dpkg-buildpackage -us -uc -b
|
||||||
|
ls -la ../*.deb
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: blue-build-modules-deb
|
||||||
|
path: ../*.deb
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Test Python Modules
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: python:3.11-bullseye
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Python environment
|
||||||
|
run: |
|
||||||
|
python3 --version
|
||||||
|
pip3 --version
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
run: |
|
||||||
|
pip3 install --upgrade pip setuptools wheel
|
||||||
|
if [ -f requirements.txt ]; then
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run Python tests
|
||||||
|
run: |
|
||||||
|
if [ -f setup.py ]; then
|
||||||
|
python3 setup.py test
|
||||||
|
else
|
||||||
|
echo "No setup.py found, skipping tests"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Test module imports
|
||||||
|
run: |
|
||||||
|
python3 -c "import debian_repository_manager; print('debian_repository_manager imported successfully')" || echo "Module not ready yet"
|
||||||
|
python3 -c "import debian_variants_manager; print('debian_variants_manager imported successfully')" || echo "Module not ready yet"
|
||||||
BIN
__pycache__/debian_repository_manager.cpython-313.pyc
Normal file
BIN
__pycache__/debian_repository_manager.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/debian_variants_manager.cpython-313.pyc
Normal file
BIN
__pycache__/debian_variants_manager.cpython-313.pyc
Normal file
Binary file not shown.
394
debian_repository_manager.py
Normal file
394
debian_repository_manager.py
Normal file
|
|
@ -0,0 +1,394 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Debian Repository Manager for Debian Forge
|
||||||
|
|
||||||
|
This module provides Debian repository management for OSBuild Composer,
|
||||||
|
handling repository configuration, mirror management, and package sources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
from typing import Dict, List, Optional, Any
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from pathlib import Path
|
||||||
|
import urllib.parse
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DebianRepository:
|
||||||
|
"""Represents a Debian repository configuration"""
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
suite: str
|
||||||
|
components: List[str]
|
||||||
|
enabled: bool = True
|
||||||
|
priority: int = 500
|
||||||
|
authentication: Optional[Dict[str, str]] = None
|
||||||
|
proxy: Optional[str] = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RepositoryMirror:
|
||||||
|
"""Represents a Debian mirror configuration"""
|
||||||
|
name: str
|
||||||
|
url: str
|
||||||
|
region: str
|
||||||
|
protocol: str = "http"
|
||||||
|
enabled: bool = True
|
||||||
|
health_check: bool = True
|
||||||
|
|
||||||
|
class DebianRepositoryManager:
|
||||||
|
"""Manages Debian repositories for composer builds"""
|
||||||
|
|
||||||
|
def __init__(self, config_dir: str = "/etc/debian-forge/repositories"):
|
||||||
|
self.config_dir = Path(config_dir)
|
||||||
|
self.config_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.repositories_file = self.config_dir / "repositories.json"
|
||||||
|
self.mirrors_file = self.config_dir / "mirrors.json"
|
||||||
|
self._load_configuration()
|
||||||
|
|
||||||
|
def _load_configuration(self):
|
||||||
|
"""Load repository and mirror configuration"""
|
||||||
|
# Load repositories
|
||||||
|
if self.repositories_file.exists():
|
||||||
|
with open(self.repositories_file, 'r') as f:
|
||||||
|
self.repositories = json.load(f)
|
||||||
|
else:
|
||||||
|
self.repositories = self._get_default_repositories()
|
||||||
|
self._save_repositories()
|
||||||
|
|
||||||
|
# Load mirrors
|
||||||
|
if self.mirrors_file.exists():
|
||||||
|
with open(self.mirrors_file, 'r') as f:
|
||||||
|
self.mirrors = json.load(f)
|
||||||
|
else:
|
||||||
|
self.mirrors = self._get_default_mirrors()
|
||||||
|
self._save_mirrors()
|
||||||
|
|
||||||
|
def _get_default_repositories(self) -> Dict[str, Any]:
|
||||||
|
"""Get default Debian repository configuration"""
|
||||||
|
return {
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"name": "debian-main",
|
||||||
|
"url": "http://deb.debian.org/debian",
|
||||||
|
"suite": "bookworm",
|
||||||
|
"components": ["main"],
|
||||||
|
"enabled": True,
|
||||||
|
"priority": 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debian-security",
|
||||||
|
"url": "http://security.debian.org/debian-security",
|
||||||
|
"suite": "bookworm-security",
|
||||||
|
"components": ["main"],
|
||||||
|
"enabled": True,
|
||||||
|
"priority": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debian-updates",
|
||||||
|
"url": "http://deb.debian.org/debian",
|
||||||
|
"suite": "bookworm-updates",
|
||||||
|
"components": ["main"],
|
||||||
|
"enabled": True,
|
||||||
|
"priority": 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debian-backports",
|
||||||
|
"url": "http://deb.debian.org/debian",
|
||||||
|
"suite": "bookworm-backports",
|
||||||
|
"components": ["main", "contrib", "non-free-firmware"],
|
||||||
|
"enabled": False,
|
||||||
|
"priority": 300
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_default_mirrors(self) -> Dict[str, Any]:
|
||||||
|
"""Get default Debian mirror configuration"""
|
||||||
|
return {
|
||||||
|
"mirrors": [
|
||||||
|
{
|
||||||
|
"name": "debian-official",
|
||||||
|
"url": "http://deb.debian.org/debian",
|
||||||
|
"region": "global",
|
||||||
|
"protocol": "http",
|
||||||
|
"enabled": True,
|
||||||
|
"health_check": True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "debian-security",
|
||||||
|
"url": "http://security.debian.org/debian-security",
|
||||||
|
"region": "global",
|
||||||
|
"protocol": "http",
|
||||||
|
"enabled": True,
|
||||||
|
"health_check": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def _save_repositories(self):
|
||||||
|
"""Save repository configuration to file"""
|
||||||
|
with open(self.repositories_file, 'w') as f:
|
||||||
|
json.dump(self.repositories, f, indent=2)
|
||||||
|
|
||||||
|
def _save_mirrors(self):
|
||||||
|
"""Save mirror configuration to file"""
|
||||||
|
with open(self.mirrors_file, 'w') as f:
|
||||||
|
json.dump(self.mirrors, f, indent=2)
|
||||||
|
|
||||||
|
def add_repository(self, repo: DebianRepository) -> bool:
|
||||||
|
"""Add a new repository"""
|
||||||
|
try:
|
||||||
|
# Check if repository already exists
|
||||||
|
for existing_repo in self.repositories["repositories"]:
|
||||||
|
if existing_repo["name"] == repo.name:
|
||||||
|
print(f"Repository {repo.name} already exists")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Add new repository
|
||||||
|
self.repositories["repositories"].append(asdict(repo))
|
||||||
|
self._save_repositories()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to add repository: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def remove_repository(self, name: str) -> bool:
|
||||||
|
"""Remove a repository by name"""
|
||||||
|
try:
|
||||||
|
self.repositories["repositories"] = [
|
||||||
|
repo for repo in self.repositories["repositories"]
|
||||||
|
if repo["name"] != name
|
||||||
|
]
|
||||||
|
self._save_repositories()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to remove repository: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_repository(self, name: str, **kwargs) -> bool:
|
||||||
|
"""Update repository configuration"""
|
||||||
|
try:
|
||||||
|
for repo in self.repositories["repositories"]:
|
||||||
|
if repo["name"] == name:
|
||||||
|
for key, value in kwargs.items():
|
||||||
|
if key in repo:
|
||||||
|
repo[key] = value
|
||||||
|
self._save_repositories()
|
||||||
|
return True
|
||||||
|
|
||||||
|
print(f"Repository {name} not found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to update repository: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_repository(self, name: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Get repository configuration by name"""
|
||||||
|
for repo in self.repositories["repositories"]:
|
||||||
|
if repo["name"] == name:
|
||||||
|
return repo
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_repositories(self) -> List[Dict[str, Any]]:
|
||||||
|
"""List all repositories"""
|
||||||
|
return self.repositories["repositories"]
|
||||||
|
|
||||||
|
def get_enabled_repositories(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Get all enabled repositories"""
|
||||||
|
return [repo for repo in self.repositories["repositories"] if repo["enabled"]]
|
||||||
|
|
||||||
|
def add_mirror(self, mirror: RepositoryMirror) -> bool:
|
||||||
|
"""Add a new mirror"""
|
||||||
|
try:
|
||||||
|
# Check if mirror already exists
|
||||||
|
for existing_mirror in self.mirrors["mirrors"]:
|
||||||
|
if existing_mirror["name"] == mirror.name:
|
||||||
|
print(f"Mirror {mirror.name} already exists")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Add new mirror
|
||||||
|
self.mirrors["mirrors"].append(asdict(mirror))
|
||||||
|
self._save_mirrors()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to add mirror: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def remove_mirror(self, name: str) -> bool:
|
||||||
|
"""Remove a mirror by name"""
|
||||||
|
try:
|
||||||
|
self.mirrors["mirrors"] = [
|
||||||
|
mirror for mirror in self.mirrors["mirrors"]
|
||||||
|
if mirror["name"] != name
|
||||||
|
]
|
||||||
|
self._save_mirrors()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to remove mirror: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def list_mirrors(self) -> List[Dict[str, Any]]:
|
||||||
|
"""List all mirrors"""
|
||||||
|
return self.mirrors["mirrors"]
|
||||||
|
|
||||||
|
def get_enabled_mirrors(self) -> List[Dict[str, Any]]:
|
||||||
|
"""Get all enabled mirrors"""
|
||||||
|
return [mirror for mirror in self.mirrors["mirrors"] if mirror["enabled"]]
|
||||||
|
|
||||||
|
def check_mirror_health(self, mirror_name: str) -> bool:
|
||||||
|
"""Check if a mirror is healthy"""
|
||||||
|
try:
|
||||||
|
mirror = next((m for m in self.mirrors["mirrors"] if m["name"] == mirror_name), None)
|
||||||
|
if not mirror:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not mirror["health_check"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Simple health check - try to access the mirror
|
||||||
|
test_url = f"{mirror['url']}/dists/{self._get_default_suite()}/Release"
|
||||||
|
|
||||||
|
import urllib.request
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(test_url, timeout=10) as response:
|
||||||
|
return response.status == 200
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Health check failed for {mirror_name}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_default_suite(self) -> str:
|
||||||
|
"""Get default Debian suite"""
|
||||||
|
return "bookworm"
|
||||||
|
|
||||||
|
def generate_sources_list(self, suite: str, components: Optional[List[str]] = None) -> str:
|
||||||
|
"""Generate sources.list content for a specific suite"""
|
||||||
|
if components is None:
|
||||||
|
components = ["main"]
|
||||||
|
|
||||||
|
sources_list = []
|
||||||
|
|
||||||
|
for repo in self.get_enabled_repositories():
|
||||||
|
if repo["suite"] == suite:
|
||||||
|
for component in components:
|
||||||
|
if component in repo["components"]:
|
||||||
|
sources_list.append(
|
||||||
|
f"deb {repo['url']} {repo['suite']} {component}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(sources_list)
|
||||||
|
|
||||||
|
def generate_apt_config(self, suite: str, proxy: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""Generate APT configuration for composer"""
|
||||||
|
config = {
|
||||||
|
"sources": {},
|
||||||
|
"preferences": {},
|
||||||
|
"proxy": proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate sources
|
||||||
|
for repo in self.get_enabled_repositories():
|
||||||
|
if repo["suite"] == suite:
|
||||||
|
config["sources"][repo["name"]] = {
|
||||||
|
"url": repo["url"],
|
||||||
|
"suite": repo["suite"],
|
||||||
|
"components": repo["components"],
|
||||||
|
"priority": repo["priority"]
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
def validate_repository_config(self) -> List[str]:
|
||||||
|
"""Validate repository configuration and return errors"""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for repo in self.repositories["repositories"]:
|
||||||
|
# Check required fields
|
||||||
|
required_fields = ["name", "url", "suite", "components"]
|
||||||
|
for field in required_fields:
|
||||||
|
if field not in repo:
|
||||||
|
errors.append(f"Repository {repo.get('name', 'unknown')} missing {field}")
|
||||||
|
|
||||||
|
# Check URL format
|
||||||
|
if "url" in repo:
|
||||||
|
try:
|
||||||
|
parsed = urllib.parse.urlparse(repo["url"])
|
||||||
|
if not parsed.scheme or not parsed.netloc:
|
||||||
|
errors.append(f"Repository {repo.get('name', 'unknown')} has invalid URL: {repo['url']}")
|
||||||
|
except:
|
||||||
|
errors.append(f"Repository {repo.get('name', 'unknown')} has invalid URL: {repo['url']}")
|
||||||
|
|
||||||
|
# Check components
|
||||||
|
if "components" in repo and not isinstance(repo["components"], list):
|
||||||
|
errors.append(f"Repository {repo.get('name', 'unknown')} components must be a list")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
def export_configuration(self, output_path: str) -> bool:
|
||||||
|
"""Export complete configuration to file"""
|
||||||
|
try:
|
||||||
|
config = {
|
||||||
|
"repositories": self.repositories,
|
||||||
|
"mirrors": self.mirrors,
|
||||||
|
"exported_at": str(datetime.now()),
|
||||||
|
"version": "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(output_path, 'w') as f:
|
||||||
|
json.dump(config, f, indent=2)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to export configuration: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def import_configuration(self, config_path: str) -> bool:
|
||||||
|
"""Import configuration from file"""
|
||||||
|
try:
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
if "repositories" in config:
|
||||||
|
self.repositories = config["repositories"]
|
||||||
|
self._save_repositories()
|
||||||
|
|
||||||
|
if "mirrors" in config:
|
||||||
|
self.mirrors = config["mirrors"]
|
||||||
|
self._save_mirrors()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to import configuration: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Example usage of Debian repository manager"""
|
||||||
|
print("Debian Repository Manager Example")
|
||||||
|
|
||||||
|
# Create manager
|
||||||
|
manager = DebianRepositoryManager()
|
||||||
|
|
||||||
|
# List repositories
|
||||||
|
print("\nCurrent repositories:")
|
||||||
|
for repo in manager.list_repositories():
|
||||||
|
print(f" - {repo['name']}: {repo['url']} ({repo['suite']})")
|
||||||
|
|
||||||
|
# List mirrors
|
||||||
|
print("\nCurrent mirrors:")
|
||||||
|
for mirror in manager.list_mirrors():
|
||||||
|
print(f" - {mirror['name']}: {mirror['url']}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
438
debian_variants_manager.py
Normal file
438
debian_variants_manager.py
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
#!/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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue