NixOS's reproducibility makes it ideal for building development environments. However, if you're used to other distros, you may encounter problems because NixOS has its own logic. We'll explain this briefly below.
On NixOS, it's recommended to only install common tools in the global environment, such as `git`, `vim`, `emacs`, `tmux`, `zsh`, etc. The development environment of each language should be an independent environment for each project.
You should NOT install the development environment of each language in the global environment. The project environment should be completely isolated from each other and will not affect each other.
We can create a development environment using `pkgs.mkShell { ... }` and open an interactive Bash shell of this development environment using `nix develop`.
To see how `pkgs.mkShell` works, let's take a look at [its source code](https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/mkshell/default.nix).
# when distributed building is enabled, prefer to build locally
preferLocalBuild = true;
} // rest)
```
`pkgs.mkShell { ... }` is a special Derivation (Nix package). Its `name``buildInputs` and other parameters are customizable, and `shellHook` is a special parameter that will be executed when `nix develop` enters the environment.
Here is a `flake.nix` that defined a development environment with nodejs 18 installed:
```nix
{
description = "A Nix-flake-based Node.js development environment";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.05";
};
outputs = { self , nixpkgs ,... }: let
# system should match the system you are running on
# system = "x86_64-linux";
system = "x86_64-darwin";
in {
devShells."${system}".default = let
pkgs = import nixpkgs {
inherit system;
overlays = [
(self: super: rec {
nodejs = super.nodejs-18_x;
pnpm = super.nodePackages.pnpm;
yarn = (super.yarn.override { inherit nodejs; });
})
];
};
in pkgs.mkShell {
# create an environment with nodejs-18_x, pnpm, and yarn
packages = with pkgs; [
node2nix
nodejs
pnpm
yarn
];
shellHook = ''
echo "node `${pkgs.nodejs}/bin/node --version`"
'';
};
};
}
```
Create an empty folder, save the above configuration as `flake.nix`, and then execute `nix develop` (or more precisely, you can use `nix develop .#default`), you will find that you have entered a nodejs 18 development environment, you can use `node``npm``pnpm``yarn` and other commands. And when you just entered, `shellHook` was also executed, outputting the current version of nodejs.
### Enter the build environment of any Nix package
Now let's take a look at `nix develop`, first read the help document output by `nix develop --help`:
```
Name
nix develop - run a bash shell that provides the build environment of a derivation
Synopsis
nix develop [option...] installable
# ......
```
It tells us that `nix develop` accepts a parameter `installable`, which means that we can enter the development environment of any installable Nix package through it, not just the environment created by `pkgs.mkShell`.
By default, `nix develop` will try to use the following attributes in the flake outputs:
-`devShells.<system>.default`
-`packages.<system>.default`
If we use `nix develop /path/to/flake#<name>` to specify the flake package address and flake output name, then `nix develop` will try the following attributes in the flake outputs:
-`devShells.<system>.<name>`
-`packages.<system>.<name>`
-`legacyPackages.<system>.<name>`
Now let's try it out. First, test it to confirm that We don't have `c++``g++` and other compilation-related commands in the current environment:
```shell
ryan in 🌐 aquamarine in ~
› c++
c++: command not found
ryan in 🌐 aquamarine in ~
› g++
g++: command not found
```
Then use `nix develop` to enter the build environment of the `hello` package in `nixpkgs`:
```shell
# login to the build environment of the package `hello`
ryan in 🌐 aquamarine in ~
› nix develop nixpkgs#hello
ryan in 🌐 aquamarine in ~ via ❄️ impure (hello-2.12.1-env)
› env | grep CXX
CXX=g++
ryan in 🌐 aquamarine in ~ via ❄️ impure (hello-2.12.1-env)
› c++ --version
g++ (GCC) 12.3.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
ryan in 🌐 aquamarine in ~ via ❄️ impure (hello-2.12.1-env)
› g++ --version
g++ (GCC) 12.3.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```
We can see that the `CXX` environment variable have been set, and the `c++``g++` and other commands can be used normally now.
In addition, we can also call every build phase of the `hello` package normally:
> The default execution order of all build phases of a Nix package is: `$prePhases unpackPhase patchPhase $preConfigurePhases configurePhase $preBuildPhases buildPhase checkPhase $preInstallPhases installPhase fixupPhase installCheckPhase $preDistPhases distPhase $postPhases`
```shell
# unpack source code
ryan in 🌐 aquamarine in /tmp/xxx via ❄️ impure (hello-2.12.1-env)
ryan in 🌐 aquamarine in /tmp/xxx/hello-2.12.1 via C v12.3.0-gcc via ❄️ impure (hello-2.12.1-env)
› ./hello
Hello, world!
```
This usage is mainly used to debug the build process of a Nix package, or to execute some commands in the build environment of a Nix package.
### `nix shell` & `nix run`
Compare to `nix develop`, these two commands are much simpler and easier to understand.
`nix shell` is used to enter an environment containing the specified Nix package and open an interactive shell for it:
```shell
# hello not exists
› hello
hello: command not found
# enter an environment containing hello
› nix shell nixpkgs#hello
# now hello exists
› hello
Hello, world!
```
`nix run` is used to create an environment containing the specified installable and run the installable in it:
```shell
# hello not exists
› hello
hello: command not found
# enter an environment containing hello and run it
› nix run nixpkgs#hello
Hello, world!
```
Because `nix run` will directly run the Nix package as an installable, the Nix package used as its parameter must be able to generate an executable program.
According to the description of `nix run --help`, `nix run` will execute `<out>/bin/<name>`, where `<out>` is the root directory of a Derivation, and `<name>` is selected in the following order:
- The meta.mainProgram attribute of the derivation.
- The pname attribute of the derivation.
- The name part of the value of the name attribute of the derivation.
For instance, if name is set to `hello-1.10`, nix run will run $out/bin/hello.
- [One too many shell, Clearing up with nix' shells nix shell and nix-shell - Yannik Sander](https://blog.ysndr.de/posts/guides/2021-12-01-nix-shells/)