Skip to content

Commit

Permalink
nixos-option: rewrite as a nix script, 2nd try (#369151)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mic92 authored Jan 7, 2025
2 parents 003867e + 8710aa0 commit 5634f52
Show file tree
Hide file tree
Showing 13 changed files with 488 additions and 845 deletions.
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2505.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
- `nixos-rebuild-ng`, a full rewrite of `nixos-rebuild` in Python, is available for testing. You can enable it by setting [system.rebuild.enableNg](options.html#opt-system.rebuild.enableNg) in your configuration (this will replace the old `nixos-rebuild`), or by adding `nixos-rebuild-ng` to your `environment.systemPackages` (in this case, it will live side-by-side with `nixos-rebuild` as `nixos-rebuild-ng`). It is expected that the next major version of NixOS (25.11) will enable `system.rebuild.enableNg` by default.
- A `nixos-rebuild build-image` sub-command has been added.

- `nixos-option` has been rewritten to a Nix expression called by a simple bash script. This lowers our maintenance threshold, makes eval errors less verbose, adds support for flake-based configurations, descending into `attrsOf` and `listOf` submodule options, and `--show-trace`.

It allows users to build platform-specific (disk) images from their NixOS configurations. `nixos-rebuild build-image` works similar to the popular [nix-community/nixos-generators](https://github.com/nix-community/nixos-generators) project. See new [section on image building in the nixpkgs manual](https://nixos.org/manual/nixpkgs/unstable/#sec-image-nixos-rebuild-build-image).

<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
Expand Down
133 changes: 133 additions & 0 deletions pkgs/by-name/ni/nixos-option/nixos-option.8
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
.Dd January 1, 1980
.Dt nixos-option 8
.Os
.Sh NAME
.Nm nixos-option
.Nd inspect a NixOS configuration
.
.
.
.Sh SYNOPSIS
.Nm
.Op Fl r | -recursive
.Op Fl I Ar path
.Op Fl F | -flake Ar flake-uri
.br
.Op Fl -no-flake
.Op Fl -show-trace
.Ar option.name
.
.
.
.Sh DESCRIPTION
This command evaluates the configuration specified in
.Ev NIXOS_CONFIG Ns
,
.Pa nixos-config
in
.Ev NIX_PATH
(which by default is
.Pa /etc/nixos/configuration.nix Ns
),
.Pa /etc/nixos/flake.nix
or the file and attribute specified by the
.Fl I
or
.Fl -flake
parameter, and returns the properties of the option name given as argument.
.
.Pp
When the option name is not an option but an attribute set, the command prints
the list of attributes contained in it. When no option name is given, the
command prints all top-level attributes in given NixOS configuration.
.
.
.
.Sh OPTIONS
.Bl -tag -width indent
.It Fl r , -recursive
Print all the values at or below the specified path recursively.
.
.It Fl I Ar path
Add an entry to the Nix expression search path. This option is passed to the
underlying
.Xr nix-instantiate 1
invocation.
.
.It Fl -show-trace
Print eval trace. This option is passed to the underlying
.Xr nix-instantiate 1
invocation.
.
.It Fl F , -flake Ar flake-uri
Specify the flake containing NixOS configuration. It defaults to
.Pa /etc/nixos/flake.nix Ns
, if the flake exists, it must contain an output named
.Ql nixosConfigurations. Ns Va name Ns
\&. If
.Va name
is omitted, it defaults to the current host name.
.
.It Fl -no-flake
Do not imply
.Fl -flake
if
.Pa /etc/nixos/flake.nix
exists. With this option, it is possible to show options in non-flake NixOS
configurations even if the current NixOS systems uses flakes.
.
.El
.
.
.
.Sh Ev ENVIRONMENT
.Bl -tag -width indent
.It Ev NIXOS_CONFIG
Path to the main NixOS configuration module. Defaults to
.Pa /etc/nixos/configuration.nix Ns
\&.
.El
.
.
.
.Sh EXAMPLES
Investigate option values:
.Bd -literal -offset indent
$ nixos-option boot.loader
This attribute set contains:
generationsDir
grub
initScript

$ nixos-option boot.loader.grub.enable
Value:
true

Default:
true

Type:
boolean

Description:
Whether to enable the GNU GRUB boot loader.

Declared by:
/nix/var/nix/profiles/per-user/root/channels/nixos/nixpkgs/nixos/modules/system/boot/loader/grub/grub.nix

Defined by:
/nix/var/nix/profiles/per-user/root/channels/nixos/nixpkgs/nixos/modules/system/boot/loader/grub/grub.nix
.Ed
.
.
.
.Sh SEE ALSO
.Xr configuration.nix 5
.
.
.
.Sh AUTHORS
.An -nosplit
.An Nicolas Pierron
and
.An the Nixpkgs/NixOS contributors
167 changes: 167 additions & 0 deletions pkgs/by-name/ni/nixos-option/nixos-option.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
{
nixos,
# list representing a nixos option path (e.g. ['console' 'enable']), or a
# prefix of such a path (e.g. ['console']), or a string representing the same
# (e.g. 'console.enable')
path,
# whether to recurse down the config attrset and show each set value instead
recursive,
}:

let
inherit (nixos.pkgs) lib;

path' = if lib.isString path then (if path == "" then [ ] else readOption path) else path;

# helper that maps `f` on subslices starting when `predStart x` and
# ending when `predEnd x` (no support for nested occurrences)
flatMapSlices =
predStart: predEnd: f: list:
let
empty = {
result = [ ];
active = [ ];
};
op =
{ result, active }:
x:
if predStart x && predEnd x then
{
result = result ++ active ++ f [ x ];
active = [ ];
}
else if predStart x then
{
result = result ++ active;
active = [ x ];
}
else if predEnd x then
{
result = result ++ f (active ++ [ x ]);
active = [ ];
}
else
{
inherit result;
active = active ++ [ x ];
};
in
(x: x.result ++ x.active) (lib.foldl op empty list);

# tries to invert showOption, taking a written-out option name and splitting
# it into its parts
readOption =
str:
let
unescape = list: lib.replaceStrings (map (c: "\\${c}") list) list;
unescapeNixString = lib.flip lib.pipe [
(lib.concatStringsSep ".")
(unescape [ "$" ])
builtins.fromJSON
];
in
flatMapSlices (lib.hasPrefix "\"") (lib.hasSuffix "\"") (x: [ (unescapeNixString x) ]) (
lib.splitString "." str
);

# like 'mapAttrsRecursiveCond' but handling errors in the attrset tree as leaf
# nodes (which means `f` is expected to handle shallow errors)
safeMapAttrsRecursiveCond =
cond: f: set:
let
recurse =
path:
lib.mapAttrs (
name: value:
let
e = builtins.tryEval value;
path' = path ++ [ name ];
in
if e.success && lib.isAttrs value && cond value then recurse path' value else f path' value
);
in
recurse [ ] set;

# traverse the option tree along `path` from `root`, returning the option or
# attrset at the given location
optionByPath =
path: root:
let
into =
opt: part:
if lib.isOption opt && opt.type.descriptionClass == "composite" then
opt.type.getSubOptions [ ]
else if lib.isOption opt then
throw "Trying to access '${part}' inside ${opt.type.name} option while traversing option path '${lib.showOption path}'"
else if lib.isAttrs opt && lib.hasAttr part opt then
opt.${part}
else
throw "Found neither an attrset nor supported option type near '${part}' while traversing option path '${lib.showOption path}'";
in
lib.foldl into root path;

toPretty = lib.generators.toPretty { multiline = true; };
safeToPretty =
x:
let
e = builtins.tryEval (toPretty x);
in
if e.success then e.value else "«error»";

indent = str: lib.concatStringsSep "\n" (map (x: " " + x) (lib.splitString "\n" str));

optionAttrNames = attrs: lib.filter (x: x != "_module") (lib.attrNames attrs);

## full, non-recursive mode: print an option from `options`
renderAttrs =
attrs: "This attribute set contains:\n${lib.concatStringsSep "\n" (optionAttrNames attrs)}";

renderOption =
option: value:
let
entry =
cond: heading: value:
lib.optional cond "${heading}:\n${indent value}";
in
lib.concatStringsSep "\n\n" (
lib.concatLists [
(entry true "Value" (toPretty value))
(entry (option ? default) "Default" (toPretty option.default))
(entry (option ? type) "Type" (option.type.description))
(entry (option ? description) "Description" (lib.removeSuffix "\n" option.description))
(entry (option ? example) "Example" (toPretty option.example))
(entry (option ? declarations) "Declared by" (lib.concatStringsSep "\n" option.declarations))
(entry (option ? files) "Defined by" (lib.concatStringsSep "\n" option.files))
]
);

renderFull =
entry: configEntry:
if lib.isOption entry then
renderOption entry configEntry
else if lib.isAttrs entry then
renderAttrs entry
else
throw "Found neither an attrset nor option at option path '${lib.showOption path'}'";

## recursive mode: print paths and values from `config`
renderRecursive =
config:
let
renderShort = n: v: "${lib.showOption (path' ++ n)} = ${safeToPretty v};";
mapAttrsRecursive' = safeMapAttrsRecursiveCond (x: !lib.isDerivation x);
in
if lib.isAttrs config then
lib.concatStringsSep "\n" (lib.collect lib.isString (mapAttrsRecursive' renderShort config))
else
renderShort [ ] config;

in
if !lib.hasAttrByPath path' nixos.config then
throw "Couldn't resolve config path '${lib.showOption path'}'"
else
let
optionEntry = optionByPath path' nixos.options;
configEntry = lib.attrByPath path' null nixos.config;
in
if recursive then renderRecursive configEntry else renderFull optionEntry configEntry
Loading

0 comments on commit 5634f52

Please sign in to comment.