1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2025-07-05 08:11:47 +02:00

Revert flake-schemas for now

This commit is contained in:
Eelco Dolstra 2024-10-29 15:18:48 +01:00
parent 6ff6baaa35
commit ffcc42faf4
25 changed files with 827 additions and 730 deletions

View file

@ -37,7 +37,6 @@ checkbindir = @checkbindir@
checklibdir = @checklibdir@
datadir = @datadir@
datarootdir = @datarootdir@
default_flake_schemas = @default_flake_schemas@
docdir = @docdir@
embedded_sandbox_shell = @embedded_sandbox_shell@
exec_prefix = @exec_prefix@

View file

@ -428,12 +428,6 @@ if test "$embedded_sandbox_shell" = yes; then
AC_DEFINE(HAVE_EMBEDDED_SANDBOX_SHELL, 1, [Include the sandbox shell in the Nix binary.])
fi
AC_ARG_WITH(default-flake-schemas, AS_HELP_STRING([--with-default-flake-schemas=PATH],[path of the default flake schemas flake]),
default_flake_schemas=$withval,
[AC_MSG_FAILURE([--with-default-flake-schemas is missing])])
AC_SUBST(default_flake_schemas)
])

View file

@ -114,7 +114,6 @@
- [Store Path Specification](protocols/store-path.md)
- [Nix Archive (NAR) Format](protocols/nix-archive.md)
- [Derivation "ATerm" file format](protocols/derivation-aterm.md)
- [Flake Schemas](protocols/flake-schemas.md)
- [C API](c-api.md)
- [Glossary](glossary.md)
- [Development](development/index.md)

View file

@ -1,64 +0,0 @@
# Flake Schemas
Flake schemas are a mechanism to allow tools like `nix flake show` and `nix flake check` to enumerate and check the contents of a flake
in a generic way, without requiring built-in knowledge of specific flake output types like `packages` or `nixosConfigurations`.
A flake can define schemas for its outputs by defining a `schemas` output. `schemas` should be an attribute set with an attribute for
every output type that you want to be supported. If a flake does not have a `schemas` attribute, Nix uses a built-in set of schemas (namely https://github.com/DeterminateSystems/flake-schemas).
A schema is an attribute set with the following attributes:
| Attribute | Description | Default |
| :---------- | :---------------------------------------------------------------------------------------------- | :------ |
| `version` | Should be set to 1 | |
| `doc` | A string containing documentation about the flake output type in Markdown format. | |
| `allowIFD` | Whether the evaluation of the output attributes of this flake can read from derivation outputs. | `true` |
| `inventory` | A function that returns the contents of the flake output (described [below](#inventory)). | |
# Inventory
The `inventory` function returns a _node_ describing the contents of the flake output. A node is either a _leaf node_ or a _non-leaf node_. This allows nested flake output attributes to be described (e.g. `x86_64-linux.hello` inside a `packages` output).
Non-leaf nodes must have the following attribute:
| Attribute | Description |
| :--------- | :------------------------------------------------------------------------------------- |
| `children` | An attribute set of nodes. If this attribute is missing, the attribute is a leaf node. |
Leaf nodes can have the following attributes:
| Attribute | Description |
| :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `derivation` | The main derivation of this node, if any. It must evaluate for `nix flake check` and `nix flake show` to succeed. |
| `evalChecks` | An attribute set of Boolean values, used by `nix flake check`. Each attribute must evaluate to `true`. |
| `isFlakeCheck` | Whether `nix flake check` should build the `derivation` attribute of this node. |
| `shortDescription` | A one-sentence description of the node (such as the `meta.description` attribute in Nixpkgs). |
| `what` | A brief human-readable string describing the type of the node, e.g. `"package"` or `"development environment"`. This is used by tools like `nix flake show` to describe the contents of a flake. |
Both leaf and non-leaf nodes can have the following attributes:
| Attribute | Description |
| :----------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `forSystems` | A list of Nix system types (e.g. `["x86_64-linux"]`) supported by this node. This is used by tools to skip nodes that cannot be built on the user's system. Setting this on a non-leaf node allows all the children to be skipped, regardless of the `forSystems` attributes of the children. If this attribute is not set, the node is never skipped. |
# Example
Here is a schema that checks that every element of the `nixosConfigurations` flake output evaluates and builds correctly (meaning that it has a `config.system.build.toplevel` attribute that yields a buildable derivation).
```nix
outputs = {
schemas.nixosConfigurations = {
version = 1;
doc = ''
The `nixosConfigurations` flake output defines NixOS system configurations.
'';
inventory = output: {
children = builtins.mapAttrs (configName: machine:
{
what = "NixOS configuration";
derivation = machine.config.system.build.toplevel;
}) output;
};
};
};
```

29
flake.lock generated
View file

@ -36,21 +36,6 @@
"type": "github"
}
},
"flake-schemas": {
"locked": {
"lastModified": 1719857163,
"narHash": "sha256-wM+8JtoKBkahHiKn+EM1ikurMnitwRQrZ91hipJIJK8=",
"owner": "DeterminateSystems",
"repo": "flake-schemas",
"rev": "61a02d7183d4241962025e6c6307a22a0bb72a21",
"type": "github"
},
"original": {
"owner": "DeterminateSystems",
"repo": "flake-schemas",
"type": "github"
}
},
"git-hooks-nix": {
"inputs": {
"flake-compat": [],
@ -63,11 +48,11 @@
]
},
"locked": {
"lastModified": 1721042469,
"narHash": "sha256-6FPUl7HVtvRHCCBQne7Ylp4p+dpP3P/OYuzjztZ4s70=",
"lastModified": 1729104314,
"narHash": "sha256-pZRZsq5oCdJt3upZIU4aslS9XwFJ+/nVtALHIciX/BI=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "f451c19376071a90d8c58ab1a953c6e9840527fd",
"rev": "3c3e88f0f544d6bb54329832616af7eb971b6be6",
"type": "github"
},
"original": {
@ -79,16 +64,15 @@
"libgit2": {
"flake": false,
"locked": {
"lastModified": 1715853528,
"narHash": "sha256-J2rCxTecyLbbDdsyBWn9w7r3pbKRMkI9E7RvRgAqBdY=",
"lastModified": 1730025633,
"narHash": "sha256-HcL9fW5crHeLpP7C7vShO+j5fwY8z95Plr1c+hIwFRQ=",
"owner": "libgit2",
"repo": "libgit2",
"rev": "36f7e21ad757a3dacc58cf7944329da6bc1d6e96",
"rev": "b363ea4b9e761fed7942eef4bbc735ccf16f9fed",
"type": "github"
},
"original": {
"owner": "libgit2",
"ref": "v1.8.1",
"repo": "libgit2",
"type": "github"
}
@ -145,7 +129,6 @@
"inputs": {
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"flake-schemas": "flake-schemas",
"git-hooks-nix": "git-hooks-nix",
"libgit2": "libgit2",
"nixpkgs": "nixpkgs",

View file

@ -5,8 +5,7 @@
inputs.nixpkgs-regression.url = "github:NixOS/nixpkgs/215d4d0fd80ca5163643b03a33fde804a29cc1e2";
inputs.nixpkgs-23-11.url = "github:NixOS/nixpkgs/a62e6edd6d5e1fa0329b8653c801147986f8d446";
inputs.flake-compat = { url = "github:edolstra/flake-compat"; flake = false; };
inputs.libgit2 = { url = "github:libgit2/libgit2/v1.8.1"; flake = false; };
inputs.flake-schemas.url = "github:DeterminateSystems/flake-schemas";
inputs.libgit2 = { url = "github:libgit2/libgit2"; flake = false; };
# dev tooling
inputs.flake-parts.url = "github:hercules-ci/flake-parts";
@ -19,7 +18,8 @@
inputs.git-hooks-nix.inputs.flake-compat.follows = "";
inputs.git-hooks-nix.inputs.gitignore.follows = "";
outputs = inputs@{ self, nixpkgs, nixpkgs-regression, libgit2, flake-schemas, ... }:
outputs = inputs@{ self, nixpkgs, nixpkgs-regression, libgit2, ... }:
let
inherit (nixpkgs) lib;
@ -156,8 +156,6 @@
};
in {
schemas = flake-schemas.schemas;
# A Nixpkgs overlay that overrides the 'nix' and
# 'nix-perl-bindings' packages.
overlays.default = overlayFor (p: p.stdenv);

View file

@ -38,8 +38,6 @@
, busybox-sandbox-shell ? null
, flake-schemas
# Configuration Options
#:
# This probably seems like too many degrees of freedom, but it
@ -261,7 +259,6 @@ in {
(lib.enableFeature enableMarkdown "markdown")
(lib.enableFeature installUnitTests "install-unit-tests")
(lib.withFeatureAs true "readline-flavor" readlineFlavor)
"--with-default-flake-schemas=${flake-schemas}"
] ++ lib.optionals (!forDevShell) [
"--sysconfdir=/etc"
] ++ lib.optionals installUnitTests [

View file

@ -152,16 +152,5 @@ scope: {
inherit resolvePath filesetToSource;
mkMesonDerivation = f: let
exts = [
miscGoodPractice
bsdNoLinkAsNeeded
localSourceLayer
];
in stdenv.mkDerivation
(lib.extends
(lib.foldr lib.composeExtensions (_: _: {}) exts)
f);
inherit (inputs) flake-schemas;
mkMesonDerivation = f: stdenv.mkDerivation (lib.extends localSourceLayer f);
}

View file

@ -28,8 +28,6 @@ let
test-daemon = daemon;
doBuild = false;
inherit (inputs) flake-schemas;
};
# Technically we could just return `pkgs.nixComponents`, but for Hydra it's

View file

@ -43,6 +43,20 @@ std::vector<std::string> InstallableFlake::getActualAttrPaths()
return res;
}
Value * InstallableFlake::getFlakeOutputs(EvalState & state, const flake::LockedFlake & lockedFlake)
{
auto vFlake = state.allocValue();
callFlake(state, lockedFlake, *vFlake);
auto aOutputs = vFlake->attrs()->get(state.symbols.create("outputs"));
assert(aOutputs);
state.forceValue(*aOutputs->value, aOutputs->value->determinePos(noPos));
return aOutputs->value;
}
static std::string showAttrPaths(const std::vector<std::string> & paths)
{
std::string s;

View file

@ -53,6 +53,8 @@ struct InstallableFlake : InstallableValue
std::vector<std::string> getActualAttrPaths();
Value * getFlakeOutputs(EvalState & state, const flake::LockedFlake & lockedFlake);
DerivedPathsWithInfo toDerivedPaths() override;
std::pair<Value *, PosIdx> toValue(EvalState & state) override;

View file

@ -449,6 +449,11 @@ ref<eval_cache::EvalCache> openEvalCache(
: std::nullopt;
auto rootLoader = [&state, lockedFlake]()
{
/* For testing whether the evaluation cache is
complete. */
if (getEnv("NIX_ALLOW_EVAL").value_or("1") == "0")
throw Error("not everything is cached, but evaluation is not allowed");
auto vFlake = state.allocValue();
flake::callFlake(state, *lockedFlake, *vFlake);

View file

@ -368,12 +368,6 @@ Value * EvalCache::getRootValue()
{
if (!value) {
debug("getting root value");
/* For testing whether the evaluation cache is
complete. */
if (getEnv("NIX_ALLOW_EVAL").value_or("1") == "0")
throw Error("not everything is cached, but evaluation is not allowed");
value = allocRootValue(rootLoader());
}
return *value;

View file

@ -34,11 +34,7 @@ class EvalCache : public std::enable_shared_from_this<EvalCache>
friend struct CachedEvalError;
std::shared_ptr<AttrDb> db;
public:
EvalState & state;
private:
typedef std::function<Value *()> RootLoader;
RootLoader rootLoader;
RootValue value;
@ -93,10 +89,7 @@ class AttrCursor : public std::enable_shared_from_this<AttrCursor>
friend class EvalCache;
friend struct CachedEvalError;
public:
ref<EvalCache> root;
private:
typedef std::optional<std::pair<std::shared_ptr<AttrCursor>, Symbol>> Parent;
Parent parent;
RootValue _value;

View file

@ -204,7 +204,7 @@ static std::map<FlakeId, FlakeInput> parseFlakeInputs(
return inputs;
}
Flake readFlake(
static Flake readFlake(
EvalState & state,
const FlakeRef & originalRef,
const FlakeRef & resolvedRef,
@ -338,16 +338,20 @@ static LockFile readLockFile(
: LockFile();
}
/* Compute an in-memory lock file for the specified top-level flake,
and optionally write it to file, if the flake is writable. */
LockedFlake lockFlake(
const Settings & settings,
EvalState & state,
const FlakeRef & topRef,
const LockFlags & lockFlags,
Flake flake,
FlakeCache & flakeCache)
const LockFlags & lockFlags)
{
FlakeCache flakeCache;
auto useRegistries = lockFlags.useRegistries.value_or(settings.useRegistries);
auto flake = getFlake(state, topRef, useRegistries, flakeCache);
if (lockFlags.applyNixConfig) {
flake.config.apply(settings);
state.store->setOptions();
@ -738,30 +742,6 @@ LockedFlake lockFlake(
}
}
LockedFlake lockFlake(
const Settings & settings,
EvalState & state,
const FlakeRef & topRef,
const LockFlags & lockFlags)
{
FlakeCache flakeCache;
auto useRegistries = lockFlags.useRegistries.value_or(settings.useRegistries);
return lockFlake(settings, state, topRef, lockFlags, getFlake(state, topRef, useRegistries, flakeCache), flakeCache);
}
LockedFlake lockFlake(
const Settings & settings,
EvalState & state,
const FlakeRef & topRef,
const LockFlags & lockFlags,
Flake flake)
{
FlakeCache flakeCache;
return lockFlake(settings, state, topRef, lockFlags, std::move(flake), flakeCache);
}
void callFlake(EvalState & state,
const LockedFlake & lockedFlake,
Value & vRes)

View file

@ -203,31 +203,12 @@ struct LockFlags
std::set<InputPath> inputUpdates;
};
Flake readFlake(
EvalState & state,
const FlakeRef & originalRef,
const FlakeRef & resolvedRef,
const FlakeRef & lockedRef,
const SourcePath & rootDir,
const InputPath & lockRootPath);
/**
* Compute an in-memory lock file for the specified top-level flake,
* and optionally write it to file, if the flake is writable.
*/
LockedFlake lockFlake(
const Settings & settings,
EvalState & state,
const FlakeRef & flakeRef,
const LockFlags & lockFlags);
LockedFlake lockFlake(
const Settings & settings,
EvalState & state,
const FlakeRef & topRef,
const LockFlags & lockFlags,
Flake flake);
void callFlake(
EvalState & state,
const LockedFlake & lockedFlake,

View file

@ -1,43 +0,0 @@
/* The flake providing default schemas. */
defaultSchemasFlake:
/* The flake whose contents we want to extract. */
flake:
let
# Helper functions.
mapAttrsToList = f: attrs: map (name: f name attrs.${name}) (builtins.attrNames attrs);
in
rec {
outputNames = builtins.attrNames flake.outputs;
allSchemas = (flake.outputs.schemas or defaultSchemasFlake.schemas) // schemaOverrides;
schemaOverrides = {}; # FIXME
schemas =
builtins.listToAttrs (builtins.concatLists (mapAttrsToList
(outputName: output:
if allSchemas ? ${outputName} then
[{ name = outputName; value = allSchemas.${outputName}; }]
else
[ ])
flake.outputs));
inventory =
builtins.mapAttrs
(outputName: output:
if schemas ? ${outputName} && schemas.${outputName}.version == 1
then
{ output = schemas.${outputName}.inventory output;
inherit (schemas.${outputName}) doc;
}
else
{ unknown = true; }
)
flake.outputs;
}

View file

@ -18,20 +18,56 @@ R""(
# Description
This command verifies that the flake specified by flake reference
*flake-url* can be evaluated and built successfully according to its
`schemas` flake output. For every flake output that has a schema
definition, `nix flake check` uses the schema to extract the contents
of the output. Then, for every item in the contents:
* It evaluates the elements of the `evalChecks` attribute set returned
by the schema for that item, printing an error or warning for every
check that fails to evaluate or that evaluates to `false`.
* It builds `derivation` attribute returned by the schema for that
item, if the item has the `isFlakeCheck` attribute.
*flake-url* can be evaluated successfully (as detailed below), and
that the derivations specified by the flake's `checks` output can be
built successfully.
If the `keep-going` option is set to `true`, Nix will keep evaluating as much
as it can and report the errors as it encounters them. Otherwise it will stop
at the first error.
# Evaluation checks
The following flake output attributes must be derivations:
* `checks.`*system*`.`*name*
* `defaultPackage.`*system*
* `devShell.`*system*
* `devShells.`*system*`.`*name*
* `nixosConfigurations.`*name*`.config.system.build.toplevel`
* `packages.`*system*`.`*name*
The following flake output attributes must be [app
definitions](./nix3-run.md):
* `apps.`*system*`.`*name*
* `defaultApp.`*system*
The following flake output attributes must be [template
definitions](./nix3-flake-init.md):
* `defaultTemplate`
* `templates.`*name*
The following flake output attributes must be *Nixpkgs overlays*:
* `overlay`
* `overlays.`*name*
The following flake output attributes must be *NixOS modules*:
* `nixosModule`
* `nixosModules.`*name*
The following flake output attributes must be
[bundlers](./nix3-bundle.md):
* `bundlers.`*name*
* `defaultBundler`
In addition, the `hydraJobs` output is evaluated in the same way as
Hydra's `hydra-eval-jobs` (i.e. as a arbitrarily deeply nested
attribute set of derivations). Similarly, the
`legacyPackages`.*system* output is evaluated like `nix-env --query --available `.
)""

View file

@ -1,224 +0,0 @@
#include "flake-schemas.hh"
#include "eval-settings.hh"
#include "fetch-to-store.hh"
#include "memory-source-accessor.hh"
#include "strings-inline.hh"
namespace nix::flake_schemas {
using namespace eval_cache;
using namespace flake;
static LockedFlake getBuiltinDefaultSchemasFlake(EvalState & state)
{
auto accessor = make_ref<MemorySourceAccessor>();
accessor->setPathDisplay("«builtin-flake-schemas»");
accessor->addFile(
CanonPath("flake.nix"),
#include "builtin-flake-schemas.nix.gen.hh"
);
// FIXME: remove this when we have lazy trees.
auto storePath = fetchToStore(*state.store, {accessor}, FetchMode::Copy);
state.allowPath(storePath);
// Construct a dummy flakeref.
auto flakeRef = parseFlakeRef(
fetchSettings,
fmt("tarball+https://builtin-flake-schemas?narHash=%s",
state.store->queryPathInfo(storePath)->narHash.to_string(HashFormat::SRI, true)));
auto flake = readFlake(state, flakeRef, flakeRef, flakeRef, state.rootPath(state.store->toRealPath(storePath)), {});
return lockFlake(flakeSettings, state, flakeRef, {}, flake);
}
std::tuple<ref<EvalCache>, ref<eval_cache::AttrCursor>>
call(EvalState & state, std::shared_ptr<flake::LockedFlake> lockedFlake, std::optional<FlakeRef> defaultSchemasFlake)
{
auto fingerprint = lockedFlake->getFingerprint(state.store);
std::string callFlakeSchemasNix =
#include "call-flake-schemas.nix.gen.hh"
;
auto lockedDefaultSchemasFlake = defaultSchemasFlake
? flake::lockFlake(flakeSettings, state, *defaultSchemasFlake, {})
: getBuiltinDefaultSchemasFlake(state);
auto lockedDefaultSchemasFlakeFingerprint = lockedDefaultSchemasFlake.getFingerprint(state.store);
std::optional<Fingerprint> fingerprint2;
if (fingerprint && lockedDefaultSchemasFlakeFingerprint)
fingerprint2 = hashString(
HashAlgorithm::SHA256,
fmt("app:%s:%s:%s",
hashString(HashAlgorithm::SHA256, callFlakeSchemasNix).to_string(HashFormat::Base16, false),
fingerprint->to_string(HashFormat::Base16, false),
lockedDefaultSchemasFlakeFingerprint->to_string(HashFormat::Base16, false)));
// FIXME: merge with openEvalCache().
auto cache = make_ref<EvalCache>(
evalSettings.useEvalCache && evalSettings.pureEval ? fingerprint2 : std::nullopt,
state,
[&state, lockedFlake, callFlakeSchemasNix, lockedDefaultSchemasFlake]() {
auto vCallFlakeSchemas = state.allocValue();
state.eval(
state.parseExprFromString(callFlakeSchemasNix, state.rootPath(CanonPath::root)), *vCallFlakeSchemas);
auto vFlake = state.allocValue();
flake::callFlake(state, *lockedFlake, *vFlake);
auto vDefaultSchemasFlake = state.allocValue();
if (vFlake->type() == nAttrs && vFlake->attrs()->get(state.symbols.create("schemas")))
vDefaultSchemasFlake->mkNull();
else
flake::callFlake(state, lockedDefaultSchemasFlake, *vDefaultSchemasFlake);
auto vRes = state.allocValue();
Value * args[] = {vDefaultSchemasFlake, vFlake};
state.callFunction(*vCallFlakeSchemas, 2, args, *vRes, noPos);
return vRes;
});
return {cache, cache->getRoot()->getAttr("inventory")};
}
/* Derive the flake output attribute path from the cursor used to
traverse the inventory. We do this so we don't have to maintain a
separate attrpath for that. */
std::vector<Symbol> toAttrPath(ref<AttrCursor> cursor)
{
auto attrPath = cursor->getAttrPath();
std::vector<Symbol> res;
auto i = attrPath.begin();
assert(i != attrPath.end());
++i; // skip "inventory"
assert(i != attrPath.end());
res.push_back(*i++); // copy output name
if (i != attrPath.end())
++i; // skip "outputs"
while (i != attrPath.end()) {
++i; // skip "children"
if (i != attrPath.end())
res.push_back(*i++);
}
return res;
}
std::string toAttrPathStr(ref<AttrCursor> cursor)
{
return concatStringsSep(".", cursor->root->state.symbols.resolve(toAttrPath(cursor)));
}
void forEachOutput(
ref<AttrCursor> inventory,
std::function<void(Symbol outputName, std::shared_ptr<AttrCursor> output, const std::string & doc, bool isLast)> f)
{
// FIXME: handle non-IFD outputs first.
// evalSettings.enableImportFromDerivation.setDefault(false);
auto outputNames = inventory->getAttrs();
for (const auto & [i, outputName] : enumerate(outputNames)) {
auto output = inventory->getAttr(outputName);
try {
auto isUnknown = (bool) output->maybeGetAttr("unknown");
Activity act(*logger, lvlInfo, actUnknown, fmt("evaluating '%s'", toAttrPathStr(output)));
f(outputName,
isUnknown ? std::shared_ptr<AttrCursor>() : output->getAttr("output"),
isUnknown ? "" : output->getAttr("doc")->getString(),
i + 1 == outputNames.size());
} catch (Error & e) {
e.addTrace(nullptr, "while evaluating the flake output '%s':", toAttrPathStr(output));
throw;
}
}
}
void visit(
std::optional<std::string> system,
ref<AttrCursor> node,
std::function<void(ref<AttrCursor> leaf)> visitLeaf,
std::function<void(std::function<void(ForEachChild)>)> visitNonLeaf,
std::function<void(ref<AttrCursor> node, const std::vector<std::string> & systems)> visitFiltered)
{
Activity act(*logger, lvlInfo, actUnknown, fmt("evaluating '%s'", toAttrPathStr(node)));
/* Apply the system type filter. */
if (system) {
if (auto forSystems = node->maybeGetAttr("forSystems")) {
auto systems = forSystems->getListOfStrings();
if (std::find(systems.begin(), systems.end(), system) == systems.end()) {
visitFiltered(node, systems);
return;
}
}
}
if (auto children = node->maybeGetAttr("children")) {
visitNonLeaf([&](ForEachChild f) {
auto attrNames = children->getAttrs();
for (const auto & [i, attrName] : enumerate(attrNames)) {
try {
f(attrName, children->getAttr(attrName), i + 1 == attrNames.size());
} catch (Error & e) {
// FIXME: make it a flake schema attribute whether to ignore evaluation errors.
if (node->root->state.symbols[toAttrPath(node)[0]] != "legacyPackages") {
e.addTrace(nullptr, "while evaluating the flake output attribute '%s':", toAttrPathStr(node));
throw;
}
}
}
});
}
else
visitLeaf(ref(node));
}
std::optional<std::string> what(ref<AttrCursor> leaf)
{
if (auto what = leaf->maybeGetAttr("what"))
return what->getString();
else
return std::nullopt;
}
std::optional<std::string> shortDescription(ref<AttrCursor> leaf)
{
if (auto what = leaf->maybeGetAttr("shortDescription")) {
auto s = trim(what->getString());
if (s != "")
return s;
}
return std::nullopt;
}
std::shared_ptr<AttrCursor> derivation(ref<AttrCursor> leaf)
{
return leaf->maybeGetAttr("derivation");
}
MixFlakeSchemas::MixFlakeSchemas()
{
addFlag(
{.longName = "default-flake-schemas",
.description = "The URL of the flake providing default flake schema definitions.",
.labels = {"flake-ref"},
.handler = {&defaultFlakeSchemas},
.completer = {[&](AddCompletions & completions, size_t, std::string_view prefix) {
completeFlakeRef(completions, getStore(), prefix);
}}});
}
std::optional<FlakeRef> MixFlakeSchemas::getDefaultFlakeSchemas()
{
if (!defaultFlakeSchemas)
return std::nullopt;
else
return parseFlakeRef(fetchSettings, *defaultFlakeSchemas, absPath("."));
}
}

View file

@ -1,45 +0,0 @@
#include "eval-cache.hh"
#include "flake/flake.hh"
#include "command.hh"
namespace nix::flake_schemas {
using namespace eval_cache;
std::tuple<ref<eval_cache::EvalCache>, ref<AttrCursor>>
call(EvalState & state, std::shared_ptr<flake::LockedFlake> lockedFlake, std::optional<FlakeRef> defaultSchemasFlake);
std::vector<Symbol> toAttrPath(ref<AttrCursor> cursor);
std::string toAttrPathStr(ref<AttrCursor> cursor);
void forEachOutput(
ref<AttrCursor> inventory,
std::function<void(Symbol outputName, std::shared_ptr<AttrCursor> output, const std::string & doc, bool isLast)> f);
typedef std::function<void(Symbol attrName, ref<AttrCursor> attr, bool isLast)> ForEachChild;
void visit(
std::optional<std::string> system,
ref<AttrCursor> node,
std::function<void(ref<AttrCursor> leaf)> visitLeaf,
std::function<void(std::function<void(ForEachChild)>)> visitNonLeaf,
std::function<void(ref<AttrCursor> node, const std::vector<std::string> & systems)> visitFiltered);
std::optional<std::string> what(ref<AttrCursor> leaf);
std::optional<std::string> shortDescription(ref<AttrCursor> leaf);
std::shared_ptr<AttrCursor> derivation(ref<AttrCursor> leaf);
/* Some helper functions for processing flake schema output. */
struct MixFlakeSchemas : virtual Args, virtual StoreCommand
{
std::optional<std::string> defaultFlakeSchemas;
MixFlakeSchemas();
std::optional<FlakeRef> getDefaultFlakeSchemas();
};
}

View file

@ -17,7 +17,6 @@
#include "eval-cache.hh"
#include "markdown.hh"
#include "users.hh"
#include "flake-schemas.hh"
#include <nlohmann/json.hpp>
#include <iomanip>
@ -166,6 +165,31 @@ struct CmdFlakeLock : FlakeCommand
}
};
static void enumerateOutputs(EvalState & state, Value & vFlake,
std::function<void(std::string_view name, Value & vProvide, const PosIdx pos)> callback)
{
auto pos = vFlake.determinePos(noPos);
state.forceAttrs(vFlake, pos, "while evaluating a flake to get its outputs");
auto aOutputs = vFlake.attrs()->get(state.symbols.create("outputs"));
assert(aOutputs);
state.forceAttrs(*aOutputs->value, pos, "while evaluating the outputs of a flake");
auto sHydraJobs = state.symbols.create("hydraJobs");
/* Hack: ensure that hydraJobs is evaluated before anything
else. This way we can disable IFD for hydraJobs and then enable
it for other outputs. */
if (auto attr = aOutputs->value->attrs()->get(sHydraJobs))
callback(state.symbols[attr->name], *attr->value, attr->pos);
for (auto & attr : *aOutputs->value->attrs()) {
if (attr.name != sHydraJobs)
callback(state.symbols[attr.name], *attr.value, attr.pos);
}
}
struct CmdFlakeMetadata : FlakeCommand, MixJSON
{
std::string description() override
@ -296,7 +320,7 @@ struct CmdFlakeInfo : CmdFlakeMetadata
}
};
struct CmdFlakeCheck : FlakeCommand, flake_schemas::MixFlakeSchemas
struct CmdFlakeCheck : FlakeCommand
{
bool build = true;
bool checkAllSystems = false;
@ -337,26 +361,16 @@ struct CmdFlakeCheck : FlakeCommand, flake_schemas::MixFlakeSchemas
auto state = getEvalState();
lockFlags.applyNixConfig = true;
auto flake = std::make_shared<LockedFlake>(lockFlake());
auto flake = lockFlake();
auto localSystem = std::string(settings.thisSystem.get());
auto [cache, inventory] = flake_schemas::call(*state, flake, getDefaultFlakeSchemas());
std::vector<DerivedPath> drvPaths;
std::set<std::string> uncheckedOutputs;
std::set<std::string> omittedSystems;
std::function<void(ref<eval_cache::AttrCursor> node)> visit;
bool hasErrors = false;
auto reportError = [&](const Error & e) {
try {
throw e;
} catch (Error & e) {
if (settings.keepGoing) {
logError({.msg = e.info().msg});
ignoreException();
hasErrors = true;
}
else
@ -364,70 +378,428 @@ struct CmdFlakeCheck : FlakeCommand, flake_schemas::MixFlakeSchemas
}
};
visit = [&](ref<eval_cache::AttrCursor> node)
{
flake_schemas::visit(
checkAllSystems ? std::optional<std::string>() : localSystem,
node,
std::set<std::string> omittedSystems;
[&](ref<eval_cache::AttrCursor> leaf)
{
if (auto evalChecks = leaf->maybeGetAttr("evalChecks")) {
auto checkNames = evalChecks->getAttrs();
for (auto & checkName : checkNames) {
// FIXME: update activity
auto cursor = evalChecks->getAttr(checkName);
auto b = cursor->getBool();
if (!b)
reportError(Error("Evaluation check '%s' failed.", flake_schemas::toAttrPathStr(cursor)));
}
}
// FIXME: rewrite to use EvalCache.
if (auto drv = flake_schemas::derivation(leaf)) {
if (auto isFlakeCheck = leaf->maybeGetAttr("isFlakeCheck")) {
if (isFlakeCheck->getBool()) {
auto drvPath = drv->forceDerivation();
drvPaths.push_back(DerivedPath::Built {
.drvPath = makeConstantStorePathRef(drvPath),
.outputs = OutputsSpec::All { },
});
}
}
}
},
[&](std::function<void(flake_schemas::ForEachChild)> forEachChild)
{
forEachChild([&](Symbol attrName, ref<eval_cache::AttrCursor> node, bool isLast)
{
visit(node);
});
},
[&](ref<eval_cache::AttrCursor> node, const std::vector<std::string> & systems) {
for (auto & s : systems)
omittedSystems.insert(s);
});
auto resolve = [&] (PosIdx p) {
return state->positions[p];
};
flake_schemas::forEachOutput(inventory, [&](Symbol outputName, std::shared_ptr<eval_cache::AttrCursor> output, const std::string & doc, bool isLast)
{
if (output) {
visit(ref(output));
} else
uncheckedOutputs.insert(std::string(state->symbols[outputName]));
});
auto argHasName = [&] (Symbol arg, std::string_view expected) {
std::string_view name = state->symbols[arg];
return
name == expected
|| name == "_"
|| (hasPrefix(name, "_") && name.substr(1) == expected);
};
if (!uncheckedOutputs.empty())
warn("The following flake outputs are unchecked: %s.",
concatStringsSep(", ", uncheckedOutputs)); // FIXME: quote
auto checkSystemName = [&](std::string_view system, const PosIdx pos) {
// FIXME: what's the format of "system"?
if (system.find('-') == std::string::npos)
reportError(Error("'%s' is not a valid system type, at %s", system, resolve(pos)));
};
auto checkSystemType = [&](std::string_view system, const PosIdx pos) {
if (!checkAllSystems && system != localSystem) {
omittedSystems.insert(std::string(system));
return false;
} else {
return true;
}
};
auto checkDerivation = [&](const std::string & attrPath, Value & v, const PosIdx pos) -> std::optional<StorePath> {
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking derivation %s", attrPath));
auto packageInfo = getDerivation(*state, v, false);
if (!packageInfo)
throw Error("flake attribute '%s' is not a derivation", attrPath);
else {
// FIXME: check meta attributes
auto storePath = packageInfo->queryDrvPath();
if (storePath) {
logger->log(lvlInfo,
fmt("derivation evaluated to %s",
store->printStorePath(storePath.value())));
}
return storePath;
}
} catch (Error & e) {
e.addTrace(resolve(pos), HintFmt("while checking the derivation '%s'", attrPath));
reportError(e);
}
return std::nullopt;
};
std::vector<DerivedPath> drvPaths;
auto checkApp = [&](const std::string & attrPath, Value & v, const PosIdx pos) {
try {
#if 0
// FIXME
auto app = App(*state, v);
for (auto & i : app.context) {
auto [drvPathS, outputName] = NixStringContextElem::parse(i);
store->parseStorePath(drvPathS);
}
#endif
} catch (Error & e) {
e.addTrace(resolve(pos), HintFmt("while checking the app definition '%s'", attrPath));
reportError(e);
}
};
auto checkOverlay = [&](std::string_view attrPath, Value & v, const PosIdx pos) {
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking overlay '%s'", attrPath));
state->forceValue(v, pos);
if (!v.isLambda()) {
throw Error("overlay is not a function, but %s instead", showType(v));
}
if (v.payload.lambda.fun->hasFormals()
|| !argHasName(v.payload.lambda.fun->arg, "final"))
throw Error("overlay does not take an argument named 'final'");
// FIXME: if we have a 'nixpkgs' input, use it to
// evaluate the overlay.
} catch (Error & e) {
e.addTrace(resolve(pos), HintFmt("while checking the overlay '%s'", attrPath));
reportError(e);
}
};
auto checkModule = [&](std::string_view attrPath, Value & v, const PosIdx pos) {
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking NixOS module '%s'", attrPath));
state->forceValue(v, pos);
} catch (Error & e) {
e.addTrace(resolve(pos), HintFmt("while checking the NixOS module '%s'", attrPath));
reportError(e);
}
};
std::function<void(std::string_view attrPath, Value & v, const PosIdx pos)> checkHydraJobs;
checkHydraJobs = [&](std::string_view attrPath, Value & v, const PosIdx pos) {
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking Hydra job '%s'", attrPath));
state->forceAttrs(v, pos, "");
if (state->isDerivation(v))
throw Error("jobset should not be a derivation at top-level");
for (auto & attr : *v.attrs()) {
state->forceAttrs(*attr.value, attr.pos, "");
auto attrPath2 = concatStrings(attrPath, ".", state->symbols[attr.name]);
if (state->isDerivation(*attr.value)) {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking Hydra job '%s'", attrPath2));
checkDerivation(attrPath2, *attr.value, attr.pos);
} else
checkHydraJobs(attrPath2, *attr.value, attr.pos);
}
} catch (Error & e) {
e.addTrace(resolve(pos), HintFmt("while checking the Hydra jobset '%s'", attrPath));
reportError(e);
}
};
auto checkNixOSConfiguration = [&](const std::string & attrPath, Value & v, const PosIdx pos) {
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking NixOS configuration '%s'", attrPath));
Bindings & bindings(*state->allocBindings(0));
auto vToplevel = findAlongAttrPath(*state, "config.system.build.toplevel", bindings, v).first;
state->forceValue(*vToplevel, pos);
if (!state->isDerivation(*vToplevel))
throw Error("attribute 'config.system.build.toplevel' is not a derivation");
} catch (Error & e) {
e.addTrace(resolve(pos), HintFmt("while checking the NixOS configuration '%s'", attrPath));
reportError(e);
}
};
auto checkTemplate = [&](std::string_view attrPath, Value & v, const PosIdx pos) {
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking template '%s'", attrPath));
state->forceAttrs(v, pos, "");
if (auto attr = v.attrs()->get(state->symbols.create("path"))) {
if (attr->name == state->symbols.create("path")) {
NixStringContext context;
auto path = state->coerceToPath(attr->pos, *attr->value, context, "");
if (!path.pathExists())
throw Error("template '%s' refers to a non-existent path '%s'", attrPath, path);
// TODO: recursively check the flake in 'path'.
}
} else
throw Error("template '%s' lacks attribute 'path'", attrPath);
if (auto attr = v.attrs()->get(state->symbols.create("description")))
state->forceStringNoCtx(*attr->value, attr->pos, "");
else
throw Error("template '%s' lacks attribute 'description'", attrPath);
for (auto & attr : *v.attrs()) {
std::string_view name(state->symbols[attr.name]);
if (name != "path" && name != "description" && name != "welcomeText")
throw Error("template '%s' has unsupported attribute '%s'", attrPath, name);
}
} catch (Error & e) {
e.addTrace(resolve(pos), HintFmt("while checking the template '%s'", attrPath));
reportError(e);
}
};
auto checkBundler = [&](const std::string & attrPath, Value & v, const PosIdx pos) {
try {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking bundler '%s'", attrPath));
state->forceValue(v, pos);
if (!v.isLambda())
throw Error("bundler must be a function");
// TODO: check types of inputs/outputs?
} catch (Error & e) {
e.addTrace(resolve(pos), HintFmt("while checking the template '%s'", attrPath));
reportError(e);
}
};
{
Activity act(*logger, lvlInfo, actUnknown, "evaluating flake");
auto vFlake = state->allocValue();
flake::callFlake(*state, flake, *vFlake);
enumerateOutputs(*state,
*vFlake,
[&](std::string_view name, Value & vOutput, const PosIdx pos) {
Activity act(*logger, lvlInfo, actUnknown,
fmt("checking flake output '%s'", name));
try {
evalSettings.enableImportFromDerivation.setDefault(name != "hydraJobs");
state->forceValue(vOutput, pos);
std::string_view replacement =
name == "defaultPackage" ? "packages.<system>.default" :
name == "defaultApp" ? "apps.<system>.default" :
name == "defaultTemplate" ? "templates.default" :
name == "defaultBundler" ? "bundlers.<system>.default" :
name == "overlay" ? "overlays.default" :
name == "devShell" ? "devShells.<system>.default" :
name == "nixosModule" ? "nixosModules.default" :
"";
if (replacement != "")
warn("flake output attribute '%s' is deprecated; use '%s' instead", name, replacement);
if (name == "checks") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
state->forceAttrs(*attr.value, attr.pos, "");
for (auto & attr2 : *attr.value->attrs()) {
auto drvPath = checkDerivation(
fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]),
*attr2.value, attr2.pos);
if (drvPath && attr_name == settings.thisSystem.get()) {
drvPaths.push_back(DerivedPath::Built {
.drvPath = makeConstantStorePathRef(*drvPath),
.outputs = OutputsSpec::All { },
});
}
}
}
}
}
else if (name == "formatter") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
checkApp(
fmt("%s.%s", name, attr_name),
*attr.value, attr.pos);
};
}
}
else if (name == "packages" || name == "devShells") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
state->forceAttrs(*attr.value, attr.pos, "");
for (auto & attr2 : *attr.value->attrs())
checkDerivation(
fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]),
*attr2.value, attr2.pos);
};
}
}
else if (name == "apps") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
state->forceAttrs(*attr.value, attr.pos, "");
for (auto & attr2 : *attr.value->attrs())
checkApp(
fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]),
*attr2.value, attr2.pos);
};
}
}
else if (name == "defaultPackage" || name == "devShell") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
checkDerivation(
fmt("%s.%s", name, attr_name),
*attr.value, attr.pos);
};
}
}
else if (name == "defaultApp") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos) ) {
checkApp(
fmt("%s.%s", name, attr_name),
*attr.value, attr.pos);
};
}
}
else if (name == "legacyPackages") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
checkSystemName(state->symbols[attr.name], attr.pos);
checkSystemType(state->symbols[attr.name], attr.pos);
// FIXME: do getDerivations?
}
}
else if (name == "overlay")
checkOverlay(name, vOutput, pos);
else if (name == "overlays") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs())
checkOverlay(fmt("%s.%s", name, state->symbols[attr.name]),
*attr.value, attr.pos);
}
else if (name == "nixosModule")
checkModule(name, vOutput, pos);
else if (name == "nixosModules") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs())
checkModule(fmt("%s.%s", name, state->symbols[attr.name]),
*attr.value, attr.pos);
}
else if (name == "nixosConfigurations") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs())
checkNixOSConfiguration(fmt("%s.%s", name, state->symbols[attr.name]),
*attr.value, attr.pos);
}
else if (name == "hydraJobs")
checkHydraJobs(name, vOutput, pos);
else if (name == "defaultTemplate")
checkTemplate(name, vOutput, pos);
else if (name == "templates") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs())
checkTemplate(fmt("%s.%s", name, state->symbols[attr.name]),
*attr.value, attr.pos);
}
else if (name == "defaultBundler") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
checkBundler(
fmt("%s.%s", name, attr_name),
*attr.value, attr.pos);
};
}
}
else if (name == "bundlers") {
state->forceAttrs(vOutput, pos, "");
for (auto & attr : *vOutput.attrs()) {
const auto & attr_name = state->symbols[attr.name];
checkSystemName(attr_name, attr.pos);
if (checkSystemType(attr_name, attr.pos)) {
state->forceAttrs(*attr.value, attr.pos, "");
for (auto & attr2 : *attr.value->attrs()) {
checkBundler(
fmt("%s.%s.%s", name, attr_name, state->symbols[attr2.name]),
*attr2.value, attr2.pos);
}
};
}
}
else if (
name == "lib"
|| name == "darwinConfigurations"
|| name == "darwinModules"
|| name == "flakeModule"
|| name == "flakeModules"
|| name == "herculesCI"
|| name == "homeConfigurations"
|| name == "homeModule"
|| name == "homeModules"
|| name == "nixopsConfigurations"
)
// Known but unchecked community attribute
;
else
warn("unknown flake output '%s'", name);
} catch (Error & e) {
e.addTrace(resolve(pos), HintFmt("while checking flake output '%s'", name));
reportError(e);
}
});
}
if (build && !drvPaths.empty()) {
Activity act(*logger, lvlInfo, actUnknown,
fmt("running %d flake checks", drvPaths.size()));
store->buildPaths(drvPaths);
}
if (hasErrors)
throw Error("some errors were encountered during the evaluation");
@ -438,7 +810,7 @@ struct CmdFlakeCheck : FlakeCommand, flake_schemas::MixFlakeSchemas
"Use '--all-systems' to check all.",
concatStringsSep(", ", omittedSystems)
);
}
};
};
};
@ -723,7 +1095,7 @@ struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun
}
};
struct CmdFlakeShow : FlakeCommand, MixJSON, flake_schemas::MixFlakeSchemas
struct CmdFlakeShow : FlakeCommand, MixJSON
{
bool showLegacy = false;
bool showAllSystems = false;
@ -756,158 +1128,267 @@ struct CmdFlakeShow : FlakeCommand, MixJSON, flake_schemas::MixFlakeSchemas
void run(nix::ref<nix::Store> store) override
{
evalSettings.enableImportFromDerivation.setDefault(false);
auto state = getEvalState();
auto flake = std::make_shared<LockedFlake>(lockFlake());
auto localSystem = std::string(settings.thisSystem.get());
auto [cache, inventory] = flake_schemas::call(*state, flake, getDefaultFlakeSchemas());
std::function<bool(
eval_cache::AttrCursor & visitor,
const std::vector<Symbol> &attrPath,
const Symbol &attr)> hasContent;
if (json) {
std::function<void(ref<eval_cache::AttrCursor> node, nlohmann::json & obj)> visit;
// For frameworks it's important that structures are as lazy as possible
// to prevent infinite recursions, performance issues and errors that
// aren't related to the thing to evaluate. As a consequence, they have
// to emit more attributes than strictly (sic) necessary.
// However, these attributes with empty values are not useful to the user
// so we omit them.
hasContent = [&](
eval_cache::AttrCursor & visitor,
const std::vector<Symbol> &attrPath,
const Symbol &attr) -> bool
{
auto attrPath2(attrPath);
attrPath2.push_back(attr);
auto attrPathS = state->symbols.resolve(attrPath2);
const auto & attrName = state->symbols[attr];
visit = [&](ref<eval_cache::AttrCursor> node, nlohmann::json & obj)
{
flake_schemas::visit(
showAllSystems ? std::optional<std::string>() : localSystem,
node,
auto visitor2 = visitor.getAttr(attrName);
[&](ref<eval_cache::AttrCursor> leaf)
{
obj.emplace("leaf", true);
if (auto what = flake_schemas::what(leaf))
obj.emplace("what", what);
if (auto shortDescription = flake_schemas::shortDescription(leaf))
obj.emplace("shortDescription", shortDescription);
if (auto drv = flake_schemas::derivation(leaf))
obj.emplace("derivationName", drv->getAttr(state->sName)->getString());
// FIXME: add more stuff
},
[&](std::function<void(flake_schemas::ForEachChild)> forEachChild)
{
auto children = nlohmann::json::object();
forEachChild([&](Symbol attrName, ref<eval_cache::AttrCursor> node, bool isLast)
{
auto j = nlohmann::json::object();
try {
visit(node, j);
} catch (EvalError & e) {
// FIXME: make it a flake schema attribute whether to ignore evaluation errors.
if (node->root->state.symbols[flake_schemas::toAttrPath(node)[0]] == "legacyPackages")
j.emplace("failed", true);
else
throw;
}
children.emplace(state->symbols[attrName], std::move(j));
});
obj.emplace("children", std::move(children));
},
[&](ref<eval_cache::AttrCursor> node, const std::vector<std::string> & systems)
{
obj.emplace("filtered", true);
});
};
auto res = nlohmann::json::object();
flake_schemas::forEachOutput(inventory, [&](Symbol outputName, std::shared_ptr<eval_cache::AttrCursor> output, const std::string & doc, bool isLast)
{
auto j = nlohmann::json::object();
if (!showLegacy && state->symbols[outputName] == "legacyPackages") {
j.emplace("skipped", true);
} else if (output) {
j.emplace("doc", doc);
auto j2 = nlohmann::json::object();
visit(ref(output), j2);
j.emplace("output", std::move(j2));
} else
j.emplace("unknown", true);
res.emplace(state->symbols[outputName], j);
});
logger->cout("%s", res.dump());
}
else {
logger->cout(ANSI_BOLD "%s" ANSI_NORMAL, flake->flake.lockedRef);
std::function<void(
ref<eval_cache::AttrCursor> node,
const std::string & headerPrefix,
const std::string & prevPrefix)> visit;
visit = [&](
ref<eval_cache::AttrCursor> node,
const std::string & headerPrefix,
const std::string & prevPrefix)
{
flake_schemas::visit(
showAllSystems ? std::optional<std::string>() : localSystem,
node,
[&](ref<eval_cache::AttrCursor> leaf)
{
auto s = headerPrefix;
if (auto what = flake_schemas::what(leaf))
s += fmt(": %s", *what);
if (auto drv = flake_schemas::derivation(leaf))
s += fmt(ANSI_ITALIC " [%s]" ANSI_NORMAL, drv->getAttr(state->sName)->getString());
logger->cout(s);
},
[&](std::function<void(flake_schemas::ForEachChild)> forEachChild)
{
logger->cout(headerPrefix);
forEachChild([&](Symbol attrName, ref<eval_cache::AttrCursor> node, bool isLast)
{
visit(node,
fmt(ANSI_GREEN "%s%s" ANSI_NORMAL ANSI_BOLD "%s" ANSI_NORMAL, prevPrefix,
isLast ? treeLast : treeConn, state->symbols[attrName]),
prevPrefix + (isLast ? treeNull : treeLine));
});
},
[&](ref<eval_cache::AttrCursor> node, const std::vector<std::string> & systems)
{
logger->cout(fmt("%s " ANSI_WARNING "omitted" ANSI_NORMAL " (use '--all-systems' to show)", headerPrefix));
});
};
flake_schemas::forEachOutput(inventory, [&](Symbol outputName, std::shared_ptr<eval_cache::AttrCursor> output, const std::string & doc, bool isLast)
{
auto headerPrefix = fmt(
ANSI_GREEN "%s" ANSI_NORMAL ANSI_BOLD "%s" ANSI_NORMAL,
isLast ? treeLast : treeConn, state->symbols[outputName]);
if (!showLegacy && state->symbols[outputName] == "legacyPackages") {
logger->cout(headerPrefix);
logger->cout(
ANSI_GREEN "%s" "%s" ANSI_NORMAL ANSI_ITALIC "%s" ANSI_NORMAL,
isLast ? treeNull : treeLine,
treeLast,
"(skipped; use '--legacy' to show)");
} else if (output) {
visit(ref(output), headerPrefix, isLast ? treeNull : treeLine);
} else {
logger->cout(headerPrefix);
logger->cout(
ANSI_GREEN "%s" "%s" ANSI_NORMAL ANSI_ITALIC "%s" ANSI_NORMAL,
isLast ? treeNull : treeLine,
treeLast,
"(unknown flake output)");
try {
if ((attrPathS[0] == "apps"
|| attrPathS[0] == "checks"
|| attrPathS[0] == "devShells"
|| attrPathS[0] == "legacyPackages"
|| attrPathS[0] == "packages")
&& (attrPathS.size() == 1 || attrPathS.size() == 2)) {
for (const auto &subAttr : visitor2->getAttrs()) {
if (hasContent(*visitor2, attrPath2, subAttr)) {
return true;
}
}
return false;
}
});
}
if ((attrPathS.size() == 1)
&& (attrPathS[0] == "formatter"
|| attrPathS[0] == "nixosConfigurations"
|| attrPathS[0] == "nixosModules"
|| attrPathS[0] == "overlays"
)) {
for (const auto &subAttr : visitor2->getAttrs()) {
if (hasContent(*visitor2, attrPath2, subAttr)) {
return true;
}
}
return false;
}
// If we don't recognize it, it's probably content
return true;
} catch (EvalError & e) {
// Some attrs may contain errors, e.g. legacyPackages of
// nixpkgs. We still want to recurse into it, instead of
// skipping it at all.
return true;
}
};
std::function<nlohmann::json(
eval_cache::AttrCursor & visitor,
const std::vector<Symbol> & attrPath,
const std::string & headerPrefix,
const std::string & nextPrefix)> visit;
visit = [&](
eval_cache::AttrCursor & visitor,
const std::vector<Symbol> & attrPath,
const std::string & headerPrefix,
const std::string & nextPrefix)
-> nlohmann::json
{
auto j = nlohmann::json::object();
auto attrPathS = state->symbols.resolve(attrPath);
Activity act(*logger, lvlInfo, actUnknown,
fmt("evaluating '%s'", concatStringsSep(".", attrPathS)));
try {
auto recurse = [&]()
{
if (!json)
logger->cout("%s", headerPrefix);
std::vector<Symbol> attrs;
for (const auto &attr : visitor.getAttrs()) {
if (hasContent(visitor, attrPath, attr))
attrs.push_back(attr);
}
for (const auto & [i, attr] : enumerate(attrs)) {
const auto & attrName = state->symbols[attr];
bool last = i + 1 == attrs.size();
auto visitor2 = visitor.getAttr(attrName);
auto attrPath2(attrPath);
attrPath2.push_back(attr);
auto j2 = visit(*visitor2, attrPath2,
fmt(ANSI_GREEN "%s%s" ANSI_NORMAL ANSI_BOLD "%s" ANSI_NORMAL, nextPrefix, last ? treeLast : treeConn, attrName),
nextPrefix + (last ? treeNull : treeLine));
if (json) j.emplace(attrName, std::move(j2));
}
};
auto showDerivation = [&]()
{
auto name = visitor.getAttr(state->sName)->getString();
if (json) {
std::optional<std::string> description;
if (auto aMeta = visitor.maybeGetAttr(state->sMeta)) {
if (auto aDescription = aMeta->maybeGetAttr(state->sDescription))
description = aDescription->getString();
}
j.emplace("type", "derivation");
j.emplace("name", name);
if (description)
j.emplace("description", *description);
} else {
logger->cout("%s: %s '%s'",
headerPrefix,
attrPath.size() == 2 && attrPathS[0] == "devShell" ? "development environment" :
attrPath.size() >= 2 && attrPathS[0] == "devShells" ? "development environment" :
attrPath.size() == 3 && attrPathS[0] == "checks" ? "derivation" :
attrPath.size() >= 1 && attrPathS[0] == "hydraJobs" ? "derivation" :
"package",
name);
}
};
if (attrPath.size() == 0
|| (attrPath.size() == 1 && (
attrPathS[0] == "defaultPackage"
|| attrPathS[0] == "devShell"
|| attrPathS[0] == "formatter"
|| attrPathS[0] == "nixosConfigurations"
|| attrPathS[0] == "nixosModules"
|| attrPathS[0] == "defaultApp"
|| attrPathS[0] == "templates"
|| attrPathS[0] == "overlays"))
|| ((attrPath.size() == 1 || attrPath.size() == 2)
&& (attrPathS[0] == "checks"
|| attrPathS[0] == "packages"
|| attrPathS[0] == "devShells"
|| attrPathS[0] == "apps"))
)
{
recurse();
}
else if (
(attrPath.size() == 2 && (attrPathS[0] == "defaultPackage" || attrPathS[0] == "devShell" || attrPathS[0] == "formatter"))
|| (attrPath.size() == 3 && (attrPathS[0] == "checks" || attrPathS[0] == "packages" || attrPathS[0] == "devShells"))
)
{
if (!showAllSystems && std::string(attrPathS[1]) != localSystem) {
if (!json)
logger->cout(fmt("%s " ANSI_WARNING "omitted" ANSI_NORMAL " (use '--all-systems' to show)", headerPrefix));
else {
logger->warn(fmt("%s omitted (use '--all-systems' to show)", concatStringsSep(".", attrPathS)));
}
} else {
if (visitor.isDerivation())
showDerivation();
else
throw Error("expected a derivation");
}
}
else if (attrPath.size() > 0 && attrPathS[0] == "hydraJobs") {
if (visitor.isDerivation())
showDerivation();
else
recurse();
}
else if (attrPath.size() > 0 && attrPathS[0] == "legacyPackages") {
if (attrPath.size() == 1)
recurse();
else if (!showLegacy){
if (!json)
logger->cout(fmt("%s " ANSI_WARNING "omitted" ANSI_NORMAL " (use '--legacy' to show)", headerPrefix));
else {
logger->warn(fmt("%s omitted (use '--legacy' to show)", concatStringsSep(".", attrPathS)));
}
} else if (!showAllSystems && std::string(attrPathS[1]) != localSystem) {
if (!json)
logger->cout(fmt("%s " ANSI_WARNING "omitted" ANSI_NORMAL " (use '--all-systems' to show)", headerPrefix));
else {
logger->warn(fmt("%s omitted (use '--all-systems' to show)", concatStringsSep(".", attrPathS)));
}
} else {
if (visitor.isDerivation())
showDerivation();
else if (attrPath.size() <= 2)
// FIXME: handle recurseIntoAttrs
recurse();
}
}
else if (
(attrPath.size() == 2 && attrPathS[0] == "defaultApp") ||
(attrPath.size() == 3 && attrPathS[0] == "apps"))
{
auto aType = visitor.maybeGetAttr("type");
if (!aType || aType->getString() != "app")
state->error<EvalError>("not an app definition").debugThrow();
if (json) {
j.emplace("type", "app");
} else {
logger->cout("%s: app", headerPrefix);
}
}
else if (
(attrPath.size() == 1 && attrPathS[0] == "defaultTemplate") ||
(attrPath.size() == 2 && attrPathS[0] == "templates"))
{
auto description = visitor.getAttr("description")->getString();
if (json) {
j.emplace("type", "template");
j.emplace("description", description);
} else {
logger->cout("%s: template: " ANSI_BOLD "%s" ANSI_NORMAL, headerPrefix, description);
}
}
else {
auto [type, description] =
(attrPath.size() == 1 && attrPathS[0] == "overlay")
|| (attrPath.size() == 2 && attrPathS[0] == "overlays") ? std::make_pair("nixpkgs-overlay", "Nixpkgs overlay") :
attrPath.size() == 2 && attrPathS[0] == "nixosConfigurations" ? std::make_pair("nixos-configuration", "NixOS configuration") :
(attrPath.size() == 1 && attrPathS[0] == "nixosModule")
|| (attrPath.size() == 2 && attrPathS[0] == "nixosModules") ? std::make_pair("nixos-module", "NixOS module") :
std::make_pair("unknown", "unknown");
if (json) {
j.emplace("type", type);
} else {
logger->cout("%s: " ANSI_WARNING "%s" ANSI_NORMAL, headerPrefix, description);
}
}
} catch (EvalError & e) {
if (!(attrPath.size() > 0 && attrPathS[0] == "legacyPackages"))
throw;
}
return j;
};
auto cache = openEvalCache(*state, flake);
auto j = visit(*cache->getRoot(), {}, fmt(ANSI_BOLD "%s" ANSI_NORMAL, flake->flake.lockedRef), "");
if (json)
logger->cout("%s", j.dump());
}
};

View file

@ -55,9 +55,3 @@ $(d)/main.cc: \
$(d)/profile.cc: $(d)/profile.md
$(d)/profile.md: $(d)/profiles.md.gen.hh
src/nix/flake.cc: src/nix/call-flake-schemas.nix.gen.hh src/nix/builtin-flake-schemas.nix.gen.hh
src/nix/builtin-flake-schemas.nix: $(default_flake_schemas)/flake.nix
$(trace-gen) cp $^ $@
@chmod +w $@

View file

@ -16,6 +16,17 @@ EOF
nix flake check $flakeDir
cat > $flakeDir/flake.nix <<EOF
{
outputs = { self }: {
overlay = finalll: prev: {
};
};
}
EOF
(! nix flake check $flakeDir)
cat > $flakeDir/flake.nix <<EOF
{
outputs = { self, ... }: {

View file

@ -15,9 +15,9 @@ nix flake show --json > show-output.json
nix eval --impure --expr '
let show_output = builtins.fromJSON (builtins.readFile ./show-output.json);
in
assert show_output.packages.output.children.someOtherSystem.filtered;
assert show_output.packages.output.children.${builtins.currentSystem}.children.default.derivationName == "simple";
assert show_output.legacyPackages.skipped;
assert show_output.packages.someOtherSystem.default == {};
assert show_output.packages.${builtins.currentSystem}.default.name == "simple";
assert show_output.legacyPackages.${builtins.currentSystem} == {};
true
'
@ -26,8 +26,8 @@ nix flake show --json --all-systems > show-output.json
nix eval --impure --expr '
let show_output = builtins.fromJSON (builtins.readFile ./show-output.json);
in
assert show_output.packages.output.children.someOtherSystem.children.default.derivationName == "simple";
assert show_output.legacyPackages.skipped;
assert show_output.packages.someOtherSystem.default.name == "simple";
assert show_output.legacyPackages.${builtins.currentSystem} == {};
true
'
@ -36,7 +36,34 @@ nix flake show --json --legacy > show-output.json
nix eval --impure --expr '
let show_output = builtins.fromJSON (builtins.readFile ./show-output.json);
in
assert show_output.legacyPackages.output.children.${builtins.currentSystem}.children.hello.derivationName == "simple";
assert show_output.legacyPackages.${builtins.currentSystem}.hello.name == "simple";
true
'
# Test that attributes are only reported when they have actual content
cat >flake.nix <<EOF
{
description = "Bla bla";
outputs = inputs: rec {
apps.$system = { };
checks.$system = { };
devShells.$system = { };
legacyPackages.$system = { };
packages.$system = { };
packages.someOtherSystem = { };
formatter = { };
nixosConfigurations = { };
nixosModules = { };
};
}
EOF
nix flake show --json --all-systems > show-output.json
nix eval --impure --expr '
let show_output = builtins.fromJSON (builtins.readFile ./show-output.json);
in
assert show_output == { };
true
'
@ -56,7 +83,7 @@ nix flake show --json --legacy --all-systems > show-output.json
nix eval --impure --expr '
let show_output = builtins.fromJSON (builtins.readFile ./show-output.json);
in
assert show_output.legacyPackages.output.children.${builtins.currentSystem}.children.AAAAAASomeThingsFailToEvaluate.failed;
assert show_output.legacyPackages.output.children.${builtins.currentSystem}.children.simple.derivationName == "simple";
assert show_output.legacyPackages.${builtins.currentSystem}.AAAAAASomeThingsFailToEvaluate == { };
assert show_output.legacyPackages.${builtins.currentSystem}.simple.name == "simple";
true
'

View file

@ -32,6 +32,4 @@ cat << EOF > flake.nix
EOF
nix fmt ./file ./folder | grep 'Formatting: ./file ./folder'
nix flake check
clearStore
nix flake show | grep -P "package.*\[formatter\]"
nix flake show | grep -P "package 'formatter'"