diff options
author | John Ericson <John.Ericson@Obsidian.Systems> | 2020-07-16 17:28:52 +0000 |
---|---|---|
committer | John Ericson <John.Ericson@Obsidian.Systems> | 2020-07-16 17:28:52 +0000 |
commit | cc0d77f8c9c71f5870bcaf3d0c041b6ba4c1bf63 (patch) | |
tree | 1776c2a44d9e32a6842f310c38f362bf59bb1ffb /src/nix | |
parent | c466cb2091b7a382aba1739c0320f2b6c0a04c1e (diff) | |
parent | 5ea817dace2b554e602d7f9df6e43084ad112e3d (diff) |
Merge branch 'hash-always-has-type' of github.com:obsidiansystems/nix into better-ca-parse-errors
Diffstat (limited to 'src/nix')
-rw-r--r-- | src/nix/app.cc | 46 | ||||
-rw-r--r-- | src/nix/build.cc | 6 | ||||
-rw-r--r-- | src/nix/cat.cc | 12 | ||||
-rw-r--r-- | src/nix/command.cc | 5 | ||||
-rw-r--r-- | src/nix/command.hh | 110 | ||||
-rw-r--r-- | src/nix/copy.cc | 13 | ||||
-rw-r--r-- | src/nix/develop.cc | 37 | ||||
-rw-r--r-- | src/nix/diff-closures.cc | 134 | ||||
-rw-r--r-- | src/nix/edit.cc | 2 | ||||
-rw-r--r-- | src/nix/eval.cc | 36 | ||||
-rw-r--r-- | src/nix/flake.cc | 955 | ||||
-rw-r--r-- | src/nix/get-env.sh | 9 | ||||
-rw-r--r-- | src/nix/hash.cc | 6 | ||||
-rw-r--r-- | src/nix/installables.cc | 734 | ||||
-rw-r--r-- | src/nix/installables.hh | 88 | ||||
-rw-r--r-- | src/nix/log.cc | 4 | ||||
-rw-r--r-- | src/nix/ls.cc | 12 | ||||
-rw-r--r-- | src/nix/main.cc | 23 | ||||
-rw-r--r-- | src/nix/make-content-addressable.cc | 4 | ||||
-rw-r--r-- | src/nix/path-info.cc | 2 | ||||
-rw-r--r-- | src/nix/profile.cc | 428 | ||||
-rw-r--r-- | src/nix/registry.cc | 150 | ||||
-rw-r--r-- | src/nix/repl.cc | 7 | ||||
-rw-r--r-- | src/nix/run.cc | 74 | ||||
-rw-r--r-- | src/nix/search.cc | 235 | ||||
-rw-r--r-- | src/nix/show-derivation.cc | 2 | ||||
-rw-r--r-- | src/nix/sigs.cc | 3 | ||||
-rw-r--r-- | src/nix/verify.cc | 7 | ||||
-rw-r--r-- | src/nix/why-depends.cc | 26 |
29 files changed, 2768 insertions, 402 deletions
diff --git a/src/nix/app.cc b/src/nix/app.cc new file mode 100644 index 000000000..3935297cf --- /dev/null +++ b/src/nix/app.cc @@ -0,0 +1,46 @@ +#include "installables.hh" +#include "store-api.hh" +#include "eval-inline.hh" +#include "eval-cache.hh" +#include "names.hh" + +namespace nix { + +App Installable::toApp(EvalState & state) +{ + auto [cursor, attrPath] = getCursor(state, true); + + auto type = cursor->getAttr("type")->getString(); + + if (type == "app") { + auto [program, context] = cursor->getAttr("program")->getStringWithContext(); + + if (!state.store->isInStore(program)) + throw Error("app program '%s' is not in the Nix store", program); + + std::vector<StorePathWithOutputs> context2; + for (auto & [path, name] : context) + context2.push_back({state.store->parseStorePath(path), {name}}); + + return App { + .context = std::move(context2), + .program = program, + }; + } + + else if (type == "derivation") { + auto drvPath = cursor->forceDerivation(); + auto outPath = cursor->getAttr(state.sOutPath)->getString(); + auto outputName = cursor->getAttr(state.sOutputName)->getString(); + auto name = cursor->getAttr(state.sName)->getString(); + return App { + .context = { { drvPath, {outputName} } }, + .program = outPath + "/bin/" + DrvName(name).name, + }; + } + + else + throw Error("attribute '%s' has unsupported type '%s'", attrPath, type); +} + +} diff --git a/src/nix/build.cc b/src/nix/build.cc index 850e09ce8..0f7e0e123 100644 --- a/src/nix/build.cc +++ b/src/nix/build.cc @@ -1,3 +1,4 @@ +#include "eval.hh" #include "command.hh" #include "common-args.hh" #include "shared.hh" @@ -17,6 +18,7 @@ struct CmdBuild : InstallablesCommand, MixDryRun, MixProfile .description = "path of the symlink to the build result", .labels = {"path"}, .handler = {&outLink}, + .completer = completePath }); addFlag({ @@ -44,14 +46,14 @@ struct CmdBuild : InstallablesCommand, MixDryRun, MixProfile }, Example{ "To make a profile point at GNU Hello:", - "nix build --profile /tmp/profile nixpkgs.hello" + "nix build --profile /tmp/profile nixpkgs#hello" }, }; } void run(ref<Store> store) override { - auto buildables = build(store, dryRun ? DryRun : Build, installables); + auto buildables = build(store, dryRun ? Realise::Nothing : Realise::Outputs, installables); if (dryRun) return; diff --git a/src/nix/cat.cc b/src/nix/cat.cc index c82819af8..97306107c 100644 --- a/src/nix/cat.cc +++ b/src/nix/cat.cc @@ -25,7 +25,11 @@ struct CmdCatStore : StoreCommand, MixCat { CmdCatStore() { - expectArg("path", &path); + expectArgs({ + .label = "path", + .handler = {&path}, + .completer = completePath + }); } std::string description() override @@ -47,7 +51,11 @@ struct CmdCatNar : StoreCommand, MixCat CmdCatNar() { - expectArg("nar", &narPath); + expectArgs({ + .label = "nar", + .handler = {&narPath}, + .completer = completePath + }); expectArg("path", &path); } diff --git a/src/nix/command.cc b/src/nix/command.cc index 3651a9e9c..af36dda89 100644 --- a/src/nix/command.cc +++ b/src/nix/command.cc @@ -63,7 +63,7 @@ void StorePathsCommand::run(ref<Store> store) } else { - for (auto & p : toStorePaths(store, realiseMode, installables)) + for (auto & p : toStorePaths(store, realiseMode, operateOn, installables)) storePaths.push_back(p); if (recursive) { @@ -80,7 +80,7 @@ void StorePathsCommand::run(ref<Store> store) void StorePathCommand::run(ref<Store> store) { - auto storePaths = toStorePaths(store, NoBuild, installables); + auto storePaths = toStorePaths(store, Realise::Nothing, operateOn, installables); if (storePaths.size() != 1) throw UsageError("this command requires exactly one store path"); @@ -108,6 +108,7 @@ MixProfile::MixProfile() .description = "profile to update", .labels = {"path"}, .handler = {&profile}, + .completer = completePath }); } diff --git a/src/nix/command.hh b/src/nix/command.hh index 959d5f19d..1c7413300 100644 --- a/src/nix/command.hh +++ b/src/nix/command.hh @@ -4,12 +4,18 @@ #include "args.hh" #include "common-eval-args.hh" #include "path.hh" -#include "eval.hh" +#include "flake/lockfile.hh" + +#include <optional> namespace nix { extern std::string programPath; +class EvalState; +struct Pos; +class Store; + static constexpr Command::Category catSecondary = 100; static constexpr Command::Category catUtility = 101; static constexpr Command::Category catNixInstallation = 102; @@ -27,28 +33,64 @@ private: std::shared_ptr<Store> _store; }; -struct SourceExprCommand : virtual StoreCommand, MixEvalArgs +struct EvalCommand : virtual StoreCommand, MixEvalArgs +{ + ref<EvalState> getEvalState(); + + std::shared_ptr<EvalState> evalState; +}; + +struct MixFlakeOptions : virtual Args, EvalCommand { - Path file; + flake::LockFlags lockFlags; + + MixFlakeOptions(); + + virtual std::optional<FlakeRef> getFlakeRefForCompletion() + { return {}; } +}; + +/* How to handle derivations in commands that operate on store paths. */ +enum class OperateOn { + /* Operate on the output path. */ + Output, + /* Operate on the .drv path. */ + Derivation +}; + +struct SourceExprCommand : virtual Args, MixFlakeOptions +{ + std::optional<Path> file; + std::optional<std::string> expr; + + // FIXME: move this; not all commands (e.g. 'nix run') use it. + OperateOn operateOn = OperateOn::Output; SourceExprCommand(); - /* Return a value representing the Nix expression from which we - are installing. This is either the file specified by ‘--file’, - or an attribute set constructed from $NIX_PATH, e.g. ‘{ nixpkgs - = import ...; bla = import ...; }’. */ - Value * getSourceExpr(EvalState & state); + std::vector<std::shared_ptr<Installable>> parseInstallables( + ref<Store> store, std::vector<std::string> ss); - ref<EvalState> getEvalState(); + std::shared_ptr<Installable> parseInstallable( + ref<Store> store, const std::string & installable); -private: + virtual Strings getDefaultFlakeAttrPaths(); - std::shared_ptr<EvalState> evalState; + virtual Strings getDefaultFlakeAttrPathPrefixes(); - RootValue vSourceExpr; + void completeInstallable(std::string_view prefix); }; -enum RealiseMode { Build, NoBuild, DryRun }; +enum class Realise { + /* Build the derivation. Postcondition: the + derivation outputs exist. */ + Outputs, + /* Don't build the derivation. Postcondition: the store derivation + exists. */ + Derivation, + /* Evaluate in dry-run mode. Postcondition: nothing. */ + Nothing +}; /* A command that operates on a list of "installables", which can be store paths, attribute paths, Nix expressions, etc. */ @@ -56,15 +98,14 @@ struct InstallablesCommand : virtual Args, SourceExprCommand { std::vector<std::shared_ptr<Installable>> installables; - InstallablesCommand() - { - expectArgs("installables", &_installables); - } + InstallablesCommand(); void prepare() override; virtual bool useDefaultInstallables() { return true; } + std::optional<FlakeRef> getFlakeRefForCompletion() override; + private: std::vector<std::string> _installables; @@ -75,16 +116,18 @@ struct InstallableCommand : virtual Args, SourceExprCommand { std::shared_ptr<Installable> installable; - InstallableCommand() - { - expectArg("installable", &_installable); - } + InstallableCommand(); void prepare() override; + std::optional<FlakeRef> getFlakeRefForCompletion() override + { + return parseFlakeRef(_installable, absPath(".")); + } + private: - std::string _installable; + std::string _installable{"."}; }; /* A command that operates on zero or more store paths. */ @@ -97,7 +140,7 @@ private: protected: - RealiseMode realiseMode = NoBuild; + Realise realiseMode = Realise::Derivation; public: @@ -141,17 +184,15 @@ static RegisterCommand registerCommand(const std::string & name) return RegisterCommand(name, [](){ return make_ref<T>(); }); } -std::shared_ptr<Installable> parseInstallable( - SourceExprCommand & cmd, ref<Store> store, const std::string & installable, - bool useDefaultInstallables); - -Buildables build(ref<Store> store, RealiseMode mode, +Buildables build(ref<Store> store, Realise mode, std::vector<std::shared_ptr<Installable>> installables); -std::set<StorePath> toStorePaths(ref<Store> store, RealiseMode mode, +std::set<StorePath> toStorePaths(ref<Store> store, + Realise mode, OperateOn operateOn, std::vector<std::shared_ptr<Installable>> installables); -StorePath toStorePath(ref<Store> store, RealiseMode mode, +StorePath toStorePath(ref<Store> store, + Realise mode, OperateOn operateOn, std::shared_ptr<Installable> installable); std::set<StorePath> toDerivations(ref<Store> store, @@ -194,4 +235,13 @@ struct MixEnvironment : virtual Args { void setEnviron(); }; +void completeFlakeRef(ref<Store> store, std::string_view prefix); + +void completeFlakeRefWithFragment( + ref<EvalState> evalState, + flake::LockFlags lockFlags, + Strings attrPathPrefixes, + const Strings & defaultFlakeAttrPaths, + std::string_view prefix); + } diff --git a/src/nix/copy.cc b/src/nix/copy.cc index 64099f476..811c8ab34 100644 --- a/src/nix/copy.cc +++ b/src/nix/copy.cc @@ -45,6 +45,8 @@ struct CmdCopy : StorePathsCommand .description = "whether to try substitutes on the destination store (only supported by SSH)", .handler = {&substitute, Substitute}, }); + + realiseMode = Realise::Outputs; } std::string description() override @@ -70,11 +72,11 @@ struct CmdCopy : StorePathsCommand #ifdef ENABLE_S3 Example{ "To copy Hello to an S3 binary cache:", - "nix copy --to s3://my-bucket?region=eu-west-1 nixpkgs.hello" + "nix copy --to s3://my-bucket?region=eu-west-1 nixpkgs#hello" }, Example{ "To copy Hello to an S3-compatible binary cache:", - "nix copy --to s3://my-bucket?region=eu-west-1&endpoint=example.com nixpkgs.hello" + "nix copy --to s3://my-bucket?region=eu-west-1&endpoint=example.com nixpkgs#hello" }, #endif }; @@ -87,11 +89,16 @@ struct CmdCopy : StorePathsCommand return srcUri.empty() ? StoreCommand::createStore() : openStore(srcUri); } - void run(ref<Store> srcStore, StorePaths storePaths) override + void run(ref<Store> store) override { if (srcUri.empty() && dstUri.empty()) throw UsageError("you must pass '--from' and/or '--to'"); + StorePathsCommand::run(store); + } + + void run(ref<Store> srcStore, StorePaths storePaths) override + { ref<Store> dstStore = dstUri.empty() ? openStore() : openStore(dstUri); copyPaths(srcStore, dstStore, StorePathSet(storePaths.begin(), storePaths.end()), diff --git a/src/nix/develop.cc b/src/nix/develop.cc index eb93f56fc..a6d7d6add 100644 --- a/src/nix/develop.cc +++ b/src/nix/develop.cc @@ -130,7 +130,9 @@ StorePath getDerivationEnvironment(ref<Store> store, const StorePath & drvPath) drvName += "-env"; for (auto & output : drv.outputs) drv.env.erase(output.first); + drv.outputs = {{"out", DerivationOutput { .path = StorePath::dummy }}}; drv.env["out"] = ""; + drv.env["_outputs_saved"] = drv.env["outputs"]; drv.env["outputs"] = "out"; drv.inputSrcs.insert(std::move(getEnvShPath)); Hash h = hashDerivationModulo(*store, drv, true); @@ -201,6 +203,11 @@ struct Common : InstallableCommand, MixProfile out << "eval \"$shellHook\"\n"; } + Strings getDefaultFlakeAttrPaths() override + { + return {"devShell." + settings.thisSystem.get(), "defaultPackage." + settings.thisSystem.get()}; + } + StorePath getShellOutPath(ref<Store> store) { auto path = installable->getStorePath(); @@ -259,11 +266,15 @@ struct CmdDevelop : Common, MixEnvironment return { Example{ "To get the build environment of GNU hello:", - "nix develop nixpkgs.hello" + "nix develop nixpkgs#hello" + }, + Example{ + "To get the build environment of the default package of flake in the current directory:", + "nix develop" }, Example{ "To store the build environment in a profile:", - "nix develop --profile /tmp/my-shell nixpkgs.hello" + "nix develop --profile /tmp/my-shell nixpkgs#hello" }, Example{ "To use a build environment previously recorded in a profile:", @@ -294,12 +305,28 @@ struct CmdDevelop : Common, MixEnvironment stopProgressBar(); - auto shell = getEnv("SHELL").value_or("bash"); - setEnviron(); // prevent garbage collection until shell exits setenv("NIX_GCROOT", gcroot.data(), 1); + Path shell = "bash"; + + try { + auto state = getEvalState(); + + auto bashInstallable = std::make_shared<InstallableFlake>( + state, + std::move(installable->nixpkgsFlakeRef()), + Strings{"bashInteractive"}, + Strings{"legacyPackages." + settings.thisSystem.get() + "."}, + lockFlags); + + shell = state->store->printStorePath( + toStorePath(state->store, Realise::Outputs, OperateOn::Output, bashInstallable)) + "/bin/bash"; + } catch (Error &) { + ignoreException(); + } + auto args = Strings{std::string(baseNameOf(shell)), "--rcfile", rcFilePath}; restoreAffinity(); @@ -323,7 +350,7 @@ struct CmdPrintDevEnv : Common return { Example{ "To apply the build environment of GNU hello to the current shell:", - ". <(nix print-dev-env nixpkgs.hello)" + ". <(nix print-dev-env nixpkgs#hello)" }, }; } diff --git a/src/nix/diff-closures.cc b/src/nix/diff-closures.cc new file mode 100644 index 000000000..56ddb575b --- /dev/null +++ b/src/nix/diff-closures.cc @@ -0,0 +1,134 @@ +#include "command.hh" +#include "shared.hh" +#include "store-api.hh" +#include "common-args.hh" +#include "names.hh" + +#include <regex> + +using namespace nix; + +struct Info +{ + std::string outputName; +}; + +// name -> version -> store paths +typedef std::map<std::string, std::map<std::string, std::map<StorePath, Info>>> GroupedPaths; + +GroupedPaths getClosureInfo(ref<Store> store, const StorePath & toplevel) +{ + StorePathSet closure; + store->computeFSClosure({toplevel}, closure); + + GroupedPaths groupedPaths; + + for (auto & path : closure) { + /* Strip the output name. Unfortunately this is ambiguous (we + can't distinguish between output names like "bin" and + version suffixes like "unstable"). */ + static std::regex regex("(.*)-([a-z]+|lib32|lib64)"); + std::smatch match; + std::string name(path.name()); + std::string outputName; + if (std::regex_match(name, match, regex)) { + name = match[1]; + outputName = match[2]; + } + + DrvName drvName(name); + groupedPaths[drvName.name][drvName.version].emplace(path, Info { .outputName = outputName }); + } + + return groupedPaths; +} + +std::string showVersions(const std::set<std::string> & versions) +{ + if (versions.empty()) return "∅"; + std::set<std::string> versions2; + for (auto & version : versions) + versions2.insert(version.empty() ? "ε" : version); + return concatStringsSep(", ", versions2); +} + +struct CmdDiffClosures : SourceExprCommand +{ + std::string _before, _after; + + CmdDiffClosures() + { + expectArg("before", &_before); + expectArg("after", &_after); + } + + std::string description() override + { + return "show what packages and versions were added and removed between two closures"; + } + + Category category() override { return catSecondary; } + + Examples examples() override + { + return { + { + "To show what got added and removed between two versions of the NixOS system profile:", + "nix diff-closures /nix/var/nix/profiles/system-655-link /nix/var/nix/profiles/system-658-link", + }, + }; + } + + void run(ref<Store> store) override + { + auto before = parseInstallable(store, _before); + auto beforePath = toStorePath(store, Realise::Outputs, operateOn, before); + auto after = parseInstallable(store, _after); + auto afterPath = toStorePath(store, Realise::Outputs, operateOn, after); + + auto beforeClosure = getClosureInfo(store, beforePath); + auto afterClosure = getClosureInfo(store, afterPath); + + std::set<std::string> allNames; + for (auto & [name, _] : beforeClosure) allNames.insert(name); + for (auto & [name, _] : afterClosure) allNames.insert(name); + + for (auto & name : allNames) { + auto & beforeVersions = beforeClosure[name]; + auto & afterVersions = afterClosure[name]; + + auto totalSize = [&](const std::map<std::string, std::map<StorePath, Info>> & versions) + { + uint64_t sum = 0; + for (auto & [_, paths] : versions) + for (auto & [path, _] : paths) + sum += store->queryPathInfo(path)->narSize; + return sum; + }; + + auto beforeSize = totalSize(beforeVersions); + auto afterSize = totalSize(afterVersions); + auto sizeDelta = (int64_t) afterSize - (int64_t) beforeSize; + auto showDelta = abs(sizeDelta) >= 8 * 1024; + + std::set<std::string> removed, unchanged; + for (auto & [version, _] : beforeVersions) + if (!afterVersions.count(version)) removed.insert(version); else unchanged.insert(version); + + std::set<std::string> added; + for (auto & [version, _] : afterVersions) + if (!beforeVersions.count(version)) added.insert(version); + + if (showDelta || !removed.empty() || !added.empty()) { + std::vector<std::string> items; + if (!removed.empty() || !added.empty()) + items.push_back(fmt("%s → %s", showVersions(removed), showVersions(added))); + if (showDelta) + items.push_back(fmt("%s%+.1f KiB" ANSI_NORMAL, sizeDelta > 0 ? ANSI_RED : ANSI_GREEN, sizeDelta / 1024.0)); + std::cout << fmt("%s: %s\n", name, concatStringsSep(", ", items)); + } + } + } +}; + +static auto r1 = registerCommand<CmdDiffClosures>("diff-closures"); diff --git a/src/nix/edit.cc b/src/nix/edit.cc index 067d3a973..dc9775635 100644 --- a/src/nix/edit.cc +++ b/src/nix/edit.cc @@ -20,7 +20,7 @@ struct CmdEdit : InstallableCommand return { Example{ "To open the Nix expression of the GNU Hello package:", - "nix edit nixpkgs.hello" + "nix edit nixpkgs#hello" }, }; } diff --git a/src/nix/eval.cc b/src/nix/eval.cc index 26e98ac2a..a8ca446be 100644 --- a/src/nix/eval.cc +++ b/src/nix/eval.cc @@ -12,10 +12,18 @@ using namespace nix; struct CmdEval : MixJSON, InstallableCommand { bool raw = false; + std::optional<std::string> apply; CmdEval() { mkFlag(0, "raw", "print strings unquoted", &raw); + + addFlag({ + .longName = "apply", + .description = "apply a function to each argument", + .labels = {"expr"}, + .handler = {&apply}, + }); } std::string description() override @@ -26,21 +34,25 @@ struct CmdEval : MixJSON, InstallableCommand Examples examples() override { return { - Example{ + { "To evaluate a Nix expression given on the command line:", - "nix eval '(1 + 2)'" + "nix eval --expr '1 + 2'" }, - Example{ + { "To evaluate a Nix expression from a file or URI:", - "nix eval -f channel:nixos-17.09 hello.name" + "nix eval -f ./my-nixpkgs hello.name" }, - Example{ + { "To get the current version of Nixpkgs:", - "nix eval --raw nixpkgs.lib.version" + "nix eval --raw nixpkgs#lib.version" }, - Example{ + { "To print the store path of the Hello package:", - "nix eval --raw nixpkgs.hello" + "nix eval --raw nixpkgs#hello" + }, + { + "To get a list of checks in the 'nix' flake:", + "nix eval nix#checks.x86_64-linux --apply builtins.attrNames" }, }; } @@ -57,6 +69,14 @@ struct CmdEval : MixJSON, InstallableCommand auto v = installable->toValue(*state).first; PathSet context; + if (apply) { + auto vApply = state->allocValue(); + state->eval(state->parseExprFromString(*apply, absPath(".")), *vApply); + auto vRes = state->allocValue(); + state->callFunction(*vApply, *v, *vRes, noPos); + v = vRes; + } + if (raw) { stopProgressBar(); std::cout << state->coerceToString(noPos, *v, context); diff --git a/src/nix/flake.cc b/src/nix/flake.cc new file mode 100644 index 000000000..027a9871e --- /dev/null +++ b/src/nix/flake.cc @@ -0,0 +1,955 @@ +#include "command.hh" +#include "common-args.hh" +#include "shared.hh" +#include "eval.hh" +#include "eval-inline.hh" +#include "flake/flake.hh" +#include "get-drvs.hh" +#include "store-api.hh" +#include "derivations.hh" +#include "attr-path.hh" +#include "fetchers.hh" +#include "registry.hh" +#include "json.hh" +#include "eval-cache.hh" + +#include <nlohmann/json.hpp> +#include <queue> +#include <iomanip> + +using namespace nix; +using namespace nix::flake; + +class FlakeCommand : virtual Args, public MixFlakeOptions +{ + std::string flakeUrl = "."; + +public: + + FlakeCommand() + { + expectArgs({ + .label = "flake-url", + .optional = true, + .handler = {&flakeUrl}, + .completer = {[&](size_t, std::string_view prefix) { + completeFlakeRef(getStore(), prefix); + }} + }); + } + + FlakeRef getFlakeRef() + { + return parseFlakeRef(flakeUrl, absPath(".")); //FIXME + } + + Flake getFlake() + { + auto evalState = getEvalState(); + return flake::getFlake(*evalState, getFlakeRef(), lockFlags.useRegistries); + } + + LockedFlake lockFlake() + { + return flake::lockFlake(*getEvalState(), getFlakeRef(), lockFlags); + } + + std::optional<FlakeRef> getFlakeRefForCompletion() override + { + return getFlakeRef(); + } +}; + +static void printFlakeInfo(const Store & store, const Flake & flake) +{ + logger->stdout("Resolved URL: %s", flake.resolvedRef.to_string()); + logger->stdout("Locked URL: %s", flake.lockedRef.to_string()); + if (flake.description) + logger->stdout("Description: %s", *flake.description); + logger->stdout("Path: %s", store.printStorePath(flake.sourceInfo->storePath)); + if (auto rev = flake.lockedRef.input.getRev()) + logger->stdout("Revision: %s", rev->to_string(Base16, false)); + if (auto revCount = flake.lockedRef.input.getRevCount()) + logger->stdout("Revisions: %s", *revCount); + if (auto lastModified = flake.lockedRef.input.getLastModified()) + logger->stdout("Last modified: %s", + std::put_time(std::localtime(&*lastModified), "%F %T")); +} + +static nlohmann::json flakeToJson(const Store & store, const Flake & flake) +{ + nlohmann::json j; + if (flake.description) + j["description"] = *flake.description; + j["originalUrl"] = flake.originalRef.to_string(); + j["original"] = attrsToJson(flake.originalRef.toAttrs()); + j["resolvedUrl"] = flake.resolvedRef.to_string(); + j["resolved"] = attrsToJson(flake.resolvedRef.toAttrs()); + j["url"] = flake.lockedRef.to_string(); // FIXME: rename to lockedUrl + j["locked"] = attrsToJson(flake.lockedRef.toAttrs()); + if (auto rev = flake.lockedRef.input.getRev()) + j["revision"] = rev->to_string(Base16, false); + if (auto revCount = flake.lockedRef.input.getRevCount()) + j["revCount"] = *revCount; + if (auto lastModified = flake.lockedRef.input.getLastModified()) + j["lastModified"] = *lastModified; + j["path"] = store.printStorePath(flake.sourceInfo->storePath); + return j; +} + +struct CmdFlakeUpdate : FlakeCommand +{ + std::string description() override + { + return "update flake lock file"; + } + + void run(nix::ref<nix::Store> store) override + { + /* Use --refresh by default for 'nix flake update'. */ + settings.tarballTtl = 0; + + lockFlake(); + } +}; + +static void enumerateOutputs(EvalState & state, Value & vFlake, + std::function<void(const std::string & name, Value & vProvide, const Pos & pos)> callback) +{ + state.forceAttrs(vFlake); + + auto aOutputs = vFlake.attrs->get(state.symbols.create("outputs")); + assert(aOutputs); + + state.forceAttrs(*aOutputs->value); + + for (auto & attr : *aOutputs->value->attrs) + callback(attr.name, *attr.value, *attr.pos); +} + +struct CmdFlakeInfo : FlakeCommand, MixJSON +{ + std::string description() override + { + return "list info about a given flake"; + } + + void run(nix::ref<nix::Store> store) override + { + auto flake = getFlake(); + + if (json) { + auto json = flakeToJson(*store, flake); + logger->stdout("%s", json.dump()); + } else + printFlakeInfo(*store, flake); + } +}; + +struct CmdFlakeListInputs : FlakeCommand, MixJSON +{ + std::string description() override + { + return "list flake inputs"; + } + + void run(nix::ref<nix::Store> store) override + { + auto flake = lockFlake(); + + if (json) + logger->stdout("%s", flake.lockFile.toJson()); + else { + logger->stdout("%s", flake.flake.lockedRef); + + std::unordered_set<std::shared_ptr<Node>> visited; + + std::function<void(const Node & node, const std::string & prefix)> recurse; + + recurse = [&](const Node & node, const std::string & prefix) + { + for (const auto & [i, input] : enumerate(node.inputs)) { + bool last = i + 1 == node.inputs.size(); + + if (auto lockedNode = std::get_if<0>(&input.second)) { + logger->stdout("%s" ANSI_BOLD "%s" ANSI_NORMAL ": %s", + prefix + (last ? treeLast : treeConn), input.first, + *lockedNode ? (*lockedNode)->lockedRef : flake.flake.lockedRef); + + bool firstVisit = visited.insert(*lockedNode).second; + + if (firstVisit) recurse(**lockedNode, prefix + (last ? treeNull : treeLine)); + } else if (auto follows = std::get_if<1>(&input.second)) { + logger->stdout("%s" ANSI_BOLD "%s" ANSI_NORMAL " follows input '%s'", + prefix + (last ? treeLast : treeConn), input.first, + printInputPath(*follows)); + } + } + }; + + visited.insert(flake.lockFile.root); + recurse(*flake.lockFile.root, ""); + } + } +}; + +struct CmdFlakeCheck : FlakeCommand +{ + bool build = true; + + CmdFlakeCheck() + { + addFlag({ + .longName = "no-build", + .description = "do not build checks", + .handler = {&build, false} + }); + } + + std::string description() override + { + return "check whether the flake evaluates and run its tests"; + } + + void run(nix::ref<nix::Store> store) override + { + settings.readOnlyMode = !build; + + auto state = getEvalState(); + auto flake = lockFlake(); + + // FIXME: rewrite to use EvalCache. + + auto checkSystemName = [&](const std::string & system, const Pos & pos) { + // FIXME: what's the format of "system"? + if (system.find('-') == std::string::npos) + throw Error("'%s' is not a valid system type, at %s", system, pos); + }; + + auto checkDerivation = [&](const std::string & attrPath, Value & v, const Pos & pos) { + try { + auto drvInfo = getDerivation(*state, v, false); + if (!drvInfo) + throw Error("flake attribute '%s' is not a derivation", attrPath); + // FIXME: check meta attributes + return store->parseStorePath(drvInfo->queryDrvPath()); + } catch (Error & e) { + e.addTrace(pos, hintfmt("while checking the derivation '%s'", attrPath)); + throw; + } + }; + + std::vector<StorePathWithOutputs> drvPaths; + + auto checkApp = [&](const std::string & attrPath, Value & v, const Pos & pos) { + try { + #if 0 + // FIXME + auto app = App(*state, v); + for (auto & i : app.context) { + auto [drvPathS, outputName] = decodeContext(i); + store->parseStorePath(drvPathS); + } + #endif + } catch (Error & e) { + e.addTrace(pos, hintfmt("while checking the app definition '%s'", attrPath)); + throw; + } + }; + + auto checkOverlay = [&](const std::string & attrPath, Value & v, const Pos & pos) { + try { + state->forceValue(v, pos); + if (v.type != tLambda || v.lambda.fun->matchAttrs || std::string(v.lambda.fun->arg) != "final") + throw Error("overlay does not take an argument named 'final'"); + auto body = dynamic_cast<ExprLambda *>(v.lambda.fun->body); + if (!body || body->matchAttrs || std::string(body->arg) != "prev") + throw Error("overlay does not take an argument named 'prev'"); + // FIXME: if we have a 'nixpkgs' input, use it to + // evaluate the overlay. + } catch (Error & e) { + e.addTrace(pos, hintfmt("while checking the overlay '%s'", attrPath)); + throw; + } + }; + + auto checkModule = [&](const std::string & attrPath, Value & v, const Pos & pos) { + try { + state->forceValue(v, pos); + if (v.type == tLambda) { + if (!v.lambda.fun->matchAttrs || !v.lambda.fun->formals->ellipsis) + throw Error("module must match an open attribute set ('{ config, ... }')"); + } else if (v.type == tAttrs) { + for (auto & attr : *v.attrs) + try { + state->forceValue(*attr.value, *attr.pos); + } catch (Error & e) { + e.addTrace(*attr.pos, hintfmt("while evaluating the option '%s'", attr.name)); + throw; + } + } else + throw Error("module must be a function or an attribute set"); + // FIXME: if we have a 'nixpkgs' input, use it to + // check the module. + } catch (Error & e) { + e.addTrace(pos, hintfmt("while checking the NixOS module '%s'", attrPath)); + throw; + } + }; + + std::function<void(const std::string & attrPath, Value & v, const Pos & pos)> checkHydraJobs; + + checkHydraJobs = [&](const std::string & attrPath, Value & v, const Pos & pos) { + try { + 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); + if (!state->isDerivation(*attr.value)) + checkHydraJobs(attrPath + "." + (std::string) attr.name, + *attr.value, *attr.pos); + } + + } catch (Error & e) { + e.addTrace(pos, hintfmt("while checking the Hydra jobset '%s'", attrPath)); + throw; + } + }; + + auto checkNixOSConfiguration = [&](const std::string & attrPath, Value & v, const Pos & pos) { + try { + Activity act(*logger, lvlChatty, actUnknown, + fmt("checking NixOS configuration '%s'", attrPath)); + Bindings & bindings(*state->allocBindings(0)); + auto vToplevel = findAlongAttrPath(*state, "config.system.build.toplevel", bindings, v).first; + state->forceAttrs(*vToplevel, pos); + if (!state->isDerivation(*vToplevel)) + throw Error("attribute 'config.system.build.toplevel' is not a derivation"); + } catch (Error & e) { + e.addTrace(pos, hintfmt("while checking the NixOS configuration '%s'", attrPath)); + throw; + } + }; + + auto checkTemplate = [&](const std::string & attrPath, Value & v, const Pos & pos) { + try { + Activity act(*logger, lvlChatty, 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")) { + PathSet context; + auto path = state->coerceToPath(*attr->pos, *attr->value, context); + if (!store->isInStore(path)) + throw Error("template '%s' has a bad 'path' attribute"); + // 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 name(attr.name); + if (name != "path" && name != "description") + throw Error("template '%s' has unsupported attribute '%s'", attrPath, name); + } + } catch (Error & e) { + e.addTrace(pos, hintfmt("while checking the template '%s'", attrPath)); + throw; + } + }; + + { + Activity act(*logger, lvlInfo, actUnknown, "evaluating flake"); + + auto vFlake = state->allocValue(); + flake::callFlake(*state, flake, *vFlake); + + enumerateOutputs(*state, + *vFlake, + [&](const std::string & name, Value & vOutput, const Pos & pos) { + Activity act(*logger, lvlChatty, actUnknown, + fmt("checking flake output '%s'", name)); + + try { + state->forceValue(vOutput, pos); + + if (name == "checks") { + state->forceAttrs(vOutput, pos); + for (auto & attr : *vOutput.attrs) { + checkSystemName(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, attr2.name), + *attr2.value, *attr2.pos); + if ((std::string) attr.name == settings.thisSystem.get()) + drvPaths.push_back({drvPath}); + } + } + } + + else if (name == "packages") { + state->forceAttrs(vOutput, pos); + for (auto & attr : *vOutput.attrs) { + checkSystemName(attr.name, *attr.pos); + state->forceAttrs(*attr.value, *attr.pos); + for (auto & attr2 : *attr.value->attrs) + checkDerivation( + fmt("%s.%s.%s", name, attr.name, attr2.name), + *attr2.value, *attr2.pos); + } + } + + else if (name == "apps") { + state->forceAttrs(vOutput, pos); + for (auto & attr : *vOutput.attrs) { + checkSystemName(attr.name, *attr.pos); + state->forceAttrs(*attr.value, *attr.pos); + for (auto & attr2 : *attr.value->attrs) + checkApp( + fmt("%s.%s.%s", name, attr.name, attr2.name), + *attr2.value, *attr2.pos); + } + } + + else if (name == "defaultPackage" || name == "devShell") { + state->forceAttrs(vOutput, pos); + for (auto & attr : *vOutput.attrs) { + checkSystemName(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) { + checkSystemName(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(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, 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, attr.name), + *attr.value, *attr.pos); + } + + else if (name == "nixosConfigurations") { + state->forceAttrs(vOutput, pos); + for (auto & attr : *vOutput.attrs) + checkNixOSConfiguration(fmt("%s.%s", name, 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, attr.name), + *attr.value, *attr.pos); + } + + else + warn("unknown flake output '%s'", name); + + } catch (Error & e) { + e.addTrace(pos, hintfmt("while checking flake output '%s'", name)); + throw; + } + }); + } + + if (build && !drvPaths.empty()) { + Activity act(*logger, lvlInfo, actUnknown, "running flake checks"); + store->buildPaths(drvPaths); + } + } +}; + +struct CmdFlakeInitCommon : virtual Args, EvalCommand +{ + std::string templateUrl = "templates"; + Path destDir; + + const Strings attrsPathPrefixes{"templates."}; + const LockFlags lockFlags{ .writeLockFile = false }; + + CmdFlakeInitCommon() + { + addFlag({ + .longName = "template", + .shortName = 't', + .description = "the template to use", + .labels = {"template"}, + .handler = {&templateUrl}, + .completer = {[&](size_t, std::string_view prefix) { + completeFlakeRefWithFragment( + getEvalState(), + lockFlags, + attrsPathPrefixes, + {"defaultTemplate"}, + prefix); + }} + }); + } + + void run(nix::ref<nix::Store> store) override + { + auto flakeDir = absPath(destDir); + + auto evalState = getEvalState(); + + auto [templateFlakeRef, templateName] = parseFlakeRefWithFragment(templateUrl, absPath(".")); + + auto installable = InstallableFlake( + evalState, std::move(templateFlakeRef), + Strings{templateName == "" ? "defaultTemplate" : templateName}, + Strings(attrsPathPrefixes), lockFlags); + + auto [cursor, attrPath] = installable.getCursor(*evalState, true); + + auto templateDir = cursor->getAttr("path")->getString(); + + assert(store->isInStore(templateDir)); + + std::vector<Path> files; + + std::function<void(const Path & from, const Path & to)> copyDir; + copyDir = [&](const Path & from, const Path & to) + { + createDirs(to); + + for (auto & entry : readDirectory(from)) { + auto from2 = from + "/" + entry.name; + auto to2 = to + "/" + entry.name; + auto st = lstat(from2); + if (S_ISDIR(st.st_mode)) + copyDir(from2, to2); + else if (S_ISREG(st.st_mode)) { + auto contents = readFile(from2); + if (pathExists(to2)) { + auto contents2 = readFile(to2); + if (contents != contents2) + throw Error("refusing to overwrite existing file '%s'", to2); + } else + writeFile(to2, contents); + } + else if (S_ISLNK(st.st_mode)) { + auto target = readLink(from2); + if (pathExists(to2)) { + if (readLink(to2) != target) + throw Error("refusing to overwrite existing symlink '%s'", to2); + } else + createSymlink(target, to2); + } + else + throw Error("file '%s' has unsupported type", from2); + files.push_back(to2); + } + }; + + copyDir(templateDir, flakeDir); + + if (pathExists(flakeDir + "/.git")) { + Strings args = { "-C", flakeDir, "add", "--intent-to-add", "--force", "--" }; + for (auto & s : files) args.push_back(s); + runProgram("git", true, args); + } + } +}; + +struct CmdFlakeInit : CmdFlakeInitCommon +{ + std::string description() override + { + return "create a flake in the current directory from a template"; + } + + Examples examples() override + { + return { + Example{ + "To create a flake using the default template:", + "nix flake init" + }, + Example{ + "To see available templates:", + "nix flake show templates" + }, + Example{ + "To create a flake from a specific template:", + "nix flake init -t templates#nixos-container" + }, + }; + } + + CmdFlakeInit() + { + destDir = "."; + } +}; + +struct CmdFlakeNew : CmdFlakeInitCommon +{ + std::string description() override + { + return "create a flake in the specified directory from a template"; + } + + CmdFlakeNew() + { + expectArgs({ + .label = "dest-dir", + .handler = {&destDir}, + .completer = completePath + }); + } +}; + +struct CmdFlakeClone : FlakeCommand +{ + Path destDir; + + std::string description() override + { + return "clone flake repository"; + } + + CmdFlakeClone() + { + addFlag({ + .longName = "dest", + .shortName = 'f', + .description = "destination path", + .labels = {"path"}, + .handler = {&destDir} + }); + } + + void run(nix::ref<nix::Store> store) override + { + if (destDir.empty()) + throw Error("missing flag '--dest'"); + + getFlakeRef().resolve(store).input.clone(destDir); + } +}; + +struct CmdFlakeArchive : FlakeCommand, MixJSON, MixDryRun +{ + std::string dstUri; + + CmdFlakeArchive() + { + addFlag({ + .longName = "to", + .description = "URI of the destination Nix store", + .labels = {"store-uri"}, + .handler = {&dstUri} + }); + } + + std::string description() override + { + return "copy a flake and all its inputs to a store"; + } + + Examples examples() override + { + return { + Example{ + "To copy the dwarffs flake and its dependencies to a binary cache:", + "nix flake archive --to file:///tmp/my-cache dwarffs" + }, + Example{ + "To fetch the dwarffs flake and its dependencies to the local Nix store:", + "nix flake archive dwarffs" + }, + Example{ + "To print the store paths of the flake sources of NixOps without fetching them:", + "nix flake archive --json --dry-run nixops" + }, + }; + } + + void run(nix::ref<nix::Store> store) override + { + auto flake = lockFlake(); + + auto jsonRoot = json ? std::optional<JSONObject>(std::cout) : std::nullopt; + + StorePathSet sources; + + sources.insert(flake.flake.sourceInfo->storePath); + if (jsonRoot) + jsonRoot->attr("path", store->printStorePath(flake.flake.sourceInfo->storePath)); + + // FIXME: use graph output, handle cycles. + std::function<void(const Node & node, std::optional<JSONObject> & jsonObj)> traverse; + traverse = [&](const Node & node, std::optional<JSONObject> & jsonObj) + { + auto jsonObj2 = jsonObj ? jsonObj->object("inputs") : std::optional<JSONObject>(); + for (auto & [inputName, input] : node.inputs) { + if (auto inputNode = std::get_if<0>(&input)) { + auto jsonObj3 = jsonObj2 ? jsonObj2->object(inputName) : std::optional<JSONObject>(); + auto storePath = + dryRun + ? (*inputNode)->lockedRef.input.computeStorePath(*store) + : (*inputNode)->lockedRef.input.fetch(store).first.storePath; + if (jsonObj3) + jsonObj3->attr("path", store->printStorePath(storePath)); + sources.insert(std::move(storePath)); + traverse(**inputNode, jsonObj3); + } + } + }; + + traverse(*flake.lockFile.root, jsonRoot); + + if (!dryRun && !dstUri.empty()) { + ref<Store> dstStore = dstUri.empty() ? openStore() : openStore(dstUri); + copyPaths(store, dstStore, sources); + } + } +}; + +struct CmdFlakeShow : FlakeCommand +{ + bool showLegacy = false; + bool useEvalCache = true; + + CmdFlakeShow() + { + addFlag({ + .longName = "legacy", + .description = "show the contents of the 'legacyPackages' output", + .handler = {&showLegacy, true} + }); + + addFlag({ + .longName = "no-eval-cache", + .description = "do not use the flake evaluation cache", + .handler = {[&]() { useEvalCache = false; }} + }); + } + + std::string description() override + { + return "show the outputs provided by a flake"; + } + + void run(nix::ref<nix::Store> store) override + { + auto state = getEvalState(); + auto flake = std::make_shared<LockedFlake>(lockFlake()); + + std::function<void(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) + { + Activity act(*logger, lvlInfo, actUnknown, + fmt("evaluating '%s'", concatStringsSep(".", attrPath))); + try { + auto recurse = [&]() + { + logger->stdout("%s", headerPrefix); + auto attrs = visitor.getAttrs(); + for (const auto & [i, attr] : enumerate(attrs)) { + bool last = i + 1 == attrs.size(); + auto visitor2 = visitor.getAttr(attr); + auto attrPath2(attrPath); + attrPath2.push_back(attr); + visit(*visitor2, attrPath2, + fmt(ANSI_GREEN "%s%s" ANSI_NORMAL ANSI_BOLD "%s" ANSI_NORMAL, nextPrefix, last ? treeLast : treeConn, attr), + nextPrefix + (last ? treeNull : treeLine)); + } + }; + + auto showDerivation = [&]() + { + auto name = visitor.getAttr(state->sName)->getString(); + + /* + std::string description; + + if (auto aMeta = visitor.maybeGetAttr("meta")) { + if (auto aDescription = aMeta->maybeGetAttr("description")) + description = aDescription->getString(); + } + */ + + logger->stdout("%s: %s '%s'", + headerPrefix, + attrPath.size() == 2 && attrPath[0] == "devShell" ? "development environment" : + attrPath.size() == 3 && attrPath[0] == "checks" ? "derivation" : + attrPath.size() >= 1 && attrPath[0] == "hydraJobs" ? "derivation" : + "package", + name); + }; + + if (attrPath.size() == 0 + || (attrPath.size() == 1 && ( + attrPath[0] == "defaultPackage" + || attrPath[0] == "devShell" + || attrPath[0] == "nixosConfigurations" + || attrPath[0] == "nixosModules" + || attrPath[0] == "defaultApp" + || attrPath[0] == "templates")) + || ((attrPath.size() == 1 || attrPath.size() == 2) + && (attrPath[0] == "checks" + || attrPath[0] == "packages" + || attrPath[0] == "apps")) + ) + { + recurse(); + } + + else if ( + (attrPath.size() == 2 && (attrPath[0] == "defaultPackage" || attrPath[0] == "devShell")) + || (attrPath.size() == 3 && (attrPath[0] == "checks" || attrPath[0] == "packages")) + ) + { + if (visitor.isDerivation()) + showDerivation(); + else + throw Error("expected a derivation"); + } + + else if (attrPath.size() > 0 && attrPath[0] == "hydraJobs") { + if (visitor.isDerivation()) + showDerivation(); + else + recurse(); + } + + else if (attrPath.size() > 0 && attrPath[0] == "legacyPackages") { + if (attrPath.size() == 1) + recurse(); + else if (!showLegacy) + logger->stdout("%s: " ANSI_YELLOW "omitted" ANSI_NORMAL " (use '--legacy' to show)", headerPrefix); + else { + if (visitor.isDerivation()) + showDerivation(); + else if (attrPath.size() <= 2) + // FIXME: handle recurseIntoAttrs + recurse(); + } + } + + else if ( + (attrPath.size() == 2 && attrPath[0] == "defaultApp") || + (attrPath.size() == 3 && attrPath[0] == "apps")) + { + auto aType = visitor.maybeGetAttr("type"); + if (!aType || aType->getString() != "app") + throw EvalError("not an app definition"); + logger->stdout("%s: app", headerPrefix); + } + + else if ( + (attrPath.size() == 1 && attrPath[0] == "defaultTemplate") || + (attrPath.size() == 2 && attrPath[0] == "templates")) + { + auto description = visitor.getAttr("description")->getString(); + logger->stdout("%s: template: " ANSI_BOLD "%s" ANSI_NORMAL, headerPrefix, description); + } + + else { + logger->stdout("%s: %s", + headerPrefix, + attrPath.size() == 1 && attrPath[0] == "overlay" ? "Nixpkgs overlay" : + attrPath.size() == 2 && attrPath[0] == "nixosConfigurations" ? "NixOS configuration" : + attrPath.size() == 2 && attrPath[0] == "nixosModules" ? "NixOS module" : + ANSI_YELLOW "unknown" ANSI_NORMAL); + } + } catch (EvalError & e) { + if (!(attrPath.size() > 0 && attrPath[0] == "legacyPackages")) + throw; + } + }; + + auto cache = openEvalCache(*state, flake, useEvalCache); + + visit(*cache->getRoot(), {}, fmt(ANSI_BOLD "%s" ANSI_NORMAL, flake->flake.lockedRef), ""); + } +}; + +struct CmdFlake : virtual MultiCommand, virtual Command +{ + CmdFlake() + : MultiCommand({ + {"update", []() { return make_ref<CmdFlakeUpdate>(); }}, + {"info", []() { return make_ref<CmdFlakeInfo>(); }}, + {"list-inputs", []() { return make_ref<CmdFlakeListInputs>(); }}, + {"check", []() { return make_ref<CmdFlakeCheck>(); }}, + {"init", []() { return make_ref<CmdFlakeInit>(); }}, + {"new", []() { return make_ref<CmdFlakeNew>(); }}, + {"clone", []() { return make_ref<CmdFlakeClone>(); }}, + {"archive", []() { return make_ref<CmdFlakeArchive>(); }}, + {"show", []() { return make_ref<CmdFlakeShow>(); }}, + }) + { + } + + std::string description() override + { + return "manage Nix flakes"; + } + + void run() override + { + if (!command) + throw UsageError("'nix flake' requires a sub-command."); + settings.requireExperimentalFeature("flakes"); + command->second->prepare(); + command->second->run(); + } + + void printHelp(const string & programName, std::ostream & out) override + { + MultiCommand::printHelp(programName, out); + } +}; + +static auto r1 = registerCommand<CmdFlake>("flake"); diff --git a/src/nix/get-env.sh b/src/nix/get-env.sh index a25ec43a9..2e0e83561 100644 --- a/src/nix/get-env.sh +++ b/src/nix/get-env.sh @@ -1,9 +1,18 @@ set -e if [ -e .attrs.sh ]; then source .attrs.sh; fi + +outputs=$_outputs_saved +for __output in $_outputs_saved; do + declare "$__output"="$out" +done +unset _outputs_saved __output + export IN_NIX_SHELL=impure export dontAddDisableDepTrack=1 + if [[ -n $stdenv ]]; then source $stdenv/setup fi + export > $out set >> $out diff --git a/src/nix/hash.cc b/src/nix/hash.cc index fb0843ce2..0eca4f8ea 100644 --- a/src/nix/hash.cc +++ b/src/nix/hash.cc @@ -31,7 +31,11 @@ struct CmdHash : Command .labels({"modulus"}) .dest(&modulus); #endif - expectArgs("paths", &paths); + expectArgs({ + .label = "paths", + .handler = {&paths}, + .completer = completePath + }); } std::string description() override diff --git a/src/nix/installables.cc b/src/nix/installables.cc index 708a0dc88..5c963d044 100644 --- a/src/nix/installables.cc +++ b/src/nix/installables.cc @@ -1,3 +1,4 @@ +#include "installables.hh" #include "command.hh" #include "attr-path.hh" #include "common-eval-args.hh" @@ -7,80 +8,262 @@ #include "get-drvs.hh" #include "store-api.hh" #include "shared.hh" +#include "flake/flake.hh" +#include "eval-cache.hh" +#include "url.hh" +#include "registry.hh" #include <regex> +#include <queue> namespace nix { +void completeFlakeInputPath( + ref<EvalState> evalState, + const FlakeRef & flakeRef, + std::string_view prefix) +{ + auto flake = flake::getFlake(*evalState, flakeRef, true); + for (auto & input : flake.inputs) + if (hasPrefix(input.first, prefix)) + completions->insert(input.first); +} -SourceExprCommand::SourceExprCommand() +MixFlakeOptions::MixFlakeOptions() { addFlag({ - .longName = "file", - .shortName = 'f', - .description = "evaluate FILE rather than the default", - .labels = {"file"}, - .handler = {&file} + .longName = "recreate-lock-file", + .description = "recreate lock file from scratch", + .handler = {&lockFlags.recreateLockFile, true} }); -} -Value * SourceExprCommand::getSourceExpr(EvalState & state) -{ - if (vSourceExpr) return *vSourceExpr; + addFlag({ + .longName = "no-update-lock-file", + .description = "do not allow any updates to the lock file", + .handler = {&lockFlags.updateLockFile, false} + }); - auto sToplevel = state.symbols.create("_toplevel"); + addFlag({ + .longName = "no-write-lock-file", + .description = "do not write the newly generated lock file", + .handler = {&lockFlags.writeLockFile, false} + }); - vSourceExpr = allocRootValue(state.allocValue()); + addFlag({ + .longName = "no-registries", + .description = "don't use flake registries", + .handler = {&lockFlags.useRegistries, false} + }); - if (file != "") - state.evalFile(lookupFileArg(state, file), **vSourceExpr); + addFlag({ + .longName = "commit-lock-file", + .description = "commit changes to the lock file", + .handler = {&lockFlags.commitLockFile, true} + }); - else { + addFlag({ + .longName = "update-input", + .description = "update a specific flake input", + .labels = {"input-path"}, + .handler = {[&](std::string s) { + lockFlags.inputUpdates.insert(flake::parseInputPath(s)); + }}, + .completer = {[&](size_t, std::string_view prefix) { + if (auto flakeRef = getFlakeRefForCompletion()) + completeFlakeInputPath(getEvalState(), *flakeRef, prefix); + }} + }); - /* Construct the installation source from $NIX_PATH. */ + addFlag({ + .longName = "override-input", + .description = "override a specific flake input (e.g. 'dwarffs/nixpkgs')", + .labels = {"input-path", "flake-url"}, + .handler = {[&](std::string inputPath, std::string flakeRef) { + lockFlags.inputOverrides.insert_or_assign( + flake::parseInputPath(inputPath), + parseFlakeRef(flakeRef, absPath("."))); + }} + }); - auto searchPath = state.getSearchPath(); + addFlag({ + .longName = "inputs-from", + .description = "use the inputs of the specified flake as registry entries", + .labels = {"flake-url"}, + .handler = {[&](std::string flakeRef) { + auto evalState = getEvalState(); + auto flake = flake::lockFlake( + *evalState, + parseFlakeRef(flakeRef, absPath(".")), + { .writeLockFile = false }); + for (auto & [inputName, input] : flake.lockFile.root->inputs) { + auto input2 = flake.lockFile.findInput({inputName}); // resolve 'follows' nodes + if (auto input3 = std::dynamic_pointer_cast<const flake::LockedNode>(input2)) { + overrideRegistry( + fetchers::Input::fromAttrs({{"type","indirect"}, {"id", inputName}}), + input3->lockedRef.input, + {}); + } + } + }}, + .completer = {[&](size_t, std::string_view prefix) { + completeFlakeRef(getEvalState()->store, prefix); + }} + }); +} - state.mkAttrs(**vSourceExpr, 1024); +SourceExprCommand::SourceExprCommand() +{ + addFlag({ + .longName = "file", + .shortName = 'f', + .description = "evaluate FILE rather than the default", + .labels = {"file"}, + .handler = {&file}, + .completer = completePath + }); - mkBool(*state.allocAttr(**vSourceExpr, sToplevel), true); + addFlag({ + .longName ="expr", + .description = "evaluate attributes from EXPR", + .labels = {"expr"}, + .handler = {&expr} + }); - std::unordered_set<std::string> seen; + addFlag({ + .longName ="derivation", + .description = "operate on the store derivation rather than its outputs", + .handler = {&operateOn, OperateOn::Derivation}, + }); +} - auto addEntry = [&](const std::string & name) { - if (name == "") return; - if (!seen.insert(name).second) return; - Value * v1 = state.allocValue(); - mkPrimOpApp(*v1, state.getBuiltin("findFile"), state.getBuiltin("nixPath")); - Value * v2 = state.allocValue(); - mkApp(*v2, *v1, mkString(*state.allocValue(), name)); - mkApp(*state.allocAttr(**vSourceExpr, state.symbols.create(name)), - state.getBuiltin("import"), *v2); - }; +Strings SourceExprCommand::getDefaultFlakeAttrPaths() +{ + return {"defaultPackage." + settings.thisSystem.get()}; +} - for (auto & i : searchPath) - /* Hack to handle channels. */ - if (i.first.empty() && pathExists(i.second + "/manifest.nix")) { - for (auto & j : readDirectory(i.second)) - if (j.name != "manifest.nix" - && pathExists(fmt("%s/%s/default.nix", i.second, j.name))) - addEntry(j.name); - } else - addEntry(i.first); +Strings SourceExprCommand::getDefaultFlakeAttrPathPrefixes() +{ + return { + // As a convenience, look for the attribute in + // 'outputs.packages'. + "packages." + settings.thisSystem.get() + ".", + // As a temporary hack until Nixpkgs is properly converted + // to provide a clean 'packages' set, look in 'legacyPackages'. + "legacyPackages." + settings.thisSystem.get() + "." + }; +} + +void SourceExprCommand::completeInstallable(std::string_view prefix) +{ + if (file) return; // FIXME + + completeFlakeRefWithFragment( + getEvalState(), + lockFlags, + getDefaultFlakeAttrPathPrefixes(), + getDefaultFlakeAttrPaths(), + prefix); +} - (*vSourceExpr)->attrs->sort(); +void completeFlakeRefWithFragment( + ref<EvalState> evalState, + flake::LockFlags lockFlags, + Strings attrPathPrefixes, + const Strings & defaultFlakeAttrPaths, + std::string_view prefix) +{ + /* Look for flake output attributes that match the + prefix. */ + try { + auto hash = prefix.find('#'); + if (hash != std::string::npos) { + auto fragment = prefix.substr(hash + 1); + auto flakeRefS = std::string(prefix.substr(0, hash)); + // FIXME: do tilde expansion. + auto flakeRef = parseFlakeRef(flakeRefS, absPath(".")); + + auto evalCache = openEvalCache(*evalState, + std::make_shared<flake::LockedFlake>(lockFlake(*evalState, flakeRef, lockFlags)), + true); + + auto root = evalCache->getRoot(); + + /* Complete 'fragment' relative to all the + attrpath prefixes as well as the root of the + flake. */ + attrPathPrefixes.push_back(""); + + for (auto & attrPathPrefixS : attrPathPrefixes) { + auto attrPathPrefix = parseAttrPath(*evalState, attrPathPrefixS); + auto attrPathS = attrPathPrefixS + std::string(fragment); + auto attrPath = parseAttrPath(*evalState, attrPathS); + + std::string lastAttr; + if (!attrPath.empty() && !hasSuffix(attrPathS, ".")) { + lastAttr = attrPath.back(); + attrPath.pop_back(); + } + + auto attr = root->findAlongAttrPath(attrPath); + if (!attr) continue; + + for (auto & attr2 : attr->getAttrs()) { + if (hasPrefix(attr2, lastAttr)) { + auto attrPath2 = attr->getAttrPath(attr2); + /* Strip the attrpath prefix. */ + attrPath2.erase(attrPath2.begin(), attrPath2.begin() + attrPathPrefix.size()); + completions->insert(flakeRefS + "#" + concatStringsSep(".", attrPath2)); + } + } + } + + /* And add an empty completion for the default + attrpaths. */ + if (fragment.empty()) { + for (auto & attrPath : defaultFlakeAttrPaths) { + auto attr = root->findAlongAttrPath(parseAttrPath(*evalState, attrPath)); + if (!attr) continue; + completions->insert(flakeRefS + "#"); + } + } + } + } catch (Error & e) { + warn(e.msg()); } - return *vSourceExpr; + completeFlakeRef(evalState->store, prefix); } -ref<EvalState> SourceExprCommand::getEvalState() +ref<EvalState> EvalCommand::getEvalState() { if (!evalState) evalState = std::make_shared<EvalState>(searchPath, getStore()); return ref<EvalState>(evalState); } +void completeFlakeRef(ref<Store> store, std::string_view prefix) +{ + if (prefix == "") + completions->insert("."); + + completeDir(0, prefix); + + /* Look for registry entries that match the prefix. */ + for (auto & registry : fetchers::getRegistries(store)) { + for (auto & entry : registry->entries) { + auto from = entry.from.to_string(); + if (!hasPrefix(prefix, "flake:") && hasPrefix(from, "flake:")) { + std::string from2(from, 6); + if (hasPrefix(from2, prefix)) + completions->insert(from2); + } else { + if (hasPrefix(from, prefix)) + completions->insert(from); + } + } + } +} + Buildable Installable::toBuildable() { auto buildables = toBuildables(); @@ -89,27 +272,54 @@ Buildable Installable::toBuildable() return std::move(buildables[0]); } +std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>> +Installable::getCursors(EvalState & state, bool useEvalCache) +{ + auto evalCache = + std::make_shared<nix::eval_cache::EvalCache>(std::nullopt, state, + [&]() { return toValue(state).first; }); + return {{evalCache->getRoot(), ""}}; +} + +std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string> +Installable::getCursor(EvalState & state, bool useEvalCache) +{ + auto cursors = getCursors(state, useEvalCache); + if (cursors.empty()) + throw Error("cannot find flake attribute '%s'", what()); + return cursors[0]; +} + struct InstallableStorePath : Installable { ref<Store> store; StorePath storePath; - InstallableStorePath(ref<Store> store, const Path & storePath) - : store(store), storePath(store->parseStorePath(storePath)) { } + InstallableStorePath(ref<Store> store, StorePath && storePath) + : store(store), storePath(std::move(storePath)) { } std::string what() override { return store->printStorePath(storePath); } Buildables toBuildables() override { - std::map<std::string, StorePath> outputs; - outputs.insert_or_assign("out", storePath); - Buildable b{ - .drvPath = storePath.isDerivation() ? storePath : std::optional<StorePath>(), - .outputs = std::move(outputs) - }; - Buildables bs; - bs.push_back(std::move(b)); - return bs; + if (storePath.isDerivation()) { + std::map<std::string, StorePath> outputs; + for (auto & [name, output] : store->readDerivation(storePath).outputs) + outputs.emplace(name, output.path); + return { + Buildable { + .drvPath = storePath, + .outputs = std::move(outputs) + } + }; + } else { + return { + Buildable { + .drvPath = {}, + .outputs = {{"out", storePath}} + } + }; + } } std::optional<StorePath> getStorePath() override @@ -118,146 +328,325 @@ struct InstallableStorePath : Installable } }; -struct InstallableValue : Installable +Buildables InstallableValue::toBuildables() +{ + Buildables res; + + StorePathSet drvPaths; + + for (auto & drv : toDerivations()) { + Buildable b{.drvPath = drv.drvPath}; + drvPaths.insert(drv.drvPath); + + auto outputName = drv.outputName; + if (outputName == "") + throw Error("derivation '%s' lacks an 'outputName' attribute", state->store->printStorePath(*b.drvPath)); + + b.outputs.emplace(outputName, drv.outPath); + + res.push_back(std::move(b)); + } + + // Hack to recognize .all: if all drvs have the same drvPath, + // merge the buildables. + if (drvPaths.size() == 1) { + Buildable b{.drvPath = *drvPaths.begin()}; + for (auto & b2 : res) + for (auto & output : b2.outputs) + b.outputs.insert_or_assign(output.first, output.second); + Buildables bs; + bs.push_back(std::move(b)); + return bs; + } else + return res; +} + +struct InstallableAttrPath : InstallableValue { SourceExprCommand & cmd; + RootValue v; + std::string attrPath; - InstallableValue(SourceExprCommand & cmd) : cmd(cmd) { } + InstallableAttrPath(ref<EvalState> state, SourceExprCommand & cmd, Value * v, const std::string & attrPath) + : InstallableValue(state), cmd(cmd), v(allocRootValue(v)), attrPath(attrPath) + { } - Buildables toBuildables() override + std::string what() override { return attrPath; } + + std::pair<Value *, Pos> toValue(EvalState & state) override { - auto state = cmd.getEvalState(); + auto [vRes, pos] = findAlongAttrPath(state, attrPath, *cmd.getAutoArgs(state), **v); + state.forceValue(*vRes); + return {vRes, pos}; + } - auto v = toValue(*state).first; + virtual std::vector<InstallableValue::DerivationInfo> toDerivations() override; +}; - Bindings & autoArgs = *cmd.getAutoArgs(*state); +std::vector<InstallableValue::DerivationInfo> InstallableAttrPath::toDerivations() +{ + auto v = toValue(*state).first; - DrvInfos drvs; - getDerivations(*state, *v, "", autoArgs, drvs, false); + Bindings & autoArgs = *cmd.getAutoArgs(*state); - Buildables res; + DrvInfos drvInfos; + getDerivations(*state, *v, "", autoArgs, drvInfos, false); - StorePathSet drvPaths; + std::vector<DerivationInfo> res; + for (auto & drvInfo : drvInfos) { + res.push_back({ + state->store->parseStorePath(drvInfo.queryDrvPath()), + state->store->parseStorePath(drvInfo.queryOutPath()), + drvInfo.queryOutputName() + }); + } - for (auto & drv : drvs) { - Buildable b{.drvPath = state->store->parseStorePath(drv.queryDrvPath())}; - drvPaths.insert(*b.drvPath); + return res; +} - auto outputName = drv.queryOutputName(); - if (outputName == "") - throw Error("derivation '%s' lacks an 'outputName' attribute", state->store->printStorePath(*b.drvPath)); +std::vector<std::string> InstallableFlake::getActualAttrPaths() +{ + std::vector<std::string> res; - b.outputs.emplace(outputName, state->store->parseStorePath(drv.queryOutPath())); + for (auto & prefix : prefixes) + res.push_back(prefix + *attrPaths.begin()); - res.push_back(std::move(b)); - } + for (auto & s : attrPaths) + res.push_back(s); + + return res; +} + +Value * InstallableFlake::getFlakeOutputs(EvalState & state, const flake::LockedFlake & lockedFlake) +{ + auto vFlake = state.allocValue(); + + callFlake(state, lockedFlake, *vFlake); - // Hack to recognize .all: if all drvs have the same drvPath, - // merge the buildables. - if (drvPaths.size() == 1) { - Buildable b{.drvPath = *drvPaths.begin()}; - for (auto & b2 : res) - for (auto & output : b2.outputs) - b.outputs.insert_or_assign(output.first, output.second); - Buildables bs; - bs.push_back(std::move(b)); - return bs; - } else - return res; + auto aOutputs = vFlake->attrs->get(state.symbols.create("outputs")); + assert(aOutputs); + + state.forceValue(*aOutputs->value); + + return aOutputs->value; +} + +ref<eval_cache::EvalCache> openEvalCache( + EvalState & state, + std::shared_ptr<flake::LockedFlake> lockedFlake, + bool useEvalCache) +{ + auto fingerprint = lockedFlake->getFingerprint(); + return make_ref<nix::eval_cache::EvalCache>( + useEvalCache && evalSettings.pureEval + ? std::optional { std::cref(fingerprint) } + : std::nullopt, + state, + [&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); + + state.forceAttrs(*vFlake); + + auto aOutputs = vFlake->attrs->get(state.symbols.create("outputs")); + assert(aOutputs); + + return aOutputs->value; + }); +} + +static std::string showAttrPaths(const std::vector<std::string> & paths) +{ + std::string s; + for (const auto & [n, i] : enumerate(paths)) { + if (n > 0) s += n + 1 == paths.size() ? " or " : ", "; + s += '\''; s += i; s += '\''; } -}; + return s; +} -struct InstallableExpr : InstallableValue +std::tuple<std::string, FlakeRef, InstallableValue::DerivationInfo> InstallableFlake::toDerivation() { - std::string text; - InstallableExpr(SourceExprCommand & cmd, const std::string & text) - : InstallableValue(cmd), text(text) { } + auto lockedFlake = getLockedFlake(); - std::string what() override { return text; } + auto cache = openEvalCache(*state, lockedFlake, true); + auto root = cache->getRoot(); - std::pair<Value *, Pos> toValue(EvalState & state) override - { - auto v = state.allocValue(); - state.eval(state.parseExprFromString(text, absPath(".")), *v); - return {v, noPos}; + for (auto & attrPath : getActualAttrPaths()) { + auto attr = root->findAlongAttrPath(parseAttrPath(*state, attrPath)); + if (!attr) continue; + + if (!attr->isDerivation()) + throw Error("flake output attribute '%s' is not a derivation", attrPath); + + auto drvPath = attr->forceDerivation(); + + auto drvInfo = DerivationInfo{ + std::move(drvPath), + state->store->parseStorePath(attr->getAttr(state->sOutPath)->getString()), + attr->getAttr(state->sOutputName)->getString() + }; + + return {attrPath, lockedFlake->flake.lockedRef, std::move(drvInfo)}; } -}; -struct InstallableAttrPath : InstallableValue + throw Error("flake '%s' does not provide attribute %s", + flakeRef, showAttrPaths(getActualAttrPaths())); +} + +std::vector<InstallableValue::DerivationInfo> InstallableFlake::toDerivations() { - std::string attrPath; + std::vector<DerivationInfo> res; + res.push_back(std::get<2>(toDerivation())); + return res; +} - InstallableAttrPath(SourceExprCommand & cmd, const std::string & attrPath) - : InstallableValue(cmd), attrPath(attrPath) - { } +std::pair<Value *, Pos> InstallableFlake::toValue(EvalState & state) +{ + auto lockedFlake = getLockedFlake(); - std::string what() override { return attrPath; } + auto vOutputs = getFlakeOutputs(state, *lockedFlake); - std::pair<Value *, Pos> toValue(EvalState & state) override - { - auto source = cmd.getSourceExpr(state); + auto emptyArgs = state.allocBindings(0); - Bindings & autoArgs = *cmd.getAutoArgs(state); + for (auto & attrPath : getActualAttrPaths()) { + try { + auto [v, pos] = findAlongAttrPath(state, attrPath, *emptyArgs, *vOutputs); + state.forceValue(*v); + return {v, pos}; + } catch (AttrPathNotFound & e) { + } + } - auto v = findAlongAttrPath(state, attrPath, autoArgs, *source).first; - state.forceValue(*v); + throw Error("flake '%s' does not provide attribute %s", + flakeRef, showAttrPaths(getActualAttrPaths())); +} - return {v, noPos}; +std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>> +InstallableFlake::getCursors(EvalState & state, bool useEvalCache) +{ + auto evalCache = openEvalCache(state, + std::make_shared<flake::LockedFlake>(lockFlake(state, flakeRef, lockFlags)), + useEvalCache); + + auto root = evalCache->getRoot(); + + std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>> res; + + for (auto & attrPath : getActualAttrPaths()) { + auto attr = root->findAlongAttrPath(parseAttrPath(state, attrPath)); + if (attr) res.push_back({attr, attrPath}); } -}; -// FIXME: extend -std::string attrRegex = R"([A-Za-z_][A-Za-z0-9-_+]*)"; -static std::regex attrPathRegex(fmt(R"(%1%(\.%1%)*)", attrRegex)); + return res; +} + +std::shared_ptr<flake::LockedFlake> InstallableFlake::getLockedFlake() const +{ + if (!_lockedFlake) + _lockedFlake = std::make_shared<flake::LockedFlake>(lockFlake(*state, flakeRef, lockFlags)); + return _lockedFlake; +} -static std::vector<std::shared_ptr<Installable>> parseInstallables( - SourceExprCommand & cmd, ref<Store> store, std::vector<std::string> ss, bool useDefaultInstallables) +FlakeRef InstallableFlake::nixpkgsFlakeRef() const { - std::vector<std::shared_ptr<Installable>> result; + auto lockedFlake = getLockedFlake(); - if (ss.empty() && useDefaultInstallables) { - if (cmd.file == "") - cmd.file = "."; - ss = {""}; + if (auto nixpkgsInput = lockedFlake->lockFile.findInput({"nixpkgs"})) { + if (auto lockedNode = std::dynamic_pointer_cast<const flake::LockedNode>(nixpkgsInput)) { + debug("using nixpkgs flake '%s'", lockedNode->lockedRef); + return std::move(lockedNode->lockedRef); + } } - for (auto & s : ss) { + return Installable::nixpkgsFlakeRef(); +} - if (s.compare(0, 1, "(") == 0) - result.push_back(std::make_shared<InstallableExpr>(cmd, s)); +std::vector<std::shared_ptr<Installable>> SourceExprCommand::parseInstallables( + ref<Store> store, std::vector<std::string> ss) +{ + std::vector<std::shared_ptr<Installable>> result; - else if (s.find("/") != std::string::npos) { + if (file || expr) { + if (file && expr) + throw UsageError("'--file' and '--expr' are exclusive"); - auto path = store->toStorePath(store->followLinksToStore(s)); + // FIXME: backward compatibility hack + if (file) evalSettings.pureEval = false; - if (store->isStorePath(path)) - result.push_back(std::make_shared<InstallableStorePath>(store, path)); + auto state = getEvalState(); + auto vFile = state->allocValue(); + + if (file) + state->evalFile(lookupFileArg(*state, *file), *vFile); + else { + auto e = state->parseExprFromString(*expr, absPath(".")); + state->eval(e, *vFile); } - else if (s == "" || std::regex_match(s, attrPathRegex)) - result.push_back(std::make_shared<InstallableAttrPath>(cmd, s)); + for (auto & s : ss) + result.push_back(std::make_shared<InstallableAttrPath>(state, *this, vFile, s == "." ? "" : s)); + + } else { + + for (auto & s : ss) { + std::exception_ptr ex; + + try { + auto [flakeRef, fragment] = parseFlakeRefWithFragment(s, absPath(".")); + result.push_back(std::make_shared<InstallableFlake>( + getEvalState(), std::move(flakeRef), + fragment == "" ? getDefaultFlakeAttrPaths() : Strings{fragment}, + getDefaultFlakeAttrPathPrefixes(), lockFlags)); + continue; + } catch (...) { + ex = std::current_exception(); + } + + if (s.find('/') != std::string::npos) { + try { + result.push_back(std::make_shared<InstallableStorePath>(store, store->followLinksToStorePath(s))); + continue; + } catch (BadStorePath &) { + } catch (...) { + if (!ex) + ex = std::current_exception(); + } + } + + std::rethrow_exception(ex); - else - throw UsageError("don't know what to do with argument '%s'", s); + /* + throw Error( + pathExists(s) + ? "path '%s' is not a flake or a store path" + : "don't know how to handle argument '%s'", s); + */ + } } return result; } -std::shared_ptr<Installable> parseInstallable( - SourceExprCommand & cmd, ref<Store> store, const std::string & installable, - bool useDefaultInstallables) +std::shared_ptr<Installable> SourceExprCommand::parseInstallable( + ref<Store> store, const std::string & installable) { - auto installables = parseInstallables(cmd, store, {installable}, false); + auto installables = parseInstallables(store, {installable}); assert(installables.size() == 1); return installables.front(); } -Buildables build(ref<Store> store, RealiseMode mode, +Buildables build(ref<Store> store, Realise mode, std::vector<std::shared_ptr<Installable>> installables) { - if (mode != Build) + if (mode == Realise::Nothing) settings.readOnlyMode = true; Buildables buildables; @@ -278,33 +667,45 @@ Buildables build(ref<Store> store, RealiseMode mode, } } - if (mode == DryRun) + if (mode == Realise::Nothing) printMissing(store, pathsToBuild, lvlError); - else if (mode == Build) + else if (mode == Realise::Outputs) store->buildPaths(pathsToBuild); return buildables; } -StorePathSet toStorePaths(ref<Store> store, RealiseMode mode, +StorePathSet toStorePaths(ref<Store> store, + Realise mode, OperateOn operateOn, std::vector<std::shared_ptr<Installable>> installables) { StorePathSet outPaths; - for (auto & b : build(store, mode, installables)) - for (auto & output : b.outputs) - outPaths.insert(output.second); + if (operateOn == OperateOn::Output) { + for (auto & b : build(store, mode, installables)) + for (auto & output : b.outputs) + outPaths.insert(output.second); + } else { + if (mode == Realise::Nothing) + settings.readOnlyMode = true; + + for (auto & i : installables) + for (auto & b : i->toBuildables()) + if (b.drvPath) + outPaths.insert(*b.drvPath); + } return outPaths; } -StorePath toStorePath(ref<Store> store, RealiseMode mode, +StorePath toStorePath(ref<Store> store, + Realise mode, OperateOn operateOn, std::shared_ptr<Installable> installable) { - auto paths = toStorePaths(store, mode, {installable}); + auto paths = toStorePaths(store, mode, operateOn, {installable}); if (paths.size() != 1) - throw Error("argument '%s' should evaluate to one store path", installable->what()); + throw Error("argument '%s' should evaluate to one store path", installable->what()); return *paths.begin(); } @@ -333,14 +734,51 @@ StorePathSet toDerivations(ref<Store> store, return drvPaths; } +InstallablesCommand::InstallablesCommand() +{ + expectArgs({ + .label = "installables", + .handler = {&_installables}, + .completer = {[&](size_t, std::string_view prefix) { + completeInstallable(prefix); + }} + }); +} + void InstallablesCommand::prepare() { - installables = parseInstallables(*this, getStore(), _installables, useDefaultInstallables()); + if (_installables.empty() && useDefaultInstallables()) + // FIXME: commands like "nix install" should not have a + // default, probably. + _installables.push_back("."); + installables = parseInstallables(getStore(), _installables); +} + +std::optional<FlakeRef> InstallablesCommand::getFlakeRefForCompletion() +{ + if (_installables.empty()) { + if (useDefaultInstallables()) + return parseFlakeRef(".", absPath(".")); + return {}; + } + return parseFlakeRef(_installables.front(), absPath(".")); +} + +InstallableCommand::InstallableCommand() +{ + expectArgs({ + .label = "installable", + .optional = true, + .handler = {&_installable}, + .completer = {[&](size_t, std::string_view prefix) { + completeInstallable(prefix); + }} + }); } void InstallableCommand::prepare() { - installable = parseInstallable(*this, getStore(), _installable, false); + installable = parseInstallable(getStore(), _installable); } } diff --git a/src/nix/installables.hh b/src/nix/installables.hh index 503984220..eb34365d4 100644 --- a/src/nix/installables.hh +++ b/src/nix/installables.hh @@ -3,11 +3,17 @@ #include "util.hh" #include "path.hh" #include "eval.hh" +#include "flake/flake.hh" #include <optional> namespace nix { +struct DrvInfo; +struct SourceExprCommand; + +namespace eval_cache { class EvalCache; class AttrCursor; } + struct Buildable { std::optional<StorePath> drvPath; @@ -16,19 +22,25 @@ struct Buildable typedef std::vector<Buildable> Buildables; +struct App +{ + std::vector<StorePathWithOutputs> context; + Path program; + // FIXME: add args, sandbox settings, metadata, ... +}; + struct Installable { virtual ~Installable() { } virtual std::string what() = 0; - virtual Buildables toBuildables() - { - throw Error("argument '%s' cannot be built", what()); - } + virtual Buildables toBuildables() = 0; Buildable toBuildable(); + App toApp(EvalState & state); + virtual std::pair<Value *, Pos> toValue(EvalState & state) { throw Error("argument '%s' cannot be evaluated", what()); @@ -40,6 +52,74 @@ struct Installable { return {}; } + + virtual std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>> + getCursors(EvalState & state, bool useEvalCache); + + std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string> + getCursor(EvalState & state, bool useEvalCache); + + virtual FlakeRef nixpkgsFlakeRef() const + { + return std::move(FlakeRef::fromAttrs({{"type","indirect"}, {"id", "nixpkgs"}})); + } }; +struct InstallableValue : Installable +{ + ref<EvalState> state; + + InstallableValue(ref<EvalState> state) : state(state) {} + + struct DerivationInfo + { + StorePath drvPath; + StorePath outPath; + std::string outputName; + }; + + virtual std::vector<DerivationInfo> toDerivations() = 0; + + Buildables toBuildables() override; +}; + +struct InstallableFlake : InstallableValue +{ + FlakeRef flakeRef; + Strings attrPaths; + Strings prefixes; + const flake::LockFlags & lockFlags; + mutable std::shared_ptr<flake::LockedFlake> _lockedFlake; + + InstallableFlake(ref<EvalState> state, FlakeRef && flakeRef, + Strings && attrPaths, Strings && prefixes, const flake::LockFlags & lockFlags) + : InstallableValue(state), flakeRef(flakeRef), attrPaths(attrPaths), + prefixes(prefixes), lockFlags(lockFlags) + { } + + std::string what() override { return flakeRef.to_string() + "#" + *attrPaths.begin(); } + + std::vector<std::string> getActualAttrPaths(); + + Value * getFlakeOutputs(EvalState & state, const flake::LockedFlake & lockedFlake); + + std::tuple<std::string, FlakeRef, DerivationInfo> toDerivation(); + + std::vector<DerivationInfo> toDerivations() override; + + std::pair<Value *, Pos> toValue(EvalState & state) override; + + std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>> + getCursors(EvalState & state, bool useEvalCache) override; + + std::shared_ptr<flake::LockedFlake> getLockedFlake() const; + + FlakeRef nixpkgsFlakeRef() const override; +}; + +ref<eval_cache::EvalCache> openEvalCache( + EvalState & state, + std::shared_ptr<flake::LockedFlake> lockedFlake, + bool useEvalCache); + } diff --git a/src/nix/log.cc b/src/nix/log.cc index 3fe22f6c2..7e10d373a 100644 --- a/src/nix/log.cc +++ b/src/nix/log.cc @@ -18,7 +18,7 @@ struct CmdLog : InstallableCommand return { Example{ "To get the build log of GNU Hello:", - "nix log nixpkgs.hello" + "nix log nixpkgs#hello" }, Example{ "To get the build log of a specific path:", @@ -26,7 +26,7 @@ struct CmdLog : InstallableCommand }, Example{ "To get a build log from a specific binary cache:", - "nix log --store https://cache.nixos.org nixpkgs.hello" + "nix log --store https://cache.nixos.org nixpkgs#hello" }, }; } diff --git a/src/nix/ls.cc b/src/nix/ls.cc index d2157f2d4..76c8bc9a3 100644 --- a/src/nix/ls.cc +++ b/src/nix/ls.cc @@ -85,7 +85,11 @@ struct CmdLsStore : StoreCommand, MixLs { CmdLsStore() { - expectArg("path", &path); + expectArgs({ + .label = "path", + .handler = {&path}, + .completer = completePath + }); } Examples examples() override @@ -117,7 +121,11 @@ struct CmdLsNar : Command, MixLs CmdLsNar() { - expectArg("nar", &narPath); + expectArgs({ + .label = "nar", + .handler = {&narPath}, + .completer = completePath + }); expectArg("path", &path); } diff --git a/src/nix/main.cc b/src/nix/main.cc index 203901168..e62657e95 100644 --- a/src/nix/main.cc +++ b/src/nix/main.cc @@ -7,7 +7,6 @@ #include "legacy.hh" #include "shared.hh" #include "store-api.hh" -#include "progress-bar.hh" #include "filetransfer.hh" #include "finally.hh" #include "loggers.hh" @@ -69,7 +68,7 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs addFlag({ .longName = "help", .description = "show usage information", - .handler = {[&]() { showHelpAndExit(); }}, + .handler = {[&]() { if (!completions) showHelpAndExit(); }}, }); addFlag({ @@ -97,7 +96,7 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs addFlag({ .longName = "version", .description = "show version information", - .handler = {[&]() { printVersion(programName); }}, + .handler = {[&]() { if (!completions) printVersion(programName); }}, }); addFlag({ @@ -165,6 +164,7 @@ void mainWrapped(int argc, char * * argv) verbosity = lvlWarn; settings.verboseBuild = false; + evalSettings.pureEval = true; setLogFormat("bar"); @@ -172,7 +172,22 @@ void mainWrapped(int argc, char * * argv) NixArgs args; - args.parseCmdline(argvToStrings(argc, argv)); + Finally printCompletions([&]() + { + if (completions) { + std::cout << (pathCompletions ? "filenames\n" : "no-filenames\n"); + for (auto & s : *completions) + std::cout << s << "\n"; + } + }); + + try { + args.parseCmdline(argvToStrings(argc, argv)); + } catch (UsageError &) { + if (!completions) throw; + } + + if (completions) return; initPlugins(); diff --git a/src/nix/make-content-addressable.cc b/src/nix/make-content-addressable.cc index 80a5cddd3..2fe2e2fb2 100644 --- a/src/nix/make-content-addressable.cc +++ b/src/nix/make-content-addressable.cc @@ -10,7 +10,7 @@ struct CmdMakeContentAddressable : StorePathsCommand, MixJSON { CmdMakeContentAddressable() { - realiseMode = Build; + realiseMode = Realise::Outputs; } std::string description() override @@ -23,7 +23,7 @@ struct CmdMakeContentAddressable : StorePathsCommand, MixJSON return { Example{ "To create a content-addressable representation of GNU Hello (but not its dependencies):", - "nix make-content-addressable nixpkgs.hello" + "nix make-content-addressable nixpkgs#hello" }, Example{ "To compute a content-addressable representation of the current NixOS system closure:", diff --git a/src/nix/path-info.cc b/src/nix/path-info.cc index b89a44f83..65f73cd94 100644 --- a/src/nix/path-info.cc +++ b/src/nix/path-info.cc @@ -40,7 +40,7 @@ struct CmdPathInfo : StorePathsCommand, MixJSON }, Example{ "To show a package's closure size and all its dependencies with human readable sizes:", - "nix path-info -rsSh nixpkgs.rust" + "nix path-info -rsSh nixpkgs#rust" }, Example{ "To check the existence of a path in a binary cache:", diff --git a/src/nix/profile.cc b/src/nix/profile.cc new file mode 100644 index 000000000..e04079603 --- /dev/null +++ b/src/nix/profile.cc @@ -0,0 +1,428 @@ +#include "command.hh" +#include "common-args.hh" +#include "shared.hh" +#include "store-api.hh" +#include "derivations.hh" +#include "archive.hh" +#include "builtins/buildenv.hh" +#include "flake/flakeref.hh" +#include "../nix-env/user-env.hh" + +#include <nlohmann/json.hpp> +#include <regex> + +using namespace nix; + +struct ProfileElementSource +{ + FlakeRef originalRef; + // FIXME: record original attrpath. + FlakeRef resolvedRef; + std::string attrPath; + // FIXME: output names +}; + +struct ProfileElement +{ + StorePathSet storePaths; + std::optional<ProfileElementSource> source; + bool active = true; + // FIXME: priority +}; + +struct ProfileManifest +{ + std::vector<ProfileElement> elements; + + ProfileManifest() { } + + ProfileManifest(EvalState & state, const Path & profile) + { + auto manifestPath = profile + "/manifest.json"; + + if (pathExists(manifestPath)) { + auto json = nlohmann::json::parse(readFile(manifestPath)); + + auto version = json.value("version", 0); + if (version != 1) + throw Error("profile manifest '%s' has unsupported version %d", manifestPath, version); + + for (auto & e : json["elements"]) { + ProfileElement element; + for (auto & p : e["storePaths"]) + element.storePaths.insert(state.store->parseStorePath((std::string) p)); + element.active = e["active"]; + if (e.value("uri", "") != "") { + element.source = ProfileElementSource{ + parseFlakeRef(e["originalUri"]), + parseFlakeRef(e["uri"]), + e["attrPath"] + }; + } + elements.emplace_back(std::move(element)); + } + } + + else if (pathExists(profile + "/manifest.nix")) { + // FIXME: needed because of pure mode; ugly. + if (state.allowedPaths) { + state.allowedPaths->insert(state.store->followLinksToStore(profile)); + state.allowedPaths->insert(state.store->followLinksToStore(profile + "/manifest.nix")); + } + + auto drvInfos = queryInstalled(state, state.store->followLinksToStore(profile)); + + for (auto & drvInfo : drvInfos) { + ProfileElement element; + element.storePaths = {state.store->parseStorePath(drvInfo.queryOutPath())}; + elements.emplace_back(std::move(element)); + } + } + } + + std::string toJSON(Store & store) const + { + auto array = nlohmann::json::array(); + for (auto & element : elements) { + auto paths = nlohmann::json::array(); + for (auto & path : element.storePaths) + paths.push_back(store.printStorePath(path)); + nlohmann::json obj; + obj["storePaths"] = paths; + obj["active"] = element.active; + if (element.source) { + obj["originalUri"] = element.source->originalRef.to_string(); + obj["uri"] = element.source->resolvedRef.to_string(); + obj["attrPath"] = element.source->attrPath; + } + array.push_back(obj); + } + nlohmann::json json; + json["version"] = 1; + json["elements"] = array; + return json.dump(); + } + + StorePath build(ref<Store> store) + { + auto tempDir = createTempDir(); + + StorePathSet references; + + Packages pkgs; + for (auto & element : elements) { + for (auto & path : element.storePaths) { + if (element.active) + pkgs.emplace_back(store->printStorePath(path), true, 5); + references.insert(path); + } + } + + buildProfile(tempDir, std::move(pkgs)); + + writeFile(tempDir + "/manifest.json", toJSON(*store)); + + /* Add the symlink tree to the store. */ + StringSink sink; + dumpPath(tempDir, sink); + + auto narHash = hashString(htSHA256, *sink.s); + + ValidPathInfo info(store->makeFixedOutputPath(FileIngestionMethod::Recursive, narHash, "profile", references)); + info.references = std::move(references); + info.narHash = narHash; + info.narSize = sink.s->size(); + info.ca = FixedOutputHash { .method = FileIngestionMethod::Recursive, .hash = *info.narHash }; + + auto source = StringSource { *sink.s }; + store->addToStore(info, source); + + return std::move(info.path); + } +}; + +struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile +{ + std::string description() override + { + return "install a package into a profile"; + } + + Examples examples() override + { + return { + Example{ + "To install a package from Nixpkgs:", + "nix profile install nixpkgs#hello" + }, + Example{ + "To install a package from a specific branch of Nixpkgs:", + "nix profile install nixpkgs/release-19.09#hello" + }, + Example{ + "To install a package from a specific revision of Nixpkgs:", + "nix profile install nixpkgs/1028bb33859f8dfad7f98e1c8d185f3d1aaa7340#hello" + }, + }; + } + + void run(ref<Store> store) override + { + ProfileManifest manifest(*getEvalState(), *profile); + + std::vector<StorePathWithOutputs> pathsToBuild; + + for (auto & installable : installables) { + if (auto installable2 = std::dynamic_pointer_cast<InstallableFlake>(installable)) { + auto [attrPath, resolvedRef, drv] = installable2->toDerivation(); + + ProfileElement element; + element.storePaths = {drv.outPath}; // FIXME + element.source = ProfileElementSource{ + installable2->flakeRef, + resolvedRef, + attrPath, + }; + + pathsToBuild.push_back({drv.drvPath, StringSet{"out"}}); // FIXME + + manifest.elements.emplace_back(std::move(element)); + } else + throw Error("'nix profile install' does not support argument '%s'", installable->what()); + } + + store->buildPaths(pathsToBuild); + + updateProfile(manifest.build(store)); + } +}; + +class MixProfileElementMatchers : virtual Args +{ + std::vector<std::string> _matchers; + +public: + + MixProfileElementMatchers() + { + expectArgs("elements", &_matchers); + } + + typedef std::variant<size_t, Path, std::regex> Matcher; + + std::vector<Matcher> getMatchers(ref<Store> store) + { + std::vector<Matcher> res; + + for (auto & s : _matchers) { + size_t n; + if (string2Int(s, n)) + res.push_back(n); + else if (store->isStorePath(s)) + res.push_back(s); + else + res.push_back(std::regex(s, std::regex::extended | std::regex::icase)); + } + + return res; + } + + bool matches(const Store & store, const ProfileElement & element, size_t pos, const std::vector<Matcher> & matchers) + { + for (auto & matcher : matchers) { + if (auto n = std::get_if<size_t>(&matcher)) { + if (*n == pos) return true; + } else if (auto path = std::get_if<Path>(&matcher)) { + if (element.storePaths.count(store.parseStorePath(*path))) return true; + } else if (auto regex = std::get_if<std::regex>(&matcher)) { + if (element.source + && std::regex_match(element.source->attrPath, *regex)) + return true; + } + } + + return false; + } +}; + +struct CmdProfileRemove : virtual EvalCommand, MixDefaultProfile, MixProfileElementMatchers +{ + std::string description() override + { + return "remove packages from a profile"; + } + + Examples examples() override + { + return { + Example{ + "To remove a package by attribute path:", + "nix profile remove packages.x86_64-linux.hello" + }, + Example{ + "To remove all packages:", + "nix profile remove '.*'" + }, + Example{ + "To remove a package by store path:", + "nix profile remove /nix/store/rr3y0c6zyk7kjjl8y19s4lsrhn4aiq1z-hello-2.10" + }, + Example{ + "To remove a package by position:", + "nix profile remove 3" + }, + }; + } + + void run(ref<Store> store) override + { + ProfileManifest oldManifest(*getEvalState(), *profile); + + auto matchers = getMatchers(store); + + ProfileManifest newManifest; + + for (size_t i = 0; i < oldManifest.elements.size(); ++i) { + auto & element(oldManifest.elements[i]); + if (!matches(*store, element, i, matchers)) + newManifest.elements.push_back(std::move(element)); + } + + // FIXME: warn about unused matchers? + + printInfo("removed %d packages, kept %d packages", + oldManifest.elements.size() - newManifest.elements.size(), + newManifest.elements.size()); + + updateProfile(newManifest.build(store)); + } +}; + +struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProfileElementMatchers +{ + std::string description() override + { + return "upgrade packages using their most recent flake"; + } + + Examples examples() override + { + return { + Example{ + "To upgrade all packages that were installed using a mutable flake reference:", + "nix profile upgrade '.*'" + }, + Example{ + "To upgrade a specific package:", + "nix profile upgrade packages.x86_64-linux.hello" + }, + }; + } + + void run(ref<Store> store) override + { + ProfileManifest manifest(*getEvalState(), *profile); + + auto matchers = getMatchers(store); + + // FIXME: code duplication + std::vector<StorePathWithOutputs> pathsToBuild; + + for (size_t i = 0; i < manifest.elements.size(); ++i) { + auto & element(manifest.elements[i]); + if (element.source + && !element.source->originalRef.input.isImmutable() + && matches(*store, element, i, matchers)) + { + Activity act(*logger, lvlChatty, actUnknown, + fmt("checking '%s' for updates", element.source->attrPath)); + + InstallableFlake installable(getEvalState(), FlakeRef(element.source->originalRef), {element.source->attrPath}, {}, lockFlags); + + auto [attrPath, resolvedRef, drv] = installable.toDerivation(); + + if (element.source->resolvedRef == resolvedRef) continue; + + printInfo("upgrading '%s' from flake '%s' to '%s'", + element.source->attrPath, element.source->resolvedRef, resolvedRef); + + element.storePaths = {drv.outPath}; // FIXME + element.source = ProfileElementSource{ + installable.flakeRef, + resolvedRef, + attrPath, + }; + + pathsToBuild.push_back({drv.drvPath, StringSet{"out"}}); // FIXME + } + } + + store->buildPaths(pathsToBuild); + + updateProfile(manifest.build(store)); + } +}; + +struct CmdProfileInfo : virtual EvalCommand, virtual StoreCommand, MixDefaultProfile +{ + std::string description() override + { + return "list installed packages"; + } + + Examples examples() override + { + return { + Example{ + "To show what packages are installed in the default profile:", + "nix profile info" + }, + }; + } + + void run(ref<Store> store) override + { + ProfileManifest manifest(*getEvalState(), *profile); + + for (size_t i = 0; i < manifest.elements.size(); ++i) { + auto & element(manifest.elements[i]); + logger->stdout("%d %s %s %s", i, + element.source ? element.source->originalRef.to_string() + "#" + element.source->attrPath : "-", + element.source ? element.source->resolvedRef.to_string() + "#" + element.source->attrPath : "-", + concatStringsSep(" ", store->printStorePathSet(element.storePaths))); + } + } +}; + +struct CmdProfile : virtual MultiCommand, virtual Command +{ + CmdProfile() + : MultiCommand({ + {"install", []() { return make_ref<CmdProfileInstall>(); }}, + {"remove", []() { return make_ref<CmdProfileRemove>(); }}, + {"upgrade", []() { return make_ref<CmdProfileUpgrade>(); }}, + {"info", []() { return make_ref<CmdProfileInfo>(); }}, + }) + { } + + std::string description() override + { + return "manage Nix profiles"; + } + + void run() override + { + if (!command) + throw UsageError("'nix profile' requires a sub-command."); + command->second->prepare(); + command->second->run(); + } + + void printHelp(const string & programName, std::ostream & out) override + { + MultiCommand::printHelp(programName, out); + } +}; + +static auto r1 = registerCommand<CmdProfile>("profile"); + diff --git a/src/nix/registry.cc b/src/nix/registry.cc new file mode 100644 index 000000000..16d7e511f --- /dev/null +++ b/src/nix/registry.cc @@ -0,0 +1,150 @@ +#include "command.hh" +#include "common-args.hh" +#include "shared.hh" +#include "eval.hh" +#include "flake/flake.hh" +#include "store-api.hh" +#include "fetchers.hh" +#include "registry.hh" + +using namespace nix; +using namespace nix::flake; + +struct CmdRegistryList : StoreCommand +{ + std::string description() override + { + return "list available Nix flakes"; + } + + void run(nix::ref<nix::Store> store) override + { + using namespace fetchers; + + auto registries = getRegistries(store); + + for (auto & registry : registries) { + for (auto & entry : registry->entries) { + // FIXME: format nicely + logger->stdout("%s %s %s", + registry->type == Registry::Flag ? "flags " : + registry->type == Registry::User ? "user " : + registry->type == Registry::System ? "system" : + "global", + entry.from.to_string(), + entry.to.to_string()); + } + } + } +}; + +struct CmdRegistryAdd : MixEvalArgs, Command +{ + std::string fromUrl, toUrl; + + std::string description() override + { + return "add/replace flake in user flake registry"; + } + + CmdRegistryAdd() + { + expectArg("from-url", &fromUrl); + expectArg("to-url", &toUrl); + } + + void run() override + { + auto fromRef = parseFlakeRef(fromUrl); + auto toRef = parseFlakeRef(toUrl); + fetchers::Attrs extraAttrs; + if (toRef.subdir != "") extraAttrs["dir"] = toRef.subdir; + auto userRegistry = fetchers::getUserRegistry(); + userRegistry->remove(fromRef.input); + userRegistry->add(fromRef.input, toRef.input, extraAttrs); + userRegistry->write(fetchers::getUserRegistryPath()); + } +}; + +struct CmdRegistryRemove : virtual Args, MixEvalArgs, Command +{ + std::string url; + + std::string description() override + { + return "remove flake from user flake registry"; + } + + CmdRegistryRemove() + { + expectArg("url", &url); + } + + void run() override + { + auto userRegistry = fetchers::getUserRegistry(); + userRegistry->remove(parseFlakeRef(url).input); + userRegistry->write(fetchers::getUserRegistryPath()); + } +}; + +struct CmdRegistryPin : virtual Args, EvalCommand +{ + std::string url; + + std::string description() override + { + return "pin a flake to its current version in user flake registry"; + } + + CmdRegistryPin() + { + expectArg("url", &url); + } + + void run(nix::ref<nix::Store> store) override + { + auto ref = parseFlakeRef(url); + auto userRegistry = fetchers::getUserRegistry(); + userRegistry->remove(ref.input); + auto [tree, resolved] = ref.resolve(store).input.fetch(store); + fetchers::Attrs extraAttrs; + if (ref.subdir != "") extraAttrs["dir"] = ref.subdir; + userRegistry->add(ref.input, resolved, extraAttrs); + } +}; + +struct CmdRegistry : virtual MultiCommand, virtual Command +{ + CmdRegistry() + : MultiCommand({ + {"list", []() { return make_ref<CmdRegistryList>(); }}, + {"add", []() { return make_ref<CmdRegistryAdd>(); }}, + {"remove", []() { return make_ref<CmdRegistryRemove>(); }}, + {"pin", []() { return make_ref<CmdRegistryPin>(); }}, + }) + { + } + + std::string description() override + { + return "manage the flake registry"; + } + + Category category() override { return catSecondary; } + + void run() override + { + if (!command) + throw UsageError("'nix registry' requires a sub-command."); + command->second->prepare(); + command->second->run(); + } + + void printHelp(const string & programName, std::ostream & out) override + { + MultiCommand::printHelp(programName, out); + } +}; + +static auto r1 = registerCommand<CmdRegistry>("registry"); diff --git a/src/nix/repl.cc b/src/nix/repl.cc index fdacf604b..8eb58f62a 100644 --- a/src/nix/repl.cc +++ b/src/nix/repl.cc @@ -760,7 +760,11 @@ struct CmdRepl : StoreCommand, MixEvalArgs CmdRepl() { - expectArgs("files", &files); + expectArgs({ + .label = "files", + .handler = {&files}, + .completer = completePath + }); } std::string description() override @@ -780,6 +784,7 @@ struct CmdRepl : StoreCommand, MixEvalArgs void run(ref<Store> store) override { + evalSettings.pureEval = false; auto repl = std::make_unique<NixRepl>(searchPath, openStore()); repl->autoArgs = getAutoArgs(*repl->state); repl->mainLoop(files); diff --git a/src/nix/run.cc b/src/nix/run.cc index 321ee1d11..cbaba9d90 100644 --- a/src/nix/run.cc +++ b/src/nix/run.cc @@ -84,31 +84,30 @@ struct CmdShell : InstallablesCommand, RunCommon, MixEnvironment { return { Example{ - "To start a shell providing GNU Hello from NixOS 17.03:", - "nix shell -f channel:nixos-17.03 hello" + "To start a shell providing GNU Hello from NixOS 20.03:", + "nix shell nixpkgs/nixos-20.03#hello" }, Example{ "To start a shell providing youtube-dl from your 'nixpkgs' channel:", - "nix shell nixpkgs.youtube-dl" + "nix shell nixpkgs#youtube-dl" }, Example{ "To run GNU Hello:", - "nix shell nixpkgs.hello -c hello --greeting 'Hi everybody!'" + "nix shell nixpkgs#hello -c hello --greeting 'Hi everybody!'" }, Example{ "To run GNU Hello in a chroot store:", - "nix shell --store ~/my-nix nixpkgs.hello -c hello" + "nix shell --store ~/my-nix nixpkgs#hello -c hello" }, }; } void run(ref<Store> store) override { - auto outPaths = toStorePaths(store, Build, installables); + auto outPaths = toStorePaths(store, Realise::Outputs, OperateOn::Output, installables); auto accessor = store->getFSAccessor(); - std::unordered_set<StorePath> done; std::queue<StorePath> todo; for (auto & path : outPaths) todo.push(path); @@ -143,6 +142,67 @@ struct CmdShell : InstallablesCommand, RunCommon, MixEnvironment static auto r1 = registerCommand<CmdShell>("shell"); +struct CmdRun : InstallableCommand, RunCommon +{ + std::vector<std::string> args; + + CmdRun() + { + expectArgs({ + .label = "args", + .handler = {&args}, + .completer = completePath + }); + } + + std::string description() override + { + return "run a Nix application"; + } + + Examples examples() override + { + return { + Example{ + "To run Blender:", + "nix run blender-bin" + }, + }; + } + + Strings getDefaultFlakeAttrPaths() override + { + Strings res{"defaultApp." + settings.thisSystem.get()}; + for (auto & s : SourceExprCommand::getDefaultFlakeAttrPaths()) + res.push_back(s); + return res; + } + + Strings getDefaultFlakeAttrPathPrefixes() override + { + Strings res{"apps." + settings.thisSystem.get() + ".", "packages"}; + for (auto & s : SourceExprCommand::getDefaultFlakeAttrPathPrefixes()) + res.push_back(s); + return res; + } + + void run(ref<Store> store) override + { + auto state = getEvalState(); + + auto app = installable->toApp(*state); + + state->store->buildPaths(app.context); + + Strings allArgs{app.program}; + for (auto & i : args) allArgs.push_back(i); + + runProgram(store, app.program, allArgs); + } +}; + +static auto r2 = registerCommand<CmdRun>("run"); + void chrootHelper(int argc, char * * argv) { int p = 1; diff --git a/src/nix/search.cc b/src/nix/search.cc index 93c3f3f83..65a1e1818 100644 --- a/src/nix/search.cc +++ b/src/nix/search.cc @@ -6,8 +6,9 @@ #include "get-drvs.hh" #include "common-args.hh" #include "json.hh" -#include "json-to-value.hh" #include "shared.hh" +#include "eval-cache.hh" +#include "attr-path.hh" #include <regex> #include <fstream> @@ -25,33 +26,17 @@ std::string hilite(const std::string & s, const std::smatch & m, std::string pos m.empty() ? s : std::string(m.prefix()) - + ANSI_RED + std::string(m.str()) + postfix + + ANSI_GREEN + std::string(m.str()) + postfix + std::string(m.suffix()); } -struct CmdSearch : SourceExprCommand, MixJSON +struct CmdSearch : InstallableCommand, MixJSON { std::vector<std::string> res; - bool writeCache = true; - bool useCache = true; - CmdSearch() { expectArgs("regex", &res); - - addFlag({ - .longName = "update-cache", - .shortName = 'u', - .description = "update the package search cache", - .handler = {[&]() { writeCache = true; useCache = false; }} - }); - - addFlag({ - .longName = "no-cache", - .description = "do not use or update the package search cache", - .handler = {[&]() { writeCache = false; useCache = false; }} - }); } std::string description() override @@ -63,24 +48,32 @@ struct CmdSearch : SourceExprCommand, MixJSON { return { Example{ - "To show all available packages:", + "To show all packages in the flake in the current directory:", "nix search" }, Example{ - "To show any packages containing 'blender' in its name or description:", - "nix search blender" + "To show packages in the 'nixpkgs' flake containing 'blender' in its name or description:", + "nix search nixpkgs blender" }, Example{ "To search for Firefox or Chromium:", - "nix search 'firefox|chromium'" + "nix search nixpkgs 'firefox|chromium'" }, Example{ - "To search for git and frontend or gui:", - "nix search git 'frontend|gui'" + "To search for packages containing 'git' and either 'frontend' or 'gui':", + "nix search nixpkgs git 'frontend|gui'" } }; } + Strings getDefaultFlakeAttrPaths() override + { + return { + "packages." + settings.thisSystem.get() + ".", + "legacyPackages." + settings.thisSystem.get() + "." + }; + } + void run(ref<Store> store) override { settings.readOnlyMode = true; @@ -88,189 +81,107 @@ struct CmdSearch : SourceExprCommand, MixJSON // Empty search string should match all packages // Use "^" here instead of ".*" due to differences in resulting highlighting // (see #1893 -- libc++ claims empty search string is not in POSIX grammar) - if (res.empty()) { + if (res.empty()) res.push_back("^"); - } std::vector<std::regex> regexes; regexes.reserve(res.size()); - for (auto &re : res) { + for (auto & re : res) regexes.push_back(std::regex(re, std::regex::extended | std::regex::icase)); - } auto state = getEvalState(); auto jsonOut = json ? std::make_unique<JSONObject>(std::cout) : nullptr; - auto sToplevel = state->symbols.create("_toplevel"); - auto sRecurse = state->symbols.create("recurseForDerivations"); - - bool fromCache = false; + uint64_t results = 0; - std::map<std::string, std::string> results; - - std::function<void(Value *, std::string, bool, JSONObject *)> doExpr; - - doExpr = [&](Value * v, std::string attrPath, bool toplevel, JSONObject * cache) { - debug("at attribute '%s'", attrPath); + std::function<void(eval_cache::AttrCursor & cursor, const std::vector<Symbol> & attrPath)> visit; + visit = [&](eval_cache::AttrCursor & cursor, const std::vector<Symbol> & attrPath) + { + Activity act(*logger, lvlInfo, actUnknown, + fmt("evaluating '%s'", concatStringsSep(".", attrPath))); try { - uint found = 0; + auto recurse = [&]() + { + for (const auto & attr : cursor.getAttrs()) { + auto cursor2 = cursor.getAttr(attr); + auto attrPath2(attrPath); + attrPath2.push_back(attr); + visit(*cursor2, attrPath2); + } + }; - state->forceValue(*v); + if (cursor.isDerivation()) { + size_t found = 0; - if (v->type == tLambda && toplevel) { - Value * v2 = state->allocValue(); - state->autoCallFunction(*state->allocBindings(1), *v, *v2); - v = v2; - state->forceValue(*v); - } + DrvName name(cursor.getAttr("name")->getString()); - if (state->isDerivation(*v)) { + auto aMeta = cursor.maybeGetAttr("meta"); + auto aDescription = aMeta ? aMeta->maybeGetAttr("description") : nullptr; + auto description = aDescription ? aDescription->getString() : ""; + std::replace(description.begin(), description.end(), '\n', ' '); + auto attrPath2 = concatStringsSep(".", attrPath); - DrvInfo drv(*state, attrPath, v->attrs); - std::string description; std::smatch attrPathMatch; std::smatch descriptionMatch; std::smatch nameMatch; - std::string name; - - DrvName parsed(drv.queryName()); - - for (auto ®ex : regexes) { - std::regex_search(attrPath, attrPathMatch, regex); - name = parsed.name; - std::regex_search(name, nameMatch, regex); - - description = drv.queryMetaString("description"); - std::replace(description.begin(), description.end(), '\n', ' '); + for (auto & regex : regexes) { + std::regex_search(attrPath2, attrPathMatch, regex); + std::regex_search(name.name, nameMatch, regex); std::regex_search(description, descriptionMatch, regex); - if (!attrPathMatch.empty() || !nameMatch.empty() || !descriptionMatch.empty()) - { found++; - } } if (found == res.size()) { + results++; if (json) { - - auto jsonElem = jsonOut->object(attrPath); - - jsonElem.attr("pkgName", parsed.name); - jsonElem.attr("version", parsed.version); + auto jsonElem = jsonOut->object(attrPath2); + jsonElem.attr("pname", name.name); + jsonElem.attr("version", name.version); jsonElem.attr("description", description); - } else { - auto name = hilite(parsed.name, nameMatch, "\e[0;2m") - + std::string(parsed.fullName, parsed.name.length()); - results[attrPath] = fmt( - "* %s (%s)\n %s\n", - wrap("\e[0;1m", hilite(attrPath, attrPathMatch, "\e[0;1m")), - wrap("\e[0;2m", hilite(name, nameMatch, "\e[0;2m")), - hilite(description, descriptionMatch, ANSI_NORMAL)); - } - } - - if (cache) { - cache->attr("type", "derivation"); - cache->attr("name", drv.queryName()); - cache->attr("system", drv.querySystem()); - if (description != "") { - auto meta(cache->object("meta")); - meta.attr("description", description); + auto name2 = hilite(name.name, nameMatch, "\e[0;2m"); + if (results > 1) logger->stdout(""); + logger->stdout( + "* %s%s", + wrap("\e[0;1m", hilite(attrPath2, attrPathMatch, "\e[0;1m")), + name.version != "" ? " (" + name.version + ")" : ""); + if (description != "") + logger->stdout( + " %s", hilite(description, descriptionMatch, ANSI_NORMAL)); } } } - else if (v->type == tAttrs) { + else if ( + attrPath.size() == 0 + || (attrPath[0] == "legacyPackages" && attrPath.size() <= 2) + || (attrPath[0] == "packages" && attrPath.size() <= 2)) + recurse(); - if (!toplevel) { - auto attrs = v->attrs; - Bindings::iterator j = attrs->find(sRecurse); - if (j == attrs->end() || !state->forceBool(*j->value, *j->pos)) { - debug("skip attribute '%s'", attrPath); - return; - } - } - - bool toplevel2 = false; - if (!fromCache) { - Bindings::iterator j = v->attrs->find(sToplevel); - toplevel2 = j != v->attrs->end() && state->forceBool(*j->value, *j->pos); - } - - for (auto & i : *v->attrs) { - auto cache2 = - cache ? std::make_unique<JSONObject>(cache->object(i.name)) : nullptr; - doExpr(i.value, - attrPath == "" ? (std::string) i.name : attrPath + "." + (std::string) i.name, - toplevel2 || fromCache, cache2 ? cache2.get() : nullptr); - } + else if (attrPath[0] == "legacyPackages" && attrPath.size() > 2) { + auto attr = cursor.maybeGetAttr(state->sRecurseForDerivations); + if (attr && attr->getBool()) + recurse(); } - } catch (AssertionError & e) { - } catch (Error & e) { - if (!toplevel) { - e.addTrace(std::nullopt, "While evaluating the attribute '%s'", attrPath); + } catch (EvalError & e) { + if (!(attrPath.size() > 0 && attrPath[0] == "legacyPackages")) throw; - } } }; - Path jsonCacheFileName = getCacheDir() + "/nix/package-search.json"; - - if (useCache && pathExists(jsonCacheFileName)) { - - warn("using cached results; pass '-u' to update the cache"); - - Value vRoot; - parseJSON(*state, readFile(jsonCacheFileName), vRoot); - - fromCache = true; - - doExpr(&vRoot, "", true, nullptr); - } + for (auto & [cursor, prefix] : installable->getCursors(*state, true)) + visit(*cursor, parseAttrPath(*state, prefix)); - else { - createDirs(dirOf(jsonCacheFileName)); - - Path tmpFile = fmt("%s.tmp.%d", jsonCacheFileName, getpid()); - - std::ofstream jsonCacheFile; - - try { - // iostream considered harmful - jsonCacheFile.exceptions(std::ofstream::failbit); - jsonCacheFile.open(tmpFile); - - auto cache = writeCache ? std::make_unique<JSONObject>(jsonCacheFile, false) : nullptr; - - doExpr(getSourceExpr(*state), "", true, cache.get()); - - } catch (std::exception &) { - /* Fun fact: catching std::ios::failure does not work - due to C++11 ABI shenanigans. - https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66145 */ - if (!jsonCacheFile) - throw Error("error writing to %s", tmpFile); - throw; - } - - if (writeCache && rename(tmpFile.c_str(), jsonCacheFileName.c_str()) == -1) - throw SysError("cannot rename '%s' to '%s'", tmpFile, jsonCacheFileName); - } - - if (!json && results.size() == 0) + if (!json && !results) throw Error("no results for the given search term(s)!"); - - RunPager pager; - for (auto el : results) std::cout << el.second << "\n"; - } }; diff --git a/src/nix/show-derivation.cc b/src/nix/show-derivation.cc index 5d77cfdca..b5434f982 100644 --- a/src/nix/show-derivation.cc +++ b/src/nix/show-derivation.cc @@ -33,7 +33,7 @@ struct CmdShowDerivation : InstallablesCommand return { Example{ "To show the store derivation that results from evaluating the Hello package:", - "nix show-derivation nixpkgs.hello" + "nix show-derivation nixpkgs#hello" }, Example{ "To show the full derivation graph (if available) that produced your NixOS system:", diff --git a/src/nix/sigs.cc b/src/nix/sigs.cc index 6c9b9a792..7821a5432 100644 --- a/src/nix/sigs.cc +++ b/src/nix/sigs.cc @@ -105,7 +105,8 @@ struct CmdSignPaths : StorePathsCommand .shortName = 'k', .description = "file containing the secret signing key", .labels = {"file"}, - .handler = {&secretKeyFile} + .handler = {&secretKeyFile}, + .completer = completePath }); } diff --git a/src/nix/verify.cc b/src/nix/verify.cc index d3cc11e06..fc7a9765c 100644 --- a/src/nix/verify.cc +++ b/src/nix/verify.cc @@ -77,13 +77,16 @@ struct CmdVerify : StorePathsCommand try { checkInterrupt(); - Activity act2(*logger, lvlInfo, actUnknown, fmt("checking '%s'", storePath)); - MaintainCount<std::atomic<size_t>> mcActive(active); update(); auto info = store->queryPathInfo(store->parseStorePath(storePath)); + // Note: info->path can be different from storePath + // for binary cache stores when using --all (since we + // can't enumerate names efficiently). + Activity act2(*logger, lvlInfo, actUnknown, fmt("checking '%s'", store->printStorePath(info->path))); + if (!noContents) { std::unique_ptr<AbstractHashSink> hashSink; diff --git a/src/nix/why-depends.cc b/src/nix/why-depends.cc index 167c974ee..7e630b745 100644 --- a/src/nix/why-depends.cc +++ b/src/nix/why-depends.cc @@ -55,15 +55,15 @@ struct CmdWhyDepends : SourceExprCommand return { Example{ "To show one path through the dependency graph leading from Hello to Glibc:", - "nix why-depends nixpkgs.hello nixpkgs.glibc" + "nix why-depends nixpkgs#hello nixpkgs#glibc" }, Example{ "To show all files and paths in the dependency graph leading from Thunderbird to libX11:", - "nix why-depends --all nixpkgs.thunderbird nixpkgs.xorg.libX11" + "nix why-depends --all nixpkgs#thunderbird nixpkgs#xorg.libX11" }, Example{ "To show why Glibc depends on itself:", - "nix why-depends nixpkgs.glibc nixpkgs.glibc" + "nix why-depends nixpkgs#glibc nixpkgs#glibc" }, }; } @@ -72,17 +72,19 @@ struct CmdWhyDepends : SourceExprCommand void run(ref<Store> store) override { - auto package = parseInstallable(*this, store, _package, false); - auto packagePath = toStorePath(store, Build, package); - auto dependency = parseInstallable(*this, store, _dependency, false); - auto dependencyPath = toStorePath(store, NoBuild, dependency); + auto package = parseInstallable(store, _package); + auto packagePath = toStorePath(store, Realise::Outputs, operateOn, package); + auto dependency = parseInstallable(store, _dependency); + auto dependencyPath = toStorePath(store, Realise::Derivation, operateOn, dependency); auto dependencyPathHash = dependencyPath.hashPart(); StorePathSet closure; store->computeFSClosure({packagePath}, closure, false, false); if (!closure.count(dependencyPath)) { - printError("'%s' does not depend on '%s'", package->what(), dependency->what()); + printError("'%s' does not depend on '%s'", + store->printStorePath(packagePath), + store->printStorePath(dependencyPath)); return; } @@ -106,7 +108,11 @@ struct CmdWhyDepends : SourceExprCommand std::map<StorePath, Node> graph; for (auto & path : closure) - graph.emplace(path, Node { .path = path, .refs = store->queryPathInfo(path)->references }); + graph.emplace(path, Node { + .path = path, + .refs = store->queryPathInfo(path)->references, + .dist = path == dependencyPath ? 0 : inf + }); // Transpose the graph. for (auto & node : graph) @@ -115,8 +121,6 @@ struct CmdWhyDepends : SourceExprCommand /* Run Dijkstra's shortest path algorithm to get the distance of every path in the closure to 'dependency'. */ - graph.emplace(dependencyPath, Node { .path = dependencyPath, .dist = 0 }); - std::priority_queue<Node *> queue; queue.push(&graph.at(dependencyPath)); |