Skip to content

Commit

Permalink
[OPS-1458] Add haskell.nix wrapper for CI
Browse files Browse the repository at this point in the history
Problem: Our CI template for libraries allows one to specify
ghc-versions so that we can build a library in different
configurations. Currently the only configurable thing is the GHC
version. We also want to be able to build libraries with different
stack.yaml files and stack resolvers.

Solution: Add a haskell.nix wrapper to the library CI capable of
building a project with different GHC versions, stack.yaml files, and
stack resolvers.
  • Loading branch information
Sereja313 committed Aug 2, 2023
1 parent 1163122 commit 8c06b9e
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 99 deletions.
4 changes: 2 additions & 2 deletions lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
#
# SPDX-License-Identifier: MPL-2.0

{ lib, gitignore-nix, nixpkgs, deploy-rs, get-tested }: {
{ lib, gitignore-nix, nixpkgs, deploy-rs, get-tested }: rec {
src = import ./src.nix { inherit lib gitignore-nix; };

# Extend nixpkgs with multiple overlays
# pkgs = pkgsWith nixpkgs.legacyPackages.${system} [ inputs.serokell-nix.overlay ];
pkgsWith = p: e: p.extend (lib.composeManyExtensions e);

haskell = import ./haskell.nix { inherit lib; };
haskell = import ./haskell.nix { inherit lib nixpkgs; inherit (cabal) getTestedWithVersions; };

systemd = import ./systemd;

Expand Down
178 changes: 81 additions & 97 deletions lib/haskell.nix
Original file line number Diff line number Diff line change
@@ -1,99 +1,85 @@
{ lib }:
{ nixpkgs, lib, getTestedWithVersions }:

let
/* Make a Nix flake for a Haskell project.
The idea is that, given a Haskell project (e.g. a stack project), this function
will return a flake with packages, checks, and apps, for multiple versions
of GHC – the ones that you sepcify, plus the “default” one (coming from the
project configuration, e.g. stack LTS).
Note that the resulting flake is “naked” in that it does not include the
system name in its outputs, so you should use `flake-utils` or something.
* Packages will contain, essentially, everything:
- Build the library: `nix build .#<package>:lib:<package>`
- Build an exectuable: `nix build .#<package>:exe:<executable>`
- Build a test: `nix build .#<package>:test:<test>`
- Build a benchmark: `nix build .#<package>:bench:<benchmark>`
* Checks will run corresponding tests:
- Run a test: `nix check .#<package>:test:<test>`
* Apps will run corresponding executables:
- Run a test: `nix run .#<package>:exe:<executable>`
Everything above can be prefixed with one of the requested GHC versions:
* Run a test for a specific GHC version:
- `nix check .#ghc<version>:<package>:test:<test>`
Inputs:
- The haskell.nix packages set (i.e. pkgs.haskell-nix)
- A haskell.nix *Project function (e.g. pkgs.haskell-nix.stackProject)
- Attrs with:
- ghcVersions: compiler versions to build with (in addition to the default one)
- Any other attributes that will be forwarded to the project function
Example:
makeFlake pkgs.haskell-nix pkgs.haskell-nix.stackProject {
src = ./.;
ghcVersions = [ "901" ];
}
=>
# Assuming you use `flake-utils.lib.eachSystem [ "x86_64-linux" ]`
$ nix flake show
<...>
├───apps
│ └───x86_64-linux
│ ├───"ghc901:package:exe:executable": app
│ ├───"ghc901:package:test:test": app
│ ├───"package:exe:executable": app
│ └───"package:test:test": app
├───checks
│ └───x86_64-linux
│ ├───"ghc901:package:test:test": derivation 'test-test-1.0.0-check'
│ └───"package:test:test": derivation 'test-test-1.0.0-check'
├───devShell
│ └───x86_64-linux: development environment 'ghc-shell-for-package'
└───packages
└───x86_64-linux
├───"ghc901:package:exe:executable": package 'package-exe-executable-1.0.0'
├───"ghc901:package:lib:package": package 'package-lib-package-1.0.0'
├───"ghc901:package:test:test": package 'test-test-1.0.0'
├───"package:exe:executable": package 'package-exe-executable-1.0.0'
├───"package:lib:package": package 'package-lib-package-1.0.0'
└───"package:test:test": package 'test-test-1.0.0'
*/
makeFlake = haskellNix: projectF: args@{ ghcVersions, ... }:
let
args' = builtins.removeAttrs args [ "ghcVersions" ];

flakeForGhc = ghcVersion:
let
compiler-nix-name =
if ghcVersion == null
then null
else "ghc${ghcVersion}";
project = projectF (args' // { inherit compiler-nix-name; });
prefix =
if compiler-nix-name == null
then ""
else "${compiler-nix-name}:";
fixFlakeOutput = name: output:
if name == "devShell"
then output
else lib.mapAttrs' (n: v: lib.nameValuePair (prefix + n) v) output;
fixFlake =
lib.mapAttrs fixFlakeOutput;
in
fixFlake (project.flake {});

combineOutputs = name:
if name == "devShell"
then lib.last
else lib.foldl (l: r: l // r) {};

in lib.zipAttrsWith combineOutputs (map flakeForGhc (ghcVersions ++ [null]));

makeCI = haskellPkgs: {
# haskell project root
src,
# haskell package names
packageNames,
# cabal file with ghc versions specified in test-with
cabalFile,
# whether to build the project with stack, disable if you are not using stack
buildWithStack ? true,
# stack files to use in addition to stack.yaml
stackFiles ? [],
# stack resolvers for building the project, they will be replaced in stack.yaml
resolvers ? [],
# extra haskell.nix arguments
extraArgs ? {}
}: let
pkgs = nixpkgs.legacyPackages.${haskellPkgs.system};
replaceDots = builtins.replaceStrings ["."] ["-"];

# invoke haskell.nix for every ghc specified in tested-with stanza of cabalFile
ghc-versions = getTestedWithVersions cabalFile;
pkgs-per-ghc = lib.genAttrs ghc-versions
(ghc: haskellPkgs.haskell-nix.cabalProject ({
inherit src;
compiler-nix-name = ghc;
} // extraArgs));

# invoke haskell.nix for stack.yaml and every file from stackFiles
stackYamls = lib.optionals buildWithStack ([ "stack.yaml" ] ++ stackFiles);
pkgs-per-stack-yaml = lib.mapAttrs' (n: v: lib.nameValuePair (replaceDots n) v)
(lib.genAttrs stackYamls
(stackYaml: haskellPkgs.haskell-nix.stackProject {
inherit src stackYaml;
} // extraArgs));

# invoke haskell.nix for every resolver specified in resolvers
stackResolvers = lib.optionals buildWithStack resolvers;
pkgs-per-resolver = lib.mapAttrs' (n: v: lib.nameValuePair (replaceDots n) v)
(lib.genAttrs stackResolvers
(resolver: haskellPkgs.haskell-nix.stackProject {
src = pkgs.runCommand "change resolver" { } ''
mkdir -p $out
cp -r ${src}/* .
${pkgs.gnused}/bin/sed -i 's/resolver:.*/resolver: ${resolver}/' stack.yaml
cp -r ./* $out
'';
} // extraArgs));

all-pkgs = pkgs-per-ghc // pkgs-per-stack-yaml // pkgs-per-resolver;

# returns the list of all components for a package
get-package-components = pkg:
# library
lib.optional (pkg ? library) pkg.library
# haddock
++ lib.optional (pkg ? library) pkg.library.haddock
# exes, tests and benchmarks
++ lib.attrValues pkg.exes
++ lib.attrValues pkg.tests
++ lib.attrValues pkg.benchmarks;

# all components for each specified ghc version or stack yaml
build-all = lib.mapAttrs' (prefix: pkg:
let components = lib.concatMap (name: get-package-components pkg.${name}.components) packageNames;
in lib.nameValuePair "${prefix}:build-all"
(pkgs.linkFarmFromDrvs "build-all" components)) all-pkgs;

# all tests for each specified ghc version or stack yaml
test-all = lib.mapAttrs' (prefix: pkg:
let tests = lib.filter lib.isDerivation (lib.concatMap (name: lib.attrValues pkg.${name}.checks) packageNames);
in lib.nameValuePair "${prefix}:test-all"
(pkgs.linkFarmFromDrvs "test-all" tests)) all-pkgs;

# build matrix used in github actions
build-matrix = { include = map (prefix: { inherit prefix; }) (ghc-versions ++ (map replaceDots (stackYamls ++ stackResolvers))); };

in {
inherit build-all test-all build-matrix all-pkgs;
};

/* Set haskell.nix package config options for all project (local) packages.
Expand Down Expand Up @@ -152,7 +138,5 @@ let
};

in {
inherit makeFlake;

inherit optionsLocalPackages ciBuildOptions;
inherit makeCI ciBuildOptions optionsLocalPackages;
}

0 comments on commit 8c06b9e

Please sign in to comment.