Creating a re-usable function in nix and consuming it in nix flake

I create a nix function that runs a shell script. I’m using flake, and it works fine if I define the function directly in my flake.nix. I want to extract the function into a separate nix file because I don’t like defining everything in flake.nix. But I cannot figure out how to do this seemingly simple task.

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    flake-utils = {
      url = "github:numtide/flake-utils";
    };
  };

  outputs = { nixpkgs, home-manager, flake-utils, ... }:
    let
      settings = import ./settings.nix;
    in
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };

        createScript = { name, dependencies ? [ ] }: pkgs.symlinkJoin
          {
            inherit name;
            paths = [
              ((pkgs.writeScriptBin name (builtins.readFile ./scripts/${name}.sh)).overrideAttrs (old: {
                buildCommand = "${old.buildCommand}\n patchShebangs $out";
              }))
            ] ++ dependencies;
            buildInputs = [
              pkgs.makeWrapper
            ];
            postBuild = "wrapProgram $out/bin/${name} --prefix PATH : $out/bin";
          };
      in
      {
        packages = {
          homeConfigurations.${settings.username} = home-manager.lib.homeManagerConfiguration {
            inherit pkgs;

            modules = [ ./home.nix ];

            extraSpecialArgs = {
              inherit settings;
            };
          };

          install = createScript {
            name = "install";
            dependencies = with pkgs; [ git jq ];
          };

          switch = createScript {
            name = "switch";
          };
        };
      });
}

I want to define createScript in a separate file, and import it here.

I tried defining a nix module, but it did not work out well. Please help.

This might do the trick for you. notice that I removed the pkgs attribute from the createScript function

#create.nix
{
  symlinkJoin,
  writeScriptBin,
  makeWrapper,
}: {
  createScript = {
    name,
    dependencies ? [],
  }:
    symlinkJoin
    {
      inherit name;
      paths =
        [
          ((writeScriptBin name (builtins.readFile ./scripts/${name}.sh)).overrideAttrs (old: {
            buildCommand = "${old.buildCommand}\n patchShebangs $out";
          }))
        ]
        ++ dependencies;
      buildInputs = [
        makeWrapper
      ];
      postBuild = "wrapProgram $out/bin/${name} --prefix PATH : $out/bin";
    };
}
#flake.nix
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    flake-utils = {
      url = "github:numtide/flake-utils";
    };
  };

  outputs = {
    nixpkgs,
    home-manager,
    flake-utils,
    ...
  }: let
    settings = import ./settings.nix;
  in
    flake-utils.lib.eachDefaultSystem (system: let
      pkgs = import nixpkgs {inherit system;};
      createScript = pkgs.callPackage ./create.nix {}; # pkgs.callPackge info: https://nixos.org/guides/nix-pills/callpackage-design-pattern.html
    in {

      packages = {
        homeConfigurations.${settings.username} = home-manager.lib.homeManagerConfiguration {
          inherit pkgs;
          modules = [./home.nix];
          extraSpecialArgs = {
            inherit settings;
          };
        };

        install = createScript {
          name = "install";
          dependencies = with pkgs; [git jq];
        };

        switch = createScript {
          name = "switch";
        };
      };
    });
}

@sebohe Thanks, but it did not work for me.

I got this error.

error: attempt to call something which is not a function but a set

       at /nix/store/9nan3naknbb1fv1y64rx6v33449zmzby-source/flake.nix:43:20:

           42|
           43|           switch = createScript {
             |                    ^
           44|             name = "switch";

Or should I try to achieve this via nix flake? I’m not sure what’s the recommend approach.

My bad.

Replace the above ^ with:

inherit (pkgs.callPackage ./create.nix {};) createScript;
# or
createScript =  (pkgs.callPackage ./create.nix {};).createScript;

Both lines are the same. I don’t have the other files such as ./scripts. I created a temp script to test what you are doing

# temporary script name that echos "sup" and does not use `readFile`.
# I did this since I don't have the contents of `./scripts`
((writeScriptBin name ''echo sup'').overrideAttrs (old: {
    buildCommand = "${old.buildCommand}\n patchShebangs $out";
}))

❯ nix repl .
Welcome to Nix 2.18.1. Type :? for help.

Loading installable 'git+file:///Users/sebas/repos/help#'...
Added 1 variables.
nix-repl> :lf . # Load the nix flake
Added 12 variables.

nix-repl> :b packages.aarch64-darwin.install # build the package "install" in aarch64-darwin

This derivation produced the following outputs:
  out -> /nix/store/589dplvhsnjw0i6i62xb697p6jr9f8s1-install
❯ cat /nix/store/589dplvhsnjw0i6i62xb697p6jr9f8s1-install/bin/install
#! /nix/store/mg966h6cir056vnva84lw86wxp1f2z2m-bash-5.2p26/bin/bash -e
PATH=${PATH:+':'$PATH':'}
PATH=${PATH/':''/nix/store/589dplvhsnjw0i6i62xb697p6jr9f8s1-install/bin'':'/':'}
PATH='/nix/store/589dplvhsnjw0i6i62xb697p6jr9f8s1-install/bin'$PATH
PATH=${PATH#':'}
PATH=${PATH%':'}
export PATH
exec -a "$0" "/nix/store/589dplvhsnjw0i6i62xb697p6jr9f8s1-install/bin/.install-wrapped"  "$@"
❯ cat /nix/store/589dplvhsnjw0i6i62xb697p6jr9f8s1-install/bin/.install-wrapped
echo sup

Here is another way of doing this without the attribute set function call of createScript.

#create.nix

{
  symlinkJoin,
  writeScriptBin,
  makeWrapper,
}: name: dependencies:
symlinkJoin
{
  inherit name;
  paths =
    [
      ((writeScriptBin name ''echo sup'').overrideAttrs (old: {
        buildCommand = "${old.buildCommand}\n patchShebangs $out";
      }))
    ]
    ++ dependencies;
  buildInputs = [
    makeWrapper
  ];
  postBuild = "wrapProgram $out/bin/${name} --prefix PATH : $out/bin";
}

in flake.nix

# you no longer need to call the attribute of `createScript` from `create.nix`
# since it does not exist any more with this new example.
createScript = pkgs.callPackage ./create.nix {}; 

Proof:

❯ nix build .#packages.aarch64-darwin.install && \
 ls -la ./result && \
 cat ./result/bin/.install-wrapped

lrwxr-xr-x  1 sebas  staff  51 Mar  3 12:26 ./result@ -> /nix/store/589dplvhsnjw0i6i62xb697p6jr9f8s1-install
echo sup⏎

Edit, forgot to mention how to call it in flake.nix with the unary function way:

install = createScript "install" [pkgs.git pkgs.jq];
1 Like

Thanks, I also figured out that create.nix was passing an attribute set since it had a property called createScript.

As you suggested, using createScript = (pkgs.callPackage ./create.nix {};).createScript; should work, but since the only thing this file outputs is the function, I modified it just return the function only.

I also had to add name and dependencies to the function otherwise it did not recognize the arguments.

#create.nix
{ symlinkJoin, writeScriptBin, makeWrapper, name, dependencies ? [ ] }:

symlinkJoin
{
  inherit name;
  paths =
    [
      ((writeScriptBin name (builtins.readFile ../scripts/${name}.sh)).overrideAttrs (old: {
        buildCommand = "${old.buildCommand}\n patchShebangs $out";
      }))
    ]
    ++ dependencies;
  buildInputs = [
    makeWrapper
  ];
  postBuild = "wrapProgram $out/bin/${name} --prefix PATH : $out/bin";
}

And I call it like this from flake.nix.

flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };
        createScript = pkgs.callPackage ./modules/createScript.nix;
      in
      {
        packages = {
          install = createScript {
            name = "install";
            dependencies = with pkgs; [ git jq ];
          };

          switch = createScript {
            name = "switch";
          };
        };
      });
1 Like

Beautiful! plenty of ways of doing it :slight_smile: