Using nixpkgs.legacyPackages.${system} vs import

I experimented the following flake.nix and both cowsay and ponysay work well with direnv.

{
  description = "cowsay ponysay";
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    stable.url = "github:NixOS/nixpkgs/nixos-21.11";
    flake-utils.url = "github:numtide/flake-utils";
    };
  };

  outputs = { self, nixpkgs, stable, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (system:
    let
      pkgs = nixpkgs.legacyPackages.${system};
      stablepkgs = import stable {  inherit system; };
    in {
    devShell = pkgs.mkShell { 
      buildInputs =
      [
        pkgs.cowsay
        stablepkgs.ponysay
      ];
    };
  });
}

Is there any difference in effect between using nixpkgs.legacyPackages.${system} and import stable apart from import being able to override packages? Can either one do everything the other can?

8 Likes

To add some context: The article ‘1000 instances of nixpkgs’ (forum post, article) mentions this approach to prevent importing nixpkgs all the time. I have the same question about the differences between them. Is importing/instantiating nixpkgs a bad thing? legacyPackages sounds ‘legacy’ ( :wink: ), so my intuition is to avoid it…

2 Likes

Using import causes nixpkgs to be evaluated another time for the import. You shouldn’t use it unless you have to, to apply some config.

4 Likes

It’s legacyPackages to reflect the fact, that it’s not following the rules for packages, ie it’s not flat but a set of sets.

8 Likes

Aren’t these two equivalent? (EDIT: they are not, please see the discussion below) legacyPackages is defined as:

So it is also using import under the hood.

2 Likes

We need at least one instance of nixpkgs (per system). The question is who is going to hold it.

If your flake has no dependencies except nixpkgs, it doesn’t make a difference to use one or the other. The issue is when flake A depends on flake B, and both create their own instance of nixpkgs. At scale, this will become a problem.

If all your flakes use nixpkgs.legacyPackages, and all of them “follow” the same nixpkgs flake, then that is a good way to ensure only one instance of nixpkgs to be used.

1 Like

I still don’t see the difference between the two. It looks like legacyPackages is a wrapper over to explicitly import the nixpkgs.

Given all dependencies follow the same nixpkgs flake; are you saying when import nixpkgs { inherit system; }; is used instead of nixpkgs.legacyPackages.${system}, there is no caching and the same expression is evaluated multiple times?

“A” depends on nixpkgs.

In that case, if A import nixpkgs { inherit system; } or accesses nixpkgs.legacyPackages.${system}, both are functionally the same. This is true but realistically, as flakes get more popular, few projects will have only a dependency on nixpkgs.

A depends on B and nixpkgs, B depends on nixpkgs

Now if both A and B import nixpkgs { inherit system; }, there are two instances of nixpkgs instead of one. In further cases where the dependency tree grows larger, the number of instances would grow linearly with the number of dependencies.

In contrast, if both A and B access nixpkgs.legacyPackages.${system}, and that they both depend on the same nixpkgs flake, there will only be one evaluation of nixpkgs.
For this case to work, another requirement is to use the inputs “follows” feature, so they are both pinned to the same version of nixpkgs.

8 Likes

I forgot that Nix does not implement maximal laziness.

I assumed the function call would be memoized in here but it seems that is not the case.

If I understand correctly, the difference in here is that the result is stored as an attribute value therefore it will be evaluated once.

That’s right. If you call a function twice, Nix returns two fresh “thunks”. As a person, you might deduce that the thunks should be the same, but Nix doesn’t know that and re-evaluates them all over again.

There is one memorization happening, on the import boundary. So if two parts of the code import <nixpkgs/lib> for example, that will be the same reference. But for nixpkgs, import <nixpkgs> returns a function. That gets memorized but it’s not super interesting for us.

4 Likes

how do I specifiy overlays with legacyPackages?

I’m trying to follow this guide: https://github.com/input-output-hk/haskell.nix/blob/a1a07a0fb9df23871c62bffef447216def0d7779/docs/tutorials/getting-started-flakes.md
but it uses the import technique

You can use nixpkgs.overlays if you only need the overlay for a nixosConfiguration.

Otherwise, you need to publish your own outputs as an overlay, and use the prev argument as a nixpkgs (because when the overlay is eventually used the user will supply a nixpkgs for it).

For “leaf” flakes that don’t have dependants using import is totally fine by the way. It’s probably also fine for use in a devShell, or checks. They won’t create excess nixpkgs instances, since the import-ed nixpkgs instance is the only one, and dependants don’t really use those outputs (though they could in theory, hence the probably); This is only a problem if you’re higher up in a dependency tree.

2 Likes

I still don’t see any difference in using import vs legacyPackages: even if A and B both use nixpkgs.legacyPackages, they are referring to different revisions of nixpkgs, which are locked in their flake.lock files. So it’s two different nixpkgs in any case.

Maybe I don’t understand it correctly, but it seems kinda problem.

E.g. I have flake-based NixOS configuration pinned to a certain revision. Then I start a project with nix shell and write a flake.nix using nixpkgs inside. Those project’s nixpkgs usually have another revision than my system. So installing a simple bash for nix shell will pull different revisions of all system libraries, which is kinda ridiculous :man_facepalming:

Unless you use follows.

Yes. follows is how you can solve that with the current design, but it’s one of the big remaining problems with flakes. Flakehub is an initial investigation into a more scaleable solution.

Could you please provide a snippet how to use follows to pin my project’s nixpkgs to my global NixOS configuration?

You can’t “pin” something to an effectively randomly changing value, that doesn’t make sense.

However, if you set the url to the name of an entry in your flake registry, the flake from your flake registry will be taken as an input. You can also omit the entry in the inputs attrset, and make the name of your outputs arg match an entry. If you don’t want to pin your nixpkgs version for a specific project for whatever reason, you can do that.

E.g.:

{
  outputs = {nixpkgs, ...}: {
    # stuff
  };
}

or more verbosely:

{
  inputs.nixpkgs.url = "nixpkgs";

  outputs = {nixpkgs, ...}: {
    # stuff
  };
}

Of course, you can just use follows as usual to override input flakes’ recursive dependencies.

Note that whatever your system flake points at is usually not the same as what your registry points at, but you can configure them to match: https://github.com/TLATER/dotfiles/blob/7c09f2c1fd4b2ba96aeffe67435a05f076cfec48/nixos-config/default.nix#L42

Doing it like that, and setting the flake inputs of your system flake to non-registry entries, will effectively “pin” your registry entry to whatever your system entry is pinned to. This is still random - or at least untraceable - as far as any consuming projects are concerned, though.

I think doing the inverse of this is probably better practice by the way, there were some caveats to the above that I forgot.

Note also that the flake registry by default sets nixpkgs to the unstable branch, and updates this automatically when you run nix commands after a time out of - I believe - a few hours. This is also why e.g. nix run nixpkgs#package will almost always download stuff by default (the latest nixpkgs unstable).

You can pin your registry to work around this, and/or set it to stable, but now you’ve just reinvented channels with all the issues having an imperative component to your system and all projects has.

So we’ve gone back in time to before niv or fetchurl-ing specific nixpkgs revisions was a thing. Depending on what you wanted, this may be good, and I think the fact that this isn’t the default way to use flakes while still being an option is nice.

I would definitely not recommend this for projects you intend to share with anyone, in either case. For those kinds of projects, offer a canonical version of nixpkgs they will work with. If dependency duplication becomes a problem, or maintenance/short-term vulnerability issues arise, override the inputs with follows on the user end, and/or wait for something like flakehub to come into general enough use that the problem is mostly kept in check.

2 Likes

Yeah, this is useful technique, thanks a lot for sharing!

The rest is also very insightful, I’ve got some missing pieces in the puzzle of how it all works :partying_face:

1 Like

I do this for all the flake inputs my system consumes, as so:

  nix = {
    # …
    registry = lib.mapAttrs (_: value: { flake = value; }) inputs;
    nixPath = lib.mapAttrsToList (key: value: "${key}=${value.to.path}") config.nix.registry;
  };

which means I get system registry entries pinned to the revisions used to build /run/current-system, for e.g.:

system flake:agenix path:/nix/store/pib5m63qfz2plbxz85hk55lv7lfkpzcw-source?lastModified=1695384796&narHash=sha256-TYlE4B0ktPtlJJF9IFxTWrEeq%2BXKG8Ny0gc2FGEAdj0%3D&rev=1f677b3e161d3bdbfd08a939e8f25de2568e0ef4
system flake:home-manager path:/nix/store/35p7gkzlws9xag3szqzgs44jmsp95jp8-source?lastModified=1696371324&narHash=sha256-0ycIheYRxzPOL9XBWiAm/af9cqRmsiy701OpjsRsKiw%3D&rev=e63c30fe9792b57dea1eab98be6871a0e42a33c9
system flake:nixpkgs path:/nix/store/5rb11cz8xmv8cdk7b0w80pzczbzk0p26-source?lastModified=1696193975&narHash=sha256-mnQjUcYgp9Guu3RNVAB2Srr1TqKcPpRXmJf4LJk6KRY%3D&rev=fdd898f8f79e8d2f99ed2ab6b3751811ef683242
…

This works well for me because I update the system regularly, and so it mostly just avoids the unnecessary downloads for the odd ad-hoc nix shell nixpkgs#… in between. Putting them in nixPath also means the same thing for most of the legacy commands that would otherwise fail to import <nixpkgs> because I don’t have channels defined at all any more.

But as noted, it doesn’t affect the revisions in any of my development workspaces’ flake.lock. Nor should it; that should be isolated from the surrounding system environment. Typically, I get the same devshell environment I was using when I last worked on the project in question, and the first thing I do is update to current.

What I haven’t looked into, but might be useful, is to work out a way for nix flake update in that workspace to update to the revision in the system registry, rather than the absolute most recent version. In my case, that’s commonly close enough to current that it doesn’t matter, although it’s usually a different revision, because I typically have systems tracking nixos-unstable and development flakes tracking nixpkgs-unstable. But sometimes around a major bulk rebuild, I can wind up with these minor differences resulting in a bunch of extra unexpected downloads.

4 Likes