- Disclaimer
- Use Cases (to grab your attention)
- Nix Language
- Derivations
- The nix store
- Flakes (short intro)
- Killer Use Cases
- This demo is "from first principles", it might sound slow
- I'm not an expert
- If anything is not clear, please interrupt me
- I'm not sure this approach/TOC works
- This is from a fork: https://github.com/aciceri/nixos-devops-talk
- Build this presentation:
nix build .\#mkPresentation
- Contribute on projects without polluting system with project-specific tooling: awesome-italia-opensource
- Nixos configuration
- declarative: There is no notion of executing sequential steps. Dependencies between operations are established only through data.
- pure: Values cannot change during computation. Functions always produce the same output if their input does not change.
- functional: Functions are like any other value. Functions can be assigned to names, taken as arguments, or returned by functions.
- lazy: Values are only computed when they are needed.
- dynamically typed: Type errors are only detected when expressions are evaluated.
Before we dive into derivations, let's cover some key language features that are essential for understanding Nix expressions.
Attribute sets are like dictionaries or key-value pairs. They're defined using curly braces:
{
name = "example";
value = 42;
}
let
bindings allow you to create local variables:
let
x = 1;
y = 2;
in
x + y # Returns 3
with
brings an attribute set's attributes into scope:
let set = { a = 1; b = 2; };
in with set; a + b # Returns 3
Functions are called by putting the argument after the function, separated by a space:
isNull null # Returns true
Functions are defined using a colon. The syntax is argument: body
x: x + 1 # A function that takes x and returns x + 1
Functions can also take attribute sets as arguments
{ a, b }: a + b # A function that takes an attrset with 'a' and 'b' keys
In Nix, function arguments can have default values. If an argument is not provided during the function call, the default value is used.
{ x ? 10 }: x + 5 # A function with a default value for x
Calling this function with no arguments will return 15
because x
defaults to 10
.
(x: x + 1) 5 # Returns 6
For functions taking attribute sets:
({ a, b }: a + b) { a = 1; b = 2; } # Returns 3
The import
function is used to load Nix expressions from files or other
sources. It can be used to import files directly or to pull in the
Nix Packages collection (nixpkgs).
let
myModule = import ./path/to/module.nix;
in
myModule.someAttribute
The expression <nixpkgs>
is a shorthand to refer to the Nix Packages
collection (it's a path, depends on how you installed nix)
let
pkgs = import <nixpkgs> {};
in
pkgs.hello # Refers to the 'hello' package from nixpkgs
If nixpkgs
"points to master", it's literally this
{ pkgs ? import <nixpkgs> {} }:
pkgs.hello
Useful to override the pkgs (e.g. pin to a specific version, different from the default one)
Ok, boring... now what?
We use the nix language mostly to produce derivations
The core building block that describes how to build a software component. It's a low-level, immutable representation of a build process, which tells Nix exactly what to do to produce a specific output.
TL;DR: a derivation is a build plan.
# simple.nix
{ pkgs ? import <nixpkgs> { } }:
pkgs.stdenv.mkDerivation {
name = "simple";
src = ./.;
installPhase = ''
mkdir $out
echo "42" > $out/output
'';
}
A derivation can be instantiated, it became a store derivation
To build this derivation, you would use the command:
nix-instantiate simple.nix
...
this derivation will be built:
/nix/store/{SOME-HASH}-simple.drv
...
We can navigate inspect a derivation:
nix derivation show /nix/store/{SOME-HASH}-simple.drv
A .drv
file contains:
- Build Instructions: How to fetch, unpack, build, and install the package.
- Dependencies: The dependencies required for the build, including other packages and build tools.
- Source Information: Where to find the source code or files needed for the build.
- Phases: Various build phases like
unpackPhase
,patchPhase
,buildPhase
,installPhase
, etc; - Output Paths: Paths where the build outputs are placed in the Nix store.
- ... other "less interesting" things.
/nix/store/<hash>-<name>
- The
hash
in the path is generated from the various inputs that make up the derivation. If all inputs remain unchanged, the resulting derivation will always produce the same hash and store path.- This approach is highly powerful, as it enables the use of caches and substitutes, allowing pre-built derivations to be shared and reused, significantly improving build efficiency.
nix-store --realise /nix/store/{SOME-HASH}-simple.drv
nix-build
does:
- Instantiation: The .nix file is instantiated into a store derivation (.drv).
- Realization: The .drv is then realized into the actual build output, based on its defined outputs (e.g., outputs.out.path).
- A symbolic link
./resultis created, pointing to the path of the realized output.
- nix-build vs nix build:
nix-build
is the older command used to build Nix expressions directly from files.nix build
is a newer command that is more compatible with flakes and newer Nix features. It's recommended to usenix build
if you're working with flakes or modern Nix expressions, as it integrates better with the new ecosystem.
Here's an example of a simple derivation that compiles a C program:
# hello.nix
{ pkgs ? import <nixpkgs> { } }:
pkgs.stdenv.mkDerivation {
name = "hello";
src = ./src;
buildPhase = ''
rustc $src/rust.rs -o ./hello
'';
installPhase = ''
mkdir -p $out/bin
cp ./hello $out/bin
'';
}
Something is missing here...
$src
: This variable refers to the source code directory specified by thesrc
attribute. In this example, it's the./src
directory.$out
: This variable represents the output directory in the Nix store where the build results will be placed. Nix automatically sets this variable to a unique path in the/nix/store
.
If the derivation depends on other derivations, these are built first.
- Nixpkgs is a vast repository that contains derivations for most of the software packages you might need.
- It's the primary package collection used by Nix and NixOS, offering a wide variety of software from simple utilities to complex applications.
For example, the sl package in nixpkgs can be examined and built with the following commands:
nix derivation show nixpkgs#sl
nix build nixpkgs#sl -L --rebuild
Its primary purpose is to store both the derivation files (.drv
) and their output artifacts.
- Location: The Nix store is located at
/nix/store
. - Immutability: Once a derivation is built and stored in the Nix store, it never changes.
- Accessibility: The store is readable by all users, allowing for shared access.
Before Nix realise
a derivation, it checks the cache to see if the output already exists.
If it does, Nix can download the output directly from the cache instead of rebuilding it.
What if the entire operating system was the output of a derivation?
NixOS is a basically a big configuration file (.nix
) that evaluates to a a big derivation that
build a Linux disto (plus some symbolic links that enables easy rollbacks).
- Everyone writes nix expressions as they want
- nixpkgs (inputs in general) are not pinned
- introduce more structured and reliable way to manage Nix projects
- are an experimental feature
- are expected to become the standard in future versions of Nix
--experimental-features 'nix-command flakes'
{ self, ... }@inputs:
{
# Executed by `nix flake check`
checks."<system>"."<name>" = derivation;
# Executed by `nix build .#<name>`
packages."<system>"."<name>" = derivation;
# Executed by `nix build .`
packages."<system>".default = derivation;
# Executed by `nix run .#<name>`
apps."<system>"."<name>" = {
type = "app";
program = "<store-path>";
};
# Executed by `nix run . -- <args?>`
apps."<system>".default = { type = "app"; program = "..."; };
# Formatter (alejandra, nixfmt or nixpkgs-fmt)
formatter."<system>" = derivation;
# Used by `nix develop .#<name>`
devShells."<system>"."<name>" = derivation;
# Used by `nix develop`
devShells."<system>".default = derivation;
}
- Easier Onboarding: New developers quickly get up and running with a nix-shell that configures all tools and dependencies automatically, eliminating setup hassles.
- Consistent Environment: Ensures all team members use the same development environment, preventing discrepancies and conflicts.
- Consistent Builds: Guarantees that builds are identical across different machines, eliminating “it works on my machine” issues.
- Stable CI Pipelines: CI systems benefit from reproducible builds as they ensure that builds are consistent and reliable, leading to more accurate testing and integration results.
- Efficient Caching: Reproducible builds enable the use of caching mechanisms to speed up development.
Some ideas of what we could do:
- let's install nix on your machine (nix is supported for macOS and WSL2 too)
- let's nixify your project (build your code, setup a developer environment, ...)
- let's convince your company that nix will make it save money (onboarding, reproducibility, ...)
- try conving me nix is not the solution for everything