From 1425aa0b7cd0d3477589f75bea4fb9c74e057fed Mon Sep 17 00:00:00 2001 From: Qyriad Date: Tue, 30 Apr 2024 18:11:14 -0600 Subject: [PATCH 1/7] implement parsing human-readable names from URLs Based off of commit 257b768436a0e8ab7887f9b790c5b92a7fe51ef5 Upstream-PR: https://github.com/NixOS/nix/pull/8678 Co-authored-by: Felix Uhl Change-Id: Idcb7f6191ca3310ef9dc854197f7798260c3f71d --- src/libutil/meson.build | 2 + src/libutil/url-name.cc | 59 ++++++++++++++++++++++++++++++ src/libutil/url-name.hh | 26 +++++++++++++ src/libutil/url-parts.hh | 1 + src/libutil/url.cc | 2 +- tests/unit/libutil/url-name.cc | 67 ++++++++++++++++++++++++++++++++++ tests/unit/meson.build | 1 + 7 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/libutil/url-name.cc create mode 100644 src/libutil/url-name.hh create mode 100644 tests/unit/libutil/url-name.cc diff --git a/src/libutil/meson.build b/src/libutil/meson.build index 069798a6f..8caa0532a 100644 --- a/src/libutil/meson.build +++ b/src/libutil/meson.build @@ -31,6 +31,7 @@ libutil_sources = files( 'tarfile.cc', 'thread-pool.cc', 'url.cc', + 'url-name.cc', 'util.cc', 'xml-writer.cc', ) @@ -92,6 +93,7 @@ libutil_headers = files( 'topo-sort.hh', 'types.hh', 'url-parts.hh', + 'url-name.hh', 'url.hh', 'util.hh', 'variant-wrapper.hh', diff --git a/src/libutil/url-name.cc b/src/libutil/url-name.cc new file mode 100644 index 000000000..6ef58c80a --- /dev/null +++ b/src/libutil/url-name.cc @@ -0,0 +1,59 @@ +#include +#include + +#include "url-name.hh" + +namespace nix { + +static std::string const attributeNamePattern("[a-z0-9_-]+"); +static std::regex const lastAttributeRegex("(?:" + attributeNamePattern + "\\.)*(?!default)(" + attributeNamePattern +")(\\^.*)?"); +static std::string const pathSegmentPattern("[a-zA-Z0-9_-]+"); +static std::regex const lastPathSegmentRegex(".*/(" + pathSegmentPattern +")"); +static std::regex const secondPathSegmentRegex("(?:" + pathSegmentPattern + ")/(" + pathSegmentPattern +")(?:/.*)?"); +static std::regex const gitProviderRegex("github|gitlab|sourcehut"); +static std::regex const gitSchemeRegex("git($|\\+.*)"); +static std::regex const defaultOutputRegex(".*\\.default($|\\^.*)"); + +std::optional getNameFromURL(ParsedURL const & url) +{ + std::smatch match; + + /* If there is a dir= argument, use its value */ + if (url.query.count("dir") > 0) { + return url.query.at("dir"); + } + + /* If the fragment isn't a "default" and contains two attribute elements, use the last one */ + if (std::regex_match(url.fragment, match, lastAttributeRegex)) { + return match.str(1); + } + + /* If this is a github/gitlab/sourcehut flake, use the repo name */ + if ( + std::regex_match(url.scheme, gitProviderRegex) + && std::regex_match(url.path, match, secondPathSegmentRegex) + ) { + return match.str(1); + } + + /* If it is a regular git flake, use the directory name */ + if ( + std::regex_match(url.scheme, gitSchemeRegex) + && std::regex_match(url.path, match, lastPathSegmentRegex) + ) { + return match.str(1); + } + + /* If everything failed but there is a non-default fragment, use it in full */ + if (!url.fragment.empty() && !std::regex_match(url.fragment, defaultOutputRegex)) + return url.fragment; + + /* If there is no fragment, take the last element of the path */ + if (std::regex_match(url.path, match, lastPathSegmentRegex)) + return match.str(1); + + /* If even that didn't work, the URL does not contain enough info to determine a useful name */ + return {}; +} + +} diff --git a/src/libutil/url-name.hh b/src/libutil/url-name.hh new file mode 100644 index 000000000..3a3f88e76 --- /dev/null +++ b/src/libutil/url-name.hh @@ -0,0 +1,26 @@ +#pragma once +///@file url-name.hh, for some hueristic-ish URL parsing. + +#include +#include + +#include "url.hh" +#include "url-parts.hh" +#include "util.hh" +#include "split.hh" + +namespace nix { + +/** + * Try to extract a reasonably unique and meaningful, human-readable + * name of a flake output from a parsed URL. + * When nullopt is returned, the callsite should use information available + * to it outside of the URL to determine a useful name. + * This is a heuristic approach intended for user interfaces. + * @return nullopt if the extracted name is not useful to identify a + * flake output, for example because it is empty or "default". + * Otherwise returns the extracted name. + */ +std::optional getNameFromURL(ParsedURL const & url); + +} diff --git a/src/libutil/url-parts.hh b/src/libutil/url-parts.hh index 6255c1d02..6efcc7e50 100644 --- a/src/libutil/url-parts.hh +++ b/src/libutil/url-parts.hh @@ -19,6 +19,7 @@ const static std::string userRegex = "(?:(?:" + unreservedRegex + "|" + pctEncod const static std::string authorityRegex = "(?:" + userRegex + "@)?" + hostRegex + "(?::[0-9]+)?"; const static std::string pcharRegex = "(?:" + unreservedRegex + "|" + pctEncoded + "|" + subdelimsRegex + "|[:@])"; const static std::string queryRegex = "(?:" + pcharRegex + "|[/? \"])*"; +const static std::string fragmentRegex = "(?:" + pcharRegex + "|[/? \"^])*"; const static std::string segmentRegex = "(?:" + pcharRegex + "*)"; const static std::string absPathRegex = "(?:(?:/" + segmentRegex + ")*/?)"; const static std::string pathRegex = "(?:" + segmentRegex + "(?:/" + segmentRegex + ")*/?)"; diff --git a/src/libutil/url.cc b/src/libutil/url.cc index a8f7d39fd..afccc4245 100644 --- a/src/libutil/url.cc +++ b/src/libutil/url.cc @@ -16,7 +16,7 @@ ParsedURL parseURL(const std::string & url) "((" + schemeRegex + "):" + "(?:(?://(" + authorityRegex + ")(" + absPathRegex + "))|(/?" + pathRegex + ")))" + "(?:\\?(" + queryRegex + "))?" - + "(?:#(" + queryRegex + "))?", + + "(?:#(" + fragmentRegex + "))?", std::regex::ECMAScript); std::smatch match; diff --git a/tests/unit/libutil/url-name.cc b/tests/unit/libutil/url-name.cc new file mode 100644 index 000000000..f637efa89 --- /dev/null +++ b/tests/unit/libutil/url-name.cc @@ -0,0 +1,67 @@ +#include "url-name.hh" +#include + +namespace nix { + +/* ----------- tests for url-name.hh --------------------------------------------------*/ + + TEST(getNameFromURL, getsNameFromURL) { + ASSERT_EQ(getNameFromURL(parseURL("path:/home/user/project")), "project"); + ASSERT_EQ(getNameFromURL(parseURL("path:~/repos/nixpkgs#packages.x86_64-linux.hello")), "hello"); + ASSERT_EQ(getNameFromURL(parseURL("path:.#nonStandardAttr.mylaptop")), "nonStandardAttr.mylaptop"); + ASSERT_EQ(getNameFromURL(parseURL("path:./repos/myflake#nonStandardAttr.mylaptop")), "nonStandardAttr.mylaptop"); + ASSERT_EQ(getNameFromURL(parseURL("path:./nixpkgs#packages.x86_64-linux.complex^bin,man")), "complex"); + ASSERT_EQ(getNameFromURL(parseURL("path:./myproj#packages.x86_64-linux.default^*")), "myproj"); + + ASSERT_EQ(getNameFromURL(parseURL("github:NixOS/nixpkgs#packages.x86_64-linux.hello")), "hello"); + ASSERT_EQ(getNameFromURL(parseURL("github:NixOS/nixpkgs#hello")), "hello"); + ASSERT_EQ(getNameFromURL(parseURL("github:NixOS/nix#packages.x86_64-linux.default")), "nix"); + ASSERT_EQ(getNameFromURL(parseURL("github:NixOS/nix#")), "nix"); + ASSERT_EQ(getNameFromURL(parseURL("github:NixOS/nix")), "nix"); + ASSERT_EQ(getNameFromURL(parseURL("github:cachix/devenv/main#packages.x86_64-linux.default")), "devenv"); + ASSERT_EQ(getNameFromURL(parseURL("github:edolstra/nix-warez?rev=1234&dir=blender&ref=master")), "blender"); + + ASSERT_EQ(getNameFromURL(parseURL("gitlab:NixOS/nixpkgs#packages.x86_64-linux.hello")), "hello"); + ASSERT_EQ(getNameFromURL(parseURL("gitlab:NixOS/nixpkgs#hello")), "hello"); + ASSERT_EQ(getNameFromURL(parseURL("gitlab:NixOS/nix#packages.x86_64-linux.default")), "nix"); + ASSERT_EQ(getNameFromURL(parseURL("gitlab:NixOS/nix#")), "nix"); + ASSERT_EQ(getNameFromURL(parseURL("gitlab:NixOS/nix")), "nix"); + ASSERT_EQ(getNameFromURL(parseURL("gitlab:cachix/devenv/main#packages.x86_64-linux.default")), "devenv"); + + ASSERT_EQ(getNameFromURL(parseURL("sourcehut:NixOS/nixpkgs#packages.x86_64-linux.hello")), "hello"); + ASSERT_EQ(getNameFromURL(parseURL("sourcehut:NixOS/nixpkgs#hello")), "hello"); + ASSERT_EQ(getNameFromURL(parseURL("sourcehut:NixOS/nix#packages.x86_64-linux.default")), "nix"); + ASSERT_EQ(getNameFromURL(parseURL("sourcehut:NixOS/nix#")), "nix"); + ASSERT_EQ(getNameFromURL(parseURL("sourcehut:NixOS/nix")), "nix"); + ASSERT_EQ(getNameFromURL(parseURL("sourcehut:cachix/devenv/main#packages.x86_64-linux.default")), "devenv"); + + ASSERT_EQ(getNameFromURL(parseURL("git://github.com/edolstra/dwarffs")), "dwarffs"); + ASSERT_EQ(getNameFromURL(parseURL("git://github.com/edolstra/nix-warez?dir=blender")), "blender"); + ASSERT_EQ(getNameFromURL(parseURL("git+file:///home/user/project")), "project"); + ASSERT_EQ(getNameFromURL(parseURL("git+file:///home/user/project?ref=fa1e2d23a22")), "project"); + ASSERT_EQ(getNameFromURL(parseURL("git+ssh://git@github.com/someuser/my-repo#")), "my-repo"); + ASSERT_EQ(getNameFromURL(parseURL("git+git://github.com/someuser/my-repo?rev=v1.2.3")), "my-repo"); + ASSERT_EQ(getNameFromURL(parseURL("git+ssh:///home/user/project?dir=subproject&rev=v2.4")), "subproject"); + ASSERT_EQ(getNameFromURL(parseURL("git+http://not-even-real#packages.x86_64-linux.hello")), "hello"); + ASSERT_EQ(getNameFromURL(parseURL("git+https://not-even-real#packages.aarch64-darwin.hello")), "hello"); + + ASSERT_EQ(getNameFromURL(parseURL("tarball+http://github.com/NixOS/nix/archive/refs/tags/2.18.1#packages.x86_64-linux.jq")), "jq"); + ASSERT_EQ(getNameFromURL(parseURL("tarball+https://github.com/NixOS/nix/archive/refs/tags/2.18.1#packages.x86_64-linux.hg")), "hg"); + ASSERT_EQ(getNameFromURL(parseURL("tarball+file:///home/user/Downloads/nixpkgs-2.18.1#packages.aarch64-darwin.ripgrep")), "ripgrep"); + + ASSERT_EQ(getNameFromURL(parseURL("https://github.com/NixOS/nix/archive/refs/tags/2.18.1.tar.gz#packages.x86_64-linux.pv")), "pv"); + ASSERT_EQ(getNameFromURL(parseURL("http://github.com/NixOS/nix/archive/refs/tags/2.18.1.tar.gz#packages.x86_64-linux.pv")), "pv"); + + ASSERT_EQ(getNameFromURL(parseURL("file:///home/user/project?ref=fa1e2d23a22")), "project"); + ASSERT_EQ(getNameFromURL(parseURL("file+file:///home/user/project?ref=fa1e2d23a22")), "project"); + ASSERT_EQ(getNameFromURL(parseURL("file+http://not-even-real#packages.x86_64-linux.hello")), "hello"); + ASSERT_EQ(getNameFromURL(parseURL("file+http://gitfantasy.com/org/user/notaflake")), "notaflake"); + ASSERT_EQ(getNameFromURL(parseURL("file+https://not-even-real#packages.aarch64-darwin.hello")), "hello"); + + ASSERT_EQ(getNameFromURL(parseURL("https://www.github.com/")), std::nullopt); + ASSERT_EQ(getNameFromURL(parseURL("path:.")), std::nullopt); + ASSERT_EQ(getNameFromURL(parseURL("file:.#")), std::nullopt); + ASSERT_EQ(getNameFromURL(parseURL("path:.#packages.x86_64-linux.default")), std::nullopt); + ASSERT_EQ(getNameFromURL(parseURL("path:.#packages.x86_64-linux.default^*")), std::nullopt); + } +} diff --git a/tests/unit/meson.build b/tests/unit/meson.build index ae850df47..339ac9a4a 100644 --- a/tests/unit/meson.build +++ b/tests/unit/meson.build @@ -52,6 +52,7 @@ libutil_tests_sources = files( 'libutil/suggestions.cc', 'libutil/tests.cc', 'libutil/url.cc', + 'libutil/url-name.cc', 'libutil/xml-writer.cc', ) From f88423813f042cf40d9207409cd05cf4b75d87a0 Mon Sep 17 00:00:00 2001 From: Qyriad Date: Wed, 1 May 2024 18:51:16 -0600 Subject: [PATCH 2/7] nix3-profile: allow using human-readable names to select packages These names are parsed from the URL provided for that package Based off of commit 257b768436a0e8ab7887f9b790c5b92a7fe51ef5 Upstream-PR: https://github.com/NixOS/nix/pull/8678 Co-authored-by: Felix Uhl Change-Id: I76d5f9cfb11d3d2915b3dd1db21d7bb49e91f4fb --- doc/manual/rl-next/nix-profile-names.md | 7 + src/libcmd/cmd-profiles.cc | 26 ++++ src/libcmd/cmd-profiles.hh | 3 + src/nix/profile-list.md | 9 +- src/nix/profile-remove.md | 11 +- src/nix/profile-upgrade.md | 11 +- src/nix/profile.cc | 170 ++++++++++++++++-------- tests/functional/nix-profile.sh | 20 ++- 8 files changed, 186 insertions(+), 71 deletions(-) create mode 100644 doc/manual/rl-next/nix-profile-names.md diff --git a/doc/manual/rl-next/nix-profile-names.md b/doc/manual/rl-next/nix-profile-names.md new file mode 100644 index 000000000..0db17b21e --- /dev/null +++ b/doc/manual/rl-next/nix-profile-names.md @@ -0,0 +1,7 @@ +--- +synopsis: "`nix profile` now allows referring to elements by human-readable name" +prs: 8678 +cls: 978 +--- + +[`nix profile`](@docroot@/command-ref/new-cli/nix3-profile.md) now uses names to refer to installed packages when running [`list`](@docroot@/command-ref/new-cli/nix3-profile-list.md), [`remove`](@docroot@/command-ref/new-cli/nix3-profile-remove.md) or [`upgrade`](@docroot@/command-ref/new-cli/nix3-profile-upgrade.md) as opposed to indices. Indices are deprecated and will be removed in a future version. diff --git a/src/libcmd/cmd-profiles.cc b/src/libcmd/cmd-profiles.cc index 4d8ff7438..5fa7f902c 100644 --- a/src/libcmd/cmd-profiles.cc +++ b/src/libcmd/cmd-profiles.cc @@ -1,6 +1,9 @@ +#include + #include "cmd-profiles.hh" #include "built-path.hh" #include "builtins/buildenv.hh" +#include "logging.hh" #include "names.hh" #include "store-api.hh" @@ -106,6 +109,8 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile) if (pathExists(manifestPath)) { auto json = nlohmann::json::parse(readFile(manifestPath)); + // Keep track of already found names so we can prevent duplicates. + std::set foundNames; auto version = json.value("version", 0); std::string sUrl; @@ -139,6 +144,26 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile) e["attrPath"], e["outputs"].get()}; } + + std::string nameCandidate(element.identifier()); + + if (e.contains("name")) { + nameCandidate = e["name"]; + } else if (element.source) { + auto const url = parseURL(element.source->to_string()); + auto const name = getNameFromURL(url); + if (name) { + nameCandidate = *name; + } + } + + auto finalName = nameCandidate; + for (unsigned appendedIndex = 1; foundNames.contains(finalName); ++appendedIndex) { + finalName = nameCandidate + std::to_string(appendedIndex); + } + element.name = finalName; + foundNames.insert(element.name); + elements.emplace_back(std::move(element)); } } else if (pathExists(profile + "/manifest.nix")) { @@ -151,6 +176,7 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile) for (auto & drvInfo : drvInfos) { ProfileElement element; element.storePaths = {drvInfo.queryOutPath()}; + element.name = element.identifier(); elements.emplace_back(std::move(element)); } } diff --git a/src/libcmd/cmd-profiles.hh b/src/libcmd/cmd-profiles.hh index d03f9b1c2..7f2c8a76f 100644 --- a/src/libcmd/cmd-profiles.hh +++ b/src/libcmd/cmd-profiles.hh @@ -6,6 +6,8 @@ #include "flake/flakeref.hh" #include "get-drvs.hh" #include "types.hh" +#include "url.hh" +#include "url-name.hh" #include #include @@ -33,6 +35,7 @@ constexpr int DEFAULT_PRIORITY = 5; struct ProfileElement { StorePathSet storePaths; + std::string name; std::optional source; bool active = true; int priority = DEFAULT_PRIORITY; diff --git a/src/nix/profile-list.md b/src/nix/profile-list.md index 5d7fcc0ec..d807a69c6 100644 --- a/src/nix/profile-list.md +++ b/src/nix/profile-list.md @@ -6,6 +6,7 @@ R""( ```console # nix profile list + Name: gdb Index: 0 Flake attribute: legacyPackages.x86_64-linux.gdb Original flake URL: flake:nixpkgs @@ -13,6 +14,7 @@ R""( Store paths: /nix/store/indzcw5wvlhx6vwk7k4iq29q15chvr3d-gdb-11.1 Index: 1 + Name: blender-bin Flake attribute: packages.x86_64-linux.default Original flake URL: flake:blender-bin Locked flake URL: github:edolstra/nix-warez/91f2ffee657bf834e4475865ae336e2379282d34?dir=blender @@ -26,7 +28,7 @@ R""( # nix build github:edolstra/nix-warez/91f2ffee657bf834e4475865ae336e2379282d34?dir=blender#packages.x86_64-linux.default ``` - will build the package with index 1 shown above. + will build the package with name blender-bin shown above. # Description @@ -34,10 +36,13 @@ This command shows what packages are currently installed in a profile. For each installed package, it shows the following information: -* `Index`: An integer that can be used to unambiguously identify the +* `Name`: A unique name used to unambiguously identify the package in invocations of `nix profile remove` and `nix profile upgrade`. +* `Index`: An integer that can be used to unambiguously identify the package in invocations of `nix profile remove` and `nix profile upgrade`. + (*Deprecated, will be removed in a future version in favor of `Name`.*) + * `Flake attribute`: The flake output attribute path that provides the package (e.g. `packages.x86_64-linux.hello`). diff --git a/src/nix/profile-remove.md b/src/nix/profile-remove.md index ba85441d8..c76f4b09c 100644 --- a/src/nix/profile-remove.md +++ b/src/nix/profile-remove.md @@ -2,10 +2,17 @@ R""( # Examples -* Remove a package by position: +* Remove a package by name: ```console - # nix profile remove 3 + # nix profile remove hello + ``` + +* Remove a package by index + *(deprecated, will be removed in a future version)*: + + ```console + $ nix profile remove 3 ``` * Remove a package by attribute path: diff --git a/src/nix/profile-upgrade.md b/src/nix/profile-upgrade.md index 39cca428b..b13cb66bb 100644 --- a/src/nix/profile-upgrade.md +++ b/src/nix/profile-upgrade.md @@ -9,18 +9,19 @@ R""( # nix profile upgrade '.*' ``` -* Upgrade a specific package: +* Upgrade a specific package by name: + + ```console + # nix profile upgrade hello + ``` ```console # nix profile upgrade packages.x86_64-linux.hello ``` -* Upgrade a specific profile element by number: +* Upgrade a specific package by index: ```console - # nix profile list - 0 flake:nixpkgs#legacyPackages.x86_64-linux.spotify … - # nix profile upgrade 0 ``` diff --git a/src/nix/profile.cc b/src/nix/profile.cc index 67f97ca9b..f702c7c06 100644 --- a/src/nix/profile.cc +++ b/src/nix/profile.cc @@ -189,13 +189,24 @@ public: { std::vector res; + auto anyIndexMatchers = false; + for (auto & s : _matchers) { - if (auto n = string2Int(s)) + if (auto n = string2Int(s)) { res.push_back(*n); - else if (store->isStorePath(s)) + anyIndexMatchers = true; + } else if (store->isStorePath(s)) { res.push_back(s); - else + } else { res.push_back(RegexPattern{s,std::regex(s, std::regex::extended | std::regex::icase)}); + } + } + + if (anyIndexMatchers) { + warn( + "Indices are deprecated and be removed in a future version!\n" + " Refer to packages by their `Name` printed by `nix profile list`.\n" + ); } return res; @@ -206,12 +217,13 @@ public: for (auto & matcher : matchers) { if (auto n = std::get_if(&matcher)) { if (*n == pos) return true; + } else if (auto path = std::get_if(&matcher)) { if (element.storePaths.count(store.parseStorePath(*path))) return true; } else if (auto regex = std::get_if(&matcher)) { - if (element.source - && std::regex_match(element.source->attrPath, regex->reg)) + if (std::regex_match(element.name, regex->reg)) { return true; + } } } @@ -294,62 +306,100 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf Installables installables; std::vector indices; + auto matchedCount = 0; auto upgradedCount = 0; for (size_t i = 0; i < manifest.elements.size(); ++i) { auto & element(manifest.elements[i]); - if (element.source - && !element.source->originalRef.input.isLocked() - && matches(*store, element, i, matchers)) - { - upgradedCount++; - - Activity act(*logger, lvlChatty, actUnknown, - fmt("checking '%s' for updates", element.source->attrPath)); - - auto installable = make_ref( - this, - getEvalState(), - FlakeRef(element.source->originalRef), - "", - element.source->outputs, - Strings{element.source->attrPath}, - Strings{}, - lockFlags); - - auto derivedPaths = installable->toDerivedPaths(); - if (derivedPaths.empty()) continue; - auto * infop = dynamic_cast(&*derivedPaths[0].info); - // `InstallableFlake` should use `ExtraPathInfoFlake`. - assert(infop); - auto & info = *infop; - - if (element.source->lockedRef == info.flake.lockedRef) continue; - - printInfo("upgrading '%s' from flake '%s' to '%s'", - element.source->attrPath, element.source->lockedRef, info.flake.lockedRef); - - element.source = ProfileElementSource { - .originalRef = installable->flakeRef, - .lockedRef = info.flake.lockedRef, - .attrPath = info.value.attrPath, - .outputs = installable->extendedOutputsSpec, - }; - - installables.push_back(installable); - indices.push_back(i); + if (!matches(*store, element, i, matchers)) { + continue; } + + matchedCount += 1; + + if (!element.source) { + warn( + "Found package '%s', but it was not installed from a flake, so it can't be checked for upgrades", + element.identifier() + ); + continue; + } + + if (element.source->originalRef.input.isLocked()) { + warn( + "Found package '%s', but it was installed from a locked flake reference so it can't be upgraded", + element.identifier() + ); + continue; + } + + upgradedCount++; + + Activity act( + *logger, + lvlChatty, + actUnknown, + fmt("checking '%s' for updates", element.source->attrPath), + Logger::Fields{element.source->attrPath} + ); + + auto installable = make_ref( + this, + getEvalState(), + FlakeRef(element.source->originalRef), + "", + element.source->outputs, + Strings{element.source->attrPath}, + Strings{}, + lockFlags + ); + + auto derivedPaths = installable->toDerivedPaths(); + if (derivedPaths.empty()) { + continue; + } + + auto * infop = dynamic_cast(&*derivedPaths[0].info); + // `InstallableFlake` should use `ExtraPathInfoFlake`. + assert(infop); + auto & info = *infop; + + if (element.source->lockedRef == info.flake.lockedRef) { + continue; + } + + printInfo( + "upgrading '%s' from flake '%s' to '%s'", + element.source->attrPath, + element.source->lockedRef, + info.flake.lockedRef + ); + + element.source = ProfileElementSource { + .originalRef = installable->flakeRef, + .lockedRef = info.flake.lockedRef, + .attrPath = info.value.attrPath, + .outputs = installable->extendedOutputsSpec, + }; + + installables.push_back(installable); + indices.push_back(i); + } if (upgradedCount == 0) { - for (auto & matcher : matchers) { - if (const size_t * index = std::get_if(&matcher)){ - warn("'%d' is not a valid index", *index); - } else if (const Path * path = std::get_if(&matcher)){ - warn("'%s' does not match any paths", *path); - } else if (const RegexPattern * regex = std::get_if(&matcher)){ - warn("'%s' does not match any packages", regex->pattern); + if (matchedCount == 0) { + for (auto & matcher : matchers) { + if (const size_t * index = std::get_if(&matcher)){ + warn("'%d' is not a valid index", *index); + } else if (const Path * path = std::get_if(&matcher)){ + warn("'%s' does not match any paths", *path); + } else if (const RegexPattern * regex = std::get_if(&matcher)){ + warn("'%s' does not match any packages", regex->pattern); + } } + } else { + warn("Found some packages but none of them could be upgraded"); } warn ("Use 'nix profile list' to see the current profile."); } @@ -394,10 +444,18 @@ struct CmdProfileList : virtual EvalCommand, virtual StoreCommand, MixDefaultPro } else { for (size_t i = 0; i < manifest.elements.size(); ++i) { auto & element(manifest.elements[i]); - if (i) logger->cout(""); - logger->cout("Index: " ANSI_BOLD "%s" ANSI_NORMAL "%s", - i, - element.active ? "" : " " ANSI_RED "(inactive)" ANSI_NORMAL); + if (i) { + logger->cout(""); + } + logger->cout( + "Name: " ANSI_BOLD "%s" ANSI_NORMAL "%s", + element.name, + element.active ? "" : " " ANSI_RED "(inactive)" ANSI_NORMAL + ); + logger->cout( + "Index: " ANSI_BOLD "%s" ANSI_NORMAL "%S", + i + ); if (element.source) { logger->cout("Flake attribute: %s%s", element.source->attrPath, element.source->outputs.to_string()); logger->cout("Original flake URL: %s", element.source->originalRef.to_string()); diff --git a/tests/functional/nix-profile.sh b/tests/functional/nix-profile.sh index 7c478a0cd..b19341851 100644 --- a/tests/functional/nix-profile.sh +++ b/tests/functional/nix-profile.sh @@ -47,7 +47,7 @@ cp ./config.nix $flake1Dir/ # Test upgrading from nix-env. nix-env -f ./user-envs.nix -i foo-1.0 -nix profile list | grep -A2 'Index:.*0' | grep 'Store paths:.*foo-1.0' +nix profile list | grep -A2 'Name:.*foo' | grep 'Store paths:.*foo-1.0' nix profile install $flake1Dir -L nix profile list | grep -A4 'Index:.*1' | grep 'Locked flake URL:.*narHash' [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello World" ]] @@ -81,7 +81,7 @@ nix profile rollback # Test uninstall. [ -e $TEST_HOME/.nix-profile/bin/foo ] -nix profile remove 0 +nix profile remove "foo" (! [ -e $TEST_HOME/.nix-profile/bin/foo ]) nix profile history | grep 'foo: 1.0 -> ∅' nix profile diff-closures | grep 'Version 3 -> 4' @@ -93,6 +93,13 @@ nix profile remove 1 nix profile install $(nix-build --no-out-link ./simple.nix) [[ $(cat $TEST_HOME/.nix-profile/hello) = "Hello World!" ]] +# Test packages with same name from different sources +mkdir $TEST_ROOT/simple-too +cp ./simple.nix ./config.nix simple.builder.sh $TEST_ROOT/simple-too +nix profile install --file $TEST_ROOT/simple-too/simple.nix '' +nix profile list | grep -A4 'Name:.*simple' | grep 'Name:.*simple1' +nix profile remove simple1 + # Test wipe-history. nix profile wipe-history [[ $(nix profile history | grep Version | wc -l) -eq 1 ]] @@ -104,7 +111,7 @@ nix profile upgrade 0 nix profile history | grep "packages.$system.default: 1.0, 1.0-man -> 3.0, 3.0-man" # Test new install of CA package. -nix profile remove 0 +nix profile remove flake1 printf 4.0 > $flake1Dir/version printf Utrecht > $flake1Dir/who nix profile install $flake1Dir @@ -112,19 +119,20 @@ nix profile install $flake1Dir [[ $(nix path-info --json $(realpath $TEST_HOME/.nix-profile/bin/hello) | jq -r .[].ca) =~ fixed:r:sha256: ]] # Override the outputs. -nix profile remove 0 1 +nix profile remove simple flake1 nix profile install "$flake1Dir^*" [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello Utrecht" ]] [ -e $TEST_HOME/.nix-profile/share/man ] [ -e $TEST_HOME/.nix-profile/include ] printf Nix > $flake1Dir/who -nix profile upgrade 0 +nix profile list +nix profile upgrade flake1 [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello Nix" ]] [ -e $TEST_HOME/.nix-profile/share/man ] [ -e $TEST_HOME/.nix-profile/include ] -nix profile remove 0 +nix profile remove flake1 nix profile install "$flake1Dir^man" (! [ -e $TEST_HOME/.nix-profile/bin/hello ]) [ -e $TEST_HOME/.nix-profile/share/man ] From e98fc952a81f89c86e871120aac6272dccdc83b9 Mon Sep 17 00:00:00 2001 From: Qyriad Date: Wed, 1 May 2024 19:12:04 -0600 Subject: [PATCH 3/7] nix3-profile: remove indices Based off of commit 3187bc9ac3dd193b9329ef68c73ac3cca794ed78 Upstream-PR: https://github.com/NixOS/nix/pull/9656 Co-authored-by: Eelco Dolstra Change-Id: I8ac4a33314cd1cf9de95404c20f58e883460acc7 --- doc/manual/rl-next/nix-profile-names.md | 4 +-- src/nix/profile-list.md | 5 ---- src/nix/profile-remove.md | 7 ----- src/nix/profile-upgrade.md | 6 ---- src/nix/profile.cc | 39 ++++++------------------- tests/functional/nix-profile.sh | 12 ++++---- 6 files changed, 17 insertions(+), 56 deletions(-) diff --git a/doc/manual/rl-next/nix-profile-names.md b/doc/manual/rl-next/nix-profile-names.md index 0db17b21e..2a37691e6 100644 --- a/doc/manual/rl-next/nix-profile-names.md +++ b/doc/manual/rl-next/nix-profile-names.md @@ -1,7 +1,7 @@ --- -synopsis: "`nix profile` now allows referring to elements by human-readable name" +synopsis: "`nix profile` now allows referring to elements by human-readable name, and no longer accepts indices" prs: 8678 cls: 978 --- -[`nix profile`](@docroot@/command-ref/new-cli/nix3-profile.md) now uses names to refer to installed packages when running [`list`](@docroot@/command-ref/new-cli/nix3-profile-list.md), [`remove`](@docroot@/command-ref/new-cli/nix3-profile-remove.md) or [`upgrade`](@docroot@/command-ref/new-cli/nix3-profile-upgrade.md) as opposed to indices. Indices are deprecated and will be removed in a future version. +[`nix profile`](@docroot@/command-ref/new-cli/nix3-profile.md) now uses names to refer to installed packages when running [`list`](@docroot@/command-ref/new-cli/nix3-profile-list.md), [`remove`](@docroot@/command-ref/new-cli/nix3-profile-remove.md) or [`upgrade`](@docroot@/command-ref/new-cli/nix3-profile-upgrade.md) as opposed to indices. Indices have been removed. diff --git a/src/nix/profile-list.md b/src/nix/profile-list.md index d807a69c6..9baea4ada 100644 --- a/src/nix/profile-list.md +++ b/src/nix/profile-list.md @@ -7,13 +7,11 @@ R""( ```console # nix profile list Name: gdb - Index: 0 Flake attribute: legacyPackages.x86_64-linux.gdb Original flake URL: flake:nixpkgs Locked flake URL: github:NixOS/nixpkgs/7b38b03d76ab71bdc8dc325e3f6338d984cc35ca Store paths: /nix/store/indzcw5wvlhx6vwk7k4iq29q15chvr3d-gdb-11.1 - Index: 1 Name: blender-bin Flake attribute: packages.x86_64-linux.default Original flake URL: flake:blender-bin @@ -40,9 +38,6 @@ information: package in invocations of `nix profile remove` and `nix profile upgrade`. -* `Index`: An integer that can be used to unambiguously identify the package in invocations of `nix profile remove` and `nix profile upgrade`. - (*Deprecated, will be removed in a future version in favor of `Name`.*) - * `Flake attribute`: The flake output attribute path that provides the package (e.g. `packages.x86_64-linux.hello`). diff --git a/src/nix/profile-remove.md b/src/nix/profile-remove.md index c76f4b09c..81f7e513a 100644 --- a/src/nix/profile-remove.md +++ b/src/nix/profile-remove.md @@ -8,13 +8,6 @@ R""( # nix profile remove hello ``` -* Remove a package by index - *(deprecated, will be removed in a future version)*: - - ```console - $ nix profile remove 3 - ``` - * Remove a package by attribute path: ```console diff --git a/src/nix/profile-upgrade.md b/src/nix/profile-upgrade.md index b13cb66bb..57983085f 100644 --- a/src/nix/profile-upgrade.md +++ b/src/nix/profile-upgrade.md @@ -19,12 +19,6 @@ R""( # nix profile upgrade packages.x86_64-linux.hello ``` -* Upgrade a specific package by index: - - ```console - # nix profile upgrade 0 - ``` - # Description This command upgrades a previously installed package in a Nix profile, diff --git a/src/nix/profile.cc b/src/nix/profile.cc index f702c7c06..131abb258 100644 --- a/src/nix/profile.cc +++ b/src/nix/profile.cc @@ -183,18 +183,15 @@ public: std::string pattern; std::regex reg; }; - typedef std::variant Matcher; + using Matcher = std::variant; std::vector getMatchers(ref store) { std::vector res; - auto anyIndexMatchers = false; - for (auto & s : _matchers) { if (auto n = string2Int(s)) { - res.push_back(*n); - anyIndexMatchers = true; + throw Error("'nix profile' no longer supports indices ('%d')", *n); } else if (store->isStorePath(s)) { res.push_back(s); } else { @@ -202,23 +199,13 @@ public: } } - if (anyIndexMatchers) { - warn( - "Indices are deprecated and be removed in a future version!\n" - " Refer to packages by their `Name` printed by `nix profile list`.\n" - ); - } - return res; } - bool matches(const Store & store, const ProfileElement & element, size_t pos, const std::vector & matchers) + bool matches(const Store & store, const ProfileElement & element, const std::vector & matchers) { for (auto & matcher : matchers) { - if (auto n = std::get_if(&matcher)) { - if (*n == pos) return true; - - } else if (auto path = std::get_if(&matcher)) { + if (auto path = std::get_if(&matcher)) { if (element.storePaths.count(store.parseStorePath(*path))) return true; } else if (auto regex = std::get_if(&matcher)) { if (std::regex_match(element.name, regex->reg)) { @@ -255,7 +242,7 @@ struct CmdProfileRemove : virtual EvalCommand, MixDefaultProfile, MixProfileElem for (size_t i = 0; i < oldManifest.elements.size(); ++i) { auto & element(oldManifest.elements[i]); - if (!matches(*store, element, i, matchers)) { + if (!matches(*store, element, matchers)) { newManifest.elements.push_back(std::move(element)); } else { notice("removing '%s'", element.identifier()); @@ -269,9 +256,7 @@ struct CmdProfileRemove : virtual EvalCommand, MixDefaultProfile, MixProfileElem if (removedCount == 0) { for (auto matcher: matchers) { - if (const size_t * index = std::get_if(&matcher)){ - warn("'%d' is not a valid index", *index); - } else if (const Path * path = std::get_if(&matcher)){ + if (const Path * path = std::get_if(&matcher)) { warn("'%s' does not match any paths", *path); } else if (const RegexPattern * regex = std::get_if(&matcher)){ warn("'%s' does not match any packages", regex->pattern); @@ -311,7 +296,7 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf for (size_t i = 0; i < manifest.elements.size(); ++i) { auto & element(manifest.elements[i]); - if (!matches(*store, element, i, matchers)) { + if (!matches(*store, element, matchers)) { continue; } @@ -390,11 +375,9 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf if (upgradedCount == 0) { if (matchedCount == 0) { for (auto & matcher : matchers) { - if (const size_t * index = std::get_if(&matcher)){ - warn("'%d' is not a valid index", *index); - } else if (const Path * path = std::get_if(&matcher)){ + if (const Path * path = std::get_if(&matcher)){ warn("'%s' does not match any paths", *path); - } else if (const RegexPattern * regex = std::get_if(&matcher)){ + } else if (const RegexPattern * regex = std::get_if(&matcher)) { warn("'%s' does not match any packages", regex->pattern); } } @@ -452,10 +435,6 @@ struct CmdProfileList : virtual EvalCommand, virtual StoreCommand, MixDefaultPro element.name, element.active ? "" : " " ANSI_RED "(inactive)" ANSI_NORMAL ); - logger->cout( - "Index: " ANSI_BOLD "%s" ANSI_NORMAL "%S", - i - ); if (element.source) { logger->cout("Flake attribute: %s%s", element.source->attrPath, element.source->outputs.to_string()); logger->cout("Original flake URL: %s", element.source->originalRef.to_string()); diff --git a/tests/functional/nix-profile.sh b/tests/functional/nix-profile.sh index b19341851..387e73b2a 100644 --- a/tests/functional/nix-profile.sh +++ b/tests/functional/nix-profile.sh @@ -49,7 +49,7 @@ cp ./config.nix $flake1Dir/ nix-env -f ./user-envs.nix -i foo-1.0 nix profile list | grep -A2 'Name:.*foo' | grep 'Store paths:.*foo-1.0' nix profile install $flake1Dir -L -nix profile list | grep -A4 'Index:.*1' | grep 'Locked flake URL:.*narHash' +nix profile list | grep -A4 'Name:.*flake1' | grep 'Locked flake URL:.*narHash' [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello World" ]] [ -e $TEST_HOME/.nix-profile/share/man ] (! [ -e $TEST_HOME/.nix-profile/include ]) @@ -60,7 +60,7 @@ nix profile diff-closures | grep 'env-manifest.nix: ε → ∅' # Test XDG Base Directories support export NIX_CONFIG="use-xdg-base-directories = true" -nix profile remove 1 +nix profile remove flake1 nix profile install $flake1Dir [[ $($TEST_HOME/.local/state/nix/profile/bin/hello) = "Hello World" ]] unset NIX_CONFIG @@ -68,7 +68,7 @@ unset NIX_CONFIG # Test upgrading a package. printf NixOS > $flake1Dir/who printf 2.0 > $flake1Dir/version -nix profile upgrade 1 +nix profile upgrade flake1 [[ $($TEST_HOME/.nix-profile/bin/hello) = "Hello NixOS" ]] nix profile history | grep "packages.$system.default: 1.0, 1.0-man -> 2.0, 2.0-man" @@ -89,7 +89,7 @@ nix profile diff-closures | grep 'Version 3 -> 4' # Test installing a non-flake package. nix profile install --file ./simple.nix '' [[ $(cat $TEST_HOME/.nix-profile/hello) = "Hello World!" ]] -nix profile remove 1 +nix profile remove simple nix profile install $(nix-build --no-out-link ./simple.nix) [[ $(cat $TEST_HOME/.nix-profile/hello) = "Hello World!" ]] @@ -107,7 +107,7 @@ nix profile wipe-history # Test upgrade to CA package. printf true > $flake1Dir/ca.nix printf 3.0 > $flake1Dir/version -nix profile upgrade 0 +nix profile upgrade flake1 nix profile history | grep "packages.$system.default: 1.0, 1.0-man -> 3.0, 3.0-man" # Test new install of CA package. @@ -139,7 +139,7 @@ nix profile install "$flake1Dir^man" (! [ -e $TEST_HOME/.nix-profile/include ]) # test priority -nix profile remove 0 +nix profile remove flake1 # Make another flake. flake2Dir=$TEST_ROOT/flake2 From ce70f02aff058c6438119e9946122f86431151f1 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 22 Dec 2023 16:35:58 +0100 Subject: [PATCH 4/7] getNameFromURL(): Support uppercase characters in attribute names In particular, this makes it handle 'legacyPackages' correctly. (cherry picked from commit 936a3642264ac159f3f9093710be3465b70e0e89) Upstream-PR: https://github.com/NixOS/nix/pull/9657 Change-Id: Icc4efe02f7f8e90a2970589f72fd3d3cd4418d95 --- src/libutil/url-name.cc | 2 +- tests/unit/libutil/url-name.cc | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libutil/url-name.cc b/src/libutil/url-name.cc index 6ef58c80a..7c526752c 100644 --- a/src/libutil/url-name.cc +++ b/src/libutil/url-name.cc @@ -5,7 +5,7 @@ namespace nix { -static std::string const attributeNamePattern("[a-z0-9_-]+"); +static std::string const attributeNamePattern("[a-zA-Z0-9_-]+"); static std::regex const lastAttributeRegex("(?:" + attributeNamePattern + "\\.)*(?!default)(" + attributeNamePattern +")(\\^.*)?"); static std::string const pathSegmentPattern("[a-zA-Z0-9_-]+"); static std::regex const lastPathSegmentRegex(".*/(" + pathSegmentPattern +")"); diff --git a/tests/unit/libutil/url-name.cc b/tests/unit/libutil/url-name.cc index f637efa89..164bb26d7 100644 --- a/tests/unit/libutil/url-name.cc +++ b/tests/unit/libutil/url-name.cc @@ -5,11 +5,13 @@ namespace nix { /* ----------- tests for url-name.hh --------------------------------------------------*/ - TEST(getNameFromURL, getsNameFromURL) { + TEST(getNameFromURL, getNameFromURL) { ASSERT_EQ(getNameFromURL(parseURL("path:/home/user/project")), "project"); ASSERT_EQ(getNameFromURL(parseURL("path:~/repos/nixpkgs#packages.x86_64-linux.hello")), "hello"); - ASSERT_EQ(getNameFromURL(parseURL("path:.#nonStandardAttr.mylaptop")), "nonStandardAttr.mylaptop"); - ASSERT_EQ(getNameFromURL(parseURL("path:./repos/myflake#nonStandardAttr.mylaptop")), "nonStandardAttr.mylaptop"); + ASSERT_EQ(getNameFromURL(parseURL("path:~/repos/nixpkgs#legacyPackages.x86_64-linux.hello")), "hello"); + ASSERT_EQ(getNameFromURL(parseURL("path:~/repos/nixpkgs#packages.x86_64-linux.Hello")), "Hello"); + ASSERT_EQ(getNameFromURL(parseURL("path:.#nonStandardAttr.mylaptop")), "mylaptop"); + ASSERT_EQ(getNameFromURL(parseURL("path:./repos/myflake#nonStandardAttr.mylaptop")), "mylaptop"); ASSERT_EQ(getNameFromURL(parseURL("path:./nixpkgs#packages.x86_64-linux.complex^bin,man")), "complex"); ASSERT_EQ(getNameFromURL(parseURL("path:./myproj#packages.x86_64-linux.default^*")), "myproj"); From e0911eef73e36d5b42ebd2e9fa114d535ab287f7 Mon Sep 17 00:00:00 2001 From: Qyriad Date: Wed, 1 May 2024 19:55:26 -0600 Subject: [PATCH 5/7] nix3-profile: make element names stable Based off of commit 6268a45b650f563bae2360e0540920a2959bdd40 Upstream-PR: https://github.com/NixOS/nix/pull/9656 Co-authored-by: Eelco Dolstra Change-Id: I0fcf069a8537c61ad6fc4eee1f3c193a708ea1c4 --- doc/manual/rl-next/nix-profile-names.md | 6 +- src/libcmd/cmd-profiles.cc | 110 +++++++++++++----------- src/libcmd/cmd-profiles.hh | 6 +- src/nix/profile.cc | 38 ++++---- src/nix/upgrade-nix.cc | 8 +- tests/functional/nix-profile.sh | 15 ++-- 6 files changed, 102 insertions(+), 81 deletions(-) diff --git a/doc/manual/rl-next/nix-profile-names.md b/doc/manual/rl-next/nix-profile-names.md index 2a37691e6..53dc53fa9 100644 --- a/doc/manual/rl-next/nix-profile-names.md +++ b/doc/manual/rl-next/nix-profile-names.md @@ -1,7 +1,9 @@ --- synopsis: "`nix profile` now allows referring to elements by human-readable name, and no longer accepts indices" prs: 8678 -cls: 978 +cls: [978, 980] --- -[`nix profile`](@docroot@/command-ref/new-cli/nix3-profile.md) now uses names to refer to installed packages when running [`list`](@docroot@/command-ref/new-cli/nix3-profile-list.md), [`remove`](@docroot@/command-ref/new-cli/nix3-profile-remove.md) or [`upgrade`](@docroot@/command-ref/new-cli/nix3-profile-upgrade.md) as opposed to indices. Indices have been removed. +[`nix profile`](@docroot@/command-ref/new-cli/nix3-profile.md) now uses names to refer to installed packages when running [`list`](@docroot@/command-ref/new-cli/nix3-profile-list.md), [`remove`](@docroot@/command-ref/new-cli/nix3-profile-remove.md) or [`upgrade`](@docroot@/command-ref/new-cli/nix3-profile-upgrade.md) as opposed to indices. Indices have been removed. Profile element names are generated when a package is installed and remain the same until the package is removed. + +**Warning**: The `manifest.nix` file used to record the contents of profiles has changed. Nix will automatically upgrade profiles to the new version when you modify the profile. After that, the profile can no longer be used by older versions of Nix. diff --git a/src/libcmd/cmd-profiles.cc b/src/libcmd/cmd-profiles.cc index 5fa7f902c..4b17d60c6 100644 --- a/src/libcmd/cmd-profiles.cc +++ b/src/libcmd/cmd-profiles.cc @@ -6,6 +6,7 @@ #include "logging.hh" #include "names.hh" #include "store-api.hh" +#include "url-name.hh" namespace nix { @@ -109,8 +110,6 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile) if (pathExists(manifestPath)) { auto json = nlohmann::json::parse(readFile(manifestPath)); - // Keep track of already found names so we can prevent duplicates. - std::set foundNames; auto version = json.value("version", 0); std::string sUrl; @@ -121,6 +120,8 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile) sOriginalUrl = "originalUri"; break; case 2: + [[fallthrough]]; + case 3: sUrl = "url"; sOriginalUrl = "originalUrl"; break; @@ -128,7 +129,10 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile) throw Error("profile manifest '%s' has unsupported version %d", manifestPath, version); } - for (auto & e : json["elements"]) { + auto elems = json["elements"]; + + for (auto & elem : elems.items()) { + auto & e = elem.value(); ProfileElement element; for (auto & p : e["storePaths"]) { element.storePaths.insert(state.store->parseStorePath((std::string) p)); @@ -145,26 +149,17 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile) e["outputs"].get()}; } - std::string nameCandidate(element.identifier()); + // TODO(Qyriad): holy crap this chain of ternaries needs cleanup. + std::string name = + elems.is_object() + ? elem.key() + : e.contains("name") + ? static_cast(e["name"]) + : element.source + ? getNameFromURL(parseURL(element.source->to_string())).value_or(element.identifier()) + : element.identifier(); - if (e.contains("name")) { - nameCandidate = e["name"]; - } else if (element.source) { - auto const url = parseURL(element.source->to_string()); - auto const name = getNameFromURL(url); - if (name) { - nameCandidate = *name; - } - } - - auto finalName = nameCandidate; - for (unsigned appendedIndex = 1; foundNames.contains(finalName); ++appendedIndex) { - finalName = nameCandidate + std::to_string(appendedIndex); - } - element.name = finalName; - foundNames.insert(element.name); - - elements.emplace_back(std::move(element)); + addElement(name, std::move(element)); } } else if (pathExists(profile + "/manifest.nix")) { // FIXME: needed because of pure mode; ugly. @@ -176,16 +171,37 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile) for (auto & drvInfo : drvInfos) { ProfileElement element; element.storePaths = {drvInfo.queryOutPath()}; - element.name = element.identifier(); - elements.emplace_back(std::move(element)); + addElement(std::move(element)); } } } +void ProfileManifest::addElement(std::string_view nameCandidate, ProfileElement element) +{ + std::string finalName(nameCandidate); + + for (unsigned i = 1; elements.contains(finalName); ++i) { + finalName = nameCandidate + "-" + std::to_string(i); + } + + elements.insert_or_assign(finalName, std::move(element)); +} + +void ProfileManifest::addElement(ProfileElement element) +{ + auto name = + element.source + ? getNameFromURL(parseURL(element.source->to_string())) + : std::nullopt; + + auto finalName = name.value_or(element.identifier()); + addElement(finalName, std::move(element)); +} + nlohmann::json ProfileManifest::toJSON(Store & store) const { - auto array = nlohmann::json::array(); - for (auto & element : elements) { + auto es = nlohmann::json::object(); + for (auto & [name, element] : elements) { auto paths = nlohmann::json::array(); for (auto & path : element.storePaths) { paths.push_back(store.printStorePath(path)); @@ -200,11 +216,11 @@ nlohmann::json ProfileManifest::toJSON(Store & store) const obj["attrPath"] = element.source->attrPath; obj["outputs"] = element.source->outputs; } - array.push_back(obj); + es[name] = obj; } nlohmann::json json; - json["version"] = 2; - json["elements"] = array; + json["version"] = 3; + json["elements"] = es; return json; } @@ -215,7 +231,7 @@ StorePath ProfileManifest::build(ref store) StorePathSet references; Packages pkgs; - for (auto & element : elements) { + for (auto & [name, element] : elements) { for (auto & path : element.storePaths) { if (element.active) { pkgs.emplace_back(store->printStorePath(path), true, element.priority); @@ -261,35 +277,29 @@ void ProfileManifest::printDiff( const ProfileManifest & prev, const ProfileManifest & cur, std::string_view indent ) { - auto prevElems = prev.elements; - std::sort(prevElems.begin(), prevElems.end()); - - auto curElems = cur.elements; - std::sort(curElems.begin(), curElems.end()); - - auto i = prevElems.begin(); - auto j = curElems.begin(); + auto prevElemIt = prev.elements.begin(); + auto curElemIt = cur.elements.begin(); bool changes = false; - while (i != prevElems.end() || j != curElems.end()) { - if (j != curElems.end() && (i == prevElems.end() || i->identifier() > j->identifier())) { - logger->cout("%s%s: ∅ -> %s", indent, j->identifier(), j->versions()); + while (prevElemIt != prev.elements.end() || curElemIt != cur.elements.end()) { + if (curElemIt != cur.elements.end() && (prevElemIt == prev.elements.end() || prevElemIt->first > curElemIt->first)) { + logger->cout("%s%s: ∅ -> %s", indent, curElemIt->second.identifier(), curElemIt->second.versions()); changes = true; - ++j; - } else if (i != prevElems.end() && (j == curElems.end() || i->identifier() < j->identifier())) { - logger->cout("%s%s: %s -> ∅", indent, i->identifier(), i->versions()); + ++curElemIt; + } else if (prevElemIt != prev.elements.end() && (curElemIt == cur.elements.end() || prevElemIt->first < curElemIt->first)) { + logger->cout("%s%s: %s -> ∅", indent, prevElemIt->second.identifier(), prevElemIt->second.versions()); changes = true; - ++i; + ++prevElemIt; } else { - auto v1 = i->versions(); - auto v2 = j->versions(); + auto v1 = prevElemIt->second.versions(); + auto v2 = curElemIt->second.versions(); if (v1 != v2) { - logger->cout("%s%s: %s -> %s", indent, i->identifier(), v1, v2); + logger->cout("%s%s: %s -> %s", indent, prevElemIt->second.identifier(), v1, v2); changes = true; } - ++i; - ++j; + ++prevElemIt; + ++curElemIt; } } diff --git a/src/libcmd/cmd-profiles.hh b/src/libcmd/cmd-profiles.hh index 7f2c8a76f..2185daa34 100644 --- a/src/libcmd/cmd-profiles.hh +++ b/src/libcmd/cmd-profiles.hh @@ -35,7 +35,6 @@ constexpr int DEFAULT_PRIORITY = 5; struct ProfileElement { StorePathSet storePaths; - std::string name; std::optional source; bool active = true; int priority = DEFAULT_PRIORITY; @@ -57,7 +56,7 @@ struct ProfileElement struct ProfileManifest { - std::vector elements; + std::map elements; ProfileManifest() { } @@ -67,6 +66,9 @@ struct ProfileManifest StorePath build(ref store); + void addElement(std::string_view nameCandidate, ProfileElement element); + void addElement(ProfileElement element); + static void printDiff(const ProfileManifest & prev, const ProfileManifest & cur, std::string_view indent); }; diff --git a/src/nix/profile.cc b/src/nix/profile.cc index 131abb258..401d5bd77 100644 --- a/src/nix/profile.cc +++ b/src/nix/profile.cc @@ -105,7 +105,7 @@ struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile element.updateStorePaths(getEvalStore(), store, res); - manifest.elements.push_back(std::move(element)); + manifest.addElement(std::move(element)); } try { @@ -115,7 +115,7 @@ struct CmdProfileInstall : InstallablesCommand, MixDefaultProfile // See https://github.com/NixOS/nix/compare/3efa476c5439f8f6c1968a6ba20a31d1239c2f04..1fe5d172ece51a619e879c4b86f603d9495cc102 auto findRefByFilePath = [&](Iterator begin, Iterator end) { for (auto it = begin; it != end; it++) { - auto profileElement = *it; + auto & profileElement = it->second; for (auto & storePath : profileElement.storePaths) { if (conflictError.fileA.starts_with(store->printStorePath(storePath))) { return std::pair(conflictError.fileA, profileElement.toInstallables(*store)); @@ -202,13 +202,19 @@ public: return res; } - bool matches(const Store & store, const ProfileElement & element, const std::vector & matchers) + bool matches( + Store const & store, + // regex_match doesn't take a string_view lol + std::string const & name, + ProfileElement const & element, + std::vector const & matchers + ) { for (auto & matcher : matchers) { if (auto path = std::get_if(&matcher)) { if (element.storePaths.count(store.parseStorePath(*path))) return true; } else if (auto regex = std::get_if(&matcher)) { - if (std::regex_match(element.name, regex->reg)) { + if (std::regex_match(name, regex->reg)) { return true; } } @@ -240,10 +246,9 @@ struct CmdProfileRemove : virtual EvalCommand, MixDefaultProfile, MixProfileElem ProfileManifest newManifest; - for (size_t i = 0; i < oldManifest.elements.size(); ++i) { - auto & element(oldManifest.elements[i]); - if (!matches(*store, element, matchers)) { - newManifest.elements.push_back(std::move(element)); + for (auto & [name, element] : oldManifest.elements) { + if (!matches(*store, name, element, matchers)) { + newManifest.elements.insert_or_assign(name, std::move(element)); } else { notice("removing '%s'", element.identifier()); } @@ -289,14 +294,13 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf auto matchers = getMatchers(store); Installables installables; - std::vector indices; + std::vector elems; auto matchedCount = 0; auto upgradedCount = 0; - for (size_t i = 0; i < manifest.elements.size(); ++i) { - auto & element(manifest.elements[i]); - if (!matches(*store, element, matchers)) { + for (auto & [name, element] : manifest.elements) { + if (!matches(*store, name, element, matchers)) { continue; } @@ -368,7 +372,7 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf }; installables.push_back(installable); - indices.push_back(i); + elems.push_back(&element); } @@ -393,7 +397,7 @@ struct CmdProfileUpgrade : virtual SourceExprCommand, MixDefaultProfile, MixProf for (size_t i = 0; i < installables.size(); ++i) { auto & installable = installables.at(i); - auto & element = manifest.elements[indices.at(i)]; + auto & element = *elems.at(i); element.updateStorePaths( getEvalStore(), store, @@ -425,14 +429,14 @@ struct CmdProfileList : virtual EvalCommand, virtual StoreCommand, MixDefaultPro if (json) { std::cout << manifest.toJSON(*store).dump() << "\n"; } else { - for (size_t i = 0; i < manifest.elements.size(); ++i) { - auto & element(manifest.elements[i]); + for (auto const & [i, nameElemPair] : enumerate(manifest.elements)) { + auto & [name, element] = nameElemPair; if (i) { logger->cout(""); } logger->cout( "Name: " ANSI_BOLD "%s" ANSI_NORMAL "%s", - element.name, + name, element.active ? "" : " " ANSI_RED "(inactive)" ANSI_NORMAL ); if (element.source) { diff --git a/src/nix/upgrade-nix.cc b/src/nix/upgrade-nix.cc index 31a051246..ca8080f88 100644 --- a/src/nix/upgrade-nix.cc +++ b/src/nix/upgrade-nix.cc @@ -208,7 +208,8 @@ struct CmdUpgradeNix : MixDryRun, EvalCommand // Find which profile element has Nix in it. // It should be impossible to *not* have Nix, since we grabbed this // store path by looking for things with bin/nix-env in them anyway. - auto findNix = [&](ProfileElement const & elem) -> bool { + auto findNix = [&](std::pair const & nameElemPair) -> bool { + auto const & [name, elem] = nameElemPair; for (auto const & ePath : elem.storePaths) { auto const nixEnv = store->printStorePath(ePath) + "/bin/nix-env"; if (pathExists(nixEnv)) { @@ -226,14 +227,15 @@ struct CmdUpgradeNix : MixDryRun, EvalCommand // *Should* be impossible... assert(elemWithNix != std::end(manifest.elements)); + auto const nixElemName = elemWithNix->first; + // Now create a new profile element for the new Nix version... ProfileElement elemForNewNix = { .storePaths = {newNix}, }; // ...and splork it into the manifest where the old profile element was. - // (Remember, elemWithNix is an iterator) - *elemWithNix = elemForNewNix; + manifest.elements.at(nixElemName) = elemForNewNix; // Build the new profile, and switch to it. StorePath const newProfile = manifest.build(store); diff --git a/tests/functional/nix-profile.sh b/tests/functional/nix-profile.sh index 387e73b2a..0f17e96ee 100644 --- a/tests/functional/nix-profile.sh +++ b/tests/functional/nix-profile.sh @@ -60,7 +60,7 @@ nix profile diff-closures | grep 'env-manifest.nix: ε → ∅' # Test XDG Base Directories support export NIX_CONFIG="use-xdg-base-directories = true" -nix profile remove flake1 +nix profile remove flake1 2>&1 | grep 'removed 1 packages' nix profile install $flake1Dir [[ $($TEST_HOME/.local/state/nix/profile/bin/hello) = "Hello World" ]] unset NIX_CONFIG @@ -81,7 +81,7 @@ nix profile rollback # Test uninstall. [ -e $TEST_HOME/.nix-profile/bin/foo ] -nix profile remove "foo" +nix profile remove "foo" 2>&1 | grep 'removed 1 packages' (! [ -e $TEST_HOME/.nix-profile/bin/foo ]) nix profile history | grep 'foo: 1.0 -> ∅' nix profile diff-closures | grep 'Version 3 -> 4' @@ -89,7 +89,7 @@ nix profile diff-closures | grep 'Version 3 -> 4' # Test installing a non-flake package. nix profile install --file ./simple.nix '' [[ $(cat $TEST_HOME/.nix-profile/hello) = "Hello World!" ]] -nix profile remove simple +nix profile remove simple 2>&1 | grep 'removed 1 packages' nix profile install $(nix-build --no-out-link ./simple.nix) [[ $(cat $TEST_HOME/.nix-profile/hello) = "Hello World!" ]] @@ -97,8 +97,9 @@ nix profile install $(nix-build --no-out-link ./simple.nix) mkdir $TEST_ROOT/simple-too cp ./simple.nix ./config.nix simple.builder.sh $TEST_ROOT/simple-too nix profile install --file $TEST_ROOT/simple-too/simple.nix '' -nix profile list | grep -A4 'Name:.*simple' | grep 'Name:.*simple1' -nix profile remove simple1 +nix profile list | grep -A4 'Name:.*simple' | grep 'Name:.*simple-1' +nix profile remove simple 2>&1 | grep 'removed 1 packages' +nix profile remove simple-1 2>&1 | grep 'removed 1 packages' # Test wipe-history. nix profile wipe-history @@ -111,7 +112,7 @@ nix profile upgrade flake1 nix profile history | grep "packages.$system.default: 1.0, 1.0-man -> 3.0, 3.0-man" # Test new install of CA package. -nix profile remove flake1 +nix profile remove flake1 2>&1 | grep 'removed 1 packages' printf 4.0 > $flake1Dir/version printf Utrecht > $flake1Dir/who nix profile install $flake1Dir @@ -132,7 +133,7 @@ nix profile upgrade flake1 [ -e $TEST_HOME/.nix-profile/share/man ] [ -e $TEST_HOME/.nix-profile/include ] -nix profile remove flake1 +nix profile remove flake1 2>&1 | grep 'removed 1 packages' nix profile install "$flake1Dir^man" (! [ -e $TEST_HOME/.nix-profile/bin/hello ]) [ -e $TEST_HOME/.nix-profile/share/man ] From 5d2031f92de4fec9e82f6e591772a8859c85c8b7 Mon Sep 17 00:00:00 2001 From: Eelco Dolstra Date: Fri, 12 Jan 2024 16:33:15 +0100 Subject: [PATCH 6/7] Add profile migration test (cherry picked from commit 72560f7bbef2ab3c02b8ca040fe084328bdd5fbe) Upstream-PR: https://github.com/NixOS/nix/pull/9656 Change-Id: I405e5848e2627a76940220fb6aebadfb8f094afb --- tests/functional/nix-profile.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/functional/nix-profile.sh b/tests/functional/nix-profile.sh index 0f17e96ee..ed014f9ef 100644 --- a/tests/functional/nix-profile.sh +++ b/tests/functional/nix-profile.sh @@ -194,3 +194,12 @@ nix profile install $flake2Dir --priority 0 clearProfiles nix profile install $(nix build $flake1Dir --no-link --print-out-paths) expect 1 nix profile install --impure --expr "(builtins.getFlake ''$flake2Dir'').packages.$system.default" + +# Test upgrading from profile version 2. +clearProfiles +mkdir -p $TEST_ROOT/import-profile +outPath=$(nix build --no-link --print-out-paths $flake1Dir/flake.nix^out) +printf '{ "version": 2, "elements": [ { "active": true, "attrPath": "legacyPackages.x86_64-linux.hello", "originalUrl": "flake:nixpkgs", "outputs": null, "priority": 5, "storePaths": [ "%s" ], "url": "github:NixOS/nixpkgs/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } ] }' "$outPath" > $TEST_ROOT/import-profile/manifest.json +nix build --profile $TEST_HOME/.nix-profile $(nix store add-path $TEST_ROOT/import-profile) +nix profile list | grep -A4 'Name:.*hello' | grep "Store paths:.*$outPath" +nix profile remove hello 2>&1 | grep 'removed 1 packages, kept 0 packages' From 6a8b3796286d2f6e56e1fd6b83dfcb5f56d09d13 Mon Sep 17 00:00:00 2001 From: Qyriad Date: Wed, 1 May 2024 19:57:35 -0600 Subject: [PATCH 7/7] nix3-profile: remove check "name" attr in manifests It doesn't seem to have ever been used. Based off of commit a748e88bf4cca0fdc6ce75188e88017a7899d16b Upstream-PR: https://github.com/NixOS/nix/pull/9656 Change-Id: Idcf250a645fa43f2ef11fb15b503b070a62a917e --- src/libcmd/cmd-profiles.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libcmd/cmd-profiles.cc b/src/libcmd/cmd-profiles.cc index 4b17d60c6..101064956 100644 --- a/src/libcmd/cmd-profiles.cc +++ b/src/libcmd/cmd-profiles.cc @@ -153,8 +153,6 @@ ProfileManifest::ProfileManifest(EvalState & state, const Path & profile) std::string name = elems.is_object() ? elem.key() - : e.contains("name") - ? static_cast(e["name"]) : element.source ? getNameFromURL(parseURL(element.source->to_string())).value_or(element.identifier()) : element.identifier();