$ mkdir my-project -$ cd my-project -$ git init -Initialized empty Git repository in /home/amy/codeberg/nix-book/source/new-flake/bash-flake/my-project/.git/-
From c015c357fed8ccdd25824b489f5737000814c548 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Amy=20de=20Buitl=C3=A9ir?=
-
@@ -316,16 +307,10 @@ pre.pygments .tok-il { color: #666666 } /* Literal.Number.Integer.Long */
-
-
-
Last updated 2025-10-13 at 19:59:52 IST.
+Last updated 2025-10-14 at 11:02:03 IST.
At last we are ready to create a flake from scratch! No matter what programming languages you normally use, -I recommend that you start by reading the Section 9.1, “Bash” section. +I recommend that you start by reading the [_bash] section. In it, I start with an extremely simple flake, and show how to improve and extend it.
In this section we will create a very simple flake, and then make several improvements. -To follow along, open a new terminal shell. -Start with an empty directory and create a git repository.
-$ mkdir my-project -$ cd my-project -$ git init -Initialized empty Git repository in /home/amy/codeberg/nix-book/source/new-flake/bash-flake/my-project/.git/-
This will be a very simple development project, -so that we can focus on how to use Nix. -We want to package the following script.
-1
-2
-3
#!/usr/bin/env sh
-
-cowsay "Hello from your flake!"
-
-Add the script to your directory and make it executable. -Let’s test it.
-$ chmod +x cow-hello.sh -$ ./cow-hello.sh # Fails -./cow-hello.sh: line 3: cowsay: command not found-
This probably failed on your machine — why?
-The cow-hello.sh script depends on cowsay,
-which isn’t part of your default environment (unless you specifically configure Nix or NixOS to include it).
-We can create a suitable temporary environment for a quick test:
$ nix shell nixpkgs#cowsay -$ ./cow-hello.sh # Succeeds - ________________________ -< Hello from your flake! > - ------------------------ - \ ^__^ - \ (oo)\_______ - (__)\ )\/\ - ||----w | - || ||-
However, that approach becomes inconvenient once you have more dependencies.
-Let’s exit to the original (default) environment,
-and see that cowsay is no longer available.
$ exit -$ ./cow-hello.sh # Fails again -./cow-hello.sh: line 3: cowsay: command not found-
For a single script like this, we could modify it by replacing the first line with a "Nix shebang", -as shown later in Section 10.3, “Scripts”; -then the script would run in any Nix environment. -But for this exercise, we will package it as a "proper" development project.
-We want to define the development environment we need for this project, -so that we can recreate it at any time. -Furthermore, anyone else who wants to work on our project will be able to recreate -the same development environment we used. -This avoids complaints of "but it works on my machine!"
-We can use a function from Nixpkgs to create the environment,
-but we have to configure the package set for the system architecture.
-The function nixpkgs takes an attribute set as input,
-and returns a configured Nix package set that we can use.
-The only attribute we need to specify at this time is the system architecture name.
-For example, the following code creates a package set for the x86_64-linux architecture.
pkgs = import nixpkgs { system = "x86_64-linux"; };
-In Section 3.4, “pkgs.mkShell and pkgs.mkShellNoCC” we learned that
-Nix provides the function called mkShell, which defines a Bash environment.
-We need to specify that the environment should provide cowsay.
pkgs.mkShell {
- packages = [ pkgs.cowsay ];
-};
-Now we’re ready to write the flake.
-Create the file flake.nix as shown below.
-If you’re not on an x86_64-linux system, modify the highlighted lines accordingly.
| - - | -
-
-
-If you’re not sure of the correct string so use for your system, -run the following command. -
-
-
-
-nix eval --impure --raw --expr 'builtins.currentSystem'- |
-
1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
{
- description = "what does the cow say";
-
- inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs";
- };
-
- outputs = { self, nixpkgs }: {
-
- devShells = {
- x86_64-linux.default =
- let
- pkgs = import nixpkgs { system = "x86_64-linux"; };
- in
- pkgs.mkShell {
- packages = [ pkgs.cowsay ];
- };
- }; # devShells
-
- }; # outputs
-}
-
-The description part is just a short description of the package.
-The inputs section should be familiar from Section 6.1, “Inputs”.
-So far, the outputs section only defines a development environment
-(we’ll add to it in Section 9.1.3, “Defining the package”).
The repetition of x86_64-linux is undesirable.
-In Section 9.1.4, “Supporting multiple architectures” we will refactor the code to eliminate some duplication and make it more readable.
-For now, we will stick with the straightforward version.
Let’s enter the shell.
-$ nix develop # Fails -error: Path 'flake.nix' in the repository "/home/amy/codeberg/nix-book/source/new-flake/bash-flake/my-project" is not tracked by Git. - - To make it visible to Nix, run: - - git -C "/home/amy/codeberg/nix-book/source/new-flake/bash-flake/my-project" add "flake.nix"-
Because we haven’t added flake.nix to the git repository, it’s essentially invisible to Nix.
-Let’s correct that and try again.
-We’ll also add the script to the git repository.
$ git add flake.nix cow-hello.sh -$ nix develop -warning: Git tree '/home/amy/codeberg/nix-book/source/new-flake/bash-flake/my-project' is dirty -warning: creating lock file '"/home/amy/codeberg/nix-book/source/new-flake/bash-flake/my-project/flake.lock"': -• Added input 'nixpkgs': - 'github:NixOS/nixpkgs/81a6b38af7d047072d87ff0f0bc75f2b563c5f36?narHash=sha256-9XWnHg2JYVLKB2wDobsLLKXZ/wpzgA6wlWm7HuAFato%3D' (2025-10-13) -warning: Git tree '/home/amy/codeberg/nix-book/source/new-flake/bash-flake/my-project' is dirty-
The warning is because the changes haven’t been committed to the git repository yet.
-We changed flake.nix of course, but when we ran nix develop it automatically created a flake.lock file.
-Don’t worry about the warnings for now.
-We can see that cowsay is now available, and our script runs.
$ ./cow-hello.sh # Succeeds - ________________________ -< Hello from your flake! > - ------------------------ - \ ^__^ - \ (oo)\_______ - (__)\ )\/\ - ||----w | - || ||-
We created an appropriate development environment, and tested our script. -Now we are ready to package it.
-In Section 3.5, “stdenv.mkDerivation” we learned that the function pkgs.std.mkDerivation
-provides a way to create a derivation by specifying the steps that need to be performed in each phase
-We specify the location of the source files.
-and use the standard unpack phase, which makes our source files available in $src
-during the build and installation.
-(We described the environment available during build and installation in Section 8.1, “The Nix standard environment”.)
-We don’t need to do anything in the build phase.
-In the install phase, we copy the script from $src to the output directory, $out,
-and make it executable.
-As with the development environment, we specify that cowsay is required.
pkgs.stdenv.mkDerivation {
- name = "cow-hello.sh";
- src = ./.;
- unpackPhase = "true";
- buildPhase = ":";
- installPhase =
- ''
- mkdir -p $out/bin
- cp $src/cow-hello.sh $out/bin
- chmod +x $out/bin/cow-hello.sh
- '';
- buildInputs = [ pkgs.cowsay ];
-}; # mkDerivation
-To update the flake, add the new lines below to flake.nix.
-Again, change x86_64-linux if needed to match your system architecture.
1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-32
-33
-34
-35
-36
-37
-38
-39
-40
-41
{
- description = "what does the cow say";
-
- inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs";
- };
-
- outputs = { self, nixpkgs }: {
-
- devShells = {
- x86_64-linux.default =
- let
- pkgs = import nixpkgs { system = "x86_64-linux"; };
- in
- pkgs.mkShell {
- packages = [ pkgs.cowsay ];
- };
- }; # devShells
-
- packages = {
- x86_64-linux.default =
- let
- pkgs = import nixpkgs { system = "x86_64-linux"; };
- in
- pkgs.stdenv.mkDerivation {
- name = "cow-hello.sh";
- src = ./.;
- unpackPhase = "true";
- buildPhase = ":";
- installPhase =
- ''
- mkdir -p $out/bin
- cp $src/cow-hello.sh $out/bin
- chmod +x $out/bin/cow-hello.sh
- '';
- buildInputs = [ pkgs.cowsay ];
- }; # mkDerivation
- }; # packages
-
- }; # outputs
-}
-
-Let’s test the package.
-$ nix run -warning: Git tree '/home/amy/codeberg/nix-book/source/new-flake/bash-flake/my-project' is dirty - ________________________ -< Hello from your flake! > - ------------------------ - \ ^__^ - \ (oo)\_______ - (__)\ )\/\ - ||----w | - || ||-
If we hadn’t added cow-hello.sh to the git repository,
-we would have an error about the file being missing.
Congratulations!
-Our package is popular, and people want to run it on aarch64-linux systems.
-So now we need to add an entry to packages.
-Of course we want to test it on the new architecture,
-so we’ll add an entry to devShells as well.
1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-32
-33
-34
-35
-36
-37
-38
-39
-40
-41
-42
-43
-44
-45
-46
-47
-48
-49
-50
-51
-52
-53
-54
-55
-56
-57
-58
-59
-60
-61
-62
-63
-64
-65
-66
-67
{
- description = "what does the cow say";
-
- inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs";
- };
-
- outputs = { self, nixpkgs }: {
-
- devShells = {
- x86_64-linux.default =
- let
- pkgs = import nixpkgs { system = "x86_64-linux"; };
- in
- pkgs.mkShell {
- packages = [ pkgs.cowsay ];
- };
-
- aarch64-linux.default =
- let
- pkgs = import nixpkgs { system = "aarch64-linux"; };
- in
- pkgs.mkShell {
- packages = [ pkgs.cowsay ];
- };
- }; # devShells
-
- packages = {
- x86_64-linux.default =
- let
- pkgs = import nixpkgs { system = "x86_64-linux"; };
- in
- pkgs.stdenv.mkDerivation {
- name = "cow-hello.sh";
- src = ./.;
- unpackPhase = "true";
- buildPhase = ":";
- installPhase =
- ''
- mkdir -p $out/bin
- cp $src/cow-hello.sh $out/bin
- chmod +x $out/bin/cow-hello.sh
- '';
- buildInputs = [ pkgs.cowsay ];
- }; # mkDerivation
-
- aarch64-linux.default =
- let
- pkgs = import nixpkgs { system = "aarch64-linux"; };
- in
- pkgs.stdenv.mkDerivation {
- name = "cow-hello.sh";
- src = ./.;
- unpackPhase = "true";
- buildPhase = ":";
- installPhase =
- ''
- mkdir -p $out/bin
- cp $src/cow-hello.sh $out/bin
- chmod +x $out/bin/cow-hello.sh
- '';
- buildInputs = [ pkgs.cowsay ];
- }; # mkDerivation
- }; # packages
-
- }; # outputs
-}
-
-Let’s make sure it still runs on our system.
-$ nix run -warning: Git tree '/home/amy/codeberg/nix-book/source/new-flake/bash-flake/my-project' is dirty - ________________________ -< Hello from your flake! > - ------------------------ - \ ^__^ - \ (oo)\_______ - (__)\ )\/\ - ||----w | - || ||-
Of course, we should also test on an aarch64-linux system.
-But the flake definition is rather long now.
-If we need to add even more architectures… ugh.
-Even worse, notice that the definitions for x86_64-linux are almost identical
-to those for aarch64-linux.
-All that replication makes maintenance more difficult.
-What if we make a change to the definition for aarch64-linux,
-and forget to make the change to x86_64-linux?
It’s time to refactor the code to eliminate duplication and make it more readable.
-In Section 3.1, “lib.genAttrs”, I introduced the lib.genAttrs function,
-and included a promising-looking example.
nix-repl> lib.genAttrs [ "x86_64-linux" "aarch64-linux" ] (system: "some definitions for ${system}")
-{
- aarch64-linux = "some definitions for aarch64-linux";
- x86_64-linux = "some definitions for x86_64-linux";
-}
-This function generates an attribute set by mapping a function over a list of attribute names. -It takes two arguments. -The first argument is a list; the list elements will become names of values in the resulting attribute set. -The second argument is a function that, given the name of the attribute, returns the attribute’s value
-In the example, we used a list of system architecture names as the first argument. -For the second argument, we used a function that "pretended" to generate definitions.
-What if we wrote a function which, given the name of the system architecture,
-would generate the development shell definition for us,
-and another function that would do the same for the package definition?
-Applying lib.genAttrs and the list of system architecture names
-to those functions would give us
-all the definitions we need for the outputs section.
The following function will generate a development shell definition.
-We will write the definition of nixpkgsFor.${system} shortly
system:
- let pkgs = nixpkgsFor.${system}; in {
- default = pkgs.mkShell {
- packages = [ pkgs.cowsay ];
- };
-}
-And this one will generate a package definition.
-system:
- let pkgs = nixpkgsFor.${system}; in {
- default = pkgs.stdenv.mkDerivation {
- name = "cow-hello.sh";
- src = ./.;
- unpackPhase = "true";
- buildPhase = ":";
- installPhase =
- ''
- mkdir -p $out/bin
- cp $src/cow-hello.sh $out/bin
- chmod +x $out/bin/cow-hello.sh
- '';
- buildInputs = [ pkgs.cowsay ];
- }; # mkDerivation
-}
-So lib.genAttrs [ "x86_64-linux" "aarch64-linux" ]
-followed by the first function will give us the development shell definitions for both systems,
-and followed by the second function will give us the package definitions for both systems.
-We can make the flake more readable with the following definitions.
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
-forEachSystem = nixpkgs.lib.genAttrs supportedSystems;
-So forEachSystem is a function which takes one argument.
-That argument should be a function that, given the system name,
-generates the appropriate definition for that system,
-Now let’s examine the definition of nixpkgsFor.${system}.
nixpkgsFor = forEachSystem (system: import nixpkgs { inherit system; });
-Here we’re using forEachSystem to access the appropriate nixpkgs for a system.
Putting everything together, we have a shiny new flake. -You may want to compare it carefully to the original version, -in order to reassure yourself that the definitions are equivalent.
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-32
-33
-34
-35
-36
-37
-38
-39
-40
-41
{
- description = "what does the cow say";
-
- inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs";
- };
-
- outputs = { self, nixpkgs }:
- let
- supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
- forEachSystem = nixpkgs.lib.genAttrs supportedSystems;
- nixpkgsFor = forEachSystem (system: import nixpkgs { inherit system; });
-
- in {
-
- devShells = forEachSystem (system:
- let pkgs = nixpkgsFor.${system}; in {
- default = pkgs.mkShell {
- packages = [ pkgs.cowsay ];
- };
- });
-
- packages = forEachSystem (system:
- let pkgs = nixpkgsFor.${system}; in {
- default = pkgs.stdenv.mkDerivation {
- name = "cow-hello.sh";
- src = ./.;
- unpackPhase = "true";
- buildPhase = ":";
- installPhase =
- ''
- mkdir -p $out/bin
- cp $src/cow-hello.sh $out/bin
- chmod +x $out/bin/cow-hello.sh
- '';
- buildInputs = [ pkgs.cowsay ];
- }; # mkDerivation
- }); # packages
-
- }; # outputs
-}
-
-Let’s verify that it runs on our system.
-$ git commit -am "refactored the flake" -[master (root-commit) ba27ec5] refactored the flake - 3 files changed, 70 insertions(+) - create mode 100755 cow-hello.sh - create mode 100644 flake.lock - create mode 100644 flake.nix -$ nix run - ________________________ -< Hello from your flake! > - ------------------------ - \ ^__^ - \ (oo)\_______ - (__)\ )\/\ - ||----w | - || ||-
I took some shortcuts in the flake definitions up to this point, just to keep it simple.
-Normally a flake has both a packages section and an apps section.
-The apps section is where we specify executable programs.
-If there is no apps section, then nix run will default to using the package, but that’s not ideal.
A typical flake might have multiple packages and multiple apps.
-(We could even have multiple development environments.)
-Normally we would specify a default package and a default app.
-The command nix run ` flakeurl#appname will run the app named appname from the `apps section of flakeurl.
-If we don’t specify appname, the default app is run.
To define an app, we specify the type and the path to the executable.
-We can re-use the definition from the packages section as shown below.
hello = {
- type = "app";
- program = pkgs.lib.getExe self.packages.${system}.hello;
-};
-Later we might want to add overlays or some configuration options to nixpkgs in our flake.
-We can include the scaffolding for it with the following change.
nixpkgsFor = forEachSystem (system: import nixpkgs {
- inherit system;
- config = { };
- overlays = [ ];
-});
-The Nix manual has more information on -config options -and -overlays.
-Putting everything together, we have:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
-10
-11
-12
-13
-14
-15
-16
-17
-18
-19
-20
-21
-22
-23
-24
-25
-26
-27
-28
-29
-30
-31
-32
-33
-34
-35
-36
-37
-38
-39
-40
-41
-42
-43
-44
-45
-46
-47
-48
-49
-50
-51
-52
-53
-54
-55
-56
-57
-58
-59
{
- description = "what does the cow say";
-
- inputs = {
- nixpkgs.url = "github:NixOS/nixpkgs";
- };
-
- outputs = { self, nixpkgs }:
- let
-
- supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
-
- forEachSystem = nixpkgs.lib.genAttrs supportedSystems;
-
- nixpkgsFor = forEachSystem (system: import nixpkgs {
- inherit system;
- config = { };
- overlays = [ ];
- });
-
- in {
-
- devShells = forEachSystem (system:
- let pkgs = nixpkgsFor.${system}; in {
- default = pkgs.mkShell {
- packages = [ pkgs.cowsay ];
- };
- });
-
- packages = forEachSystem (system:
- let pkgs = nixpkgsFor.${system}; in rec {
- hello = pkgs.stdenv.mkDerivation {
- name = "cow-hello.sh";
- src = ./.;
- unpackPhase = "true";
- buildPhase = ":";
- installPhase =
- ''
- mkdir -p $out/bin
- cp $src/cow-hello.sh $out/bin
- chmod +x $out/bin/cow-hello.sh
- '';
- buildInputs = [ pkgs.cowsay ];
- }; # mkDerivation
-
- default = hello;
- }); # packages
-
- apps = forEachSystem (system:
- let pkgs = nixpkgsFor.${system}; in rec {
- hello = {
- type = "app";
- program = pkgs.lib.getExe self.packages.${system}.hello;
- };
-
- default = hello;
- });
- }; # outputs
-}
-
-Let’s verify that it runs on our system.
-$ git commit -am "refactored the flake" -[master 4c38386] refactored the flake - 1 file changed, 21 insertions(+), 3 deletions(-) -$ nix run -evaluation warning: getExe: Package "cow-hello.sh" does not have the meta.mainProgram attribute. We'll assume that the main program has the same name for now, but this behavior is deprecated, because it leads to surprising errors when the assumption does not hold. If the package has a main program, please set `meta.mainProgram` in its definition to make this warning go away. Otherwise, if the package does not have a main program, or if you don't control its definition, use getExe' to specify the name to the program, such as lib.getExe' foo "bar". - ________________________ -< Hello from your flake! > - ------------------------ - \ ^__^ - \ (oo)\_______ - (__)\ )\/\ - ||----w | - || ||-
sh: line 1: run-code-inline: command not found
Start with an empty directory and create a git repository.
Next, we’ll create a simple Haskell program.
|
For non-trivial Haskell development projects,
-it’s usually more convenient to use |
@@ -6477,161 +5486,11 @@ Line 15 should be replaced with:
1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 | { - description = "a very simple and friendly flake"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs"; - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs { inherit system; }; - in - { - devShells = rec { - default = pkgs.mkShell { - packages = [ pkgs.cowsay ]; - }; - }; - - packages = rec { - hello = pkgs.writeShellApplication { - name = "hello-cow"; - - runtimeInputs = [ - pkgs.cowsay - ]; - - text = '' - cowsay "hello"; - ''; - }; - default = hello; - }; - - apps = rec { - hello = flake-utils.lib.mkApp { drv = self.packages.${system}.hello; }; - default = hello; - }; - } - ); -} - |
We can build the flake to see how it modifies the script.
-$ nix build -$ cat result/bin/hello-cow -#!/nix/store/cl2gkgnh26mmpka81pc2g5bzjfrili92-bash-5.3p3/bin/bash -set -o errexit -set -o nounset -set -o pipefail - -export PATH="/nix/store/gmqzhlll2iz6bin5389j2rwbrlisp18b-cowsay-3.8.4/bin:$PATH" - -cowsay "hello";-
Here’s the flake in action.
-$ nix run - _______ -< hello > - ------- - \ ^__^ - \ (oo)\_______ - (__)\ )\/\ - ||----w | - || ||-
Nix provides a variety of functions that help with common tasks such as creating scripts. -See the section on trivial build helpers -in the Nixpkgs reference manual, particularly -writeShellScript -and writeShellScriptBin.
-sh: line 1: run-code-inline: command not found
In this example, we will use a flake defined in a remote git repo. However, you can use any of the flake reference styles defined in Section 10.1.2, “Run a flake”.
@@ -6801,15 +5660,15 @@ Hello from your flake!If you use haskell-flake (see Section 9.2.5, “The Nix flake”)
+
If you use haskell-flake (see Section 9.1.5, “The Nix flake”)
nothing special needs to be added to flake.nix.
Simply list your Haskell package dependencies to your cabal file as you normally would.
In this example we will access three Haskell packages
(pandoc-linear-table, pandoc-logic-proof, and pandoc-columns)
@@ -6890,7 +5749,7 @@ set up automatically; no need to define devShells.
In this example, we will use a nix package (not a flake) defined in a remote git repo. However, you can use any of the flake reference styles defined in Section 10.1.2, “Run a flake”.
@@ -7112,7 +5971,7 @@ Hello from your flake!