Mar 28, 2023

By Luc Perkins

Nuenv: an experimental Nushell environment for Nix

I appreciate Bash for its stability and its near-ubiquity in Unix environments, but it has some clear drawbacks. I constantly get bitten by things like variable handling and string interpolation, I almost always need to supplement my Bash scripts with tools like awk, sed, curl, jq, and wget to get even basic things done, and I'd be hard-pressed to use “Bash” and “elegant” or “expressive” in the same sentence.

Despite shortcomings like these, Nixpkgs uses Bash as the builder in its standard environment (or stdenv), most notably in the stdenv.mkDerivation function. This wrapper over Nix's built-in derivation function is used almost everywhere in Nixpkgs and acts as a de facto default in the Nix community.

I believe that standardizing on Bash as a known quantity was absolutely the right choice for the standard environment. But I've always been intrigued by the possibility of using a more powerful, ergonomically-minded shell to realise Nix derivations. So I decided to give it a try by creating a Nix realisation environment that uses Nushell, a powerful, cutting-edge shell written in Rust and under heavy development, instead of Bash. I'm calling it Nuenv and it's proven to be quite a fruitful exercise and yielded some promising initial results.

Why I chose Nushell

There are many “alt” shells out there, like Fish, Elvish, and Oil. These are all exciting projects but Nushell really stands out to me. Here's a non-exhaustive list of reasons why:

While Nushell isn't quite a general-purpose programming language, it certainly skirts the boundary between a shell and a full-fledged language. Not all of Nushell's features are equally germane to an alternative environment for Nix, of course, but I think that these features show how ambitious the project is.

Here's what I think Nushell has to offer a new Nix environment:

# Say hello to <name> and add a ! to the end if <exclaim> is set.
def sayHello [
  name: string,        # The person to say hello to
  --exclaim(-e): bool, # Whether to add a ! at the end
] {
  echo $"Hello, ($name)(if $exclaim { "!" })"
}

Now here's the help output you get by running sayHello --help:

Say hello to <name> and add a ! to the end if <exclaim> is set.

Usage:
  > sayHello {flags} <name>

Flags:
  -e, --exclaim - Whether to add a ! at the end
  -h, --help - Display the help message for this command

Parameters:
  name <string>: The person to say hello to

As you can see, custom commands in Nushell feel like well-crafted CLI tools in their own right. This makes Nushell-based environments far more introspectable than their Bash counterparts.

How I created the environment

To create my new build environment for Nix, Nuenv, I first wrote a Nix function wrapping Nix's built-in derivation function:

{
  mkNushellDerivation = {
    nushell,       # A Nushell package
    name,          # The name of the derivation
    src,           # The derivation's sources
    system,        # The host system
    packages ? [], # Same as in stdenv
    build ? "",    # Same as in stdenv
  }:

  derivation {
    inherit build name packages src system;
    builder = "${nushell}/bin/nu";
    args = [ ../nuenv/bootstrap.nu ];

    # Attributes passed to the environment
    # (prefaced with __nu_ to avoid naming collisions)
    __nu_builder = ../nuenv/builder.nu;
    __nu_nushell_version = nushell.version;
    __nu_envFile = ./env.nu; # Helper functions
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

The basic mechanics: Nushell (set as the builder instead of Bash) runs a script called bootstrap.nu that performs some setup actions (like making the Nushell executable itself discoverable in the env). The bootstrapper then runs a builder.nu script that performs the actual realisation. As in all Nix derivations, the other attributes of the derivation are set as environment variables available to that script (plus some others that Nix provides). The builder.nu script needs to do quite a lot, including:

That establishes a kind of bare minimum environment. Beyond that, a builder script should provide things like helper functions (commands in Nushell) for use in derivation logic. The standard environment provides many such functions via the user-env.nu script, like wrapProgram and substituteInPlace. For Nuenv, I've provided two so far:

In principle, however, the full battery of stdenv functions could be replaced with custom Nushell commands. Overall, I was extremely impressed with how elegant the Nushell build script is compared to what its Bash equivalent would be. I'll provide an illustrative example. Here's how I set the PATH to discover packages in the Nix store:

# Parse the __nu_packages environment variable string, where each
# package path is separate by a space, and convert to a list
let packages = ($env.__nu_packages | split row (char space))

# Convert the list to a colon-separated string
let $packagesPath = (
	$packages
	| each { |pkg| $"($pkg)/bin" }
	| str collect (char esep)
)

# Set the PATH
let-env PATH = $packagesPath

Here's how you would accomplish something like that in Bash:

export PATH=
for i in $packages; do
    if [ "$i" = / ]; then i=; fi
    PATH=$PATH${PATH:+:}$i/bin
done

As you can see, Nushell feels closer to something like Ruby.

Despite wins like this, Nuenv in its current state does have some major shortcomings vis-à-vis the Nixpkgs standard environment:

So definitely don't use Nuenv for any serious purpose just yet.

User-facing benefits of Nuenv

Although Nuenv is still at an early stage, I think that it already has some nice advantages over stdenv:

Try it out

You can try out Nuenv in several ways. To build an example package using the environment:

nix build --print-build-logs "github:DeterminateSystems/nuenv"

# See the result
cat result/share/hello.txt

This is a pretty simple derivation that pipes a string to GNU hello and writes that output to the share directory in the build output.

You can also build your own package with Nuenv! Here's an example flake.nix:

{
  inputs = {
    nixpkgs.url = "nixpkgs";
    nuenv.url = "github:DeterminateSystems/nuenv";
  };

  outputs = { self, nixpkgs, nuenv }: let
    overlays = [ nuenv.overlays.default ];
    systems = [
      "x86_64-linux"
      "aarch64-linux"
      "x86_64-darwin"
      "aarch64-darwin"
    ];
    forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f {
      inherit system;
      pkgs = import nixpkgs { inherit overlays system; };
    });
  in {
    packages = forAllSystems ({ pkgs, system }: {
      default = pkgs.nuenv.mkDerivation {
        name = "hello";
        src = ./.;
        # This script is Nushell, not Bash!
        build = ''
          "Hello" | save hello.txt
          let out = $"($env.out)/share"
          mkdir $out
          cp hello.txt $out
        '';
      };
    });
  };
}
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

If you've installed Nix with flakes enabled, you can copy this flake.nix into a directory and run nix build && cat ./result/share/hello.txt to see the result. To run a Nushell script instead of providing a raw string, you can supply build = builtins.readFile ./my-nushell-script.nu to the derivation.

Ramifications

I think that using a more powerful builder for Nix—maybe Nushell, maybe something else—has major implications:

While I don't see Nushell or its ilk taking over the Nix world by storm any time soon, I do hope that this post gets you thinking about what a qualitatively better builder might mean for Nix.

Conclusion: an oh-so-worthy experiment

My work on Nuenv hasn't yielded a particularly robust Nix environment just yet but it's been a fantastic learning experience about some of the lower-level nitty gritty of how Nix works. If you're looking to level up your Nix knowledge, I strongly encourage you to take on a similar project (or hack on Nuenv!). You may just end up blazing a fruitful new trail for yourself and others in the Nix community—or with a renewed appreciation for the tried-and-true standard environment.

Finally, using Nushell in conjunction with Nix has frankly sparked joy in a way that trusty old Bash just doesn't (for me, at least). While that isn't a very “engineeringly” justification for building something, there's much to be said for working on things that compel a dramatic shift of energy and attention. We don't have any concrete plans to take Nuenv much further at this time but we'd love to see how the community reacts.