mirror of
https://codeberg.org/mhwombat/nix-book.git
synced 2025-12-26 16:24:56 +08:00
441 lines
12 KiB
Text
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
|
|
////
|