# Options are available in the following format: # https://github.com/nixos/nixpkgs/blob/master/nixos/lib/make-options-doc/default.nix # # ```json # { # ... # "fileSystems..options": { # "declarations": ["nixos/modules/tasks/filesystems.nix"], # "default": { # "_type": "literalExpression", # "text": "[\n \"defaults\"\n]" # }, # "description": "Options used to mount the file system.", # "example": { # "_type": "literalExpression", # "text": "[\n \"data=journal\"\n]" # }, # "loc": ["fileSystems", "", "options"], # "readOnly": false, # "type": "non-empty (list of string (with check: non-empty))" # "relatedPackages": "- [`pkgs.tmux`](\n https://search.nixos.org/packages?show=tmux&sort=relevance&query=tmux\n )\n", # } # } # ``` import json import os from pathlib import Path from typing import Any from clan_cli.api.modules import Frontmatter, extract_frontmatter, get_roles # Get environment variables CLAN_CORE_PATH = os.getenv("CLAN_CORE_PATH") CLAN_CORE_DOCS = os.getenv("CLAN_CORE_DOCS") CLAN_MODULES = os.environ.get("CLAN_MODULES") OUT = os.environ.get("out") def sanitize(text: str) -> str: return text.replace(">", "\\>") def replace_store_path(text: str) -> tuple[str, str]: res = text if text.startswith("/nix/store/"): res = "https://git.clan.lol/clan/clan-core/src/branch/main/" + str( Path(*Path(text).parts[4:]) ) name = Path(res).name return (res, name) def render_option_header(name: str) -> str: return f"# {name}\n" def join_lines_with_indentation(lines: list[str], indent: int = 4) -> str: """ Joins multiple lines with a specified number of whitespace characters as indentation. Args: lines (list of str): The lines of text to join. indent (int): The number of whitespace characters to use as indentation for each line. Returns: str: The indented and concatenated string. """ # Create the indentation string (e.g., four spaces) indent_str = " " * indent # Join each line with the indentation added at the beginning return "\n".join(indent_str + line for line in lines) def render_option(name: str, option: dict[str, Any], level: int = 3) -> str: read_only = option.get("readOnly") res = f""" {"#" * level} {sanitize(name)} {"**Readonly**" if read_only else ""} {option.get("description", "No description available.")} **Type**: `{option["type"]}` """ if option.get("default"): res += f""" **Default**: ```nix {option["default"]["text"] if option.get("default") else "No default set."} ``` """ example = option.get("example", {}).get("text") if example: example_indented = join_lines_with_indentation(example.split("\n")) res += f""" ???+ example ```nix {example_indented} ``` """ if option.get("relatedPackages"): res += f""" ### Related Packages {option["relatedPackages"]} """ decls = option.get("declarations", []) source_path, name = replace_store_path(decls[0]) print(source_path, name) res += f""" :simple-git: [{name}]({source_path}) """ res += "\n" return res def module_header(module_name: str) -> str: return f"# {module_name}\n\n" def module_usage(module_name: str) -> str: return f"""## Usage To use this module, import it like this: ```nix {{config, lib, inputs, ...}}: {{ imports = [ inputs.clan-core.clanModules.{module_name} ]; # ... }} ``` """ clan_core_descr = """ClanCore delivers all the essential features for every clan. It's always included in your setup, and you can customize your clan's behavior with the configuration [options](#module-options) provided below. """ options_head = "\n## Module Options\n" def produce_clan_core_docs() -> None: if not CLAN_CORE_DOCS: raise ValueError( f"Environment variables are not set correctly: $CLAN_CORE_DOCS={CLAN_CORE_DOCS}" ) if not OUT: raise ValueError(f"Environment variables are not set correctly: $out={OUT}") # A mapping of output file to content core_outputs: dict[str, str] = {} with open(CLAN_CORE_DOCS) as f: options: dict[str, dict[str, Any]] = json.load(f) module_name = "clan-core" for option_name, info in options.items(): outfile = f"{module_name}/index.md" # Create separate files for nested options if len(option_name.split(".")) <= 3: # i.e. clan-core.clanDir output = core_outputs.get( outfile, module_header(module_name) + clan_core_descr + options_head, ) output += render_option(option_name, info) # Update the content core_outputs[outfile] = output else: # Clan sub-options [_, sub] = option_name.split(".")[1:3] outfile = f"{module_name}/{sub}.md" # Get the content or write the header output = core_outputs.get(outfile, render_option_header(sub)) output += render_option(option_name, info) # Update the content core_outputs[outfile] = output for outfile, output in core_outputs.items(): (Path(OUT) / outfile).parent.mkdir(parents=True, exist_ok=True) with open(Path(OUT) / outfile, "w") as of: of.write(output) def render_roles(roles: list[str] | None, module_name: str) -> str: if roles: roles_list = "\n".join([f" - `{r}`" for r in roles]) return f""" ???+ tip "Inventory usage" Predefined roles: {roles_list} Usage: ```{{.nix hl_lines="4"}} buildClan {{ inventory.services = {{ {module_name}.instance_1 = {{ roles.{roles[0]}.machines = [ "sara_machine" ]; # ... }}; }}; }} ``` """ return "" def produce_clan_modules_docs() -> None: if not CLAN_MODULES: raise ValueError( f"Environment variables are not set correctly: $CLAN_MODULES={CLAN_MODULES}" ) if not CLAN_CORE_PATH: raise ValueError( f"Environment variables are not set correctly: $CLAN_CORE_PATH={CLAN_CORE_PATH}" ) if not OUT: raise ValueError(f"Environment variables are not set correctly: $out={OUT}") with open(CLAN_MODULES) as f: links: dict[str, str] = json.load(f) # with open(CLAN_MODULES_READMES) as readme: # readme_map: dict[str, str] = json.load(readme) # with open(CLAN_MODULES_META) as f: # meta_map: dict[str, Any] = json.load(f) # print(meta_map) # {'borgbackup': '/nix/store/hi17dwgy7963ddd4ijh81fv0c9sbh8sw-options.json', ... } for module_name, options_file in links.items(): readme_file = Path(CLAN_CORE_PATH) / "clanModules" / module_name / "README.md" print(module_name, readme_file) with open(readme_file) as f: readme = f.read() frontmatter: Frontmatter frontmatter, readme_content = extract_frontmatter(readme, str(readme_file)) print(frontmatter, readme_content) with open(Path(options_file) / "share/doc/nixos/options.json") as f: options: dict[str, dict[str, Any]] = json.load(f) print(f"Rendering options for {module_name}...") output = module_header(module_name) if frontmatter.description: output += f"**{frontmatter.description}**\n\n" output += f"{readme_content}\n" # get_roles(str) -> list[str] | None roles = get_roles(str(Path(CLAN_CORE_PATH) / "clanModules" / module_name)) if roles: output += render_roles(roles, module_name) output += module_usage(module_name) output += options_head if len(options.items()) else "" for option_name, info in options.items(): output += render_option(option_name, info) outfile = Path(OUT) / f"clanModules/{module_name}.md" outfile.parent.mkdir( parents=True, exist_ok=True, ) with open(outfile, "w") as of: of.write(output) if __name__ == "__main__": produce_clan_core_docs() produce_clan_modules_docs()