Nix Package Management: A Modular Approach for Customization

Nix Package Management: A Modular Approach for Customization

Play this article

As a passionate Linux enthusiast, I've always been drawn to the freedom and customization that Linux offers. Choosing my desktop environment, selecting the perfect software stack, and tailoring my system to my preferences – that's what Linux is all about. However, managing packages on Linux can sometimes be quite a challenge.

Introducing Nix

That's where Nix, the package manager for NixOS, comes into play. It revolutionizes package management by introducing a modular approach, allowing me to fine-tune every aspect of my system.

What is Nix?

Nix is an open-source package manager and configuration management system initially developed by Eelco Dolstra in the early 2000s. Unlike traditional package managers, such as APT, YUM, or Homebrew, Nix takes a unique approach to package management by focusing on functional package management. This means that packages are built and managed in an isolated, reproducible environment, ensuring that they don't interfere with one another and can be rolled back or upgraded with ease.

Beyond package management, NixOS extends Nix's capabilities to system configuration. In NixOS, you define your entire system's configuration using Nix expressions. This declarative approach makes it easier to manage and reproduce system configurations.

Declarative Configuration with NixOS

NixOS uses a declarative approach to system configuration. Instead of manually configuring your system through various configuration files, you describe your system's entire configuration in a single, well-structured NixOS configuration file. This file specifies everything from package installations to system services, users, and hardware settings.

# configuration.nix
{ config, pkgs, ... }:

{
  imports = [
      /etc/nixos/hardware-configuration.nix
      ./neeraj.nix # Note this!
  ];

  boot.loader.grub.enable = true;
  networking.firewall.enable = true;
  services.sshd.enable = true;

  environment.systemPackages = with pkgs; [
    vim
    wget
  ];
}

Managing Packages

In the example provided above, we demonstrated how to specify a list of packages for system-wide installation within our configuration.nix file. However, if you intend to install software for a specific user, you can follow these steps:

  1. Create a new file named <username>.nix. In this instance, I will use neeraj.nix as the filename. Don't forget to import this file in the configuration.nix file.

  2. Within the neeraj.nix file, import the necessary modules and define the user. Users can be defined in the users.users attribute, as illustrated below:

# neeraj.nix
{ config, lib, pkgs, ... }:

# Your configuration options go here
users.users.neeraj = {
    isNormalUser = true;
    description = "Neeraj";
    extraGroups = [ "networkmanager" "wheel" "libvirtd" ];
    packages = with pkgs; [
       vim
       thunar
    ];
};

Grouping Applications

Organize your applications logically. You can group them based on categories like development tools, utilities, or graphical applications:

users.users.neeraj = with pkgs; [
  # Development Tools
  git
  python
  nodejs

  # Utilities
  htop
  tmux

  # Graphical Applications
  gimp
  inkscape
];

This type of grouping helps to streamline your system configuration and package management in several ways:

  • Ease of Maintenance: By organizing your applications logically, you make it easier to update and manage your system. When you want to add or remove packages, you can quickly find them within their respective categories. This reduces the chances of overlooking essential tools during system updates.

  • Modularity: Grouping packages by function or purpose promotes modularity. You can easily reuse or share specific categories of packages with others who have similar needs. For instance, you can share your development tools category with fellow developers, making it convenient for them to set up their development environments.

Enhanced Modularity

Let us take a step further. I've organized packages into modular sections. This approach enhances code readability, maintainability, and flexibility. It's like building with Lego blocks – I can mix and match to create my perfect package set.

{
  /* Modular Package Lists */
  # Unstable apps
  unstableApps = with pkgs; with unstable; [
    nodejs_20
  ];

  # Common stable apps
  commonStableApps = with pkgs; [
    brave
  ];
}

Now, we have the flexibility to create combinations of these lists and append them to the user or environment settings for installation on our system. This modular approach simplifies customization and allows us to tailor our package selection precisely to our requirements.

Final Configuration

# neeraj.nix

{ config, lib, pkgs, ... }:

let
  /*
  sudo nix-channel --add https://nixos.org/channels/nixos-unstable nixos-unstable
  sudo nix-channel --update
  */
  unstable = import <nixos-unstable> { config = { allowUnfree = true; }; };

  # Qtile specific packages
  qtileApps = with pkgs; [
    alacritty
    blueman
    dmenu
    dunst
    gnome.gnome-keyring
    libnotify
    libsecret
    nitrogen
    xfce.thunar
  ];

  # Gnome specific packages
  gnomeApps = with pkgs; [
    gnome.gnome-tweaks
  ];

  # XFCE Specific packages
  xfceApps = with pkgs; with xfce; [
    xfce4-pulseaudio-plugin
  ];

  # KDE Specific packages
  kdeApps = with pkgs; with libsForQt5; [
    plasma-browser-integration
  ];

  # Unstable apps
  unstableApps = with pkgs; with unstable; [
    nodejs_20
    virt-manager
    virtualenv
    vlc
    vscode
    zoom-us
  ];

  # Apps to be installed irrespective of desktop env
  commonStableApps = with pkgs; [
    brave
    firefox
    gh
    git
    gparted
    htop
    micro
    neofetch
    onlyoffice-bin
    parted
    python3
    ventoy-full
  ];

  appendApps = apps: kdeApps ++ unstableApps ++  commonStableApps ++ apps;

in {
  users.users.neeraj = {
    isNormalUser = true;
    description = "Neeraj";
    extraGroups = [ "networkmanager" "wheel" "libvirtd" ];
    packages = with pkgs; appendApps [];
  };
}

Explanation

This script, in particular, is part of a NixOS configuration for a user named Neeraj.

Variables

{ config, lib, pkgs, ... }:: This line introduces the Nix expression and specifies the variables (config, lib, pkgs, and more) that will be available for use within the script.

Package Channels

The next section of the script deals with package channels. It appears to be commented out with /* ... */, but it suggests adding and updating channels for Nix packages. Channels are sources of Nix packages that you can use to install software. In this case, it's showing how to add the nixos-unstable channel, which contains potentially less stable but more bleeding-edge packages.

Importing Unstable Packages

The script proceeds to import packages from the nixos-unstable channel using the import statement. These packages are intended to be more experimental or less stable than the ones available in the standard NixOS channel.

  unstable = import <nixos-unstable> { config = { allowUnfree = true; }; };
  • unstable: This variable stores the imported packages from the nixos-unstable channel, allowing them to be used later in the script.

  • allowUnfree = true;: This setting allows the use of packages that might contain proprietary or non-free software, which might not be suitable for all users.

Defining Package Lists

The script categorizes packages into different lists based on their purpose. These lists include packages for various desktop environments (Qtile, Gnome, XFCE, KDE), unstable packages, and common stable packages.

  • qtileApps, gnomeApps, xfceApps, and kdeApps: These variables store lists of packages specific to different desktop environments (Qtile, Gnome, XFCE, and KDE).

  • unstableApps: This variable contains packages from the nixos-unstable channel.

  • commonStableApps: This variable contains packages that are stable and common across different desktop environments.

Combining Packages

The appendApps variable combines all the package lists into a single list using the ++ operator.

  appendApps = apps: kdeApps ++ unstableApps ++ commonStableApps ++ apps;

appendApps: This variable represents the combined list of packages that will be installed for Neeraj's user.

User Configuration

Finally, the script defines the user configuration for Neeraj. It specifies user-related settings such as groups, description, and the list of packages to be installed.

in {
  users.users.neeraj = {
    isNormalUser = true;
    description = "Neeraj";
    extraGroups = [ "networkmanager" "wheel" "libvirtd" ];
    packages = with pkgs; appendApps [];
  };
}
  • users.users.neeraj: This part of the script defines a user named Neeraj.

  • isNormalUser = true;: It indicates that Neeraj is a normal user.

  • description = "Neeraj";: This describes the user.

  • extraGroups: These are additional groups that Neeraj belongs to.

  • packages: It specifies the list of packages to be installed for Neeraj, using the appendApps list.

Conclusion

In this blog post, we've taken a deep dive into a Nix script that sets up a NixOS environment for a user named Neeraj. This script makes use of Nixpkgs, package channels, and different package lists to tailor the user's desktop environment and software packages. Nix offers a highly structured and repeatable system configuration, making it a robust tool for handling your system's software and preferences.

As you start setting up your computer and handling your software, bear in mind the importance of careful organization. Whether you're a software developer seeking a tidy workspace or just an ordinary computer user striving to improve your computer's performance, categorizing your apps into logical groups and enhancing their flexibility will undoubtedly lead to a smoother and more efficient computer experience.

This is it for this blog; now it's time for you to embark on your experimentation. Till then, keep reading, keep exploring, and keep learning."