This is my revised version of the instructions here: https://christine.website/blog/how-i-start-nix-2020-03-08
Christine's workflow works with any Nix install, whereas these instructions assume we're running on NixOS, using the power of Nix to maintain system state.
Use direnv, lorri, and niv to craft a shell environment optimized for Nix workflows, on a per-directory basis.
- Direnv maintains a project-specific shell environment, triggered by entering the project directory.
- Lorri integrates direnv with Nix workflows, maintaining some state to avoid costly unnecessary rebuilds.
- Niv helps manage dependencies in the Nix realm.
Use NixOS packages for direnv, lorri, and niv, installed at the
system level.
Edit /etc/nixos/configuration.nix
:
# /etc/nixos/configuration.nix
# ...
environment.systemPackages = with pkgs; [
direnv
niv
];
services.lorri.enable = true;
# ...
The lorri daemon should be running after nixos-rebuild switch
, but
there is currently a known issue where systemctl start lorri
may
be needed after the install.
Add the direnv shell hook, e.g. in ~/.bashrc
for bash shells:
eval "$(direnv hook bash)"`
Create a project directory and do some initialization:
$ mkdir -p hello
$ cd hello
$ lorri init # creates shell.nix and .envrc
$ niv init # creates nix/sources.json and nix/sources.nix
Direnv works via the presence of .envrc
, as created by lorri init
.
Changing the current dir to a project dir triggers direnv behavior.
However, direnv is blocked from operating unless it is specifically allowed.
You will see a message like:
$ cd .
direnv: error /path/to/.envrc is blocked. Run `direnv allow` to approve its content
Run direnv allow
.
Now direnv should give positive output when changing to a project dir.
Add the hello
package (from nixpkgs) to our project environment by
editing the shell.nix
created by lorri init
:
# shell.nix
let
sources = import ./nix/sources.nix;
pkgs = import sources.nixpkgs {};
in
pkgs.mkShell {
buildInputs = [
pkgs.hello
];
}
Here, we pin the source packages (specified in sources.nix
and sources.json
), rather than depending on the external state of nixpkgs.
With lorri running as a service, you should now be able to run hello
:
$ hello
Hello, world!
Add HELLO
to shell.nix
:
# shell.nix
let
sources = import ./nix/sources.nix;
pkgs = import sources.nixpkgs {};
in
pkgs.mkShell {
buildInputs = [
pkgs.hello
];
# Environment variables
HELLO="world";
}
Whenever direnv notices changes, lorri will rebuild what is necessary in the background. Note that this may take some time before your changes are apparent. You will see some direnv output coming from the lorri service when the environment is updated.
$ echo $HELLO
world
OK, now we have direnv, lorri, and niv working for us.
Let's make a demo project using Rust. Since we are managing our dependencies with niv, make niv aware of Rust packages directly from mozilla (via github):
$ niv add mozilla/nixpkgs-mozilla`
Create nix/rust.nix
:
# nix/rust.nix
{ sources ? import ./sources.nix }:
let
pkgs =
import sources.nixpkgs {
overlays = [ (import sources.nixpkgs-mozilla) ];
};
channel = "nightly";
date = "2020-06-10";
targets = [ ];
chan = pkgs.rustChannelOfTargets channel date targets;
in chan
Reference rust.nix
in shell.nix
so that lorri will pick up the changes:
# shell.nix
let
sources = import ./nix/sources.nix;
rust = import ./nix/rust.nix { inherit sources; };
pkgs = import sources.nixpkgs { };
in
pkgs.mkShell {
buildInputs = [
rust
];
}
lorri will start building in the background.
lorri shell
will bring this activity to the foreground if you want to see it.
It will take a few minutes before your new environment is ready.
$ rustc --version
rustc 1.46.0-nightly (feb3536eb 2020-06-09)
Confirmed! Create a new Rust project:
$ cargo init --vcs git .
This creates src/main.rs
and Cargo.toml
.
It will also initiate a git repo.
Build the default hello world program:
$ cargo build
This will build src/main.rs
, producing target/debug/$name_of_proj_dir
.
Try it:
$ target/debug/hello
Hello, world!
Let's serve some HTTP with Rocket.
Add Rocket as a dependency to Cargo.toml
:
# Cargo.toml
[dependencies]
rocket = "0.4.3"
$ cargo build
This will download all dependencies and precompile Rocket.
Now let's make a dumb HTTP server.
Edit src/main.rs
:
# src/main.rs
#![feature(proc_macro_hygiene, decl_macro)] // language features needed by Rocket
// Import the rocket macros
#[macro_use]
extern crate rocket;
// Create route / that returns "Hello, world!"
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
fn main() {
rocket::ignite().mount("/", routes![index]).launch();
}
$ cargo build
This will create a binary at target/debug/hello
$ target/debug/hello &
$ curl http://localhost:8000
Hello world!
$ fg # (control-c to kill the server)
$ niv add nmattia/naersk
Create hello.nix
:
# hello.nix
# import niv sources and the pinned nixpkgs
{ sources ? import ./nix/sources.nix, pkgs ? import sources.nixpkgs { }}:
let
# import rust compiler
rust = import ./nix/rust.nix { inherit sources; };
# configure naersk to use our pinned rust compiler
naersk = pkgs.callPackage sources.naersk {
rustc = rust;
cargo = rust;
};
# tell nix-build to ignore the `target` directory
src = builtins.filterSource
(path: type: type != "directory" || builtins.baseNameOf path != "target")
./.;
in naersk.buildPackage {
inherit src;
remapPathPrefix =
true; # remove nix store references for a smaller output package
}
Build it:
$ nix-build hello.nix
Run it:
$ result/bin/hello