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?

6 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…

1 Like

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.

3 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.

6 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.

6 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.

3 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.