nix-book/source/new-flake/bash-flake/main.adoc0
Amy de Buitléir 095401025c temp
2025-10-12 22:05:59 +01:00

311 lines
8.5 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
....
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 we will 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!"
////
$ cp ../flake.nix .
////
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="10,12,14"]
.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>>.
If we ignore parts of the long package names, the `outputs` section looks like this:
[source,subs=quotes]
....
devShells = {
x86_64-linux.default =
_blahblah_.mkShell {
packages = [
_blahblah_.cowsay
];
}; # mkShell
}; # devShells
....
This says, in effect, that to create a `default` shell for the `x86_64-linux` architecture,
call the `mkShell` command and tell it you need the `cowsay` package.
The code is rather wordy, with all the _this.that.the.other.thing_ stuff.
Also, the name `legacyPackages` suggests that we're not following current best practices.
(In fact, it could in some circumstances result in duplicate instances of nixpkgs.)
In <<#multi-arch>> we will refactor the code to eliminate some duplication, make it more readable,
and eliminate the references to `legacyPackages`.
For now, we will stick with the ugly, but 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.
....
$ 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
....
== Defining the package
We created an appropriate development environment, and tested our script.
Now we are ready to package it.
Add the new lines below to `flake.nix`.
Again, change `x86_64-linux` if needed to match your system architecture.
////
$ cp ../flake-2.nix flake.nix
////
[source,nix,linenums,highlight="19..36"]
.flake.nix (version 2)
....
$# cat flake.nix
....
Let's test the package.
....
$ nix run
....
If we hadn't added `cow-hello.sh` to the git repository,
we would have an error about the file being missing.
[#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 for that 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-3.nix flake.nix
////
[source,nix,linenums,highlight="18..23,44..59"]
.flake.nix (version 3)
....
$# cat flake.nix
....
Let's make sure it still runs on our system.
....
$ 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 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" ];
forAllSupportedSystems = nixpkgs.lib.genAttrs supportedSystems;
....
Now let's examine the definition of `nixpkgsFor.${system}`.
[source,nix]
....
nixpkgsFor = forAllSupportedSystems (system: import nixpkgs { inherit 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-4.nix flake.nix
////
[source,nix,linenums,highlight="18..23,44..59"]
.flake.nix (version 3)
....
$# cat flake.nix
....
Let's verify that it runs on our system.
....
$ git commit -am "refactored the flake"
$ nix run
....
// TODO In packages, add cow-hello and set default = cow-hello. Explain why.
// TODO Add an apps section and explain why.
////
Good adoc0 scripts clean up after themselves.
$ cd .. ; rm -rf my-project # clean up
////