1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 11:36:05 +01:00

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 <khaneliman12@gmail.com>
This commit is contained in:
Austin Horstman 2025-07-03 15:13:18 -05:00
parent b46c693797
commit d03fa2d84c
2 changed files with 59 additions and 180 deletions

View file

@ -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: |

View file

@ -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', '<nixpkgs>', '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()