Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nix profile: Allow referring to elements by human-readable name #8678

Merged
merged 6 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/manual/rl-next/nix-profile-names.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
synopsis: "`nix profile` now allows referring to elements by human-readable name"
prs: 8678
---

[`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.
2 changes: 1 addition & 1 deletion src/libexpr/flake/flakeref.cc
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ std::optional<std::pair<FlakeRef, std::string>> parseFlakeIdRef(

static std::regex flakeRegex(
"((" + flakeIdRegexS + ")(?:/(?:" + refAndOrRevRegex + "))?)"
+ "(?:#(" + queryRegex + "))?",
+ "(?:#(" + fragmentRegex + "))?",
std::regex::ECMAScript);

if (std::regex_match(url, match, flakeRegex)) {
Expand Down
48 changes: 48 additions & 0 deletions src/libutil/url-name.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include "url-name.hh"
#include <regex>
#include <iostream>

namespace nix {

static const std::string attributeNamePattern("[a-z0-9_-]+");
static const std::regex lastAttributeRegex("(?:" + attributeNamePattern + "\\.)*(?!default)(" + attributeNamePattern +")(\\^.*)?");
static const std::string pathSegmentPattern("[a-zA-Z0-9_-]+");
static const std::regex lastPathSegmentRegex(".*/(" + pathSegmentPattern +")");
static const std::regex secondPathSegmentRegex("(?:" + pathSegmentPattern + ")/(" + pathSegmentPattern +")(?:/.*)?");
static const std::regex gitProviderRegex("github|gitlab|sourcehut");
static const std::regex gitSchemeRegex("git($|\\+.*)");
static const std::regex defaultOutputRegex(".*\\.default($|\\^.*)");

std::optional<std::string> getNameFromURL(const ParsedURL & 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 {};
}

}
20 changes: 20 additions & 0 deletions src/libutil/url-name.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#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<std::string> getNameFromURL(const ParsedURL & url);

}
Comment on lines +1 to +20
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iFreilicht I would appreciate if you could move this elsewhere. libutil should be just utilities and not define Nix-specific concepts like store paths or flake things. (In the "Domain-Driven Design" parlance, this is a "application layer" or "domain layer" concept, but libutil is supposed to be the "infrastructure layer".)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Could you maybe give me a hint where "elsewhere" could be?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I would put in src/libexpr/flake/ for now. There are other flakes things in there that don't strictly relate to evaluation already.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, see #9655

1 change: 1 addition & 0 deletions src/libutil/url-parts.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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 + ")*/?)";
Expand Down
2 changes: 1 addition & 1 deletion src/libutil/url.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ParsedURL parseURL(const std::string & url)
"((" + schemeNameRegex + "):"
+ "(?:(?://(" + authorityRegex + ")(" + absPathRegex + "))|(/?" + pathRegex + ")))"
+ "(?:\\?(" + queryRegex + "))?"
+ "(?:#(" + queryRegex + "))?",
+ "(?:#(" + fragmentRegex + "))?",
std::regex::ECMAScript);

std::smatch match;
Expand Down
10 changes: 8 additions & 2 deletions src/nix/profile-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ R""(

```console
# nix profile list
Name: gdb
iFreilicht marked this conversation as resolved.
Show resolved Hide resolved
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

Name: blender-bin
Index: 1
Flake attribute: packages.x86_64-linux.default
Original flake URL: flake:blender-bin
Expand All @@ -26,18 +28,22 @@ 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 `blender-bin` shown above.

# Description

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we guarantee that names are unambiguous? A name like packages.x86_64-linux.hello isn't.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unambiguous in the context of the current profile. They're generated when listing the profile contents. If a name like hello shows up multiple times, subsequent instances will be suffixed with an increasing number, so you have hello, hello1, hello2 etc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isnt that susceptible to the same issues this outlines: #7961?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isnt that susceptible to the same issues this outlines: #7961?

Only for packages with the same name. #7961 is about any package.

A decision about installing multiple packages with the same name can be made in a follow up PR. (Error? Replace? Interactive question? Allow duplicates?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it makes sense to use some portion of the store hash as an alternative suffix? e.g. hello-3bjqzx, hello-6r9h8

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could also be an option.

Personally I'd like to avoid duplicate names eventually altogether by erroring upon install. I think these kinds of improvements/iterations should be a decision for a follow-up PR. The current implementation of this PR is simple and more or less in line with the current implementation on master (numbering the packages).

I see this PR as the most minimal step we can make towards using names for profile entries.

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`).

Expand Down
9 changes: 5 additions & 4 deletions src/nix/profile-remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +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 attribute path:
* Remove a package by index
*(deprecated, will be removed in a future version)*:

```console
# nix profile remove packages.x86_64-linux.hello
# nix profile remove 3
```

* Remove all packages:
Expand Down
10 changes: 4 additions & 6 deletions src/nix/profile-upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,16 @@ R""(
# nix profile upgrade '.*'
```

* Upgrade a specific package:
* Upgrade a specific package by name:

```console
# nix profile upgrade packages.x86_64-linux.hello
# nix profile upgrade hello
```

* Upgrade a specific profile element by number:
* Upgrade a specific package by index
*(deprecated, will be removed in a future version)*:

```console
# nix profile list
0 flake:nixpkgs#legacyPackages.x86_64-linux.spotify …

# nix profile upgrade 0
```

Expand Down
Loading
Loading