diff options
author | Chuck <chuck@intelligence.org> | 2019-09-05 17:29:01 -0700 |
---|---|---|
committer | Linus Heckemann <git@sphalerite.org> | 2019-11-04 15:11:44 +0100 |
commit | 59c5bfc86b75247cb48539eeaaea2a3c5f320b1d (patch) | |
tree | 53d224e3a4003942d0c366955afa26a98875d048 /nixos/modules/installer | |
parent | d690c20efd07b422c68529b8719454ee74f2f1df (diff) | |
download | nixlib-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')
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"; |