NixOS: For developers
Contents
After my brief introduction to NixOS and the process of installing it, I
thought I’d dive into my experience with development on NixOS. I failed my
initial intention to come with a follow-up post in quick succession to the
introduction, but this one dragged on for several reasons. Finding structure and
trying to be concise when discussing something as “generic” as Nix turned out
to be quite challenging. Also, while still learning I found many of the things
I’d written in the past to be obsolete the next time I returned to writing.
Again, why Nix?
One of the main selling points of Nix and NixOS is its focus on
deterministic and reproducable builds, something all developers should strive
for in their projects. Many build and packaging tools have been putting
considerable effort into maintaining integrity, but normally this is localized
to libraries and dependencies in a single language1. It is still common for
programming languages to depend on system-wide compilers, interpreters and
shared libraries by default. No tool2 has pioneered and gone to the lengths
Nix has to ensure reproducible builds, where not just the project’s direct
dependencies are locked, but practically every external dependency up to and
including the global system3.
Nix does not do away with the build tools you would use for building and
deploying a project. Instead, it formalizes and encapsulates these tools in a
way that they too are locked to a given version. Nix-compatible projects are
still built using the regular tools for that project’s ecosystem, however the
versions of these tools will be deterministic.
Arrested development
Nix as a language is quite minimalistic, yet built on sound functional
programming concepts. The result is a language which shines when creating
reusable functions, which again allows it to express build recipes (derivations)
for a wide variety of projects. Nix does not directly compete with existing
build tools, but tries to complement and combine them. While the “core” of
the Nix ecosystem may be small, the community has accumulated many conventions
and utilities with the aim to reduce duplication and boilerplate. This effort is
mainly contained within the nixpkgs package collection. And while the modularity
and reusability is impressive, the information overload when dealing with
nixpkgs leads to a steep learning curve for newcomers.
I was of course expecting a learning curve when diving head first into NixOS,
but I must admit there were several times when I questioned my decision to
switch. Learning curves imply a drop in productivity as you spend time learning,
when you instead could have been producing. It is not easy to value ongoing
efforts like these which have yet to produce measurable results. I was steadily
learning more about Nix, yet I felt a growing desperation and despair because
despite my efforts, I had very little to show for it. Progress was slow.
In retrospect the goals I had to reach seem more well-defined than they were up front:
- Create project environments free of system dependencies.
- Change my development workflow to accommodate new restrictions and requirements.
- Manage my own software using
Nix.
But to begin with I was a bit stuck in NixOS, enjoying all the great software
built and maintained by others, yet having quite a bit of trouble getting
anywhere with my own projects.
Turtles all the way down
While learning Nix I’ve had many aha moments. One was when I finally
realized that Nix isn’t a package manager in the normal sense used for
distributing binary builds. The fact that it can fetch pre-built derivations is
merely a consequence of its design. Primarily it is a source distribution and
build tool. I gradually grokked this as I got further involved with writing nix
expressions. Documentation might already state it clearly, but here I’m talking
about reaching enlightenment at a deeper level. Perhaps similar to being told
something as a kid, but still having to experience it first hand in order to
“get it”.
The Nix expression for a derivation (a build unit) must state all of its
dependencies in order to build. This first and foremost includes its build
dependencies, but also its runtime dependencies. And here’s where it gets weird.
These dependencies are themselves merely other Nix expressions for other
derivations. More concretely, if project A uses tool B in its build process,
its obvious that B must be built before attempting to build A. In most
environment I’ve encountered this typically means to use “some package manager”™
to go fetch B, typically not caring how it is built or distributed. In Nix
though, the dependency A has on B is declared by simply referring to the
recipe for building B. This means Nix will simply go ahead and build B in
order to build A. And the same goes for all of the dependencies B might have
on other tools, even up to the C library and compiler.
Nobody wants to waste precious CPU cycles (and time) on rebuilding the “whole
world” whenever we wish to build a project, which is why most build tools
implement caching in one way or another. By tracking all inputs to every
derivation, Nix is able to implement a content-addressable cache which is
queried for pre-built derivations. This cache is also distributed, allowing
content to be fetched from trusted sources, primarily the NixOS cache at
cache.nixos.org. It is populated by build servers, ensuring that the most
common/popular derivations are always up to date. Locally this doubles as the
Nix store, in which all the artifacts built and used in the current system
or user profile reside.
In the end it’s the sole fact that by having deterministic builds and knowing
all the inputs involved, it’s possible to determine up-front which identifier
such an artifact will have in the Nix store. And if it’s already there,
there’s no point in building it again. Et voilà, you get binary package
distribution for “free”!4 However, if a dependency is neither in the local
Nix store, nor in one of the trusted binary caches, Nix simply builds the
nested dependencies on demand. They’re just layers upon layers of Nix
expressions after all. Simply mind-bending!
System integration
Virtual environments using nix-shell
Nix provides packages for many compilers, interpreters, libraries, and related
tools. Through Nix we get a uniform way of installing dependencies, as opposed
to using several domain-specific ones, each with their own unique behavior.
Nix also comes with nix-shell, which starts an interactive shell based on a
Nix expression, analogous to the way virtualenv work in Python. It either
builds or fetches cached builds of dependencies and adds them to the
Nix store, before making them accessible in a subshell through modified
environment variables and symlinks. The user or system environment remains
untouched, which means projects can pick and choose developer tools at their
leisure, without polluting the user’s environment or requiring root-access.
Following is a short example of my system where neither python3 nor node is
found in my $PATH, then using nix-shell to create an ad-hoc environment
where the Python 3.7 and Node.js 10.x interpreters are available:
❯ which python
python not found
~
❯ which node
node not found
~
❯ nix-shell -p python3 -p nodejs-10_x
[nix-shell:~]$ python --version
Python 3.7.3
[nix-shell:~]$ node --version
v10.15.3
Nix will download pre-built binaries of Python and Node.js on the first
run, then cache them in the Nix store until garbage collected. The -p
<package> flag to nix-shell is really convenient when you want to quickly try
something out, but for proper projects you’d want something more persistent and
declarative. Without the -p flag nix-shell will look for and evaluate Nix
expressions from files named shell.nix, or fall back to default.nix.
Invoking nix-shell in the same directory then loads the environment in a
subshell:
~/project $ nix-shell
[nix-shell:~/project]$ node --version
v10.15.3
[nix-shell:~/project]$ python --version
Python 3.7.3
[nix-shell:~/project]$
We can also instruct Nix to include Python packages in our environment:
with import <nixpkgs> {};
mkShell {
buildInputs = [
(python3.withPackages (ps: with ps; [
requests
]))
];
}Where invoking nix-shell gives us:
[nix-shell:~/tmp]$ python
Python 3.7.3 (default, Mar 25 2019, 20:59:09)
[GCC 7.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
>>> requests
<module 'requests' from
'/nix/store/j70h9pxi8sn1sq0cy65k5y3knhrmyqb7-python3-3.7.3-env/lib/python3.7/site-packages/requests/__init__.py'>
nixpkgs provides definitions for a large set of Python packages. However, if
a package is not available it’s fully possible to pull it down using pip. In
order to use pip from within the environment it has to be added as a
buildInput like any other. Furthermore, pip install must either be invoked
with the --user option to install dependencies under ~/.local/lib, or even
better using a virtualenv. There are also ways of instructing Nix about how
to fetch packages from package archives like pypi, typically through utilities
available in nixpkgs or using external tools called generators.
Automatic environment activation using direnv
If you, like me, jump around a lot between projects and environments, the
inconvenience of having to invoke nix-shell all the time quickly becomes
apparent. To automate this I rely on a tool called direnv, a companion for your
shell:
direnv is an extension for your shell. It augments existing shells with a new feature that can load and unload environment variables depending on the current directory.
Personally I integrate it with zsh, which means that whenever I cd into a
project directory tree, direnv will ensure that the shell is setup with the
same environment you would get by invoking nix-shell directly. Another
difference is that direnv does not invoke a new sub-shell for the new
environment, but mutates the current process’ environment. This provides a
seamless experience navigating between different projects, not having to worry
about loading the correct virtualenvs or switching between interpreter
versions using tools like nvm or pyenv:
~
❯ for prg in cabal ghc hlint; do which "$prg"; done
cabal not found
ghc not found
hlint not found
~
❯ cd ~/projects/nixon
direnv: loading .envrc
direnv: using nix
direnv: using cached derivation
direnv: eval .direnv/cache-.1926.5d6da42cf79
direnv: export +AR +AR_FOR_TARGET ... ~PATH
nixon on master [$!?]
❯ for prg in cabal ghc hlint; do which "$prg"; done
/nix/store/h433cxh423lrm3d3hb960l056xpdagkh-cabal-install-2.4.1.0/bin/cabal
/nix/store/zj821y9lddvn8wkh1wwk6c3j5z6hpjhh-ghc-8.6.5-with-packages/bin/ghc
/nix/store/1pwskgibynsvr5fjqbvkdbw616baw8c4-hlint-2.2.2/bin/hlint
For direnv to know when and how to load an environment, it checks for the
existence of .envrc files. These files are basic shell scripts evaluated using
bash and should output expressions for setting environment variables. In the
case of Nix I typically just invoke use_nix in these files. The first time
an .envrc file is found (and on changes) direnv will ask for permission to
evaluate its content. This is a security mechanism in order to avoid
accidentally invoking malicious code. Once allowed, direnv will continue to
load and unload the environment when entering and leaving project directory
trees.
~/tmp/project
❯ echo 'use_nix' > .envrc
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.
~/tmp/project
❯ direnv allow
direnv: loading .envrc
error: getting status of '/home/mmyrseth/tmp/project/default.nix': No such file or directory
direnv: eval .direnv/cache-.1926.5d6da42cf79
direnv: export ~PATH
The single Emacs process conundrum
Back in my vim days I’d typically launch the editor from within a virtualenv
in a shell, or at least starting in a project directory. Typically I’d have a
tmux session for each project, a single vim for that project in one pane,
and potentially several shells in other panes. When switching to Emacs I
quickly got used to using projectile for switching between projects in
combination with perspective to provide workspaces for each project. This keeps
buffer lists and window layouts tidy and organized while working on multiple
projects in a single Emacs process.
Emacs uses a single variable for the execution path (exec-path) and other
similar globals defining environmental values, which ultimately affect how
Emacs will spawn external commands like compilers, linters, repls, and so on.
Naturally Emacs won’t be able to launch these tools if they aren’t in the
$PATH, and so these globals have to change when switching between projects.
This can be done manually by invoking commands, or automatically by hooks
triggered when switching between buffers. I was already using plugins like
pyvenv to switch between virtualenvs in Python projects. Most node-related
plugins already support finding tools in npm bin.
I started off looking for solutions which would allow me to keep my “single
process Emacs”-based workflow. There are direnv plugins for Emacs which
loads the project environment on file/buffer changes in Emacs. Unfortunately,
after using emacs-direnv for while I came to realize it wasn’t the solution I
wanted. The main issue with the direnv plugin for Emacs is that environments
are loaded automatically, while this is typically what you want, I found that
switching between buffers Emacs would keep evaluating and updating the
environment. In the end this caused the editor to feel slow and unresponsive. A
deal-breaker!
Biting the bullet, I moved on to a workflow centered around having one Emacs
instance per project I was currently working on. I dropped my single long-lived
Emacs sessions in favor of multiple sessions, each running within the project
environment set up by nix-shell. It ended up with me firing up and shutting
down Emacs much more often than before, as well as having to find the correct
editor instance for a certain project. This quickly started to annoy me in the
same way using a slow direnv did. If only I could make the first approach
faster…
Turns out I wasn’t the only one looking for this and I eventually stumbled on an implementation of the use_nix function used by direnv. This provided a
significant performance increase by caching the result of evaluating
nix-shell. Another benefit of this function is that it also symlinks the
environment derivation into Nix’s gcroots. Don’t worry, this basically means
that the artifacts required by the development environment won’t be garbage
collected when cleaning out the Nix store using nix-collect-garbage.
Even more time passes, and I became aware of a new tool built by Target, called
lorri. It is basically a daemon you can run in the background, building all your
environments as their expressions or dependencies change, while also ensuring
they are not garbage collected. I have yet to start using lorri myself mostly
out of laziness, but I must say it looks very promising.
Defining development environments
Installing my own tools
In Nix it’s important to distinguish between software intended to be used as a
dependency, like libraries, compilers, and so on, and end-user software, which
can be command line tools and GUI applications. While libraries and developer
tools should only be available from within any given project depending on them,
end-user software should be accessible from a user environment. I do develop a
few end-user tools that make my life easier, and so I had to figure out how to
best install these projects into my user profile.
Both stack and npm, and many other package managers5, are able to
install software into a “global” location. The stack install and npm install
--global commands allow installing not just upstream packages, but also locally
from the same machine. Even though this was the way I installed my own software
on other operating systems, it was not the way I liked to do it on NixOS. In
my opinion it’s a smell when you have to invoke several different tools to not
only install software, but also figure out what you’ve already installed. Some
tools do not even track what they installed, forcing you to manually go
through and remove stuff from you ~/.local~.
Nix resolves these issues in one go, at the cost of having to figure out how
to create proper Nix expressions for Python, JavaScript, and Haskell
code bases. Luckily, nixpkgs has us covered, normally providing a single
function doing what you want. Some nixpkgs functions also wrap Nix
generators like callCabal2nix, saving you from having to run these tools
yourself. It took me a while to figure out it was callCabal2nix and
buildPythonApplication I wanted for most Haskell and Python projects,
respectively. I have yet to make an attempt at installing any of my JavaScript
tools on NixOS.
A quick note on generators
I’ve mentioned that Nix doesn’t stop you from using package managers like
pip and yarn from within a project environment. The downside is that Nix
has no knowledge of what these tools are doing, and so cannot ensure the same
guarantees as if it knew about the artifacts these tools create (or fetch). It
is possible to use these other tools to fetch or build the software we want,
then inform Nix about the artifacts, which is then able to add these to the
Nix store.
Since package managers normally operate based on existing dependency meta-data,
it’s possible to automate the process of listing out the dependencies,
performing the build steps for each, adding artifacts to the Nix store, and so
on. Tools that automatically generate Nix expressions from some input are
called generators. The output of these generators are Nix expressions which
can then be saved to file and evaluated by nix-build and nix-shell. In the
case of nixpkgs there are also wrapper functions around generators, which
saves you from having to use the generators themselves, One example of this is
callCabal2nix used for building Haskell packages.
Here’s a list of a few assorted generators for different project types:
- node2nix: Generate
Nixderivations to buildnpmpackages. - setup.nix: Generate
Nixderivations forPythonpackages. - cabal2nix: Generate
Nixderivations from a.cabalfile.
Pinning nixpkgs
The package repository nixpkgs is based on the concept of channels. Channels
are basically branches of development in the git repository moving the
contained Nix expressions forward by updating upstream versions, fixing bugs
and security issues, and provide new Nix utilities. Channels are also moving
targets. System users want to automatically receive security updates, new
application versions, and so on. Software developers on the other hand want to
control the upgrade of dependencies in a controlled manner.
The Nix way of locking down dependencies is to pin the nixpkgs versions. In
essence this is to use a version of nixpkgs from a specific commit, a
snapshot. This ensures that building the Nix derivation will always result in
the same output, regardless of future upstream changes to nixpkgs. Different
derivations may also use different versions of nixpkgs without that
necessarily becoming an issue. To upgrade one or more dependencies it is often
enough to just change the snapshot of nixpkgs to a newer version.
Haskell
Haskell projects are typically built using cabal. stack is another popular
tool, which manages package sets of GHC versions along with compatible
Haskell packages. Gabriel Gonzales’ writeup of Nix and Haskell in production
state that Nix is not a replacement for cabal, but rather a stack
replacement.
Nix has become quite popular in the Haskell community and it seems many
people choose it to build their projects. In a way similar to Stackage,
nixpkgs contains package sets build for different versions of ghc6.
There’s a section in the nixpkgs manual under “User’s Guide to the Haskell Infrastructure” providing some information on how to use Nix for Haskell.
I used stack for all Haskell development I’d been doing leading up to my
switch to NixOS, and so it felt natural to continue using stack under Nix.
stack even has native Nix support. However, since there’s quite a bit of
overlap what stack and Nix attempts to solve, I’ve since switched my
workflow over to Nix and just cabal. nixpkgs provide a callCabal2nix
function which in short suffices to setup a simple project. Following are a few
hobby projects which I’ve recently switched over to this model:
nixon - Nix-aware project environment launcher
Using either rofi or fzf, nixon selects projects from predefined directories
and launches nix-shell (or other commands) in the project’s environment. This
is very useful when projects have .nix files setting up shell environments in
which you want to spawn a terminal, an editor, run compilation commands, and so
on.
This project uses a single default.nix file which also works by creating a
shell environment with additional developer tools when run in nix-shell:
default.nix:
{
pkgs ? import ./nixpkgs.nix {},
haskellPackages ? pkgs.haskellPackages,
}:
let
gitignore = pkgs.nix-gitignore.gitignoreSourcePure [ ./.gitignore ];
in haskellPackages.mkDerivation {
pname = "nixon";
version = "0.1.0.0";
src = (gitignore ./.);
isLibrary = true;
isExecutable = true;
executableHaskellDepends = with haskellPackages; [
aeson
base
bytestring
containers
directory
foldl
haskeline
process
text
transformers
turtle
unix
unordered-containers
wordexp
];
executableSystemDepends = with pkgs; [
fzf
rofi
];
testDepends = with haskellPackages; [
hspec
];
license = pkgs.stdenv.lib.licenses.mit;
}shell.nix:
{
pkgs ? import ./nixpkgs.nix {},
haskellPackages ? pkgs.haskellPackages,
}:
let
drv = (import ./default.nix) {
inherit pkgs haskellPackages;
};
in haskellPackages.shellFor {
packages = _: [ drv ];
buildInputs = (with pkgs; [
cabal2nix
cabal-install
hlint
]) ++ (with haskellPackages; [
ghcid
]);
}In short, to define the derivation (drv) I’m using the Haskell
specialization of mkDerivation in haskellPackages.mkDerivation. It also
makes use of haskellPackages.shellFor to setup a shell environment used when
developing. This shell includes cabal2nix, cabal, hlint, and ghcid.
i3ws - Automatic workspace management in i3.
This project is interesting because the project was using stack in a monorepo
style layout before switching to Nix. This meant that I had to find a nice way
to have several packages under development integrating nicely in Nix. Luckily
somebody beat me to it, and I drew some inspiration from the “Nix + Haskell monorepo tutorial” post on the NixOS discourse, pointing to the
nix-haskell-monorepo GitHub repo.
The new-style commands of cabal supports multiple projects using a
cabal.project file. This file contains a listing of the
packages/subdirectories contained in the project, each with their own .cabal
file:
$ cat cabal.project
packages: foo
bar
baz
For a working example of this setup, see the GitHub repo7 for i3ws.
Python
We use Python extensively at work, and our most active codebase is a web
application with a Python backend and a JavaScript/TypeScript frontend.
It was this project I first tried to get working on my laptop after switching it
to NixOS. We use some automation scripts which call out to pip and yarn to
install dependencies.
This is not a trivial project, but still I find the shell.nix file I use to
setup the environment to not be very large. It is worth noting that we do not
build and deploy this project using Nix, and so the expression is only
setting up enough for me to successfully run our install, testing and packaging
scripts:
{
pkgs ? import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/19.09.tar.gz";
sha256 = "0mhqhq21y5vrr1f30qd2bvydv4bbbslvyzclhw0kdxmkgg3z4c92";
}) {},
}:
let
# Pin Pillow to v6.0.0
pillowOverride = ps: with ps; pillow.override {
buildPythonPackage = attrs: buildPythonPackage (attrs // rec {
pname = "Pillow";
version = "6.0.0";
src = fetchPypi {
inherit pname version;
sha256 = "809c0a2ce9032cbcd7b5313f71af4bdc5c8c771cb86eb7559afd954cab82ebb5";
};
});
};
venv = "./venv";
in self.mkShell {
buildInputs = with pkgs; [
binutils
gcc
gnumake
libffi.dev
libjpeg.dev
libxslt.dev
nodejs
openssl.dev
(python36.withPackages (ps: with ps; [
(pillowOverride ps)
pip
python-language-server
virtualenv
]))
squashfsTools
sshpass
yarn
zip
zlib.dev
];
shellHook = ''
# For using Python wheels
export SOURCE_DATE_EPOCH="$(date +%s)"
# https://github.com/NixOS/nixpkgs/issues/66366
export PYTHONEXECUTABLE=${venv}/bin/python
export PYTHONPATH=${python}/lib/python3.7/site-packages
if [ -d ${venv} ]; then
source ${venv}/bin/activate
fi
'';
};First of all, none of the other developers on the team use Nix8, which means I
have to add my Nix configuration without being too intrusive on the others. I
also want to make sure I don’t deviate too much from the rest, leading to issues
caused by differences in my environment. We also have several scripts and
workflows centered around some of these tools, like automating dependency
installation across multiple sub-projects, package introspection, and yarn
workspace symlinking, to name a few.
I could go on a digression as to how NixOS breaks the Filesystem Hierarchy Standard of Linux, but essentially it means that libraries and executables are
not found in standard locations. Pillow uses some hardcoded paths in its
setup.py which point to invalid locations on NixOS. That makes it hard to
install it using pip, and so it’s the only Python dependency installed from
nixpkgs. Overriding it to pin it to the version we are using, which ensures
pip is not going to try to install another version by itself. In the end this
works well, but I spent a lot of time trying to do this in several other ways.
In my quest to get Pillow working nicely in our project I had to dive through
the nixpkgs codebase. At which point I got more aware of all the helpers
functions in that repository for building projects of different shapes and
sizes. What buildPythonPackage does should be obvious from its name, but I
found that figuring out usage, differences, and even discovering of all these
different utilities within nixpkgs is not very easy. Much improvement could be
made in the Nix community on this front.
JavaScript & TypeScript
The Node.js packages in nixpkgs are mainly end user packages. Some few
nodejs libraries are present because they are dependencies of non-NPM packages.
The nixpkgs docs has a section on Node.js packages. The recommendation is to
use the node2nix generator directly on a project’s package.json file. Here’s
a short list of possible generators for Node.js packages:
For simpler setups I prefer to use Nix to only provide node, npm, and
yarn, then invoke these directly as it seems to work fine in most scenarios. I
haven’t had much reason for using node2nix yet, so I can’t say much about that
experience.
One thing I typically do in my JavaScript/TypeScript environments is to
include the javascript-typescript-langserver package, which is used by
lsp-mode in Emacs to provide IDE-like tools.
Ad-hoc environments
Sometimes you want access to certain language tools in order to test something.
While on other systems you typically have node or python installed somewhere
directly accessible on the shell, in NixOS this isn’t the case. Instead, by
adding a few expressions to the nixpkgs configuration file it’s easy to launch
shells with access to these tools.
Using nix-shell to run scripts
nix-shell also has support for being used in shebangs, making it ideal for
setting up ad-hoc environments used by simple scripts. The following example
instructs nix-shell to create a Haskell environment with GHC along with a
predefined package turtle.
#! /usr/bin/env nix-shell
#! nix-shell -p "haskellPackages.ghcWithPackages (ps: with ps; [turtle])"
#! nix-shell -i runghc
{-# LANGUAGE OverloadedStrings #-}
import Turtle
main :: IO ()
main = do
echo "Hello, World!"Pre-defined environments
Using Nix overlays we can also define environments which can be referenced in
nix-shell invocations to provide ad-hoc environments when testing out things.
Overlays are a way in nixpkgs to define new packages and overrides to existing
packages. It’s a powerful concept, but here we’re using it just to create our
own derivations:
Node.js
Define
env-nodeas an overlay in~/.config/nixpkgs/overlays.nix:let overlay = self: super: { nodeEnv = with self; buildEnv { name = "env-node"; paths = [ nodejs-10_x nodePackages_10_x.javascript-typescript-langserver yarn ]; }; }; in [overlay]Launching the environment:
$ nix-shell -p nodeEnv [nix-shell:~]$ node --version v10.15.3 [nix-shell:~]$ npm --version 6.4.1 [nix-shell:~]$ yarn --version 1.13.0 [nix-shell:~]$Python
Similarly to
nodeEnv, define an overlay in~/.config/nixpkgs/overlays.nix:let overlay = self: super: { pythonEnv = with self; buildEnv { name = "env-python"; paths = [ (python3.withPackages (ps: with ps; [ pip virtualenv ])) ]; }; }; in [overlay]Launching the environment (here we’re also adding
ipythonmanually):❯ nix-shell -p pythonEnv -p python3Packages.ipython [nix-shell:~]$ python --version Python 3.7.3 [nix-shell:~]$ ipython Python 3.7.3 (default, Mar 25 2019, 20:59:09) Type 'copyright', 'credits' or 'license' for more information IPython 7.2.0 -- An enhanced Interactive Python. Type '?' for help. In [1]:
Summary
In hindsight I should have known attempting to write a post like this would be
opening a can of worms. Well, my setup and configurations did end up changing
parallel to writing this post, and so time dragged on. Also, nailing the scope
of something as broad as this is not easy and I feel I’ve only managed to scrape
the surface of describing development on NixOS (or using just Nix, the package
manager).
Development based around Nix can be a very powerful thing indeed, but don’t
expect it to be a walk in the park. I see the lack of proper documentation and
poor discoverability as one of the main hurdles Nix and nixpkgs has to
overcome. Again, nixpkgs is a huge collection of Nix expressions for
applications, libraries, and tools ranging across many different programming
languages and ecosystems. I think because of both the size of the repository and
the diversity of its content, there has evolved certain idioms within
different areas of the nixpkgs repo. This makes finding the correct functions
and utilities to use for building a certain project harder for newcomers (and
perhaps even seasoned Nix-ers).
Despite some of these areas of improvement I’m conviced that the concepts
pioneered by Nix is here to stay. I have yet to find better alternatives for
managing the complexity of building and distributing software.
Finally I’d like to thank Bjørnar Snoksrud @snoksrud for proofreading.
Footnotes
To provide an example of this
npmintroducednpm-shrinkwrap.jsonand laterpackage-lock.jsonfiles to lock down the entire dependency tree of a project.↩︎No tool I’m aware of, that is.↩︎
Nix has plenty shortcomings though, and there are definitely ways to mess up a reproducible build by relying on e.g. the file system or hardcoded paths.↩︎
By “free” I’m not trying to undermine the amount of effort and hard work of developers, as well as the cost and computing power required to provide a much appreciated, fully-populated binary cache.↩︎
stackdoesn’t market itself as a package manager, but that’s besides the point.↩︎See: https://github.com/NixOS/nixpkgs/blob/master/pkgs/top-level/haskell-packages.nix↩︎
Linked to the commit at the time of writing.
mastermight move away from this design at a later time.↩︎I’m hoping I’ll be able to convince them how useful
Nixis.↩︎