Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to replace paths to executables in source code with Nix that are not in PATH

I wish to write some Haskell that calls an executable as part of its work; and install this on a nixOS host. I don't want the executable to be in my PATH (and to rely on that would disrupt the beautiful dependency model of nix).

If this were, say, a Perl script, I would have a simple builder that looked for strings of a certain format, and replaced them with the executable names, based upon dependencies declared in the .nix file. But that seems somewhat harder with the cabal-based building common to haskell.

Is there a standard idiom for encoding the paths to executables at build time (including during development, as well as at install time) within Haskell code on nix?

For the sake of a concrete example, here is a trivial "script":

import System.Process ( readProcess )

main = do
  stdout <- readProcess "hostname" [] ""
  putStrLn $ "Hostname: " ++ stdout

I would like to be able to compile run this (in principle) without relying on hostname being in the PATH, but rather replacing hostname with the full /nix/store/-inetutils-/bin/hostname path, and thus also gaining the benefits of dependency management under nix.

This could possibly be managed by using a shell (or similar) script, built using a replacement scheme as defined above, that sets up an environment that the haskell executable expects; but still that would need some bootstrapping via the cabal.mkDerivation, and since I'm a lover of OptParse-Applicative's bash completion, I'm loathe to slow that down with another script to fire up every time I hit the tab key. But if that's what's needed, fair enough.

I did look through cabal.mkDerivation for some sort of pre-build step, but if it's there I'm not seeing it.

Thanks,

like image 682
user3416536 Avatar asked Jul 11 '17 05:07

user3416536


2 Answers

Assuming you're building the Haskell app in Nix, you can patch a configuration file via your Nix expression. For an example of how to do this, have a look at this small project.

The crux is that you can define a postConfigure hook like this:

pkgs.haskell.lib.overrideCabal yourProject (old: {
  postConfigure = ''
    substituteInPlace src/Configuration.hs --replace 'helloPrefix = Nothing' 'helloPrefix = Just "${pkgs.hello}"'
  '';
})
like image 140
Robert Hensing Avatar answered Sep 30 '22 13:09

Robert Hensing


What I do with my xmonad build in nix1 is refer to executable paths as things like @@compton@@/bin/compton. Then I use a script like this to generate my default.nix file:

#!/usr/bin/env bash

set -eu

packages=($(grep '@@[^@]*@@' src/Main.hs | sed -e 's/.*@@\(.*\)@@.*/\1/' | sort -u))

extra_args=()
for p in "${packages[@]}"; do
    extra_args+=(--extra-arguments "$p")
done

cabal2nix . "${extra_args[@]}" \
    | head -n-1

echo "  patchPhase = ''";
echo "    substituteInPlace src/Main.hs \\"
for p in "${packages[@]}"; do
    echo "      --replace '@@$p@@' '\${$p}' \\"
done
echo "  '';"

echo "}"

What it does is grep through src/Main.hs (could easily be changed to find all haskell files, or to some specific configuration module) and pick out all the tags surrounded by@@ like @@some-package-name@@. It then does 2 things with them:

  1. passes them to cabal2nix as extra arguments for the nix expression it generates
  2. post-processes nix expression output from cabal2nix to add a patch phase, which replaces the @@some-package-name@@ tag in the Haskell source file with the actual path to the derivation.2

This generates a nix-expression like this:

{ mkDerivation, base, compton, networkmanagerapplet, notify-osd
, powerline, setxkbmap, stdenv, synapse, system-config-printer
, taffybar, udiskie, unix, X11, xmonad, xmonad-contrib
}:
mkDerivation {
  pname = "xmonad-custom";
  version = "0.0.0.0";
  src = ./.;
  isLibrary = false;
  isExecutable = true;
  executableHaskellDepends = [
    base taffybar unix X11 xmonad xmonad-contrib
  ];
  description = "My XMonad build";
  license = stdenv.lib.licenses.bsd3;
  patchPhase = ''
    substituteInPlace src/Main.hs \
      --replace '@@compton@@' '${compton}' \
      --replace '@@networkmanagerapplet@@' '${networkmanagerapplet}' \
      --replace '@@notify-osd@@' '${notify-osd}' \
      --replace '@@powerline@@' '${powerline}' \
      --replace '@@setxkbmap@@' '${setxkbmap}' \
      --replace '@@synapse@@' '${synapse}' \
      --replace '@@system-config-printer@@' '${system-config-printer}' \
      --replace '@@udiskie@@' '${udiskie}' \
  '';
}

The net result is I can just write Haskell code and a cabal package file; I don't have to worry much about maintaining the nix package file as well, only re-running my generate-nix script if my dependencies change.

In my Haskell code I just write paths to executables as if @@the-nix-package-name@@ was an absolute path to a folder where that package is installed, and everything magically works.

The installed xmonad binary ends up containing hardcoded references to the absolute paths to the executables I call, which is how nix likes to work (this means it automatically knows about the dependency during garbage collection, for example). And I don't have to worry about keeping the things I called in my interactive environment's PATH, or maintaining a wrapper that sets up PATH just for this executable.


1 I have it set up as a cabal project that gets built and installed into the nix store, rather than having it dynamically recompile itself from ~/.xmonad/xmonad.hs

2 Step 2 is a little meta, since I'm using a bash script to generate nix code with an embedded bash script in it

like image 44
Ben Avatar answered Sep 30 '22 14:09

Ben