···176176177177- NixOS now defaults to using nsncd (a non-caching reimplementation in Rust) as NSS lookup dispatcher, instead of the buggy and deprecated glibc-provided nscd. If you need to switch back, set `services.nscd.enableNsncd = false`, but please open an issue in nixpkgs so your issue can be fixed.
178178179179+- `services.borgmatic` now allows for multiple configurations, placed in `/etc/borgmatic.d/`, you can define them with `services.borgmatic.configurations`.
180180+179181- The `dnsmasq` service now takes configuration via the
180182 `services.dnsmasq.settings` attribute set. The option
181183 `services.dnsmasq.extraConfig` will be deprecated when NixOS 22.11 reaches
+4-1
nixos/lib/make-disk-image.nix
···154154, # Shell code executed after the VM has finished.
155155 postVM ? ""
156156157157+, # Guest memory size
158158+ memSize ? 1024
159159+157160, # Copy the contents of the Nix store to the root of the image and
158161 # skip further setup. Incompatible with `contents`,
159162 # `installBootLoader` and `configFile`.
···525528 "-drive if=pflash,format=raw,unit=1,file=$efiVars"
526529 ]
527530 );
528528- memSize = 1024;
531531+ inherit memSize;
529532 } ''
530533 export PATH=${binPath}:$PATH
531534
+4
nixos/lib/make-multi-disk-zfs-image.nix
···7373, # Shell code executed after the VM has finished.
7474 postVM ? ""
75757676+, # Guest memory size
7777+ memSize ? 1024
7878+7679, name ? "nixos-disk-image"
77807881, # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
···242245 {
243246 QEMU_OPTS = "-drive file=$bootDiskImage,if=virtio,cache=unsafe,werror=report"
244247 + " -drive file=$rootDiskImage,if=virtio,cache=unsafe,werror=report";
248248+ inherit memSize;
245249 preVM = ''
246250 PATH=$PATH:${pkgs.qemu_kvm}/bin
247251 mkdir $out
+52-34
nixos/modules/services/backup/borgmatic.nix
···55let
66 cfg = config.services.borgmatic;
77 settingsFormat = pkgs.formats.yaml { };
88+99+ cfgType = with types; submodule {
1010+ freeformType = settingsFormat.type;
1111+ options.location = {
1212+ source_directories = mkOption {
1313+ type = listOf str;
1414+ description = mdDoc ''
1515+ List of source directories to backup (required). Globs and
1616+ tildes are expanded.
1717+ '';
1818+ example = [ "/home" "/etc" "/var/log/syslog*" ];
1919+ };
2020+ repositories = mkOption {
2121+ type = listOf str;
2222+ description = mdDoc ''
2323+ Paths to local or remote repositories (required). Tildes are
2424+ expanded. Multiple repositories are backed up to in
2525+ sequence. Borg placeholders can be used. See the output of
2626+ "borg help placeholders" for details. See ssh_command for
2727+ SSH options like identity file or port. If systemd service
2828+ is used, then add local repository paths in the systemd
2929+ service file to the ReadWritePaths list.
3030+ '';
3131+ example = [
3232+ "ssh://user@backupserver/./sourcehostname.borg"
3333+ "ssh://user@backupserver/./{fqdn}"
3434+ "/var/local/backups/local.borg"
3535+ ];
3636+ };
3737+ };
3838+ };
3939+840 cfgfile = settingsFormat.generate "config.yaml" cfg.settings;
99-in {
4141+in
4242+{
1043 options.services.borgmatic = {
1111- enable = mkEnableOption (lib.mdDoc "borgmatic");
4444+ enable = mkEnableOption (mdDoc "borgmatic");
12451346 settings = mkOption {
1414- description = lib.mdDoc ''
4747+ description = mdDoc ''
1548 See https://torsion.org/borgmatic/docs/reference/configuration/
1649 '';
1717- type = types.submodule {
1818- freeformType = settingsFormat.type;
1919- options.location = {
2020- source_directories = mkOption {
2121- type = types.listOf types.str;
2222- description = lib.mdDoc ''
2323- List of source directories to backup (required). Globs and
2424- tildes are expanded.
2525- '';
2626- example = [ "/home" "/etc" "/var/log/syslog*" ];
2727- };
2828- repositories = mkOption {
2929- type = types.listOf types.str;
3030- description = lib.mdDoc ''
3131- Paths to local or remote repositories (required). Tildes are
3232- expanded. Multiple repositories are backed up to in
3333- sequence. Borg placeholders can be used. See the output of
3434- "borg help placeholders" for details. See ssh_command for
3535- SSH options like identity file or port. If systemd service
3636- is used, then add local repository paths in the systemd
3737- service file to the ReadWritePaths list.
3838- '';
3939- example = [
4040- "user@backupserver:sourcehostname.borg"
4141- "user@backupserver:{fqdn}"
4242- ];
4343- };
4444- };
4545- };
5050+ default = null;
5151+ type = types.nullOr cfgType;
5252+ };
5353+5454+ configurations = mkOption {
5555+ description = mdDoc ''
5656+ Set of borgmatic configurations, see https://torsion.org/borgmatic/docs/reference/configuration/
5757+ '';
5858+ default = { };
5959+ type = types.attrsOf cfgType;
4660 };
4761 };
4862···50645165 environment.systemPackages = [ pkgs.borgmatic ];
52665353- environment.etc."borgmatic/config.yaml".source = cfgfile;
6767+ environment.etc = (optionalAttrs (cfg.settings != null) { "borgmatic/config.yaml".source = cfgfile; }) //
6868+ mapAttrs'
6969+ (name: value: nameValuePair
7070+ "borgmatic.d/${name}.yaml"
7171+ { source = settingsFormat.generate "${name}.yaml" value; })
7272+ cfg.configurations;
54735574 systemd.packages = [ pkgs.borgmatic ];
5656-5775 };
5876}
+2-2
nixos/modules/services/web-apps/akkoma.md
···318318{option}`services.systemd.akkoma.serviceConfig.BindPaths` and
319319{option}`services.systemd.akkoma.serviceConfig.BindReadOnlyPaths` permit access to outside paths
320320through bind mounts. Refer to
321321-[{manpage}`systemd.exec(5)`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths=)
322322-for details.
321321+[`BindPaths=`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths=)
322322+of {manpage}`systemd.exec(5)` for details.
323323324324### Distributed deployment {#modules-services-akkoma-distributed-deployment}
325325
+1-1
nixos/modules/system/boot/networkd.nix
···19481948 Extra command-line arguments to pass to systemd-networkd-wait-online.
19491949 These also affect per-interface `systemd-network-wait-online@` services.
1950195019511951- See [{manpage}`systemd-networkd-wait-online.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-networkd-wait-online.service.html) for all available options.
19511951+ See {manpage}`systemd-networkd-wait-online.service(8)` for all available options.
19521952 '';
19531953 type = with types; listOf str;
19541954 default = [];
···11+from __future__ import annotations
22+33+import dataclasses as dc
44+import html
55+import itertools
66+77+from typing import cast, get_args, Iterable, Literal, Sequence
88+99+from markdown_it.token import Token
1010+1111+from .utils import Freezeable
1212+1313+# FragmentType is used to restrict structural include blocks.
1414+FragmentType = Literal['preface', 'part', 'chapter', 'section', 'appendix']
1515+1616+# in the TOC all fragments are allowed, plus the all-encompassing book.
1717+TocEntryType = Literal['book', 'preface', 'part', 'chapter', 'section', 'appendix']
1818+1919+def is_include(token: Token) -> bool:
2020+ return token.type == "fence" and token.info.startswith("{=include=} ")
2121+2222+# toplevel file must contain only the title headings and includes, anything else
2323+# would cause strange rendering.
2424+def _check_book_structure(tokens: Sequence[Token]) -> None:
2525+ for token in tokens[6:]:
2626+ if not is_include(token):
2727+ assert token.map
2828+ raise RuntimeError(f"unexpected content in line {token.map[0] + 1}, "
2929+ "expected structural include")
3030+3131+# much like books, parts may not contain headings other than their title heading.
3232+# this is a limitation of the current renderers and TOC generators that do not handle
3333+# this case well even though it is supported in docbook (and probably supportable
3434+# anywhere else).
3535+def _check_part_structure(tokens: Sequence[Token]) -> None:
3636+ _check_fragment_structure(tokens)
3737+ for token in tokens[3:]:
3838+ if token.type == 'heading_open':
3939+ assert token.map
4040+ raise RuntimeError(f"unexpected heading in line {token.map[0] + 1}")
4141+4242+# two include blocks must either be adjacent or separated by a heading, otherwise
4343+# we cannot generate a correct TOC (since there'd be nothing to link to between
4444+# the two includes).
4545+def _check_fragment_structure(tokens: Sequence[Token]) -> None:
4646+ for i, token in enumerate(tokens):
4747+ if is_include(token) \
4848+ and i + 1 < len(tokens) \
4949+ and not (is_include(tokens[i + 1]) or tokens[i + 1].type == 'heading_open'):
5050+ assert token.map
5151+ raise RuntimeError(f"unexpected content in line {token.map[0] + 1}, "
5252+ "expected heading or structural include")
5353+5454+def check_structure(kind: TocEntryType, tokens: Sequence[Token]) -> None:
5555+ wanted = { 'h1': 'title' }
5656+ wanted |= { 'h2': 'subtitle' } if kind == 'book' else {}
5757+ for (i, (tag, role)) in enumerate(wanted.items()):
5858+ if len(tokens) < 3 * (i + 1):
5959+ raise RuntimeError(f"missing {role} ({tag}) heading")
6060+ token = tokens[3 * i]
6161+ if token.type != 'heading_open' or token.tag != tag:
6262+ assert token.map
6363+ raise RuntimeError(f"expected {role} ({tag}) heading in line {token.map[0] + 1}", token)
6464+ for t in tokens[3 * len(wanted):]:
6565+ if t.type != 'heading_open' or not (role := wanted.get(t.tag, '')):
6666+ continue
6767+ assert t.map
6868+ raise RuntimeError(
6969+ f"only one {role} heading ({t.markup} [text...]) allowed per "
7070+ f"{kind}, but found a second in line {t.map[0] + 1}. "
7171+ "please remove all such headings except the first or demote the subsequent headings.",
7272+ t)
7373+7474+ last_heading_level = 0
7575+ for token in tokens:
7676+ if token.type != 'heading_open':
7777+ continue
7878+7979+ # book subtitle headings do not need an id, only book title headings do.
8080+ # every other headings needs one too. we need this to build a TOC and to
8181+ # provide stable links if the manual changes shape.
8282+ if 'id' not in token.attrs and (kind != 'book' or token.tag != 'h2'):
8383+ assert token.map
8484+ raise RuntimeError(f"heading in line {token.map[0] + 1} does not have an id")
8585+8686+ level = int(token.tag[1:]) # because tag = h1..h6
8787+ if level > last_heading_level + 1:
8888+ assert token.map
8989+ raise RuntimeError(f"heading in line {token.map[0] + 1} skips one or more heading levels, "
9090+ "which is currently not allowed")
9191+ last_heading_level = level
9292+9393+ if kind == 'book':
9494+ _check_book_structure(tokens)
9595+ elif kind == 'part':
9696+ _check_part_structure(tokens)
9797+ else:
9898+ _check_fragment_structure(tokens)
9999+100100+@dc.dataclass(frozen=True)
101101+class XrefTarget:
102102+ id: str
103103+ """link label for `[](#local-references)`"""
104104+ title_html: str
105105+ """toc label"""
106106+ toc_html: str | None
107107+ """text for `<title>` tags and `title="..."` attributes"""
108108+ title: str | None
109109+ """path to file that contains the anchor"""
110110+ path: str
111111+ """whether to drop the `#anchor` from links when expanding xrefs"""
112112+ drop_fragment: bool = False
113113+114114+ def href(self) -> str:
115115+ path = html.escape(self.path, True)
116116+ return path if self.drop_fragment else f"{path}#{html.escape(self.id, True)}"
117117+118118+@dc.dataclass
119119+class TocEntry(Freezeable):
120120+ kind: TocEntryType
121121+ target: XrefTarget
122122+ parent: TocEntry | None = None
123123+ prev: TocEntry | None = None
124124+ next: TocEntry | None = None
125125+ children: list[TocEntry] = dc.field(default_factory=list)
126126+ starts_new_chunk: bool = False
127127+128128+ @property
129129+ def root(self) -> TocEntry:
130130+ return self.parent.root if self.parent else self
131131+132132+ @classmethod
133133+ def of(cls, token: Token) -> TocEntry:
134134+ entry = token.meta.get('TocEntry')
135135+ if not isinstance(entry, TocEntry):
136136+ raise RuntimeError('requested toc entry, none found', token)
137137+ return entry
138138+139139+ @classmethod
140140+ def collect_and_link(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token]) -> TocEntry:
141141+ result = cls._collect_entries(xrefs, tokens, 'book')
142142+143143+ def flatten_with_parent(this: TocEntry, parent: TocEntry | None) -> Iterable[TocEntry]:
144144+ this.parent = parent
145145+ return itertools.chain([this], *[ flatten_with_parent(c, this) for c in this.children ])
146146+147147+ flat = list(flatten_with_parent(result, None))
148148+ prev = flat[0]
149149+ prev.starts_new_chunk = True
150150+ paths_seen = set([prev.target.path])
151151+ for c in flat[1:]:
152152+ if prev.target.path != c.target.path and c.target.path not in paths_seen:
153153+ c.starts_new_chunk = True
154154+ c.prev, prev.next = prev, c
155155+ prev = c
156156+ paths_seen.add(c.target.path)
157157+158158+ for c in flat:
159159+ c.freeze()
160160+161161+ return result
162162+163163+ @classmethod
164164+ def _collect_entries(cls, xrefs: dict[str, XrefTarget], tokens: Sequence[Token],
165165+ kind: TocEntryType) -> TocEntry:
166166+ # we assume that check_structure has been run recursively over the entire input.
167167+ # list contains (tag, entry) pairs that will collapse to a single entry for
168168+ # the full sequence.
169169+ entries: list[tuple[str, TocEntry]] = []
170170+ for token in tokens:
171171+ if token.type.startswith('included_') and (included := token.meta.get('included')):
172172+ fragment_type_str = token.type[9:].removesuffix('s')
173173+ assert fragment_type_str in get_args(TocEntryType)
174174+ fragment_type = cast(TocEntryType, fragment_type_str)
175175+ for fragment, _path in included:
176176+ entries[-1][1].children.append(cls._collect_entries(xrefs, fragment, fragment_type))
177177+ elif token.type == 'heading_open' and (id := cast(str, token.attrs.get('id', ''))):
178178+ while len(entries) > 1 and entries[-1][0] >= token.tag:
179179+ entries[-2][1].children.append(entries.pop()[1])
180180+ entries.append((token.tag,
181181+ TocEntry(kind if token.tag == 'h1' else 'section', xrefs[id])))
182182+ token.meta['TocEntry'] = entries[-1][1]
183183+184184+ while len(entries) > 1:
185185+ entries[-2][1].children.append(entries.pop()[1])
186186+ return entries[0][1]
···11+from typing import Any
22+33+_frozen_classes: dict[type, type] = {}
44+55+# make a derived class freezable (ie, disallow modifications).
66+# we do this by changing the class of an instance at runtime when freeze()
77+# is called, providing a derived class that is exactly the same except
88+# for a __setattr__ that raises an error when called. this beats having
99+# a field for frozenness and an unconditional __setattr__ that checks this
1010+# field because it does not insert anything into the class dict.
1111+class Freezeable:
1212+ def freeze(self) -> None:
1313+ cls = type(self)
1414+ if not (frozen := _frozen_classes.get(cls)):
1515+ def __setattr__(instance: Any, n: str, v: Any) -> None:
1616+ raise TypeError(f'{cls.__name__} is frozen')
1717+ frozen = type(cls.__name__, (cls,), {
1818+ '__setattr__': __setattr__,
1919+ })
2020+ _frozen_classes[cls] = frozen
2121+ self.__class__ = frozen
···11-import nixos_render_docs
11+import nixos_render_docs as nrd
2233from sample_md import sample1
44···6677import markdown_it
8899-class Converter(nixos_render_docs.md.Converter):
1010- __renderer__ = nixos_render_docs.commonmark.CommonMarkRenderer
99+class Converter(nrd.md.Converter[nrd.commonmark.CommonMarkRenderer]):
1010+ def __init__(self, manpage_urls: Mapping[str, str]):
1111+ super().__init__()
1212+ self._renderer = nrd.commonmark.CommonMarkRenderer(manpage_urls)
11131214# NOTE: in these tests we represent trailing spaces by ` ` and replace them with real space later,
1315# since a number of editors will strip trailing whitespace on save and that would break the tests.