I've been wanting to use nix for a long time, and it seems that the time has finally come.
Nix always has been something familiar but unknown to me for a while. I know Nix lets you manage packages in a declarative fashion and it is reproducible. But in all honesty, these are just buzz words which I really don't understand just yet. So, I'll just look into what actually Nix is.
Apparently, Nix is a package manager that works on a functional programming language, called Nix, which is interesting as I'm interested in functional programming.
I'm obviously not going to opt into using nixOS or nix for my host system just right now. For now, the middleground for me seems to just use nix in my host system to build a nixOS system and run it on QEMU.
PS: QEMU is a great tool to emulate a PC environment.
Something I realized off the boat was that for some reason, nix has majority of their "useful" utilities and tools under an experimental feature, which seems quite odd.
sudo pacman -S nix
I'll have to make sure that locked features like nix-shell and flake are added to: /etc/nix/nix.conf
experimental-features = nix-command flakes
So apparently, nix uses flakes for packaging and reproducing builds, which seems analogous to cargo for rust. The nix package repository stores all the nix packages in the form of nix expressions or sometimes a flake itself.
So, looking into how nix works, it seems to me that I've gone ahead and built a simple hierarchy of how my files are structured and how the flake is going to look, and named the configuration kanto.
nix flake init
├── flake.lock ├── flake.nix ├── hosts │ └── kanto │ ├── default.nix │ ├── hardware.nix │ ├── services.nix │ └── users.nix ├── justfile ├── README.md └── result -> /nix/store/jibberish-hash
hosts/ - Holds configuration for multiple hosts kanto/ - Holds configuration for kanto default.nix - entrypoint hardware.nix - fs and bootloader users.nix - user config services.nix - daemons
Since the most interesting part of nix is about how I can "declare" my OS, I ended up writing:
default.nix
{ config, pkgs, ... }:
{
imports = [ ./hardware.nix ./users.nix ./services.nix ];
networking.hostName = "kanto";
time.timeZone = "Asia/Kathmandu";
system.stateVersion = "24.11";
}
{config, pkgs, ...}: — This essentially introduces my flake's function which takes in a config, pkgs, and a variadic.
{ bunch of declarations } — This is the block; it returns an object with import, networking, time and system fields, which define our system.
This makes our configuration able to run on QEMU. Let's try to build it:
nix run nixpkgs#nixos-rebuild -- build-vm --flake .#kanto
'#' here is the fragment specifier — I liked to think of it as a package access specifier, like accessing the package from a flake.
It works, but I might've cheated, as I've already written some declarations in some of the files.
hardware.nix
{ config, ... }:
{
boot.loader = {
grub = {
enable = true;
efiSupport = true;
device = "nodev";
};
efi.canTouchEfiVariables = true;
};
fileSystems."/" = {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
options = [ "noatime" ];
};
fileSystems."/boot" = {
device = "/dev/disk/by-label/BOOT";
fsType = "vfat";
};
swapDevices = [
{ device = "/dev/disk/by-label/swap"; }
];
}
This defines my hardware, currently assumed that my system has simple dual partitions:
"/" - primary partition "/boot" - boot partition
Nix actually stores each of its rebuilds as generations and allows the user to boot into any of its builds, hence making it extremely tolerant to any faulty builds.
users.nix
{config, pkgs, ...}:
{
users.users.irhs = {
isNormalUser = true;
description = "Admin";
extraGroups = [ "networkmanager" "wheel" ];
openssh.authorizedKeys.keys= [
"your-ssh-pub-key"
];
};
programs.zsh.enable = true;
}
Here, we're declaring a simple user called "irhs". This structure has a high resemblance to recursive structures defined in LISP:
'((
users . ((
users . ((
irhs . ((
; above configuration
))
))
))
))
This essentially shows how the origin of nix was highly inspired from LISP and how it has vastly simplified it. In my mind, I've mostly been reiterating how nix seemed really familiar with my time configuring emacs.
Now for the final part, I have to ensure that I am able to remotely access the machine, so I have to set up sshd,
for which I'll have to populate services.nix.
services.nix
{ config, pkgs, ... }:
{
services = {
openssh = {
enable = true;
settings.PermitRootLogin = "no";
settings.PasswordAuthentication = true;
};
};
}
This is relatively straightforward, and now if I build the OS:
building the system configuration... Done. The virtual machine can be started by running /nix/store/2b59ryh0v0fan4wr8q0b04b2zniz4srp-nixos-vm/bin/run-kanto-vm
run-kanto-vm
Actually, all the packages that I intend on adding to my system in the future
are stored in a central repository /nix/store in my system.
And it successfully boots into NixOS. We'll have to try to ssh into it.
And well it works as well.
Nix isn't just a package manager; it's an abstraction layer over the messiness of Linux.
By treating my OS like my emacs configuration — with simpler syntax and a large amount of packages — I can avoid imperative deficiencies like failing commands and instead enjoy the functional reliability of declaring my operating system.
For anybody looking into this article, this is not something you should follow — I've only written this as a note to myself and to understand about Nix a little bit more.