Trick to utilize binary cache in flake devShell

On CI I often need to execute tasks from inside flake’s devShell – unfortunately not everything is nixifiable like some node monorepos or some cargo subcommands which require compilation of crate with some compile-time dependencies. I’ve not found any obvious way to populate binary cache for devShell dependencies. Even if I replace mkShell by stdenv.mkDerivation, the derivation does not contain it’s buildInputs so nix copy doesn’t copy them and the next time CI runs on preemptible instance it wastes tens of minutes on building buildInputs again.

But I’ve found hacky way to guarantee that nix copy --to 'http://some-nix-store' .#devShell.x86_64-linux copies all devShell dependencies. I’m sharing it here hoping that there is a less hacky way. If not, I hope it hepls someone who also use spot/preemptible instances which run flake devShell with slow-to-build dependencies.

{
  inputs = {
    # your inputs
  };
  outputs = { utils, ... }: utils.lib.eachDefaultSystem (system:
    {
        packages = {
            # packages
        };
        devShell = pkgs.stdenv.mkDerivation rec {
            name = "dev-shell";
            src = ./.;
            phases = "installPhase";
              installPhase =
                let keep =
                  writeScript "__keep" ''
                    echo ${stdenv.lib.makeBinPath buildInputs}
                  ''; in
                ''
                  mkdir -p $out/bin
                  cd $out/bin
                  cp ${keep} __keep
                '';

            # usual devShell setup goes here..
        };
    };
  );
}

I replace mkShell by mkDerivation to make nix copy think that devShell is buildable and create __keep script which holds references to all buildInputs to convince nix that buildInputs are needed by __keep scripts. Such way commands like nix copy --print-build-logs --to 'file:///tmp/cache' .#devShell.x86_64-darwin copy all devShell dependencies.

The cachix GHA keeps track of new pathes in the store before and after actual build steps and only uploads the new ones.

The gitlab example shows how to achieve similar behaviour.

Perhaps you can use this technique to get a list of pathes to upload in your situation?

1 Like

On reddit I was adviced to use inputDerivation.
Just tried to add dev-shell.inputDerivation to packages and run nix copy --to ... .#dev-shell where dev-shell is pkgs.mkShell { ... }; - works like a charm!

@tailrecursive do you mind providing an example of using inputDerivation? I’m facing the same issue x)

1 Like
{
  inputs = ...
  outputs = { ... }: utils.lib.eachDefaultSystem (system: rec {
    ...
    packages = {
      dev-shell = devShell.inputDerivation;
    };
    devShell = mkShell { ... };
  });
}

and then:

nix copy --to 'file:///tmp/cache' .#dev-shell
3 Likes