logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·31.10.26
← Back to news

One Nix flake to rule them all

Original article
fzakaria.com|64 points|6 comments|by ingve|Aug 30, 2026

One Nix Flake to Rule Them All

By Farid Zakaria | August 28, 2026 | ⏱️ 8 min read

It is an undeniable fact that Flakes are a permanent fixture of the Nix ecosystem. My personal take on them? They are revolutionary “meh”.

Even though they are ubiquitous, the act of adding a new flake input remains a persistent, minor irritation. While proponents argue that the federated nature of flakes is a feature, in practice, I crave the simplicity of a centralized approach.

The Current Friction

The typical workflow is tedious:

  1. You decide you need disko.
  2. You add the url.
  3. You add a follows attribute so it doesn't pull in its own redundant copy of nixpkgs.
  4. Repeat for every single dependency.

This cycle has become a community meme—specifically, how every single flake seems to drag its own version of flake-utils into the mix.

The Big Question: Could a single "mega-flake" house every other flake, allowing users to simply reach in and grab what they need?


🤯 Introducing omniflake

omniflake is essentially thousands of Nix flakes bundled behind a single flake input.

How to use it

Once you've integrated omniflake, you can treat it as a gateway to other inputs. For example, you can access disko through it. Alternatively, you can bypass the flake configuration entirely and run packages directly from your terminal:

$ nix run 'github:fzakaria/omniflake#flakes.nh.packages.x86_64-linux.default' -- --version

Key Resources:

  • 🌐 Website: https://omniflake.com/ (Contains the full list of carried flakes and documentation).
  • 📦 Capacity: As of this writing, it provides lazy access to nearly 12,000 flakes.

You might assume a flake with thousands of inputs would be completely unusable. However, thanks to the laziness of the Nix language and the way the lock mechanism functions, it works flawlessly.


📚 A Brief Primer on Flakes

To understand why this works, we need to look at what a flake actually is. A flake is just a directory containing a flake.nix file that defines two primary components:

  1. Inputs: The dependencies (other flakes).
  2. Outputs: A function that transforms those inputs into something useful.
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  };
  outputs = { self, nixpkgs }: {
    packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.hello;
  };
}

The Reproducibility Gap

The inputs section defines a loose dependency (e.g., a branch). The flake.lock file captures the exact commit hash, ensuring the build is reproducible.

The problem arises when two different child flakes both depend on nixpkgs but at different commits. Nix handles this by creating multiple nodes in the lock file:

Node NameRoleVersion/Commit
nixpkgsRoot dependency9fbb...
agenixInput5182...
nixpkgs_2agenix →\rightarrow→ home-manager →\rightarrow→ nixpkgs50ab...

This duplication is the source of much frustration. The follows keyword solves this by forcing a dependency to point to a node you already have.

{
  inputs = {
    disko.url = "...";
    disko.follows = "nixpkgs"; 
  };
}

Visualizing the Dependency Collapse

Solid lines represent standard inputs; dashed lines represent follows edges collapsing onto a shared nixpkgs.


⚡ The Power of Laziness

Nix is a lazy language. This means it doesn't evaluate something until it absolutely has to. You can prove this by intentionally breaking your flake.lock file.

If you use sed to replace a valid commit hash with a string of zeros:

$ sed -i 's/2810303efc.../0000000000000000000000000000000000000000/' flake.lock
$ nix eval .#justB [ "x86_64-linux" ]

The command above still works because justB doesn't depend on the corrupted input. Only when you force the evaluation of the "poisoned" input does Nix complain:

error: unable to download '.../0000000000000000000000000000000000000000.tar.gz': HTTP error 404

When adding a flake, Nix simply copies the transitive graph as metadata. No fetching or evaluation occurs at that stage.


⚠️ The "False Start" and the Quadratic Bug

My first attempt at a mega-flake involved adding 11,000 inputs directly into flake.nix. Surprisingly, even with Nix's laziness, evaluating an output that touched nothing became incredibly slow.

The Root Cause: Name Collisions

When Nix generates a lock file, every node needs a unique name. If a collision occurs, Nix appends a suffix (_2, _3, etc.).

The original logic worked like this:

  1. Collision found? Start searching from _2.
  2. Try _2, then _3, then _4... until a free name is found.

In a mega-flake, almost every input has a dependency named systems. With 4,000 inputs, Nix created nodes systems_2 through systems_4000. For the 4,000th input, Nix had to check 3,999 previous names.

This resulted in a time complexity of: O(n2)\mathcal{O}(n^2)O(n2) Where nnn is the number of inputs. This led to roughly 8 million string formatting operations per evaluation.

The Fix

The solution was to track the highest suffix used for each name and resume the search from there, rather than starting at _2 every time.

  • Submit PR NixOS/nix#16387
  • Verify lock files remain byte-identical
  • Test performance

The Result: Performance Graph Placeholder The speedup was dramatic: approximately 21x faster for 4,000 inputs.

Final Note on Locking

To finalize the flake.lock file, Nix must evaluate every input one by one. It fetches each tree to read the flake.nix. If a single input fails to lock, the entire process aborts. I even experimented with assembling the lock file manually from individual flake.lock files to bypass this...