numtide / devshell

Per project developer environments
https://numtide.github.io/devshell/
MIT License
1.22k stars 87 forks source link

Arrays as environment variable in Hooks #265

Closed Mikilio closed 1 year ago

Mikilio commented 1 year ago

I have a package that would need a list of values as an array in an environment variable. Example

coolArr= ( "foo" "bar" "baz" )
export coolEnv=("$(coolArr[@]}")

It would be intuitive to me to declare those in TOML like

[[env]]
name = "coolEnv"
value = ["foo",  "bar", "baz" ]

But what I get is:

error:
       … while calling the 'derivationStrict' builtin

         at //builtin/derivation.nix:9:12: (source not available)

       … while evaluating derivation 'devshell'
         whose name attribute is located at /nix/store/8rz17fv8rxpzzb7qglhvsk3p034g5hmf-source/nix/mkNakedShell.nix:30:10

       … while evaluating attribute 'args' of derivation 'devshell'

         at /nix/store/8rz17fv8rxpzzb7qglhvsk3p034g5hmf-source/nix/mkNakedShell.nix:36:3:

           35|   # Bring in the dependencies on `nix-build`
           36|   args = [ "-ec" "${coreutils}/bin/ln -s ${profile} $out; exit 0" ];
             |   ^
           37|

       (stack trace truncated; use '--show-trace' to show the full trace)

       error: A definition for option `env."[definition 2-entry 1]".value' is not of type `null or string or signed integer or boolean or package'. Definition values:
       - In `/nix/store/dc3k6h8vl2pnnaxapqpyynq690bncgfg-source/devshell.toml':
           [
             "foo"
             "bar"
             "baz"
           ]

it would be nice if arrays were supported.

Mikilio commented 1 year ago

I also wanted to add that the only obvious workaround does not work:

{
  description = "virtual environments";

  inputs.devshell.url = "github:numtide/devshell";
  inputs.flake-utils.url = "github:numtide/flake-utils";
  inputs.sops-nix = {
    url = "github:Mic92/sops-nix";
    inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = { self, flake-utils, devshell, sops-nix, nixpkgs }:
    flake-utils.lib.eachDefaultSystem (system: {
      devShell =
        let
          pkgs = import nixpkgs {
            inherit system;

            overlays = [ devshell.overlays.default ];
          };

          extraConfig = { config, lib, pkgs, options, ... }:
          with lib;
          {
            config.devshell = {
              startup_env = mkForce (concatStringsSep "\n" [
                (concatStringsSep "\n" config.env)
                "test=( \"1\" \"2\" \"3\")\nexport TEST=(\"\${test[@]}\")"
              ]);
              packages = [
                sops-nix.packages.${system}.sops-import-keys-hook
              ];
            };
          };

        in pkgs.devshell.mkShell {
          imports = [
            (pkgs.devshell.importTOML ./devshell.toml)
            extraConfig
          ];
        };
    });
}

as echo $TEST prints nothing

zimbatm commented 1 year ago

On the system level, environment variables can only be expressed as <string>=<string>.

Bash uses some special encoding to add support for exporting functions and arrays, but it doesn't work unless both the parent and child processes are Bash. So if you have an intermediate tool like direnv, or use another type of shell, it will be lost.

I think I would encode the value as JSON and decode it in the host shell using jq.

Mikilio commented 1 year ago

Indeed I'm using both direnv and a different shell (zsh). I think I'll have to encode it in some way. Unfortunately it's very hard for me to find out how to attach shellHooks and where they will run. Having to decode the environment variable myself and then run the script kind of defeats the purpose of my devshell.

Aside from the topic about the workaround I still think that some form of list and attribute set as environment variables for shellHooks should be allowed because that was possible with mkShell and devshell is supposed to replace it.

Mikilio commented 1 year ago

So I did some research (nix is such a deep hole) and came upon this. It describes how derivation works

Every attribute is passed as an environment variable to the builder. Attribute values are translated to environment variables as follows:

  • Strings and numbers are just passed verbatim.

  • A path (e.g., ../foo/sources.tar) causes the referenced file to be copied to the store; its location in the store is put in the environment variable. The idea is that all sources should reside in the Nix store, since all inputs to a derivation should reside in the Nix store.

  • A derivation causes that derivation to be built prior to the present derivation; its default output path is put in the environment variable.

  • Lists of the previous types are also allowed. They are simply concatenated, separated by spaces.

  • true is passed as the string 1, false and null are passed as an empty string.

This is what I was looking for to provide variables to shellHooks

The derivation for the shell is built here. So to make things backwards compatible we need a way to insert variables there.

Personally I decided to just ditch the packaged shellHooks I want to use and instead just copy the scripts and paste them in config.devshell.startup.

Maybe better documentation for this would be nice, so people know about this without reading the source.

Also an option to set variables only for the hook would be nice. Example:

[[env]]
name = "foo"
hookOnly = "bar"

I wouldn't want to unset the variables in a script because it would create a discrepancy between what's in the TOML and what's real.

But for me the issue is resolved.

zimbatm commented 1 year ago

What you can do is execute the devshell directly. Unlike pkgs.mkShell, devshell.mkShell also has an entrypoint program that spawns bash if no arguments are passed. The only downside is that the shell is then bash.

Mikilio commented 1 year ago

The TL;DR is that, for what I wanted to do, the environment variables are not the issue but my inability to set nativeBuildInputs in the final derivation is. That is how most packages add shell hooks. They then exist in the same derivation. This means that shellHook is in scope. This means they can append to it. In devshells everything is kind of done by env.bash

I don't feel entitled to decide if and how this is going to be solved. For now I'll stick to my solution.

Mikilio commented 1 year ago

Since I decided to port any packages that add hooks to simple executables that I can add to startup, I will conclude this issue as "won't" fix. It is the better design choice anyway because it's less complex and easier to maintain in exchange for being just a bit more tedious to configure for the consumer.