Reuse Nix shell nixpkgs in flake

I’m trying to write a Nix shell which is compatible with both flakes and non-flake environments. I currently have nixpkgs.nix:

import (builtins.fetchTarball {
  name = "nixos-unstable-2024-11-27";
  url = "https://github.com/nixos/nixpkgs/archive/4633a7c72337ea8fd23a4f2ba3972865e3ec685d.tar.gz";
  sha256 = "0z9jlamk8krq097a375qqhyj7ljzb6nlqh652rl4s00p2mf60f6r";
}) { }

and it’s used in shell.nix with pkgs = import ./nixpkgs.nix;. How would I share this with flake.nix (without duplicating any part of nixpkgs.nix)?


I tried to pare down nixpkgs.nix to just

{
  name = "nixos-unstable-2024-11-27";
  url = "https://github.com/nixos/nixpkgs/archive/4633a7c72337ea8fd23a4f2ba3972865e3ec685d.tar.gz";
  sha256 = "0z9jlamk8krq097a375qqhyj7ljzb6nlqh652rl4s00p2mf60f6r";
}

, and to use pkgs = import (builtins.fetchTarball (import ./nixpkgs.nix)) {}; in shell.nix (works), but it seems like such indirect inputs won’t work in flake.nix with inputs.nixpkgs = (import ./nixpkgs.nix);:

error: expected a set but got a thunk at /nix/store/vl8kinwvl9b4k3k0f707hhkccb6y7nnl-source/flake.nix:2:3

Yeah, the flake input section is technically not written in nix, but a subset of the language. You can basically only specify flake input attrsets, precisely to prevent what you’re trying to accomplish. Flakes are designed not to perform any evaluation as part of their basic commands, otherwise things get slow, especially if you have a large dependency tree.

You should probably go the other way around and use builtins.getFlake, or only use traditional nix.

Maybe you could use flake-compat?

The other alternative, which I prefer for various reasons, is to circumvent flake inputs altogether and manage remote sources in stable Nix. For example (untested):

# default.nix
{
  sources ? import ./npins,
}:
let
  /*
  Try to import dependencies managed by `npins` the way `flake.nix` would expect them.
  This is not strictly required but avoids having to refactor an existing `flake.nix`.
  */
  flake-import =
    source:
    if source ? outPath then
      flake-import (import source)
    else
      let
        evil = builtins.tryEval (source.outputs);
      in
      if evil.success then evil.result else source;
in
{
  inputs = builtins.mapAttrs (n: s: flake-import s) sources;
}
# flake.nix
{
  outputs =
    _:
    let
      inputs = (import ./example.nix { }).inputs;
      pkgs = import inputs.nixpkgs { system = "x86_64-linux"; };
    in
    {
      packages.x86_64-linux = {
        inherit (pkgs) hello;
      };
    };
}

This is inspired by this little sketch made by @RaitoBezarius: ryan/flakemelt: Melt all the boundaries! - flakemelt - Newtype's Git

Morally this is no loss over flake inputs, since you can still do static dependency analysis on npins/sources.json. And as opposed to flake inputs, it doesn’t flatten recursive dependencies into flake.lock, so you can control how deep you want to go.

And if you for some reason need to go the other way round and want to keep managing dependencies with flake inputs, you can use the lockfile from default.nix with my port for the original flake-compat that exposes lockfile processing as a library function:

@FedericoSchonborn that one particular flake-compat seems to be unmaintained, and isn’t solving a particular problem I ran into and tried to solve with my library: it only exposes the flake outputs baked into flake-compat, but for full control in stable Nix you only want the lockfile contents resolved to metadata and store paths.

2 Likes

The currently-maintained flake-compat is GitHub - edolstra/flake-compat, though it uses the flakehub silliness in the example which would be better served by the github: url.

getFlake is pretty much broken and inadvisable, see Flakes using paths do not infer `git+file:` despite documentation to that effect · Issue #5836 · NixOS/nix · GitHub which clearly indicates that it behaves differently from actual flake inputs for some reason. And, obviously, that still requires flakes, so being compatible with paths-based nix goes out the window there.

I think I might have found a solution:

That’s basically what flake-compat is suggesting, however, you picked the wrong node.

let
  nixpkgsAttrs = (builtins.fromJSON (builtins.readFile ./flake.lock)).nodes.nixpkgs.locked;
in

is wrong because it will just pick whatever is the first nixpkgs input it runs into within the lockfile (alphabetically), and you may eventually have multiple in the lockfile due to transitive dependencies and whatnot. What you should use is

let
  lock = builtins.fromJSON (builtins.readFile ./flake.lock);
  nixpkgsAttrs = lock.nodes.${lock.nodes.root.inputs.nixpkgs}.locked;
in

to ensure to get the direct value of inputs.nixpkgs.

1 Like