From 3555ae524c38a66634921a922a62dca212544508 Mon Sep 17 00:00:00 2001 From: John Ericson Date: Sun, 19 Jan 2025 23:56:59 -0500 Subject: [PATCH] Introduce `DerivationOptions` This is a first step towards PR #10760, and the issues it addresses. See the Doxygen for details. Thanks to these changes, we are able to drastically restrict how the rest of the code-base uses `ParseDerivation`. Co-Authored-By: HaeNoe --- .../derivation-advanced-attrs.cc | 153 ++++++----- src/libstore/build/derivation-goal.cc | 7 +- src/libstore/build/derivation-goal.hh | 2 + src/libstore/derivation-options.cc | 237 ++++++++++++++++++ src/libstore/derivation-options.hh | 171 +++++++++++++ src/libstore/meson.build | 2 + src/libstore/misc.cc | 6 +- src/libstore/parsed-derivations.cc | 44 ---- src/libstore/parsed-derivations.hh | 32 +-- .../unix/build/local-derivation-goal.cc | 123 ++------- .../unix/build/local-derivation-goal.hh | 5 - src/libutil/json-utils.cc | 10 + src/libutil/json-utils.hh | 1 + 13 files changed, 541 insertions(+), 252 deletions(-) create mode 100644 src/libstore/derivation-options.cc create mode 100644 src/libstore/derivation-options.hh diff --git a/src/libstore-tests/derivation-advanced-attrs.cc b/src/libstore-tests/derivation-advanced-attrs.cc index 9d2c64ef3e4..5911b7811d3 100644 --- a/src/libstore-tests/derivation-advanced-attrs.cc +++ b/src/libstore-tests/derivation-advanced-attrs.cc @@ -3,13 +3,15 @@ #include "experimental-features.hh" #include "derivations.hh" - -#include "tests/libstore.hh" -#include "tests/characterization.hh" +#include "derivations.hh" +#include "derivation-options.hh" #include "parsed-derivations.hh" #include "types.hh" #include "json-utils.hh" +#include "tests/libstore.hh" +#include "tests/characterization.hh" + namespace nix { using nlohmann::json; @@ -80,21 +82,22 @@ TEST_F(DerivationAdvancedAttrsTest, Derivation_advancedAttributes_defaults) auto drvPath = writeDerivation(*store, got, NoRepair, true); ParsedDerivation parsedDrv(drvPath, got); - - EXPECT_EQ(parsedDrv.getStringAttr("__sandboxProfile").value_or(""), ""); - EXPECT_EQ(parsedDrv.getBoolAttr("__noChroot"), false); - EXPECT_EQ(parsedDrv.getStringsAttr("__impureHostDeps").value_or(Strings()), Strings()); - EXPECT_EQ(parsedDrv.getStringsAttr("impureEnvVars").value_or(Strings()), Strings()); - EXPECT_EQ(parsedDrv.getBoolAttr("__darwinAllowLocalNetworking"), false); - EXPECT_EQ(parsedDrv.getStringsAttr("allowedReferences"), std::nullopt); - EXPECT_EQ(parsedDrv.getStringsAttr("allowedRequisites"), std::nullopt); - EXPECT_EQ(parsedDrv.getStringsAttr("disallowedReferences"), std::nullopt); - EXPECT_EQ(parsedDrv.getStringsAttr("disallowedRequisites"), std::nullopt); - EXPECT_EQ(parsedDrv.getRequiredSystemFeatures(), StringSet()); - EXPECT_EQ(parsedDrv.canBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.willBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.substitutesAllowed(), true); - EXPECT_EQ(parsedDrv.useUidRange(), false); + DerivationOptions options = DerivationOptions::fromParsedDerivation(parsedDrv); + + EXPECT_EQ(options.additionalSandboxProfile, ""); + EXPECT_EQ(options.noChroot, false); + EXPECT_EQ(options.impureHostDeps, Strings()); + EXPECT_EQ(options.impureEnvVars, Strings()); + EXPECT_EQ(options.allowLocalNetworking, false); + EXPECT_EQ(options.checksAllOutputs.allowedReferences, std::nullopt); + EXPECT_EQ(options.checksAllOutputs.allowedRequisites, std::nullopt); + EXPECT_EQ(options.checksAllOutputs.disallowedReferences, std::nullopt); + EXPECT_EQ(options.checksAllOutputs.disallowedRequisites, std::nullopt); + EXPECT_EQ(options.getRequiredSystemFeatures(got), StringSet()); + EXPECT_EQ(options.canBuildLocally(*store, got), false); + EXPECT_EQ(options.willBuildLocally(*store, got), false); + EXPECT_EQ(options.substitutesAllowed(), true); + EXPECT_EQ(options.useUidRange(got), false); }); }; @@ -106,29 +109,28 @@ TEST_F(DerivationAdvancedAttrsTest, Derivation_advancedAttributes) auto drvPath = writeDerivation(*store, got, NoRepair, true); ParsedDerivation parsedDrv(drvPath, got); + DerivationOptions options = DerivationOptions::fromParsedDerivation(parsedDrv); StringSet systemFeatures{"rainbow", "uid-range"}; - EXPECT_EQ(parsedDrv.getStringAttr("__sandboxProfile").value_or(""), "sandcastle"); - EXPECT_EQ(parsedDrv.getBoolAttr("__noChroot"), true); - EXPECT_EQ(parsedDrv.getStringsAttr("__impureHostDeps").value_or(Strings()), Strings{"/usr/bin/ditto"}); - EXPECT_EQ(parsedDrv.getStringsAttr("impureEnvVars").value_or(Strings()), Strings{"UNICORN"}); - EXPECT_EQ(parsedDrv.getBoolAttr("__darwinAllowLocalNetworking"), true); + EXPECT_EQ(options.additionalSandboxProfile, "sandcastle"); + EXPECT_EQ(options.noChroot, true); + EXPECT_EQ(options.impureHostDeps, Strings{"/usr/bin/ditto"}); + EXPECT_EQ(options.impureEnvVars, Strings{"UNICORN"}); + EXPECT_EQ(options.allowLocalNetworking, true); EXPECT_EQ( - parsedDrv.getStringsAttr("allowedReferences"), Strings{"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"}); + options.checksAllOutputs.allowedReferences, Strings{"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"}); EXPECT_EQ( - parsedDrv.getStringsAttr("allowedRequisites"), Strings{"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"}); + options.checksAllOutputs.allowedRequisites, Strings{"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"}); EXPECT_EQ( - parsedDrv.getStringsAttr("disallowedReferences"), - Strings{"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"}); + options.checksAllOutputs.disallowedReferences, Strings{"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"}); EXPECT_EQ( - parsedDrv.getStringsAttr("disallowedRequisites"), - Strings{"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"}); - EXPECT_EQ(parsedDrv.getRequiredSystemFeatures(), systemFeatures); - EXPECT_EQ(parsedDrv.canBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.willBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.substitutesAllowed(), false); - EXPECT_EQ(parsedDrv.useUidRange(), true); + options.checksAllOutputs.disallowedRequisites, Strings{"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"}); + EXPECT_EQ(options.getRequiredSystemFeatures(got), systemFeatures); + EXPECT_EQ(options.canBuildLocally(*store, got), false); + EXPECT_EQ(options.willBuildLocally(*store, got), false); + EXPECT_EQ(options.substitutesAllowed(), false); + EXPECT_EQ(options.useUidRange(got), true); }); }; @@ -140,27 +142,25 @@ TEST_F(DerivationAdvancedAttrsTest, Derivation_advancedAttributes_structuredAttr auto drvPath = writeDerivation(*store, got, NoRepair, true); ParsedDerivation parsedDrv(drvPath, got); + DerivationOptions options = DerivationOptions::fromParsedDerivation(parsedDrv); - EXPECT_EQ(parsedDrv.getStringAttr("__sandboxProfile").value_or(""), ""); - EXPECT_EQ(parsedDrv.getBoolAttr("__noChroot"), false); - EXPECT_EQ(parsedDrv.getStringsAttr("__impureHostDeps").value_or(Strings()), Strings()); - EXPECT_EQ(parsedDrv.getStringsAttr("impureEnvVars").value_or(Strings()), Strings()); - EXPECT_EQ(parsedDrv.getBoolAttr("__darwinAllowLocalNetworking"), false); + EXPECT_EQ(options.additionalSandboxProfile, ""); + EXPECT_EQ(options.noChroot, false); + EXPECT_EQ(options.impureHostDeps, Strings()); + EXPECT_EQ(options.impureEnvVars, Strings()); + EXPECT_EQ(options.allowLocalNetworking, false); { - auto structuredAttrs_ = parsedDrv.getStructuredAttrs(); - ASSERT_TRUE(structuredAttrs_); - auto & structuredAttrs = *structuredAttrs_; + ASSERT_TRUE(parsedDrv.hasStructuredAttrs()); - auto outputChecks_ = get(structuredAttrs, "outputChecks"); - ASSERT_FALSE(outputChecks_); + ASSERT_EQ(options.checksPerOutput.size(), 0); } - EXPECT_EQ(parsedDrv.getRequiredSystemFeatures(), StringSet()); - EXPECT_EQ(parsedDrv.canBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.willBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.substitutesAllowed(), true); - EXPECT_EQ(parsedDrv.useUidRange(), false); + EXPECT_EQ(options.getRequiredSystemFeatures(got), StringSet()); + EXPECT_EQ(options.canBuildLocally(*store, got), false); + EXPECT_EQ(options.willBuildLocally(*store, got), false); + EXPECT_EQ(options.substitutesAllowed(), true); + EXPECT_EQ(options.useUidRange(got), false); }); }; @@ -172,62 +172,49 @@ TEST_F(DerivationAdvancedAttrsTest, Derivation_advancedAttributes_structuredAttr auto drvPath = writeDerivation(*store, got, NoRepair, true); ParsedDerivation parsedDrv(drvPath, got); + DerivationOptions options = DerivationOptions::fromParsedDerivation(parsedDrv); StringSet systemFeatures{"rainbow", "uid-range"}; - EXPECT_EQ(parsedDrv.getStringAttr("__sandboxProfile").value_or(""), "sandcastle"); - EXPECT_EQ(parsedDrv.getBoolAttr("__noChroot"), true); - EXPECT_EQ(parsedDrv.getStringsAttr("__impureHostDeps").value_or(Strings()), Strings{"/usr/bin/ditto"}); - EXPECT_EQ(parsedDrv.getStringsAttr("impureEnvVars").value_or(Strings()), Strings{"UNICORN"}); - EXPECT_EQ(parsedDrv.getBoolAttr("__darwinAllowLocalNetworking"), true); + EXPECT_EQ(options.additionalSandboxProfile, "sandcastle"); + EXPECT_EQ(options.noChroot, true); + EXPECT_EQ(options.impureHostDeps, Strings{"/usr/bin/ditto"}); + EXPECT_EQ(options.impureEnvVars, Strings{"UNICORN"}); + EXPECT_EQ(options.allowLocalNetworking, true); { - auto structuredAttrs_ = parsedDrv.getStructuredAttrs(); - ASSERT_TRUE(structuredAttrs_); - auto & structuredAttrs = *structuredAttrs_; - - auto outputChecks_ = get(structuredAttrs, "outputChecks"); - ASSERT_TRUE(outputChecks_); - auto & outputChecks = *outputChecks_; + ASSERT_TRUE(parsedDrv.hasStructuredAttrs()); { - auto output_ = get(outputChecks, "out"); + auto output_ = get(options.checksPerOutput, "out"); ASSERT_TRUE(output_); auto & output = *output_; - EXPECT_EQ( - get(output, "allowedReferences")->get(), - Strings{"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"}); - EXPECT_EQ( - get(output, "allowedRequisites")->get(), - Strings{"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"}); + EXPECT_EQ(output.allowedReferences, Strings{"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"}); + EXPECT_EQ(output.allowedRequisites, Strings{"/nix/store/3c08bzb71z4wiag719ipjxr277653ynp-foo"}); } { - auto output_ = get(outputChecks, "bin"); + auto output_ = get(options.checksPerOutput, "bin"); ASSERT_TRUE(output_); auto & output = *output_; - EXPECT_EQ( - get(output, "disallowedReferences")->get(), - Strings{"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"}); - EXPECT_EQ( - get(output, "disallowedRequisites")->get(), - Strings{"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"}); + EXPECT_EQ(output.disallowedReferences, Strings{"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"}); + EXPECT_EQ(output.disallowedRequisites, Strings{"/nix/store/7rhsm8i393hm1wcsmph782awg1hi2f7x-bar"}); } { - auto output_ = get(outputChecks, "dev"); + auto output_ = get(options.checksPerOutput, "dev"); ASSERT_TRUE(output_); auto & output = *output_; - EXPECT_EQ(get(output, "maxSize")->get(), 789); - EXPECT_EQ(get(output, "maxClosureSize")->get(), 5909); + EXPECT_EQ(output.maxSize, 789); + EXPECT_EQ(output.maxClosureSize, 5909); } } - EXPECT_EQ(parsedDrv.getRequiredSystemFeatures(), systemFeatures); - EXPECT_EQ(parsedDrv.canBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.willBuildLocally(*store), false); - EXPECT_EQ(parsedDrv.substitutesAllowed(), false); - EXPECT_EQ(parsedDrv.useUidRange(), true); + EXPECT_EQ(options.getRequiredSystemFeatures(got), systemFeatures); + EXPECT_EQ(options.canBuildLocally(*store, got), false); + EXPECT_EQ(options.willBuildLocally(*store, got), false); + EXPECT_EQ(options.substitutesAllowed(), false); + EXPECT_EQ(options.useUidRange(got), true); }); }; diff --git a/src/libstore/build/derivation-goal.cc b/src/libstore/build/derivation-goal.cc index 4d97250d3af..4eccdeb444a 100644 --- a/src/libstore/build/derivation-goal.cc +++ b/src/libstore/build/derivation-goal.cc @@ -195,6 +195,7 @@ Goal::Co DerivationGoal::haveDerivation() trace("have derivation"); parsedDrv = std::make_unique(drvPath, *drv); + drvOptions = std::make_unique(DerivationOptions::fromParsedDerivation(*parsedDrv)); if (!drv->type().hasKnownOutputPaths()) experimentalFeatureSettings.require(Xp::CaDerivations); @@ -246,7 +247,7 @@ Goal::Co DerivationGoal::haveDerivation() /* We are first going to try to create the invalid output paths through substitutes. If that doesn't work, we'll build them. */ - if (settings.useSubstitutes && parsedDrv->substitutesAllowed()) + if (settings.useSubstitutes && drvOptions->substitutesAllowed()) for (auto & [outputName, status] : initialOutputs) { if (!status.wanted) continue; if (!status.known) @@ -718,7 +719,7 @@ Goal::Co DerivationGoal::tryToBuild() `preferLocalBuild' set. Also, check and repair modes are only supported for local builds. */ bool buildLocally = - (buildMode != bmNormal || parsedDrv->willBuildLocally(worker.store)) + (buildMode != bmNormal || drvOptions->willBuildLocally(worker.store, *drv)) && settings.maxBuildJobs.get() != 0; if (!buildLocally) { @@ -1147,7 +1148,7 @@ HookReply DerivationGoal::tryBuildHook() << (worker.getNrLocalBuilds() < settings.maxBuildJobs ? 1 : 0) << drv->platform << worker.store.printStorePath(drvPath) - << parsedDrv->getRequiredSystemFeatures(); + << drvOptions->getRequiredSystemFeatures(*drv); worker.hook->sink.flush(); /* Read the first line of input, which should be a word indicating diff --git a/src/libstore/build/derivation-goal.hh b/src/libstore/build/derivation-goal.hh index ad3d9ca2acf..b23eab94c70 100644 --- a/src/libstore/build/derivation-goal.hh +++ b/src/libstore/build/derivation-goal.hh @@ -2,6 +2,7 @@ ///@file #include "parsed-derivations.hh" +#include "derivation-options.hh" #ifndef _WIN32 # include "user-lock.hh" #endif @@ -143,6 +144,7 @@ struct DerivationGoal : public Goal std::unique_ptr drv; std::unique_ptr parsedDrv; + std::unique_ptr drvOptions; /** * The remainder is state held during the build. diff --git a/src/libstore/derivation-options.cc b/src/libstore/derivation-options.cc new file mode 100644 index 00000000000..a7478dd1b75 --- /dev/null +++ b/src/libstore/derivation-options.cc @@ -0,0 +1,237 @@ +#include "derivation-options.hh" +#include "json-utils.hh" +#include "parsed-derivations.hh" +#include "types.hh" +#include "util.hh" +#include +#include +#include + +namespace nix { + +DerivationOptions DerivationOptions::fromParsedDerivation(const ParsedDerivation & parsed, bool shouldWarn) +{ + DerivationOptions defaults = {}; + + auto structuredAttrs = parsed.structuredAttrs.get(); + + if (shouldWarn && structuredAttrs) { + if (get(*structuredAttrs, "allowedReferences")) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'allowedReferences'; use 'outputChecks' instead"); + } + if (get(*structuredAttrs, "allowedRequisites")) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'allowedRequisites'; use 'outputChecks' instead"); + } + if (get(*structuredAttrs, "disallowedRequisites")) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'disallowedRequisites'; use 'outputChecks' instead"); + } + if (get(*structuredAttrs, "disallowedReferences")) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'disallowedReferences'; use 'outputChecks' instead"); + } + if (get(*structuredAttrs, "maxSize")) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'maxSize'; use 'outputChecks' instead"); + } + if (get(*structuredAttrs, "maxClosureSize")) { + warn( + "'structuredAttrs' disables the effect of the top-level attribute 'maxClosureSize'; use 'outputChecks' instead"); + } + } + + return { + .checksPerOutput = + [&] { + std::map res; + + if (auto structuredAttrs = parsed.structuredAttrs.get()) { + if (auto outputChecks = get(*structuredAttrs, "outputChecks")) { + for (auto & [outputName, output] : getObject(*outputChecks)) { + DerivationOptions::OutputChecks checks; + + if (auto maxSize = get(output, "maxSize")) + checks.maxSize = maxSize->get(); + + if (auto maxClosureSize = get(output, "maxClosureSize")) + checks.maxClosureSize = maxClosureSize->get(); + + auto get_ = [&](const std::string & name) -> std::optional { + if (auto i = get(output, name)) { + Strings res; + for (auto j = i->begin(); j != i->end(); ++j) { + if (!j->is_string()) + throw Error("attribute '%s' must be a list of strings", name); + res.push_back(j->get()); + } + checks.disallowedRequisites = res; + return res; + } + return {}; + }; + + checks.allowedReferences = get_("allowedReferences"); + checks.allowedRequisites = get_("allowedRequisites"); + checks.disallowedReferences = get_("disallowedReferences"); + checks.disallowedRequisites = get_("disallowedRequisites"); + + res.insert_or_assign(outputName, std::move(checks)); + } + } + } + + return res; + }(), + .checksAllOutputs = static_cast(parsed.structuredAttrs) + ? (DerivationOptions::OutputChecks{}) // ignore + : (DerivationOptions::OutputChecks{ + // legacy non-structured-attributes case + .ignoreSelfRefs = true, + .allowedReferences = parsed.getStringsAttr("allowedReferences"), + .disallowedReferences = parsed.getStringsAttr("disallowedReferences"), + .allowedRequisites = parsed.getStringsAttr("allowedRequisites"), + .disallowedRequisites = parsed.getStringsAttr("disallowedRequisites"), + }), + .unsafeDiscardReferences = + [&] { + std::map res; + + if (auto structuredAttrs = parsed.structuredAttrs.get()) { + if (auto udr = get(*structuredAttrs, "unsafeDiscardReferences")) { + for (auto & [outputName, output] : getObject(*udr)) { + if (!output.is_boolean()) + throw Error("attribute 'unsafeDiscardReferences.\"%s\"' must be a Boolean", outputName); + res.insert_or_assign(outputName, output.get()); + } + } + } + + return res; + }(), + .additionalSandboxProfile = + parsed.getStringAttr("__sandboxProfile").value_or(defaults.additionalSandboxProfile), + .noChroot = parsed.getBoolAttr("__noChroot", defaults.noChroot), + .impureHostDeps = parsed.getStringsAttr("__impureHostDeps").value_or(defaults.impureHostDeps), + .impureEnvVars = parsed.getStringsAttr("impureEnvVars").value_or(defaults.impureEnvVars), + .allowLocalNetworking = parsed.getBoolAttr("__darwinAllowLocalNetworking", defaults.allowLocalNetworking), + .requiredSystemFeatures = + parsed.getStringsAttr("requiredSystemFeatures").value_or(defaults.requiredSystemFeatures), + .preferLocalBuild = parsed.getBoolAttr("preferLocalBuild", defaults.preferLocalBuild), + .allowSubstitutes = parsed.getBoolAttr("allowSubstitutes", defaults.allowSubstitutes), + }; +} + +StringSet DerivationOptions::getRequiredSystemFeatures(const BasicDerivation & drv) const +{ + // FIXME: cache this? + StringSet res; + for (auto & i : requiredSystemFeatures) + res.insert(i); + if (!drv.type().hasKnownOutputPaths()) + res.insert("ca-derivations"); + return res; +} + +bool DerivationOptions::canBuildLocally(Store & localStore, const BasicDerivation & drv) const +{ + if (drv.platform != settings.thisSystem.get() && !settings.extraPlatforms.get().count(drv.platform) + && !drv.isBuiltin()) + return false; + + if (settings.maxBuildJobs.get() == 0 && !drv.isBuiltin()) + return false; + + for (auto & feature : getRequiredSystemFeatures(drv)) + if (!localStore.systemFeatures.get().count(feature)) + return false; + + return true; +} + +bool DerivationOptions::willBuildLocally(Store & localStore, const BasicDerivation & drv) const +{ + return preferLocalBuild && canBuildLocally(localStore, drv); +} + +bool DerivationOptions::substitutesAllowed() const +{ + return settings.alwaysAllowSubstitutes ? true : allowSubstitutes; +} + +bool DerivationOptions::useUidRange(const BasicDerivation & drv) const +{ + return getRequiredSystemFeatures(drv).count("uid-range"); +} + +} + +namespace nlohmann { + +using namespace nix; + +DerivationOptions adl_serializer::from_json(const json & json) +{ + DerivationOptions res; + + res.additionalSandboxProfile = getString(valueAt(json, "additionalSandboxProfile")); + res.noChroot = getBoolean(valueAt(json, "noChroot")); + res.impureHostDeps = getStringList(valueAt(json, "impureHostDeps")); + res.impureEnvVars = getStringList(valueAt(json, "impureEnvVars")); + res.allowLocalNetworking = getBoolean(valueAt(json, "allowLocalNetworking")); + + res.checksAllOutputs = json; + res.checksPerOutput = valueAt(json, "outputChecks"); + + res.requiredSystemFeatures = getStringList(valueAt(json, "requiredSystemFeatures")); + res.preferLocalBuild = getBoolean(valueAt(json, "preferLocalBuild")); + res.allowSubstitutes = getBoolean(valueAt(json, "allowSubstitutes")); + + return res; +} + +void adl_serializer::to_json(json & json, DerivationOptions o) +{ + json["additionalSandboxProfile"] = o.additionalSandboxProfile; + json["noChroot"] = o.noChroot; + json["impureHostDeps"] = o.impureHostDeps; + json["impureEnvVars"] = o.impureEnvVars; + json["allowLocalNetworking"] = o.allowLocalNetworking; + + json["outputChecks"] = o.checksPerOutput; + + json["ignoreSelfRefs"] = o.checksAllOutputs.ignoreSelfRefs; + json["allowedReferences"] = o.checksAllOutputs.allowedReferences; + json["allowedRequisites"] = o.checksAllOutputs.allowedRequisites; + json["disallowedReferences"] = o.checksAllOutputs.disallowedReferences; + json["disallowedRequisites"] = o.checksAllOutputs.disallowedRequisites; + + json["requiredSystemFeatures"] = o.requiredSystemFeatures; + json["preferLocalBuild"] = o.preferLocalBuild; + json["allowSubstitutes"] = o.allowSubstitutes; +} + +DerivationOptions::OutputChecks adl_serializer::from_json(const json & json) +{ + DerivationOptions::OutputChecks res; + + res.ignoreSelfRefs = getBoolean(valueAt(json, "ignoreSelfRefs")); + res.allowedReferences = nullableValueAt(json, "allowedReferences"); + res.allowedRequisites = nullableValueAt(json, "allowedRequisites"); + res.disallowedReferences = nullableValueAt(json, "disallowedReferences"); + res.disallowedRequisites = nullableValueAt(json, "disallowedRequisites"); + + return res; +} + +void adl_serializer::to_json(json & json, DerivationOptions::OutputChecks c) +{ + json["ignoreSelfRefs"] = c.ignoreSelfRefs; + json["allowedReferences"] = c.allowedReferences; + json["allowedRequisites"] = c.allowedRequisites; + json["disallowedReferences"] = c.disallowedReferences; + json["disallowedRequisites"] = c.disallowedRequisites; +} + +} diff --git a/src/libstore/derivation-options.hh b/src/libstore/derivation-options.hh new file mode 100644 index 00000000000..00eda321f92 --- /dev/null +++ b/src/libstore/derivation-options.hh @@ -0,0 +1,171 @@ +#pragma once +///@file + +#include +#include +#include +#include + +#include "types.hh" +#include "json-impls.hh" + +namespace nix { + +class Store; +struct BasicDerivation; +class ParsedDerivation; + +/** + * This represents all the special options on a `Derivation`. + * + * Currently, these options are parsed from the environment variables + * with the aid of `ParsedDerivation`. + * + * The first goal of this data type is to make sure that no other code + * uses `ParsedDerivation` to ad-hoc parse some additional options. That + * ensures this data type is up to date and fully correct. + * + * The second goal of this data type is to allow an alternative to + * hackily parsing the options from the environment variables. The ATerm + * format cannot change, but in alternatives to it (like the JSON + * format), we have the option of instead storing the options + * separately. That would be nice to separate concerns, and not make any + * environment variable names magical. + */ +struct DerivationOptions +{ + struct OutputChecks + { + bool ignoreSelfRefs = false; + std::optional maxSize, maxClosureSize; + + /** + * env: allowedReferences + * + * A value of `nullopt` indicates that the check is skipped. + * This means that all references are allowed. + */ + std::optional allowedReferences = std::nullopt; + + /** + * env: disallowedReferences + * + * A value of `nullopt` indicates that the check is skipped. + * This means that there are no disallowed references. + */ + std::optional disallowedReferences = std::nullopt; + + /** + * env: allowedRequisites + * + * See `allowedReferences` + */ + std::optional allowedRequisites = std::nullopt; + + /** + * env: disallowedRequisites + * + * See `disallowedReferences` + */ + std::optional disallowedRequisites = std::nullopt; + + bool operator==(const OutputChecks &) const = default; + }; + + std::map checksPerOutput; + + OutputChecks checksAllOutputs; + + /** + * Whether to avoid scanning for references for a given output. + */ + std::map unsafeDiscardReferences; + + /** + * env: __sandboxProfile + * + * Just for Darwin + */ + std::string additionalSandboxProfile = ""; + + /** + * env: __noChroot + * + * Derivation would like to opt out of the sandbox. + * + * Builder is free to not respect this wish (because it is + * insecure) and fail the build instead. + */ + bool noChroot = false; + + /** + * env: __impureHostDeps + */ + Strings impureHostDeps = {}; + + /** + * env: impureEnvVars + */ + Strings impureEnvVars = {}; + + /** + * env: __darwinAllowLocalNetworking + * + * Just for Darwin + */ + bool allowLocalNetworking = false; + + /** + * env: requiredSystemFeatures + */ + Strings requiredSystemFeatures = {}; + + /** + * env: preferLocalBuild + */ + bool preferLocalBuild = false; + + /** + * env: allowSubstitutes + */ + bool allowSubstitutes = true; + + bool operator==(const DerivationOptions &) const = default; + + /** + * Parse this information from its legacy encoding as part of the + * environment. This should not be used with nice greenfield formats + * (e.g. JSON) but is necessary for supporing old formats (e.g. + * ATerm). + */ + static DerivationOptions fromParsedDerivation(const ParsedDerivation & parsed, bool shouldWarn = true); + + /** + * @param drv Must be the same derivation we parsed this from. In + * the future we'll flip things around so a `BasicDerivation` has + * `DerivationOptions` instead. + */ + StringSet getRequiredSystemFeatures(const BasicDerivation & drv) const; + + /** + * @param drv See note on `getRequiredSystemFeatures` + */ + bool canBuildLocally(Store & localStore, const BasicDerivation & drv) const; + + /** + * @param drv See note on `getRequiredSystemFeatures` + */ + bool willBuildLocally(Store & localStore, const BasicDerivation & drv) const; + + bool substitutesAllowed() const; + + /** + * @param drv See note on `getRequiredSystemFeatures` + */ + bool useUidRange(const BasicDerivation & drv) const; +}; + +}; + +JSON_IMPL(DerivationOptions); +JSON_IMPL(DerivationOptions::OutputChecks) diff --git a/src/libstore/meson.build b/src/libstore/meson.build index 79d91249722..496c5b10da7 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -196,6 +196,7 @@ sources = files( 'content-address.cc', 'daemon.cc', 'derivations.cc', + 'derivation-options.cc', 'derived-path-map.cc', 'derived-path.cc', 'downstream-placeholder.cc', @@ -267,6 +268,7 @@ headers = [config_h] + files( 'content-address.hh', 'daemon.hh', 'derivations.hh', + 'derivation-options.hh', 'derived-path-map.hh', 'derived-path.hh', 'downstream-placeholder.hh', diff --git a/src/libstore/misc.cc b/src/libstore/misc.cc index bcc02206bc9..9d3b243266e 100644 --- a/src/libstore/misc.cc +++ b/src/libstore/misc.cc @@ -2,6 +2,7 @@ #include "derivations.hh" #include "parsed-derivations.hh" +#include "derivation-options.hh" #include "globals.hh" #include "store-api.hh" #include "thread-pool.hh" @@ -222,8 +223,9 @@ void Store::queryMissing(const std::vector & targets, auto drv = make_ref(derivationFromPath(drvPath)); ParsedDerivation parsedDrv(StorePath(drvPath), *drv); + DerivationOptions drvOptions = DerivationOptions::fromParsedDerivation(parsedDrv); - if (!knownOutputPaths && settings.useSubstitutes && parsedDrv.substitutesAllowed()) { + if (!knownOutputPaths && settings.useSubstitutes && drvOptions.substitutesAllowed()) { experimentalFeatureSettings.require(Xp::CaDerivations); // If there are unknown output paths, attempt to find if the @@ -253,7 +255,7 @@ void Store::queryMissing(const std::vector & targets, } } - if (knownOutputPaths && settings.useSubstitutes && parsedDrv.substitutesAllowed()) { + if (knownOutputPaths && settings.useSubstitutes && drvOptions.substitutesAllowed()) { auto drvState = make_ref>(DrvState(invalid.size())); for (auto & output : invalid) pool.enqueue(std::bind(checkOutput, drvPath, drv, output, drvState)); diff --git a/src/libstore/parsed-derivations.cc b/src/libstore/parsed-derivations.cc index d8459d4d71c..29bcb5e3990 100644 --- a/src/libstore/parsed-derivations.cc +++ b/src/libstore/parsed-derivations.cc @@ -87,49 +87,6 @@ std::optional ParsedDerivation::getStringsAttr(const std::string & name } } -StringSet ParsedDerivation::getRequiredSystemFeatures() const -{ - // FIXME: cache this? - StringSet res; - for (auto & i : getStringsAttr("requiredSystemFeatures").value_or(Strings())) - res.insert(i); - if (!drv.type().hasKnownOutputPaths()) - res.insert("ca-derivations"); - return res; -} - -bool ParsedDerivation::canBuildLocally(Store & localStore) const -{ - if (drv.platform != settings.thisSystem.get() - && !settings.extraPlatforms.get().count(drv.platform) - && !drv.isBuiltin()) - return false; - - if (settings.maxBuildJobs.get() == 0 - && !drv.isBuiltin()) - return false; - - for (auto & feature : getRequiredSystemFeatures()) - if (!localStore.systemFeatures.get().count(feature)) return false; - - return true; -} - -bool ParsedDerivation::willBuildLocally(Store & localStore) const -{ - return getBoolAttr("preferLocalBuild") && canBuildLocally(localStore); -} - -bool ParsedDerivation::substitutesAllowed() const -{ - return settings.alwaysAllowSubstitutes ? true : getBoolAttr("allowSubstitutes", true); -} - -bool ParsedDerivation::useUidRange() const -{ - return getRequiredSystemFeatures().count("uid-range"); -} - static std::regex shVarName("[A-Za-z_][A-Za-z0-9_]*"); /** @@ -188,7 +145,6 @@ static nlohmann::json pathInfoToJSON( std::optional ParsedDerivation::prepareStructuredAttrs(Store & store, const StorePathSet & inputPaths) { - auto structuredAttrs = getStructuredAttrs(); if (!structuredAttrs) return std::nullopt; auto json = *structuredAttrs; diff --git a/src/libstore/parsed-derivations.hh b/src/libstore/parsed-derivations.hh index 71085a604d4..0099f55f4ec 100644 --- a/src/libstore/parsed-derivations.hh +++ b/src/libstore/parsed-derivations.hh @@ -8,38 +8,38 @@ namespace nix { +struct DerivationOptions; + class ParsedDerivation { StorePath drvPath; BasicDerivation & drv; std::unique_ptr structuredAttrs; -public: - - ParsedDerivation(const StorePath & drvPath, BasicDerivation & drv); - - ~ParsedDerivation(); - - const nlohmann::json * getStructuredAttrs() const - { - return structuredAttrs.get(); - } - std::optional getStringAttr(const std::string & name) const; bool getBoolAttr(const std::string & name, bool def = false) const; std::optional getStringsAttr(const std::string & name) const; - StringSet getRequiredSystemFeatures() const; + /** + * Only `DerivationOptions` is allowed to parse individual fields + * from `ParsedDerivation`. This ensure that it includes all + * derivation options, and, the likes of `LocalDerivationGoal` are + * incapable of more ad-hoc options. + */ + friend struct DerivationOptions; - bool canBuildLocally(Store & localStore) const; +public: - bool willBuildLocally(Store & localStore) const; + ParsedDerivation(const StorePath & drvPath, BasicDerivation & drv); - bool substitutesAllowed() const; + ~ParsedDerivation(); - bool useUidRange() const; + bool hasStructuredAttrs() const + { + return static_cast(structuredAttrs); + } std::optional prepareStructuredAttrs(Store & store, const StorePathSet & inputPaths); }; diff --git a/src/libstore/unix/build/local-derivation-goal.cc b/src/libstore/unix/build/local-derivation-goal.cc index 06a2f85be84..93944ee7380 100644 --- a/src/libstore/unix/build/local-derivation-goal.cc +++ b/src/libstore/unix/build/local-derivation-goal.cc @@ -184,10 +184,6 @@ void LocalDerivationGoal::killSandbox(bool getStats) Goal::Co LocalDerivationGoal::tryLocalBuild() { -#if __APPLE__ - additionalSandboxProfile = parsedDrv->getStringAttr("__sandboxProfile").value_or(""); -#endif - unsigned int curBuilds = worker.getNrLocalBuilds(); if (curBuilds >= settings.maxBuildJobs) { worker.waitForBuildSlot(shared_from_this()); @@ -200,13 +196,12 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() /* Are we doing a chroot build? */ { - auto noChroot = parsedDrv->getBoolAttr("__noChroot"); if (settings.sandboxMode == smEnabled) { - if (noChroot) + if (drvOptions->noChroot) throw Error("derivation '%s' has '__noChroot' set, " "but that's not allowed when 'sandbox' is 'true'", worker.store.printStorePath(drvPath)); #if __APPLE__ - if (additionalSandboxProfile != "") + if (drvOptions->additionalSandboxProfile != "") throw Error("derivation '%s' specifies a sandbox profile, " "but this is only allowed when 'sandbox' is 'relaxed'", worker.store.printStorePath(drvPath)); #endif @@ -215,7 +210,7 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() else if (settings.sandboxMode == smDisabled) useChroot = false; else if (settings.sandboxMode == smRelaxed) - useChroot = derivationType->isSandboxed() && !noChroot; + useChroot = derivationType->isSandboxed() && !drvOptions->noChroot; } auto & localStore = getLocalStore(); @@ -240,7 +235,7 @@ Goal::Co LocalDerivationGoal::tryLocalBuild() if (useBuildUsers()) { if (!buildUser) - buildUser = acquireUserLock(parsedDrv->useUidRange() ? 65536 : 1, useChroot); + buildUser = acquireUserLock(drvOptions->useUidRange(*drv) ? 65536 : 1, useChroot); if (!buildUser) { if (!actLock) @@ -531,10 +526,10 @@ void LocalDerivationGoal::startBuilder() killSandbox(false); /* Right platform? */ - if (!parsedDrv->canBuildLocally(worker.store)) + if (!drvOptions->canBuildLocally(worker.store, *drv)) throw Error("a '%s' with features {%s} is required to build '%s', but I am a '%s' with features {%s}", drv->platform, - concatStringsSep(", ", parsedDrv->getRequiredSystemFeatures()), + concatStringsSep(", ", drvOptions->getRequiredSystemFeatures(*drv)), worker.store.printStorePath(drvPath), settings.thisSystem, concatStringsSep(", ", worker.store.systemFeatures)); @@ -622,7 +617,7 @@ void LocalDerivationGoal::startBuilder() writeStructuredAttrs(); /* Handle exportReferencesGraph(), if set. */ - if (!parsedDrv->getStructuredAttrs()) { + if (!parsedDrv->hasStructuredAttrs()) { /* The `exportReferencesGraph' feature allows the references graph to be passed to a builder. This attribute should be a list of pairs [name1 path1 name2 path2 ...]. The references graph of @@ -696,7 +691,7 @@ void LocalDerivationGoal::startBuilder() PathSet allowedPaths = settings.allowedImpureHostPrefixes; /* This works like the above, except on a per-derivation level */ - auto impurePaths = parsedDrv->getStringsAttr("__impureHostDeps").value_or(Strings()); + auto impurePaths = drvOptions->impureHostDeps; for (auto & i : impurePaths) { bool found = false; @@ -716,7 +711,7 @@ void LocalDerivationGoal::startBuilder() throw Error("derivation '%s' requested impure path '%s', but it was not in allowed-impure-host-deps", worker.store.printStorePath(drvPath), i); - /* Allow files in __impureHostDeps to be missing; e.g. + /* Allow files in drvOptions->impureHostDeps to be missing; e.g. macOS 11+ has no /usr/lib/libSystem*.dylib */ pathsInChroot[i] = {i, true}; } @@ -756,10 +751,10 @@ void LocalDerivationGoal::startBuilder() nobody account. The latter is kind of a hack to support Samba-in-QEMU. */ createDirs(chrootRootDir + "/etc"); - if (parsedDrv->useUidRange()) + if (drvOptions->useUidRange(*drv)) chownToBuilder(chrootRootDir + "/etc"); - if (parsedDrv->useUidRange() && (!buildUser || buildUser->getUIDCount() < 65536)) + if (drvOptions->useUidRange(*drv) && (!buildUser || buildUser->getUIDCount() < 65536)) throw Error("feature 'uid-range' requires the setting '%s' to be enabled", settings.autoAllocateUids.name); /* Declare the build user's group so that programs get a consistent @@ -818,7 +813,7 @@ void LocalDerivationGoal::startBuilder() } #else - if (parsedDrv->useUidRange()) + if (drvOptions->useUidRange(*drv)) throw Error("feature 'uid-range' is not supported on this platform"); #if __APPLE__ /* We don't really have any parent prep work to do (yet?) @@ -828,7 +823,7 @@ void LocalDerivationGoal::startBuilder() #endif #endif } else { - if (parsedDrv->useUidRange()) + if (drvOptions->useUidRange(*drv)) throw Error("feature 'uid-range' is only supported in sandboxed builds"); } @@ -873,7 +868,7 @@ void LocalDerivationGoal::startBuilder() /* Fire up a Nix daemon to process recursive Nix calls from the builder. */ - if (parsedDrv->getRequiredSystemFeatures().count("recursive-nix")) + if (drvOptions->getRequiredSystemFeatures(*drv).count("recursive-nix")) startDaemon(); /* Run the builder. */ @@ -1148,7 +1143,7 @@ void LocalDerivationGoal::initTmpDir() passAsFile is ignored in structure mode because it's not needed (attributes are not passed through the environment, so there is no size constraint). */ - if (!parsedDrv->getStructuredAttrs()) { + if (!parsedDrv->hasStructuredAttrs()) { StringSet passAsFile = tokenizeString(getOr(drv->env, "passAsFile", "")); for (auto & i : drv->env) { @@ -1229,7 +1224,7 @@ void LocalDerivationGoal::initEnv() if (!impureEnv.empty()) experimentalFeatureSettings.require(Xp::ConfigurableImpureEnv); - for (auto & i : parsedDrv->getStringsAttr("impureEnvVars").value_or(Strings())) { + for (auto & i : drvOptions->impureEnvVars){ auto envVar = impureEnv.find(i); if (envVar != impureEnv.end()) { env[i] = envVar->second; @@ -1989,7 +1984,7 @@ void LocalDerivationGoal::runChild() } /* Make /etc unwritable */ - if (!parsedDrv->useUidRange()) + if (!drvOptions->useUidRange(*drv)) chmod_(chrootRootDir + "/etc", 0555); /* Unshare this mount namespace. This is necessary because @@ -2176,7 +2171,7 @@ void LocalDerivationGoal::runChild() } sandboxProfile += ")\n"; - sandboxProfile += additionalSandboxProfile; + sandboxProfile += drvOptions->additionalSandboxProfile; } else sandboxProfile += #include "sandbox-minimal.sb" @@ -2389,14 +2384,8 @@ SingleDrvOutputs LocalDerivationGoal::registerOutputs() inodesSeen); bool discardReferences = false; - if (auto structuredAttrs = parsedDrv->getStructuredAttrs()) { - if (auto udr = get(*structuredAttrs, "unsafeDiscardReferences")) { - if (auto output = get(*udr, outputName)) { - if (!output->is_boolean()) - throw Error("attribute 'unsafeDiscardReferences.\"%s\"' of derivation '%s' must be a Boolean", outputName, drvPath.to_string()); - discardReferences = output->get(); - } - } + if (auto udr = get(drvOptions->unsafeDiscardReferences, outputName)) { + discardReferences = *udr; } StorePathSet references; @@ -2863,13 +2852,6 @@ void LocalDerivationGoal::checkOutputs(const std::map maxSize, maxClosureSize; - std::optional allowedReferences, allowedRequisites, disallowedReferences, disallowedRequisites; - }; - /* Compute the closure and closure size of some output. This is slightly tricky because some of its references (namely other outputs) may not be valid yet. */ @@ -2901,7 +2883,7 @@ void LocalDerivationGoal::checkOutputs(const std::map *checks.maxSize) throw BuildError("path '%s' is too large at %d bytes; limit is %d bytes", @@ -2966,67 +2948,10 @@ void LocalDerivationGoal::checkOutputs(const std::mapgetStructuredAttrs()) { - if (get(*structuredAttrs, "allowedReferences")){ - warn("'structuredAttrs' disables the effect of the top-level attribute 'allowedReferences'; use 'outputChecks' instead"); - } - if (get(*structuredAttrs, "allowedRequisites")){ - warn("'structuredAttrs' disables the effect of the top-level attribute 'allowedRequisites'; use 'outputChecks' instead"); - } - if (get(*structuredAttrs, "disallowedRequisites")){ - warn("'structuredAttrs' disables the effect of the top-level attribute 'disallowedRequisites'; use 'outputChecks' instead"); - } - if (get(*structuredAttrs, "disallowedReferences")){ - warn("'structuredAttrs' disables the effect of the top-level attribute 'disallowedReferences'; use 'outputChecks' instead"); - } - if (get(*structuredAttrs, "maxSize")){ - warn("'structuredAttrs' disables the effect of the top-level attribute 'maxSize'; use 'outputChecks' instead"); - } - if (get(*structuredAttrs, "maxClosureSize")){ - warn("'structuredAttrs' disables the effect of the top-level attribute 'maxClosureSize'; use 'outputChecks' instead"); - } - if (auto outputChecks = get(*structuredAttrs, "outputChecks")) { - if (auto output = get(*outputChecks, outputName)) { - Checks checks; - - if (auto maxSize = get(*output, "maxSize")) - checks.maxSize = maxSize->get(); - - if (auto maxClosureSize = get(*output, "maxClosureSize")) - checks.maxClosureSize = maxClosureSize->get(); - - auto get_ = [&](const std::string & name) -> std::optional { - if (auto i = get(*output, name)) { - Strings res; - for (auto j = i->begin(); j != i->end(); ++j) { - if (!j->is_string()) - throw Error("attribute '%s' of derivation '%s' must be a list of strings", name, worker.store.printStorePath(drvPath)); - res.push_back(j->get()); - } - checks.disallowedRequisites = res; - return res; - } - return {}; - }; - - checks.allowedReferences = get_("allowedReferences"); - checks.allowedRequisites = get_("allowedRequisites"); - checks.disallowedReferences = get_("disallowedReferences"); - checks.disallowedRequisites = get_("disallowedRequisites"); + if (auto outputChecks = get(drvOptions->checksPerOutput, outputName)) + applyChecks(*outputChecks); - applyChecks(checks); - } - } - } else { - // legacy non-structured-attributes case - Checks checks; - checks.ignoreSelfRefs = true; - checks.allowedReferences = parsedDrv->getStringsAttr("allowedReferences"); - checks.allowedRequisites = parsedDrv->getStringsAttr("allowedRequisites"); - checks.disallowedReferences = parsedDrv->getStringsAttr("disallowedReferences"); - checks.disallowedRequisites = parsedDrv->getStringsAttr("disallowedRequisites"); - applyChecks(checks); - } + applyChecks(drvOptions->checksAllOutputs); } } diff --git a/src/libstore/unix/build/local-derivation-goal.hh b/src/libstore/unix/build/local-derivation-goal.hh index 1ea2476610a..19b09bc384b 100644 --- a/src/libstore/unix/build/local-derivation-goal.hh +++ b/src/libstore/unix/build/local-derivation-goal.hh @@ -109,11 +109,6 @@ struct LocalDerivationGoal : public DerivationGoal typedef map Environment; Environment env; -#if __APPLE__ - typedef std::string SandboxProfile; - SandboxProfile additionalSandboxProfile; -#endif - /** * Hash rewriting. */ diff --git a/src/libutil/json-utils.cc b/src/libutil/json-utils.cc index dff068e07c5..f67811e2162 100644 --- a/src/libutil/json-utils.cc +++ b/src/libutil/json-utils.cc @@ -3,6 +3,7 @@ #include "types.hh" #include #include +#include namespace nix { @@ -38,6 +39,15 @@ std::optional optionalValueAt(const nlohmann::json::object_t & m return std::optional { map.at(key) }; } +std::optional nullableValueAt(const nlohmann::json::object_t & map, const std::string & key) +{ + auto value = valueAt(map, key); + + if (value.is_null()) + return std::nullopt; + + return std::optional { std::move(value) }; +} const nlohmann::json * getNullable(const nlohmann::json & value) { diff --git a/src/libutil/json-utils.hh b/src/libutil/json-utils.hh index 546334e1e1f..dcde8bdf21e 100644 --- a/src/libutil/json-utils.hh +++ b/src/libutil/json-utils.hh @@ -25,6 +25,7 @@ const nlohmann::json & valueAt( const std::string & key); std::optional optionalValueAt(const nlohmann::json::object_t & value, const std::string & key); +std::optional nullableValueAt(const nlohmann::json::object_t & value, const std::string & key); /** * Downcast the json object, failing with a nice error if the conversion fails.