Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ src = [
'buffered-io.cc',
'constituents.cc',
'worker.cc',
'warning-capture.cc',
'strings-portable.cc',
'output-stream-lock.cc'
]
Expand Down
132 changes: 132 additions & 0 deletions src/warning-capture.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#include "warning-capture.hh"
#include <nix/util/error.hh>
#include <nix/util/logging.hh>
// NOLINTNEXTLINE(misc-include-cleaner)
#include <nix/util/position.hh>
#include <nix/util/terminal.hh>
#include <nlohmann/json.hpp>
#include <nlohmann/json_fwd.hpp>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <utility>

namespace nix_eval_jobs {

WarningCapturingLogger::WarningCapturingLogger(
std::unique_ptr<nix::Logger> delegate)
: delegate(std::move(delegate)) {}

void WarningCapturingLogger::stop() { delegate->stop(); }

void WarningCapturingLogger::pause() { delegate->pause(); }

void WarningCapturingLogger::resume() { delegate->resume(); }

auto WarningCapturingLogger::isVerbose() -> bool {
return delegate->isVerbose();
}

void WarningCapturingLogger::log(nix::Verbosity lvl, std::string_view msg) {
delegate->log(lvl, msg);
}

void WarningCapturingLogger::logEI(const nix::ErrorInfo &errInfo) {
// Capture warnings from builtins.warn
if (errInfo.level == nix::lvlWarn) {
nlohmann::json warning;
warning["msg"] = errInfo.msg.str();

{
const std::scoped_lock lock(mutex);
warnings.push_back(std::move(warning));
}
}

// Always delegate to original logger
delegate->logEI(errInfo);
}

void WarningCapturingLogger::warn(const std::string &msg) {
delegate->warn(msg);
}

void WarningCapturingLogger::startActivity(
nix::ActivityId act, nix::Verbosity lvl, nix::ActivityType type,
const std::string &msg, const Fields &fields, nix::ActivityId parent) {
delegate->startActivity(act, lvl, type, msg, fields, parent);
}

void WarningCapturingLogger::stopActivity(nix::ActivityId act) {
delegate->stopActivity(act);
}

void WarningCapturingLogger::result(nix::ActivityId act, nix::ResultType type,
const Fields &fields) {
delegate->result(act, type, fields);
}

void WarningCapturingLogger::writeToStdout(std::string_view msg) {
delegate->writeToStdout(msg);
}

auto WarningCapturingLogger::ask(std::string_view msg) -> std::optional<char> {
return delegate->ask(msg);
}

void WarningCapturingLogger::setPrintBuildLogs(bool printBuildLogs) {
delegate->setPrintBuildLogs(printBuildLogs);
}

auto WarningCapturingLogger::takeWarnings() -> nlohmann::json {
const std::scoped_lock lock(mutex);
auto result = nlohmann::json(warnings);
warnings.clear();
return result;
}

void WarningCapturingLogger::attachTracesToLastWarning(
const nix::ErrorInfo &errInfo) {
if (errInfo.traces.empty()) {
return;
}

const std::scoped_lock lock(mutex);
if (warnings.empty()) {
return;
}

auto &lastWarning = warnings.back();
// Only add trace if this warning doesn't have one yet
if (lastWarning.contains("trace")) {
return;
}

auto traces = nlohmann::json::array();
for (const auto &trace : errInfo.traces) {
nlohmann::json traceJson;
traceJson["msg"] = nix::filterANSIEscapes(trace.hint.str(), true);
if (trace.pos && *trace.pos) {
traceJson["line"] = trace.pos->line;
traceJson["column"] = trace.pos->column;
// Extract just the file path from origin
if (auto path = trace.pos->getSourcePath()) {
traceJson["file"] = path->to_string();
}
}
traces.push_back(std::move(traceJson));
}
lastWarning["trace"] = std::move(traces);
}

auto installWarningCapturingLogger() -> WarningCapturingLogger * {
auto capturingLogger =
std::make_unique<WarningCapturingLogger>(std::move(nix::logger));
auto *ptr = capturingLogger.get();
nix::logger = std::move(capturingLogger);
return ptr;
}

} // namespace nix_eval_jobs
68 changes: 68 additions & 0 deletions src/warning-capture.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#pragma once

#include <nix/util/error.hh>
#include <nix/util/logging.hh>
#include <nlohmann/json_fwd.hpp>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

namespace nix_eval_jobs {

/**
* A logger that captures evaluation warnings while delegating
* all other logging to the original logger.
*/
class WarningCapturingLogger : public nix::Logger {
public:
explicit WarningCapturingLogger(std::unique_ptr<nix::Logger> delegate);

void stop() override;
void pause() override;
void resume() override;
auto isVerbose() -> bool override;

void log(nix::Verbosity lvl, std::string_view msg) override;
void logEI(const nix::ErrorInfo &errInfo) override;
void warn(const std::string &msg) override;

void startActivity(nix::ActivityId act, nix::Verbosity lvl,
nix::ActivityType type, const std::string &msg,
const Fields &fields, nix::ActivityId parent) override;
void stopActivity(nix::ActivityId act) override;
void result(nix::ActivityId act, nix::ResultType type,
const Fields &fields) override;
void writeToStdout(std::string_view msg) override;
auto ask(std::string_view msg) -> std::optional<char> override;
void setPrintBuildLogs(bool printBuildLogs) override;

/**
* Clear all captured warnings and return them.
* Thread-safe.
*/
auto takeWarnings() -> nlohmann::json;

/**
* Attach trace information from a caught error to the last warning.
* Used when abort-on-warn is set: the error following a warning
* contains position info in its traces.
* Thread-safe.
*/
void attachTracesToLastWarning(const nix::ErrorInfo &errInfo);

private:
std::unique_ptr<nix::Logger> delegate;
std::mutex mutex;
std::vector<nlohmann::json> warnings;
};

/**
* Install a warning-capturing logger as the global nix::logger.
* Returns a pointer to the installed logger for later retrieval of warnings.
*/
auto installWarningCapturingLogger() -> WarningCapturingLogger *;

} // namespace nix_eval_jobs
38 changes: 33 additions & 5 deletions src/worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
// NOLINTEND(modernize-deprecated-headers)
#include <exception>
#include <filesystem>
#include <typeinfo>
#include <nix/expr/attr-set.hh>
#include <nix/cmd/common-eval-args.hh>
#include <nix/util/error.hh>
Expand Down Expand Up @@ -50,6 +51,7 @@
#include "buffered-io.hh"
#include "eval-args.hh"
#include "store.hh"
#include "warning-capture.hh"

namespace nix {
struct Expr;
Expand Down Expand Up @@ -313,7 +315,9 @@ auto shouldRestart(const MyArgs &args) -> bool {

auto processJobRequest(nix::EvalState &state, LineReader &fromReader,
nix::AutoCloseFD &toParent, nix::Bindings &autoArgs,
nix::Value *vRoot, MyArgs &args) -> bool {
nix::Value *vRoot, MyArgs &args,
nix_eval_jobs::WarningCapturingLogger *warningLogger)
-> bool {
/* Wait for the collector to send us a job name. */
if (tryWriteLine(toParent.get(), "next") < 0) {
return false; // main process died
Expand All @@ -337,6 +341,11 @@ auto processJobRequest(nix::EvalState &state, LineReader &fromReader,
nlohmann::json reply =
nlohmann::json{{"attr", attrPathS}, {"attrPath", path}};

// Clear any warnings from previous evaluation
if (warningLogger != nullptr) {
warningLogger->takeWarnings();
}

try {
auto *vTmp =
nix::findAlongAttrPath(state, attrPathS, autoArgs, *vRoot).first;
Expand All @@ -350,8 +359,17 @@ auto processJobRequest(nix::EvalState &state, LineReader &fromReader,
// We ignore everything that cannot be built
reply["attrs"] = nlohmann::json::array();
}
} catch (nix::EvalError &e) {
} catch (nix::Error &e) {
const auto &err = e.info();

// When abort-on-warn is set, EvalBaseError is thrown (not a subclass).
// Only attach traces in this case, not for unrelated errors like
// ThrownError.
if (warningLogger != nullptr &&
typeid(e) == typeid(nix::EvalBaseError)) {
warningLogger->attachTracesToLastWarning(err);
}

std::ostringstream oss;
nix::showErrorInfo(oss, err, nix::loggerSettings.showTrace.get());
auto msg = oss.str();
Expand All @@ -361,13 +379,20 @@ auto processJobRequest(nix::EvalState &state, LineReader &fromReader,
// Print to STDERR for Hydra UI
std::cerr << msg << "\n";
} catch (const std::exception &e) {
// FIXME: for some reason the catch block above doesn't trigger on macOS
// (?)
// Fallback for non-nix exceptions
const auto *msg = e.what();
reply["error"] = nix::filterANSIEscapes(msg, true);
std::cerr << msg << '\n';
}

// Capture any warnings that were emitted during evaluation
if (warningLogger != nullptr) {
auto warnings = warningLogger->takeWarnings();
if (!warnings.empty()) {
reply["warnings"] = std::move(warnings);
}
}

if (tryWriteLine(toParent.get(), reply.dump()) < 0) {
return false; // main process died
}
Expand All @@ -383,6 +408,9 @@ void worker(
nix::AutoCloseFD &toParent, // NOLINT(bugprone-easily-swappable-parameters)
nix::AutoCloseFD &fromParent) {

// Install warning-capturing logger to collect evaluation warnings
auto *warningLogger = nix_eval_jobs::installWarningCapturingLogger();

auto evalStore = nix_eval_jobs::openStore(args.evalStoreUrl);
auto state = nix::make_ref<nix::EvalState>(
args.lookupPath, evalStore, nix::fetchSettings, nix::evalSettings);
Expand All @@ -393,7 +421,7 @@ void worker(
LineReader fromReader(fromParent.release());

while (processJobRequest(*state, fromReader, toParent, autoArgs, vRoot,
args)) {
args, warningLogger)) {
// Continue processing jobs until we need to exit
}

Expand Down
26 changes: 26 additions & 0 deletions tests/assets/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,32 @@
hydraJobs = import ./ci.nix { inherit system; };

legacyPackages.x86_64-linux = {
warningPkgs = {
# Test package that emits a warning during evaluation
packageWithWarning = builtins.warn "this is a test warning" (derivation {
name = "package-with-warning";
inherit system;
builder = "/bin/sh";
args = [
"-c"
"echo 'content' > $out"
];
});
# Package with multiple warnings
packageWithMultipleWarnings = builtins.warn "first warning" (
builtins.warn "second warning" (derivation {
name = "package-with-multiple-warnings";
inherit system;
builder = "/bin/sh";
args = [
"-c"
"echo 'content' > $out"
];
})
);
# Package with warning followed by unrelated error
warningThenError = builtins.warn "warning before error" (throw "unrelated error after warning");
};
emptyNeeded = rec {
# This is a reproducer for issue #369 where neededBuilds and neededSubstitutes are empty
# when they should contain values
Expand Down
Loading