Pin each package that goes into mkShell

I have done an attempt here at pinning pnpm to 8.5.1:

{
  description = "Sample Nix ts-node build";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    pnpm_851.url = "github:nixos/nixpkgs?rev=d81de9fc07060ba680fae01da33aa6390d2a2666"; # Revision where pnpm is at version 8.5.1
  };
  outputs = { self, nixpkgs, flake-utils, pnpm_851, ... }:
    flake-utils.lib.eachDefaultSystem(system:
      let pkgs = import nixpkgs {
        inherit system;
      };
        pnpmpkgs = import pnpm_851 {
          inherit system;
        };
      in
      with pkgs;
      {
        devShells.default = mkShell {
          packages = [
            awscli2
            protobuf
            pnpmpkgs.nodePackages.pnpm
            nodePackages.ts-node
            nodePackages.prettier
            nodejs_16
          ];

          # We'd need to extract token and such in a developer specific .envrc
          NPM_TOKEN = "test";
        };
      }
    );
}

I’m not super happy with this approach to begin with.

But for any serious setup where more developers depend on this, we would probably want to lock each package passed in to a specific version/hash (not necessarily the same hash).

What would be the cleanest way to achieve this?

That depends on what kind of environment/workflow/whatever you’re intending to create.

If this is going to be a bunch of packages and intended for strict regulatory input control, I’d suggest creating a flake that just contains proper package definitions for all of them, and then make developers use that as their input. Your own, more limited nixpkgs if you will. The question then becomes which things you intend to pin, what you defer to your nixpkgs input, what your update flow actually is, how you enforce this at build/deploy time, who is responsible for maintenance, etc.

If it’s just a one-off like this that solution seems appropriate to me.

Give us some more context on the intended use case, what you would like to enable or prevent, what you don’t like about this solution, etc.

This forum might be a bad place to ask, frankly, this sounds like a big commercial question, lots of organizational questions hidden in this that you’ll need to consider (and perhaps write) company policies for. Or not, hard to say without more info on your use case :wink:

It’s not that organizational. It’s this use case:

  • We have a project with this flake
  • Developers run nix develop
  • They should get these tools installed in their environment at versions that work
  • This would mean to specify an exact revision for each of the packages

What I don’t like particularly is that I have to touch the file in four places to make this work. Suppose I had to do this for each package, things would get somewhat ugly.l I can live with it, but if there were a better way to do this, that would be great.

I asked how to do this and I got something about “overrides or repackaging”. No idea what is meant by either of those.

(This overrideDerivation looks like it could do what I want, but not quite.)

Well, to begin with you could simplify your example, remove the redundant evaluations and reduce the scope of your with for clarity:

{
  description = "Sample Nix ts-node build";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    pnpm_851.url = "github:nixos/nixpkgs?rev=d81de9fc07060ba680fae01da33aa6390d2a2666"; # Revision where pnpm is at version 8.5.1
  };
  outputs = { self, nixpkgs, flake-utils, pnpm_851, ... }:
    flake-utils.lib.eachDefaultSystem(system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
        pnpm = pnpm_851.legacyPackages.${system}.nodePackages.pnpm;
      in {
        devShells.default = pkgs.mkShell {
          packages = with pkgs; [
            awscli2
            protobuf
            pnpm
            nodePackages.ts-node
            nodePackages.prettier
            nodejs_16
          ];

          # We'd need to extract token and such in a developer specific .envrc
          NPM_TOKEN = "test";
        };
      }
    );
}

You can also reduce it to just “touching it in two places”, I guess:

{
  description = "Sample Nix ts-node build";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    pnpm_851.url = "github:nixos/nixpkgs?rev=d81de9fc07060ba680fae01da33aa6390d2a2666"; # Revision where pnpm is at version 8.5.1
  };
  outputs = { self, nixpkgs, flake-utils, pnpm_851, ... }:
    flake-utils.lib.eachDefaultSystem(system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in {
        devShells.default = pkgs.mkShell {
          packages = with pkgs; [
            awscli2
            protobuf
            pnpm_851.legacyPackages.${system}.nodePackages.pnpm
            nodePackages.ts-node
            nodePackages.prettier
            nodejs_16
          ];

          # We'd need to extract token and such in a developer specific .envrc
          NPM_TOKEN = "test";
        };
      }
    );
}

But that becomes a question of style rather than implementation. I’m not sure this is more readable. Garnish with maps over an @ inputs as you see fit.

You can also indeed alternatively use overrideAttrs, although this involves some tradeoffs:

{
  description = "Sample Nix ts-node build";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };
  outputs = { self, nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem(system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
      in {
        devShells.default = pkgs.mkShell {
          packages = with pkgs; [
            awscli2
            protobuf
            nodePackages.ts-node
            nodePackages.prettier
            nodejs_16

            (nodePackages.pnpm.overrideAttrs (old: {
              src = fetchurl {
                url = "https://registry.npmjs.org/pnpm/-/pnpm-8.5.1.tgz";
                sha256 = "W6elL7Nww0a/MCICkzpkbxW6f99TQuX4DuJoDjWp39X08PKDkEpg4cgj3d6EtgYADcdQWl/eM8NdlLJVE3RgpA==";
              };
            }))
          ];

          # We'd need to extract token and such in a developer specific .envrc
          NPM_TOKEN = "test";
        };
      }
    );
}

This only overrides the pnpm source specifically, which means that the build may break if something else about its build process changes eventually.

The next step up from there is to write a package for it from scratch. You can either follow the instructions in the nixpkgs manual for this or use dream2nix to convert pnpm into a package whose checksum you control via a package.json.

Which is best depends on how permanent this pin should be. For a short-term fix I’d stick to the gist of your current solution, if it’s something you’d like to keep longer-term write a custom package. The override is iffy, it will probably work, but I wouldn’t trust it long term, and I think I prefer using the pin to an old version of nixpkgs.

1 Like

Ah, I skimmed past this:

Absolutely, that’s when you use dream2nix. Or if you’re happy with pinning all the npm packages to the ones from a specific, old version of nixpkgs while moving ahead the general nixpkgs independently:

{
  description = "Sample Nix ts-node build";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
    old-nixpkgs.url = "github:nixos/nixpkgs?rev=d81de9fc07060ba680fae01da33aa6390d2a2666"; # Revision where pnpm is at version 8.5.1
  };
  outputs = { self, nixpkgs, flake-utils, old-nixpkgs, ... }:
    flake-utils.lib.eachDefaultSystem(system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
        nodePackages = old-nixpkgs.legacyPackages.${system}.nodePackages;
      in {
        devShells.default = pkgs.mkShell {
          packages = with pkgs; [
            awscli2
            protobuf
            nodePackages.pnpm # Real bindings always have priority over "with"
            nodePackages.ts-node
            nodePackages.prettier
            nodejs_16
          ];

          # We'd need to extract token and such in a developer specific .envrc
          NPM_TOKEN = "test";
        };
      }
    );
}

Of course, if you want all packages to be pinned, nothing stops you from just never running nix flake update. Nix by default pins all your inputs.

If you do need independent versions of each package, I’d really recommend dream2nix (or yarn2nix - which is how nixpkgs does it these days - if you want something more stable), importing lots of nixpkgs versions doesn’t sound great for evaluation times, and the *2nix implementations give you a lot more flexibility.

1 Like