Split OutputsSpec and ExtendedOutputsSpec, use the former more

`DerivedPath::Built` and `DerivationGoal` were previously using a
regular set with the convention that the empty set means all outputs.
But it is easy to forget about this rule when processing those sets.
Using `OutputSpec` forces us to get it right.
This commit is contained in:
John Ericson 2023-01-11 18:57:18 -05:00
parent a7c0cff07f
commit ce2f91d356
23 changed files with 377 additions and 193 deletions

View file

@ -488,18 +488,23 @@ struct InstallableAttrPath : InstallableValue
if (!drvPath) if (!drvPath)
throw Error("'%s' is not a derivation", what()); throw Error("'%s' is not a derivation", what());
auto derivedPath = byDrvPath.emplace(*drvPath, DerivedPath::Built {
.drvPath = *drvPath,
// Not normally legal, but we will merge right below
.outputs = OutputsSpec::Names { },
}).first;
derivedPath->second.outputs.merge(std::visit(overloaded {
[&](const ExtendedOutputsSpec::Default & d) -> OutputsSpec {
std::set<std::string> outputsToInstall; std::set<std::string> outputsToInstall;
for (auto & output : drvInfo.queryOutputs(false, true))
if (auto outputNames = std::get_if<OutputNames>(&extendedOutputsSpec))
outputsToInstall = *outputNames;
else
for (auto & output : drvInfo.queryOutputs(false, std::get_if<DefaultOutputs>(&extendedOutputsSpec)))
outputsToInstall.insert(output.first); outputsToInstall.insert(output.first);
return OutputsSpec::Names { std::move(outputsToInstall) };
auto derivedPath = byDrvPath.emplace(*drvPath, DerivedPath::Built { .drvPath = *drvPath }).first; },
[&](const ExtendedOutputsSpec::Explicit & e) -> OutputsSpec {
for (auto & output : outputsToInstall) return e;
derivedPath->second.outputs.insert(output); },
}, extendedOutputsSpec.raw()));
} }
DerivedPathsWithInfo res; DerivedPathsWithInfo res;
@ -639,41 +644,40 @@ DerivedPathsWithInfo InstallableFlake::toDerivedPaths()
auto drvPath = attr->forceDerivation(); auto drvPath = attr->forceDerivation();
std::set<std::string> outputsToInstall;
std::optional<NixInt> priority; std::optional<NixInt> priority;
if (attr->maybeGetAttr(state->sOutputSpecified)) {
} else if (auto aMeta = attr->maybeGetAttr(state->sMeta)) {
if (auto aPriority = aMeta->maybeGetAttr("priority"))
priority = aPriority->getInt();
}
return {{
.path = DerivedPath::Built {
.drvPath = std::move(drvPath),
.outputs = std::visit(overloaded {
[&](const ExtendedOutputsSpec::Default & d) -> OutputsSpec {
std::set<std::string> outputsToInstall;
if (auto aOutputSpecified = attr->maybeGetAttr(state->sOutputSpecified)) { if (auto aOutputSpecified = attr->maybeGetAttr(state->sOutputSpecified)) {
if (aOutputSpecified->getBool()) { if (aOutputSpecified->getBool()) {
if (auto aOutputName = attr->maybeGetAttr("outputName")) if (auto aOutputName = attr->maybeGetAttr("outputName"))
outputsToInstall = { aOutputName->getString() }; outputsToInstall = { aOutputName->getString() };
} }
} } else if (auto aMeta = attr->maybeGetAttr(state->sMeta)) {
else if (auto aMeta = attr->maybeGetAttr(state->sMeta)) {
if (auto aOutputsToInstall = aMeta->maybeGetAttr("outputsToInstall")) if (auto aOutputsToInstall = aMeta->maybeGetAttr("outputsToInstall"))
for (auto & s : aOutputsToInstall->getListOfStrings()) for (auto & s : aOutputsToInstall->getListOfStrings())
outputsToInstall.insert(s); outputsToInstall.insert(s);
if (auto aPriority = aMeta->maybeGetAttr("priority"))
priority = aPriority->getInt();
}
if (outputsToInstall.empty() || std::get_if<AllOutputs>(&extendedOutputsSpec)) {
outputsToInstall.clear();
if (auto aOutputs = attr->maybeGetAttr(state->sOutputs))
for (auto & s : aOutputs->getListOfStrings())
outputsToInstall.insert(s);
} }
if (outputsToInstall.empty()) if (outputsToInstall.empty())
outputsToInstall.insert("out"); outputsToInstall.insert("out");
if (auto outputNames = std::get_if<OutputNames>(&extendedOutputsSpec)) return OutputsSpec::Names { std::move(outputsToInstall) };
outputsToInstall = *outputNames; },
[&](const ExtendedOutputsSpec::Explicit & e) -> OutputsSpec {
return {{ return e;
.path = DerivedPath::Built { },
.drvPath = std::move(drvPath), }, extendedOutputsSpec.raw()),
.outputs = std::move(outputsToInstall),
}, },
.info = { .info = {
.priority = priority, .priority = priority,
@ -801,7 +805,7 @@ std::vector<std::shared_ptr<Installable>> SourceExprCommand::parseInstallables(
result.push_back( result.push_back(
std::make_shared<InstallableAttrPath>( std::make_shared<InstallableAttrPath>(
state, *this, vFile, state, *this, vFile,
prefix == "." ? "" : prefix, prefix == "." ? "" : std::string { prefix },
extendedOutputsSpec)); extendedOutputsSpec));
} }
@ -918,32 +922,7 @@ std::vector<std::pair<std::shared_ptr<Installable>, BuiltPathWithResult>> Instal
for (auto & aux : backmap[path]) { for (auto & aux : backmap[path]) {
std::visit(overloaded { std::visit(overloaded {
[&](const DerivedPath::Built & bfd) { [&](const DerivedPath::Built & bfd) {
OutputPathMap outputs; auto outputs = resolveDerivedPath(*store, bfd, &*evalStore);
auto drv = evalStore->readDerivation(bfd.drvPath);
auto outputHashes = staticOutputHashes(*evalStore, drv); // FIXME: expensive
auto drvOutputs = drv.outputsAndOptPaths(*store);
for (auto & output : bfd.outputs) {
auto outputHash = get(outputHashes, output);
if (!outputHash)
throw Error(
"the derivation '%s' doesn't have an output named '%s'",
store->printStorePath(bfd.drvPath), output);
if (settings.isExperimentalFeatureEnabled(Xp::CaDerivations)) {
DrvOutput outputId { *outputHash, output };
auto realisation = store->queryRealisation(outputId);
if (!realisation)
throw MissingRealisation(outputId);
outputs.insert_or_assign(output, realisation->outPath);
} else {
// If ca-derivations isn't enabled, assume that
// the output path is statically known.
auto drvOutput = get(drvOutputs, output);
assert(drvOutput);
assert(drvOutput->second);
outputs.insert_or_assign(
output, *drvOutput->second);
}
}
res.push_back({aux.installable, { res.push_back({aux.installable, {
.path = BuiltPath::Built { bfd.drvPath, outputs }, .path = BuiltPath::Built { bfd.drvPath, outputs },
.info = aux.info}}); .info = aux.info}});

View file

@ -245,7 +245,7 @@ std::tuple<FlakeRef, std::string, ExtendedOutputsSpec> parseFlakeRefWithFragment
bool isFlake) bool isFlake)
{ {
auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse(url); auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse(url);
auto [flakeRef, fragment] = parseFlakeRefWithFragment(prefix, baseDir, allowMissing, isFlake); auto [flakeRef, fragment] = parseFlakeRefWithFragment(std::string { prefix }, baseDir, allowMissing, isFlake);
return {std::move(flakeRef), fragment, extendedOutputsSpec}; return {std::move(flakeRef), fragment, extendedOutputsSpec};
} }

View file

@ -53,7 +53,7 @@ StringMap EvalState::realiseContext(const PathSet & context)
[&](const NixStringContextElem::Built & b) { [&](const NixStringContextElem::Built & b) {
drvs.push_back(DerivedPath::Built { drvs.push_back(DerivedPath::Built {
.drvPath = b.drvPath, .drvPath = b.drvPath,
.outputs = std::set { b.output }, .outputs = OutputsSpec::Names { b.output },
}); });
ensureValid(b.drvPath); ensureValid(b.drvPath);
}, },
@ -84,16 +84,12 @@ StringMap EvalState::realiseContext(const PathSet & context)
store->buildPaths(buildReqs); store->buildPaths(buildReqs);
/* Get all the output paths corresponding to the placeholders we had */ /* Get all the output paths corresponding to the placeholders we had */
for (auto & [drvPath, outputs] : drvs) { for (auto & drv : drvs) {
const auto outputPaths = store->queryDerivationOutputMap(drvPath); auto outputs = resolveDerivedPath(*store, drv);
for (auto & outputName : outputs) { for (auto & [outputName, outputPath] : outputs) {
auto outputPath = get(outputPaths, outputName);
if (!outputPath)
debugThrowLastTrace(Error("derivation '%s' does not have an output named '%s'",
store->printStorePath(drvPath), outputName));
res.insert_or_assign( res.insert_or_assign(
downstreamPlaceholder(*store, drvPath, outputName), downstreamPlaceholder(*store, drv.drvPath, outputName),
store->printStorePath(*outputPath) store->printStorePath(outputPath)
); );
} }
} }

View file

@ -63,7 +63,7 @@
namespace nix { namespace nix {
DerivationGoal::DerivationGoal(const StorePath & drvPath, DerivationGoal::DerivationGoal(const StorePath & drvPath,
const StringSet & wantedOutputs, Worker & worker, BuildMode buildMode) const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode)
: Goal(worker, DerivedPath::Built { .drvPath = drvPath, .outputs = wantedOutputs }) : Goal(worker, DerivedPath::Built { .drvPath = drvPath, .outputs = wantedOutputs })
, useDerivation(true) , useDerivation(true)
, drvPath(drvPath) , drvPath(drvPath)
@ -82,7 +82,7 @@ DerivationGoal::DerivationGoal(const StorePath & drvPath,
DerivationGoal::DerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, DerivationGoal::DerivationGoal(const StorePath & drvPath, const BasicDerivation & drv,
const StringSet & wantedOutputs, Worker & worker, BuildMode buildMode) const OutputsSpec & wantedOutputs, Worker & worker, BuildMode buildMode)
: Goal(worker, DerivedPath::Built { .drvPath = drvPath, .outputs = wantedOutputs }) : Goal(worker, DerivedPath::Built { .drvPath = drvPath, .outputs = wantedOutputs })
, useDerivation(false) , useDerivation(false)
, drvPath(drvPath) , drvPath(drvPath)
@ -142,17 +142,10 @@ void DerivationGoal::work()
(this->*state)(); (this->*state)();
} }
void DerivationGoal::addWantedOutputs(const StringSet & outputs) void DerivationGoal::addWantedOutputs(const OutputsSpec & outputs)
{ {
/* If we already want all outputs, there is nothing to do. */ bool newOutputs = wantedOutputs.merge(outputs);
if (wantedOutputs.empty()) return; if (newOutputs)
if (outputs.empty()) {
wantedOutputs.clear();
needRestart = true;
} else
for (auto & i : outputs)
if (wantedOutputs.insert(i).second)
needRestart = true; needRestart = true;
} }
@ -390,7 +383,7 @@ void DerivationGoal::repairClosure()
auto outputs = queryDerivationOutputMap(); auto outputs = queryDerivationOutputMap();
StorePathSet outputClosure; StorePathSet outputClosure;
for (auto & i : outputs) { for (auto & i : outputs) {
if (!wantOutput(i.first, wantedOutputs)) continue; if (!wantedOutputs.contains(i.first)) continue;
worker.store.computeFSClosure(i.second, outputClosure); worker.store.computeFSClosure(i.second, outputClosure);
} }
@ -422,7 +415,7 @@ void DerivationGoal::repairClosure()
if (drvPath2 == outputsToDrv.end()) if (drvPath2 == outputsToDrv.end())
addWaitee(upcast_goal(worker.makePathSubstitutionGoal(i, Repair))); addWaitee(upcast_goal(worker.makePathSubstitutionGoal(i, Repair)));
else else
addWaitee(worker.makeDerivationGoal(drvPath2->second, StringSet(), bmRepair)); addWaitee(worker.makeDerivationGoal(drvPath2->second, OutputsSpec::All(), bmRepair));
} }
if (waitees.empty()) { if (waitees.empty()) {
@ -991,10 +984,15 @@ void DerivationGoal::resolvedFinished()
StorePathSet outputPaths; StorePathSet outputPaths;
// `wantedOutputs` might be empty, which means “all the outputs” // `wantedOutputs` might merely indicate “all the outputs”
auto realWantedOutputs = wantedOutputs; auto realWantedOutputs = std::visit(overloaded {
if (realWantedOutputs.empty()) [&](const OutputsSpec::All &) {
realWantedOutputs = resolvedDrv.outputNames(); return resolvedDrv.outputNames();
},
[&](const OutputsSpec::Names & names) {
return names;
},
}, wantedOutputs.raw());
for (auto & wantedOutput : realWantedOutputs) { for (auto & wantedOutput : realWantedOutputs) {
auto initialOutput = get(initialOutputs, wantedOutput); auto initialOutput = get(initialOutputs, wantedOutput);
@ -1322,7 +1320,14 @@ std::pair<bool, DrvOutputs> DerivationGoal::checkPathValidity()
if (!drv->type().isPure()) return { false, {} }; if (!drv->type().isPure()) return { false, {} };
bool checkHash = buildMode == bmRepair; bool checkHash = buildMode == bmRepair;
auto wantedOutputsLeft = wantedOutputs; auto wantedOutputsLeft = std::visit(overloaded {
[&](const OutputsSpec::All &) {
return StringSet {};
},
[&](const OutputsSpec::Names & names) {
return names;
},
}, wantedOutputs.raw());
DrvOutputs validOutputs; DrvOutputs validOutputs;
for (auto & i : queryPartialDerivationOutputMap()) { for (auto & i : queryPartialDerivationOutputMap()) {
@ -1331,7 +1336,7 @@ std::pair<bool, DrvOutputs> DerivationGoal::checkPathValidity()
// this is an invalid output, gets catched with (!wantedOutputsLeft.empty()) // this is an invalid output, gets catched with (!wantedOutputsLeft.empty())
continue; continue;
auto & info = *initialOutput; auto & info = *initialOutput;
info.wanted = wantOutput(i.first, wantedOutputs); info.wanted = wantedOutputs.contains(i.first);
if (info.wanted) if (info.wanted)
wantedOutputsLeft.erase(i.first); wantedOutputsLeft.erase(i.first);
if (i.second) { if (i.second) {
@ -1369,7 +1374,7 @@ std::pair<bool, DrvOutputs> DerivationGoal::checkPathValidity()
validOutputs.emplace(drvOutput, Realisation { drvOutput, info.known->path }); validOutputs.emplace(drvOutput, Realisation { drvOutput, info.known->path });
} }
// If we requested all the outputs via the empty set, we are always fine. // If we requested all the outputs, we are always fine.
// If we requested specific elements, the loop above removes all the valid // If we requested specific elements, the loop above removes all the valid
// ones, so any that are left must be invalid. // ones, so any that are left must be invalid.
if (!wantedOutputsLeft.empty()) if (!wantedOutputsLeft.empty())

View file

@ -2,6 +2,7 @@
#include "parsed-derivations.hh" #include "parsed-derivations.hh"
#include "lock.hh" #include "lock.hh"
#include "outputs-spec.hh"
#include "store-api.hh" #include "store-api.hh"
#include "pathlocks.hh" #include "pathlocks.hh"
#include "goal.hh" #include "goal.hh"
@ -55,7 +56,7 @@ struct DerivationGoal : public Goal
/* The specific outputs that we need to build. Empty means all of /* The specific outputs that we need to build. Empty means all of
them. */ them. */
StringSet wantedOutputs; OutputsSpec wantedOutputs;
/* Mapping from input derivations + output names to actual store /* Mapping from input derivations + output names to actual store
paths. This is filled in by waiteeDone() as each dependency paths. This is filled in by waiteeDone() as each dependency
@ -128,10 +129,10 @@ struct DerivationGoal : public Goal
std::string machineName; std::string machineName;
DerivationGoal(const StorePath & drvPath, DerivationGoal(const StorePath & drvPath,
const StringSet & wantedOutputs, Worker & worker, const OutputsSpec & wantedOutputs, Worker & worker,
BuildMode buildMode = bmNormal); BuildMode buildMode = bmNormal);
DerivationGoal(const StorePath & drvPath, const BasicDerivation & drv, DerivationGoal(const StorePath & drvPath, const BasicDerivation & drv,
const StringSet & wantedOutputs, Worker & worker, const OutputsSpec & wantedOutputs, Worker & worker,
BuildMode buildMode = bmNormal); BuildMode buildMode = bmNormal);
virtual ~DerivationGoal(); virtual ~DerivationGoal();
@ -142,7 +143,7 @@ struct DerivationGoal : public Goal
void work() override; void work() override;
/* Add wanted outputs to an already existing derivation goal. */ /* Add wanted outputs to an already existing derivation goal. */
void addWantedOutputs(const StringSet & outputs); void addWantedOutputs(const OutputsSpec & outputs);
/* The states. */ /* The states. */
void getDerivation(); void getDerivation();

View file

@ -130,7 +130,8 @@ void LocalStore::repairPath(const StorePath & path)
auto info = queryPathInfo(path); auto info = queryPathInfo(path);
if (info->deriver && isValidPath(*info->deriver)) { if (info->deriver && isValidPath(*info->deriver)) {
goals.clear(); goals.clear();
goals.insert(worker.makeDerivationGoal(*info->deriver, StringSet(), bmRepair)); // FIXME: Should just build the specific output we need.
goals.insert(worker.makeDerivationGoal(*info->deriver, OutputsSpec::All { }, bmRepair));
worker.run(goals); worker.run(goals);
} else } else
throw Error(worker.exitStatus(), "cannot repair path '%s'", printStorePath(path)); throw Error(worker.exitStatus(), "cannot repair path '%s'", printStorePath(path));

View file

@ -2735,7 +2735,7 @@ DrvOutputs LocalDerivationGoal::registerOutputs()
signRealisation(thisRealisation); signRealisation(thisRealisation);
worker.store.registerDrvOutput(thisRealisation); worker.store.registerDrvOutput(thisRealisation);
} }
if (wantOutput(outputName, wantedOutputs)) if (wantedOutputs.contains(outputName))
builtOutputs.emplace(thisRealisation.id, thisRealisation); builtOutputs.emplace(thisRealisation.id, thisRealisation);
} }

View file

@ -42,7 +42,7 @@ Worker::~Worker()
std::shared_ptr<DerivationGoal> Worker::makeDerivationGoalCommon( std::shared_ptr<DerivationGoal> Worker::makeDerivationGoalCommon(
const StorePath & drvPath, const StorePath & drvPath,
const StringSet & wantedOutputs, const OutputsSpec & wantedOutputs,
std::function<std::shared_ptr<DerivationGoal>()> mkDrvGoal) std::function<std::shared_ptr<DerivationGoal>()> mkDrvGoal)
{ {
std::weak_ptr<DerivationGoal> & goal_weak = derivationGoals[drvPath]; std::weak_ptr<DerivationGoal> & goal_weak = derivationGoals[drvPath];
@ -59,7 +59,7 @@ std::shared_ptr<DerivationGoal> Worker::makeDerivationGoalCommon(
std::shared_ptr<DerivationGoal> Worker::makeDerivationGoal(const StorePath & drvPath, std::shared_ptr<DerivationGoal> Worker::makeDerivationGoal(const StorePath & drvPath,
const StringSet & wantedOutputs, BuildMode buildMode) const OutputsSpec & wantedOutputs, BuildMode buildMode)
{ {
return makeDerivationGoalCommon(drvPath, wantedOutputs, [&]() -> std::shared_ptr<DerivationGoal> { return makeDerivationGoalCommon(drvPath, wantedOutputs, [&]() -> std::shared_ptr<DerivationGoal> {
return !dynamic_cast<LocalStore *>(&store) return !dynamic_cast<LocalStore *>(&store)
@ -70,7 +70,7 @@ std::shared_ptr<DerivationGoal> Worker::makeDerivationGoal(const StorePath & drv
std::shared_ptr<DerivationGoal> Worker::makeBasicDerivationGoal(const StorePath & drvPath, std::shared_ptr<DerivationGoal> Worker::makeBasicDerivationGoal(const StorePath & drvPath,
const BasicDerivation & drv, const StringSet & wantedOutputs, BuildMode buildMode) const BasicDerivation & drv, const OutputsSpec & wantedOutputs, BuildMode buildMode)
{ {
return makeDerivationGoalCommon(drvPath, wantedOutputs, [&]() -> std::shared_ptr<DerivationGoal> { return makeDerivationGoalCommon(drvPath, wantedOutputs, [&]() -> std::shared_ptr<DerivationGoal> {
return !dynamic_cast<LocalStore *>(&store) return !dynamic_cast<LocalStore *>(&store)

View file

@ -140,15 +140,15 @@ public:
/* derivation goal */ /* derivation goal */
private: private:
std::shared_ptr<DerivationGoal> makeDerivationGoalCommon( std::shared_ptr<DerivationGoal> makeDerivationGoalCommon(
const StorePath & drvPath, const StringSet & wantedOutputs, const StorePath & drvPath, const OutputsSpec & wantedOutputs,
std::function<std::shared_ptr<DerivationGoal>()> mkDrvGoal); std::function<std::shared_ptr<DerivationGoal>()> mkDrvGoal);
public: public:
std::shared_ptr<DerivationGoal> makeDerivationGoal( std::shared_ptr<DerivationGoal> makeDerivationGoal(
const StorePath & drvPath, const StorePath & drvPath,
const StringSet & wantedOutputs, BuildMode buildMode = bmNormal); const OutputsSpec & wantedOutputs, BuildMode buildMode = bmNormal);
std::shared_ptr<DerivationGoal> makeBasicDerivationGoal( std::shared_ptr<DerivationGoal> makeBasicDerivationGoal(
const StorePath & drvPath, const BasicDerivation & drv, const StorePath & drvPath, const BasicDerivation & drv,
const StringSet & wantedOutputs, BuildMode buildMode = bmNormal); const OutputsSpec & wantedOutputs, BuildMode buildMode = bmNormal);
/* substitution goal */ /* substitution goal */
std::shared_ptr<PathSubstitutionGoal> makePathSubstitutionGoal(const StorePath & storePath, RepairFlag repair = NoRepair, std::optional<ContentAddress> ca = std::nullopt); std::shared_ptr<PathSubstitutionGoal> makePathSubstitutionGoal(const StorePath & storePath, RepairFlag repair = NoRepair, std::optional<ContentAddress> ca = std::nullopt);

View file

@ -688,12 +688,6 @@ std::map<std::string, Hash> staticOutputHashes(Store & store, const Derivation &
} }
bool wantOutput(const std::string & output, const std::set<std::string> & wanted)
{
return wanted.empty() || wanted.find(output) != wanted.end();
}
static DerivationOutput readDerivationOutput(Source & in, const Store & store) static DerivationOutput readDerivationOutput(Source & in, const Store & store)
{ {
const auto pathS = readString(in); const auto pathS = readString(in);

View file

@ -294,8 +294,6 @@ typedef std::map<StorePath, DrvHash> DrvHashes;
// FIXME: global, though at least thread-safe. // FIXME: global, though at least thread-safe.
extern Sync<DrvHashes> drvHashes; extern Sync<DrvHashes> drvHashes;
bool wantOutput(const std::string & output, const std::set<std::string> & wanted);
struct Source; struct Source;
struct Sink; struct Sink;

View file

@ -19,11 +19,11 @@ nlohmann::json DerivedPath::Built::toJSON(ref<Store> store) const {
res["drvPath"] = store->printStorePath(drvPath); res["drvPath"] = store->printStorePath(drvPath);
// Fallback for the input-addressed derivation case: We expect to always be // Fallback for the input-addressed derivation case: We expect to always be
// able to print the output paths, so lets do it // able to print the output paths, so lets do it
const auto knownOutputs = store->queryPartialDerivationOutputMap(drvPath); const auto outputMap = store->queryPartialDerivationOutputMap(drvPath);
for (const auto & output : outputs) { for (const auto & [output, outputPathOpt] : outputMap) {
auto knownOutput = get(knownOutputs, output); if (!outputs.contains(output)) continue;
if (knownOutput && *knownOutput) if (outputPathOpt)
res["outputs"][output] = store->printStorePath(**knownOutput); res["outputs"][output] = store->printStorePath(*outputPathOpt);
else else
res["outputs"][output] = nullptr; res["outputs"][output] = nullptr;
} }
@ -63,7 +63,7 @@ std::string DerivedPath::Built::to_string(const Store & store) const
{ {
return store.printStorePath(drvPath) return store.printStorePath(drvPath)
+ "!" + "!"
+ (outputs.empty() ? std::string { "*" } : concatStringsSep(",", outputs)); + outputs.to_string();
} }
std::string DerivedPath::to_string(const Store & store) const std::string DerivedPath::to_string(const Store & store) const
@ -81,15 +81,10 @@ DerivedPath::Opaque DerivedPath::Opaque::parse(const Store & store, std::string_
DerivedPath::Built DerivedPath::Built::parse(const Store & store, std::string_view drvS, std::string_view outputsS) DerivedPath::Built DerivedPath::Built::parse(const Store & store, std::string_view drvS, std::string_view outputsS)
{ {
auto drvPath = store.parseStorePath(drvS); return {
std::set<std::string> outputs; .drvPath = store.parseStorePath(drvS),
if (outputsS != "*") { .outputs = OutputsSpec::parse(outputsS),
outputs = tokenizeString<std::set<std::string>>(outputsS, ","); };
if (outputs.empty())
throw Error(
"Explicit list of wanted outputs '%s' must not be empty. Consider using '*' as a wildcard meaning all outputs if no output in particular is wanted.", outputsS);
}
return {drvPath, outputs};
} }
DerivedPath DerivedPath::parse(const Store & store, std::string_view s) DerivedPath DerivedPath::parse(const Store & store, std::string_view s)

View file

@ -3,6 +3,7 @@
#include "util.hh" #include "util.hh"
#include "path.hh" #include "path.hh"
#include "realisation.hh" #include "realisation.hh"
#include "outputs-spec.hh"
#include <optional> #include <optional>
@ -44,7 +45,7 @@ struct DerivedPathOpaque {
*/ */
struct DerivedPathBuilt { struct DerivedPathBuilt {
StorePath drvPath; StorePath drvPath;
std::set<std::string> outputs; OutputsSpec outputs;
std::string to_string(const Store & store) const; std::string to_string(const Store & store) const;
static DerivedPathBuilt parse(const Store & store, std::string_view, std::string_view); static DerivedPathBuilt parse(const Store & store, std::string_view, std::string_view);

View file

@ -185,7 +185,7 @@ void Store::queryMissing(const std::vector<DerivedPath> & targets,
knownOutputPaths = false; knownOutputPaths = false;
break; break;
} }
if (wantOutput(outputName, bfd.outputs) && !isValidPath(*pathOpt)) if (bfd.outputs.contains(outputName) && !isValidPath(*pathOpt))
invalid.insert(*pathOpt); invalid.insert(*pathOpt);
} }
if (knownOutputPaths && invalid.empty()) return; if (knownOutputPaths && invalid.empty()) return;
@ -301,4 +301,47 @@ std::map<DrvOutput, StorePath> drvOutputReferences(
return drvOutputReferences(Realisation::closure(store, inputRealisations), info->references); return drvOutputReferences(Realisation::closure(store, inputRealisations), info->references);
} }
OutputPathMap resolveDerivedPath(Store & store, const DerivedPath::Built & bfd, Store * evalStore_)
{
auto & evalStore = evalStore_ ? *evalStore_ : store;
OutputPathMap outputs;
auto drv = evalStore.readDerivation(bfd.drvPath);
auto outputHashes = staticOutputHashes(store, drv);
auto drvOutputs = drv.outputsAndOptPaths(store);
auto outputNames = std::visit(overloaded {
[&](const OutputsSpec::All &) {
StringSet names;
for (auto & [outputName, _] : drv.outputs)
names.insert(outputName);
return names;
},
[&](const OutputsSpec::Names & names) {
return names;
},
}, bfd.outputs);
for (auto & output : outputNames) {
auto outputHash = get(outputHashes, output);
if (!outputHash)
throw Error(
"the derivation '%s' doesn't have an output named '%s'",
store.printStorePath(bfd.drvPath), output);
if (settings.isExperimentalFeatureEnabled(Xp::CaDerivations)) {
DrvOutput outputId { *outputHash, output };
auto realisation = store.queryRealisation(outputId);
if (!realisation)
throw MissingRealisation(outputId);
outputs.insert_or_assign(output, realisation->outPath);
} else {
// If ca-derivations isn't enabled, assume that
// the output path is statically known.
auto drvOutput = get(drvOutputs, output);
assert(drvOutput);
assert(drvOutput->second);
outputs.insert_or_assign(output, *drvOutput->second);
}
}
return outputs;
}
} }

View file

@ -6,57 +6,153 @@
namespace nix { namespace nix {
std::pair<std::string, ExtendedOutputsSpec> ExtendedOutputsSpec::parse(std::string s) bool OutputsSpec::contains(const std::string & outputName) const
{ {
static std::regex regex(R"((.*)\^((\*)|([a-z]+(,[a-z]+)*)))"); return std::visit(overloaded {
[&](const OutputsSpec::All &) {
return true;
},
[&](const OutputsSpec::Names & outputNames) {
return outputNames.count(outputName) > 0;
},
}, raw());
}
std::optional<OutputsSpec> OutputsSpec::parseOpt(std::string_view s)
{
static std::regex regex(R"((\*)|([a-z]+(,[a-z]+)*))");
std::smatch match; std::smatch match;
if (!std::regex_match(s, match, regex)) std::string s2 { s }; // until some improves std::regex
return {s, DefaultOutputs()}; if (!std::regex_match(s2, match, regex))
return std::nullopt;
if (match[3].matched) if (match[1].matched)
return {match[1], AllOutputs()}; return { OutputsSpec::All {} };
return {match[1], tokenizeString<OutputNames>(match[4].str(), ",")}; if (match[2].matched)
return { tokenizeString<OutputsSpec::Names>(match[2].str(), ",") };
assert(false);
} }
OutputsSpec OutputsSpec::parse(std::string_view s)
{
std::optional spec = OutputsSpec::parseOpt(s);
if (!spec)
throw Error("Invalid outputs specifier: '%s'", s);
return *spec;
}
std::pair<std::string_view, ExtendedOutputsSpec> ExtendedOutputsSpec::parse(std::string_view s)
{
auto found = s.rfind('^');
if (found == std::string::npos)
return { s, ExtendedOutputsSpec::Default {} };
auto spec = OutputsSpec::parse(s.substr(found + 1));
return { s.substr(0, found), ExtendedOutputsSpec::Explicit { spec } };
}
std::string OutputsSpec::to_string() const
{
return std::visit(overloaded {
[&](const OutputsSpec::All &) -> std::string {
return "*";
},
[&](const OutputsSpec::Names & outputNames) -> std::string {
return concatStringsSep(",", outputNames);
},
}, raw());
}
std::string ExtendedOutputsSpec::to_string() const std::string ExtendedOutputsSpec::to_string() const
{ {
return std::visit(overloaded { return std::visit(overloaded {
[&](const ExtendedOutputsSpec::Default &) -> std::string { [&](const ExtendedOutputsSpec::Default &) -> std::string {
return ""; return "";
}, },
[&](const ExtendedOutputsSpec::All &) -> std::string { [&](const ExtendedOutputsSpec::Explicit & outputSpec) -> std::string {
return "*"; return "^" + outputSpec.to_string();
},
[&](const ExtendedOutputsSpec::Names & outputNames) -> std::string {
return "^" + concatStringsSep(",", outputNames);
}, },
}, raw()); }, raw());
} }
bool OutputsSpec::merge(const OutputsSpec & that)
{
return std::visit(overloaded {
[&](OutputsSpec::All &) {
/* If we already refer to all outputs, there is nothing to do. */
return false;
},
[&](OutputsSpec::Names & theseNames) {
return std::visit(overloaded {
[&](const OutputsSpec::All &) {
*this = OutputsSpec::All {};
return true;
},
[&](const OutputsSpec::Names & thoseNames) {
bool ret = false;
for (auto & i : thoseNames)
if (theseNames.insert(i).second)
ret = true;
return ret;
},
}, that.raw());
},
}, raw());
}
void to_json(nlohmann::json & json, const OutputsSpec & outputsSpec)
{
std::visit(overloaded {
[&](const OutputsSpec::All &) {
json = std::vector<std::string>({"*"});
},
[&](const OutputsSpec::Names & names) {
json = names;
},
}, outputsSpec);
}
void to_json(nlohmann::json & json, const ExtendedOutputsSpec & extendedOutputsSpec) void to_json(nlohmann::json & json, const ExtendedOutputsSpec & extendedOutputsSpec)
{ {
if (std::get_if<DefaultOutputs>(&extendedOutputsSpec)) std::visit(overloaded {
[&](const ExtendedOutputsSpec::Default &) {
json = nullptr; json = nullptr;
},
else if (std::get_if<AllOutputs>(&extendedOutputsSpec)) [&](const ExtendedOutputsSpec::Explicit & e) {
json = std::vector<std::string>({"*"}); to_json(json, e);
},
else if (auto outputNames = std::get_if<OutputNames>(&extendedOutputsSpec)) }, extendedOutputsSpec);
json = *outputNames;
} }
void from_json(const nlohmann::json & json, OutputsSpec & outputsSpec)
{
auto names = json.get<OutputNames>();
if (names == OutputNames({"*"}))
outputsSpec = OutputsSpec::All {};
else
outputsSpec = names;
}
void from_json(const nlohmann::json & json, ExtendedOutputsSpec & extendedOutputsSpec) void from_json(const nlohmann::json & json, ExtendedOutputsSpec & extendedOutputsSpec)
{ {
if (json.is_null()) if (json.is_null())
extendedOutputsSpec = DefaultOutputs(); extendedOutputsSpec = ExtendedOutputsSpec::Default {};
else { else {
auto names = json.get<OutputNames>(); extendedOutputsSpec = ExtendedOutputsSpec::Explicit { json.get<OutputsSpec>() };
if (names == OutputNames({"*"}))
extendedOutputsSpec = AllOutputs();
else
extendedOutputsSpec = names;
} }
} }

View file

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <optional>
#include <set> #include <set>
#include <variant> #include <variant>
@ -13,31 +14,66 @@ struct AllOutputs {
bool operator < (const AllOutputs & _) const { return false; } bool operator < (const AllOutputs & _) const { return false; }
}; };
typedef std::variant<AllOutputs, OutputNames> _OutputsSpecRaw;
struct OutputsSpec : _OutputsSpecRaw {
using Raw = _OutputsSpecRaw;
using Raw::Raw;
using Names = OutputNames;
using All = AllOutputs;
inline const Raw & raw() const {
return static_cast<const Raw &>(*this);
}
inline Raw & raw() {
return static_cast<Raw &>(*this);
}
bool contains(const std::string & output) const;
/* Modify the receiver outputs spec so it is the union of it's old value
and the argument. Return whether the output spec needed to be modified
--- if it didn't it was already "large enough". */
bool merge(const OutputsSpec & outputs);
/* Parse a string of the form 'output1,...outputN' or
'*', returning the outputs spec. */
static OutputsSpec parse(std::string_view s);
static std::optional<OutputsSpec> parseOpt(std::string_view s);
std::string to_string() const;
};
struct DefaultOutputs { struct DefaultOutputs {
bool operator < (const DefaultOutputs & _) const { return false; } bool operator < (const DefaultOutputs & _) const { return false; }
}; };
typedef std::variant<DefaultOutputs, AllOutputs, OutputNames> _ExtendedOutputsSpecRaw; typedef std::variant<DefaultOutputs, OutputsSpec> _ExtendedOutputsSpecRaw;
struct ExtendedOutputsSpec : _ExtendedOutputsSpecRaw { struct ExtendedOutputsSpec : _ExtendedOutputsSpecRaw {
using Raw = _ExtendedOutputsSpecRaw; using Raw = _ExtendedOutputsSpecRaw;
using Raw::Raw; using Raw::Raw;
using Names = OutputNames;
using All = AllOutputs;
using Default = DefaultOutputs; using Default = DefaultOutputs;
using Explicit = OutputsSpec;
inline const Raw & raw() const { inline const Raw & raw() const {
return static_cast<const Raw &>(*this); return static_cast<const Raw &>(*this);
} }
/* Parse a string of the form 'prefix^output1,...outputN' or /* Parse a string of the form 'prefix^output1,...outputN' or
'prefix^*', returning the prefix and the outputs spec. */ 'prefix^*', returning the prefix and the extended outputs spec. */
static std::pair<std::string, ExtendedOutputsSpec> parse(std::string s); static std::pair<std::string_view, ExtendedOutputsSpec> parse(std::string_view s);
std::string to_string() const; std::string to_string() const;
}; };
void to_json(nlohmann::json &, const OutputsSpec &);
void from_json(const nlohmann::json &, OutputsSpec &);
void to_json(nlohmann::json &, const ExtendedOutputsSpec &); void to_json(nlohmann::json &, const ExtendedOutputsSpec &);
void from_json(const nlohmann::json &, ExtendedOutputsSpec &); void from_json(const nlohmann::json &, ExtendedOutputsSpec &);

View file

@ -15,10 +15,14 @@ std::string StorePathWithOutputs::to_string(const Store & store) const
DerivedPath StorePathWithOutputs::toDerivedPath() const DerivedPath StorePathWithOutputs::toDerivedPath() const
{ {
if (!outputs.empty() || path.isDerivation()) if (!outputs.empty()) {
return DerivedPath::Built { path, outputs }; return DerivedPath::Built { path, OutputsSpec::Names { outputs } };
else } else if (path.isDerivation()) {
assert(outputs.empty());
return DerivedPath::Built { path, OutputsSpec::All { } };
} else {
return DerivedPath::Opaque { path }; return DerivedPath::Opaque { path };
}
} }
@ -41,7 +45,18 @@ std::variant<StorePathWithOutputs, StorePath> StorePathWithOutputs::tryFromDeriv
return StorePathWithOutputs { bo.path }; return StorePathWithOutputs { bo.path };
}, },
[&](const DerivedPath::Built & bfd) -> std::variant<StorePathWithOutputs, StorePath> { [&](const DerivedPath::Built & bfd) -> std::variant<StorePathWithOutputs, StorePath> {
return StorePathWithOutputs { bfd.drvPath, bfd.outputs }; return StorePathWithOutputs {
.path = bfd.drvPath,
// Use legacy encoding of wildcard as empty set
.outputs = std::visit(overloaded {
[&](const OutputsSpec::All &) -> StringSet {
return {};
},
[&](const OutputsSpec::Names & outputs) {
return outputs;
},
}, bfd.outputs.raw()),
};
}, },
}, p.raw()); }, p.raw());
} }

View file

@ -64,7 +64,6 @@ public:
typedef std::set<StorePath> StorePathSet; typedef std::set<StorePath> StorePathSet;
typedef std::vector<StorePath> StorePaths; typedef std::vector<StorePath> StorePaths;
typedef std::map<std::string, StorePath> OutputPathMap;
typedef std::map<StorePath, std::optional<ContentAddress>> StorePathCAMap; typedef std::map<StorePath, std::optional<ContentAddress>> StorePathCAMap;

View file

@ -867,8 +867,8 @@ std::vector<BuildResult> RemoteStore::buildPathsWithResults(
OutputPathMap outputs; OutputPathMap outputs;
auto drv = evalStore->readDerivation(bfd.drvPath); auto drv = evalStore->readDerivation(bfd.drvPath);
const auto outputHashes = staticOutputHashes(*evalStore, drv); // FIXME: expensive const auto outputHashes = staticOutputHashes(*evalStore, drv); // FIXME: expensive
const auto drvOutputs = drv.outputsAndOptPaths(*this); auto built = resolveDerivedPath(*this, bfd, &*evalStore);
for (auto & output : bfd.outputs) { for (auto & [output, outputPath] : built) {
auto outputHash = get(outputHashes, output); auto outputHash = get(outputHashes, output);
if (!outputHash) if (!outputHash)
throw Error( throw Error(
@ -882,16 +882,11 @@ std::vector<BuildResult> RemoteStore::buildPathsWithResults(
throw MissingRealisation(outputId); throw MissingRealisation(outputId);
res.builtOutputs.emplace(realisation->id, *realisation); res.builtOutputs.emplace(realisation->id, *realisation);
} else { } else {
// If ca-derivations isn't enabled, assume that
// the output path is statically known.
const auto drvOutput = get(drvOutputs, output);
assert(drvOutput);
assert(drvOutput->second);
res.builtOutputs.emplace( res.builtOutputs.emplace(
outputId, outputId,
Realisation { Realisation {
.id = outputId, .id = outputId,
.outPath = *drvOutput->second, .outPath = outputPath,
}); });
} }
} }

View file

@ -71,6 +71,9 @@ class NarInfoDiskCache;
class Store; class Store;
typedef std::map<std::string, StorePath> OutputPathMap;
enum CheckSigsFlag : bool { NoCheckSigs = false, CheckSigs = true }; enum CheckSigsFlag : bool { NoCheckSigs = false, CheckSigs = true };
enum SubstituteFlag : bool { NoSubstitute = false, Substitute = true }; enum SubstituteFlag : bool { NoSubstitute = false, Substitute = true };
enum AllowInvalidFlag : bool { DisallowInvalid = false, AllowInvalid = true }; enum AllowInvalidFlag : bool { DisallowInvalid = false, AllowInvalid = true };
@ -120,6 +123,8 @@ public:
typedef std::map<std::string, std::string> Params; typedef std::map<std::string, std::string> Params;
protected: protected:
struct PathInfoCacheValue { struct PathInfoCacheValue {
@ -719,6 +724,11 @@ void copyClosure(
void removeTempRoots(); void removeTempRoots();
/* Resolve the derived path completely, failing if any derivation output
is unknown. */
OutputPathMap resolveDerivedPath(Store &, const DerivedPath::Built &, Store * evalStore = nullptr);
/* Return a Store object to access the Nix store denoted by /* Return a Store object to access the Nix store denoted by
uri (slight misnomer...). Supported values are: uri (slight misnomer...). Supported values are:

View file

@ -4,42 +4,62 @@
namespace nix { namespace nix {
TEST(OutputsSpec_parse, basic)
{
{
auto outputsSpec = OutputsSpec::parse("*");
ASSERT_TRUE(std::get_if<OutputsSpec::All>(&outputsSpec));
}
{
auto outputsSpec = OutputsSpec::parse("out");
ASSERT_TRUE(std::get<OutputsSpec::Names>(outputsSpec) == OutputsSpec::Names({"out"}));
}
{
auto outputsSpec = OutputsSpec::parse("out,bin");
ASSERT_TRUE(std::get<OutputsSpec::Names>(outputsSpec) == OutputsSpec::Names({"out", "bin"}));
}
{
std::optional outputsSpecOpt = OutputsSpec::parseOpt("&*()");
ASSERT_FALSE(outputsSpecOpt);
}
}
TEST(ExtendedOutputsSpec_parse, basic) TEST(ExtendedOutputsSpec_parse, basic)
{ {
{ {
auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo"); auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo");
ASSERT_EQ(prefix, "foo"); ASSERT_EQ(prefix, "foo");
ASSERT_TRUE(std::get_if<DefaultOutputs>(&extendedOutputsSpec)); ASSERT_TRUE(std::get_if<ExtendedOutputsSpec::Default>(&extendedOutputsSpec));
} }
{ {
auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo^*"); auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo^*");
ASSERT_EQ(prefix, "foo"); ASSERT_EQ(prefix, "foo");
ASSERT_TRUE(std::get_if<AllOutputs>(&extendedOutputsSpec)); auto * explicit_p = std::get_if<ExtendedOutputsSpec::Explicit>(&extendedOutputsSpec);
ASSERT_TRUE(explicit_p);
ASSERT_TRUE(std::get_if<OutputsSpec::All>(explicit_p));
} }
{ {
auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo^out"); auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo^out");
ASSERT_EQ(prefix, "foo"); ASSERT_EQ(prefix, "foo");
ASSERT_TRUE(std::get<OutputNames>(extendedOutputsSpec) == OutputNames({"out"})); ASSERT_TRUE(std::get<OutputsSpec::Names>(std::get<ExtendedOutputsSpec::Explicit>(extendedOutputsSpec)) == OutputsSpec::Names({"out"}));
} }
{ {
auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo^out,bin"); auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo^out,bin");
ASSERT_EQ(prefix, "foo"); ASSERT_EQ(prefix, "foo");
ASSERT_TRUE(std::get<OutputNames>(extendedOutputsSpec) == OutputNames({"out", "bin"})); ASSERT_TRUE(std::get<OutputsSpec::Names>(std::get<ExtendedOutputsSpec::Explicit>(extendedOutputsSpec)) == OutputsSpec::Names({"out", "bin"}));
} }
{ {
auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo^bar^out,bin"); auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo^bar^out,bin");
ASSERT_EQ(prefix, "foo^bar"); ASSERT_EQ(prefix, "foo^bar");
ASSERT_TRUE(std::get<OutputNames>(extendedOutputsSpec) == OutputNames({"out", "bin"})); ASSERT_TRUE(std::get<OutputsSpec::Names>(std::get<ExtendedOutputsSpec::Explicit>(extendedOutputsSpec)) == OutputsSpec::Names({"out", "bin"}));
}
{
auto [prefix, extendedOutputsSpec] = ExtendedOutputsSpec::parse("foo^&*()");
ASSERT_EQ(prefix, "foo^&*()");
ASSERT_TRUE(std::get_if<DefaultOutputs>(&extendedOutputsSpec));
} }
} }

View file

@ -397,7 +397,7 @@ static void main_nix_build(int argc, char * * argv)
auto bashDrv = drv->requireDrvPath(); auto bashDrv = drv->requireDrvPath();
pathsToBuild.push_back(DerivedPath::Built { pathsToBuild.push_back(DerivedPath::Built {
.drvPath = bashDrv, .drvPath = bashDrv,
.outputs = {"out"}, .outputs = OutputsSpec::Names {"out"},
}); });
pathsToCopy.insert(bashDrv); pathsToCopy.insert(bashDrv);
shellDrv = bashDrv; shellDrv = bashDrv;
@ -591,7 +591,7 @@ static void main_nix_build(int argc, char * * argv)
if (outputName == "") if (outputName == "")
throw Error("derivation '%s' lacks an 'outputName' attribute", store->printStorePath(drvPath)); throw Error("derivation '%s' lacks an 'outputName' attribute", store->printStorePath(drvPath));
pathsToBuild.push_back(DerivedPath::Built{drvPath, {outputName}}); pathsToBuild.push_back(DerivedPath::Built{drvPath, OutputsSpec::Names{outputName}});
pathsToBuildOrdered.push_back({drvPath, {outputName}}); pathsToBuildOrdered.push_back({drvPath, {outputName}});
drvsToCopy.insert(drvPath); drvsToCopy.insert(drvPath);

View file

@ -86,13 +86,13 @@ UnresolvedApp Installable::toApp(EvalState & state)
/* We want all outputs of the drv */ /* We want all outputs of the drv */
return DerivedPath::Built { return DerivedPath::Built {
.drvPath = d.drvPath, .drvPath = d.drvPath,
.outputs = {}, .outputs = OutputsSpec::All {},
}; };
}, },
[&](const NixStringContextElem::Built & b) -> DerivedPath { [&](const NixStringContextElem::Built & b) -> DerivedPath {
return DerivedPath::Built { return DerivedPath::Built {
.drvPath = b.drvPath, .drvPath = b.drvPath,
.outputs = { b.output }, .outputs = OutputsSpec::Names { b.output },
}; };
}, },
[&](const NixStringContextElem::Opaque & o) -> DerivedPath { [&](const NixStringContextElem::Opaque & o) -> DerivedPath {
@ -127,7 +127,7 @@ UnresolvedApp Installable::toApp(EvalState & state)
return UnresolvedApp { App { return UnresolvedApp { App {
.context = { DerivedPath::Built { .context = { DerivedPath::Built {
.drvPath = drvPath, .drvPath = drvPath,
.outputs = {outputName}, .outputs = OutputsSpec::Names { outputName },
} }, } },
.program = program, .program = program,
}}; }};