Nix run aliases
Introduction
I use NixOS
each and every day, everywhere. One really cool feature of nix
is
nix-shell
and more recently (with nix
>= 2.0.0
), nix run
.
Usage: nix run <FLAGS>... <INSTALLABLES>... Summary: run a shell in which the specified packages are available. Flags: --arg <NAME> <EXPR> argument to be passed to Nix functions --argstr <NAME> <STRING> string-valued argument to be passed to Nix functions -c, --command <COMMAND> <ARGS> command and arguments to be executed; defaults to 'bash' -f, --file <FILE> evaluate FILE rather than the default -i, --ignore-environment clear the entire environment (except those specified with --keep) -I, --include <PATH> add a path to the list of locations used to look up <...> file names -k, --keep <NAME> keep specified environment variable -u, --unset <NAME> unset specified environment variable Examples: To start a shell providing GNU Hello from NixOS 17.03: $ nix run -f channel:nixos-17.03 hello To start a shell providing youtube-dl from your 'nixpkgs' channel: $ nix run nixpkgs.youtube-dl To run GNU Hello: $ nix run nixpkgs.hello -c hello --greeting 'Hi everybody!' To run GNU Hello in a chroot store: $ nix run --store ~/my-nix nixpkgs.hello -c hello Note: this program is EXPERIMENTAL and subject to change.
As you can see from the -h
summary, it makes it really easy to run a shell or a command
with some packages that are not in your main configuration. It will download the
package(s) if there are not available in the Nix store (/nix/store/
).
A few month ago I decided it would be a perfect use-case for command I do not run
often. My idea was, let’s define aliases
(in the shell) that would make a simple command
call, like ncdu
, become nix run nixpkgs.ncdu -c ndcu
. My shell of choice is fish, so
I decided to dig into the language in order to implement that.
The use case is the following :
- When I type
foo
, I want the commandfoo
in packagebar
to be executed. - I want to be able to pin a channel for the package — I’m using Matthew Bauer Channel Changing with Nix setup for pin-pointing a given channel.
Fish aliases experimentation
I had a feeling the built-in alias
would not work so I ended up trying to define a
dynamic function that would be the name of the command. That’s the beauty of the shell,
everything is a command, even function appears as commands. If you define a function
foo()
, you will be able to run foo
in your shell, and it will take precedence over
the foo
executable file that would be in your PATH
.
I ended up with two main helper function that would create those alias function.
function _nix_run_package set -l s $argv[1] set -l package (string split ":" $s) switch (count $package) case 1 _nix_run $s $s $argv[2] $argv[3] case 2 _nix_run $package[1] $package[2] $argv[2] $argv[3] end end function _nix_run set -l c $argv[1] set -l p $argv[2] set -l channel $argv[3] set -l channelsfile $argv[4] function $c --inherit-variable c --inherit-variable p --inherit-variable channel --inherit-variable channelsfile set -l cmd nix run if test -n "$channelsfile" set cmd $cmd -f $channelsfile end eval $cmd $channel.$p -c $c $argv end end
In a nutshell, _nix_run
is the function that create the alias function. There is so
condition in there depending on whether we gave it a channel or not. So, a call like
_nix_run foo bar unstable channels.nix
would, in the end generate a function foo
with
the following call : nix run -f channels.nix unstable.bar -c foo
.
The other function, _nix_run_package
is there to make me write less when I define those
aliases — aka if the command and the package share the same name, I don’t want to write it
twice. So, a call like _nix_run_package foo nixpkgs
would result in a _nix_run foo foo
nixpkgs
, whereas a call like _nix_run_package foo:bar unstable channels.nix
would
result in a _nix_run foo bar unstable channels.nix
.
An example is gonna be better than the above paragraphs. This is what I used to have in my fish configuration.
function _def_nix_run_aliases set -l stable mr sshfs ncdu wakeonlan:python36Packages.wakeonlan lspci:pciutils lsusb:usbutils beet:beets gotop virt-manager:virtmanager pandoc nix-prefetch-git:nix-prefetch-scripts nix-prefetch-hg:nix-prefetch-scripts set -l unstable op:_1password update-desktop-database:desktop-file-utils lgogdownloader for s in $stable _nix_run_package $s nixpkgs end for s in $unstable _nix_run_package $s unstable ~/.config/nixpkgs/channels.nix end end # Call the function to create the aliases _def_nix_run_aliases
This works like a charm, and for a while, I was happy. But I soon realized something : I’m
not always on my shell — like, I tend to spend more and more time in eshell
. This also
doesn’t work with graphic tools like rofi
. I needed actual command, so that external
tools would benefit from that. I ended up writing a small tool, nr
that integrates
nicely with nix
and home-manager
.
A proper tool : nr
The gist for this tool is simple :
- create an executable script that will call
nix run ...
instead of the command - as for the above fish script, support different channels
- make sure we don’t have conflicts — if the command already exists, then don’t create the command
The nr
tool would have to be able to manage multiple profile, which really stands for
multiple file. The main reason is really about how I manage my configuration ; To make it
simple, depending on the computer my configurations are setup, I may not have go
, thus I
don’t want any go
-related aliases for a computer that doesn’t have go
(using go
here
but you can replace with anything).
$ nr default > nr generate default > virtmanager already exists $ nr git > nr generate git
nr
generates a bash script that does the nr run …
and mark it as executable. nr
needs to be able to clean files it has generated (in case we removed it from
aliases). Thus, I went for a really naive comment in the script. When generating a new set
of commands, nr
will first remove previously generated script for this profile, and for
that, it uses the comment. Let’s look at what a generated script looks like, for the
default profile.
#!/usr/bin/env bash # Generated by nr default nix run nixpkgs.nix-prefetch-scripts -c nix-prefetch-git $@
The format used in nr
is json
. I’m not a huge fan of json
but it really was the
best format to use for this tool. The reason to use json
are simple :
Go has
encoding/json
built-in, so it’s really easy toMarshall
andUnmarshall
structure.type alias struct { Command string `json:"cmd"` Package string `json:"pkg"` Channel string `json:"chan"` }
- Nix also has built-in support for
json
:builtins.toJSON
will marshall a struct into a json file.
Finally, to avoid conflicts at build time (home-manager switch
) I couldn’t use/define
a nix package, but to execute command(s) at the end of the build. One way to achieve it is
to use file.?.onChange
script, which is executed after home-manager
has updated the
environment, if the file has changed. That means it’s possible to check for executable
files in ~/.nix-profile/bin/
for defined aliases and create those that are not there,
with nr
. My configuration then looks like the following.
xdg.configFile."nr/default" = { text = builtins.toJSON [ {cmd = "ncdu";} {cmd = "sshfs";} {cmd = "gotop";} {cmd = "pandoc";} {cmd = "wakeonlan"; pkg = "python36Packages.wakeonlan";} {cmd = "beet"; pkg = "beets";} {cmd = "virt-manager"; pkg = "virtmanager";} {cmd = "nix-prefetch-git"; pkg = "nix-prefetch-scripts";} {cmd = "nix-prefetch-hg"; pkg = "nix-prefetch-scripts";} ]; onChange = "${pkgs.nur.repos.vdemeester.nr}/bin/nr default"; };
And there you are, now, each time I update my environment (home-manager switch
), nr
will regenerate my nix run
aliases.