Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: move impl to inner config #63

Merged
merged 8 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
158 changes: 26 additions & 132 deletions flake-module.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@

let
inherit (flake-parts-lib)
mkSubmoduleOptions
mkPerSystemOption;
inherit (lib)
mkOption
mkDefault
types;
inherit (types)
functionTo
Expand Down Expand Up @@ -94,7 +92,7 @@ in
};
};
};
projectSubmodule = types.submodule {
projectSubmodule = types.submodule (args@{ name, config, lib, ... }: {
options = {
haskellPackages = mkOption {
type = types.attrsOf raw;
Expand Down Expand Up @@ -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; });
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roberth Is there a better way of doing this (instead of the //, which seems like kind of a hack)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we can use module machinery for this, and we probably want that for those little modules with haskell-flake overrides anyway.

  • Above, use projectSubmodule = submoduleWith { specialArgs = { inherit pkgs self; }; modules = [ ..... ] }. specialArgs should be fine here, as we don't think we want modules to affect those parameters (not 100% sure about pkgs, but that can be improved later if necessary)
  • Use imports = [ ./haskell-project.nix ]; here

});
in
{
options.haskellProjects = mkOption {
Expand All @@ -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;
};
};
}
117 changes: 117 additions & 0 deletions haskell-project.nix
Original file line number Diff line number Diff line change
@@ -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;
};
};
}