OFFSET偏移
← All articlesOFFSET / FIELD NOTESNO. 031

How Flake updates work

Distinguish input updates, package sets, system generations, and cache downloads.

English translation · Code and commands follow the Chinese original.

IN THESE READING PATHSFlakes: project and dependency management ↗
About 3 min readStart reading

1. Flake = specification; lock = resolved instance#

flake.nix describes the dependencies (dependency specification), while flake.lock pins their resolved instances (dependency resolution).

flake.nix
    │
    ├── nixpkgs = github:NixOS/nixpkgs/nixos-unstable
    └── home-manager = github:nix-community/home-manager

            │

flake.lock
    │
    ├── nixpkgs -> commit A
    ├── home-manager -> commit B
    └── narHash

Without flake.lock, nixos-unstable always points to the latest commit, making builds unreproducible. Reproducibility comes from (flake.nix + flake.lock).


2. Why the update covers all of nixpkgs#

Nix manages a package set, rather than an individual package.

nixpkgs(commit A)
├── git
├── gcc
├── firefox
└── neovim

↓

nixpkgs(commit B)
├── git
├── gcc
├── firefox
└── neovim

Updating the nixpkgs input replaces the definition of the entire package set, potentially changing multiple applications and shared dependencies together. Actual builds involve only the closure required by the configuration.

To track a newer version of just one application, you can introduce a second nixpkgs input and select that package from it, or maintain an overlay, a separate Flake package, or a derivation with pinned source code. These approaches can still introduce different dependencies, so check compatibility. They are not equivalent to an in-place, single-package upgrade with a traditional package manager.


3. What flake update actually does#

nix flake update

Updates all inputs.

nix flake update nixpkgs

Updates only the specified input.

Both essentially do the following:

修改 flake.lock

Neither modifies flake.nix nor automatically rebuilds the system.


4. follows#

Recommended:

home-manager.inputs.nixpkgs.follows = "nixpkgs";

Purpose:

没有 follows

my flake
├── nixpkgs(A)
└── home-manager
    └── nixpkgs(B)

有 follows

my flake
├── nixpkgs(A)
└── home-manager
    └── follows -> nixpkgs(A)

Reduces duplicate input sources. It does not guarantee the removal of every duplicate package in the Store, and version compatibility still needs checking.


5. Generations and the Store#

A generation is not a copy of the system, but a set of symbolic links into /nix/store.

/run/current-system
        │
        └── symlink
                │
                ▼
           /nix/store

Therefore:

  • Generations are lightweight.
  • When the closure is complete, the corresponding build outputs can be reused; evaluation still needs the referenced sources and other inputs.
  • Downloads or builds are needed only for items missing from the Store.

6. Why updates download so much#

Updating flake.lock may change the entire package set.

For example:

mesa
linux
linux-firmware
systemd
glibc
llvm

Even software you did not explicitly install may be updated because the dependency chain has changed.


7. Binary caches and fetchFromGitHub#

Binary Cache:

cache.nixos.org
        ↓
下载已构建好的 nar

fetchFromGitHub:

GitHub
        ↓
下载源码(若 Store/缓存中没有)
        ↓
构建或复用缓存产物

These operate at different stages: both fixed-output source derivations and final packages may be cached. A cache hit can avoid an actual download or build.

A working binary cache ≠ working GitHub access.


8. How a source-download failure propagates through dependencies#

Suppose the first failure occurs while downloading font sources:

example-font
        ↓
fetchFromGitHub
        ↓
codeload.github.com
        ↓
下载失败

Builds that depend on them may then fail as well:

example-font
        ↓
font-package-set
        ↓
fontconfig
        ↓
system-path
        ↓
system

These are all propagated dependency failures, not independent errors.

Troubleshooting rule: always identify the first .drv that failed.


9. Why it cannot automatically switch to jsDelivr#

fetchFromGitHub pins the following:

  • URL
  • hash
  • Output content

Verify the content after changing the source: if the normalized output is exactly the same, the hash can stay; if the content differs, a new, accurate hash is required. Do not assume that archive formats and paths are identical just because you replaced the domain name.

If you maintain the derivation yourself, you can use:

fetchurl

And switch to jsDelivr, but you must:

  • Pin a commit/tag rather than using main
  • Update the hash

10. Key takeaways#

Updating the nixpkgs input changes the package set; Nix can also manage derivations and packages independently. The minimum unit of reproducibility is not the configuration alone, but (flake.nix + flake.lock). System state is not a collection of files, but the reference relationship Generation → Store.

Most seemingly "inconvenient" design choices, such as dependency-graph updates, strict hash verification, and pinned inputs, serve the same goal: reproducibility and consistency.

References & further reading

  1. Nix Flake Manual ↗
  2. Nix flake update ↗
  3. Nix flake check ↗

Adapted from technical notes. Examples use generic names.

Continue along a reading path

Flakes: project and dependency management

  1. 01Flake structure: inputs and outputs
  2. 02flake.lock and reproducibility
  3. 03Flake Utils and project templates
  4. 04External Flake references and collaboration
  5. 05Migrating NixOS configuration to Flakes
  6. 06A tutorial on Flake projects and dependency management
  7. 07How Flake updates work
  8. 08Rolling back and manually editing flake.lock
CONTINUE READING / NEXT ARTICLERolling back and manually editing flake.lock

Preserve existing changes before restoring the lock file, and distinguish dependency rollback from system-generation rollback.