Nix for devs
This is collection of recipes focused on nix-shell
to make setting up project environments easy.
Pragmatism and low project impact are prioritized:
it's not a goal to change a nodejs project to use nix instead of npm,
but it is a goal to run npm projects without having to install any OS dependencies (including nodejs itself!).
nix-ops
and other super-cool nix tech are completely out of scope.
Every solution should work today. If something is broken, open an issue!. If there is a better solution that will be supported by nix soon, we wait until it's released before mentioning it here (but don't hesitate to open a tracking issue!).
Maybe in the best case, this document can be a kind of gateway to nix and nixos. I keep telling my developer friends about the cool things I do with nix, but I have to add huge warnings about how much time it's taken me to get to my current level of productivity. I'm annoyingly aware of the amount of trivia I'm carying in my head (though I still feel like a beginner!), and I constantly go back to old projects to look up what I did last time to fix problems. I frequently find myself running into hard-to-google issues. So, I'm finally writing it all down, for myself as much as for my friends :)
Work-in-progress
Current state: rough sketches and todos. Pull requests welcome!
nix-shell
TODO
Run stuff without installing anything
Stuff I used to do with docker. TODO :)
Node.js
shell.nix
with import <nixpkgs> {};
stdenv.mkDerivation {
name = "node";
buildInputs = [
nodejs
];
shellHook = ''
export PATH="$PWD/node_modules/.bin/:$PATH"
'';
}
The nodejs
build input sets up node
and npm
in the shell.
The shell hook adds the .bin/
folder to your PATH
, so you can install node command-line tools like grunt
with npm
without the -g
flag.
Finally, it automatically installs any dependencies found in package.json
, if one exists.
If you prefer to run npm install
manually inside the shell, just delete that line from shellHook
.
npm run <...>
shortcut
It's handy to have an alias run
for npm run
, which makes the following two commands equivalent:
$ npm run watch:test
$ run watch:test
You can add this to shellHook
:
alias run='npm run'
Newer nodejs
shell.nix
with import <nixpkgs> {};
stdenv.mkDerivation {
name = "node";
buildInputs = [
nodejs-8_x
];
shellHook = ''
export PATH="$PWD/node_modules/.bin/:$PATH"
'';
}
Node versions are published as -_x, so etg., nodejs-7_x
and nodejs-6_x
are also valid.
View available npm scripts
I use jq
to quickly get a summary of all scripts defined in a package's package.json
file:
shell.nix
with import <nixpkgs> {};
stdenv.mkDerivation {
name = "node";
buildInputs = [
jq
nodejs
];
shellHook = ''
export PATH="$PWD/node_modules/.bin/:$PATH"
alias scripts='jq ".scripts" package.json'
'';
}
This sets up an alias called scripts
. Example output:
$ scripts
{
"build": "babel --ignore __tests__,story.js -d lib/ src/",
"lint": "eslint *.js src/ storybook/",
"start": "start-storybook -p 9001 -c ./storybook/web",
"test": "jest --roots src --silent",
"test:watch": "jest --roots src --watch"
}
React native
shell.nix
with import <nixpkgs> {};
let
jdk = openjdk;
node = nodejs-8_x;
sdk = androidenv.androidsdk {
platformVersions = [ "23" ];
abiVersions = [ "x86" ];
useGoogleAPIs = true;
useExtraSupportLibs = false;
useGooglePlayServices = false;
};
unpatched-sdk =
let version = "3859397";
in stdenv.mkDerivation {
name = "unpatched-sdk";
src = fetchzip {
url = "https://dl.google.com/android/repository/sdk-tools-linux-${version}.zip";
sha256 = "03vh2w1i7sjgsh91bpw40ryhwmz46rv8b9mp7xzg89zs18021plr";
};
installPhase = ''
mkdir -p $out
cp -r * $out/
'';
dontPatchELF = true;
};
run-android = pkgs.buildFHSUserEnv {
name = "run-android";
targetPkgs = (pkgs: [
node
]);
profile = ''
export JAVA_HOME=${jdk.home}
export ANDROID_HOME=$PWD/.android
export PATH=$PWD/node_modules/.bin:$PATH
'';
runScript = "react-native run-android";
};
in
stdenv.mkDerivation {
name = "react-native-android";
nativeBuildInputs = [
run-android
];
buildInputs = [
coreutils
node
sdk
unpatched-sdk
];
shellHook = ''
export JAVA_HOME=${jdk}
export ANDROID_HOME=$PWD/.android/sdk
export PATH="$ANDROID_HOME/bin:$PWD/node_modules/.bin:$PATH"
if ! test -d .android ; then
echo doing hacky setup stuff:
echo "=> pull the sdk out of the nix store and into a writeable directory"
mkdir -p .android/sdk
cp -r ${unpatched-sdk}/* .android/sdk/
echo "=> don't track the sdk directory"
echo .android/ >> .gitignore
echo "=> get react-native-cli in here"
npm install --no-save react-native-cli
echo "=> set up react-native plugins... need an FHS env for... reasons."
cd .android/sdk
$PWD/bin/sdkmanager --update
echo "=> installing platform stuff (you'll need to accept a license in a second)..."
$PWD/bin/sdkmanager "platforms;android-23" "build-tools;23.0.1" "add-ons;addon-google_apis-google-23"
cd ../../
fi;
'';
}
This one is super-hacky and requires usage instructions :( also probably only works on linux :/
1. Enter the environment
$ nix-shell
[...lots of console spam the first time]
[nix-shell:]$
2. If you don't have an avd set up, make one
[nix-shell:]$ android create avd -t android-23 -b x86 -d "Nexus 5" -n nexus
That command seeme to create slightly screwy avds. I ran $ android avd
and then hit edit
and save
without any changes on mine, which seems to fix it :/
3. Start the JS server and emulator
Both commands block: either background then (add &
at the end) to run in the same terminal, or open a terminal for each
[nix-shell:]$ npm start
[nix-shell:]$ emulator -avd nexus
4. Run it!
[nix-shell:]$ run-android
Note that this run-android
is provided by shell.nix
, and wraps the call to react-native-cli
's react-native run-android
command in the FHS environment so that the build works.
Python
shell.nix
with import <nixpkgs> {};
with pkgs.python27Packages;
stdenv.mkDerivation {
name = "python";
buildInputs = [
pip
python27Full
virtualenv
];
shellHook = ''
SOURCE_DATE_EPOCH=$(date +%s) # so that we can use python wheels
YELLOW='\033[1;33m'
NC="$(printf '\033[0m')"
echo -e "''${YELLOW}Creating python environment...''${NC}"
virtualenv --no-setuptools venv > /dev/null
export PATH=$PWD/venv/bin:$PATH > /dev/null
pip install -r requirements.txt > /dev/null
'';
}
This expression sets up a python 2 environment, and installs any dependencies from requirements.txt
with pip
in a virtualenv
.
OS library dependencies
There is a LD_LIBRARY_PATH
attribute for nix packages that can help python dependencies find the libraries they may need to complete installation.
You generally need to format two pieces of information together: the path to the dependency in the nix store, and /lib
.
Usually this means you just need LD_LIBRARY_PATH=${<NAME>}/lib
.
However, sometimes nix packages divide up their outputs, with the /lib
folder and its contents at their own path in the nix store. Usually, the output with lib//
is called out
, and can be used like LD_LIBRARY_PATH=${<NAME>.out}/lib
.
If you need to put more than on dependency into LD_LIBRARY_PATH
, separate them with a colon :
, like for $PATH
.
geos
and gdal
shell.nix
with import <nixpkgs> {};
with pkgs.python27Packages;
stdenv.mkDerivation {
name = "python";
buildInputs = [
gdal
geos
pip
python27Full
virtualenv
];
LD_LIBRARY_PATH="${geos}/lib:${gdal}/lib";
shellHook = ''
SOURCE_DATE_EPOCH=$(date +%s) # so that we can use python wheels
YELLOW='\033[1;33m'
NC="$(printf '\033[0m')"
echo -e "''${YELLOW}Creating python environment...''${NC}"
virtualenv --no-setuptools venv > /dev/null
export PATH=$PWD/venv/bin:$PATH > /dev/null
pip install -r requirements.txt > /dev/null
'';
}
zlib
TODO
sqlite3
TODO
also, http://nixos.org/nixpkgs/manual/#using-python
Rust
TODO: mozilla nix overlay for nightly
shell.nix
with import <nixpkgs> {};
stdenv.mkDerivation {
name = "rust";
buildInputs = [
rustChannels.nightly.cargo
rustChannels.nightly.rust
];
}
OpenSSL
shell.nix
with import <nixpkgs> {};
stdenv.mkDerivation {
name = "rust";
buildInputs = [
openssl
rustChannels.nightly.cargo
rustChannels.nightly.rust
];
shellHook = ''
export OPENSSL_DIR="${openssl.dev}"
export OPENSSL_LIB_DIR="${openssl.out}/lib"
'';
}
diesel
shell.nix
with import <nixpkgs> {};
stdenv.mkDerivation {
name = "rust";
buildInputs = [
postgresql
rustChannels.nightly.cargo
rustChannels.nightly.rust
];
shellHook = ''
export OPENSSL_DIR="${openssl.dev}"
export OPENSSL_LIB_DIR="${openssl.out}/lib"
export PATH="$PWD/bin:$PATH"
export DATABASE_URL="postgres://postgres@localhost/db"
if ! type diesel > /dev/null 2> /dev/null; then
cargo install diesel_cli --no-default-features --features postgres --root $PWD
fi
diesel setup
'';
}
TODO: talk about running postgres in a nix-shell, and don't hard-code DATABASE_URL
in such an ugly way
cargo.toml
[dependencies.diesel]
version = "0.11"
features = ["postgres"]
[dependencies.diesel_codegen]
version = "0.11"
features = ["postgres"]
.gitignore
bin/