From 996f5c2cdc67285c4990df378976f9dbf26f8401 Mon Sep 17 00:00:00 2001 From: Sridhar Ratnakumar <3998+srid@users.noreply.github.com> Date: Tue, 30 May 2023 13:55:26 -0400 Subject: [PATCH] Modular overrides (#162) **Completely new way to override Haskell packages**: removed `overrides` and `source-overrides`. Use `packages` to specify your source overrides; use `settings` to override individual packages in modular fashion (like NixOS modules). Additional changes include: - Add `package..cabal.executables` referring to the executables in a package. This is auto-detected by parsing the Cabal file. - Add `packages..local.*` to determine of a package is a local package or not. - Add `projectFlakeName` option (useful in debug logging prefix) - `flake.haskellFlakeProjectModules`: Dropped all defaults, except the `output` module, which now exports `packages` and `settings`. Added a `defaults.projectModules.output` option that allows the user to override this module, or directly access the generated module. - Add `project.config.defaults.settings.default` defining sensible defaults for local packages. - Add `project.config.defaults.enable` to turn off all default settings en masse. Also, disable docs test due to https://github.com/hercules-ci/flake.parts-website/issues/332 --- .gitignore | 1 + CHANGELOG.md | 10 + doc/guide/dependency.md | 49 ++-- doc/guide/modules.md | 16 +- doc/guide/package-set.md | 28 +- doc/start.md | 1 + example/flake.nix | 25 +- nix/build-haskell-package.nix | 9 +- nix/haskell-parsers/default.nix | 12 +- nix/logging.nix | 8 +- nix/modules/project-modules.nix | 41 +-- nix/modules/project.nix | 243 ----------------- nix/modules/project/default.nix | 98 +++++++ nix/modules/project/defaults.nix | 96 ++++++- nix/modules/project/devshell.nix | 5 +- nix/modules/project/outputs.nix | 120 +++++++++ nix/modules/project/packages.nix | 56 ---- nix/modules/project/packages/default.nix | 74 ++++++ nix/modules/project/packages/package.nix | 82 ++++++ nix/modules/project/settings/all.nix | 323 +++++++++++++++++++++++ nix/modules/project/settings/default.nix | 81 ++++++ nix/modules/project/settings/lib.nix | 45 ++++ nix/modules/projects.nix | 2 +- nix/types/haskell-overlay-type.nix | 31 +-- nix/types/haskell-source-type.nix | 15 ++ runtest.sh | 3 +- test/simple/flake.nix | 27 +- test/simple/test.sh | 14 +- 28 files changed, 1070 insertions(+), 445 deletions(-) delete mode 100644 nix/modules/project.nix create mode 100644 nix/modules/project/default.nix create mode 100644 nix/modules/project/outputs.nix delete mode 100644 nix/modules/project/packages.nix create mode 100644 nix/modules/project/packages/default.nix create mode 100644 nix/modules/project/packages/package.nix create mode 100644 nix/modules/project/settings/all.nix create mode 100644 nix/modules/project/settings/default.nix create mode 100644 nix/modules/project/settings/lib.nix create mode 100644 nix/types/haskell-source-type.nix diff --git a/.gitignore b/.gitignore index 126f405..fa77ac6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vscode .direnv result +result-* dist-newstyle diff --git a/CHANGELOG.md b/CHANGELOG.md index b6943e7..0c03b9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Revision history for haskell-flake +## `master` + +- #162: **Completely new way to override Haskell packages**: removed `overrides` and `source-overrides`. Use `packages` to specify your source overrides; use `settings` to override individual packages in modular fashion (like NixOS modules). Additional changes include: + - Add `package..cabal.executables` referring to the executables in a package. This is auto-detected by parsing the Cabal file. + - Add `packages..local.*` to determine of a package is a local package or not. + - Add `projectFlakeName` option (useful in debug logging prefix) + - `flake.haskellFlakeProjectModules`: Dropped all defaults, except the `output` module, which now exports `packages` and `settings`. Added a `defaults.projectModules.output` option that allows the user to override this module, or directly access the generated module. + - Add `project.config.defaults.settings.default` defining sensible defaults for local packages. + - Add `project.config.defaults.enable` to turn off all default settings en masse. + ## 0.3.0 (May 22, 2023) - #134: Add `autoWire` option to control generation of flake outputs diff --git a/doc/guide/dependency.md b/doc/guide/dependency.md index 47ba24e..f3913ba 100644 --- a/doc/guide/dependency.md +++ b/doc/guide/dependency.md @@ -4,13 +4,13 @@ slug: dependency # Overriding dependencies -Haskell libraries ultimately come from [Hackage](https://hackage.haskell.org/), and [nixpkgs] contains [most of these](https://nixpkgs.haskell.page/). Adding a library to your project is done as follows (requiring no change to Nix!): +Haskell libraries ultimately come from [Hackage](https://hackage.haskell.org/), and [nixpkgs] contains [most of these](https://nixpkgs.haskell.page/). Adding a library to your project usually involves modifying the `.cabal` file and restart the nix shell: 1. Identify the package name from Hackage. Let's say you want to use [`ema`](https://hackage.haskell.org/package/ema) 2. Add the package, `ema`, to the `.cabal` file under [the `build-depends` section](https://cabal.readthedocs.io/en/3.4/cabal-package.html#pkg-field-build-depends). 3. Exit and restart the nix shell (`nix develop`). -Step (3) above will try to fetch the package from the Haskell package set in [nixpkgs] (the one that is pinned in `flake.lock`). For various reasons, this package may be either broken or does not exist. In such cases, you will have to override the package in the `overrides` argument (see the next section). +Step (3) above will try to fetch the package from the Haskell package set in [nixpkgs] (the one that is pinned in `flake.lock`). For various reasons, this package may be either missing or marked as broken. In such cases, you will have to override the package locally in the project (see the next section). ## Overriding a Haskell package in Nix @@ -25,61 +25,46 @@ In Nix, it is possible to use an exact package built from an arbitrary source (G }; } ``` -1. Build it using `callCabal2nix` and assign it to the `ema` name in the Haskell package set by adding it to the `overrides` argument of your `flake.nix` that is using haskell-flake: +1. Build it using `callCabal2nix` and assign it to the `ema` name in the Haskell package set by adding it to the `packages` argument of your `flake.nix` that is using haskell-flake: ```nix { perSystem = { self', config, pkgs, ... }: { haskellProjects.default = { - overrides = self: super: with pkgs.haskell.lib; { - ema = dontCheck (self.callCabal2nix "ema" inputs.ema {}); + packages = { + ema.source = inputs.ema; + }; + settings = { + ema = { # This module can take `{self, super, ...}` args, optionally. + check = false; + }; }; }; }; } ``` - We use `dontCheck` here to disable running tests. + We use `check = false` here to disable running tests. 1. Re-run the nix shell (`nix develop`). ### [nixpkgs] functions -- The `pkgs.haskell.lib` module provides various utility functions (like `dontCheck` above, to disable running tests) that you can use to override Haskell packages. The canonical place to find documentation on these is [the source](https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/haskell-modules/lib/compose.nix). -- `self.callHackage` - Build a library from Hackage given its version. You can also do this with `source-overrides` (see below). -- [Artyom's tutorial](https://tek.brick.do/how-to-override-dependency-versions-when-building-a-haskell-project-with-nix-K3VXJd8mEKO7) +- The `pkgs.haskell.lib` module provides various utility functions that you can use to override Haskell packages. The canonical place to find documentation on these is [the source](https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/haskell-modules/lib/compose.nix). haskell-flake provides a `settings` submodule for convienience; for eg., the `dontCheck` function translates to `settings..check`. -## Using `source-overrides` +## Using Hackage versions -If you are only specifying the source of the Haskell package, but are not overriding anything else, you may use the simpler `source-overrides` option instead. The above example would look like: +`packages..source` also supports Hackage versions. So the following works to pull [ema 0.8.2.0](https://hackage.haskell.org/package/ema-0.8.2.0): ```nix { perSystem = { self', config, pkgs, ... }: { haskellProjects.default = { - source-overrides = { - ema = inputs.ema; + packages = { + ema.source = "0.8.2.0"; }; }; }; } ``` -`source-overrides` also supports specifying referring directly to a Hackage version. So the following works to pull [ema 0.8.2.0](https://hackage.haskell.org/package/ema-0.8.2.0): - -```nix -{ - perSystem = { self', config, pkgs, ... }: { - haskellProjects.default = { - source-overrides = { - ema = "0.8.2.0"; - }; - }; - }; -} -``` - -If you are using both the options, `overrides` take priority over `source-overrides`. - -## See also - -- [Setting environment variables for build](https://github.com/srid/haskell-flake/discussions/159#discussioncomment-5840505) +[[project-modules]] export both `packages` and `settings` options for reuse in downstream Haskell projects. [nixpkgs]: https://zero-to-nix.com/concepts/nixpkgs diff --git a/doc/guide/modules.md b/doc/guide/modules.md index cc34c78..1ea4fcb 100644 --- a/doc/guide/modules.md +++ b/doc/guide/modules.md @@ -18,8 +18,8 @@ Let's say you have two repositories -- `common` and `myapp`. The `common` reposi cabal-fmt ormolu; }; - source-overrides = { - mylib = inputs.mylib; + packages = { + mylib.source = inputs.mylib; }; }; } @@ -58,11 +58,9 @@ By default, haskell-flake will generate the following modules for the "default" | Module | Contents | | -- | -- | -| `haskellFlakeProjectModules.input` | Dependency overrides only | -| `haskellFlakeProjectModules.local` | Local packages only | | `haskellFlakeProjectModules.output` | Local packages & dependency overrides | -The idea here being that you can "connect" two Haskell projects such that they depend on one another while reusing the overrides from one place. For example, if you have a project "foo" that depends on "bar" and if "foo"'s flake.nix has "bar" as its input, then in "foo"'s `haskellProject.default` entry you can import "bar" as follows: +The idea here being that you can "connect" two Haskell projects such that they depend on one another while reusing the overrides (`packages` and `settings`) from one place. For example, if you have a project "foo" that depends on "bar" and if "foo"'s flake.nix has "bar" as its input, then in "foo"'s `haskellProject.default` entry you can import "bar" as follows: ```nix # foo's flake.nix's perSystem @@ -78,13 +76,9 @@ The idea here being that you can "connect" two Haskell projects such that they d } ``` -By importing "bar"'s `output` project module, you automatically get the overrides from "bar" (unless you use the `local` module) as well as the local packages[^bar]. This way you don't have to duplicate the `overrides` and manually specify the `source-overrides` in "foo"'s flake.nix. +By importing "bar"'s `output` project module, you automatically get the overrides from "bar" as well as the local packages. This way you don't have to duplicate the `settings` and manually specify the `packages..source` in "foo"'s flake.nix. -[^bar]: Local packages come from the `packages` option. So this is typically the "bar" package itself for single-package projects; or all the local projects if it is a multi-package project. ## Examples -- https://github.com/srid/nixpkgs-140774-workaround -- [shared-kernel](https://github.com/nammayatri/shared-kernel/blob/591bdc1c87b3f80b57a3c3849414bd106a1f8365/flake.nix#L24-L26) importing [euler-hs overrides](https://github.com/juspay/euler-hs/blob/168dc51f8a68e4bf52de6c691343afa594f933a9/flake.nix#L31-L52) and local packages. - -- https://github.com/juspay/prometheus-haskell/pull/3 +- https://github.com/nammayatri/nammayatri (imports `shared-kernel` which in turn imports `euler-hs`) diff --git a/doc/guide/package-set.md b/doc/guide/package-set.md index c08d2ce..96bd40d 100644 --- a/doc/guide/package-set.md +++ b/doc/guide/package-set.md @@ -11,9 +11,9 @@ A "project" in haskell-flake primarily serves the purpose of developing Haskell ```nix { haskellProjects.ghc810 = { - packages = {}; # No local packages - devShell.enable = false; - autoWire = [ ]; # Don't wire any flake outputs + defaults.packages = {}; # Disable scanning for local package + devShell.enable = false; # Disable devShells + autoWire = [ ]; # Don't wire any flake outputs # Start from nixpkgs's ghc8107 package set basePackages = pkgs.haskell.packages.ghc8107; @@ -26,41 +26,41 @@ You can access this package set as `config.haskellProjects.ghc810.outputs.finalP ```nix { haskellProjects.ghc810 = { - packages = {}; # No local packages + defaults.packages = {}; # No local packages devShell.enable = false; basePackages = pkgs.haskell.packages.ghc8107; - source-overrides = { + packages = { # New packages from flake inputs - mylib = inputs.mylib; + mylib.source = inputs.mylib; # Dependencies from Hackage - aeson = "1.5.6.0"; - dhall = "1.35.0"; + aeson.source = "1.5.6.0"; + dhall.source = "1.35.0"; }; - overrides = self: super: with pkgs.haskell.lib; { - aeson = doJailbreak super.aeson; + settings = { + aeson.jailbreak = true; }; }; } ``` -This will create a package set that overrides the `aeson` and `dhall` packages using the specified versions from Hackage, but with the `aeson` package having the `doJailbreak` flag set (which relaxes its Cabal constraints). It also adds the `mylib` package which exists neither in nixpkgs nor in Hackage, but comes from somewhere arbitrary and specified as flake input. +This will create a package set that overrides the `aeson` and `dhall` packages using the specified versions from Hackage, but with the `aeson` package having the `jailbreak` flag set (which relaxes its Cabal constraints). It also adds the `mylib` package which exists neither in nixpkgs nor in Hackage, but comes from somewhere arbitrary and specified as flake input. In your *actual* haskell project, you can use this package set (`config.haskellProjects.ghc810.outputs.finalPackages`) as its base package set: ```nix { haskellProjects.myproject = { - packages.mypackage = ./.; + packages.mypackage.source = ./.; basePackages = config.haskellProjects.ghc810.outputs.finalPackages; }; } ``` -Finally you can externalized this `ghc810` package set as either a flake-parts module or as a [[modules|haskell-flake module]], and import it in multiple repositories. +Finally, you can externalize this `ghc810` package set as either a flake-parts module or as a [[modules|haskell-flake module]], and thereon import it from multiple repositories. ## Examples -- https://github.com/nammayatri/common/pull/3/files +- https://github.com/nammayatri/common/pull/11/files diff --git a/doc/start.md b/doc/start.md index 1e335fb..5d07575 100644 --- a/doc/start.md +++ b/doc/start.md @@ -37,6 +37,7 @@ In addition, compared to using plain nixpkgs, haskell-flake supports: - Auto-detection of local packages based on `cabal.project` file (via [haskell-parsers](https://github.com/srid/haskell-flake/tree/master/nix/haskell-parsers)) - Parse executables from `.cabal` file +- Modular interface to `pkgs.haskell.lib.compose.*` (via `packages` and `settings` submodules) - Composition of dependency overrides, and other project settings, via [[modules]] ## Next steps diff --git a/example/flake.nix b/example/flake.nix index 919adfa..d9491d6 100644 --- a/example/flake.nix +++ b/example/flake.nix @@ -14,18 +14,29 @@ # Typically, you just want a single project named "default". But # multiple projects are also possible, each using different GHC version. haskellProjects.default = { - # If you have a .cabal file in the root, this option is determined - # automatically. Otherwise, specify all your local packages here. - # packages.example.root = ./.; - # The base package set representing a specific GHC version. # By default, this is pkgs.haskellPackages. # You may also create your own. See https://haskell.flake.page/package-set # basePackages = pkgs.haskellPackages; - # Dependency overrides go here. See https://haskell.flake.page/dependency - # source-overrides = { }; - # overrides = self: super: { }; + # Extra package information. See https://haskell.flake.page/dependency + # + # Note that local packages are automatically included in `packages` + # (defined by `defaults.packages` option). + # + # packages = { + # aeson.source = "1.5.0.0"; # Hackage version override + # shower.source = inputs.shower; + # }; + # settings = { + # aeson = { + # check = false; + # }; + # relude = { + # haddock = false; + # broken = false; + # }; + # }; # devShell = { # # Enabled by default diff --git a/nix/build-haskell-package.nix b/nix/build-haskell-package.nix index fb07371..2977be6 100644 --- a/nix/build-haskell-package.nix +++ b/nix/build-haskell-package.nix @@ -1,7 +1,10 @@ # Like callCabal2nix, but does more: # - Source filtering (to prevent parent content changes causing rebuilds) # - Always build from cabal's sdist for release-worthiness -{ pkgs, lib, self, log, ... }: +# +# This function can only be called from inside a Haskell overlay, whose 'self' +# and 'super' are accessible in args here. +{ pkgs, lib, self, super, log, ... }: let fromSdist = self.buildFromCabalSdist or @@ -19,8 +22,8 @@ let }; in -name: pkgCfg: -lib.pipe pkgCfg.root +name: root: +lib.pipe root [ # Avoid rebuilding because of changes in parent directories (mkNewStorePath "source-${name}") diff --git a/nix/haskell-parsers/default.nix b/nix/haskell-parsers/default.nix index db8c12e..f81521c 100644 --- a/nix/haskell-parsers/default.nix +++ b/nix/haskell-parsers/default.nix @@ -68,9 +68,13 @@ in getCabalExecutables = path: let cabalFile = traversal.findSingleCabalFile path; - res = parser.parseCabalExecutableNames (builtins.readFile (lib.concatStrings [ path "/" cabalFile ])); in - if res.type == "success" - then res.value - else throwError "Failed to parse ${cabalFile}: ${builtins.toJSON res}"; + if cabalFile != null then + let res = parser.parseCabalExecutableNames (builtins.readFile (lib.concatStrings [ path "/" cabalFile ])); + in + if res.type == "success" + then res.value + else throwError "Failed to parse ${cabalFile}: ${builtins.toJSON res}" + else + throwError "No .cabal file found under ${path}"; } diff --git a/nix/logging.nix b/nix/logging.nix index bf89a36..f01a8d9 100644 --- a/nix/logging.nix +++ b/nix/logging.nix @@ -1,16 +1,16 @@ -{ debug ? false, ... }: +{ name, debug ? false, ... }: { traceDebug = msg: if debug then - builtins.trace ("DEBUG[haskell-flake]: " + msg) + builtins.trace ("DEBUG[haskell-flake] [${name}]: " + msg) else x: x; traceWarning = msg: - builtins.trace ("WARNING[haskell-flake]: " + msg); + builtins.trace ("WARNING[haskell-flake] [${name}]: " + msg); throwError = msg: builtins.throw '' - ERROR[haskell-flake]: ${msg} + ERROR[haskell-flake] [${name}]: ${msg} ''; } diff --git a/nix/modules/project-modules.nix b/nix/modules/project-modules.nix index 8b92171..1aafbdb 100644 --- a/nix/modules/project-modules.nix +++ b/nix/modules/project-modules.nix @@ -16,44 +16,21 @@ in A lazy attrset of `haskellProjects.` modules that can be imported in other flakes. ''; - defaultText = '' + defaultText = lib.literalMD '' Package and dependency information for this project exposed for reuse in another flake, when using this project as a Haskell dependency. - Typically the consumer of this flake will want to use one of the - following modules: - - - output: provides both local package and dependency overrides. - - local: provides only local package overrides (ignores dependency - overrides in this flake) - - These default modules are always available. + The 'output' module of the default project is included by default, + returning `defaults.projectModules.output`. ''; - default = { }; # Set in config (see ./default-project-modules.nix) + default = { }; }; - config.haskellFlakeProjectModules = - let - defaults = rec { - # The 'output' module provides both local package and dependency - # overrides. - output = { - imports = [ input local ]; - }; - # The 'local' module provides only local package overrides. - local = { pkgs, lib, ... }: withSystem pkgs.system ({ config, ... }: { - source-overrides = - lib.mapAttrs (_: v: v.root) - config.haskellProjects.default.packages; - }); - # The 'input' module contains only dependency overrides. - input = { pkgs, ... }: withSystem pkgs.system ({ config, ... }: { - inherit (config.haskellProjects.default) - source-overrides overrides; - }); - }; - in - defaults; + config.haskellFlakeProjectModules = { + output = { pkgs, lib, ... }: withSystem pkgs.system ({ config, ... }: + config.haskellProjects."default".defaults.projectModules.output + ); + }; }]; }; }; diff --git a/nix/modules/project.nix b/nix/modules/project.nix deleted file mode 100644 index ae71454..0000000 --- a/nix/modules/project.nix +++ /dev/null @@ -1,243 +0,0 @@ -# Definition of the `haskellProjects.${name}` submodule's `config` -{ self, config, lib, pkgs, ... }: -let - inherit (lib) - mkOption - types; - inherit (types) - raw; - - appType = import ../types/app-type.nix { inherit pkgs lib; }; - haskellOverlayType = import ../types/haskell-overlay-type.nix { inherit lib; }; - - outputsSubmodule = types.submodule { - options = { - finalOverlay = mkOption { - type = types.raw; - readOnly = true; - internal = true; - }; - finalPackages = mkOption { - # This must be raw because the Haskell package set also contains functions. - type = types.attrsOf types.raw; - readOnly = true; - description = '' - The final Haskell package set including local packages and any - overrides, on top of `basePackages`. - ''; - }; - packages = mkOption { - type = types.attrsOf packageInfoSubmodule; - readOnly = true; - description = '' - Package information for all local packages. Contains the following keys: - - - `package`: The Haskell package derivation - - `executables`: Attrset of executables found in the .cabal file - ''; - }; - apps = mkOption { - type = types.attrsOf appType; - readOnly = true; - description = '' - Flake apps for each Cabal executable in the project. - ''; - }; - }; - }; - - packageInfoSubmodule = types.submodule { - options = { - package = mkOption { - type = types.package; - description = '' - The local package derivation. - ''; - }; - exes = mkOption { - type = types.attrsOf appType; - description = '' - Attrset of executables from `.cabal` file. - - If the associated Haskell project has a separate bin output - (cf. `enableSeparateBinOutput`), then this exes will refer - only to the bin output. - - NOTE: Evaluating up to this option will involve IFD. - ''; - }; - }; - }; -in -{ - imports = [ - ./project/defaults.nix - ./project/packages.nix - ./project/devshell.nix - ]; - options = { - projectRoot = mkOption { - type = types.path; - description = '' - Path to the root of the project directory. - - Chaning this affects certain functionality, like where to - look for the 'cabal.project' file. - ''; - default = self; - defaultText = "Top-level directory of the flake"; - }; - debug = mkOption { - type = types.bool; - default = false; - description = '' - Whether to enable verbose trace output from haskell-flake. - - Useful for debugging. - ''; - }; - log = mkOption { - type = types.attrsOf (types.functionTo types.raw); - default = import ../logging.nix { inherit (config) debug; }; - internal = true; - readOnly = true; - description = '' - Internal logging module - ''; - }; - basePackages = mkOption { - type = types.attrsOf raw; - description = '' - Which Haskell package set / compiler to use. - - You can effectively select the GHC version here. - - To get the appropriate value, run: - - nix-env -f "" -qaP -A haskell.compiler - - And then, use that in `pkgs.haskell.packages.ghc` - ''; - example = "pkgs.haskell.packages.ghc924"; - default = pkgs.haskellPackages; - defaultText = lib.literalExpression "pkgs.haskellPackages"; - }; - source-overrides = mkOption { - type = types.attrsOf (types.oneOf [ types.path types.str ]); - description = '' - Source overrides for Haskell packages - - You can either assign a path to the source, or Hackage - version string. - ''; - default = { }; - }; - overrides = mkOption { - type = haskellOverlayType; - description = '' - Cabal package overrides for this Haskell project - - For handy functions, see - - - **WARNING**: When using `imports`, multiple overlays - will be merged using `lib.composeManyExtensions`. - However the order the overlays are applied can be - arbitrary (albeit deterministic, based on module system - implementation). Thus, the use of `overrides` via - `imports` is not officiallly supported. If you'd like - to see proper support, add your thumbs up to - . - ''; - default = self: super: { }; - defaultText = lib.literalExpression "self: super: { }"; - }; - - outputs = mkOption { - type = outputsSubmodule; - description = '' - The flake outputs generated for this project. - - This is an internal option, not meant to be set by the user. - ''; - }; - autoWire = - let - outputTypes = [ "packages" "checks" "apps" "devShells" ]; - in - mkOption { - type = types.listOf (types.enum outputTypes); - description = '' - List of flake output types to autowire. - - Using an empty list will disable autowiring entirely, - enabling you to manually wire them using - `config.haskellProjects..outputs`. - ''; - default = outputTypes; - }; - }; - config = - let - inherit (config.outputs) finalPackages packages; - - localPackagesOverlay = self: _: - let - build-haskell-package = import ../build-haskell-package.nix { - inherit pkgs lib self; - inherit (config) log; - }; - in - lib.mapAttrs build-haskell-package 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 - ]; - - buildPackageInfo = name: value: { - package = finalPackages.${name}; - exes = - let - haskell-parsers = import ../haskell-parsers { - inherit pkgs lib; - throwError = msg: config.log.throwError '' - Unable to determine executable names for package ${name}: - - ${msg} - ''; - }; - exeNames = haskell-parsers.getCabalExecutables value.root; - in - lib.listToAttrs - (map - (exe: - lib.nameValuePair exe { - program = "${lib.getBin finalPackages.${name}}/bin/${exe}"; - } - ) - exeNames - ); - }; - - in - { - outputs = { - inherit finalOverlay; - - finalPackages = config.basePackages.extend finalOverlay; - - packages = lib.mapAttrs buildPackageInfo config.packages; - - apps = - lib.mkMerge - (lib.mapAttrsToList (_: packageInfo: packageInfo.exes) packages); - }; - }; -} diff --git a/nix/modules/project/default.nix b/nix/modules/project/default.nix new file mode 100644 index 0000000..c186050 --- /dev/null +++ b/nix/modules/project/default.nix @@ -0,0 +1,98 @@ +# Definition of the `haskellProjects.${name}` submodule's `config` +{ self, name, config, lib, pkgs, ... }: +let + inherit (lib) + mkOption + types; + inherit (types) + raw; +in +{ + imports = [ + ./defaults.nix + ./packages + ./settings + ./devshell.nix + ./outputs.nix + ]; + options = { + projectRoot = mkOption { + type = types.path; + description = '' + Path to the root of the project directory. + + Chaning this affects certain functionality, like where to + look for the 'cabal.project' file. + ''; + default = self; + defaultText = "Top-level directory of the flake"; + }; + projectFlakeName = mkOption { + type = types.nullOr types.str; + description = '' + A descriptive name for the flake in which this project resides. + + If unspecified, the Nix store path's basename will be used. + ''; + default = null; + apply = cls: + if cls == null + then builtins.baseNameOf config.projectRoot + else cls; + }; + debug = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable verbose trace output from haskell-flake. + + Useful for debugging. + ''; + }; + log = mkOption { + type = types.attrsOf (types.functionTo types.raw); + default = import ../../logging.nix { + name = config.projectFlakeName + "#haskellProjects." + name; + inherit (config) debug; + }; + internal = true; + readOnly = true; + description = '' + Internal logging module + ''; + }; + basePackages = mkOption { + type = types.attrsOf raw; + description = '' + Which Haskell package set / compiler to use. + + You can effectively select the GHC version here. + + To get the appropriate value, run: + + nix-env -f "" -qaP -A haskell.compiler + + And then, use that in `pkgs.haskell.packages.ghc` + ''; + example = "pkgs.haskell.packages.ghc924"; + default = pkgs.haskellPackages; + defaultText = lib.literalExpression "pkgs.haskellPackages"; + }; + autoWire = + let + outputTypes = [ "packages" "checks" "apps" "devShells" ]; + in + mkOption { + type = types.listOf (types.enum outputTypes); + description = '' + List of flake output types to autowire. + + Using an empty list will disable autowiring entirely, + enabling you to manually wire them using + `config.haskellProjects..outputs`. + ''; + default = outputTypes; + }; + }; +} + diff --git a/nix/modules/project/defaults.nix b/nix/modules/project/defaults.nix index 59d1652..6e3756e 100644 --- a/nix/modules/project/defaults.nix +++ b/nix/modules/project/defaults.nix @@ -1,5 +1,5 @@ # A module representing the default values used internally by haskell-flake. -{ lib, ... }: +{ lib, pkgs, config, ... }: let inherit (lib) mkOption @@ -9,10 +9,18 @@ let in { options.defaults = { + enable = mkOption { + type = types.bool; + description = '' + Whether to enable haskell-flake's default settings for this project. + ''; + default = true; + }; + devShell.tools = mkOption { type = functionTo (types.attrsOf (types.nullOr types.package)); description = ''Build tools always included in devShell''; - default = hp: with hp; { + default = hp: with hp; lib.optionalAttrs config.defaults.enable { inherit cabal-install haskell-language-server @@ -20,5 +28,89 @@ in hlint; }; }; + + packages = mkOption { + type = types.lazyAttrsOf types.deferredModule; + description = ''Local packages scanned from projectRoot''; + default = + let + haskell-parsers = import ../../haskell-parsers { + inherit pkgs lib; + throwError = msg: config.log.throwError '' + A default value for `packages` cannot be auto-determined: + + ${msg} + + Please specify the `packages` option manually or change your project configuration (cabal.project). + ''; + }; + localPackages = lib.pipe config.projectRoot [ + haskell-parsers.findPackagesInCabalProject + (lib.mapAttrs (_: path: { + # The rest of the module options are not defined, because we'll use + # the submodule defaults. + source = path; + })) + ]; + in + lib.optionalAttrs config.defaults.enable localPackages; + apply = x: + config.log.traceDebug "defaults.packages = ${builtins.toJSON x}" x; + defaultText = lib.literalMD '' + If you have a `cabal.project` file (under `projectRoot`), those packages + are automatically discovered. Otherwise, the top-level .cabal file is + used to discover the only local package. + + haskell-flake currently supports a limited range of syntax for + `cabal.project`. Specifically it requires an explicit list of package + directories under the "packages" option. + ''; + }; + + settings.default = mkOption { + type = types.deferredModule; + description = '' + Default settings for all packages in `packages` option. + ''; + defaultText = '' + - Speed up builds by disabling haddock and library profiling. + - Separate bin output (for reduced closure size when using `getBin` in apps) + + This uses `local.toDefinedProject` option to determine which packages to + override. Thus, it applies to both local packages as well as + transitively imported packags that are local to that flake (managed by + haskell-flake). The goal being to use the same configuration + consistently for all packages using haskell-flake. + ''; + default = + let + localSettings = { name, package, config, ... }: + lib.optionalAttrs (package.local.toDefinedProject or false) { + # Disabling haddock and profiling is mainly to speed up Nix builds. + haddock = lib.mkDefault false; # Because, this is end-user software. No need for library docs. + libraryProfiling = lib.mkDefault false; # Avoid double-compilation. + separateBinOutput = lib.mkDefault ( + # Reduce closure size + if package.cabal.executables == [ ] + then null + else true + ); + }; + in + if config.defaults.enable then localSettings else { }; + }; + + projectModules.output = mkOption { + type = types.deferredModule; + description = '' + A haskell-flake project module that exports the `packages` and + `settings` options to the consuming flake. This enables the use of this + flake's Haskell package as a dependency, re-using its overrides. + ''; + default = lib.optionalAttrs config.defaults.enable { + inherit (config) + packages settings; + }; + }; }; } diff --git a/nix/modules/project/devshell.nix b/nix/modules/project/devshell.nix index a568138..998445c 100644 --- a/nix/modules/project/devshell.nix +++ b/nix/modules/project/devshell.nix @@ -93,9 +93,12 @@ in }; devShell = finalPackages.shellFor (mkShellArgs // { packages = p: + let + localPackages = (lib.filterAttrs (k: v: v.local.toCurrentProject) config.packages); + in map (name: p."${name}") - (lib.attrNames config.packages); + (lib.attrNames localPackages); withHoogle = true; extraDependencies = p: let o = mkShellArgs.extraDependencies or (_: { }) p; diff --git a/nix/modules/project/outputs.nix b/nix/modules/project/outputs.nix new file mode 100644 index 0000000..2dce15d --- /dev/null +++ b/nix/modules/project/outputs.nix @@ -0,0 +1,120 @@ +# haskellProjects..outputs module. +{ config, lib, pkgs, ... }: +let + inherit (lib) + mkOption + types; + + appType = import ../../types/app-type.nix { inherit pkgs lib; }; + + outputsSubmodule = types.submodule { + options = { + finalOverlay = mkOption { + type = types.raw; + readOnly = true; + internal = true; + }; + finalPackages = mkOption { + # This must be raw because the Haskell package set also contains functions. + type = types.attrsOf types.raw; + readOnly = true; + description = '' + The final Haskell package set including local packages and any + overrides, on top of `basePackages`. + ''; + }; + packages = mkOption { + type = types.attrsOf packageInfoSubmodule; + readOnly = true; + description = '' + Package information for all local packages. Contains the following keys: + + - `package`: The Haskell package derivation + - `exes`: Attrset of executables found in the .cabal file + ''; + }; + apps = mkOption { + type = types.attrsOf appType; + readOnly = true; + description = '' + Flake apps for each Cabal executable in the project. + ''; + }; + }; + }; + + packageInfoSubmodule = types.submodule { + options = { + package = mkOption { + type = types.package; + description = '' + The local package derivation. + ''; + }; + exes = mkOption { + type = types.attrsOf appType; + description = '' + Attrset of executables from `.cabal` file. + + If the associated Haskell project has a separate bin output + (cf. `enableSeparateBinOutput`), then this exe will refer + only to the bin output. + + NOTE: Evaluating up to this option will involve IFD. + ''; + }; + }; + }; +in +{ + options = { + outputs = mkOption { + type = outputsSubmodule; + description = '' + The flake outputs generated for this project. + + This is an internal option, not meant to be set by the user. + ''; + }; + }; + config = + let + # Subet of config.packages that are local to the project. + localPackages = + lib.filterAttrs (_: cfg: cfg.local.toCurrentProject) config.packages; + + finalOverlay = lib.composeManyExtensions [ + config.packagesOverlay + config.settingsOverlay + ]; + + finalPackages = config.basePackages.extend finalOverlay; + + buildPackageInfo = name: value: { + package = finalPackages.${name}; + exes = + lib.listToAttrs + (map + (exe: + lib.nameValuePair exe { + program = "${lib.getBin finalPackages.${name}}/bin/${exe}"; + } + ) + value.cabal.executables + ); + }; + + in + { + outputs = { + inherit finalOverlay finalPackages; + + packages = lib.mapAttrs buildPackageInfo localPackages; + + apps = + lib.mkMerge + (lib.mapAttrsToList (_: packageInfo: packageInfo.exes) config.outputs.packages); + }; + }; +} + diff --git a/nix/modules/project/packages.nix b/nix/modules/project/packages.nix deleted file mode 100644 index dfa5925..0000000 --- a/nix/modules/project/packages.nix +++ /dev/null @@ -1,56 +0,0 @@ -# Definition of the `haskellProjects.${name}` submodule's `config` -{ name, config, lib, pkgs, ... }: -let - inherit (lib) - mkOption - types; - - haskell-parsers = import ../../haskell-parsers { - inherit pkgs lib; - throwError = msg: config.log.throwError '' - A default value for `packages` cannot be auto-determined: - - ${msg} - - Please specify the `packages` option manually or change your project configuration (cabal.project). - ''; - }; - - packageSubmodule = with types; submodule { - options = { - root = mkOption { - type = path; - description = '' - Path containing the Haskell package's `.cabal` file. - ''; - }; - }; - }; - -in -{ - options = { - packages = mkOption { - type = types.lazyAttrsOf packageSubmodule; - description = '' - Set of local packages in the project repository. - - If you have a `cabal.project` file (under `projectRoot`), those packages - are automatically discovered. Otherwise, the top-level .cabal file is - used to discover the only local package. - - haskell-flake currently supports a limited range of syntax for - `cabal.project`. Specifically it requires an explicit list of package - directories under the "packages" option. - ''; - default = - lib.pipe config.projectRoot [ - haskell-parsers.findPackagesInCabalProject - (x: config.log.traceDebug "config.haskellProjects.${name}.packages = ${builtins.toJSON x}" x) - - (lib.mapAttrs (_: path: { root = path; })) - ]; - defaultText = lib.literalMD "autodiscovered by reading `self` files."; - }; - }; -} diff --git a/nix/modules/project/packages/default.nix b/nix/modules/project/packages/default.nix new file mode 100644 index 0000000..f46936e --- /dev/null +++ b/nix/modules/project/packages/default.nix @@ -0,0 +1,74 @@ +# Definition of the `haskellProjects.${name}` submodule's `config` +project@{ name, lib, pkgs, ... }: +let + inherit (lib) + types; + + packageSubmodule = import ./package.nix { inherit project lib pkgs; }; + + # Merge the list of attrset of modules. + mergeModuleAttrs = + lib.zipAttrsWith (k: vs: { imports = vs; }); + + tracePackages = k: x: + project.config.log.traceDebug "${k} ${builtins.toJSON x}" x; +in +{ + options = { + packages = lib.mkOption { + type = types.lazyAttrsOf types.deferredModule; + default = { }; + apply = packages: + let + packages' = + # Merge user-provided 'packages' with 'defaults.packages'. + # + # Note that the user can override the latter too if they wish. + mergeModuleAttrs + [ project.config.defaults.packages packages ]; + in + tracePackages "${name}.packages:apply" ( + lib.mapAttrs + (name: v: + (lib.evalModules { + modules = [ packageSubmodule v ]; + specialArgs = { inherit name pkgs; }; + }).config + ) + packages'); + + description = '' + Additional packages to add to `basePackages`. + + Local packages are added automatically (see `config.defaults.packages`): + + You can also override the source for existing packages here. + ''; + }; + + packagesOverlay = lib.mkOption { + type = import ../../../types/haskell-overlay-type.nix { inherit lib; }; + description = '' + The Haskell overlay computed from `packages` modules. + ''; + internal = true; + default = self: super: + let + inherit (project.config) log; + isPathUnderNixStore = path: builtins.hasContext (builtins.toString path); + build-haskell-package = import ../../../build-haskell-package.nix { + inherit pkgs lib self super log; + }; + getOrMkPackage = name: cfg: + if isPathUnderNixStore cfg.source + then + log.traceDebug "${name}.callCabal2nix ${cfg.source}" + (build-haskell-package name cfg.source) + else + log.traceDebug "${name}.callHackage ${cfg.source}" + (self.callHackage name cfg.source { }); + in + lib.mapAttrs getOrMkPackage project.config.packages; + }; + }; +} diff --git a/nix/modules/project/packages/package.nix b/nix/modules/project/packages/package.nix new file mode 100644 index 0000000..e7ea9fd --- /dev/null +++ b/nix/modules/project/packages/package.nix @@ -0,0 +1,82 @@ +# haskellProjects..packages. module. +{ project, lib, pkgs, ... }: +let + inherit (lib) + mkOption + types; + # TODO: DRY + isPathUnderNixStore = path: builtins.hasContext (builtins.toString path); + + # Whether the 'path' is local to `project.config.projectRoot` + localToProject = path: + path != null && + isPathUnderNixStore path && + lib.strings.hasPrefix "${project.config.projectRoot}" "${path}"; +in +{ name, config, ... }: { + options = { + source = mkOption { + type = import ../../../types/haskell-source-type.nix { inherit lib; }; + description = '' + Source refers to a Haskell package defined by one of the following: + + - Path containing the Haskell package's `.cabal` file. + - Hackage version string + ''; + }; + + cabal.executables = mkOption { + type = types.nullOr (types.listOf types.string); + description = '' + List of executable names found in the cabal file of the package. + + The value is null if 'source' option is Hackage version. + ''; + default = + let + haskell-parsers = import ../../../haskell-parsers { + inherit pkgs lib; + throwError = msg: project.config.log.throwError '' + Unable to determine executable names for package ${name}: + + ${msg} + ''; + }; + in + if isPathUnderNixStore config.source + then haskell-parsers.getCabalExecutables config.source + else null; # cfg.source is Hackage version; nothing to do. + }; + + local.toCurrentProject = mkOption { + type = types.bool; + description = '' + Whether this package is local to the project that is importing it. + ''; + internal = true; + readOnly = true; + # We use 'apply' rather than 'default' to make this evaluation lazy at + # call site (which could be different projectRoot) + apply = _: + localToProject config.source; + defaultText = '' + Computed automatically if package 'source' is under 'projectRoot' of the + importing project. + ''; + }; + + local.toDefinedProject = mkOption { + type = types.bool; + description = '' + Whether this package is local to the project it is defined in. + ''; + internal = true; + default = + localToProject config.source; + defaultText = '' + Computed automatically if package 'source' is under 'projectRoot' of the + defining project. + ''; + }; + }; +} diff --git a/nix/modules/project/settings/all.nix b/nix/modules/project/settings/all.nix new file mode 100644 index 0000000..0be0578 --- /dev/null +++ b/nix/modules/project/settings/all.nix @@ -0,0 +1,323 @@ +{ pkgs, lib, config, ... }: +let + inherit (lib) types; + inherit (import ./lib.nix { + inherit lib config; + }) mkCabalSettingOptions; + + # Convenient way to create multiple settings using `mkCabalSettingOptions` + cabalSettingsFrom = + lib.mapAttrsToList (name: args: { + options = mkCabalSettingOptions (args // { + inherit name; + }); + }); +in +{ + # NOTE: These settings are based on the functions in: + # https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/haskell-modules/lib/compose.nix + # + # Some functions (like checkUnusedPackages) are not included here, but we + # probably should, especially if there is demand. + imports = with pkgs.haskell.lib.compose; cabalSettingsFrom { + check = { + type = types.bool; + description = '' + Whether to run cabal tests as part of the nix build + ''; + impl = enable: + if enable then doCheck else dontCheck; + }; + jailbreak = { + type = types.bool; + description = '' + Remove version bounds from this package's cabal file. + ''; + impl = enable: + if enable then doJailbreak else dontJailbreak; + }; + broken = { + type = types.bool; + description = '' + Whether to mark the package as broken + ''; + impl = enable: + if enable then markBroken else unmarkBroken; + }; + brokenVersions = { + type = types.attrsOf types.string; + description = '' + List of versions that are known to be broken. + ''; + impl = versions: + let + markBrokenVersions = vs: drv: + builtins.foldl' markBrokenVersion drv vs; + in + markBrokenVersions versions; + }; + haddock = { + type = types.bool; + description = '' + Whether to build the haddock documentation. + ''; + impl = enable: + if enable then doHaddock else dontHaddock; + }; + coverage = { + type = types.bool; + description = '' + Modifies thae haskell package to disable the generation + and installation of a coverage report. + ''; + impl = enable: + if enable then doCoverage else dontCoverage; + }; + benchmark = { + type = types.bool; + description = '' + Enables dependency checking and compilation + for benchmarks listed in the package description file. + Benchmarks are, however, not executed at the moment. + ''; + impl = enable: + if enable then doBenchmark else dontBenchmark; + }; + libraryProfiling = { + type = types.bool; + description = '' + Build the library for profiling by default. + ''; + impl = enable: + if enable then enableLibraryProfiling else disableLibraryProfiling; + }; + executableProfiling = { + type = types.bool; + description = '' + Build the executable with profiling enabled. + ''; + impl = enable: + if enable then enableExecutableProfiling else disableExecutableProfiling; + }; + sharedExecutables = { + type = types.bool; + description = '' + Build the executables as shared libraries. + ''; + impl = enable: + if enable then enableSharedExecutables else disableSharedExecutables; + }; + sharedLibraries = { + type = types.bool; + description = '' + Build the libraries as shared libraries. + ''; + impl = enable: + if enable then enableSharedLibraries else disableSharedLibraries; + }; + deadCodeElimination = { + type = types.bool; + description = '' + Enable dead code elimination. + ''; + impl = enable: + if enable then enableDeadCodeElimination else disableDeadCodeElimination; + }; + staticLibraries = { + type = types.bool; + description = '' + Build the libraries as static libraries. + ''; + impl = enable: + if enable then enableStaticLibraries else disableStaticLibraries; + }; + extraBuildDepends = { + type = types.listOf types.package; + description = '' + Extra build dependencies for the package. + ''; + impl = addBuildDepends; + }; + extraBuildTools = { + type = types.listOf types.package; + description = '' + Extra build tools for the package. + ''; + impl = addBuildTools; + }; + extraTestToolDepends = { + type = types.listOf types.package; + description = '' + Extra test tool dependencies for the package. + ''; + impl = addTestToolDepends; + }; + extraPkgconfigDepends = { + type = types.listOf types.package; + description = '' + Extra pkgconfig dependencies for the package. + ''; + impl = addPkgconfigDepends; + }; + extraSetupDepends = { + type = types.listOf types.package; + description = '' + Extra setup dependencies for the package. + ''; + impl = addSetupDepends; + }; + extraConfigureFlags = { + type = types.listOf types.string; + description = '' + Extra flags to pass to 'cabal configure' + ''; + impl = appendConfigureFlags; + }; + extraBuildFlags = { + type = types.listOf types.string; + description = '' + Extra flags to pass to 'cabal build' + ''; + impl = appendBuildFlags; + }; + removeConfigureFlags = { + type = types.listOf types.string; + description = '' + Flags to remove from the default flags passed to 'cabal configure' + ''; + impl = + let + removeConfigureFlags = flags: drv: + builtins.foldl' removeConfigureFlag drv flags; + in + removeConfigureFlags; + }; + cabalFlags = { + type = types.attrsOf types.bool; + description = '' + Cabal flags to enable or disable explicitly. + ''; + impl = flags: drv: + let + enabled = lib.filterAttrs (_: v: v) flags; + disabled = lib.filterAttrs (_: v: !v) flags; + enableCabalFlags = fs: drv: builtins.foldl' enableCabalFlag drv fs; + disableCabalFlags = fs: drv: builtins.foldl' disableCabalFlag drv fs; + in + lib.pipe drv [enableCabalFlag disableCabalFlag]; + }; + patches = { + type = types.listOf types.path; + description = '' + A list of patches to apply to the package. + ''; + impl = appendPatches; + }; + justStaticExecutables = { + type = types.bool; + description = '' + Link executables statically against haskell libs to reduce closure size + ''; + impl = enable: + if enable then justStaticExecutables else x: x; + }; + separateBinOutput = { + type = types.bool; + description = '' + Create two outputs for this Haskell package -- 'out' and 'bin'. This is + useful to separate out the binary with a reduced closure size. + ''; + impl = enable: + let + disableSeparateBinOutput = + overrideCabal (drv: { enableSeparateBinOutput = false; }); + in + if enable then enableSeparateBinOutput else disableSeparateBinOutput; + }; + buildTargets = { + type = types.listOf types.string; + description = '' + A list of targets to build. + + By default all cabal executable targets are built. + ''; + impl = setBuildTargets; + }; + hyperlinkSource = { + type = types.bool; + description = '' + Whether to hyperlink the source code in the generated documentation. + ''; + impl = enable: + if enable then doHyperlinkSource else dontHyperlinkSource; + }; + disableHardening = { + type = types.bool; + description = '' + Disable hardening flags for the package. + ''; + impl = enable: + if enable then disableHardening else x: x; + }; + strip = { + type = types.bool; + description = '' + Let Nix strip the binary files. + + This removes debugging symbols. + ''; + impl = enable: + if enable then doStrip else dontStrip; + }; + enableDWARFDebugging = { + type = types.bool; + description = '' + Enable DWARF debugging. + ''; + impl = enable: + if enable then enableDWARFDebugging else x: x; + }; + disableOptimization = { + type = types.bool; + description = '' + Disable core optimizations, significantly speeds up build time + ''; + impl = enable: + if enable then disableOptimization else x: x; + }; + failOnAllWarnings = { + type = types.bool; + description = '' + Turn on most of the compiler warnings and fail the build if any of them occur + ''; + impl = enable: + if enable then failOnAllWarnings else x: x; + }; + triggerRebuild = { + type = types.raw; + description = '' + Add a dummy command to trigger a build despite an equivalent earlier + build that is present in the store or cache. + ''; + impl = triggerRebuild; + }; + + # When none of the above settings is suitable: + custom = { + type = types.functionTo types.package; + description = '' + A custom funtion to apply on the Haskell package. + + Use this only if none of the existing settings are suitable. + + The function must take three arguments: self, super and the package being + applied to. + + Example: + + custom = pkg: builtins.trace pkg.version pkg; + ''; + impl = f: f; + }; + }; +} diff --git a/nix/modules/project/settings/default.nix b/nix/modules/project/settings/default.nix new file mode 100644 index 0000000..da3c3e4 --- /dev/null +++ b/nix/modules/project/settings/default.nix @@ -0,0 +1,81 @@ +project@{ name, pkgs, lib, ... }: + +let + inherit (lib) + types; + traceSettings = hpkg: x: + let + # Convert the settings config (x) to be stripped of functions, so we can + # convert it to JSON for logging. + xSanitized = lib.filterAttrs (s: v: + !(builtins.isFunction v) && !(s == "impl") && v != null) x; + in + project.config.log.traceDebug "settings.${hpkg} ${builtins.toJSON xSanitized}" x; +in +{ + options.settings = lib.mkOption { + type = types.lazyAttrsOf types.deferredModule; + default = { }; + apply = settings: + # Polyfill 'packages'; because overlay's defaults setting merge requires it. + let + packagesEmptySettings = + lib.mapAttrs (_: _: {}) project.config.packages; + in packagesEmptySettings // settings; + description = '' + Overrides for packages in `basePackages` and `packages`. + + Attr values are submodules that take the following arguments: + + - `name`: Package name + - `package`: The reference to the package in `packages` option if it exists, null otherwise. + - `self`/`super`: The 'self' and 'super' (aka. 'final' and 'prev') used in the Haskell overlay. + - `pkgs`: Nixpkgs instance of the module user (import'er). + + Default settings are defined in `project.config.defaults.settings` which can be overriden. + ''; + }; + + options.settingsOverlay = lib.mkOption { + type = import ../../../types/haskell-overlay-type.nix { inherit lib; }; + description = '' + The Haskell overlay computed from `settings` modules, as well as + `defaults.settings.default` module. + ''; + internal = true; + default = self: super: + let + applySettingsFor = name: mod: + let + cfg = (lib.evalModules { + modules = [ + # Settings spec + ./all.nix + + # Default settings + project.config.defaults.settings.default + + # User module + mod + ]; + specialArgs = { + inherit name pkgs self super; + package = project.config.packages.${name} or null; + } // (import ./lib.nix { + inherit lib; + # NOTE: Recursively referring generated config in lib.nix. + config = cfg; + }); + }).config; + in + lib.pipe super.${name} ( + # TODO: Do we care about the *order* of overrides? + # Might be relevant for the 'custom' option. + lib.concatMap + (impl: impl) + (lib.attrValues (traceSettings name cfg).impl) + ); + in + lib.mapAttrs applySettingsFor project.config.settings; + }; +} diff --git a/nix/modules/project/settings/lib.nix b/nix/modules/project/settings/lib.nix new file mode 100644 index 0000000..5005b74 --- /dev/null +++ b/nix/modules/project/settings/lib.nix @@ -0,0 +1,45 @@ +# Provides the `mkCabalSettingOptions` helper for defining settings..???. +{ lib, config, ... }: + +let + inherit (lib) + mkOption + types; + inherit (types) + functionTo listOf; + + mkImplOption = name: f: mkOption { + # [ pkg -> pkg ] + type = listOf (functionTo types.package); + description = '' + Implementation for settings.${name} + ''; + default = + let + cfg = config.${name}; + in + lib.optional (cfg != null) + (f cfg); + }; + + + mkNullableOption = attrs: + mkOption (attrs // { + type = types.nullOr attrs.type; + default = null; + }); + + # This creates `options.${name}` and `options.impl.${name}`. + # + # The user sets the former, whereas the latter provides the list of functions + # to apply on the package (as implementation for this setting). + mkCabalSettingOptions = { name, type, description, impl }: { + "${name}" = mkNullableOption { + inherit type description; + }; + impl."${name}" = mkImplOption name impl; + }; +in +{ + inherit mkCabalSettingOptions; +} diff --git a/nix/modules/projects.nix b/nix/modules/projects.nix index 2145530..4232500 100644 --- a/nix/modules/projects.nix +++ b/nix/modules/projects.nix @@ -15,7 +15,7 @@ in type = types.attrsOf (types.submoduleWith { specialArgs = { inherit pkgs self; }; modules = [ - ./project.nix + ./project ]; }); }; diff --git a/nix/types/haskell-overlay-type.nix b/nix/types/haskell-overlay-type.nix index dd5db6b..7605d8b 100644 --- a/nix/types/haskell-overlay-type.nix +++ b/nix/types/haskell-overlay-type.nix @@ -1,28 +1,13 @@ { lib, ... }: -let - log = import ../logging.nix { }; -in -# WARNING: While the order is deterministic, it is not - # determined by the user. Thus overlays may be applied in - # an unexpected order. - # We need: https://github.com/NixOS/nixpkgs/issues/215486 -lib.types.mkOptionType { - name = "haskellOverlay"; - description = "An Haskell overlay function"; +# Use this instead of types.functionTo, because Haskell overlay functions cannot +# be merged (whereas functionTo's can). +lib.mkOptionType { + name = "haskell-overlay"; + description = '' + A Haskell overlay function taking 'self' and 'super' args. + ''; descriptionClass = "noun"; - # NOTE: This check is not exhaustive, as there is no way - # to check that the function takes two arguments, and - # returns an attrset. check = lib.isFunction; - merge = _loc: defs': - let - defs = - if builtins.length defs' > 1 - then log.traceWarning "Multiple haskell overlays are applied in arbitrary order" defs' - else defs'; - overlays = - map (x: x.value) defs; - in - lib.composeManyExtensions overlays; + merge = lib.mergeOneOption; } diff --git a/nix/types/haskell-source-type.nix b/nix/types/haskell-source-type.nix new file mode 100644 index 0000000..be5f0d0 --- /dev/null +++ b/nix/types/haskell-source-type.nix @@ -0,0 +1,15 @@ +{ lib, ... }: + +let + isPathUnderNixStore = path: builtins.hasContext (builtins.toString path); +in +lib.mkOptionType { + name = "haskell-source"; + description = '' + Path to Haskell package source, or version from Hackage. + ''; + descriptionClass = "noun"; + check = path: + isPathUnderNixStore path || builtins.isString path; + merge = lib.mergeOneOption; +} \ No newline at end of file diff --git a/runtest.sh b/runtest.sh index fbeecae..a8675d4 100755 --- a/runtest.sh +++ b/runtest.sh @@ -13,7 +13,8 @@ TESTS=( ./test/simple ./test/with-subdir ./test/project-module - ./doc + # Disabled due to https://github.com/hercules-ci/flake.parts-website/issues/332 + # ./doc ) for testDir in "${TESTS[@]}" diff --git a/test/simple/flake.nix b/test/simple/flake.nix index 2591719..9eec065 100644 --- a/test/simple/flake.nix +++ b/test/simple/flake.nix @@ -19,11 +19,19 @@ inputs.haskell-flake.flakeModule inputs.check-flake.flakeModule ]; - flake.haskellFlakeProjectModules.default = { pkgs, ... }: { - overrides = self: super: { + flake.haskellFlakeProjectModules.default = { pkgs, lib, ... }: { + packages = { # This is purposefully incorrect (pointing to ./.) because we # expect it to be overriden in perSystem below. - foo = self.callCabal2nix "foo" ./. { }; + foo.source = ./.; + }; + settings = { + # Test that self and super are passed + foo = { self, super, ... }: { + custom = _: builtins.seq + (lib.assertMsg (lib.hasAttr "ghc" self) "self is bad") + super.foo; + }; }; devShell = { tools = hp: { @@ -33,15 +41,16 @@ hlsCheck.enable = true; }; }; - perSystem = { self', pkgs, ... }: { + perSystem = { self', pkgs, lib, ... }: { haskellProjects.default = { # Multiple modules should be merged correctly. imports = [ self.haskellFlakeProjectModules.default ]; - overrides = self: super: { - # This overrides the overlay above (in `flake.*`), because the - # module system merges them in such order. cf. the WARNING in option - # docs. - foo = self.callCabal2nix "foo" (inputs.haskell-multi-nix + /foo) { }; + # Debug logging should work. + debug = true; + packages = { + # Because the module being imported above also defines a root for + # the 'foo' package, we must override it here using `lib.mkForce`. + foo.source = lib.mkForce (inputs.haskell-multi-nix + /foo); }; devShell = { tools = hp: { diff --git a/test/simple/test.sh b/test/simple/test.sh index a2d1bc7..436cdf1 100755 --- a/test/simple/test.sh +++ b/test/simple/test.sh @@ -1,9 +1,18 @@ source ../common.sh set -euxo pipefail +rm -f result result-bin + # First, build the flake logHeader "Testing nix build" nix build ${OVERRIDE_ALL} + +# Test defaults.settings module behaviour, viz: separateBinOutput +test -f result-bin/bin/haskell-flake-test || { + echo "ERROR: separateBinOutput (from defaults.settings) not in effect" + exit 1 +} + # Run the devshell test script in a nix develop shell. logHeader "Testing nix devshell" nix develop ${OVERRIDE_ALL} -c ./test-in-devshell.sh @@ -13,5 +22,6 @@ nix run ${OVERRIDE_ALL} .#app1 # Test non-devshell features: # Checks logHeader "Testing nix flake checks" -nix --option sandbox false \ - build ${OVERRIDE_ALL} -L .#check +nix --option sandbox false build \ + --no-link --print-out-paths \ + ${OVERRIDE_ALL} -L .#check