Nix From First Principles: Flake Edition

When I was learning Nix a few months back, I found the community often suggesting flakes as an improvements to some of the rough edges that were encountered, yet the flakes documentation is fragemented and largely targeted at audiences that already know basic Nix, so is detailed as differences from the current model.

So I wrote a guide that starts from the basics with the new CLI and builds up to flakes, you can see it here:

There’s 8 parts so far and it now covers enough I feel comfortable sharing it, as it now covers the basics of Nix and flakes, but I do plan to add more in the future to cover things like NixOS, overlays, home-manager etc. (Also maybe NixOps, but I need to learn NixOps myself first).

42 Likes

Nice job. Thanks for sharing.

One problem to take into account going forward:

Upon including (in the same guide) NixOS & al. you branch off the general Nix use case.

This makes the documentation unsuitable for a subset of the current audience.

An alternative would be to make separate guides on NixOS, home-manager from first principles, and thereby marking the different target use cases.

NixOps is a bit of a special case and there isn’t really a consensus about it’s design choices and target use cases as evidenced by the emergence of: morph, krops, colmena, deploy-rs, nixus, terraform-nixos, etc.

I’d personally say about nixops: maybe it’s eating up the world a little too much. But on the bright side, I feel the first principle of interoperability with other ecosystems is raising in the Nix ecosystem. :slightly_smiling_face:

and the ability to have a config file declare the entire state of a system without storing huge docker container artifacts for every permutation.

I read this as a reference to NixOS, and as such it might be actually a hidden layer violation.

What caracterizes Nix is the ability to program huge configuration data trees just like any other config management language (cue, dhall , jsonnet, nickel, etc).

But it also has a built-in API into the filesystem via a derivation (it somewhat lacks a built-in method to mutate remote state though, after all local state is just a special case).

Combinig these two capabilities and enriching them with plain bash for the init script called by the bootmanager can give you an operating system with interesting properties. Swapping the init script with a file that is relevant to a shell’s init sequence (e.g. .bashrc) gives you home-manager, and so forth.

Unfortunately, as stated, you can’t mutate a remote state (i.e. API) from the same built-ins and in my opinion this is actually one of the biggest shortcomings: it completely blurs the vision for the fact that local state is only a special case.

Brilliant. Tony, when I try to rebuild my config file (etc/nixos/configuration.nix) with the instructions from the Nixos wiki on flakes:

{ pkgs, ... }: {
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
}

(Although my config looks like:)

{ config, pkgs, ... }: {
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
}

I get an error:

"attempt to call something which is not a function but a set at /etc/nixos/configuration.nix:5:24:

Is that your whole NixOS configuration.nix? If not, can you provide the context of where you put that?

3 Likes
{ config, pkgs, ... }: {
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
} 

{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
    ];

  # Bootloader.
  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;
  boot.loader.efi.efiSysMountPoint = "/boot/efi";

  networking.hostName = "nixos"; # Define your hostname.
  # networking.wireless.enable = true;  # Enables wireless support via wpa_supplicant.

  # Configure network proxy if necessary
  # networking.proxy.default = "http://user:password@proxy:port/";
  # networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";

  # Enable networking
  networking.networkmanager.enable = true;

  # Set your time zone.
  time.timeZone = "Europe/Paris";

  # Select internationalisation properties.
  i18n.defaultLocale = "en_GB.utf8";

  i18n.extraLocaleSettings = {
    LC_ADDRESS = "fr_FR.utf8";
    LC_IDENTIFICATION = "fr_FR.utf8";
    LC_MEASUREMENT = "fr_FR.utf8";
    LC_MONETARY = "fr_FR.utf8";
    LC_NAME = "fr_FR.utf8";
    LC_NUMERIC = "fr_FR.utf8";
    LC_PAPER = "fr_FR.utf8";
    LC_TELEPHONE = "fr_FR.utf8";
    LC_TIME = "fr_FR.utf8";
  };

  # Enable the X11 windowing system.
  services.xserver.enable = true;

  # Enable the GNOME Desktop Environment.
  services.xserver.displayManager.gdm.enable = true;
  services.xserver.desktopManager.gnome.enable = true;

  # Configure keymap in X11
  services.xserver = {
    layout = "fr";
    xkbVariant = "azerty";
  };

  # Configure console keymap
  console.keyMap = "fr";

  # Enable CUPS to print documents.
  services.printing.enable = true;

  # Enable sound with pipewire.
  sound.enable = true;
  hardware.pulseaudio.enable = false;
  security.rtkit.enable = true;
  services.pipewire = {
    enable = true;
    alsa.enable = true;
    alsa.support32Bit = true;
    pulse.enable = true;
    # If you want to use JACK applications, uncomment this
    #jack.enable = true;

    # use the example session manager (no others are packaged yet so this is enabled by default,
    # no need to redefine it in your config for now)
    #media-session.enable = true;
  };

  # Enable touchpad support (enabled default in most desktopManager).
  # services.xserver.libinput.enable = true;

  # Define a user account. Don't forget to set a password with ‘passwd’.
  users.users.marcus = {
    isNormalUser = true;
    description = "Marcus";
    extraGroups = [ "networkmanager" "wheel" ];
    packages = with pkgs; [
      firefox
	vim
        neovim
        gimp
        python3Full
        hugo
        atom
        gparted
        libreoffice
        tilix
    #  thunderbird
    ];
  };

  # Allow unfree packages
  nixpkgs.config.allowUnfree = true;

  # List packages installed in system profile. To search, run:
  # $ nix search wget
  environment.systemPackages = with pkgs; [
  #  vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
  #  wget
  ];

  # Some programs need SUID wrappers, can be configured further or are
  # started in user sessions.
  # programs.mtr.enable = true;
  # programs.gnupg.agent = {
  #   enable = true;
  #   enableSSHSupport = true;
  # };

  # List services that you want to enable:

  # Enable the OpenSSH daemon.
  # services.openssh.enable = true;

  # Open ports in the firewall.
  # networking.firewall.allowedTCPPorts = [ ... ];
  # networking.firewall.allowedUDPPorts = [ ... ];
  # Or disable the firewall altogether.
  # networking.firewall.enable = false;

  # This value determines the NixOS release from which the default
  # settings for stateful data, like file locations and database versions
  # on your system were taken. It‘s perfectly fine and recommended to leave
  # this value at the release version of the first install of this system.
  # Before changing this value read the documentation for this option
  # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
  system.stateVersion = "22.05"; # Did you read the comment?

}

Hi Ryan,
Thank you for taking the trouble to reply.
There is a more in-depth error message, if this info is insufficient

Those things need to be in the same attribute set (bunch of variables set inside one {}). Right now nix interprets this as you calling the function:

{
  nix.settings.experimental-features = [ "nix-command" "flakes" ];
} 

With an argument of your entire configuration. Only problem is, that isn’t a function, but an attribute set, and therefore can’t be called, so it gives you that error.

In short, just do this:

{ config, pkgs, ... }: {
  nix.settings.experimental-features = [ "nix-command" "flakes" ];

  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
    ];

The nix language basics tutorial might help avoid things like this, maybe it would be good to link in the blog series?

3 Likes

Thank you TLATER. That was a great help.
Unfortunately I need to ‘brute force’ an essential package onto Nixos, as I rather like Nixos and want to stay with it, even if I am a rank amateur with zero skills.

Many thanks for taking the time to help out

1 Like

Most of us have, at some point! The Nixpkgs docs are a great place to start, as is the Nixpkgs codebase a source of examples. If you feel stuck, you can get packaging help here, on Matrix, on Reddit, or on GitHub (by asking for feedback on a partially working package that you open a PR for).

Did you get your packaging thing taken care of? Would you like to open a thread for the package you need to add?

2 Likes

Yes, I think so. This really isn’t child’s play. Do you think I should read “Learn You a Haskell for Great Good” to get a good grounding in fun_lingos? (Sorry I have to gee myself up).

2 Likes

I found “Haskell Programming from first principles” a better start :wink:

1 Like

Created an account just to say thank you, I’ve been banging my head against a wall for quite a while and I found your series illuminating. I look forward to your next posts!

2 Likes

I also created an account to say thanks. Your guide was exactly what I needed. This has been a tremendous help.

1 Like