While Nix is most widely known for its core features, like fully reproducible package builds and hermetic development environments, it has numerous features that are less fundamental to its value proposition but nonetheless extremely useful. Today, I’d like to bring one such Nix feature into focus: Nix’s ability to run executables with zero setup and nothing more than a URL in hand. Here’s an example:
If you have Nix installed and flakes enabled,
this one command enables you to run Riff in one go, without
needing to run nix profile install
or apt-get
or brew install
or anything
else. There’s really nothing quite like it in the software world, and I’m
excited to let you in on this little secret.
The core mechanism: Nix flakes
The nix run
feature only works with Nix
flakes, that is, with any Nix project with a
properly structured flake.nix
file in the root.. There are two Nix flake
outputs that you can directly
nix run
: packages and apps.
Nix packages
While flake package outputs are typically used to build final artifacts, such
as nix build .#my-app
, you can also nix run
those outputs if **the built
derivation has a bin
directory with an executable in it. If so, Nix tries
these executable names in order:
- The
meta.mainProgram
attribute of the derivation - The pname attribute of the derivation
- The name attribute of the derivation
If none of those are successful, then you can’t nix run
the package output.
The command nix run github:DeterminateSystems/riff
works, for example,
because the default package output has
pname set
to riff
.
Nix apps
Nix apps are executable programs that you can specify using an attribute set with two fields:
If you add the above to your flake outputs, then you can run the app using nix run
in the current directory or nix run <flake>
if flake.nix
is somewhere
else. Because this is the default app, you don’t need to specify an app name
like nix run .#my-app
. To specify multiple apps in a flake:
To run these apps:
Using Nix apps for nix run
instead of packages is especially beneficial when
a single derivation builds multiple executables. Here’s an example:
Full example
Let’s dig in a bit more concretely. Let’s say that you’re working on a tool
called runme in a public Git repo at https://github.com/omnicorp/runme. Your
flake.nix
looks like this:
With that configuration in place and pushed to the repo, you could run the runme script with just one command:
In this case, runme is the default app so nothing needs to come after the
flake URL, but let’s say that you add a second app, called runme-lint
:
You can now run both of those apps:
An important thing to note here is that these nix run
invocations
require zero installation or setup. Here’s what happens instead:
- Nix inspects the
program
string in each app and determines that it needs to build a new package. - Nix builds the package and all of its dependencies and stores the
results in the local Nix store (by default under
/nix/store
). - Once the package has been built, Nix can run it directly from the
local store. In this case, the
runme
package may end up in a location likenix/store/khid71caxbq4g8dhfln8rihyclmrz7c3-runme
. But the installation process happens seamlessly and without any user input.
This multi-step process accounts for why the first nix run
invocation usually
takes a while but is near-instantaneous on future runs. If you opt to use nix run
for running packages—instead of installing them using [nix profile install
] or Home Manager or
some other means—you should take this potential time lag into account.
Using Git revisions as a versioning mechanism
For Nix flakes that are Git repositories, you can point nix run
at specific
Git revisions to run different versions of a package or app. Here’s the basic
syntax for specific revisions on GitHub, along with some examples:
Let’s say that you’ve released a great new tool called quicknav at version
0.1.0. You’ve worked out the kinks and it’s stable enough to offer to the
world. But late at night you implement a wacky idea for the UI that’s
definitely not yet ready for a stable release, but you think your friends
would be entertained by it at the very least. You want them to try it out. What
to do? With Nix, one possibility is to push those changes to separate branch,
let’s say experimental-ui
, and let your friends run that:
In this case, github:quicknav/quicknav
and
github:quicknav/quicknav/experimental-ui
aren’t “versions” of one flake—they’re really just
different flakes. In other words, Nix understands both of those addresses as different “universes” of Nix
code with no necessary connection to each other. The same applies to all Git refs.
Security warning
It bears noting here that nix run
has a pretty loose trust model.
Running nix run github:DeterminateSystems/riff
today and then again tomorrow,
for example, could execute entirely different executables because the flake
address (github:DeterminateSystems/riff) could resolve to different Git
commits at different times. If you’re concerned about this problem—and you
should be!—the best course of action is to always tie your nix run
invocations to specific commits or to Git refs that resolve to a known
commit. Below you’ll find a bad nix run
and a good nix run
:
Granted, worrying about Git refs negates much of the “a-ha!” effect of
nix run
, but if you plan on using this feature with less-than-100%-trusted remote flakes, it’s
nonetheless advised.
How to find out which nix run
targets are available
With the Nix CLI, you can run nix flake show <flake>
to see
what a flake outputs. Let’s see which packages are output by the flake in the
Riff repo:
The output:
From this we can see that Riff’s flake outputs a default package on four
different platforms. When you run nix run
, Nix infers the current host
platform (in my case aarch64-darwin
) and runs the system-specific package if
it’s available or errors out if it isn’t available.
Find out more about how Determinate Systems is transforming the developer experience around Nix
Implications
To my knowledge, no software ecosystem outside of Nix provides a comparably
simple mechanism for running executables. Containerization tools like
Docker and Podman get you
somewhere in the ballpark by enabling you to use docker run
and podman run
to target OCI-compliant images in container registries, but both of those tools
require that the image be already built and pushed, whereas Nix needs nothing
more than a properly formed Nix expression exposed through a known flake URL.
While I wouldn’t necessarily call this a primary, bread-and-butter use case for Nix, I do think that it provides a vivid illustration not just of what Nix can do but also of the power of the Nix flake model for distributing expressions. With Nix, you can often get things done without needing to distribute build artifacts. All you need is valid Nix code and the Nix CLI can build—and run—arbitrarily complex artifacts on the current host. And with flakes providing convenient “addresses” for Nix code, you end up with a robust mechanism for distributing executables.