Merge remote-tracking branch 'upstream/master' into list-experimental-features

This commit is contained in:
John Ericson 2023-04-04 21:34:20 -04:00
commit 3f98353f19
29 changed files with 241 additions and 84 deletions

View File

@ -23,6 +23,7 @@ Maintainers: tick if completed or explain if not relevant
- unit tests - `src/*/tests` - unit tests - `src/*/tests`
- integration tests - `tests/nixos/*` - integration tests - `tests/nixos/*`
- [ ] documentation in the manual - [ ] documentation in the manual
- [ ] documentation in the internal API docs
- [ ] code and comments are self-explanatory - [ ] code and comments are self-explanatory
- [ ] commit message explains why the change was made - [ ] commit message explains why the change was made
- [ ] new feature or incompatible change: updated release notes - [ ] new feature or incompatible change: updated release notes

View File

@ -1,6 +1,8 @@
clean-files += Makefile.config clean-files += Makefile.config
GLOBAL_CXXFLAGS += -Wno-deprecated-declarations GLOBAL_CXXFLAGS += -Wno-deprecated-declarations -Werror=switch
# Allow switch-enum to be overridden for files that do not support it, usually because of dependency headers.
ERROR_SWITCH_ENUM = -Werror=switch-enum
$(foreach i, config.h $(wildcard src/lib*/*.hh), \ $(foreach i, config.h $(wildcard src/lib*/*.hh), \
$(eval $(call install-file-in, $(i), $(includedir)/nix, 0644))) $(eval $(call install-file-in, $(i), $(includedir)/nix, 0644)))

View File

@ -69,6 +69,7 @@ Issues on the board progress through the following states:
2. [security](https://github.com/NixOS/nix/labels/security) 2. [security](https://github.com/NixOS/nix/labels/security)
3. [regression](https://github.com/NixOS/nix/labels/regression) 3. [regression](https://github.com/NixOS/nix/labels/regression)
4. [bug](https://github.com/NixOS/nix/issues?q=is%3Aopen+label%3Abug+sort%3Areactions-%2B1-desc) 4. [bug](https://github.com/NixOS/nix/issues?q=is%3Aopen+label%3Abug+sort%3Areactions-%2B1-desc)
5. [tests of existing functionality](https://github.com/NixOS/nix/issues?q=is%3Aopen+label%3Atests+-label%3Afeature+sort%3Areactions-%2B1-desc)
- [oldest pull requests](https://github.com/NixOS/nix/pulls?q=is%3Apr+is%3Aopen+sort%3Acreated-asc) - [oldest pull requests](https://github.com/NixOS/nix/pulls?q=is%3Apr+is%3Aopen+sort%3Acreated-asc)
- [most popular pull requests](https://github.com/NixOS/nix/pulls?q=is%3Apr+is%3Aopen+sort%3Areactions-%2B1-desc) - [most popular pull requests](https://github.com/NixOS/nix/pulls?q=is%3Apr+is%3Aopen+sort%3Areactions-%2B1-desc)
@ -91,7 +92,7 @@ Issues on the board progress through the following states:
Contributors who took the time to implement concrete change proposals should not wait indefinitely. Contributors who took the time to implement concrete change proposals should not wait indefinitely.
- Prioritise fixing bugs over documentation, improvements or new features - Prioritise fixing bugs and testing over documentation, improvements or new features
The team values stability and accessibility higher than raw functionality. The team values stability and accessibility higher than raw functionality.

View File

@ -1,10 +1,10 @@
$(buildprefix)%.o: %.cc $(buildprefix)%.o: %.cc
@mkdir -p "$(dir $@)" @mkdir -p "$(dir $@)"
$(trace-cxx) $(CXX) -o $@ -c $< $(CPPFLAGS) $(GLOBAL_CXXFLAGS_PCH) $(GLOBAL_CXXFLAGS) $(CXXFLAGS) $($@_CXXFLAGS) -MMD -MF $(call filename-to-dep, $@) -MP $(trace-cxx) $(CXX) -o $@ -c $< $(CPPFLAGS) $(GLOBAL_CXXFLAGS_PCH) $(GLOBAL_CXXFLAGS) $(CXXFLAGS) $($@_CXXFLAGS) $(ERROR_SWITCH_ENUM) -MMD -MF $(call filename-to-dep, $@) -MP
$(buildprefix)%.o: %.cpp $(buildprefix)%.o: %.cpp
@mkdir -p "$(dir $@)" @mkdir -p "$(dir $@)"
$(trace-cxx) $(CXX) -o $@ -c $< $(CPPFLAGS) $(GLOBAL_CXXFLAGS_PCH) $(GLOBAL_CXXFLAGS) $(CXXFLAGS) $($@_CXXFLAGS) -MMD -MF $(call filename-to-dep, $@) -MP $(trace-cxx) $(CXX) -o $@ -c $< $(CPPFLAGS) $(GLOBAL_CXXFLAGS_PCH) $(GLOBAL_CXXFLAGS) $(CXXFLAGS) $($@_CXXFLAGS) $(ERROR_SWITCH_ENUM) -MMD -MF $(call filename-to-dep, $@) -MP
$(buildprefix)%.o: %.c $(buildprefix)%.o: %.c
@mkdir -p "$(dir $@)" @mkdir -p "$(dir $@)"

View File

@ -102,6 +102,28 @@ MixFlakeOptions::MixFlakeOptions()
}} }}
}); });
addFlag({
.longName = "reference-lock-file",
.description = "Read the given lock file instead of `flake.lock` within the top-level flake.",
.category = category,
.labels = {"flake-lock-path"},
.handler = {[&](std::string lockFilePath) {
lockFlags.referenceLockFilePath = lockFilePath;
}},
.completer = completePath
});
addFlag({
.longName = "output-lock-file",
.description = "Write the given lock file instead of `flake.lock` within the top-level flake.",
.category = category,
.labels = {"flake-lock-path"},
.handler = {[&](std::string lockFilePath) {
lockFlags.outputLockFilePath = lockFilePath;
}},
.completer = completePath
});
addFlag({ addFlag({
.longName = "inputs-from", .longName = "inputs-from",
.description = "Use the inputs of the specified flake as registry entries.", .description = "Use the inputs of the specified flake as registry entries.",

View File

@ -252,7 +252,9 @@ void NixRepl::mainLoop()
el_hist_size = 1000; el_hist_size = 1000;
#endif #endif
read_history(historyFile.c_str()); read_history(historyFile.c_str());
auto oldRepl = curRepl;
curRepl = this; curRepl = this;
Finally restoreRepl([&] { curRepl = oldRepl; });
#ifndef READLINE #ifndef READLINE
rl_set_complete_func(completionCallback); rl_set_complete_func(completionCallback);
rl_set_list_possib_func(listPossibleCallback); rl_set_list_possib_func(listPossibleCallback);
@ -1024,6 +1026,8 @@ std::ostream & NixRepl::printValue(std::ostream & str, Value & v, unsigned int m
str << v.fpoint; str << v.fpoint;
break; break;
case nThunk:
case nExternal:
default: default:
str << ANSI_RED "«unknown»" ANSI_NORMAL; str << ANSI_RED "«unknown»" ANSI_NORMAL;
break; break;

View File

@ -173,7 +173,17 @@ void Value::print(const SymbolTable & symbols, std::ostream & str,
case tFloat: case tFloat:
str << fpoint; str << fpoint;
break; break;
case tBlackhole:
// Although we know for sure that it's going to be an infinite recursion
// when this value is accessed _in the current context_, it's likely
// that the user will misinterpret a simpler «infinite recursion» output
// as a definitive statement about the value, while in fact it may be
// a valid value after `builtins.trace` and perhaps some other steps
// have completed.
str << "«potential infinite recursion»";
break;
default: default:
printError("Nix evaluator internal error: Value::print(): invalid value type %1%", internalType);
abort(); abort();
} }
} }
@ -229,6 +239,9 @@ std::string_view showType(ValueType type)
std::string showType(const Value & v) std::string showType(const Value & v)
{ {
// Allow selecting a subset of enum values
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum"
switch (v.internalType) { switch (v.internalType) {
case tString: return v.string.context ? "a string with context" : "a string"; case tString: return v.string.context ? "a string with context" : "a string";
case tPrimOp: case tPrimOp:
@ -242,16 +255,21 @@ std::string showType(const Value & v)
default: default:
return std::string(showType(v.type())); return std::string(showType(v.type()));
} }
#pragma GCC diagnostic pop
} }
PosIdx Value::determinePos(const PosIdx pos) const PosIdx Value::determinePos(const PosIdx pos) const
{ {
// Allow selecting a subset of enum values
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum"
switch (internalType) { switch (internalType) {
case tAttrs: return attrs->pos; case tAttrs: return attrs->pos;
case tLambda: return lambda.fun->pos; case tLambda: return lambda.fun->pos;
case tApp: return app.left->determinePos(pos); case tApp: return app.left->determinePos(pos);
default: return pos; default: return pos;
} }
#pragma GCC diagnostic pop
} }
bool Value::isTrivial() const bool Value::isTrivial() const
@ -2327,6 +2345,7 @@ bool EvalState::eqValues(Value & v1, Value & v2, const PosIdx pos, std::string_v
case nFloat: case nFloat:
return v1.fpoint == v2.fpoint; return v1.fpoint == v2.fpoint;
case nThunk: // Must not be left by forceValue
default: default:
error("cannot compare %1% with %2%", showType(v1), showType(v2)).withTrace(pos, errorCtx).debugThrow<EvalError>(); error("cannot compare %1% with %2%", showType(v1), showType(v2)).withTrace(pos, errorCtx).debugThrow<EvalError>();
} }

View File

@ -125,6 +125,9 @@ static FlakeInput parseFlakeInput(EvalState & state,
follows.insert(follows.begin(), lockRootPath.begin(), lockRootPath.end()); follows.insert(follows.begin(), lockRootPath.begin(), lockRootPath.end());
input.follows = follows; input.follows = follows;
} else { } else {
// Allow selecting a subset of enum values
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum"
switch (attr.value->type()) { switch (attr.value->type()) {
case nString: case nString:
attrs.emplace(state.symbols[attr.name], attr.value->string.s); attrs.emplace(state.symbols[attr.name], attr.value->string.s);
@ -139,6 +142,7 @@ static FlakeInput parseFlakeInput(EvalState & state,
throw TypeError("flake input attribute '%s' is %s while a string, Boolean, or integer is expected", throw TypeError("flake input attribute '%s' is %s while a string, Boolean, or integer is expected",
state.symbols[attr.name], showType(*attr.value)); state.symbols[attr.name], showType(*attr.value));
} }
#pragma GCC diagnostic pop
} }
} catch (Error & e) { } catch (Error & e) {
e.addTrace( e.addTrace(
@ -334,10 +338,14 @@ LockedFlake lockFlake(
} }
try { try {
if (!fetchSettings.allowDirty && lockFlags.referenceLockFilePath) {
throw Error("reference lock file was provided, but the `allow-dirty` setting is set to false");
}
// FIXME: symlink attack // FIXME: symlink attack
auto oldLockFile = LockFile::read( auto oldLockFile = LockFile::read(
flake.sourceInfo->actualPath + "/" + flake.lockedRef.subdir + "/flake.lock"); lockFlags.referenceLockFilePath.value_or(
flake.sourceInfo->actualPath + "/" + flake.lockedRef.subdir + "/flake.lock"));
debug("old lock file: %s", oldLockFile); debug("old lock file: %s", oldLockFile);
@ -619,13 +627,20 @@ LockedFlake lockFlake(
debug("new lock file: %s", newLockFile); debug("new lock file: %s", newLockFile);
auto relPath = (topRef.subdir == "" ? "" : topRef.subdir + "/") + "flake.lock";
auto sourcePath = topRef.input.getSourcePath();
auto outputLockFilePath = sourcePath ? std::optional{*sourcePath + "/" + relPath} : std::nullopt;
if (lockFlags.outputLockFilePath) {
outputLockFilePath = lockFlags.outputLockFilePath;
}
/* Check whether we need to / can write the new lock file. */ /* Check whether we need to / can write the new lock file. */
if (!(newLockFile == oldLockFile)) { if (newLockFile != oldLockFile || lockFlags.outputLockFilePath) {
auto diff = LockFile::diff(oldLockFile, newLockFile); auto diff = LockFile::diff(oldLockFile, newLockFile);
if (lockFlags.writeLockFile) { if (lockFlags.writeLockFile) {
if (auto sourcePath = topRef.input.getSourcePath()) { if (outputLockFilePath) {
if (auto unlockedInput = newLockFile.isUnlocked()) { if (auto unlockedInput = newLockFile.isUnlocked()) {
if (fetchSettings.warnDirty) if (fetchSettings.warnDirty)
warn("will not write lock file of flake '%s' because it has an unlocked input ('%s')", topRef, *unlockedInput); warn("will not write lock file of flake '%s' because it has an unlocked input ('%s')", topRef, *unlockedInput);
@ -633,25 +648,24 @@ LockedFlake lockFlake(
if (!lockFlags.updateLockFile) if (!lockFlags.updateLockFile)
throw Error("flake '%s' requires lock file changes but they're not allowed due to '--no-update-lock-file'", topRef); throw Error("flake '%s' requires lock file changes but they're not allowed due to '--no-update-lock-file'", topRef);
auto relPath = (topRef.subdir == "" ? "" : topRef.subdir + "/") + "flake.lock"; bool lockFileExists = pathExists(*outputLockFilePath);
auto path = *sourcePath + "/" + relPath;
bool lockFileExists = pathExists(path);
if (lockFileExists) { if (lockFileExists) {
auto s = chomp(diff); auto s = chomp(diff);
if (s.empty()) if (s.empty())
warn("updating lock file '%s'", path); warn("updating lock file '%s'", *outputLockFilePath);
else else
warn("updating lock file '%s':\n%s", path, s); warn("updating lock file '%s':\n%s", *outputLockFilePath, s);
} else } else
warn("creating lock file '%s'", path); warn("creating lock file '%s'", *outputLockFilePath);
newLockFile.write(path); newLockFile.write(*outputLockFilePath);
std::optional<std::string> commitMessage = std::nullopt; std::optional<std::string> commitMessage = std::nullopt;
if (lockFlags.commitLockFile) { if (lockFlags.commitLockFile) {
if (lockFlags.outputLockFilePath) {
throw Error("--commit-lock-file and --output-lock-file are currently incompatible");
}
std::string cm; std::string cm;
cm = fetchSettings.commitLockFileSummary.get(); cm = fetchSettings.commitLockFileSummary.get();

View File

@ -118,6 +118,12 @@ struct LockFlags
/* Whether to commit changes to flake.lock. */ /* Whether to commit changes to flake.lock. */
bool commitLockFile = false; bool commitLockFile = false;
/* The path to a lock file to read instead of the `flake.lock` file in the top-level flake */
std::optional<std::string> referenceLockFilePath;
/* The path to a lock file to write to instead of the `flake.lock` file in the top-level flake */
std::optional<Path> outputLockFilePath;
/* Flake inputs to be overridden. */ /* Flake inputs to be overridden. */
std::map<InputPath, FlakeRef> inputOverrides; std::map<InputPath, FlakeRef> inputOverrides;

View File

@ -46,3 +46,5 @@ $(foreach i, $(wildcard src/libexpr/flake/*.hh), \
$(d)/primops.cc: $(d)/imported-drv-to-derivation.nix.gen.hh $(d)/primops/derivation.nix.gen.hh $(d)/fetchurl.nix.gen.hh $(d)/primops.cc: $(d)/imported-drv-to-derivation.nix.gen.hh $(d)/primops/derivation.nix.gen.hh $(d)/fetchurl.nix.gen.hh
$(d)/flake/flake.cc: $(d)/flake/call-flake.nix.gen.hh $(d)/flake/flake.cc: $(d)/flake/call-flake.nix.gen.hh
src/libexpr/primops/fromTOML.o: ERROR_SWITCH_ENUM =

View File

@ -577,6 +577,9 @@ struct CompareValues
return v1->integer < v2->fpoint; return v1->integer < v2->fpoint;
if (v1->type() != v2->type()) if (v1->type() != v2->type())
state.error("cannot compare %s with %s", showType(*v1), showType(*v2)).debugThrow<EvalError>(); state.error("cannot compare %s with %s", showType(*v1), showType(*v2)).debugThrow<EvalError>();
// Allow selecting a subset of enum values
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum"
switch (v1->type()) { switch (v1->type()) {
case nInt: case nInt:
return v1->integer < v2->integer; return v1->integer < v2->integer;
@ -599,6 +602,7 @@ struct CompareValues
} }
default: default:
state.error("cannot compare %s with %s; values of that type are incomparable", showType(*v1), showType(*v2)).debugThrow<EvalError>(); state.error("cannot compare %s with %s; values of that type are incomparable", showType(*v1), showType(*v2)).debugThrow<EvalError>();
#pragma GCC diagnostic pop
} }
} catch (Error & e) { } catch (Error & e) {
if (!errorCtx.empty()) if (!errorCtx.empty())

View File

@ -53,6 +53,7 @@ struct BuildResult
case LogLimitExceeded: return "LogLimitExceeded"; case LogLimitExceeded: return "LogLimitExceeded";
case NotDeterministic: return "NotDeterministic"; case NotDeterministic: return "NotDeterministic";
case ResolvesToAlreadyValid: return "ResolvesToAlreadyValid"; case ResolvesToAlreadyValid: return "ResolvesToAlreadyValid";
case NoSubstituters: return "NoSubstituters";
default: return "Unknown"; default: return "Unknown";
}; };
}(); }();

View File

@ -407,6 +407,10 @@ struct curlFileTransfer : public FileTransfer
err = Misc; err = Misc;
} else { } else {
// Don't bother retrying on certain cURL errors either // Don't bother retrying on certain cURL errors either
// Allow selecting a subset of enum values
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-enum"
switch (code) { switch (code) {
case CURLE_FAILED_INIT: case CURLE_FAILED_INIT:
case CURLE_URL_MALFORMAT: case CURLE_URL_MALFORMAT:
@ -427,6 +431,7 @@ struct curlFileTransfer : public FileTransfer
default: // Shut up warnings default: // Shut up warnings
break; break;
} }
#pragma GCC diagnostic pop
} }
attempt++; attempt++;

View File

@ -1134,54 +1134,6 @@ StorePathSet LocalStore::querySubstitutablePaths(const StorePathSet & paths)
} }
// FIXME: move this, it's not specific to LocalStore.
void LocalStore::querySubstitutablePathInfos(const StorePathCAMap & paths, SubstitutablePathInfos & infos)
{
if (!settings.useSubstitutes) return;
for (auto & sub : getDefaultSubstituters()) {
for (auto & path : paths) {
if (infos.count(path.first))
// Choose first succeeding substituter.
continue;
auto subPath(path.first);
// Recompute store path so that we can use a different store root.
if (path.second) {
subPath = makeFixedOutputPathFromCA(path.first.name(), *path.second);
if (sub->storeDir == storeDir)
assert(subPath == path.first);
if (subPath != path.first)
debug("replaced path '%s' with '%s' for substituter '%s'", printStorePath(path.first), sub->printStorePath(subPath), sub->getUri());
} else if (sub->storeDir != storeDir) continue;
debug("checking substituter '%s' for path '%s'", sub->getUri(), sub->printStorePath(subPath));
try {
auto info = sub->queryPathInfo(subPath);
if (sub->storeDir != storeDir && !(info->isContentAddressed(*sub) && info->references.empty()))
continue;
auto narInfo = std::dynamic_pointer_cast<const NarInfo>(
std::shared_ptr<const ValidPathInfo>(info));
infos.insert_or_assign(path.first, SubstitutablePathInfo{
info->deriver,
info->references,
narInfo ? narInfo->fileSize : 0,
info->narSize});
} catch (InvalidPath &) {
} catch (SubstituterDisabled &) {
} catch (Error & e) {
if (settings.tryFallback)
logError(e.info());
else
throw;
}
}
}
}
void LocalStore::registerValidPath(const ValidPathInfo & info) void LocalStore::registerValidPath(const ValidPathInfo & info)
{ {
registerValidPaths({{info.path, info}}); registerValidPaths({{info.path, info}});

View File

@ -134,9 +134,6 @@ public:
StorePathSet querySubstitutablePaths(const StorePathSet & paths) override; StorePathSet querySubstitutablePaths(const StorePathSet & paths) override;
void querySubstitutablePathInfos(const StorePathCAMap & paths,
SubstitutablePathInfos & infos) override;
bool pathInfoIsUntrusted(const ValidPathInfo &) override; bool pathInfoIsUntrusted(const ValidPathInfo &) override;
bool realisationIsUntrusted(const Realisation & ) override; bool realisationIsUntrusted(const Realisation & ) override;

View File

@ -275,6 +275,7 @@ json listNar(ref<FSAccessor> accessor, const Path & path, bool recurse)
obj["type"] = "symlink"; obj["type"] = "symlink";
obj["target"] = accessor->readLink(path); obj["target"] = accessor->readLink(path);
break; break;
case FSAccessor::Type::tMissing:
default: default:
throw Error("path '%s' does not exist in NAR", path); throw Error("path '%s' does not exist in NAR", path);
} }

View File

@ -507,6 +507,54 @@ StorePathSet Store::queryDerivationOutputs(const StorePath & path)
return outputPaths; return outputPaths;
} }
void Store::querySubstitutablePathInfos(const StorePathCAMap & paths, SubstitutablePathInfos & infos)
{
if (!settings.useSubstitutes) return;
for (auto & sub : getDefaultSubstituters()) {
for (auto & path : paths) {
if (infos.count(path.first))
// Choose first succeeding substituter.
continue;
auto subPath(path.first);
// Recompute store path so that we can use a different store root.
if (path.second) {
subPath = makeFixedOutputPathFromCA(path.first.name(), *path.second);
if (sub->storeDir == storeDir)
assert(subPath == path.first);
if (subPath != path.first)
debug("replaced path '%s' with '%s' for substituter '%s'", printStorePath(path.first), sub->printStorePath(subPath), sub->getUri());
} else if (sub->storeDir != storeDir) continue;
debug("checking substituter '%s' for path '%s'", sub->getUri(), sub->printStorePath(subPath));
try {
auto info = sub->queryPathInfo(subPath);
if (sub->storeDir != storeDir && !(info->isContentAddressed(*sub) && info->references.empty()))
continue;
auto narInfo = std::dynamic_pointer_cast<const NarInfo>(
std::shared_ptr<const ValidPathInfo>(info));
infos.insert_or_assign(path.first, SubstitutablePathInfo{
info->deriver,
info->references,
narInfo ? narInfo->fileSize : 0,
info->narSize});
} catch (InvalidPath &) {
} catch (SubstituterDisabled &) {
} catch (Error & e) {
if (settings.tryFallback)
logError(e.info());
else
throw;
}
}
}
}
bool Store::isValidPath(const StorePath & storePath) bool Store::isValidPath(const StorePath & storePath)
{ {
{ {

View File

@ -456,7 +456,7 @@ public:
* resulting infos map. * resulting infos map.
*/ */
virtual void querySubstitutablePathInfos(const StorePathCAMap & paths, virtual void querySubstitutablePathInfos(const StorePathCAMap & paths,
SubstitutablePathInfos & infos) { return; }; SubstitutablePathInfos & infos);
/** /**
* Import a path into the store. * Import a path into the store.

View File

@ -236,8 +236,6 @@ nlohmann::json Args::toJSON()
auto flags = nlohmann::json::object(); auto flags = nlohmann::json::object();
for (auto & [name, flag] : longFlags) { for (auto & [name, flag] : longFlags) {
/* Skip experimental flags when listing flags. */
if (!experimentalFeatureSettings.isEnabled(flag->experimentalFeature)) continue;
auto j = nlohmann::json::object(); auto j = nlohmann::json::object();
if (flag->aliases.count(name)) continue; if (flag->aliases.count(name)) continue;
if (flag->shortName) if (flag->shortName)
@ -249,6 +247,11 @@ nlohmann::json Args::toJSON()
j["arity"] = flag->handler.arity; j["arity"] = flag->handler.arity;
if (!flag->labels.empty()) if (!flag->labels.empty())
j["labels"] = flag->labels; j["labels"] = flag->labels;
// TODO With C++23 use `std::optional::tranform`
if (auto & xp = flag->experimentalFeature)
j["experimental-feature"] = showExperimentalFeature(*xp);
else
j["experimental-feature"] = nullptr;
flags[name] = std::move(j); flags[name] = std::move(j);
} }
@ -345,6 +348,11 @@ Strings argvToStrings(int argc, char * * argv)
return args; return args;
} }
std::optional<ExperimentalFeature> Command::experimentalFeature ()
{
return { Xp::NixCommand };
}
MultiCommand::MultiCommand(const Commands & commands_) MultiCommand::MultiCommand(const Commands & commands_)
: commands(commands_) : commands(commands_)
{ {
@ -408,6 +416,11 @@ nlohmann::json MultiCommand::toJSON()
cat["id"] = command->category(); cat["id"] = command->category();
cat["description"] = trim(categories[command->category()]); cat["description"] = trim(categories[command->category()]);
j["category"] = std::move(cat); j["category"] = std::move(cat);
// TODO With C++23 use `std::optional::tranform`
if (auto xp = command->experimentalFeature())
cat["experimental-feature"] = showExperimentalFeature(*xp);
else
cat["experimental-feature"] = nullptr;
cmds[name] = std::move(j); cmds[name] = std::move(j);
} }

View File

@ -236,6 +236,8 @@ struct Command : virtual public Args
static constexpr Category catDefault = 0; static constexpr Category catDefault = 0;
virtual std::optional<ExperimentalFeature> experimentalFeature ();
virtual Category category() { return catDefault; } virtual Category category() { return catDefault; }
}; };

View File

@ -65,9 +65,10 @@ public:
switch (lvl) { switch (lvl) {
case lvlError: c = '3'; break; case lvlError: c = '3'; break;
case lvlWarn: c = '4'; break; case lvlWarn: c = '4'; break;
case lvlInfo: c = '5'; break; case lvlNotice: case lvlInfo: c = '5'; break;
case lvlTalkative: case lvlChatty: c = '6'; break; case lvlTalkative: case lvlChatty: c = '6'; break;
default: c = '7'; case lvlDebug: case lvlVomit: c = '7';
default: c = '7'; break; // should not happen, and missing enum case is reported by -Werror=switch-enum
} }
prefix = std::string("<") + c + ">"; prefix = std::string("<") + c + ">";
} }

View File

@ -277,17 +277,17 @@ static void printTree(const StorePath & path,
static void opQuery(Strings opFlags, Strings opArgs) static void opQuery(Strings opFlags, Strings opArgs)
{ {
enum QueryType enum QueryType
{ qDefault, qOutputs, qRequisites, qReferences, qReferrers { qOutputs, qRequisites, qReferences, qReferrers
, qReferrersClosure, qDeriver, qBinding, qHash, qSize , qReferrersClosure, qDeriver, qBinding, qHash, qSize
, qTree, qGraph, qGraphML, qResolve, qRoots }; , qTree, qGraph, qGraphML, qResolve, qRoots };
QueryType query = qDefault; std::optional<QueryType> query;
bool useOutput = false; bool useOutput = false;
bool includeOutputs = false; bool includeOutputs = false;
bool forceRealise = false; bool forceRealise = false;
std::string bindingName; std::string bindingName;
for (auto & i : opFlags) { for (auto & i : opFlags) {
QueryType prev = query; std::optional<QueryType> prev = query;
if (i == "--outputs") query = qOutputs; if (i == "--outputs") query = qOutputs;
else if (i == "--requisites" || i == "-R") query = qRequisites; else if (i == "--requisites" || i == "-R") query = qRequisites;
else if (i == "--references") query = qReferences; else if (i == "--references") query = qReferences;
@ -312,15 +312,15 @@ static void opQuery(Strings opFlags, Strings opArgs)
else if (i == "--force-realise" || i == "--force-realize" || i == "-f") forceRealise = true; else if (i == "--force-realise" || i == "--force-realize" || i == "-f") forceRealise = true;
else if (i == "--include-outputs") includeOutputs = true; else if (i == "--include-outputs") includeOutputs = true;
else throw UsageError("unknown flag '%1%'", i); else throw UsageError("unknown flag '%1%'", i);
if (prev != qDefault && prev != query) if (prev && prev != query)
throw UsageError("query type '%1%' conflicts with earlier flag", i); throw UsageError("query type '%1%' conflicts with earlier flag", i);
} }
if (query == qDefault) query = qOutputs; if (!query) query = qOutputs;
RunPager pager; RunPager pager;
switch (query) { switch (*query) {
case qOutputs: { case qOutputs: {
for (auto & i : opArgs) { for (auto & i : opArgs) {

View File

@ -39,6 +39,14 @@ struct CmdDoctor : StoreCommand
{ {
bool success = true; bool success = true;
/**
* This command is stable before the others
*/
std::optional<ExperimentalFeature> experimentalFeature() override
{
return std::nullopt;
}
std::string description() override std::string description() override
{ {
return "check your system for potential problems and print a PASS or FAIL for each check"; return "check your system for potential problems and print a PASS or FAIL for each check";

View File

@ -83,6 +83,7 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs
.description = "Print full build logs on standard error.", .description = "Print full build logs on standard error.",
.category = loggingCategory, .category = loggingCategory,
.handler = {[&]() { logger->setPrintBuildLogs(true); }}, .handler = {[&]() { logger->setPrintBuildLogs(true); }},
.experimentalFeature = Xp::NixCommand,
}); });
addFlag({ addFlag({
@ -98,6 +99,7 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs
.description = "Disable substituters and consider all previously downloaded files up-to-date.", .description = "Disable substituters and consider all previously downloaded files up-to-date.",
.category = miscCategory, .category = miscCategory,
.handler = {[&]() { useNet = false; }}, .handler = {[&]() { useNet = false; }},
.experimentalFeature = Xp::NixCommand,
}); });
addFlag({ addFlag({
@ -105,6 +107,7 @@ struct NixArgs : virtual MultiCommand, virtual MixCommonArgs
.description = "Consider all previously downloaded files out-of-date.", .description = "Consider all previously downloaded files out-of-date.",
.category = miscCategory, .category = miscCategory,
.handler = {[&]() { refresh = true; }}, .handler = {[&]() { refresh = true; }},
.experimentalFeature = Xp::NixCommand,
}); });
} }
@ -420,10 +423,8 @@ void mainWrapped(int argc, char * * argv)
if (!args.command) if (!args.command)
throw UsageError("no subcommand specified"); throw UsageError("no subcommand specified");
if (args.command->first != "repl" experimentalFeatureSettings.require(
&& args.command->first != "doctor" args.command->second->experimentalFeature());
&& args.command->first != "upgrade-nix")
experimentalFeatureSettings.require(Xp::NixCommand);
if (args.useNet && !haveInternet()) { if (args.useNet && !haveInternet()) {
warn("you don't have Internet access; disabling some network-dependent features"); warn("you don't have Internet access; disabling some network-dependent features");

View File

@ -12,6 +12,14 @@ struct CmdRepl : RawInstallablesCommand
evalSettings.pureEval = false; evalSettings.pureEval = false;
} }
/**
* This command is stable before the others
*/
std::optional<ExperimentalFeature> experimentalFeature() override
{
return std::nullopt;
}
std::vector<std::string> files; std::vector<std::string> files;
Strings getDefaultFlakeAttrPaths() override Strings getDefaultFlakeAttrPaths() override

View File

@ -32,6 +32,14 @@ struct CmdUpgradeNix : MixDryRun, StoreCommand
}); });
} }
/**
* This command is stable before the others
*/
std::optional<ExperimentalFeature> experimentalFeature() override
{
return std::nullopt;
}
std::string description() override std::string description() override
{ {
return "upgrade Nix to the stable version declared in Nixpkgs"; return "upgrade Nix to the stable version declared in Nixpkgs";

View File

@ -15,9 +15,26 @@ function both_ways {
# Simple case, the configuration effects the running command # Simple case, the configuration effects the running command
both_ways show-config both_ways show-config
# Complicated case, earlier args effect later args # Skipping for now, because we actually *do* want these to show up in
# the manual, just be marked experimental. Will reenable once the manual
# generation takes advantage of the JSON metadata on this.
both_ways store gc --help # both_ways store gc --help
expect 1 nix --experimental-features 'nix-command' show-config --flake-registry 'https://no' expect 1 nix --experimental-features 'nix-command' show-config --flake-registry 'https://no'
nix --experimental-features 'nix-command flakes' show-config --flake-registry 'https://no' nix --experimental-features 'nix-command flakes' show-config --flake-registry 'https://no'
# Double check these are stable
nix --experimental-features '' --help
nix --experimental-features '' doctor --help
nix --experimental-features '' repl --help
nix --experimental-features '' upgrade-nix --help
# These 3 arguments are currently given to all commands, which is wrong (as not
# all care). To deal with fixing later, we simply make them require the
# nix-command experimental features --- it so happens that the commands we wish
# stabilizing to do not need them anyways.
for arg in '--print-build-logs' '--offline' '--refresh'; do
nix --experimental-features 'nix-command' "$arg" --help
! nix --experimental-features '' "$arg" --help
done

View File

@ -96,7 +96,9 @@ json=$(nix flake metadata flake1 --json | jq .)
hash1=$(echo "$json" | jq -r .revision) hash1=$(echo "$json" | jq -r .revision)
echo -n '# foo' >> $flake1Dir/flake.nix echo -n '# foo' >> $flake1Dir/flake.nix
flake1OriginalCommit=$(git -C $flake1Dir rev-parse HEAD)
git -C $flake1Dir commit -a -m 'Foo' git -C $flake1Dir commit -a -m 'Foo'
flake1NewCommit=$(git -C $flake1Dir rev-parse HEAD)
hash2=$(nix flake metadata flake1 --json --refresh | jq -r .revision) hash2=$(nix flake metadata flake1 --json --refresh | jq -r .revision)
[[ $hash1 != $hash2 ]] [[ $hash1 != $hash2 ]]
@ -491,3 +493,14 @@ nix store delete $(nix store add-path $badFlakeDir)
[[ $(nix-instantiate --eval flake:git+file://$flake3Dir -A x) = 123 ]] [[ $(nix-instantiate --eval flake:git+file://$flake3Dir -A x) = 123 ]]
[[ $(nix-instantiate -I flake3=flake:flake3 --eval '<flake3>' -A x) = 123 ]] [[ $(nix-instantiate -I flake3=flake:flake3 --eval '<flake3>' -A x) = 123 ]]
[[ $(NIX_PATH=flake3=flake:flake3 nix-instantiate --eval '<flake3>' -A x) = 123 ]] [[ $(NIX_PATH=flake3=flake:flake3 nix-instantiate --eval '<flake3>' -A x) = 123 ]]
# Test alternate lockfile paths.
nix flake lock $flake2Dir --output-lock-file $TEST_ROOT/flake2.lock
cmp $flake2Dir/flake.lock $TEST_ROOT/flake2.lock >/dev/null # lockfiles should be identical, since we're referencing flake2's original one
nix flake lock $flake2Dir --output-lock-file $TEST_ROOT/flake2-overridden.lock --override-input flake1 git+file://$flake1Dir?rev=$flake1OriginalCommit
expectStderr 1 cmp $flake2Dir/flake.lock $TEST_ROOT/flake2-overridden.lock
nix flake metadata $flake2Dir --reference-lock-file $TEST_ROOT/flake2-overridden.lock | grepQuiet $flake1OriginalCommit
# reference-lock-file can only be used if allow-dirty is set.
expectStderr 1 nix flake metadata $flake2Dir --no-allow-dirty --reference-lock-file $TEST_ROOT/flake2-overridden.lock

View File

@ -5,12 +5,19 @@ export NIX_REMOTE=dummy://
export NIX_STORE_DIR=/nix/store export NIX_STORE_DIR=/nix/store
nix-instantiate --eval -E 'builtins.trace "Hello" 123' 2>&1 | grepQuiet Hello nix-instantiate --eval -E 'builtins.trace "Hello" 123' 2>&1 | grepQuiet Hello
nix-instantiate --eval -E 'builtins.trace "Hello" 123' 2>/dev/null | grepQuiet 123
nix-instantiate --eval -E 'builtins.addErrorContext "Hello" 123' 2>&1 nix-instantiate --eval -E 'builtins.addErrorContext "Hello" 123' 2>&1
nix-instantiate --trace-verbose --eval -E 'builtins.traceVerbose "Hello" 123' 2>&1 | grepQuiet Hello nix-instantiate --trace-verbose --eval -E 'builtins.traceVerbose "Hello" 123' 2>&1 | grepQuiet Hello
nix-instantiate --eval -E 'builtins.traceVerbose "Hello" 123' 2>&1 | grepQuietInverse Hello nix-instantiate --eval -E 'builtins.traceVerbose "Hello" 123' 2>&1 | grepQuietInverse Hello
nix-instantiate --show-trace --eval -E 'builtins.addErrorContext "Hello" 123' 2>&1 | grepQuietInverse Hello nix-instantiate --show-trace --eval -E 'builtins.addErrorContext "Hello" 123' 2>&1 | grepQuietInverse Hello
expectStderr 1 nix-instantiate --show-trace --eval -E 'builtins.addErrorContext "Hello" (throw "Foo")' | grepQuiet Hello expectStderr 1 nix-instantiate --show-trace --eval -E 'builtins.addErrorContext "Hello" (throw "Foo")' | grepQuiet Hello
nix-instantiate --eval -E 'let x = builtins.trace { x = x; } true; in x' \
2>&1 | grepQuiet -E 'trace: { x = «potential infinite recursion»; }'
nix-instantiate --eval -E 'let x = { repeating = x; tracing = builtins.trace x true; }; in x.tracing'\
2>&1 | grepQuiet -F 'trace: { repeating = «repeated»; tracing = «potential infinite recursion»; }'
set +x set +x
fail=0 fail=0