Introduction to Nix

Ruby meetup

Ljubljana, June 11th 2024

by Simon Žlender

About me

  • Simon Žlender, GitHub: @szlend

  • Engineering Manager at "a fintech company"

  • I lead the Development Experience team
  • My toolset: Rust / Elixir / ex-Ruby
  • Jackhammer of all tools: Nix

👋

It works fine on my machine™

"Probably something weird on your machine"

"Just read my 3 page long outdated setup guide"
"Try reinstalling Ruby"
"Did you symlink OpenSSL v1.0.2h to /usr/lib?"

"Bro just build with --with-xml2-dir=/please/help"

Your dev environment is not

We 🤍 Gemfile.lock

So why don't we have lock files for system dependencies?

"Just use Docker"

"Why is the Rails server on my Mac now 50% slower?"

 

"Wait why are these files now owned by root?"

 

"I need Arm images for MacOS now? Multi-arch what?"

 

"Debuggers are for people who write shit code."

"OK maybe debuggers are useful sometimes. I'll just run my code editor in Docker as well."

"Why are these walls soft-padded?"

Sane people use Asdf

  • Toolchain version management
  • Really good for ruby, node, etc.
  • Lacks the really tricky bits:

It can't manage system libraries

"We have system lock files at home"

It's called Nix

So Nix is a package manager?

  • Nix manages packages yes
  • Cross-platform (Linux/MacOS)
  • Nixpkgs - Official Nix package repository
  • Channels - nixpkgs-unstable / nixos-24.11
  • Over 90.000 packages
  • Community maintained
  • Largest package repository out there*

* as tracked by repology.org

Different package versions can co-exist

/nix/store/23fsf8idl4s1awpxra2r51wfqwsc0d6d-rustc-1.67.1
/nix/store/abhjlgf3rmnckl54nfj24gh82ygzk9b4-rustc-1.70.0
/nix/store/japwpy9z6fndzakc17r1sw1pkbsk0961-rustc-1.71.1

Packages are symlinked into profiles

~/.nix-profile (included in PATH)
├── bin
│   ├── aws   -> /nix/store/fqii9...-aws-cli2/bin/aws
│   ├── cargo -> /nix/store/w7hr1...-rustup-1.75.0/bin/cargo
...

Package manager, but also...

  • Nix - the sandboxed build tool
  • Nix - the programmable configuration language
  • NixOS - the Nix-based Linux operating system

NO

OK, slow down. So I want to install a package. I should just

`nix-whatever-install ruby` right?

DO NOT USE THE NIX CLI TO MANAGE PACKAGES

Please for your sanity's sake...

But why...

  • Nobody experienced with Nix actually uses the CLI to manage packages
  • As a result, nobody cares about making the UX better
  • Just don't. It's not a thing. Forget it exists.

nix profile install

nix-env -iA

OK so what should I use then?

What is a dev shell?

  • It's just a shell with a bunch of injected environmental variables
  • Most notably:
    • PATH - where to look for applications
    • CC - the path to the C compiler
    • CFLAGS - C compiler flags
    • LDFLAGS - system linker flags
    • etc...
  • Nix can build a dev shell that suits your exact needs

Let's write a Rails dev shell

pkgs.mkShell {
  packages = [
    pkgs.ruby_3_3
    pkgs.rubyPackages_3_3.rails
  ];
}
$ nix develop

$ echo $PATH
/nix/store/pkp3jxxrv5k2q30d7dlrhi3iavf7yx30-ruby-3.3.1/bin # etc...

$ which ruby
/nix/store/pkp3jxxrv5k2q30d7dlrhi3iavf7yx30-ruby-3.3.1/bin/ruby

$ ruby --version
ruby 3.3.1 (2024-04-23 revision c56cd86388) [arm64-darwin23]
$ rails --version
Rails 7.1.3.2

$ rails new -n my-app .

Bundle complete! 14 Gemfile dependencies, 82 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

Let's enter the shell

We can now generate a Rails project

$ bundle exec rails s

=> Booting Puma
=> Rails 7.1.3.2 application starting in development
...
* Listening on http://127.0.0.1:3000
$ bundle add rmagick

Installing rmagick 6.0.1 with native extensions
Gem::Ext::BuildError: ERROR: Failed to build gem native extension.

*** extconf.rb failed ***
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers.  Check the mkmf.log file for more details.  You may
need configuration options.

...

pkg-config.rb:504:in `parse_pc': .pc doesn't exist: <MagickCore> (PackageConfig::NotFoundError)

Ok now let's add everyone's favorite image manipulation tool!

Oh boy, it's one of those days again.

Let me open that bottle of rakija and hope Google ChatGPT is up to date on this one.

On second thought, forget AI. Let's use some actual brain cells for this one.

pkg-config.rb:504:in `parse_pc': .pc doesn't exist: <MagickCore> (PackageConfig::NotFoundError)

Here's our first clue. Something called pkg-config is complaining about a missing Package Config for MagickCore.

Install pkg-config

Rule #1 when dealing with build errors

I CANT STRESS THIS ENOUGH. PKG-CONFIG WILL SOLVE 90% OF ALL YOUR LIFE PROBLEMS.

Rule #2: RTFM (Read the friendly manual)

pkgs.mkShell {
  packages = [
    pkgs.ruby_3_3
    pkgs.rubyPackages_3_3.rails
    pkgs.pkg-config
    pkgs.imagemagick
  ];
};

Shocking, I know. The rmagick README says to install imagemagick libs. Let's put them in the shell.

$ $ pkg-config --list-all
...
ImageMagick    convert, edit, and compose images (ABI Q16HDRI)


$ bundle add rmagick
Installing rmagick 6.0.1 with native extensions

Let's try this again:

Congratulations!

You have not only configured rmagick for yourself. But also for your colleagues and future self.

And you didn't have to write a 3 page setup manual that nobody will read.

Full devshell for fellow boilerplate enjoyers

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { nixpkgs, flake-utils, ... }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { system = system; };
      in
      {
        devShells.default = pkgs.mkShell {
          packages = [
            pkgs.ruby_3_3
            pkgs.rubyPackages_3_3.rails
            pkgs.pkg-config
            pkgs.imagemagick
          ];
        };
      });
}

Inputs are locked in flake.lock

{
  "nodes": {
    "nixpkgs": {
      "locked": {
        "lastModified": 1717786204,
        "narHash": "sha256-4q0s6m0GUcN7q+Y2DqD27iLvbcd1G50T2lv08kKxkSI=",
        "owner": "nixos",
        "repo": "nixpkgs",
        "rev": "051f920625ab5aabe37c920346e3e69d7d34400e",
        "type": "github"
      },
  ...
}
$ nix flake update
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/b1d9ab70662946ef0850d488da1c9019f3a9752a' (2024-06-02)
  → 'github:nixos/nixpkgs/051f920625ab5aabe37c920346e3e69d7d34400e' (2024-06-07)

What about CI?

OK that's cool and all

Load the dev shell in (GitLab) CI

my-job:
  image: docker.nix-community.org/nixpkgs/nix-flakes:nixos-24.05
  before_script:
    - source <(nix print-dev-env)
  script:
    - ruby --version

Congratulations

Now the CI environment is identical to your local machine

And I didn't even need to build a container?

What about production?

YES

Can I deploy Nix packages to productinon?

Maybe?

Can I SHOULD I deploy Nix packages to productinon?

How can I deploy to production?

  • Build Nix-based containers
  • Install to Nix-enabled Linux server
  • Provision to NixOS

Not enough to get into this today...

Prerequisite: My service needs to be built in the Nix sandbox.

Caveats

  • Not every language is great to package in Nix
  • Ruby is one of the not-so-great ones
  • Well supported languages:
    • Through sheer will power: C/C++, Node/npm
    • Because of actual good build systems: Rust, Go
  • Python and Haskell have fairly big Nix communities but I don't have any personal experience with them

What about global system packages?

I don't spend all my day in dev shells.

home-manager

  • Declaratively manage your HOME environment
  • This includes the packages installed on your system
  • But also your dotfiles

Provision packages

{ pkgs, ... }: # Your inputs

# Your home configuration
{
  home.packages = [
    pkgs.jq
    pkgs.ruby
    pkgs.vim
  ];
}
$ home-manager switch

$ which ruby
~/.nix-profile/bin/ruby
$ vim ~/.config/home-manager/home.nix
{
  programs.git = {
    enable = true;
    userName = "Simon Žlender";
    userEmail = "pub.git@zlender.si";
  
    extraConfig = {
      pull.ff = "only";
      push.autoSetupRemote = "true";
    };
  };
}
$ ls -la .config/git/config
.config/git/config -> /nix/store/79yz3g3s37qw3vayhl55kkjm2xhf9hjk-home-manager-files/.config/git/config

$ cat .config/git/config
[pull]
	ff = "only"

[push]
	autoSetupRemote = "true"

[user]
	email = "pub.git@zlender.si"
	name = "Simon Žlender"

Provision dotfiles

{
  programs.fish = {
    enable = true;
    shellAbbrs = {
      sc = "systemctl";
      jc = "journalctl";
    };
  };
  
  programs.starship = {
    enable = true;
  };
  
  programs.direnv = {
    enable = true;
    nix-direnv.enable = true;
  };
}

Provision your shell

{ pkgs, ... }:

{
  home.packages = [ pkgs.gradle ];

  home.file = {
    ".gradle/gradle.properties".text = ''
      org.gradle.console=verbose
      org.gradle.daemon.idletimeout=3600000
    '';
  };
}

What if my app isn't supported?

https://nix-community.github.io/home-manager/options.xhtml

Endless options...

But your entire OS

Like home-manager...

{
  services.plex = {
    enable = true;
  };

  services.nginx.virtualHosts."plex.myhost.com" = {
    forceSSL = true;
    enableACME = true;

    locations."/" = {
      proxyPass = "http://localhost:32400";
      proxyWebsockets = true;
    };
  };
  
  security.acme = {
    acceptTerms = true;
    defaults = {
      email = "admin@myhost.com";
    };
  };

  networking.firewall.allowedTCPPorts = [ 443 ];
}

Provision services

Plex, nginx, letsencrypt and firewall in few lines of nix

{ pkgs, ... }:

{
  systemd.services.my-service = {
    wantedBy = [ "multi-user.target" ];

    serviceConfig = {
      ExecStart = pkgs.my-service;
    };
  };
}

Define custom services

Oh no, I turned my system into a brick 🧱🗿

No worries, NixOS got you covered

But for MacOS

Like NixOS...

🍎

👨

🎩

{ pkgs, ...}:

{
  system.defaults.NSGlobalDomain.InitialKeyRepeat = 10;
  system.defaults.NSGlobalDomain.KeyRepeat = 1;
  
  system.defaults.dock.autohide = true;
  system.defaults.dock.orientation = "left";
  
  system.defaults.finder.AppleShowAllExtensions = true;
  
  environment.systemPackages = [
    pkgs.vim
  ];
}

Provision global system configuration

$ darwin-rebuild switch

Recommendations

  • ✅ Nix dev shells
  • ✅ Nix in CI
  • ❔ Nix in production
  • ✅ Home-manager on your laptop
  • ❌ Home-manager for GUI applications
  • ❔ NixOS on your laptop
  • ✅ NixOS on your home server
  • ✅ Nix-darwin

Where to start?

  • Use the DeterminateSystems Nix Installer
  • Prefer Nix Flakes over nix-channel
  • Setup a dev shell for a personal project
  • Use nix-direnv instead of manually entering the shell
  • Give home-manager a shot
  • Go door-to-door and spread the holy word

BONUS ROUND

If we have time...

🏎️💨

Why does my shell script work on Linux, but not on the Mac?

$ bash --version

GNU bash, version 3.2.57(1)-release (arm64-apple-darwin23)

THANKS APPLE

$ nix develop
$ bash --version

GNU bash, version 5.2.26(1)-release (aarch64-apple-darwin23.4.0)
$ sed -i 's/foo/bar/g' my-file

sed: 1: "my-file": invalid command code m

$ sed --version

/usr/bin/sed: illegal option -- -
usage: sed script [-Ealnru] [-i extension] [file ...]
	sed [-Ealnu] [-i extension] [-e script] ... [-f script_file] ... [file ...]

Differences in core system utils: sed

$ nix develop
$ sed -i 's/foo/bar/g' my-file

$ sed --version

sed (GNU sed) 4.9
$ echo 1.2.3 | grep -E "\d\.\d\.\d"
1.2.3

$ grep --version

grep (BSD grep, GNU compatible) 2.6.0-FreeBSD
$ nix develop
$ echo 1.2.3 | grep -E "\d\.\d\.\d"
grep: warning: stray \ before d
grep: warning: stray \ before d
grep: warning: stray \ before d

$ grep --version

grep (GNU grep) 3.11

Differences in core system utils: grep

Lesson learned:

Cross-platforms shell scripting is INSANITY

#!/usr/bin/env nix-shell
#!nix-shell -i bash
#!nix-shell -p coreutils gnused gnugrep

bash --version
coreutils --version
sed --version
grep --version
$ ./bash.sh
GNU bash, version 5.2.26(1)-release (aarch64-apple-darwin23.4.0)
...
coreutils (GNU coreutils) 9.5
...
sed (GNU sed) 4.9
...
grep (GNU grep) 3.11

Nix-shell shebang

💥

The lazy man's dependency management

#!/usr/bin/env nix-shell
#!nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/refs/heads/nixos-24.05.tar.gz
#!nix-shell -i ruby
#!nix-shell -p "ruby.withPackages (p: [ p.nokogiri ])"

require 'nokogiri'
require 'open-uri'

doc = Nokogiri::HTML(URI.open('https://www.rug.si/'))
doc.css('.post-wrap h1 a').each do |link|
  puts link.content
end

Nix-shell shebang

Ruby edition

💎

THANK YOU

🙏

🗿

Introduction to Nix

By szlend

Introduction to Nix

  • 40