Why does mkShell come from pkgs rather than lib?

Hey everyone! :slight_smile:

Hope it’s not a naive question, I am working on my intuition of the Nix evaluation pipeline, I am used to work with NixOS/Home-manager/nix-on-droid. I am starting to play with the shell to build my dev environement, and I have a question regarding the design choice behind mkShell

I understand basics in nix, and I think I got the fundamental difference between lib.* (pure Nix logic/functions) and pkgs.* (“cooked” package set for a specific system/architecture). As far as I understand, pkgs.mkShell just builds an AttrSet to populate the devShells.<system>.<name> for the flake output, so I don’t relly get why it comes from pkgs, I think I may be missing a fundamental concept somewhere

Actually I have this mental analogy:

The NixOS pipeline:

  • lib.nixosSystem: function that evaluates/merges modules, returns a huge AttrSet containing config (which holds the toplevel derivation)
  • nixosConfigurations.<hostname>: the flake output that receives this generated AttrSet
  • nixos-rebuild switch: CLI wrapper that builds the toplevel and activates the system (symlinks in store according to nixosConfigurations. attrset, systemd restarts, etc.)

Here it makes sense to me that nixosSystem comes from lib, as its primary job is purely logical (evaluating modules to build an AttrSet)

The devShell pipeline:

  • pkgs.mkShell: function that builds the AttrSet (the derivation)

  • devShells.<system>.<name>: flake output that receives this AttrSet

  • nix shell: CLI tool that build the flake attrset and set a shell with the $PATH that points to the nix/store/xxx-<my_tool>/user/bin (according to the devShells.. attrset )

So here is my question:
Since mkShell just builds an AttrSet (like lib.nixosSystem does), why does it live inside pkgs?
Wouldn’t it be possible to have a lib.mkShell function where we just inject the instantiated pkgs? Something like: lib.mkShell { inherit pkgs; packages = [ ... ]; }?

Thanks in advance for your answer! :slight_smile:

1 Like

My first though is that pkgs.mkShell makes a derivation, but lib.nixosSystem evaluates a system of modules.

1 Like

Yeah, the difference is that mkShell is used by a nix subcommand directly, which operates on derivations exclusively. Here the derivation must be fully evaluated before the command will operate on it.

nixosSystem on the other hand returns something later processed by nixos-rebuild, which only creates an appropriate derivation as part of its internal logic.

Incidentally, this accidentally underlines the distinction between NixOS and nix.

2 Likes

Ah, of course.

mkShell doesn’t just build an attrset, it builds a derivation. mkShell isn’t the only builder that can be used; any derivation can be fed to nix shell and set up a build environment.

It would of course be possible to change nix shell, but that goes against how nix was designed, and is anyway pointless, since you’d need to instantiate pkgs anyway.

nixosSystem on the other hand is post-processed by a different tool, and the instantiation of nixpkgs is part of the system build, and is modified during it. The way a NixOS evaluation works is totally different under the hood. You can’t make nixosSystem depend on an already-evaluated pkgs without wasting eval time.

That’s kinda the long and short of it.

1 Like

One of the reasons why mkShell comes from pkgs rather than lib is because mkShell is effectively a special wrapper around mkDerivation. It doesn’t just pull in your packages from packages or other attributes, it also can pull in a C compiler and various system utilities. The packages it provides cannot be provided without pkgs.

The difference is that lib doesn’t depend on host architecture. You can use lib without import nixpkgs{system="x86_64-linux";} or nixpkgs.legacyPackages.x86_64-linux, just use nixpkgs.lib directly.

It’s possible to define a lib.mkShell, but you still need to define your pkgs in mkShell. It’s more convenient to define mkShell in pkgs and more importantly, it makes lib more PURE.

when I use pkgs.runCommand, I have similar thought. Isn’t runCommand more like a library function? Actually, Nix doesn’t run the command it self, it’s run by the derivation builder. It is not just a function, there is a lot of work happening in background.

@user.compactor understands this, I’m pretty sure, that’s what they mean by:

The question is, why couldn’t pkgs.mkShell also be platform-independent (nix shell could resolve that at runtime after all), or if it cannot be, why isn’t lib.nixosSystem platform-dependant (it also builds a derivation for which the system must be resolved, after all).

It’s a pretty reasonable question, and comes down to design decisions.

nix shell is impure, but pkgs.mkShell is pure. The pure function pkgs.mkShell will always produce the same outputs once built, but the outputs won’t work on all platforms. To make pkgs.mkShell platform-independent, we need to make it impure. This is against the nix’s philosophy.

lib.nixosSystem doesn’t build a derivation, it evaluates the modules and produces an attrset. You need to evaluate config.system.build.toplevel and then build the derivation to get the system path outputs.

The problem is that you need to know the system at build time. We need to run binaries, not just nix code. The nix code is platform-independent, but the pkgs aren’t.

1 Like

I fully understand this, and explained that myself further up; my point is, the opposite philosophy could have been taken. @user.compactor was confused about why it wasn’t.

Besides, you could reasonably design a pkgs.nixosSystem that evaluates directly to the toplevel element, and requires knowing the system up-front.

The design that is used in practice is not a no-brainer; it even used to be that the system was an argument to that function, wouldn’t it have been more reasonable to take the system from an evaluated pkgs?

Of course, the issue with that is that config.nixpkgs would then be harder to implement, and at this point not exposing all the module system evaluation components would be backwards incompatible. Still, it’s a design that could have been different, and it’s reasonable to ask why it is not.