Skip to content

Commit 846a9c9

Browse files
committed
Extract __meta to top-level meta field in Derivation JSON format
When the derivation-meta experimental feature is enabled, extract the __meta field from structuredAttrs to a top-level meta field in the Derivation JSON format (used by `nix derivation show/add`). This makes the format consistent with DerivationOptions JSON and provides better visibility of metadata without it being buried in structuredAttrs. The implementation: - Creates derivationToJson() helper accepting ExperimentalFeatureSettings - Extracts __meta to top-level meta field when feature is enabled - Requires experimental feature when deserializing JSON with meta field - Reconstructs __meta in structuredAttrs during deserialization - Maintains backward compatibility with JSON lacking meta field Tests verify both serialization and deserialization with mocked experimental feature settings, and functional tests ensure the experimental feature requirement is enforced.
1 parent 303e07c commit 846a9c9

File tree

5 files changed

+154
-88
lines changed

5 files changed

+154
-88
lines changed

src/libstore-tests/data/derivation/meta-derivation.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@
1313
"c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-dep1"
1414
]
1515
},
16+
"meta": {
17+
"description": "A test derivation",
18+
"maintainer": "test@example.com",
19+
"version": "1.0"
20+
},
1621
"name": "meta-derivation",
1722
"outputs": {
1823
"out": {
1924
"path": "c015dhfh5l0lp6wxyvdn7bmwhbbr6hr9-meta-derivation"
2025
}
2126
},
2227
"structuredAttrs": {
23-
"__meta": {
24-
"description": "A test derivation",
25-
"maintainer": "test@example.com",
26-
"version": "1.0"
27-
},
2828
"requiredSystemFeatures": [
2929
"derivation-meta"
3030
]

src/libstore-tests/derivation/external-formats.cc

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -140,34 +140,43 @@ INSTANTIATE_TEST_SUITE_P(
140140

141141
#undef MAKE_OUTPUT_JSON_TEST_P
142142

143-
#define MAKE_TEST_P(FIXTURE) \
144-
TEST_P(FIXTURE, from_json) \
145-
{ \
146-
const auto & drv = GetParam(); \
147-
readJsonTest(drv.name, drv, mockXpSettings); \
148-
} \
149-
\
150-
TEST_P(FIXTURE, to_json) \
151-
{ \
152-
const auto & drv = GetParam(); \
153-
writeJsonTest(drv.name, drv); \
154-
} \
155-
\
156-
TEST_P(FIXTURE, from_aterm) \
157-
{ \
158-
const auto & drv = GetParam(); \
159-
readTest(drv.name + ".drv", [&](auto encoded) { \
160-
auto got = parseDerivation(*store, std::move(encoded), drv.name, mockXpSettings); \
161-
using nlohmann::json; \
162-
ASSERT_EQ(static_cast<json>(got), static_cast<json>(drv)); \
163-
ASSERT_EQ(got, drv); \
164-
}); \
165-
} \
166-
\
167-
TEST_P(FIXTURE, to_aterm) \
168-
{ \
169-
const auto & drv = GetParam(); \
170-
writeTest(drv.name + ".drv", [&]() -> std::string { return drv.unparse(*store, false); }); \
143+
#define MAKE_TEST_P(FIXTURE) \
144+
TEST_P(FIXTURE, from_json) \
145+
{ \
146+
const auto & drv = GetParam(); \
147+
readJsonTest(drv.name, drv, mockXpSettings); \
148+
} \
149+
\
150+
TEST_P(FIXTURE, to_json) \
151+
{ \
152+
const auto & drv = GetParam(); \
153+
using namespace nlohmann; \
154+
writeTest( \
155+
drv.name + ".json", \
156+
[&]() -> json { \
157+
json res; \
158+
derivationToJson(res, drv, mockXpSettings); \
159+
return res; \
160+
}, \
161+
[](const auto & file) { return json::parse(readFile(file)); }, \
162+
[](const auto & file, const auto & got) { return writeFile(file, got.dump(2) + "\n"); }); \
163+
} \
164+
\
165+
TEST_P(FIXTURE, from_aterm) \
166+
{ \
167+
const auto & drv = GetParam(); \
168+
readTest(drv.name + ".drv", [&](auto encoded) { \
169+
auto got = parseDerivation(*store, std::move(encoded), drv.name, mockXpSettings); \
170+
using nlohmann::json; \
171+
ASSERT_EQ(static_cast<json>(got), static_cast<json>(drv)); \
172+
ASSERT_EQ(got, drv); \
173+
}); \
174+
} \
175+
\
176+
TEST_P(FIXTURE, to_aterm) \
177+
{ \
178+
const auto & drv = GetParam(); \
179+
writeTest(drv.name + ".drv", [&]() -> std::string { return drv.unparse(*store, false); }); \
171180
}
172181

173182
struct DerivationJsonAtermTest : DerivationTest,

src/libstore/derivations.cc

Lines changed: 97 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,80 @@ Derivation Derivation::parseJsonAndValidate(Store & store, const nlohmann::json
14381438

14391439
const Hash impureOutputHash = hashString(HashAlgorithm::SHA256, "impure");
14401440

1441+
static unsigned constexpr expectedJsonVersionDerivation = 4;
1442+
1443+
void derivationToJson(nlohmann::json & res, const Derivation & d, const ExperimentalFeatureSettings & xpSettings)
1444+
{
1445+
using json = nlohmann::json;
1446+
res = json::object();
1447+
1448+
res["name"] = d.name;
1449+
1450+
res["version"] = expectedJsonVersionDerivation;
1451+
1452+
{
1453+
json & outputsObj = res["outputs"];
1454+
outputsObj = json::object();
1455+
for (auto & [outputName, output] : d.outputs) {
1456+
outputsObj[outputName] = output;
1457+
}
1458+
}
1459+
1460+
{
1461+
auto & inputsObj = res["inputs"];
1462+
inputsObj = json::object();
1463+
1464+
{
1465+
auto & inputsList = inputsObj["srcs"];
1466+
inputsList = json::array();
1467+
for (auto & input : d.inputSrcs)
1468+
inputsList.emplace_back(input);
1469+
}
1470+
1471+
auto doInput = [&](this const auto & doInput, const auto & inputNode) -> json {
1472+
auto value = json::object();
1473+
value["outputs"] = inputNode.value;
1474+
{
1475+
auto next = json::object();
1476+
for (auto & [outputId, childNode] : inputNode.childMap)
1477+
next[outputId] = doInput(childNode);
1478+
value["dynamicOutputs"] = std::move(next);
1479+
}
1480+
return value;
1481+
};
1482+
1483+
auto & inputDrvsObj = inputsObj["drvs"];
1484+
inputDrvsObj = json::object();
1485+
for (auto & [inputDrv, inputNode] : d.inputDrvs.map) {
1486+
inputDrvsObj[inputDrv.to_string()] = doInput(inputNode);
1487+
}
1488+
}
1489+
1490+
res["system"] = d.platform;
1491+
res["builder"] = d.builder;
1492+
res["args"] = d.args;
1493+
res["env"] = d.env;
1494+
1495+
if (d.structuredAttrs) {
1496+
auto structuredAttrs = d.structuredAttrs->structuredAttrs;
1497+
1498+
// Only extract __meta when derivation-meta experimental feature is enabled
1499+
if (xpSettings.isEnabled(Xp::DerivationMeta)) {
1500+
if (hasDerivationMetaFeature(structuredAttrs)) {
1501+
auto metaIt = structuredAttrs.find("__meta");
1502+
res["meta"] = metaIt->second;
1503+
structuredAttrs.erase(metaIt);
1504+
} else {
1505+
res["meta"] = nullptr;
1506+
}
1507+
}
1508+
1509+
res["structuredAttrs"] = std::move(structuredAttrs);
1510+
} else if (xpSettings.isEnabled(Xp::DerivationMeta)) {
1511+
res["meta"] = nullptr;
1512+
}
1513+
}
1514+
14411515
} // namespace nix
14421516

14431517
namespace nlohmann {
@@ -1539,61 +1613,9 @@ adl_serializer<DerivationOutput>::from_json(const json & _json, const Experiment
15391613
}
15401614
}
15411615

1542-
static unsigned constexpr expectedJsonVersionDerivation = 4;
1543-
15441616
void adl_serializer<Derivation>::to_json(json & res, const Derivation & d)
15451617
{
1546-
res = nlohmann::json::object();
1547-
1548-
res["name"] = d.name;
1549-
1550-
res["version"] = expectedJsonVersionDerivation;
1551-
1552-
{
1553-
nlohmann::json & outputsObj = res["outputs"];
1554-
outputsObj = nlohmann::json::object();
1555-
for (auto & [outputName, output] : d.outputs) {
1556-
outputsObj[outputName] = output;
1557-
}
1558-
}
1559-
1560-
{
1561-
auto & inputsObj = res["inputs"];
1562-
inputsObj = nlohmann::json::object();
1563-
1564-
{
1565-
auto & inputsList = inputsObj["srcs"];
1566-
inputsList = nlohmann::json::array();
1567-
for (auto & input : d.inputSrcs)
1568-
inputsList.emplace_back(input);
1569-
}
1570-
1571-
auto doInput = [&](this const auto & doInput, const auto & inputNode) -> nlohmann::json {
1572-
auto value = nlohmann::json::object();
1573-
value["outputs"] = inputNode.value;
1574-
{
1575-
auto next = nlohmann::json::object();
1576-
for (auto & [outputId, childNode] : inputNode.childMap)
1577-
next[outputId] = doInput(childNode);
1578-
value["dynamicOutputs"] = std::move(next);
1579-
}
1580-
return value;
1581-
};
1582-
1583-
auto & inputDrvsObj = inputsObj["drvs"];
1584-
inputDrvsObj = nlohmann::json::object();
1585-
for (auto & [inputDrv, inputNode] : d.inputDrvs.map) {
1586-
inputDrvsObj[inputDrv.to_string()] = doInput(inputNode);
1587-
}
1588-
}
1589-
1590-
res["system"] = d.platform;
1591-
res["builder"] = d.builder;
1592-
res["args"] = d.args;
1593-
res["env"] = d.env;
1594-
1595-
if (d.structuredAttrs)
1596-
res["structuredAttrs"] = d.structuredAttrs->structuredAttrs;
1618+
nix::derivationToJson(res, d, nix::experimentalFeatureSettings);
15971619
}
15981620

15991621
Derivation adl_serializer<Derivation>::from_json(const json & _json, const ExperimentalFeatureSettings & xpSettings)
@@ -1674,8 +1696,28 @@ Derivation adl_serializer<Derivation>::from_json(const json & _json, const Exper
16741696
throw;
16751697
}
16761698

1677-
if (auto structuredAttrs = get(json, "structuredAttrs"))
1678-
res.structuredAttrs = StructuredAttrs{*structuredAttrs};
1699+
if (auto structuredAttrs = get(json, "structuredAttrs")) {
1700+
auto structuredAttrsObj = getObject(*structuredAttrs);
1701+
1702+
// If there's a top-level meta field, require experimental feature and reconstruct __meta
1703+
if (auto metaPtr = optionalValueAt(json, "meta")) {
1704+
if (!metaPtr->is_null()) {
1705+
xpSettings.require(Xp::DerivationMeta);
1706+
// Check if derivation-meta is in requiredSystemFeatures
1707+
if (auto it = structuredAttrsObj.find("requiredSystemFeatures");
1708+
it != structuredAttrsObj.end() && it->second.is_array()) {
1709+
for (const auto & feature : it->second) {
1710+
if (feature.is_string() && feature.get<std::string>() == "derivation-meta") {
1711+
structuredAttrsObj["__meta"] = *metaPtr;
1712+
break;
1713+
}
1714+
}
1715+
}
1716+
}
1717+
}
1718+
1719+
res.structuredAttrs = StructuredAttrs{structuredAttrsObj};
1720+
}
16791721

16801722
return res;
16811723
}

src/libstore/include/nix/store/derivations.hh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,12 @@ struct Sink;
591591
Source & readDerivation(Source & in, const StoreDirConfig & store, BasicDerivation & drv, std::string_view name);
592592
void writeDerivation(Sink & out, const StoreDirConfig & store, const BasicDerivation & drv);
593593

594+
/**
595+
* Convert a derivation to JSON, with optional experimental feature settings.
596+
* This is exposed for testing purposes to allow passing mock experimental feature settings.
597+
*/
598+
void derivationToJson(nlohmann::json & res, const Derivation & d, const ExperimentalFeatureSettings & xpSettings);
599+
594600
/**
595601
* This creates an opaque and almost certainly unique string
596602
* deterministically from the output name.

tests/functional/derivation-json.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,12 @@ jq '.name = "foo"' < "$TEST_HOME/simple.json" | expectStderr 1 nix derivation ad
2121
# deferred and fill in the right input address for us.
2222
drvPath3=$(jq '.outputs |= map_values(del(.path))' < "$TEST_HOME/simple.json" | nix derivation add)
2323
[[ "$drvPath" = "$drvPath3" ]]
24+
25+
# Test backward compatibility: JSON without 'meta' field should still be ingestible
26+
drvPath4=$(jq 'del(.meta)' < "$TEST_HOME/simple.json" | nix derivation add)
27+
[[ "$drvPath" = "$drvPath4" ]]
28+
29+
# Test that ingesting derivation with 'meta' field requires experimental feature
30+
jq '.meta = {"description": "test"} | .structuredAttrs = {"requiredSystemFeatures": ["derivation-meta"]}' < "$TEST_HOME/simple.json" \
31+
| expectStderr 1 nix derivation add --experimental-features nix-command \
32+
| grepQuiet "experimental Nix feature 'derivation-meta' is disabled"

0 commit comments

Comments
 (0)