2024-07-08 13:00:16 -05:00
This post is a Flake-based rewrite of [Learn Nix the Fun Way on fzakaria.com][0].
I really enjoyed the content of the post and wanted to write it as a Nix
user who is just using and prefers flakes. It does add a few extra steps and
complexity, but I think it's still valuable and perhaps reveals a bit more about
Nix and why it's pretty fantastic.
2024-07-08 09:35:53 -05:00
<!-- more -->
## what-is-my-ip
Let's walk through a single example of a shell script one may write: _what-is-my-ip_
```bash
2024-07-08 12:18:24 -05:00
#!/usr/bin/env bash
2024-07-08 13:00:16 -05:00
curl -s http://httpbin.org/get | \
jq --raw-output .origin
2024-07-08 09:35:53 -05:00
```
Sure, it's _sort of portable_ , if you tell the person running it to have _curl_ and _jq_ . What if you relied on a specific version of either though?
Nix **guarantees** portability.
2024-07-08 13:00:16 -05:00
We might leverage _[Nixpkgs' trivial builders](https://ryantm.github.io/nixpkgs/builders/trivial-builders/)_ (specifically, `writeShellScriptBin` ) in a basic Nix flake to turn this into a Nix derivation (i.e. build recipe):
2024-07-08 09:35:53 -05:00
```nix
# flake.nix
{
inputs.nixpkgs.url = "nixpkgs/nixos-24.05";
outputs = {nixpkgs, ...}: let
systems = ["aarch64-linux" "aarch64-darwin" "x86_64-darwin" "x86_64-linux"];
pkgsFor = func: (nixpkgs.lib.genAttrs systems (system: (func (import nixpkgs {inherit system;}))));
in {
packages = pkgsFor (pkgs: {
2024-07-08 12:18:24 -05:00
default = pkgs.callPackage ./what-is-my-ip.nix {};
2024-07-08 09:35:53 -05:00
});
};
}
```
```nix
# what-is-my-ip.nix
{pkgs}:
pkgs.writeShellScriptBin "what-is-my-ip" ''
${pkgs.curl}/bin/curl -s http://httpbin.org/get | \
${pkgs.jq}/bin/jq --raw-output .origin
''
```
> 😬 Avoid over-focusing on the fact I just introduced a new language and a good chunk of boilerplate. Just come along for the ride.
Here we are pinning our package to dependencies which come from NixOS/Nixpkgs release branch 24.05.
2024-07-08 13:00:16 -05:00
We can build our package and find out the Nix store path (which contains the
hash) like so:
2024-07-08 09:35:53 -05:00
2024-07-08 13:00:16 -05:00
```console
$ nix build --print-out-paths
/nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip
```
2024-07-08 09:35:53 -05:00
2024-07-08 12:18:24 -05:00
And, of course, we can run our built result:
2024-07-08 09:35:53 -05:00
2024-07-08 12:18:24 -05:00
```console
2024-07-08 09:35:53 -05:00
$ ./result/bin/what-is-my-ip
24.5.113.148
```
2024-07-08 12:18:24 -05:00
Or run it from the Flake directly:
2024-07-08 09:35:53 -05:00
2024-07-08 12:18:24 -05:00
```console
$ nix run
2024-07-08 09:35:53 -05:00
24.5.113.148
```
2024-07-08 13:00:16 -05:00
Now that this is in our Nix store, we've naturally modeled our dependencies
and can do _fun_ things like generate graph diagrams (click the image to view
larger):
2024-07-08 09:35:53 -05:00
2024-07-08 12:43:08 -05:00
```console
2024-07-08 09:35:53 -05:00
$ nix-store --query --graph $(readlink result) | nix shell nixpkgs#graphviz -c dot -Tpng -o what-is-my-ip-deps.png
```
2024-07-08 12:41:46 -05:00
[![Image of what-is-my-ip dependencies as a graph ](./what-is-my-ip-deps.png )](./what-is-my-ip-deps.png)
2024-07-08 09:35:53 -05:00
2024-07-08 13:00:16 -05:00
Let's add a _developer environment_ which contains our new tool.
2024-07-08 12:18:24 -05:00
This is a great way to create developer environments with reproducible tools.
```diff
diff --git a/flake.nix b/flake.nix
index 2a99357..ab32421 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,11 +1,24 @@
{
inputs.nixpkgs.url = "nixpkgs/nixos-24.05";
- outputs = {nixpkgs, ...}: let
+ outputs = {
2024-07-08 13:00:16 -05:00
+ self, # we will need to reference our own outputs to pull in the package we've declared
2024-07-08 12:18:24 -05:00
+ nixpkgs,
+ ...
+ }: let
systems = ["aarch64-linux" "aarch64-darwin" "x86_64-darwin" "x86_64-linux"];
pkgsFor = func: (nixpkgs.lib.genAttrs systems (system: (func (import nixpkgs {inherit system;}))));
in {
packages = pkgsFor (pkgs: {
2024-07-08 13:00:16 -05:00
default = pkgs.callPackage ./what-is-my-ip.nix {};
2024-07-08 12:18:24 -05:00
});
+
+ devShells = pkgsFor (pkgs: {
+ default = pkgs.mkShell {
2024-07-08 13:00:16 -05:00
+ packages = [self.outputs.packages.${pkgs.system}.default];
2024-07-08 12:18:24 -05:00
+ shellHook = ''
+ echo "Hello, Nix!"
+ '';
+ };
+ });
};
}
2024-07-08 09:35:53 -05:00
```
2024-07-08 12:18:24 -05:00
```console
$ nix develop -c $SHELL
2024-07-08 09:35:53 -05:00
Hello, Nix!
2024-07-08 12:18:24 -05:00
$ which what-is-my-ip
2024-07-08 09:35:53 -05:00
/nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip/bin/what-is-my-ip
```
🕵️ Notice that the hash **lr6wlz2652r35rwzc79samg77l6iqmii** is _exactly_ the same which we built earlier.
2024-07-08 13:00:16 -05:00
We can now do binary or source deployments 🚀🛠️📦 since we know the full dependency closure of our tool. We simply copy the necessary `/nix/store` paths to another machine with Nix installed.
2024-07-08 09:35:53 -05:00
2024-07-08 12:18:24 -05:00
```console
$ nix copy --to ssh://beefcake $(nix build --print-out-paths)
2024-07-08 12:38:39 -05:00
$ ssh beefcake /nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip/bin/what-is-my-ip
2024-07-08 09:35:53 -05:00
98.147.178.19
```
2024-07-08 13:00:16 -05:00
Maybe though you are stuck with Kubernetes or Docker. Let's use Nix to create an OCI-compatible image with our tool:
2024-07-08 09:35:53 -05:00
2024-07-08 12:38:39 -05:00
```diff
diff --git a/flake.nix b/flake.nix
index 99d6d52..81e98c9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -10,6 +10,12 @@
in {
packages = pkgsFor (pkgs: {
default = pkgs.callPackage ./what-is-my-ip.nix {};
+ container = pkgs.dockerTools.buildImage {
+ name = "what-is-my-ip-container";
+ config = {
+ Cmd = ["${self.outputs.packages.${pkgs.system}.default}/bin/what-is-my-ip"];
+ };
+ };
});
devShells = pkgsFor (pkgs: {
2024-07-08 09:35:53 -05:00
```
2024-07-08 12:40:25 -05:00
```console
2024-07-08 12:38:39 -05:00
$ docker load < $(nix build .#docker-image --print-out-paths)
2024-07-08 09:35:53 -05:00
Loaded image: what-is-my-ip-docker:c9g6x30invdq1bjfah3w1aw5w52vkdfn
2024-07-08 13:00:16 -05:00
$ docker run -it what-is-my-ip-container:c9g6x30invdq1bjfah3w1aw5w52vkdfn
2024-07-08 09:35:53 -05:00
24.5.113.148
```
2024-07-08 12:38:39 -05:00
Cool! Nix + Docker integration perfectly. The image produced has only the files exactly necessary to run the tool provided, effectively **distroless** . You may also note that if you are following along, your image digest is exactly the same. **Reproducibility!**
2024-07-08 09:35:53 -05:00
2024-07-08 13:00:16 -05:00
Finally, let's take the last step and create a reproducible operating system using NixOS to contain only the programs we want:
2024-07-08 09:35:53 -05:00
2024-07-08 12:38:39 -05:00
```diff
diff --git a/flake.nix b/flake.nix
index 99d6d52..81e98c9 100644
--- a/flake.nix
+++ b/flake.nix
@@ -20,5 +26,27 @@
'';
};
});
+
+ nixosConfigurations = let
+ system = "x86_64-linux";
+ in {
+ default = nixpkgs.lib.nixosSystem {
+ inherit system;
+ modules = [
+ {
+ users.users.alice = {
+ isNormalUser = true;
+ # enable sudo
+ extraGroups = ["wheel"];
+ packages = [
+ self.outputs.packages.${system}.default
+ ];
+ initialPassword = "swordfish";
+ };
+ system.stateVersion = "24.05";
+ }
+ ];
+ };
+ };
};
}
2024-07-08 09:35:53 -05:00
```
2024-07-08 13:00:16 -05:00
Now we can run this NixOS configuration as a reproducible virtual machine:
2024-07-08 09:35:53 -05:00
```console
2024-07-08 12:38:39 -05:00
$ nixos-rebuild build-vm --flake .#default
$ ./result/bin/run-nixos-vm
2024-07-08 09:35:53 -05:00
2024-07-08 12:38:39 -05:00
# I/O snippet from QEMU
2024-07-08 09:35:53 -05:00
nixos login: alice
Password:
2024-07-08 12:40:25 -05:00
$ readlink $(which what-is-my-ip)
2024-07-08 09:35:53 -05:00
/nix/store/lr6wlz2652r35rwzc79samg77l6iqmii-what-is-my-ip/bin/what-is-my-ip
2024-07-08 12:40:25 -05:00
$ what-is-my-ip
2024-07-08 09:35:53 -05:00
24.5.113.148
```
💥 Hash **lr6wlz2652r35rwzc79samg77l6iqmii** present again!
2024-07-08 12:38:39 -05:00
We took a relatively simple script through a variety of applications in the Nix ecosystem: build recipe, shell, docker image, and finally NixOS VM.
2024-07-08 09:35:53 -05:00
2024-07-08 13:00:16 -05:00
One of the super neat part about flakes is that anywhere you find a flake, you
can make use of it. Try it out now!
2024-07-08 13:21:21 -05:00
> **NOTE**: The following obviously runs code from the internet. Be wary of doing this in general.
2024-07-08 13:00:16 -05:00
2024-07-08 13:20:07 -05:00
Run a flake's package:
2024-07-08 13:00:16 -05:00
```console
$ nix run git+https://git.lyte.dev/lytedev/learn-flakes-the-fun-way
24.5.113.148
2024-07-08 13:20:07 -05:00
```
Enter a flake's development environment:
2024-07-08 13:00:16 -05:00
2024-07-08 13:20:07 -05:00
```console
2024-07-08 13:00:16 -05:00
$ nix develop git+https://git.lyte.dev/lytedev/learn-flakes-the-fun-way -c $SHELL
Hello, Nix!
$ what-is-my-ip
24.5.113.148
2024-07-08 13:20:07 -05:00
# want to hack on the Helix text editor?
$ nix develop github:helix-editor/helix
# yeah, seriously! that's it!
```
Load a flake's docker image and run it:
```console
2024-07-08 13:00:16 -05:00
$ docker load < $(nix build git+https://git.lyte.dev/lytedev/learn-flakes-the-fun-way#container --print-out-paths)
Loaded image: what-is-my-ip-container:wg0z43v4sc1qhq7rsqg02w80vsfk9dl0
$ docker run -it what-is-my-ip-container:c9g6x30invdq1bjfah3w1aw5w52vkdfn
2024-07-08 13:20:07 -05:00
```
2024-07-08 13:00:16 -05:00
2024-07-08 13:20:07 -05:00
Run a flake's NixOS configuration as a virtual machine:
```console
2024-07-08 13:00:16 -05:00
$ nixos-rebuild build-vm --flake git+https://git.lyte.dev/lytedev/learn-flakes-the-fun-way#default
Done. The virtual machine can be started by running /nix/store/xswwdly9m5bwhcz9ajd6km5hx9vdmfzw-nixos-vm/bin/run-nixos-vm
$ /nix/store/xswwdly9m5bwhcz9ajd6km5hx9vdmfzw-nixos-vm/bin/run-nixos-vm
2024-07-08 13:20:07 -05:00
```
Run an exact replica my workstation in a virtual machine (as configured via Nix
-- meaning it will not have my passwords on it 😉):
2024-07-08 13:00:16 -05:00
2024-07-08 13:20:07 -05:00
```console
2024-07-08 13:00:16 -05:00
# NOTE: this will probably take a good, long time to build and lots of bandwidth
2024-07-08 13:01:59 -05:00
# NOTE: you probably won't even be able to login
2024-07-08 13:00:16 -05:00
$ nixos-rebuild build-vm --flake git+https://git.lyte.dev/lytedev/nix#dragon
2024-07-08 13:01:59 -05:00
Done. The virtual machine can be started by running /nix/store/abc-nixos-vm/bin/run-dragon-vm
$ /nix/store/abc-nixos-vm/bin/run-dragon-vm
2024-07-08 13:00:16 -05:00
```
2024-07-08 13:20:07 -05:00
Finally, Flakes can take any other Flake as input and use them for their own outputs. This means that they compose wonderfully. Any Flake can use anything from any other Flake. They're basically big ol' distributed functions (if you squint just right).
You can generate your own flake however you like; Flakes provide templating
facilities. Here, you can use my very own template:
```console
$ mkdir -p ~/my-fun-flake
$ cd ~/my-fun-flake
$ nix flake init --template git+https://git.lyte.dev/lytedev/nix#nix-flake
$ git init
$ git add -A
$ git commit -am 'initial commit'
$ nix develop -c $SHELL
```
And now you have everything you need to work on a Nix flake as if you were me!
To add the package from this tutorial, we can simply do this:
```diff
diff --git a/flake.nix b/flake.nix
index 408d4f2..c5d6883 100644
--- a/flake.nix
+++ b/flake.nix
@@ -2,16 +2,20 @@
inputs.pre-commit-hooks.url = "github:cachix/pre-commit-hooks.nix";
inputs.pre-commit-hooks.inputs.nixpkgs.follows = "nixpkgs";
+ inputs.learn-flakes-the-fun-way.url = "git+https://git.lyte.dev/lytedev/learn-flakes-the-fun-way";
+ inputs.learn-flakes-the-fun-way.inputs.nixpkgs.follows = "nixpkgs";
+
outputs = {
self,
nixpkgs,
pre-commit-hooks,
+ learn-flakes-the-fun-way,
...
}: let
systems = ["aarch64-linux" "aarch64-darwin" "x86_64-darwin" "x86_64-linux"];
forSystems = nixpkgs.lib.genAttrs systems;
in {
formatter = genPkgs (pkgs: pkgs.alejandra);
@@ -26,7 +30,7 @@
devShells = genPkgs (pkgs: {
nix = pkgs.mkShell {
- packages = with pkgs; [nil alejandra];
+ packages = with pkgs; [nil alejandra learn-flakes-the-fun-way.packages.${pkgs.system}.default];
inherit (self.outputs.checks.${pkgs.system}.pre-commit-check) shellHook;
};
```
Afterwards you can re-enter your development shell and run our script:
```console
$ nix develop -c $SHELL
$ what-is-my-ip
24.5.113.148
```
2024-07-08 09:35:53 -05:00
Hopefully, seeing the _fun things_ you can do with Nix might inspire you to push through the hard parts.
There is a golden pot 💰 at the end of this rainbow 🌈 awaiting you.
**Learn Nix the fun way.**
[0]: https://fzakaria.com/2024/07/05/learn-nix-the-fun-way.html