# SPDX-FileCopyrightText: 2024 Lubin Bailly # SPDX-FileCopyrightText: 2025 Tom Hubrecht # # SPDX-License-Identifier: EUPL-1.2 { lib, config, pkgs, ... }: let inherit (lib) attrNames concatStringsSep escapeXML evalModules filter flip getAttr hasPrefix head listToAttrs mapAttrsToList literalExpression mkEnableOption mkIf mkOption nameValuePair optionAttrSetToDocList optionalString pathIsDirectory pipe removeAttrs removePrefix removeSuffix ; inherit (lib.strings) sanitizeDerivationName ; inherit (lib.types) attrs attrsOf deferredModule listOf nullOr path submodule str ; mkDocJSON = module: { ignored-modules, path-translations, paths, specialArgs, ... }: let # NOTE: A big simplification of nixpkgs' make-options-doc # It turns out that we only need the json output, and have # no `baseOptionsJSON`, so the transformation done is `id` mkOpts = (flip pipe) [ # Evaluate the modules (modules: evalModules { inherit modules specialArgs; }) # Extract the options (getAttr "options") # Transform into a list of options optionAttrSetToDocList # Remove invisible and internal options (filter (o: o.visible && !o.internal)) # Remove extraneous attributes (builtins.map ( o: nameValuePair o.name ( removeAttrs o [ "name" "visible" "internal" ] ) )) # Transform back to an attribute set listToAttrs ]; # INFO: Subtract options of the ignored modules from # the complete set of options ignored = attrNames (mkOpts ignored-modules); options = removeAttrs (mkOpts (paths ++ ignored-modules)) ignored; directories = builtins.map (getAttr "base") path-translations; mkTranslation = path: let # Get the file associated to the module file = if pathIsDirectory path then path + "/default.nix" else path; # Get the parent module set matching = filter (m: hasPrefix m.base path) path-translations; parent = head matching; filePath = removePrefix parent.base file; in if matching == [ ] then (throw "${file} is not a descendant of ${module}. Declared parents are: \n ${concatStringsSep "\n " directories}") else { name = "<${filePath}>"; url = "${parent.url}/${filePath}"; }; in pkgs.runCommand "options-extranix" { fileName = sanitizeDerivationName "options-${module}.json"; nativeBuildInputs = [ pkgs.jq ]; passAsFile = [ "result" ]; result = builtins.toJSON { last_update = "-/-"; options = mapAttrsToList ( title: { default ? { text = ""; }, description, example ? { text = ""; }, loc, readOnly, type, declarations, ... }: { inherit loc readOnly title type ; descriptionHTML = pkgs.runCommandLocal "option-${title}.html" { inherit description; passAsFile = [ "description" ]; nativeBuildInputs = [ pkgs.pandoc ]; } "pandoc -f markdown-raw_html $descriptionPath > $out"; description = escapeXML description; example = escapeXML example.text; default = escapeXML default.text; declarations = builtins.map mkTranslation declarations; } ) options; }; } '' mkdir -p $out jq -r '.options[].descriptionHTML | "--rawfile\n" + . + "\n" + .' $resultPath | xargs \ jq -c '.options |= map(.descriptionHTML as $desc | .descriptionHTML |= $ARGS.named.[$desc])' $resultPath \ > $out/$fileName ''; website = pkgs.runCommand "search-infra" { inherit (cfg) theme; nativeBuildInputs = [ pkgs.hugo ]; config = builtins.toJSON cfg.settings; passAsFile = [ "config" ]; data = pkgs.symlinkJoin { name = "options-data"; paths = mapAttrsToList mkDocJSON cfg.modules; }; } '' # Setup the directory structure mkdir themes && ln -s $theme themes/options-search ln -s $configPath hugo.json mkdir static ${optionalString (cfg.static != null) "cp -R ${cfg.static} static"} ln -s $data static/data # Build the website hugo -d $out ''; cfg = config.services.extranix; in { options.services.extranix = { enable = mkEnableOption "extranix documentation"; theme = mkOption { type = path; description = '' Path to a hugo theme for the search website. ''; }; modules = mkOption { type = attrsOf (submodule { options = { specialArgs = mkOption { type = attrs; default = { }; description = '' Special arguments to give to evalModules. ''; }; paths = mkOption { type = listOf deferredModule; description = '' Modules to from which to document options. ''; }; ignored-modules = mkOption { type = listOf deferredModule; default = [ ]; description = '' Modules required to make modules of `paths` valid. ''; example = '' import "''${infra-modulesPath}/module-list.nix" ''; }; path-translations = mkOption { type = listOf (submodule { options = { base = mkOption { type = path; description = '' Base path of some module files to be documented. ''; apply = v: "${removeSuffix "/" (builtins.toString v)}/"; }; url = mkOption { type = str; description = '' Url root to use for files on this path. ''; apply = removeSuffix "/"; }; }; }); description = '' Rules to convert file paths to urls in the documentation to indicate where the option is declared. ''; }; }; }); description = '' Sets of modules to be documented separately. The identifier to give for `settings.params.release_current_stable` (which is the default module shown) is the key after passing through `sanitizeDerivationName`. ''; }; host = mkOption { type = str; description = '' Hostname of the service. ''; }; index = mkOption { type = str; default = head (attrNames cfg.modules); defaultText = literalExpression "head (attrNames config.services.extranix.modules)"; description = '' The main module to show when loading the website. ''; apply = sanitizeDerivationName; }; static = mkOption { type = nullOr path; default = null; description = '' Path to extra static files. ''; }; settings = mkOption { inherit (pkgs.formats.json { }) type; description = '' Settings of the hugo website. ''; example = { baseUrl = "https://example.org/"; title = "Module documentation"; languageCode = "en-us"; params = { release_current_stable = "Some-Module"; logo = "logo.png"; footer_credits_line = '' Based on Home Manager Option Search ''; footer_copyright_line = '' Made by catvayor for the DGNum. ''; main_menu = [ { name = ''Sources''; url = "https://git.example.org/"; } ]; }; }; }; }; config = mkIf cfg.enable { services = { extranix.settings = { theme = "options-search"; params = { releases = mapAttrsToList (name: _: { inherit name; value = sanitizeDerivationName name; }) cfg.modules; release_current_stable = cfg.index; }; }; nginx = { enable = true; virtualHosts.${cfg.host}.locations."/".root = website; }; }; assertions = [ { assertion = cfg.modules != { }; message = '' `services.extranix` can't be enabled without any modules to document. ''; } ]; }; }