so for fun I'm working through writing an interpreter in go (but in rust). one thing I do for all my projects is put in a lot of effort to try to make the developer environment reproducible. this is actually a big reason why I use nix in the first place
in order to make the development environment reproducible, I need to have all the project dependencies available. this means having the rust compiler and rust toolchain installed, as well as anything incidental that I use over the course of development, such as the rust language server (the latter is often a nice to have, but I'd be lost without rust-analyzer)
nix lets me do all of this, and easily. enter nix flakes
nix flakes are... kind of like packages for nix/NixOS. kind of. flakes specify packages, which can be applications, as well as apps, which are executables specifically. flakes are supposed to be purely functional: they take a set of inputs and specify a set of outputs
the nix toolchain interacts with flakes in a special way. if you run nix build off of a flake, nix will build the package specified in your flake. if you run nix run, nix will build and execute the specified app in your flake. you can also specify devShells, which drop you into a shell environment for development when you run nix develop.
my flake for this project looks like this:
{
description = "monkey";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
projectName = "monkey";
in rec {
packages.${projectName} = pkgs.rustPlatform.buildRustPackage {
pname = projectName;
version = "0.1.0";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
};
packages.default = self.packages.${system}.${projectName};
apps.${projectName} =
flake-utils.lib.mkApp { drv = packages.${projectName}; };
apps.default = self.apps.${system}.${projectName};
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
cargo
rustc
rustfmt
clippy
rust-analyzer
rustup
];
};
});
}
in writing an interpreter we implement the monkey programming language, a toy language, hence why the word "monkey" appears above
the nix language is like if JSON had haskell-like functions. so outputs is a function that takes a set of inputs and returns a few different properties.
line 14 specifies the "monkey" derivation. a derivation in nix is a build action, and is the basic unit of the nix package manager. note that I'm not using the built-in derivation function, but a helper for making derivations from rust projects specifically
line 21 says that the "monkey" package is the default package for this flake. line 26 says the same thing regarding apps
line 23 specifies the "monkey" app, which is my executable. when I run nix run, I'll actually be running my main monkey executable, which will be the monkey CLI
line 28 and below specifies the default development environment. maybe the most interesting part. when working on this project, we want cargo, rustc, etc. all available in our environment
the "package" and "app" properties would be useful to someone who wanted to invoke the monkey CLI without globally installing it. if I published this flake publicly someone could simply run my monkey CLI with
nix run github:my-github-user-name/monkey
on all the systems I work on, I use direnv to automatically provision this environment when I enter this directory. I do not have any of these utilities installed globally. I use the corresponding emacs package too to change my emacs environment to notice that this environment is available, allowing things like lsp to use rust-analzyer, again all while not having it globally installed
the whole outputs section is wrapped in a function call to something called flake-utils, which makes this flake cross-platform. otherwise I'd have to specify the default app/package/devShell for each platform I want to run on
nix flakes are pretty cool honestly and unlike any other tool I've used. it saves me a ton of effort and I don't have to worry if each system is configured specifically to run this project. though to be fair, this is less of a problem for a language like rust and would be a bigger deal for a python or ruby program