diff --git a/src/libcmd/command.hh b/src/libcmd/command.hh index 4a72627ed..790043483 100644 --- a/src/libcmd/command.hh +++ b/src/libcmd/command.hh @@ -352,6 +352,7 @@ void printClosureDiff( ref store, const StorePath & beforePath, const StorePath & afterPath, + bool json, std::string_view indent); } diff --git a/src/nix/diff-closures.cc b/src/nix/diff-closures.cc index 2bc7fe82b..eff2a42d5 100644 --- a/src/nix/diff-closures.cc +++ b/src/nix/diff-closures.cc @@ -4,6 +4,7 @@ #include "common-args.hh" #include "names.hh" +#include #include #include "strings.hh" @@ -15,9 +16,38 @@ struct Info std::string outputName; }; +struct DiffInfoForPackage +{ + int64_t sizeDelta; + std::string addedVersions; + std::string removedVersions; +}; + // name -> version -> store paths typedef std::map>> GroupedPaths; +typedef std::map DiffInfo; + + +nlohmann::json toJSON(DiffInfo diff) +{ + nlohmann::json res = nlohmann::json::object(); + + for (auto & [name, item] : diff) { + auto content = nlohmann::json::object(); + + if (!item.removedVersions.empty() || !item.addedVersions.empty()) { + content["versionsBefore"] = item.removedVersions; + content["versionsAfter"] = item.addedVersions; + } + content["sizeDelta"] = item.sizeDelta; + + res[name] = std::move(content); + } + + return res; +} + GroupedPaths getClosureInfo(ref store, const StorePath & toplevel) { StorePathSet closure; @@ -47,20 +77,11 @@ GroupedPaths getClosureInfo(ref store, const StorePath & toplevel) return groupedPaths; } -std::string showVersions(const std::set & versions) -{ - if (versions.empty()) return "∅"; - std::set versions2; - for (auto & version : versions) - versions2.insert(version.empty() ? "ε" : version); - return concatStringsSep(", ", versions2); -} - -void printClosureDiff( +DiffInfo getDiffInfo( ref store, const StorePath & beforePath, - const StorePath & afterPath, - std::string_view indent) + const StorePath & afterPath +) { auto beforeClosure = getClosureInfo(store, beforePath); auto afterClosure = getClosureInfo(store, afterPath); @@ -69,6 +90,8 @@ void printClosureDiff( 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]; @@ -85,7 +108,6 @@ void printClosureDiff( auto beforeSize = totalSize(beforeVersions); auto afterSize = 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) @@ -95,22 +117,65 @@ void printClosureDiff( 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 = showVersions(added), + .removedVersions = showVersions(removed) + }; + itemsToPrint[name] = std::move(info); } } + + return itemsToPrint; +} + +std::string showVersions(const std::set & versions) +{ + if (versions.empty()) return "∅"; + std::set versions2; + for (auto & version : versions) + versions2.insert(version.empty() ? "ε" : version); + return concatStringsSep(", ", versions2); +} + +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", item.removedVersions, 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)); + } +} + +void printClosureDiff( + ref store, + const StorePath & beforePath, + const StorePath & afterPath, + const bool json, + const std::string_view indent) +{ + DiffInfo diff = getDiffInfo(store, beforePath, afterPath); + + if (json) { + logger->cout(toJSON(diff).dump()); + } else { + renderDiffInfo(diff, indent); + } } } using namespace nix; -struct CmdDiffClosures : SourceExprCommand, MixOperateOnOptions +struct CmdDiffClosures : SourceExprCommand, MixJSON, MixOperateOnOptions { std::string _before, _after; @@ -138,7 +203,7 @@ struct CmdDiffClosures : SourceExprCommand, MixOperateOnOptions auto beforePath = Installable::toStorePath(getEvalStore(), store, Realise::Outputs, operateOn, before); auto after = parseInstallable(store, _after); auto afterPath = Installable::toStorePath(getEvalStore(), store, Realise::Outputs, operateOn, after); - printClosureDiff(store, beforePath, afterPath, ""); + printClosureDiff(store, beforePath, afterPath, json, ""); } }; diff --git a/src/nix/diff-closures.md b/src/nix/diff-closures.md index 0294c0d8d..967ca42bb 100644 --- a/src/nix/diff-closures.md +++ b/src/nix/diff-closures.md @@ -24,28 +24,50 @@ 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 printed result looks like this: + +```json +{ + "dolphin": { + "after": "20.08.1", + "before": "20.08.2", + "sizeDelta": 13900 + }, + ... +} +``` + )"" diff --git a/src/nix/profile.cc b/src/nix/profile.cc index 324fd6330..35984134c 100644 --- a/src/nix/profile.cc +++ b/src/nix/profile.cc @@ -850,6 +850,7 @@ struct CmdProfileDiffClosures : virtual StoreCommand, MixDefaultProfile printClosureDiff(store, store->followLinksToStorePath(prevGen->path), store->followLinksToStorePath(gen.path), + false, " "); }