Transcript
This transcript was autogenerated. To make changes, submit a PR.
Hello everyone, welcome to my talk about reproducible and ephemeral development
environments with Nix for our Golang projects.
A little bit about myself my name is Haseeb Marjid. I'm a backend
software engineer at Fintech called Curve. There's a link to my
blog, a few fun facts about myself, very like cats, and I'm an
avid village cricketer, bold and underlined on the
village part there. But I'm looking forward to the cricket seasons dying in the next
couple of weeks or so. It'll be good to get outside.
So who is this talk aimed at? It's aimed at few groups of people,
the first group being those who are kind of interested in Knicks and want to
learn a bit more. And for sure we'll cover that. It's also
aimed at those people who are looking to improve the developer experience, particularly around
the consistency of the development environments. So things like
going back to an old project like six months ago that you haven't touched and
you're worried that you're going to spend a half a day yak shaving getting the
development environment working, you're going to want to want
to faff around with that. You want to just get on building features, bug fixes,
adding tests, whatever you want to do, work on actual code.
And so we'll take a look at how we can use nics to help with
that. We'll also look at how we can make it easier to onboard
new developers. So they have to type one or two commands just to
get set up on a project. And especially if we can have consistency across lots
of our projects, imagine at work then
it means that developers kind of know they have one or two commands to write
and they can jump between projects really easily and again get coding as
fast as possible. Then also we don't just want things to
work on our machine, we want them to work across everywhere
we run this. So whether that be locally CI or
even say like your various environments
that you have production dev, etcetera, etcetera.
And we don't want to annoy Samuel Jackson, do we want it to work everywhere?
We don't want it to just work on our machine. So some of
you probably wondering what's Nix? Nix is a declarative package
manager, and we'll cover what declarative means in a second. But package manager
in the sense it's a tool for installing packages on our
machine, which is kind of, especially as a software developer, kind of a
fundamental thing we need to do on our machines is install packages.
It's similar to tools like ApT, Pac man or brew that
you may be familiar with. It's powered by this thing that I'm going to call
Nixlang. You may hear it called the Nix
programming language and it's this pure, functional and
lazily evaluated language. And what we mean by pure
and functional is that it doesn't have really side
effects. It does have like one or two, but basically for the same input,
has the same output. It doesn't really depend on the state of your machine.
So it means if we have these like NICs configuration files, we can kind of
easily move them between devices and people because,
you know, it's not really relying on the state of your machine, which is quite
nice and lazily evaluated in the sense that it just works out
kind of the bare minimum it needs. It's lazy, it's just like, okay,
I have enough information to go build this package, I don't need any more.
And that's particularly useful. You know, we have 80,000 packages in the Nix
packages repository. We don't want to build all of them,
for example. We just want to build the ones that we need for the packages
we want, right? There's also this thing called Nixos Linux
distribution that's powered by Nixlang and can be configured
using Nixlang. And it's also powered by the Nix package manager.
We're not really going to talk much about that. I do daily drive it,
it's pretty cool. But just know it's separate from Nix. You could have like say
an Ubuntu machine that's running the Nix package manager.
So when we say something is declarative, what we mean is
that we kind of just care about the final state of things. Whereas typically
package managers are imperative where we're giving it like step by step instructions.
So for example, if I was to say make me a cup of tea and
then I gave you instructions like, you know,
get the teabag, turn on the kettle, etc. Etcetera, that would
be what you might classify as imperative. Whereas declarative is just me saying can you
make me a cup of tea with milk and sugar? And then you kind of
work out how to get to that final state. And it's very much the same
with nics. In this case we have the sway tiling window manager
that we want to turn on and we want to turn off the I three
window manager. We're not telling Nix how to do that. We just say this
is the final state and mix goes off, runs off and does that.
One of the other cool things about generally
when things are declarative is we often put them into code and then we
get other benefits, like version control, easier for people to review,
easier to reproduce as well, because again, it's not really caring
about the state of our system. If I don't have I three installed, for example,
Nix will just work this out fine, it just won't uninstall I three.
It's pretty cool. Then I think, I think it's definitely
a really useful feature of Nix. And then once
I definitely will have converted you to Nix, you can start busting this out into
conversation. By the way, I use Nix to your friends and just
doesn't matter what the conversations, you can always change it and I'm sure
they won't mind at all.
What's the problem we're solving? Well, imagine like typically when
we have binaries installed, we might have them in user local bin, like go
Lang ci lint. There are a few problems that
the typical package managers have. It's like,
what dependencies does this need? Like runtime and a
build time? Like how easy is it to discover that? What configuration
flags was this tool built with an environment? Variables,
right? If you wanted to like build the same version yourself, how'd you
do that? And then how do we have two versions of this package? What if
we need to have version two, for example, in version one?
Now as far as I'm aware, Go Lang Ci lint doesn't have a version two,
but it might. And for some projects, you might want to use version until
you've upgraded it. And for some projects, you might want to use version two.
How do you do that? Because typically our package managers replace the binaries
in place, right? So we replace this with version two. So how do we
maintain multiple versions? And there's various packaging solutions that kind of
solve some of these problems. You have like snaps and flat packs which create these
sandbox environments, and they have their dependencies, I believe,
in these kind of sandbox environments, and they don't really interact with the rest of
the system. We have ASDF for managing like
multiple versions of some of our tooling, like Go node,
Python, et cetera, et cetera. We have virtual environments and to a
certain degree go modules, so we can have per project
dependencies and we don't need them globally installed.
More so for virtual environments than go modules, but yeah, virtual environments
used in python. For those of you who may not know,
to summarize this section, we want to have reproducible and
ephemeral environments. NICS is an ecosystem of tools. So we
have NICs, Nixos, Nix packages, the Nix programming language,
the main thing, of course, being the package manager. And then our current packaging
systems all have various flaws. There's nothing in software engineering that's
a silver bullet, Nics included. It can be a bit complicated. Nics,
you kind of have to learn this programming language, which puts people off.
But I think the upsides beat the
downsides of Nics, in my opinion. So if we take a look at this demo,
we look for this go lang ci lint binary, can't find
it. We go into this project, we load this dev environment, and then when we
look for this binary, we can find it. And when we leave this
project, we will no longer have this. And this is kind of the state we
want to get to. This is kind of what we want for our developers.
And we want to make sure that people are getting the same versions of tooling
as everyone else. In this case, I think it's version 1.56.2.
We want to make sure all the developers are getting that same versioning.
And when I say ephemeral development environments, what I
mean is short lived, temporary in the sense that just
existing for the lifecycle of this project. And when we leave it, the environment's kind
of gone or not loaded.
But yeah, so that's kind of what we're going to achieve and we're going to
take a look throughout this talk how we can achieve that.
So how does this relate to go lang? Well,
with go, we need tooling to aid development, right?
Like we might need binaries to generate code coverage reports. We may
have tools to vet our code. Static code analysis,
container code analysis, all these things that go into development,
you know, Docker and Docker compose. We have,
you know, dependencies for our projects and we're not really going to worry about those,
you know, that we manage viago, but the other tooling
we need to aid our development, maybe we have a task runner, like we have
makefiles, right? Like we might do make lint or make test.
And we want to make sure developers have similar versions or the same versions if
we can. And we want to make sure the same versions are running in CI,
they're running locally, because I'm sure we've all been bit by the
bug where it fails in CI is working
locally and it's just because of a version mismatch. Couldn't see our
word for a second. So one way we can kind of do this is,
and we do the set curve for some of our projects is we have this
tools go file. And essentially what we're going to do here
is we're going to manage our dependencies using go module. So we install these
various packages using go modules and
then we add these underscores here to trick go modules into thinking
it's important. So if you do a go mod tidy, it doesn't remove these.
And the cool thing is then they're all kind of managed Vigo modules and
we can do an update. We just update one file and it will update
our dependencies and etc etcetera. It's quite
nice. And we have this build flag so it doesn't get built with our binary
and that works for go dependencies. And we maybe have a make target like this
which installs these dependencies in our go path bin folder,
which is not which works. But then we encounter similar
problems as what we're talking about before. What if between projects we
have version one of one tool and version two of another tool. We kind
of have to remember then to run this tool which is going to overwrite the
binary in that go path bin folder. There's something else we have
to remember to do when we're jumping between projects. And then what if we want
to manage tooling not related to go? What if we wanted to make sure that
the user has GNU make or GNU
parallels or some other CLI tools and
maybe we have some bash scripts or something. And we want to make sure certain
tooling is available in that. And so I think this is where Nix can come
in and fix a few of our problems. So let's take a look at how
we can create a development environment in Nixon.
So imagine we have a project like this, really simple go project.
How are we going to create this Nix environment? Don't worry about the syntax of
nics and what's kind of happening behind the scenes. We're going to take a look
at that just later in this talk in a couple of minutes. But first we're
going to take a look at how we can create this development environment.
So we have this flake Nix file. Think of it as a main go file,
as the entry point to our Nix configuration.
It has a bunch of inputs and a bunch of outputs. In this case,
our inputs are all basically going to be git repositories. So one of them being
Nix packages, which is this repository that has 80,000 packages.
So this is where we're going to install our packages from, has a bunch
of outputs. Nix flakes can output a lot of stuff.
They can output build a docker image they can build an ISO, they can
create a development shell, which is what we're going to do, Dev Shell.
They can build a package, they can do lots of various different things, but we're
really just going to focus on dev shells today. But it's just good to know
generally speaking. So we have this helper library called flake utils,
which basically just reduces some boilerplate in our flake where
obviously packages have to be built for specific architecture.
So you know, like x 86 64 Linux or Arch
Darwin, you know, it's like AMD and intel versus
arm based chips, different architecture. So we have to build the binaries
differently and so packages are built, we have different slight packages.
So we're here basically what we're doing is we're just specifying that we want to
just get the packages for our system architecture and flake
utils. Lib is this library that helps us reduce the boilerplate to do that.
But that's basically all we're doing here. So don't necessarily worry about that here.
What we're doing is we're creating a default dev shell. We have this
packages make shell function. So between the kind of curly braces is
this function and we're passing a parameter called packages,
and these are all the packages we want to make available to the user of
this dev shell. The details
again don't matter too much that I just took that from a project I had
called Optinx, which I've linked later on. You can take a look at that.
So we take a look at, so how do we use this? Well,
if we kind of look for the binary, we won't find it,
it's fine. Then we do nixdevelop, which will load our development shell that we just
created there. And then when we look for our go lang ci link binary,
we can kind of see at this funny path next door, some funny
characters hash maybe, and then yeah, cool.
So we've created a development environment. So if we kind of summarize what we've done
so far, well, we can leverage flakes and dev shells for installing
packages. We can load into those shells or that shell using nixdevelop.
We can make sure each developer gets the same package. We have this concept
of this flake lock file which locks our inputs, and we'll take
a look at the syntax of that and how that works a little bit later
in this talk, but just keep that in mind and
we can update this lock file, but we have to kind of manually do it.
So one other thing that's quite cool is we can
use this again nix agnostic tool called diranv.
And with Diram what we do is create an MVC file,
and what we put in this MVC file will get executed when
we load into this directory automatically. So what we can do here
is do this useflake, which is kind of this helper function for
running Nix developer automatically. The very first time we load into a
directory that has deriv, we do have to has this MVC
file with Dirham, we do have to approve it so we don't just run
arbitrary code on our machine. So let's take a look at what that looks like.
So imagine I'm at work and I need to add a feature to a project.
I clone this example project, I try and find this linter.
I want to lint the code right, can't find it? Fine,
I load in. I do Durin Valalau, because remember that first time we have to
do Durin Valalau which has this useflake. It will load in this dev
shell. In this case it's ready cached.
It doesn't need to do anything. It can just load in one that I already
had. Then in my case, because I'm
using starship prompt, and I'm sure other prompts do this as well, it will let
me know that I'm in a development environment here with the viya, and it has
that little flake, which is quite nice. Just a good reminder that you're
in this dev shell. Then when we look for this binary, we can
find it. When we leave this folder, we can't find anymore.
And so that's again that ephemeral nature I was talking about. And one
of the cool things about Dirham is the first time you go into a directory,
it'll tell you you have to do Durham Valao, so it's not something that you
have to remember, it will tell you. And again, you only have to do it
once. Next time I go to that directory, you won't have to run that again.
One other thing we can do with Durham, the Nix flakes,
is we can point to a dev shell or
some Nix configuration that has a dev shell in
a remote repository. In this case it's on GitHub, and we can use
that if we want. So we can share configuration, and we could
use multiple flakes as well if we wanted to. But we're not really going to
get into that for this talk. But just to know if you want to have
a remote development environment, you can as well, or the
config remote, you can use that. One other
thing that I think can really improve the developer experience and something we can manage
with NICs is pre commit. So you know,
we have these things called githooks, which is these scripts we can run at various
stages of the git process, like pre commit, post commit, pre push,
post push, etc. Etcetera. Then there's this tool which can be a bit
confusing, as in this bit confusingly named called pre commit,
which will basically help us create these pre commit git hooks for us.
So we can create this using nics. So if we do pre commit hooks,
add that as input. Again, you can name these inputs wherever you
want, just helps to kind of make them somewhat related.
We add that into our output section here and say we want to create these
pre commit hooks. It has some built in hooks for go that we can use.
So we use golangs. We're going to enable the linter and we're going
to enable tests. The cool thing is it will only lint
and run tests on the files that have changed, as in the ones we're trying
to commit. And so yeah, we get some really fast feedback when
these run. Saves us time waiting for CI and save some
credits as well, build time that could go
used for somewhere else. Then we can add the to our make shell
function that we had. And the shell hook is just a command that
will run automatically. When we do nix develop, I will load into the shell,
which is going to happen automatically when we're using Diran. So essentially when we go
into the folder it's going to install our pre commit hooks for us.
Whereas normally the developer would have to remember to run the pre commit
install like it's another command they have to do and now they don't have to
think about, which I think is pretty cool. And with these pre
commit hooks you can get some again really fast feedback. So it closes that kind
of feedback loop and lets the developer know something's going
wrong or not. To kind of summarize this bit, what we've done, we can
use Durham to further reduce cognitive load on our developers.
We can use flakes from remote git repositories, share them between multiple projects
if we want. We can also manage precommit in nics. Just something to
note that pre commit is usually managed using a YAML file,
and now we're using a nIcs. And some people do have an issue with
abstracting away from the original the way we configure
a tool. I don't mind it, but just something to consider.
It's kind of the next section I want to cover is how does Nix work?
Like what's happening behind the, behind the scenes. So everything in Nics is
an expression which I believe is quite common for functional programming languages.
Remember, this is powered by Nixlang. And so what we have is we
have a file, maybe it's called shell Nix, and this will get imported somewhere.
And we have this function essentially here in this file that takes in
one parameter called packages, and then we have these triple dots which
ignore any other parameters passed.
Then we have this function call called packages make shell,
and we pass a bunch of packages we want to install. So in this case,
this nix expression, we return one nics expression from
the file, which can be a compound of
other Nics expressions put together, but we always return one.
So in this case we're returning this function, and again this, this file
will get imported and during the import that you'll
have to pass packages. And yeah, we were kind of doing
this with our flake dot nix file, but it was a little bit more
hidden, I guess, what was
going on. But that's kind of what nix is and kind of what we're doing
here. So you might be wondering like, okay, we're, what's this
go Lang ci lint? I get it's coming from Nics packages,
but what, what does that mean? Well, on nix packages, the GitHub repository,
we can go find the go Lang ci Linux expression, and it has this function
called build gomodule, which is a helper function
for building go modules. And you can see as like a name a version where
to fetch it from. GitHub has a bunch of other information about
how to build it and if it has any dependencies, et cetera, et cetera.
So we have this nix expression there. Cool. If we dive
a bit deeper and look at what's behind the build go module, it abstracts
away the standard env derivation, where this derivation
function is the most important built in Nics function.
So when we're building packages, what's actually happening? It's a two step
process. So when we do nix develop behind the scenes it'll be calling like Nix
build of some kind. And behind the scenes we'll be doing this,
this in two steps. And you'll see why we
do this in two steps. So the first step is evaluation time. We take
the nix expressions and the Golang Ci lint expression, and we return
a derivation set. This DRV file where a derivation
set or derivation is just a set of instructions how to build a package,
kind of like a recipe. Then we have this build time.
The derivation is built into a package, and that is what has a
side effect on our machine where stuff is actually getting installed.
So let's take a look at derivation. So derivations are put into
our next store folder. They have the format hash name
version dRv, where a hash is a cryptographic hash
of all the inputs to that derivation. So let's say we have this go 121
eight derivation, even if we're building go 121 eight, let's say
we change an environment variable. We are
going to get a different cryptographic hash there, and so we're going to get a
different derivation, and for all intents and purposes it's a different package as
far as Nix is concerned. So derivations
and also packages are mutable. I mean, you can obviously
go change them if you wanted to. Nix discourages you from doing that.
You probably shouldn't, but you can if you really want to.
Let's take a look at what this derivation looks like, and we can run this
command at the top there. Don't worry if some of
this is cut off. Doesn't really matter, it's more just high level what's
going on. So we have a builder, how we're going to build it bash
we have a bunch of environment variables. These are the only environment variables made
available during the build. We have a bunch of input derivations,
so other derivations this depends on, and Nix will make sure these
derivations are built into packages before then we have a
bunch of metadata where this package is going to get installed, what system it's
for, name of the package. Then we have this package, which again is
immutable. So if we wanted a different version of this package,
let's say environment variable changes,
it would be at a different path. And you can think of the
path as a unique identifier, as far as Nix is concerned, of a package.
Then there's some sim linking done later, which will determine which
binary we end up using. Even if you think they're basically
the same. Then within Nixdor we have everything we need. We have the binary,
we have Sharego and has a bunch of other stuff, which is quite nice because
our packages are immutable and we
can kind of pre compute them. So Nix
has a bunch of servers available to it
that are kind of pre building these binaries and packages.
And so what that means if a derivation says we need to build a package
at this path, we can check if that package exists in our path. If it
doesn't. We can go fetch from various different caches. In this
case I'm fetching from the official Nixos cache.
And the cool thing about that is often because these are being pre built and
you can pre build them yourself as well if you want, and you can pre
build your own packages if you wanted. It just means we just have to
fetch them and download them. We don't actually have to build them ourselves on
our machine, which is really cool. So often we're just downloading stuff we're
not actually building because lots of these packages are pre built.
Another advantage that this kind of approach gives us
is that the dependency tree is explicit
and we can see what go depends on and then what those tools
depend on. So like it depends on bash, and then bash depends
on glib C. And every anyone with this exact same
unique identifiers and you know, starts with k, seven, ch,
et cetera, et cetera will have the exact same dependencies
as us if we print out this dependency tree
and nextore basically becomes this kind of graph database of our
dependencies. One other thing that doesn't matter so
much for dev,
specifically for dev environments, but it's kind of useful to know because of this approach
of we're not updating stuff in place. We can kind of have this concept of
generations and profiles and we can roll back to earlier versions of generations.
We can also then have atomic updates if we want.
Sorry, we do have atomic updates, and that basically means that
if something fails during the update we
can go back to an older version. Or we don't even end up updating
the NICs profile at all because these binaries just end up getting
mapped to stuff in Nick store. And yeah, unless we garbage
collecting cleanup stuff it's kind of going to be there.
One other thing to note is you'll notice that we use an epoch time of
one, that is 1 January 1970. The reason for that
is because the timestamp date time can be a form of non determinism,
because sometimes that gets injected into the binary. What we want to make sure
is that we build the exact same binary. Otherwise every time you built
a package you get a different binary and so wouldn't, you know,
we don't want that. So we set this to 1970 and we make
it a deterministic timestamp. Then in the Nix world this
is what bincat looks like, not user bincat,
but in NICs store. Just to kind of
summarize, NICs derivations allow us to have immutable packages, require us
to make our dependencies explicit. One thing to kind of note is if
a package is not nix packages, you will likely have to package it yourself.
But because Nix packages has 80,000 other packages, often or
always, I found if I need to package something, it's just that one thing.
The dependencies it needs is almost always in nix packages
itself. The next bit we're going to cover is nix flakes.
So how does Nix flakes relate to stuff?
So nixflakes basically take state on our system and
kind of put them into code in the sense that we have these things called
nix channels which would refer to what version
of nics packages were pointing to. And what
we do is we just take that and make that kind of more explicit and
we'll see exactly what that looks like. With this flake lock file,
we lock our stuff to specific revisions, and that means other, because we can put
that lock file in code, other developers can then point to the same inputs.
We can also use other git repositories, non nix related,
and manage them using our flake nix file if we want.
They also define some basic structure, because now our flake Nix file becomes the entry
point for our Nix configuration. So you know, that's kind of the first file you
can go to to look for that Nix configuration. So as we said before,
we have a bunch of inputs, a bunch of outputs, the main input being
nix packages, or the default input we get when we
initialize our flake. Then we
also generate this flake lock file, which has this kind of
concept. So this is the lock for the next packages input.
We have this null hash, which is just a hash of the contents
of that input. Less important because
it's a git repository, but we could have non git related inputs.
In this case, we also have a revision. So anyone using this flake
Nixonflake lock file combination will be locked to this
revision until we update that lock file. And that means if we're using this
in CI or other developers using this will be point to the same version of
Nix packages, which is really cool. We can also use like
say a GitHub action or CI to update this flake lock file for
us if we want to think a bit like dependable style
stuff, if you want to do that. To kind of summarize nics, flakes improve reproducibility
across our system by locking our dependencies. They provide a more standard
way to configure our system. But do note they are an experimental feature,
nics, and they could break in terms of
like they could be breaking changes, so just keep that in mind.
But I think they're great and I use them in all my projects and my
own Nix configuration. So kind of one of the
final topics we're going to touch on is CI. We've spoken about we want consistent
environments between what's running locally and what's running in CI. How can we do
that? And definitely we want to kind of leverage Nix's cacheability
and sharing dependencies. GitHub Actions has
some great stuff, especially from I think determinant systems for
leveraging caches and various things to speed up your
pipeline. I use GitLab CI and I found this great project by
this user called Cynerd and it has this
next job. And what it does by default is leverages the GitLab
cache, but we could use a cache from an SSh machine as
well, or ssh to a machine and
copy from there. But essentially what it does is before and after the job,
it will just copy from the cache to the next door of the
job and then from the next door to the cache. So it shares dependencies
between different stages and different jobs. So in our GitLab Ci file
we include this GitLab Ci file, the GitLab project
we just saw. Then I have a stage called pre
that extends this nix job and we just do nix develop to install
our dependencies. Then at future stage dependencies
have been cached, they'll be copied over to this job and we can do nix
develop C Go Lang ci lint run, whereas normally we'd be in that NaICs
development environment and we could just do Golang Cilantro and we should be running
the exact same versions because of the nature of Flake Nix
and the various things we've talked about with Nix. So that's really cool.
And this is kind of what it looks like, eleven and a half minutes total
runtime. I think before I did this change it was 22 minutes,
rather anecdotal, but something to note. Kind of just taking
a look at Ci logs again, we can see it's copying from that
cache, which is really nice as well.
And this was definitely me with Nix when
I first heard about it, I didn't really get it,
and then eventually it clicked and I think it's really, really cool. And hopefully you
guys either think that at the end of this talk, which means I've done an
amazing job. Unlikely, but I'd recommend just giving it
a go and seeing what you think. But before we close out this talk,
I'm sure some of you have been thinking, or maybe even screaming at your
computer, why not Docker? And I definitely was confused, like how does this
relate to Docker? So remember, we're talking specifically
for Docker in terms of like as a development environment.
I think Docker is still great for building and packaging and
deploying our stuff, especially on the cloud. And specifically what
I'm talking about is Docker files to build docker images. And one of the problems
Docker has, I think by being imperative, you'll often hear the term it's repeatable,
not reproducible, often because of the package managers we're
using. But imagine you have a Docker file. If two people try and build it
like six months apart, you're probably going to get a different docker image and therefore
it's not reproducible. One other problem I have with, say,
Docker dev containers, which versus code plugin,
which I think now works with jetbrains as well to try and
make it easier to develop within a container, is what if I
have specific tooling that I want, I now need to make that available, like say
fzf z oxide. I have a specific shell,
I have to make that available in the Docker image, and I'm potentially bloating that
Docker image as well. I did find a way to personalize it. You can kind
of run this startup script, but that was a bit slow and
a bit more cumbersome than I found comparing it with nics dev
containers. Sorry, Nick's dev shells. So just something to note there.
Give both a go, see which one you prefer. But I think I found nics
a lot easier. A few things you can look at in your own time to
further use nics even more like nics. All the things
here's a link to my slides. I'm going to have a bunch of other links,
just go through them in your own time. Just a bunch
of lots of literature and articles and YouTube
videos. I want to give a shout out to Vimjoyer specifically.
They do really great videos on YouTube. Highly recommend checking them out.
And just a thanks to everyone who gave me feedback on this talk and improved
a lot better than it was, I think version one. And thanks
to comp 42 for giving me the chance to talk about
this. I really like talking
about Nix and want to share it with other people to see let them know
what they can do. And thanks to you of course,
for seeing through this talk and listening to me ramble on
about NyX. Hopefully you found that useful and
you'll give Nick's a try. Thank you very much. Have a lovely day
and enjoy the rest of the conference.