diff --git a/CHANGELOG.md b/CHANGELOG.md index 27bda4fc..5b1e006c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,10 @@ ## `master` branch -- #37: Group `buildTools` (renamed to `tools`), `hlsCheck` and `hlintCheck` under the `devShell` submodule option; and allow disabling them all using `devShell.enable = false;` (useful if you want haskell-flake to produce just the package outputs). +- New features + - #63: Add `config.haskellProjects.${name}.outputs` containing all flake outputs for that project. +- API changes + - #37: Group `buildTools` (renamed to `tools`), `hlsCheck` and `hlintCheck` under the `devShell` submodule option; and allow disabling them all using `devShell.enable = false;` (useful if you want haskell-flake to produce just the package outputs). ## 0.1.0 diff --git a/flake-module.nix b/flake-module.nix index f28513ab..73639d01 100644 --- a/flake-module.nix +++ b/flake-module.nix @@ -3,11 +3,9 @@ let inherit (flake-parts-lib) - mkSubmoduleOptions mkPerSystemOption; inherit (lib) mkOption - mkDefault types; inherit (types) functionTo @@ -94,7 +92,7 @@ in }; }; }; - projectSubmodule = types.submodule { + projectSubmodule = types.submodule (args@{ name, config, lib, ... }: { options = { haskellPackages = mkOption { type = types.attrsOf raw; @@ -148,8 +146,17 @@ in ''; default = { }; }; + outputs = mkOption { + type = types.attrsOf types.raw; + description = '' + The flake outputs generated for this project. + + This is an internal option, not meant to be set by the user. + ''; + }; }; - }; + config = import ./haskell-project.nix (args // { inherit self pkgs; }); + }); in { options.haskellProjects = mkOption { @@ -158,139 +165,26 @@ in }; }); }; + config = { - perSystem = { config, self', inputs', pkgs, ... }: + perSystem = { config, self', lib, inputs', pkgs, ... }: let - # Like pkgs.runCommand but runs inside nix-shell with a mutable project directory. - # - # It currenty respects only the nativeBuildInputs (and no shellHook for - # instance), which seems sufficient for our purposes. We also set $HOME and - # make the project root mutable, because those are expected when running - # something in a project shell (it is indeed the case with HLS). - runCommandInSimulatedShell = devShell: projectRoot: name: attrs: command: - pkgs.runCommand name (attrs // { nativeBuildInputs = devShell.nativeBuildInputs; }) - '' - # Set pipefail option for safer bash - set -euo pipefail - - # Copy project root to a mutable area - # We expect "command" to mutate it. - export HOME=$TMP - cp -R ${projectRoot} $HOME/project - chmod -R a+w $HOME/project - pushd $HOME/project - - ${command} - touch $out - ''; - projects = - lib.mapAttrs - (projectKey: cfg: - let - inherit (pkgs.lib.lists) optionals; - localPackagesOverlay = self: _: - let - fromSdist = self.buildFromCabalSdist or (builtins.trace "Your version of Nixpkgs does not support hs.buildFromCabalSdist yet." (pkg: pkg)); - filterSrc = name: src: lib.cleanSourceWith { inherit src name; filter = path: type: true; }; - in - lib.mapAttrs - (name: value: - let - # callCabal2nix does not need a filtered source. It will - # only pick out the cabal and/or hpack file. - pkgProto = self.callCabal2nix name value.root { }; - pkgFiltered = pkgs.haskell.lib.overrideSrc pkgProto { - src = filterSrc name value.root; - }; - in - fromSdist pkgFiltered) - cfg.packages; - finalOverlay = - pkgs.lib.composeManyExtensions - [ - # The order here matters. - # - # User's overrides (cfg.overrides) is applied **last** so - # as to give them maximum control over the final package - # set used. - localPackagesOverlay - (pkgs.haskell.lib.packageSourceOverrides cfg.source-overrides) - cfg.overrides - ]; - finalPackages = cfg.haskellPackages.extend finalOverlay; - - defaultBuildTools = hp: with hp; { - inherit - cabal-install - haskell-language-server - ghcid - hlint; - }; - nativeBuildInputs = lib.attrValues (defaultBuildTools finalPackages // cfg.devShell.tools finalPackages); - devShell = finalPackages.shellFor { - inherit nativeBuildInputs; - packages = p: - map - (name: p."${name}") - (lib.attrNames cfg.packages); - withHoogle = true; - }; - devShellCheck = name: command: - runCommandInSimulatedShell devShell self "${projectKey}-${name}-check" { } command; - in - { - packages = - lib.mapAttrs - (name: _: finalPackages."${name}") - cfg.packages; - } // lib.optionalAttrs cfg.devShell.enable { - inherit devShell; - checks = lib.filterAttrs (_: v: v != null) - { - "${projectKey}-hls" = - if cfg.devShell.hlsCheck.enable then - devShellCheck "hls" "haskell-language-server" - else null; - "${projectKey}-hlint" = - if cfg.devShell.hlintCheck.enable then - devShellCheck "hlint" '' - hlint ${lib.concatStringsSep " " cfg.devShell.hlintCheck.dirs} - '' - else null; - }; - } - ) - config.haskellProjects; + # Like mapAttrs, but merges the values (also attrsets) of the resulting attrset. + flatAttrMap = f: attrs: lib.mkMerge (lib.attrValues (lib.mapAttrs f attrs)); in { - packages = with lib; - mkMerge - ( - mapAttrsToList - (projectName: project: - mapAttrs' - (packageName: package: { - name = - # Prefix package names with the project name (unless - # project is named `default`) - if projectName == "default" - then packageName - else "${projectName}-${packageName}"; - value = package; - }) - project.packages - ) - projects - ); - checks = - lib.mkMerge - (lib.mapAttrsToList - (_: project: project.checks) - projects); + packages = + flatAttrMap (_: project: project.outputs.packages) config.haskellProjects; devShells = - lib.mapAttrs - (_: project: project.devShell) - projects; + flatAttrMap + (_: project: + lib.optionalAttrs project.devShell.enable project.outputs.devShells) + config.haskellProjects; + checks = + flatAttrMap + (_: project: + lib.optionalAttrs project.devShell.enable project.outputs.checks) + config.haskellProjects; }; }; } diff --git a/haskell-project.nix b/haskell-project.nix new file mode 100644 index 00000000..d5f91d4d --- /dev/null +++ b/haskell-project.nix @@ -0,0 +1,117 @@ +# Definition of the `haskellProjects.${name}` submodule's `config` +{ name, self, config, lib, pkgs, ... }: +let + # Like pkgs.runCommand but runs inside nix-shell with a mutable project directory. + # + # It currenty respects only the nativeBuildInputs (and no shellHook for + # instance), which seems sufficient for our purposes. We also set $HOME and + # make the project root mutable, because those are expected when running + # something in a project shell (it is indeed the case with HLS). + runCommandInSimulatedShell = devShell: projectRoot: name: attrs: command: + pkgs.runCommand name (attrs // { nativeBuildInputs = devShell.nativeBuildInputs; }) + '' + # Set pipefail option for safer bash + set -euo pipefail + + # Copy project root to a mutable area + # We expect "command" to mutate it. + export HOME=$TMP + cp -R ${projectRoot} $HOME/project + chmod -R a+w $HOME/project + pushd $HOME/project + + ${command} + touch $out + ''; +in +{ + outputs = + let + projectKey = name; + + localPackagesOverlay = self: _: + let + fromSdist = self.buildFromCabalSdist or (builtins.trace "Your version of Nixpkgs does not support hs.buildFromCabalSdist yet." (pkg: pkg)); + filterSrc = name: src: lib.cleanSourceWith { inherit src name; filter = path: type: true; }; + in + lib.mapAttrs + (name: value: + let + # callCabal2nix does not need a filtered source. It will + # only pick out the cabal and/or hpack file. + pkgProto = self.callCabal2nix name value.root { }; + pkgFiltered = pkgs.haskell.lib.overrideSrc pkgProto { + src = filterSrc name value.root; + }; + in + fromSdist pkgFiltered) + config.packages; + finalOverlay = + lib.composeManyExtensions + [ + # The order here matters. + # + # User's overrides (cfg.overrides) is applied **last** so + # as to give them maximum control over the final package + # set used. + localPackagesOverlay + (pkgs.haskell.lib.packageSourceOverrides config.source-overrides) + config.overrides + ]; + finalPackages = config.haskellPackages.extend finalOverlay; + + defaultBuildTools = hp: with hp; { + inherit + cabal-install + haskell-language-server + ghcid + hlint; + }; + nativeBuildInputs = lib.attrValues (defaultBuildTools finalPackages // config.devShell.tools finalPackages); + devShell = finalPackages.shellFor { + inherit nativeBuildInputs; + packages = p: + map + (name: p."${name}") + (lib.attrNames config.packages); + withHoogle = true; + }; + in + { + packages = + let + mapKeys = f: attrs: lib.mapAttrs' (n: v: { name = f n; value = v; }) attrs; + # Prefix package names with the project name (unless + # project is named `default`) + dropDefaultPrefix = packageName: + if projectKey == "default" + then packageName + else "${projectKey}-${packageName}"; + in + mapKeys dropDefaultPrefix + (lib.mapAttrs + (name: _: finalPackages."${name}") + config.packages); + + devShells."${projectKey}" = devShell; + + checks = lib.filterAttrs (_: v: v != null) { + "${projectKey}-hls" = + if config.devShell.hlsCheck.enable then + runCommandInSimulatedShell + devShell + self "${projectKey}-hls-check" + { } "haskell-language-server" + else null; + "${projectKey}-hlint" = + if config.devShell.hlintCheck.enable then + runCommandInSimulatedShell + devShell + self "${projectKey}-hlint-check" + { } '' + hlint ${lib.concatStringsSep " " config.devShell.hlintCheck.dirs} + '' + else null; + }; + }; +}