about summary refs log tree commit diff
path: root/nixos/modules/installer
diff options
context:
space:
mode:
authorChuck <chuck@intelligence.org>2019-09-05 17:29:01 -0700
committerLinus Heckemann <git@sphalerite.org>2019-11-04 15:11:44 +0100
commit59c5bfc86b75247cb48539eeaaea2a3c5f320b1d (patch)
tree53d224e3a4003942d0c366955afa26a98875d048 /nixos/modules/installer
parentd690c20efd07b422c68529b8719454ee74f2f1df (diff)
downloadnixlib-59c5bfc86b75247cb48539eeaaea2a3c5f320b1d.tar
nixlib-59c5bfc86b75247cb48539eeaaea2a3c5f320b1d.tar.gz
nixlib-59c5bfc86b75247cb48539eeaaea2a3c5f320b1d.tar.bz2
nixlib-59c5bfc86b75247cb48539eeaaea2a3c5f320b1d.tar.lz
nixlib-59c5bfc86b75247cb48539eeaaea2a3c5f320b1d.tar.xz
nixlib-59c5bfc86b75247cb48539eeaaea2a3c5f320b1d.tar.zst
nixlib-59c5bfc86b75247cb48539eeaaea2a3c5f320b1d.zip
nixos/nixos-option: Rewrite in a more suitable language
Also add --all, which shows the value of all options.  Diffing the --all
output on either side of contemplated changes is a lovely way to better
understand what's going on inside nixos.
Diffstat (limited to 'nixos/modules/installer')
-rw-r--r--nixos/modules/installer/tools/nixos-option.sh327
-rw-r--r--nixos/modules/installer/tools/nixos-option/CMakeLists.txt8
-rw-r--r--nixos/modules/installer/tools/nixos-option/default.nix8
-rw-r--r--nixos/modules/installer/tools/nixos-option/libnix-copy-paste.cc81
-rw-r--r--nixos/modules/installer/tools/nixos-option/libnix-copy-paste.hh9
-rw-r--r--nixos/modules/installer/tools/nixos-option/nixos-option.cc585
-rw-r--r--nixos/modules/installer/tools/tools.nix5
7 files changed, 692 insertions, 331 deletions
diff --git a/nixos/modules/installer/tools/nixos-option.sh b/nixos/modules/installer/tools/nixos-option.sh
deleted file mode 100644
index 4560e9c7403a..000000000000
--- a/nixos/modules/installer/tools/nixos-option.sh
+++ /dev/null
@@ -1,327 +0,0 @@
-#! @shell@ -e
-
-# FIXME: rewrite this in a more suitable language.
-
-usage () {
-    exec man nixos-option
-    exit 1
-}
-
-#####################
-# Process Arguments #
-#####################
-
-xml=false
-verbose=false
-nixPath=""
-
-option=""
-exit_code=0
-
-argfun=""
-for arg; do
-  if test -z "$argfun"; then
-    case $arg in
-      -*)
-        sarg="$arg"
-        longarg=""
-        while test "$sarg" != "-"; do
-          case $sarg in
-            --*) longarg=$arg; sarg="--";;
-            -I) argfun="include_nixpath";;
-            -*) usage;;
-          esac
-          # remove the first letter option
-          sarg="-${sarg#??}"
-        done
-        ;;
-      *) longarg=$arg;;
-    esac
-    for larg in $longarg; do
-      case $larg in
-        --xml) xml=true;;
-        --verbose) verbose=true;;
-        --help) usage;;
-        -*) usage;;
-        *) if test -z "$option"; then
-             option="$larg"
-           else
-             usage
-           fi;;
-      esac
-    done
-  else
-    case $argfun in
-      set_*)
-        var=$(echo $argfun | sed 's,^set_,,')
-        eval $var=$arg
-        ;;
-      include_nixpath)
-        nixPath="-I $arg $nixPath"
-        ;;
-    esac
-    argfun=""
-  fi
-done
-
-if $verbose; then
-  set -x
-else
-  set +x
-fi
-
-#############################
-# Process the configuration #
-#############################
-
-evalNix(){
-  # disable `-e` flag, it's possible that the evaluation of `nix-instantiate` fails (e.g. due to broken pkgs)
-  set +e
-  result=$(nix-instantiate ${nixPath:+$nixPath} - --eval-only "$@" 2>&1)
-  exit_code=$?
-  set -e
-
-  if test $exit_code -eq 0; then
-      sed '/^warning: Nix search path/d' <<EOF
-$result
-EOF
-      return 0;
-  else
-      sed -n '
-  /^error/ { s/, at (string):[0-9]*:[0-9]*//; p; };
-  /^warning: Nix search path/ { p; };
-' >&2 <<EOF
-$result
-EOF
-    exit_code=1
-  fi
-}
-
-header="let
-  nixos = import <nixpkgs/nixos> {};
-  nixpkgs = import <nixpkgs> {};
-in with nixpkgs.lib;
-"
-
-# This function is used for converting the option definition path given by
-# the user into accessors for reaching the definition and the declaration
-# corresponding to this option.
-generateAccessors(){
-  if result=$(evalNix --strict --show-trace <<EOF
-$header
-
-let
-  path = "${option:+$option}";
-  pathList = splitString "." path;
-
-  walkOptions = attrsNames: result:
-    if attrsNames == [] then
-      result
-    else
-      let name = head attrsNames; rest = tail attrsNames; in
-      if isOption result.options then
-        walkOptions rest {
-          options = result.options.type.getSubOptions "";
-          opt = ''(\${result.opt}.type.getSubOptions "")'';
-          cfg = ''\${result.cfg}."\${name}"'';
-        }
-      else
-        walkOptions rest {
-          options = result.options.\${name};
-          opt = ''\${result.opt}."\${name}"'';
-          cfg = ''\${result.cfg}."\${name}"'';
-        }
-    ;
-
-  walkResult = (if path == "" then x: x else walkOptions pathList) {
-    options = nixos.options;
-    opt = ''nixos.options'';
-    cfg = ''nixos.config'';
-  };
-
-in
-  ''let option = \${walkResult.opt}; config = \${walkResult.cfg}; in''
-EOF
-)
-  then
-      echo $result
-  else
-      # In case of error we want to ignore the error message roduced by the
-      # script above, as it is iterating over each attribute, which does not
-      # produce a nice error message.  The following code is a fallback
-      # solution which is cause a nicer error message in the next
-      # evaluation.
-      echo "\"let option = nixos.options${option:+.$option}; config = nixos.config${option:+.$option}; in\""
-  fi
-}
-
-header="$header
-$(eval echo $(generateAccessors))
-"
-
-evalAttr(){
-  local prefix="$1"
-  local strict="$2"
-  local suffix="$3"
-
-  # If strict is set, then set it to "true".
-  test -n "$strict" && strict=true
-
-  evalNix ${strict:+--strict} <<EOF
-$header
-
-let
-  value = $prefix${suffix:+.$suffix};
-  strict = ${strict:-false};
-  cleanOutput = x: with nixpkgs.lib;
-    if isDerivation x then x.outPath
-    else if isFunction x then "<CODE>"
-    else if strict then
-      if isAttrs x then mapAttrs (n: cleanOutput) x
-      else if isList x then map cleanOutput x
-      else x
-    else x;
-in
-  cleanOutput value
-EOF
-}
-
-evalOpt(){
-  evalAttr "option" "" "$@"
-}
-
-evalCfg(){
-  local strict="$1"
-  evalAttr "config" "$strict"
-}
-
-findSources(){
-  local suffix=$1
-  evalNix --strict <<EOF
-$header
-
-option.$suffix
-EOF
-}
-
-# Given a result from nix-instantiate, recover the list of attributes it
-# contains.
-attrNames() {
-  local attributeset=$1
-  # sed is used to replace un-printable subset by 0s, and to remove most of
-  # the inner-attribute set, which reduce the likelyhood to encounter badly
-  # pre-processed input.
-  echo "builtins.attrNames $attributeset" | \
-    sed 's,<[A-Z]*>,0,g; :inner; s/{[^\{\}]*};/0;/g; t inner;' | \
-    evalNix --strict
-}
-
-# map a simple list which contains strings or paths.
-nixMap() {
-  local fun="$1"
-  local list="$2"
-  local elem
-  for elem in $list; do
-    test $elem = '[' -o $elem = ']' && continue;
-    $fun $elem
-  done
-}
-
-# This duplicates the work made below, but it is useful for processing
-# the output of nixos-option with other tools such as nixos-gui.
-if $xml; then
-  evalNix --xml --no-location <<EOF
-$header
-
-let
-  sources = builtins.map (f: f.source);
-  opt = option;
-  cfg = config;
-in
-
-with nixpkgs.lib;
-
-let
-  optStrict = v:
-    let
-      traverse = x :
-        if isAttrs x then
-          if x ? outPath then true
-          else all id (mapAttrsFlatten (n: traverseNoAttrs) x)
-        else traverseNoAttrs x;
-      traverseNoAttrs = x:
-        # do not continue in attribute sets
-        if isAttrs x then true
-        else if isList x then all id (map traverse x)
-        else true;
-    in assert traverse v; v;
-in
-
-if isOption opt then
-  optStrict ({}
-  // optionalAttrs (opt ? default) { inherit (opt) default; }
-  // optionalAttrs (opt ? example) { inherit (opt) example; }
-  // optionalAttrs (opt ? description) { inherit (opt) description; }
-  // optionalAttrs (opt ? type) { typename = opt.type.description; }
-  // optionalAttrs (opt ? options) { inherit (opt) options; }
-  // {
-    # to disambiguate the xml output.
-    _isOption = true;
-    declarations = sources opt.declarations;
-    definitions = sources opt.definitions;
-    value = cfg;
-  })
-else
-  opt
-EOF
-  exit $?
-fi
-
-if test "$(evalOpt "_type" 2> /dev/null)" = '"option"'; then
-  echo "Value:"
-  evalCfg 1
-
-  echo
-
-  echo "Default:"
-  if default=$(evalOpt "default" - 2> /dev/null); then
-    echo "$default"
-  else
-    echo "<None>"
-  fi
-  echo
-  if example=$(evalOpt "example" - 2> /dev/null); then
-    echo "Example:"
-    echo "$example"
-    echo
-  fi
-  echo "Description:"
-  echo
-  echo $(evalOpt "description")
-
-  echo $desc;
-
-  printPath () { echo "  $1"; }
-
-  echo "Declared by:"
-  nixMap printPath "$(findSources "declarations")"
-  echo
-  echo "Defined by:"
-  nixMap printPath "$(findSources "files")"
-  echo
-
-else
-  # echo 1>&2 "Warning: This value is not an option."
-
-  result=$(evalCfg "")
-  if [ ! -z "$result" ]; then
-    names=$(attrNames "$result" 2> /dev/null)
-    echo 1>&2 "This attribute set contains:"
-    escapeQuotes () { eval echo "$1"; }
-    nixMap escapeQuotes "$names"
-  else
-    echo 1>&2 "An error occurred while looking for attribute names. Are you sure that '$option' exists?"
-  fi
-fi
-
-exit $exit_code
diff --git a/nixos/modules/installer/tools/nixos-option/CMakeLists.txt b/nixos/modules/installer/tools/nixos-option/CMakeLists.txt
new file mode 100644
index 000000000000..e5834598c4fd
--- /dev/null
+++ b/nixos/modules/installer/tools/nixos-option/CMakeLists.txt
@@ -0,0 +1,8 @@
+cmake_minimum_required (VERSION 2.6)
+project (nixos-option)
+
+add_executable(nixos-option nixos-option.cc libnix-copy-paste.cc)
+target_link_libraries(nixos-option PRIVATE -lnixmain -lnixexpr -lnixstore -lnixutil)
+target_compile_features(nixos-option PRIVATE cxx_std_17)
+
+install (TARGETS nixos-option DESTINATION bin)
diff --git a/nixos/modules/installer/tools/nixos-option/default.nix b/nixos/modules/installer/tools/nixos-option/default.nix
new file mode 100644
index 000000000000..6464a91052cc
--- /dev/null
+++ b/nixos/modules/installer/tools/nixos-option/default.nix
@@ -0,0 +1,8 @@
+{stdenv, boost, cmake, pkgconfig, nix, ... }:
+stdenv.mkDerivation rec {
+  name = "nixos-option";
+  src = ./.;
+  nativeBuildInputs = [ cmake pkgconfig ];
+  buildInputs = [ boost nix ];
+  enableParallelBuilding = true;
+}
diff --git a/nixos/modules/installer/tools/nixos-option/libnix-copy-paste.cc b/nixos/modules/installer/tools/nixos-option/libnix-copy-paste.cc
new file mode 100644
index 000000000000..81de5ff8523b
--- /dev/null
+++ b/nixos/modules/installer/tools/nixos-option/libnix-copy-paste.cc
@@ -0,0 +1,81 @@
+// These are useful methods inside the nix library that ought to be exported.
+// Since they are not, copy/paste them here.
+// TODO: Delete these and use the ones in the library as they become available.
+
+#include <nix/config.h> // for nix/globals.hh's reference to SYSTEM
+
+#include "libnix-copy-paste.hh"
+#include <boost/format/alt_sstream.hpp>           // for basic_altstringbuf...
+#include <boost/format/alt_sstream_impl.hpp>      // for basic_altstringbuf...
+#include <boost/format/format_class.hpp>          // for basic_format
+#include <boost/format/format_fwd.hpp>            // for format
+#include <boost/format/format_implementation.hpp> // for basic_format::basi...
+#include <boost/optional/optional.hpp>            // for get_pointer
+#include <iostream>                               // for operator<<, basic_...
+#include <nix/types.hh>                           // for Strings, Error
+#include <string>                                 // for string, basic_string
+
+using boost::format;
+using nix::Error;
+using nix::Strings;
+using std::string;
+
+// From nix/src/libexpr/attr-path.cc
+Strings parseAttrPath(const string &s) {
+  Strings res;
+  string cur;
+  string::const_iterator i = s.begin();
+  while (i != s.end()) {
+    if (*i == '.') {
+      res.push_back(cur);
+      cur.clear();
+    } else if (*i == '"') {
+      ++i;
+      while (1) {
+        if (i == s.end())
+          throw Error(format("missing closing quote in selection path '%1%'") %
+                      s);
+        if (*i == '"')
+          break;
+        cur.push_back(*i++);
+      }
+    } else
+      cur.push_back(*i);
+    ++i;
+  }
+  if (!cur.empty())
+    res.push_back(cur);
+  return res;
+}
+
+// From nix/src/nix/repl.cc
+bool isVarName(const string &s) {
+  if (s.size() == 0)
+    return false;
+  char c = s[0];
+  if ((c >= '0' && c <= '9') || c == '-' || c == '\'')
+    return false;
+  for (auto &i : s)
+    if (!((i >= 'a' && i <= 'z') || (i >= 'A' && i <= 'Z') ||
+          (i >= '0' && i <= '9') || i == '_' || i == '-' || i == '\''))
+      return false;
+  return true;
+}
+
+// From nix/src/nix/repl.cc
+std::ostream &printStringValue(std::ostream &str, const char *string) {
+  str << "\"";
+  for (const char *i = string; *i; i++)
+    if (*i == '\"' || *i == '\\')
+      str << "\\" << *i;
+    else if (*i == '\n')
+      str << "\\n";
+    else if (*i == '\r')
+      str << "\\r";
+    else if (*i == '\t')
+      str << "\\t";
+    else
+      str << *i;
+  str << "\"";
+  return str;
+}
diff --git a/nixos/modules/installer/tools/nixos-option/libnix-copy-paste.hh b/nixos/modules/installer/tools/nixos-option/libnix-copy-paste.hh
new file mode 100644
index 000000000000..225e8b1b87ee
--- /dev/null
+++ b/nixos/modules/installer/tools/nixos-option/libnix-copy-paste.hh
@@ -0,0 +1,9 @@
+#pragma once
+
+#include <iostream>
+#include <nix/types.hh>
+#include <string>
+
+nix::Strings parseAttrPath(const std::string &s);
+bool isVarName(const std::string &s);
+std::ostream &printStringValue(std::ostream &str, const char *string);
diff --git a/nixos/modules/installer/tools/nixos-option/nixos-option.cc b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
new file mode 100644
index 000000000000..c778596d6150
--- /dev/null
+++ b/nixos/modules/installer/tools/nixos-option/nixos-option.cc
@@ -0,0 +1,585 @@
+#include <nix/config.h> // for nix/globals.hh's reference to SYSTEM
+
+#include <algorithm>               // for sort
+#include <functional>              // for function
+#include <iostream>                // for operator<<, basic_ostream, ostrin...
+#include <iterator>                // for next
+#include <list>                    // for _List_iterator
+#include <memory>                  // for allocator, unique_ptr, make_unique
+#include <nix/args.hh>             // for argvToStrings, UsageError
+#include <nix/attr-path.hh>        // for findAlongAttrPath
+#include <nix/attr-set.hh>         // for Attr, Bindings, Bindings::iterator
+#include <nix/common-eval-args.hh> // for MixEvalArgs
+#include <nix/eval-inline.hh>      // for EvalState::forceValue
+#include <nix/eval.hh>             // for EvalState, initGC, operator<<
+#include <nix/globals.hh>          // for initPlugins, Settings, settings
+#include <nix/nixexpr.hh>          // for Pos
+#include <nix/shared.hh>           // for getArg, LegacyArgs, printVersion
+#include <nix/store-api.hh>        // for openStore
+#include <nix/symbol-table.hh>     // for Symbol, SymbolTable
+#include <nix/types.hh>            // for Error, Path, Strings, PathSet
+#include <nix/util.hh>             // for absPath, baseNameOf
+#include <nix/value.hh>            // for Value, Value::(anonymous), Value:...
+#include <string>                  // for string, operator+, operator==
+#include <utility>                 // for move
+#include <variant>                 // for get, holds_alternative, variant
+#include <vector>                  // for vector<>::iterator, vector
+
+#include "libnix-copy-paste.hh"
+
+using nix::absPath;
+using nix::Bindings;
+using nix::Error;
+using nix::EvalState;
+using nix::Path;
+using nix::PathSet;
+using nix::Strings;
+using nix::Symbol;
+using nix::tAttrs;
+using nix::tLambda;
+using nix::tString;
+using nix::UsageError;
+using nix::Value;
+
+// An ostream wrapper to handle nested indentation
+class Out {
+public:
+  class Separator {};
+  const static Separator sep;
+  enum LinePolicy { ONE_LINE, MULTI_LINE };
+  explicit Out(std::ostream &ostream)
+      : ostream(ostream), policy(ONE_LINE), write_since_sep(true) {}
+  Out(Out &o, std::string const &start, std::string const &end,
+      LinePolicy policy);
+  Out(Out &o, std::string const &start, std::string const &end, int count)
+      : Out(o, start, end, count < 2 ? ONE_LINE : MULTI_LINE) {}
+  Out(Out const &) = delete;
+  Out(Out &&) = default;
+  Out &operator=(Out const &) = delete;
+  Out &operator=(Out &&) = delete;
+  ~Out() { ostream << end; }
+
+private:
+  std::ostream &ostream;
+  std::string indentation;
+  std::string end;
+  LinePolicy policy;
+  bool write_since_sep;
+  template <typename T> friend Out &operator<<(Out &o, T thing);
+};
+
+template <typename T> Out &operator<<(Out &o, T thing) {
+  if (!o.write_since_sep && o.policy == Out::MULTI_LINE) {
+    o.ostream << o.indentation;
+  }
+  o.write_since_sep = true;
+  o.ostream << thing;
+  return o;
+}
+
+template <>
+Out &operator<<<Out::Separator>(Out &o, Out::Separator /* thing */) {
+  o.ostream << (o.policy == Out::ONE_LINE ? " " : "\n");
+  o.write_since_sep = false;
+  return o;
+}
+
+Out::Out(Out &o, std::string const &start, std::string const &end,
+         LinePolicy policy)
+    : ostream(o.ostream),
+      indentation(policy == ONE_LINE ? o.indentation : o.indentation + "  "),
+      end(policy == ONE_LINE ? end : o.indentation + end), policy(policy),
+      write_since_sep(true) {
+  o << start;
+  *this << Out::sep;
+}
+
+// Stuff needed for evaluation
+struct Context {
+  Context(EvalState *state, Bindings *autoArgs, Value options_root,
+          Value config_root)
+      : state(state), autoArgs(autoArgs), options_root(options_root),
+        config_root(config_root),
+        underscore_type(state->symbols.create("_type")) {}
+  EvalState *state;
+  Bindings *autoArgs;
+  Value options_root;
+  Value config_root;
+  Symbol underscore_type;
+};
+
+Value evaluateValue(Context *ctx, Value *v) {
+  ctx->state->forceValue(*v);
+  if (ctx->autoArgs->empty()) {
+    return *v;
+  }
+  Value called{};
+  ctx->state->autoCallFunction(*ctx->autoArgs, *v, called);
+  return called;
+}
+
+bool isOption(Context *ctx, Value const &v) {
+  if (v.type != tAttrs) {
+    return false;
+  }
+  auto const &actual_type = v.attrs->find(ctx->underscore_type);
+  if (actual_type == v.attrs->end()) {
+    return false;
+  }
+  try {
+    Value evaluated_type = evaluateValue(ctx, actual_type->value);
+    if (evaluated_type.type != tString) {
+      return false;
+    }
+    return evaluated_type.string.s == static_cast<std::string>("option");
+  } catch (Error &) {
+    return false;
+  }
+}
+
+// Add quotes to a component of a path.
+// These are needed for paths like:
+//    fileSystems."/".fsType
+//    systemd.units."dbus.service".text
+std::string quoteAttribute(std::string const &attribute) {
+  if (isVarName(attribute)) {
+    return attribute;
+  }
+  std::ostringstream buf;
+  printStringValue(buf, attribute.c_str());
+  return buf.str();
+}
+
+std::string const appendPath(std::string const &prefix,
+                             std::string const &suffix) {
+  if (prefix.empty()) {
+    return quoteAttribute(suffix);
+  }
+  return prefix + "." + quoteAttribute(suffix);
+}
+
+bool forbiddenRecursionName(std::string name) {
+  return (!name.empty() && name[0] == '_') || name == "haskellPackages";
+}
+
+void recurse(const std::function<bool(std::string const &path,
+                                      std::variant<Value, Error>)> &f,
+             Context *ctx, Value v, std::string const &path) {
+  std::variant<Value, Error> evaluated;
+  try {
+    evaluated = evaluateValue(ctx, &v);
+  } catch (Error &e) {
+    evaluated = e;
+  }
+  if (!f(path, evaluated)) {
+    return;
+  }
+  if (std::holds_alternative<Error>(evaluated)) {
+    return;
+  }
+  Value const &evaluated_value = std::get<Value>(evaluated);
+  if (evaluated_value.type != tAttrs) {
+    return;
+  }
+  for (auto const &child : evaluated_value.attrs->lexicographicOrder()) {
+    if (forbiddenRecursionName(child->name)) {
+      continue;
+    }
+    recurse(f, ctx, *child->value, appendPath(path, child->name));
+  }
+}
+
+// Calls f on all the option names
+void mapOptions(const std::function<void(std::string const &path)> &f,
+                Context *ctx, Value root) {
+  recurse(
+      [f, ctx](std::string const &path, std::variant<Value, Error> v) {
+        bool isOpt = std::holds_alternative<Error>(v) ||
+                     isOption(ctx, std::get<Value>(v));
+        if (isOpt) {
+          f(path);
+        }
+        return !isOpt;
+      },
+      ctx, root, "");
+}
+
+// Calls f on all the config values inside one option.
+// Simple options have one config value inside, like sound.enable = true.
+// Compound options have multiple config values.  For example, the option
+// "users.users" has about 1000 config values inside it:
+//   users.users.avahi.createHome = false;
+//   users.users.avahi.cryptHomeLuks = null;
+//   users.users.avahi.description = "`avahi-daemon' privilege separation user";
+//   ...
+//   users.users.avahi.openssh.authorizedKeys.keyFiles = [ ];
+//   users.users.avahi.openssh.authorizedKeys.keys = [ ];
+//   ...
+//   users.users.avahi.uid = 10;
+//   users.users.avahi.useDefaultShell = false;
+//   users.users.cups.createHome = false;
+//   ...
+//   users.users.cups.useDefaultShell = false;
+//   users.users.gdm = ... ... ...
+//   users.users.messagebus = ... .. ...
+//   users.users.nixbld1 = ... .. ...
+//   ...
+//   users.users.systemd-timesync = ... .. ...
+void mapConfigValuesInOption(
+    const std::function<void(std::string const &path,
+                             std::variant<Value, Error> v)> &f,
+    std::string const &path, Context *ctx) {
+  Value *option;
+  try {
+    option =
+        findAlongAttrPath(*ctx->state, path, *ctx->autoArgs, ctx->config_root);
+  } catch (Error &e) {
+    f(path, e);
+    return;
+  }
+  recurse(
+      [f, ctx](std::string const &path, std::variant<Value, Error> v) {
+        bool leaf = std::holds_alternative<Error>(v) ||
+                    std::get<Value>(v).type != tAttrs ||
+                    ctx->state->isDerivation(std::get<Value>(v));
+        if (!leaf) {
+          return true; // Keep digging
+        }
+        f(path, v);
+        return false;
+      },
+      ctx, *option, path);
+}
+
+std::string describeError(Error const &e) { return "«error: " + e.msg() + "»"; }
+
+void describeDerivation(Context *ctx, Out &out, Value v) {
+  // Copy-pasted from nix/src/nix/repl.cc  :(
+  Bindings::iterator i = v.attrs->find(ctx->state->sDrvPath);
+  PathSet pathset;
+  try {
+    Path drvPath = i != v.attrs->end()
+                       ? ctx->state->coerceToPath(*i->pos, *i->value, pathset)
+                       : "???";
+    out << "«derivation " << drvPath << "»";
+  } catch (Error &e) {
+    out << describeError(e);
+  }
+}
+
+Value parseAndEval(EvalState *state, std::string const &expression,
+                   std::string const &path) {
+  Value v{};
+  state->eval(state->parseExprFromString(expression, absPath(path)), v);
+  return v;
+}
+
+void printValue(Context *ctx, Out &out, std::variant<Value, Error> maybe_value,
+                std::string const &path);
+
+void printUnsortedList(Context *ctx, Out &out, Value &v) {
+  Out list_out(out, "[", "]", v.listSize());
+  for (unsigned int n = 0; n < v.listSize(); ++n) {
+    printValue(ctx, list_out, *v.listElems()[n], "");
+    list_out << Out::sep;
+  }
+}
+
+void printSortedList(Context *ctx, Out &out, Value &v) {
+  std::vector<std::string> results;
+  for (unsigned int n = 0; n < v.listSize(); ++n) {
+    std::ostringstream buf;
+    Out buf_out(buf);
+    printValue(ctx, buf_out, *v.listElems()[n], "");
+    results.push_back(buf.str());
+  }
+  std::sort(results.begin(), results.end());
+  Out list_out(out, "[", "]", v.listSize());
+  for (auto const &v : results) {
+    list_out << v << Out::sep;
+  }
+}
+
+bool shouldSort(Context *ctx, Value &v) {
+  // Some lists should clearly be printed in sorted order, like
+  // environment.systemPackages.  Some clearly should not, like
+  // services.xserver.multitouch.buttonsMap.  As a conservative heuristic, sort
+  // lists of derivations.
+  return v.listSize() > 0 && ctx->state->isDerivation(*v.listElems()[0]);
+}
+
+void printList(Context *ctx, Out &out, Value &v) {
+  if (shouldSort(ctx, v)) {
+    printSortedList(ctx, out, v);
+  } else {
+    printUnsortedList(ctx, out, v);
+  }
+}
+
+void printAttrs(Context *ctx, Out &out, Value &v, std::string const &path) {
+  Out attrs_out(out, "{", "}", v.attrs->size());
+  for (const auto &a : v.attrs->lexicographicOrder()) {
+    std::string name = a->name;
+    attrs_out << name << " = ";
+    printValue(ctx, attrs_out, *a->value, appendPath(path, name));
+    attrs_out << ";" << Out::sep;
+  }
+}
+
+void multiLineStringEscape(Out &out, std::string const &s) {
+  int i;
+  for (i = 1; i < s.size(); i++) {
+    if (s[i - 1] == '$' && s[i] == '{') {
+      out << "''${";
+      i++;
+    } else if (s[i - 1] == '\'' && s[i] == '\'') {
+      out << "'''";
+      i++;
+    } else {
+      out << s[i - 1];
+    }
+  }
+  if (i == s.size()) {
+    out << s[i - 1];
+  }
+}
+
+void printMultiLineString(Out &out, Value const &v) {
+  std::string s = v.string.s;
+  Out str_out(out, "''", "''", Out::MULTI_LINE);
+  std::string::size_type begin = 0;
+  while (begin < s.size()) {
+    std::string::size_type end = s.find('\n', begin);
+    if (end == std::string::npos) {
+      multiLineStringEscape(str_out, s.substr(begin, s.size() - begin));
+      break;
+    }
+    multiLineStringEscape(str_out, s.substr(begin, end - begin));
+    str_out << Out::sep;
+    begin = end + 1;
+  }
+}
+
+void printValue(Context *ctx, Out &out, std::variant<Value, Error> maybe_value,
+                std::string const &path) {
+  try {
+    if (std::holds_alternative<Error>(maybe_value)) {
+      throw Error{std::get<Error>(maybe_value)};
+    }
+    Value v = evaluateValue(ctx, &std::get<Value>(maybe_value));
+    if (ctx->state->isDerivation(v)) {
+      describeDerivation(ctx, out, v);
+    } else if (v.isList()) {
+      printList(ctx, out, v);
+    } else if (v.type == tAttrs) {
+      printAttrs(ctx, out, v, path);
+    } else if (v.type == tString &&
+               std::string(v.string.s).find('\n') != std::string::npos) {
+      printMultiLineString(out, v);
+    } else {
+      ctx->state->forceValueDeep(v);
+      out << v;
+    }
+  } catch (Error &e) {
+    if (e.msg() == "The option `" + path + "' is used but not defined.") {
+      // 93% of errors are this, and just letting this message through would be
+      // misleading.  These values may or may not actually be "used" in the
+      // config.  The thing throwing the error message assumes that if anything
+      // ever looks at this value, it is a "use" of this value.  But here in
+      // nixos-options-summary, we are looking at this value only to print it.
+      // In order to avoid implying that this undefined value is actually
+      // referenced, eat the underlying error message and emit "«not defined»".
+      out << "«not defined»";
+    } else {
+      out << describeError(e);
+    }
+  }
+}
+
+void printConfigValue(Context *ctx, Out &out, std::string const &path,
+                      std::variant<Value, Error> v) {
+  out << path << " = ";
+  printValue(ctx, out, std::move(v), path);
+  out << ";\n";
+}
+
+void printAll(Context *ctx, Out &out) {
+  mapOptions(
+      [ctx, &out](std::string const &option_path) {
+        mapConfigValuesInOption(
+            [ctx, &out](std::string const &config_path,
+                        std::variant<Value, Error> v) {
+              printConfigValue(ctx, out, config_path, v);
+            },
+            option_path, ctx);
+      },
+      ctx, ctx->options_root);
+}
+
+void printAttr(Context *ctx, Out &out, std::string const &path, Value *root) {
+  try {
+    printValue(ctx, out,
+               *findAlongAttrPath(*ctx->state, path, *ctx->autoArgs, *root),
+               path);
+  } catch (Error &e) {
+    out << describeError(e);
+  }
+}
+
+void printOption(Context *ctx, Out &out, std::string const &path,
+                 Value *option) {
+  out << "Value:\n";
+  printAttr(ctx, out, path, &ctx->config_root);
+
+  out << "\n\nDefault:\n";
+  printAttr(ctx, out, "default", option);
+
+  out << "\n\nExample:\n";
+  printAttr(ctx, out, "example", option);
+
+  out << "\n\nDescription:\n";
+  printAttr(ctx, out, "description", option);
+
+  out << "\n\nDeclared by:\n";
+  printAttr(ctx, out, "declarations", option);
+
+  out << "\n\nDefined by:\n";
+  printAttr(ctx, out, "files", option);
+  out << "\n";
+}
+
+void printListing(Out &out, Value *v) {
+  // Print this header on stderr rather than stdout because the old shell script
+  // implementation did.  I don't know why.
+  std::cerr << "This attribute set contains:\n";
+  for (const auto &a : v->attrs->lexicographicOrder()) {
+    std::string name = a->name;
+    if (!name.empty() && name[0] != '_') {
+      out << name << "\n";
+    }
+  }
+}
+
+// Carefully walk an option path, looking for sub-options when a path walks past
+// an option value.
+Value findAlongOptionPath(Context *ctx, std::string const &path) {
+  Strings tokens = parseAttrPath(path);
+  Value v = ctx->options_root;
+  for (auto i = tokens.begin(); i != tokens.end(); i++) {
+    bool last_attribute = std::next(i) == tokens.end();
+    auto const &attr = *i;
+    v = evaluateValue(ctx, &v);
+    if (attr.empty()) {
+      throw Error("empty attribute name in selection path '" + path + "'");
+    }
+    if (isOption(ctx, v) && !last_attribute) {
+      Value getSubOptions = evaluateValue(
+          ctx, findAlongAttrPath(*ctx->state, "type.getSubOptions",
+                                 *ctx->autoArgs, v));
+      if (getSubOptions.type != tLambda) {
+        throw Error("Option's type.getSubOptions isn't a function at '" + attr +
+                    "' in path '" + path + "'");
+      }
+      Value emptyString{};
+      nix::mkString(emptyString, "");
+      ctx->state->callFunction(getSubOptions, emptyString, v, nix::Pos{});
+      // Note that we've consumed attr, but didn't actually use it.
+    } else if (v.type != tAttrs) {
+      throw Error("attribute '" + attr + "' in path '" + path +
+                  "' attempts to index a value that should be a set but is " +
+                  showType(v));
+    } else {
+      auto const &next = v.attrs->find(ctx->state->symbols.create(attr));
+      if (next == v.attrs->end()) {
+        throw Error("attribute '" + attr + "' in path '" + path +
+                    "' not found");
+      }
+      v = *next->value;
+    }
+  }
+  return v;
+}
+
+void printOne(Context *ctx, Out &out, std::string const &path) {
+  try {
+    Value option = findAlongOptionPath(ctx, path);
+    option = evaluateValue(ctx, &option);
+    if (isOption(ctx, option)) {
+      printOption(ctx, out, path, &option);
+    } else {
+      printListing(out, &option);
+    }
+  } catch (Error &e) {
+    std::cerr << "error: " << e.msg()
+              << "\nAn error occurred while looking for attribute names. Are "
+                 "you sure that '"
+              << path << "' exists?\n";
+  }
+}
+
+int main(int argc, char **argv) {
+  bool all = false;
+  std::string path = ".";
+  std::string options_expr = "(import <nixpkgs/nixos> {}).options";
+  std::string config_expr = "(import <nixpkgs/nixos> {}).config";
+  std::vector<std::string> args;
+
+  struct MyArgs : nix::LegacyArgs, nix::MixEvalArgs {
+    using nix::LegacyArgs::LegacyArgs;
+  };
+
+  MyArgs myArgs(nix::baseNameOf(argv[0]),
+                [&](Strings::iterator &arg, const Strings::iterator &end) {
+                  if (*arg == "--help") {
+                    nix::showManPage("nixos-options-summary");
+                  } else if (*arg == "--version") {
+                    nix::printVersion("nixos-options-summary");
+                  } else if (*arg == "--all") {
+                    all = true;
+                  } else if (*arg == "--path") {
+                    path = nix::getArg(*arg, arg, end);
+                  } else if (*arg == "--options_expr") {
+                    options_expr = nix::getArg(*arg, arg, end);
+                  } else if (*arg == "--config_expr") {
+                    config_expr = nix::getArg(*arg, arg, end);
+                  } else if (!arg->empty() && arg->at(0) == '-') {
+                    return false;
+                  } else {
+                    args.push_back(*arg);
+                  }
+                  return true;
+                });
+
+  myArgs.parseCmdline(nix::argvToStrings(argc, argv));
+
+  nix::initPlugins();
+  nix::initGC();
+  nix::settings.readOnlyMode = true;
+  auto store = nix::openStore();
+  auto state = std::make_unique<EvalState>(myArgs.searchPath, store);
+
+  Value options_root = parseAndEval(state.get(), options_expr, path);
+  Value config_root = parseAndEval(state.get(), config_expr, path);
+
+  Context ctx{state.get(), myArgs.getAutoArgs(*state), options_root,
+              config_root};
+  Out out(std::cout);
+
+  if (all) {
+    if (!args.empty()) {
+      throw UsageError("--all cannot be used with arguments");
+    }
+    printAll(&ctx, out);
+  } else {
+    if (args.empty()) {
+      printOne(&ctx, out, "");
+    }
+    for (auto const &arg : args) {
+      printOne(&ctx, out, arg);
+    }
+  }
+
+  ctx.state->printStats();
+
+  return 0;
+}
diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix
index 052e7fdd4fc1..e4db39b5c810 100644
--- a/nixos/modules/installer/tools/tools.nix
+++ b/nixos/modules/installer/tools/tools.nix
@@ -41,10 +41,7 @@ let
     inherit (config.system.nixos-generate-config) configuration;
   };
 
-  nixos-option = makeProg {
-    name = "nixos-option";
-    src = ./nixos-option.sh;
-  };
+  nixos-option = pkgs.callPackage ./nixos-option { };
 
   nixos-version = makeProg {
     name = "nixos-version";