Compare commits

...

2 Commits

Author SHA1 Message Date
John Ericson
97d29276b9 WIP 2026-01-28 18:06:36 -05:00
John Ericson
2545b6716a EvalState::coerceToSingleDerivedPath also alow taking "packages"
This relaxes `builtins.outputOf` and also allows it to be used for
`builtins.derivationOf`
2026-01-28 17:47:13 -05:00
7 changed files with 291 additions and 105 deletions

View File

@@ -43,7 +43,7 @@ RC_GTEST_FIXTURE_PROP(
auto * v = state.allocValue();
state.mkOutputString(*v, b, std::nullopt, mockXpSettings);
auto [d, _] = state.coerceToSingleDerivedPathUnchecked(noPos, *v, "", mockXpSettings);
auto d = state.coerceToSingleDerivedPath(noPos, *v, "", mockXpSettings, true);
RC_ASSERT(SingleDerivedPath{b} == d);
}
@@ -57,7 +57,7 @@ RC_GTEST_FIXTURE_PROP(
auto * v = state.allocValue();
state.mkOutputString(*v, b, outPath, mockXpSettings);
auto [d, _] = state.coerceToSingleDerivedPathUnchecked(noPos, *v, "", mockXpSettings);
auto d = state.coerceToSingleDerivedPath(noPos, *v, "", mockXpSettings, true);
RC_ASSERT(SingleDerivedPath{b} == d);
}

View File

@@ -2406,7 +2406,8 @@ BackedStringView EvalState::coerceToString(
std::string_view errorCtx,
bool coerceMore,
bool copyToStore,
bool canonicalizePath)
bool canonicalizePath,
bool strict)
{
auto _level = addCallDepth(pos);
@@ -2433,14 +2434,28 @@ BackedStringView EvalState::coerceToString(
auto maybeString = tryAttrsToString(pos, v, context, coerceMore, copyToStore);
if (maybeString)
return std::move(*maybeString);
auto i = v.attrs()->get(s.outPath);
if (!i) {
auto outPathAttr = v.attrs()->get(s.outPath);
if (!outPathAttr) {
error<TypeError>(
"cannot coerce %1% to a string: %2%", showType(v), ValuePrinter(*this, v, errorPrintOptions))
.withTrace(pos, errorCtx)
.debugThrow();
}
return coerceToString(pos, *i->value, context, errorCtx, coerceMore, copyToStore, canonicalizePath);
try {
auto derivedPath = coerceToSingleDerivedPath(pos, v, errorCtx);
auto result = mkSingleDerivedPathStringRaw(derivedPath);
context.insert(
std::visit([](auto && o) -> NixStringContextElem { return std::move(o); }, std::move(derivedPath)));
return result;
} catch (Error & e) {
if (strict)
throw;
auto info = e.info();
info.msg = HintFmt("in a future version of Nix this will be an error: %s", Uncolored(info.msg.str()));
logWarning(info);
return coerceToString(
pos, *outPathAttr->value, context, errorCtx, coerceMore, copyToStore, canonicalizePath, strict);
}
}
if (v.type() == nExternal) {
@@ -2478,7 +2493,8 @@ BackedStringView EvalState::coerceToString(
"while evaluating one element of the list",
coerceMore,
copyToStore,
canonicalizePath);
canonicalizePath,
strict);
} catch (Error & e) {
e.addTrace(positions[pos], errorCtx);
throw;
@@ -2565,63 +2581,131 @@ EvalState::coerceToStorePath(const PosIdx pos, Value & v, NixStringContext & con
error<EvalError>("path '%1%' is not in the Nix store", path).withTrace(pos, errorCtx).debugThrow();
}
std::pair<SingleDerivedPath, std::string_view> EvalState::coerceToSingleDerivedPathUnchecked(
const PosIdx pos, Value & v, std::string_view errorCtx, const ExperimentalFeatureSettings & xpSettings)
SingleDerivedPath EvalState::coerceToSingleDerivedPath(
const PosIdx pos,
Value & v,
std::string_view errorCtx,
const ExperimentalFeatureSettings & xpSettings,
bool skipStringValidation)
{
NixStringContext context;
auto s = forceString(v, context, pos, errorCtx, xpSettings);
auto csize = context.size();
if (csize != 1)
error<EvalError>("string '%s' has %d entries in its context. It should only have exactly one entry", s, csize)
.withTrace(pos, errorCtx)
.debugThrow();
auto derivedPath = std::visit(
overloaded{
[&](NixStringContextElem::Opaque && o) -> SingleDerivedPath { return std::move(o); },
[&](NixStringContextElem::DrvDeep &&) -> SingleDerivedPath {
error<EvalError>(
"string '%s' has a context which refers to a complete source and binary closure. This is not supported at this time",
s)
.withTrace(pos, errorCtx)
.debugThrow();
},
[&](NixStringContextElem::Built && b) -> SingleDerivedPath { return std::move(b); },
},
((NixStringContextElem &&) *context.begin()).raw);
return {
std::move(derivedPath),
std::move(s),
};
}
forceValue(v, pos);
SingleDerivedPath EvalState::coerceToSingleDerivedPath(const PosIdx pos, Value & v, std::string_view errorCtx)
{
auto [derivedPath, s_] = coerceToSingleDerivedPathUnchecked(pos, v, errorCtx);
auto s = s_;
auto sExpected = mkSingleDerivedPathStringRaw(derivedPath);
if (s != sExpected) {
/* `std::visit` is used here just to provide a more precise
error message. */
std::visit(
switch (v.type()) {
case nAttrs: {
if (!isDerivation(v)) {
error<TypeError>(
"cannot coerce a set that is not a package to a single derived path: %1%",
ValuePrinter(*this, v, errorPrintOptions))
.withTrace(pos, errorCtx)
.debugThrow();
}
auto outPathAttr = v.attrs()->get(s.outPath);
if (!outPathAttr) {
error<TypeError>(
"cannot coerce %1% to a single derived path: no 'outPath' attribute: %2%",
showType(v),
ValuePrinter(*this, v, errorPrintOptions))
.withTrace(pos, errorCtx)
.debugThrow();
}
auto outPathDerivedPath =
coerceToSingleDerivedPath(pos, *outPathAttr->value, errorCtx, xpSettings, skipStringValidation);
auto drvPathAttr = v.attrs()->get(s.drvPath);
if (drvPathAttr) {
auto drvPathDerivedPath =
coerceToSingleDerivedPath(pos, *drvPathAttr->value, errorCtx, xpSettings, skipStringValidation);
std::visit(
overloaded{
[&](const SingleDerivedPath::Built & built) {
if (*built.drvPath != drvPathDerivedPath) {
error<EvalError>(
"relative outPath '%s' does not refer to drvPath '%s'",
outPathDerivedPath.to_string(*store),
drvPathDerivedPath.to_string(*store))
.withTrace(pos, errorCtx)
.debugThrow();
}
},
[&](const SingleDerivedPath::Opaque &) {
error<EvalError>(
"relative outPath '%s' does not refer to drvPath '%s'",
outPathDerivedPath.to_string(*store),
drvPathDerivedPath.to_string(*store))
.withTrace(pos, errorCtx)
.debugThrow();
},
},
outPathDerivedPath.raw());
}
return outPathDerivedPath;
}
case nString: {
auto s = v.string_view();
NixStringContext context;
copyContext(v, context, xpSettings);
auto csize = context.size();
if (csize != 1)
error<EvalError>(
"string '%s' has %d entries in its context. It should only have exactly one entry", s, csize)
.withTrace(pos, errorCtx)
.debugThrow();
auto derivedPath = std::visit(
overloaded{
[&](const SingleDerivedPath::Opaque & o) {
error<EvalError>("path string '%s' has context with the different path '%s'", s, sExpected)
[&](NixStringContextElem::Opaque && o) -> SingleDerivedPath { return std::move(o); },
[&](NixStringContextElem::DrvDeep &&) -> SingleDerivedPath {
error<EvalError>(
"string '%s' has a context which refers to a complete source and binary closure. This is not "
"supported at this time",
s)
.withTrace(pos, errorCtx)
.debugThrow();
},
[&](const SingleDerivedPath::Built & b) {
error<EvalError>(
"string '%s' has context with the output '%s' from derivation '%s', but the string is not the right placeholder for this derivation output. It should be '%s'",
s,
b.output,
b.drvPath->to_string(*store),
sExpected)
.withTrace(pos, errorCtx)
.debugThrow();
}},
derivedPath.raw());
[&](NixStringContextElem::Built && b) -> SingleDerivedPath { return std::move(b); },
},
((NixStringContextElem &&) *context.begin()).raw);
if (!skipStringValidation) {
auto sExpected = mkSingleDerivedPathStringRaw(derivedPath);
if (s != sExpected) {
/* `std::visit` is used here just to provide a more precise
error message. */
std::visit(
overloaded{
[&](const SingleDerivedPath::Opaque & o) {
error<EvalError>("path string '%s' has context with the different path '%s'", s, sExpected)
.withTrace(pos, errorCtx)
.debugThrow();
},
[&](const SingleDerivedPath::Built & b) {
error<EvalError>(
"string '%s' has context with the output '%s' from derivation '%s', but the string is "
"not the right placeholder for this derivation output. It should be '%s'",
s,
b.output,
b.drvPath->to_string(*store),
sExpected)
.withTrace(pos, errorCtx)
.debugThrow();
}},
derivedPath.raw());
}
}
return derivedPath;
}
default:
error<TypeError>(
"cannot coerce %1% to a single derived path: %2%", showType(v), ValuePrinter(*this, v, errorPrintOptions))
.withTrace(pos, errorCtx)
.debugThrow();
}
return derivedPath;
}
// NOTE: This implementation must match eqValues!

View File

@@ -1,5 +1,6 @@
#include "nix/expr/get-drvs.hh"
#include "nix/expr/eval-inline.hh"
#include "nix/store/derived-path.hh"
#include "nix/store/derivations.hh"
#include "nix/store/store-api.hh"
#include "nix/store/path-with-outputs.hh"
@@ -21,13 +22,13 @@ PackageInfo::PackageInfo(EvalState & state, ref<Store> store, const std::string
, attrs(nullptr)
, attrPath("")
{
auto [drvPath, selectedOutputs] = parsePathWithOutputs(*store, drvPathWithOutputs);
auto [drvStorePath, selectedOutputs] = parsePathWithOutputs(*store, drvPathWithOutputs);
this->drvPath = drvPath;
this->drvPath = {{Path{SingleDerivedPath::Opaque{drvStorePath}, drvStorePath}}};
auto drv = store->derivationFromPath(drvPath);
auto drv = store->derivationFromPath(drvStorePath);
name = drvPath.name();
name = drvStorePath.name();
if (selectedOutputs.size() > 1)
throw Error("building more than one derivation output is not supported, in '%s'", drvPathWithOutputs);
@@ -36,10 +37,18 @@ PackageInfo::PackageInfo(EvalState & state, ref<Store> store, const std::string
auto i = drv.outputs.find(outputName);
if (i == drv.outputs.end())
throw Error("derivation '%s' does not have output '%s'", store->printStorePath(drvPath), outputName);
auto & [outputName, output] = *i;
throw Error("derivation '%s' does not have output '%s'", store->printStorePath(drvStorePath), outputName);
auto & [outputName_, output] = *i;
outPath = {output.path(*store, drv.name, outputName)};
auto outStorePath = output.path(*store, drv.name, outputName_);
if (outStorePath) {
outPath = Path{SingleDerivedPath::Opaque{*outStorePath}, *outStorePath};
} else {
// CA derivation with unknown output path
outPath = Path{
SingleDerivedPath::Built{makeConstantStorePathRef(drvStorePath), outputName_},
std::nullopt};
}
}
std::string PackageInfo::queryName() const
@@ -64,26 +73,60 @@ std::string PackageInfo::querySystem() const
return system;
}
std::optional<StorePath> PackageInfo::queryDrvPath() const
std::optional<PackageInfo::Path> PackageInfo::queryDrvPathFlexible() const
{
if (!drvPath && attrs) {
if (auto i = attrs->get(state->s.drvPath)) {
NixStringContext context;
auto found = state->coerceToStorePath(
i->pos, *i->value, context, "while evaluating the 'drvPath' attribute of a derivation");
auto i = attrs->get(state->s.drvPath);
if (i) {
Value v;
v.mkAttrs(const_cast<Bindings *>(attrs));
std::optional<SingleDerivedPath> outPathDerivedPath;
try {
found.requireDerivation();
// Validate derivation structure
outPathDerivedPath = state->coerceToSingleDerivedPath(
noPos, v, "while evaluating the derivation");
} catch (Error & e) {
e.addTrace(state->positions[i->pos], "while evaluating the 'drvPath' attribute of a derivation");
throw;
auto info = e.info();
info.msg = HintFmt("in a future version of Nix this will be an error: %s", Uncolored(info.msg.str()));
logWarning(info);
}
if (outPathDerivedPath) {
// Validation passed. Now get drvPath.
auto drvPathDerivedPath = state->coerceToSingleDerivedPath(
i->pos, *i->value, "while evaluating the 'drvPath' attribute of a derivation");
if (auto * opaque = std::get_if<SingleDerivedPath::Opaque>(&drvPathDerivedPath.raw())) {
opaque->path.requireDerivation();
drvPath = {{Path{std::move(drvPathDerivedPath), opaque->path}}};
} else {
// Dynamic derivation - drvPath is itself a derivation output
drvPath = {{Path{std::move(drvPathDerivedPath), std::nullopt}}};
}
} else {
// Fall back to old behavior
NixStringContext context;
auto found = state->coerceToStorePath(
i->pos, *i->value, context, "while evaluating the 'drvPath' attribute of a derivation");
try {
found.requireDerivation();
} catch (Error & e) {
e.addTrace(state->positions[i->pos], "while evaluating the 'drvPath' attribute of a derivation");
throw;
}
drvPath = {{Path{SingleDerivedPath::Opaque{found}, std::move(found)}}};
}
drvPath = {std::move(found)};
} else
drvPath = {std::nullopt};
}
return drvPath.value_or(std::nullopt);
}
std::optional<StorePath> PackageInfo::queryDrvPath() const
{
if (auto path = queryDrvPathFlexible())
return path->storePath;
return std::nullopt;
}
StorePath PackageInfo::requireDrvPath() const
{
if (auto drvPath = queryDrvPath())
@@ -91,20 +134,51 @@ StorePath PackageInfo::requireDrvPath() const
throw Error("derivation does not contain a 'drvPath' attribute");
}
StorePath PackageInfo::queryOutPath() const
PackageInfo::Path PackageInfo::queryOutPathFlexible() const
{
if (!outPath && attrs) {
auto i = attrs->get(state->s.outPath);
NixStringContext context;
if (i)
outPath = state->coerceToStorePath(
i->pos, *i->value, context, "while evaluating the output path of a derivation");
Value v;
v.mkAttrs(const_cast<Bindings *>(attrs));
std::optional<SingleDerivedPath> derivedPath;
try {
derivedPath = state->coerceToSingleDerivedPath(
noPos, v, "while evaluating the derivation");
} catch (Error & e) {
auto info = e.info();
info.msg = HintFmt("in a future version of Nix this will be an error: %s", Uncolored(info.msg.str()));
logWarning(info);
}
if (derivedPath) {
if (auto * opaque = std::get_if<SingleDerivedPath::Opaque>(&derivedPath->raw())) {
outPath = Path{std::move(*derivedPath), opaque->path};
} else {
// Built path - no concrete output path available (placeholder)
outPath = Path{std::move(*derivedPath), std::nullopt};
}
} else {
// Fall back to old behavior
auto i = attrs->get(state->s.outPath);
NixStringContext context;
if (i) {
auto path = state->coerceToStorePath(
i->pos, *i->value, context, "while evaluating the output path of a derivation");
outPath = Path{SingleDerivedPath::Opaque{path}, std::move(path)};
}
}
}
if (!outPath)
throw UnimplementedError("CA derivations are not yet supported");
throw Error("derivation does not have an 'outPath' attribute");
return *outPath;
}
StorePath PackageInfo::queryOutPath() const
{
auto path = queryOutPathFlexible();
if (!path.storePath)
throw UnimplementedError("CA derivations are not yet supported");
return *path.storePath;
}
PackageInfo::Outputs PackageInfo::queryOutputs(bool withPaths, bool onlyOutputsToInstall)
{
if (outputs.empty()) {

View File

@@ -715,6 +715,9 @@ public:
* string. If `coerceMore` is set, also converts nulls, integers,
* booleans and lists to a string. If `copyToStore` is set,
* referenced paths are copied to the Nix store as a side effect.
* If `strict` is set, ensures that when coercing an attrset with
* both `outPath` and `drvPath`, the string context for `outPath`
* agrees with the `drvPath` attribute.
*/
BackedStringView coerceToString(
const PosIdx pos,
@@ -723,7 +726,8 @@ public:
std::string_view errorCtx,
bool coerceMore = false,
bool copyToStore = true,
bool canonicalizePath = true);
bool canonicalizePath = true,
bool strict = false);
StorePath copyPathToStore(NixStringContext & context, const SourcePath & path);
@@ -742,30 +746,33 @@ public:
StorePath coerceToStorePath(const PosIdx pos, Value & v, NixStringContext & context, std::string_view errorCtx);
/**
* Part of `coerceToSingleDerivedPath()` without any store IO which is exposed for unit testing only.
* Coerce to `SingleDerivedPath`.
*
* For strings: must be either a literal store path or a
* placeholder (see `DownstreamPlaceholder`). The string context
* must be exactly one element, which is either a
* `NixStringContextElem::Opaque` or `NixStringContextElem::Built`.
* (`NixStringContextElem::DrvDeep` is not permitted). The string
* is parsed based on the context --- the context is the source of
* truth, and ultimately tells us what we want, and then we ensure
* the string corresponds to it.
*
* For attrsets: must have an `outPath` attribute which is
* recursively coerced. If a `drvPath` attribute is present, it is
* also coerced and validated to agree with the `outPath` context.
*
* @param xpSettings Stop-gap to avoid globals during unit tests.
*
* @param skipStringValidation Skip validating that the string
* matches the expected placeholder for the context. This is
* exposed for unit testing only to avoid store IO.
*/
std::pair<SingleDerivedPath, std::string_view> coerceToSingleDerivedPathUnchecked(
SingleDerivedPath coerceToSingleDerivedPath(
const PosIdx pos,
Value & v,
std::string_view errorCtx,
const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings);
/**
* Coerce to `SingleDerivedPath`.
*
* Must be a string which is either a literal store path or a
* "placeholder (see `DownstreamPlaceholder`).
*
* Even more importantly, the string context must be exactly one
* element, which is either a `NixStringContextElem::Opaque` or
* `NixStringContextElem::Built`. (`NixStringContextEleme::DrvDeep`
* is not permitted).
*
* The string is parsed based on the context --- the context is the
* source of truth, and ultimately tells us what we want, and then
* we ensure the string corresponds to it.
*/
SingleDerivedPath coerceToSingleDerivedPath(const PosIdx pos, Value & v, std::string_view errorCtx);
const ExperimentalFeatureSettings & xpSettings = experimentalFeatureSettings,
bool skipStringValidation = false);
#if NIX_USE_BOEHMGC
/** A GC root for the baseEnv reference. */

View File

@@ -2,6 +2,7 @@
///@file
#include "nix/expr/eval.hh"
#include "nix/store/derived-path.hh"
#include "nix/store/path.hh"
#include <string>
@@ -20,10 +21,23 @@ public:
private:
EvalState * state;
struct Path {
/**
* Underlying semantic path, valid in CA and IA cases.
*/
SingleDerivedPath derivedPath;
/**
* If we don't need a placeholder, we'll have a real store path, which we can remember here.
* For CA derivations using placeholders, this will be std::nullopt.
*/
std::optional<StorePath> storePath;
};
mutable std::string name;
mutable std::string system;
mutable std::optional<std::optional<StorePath>> drvPath;
mutable std::optional<StorePath> outPath;
mutable std::optional<std::optional<Path>> drvPath;
mutable std::optional<Path> outPath;
mutable std::string outputName;
Outputs outputs;
@@ -52,8 +66,10 @@ public:
std::string queryName() const;
std::string querySystem() const;
std::optional<StorePath> queryDrvPath() const;
std::optional<Path> queryDrvPathFlexible() const;
StorePath requireDrvPath() const;
StorePath queryOutPath() const;
Path queryOutPathFlexible() const;
std::string queryOutputName() const;
/**
* Return the unordered map of output names to (optional) output paths.
@@ -81,12 +97,12 @@ public:
void setDrvPath(StorePath path)
{
drvPath = {{std::move(path)}};
drvPath = {{Path{SingleDerivedPath::Opaque{path}, std::move(path)}}};
}
void setOutPath(StorePath path)
{
outPath = {{std::move(path)}};
outPath = Path{SingleDerivedPath::Opaque{path}, std::move(path)};
}
void setFailed()

View File

@@ -0,0 +1,4 @@
warning:
… while evaluating the first argument passed to builtins.toPath
warning: in a future version of Nix this will be an error: cannot coerce a set that is not a package to a single derived path: { outPath = «thunk»; }

View File

@@ -0,0 +1 @@
warning: in a future version of Nix this will be an error: derivation '/nix/store/y1s2fiq89v2h9vkb38w508ir20dwv6v2-test.drv' is not valid