Updating local subflakes inputs when building root flake?

I have trouble wrapping my head on how lockfiles are used when composing subflakes.

Following My subflake does not build properly? or How to package monorepo submodules using Nix?, I got that subflakes are a rather experimental; but corresponds very well to use case I am working on.

I have the following repository structure:

root
|_flake.nix
|_flake.lock
|_vendor/
  |_dep_a/{flake.nix, flake.lock}
  |_dep_b/{flake.nix, flake.lock}
  |_dep_c/{flake.nix, flake.lock}

Each dep_ has its own flake and lockfile. It is important because each dependency will rely on different version of nixpkgs.

My goal is to create in the root flake a shell that provides all outputs of dep_.

I am able to successfully build each dep_ with its own flake. However, when running nix flake build on the root, I noticed that I have build errors.

Tracing back to comparing the dep_ lockfiles and the root lockfile, it seems that I updated the inputs in my dep_, but those updates are not propagated up to the root lockfile (specifically, nixpkgs revisions are not the same).

How can I ensure that my subflakes lock inputs updates are propagated up to the root lock file?

Hello.

You have 3 options:

  1. Call nix flake lock --update-input dep_n for each subflake n whenever it’s modified; this should propogate updates from subflakes to the root flake.
  2. Store each package inside their own separate git repositories, and import them via URL in the root flake.
  3. Don’t use sub-flakes.

I decided to stop trying to use sub-flakes.
Instead, I used raw Nix (default.nix + shell.nix) that the flake imports.
You actually don’t lose any functionality this way; the dependencies are just declared in the root flake.nix.

root/

root
|_flake.nix
|_flake.lock
|_vendor/
  |_dep_a/{default.nix, shell.nix}
  |_dep_b/{default.nix, shell.nix}
  |_dep_c/{default.nix, shell.nix}

root/vendor/dep_n/default.nix

{
  pkgs ? import <nixpkgs> {},
}: let
  name = "dep_n";
  version = "0.0.0";
in rec {
  packages = {
    // TODO declare your package(s) here
    package-name = ;
  };
  devShells.default = import ./shell.nix { inherit pkgs; };
}

root/vendor/dep_n/shell.nix

{
  pkgs ? import <nixpkgs> {},
}: pkgs.mkShell {
  packages = [
    pkgs.nodejs               # npm & npx
    pkgs.prefetch-npm-deps    # see packages/server.nix
    pkgs.nix-prefetch-docker  # see packages/docker-image.nix
  ];
}

For your use case, you need different versions of nixpkgs for your modules; so declare more than one in your root flake, and pass them to your modules.

root/flake.nix

{
  description = "root flake for each dep_n";
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-24.11";
    nixpkgs-dep-n.url = "github:nixos/nixpkgs/nixos-24.05";
    flake-utils.url = "github:numtide/flake-utils";
  };
  outputs = { self, nixpkgs, nixpkgs-dep-n, flake-utils, ... }: let
    name = "root-flake";
    version = "0.0.0";
    utils = flake-utils;
  in utils.lib.eachDefaultSystem (
    system: let
      pkgs = import nixpkgs { inherit system; };
      pkgs-dep-n = import nixpkgs-dep-n { inherit system; };
      # import each package from vendor/ in "modules"
      modules = {
        dep-n = import ./vendor/dep_n { inherit pkgs-dep-n; };
      }
    in rec {
      packages = {
        # TODO insert some nix derivation that consumes packages from "modules"
        # modules.dep-n.package-name
        default = ;
      };
      apps = {
        default = utils.lib.mkApp { drv = packages.default; };
      };
      # the default devShell for each module is provided by nix develop
      devShells = (pkgs.lib.attrsets.mapAttrs
        (n: v: v.devShells.default)
        modules
      ) // {
        # the root devShell provides additional packages
        root = pkgs.mkShell {
          packages = [
            # TODO declare some additional dev packages for root flake
          ];
          shellHook = ''
            # TODO export ENV_VAR=""
          '';
        };
        # the default devShell is a combination of every devShell
        default = pkgs.mkShell {
          inputsFrom = (
            pkgs.lib.attrsets.mapAttrsToList
            (n: v: devShells."${n}")
            modules
          ) ++ [ devShells.root ];
        };
      };
    }
  );
}

For a simple and complete example of this architecture, check out github.com/mboyea/www-mboyea-com

1 Like

Which exact version of nix are you using? I assume it’s at least 2.26 because there were some important changes in 2.26 relating to relative path inputs.

I have done some testing of subflakes using nix 2.26.1. In general, things are working very well for me.

Testing with your example, changes to ./dep_n/flake.lock aren’t automatically propagated to the root flake.lock.

Certainly, the changes to ./dep_n/flake.lock are only propagated to flake.lock after running nix flake update dep_n.

Details of testing
$ nix --version
nix (Nix) 2.26.1

$ jq -r '.nodes["nixpkgs"].locked.rev' flake.lock dep_a/flake.lock
c80f6a7e10b39afcc1894e02ef785b1ad0b0d7e5
c80f6a7e10b39afcc1894e02ef785b1ad0b0d7e5

$ nix flake update --flake path:$(pwd)/dep_a --override-input nixpkgs github:nixos/nixpkgs/nixos-24.11   
warning: updating lock file '"/home/user/root/dep_a/flake.lock"':
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/c80f6a7e10b39afcc1894e02ef785b1ad0b0d7e5?narHash=sha256-C7jVfohcGzdZRF6DO%2BybyG/sqpo1h6bZi9T56sxLy%2Bk%3D' (2025-03-15)
  → 'github:nixos/nixpkgs/a1185f4064c18a5db37c5c84e5638c78b46e3341?narHash=sha256-DDe16FJk18sadknQKKG/9FbwEro7A57tg9vB5kxZ8kY%3D' (2025-03-16)

$ jq -r '.nodes["nixpkgs"].locked.rev' flake.lock dep_a/flake.lock
c80f6a7e10b39afcc1894e02ef785b1ad0b0d7e5
a1185f4064c18a5db37c5c84e5638c78b46e3341

$ nix flake update dep_a
warning: Git tree '/home/user/root' is dirty
warning: updating lock file '"/home/user/root/flake.lock"':
• Updated input 'dep_a':
    'path:dep_a'
  → 'path:dep_a'
• Updated input 'dep_a/nixpkgs':
    'github:nixos/nixpkgs/c80f6a7e10b39afcc1894e02ef785b1ad0b0d7e5?narHash=sha256-C7jVfohcGzdZRF6DO%2BybyG/sqpo1h6bZi9T56sxLy%2Bk%3D' (2025-03-15)
  → 'github:nixos/nixpkgs/a1185f4064c18a5db37c5c84e5638c78b46e3341?narHash=sha256-DDe16FJk18sadknQKKG/9FbwEro7A57tg9vB5kxZ8kY%3D' (2025-03-16)
warning: Git tree '/home/user/root' is dirty

$ jq -r '.nodes["nixpkgs"].locked.rev' flake.lock dep_a/flake.lock
a1185f4064c18a5db37c5c84e5638c78b46e3341
a1185f4064c18a5db37c5c84e5638c78b46e3341

I’m not sure whether this is a bug or intended behaviour.

On one hand, I would expect lock files to keep inputs locked, unless the user asks for an update. On the other hand, with relative path inputs, changes to ./dep_n/flake.nix take effect immediately, so perhaps changes to ./dep_n/flake.lock should also take effect immediately.

Indeed, I see that there is no evident choice. My expectation would be that an update on the lockfile of the subdependency would be propagated to the root flake (given that the root flake does not constraint anything on the subflake deps). But I see how this may become a problem if the root flake would constraint the inputs (for instance, in case of inputs.nixpkgs.follows AND a local subflake, who should take precedence?).

Thanks for your answer. I read your initial post thoroughly. I may consider switching to this, but I think having separate lockfiles for the dependencies is actually more sustainable in the long run (the root lockfile is clogged already).

So after a change to ./dep_a/flake.lock, next time the root flake is evaluated, nix should print:

$ nix build .
warning: Git tree '/home/user/root' is dirty
unpacking 'github:nixos/nixpkgs/<rev>'' into the Git cache
warning: updating lock file '"/home/user/root/flake.lock"':
• Updated input 'dep_a/nixpkgs':
    <previous value>
  → <value from ./dep_a/flake.lock>

Yes, that way seems to me the better choice.

In the case of a follows from the root flake, I would expect it to work the same way it does for any non-relative-path flake input. That is, the root flake follows overrides both ./dep_n/flake.lock and ./dep_n/flake.nix.

1 Like