Determining the project root of a Nix-tooled project

I have a recurring problem that has taken two shapes now:

  1. In a simple Nix configuration for a project, I have repeatedly created a shell script called project-root in shell.nix that determines the project root by recursively walking backwards until shell.nix / .git occur. I think this is a recurring problem, e.g. Cargo solves it with cargo locate-project.

  2. When transitioning to dendritic flake-parts modules using import-tree, I’ll have e.g. ./nix/devshell/flake-module.nix (or just ./nix/dev-shell.nix) that might refer to some file at the project root:

    { inputs, ... }: {
      perSystem = { pkgs, ... }:
        let
          rust-toolchain = pkgs.rust-bin.fromRustupToolchainFile ../../rust-toolchain.toml;
        in
          ...
    }
    

And I can’t help but think that those relative paths are sensitive to refactoring, as well as a little ugly.

I can imagine a similar custom job of having a flake-parts module that exports a project-root output that other flake-parts modules can rely on. But I can’t help but wonder: Since I have this problem repeatedly, is there a standard way to do it with Nix (with or without flake-parts modules)?

The location, or even existence of a shell.nix, does not denote the root of a project by necessity. You can not even rely on flake.nix.

If you are sure that the git-root is good enough for you, just use git rev-parse --show-toplevel.

1 Like

If you restructure your project, you’re going to have to change the paths anyway. Just use the relative paths.

Since you are using flakes, "${self}/rust-toolchain.toml"?

2 Likes

And that will re-copy that file to the store, so don’t do that. (Not to mention it will cause rebuilds every time you change the flake if you use that path in a derivation.)

2 Likes

Yeah, I figured there was some reason why I never saw that used anywhere. :upside_down_face:

You can use (self + "/path/to/thing") but in many modules and in derivations that path will get silently stringified (leading back to said issues). The only place this really works is in imports, but that doesn’t seem to be what OP is using here.

1 Like

You’re right, those were just local assumptions. In one current case I’m hosting various projects in a subdirectory, but thinking about it, it’s probably more stable to use "$(git rev-parse --show-toplevel)/subdir" and let git figure out the traversal, since git is a common denominator.

Thanks a lot for chiming in, because I would have went with this (very aesthetic solution) and learned of the cost the hard way.

For any future readers: “silently stringified” means: even though it will resolve to an absolute path outside of the nix store, e.g. /home/sshine/Projects/foo/rust-toolchain.toml, when used directly in my flake, the moment this flake gets imported into some other context, it might end up in the Nix store anyway, resulting in the same copying of the flake.

1 Like