HORIZON HASKELLDocsSearch packagesCtrl K
Theme: |

9 sites

Configuration

About this page

A horizon.vx file describes a Haskell project or package set: the set it builds on, the packages it adds and who owns its flake.nix. horizon-config reads the file and writes the Nix that builds it.

This page describes the language of horizon-config 0.15.0 as interpreted by horizon-config-shell 0.24.0. The first sections walk through a minimal and a fuller file; the reference after them lists every statement. For a first project, start with the QuickStart.

Every command on this page runs the released generator:

nix run 'git+https://nixlab.openfp.net/horizon/horizon-config-shell?ref=refs/tags/0.24.0' -- update horizon.vx

The page writes this as horizon-config update horizon.vx. The executable is horizon-config.

Before you start

You need Nix with flakes enabled, and git. Your project must be a git repository: Nix reads only files that git tracks, so git add every file before you build.

A minimal file

This file builds the package in the directory hello against horizon-platform's compiler and build tools, and gives the repository a development shell:

do
  name "hello"
  compiler "ghc-10.0.0.20260917"
  on (toolchain (forge "nixlab.openfp.net/package-sets/haskell/horizon-platform") (branch "refs/tags/snapshots/lts/ghc-10.0.x/9"))
  flake Generated
  local "hello" "hello"
  root "hello"
  devShell

A file is a do block with one statement per line. Text values are written in double quotes. Generate the files, lock the flake and build:

git add -A
horizon-config update horizon.vx
git add -A
nix flake lock
git add flake.lock
nix build .#hello

The build has succeeded when ./result/bin/hello runs.

A fuller file

This file also states the repository's licence, language and warnings contract, adds a package from Hackage, and puts hlint in the development shell:

do
  name "hello"
  compiler "ghc-10.0.0.20260917"
  description "Says hello."
  on (toolchain (forge "nixlab.openfp.net/package-sets/haskell/horizon-platform") (branch "refs/tags/snapshots/lts/ghc-10.0.x/9"))
  flake Generated
  nixConfig "extra-substituters" "https://cache.openfp.net"
  nixConfig "extra-trusted-public-keys" "cache.openfp.net-1:ThN0P23SdgO8mRfZDCr0FZICFv9f2Llv1wqpfDoaMY4="
  license "MIT"
  language "GHC2024"
  warnings "werrorwolf"
  hackage "acme-missiles" "0.3"
  local "hello" "hello"
  root "hello"
  root "acme-missiles"
  devShellWith ["cabal-install", "hlint"]

license and language are checked against hello/hello.cabal, so that file must say license: MIT and default-language: GHC2024, or the update stops. With warnings "werrorwolf", nix flake check also builds the check hello-0.1.0.0-werror.

Commands

horizon-config update horizon.vx reads the file, fetches the sources it names, runs the solver if the file says solve, and writes the generated files and horizon.lock. It reuses what it resolved before for packages whose inputs have not changed. Run it after changing horizon.vx or a local package's .cabal file. horizon-config horizon.vx does the same.

horizon-config check horizon.vx confirms that the committed files match horizon.vx, without fetching anything, running Nix or the solver, or writing files. Run it in CI. When everything matches, it prints Horizon snapshot is consistent. It fails, and says what to do, when:

  • horizon.lock is missing, stale or from a generator before 0.21; run update.
  • a generated file is missing or has been edited, for example Horizon generated files differ: flake.nix; run horizon-config update horizon.vx.
  • under flake Generated, flake.lock is missing: flake.lock is missing beside the generated flake.nix; run nix flake lock.
  • under flake Manual with an on, flake.nix is missing, flake.lock has no input for the base, or that input follows another repository or branch. The message names the input to declare; see "Who owns flake.nix".

horizon-config --prune horizon.vx lists declared packages that no root needs. Run it after update, because it reads the generated package files. For the fuller file without root "acme-missiles", it reports acme-missiles as unreachable.

nix flake check runs the generated checks: assurances, which fails unless every root is in the set; treefmt; and, with warnings "werrorwolf", one -werror build per local package.

Generated files

| File | Written when | | --- | --- | | horizon.lock | always | | nix/horizon/ | always, except with a set base: one module per package, with modules.nix and repository.nix | | nix/horizon-pkgs/, nix/horizon-overlays.nix, nix/horizon-repository.nix | in place of nix/horizon/, with a set base | | cabal.project | the file has a local package | | nix/configuration.nix, nix/horizon.nix | the file has an on | | nix/horizon/overlays.nix | the file has no on | | flake.nix | flake Generated |

Commit all of them, with flake.lock. Do not edit them: check reports an edit, and the next update overwrites it. Other files under nix/ are yours; the generator neither writes nor checks them.

Who owns flake.nix

flake says who writes flake.nix. Declare it at most once.

flake Generated: the generator writes flake.nix, with the base named by on as its root input. It needs an on. Run nix flake lock after the first update and commit flake.lock; check fails without it, and reports any edit to flake.nix.

flake Manual: the repository writes flake.nix; the generator never writes or checks it. Use this to add your own inputs or outputs. The flake must load every module under nix/. With an on, it must also declare the base as an input named after the base's repository, on the same branch as horizon.vx. For the minimal file with flake Manual:

{
  inputs = {
    horizon-platform.url = "git+https://nixlab.openfp.net/package-sets/haskell/horizon-platform?ref=refs/tags/snapshots/lts/ghc-10.0.x/9";
    horizon-flake-parts.url = "git+https://nixlab.openfp.net/nix/horizon-flake-parts?ref=refs/tags/0.35.2";
    nixpkgs.follows = "horizon-platform/nixpkgs";
    horizon-ghc.follows = "horizon-platform/horizon-ghc";
  };

  outputs =
    inputs:
    inputs.horizon-flake-parts.inputs.nixica.lib.mkFlake {
      inputs = inputs.horizon-platform.inputs // inputs // { inherit (inputs.horizon-flake-parts.inputs) nixica; };
      modules = ./nix;
    };
}

If the input is missing, check prints the line to add:

flake Manual: flake.lock records no input "horizon-platform"; declare horizon-platform.url = "git+https://nixlab.openfp.net/package-sets/haskell/horizon-platform?ref=refs/tags/snapshots/lts/ghc-10.0.x/9" in flake.nix and run nix flake lock

Without an on, the generated packages are an overlay. Name the set they extend in a file of your own, such as nix/base.nix, and declare that set as a flake input. This example extends horizon-core:

do
  compiler "ghc-10.0.0.20260917"
  flake Manual
  local "hello" "hello"
  root "hello"
{
  inputs = {
    horizon-core.url = "git+https://nixlab.openfp.net/package-sets/haskell/horizon-core?ref=refs/tags/snapshots/lts/ghc-10.0.x/8";
    horizon-flake-parts.url = "git+https://nixlab.openfp.net/nix/horizon-flake-parts?ref=refs/tags/0.35.2";
    nixpkgs.follows = "horizon-flake-parts/nixpkgs";
  };

  outputs =
    inputs:
    inputs.horizon-flake-parts.inputs.nixica.lib.mkFlake {
      inputs = inputs // { inherit (inputs.horizon-flake-parts.inputs) nixica; };
      modules = ./nix;
    };
}
# nix/base.nix
local: {
  horizon.systems.supported.enable = true;
  horizon.systems.supported.targets = builtins.attrNames local.inputs.horizon-core.legacyPackages;
  perSystem =
    { system, ... }:
    {
      horizon.overlay.basePackages = local.inputs.horizon-core.legacyPackages.${system};
    };
}

Undeclared: a file with an on is flake Generated, and one without is flake Manual, as before horizon-config-shell 0.24.0.

The generator refuses flake Generated without an on, and a second flake statement, before it fetches anything. Switching from flake Generated to flake Manual leaves the last generated flake.nix in place for you to edit.

Reference: what the set builds on

on BASE, at most once. The set's base, which is one of:

  • toolchain (forge "HOST/PATH") (branch "REF"): build the set from scratch with that toolchain's compiler and build tools. The examples on this page use horizon-platform as the toolchain.
  • set "NAME" (forge "HOST/PATH") (branch "REF"): an overlay on a lower package set. The set takes its compiler and packages from the lower set, and a solve keeps them at the versions that set holds.

branch takes a branch, such as "lts/ghc-10.0.x", or a tag, such as "refs/tags/snapshots/lts/ghc-10.0.x/9". A branch moves; pin a snapshot tag to keep builds reproducible. flake.lock records the commit. Without on, the file describes an overlay for an application; see "Who owns flake.nix".

compiler "ghc-VERSION": the GHC, such as compiler "ghc-10.0.0.20260917". Nix builds use the base's compiler, but the generated cabal.project names this one, so a file with a local package must state the base's GHC for cabal build to work in the development shell. Without it, cabal.project names ghc-9.10.1.

flake Generated or flake Manual: who owns flake.nix.

platform "ARCH-OS": the platform whose arch(...) and os(...) conditions the packages are rendered for, such as platform "javascript-ghcjs". The default is x86_64-linux.

indexState "TIMESTAMP": read Hackage as it was at that time, such as indexState "2025-03-01T00:00:00Z", so that later uploads do not change the set. The default is the newest index on the machine; if that index is older than the time given, the update stops and asks you to run cabal update.

Reference: packages

hackage "NAME" "VERSION": a release from Hackage.

git "NAME" "URL" SELECTOR and gitSubdir "NAME" "URL" SELECTOR "DIR": a package from a git repository, or from a directory in one. The selector is (tag "NAME") or (rev "HASH"), a hash of at least seven hexadecimal digits:

gitSubdir "nixlab-dsl" "https://nixlab.openfp.net/nixlab/nixlab-dsl" (tag "0.45.0") "nixlab-dsl"

allowRevs ["NAME", ...]: allow rev for the named git packages. Without it, the generator refuses a rev. Statements combine. Naming a package that is not pinned by rev is an error.

tarball "NAME" "URL" and tarballSubdir "NAME" "URL" "DIR": a package from a tarball, or from a directory in one.

local "NAME" "DIR": a package in this repository. DIR must be inside the repository and hold one .cabal file, for that package.

openfp "NAME" "VERSION": a published OpenFP package release. The generator verifies its signed index and archive before using it.

repository "NAME" "URL" THRESHOLD ["KEY", ...]: a secure cabal repository, with how many of the listed root keys must sign its metadata. Declare it before the packages that use it.

hackageFrom "REPOSITORY" "NAME" "VERSION": a release from a declared repository, such as hackageFrom "nixlab" "vortex-trace" "0.3.0".

root "NAME": a package the set exists to provide. The assurances check fails unless every root is in the set, and --prune reports declared packages that no root needs.

missing "NAME": a package the set deliberately does not provide. If a root needs it, the update stops with an error.

Reference: changing a package

These statements name a package declared in the same file.

enableFlag "PACKAGE" "FLAG" and disableFlag "PACKAGE" "FLAG": turn a cabal flag on or off.

patch "PACKAGE" "FILE": apply a .patch or .diff file, whose path is relative to horizon.vx. Change a local package's source directly instead.

systemDeps "PACKAGE" "LIBRARY": pass the nixpkgs library of that name to the package, such as systemDeps "zlib" "zlib".

systemDepFrom "PACKAGE" "ARGUMENT" "ATTRIBUTE": the same when the package's argument and the nixpkgs attribute differ, such as systemDepFrom "cardano-crypto-class" "libblst" "blst".

Reference: set-wide build policy

Each of these takes True or False and applies to every pinned package. A local package follows its own .cabal file.

doBenchmark, doCheck, doHaddock, doJailbreak, enableProfiling, includeBenchmarks, includeExecutables, includeTests.

For example, doJailbreak True builds every pinned package with its version bounds removed.

Reference: solving

Without solve, the set holds exactly the packages the file pins. With it, cabal's solver adds everything the roots need.

solve: expand the roots into their dependencies and record the result in horizon.lock. Declare it once.

index "NAME": an index to solve against: hackage, head.hackage, chap, openfp, or a repository the file declares. Earlier statements take precedence. Use index "openfp" when solving with openfp packages.

allowNewer "NAME": ignore upper bounds on that package while solving, such as allowNewer "base" for a compiler newer than packages expect.

allowOlder "NAME": ignore lower bounds on that package while solving. Use it only when the older version really works, for example because a patch restores an older API.

Reference: the repository

name "NAME" and description "TEXT": the project's name and a one-line description, used in the generated flake and the set's README.

license "SPDX", language "LANGUAGE" and extension "EXTENSION": facts about every local package. The update checks each one against the package's .cabal file and stops if they disagree. Write extension once per extension.

exposed "PACKAGE" "MODULE", internal "PACKAGE" "MODULE" and suite "PACKAGE" "SUITE" "FILE": an exposed module, an internal module and a test suite of a local package, checked in the same way.

backend "PACKAGE" "BACKEND": recorded, but builds nothing yet.

warnings "werrorwolf": also build every local package with -Werror as the check NAME-VERSION-werror. warnings "none" declares no contract.

devShell: a development shell, nix develop, with GHC, the local packages' dependencies and cabal-install.

devShellWith ["PROGRAM", ...]: the same with the named nixpkgs programs in place of cabal-install, such as devShellWith ["cabal-install", "nodejs-slim"].

nixConfig "KEY" "VALUE": a nixConfig setting in the generated flake.nix. It takes effect only under flake Generated with a toolchain base.

Reference: output layout

overlay "FILE" and packageSet "FILE": export the packages as an overlay, or as a whole package set, to that file.

modules: accepted for older files. Every set is now written as modules, and the generator refuses it on a set with a set base.

Further reading

The horizon-config-shell README explains the lock and the checks in detail. horizon-platform's own horizon.vx is a full set with solve, patches and version exceptions.