1
1
Fork 0
mirror of https://github.com/NixOS/nix.git synced 2025-11-08 19:46:02 +01:00

Simplify handling of statuses for build errors

Instead of passing them around separately, or doing finicky logic in a
try-catch block to recover them, just make `BuildError` always contain a
status, and make it the thrower's responsibility to set it. This is much
more simple and explicit.

Once that change is done, split the `done` functions of `DerivationGoal`
and `DerivationBuildingGoal` into separate success and failure
functions, which ends up being easier to understand and hardly any
duplication.

Also, change the handling of failures in resolved cases to use
`BuildResult::DependencyFailed` and a new message. This is because the
underlying derivation will also get its message printed --- which is
good, because in general the resolved derivation is not unique. One dyn
drv test had to be updated, but CA (and dyn drv) is experimental, so I
do not mind.

Finally, delete `SubstError` because it is unused.
This commit is contained in:
John Ericson 2025-08-27 18:58:58 -04:00
parent 0590b13156
commit 169033001d
16 changed files with 153 additions and 92 deletions

View file

@ -118,7 +118,7 @@ void DerivationBuildingGoal::timedOut(Error && ex)
killChild();
// We're not inside a coroutine, hence we can't use co_return here.
// Thus we ignore the return value.
[[maybe_unused]] Done _ = done(BuildResult::TimedOut, {}, std::move(ex));
[[maybe_unused]] Done _ = doneFailure({BuildResult::TimedOut, std::move(ex)});
}
/**
@ -258,7 +258,7 @@ Goal::Co DerivationBuildingGoal::gaveUpOnSubstitution()
nrFailed,
nrFailed == 1 ? "dependency" : "dependencies");
msg += showKnownOutputs(worker.store, *drv);
co_return done(BuildResult::DependencyFailed, {}, Error(msg));
co_return doneFailure(BuildError(BuildResult::DependencyFailed, msg));
}
/* Gather information necessary for computing the closure and/or
@ -359,9 +359,9 @@ Goal::Co DerivationBuildingGoal::gaveUpOnSubstitution()
auto resolvedResult = resolvedDrvGoal->buildResult;
SingleDrvOutputs builtOutputs;
if (resolvedResult.success()) {
SingleDrvOutputs builtOutputs;
auto resolvedHashes = staticOutputHashes(worker.store, drvResolved);
StorePathSet outputPaths;
@ -411,13 +411,19 @@ Goal::Co DerivationBuildingGoal::gaveUpOnSubstitution()
}
runPostBuildHook(worker.store, *logger, drvPath, outputPaths);
auto status = resolvedResult.status;
if (status == BuildResult::AlreadyValid)
status = BuildResult::ResolvesToAlreadyValid;
co_return doneSuccess(status, std::move(builtOutputs));
} else {
co_return doneFailure({
BuildResult::DependencyFailed,
"build of resolved derivation '%s' failed",
worker.store.printStorePath(pathResolved),
});
}
auto status = resolvedResult.status;
if (status == BuildResult::AlreadyValid)
status = BuildResult::ResolvesToAlreadyValid;
co_return done(status, std::move(builtOutputs));
}
/* If we get this far, we know no dynamic drvs inputs */
@ -542,7 +548,7 @@ Goal::Co DerivationBuildingGoal::tryToBuild()
debug("skipping build of derivation '%s', someone beat us to it", worker.store.printStorePath(drvPath));
outputLocks.setDeletion(true);
outputLocks.unlock();
co_return done(BuildResult::AlreadyValid, std::move(validOutputs));
co_return doneSuccess(BuildResult::AlreadyValid, std::move(validOutputs));
}
/* If any of the outputs already exist but are not valid, delete
@ -752,7 +758,7 @@ Goal::Co DerivationBuildingGoal::tryToBuild()
} catch (BuildError & e) {
outputLocks.unlock();
worker.permanentFailure = true;
co_return done(BuildResult::InputRejected, {}, std::move(e));
co_return doneFailure(std::move(e));
}
/* If we have to wait and retry (see below), then `builder` will
@ -800,7 +806,7 @@ Goal::Co DerivationBuildingGoal::tryToBuild()
builder.reset();
outputLocks.unlock();
worker.permanentFailure = true;
co_return done(BuildResult::InputRejected, {}, std::move(e));
co_return doneFailure(std::move(e)); // InputRejected
}
started();
@ -812,7 +818,7 @@ Goal::Co DerivationBuildingGoal::tryToBuild()
// N.B. cannot use `std::visit` with co-routine return
if (auto * ste = std::get_if<0>(&res)) {
outputLocks.unlock();
co_return done(std::move(ste->first), {}, std::move(ste->second));
co_return doneFailure(std::move(*ste));
} else if (auto * builtOutputs = std::get_if<1>(&res)) {
StorePathSet outputPaths;
for (auto & [_, output] : *builtOutputs)
@ -825,7 +831,7 @@ Goal::Co DerivationBuildingGoal::tryToBuild()
(unlinked) lock files. */
outputLocks.setDeletion(true);
outputLocks.unlock();
co_return done(BuildResult::Built, std::move(*builtOutputs));
co_return doneSuccess(BuildResult::Built, std::move(*builtOutputs));
} else {
unreachable();
}
@ -970,7 +976,7 @@ Goal::Co DerivationBuildingGoal::hookDone()
/* TODO (once again) support fine-grained error codes, see issue #12641. */
co_return done(BuildResult::MiscFailure, {}, BuildError(msg));
co_return doneFailure(BuildError{BuildResult::MiscFailure, msg});
}
/* Compute the FS closure of the outputs and register them as
@ -997,7 +1003,7 @@ Goal::Co DerivationBuildingGoal::hookDone()
outputLocks.setDeletion(true);
outputLocks.unlock();
co_return done(BuildResult::Built, std::move(builtOutputs));
co_return doneSuccess(BuildResult::Built, std::move(builtOutputs));
}
HookReply DerivationBuildingGoal::tryBuildHook()
@ -1179,10 +1185,11 @@ void DerivationBuildingGoal::handleChildOutput(Descriptor fd, std::string_view d
killChild();
// We're not inside a coroutine, hence we can't use co_return here.
// Thus we ignore the return value.
[[maybe_unused]] Done _ = done(
[[maybe_unused]] Done _ = doneFailure(BuildError(
BuildResult::LogLimitExceeded,
{},
Error("%s killed after writing more than %d bytes of log output", getName(), settings.maxLogSize));
"%s killed after writing more than %d bytes of log output",
getName(),
settings.maxLogSize));
return;
}
@ -1343,13 +1350,27 @@ SingleDrvOutputs DerivationBuildingGoal::assertPathValidity()
return validOutputs;
}
Goal::Done
DerivationBuildingGoal::done(BuildResult::Status status, SingleDrvOutputs builtOutputs, std::optional<Error> ex)
Goal::Done DerivationBuildingGoal::doneSuccess(BuildResult::Status status, SingleDrvOutputs builtOutputs)
{
outputLocks.unlock();
buildResult.status = status;
if (ex)
buildResult.errorMsg = fmt("%s", Uncolored(ex->info().msg));
assert(buildResult.success());
mcRunningBuilds.reset();
buildResult.builtOutputs = std::move(builtOutputs);
if (status == BuildResult::Built)
worker.doneBuilds++;
worker.updateProgress();
return amDone(ecSuccess, std::nullopt);
}
Goal::Done DerivationBuildingGoal::doneFailure(BuildError ex)
{
buildResult.status = ex.status;
buildResult.errorMsg = fmt("%s", Uncolored(ex.info().msg));
if (buildResult.status == BuildResult::TimedOut)
worker.timedOut = true;
if (buildResult.status == BuildResult::PermanentFailure)
@ -1357,18 +1378,12 @@ DerivationBuildingGoal::done(BuildResult::Status status, SingleDrvOutputs builtO
mcRunningBuilds.reset();
if (buildResult.success()) {
buildResult.builtOutputs = std::move(builtOutputs);
if (status == BuildResult::Built)
worker.doneBuilds++;
} else {
if (status != BuildResult::DependencyFailed)
worker.failedBuilds++;
}
if (ex.status != BuildResult::DependencyFailed)
worker.failedBuilds++;
worker.updateProgress();
return amDone(buildResult.success() ? ecSuccess : ecFailed, std::move(ex));
return amDone(ecFailed, {std::move(ex)});
}
} // namespace nix

View file

@ -1,6 +1,7 @@
#include <queue>
#include "nix/store/store-api.hh"
#include "nix/store/build-result.hh"
#include "derivation-check.hh"
@ -54,6 +55,7 @@ void checkOutputs(
auto applyChecks = [&](const DerivationOptions::OutputChecks & checks) {
if (checks.maxSize && info.narSize > *checks.maxSize)
throw BuildError(
BuildResult::OutputRejected,
"path '%s' is too large at %d bytes; limit is %d bytes",
store.printStorePath(info.path),
info.narSize,
@ -63,6 +65,7 @@ void checkOutputs(
uint64_t closureSize = getClosure(info.path).second;
if (closureSize > *checks.maxClosureSize)
throw BuildError(
BuildResult::OutputRejected,
"closure of path '%s' is too large at %d bytes; limit is %d bytes",
store.printStorePath(info.path),
closureSize,
@ -83,6 +86,7 @@ void checkOutputs(
std::string outputsListing =
concatMapStringsSep(", ", outputs, [](auto & o) { return o.first; });
throw BuildError(
BuildResult::OutputRejected,
"derivation '%s' output check for '%s' contains an illegal reference specifier '%s',"
" expected store path or output name (one of [%s])",
store.printStorePath(drvPath),
@ -115,6 +119,7 @@ void checkOutputs(
badPathsStr += store.printStorePath(i);
}
throw BuildError(
BuildResult::OutputRejected,
"output '%s' is not allowed to refer to the following paths:%s",
store.printStorePath(info.path),
badPathsStr);

View file

@ -94,7 +94,7 @@ Goal::Co DerivationGoal::haveDerivation()
/* If they are all valid, then we're done. */
if (checkResult && checkResult->second == PathStatus::Valid && buildMode == bmNormal) {
co_return done(BuildResult::AlreadyValid, checkResult->first);
co_return doneSuccess(BuildResult::AlreadyValid, checkResult->first);
}
Goals waitees;
@ -122,12 +122,10 @@ Goal::Co DerivationGoal::haveDerivation()
assert(!drv->type().isImpure());
if (nrFailed > 0 && nrFailed > nrNoSubstituters && !settings.tryFallback) {
co_return done(
co_return doneFailure(BuildError(
BuildResult::TransientFailure,
{},
Error(
"some substitutes for the outputs of derivation '%s' failed (usually happens due to networking issues); try '--fallback' to build derivation from source ",
worker.store.printStorePath(drvPath)));
"some substitutes for the outputs of derivation '%s' failed (usually happens due to networking issues); try '--fallback' to build derivation from source ",
worker.store.printStorePath(drvPath)));
}
nrFailed = nrNoSubstituters = 0;
@ -137,7 +135,7 @@ Goal::Co DerivationGoal::haveDerivation()
bool allValid = checkResult && checkResult->second == PathStatus::Valid;
if (buildMode == bmNormal && allValid) {
co_return done(BuildResult::Substituted, checkResult->first);
co_return doneSuccess(BuildResult::Substituted, checkResult->first);
}
if (buildMode == bmRepair && allValid) {
co_return repairClosure();
@ -281,7 +279,7 @@ Goal::Co DerivationGoal::repairClosure()
"some paths in the output closure of derivation '%s' could not be repaired",
worker.store.printStorePath(drvPath));
}
co_return done(BuildResult::AlreadyValid, assertPathValidity());
co_return doneSuccess(BuildResult::AlreadyValid, assertPathValidity());
}
std::optional<std::pair<Realisation, PathStatus>> DerivationGoal::checkPathValidity()
@ -339,12 +337,27 @@ Realisation DerivationGoal::assertPathValidity()
return checkResult->first;
}
Goal::Done
DerivationGoal::done(BuildResult::Status status, std::optional<Realisation> builtOutput, std::optional<Error> ex)
Goal::Done DerivationGoal::doneSuccess(BuildResult::Status status, Realisation builtOutput)
{
buildResult.status = status;
if (ex)
buildResult.errorMsg = fmt("%s", Uncolored(ex->info().msg));
assert(buildResult.success());
mcExpectedBuilds.reset();
buildResult.builtOutputs = {{wantedOutput, std::move(builtOutput)}};
if (status == BuildResult::Built)
worker.doneBuilds++;
worker.updateProgress();
return amDone(ecSuccess, std::nullopt);
}
Goal::Done DerivationGoal::doneFailure(BuildError ex)
{
buildResult.status = ex.status;
buildResult.errorMsg = fmt("%s", Uncolored(ex.info().msg));
if (buildResult.status == BuildResult::TimedOut)
worker.timedOut = true;
if (buildResult.status == BuildResult::PermanentFailure)
@ -352,19 +365,12 @@ DerivationGoal::done(BuildResult::Status status, std::optional<Realisation> buil
mcExpectedBuilds.reset();
if (buildResult.success()) {
assert(builtOutput);
buildResult.builtOutputs = {{wantedOutput, std::move(*builtOutput)}};
if (status == BuildResult::Built)
worker.doneBuilds++;
} else {
if (status != BuildResult::DependencyFailed)
worker.failedBuilds++;
}
if (ex.status != BuildResult::DependencyFailed)
worker.failedBuilds++;
worker.updateProgress();
return amDone(buildResult.success() ? ecSuccess : ecFailed, std::move(ex));
return amDone(ecFailed, {std::move(ex)});
}
} // namespace nix

View file

@ -265,7 +265,8 @@ DerivationOptions::getParsedExportReferencesGraph(const StoreDirConfig & store)
StorePathSet storePaths;
for (auto & storePathS : ss) {
if (!store.isInStore(storePathS))
throw BuildError("'exportReferencesGraph' contains a non-store path '%1%'", storePathS);
throw BuildError(
BuildResult::InputRejected, "'exportReferencesGraph' contains a non-store path '%1%'", storePathS);
storePaths.insert(store.toStorePath(storePathS).first);
}
res.insert_or_assign(fileName, storePaths);

View file

@ -1,13 +1,13 @@
#pragma once
///@file
#include "nix/store/realisation.hh"
#include "nix/store/derived-path.hh"
#include <string>
#include <chrono>
#include <optional>
#include "nix/store/derived-path.hh"
#include "nix/store/realisation.hh"
namespace nix {
struct BuildResult
@ -90,6 +90,26 @@ struct BuildResult
}
};
/**
* denotes a permanent build failure
*/
struct BuildError : public Error
{
BuildResult::Status status;
BuildError(BuildResult::Status status, BuildError && error)
: Error{std::move(error)}
, status{status}
{
}
BuildError(BuildResult::Status status, auto &&... args)
: Error{args...}
, status{status}
{
}
};
/**
* A `BuildResult` together with its "primary key".
*/

View file

@ -191,7 +191,7 @@ struct DerivationBuilder : RestrictionContext
* more information. The second case indicates success, and
* realisations for each output of the derivation are returned.
*/
virtual std::variant<std::pair<BuildResult::Status, Error>, SingleDrvOutputs> unprepareBuild() = 0;
virtual std::variant<BuildError, SingleDrvOutputs> unprepareBuild() = 0;
/**
* Stop the in-process nix daemon thread.

View file

@ -170,7 +170,9 @@ struct DerivationBuildingGoal : public Goal
void started();
Done done(BuildResult::Status status, SingleDrvOutputs builtOutputs = {}, std::optional<Error> ex = {});
Done doneSuccess(BuildResult::Status status, SingleDrvOutputs builtOutputs);
Done doneFailure(BuildError ex);
void appendLogTailErrorMsg(std::string & msg);

View file

@ -99,13 +99,9 @@ private:
Co repairClosure();
/**
* @param builtOutput Must be set if `status` is successful.
*/
Done done(
BuildResult::Status status,
std::optional<Realisation> builtOutput = std::nullopt,
std::optional<Error> ex = {});
Done doneSuccess(BuildResult::Status status, Realisation builtOutput);
Done doneFailure(BuildError ex);
};
} // namespace nix

View file

@ -24,11 +24,6 @@
namespace nix {
MakeError(SubstError, Error);
/**
* denotes a permanent build failure
*/
MakeError(BuildError, Error);
MakeError(InvalidPath, Error);
MakeError(Unsupported, Error);
MakeError(SubstituteGone, Error);

View file

@ -1002,7 +1002,10 @@ void LocalStore::registerValidPaths(const ValidPathInfos & infos)
}},
{[&](const StorePath & path, const StorePath & parent) {
return BuildError(
"cycle detected in the references of '%s' from '%s'", printStorePath(path), printStorePath(parent));
BuildResult::OutputRejected,
"cycle detected in the references of '%s' from '%s'",
printStorePath(path),
printStorePath(parent));
}});
txn.commit();

View file

@ -322,7 +322,10 @@ StorePaths Store::topoSortPaths(const StorePathSet & paths)
}},
{[&](const StorePath & path, const StorePath & parent) {
return BuildError(
"cycle detected in the references of '%s' from '%s'", printStorePath(path), printStorePath(parent));
BuildResult::OutputRejected,
"cycle detected in the references of '%s' from '%s'",
printStorePath(path),
printStorePath(parent));
}});
}

View file

@ -98,7 +98,7 @@ static void canonicalisePathMetaData_(
(i.e. "touch $out/foo; ln $out/foo $out/bar"). */
if (uidRange && (st.st_uid < uidRange->first || st.st_uid > uidRange->second)) {
if (S_ISDIR(st.st_mode) || !inodesSeen.count(Inode(st.st_dev, st.st_ino)))
throw BuildError("invalid ownership on file '%1%'", path);
throw BuildError(BuildResult::OutputRejected, "invalid ownership on file '%1%'", path);
mode_t mode = st.st_mode & ~S_IFMT;
assert(
S_ISLNK(st.st_mode)

View file

@ -770,6 +770,7 @@ StorePathSet Store::exportReferences(const StorePathSet & storePaths, const Stor
for (auto & storePath : storePaths) {
if (!inputPaths.count(storePath))
throw BuildError(
BuildResult::InputRejected,
"cannot export references of path '%s' because it is not in the input closure of the derivation",
printStorePath(storePath));

View file

@ -46,7 +46,13 @@
namespace nix {
MakeError(NotDeterministic, BuildError);
struct NotDeterministic : BuildError
{
NotDeterministic(auto &&... args)
: BuildError(BuildResult::NotDeterministic, args...)
{
}
};
/**
* This class represents the state for building locally.
@ -185,7 +191,7 @@ public:
void startBuilder() override;
std::variant<std::pair<BuildResult::Status, Error>, SingleDrvOutputs> unprepareBuild() override;
std::variant<BuildError, SingleDrvOutputs> unprepareBuild() override;
protected:
@ -420,7 +426,7 @@ bool DerivationBuilderImpl::prepareBuild()
return true;
}
std::variant<std::pair<BuildResult::Status, Error>, SingleDrvOutputs> DerivationBuilderImpl::unprepareBuild()
std::variant<BuildError, SingleDrvOutputs> DerivationBuilderImpl::unprepareBuild()
{
// FIXME: get rid of this, rely on RAII.
Finally releaseBuildUser([&]() {
@ -493,7 +499,10 @@ std::variant<std::pair<BuildResult::Status, Error>, SingleDrvOutputs> Derivation
if (diskFull)
msg += "\nnote: build failure may have been caused by lack of free disk space";
throw BuildError(msg);
throw BuildError(
!derivationType.isSandboxed() || diskFull ? BuildResult::TransientFailure
: BuildResult::PermanentFailure,
msg);
}
/* Compute the FS closure of the outputs and register them as
@ -509,12 +518,7 @@ std::variant<std::pair<BuildResult::Status, Error>, SingleDrvOutputs> Derivation
return std::move(builtOutputs);
} catch (BuildError & e) {
BuildResult::Status st = dynamic_cast<NotDeterministic *>(&e) ? BuildResult::NotDeterministic
: statusOk(status) ? BuildResult::OutputRejected
: !derivationType.isSandboxed() || diskFull ? BuildResult::TransientFailure
: BuildResult::PermanentFailure;
return std::pair{std::move(st), std::move(e)};
return std::move(e);
}
}
@ -682,7 +686,7 @@ void DerivationBuilderImpl::startBuilder()
fmt("\nNote: run `%s` to run programs for x86_64-darwin",
Magenta("/usr/sbin/softwareupdate --install-rosetta && launchctl stop org.nixos.nix-daemon"));
throw BuildError(msg);
throw BuildError(BuildResult::InputRejected, msg);
}
auto buildDir = store.config->getBuildDir();
@ -1378,6 +1382,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs()
auto optSt = maybeLstat(actualPath.c_str());
if (!optSt)
throw BuildError(
BuildResult::OutputRejected,
"builder for '%s' failed to produce output path for output '%s' at '%s'",
store.printStorePath(drvPath),
outputName,
@ -1392,6 +1397,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs()
if ((!S_ISLNK(st.st_mode) && (st.st_mode & (S_IWGRP | S_IWOTH)))
|| (buildUser && st.st_uid != buildUser->getUID()))
throw BuildError(
BuildResult::OutputRejected,
"suspicious ownership or permission on '%s' for output '%s'; rejecting this build output",
actualPath,
outputName);
@ -1428,7 +1434,11 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs()
{[&](const std::string & name) {
auto orifu = get(outputReferencesIfUnregistered, name);
if (!orifu)
throw BuildError("no output reference for '%s' in build of '%s'", name, store.printStorePath(drvPath));
throw BuildError(
BuildResult::OutputRejected,
"no output reference for '%s' in build of '%s'",
name,
store.printStorePath(drvPath));
return std::visit(
overloaded{
/* Since we'll use the already installed versions of these, we
@ -1450,6 +1460,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs()
{[&](const std::string & path, const std::string & parent) {
// TODO with more -vvvv also show the temporary paths for manual inspection.
return BuildError(
BuildResult::OutputRejected,
"cycle detected in build of '%s' in the references of output '%s' from output '%s'",
store.printStorePath(drvPath),
path,
@ -1543,11 +1554,12 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs()
auto newInfoFromCA = [&](const DerivationOutput::CAFloating outputHash) -> ValidPathInfo {
auto st = get(outputStats, outputName);
if (!st)
throw BuildError("output path %1% without valid stats info", actualPath);
throw BuildError(BuildResult::OutputRejected, "output path %1% without valid stats info", actualPath);
if (outputHash.method.getFileIngestionMethod() == FileIngestionMethod::Flat) {
/* The output path should be a regular file without execute permission. */
if (!S_ISREG(st->st_mode) || (st->st_mode & S_IXUSR) != 0)
throw BuildError(
BuildResult::OutputRejected,
"output path '%1%' should be a non-executable regular file "
"since recursive hashing is not enabled (one of outputHashMode={flat,text} is true)",
actualPath);
@ -1649,6 +1661,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs()
valid. */
miscMethods->noteHashMismatch();
delayedException = std::make_exception_ptr(BuildError(
BuildResult::OutputRejected,
"hash mismatch in fixed-output derivation '%s':\n specified: %s\n got: %s",
store.printStorePath(drvPath),
wanted.to_string(HashFormat::SRI, true),
@ -1657,6 +1670,7 @@ SingleDrvOutputs DerivationBuilderImpl::registerOutputs()
if (!newInfo0.references.empty()) {
auto numViolations = newInfo.references.size();
delayedException = std::make_exception_ptr(BuildError(
BuildResult::OutputRejected,
"fixed-output derivations must not reference store paths: '%s' references %d distinct paths, e.g. '%s'",
store.printStorePath(drvPath),
numViolations,

View file

@ -659,7 +659,7 @@ struct ChrootLinuxDerivationBuilder : ChrootDerivationBuilder, LinuxDerivationBu
throw SysError("setuid failed");
}
std::variant<std::pair<BuildResult::Status, Error>, SingleDrvOutputs> unprepareBuild() override
std::variant<BuildError, SingleDrvOutputs> unprepareBuild() override
{
sandboxMountNamespace = -1;
sandboxUserNamespace = -1;

View file

@ -9,4 +9,4 @@ expected=100
if [[ -v NIX_DAEMON_PACKAGE ]]; then expected=1; fi # work around the daemon not returning a 100 status correctly
expectStderr "$expected" nix-build ./text-hashed-output.nix -A failingWrapper --no-out-link \
| grepQuiet "build of '.*use-dynamic-drv-in-non-dynamic-drv-wrong.drv' failed"
| grepQuiet "build of resolved derivation '.*use-dynamic-drv-in-non-dynamic-drv-wrong.drv' failed"