nixpkgs: Overlays, Channels and Patches
The vocabulary
Section titled “The vocabulary”An overlay on one host
Section titled “An overlay on one host”An overlay is plain NixOS configuration, so it goes in a class module:
{ inputs, ... }:{ den.aspects.igloo.nixos = { pkgs, ... }: { nixpkgs.overlays = [ inputs.rust-overlay.overlays.default ]; environment.systemPackages = [ pkgs.rust-bin.stable.latest.default ]; };}A remote flake’s overlay and your own local overlay are used the same way. To apply it everywhere, put it on an aspect that every host includes:
{ den, inputs, ... }:{ den.aspects.my-overlays.os.nixpkgs.overlays = [ inputs.rust-overlay.overlays.default (final: prev: { my-tool = final.callPackage ./pkgs/my-tool.nix { }; }) ];
den.schema.host.includes = [ den.aspects.my-overlays ];}os writes to nixos and darwin alike. den.default.includes works too,
but it also reaches every user and home, so the host schema is the narrower
choice for a host-level setting.
Overlays contributed by the aspects that need them
Section titled “Overlays contributed by the aspects that need them”An overlay listed centrally stays applied after the aspect that needed it is
removed. The alternative is a quirk: each aspect contributes its overlays under
a nixpkgs-overlays key, and one consumer per class applies whatever was
collected. The full recipe is in
Quirk Recipes.
The short version:
den.quirks.nixpkgs-overlays.description = "nixpkgs overlays collected from aspects";
den.aspects.rust.nixpkgs-overlays = _: [ inputs.rust-overlay.overlays.default ];
den.aspects.nixpkgs-overlays = { os = { nixpkgs-overlays, lib, ... }: { nixpkgs.overlays = lib.unique nixpkgs-overlays; }; homeManager = { nixpkgs-overlays, lib, ... }: { nixpkgs.overlays = lib.unique nixpkgs-overlays; };};den.default.includes = [ den.aspects.nixpkgs-overlays ];Home Manager: own pkgs or the host’s
Section titled “Home Manager: own pkgs or the host’s”Which package set a home uses decides where its overlays must be:
flowchart TD
q{"home-manager.useGlobalPkgs"}
q -->|"false (Home Manager default)"| own["Each home builds its own pkgs"]
q -->|"true"| shared["Homes reuse the host's pkgs"]
own --> ownWhere["Overlays for home packages go in the homeManager class"]
shared --> sharedWhere["Overlays for home packages go in the host's os / nixos class"]
| Host overlay visible in the home | Home’s nixpkgs.overlays | |
|---|---|---|
| own pkgs (default) | no | applied |
useGlobalPkgs = true | yes | ignored, with a Home Manager warning |
Den does not set useGlobalPkgs. Set it on the host like any other option:
den.aspects.igloo.nixos.home-manager.useGlobalPkgs = true;Under useGlobalPkgs, an overlay a user aspect contributes must reach the
host. With the quirk above, a policy on the user scope exposes the user’s
nixpkgs-overlays up to the host, so the host’s consumer includes them (see
the worked example).
A standalone home (den.homes.<system>.<name>) has no host. Its base package
set is the home’s pkgs option, inputs.nixpkgs.legacyPackages.<system> by
default, and nixpkgs.overlays in its homeManager class is applied on top:
den.homes.x86_64-linux.tux.pkgs = import inputs.nixpkgs { system = "x86_64-linux"; overlays = [ inputs.rust-overlay.overlays.default ];};Stable and unstable together
Section titled “Stable and unstable together”There are three ways to mix channels, depending on how much of the system should move.
A second package set as pkgs.unstable
Section titled “A second package set as pkgs.unstable”Most flakes want the host on stable and a few packages from unstable. An
overlay adds the unstable set as an attribute of pkgs:
{ den, inputs, ... }:{ den.aspects.unstable-packages.os.nixpkgs.overlays = [ (final: _prev: { unstable = import inputs.nixpkgs-unstable { inherit (final.stdenv.hostPlatform) system; inherit (final) config; }; }) ]; den.schema.host.includes = [ den.aspects.unstable-packages ];
den.aspects.igloo.nixos = { pkgs, ... }: { environment.systemPackages = [ pkgs.git pkgs.unstable.helix ]; };}inherit (final) config passes the host’s nixpkgs config to the second set, so
an unfree package allowed with
den.batteries.unfree is
also allowed in pkgs.unstable. With useGlobalPkgs = true, homes see
pkgs.unstable as well. Without it, add the same overlay in the
homeManager class, or contribute it through the quirk.
A module argument instead
Section titled “A module argument instead”If you’d rather not add to pkgs, pass the second set as its own module
argument. This mirrors how
den.batteries.inputs' works:
{ den, inputs, ... }:let unstableFor = { host, ... }: { _module.args.unstable = import inputs.nixpkgs-unstable { inherit (host) system; }; };in{ den.aspects.pkgs-unstable = { os = unstableFor; homeManager = unstableFor; }; den.default.includes = [ den.aspects.pkgs-unstable ];
den.aspects.igloo.nixos = { unstable, ... }: { environment.systemPackages = [ unstable.helix ]; }; den.aspects.tux.homeManager = { unstable, ... }: { home.packages = [ unstable.helix ]; };}This set has its own nixpkgs config. Allow unfree packages in the import
call if you need them.
A whole host on another channel
Section titled “A whole host on another channel”To move an entire host, change what builds it. instantiate is an option of
the host entity (not of its aspect). It defaults to
inputs.nixpkgs.lib.nixosSystem for nixos hosts:
den.hosts.x86_64-linux.igloo = { instantiate = inputs.nixpkgs-unstable.lib.nixosSystem; # keep Home Manager on the matching release home-manager.module = inputs.home-manager-unstable.nixosModules.home-manager;};To pick the channel by name, see the worked example.
Patching nixpkgs
Section titled “Patching nixpkgs”A patch that hasn’t been merged, or a backport, means building nixpkgs itself
from a patched source. Apply the patches with applyPatches, load the patched
tree’s flake, and give its nixosSystem to the host:
{ inputs, ... }:let # the machine that evaluates the flake, not necessarily the target pkgs = inputs.nixpkgs.legacyPackages.x86_64-linux;
src = pkgs.applyPatches { name = "nixpkgs-patched"; src = inputs.nixpkgs; patches = [ (pkgs.fetchpatch { url = "https://github.com/NixOS/nixpkgs/pull/<number>.diff"; hash = "sha256-…"; }) ./patches/my-fix.patch ]; };
nixpkgs-patched = (import "${src}/flake.nix").outputs { self = nixpkgs-patched // { outPath = src; }; };in{ den.hosts.x86_64-linux.igloo.instantiate = nixpkgs-patched.lib.nixosSystem;}The host’s pkgs and its NixOS modules both come from the patched tree. Other
hosts keep the unpatched input.
Unfree and insecure packages
Section titled “Unfree and insecure packages”nixpkgs refuses to evaluate unfree packages, and packages marked insecure, unless you allow them. Den’s batteries allow them by name from the aspect that uses them, so the permission goes away with the aspect:
den.aspects.diagrams = { includes = [ (den.batteries.unfree [ "drawio" ]) ]; homeManager = { pkgs, ... }: { home.packages = [ pkgs.drawio ]; };};
den.aspects.legacy-client.includes = [ (den.batteries.insecure [ "openssl-1.1.1w" ])];Several aspects can each include the battery. The names from all of them are
merged into one list on the host, so allowing unrar in one aspect and
vscode in another permits both.
The batteries write nixpkgs.config.allowUnfreePredicate and
nixpkgs.config.permittedInsecurePackages for each class. Included from a user
aspect, the names are allowed on the host too, which is what a home needs under
useGlobalPkgs. See
Batteries for details.
Worked example: a multi-channel fleet
Section titled “Worked example: a multi-channel fleet”This is adapted from a production den flake with a few dozen hosts. Names are generic, and the structure is kept.
-
Pick each host’s channel by name
The host schema gains a
channeloption. The builder and the matching Home Manager module are derived from it, withmkDefaultso a single host can still override them:{ inputs, ... }:letchannels = {nixos-stable = {nixosSystem = inputs.nixpkgs.lib.nixosSystem;hmModule = inputs.home-manager.nixosModules.home-manager;};nixos-unstable = {nixosSystem = inputs.nixpkgs-unstable.lib.nixosSystem;hmModule = inputs.home-manager-unstable.nixosModules.home-manager;};nixpkgs-master = {nixosSystem = inputs.nixpkgs-master.lib.nixosSystem;hmModule = inputs.home-manager-master.nixosModules.home-manager;};};in{den.schema.host = { config, lib, ... }: {options.channel = lib.mkOption {type = lib.types.enum (builtins.attrNames channels);default = "nixos-unstable";};config = {instantiate = lib.mkDefault channels.${config.channel}.nixosSystem;home-manager.module = lib.mkDefault channels.${config.channel}.hmModule;};};den.hosts.x86_64-linux.media-server.channel = "nixpkgs-master";}The source flake also has darwin entries (
darwinSystem, the darwin Home Manager module) keyed onconfig.class. -
One aspect owns the package set
A single
nixpkgsaspect, included by every host, holds the shared config, the base overlays and both consumers:{ inputs, withSystem, ... }:letconfig.allowUnfree = true;in{den.quirks.nixpkgs-overlays.description = "nixpkgs overlays collected from aspects";den.aspects.nixpkgs = {nixpkgs-overlays = _: [# this flake's own packages, as pkgs.local(final: _prev: {local = withSystem final.stdenv.hostPlatform.system ({ config, ... }: config.packages);})# pkgs.unstable, as in the section above(final: _prev: {unstable = import inputs.nixpkgs-unstable {inherit (final.stdenv.hostPlatform) system;config.allowUnfree = true;};})# version bumps and patched builds of individual packages(import ./overlays/modifications.nix { inherit inputs; })];os = { nixpkgs-overlays, lib, ... }: {nixpkgs = {inherit config;overlays = lib.unique nixpkgs-overlays;};};homeManager = { host, nixpkgs-overlays, lib, ... }:lib.mkIf (!host.sharedHomePkgs) {nixpkgs = {inherit config;overlays = lib.unique nixpkgs-overlays;};};};}The
homeManagerhalf stands down on hosts that share their package set, because Home Manager ignoresnixpkgs.*there. -
Aspects bring their own overlays
Everything else is contributed by the aspect that uses it, wrapped in
_:. A language aspect brings its toolchain overlay, a gaming aspect its compatibility-layer overlay, and a CPU aspect patches one package’s build:den.aspects.rust = {nixpkgs-overlays = _: [ inputs.fenix.overlays.default ];homeManager = { pkgs, ... }: {home.packages = [ (pkgs.fenix.complete.withComponents [ "cargo" "clippy" "rustc" ]) ];};};den.aspects.cpu-amd.nixpkgs-overlays = _: [(_final: prev: {ucodenix = prev.ucodenix.overrideAttrs (old: {nativeBuildInputs = (old.nativeBuildInputs or [ ]) ++ [ prev.jql ];});})];A host without the
rustaspect never applies thefenixoverlay. -
Shared package sets: project user overlays to the host
A host flag turns
useGlobalPkgson. When it is set, a policy exposes each user’s overlays to the host’s consumer, sorustincluded on a user still reaches the one package set that user’s home now uses:{ den, lib, ... }:{den.schema.host = {includes = [ den.aspects.home-manager-shared ];options.sharedHomePkgs = lib.mkOption {type = lib.types.bool;default = false;};};den.aspects.home-manager-shared.os = { host, ... }: {home-manager.useGlobalPkgs = host.sharedHomePkgs;home-manager.useUserPackages = true;};den.policies.project-user-overlays = { user, host, ... }:lib.optionals host.sharedHomePkgs [(den.lib.policy.pipe.from "nixpkgs-overlays" [ den.lib.policy.pipe.expose ])];den.schema.user.includes = [ den.policies.project-user-overlays ];}On hosts without the flag the policy returns nothing, and each home keeps collecting its own overlays. The source flake declares the flag as a typed per-aspect setting (Aspect Settings). A plain host option works the same way.
See also
Section titled “See also”- Quirk Recipes: the overlay recipe in full, plus other contributor/consumer patterns
- Home Environments: enabling Home Manager, custom Home Manager modules, standalone homes
- Batteries:
unfree,insecure,inputs',self' - Schema: the host’s
instantiateoption and its defaults