diff --git a/docs/default.nix b/docs/default.nix
index 9c75ca6a..b2b0d103 100644
--- a/docs/default.nix
+++ b/docs/default.nix
@@ -253,9 +253,15 @@ let
# Generate the HTML manual pages
home-manager-manual = pkgs.callPackage ./home-manager-manual.nix {
home-manager-options = {
- home-manager = hmOptionsDocs.optionsJSON;
- nixos = nixosOptionsDocs.optionsJSON;
- nix-darwin = nixDarwinOptionsDocs.optionsJSON;
+ home-manager = {
+ json = hmOptionsDocs.optionsJSON;
+ };
+ nixos = {
+ json = nixosOptionsDocs.optionsJSON;
+ };
+ nix-darwin = {
+ json = nixDarwinOptionsDocs.optionsJSON;
+ };
};
inherit revision;
};
diff --git a/docs/home-manager-manual.nix b/docs/home-manager-manual.nix
index c48cf88e..fc6ab660 100644
--- a/docs/home-manager-manual.nix
+++ b/docs/home-manager-manual.nix
@@ -1,63 +1,99 @@
{
stdenv,
lib,
- documentation-highlighter,
+ callPackage,
+ mdbook,
+ python3,
revision,
home-manager-options,
- nixos-render-docs,
}:
let
outputPath = "share/doc/home-manager";
+ mdbookOptions = callPackage ./mdbook/options.nix {
+ manpageUrls = ./manual/manpage-urls.json;
+ inherit revision;
+ optionDocs = {
+ home-manager = {
+ title = "Home Manager Configuration Options";
+ path = "home-manager";
+ prefix = "opt-";
+ json = "${home-manager-options.home-manager.json}";
+ };
+ nixos = {
+ title = "NixOS Configuration Options";
+ path = "nixos";
+ prefix = "nixos-opt-";
+ json = "${home-manager-options.nixos.json}";
+ };
+ nix-darwin = {
+ title = "nix-darwin Configuration Options";
+ path = "nix-darwin";
+ prefix = "nix-darwin-opt-";
+ json = "${home-manager-options.nix-darwin.json}";
+ };
+ };
+ };
in
stdenv.mkDerivation {
name = "home-manager-manual";
- nativeBuildInputs = [ nixos-render-docs ];
+ nativeBuildInputs = [
+ mdbook
+ python3
+ ];
- src = ./manual;
+ src = ./.;
buildPhase = ''
- mkdir -p out/{highlightjs,media}
+ runHook preBuild
- cp -t out/highlightjs \
- ${documentation-highlighter}/highlight.pack.js \
- ${documentation-highlighter}/LICENSE \
- ${documentation-highlighter}/mono-blue.css \
- ${documentation-highlighter}/loader.js
+ mkdir -p source
+ python3 ${./mdbook/convert-markup.py} "$src/manual" source
+ python3 ${./mdbook/convert-markup.py} \
+ --base-depth 1 \
+ "$src/release-notes" \
+ source/release-notes
- substituteInPlace ./options.md \
- --subst-var-by \
- OPTIONS_JSON \
- ${home-manager-options.home-manager}/share/doc/nixos/options.json
+ cp -r ${mdbookOptions}/options source/options
- substituteInPlace ./nixos-options.md \
- --subst-var-by \
- OPTIONS_JSON \
- ${home-manager-options.nixos}/share/doc/nixos/options.json
+ python3 ${./mdbook/substitute-summary.py} \
+ source/SUMMARY.md \
+ ${mdbookOptions}/summary/home-manager.md \
+ ${mdbookOptions}/summary/nixos.md \
+ ${mdbookOptions}/summary/nix-darwin.md
- substituteInPlace ./nix-darwin-options.md \
- --subst-var-by \
- OPTIONS_JSON \
- ${home-manager-options.nix-darwin}/share/doc/nixos/options.json
+ mdbook build source --dest-dir book
- cp ${./options.html} out/options.html
+ mkdir -p out
+ cp -r book/* out/
- cp ${./static/style.css} out/style.css
- cp ${./static/anchor-links.js} out/anchor-links.js
+ makeRedirect() {
+ local target=$1
+ local destination=$2
+ printf '%s\n' \
+ '' \
+ '' \
+ '
' \
+ ' ' \
+ " " \
+ " " \
+ ' Redirecting...' \
+ ' ' \
+ ' ' \
+ " Redirecting to $destination.
" \
+ ' ' \
+ '' \
+ > "out/$target"
+ }
- cp -r ${./release-notes} release-notes
+ makeRedirect index.xhtml index.html
+ makeRedirect options.html options/home-manager/index.html
+ makeRedirect options.xhtml options/home-manager/index.html
+ makeRedirect nixos-options.xhtml options/nixos/index.html
+ makeRedirect nix-darwin-options.xhtml options/nix-darwin/index.html
+ makeRedirect release-notes.xhtml release-notes/release-notes.html
- nixos-render-docs manual html \
- --manpage-urls ./manpage-urls.json \
- --revision ${lib.trivial.revisionWithDefault revision} \
- --stylesheet style.css \
- --script highlightjs/highlight.pack.js \
- --script highlightjs/loader.js \
- --script anchor-links.js \
- --toc-depth 1 \
- --section-toc-depth 1 \
- manual.md \
- out/index.xhtml
+ runHook postBuild
'';
installPhase = ''
@@ -69,7 +105,9 @@ stdenv.mkDerivation {
echo "doc manual $dest index.html" >> $out/nix-support/hydra-build-products
'';
- passthru = { inherit home-manager-options; };
+ passthru = {
+ inherit home-manager-options mdbookOptions;
+ };
meta = {
maintainers = [ lib.maintainers.considerate ];
diff --git a/docs/html-open-tool.nix b/docs/html-open-tool.nix
index 279a8a06..f8e97adf 100644
--- a/docs/html-open-tool.nix
+++ b/docs/html-open-tool.nix
@@ -25,9 +25,14 @@ let
if [[ ! -v BROWSER || -z $BROWSER ]]; then
echo "$0: unable to start a web browser; please set \$BROWSER"
exit 1
- else
- exec "$BROWSER" "${html}/share/doc/${pathName}/index.xhtml"
fi
+
+ manualPath="${html}/share/doc/${pathName}/index.html"
+ if [[ ! -e $manualPath ]]; then
+ manualPath="${html}/share/doc/${pathName}/index.xhtml"
+ fi
+
+ exec "$BROWSER" "$manualPath"
'';
desktopItem = makeDesktopItem {
diff --git a/docs/manual/SUMMARY.md b/docs/manual/SUMMARY.md
new file mode 100644
index 00000000..27971e5a
--- /dev/null
+++ b/docs/manual/SUMMARY.md
@@ -0,0 +1,64 @@
+# Summary
+
+- [Preface](preface.md)
+- [Introduction to Home Manager](introduction.md)
+- [Installing Home Manager](installation.md)
+ - [Standalone installation](installation/standalone.md)
+ - [NixOS module](installation/nixos.md)
+ - [nix-darwin module](installation/nix-darwin.md)
+- [Using Home Manager](usage.md)
+ - [Configuration Example](usage/configuration.md)
+ - [Rollbacks](usage/rollbacks.md)
+ - [Keeping your ~ safe from harm](usage/dotfiles.md)
+ - [Graphical services](usage/graphical.md)
+ - [GPU on non-NixOS systems](usage/gpu-non-nixos.md)
+ - [Modular Services](usage/modular-services.md)
+ - [Updating](usage/updating.md)
+ - [Upgrading to a new Home Manager release](usage/upgrading.md)
+- [Nix Flakes](nix-flakes.md)
+ - [Prerequisites](nix-flakes/prerequisites.md)
+ - [Standalone setup](nix-flakes/standalone.md)
+ - [NixOS module](nix-flakes/nixos.md)
+ - [nix-darwin module](nix-flakes/nix-darwin.md)
+ - [flake-parts module](nix-flakes/flake-parts.md)
+- [Writing Home Manager Modules](writing-modules.md)
+ - [Option Types](writing-modules/types.md)
+- [Contributing](contributing.md)
+ - [Getting started](contributing/getting-started.md)
+ - [Guidelines](contributing/guidelines.md)
+ - [Release Notes](contributing/release-notes.md)
+ - [News](contributing/news.md)
+ - [Tests](contributing/tests.md)
+- [Home Manager Internals](internals.md)
+ - [Activation](internals/activation.md)
+- [Third-Party Tools and Extensions](3rd-party.md)
+ - [Module Collections](3rd-party/collections.md)
+- [Frequently Asked Questions (FAQ)](faq.md)
+ - [Why is there a collision error when switching generation?](faq/collision.md)
+ - [Why are the session variables not set?](faq/session-variables.md)
+ - [How to set up a configuration for multiple users/machines?](faq/multiple-users-machines.md)
+ - [Why do I get an error message about `ca.desrt.dconf` or `dconf.service`?](faq/ca-desrt-dconf.md)
+ - [How do I install packages from Nixpkgs unstable?](faq/unstable.md)
+ - [How do I change the package used by a module?](faq/change-package-module.md)
+- [Options](options.md)
+@HOME_MANAGER_OPTIONS@
+@NIXOS_OPTIONS@
+@NIX_DARWIN_OPTIONS@
+- [Release Notes](release-notes/release-notes.md)
+ - [Release 26.11](release-notes/rl-2611.md)
+ - [Release 26.05](release-notes/rl-2605.md)
+ - [Release 25.11](release-notes/rl-2511.md)
+ - [Release 25.05](release-notes/rl-2505.md)
+ - [Release 24.11](release-notes/rl-2411.md)
+ - [Release 24.05](release-notes/rl-2405.md)
+ - [Release 23.11](release-notes/rl-2311.md)
+ - [Release 23.05](release-notes/rl-2305.md)
+ - [Release 22.11](release-notes/rl-2211.md)
+ - [Release 22.05](release-notes/rl-2205.md)
+ - [Release 21.11](release-notes/rl-2111.md)
+ - [Release 21.05](release-notes/rl-2105.md)
+ - [Release 20.09](release-notes/rl-2009.md)
+ - [Release 20.03](release-notes/rl-2003.md)
+ - [Release 19.09](release-notes/rl-1909.md)
+ - [Release 19.03](release-notes/rl-1903.md)
+ - [Release 18.09](release-notes/rl-1809.md)
diff --git a/docs/manual/book.toml b/docs/manual/book.toml
new file mode 100644
index 00000000..5cccaffe
--- /dev/null
+++ b/docs/manual/book.toml
@@ -0,0 +1,11 @@
+[book]
+title = "Home Manager Manual"
+language = "en"
+src = "."
+
+[output.html]
+git-repository-url = "https://github.com/nix-community/home-manager"
+
+[output.html.fold]
+enable = true
+level = 0
diff --git a/docs/manual/contributing/guidelines.md b/docs/manual/contributing/guidelines.md
index ecec4da9..44b9dd9e 100644
--- a/docs/manual/contributing/guidelines.md
+++ b/docs/manual/contributing/guidelines.md
@@ -108,7 +108,7 @@ Manager Git repository:
``` shell
$ nix-build -A docs.html
-$ xdg-open ./result/share/doc/home-manager/index.xhtml
+$ xdg-open ./result/share/doc/home-manager/index.html
```
When you have made changes to a module, it is a good idea to check that
diff --git a/docs/manual/usage/upgrading.md b/docs/manual/usage/upgrading.md
index f4e89927..1f318145 100644
--- a/docs/manual/usage/upgrading.md
+++ b/docs/manual/usage/upgrading.md
@@ -16,7 +16,7 @@ use Home Manager's `master` branch.
Home Manager follows NixOS release cycles and provides corresponding branches:
-- **release-**: Stable branch for the matching NixOS or
+- **release-\**: Stable branch for the matching NixOS or
Nixpkgs release, such as `release-25.11`.
- **master**: Development branch (tracks nixpkgs-unstable)
diff --git a/docs/mdbook/convert-markup.py b/docs/mdbook/convert-markup.py
new file mode 100644
index 00000000..0f5924c7
--- /dev/null
+++ b/docs/mdbook/convert-markup.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import html
+import re
+import shutil
+import sys
+from pathlib import Path
+
+
+SIMPLE_ROLES = (
+ "command",
+ "component",
+ "description",
+ "file",
+ "index",
+ "system",
+ "type",
+)
+
+HEADING_ANCHOR = re.compile(r"^(#{1,6}\s+)(.*)\s+\{#([^}]+)\}\s*$")
+INLINE_ANCHOR = re.compile(r"\[\]\{#([^}]+)\}")
+OPTION_ROLE = re.compile(r"(?[^\]]*)\]\(#(?P(?:opt|nixos-opt|nix-darwin-opt)-[^)]+)\)"
+)
+LEFTOVER_ROLE = re.compile(
+ r"(? str:
+ if anchor.startswith("nix-darwin-opt-"):
+ option = anchor.removeprefix("nix-darwin-opt-")
+ option = option.replace("<", "_").replace(">", "_")
+ anchor = f"nix-darwin-opt-{option}"
+ base = "options/nix-darwin"
+ elif anchor.startswith("nixos-opt-"):
+ option = anchor.removeprefix("nixos-opt-")
+ option = option.replace("<", "_").replace(">", "_")
+ anchor = f"nixos-opt-{option}"
+ base = "options/nixos"
+ else:
+ option = anchor.removeprefix("opt-")
+ option = option.replace("<", "_").replace(">", "_")
+ anchor = f"opt-{option}"
+ base = "options/home-manager"
+
+ page_parts = option_page_parts(option)
+ prefix = "../" * (base_depth + len(current_file.parent.parts))
+ return f"{prefix}{base}/{'/'.join(page_parts)}.md#{anchor}"
+
+
+def option_page_parts(option_name: str) -> list[str]:
+ parts = option_name.split(".")
+ namespace = parts[0]
+ if namespace in DEEP_SPLIT_NAMESPACES and len(parts) > 1:
+ return parts[:2]
+ return [namespace]
+
+
+def option_label(anchor: str) -> str:
+ if anchor.startswith("nix-darwin-opt-"):
+ return anchor.removeprefix("nix-darwin-opt-")
+ if anchor.startswith("nixos-opt-"):
+ return anchor.removeprefix("nixos-opt-")
+ return anchor.removeprefix("opt-")
+
+
+def markdown_label(value: str) -> str:
+ return value.replace("<", "<").replace(">", ">")
+
+
+def convert_inline(line: str, current_file: Path, base_depth: int) -> str:
+ line = line.replace("index.xhtml", "index.html")
+ line = INLINE_ANCHOR.sub(
+ lambda match: f'',
+ line,
+ )
+ line = OPTION_ROLE.sub(
+ lambda match: (
+ f"[{markdown_label(match.group(1))}]"
+ f"({option_target(f'opt-{match.group(1)}', current_file, base_depth)})"
+ ),
+ line,
+ )
+ line = OPTION_LINK.sub(
+ lambda match: (
+ f"[{markdown_label(match.group('label') or option_label(match.group('anchor')))}]"
+ f"({option_target(match.group('anchor'), current_file, base_depth)})"
+ ),
+ line,
+ )
+ return SIMPLE_ROLE.sub(lambda match: f"`{match.group(2)}`", line)
+
+
+def convert_heading(line: str, current_file: Path, base_depth: int) -> str:
+ match = HEADING_ANCHOR.match(line)
+ if match is None:
+ return convert_inline(line, current_file, base_depth)
+
+ prefix, title, anchor = match.groups()
+ return (
+ f'\n'
+ f"{prefix}{convert_inline(title, current_file, base_depth)}"
+ )
+
+
+def is_include_fence(line: str) -> tuple[bool, str]:
+ match = FENCE.match(line)
+ if match is None:
+ return False, ""
+
+ info = match.group(2).strip()
+ return info.startswith(("{=include=}", "include")), match.group(1)
+
+
+def convert_markdown(
+ text: str,
+ source: Path,
+ current_file: Path,
+ base_depth: int,
+) -> str:
+ output: list[str] = []
+ in_code_fence = False
+ code_fence = ""
+ in_include = False
+ include_fence = ""
+ in_admonition = False
+
+ for raw_line in text.splitlines(keepends=True):
+ line = raw_line[:-1] if raw_line.endswith("\n") else raw_line
+ newline = "\n" if raw_line.endswith("\n") else ""
+
+ if in_include:
+ if FENCE_CLOSE.match(line) and len(line.strip()) >= len(include_fence):
+ in_include = False
+ include_fence = ""
+ continue
+
+ if in_code_fence:
+ output.append(raw_line)
+ if FENCE_CLOSE.match(line) and len(line.strip()) >= len(code_fence):
+ in_code_fence = False
+ code_fence = ""
+ continue
+
+ include, fence = is_include_fence(line)
+ if include:
+ in_include = True
+ include_fence = fence
+ continue
+
+ fence_match = FENCE.match(line)
+ if fence_match is not None:
+ in_code_fence = True
+ code_fence = fence_match.group(1)
+ output.append(raw_line)
+ continue
+
+ if in_admonition:
+ if ADMONITION_CLOSE.match(line):
+ in_admonition = False
+ continue
+ converted = convert_inline(line, current_file, base_depth)
+ output.append(f"> {converted}{newline}" if converted else ">\n")
+ continue
+
+ admonition = ADMONITION_OPEN.match(line)
+ if admonition is not None:
+ in_admonition = True
+ output.append(f"> **{admonition.group(1).title()}**\n")
+ continue
+
+ output.append(convert_heading(line, current_file, base_depth) + newline)
+
+ if in_include:
+ raise ValueError(f"{source}: unterminated include block")
+
+ converted = "".join(output)
+ if LEFTOVER_ROLE.search(converted):
+ raise ValueError(f"{source}: unconverted NixOS-render-docs role remains")
+ if "```{=include=}" in converted:
+ raise ValueError(f"{source}: unconverted include block remains")
+
+ return converted
+
+
+def convert_tree(source: Path, destination: Path, base_depth: int) -> None:
+ for path in source.rglob("*"):
+ if not path.is_file():
+ continue
+
+ target = destination / path.relative_to(source)
+ target.parent.mkdir(parents=True, exist_ok=True)
+
+ if path.suffix == ".md":
+ text = path.read_text(encoding="utf-8")
+ target.write_text(
+ convert_markdown(text, path, path.relative_to(source), base_depth),
+ encoding="utf-8",
+ )
+ else:
+ shutil.copy2(path, target)
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--base-depth", type=int, default=0)
+ parser.add_argument("source", type=Path)
+ parser.add_argument("destination", type=Path)
+ args = parser.parse_args()
+
+ if not args.source.is_dir():
+ print(f"missing source directory: {args.source}", file=sys.stderr)
+ return 1
+
+ args.destination.mkdir(parents=True, exist_ok=True)
+ convert_tree(args.source, args.destination, args.base_depth)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/docs/mdbook/options.nix b/docs/mdbook/options.nix
new file mode 100644
index 00000000..5d3321c2
--- /dev/null
+++ b/docs/mdbook/options.nix
@@ -0,0 +1,23 @@
+{
+ pkgs,
+ optionDocs,
+ manpageUrls,
+ revision,
+}:
+
+pkgs.runCommand "home-manager-mdbook-options"
+ {
+ nativeBuildInputs = [
+ pkgs.buildPackages.nixos-render-docs
+ pkgs.buildPackages.python3
+ ];
+ optionDocsJson = builtins.toJSON optionDocs;
+ passAsFile = [ "optionDocsJson" ];
+ }
+ ''
+ python3 ${./render-options.py} \
+ "$optionDocsJsonPath" \
+ ${manpageUrls} \
+ ${revision} \
+ "$out"
+ ''
diff --git a/docs/mdbook/render-options.py b/docs/mdbook/render-options.py
new file mode 100644
index 00000000..5fd4a28a
--- /dev/null
+++ b/docs/mdbook/render-options.py
@@ -0,0 +1,221 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import json
+import re
+import subprocess
+import sys
+from pathlib import Path
+
+
+OPTION_LINK = re.compile(
+ r"\[(?P