nix-community / nix-direnv

A fast, persistent use_nix/use_flake implementation for direnv [maintainer=@Mic92 / @bbenne10]
MIT License
1.78k stars 101 forks source link

Support nix shell equivalent for flakes #469

Closed stessaris closed 7 months ago

stessaris commented 7 months ago

Since use flake function uses the nix print-dev-env command, the result is roughly equivalent to entering a subshell with nix develop command (minus the additional functions). See the output of

diff <(bash -c 'eval "$(nix print-dev-env nixpkgs#cowsay)"; direnv dump json') <(nix develop 'nixpkgs#cowsay' --command direnv dump json)

However, it's often the case that flakes are used via the nix shell command, and right now this is not supported by nix-direnv. The difference is substantial, because nix develop brings in a lot of tools that might not be desirable or even clashing with the actual environment; e.g., the barebone bash command that replaces the default one, or setting environment variables like SOURCE_DATE_EPOCH or CC. See the output of

diff <(bash -c 'eval "$(nix print-dev-env nixpkgs#cowsay)"; direnv dump json') <(nix shell 'nixpkgs#cowsay' --command direnv dump json)

This issue is related to #324, but it'd be useful to enable both develop and shell. Moreover, I think that the shell behaviour would be more intuitive, because direnv doesn't support the functions the user would expect in a nix develop subshell.

Unfortunately, it doesn't seem that nix is providing a print-env command, so I guess that the alternative would be to use something like the above nix shell 'nixpkgs#cowsay' --command direnv dump

bbenne10 commented 7 months ago

I am inclined to say that I don't want to implement another use flake variant based on this (very valid) critique. However, I want to let this simmer for a bit to make sure that's my actual stance.

Current justification for my stance is that bash code is inherently pretty bad to maintain and I don't want more code paths. A fork could address this easily, I suspect, and would not have to be burdened by (what I consider to be) legacy "use nix" implementation at all. (All I would ask is to rename the entry point away from use flake. There's enough difficulty in figuring out which implementation is in use now between the builtin direnv use flake and ours - adding another would be frustrating.)

stessaris commented 7 months ago

I just realised that a strong reason for changing the current behaviour is that the following .envrc file

# .envrc
if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
  source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
fi

use flake 'nixpkgs#cowsay'

doesn't provide a shell where cowsay is available:

~/temp/direnv_nix_test via ❄️  impure (cowsay-3.7.0-env)
❯ cowsay help
bash: command not found: cowsay
~
❯ nix shell nixpkgs#cowsay

~ via ❄️
↕️  2 ❯ cowsay help
 ______
< help >
 ------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
bbenne10 commented 7 months ago

Our focus has been on local flakes with tailored mkShell invocations intended to provide a meaningful dev shell. The current behavior is certainly surprising, but not out of line with what we are trying to show off. You ended up in the devshell for cowsay and you could resolve the problem with a local flake that adds cowsay to your devShell (and an update to your .envrc).

I want to continue to make clear that I see the critique, but I'm not sure what to do.

Is your goal to avoid a local flake?

stessaris commented 7 months ago

I understand the reasons for the current behaviour of use flake, but since the use of a remote flake is supported, I think that it would be useful to support also the nix shell behaviour (with a different layout, e.g., use flakeshell).

My understanding is that, even by using a local flake I'd end up with a development shell that is bringing in the packages for nix development and not just the packages I need, as with nix shell.

Would be possible to use the current caching technique that makes nix-direnv better than the default layout with nix shell ... --command direnv dump bash instead of nix print-dev-env?

bbenne10 commented 7 months ago

Would be possible to use the current caching technique that makes nix-direnv better than the default layout with nix shell ... --command direnv dump bash instead of nix print-dev-env?

Not really. Your proposed approach breaks current usage expectations as shown below:

Given the following flake.nix:

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system:
    let pkgs = nixpkgs.legacyPackages.${system};
    in
    {
      packages = { };
      devShell = pkgs.mkShell {
        packages = with pkgs; [
          coreutils
          gawk
        ];
      };
    });
}

This is the behavior you see:

$ nix shell . --command direnv dump bash
error: flake 'path:.../testflake' does not provide attribute 'packages.x86_64-linux.default' or 'defaultPackage.x86_64-linux'

$ nix print-dev-env . >/dev/null; echo $? 
0

The thing you'd have to do here is to move the packages you want into a meta-derivation that creates a symlink farm and expose that as packages.x86_64-linux.default. This behavior is surprising to current users and arbitrarily different from what we have. Current understanding is that the devShell sets up this project to be workable. You put your dependencies in devShell.packages (or any of the equivalents for such) - this flies in the face of that understanding.

Frankly, creating the symlink farm is an extra step that I don't ever want to do and I'm not sure it is any better than the current approach in any meaningful way. Your proposed approach would also fail to set up paths required for libraries.

Lets use Ocaml development as an example. To expose an Ocaml library to a nix builder, you need to add ocamlPackages.dune3 (a build tool for ocaml), ocamlPackages.findlib (sort of pkgconfig for Ocaml) and your Ocaml library (we'll pretend it is ocamlPackages.re - a regex implementation - for this discussion) to the set of devShell inputs. findlib has a hook that sets OCAMLPATH to all of the ocamlPackages members paths, which allows dune to find re when you open Re in your code. nix shell only wires up PATH, so OCAMLPATH never gets set and suddenly your builds fail.

I think we're really talking about two related things here:

The first part is somewhat by design and the fact that you can use remote packages is - imo - an implementation detail. nix print-dev-env accepts the flake specifier that you provided as valid and so you can do it. I have never actually wanted to do this particular thing and I have yet to be convinced that it is actually useful for real-world scenarios. I am open to being proven wrong though.

The second point is somewhat unfortunate and we do work around the "worst" of them by caching their values and resetting them after sourcing the print-dev-env output. I'm not entirely sure "intentional" is the right word for this, but it does come with the implementation we have chosen.

If any of this is wrong, please do not hesitate to correct me. I have now put enough thought into this that I am a solid "no" without correction or further clarification that substantially changes my understanding of this scenario and request.

bbenne10 commented 7 months ago

My understanding is that, even by using a local flake I'd end up with a development shell that is bringing in the packages for nix development and not just the packages I need, as with nix shell.

This is only true if you are expecting to set up a remote flake as your source. If you simply do what is shown above (a local flake that uses nixpkgs provided packages as devshell inputs), you end up with exactly what you specified. You get to define your flake devShell, which is what we're relying on currently.

And I never addressed this from your first post:

Moreover, I think that the shell behaviour would be more intuitive, because direnv doesn't support the functions the user would expect in a nix develop subshell.

I don't understand this statement. The thing that does the subshell spawning is direnv, not nix develop or nix print-dev-env. Because of this, we still are in a subshell when invoked in either your proposed path or our current one, as direnv spawns the shell, calls nix-direnv (which sets up the new environment in the subshell), and then tears down the subshell while keeping track of the changed environment variables. We still can't export functions or aliases with this change.

stessaris commented 7 months ago

I don't understand this statement. The thing that does the subshell spawning is direnv, not nix develop or nix print-dev-env. Because of this, we still are in a subshell when invoked in either your proposed path or our current one, as direnv spawns the shell, calls nix-direnv (which sets up the new environment in the subshell), and then tears down the subshell while keeping track of the changed environment variables. We still can't export functions or aliases with this change.

Probably I wasn't clear enough; what I was trying to say is that use flake put you in an nix develop like environment, but without the buildPhase, etc. commands that you'd have in there. I know that the reason is that direnv doesn't track functions and aliases, so it's not a surprise.

Anyway, I think I can modify my remote flakes following your example in order to get the behaviour I'm looking for.