The code and data behind xeiaso.net
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

blog: I was Wrong About Nix (#116)

authored by

Christine Dodrill and committed by
GitHub
fa0b111b dda1916f

+444 -2
+431
blog/i-was-wrong-about-nix-2020-02-10.markdown
··· 1 + --- 2 + title: I was Wrong about Nix 3 + date: 2020-02-10 4 + tags: 5 + - nix 6 + - witchcraft 7 + --- 8 + 9 + From time to time, I am outright wrong on my blog. This is one of those times. 10 + In my [last post about Nix][nixpost], I didn't see the light yet. I think I do 11 + now, and I'm going to attempt to clarify below. 12 + 13 + [nixpost]: https://christine.website/blog/thoughts-on-nix-2020-01-28 14 + 15 + Let's talk about a more simple scenario: writing a service in Go. This service 16 + will depend on at least the following: 17 + 18 + - A Go compiler to build the code into a binary 19 + - An appropriate runtime to ensure the code will run successfully 20 + - Any data files needed at runtime 21 + 22 + A popular way to model this is with a Dockerfile. Here's the Dockerfile I use 23 + for my website (the one you are reading right now): 24 + 25 + ``` 26 + FROM xena/go:1.13.6 AS build 27 + ENV GOPROXY https://cache.greedo.xeserv.us 28 + COPY . /site 29 + WORKDIR /site 30 + RUN CGO_ENABLED=0 go test -v ./... 31 + RUN CGO_ENABLED=0 GOBIN=/root go install -v ./cmd/site 32 + 33 + FROM xena/alpine 34 + EXPOSE 5000 35 + WORKDIR /site 36 + COPY --from=build /root/site . 37 + COPY ./static /site/static 38 + COPY ./templates /site/templates 39 + COPY ./blog /site/blog 40 + COPY ./talks /site/talks 41 + COPY ./gallery /site/gallery 42 + COPY ./css /site/css 43 + HEALTHCHECK CMD wget --spider http://127.0.0.1:5000/.within/health || exit 1 44 + CMD ./site 45 + ``` 46 + 47 + This fetches the Go compiler from [an image I made][godockerfile], copies the 48 + source code to the image, builds it (in a way that makes the resulting binary a 49 + [static executable][staticbin]), and creates the runtime environment for it. 50 + 51 + [godockerfile]: https://github.com/Xe/dockerfiles/blob/master/lang/go/Dockerfile 52 + [staticbin]: https://oddcode.daveamit.com/2018/08/16/statically-compile-golang-binary/ 53 + 54 + Let's let it build and see how big the result is: 55 + 56 + ``` 57 + $ docker build -t xena/christinewebsite:example1 . 58 + <output omitted> 59 + $ docker images | grep xena 60 + xena/christinewebsite example1 4b8ee64969e8 24 seconds ago 111MB 61 + ``` 62 + 63 + Investigating this image with [dive][dive], we see the following: 64 + 65 + [dive]: https://github.com/wagoodman/dive 66 + 67 + - The package manager is included in the image 68 + - The package manager's database is included in the image 69 + - An entire copy of the C library is included in the image (even though the 70 + binary was _statically linked_ to specifically avoid this) 71 + - Most of the files in the docker image are unrelated to my website's 72 + functionality and are involved with the normal functioning of Linux systems 73 + 74 + Granted, [Alpine Linux][alpine] does a good job at keeping this chaff to a 75 + minimum, but it is still there, still needs to be updated (causing all of my 76 + docker images to be rebuilt and applications to be redeployed) and still takes 77 + up space in transfer quotas and on the disk. 78 + 79 + [alpine]: https://alpinelinux.org 80 + 81 + Let's compare this to the same build process but done with Nix. My Nix setup is 82 + done in a few phases. First I use [niv][niv] to manage some dependencies a-la 83 + git submodules that don't hate you: 84 + 85 + [niv]: https://github.com/nmattia/niv 86 + 87 + ``` 88 + $ nix-shell -p niv 89 + [nix-shel]$ niv init 90 + <writes nix/*> 91 + ``` 92 + 93 + Now I add the tool [vgo2nix][vgo2nix] in niv: 94 + 95 + [vgo2nix]: https://github.com/adisbladis/vgo2nix 96 + 97 + ``` 98 + [nix-shell]$ niv add adisbladis/vgo2nix 99 + ``` 100 + 101 + And I can use it in my shell.nix: 102 + 103 + ```nix 104 + let 105 + pkgs = import <nixpkgs> { }; 106 + sources = import ./nix/sources.nix; 107 + vgo2nix = (import sources.vgo2nix { }); 108 + in pkgs.mkShell { buildInputs = [ pkgs.go pkgs.niv vgo2nix ]; } 109 + ``` 110 + 111 + And then relaunch nix-shell with vgo2nix installed and convert my [go modules][gomod] 112 + dependencies to a Nix expression: 113 + 114 + [gomod]: https://github.com/golang/go/wiki/Modules 115 + 116 + ``` 117 + $ nix-shell 118 + <some work is done to compile things, etc> 119 + [nix-shell]$ vgo2nix 120 + <writes deps.nix> 121 + ``` 122 + 123 + Now that I have this, I can follow the [buildGoPackage 124 + instructions][buildgopackage] from the upstream nixpkgs documentation and create 125 + `site.nix`: 126 + 127 + [buildgopackage]: https://nixos.org/nixpkgs/manual/#ssec-go-legacy 128 + 129 + ``` 130 + { pkgs ? import <nixpkgs> {} }: 131 + with pkgs; 132 + 133 + assert lib.versionAtLeast go.version "1.13"; 134 + 135 + buildGoPackage rec { 136 + name = "christinewebsite-HEAD"; 137 + version = "latest"; 138 + goPackagePath = "christine.website"; 139 + src = ./.; 140 + 141 + goDeps = ./deps.nix; 142 + allowGoReference = false; 143 + preBuild = '' 144 + export CGO_ENABLED=0 145 + buildFlagsArray+=(-pkgdir "$TMPDIR") 146 + ''; 147 + 148 + postInstall = '' 149 + cp -rf $src/blog $bin/blog 150 + cp -rf $src/css $bin/css 151 + cp -rf $src/gallery $bin/gallery 152 + cp -rf $src/static $bin/static 153 + cp -rf $src/talks $bin/talks 154 + cp -rf $src/templates $bin/templates 155 + ''; 156 + } 157 + ``` 158 + 159 + And this will do the following: 160 + 161 + - Download all of the needed dependencies and place them in the system-level Nix 162 + store so that they are not downloaded again 163 + - Set the `CGO_ENABLED` environment variable to `0` so the Go compiler emits a 164 + static binary 165 + - Copy all of the needed files to the right places so that the blog, gallery and 166 + talks features can load all of their data 167 + - Depend on nothing other than a working system at runtime 168 + 169 + This Nix build manifest doesn't just work on Linux. It works on my mac too. The 170 + dockerfile approach works great for Linux boxes, but (unlike what the me of a 171 + decade ago would have hoped) the whole world just doesn't run Linux on their 172 + desktops. The real world has multiple OSes and Nix allows me to compensate. 173 + 174 + So, now that we have a working _cross-platform_ build, let's see how big it 175 + comes out as: 176 + 177 + ``` 178 + $ readlink ./result-bin 179 + /nix/store/ayvafpvn763wwdzwjzvix3mizayyblx5-christinewebsite-HEAD-bin 180 + $ du -hs result-bin/ 181 + 89M ./result-bin/ 182 + $ du -hs result-bin/ 183 + 11M ./result-bin/bin 184 + 888K ./result-bin/blog 185 + 40K ./result-bin/css 186 + 44K ./result-bin/gallery 187 + 77M ./result-bin/static 188 + 28K ./result-bin/talks 189 + 64K ./result-bin/templates 190 + ``` 191 + 192 + As expected, most of the build results are static assets. I have a lot of larger 193 + static assets including an entire copy of TempleOS, so this isn't too 194 + surprising. Let's compare this to on the mac: 195 + 196 + ``` 197 + $ du -hs result-bin/ 198 + 91M result-bin/ 199 + $ du -hs result-bin/* 200 + 14M result-bin/bin 201 + 872K result-bin/blog 202 + 36K result-bin/css 203 + 40K result-bin/gallery 204 + 77M result-bin/static 205 + 24K result-bin/talks 206 + 60K result-bin/templates 207 + ``` 208 + 209 + Which is damn-near identical save some macOS specific crud that Go has to deal 210 + with. 211 + 212 + I mentioned this is used for Docker builds, so let's make `docker.nix`: 213 + 214 + ```nix 215 + { system ? builtins.currentSystem }: 216 + 217 + let 218 + pkgs = import <nixpkgs> { inherit system; }; 219 + 220 + callPackage = pkgs.lib.callPackageWith pkgs; 221 + 222 + site = callPackage ./site.nix { }; 223 + 224 + dockerImage = pkg: 225 + pkgs.dockerTools.buildImage { 226 + name = "xena/christinewebsite"; 227 + tag = pkg.version; 228 + 229 + contents = [ pkg ]; 230 + 231 + config = { 232 + Cmd = [ "/bin/site" ]; 233 + WorkingDir = "/"; 234 + }; 235 + }; 236 + 237 + in dockerImage site 238 + ``` 239 + 240 + And then build it: 241 + 242 + ``` 243 + $ nix-build docker.nix 244 + <output omitted> 245 + $ docker load -i result 246 + c6b1d6ce7549: Loading layer [==================================================>] 95.81MB/95.81MB 247 + $ docker images | grep xena 248 + xena/christinewebsite latest 0d1ccd676af8 50 years ago 94.6MB 249 + ``` 250 + 251 + And the output is 16 megabytes smaller. 252 + 253 + The image age might look weird at first, but it's part of the reproducibility 254 + Nix offers. The date an image was built is something that can change with time 255 + and is actually a part of the resulting file. This means that an image built one 256 + second after another has a different cryptographic hash. It helpfully pins all 257 + images to Unix timestamp 0, which just happens to be about 50 years ago. 258 + 259 + Looking into the image with `dive`, the only packages installed into this image 260 + are: 261 + 262 + - The website and all of its static content goodness 263 + - IANA portmaps that Go depends on as part of the [`net`][gonet] package 264 + - The standard list of [MIME types][mimetypes] that the [`net/http`][gonethttp] 265 + package needs 266 + - Time zone data that the [`time`][gotime] package needs 267 + 268 + [gonet]: https://godoc.org/net 269 + [gonethttp]: https://godoc.org/net/http 270 + [gotime]: https://godoc.org/time 271 + 272 + And that's it. This is _fantastic_. Nearly all of the disk usage has been 273 + eliminated. If someone manages to trick my website into executing code, that 274 + attacker cannot do anything but run more copies of my website (that will 275 + immediately fail and die because the port is already allocated). 276 + 277 + This strategy pans out to more complicated projects too. Consider a case where a 278 + frontend and backend need to be built and deployed as a unit. Let's create a new 279 + setup using niv: 280 + 281 + ``` 282 + $ niv init 283 + ``` 284 + 285 + Since we are using [Elm][elm] for this complicated project, let's add the 286 + [elm2nix][elm2nix] tool so that our Elm dependencies have repeatable builds, and 287 + [gruvbox-css][gcss] for some nice simple CSS: 288 + 289 + [elm]: https://elm-lang.org 290 + [elm2nix]: https://github.com/cachix/elm2nix 291 + [gcss]: https://github.com/Xe/gruvbox-css 292 + 293 + ``` 294 + $ niv add cachix/elm2nix 295 + $ niv add Xe/gruvbox-css 296 + ``` 297 + 298 + And then add it to our `shell.nix`: 299 + 300 + ``` 301 + let 302 + pkgs = import <nixpkgs> {}; 303 + sources = import ./nix/sources.nix; 304 + elm2nix = (import sources.elm2nix { }); 305 + in 306 + pkgs.mkShell { 307 + buildInputs = [ 308 + pkgs.elmPackages.elm 309 + pkgs.elmPackages.elm-format 310 + elm2nix 311 + ]; 312 + } 313 + ``` 314 + 315 + And then enter `nix-shell` to create the Elm boilerplate: 316 + 317 + ``` 318 + $ nix-shell 319 + [nix-shell]$ cd frontend 320 + [nix-shell:frontend]$ elm2nix init > default.nix 321 + [nix-shell:frontend]$ elm2nix convert > elm-srcs.nix 322 + [nix-shell:frontend]$ elm2nix snapshot 323 + ``` 324 + 325 + And then we can edit the generated Nix expression: 326 + 327 + ``` 328 + let 329 + sources = import ./nix/sources.nix; 330 + gcss = (import sources.gruvbox-css { }); 331 + # ... 332 + buildInputs = [ elmPackages.elm gcss ] 333 + ++ lib.optional outputJavaScript nodePackages_10_x.uglify-js; 334 + # ... 335 + cp -rf ${gcss}/gruvbox.css $out/public 336 + cp -rf $src/public/* $out/public/ 337 + # ... 338 + outputJavaScript = true; 339 + ``` 340 + 341 + And then test it with `nix-build`: 342 + 343 + ``` 344 + $ nix-build 345 + <output omitted> 346 + ``` 347 + 348 + And now create a `name.nix` for your Go service like I did above. The real 349 + magic comes from the `docker.nix` file: 350 + 351 + ``` 352 + { system ? builtins.currentSystem }: 353 + 354 + let 355 + pkgs = import <nixpkgs> { inherit system; }; 356 + sources = import ./nix/sources.nix; 357 + backend = import ./backend.nix { }; 358 + frontend = import ./frontend/default.nix { }; 359 + in 360 + 361 + pkgs.dockerTools.buildImage { 362 + name = "xena/complicatedservice"; 363 + tag = "latest"; 364 + 365 + contents = [ backend frontend ]; 366 + 367 + config = { 368 + Cmd = [ "/bin/backend" ]; 369 + WorkingDir = "/public"; 370 + }; 371 + }; 372 + ``` 373 + 374 + Now both your backend and frontend services are built with the dependencies in 375 + the Nix store and shipped as a repeatable Docker image. 376 + 377 + Sometimes it might be useful to ship the dependencies to a service like 378 + [Cachix][cachix] to help speed up builds. 379 + 380 + [cachix]: https://cachix.org 381 + 382 + You can install the cachix tool like this: 383 + 384 + ``` 385 + $ nix-env -iA cachix -f https://cachix.org/api/v1/install 386 + ``` 387 + 388 + And then follow the steps at [cachix.org][cachix] to create a new binary cache. 389 + Let's assume you made a cache named `teddybear`. When you've created a new 390 + cache, logged in with an API token and created a signing key, you can pipe 391 + nix-build to the Cachix client like so: 392 + 393 + ``` 394 + $ nix-build | cachix push teddybear 395 + ``` 396 + 397 + And other people using that cache will benefit from your premade dependency and 398 + binary downloads. 399 + 400 + To use the cache somewhere, install the Cachix client and then run the 401 + following: 402 + 403 + ``` 404 + $ cachix use teddybear 405 + ``` 406 + 407 + I've been able to use my Go, Elm, Rust and Haskell dependencies on other 408 + machines using this. It's saved so much extra download time. 409 + 410 + ## tl;dr 411 + 412 + I was wrong about Nix. It's actually quite good once you get past the 413 + documentation being baroque and hard to read as a beginner. I'm going to try and 414 + do what I can to get the documentation improved. 415 + 416 + As far as getting started with Nix, I suggest following these posts: 417 + 418 + - Nix Pills: https://nixos.org/nixos/nix-pills/ 419 + - Nix Shorts: https://github.com/justinwoo/nix-shorts 420 + - NixOS: For Developers: https://myme.no/posts/2020-01-26-nixos-for-development.html 421 + 422 + Also, I really suggest trying stuff as a vehicle to understand how things work. 423 + I got really far by experimenting with getting [this Discord bot I am writing in 424 + Rust][withinbot] working in Nix and have been very pleased with how it's turned 425 + out. I don't need to use `rustup` anymore to manage my Rust compiler or the 426 + language server. With a combination of [direnv][direnv] and [lorri][lorri], I 427 + can avoid needing to set up language servers or the like _at all_. I can define 428 + them as part of the _project environment_ and then trust the tools I build on 429 + top of to take care of that for me. 430 + 431 + Give Nix a try. It's worth at least that much in my opinion.
+4
blog/thoughts-on-nix-2020-01-28.markdown
··· 9 9 10 10 # Thoughts on Nix 11 11 12 + EDIT(M02 20 2020): I've written a bit of a rebuttal to my own post 13 + [here](https://christine.website/blog/i-was-wrong-about-nix-2020-02-10). I am 14 + keeping this post up for posterity. 15 + 12 16 I don't really know how I feel about [Nix][nix]. It's a functional package 13 17 manager that's designed to help with dependency hell. It also lets you define 14 18 packages using [Nix][nixlang], which is an identically named yet separate thing.
+6
nix/sources.json
··· 10 10 "type": "tarball", 11 11 "url": "https://github.com/adisbladis/vgo2nix/archive/1288e3dbf23ed79cef237661225df0afa30f8510.tar.gz", 12 12 "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz" 13 + }, 14 + "xepkgs": { 15 + "ref": "master", 16 + "repo": "https://tulpa.dev/Xe/nixpkgs", 17 + "rev": "71488e7dd46c9530d6781ab7845e6f720591a0b0", 18 + "type": "git" 13 19 } 14 20 }
+3 -2
shell.nix
··· 1 1 let 2 2 pkgs = import <nixpkgs> { }; 3 3 sources = import ./nix/sources.nix; 4 - vgo2nix = (import sources.vgo2nix { }); 5 - in pkgs.mkShell { buildInputs = [ pkgs.go pkgs.niv vgo2nix ]; } 4 + xepkgs = import sources.xepkgs { }; 5 + vgo2nix = import sources.vgo2nix { }; 6 + in pkgs.mkShell { buildInputs = [ pkgs.go pkgs.niv xepkgs.gopls vgo2nix ]; }