nix-book/source/new-flake/bash-flake/main.adoc0
Amy de Buitléir 92900265e2 temp
2025-10-14 11:43:27 +01:00

441 lines
12 KiB
Text

= Bash
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
....
== A simple Bash script
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.
////
$ cp ../cow-hello.sh .
////
[source,bash,linenums]
.cow-hello.sh
....
$# cat cow-hello.sh
....
Add the script to your directory and make it executable.
Let's test it.
....
$ chmod +x cow-hello.sh
$ ./cow-hello.sh # Fails
....
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
....
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
....
For a single script like this, we could modify it by replacing the first line with a "Nix shebang",
as shown later in <<#shebang>>;
then the script would run in any Nix environment.
But for this exercise, we will package it as a "proper" development project.
== Defining the development environment
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.
[source,nix]
....
pkgs = import nixpkgs { system = "x86_64-linux"; };
....
In <<#mkShell>> we learned that
Nix provides the function called `mkShell`, which defines a Bash environment.
We need to specify that the environment should provide `cowsay`.
[source,nix]
....
pkgs.mkShell {
packages = [ pkgs.cowsay ];
};
....
////
$ cp ../flake.nix .
////
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.
[NOTE]
====
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'
====
[source,nix,linenums,highlight="11,13"]
.flake.nix (version 1)
....
$# cat flake.nix
....
The `description` part is just a short description of the package.
The `inputs` section should be familiar from <<#flake-inputs>>.
So far, the `outputs` section only defines a development environment
(we'll add to it in <<#define-package>>).
The repetition of `x86_64-linux` is undesirable.
In <<#multi-arch>> 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.
....
$# echo '$ nix develop # Fails'
$# nix develop --command sh
....
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
(otherwise we would get a confusing message about the file being "missing").
....
$ git add flake.nix cow-hello.sh
$# echo '$ nix develop'
$# nix develop --command sh
....
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
....
[#define-package]
== Defining the package
We created an appropriate development environment, and tested our script.
Now we are ready to package it.
In <<#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 <<#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.
[source,nix]
....
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
....
Here's the updated `flake.nix`.
Again, change `x86_64-linux` if needed to match your system architecture.
////
$ cp ../flake-2.nix flake.nix
////
[source,nix,linenums,highlight="20..38"]
.flake.nix (version 2)
....
$# cat flake.nix
....
Let's test the package.
First, we should exit the development shell so we can verify that the flake dependencies are automatically loaded.
Otherwise the script could use `cowsay` from the development shell.
We'll also commit the changes to get rid of the warnings.
....
$ exit
$ git commit -am "define package"
$ nix run # fails
....
What went wrong?
Although we made `cowsay` available in the runtime environment,
the `cow-hello.sh` script can't find it.
For now, we can just edit the script during the build phase.
We'll see another way to handle this in <<#writeShellApplication>>.
////
$ cp ../flake-3.nix flake.nix
////
[source,nix,linenums,highlight="36..38"]
.flake.nix (version 3)
....
$# cat flake.nix
....
Let's try running the flake again.
....
$ git commit -am "fill in cowsay path"
$ nix run
....
[#multi-arch]
== Supporting multiple architectures
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.
////
$ cp ../flake-4.nix flake.nix
////
[source,nix,linenums,highlight="19..25,51..71"]
.flake.nix (version 4)
....
$# cat flake.nix
....
Let's make sure it still runs on our system.
....
$ git commit -am "suport aarch64-linux"
$ nix run
....
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 <<#genAttrs>>, I introduced the `lib.genAttrs` function,
and included a promising-looking example.
[source]
.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
[source,nix]
....
system:
let pkgs = nixpkgsFor.${system}; in {
default = pkgs.mkShell {
packages = [ pkgs.cowsay ];
};
}
....
And this one will generate a package definition.
[source,nix]
....
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.
[source,nix]
....
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}`.
[source,nix]
....
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.
////
$ cp ../flake-5.nix flake.nix
////
[source,nix,linenums,highlight="10..12,16,17,23,24"]
.flake.nix (version 5)
....
$# cat flake.nix
....
Note that if we need to support another architecture, we only need to add it to line 10.
Let's verify that it runs on our system.
....
$ git commit -am "refactored the flake"
$ nix run
....
== A few more improvements
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.
[source,nix]
....
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.
[source,nix]
....
nixpkgsFor = forEachSystem (system: import nixpkgs {
inherit system;
config = { };
overlays = [ ];
});
....
The Nix manual has more information on
https://nixos.org/manual/nixpkgs/stable/#sec-config-options-reference[config options]
and
https://nixos.org/manual/nixpkgs/stable/#chap-overlays[overlays].
Putting everything together, we have:
////
$ cp ../flake-6.nix flake.nix
////
[source,nix,linenums,highlight="17,18,31,32,50,53..61"]
.flake.nix (version 6)
....
$# cat flake.nix
....
Let's verify that it runs on our system.
....
$ git commit -am "refactored the flake"
$ nix run
....
////
Good adoc0 scripts clean up after themselves.
$ cd .. ; rm -rf my-project # clean up
////