Compare commits

...

5 Commits

Author SHA1 Message Date
Sergei Zimmerman
2ded675e56 treewide: Make exceptions cloneable
This is needed to make it possible to store exceptions in failed values
with each new rethrow getting a fresh copy of the exception object.
2026-02-18 19:45:49 +03:00
John Ericson
663db5b48b Merge pull request #15278 from puffnfresh/windows/bar-log-format
Windows: don't use bar log format
2026-02-18 05:14:27 +00:00
Brian McKenna
c486e78235 Windows: don't use bar log format
Relies on terminal features that don't always work on Windows.
2026-02-18 14:35:35 +11:00
John Ericson
4fff871383 Merge pull request #15274 from obsidiansystems/tryToBuild-raii
libstore: refactor `tryToBuild` with coroutine lambdas and RAII
2026-02-17 22:10:42 +00:00
Amaan Qureshi
b9acea908e libstore: refactor tryToBuild with coroutine lambdas and RAII
`tryToBuild` threaded a single `PathLocks outputLocks` by reference
across all build phases and managed a `std::unique_ptr<Activity> actLock`
with explicit `if (!actLock)` guards and `.reset()` calls around the hook
retry loop. This commit introduces coroutine lambdas for the three phases:
`tryHookLoop` owns a `PathLocks` in a scoped block for the first attempt
and per-iteration in the retry loop, `tryBuildLocally` acquires its own
`PathLocks`, and the hook-wait `Activity` is a stack variable scoped to
the postpone block.
2026-02-17 16:23:44 -05:00
27 changed files with 203 additions and 126 deletions

View File

@@ -11,7 +11,7 @@
namespace nix::eval_cache {
CachedEvalError::CachedEvalError(ref<AttrCursor> cursor, Symbol attr)
: EvalError(cursor->root->state, "cached failure of attribute '%s'", cursor->getAttrPathStr(attr))
: CloneableError(cursor->root->state, "cached failure of attribute '%s'", cursor->getAttrPathStr(attr))
, cursor(cursor)
, attr(attr)
{

View File

@@ -14,7 +14,7 @@ namespace nix::eval_cache {
struct AttrDb;
class AttrCursor;
struct CachedEvalError : EvalError
struct CachedEvalError : CloneableError<CachedEvalError, EvalError>
{
const ref<AttrCursor> cursor;
const Symbol attr;

View File

@@ -18,7 +18,7 @@ class EvalErrorBuilder;
*
* Most subclasses should inherit from `EvalError` instead of this class.
*/
class EvalBaseError : public Error
class EvalBaseError : public CloneableError<EvalBaseError, Error>
{
template<class T>
friend class EvalErrorBuilder;
@@ -26,14 +26,14 @@ public:
EvalState & state;
EvalBaseError(EvalState & state, ErrorInfo && errorInfo)
: Error(errorInfo)
: CloneableError(errorInfo)
, state(state)
{
}
template<typename... Args>
explicit EvalBaseError(EvalState & state, const std::string & formatString, const Args &... formatArgs)
: Error(formatString, formatArgs...)
: CloneableError(formatString, formatArgs...)
, state(state)
{
}
@@ -60,23 +60,23 @@ MakeError(InfiniteRecursionError, EvalError);
* Inherits from EvalBaseError (not EvalError) because resource exhaustion
* should not be cached.
*/
struct StackOverflowError : public EvalBaseError
struct StackOverflowError : public CloneableError<StackOverflowError, EvalBaseError>
{
StackOverflowError(EvalState & state)
: EvalBaseError(state, "stack overflow; max-call-depth exceeded")
: CloneableError(state, "stack overflow; max-call-depth exceeded")
{
}
};
MakeError(IFDError, EvalBaseError);
struct InvalidPathError : public EvalError
struct InvalidPathError : public CloneableError<InvalidPathError, EvalError>
{
public:
Path path;
InvalidPathError(EvalState & state, const Path & path)
: EvalError(state, "path '%s' is not valid", path)
: CloneableError(state, "path '%s' is not valid", path)
{
}
};

View File

@@ -9,14 +9,14 @@
namespace nix {
class BadNixStringContextElem : public Error
class BadNixStringContextElem final : public CloneableError<BadNixStringContextElem, Error>
{
public:
std::string_view raw;
template<typename... Args>
BadNixStringContextElem(std::string_view raw_, const Args &... args)
: Error("")
: CloneableError("")
{
raw = raw_;
auto hf = HintFmt(args...);

View File

@@ -1312,11 +1312,12 @@ static void prim_warn(EvalState & state, const PosIdx pos, Value ** args, Value
state.forceString(*args[0], pos, "while evaluating the first argument; the message passed to builtins.warn");
{
BaseError msg(std::string{msgStr});
msg.atPos(state.positions[pos]);
auto info = msg.info();
info.level = lvlWarn;
info.isFromExpr = true;
ErrorInfo info{
.level = lvlWarn,
.msg = HintFmt(std::string(msgStr)),
.pos = state.positions[pos],
.isFromExpr = true,
};
logWarning(info);
}

View File

@@ -74,11 +74,11 @@ namespace nix {
struct GitSourceAccessor;
struct GitError : public Error
struct GitError final : public CloneableError<GitError, Error>
{
template<typename... Ts>
GitError(const git_error & error, Ts &&... args)
: Error("")
: CloneableError("")
{
auto hf = HintFmt(std::forward<Ts>(args)...);
err.msg = HintFmt("%1%: %2% (libgit2 error code = %3%)", Uncolored(hf.str()), error.message, error.klass);

View File

@@ -28,7 +28,7 @@
namespace nix {
AwsAuthError::AwsAuthError(int errorCode)
: Error("AWS authentication error: '%s' (%d)", aws_error_str(errorCode), errorCode)
: CloneableError("AWS authentication error: '%s' (%d)", aws_error_str(errorCode), errorCode)
, errorCode(errorCode)
{
}

View File

@@ -362,25 +362,17 @@ Goal::Co DerivationBuildingGoal::tryToBuild(StorePathSet inputPaths)
return LocalBuildCapability{*localStoreP, ext};
}();
/**
* Activity that denotes waiting for a lock.
*/
std::unique_ptr<Activity> actLock;
/**
* Locks on (fixed) output paths.
*/
PathLocks outputLocks;
while (true) {
auto acquireResources = [&](bool & done, PathLocks & outputLocks) -> Goal::Co {
trace("trying to build");
/* Obtain locks on all output paths, if the paths are known a priori.
The locks are automatically released when we exit this function or Nix
crashes. If we can't acquire the lock, then continue; hopefully some
other goal can start a build, and if not, the main loop will sleep a few
seconds and then retry this goal. */
/**
* Output paths to acquire locks on, if known a priori.
*
* The locks are automatically released when the caller's `PathLocks` goes
* out of scope, including on exception unwinding. If we can't acquire the lock, then
* continue; hopefully some other goal can start a build, and if not, the
* main loop will sleep a few seconds and then retry this goal.
*/
std::set<std::filesystem::path> lockFiles;
/* FIXME: Should lock something like the drv itself so we don't build same
CA drv concurrently */
@@ -424,7 +416,8 @@ Goal::Co DerivationBuildingGoal::tryToBuild(StorePathSet inputPaths)
debug("skipping build of derivation '%s', someone beat us to it", worker.store.printStorePath(drvPath));
outputLocks.setDeletion(true);
outputLocks.unlock();
co_return doneSuccess(BuildResult::Success::AlreadyValid, std::move(validOutputs));
done = true;
co_return Return{};
}
/* If any of the outputs already exist but are not valid, delete
@@ -439,71 +432,133 @@ Goal::Co DerivationBuildingGoal::tryToBuild(StorePathSet inputPaths)
}
}
// Check and repair modes are only supported for local builds.
if (buildMode != bmNormal) {
// can try hook: false
// can try local: true
// preference: doesn't matter because it's only one option
break;
} else {
// can try hook: true
// can try local: true
// preference: unknown
co_return Return{};
};
if (drvOptions.preferLocalBuild) {
// preference: local
if (std::holds_alternative<LocalBuildCapability>(localBuildResult)) {
// Can build locally as preferred, so do it.
break;
}
// We cannot build locally, but we can maybe do the hook.
}
auto tryHookLoop = [&](bool & valid) -> Goal::Co {
{
PathLocks outputLocks;
co_await acquireResources(valid, outputLocks);
if (valid)
co_return doneSuccess(BuildResult::Success::AlreadyValid, checkPathValidity(initialOutputs).second);
// If we don't prefer a local build, OR we do but the local build was rejected, then we try the hook if
// possible.
switch (tryBuildHook(drvOptions)) {
case rpAccept:
/* Yes, it has started doing so. Wait until we get
EOF from the hook. */
actLock.reset();
valid = true;
co_return buildWithHook(
std::move(inputPaths), std::move(initialOutputs), std::move(drvOptions), std::move(outputLocks));
break;
case rpDecline:
// We should do it ourselves.
co_return Return{};
case rpPostpone:
/* Not now; wait until at least one child finishes or
the wake-up timeout expires. */
if (!actLock)
actLock = std::make_unique<Activity>(
*logger,
lvlWarn,
actBuildWaiting,
fmt("waiting for a machine to build '%s'", Magenta(worker.store.printStorePath(drvPath))));
outputLocks.unlock();
co_await waitForAWhile();
continue;
case rpDecline:
// We should do it ourselves.
break;
}
}
break;
}
PathLocks outputLocks;
{
// First attempt was postponed. Retry in a loop with an activity
// that lives until accept or decline.
Activity act(
*logger,
lvlWarn,
actBuildWaiting,
fmt("waiting for a machine to build '%s'", Magenta(worker.store.printStorePath(drvPath))));
// can do hook: false (either we can't try, or we tried and were declined)
// can try local: true
// preference: doesn't matter
while (true) {
co_await waitForAWhile();
co_await acquireResources(valid, outputLocks);
if (valid)
break;
actLock.reset();
switch (tryBuildHook(drvOptions)) {
case rpAccept:
/* Yes, it has started doing so. Wait until we get
EOF from the hook. */
break;
case rpPostpone:
/* Not now; wait until at least one child finishes or
the wake-up timeout expires. */
outputLocks.unlock();
continue;
case rpDecline:
// We should do it ourselves.
co_return Return{};
}
if (auto * cap = std::get_if<LocalBuildCapability>(&localBuildResult)) {
co_return buildLocally(
*cap, std::move(inputPaths), std::move(initialOutputs), std::move(drvOptions), std::move(outputLocks));
break;
}
}
if (valid) {
co_return doneSuccess(BuildResult::Success::AlreadyValid, checkPathValidity(initialOutputs).second);
} else {
co_return buildWithHook(
std::move(inputPaths), std::move(initialOutputs), std::move(drvOptions), std::move(outputLocks));
}
};
auto tryBuildLocally = [&](bool & valid) -> Goal::Co {
if (auto * cap = std::get_if<LocalBuildCapability>(&localBuildResult)) {
PathLocks outputLocks;
co_await acquireResources(valid, outputLocks);
if (valid)
co_return doneSuccess(BuildResult::Success::AlreadyValid, checkPathValidity(initialOutputs).second);
valid = true;
co_return buildLocally(
*cap, std::move(inputPaths), std::move(initialOutputs), std::move(drvOptions), std::move(outputLocks));
}
co_return Return{};
};
if (buildMode != bmNormal) {
// Check and repair modes operate on the state of this store specifically,
// so they must always build locally.
bool valid = false;
co_await tryBuildLocally(valid);
if (valid)
co_return Return{};
} else if (drvOptions.preferLocalBuild) {
// Local is preferred, so try it first. If it's not available, fall back to the hook.
{
bool valid = false;
co_await tryBuildLocally(valid);
if (valid)
co_return Return{};
}
{
bool valid = false;
co_await tryHookLoop(valid);
if (valid)
co_return Return{};
}
} else {
outputLocks.unlock();
std::string storePath = worker.store.printStorePath(drvPath);
co_return doneFailure(reject(std::get<LocalBuildRejection>(localBuildResult), storePath));
// Default preference is a remote build: they tend to be faster and preserve local
// resources for other tasks. Fall back to local if no remote is available.
{
bool valid = false;
co_await tryHookLoop(valid);
if (valid)
co_return Return{};
}
{
bool valid = false;
co_await tryBuildLocally(valid);
if (valid)
co_return Return{};
}
}
std::string storePath = worker.store.printStorePath(drvPath);
auto * rejection = std::get_if<LocalBuildRejection>(&localBuildResult);
assert(rejection);
co_return doneFailure(reject(*rejection, storePath));
}
Goal::Co DerivationBuildingGoal::buildWithHook(

View File

@@ -1,11 +1,11 @@
#include "nix/store/build/goal.hh"
#include "nix/store/build/worker.hh"
#include "nix/store/globals.hh"
#include "nix/store/worker-settings.hh"
namespace nix {
TimedOut::TimedOut(time_t maxDuration)
: BuildError(BuildResult::Failure::TimedOut, "timed out after %1% seconds", maxDuration)
: CloneableError(BuildResult::Failure::TimedOut, "timed out after %1% seconds", maxDuration)
, maxDuration(maxDuration)
{
}

View File

@@ -60,12 +60,12 @@ namespace {
using curlSList = std::unique_ptr<::curl_slist, decltype([](::curl_slist * list) { ::curl_slist_free_all(list); })>;
using curlMulti = std::unique_ptr<::CURLM, decltype([](::CURLM * multi) { ::curl_multi_cleanup(multi); })>;
struct curlMultiError : Error
struct curlMultiError final : CloneableError<curlMultiError, Error>
{
::CURLMcode code;
curlMultiError(::CURLMcode code)
: Error{"unexpected curl multi error: %s", ::curl_multi_strerror(code)}
: CloneableError{"unexpected curl multi error: %s", ::curl_multi_strerror(code)}
{
assert(code != CURLM_OK);
}
@@ -1212,7 +1212,7 @@ void FileTransfer::download(
template<typename... Args>
FileTransferError::FileTransferError(
FileTransfer::Error error, std::optional<std::string> response, const Args &... args)
: Error(args...)
: CloneableError(args...)
, error(error)
, response(response)
{

View File

@@ -34,12 +34,13 @@ struct AwsCredentials
}
};
class AwsAuthError : public Error
class AwsAuthError final : public CloneableError<AwsAuthError, Error>
{
std::optional<int> errorCode;
public:
using Error::Error;
using CloneableError::CloneableError;
AwsAuthError(int errorCode);
std::optional<int> getErrorCode() const

View File

@@ -58,7 +58,7 @@ enum struct BuildResultFailureStatus : uint8_t {
* This is both an exception type (inherits from Error) and serves as
* the failure variant in BuildResult::inner.
*/
struct BuildError : public Error
struct BuildError : public CloneableError<BuildError, Error>
{
using Status = BuildResultFailureStatus;
using enum Status;
@@ -80,7 +80,7 @@ public:
*/
template<typename... Args>
BuildError(Status status, const Args &... args)
: Error(args...)
: CloneableError(args...)
, status{status}
{
}
@@ -97,7 +97,7 @@ public:
* Also used for deserialization.
*/
BuildError(Args args)
: Error(std::move(args.msg))
: CloneableError(std::move(args.msg))
, status{args.status}
, isNonDeterministic{args.isNonDeterministic}
@@ -108,7 +108,7 @@ public:
* Default constructor for deserialization.
*/
BuildError()
: Error("")
: CloneableError("")
{
}

View File

@@ -20,14 +20,14 @@ namespace nix {
* Denotes a build failure that stemmed from the builder exiting with a
* failing exist status.
*/
struct BuilderFailureError : BuildError
struct BuilderFailureError final : CloneableError<BuilderFailureError, BuildError>
{
int builderStatus;
std::string extraMsgAfter;
BuilderFailureError(BuildResult::Failure::Status status, int builderStatus, std::string extraMsgAfter)
: BuildError{
: CloneableError{
status,
/* No message for now, because the caller will make for
us, with extra context */

View File

@@ -10,7 +10,7 @@
namespace nix {
struct TimedOut : BuildError
struct TimedOut final : CloneableError<TimedOut, BuildError>
{
time_t maxDuration;

View File

@@ -22,7 +22,7 @@ struct Package
}
};
class BuildEnvFileConflictError : public Error
class BuildEnvFileConflictError final : public CloneableError<BuildEnvFileConflictError, Error>
{
public:
const Path fileA;
@@ -30,7 +30,7 @@ public:
int priority;
BuildEnvFileConflictError(const Path fileA, const Path fileB, int priority)
: Error(
: CloneableError(
"Unable to build profile. There is a conflict for the following files:\n"
"\n"
" %1%\n"

View File

@@ -403,7 +403,7 @@ ref<FileTransfer> getFileTransfer();
*/
ref<FileTransfer> makeFileTransfer(const FileTransferSettings & settings = fileTransferSettings);
class FileTransferError : public Error
class FileTransferError final : public CloneableError<FileTransferError, Error>
{
public:
FileTransfer::Error error;

View File

@@ -146,7 +146,7 @@ struct RealisedPath
auto operator<=>(const RealisedPath &) const = default;
};
class MissingRealisation : public Error
class MissingRealisation final : public CloneableError<MissingRealisation, Error>
{
public:
MissingRealisation(DrvOutput & outputId)
@@ -155,7 +155,7 @@ public:
}
MissingRealisation(std::string_view drv, OutputName outputName)
: Error(
: CloneableError(
"cannot operate on output '%s' of the "
"unbuilt derivation '%s'",
outputName,

View File

@@ -166,7 +166,7 @@ struct SQLiteTxn
~SQLiteTxn();
};
struct SQLiteError : Error
struct SQLiteError : CloneableError<SQLiteError, Error>
{
std::string path;
std::string errMsg;

View File

@@ -17,7 +17,7 @@ namespace nix {
SQLiteError::SQLiteError(
const char * path, const char * errMsg, int errNo, int extendedErrNo, int offset, HintFmt && hf)
: Error("")
: CloneableError("")
, path(path)
, errMsg(errMsg)
, errNo(errNo)

View File

@@ -18,11 +18,11 @@ static std::string parsePublicHostKey(std::string_view host, std::string_view ss
}
}
class InvalidSSHAuthority : public Error
class InvalidSSHAuthority final : public CloneableError<InvalidSSHAuthority, Error>
{
public:
InvalidSSHAuthority(const ParsedURL::Authority & authority, std::string_view reason)
: Error("invalid SSH authority: '%s': %s", authority.to_string(), reason)
: CloneableError("invalid SSH authority: '%s': %s", authority.to_string(), reason)
{
}
};

View File

@@ -55,10 +55,10 @@
namespace nix {
struct NotDeterministic : BuildError
struct NotDeterministic final : CloneableError<NotDeterministic, BuildError>
{
NotDeterministic(auto &&... args)
: BuildError(BuildResult::Failure::NotDeterministic, args...)
: CloneableError(BuildResult::Failure::NotDeterministic, args...)
{
isNonDeterministic = true;
}

View File

@@ -378,7 +378,7 @@ std::set<ExperimentalFeature> parseFeatures(const StringSet & rawFeatures)
}
MissingExperimentalFeature::MissingExperimentalFeature(ExperimentalFeature feature, std::string reason)
: Error(
: CloneableError(
"experimental Nix feature '%1%' is disabled%2%; add '--extra-experimental-features %1%' to enable it",
showExperimentalFeature(feature),
Uncolored(optionalBracket(" (", reason, ")")))

View File

@@ -226,13 +226,31 @@ public:
{
return err;
};
[[noreturn]] virtual void throwClone() const = 0;
};
#define MakeError(newClass, superClass) \
class newClass : public superClass \
{ \
public: \
using superClass::superClass; \
template<typename Derived, typename Base>
class CloneableError : public Base
{
public:
using Base::Base;
/**
* Rethrow a copy of this exception. Useful when the exception can get
* modified when appending traces.
*/
[[noreturn]] void throwClone() const override
{
throw Derived(static_cast<const Derived &>(*this));
}
};
#define MakeError(newClass, superClass) \
class newClass : public CloneableError<newClass, superClass> \
{ \
public: \
using CloneableError<newClass, superClass>::CloneableError; \
}
MakeError(Error, BaseError);
@@ -244,21 +262,21 @@ MakeError(UnimplementedError, Error);
* std::error_code. Use when you want to catch and check an error condition like
* no_such_file_or_directory (ENOENT) without ifdefs.
*/
class SystemError : public Error
class SystemError : public CloneableError<SystemError, Error>
{
std::error_code errorCode;
public:
template<typename... Args>
SystemError(std::errc posixErrNo, Args &&... args)
: Error(std::forward<Args>(args)...)
: CloneableError(std::forward<Args>(args)...)
, errorCode(std::make_error_code(posixErrNo))
{
}
template<typename... Args>
SystemError(std::error_code errorCode, Args &&... args)
: Error(std::forward<Args>(args)...)
: CloneableError(std::forward<Args>(args)...)
, errorCode(errorCode)
{
}
@@ -290,7 +308,7 @@ public:
* support is too WIP to justify the code churn, but if it is finished
* then a better identifier becomes moe worth it.
*/
class SysError : public SystemError
class SysError final : public CloneableError<SysError, SystemError>
{
public:
int errNo;
@@ -301,7 +319,7 @@ public:
*/
template<typename... Args>
SysError(int errNo, const Args &... args)
: SystemError(static_cast<std::errc>(errNo), "")
: CloneableError(static_cast<std::errc>(errNo), "")
, errNo(errNo)
{
auto hf = HintFmt(args...);
@@ -369,7 +387,7 @@ namespace windows {
* Unless you need to catch a specific error number, don't catch this in
* portable code. Catch `SystemError` instead.
*/
class WinError : public SystemError
class WinError : public CloneableError<WinError, SystemError>
{
public:
DWORD lastError;
@@ -381,7 +399,7 @@ public:
*/
template<typename... Args>
WinError(DWORD lastError, const Args &... args)
: SystemError(std::error_code(lastError, std::system_category()), "")
: CloneableError(std::error_code(lastError, std::system_category()), "")
, lastError(lastError)
{
auto hf = HintFmt(args...);

View File

@@ -80,7 +80,7 @@ std::set<ExperimentalFeature> parseFeatures(const StringSet &);
* An experimental feature was required for some (experimental)
* operation, but was not enabled.
*/
class MissingExperimentalFeature : public Error
class MissingExperimentalFeature final : public CloneableError<MissingExperimentalFeature, Error>
{
public:
/**

View File

@@ -133,14 +133,14 @@ std::pair<int, std::string> runProgram(RunOptions && options);
void runProgram2(const RunOptions & options);
class ExecError : public Error
class ExecError final : public CloneableError<ExecError, Error>
{
public:
int status;
template<typename... Args>
ExecError(int status, const Args &... args)
: Error(args...)
: CloneableError(args...)
, status(status)
{
}

View File

@@ -231,19 +231,19 @@ ref<SourceAccessor> makeEmptySourceAccessor();
*/
MakeError(RestrictedPathError, Error);
struct SymlinkNotAllowed : public Error
struct SymlinkNotAllowed final : public CloneableError<SymlinkNotAllowed, Error>
{
CanonPath path;
SymlinkNotAllowed(CanonPath path)
: Error("relative path '%s' points to a symlink, which is not allowed", path.rel())
: CloneableError("relative path '%s' points to a symlink, which is not allowed", path.rel())
, path(std::move(path))
{
}
template<typename... Args>
SymlinkNotAllowed(CanonPath path, const std::string & fs, Args &&... args)
: Error(fs, std::forward<Args>(args)...)
: CloneableError(fs, std::forward<Args>(args)...)
, path(std::move(path))
{
}

View File

@@ -427,7 +427,9 @@ void mainWrapped(int argc, char ** argv)
evalSettings.pureEval = true;
#ifndef _WIN32
setLogFormat("bar");
#endif
settings.verboseBuild = false;
// If on a terminal, progress will be displayed via progress bars etc. (thus verbosity=notice)