-----BEGIN YUNFACHI WEBSITE-----
Separate development inputs into their own `dev` flake to avoid redundant dependencies - 2026-08-20
[back to posts] [edit]
-----BEGIN POST CONTENT-----
First of all, this post is aimed at flakes that provide external outputs, like `nixosModules` or `packages` — your private NixOS configuration DOES NOT need this. For example, you have a project called `niri-nix`, which introduces Niri's NixOS module and Niri package. But besides nixpkgs, that flake's inputs also include `flake-parts`, `treefmt-nix`, `git-hooks.nix`, and so on. Nix flake inputs are NOT lazily fetched. This means that if you only use, for example, `nixosModules.default` from that flake, and `nixosModules.default` does not mention `treefmt-nix` or any of the other inputs at all, they will still be added to the user's flake lock as dependencies of your flake, and they will still be downloaded. ? But how can you prevent some inputs from being redundantly and meaninglessly fetched? > Implement a separate flake for those inputs — I call it a `dev` flake. The idea is to keep development-only inputs outside of the flake that is consumed by users. There are 2 ways to set up a `dev` flake: 1. Use the `dev` flake for both inputs and logic, while the main flake only needs to integrate the `dev` flake's outputs into itself. 2. Use the `dev` flake only for inputs, while the logic stays in the main flake. Both of these methods will be discussed. But to quickly answer which is the better option: if your `dev` logic is huge, then choose the first option; otherwise, you are good to go with the second one. Let's begin with the first option. First, initialize the `dev` flake in the `dev/` directory: 01# dev/flake.nix 02{ 03 inputs = { 04 # By this "hack", you can access your root flake's inputs to deduplicate them 05 # in the `dev` flake too. It only works in a git repository. 06 # my-project.url = "path:../."; 07 # nixpkgs.follows = "my-project/nixpkgs"; 08 09 # Nixpkgs is required for flake-parts `perSystem` to work. 10 nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 11 12 # Remove this if flake-parts is already used in your root flake 13 # or if you want to implement the mechanism yourself. 14 flake-parts = { 15 url = "github:hercules-ci/flake-parts"; 16 # inputs.nixpkgs-lib.follows = "nixpkgs"; 17 }; 18 19 # Add any inputs that you want here; `treefmt-nix` is used as an example. 20 treefmt-nix = { 21 url = "github:numtide/treefmt-nix"; 22 # inputs.nixpkgs.follows = "nixpkgs"; 23 }; 24 }; 25 26 # Logic will be written in a separate file. 27 outputs = _: { }; 28} Then generate the lock file by running this command in the `dev/` directory: $nix flake lock ? Can I do this without flake-parts? > Yes, but you'll need to implement your own mechanism for separating the `dev` flake's logic into another file. This can be a bit tricky. You may want to consider the second option below instead. Now we'll create a flake-parts module in it. Don't worry, flake-parts is not required in your root flake: 01# dev/config.nix 02# The content of this file is just an example. You can write anything you want here: 03# https://flake.parts/options/flake-parts.html 04{ inputs, ... }: { 05 systems = [ "x86_64-linux" ]; 06 07 imports = [ inputs.treefmt-nix.flakeModule ]; 08 09 perSystem = _: { 10 # https://flake.parts/options/treefmt-nix.html 11 treefmt = { 12 projectRootFile = "flake.nix"; 13 14 programs = { 15 nixfmt.enable = true; 16 deadnix.enable = true; 17 }; 18 }; 19 }; 20} Finally, you have to export the `dev` outputs to your root flake. If you already use `flake-parts` in your root flake, you should use its partitions feature instead. Otherwise, to avoid adding flake-parts to the root flake's dependencies, write this: 01# flake.nix 02{ 03 inputs = { 04 # You may remove this; read the comments below. 05 flake-compat.url = "github:NixOS/flake-compat"; 06 07 # ... 08 }; 09 10 outputs = { flake-compat, self, ... }: let 11 # If you have `flake-compat` in your inputs, keep this: 12 devFlake = (import flake-compat { src = ./dev; }).defaultNix; 13 14 # But if you don't want `flake-compat` itself to become a dependency of your users, you can fetch the pinned flake-compat implementation directly: 15 # devFlake = 16 # (import (builtins.fetchurl { 17 # url = "https://raw.githubusercontent.com/NixOS/flake-compat/f275e157c50c3a9a682b4c9b4aa4db7a4cd3b5f2/default.nix"; 18 # sha256 = "sha256:0nycwx0777d451k63ghp6p5lcv791kziqgkvqzmr1qwzywkdk1cj"; 19 # }) { src = ./dev; }).defaultNix; 20 21 devOutputs = devFlake.inputs.flake-parts.lib.mkFlake { 22 inputs = devFlake.inputs // { 23 my-project = self; 24 self = devFlake; 25 }; 26 } ./dev/config.nix; 27 in { 28 # Explicitly inherit outputs to prevent forcing evaluation of the `dev` flake 29 # when accessing root flake outputs. 30 inherit (devOutputs) checks formatter; 31 32 # Some output that is free from fetching `dev` inputs. 33 # nixosModules.default = ...; 34 }; 35} Harder than just having `treefmt-nix` in the root flake's inputs? Yes. But better for users? Absolutely. This approach is also used in the following projects: - ://github.com/unnamed-systems/nix-secrets (without flake-parts in the root flake) - ://github.com/NixOS/flake-compat (without flake-parts in the root flake) - ://github.com/hercules-ci/flake-parts (with flake-parts in the root flake) --- Now, let's discuss the second option. First, you need to initialize the `dev` flake: 01# dev/flake.nix 02{ 03 inputs = { 04 # By this "hack", you can access your root flake and its inputs to 05 # deduplicate them in the `dev` flake too. It only works in a git repository. 06 # my-project.url = "path:../."; 07 # nixpkgs.follows = "my-project/nixpkgs"; 08 09 # Add any inputs that you want here; `treefmt-nix` is used as an example. 10 treefmt-nix = { 11 url = "github:numtide/treefmt-nix"; 12 # inputs.nixpkgs.follows = "nixpkgs"; 13 }; 14 }; 15 16 # This flake is only for inputs; there is no logic here. 17 outputs = _: { }; 18} Then generate the lock file by running this command in the `dev/` directory: $nix flake lock And, finally, you have to access the `dev` inputs from the root flake: 01# flake.nix 02{ 03 inputs = { 04 # You may remove this; read the comments below. 05 flake-compat.url = "github:NixOS/flake-compat"; 06 07 # ... 08 }; 09 10 outputs = { flake-compat, ... }: let 11 # If you have `flake-compat` in your inputs, keep this: 12 devInputs = (import flake-compat { src = ./dev; }).defaultNix.inputs; 13 14 # But if you don't want `flake-compat` itself to become a dependency of your users, you can fetch the pinned flake-compat implementation directly: 15 # devInputs = 16 # (import (builtins.fetchurl { 17 # url = "https://raw.githubusercontent.com/NixOS/flake-compat/f275e157c50c3a9a682b4c9b4aa4db7a4cd3b5f2/default.nix"; 18 # sha256 = "sha256:0nycwx0777d451k63ghp6p5lcv791kziqgkvqzmr1qwzywkdk1cj"; 19 # }) { src = ./dev; }).defaultNix.inputs; 20 in { 21 # Some output that is free from fetching `dev` inputs. 22 # nixosModules.default = ...; 23 24 # Actually, read the `treefmt-nix` manual for the proper setup of it; this post is not about how to use `treefmt-nix`. 25 formatter = devInputs.treefmt-nix.formatter; 26 }; 27}
-----END POST CONTENT-----
-----END YUNFACHI WEBSITE-----
Evaluated with Nix 2.34.7 on x86_64-linux
Revision 48fa63b (modified on 2026-10-22)