From 04cc3db4df644cfc0b66c122ef60eac64ef0b721 Mon Sep 17 00:00:00 2001 From: Pamplemousse Date: Fri, 10 Jan 2025 12:29:11 +0100 Subject: [PATCH] Allow `diff-closures` to output JSON Change-Id: Ia17ea93d98b38e3415c35257daf07c7978f50ef4 --- doc/manual/change-authors.yml | 4 + doc/manual/rl-next/diff-closure-as-json.md | 36 ++++++++ lix/libcmd/command.hh | 1 + lix/nix/diff-closures.cc | 100 ++++++++++++++++++--- lix/nix/diff-closures.md | 57 +++++++++--- lix/nix/profile.cc | 1 + 6 files changed, 173 insertions(+), 26 deletions(-) create mode 100644 doc/manual/rl-next/diff-closure-as-json.md diff --git a/doc/manual/change-authors.yml b/doc/manual/change-authors.yml index 91a4eb898..20369d531 100644 --- a/doc/manual/change-authors.yml +++ b/doc/manual/change-authors.yml @@ -139,6 +139,10 @@ midnightveil: ncfavier: github: ncfavier +pamplemousse: + display_name: Xavier Maso + github: pamplemousse + piegames: display_name: piegames forgejo: piegames diff --git a/doc/manual/rl-next/diff-closure-as-json.md b/doc/manual/rl-next/diff-closure-as-json.md new file mode 100644 index 000000000..ef06b6007 --- /dev/null +++ b/doc/manual/rl-next/diff-closure-as-json.md @@ -0,0 +1,36 @@ +--- +synopsis: "Allow `nix store diff-closures` to output JSON" +issues: [] +cls: [2360] +category: Improvements +credits: [pamplemousse] +--- + +Add the `--json` option to the `nix store diff-closures` command to allow users to collect diff information into a machine readable format. + +```bash +$ build/lix/nix/nix store diff-closures --json /run/current-system /nix/store/n1prick95pihd4lkv58nn3pzg1yivcdb-neovim-0.10.4/bin/nvim | jq | head -n 23 +{ + "packages": { + "02overridedns": { + "sizeDelta": -688, + "versionsAfter": [], + "versionsBefore": [ + "" + ] + }, + "50-coredump.conf": { + "sizeDelta": -1976, + "versionsAfter": [], + "versionsBefore": [ + "" + ] + }, + "Diff": { + "sizeDelta": -514864, + "versionsAfter": [], + "versionsBefore": [ + "0.4.1" + ] + }, +``` diff --git a/lix/libcmd/command.hh b/lix/libcmd/command.hh index 3604118b9..82ad562e5 100644 --- a/lix/libcmd/command.hh +++ b/lix/libcmd/command.hh @@ -375,6 +375,7 @@ kj::Promise> printClosureDiff( ref store, const StorePath & beforePath, const StorePath & afterPath, + bool json, std::string_view indent); } diff --git a/lix/nix/diff-closures.cc b/lix/nix/diff-closures.cc index cdb1480de..d51fb1a75 100644 --- a/lix/nix/diff-closures.cc +++ b/lix/nix/diff-closures.cc @@ -6,18 +6,53 @@ #include "lix/libstore/names.hh" #include "lix/libutil/result.hh" +#include #include namespace nix { +static constexpr std::string_view CLOSURE_DIFF_SCHEMA_VERSION = "lix-closure-diff-v1"; + struct Info { std::string outputName; }; +struct DiffInfoForPackage +{ + int64_t sizeDelta; + std::set addedVersions; + std::set removedVersions; +}; + // name -> version -> store paths typedef std::map>> GroupedPaths; +typedef std::map DiffInfo; + +nlohmann::json toJSON(const DiffInfo & diff) +{ + nlohmann::json res = nlohmann::json::object(); + nlohmann::json content = nlohmann::json::object(); + + for (auto & [name, item] : diff) { + auto packageContent = nlohmann::json::object(); + + if (!item.removedVersions.empty() || !item.addedVersions.empty()) { + packageContent["versionsBefore"] = item.removedVersions; + packageContent["versionsAfter"] = item.addedVersions; + } + packageContent["sizeDelta"] = item.sizeDelta; + + content[name] = std::move(packageContent); + } + + res["packages"] = std::move(content); + res["schema"] = CLOSURE_DIFF_SCHEMA_VERSION; + + return res; +} + static kj::Promise> getClosureInfo(ref store, const StorePath & toplevel) try { @@ -50,11 +85,10 @@ try { co_return result::current_exception(); } -kj::Promise> printClosureDiff( +kj::Promise> getDiffInfo( ref store, const StorePath & beforePath, - const StorePath & afterPath, - std::string_view indent) + const StorePath & afterPath) try { auto beforeClosure = TRY_AWAIT(getClosureInfo(store, beforePath)); auto afterClosure = TRY_AWAIT(getClosureInfo(store, afterPath)); @@ -63,6 +97,8 @@ try { for (auto & [name, _] : beforeClosure) allNames.insert(name); for (auto & [name, _] : afterClosure) allNames.insert(name); + DiffInfo itemsToPrint; + for (auto & name : allNames) { auto & beforeVersions = beforeClosure[name]; auto & afterVersions = afterClosure[name]; @@ -84,7 +120,6 @@ try { auto beforeSize = TRY_AWAIT(totalSize(beforeVersions)); auto afterSize = TRY_AWAIT(totalSize(afterVersions)); auto sizeDelta = (int64_t) afterSize - (int64_t) beforeSize; - auto showDelta = std::abs(sizeDelta) >= 8 * 1024; std::set removed, unchanged; for (auto & [version, _] : beforeVersions) @@ -94,17 +129,56 @@ try { for (auto & [version, _] : afterVersions) if (!beforeVersions.count(version)) added.insert(version); - if (showDelta || !removed.empty() || !added.empty()) { - std::vector items; - if (!removed.empty() || !added.empty()) - items.push_back(fmt("%s → %s", showVersions(removed), showVersions(added))); - if (showDelta) - items.push_back(fmt("%s%+.1f KiB" ANSI_NORMAL, sizeDelta > 0 ? ANSI_RED : ANSI_GREEN, sizeDelta / 1024.0)); - logger->cout("%s%s: %s", indent, name, concatStringsSep(", ", items)); + if (!removed.empty() || !added.empty()) { + auto info = DiffInfoForPackage { + .sizeDelta = sizeDelta, + .addedVersions = added, + .removedVersions = removed + }; + + itemsToPrint[name] = std::move(info); } } + + co_return itemsToPrint; +} catch (...) { + co_return result::current_exception(); +} + +void renderDiffInfo( + DiffInfo diff, + const std::string_view indent) +{ + for (auto & [name, item] : diff) { + auto showDelta = std::abs(item.sizeDelta) >= 8 * 1024; + + std::vector line; + if (!item.removedVersions.empty() || !item.addedVersions.empty()) + line.push_back(fmt("%s → %s", showVersions(item.removedVersions), showVersions(item.addedVersions))); + if (showDelta) + line.push_back(fmt("%s%+.1f KiB" ANSI_NORMAL, item.sizeDelta > 0 ? ANSI_RED : ANSI_GREEN, item.sizeDelta / 1024.0)); + logger->cout("%s%s: %s", indent, name, concatStringsSep(", ", line)); + } +} + +kj::Promise> printClosureDiff( + ref store, + const StorePath & beforePath, + const StorePath & afterPath, + const bool json, + const std::string_view indent) +try { + DiffInfo diff = TRY_AWAIT(getDiffInfo(store, beforePath, afterPath)); + + if (json) { + logger->cout(toJSON(diff).dump()); + } else { + renderDiffInfo(diff, indent); + } + co_return result::success(); } catch (...) { + co_return result::current_exception(); } @@ -112,7 +186,7 @@ try { using namespace nix; -struct CmdDiffClosures : SourceExprCommand, MixOperateOnOptions +struct CmdDiffClosures : SourceExprCommand, MixJSON, MixOperateOnOptions { std::string _before, _after; @@ -141,7 +215,7 @@ struct CmdDiffClosures : SourceExprCommand, MixOperateOnOptions auto beforePath = Installable::toStorePath(*state, getEvalStore(), store, Realise::Outputs, operateOn, before); auto after = parseInstallable(*state, store, _after); auto afterPath = Installable::toStorePath(*state, getEvalStore(), store, Realise::Outputs, operateOn, after); - aio().blockOn(printClosureDiff(store, beforePath, afterPath, "")); + aio().blockOn(printClosureDiff(store, beforePath, afterPath, json, "")); } }; diff --git a/lix/nix/diff-closures.md b/lix/nix/diff-closures.md index 5f81aa4e2..552eb0809 100644 --- a/lix/nix/diff-closures.md +++ b/lix/nix/diff-closures.md @@ -26,28 +26,59 @@ of packages, as well as changes in store path sizes. For each package name in the two closures (where a package name is defined as the name component of a store path excluding the version), -if there is a change in the set of versions of the package, or a -change in the size of the store paths of more than 8 KiB, it prints a -line like this: +it returns information about the change in the set of versions of the +package, and the change in the size of the store paths. -```console -dolphin: 20.08.1 → 20.08.2, +13.9 KiB -``` - -No size change is shown if it's below the threshold. If the package -does not exist in either the *before* or *after* closures, it is -represented using `∅` (empty set) on the appropriate side of the -arrow. If a package has an empty version string, the version is -rendered as `ε` (epsilon). +If the package does not exist in either the *before* or *after* +closures, it is represented using `∅` (empty set) on the appropriate +side of the arrow. If a package has an empty version string, the +version is rendered as `ε` (epsilon). There may be multiple versions of a package in each closure. In that case, only the changed versions are shown. Thus, ```console -libfoo: 1.2, 1.3 → 1.4 +1.2, 1.3 → 1.4 ``` leaves open the possibility that there are other versions (e.g. `1.1`) that exist in both closures. +## Regular output + +The regular output prints lines like this: + +```console +dolphin: 20.08.1 → 20.08.2, +13.9 KiB +``` + +No size change is shown if it's below the 8 KiB threshold. + +## JSON output + +With `--json`, the output is in a JSON representation suitable for automatic +processing by other tools. The resulting object will contain a `schema` key +specifying the version of the output structure, and a `packages` key listing +the diff information for each package in both closures. + +The printed result looks like this: + +```json +{ + "packages": { + "zlib": { + "sizeDelta": 118856, + "versionsAfter": [ + "1.3.1" + ], + "versionsBefore": [ + "1.3" + ] + } + // ... + }, + "schema": "lix-closure-diff-v1" +} +``` + )"" diff --git a/lix/nix/profile.cc b/lix/nix/profile.cc index 6501049af..98b163022 100644 --- a/lix/nix/profile.cc +++ b/lix/nix/profile.cc @@ -481,6 +481,7 @@ struct CmdProfileDiffClosures : virtual StoreCommand, MixDefaultProfile aio().blockOn(printClosureDiff(store, store->followLinksToStorePath(prevGen->path), store->followLinksToStorePath(gen.path), + false, " ")); }