nixos-option: rewrite as a nix script
This ports the functionality of the C++ nixos-option to a nix script (with a tiny shellscript for argument processing and invoking the nix script). Benefits compared to the native binary include no longer being tied to a specific nix version, generally improved maintainability and improved stability. The main tradeoff is that the C++ version would have better access to introspecting and reporting errors nicely, but that doesn't seem to have been the case in practice anyway. The other tradeoff is that we generate all the output at the end instead of streaming it as we traverse the option tree. Co-authored-by: Zhong Jianxin <azuwis@gmail.com> Co-authored-by: aleksana <me@aleksana.moe> Co-authored-by: eclairevoyant <contactmeongithubinstead@proton.me>
This commit is contained in:
committed by
Thiago Kenji Okada
co-authored by
Zhong Jianxin
aleksana
eclairevoyant
parent
1c5eed9e58
commit
7c8e422689
@@ -1,43 +1,73 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
boost,
|
||||
meson,
|
||||
ninja,
|
||||
pkg-config,
|
||||
makeWrapper,
|
||||
stdenvNoCC,
|
||||
installShellFiles,
|
||||
shellcheck,
|
||||
nix,
|
||||
nixosTests,
|
||||
jq,
|
||||
man-db,
|
||||
coreutils,
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
stdenvNoCC.mkDerivation {
|
||||
name = "nixos-option";
|
||||
|
||||
src = ./.;
|
||||
|
||||
postInstall = ''
|
||||
installManPage ../nixos-option.8
|
||||
'';
|
||||
|
||||
strictDeps = true;
|
||||
src = ./nixos-option.sh;
|
||||
|
||||
nativeBuildInputs = [
|
||||
meson
|
||||
ninja
|
||||
pkg-config
|
||||
installShellFiles
|
||||
];
|
||||
buildInputs = [
|
||||
boost
|
||||
nix
|
||||
makeWrapper
|
||||
shellcheck
|
||||
];
|
||||
|
||||
passthru.tests.installer-simpleUefiSystemdBoot = nixosTests.installer.simpleUefiSystemdBoot;
|
||||
env = {
|
||||
nixosOptionNix = "${./nixos-option.nix}";
|
||||
nixosOptionManpage = "${placeholder "out"}/share/man";
|
||||
};
|
||||
|
||||
meta = with lib; {
|
||||
license = licenses.lgpl2Plus;
|
||||
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 [
|
||||
nix
|
||||
jq
|
||||
man-db
|
||||
coreutils
|
||||
]
|
||||
}
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Evaluate NixOS configuration and return the properties of given option";
|
||||
license = lib.licenses.mit;
|
||||
mainProgram = "nixos-option";
|
||||
maintainers = [ ];
|
||||
inherit (nix.meta) platforms;
|
||||
maintainers = with lib.maintainers; [
|
||||
FireyFly
|
||||
azuwis
|
||||
aleksana
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
project('nixos-option', 'cpp',
|
||||
version : '0.1.6',
|
||||
license : 'GPL-3.0',
|
||||
)
|
||||
|
||||
nix_main_dep = dependency('nix-main', required: true)
|
||||
nix_store_dep = dependency('nix-store', required: true)
|
||||
nix_expr_dep = dependency('nix-expr', required: true)
|
||||
nix_cmd_dep = dependency('nix-cmd', required: true)
|
||||
nix_flake_dep = dependency('nix-flake', required: true)
|
||||
threads_dep = dependency('threads', required: true)
|
||||
nlohmann_json_dep = dependency('nlohmann_json', required: true)
|
||||
boost_dep = dependency('boost', required: true)
|
||||
|
||||
subdir('src')
|
||||
@@ -0,0 +1,167 @@
|
||||
{
|
||||
nixos ? import <nixpkgs/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 ? false,
|
||||
}:
|
||||
|
||||
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 "[1;31m«error»[m";
|
||||
|
||||
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
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
# shellcheck shell=bash
|
||||
set -eou pipefail
|
||||
|
||||
recursive=false
|
||||
no_flake=false
|
||||
positional_args=()
|
||||
nix_args=()
|
||||
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
|
||||
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
|
||||
if [[ -d "$flake" ]] && [[ -e "$flake/.git" ]] ; 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
|
||||
@@ -1,22 +0,0 @@
|
||||
// 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 "libnix-copy-paste.hh"
|
||||
#include <nix/print.hh> // for Strings
|
||||
|
||||
// From nix/src/nix/repl.cc
|
||||
bool isVarName(const std::string_view & s)
|
||||
{
|
||||
if (s.size() == 0) return false;
|
||||
if (nix::isReservedKeyword(s)) 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;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <nix/types.hh>
|
||||
|
||||
bool isVarName(const std::string_view & s);
|
||||
@@ -1,20 +0,0 @@
|
||||
src = [
|
||||
'nixos-option.cc',
|
||||
'libnix-copy-paste.cc',
|
||||
]
|
||||
|
||||
cc = meson.get_compiler('cpp')
|
||||
|
||||
executable('nixos-option', src,
|
||||
dependencies : [
|
||||
nix_main_dep,
|
||||
nix_store_dep,
|
||||
nix_expr_dep,
|
||||
nix_cmd_dep,
|
||||
nix_flake_dep,
|
||||
boost_dep,
|
||||
nlohmann_json_dep,
|
||||
threads_dep
|
||||
],
|
||||
install: true,
|
||||
cpp_args: ['-std=c++2a', '--include', 'nix/config.h'])
|
||||
@@ -1,649 +0,0 @@
|
||||
#include <nix/args.hh> // for argvToStrings, UsageError
|
||||
#include <nix/attr-path.hh> // for findAlongAttrPath, parseAttrPath
|
||||
#include <nix/attr-set.hh> // for Attr, Bindings, Bindings::iterator
|
||||
#include <nix/common-eval-args.hh> // for MixEvalArgs
|
||||
#include <nix/eval-gc.hh> // for initGC, initNix
|
||||
#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 "libnix-copy-paste.hh"
|
||||
|
||||
using nix::absPath;
|
||||
using nix::Bindings;
|
||||
using nix::Error;
|
||||
using nix::EvalError;
|
||||
using nix::EvalState;
|
||||
using nix::Path;
|
||||
using nix::PathSet;
|
||||
using nix::Strings;
|
||||
using nix::Symbol;
|
||||
using nix::nAttrs;
|
||||
using nix::ThrownError;
|
||||
using nix::tLambda;
|
||||
using nix::nString;
|
||||
using nix::UsageError;
|
||||
using nix::Value;
|
||||
|
||||
struct Context
|
||||
{
|
||||
Context(EvalState & state, Bindings & autoArgs, Value optionsRoot, Value configRoot)
|
||||
: state(state), autoArgs(autoArgs), optionsRoot(optionsRoot), configRoot(configRoot),
|
||||
underscoreType(state.symbols.create("_type"))
|
||||
{}
|
||||
EvalState & state;
|
||||
Bindings & autoArgs;
|
||||
Value optionsRoot;
|
||||
Value configRoot;
|
||||
Symbol underscoreType;
|
||||
};
|
||||
|
||||
// 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), writeSinceSep(true) {}
|
||||
Out(Out & o, const std::string & start, const std::string & end, LinePolicy policy);
|
||||
Out(Out & o, const std::string & start, const std::string & end, int count)
|
||||
: Out(o, start, end, count < 2 ? ONE_LINE : MULTI_LINE)
|
||||
{}
|
||||
Out(const Out &) = delete;
|
||||
Out(Out &&) = default;
|
||||
Out & operator=(const Out &) = delete;
|
||||
Out & operator=(Out &&) = delete;
|
||||
~Out() { ostream << end; }
|
||||
|
||||
private:
|
||||
std::ostream & ostream;
|
||||
std::string indentation;
|
||||
std::string end;
|
||||
LinePolicy policy;
|
||||
bool writeSinceSep;
|
||||
template <typename T> friend Out & operator<<(Out & o, T thing);
|
||||
|
||||
friend void printValue(Context & ctx, Out & out, std::variant<Value, std::exception_ptr> maybeValue, const std::string & path);
|
||||
};
|
||||
|
||||
template <typename T> Out & operator<<(Out & o, T thing)
|
||||
{
|
||||
if (!o.writeSinceSep && o.policy == Out::MULTI_LINE) {
|
||||
o.ostream << o.indentation;
|
||||
}
|
||||
o.writeSinceSep = 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.writeSinceSep = false;
|
||||
return o;
|
||||
}
|
||||
|
||||
Out::Out(Out & o, const std::string & start, const std::string & end, LinePolicy policy)
|
||||
: ostream(o.ostream), indentation(policy == ONE_LINE ? o.indentation : o.indentation + " "),
|
||||
end(policy == ONE_LINE ? end : o.indentation + end), policy(policy), writeSinceSep(true)
|
||||
{
|
||||
o << start;
|
||||
*this << Out::sep;
|
||||
}
|
||||
|
||||
|
||||
Value evaluateValue(Context & ctx, Value & v)
|
||||
{
|
||||
ctx.state.forceValue(v, v.determinePos(nix::noPos));
|
||||
if (ctx.autoArgs.empty()) {
|
||||
return v;
|
||||
}
|
||||
Value called{};
|
||||
ctx.state.autoCallFunction(ctx.autoArgs, v, called);
|
||||
return called;
|
||||
}
|
||||
|
||||
bool isOption(Context & ctx, const Value & v)
|
||||
{
|
||||
if (v.type() != nAttrs) {
|
||||
return false;
|
||||
}
|
||||
const auto & actualType = v.attrs()->find(ctx.underscoreType);
|
||||
if (actualType == v.attrs()->end()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
Value evaluatedType = evaluateValue(ctx, *actualType->value);
|
||||
if (evaluatedType.type() != nString) {
|
||||
return false;
|
||||
}
|
||||
return evaluatedType.string_view() == "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(const std::string_view & attribute)
|
||||
{
|
||||
if (isVarName(attribute)) {
|
||||
return std::string(attribute);
|
||||
}
|
||||
std::ostringstream buf;
|
||||
nix::printLiteralString(buf, attribute);
|
||||
return buf.str();
|
||||
}
|
||||
|
||||
const std::string appendPath(const std::string & prefix, const std::string_view & suffix)
|
||||
{
|
||||
if (prefix.empty()) {
|
||||
return quoteAttribute(suffix);
|
||||
}
|
||||
return prefix + "." + quoteAttribute(suffix);
|
||||
}
|
||||
|
||||
bool forbiddenRecursionName(const nix::Symbol symbol, const nix::SymbolTable & symbolTable) {
|
||||
// note: this is created from a pointer
|
||||
// According to standard, it may never point to null, and hence attempts to check against nullptr are not allowed.
|
||||
// However, at the time of writing, I am not certain about the full implications of the omission of a nullptr check here.
|
||||
const std::string_view & name = symbolTable[symbol];
|
||||
// TODO: figure out why haskellPackages is not recursed here
|
||||
return (!name.empty() && name[0] == '_') || name == "haskellPackages";
|
||||
}
|
||||
|
||||
void recurse(const std::function<bool(const std::string & path, std::variant<Value, std::exception_ptr>)> & f,
|
||||
Context & ctx, Value v, const std::string & path)
|
||||
{
|
||||
std::variant<Value, std::exception_ptr> evaluated;
|
||||
try {
|
||||
evaluated = evaluateValue(ctx, v);
|
||||
} catch (Error &) {
|
||||
evaluated = std::current_exception();
|
||||
}
|
||||
if (!f(path, evaluated)) {
|
||||
return;
|
||||
}
|
||||
if (std::holds_alternative<std::exception_ptr>(evaluated)) {
|
||||
return;
|
||||
}
|
||||
const Value & evaluated_value = std::get<Value>(evaluated);
|
||||
if (evaluated_value.type() != nAttrs) {
|
||||
return;
|
||||
}
|
||||
for (const auto & child : evaluated_value.attrs()->lexicographicOrder(ctx.state.symbols)) {
|
||||
if (forbiddenRecursionName(child->name, ctx.state.symbols)) {
|
||||
continue;
|
||||
}
|
||||
std::string_view name = ctx.state.symbols[child->name];
|
||||
recurse(f, ctx, *child->value, appendPath(path, name));
|
||||
}
|
||||
}
|
||||
|
||||
bool optionTypeIs(Context & ctx, Value & v, const std::string & soughtType)
|
||||
{
|
||||
try {
|
||||
const auto & typeLookup = v.attrs()->find(ctx.state.sType);
|
||||
if (typeLookup == v.attrs()->end()) {
|
||||
return false;
|
||||
}
|
||||
Value type = evaluateValue(ctx, *typeLookup->value);
|
||||
if (type.type() != nAttrs) {
|
||||
return false;
|
||||
}
|
||||
const auto & nameLookup = type.attrs()->find(ctx.state.sName);
|
||||
if (nameLookup == type.attrs()->end()) {
|
||||
return false;
|
||||
}
|
||||
Value name = evaluateValue(ctx, *nameLookup->value);
|
||||
if (name.type() != nString) {
|
||||
return false;
|
||||
}
|
||||
return name.string_view() == soughtType;
|
||||
} catch (Error &) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isAggregateOptionType(Context & ctx, Value & v)
|
||||
{
|
||||
return optionTypeIs(ctx, v, "attrsOf") || optionTypeIs(ctx, v, "listOf");
|
||||
}
|
||||
|
||||
MakeError(OptionPathError, EvalError);
|
||||
|
||||
Value getSubOptions(Context & ctx, Value & option)
|
||||
{
|
||||
Value getSubOptions = evaluateValue(ctx, *findAlongAttrPath(ctx.state, "type.getSubOptions", ctx.autoArgs, option).first);
|
||||
if (getSubOptions.isLambda()) {
|
||||
throw OptionPathError(ctx.state, "Option's type.getSubOptions isn't a function");
|
||||
}
|
||||
Value emptyString{};
|
||||
emptyString.mkString("");
|
||||
Value v;
|
||||
ctx.state.callFunction(getSubOptions, emptyString, v, nix::PosIdx{});
|
||||
return v;
|
||||
}
|
||||
|
||||
// Carefully walk an option path, looking for sub-options when a path walks past
|
||||
// an option value.
|
||||
struct FindAlongOptionPathRet
|
||||
{
|
||||
Value option;
|
||||
std::string path;
|
||||
};
|
||||
FindAlongOptionPathRet findAlongOptionPath(Context & ctx, const std::string & path)
|
||||
{
|
||||
std::vector<Symbol> tokens = nix::parseAttrPath(ctx.state, path);
|
||||
Value v = ctx.optionsRoot;
|
||||
std::string processedPath;
|
||||
for (auto i = tokens.begin(); i != tokens.end(); i++) {
|
||||
const std::string_view attr = ctx.state.symbols[*i];
|
||||
try {
|
||||
bool lastAttribute = std::next(i) == tokens.end();
|
||||
v = evaluateValue(ctx, v);
|
||||
if (attr.empty()) {
|
||||
throw OptionPathError(ctx.state, "empty attribute name");
|
||||
}
|
||||
if (isOption(ctx, v) && optionTypeIs(ctx, v, "submodule")) {
|
||||
v = getSubOptions(ctx, v);
|
||||
}
|
||||
if (isOption(ctx, v) && isAggregateOptionType(ctx, v)) {
|
||||
auto subOptions = getSubOptions(ctx, v);
|
||||
if (lastAttribute && subOptions.attrs()->empty()) {
|
||||
break;
|
||||
}
|
||||
v = subOptions;
|
||||
// Note that we've consumed attr, but didn't actually use it. This is the path component that's looked
|
||||
// up in the list or attribute set that doesn't name an option -- the "root" in "users.users.root.name".
|
||||
} else if (v.type() != nAttrs) {
|
||||
throw OptionPathError(ctx.state, "Value is %s while a set was expected", showType(v));
|
||||
} else {
|
||||
const auto & next = v.attrs()->find(ctx.state.symbols.create(attr));
|
||||
if (next == v.attrs()->end()) {
|
||||
throw OptionPathError(ctx.state, "Attribute not found", attr, path);
|
||||
}
|
||||
v = *next->value;
|
||||
}
|
||||
processedPath = appendPath(processedPath, attr);
|
||||
} catch (OptionPathError & e) {
|
||||
throw OptionPathError(ctx.state, "At '%s' in path '%s': %s", attr, path, e.msg());
|
||||
}
|
||||
}
|
||||
return {v, processedPath};
|
||||
}
|
||||
|
||||
// Calls f on all the option names at or below the option described by `path`.
|
||||
// Note that "the option described by `path`" is not trivial -- if path describes a value inside an aggregate
|
||||
// option (such as users.users.root), the *option* described by that path is one path component shorter
|
||||
// (eg: users.users), which results in f being called on sibling-paths (eg: users.users.nixbld1). If f
|
||||
// doesn't want these, it must do its own filtering.
|
||||
void mapOptions(const std::function<void(const std::string & path)> & f, Context & ctx, const std::string & path)
|
||||
{
|
||||
auto root = findAlongOptionPath(ctx, path);
|
||||
recurse(
|
||||
[f, &ctx](const std::string & path, std::variant<Value, std::exception_ptr> v) {
|
||||
bool isOpt = std::holds_alternative<std::exception_ptr>(v) || isOption(ctx, std::get<Value>(v));
|
||||
if (isOpt) {
|
||||
f(path);
|
||||
}
|
||||
return !isOpt;
|
||||
},
|
||||
ctx, root.option, root.path);
|
||||
}
|
||||
|
||||
// Calls f on all the config values inside one option.
|
||||
// Simple options have one config value inside, like services.foo.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(const std::string & path, std::variant<Value, std::exception_ptr> v)> & f,
|
||||
const std::string & path, Context & ctx)
|
||||
{
|
||||
Value * option;
|
||||
try {
|
||||
option = findAlongAttrPath(ctx.state, path, ctx.autoArgs, ctx.configRoot).first;
|
||||
} catch (Error &) {
|
||||
f(path, std::current_exception());
|
||||
return;
|
||||
}
|
||||
recurse(
|
||||
[f, ctx](const std::string & path, std::variant<Value, std::exception_ptr> v) {
|
||||
bool leaf = std::holds_alternative<std::exception_ptr>(v) || std::get<Value>(v).type() != nAttrs ||
|
||||
ctx.state.isDerivation(std::get<Value>(v));
|
||||
if (!leaf) {
|
||||
return true; // Keep digging
|
||||
}
|
||||
f(path, v);
|
||||
return false;
|
||||
},
|
||||
ctx, *option, path);
|
||||
}
|
||||
|
||||
std::string describeError(const Error & e) { return "«error: " + e.msg() + "»"; }
|
||||
|
||||
void describeDerivation(Context & ctx, Out & out, Value v)
|
||||
{
|
||||
// Copy-pasted from nix/src/nix/repl.cc printDerivation() :(
|
||||
std::optional<nix::StorePath> storePath = std::nullopt;
|
||||
if (auto i = v.attrs()->get(ctx.state.sDrvPath)) {
|
||||
nix::NixStringContext context;
|
||||
storePath = ctx.state.coerceToStorePath(i->pos, *i->value, context, "while evaluating the drvPath of a derivation");
|
||||
}
|
||||
out << "«derivation ";
|
||||
if (storePath) {
|
||||
out << " " << ctx.state.store->printStorePath(*storePath);
|
||||
}
|
||||
out << "»";
|
||||
}
|
||||
|
||||
Value parseAndEval(EvalState & state, const std::string & expression, const std::string & path)
|
||||
{
|
||||
Value v{};
|
||||
state.eval(state.parseExprFromString(expression, state.rootPath(".")), v);
|
||||
return v;
|
||||
}
|
||||
|
||||
void printValue(Context & ctx, Out & out, std::variant<Value, std::exception_ptr> maybeValue, const std::string & path);
|
||||
|
||||
void printList(Context & ctx, Out & out, Value & v)
|
||||
{
|
||||
Out listOut(out, "[", "]", v.listSize());
|
||||
for (unsigned int n = 0; n < v.listSize(); ++n) {
|
||||
printValue(ctx, listOut, *v.listElems()[n], "");
|
||||
listOut << Out::sep;
|
||||
}
|
||||
}
|
||||
|
||||
void printAttrs(Context & ctx, Out & out, Value & v, const std::string & path)
|
||||
{
|
||||
Out attrsOut(out, "{", "}", v.attrs()->size());
|
||||
for (const auto & a : v.attrs()->lexicographicOrder(ctx.state.symbols)) {
|
||||
if (!forbiddenRecursionName(a->name, ctx.state.symbols)) {
|
||||
std::string_view name = ctx.state.symbols[a->name];
|
||||
attrsOut << name << " = ";
|
||||
printValue(ctx, attrsOut, *a->value, appendPath(path, name));
|
||||
attrsOut << ";" << Out::sep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void multiLineStringEscape(Out & out, const std::string_view & s)
|
||||
{
|
||||
size_t 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, const Value & v)
|
||||
{
|
||||
std::string_view s = v.string_view();
|
||||
Out strOut(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(strOut, s.substr(begin, s.size() - begin));
|
||||
break;
|
||||
}
|
||||
multiLineStringEscape(strOut, s.substr(begin, end - begin));
|
||||
strOut << Out::sep;
|
||||
begin = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void printValue(Context & ctx, Out & out, std::variant<Value, std::exception_ptr> maybeValue, const std::string & path)
|
||||
{
|
||||
try {
|
||||
if (auto ex = std::get_if<std::exception_ptr>(&maybeValue)) {
|
||||
std::rethrow_exception(*ex);
|
||||
}
|
||||
Value v = evaluateValue(ctx, std::get<Value>(maybeValue));
|
||||
if (ctx.state.isDerivation(v)) {
|
||||
describeDerivation(ctx, out, v);
|
||||
} else if (v.isList()) {
|
||||
printList(ctx, out, v);
|
||||
} else if (v.type() == nAttrs) {
|
||||
printAttrs(ctx, out, v, path);
|
||||
} else if (v.type() == nString && std::string(v.string_view()).find('\n') != std::string::npos) {
|
||||
printMultiLineString(out, v);
|
||||
} else {
|
||||
ctx.state.forceValueDeep(v);
|
||||
v.print(ctx.state, out.ostream);
|
||||
}
|
||||
} catch (ThrownError & e) {
|
||||
if (e.msg() == "The option `" + path + "' was accessed but has no value defined. Try setting the option.") {
|
||||
// 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-option, 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);
|
||||
}
|
||||
} catch (Error & e) {
|
||||
out << describeError(e);
|
||||
}
|
||||
}
|
||||
|
||||
void printConfigValue(Context & ctx, Out & out, const std::string & path, std::variant<Value, std::exception_ptr> v)
|
||||
{
|
||||
out << path << " = ";
|
||||
printValue(ctx, out, std::move(v), path);
|
||||
out << ";\n";
|
||||
}
|
||||
|
||||
// Replace with std::starts_with when C++20 is available
|
||||
bool starts_with(const std::string & s, const std::string & prefix)
|
||||
{
|
||||
return s.size() >= prefix.size() &&
|
||||
std::equal(s.begin(), std::next(s.begin(), prefix.size()), prefix.begin(), prefix.end());
|
||||
}
|
||||
|
||||
void printRecursive(Context & ctx, Out & out, const std::string & path)
|
||||
{
|
||||
mapOptions(
|
||||
[&ctx, &out, &path](const std::string & optionPath) {
|
||||
mapConfigValuesInOption(
|
||||
[&ctx, &out, &path](const std::string & configPath, std::variant<Value, std::exception_ptr> v) {
|
||||
if (starts_with(configPath, path)) {
|
||||
printConfigValue(ctx, out, configPath, v);
|
||||
}
|
||||
},
|
||||
optionPath, ctx);
|
||||
},
|
||||
ctx, path);
|
||||
}
|
||||
|
||||
void printAttr(Context & ctx, Out & out, const std::string & path, Value & root)
|
||||
{
|
||||
try {
|
||||
printValue(ctx, out, *findAlongAttrPath(ctx.state, path, ctx.autoArgs, root).first, path);
|
||||
} catch (Error & e) {
|
||||
out << describeError(e);
|
||||
}
|
||||
}
|
||||
|
||||
bool hasExample(Context & ctx, Value & option)
|
||||
{
|
||||
try {
|
||||
findAlongAttrPath(ctx.state, "example", ctx.autoArgs, option);
|
||||
return true;
|
||||
} catch (Error &) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void printOption(Context & ctx, Out & out, const std::string & path, Value & option)
|
||||
{
|
||||
out << "Value:\n";
|
||||
printAttr(ctx, out, path, ctx.configRoot);
|
||||
|
||||
out << "\n\nDefault:\n";
|
||||
printAttr(ctx, out, "default", option);
|
||||
|
||||
out << "\n\nType:\n";
|
||||
printAttr(ctx, out, "type.description", option);
|
||||
|
||||
if (hasExample(ctx, 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(Context & ctx, Out & out, Value & v)
|
||||
{
|
||||
out << "This attribute set contains:\n";
|
||||
for (const auto & a : v.attrs()->lexicographicOrder(ctx.state.symbols)) {
|
||||
const std::string_view & name = ctx.state.symbols[a->name];
|
||||
if (!name.empty() && name[0] != '_') {
|
||||
out << name << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void printOne(Context & ctx, Out & out, const std::string & path)
|
||||
{
|
||||
try {
|
||||
auto result = findAlongOptionPath(ctx, path);
|
||||
Value & option = result.option;
|
||||
option = evaluateValue(ctx, option);
|
||||
if (path != result.path) {
|
||||
out << "Note: showing " << result.path << " instead of " << path << "\n";
|
||||
}
|
||||
if (isOption(ctx, option)) {
|
||||
printOption(ctx, out, result.path, option);
|
||||
} else {
|
||||
printListing(ctx, 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 recursive = false;
|
||||
std::string path = ".";
|
||||
std::string optionsExpr = "(import <nixpkgs/nixos> {}).options";
|
||||
std::string configExpr = "(import <nixpkgs/nixos> {}).config";
|
||||
std::vector<std::string> args;
|
||||
|
||||
struct MyArgs : nix::LegacyArgs, nix::MixEvalArgs
|
||||
{
|
||||
using nix::LegacyArgs::LegacyArgs;
|
||||
};
|
||||
|
||||
MyArgs myArgs(std::string(nix::baseNameOf(argv[0])), [&](Strings::iterator & arg, const Strings::iterator & end) {
|
||||
if (*arg == "--help") {
|
||||
nix::showManPage("nixos-option");
|
||||
} else if (*arg == "--version") {
|
||||
nix::printVersion("nixos-option");
|
||||
} else if (*arg == "-r" || *arg == "--recursive") {
|
||||
recursive = true;
|
||||
} else if (*arg == "--path") {
|
||||
path = nix::getArg(*arg, arg, end);
|
||||
} else if (*arg == "--options_expr") {
|
||||
optionsExpr = nix::getArg(*arg, arg, end);
|
||||
} else if (*arg == "--config_expr") {
|
||||
configExpr = 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::initNix();
|
||||
nix::initGC();
|
||||
nix::settings.readOnlyMode = true;
|
||||
auto store = nix::openStore();
|
||||
|
||||
auto evalStore = myArgs.evalStoreUrl ? nix::openStore(*myArgs.evalStoreUrl)
|
||||
: nix::openStore();
|
||||
auto state = nix::make_ref<nix::EvalState>(
|
||||
myArgs.lookupPath, evalStore, nix::fetchSettings, nix::evalSettings);
|
||||
|
||||
Value optionsRoot = parseAndEval(*state, optionsExpr, path);
|
||||
Value configRoot = parseAndEval(*state, configExpr, path);
|
||||
|
||||
Context ctx{*state, *myArgs.getAutoArgs(*state), optionsRoot, configRoot};
|
||||
Out out(std::cout);
|
||||
|
||||
auto print = recursive ? printRecursive : printOne;
|
||||
if (args.empty()) {
|
||||
print(ctx, out, "");
|
||||
}
|
||||
for (const auto & arg : args) {
|
||||
print(ctx, out, arg);
|
||||
}
|
||||
|
||||
ctx.state.maybePrintStats();
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user