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
Nix
derivations to buildnpm
packages. - setup.nix: Generate
Nix
derivations forPython
packages. - cabal2nix: Generate
Nix
derivations from a.cabal
file.
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 ghc
6.
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 Nix
8, 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 ()
= do
main "Hello, World!" echo
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-node
as an overlay in~/.config/nixpkgs/overlays.nix
:let overlay = self: super: { nodeEnv = with self; buildEnv { name = "env-node"; paths = [ -10_x nodejs 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
ipython
manually):❯ 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
npm
introducednpm-shrinkwrap.json
and laterpackage-lock.json
files 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.↩︎
stack
doesn’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.
master
might move away from this design at a later time.↩︎I’m hoping I’ll be able to convince them how useful
Nix
is.↩︎