March 28, 2023·5 min read·engineeringnixnushellbuildstooling

Nuenv separates Nix's build model from its Bash conventions

Nuenv experiments with Nushell as a Nix builder. Its implementation shows which behavior comes from Nix and which facilities the standard environment supplies on top.

Bash is deeply embedded in the Nixpkgs standard environment, but it is not the only program Nix can execute as a builder. Nuenv makes that distinction concrete by replacing the familiar shell environment with Nushell. The experiment asks how much package-building work a different shell could express, and what still has to be reconstructed around it.

Luc Perkins introduces Nuenv in the March 28 announcement. He explicitly describes an early experiment with substantial limitations. The useful result is an account of the division between the Nix evaluator, the low-level build contract, and the helper environment package authors normally take for granted.

I like Nix's ability to describe inputs and intended system state, while finding the language and its conventions awkward at times. An experiment that separates the build model from one particular scripting language is therefore interesting. It can identify which difficulties follow from Nix itself and which come from the environment layered above it.

The builder is an input

The Nix 2.13 derivation reference requires a builder attribute naming the program that performs the build. It also describes args for the program's arguments. Bash appears through the standard environment's wrapper, rather than as the only executable allowed by this lower-level interface.

Nuenv wraps the built-in derivation function and selects the Nushell executable as its builder. The announcement shows that executable running a bootstrap script, which then invokes a separate builder script. This preserves Nix's role in supplying declared inputs while moving the build script into another language.

The interpreter and its scripts therefore become part of the declared build arrangement. Using Nushell is not a request to use whichever interactive shell happens to be installed on the developer's machine. The chosen Nushell package supplies the executable used for realization.

This distinction also explains why the experiment can coexist with ordinary Nixpkgs packages. A derivation identifies its own builder. Changing that input for one package does not logically require replacing the builder of every dependency that package consumes.

A fresh environment still needs setup

The low-level derivation documentation says Nix creates a temporary working directory and clears the environment before supplying the derivation's attributes. It deliberately gives PATH an unusable default. A builder cannot assume that the host's normal command search path supplies all its tools.

Nuenv consequently needs to construct the environment in which its build script will run. The announcement describes discovering declared package executables and arranging their bin directories on PATH. Nushell's list processing lets the script transform package paths into the intended search path.

The script must also arrange writable sources. Nix puts path inputs into the store, while the build happens in a temporary directory. The announcement describes copying sources into the build environment. A declared source path supplies the input, but it does not automatically implement the working-tree preparation that a package's commands expect.

Outputs have a similar distinction. Nix supplies an environment variable containing the intended output path. The builder still needs to create the relevant directory and place its files there. The announcement emphasizes that the existence of $env.out does not mean the output directory has already been created.

These details expose work normally hidden inside the standard environment. Replacing Bash is only one part of replacing that environment. The new scripts also need the setup conventions and helpers that make individual package definitions manageable.

Structured commands change the authoring model

Perkins chooses Nushell partly for its structured values and built-in handling of data formats. His account highlights the distinction between ordinary variables and environment variables. In the examples, $foo is local state while $env.foo addresses an environment variable. That separation makes an accidental collision more visible in source.

Custom Nushell commands can also declare typed parameters and provide generated help. The announcement uses that capability for helper commands that package authors can inspect. This changes how the environment documents its own operations, since a helper can expose its interface through the command system.

The initial helpers include substitution operations, with separate commands for writing another file and modifying a file in place. These correspond to a small portion of the standard environment's convenience functions. The announcement does not claim that all existing Nixpkgs helpers have Nushell replacements.

The advertised example build can be invoked through the experiment's flake. Its published commands make the package result concrete:

nix build --print-build-logs github:DeterminateSystems/nuenv
cat result/share/hello.txt

These commands build the example described in the launch article and inspect its output. They do not establish that an arbitrary package written for the Bash standard environment will work unchanged with Nuenv.

Phase and dependency models are unfinished

The experiment initially has one realization phase, named build. Perkins contrasts that with the standard environment's separate configuration, build, installation, and checking phases. Existing packaging conventions can put work into those different stages. A single script phase does not implement all their behavior merely by using a more expressive shell.

Dependencies are also simplified into one package set. The standard environment distinguishes inputs for different roles, including native build tools and inputs relevant to cross-compilation. Nuenv does not yet reproduce those distinctions, and the announcement explicitly excludes cross-compilation support.

The article says Nuenv's supplied packages are available at build time and are not automatically included in the runtime package. The low-level Nix manual separately explains that output scanning records store references found in built outputs as potential runtime dependencies. Together, those accounts distinguish placing a tool on the build path from retaining a reference to an input in the result.

I find those limitations more informative than a comparison of shell syntax alone. The standard environment encodes a packaging model as well as convenient functions. Replacing it requires decisions about dependency roles and phase behavior that a shell cannot supply by itself.

The announcement also identifies the cost of supplying Nushell and the restriction to systems where it runs. Its built-in commands can reduce reliance on external utilities, but that does not prove the resulting environment is smaller or faster for every build. Nuenv's immediate contribution is to make this alternative implementation concrete, while documenting which parts of the established build environment it has yet to provide.