Revive 'nix search'

It uses the evaluation cache now rather than the ad hoc JSON cache.
This commit is contained in:
Eelco Dolstra 2020-04-20 15:27:09 +02:00
parent 42a12f9232
commit b69323f8c9
5 changed files with 117 additions and 203 deletions

View file

@ -695,7 +695,7 @@ struct CmdFlakeShow : FlakeCommand
void run(nix::ref<nix::Store> store) override void run(nix::ref<nix::Store> store) override
{ {
auto state = getEvalState(); auto state = getEvalState();
auto flake = lockFlake(); 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; std::function<void(eval_cache::AttrCursor & visitor, const std::vector<Symbol> & attrPath, const std::string & headerPrefix, const std::string & nextPrefix)> visit;
@ -815,7 +815,7 @@ struct CmdFlakeShow : FlakeCommand
auto cache = openEvalCache(*state, flake, useEvalCache); auto cache = openEvalCache(*state, flake, useEvalCache);
visit(*cache->getRoot(), {}, fmt(ANSI_BOLD "%s" ANSI_NORMAL, flake.flake.lockedRef), ""); visit(*cache->getRoot(), {}, fmt(ANSI_BOLD "%s" ANSI_NORMAL, flake->flake.lockedRef), "");
} }
}; };

View file

@ -133,6 +133,15 @@ App Installable::toApp(EvalState & state)
return App(state, *toValue(state).first); return App(state, *toValue(state).first);
} }
std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>>
Installable::getCursor(EvalState & state, bool useEvalCache)
{
auto evalCache =
std::make_shared<nix::eval_cache::EvalCache>(false, Hash(), state,
[&]() { return toValue(state).first; });
return {{evalCache->getRoot(), ""}};
}
struct InstallableStorePath : Installable struct InstallableStorePath : Installable
{ {
ref<Store> store; ref<Store> store;
@ -285,14 +294,14 @@ Value * InstallableFlake::getFlakeOutputs(EvalState & state, const flake::Locked
ref<eval_cache::EvalCache> openEvalCache( ref<eval_cache::EvalCache> openEvalCache(
EvalState & state, EvalState & state,
const flake::LockedFlake & lockedFlake, std::shared_ptr<flake::LockedFlake> lockedFlake,
bool useEvalCache) bool useEvalCache)
{ {
return ref(std::make_shared<nix::eval_cache::EvalCache>( return ref(std::make_shared<nix::eval_cache::EvalCache>(
useEvalCache, useEvalCache,
lockedFlake.getFingerprint(), lockedFlake->getFingerprint(),
state, state,
[&]() [&state, lockedFlake]()
{ {
/* For testing whether the evaluation cache is /* For testing whether the evaluation cache is
complete. */ complete. */
@ -300,7 +309,7 @@ ref<eval_cache::EvalCache> openEvalCache(
throw Error("not everything is cached, but evaluation is not allowed"); throw Error("not everything is cached, but evaluation is not allowed");
auto vFlake = state.allocValue(); auto vFlake = state.allocValue();
flake::callFlake(state, lockedFlake, *vFlake); flake::callFlake(state, *lockedFlake, *vFlake);
state.forceAttrs(*vFlake); state.forceAttrs(*vFlake);
@ -315,7 +324,8 @@ std::tuple<std::string, FlakeRef, InstallableValue::DerivationInfo> InstallableF
{ {
auto state = cmd.getEvalState(); auto state = cmd.getEvalState();
auto lockedFlake = lockFlake(*state, flakeRef, cmd.lockFlags); auto lockedFlake = std::make_shared<flake::LockedFlake>(
lockFlake(*state, flakeRef, cmd.lockFlags));
auto cache = openEvalCache(*state, lockedFlake, true); auto cache = openEvalCache(*state, lockedFlake, true);
auto root = cache->getRoot(); auto root = cache->getRoot();
@ -343,7 +353,7 @@ std::tuple<std::string, FlakeRef, InstallableValue::DerivationInfo> InstallableF
attr->getAttr(state->sOutputName)->getString() attr->getAttr(state->sOutputName)->getString()
}; };
return {attrPath, lockedFlake.flake.lockedRef, std::move(drvInfo)}; return {attrPath, lockedFlake->flake.lockedRef, std::move(drvInfo)};
} }
throw Error("flake '%s' does not provide attribute %s", throw Error("flake '%s' does not provide attribute %s",
@ -378,6 +388,25 @@ std::pair<Value *, Pos> InstallableFlake::toValue(EvalState & state)
flakeRef, concatStringsSep(", ", quoteStrings(attrPaths))); flakeRef, concatStringsSep(", ", quoteStrings(attrPaths)));
} }
std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>>
InstallableFlake::getCursor(EvalState & state, bool useEvalCache)
{
auto evalCache = openEvalCache(state,
std::make_shared<flake::LockedFlake>(lockFlake(state, flakeRef, cmd.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});
}
return res;
}
// FIXME: extend // FIXME: extend
std::string attrRegex = R"([A-Za-z_][A-Za-z0-9-_+]*)"; std::string attrRegex = R"([A-Za-z_][A-Za-z0-9-_+]*)";
static std::regex attrPathRegex(fmt(R"(%1%(\.%1%)*)", attrRegex)); static std::regex attrPathRegex(fmt(R"(%1%(\.%1%)*)", attrRegex));

View file

@ -12,7 +12,7 @@ namespace nix {
struct DrvInfo; struct DrvInfo;
struct SourceExprCommand; struct SourceExprCommand;
namespace eval_cache { class EvalCache; } namespace eval_cache { class EvalCache; class AttrCursor; }
struct Buildable struct Buildable
{ {
@ -57,6 +57,9 @@ struct Installable
{ {
return {}; return {};
} }
virtual std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>>
getCursor(EvalState & state, bool useEvalCache);
}; };
struct InstallableValue : Installable struct InstallableValue : Installable
@ -100,11 +103,14 @@ struct InstallableFlake : InstallableValue
std::vector<DerivationInfo> toDerivations() override; std::vector<DerivationInfo> toDerivations() override;
std::pair<Value *, Pos> toValue(EvalState & state) override; std::pair<Value *, Pos> toValue(EvalState & state) override;
std::vector<std::pair<std::shared_ptr<eval_cache::AttrCursor>, std::string>>
getCursor(EvalState & state, bool useEvalCache) override;
}; };
ref<eval_cache::EvalCache> openEvalCache( ref<eval_cache::EvalCache> openEvalCache(
EvalState & state, EvalState & state,
const flake::LockedFlake & lockedFlake, std::shared_ptr<flake::LockedFlake> lockedFlake,
bool useEvalCache); bool useEvalCache);
} }

View file

@ -6,8 +6,9 @@
#include "get-drvs.hh" #include "get-drvs.hh"
#include "common-args.hh" #include "common-args.hh"
#include "json.hh" #include "json.hh"
#include "json-to-value.hh"
#include "shared.hh" #include "shared.hh"
#include "eval-cache.hh"
#include "attr-path.hh"
#include <regex> #include <regex>
#include <fstream> #include <fstream>
@ -25,7 +26,7 @@ std::string hilite(const std::string & s, const std::smatch & m, std::string pos
m.empty() m.empty()
? s ? s
: std::string(m.prefix()) : std::string(m.prefix())
+ ANSI_RED + std::string(m.str()) + postfix + ANSI_GREEN + std::string(m.str()) + postfix
+ std::string(m.suffix()); + std::string(m.suffix());
} }
@ -33,23 +34,9 @@ struct CmdSearch : InstallableCommand, MixJSON
{ {
std::vector<std::string> res; std::vector<std::string> res;
bool writeCache = true;
bool useCache = true;
CmdSearch() CmdSearch()
{ {
expectArgs("regex", &res); expectArgs("regex", &res);
mkFlag()
.longName("update-cache")
.shortName('u')
.description("update the package search cache")
.handler([&]() { writeCache = true; useCache = false; });
mkFlag()
.longName("no-cache")
.description("do not use or update the package search cache")
.handler([&]() { writeCache = false; useCache = false; });
} }
std::string description() override std::string description() override
@ -61,27 +48,30 @@ struct CmdSearch : InstallableCommand, MixJSON
{ {
return { return {
Example{ Example{
"To show all available packages:", "To show all packages in the flake in the current directory:",
"nix search" "nix search"
}, },
Example{ Example{
"To show any packages containing 'blender' in its name or description:", "To show packages in the 'nixpkgs' flake containing 'blender' in its name or description:",
"nix search blender" "nix search nixpkgs blender"
}, },
Example{ Example{
"To search for Firefox or Chromium:", "To search for Firefox or Chromium:",
"nix search 'firefox|chromium'" "nix search nixpkgs 'firefox|chromium'"
}, },
Example{ Example{
"To search for git and frontend or gui:", "To search for packages containing 'git' and either 'frontend' or 'gui':",
"nix search git 'frontend|gui'" "nix search nixpkgs git 'frontend|gui'"
} }
}; };
} }
Strings getDefaultFlakeAttrPaths() override Strings getDefaultFlakeAttrPaths() override
{ {
return {""}; return {
"packages." + settings.thisSystem.get() + ".",
"legacyPackages." + settings.thisSystem.get() + "."
};
} }
void run(ref<Store> store) override void run(ref<Store> store) override
@ -91,9 +81,8 @@ struct CmdSearch : InstallableCommand, MixJSON
// Empty search string should match all packages // Empty search string should match all packages
// Use "^" here instead of ".*" due to differences in resulting highlighting // Use "^" here instead of ".*" due to differences in resulting highlighting
// (see #1893 -- libc++ claims empty search string is not in POSIX grammar) // (see #1893 -- libc++ claims empty search string is not in POSIX grammar)
if (res.empty()) { if (res.empty())
res.push_back("^"); res.push_back("^");
}
std::vector<std::regex> regexes; std::vector<std::regex> regexes;
regexes.reserve(res.size()); regexes.reserve(res.size());
@ -103,181 +92,91 @@ struct CmdSearch : InstallableCommand, MixJSON
auto state = getEvalState(); auto state = getEvalState();
//auto [value, pos] = installable->toValue(*state);
#if 0
auto jsonOut = json ? std::make_unique<JSONObject>(std::cout) : nullptr; auto jsonOut = json ? std::make_unique<JSONObject>(std::cout) : nullptr;
auto sToplevel = state->symbols.create("_toplevel"); uint64_t results = 0;
auto sRecurse = state->symbols.create("recurseForDerivations");
bool fromCache = false; std::function<void(eval_cache::AttrCursor & cursor, const std::vector<Symbol> & attrPath)> visit;
std::map<std::string, std::string> results; visit = [&](eval_cache::AttrCursor & cursor, const std::vector<Symbol> & attrPath)
std::function<void(Value *, std::string, bool, JSONObject *)> doExpr;
doExpr = [&](Value * v, std::string attrPath, bool toplevel, JSONObject * cache) {
debug("at attribute '%s'", attrPath);
try {
uint found = 0;
state->forceValue(*v);
if (v->type == tLambda && toplevel) {
Value * v2 = state->allocValue();
state->autoCallFunction(*state->allocBindings(1), *v, *v2);
v = v2;
state->forceValue(*v);
}
if (state->isDerivation(*v)) {
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 &regex : 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', ' ');
std::regex_search(description, descriptionMatch, regex);
if (!attrPathMatch.empty()
|| !nameMatch.empty()
|| !descriptionMatch.empty())
{ {
found++; Activity act(*logger, lvlInfo, actUnknown,
} fmt("evaluating '%s'", concatStringsSep(".", attrPath)));
} try {
auto recurse = [&]()
if (found == res.size()) { {
if (json) { for (const auto & attr : cursor.getAttrs()) {
auto cursor2 = cursor.getAttr(attr);
auto jsonElem = jsonOut->object(attrPath); auto attrPath2(attrPath);
attrPath2.push_back(attr);
jsonElem.attr("pkgName", parsed.name); visit(*cursor2, attrPath2);
jsonElem.attr("version", parsed.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);
}
}
}
else if (v->type == tAttrs) {
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);
}
}
} catch (AssertionError & e) {
} catch (Error & e) {
if (!toplevel) {
e.addPrefix(fmt("While evaluating the attribute '%s':\n", attrPath));
throw;
}
} }
}; };
Path jsonCacheFileName = getCacheDir() + "/nix/package-search.json"; if (cursor.isDerivation()) {
size_t found = 0;
if (useCache && pathExists(jsonCacheFileName)) { DrvName name(cursor.getAttr("name")->getString());
warn("using cached results; pass '-u' to update the cache"); 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);
Value vRoot; std::smatch attrPathMatch;
parseJSON(*state, readFile(jsonCacheFileName), vRoot); std::smatch descriptionMatch;
std::smatch nameMatch;
fromCache = true; for (auto & regex : regexes) {
std::regex_search(attrPath2, attrPathMatch, regex);
doExpr(&vRoot, "", true, nullptr); std::regex_search(name.name, nameMatch, regex);
std::regex_search(description, descriptionMatch, regex);
if (!attrPathMatch.empty()
|| !nameMatch.empty()
|| !descriptionMatch.empty())
found++;
} }
else { if (found == res.size()) {
createDirs(dirOf(jsonCacheFileName)); results++;
if (json) {
auto jsonElem = jsonOut->object(attrPath2);
jsonElem.attr("pkgName", name.name);
jsonElem.attr("version", name.version);
jsonElem.attr("description", description);
} else {
auto name2 = hilite(name.name, nameMatch, "\e[0;2m")
+ std::string(name.fullName, name.name.length());
if (results > 1) logger->stdout("");
logger->stdout(
"* %s (%s)",
wrap("\e[0;1m", hilite(attrPath2, attrPathMatch, "\e[0;1m")),
wrap("\e[0;2m", hilite(name2, nameMatch, "\e[0;2m")));
if (description != "")
logger->stdout(
" %s", hilite(description, descriptionMatch, ANSI_NORMAL));
}
}
}
Path tmpFile = fmt("%s.tmp.%d", jsonCacheFileName, getpid()); else if (
attrPath.size() == 0
|| (attrPath[0] == "legacyPackages" && attrPath.size() <= 2)
|| (attrPath[0] == "packages" && attrPath.size() <= 2))
recurse();
std::ofstream jsonCacheFile; } catch (EvalError & e) {
if (!(attrPath.size() > 0 && attrPath[0] == "legacyPackages"))
try {
// iostream considered harmful
jsonCacheFile.exceptions(std::ofstream::failbit);
jsonCacheFile.open(tmpFile);
auto cache = writeCache ? std::make_unique<JSONObject>(jsonCacheFile, false) : nullptr;
// FIXME
throw Error("NOT IMPLEMENTED");
//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; throw;
} }
};
if (writeCache && rename(tmpFile.c_str(), jsonCacheFileName.c_str()) == -1) for (auto & [cursor, prefix] : installable->getCursor(*state, true))
throw SysError("cannot rename '%s' to '%s'", tmpFile, jsonCacheFileName); visit(*cursor, parseAttrPath(*state, prefix));
}
if (results.size() == 0) if (!results)
throw Error("no results for the given search term(s)!"); throw Error("no results for the given search term(s)!");
RunPager pager;
for (auto el : results) std::cout << el.second << "\n";
#endif
} }
}; };

View file

@ -3,43 +3,23 @@ source common.sh
clearStore clearStore
clearCache clearCache
exit 0 # FIXME (( $(nix search -f search.nix '' hello | wc -l) > 0 ))
# No packages
(( $(NIX_PATH= nix search -u|wc -l) == 0 ))
# Haven't updated cache, still nothing
(( $(nix search -f search.nix hello|wc -l) == 0 ))
(( $(nix search -f search.nix |wc -l) == 0 ))
# Update cache, search should work
(( $(nix search -f search.nix -u hello|wc -l) > 0 ))
# Use cache
(( $(nix search -f search.nix foo|wc -l) > 0 ))
(( $(nix search foo|wc -l) > 0 ))
# Test --no-cache works
# No results from cache
(( $(nix search --no-cache foo |wc -l) == 0 ))
# Does find results from file pointed at
(( $(nix search -f search.nix --no-cache foo |wc -l) > 0 ))
# Check descriptions are searched # Check descriptions are searched
(( $(nix search broken | wc -l) > 0 )) (( $(nix search -f search.nix '' broken | wc -l) > 0 ))
# Check search that matches nothing # Check search that matches nothing
(( $(nix search nosuchpackageexists | wc -l) == 0 )) (( $(nix search -f search.nix '' nosuchpackageexists | wc -l) == 0 ))
# Search for multiple arguments # Search for multiple arguments
(( $(nix search hello empty | wc -l) == 3 )) (( $(nix search -f search.nix '' hello empty | wc -l) == 2 ))
# Multiple arguments will not exist # Multiple arguments will not exist
(( $(nix search hello broken | wc -l) == 0 )) (( $(nix search -f search.nix '' hello broken | wc -l) == 0 ))
## Search expressions ## Search expressions
# Check that empty search string matches all # Check that empty search string matches all
nix search|grep -q foo nix search -f search.nix '' |grep -q foo
nix search|grep -q bar nix search -f search.nix '' |grep -q bar
nix search|grep -q hello nix search -f search.nix '' |grep -q hello