Image for post nix-run
Oct 7, 2022 by Luc Perkins

Using Nix to run software with no installation steps

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:

nix run github:DeterminateSystems/riff

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:

{
  apps.default = {
    type = "app";
    program = "${myPkg}/bin/my-pkg";
    # Assuming there's a local derivation called myPkg
  };
}

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:

{
  apps = {
    my-linter = {
      type = "app";
      program = "${myLinter}/bin/my-linter";
    };

    my-checker = {
      type = "app";
      program = "${myChecker}/bin/my-checker";
    };
  };
}

To run these apps:

nix run <flake>#my-linter
nix run <flake>#my-checker

# If the flake were in the current directory
nix run .#my-linter
nix run .#my-checker

# If the flake were in a GitHub repo
nix run github:my-org/my-repo#my-linter
nix run github:my-org/my-repo#my-checker

Using Nix apps for nix run instead of packages is especially beneficial when a single derivation builds multiple executables. Here’s an example:

{
  apps = {
    # nix run <flake>#server
    server = {
      type = "app";
      program = "${myPkg}/bin/server";
    };

    # nix run <flake>#client
    client = {
      type = "app";
      program = "${myPkg}/bin/client";
    };
  };
}

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:

{
  description = "runme application";

  inputs = {
    nixpkgs.url = "nixpkgs"; # Resolves to github:NixOS/nixpkgs
    # Helpers for system-specific outputs
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    # Create system-specific outputs for the standard Nix systems
    # https://github.com/numtide/flake-utils/blob/master/default.nix#L3-L9
    flake-utils.lib.eachDefaultSystem (system:
      let
      	pkgs = import nixpkgs { inherit system; };
      in
      {
        # A simple executable package
        packages.default = pkgs.writeScriptBin "runme" ''
          echo "I am currently being run!"
        '';

        # An app that uses the `runme` package
        apps.default = {
          type = "app";
          program = "${self.packages.${system}.runme}/bin/runme";
        };
      });
}

With that configuration in place and pushed to the repo, you could run the runme script with just one command:

nix run github:omnicorp/runme

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:

{
  apps = rec {
    default = runme;

    runme = {
      type = "app";
      program = "${self.packages.${system}.runme}/bin/runme";
    };

    lint = {
      type = "app";
      program = "${self.packages.${system}.runme-lint}/bin/runme-lint";
    };
  };
}

You can now run both of those apps:

nix run github:omnicorp/runme
nix run github:omnicorp/runme#runme # Equivalent to the above

nix run github:omnicorp/runme#runme-lint

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 like nix/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:

# Syntax

nix run github:<owner>/<repo>/<revision>#<executable>

# Examples

## Specific commit ID
nix run github:DeterminateSystems/riff/a71a8b5ddf680df5db8cc17fa7fddd393ee39ffe

## Tag
nix run github:DeterminateSystems/riff/v1.0.0

## Latest commit in a branch
nix run github:DeterminateSystems/riff/secret-branch-for-nix-run

## Target a flake in a subdirectory
nix run "github:hard-to-find/cool-app?dir=nested#specific-app"

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:

nix run github:quicknav/quicknav/experimental-ui

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:

# 🚫 BAD
nix run github:kinda-sketch/could-be-a-bitcoin-miner

# ✅ BETTER (if you've already vetted the code for this commit)
nix run github:kinda-sketch/could-be-a-bitcoin-miner/0a8e7a241db7938b10062218309ba30f6d9f0e2d

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:

nix flake show github:DeterminateSystems/riff

The output:

github:DeterminateSystems/riff/a54624ac12aa2c125d8891d124519fba17aa2016
│├───defaultPackage                                                                                                                                                                                                                                 │
││   ├───aarch64-darwin: package 'riff-1.0.2'                                                                                                                                                                                                       │
││   ├───aarch64-linux: package 'riff-1.0.2'                                                                                                                                                                                                        │
││   ├───x86_64-darwin: package 'riff-1.0.2'                                                                                                                                                                                                        │
││   └───x86_64-linux: package 'riff-1.0.2'
# Other 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.


Share
Avatar for Luc Perkins
Written by Luc Perkins

Luc is a technical writer, software engineer, and Nix advocate who's always on the lookout for qualitatively better ways of building software. He originally hails from the Pacific Northwest but has recently taken to living abroad.