Nix Flakes and Their Guix Equivalents

birdculture1 pts0 comments

Nix Flakes and their Guix Equivalents

I want to tell you a secret that took me WAY too long to internalize: there is<br>no such thing as "the Guix equivalent of a flake."

I KNOW!! That sounds like a cop-out!! But stick with me — it's actually the<br>most interesting thing about this entire comparison. Flakes are one big feature<br>that solves a bunch of problems at once (Dolstra, 2020). Guix solves<br>those same problems, but it does so with a collection of smaller, orthogonal<br>tools that were mostly in place before flakes even existed (Contributors, 2025a).<br>So the mapping isn't flake → ??? — it's flake → channels, and manifests, and<br>guix describe, and guix shell, and operating-system declarations, and…

Okay I'm getting ahead of myself. Let me set the stage first.

1. Who is this for?

If you're a Nix person, you probably know Guix as "that other functional package<br>manager, the one that uses Scheme." You might have heard it described as a Nix<br>fork — and there is shared lineage! Guix reused the Nix daemon (the C++<br>component that handles build isolation and store management) and builds<br>everything else from scratch in Guile Scheme ((rekado), 2019). The<br>derivation format (ATerm) is shared (Khana, 2026), the daemon<br>lineage is shared, but the language, the package definitions, the service<br>system, the whole world above the daemon — that's all Guix's own thing<br>((rekado), 2020).

If you're a Guix person, you've probably seen Nix people talking about "flakes"<br>and wondered what the big deal is. Maybe you've felt a tiny pang of — wait, do<br>we not have that? Are we behind?

The answer to both is: RELAX!! Guix has all of the capabilities that flakes<br>provide. It just delivers them in a different shape. And understanding that<br>shape is what this post is about.

2. What even is a flake, anyway?

A brief primer for the Guix folks in the room!! (Nix people, you can skim this<br>— or maybe read it anyway, sometimes the refresher is nice :3)

A Nix flake is, at its core, just a source tree — typically a Git repository —<br>that contains a file called flake.nix in its root. That's it. The presence of<br>flake.nix is what makes it a flake (Project, 2024). The file has a<br>specific structure:

# A human-readable description of what this flake provides.<br>description = "A simple Go web server";

# Dependencies — other flakes, Git repos, tarballs. Nix fetches them,<br># evaluates them, and passes them to outputs.<br>inputs = {<br># "github:NixOS/nixpkgs/nixos-unstable" means: fetch the nixos-unstable<br># branch from github.com/NixOS/nixpkgs<br>nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";<br>};

# outputs is a function. Its parameters are the resolved inputs (plus the<br># special "self" input, which is this flake itself). It returns an attrset of<br># things this flake provides.<br>outputs = { self, nixpkgs }:<br>let<br># Helper to generate outputs for multiple CPU architectures without<br># repeating yourself. Without this you'd need to write<br># packages.x86_64-linux, packages.aarch64-linux, etc. separately for every<br># single platform.<br>supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" ];<br>forAllSystems = nixpkgs.lib.genAttrs supportedSystems;<br>in {<br># Packages this flake can build. nix build .#myapp looks here. The<br># level is mandatory — flakes require explicit platform<br># targeting.<br>packages = forAllSystems (system:<br>let<br># Import nixpkgs for the current system. This gives us access to the<br># full nixpkgs package set — stdenv, buildGoModule, and ~120,000 other<br># packages [cite:@repology].<br>pkgs = nixpkgs.legacyPackages.${system};<br>in {<br># "default" means nix build . (without specifying a name) builds<br># this package.<br>default = pkgs.buildGoModule {<br>pname = "myapp";<br>version = "0.1.0";<br>src = ./.; # The entire git repo becomes the source<br>};<br>});

# Development shells. nix develop looks here. Think: a reproducible<br># shell with all your build tools.<br>devShells = forAllSystems (system:<br>let pkgs = nixpkgs.legacyPackages.${system};<br>in {<br>default = pkgs.mkShell {<br># These packages will be available in PATH inside the shell.<br>buildInputs = with pkgs; [ go gopls gotools ];<br>};<br>});<br>};

Three moving parts:

descriptionA human-readable string. Self-explanatory.<br>inputsYour dependencies — other flakes, Git repos, tarballs. Nix fetches<br>them, pins them, and passes them to your outputs function.<br>outputsA function that receives all the resolved inputs and produces a<br>structured attrset of things — packages, dev shells, NixOS configurations,<br>overlays, and so on (Project, 2024).

When you run any nix command against a flake, Nix also generates a flake.lock —<br>a JSON file that pins every input and their inputs, transitively, to exact<br>revisions. This lock file is what makes builds reproducible across machines and<br>across time.

Flakes also enforce pure evaluation: no $NIX_PATH, no builtins.currentSystem, no<br>environment variables leaking in. Everything must be explicit<br>(Contributors, 2026).

So, to summarize, flakes do roughly six things:

Declare dependencies (inputs)

Pin dependencies (flake.lock)

Enforce...

flake guix flakes nixpkgs system nixos

Related Articles