From d03fa2d84c6c7def463f9ff90d6a36d5873de0fc Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Thu, 3 Jul 2025 15:13:18 -0500 Subject: [PATCH] ci: generate-all-maintainers use nix eval update Previously, we had to hack together some string matching to identify and retrieve the maintainers in the repo. We can just eval the modules to retrieve the list of maintainers more accurately. Signed-off-by: Austin Horstman --- .github/workflows/update-maintainers.yml | 2 + lib/python/generate-all-maintainers.py | 237 ++++++----------------- 2 files changed, 59 insertions(+), 180 deletions(-) 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()