What is the best dev workflow around nix-shell?

Hi! I’ve been a happy user of NixOS for quite some time, but I couldn’t quite polish my nix-shell workflow and I am wondering if you have any practical tips?

I think that my problems with nix-shell boil down to the fact that it serves two purposes:

  • it can be used to hack on derivations for nixos itself. I.e, if I want to hack on a nix package, I can use nix-shell to recreate the derivation environment and poke around.

  • it can be used to hack on the usual software, which is not package as a derivation. I.e, the foo project can have something like sudo apt-get libbar libbaz libqux in it’s README.md, and I can use nix-shell to install the said packages to make the usual make workflow work.

Now, I use nix-shell solely for the second use-case; I hack on projects which needs stuff like openssl, zlib and pkg-config to build. However, it seems to me that nix-shell was designed with the first use-case in mind. So, here’s the list of specific problems I face when using nix-shell:

  • it’s not really intuitive to me how to write default.nix. For Cargo, I have

    with import <nixpkgs> {}; {
      cargoEnv = stdenv.mkDerivation {
        name = "cargo";
        buildInputs = [ pkgconfig openssl cmake zlib libgit2 ];
        shellHook = ''
          export CFG_DISABLE_CROSS_TESTS=1
        '';
      }; 
    }
    

    It seems that name = "cargo" and cargoEnv bits don’t actually have any relevance, that is, it’s possible to use arbitrary names in their place. I generally just copy-paste this to over projects I am working on, because I can’t really write it from scratch.

  • today’s problem: when I run nix-shell --run zsh, my $SHELL changes to bash from zsh, which breaks other tools

  • problem a friend of mine faced: if you run nix-shell --run zsh, the gcc there is actually a wrapper which injects some nix-specific flags (for example, for hardening), and those flags might break compilation

  • there’s no way do create a “default, system-wide dev environment”, I need to launch nix-shell explicitly. This is not ideal when you want to create a second terminal tab, or launch an IDE from the launcher and not from the console.

So, does anyone know a better workflows for developing on NixOS? It seems like I am looking for something like “nix-shell for humans”…

12 Likes

direnv integrates the nix-shell quite well and allows to keep the existing shell (for example zsh):

Personally I use the following shell function to make it easier to bootstrap:

I have some environments for various projects in my dotfiles as well:

https://github.com/Mic92/dotfiles/tree/master/nixos/environments

direnv launches automatically when you enter the shell. It is not global though. Maybe you create your own wrapper for IDEs though (with direnv exec…)

4 Likes

I have had luck with this setup:
https://www.reddit.com/r/NixOS/comments/8tkllx/standard_project_structure/

I use this dirty hack to save my direnv “use nix” environments from being garbage collected:

savedrv () {
  if [ "$out" ]
  then
    drv="$(nix show-derivation "$out" | perl -ne 'if(/"(.*\.drv)"/){print$1;exit}')"
    if [ "$drv" ] && [ -e "$drv" ]
    then
      ln -fs "$drv" .drv
      ln -fs "$PWD/.drv" "/nix/var/nix/gcroots/per-user/$LOGNAME/$(basename "$PWD")"
    fi
  fi
}

Works fine with shell.nix, default.nix and also “use nix -p …”. Comments welcome, I don’t know a more elegant way to get the derivation from $out than by show-derivation.

shell.nix can be a little easier to write by hand with mkShell https://github.com/NixOS/nixpkgs/blob/37f50c15da53fbb017a1136049bb11dd8280eed6/doc/shell.section.md

6 Likes

Have a look at the direnv wiki, there are a few similar attempts: Nix · direnv/direnv Wiki · GitHub

3 Likes

My setup is usually like this:

shell.nix:

{ nixpkgs ? import (fetchGit {
  url = https://github.com/NixOS/nixpkgs-channels;
  ref = "nixpkgs-unstable";
}) {} }:
with nixpkgs;
mkShell {
  buildInputs = [ ... ];
  SOME_ENV_VAR = "foo";
  shellHook = ''
    # usually link in a nix-built vendor directory or other housekeeping
  '';
}

for the already mentioned direnv, my super sophisticated .envrc:

use nix
# optionally also some env vars, if they're supposed to be secret, otherwise they go into `shell.nix`.

My editor of choice is spacemacs, and I’m using emacs-direnv to automatically load my nix-shell for each buffer so all dependencies are found.

For caching the nix-shell I’m using the persistent cached shell variant of use nix, but that’s pretty optional, just speeds things up a lot.

Depending on who I’m working with, I might try to keep all other nix things in a nix/ directory in the project, but I think having a shell.nix and default.nix at top-level is just good practice and easier to work with. So in the subdir go things like a common pinned nixpkgs version, dockertools, overlays, gemset.nix, etc…

To be honest I’m still experimenting with what I find the most comfortable project layout to work with, but after a few years this has been pretty much the standard I build on.

6 Likes

I had read them, they did not handle the “nix-shell -p” case and I did not want to fiddle with nix-instantiate to duplicate what nix-shell already does.

You don’t have to add that { cargoEnv = foo; } bit – just foo is enough, i.e. the shell.nix file evaluates just to a mkDerivation call directly (with with and optionally let parts before it).

For me creating those shell.nix files just works; I always call nix-shell --pure -Q in that directory. I assume it depends on the particular projects and desired workflows.

1 Like

I actually sort faced the same issues and thus built myself a small and very opinionated utils: GitHub - kampka/nixify: Bootstrap nix-shell environments

It is basically a tool to bootstrap your nix-shell development environment with autoenv support and templates.

It may address some of your issues, but I have to admit that documentation is poor.
You should have a look at how the templates are set up in source code and see if that works for you.

It should at least cover your first two points in the list (maybe 4), but I only tested it with zsh and GitHub - zpm-zsh/autoenv: Autoenv for zsh but I’m guessing it’ll work with other shells / autoenvs / direnvs just as well (after bug fixing … )

The tool was built for myself, but I’m happy to look at any issues / pull requests.

Hope this helps :slight_smile:

This is really helpful. The part that I resonate with is

Roughly, I found a tension between the style nixpkgs expects and the style conducive to development

Yes, I don’t think there’s a super simple pathway here. I actually think it needs it’s own topic! Basically there’s no clear “this is what you add to your repo, and he’s how it ends up in a nixpkgs PR”

I don’t think there is a single example of projects that people have nix expression in their repository, and are also contained in nixpkgs. Why? Surely it would make sense. Do you have any examples?

I transformed it in an Org Mode gist file:

4 Likes

I like this setup, along with @Mic92’s personal use_nix function. My only suggestion would be to switch the perl command to a jq:

nix show-derivation $out | jq --raw-output 'keys | .[]'

I also updated my use_nix based on the latest version in the wiki. The only things I changed is to make it less likely for project names to collide and no dependency on jq.

I came to conclusion that nix-build (not nix-shell, because scripts of nix-shell interactive workflow tend to diverge with the derivation code, so a programmer has to maintain both) should have the following command-line options

  1. --nix-build-top=___ to specify the working dir of $NIX_BUILD_TOP
  2. --no-daemon for not sending job to nix-daemon as nixbld1 user will likely have no access to user’s project dir (--nix-build-top=___ might imply --no-daemon)
  3. --phases=___ to choose which phases to perform

Then (in any shell):

  1. Download and unpack sources:
    nix-build --nix-build-top=./ --phases=unpackPhase,patchPhase
  2. Alternative to ./configure (can be run many times with the same working dir)
    nix-build --nix-build-top=./ --phases=configurePhase
  3. Alternative to make (can be run many times with the same working dir reusing .o files)
    nix-build --nix-build-top=./ --phases=buildPhase
  4. Alternative to make test (can be run many times with the same working dir)
    nix-build --nix-build-top=./ --phases=checkPhase
  5. Alternative to make install (requires root privileges, dangerous as it impurely writes the result (which might be manually patched between phases) to nix store; overriding of $out to an user-writable directory can make it safe)
    nix-build --nix-build-top=./ --phases=installPhase,fixupPhase,installCheckPhase
1 Like

That would nice, nix-shell is good for setting project-specific environments but it’s not easy to test and debug the building process. But will the dependency to stdenv become a problem?

A simple approach to workflow which unfortunately requires a patch https://github.com/NixOS/nix/pull/3036

2 Likes

I don’t use NixOS, but I rely on Nix packages on macOS (Darwin), primarily for the isolated development environments use case. I’ve had pretty good luck using the approach of direnv along with a default.nix file in a project directory. It’s a bit slow when switching into the directory, but otherwise tolerable.

Problem: In some projects, I do not want to have Nix developer tools to be available. This happens when the default OS developer tools or environment are actually the ones that should be used to do some project-specific building, and Nix tools get in the way. What should I do if I do not want Nix-specific Clang or anything else to be prepended to the shell’s default path?

Sample default.nix for reference:

with import <nixpkgs> {};

let
  deps = [
    imagemagick
    mysql57
  ];
in
stdenv.mkDerivation {
  name = "my-project";
  src = null;
  buildInputs = deps;
  configurePhase = "";
  buildPhase = "";
}
1 Like

I haven’t ever tried this, but I’d be curious to know what happens if you use stdenvNoCC.mkDerivation instead.

1 Like

I’m no expert, but as far as I know nix-shell “just” install stuff in
the nix-store and fiddels around with your environment variables.
And if you are using direnv you can manipulate your environment
variables, right after you ran use-nix.

Either you do something like PATH=/usr/bin:$PATH (or where every your
compiler ist) or you fiddel around in you PATH variable and remove the
compiler paths, from the nix-store.
I guess you have to fix more than just the PATH variable.