home

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

2019-01-26-nix-run-alias.org (8922B)


      1 #+title: Nix run aliases
      2 #+date: <2019-01-26 Sat>
      3 #+filetags: nixos fish alias nix shell home manager
      4 
      5 * Introduction
      6 
      7 I use [[https://nixos.org/][=NixOS=]] each and every day, everywhere. One really cool feature of =nix= is
      8 =nix-shell= and more recently (with =nix= >= =2.0.0=), =nix run=.
      9 
     10 #+begin_src man
     11 Usage: nix run <FLAGS>... <INSTALLABLES>...
     12 
     13 Summary: run a shell in which the specified packages are available.
     14 
     15 Flags:
     16       --arg <NAME> <EXPR>         argument to be passed to Nix functions
     17       --argstr <NAME> <STRING>    string-valued argument to be passed to Nix functions
     18   -c, --command <COMMAND> <ARGS>  command and arguments to be executed; defaults to 'bash'
     19   -f, --file <FILE>               evaluate FILE rather than the default
     20   -i, --ignore-environment        clear the entire environment (except those specified with --keep)
     21   -I, --include <PATH>            add a path to the list of locations used to look up <...> file names
     22   -k, --keep <NAME>               keep specified environment variable
     23   -u, --unset <NAME>              unset specified environment variable
     24 
     25 Examples:
     26 
     27   To start a shell providing GNU Hello from NixOS 17.03:
     28   $ nix run -f channel:nixos-17.03 hello
     29 
     30   To start a shell providing youtube-dl from your 'nixpkgs' channel:
     31   $ nix run nixpkgs.youtube-dl
     32 
     33   To run GNU Hello:
     34   $ nix run nixpkgs.hello -c hello --greeting 'Hi everybody!'
     35 
     36   To run GNU Hello in a chroot store:
     37   $ nix run --store ~/my-nix nixpkgs.hello -c hello
     38 
     39 Note: this program is EXPERIMENTAL and subject to change.
     40 #+end_src
     41 
     42 As you can see from the =-h= summary, it makes it really easy to run a shell or a command
     43 with some packages that are not in your main configuration. It will download the
     44 package(s) if there are not available in the Nix store (=/nix/store/=).
     45 
     46 A few month ago I decided it would be a perfect use-case for command I do not run
     47 often. My idea was, let's define =aliases= (in the shell) that would make a simple command
     48 call, like =ncdu=, become =nix run nixpkgs.ncdu -c ndcu=. My /shell of choice/ is [[https://fishshell.com/][fish]], so
     49 I decided to dig into the /language/ in order to implement that.
     50 
     51 The use case is the following :
     52 - When I type =foo=, I want the command =foo= in package =bar= to be executed.
     53 - I want to be able to pin a channel for the package — I'm using [[https://matthewbauer.us/][Matthew Bauer]] [[https://matthewbauer.us/blog/channel-changing.html][Channel
     54   Changing with Nix]] setup for pin-pointing a given channel.
     55 
     56 * Fish aliases experimentation
     57 
     58 I had a feeling the built-in =alias= would not work so I ended up trying to define a
     59 /dynamic/ function that would be the name of the command. That's the beauty of the shell,
     60 everything is a command, even function appears as commands. If you define a function
     61 =foo()=, you will be able to run =foo= in your shell, *and* it will take precedence over
     62 the =foo= executable file that would be in your =PATH=.
     63 
     64 I ended up with two main helper function that would create those /alias/ function.
     65 
     66 #+begin_src fish
     67   function _nix_run_package
     68       set -l s $argv[1]
     69       set -l package (string split ":" $s)
     70       switch (count $package)
     71           case 1
     72               _nix_run $s $s $argv[2] $argv[3]
     73           case 2
     74               _nix_run $package[1] $package[2] $argv[2] $argv[3]
     75       end
     76   end
     77 
     78   function _nix_run
     79       set -l c $argv[1]
     80       set -l p $argv[2]
     81       set -l channel $argv[3]
     82       set -l channelsfile $argv[4]
     83       function $c --inherit-variable c --inherit-variable p --inherit-variable channel --inherit-variable channelsfile
     84           set -l cmd nix run
     85           if test -n "$channelsfile"
     86               set cmd $cmd -f $channelsfile
     87           end
     88           eval $cmd $channel.$p -c $c $argv
     89       end
     90   end
     91 #+end_src
     92 
     93 In a nutshell, =_nix_run= is the function that create the alias function. There is so
     94 condition in there depending on whether we gave it a channel or not. So, a call like
     95 =_nix_run foo bar unstable channels.nix= would, in the end generate a function =foo= with
     96 the following call : =nix run -f channels.nix unstable.bar -c foo=.
     97 
     98 The other function, =_nix_run_package= is there to make me write less when I define those
     99 aliases — aka if the command and the package share the same name, I don't want to write it
    100 twice. So, a call like =_nix_run_package foo nixpkgs= would result in a =_nix_run foo foo
    101 nixpkgs=, whereas a call like =_nix_run_package foo:bar unstable channels.nix= would
    102 result in a =_nix_run foo bar unstable channels.nix=.
    103 
    104 An example is gonna be better than the above paragraphs. This is what I used to have in my
    105 fish configuration.
    106 
    107 #+begin_src fish
    108   function _def_nix_run_aliases
    109       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
    110       set -l unstable op:_1password update-desktop-database:desktop-file-utils lgogdownloader
    111       for s in $stable
    112           _nix_run_package $s nixpkgs
    113       end
    114       for s in $unstable
    115           _nix_run_package $s unstable ~/.config/nixpkgs/channels.nix
    116       end
    117   end
    118   # Call the function to create the aliases
    119   _def_nix_run_aliases
    120 #+end_src
    121 
    122 This works like a charm, and for a while, I was happy. But I soon realized something : I'm
    123 not always on my shell — like, I tend to spend more and more time in =eshell=. This also
    124 doesn't work with graphic tools like [[https://github.com/DaveDavenport/rofi][=rofi=]]. I needed actual command, so that external
    125 tools would benefit from that. I ended up writing a small tool, [[https://github.com/vdemeester/nr][=nr=]] that integrates
    126 nicely with =nix= and [[https://github.com/rycee/home-manager][=home-manager=]].
    127 
    128 * A proper tool : =nr=
    129 
    130 The gist for this tool is simple :
    131 - create an executable script that will call =nix run ...= instead of the command
    132 - as for the above fish script, support different channels
    133 - make sure we don't have conflicts — if the command already exists, then don't create the
    134   command
    135 
    136 The =nr= tool would have to be able to manage multiple /profile/, which really stands for
    137 multiple file. The main reason is really about how I manage my configuration ; To make it
    138 simple, depending on the computer my configurations are setup, I may not have =go=, thus I
    139 don't want any =go=-related aliases for a computer that doesn't have =go= (using =go= here
    140 but you can replace with anything).
    141 
    142 #+begin_src fish
    143 $ nr default
    144 > nr generate default
    145 > virtmanager already exists
    146 $ nr git
    147 > nr generate git
    148 #+end_src
    149 
    150 =nr= generates a bash script that does the =nr run …= and mark it as executable. =nr=
    151 needs to be able to clean files it has generated (in case we removed it from
    152 aliases). Thus, I went for a really naive comment in the script. When generating a new set
    153 of commands, =nr= will first remove previously generated script for this profile, and for
    154 that, it uses the comment. Let's look at what a generated script looks like, for the
    155 default profile.
    156 
    157 #+begin_src bash
    158 #!/usr/bin/env bash
    159 # Generated by nr default
    160 nix run nixpkgs.nix-prefetch-scripts -c nix-prefetch-git $@
    161 #+end_src
    162 
    163 The format used in =nr= is =json=. I'm not a /huge fan/ of =json= but it really was the
    164 best format to use for this tool. The reason to use =json= are simple :
    165 
    166 - Go has =encoding/json= built-in, so it's really easy to =Marshall= and =Unmarshall=
    167   structure.
    168   #+begin_src go
    169     type alias struct {
    170             Command string `json:"cmd"`
    171             Package string `json:"pkg"`
    172             Channel string `json:"chan"`
    173     }
    174   #+end_src
    175 - Nix also has built-in support for =json= : =builtins.toJSON= will marshall a /struct/
    176   into a json file.
    177 
    178 Finally, to avoid conflicts at /build time/ (=home-manager switch=) I couldn't use/define
    179 a nix package, but to execute command(s) at the end of the build. One way to achieve it is
    180 to use =file.?.onChange= script, which is executed after [[https://github.com/rycee/home-manager][=home-manager=]] has updated the
    181 environment, *if* the file has changed. That means it's possible to check for executable
    182 files in =~/.nix-profile/bin/= for defined aliases and create those that are not there,
    183 with =nr=. My configuration then looks like the following.
    184 
    185 #+BEGIN_SRC nix
    186   xdg.configFile."nr/default" = {
    187     text = builtins.toJSON [
    188       {cmd = "ncdu";} {cmd = "sshfs";} {cmd = "gotop";} {cmd = "pandoc";}
    189       {cmd = "wakeonlan"; pkg = "python36Packages.wakeonlan";}
    190       {cmd = "beet"; pkg = "beets";}
    191       {cmd = "virt-manager"; pkg = "virtmanager";}
    192       {cmd = "nix-prefetch-git"; pkg = "nix-prefetch-scripts";}
    193       {cmd = "nix-prefetch-hg"; pkg = "nix-prefetch-scripts";}
    194     ];
    195     onChange = "${pkgs.nur.repos.vdemeester.nr}/bin/nr default";
    196   };
    197 #+END_SRC
    198 
    199 And there you are, now, each time I update my environment (=home-manager switch=), =nr=
    200 will regenerate my =nix run= aliases.