From 0a71cf07a82eacd0a618fe7ba7016f7de5f5c2f5 Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Wed, 20 Sep 2023 22:16:03 +0200 Subject: [PATCH 1/5] nixos/rust-motd: run once on bootup That way e.g. the last login and uptime isn't completely bogus when accessing a machine for the first time after a reboot. --- nixos/modules/programs/rust-motd.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nixos/modules/programs/rust-motd.nix b/nixos/modules/programs/rust-motd.nix index d5f1820ba752..7fd89e1658ef 100644 --- a/nixos/modules/programs/rust-motd.nix +++ b/nixos/modules/programs/rust-motd.nix @@ -50,6 +50,7 @@ in { path = with pkgs; [ bash ]; documentation = [ "https://github.com/rust-motd/rust-motd/blob/v${pkgs.rust-motd.version}/README.md" ]; description = "motd generator"; + wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = "${pkgs.writeShellScript "update-motd" '' ${pkgs.rust-motd}/bin/rust-motd ${format.generate "motd.conf" cfg.settings} > motd From 11376df6d40c8fdbbd3c9a048b742dc032a88d15 Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Thu, 21 Sep 2023 00:05:35 +0200 Subject: [PATCH 2/5] nixos/rust-motd: allow ordering sections by `priority` Closes #234802 The problem here is that with e.g. { uptime.prefix = "Up"; banner.command = "hostname | figlet -f slant"; } `banner` still appears before `uptime` in the final motd text because Nix sorts attribute names alphabetically internally. To work around this without breaking compatibility or losing the property to override individual sections in other modules - e.g. { banner.color = mkForce "blue"; } I decided to introduce an option `priority` here, similar to the priority field for `nginx`[1] and with the same semantics (i.e. higher value means lower priority). Internally a bunch of env vars are generated, i.e. `env0` to `envN` for `N` sections with each of them containing a declaration for the TOML, i.e. `env0` contains `{ uptime.prefix = "Up"; }` and `env1` contains `{ banner.command = "hostname | figlet -f slant"; }` if `uptime.priority` is set to a value below 1000. In this order, the declarations are concatenated together by `jq(1)` which doesn't sort keys alphabetically which results in a JSON representation with `uptime` before `banner`. This is finally piped to `json2toml` which converts this into TOML for rust-motd. [1] https://nixos.org/manual/nixos/unstable/options#opt-services.nginx.virtualHosts._name_.locations._name_.priority --- nixos/modules/programs/rust-motd.nix | 59 ++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/nixos/modules/programs/rust-motd.nix b/nixos/modules/programs/rust-motd.nix index 7fd89e1658ef..3e3ce63ab212 100644 --- a/nixos/modules/programs/rust-motd.nix +++ b/nixos/modules/programs/rust-motd.nix @@ -5,6 +5,33 @@ with lib; let cfg = config.programs.rust-motd; format = pkgs.formats.toml { }; + + orderedSections = listToAttrs + (imap0 + (i: items@{ sectionHeader, ... }: nameValuePair "env${toString i}" { + ${sectionHeader} = removeAttrs items [ "priority" "sectionHeader" ]; + }) + (sortProperties (mapAttrsToList (k: v: v // { sectionHeader = k; }) cfg.settings))); + + # Order the sections in the TOML according to the `priority` field. + # This is done by + # * creating an attribute set with keys `env0`/`env1`/.../`envN` + # where `env0` contains the first section and `envN` the last + # (in the form of `{ sectionName = { /* ... */ }}`) + # * the declarations of `env0` to `envN` in ascending order are + # concatenated with `jq`. Now we have a JSON representation of + # the config in the correct order. + # * this is piped to `json2toml` to get the correct format for rust-motd. + motdConf = pkgs.runCommand "motd.conf" + (orderedSections // { + __structuredAttrs = true; + nativeBuildInputs = [ pkgs.remarshal pkgs.jq ]; + }) + '' + cat "$NIX_BUILD_TOP"/.attrs.json \ + | jq '${concatMapStringsSep " + " (key: ''."${key}"'') (attrNames orderedSections)}' \ + | json2toml /dev/stdin "$out" + ''; in { options.programs.rust-motd = { enable = mkEnableOption (lib.mdDoc "rust-motd"); @@ -28,9 +55,35 @@ in { ''; }; settings = mkOption { - type = types.submodule { + type = types.attrsOf (types.submodule { freeformType = format.type; - }; + options.priority = mkOption { + type = types.int; + default = modules.defaultOrderPriority; + description = mdDoc '' + In `rust-motd`, the order of the sections in TOML correlates to the order + of the items displayed in the resulting `motd`. Attributes in Nix are + ordered alphabetically, e.g. `banner` would always be before `uptime`. + + To change that, this option can be used. The lower this number is, the higher + is the priority and the more a section is at the top of the message. + + For instance + + ```nix + { + banner.command = "hostname | figlet -f slant"; + uptime = { + prefix = "Up"; + priority = 0; + }; + } + ``` + + would make the `uptime` appear before the banner. + ''; + }; + }); description = mdDoc '' Settings on what to generate. Please read the [upstream documentation](https://github.com/rust-motd/rust-motd/blob/main/README.md#configuration) @@ -53,7 +106,7 @@ in { wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = "${pkgs.writeShellScript "update-motd" '' - ${pkgs.rust-motd}/bin/rust-motd ${format.generate "motd.conf" cfg.settings} > motd + ${pkgs.rust-motd}/bin/rust-motd ${motdConf} > motd ''}"; CapabilityBoundingSet = [ "" ]; LockPersonality = true; From 214cf0b9343dc619a36aedd2320bb1d73915c6de Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Thu, 21 Sep 2023 01:09:32 +0200 Subject: [PATCH 3/5] nixos/rust-motd: .attrs.json -> "$NIX_ATTRS_JSON_FILE" That way the derivation can also be built in a `nix-shell` where `.attrs.json` isn't under "$NIX_BUILD_TOP". --- nixos/modules/programs/rust-motd.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/modules/programs/rust-motd.nix b/nixos/modules/programs/rust-motd.nix index 3e3ce63ab212..8999e882d01a 100644 --- a/nixos/modules/programs/rust-motd.nix +++ b/nixos/modules/programs/rust-motd.nix @@ -28,7 +28,7 @@ let nativeBuildInputs = [ pkgs.remarshal pkgs.jq ]; }) '' - cat "$NIX_BUILD_TOP"/.attrs.json \ + cat "$NIX_ATTRS_JSON_FILE" \ | jq '${concatMapStringsSep " + " (key: ''."${key}"'') (attrNames orderedSections)}' \ | json2toml /dev/stdin "$out" ''; From d77b59b41d038ad9c3878d8ccb2a35f87d178acb Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Tue, 26 Sep 2023 23:28:40 +0200 Subject: [PATCH 4/5] nixos/rust-motd: use a second attribute (`order`) for the of sections in TOML Rather than using `priority` with `sortProperties`, a new option called `order` defines the ordering of the sections. I.e. order = [ "global" "uptime" "banner" ] means that `uptime` comes before `banner`. Please note that `global` is for global settings and not a section. I figured that it'd be too much magic to hide this in the implementation and ask the user to specify the order of _each_ section in `settings` instead. OTOH this makes the intent way clearer than priorities. Also, this remains opt-in, the option defaults to `attrNames cfg.settings`, i.e. all sections ordered alphabetically. --- nixos/modules/programs/rust-motd.nix | 75 +++++++++++++++++----------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/nixos/modules/programs/rust-motd.nix b/nixos/modules/programs/rust-motd.nix index 8999e882d01a..e37d72f392f4 100644 --- a/nixos/modules/programs/rust-motd.nix +++ b/nixos/modules/programs/rust-motd.nix @@ -8,12 +8,13 @@ let orderedSections = listToAttrs (imap0 - (i: items@{ sectionHeader, ... }: nameValuePair "env${toString i}" { - ${sectionHeader} = removeAttrs items [ "priority" "sectionHeader" ]; + (i: attr: nameValuePair "env${toString i}" { + ${attr} = cfg.settings.${attr}; }) - (sortProperties (mapAttrsToList (k: v: v // { sectionHeader = k; }) cfg.settings))); + cfg.order); - # Order the sections in the TOML according to the `priority` field. + # Order the sections in the TOML according to the order of sections + # in `cfg.order`. # This is done by # * creating an attribute set with keys `env0`/`env1`/.../`envN` # where `env0` contains the first section and `envN` the last @@ -54,35 +55,42 @@ in { For possible formats, please refer to {manpage}`systemd.time(7)`. ''; }; + order = mkOption { + type = types.listOf types.str; + default = attrNames cfg.settings; + defaultText = literalExpression "attrNames cfg.settings"; + description = mdDoc '' + The order of the sections in [](#opt-programs.rust-motd.settings) implies + the order of sections in the motd. Since attribute sets in Nix are always + ordered alphabetically internally this means that + + ```nix + { + uptime = { /* ... */ }; + banner = { /* ... */ }; + } + ``` + + will still have `banner` displayed before `uptime`. + + To work around that, this option can be used to define the order of all keys, + i.e. + + ```nix + { + order = [ + "uptime" + "banner" + ]; + } + ``` + + makes sure that `uptime` is placed before `banner` in the motd. + ''; + }; settings = mkOption { type = types.attrsOf (types.submodule { freeformType = format.type; - options.priority = mkOption { - type = types.int; - default = modules.defaultOrderPriority; - description = mdDoc '' - In `rust-motd`, the order of the sections in TOML correlates to the order - of the items displayed in the resulting `motd`. Attributes in Nix are - ordered alphabetically, e.g. `banner` would always be before `uptime`. - - To change that, this option can be used. The lower this number is, the higher - is the priority and the more a section is at the top of the message. - - For instance - - ```nix - { - banner.command = "hostname | figlet -f slant"; - uptime = { - prefix = "Up"; - priority = 0; - }; - } - ``` - - would make the `uptime` appear before the banner. - ''; - }; }); description = mdDoc '' Settings on what to generate. Please read the @@ -98,6 +106,13 @@ in { `programs.rust-motd` is incompatible with `users.motd`! ''; } + { assertion = length cfg.order == length (attrNames cfg.settings) + && all (section: cfg.settings?${section}) cfg.order; + message = '' + Please ensure that every section from `programs.rust-motd.settings` is present in + `programs.rust-motd.order`. + ''; + } ]; systemd.services.rust-motd = { path = with pkgs; [ bash ]; From 57de6a855001eaf849fff1f25859e5a1ddd39ddc Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Wed, 27 Sep 2023 12:45:41 +0200 Subject: [PATCH 5/5] nixos/rust-motd: refactor assertion and TOML generation * `sort (<)` also works for strings (TIL!), so no need for comparing length and whether all keys from `cfg.settings` exist in `cfg.order` (slightly less overhead). * Don't build another piece of JSON (`orderedSections`), simply use `cfg.settings`/`cfg.order` with `__structuredAttrs` to ensure a properly ordered TOML. This also has the upside of not having to do quote hackery. * Also, a freeform submodule isn't strictly needed because we don't have any special options defined, so replacing that with `attrsOf format.type`. Co-authored-by: Silvan Mosberger --- nixos/modules/programs/rust-motd.nix | 38 ++++++++++------------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/nixos/modules/programs/rust-motd.nix b/nixos/modules/programs/rust-motd.nix index e37d72f392f4..4c9b1018596b 100644 --- a/nixos/modules/programs/rust-motd.nix +++ b/nixos/modules/programs/rust-motd.nix @@ -6,31 +6,20 @@ let cfg = config.programs.rust-motd; format = pkgs.formats.toml { }; - orderedSections = listToAttrs - (imap0 - (i: attr: nameValuePair "env${toString i}" { - ${attr} = cfg.settings.${attr}; - }) - cfg.order); - # Order the sections in the TOML according to the order of sections # in `cfg.order`. - # This is done by - # * creating an attribute set with keys `env0`/`env1`/.../`envN` - # where `env0` contains the first section and `envN` the last - # (in the form of `{ sectionName = { /* ... */ }}`) - # * the declarations of `env0` to `envN` in ascending order are - # concatenated with `jq`. Now we have a JSON representation of - # the config in the correct order. - # * this is piped to `json2toml` to get the correct format for rust-motd. motdConf = pkgs.runCommand "motd.conf" - (orderedSections // { + { __structuredAttrs = true; + inherit (cfg) order settings; nativeBuildInputs = [ pkgs.remarshal pkgs.jq ]; - }) + } '' cat "$NIX_ATTRS_JSON_FILE" \ - | jq '${concatMapStringsSep " + " (key: ''."${key}"'') (attrNames orderedSections)}' \ + | jq '.settings as $settings + | .order + | map({ key: ., value: $settings."\(.)" }) + | from_entries' -r \ | json2toml /dev/stdin "$out" ''; in { @@ -60,8 +49,10 @@ in { default = attrNames cfg.settings; defaultText = literalExpression "attrNames cfg.settings"; description = mdDoc '' - The order of the sections in [](#opt-programs.rust-motd.settings) implies - the order of sections in the motd. Since attribute sets in Nix are always + The order of the sections in [](#opt-programs.rust-motd.settings). + By default they are ordered alphabetically. + + Context: since attribute sets in Nix are always ordered alphabetically internally this means that ```nix @@ -89,9 +80,7 @@ in { ''; }; settings = mkOption { - type = types.attrsOf (types.submodule { - freeformType = format.type; - }); + type = types.attrsOf format.type; description = mdDoc '' Settings on what to generate. Please read the [upstream documentation](https://github.com/rust-motd/rust-motd/blob/main/README.md#configuration) @@ -106,8 +95,7 @@ in { `programs.rust-motd` is incompatible with `users.motd`! ''; } - { assertion = length cfg.order == length (attrNames cfg.settings) - && all (section: cfg.settings?${section}) cfg.order; + { assertion = sort (a: b: a < b) cfg.order == attrNames cfg.settings; message = '' Please ensure that every section from `programs.rust-motd.settings` is present in `programs.rust-motd.order`.