Skip to content

Commit 00d0676

Browse files
committed
modules/docs: add optionPages option
Provides a way to add a page to the docs that documents a subset of `options` using nixos-render-docs.
1 parent 5d4f54d commit 00d0676

File tree

4 files changed

+296
-0
lines changed

4 files changed

+296
-0
lines changed

modules/docs/_utils.nix

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
{ lib, ... }:
2+
let
3+
transformOption =
4+
let
5+
root = builtins.toString ../../.;
6+
mkGitHubDeclaration = user: repo: branch: subpath: {
7+
url = "https://github.com/${user}/${repo}/blob/${branch}/${subpath}";
8+
name = "<${repo}/${subpath}>";
9+
};
10+
transformDeclaration =
11+
decl:
12+
if lib.hasPrefix root (builtins.toString decl) then
13+
mkGitHubDeclaration "nix-community" "nixvim" "main" (
14+
lib.removePrefix "/" (lib.strings.removePrefix root (builtins.toString decl))
15+
)
16+
else if decl == "lib/modules.nix" then
17+
mkGitHubDeclaration "NixOS" "nixpkgs" "master" decl
18+
else
19+
decl;
20+
in
21+
opt: opt // { declarations = builtins.map transformDeclaration opt.declarations; };
22+
in
23+
{
24+
options.docs._utils = lib.mkOption {
25+
type = with lib.types; lazyAttrsOf raw;
26+
description = "internal utils, modules, functions, etc";
27+
default = { };
28+
internal = true;
29+
visible = false;
30+
};
31+
32+
config.docs._utils = {
33+
/**
34+
Uses `lib.optionAttrSetToDocList` to produce a list of docs-options.
35+
36+
A doc-option has the following attrs, as expected by `nixos-render-docs`:
37+
38+
```
39+
{
40+
loc,
41+
name, # rendered with `showOption loc`
42+
description,
43+
declarations,
44+
internal,
45+
visible, # normalised to a boolean
46+
readOnly,
47+
type, # normalised to `type.description`
48+
default,? # rendered with `lib.options.renderOptionValue`
49+
example,? # rendered with `lib.options.renderOptionValue`
50+
relatedPackages,?
51+
}
52+
```
53+
54+
Additionally, sub-options are recursively flattened into the list,
55+
unless `visible == "shallow"` or `visible == false`.
56+
57+
This function extends `lib.optionAttrSetToDocList` by also filtering out
58+
invisible and internal options, and by applying Nixvim's `transformOption`
59+
function.
60+
61+
The implementation is based on `pkgs.nixosOptionsDoc`:
62+
https://github.com/NixOS/nixpkgs/blob/e2078ef3/nixos/lib/make-options-doc/default.nix#L117-L126
63+
*/
64+
mkOptionList = lib.flip lib.pipe [
65+
(lib.flip builtins.removeAttrs [ "_module" ])
66+
lib.optionAttrSetToDocList
67+
(builtins.map transformOption)
68+
(builtins.filter (opt: opt.visible && !opt.internal))
69+
# TODO: consider supporting `relatedPackages`
70+
# See https://github.com/NixOS/nixpkgs/blob/61235d44/lib/options.nix#L103-L104
71+
# and https://github.com/NixOS/nixpkgs/blob/61235d44/nixos/lib/make-options-doc/default.nix#L128-L165
72+
];
73+
};
74+
}

modules/docs/default.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ in
1111
};
1212

1313
imports = [
14+
./_utils.nix
1415
./mdbook
1516
./menu
17+
./option-pages
1618
./pages.nix
1719
./user-guide
1820
];

modules/docs/option-pages/default.nix

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
{
2+
lib,
3+
config,
4+
options,
5+
pkgs,
6+
...
7+
}:
8+
let
9+
inherit (config.docs._utils)
10+
mkOptionList
11+
;
12+
13+
# Gets the page that owns this option.
14+
# We can use `findFirst` because `pageScopes` is a sorted list.
15+
getPageFor =
16+
loc: (lib.findFirst (pair: lib.lists.hasPrefix pair.scope loc) null pageScopePairs).page or null;
17+
18+
optionsLists = builtins.groupBy (opt: getPageFor opt.loc) (mkOptionList options);
19+
20+
# A list of { page, scope } pairs, sorted by scope length (longest first)
21+
pageScopePairs = lib.pipe config.docs.optionPages [
22+
(lib.mapAttrsToList (
23+
name: page: {
24+
page = name;
25+
scopes = page.optionScopes;
26+
}
27+
))
28+
(builtins.concatMap ({ page, scopes }: builtins.map (scope: { inherit page scope; }) scopes))
29+
(lib.sortOn (pair: 0 - builtins.length pair.scope))
30+
];
31+
32+
# Custom type to simplify type checking & merging.
33+
# `listOf str` is overkill and problematic for our use-case.
34+
optionLocType = lib.mkOptionType {
35+
name = "option-loc";
36+
description = "option location";
37+
descriptionClass = "noun";
38+
check = v: lib.isList v && lib.all lib.isString v;
39+
};
40+
41+
optionsPageModule =
42+
{ name, config, ... }:
43+
{
44+
options = {
45+
enable = lib.mkOption {
46+
type = lib.types.bool;
47+
default = config.optionsList != [ ];
48+
defaultText = lib.literalExpression ''optionsList != [ ]'';
49+
example = true;
50+
description = "Whether to define a page derived from this optionsPage.";
51+
};
52+
optionsList = lib.mkOption {
53+
type = with lib.types; listOf raw;
54+
description = ''
55+
List of options matching `scopes`.
56+
'';
57+
readOnly = true;
58+
};
59+
optionsJSON = lib.mkOption {
60+
type = lib.types.package;
61+
description = ''
62+
`options.json` file, as expected by `nixos-render-docs`.
63+
'';
64+
readOnly = true;
65+
};
66+
page = lib.mkOption {
67+
type = lib.types.deferredModule;
68+
description = ''
69+
The `page` module, to be assigned to `docs.pages.<name>`.
70+
'';
71+
defaultText = lib.literalMD ''
72+
Options derived from the outer options-page
73+
'';
74+
};
75+
};
76+
77+
config =
78+
let
79+
cfg = config;
80+
drvName = lib.replaceStrings [ "/" ] [ "-" ] name;
81+
# Convert the doc-options list into the structure required for options.json
82+
# See https://github.com/NixOS/nixpkgs/blob/e2078ef3/nixos/lib/make-options-doc/default.nix#L167-L176
83+
optionsSet = builtins.listToAttrs (
84+
builtins.map (opt: {
85+
inherit (opt) name;
86+
value = builtins.removeAttrs opt [
87+
"name"
88+
"visible"
89+
"internal"
90+
];
91+
}) config.optionsList
92+
);
93+
in
94+
{
95+
optionsJSON = builtins.toFile "options-${drvName}.json" (
96+
builtins.unsafeDiscardStringContext (builtins.toJSON optionsSet)
97+
);
98+
99+
page =
100+
{ config, ... }:
101+
{
102+
# TODO: should this be conditional on something?
103+
text = lib.mkMerge [
104+
# TODO: title
105+
# TODO: description
106+
];
107+
# NOTE: use a _really_ high override priority because there will
108+
# be another definition with the same prio as `text`
109+
source = lib.mkIf (cfg.optionsList != [ ]) (
110+
lib.mkOverride 1 (
111+
pkgs.callPackage ./render-page.nix {
112+
inherit (config) text;
113+
inherit (cfg) optionsJSON;
114+
name = drvName;
115+
}
116+
)
117+
);
118+
};
119+
};
120+
};
121+
122+
optionsPageType = lib.types.submodule (
123+
{ name, ... }:
124+
{
125+
imports = [
126+
optionsPageModule
127+
];
128+
options.optionScopes = lib.mkOption {
129+
type = with lib.types; coercedTo optionLocType lib.singleton (nonEmptyListOf optionLocType);
130+
description = ''
131+
A list of option-locations to be included in this page.
132+
'';
133+
};
134+
config = {
135+
optionsList = optionsLists.${name} or [ ];
136+
page.menu.section = lib.mkDefault "options";
137+
};
138+
}
139+
);
140+
141+
checkDocs =
142+
value:
143+
let
144+
duplicates = lib.pipe value [
145+
builtins.attrValues
146+
(builtins.concatMap (doc: doc.optionScopes))
147+
(builtins.groupBy lib.showOption)
148+
(lib.mapAttrs (_: builtins.length))
149+
(lib.filterAttrs (_: count: count > 1))
150+
];
151+
in
152+
assert lib.assertMsg (duplicates == { }) ''
153+
`docs.options` has conflicting `optionScopes` definitions:
154+
${lib.concatMapAttrsStringSep "\n" (
155+
name: count: "- `${name}' defined ${toString count} times"
156+
) duplicates}
157+
Definitions:${lib.options.showDefs options.docs.options.definitionsWithLocations}
158+
'';
159+
value;
160+
in
161+
{
162+
options.docs = {
163+
optionPages = lib.mkOption {
164+
type = with lib.types; lazyAttrsOf optionsPageType;
165+
description = ''
166+
A set of option scopes to include in the docs.
167+
168+
Each enabled options page will produce a corresponding `pages` page.
169+
'';
170+
default = { };
171+
apply = checkDocs;
172+
};
173+
};
174+
config.docs = {
175+
# Define pages for each "optionPages" attr
176+
pages = lib.pipe config.docs.optionPages [
177+
(lib.filterAttrs (_: v: v.enable))
178+
(builtins.mapAttrs (_: cfg: cfg.page))
179+
];
180+
};
181+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
path,
3+
nixos-render-docs,
4+
runCommand,
5+
name,
6+
text,
7+
optionsJSON,
8+
revision ? "",
9+
}:
10+
runCommand "page-${name}.md"
11+
{
12+
inherit
13+
text
14+
optionsJSON
15+
revision
16+
;
17+
18+
# https://github.com/NixOS/nixpkgs/blob/master/doc/manpage-urls.json
19+
manpageUrls = path + "/doc/manpage-urls.json";
20+
21+
nativeBuildInputs = [
22+
nixos-render-docs
23+
];
24+
}
25+
''
26+
nixos-render-docs -j $NIX_BUILD_CORES \
27+
options commonmark \
28+
--manpage-urls $manpageUrls \
29+
--revision "$revision" \
30+
--anchor-prefix opt- \
31+
--anchor-style legacy \
32+
$optionsJSON options.md
33+
34+
(
35+
echo "$text"
36+
echo
37+
cat options.md
38+
) > $out
39+
''

0 commit comments

Comments
 (0)