Nix scaffolding for running Haskell plugins
Posted on June 24, 2018
I’ve been all about writing source plugins recently but have been dissatisfied with how rough it is to use them practically. In particular, I am writing plugins which don’t change the semantics of my programs but are useful for debugging. I only sometimes want to run them and don’t want them to appear as dependencies at all on Hackage. It needs to be easy to apply them to my own and other people’s packages.
Of course, the solution was to leverage my knowledge of nix to wrap up everything into a nice interface. The key to the interface is a new function haskell.lib.addPlugin
which augments an existing package with a plugin.
This invocation will now also output an HTML representation of the core program of the either
package by using the dump-core
plugin.
This post will explain this interface in more detail but will not get into the gritty implementation details. The implementation can be found in the haskell-nix-plugin
repo.
Two uses of plugins
There are two kinds of plugins:
- A plugin which modify your program so should always run. For example, the
ghc-typelits-natnormalise
plugin which solves constraints containing type level numbers. - A plugin which collects information or debugging information which is optional. For example, the
dump-core
plugin which outputs an HTML rendering of the core of your program.
Even though you only sometimes want to run plugins in the second category, the convenient way to involves several steps:
- Add the plugin package to your
build-depends
. - Pass a specific option
-fplugin=DumpCore
by modifyingghc-options
.
If you want to package your library correctly, you then need to hide this information behind a cabal flag. However, this is still undesirable as a package like dump-core
is only going to be used by developers but the flag now appears in the public interface. Further to this, I can’t run my plugin on someone else’s package without modifying the cabal file.
What we really want is a way to take an existing package and to augment it to run the plugin. In order to do this, we define a function addPlugin
which takes a plugin and a package as an argument and then compiles the package whilst running the plugin. For example, if we want to inspect the core of the either
package, we could specify this like so.
If we then build this modified package, there will be a new output which has the same name as the plugin which contains the output of the plugin. So in this case, we will find the relevant HTML by inspecting the DumpCore
attribute. It will also be symlinked to result-DumpCore
.
There are three new elements to the nixpkgs API.
- A new function
haskell.lib.addPlugin
which adds a plugin to a package. - A new attribute
haskell.plugins
which is parameterised by a Haskell package set and contains a set of plugins. - A new
with*
function,haskellPackages.withPlugin
which takes a function expecting two arguments, the first being a set of plugins for that package set and the second being a list of packages for that package set. The result of the function should be a Haskell package.
The plugin set
The haskell.plugins
attribute is a set of plugins parameterised by a normal Haskell package set. It is designed in this manner so the same plugin definitions can be used with different compilers.
Each attribute is a different plugin which we might want to use with our program.
A plugin
A plugin is a Haskell package which provides the plugin with four additional attributes which describe how to run it. For example, here is the definition for the dump-core
plugin.
dump-core = { pluginPackage = hp.dump-core ;
pluginName = "DumpCore";
pluginOpts = (out-path: [out-path]);
pluginDepends = [];
finalPhase = _: ""; } ;
pluginPackage
- The Haskell package which provides the plugin.
pluginName
- The module name where the plugin is defined.
pluginOpts
- Additional options to pass to the plugin. The path where it places its output is passed as an argument.
pluginDepends
- Any additional system dependencies the plugin needs for the finalPhase.
finalPhase
- An action to run in the
postBuild
phase, after the plugin has run. The output directory is passed as an argument.
In most cases, pluginDepends
and finalPhase
can be omitted (they then take these default values) but they are useful for when a plugin emits information as it compiles each module which is then summarised at the end.
An example of this architecture is the graphmod-plugin
. As each module is compiled, the import information is serialised. Then, at the end we read all the serialised files and create a dot graph of the module import structure. Here is how we specify the final phase of the plugin:
graphmod = { pluginPackage = hp.graphmod-plugin;
pluginName = "GraphMod";
pluginOpts = (out-path: ["${out-path}/output"]);
pluginDepends = [ nixpkgs.graphviz ];
finalPhase = out-path: ''
graphmod-plugin --indir ${out-path}/output > ${out-path}/out.dot
cat ${out-path}/out.dot | tred | dot -Tpdf > ${out-path}/modules.pdf
''; } ;
The first three fields are standard, however we now populate the final two arguments as well. We firstly add a dependency on graphviz
which we will use to render the module graph and then specify the invocations needed to firstly summarise and then render the information.
In this architecture, the plugin package provides a library interface which exposes the plugin and an executable which is invoked to collect the information output by the plugin. This is what the call to graphmod-plugin
achieves.
withPlugin
We also provide the withPlugin
attribute which supplies both the plugins and packages already applied to a specific package set. The reason for this is that a plugin and a package must be both compiled by the same compiler. Thus, unrestricted usage of addPlugin
can lead to confusing errors if the plugin and package are compiled with different compilers. The withPlugin
attribute ensures that the versions align correctly.
core-either =
haskellPackages.withPlugin
(plugins: packages: addPlugin plugins.dump-core packages.either)
How can I use it?
This infrastructure is provided as an overlay. Install the overlay as you would normally, one suggested method can be see in the example.nix
file.
let
plugin-overlay-git = builtins.fetchGit
{ url = https://github.com/mpickering/haskell-nix-plugin.git;} ;
plugin-overlay = import "${plugin-overlay-git}/overlay.nix";
nixpkgs = import <nixpkgs> { overlays = [plugin-overlay]; };
in ...
Conclusion
So far, I have not package many plugins in this manner but as source plugins are released in GHC 8.6 I expect to want to use plugins more and more regularly.