Mixing stable and unstable packages on flake-based NixOS system

Hello everyone. My system is based on flakes (nix-channel is disabled). How can I use some packages from nixos-unstable while keeping the rest of my system on the stable branch? I’d like to achieve something like this:

environment.systemPackages = with pkgs; [
    firefox
    thunderbird
    unstable.osu-lazer-bin
];

In other words, I wanna be able to access osu-lazer-bin from the unstable branch via pkgs.unstable.osu-lazer-bin.
I’ve already seen multiple approaches to achieve this, but none of them worked for me, maybe it’s because my system uses flakes, idk.

translated with llama 3.1 405b :3

1 Like

You should be able to add the unstable nixpkgs as part of your inputs, then import it and add that package directly. Something like this:

 inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/release-24.05";
    nixpkgs-unstable.url = "github:nixos/nixpkgs/nixpkgs-unstable";
 };

 outputs = {nixpkgs, nixpkgs-unstable, ... }:
 let
   system = "x86_64-linux";
   pkgs = import nixpkgs { inherit system; };
   pkgsUnstable = import nixpkgs-unstable { inherit system; };
 in
 {
    # now along with `pkgs` you also have `pkgsUnstable`
    # and can do: pkgsUnstable.osu-lazer-bin
 }

Something like this should work:

  overlay-unstable = final: prev: {
    unstable = import nixpkgs-unstable {
      inherit system;
      config.allowUnfree = true;
    };
  };

  pkgs = import nixpkgs {
    inherit system;
    config = {
      allowUnfree = true;
    };
    overlays = [
      overlay-unstable
    ];
  };

Then you can do pkgs.firefox or pkgs.unstable.firefox.

2 Likes

Never set pkgs directly in the NixOS module system, as the other answers are indicating.

Here’s a couple of concepts you need to know:

  1. If you want to access some inputs, or some other value across your config without creating an explicit option for it, use specialArgs or _module.args. (For things that you import, you must pass them in specialArgs specifically to avoid infrec.)
  2. Changing pkgs requires an overlay; however, for this application, you may not necessarily need one if you are willing to get away from the pkgs.unstable syntax specifically.

Which means the following:

  1. Add an entry to your inputs, and pass them to your config via specialArgs, in your flake.nix:

    inputs = {
      nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
      nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
    };
    
    outputs = { nixpkgs, ... }@inputs:
      nixosConfigurations = {
        foo = nixpkgs.lib.nixosSystem {
          #...
          specialArgs = { inherit inputs; };
        };
      };
    };
    
  2. Since you need an unfree package specifically, allow the unfree package somewhere in your NixOS config:

    {
      nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
        "osu-lazer-bin"
      ];
    }
    

    or if you want, you can just blanket allow all unfree packages, that’s up to you:

    {
      nixpkgs.config.allowUnfree = true;
    }
    
  3. Then, use that config when importing the unstable nixpkgs, and then use the appropriate module arg in your package list.
    a. EITHER create a new module arg:

    { config, inputs, pkgs, pkgsUnstable, ... }:
    {
      # this allows you to access `pkgsUnstable` anywhere in your config
      _module.args.pkgsUnstable = import inputs.nixpkgs-unstable {
        inherit (pkgs.stdenv.hostPlatform) system;
        inherit (config.nixpkgs) config;
      };
    
      environment.systemPackages = [
        pkgsUnstable.osu-lazer-bin
      ];
    }
    

    b. OR, add an overlay to modify the existing nixpkgs instance (pkgs):

    { inputs, pkgs, ... }:
    {
      nixpkgs.overlays = [
        (final: _: {
          # this allows you to access `pkgs.unstable` anywhere in your config
          unstable = import inputs.nixpkgs-unstable {
            inherit (final.stdenv.hostPlatform) system;
            inherit (final) config;
          };
        })
      ];
    
      environment.systemPackages = [
        pkgs.unstable.osu-lazer-bin
      ];
    }
    
    

I’m personally against 3b, because an overlay for this is completely unnecessary and will likely slow down eval.

PS this has got to be one of the most-asked questions, it’s silly that we are still having to rewrite the answers from scratch in 2024…

5 Likes

Is that what is happening here?

You could add an article to nix.dev or the wiki.

Perfect! Used option 3a, this should definitely be written somewhere in the docs, at least not official.

Hello, I am a bit new to flakes and the nix syntax. The tutorial i followed led me to a different outputs syntax so i am not sure how i can follow your step 1. Can you help? I am figuring this stuff out as i go so there are some holes in my understanding.

{
  description = "My system configuration";
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
    unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
  };

  outputs = { self, nixpkgs, ... }:
    let
      lib = nixpkgs.lib;
    in {
      nixosConfigurations = {
        tartarus = lib.nixosSystem {
          system = "x86_64-linux";
          modules = [ ./configuration.nix ];
          # revision = self.shortRev or self.dirtyShortRev or self.lastModified or "unknown";
        };
      };
    };
}
1 Like

Hey! Since you are new, I’ll throw in some syntax explanation. If you open your configuration.nix, you’ll see something like this:

{ config, pkgs, ... }:
{
  imports = [ ./hardware-configuration.nix ];
  # ...
}

This is a nix function. On the first line you can see the attribute set that would be used as an argument, and the big attribute set after : is the return of the function.

So when you rebuild your system with a flake that you provided, here

tartarus = lib.nixosSystem {
  system = "x86_64-linux";
  modules = [ ./configuration.nix ];
  # revision = self.shortRev or self.dirtyShortRev or self.lastModified or "unknown";
};

you call a function lib.nixosSystem that creates a configuration tartarus for you. Somewhere inside that lib.nixosSystem function the following happens:

  • config is extracted from nixpkgs
  • pkgs is extracted from nixpkgs. It looks something like pkgs = nixpkgs.legacyPackages.${system}; (system is provided by you in argument to nixosSystem)
  • ./configuration.nix is read and called with these (and other) arguments

What you need to do is to extract the unstable version of pkgs and provide it in inputs to function in ./configuration.nix.

To do this nixosSystem allows you adding attributes that would be used in calls to functions from modules. You can add attribute specialArgs = { /* your args */ }; and your args would be used in a call to function in ./configuration.nix. And you would also need to modify ./configuration.nix by adding unstable version of pkgs as input.

So you’ll get something like this:

# In your flake declaration
inputs = {
  nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
  unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs, ... }:
let
  system = "x86_64-linux"
  lib = nixpkgs.lib;
  pkgs-unstable = unstable.legacyPackages.${system};
in {
  nixosConfigurations = {
    tartarus = lib.nixosSystem {
      inherit system; # inherited it from 'let' block
      modules = [ ./configuration.nix ];
      specialArgs = { inherit pkgs-unstable; };
    };
  };
};

# In your configuration.nix:
{ config, pkgs, pkgs-unstable, ... }:
{
  # ...
  environment.systemPackages = with pkgs; [
    neovim # package from pkgs
    pkgs-unstable.ripgrep # package from pkgs-unstable
  ];
  # ...
}
5 Likes

Wow, thank you so much for the detailed explanation. This is literally a game changer. One can write a very dynamic configuration logic with this. It is awesome.

2 Likes