Hix Manual

Nix development tools for Haskell projects


Table of Contents

Introduction
About
Cabal builds
Nix flakes
Declaring Hix builds
Package definitions
Multiple systems and IFD
User Interface
Environments and commands
Environments
Commands
Services
Other tools
Haskell Language Server
GHC version checks
Hackage upload
CTags
Cross-compilation and static linking
Automatic dependency management
Miscellaneous tools

Introduction

Table of Contents

About
Cabal builds
Nix flakes

About

Hix is a toolkit for Haskell development that uses Nix to provide a unified, declarative interface for a range of build related tasks:

  • Reproducible environments and dependency overrides

  • Cabal file generation

  • Hackage upload

  • Rapid-recompilation testing with GHCid

  • Haskell Language Server

  • CTags generation

  • Virtual Machines for testing

  • Compatibility checks for multiple GHC versions

The following two sections explain the basics of Cabal builds and Nix flakes. If you’re already familiar with those, you can skip ahead to the section called “Package definitions”, but the later sections will build upon the examples introduced here.

Cabal builds

Cabal is the basic build tool for Haskell projects. It reads package configuration from a .cabal file and performs compilation and execution of tests and applications. A Cabal package consists of one or more components like libraries, test suites and executables.

The example project for this tutorial is called parser and defines a library and an executable. The library has a single module named Parser.hs in the subdirectory lib/ with the content:

module Parser where

import Data.Aeson (encode, object, toJSON, (.=))
import Data.ByteString.Lazy.Char8 (unpack)
import Data.String (fromString)
import System.Environment (getArgs)
import Text.Read (readMaybe)

createJson :: Int -> String
createJson n = unpack (encode (object [fromString "number" .= toJSON n]))

parseNumber :: IO String
parseNumber = parse <$> getArgs
  where
    parse (input : _) = maybe ("Not a number: " ++ input) createJson (readMaybe input)
    parse _ = "No argument given."

The function parseNumber converts the first command line argument into an integer and returns a JSON string that looks like {number:5}, or an error message if the argument is not a number. It uses the JSON library aeson and the GHC core library bytestring, which have to be specified as dependencies in the Cabal file like this:

library
  exposed-modules:
      Parser
  hs-source-dirs:
      lib
  build-depends:
      aeson ==2.0.*
    , base ==4.*
    , bytestring

We need to configure the source directory lib, the module Parser as well as the dependencies, including the standard library base.

The second file in this project is the executable’s entry point, located at app/Main.hs:

module Main where

import Parser (parseNumber)

main :: IO ()
main = putStrLn =<< parseNumber

The main function calls parseNumber and prints the returned string to stdout. It has no dependencies except for base and the library component, therefore its Cabal section is:

executable parser
  main-is: Main.hs
  hs-source-dirs:
      app
  build-depends:
      base ==4.*
    , parser

The only difference here is that we need to specify the module that contains the main function with the key main-is.

When these two fragments are written to parser.cabal in the project root along with a bit of boilerplate, the Cabal CLI tool be used to compile and run the application by invoking it as cabal build and cabal run.

In order for this to work, the developer has to ensure that GHC and Cabal are installed and use the right version and package set snapshot to be certain that the application is built the same way on different machines. Hix aims to reduce the overhead of this process to requiring only the presence of Nix, with the goal of replicating the development environment and all involved tools identically on any machine. While Cabal offers some mechanisms for this kind of reproducibility, Nix provides a unified and ergonomic interface to it on multiple levels of the build.

Nix flakes

The build definition for a Nix project is declared in the file flake.nix with the following protocol:

{
  inputs = {...};
  outputs = inputs: {
    packages = {...};
    apps = {...};
    ...
  };
}

The input set declares dependencies on other repositories, which are downloaded and passed to the output function as source directories when a Nix command is executed. Depending on the command, Nix chooses one of the entries in the output set and builds the requested package.

A basic example for an application that prints “Hello” could look like this:

{
  inputs = { nixpkgs.url = "github:nixos/nixpkgs/b139e44d78c36c69bcbb825b20dbfa51e7738347"; };
  outputs = {self, nixpkgs}: let
    pkgs = import nixpkgs { system = "x86_64-linux"; };
    drv = pkgs.writeScriptBin "greet" "echo Hello";
  in {
    apps.x86_64-linux.greet = {
      type = "app";
      program = "${drv}/bin/greet";
    };
  };
}

The single input is the nixpkgs repository at a specific commit, which contains both the build definitions for tens of thousands of available packages and many tools for creating new packages.

The single output is an app named greet declared only for the architecture x86_64-linux, which can be executed with:

nix run .#greet

where the . denotes the directory of the flake and the part after the # is the name of the output, which Nix tries to locate in different categories depending on the command – in the case of run, it starts looking in apps using the current system’s architecture.

To gain access to nixpkgs’ functionality, we import the default.nix (implicitly) at the root of the repository, passing the identifier of the architecture we want to use as the system argument. The function writeScriptBin is a so-called “trivial builder”, a function that produces a very simple package, like a single shell script.

Under the hood, Nix wraps the builder in a data structure called derivation that serves as the universal protocol for the specification of dependencies, build steps and build outputs. When Nix is instructed to process a derivation, it follows its build steps and writes the resulting files to a directory in /nix/store. In this case, the text echo Hello is written to /nix/store/bkz0kkv0hxhb5spcxw84aizcj5rm4qq9-greet/bin/greet, where the hash is calculated from the text and other environmental parameters. When the value drv is interpolated into the string in the declaration of the app output, Nix builds the derivation and inserts the resulting store path (minus the bin/greet).

Derivations are ubiquitous – when we build a Haskell application with Nix, it is represented as a derivation, just like all of its dependencies. For the Cabal project described in the previous section, we can create a derivation and an app with this flake:

{
  inputs = { nixpkgs.url = "github:nixos/nixpkgs/b139e44d78c36c69bcbb825b20dbfa51e7738347"; };
  outputs = {self, nixpkgs}: let
    pkgs = import nixpkgs { system = "x86_64-linux"; };
    parser = pkgs.haskell.packages.ghc925.callCabal2nix "parser" ./. {};
  in {
    packages.x86_64-linux.parser = parser;
    apps.x86_64-linux.greet = {
      type = "app";
      program = "${parser}/bin/parser";
    };
  };
}

Now the app can be executed with:

$ nix run .#parser -- 63
{"number":63}

To create a derivation for the Haskell app, we select a package set for the GHC version 9.2.5 with pkgs.haskell.packages.ghc92 and call the builder exposed by that set that wraps the tool cabal2nix, which converts the Cabal file in the specified directory to a derivation. Cabal2nix reads the dependencies from the file and ensures that they are accessible during the build, in which Cabal is invoked like in the previous section, though using a lower-level interface.

If this project were cloned on a different machine, like in a CI pipeline, the nixpkgs snapshot, and hence the entire build environment, would be identical to that on your development machine – not just because the Git revision is hardcoded in the flake input, but because Nix records the revision in the lockfile, flake.lock. If the revision were omitted, the latest commit at the time the project was first evaluated would be used, and updated only when explicitly requested by executing either nix flake update (to update all inputs) or nix flake lock --update-input nixpkgs.

For a trivial Cabal project like this, the build definition may not look complicated enough to elicit the desire for higher abstractions. However, when the requirements become more elaborate, the basic tools provided by nixpkgs may prove inadequate, and the following sections attempt to illustrate some of the circumstances that warrant a more complex toolkit.

Declaring Hix builds

Package definitions

Hix provides two fundamental services to combine Cabal and Nix:

  • Generating Cabal build files from configuration declared in Nix expressions.

  • Creating reproducible derivations from those files for different configurable environments that obtain dependencies from the Nix package set, which can be built and run via the flake interface.

The package from the tutorial section, consisting of an executable in app/ and a library in lib/ with dependencies on the packages aeson and bytestring, can be declared in a flake like this:

{
  description = "Example";
  inputs.hix.url = "github:tek/hix?ref=0.8.0";
  outputs = {hix, ...}: hix {
    packages.parser = {
      src = ./.;
      library = {
        enable = true;
        dependencies = ["aeson ^>= 2.0" "bytestring"];
      };
      executable.enable = true;
    };
  };
}

In order to build the project, we first have to generate the Cabal file:

$ nix run .#gen-cabal

Note

Flake commands ignore files that are not under version control when operating in a git repository. If there are untracked files in your project, running nix build .#gen-cabal might fail, so either git add everything or run the command as nix run path:.#gen-cabal, which bypasses this mechanism.

This command will create the file parser.cabal in the project directory, with the following content:

cabal-version: 1.12

-- This file has been generated from package.yaml by hpack version 0.35.0.
--
-- see: https://github.com/sol/hpack

name:           parser
version:        0.1.0.0
description:    See https://hackage.haskell.org/package/parser/docs/Parser.html
license:        GPL-3
build-type:     Simple

library
  exposed-modules:
      Parser
  other-modules:
      Paths_parser
  hs-source-dirs:
      lib
  build-depends:
      aeson ==2.0.*
    , base ==4.*
    , bytestring
  default-language: Haskell2010

executable parser
  main-is: Main.hs
  other-modules:
      Paths_parser
  hs-source-dirs:
      app
  ghc-options: -threaded -rtsopts -with-rtsopts=-N
  build-depends:
      base ==4.*
    , parser
  default-language: Haskell2010

Using the package definition and the generated Cabal file, Hix creates flake outputs for building and running the application with nix build and nix run:

$ nix run .#parser -- 63
{"number":63}

The generated flake outputs have roughly the following structure, analogous to the example in {#nix-flakes}:

{
  outputs = let
    parser = callCabal2nix "parser" ./. {};
  in {
    packages.x86_64-linux.parser = parser;
    apps.x86_64-linux.parser = { type = "app"; program = "${parser}/bin/parser"; };
  };
}

Generating flakes

Rather than writing the boilerplate yourself, the Hix CLI application can generate it for you.

The CLI command new will create a project skeleton with an executable and test suite in the current directory:

nix run 'github:tek/hix?ref=0.8.0#new' -- --name 'project-name' --author 'Your Name'

If you have an existing project with Cabal files in it, the bootstrap command will create a flake that configures the more basic components:

nix run 'github:tek/hix?ref=0.8.0#bootstrap'

Cabal configuration

There are three levels of generality at which Cabal options can be specified:

  • Global options in the module cabal apply to all packages and components

  • Package-level options in the module packages.<name>.cabal apply to all components in a package

  • Component-level options in all component modules

The structure looks like this:

{
  cabal = {
    dependencies = ["global-dep"];
  };
  packages = {
    core = {
      cabal = {
        dependencies = ["core-dep"];
      };
      library = {
        dependencies = ["core-lib-dep"];
      };
      executables.run = {
        dependencies = ["core-exe-dep"];
      };
    };
    api = {
      cabal = {
        dependencies = ["api-dep"];
      };
      library = {
        dependencies = ["api-lib-dep"];
      };
      tests.unit = {
        dependencies = ["api-test-dep"];
      };
    };
  };
}

Each component gets global-dep, while all components in core get core-dep. Since dependencies is a list option, the values are merged, so that the unit component in api will have the dependencies ["global-dep" "api-dep" "api-test-dep"].

Verbatim configuration for individual components that have no specific option may be specified in the component module:

{
  packages = {
    core = {
      library = {
        component = {
          other-modules = ["Prelude"];
        };
      };
    };
  };
}

Cabal options

These are the Cabal options for packages and components that can be specified at any level.

enable

Whether to enable this <component type>.

Type: boolean

Default: true

Example: true

author

The author of the packages in this option tree. May be null to omit it from the config.

Type: null or string

Default: null

base

The dependency spec for the base package.

Type: string or (submodule)

Default: "base >= 4 && < 5"

baseHide

The dependency spec for the base package used when prelude is set.

Type: string or (submodule)

Default:

{
  mixin = [
    "hiding (Prelude)"
  ];
  name = "base";
  version = ">= 4 && < 5";
}
benchSuffix

This string is appended to the package name to form the single benchmark component. See testSuffix for an example.

Type: string

Default: "-bench"

build-type

The build type for the packages in this option tree. May be null to omit it from the config.

Type: null or string

Default: "Simple"

component

Verbatim Cabal configuration in HPack format. Values defined here will be applied to components, not packages.

Cascades down into all components.

Note

This unconditionally overrides all option definitions with the same keys if they are not mergeable (like lists and attrsets).

Type: attribute set of unspecified value

Default: { }

copyright

The copyright string for the packages in this option tree. The default is to combine copyrightYear and author; May be null to omit it from the config.

Type: null or string

Default: null

copyrightYear

The year for the copyright string.

Type: string

Default: "2025"

default-extensions

GHC extensions for all components in this option tree.

Type: list of string

Default: [ ]

Example: ["DataKinds" "FlexibleContexts" "OverloadedLists"]

dependOnLibrary

Convenience feature that automatically adds a dependency on the library component to all executable components, if the library exists.

Type: boolean

Default: true

dependencies

Cabal dependencies used for all components in this option tree.

Type: list of package dependency in HPack format

Default: [ ]

Example: ["aeson" "containers"]

env

The environment used when running GHCi with a module from this component.

Type: null or name of an environment defined in config.envs

Default: null

exeSuffix

This string is appended to the package name to form the single executable component. See testSuffix for an example. The default is to use no suffix, resulting in the same name as the package and library.

Type: string

Default: ""

ghc-options

GHC options for all components in this option tree.

Type: list of string

Default: [ ]

Example: ["-Wunused-imports" "-j6"]

ghc-options-exe

GHC options for all executables in this option tree. The purpose of this is to allow ghc-options to use it as the default for executables without requiring complicated overrides to disable it. If you don’t want to use these options, set this option to [] instead of forcing other values in ghc-options. These options are not used for benchmarks.

Type: list of string

Default:

[
  "-threaded"
  "-rtsopts"
  "-with-rtsopts=-N"
]
internal.single

Whether this is the main component of its sort, declared as test rather than tests.foo.

Type: boolean (read only)

Default: false

internal.sort

Sort of the component (test, executable etc)

Type: one of “library”, “executable”, “test”, “benchmark” (read only)

Default: "<component type>"

language

The default extension set used for all components in this option tree. It is set to GHC2021 if the GHC versions of all defined envs are 9.2 or greater, and Haskell2010 otherwise.

Type: string

Default: "GHC2021"

license

The license for all packages in this option tree. May be null to omit it from the config.

Type: null or string

Default: "GPL-3"

license-file

The name of the file containing the license text for all packages in this option tree. May be null to omit it from the config.

Type: null or string

Default: null

meta

Verbatim top-level Cabal configuration in HPack format. Values defined here will not be applied to components, only packages.

Cascades down into all packages.

This should only be used for keys that have no corresponding module option, otherwise the values defined in a package might be overridden by option definitions in the global config.

Type: attribute set of unspecified value

Default: { }

name

The name of the <component type>, defaulting to the attribute name in the config or the package name.

Type: string

Default: "<name>"

paths

Cabal generates the module Paths_packagename for each component, which provides access to data files included in a package, but is rarely used. This may cause trouble if prelude is configured to use an alternative Prelude that does not export some of the names used in this module. Setting this option to false prevents this module from being generated.

Type: boolean

Default: true

prelude

Configure an alternative Prelude package.

Type: submodule

Default: { }

prelude.enable

Whether to enable an alternative Prelude.

Type: boolean

Default: false

Example: true

prelude.package

The package containing the alternative Prelude.

Type: string or (submodule)

Default: "base"

Example: "relude"

prelude.module

The module name of the alternative Prelude.

Type: string

Default: "Prelude"

Example: "Relude"

source-dirs

Directories with Haskell sources.

Type: string or list of string

Default: "<name>"

testSuffix

This string is appended to the package name to form the single test component. For example, given the config:

{
  packages.spaceship = {
    test.cabal.testSuffix = "-integration";
  }
}

The name of the generated testsuite will be spaceship-integration.

Type: string

Default: "-test"

version

The version for all packages in this option tree.

Type: string

Default: "0.1.0.0"

Package configuration

Packages may contain multiple components: an optional library and any number of sublibraries, executables, test suites or benchmarks.

Each of those can be specified as the single component of its type using the singular-name option, like executable, or as a value in the plural-name submodule, or both:

{
  packages.api = {
    executable = { enable = true; };
    executables = {
      server = { enable = true; source-dirs = "api-server"; };
      client = {};
      debug = { enable = false; };
    };
  };
}

This configuration generates three Cabal executables:

  • The default executable requires the option enable to be set explicitly and uses the source directory app unless specified otherwise.

  • The option server configures a custom source directory

  • The option client uses the default source directory, which is the same as the attribute name. All components configured in the plural-name submodule are enabled by default, so this one is also generated.

  • The option debug sets enable to false, so it is omitted from the configuration.

If no component was enabled, Hix defaults to enabling the default executable.

Multiple libraries are supported with the same syntax as other components. You can depend on them with the dependency string <pkg>:<lib>; when depending from another package, the library must have public = true; set.

Package options

benchmark

The single benchmark for this package. To define multiple benchmarks, use benchmarks.

Type: submodule of cabal-options and cabal-component

Default: { }

benchmark.name

The name of the benchmark, defaulting to the attribute name in the config or the package name.

Type: string

Default: "<name>-bench"

benchmark.source-dirs

Directories with Haskell sources.

Type: string or list of string

Default: "benchmark"

benchmarks

Benchmarks for this package. If benchmark is defined, it will be added.

Type: attribute set of (submodule of cabal-options and cabal-component)

Default: { }

benchmarks.<name>.name

The name of the benchmark, defaulting to the attribute name in the config or the package name.

Type: string

Default: "‹name›"

benchmarks.<name>.source-dirs

Directories with Haskell sources.

Type: string or list of string

Default: "‹name›"

buildInputs

Additional non-Haskell dependencies required by this package.

Type: (function that evaluates to a(n) list of package) or list of package

Default: [ ]

cabal

Cabal options that are applied to all components.

Note: In order to enable cascading of these options, the definitions are not evaluated in-place, but when evaluating components. Therefore, referring to these values with e.g. config.packages.name.cabal.version does not work as expected if the value uses an option property like mkIf or mkOverride. You can use cabal-config for this purpose, though.

Type: module

Default: { }

cabal-config

Evaluated version of cabal, for referencing in other config values. May not be set by the user.

Type: submodule of cabal-options (read only)

Default: { }

dep.exact

Dependency string for referencing this package with its version from other Cabal package. Like dep.minor, but uses exact version equality, like core ==0.4.1.0.

Type: package dependency in HPack format (read only)

dep.minor

Dependency string for referencing this package with its version from other Cabal package. Uses the minor version dependency bound, strictly greater than the precise version.

{config, ...}: {
  packages = {
    core = { version = "0.4.1.0"; };
    api = {
      dependencies = [config.packages.core.dep.minor];
    };
  }
}

This results in the dependency string core >= 0.4.1.0 && < 0.5 in the Cabal file.

Also works when using versionFile.

Type: package dependency in HPack format (read only)

description

The Cabal description of this packages. The default is a link to the rootModule on Hackage, using the option hackageRootLink. May be null to omit it from the config.

Type: null or string

Default: null

executable

The single executable for this package. To define multiple executables, use executables.

Type: submodule of cabal-options and cabal-component

Default: { }

executable.name

The name of the executable, defaulting to the attribute name in the config or the package name.

Type: string

Default: "<name>"

executable.source-dirs

Directories with Haskell sources.

Type: string or list of string

Default: "app"

executables

Executables for this package. If executable is defined, it will be added.

Type: attribute set of (submodule of cabal-options and cabal-component)

Default: { }

executables.<name>.name

The name of the executable, defaulting to the attribute name in the config or the package name.

Type: string

Default: "‹name›"

executables.<name>.source-dirs

Directories with Haskell sources.

Type: string or list of string

Default: "‹name›"

expose

The parts of this package that should be accessible as flake outputs, like being able to run nix build .#<env>.<package>. If the value is boolean, all parts are affected. If it is a set, submodule options configure the individual parts.

Type: boolean or (submodule)

Default: true

hackageLink

A convenience option containing the URL to the Hackage page using the package name.

Type: string

hackageRootLink

A convenience option containing the URL to the root module’s documentation on Hackage using the package name and rootModule.

Type: string

libraries

The sublibraries of this package. Unlike library, these are treated specially by cabal. To depend on them, use <pkg>:<lib>. If libraries.<name>.public is set to false, you can only depend on them from other components in the same package (this is then called an internal library – default is true).

Type: attribute set of (submodule of cabal-options and cabal-component)

Default: { }

libraries.<name>.enable

Whether to enable this library.

Type: boolean

Default: true

Example: true

libraries.<name>.author

The author of the packages in this option tree. May be null to omit it from the config.

Type: null or string

Default: null

libraries.<name>.base

The dependency spec for the base package.

Type: string or (submodule)

Default: "base >= 4 && < 5"

libraries.<name>.baseHide

The dependency spec for the base package used when prelude is set.

Type: string or (submodule)

Default:

{
  mixin = [
    "hiding (Prelude)"
  ];
  name = "base";
  version = ">= 4 && < 5";
}
libraries.<name>.benchSuffix

This string is appended to the package name to form the single benchmark component. See testSuffix for an example.

Type: string

Default: "-bench"

libraries.<name>.build-type

The build type for the packages in this option tree. May be null to omit it from the config.

Type: null or string

Default: "Simple"

libraries.<name>.component

Verbatim Cabal configuration in HPack format. Values defined here will be applied to components, not packages.

Cascades down into all components.

Note

This unconditionally overrides all option definitions with the same keys if they are not mergeable (like lists and attrsets).

Type: attribute set of unspecified value

Default: { }

libraries.<name>.copyright

The copyright string for the packages in this option tree. The default is to combine copyrightYear and author; May be null to omit it from the config.

Type: null or string

Default: null

libraries.<name>.copyrightYear

The year for the copyright string.

Type: string

Default: "2025"

libraries.<name>.default-extensions

GHC extensions for all components in this option tree.

Type: list of string

Default: [ ]

Example: ["DataKinds" "FlexibleContexts" "OverloadedLists"]

libraries.<name>.dep.exact

Dependency string for referencing this library with its version from other Cabal package. Like libraries.<name>.dep.minor, but uses exact version equality, like core ==0.4.1.0.

Type: package dependency in HPack format (read only)

libraries.<name>.dep.minor

Dependency string for referencing this library with its version from other Cabal package. Like dep.minor, but for sublibraries.

Type: package dependency in HPack format (read only)

libraries.<name>.dependOnLibrary

Convenience feature that automatically adds a dependency on the library component to all executable components, if the library exists.

Type: boolean

Default: true

libraries.<name>.dependencies

Cabal dependencies used for all components in this option tree.

Type: list of package dependency in HPack format

Default: [ ]

Example: ["aeson" "containers"]

libraries.<name>.env

The environment used when running GHCi with a module from this component.

Type: null or name of an environment defined in config.envs

Default: null

libraries.<name>.exeSuffix

This string is appended to the package name to form the single executable component. See testSuffix for an example. The default is to use no suffix, resulting in the same name as the package and library.

Type: string

Default: ""

libraries.<name>.ghc-options

GHC options for all components in this option tree.

Type: list of string

Default: [ ]

Example: ["-Wunused-imports" "-j6"]

libraries.<name>.ghc-options-exe

GHC options for all executables in this option tree. The purpose of this is to allow ghc-options to use it as the default for executables without requiring complicated overrides to disable it. If you don’t want to use these options, set this option to [] instead of forcing other values in ghc-options. These options are not used for benchmarks.

Type: list of string

Default:

[
  "-threaded"
  "-rtsopts"
  "-with-rtsopts=-N"
]
libraries.<name>.internal.single

Whether this is the main component of its sort, declared as test rather than tests.foo.

Type: boolean (read only)

Default: false

libraries.<name>.internal.sort

Sort of the component (test, executable etc)

Type: one of “library”, “executable”, “test”, “benchmark” (read only)

Default: "library"

libraries.<name>.language

The default extension set used for all components in this option tree. It is set to GHC2021 if the GHC versions of all defined envs are 9.2 or greater, and Haskell2010 otherwise.

Type: string

Default: "GHC2021"

libraries.<name>.license

The license for all packages in this option tree. May be null to omit it from the config.

Type: null or string

Default: "GPL-3"

libraries.<name>.license-file

The name of the file containing the license text for all packages in this option tree. May be null to omit it from the config.

Type: null or string

Default: null

libraries.<name>.meta

Verbatim top-level Cabal configuration in HPack format. Values defined here will not be applied to components, only packages.

Cascades down into all packages.

This should only be used for keys that have no corresponding module option, otherwise the values defined in a package might be overridden by option definitions in the global config.

Type: attribute set of unspecified value

Default: { }

libraries.<name>.name

The name of the library, defaulting to the attribute name in the config or the package name.

Type: string

Default: "‹name›"

libraries.<name>.paths

Cabal generates the module Paths_packagename for each component, which provides access to data files included in a package, but is rarely used. This may cause trouble if prelude is configured to use an alternative Prelude that does not export some of the names used in this module. Setting this option to false prevents this module from being generated.

Type: boolean

Default: true

libraries.<name>.prelude

Configure an alternative Prelude package.

Type: submodule

Default: { }

libraries.<name>.prelude.enable

Whether to enable an alternative Prelude.

Type: boolean

Default: false

Example: true

libraries.<name>.prelude.package

The package containing the alternative Prelude.

Type: string or (submodule)

Default: "base"

Example: "relude"

libraries.<name>.prelude.module

The module name of the alternative Prelude.

Type: string

Default: "Prelude"

Example: "Relude"

libraries.<name>.public

Whether to expose an internal library.

Type: boolean

Default: true

libraries.<name>.reexported-modules

Modules from dependencies that this library exposes for downstream projects to import.

Type: list of string

Default: [ ]

Example: ["Control.Concurrent.STM" "Data.Text"]

libraries.<name>.source-dirs

Directories with Haskell sources.

Type: string or list of string

Default: "‹name›"

libraries.<name>.testSuffix

This string is appended to the package name to form the single test component. For example, given the config:

{
  packages.spaceship = {
    test.cabal.testSuffix = "-integration";
  }
}

The name of the generated testsuite will be spaceship-integration.

Type: string

Default: "-test"

libraries.<name>.version

The version for all packages in this option tree.

Type: string

Default: "0.1.0.0"

library

The library for this package.

Type: submodule of cabal-options and cabal-component

Default: { }

library.name

The name of the library, defaulting to the attribute name in the config or the package name.

Type: string

Default: "<name>"

library.source-dirs

Directories with Haskell sources.

Type: string or list of string

Default: "lib"

name

The name of the package, determined by the attribute name in the config.

Type: string (read only)

Default: "<name>"

override

Manipulate the package’s derivation using the combinators described in the section called “Override combinators”.

Type: function that evaluates to a(n) function that evaluates to a(n) unspecified value

Default: <function>

relativePath

A string representation of src relative to the project root. Its value is inferred if possible, but if src is not a plain path, it must be set explicitly. A common reason for this is when the path is constructed with a source filter, causing the creation a separate store path for the subdirectory. In the basic case, Hix infers the root directory (for base) by taking the prefix /nix/store/*/ from one of the package paths, and stripping it from each package’s src. This works well for simple projects, but it helps to provide base, as well as this option, explicitly. If the package is at the project root, this value should be ".".

Type: null or string

Default: null

rootModule

A convenience option that is used to generate a Hackage link. It should denote the module that represents the most high-level API of the package, if applicable. The default is to replace dashes in the name with dots.

Type: string

src

The root directory of the package.

Type: path

Example: ./packages/api

subpath

The computed relative path of the package root directory.

Type: string (read only)

test

The single test suite for this package. To define multiple test suites, use tests.

Type: submodule of cabal-options and cabal-component

Default: { }

test.name

The name of the test suite, defaulting to the attribute name in the config or the package name.

Type: string

Default: "<name>-test"

test.source-dirs

Directories with Haskell sources.

Type: string or list of string

Default: "test"

tests

Test suites for this package. If test is defined, it will be added.

Type: attribute set of (submodule of cabal-options and cabal-component)

Default: { }

tests.<name>.name

The name of the test suite, defaulting to the attribute name in the config or the package name.

Type: string

Default: "‹name›"

tests.<name>.source-dirs

Directories with Haskell sources.

Type: string or list of string

Default: "‹name›"

versionFile

The version file for this package, defaulting to the global hackage.versionFile if null. When generating Cabal files, the version field will be set to the content of this file, unless version is set explicitly. When bumping the version of a package with nix run .#release, this file is updated. Should be relative to the project root.

Type: null or string

Default: null

General options

packages

The project’s Cabal packages, with Cabal package names as keys and package config as values. The config is processed with HPack.

Type: attribute set of (submodule)

Default: { }

Example:

{
  core.src = ./.;
  api = { src = ./api; cabal.dependencies = ["aeson"]; library.enable = true; };
}

base

The project’s base directory.

Will be inferred from package source directories if unset. If the Hix project is a subdirectory of a git repository and flake.nix isn’t tracked by git, this option has to be set to ./. explicitly.

Type: null or path

Default: null

Example: ./.

buildInputs

Additional non-Haskell dependencies provided to all packages and environments.

Type: (function that evaluates to a(n) list of package) or list of package

Default: [ ]

buildOutputsPrefix

Some of Hix’s features are exposed as top level outputs, like nix run .#release. Since these are quite numerous, it becomes increasingly likely that these clash with some of the project’s package or command names, making them inaccessible.

For this reason, built-in Hix outputs are added to the outputs with lower precedence, to ensure that user-defined outputs aren’t shadowed. To keep the built-in outputs accessible as well, they are additionally exposed in a dedicated prefix set named build, as in nix run .#build.release.

If you prefer a different prefix, set this option to the desired string.

Type: string

Default: "build"

cabal

Cabal options that are applied to all packages and components.

If you define any options here, they will be merged with definitions that are set in packages or components. This means that the default priority handling applies – definitions in components don’t automatically override those in packages or the global config. You will need to use mkDefault or mkForce, or even mkOverride if you define an option at all three levels.

Note

In order to enable cascading for these options, the definitions are not evaluated in-place, but when evaluating packages and components. Therefore, referring to these values with e.g. config.cabal.version does not work as expected if the value uses an option property like mkIf or mkOverride. You can use cabal-config for this purpose, though.

Type: module

Default: { }

cabal-config

Evaluated version of cabal, for referencing in other config values. May not be set by the user.

Type: submodule (read only)

Default: { }

commands

Commands are shell scripts associated with an environment that are exposed as flake apps. All commands are accessible as attributes of .#cmd.<name>, and those that set expose = true are additionally exposed at the top level.

Type: attribute set of (submodule)

Default: { }

compat.enable

Create derivations in outputs.checks that build the packages with different GHC versions. The set of versions is configured by compat.versions.

Type: boolean

Default: true

compat.ifd

Whether to allow IFD for compat checks.

Type: boolean

Default: false

compat.versions

The GHC versions for which to create compat checks. Defaults to ghcVersions. There has to be an env in envs with the version as its name for each of these.

Type: list of string

Default:

[
  "ghc94"
  "ghc96"
  "ghc98"
  "ghc910"
]
compiler

The GHC version used for internal tasks and for the default environment. This is an attribute name in the nixpkgs set haskell.packages, which is usually in the format ghc96.

Type: string

Default: "ghc98"

deps

Flake inputs containing hix projects whose overrides are merged into this project’s. The local overrides are ignored to prevent the dependencies’ project packages from being injected into the compat checks.

Type: list of path

Default: [ ]

depsFull

Flake inputs containing hix projects whose overrides are merged into this project’s. Unlike deps, this includes the local overrides.

Type: list of path

Default: [ ]

devGhc

Backwards-compat alias for envs.dev.ghc.

Type: submodule (read only)

envs

All environments for this project.

Type: attribute set of (submodule)

Default: { }

exportedOverrides

These overrides are exposed from the flake for integration in downstream projects via the options deps and depsFull.

This is an attrset whose keys indicate where to put the overrides in the dependent project – each version env and the dev env has their own, while the all key is applied globally. The special keys local and localMin contain the local packages and their minimal build variants, respectively. Local packages are only propagated when depsFull is used.

Type: lazy attribute set of Haskell package override function specified in the Hix DSL

Default: { }

forceCabal2nix

Whether to use cabal2nix even if there is no Cabal file.

Type: boolean

Default: false

forceCabalGen

Whether to generate a Cabal file from Nix config even if there is one in the source directory.

Type: boolean

Default: false

gen-overrides.enable

The flake app .#gen-overrides collects all cabal2nix-based derivations from the overrides that would require IFD when computed on the fly.

Setting this flag instructs Hix to read the generated derivations when building, and to abort the build when they are missing or outdated.

Type: boolean

Default: false

gen-overrides.file

The relative path of the file in which the overrides are stored.

Type: string

Default: "ops/overrides.nix"

gen-overrides.gitAdd

Git-add the overrides file after the first run. Since nix ignores untracked files in flakes, the next command would complain if the file wasn’t added.

Type: boolean

Default: true

ghcVersions

The GHC versions for which to create envs, specified by their attribute names in pkgs.haskell.packages.

Type: list of string

Default:

[
  "ghc94"
  "ghc96"
  "ghc98"
  "ghc910"
]
haskellTools

Function returning a list of names of Haskell packages that should be included in every environment’s $PATH. This is a convenience variant of buildInputs that provides the environment’s GHC package set (without overrides) as a function argument. This is intended for tooling like fourmolu. The default consists of cabal-install, since that’s a crucial tool most users would expect to be available. If you want to provide a custom cabal-install package, you’ll have to set haskellTools = lib.mkForce (...), since the built-in definition doesn’t use mkDefault to ensure that adding tools in your project won’t mysteriously remove cabal-install from all shells.

Type: function that evaluates to a(n) list of package

Example: ghc: [ghc.fourmolu]

hls.genCabal

When running HLS with nix run .#hls, the command first generates Cabal files from Hix config to ensure that HLS works. If that is not desirable, set this option to false.

Type: boolean

Default: true

ifd

Whether to use cabal2nix, which uses Import From Derivation, or to generate simple derivations, for local packages.

Type: boolean

Default: false

inheritSystemDependentOverrides

Overrides can be exported without a dependency on a system, such that a dependent could use them even if the dependency wasn’t declared for the desired system. However, if ifd is false in the dependency, the local packages will need the system option, and therefore need to be imported from legacyPackages.<system>.overrides.

Type: boolean

Default: true

inputs

The inputs of the Hix flake.

Type: lazy attribute set of unspecified value (read only)

main

The name of a key in packages that is considered to be the main package. This package will be assigned to the defaultPackage flake output that is built by a plain nix build. If this option is undefined, Hix will choose one of the packages that are not in the dependencies of any other package.

Type: string

manualCabal

Don’t use the options in packages as Cabal configuration for the ghci preprocessor.

Type: boolean

Default: false

name

The name of the project is used for some temporary directories and defaults to main. If no packages are defined, a dummy is used if possible.

Type: string

Default: "hix-project"

output.expose.appimage

Include AppImage derivations for all executables in the outputs.

Type: boolean

Default: true

output.expose.cross

Include full cross-compilation system sets in the outputs (like hix.cross.{mingw32,aarch64-android,...}).

Type: boolean

Default: true

output.expose.internals

Include the config set, GHC packages and other misc data in the outputs.

Type: boolean

Default: true

output.expose.managed

Include apps for managed dependencies in the outputs, even as stubs if the feature is disabled.

Type: boolean

Default: true

output.extraPackages

Names of packages that will be added to the flake outputs, despite not being declared in packages.

This may be a simple Hackage package like aeson or a local package that is added in overrides due to the way its source is obtained.

Type: list of string

Default: [ ]

output.final

The final flake outputs computed by Hix, defaulting to the set in outputs and its siblings. May be overriden for unusual customizations.

Type: raw value

outputs.packages

The flake output attribute packages.

Type: lazy attribute set of package

outputs.apps

The flake output attribute apps.

Type: lazy attribute set of flake output of type ‘app’

outputs.checks

The flake output attribute checks.

Type: lazy attribute set of package

outputs.devShells

The flake output attribute devShells.

Type: lazy attribute set of package

outputs.legacyPackages

The flake output attribute legacyPackages.

Type: lazy attribute set of raw value

overrides

Cabal package specifications and overrides injected into GHC package sets. Each override spec is a function that takes a set of combinators and resources like nixpkgs and should return an attrset containing either derivations or a transformation built from those combinators.

The combinators are described in the section called “Override combinators”.

Type: Haskell package override function specified in the Hix DSL

Default: [ ]

Example:

{hackage, fast, jailbreak, ...}: {
  aeson = fast (hackage "2.0.0.0" "sha54321");
  http-client = unbreak;
};

pkgs

The nixpkgs attrset used by the default GHC.

Type: nixpkgs attrset (read only)

services

Services are fragments of NixOS config that can be added to an environment to be started as virtual machines when the environment is used in a command or shell.

Type: attribute set of module

Default: { }

system

The system string like x86_64-linux, set by iterating over systems.

Type: string (read only)

systems

The systems for which to create outputs.

Type: list of string

Default:

[
  "aarch64-linux"
  "aarch64-darwin"
  "i686-linux"
  "x86_64-darwin"
  "x86_64-linux"
]

Multiple systems and IFD

A set of flake outputs is defined for a specific architecture/system combination like x86_64-linux, which is the second level key in the outputs set. When a flake command like nix build is executed, Nix determines the system identifier matching the current machine and looks up the target in that set, like packages.x86_64-linux.parser. This means that you have to define a (not necessarily) identical output set for each architecture and system you would like to support for your project.

While Hix takes care of structuring outputs for all supported systems (via flake-utils), this feature comes with a significant caveat: When any part of an output set cannot be purely evaluated because it imports a file that was created by a derivation in the same evaluation (which is called Import From Derivation, IFD), evaluating this output will cause that derivation to be built.

This becomes a critical problem when running the command nix flake check, which is a sort of test suite runner for flakes, because it evaluates all outputs before running the checks. Hix defines checks for all packages and GHC versions, so it is generally desirable to be able to run this command in CI. Unfortunately, cabal2nix (which generates all Haskell derivations) uses IFD.

On-the-fly derivations

To mitigate this problem, Hix can generate derivations in a similar manner to cabal2nix, controlled by the option ifd (on by default). For local packages, this is rather straightforward – Hix can use the package config to determine the derivations dependencies and synthesize a simple derivation.

It gets more difficult when overrides are used, since these are usually generated from Hackage or flake inputs, where the only information available is a Cabal file. In order to extract the dependencies, we would either have to run a subprocess (via IFD) or implement a Cabal config parser in Nix (which has also been done).

As a pragmatic compromise, Hix instead splits the override derivation synthesis into two separate passes:

  • The flake app gen-overrides collects all cabal2nix overrides and stores their derivations in a file in the repository.

  • Whenever a build is performed afterwards, it loads the derivations from that file, avoiding the need for IFD!

In order to use this feature, the option gen-overrides.enable must be set to true.

The derivations can be written to the file configured by gen-overrides.file with:

$ nix run .#gen-overrides

Of course this means that gen-overrides will have to be executed every time an override is changed, but don’t worry – Hix will complain when the metadata doesn’t match!

Note

This feature is experimental.

Note

If your configuration evaluates overridden packages unconditionally, the command will not work on the first execution, since it will try to read the file and terminate with an error. In that (unlikely) case, you’ll have to set gen-overrides.enable = false before running it for the first time.

Another potential source for nix flake check to fail is when an environment with a virtual machine is exposed as a shell. For that purpose, the option systems can be used to define for which systems the environment should be exposed.

User Interface

Options for UI behavior:

UI options

ui.warnings.all

Whether to enable all warnings. Set this to false to suppress all warnings and use ui.warnings.keys to enable them selectively.

Type: boolean

Default: true

ui.warnings.keys

The set of warnings that should be enabled or disabled individually. Keys are warning IDs and values are booleans, where true enables a warning and false disables it. If a key is not present in this set, the warning is printed if ui.warnings.all is true.

Type: attribute set of boolean

Default: { }

Environments and commands

Environments

Actions like building a derivation and running HLS or GHCi are executed in an environment, which is a set of configuration options consisting of:

  • A GHC at a specific version

  • A set of packages that include project dependencies and tools like Cabal and HLS

  • An optional set of services that may be run in a virtual machine, like database servers

  • Environment variables

This example configuration defines an environment that uses GHC 9.4, adds socat to the packages in $PATH and runs a PostgreSQL server:

{
  outputs = {hix, ...}: hix ({config, ...}: {
    envs.example = {

      ghc.compiler = "ghc94";

      buildInputs = [config.pkgs.socat];

      services.postgres = {
        enable = true;
        config = { name = "test-db"; };
      };

    };
  });
}

Using environments

An environment can be used in different ways: By a derivation, a devshell, or a command. A derivation only uses the GHC, Cabal and the project dependencies as inputs, while a command execution, like GHCid, is preceded by booting a VM if any services are configured. For the latter, the environment provides a script that wraps the command with the startup/shutdown code for the VM and adds all build inputs to the $PATH.

The simplest way to use an environment is as a devshell:

$ nix develop .#example
>>> Starting VM with base port 20000
>>> Waiting 30 seconds for VM to boot...
$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 9.4.3
$ psql "host=localhost port=25432 user=test-db password=test-db dbname=test-db" -c 'select 1'
?column?
----------
        1
(1 row)
$ exit
>>> Killing VM

Note

Using nix develop -c some-command to execute a shell command in an environment will fail to stop the VM afterwards. Use a command for one-shot executions.

The default environment is called dev and is used for everything that doesn’t have a custom environment. For instance, the default devshell uses this environment when entered with nix develop without an explicit argument. Running cabal build in that shell will use the configured GHC.

Configuring GHC

The most important part of an environment is the associated GHC. As demonstrated in the above example, the option ghc.compiler in an environment is used to select the package set corresponding to a version. These package sets are predefined by nixpkgs – each snapshot has a certain number of GHC versions with packages obtained from Stackage and Hackage.

The GHC executable provided by an environment comes preloaded with all dependencies declared in the project’s Cabal files (which amounts to those declared in packages). When running ghci, those dependencies are importable.

The default package set doesn’t always provide the version of a package that the project requires – for example, sometimes you want to use a newer version than the stable one in a Stackage snapshot. For this purpose, Hix offers a DSL for package overrides:

{
  overrides = {hackage, ...}: {
    streamly = hackage "0.9.0" "1ab5n253krgccc66j7ch1lx328b1d8mhkfz4szl913chr9pmbv3q";
  };
}

Overrides are defined as a function that produces a set mapping package names to dependency specifications and takes as its argument a set of combinators and metadata for declaring those specifications, like the hackage combinator used above that takes a version and Nix store hash to fetch a package directly from Hackage. They can be specified at different levels, like dependencies: At the top level for all environments, in each individual environment, and on the ghc module in an environment (although the latter is populated by Hix with the merged global and local overrides).

Note

nixpkgs’ shipped GHC package sets come with a few special derivations whose attribute names are suffixed with a concrete version, like Cabal_3_10_2_1, to be used as overrides for packages with incompatible requirements. If you create a Hackage override for such a package, there’s a chance that it will result in an infinite recursion error. The precise reason for this is not clear to me yet, but it can be avoided by using that special derivation, if it is compatible with your dependency set:

{ overrides = {super, …}: { broken-dep = super.broken-dep_1_2_3; }; }

Override combinators

There are different classes of combinators. The most fundamental ones are used to declare package sources:

  • hackage takes two arguments, version and hash, to pull a dependency directly from Hackage.

  • source.root takes a path to a source directory (like a flake input) and builds the dependency from its contents.

  • source.sub is the same as source.root, but takes a second argument describing a subdirectory of the path.

  • source.package is the same as source.sub, but it prepends packages/ to the subdirectory.

  • hackageAt is like hackage, but takes an additional first argument specifying the address of the server.

  • hackageConf is like hackageAt, but instead of an address, it takes the name of an item in hackage.repos. If a server with the key hix-override is configured, the combinator hackage will automatically use it instead of nixpkgs’ built-in logic.

  • disable or null (the literal) results in the final derivation being assigned null. This may be useful to force the dependency by that name to be provided by some other mechanism; for example, boot libraries are available in the compiler package and are set to null in nixpkgs.

When an override is defined using source combinators, the derivation is generated by cabal2nix. Consider the following override for a package named dep that uses a flake input as the source path:

{
  inputs = {
    hix.url = "...";
    dep.url = "...";
  };
  outputs = {hix, dep, ...}: hix {
    overrides = {source, ...}: {
      dep = source.root dep;
    };
  };
}

The implementation of source.root roughly consists of the function call haskellPackages.callCabal2nix "dep" dep { <optional overrides>; } which invokes haskellPackages.haskellSrc2nix, which executes the program cabal2nix, which in turn analyzes the dep.cabal in the source tree of the flake input and generates a function that looks like this:

{
  cabal2nix_dep = {mkDerivation, aeson, base, pcre}: mkDerivation {
    pname = "dep";
    src = <path passed to source.root>;
    libraryHaskellDepends = [aeson base];
    librarySystemDepends = [pcre];
    ...
  }
}

Its arguments consist of dep’s Haskell and system dependencies as well as the mkDerivation function. All of these are ultimately provided by the GHC package set, and callCabal2nix wraps this generated function with the common callPackage pattern, which allows individual arguments to be overridden. For example, to sketch a simplified representation of how our local package would be merged into the nixpkgs GHC package set, the above function would be called like this:

let
  callPackage = lib.callPackageWith ghc;
  ghc = {
    # These two are defined in `pkgs/development/haskell-modules/make-package-set.nix`
    mkDerivation = pkgs.callPackage ./generic-builder.nix { ... };
    aeson = callPackage aesonDrv {};
    ...
    api = callPackage cabal2nix_dep {
      aeson = someCustomAesonDerivation; # Haskell package override
      pcre = pkgs.pcre; # System package override
    };
  };
in ghc

Now, this is already very convoluted, and the real thing is even worse; but you can see:

  • The callPackage variant used for the Haskell derivations is created with lib.callPackageWith by recursively providing the set ghc, which consists of all the lazy attributes that are themselves constructed with that same function. Just like most of nixpkgs, the fixpoint of this computation is the final value of ghc, allowing each package to depend on others from the same set without having to provide the deps directly.

  • mkDerivation is also created with a callPackage variant, but it’s the top-level nixpkgs set that provides the values.

  • mkDerivation will be passed to the calls in aeson and dep automatically.

  • The call to our cabal2nix derivation created by source would get the sibling attribute aeson, but the second argument to callPackage here contains some custom override (as well as another for pcre), which takes precedence over the “default arguments” in the recursive set. Now, of course, these overrides need to be specified somehow, and the mechanism for that is the third argument to callCabal2nix that is labeled with <overrides> in the above example. However, this argument is not directly exposed in source.root. Instead, another type of override combinator provides that functionality, described below (cabal2nixOverrides).

Other combinators

The second class consists of “transformers”, which perform some modification on a derivation. They can either be applied to a source combinator or used on their own, in which case they operate on whatever the previous definition of the overridden package is (for example, the default package from nixpkgs, or an override from another definition of overrides).

Transformers also compose – when using multiple of them in succession, their effects accumulate:

{
  overrides = {hackage, notest, nodoc, configure, jailbreak, nobench, ...}: {
    # Fetch streamly from Hackage and disable tests and haddock
    streamly = notest (nodoc (hackage "0.9.0" "1ab5n253krgccc66j7ch1lx328b1d8mhkfz4szl913chr9pmbv3q"));

    # Use the default aeson package and disable tests
    aeson = notest;

    # Disable version bounds, tests, benchmarks and Haddock, and add a configure flag
    parser = configure "-f debug" (jailbreak (notest (nobench nodoc)));
  };
}

The available transformers are:

  • unbreak – Override nixpkgs’ “broken” flag

  • jailbreak – Disable Cabal version bounds for the package’s dependencies

  • notest – Disable tests

  • nodoc – Disable Haddock

  • bench – Enable benchmarks

  • nobench – Disable benchmarks

  • minimal – Unbreak and disable profiling libraries, Haddock, benchmarks and tests

  • fast – Disable profiling and Haddock

  • force – Unbreak, jailbreak and disable Haddock, benchmarks and tests

  • noprofiling – Disable profiling libraries for the package

  • profiling – Enable executable profiling for the package

  • configure – Add a Cabal configure CLI option

  • configures – Add multiple Cabal configure CLI options (in a list)

  • enable – Enable a Cabal flag (-f<flag>)

  • disable – Disable a Cabal flag (-f-<flag>)

  • ghcOptions – Pass GHC options to Cabal (--ghc-options)

  • override – Call overrideCabal on the derivation, allowing arbitrary Cabal manipulation

  • overrideAttrs – Call overrideAttrs on the derivation

  • buildInputs – Add Nix build inputs

The third class consists of “options”, which modify the behavior of other override combinators:

  • cabal2nixOverrides – An attrset used as the third argument of callCabal2nix when called by source combinators, as described in the example in the previous section, used as follows:

    {
      overrides = {cabal2nixOverrides, source, pkgs}: {
        dep = cabal2nixOverrides { aeson = ...; pcre = pkgs.pcre.override {...}; } (source.root dep);
      }
    }
    

    This would result in both aeson and pcre being overridden, for the derivation of dep, by the packages supplied explicitly.

  • cabal2nixArgs – CLI options for cabal2nix.

    cabal2nix doesn’t have many options that would make a lot of sense in this context, but one is particularly useful:

    Like cabal-install, cabal2nix recognizes the option -f to enable or disable Cabal flags, which are often used to configure optional dependencies. The problem with passing these only to Cabal in the final derivation is that the function generated by cabal2nix will require exactly those dependencies that were enabled when it was invoked.

    Imagine that our example package has a Cabal flag large-records that controls two effects: an additional dependency on the package large-records and some CPP-guarded code that uses this library. Disabling a flag with disable "large-records" (source.root dep) will ultimately cause Cabal to build dep without large-records visible in the GHC package DB, as well as ignore the additional code, but the derivation will still have the dependency on large-records, since that is not interpreted by Cabal. However, cabal2nixArgs "-f-large-records" (source.root dep) will avoid the dependency entirely.

Finally, there are some special values that are injected into the override function:

  • self and super – The final and previous state of the package set, as is common for nixpkgs extension functions

  • pkgs – The nixpkgs set

  • keep – Reset the previous combinators and use the default package

  • hsLib – The entire nixpkgs tool set haskell.lib

  • hsLibC – The entire nixpkgs tool set haskell.lib.compose. This is preferable to hsLib, but for historic reasons that one ended up in here first, and it’s gonna stay for compatibility.

  • compilerName – The name attribute of the GHC package

  • compilerVersion – The version attribute of the GHC package

Transitive overrides

Overrides can be exported from the flake in order to reuse them in other projects. When a downstream flake has project foo as an input, setting deps = [foo] will cause foo’s overrides to be incorporated into the local ones. Furthermore, the option depsFull will additionally include foo’s local packages in the overrides:

{
  inputs.dep1.url = github:me/dep1;
  inputs.dep2.url = github:me/dep2;

  outputs = { hix, dep1, dep2, ... }: hix {
    deps = [dep1];
    depsFull = [dep2];
  };
}

Configuring nixpkgs

The GHC package set uses the same nixpkgs snapshot that is also used for Hix internals, which is configured as a flake input of the Hix repository. You can override this globally:

{
  inputs.hix.inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-23.05";
}

In order to avoid incompatiblities with the Hix internals, it might be advisable to only override the nixpkgs used by GHC:

{
  inputs.hix.url = "github:tek/hix";
  inputs.ghc_nixpkgs.url = "github:nixos/nixpkgs/nixos-23.05";

  outputs = { hix, ghc_nixpkgs, ... }: hix {
    envs.dev.ghc.nixpkgs = ghc_nixpkgs;
  };
}

Since the usage of nixpkgs within the library is tightly interwoven with the GHC package set, this might have a slight potential for breakage, but (like the global variant) it should be minimal.

Commands

Services in an environment are relevant when executing a command, which consists of an arbitrary shell script associated with an environment. When the command is executed, the environment’s code option will be run beforehand, which boots the VM with the services and sets up an exit hook for shutting it down. code is a script assembled from other options, notably setup-pre, setup, exit-pre and exit.

A command can be defined as follows:

{
  commands.integration-test = {
    env = "example";
    command = "cabal test api-integration";
  };
}

This assumes that your project contains some Cabal component named api-integration that needs a PostgreSQL server running. When executing this command, the setup code from the previously defined environment example will be executed, starting the virtual machine, before running the specified Cabal command line:

nix run .#cmd.integration-test

Component-dependent environments

When the command option component is set to true, the command will take two argument lists, separated by a --. The first batch of arguments is passed to the Hix CLI to select the environment, the second one is assigned to the environment variable $cmd_args to be used by the command script.

This allows the user to select an environment dynamically from the command line, without having to statically associate it in the Nix config or use complex expressions with nested invocations of Nix, realized by encoding all components and environments as JSON and extracting the environment runner based on the CLI arguments. The primary use case for this is to use a different environment when running a GHCid test, like running a database server for integration tests.

There are three alternative selection methods, illustrated by this example:

{
  description = "hix test project";
  inputs.hix.url = "github:tek/hix?ref=0.8.0";
  outputs = {hix, ...}: hix ({config, ...}: {
    envs = {
      one.env = { number = 1; };
      two.env = { number = 2; };
      three.env = { number = 3; };
    };

    packages.root = {
      src = ./.;
      executable.env = config.envs.two;
    };

    commands.number = {
      env = config.envs.one;
      command = ''
      echo $number
      '';
      component = true;
    };

  });
}

The first method is to use the flake app path to select an environment by name, then append the command name:

$ nix run .#env.three.number
3

The second method is to select a component by its package name or directory and component name or source directory:

$ nix run .#cmd.number -- -p api -c server
$ nix run .#cmd.number -- -p packages/api -c app
2

The third method is to specify a Haskell source file and let Hix figure out which component it belongs to:

$ nix run .#cmd.number -- -f /path/to/packages/api/test/NumberTest.hs
2

This method needs to know the root directory of the project, which is determined by searching for flake.nix with a fallback to the current working directory. The root directory may also be specified explicitly using the CLI option --root.

If no selection arguments are given, the command’s default environment is used:

$ nix run .#cmd.number
1

Built-in commands

Hix provides two special commands for executing a function in GHCi or GHCid.

The ghcid command in particular is highly useful for repeatedly executing a test whenever a source file is written. Compilation is very fast since GHCi doesn’t need to link the executable and doesn’t perform optimization in the default mode, leading to sub-second feedback loops on small projects (or dependency closures).

nix run .#ghci -- -p root -r server
nix run .#ghcid -- -p root -t test_server -r hedgehog-unit

Their interface is mostly identical to the generic commands described above, while taking three additional, optional command line options:

  • The name of a module (-m), defaulting to Main. This module is loaded in GHCi and hence only its dependencies are (re)compiled.

  • The name of a Haskell test function (-t) that will be called after the module was loaded successfully. See next bullet for defaults.

  • The name of an attribute in the Hix option ghci.run (-r). This option contains a Haskell expression for each key which will be executed after the module was loaded successfully. If a test function was specified, this expression will be applied to it. If the runner wasn’t specified, the test function will be run directly. If neither this nor the test function was specified, the ghci command will drop to interactive mode directly while the ghcid command defaults to main.

For example, the built-in hedgehog-unit entry in ghci.run has the value check . withTests 1 . property . test. When specifying the name of a Hedgehog test like in the example above, the evaluated epression will be:

ghci> (check . withTests 1 . property . test) test_server

You can specify arbitrary additional command line arguments for GHCi and GHCid with --ghci-options and --ghcid-options.

Another built-in command is run, which executes an arbitrary shell command in an environment:

$ nix run .#env.ghc92.run -- "ghc --version"
The Glorious Glasgow Haskell Compilation System, version 9.2.4

Note

If you depend on the GHC library package ghc, you’ll have to set ghci.args = ["-package ghc"];. Otherwise it won’t be visible, due to a bug.

Services

Services used in environments and commands can be user-defined in a flake:

{
  services.http = {
    nixos.services.nginx = {
      enable = true;
      virtualHosts.localhost.locations."/test".return = "200 Yay!";
    };
    ports.nginx = { host = 2000; guest = 80; };
  };

  envs.test = {
    basePort = 10000;
    services = { http = { enable = true; }; };
  };
}

This defines a service named http that runs an nginx with a single endpoint at /test and exposes it in the host system at port offset 2000 (relative to an environment’s basePort). The environment test references the service, so when a command uses this environment, a VM will be started:

nix run .#env.test.ghcid -- -p root

Since the environment uses basePort = 10000, the nginx server will listen on port 12000. You can refer to the effective port from other options with config.envs.hostPorts.nginx (the attribute name in ports).

Hix provides built-in services, like the previously mentioned PostgreSQL server, that have specialized configuration options. They can be configured in the same option as the definition of a new service, still allowing the specification of additional NixOS config as described before, in services.<name>.nixos.

Furthermore, an environment may provide Hix config overrides in envs.<name>.services.<name>.config that is combined with the config in services.<name>.

{
  outputs = {hix, ...}: hix ({config, ...}: {

    services.postgres = {
      # Add NixOS config to the default config computed by Hix
      nixos.users.users.postgres.extraGroups = ["docker"];

      # Configure PostgreSQL specifically, used by `services.postgres.nixos-base` internally
      creds.user = "root";
    };

    envs.example = {

      services.postgres = {
        # Declare that this environment uses `config.services.postgres` above
        enable = true;

        # Add overrides for the configuration in `config.services.postgres`
        config = { name = "test-db"; };
      };

    };

  });
}

In order to define a service with specialized config, an entry in the option internal.services.myservice must be provided that contains a module with option declarations. This option has type deferredModule, which means that it’s not evaluated at the definition site, but used in a magic way somewhere else to create a new combined module set consisting of all the configs described before.

You can log into a service VM via ssh. The default configuration sets the root password to the empty string and exposes the ssh port in the host system at basePort + 22:

ssh -p 1022 root@localhost

Defining modular services

A service can be defined in services with plain NixOS config, but it is useful to allow the service to be specially configurable. For that purpose, the value assigned to the entry in services should be a full module that defines options as well as their translation to service config:

{
  services.greet = ({config, lib, ...}: {

    options.response = mkOption {
      type = lib.types.str;
      default = "Hello";
    };

    config = {
      ports.greet = { guest = 80; host = 10; };
      nixos-base.services.nginx = {
        enable = true;
        virtualHosts.localhost.locations."/greet".return = "200 ${config.response}";
      };
    };

  });

  envs.bye = {
    services.greet = {
      enable = true;
      config.response = "Goodbye";
    };
  };
}

This defines a service running nginx that has a single endpoint at /greet, which responds with whatever is configured in the service option response. The environment bye uses that service and overrides the response string.

Any option defined within services.greet.options is configurable from within the environment, and the values in services.greet.config correspond to the regular service options.

Environment options

enable

Whether to enable this environment.

Type: boolean

Default: false

Example: true

packages

The subset of local packages that should be built by this environment.

Entries must correspond to existing keys in packages. If the value is null, all packages are included.

This is useful when the project contains multiple sets of packages that should have separate dependency trees with different versions.

Setting this has a variety of effects:

  • These packages will be exposed as outputs for this env

  • The GHC package db will not contain the excluded packages, so they won’t be built when entering a shell or starting .#ghci for this env

  • Managed dependencies envs use this to produce separate sets of bounds

Type: null or (list of name of a package defined in config.packages)

Default: null

basePort

The number used as a base for ports in this env’s VM, like ssh getting basePort + 22.

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Default: 20000

buildInputs

Additional system package dependencies for this environment.

Note

These are only made available to shells and commands, not added to packages, like when they are set in overrides.

Type: (function that evaluates to a(n) list of package) or list of package

Default: [ ]

code

The shell script code that starts this env’s services and sets its environment variables.

Type: string

Default:

''
  _hix_unrestricted() {
    [[ -z ''${DIRENV_IN_ENVRC-} ]] && [[ -z ''${HIX_ONLY_ENV-} ]]
  }
  quitting=0
  quit() {
    if [[ $quitting == 0 ]]
    then
      quitting=1
      if [[ -n ''${1-} ]]
      then
        echo ">>> Terminated by signal $1" >&2
      fi
      
      
      
      # kill zombie GHCs
      /nix/store/nyqwmqlnnhq3x5718309wrkif0wrppvp-procps-4.0.4/bin/pkill -9 -x -P 1 ghc || true
    fi
    if [[ -n ''${1-} ]]
    then
      exit 1
    fi
  }
  if _hix_unrestricted
  then
    if [[ -z ''${_hix_is_shell-} ]]
    then
      trap "quit INT" INT
    fi
    trap "quit TERM" TERM
    trap "quit KILL" KILL
    trap quit EXIT
  fi
  
  export PATH="/nix/store/0px0carb0vl8bzjviz3siqa7asq97a5x-cabal-install-3.14.1.0/bin:/nix/store/x6zmmsvwyfa1yxgpzl0dlkdllcd8jckm-ghcid-0.8.9-bin/bin:/nix/store/d9v1lxs84wq29796v1s8b558g9kc5cf5-ghc-9.8.4/bin:$PATH"
  export env_args
  if _hix_unrestricted
  then
    :
    
    
    
    
    
  fi
''
defaults

Whether to use the common NixOS options for VMs.

Type: boolean

Default: true

env

Environment variables to set when running scripts in this environment.

Type: attribute set of (signed integer or string)

Default: { }

exit

Command to run when the env exits.

Type: string

Default: ""

exit-pre

Command to run before the service VM is shut down.

Type: string

Default: ""

expose

The parts of this environment that should be accessible as flake outputs, like being able to run nix build .#<env>.<package>. If the value is boolean, all parts are affected. If it is a set, submodule options configure the individual parts.

Type: boolean or (submodule)

Default: false

ghc

The GHC configuration for this environment.

Type: submodule

Default: { }

ghcWithPackages

The fully configured GHC package exposing this environment’s dependencies.

Type: package (read only)

ghcWithPackagesArgs

Additional arguments to pass to ghcWithPackages.

Type: attribute set of unspecified value

Default: { }

ghcid.enable

Whether to enable GHCid for this env.

Type: boolean

Default: true

Example: true

ghcid.package

The package for GHCid, defaulting to the one from the env’s GHC without overrides.

Type: package

Default: <derivation ghcid-0.8.9>

haskellPackages

Names of Haskell packages that should be added to this environment’s GHC’s package db, making them available for import. These may include the local packages.

Type: (function that evaluates to a(n) list of package) or list of string

Default: [ ]

haskellTools

Function returning a list of names of Haskell packages that should be included in the environment’s $PATH. This is a convenience variant of buildInputs that provides the environment’s GHC package set (without overrides) as a function argument. This is intended for tooling like fourmolu.

Type: function that evaluates to a(n) list of package

Default: <function>

Example: ghc: [ghc.fourmolu]

hls.enable

Whether to enable HLS for this env.

Type: boolean

Default: false

Example: true

hls.package

The package for HLS, defaulting to the one from the env’s GHC without overrides.

Type: package

Default: <derivation haskell-language-server-2.9.0.0>

hoogle

Whether to enable Hoogle in this environment.

Type: boolean

Default: false

hostPorts

The effective ports of the VM services in the host system. Computed from basePort and ports.

Type: attribute set of 16 bit unsigned integer; between 0 and 65535 (both inclusive) (read only)

ifd

Whether to use cabal2nix, which uses Import From Derivation, or to generate simple derivations.

Type: boolean

Default: false

libraryProfiling

Whether to build local libraries with profiling enabled. This is the default mode for Haskell derivations.

Type: boolean

Default: true

localDeps

Whether to add the dependencies of the env’s local packages to GHC’s package db.

Type: boolean

Default: true

localPackage

A function that takes override combinators and a derivation and returns a modified version of that derivation. Called for each cabal2nix derivation of the local packages before inserting it into the overrides. Like overrides, but applies too all packages when building with this env.

Type: unspecified value

Default: <function>

Example:

{ fast, nobench, ... }: pkg: nobench (fast pkg);

main

The name of the main package of this env, defaulting to main if that is in packages or one of those packages determined by the same method as described for the global equivalent.

Type: null or string

Default: null

managed

Whether this env’s dependencies are the section called “Automatic dependency management”. This has the effect that its bounds and overrides are read from the managed state in managed.file.

Type: boolean

Default: false

name

Name of this environment.

Type: string

Default: "<name>"

overrides

Like overrides, but used only when this environment is used to build packages.

Type: Haskell package override function specified in the Hix DSL

Default: [ ]

profiling

Whether to build local libraries and executables with profiling enabled.

Type: boolean

Default: false

runner

An executable script file that sets up the environment and executes its command line arguments.

Type: path

Default: <derivation env--name--runner.bash>

services

Services for this environment.

Type: attribute set of (submodule)

Default: { }

setup

Commands to run after the service VM has started.

Type: string

Default: ""

setup-pre

Commands to run before the service VM has started.

Type: string

Default: ""

shell

The shell derivation for this environment, starting the service VM in the shellHook.

Note

If this shell is used with nix develop -c, the exit hook will never be called and the VM will not be shut down. Use a command instead for this purpose.

Type: package

systems

The architecture/system identifiers like x86_64-linux for which this environment works. This is used to exclude environments from being exposed as shells when they are system-specific, for example when using a VM that only works with Linux. If those shells were exposed, the command nix flake check would fail while evaluating the devShells outputs, since that doesn’t only select the current system.

If set to null (the default), all systems are accepted.

Type: null or (list of string)

Default: null

vm.enable

Whether to enable the service VM for this env.

Type: boolean

Default: false

Example: true

vm.derivation

The VM derivation

Type: path

vm.dir

Type: string

Default: "/tmp/hix-vm/app/<name>"

vm.exit

Commands for shutting down the VM.

Type: string

vm.headless

VMs are run without a graphical connection to their console. For debugging purposes, this option can be disabled to show the window.

Type: boolean

Default: true

vm.image

The path to the image file.

Type: string

Default: "/tmp/hix-vm/app/<name>/vm.qcow2"

vm.monitor

The monitor socket for the VM.

Type: string

Default: "/tmp/hix-vm/app/<name>/monitor"

vm.name

Name of the VM, used in the directory housing the image file.

Type: string

Default: "<name>"

vm.pidfile

The file storing the qemu process’ process ID.

Type: string

Default: "/tmp/hix-vm/app/<name>/vm.pid"

vm.setup

Commands for starting the VM.

Type: string

vm.system

The system architecture string used for this VM, defaulting to system.

Type: string

Default: "x86_64-linux"

wait

Wait for the VM to complete startup within the given number of seconds. 0 disables the feature.

Type: signed integer

Default: 30

GHC options

compiler

The attribute name for a GHC version in the set haskell.packages.

Type: string

crossPkgs

This option can be used to override the pkgs set used for the Haskell package set, for example an element of pkgsCross: envs.dev.ghc.crossPkgs = config.envs.dev.ghc.pkgs.pkgsCross.musl64

Type: nixpkgs attrset

gen-overrides

Allow this GHC to use pregenerated overrides. Has no effect when gen-overrides.enable is false.

Disabled by default, but enabled for GHCs that are defined in an environment.

Type: boolean

Default: false

ghc

The package set with overrides.

Type: Haskell package set (read only)

name

A unique identifier of the package set.

Type: string

nixpkgs

The path to a nixpkgs source tree, used as the basis for the package set.

This can be a flake input or a regular type of path, like the result of fetchGit.

Type: nixpkgs snapshot

nixpkgsOptions

Additional options to pass to nixpkgs when importing.

Type: attribute set of unspecified value

overlays

Additional nixpkgs overlays.

Type: list of (overlay)

overrides

The overrides used for this package set – see the section called “Configuring GHC” for an explanation.

This option is set by environments (see the section called “Environments”), but GHC modules can be used outside of environments, so this might be set by the user.

Type: Haskell package override function specified in the Hix DSL

Default: [ ]

pkgs

The nixpkgs set used for this GHC.

Type: nixpkgs attrset

vanillaGhc

The package set without overrides.

Type: Haskell package set (read only)

version

The GHC version as a canonical string, like 9.2.5, for use in conditions.

Type: string (read only)

Command options

command

The script executed by this command.

Type: string

component

Whether this command should determine the env based on a target component specified by command line arguments.

Note

The component selector chooses a default component when no arguments are given. If that component has an explicit environment configured, it will be used instead of the one configured in this command.

Type: boolean

Default: false

env

The default env for the command.

Type: name of an environment defined in config.envs

Default: "dev"

expose

Whether this command should be a top-level flake app.

Type: boolean

Default: false

ghci.enable

Create a command that runs GHCi (like the built-in command) with some static options. For example, you can specify a runner, and the app will be equivalent to running nix run .#ghci -r <runner>.

Type: boolean

Default: false

ghci.package

The name of the package passed to the GHCi runner with -p.

Type: null or string

Default: null

ghci.component

The name of the component passed to the GHCi runner with -c.

Type: null or string

Default: null

ghci.ghcid

Whether to run this command with GHCid instead of plain GHCi.

Type: boolean

Default: false

ghci.module

The name of the module passed to the GHCi runner with -m.

Type: null or string

Default: null

ghci.runner

The name of a runner in ghci.run and ghci.run.

Type: null or string

Default: null

name

Name

Type: string

Default: "<name>"

GHCi(d) options

ghci.args

The command line arguments passed to GHCi. Setting this option appends to the defaults, so in order to replace them, use mkForce. To only override basic GHC options like -Werror, use ghci.ghcOptions.

Type: list of string

ghci.cores

The value for the GHC option -j, specifying the number of system threads to use.

Type: signed integer or string

Default: "\${NIX_BUILD_CORES-}"

ghci.ghcOptions

Command line arguments passed to GHCi that aren’t related to more complex Hix config like the preprocessor.

This option is initialized with values that use the Nix setting cores to set the number of threads GHCi should use. If you want to control this yourself, use mkForce here.

Type: list of string

Default: [ ]

ghci.preprocessor

The preprocessor script used to insert extensions and a custom Prelude into source files. This is generated by Hix, but may be overridden.

Type: path

ghci.run

Test functions for GHCi commands. The attribute name is matched against the command line option -r when running apps like nix run .#ghci.

Type: attribute set of string

ghci.setup

Scripts that should be executed when starting a GHCi command, like imports. The attribute name is matched against the command line option -r when running apps like nix run .#ghci.

Type: attribute set of string

Service options

enable

Enable this service

Type: boolean flag that uses conjunction for merging

Default: true

messages

Informational messages that will be echoed when an environment starts this service.

Type: function that evaluates to a(n) list of string

Default: <function>

nixos

NixOS config used for the service VM.

Type: module

Default: { }

nixos-base

NixOS base config used for the service VM.

Type: module

Default: { }

ports

Simple ports forwarded relative to the env’s basePort.

Type: attribute set of (submodule)

Default: { }

ports.<name>.absolute

Whether the host port is an absolute number. If false (default), the port is added to basePort.

Type: boolean

Default: false

ports.<name>.guest

Port used in the VM.

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

ports.<name>.host

Port exposed in the system, relative to the env’s basePort unless ports.<name>.absolute is set.

Type: 16 bit unsigned integer; between 0 and 65535 (both inclusive)

Other tools

Haskell Language Server

Hix provides a flake app for running HLS with the proper GHC (for the dev env) and the project dependencies:

nix run .#hls

In order to use it with your IDE, you need to specify the command in the editor configuration as: nix run .#hls --, since nix consumes all options until a -- is encountered, and only passes what comes afterwards to the program (which in this case would be --lsp).

This app corresponds to the command named hls, which uses the dev environment but gets the HLS executable from a special environment named hls. This allows the HLS package and its dependencies to be configured separately from the project dependencies. For example, to use the package exposed from the HLS flake:

{
  inputs.hls.url = "github:haskell/haskell-language-server?ref=1.9.0.0";

  outputs = {hix, hls, ...}: ({config, ...}: {
    envs.hls.hls.package = hls.packages.${config.system}.haskell-language-server-925;
  });
}

Additionally, all other environments can expose HLS as well:

{
  envs.ghc94.hls.enable = true;
}
nix run .#env.ghc94.hls

This is disabled by default to avoid building HLS for environments whose GHCs don’t have its derivation in the Nix cache.

Since the dev environment exposes HLS by default, the executable (haskell-language-server) is in $PATH in the default devshell, so it can also be run with nix develop -c haskell-language-server.

GHC version checks

The environments created for each entry in ghcVersions are intended primarily as a CI tool to ensure that the project builds with versions other than the main development GHC. For that purpose, the checks output contains all packages across those environments, which can be built with:

nix flake check

Hackage upload

Hix provides flake apps that run the flake checks and upload package candidates, releases or docs to Hackage:

nix run .#candidates
nix run .#release
nix run .#docs

If versionFile is set, the script will substitute the version: line in that Cabal file after asking for the next version. If you use nix run .#gen-cabal to maintain the Cabal files, this should be a .nix file containing a string that’s also used for version:

{
  packages.parser = {
    cabal.version = import ./parser-version.nix;
    versionFile = ./parser-version.nix;
  };
}

The options hackage.versionFileExtract and hackage.versionFileUpdate can be customized to allow for arbitrary other formats.

The command line option --version/-v may be used to specify the version noninteractively. Furthermore, if a package name is specified as a positional argument, only that package will be uploaded.

For example, to publish parser at version 2.5.0.1:

nix run .#release -- parser -v 2.5.0.1

The upload command will use your global Cabal config to obtain credentials, please consult the Cabal docs for more.

The options module hackage.hooks provides a way to execute scripts at certain points in the release process.

Hackage options

hackage.packages

The set of packages that will be published to Hackage when the release command is run without arguments. If it is null, all packages are published. The items in the list should be Cabal package names as defined in options.packages.

Type: null or (list of string)

Default: null

hackage.add

When hackage.add is set to false, this option can be enabled to git-add the files but not commit them.

Type: boolean

Default: false

hackage.allPackages

There are two modes for versioning: Either all packages share the same version, in which case the release app will publish all packages at the same time, or each package has an individual version, in which case the release app expects the name of a package to be specified.

Type: boolean

Default: true

hackage.askVersion

Whether to interactively query the user for a new version when releasing.

Type: boolean

Default: true

hackage.cabalArgs

Extra global CLI arguments for cabal.

Type: string

Default: ""

hackage.cabalUploadArgs

Extra CLI arguments for cabal upload to use in hackage.uploadCommand.

Type: string

Default: ""

hackage.check

Whether to run nix flake check before the release process.

Type: boolean

Default: true

hackage.commit

After successfully uploading a new release, the changes to the version file, cabal files and changelog will be committed unless this is set to false.

Type: boolean

Default: true

hackage.commitExtraArgs

Extra CLI options for git commit.

Type: string

Default: ""

hackage.confirm

Whether to ask for confirmation before uploading.

Type: boolean

Default: true

hackage.formatTag

Function that creates a tag name from a version and an optional package name.

Type: function that evaluates to a(n) string

Default: <function, args: {name, version}>

hackage.hooks.postCommitAll

Shell script lines (zsh) to run after commiting the version change after publishing all packages.

Type: strings concatenated with “\n”

Default: ""

hackage.hooks.postUploadAll

Shell script lines (zsh) to run after uploading all packages.

Value is a function that gets the set {source, publish}, two booleans that indicate whether the sources (or only docs) were uploaded, and whether the artifacts were published (or just candidates).

Type: function that evaluates to a(n) strings concatenated with “\n”

Default: <function>

hackage.hooks.preCommitAll

Shell script lines (zsh) to run before commiting the version change after publishing all packages.

Type: strings concatenated with “\n”

Default: ""

hackage.repos

Hackage repos used by the CLI for several tasks, like resolving managed dependencies and publishing packages and revisions. The default config consists of the usual server at hackage.haskell.org.

Type: attribute set of (submodule)

Default: { }

hackage.repos.<name>.enable

Whether to enable this Hackage server.

Type: boolean

Default: true

hackage.repos.<name>.description

Arbitrary short text for presentation, like ‘local Hackage’.

Type: null or string

Default: null

hackage.repos.<name>.indexState

When resolving, use the index at this time.

Type: null or string

Default: null

Example: "2024-01-01T00:00:00Z"

hackage.repos.<name>.keys

Security keys for this server.

Type: null or (non-empty (list of string))

Default: null

hackage.repos.<name>.location

Server URL with scheme and optional port.

Type: string

Default: "https://hackage.haskell.org"

Example: "https://company-hackage.com:8080"

hackage.repos.<name>.name

Name used to refer to this server in other components. Uses the attribute name and cannot be changed.

Type: string (read only)

hackage.repos.<name>.password

Password for uploading.

Type: null or string or (submodule)

Default: null

hackage.repos.<name>.publish

Publish packages to this server.

Type: boolean

Default: false

hackage.repos.<name>.secure

Use the newer Cabal client that verifies index signatures via hackage-security.

Type: null or boolean

Default: true

hackage.repos.<name>.solver

Use this server for the Cabal resolver when managing dependency versions.

Type: boolean

Default: true

hackage.repos.<name>.user

User name for uploading.

Type: null or string

Default: null

hackage.setChangelogVersion

Whether to substitute the word ‘Unreleased’ with the new version in changelogs.

Type: boolean

Default: false

hackage.tag

After successfully uploading a new release, a tag with the version name will be created unless this is set to false.

Type: boolean

Default: true

hackage.tagExtraArgs

Extra CLI options for git tag.

Type: string

Default: ""

hackage.uploadCommand

The command used to upload a tarball, specified as a function that takes a set as a parameter with the attributes:

{
  publish = "Boolean indicating whether this is a candidate or release";
  doc = "Boolean indicating whether this is a source or doc tarball";
  path = "The tarball's file path";
}

Type: function that evaluates to a(n) string

hackage.versionFile

If multiple packages use the same file for the version (like when using shared hpack files) this option may point to that file. If hackage.allPackages is true and this option is null, the version will not be modified by the release app. If the project uses the feature for hpack config synthesis from nix expressions, the version must be defined in a nix file. In that case, the simplest mechanism would be to use a separate file that only contains a string and is integrated into the config with version = import ./version.nix;. The default version handlers make this assumption; if a different method is used, the options hackage.versionFileExtract and hackage.versionFileUpdate must be adapted.

Type: null or string

Default: null

hackage.versionFileExtract

A function that returns a shell script fragment that extracts the current version from a version file. The default assumes hpack/cabal format, like version: 5, unless the file has the extension .nix, in which case it is assumed the file only contains a string.

Type: function that evaluates to a(n) string

Default: <function>

hackage.versionFileUpdate

A function that returns a shell script fragment that updates the current version in a version file. The new version is stored in the environment variable $new_version in the surrounding shell script. The default assumes hpack/cabal format, like version: 5, unless the file has the extension .nix, in which case it is assumed the file only contains a string.

Type: function that evaluates to a(n) string

Default: <function>

CTags

Hix exposes an app that runs thax to generate a CTags file covering all dependencies and the local packages:

nix run .#tags

This will result in the creation of the file .tags.

Cross-compilation and static linking

All package outputs can be cross-compiled with the following syntax:

nix build .#parser.cross.musl64

In addition, the package may also be linked statically against its Haskell dependencies:

nix build .#parser.cross.musl64.static

Note that this does not result in a static binary – it will still be linked dynamically against libc.

For more elaborate cross-compilation setups, each GHC can be configured to use a cross pkgs set:

{
  envs.dev.ghc.crossPkgs = config.envs.dev.ghc.pkgs.pkgsCross.musl64;
}

For musl, there are two native package sets in nixpkgs that are supported by Hix:

nix build .#parser.musl

This will result in a binary that’s similar to .#parser.cross.musl64.static.

For a fully static build, you can use the static attribute:

$ nix build .#parser.static
$ file result/bin/parser
result/bin/parser: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped

AppImage bundles

Hix can create more portable distributable bundles by using nix-appimage to generate AppImage executables, exposed in the following flake apps:

nix run '.#appimage' # The default executable from the default package
nix run '.#<pkgname>.appimage' # The default executable from the specified package
nix run '.#<exename>.appimage' # The specified executable from whatever package defines it
nix run '.#env.<envname>.[<pkg/exe>.]appimage' # The same as above, but from the specified env

This will print a store path:

>>> AppImage bundle for <exename> created at:
/nix/store/bxbcp9mk9rf0sjg88hxsjqzpql5is280-<exename>-0.1.0.0-x86_64.AppImage

Automatic dependency management

Hix provides some functionality for adapting and verifying dependency bounds.

Note

This feature is new, so there are likely many edge cases that have not been tested.

Upper bounds and latest versions

If the option managed.enable is enabled, the flake will expose an environment named latest and an app named bump.

Running this app with nix run .#bump will fetch the newest versions of all dependencies and create a file in the project at the path configured by managed.file, containing updated dependency version ranges that include the new version.

For each dependency, this app will build all of the project’s packages and omit the version update if the build fails. When generating cabal files or derivations, the version ranges from this file will override those from the flake.

Additionally, the app will add overrides to the same file that select the newest version for the environment latest, so that the dev environment (and all others) will still use the default versions from nixpkgs (plus regular overrides), making the latest environment the testing ground for bleeding-edge dependency versions.

You can change this behavior to apply to other environments by setting managed to true and running nix run .#env.<name>.bump instead.

If managed.check is true (the default), the derivations with latest versions will be added to the checks output, so your CI may depend on them.

Lower bounds

If the option managed.lower.enable is enabled, the flake will expose an environment named lower and an app named lower. The app executes a subset of three stages based on the current state, init, optimize and stabilize, that are also available as subcommands of this app (by running e.g. nix run .#lower optimize). The mode of operation is similar to bump, with the most crucial difference being that it manipulates the lower bounds of dependencies.

The operational details of this app that are most important for a user are that optimize determines the lowest possible bounds with which the project builds, and that stabilize attempts to repair them after the code was changed in a way that invalidates them, e.g. by importing a name that wasn’t available in a dependency at the version in the lower bound. This allows the bounds validation to be integrated into the development process by running nix run .#lower -- --stabilize as a pre-commit hook or a pre-release CI job.

Since older versions require boot packages from older GHCs, it is advisable to use the oldest GHC available. See the option managed.lower.compiler for more information. The default is to use the first version in ghcVersions.

Details

The first stage of lower is called init, and it attempts to determine initial lower bounds, which consist of the lowest version in the highest major that builds successfully with the environment’s GHC. For example, if a dependency has the majors 1.4, 1.5 and 1.6, and all versions greater than 1.5.4 fail to build, the initial lower bound will be 1.5.0. Repeatedly executing this stage will only compute initial bounds for dependencies that don’t have any yet (unless --reset is specified).

The purpose of this step is preparation for the other two apps: The initial bounds will be sufficiently recent that the project won’t break with these versions after changes with a high probability. The initial versions are stored in the managed state file along with the bounds.

Executing the app with a subcommand, nix run .#lower init, will only run this stage; otherwise, the next stage will depend on the final state and the outcome of init.

If the specified dependencies have lower bounds configured in the flake, they will be ignored.

After the lower bounds have been initialized, or if the first stage is skipped due to all dependencies having bounds, the app will build the current state to decide what to do next. If the build succeeds, it will continue with the optimize stage, otherwise it will run stabilize, although this requires the user to pass --stabilize in the command line, since it is an expensive operation with a high failure rate.

The optimize stage will iterate over the majors of all dependencies, building all versions in each of them until it finds a working one, and terminating when an entire majors fails after at least one working version had been found. E.g. if a package has the majors 1.1, 1.2, 1.3, 1.4 and 1.5, and some versions in 1.3 and 1.4 build successfully, but none in 1.3 do, then the lowest version in 1.4 will be the optimized lower bound.

Before the stabilize stage is executed, the app will first build the project with the versions that form the initial lower bounds. If that fails, it will refuse to continue, and require the user to fix the situation manually (the easiest first step would be to run nix run .#lower -- --reset). Otherwise, it will build the project with the initial lower versions and the optimized lower bound of the first dependency, continuing upwards in the version sequence until a working version is found. Then the same process is performed with the next dependency, keeping the stabilized bounds of all previously processed dependencies.

Target sets

In the default mode, the managed-deps apps operate on the entirety of local packages, finding new bounds that work for all packages. This might be undesirable – some of the packages might have stricter version requirements than others, or they might be completely independent from each other.

For that purpose, the option managed.sets may be used to specify multiple sets of packages that are processed independently.

The simplest variant, with the value managed.sets = "each";, is to create one app and one env for each package, so that you would run nix run .#bump.api to bump only the package api, with a flake check created as bump-api.

More granular sets can be specified like this:

{
  main = ["core" "api" "app"];
  other = ["docs" "compat"];
}

Now the apps have paths like nix run .#lower.init.main, while the checks use the schema lower-main-api.

The default value for sets is "all", which processes all packages at once as described above. If your packages are dependent on each other, this might be more desirable, since it reduces the build time.

Fine tuning

This feature is incredibly complicated and suffers from vulnerabilities to myriad failure scenarios, some of which might be mitigated by configuration.

The app uses the Cabal solver to optimize the selection of overrides, which requires two package indexes:

  • A source package database of available packages with their versions and dependencies, which is read from Hackage.

  • An installed package index of concrete versions of packages that are considered to be bundled with the compiler, which is provided as the executable of a nixpkgs GHC with a selection of packages.

The latter of those is a very difficult mechanism that serves multiple independent purposes.

In order to make installed packages visible to Cabal, the Nix GHC has to be outfitted with a package DB, which is the same structure used for development shells with GHCi, obtained by calling ghcWithPackages and specifying the dependencies from the flake config as the working set. This results in a GHC executable that has those dependencies “built in”, meaning that you can just import them without any additional efforts, and the solver can query them by executing ghc-pkg list.

Since we’re required to ultimately build the project with Nix, a major concern is to avoid rebuilding dependencies that are already available in the central cache. If a version selected by the solver matches the one present in the GHC set, we therefore don’t want to specify an override (to pull the version from Hackage and build it), since that version is very likely cached.

However, the GHC package set from nixpkgs always has the potential of containing broken packages, most often due to incompatible version bounds, or simply because a package fails to build with a bleeding-edge GHC (not to mention any requirements for custom build flags that your project might have). Even though this is a problem that the Cabal solver is intended to, uh, solve, the GHC set must be fully working before starting the app, since that is how Nix works – we pass a store path to the GHC with packages to the app as a CLI option (roughly).

Now, the set of installed packages should resemble the “vanilla” Nixpkgs as closely as possible because of the aforementioned benefit of avoiding builds of cached versions, but if a broken package requires on override, parts of the package set will differ from the vanilla state!

To complicate matters even more, the same issue arises twice when building the project with the computed overrides – once when the app tests the build, and again when the final state has been written and the environment is built by a flake check or manual invocation.

At least for test builds and bound mismatches there’s a partial mitigation in place, since the app forces a jailbreak (removal of most bounds) of all overridden dependencies, but this only covers a tiny part of the problem space.

For more direct control, you can specify overrides in the flake config. However, since the solver is supposed to work on vanilla package sets, most of the usual override sources are ignored, so there is a special option for this purpose. In addition, a project might contain several managed environments with different requirements, so each must be configurable individually, but we also don’t want to be forced to specify a common override multiple times.

To achieve this, the modules managed.envs, managed.latest.envs and managed.lower.envs allow you to specify configuration for all managed envs and all latest and lower envs, respectively. The option managed.envs.solverOverrides is used only for the solver package set (with the usual the section called “Override combinators” protocol). The actual build overrides can be defined at managed.envs.verbatim, which is equivalent to specifying regular env-specific overrides for all managed (or latest/lower) environments individually. Note that at the moment, the config in latest/lower completely overrides the more general one; they are not combined.

For example, if your project depends on type-errors, which has an insufficient upper bound on a dependency in the current Nipxkgs set for the latest GHC that prevents the set from building, you might want to specify:

{
  managed.latest.envs = {
    solverOverrides = {jailbreak, ...}: { type-errors = jailbreak; };
    verbatim.overrides = {jailbreak, ...}: { type-errors = jailbreak; };
  };
};

This will only use those overrides for latest envs used by .#bump – if some overrides should be used for lower bounds envs as well, you’d set this on managed.envs instead of managed.latest.envs. The “verbatim” config is copied to all envs that are generated for the section called “Target sets”, so with the setup from that section, this config would be equivalent to:

{
  envs = {
    latest-main.overrides = {jailbreak, ...}: { type-errors = jailbreak; };
    latest-other.overrides = {jailbreak, ...}: { type-errors = jailbreak; };
  };
};

Managed dependencies options

managed.enable

Enable managed dependencies.

Type: boolean

Default: false

managed.check

Add builds with latest versions and lower bounds to the flake checks.

Type: boolean

Default: true

managed.debug

Print debug messages when managing dependencies.

Type: boolean

Default: false

managed.envs

Options for environments generated for managed dependencies. These apply to both latest and lower environments; the modules managed.latest.envs and managed.lower.envs have precedence over them.

Type: submodule

Default: { }

managed.envs.solverOverrides

Dependency overrides for the package set used only by the solver while finding new versions. Specifying these should only be necessary if the vanilla package set contains broken packages that would prevent the managed apps from starting.

Type: Haskell package override function specified in the Hix DSL

managed.envs.verbatim

Default config for environments generated for managed dependencies. These can be overriden per-environment by specifying envs.*.<attr> like for any other environment.

Type: unspecified value

managed.file

Relative path to the file in which dependency versions should be stored.

Type: string

Default: "ops/managed.nix"

managed.forceBounds

Concrete bounds that fully override those computed by the app when generating Cabal files. This is useful to relax the bounds of packages that cannot be managed, like base, for example when the GHC used for the latest env isn’t the newest one because the dependencies are all broken right after release, but you want it to build with that version anyway.

Type: attribute set of (submodule)

Default: { }

Example:

{
  base = { upper = "4.21"; };
}

managed.forceBounds.<name>.lower

The lower bound, inclusive.

Type: null or string

Default: null

Example: "1.4.8"

managed.forceBounds.<name>.upper

The upper bound, exclusive.

Type: null or string

Default: null

Example: "1.7"

managed.generate

Whether to regenerate cabal files and override derivations after updating the project.

Type: boolean

Default: true

managed.gitAdd

Git-add the managed deps after the first run. Since nix ignores untracked files in flakes, the state wouldn’t be loaded if you forgot to add the file yourself.

Type: boolean

Default: true

managed.internal.localsInPackageDb

Whether to include local packages as source derivations in the package db used for the solver

Type: boolean

Default: false

managed.latest.compiler

The GHC version (as the attribute name in haskell.packages) that should be used for latest versions environments. The default is to use the last entry in ghcVersions, or compiler if the former is empty. It is advisable to use the latest GHC version that you want to support, since boot libraries will fail to build with different GHCs.

Type: string

Default: "ghc910"

managed.latest.envs

Options for environments generated for latest versions. These default to the values in managed.envs.

Type: submodule

Default: { }

managed.latest.envs.solverOverrides

Dependency overrides for the package set used only by the solver while finding new versions. Specifying these should only be necessary if the vanilla package set contains broken packages that would prevent the managed apps from starting.

Type: Haskell package override function specified in the Hix DSL

managed.latest.envs.verbatim

Default config for environments generated for managed dependencies. These can be overriden per-environment by specifying envs.*.<attr> like for any other environment.

Type: unspecified value

managed.latest.readFlakeBounds

Use the upper bounds from the flake for the first run.

Type: boolean

Default: false

managed.lower.enable

Enable an environment for testing lower bounds.

Type: boolean

Default: false

managed.lower.compiler

The GHC version (as the attribute name in haskell.packages) that should be used for lower bounds environments. The default is to use the first entry in ghcVersions, or compiler if the former is empty. It is advisable to use the lowest GHC version that you want to support, since boot libraries will fail to build with different GHCs.

Type: string

Default: "ghc94"

managed.lower.envs

Options for environments generated for lower bounds. These default to the values in managed.envs.

Type: submodule

Default: { }

managed.lower.envs.solverOverrides

Dependency overrides for the package set used only by the solver while finding new versions. Specifying these should only be necessary if the vanilla package set contains broken packages that would prevent the managed apps from starting.

Type: Haskell package override function specified in the Hix DSL

managed.lower.envs.verbatim

Default config for environments generated for managed dependencies. These can be overriden per-environment by specifying envs.*.<attr> like for any other environment.

Type: unspecified value

managed.mergeBounds

Add the flake bounds to the managed bounds. Aside from going in the Cabal file, they are added to Cabal’s dependency solver when finding new bounds. This can be used to avoid problematic versions that have dependencies with a high tendency to break the build. The ranges defined here are intersected with the managed bounds. If you want to relax bounds, use managed.forceBounds.

Type: boolean

Default: false

managed.quiet

Suppress informational messages when managing dependencies.

Type: boolean

Default: false

managed.sets

Select how to group packages for processing by the managed deps tool. all for a single set, each for one set per package, and an attrset for custom grouping.

Type: one of “all”, “each” or attribute set of list of name of a package defined in config.packages

Default: "all"

Example:

{
  main = ["core" "api" "app"];
  other = ["docs" "compat"];
}

managed.verbose

Print verbose messages when managing dependencies.

Type: boolean

Default: false

Miscellaneous tools

Show overrides

nix run .#show-overrides

Prints all environments’ overrides.

Show config

nix run .#show-config

Prints the project’s entire configuration.

Show dependency versions

nix run .#dep-versions
nix run .#env.ghc96.dep-versions

Prints all components’ dependencies and their actual versions in the dev environment, or the named environment in the second variant.

Access to intermediate outputs

Packages and environments are subjected to several stages of transformation in order to arrive at the final flake outputs from the initial configuration obtained from module options. In order to make this process more transparent and flexible, in particular for overriding outputs without having to reimplement substantial parts of Hix’s internals, intermediate data is exposed in the module arguments project, build, and outputs. This means that the module that constitutes the Hix flake config can access these arguments by listing them in its parameter set:

{
  outputs = {hix, ...}: hix ({config, project, build, outputs, ...}: {
    packages.core.src = ./core;
    outputs.packages.custom-build = build.packages.dev.core.static.overrideAttrs (...);
  });
}

For now, these sets don’t have a stable API, but here is a brief overview:

  • project contains values that are closely related to the config options they are computed from, to be used by a wide spectrum of consumers:

    • project.base contains the project’s base directory in the nix store, which is either base if that’s non-null, or the directory inferred from src if only the package config contains paths.

    • project.packages.*.path contains the relative path to a package, either given by relativePath if that’s non-null, or the directory inferred from project.base.

  • build contains the full set of derivations for each package in each env. Its API looks roughly like this:

    {
      packages = {
        dev = {
          core = {
            package = <derivation>;
            static = <derivation>;
            musl = <derivation>;
            cross = {
              aarch64-android = <derivation>;
              ...
            };
            release = <derivation>;
            ghc = { <package set used for this package> };
            cabal = { <cabal config for this package> };
            expose = true;
            executables = {
              <name> = {
                package = <derivation>;
                static = <derivation>;
                musl = <derivation>;
                app = <flake app>;
                appimage = <derivation>;
              };
              ...
            };
          };
          api = {
            ...
          };
          ...
        };
        ghc910 = {
          ...
        };
        ...
      };
      envs = {
        dev = {
          # Like above, but only for the main package of the set
          static = <derivation>;
          musl = <derivation>;
          cross = <attrs>;
          release = <derivation>;
          # Main executable of the main package
          api = <derivation>;
          # All executables of all packages, flattened
          executables = <attrs>;
        };
        ...
      };
      commands = {
        default = {
          # All built-in and custom commands using their default env env
          ghci = <derivation>;
          hls = <derivation>;
          ...
        };
        envs = {
          dev = {
            # All built-in and custom commands using this env
            ghci = <derivation>;
            hls = <derivation>;
            ...
          };
        };
      };
    }
    
  • outputs contains most of what will eventually end up in the flake outputs, keyed by output type.

All of this data is also exposed in the flake outputs, and can therefore be inspected from nix repl:

Welcome to Nix 2.18.5. Type :? for help.

nix-repl> :load-flake .
Added 19 variables.

nix-repl> legacyPackages.x86_64-linux.project
{
  # Values from `project`
  base = /nix/store/ga9ifpvqzqgi6sqcfqhdvhj0qmfms8hk-source;
  packages = { ... };
  # The sets `build` and `outputs`, as attributes of `project` for simpler scoping
  build = { ... };
  outputs = { ... };
  # Other internal data
  config = <full config>;
  ghc = <dev ghc set>;
  ghc0 = <dev ghc set without overrides>;
  pkgs = <dev nixpkgs>;
  show-config = <see above>;
}

nix-repl> legacyPackages.x86_64-linux.project.build.packages.dev.hix.package
«derivation /nix/store/qys3qc4kyvfx6wlsqkvjxk40dyq28gl3-hix-0.7.2.drv»

nix-repl> packages.x86_64-linux.hix
«derivation /nix/store/qys3qc4kyvfx6wlsqkvjxk40dyq28gl3-hix-0.7.2.drv»