From 2590268ca7627a3ec1a9c9422983495cea51e2e1 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 25 Jan 2026 16:39:00 -0600 Subject: [PATCH] ci: extract maintainers searches for main module Some modules are broken up into multiple files. Search for primary import file that will contain the meta information needed, when missing. Signed-off-by: Austin Horstman --- lib/nix/extract-maintainers.nix | 219 ++++++++++++++++++++++++++------ 1 file changed, 177 insertions(+), 42 deletions(-) diff --git a/lib/nix/extract-maintainers.nix b/lib/nix/extract-maintainers.nix index affc50b1..88ab9ab0 100644 --- a/lib/nix/extract-maintainers.nix +++ b/lib/nix/extract-maintainers.nix @@ -3,57 +3,192 @@ file ? throw "provide file argument", }: let + inherit (lib) + init + isFunction + optionalAttrs + optionals + ; + + # Minimal module evaluation context + # NOTE: These are empty/mock values. Modules that deeply access config/options + # or pkgs attributes may fail evaluation. This is intentional for safety. config = { }; options = { }; releaseInfo = lib.importJSON ../../release.json; + # Path utilities with sanitization + sanitizePath = + path: + let + normalized = lib.removePrefix "/" (lib.removeSuffix "/" path); + in + assert !(lib.hasInfix ".." normalized); + normalized; + + mkAbsolutePath = relPath: ../../. + "/${sanitizePath relPath}"; + isNixFile = lib.hasSuffix ".nix" file; - filePath = ../../. + "/${file}"; + filePath = mkAbsolutePath file; fileExists = builtins.pathExists filePath; - maintainers = - if isNixFile && fileExists then - let - fileContent = import filePath; + findParentModule = + file: + let + pathParts = lib.splitString "/" file; + fileName = lib.last pathParts; + fileDir = lib.concatStringsSep "/" (init pathParts); - module = - if lib.isFunction fileContent then - # TODO: Find a better way of handling this... - if lib.hasPrefix "docs/" file then - if lib.hasSuffix "home-manager-manual.nix" file then - fileContent { - stdenv = { - mkDerivation = x: x; - }; - inherit lib; - documentation-highlighter = { }; - revision = "unknown"; - home-manager-options = { - home-manager = { }; - nixos = { }; - nix-darwin = { }; - }; - nixos-render-docs = { }; - } - else - fileContent { - inherit lib; - pkgs = null; - inherit (releaseInfo) release isReleaseBranch; - } - else if lib.hasPrefix "lib/" file then - fileContent { inherit lib; } - else - fileContent { - inherit lib config options; - pkgs = null; - } - else - fileContent; + sameDirDefault = { + path = mkAbsolutePath "${fileDir}/default.nix"; + relPath = "${fileDir}/default.nix"; + exists = fileName != "default.nix" && builtins.pathExists (mkAbsolutePath "${fileDir}/default.nix"); + }; + + parentParts = init (init pathParts); + parentDir = lib.concatStringsSep "/" parentParts; + parentDirDefault = { + path = mkAbsolutePath "${parentDir}/default.nix"; + relPath = "${parentDir}/default.nix"; + exists = parentParts != [ ] && builtins.pathExists (mkAbsolutePath "${parentDir}/default.nix"); + }; + + candidates = lib.filter (c: c.exists) [ + sameDirDefault + parentDirDefault + ]; + in + optionalAttrs (candidates != [ ]) (lib.head candidates); + + # Detect if a function is NOT a standard module (helper function, library function, etc.) + # Standard modules accept common NixOS/Home Manager parameters. + isNonModuleFunction = + fileContent: + isFunction fileContent + && ( + let + functor = builtins.functionArgs fileContent; + argNames = builtins.attrNames functor; + # Standard module parameters used across NixOS, Home Manager, and nix-darwin + standardModuleParams = [ + "config" + "lib" + "pkgs" + "options" + "modulesPath" + "specialArgs" + "osConfig" + "inputs" + ]; + hasNonStandardParams = lib.any (name: !(lib.elem name standardModuleParams)) argNames; in - module.meta.maintainers or [ ] - else - [ ]; + hasNonStandardParams + ); + + # Create a mock pkgs that provides helpful error messages + # instead of cryptic "null has no attribute" errors + mockPkgs = + builtins.mapAttrs + ( + name: _: + throw '' + pkgs.${name} not available during maintainer extraction. + This is intentional for safety - maintainer extraction runs with minimal context. + If your module's meta.maintainers depends on pkgs, consider restructuring. + '' + ) + { + stdenv = null; + lib = null; + system = null; + pkgsCross = null; + buildPackages = null; + }; + + # Module arguments based on file path + # Priority: specific file overrides > prefix-based args > default args + mkModuleArgs = + file: + let + # Special case for specific files + specialCases = { + "docs/home-manager-manual.nix" = { + stdenv.mkDerivation = x: x; + inherit lib; + documentation-highlighter = { }; + revision = "unknown"; + home-manager-options = { + home-manager = { }; + nixos = { }; + nix-darwin = { }; + }; + nixos-render-docs = { }; + }; + }; + + # Prefix-based argument sets + prefixArgs = + if lib.hasPrefix "docs/" file then + { + inherit lib; + pkgs = mockPkgs; + inherit (releaseInfo) release isReleaseBranch; + } + else if lib.hasPrefix "lib/" file then + { inherit lib; } + else + null; + + # Default arguments for standard modules + defaultArgs = { + inherit lib config options; + pkgs = mockPkgs; + }; + in + specialCases.${file} or (if prefixArgs != null then prefixArgs else defaultArgs); + + evaluateModule = + fileContent: file: + let + isFunctionContent = isFunction fileContent; + isHelper = isNonModuleFunction fileContent; + + args = mkModuleArgs file; + in + optionalAttrs (!isHelper) ( + builtins.tryEval (if isFunctionContent then fileContent args else fileContent) + ); + + getMaintainers = + evalResult: optionals (evalResult.success or false) (evalResult.value.meta.maintainers or [ ]); + + getParentMaintainers = + parentModule: + optionals (parentModule != { }) ( + let + parentContent = import parentModule.path; + parentArgs = { + inherit lib config options; + pkgs = mockPkgs; + }; + parent = if isFunction parentContent then parentContent parentArgs else parentContent; + in + parent.meta.maintainers or [ ] + ); + + extractMaintainers = + let + fileContent = import filePath; + evalResult = evaluateModule fileContent file; + moduleMaintainers = getMaintainers evalResult; + + # Only check parent if no maintainers found directly + parentModule = optionalAttrs (moduleMaintainers == [ ]) (findParentModule file); + parentMaintainers = getParentMaintainers parentModule; + in + moduleMaintainers ++ parentMaintainers; + + maintainers = optionals (isNixFile && fileExists) extractMaintainers; in map (maintainer: maintainer.github) maintainers