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.