From 056cd86cc0b1b67cdc8e8f60a4e9e63f802d7d4e Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Thu, 3 Jul 2025 09:56:32 -0500 Subject: [PATCH] flake/dev/generate-all-maintainers: init Used to generate a full maintainers.nix file that can be used for RFC39 invites. We will use these invites to support requesting reviews from maintainers. --- flake/default.nix | 1 + flake/dev/default.nix | 1 + .../dev/generate-all-maintainers/default.nix | 42 ++++ .../extract-maintainers-meta.nix | 74 ++++++ .../generate-all-maintainers.py | 210 ++++++++++++++++++ 5 files changed, 328 insertions(+) create mode 100644 flake/dev/generate-all-maintainers/default.nix create mode 100644 flake/dev/generate-all-maintainers/extract-maintainers-meta.nix create mode 100755 flake/dev/generate-all-maintainers/generate-all-maintainers.py diff --git a/flake/default.nix b/flake/default.nix index 36875c31..78560d99 100644 --- a/flake/default.nix +++ b/flake/default.nix @@ -51,6 +51,7 @@ packages = lib.optionalAttrs (partitionStack == [ ]) { # Propagate `packages` from the `dev` partition: inherit (config.partitions.dev.module.flake.packages.${system}) + generate-all-maintainers list-plugins ; }; diff --git a/flake/dev/default.nix b/flake/dev/default.nix index a2fa309e..58ba57d8 100644 --- a/flake/dev/default.nix +++ b/flake/dev/default.nix @@ -2,6 +2,7 @@ { imports = [ ./devshell.nix + ./generate-all-maintainers ./list-plugins ./package-tests.nix ./template-tests.nix diff --git a/flake/dev/generate-all-maintainers/default.nix b/flake/dev/generate-all-maintainers/default.nix new file mode 100644 index 00000000..1157856a --- /dev/null +++ b/flake/dev/generate-all-maintainers/default.nix @@ -0,0 +1,42 @@ +{ self, ... }: +{ + perSystem = + { + lib, + pkgs, + ... + }: + let + package = pkgs.writers.writePython3Bin "generate-all-maintainers" { + # Disable flake8 checks that are incompatible with the ruff ones + flakeIgnore = [ + # Thinks shebang is a block comment + "E265" + # line too long + "E501" + # line break before binary operator + "W503" + ]; + } (builtins.readFile ./generate-all-maintainers.py); + in + { + packages.generate-all-maintainers = package; + + checks.generate-all-maintainers-test = + pkgs.runCommand "generate-all-maintainers-test" + { + nativeBuildInputs = [ package ]; + } + '' + generate-all-maintainers --root ${self} --output $out + ''; + + devshells.default.commands = [ + { + name = "generate-all-maintainers"; + command = ''${lib.getExe package} "$@"''; + help = "Generate a single nix file with all `nixvim` and `nixpkgs` maintainer entries"; + } + ]; + }; +} diff --git a/flake/dev/generate-all-maintainers/extract-maintainers-meta.nix b/flake/dev/generate-all-maintainers/extract-maintainers-meta.nix new file mode 100644 index 00000000..4b950266 --- /dev/null +++ b/flake/dev/generate-all-maintainers/extract-maintainers-meta.nix @@ -0,0 +1,74 @@ +# Extract maintainers from nixvim configuration using meta.maintainers +# This script evaluates an empty nixvimConfiguration and extracts the merged maintainer information +let + nixvim = import ../../..; + lib = nixvim.inputs.nixpkgs.lib.extend nixvim.lib.overlay; + + emptyConfig = lib.nixvim.evalNixvim { + modules = [ { _module.check = false; } ]; + extraSpecialArgs = { + pkgs = null; + }; + }; + + inherit (emptyConfig.config.meta) maintainers; + + extractMaintainerObjects = + maintainerData: + lib.pipe maintainerData [ + lib.attrValues + lib.concatLists + lib.unique + ]; + + allMaintainerObjects = extractMaintainerObjects maintainers; + + allMaintainerNames = lib.filter (name: name != null) ( + map (maintainer: maintainer.github) allMaintainerObjects + ); + + nixvimMaintainers = import ../../../lib/maintainers.nix; + nixvimMaintainerNames = lib.attrNames nixvimMaintainers; + partitionedMaintainers = lib.partition (nameValue: lib.elem nameValue.name nixvimMaintainerNames) ( + lib.attrsToList maintainerDetails + ); + + maintainerDetails = lib.pipe allMaintainerObjects [ + (map (obj: { + name = obj.github; + value = obj // { + source = + if categorizedMaintainers.nixvim ? ${obj.github} then + "nixvim" + else if categorizedMaintainers.nixpkgs ? ${obj.github} then + "nixpkgs" + else + throw "${obj.github} is neither a nixvim or nixpkgs maintainer"; + }; + })) + lib.listToAttrs + ]; + + categorizedMaintainers = { + nixvim = lib.listToAttrs partitionedMaintainers.right; + nixpkgs = lib.listToAttrs partitionedMaintainers.wrong; + }; + + formattedMaintainers = lib.generators.toPretty { + multiline = true; + indent = ""; + } maintainerDetails; +in +{ + raw = maintainers; + names = allMaintainerNames; + details = maintainerDetails; + categorized = categorizedMaintainers; + formatted = formattedMaintainers; + + stats = { + totalMaintainers = lib.length allMaintainerNames; + nixvimMaintainers = lib.length (lib.attrNames categorizedMaintainers.nixvim); + nixpkgsMaintainers = lib.length (lib.attrNames categorizedMaintainers.nixpkgs); + }; +} diff --git a/flake/dev/generate-all-maintainers/generate-all-maintainers.py b/flake/dev/generate-all-maintainers/generate-all-maintainers.py new file mode 100755 index 00000000..52d923d0 --- /dev/null +++ b/flake/dev/generate-all-maintainers/generate-all-maintainers.py @@ -0,0 +1,210 @@ +#!/usr/bin/env nix-shell +#!nix-shell -i python3 -p python3 +""" +Generate all-maintainers.nix using meta.maintainers as source of truth. + +This script uses the meta.maintainers system to extract maintainer information +by evaluating an empty nixvimConfiguration, which is much simpler and more +reliable than parsing files with regex. +""" + +import argparse +import inspect +import json +import subprocess +import sys +from pathlib import Path + + +def get_project_root() -> Path: + """ + Find the project root directory. + + Tries to find the git repository root. If that fails, falls back to + locating it relative to this script file. + """ + try: + # Ask git for the top-level directory of the current repository. + git_root_bytes = subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], stderr=subprocess.DEVNULL + ) + return Path(git_root_bytes.decode("utf-8").strip()) + except (subprocess.CalledProcessError, FileNotFoundError): + # Fallback for when not in a git repo or git is not installed. + print( + "Warning: 'git rev-parse --show-toplevel' failed.", + "Falling back to script location to determine root.", + "This may not work correctly with flakes.", + file=sys.stderr, + ) + # Assumes this script is at: /flake/dev/generate-all-maintainers/ + return Path(__file__).parent.parent.parent.parent.resolve() + + +class MetaMaintainerGenerator: + """Generates maintainers list using meta.maintainers from nixvim evaluation.""" + + def __init__(self, nixvim_root: Path): + self.nixvim_root = nixvim_root + self.nixvim_maintainers_file = nixvim_root / "lib" / "maintainers.nix" + self.output_file = nixvim_root / "generated" / "all-maintainers.nix" + self.extractor_script = ( + nixvim_root + / "flake" + / "dev" + / "generate-all-maintainers" + / "extract-maintainers-meta.nix" + ) + + 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", + str(self.extractor_script), + "--json", + ], + capture_output=True, + text=True, + timeout=60, + ) + + if result.returncode == 0: + data = json.loads(result.stdout) + print("āœ… Successfully extracted maintainers using meta.maintainers") + return data + else: + 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 generate_maintainers_file(self) -> None: + """Generate the complete all-maintainers.nix file.""" + print("šŸ“„ Generating all-maintainers.nix using meta.maintainers...") + + # Extract maintainers using meta.maintainers + maintainer_data = self.extract_maintainers_from_meta() + + nixvim_maintainers = maintainer_data["categorized"]["nixvim"] + nixpkgs_maintainers = maintainer_data["categorized"]["nixpkgs"] + formatted_maintainers = maintainer_data["formatted"] + + print(f"šŸ  Nixvim maintainers: {len(nixvim_maintainers)}") + print(f"šŸ“¦ Nixpkgs maintainers: {len(nixpkgs_maintainers)}") + + with open(self.output_file, "w") as f: + f.write(inspect.cleandoc(""" + # Nixvim all maintainers list. + # + # This file lists all referenced maintainers in Nixvim. + # + # This file is automatically generated using meta.maintainers from nixvim evaluation + # DO NOT EDIT MANUALLY + # + # To regenerate: nix run .#generate-all-maintainers + # + """)) + + # Use the formatted maintainers from Nix evaluation + print( + "✨ Serializing formatted maintainers using lib.generators.toPretty..." + ) + f.write("\n") + f.write(formatted_maintainers) + f.write("\n") + + self.validate_generated_file() + self.print_statistics(maintainer_data) + + def validate_generated_file(self) -> bool: + """Validate the generated Nix file syntax.""" + try: + result = subprocess.run( + ["nix-instantiate", "--eval", str(self.output_file), "--strict"], + 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, maintainer_data: dict) -> None: + """Print generation statistics.""" + stats = maintainer_data["stats"] + + print(f"āœ… Generated {self.output_file}") + print("šŸ“Š Statistics:") + print(f" - Total unique maintainers: {stats['totalMaintainers']}") + print(f" - Nixvim maintainers: {stats['nixvimMaintainers']}") + print(f" - Nixpkgs maintainers: {stats['nixpkgsMaintainers']}") + print() + print("šŸŽ‰ Generation completed successfully using meta.maintainers!") + + +def main(): + parser = argparse.ArgumentParser( + description="Generate Nixvim all-maintainers.nix using meta.maintainers" + ) + parser.add_argument( + "--root", + type=Path, + default=None, + help="Path to Nixvim root (default: auto-detect)", + ) + parser.add_argument( + "--output", + type=Path, + default=None, + help="Output file path (default: /generated/all-maintainers.nix)", + ) + + args = parser.parse_args() + + if args.root: + nixvim_root = args.root + else: + nixvim_root = get_project_root() + + if not (nixvim_root / "lib" / "maintainers.nix").exists(): + print(f"Error: Could not find maintainers.nix in {nixvim_root}") + print("Please specify --root or run from Nixvim directory") + sys.exit(1) + + generator = MetaMaintainerGenerator(nixvim_root) + if args.output: + generator.output_file = args.output + + print("šŸš€ Generating maintainers using meta.maintainers approach...") + + 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()