Unverified Commit 5634f525 authored by Jörg Thalheim's avatar Jörg Thalheim Committed by GitHub
Browse files

nixos-option: rewrite as a nix script, 2nd try (#369151)

parents 003867e8 8710aa0a
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -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. -->
+133 −0
Original line number Diff line number Diff line
@@ -11,18 +11,35 @@
.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
.Pa /etc/nixos/configuration.nix
and returns the properties of the option name given as argument.
.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, the command prints the list of attributes
contained in the attribute set.
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.
.
.
.
@@ -32,14 +49,38 @@ contained in the attribute set.
Print all the values at or below the specified path recursively.
.
.It Fl I Ar path
This option is passed to the underlying
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 ENVIRONMENT
.Sh Ev ENVIRONMENT
.Bl -tag -width indent
.It Ev NIXOS_CONFIG
Path to the main NixOS configuration module. Defaults to
@@ -65,14 +106,17 @@ 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"
  /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"
  /nix/var/nix/profiles/per-user/root/channels/nixos/nixpkgs/nixos/modules/system/boot/loader/grub/grub.nix
.Ed
.
.
+167 −0
Original line number Diff line number Diff line
{
  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
+106 −0
Original line number Diff line number Diff line
#!/bin/bash
# shellcheck shell=bash
set -eou pipefail

recursive=false
no_flake=false
positional_args=()
nix_args=(--arg nixos "import <nixpkgs/nixos> { }")
flake=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --help)
      exec man -M @nixosOptionManpage@ 8 nixos-option
      ;;

    -r|--recursive)
      recursive=true
      shift
      ;;

    -I)
      nix_args+=("$1" "$2")
      # Not breaking existing usages of it
      no_flake=true
      shift 2
      ;;

    -F|--flake)
      flake=$2
      shift 2
      ;;

    --no-flake)
      no_flake=true
      shift
      ;;

    --show-trace)
      nix_args+=("$1")
      shift
      ;;

    -*)
      echo >&2 "Unsupported option $1"
      exit 1
      ;;

    *)
      positional_args+=("$1")
      shift
      ;;
  esac
done

# Detection order, from high to low:
# `--flake`
# $NIXOS_CONFIG
# nixos-config=<path> in NIX_PATH (normally /etc/nixos/configuration.nix)
# `-I` (implies `--no-flake`)
# `--no-flake`
# /etc/nixos/flake.nix (if exists)

if [[ -z "$flake" ]] && [[ -e /etc/nixos/flake.nix ]] && [[ "$no_flake" == "false" ]]; then
  flake="$(dirname "$(realpath /etc/nixos/flake.nix)")"
fi

if [[ -n "$flake" ]]; then
  echo >&2 "[WARN] Flake support in nixos-option is experimental and has known issues."

  if [[ $flake =~ ^(.*)\#([^\#\"]*)$ ]]; then
    flake="${BASH_REMATCH[1]}"
    flakeAttr="${BASH_REMATCH[2]}"
  fi
  # Unlike nix cli, builtins.getFlake infer path:// when a path is given
  # See https://github.com/NixOS/nix/issues/5836
  # Using `git rev-parse --show-toplevel` since we can't assume the flake dir
  # itself is a git repo, because the flake could be in a sub directory
  if [[ -d "$flake" ]] && git -C "$flake" rev-parse --show-toplevel &>/dev/null; then
    flake="git+file://$(realpath "$flake")"
  fi
  if [[ -z "${flakeAttr:-}" ]]; then
    hostname=$(< /proc/sys/kernel/hostname)
    if [[ -z "${hostname:-}" ]]; then
      hostname=default
    fi
    flakeAttr="nixosConfigurations.\"$hostname\""
  else
    flakeAttr="nixosConfigurations.\"$flakeAttr\""
  fi
  nix_args+=(--arg nixos "(builtins.getFlake \"$flake\").$flakeAttr")
fi

case ${#positional_args[@]} in
  0) path= ;;
  # Remove trailing dot if exists, match the behavior of
  # old nixos-option and make shell completions happy
  1) path="${positional_args[0]%.}" ;;
  *) echo >&2 "Only one option path can be provided"; exit 1 ;;
esac

nix-instantiate "${nix_args[@]}" --eval --json \
    --argstr path "$path" \
    --arg recursive "$recursive" \
    @nixosOptionNix@ \
| jq -r
+80 −0
Original line number Diff line number Diff line
{
  lib,
  coreutils,
  git,
  installShellFiles,
  jq,
  makeWrapper,
  man-db,
  nix,
  nixosTests,
  shellcheck,
  stdenvNoCC,
}:

stdenvNoCC.mkDerivation {
  name = "nixos-option";

  src = ./nixos-option.sh;

  nativeBuildInputs = [
    installShellFiles
    makeWrapper
    shellcheck
  ];

  env = {
    nixosOptionNix = "${./nixos-option.nix}";
    nixosOptionManpage = "${placeholder "out"}/share/man";
  };

  dontUnpack = true;
  dontConfigure = true;
  dontPatch = true;
  dontBuild = true;

  installPhase = ''
    runHook preInstall

    install -Dm555 $src $out/bin/nixos-option
    substituteAllInPlace $out/bin/nixos-option
    installManPage ${./nixos-option.8}

    runHook postInstall
  '';

  doInstallCheck = true;
  installCheckPhase = ''
    runHook preInstallCheck

    shellcheck $out/bin/nixos-option

    runHook postInstallCheck
  '';

  postFixup = ''
    wrapProgram $out/bin/nixos-option \
      --prefix PATH : ${
        lib.makeBinPath [
          coreutils
          git
          jq
          man-db
          nix
        ]
      }
  '';

  passthru.tests.installer-simpleUefiSystemdBoot = nixosTests.installer.simpleUefiSystemdBoot;

  meta = {
    description = "Evaluate NixOS configuration and return the properties of given option";
    license = lib.licenses.mit;
    mainProgram = "nixos-option";
    maintainers = with lib.maintainers; [
      FireyFly
      azuwis
      aleksana
    ];
  };
}
Loading