mirror of
https://github.com/nix-community/home-manager.git
synced 2025-11-08 19:46:05 +01:00
scripts/generate-all-maintainers.py: add script
Create a script for generating the master maintainer list we will use for inviting / pinging on changed files. Signed-off-by: Austin Horstman <khaneliman12@gmail.com>
This commit is contained in:
parent
479f888967
commit
44a2308db9
1 changed files with 344 additions and 0 deletions
344
lib/python/generate-all-maintainers.py
Executable file
344
lib/python/generate-all-maintainers.py
Executable file
|
|
@ -0,0 +1,344 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -i python3 -p python3
|
||||
"""
|
||||
Generate all-maintainers.nix combining local and nixpkgs maintainers.
|
||||
|
||||
This script analyzes Home Manager modules to find maintainer references
|
||||
and combines them with local maintainers to create a master list.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
|
||||
class MaintainerGenerator:
|
||||
"""Generates a comprehensive maintainers list from HM and nixpkgs sources."""
|
||||
|
||||
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.output_file = hm_root / "all-maintainers.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...")
|
||||
|
||||
try:
|
||||
result = subprocess.run([
|
||||
'nix', 'eval', '--file', '<nixpkgs>', 'lib.maintainers', '--json'
|
||||
], capture_output=True, text=True, timeout=30)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("✅ Successfully fetched nixpkgs maintainers")
|
||||
return json.loads(result.stdout)
|
||||
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
|
||||
|
||||
def format_maintainer_entry(self, name: str, info: Dict, source: str) -> str:
|
||||
"""Format a single maintainer entry with nix fmt compatible formatting."""
|
||||
lines = [f" # {source}"]
|
||||
lines.append(f" {name} = {{")
|
||||
|
||||
key_order = ["name", "email", "github", "githubId", "matrix", "keys"]
|
||||
sorted_keys = sorted(info.keys(), key=lambda k: key_order.index(k) if k in key_order else len(key_order))
|
||||
|
||||
for key in sorted_keys:
|
||||
if key.startswith('_'): # Skip internal fields
|
||||
continue
|
||||
|
||||
value = info[key]
|
||||
if isinstance(value, str):
|
||||
lines.append(f' {key} = "{value}";')
|
||||
elif isinstance(value, int):
|
||||
lines.append(f' {key} = {value};')
|
||||
elif isinstance(value, list) and value:
|
||||
if all(isinstance(item, dict) for item in value):
|
||||
formatted_items = []
|
||||
for item in value:
|
||||
if isinstance(item, dict):
|
||||
# Handle dict items with proper spacing
|
||||
item_parts = []
|
||||
for k, v in item.items():
|
||||
if isinstance(v, str):
|
||||
item_parts.append(f'{k} = "{v}"')
|
||||
else:
|
||||
item_parts.append(f'{k} = {v}')
|
||||
formatted_items.append("{ " + "; ".join(item_parts) + "; }")
|
||||
else:
|
||||
formatted_items.append(f'"{item}"')
|
||||
if len(formatted_items) == 1:
|
||||
lines.append(f' {key} = [ {formatted_items[0]} ];')
|
||||
else:
|
||||
lines.append(f' {key} = [')
|
||||
for item in formatted_items:
|
||||
lines.append(f' {item}')
|
||||
lines.append(' ];')
|
||||
else:
|
||||
items = [f'"{item}"' if isinstance(item, str) else str(item) for item in value]
|
||||
if len(items) == 1:
|
||||
lines.append(f' {key} = [ {items[0]} ];')
|
||||
else:
|
||||
lines.append(f' {key} = [')
|
||||
for item in items:
|
||||
lines.append(f' {item}')
|
||||
lines.append(' ];')
|
||||
|
||||
lines.append(" };")
|
||||
return "\n".join(lines)
|
||||
|
||||
def generate_maintainers_file(self) -> None:
|
||||
"""Generate the complete all-maintainers.nix file."""
|
||||
print("📄 Generating all-maintainers.nix...")
|
||||
|
||||
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)}")
|
||||
|
||||
nixpkgs_data = self.fetch_nixpkgs_maintainers() or {}
|
||||
|
||||
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 is automatically generated by lib/python/generate-all-maintainers.py
|
||||
# DO NOT EDIT MANUALLY
|
||||
#
|
||||
# To regenerate: ./lib/python/generate-all-maintainers.py
|
||||
#
|
||||
{
|
||||
''')
|
||||
|
||||
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")
|
||||
|
||||
f.write('''}
|
||||
''')
|
||||
|
||||
self.validate_generated_file()
|
||||
self.print_statistics()
|
||||
|
||||
def validate_generated_file(self) -> bool:
|
||||
"""Validate the generated Nix file syntax."""
|
||||
try:
|
||||
result = subprocess.run([
|
||||
'nix', 'eval', '--file', str(self.output_file), '--json'
|
||||
], capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
print("✅ Generated file has valid Nix syntax")
|
||||
return True
|
||||
else:
|
||||
print("❌ Warning: Generated file has Nix syntax errors")
|
||||
print(result.stderr[:500])
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not validate file: {e}")
|
||||
return False
|
||||
|
||||
def print_statistics(self) -> None:
|
||||
"""Print generation statistics."""
|
||||
try:
|
||||
with open(self.output_file, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
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}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate Home Manager all-maintainers.nix")
|
||||
parser.add_argument(
|
||||
'--root',
|
||||
type=Path,
|
||||
default=None,
|
||||
help='Path to Home Manager root (default: auto-detect)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
type=Path,
|
||||
default=None,
|
||||
help='Output file path (default: <root>/all-maintainers.nix)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.root:
|
||||
hm_root = args.root
|
||||
else:
|
||||
script_dir = Path(__file__).parent
|
||||
hm_root = script_dir.parent.parent
|
||||
|
||||
if not (hm_root / "modules" / "lib" / "maintainers.nix").exists():
|
||||
print(f"Error: Could not find maintainers.nix in {hm_root}")
|
||||
print("Please specify --root or run from Home Manager directory")
|
||||
sys.exit(1)
|
||||
|
||||
generator = MaintainerGenerator(hm_root)
|
||||
if args.output:
|
||||
generator.output_file = args.output
|
||||
|
||||
print("🔍 Analyzing Home Manager modules for maintainer references...")
|
||||
|
||||
try:
|
||||
generator.generate_maintainers_file()
|
||||
except KeyboardInterrupt:
|
||||
print("\n❌ Generation cancelled by user")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"❌ Error generating maintainers file: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue