diff --git a/.github/workflows/update-maintainers.yml b/.github/workflows/update-maintainers.yml index c2a9ba0aa..fb04733bf 100644 --- a/.github/workflows/update-maintainers.yml +++ b/.github/workflows/update-maintainers.yml @@ -59,6 +59,8 @@ jobs: run: | echo "📋 Generating updated all-maintainers.nix..." ./lib/python/generate-all-maintainers.py + echo "🎨 Formatting with nixfmt..." + nix fmt all-maintainers.nix - name: Check for changes id: check-changes run: | diff --git a/lib/python/generate-all-maintainers.py b/lib/python/generate-all-maintainers.py index e457d57cc..a7ed09c23 100755 --- a/lib/python/generate-all-maintainers.py +++ b/lib/python/generate-all-maintainers.py @@ -1,134 +1,53 @@ #!/usr/bin/env nix-shell #!nix-shell -i python3 -p python3 """ -Generate all-maintainers.nix combining local and nixpkgs maintainers. +Generate all-maintainers.nix using meta.maintainers as source of truth. -This script analyzes Home Manager modules to find maintainer references -and combines them with local maintainers to create a master list. +This script uses the meta.maintainers system to extract maintainer information +by evaluating Home Manager modules, which is much simpler and more +reliable than parsing files with regex. """ import argparse import json -import re import subprocess import sys from pathlib import Path -from typing import Dict, List, Optional, Set +from typing import Dict -class MaintainerGenerator: - """Generates a comprehensive maintainers list from HM and nixpkgs sources.""" +class MetaMaintainerGenerator: + """Generates maintainers list using meta.maintainers from Home Manager evaluation.""" def __init__(self, hm_root: Path): self.hm_root = hm_root - self.modules_dir = hm_root / "modules" - self.hm_maintainers_file = self.modules_dir / "lib" / "maintainers.nix" + self.hm_maintainers_file = hm_root / "modules" / "lib" / "maintainers.nix" self.output_file = hm_root / "all-maintainers.nix" + self.extractor_script = hm_root / "lib" / "nix" / "extract-maintainers-meta.nix" - def find_nix_files(self) -> List[Path]: - """Find all .nix files in the modules directory.""" - nix_files = list(self.modules_dir.rglob("*.nix")) - print(f"📁 Found {len(nix_files)} .nix files in modules") - return nix_files - - def extract_maintainer_lines(self, file_path: Path) -> List[str]: - """Extract lines containing maintainer references from a file.""" - try: - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - lines = [] - for line in content.splitlines(): - if any(pattern in line for pattern in [ - "meta.maintainers", - "lib.maintainers.", - "lib.hm.maintainers.", - "with lib.maintainers", - "with lib.hm.maintainers" - ]): - lines.append(line.strip()) - return lines - except Exception as e: - print(f"Warning: Could not read {file_path}: {e}") - return [] - - def parse_maintainer_names(self, lines: List[str]) -> Set[str]: - """Parse maintainer names from extracted lines.""" - nixpkgs_maintainers = set() - - for line in lines: - matches = re.findall(r'lib\.maintainers\.([a-zA-Z0-9_-]+)', line) - nixpkgs_maintainers.update(matches) - - if 'with lib.maintainers' in line: - bracket_match = re.search(r'\[([^\]]+)\]', line) - if bracket_match: - content = bracket_match.group(1) - names = re.findall(r'\b([a-zA-Z0-9_-]+)\b', content) - filtered_names = [ - name for name in names - if name not in {'with', 'lib', 'maintainers', 'meta', 'if', 'then', 'else'} - ] - nixpkgs_maintainers.update(filtered_names) - - return nixpkgs_maintainers - - def extract_all_maintainers(self) -> Dict[str, Set[str]]: - """Extract all maintainer references from modules.""" - print("🔎 Extracting maintainer references...") - - nix_files = self.find_nix_files() - all_lines = [] - hm_maintainers_used = set() - - for file_path in nix_files: - lines = self.extract_maintainer_lines(file_path) - all_lines.extend(lines) - - for line in lines: - hm_matches = re.findall(r'lib\.hm\.maintainers\.([a-zA-Z0-9_-]+)', line) - hm_maintainers_used.update(hm_matches) - - print("📝 Parsing maintainer names...") - nixpkgs_maintainers = self.parse_maintainer_names(all_lines) - - print(f"👥 Found potential nixpkgs maintainers: {len(nixpkgs_maintainers)}") - print(f"🏠 Found HM maintainers used: {len(hm_maintainers_used)}") - - return { - 'nixpkgs': nixpkgs_maintainers, - 'hm_used': hm_maintainers_used - } - - def load_hm_maintainers(self) -> Set[str]: - """Load Home Manager maintainer names.""" - try: - with open(self.hm_maintainers_file, 'r') as f: - content = f.read() - names = re.findall(r'^\s*"?([a-zA-Z0-9_-]+)"?\s*=', content, re.MULTILINE) - return set(names) - except Exception as e: - print(f"Error loading HM maintainers: {e}") - return set() - - def fetch_nixpkgs_maintainers(self) -> Optional[Dict]: - """Fetch nixpkgs maintainers data using nix eval.""" - print("📡 Attempting to fetch nixpkgs maintainer information...") + def extract_maintainers_from_meta(self) -> Dict: + """Extract maintainer information using meta.maintainers.""" + print("🔍 Extracting maintainers using meta.maintainers...") try: result = subprocess.run([ - 'nix', 'eval', '--file', '', 'lib.maintainers', '--json' - ], capture_output=True, text=True, timeout=30) + "nix", "eval", "--impure", "--file", str(self.extractor_script), "--json" + ], capture_output=True, text=True, timeout=60) if result.returncode == 0: - print("✅ Successfully fetched nixpkgs maintainers") - return json.loads(result.stdout) + data = json.loads(result.stdout) + print("✅ Successfully extracted maintainers using meta.maintainers") + return data else: - print("⚠️ Could not fetch nixpkgs maintainers - will create placeholders") - return None - except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError) as e: - print(f"⚠️ Nix command failed: {e}") - return None + print(f"❌ Failed to extract maintainers: {result.stderr}") + sys.exit(1) + + except subprocess.TimeoutExpired: + print("❌ Timeout while extracting maintainers") + sys.exit(1) + except Exception as e: + print(f"❌ Error extracting maintainers: {e}") + sys.exit(1) def format_maintainer_entry(self, name: str, info: Dict, source: str) -> str: """Format a single maintainer entry with nix fmt compatible formatting.""" @@ -184,24 +103,24 @@ class MaintainerGenerator: def generate_maintainers_file(self) -> None: """Generate the complete all-maintainers.nix file.""" - print("📄 Generating all-maintainers.nix...") + print("📄 Generating all-maintainers.nix using meta.maintainers...") - extracted = self.extract_all_maintainers() - nixpkgs_maintainers = extracted['nixpkgs'] - hm_maintainer_names = self.load_hm_maintainers() - nixpkgs_only = nixpkgs_maintainers - hm_maintainer_names - print(f"📦 Nixpkgs-only maintainers after deduplication: {len(nixpkgs_only)}") + # Extract maintainers using meta.maintainers + maintainer_data = self.extract_maintainers_from_meta() - nixpkgs_data = self.fetch_nixpkgs_maintainers() or {} + hm_maintainers = maintainer_data["categorized"]["home-manager"] + nixpkgs_maintainers = maintainer_data["categorized"]["nixpkgs"] + formatted_maintainers = maintainer_data["formatted"] + + print(f"🏠 Home Manager maintainers: {len(hm_maintainers)}") + print(f"📦 Nixpkgs maintainers: {len(nixpkgs_maintainers)}") with open(self.output_file, 'w') as f: f.write('''# Home Manager all maintainers list. # -# This file combines maintainers from: -# - Home Manager specific maintainers (modules/lib/maintainers.nix) -# - Nixpkgs maintainers referenced in Home Manager modules +# This file lists all referenced maintainers in Home Manager. # -# This file is automatically generated by lib/python/generate-all-maintainers.py +# This file is automatically generated using meta.maintainers from Home Manager evaluation # DO NOT EDIT MANUALLY # # To regenerate: ./lib/python/generate-all-maintainers.py @@ -209,54 +128,16 @@ class MaintainerGenerator: { ''') - print("🏠 Adding Home Manager maintainers...") - try: - with open(self.hm_maintainers_file, 'r') as hm_file: - hm_content = hm_file.read() - - start = hm_content.find('{') - end = hm_content.rfind('}') - if start != -1 and end != -1: - inner_content = hm_content[start+1:end] - lines = inner_content.split('\n') - in_entry = False - for line in lines: - stripped = line.strip() - if not stripped or stripped.startswith('#') or 'keep-sorted' in stripped: - continue - - if '= {' in line and not in_entry: - f.write(" # home-manager\n") - f.write(f"{line}\n") - in_entry = True - elif line.strip() == '};' and in_entry: - f.write(f"{line}\n") - in_entry = False - else: - f.write(f"{line}\n") - except Exception as e: - print(f"Warning: Could not process HM maintainers file: {e}") - - print("📦 Adding referenced nixpkgs maintainers...") - for maintainer in sorted(nixpkgs_only): - if maintainer in nixpkgs_data: - entry = self.format_maintainer_entry(maintainer, nixpkgs_data[maintainer], "nixpkgs") - f.write(f"{entry}\n") - else: - placeholder = { - 'name': maintainer, - 'email': f'{maintainer}@example.com', - 'github': maintainer, - 'githubId': 0 - } - entry = self.format_maintainer_entry(maintainer, placeholder, "nixpkgs (placeholder)") - f.write(f"{entry}\n") + # Use the formatted maintainers from Nix evaluation + print("✨ Adding formatted maintainers using lib.generators.toPretty...") + f.write(formatted_maintainers) + f.write("\n") f.write('''} ''') self.validate_generated_file() - self.print_statistics() + self.print_statistics(maintainer_data) def validate_generated_file(self) -> bool: """Validate the generated Nix file syntax.""" @@ -276,28 +157,24 @@ class MaintainerGenerator: print(f"Warning: Could not validate file: {e}") return False - def print_statistics(self) -> None: + def print_statistics(self, maintainer_data: Dict) -> None: """Print generation statistics.""" - try: - with open(self.output_file, 'r') as f: - content = f.read() + stats = maintainer_data["stats"] - hm_count = content.count('# home-manager') - nixpkgs_count = content.count('# nixpkgs') - total_entries = content.count(' = {') - - print(f"✅ Generated {self.output_file}") - print("📊 Statistics:") - print(f" - Home Manager maintainers: {hm_count}") - print(f" - Nixpkgs maintainers: {nixpkgs_count}") - print(f" - Total entries: {total_entries}") - print() - except Exception as e: - print(f"Could not generate statistics: {e}") + print(f"✅ Generated {self.output_file}") + print("📊 Statistics:") + print(f" - Total files with maintainers: {stats['totalFiles']}") + print(f" - Total unique maintainers: {stats['totalMaintainers']}") + print(f" - Home Manager maintainers: {stats['hmMaintainers']}") + print(f" - Nixpkgs maintainers: {stats['nixpkgsMaintainers']}") + print() + print("🎉 Generation completed successfully using meta.maintainers!") def main(): - parser = argparse.ArgumentParser(description="Generate Home Manager all-maintainers.nix") + parser = argparse.ArgumentParser( + description="Generate Home Manager all-maintainers.nix using meta.maintainers" + ) parser.add_argument( '--root', type=Path, @@ -324,11 +201,11 @@ def main(): print("Please specify --root or run from Home Manager directory") sys.exit(1) - generator = MaintainerGenerator(hm_root) + generator = MetaMaintainerGenerator(hm_root) if args.output: generator.output_file = args.output - print("🔍 Analyzing Home Manager modules for maintainer references...") + print("🚀 Generating maintainers using meta.maintainers approach...") try: generator.generate_maintainers_file()