packages feed

stack 2.13.1 → 2.15.1

raw patch · 195 files changed

+15728/−11613 lines, 195 filesdep ~Win32dep ~aeson-warning-parserdep ~ansi-terminal

Dependency ranges changed: Win32, aeson-warning-parser, ansi-terminal, array, async, base, bytestring, crypton, directory, exceptions, filepath, ghc-boot, hi-file-parser, hpc, hspec, http-client, http-conduit, http-types, mtl, neat-interpolation, optparse-generic, pantry, path, persistent-sqlite, rio-prettyprint, split, template-haskell, transformers, unix-compat, unordered-containers, vector

Files

CONTRIBUTING.md view
@@ -299,7 +299,7 @@ 12 February 2017, was the first LTS Haskell snapshot to provide GHC 8.0.2 which
 comes with `base-4.9.1.0` and `Cabal-1.24.2.0`. Until, at least,
 13 February 2024, Stack releases would aim to support the immediate
-predecessor, GHC 8.0.1 and `base-4.9.0.0` and `Cabal-1.24.0.0`.
+predecessor, GHC 8.0.1 and `base-4.9.0.0`, `Cabal-1.24.0.0` and Haddock 2.17.2.
 
 When a version of the Stack executable actually ceases to support a version of
 GHC and `Cabal`, that should be recorded in Stack's
@@ -350,6 +350,43 @@ stack exec -- sh ./etc/scripts/hlint.sh
 ~~~
 
+## Code syntax
+
+Stack makes use of GHC's `GHC2021` collection of language extensions, which is
+set using the `language` key in the `package.yaml` file.
+
+Stack makes use of single-constructor types where the constructor has a large
+number of fields. Some of those fields have similar types, and so on. Given
+that, Stack makes use of `OverloadedRecordDot`, introduced in GHC 9.2.1. It also
+makes use of `NoFieldSelectors`, also introduced in GHC 9.2.1, and, where
+necessary, `DuplicateRecordFields`. Together, these language extensions enable
+the removal from the names of fields of the prefixes that were used historically
+to indicate the type and make field names unique. This is because the names of
+fields no longer need to be unique in situations where the intended field is
+unambiguous. This allows for a terser syntax without loss of expressiveness.
+For example:
+
+~~~haskell
+let cliTargets = (boptsCLITargets . bcoBuildOptsCLI) bco
+~~~
+
+can become:
+
+~~~haskell
+let cliTargets = bco.buildOptsCLI.targets
+~~~
+
+The intended field is unambiguous in almost all cases. In the case of a few
+record updates it is ambiguous. The name of the field needs to be qualified in
+those cases. For example:
+
+~~~haskell
+import qualified  Stack.Types.Build as ConfigCache ( ConfigCache (..) )
+...
+let ignoreComponents :: ConfigCache -> ConfigCache
+    ignoreComponents cc = cc { ConfigCache.components = Set.empty }
+~~~
+
 ## Code Style
 
 A single code style is not applied consistently to Stack's code and Stack is not
@@ -490,6 +527,7 @@ ### Linting - `lint.yml`
 
 This workflow will run if:
+
 * there is a pull request
 * commits are pushed to these branches: `master`, `stable` and `rc/**`
 
@@ -499,6 +537,7 @@ ### Test suite - `unit-tests.yml`
 
 This workflow will run if:
+
 * there is a pull request
 * commits are pushed to these branches: `master`, `stable` and `rc/**`.
 * requested
@@ -521,6 +560,7 @@ ### Integration-based - `integration-tests.yml`
 
 This workflow will run if:
+
 * there is a pull request
 * commits are pushed to these branches: `master`, `stable` and `rc/**`
 * any tag is created
@@ -562,12 +602,17 @@ That key is imported into GPG and then used by GPG to create a detached signature
 for each file.
 
-### Inactive - `stan.yml`
+### Stan tool - `stan.yml`
 
-Stan is a Haskell static analysis tool. As of 29 August 2022, it does not
-support GHC >= 9.0.1 and Stack is built with GHC >= 9.2.4. Consequently, this
-workflow does not run. Its intent is to apply Stan to Stack.
+[Stan](https://hackage.haskell.org/package/stan) is a Haskell static analysis
+tool. As of `stan-0.1.0.1`, it supports GHC >= 9.6.3 and Stack is built with
+GHC 9.6.4. The tool is configured by the contents of the `.stan.toml` file.
 
+This workflow will run if:
+
+* there is a pull request
+* requested
+
 ## Haskell Language Server
 
 You may be using [Visual Studio Code](https://code.visualstudio.com/) (VS Code)
@@ -714,3 +759,9 @@ would be helpful, we have a `#stack-collaborators` Slack channel in the
 Haskell Foundation workspace. To join the workspace, follow this
 [link](https://haskell-foundation.slack.com/join/shared_invite/zt-z45o9x38-8L55P27r12YO0YeEufcO2w#/shared-invite/email).
+
+## Matrix room
+
+There is also a
+[Haskell Stack room](https://matrix.to/#/#haskell-stack:matrix.org)
+at address `#haskell-stack:matrix.org` on [Matrix](https://matrix.org/).
ChangeLog.md view
@@ -1,5 +1,108 @@ # Changelog
 
+## Unreleased changes
+
+Release notes:
+
+**Changes since v2.15.1:**
+
+Major changes:
+
+Behavior changes:
+
+Other enhancements:
+
+Bug fixes:
+
+## v2.15.1 - 2024-02-09
+
+Release notes:
+
+* After an upgrade from an earlier version of Stack, on first use only,
+  Stack 2.15.1 may warn that it had trouble loading the CompilerPaths cache.
+* The hash used as a key for Stack's pre-compiled package cache has changed,
+  following the dropping of support for Cabal versions older than `1.24.0.0`.
+
+**Changes since v2.13.1:**
+
+Behavior changes:
+
+* Stack does not leave `*.hi` or `*.o` files in the `setup-exe-src` directory of
+  the Stack root, and deletes any corresponding to a `setup-<hash>.hs` or
+  `setup-shim-<hash>.hs` file, to avoid GHC issue
+  [#21250](https://gitlab.haskell.org/ghc/ghc/-/issues/21250).
+* If Stack's Nix integration is not enabled, Stack will notify the user if a
+  `nix` executable is on the PATH. This usually indicates the Nix package
+  manager is available. In YAML configuration files, the `notify-if-nix-on-path`
+  key is introduced, to allow the notification to be muted if unwanted.
+* Drop support for Intero (end of life in November 2019).
+* `stack path --stack-root` no longer sets up Stack's environment and does not
+  load Stack's configuration.
+* Stack no longer locks on configuration, so packages (remote and local) can
+  be configured in parallel. This increases the effective concurrency of builds
+  that before would use fewer threads. Reconsider your `--jobs` setting
+  accordingly. See [#84](https://github.com/commercialhaskell/stack/issues/84).
+* Stack warns that its support for Cabal versions before `2.2.0.0` is deprecated
+  and may be removed in the next version of Stack. Removal would mean that
+  projects using snapshots earlier than `lts-12.0` or `nightly-2018-03-18`
+  (GHC 8.4.1) might no longer build. See
+  [#6377](https://github.com/commercialhaskell/stack/issues/6377).
+* If Stack's `--resolver` option is not specified, Stack's `unpack` command with
+  a package name will seek to update the package index before seeking to
+  download the most recent version of the package in the index.
+* If the version of Cabal (the library) provided with the specified GHC can copy
+  specific components, Stack will copy only the components built and will not
+  build all executable components at least once.
+
+Other enhancements:
+
+* Consider GHC 9.8 to be a tested compiler and remove warnings.
+* Stack can build packages with dependencies on public sub-libraries of other
+  packages.
+* Add flag `--no-init` to Stack's `new` command to skip the initialisation of
+  the newly-created project for use with Stack.
+* The HTML file paths produced at the end of `stack haddock` are printed on
+  separate lines and without a trailing dot.
+* Add option of the form `--doctest-option=<argument>` to `stack build`, where
+  `doctest` is a program recognised by versions of the Cabal library from
+  `1.24.0.0`.
+* Experimental: Add flag `--haddock-for-hackage` to Stack's `build` command
+  (including the `haddock` synonym for `build --haddock`) to enable building
+  local packages with flags to generate Haddock documentation, and an archive
+  file, suitable for upload to Hackage. The form of the Haddock documentation
+  generated for other packages is unaffected.
+* Experimental: Add flag `--documentation` (`-d` for short) to Stack's `upload`
+  command to allow uploading of documentation for packages to Hackage.
+* `stack new` no longer rejects project templates that specify a `package.yaml`
+  in a subdirectory of the project directory.
+* Stack will notify the user if Stack has not been tested with the version of
+  GHC that is being user or a version of Cabal (the library) that has been
+  found. In YAML configuration files, the `notify-if-ghc-untested` and
+  `notify-if-cabal-untested` keys are introduced, to allow the notification to
+  be muted if unwanted.
+* The compiler version is included in Stack's build message (e.g.
+  `stack> build (lib + exe + test) with ghc-9.6.4`).
+* Add flag `--candidate` to Stack's `unpack` command, to allow package
+  candidates to be unpacked locally.
+* Stack will notify the user if a specified architecture value is unknown to
+  Cabal (the library). In YAML configuration files, the `notify-if-arch-unknown`
+  key is introduced, to allow the notification to be muted if unwanted.
+* Add option `--filter <item>` to Stack's `ls dependencies text` command to
+  filter out an item from the results, if present. The item can be `$locals` for
+  all local packages.
+* Add option `--snapshot` as synonym for `--resolver`.
+* Add the `config set snapshot` command, corresponding to the
+  `config set resolver` command.
+
+Bug fixes:
+
+* Fix the `Curator` instance of `ToJSON`, as regards `expect-haddock-failure`.
+* Better error message if a `resolver:` or `snapshot:` value is, in error, a
+  YAML number.
+* Stack accepts all package names that are, in fact, acceptable to Cabal.
+* Stack's `sdist` command can check packages with names that include non-ASCII
+  characters.
+
 ## v2.13.1 - 2023-09-29
 
 Release notes:
@@ -27,8 +130,8 @@   considered a possible GHC build if `libc.musl-x86_64.so.1` is found in `\lib`
   or `\lib64`.
 * No longer supports Cabal versions older than `1.24.0.0`. This means projects
-  using snapshots earlier than `lts-7.0` or `nightly-2016-05-26` will no longer
-  build.
+  using snapshots earlier than `lts-7.0` or `nightly-2016-05-26` (GHC 8.0.1)
+  will no longer build. GHC 8.0.1 comes with Haddock 2.17.2.
 * When unregistering many packages in a single step, Stack can now do that
   efficiently. Stack no longer uses GHC-supplied `ghc-pkg unregister` (which is,
   currently, slower).
@@ -40,6 +143,8 @@ 
 Other enhancements:
 
+* Consider GHC 9.6 to be a tested compiler and remove warnings.
+* Consider Cabal 3.10 to be a tested library and remove warnings.
 * Bump to Hpack 0.36.0.
 * Depend on `pantry-0.9.2`, for support for long filenames and directory names
   in archives created by `git archive`.
@@ -197,8 +302,8 @@   `STACK_ROOT` environment variable.
 * Add `stack path --global-config`, to yield the full path of Stack's
   user-specific global YAML configuration file (`config.yaml`).
-* Add an experimental option, `allow-newer-deps`, which allows users to
-  specify a subset of dependencies for which version bounds should be ignored
+* Experimental: Add option `allow-newer-deps`, which allows users to specify a
+  subset of dependencies for which version bounds should be ignored
   (`allow-newer-deps: ['foo', 'bar']`). This field has no effect unless
   `allow-newer` is enabled.
 
@@ -240,6 +345,8 @@ 
 Other enhancements:
 
+* Consider GHC 9.2 and 9.4 to be tested compilers and remove warnings.
+* Consider Cabal 3.6 and 3.8 to be a tested libraries and remove warnings.
 * Bump to Hpack 0.35.0.
 * On Windows, the installer now sets `DisplayVersion` in the registry, enabling
   tools like `winget` to properly read the version number.
@@ -298,7 +405,6 @@ Other enhancements:
 
 * `stack setup` supports installing GHC for macOS aarch64 (M1)
-
 * `stack upload` supports authentication with a Hackage API key (via
   `HACKAGE_KEY` environment variable).
 
@@ -358,7 +464,6 @@   packages. It also sets now proper working directory when invoked with
   one package. See
   [#5421](https://github.com/commercialhaskell/stack/issues/5421)
-
 * `custom-setup` dependencies are now properly initialized for `stack dist`.
   This makes `explicit-setup-deps` no longer required and that option was
   removed. See
@@ -366,25 +471,20 @@ 
 Other enhancements:
 
+* Consider GHC 9.0 to be a tested compiler and remove warnings.
+* Consider Cabal 3.6 to be a tested library and remove warnings.
 * Nix integration now passes `ghcVersion` (in addition to existing `ghc`) to
   `shell-file` as an identifier that can be looked up in a compiler attribute
   set.
-
 * Nix integration now allows Nix integration if the user is ready in nix-shell.
   This gets rid of "In Nix shell but reExecL is False" error.
-
 * `stack list` is a new command to list package versions in a snapshot.
   See [#5431](https://github.com/commercialhaskell/stack/pull/5431)
-
-* Consider GHC 9.0 a tested compiler and remove warnings.
-
 * `custom-preprocessor-extensions` is a new configuration option for allowing
   Stack to be aware of any custom preprocessors you have added to `Setup.hs`.
   See [#3491](https://github.com/commercialhaskell/stack/issues/3491)
-
 * Added `--candidate` flag to `upload` command to upload a package candidate
   rather than publishing the package.
-
 * Error output using `--no-interleaved-output` no longer prepends indenting
   whitespace. This allows emacs compilation-mode and vim quickfix to locate
   and track errors. See
@@ -395,14 +495,11 @@ * `stack new` now supports branches other than `master` as default for GitHub
   repositories. See
   [#5422](https://github.com/commercialhaskell/stack/issues/5422)
-
 * Ignore all errors from `hi-file-parser`. See
   [#5445](https://github.com/commercialhaskell/stack/issues/5445) and
   [#5486](https://github.com/commercialhaskell/stack/issues/5486).
-
 * Support basic auth in package-indices. See
   [#5509](https://github.com/commercialhaskell/stack/issues/5509).
-
 * Add support for parsing `.hi`. files from GHC 8.10 and 9.0. See
   [hi-file-parser#2](https://github.com/commercialhaskell/hi-file-parser/pull/2).
 
@@ -424,7 +521,6 @@   override the default location of snapshot configuration files. This option
   affects how snapshot synonyms (LTS/Nightly) are expanded to URLs by the
   `pantry` library.
-
 * `docker-network` configuration key added to override docker `--net` arg
 
 Behavior changes:
@@ -750,7 +846,7 @@ * Remove the `stack image` command. With the advent of Docker multistage
   builds, this functionality is no longer useful. For an example, please see
   [Building Haskell Apps with Docker](https://www.fpcomplete.com/blog/2017/12/building-haskell-apps-with-docker).
-* Support building GHC from source (experimental)
+* Experimental: Support building GHC from source
     * Stack now supports building and installing GHC from source. The built GHC
       is uniquely identified by a commit id and an Hadrian "flavour" (Hadrian is
       the newer GHC build system), hence `compiler` can be set to use a GHC
@@ -1633,11 +1729,10 @@   [#3126](https://github.com/commercialhaskell/stack/issues/3126)
 * When using Nix, nix-shell now depends always on git to prevent runtime errors
   while fetching metadata
-* The `stack unpack` command now accepts a form where an explicit
-  Hackage revision hash is specified, e.g. `stack unpack
-  foo-1.2.3@gitsha1:deadbeef`. Note that this should be considered
-  _experimental_, Stack will likely move towards a different hash
-  format in the future.
+* Experimental: The `stack unpack` command now accepts a form where an explicit
+  Hackage revision hash is specified, e.g.
+  `stack unpack foo-1.2.3@gitsha1:deadbeef`. Note that Stack will likely move
+  towards a different hash format in the future.
 * Binary "stack upgrade" will now warn if the installed executable is not
   on the PATH or shadowed by another entry.
 * Allow running tests on tarball created by sdist and upload
@@ -2252,7 +2347,7 @@ * Fix too much rebuilding when enabling/disabling profiling flags.
 * `stack build pkg-1.0` will now build `pkg-1.0` even if the snapshot specifies
   a different version (it introduces a temporary extra-dep)
-* Experimental support for `--split-objs` added
+* Experimental: Support for `--split-objs` added
   [#1284](https://github.com/commercialhaskell/stack/issues/1284).
 * `git` packages with submodules are supported by passing the `--recursive`
   flag to `git clone`.
@@ -2731,7 +2826,7 @@   [#1070](https://github.com/commercialhaskell/stack/pull/1070)
 * Use Stack-installed GHCs for `stack init --solver`
   [#1072](https://github.com/commercialhaskell/stack/issues/1072)
-* New experimental `stack query` command
+* Experimental: Add `stack query` command
   [#1087](https://github.com/commercialhaskell/stack/issues/1087)
 * By default, Stack no longer rebuilds a package due to GHC options changes.
   This behavior can be tweaked with the `rebuild-ghc-options` setting.
LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2015-2023, Stack contributors
+Copyright (c) 2015-2024, Stack contributors
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
README.md view
@@ -24,4 +24,7 @@   [Slack workspace](https://haskell-foundation.slack.com/join/shared_invite/zt-z45o9x38-8L55P27r12YO0YeEufcO2w#/shared-invite/email)
     * `#stack-users` channel, for general Stack discussion
     * `#stack-collaborators` channel, for working on Stack's code base
-* the [Google Group mailing list](https://groups.google.com/g/haskell-stack) for Stack
+* the [Haskell Stack room](https://matrix.to/#/#haskell-stack:matrix.org) on
+  [Matrix](https://matrix.org/)
+* the [Google Group mailing list](https://groups.google.com/g/haskell-stack) for
+  Stack
cabal.config view
@@ -1,42 +1,42 @@ constraints:
-  , Cabal ==3.8.1.0
-  , Cabal-syntax ==3.8.1.0
+  , Cabal ==3.10.1.0
+  , Cabal-syntax ==3.10.1.0
   , Glob ==0.10.2
   , OneTuple ==0.4.1.1
   , QuickCheck ==2.14.3
   , StateVar ==1.2.2
-  , Win32 ==2.12.0.1
+  , Win32 ==2.13.3.0
   , aeson ==2.1.2.1
-  , aeson-warning-parser ==0.1.0
+  , aeson-warning-parser ==0.1.1
   , annotated-wl-pprint ==0.7.0
-  , ansi-terminal ==1.0
+  , ansi-terminal ==1.0.2
   , ansi-terminal-types ==0.11.5
   , appar ==0.1.8
-  , array ==0.5.4.0
+  , array ==0.5.6.0
   , asn1-encoding ==0.9.6
   , asn1-parse ==0.9.5
   , asn1-types ==0.3.4
   , assoc ==1.1
-  , async ==2.2.4
+  , async ==2.2.5
   , attoparsec ==0.14.4
   , attoparsec-aeson ==2.1.0.0
-  , attoparsec-iso8601 ==1.1.0.0
+  , attoparsec-iso8601 ==1.1.0.1
   , auto-update ==0.1.6
-  , base ==4.17.2.0
-  , base-compat ==0.12.3
-  , base-compat-batteries ==0.12.3
-  , base-orphans ==0.9.0
+  , base ==4.18.2.0
+  , base-compat ==0.13.1
+  , base-compat-batteries ==0.13.1
+  , base-orphans ==0.9.1
   , base16-bytestring ==1.0.2.0
   , base64-bytestring ==1.2.1.0
   , basement ==0.0.16
-  , bifunctors ==5.5.15
+  , bifunctors ==5.6.1
   , binary ==0.8.9.1
   , bitvec ==1.1.5.0
   , blaze-builder ==0.4.2.3
   , blaze-html ==0.9.1.2
-  , blaze-markup ==0.8.2.8
+  , blaze-markup ==0.8.3.0
   , byteorder ==1.0.4
-  , bytestring ==0.11.5.2
+  , bytestring ==0.11.5.3
   , casa-client ==0.0.2
   , casa-types ==0.0.2
   , case-insensitive ==1.2.1.0
@@ -53,7 +53,7 @@   , contravariant ==1.5.5
   , cookie ==0.4.6
   , cryptohash-sha256 ==0.11.102.1
-  , crypton ==0.33
+  , crypton ==0.34
   , crypton-conduit ==0.2.3
   , crypton-connection ==0.3.1
   , crypton-x509 ==1.7.6
@@ -62,45 +62,45 @@   , crypton-x509-validation ==1.6.12
   , data-default-class ==0.1.2.0
   , data-fix ==0.3.2
-  , deepseq ==1.4.8.0
-  , digest ==0.0.1.7
-  , directory ==1.3.7.1
+  , deepseq ==1.4.8.1
+  , digest ==0.0.2.0
+  , directory ==1.3.8.1
   , distributive ==0.6.2.1
   , dlist ==1.0
   , easy-file ==0.2.5
   , echo ==0.1.4
   , ed25519 ==0.0.5.0
-  , exceptions ==0.10.5
+  , exceptions ==0.10.7
   , extra ==1.7.14
   , fast-logger ==3.2.2
   , file-embed ==0.0.15.0
   , filelock ==0.1.1.7
-  , filepath ==1.4.2.2
-  , foldable1-classes-compat ==0.1
+  , filepath ==1.4.200.1
   , fsnotify ==0.4.1.0
   , generic-deriving ==1.14.5
   , generically ==0.1.1
   , ghc-bignum ==1.3
-  , ghc-boot ==9.4.7
-  , ghc-boot-th ==9.4.7
-  , ghc-prim ==0.9.1
+  , ghc-boot ==9.6.4
+  , ghc-boot-th ==9.6.4
+  , ghc-prim ==0.10.0
   , githash ==0.1.7.0
-  , hackage-security ==0.6.2.3
+  , hackage-security ==0.6.2.4
   , hashable ==1.4.3.0
-  , hi-file-parser ==0.1.4.0
+  , hi-file-parser ==0.1.6.0
   , hinotify ==0.4.1
   , hourglass ==0.2.12
   , hpack ==0.36.0
-  , hpc ==0.6.1.0
-  , http-api-data ==0.5
-  , http-client ==0.7.14
+  , hpc ==0.6.2.0
+  , http-api-data ==0.5.1
+  , http-client ==0.7.16
   , http-client-tls ==0.3.6.3
-  , http-conduit ==2.3.8.1
+  , http-conduit ==2.3.8.3
   , http-download ==0.2.1.0
-  , http-types ==0.12.3
-  , indexed-traversable ==0.1.2.1
+  , http-types ==0.12.4
+  , indexed-traversable ==0.1.3
   , indexed-traversable-instances ==0.1.1.2
   , infer-license ==0.2.0
+  , integer-conversion ==0.1.0.1
   , integer-gmp ==1.1
   , integer-logarithms ==1.0.3.1
   , iproute ==1.7.12
@@ -108,98 +108,98 @@   , lift-type ==0.1.1.1
   , lifted-base ==0.2.3.12
   , lukko ==0.1.1.3
-  , megaparsec ==9.3.1
+  , megaparsec ==9.5.0
   , memory ==0.18.0
   , microlens ==0.4.13.1
   , microlens-mtl ==0.2.0.3
   , microlens-th ==0.4.3.14
-  , mime-types ==0.1.1.0
+  , mime-types ==0.1.2.0
   , mintty ==0.1.4
   , monad-control ==1.0.3.1
   , monad-logger ==0.3.40
   , monad-loops ==0.4.3
   , mono-traversable ==1.0.15.3
-  , mtl ==2.2.2
+  , mtl ==2.3.1
   , mtl-compat ==0.2.2
   , mustache ==2.4.2
-  , neat-interpolation ==0.5.1.3
+  , neat-interpolation ==0.5.1.4
   , network ==3.1.4.0
   , network-uri ==2.6.4.2
   , old-locale ==1.0.0.7
-  , old-time ==1.1.0.3
+  , old-time ==1.1.0.4
   , open-browser ==0.2.1.0
   , optparse-applicative ==0.18.1.0
   , optparse-simple ==0.1.1.4
-  , pantry ==0.9.2
+  , pantry ==0.9.3.1
   , parsec ==3.1.16.1
   , parser-combinators ==1.3.0
-  , path ==0.9.2
+  , path ==0.9.5
   , path-io ==1.8.1
   , path-pieces ==0.2.1
   , pem ==0.2.4
-  , persistent ==2.14.5.2
-  , persistent-sqlite ==2.13.1.1
+  , persistent ==2.14.6.0
+  , persistent-sqlite ==2.13.3.0
   , persistent-template ==2.12.0.0
   , pretty ==1.1.3.6
   , prettyprinter ==1.7.1
   , prettyprinter-ansi-terminal ==1.1.3
   , primitive ==0.8.0.0
-  , process ==1.6.17.0
+  , process ==1.6.18.0
   , project-template ==0.2.1.0
   , random ==1.2.1.1
   , resource-pool ==0.4.0.0
-  , resourcet ==1.2.6
+  , resourcet ==1.3.0
   , retry ==0.9.3.1
   , rio ==0.1.22.0
   , rio-orphans ==0.1.2.0
-  , rio-prettyprint ==0.1.7.0
+  , rio-prettyprint ==0.1.8.0
   , rts ==1.0.2
-  , safe ==0.3.19
+  , safe ==0.3.21
   , safe-exceptions ==0.1.7.4
   , scientific ==0.3.7.0
   , semialign ==1.3
-  , semigroupoids ==5.3.7
+  , semigroupoids ==6.0.0.1
   , silently ==1.2.5.3
   , socks ==0.6.1
-  , split ==0.2.3.5
-  , splitmix ==0.1.0.4
-  , stack ==2.13.1
+  , split ==0.2.5
+  , splitmix ==0.1.0.5
+  , stack ==2.15.1
   , static-bytes ==0.1.0
   , stm ==2.5.1.0
   , stm-chans ==3.0.0.9
   , streaming-commons ==0.2.2.6
   , strict ==0.5
-  , tagged ==0.8.7
+  , tagged ==0.8.8
   , tar ==0.5.1.1
-  , tar-conduit ==0.4.0
-  , template-haskell ==2.19.0.0
+  , tar-conduit ==0.4.1
+  , template-haskell ==2.20.0.0
   , temporary ==1.3
   , text ==2.0.2
   , text-metrics ==0.3.2
   , text-short ==0.1.5
-  , th-abstraction ==0.4.5.0
+  , th-abstraction ==0.5.0.0
   , th-compat ==0.1.4
   , th-lift ==0.8.4
   , th-lift-instances ==0.1.20
   , these ==1.2
   , time ==1.12.2
   , time-compat ==1.9.6.1
-  , tls ==1.9.0
-  , transformers ==0.5.6.2
+  , tls ==1.8.0
+  , transformers ==0.6.1.0
   , transformers-base ==0.4.6
   , transformers-compat ==0.7.2
-  , typed-process ==0.2.11.0
-  , unix ==2.7.3
-  , unix-compat ==0.7
+  , typed-process ==0.2.11.1
+  , unix ==2.8.4.0
+  , unix-compat ==0.7.1
   , unix-time ==0.4.11
   , unliftio ==0.2.25.0
   , unliftio-core ==0.2.1.0
-  , unordered-containers ==0.2.19.1
-  , uuid-types ==1.0.5
+  , unordered-containers ==0.2.20
+  , uuid-types ==1.0.5.1
   , vault ==0.3.1.5
-  , vector ==0.13.0.0
+  , vector ==0.13.1.0
   , vector-algorithms ==0.9.0.1
-  , vector-stream ==0.1.0.0
+  , vector-stream ==0.1.0.1
   , witherable ==0.4.2
   , yaml ==0.11.11.2
   , zip-archive ==0.4.3
cabal.project view
@@ -33,8 +33,8 @@ -- specified by the snapshot specifed in Stack's project-level YAML
 -- configuration file (`stack.yaml`). The relevant version of GHC can be
 -- confirmed by reviewing the snapshot on Stackage. For example, at:
--- https://www.stackage.org/lts-21.13/cabal.config.
+-- https://www.stackage.org/lts-22.7/cabal.config.
 --
-with-compiler: ghc-9.4.7
+with-compiler: ghc-9.6.4
 import: cabal.config
 packages: .
doc/CONTRIBUTING.md view
@@ -299,7 +299,7 @@ 12 February 2017, was the first LTS Haskell snapshot to provide GHC 8.0.2 which
 comes with `base-4.9.1.0` and `Cabal-1.24.2.0`. Until, at least,
 13 February 2024, Stack releases would aim to support the immediate
-predecessor, GHC 8.0.1 and `base-4.9.0.0` and `Cabal-1.24.0.0`.
+predecessor, GHC 8.0.1 and `base-4.9.0.0`, `Cabal-1.24.0.0` and Haddock 2.17.2.
 
 When a version of the Stack executable actually ceases to support a version of
 GHC and `Cabal`, that should be recorded in Stack's
@@ -350,6 +350,43 @@ stack exec -- sh ./etc/scripts/hlint.sh
 ~~~
 
+## Code syntax
+
+Stack makes use of GHC's `GHC2021` collection of language extensions, which is
+set using the `language` key in the `package.yaml` file.
+
+Stack makes use of single-constructor types where the constructor has a large
+number of fields. Some of those fields have similar types, and so on. Given
+that, Stack makes use of `OverloadedRecordDot`, introduced in GHC 9.2.1. It also
+makes use of `NoFieldSelectors`, also introduced in GHC 9.2.1, and, where
+necessary, `DuplicateRecordFields`. Together, these language extensions enable
+the removal from the names of fields of the prefixes that were used historically
+to indicate the type and make field names unique. This is because the names of
+fields no longer need to be unique in situations where the intended field is
+unambiguous. This allows for a terser syntax without loss of expressiveness.
+For example:
+
+~~~haskell
+let cliTargets = (boptsCLITargets . bcoBuildOptsCLI) bco
+~~~
+
+can become:
+
+~~~haskell
+let cliTargets = bco.buildOptsCLI.targets
+~~~
+
+The intended field is unambiguous in almost all cases. In the case of a few
+record updates it is ambiguous. The name of the field needs to be qualified in
+those cases. For example:
+
+~~~haskell
+import qualified  Stack.Types.Build as ConfigCache ( ConfigCache (..) )
+...
+let ignoreComponents :: ConfigCache -> ConfigCache
+    ignoreComponents cc = cc { ConfigCache.components = Set.empty }
+~~~
+
 ## Code Style
 
 A single code style is not applied consistently to Stack's code and Stack is not
@@ -490,6 +527,7 @@ ### Linting - `lint.yml`
 
 This workflow will run if:
+
 * there is a pull request
 * commits are pushed to these branches: `master`, `stable` and `rc/**`
 
@@ -499,6 +537,7 @@ ### Test suite - `unit-tests.yml`
 
 This workflow will run if:
+
 * there is a pull request
 * commits are pushed to these branches: `master`, `stable` and `rc/**`.
 * requested
@@ -521,6 +560,7 @@ ### Integration-based - `integration-tests.yml`
 
 This workflow will run if:
+
 * there is a pull request
 * commits are pushed to these branches: `master`, `stable` and `rc/**`
 * any tag is created
@@ -562,12 +602,17 @@ That key is imported into GPG and then used by GPG to create a detached signature
 for each file.
 
-### Inactive - `stan.yml`
+### Stan tool - `stan.yml`
 
-Stan is a Haskell static analysis tool. As of 29 August 2022, it does not
-support GHC >= 9.0.1 and Stack is built with GHC >= 9.2.4. Consequently, this
-workflow does not run. Its intent is to apply Stan to Stack.
+[Stan](https://hackage.haskell.org/package/stan) is a Haskell static analysis
+tool. As of `stan-0.1.0.1`, it supports GHC >= 9.6.3 and Stack is built with
+GHC 9.6.4. The tool is configured by the contents of the `.stan.toml` file.
 
+This workflow will run if:
+
+* there is a pull request
+* requested
+
 ## Haskell Language Server
 
 You may be using [Visual Studio Code](https://code.visualstudio.com/) (VS Code)
@@ -714,3 +759,9 @@ would be helpful, we have a `#stack-collaborators` Slack channel in the
 Haskell Foundation workspace. To join the workspace, follow this
 [link](https://haskell-foundation.slack.com/join/shared_invite/zt-z45o9x38-8L55P27r12YO0YeEufcO2w#/shared-invite/email).
+
+## Matrix room
+
+There is also a
+[Haskell Stack room](https://matrix.to/#/#haskell-stack:matrix.org)
+at address `#haskell-stack:matrix.org` on [Matrix](https://matrix.org/).
doc/ChangeLog.md view
@@ -1,5 +1,108 @@ # Changelog
 
+## Unreleased changes
+
+Release notes:
+
+**Changes since v2.15.1:**
+
+Major changes:
+
+Behavior changes:
+
+Other enhancements:
+
+Bug fixes:
+
+## v2.15.1 - 2024-02-09
+
+Release notes:
+
+* After an upgrade from an earlier version of Stack, on first use only,
+  Stack 2.15.1 may warn that it had trouble loading the CompilerPaths cache.
+* The hash used as a key for Stack's pre-compiled package cache has changed,
+  following the dropping of support for Cabal versions older than `1.24.0.0`.
+
+**Changes since v2.13.1:**
+
+Behavior changes:
+
+* Stack does not leave `*.hi` or `*.o` files in the `setup-exe-src` directory of
+  the Stack root, and deletes any corresponding to a `setup-<hash>.hs` or
+  `setup-shim-<hash>.hs` file, to avoid GHC issue
+  [#21250](https://gitlab.haskell.org/ghc/ghc/-/issues/21250).
+* If Stack's Nix integration is not enabled, Stack will notify the user if a
+  `nix` executable is on the PATH. This usually indicates the Nix package
+  manager is available. In YAML configuration files, the `notify-if-nix-on-path`
+  key is introduced, to allow the notification to be muted if unwanted.
+* Drop support for Intero (end of life in November 2019).
+* `stack path --stack-root` no longer sets up Stack's environment and does not
+  load Stack's configuration.
+* Stack no longer locks on configuration, so packages (remote and local) can
+  be configured in parallel. This increases the effective concurrency of builds
+  that before would use fewer threads. Reconsider your `--jobs` setting
+  accordingly. See [#84](https://github.com/commercialhaskell/stack/issues/84).
+* Stack warns that its support for Cabal versions before `2.2.0.0` is deprecated
+  and may be removed in the next version of Stack. Removal would mean that
+  projects using snapshots earlier than `lts-12.0` or `nightly-2018-03-18`
+  (GHC 8.4.1) might no longer build. See
+  [#6377](https://github.com/commercialhaskell/stack/issues/6377).
+* If Stack's `--resolver` option is not specified, Stack's `unpack` command with
+  a package name will seek to update the package index before seeking to
+  download the most recent version of the package in the index.
+* If the version of Cabal (the library) provided with the specified GHC can copy
+  specific components, Stack will copy only the components built and will not
+  build all executable components at least once.
+
+Other enhancements:
+
+* Consider GHC 9.8 to be a tested compiler and remove warnings.
+* Stack can build packages with dependencies on public sub-libraries of other
+  packages.
+* Add flag `--no-init` to Stack's `new` command to skip the initialisation of
+  the newly-created project for use with Stack.
+* The HTML file paths produced at the end of `stack haddock` are printed on
+  separate lines and without a trailing dot.
+* Add option of the form `--doctest-option=<argument>` to `stack build`, where
+  `doctest` is a program recognised by versions of the Cabal library from
+  `1.24.0.0`.
+* Experimental: Add flag `--haddock-for-hackage` to Stack's `build` command
+  (including the `haddock` synonym for `build --haddock`) to enable building
+  local packages with flags to generate Haddock documentation, and an archive
+  file, suitable for upload to Hackage. The form of the Haddock documentation
+  generated for other packages is unaffected.
+* Experimental: Add flag `--documentation` (`-d` for short) to Stack's `upload`
+  command to allow uploading of documentation for packages to Hackage.
+* `stack new` no longer rejects project templates that specify a `package.yaml`
+  in a subdirectory of the project directory.
+* Stack will notify the user if Stack has not been tested with the version of
+  GHC that is being user or a version of Cabal (the library) that has been
+  found. In YAML configuration files, the `notify-if-ghc-untested` and
+  `notify-if-cabal-untested` keys are introduced, to allow the notification to
+  be muted if unwanted.
+* The compiler version is included in Stack's build message (e.g.
+  `stack> build (lib + exe + test) with ghc-9.6.4`).
+* Add flag `--candidate` to Stack's `unpack` command, to allow package
+  candidates to be unpacked locally.
+* Stack will notify the user if a specified architecture value is unknown to
+  Cabal (the library). In YAML configuration files, the `notify-if-arch-unknown`
+  key is introduced, to allow the notification to be muted if unwanted.
+* Add option `--filter <item>` to Stack's `ls dependencies text` command to
+  filter out an item from the results, if present. The item can be `$locals` for
+  all local packages.
+* Add option `--snapshot` as synonym for `--resolver`.
+* Add the `config set snapshot` command, corresponding to the
+  `config set resolver` command.
+
+Bug fixes:
+
+* Fix the `Curator` instance of `ToJSON`, as regards `expect-haddock-failure`.
+* Better error message if a `resolver:` or `snapshot:` value is, in error, a
+  YAML number.
+* Stack accepts all package names that are, in fact, acceptable to Cabal.
+* Stack's `sdist` command can check packages with names that include non-ASCII
+  characters.
+
 ## v2.13.1 - 2023-09-29
 
 Release notes:
@@ -27,8 +130,8 @@   considered a possible GHC build if `libc.musl-x86_64.so.1` is found in `\lib`
   or `\lib64`.
 * No longer supports Cabal versions older than `1.24.0.0`. This means projects
-  using snapshots earlier than `lts-7.0` or `nightly-2016-05-26` will no longer
-  build.
+  using snapshots earlier than `lts-7.0` or `nightly-2016-05-26` (GHC 8.0.1)
+  will no longer build. GHC 8.0.1 comes with Haddock 2.17.2.
 * When unregistering many packages in a single step, Stack can now do that
   efficiently. Stack no longer uses GHC-supplied `ghc-pkg unregister` (which is,
   currently, slower).
@@ -40,6 +143,8 @@ 
 Other enhancements:
 
+* Consider GHC 9.6 to be a tested compiler and remove warnings.
+* Consider Cabal 3.10 to be a tested library and remove warnings.
 * Bump to Hpack 0.36.0.
 * Depend on `pantry-0.9.2`, for support for long filenames and directory names
   in archives created by `git archive`.
@@ -197,8 +302,8 @@   `STACK_ROOT` environment variable.
 * Add `stack path --global-config`, to yield the full path of Stack's
   user-specific global YAML configuration file (`config.yaml`).
-* Add an experimental option, `allow-newer-deps`, which allows users to
-  specify a subset of dependencies for which version bounds should be ignored
+* Experimental: Add option `allow-newer-deps`, which allows users to specify a
+  subset of dependencies for which version bounds should be ignored
   (`allow-newer-deps: ['foo', 'bar']`). This field has no effect unless
   `allow-newer` is enabled.
 
@@ -240,6 +345,8 @@ 
 Other enhancements:
 
+* Consider GHC 9.2 and 9.4 to be tested compilers and remove warnings.
+* Consider Cabal 3.6 and 3.8 to be a tested libraries and remove warnings.
 * Bump to Hpack 0.35.0.
 * On Windows, the installer now sets `DisplayVersion` in the registry, enabling
   tools like `winget` to properly read the version number.
@@ -298,7 +405,6 @@ Other enhancements:
 
 * `stack setup` supports installing GHC for macOS aarch64 (M1)
-
 * `stack upload` supports authentication with a Hackage API key (via
   `HACKAGE_KEY` environment variable).
 
@@ -358,7 +464,6 @@   packages. It also sets now proper working directory when invoked with
   one package. See
   [#5421](https://github.com/commercialhaskell/stack/issues/5421)
-
 * `custom-setup` dependencies are now properly initialized for `stack dist`.
   This makes `explicit-setup-deps` no longer required and that option was
   removed. See
@@ -366,25 +471,20 @@ 
 Other enhancements:
 
+* Consider GHC 9.0 to be a tested compiler and remove warnings.
+* Consider Cabal 3.6 to be a tested library and remove warnings.
 * Nix integration now passes `ghcVersion` (in addition to existing `ghc`) to
   `shell-file` as an identifier that can be looked up in a compiler attribute
   set.
-
 * Nix integration now allows Nix integration if the user is ready in nix-shell.
   This gets rid of "In Nix shell but reExecL is False" error.
-
 * `stack list` is a new command to list package versions in a snapshot.
   See [#5431](https://github.com/commercialhaskell/stack/pull/5431)
-
-* Consider GHC 9.0 a tested compiler and remove warnings.
-
 * `custom-preprocessor-extensions` is a new configuration option for allowing
   Stack to be aware of any custom preprocessors you have added to `Setup.hs`.
   See [#3491](https://github.com/commercialhaskell/stack/issues/3491)
-
 * Added `--candidate` flag to `upload` command to upload a package candidate
   rather than publishing the package.
-
 * Error output using `--no-interleaved-output` no longer prepends indenting
   whitespace. This allows emacs compilation-mode and vim quickfix to locate
   and track errors. See
@@ -395,14 +495,11 @@ * `stack new` now supports branches other than `master` as default for GitHub
   repositories. See
   [#5422](https://github.com/commercialhaskell/stack/issues/5422)
-
 * Ignore all errors from `hi-file-parser`. See
   [#5445](https://github.com/commercialhaskell/stack/issues/5445) and
   [#5486](https://github.com/commercialhaskell/stack/issues/5486).
-
 * Support basic auth in package-indices. See
   [#5509](https://github.com/commercialhaskell/stack/issues/5509).
-
 * Add support for parsing `.hi`. files from GHC 8.10 and 9.0. See
   [hi-file-parser#2](https://github.com/commercialhaskell/hi-file-parser/pull/2).
 
@@ -424,7 +521,6 @@   override the default location of snapshot configuration files. This option
   affects how snapshot synonyms (LTS/Nightly) are expanded to URLs by the
   `pantry` library.
-
 * `docker-network` configuration key added to override docker `--net` arg
 
 Behavior changes:
@@ -750,7 +846,7 @@ * Remove the `stack image` command. With the advent of Docker multistage
   builds, this functionality is no longer useful. For an example, please see
   [Building Haskell Apps with Docker](https://www.fpcomplete.com/blog/2017/12/building-haskell-apps-with-docker).
-* Support building GHC from source (experimental)
+* Experimental: Support building GHC from source
     * Stack now supports building and installing GHC from source. The built GHC
       is uniquely identified by a commit id and an Hadrian "flavour" (Hadrian is
       the newer GHC build system), hence `compiler` can be set to use a GHC
@@ -1633,11 +1729,10 @@   [#3126](https://github.com/commercialhaskell/stack/issues/3126)
 * When using Nix, nix-shell now depends always on git to prevent runtime errors
   while fetching metadata
-* The `stack unpack` command now accepts a form where an explicit
-  Hackage revision hash is specified, e.g. `stack unpack
-  foo-1.2.3@gitsha1:deadbeef`. Note that this should be considered
-  _experimental_, Stack will likely move towards a different hash
-  format in the future.
+* Experimental: The `stack unpack` command now accepts a form where an explicit
+  Hackage revision hash is specified, e.g.
+  `stack unpack foo-1.2.3@gitsha1:deadbeef`. Note that Stack will likely move
+  towards a different hash format in the future.
 * Binary "stack upgrade" will now warn if the installed executable is not
   on the PATH or shadowed by another entry.
 * Allow running tests on tarball created by sdist and upload
@@ -2252,7 +2347,7 @@ * Fix too much rebuilding when enabling/disabling profiling flags.
 * `stack build pkg-1.0` will now build `pkg-1.0` even if the snapshot specifies
   a different version (it introduces a temporary extra-dep)
-* Experimental support for `--split-objs` added
+* Experimental: Support for `--split-objs` added
   [#1284](https://github.com/commercialhaskell/stack/issues/1284).
 * `git` packages with submodules are supported by passing the `--recursive`
   flag to `git clone`.
@@ -2731,7 +2826,7 @@   [#1070](https://github.com/commercialhaskell/stack/pull/1070)
 * Use Stack-installed GHCs for `stack init --solver`
   [#1072](https://github.com/commercialhaskell/stack/issues/1072)
-* New experimental `stack query` command
+* Experimental: Add `stack query` command
   [#1087](https://github.com/commercialhaskell/stack/issues/1087)
 * By default, Stack no longer rebuilds a package due to GHC options changes.
   This behavior can be tweaked with the `rebuild-ghc-options` setting.
doc/GUIDE.md view
@@ -37,8 +37,8 @@ 
 To build your project, Stack uses a project-level configuration file, named
 `stack.yaml`, in the root directory of your project as a sort of blueprint. That
-file contains a reference, called a __resolver__, to the snapshot which your
-package will be built against.
+file contains a reference to the snapshot (also known as a __resolver__) which
+your package will be built against.
 
 Finally, Stack is __isolated__: it will not make changes outside of specific
 Stack directories. Stack-built files generally go in either the Stack root
@@ -253,12 +253,13 @@ - .
 ~~~
 
-The value of the `resolver` key tells Stack *how* to build your package: which
-GHC version to use, versions of package dependencies, and so on. Our value here
-says to use [LTS Haskell 21.13](https://www.stackage.org/lts-21.13), which
-implies GHC 9.4.7 (which is why `stack build` installs that version of GHC if it
-is not already available to Stack). There are a number of values you can use for
-`resolver`, which we'll cover later.
+The value of the [`resolver`](yaml_configuration.md#resolver) key tells Stack
+*how* to build your package: which GHC version to use, versions of package
+dependencies, and so on. Our value here says to use
+[LTS Haskell 22.7](https://www.stackage.org/lts-22.7), which implies GHC 9.6.4
+(which is why `stack build` installs that version of GHC if it is not already
+available to Stack). There are a number of values you can use for `resolver`,
+which we'll cover later.
 
 The value of the `packages` key tells Stack which local packages to build. In
 our simple example, we have only a single package in our project, located in the
@@ -479,7 +480,7 @@ ## Curated package sets
 
 Remember above when `stack new` selected some
-[LTS resolver](https://github.com/commercialhaskell/lts-haskell#readme) for us?
+[LTS snapshot](https://github.com/commercialhaskell/lts-haskell#readme) for us?
 That defined our build plan and available packages. When we tried using the
 `text` package, it just worked, because it was part of the LTS *package set*.
 
@@ -489,22 +490,21 @@ 
 To add `acme-missiles` to the available packages, we'll use the `extra-deps` key
 in the `stack.yaml` file. That key defines extra packages, not present in the
-resolver, that will be needed as dependencies. You can add this like so:
+snapshot, that will be needed as dependencies. You can add this like so:
 
 ~~~yaml
 extra-deps:
-- acme-missiles-0.3 # not in the LTS resolver
+- acme-missiles-0.3 # not in the LTS snapshot
 ~~~
 
 Now `stack build` will succeed.
 
 With that out of the way, let's dig a little bit more into these package sets,
-also known as *snapshots*. We mentioned the LTS resolvers, and you can get quite
+also known as *snapshots*. We mentioned the LTS snapshots, and you can get quite
 a bit of information about it at
 [https://www.stackage.org/lts](https://www.stackage.org/lts), including:
 
-* The appropriate resolver value (`resolver: lts-21.13`, as is currently the
-  latest LTS)
+* The appropriate value (`lts-22.7`, as is currently the latest LTS)
 * The GHC version used
 * A full list of all packages available in this snapshot
 * The ability to perform a Hoogle search on the packages in this snapshot
@@ -520,12 +520,12 @@ If you're not sure which to use, start with LTS Haskell (which Stack will lean
 towards by default as well).
 
-## Resolvers and changing your compiler version
+## Snapshots and changing your compiler version
 
-Let's explore package sets a bit further. Instead of `lts-21.13`, let's change
+Let's explore package sets a bit further. Instead of `lts-22.7`, let's change
 our `stack.yaml` file to use the
 [latest nightly](https://www.stackage.org/nightly). Right now, this is currently
-2023-09-24 - please see the resolver from the link above to get the latest.
+2023-09-24 - please see the snapshot from the link above to get the latest.
 
 Then, commanding `stack build` again will produce:
 
@@ -535,7 +535,7 @@ # build output ...
 ~~~
 
-We can also change resolvers on the command line, which can be useful in a
+We can also change snapshots on the command line, which can be useful in a
 Continuous Integration (CI) setting, like on Travis. For example, command:
 
 ~~~text
@@ -545,7 +545,7 @@ ~~~
 
 When passed on the command line, you also get some additional "short-cut"
-versions of resolvers: `--resolver nightly` will use the newest Nightly resolver
+versions of snapshots: `--resolver nightly` will use the newest Nightly snapshot
 available, `--resolver lts` will use the newest LTS, and `--resolver lts-21`
 will use the newest LTS in the 21.x series. The reason these are only available
 on the command line and not in your `stack.yaml` file is that using them:
@@ -569,9 +569,9 @@ see that different LTS versions use different GHC versions and Stack can handle
 that.
 
-### Other resolver values
+### Other snapshot values
 
-We've mentioned `nightly-YYYY-MM-DD` and `lts-X.Y` values for the resolver.
+We've mentioned `nightly-YYYY-MM-DD` and `lts-X.Y` values for the snapshot.
 There are actually other options available, and the list will grow over time.
 At the time of writing:
 
@@ -579,7 +579,7 @@ * Experimental custom snapshot support
 
 The most up-to-date information can always be found in the
-[stack.yaml documentation](yaml_configuration.md#resolver).
+[stack.yaml documentation](yaml_configuration.md#snapshot).
 
 ## Existing projects
 
@@ -664,7 +664,7 @@ you can uncomment the right one and comment the other one.
 
 Packages may get excluded due to conflicting requirements among user packages or
-due to conflicting requirements between a user package and the resolver
+due to conflicting requirements between a user package and the snapshot
 compiler. If all of the packages have a conflict with the compiler then all of
 them may get commented out.
 
@@ -672,15 +672,15 @@ command which needs the configuration file. The warning can be disabled by
 editing the configuration file and removing it.
 
-#### Using a specific resolver
+#### Using a specific snapshot
 
-Sometimes you may want to use a specific resolver for your project instead of
+Sometimes you may want to use a specific snapshot for your project instead of
 `stack init` picking one for you. You can do that by using
-`stack init --resolver <resolver>`.
+`stack init --resolver <snapshot>`.
 
-You can also init with a compiler resolver if you do not want to use a snapshot.
-That will result in all of your project's dependencies being put under the
-`extra-deps` section.
+You can also init with a compiler snapshot if you do not want to use a
+Stackage snapshot. That will result in all of your project's dependencies being
+put under the `extra-deps` section.
 
 #### Installing the compiler
 
@@ -972,88 +972,44 @@ 
 You can bypass this implicit adding of components by being much more explicit,
 and stating the components directly. For example, the following will not build
-the `helloworld-exe` executable once all executables have been successfully
-built:
+the `helloworld-exe` executable:
 
 ~~~text
-stack clean
+stack purge
 stack build :helloworld-test
-Building all executables for `helloworld' once. After a successful build of all of them, only specified executables will be rebuilt.
-helloworld> configure (lib + exe + test)
+helloworld> configure (lib + test)
 Configuring helloworld-0.1.0.0...
-helloworld> build (lib + exe + test)
+helloworld> build (lib + test) with ghc-9.6.4
 Preprocessing library for helloworld-0.1.0.0..
 Building library for helloworld-0.1.0.0..
 [1 of 2] Compiling Lib
 [2 of 2] Compiling Paths_helloworld
-Preprocessing executable 'helloworld-exe' for helloworld-0.1.0.0..
-Building executable 'helloworld-exe' for helloworld-0.1.0.0..
-[1 of 2] Compiling Main
-[2 of 2] Compiling Paths_helloworld
-Linking .stack-work\dist\<hash>\build\helloworld-exe\helloworld-exe.exe ...
 Preprocessing test suite 'helloworld-test' for helloworld-0.1.0.0..
 Building test suite 'helloworld-test' for helloworld-0.1.0.0..
 [1 of 2] Compiling Main
 [2 of 2] Compiling Paths_helloworld
-Linking .stack-work\dist\<hash>\build\helloworld-test\helloworld-test.exe ...
+[3 of 3] Linking .stack-work\dist\<hash>\build\helloworld-test\helloworld-test.exe
 helloworld> copy/register
 Installing library in ...\helloworld\.stack-work\install\...
-Installing executable helloworld-exe in ...\helloworld\.stack-work\install\...\bin
 Registering library for helloworld-0.1.0.0..
 helloworld> test (suite: helloworld-test)
 
 Test suite not yet implemented
 
-helloworld> Test suite helloworld-test passed
-Completed 2 action(s).
-~~~
 
-We first cleaned our project to clear old results so we know exactly what Stack
-is trying to do. Note that it says it is building all executables for
-`helloworld` once, and that after a successful build of all of them, only
-specified executables will be rebuilt. If we change the source code of
-`test/Spec.hs`, say to:
 
-~~~haskell
-main :: IO ()
-main = putStrLn "Test suite still not yet implemented"
-~~~
-
-and command again:
-
-~~~text
-stack build :helloworld-test
-helloworld-0.1.0.0: unregistering (local file changes: test\Spec.hs)
-helloworld> build (lib + test)
-Preprocessing library for helloworld-0.1.0.0..
-Building library for helloworld-0.1.0.0..
-Preprocessing test suite 'helloworld-test' for helloworld-0.1.0.0..
-Building test suite 'helloworld-test' for helloworld-0.1.0.0..
-[2 of 2] Compiling Main
-Linking .stack-work\dist\<hash>\build\helloworld-test\helloworld-test.exe ...
-helloworld> copy/register
-Installing library in ...\helloworld\.stack-work\install\...
-Installing executable helloworld-exe in ...\helloworld\.stack-work\install\...\bin
-Registering library for helloworld-0.1.0.0..
-helloworld> blocking for directory lock on ...\helloworld\.stack-work\dist\<hash>\build-lock
-helloworld> test (suite: helloworld-test)
-
-Test suite still not yet implemented
-
 helloworld> Test suite helloworld-test passed
 Completed 2 action(s).
 ~~~
 
-Notice that this time it builds the `helloworld-test` test suite, and the
-`helloworld` library (since it's used by the test suite), but it does not build
-the `helloworld-exe` executable.
+We first purged our project to clear old results so we know exactly what Stack
+is trying to do.
 
-And now the final point: in both cases, the last line shows that our command
-also *runs* the test suite it just built. This may surprise some people who
-would expect tests to only be run when using `stack test`, but this design
-decision is what allows the `stack build` command to be as composable as it is
-(as described previously). The same rule applies to benchmarks. To spell it out
-completely:
+The last line shows that our command also *runs* the test suite it just built.
+This may surprise some people who would expect tests to only be run when using
+`stack test`, but this design decision is what allows the `stack build` command
+to be as composable as it is (as described previously). The same rule applies to
+benchmarks. To spell it out completely:
 
 * The `--test` and `--bench` flags simply state which components of a package
   should be built, if no explicit set of components is given
@@ -1249,7 +1205,7 @@ --compiler-exe           Compiler binary (e.g. ghc)
 --compiler-bin           Directory containing the compiler binary (e.g. ghc)
 --compiler-tools-bin     Directory containing binaries specific to a
-                         particular compiler (e.g. intero)
+                         particular compiler
 --local-bin              Directory where Stack installs executables (e.g.
                          ~/.local/bin (Unix-like OSs) or %APPDATA%\local\bin
                          (Windows))
@@ -1340,7 +1296,7 @@ 
 ~~~text
 Run from outside a project, using implicit global project config
-Using latest snapshot resolver: lts-21.13
+Using latest snapshot resolver: lts-22.7
 Writing global (non-project-specific) config file to: /home/michael/.stack/global/stack.yaml
 Note: You can change the snapshot via the resolver field there.
 I installed the stm package via --package stm
@@ -1416,8 +1372,7 @@ noticed that phrase a few times in the output from commands above. Implicit
 global is essentially a hack to allow Stack to be useful in a non-project
 setting. When no implicit global configuration file exists, Stack creates one
-for you with the latest LTS snapshot as the resolver. This allows you to do
-things like:
+for you with the latest LTS snapshot. This allows you to do things like:
 
 * compile individual files easily with `stack ghc`
 * build executables without starting a project, e.g. `stack install pandoc`
doc/GUIDE_advanced.md view
@@ -59,7 +59,7 @@ * [`repl`](ghci.md) - a synonym for `stack ghci`
 * [`run`](run_command.md) - build and run an executable
 * [`runghc`](runghc_command.md) - run `runghc`
-* [`runhaskell`](runghc_command.md) - a synoynm for `stack runghc`
+* [`runhaskell`](runghc_command.md) - a synonym for `stack runghc`
 * [`script`](script_command.md) - run a Haskell source file as a script
 * [`sdist`](sdist_command.md) - create an archive file for a package, in a form
   accepted by Hackage
doc/README.md view
@@ -21,7 +21,8 @@ ## How to install Stack
 
 Stack can be installed on most Unix-like operating systems (including macOS) and
-Windows.
+Windows. It will require at least about 5 GB of disk space, for use with one
+version of GHC.
 
 !!! info
 
@@ -122,10 +123,10 @@ 
     !!! warning
 
-        The Windows installer for Stack 2.9.1, 2.9.3 and 2.11.1 (current) (only)
-        will replace the user `PATH` environment variable (rather than append to
-        it) if a 1024 character limit is exceeded. If the content of your
-        existing user `PATH` is long, preserve it before running the installer.
+        The Windows installer for Stack 2.9.1, 2.9.3 and 2.11.1 (only) will
+        replace the user `PATH` environment variable (rather than append to it)
+        if a 1024 character limit is exceeded. If the content of your existing
+        user `PATH` is long, preserve it before running the installer.
 
     !!! note
 
@@ -325,17 +326,21 @@ 
 ## How to contribute to the maintenance or development of Stack
 
-The following assumes that you already have installed a version of Stack and the
-[Git application](https://git-scm.com/).
+A [guide](CONTRIBUTING.md) is provided to help potential contributors to the
+Stack project.
 
+If you have already installed a version of Stack and the
+[Git application](https://git-scm.com/) the followings steps should get you
+started with building Stack from source with Stack:
+
 1.  Clone the `stack` repository from GitHub with the command:
 
     ~~~text
     git clone https://github.com/commercialhaskell/stack.git
     ~~~
 
-2.  Change the current working directory to the cloned `stack` directory with the
-    command:
+2.  Change the current working directory to the cloned `stack` directory with
+    the command:
 
     ~~~text
     cd stack
doc/Stack_and_VS_Code.md view
@@ -73,11 +73,11 @@ [documentation](yaml_configuration.md#install-ghc) and the `system-ghc`
 [documentation](yaml_configuration.md#system-ghc).
 
-For this workaround to work, each time that a resolver is used that references a
+For this workaround to work, each time that a snapshot is used that references a
 different version of GHC, then GHCup must be used to install it (if GHCup has
-not already installed that version). For example, to use `resolver: lts-21.13`
-(GHC 9.4.7), the command `ghcup install ghc 9.4.7` must have been used to
-install GHC 9.4.7. That may be a minor inconvenience for some people, as one the
+not already installed that version). For example, to use `snapshot: lts-22.7`
+(GHC 9.6.4), the command `ghcup install ghc 9.6.4` must have been used to
+install GHC 9.6.4. That may be a minor inconvenience for some people, as one the
 primary benefits of Stack over other Haskell build tools has been that Stack
 automatically ensures that the necessary version of GHC is available.
 
doc/azure_ci.md view
@@ -28,7 +28,7 @@ * The complex Azure configuration is intended for projects that need to support
   multiple GHC versions and multiple operating systems, such as open source
   libraries to be released to Hackage. It tests against Stack for different
-  resolvers on Linux, macOS and Windows. These are the files for the complex
+  snapshots on Linux, macOS and Windows. These are the files for the complex
   configuration:
   - [azure-pipelines.yml](https://raw.githubusercontent.com/commercialhaskell/stack/stable/doc/azure/azure-pipelines.yml)
     : This is the starter file used by the Azure CI.
@@ -132,7 +132,7 @@ 
 For different GHC versions, you probably want to use different project-level
 configuration files (`stack.yaml`). If you don't want to put a specific
-`stack.yaml` for a particular resolver and still want to test it, you have
+`stack.yaml` for a particular snapshot and still want to test it, you have
 specify your resolver argument in `ARGS` environment variable (you will see an
 example below).
 
doc/build_command.md view
@@ -12,11 +12,11 @@             [--[no-]library-stripping] [--[no-]executable-stripping]
             [--[no-]haddock] [--haddock-arguments HADDOCK_ARGS]
             [--[no-]open] [--[no-]haddock-deps] [--[no-]haddock-internal]
-            [--[no-]haddock-hyperlink-source] [--[no-]copy-bins]
-            [--[no-]copy-compiler-tool] [--[no-]prefetch] [--[no-]keep-going]
-            [--[no-]keep-tmp-files] [--[no-]force-dirty] [--[no-]test]
-            [--[no-]rerun-tests] [--ta|--test-arguments TEST_ARGS] [--coverage]
-            [--no-run-tests] [--test-suite-timeout ARG]
+            [--[no-]haddock-hyperlink-source] [--[no-]haddock-for-hackage]
+            [--[no-]copy-bins] [--[no-]copy-compiler-tool] [--[no-]prefetch]
+            [--[no-]keep-going] [--[no-]keep-tmp-files] [--[no-]force-dirty]
+            [--[no-]test] [--[no-]rerun-tests] [--ta|--test-arguments TEST_ARGS]
+            [--coverage] [--no-run-tests] [--test-suite-timeout ARG]
             [--[no-]tests-allow-stdin] [--[no-]bench]
             [--ba|--benchmark-arguments BENCH_ARGS] [--no-run-benchmarks]
             [--[no-]reconfigure] [--cabal-verbosity VERBOSITY |
@@ -168,7 +168,11 @@ about how these dependencies get specified.
 
 In addition to specifying targets, you can also control what gets built, or
-retained, with the following flags:
+retained, with the flags and options listed below. You can also affect what gets
+built by specifying Cabal (the library) options for the configure step
+of the Cabal build process (for further information, see the documentation for
+the [configure-options](yaml_configuration.md#configure-options) configuration
+option).
 
 ### `--bench` flag
 
@@ -223,29 +227,111 @@ get re-built, so that the documentation links work. The `stack haddock` synonym
 sets this flag.
 
+Stack applies Haddock's `--gen-contents` and `--gen-index` flags to generate a
+single HTML contents and index for multiple sets of Haddock documentation.
+
+!!! warning
+
+    On Windows, the values for the `haddock-interfaces` and `haddock-html` keys
+    in the `*.conf` files for boot packages provided with certain versions of
+    GHC (in its `lib\package.conf.d` directory) can be corrupt and refer to
+    non-existent files and directories. For example, in the case of GHC 9.0.1
+    to GHC 9.8.1 the references are to
+    `${pkgroot}/../../docs/html/libraries/...` or
+    `${pkgroot}/../../doc/html/libraries/...` instead of
+    `${pkgroot}/../docs/html/libraries/...` or
+    `${pkgroot}/../doc/html/libraries/...`. Until those values are corrected,
+    Haddock documentation will be missing links to what those packages expose.
+
 ### `--haddock-arguments` option
 
-`stack haddock --haddock-arguments <haddock_arguments>` passes the specified
+`stack haddock --haddock-arguments <haddock_argument(s)>` passes the specified
 arguments to the Haddock tool.
 
+Specified arguments are separated by spaces. Arguments can be unquoted (if they
+do not contain space or `"` characters) or quoted (`""`). Quoted arguments can
+include 'escaped' characters, escaped with an initial `\` character.
+
+!!! note
+
+    Haddock's `--latex` flag is incompatible with the Haddock flags used by
+    Stack to generate a single HTML contents and index.
+
 ### `--[no-]haddock-deps` flag
 
 Default: Enabled (if building Haddock documnentation)
 
 Unset the flag to disable building Haddock documentation for dependencies.
 
+### `--[no-]haddock-for-haddock` flag
+
+:octicons-beaker-24: Experimental
+
+[:octicons-tag-24: 2.15.1](https://github.com/commercialhaskell/stack/releases/tag/v2.15.1)
+
+Default: Disabled
+
+Set the flag to build local packages with flags to generate Haddock
+documentation suitable for upload to Hackage. The form of the Haddock
+documentation generated for other packages is unaffected.
+
+For each local package:
+
+* the generated Haddock documentation files are in directory
+  `doc\html\<package_version>-docs\`, relative to Stack's dist work directory
+  (see [`stack path --dist-dir`](path_command.md)); and
+* an archive of the `<package_version>-docs` directory and its contents is in
+  Stack's dist work directory.
+
+If the flag is set:
+
+* the [`--[no-]haddock-hyperlink-source`](#-no-haddock-hyperlink-source-flag)
+  flag is ignored and `--haddock-hyperlink-source` is implied;
+* the [`--[no-]haddock-deps`](#-no-haddock-deps-flag) flag is ignored and the
+  default value for the flag is implied;
+* the [`--[no-]haddock-internal`](#-no-haddock-hyperlink-internal-flag) flag is
+  ignored and `--no-haddock-internal` is implied;
+* the [`--[no-]open`](#-no-open-flag) flag is ignored and `--no-open` is
+  implied; and
+* the [`--[no-]force-dirty`](#-no-force-dirty-flag) flag is ignored and
+  `--force-dirty` is implied.
+
+!!! info
+
+    Stack does not distinguish the building of Haddock documentation for Hackage
+    from the building of Haddock documentation generally, which is why the
+    `--force-dirty` flag is implied.
+
+!!! note
+
+    If set, Haddock will warn that `-source-*` options are ignored when
+    `--hyperlinked-source` is enabled. That is due to a known bug in Cabal
+    (the libiary).
+
+!!! note
+
+    If set, Cabal (the library) will report that documentation has been created
+    in `index.html` and `<package_name>.txt` files. Those files do not exist.
+    That false report is due to a known bug in Cabal (the library).
+
 ### `--[no-]haddock-hyperlink-source` flag
 
 Default: Enabled
 
 Unset the flag to disable building building hyperlinked source for Haddock.
 
+If the [`--haddock-for-hackage`](#-no-haddock-for-haddock-flag) flag is passed,
+this flag is ignored.
+
 ### `--[no-]haddock-internal` flag
 
 Default: Disabled
 
 Set the flag to enable building Haddock documentation for internal modules.
 
+If the [`--haddock-for-hackage`](#-no-haddock-for-haddock-flag) flag is passed,
+this flag is ignored.
+
 ### `--[no-]keep-going` flag
 
 Default (`stack build`): Disabled
@@ -336,11 +422,34 @@ 
 ## Controlling what happens after building
 
+### `--benchmark-arguments`, `--ba` option
+
+`stack build --bench --benchmark-arguments=<argument(s)>` will pass the
+specified argument, or arguments, to each benchmark when it is run.
+
+Specified arguments are separated by spaces. Arguments can be unquoted (if they
+do not contain space or `"` characters) or quoted (`""`). Quoted arguments can
+include 'escaped' characters, escaped with an initial `\` character.
+
 ### `--exec` option
 
-`stack build --exec "<command> [<arguments>]"` will run the specified command
+`stack build --exec "<command> [<argument(s)>]"` will run the specified command
 after a successful build.
 
+Specified arguments are separated by spaces. Arguments can be unquoted (if they
+do not contain space or `"` characters) or quoted (`""`). Quoted arguments can
+include 'escaped' characters, escaped with an initial `\` character.
+
+### `--test-arguments`, `--ta` option
+
+`stack build --test --test-arguments=<argument(s)>` will pass the specified
+argument, or arguments, to each test when it is run. This option can be
+specified multiple times.
+
+Specified arguments are separated by spaces. Arguments can be unquoted (if they
+do not contain space or `"` characters) or quoted (`""`). Quoted arguments can
+include 'escaped' characters, escaped with an initial `\` character.
+
 ## Flags affecting GHC's behaviour
 
 ### `--[no-]executable-profiling` flag
@@ -359,20 +468,43 @@ 
 ### `--fast` flag
 
-Pass the flag to build your project with the GHC option `-O0`. `-O0` disables
-GHC's optimisations (which is GHC's default).
+GHC has many flags that specify individual optimisations of the compiler. GHC
+also uses its `-O*` flags to specify convenient 'packages' of GHC optimisation
+flags. GHC's flags are evaluated from left to right and later flags can override
+the effect of earlier ones.
 
+If no GHC `-O*` type flag is specified, GHC takes that to mean "Please
+compile quickly; I'm not over-bothered about compiled-code quality." GHC's `-O0`
+flag reverts to the same settings as if no `-O*` flags had been specified.
+
+Pass Stack's `--fast` flag to add `-O0` to the flags and options passed to GHC.
+The effect of `--fast` can be overriden with Stack's
+[`--ghc-options`](#-ghc-options-option) command line options.
+
+!!! note
+
+    With one exception, GHC's `-O` flag is always passed to GHC first (being
+    Cabal's default behaviour). The exception is if Cabal's
+    `--disable-optimization` flag or `--enable-optimization[=n]`, `-O[n]`
+    options are used during the configure step of the Cabal build process; see
+    Stack's [`configure-options`](yaml_configuration.md#configure-options) YAML
+    configuration option.
+
 ### `--ghc-options` option
 
+Augment and, if applicable, override any GHC command line options specified in
+Cabal files (including those created from `package.yaml` files) or in Stack's
+YAML configuration files.
+
 `stack build --ghc-options <ghc_options>` passes the specified command line
 options to GHC, depending on Stack's
 [`apply-ghc-options`](yaml_configuration.md#apply-ghc-options) YAML
 configuration option. This option can be specified multiple times.
 
 GHC's command line options are _order-dependent_ and evaluated from left to
-right. Later options can override earlier options. Stack applies the options
-specified at the command line last. Any existing GHC command line options of a
-package are applied after those specified at the command line.
+right. Later options can override the effect of earlier ones. Any GHC command
+line options for a package specified at Stack's command line are applied after
+those specified in Stack's YAML configuration files.
 
 ### `--[no-]library-profiling` flag
 
doc/build_overview.md view
@@ -59,7 +59,7 @@ 
 This file is parsed to provide the following config values:
 
-* `resolver` (required field)
+* `snapshot` (or, alternatively, `resolver`) (required field)
 * `compiler` (optional field)
 * `packages` (optional field, defaults to `["."]`)
 * `extra-deps` (optional field, defaults to `[]`)
@@ -72,10 +72,10 @@ 
 ## Wanted compiler, dependencies, and project packages
 
-* If the `--resolver` CLI is present, ignore the `resolver` and
+* If the `--resolver` CLI is present, ignore the `snapshot` (or `resolver`) and
   `compiler` config values
-* Load up the snapshot indicated by the `resolver` (either config
-  value or CLI arg). This will provide:
+* Load up the indicated snapshot (either config value or CLI arg). This will
+  provide:
     * A map from package name to package location, flags, GHC options,
       and if a package should be hidden. All package locations here
       are immutable.
doc/config_command.md view
@@ -42,7 +42,8 @@   install-ghc              Configure whether Stack should automatically install
                            GHC when necessary.
   package-index            Configure Stack's package index
-  resolver                 Change the resolver of the current project.
+  resolver                 Change the resolver key of the current project.
+  snapshot                 Change the snapshot of the current project.
   system-ghc               Configure whether Stack should use a system GHC
                            installation or not.
 ~~~
@@ -85,12 +86,31 @@ project-level configuration file (`stack.yaml`).
 
 A snapshot of `lts` or `nightly` will be translated into the most recent
-available. A snapshot of `lts-20` will be translated into the most recent
-available in the `lts-20` sequence.
+available. A snapshot of `lts-22` will be translated into the most recent
+available in the `lts-22` sequence.
 
 Known bug:
 
 * The command does not respect the presence of a `snapshot` key.
+
+## The `stack config set snapshot` command
+
+[:octicons-tag-24: 2.15.1](https://github.com/commercialhaskell/stack/releases/tag/v2.15.1)
+
+~~~text
+stack config set snapshot SNAPSHOT
+~~~
+
+`stack config set snapshot <snapshot>` sets the `snapshot` key in the
+project-level configuration file (`stack.yaml`).
+
+A snapshot of `lts` or `nightly` will be translated into the most recent
+available. A snapshot of `lts-22` will be translated into the most recent
+available in the `lts-22` sequence.
+
+Known bug:
+
+* The command does not respect the presence of a `resolver` key.
 
 ## The `stack config set system-ghc` command
 
doc/custom_snapshot.md view
@@ -6,19 +6,20 @@ 
 Snapshots provide a list of packages to use, along with flags, GHC options, and
 a few other settings. Snapshots may extend any other snapshot that can be
-specified in a `resolver` or `snapshot` key. The packages specified follow the
-same syntax for dependencies in Stack's project-level configuration files.
+specified in a [`snapshot`](yaml_configuration.md#snapshot) or
+[`resolver`](yaml_configuration.md#resolver) key. The packages specified follow
+the same syntax for dependencies in Stack's project-level configuration files.
 Unlike the `extra-deps` key, however, no support for local directories is
 available in snapshots to ensure reproducibility.
 
 !!! info
 
-    Stack uses the [Pantry](https://hackage.haskell.org/package/pantry) for
-    snapshot specification.
+    Stack uses the [Pantry](https://hackage.haskell.org/package/pantry) library
+    for snapshot specification.
 
 ~~~yaml
-resolver: lts-21.13 # Inherits GHC version and package set
-compiler: ghc-9.6.2 # Overwrites GHC version in the resolver, optional
+snapshot: lts-22.7 # Inherits GHC version and package set
+compiler: ghc-9.6.3 # Overwrites GHC version in the snapshot, optional
 
 # Additional packages, follows extra-deps syntax
 packages:
@@ -50,7 +51,7 @@ you can now use the snapshot like this:
 
 ~~~yaml
-resolver: snapshot.yaml
+snapshot: snapshot.yaml
 ~~~
 
 This is an example of a custom snapshot stored in the filesystem. They are
@@ -61,35 +62,35 @@ 
 ### Overriding the compiler
 
-The following snapshot specification will be identical to `lts-21.13`, but
-instead use `ghc-9.4.5` instead of `ghc-9.4.7`:
+The following snapshot specification will be identical to `lts-22.7`, but
+instead use `ghc-9.6.3` instead of `ghc-9.6.4`:
 
 ~~~yaml
-resolver: lts-21.13
-compiler: ghc-9.4.5
+snapshot: lts-22.7
+compiler: ghc-9.6.3
 ~~~
 
 ### Dropping packages
 
-The following snapshot specification will be identical to `lts-21.13`, but
+The following snapshot specification will be identical to `lts-22.7`, but
 without the `text` package in our snapshot. Removing this package will cause all
 the packages that depend on `text` to be unbuildable, but they will still be
 present in the snapshot.
 
 ~~~yaml
-resolver: lts-21.13
+snapshot: lts-22.7
 drop-packages:
 - text
 ~~~
 
 ### Hiding packages
 
-The following snapshot specification will be identical to `lts-21.13`, but the
+The following snapshot specification will be identical to `lts-22.7`, but the
 `text` package will be hidden when registering. This will affect, for example,
 the import parser in the script command.
 
 ~~~yaml
-resolver: lts-21.13
+snapshot: lts-22.7
 hidden:
 - text
 ~~~
@@ -99,11 +100,11 @@ In order to specify GHC options for a package, you use the same syntax as the
 [ghc-options](yaml_configuration.md#ghc-options) key for build configuration.
 
-The following snapshot specification will be identical to `lts-21.13`, but
+The following snapshot specification will be identical to `lts-22.7`, but
 provides `-O1` as a ghc-option for `text`:
 
 ~~~yaml
-resolver: lts-21.13
+snapshot: lts-22.7
 packages:
 - text-2.0.2
 ghc-options:
@@ -122,11 +123,11 @@ 
 In order to specify Cabal flags for a package, you use the same syntax as the
 [flags](yaml_configuration.md#flags) key for build configuration. The
-following snapshot specification will be identical to `lts-21.13`, but
+following snapshot specification will be identical to `lts-22.7`, but
 it enables the `developer` Cabal flag:
 
 ~~~yaml
-resolver: lts-21.13
+snapshot: lts-22.7
 packages:
 - text-2.0.2
 flags:
doc/dev_containers.md view
@@ -36,9 +36,9 @@     The PATH is
     `$HOME/.cabal/bin:$HOME/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`.
     Consequently, executables installed with Cabal (the tool) (at
-    `$HOME/.cabal/bin`) or Stack or Pip (at `$HOME/.local/bin`) take precedence
-    over the same executable installed at `/usr/local/sbin`, `/usr/local/bin`,
-    etc.
+    `$HOME/.cabal/bin` or `$HOME/.local/bin`) or Stack or Pip (at
+    `$HOME/.local/bin`) take precedence over the same executable installed at
+    `/usr/local/sbin`, `/usr/local/bin`, etc.
 
 [VS Code](https://code.visualstudio.com) is used as IDE, with the following
 extensions pre‑installed:
@@ -55,24 +55,24 @@ 
 ## Parent images
 
-Stack's Dev Containers are derived from Docker images that are used to build the
-*statically linked* Linux/x86_64 and Linux/AArch64 binary distributions of
+Stack's Dev Containers are derived from Docker images that are used to build
+the *statically linked* Linux/x86_64 and Linux/AArch64 binary distributions of
 Stack.
 
-These Docker images are multi-architecture (`linux/amd64`, `linux/arm64/v8`)
-*ghc‑musl* images. They are based on Alpine Linux (that is
+These Docker images are multi‑architecture (`linux/amd64`, `linux/arm64/v8`)
+<nobr>*GHC musl*</nobr> images. They are based on Alpine Linux (that is
 [musl libc](https://musl.libc.org) and [BusyBox](https://www.busybox.net)).
 
-The images contain *unofficial* binary distributions of GHC 9.4.7 and 9.6.2
-(that is, ones not released by the GHC developers). That is because:
+The images contain *unofficial* binary distributions of GHC (that is, ones not
+released by the GHC developers). That is because:
 
-1.  the official GHC 9.6.2 binary distributions for Alpine Linux/x86_64 have
-    known bugs; and
+1.  the official GHC binary distributions for Alpine Linux/x86_64 have known
+    bugs; and
 2.  there are no official binary distributions for Alpine Linux/AArch64.
 
-Stack's global configuration (`/etc/stack/config.yaml`) sets `system-ghc: true`
-and `install-ghc: false`. That ensures that only the GHC available in the Dev
-Containers is used.
+Stack's global configuration (`/etc/stack/config.yaml`) sets
+<nobr>`system-ghc: true`</nobr> and <nobr>`install-ghc: false`</nobr>. That
+ensures that only the GHC available in the Dev Containers is used.
 
 ## Usage
 
@@ -89,24 +89,6 @@ 
     For use with GitHub Codespaces, follow the instructions at
     [Creating a codespace for a repository](https://docs.github.com/en/codespaces/developing-in-codespaces/creating-a-codespace-for-a-repository#creating-a-codespace-for-a-repository).
-
-### Persistence
-
-Data in the following locations is persisted:
-
-1. The user's home directory (`/home/vscode` or, for the root user, `/root`).
-   Use with Docker/Podman in *rootless mode*.
-2. The Dev Container's workspace (`/workspaces`)
-
-This is accomplished via either a *volume* or *bind mount* (or *loop device*
-on Codespaces) and is preconfigured.
-
-!!! info
-
-    The VS Code Command Palette command
-    ['Codespaces: Full Rebuild Container'](https://docs.github.com/en/codespaces/developing-in-codespaces/rebuilding-the-container-in-a-codespace#rebuilding-a-container)
-    resets the home directory. This is never necessary unless you want exactly
-    that.
 
 ## Build Stack
 
doc/developing_on_windows.md view
@@ -3,10 +3,11 @@ # Developing on Windows #
 
 On Windows, Stack comes with an installation of [MSYS2](https://www.msys2.org/).
-MSYS2 will be used by Stack to provide a Unix-like shell and environment for
-Stack. This may be necessary for installing some Haskell packages, such as those
-which use `configure` scripts, or if your project needs some additional tools
-during the build phase.
+The MINGW64 (MINGW32 on 32-bit Windows) environment of MSYS2 will be used by
+Stack to provide a Unix-like shell and environment for Stack. This may be
+necessary for installing some Haskell packages, such as those which use
+`configure` scripts, or if your project needs some additional tools during the
+build phase.
 
 No matter which terminal software you choose (Windows Terminal, Console Windows
 Host, Command Prompt, PowerShell, Git bash or any other) you can use this
@@ -26,11 +27,18 @@ `stack exec -- pacman -Sh`.
 
 Command `stack path --bin-path` to see the PATH in the Stack environment. On
-Windows, it includes the `\mingw64\bin`, `\usr\bin` and `\usr\local\bin`
-directories of the Stack-supplied MSYS2. If your executable depends on files
-(for example, dynamic-link libraries) in those directories and you want ro run
-it outside of the Stack environment, you will need to ensure copies of those
-files are on the PATH.
+Windows, it includes the `\mingw64\bin` (`\mingw32\bin` on 32-bit Windows),
+`\usr\bin` and `\usr\local\bin` directories of the Stack-supplied MSYS2. If your
+executable depends on files (for example, dynamic-link libraries) in those
+directories and you want to run it outside of the Stack environment, you will
+need to ensure copies of those files are on the PATH.
+
+Command `stack path --extra-include-dirs` and `stack path --extra-library-dirs`
+to see the extra directories searched for C header files or system libraries
+files in the Stack environment. On Windows, it includes the `\mingw64\include`
+(`mingw32\include` on 32-bit Windows) (include) and the `\mingw64\lib` and
+`\mingw64\bin` directories (`mingw32\lib` and `mingw32\bin` on 32-bit Windows)
+(library) of the Stack-supplied MSYS2.
 
 ## Updating the Stack-supplied MSYS2 ##
 
doc/docker_integration.md view
@@ -105,7 +105,7 @@ 
 The first time you run a command with a new image, you will be prompted to run
 `stack docker pull` to pull the image first. This will pull a Docker
-image with a tag that matches your resolver. Only LTS resolvers are supported
+image with a tag that matches your snapshot. Only LTS snapshots are supported
 (we do not generate images for nightly snapshots).  Not every LTS version is
 guaranteed to have an image existing, and new LTS images tend to lag behind
 the LTS snapshot being published on stackage.org.  Be warned: these images are
doc/editor_integration.md view
@@ -7,12 +7,6 @@ For further information, see the [Stack and Visual Code](Stack_and_VS_Code.md)
 documentation.
 
-## The `intero` project
-
-For editor integration, Stack has a related project called
-[intero](https://github.com/commercialhaskell/intero). It is particularly well
-supported by Emacs, but some other editors have integration for it as well.
-
 ## Shell auto-completion
 
 Love tab-completion of commands? You're not alone. If you're on bash, just run
doc/exec_command.md view
@@ -26,9 +26,15 @@   `--cwd <directory>` to execute the executable in the specified directory.
 
 The option `--package <package>` has no effect for the `stack exec` command. For
-further information about its use, see the [`stack ghc` command](ghc_command.md) documentation or the [`stack runghc` command](runghc_command.md) documentation.
+further information about its use, see the [`stack ghc` command](ghc_command.md)
+documentation or the [`stack runghc` command](runghc_command.md) documentation.
 
-Pass the option `--rts-option <rts_flag>` to specify a GHC RTS flag or option.
+Pass the option `--rts-option <rts_flag(s)>` to specify a GHC RTS flag or option.
 The option can be specified multiple times. All specified GHC RTS flags and
 options are added to the arguments for the specified executable between
 arguments `+RTS` and `-RTS`.
+
+Specified GHC RTS flags and options are separated by spaces. Items can be
+unquoted (if they do not contain space or `"` characters) or quoted (`""`).
+Quoted items can include 'escaped' characters, escaped with an initial `\`
+character.
doc/faq.md view
@@ -9,11 +9,11 @@ ## What version of GHC is used when I run something like `stack ghci`?
 
 The version of GHC, as well as which packages can be installed, are specified by
-the _resolver_. This may be something like `lts-21.13`, which is from
+the _snapshot_. This may be something like `lts-22.7`, which is from
 [Stackage](https://www.stackage.org/). The [user's guide](GUIDE.md) discusses
-the resolver in more detail.
+the snapshot in more detail.
 
-The resolver is determined by finding the relevant project-level configuration
+The snapshot is determined by finding the relevant project-level configuration
 file (`stack.yaml`) for the directory you're running the command from. This
 essentially works by:
 
@@ -76,7 +76,7 @@ value in your `stack.yaml` file, e.g.:
 
 ~~~yaml
-resolver: lts-21.13
+snapshot: lts-22.7
 packages:
 - .
 extra-deps:
@@ -91,7 +91,7 @@ directory where your `stack.yaml` file lives, e.g.
 
 ~~~yaml
-resolver: lts-21.13
+snapshot: lts-22.7
 packages:
 - .
 extra-deps:
@@ -216,8 +216,9 @@ suitable GHC by default.
 
 Stack can only use a system GHC installation if its version is compatible with
-the configuration of the current project, particularly the
-[`resolver` or `snapshot`](yaml_configuration.md#resolver-or-snapshot) setting.
+the configuration of the current project, particularly the snapshot specified by
+the [`snapshot`](yaml_configuration.md#snapshot) or
+[`resolver`](yaml_configuration.md#resolver) key.
 
 GHC installation doesn't work for all operating systems, so in some cases you
 will need to use `system-ghc` and install GHC yourself.
@@ -246,8 +247,8 @@ 
 !!! note
 
-    This works when using LTS or nightly resolvers, not with GHC or custom
-    resolvers. You can manually install build tools by running, e.g.,
+    This works when using LTS or nightly snapshots, not with GHC or custom
+    snapshots. You can manually install build tools by running, e.g.,
     `stack build alex happy`.
 
 ## How does Stack choose which snapshot to use when creating a new configuration file?
@@ -371,7 +372,7 @@ prompt because they're not in the PATH environment variable. `stack exec` works
 because it's modifying PATH to include extra things.
 
-Those libraries are shipped with GHC (and, theoretically in some cases, MSYS).
+Those libraries are shipped with GHC (and, theoretically in some cases, MSYS2).
 The easiest way to find them is `stack exec which`. For example, command:
 
 ~~~text
@@ -625,7 +626,7 @@ ~~~
 
 **Note that we're fixing `ghc-8.2.2` in this case; repeat for other versions as necessary.**
-You should apply this fix for the version of GHC that matches your resolver.
+You should apply this fix for the version of GHC that matches your snapshot.
 
 Issue [#4009](https://github.com/commercialhaskell/stack/issues/4009) goes into
 further detail.
doc/global_flags.md view
@@ -172,17 +172,8 @@ 
 ## `--resolver` option
 
-Pass the option `--resolver <snapshot>` to specify the snapshot. For further
-information, see the
-[YAML configuration](yaml_configuration.md#resolver-or-snapshot) documentation.
-
-At the command line (only):
-
-*   `--resolver lts-<major_version>` specifies the latest Stackage LTS Haskell
-    snapshot with the specified major version;
-*   `--resolver lts` specifies, from those with the greatest major version, the
-    latest Stackage LTS Haskell snapshot; and
-*   `--resolver nightly` specifies the most recent Stackage Nightly snapshot.
+A synonym for the [`--snapshot` option](#snapshot-option) to specify the
+snapshot resolver.
 
 ## `--[no-]rsl-in-log` flag
 
@@ -222,6 +213,22 @@ Enables/disables the skipping of installing MSYS2. For further information, see
 the documentation for the corresponding non-project specific configuration
 [option](yaml_configuration.md#skip-msys).
+
+## `--snapshot` option
+
+[:octicons-tag-24: 2.15.1](https://github.com/commercialhaskell/stack/releases/tag/v2.15.1)
+
+Pass the option `--snapshot <snapshot>` to specify the snapshot. For further
+information, see the [YAML configuration](yaml_configuration.md#snapshot)
+documentation.
+
+At the command line (only):
+
+*   `--snapshot lts-<major_version>` specifies the latest Stackage LTS Haskell
+    snapshot with the specified major version;
+*   `--snapshot lts` specifies, from those with the greatest major version, the
+    latest Stackage LTS Haskell snapshot; and
+*   `--snapshot nightly` specifies the most recent Stackage Nightly snapshot.
 
 ## `--stack-colors` or `--stack-colours` options
 
doc/glossary.md view
@@ -36,9 +36,11 @@ |Make               |A [build automation tool](https://www.gnu.org/software/make/).|
 |MSYS2              |The [MSYS2](https://www.msys2.org/) software distribution and building platform for Windows.|
 |Nix                |A purely functional [package manager](https://nixos.org/), available for Linux and macOS.|
+|package            |A Haskell package is an organised collection of Haskell code and related files. It is described by a Cabal file or a `package.yaml` file, which is itself part of the package.|
 |`package.yaml`     |A file that describes a package in the Hpack format.      |
 |Pantry             |A library for content-addressable Haskell package management, provided by the [`pantry` package](https://hackage.haskell.org/package/pantry). A dependency of Stack.|
 |PATH               |The `PATH` environment variable, specifying a list of directories searched for executable files.|
+|project            |A Stack project is a local directory that contains a project-level configuration file (`stack.yaml`). A project may relate to more than one local package.|
 |PVP                |The Haskell [Package Versioning Policy](https://pvp.haskell.org/), which tells developers of libraries how to set their version numbers.|
 |REPL               |An interactive (run-eval-print loop) programming environment.|
 |resolver           |A synonym for snapshot.                                   |
doc/install_and_upgrade.md view
@@ -4,7 +4,10 @@ 
 ## Install Stack
 
-Stack can be installed on most Linux distributions, macOS and Windows.
+Stack can be installed on most Linux distributions, macOS and Windows. It will
+require at least about 5 GB of disk space, of which about 3 GB is for a single
+version of GHC and about 2 GB is for Stack's local copy of the Hackage package
+index.
 
 Stack is open to supporting more operating systems. To request support for an
 operating system, please submit an
@@ -130,8 +133,8 @@ 
     === "Arch Linux"
 
-        The Arch community package repository provides an official
-        [package](https://www.archlinux.org/packages/community/x86_64/stack/).
+        The Arch extra package repository provides an official
+        [package](https://www.archlinux.org/packages/extra/x86_64/stack/).
         You can install it with the command:
 
         ~~~text
@@ -428,10 +431,10 @@ 
     !!! warning "Long user PATH environment variable"
 
-        The Windows installer for Stack 2.9.1, 2.9.3 and 2.11.1 (current) (only)
-        will replace the user `PATH` environment variable (rather than append to
-        it) if a 1024 character limit is exceeded. If the content of your
-        existing user `PATH` is long, preserve it before running the installer.
+        The Windows installer for Stack 2.9.1, 2.9.3 and 2.11.1 (only) will
+        replace the user `PATH` environment variable (rather than append to it)
+        if a 1024 character limit is exceeded. If the content of your existing
+        user `PATH` is long, preserve it before running the installer.
 
     !!! note "Anti-virus software"
 
doc/list_command.md view
@@ -29,11 +29,11 @@ 
 ~~~text
 stack list base unix Win32 acme-missiles pantry
-base-4.18.0.0
-unix-2.8.1.1
+base-4.19.0.0
+unix-2.8.5.0
 Win32-2.13.4.0
 acme-missiles-0.3
-pantry-0.9.2
+pantry-0.9.3.1
 
 stack list paltry
 Could not find package paltry, updating
@@ -44,20 +44,20 @@          pantry, pretty, pasty, xattr, alloy, para, pappy, alure, polar and
          factory.
 
-stack --resolver lts-21.13 list base unix Win32 acme-missiles pantry
+stack --resolver lts-22.7 list base unix Win32 acme-missiles pantry
 Error: [S-4926]
        * Package does not appear in snapshot: base.
        * Package does not appear in snapshot: unix.
        * Package does not appear in snapshot: Win32.
        * Package does not appear in snapshot: acme-missiles.
 
-stack --resolver lts-21.13 list pantry
-pantry-0.8.3
+stack --resolver lts-22.7 list pantry
+pantry-0.9.3.1
 
-stack --resolver lts-21.13 list
+stack --resolver lts-22.7 list
 AC-Angle-1.0
 ALUT-2.4.0.3
 ...
-zot-0.0.3
 zstd-0.1.3.0
+zxcvbn-hs-0.3.6
 ~~~
doc/lock_files.md view
@@ -41,13 +41,14 @@ Relevant to this discussion, Stack's project-level configuration file
 (`stack.yaml`) specifies:
 
-* the parent snapshot (`resolver` or `snapshot`)
+* the parent snapshot (the [`snapshot`](yaml_configuration.md#snapshot) or
+  [`resolver`](yaml_configuration.md#resolver) key)
 * extra-deps
 
 Some of this information can be incomplete. Consider this `stack.yaml` file:
 
 ~~~yaml
-resolver: lts-19.22
+snapshot: lts-19.22
 packages:
 - .
 extra-deps:
@@ -71,7 +72,7 @@ happening. Instead, the complete version of that key is:
 
 ~~~yaml
-resolver:
+snapshot:
 - url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/22.yaml
   size: 619399
   sha256: 5098594e71bdefe0c13e9e6236f12e3414ef91a2b89b029fd30e8fc8087f3a07
doc/ls_command.md view
@@ -33,10 +33,10 @@ or
 
 ~~~text
-stack ls dependencies [--separator SEP] [--[no-]license] [--[no-]external]
-                      [--[no-]include-base] [--depth DEPTH] [--prune PACKAGES]
-                      [TARGET] [--flag PACKAGE:[-]FLAG] [--test] [--bench]
-                      [--global-hints]
+stack ls dependencies [--separator SEP] [--[no-]license] [--filter ITEM]
+                      [--[no-]external] [--[no-]include-base] [--depth DEPTH]
+                      [--prune PACKAGES] [TARGET] [--flag PACKAGE:[-]FLAG]
+                      [--test] [--bench] [--global-hints]
 ~~~
 
 `stack ls dependencies` lists all of the packages and versions used for a
@@ -46,24 +46,40 @@ 
 Subcommands specify the format of the output, as follows:
 
-* `cabal` lists the packages in the format of exact Cabal constraints.
-  For example (extract):
+*   `cabal` lists the packages in the format of exact Cabal constraints.
 
     ~~~text
+    stack ls dependencies cabal [--[no-]external] [--[no-]include-base]
+                                [--depth DEPTH] [--prune PACKAGES] [TARGET]
+                                [--flag PACKAGE:[-]FLAG] [--test] [--bench]
+                                [--global-hints]
+    ~~~
+
+    For example (extract):
+
+    ~~~text
     constraints:
     , Cabal ==3.6.3.0
     , Cabal-syntax ==3.6.0.0
     , Glob ==0.10.2
     ~~~
 
-* `json` lists dependencies in JSON format (an array of objects). For example
-  (extract):
+*   `json` lists dependencies in JSON format (an array of objects).
 
     ~~~text
+    stack ls dependencies json [--[no-]external] [--[no-]include-base]
+                               [--depth DEPTH] [--prune PACKAGES] [TARGET]
+                               [--flag PACKAGE:[-]FLAG] [--test] [--bench]
+                               [--global-hints]
+    ~~~
+
+    For example (extract):
+
+    ~~~text
     [{"dependencies":["base","bytestring"],"license":"BSD3","location":{"type":"hackage","url":"https://hackage.haskell.org/package/zlib-0.6.3.0"},"name":"zlib","version":"0.6.3.0"},
     ~~~
 
-  Each object has the following keys:
+    Each object has the following keys:
 
     ~~~json
     name: zlib
@@ -77,18 +93,35 @@     - bytestring
     ~~~
 
-* `text` (the default) lists the packages, each on a separate line. For example
-  (extract):
+*   `text` (the default) lists the packages, each on a separate line.
 
     ~~~text
+    stack ls dependencies text [--separator SEP] [--[no-]license] [--filter ITEM]
+                               [--[no-]external] [--[no-]include-base]
+                               [--depth DEPTH] [--prune PACKAGES] [TARGET]
+                               [--flag PACKAGE:[-]FLAG] [--test] [--bench]
+                               [--global-hints]
+    ~~~
+
+    For example (extract):
+
+    ~~~text
     Cabal 3.6.3.0
     Cabal-syntax 3.6.0.0
     Glob 0.10.2
     ~~~
 
-* `tree` lists dependencies in the format of a tree. For example (extract):
+*   `tree` lists dependencies in the format of a tree.
 
     ~~~text
+    stack ls dependencies tree [--separator SEP] [--[no-]license] [--[no-]external]
+                               [--[no-]include-base] [--depth DEPTH]
+                               [--prune PACKAGES] [TARGET] [--flag PACKAGE:[-]FLAG] [--test] [--bench] [--global-hints]
+    ~~~
+
+    For example (extract):
+
+    ~~~text
     Packages
     └─┬ stack 2.10.0
       ├─┬ Cabal 3.6.3.0
@@ -100,13 +133,23 @@       │ │ │ ├─┬ ghc-prim 0.8.0
     ~~~
 
-The `--separator` option specifies the separator between the package name and
-its version. The default is a space character.
+The `--separator` option, with the `text` or `tree` subcommand, specifies the
+separator between the package name and its version. The default is a space
+character.
 
 Set the `--license` flag, after the `text` or `tree` subcommand, to replace each
 package's version with its licence. (Consistent with the Cabal package
 description format specification, only the American English spelling (license)
 is accepted.)
+
+The `--filter` option, with the `text` subcommand, specifies an item to be
+filtered out from the results, if present. An item can be `$locals` (for all
+local packages) or a package name. It can be specified multiple times.
+
+!!! note
+
+    The special value `$locals` will need to be enclosed with single quotes to
+    distinguish it from a shell variable.
 
 Set the `--no-external` flag to exclude external dependencies.
 
doc/new_command.md view
@@ -3,21 +3,46 @@ # The `stack new` command
 
 ~~~text
-stack new PACKAGE_NAME [--bare] [TEMPLATE_NAME] [-p|--param KEY:VALUE] [DIR(S)]
-          [--omit-packages] [--force] [--ignore-subdirs]
+stack new PACKAGE_NAME [--bare] [--[no-]init] [TEMPLATE_NAME]
+          [-p|--param KEY:VALUE] [DIR(S)] [--omit-packages] [--force]
+          [--ignore-subdirs]
 ~~~
 
-`stack new` creates a new Stack project for a package using a project template.
+`stack new` creates a new project using a project template.
 
-The project is created in a new directory named after the package, unless the
-`--bare` flag is passed, in which case the project is created in the current
-directory.
+By default:
 
+* the project is created in a new directory named after the package. Pass the
+  `--bare` flag to create the project in the current directory; and
+
+* the project is initialised for use with Stack. Pass the `--no-init` flag to
+  skip such initialisation.
+
+A package name acceptable to Cabal comprises an alphanumeric 'word'; or two or
+more such words, with the words separated by a hyphen/minus character (`-`). A
+word cannot be comprised only of the characters `0` to `9`.
+
+An alphanumeric character is one in one of the Unicode Letter categories
+(Lu (uppercase), Ll (lowercase), Lt (titlecase), Lm (modifier), or Lo (other))
+or Number categories (Nd (decimal), Nl (letter), or No (other)).
+
+!!! note
+
+    In the case of Hackage and acceptable package names, an alphanumeric
+    character is limited to one of `A` to `Z`, `a` to `z`, and `0` to `9`.
+
+!!! note
+
+    The name of a project is not constrained to be an acceptable package name. A
+    single-package project can be renamed to differ from the name of its
+    package.
+
 The `--param <key>:<value>` option specifies a key-value pair to populate a key
 in a template. The option can be specified multiple times.
 
 The arguments specifying directories and the `--ignore-subdirs`, `--force` and
 `--omit-packages` flags are as for the [`stack init` command](init_command.md).
+These arguments are ignored if the `--no-init` flag is passed.
 
 ## Project templates
 
@@ -45,49 +70,60 @@ ## Examples
 
 Create a project for package `my-project` in new directory `my-project` with the
-default project template file:
+default project template file and initialise it for use with Stack:
 
 ~~~text
 stack new my-project
 ~~~
 
 Create a project for package `my-package` in the current directory with the
-default project template file:
+default project template file and initialise it for use with Stack:
 
 ~~~text
 stack new my-package --bare
 ~~~
 
-Create a project with the `rio` project template at the default repository:
+Create a project with the `rio` project template at the default repository and
+initialise it for use with Stack:
 
 ~~~text
 stack new my-project rio
 ~~~
 
 Create a project with the `mysql` project template provided by the
-`yesodweb/stack-templates` repository on GitHub:
+`yesodweb/stack-templates` repository on GitHub and initialise it for use with
+Stack:
 
 ~~~text
 stack new my-project yesodweb/mysql
 ~~~
 
 Create a project with the `my-template` project template provided by the
-`username/stack-templates` repository on Bitbucket:
+`username/stack-templates` repository on Bitbucket and initialise it for use
+with Stack:
 
 ~~~text
 stack new my-project bitbucket:username/my-template
 ~~~
 
 Create a project with the `my-template.hsfiles` project template file at
-`https://example.com`:
+`https://example.com` and initialise it for use with Stack:
 
 ~~~text
 stack new my-project https://example.com/my-template
 ~~~
 
 Create a project with the local project template file
-`<path_to_template>/my-template.hsfiles`:
+`<path_to_template>/my-template.hsfiles` and initialise it for use with Stack:
 
 ~~~text
 stack new my-project <path_to_template_file>/my-template
+~~~
+
+Create a project with the `simple` project template file at the default
+repository (which does not use Hpack and a `package.yaml` file) and do not
+initialise it for use with Stack (`stack init` could be used subsequently):
+
+~~~text
+stack new my-project --no-init simple
 ~~~
doc/nix_integration.md view
@@ -70,17 +70,24 @@ 
 ### Enable Nix integration
 
-To enable Nix integration, add the following section to your Stack YAML
-configuration file (`stack.yaml` or `config.yaml`):
+On NixOS, Nix integration is enabled by default; on other operating systems it
+is disabled. To enable Nix integration, add the following section to your Stack
+YAML configuration file (`stack.yaml` or `config.yaml`):
 
 ~~~yaml
 nix:
-  enable: true  # false by default
+  enable: true  # false by default, except on NixOS
 ~~~
 
 The equivalent command line flag (which will prevail) is `--[no-]nix`. Passing
 any `--nix-*` option on the command line will imply the `--nix` option.
 
+If Nix integration is not enabled, Stack will notify the user if a `nix`
+executable is on the PATH. If that notification is unwanted, it can be muted by
+setting Stack's configuration option
+[`notify-if-nix-on-path`](yaml_configuration.md#notify-if-nix-on-path) to
+`false`.
+
 With Nix integration enabled, `stack build` and `stack exec` will automatically
 launch themselves in a local build environment (using `nix-shell` behind the
 scenes). It is not necessary to run `stack setup`, unless you want to cache a
@@ -121,7 +128,7 @@ 
         hPkgs =
           pkgs.haskell.packages."ghc8107"; # need to match Stackage LTS version
-                                           # from stack.yaml resolver
+                                           # from stack.yaml snapshot resolver
 
         myDevTools = [
           hPkgs.ghc # GHC compiler in the desired version (will be available on PATH)
@@ -179,8 +186,8 @@ Nix integration will instruct Stack to build inside a local build environment.
 That environment will also download and use a
 [GHC Nix package](https://search.nixos.org/packages?query=haskell.compiler.ghc)
-matching the required version of the configured
-[Stack resolver](yaml_configuration.md#resolver-or-snapshot).
+matching the required version of the configured Stack
+[snapshot](yaml_configuration.md#snapshot).
 
 Enabling Nix integration means that packages will always be built using the
 local GHC from Nix inside your shell, rather than your globally installed system
@@ -366,34 +373,32 @@ The Tweag example [repository][tweag-example] shows how you can pin a package
 set.
 
-## Configuration options
+## Non-project specific configuration
 
-Below is a summary of the Stack YAML configuration file settings, identifying
-default values:
+Below is a summary of the non-project specific configuration options and their
+default values. The options can be set in Stack's project-level configuration
+file (`stack.yaml`) or its global configuration file (`config.yaml`).
 
 ~~~yaml
 nix:
 
-  # false by default. Must be present and set to `true` to enable Nix, except on
-  # NixOS where it is enabled by default (see #3938).  You can set it in
-  # your `$HOME/.stack/config.yaml` to enable Nix for all your projects without
-  # having to repeat it
+  # false by default, except on NixOS. Is Nix integration enabled?
   enable: true
 
-  # true by default. Tells Nix whether to run in a pure shell or not.
+  # true by default. Should Nix run in a pure shell?
   pure: true
 
-  # Empty by default. The list of packages you want to be
-  # available in the nix-shell at build time (with `stack
-  # build`) and run time (with `stack exec`).
+  # Empty by default. The list of packages you want to be available in the
+  # nix-shell at build time (with `stack build`) and run time (with
+  # `stack exec`).
   packages: []
 
   # Unset by default. You cannot set this option if `packages:`
   # is already present and not empty.
   shell-file: shell.nix
 
-  # A list of strings, empty by default. Additional options that
-  # will be passed verbatim to the `nix-shell` command.
+  # A list of strings, empty by default. Additional options that will be passed
+  # verbatim to the `nix-shell` command.
   nix-shell-options: []
 
   # A list of strings, empty by default, such as
@@ -405,6 +410,7 @@   # collection roots. This way, calling nix-collect-garbage will not remove
   # those packages from the Nix store, saving you some time when running
   # stack build again with Nix support activated.
+  #
   # This creates a `nix-gc-symlinks` directory in the project `.stack-work`.
   # To revert that, just delete this `nix-gc-symlinks` directory.
   add-gc-roots: false
doc/pantry.md view
@@ -6,7 +6,9 @@ 
 This document describes:
 
-* the specification of a snapshot location (in the `resolver` key)
+* the specification of a snapshot location (in the
+  [`snapshot`](yaml_configuration.md#snapshot) or
+  [`resolver`](yaml_configuration.md#resolver) key)
 * the specification of a package location (in the `extra-deps` key and in a
   snapshot)
 
@@ -25,20 +27,20 @@     for example:
 
     ~~~yaml
-    resolver: ghc-8.6.5`
+    snapshot: ghc-8.6.5`
     ~~~
 
 2.  Via a URL pointing to a snapshot configuration file, for example:
 
     ~~~yaml
-    resolver: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2018/8/21.yaml`
+    snapshot: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2018/8/21.yaml`
     ~~~
 
 3.  Via a local file path pointing to a snapshot configuration file, for
     example:
 
     ~~~yaml
-    resolver: my-local-snapshot.yaml
+    snapshot: my-local-snapshot.yaml
     ~~~
 
 4.  Via a _convenience synonym_, which provides a short form for some common
@@ -74,7 +76,7 @@ together with a cryptographic hash of its content. For example:
 
 ~~~yaml
-resolver:
+snapshot:
   url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/12/0.yaml
   size: 499143
   sha256: 781ea577595dff08b9c8794761ba1321020e3e1ec3297fb833fe951cce1bee11
@@ -165,11 +167,11 @@ ~~~yaml
 extra-deps:
 - git: git@github.com:commercialhaskell/stack.git
-  commit: 6a86ee32e5b869a877151f74064572225e1a0398
+  commit: '6a86ee32e5b869a877151f74064572225e1a0398'
 - git: git@github.com:snoyberg/http-client.git
-  commit: "a5f4f3"
+  commit: 'a5f4f3'
 - hg: https://example.com/hg/repo
-  commit: da39a3ee5e6b4b0d3255bfef95601890afd80709
+  commit: 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
 ~~~
 
 !!! note
@@ -181,6 +183,12 @@     that your build will not be deterministic, because when someone else tries
     to build the project they can get a different checkout of the package.
 
+!!! note
+
+    The `commit:` key expects a YAML string. A commit hash, or partial hash,
+    comprised only of digits represents a YAML number, unless it is enclosed in
+    quotation marks.
+
 !!! warning
 
     For the contents of a Git repository, Stack cannot handle filepaths or
@@ -203,7 +211,7 @@ ~~~yaml
 extra-deps:
 - git: git@github.com:yesodweb/wai
-  commit: 2f8a8e1b771829f4a8a77c0111352ce45a14c30f
+  commit: '2f8a8e1b771829f4a8a77c0111352ce45a14c30f'
   subdirs:
   - auto-update
   - wai
@@ -242,6 +250,12 @@ 
 !!! note
 
+    An example of a remote archive file is a Hackage package candidate, usually
+    located at (for example)
+    https://hackage.haskell.org/package/my-package-1.0.0/candidate/my-package-1.0.0.tar.gz.
+
+!!! warning
+
     Stack assumes that these archive files never change after downloading to
     avoid needing to make an HTTP request on each build.
 
@@ -272,7 +286,7 @@ ~~~yaml
 extra-deps:
 - github: snoyberg/http-client
-  commit: a5f4f30f01366738f913968163d856366d7e0342
+  commit: 'a5f4f30f01366738f913968163d856366d7e0342'
 ~~~
 
 !!! note
doc/path_command.md view
@@ -23,7 +23,7 @@ |--compiler-exe         |The GHC executable.                                   |
 |--compiler-tools-bin   |The directory containing binaries specific to a particular compiler.|
 |--config-location      |Stack's project-level YAML configuration file (`stack.yaml`).|
-|--dist-dir             |The dist working directory, relative to the package directory.|
+|--dist-dir             |The dist work directory, relative to the package directory.|
 |--extra-include-dirs   |Extra include directories.                            |
 |--extra-library-dirs   |Extra library directories.                            |
 |--ghc-package-path     |The `GHC_PACKAGE_PATH` environment variable.          |
doc/script_command.md view
@@ -18,7 +18,7 @@ the command line (with the `--resolver` option). For example:
 
 ~~~text
-stack script --resolver lts-21.13 MyScript.hs
+stack script --resolver lts-22.7 MyScript.hs
 ~~~
 
 The `stack script` command behaves as if the `--install-ghc` flag had been
@@ -100,7 +100,7 @@ can be compiled and run, with arguments, with:
 
 ~~~text
-stack --resolver lts-21.13 script --package acme-missiles --compile MyScript.hs -- "Don't panic!" "Duck and cover!"
+stack --resolver lts-22.7 script --package acme-missiles --compile MyScript.hs -- "Don't panic!" "Duck and cover!"
 ~~~
 
 All the compilation outputs (like `Main.hi`, `Main.o`, and the executable
doc/scripts.md view
@@ -19,7 +19,7 @@ 
 ~~~haskell
 #!/usr/bin/env stack
--- stack script --resolver lts-21.13 --package turtle
+-- stack script --resolver lts-22.7 --package turtle
 {-# LANGUAGE OverloadedStrings #-}
 import Turtle (echo)
 main = echo "Hello World!"
@@ -78,10 +78,10 @@ 
 The second line of the source code is the Stack interpreter options comment. In
 this example, it specifies the `stack script` command with the options of a
-LTS Haskell 21.13 snapshot (`--resolver lts-21.13`) and ensuring the
+LTS Haskell 22.7 snapshot (`--resolver lts-22.7`) and ensuring the
 [`turtle` package](https://hackage.haskell.org/package/turtle) is available
 (`--package turtle`). The version of the package will be that in the specified
-snapshot (`lts-21.13` provides `turtle-1.6.1`).
+snapshot (`lts-22.7` provides `turtle-1.6.2`).
 
 ## Arguments and interpreter options and arguments
 
@@ -116,7 +116,7 @@ ~~~haskell
 #!/usr/bin/env stack
 {- stack script
-   --resolver lts-21.13
+   --resolver lts-22.7
    --
    +RTS -s -RTS
 -}
@@ -132,7 +132,7 @@ is equivalent to the following command at the command line:
 
 ~~~text
-stack script --resolver lts-21.13 -- MyScript.hs arg1 arg2 +RTS -s -RTS
+stack script --resolver lts-22.7 -- MyScript.hs arg1 arg2 +RTS -s -RTS
 ~~~
 
 where `+RTS -s -RTS` are some of GHC's
@@ -161,7 +161,7 @@ ~~~haskell
 #!/usr/bin/env stack
 {- stack script
-   --resolver lts-21.13
+   --resolver lts-22.7
    --package turtle
    --package "stm async"
    --package http-client,http-conduit
@@ -191,7 +191,7 @@ 
 ~~~haskell
 {- stack script
-   --resolver lts-21.13
+   --resolver lts-22.7
    --package acme-missiles
 -}
 import Acme.Missiles (launchMissiles)
@@ -202,7 +202,7 @@ 
 The command `stack --script-no-run-compile Script.hs` then behaves as if the
 command
-`stack script --resolver lts-21.13 --package acme-missiles --no-run --compile -- Script.hs`
+`stack script --resolver lts-22.7 --package acme-missiles --no-run --compile -- Script.hs`
 had been given. `Script.hs` is compiled (without optimisation) and the resulting
 executable is not run: no missiles are launched in the process!
 
@@ -239,7 +239,7 @@ {- stack
   runghc
   --install-ghc
-  --resolver lts-21.13
+  --resolver lts-22.7
   --package base
   --package turtle
   --
@@ -262,7 +262,7 @@ {- stack
    exec ghci
    --install-ghc
-   --resolver lts-21.13
+   --resolver lts-22.7
    --package turtle
 -}
 ~~~
doc/setup_command.md view
@@ -43,7 +43,7 @@     required on Linux, Stack will refer to the presence or absence of certain
     libraries or the versions of those libraries.
 
-    For example, Stack 2.13.1 considers:
+    For example, Stack 2.15.1 considers:
 
     *   If `libc.musl-x86_64.so.1` is present. This file is provided by the
         [musl libc](https://musl.libc.org/).
@@ -63,7 +63,7 @@         provided by different versions of a shared low-level terminfo library
         for terminal handling.
 
-    Stack 2.13.1 uses `ghc-build`:
+    Stack 2.15.1 uses `ghc-build`:
 
     * `musl` to indicate `libc.musl-x86_64.so.1` is present and Stack should use
        the GHC binary distribution for Alpine Linux.
doc/stack_root.md view
@@ -1,13 +1,21 @@ <div class="hidden-warning"><a href="https://docs.haskellstack.org/"><img src="https://cdn.jsdelivr.net/gh/commercialhaskell/stack/doc/img/hidden-warning.svg"></a></div>
 
-# Stack root location
+# Stack root
 
-The Stack root is a directory where Stack stores important files. The location
-and contents of the directory depend on the operating system, whether
-Stack is configured to use the XDG Base Directory Specification, and/or
-whether an alternative location to Stack's default 'programs' directory has
-been specified.
+The Stack root is a directory where Stack stores important files.
 
+On Unix-like operating systems and Windows, Stack can be configured to follow
+the XDG Base Directory Specification if the environment variable `STACK_XDG` is
+set to any non-empty value. However, Stack will ignore that configuration if the
+Stack root location has been set on the command line or the `STACK_ROOT`
+environment variable exists.
+
+## Location
+
+The location of the Stack root depends on the operating system, whether Stack is
+configured to use the XDG Base Directory Specification, and/or whether an
+alternative location to Stack's default 'programs' directory has been specified.
+
 The location of the Stack root can be configured by setting the
 [`STACK_ROOT`](environment_variables.md#stack_root) environment variable or
 using Stack's [`--stack-root`](global_flags.md#stack-root-option) option on the
@@ -25,13 +33,6 @@ 
 === "Windows"
 
-    The Stack root contains snapshot packages; Stack's global
-    [YAML configuration](yaml_configuration.md#yaml-configuration) file
-    (`config.yaml`); and Stack's
-    [`global-projects`](yaml_configuration.md#yaml-configuration) directory. The
-    default location of tools such as GHC and MSYS2 is outside of the Stack
-    root.
-
     The default Stack root is `%APPDIR%\stack`.
 
     If the `LOCALAPPDATA` environment variable exists, the default location of
@@ -60,17 +61,6 @@ 
 === "XDG Base Directory Specification"
 
-    On Unix-like operating systems and Windows, Stack can be configured to
-    follow the XDG Base Directory Specification if the environment variable
-    `STACK_XDG` is set to any non-empty value. However, Stack will ignore that
-    configuration if the Stack root location has been set on the command line or
-    the `STACK_ROOT` environment variable exists.
-
-    If Stack is following the XDG Base Directory Specification, the Stack root
-    contains what it would otherwise contain for the operating system, but
-    Stack's global YAML configuration file (`config.yaml`) may be located
-    elsewhere.
-
     The Stack root is `<XDG_DATA_HOME>/stack`. If the `XDG_DATA_HOME`
     environment variable does not exist, the default is `~/.local/share/stack`
     on Unix-like operating systems and `%APPDIR%\stack` on Windows.
@@ -117,3 +107,129 @@ ~~~text
 stack path --programs
 ~~~
+
+## Contents
+
+The contents of the Stack root depend on the operating system, whether Stack is
+configured to use the XDG Base Directory Specification, and/or whether an
+alternative location to Stack's default 'programs' directory has been specified.
+
+=== "Unix-like"
+
+    The Stack root contains snapshot packages; (by default) tools such as GHC,
+    in a `programs` directory; Stack's global
+    [YAML configuration](yaml_configuration.md#yaml-configuration) file
+    (`config.yaml`); and Stack's
+    [`global-projects`](yaml_configuration.md#yaml-configuration) directory.
+
+=== "Windows"
+
+    The Stack root contains snapshot packages; Stack's global
+    [YAML configuration](yaml_configuration.md#yaml-configuration) file
+    (`config.yaml`); and Stack's
+    [`global-projects`](yaml_configuration.md#yaml-configuration) directory. The
+    default location of tools such as GHC and MSYS2 is outside of the Stack
+    root.
+
+=== "XDG Base Directory Specification"
+
+    If Stack is following the XDG Base Directory Specification, the Stack root
+    contains what it would otherwise contain for the operating system, but
+    Stack's global YAML configuration file (`config.yaml`) may be located
+    elsewhere.
+
+### `config.yaml`
+
+This is Stack's global configuration file. For further information, see the
+documentation for non-project specific
+[configuration](yaml_configuration.md#non-project-specific-configuration).
+
+If the file is deleted, and Stack needs to consult it, Stack will create a file
+with default contents.
+
+### `stack.sqlite3`
+
+This is a 'user' database that Stack uses to cache certain information. The
+associated lock file is `stack.sqlite3.pantry-write-lock`.
+
+### `global-project` directory
+
+This contains:
+
+* an explanation of the directory (`README.txt`);
+* the project-level configuration file (`stack.yaml`) for the global project
+  and its associated lock file (`stack.yaml.lock`); and
+* if created, Stack's working directory (`.stack-work`) for the global project.
+
+If the project-level configuration file is deleted, and Stack needs to consult
+it, Stack will recreate the contents of the directory.
+
+### `pantry\hackage` directory
+
+This contains the package index. If the contents of the directory are deleted,
+and Stack needs to consult the package index, Stack will seek to download the
+latest package index.
+
+### `pantry` directory
+
+This contains:
+
+* the Pantry database used by Stack (`pantry.sqlite3`) and its associated lock
+  file (`pantry.sqlite2.pantry-write-lock`). If the database is deleted, and
+  Stack needs to consult it, Stack will seek to create and initialise it. The
+  database is initialised with information from the package index; and
+* a database of package versions that come with each version of GHC
+  (`global-hints-cache.yaml`).
+
+### `programs` directory
+
+This contains a directory for the platform. That directory contains for each
+installed Stack-supplied tool:
+
+* the archive file for the tool. This can be deleted;
+* a file indicating the tool is installed (`<tool_name>.installed`); and
+* a directory for the tool.
+
+To remove a Stack-supplied tool, delete all of the above. If Stack needs a
+Stack-supplied tool and it is unavailable, Stack will seek to obtain it.
+
+### `setup-exe-cache` directory
+
+This contains a directory for the platform. That directory contains, for each
+version of GHC (an associated version of Cabal (the library)) that Stack has
+used, an executable that Stack uses to access Cabal (the library).
+
+If the contents of the directory are deleted, and Stack needs the executable,
+Stack will seek to rebuild it.
+
+### `setup-exe-src` directory
+
+See the documentation for the
+[`setup-exe-cache` directory](#setup-exe-cache-directorysetup-exe-cache). This
+contains the two source files (`setup-<hash>.hs` and `setup-shim-<hash>.hs`)
+that Stack uses to build the executable.
+
+If the contents of the directory are deleted, and Stack needs the executable,
+Stack will recreate them.
+
+### `snapshots` directory
+
+This contains a directory for each snapshot that Stack creates when building
+immutable dependencies of projects.
+
+If the contents of the directory are deleted, and the snapshot is not available
+to Stack when it builds, Stack will recreate the snapshot.
+
+### `templates` directory
+
+This contains a `.hsfile` for each project template that Stack has used. For
+further information, see the [`stack templates`](templates_command.md) command
+documentation.
+
+If the contents of the directory are deleted, an Stack needs a project template,
+Stack will seek to download the template.
+
+### `upload` directory
+
+This may contain saved credentials for uploading packages to Hackage
+(`credentials.json`).
doc/stack_yaml_vs_cabal_package_file.md view
@@ -43,7 +43,7 @@ 
 Stack defines a new concept called a _project_. A project has:
 
-* A _resolver_, which tells it about a snapshot (more on this later)
+* A snapshot _resolver_ (more on this later)
 * Extra dependencies on top of the snapshot
 * Optionally, one or more local Cabal packages
 * Flag and GHC options configurations
@@ -55,7 +55,7 @@ in the Cabal file. To explain, let's take a quick detour to talk about snapshots
 and how Stack resolves dependencies.
 
-## Resolvers and snapshots
+## Snapshots and resolvers
 
 Stack follows a rule that says, for any projects, there is precisely one version
 of each package available. Obviously, for many packages there are _many_
@@ -64,17 +64,19 @@ 
 The most common means by which this set of packages is defined is via a
 snapshot provided by Stackage. For example, if you go to the page
-<https://www.stackage.org/lts-21.13>, you will see a list of 3,010 packages at
-specific version numbers. When you then specify `resolver: lts-21.13`, you're
-telling Stack to use those package versions in resolving dependencies down to
-specific versions of packages.
+<https://www.stackage.org/lts-22.7>, you will see a list of 3,341 packages at
+specific version numbers. When you then specify `snapshot: lts-22.7` or,
+alternatively, `resolver: lts-22.7`, you're telling Stack to use those package
+versions in resolving dependencies down to specific versions of packages.
 
 Sometimes a snapshot doesn't have all of the packages that you want. Or you want
 a different version of a package. Or you want to work on a local modification of
 a package. In all of those cases, you can add more configuration data to your
-`stack.yaml` file to override the values it received from your `resolver`
-setting. At the end of the day, each of your projects will end up with some way
-of resolving a package name into a specific version of that package.
+`stack.yaml` file to override the values it received from your
+[`snapshot`](yaml_configuration.md#snapshot) or
+[`resolver`](yaml_configuration.md#resolver) setting. At the end of the day,
+each of your projects will end up with some way of resolving a package name into
+a specific version of that package.
 
 ## Why specify dependencies twice?
 
doc/unpack_command.md view
@@ -3,17 +3,49 @@ # The `stack unpack` command
 
 ~~~text
-stack unpack PACKAGE [--to ARG]
+stack unpack TARGET [--candidate] [--to DIR]
 ~~~
 
-`stack unpack` downloads a tarball for the specified package and unpacks it.
+`stack unpack` downloads an archive file for one or more specified target
+packages from the package index (e.g. Hackage), or one or more specified target
+package candidates, and unpacks each archive into a subdirectory named after the
+package version.
 
+In the case of packages from the package index, a target can be a package
+name only. In that case, by default:
+
+*   if Stack's `--resolver` option is not specified, the download is for the
+    most recent version of the package in the package index. Stack will first
+    seek to update the index; and
+
+*   if Stack's `--resolver` option is specified, the download is for the version
+    of the package included directly in the specified snapshot.
+
+!!! note
+
+    Stackage snapshots do not include directly most GHC boot packages (packages
+    that come with GHC and are included in GHC's global package database) but
+    some snapshots may include directly some boot packages. In particular, some
+    snapshots include directly `Win32` (which is a boot package on Windows)
+    while others do not.
+
+Otherwise, a target should specify a package name and version (for example,
+`acme-missiles-0.3`). In the case of package versions from the package index,
+optionally, a revision in the package index can be specified by appending
+`@rev:<number>` or `@sha256:<sha>` (for example, `acme-missiles-0.3@rev:0`).
+
 By default:
 
-*   the download is for the most recent version of the package in the package
-    index (eg Hackage). Specify the package name and its version (for example,
-    `acme-missiles-0.1.0.0`) for a particular version of the package; and
+*   the download is from the package index. Pass the flag `--candidate` to
+    specify package candidates; and
 
-*   the package is unpacked into a directory named after the package and its
-    version. Pass the option `--to <directory>` to specify the destination
-    directory.
+    !!! note
+
+        Stack assumes that a package candidate archive is a `.tar.gz` file named
+        after the package version and located at endpoint
+        `package\<package_version>\candidate\`. This is true of Hackage.
+
+*   the target is unpacked into a subdirectory of the current directory. Pass
+    the option `--to <directory>` to specify an alternative destination
+    directory to the current directory. The destination directory can be an
+    absolute one or relative to the current directory.
doc/upgrade_command.md view
@@ -79,7 +79,7 @@   `my-stack upgrade --no-only-local-bin` seeks also to upgrade `my-stack` to the
   latest version of Stack available.
 
-* `stack upgrade --binary-version 2.13.1` seeks an upgrade to Stack 2.13.1 if
+* `stack upgrade --binary-version 2.15.1` seeks an upgrade to Stack 2.15.1 if
   available as a binary distribution for the platform, even if not newer.
 
 * `stack upgrade --source-only` seeks an upgrade by building Stack with
doc/upload_command.md view
@@ -3,21 +3,83 @@ # The `stack upload` command
 
 ~~~text
-stack upload [DIR] [--pvp-bounds PVP-BOUNDS] [--ignore-check]
-             [--[no-]test-tarball] [--tar-dir ARG] [--candidate]
+stack upload [ITEM] [-d|--documentation] [--pvp-bounds PVP-BOUNDS]
+             [--ignore-check] [--[no-]test-tarball] [--tar-dir ARG]
+             [--candidate] [--setup-info-yaml URL]
+             [--snapshot-location-base URL]
 ~~~
 
-Hackage accepts packages for uploading in a standard form, a compressed archive
-('tarball') in the format produced by Cabal's `sdist` action.
+By default:
 
-`stack upload` generates a file for your package, in the format accepted by
-Hackage for uploads, and uploads the package to Hackage. For example, if the
-current working directory is the root directory of your project:
+* the command uploads one or more packages. Pass the flag `--documentation`
+  (`-d` for short) to upload documentation for one or more packages; and
 
+* the upload is a package to be published or documentation for a published
+  package. Pass the flag `--candidate` to upload a
+  [package candidate](http://hackage.haskell.org/upload#candidates) or
+  documentation for a package candidate.
+
+At least one `ITEM` must be specified. For example, if the current working
+directory is a package directory:
+
 ~~~text
 stack upload .
 ~~~
 
+## Upload one or more packages
+
+Hackage accepts packages for uploading in a standard form, a compressed archive
+('tarball') in the format produced by Cabal's `sdist` action.
+
+If `ITEM` is a relative path to an sdist tarball, `stack upload` uploads the
+package to Hackage.
+
+If `ITEM` is a relative path to a package directory, `stack upload` generates a
+file for your package, in the format accepted by Hackage for uploads, and
+uploads the package to Hackage.
+
+By default:
+
+* the command will check each package for common mistakes. Pass the flag
+  `--ignore-check` to disable such checks;
+
+* Stack will not test the resulting package archive. Pass the flag
+  `--test-tarball` to cause Stack to test each resulting package archive, by
+  attempting to build it.
+
+The `--pvp-bounds <pvp_bounds_mode>` option determines whether and, if so, how
+PVP version bounds should be added to the Cabal file of the package. The
+available modes for basic use are: `none`, `lower`, `upper`, and `both`. The
+available modes for use with Cabal file revisions are `lower-revision`,
+`upper-revision` and `both-revision`.
+
+For futher information, see the
+[YAML configuration](yaml_configuration.md#pvp-bounds) documentation.
+
+The `--tar-dir <path_to_directory>` option determines whether the package
+archive should be copied to the specified directory.
+
+## Upload documentation for a package
+
+:octicons-beaker-24: Experimental
+
+[:octicons-tag-24: 2.15.1](https://github.com/commercialhaskell/stack/releases/tag/v2.15.1)
+
+Hackage accepts documentation for a package for uploading in a standard form and
+in a compressed archive ('tarball') in the `.tar.gz` format.
+
+For further information about how to create such an archive file, see the
+documentation for the
+[`stack haddock --haddock-for-hackage`](build_command.md#-no-haddock-for-haddock-flag)
+command.
+
+If `ITEM` is a relative path to a package directory,
+`stack upload <package_directory> --documentation` uploads an existing archive
+file of documentation for the specified package to Hackage.
+
+If the `--documentation` flag is passed then flags specific to package upload
+are ignored.
+
 ## The `HACKAGE_USERNAME` and `HACKAGE_PASSWORD` environment variables
 
 [:octicons-tag-24: 2.3.1](https://github.com/commercialhaskell/stack/releases/tag/v2.3.1)
@@ -67,36 +129,3 @@      $Env:HACKAGE_KEY=<api_authentification_token>
      stack upload .
      ~~~
-
-## `--candidate` flag
-
-Pass the flag to upload a
-[package candidate](http://hackage.haskell.org/upload#candidates).
-
-## `--ignore-check` flag
-
-Pass the flag to disable checks of the package for common mistakes. By default,
-the command will check the package for common mistakes.
-
-## `--pvp-bounds` option
-
-The `--pvp-bounds <pvp_bounds_mode>` option determines whether and, if so, how
-PVP version bounds should be added to the Cabal file of the package. The
-available modes for basic use are: `none`, `lower`, `upper`, and `both`. The
-available modes for use with Cabal file revisions are `lower-revision`,
-`upper-revision` and `both-revision`.
-
-For futher information, see the
-[YAML configuration](yaml_configuration.md#pvp-bounds) documentation.
-
-## `--tar-dir` option
-
-The `--tar-dir <path_to_directory>` option determines whether the package
-archive should be copied to the specified directory.
-
-## `--[no-]test-tarball` flag
-
-Default: Disabled
-
-Set the flag to cause Stack to test the resulting package archive, by attempting
-to build it.
doc/yaml_configuration.md view
@@ -5,6 +5,18 @@ Stack is configured by the content of YAML files. Some Stack operations can also
 be customised by the use of scripts.
 
+!!! info
+
+    A Haskell package is an organised collection of Haskell code and related
+    files. It is described by a Cabal file or a `package.yaml` file (which can
+    be used to generate a Cabal file). The package description is itself part of
+    the package. Its file is located in the root directory of a local package.
+
+    A Stack project is a local directory that contains a Stack project-level
+    configuration file (`stack.yaml`). A project may relate to more than one
+    local package. A single-package project's directory will usually also be the
+    package's root directory.
+
 ## YAML configuration
 
 Stack's YAML configuration options break down into
@@ -69,10 +81,6 @@ Project-specific configuration options are valid only in a project-level
 configuration file (`stack.yaml`).
 
-> Note: We define **project** to mean a directory that contains a `stack.yaml`
-> file, which specifies how to build a set of packages. We define **package** to
-> be a package with a Cabal file or an Hpack `package.yaml` file.
-
 In your project-specific options, you specify both **which local packages** to
 build and **which dependencies to use** when building these packages. Unlike the
 user's local packages, these dependencies aren't built by default. They only get
@@ -83,35 +91,39 @@ applied to your configuration. So, if you add a package to your `packages` list,
 it will be used even if you're using a snapshot that specifies a particular
 version. Similarly, `extra-deps` will shadow the version specified in the
-resolver.
-
-### resolver or snapshot
+snapshot.
 
-Command line equivalent (takes precedence): `--resolver` option
+### snapshot
 
-`resolver` and `snapshot` are synonyms. Only one of these keys is permitted, not
-both.
+Command line equivalent (takes precedence):
+[`--snapshot`](global_flags.md#snapshot-option) or
+[`--resolver`](global_flags.md#resolver-option) option
 
-The `resolver` or `snapshot` key specifies which snapshot is to be used for this
-project. A snapshot defines a GHC version, a number of packages available for
-installation, and various settings like build flags. It is called a resolver
-since a snapshot states how dependencies are resolved. There are currently
-four resolver types:
+The `snapshot` key specifies which snapshot is to be used for this project. A
+snapshot defines a GHC version, a number of packages available for installation,
+and various settings like build flags. It is also called a resolver since a
+snapshot states how dependencies are resolved. There are currently four snapshot
+types:
 
-* LTS Haskell snapshots, e.g. `resolver: lts-21.13`
-* Stackage Nightly snapshots, e.g. `resolver: nightly-2023-09-24`
+* LTS Haskell snapshots, e.g. `snapshot: lts-22.7`
+* Stackage Nightly snapshots, e.g. `snapshot: nightly-2023-12-16`
 * No snapshot, just use packages shipped with the compiler. For GHC this looks
-  like `resolver: ghc-9.6.2`
+  like `snapshot: ghc-9.6.4`
 * Custom snapshot, via a URL or relative file path. For further information, see
-  the [Pantry](pantry.md) documentation.
+  the [snapshot and package location](pantry.md) documentation.
 
-Each of these resolvers will also determine what constraints are placed on the
+Each of these snapshots will also determine what constraints are placed on the
 compiler version. See the [compiler-check](#compiler-check) option for some
 additional control over compiler version.
 
-The `resolver` key corresponds to a Pantry snapshot location. For further
-information, see the [Pantry](pantry.md) documentation.
+The `snapshot` key specifies a snapshot location. For further information, see
+the [snapshot and package location](pantry.md) documentation.
 
+### resolver
+
+`resolver` and [`snapshot`](#snapshot) are synonyms. Only one of these keys is
+permitted, not both.
+
 ### packages
 
 Default:
@@ -144,7 +156,7 @@ item in the list of packages.
 
 A project package is different from a dependency, both a snapshot dependency
-(via the [`resolver` or `snapshot`](#resolver-or-snapshot) key) and an
+(via the [`snapshot`](#snapshot) or [`resolver`](#resolver) key) and an
 extra-deps dependency (via the [`extra-deps`](#extra-deps) key). For example:
 
 * a project package will be built by default by commanding
@@ -158,9 +170,9 @@ Default: `[]`
 
 The `extra-deps` key specifies a list of extra dependencies on top of what is
-defined in the snapshot (specified by the
-[`resolver` or `snapshot`](#resolver-or-snapshot) key). A dependency may come
-from either a Pantry package location or a local file path.
+defined in the snapshot (specified by the [`snapshot`](#snapshot) or
+[`resolver`](#resolver) key). A dependency may come from either a Pantry package
+location or a local file path.
 
 A Pantry package location is one or three different kinds of sources:
 
@@ -183,11 +195,11 @@   - my-package
 # A Git repository at a specific commit:
 - git: https://github.com/example-user/my-repo.git
-  commit: 08c9b4cdf977d5bcd1baba046a007940c1940758
+  commit: '08c9b4cdf977d5bcd1baba046a007940c1940758'
 # An archive of files at a point in the history of a GitHub repository
 # (identified by a specific commit):
 - github: example-user/my-repo
-  commit: 08c9b4cdf977d5bcd1baba046a007940c1940758
+  commit: '08c9b4cdf977d5bcd1baba046a007940c1940758'
   subdirs:
   - my-package
 ~~~
@@ -197,6 +209,12 @@     GHC boot packages are special. An extra-dep with the same package name and
     version as a GHC boot package will be ignored.
 
+!!! note
+
+    The `commit:` key expects a YAML string. A commit hash, or partial hash,
+    comprised only of digits represents a YAML number, unless it is enclosed in
+    quotation marks.
+
 For a local file path source, the path is considered relative to the directory
 containing the `stack.yaml` file. For example, if the `stack.yaml` is located
 at `/dir1/dir2/stack.yaml`, and has:
@@ -245,24 +263,32 @@ 
 Default: `[]`
 
-Packages which, when present in the snapshot specified in `resolver`, should not
-be included in our package. This can be used for a few different purposes, e.g.:
+Packages which, when present in the snapshot specified in the
+[`snapshot`](#snapshot) or [`resolver`](#resolver) key, should not be included
+in our package. This can be used for a few different purposes, e.g.:
 
 * Ensure that packages you don't want used in your project cannot be used in a
   `package.yaml` file (e.g., for license reasons)
-* Prevent overriding of a global package like `Cabal`. For more information, see
-  Stackage issue
-  [#4425](https://github.com/commercialhaskell/stackage/issues/4425)
 * When using a custom GHC build, avoid incompatible packages (see this
   [comment](https://github.com/commercialhaskell/stack/pull/4655#issuecomment-477954429)).
 
 ~~~yaml
 drop-packages:
-- Cabal
 - buggy-package
 - package-with-unacceptable-license
 ~~~
 
+!!! info
+
+    Stackage snapshots LTS Haskell 14.27 (GHC 8.6.5) and earlier, and Nightly
+    2022-02-08 (GHC 8.8.2) and earlier, included directly the `Cabal` package.
+    Later snapshots do not include directly that package (which is a GHC boot
+    package).
+
+    For the older Stackage snapshots, it could be handy to drop the
+    snapshot-specified `Cabal` package, to avoid building that version of the
+    package. For the later snapshots, there is no package version to drop.
+
 ### user-message
 
 If present, specifies a message to be displayed every time the configuration is
@@ -301,6 +327,55 @@ 
 TODO: Add a simple example of how to use custom preprocessors.
 
+### extra-package-dbs
+
+[:octicons-tag-24: 0.1.6.0](https://github.com/commercialhaskell/stack/releases/tag/v0.1.6.0)
+
+Default: `[]`
+
+A list of relative or absolute paths to package databases. These databases will
+be added on top of GHC's global package database before the addition of other
+package databases.
+
+!!! warning
+
+    Use of this feature may result in builds that are not reproducible, as Stack
+    has no control over the contents of the extra package databases.
+
+### curator
+
+:octicons-beaker-24: Experimental
+
+[:octicons-tag-24: 2.1.0.1](https://github.com/commercialhaskell/stack/releases/tag/v2.1.0.1)
+
+Default: `{}`
+
+Configuration intended for use only by the
+[`curator` tool](https://github.com/commercialhaskell/curator), which uses Stack
+to build packages. For given package names (which need not exist in the
+project), Stack can be configured to ignore (skip) silently building test
+suites, building benchmarks and/or creating Haddock documentation or to expect
+that building test suites, building benchmarks and/or creating Haddock
+documentation will fail.
+
+For example:
+
+~~~yaml
+curator:
+  skip-test:
+  - my-package1
+  expect-test-failure:
+  - my-package2
+  skip-bench:
+  - my-package3
+  expect-benchmark-failure:
+  - my-package4
+  skip-haddock:
+  - my-package5
+  expect-haddock-failure:
+  - my-package6
+~~~
+
 ## Non-project-specific configuration
 
 Non-project configuration options can be included in a project-level
@@ -381,7 +456,7 @@ 
     The use of `everything` can break invariants about your snapshot database.
 
-!!! note
+!!! info
 
     Before Stack 0.1.6.0, the default value was `targets`.
 
@@ -407,15 +482,35 @@ 
 Default: The machine architecture on which Stack is running.
 
-Command line equivalent (takes precedence): `--arch` option
+Command line equivalent (takes precedence):
+[`--arch`](global_flags.md#-arch-option) option
 
 Stack identifies different GHC executables by platform (operating system and
 machine architecture), (optional) GHC variant and (optional) GHC build.
 See [`setup-info`](#setup-info).
 
-`arch` sets the machine architecture. Values are those recognized by Cabal,
-including `x86_64`, `i386` and `aarch64`.
+`arch` sets the machine architecture. Values can be those recognized by Cabal
+(the library) (which are case-insensitive and include `i386`, `x86_64`, and
+`aarch64` / `arm64`), or other values (which are case-sensitive and treated as
+an unknown 'other' architecture of the specified name).
 
+By default, Stack will warn the user if the specified machine architecture is an
+unknown 'other' architecture. The warning can be muted; see
+[`notify-if-arch-unknown`](#notify-if-arch-unknown)
+
+!!! note
+
+    The machine architecture on which Stack is running is as classified by
+    Cabal (the library). Cabal does not distinguish between certain
+    architectures. Examples are `ppc64`/`powerpc64`/`powerpc64le` (classified as
+    `ppc64`) and `arm`/`armel`/`armeb` (classified as `arm`).
+
+!!! note
+
+    As Cabal (the library) does not distinguish between machine architectures
+    `powerpc64` and `powerpc64le`, the latter can be specified in Stack's
+    configuration as an 'other' architecture, such as `arch: ppc64le`.
+
 ### build
 
 [:octicons-tag-24: 1.1.0](https://github.com/commercialhaskell/stack/releases/tag/v1.1.0)
@@ -426,46 +521,77 @@ build:
   library-profiling: false
   executable-profiling: false
-  copy-bins: false
-  prefetch: false
-  keep-going: false
-  keep-tmp-files: false
+  library-stripping: true
+  executable-stripping: true
+
   # NOTE: global usage of haddock can cause build failures when documentation is
   # incorrectly formatted.  This could also affect scripts which use Stack.
   haddock: false
   haddock-arguments:
-    haddock-args: [] # Additional arguments passed to haddock, --haddock-arguments
+
+    # Additional arguments passed to haddock. The corresponding command line
+    # option is --haddock-arguments. Example of use:
+    #
     # haddock-args:
     # - "--css=/home/user/my-css"
-  open-haddocks: false # --open
-  haddock-deps: false # if unspecified, defaults to true if haddock is set
+    haddock-args: []
+
+  # The corresponding command line flag is --[no-]open.
+  open-haddocks: false
+
+  # If Stack is configured to build Haddock documentation, defaults to true.
+  haddock-deps: false
+
+  # The configuration is ignored, if haddock-for-hackage: true.
   haddock-internal: false
 
+  # The configuration is ignored, if haddock-for-hackage: true.
+  haddock-hyperlink-source: true
+
+  # If specified, implies haddock-internal: false and
+  # haddock-hyperlink-source: true. Since Stack 2.15.1.
+  haddock-for-hackage: false
+  copy-bins: false
+  copy-compiler-tool: false
+  prefetch: false
+  keep-going: false
+  keep-tmp-files: false
+
   # These are inadvisable to use in your global configuration, as they make the
   # Stack build command line behave quite differently.
+  force-dirty: false
   test: false
   test-arguments:
     rerun-tests: true   # Rerun successful tests
-    additional-args: [] # --test-arguments
+
+    # The corresponding command line option is --test-arguments. Example of use:
+    #
     # additional-args:
     # - "--fail-fast"
+    additional-args: []
     coverage: false
     no-run-tests: false
   bench: false
   benchmark-opts:
-    benchmark-arguments: ""
+
+    # Example of use:
+    #
     # benchmark-arguments: "--csv bench.csv"
+    benchmark-arguments: ""
     no-run-benchmarks: false
-  force-dirty: false
   reconfigure: false
+  cabal-verbosity: normal
   cabal-verbose: false
   split-objs: false
+  skip-components: [] # --skip
 
-  # Since 1.8. Starting with 2.0, the default is true
+  # Since Stack 1.8. Starting with Stack 2.0, the default is true
   interleaved-output: true
-  # Since 2.13.1. Available options are none, count-only, capped and full.
+
+  # Since Stack 2.13.1. Available options are none, count-only, capped and full.
   progress-bar: capped
-  # Since 1.10.
+
+  # Since Stack 1.10.
   ddump-dir: ""
 ~~~
 
@@ -530,14 +656,14 @@ 
 Command line equivalent (takes precedence): `--compiler` option
 
-Overrides the compiler version in the resolver. Note that the `compiler-check`
+Overrides the compiler version in the snapshot. Note that the `compiler-check`
 flag also applies to the version numbers. This uses the same syntax as compiler
-resolvers like `ghc-9.6.2`. This can be used to override the
+resolvers like `ghc-9.6.3`. This can be used to override the
 compiler for a Stackage snapshot, like this:
 
 ~~~yaml
-resolver: lts-21.13
-compiler: ghc-9.6.2
+snapshot: lts-22.7
+compiler: ghc-9.6.3
 compiler-check: match-exact
 ~~~
 
@@ -588,7 +714,7 @@ 
 !!! note
 
-    For some commit IDs, the resolver specified in `hadrian/stack.yaml`
+    For some commit IDs, the snapshot specified in `hadrian/stack.yaml`
     specifies a version of GHC that cannot be used to build GHC. This results in
     GHC's `configure` script reporting messages similar to the following before
     aborting:
@@ -598,11 +724,11 @@     configure: error: GHC version 9.2 or later is required to compile GHC.
     ~~~
 
-    The resolution is: (1) to specify an alternative resolver (one that
+    The resolution is: (1) to specify an alternative snapshot (one that
     specifies a sufficiently recent version of GHC) on the command line, using
-    Stack's option `--resolver <resolver>`. Stack will use that resolver when
+    Stack's option `--resolver <snapshot>`. Stack will use that snapshot when
     running GHC's `configure` script; and (2) to set the contents of the `STACK`
-    environment variable to be `stack --resolver <resolver>`. Hadrian's
+    environment variable to be `stack --resolver <snapshot>`. Hadrian's
     `build-stack` script wil refer to that environment variable for the Stack
     command it uses.
 
@@ -676,7 +802,7 @@ compatible with the compiler.
 
 The easiest way to deal with this issue is to drop the offending packages as
-follows. Instead of using the packages specified in the resolver, the global
+follows. Instead of using the packages specified in the snapshot, the global
 packages bundled with GHC will be used.
 
 ~~~yaml
@@ -692,7 +818,7 @@ ~~~
 extra-deps:
 - git: https://gitlab.haskell.org/ghc/ghc.git
-  commit: 5be7ad7861c8d39f60b7101fd8d8e816ff50353a
+  commit: '5be7ad7861c8d39f60b7101fd8d8e816ff50353a'
   subdirs:
     - libraries/Cabal/Cabal
     - libraries/...
@@ -704,14 +830,14 @@ 
 Default: `match-minor`
 
-Specifies how the compiler version in the resolver is matched against concrete
+Specifies how the compiler version in the snapshot is matched against concrete
 versions. Valid values:
 
 * `match-minor`: make sure that the first three components match, but allow
   patch-level differences. For example< 7.8.4.1 and 7.8.4.2 would both match
   7.8.4. This is useful to allow for custom patch levels of a compiler.
 * `match-exact`: the entire version number must match precisely
-* `newer-minor`: the third component can be increased, e.g. if your resolver is
+* `newer-minor`: the third component can be increased, e.g. if your snapshot is
   `ghc-7.10.1`, then 7.10.2 will also be allowed. This was the default up
   through Stack 0.1.3
 
@@ -752,10 +878,19 @@   - /some/path
   $locals:
   - --happy-option=--ghc
+  $targets:
+  # Only works on platforms where GHC supports linking against shared Haskell
+  # libraries:
+  - --enable-executable-dynamic
   my-package:
   - --another-flag
 ~~~
 
+On platforms where GHC supports linking against shared Haskell libraries (that
+currently excludes Windows), Cabal's `--enable-executable-dynamic` flag (which
+implies `--enable-shared`, unless `--disable-shared` is specified) links
+dependent Haskell libraries into executables dynamically.
+
 ### connection-count
 
 Default: `8`
@@ -897,8 +1032,11 @@ Default: `{}`
 
 Related command line (takes precedence):
-[`stack build --ghc-options`](build_command.md#ghc-options-option) option
+[`stack build --ghc-options`](build_command.md#-ghc-options-option) option
 
+Augment and, if applicable, override any GHC command line options specified in
+Cabal files (including those created from `package.yaml` files).
+
 `ghc-options` can specify GHC command line options for a named package, all
 local packages that are targets (using the `$targets` key), all local packages
 (targets or otherwise) (using the `$locals` key), or all packages (local or
@@ -913,10 +1051,11 @@ ~~~
 
 GHC's command line options are _order-dependent_ and evaluated from left to
-right. Later options can override earlier options. Stack applies options (as
-applicable) in the order of `$everything`, `$locals`, `$targets`, and then those
-for the named package. Any existing GHC command line options of a package are
-applied after those specified in Stack's YAML configuration.
+right. Later options can override the effect of earlier ones. Stack applies
+options (as applicable) in the order of `$everything`, `$locals`, `$targets`,
+and then those for the named package. Any GHC command line options for a package
+specified at Stack's command line are applied after those specified in Stack's
+YAML configuration files.
 
 Since Stack 1.6.1, setting a GHC options for a specific package will
 automatically promote it to a local package (much like setting a custom package
@@ -926,7 +1065,7 @@ for reasoning). This can lead to unpredictable behavior by affecting your
 snapshot packages.
 
-!!! note
+!!! info
 
     Before Stack 1.6.1, the key `*` (then deprecated) had the same function as
     the key `$everything`.
@@ -1116,13 +1255,13 @@ 
 ~~~yaml
 nix:
-  add-gc-roots: false
-  enable: false
-  nix-shell-options: []
-  packages: []
-  path: []
+  enable: false # Except on NixOS, where `enable: true`
   pure: true
+  packages: []
   shell-file:
+  nix-shell-options: []
+  path: []
+  add-gc-roots: false
 ~~~
 
 Command line equivalents: `--nix-*` flags and options (see `stack --nix-help`
@@ -1131,6 +1270,42 @@ For further information, see the
 [Nix integration](nix_integration.md#configuration) documentation.
 
+### notify-if-arch-unknown
+
+[:octicons-tag-24: 2.15.1](https://github.com/commercialhaskell/stack/releases/tag/v2.15.1)
+
+Default: `true`
+
+If the specified machine architecture value is unknown to Cabal (the library),
+should Stack notify the user of that?
+
+### notify-if-cabal-untested
+
+[:octicons-tag-24: 2.15.1](https://github.com/commercialhaskell/stack/releases/tag/v2.15.1)
+
+Default: `true`
+
+If Stack has not been tested with the version of Cabal (the library) that has
+been found, should Stack notify the user of that?
+
+### notify-if-ghc-untested
+
+[:octicons-tag-24: 2.15.1](https://github.com/commercialhaskell/stack/releases/tag/v2.15.1)
+
+Default: `true`
+
+If Stack has not been tested with the version of GHC that is being used, should
+Stack notify the user of that?
+
+### notify-if-nix-on-path
+
+[:octicons-tag-24: 2.15.1](https://github.com/commercialhaskell/stack/releases/tag/v2.15.1)
+
+Default: `true`
+
+If Stack's integration with the Nix package manager is not enabled, should Stack
+notify the user if a `nix` executable is on the PATH?
+
 ### package-index
 
 [:octicons-tag-24: 2.9.3](https://github.com/commercialhaskell/stack/releases/tag/v2.9.3)
@@ -1204,13 +1379,13 @@     ignore-expiry: true
 ~~~
 
-!!! note
+!!! info
 
     Before Stack 2.1.3, the default for `ignore-expiry` was `false`. For more
     information, see
     [issue #4928](https://github.com/commercialhaskell/stack/issues/4928).
 
-!!! note
+!!! info
 
     Before Stack 2.1.1, Stack had a different approach to `package-indices`. For
     more information, see
@@ -1295,7 +1470,7 @@ optimization levels and warning behavior, for which GHC does not recompile the
 modules.
 
-!!! note
+!!! info
 
     Before Stack 0.1.6.0, Stack rebuilt a package when its GHC options changed.
 
@@ -1363,7 +1538,7 @@ 
 'Platforms' are pairs of an operating system and a machine architecture (for
 example, 32-bit i386 or 64-bit x86-64) (represented by the
-`Cabal.Distribution.Systems.Platform` type). Stack currently (version 2.13.1)
+`Cabal.Distribution.Systems.Platform` type). Stack currently (version 2.15.1)
 supports the following pairs in the format of the `setup-info` key:
 
 |Operating system|I386 arch|X86_64 arch|Other machine architectures                                 |
@@ -1638,7 +1813,7 @@   set per project by passing `-p "category:value"` to the `stack new` command.
 * _copyright_ - sets the `copyright` property in Cabal. It is typically the
   name of the holder of the copyright on the package and the year(s) from which
-  copyright is claimed. For example: `Copyright (c) 2006-2007 Joe Bloggs`
+  copyright is claimed. For example: `Copyright (c) 2023-2024 Joe Bloggs`
 * _year_ - if `copyright` is not specified, `year` and `author-name` are used
   to generate the copyright property in Cabal. If `year` is not specified, it
   defaults to the current year.
@@ -1661,7 +1836,7 @@     author-name: Your Name
     author-email: youremail@example.com
     category: Your Projects Category
-    copyright: 'Copyright (c) 2023 Your Name'
+    copyright: 'Copyright (c) 2024 Your Name'
     github-username: yourusername
 ~~~
 
+ src/Codec/Archive/Tar/Utf8.hs view
@@ -0,0 +1,182 @@+module Codec.Archive.Tar.Utf8
+  ( module Codec.Archive.Tar
+  , entryPath
+  , unpack
+  ) where
+
+-- | A module that is equivalent to "Codec.Archive.Tar" from the @tar@ package,
+-- except that @unpack@ assumes that the file paths in an archive are UTF8
+-- encoded.
+
+import           Codec.Archive.Tar hiding ( entryPath, unpack )
+import           Codec.Archive.Tar.Check ( checkSecurity )
+import           Codec.Archive.Tar.Entry ( Entry (..), TarPath, fromLinkTarget )
+import qualified Codec.Archive.Tar.Entry as Tar
+import           Control.Exception ( Exception, catch, throwIO )
+import           Data.Bits ( (.|.), (.&.), shiftL )
+import qualified Data.ByteString.Lazy as LBS
+import           Data.Char ( chr, ord )
+import           Data.Int ( Int64 )
+import           Data.Maybe ( fromMaybe )
+import           Data.Time.Clock.POSIX ( posixSecondsToUTCTime )
+import           System.Directory
+                   ( copyFile, createDirectoryIfMissing, setModificationTime )
+import           System.FilePath ( (</>) )
+import qualified System.FilePath as FP
+import           System.IO.Error ( isPermissionError )
+
+type EpochTime = Int64
+
+-- | Native 'FilePath' of the file or directory within the archive.
+--
+-- Assumes that the 'TarPath' of an 'Entry' is UTF8 encoded.
+entryPath :: Entry -> FilePath
+entryPath = fromTarPath . entryTarPath
+
+-- | Convert a 'TarPath' to a native 'FilePath'.
+--
+-- The native 'FilePath' will use the native directory separator but it is not
+-- otherwise checked for validity or sanity. In particular:
+--
+-- * The tar path may be invalid as a native path, eg the file name @\"nul\"@
+--   is not valid on Windows.
+--
+-- * The tar path may be an absolute path or may contain @\"..\"@ components.
+--   For security reasons this should not usually be allowed, but it is your
+--   responsibility to check for these conditions (eg using 'checkSecurity').
+--
+-- Assumes that the 'TarPath' is UTF8 encoded.
+fromTarPath :: TarPath -> FilePath
+fromTarPath tp = decodeIfUtf8Encoded $ Tar.fromTarPath tp
+
+-- | Create local files and directories based on the entries of a tar archive.
+--
+-- This is a portable implementation of unpacking suitable for portable
+-- archives. It handles 'NormalFile' and 'Directory' entries and has simulated
+-- support for 'SymbolicLink' and 'HardLink' entries. Links are implemented by
+-- copying the target file. This therefore works on Windows as well as Unix.
+-- All other entry types are ignored, that is they are not unpacked and no
+-- exception is raised.
+--
+-- If the 'Entries' ends in an error then it is raised an an exception. Any
+-- files or directories that have been unpacked before the error was
+-- encountered will not be deleted. For this reason you may want to unpack
+-- into an empty directory so that you can easily clean up if unpacking fails
+-- part-way.
+--
+-- On its own, this function only checks for security (using 'checkSecurity').
+-- You can do other checks by applying checking functions to the 'Entries' that
+-- you pass to this function. For example:
+--
+-- > unpack dir (checkTarbomb expectedDir entries)
+--
+-- If you care about the priority of the reported errors then you may want to
+-- use 'checkSecurity' before 'checkTarbomb' or other checks.
+--
+-- Assumes that the 'TarPath' of an `Entry` is UTF8 encoded.
+unpack :: Exception e => FilePath -> Entries e -> IO ()
+unpack baseDir entries = unpackEntries [] (checkSecurity entries)
+                     >>= emulateLinks
+
+  where
+    -- We're relying here on 'checkSecurity' to make sure we're not scribbling
+    -- files all over the place.
+
+    unpackEntries _     (Fail err)      = either throwIO throwIO err
+    unpackEntries links Done            = return links
+    unpackEntries links (Next entry es) = case entryContent entry of
+      NormalFile file _ -> extractFile path file mtime
+                        >> unpackEntries links es
+      Directory         -> extractDir path mtime
+                        >> unpackEntries links es
+      HardLink     link -> (unpackEntries $! saveLink path link links) es
+      SymbolicLink link -> (unpackEntries $! saveLink path link links) es
+      _                 -> unpackEntries links es --ignore other file types
+      where
+        path  = entryPath entry
+        mtime = entryTime entry
+
+    extractFile path content mtime = do
+      -- Note that tar archives do not make sure each directory is created
+      -- before files they contain, indeed we may have to create several
+      -- levels of directory.
+      createDirectoryIfMissing True absDir
+      LBS.writeFile absPath content
+      setModTime absPath mtime
+      where
+        absDir  = baseDir </> FP.takeDirectory path
+        absPath = baseDir </> path
+
+    extractDir path mtime = do
+      createDirectoryIfMissing True absPath
+      setModTime absPath mtime
+      where
+        absPath = baseDir </> path
+
+    saveLink path link links = seq (length path)
+                             $ seq (length link')
+                             $ (path, link'):links
+      where link' = fromLinkTarget link
+
+    emulateLinks = mapM_ $ \(relPath, relLinkTarget) ->
+      let absPath   = baseDir </> relPath
+          absTarget = FP.takeDirectory absPath </> relLinkTarget
+       in copyFile absTarget absPath
+
+setModTime :: FilePath -> EpochTime -> IO ()
+setModTime path t =
+    setModificationTime path (posixSecondsToUTCTime (fromIntegral t))
+      `catch` \e ->
+        if isPermissionError e then return () else throwIO e
+
+-- | If the given 'String' can be interpreted as a string of bytes that encodes
+-- a string using UTF8, then yields the string decoded, otherwise yields the
+-- given 'String'.
+
+-- Inspired by the utf8-string package.
+decodeIfUtf8Encoded :: String -> String
+decodeIfUtf8Encoded s = fromMaybe s $ decode s
+ where
+  decode :: String -> Maybe String
+  decode [] = Just ""
+  decode (c:cs)
+    | c' < 0x80  = decode' c cs
+    | c' < 0xc0  = Nothing
+    | c' < 0xe0  = multi1
+    | c' < 0xf0  = multiByte 2 0b1111 0x00000800
+    | c' < 0xf8  = multiByte 3 0b0111 0x00010000
+    | c' < 0xfc  = multiByte 4 0b0011 0x00200000
+    | c' < 0xfe  = multiByte 5 0b0001 0x04000000
+    | otherwise = Nothing
+   where
+    c' = ord c
+    isValidByte b = b <= 0xff && b .&. 0b11000000 == 0b10000000
+    combine b1 b2 = (b1 `shiftL` 6) .|. (b2 .&. 0b00111111)
+    multi1 = case cs of
+      c1:ds | isValidByte c1' ->
+        let d = combine (c' .&. 0b00011111) c1'
+        in  if d >= 0x80
+              then decode' (chr d) ds
+              else Nothing
+       where
+        c1' = ord c1
+      _ -> Nothing
+    multiByte :: Int -> Int -> Int -> Maybe String
+    multiByte i mask overlong = aux i cs (c' .&. mask)
+      where
+        aux 0 rs acc
+          | isValidAcc = decode' (chr acc) rs
+          | otherwise = Nothing
+         where
+          isValidAcc =  overlong <= acc
+                     && acc <= 0x10ffff
+                     && (acc < 0xd800 || 0xdfff < acc)
+                     && (acc < 0xfffe || 0xffff < acc)
+        aux n (r : rs) acc | isValidByte r' = aux (n - 1) rs $ combine acc r'
+         where
+          r' = ord r
+        aux _ _ _ = Nothing
+  decode' :: Char -> String -> Maybe String
+  decode' x xs = do
+    xs' <- decode xs
+    pure $ x : xs'
src/Control/Concurrent/Execute.hs view
@@ -1,5 +1,7 @@-{-# LANGUAGE NoImplicitPrelude  #-}
-{-# LANGUAGE RecordWildCards    #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 
 -- Concurrent execution with dependencies. Types currently hard-coded for needs
 -- of stack, but could be generalized easily.
@@ -55,9 +57,9 @@     -- ^ The action's unique id.
   , actionDeps :: !(Set ActionId)
     -- ^ Actions on which this action depends.
-  , actionDo :: !(ActionContext -> IO ())
+  , action :: !(ActionContext -> IO ())
     -- ^ The action's 'IO' action, given a context.
-  , actionConcurrency :: !Concurrency
+  , concurrency :: !Concurrency
     -- ^ Whether this action may be run concurrently with others.
   }
 
@@ -69,23 +71,23 @@   deriving Eq
 
 data ActionContext = ActionContext
-  { acRemaining :: !(Set ActionId)
+  { remaining :: !(Set ActionId)
     -- ^ Does not include the current action.
-  , acDownstream :: [Action]
+  , downstream :: [Action]
     -- ^ Actions which depend on the current action.
-  , acConcurrency :: !Concurrency
+  , concurrency :: !Concurrency
     -- ^ Whether this action may be run concurrently with others.
   }
 
 data ExecuteState = ExecuteState
-  { esActions    :: TVar [Action]
-  , esExceptions :: TVar [SomeException]
-  , esInAction   :: TVar (Set ActionId)
-  , esCompleted  :: TVar Int
-  , esKeepGoing  :: Bool
+  { actions    :: TVar [Action]
+  , exceptions :: TVar [SomeException]
+  , inAction   :: TVar (Set ActionId)
+  , completed  :: TVar Int
+  , keepGoing  :: Bool
   }
 
-runActions :: 
+runActions ::
      Int -- ^ threads
   -> Bool -- ^ keep going after one task has failed
   -> [Action]
@@ -98,16 +100,16 @@     <*> newTVarIO Set.empty -- esInAction
     <*> newTVarIO 0 -- esCompleted
     <*> pure keepGoing -- esKeepGoing
-  _ <- async $ withProgress (esCompleted es) (esInAction es)
+  _ <- async $ withProgress es.completed es.inAction
   if threads <= 1
     then runActions' es
     else replicateConcurrently_ threads $ runActions' es
-  readTVarIO $ esExceptions es
+  readTVarIO es.exceptions
 
 -- | Sort actions such that those that can't be run concurrently are at
 -- the end.
 sortActions :: [Action] -> [Action]
-sortActions = sortBy (compareConcurrency `on` actionConcurrency)
+sortActions = sortBy (compareConcurrency `on` (.concurrency))
  where
   -- NOTE: Could derive Ord. However, I like to make this explicit so
   -- that changes to the datatype must consider how it's affecting
@@ -117,73 +119,74 @@   compareConcurrency _ _ = EQ
 
 runActions' :: ExecuteState -> IO ()
-runActions' ExecuteState {..} = loop
+runActions' es = loop
  where
   loop :: IO ()
   loop = join $ atomically $ breakOnErrs $ withActions processActions
 
   breakOnErrs :: STM (IO ()) -> STM (IO ())
   breakOnErrs inner = do
-    errs <- readTVar esExceptions
-    if null errs || esKeepGoing
+    errs <- readTVar es.exceptions
+    if null errs || es.keepGoing
       then inner
       else doNothing
 
   withActions :: ([Action] -> STM (IO ())) -> STM (IO ())
   withActions inner = do
-    actions <- readTVar esActions
+    actions <- readTVar es.actions
     if null actions
       then doNothing
       else inner actions
 
   processActions :: [Action] -> STM (IO ())
   processActions actions = do
-    inAction <- readTVar esInAction
-    case break (Set.null . actionDeps) actions of
+    inAction <- readTVar es.inAction
+    case break (Set.null . (.actionDeps)) actions of
       (_, []) -> do
         check (Set.null inAction)
-        unless esKeepGoing $
-          modifyTVar esExceptions (toException InconsistentDependenciesBug:)
+        unless es.keepGoing $
+          modifyTVar es.exceptions (toException InconsistentDependenciesBug:)
         doNothing
       (xs, action:ys) -> processAction inAction (xs ++ ys) action
 
   processAction :: Set ActionId -> [Action] -> Action -> STM (IO ())
   processAction inAction otherActions action = do
-    let concurrency = actionConcurrency action
+    let concurrency = action.concurrency
     unless (concurrency == ConcurrencyAllowed) $
       check (Set.null inAction)
-    let action' = actionId action
-        otherActions' = Set.fromList $ map actionId otherActions
+    let action' = action.actionId
+        otherActions' = Set.fromList $ map (.actionId) otherActions
         remaining = Set.union otherActions' inAction
+        downstream = downstreamActions action' otherActions
         actionContext = ActionContext
-          { acRemaining = remaining
-          , acDownstream = downstreamActions action' otherActions
-          , acConcurrency = concurrency
+          { remaining
+          , downstream
+          , concurrency
           }
-    writeTVar esActions otherActions
-    modifyTVar esInAction (Set.insert action')
+    writeTVar es.actions otherActions
+    modifyTVar es.inAction (Set.insert action')
     pure $ do
       mask $ \restore -> do
-        eres <- try $ restore $ actionDo action actionContext
+        eres <- try $ restore $ action.action actionContext
         atomically $ do
-          modifyTVar esInAction (Set.delete action')
-          modifyTVar esCompleted (+1)
+          modifyTVar es.inAction (Set.delete action')
+          modifyTVar es.completed (+1)
           case eres of
-            Left err -> modifyTVar esExceptions (err:)
-            Right () -> modifyTVar esActions $ map (dropDep action')
+            Left err -> modifyTVar es.exceptions (err:)
+            Right () -> modifyTVar es.actions $ map (dropDep action')
       loop
 
   -- | Filter a list of actions to include only those that depend on the given
   -- action.
   downstreamActions :: ActionId -> [Action] -> [Action]
-  downstreamActions aid = filter (\a -> aid `Set.member` actionDeps a)
-  
+  downstreamActions aid = filter (\a -> aid `Set.member` a.actionDeps)
+
   -- | Given two actions (the first specified by its id) yield an action
   -- equivalent to the second but excluding any dependency on the first action.
   dropDep :: ActionId -> Action -> Action
   dropDep action' action =
-    action { actionDeps = Set.delete action' $ actionDeps action }
-  
+    action { actionDeps = Set.delete action' action.actionDeps }
+
   -- | @IO ()@ lifted into 'STM'.
   doNothing :: STM (IO ())
   doNothing = pure $ pure ()
src/Data/Attoparsec/Args.hs view
@@ -28,18 +28,24 @@ parseArgsFromString :: EscapingMode -> String -> Either String [String]
 parseArgsFromString mode = P.parseOnly (argsParser mode) . T.pack
 
--- | A basic argument parser. It supports space-separated text, and
--- string quotation with identity escaping: \x -> x.
+-- | A basic argument parser. It supports space-separated text, and string
+-- quotation with identity escaping: \x -> x.
 argsParser :: EscapingMode -> P.Parser [String]
-argsParser mode = many (P.skipSpace *> (quoted <|> unquoted)) <*
-                  P.skipSpace <* (P.endOfInput <?> "unterminated string")
+argsParser mode =
+     many
+       (  P.skipSpace
+       *> (quoted <|> unquoted)
+       )
+  <* P.skipSpace
+  <* (P.endOfInput <?> "unterminated string")
  where
-  unquoted = P.many1 naked
   quoted = P.char '"' *> str <* P.char '"'
-  str = many ( case mode of
-                 Escaping -> escaped <|> nonquote
-                 NoEscaping -> nonquote
-             )
+  unquoted = P.many1 naked
+  str = many
+    ( case mode of
+        Escaping -> escaped <|> nonquote
+        NoEscaping -> nonquote
+    )
   escaped = P.char '\\' *> P.anyChar
   nonquote = P.satisfy (/= '"')
   naked = P.satisfy (not . flip elem ("\" " :: String))
src/Data/Attoparsec/Interpreter.hs view
@@ -60,7 +60,9 @@ import           Conduit ( decodeUtf8C, withSourceFile )
 import           Data.Conduit.Attoparsec ( ParseError (..), Position (..), sinkParserEither )
 import           Data.List ( intercalate )
+import           Data.List.NonEmpty ( singleton )
 import           Data.Text ( pack )
+import           RIO.NonEmpty ( nonEmpty )
 import           Stack.Constants ( stackProgName )
 import           Stack.Prelude
 import           System.FilePath ( takeExtension )
@@ -108,7 +110,7 @@ 
 -- | Extract Stack arguments from a correctly placed and correctly formatted
 -- comment when it is being used as an interpreter
-getInterpreterArgs :: String -> IO [String]
+getInterpreterArgs :: String -> IO (NonEmpty String)
 getInterpreterArgs file = do
   eArgStr <- withSourceFile file parseFile
   case eArgStr of
@@ -134,14 +136,16 @@     mapM_ stackWarn (lines err)
     stackWarn "Missing or unusable Stack options specification"
     stackWarn "Using runghc without any additional Stack options"
-    pure ["runghc"]
+    pure $ singleton "runghc"
 
   parseArgStr str =
     case P.parseOnly (argsParser Escaping) (pack str) of
       Left err -> handleFailure ("Error parsing command specified in the "
                       ++ "Stack options comment: " ++ err)
-      Right [] -> handleFailure "Empty argument list in Stack options comment"
-      Right args -> pure args
+      Right args -> maybe
+        (handleFailure "Empty argument list in Stack options comment")
+        pure
+        (nonEmpty args)
 
   decodeError e =
     case e of
src/GHC/Utils/GhcPkg/Main/Compat.hs view
@@ -5,6 +5,7 @@ {-# LANGUAGE KindSignatures        #-}
 {-# LANGUAGE LambdaCase            #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 {-# LANGUAGE OverloadedStrings     #-}
 {-# LANGUAGE ScopedTypeVariables   #-}
 {-# LANGUAGE TupleSections         #-}
@@ -41,7 +42,6 @@ -----------------------------------------------------------------------------
 
 import qualified Data.Foldable as F
-import           Data.List ( init, isPrefixOf, isSuffixOf, last )
 import qualified Data.Traversable as F
 import           Distribution.InstalledPackageInfo as Cabal
 import           Distribution.Package ( UnitId, mungedId )
@@ -58,7 +58,9 @@ import           Path.IO
                    ( createDirIfMissing, doesDirExist, listDir, removeFile )
 import qualified RIO.ByteString as BS
-import           RIO.Partial ( fromJust )
+import           RIO.List ( isPrefixOf, stripSuffix )
+import           RIO.NonEmpty ( nonEmpty )
+import qualified RIO.NonEmpty as NE
 import           Stack.Constants ( relFilePackageCache )
 import           Stack.Prelude hiding ( display )
 import           System.Environment ( getEnv )
@@ -97,6 +99,8 @@   | SingleFileDBUnsupported !(SomeBase Dir)
   | ParsePackageInfoExceptions !String
   | CannotFindPackage !PackageArg !(Maybe (SomeBase Dir))
+  | CannotParseRelFileBug !String
+  | CannotParseDirectoryWithDBug !String
   deriving (Show, Typeable)
 
 instance Pretty GhcPkgPrettyException where
@@ -149,6 +153,18 @@    where
     pkg_msg (Substring pkgpat _) = fillSep ["matching", fromString pkgpat]
     pkg_msg pkgarg' = fromString $ show pkgarg'
+  pretty (CannotParseRelFileBug relFileName) = bugPrettyReport "[S-9323]" $
+    fillSep
+      [ flow "changeDBDir': Could not parse"
+      , style File (fromString relFileName)
+      , flow "as a relative path to a file."
+      ]
+  pretty (CannotParseDirectoryWithDBug dirName) = bugPrettyReport "[S-7651]" $
+    fillSep
+      [ flow "adjustOldDatabasePath: Could not parse"
+      , style Dir (fromString dirName)
+      , flow "as a directory."
+      ]
 
 instance Exception GhcPkgPrettyException
 
@@ -194,10 +210,11 @@ displayGlobPkgId (GlobPackageIdentifier pn) = display pn ++ "-*"
 
 readGlobPkgId :: String -> RIO env GlobPackageIdentifier
-readGlobPkgId str
-  | "-*" `isSuffixOf` str =
-    GlobPackageIdentifier <$> parseCheck (init (init str)) "package identifier (glob)"
-  | otherwise = ExactPackageIdentifier <$> parseCheck str "package identifier (exact)"
+readGlobPkgId str = case stripSuffix "-*" str of
+  Nothing ->
+    ExactPackageIdentifier <$> parseCheck str "package identifier (exact)"
+  Just str' ->
+    GlobPackageIdentifier <$> parseCheck str' "package identifier (glob)"
 
 readPackageArg :: AsPackageArg -> String -> RIO env PackageArg
 readPackageArg AsUnitId str = IUId <$> parseCheck str "installed package id"
@@ -251,13 +268,14 @@   let sys_databases = [Abs globalDb]
   e_pkg_path <- tryIO (liftIO $ System.Environment.getEnv "GHC_PACKAGE_PATH")
   let env_stack =
-        case e_pkg_path of
+        case nonEmpty <$> e_pkg_path of
           Left _ -> sys_databases
-          Right path
-            | not (null path) && isSearchPathSeparator (last path)
-            -> mapMaybe parseSomeDir (splitSearchPath (init path)) <> sys_databases
+          Right Nothing -> []
+          Right (Just path)
+            | isSearchPathSeparator (NE.last path)
+            -> mapMaybe parseSomeDir (splitSearchPath (NE.init path)) <> sys_databases
             | otherwise
-            -> mapMaybe parseSomeDir (splitSearchPath path)
+            -> mapMaybe parseSomeDir (splitSearchPath $ NE.toList path)
 
   -- -f flags on the command line add to the database stack, unless any of them
   -- are present in the stack already.
@@ -265,19 +283,19 @@ 
   (db_stack, db_to_operate_on) <- getDatabases pkgDb final_stack
 
-  let flag_db_stack = [ db | db <- db_stack, location db == Abs pkgDb ]
+  let flag_db_stack = [ db | db <- db_stack, db.location == Abs pkgDb ]
 
   prettyDebugL
     $ flow "Db stack:"
-    : map (pretty . location) db_stack
+    : map (pretty . (.location)) db_stack
   F.forM_ db_to_operate_on $ \db ->
     prettyDebugL
       [ "Modifying:"
-      , pretty $ location db
+      , pretty db.location
       ]
   prettyDebugL
     $ flow "Flag db stack:"
-    : map (pretty . location) flag_db_stack
+    : map (pretty . (.location)) flag_db_stack
 
   pure (db_stack, db_to_operate_on, flag_db_stack)
  where
@@ -293,7 +311,7 @@               then (, Nothing) <$> readDatabase db_path
               else do
                 let hasPkg :: PackageDB mode -> Bool
-                    hasPkg = not . null . findPackage pkgarg . packages
+                    hasPkg = not . null . findPackage pkgarg . (.packages)
 
                     openRo (e::IOException) = do
                       db <- readDatabase db_path
@@ -315,7 +333,7 @@                       -- If the database is not for modification after all,
                       -- drop the write lock as we are already finished with
                       -- the database.
-                      case packageDbLock db of
+                      case db.packageDbLock of
                         GhcPkg.DbOpenReadWrite lock ->
                           liftIO $ GhcPkg.unlockPackageDb lock
                       pure (ro_db, Nothing)
@@ -380,8 +398,8 @@        [InstalledPackageInfo]
     -> GhcPkg.DbOpenMode mode GhcPkg.PackageDbLock
     -> RIO env (PackageDB mode)
-  mkPackageDB pkgs lock = do
-    pure $ PackageDB
+  mkPackageDB pkgs lock =
+    pure PackageDB
       { location = path
       , packageDbLock = lock
       , packages = pkgs
@@ -422,7 +440,7 @@   content <- liftIO $ readFile (prjSomeBase toFilePath path) `catchIO` \_ -> pure ""
   if take 2 content == "[]"
     then do
-      let path_dir = adjustOldDatabasePath path
+      path_dir <- adjustOldDatabasePath path
       prettyWarnL
         [ flow "Ignoring old file-style db and trying"
         , pretty path_dir
@@ -451,19 +469,26 @@ adjustOldFileStylePackageDB db = do
   -- assumes we have not yet established if it's an old style or not
   mcontent <- liftIO $
-    fmap Just (readFile (prjSomeBase toFilePath (location db))) `catchIO` \_ -> pure Nothing
+    fmap Just (readFile (prjSomeBase toFilePath db.location)) `catchIO` \_ -> pure Nothing
   case fmap (take 2) mcontent of
     -- it is an old style and empty db, so look for a dir kind in location.d/
-    Just "[]" -> pure db
-      { location = adjustOldDatabasePath $ location db }
+    Just "[]" -> do
+      adjustedDatabasePath <- adjustOldDatabasePath db.location
+      pure db { location = adjustedDatabasePath }
     -- it is old style but not empty, we have to bail
-    Just _ -> prettyThrowIO $ SingleFileDBUnsupported (location db)
+    Just _ -> prettyThrowIO $ SingleFileDBUnsupported db.location
     -- probably not old style, carry on as normal
     Nothing -> pure db
 
-adjustOldDatabasePath :: SomeBase Dir -> SomeBase Dir
-adjustOldDatabasePath =
-  fromJust . prjSomeBase (parseSomeDir . (<> ".d") . toFilePath)
+adjustOldDatabasePath :: SomeBase Dir -> RIO env (SomeBase Dir)
+adjustOldDatabasePath = prjSomeBase addDToDirName
+ where
+  addDToDirName dir = do
+    let dirNameWithD = toFilePath dir <> ".d"
+    maybe
+      (prettyThrowIO $ CannotParseDirectoryWithDBug dirNameWithD)
+      pure
+      (parseSomeDir dirNameWithD)
 
 parsePackageInfo :: BS.ByteString -> RIO env (InstalledPackageInfo, [String])
 parsePackageInfo str =
@@ -489,7 +514,7 @@   -> RIO env ()
 changeNewDB cmds new_db = do
   new_db' <- adjustOldFileStylePackageDB new_db
-  prjSomeBase (createDirIfMissing True) (location new_db')
+  prjSomeBase (createDirIfMissing True) new_db'.location
   changeDBDir' cmds new_db'
 
 changeDBDir' ::
@@ -499,13 +524,16 @@   -> RIO env ()
 changeDBDir' cmds db = do
   mapM_ do_cmd cmds
-  case packageDbLock db of
+  case db.packageDbLock of
     GhcPkg.DbOpenReadWrite lock -> liftIO $ GhcPkg.unlockPackageDb lock
  where
   do_cmd (RemovePackage p) = do
-    let relFileConf =
-          fromJust (parseRelFile $ display (installedUnitId p) <> ".conf")
-        file = mapSomeBase (P.</> relFileConf) (location db)
+    let relFileConfName = display (installedUnitId p) <> ".conf"
+    relFileConf <- maybe
+      (prettyThrowIO $ CannotParseRelFileBug relFileConfName)
+      pure
+      (parseRelFile relFileConfName)
+    let file = mapSomeBase (P.</> relFileConf) db.location
     prettyDebugL
       [ "Removing"
       , pretty file
@@ -539,7 +567,7 @@     getPkgDatabases globalDb pkgarg pkgDb >>= \case
       (_, GhcPkg.DbOpenReadWrite (db :: PackageDB GhcPkg.DbReadWrite), _) -> do
         pks <- do
-          let pkgs = packages db
+          let pkgs = db.packages
               ps = findPackage pkgarg pkgs
           -- This shouldn't happen if getPkgsByPkgDBs picks the DB correctly.
           when (null ps) $ cannotFindPackage pkgarg $ Just db
@@ -550,7 +578,7 @@   -- consider.
   getPkgsByPkgDBs pkgsByPkgDBs ( pkgsByPkgDB : pkgsByPkgDBs') pkgarg = do
     let (db, pks') = pkgsByPkgDB
-        pkgs = packages db
+        pkgs = db.packages
         ps = findPackage pkgarg pkgs
         pks = map installedUnitId ps
         pkgByPkgDB' = (db, pks <> pks')
@@ -568,7 +596,7 @@ 
   unregisterPackages' :: (PackageDB GhcPkg.DbReadWrite, [UnitId]) -> RIO env ()
   unregisterPackages' (db, pks) = do
-    let pkgs = packages db
+    let pkgs = db.packages
         cmds = [ RemovePackage pkg
                | pkg <- pkgs, installedUnitId pkg `elem` pks
                ]
@@ -591,7 +619,7 @@ 
 cannotFindPackage :: PackageArg -> Maybe (PackageDB mode) -> RIO env a
 cannotFindPackage pkgarg mdb =
-  prettyThrowIO $ CannotFindPackage pkgarg (location <$> mdb)
+  prettyThrowIO $ CannotFindPackage pkgarg ((.location) <$> mdb)
 
 matches :: GlobPackageIdentifier -> MungedPackageId -> Bool
 GlobPackageIdentifier pn `matches` pid' = pn == mungedName pid'
src/Network/HTTP/StackClient.hs view
@@ -14,6 +14,7 @@   , setRequestCheckStatus
   , setRequestMethod
   , setRequestHeader
+  , setRequestHeaders
   , addRequestHeader
   , setRequestBody
   , getResponseHeaders
@@ -37,6 +38,8 @@   , hAccept
   , hContentLength
   , hContentMD5
+  , method
+  , methodPost
   , methodPut
   , formDataBody
   , partFileRequestBody
@@ -45,6 +48,7 @@   , setGitHubHeaders
   , download
   , redownload
+  , requestBody
   , verifiedDownload
   , verifiedDownloadWithProgress
   , CheckHexDigest (..)
@@ -66,6 +70,7 @@                    ( ConduitM, ConduitT, awaitForever, (.|), yield, await )
 import           Data.Conduit.Lift ( evalStateC )
 import qualified Data.Conduit.List as CL
+import           Data.List.Extra ( (!?) )
 import           Data.Monoid ( Sum (..) )
 import qualified Data.Text as T
 import           Data.Time.Clock
@@ -73,7 +78,7 @@ import           Network.HTTP.Client
                    ( HttpException (..), HttpExceptionContent (..), Request
                    , RequestBody (..), Response (..), checkResponse, getUri
-                   , parseRequest, parseUrlThrow, path
+                   , method, parseRequest, parseUrlThrow, path, requestBody
                    )
 import           Network.HTTP.Client.MultipartFormData
                    ( formDataBody, partBS, partFileRequestBody, partLBS )
@@ -92,16 +97,17 @@ import           Network.HTTP.Simple
                    ( addRequestHeader, getResponseBody, getResponseHeaders
                    , getResponseStatusCode, setRequestBody
-                   , setRequestCheckStatus, setRequestHeader, setRequestMethod
+                   , setRequestCheckStatus, setRequestHeader, setRequestHeaders
+                   , setRequestMethod
                    )
 import qualified Network.HTTP.Simple
                    ( httpJSON, httpLbs, httpNoBody, httpSink, withResponse )
 import           Network.HTTP.Types
-                   ( hAccept, hContentLength, hContentMD5, methodPut
+                   ( hAccept, hContentLength, hContentMD5, methodPost, methodPut
                    , notFound404
                    )
 import           Path ( Abs, File, Path )
-import           Prelude ( until, (!!) )
+import           Prelude ( until )
 import           RIO
 import           RIO.PrettyPrint ( HasTerm )
 import           Text.Printf ( printf )
@@ -248,14 +254,17 @@ bytesfmt :: Integral a => String -> a -> String
 bytesfmt formatter bs = printf (formatter <> " %s")
                                (fromIntegral (signum bs) * dec :: Double)
-                               (bytesSuffixes !! i)
+                               bytesSuffix
  where
-  (dec,i) = getSuffix (abs bs)
-  getSuffix n = until p (\(x,y) -> (x / 1024, y+1)) (fromIntegral n,0)
+  (dec, i) = getSuffix (abs bs)
+  getSuffix n = until p (\(x, y) -> (x / 1024, y + 1)) (fromIntegral n, 0)
    where
-    p (n',numDivs) = n' < 1024 || numDivs == length bytesSuffixes - 1
+    p (n', numDivs) = n' < 1024 || numDivs == length bytesSuffixes - 1
   bytesSuffixes :: [String]
   bytesSuffixes = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
+  bytesSuffix = fromMaybe
+    (error "bytesfmt: the impossible happened! Index out of range.")
+    (bytesSuffixes !? i)
 
 -- Await eagerly (collect with monoidal append),
 -- but space out yields by at least the given amount of time.
src/Options/Applicative/Builder/Extra.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE NoImplicitPrelude   #-}
-{-# LANGUAGE RecordWildCards     #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 
 -- | Extra functions for optparse-applicative.
 
@@ -260,13 +261,13 @@ absFileOption :: Mod OptionFields (Path Abs File) -> Parser (Path Abs File)
 absFileOption mods = option (eitherReader' parseAbsFile) $
      completer
-       (pathCompleterWith defaultPathCompleterOpts { pcoRelative = False })
+       (pathCompleterWith defaultPathCompleterOpts { relative = False })
   <> mods
 
 relFileOption :: Mod OptionFields (Path Rel File) -> Parser (Path Rel File)
 relFileOption mods = option (eitherReader' parseRelFile) $
      completer
-       (pathCompleterWith defaultPathCompleterOpts { pcoAbsolute = False })
+       (pathCompleterWith defaultPathCompleterOpts { absolute = False })
   <> mods
 
 absDirOption :: Mod OptionFields (Path Abs Dir) -> Parser (Path Abs Dir)
@@ -274,8 +275,8 @@      completer
        ( pathCompleterWith
           defaultPathCompleterOpts
-            { pcoRelative = False
-            , pcoFileFilter = const False
+            { relative = False
+            , fileFilter = const False
             }
        )
   <> mods
@@ -285,8 +286,8 @@      completer
        ( pathCompleterWith
            defaultPathCompleterOpts
-             { pcoAbsolute = False
-             , pcoFileFilter = const False
+             { absolute = False
+             , fileFilter = const False
              }
        )
   <> mods
@@ -296,20 +297,20 @@ eitherReader' f = eitherReader (mapLeft show . f)
 
 data PathCompleterOpts = PathCompleterOpts
-  { pcoAbsolute :: Bool
-  , pcoRelative :: Bool
-  , pcoRootDir :: Maybe FilePath
-  , pcoFileFilter :: FilePath -> Bool
-  , pcoDirFilter :: FilePath -> Bool
+  { absolute :: Bool
+  , relative :: Bool
+  , rootDir :: Maybe FilePath
+  , fileFilter :: FilePath -> Bool
+  , dirFilter :: FilePath -> Bool
   }
 
 defaultPathCompleterOpts :: PathCompleterOpts
 defaultPathCompleterOpts = PathCompleterOpts
-  { pcoAbsolute = True
-  , pcoRelative = True
-  , pcoRootDir = Nothing
-  , pcoFileFilter = const True
-  , pcoDirFilter = const True
+  { absolute = True
+  , relative = True
+  , rootDir = Nothing
+  , fileFilter = const True
+  , dirFilter = const True
   }
 
 fileCompleter :: Completer
@@ -318,14 +319,14 @@ fileExtCompleter :: [String] -> Completer
 fileExtCompleter exts =
   pathCompleterWith
-    defaultPathCompleterOpts { pcoFileFilter = (`elem` exts) . takeExtension }
+    defaultPathCompleterOpts { fileFilter = (`elem` exts) . takeExtension }
 
 dirCompleter :: Completer
 dirCompleter =
-  pathCompleterWith defaultPathCompleterOpts { pcoFileFilter = const False }
+  pathCompleterWith defaultPathCompleterOpts { fileFilter = const False }
 
 pathCompleterWith :: PathCompleterOpts -> Completer
-pathCompleterWith PathCompleterOpts {..} = mkCompleter $ \inputRaw -> do
+pathCompleterWith pco = mkCompleter $ \inputRaw -> do
   -- Unescape input, to handle single and double quotes. Note that the
   -- results do not need to be re-escaped, due to some fiddly bash
   -- magic.
@@ -333,15 +334,15 @@   let (inputSearchDir0, searchPrefix) = splitFileName input
       inputSearchDir = if inputSearchDir0 == "./" then "" else inputSearchDir0
   msearchDir <-
-    case (isRelative inputSearchDir, pcoAbsolute, pcoRelative) of
+    case (isRelative inputSearchDir, pco.absolute, pco.relative) of
       (True, _, True) -> do
-        rootDir <- maybe getCurrentDirectory pure pcoRootDir
+        rootDir <- maybe getCurrentDirectory pure pco.rootDir
         pure $ Just (rootDir </> inputSearchDir)
       (False, True, _) -> pure $ Just inputSearchDir
       _ -> pure Nothing
   case msearchDir of
     Nothing
-      | input == "" && pcoAbsolute -> pure ["/"]
+      | input == "" && pco.absolute -> pure ["/"]
       | otherwise -> pure []
     Just searchDir -> do
       entries <-
@@ -354,7 +355,7 @@             if searchPrefix `isPrefixOf` entry
               then do
                 let path = searchDir </> entry
-                case (pcoFileFilter path, pcoDirFilter path) of
+                case (pco.fileFilter path, pco.dirFilter path) of
                   (True, True) -> pure $ Just (inputSearchDir </> entry)
                   (fileAllowed, dirAllowed) -> do
                     isDir <- doesDirectoryExist path
src/Options/Applicative/Complicated.hs view
@@ -57,8 +57,17 @@   -> AddCommand
      -- ^ commands (use 'addCommand')
   -> IO (GlobalOptsMonoid, RIO Runner ())
-complicatedOptions numericVersion stringVersion numericHpackVersion h pd
-  footerStr commonParser mOnFailure commandParser = do
+complicatedOptions
+    numericVersion
+    stringVersion
+    numericHpackVersion
+    h
+    pd
+    footerStr
+    commonParser
+    mOnFailure
+    commandParser
+  = do
     args <- getArgs
     (a, (b, c)) <- let parserPrefs = prefs $ noBacktrack <> showHelpOnEmpty
                    in  case execParserPure parserPrefs parser args of
src/Path/Extra.hs view
@@ -4,7 +4,6 @@ -- | Extra Path utilities.
 module Path.Extra
   ( toFilePathNoTrailingSep
-  , dropRoot
   , parseCollapsedAbsDir
   , parseCollapsedAbsFile
   , concatAndCollapseAbsDir
@@ -21,10 +20,9 @@ 
 import           Data.Time ( UTCTime )
 import           Path
-                   ( Abs, Dir, File, PathException (..), Rel, parseAbsDir
+                   ( Abs, Dir, File, Path, PathException (..), parseAbsDir
                    , parseAbsFile, toFilePath
                    )
-import           Path.Internal ( Path (Path) )
 import           Path.IO
                    ( doesDirExist, doesFileExist, getCurrentDir
                    , getModificationTime
@@ -88,11 +86,6 @@   go rs x = x:rs
   checkPathSeparator [x] = FP.isPathSeparator x
   checkPathSeparator _ = False
-
--- | Drop the root (either @\/@ on POSIX or @C:\\@, @D:\\@, etc. on
--- Windows).
-dropRoot :: Path Abs t -> Path Rel t
-dropRoot (Path l) = Path (FP.dropDrive l)
 
 -- | If given file in 'Maybe' does not exist, ensure we have 'Nothing'. This
 -- is to be used in conjunction with 'forgivingAbsence' and
src/Stack.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE NoImplicitPrelude   #-}
 {-# LANGUAGE GADTs               #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 {-# LANGUAGE OverloadedStrings   #-}
 
 -- | Main Stack tool entry point.
@@ -77,9 +78,9 @@       throwIO exitCode
     Right (globalMonoid, run) -> do
       global <- globalOptsFromMonoid isTerminal globalMonoid
-      when (globalLogLevel global == LevelDebug) $
+      when (global.logLevel == LevelDebug) $
         hPutStrLn stderr versionString'
-      case globalReExecVersion global of
+      case global.reExecVersion of
         Just expectVersion -> do
           expectVersion' <- parseVersionThrowing expectVersion
           unless (checkVersion MatchMinor expectVersion' stackVersion) $
src/Stack/Build.hs view
@@ -1,6 +1,7 @@-{-# LANGUAGE NoImplicitPrelude   #-}
-{-# LANGUAGE LambdaCase          #-}
-{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- | Build the project.
 
@@ -16,33 +17,37 @@ import           Data.Attoparsec.Args ( EscapingMode (Escaping), parseArgs )
 import           Data.List ( (\\) )
 import           Data.List.Extra ( groupSort )
-import qualified Data.List.NonEmpty as NE
 import qualified Data.Map as Map
 import qualified Data.Set as Set
 import qualified Data.Text as T
-import qualified Distribution.PackageDescription as C
-import           Distribution.Types.Dependency ( Dependency (..), depLibraries )
+-- import qualified Distribution.PackageDescription as C
+-- import           Distribution.Types.Dependency ( Dependency (..), depLibraries )
 import           Distribution.Version ( mkVersion )
+import           RIO.NonEmpty ( nonEmpty )
+import qualified RIO.NonEmpty as NE
 import           Stack.Build.ConstructPlan ( constructPlan )
 import           Stack.Build.Execute ( executePlan, preFetch, printPlan )
 import           Stack.Build.Installed ( getInstalled, toInstallMap )
 import           Stack.Build.Source ( localDependencies, projectLocalPackages )
 import           Stack.Build.Target ( NeedTargets (..) )
 import           Stack.FileWatch ( fileWatch, fileWatchPoll )
-import           Stack.Package ( resolvePackage )
+import           Stack.Package ( buildableExes, resolvePackage )
 import           Stack.Prelude hiding ( loadPackage )
 import           Stack.Runners ( ShouldReexec (..), withConfig, withEnvConfig )
 import           Stack.Setup ( withNewLocalBuildTargets )
 import           Stack.Types.Build
-                   ( Plan (..), Task (..), TaskType (..), taskLocation )
+                   ( Plan (..), Task (..), TaskType (..), taskLocation
+                   , taskProvides
+                   )
 import           Stack.Types.Build.Exception
                    ( BuildException (..), BuildPrettyException (..) )
 import           Stack.Types.BuildConfig ( HasBuildConfig, stackYamlL )
-import           Stack.Types.BuildOpts
-                   ( BuildCommand (..), BuildOpts (..), BuildOptsCLI (..)
-                   , FileWatchOpts (..), buildOptsMonoidBenchmarksL
-                   , buildOptsMonoidHaddockL, buildOptsMonoidInstallExesL
-                   , buildOptsMonoidTestsL
+import           Stack.Types.BuildOpts ( BuildOpts (..) )
+import           Stack.Types.BuildOptsCLI
+                   ( BuildCommand (..), BuildOptsCLI (..), FileWatchOpts (..) )
+import           Stack.Types.BuildOptsMonoid
+                   ( buildOptsMonoidBenchmarksL, buildOptsMonoidHaddockL
+                   , buildOptsMonoidInstallExesL, buildOptsMonoidTestsL
                    )
 import           Stack.Types.Compiler ( getGhcVersion )
 import           Stack.Types.CompilerPaths ( cabalVersionL )
@@ -60,11 +65,12 @@ import           Stack.Types.NamedComponent ( exeComponents )
 import           Stack.Types.Package
                    ( InstallLocation (..), LocalPackage (..), Package (..)
-                   , PackageConfig (..), lpFiles, lpFilesForComponents )
+                   , PackageConfig (..), lpFiles, lpFilesForComponents
+                   )
 import           Stack.Types.Platform ( HasPlatform (..) )
 import           Stack.Types.Runner ( Runner, globalOptsL )
 import           Stack.Types.SourceMap
-                   ( CommonPackage (..), ProjectPackage (..), SMTargets (..)
+                   ( SMTargets (..)
                    , SourceMap (..), Target (..) )
 import           System.Terminal ( fixCodePage )
 
@@ -92,10 +98,10 @@ -- | Helper for build and install commands
 buildCmd :: BuildOptsCLI -> RIO Runner ()
 buildCmd opts = do
-  when (any (("-prof" `elem`) . fromRight [] . parseArgs Escaping) (boptsCLIGhcOptions opts)) $
+  when (any (("-prof" `elem`) . fromRight [] . parseArgs Escaping) opts.ghcOptions) $
     prettyThrowIO GHCProfOptionInvalid
   local (over globalOptsL modifyGO) $
-    case boptsCLIFileWatch opts of
+    case opts.fileWatch of
       FileWatchPoll -> fileWatchPoll (inner . Just)
       FileWatch -> fileWatch (inner . Just)
       NoFileWatch -> inner Nothing
@@ -107,14 +113,19 @@       Stack.Build.build setLocalFiles
   -- Read the build command from the CLI and enable it to run
   modifyGO =
-    case boptsCLICommand opts of
-      Test -> set (globalOptsBuildOptsMonoidL.buildOptsMonoidTestsL) (Just True)
-      Haddock ->
-        set (globalOptsBuildOptsMonoidL.buildOptsMonoidHaddockL) (Just True)
-      Bench ->
-        set (globalOptsBuildOptsMonoidL.buildOptsMonoidBenchmarksL) (Just True)
-      Install ->
-        set (globalOptsBuildOptsMonoidL.buildOptsMonoidInstallExesL) (Just True)
+    case opts.command of
+      Test -> set
+        (globalOptsBuildOptsMonoidL . buildOptsMonoidTestsL)
+        (Just True)
+      Haddock -> set
+        (globalOptsBuildOptsMonoidL . buildOptsMonoidHaddockL)
+        (Just True)
+      Bench -> set
+        (globalOptsBuildOptsMonoidL . buildOptsMonoidBenchmarksL)
+        (Just True)
+      Install -> set
+        (globalOptsBuildOptsMonoidL . buildOptsMonoidInstallExesL)
+        (Just True)
       Build -> id -- Default case is just Build
 
 -- | Build.
@@ -126,27 +137,25 @@       => Maybe (Set (Path Abs File) -> IO ()) -- ^ callback after discovering all local files
       -> RIO env ()
 build msetLocalFiles = do
-  mcp <- view $ configL.to configModifyCodePage
-  ghcVersion <- view $ actualCompilerVersionL.to getGhcVersion
+  mcp <- view $ configL . to (.modifyCodePage)
+  ghcVersion <- view $ actualCompilerVersionL . to getGhcVersion
   fixCodePage mcp ghcVersion $ do
     bopts <- view buildOptsL
-    sourceMap <- view $ envConfigL.to envConfigSourceMap
+    sourceMap <- view $ envConfigL . to (.sourceMap)
     locals <- projectLocalPackages
     depsLocals <- localDependencies
     let allLocals = locals <> depsLocals
 
-    checkSubLibraryDependencies (Map.elems $ smProject sourceMap)
-
-    boptsCli <- view $ envConfigL.to envConfigBuildOptsCLI
+    boptsCli <- view $ envConfigL . to (.buildOptsCLI)
     -- Set local files, necessary for file watching
     stackYaml <- view stackYamlL
     for_ msetLocalFiles $ \setLocalFiles -> do
       files <-
-        if boptsCLIWatchAll boptsCli
+        if boptsCli.watchAll
         then sequence [lpFiles lp | lp <- allLocals]
         else forM allLocals $ \lp -> do
-          let pn = packageName (lpPackage lp)
-          case Map.lookup pn (smtTargets $ smTargets sourceMap) of
+          let pn = lp.package.name
+          case Map.lookup pn sourceMap.targets.targets of
             Nothing ->
               pure Set.empty
             Just (TargetAll _) ->
@@ -162,9 +171,15 @@         getInstalled installMap
 
     baseConfigOpts <- mkBaseConfigOpts boptsCli
-    plan <- constructPlan baseConfigOpts localDumpPkgs loadPackage sourceMap installedMap (boptsCLIInitialBuildSteps boptsCli)
+    plan <- constructPlan
+              baseConfigOpts
+              localDumpPkgs
+              loadPackage
+              sourceMap
+              installedMap
+              boptsCli.initialBuildSteps
 
-    allowLocals <- view $ configL.to configAllowLocals
+    allowLocals <- view $ configL . to (.allowLocals)
     unless allowLocals $ case justLocals plan of
       [] -> pure ()
       localsIdents -> throwM $ LocalPackagesPresent localsIdents
@@ -173,10 +188,10 @@     warnAboutSplitObjs bopts
     warnIfExecutablesWithSameNameCouldBeOverwritten locals plan
 
-    when (boptsPreFetch bopts) $
+    when bopts.preFetch $
         preFetch plan
 
-    if boptsCLIDryrun boptsCli
+    if boptsCli.dryrun
       then printPlan plan
       else executePlan
              boptsCli
@@ -186,7 +201,7 @@              snapshotDumpPkgs
              localDumpPkgs
              installedMap
-             (smtTargets $ smTargets sourceMap)
+             sourceMap.targets.targets
              plan
 
 buildLocalTargets ::
@@ -201,7 +216,7 @@   map taskProvides .
   filter ((== Local) . taskLocation) .
   Map.elems .
-  planTasks
+  (.tasks)
 
 checkCabalVersion :: HasEnvConfig env => RIO env ()
 checkCabalVersion = do
@@ -258,14 +273,14 @@   warnings :: Map Text ([PackageName],[PackageName])
   warnings =
     Map.mapMaybe
-      (\(pkgsToBuild,localPkgs) ->
-        case (pkgsToBuild,NE.toList localPkgs \\ NE.toList pkgsToBuild) of
-          (_ :| [],[]) ->
+      (\(pkgsToBuild, localPkgs) ->
+        case (pkgsToBuild, NE.toList localPkgs \\ NE.toList pkgsToBuild) of
+          (_ :| [], []) ->
             -- We want to build the executable of single local package
             -- and there are no other local packages with an executable of
             -- the same name. Nothing to warn about, ignore.
             Nothing
-          (_,otherLocals) ->
+          (_, otherLocals) ->
             -- We could be here for two reasons (or their combination):
             -- 1) We are building two or more executables with the same
             --    name that will end up overwriting each other.
@@ -273,28 +288,28 @@             --    there are other local packages with an executable of the
             --    same name that might get overwritten.
             -- Both cases warrant a warning.
-            Just (NE.toList pkgsToBuild,otherLocals))
+            Just (NE.toList pkgsToBuild, otherLocals))
       (Map.intersectionWith (,) exesToBuild localExes)
   exesToBuild :: Map Text (NonEmpty PackageName)
   exesToBuild =
     collect
-      [ (exe,pkgName')
-      | (pkgName',task) <- Map.toList (planTasks plan)
-      , TTLocalMutable lp <- [taskType task]
-      , exe <- (Set.toList . exeComponents . lpComponents) lp
+      [ (exe, pkgName')
+      | (pkgName', task) <- Map.toList plan.tasks
+      , TTLocalMutable lp <- [task.taskType]
+      , exe <- (Set.toList . exeComponents . (.components)) lp
       ]
   localExes :: Map Text (NonEmpty PackageName)
   localExes =
     collect
-      [ (exe,packageName pkg)
-      | pkg <- map lpPackage locals
-      , exe <- Set.toList (packageExes pkg)
+      [ (exe, pkg.name)
+      | pkg <- map (.package) locals
+      , exe <- Set.toList (buildableExes pkg)
       ]
-  collect :: Ord k => [(k,v)] -> Map k (NonEmpty v)
-  collect = Map.map NE.fromList . Map.fromDistinctAscList . groupSort
+  collect :: Ord k => [(k, v)] -> Map k (NonEmpty v)
+  collect = Map.mapMaybe nonEmpty . Map.fromDistinctAscList . groupSort
 
 warnAboutSplitObjs :: HasTerm env => BuildOpts -> RIO env ()
-warnAboutSplitObjs bopts | boptsSplitObjs bopts =
+warnAboutSplitObjs bopts |  bopts.splitObjs =
   prettyWarnL
     [ flow "Building with"
     , style Shell "--split-objs"
@@ -313,21 +328,21 @@ -- | Get the @BaseConfigOpts@ necessary for constructing configure options
 mkBaseConfigOpts :: (HasEnvConfig env)
                  => BuildOptsCLI -> RIO env BaseConfigOpts
-mkBaseConfigOpts boptsCli = do
-  bopts <- view buildOptsL
-  snapDBPath <- packageDatabaseDeps
-  localDBPath <- packageDatabaseLocal
+mkBaseConfigOpts buildOptsCLI = do
+  buildOpts <- view buildOptsL
+  snapDB <- packageDatabaseDeps
+  localDB <- packageDatabaseLocal
   snapInstallRoot <- installationRootDeps
   localInstallRoot <- installationRootLocal
-  packageExtraDBs <- packageDatabaseExtra
+  extraDBs <- packageDatabaseExtra
   pure BaseConfigOpts
-    { bcoSnapDB = snapDBPath
-    , bcoLocalDB = localDBPath
-    , bcoSnapInstallRoot = snapInstallRoot
-    , bcoLocalInstallRoot = localInstallRoot
-    , bcoBuildOpts = bopts
-    , bcoBuildOptsCLI = boptsCli
-    , bcoExtraDBs = packageExtraDBs
+    { snapDB
+    , localDB
+    , snapInstallRoot
+    , localInstallRoot
+    , buildOpts
+    , buildOptsCLI
+    , extraDBs
     }
 
 -- | Provide a function for loading package information from the package index
@@ -339,16 +354,16 @@   -> [Text] -- ^ Cabal configure options
   -> RIO env Package
 loadPackage loc flags ghcOptions cabalConfigOpts = do
-  compiler <- view actualCompilerVersionL
+  compilerVersion <- view actualCompilerVersionL
   platform <- view platformL
   let pkgConfig = PackageConfig
-        { packageConfigEnableTests = False
-        , packageConfigEnableBenchmarks = False
-        , packageConfigFlags = flags
-        , packageConfigGhcOptions = ghcOptions
-        , packageConfigCabalConfigOpts = cabalConfigOpts
-        , packageConfigCompilerVersion = compiler
-        , packageConfigPlatform = platform
+        { enableTests = False
+        , enableBenchmarks = False
+        , flags
+        , ghcOptions
+        , cabalConfigOpts
+        , compilerVersion
+        , platform
         }
   resolvePackage pkgConfig <$> loadCabalFileImmutable loc
 
@@ -358,38 +373,7 @@     prettyThrowM $ SomeTargetsNotBuildable unbuildable
  where
   unbuildable =
-    [ (packageName (lpPackage lp), c)
+    [ (lp.package.name, c)
     | lp <- lps
-    , c <- Set.toList (lpUnbuildable lp)
+    , c <- Set.toList lp.unbuildable
     ]
-
--- | Find if any sublibrary dependency (other than internal libraries) exists in
--- each project package.
-checkSubLibraryDependencies :: HasTerm env => [ProjectPackage] -> RIO env ()
-checkSubLibraryDependencies projectPackages =
-  forM_ projectPackages $ \projectPackage -> do
-    C.GenericPackageDescription pkgDesc _ _ lib subLibs foreignLibs exes tests benches <-
-      liftIO $ cpGPD . ppCommon $ projectPackage
-
-    let pName = pkgName . C.package $ pkgDesc
-        dependencies = concatMap getDeps subLibs <>
-                       concatMap getDeps foreignLibs <>
-                       concatMap getDeps exes <>
-                       concatMap getDeps tests <>
-                       concatMap getDeps benches <>
-                       maybe [] C.condTreeConstraints lib
-        notInternal (Dependency pName' _ _) = pName' /= pName
-        publicDependencies = filter notInternal dependencies
-        publicLibraries = concatMap (toList . depLibraries) publicDependencies
-
-    when (subLibDepExist publicLibraries) $
-      prettyWarnS
-        "Sublibrary dependency is not supported, this will almost certainly \
-        \fail."
- where
-  getDeps (_, C.CondNode _ dep _) = dep
-  subLibDepExist = any
-    ( \case
-        C.LSubLibName _ -> True
-        C.LMainLibName  -> False
-    )
src/Stack/Build/Cache.hs view
@@ -1,6 +1,7 @@-{-# LANGUAGE NoImplicitPrelude     #-}
-{-# LANGUAGE DataKinds             #-}
-{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 -- | Cache information about previous builds
 module Stack.Build.Cache
@@ -70,7 +71,9 @@                    , installationRootDeps, installationRootLocal
                    , platformGhcRelDir
                    )
-import           Stack.Types.GhcPkgId ( GhcPkgId, ghcPkgIdString )
+import           Stack.Types.GhcPkgId ( ghcPkgIdString )
+import           Stack.Types.Installed
+                   (InstalledLibraryInfo (..), foldOnGhcPkgId' )
 import           Stack.Types.NamedComponent ( NamedComponent (..) )
 import           Stack.Types.SourceMap ( smRelDir )
 import           System.PosixCompat.Files
@@ -129,12 +132,13 @@                -> m (Path Abs File)
 buildCacheFile dir component = do
   cachesDir <- buildCachesDir dir
-  smh <- view $ envConfigL.to envConfigSourceMapHash
+  smh <- view $ envConfigL . to (.sourceMapHash)
   smDirName <- smRelDir smh
   let nonLibComponent prefix name = prefix <> "-" <> T.unpack name
   cacheFileName <- parseRelFile $ case component of
     CLib -> "lib"
-    CInternalLib name -> nonLibComponent "internal-lib" name
+    CSubLib name -> nonLibComponent "sub-lib" name
+    CFlib name -> nonLibComponent "flib" name
     CExe name -> nonLibComponent "exe" name
     CTest name -> nonLibComponent "test" name
     CBench name -> nonLibComponent "bench" name
@@ -148,8 +152,9 @@ tryGetBuildCache dir component = do
   fp <- buildCacheFile dir component
   ensureDir $ parent fp
-  either (const Nothing) (Just . buildCacheTimes) <$>
-    liftIO (tryAny (Yaml.decodeFileThrow (toFilePath fp)))
+  let decode :: MonadIO m => m BuildCache
+      decode = Yaml.decodeFileThrow (toFilePath fp)
+  either (const Nothing) (Just . (.times)) <$> liftIO (tryAny decode)
 
 -- | Try to read the dirtiness cache for the given package directory.
 tryGetConfigCache ::
@@ -203,9 +208,7 @@                 -> Map FilePath FileCacheInfo -> RIO env ()
 writeBuildCache dir component times = do
   fp <- toFilePath <$> buildCacheFile dir component
-  liftIO $ Yaml.encodeFile fp BuildCache
-    { buildCacheTimes = times
-    }
+  liftIO $ Yaml.encodeFile fp BuildCache { times = times }
 
 -- | Write the dirtiness cache for this package's configuration.
 writeConfigCache :: HasEnvConfig env
@@ -262,14 +265,11 @@ flagCacheKey installed = do
   installationRoot <- installationRootLocal
   case installed of
-    Library _ gid _ ->
-      pure $
-      configCacheKey installationRoot (ConfigCacheTypeFlagLibrary gid)
-    Executable ident ->
-      pure $
-        configCacheKey
-          installationRoot
-          (ConfigCacheTypeFlagExecutable ident)
+    Library _ installedInfo -> do
+      let gid = installedInfo.ghcPkgId
+      pure $ configCacheKey installationRoot (ConfigCacheTypeFlagLibrary gid)
+    Executable ident -> pure $
+      configCacheKey installationRoot (ConfigCacheTypeFlagExecutable ident)
 
 -- | Loads the flag cache for the given installed extra-deps
 tryGetFlagCache :: HasEnvConfig env
@@ -341,13 +341,13 @@ --
 -- We only pay attention to non-directory options. We don't want to avoid a
 -- cache hit just because it was installed in a different directory.
-getPrecompiledCacheKey :: HasEnvConfig env
-                    => PackageLocationImmutable
-                    -> ConfigureOpts
-                    -> Bool -- ^ build haddocks
-                    -> Set GhcPkgId -- ^ dependencies
-                    -> RIO env PrecompiledCacheKey
-getPrecompiledCacheKey loc copts buildHaddocks installedPackageIDs = do
+getPrecompiledCacheKey ::
+     HasEnvConfig env
+  => PackageLocationImmutable
+  -> ConfigureOpts
+  -> Bool -- ^ build haddocks
+  -> RIO env PrecompiledCacheKey
+getPrecompiledCacheKey loc configureOpts buildHaddocks = do
   compiler <- view actualCompilerVersionL
   cabalVersion <- view cabalVersionL
 
@@ -359,63 +359,75 @@   platformGhcDir <- platformGhcRelDir
 
   -- In Cabal versions 1.22 and later, the configure options contain the
-  -- installed package IDs, which is what we need for a unique hash.
-  -- Unfortunately, earlier Cabals don't have the information, so we must
-  -- supplement it with the installed package IDs directly.
-  -- See issue: https://github.com/commercialhaskell/stack/issues/1103
-  let input = (coNoDirs copts, installedPackageIDs)
-      optionsHash = Mem.convert $ hashWith SHA256 $ encodeUtf8 $ tshow input
+  -- installed package IDs, which is what we need for a unique hash. See also
+  -- issue: https://github.com/commercialhaskell/stack/issues/1103
+  let optionsToHash = configureOpts.nonPathRelated
+      optionsHash =
+        Mem.convert $ hashWith SHA256 $ encodeUtf8 $ tshow optionsToHash
 
-  pure $ precompiledCacheKey platformGhcDir compiler cabalVersion packageKey optionsHash buildHaddocks
+  pure $ precompiledCacheKey
+    platformGhcDir compiler cabalVersion packageKey optionsHash buildHaddocks
 
 -- | Write out information about a newly built package
-writePrecompiledCache :: HasEnvConfig env
-                      => BaseConfigOpts
-                      -> PackageLocationImmutable
-                      -> ConfigureOpts
-                      -> Bool -- ^ build haddocks
-                      -> Set GhcPkgId -- ^ dependencies
-                      -> Installed -- ^ library
-                      -> [GhcPkgId] -- ^ sublibraries, in the GhcPkgId format
-                      -> Set Text -- ^ executables
-                      -> RIO env ()
-writePrecompiledCache baseConfigOpts loc copts buildHaddocks depIDs mghcPkgId sublibs exes = do
-  key <- getPrecompiledCacheKey loc copts buildHaddocks depIDs
-  ec <- view envConfigL
-  let stackRootRelative = makeRelative (view stackRootL ec)
-  mlibpath <- case mghcPkgId of
-    Executable _ -> pure Nothing
-    Library _ ipid _ -> Just <$> pathFromPkgId stackRootRelative ipid
-  sublibpaths <- mapM (pathFromPkgId stackRootRelative) sublibs
-  exes' <- forM (Set.toList exes) $ \exe -> do
-    name <- parseRelFile $ T.unpack exe
-    stackRootRelative $ bcoSnapInstallRoot baseConfigOpts </> bindirSuffix </> name
-  let precompiled = PrecompiledCache
-        { pcLibrary = mlibpath
-        , pcSubLibs = sublibpaths
-        , pcExes = exes'
-        }
-  savePrecompiledCache key precompiled
-  -- reuse precompiled cache with haddocks also in case when haddocks are not
-  -- required
-  when buildHaddocks $ do
-    key' <- getPrecompiledCacheKey loc copts False depIDs
-    savePrecompiledCache key' precompiled
+writePrecompiledCache ::
+     HasEnvConfig env
+  => BaseConfigOpts
+  -> PackageLocationImmutable
+  -> ConfigureOpts
+  -> Bool -- ^ build haddocks
+  -> Installed -- ^ library
+  -> Set Text -- ^ executables
+  -> RIO env ()
+writePrecompiledCache
+    baseConfigOpts
+    loc
+    copts
+    buildHaddocks
+    mghcPkgId
+    exes
+  = do
+      key <- getPrecompiledCacheKey loc copts buildHaddocks
+      ec <- view envConfigL
+      let stackRootRelative = makeRelative (view stackRootL ec)
+      exes' <- forM (Set.toList exes) $ \exe -> do
+        name <- parseRelFile $ T.unpack exe
+        stackRootRelative $
+           baseConfigOpts.snapInstallRoot </> bindirSuffix </> name
+      let installedLibToPath libName ghcPkgId pcAction = do
+            libPath <- pathFromPkgId stackRootRelative ghcPkgId
+            pc <- pcAction
+            pure $ case libName of
+              Nothing -> pc { library = Just libPath }
+              _ -> pc { subLibs = libPath : pc.subLibs }
+      precompiled <- foldOnGhcPkgId'
+        installedLibToPath
+        mghcPkgId
+        ( pure PrecompiledCache
+            { library = Nothing
+            , subLibs = []
+            , exes = exes'
+            }
+        )
+      savePrecompiledCache key precompiled
+      -- reuse precompiled cache with haddocks also in case when haddocks are
+      -- not required
+      when buildHaddocks $ do
+        key' <- getPrecompiledCacheKey loc copts False
+        savePrecompiledCache key' precompiled
  where
   pathFromPkgId stackRootRelative ipid = do
     ipid' <- parseRelFile $ ghcPkgIdString ipid ++ ".conf"
-    stackRootRelative $ bcoSnapDB baseConfigOpts </> ipid'
+    stackRootRelative $ baseConfigOpts.snapDB </> ipid'
 
--- | Check the cache for a precompiled package matching the given
--- configuration.
-readPrecompiledCache :: forall env. HasEnvConfig env
-                     => PackageLocationImmutable -- ^ target package
-                     -> ConfigureOpts
-                     -> Bool -- ^ build haddocks
-                     -> Set GhcPkgId -- ^ dependencies
-                     -> RIO env (Maybe (PrecompiledCache Abs))
-readPrecompiledCache loc copts buildHaddocks depIDs = do
-  key <- getPrecompiledCacheKey loc copts buildHaddocks depIDs
+-- | Check the cache for a precompiled package matching the given configuration.
+readPrecompiledCache ::
+     forall env. HasEnvConfig env
+  => PackageLocationImmutable -- ^ target package
+  -> ConfigureOpts
+  -> Bool -- ^ build haddocks
+  -> RIO env (Maybe (PrecompiledCache Abs))
+readPrecompiledCache loc copts buildHaddocks = do
+  key <- getPrecompiledCacheKey loc copts buildHaddocks
   mcache <- loadPrecompiledCache key
   maybe (pure Nothing) (fmap Just . mkAbs) mcache
  where
@@ -429,7 +441,7 @@     stackRoot <- view stackRootL
     let mkAbs' = (stackRoot </>)
     pure PrecompiledCache
-      { pcLibrary = mkAbs' <$> pcLibrary pc0
-      , pcSubLibs = mkAbs' <$> pcSubLibs pc0
-      , pcExes = mkAbs' <$> pcExes pc0
+      { library = mkAbs' <$> pc0.library
+      , subLibs = mkAbs' <$> pc0.subLibs
+      , exes = mkAbs' <$> pc0.exes
       }
src/Stack/Build/ConstructPlan.hs view
@@ -1,1309 +1,1249 @@ {-# LANGUAGE NoImplicitPrelude     #-}
-{-# LANGUAGE LambdaCase            #-}
-{-# LANGUAGE OverloadedStrings     #-}
-{-# LANGUAGE ViewPatterns          #-}
-
--- | Construct a @Plan@ for how to build
-module Stack.Build.ConstructPlan
-  ( constructPlan
-  ) where
-
-import           Control.Monad.RWS.Strict
-                   ( RWST, get, modify, modify', pass, put, runRWST, tell )
-import           Control.Monad.Trans.Maybe ( MaybeT (..) )
-import qualified Data.List as L
-import qualified Data.Map.Merge.Strict as Map
-import qualified Data.Map.Strict as Map
-import           Data.Monoid.Map ( MonoidMap(..) )
-import qualified Data.Set as Set
-import qualified Data.Text as T
-import           Distribution.Types.BuildType ( BuildType (Configure) )
-import           Distribution.Types.PackageName ( mkPackageName )
-import           Generics.Deriving.Monoid ( memptydefault, mappenddefault )
-import           Path ( parent )
-import           RIO.Process ( HasProcessContext (..), findExecutable )
-import           RIO.State ( State, execState )
-import           Stack.Build.Cache ( tryGetFlagCache )
-import           Stack.Build.Haddock ( shouldHaddockDeps )
-import           Stack.Build.Source ( loadLocalPackage )
-import           Stack.Constants ( compilerOptionsCabalFlag )
-import           Stack.Package ( applyForceCustomBuild )
-import           Stack.Prelude hiding ( loadPackage )
-import           Stack.SourceMap ( getPLIVersion, mkProjectPackage )
-import           Stack.Types.Build
-                   ( CachePkgSrc (..), ConfigCache (..), Plan (..), Task (..)
-                   , TaskConfigOpts (..), TaskType (..)
-                   , installLocationIsMutable, taskIsTarget, taskLocation
-                   , taskTargetIsMutable, toCachePkgSrc
-                   )
-import           Stack.Types.Build.Exception
-                   ( BadDependency (..), BuildException (..)
-                   , BuildPrettyException (..), ConstructPlanException (..)
-                   )
-import           Stack.Types.BuildConfig
-                   ( BuildConfig (..), HasBuildConfig (..), stackYamlL )
-import           Stack.Types.BuildOpts
-                   ( BuildOpts (..), BuildOptsCLI (..), BuildSubset (..) )
-import           Stack.Types.Compiler ( WhichCompiler (..) )
-import           Stack.Types.CompilerPaths
-                   ( CompilerPaths (..), HasCompiler (..) )
-import           Stack.Types.Config ( Config (..), HasConfig (..), stackRootL )
-import           Stack.Types.ConfigureOpts
-                   ( BaseConfigOpts (..), ConfigureOpts (..), configureOpts )
-import           Stack.Types.Curator ( Curator (..) )
-import           Stack.Types.Dependency
-                   ( DepValue (DepValue), DepType (AsLibrary) )
-import           Stack.Types.DumpPackage ( DumpPackage (..) )
-import           Stack.Types.EnvConfig
-                   ( EnvConfig (..), HasEnvConfig (..), HasSourceMap (..) )
-import           Stack.Types.EnvSettings ( EnvSettings (..), minimalEnvSettings )
-import           Stack.Types.GHCVariant ( HasGHCVariant (..) )
-import           Stack.Types.GhcPkgId ( GhcPkgId )
-import           Stack.Types.GlobalOpts ( GlobalOpts (..) )
-import           Stack.Types.IsMutable ( IsMutable (..) )
-import           Stack.Types.NamedComponent ( exeComponents, renderComponent )
-import           Stack.Types.Package
-                   ( ExeName (..), InstallLocation (..), Installed (..)
-                   , InstalledMap, LocalPackage (..), Package (..)
-                   , PackageLibraries (..), PackageSource (..), installedVersion
-                   , packageIdentifier, psVersion, runMemoizedWith
-                   )
-import           Stack.Types.ParentMap ( ParentMap )
-import           Stack.Types.Platform ( HasPlatform (..) )
-import           Stack.Types.ProjectConfig ( isPCGlobalProject )
-import           Stack.Types.Runner ( HasRunner (..), globalOptsL )
-import           Stack.Types.SourceMap
-                   ( CommonPackage (..), DepPackage (..), FromSnapshot (..)
-                   , GlobalPackage (..), SMTargets (..), SourceMap (..)
-                   )
-import           Stack.Types.Version
-                   ( latestApplicableVersion, versionRangeText, withinRange )
-import           System.Environment ( lookupEnv )
-
-data PackageInfo
-  = PIOnlyInstalled InstallLocation Installed
-    -- ^ This indicates that the package is already installed, and that we
-    -- shouldn't build it from source. This is only the case for global
-    -- packages.
-  | PIOnlySource PackageSource
-    -- ^ This indicates that the package isn't installed, and we know where to
-    -- find its source.
-  | PIBoth PackageSource Installed
-    -- ^ This indicates that the package is installed and we know where to find
-    -- its source. We may want to reinstall from source.
-  deriving Show
-
-combineSourceInstalled :: PackageSource
-                       -> (InstallLocation, Installed)
-                       -> PackageInfo
-combineSourceInstalled ps (location, installed) =
-  assert (psVersion ps == installedVersion installed) $
-    case location of
-      -- Always trust something in the snapshot
-      Snap -> PIOnlyInstalled location installed
-      Local -> PIBoth ps installed
-
-type CombinedMap = Map PackageName PackageInfo
-
-combineMap :: Map PackageName PackageSource -> InstalledMap -> CombinedMap
-combineMap = Map.merge
-  (Map.mapMissing (\_ s -> PIOnlySource s))
-  (Map.mapMissing (\_ i -> uncurry PIOnlyInstalled i))
-  (Map.zipWithMatched (\_ s i -> combineSourceInstalled s i))
-
-data AddDepRes
-  = ADRToInstall Task
-  | ADRFound InstallLocation Installed
-  deriving Show
-
-data W = W
-  { wFinals :: !(Map PackageName (Either ConstructPlanException Task))
-  , wInstall :: !(Map Text InstallLocation)
-    -- ^ executable to be installed, and location where the binary is placed
-  , wDirty :: !(Map PackageName Text)
-    -- ^ why a local package is considered dirty
-  , wWarnings :: !([StyleDoc] -> [StyleDoc])
-    -- ^ Warnings
-  , wParents :: !ParentMap
-    -- ^ Which packages a given package depends on, along with the package's
-    -- version
-  }
-  deriving Generic
-
-instance Semigroup W where
-  (<>) = mappenddefault
-
-instance Monoid W where
-  mempty = memptydefault
-  mappend = (<>)
-
-type M = RWST -- TODO replace with more efficient WS stack on top of (RIO Ctx).
-  Ctx
-  W
-  (Map PackageName (Either ConstructPlanException AddDepRes))
-  -- ^ Library map
-  IO
-
-data Ctx = Ctx
-  { baseConfigOpts :: !BaseConfigOpts
-  , loadPackage    :: !(  PackageLocationImmutable
-                       -> Map FlagName Bool
-                       -> [Text]
-                       -> [Text]
-                       -> M Package
-                       )
-  , combinedMap    :: !CombinedMap
-  , ctxEnvConfig   :: !EnvConfig
-  , callStack      :: ![PackageName]
-  , wanted         :: !(Set PackageName)
-  , localNames     :: !(Set PackageName)
-  , mcurator       :: !(Maybe Curator)
-  , pathEnvVar     :: !Text
-  }
-
-instance HasPlatform Ctx where
-  platformL = configL.platformL
-  {-# INLINE platformL #-}
-  platformVariantL = configL.platformVariantL
-  {-# INLINE platformVariantL #-}
-
-instance HasGHCVariant Ctx where
-  ghcVariantL = configL.ghcVariantL
-  {-# INLINE ghcVariantL #-}
-
-instance HasLogFunc Ctx where
-  logFuncL = configL.logFuncL
-
-instance HasRunner Ctx where
-  runnerL = configL.runnerL
-
-instance HasStylesUpdate Ctx where
-  stylesUpdateL = runnerL.stylesUpdateL
-
-instance HasTerm Ctx where
-  useColorL = runnerL.useColorL
-  termWidthL = runnerL.termWidthL
-
-instance HasConfig Ctx where
-  configL = buildConfigL.lens bcConfig (\x y -> x { bcConfig = y })
-  {-# INLINE configL #-}
-
-instance HasPantryConfig Ctx where
-  pantryConfigL = configL.pantryConfigL
-
-instance HasProcessContext Ctx where
-  processContextL = configL.processContextL
-
-instance HasBuildConfig Ctx where
-  buildConfigL = envConfigL.lens
-    envConfigBuildConfig
-    (\x y -> x { envConfigBuildConfig = y })
-
-instance HasSourceMap Ctx where
-  sourceMapL = envConfigL.sourceMapL
-
-instance HasCompiler Ctx where
-  compilerPathsL = envConfigL.compilerPathsL
-
-instance HasEnvConfig Ctx where
-  envConfigL = lens ctxEnvConfig (\x y -> x { ctxEnvConfig = y })
-
--- | Computes a build plan. This means figuring out which build 'Task's to take,
--- and the interdependencies among the build 'Task's. In particular:
---
--- 1) It determines which packages need to be built, based on the transitive
--- deps of the current targets. For local packages, this is indicated by the
--- 'lpWanted' boolean. For extra packages to build, this comes from the
--- @extraToBuild0@ argument of type @Set PackageName@. These are usually
--- packages that have been specified on the command line.
---
--- 2) It will only rebuild an upstream package if it isn't present in the
--- 'InstalledMap', or if some of its dependencies have changed.
---
--- 3) It will only rebuild a local package if its files are dirty or some of its
--- dependencies have changed.
-constructPlan ::
-     forall env. HasEnvConfig env
-  => BaseConfigOpts
-  -> [DumpPackage] -- ^ locally registered
-  -> (  PackageLocationImmutable
-     -> Map FlagName Bool
-     -> [Text]
-     -> [Text]
-     -> RIO EnvConfig Package
-     )
-     -- ^ load upstream package
-  -> SourceMap
-  -> InstalledMap
-  -> Bool
-  -> RIO env Plan
-constructPlan baseConfigOpts0 localDumpPkgs loadPackage0 sourceMap installedMap initialBuildSteps = do
-  logDebug "Constructing the build plan"
-
-  when hasBaseInDeps $
-    prettyWarn $
-         fillSep
-           [ flow "You are trying to upgrade or downgrade the"
-           , style Current "base"
-           , flow "package, which is almost certainly not what you really \
-                  \want. Please, consider using another GHC version if you \
-                  \need a certain version of"
-           , style Current "base" <> ","
-           , flow "or removing"
-           , style Current "base"
-           , flow "as an"
-           , style Shell "extra-deps" <> "."
-           , flow "For further information, see"
-           , style Url "https://github.com/commercialhaskell/stack/issues/3940" <> "."
-           ]
-      <> line
-
-  econfig <- view envConfigL
-  globalCabalVersion <- view $ compilerPathsL.to cpCabalVersion
-  sources <- getSources globalCabalVersion
-  mcur <- view $ buildConfigL.to bcCurator
-
-  let onTarget = void . getCachedDepOrAddDep
-  let inner = mapM_ onTarget $ Map.keys (smtTargets $ smTargets sourceMap)
-  pathEnvVar' <- liftIO $ maybe mempty T.pack <$> lookupEnv "PATH"
-  let ctx = mkCtx econfig globalCabalVersion sources mcur pathEnvVar'
-  ((), m, W efinals installExes dirtyReason warnings parents) <-
-    liftIO $ runRWST inner ctx Map.empty
-  mapM_ prettyWarn (warnings [])
-  let toEither (_, Left e)  = Left e
-      toEither (k, Right v) = Right (k, v)
-      (errlibs, adrs) = partitionEithers $ map toEither $ Map.toList m
-      (errfinals, finals) = partitionEithers $ map toEither $ Map.toList efinals
-      errs = errlibs ++ errfinals
-  if null errs
-    then do
-      let toTask (_, ADRFound _ _) = Nothing
-          toTask (name, ADRToInstall task) = Just (name, task)
-          tasks = Map.fromList $ mapMaybe toTask adrs
-          takeSubset =
-            case boptsCLIBuildSubset $ bcoBuildOptsCLI baseConfigOpts0 of
-              BSAll -> pure
-              BSOnlySnapshot -> pure . stripLocals
-              BSOnlyDependencies ->
-                pure . stripNonDeps (Map.keysSet $ smDeps sourceMap)
-              BSOnlyLocals -> errorOnSnapshot
-      takeSubset Plan
-        { planTasks = tasks
-        , planFinals = Map.fromList finals
-        , planUnregisterLocal =
-            mkUnregisterLocal tasks dirtyReason localDumpPkgs initialBuildSteps
-        , planInstallExes =
-            if boptsInstallExes (bcoBuildOpts baseConfigOpts0) ||
-               boptsInstallCompilerTool (bcoBuildOpts baseConfigOpts0)
-                then installExes
-                else Map.empty
-        }
-    else do
-      stackYaml <- view stackYamlL
-      stackRoot <- view stackRootL
-      isImplicitGlobal <- view $ configL.to (isPCGlobalProject . configProject)
-      prettyThrowM $ ConstructPlanFailed
-        errs stackYaml stackRoot isImplicitGlobal parents (wanted ctx) prunedGlobalDeps
- where
-  hasBaseInDeps = Map.member (mkPackageName "base") (smDeps sourceMap)
-
-  mkCtx econfig globalCabalVersion sources mcur pathEnvVar' = Ctx
-    { baseConfigOpts = baseConfigOpts0
-    , loadPackage = \w x y z -> runRIO econfig $
-        applyForceCustomBuild globalCabalVersion <$> loadPackage0 w x y z
-    , combinedMap = combineMap sources installedMap
-    , ctxEnvConfig = econfig
-    , callStack = []
-    , wanted = Map.keysSet (smtTargets $ smTargets sourceMap)
-    , localNames = Map.keysSet (smProject sourceMap)
-    , mcurator = mcur
-    , pathEnvVar = pathEnvVar'
-    }
-
-  prunedGlobalDeps = flip Map.mapMaybe (smGlobal sourceMap) $
-    \case
-      ReplacedGlobalPackage deps ->
-        let pruned = filter (not . inSourceMap) deps
-        in  if null pruned then Nothing else Just pruned
-      GlobalPackage _ -> Nothing
-
-  inSourceMap pname = pname `Map.member` smDeps sourceMap ||
-                      pname `Map.member` smProject sourceMap
-
-  getSources :: Version -> RIO env (Map PackageName PackageSource)
-  getSources globalCabalVersion = do
-    let loadLocalPackage' pp = do
-          lp <- loadLocalPackage pp
-          let lpPackage' =
-                applyForceCustomBuild globalCabalVersion $ lpPackage lp
-          pure lp { lpPackage = lpPackage' }
-    pPackages <- for (smProject sourceMap) $ \pp -> do
-      lp <- loadLocalPackage' pp
-      pure $ PSFilePath lp
-    bopts <- view $ configL.to configBuild
-    deps <- for (smDeps sourceMap) $ \dp ->
-      case dpLocation dp of
-        PLImmutable loc ->
-          pure $
-            PSRemote loc (getPLIVersion loc) (dpFromSnapshot dp) (dpCommon dp)
-        PLMutable dir -> do
-          pp <- mkProjectPackage YesPrintWarnings dir (shouldHaddockDeps bopts)
-          lp <- loadLocalPackage' pp
-          pure $ PSFilePath lp
-    pure $ pPackages <> deps
-
--- | Throw an exception if there are any snapshot packages in the plan.
-errorOnSnapshot :: Plan -> RIO env Plan
-errorOnSnapshot plan@(Plan tasks _finals _unregister installExes) = do
-  let snapTasks = Map.keys $ Map.filter (\t -> taskLocation t == Snap) tasks
-  let snapExes = Map.keys $ Map.filter (== Snap) installExes
-  unless (null snapTasks && null snapExes) $ throwIO $
-    NotOnlyLocal snapTasks snapExes
-  pure plan
-
-data NotOnlyLocal
-  = NotOnlyLocal [PackageName] [Text]
-  deriving (Show, Typeable)
-
-instance Exception NotOnlyLocal where
-  displayException (NotOnlyLocal packages exes) = concat
-    [ "Error: [S-1727]\n"
-    , "Specified only-locals, but I need to build snapshot contents:\n"
-    , if null packages then "" else concat
-        [ "Packages: "
-        , L.intercalate ", " (map packageNameString packages)
-        , "\n"
-        ]
-    , if null exes then "" else concat
-        [ "Executables: "
-        , L.intercalate ", " (map T.unpack exes)
-        , "\n"
-        ]
-    ]
-
--- | State to be maintained during the calculation of local packages
--- to unregister.
-data UnregisterState = UnregisterState
-  { usToUnregister :: !(Map GhcPkgId (PackageIdentifier, Text))
-  , usKeep :: ![DumpPackage]
-  , usAnyAdded :: !Bool
-  }
-
--- | Determine which packages to unregister based on the given tasks and
--- already registered local packages.
-mkUnregisterLocal ::
-     Map PackageName Task
-     -- ^ Tasks
-  -> Map PackageName Text
-     -- ^ Reasons why packages are dirty and must be rebuilt
-  -> [DumpPackage]
-     -- ^ Local package database dump
-  -> Bool
-     -- ^ If true, we're doing a special initialBuildSteps build - don't
-     -- unregister target packages.
-  -> Map GhcPkgId (PackageIdentifier, Text)
-mkUnregisterLocal tasks dirtyReason localDumpPkgs initialBuildSteps =
-  -- We'll take multiple passes through the local packages. This will allow us
-  -- to detect that a package should be unregistered, as well as all packages
-  -- directly or transitively depending on it.
-  loop Map.empty localDumpPkgs
- where
-  loop ::
-       Map GhcPkgId (PackageIdentifier, Text)
-       -- ^ Current local packages to unregister.
-    -> [DumpPackage]
-       -- ^ Current local packages to keep.
-    -> Map GhcPkgId (PackageIdentifier, Text)
-       -- ^ Revised local packages to unregister.
-  loop toUnregister keep
-    -- If any new packages were added to the unregister Map, we need to loop
-    -- through the remaining packages again to detect if a transitive dependency
-    -- is being unregistered.
-    | usAnyAdded us = loop (usToUnregister us) (usKeep us)
-    -- Nothing added, so we've already caught them all. Return the Map we've
-    -- already calculated.
-    | otherwise = usToUnregister us
-   where
-    -- Run the unregister checking function on all packages we currently think
-    -- we'll be keeping.
-    us = execState (mapM_ go keep) initialUnregisterState
-    initialUnregisterState = UnregisterState
-      { usToUnregister = toUnregister
-      , usKeep = []
-      , usAnyAdded = False
-      }
-
-  go :: DumpPackage -> State UnregisterState ()
-  go dp = do
-    us <- get
-    case maybeUnregisterReason (usToUnregister us) ident mParentLibId deps of
-      -- Not unregistering, add it to the keep list.
-      Nothing -> put us { usKeep = dp : usKeep us }
-      -- Unregistering, add it to the unregister Map; and indicate that a
-      -- package was in fact added to the unregister Map, so we loop again.
-      Just reason -> put us
-        { usToUnregister = Map.insert gid (ident, reason) (usToUnregister us)
-        , usAnyAdded = True
-        }
-   where
-    gid = dpGhcPkgId dp
-    ident = dpPackageIdent dp
-    mParentLibId = dpParentLibIdent dp
-    deps = dpDepends dp
-
-  maybeUnregisterReason ::
-       Map GhcPkgId (PackageIdentifier, Text)
-       -- ^ Current local packages to unregister.
-    -> PackageIdentifier
-       -- ^ Package identifier.
-    -> Maybe PackageIdentifier
-       -- ^ If package for sub library, package identifier of the parent.
-    -> [GhcPkgId]
-       -- ^ Dependencies of the package.
-    -> Maybe Text
-       -- ^ If to be unregistered, the reason for doing so.
-  maybeUnregisterReason toUnregister ident mParentLibId deps
-    -- If the package is not for a sub library, then it is directly relevant. If
-    -- it is, then the relevant package is the parent. If we are planning on
-    -- running a task on the relevant package, then the package must be
-    -- unregistered, unless it is a target and an initial-build-steps build is
-    -- being done.
-    | Just task <- Map.lookup relevantPkgName tasks =
-        if    initialBuildSteps
-           && taskIsTarget task
-           && taskProvides task == relevantPkgId
-          then Nothing
-          else Just $ fromMaybe "" $ Map.lookup relevantPkgName dirtyReason
-    -- Check if a dependency is going to be unregistered
-    | (dep, _):_ <- mapMaybe (`Map.lookup` toUnregister) deps =
-        Just $ "Dependency being unregistered: "
-          <> T.pack (packageIdentifierString dep)
-    -- None of the above, keep it!
-    | otherwise = Nothing
-    where
-      -- If the package is not for a sub library, then the relevant package
-      -- identifier is that of the package. If it is, then the relevant package
-      -- identifier is that of the parent.
-      relevantPkgId :: PackageIdentifier
-      relevantPkgId = fromMaybe ident mParentLibId
-      -- If the package is not for a sub library, then the relevant package name
-      -- is that of the package. If it is, then the relevant package name is
-      -- that of the parent.
-      relevantPkgName :: PackageName
-      relevantPkgName = maybe (pkgName ident) pkgName mParentLibId
-
--- | Given a 'LocalPackage' and its 'lpTestBench', adds a 'Task' for running its
--- tests and benchmarks.
---
--- If @isAllInOne@ is 'True', then this means that the build step will also
--- build the tests. Otherwise, this indicates that there's a cyclic dependency
--- and an additional build step needs to be done.
---
--- This will also add all the deps needed to build the tests / benchmarks. If
--- @isAllInOne@ is 'True' (the common case), then all of these should have
--- already been taken care of as part of the build step.
-addFinal :: LocalPackage -> Package -> Bool -> Bool -> M ()
-addFinal lp package isAllInOne buildHaddocks = do
-  depsRes <- addPackageDeps package
-  res <- case depsRes of
-    Left e -> pure $ Left e
-    Right (missing, present, _minLoc) -> do
-      ctx <- ask
-      pure $ Right Task
-        { taskProvides = PackageIdentifier
-            (packageName package)
-            (packageVersion package)
-        , taskConfigOpts = TaskConfigOpts missing $ \missing' ->
-            let allDeps = Map.union present missing'
-            in  configureOpts
-                  (view envConfigL ctx)
-                  (baseConfigOpts ctx)
-                  allDeps
-                  True -- local
-                  Mutable
-                  package
-        , taskBuildHaddock = buildHaddocks
-        , taskPresent = present
-        , taskType = TTLocalMutable lp
-        , taskAllInOne = isAllInOne
-        , taskCachePkgSrc = CacheSrcLocal (toFilePath (parent (lpCabalFile lp)))
-        , taskAnyMissing = not $ Set.null missing
-        , taskBuildTypeConfig = packageBuildTypeConfig package
-        }
-  tell mempty { wFinals = Map.singleton (packageName package) res }
-
--- | Given a 'PackageName', adds all of the build tasks to build the package, if
--- needed. First checks if the package name is in the library map.
---
--- 'constructPlan' invokes this on all the target packages, setting
--- @treatAsDep'@ to False, because those packages are direct build targets.
--- 'addPackageDeps' invokes this while recursing into the dependencies of a
--- package. As such, it sets @treatAsDep'@ to True, forcing this package to be
--- marked as a dependency, even if it is directly wanted. This makes sense - if
--- we left out packages that are deps, it would break the --only-dependencies
--- build plan.
-getCachedDepOrAddDep ::
-     PackageName
-  -> M (Either ConstructPlanException AddDepRes)
-getCachedDepOrAddDep name = do
-  libMap <- get
-  case Map.lookup name libMap of
-    Just res -> do
-      logDebugPlanS "getCachedDepOrAddDep" $
-           "Using cached result for "
-        <> fromString (packageNameString name)
-        <> ": "
-        <> fromString (show res)
-      pure res
-    Nothing -> checkCallStackAndAddDep name
-
--- | Given a 'PackageName', known not to be in the library map, adds all of the
--- build tasks to build the package. First checks that the package name is not
--- already in the call stack.
-checkCallStackAndAddDep ::
-     PackageName
-  -> M (Either ConstructPlanException AddDepRes)
-checkCallStackAndAddDep name = do
-  ctx <- ask
-  res <- if name `elem` callStack ctx
-    then do
-      logDebugPlanS "checkCallStackAndAddDep" $
-           "Detected cycle "
-        <> fromString (packageNameString name)
-        <> ": "
-        <> fromString (show $ map packageNameString (callStack ctx))
-      pure $ Left $ DependencyCycleDetected $ name : callStack ctx
-    else case Map.lookup name $ combinedMap ctx of
-      -- TODO look up in the package index and see if there's a
-      -- recommendation available
-      Nothing -> do
-        logDebugPlanS "checkCallStackAndAddDep" $
-             "No package info for "
-          <> fromString (packageNameString name)
-          <> "."
-        pure $ Left $ UnknownPackage name
-      Just packageInfo ->
-        -- Add the current package name to the head of the call stack.
-        local (\ctx' -> ctx' { callStack = name : callStack ctx' }) $
-          addDep name packageInfo
-  updateLibMap name res
-  pure res
-
--- | Given a 'PackageName' and its 'PackageInfo' from the combined map, adds all
--- of the build tasks to build the package. Assumes that the head of the call
--- stack is the current package name.
-addDep ::
-     PackageName
-  -> PackageInfo
-  -> M (Either ConstructPlanException AddDepRes)
-addDep name packageInfo = do
-  logDebugPlanS "addDep" $
-       "Package info for "
-    <> fromString (packageNameString name)
-    <> ": "
-    <> fromString (show packageInfo)
-  case packageInfo of
-    PIOnlyInstalled loc installed -> do
-      -- FIXME Slightly hacky, no flags since they likely won't affect
-      -- executable names. This code does not feel right.
-      let version = installedVersion installed
-          askPkgLoc = liftRIO $ do
-            mrev <- getLatestHackageRevision YesRequireHackageIndex name version
-            case mrev of
-              Nothing -> do
-                -- This could happen for GHC boot libraries missing from
-                -- Hackage.
-                cs <- asks (L.tail . callStack)
-                prettyWarnL
-                  $ flow "No latest package revision found for"
-                  : style Current (fromString $ packageNameString name) <> ","
-                  : flow "dependency callstack:"
-                  : mkNarrativeList Nothing False
-                      (map (fromString . packageNameString) cs :: [StyleDoc])
-                pure Nothing
-              Just (_rev, cfKey, treeKey) ->
-                pure $ Just $
-                  PLIHackage (PackageIdentifier name version) cfKey treeKey
-      tellExecutablesUpstream name askPkgLoc loc Map.empty
-      pure $ Right $ ADRFound loc installed
-    PIOnlySource ps -> do
-      tellExecutables name ps
-      installPackage name ps Nothing
-    PIBoth ps installed -> do
-      tellExecutables name ps
-      installPackage name ps (Just installed)
-
--- FIXME what's the purpose of this? Add a Haddock!
-tellExecutables :: PackageName -> PackageSource -> M ()
-tellExecutables _name (PSFilePath lp)
-  | lpWanted lp = tellExecutablesPackage Local $ lpPackage lp
-  | otherwise = pure ()
--- Ignores ghcOptions because they don't matter for enumerating executables.
-tellExecutables name (PSRemote pkgloc _version _fromSnapshot cp) =
-  tellExecutablesUpstream name (pure $ Just pkgloc) Snap (cpFlags cp)
-
-tellExecutablesUpstream ::
-     PackageName
-  -> M (Maybe PackageLocationImmutable)
-  -> InstallLocation
-  -> Map FlagName Bool
-  -> M ()
-tellExecutablesUpstream name retrievePkgLoc loc flags = do
-  ctx <- ask
-  when (name `Set.member` wanted ctx) $ do
-    mPkgLoc <- retrievePkgLoc
-    forM_ mPkgLoc $ \pkgLoc -> do
-      p <- loadPackage ctx pkgLoc flags [] []
-      tellExecutablesPackage loc p
-
-tellExecutablesPackage :: InstallLocation -> Package -> M ()
-tellExecutablesPackage loc p = do
-  cm <- asks combinedMap
-  -- Determine which components are enabled so we know which ones to copy
-  let myComps =
-        case Map.lookup (packageName p) cm of
-          Nothing -> assert False Set.empty
-          Just (PIOnlyInstalled _ _) -> Set.empty
-          Just (PIOnlySource ps) -> goSource ps
-          Just (PIBoth ps _) -> goSource ps
-
-      goSource (PSFilePath lp)
-        | lpWanted lp = exeComponents (lpComponents lp)
-        | otherwise = Set.empty
-      goSource PSRemote{} = Set.empty
-
-  tell mempty
-    { wInstall = Map.fromList $
-        map (, loc) $ Set.toList $ filterComps myComps $ packageExes p
-    }
- where
-  filterComps myComps x
-    | Set.null myComps = x
-    | otherwise = Set.intersection x myComps
-
--- | Given a 'PackageSource' and perhaps an 'Installed' value, adds build
--- 'Task's for the package and its dependencies.
-installPackage :: PackageName
-               -> PackageSource
-               -> Maybe Installed
-               -> M (Either ConstructPlanException AddDepRes)
-installPackage name ps minstalled = do
-  ctx <- ask
-  case ps of
-    PSRemote pkgLoc _version _fromSnapshot cp -> do
-      logDebugPlanS "installPackage" $
-           "Doing all-in-one build for upstream package "
-        <> fromString (packageNameString name)
-        <> "."
-      package <- loadPackage
-        ctx pkgLoc (cpFlags cp) (cpGhcOptions cp) (cpCabalConfigOpts cp)
-      resolveDepsAndInstall True (cpHaddocks cp) ps package minstalled
-    PSFilePath lp -> do
-      case lpTestBench lp of
-        Nothing -> do
-          logDebugPlanS "installPackage" $
-               "No test or bench component for "
-            <> fromString (packageNameString name)
-            <> " so doing an all-in-one build."
-          resolveDepsAndInstall
-            True (lpBuildHaddocks lp) ps (lpPackage lp) minstalled
-        Just tb -> do
-          -- Attempt to find a plan which performs an all-in-one build. Ignore
-          -- the writer action + reset the state if it fails.
-          libMap <- get
-          res <- pass $ do
-            res <- addPackageDeps tb
-            let writerFunc w = case res of
-                  Left _ -> mempty
-                  _ -> w
-            pure (res, writerFunc)
-          case res of
-            Right deps -> do
-              logDebugPlanS "installPackage" $
-                   "For "
-                <> fromString (packageNameString name)
-                <> ", successfully added package deps."
-              -- in curator builds we can't do all-in-one build as
-              -- test/benchmark failure could prevent library from being
-              -- available to its dependencies but when it's already available
-              -- it's OK to do that
-              splitRequired <- expectedTestOrBenchFailures <$> asks mcurator
-              let isAllInOne = not splitRequired
-              adr <- installPackageGivenDeps
-                isAllInOne (lpBuildHaddocks lp) ps tb minstalled deps
-              let finalAllInOne = case adr of
-                    ADRToInstall _ | splitRequired -> False
-                    _ -> True
-              -- FIXME: this redundantly adds the deps (but they'll all just
-              -- get looked up in the map)
-              addFinal lp tb finalAllInOne False
-              pure $ Right adr
-            Left _ -> do
-              -- Reset the state to how it was before attempting to find an
-              -- all-in-one build plan.
-              logDebugPlanS "installPackage" $
-                   "Before trying cyclic plan, resetting lib result map to: "
-                <> fromString (show libMap)
-              put libMap
-              -- Otherwise, fall back on building the tests / benchmarks in a
-              -- separate step.
-              res' <- resolveDepsAndInstall
-                False (lpBuildHaddocks lp) ps (lpPackage lp) minstalled
-              when (isRight res') $ do
-                -- Insert it into the map so that it's available for addFinal.
-                updateLibMap name res'
-                addFinal lp tb False False
-              pure res'
- where
-  expectedTestOrBenchFailures maybeCurator = fromMaybe False $ do
-    curator <- maybeCurator
-    pure $ Set.member name (curatorExpectTestFailure curator) ||
-           Set.member name (curatorExpectBenchmarkFailure curator)
-
-resolveDepsAndInstall :: Bool
-                      -> Bool
-                      -> PackageSource
-                      -> Package
-                      -> Maybe Installed
-                      -> M (Either ConstructPlanException AddDepRes)
-resolveDepsAndInstall isAllInOne buildHaddocks ps package minstalled = do
-  res <- addPackageDeps package
-  case res of
-    Left err -> pure $ Left err
-    Right deps ->
-      Right <$>
-        installPackageGivenDeps
-          isAllInOne buildHaddocks ps package minstalled deps
-
--- | Checks if we need to install the given 'Package', given the results
--- of 'addPackageDeps'. If dependencies are missing, the package is dirty, or
--- it's not installed, then it needs to be installed.
-installPackageGivenDeps :: Bool
-                        -> Bool
-                        -> PackageSource
-                        -> Package
-                        -> Maybe Installed
-                        -> ( Set PackageIdentifier
-                           , Map PackageIdentifier GhcPkgId
-                           , IsMutable )
-                        -> M AddDepRes
-installPackageGivenDeps isAllInOne buildHaddocks ps package minstalled
-  (missing, present, minMutable) = do
-    let name = packageName package
-    ctx <- ask
-    mRightVersionInstalled <- case (minstalled, Set.null missing) of
-      (Just installed, True) -> do
-        shouldInstall <-
-          checkDirtiness ps installed package present buildHaddocks
-        pure $ if shouldInstall then Nothing else Just installed
-      (Just _, False) -> do
-        let t = T.intercalate ", " $
-                  map (T.pack . packageNameString . pkgName) (Set.toList missing)
-        tell mempty
-          { wDirty =
-              Map.singleton name $ "missing dependencies: " <> addEllipsis t
-          }
-        pure Nothing
-      (Nothing, _) -> pure Nothing
-    let loc = psLocation ps
-        mutable = installLocationIsMutable loc <> minMutable
-    pure $ case mRightVersionInstalled of
-      Just installed -> ADRFound loc installed
-      Nothing -> ADRToInstall Task
-        { taskProvides = PackageIdentifier
-            (packageName package)
-            (packageVersion package)
-        , taskConfigOpts = TaskConfigOpts missing $ \missing' ->
-            let allDeps = Map.union present missing'
-            in  configureOpts
-                  (view envConfigL ctx)
-                  (baseConfigOpts ctx)
-                  allDeps
-                  (psLocal ps)
-                  mutable
-                  package
-        , taskBuildHaddock = buildHaddocks
-        , taskPresent = present
-        , taskType =
-            case ps of
-              PSFilePath lp ->
-                TTLocalMutable lp
-              PSRemote pkgLoc _version _fromSnapshot _cp ->
-                TTRemotePackage mutable package pkgLoc
-        , taskAllInOne = isAllInOne
-        , taskCachePkgSrc = toCachePkgSrc ps
-        , taskAnyMissing = not $ Set.null missing
-        , taskBuildTypeConfig = packageBuildTypeConfig package
-        }
-
--- | Is the build type of the package Configure
-packageBuildTypeConfig :: Package -> Bool
-packageBuildTypeConfig pkg = packageBuildType pkg == Configure
-
--- Update response in the library map. If it is an error, and there's already an
--- error about cyclic dependencies, prefer the cyclic error.
-updateLibMap :: PackageName -> Either ConstructPlanException AddDepRes -> M ()
-updateLibMap name val = modify $ \mp ->
-  case (Map.lookup name mp, val) of
-    (Just (Left DependencyCycleDetected{}), Left _) -> mp
-    _ -> Map.insert name val mp
-
-addEllipsis :: Text -> Text
-addEllipsis t
-  | T.length t < 100 = t
-  | otherwise = T.take 97 t <> "..."
-
--- | Given a package, recurses into all of its dependencies. The results
--- indicate which packages are missing, meaning that their 'GhcPkgId's will be
--- figured out during the build, after they've been built. The 2nd part of the
--- tuple result indicates the packages that are already installed which will be
--- used.
---
--- The 3rd part of the tuple is an 'InstallLocation'. If it is 'Local', then the
--- parent package must be installed locally. Otherwise, if it is 'Snap', then it
--- can either be installed locally or in the snapshot.
-addPackageDeps ::
-     Package
-  -> M ( Either
-           ConstructPlanException
-           ( Set PackageIdentifier
-           , Map PackageIdentifier GhcPkgId
-           , IsMutable
-           )
-       )
-addPackageDeps package = do
-  ctx <- ask
-  checkAndWarnForUnknownTools package
-  let deps' = packageDeps package
-  deps <- forM (Map.toList deps') $ \(depname, DepValue range depType) -> do
-    eres <- getCachedDepOrAddDep depname
-    let getLatestApplicableVersionAndRev :: M (Maybe (Version, BlobKey))
-        getLatestApplicableVersionAndRev = do
-          vsAndRevs <-
-            runRIO ctx $
-              getHackagePackageVersions
-                YesRequireHackageIndex UsePreferredVersions depname
-          pure $ do
-            lappVer <- latestApplicableVersion range $ Map.keysSet vsAndRevs
-            revs <- Map.lookup lappVer vsAndRevs
-            (cabalHash, _) <- Map.maxView revs
-            Just (lappVer, cabalHash)
-    case eres of
-      Left e -> do
-        addParent depname range Nothing
-        let bd = case e of
-              UnknownPackage name -> assert (name == depname) NotInBuildPlan
-              DependencyCycleDetected names -> BDDependencyCycleDetected names
-              -- ultimately we won't show any information on this to the user,
-              -- we'll allow the dependency failures alone to display to avoid
-              -- spamming the user too much
-              DependencyPlanFailures _ _  ->
-                Couldn'tResolveItsDependencies (packageVersion package)
-        mlatestApplicable <- getLatestApplicableVersionAndRev
-        pure $ Left (depname, (range, mlatestApplicable, bd))
-      Right adr | depType == AsLibrary && not (adrHasLibrary adr) ->
-        pure $ Left (depname, (range, Nothing, HasNoLibrary))
-      Right adr -> do
-        addParent depname range Nothing
-        inRange <- if adrVersion adr `withinRange` range
-          then pure True
-          else do
-            let warn_ isIgnoring reason = tell mempty { wWarnings = (msg:) }
-                 where
-                  msg =
-                       fillSep
-                         [ if isIgnoring then "Ignoring" else flow "Not ignoring"
-                         , style Current (fromString . packageNameString $ packageName package) <> "'s"
-                         , flow "bounds on"
-                         , style Current (fromString $ packageNameString depname)
-                         , parens (fromString . T.unpack $ versionRangeText range)
-                         , flow "and using"
-                         , style Current (fromString . packageIdentifierString $
-                             PackageIdentifier depname (adrVersion adr)) <> "."
-                         ]
-                    <> line
-                    <> fillSep
-                         [ "Reason:"
-                         , reason <> "."
-                         ]
-            allowNewer <- view $ configL.to configAllowNewer
-            allowNewerDeps <- view $ configL.to configAllowNewerDeps
-            let inSnapshotCheck = do
-                  -- We ignore dependency information for packages in a snapshot
-                  x <- inSnapshot (packageName package) (packageVersion package)
-                  y <- inSnapshot depname (adrVersion adr)
-                  if x && y
-                    then do
-                      warn_ True
-                        ( flow "trusting snapshot over Cabal file dependency \
-                               \information"
-                        )
-                      pure True
-                    else pure False
-            if allowNewer
-              then case allowNewerDeps of
-                Nothing -> do
-                  warn_ True $
-                    fillSep
-                      [ style Shell "allow-newer"
-                      , "enabled"
-                      ]
-                  pure True
-                Just boundsIgnoredDeps -> do
-                  let pkgName = packageName package
-                      pkgName' = fromString $ packageNameString pkgName
-                      isBoundsIgnoreDep = pkgName `elem` boundsIgnoredDeps
-                      reason = if isBoundsIgnoreDep
-                        then fillSep
-                          [ style Current pkgName'
-                          , flow "is an"
-                          , style Shell "allow-newer-dep"
-                          , flow "and"
-                          , style Shell "allow-newer"
-                          , "enabled"
-                          ]
-                        else fillSep
-                          [ style Current pkgName'
-                          , flow "is not an"
-                          , style Shell "allow-newer-dep"
-                          , flow "although"
-                          , style Shell "allow-newer"
-                          , "enabled"
-                          ]
-                  warn_ isBoundsIgnoreDep reason
-                  pure isBoundsIgnoreDep
-              else do
-                when (isJust allowNewerDeps) $
-                  warn_ False $
-                    fillSep
-                      [ "although"
-                      , style Shell "allow-newer-deps"
-                      , flow "are specified,"
-                      , style Shell "allow-newer"
-                      , "is"
-                      , style Shell "false"
-                      ]
-                inSnapshotCheck
-        if inRange
-          then case adr of
-            ADRToInstall task -> pure $ Right
-              ( Set.singleton $ taskProvides task
-              , Map.empty, taskTargetIsMutable task
-              )
-            ADRFound loc (Executable _) -> pure $ Right
-              ( Set.empty, Map.empty
-              , installLocationIsMutable loc
-              )
-            ADRFound loc (Library ident gid _) -> pure $ Right
-              ( Set.empty, Map.singleton ident gid
-              , installLocationIsMutable loc
-              )
-          else do
-            mlatestApplicable <- getLatestApplicableVersionAndRev
-            pure $ Left
-              ( depname
-              , ( range
-                , mlatestApplicable
-                , DependencyMismatch $ adrVersion adr
-                )
-              )
-  case partitionEithers deps of
-    -- Note that the Monoid for 'InstallLocation' means that if any
-    -- is 'Local', the result is 'Local', indicating that the parent
-    -- package must be installed locally. Otherwise the result is
-    -- 'Snap', indicating that the parent can either be installed
-    -- locally or in the snapshot.
-    ([], pairs) -> pure $ Right $ mconcat pairs
-    (errs, _) -> pure $ Left $ DependencyPlanFailures
-      package
-      (Map.fromList errs)
- where
-  adrVersion (ADRToInstall task) = pkgVersion $ taskProvides task
-  adrVersion (ADRFound _ installed) = installedVersion installed
-  -- Update the parents map, for later use in plan construction errors
-  -- - see 'getShortestDepsPath'.
-  addParent depname range mversion =
-    tell mempty { wParents = MonoidMap $ Map.singleton depname val }
-   where
-    val = (First mversion, [(packageIdentifier package, range)])
-
-  adrHasLibrary :: AddDepRes -> Bool
-  adrHasLibrary (ADRToInstall task) = taskHasLibrary task
-  adrHasLibrary (ADRFound _ Library{}) = True
-  adrHasLibrary (ADRFound _ Executable{}) = False
-
-  taskHasLibrary :: Task -> Bool
-  taskHasLibrary task =
-    case taskType task of
-      TTLocalMutable lp -> packageHasLibrary $ lpPackage lp
-      TTRemotePackage _ p _ -> packageHasLibrary p
-
-  -- make sure we consider internal libraries as libraries too
-  packageHasLibrary :: Package -> Bool
-  packageHasLibrary p =
-    not (Set.null (packageInternalLibraries p)) ||
-    case packageLibraries p of
-      HasLibraries _ -> True
-      NoLibraries -> False
-
-checkDirtiness :: PackageSource
-               -> Installed
-               -> Package
-               -> Map PackageIdentifier GhcPkgId
-               -> Bool
-               -> M Bool
-checkDirtiness ps installed package present buildHaddocks = do
-  ctx <- ask
-  moldOpts <- runRIO ctx $ tryGetFlagCache installed
-  let configOpts = configureOpts
-        (view envConfigL ctx)
-        (baseConfigOpts ctx)
-        present
-        (psLocal ps)
-        (installLocationIsMutable $ psLocation ps) -- should be Local i.e. mutable always
-        package
-      wantConfigCache = ConfigCache
-        { configCacheOpts = configOpts
-        , configCacheDeps = Set.fromList $ Map.elems present
-        , configCacheComponents =
-            case ps of
-              PSFilePath lp ->
-                Set.map (encodeUtf8 . renderComponent) $ lpComponents lp
-              PSRemote{} -> Set.empty
-        , configCacheHaddock = buildHaddocks
-        , configCachePkgSrc = toCachePkgSrc ps
-        , configCachePathEnvVar = pathEnvVar ctx
-        }
-      config = view configL ctx
-  mreason <-
-    case moldOpts of
-      Nothing -> pure $ Just "old configure information not found"
-      Just oldOpts
-        | Just reason <- describeConfigDiff config oldOpts wantConfigCache ->
-            pure $ Just reason
-        | True <- psForceDirty ps -> pure $ Just "--force-dirty specified"
-        | otherwise -> do
-            dirty <- psDirty ps
-            pure $
-              case dirty of
-                Just files -> Just $
-                     "local file changes: "
-                  <> addEllipsis (T.pack $ unwords $ Set.toList files)
-                Nothing -> Nothing
-  case mreason of
-    Nothing -> pure False
-    Just reason -> do
-      tell mempty { wDirty = Map.singleton (packageName package) reason }
-      pure True
-
-describeConfigDiff :: Config -> ConfigCache -> ConfigCache -> Maybe Text
-describeConfigDiff config old new
-  | configCachePkgSrc old /= configCachePkgSrc new = Just $
-      "switching from " <>
-      pkgSrcName (configCachePkgSrc old) <> " to " <>
-      pkgSrcName (configCachePkgSrc new)
-  | not (configCacheDeps new `Set.isSubsetOf` configCacheDeps old) =
-      Just "dependencies changed"
-  | not $ Set.null newComponents =
-      Just $ "components added: " `T.append` T.intercalate ", "
-          (map (decodeUtf8With lenientDecode) (Set.toList newComponents))
-  | not (configCacheHaddock old) && configCacheHaddock new =
-      Just "rebuilding with haddocks"
-  | oldOpts /= newOpts = Just $ T.pack $ concat
-      [ "flags changed from "
-      , show oldOpts
-      , " to "
-      , show newOpts
-      ]
-  | otherwise = Nothing
- where
-  stripGhcOptions = go
-   where
-    go [] = []
-    go ("--ghc-option":x:xs) = go' Ghc x xs
-    go ("--ghc-options":x:xs) = go' Ghc x xs
-    go ((T.stripPrefix "--ghc-option=" -> Just x):xs) = go' Ghc x xs
-    go ((T.stripPrefix "--ghc-options=" -> Just x):xs) = go' Ghc x xs
-    go (x:xs) = x : go xs
-
-    go' wc x xs = checkKeepers wc x $ go xs
-
-    checkKeepers wc x xs =
-      case filter isKeeper $ T.words x of
-        [] -> xs
-        keepers -> T.pack (compilerOptionsCabalFlag wc) : T.unwords keepers : xs
-
-    -- GHC options which affect build results and therefore should always force
-    -- a rebuild
-    --
-    -- For the most part, we only care about options generated by Stack itself
-    isKeeper = (== "-fhpc") -- more to be added later
-
-  userOpts = filter (not . isStackOpt)
-           . (if configRebuildGhcOptions config
-                then id
-                else stripGhcOptions)
-           . map T.pack
-           . (\(ConfigureOpts x y) -> x ++ y)
-           . configCacheOpts
-   where
-    -- options set by Stack
-    isStackOpt :: Text -> Bool
-    isStackOpt t = any (`T.isPrefixOf` t)
-      [ "--dependency="
-      , "--constraint="
-      , "--package-db="
-      , "--libdir="
-      , "--bindir="
-      , "--datadir="
-      , "--libexecdir="
-      , "--sysconfdir"
-      , "--docdir="
-      , "--htmldir="
-      , "--haddockdir="
-      , "--enable-tests"
-      , "--enable-benchmarks"
-      , "--exact-configuration"
-      -- Treat these as causing dirtiness, to resolve
-      -- https://github.com/commercialhaskell/stack/issues/2984
-      --
-      -- , "--enable-library-profiling"
-      -- , "--enable-executable-profiling"
-      -- , "--enable-profiling"
-      ] || t == "--user"
-
-  (oldOpts, newOpts) = removeMatching (userOpts old) (userOpts new)
-
-  removeMatching (x:xs) (y:ys)
-    | x == y = removeMatching xs ys
-  removeMatching xs ys = (xs, ys)
-
-  newComponents =
-    configCacheComponents new `Set.difference` configCacheComponents old
-
-  pkgSrcName (CacheSrcLocal fp) = T.pack fp
-  pkgSrcName CacheSrcUpstream = "upstream source"
-
-psForceDirty :: PackageSource -> Bool
-psForceDirty (PSFilePath lp) = lpForceDirty lp
-psForceDirty PSRemote{} = False
-
-psDirty ::
-     (MonadIO m, HasEnvConfig env, MonadReader env m)
-  => PackageSource
-  -> m (Maybe (Set FilePath))
-psDirty (PSFilePath lp) = runMemoizedWith $ lpDirtyFiles lp
-psDirty PSRemote {} = pure Nothing -- files never change in a remote package
-
-psLocal :: PackageSource -> Bool
-psLocal (PSFilePath _ ) = True
-psLocal PSRemote{} = False
-
-psLocation :: PackageSource -> InstallLocation
-psLocation (PSFilePath _) = Local
-psLocation PSRemote{} = Snap
-
--- | Get all of the dependencies for a given package, including build
--- tool dependencies.
-checkAndWarnForUnknownTools :: Package -> M ()
-checkAndWarnForUnknownTools p = do
-  let unknownTools = Set.toList $ packageUnknownTools p
-  -- Check whether the tool is on the PATH or a package executable before
-  -- warning about it.
-  warnings <-
-    fmap catMaybes $ forM unknownTools $ \name@(ExeName toolName) ->
-      runMaybeT $ notOnPath toolName *> notPackageExe toolName *> warn name
-  tell mempty { wWarnings = (map toolWarningText warnings ++) }
-  pure ()
- where
-  -- From Cabal 2.0, build-tools can specify a pre-built executable that should
-  -- already be on the PATH.
-  notOnPath toolName = MaybeT $ do
-    let settings = minimalEnvSettings { esIncludeLocals = True }
-    config <- view configL
-    menv <- liftIO $ configProcessContextSettings config settings
-    eFound <- runRIO menv $ findExecutable $ T.unpack toolName
-    skipIf $ isRight eFound
-  -- From Cabal 1.12, build-tools can specify another executable in the same
-  -- package.
-  notPackageExe toolName = MaybeT $ skipIf $ toolName `Set.member` packageExes p
-  warn name = MaybeT . pure . Just $ ToolWarning name (packageName p)
-  skipIf p' = pure $ if p' then Nothing else Just ()
-
--- | Warn about tools in the snapshot definition. States the tool name
--- expected and the package name using it.
-data ToolWarning
-  = ToolWarning ExeName PackageName
-  deriving Show
-
-toolWarningText :: ToolWarning -> StyleDoc
-toolWarningText (ToolWarning (ExeName toolName) pkgName') = fillSep
-  [ flow "No packages found in snapshot which provide a"
-  , style PkgComponent (fromString $ show toolName)
-  , flow "executable, which is a build-tool dependency of"
-  , style Current (fromString $ packageNameString pkgName')
-  ]
-
--- | Strip out anything from the @Plan@ intended for the local database
-stripLocals :: Plan -> Plan
-stripLocals plan = plan
-  { planTasks = Map.filter checkTask $ planTasks plan
-  , planFinals = Map.empty
-  , planUnregisterLocal = Map.empty
-  , planInstallExes = Map.filter (/= Local) $ planInstallExes plan
-  }
- where
-  checkTask task = taskLocation task == Snap
-
-stripNonDeps :: Set PackageName -> Plan -> Plan
-stripNonDeps deps plan = plan
-  { planTasks = Map.filter checkTask $ planTasks plan
-  , planFinals = Map.empty
-  , planInstallExes = Map.empty -- TODO maybe don't disable this?
-  }
- where
-  checkTask task = taskProvides task `Set.member` missingForDeps
-  providesDep task = pkgName (taskProvides task) `Set.member` deps
-  missing = Map.fromList $ map (taskProvides &&& tcoMissing . taskConfigOpts) $
-            Map.elems (planTasks plan)
-  missingForDeps = flip execState mempty $
-    for_ (Map.elems $ planTasks plan) $ \task ->
-      when (providesDep task) $
-        collectMissing mempty (taskProvides task)
-
-  collectMissing dependents pid = do
-    when (pid `elem` dependents) $
-      impureThrow $ TaskCycleBug pid
-    modify' (<> Set.singleton pid)
-    mapM_ (collectMissing (pid:dependents)) (fromMaybe mempty $ Map.lookup pid missing)
-
--- | Is the given package/version combo defined in the snapshot or in the global
--- database?
-inSnapshot :: PackageName -> Version -> M Bool
-inSnapshot name version = do
-  ctx <- ask
-  pure $ fromMaybe False $ do
-    ps <- Map.lookup name (combinedMap ctx)
-    case ps of
-      PIOnlySource (PSRemote _ srcVersion FromSnapshot _) ->
-        pure $ srcVersion == version
-      PIBoth (PSRemote _ srcVersion FromSnapshot _) _ ->
-        pure $ srcVersion == version
-      -- OnlyInstalled occurs for global database
-      PIOnlyInstalled loc (Library pid _gid _lic) ->
-        assert (loc == Snap) $
-        assert (pkgVersion pid == version) $
-        Just True
-      _ -> pure False
-
--- TODO: Consider intersecting version ranges for multiple deps on a
--- package.  This is why VersionRange is in the parent map.
-
-logDebugPlanS ::
-     (HasCallStack, HasRunner env, MonadIO m, MonadReader env m)
-  => LogSource
-  -> Utf8Builder
-  -> m ()
-logDebugPlanS s msg = do
-  debugPlan <- view $ globalOptsL.to globalPlanInLog
-  when debugPlan $ logDebugS s msg
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE LambdaCase            #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE ViewPatterns          #-}
+
+-- | Construct a @Plan@ for how to build
+module Stack.Build.ConstructPlan
+  ( constructPlan
+  ) where
+
+import           Control.Monad.Trans.Maybe ( MaybeT (..) )
+import qualified Data.Map.Merge.Strict as Map
+import qualified Data.Map.Strict as Map
+import           Data.Monoid.Map ( MonoidMap(..) )
+import qualified Data.Set as Set
+import qualified Data.Text as T
+import           Distribution.Types.BuildType ( BuildType (Configure) )
+import           Distribution.Types.PackageName ( mkPackageName )
+import           Path ( parent )
+import qualified RIO.NonEmpty as NE
+import           RIO.Process ( findExecutable )
+import           RIO.State
+                   ( State, StateT (..), execState, get, modify, modify', put )
+import           RIO.Writer ( WriterT (..), pass, tell )
+import           Stack.Build.Cache ( tryGetFlagCache )
+import           Stack.Build.Haddock ( shouldHaddockDeps )
+import           Stack.Build.Source ( loadLocalPackage )
+import           Stack.Constants ( compilerOptionsCabalFlag )
+import           Stack.Package
+                   ( applyForceCustomBuild, buildableExes, packageUnknownTools
+                   , processPackageDepsToList
+                   )
+import           Stack.Prelude hiding ( loadPackage )
+import           Stack.SourceMap ( getPLIVersion, mkProjectPackage )
+import           Stack.Types.Build
+                   ( CachePkgSrc (..), ConfigCache (..), Plan (..), Task (..)
+                   , TaskConfigOpts (..), TaskType (..)
+                   , installLocationIsMutable, taskIsTarget, taskLocation
+                   , taskProvides, taskTargetIsMutable, toCachePkgSrc
+                   )
+import           Stack.Types.Build.ConstructPlan
+                   ( AddDepRes (..), CombinedMap, Ctx (..), M, PackageInfo (..)
+                   , ToolWarning(..), UnregisterState (..), W (..)
+                   , adrHasLibrary, adrVersion, toTask
+                   )
+import           Stack.Types.Build.Exception
+                   ( BadDependency (..), BuildException (..)
+                   , BuildPrettyException (..), ConstructPlanException (..)
+                   )
+import           Stack.Types.BuildConfig
+                   ( BuildConfig (..), HasBuildConfig (..), stackYamlL )
+import           Stack.Types.BuildOpts ( BuildOpts (..) )
+import           Stack.Types.BuildOptsCLI
+                   ( BuildOptsCLI (..), BuildSubset (..) )
+import           Stack.Types.CompCollection ( collectionMember )
+import           Stack.Types.Compiler ( WhichCompiler (..) )
+import           Stack.Types.CompilerPaths
+                   ( CompilerPaths (..), HasCompiler (..) )
+import           Stack.Types.Config ( Config (..), HasConfig (..), stackRootL )
+import           Stack.Types.ConfigureOpts
+                   ( BaseConfigOpts (..), ConfigureOpts (..) )
+import qualified Stack.Types.ConfigureOpts as ConfigureOpts
+import           Stack.Types.Curator ( Curator (..) )
+import           Stack.Types.Dependency ( DepValue (..), isDepTypeLibrary )
+import           Stack.Types.DumpPackage ( DumpPackage (..), dpParentLibIdent )
+import           Stack.Types.EnvConfig ( EnvConfig (..), HasEnvConfig (..) )
+import           Stack.Types.EnvSettings
+                   ( EnvSettings (..), minimalEnvSettings )
+import           Stack.Types.GhcPkgId ( GhcPkgId )
+import           Stack.Types.GlobalOpts ( GlobalOpts (..) )
+import           Stack.Types.Installed
+                   ( InstallLocation (..), Installed (..), InstalledMap
+                   , installedVersion
+                   )
+import           Stack.Types.IsMutable ( IsMutable (..) )
+import           Stack.Types.NamedComponent ( exeComponents, renderComponent )
+import           Stack.Types.Package
+                   ( ExeName (..), LocalPackage (..), Package (..)
+                   , PackageSource (..), installedMapGhcPkgId
+                   , packageIdentifier, psVersion, runMemoizedWith
+                   )
+import           Stack.Types.ProjectConfig ( isPCGlobalProject )
+import           Stack.Types.Runner ( HasRunner (..), globalOptsL )
+import           Stack.Types.SourceMap
+                   ( CommonPackage (..), DepPackage (..), FromSnapshot (..)
+                   , GlobalPackage (..), SMTargets (..), SourceMap (..)
+                   )
+import           Stack.Types.Version
+                   ( VersionRange, latestApplicableVersion, versionRangeText
+                   , withinRange
+                   )
+import           System.Environment ( lookupEnv )
+
+-- | Computes a build plan. This means figuring out which build 'Task's to take,
+-- and the interdependencies among the build 'Task's. In particular:
+--
+-- 1) It determines which packages need to be built, based on the transitive
+-- deps of the current targets. For local packages, this is indicated by the
+-- 'lpWanted' boolean. For extra packages to build, this comes from the
+-- @extraToBuild0@ argument of type @Set PackageName@. These are usually
+-- packages that have been specified on the command line.
+--
+-- 2) It will only rebuild an upstream package if it isn't present in the
+-- 'InstalledMap', or if some of its dependencies have changed.
+--
+-- 3) It will only rebuild a local package if its files are dirty or some of its
+-- dependencies have changed.
+constructPlan ::
+     forall env. HasEnvConfig env
+  => BaseConfigOpts
+  -> [DumpPackage] -- ^ locally registered
+  -> (  PackageLocationImmutable
+     -> Map FlagName Bool
+     -> [Text]
+        -- ^ GHC options
+     -> [Text]
+        -- ^ Cabal configure options
+     -> RIO EnvConfig Package
+     )
+     -- ^ load upstream package
+  -> SourceMap
+  -> InstalledMap
+  -> Bool
+     -- ^ Only include initial build steps required for GHCi?
+  -> RIO env Plan
+constructPlan
+    baseConfigOpts0
+    localDumpPkgs
+    loadPackage0
+    sourceMap
+    installedMap
+    initialBuildSteps
+  = do
+    logDebug "Constructing the build plan"
+
+    when hasBaseInDeps $
+      prettyWarn $
+           fillSep
+             [ flow "You are trying to upgrade or downgrade the"
+             , style Current "base"
+             , flow "package, which is almost certainly not what you really \
+                    \want. Please, consider using another GHC version if you \
+                    \need a certain version of"
+             , style Current "base" <> ","
+             , flow "or removing"
+             , style Current "base"
+             , flow "as an"
+             , style Shell "extra-deps" <> "."
+             , flow "For further information, see"
+             , style Url "https://github.com/commercialhaskell/stack/issues/3940" <> "."
+             ]
+        <> line
+
+    econfig <- view envConfigL
+    globalCabalVersion <- view $ compilerPathsL . to (.cabalVersion)
+    sources <- getSources globalCabalVersion
+    curator <- view $ buildConfigL . to (.curator)
+    pathEnvVar <- liftIO $ maybe mempty T.pack <$> lookupEnv "PATH"
+    let ctx = mkCtx econfig globalCabalVersion sources curator pathEnvVar
+        targetPackageNames = Map.keys sourceMap.targets.targets
+        -- Ignore the result of 'getCachedDepOrAddDep'.
+        onTarget = void . getCachedDepOrAddDep
+        inner = mapM_ onTarget targetPackageNames
+    (((), W efinals installExes dirtyReason warnings parents), m) <-
+      liftIO $ runRIO ctx (runStateT (runWriterT inner) Map.empty)
+    -- Report any warnings
+    mapM_ prettyWarn (warnings [])
+    -- Separate out errors
+    let (errlibs, adrs) = partitionEithers $ map toEither $ Map.toList m
+        (errfinals, finals) =
+          partitionEithers $ map toEither $ Map.toList efinals
+        errs = errlibs ++ errfinals
+    if null errs
+      then do
+        let tasks = Map.fromList $ mapMaybe (toMaybe . second toTask) adrs
+        takeSubset Plan
+          { tasks = tasks
+          , finals = Map.fromList finals
+          , unregisterLocal =
+              mkUnregisterLocal tasks dirtyReason localDumpPkgs initialBuildSteps
+          , installExes =
+              if    baseConfigOpts0.buildOpts.installExes
+                 || baseConfigOpts0.buildOpts.installCompilerTool
+                then installExes
+                else Map.empty
+          }
+      else do
+        stackYaml <- view stackYamlL
+        stackRoot <- view stackRootL
+        isImplicitGlobal <-
+          view $ configL . to (isPCGlobalProject . (.project))
+        prettyThrowM $ ConstructPlanFailed
+          errs
+          stackYaml
+          stackRoot
+          isImplicitGlobal
+          parents
+          ctx.wanted
+          prunedGlobalDeps
+ where
+  sourceProject = sourceMap.project
+  sourceDeps = sourceMap.deps
+
+  hasBaseInDeps = Map.member (mkPackageName "base") sourceDeps
+
+  mkCtx ctxEnvConfig globalCabalVersion sources curator pathEnvVar = Ctx
+    { baseConfigOpts = baseConfigOpts0
+    , loadPackage = \w x y z -> runRIO ctxEnvConfig $
+        applyForceCustomBuild globalCabalVersion <$> loadPackage0 w x y z
+    , combinedMap = combineMap sources installedMap
+    , ctxEnvConfig
+    , callStack = []
+    , wanted = Map.keysSet sourceMap.targets.targets
+    , localNames = Map.keysSet sourceProject
+    , curator
+    , pathEnvVar
+    }
+
+  toEither :: (k, Either e v) -> Either e (k, v)
+  toEither (_, Left e)  = Left e
+  toEither (k, Right v) = Right (k, v)
+
+  toMaybe :: (k, Maybe v) -> Maybe (k, v)
+  toMaybe (_, Nothing) = Nothing
+  toMaybe (k, Just v) = Just (k, v)
+
+  takeSubset :: Plan -> RIO env Plan
+  takeSubset = case baseConfigOpts0.buildOptsCLI.buildSubset of
+    BSAll -> pure
+    BSOnlySnapshot -> stripLocals
+    BSOnlyDependencies -> stripNonDeps
+    BSOnlyLocals -> errorOnSnapshot
+
+  -- | Strip out anything from the 'Plan' intended for the local database.
+  stripLocals :: Plan -> RIO env Plan
+  stripLocals plan = pure plan
+    { tasks = Map.filter checkTask plan.tasks
+    , finals = Map.empty
+    , unregisterLocal = Map.empty
+    , installExes = Map.filter (/= Local) plan.installExes
+    }
+   where
+    checkTask task = taskLocation task == Snap
+
+  stripNonDeps :: Plan -> RIO env Plan
+  stripNonDeps plan = pure plan
+    { tasks = Map.filter checkTask plan.tasks
+    , finals = Map.empty
+    , installExes = Map.empty -- TODO maybe don't disable this?
+    }
+   where
+    deps = Map.keysSet sourceDeps
+    checkTask task = taskProvides task `Set.member` missingForDeps
+    providesDep task = pkgName (taskProvides task) `Set.member` deps
+    tasks = Map.elems plan.tasks
+    missing =
+      Map.fromList $ map (taskProvides &&&  (.configOpts.missing)) tasks
+    missingForDeps = flip execState mempty $
+      for_ tasks $ \task ->
+        when (providesDep task) $
+          collectMissing mempty (taskProvides task)
+    collectMissing dependents pid = do
+      when (pid `elem` dependents) $
+        impureThrow $ TaskCycleBug pid
+      modify' (<> Set.singleton pid)
+      mapM_
+        (collectMissing (pid:dependents))
+        (fromMaybe mempty $ Map.lookup pid missing)
+
+  -- | Throw an exception if there are any snapshot packages in the plan.
+  errorOnSnapshot :: Plan -> RIO env Plan
+  errorOnSnapshot plan@(Plan tasks _finals _unregister installExes) = do
+    let snapTasks = Map.keys $ Map.filter (\t -> taskLocation t == Snap) tasks
+        snapExes = Map.keys $ Map.filter (== Snap) installExes
+    unless (null snapTasks && null snapExes) $
+      prettyThrowIO $ NotOnlyLocal snapTasks snapExes
+    pure plan
+
+  prunedGlobalDeps :: Map PackageName [PackageName]
+  prunedGlobalDeps = flip Map.mapMaybe sourceMap.globalPkgs $
+    \case
+      ReplacedGlobalPackage deps ->
+        let pruned = filter (not . inSourceMap) deps
+        in  if null pruned then Nothing else Just pruned
+      GlobalPackage _ -> Nothing
+   where
+    inSourceMap pname =
+      pname `Map.member` sourceDeps || pname `Map.member` sourceProject
+
+  getSources :: Version -> RIO env (Map PackageName PackageSource)
+  getSources globalCabalVersion = do
+    let loadLocalPackage' pp = do
+          lp <- loadLocalPackage pp
+          let lpPackage' =
+                applyForceCustomBuild globalCabalVersion lp.package
+          pure lp { package = lpPackage' }
+    pPackages <- for sourceProject $ \pp -> do
+      lp <- loadLocalPackage' pp
+      pure $ PSFilePath lp
+    bopts <- view $ configL . to (.build)
+    deps <- for sourceDeps $ \dp ->
+      case dp.location of
+        PLImmutable loc ->
+          pure $
+            PSRemote loc (getPLIVersion loc) dp.fromSnapshot dp.depCommon
+        PLMutable dir -> do
+          pp <- mkProjectPackage YesPrintWarnings dir (shouldHaddockDeps bopts)
+          lp <- loadLocalPackage' pp
+          pure $ PSFilePath lp
+    pure $ pPackages <> deps
+
+-- | Determine which packages to unregister based on the given tasks and
+-- already registered local packages.
+mkUnregisterLocal ::
+     Map PackageName Task
+     -- ^ Tasks
+  -> Map PackageName Text
+     -- ^ Reasons why packages are dirty and must be rebuilt
+  -> [DumpPackage]
+     -- ^ Local package database dump
+  -> Bool
+     -- ^ If true, we're doing a special initialBuildSteps build - don't
+     -- unregister target packages.
+  -> Map GhcPkgId (PackageIdentifier, Text)
+mkUnregisterLocal tasks dirtyReason localDumpPkgs initialBuildSteps =
+  -- We'll take multiple passes through the local packages. This will allow us
+  -- to detect that a package should be unregistered, as well as all packages
+  -- directly or transitively depending on it.
+  loop Map.empty localDumpPkgs
+ where
+  loop ::
+       Map GhcPkgId (PackageIdentifier, Text)
+       -- ^ Current local packages to unregister.
+    -> [DumpPackage]
+       -- ^ Current local packages to keep.
+    -> Map GhcPkgId (PackageIdentifier, Text)
+       -- ^ Revised local packages to unregister.
+  loop toUnregister keep
+    -- If any new packages were added to the unregister Map, we need to loop
+    -- through the remaining packages again to detect if a transitive dependency
+    -- is being unregistered.
+    | us.anyAdded = loop us.toUnregister us.toKeep
+    -- Nothing added, so we've already caught them all. Return the Map we've
+    -- already calculated.
+    | otherwise = us.toUnregister
+   where
+    -- Run the unregister checking function on all packages we currently think
+    -- we'll be keeping.
+    us = execState (mapM_ go keep) initialUnregisterState
+    initialUnregisterState = UnregisterState
+      { toUnregister
+      , toKeep = []
+      , anyAdded = False
+      }
+
+  go :: DumpPackage -> State UnregisterState ()
+  go dp = do
+    us <- get
+    case maybeUnregisterReason us.toUnregister ident mParentLibId deps of
+      -- Not unregistering, add it to the keep list.
+      Nothing -> put us { toKeep = dp : us.toKeep }
+      -- Unregistering, add it to the unregister Map; and indicate that a
+      -- package was in fact added to the unregister Map, so we loop again.
+      Just reason -> put us
+        { toUnregister = Map.insert gid (ident, reason) us.toUnregister
+        , anyAdded = True
+        }
+   where
+    gid = dp.ghcPkgId
+    ident = dp.packageIdent
+    mParentLibId = dpParentLibIdent dp
+    deps = dp.depends
+
+  maybeUnregisterReason ::
+       Map GhcPkgId (PackageIdentifier, Text)
+       -- ^ Current local packages to unregister.
+    -> PackageIdentifier
+       -- ^ Package identifier.
+    -> Maybe PackageIdentifier
+       -- ^ If package for sub library, package identifier of the parent.
+    -> [GhcPkgId]
+       -- ^ Dependencies of the package.
+    -> Maybe Text
+       -- ^ If to be unregistered, the reason for doing so.
+  maybeUnregisterReason toUnregister ident mParentLibId deps
+    -- If the package is not for a sub library, then it is directly relevant. If
+    -- it is, then the relevant package is the parent. If we are planning on
+    -- running a task on the relevant package, then the package must be
+    -- unregistered, unless it is a target and an initial-build-steps build is
+    -- being done.
+    | Just task <- Map.lookup relevantPkgName tasks =
+        if    initialBuildSteps
+           && taskIsTarget task
+           && taskProvides task == relevantPkgId
+          then Nothing
+          else Just $ fromMaybe "" $ Map.lookup relevantPkgName dirtyReason
+    -- Check if a dependency is going to be unregistered
+    | (dep, _):_ <- mapMaybe (`Map.lookup` toUnregister) deps =
+        Just $ "Dependency being unregistered: "
+          <> T.pack (packageIdentifierString dep)
+    -- None of the above, keep it!
+    | otherwise = Nothing
+    where
+      -- If the package is not for a sub library, then the relevant package
+      -- identifier is that of the package. If it is, then the relevant package
+      -- identifier is that of the parent.
+      relevantPkgId :: PackageIdentifier
+      relevantPkgId = fromMaybe ident mParentLibId
+      -- If the package is not for a sub library, then the relevant package name
+      -- is that of the package. If it is, then the relevant package name is
+      -- that of the parent.
+      relevantPkgName :: PackageName
+      relevantPkgName = maybe (pkgName ident) pkgName mParentLibId
+
+-- | Given a 'LocalPackage' and its 'lpTestBench', adds a 'Task' for running its
+-- tests and benchmarks.
+--
+-- If @isAllInOne@ is 'True', then this means that the build step will also
+-- build the tests. Otherwise, this indicates that there's a cyclic dependency
+-- and an additional build step needs to be done.
+--
+-- This will also add all the deps needed to build the tests / benchmarks. If
+-- @isAllInOne@ is 'True' (the common case), then all of these should have
+-- already been taken care of as part of the build step.
+addFinal ::
+     LocalPackage
+  -> Package
+  -> Bool
+     -- ^ Will the build step also build the tests?
+  -> Bool
+     -- ^ Should Haddock documentation be built?
+  -> M ()
+addFinal lp package isAllInOne buildHaddocks = do
+  depsRes <- addPackageDeps package
+  res <- case depsRes of
+    Left e -> pure $ Left e
+    Right (missing, present, _minLoc) -> do
+      ctx <- ask
+      pure $ Right Task
+        { configOpts = TaskConfigOpts missing $ \missing' ->
+            let allDeps = Map.union present missing'
+            in  ConfigureOpts.configureOpts
+                  (view envConfigL ctx)
+                  ctx.baseConfigOpts
+                  allDeps
+                  True -- local
+                  Mutable
+                  package
+        , buildHaddocks
+        , present
+        , taskType = TTLocalMutable lp
+        , allInOne = isAllInOne
+        , cachePkgSrc = CacheSrcLocal (toFilePath (parent lp.cabalFP))
+        , buildTypeConfig = packageBuildTypeConfig package
+        }
+  tell mempty { wFinals = Map.singleton package.name res }
+
+-- | Given a 'PackageName', adds all of the build tasks to build the package, if
+-- needed. First checks if the package name is in the library map.
+--
+-- 'constructPlan' invokes this on all the target packages, setting
+-- @treatAsDep'@ to False, because those packages are direct build targets.
+-- 'addPackageDeps' invokes this while recursing into the dependencies of a
+-- package. As such, it sets @treatAsDep'@ to True, forcing this package to be
+-- marked as a dependency, even if it is directly wanted. This makes sense - if
+-- we left out packages that are deps, it would break the --only-dependencies
+-- build plan.
+getCachedDepOrAddDep ::
+     PackageName
+  -> M (Either ConstructPlanException AddDepRes)
+getCachedDepOrAddDep name = do
+  libMap <- get
+  case Map.lookup name libMap of
+    Just res -> do
+      logDebugPlanS "getCachedDepOrAddDep" $
+           "Using cached result for "
+        <> fromPackageName name
+        <> ": "
+        <> fromString (show res)
+      pure res
+    Nothing -> checkCallStackAndAddDep name
+
+-- | Given a 'PackageName', known not to be in the library map, adds all of the
+-- build tasks to build the package. First checks that the package name is not
+-- already in the call stack.
+checkCallStackAndAddDep ::
+     PackageName
+  -> M (Either ConstructPlanException AddDepRes)
+checkCallStackAndAddDep name = do
+  ctx <- ask
+  res <- if name `elem` ctx.callStack
+    then do
+      logDebugPlanS "checkCallStackAndAddDep" $
+           "Detected cycle "
+        <> fromPackageName name
+        <> ": "
+        <> fromString (show $ map packageNameString ctx.callStack)
+      pure $ Left $ DependencyCycleDetected $ name : ctx.callStack
+    else case Map.lookup name ctx.combinedMap of
+      -- TODO look up in the package index and see if there's a
+      -- recommendation available
+      Nothing -> do
+        logDebugPlanS "checkCallStackAndAddDep" $
+             "No package info for "
+          <> fromPackageName name
+          <> "."
+        pure $ Left $ UnknownPackage name
+      Just packageInfo ->
+        -- Add the current package name to the head of the call stack.
+        local (\ctx' -> ctx' { callStack = name : ctx'.callStack }) $
+          addDep name packageInfo
+  updateLibMap name res
+  pure res
+
+-- | Given a 'PackageName' and its 'PackageInfo' from the combined map, adds all
+-- of the build tasks to build the package. Assumes that the head of the call
+-- stack is the current package name.
+addDep ::
+     PackageName
+  -> PackageInfo
+  -> M (Either ConstructPlanException AddDepRes)
+addDep name packageInfo = do
+  logDebugPlanS "addDep" $
+       "Package info for "
+    <> fromPackageName name
+    <> ": "
+    <> fromString (show packageInfo)
+  case packageInfo of
+    PIOnlyInstalled loc installed -> do
+      -- FIXME Slightly hacky, no flags since they likely won't affect
+      -- executable names. This code does not feel right.
+      let version = installedVersion installed
+          askPkgLoc = liftRIO $ do
+            mrev <- getLatestHackageRevision YesRequireHackageIndex name version
+            case mrev of
+              Nothing -> do
+                -- This could happen for GHC boot libraries missing from
+                -- Hackage.
+                cs <- asks (NE.nonEmpty . (.callStack))
+                cs' <- maybe
+                  (throwIO CallStackEmptyBug)
+                  (pure . NE.tail)
+                  cs
+                prettyWarnL
+                  $ flow "No latest package revision found for"
+                  : style Current (fromPackageName name) <> ","
+                  : flow "dependency callstack:"
+                  : mkNarrativeList Nothing False
+                      (map fromPackageName cs' :: [StyleDoc])
+                pure Nothing
+              Just (_rev, cfKey, treeKey) ->
+                pure $ Just $
+                  PLIHackage (PackageIdentifier name version) cfKey treeKey
+      tellExecutablesUpstream name askPkgLoc loc Map.empty
+      pure $ Right $ ADRFound loc installed
+    PIOnlySource ps -> do
+      tellExecutables name ps
+      installPackage name ps Nothing
+    PIBoth ps installed -> do
+      tellExecutables name ps
+      installPackage name ps (Just installed)
+
+-- | For given 'PackageName' and 'PackageSource' values, adds relevant
+-- executables to the collected output.
+tellExecutables :: PackageName -> PackageSource -> M ()
+tellExecutables _name (PSFilePath lp)
+  | lp.wanted = tellExecutablesPackage Local lp.package
+  | otherwise = pure ()
+-- Ignores ghcOptions because they don't matter for enumerating executables.
+tellExecutables name (PSRemote pkgloc _version _fromSnapshot cp) =
+  tellExecutablesUpstream name (pure $ Just pkgloc) Snap cp.flags
+
+-- | For a given 'PackageName' value, known to be immutable, adds relevant
+-- executables to the collected output.
+tellExecutablesUpstream ::
+     PackageName
+  -> M (Maybe PackageLocationImmutable)
+  -> InstallLocation
+  -> Map FlagName Bool
+  -> M ()
+tellExecutablesUpstream name retrievePkgLoc loc flags = do
+  ctx <- ask
+  when (name `Set.member` ctx.wanted) $ do
+    mPkgLoc <- retrievePkgLoc
+    forM_ mPkgLoc $ \pkgLoc -> do
+      p <- ctx.loadPackage pkgLoc flags [] []
+      tellExecutablesPackage loc p
+
+-- | For given 'InstallLocation' and 'Package' values, adds relevant executables
+-- to the collected output. In most cases, the relevant executables are all the
+-- executables of the package. If the package is a wanted local one, the
+-- executables are those executables that are wanted executables.
+tellExecutablesPackage :: InstallLocation -> Package -> M ()
+tellExecutablesPackage loc p = do
+  cm <- asks (.combinedMap)
+  -- Determine which components are enabled so we know which ones to copy
+  let myComps =
+        case Map.lookup p.name cm of
+          Nothing -> assert False Set.empty
+          Just (PIOnlyInstalled _ _) -> Set.empty
+          Just (PIOnlySource ps) -> goSource ps
+          Just (PIBoth ps _) -> goSource ps
+
+      goSource (PSFilePath lp)
+        | lp.wanted = exeComponents lp.components
+        | otherwise = Set.empty
+      goSource PSRemote{} = Set.empty
+
+  tell mempty
+    { wInstall = Map.fromList $
+        map (, loc) $ Set.toList $ filterComps myComps $ buildableExes p
+    }
+ where
+  filterComps myComps x
+    | Set.null myComps = x
+    | otherwise = Set.intersection x myComps
+
+-- | Given a 'PackageSource' and perhaps an 'Installed' value, adds build
+-- 'Task's for the package and its dependencies.
+installPackage :: PackageName
+               -> PackageSource
+               -> Maybe Installed
+               -> M (Either ConstructPlanException AddDepRes)
+installPackage name ps minstalled = do
+  ctx <- ask
+  case ps of
+    PSRemote pkgLoc _version _fromSnapshot cp -> do
+      logDebugPlanS "installPackage" $
+           "Doing all-in-one build for upstream package "
+        <> fromPackageName name
+        <> "."
+      package <- ctx.loadPackage
+        pkgLoc cp.flags cp.ghcOptions cp.cabalConfigOpts
+      resolveDepsAndInstall True cp.buildHaddocks ps package minstalled
+    PSFilePath lp -> do
+      case lp.testBench of
+        Nothing -> do
+          logDebugPlanS "installPackage" $
+               "No test or bench component for "
+            <> fromPackageName name
+            <> " so doing an all-in-one build."
+          resolveDepsAndInstall
+            True lp.buildHaddocks ps lp.package minstalled
+        Just tb -> do
+          -- Attempt to find a plan which performs an all-in-one build. Ignore
+          -- the writer action + reset the state if it fails.
+          libMap <- get
+          res <- pass $ do
+            res <- addPackageDeps tb
+            let writerFunc w = case res of
+                  Left _ -> mempty
+                  _ -> w
+            pure (res, writerFunc)
+          case res of
+            Right deps -> do
+              logDebugPlanS "installPackage" $
+                   "For "
+                <> fromPackageName name
+                <> ", successfully added package deps."
+              -- in curator builds we can't do all-in-one build as
+              -- test/benchmark failure could prevent library from being
+              -- available to its dependencies but when it's already available
+              -- it's OK to do that
+              splitRequired <- expectedTestOrBenchFailures <$> asks (.curator)
+              let isAllInOne = not splitRequired
+              adr <- installPackageGivenDeps
+                isAllInOne lp.buildHaddocks ps tb minstalled deps
+              let finalAllInOne = case adr of
+                    ADRToInstall _ | splitRequired -> False
+                    _ -> True
+              -- FIXME: this redundantly adds the deps (but they'll all just
+              -- get looked up in the map)
+              addFinal lp tb finalAllInOne False
+              pure $ Right adr
+            Left _ -> do
+              -- Reset the state to how it was before attempting to find an
+              -- all-in-one build plan.
+              logDebugPlanS "installPackage" $
+                   "Before trying cyclic plan, resetting lib result map to: "
+                <> fromString (show libMap)
+              put libMap
+              -- Otherwise, fall back on building the tests / benchmarks in a
+              -- separate step.
+              res' <- resolveDepsAndInstall
+                False lp.buildHaddocks ps lp.package minstalled
+              when (isRight res') $ do
+                -- Insert it into the map so that it's available for addFinal.
+                updateLibMap name res'
+                addFinal lp tb False False
+              pure res'
+ where
+  expectedTestOrBenchFailures maybeCurator = fromMaybe False $ do
+    curator <- maybeCurator
+    pure $  Set.member name curator.expectTestFailure
+         || Set.member name curator.expectBenchmarkFailure
+
+resolveDepsAndInstall ::
+     Bool
+     -- ^ will the build step also build any tests?
+  -> Bool
+     -- ^ Should Haddock documentation be built?
+  -> PackageSource
+  -> Package
+  -> Maybe Installed
+  -> M (Either ConstructPlanException AddDepRes)
+resolveDepsAndInstall isAllInOne buildHaddocks ps package minstalled = do
+  res <- addPackageDeps package
+  case res of
+    Left err -> pure $ Left err
+    Right deps ->
+      Right <$>
+        installPackageGivenDeps
+          isAllInOne buildHaddocks ps package minstalled deps
+
+-- | Checks if we need to install the given 'Package', given the results
+-- of 'addPackageDeps'. If dependencies are missing, the package is dirty, or
+-- it's not installed, then it needs to be installed.
+installPackageGivenDeps ::
+     Bool
+     -- ^ will the build step also build any tests?
+  -> Bool
+     -- ^ Should Haddock documentation be built?
+  -> PackageSource
+  -> Package
+  -> Maybe Installed
+  -> ( Set PackageIdentifier
+     , Map PackageIdentifier GhcPkgId
+     , IsMutable )
+  -> M AddDepRes
+installPackageGivenDeps isAllInOne buildHaddocks ps package minstalled
+  (missing, present, minMutable) = do
+    let name = package.name
+    ctx <- ask
+    mRightVersionInstalled <- case (minstalled, Set.null missing) of
+      (Just installed, True) -> do
+        shouldInstall <-
+          checkDirtiness ps installed package present buildHaddocks
+        pure $ if shouldInstall then Nothing else Just installed
+      (Just _, False) -> do
+        let t = T.intercalate ", " $
+                  map (T.pack . packageNameString . pkgName) (Set.toList missing)
+        tell mempty
+          { wDirty =
+              Map.singleton name $ "missing dependencies: " <> addEllipsis t
+          }
+        pure Nothing
+      (Nothing, _) -> pure Nothing
+    let loc = psLocation ps
+        mutable = installLocationIsMutable loc <> minMutable
+    pure $ case mRightVersionInstalled of
+      Just installed -> ADRFound loc installed
+      Nothing -> ADRToInstall Task
+        { configOpts = TaskConfigOpts missing $ \missing' ->
+            let allDeps = Map.union present missing'
+            in  ConfigureOpts.configureOpts
+                  (view envConfigL ctx)
+                  ctx.baseConfigOpts
+                  allDeps
+                  (psLocal ps)
+                  mutable
+                  package
+        , buildHaddocks
+        , present
+        , taskType =
+            case ps of
+              PSFilePath lp ->
+                TTLocalMutable lp
+              PSRemote pkgLoc _version _fromSnapshot _cp ->
+                TTRemotePackage mutable package pkgLoc
+        , allInOne = isAllInOne
+        , cachePkgSrc = toCachePkgSrc ps
+        , buildTypeConfig = packageBuildTypeConfig package
+        }
+
+-- | Is the build type of the package Configure
+packageBuildTypeConfig :: Package -> Bool
+packageBuildTypeConfig pkg = pkg.buildType == Configure
+
+-- Update response in the library map. If it is an error, and there's already an
+-- error about cyclic dependencies, prefer the cyclic error.
+updateLibMap :: PackageName -> Either ConstructPlanException AddDepRes -> M ()
+updateLibMap name val = modify $ \mp ->
+  case (Map.lookup name mp, val) of
+    (Just (Left DependencyCycleDetected{}), Left _) -> mp
+    _ -> Map.insert name val mp
+
+addEllipsis :: Text -> Text
+addEllipsis t
+  | T.length t < 100 = t
+  | otherwise = T.take 97 t <> "..."
+
+-- | Given a package, recurses into all of its dependencies. The resulting
+-- triple indicates: (1) which packages are missing. This means that their
+-- 'GhcPkgId's will be figured out during the build, after they've been built;
+-- (2) the packages that are already installed and which will be used; and
+-- (3) whether the package itself is mutable or immutable.
+addPackageDeps ::
+     Package
+  -> M ( Either
+           ConstructPlanException
+           ( Set PackageIdentifier
+           , Map PackageIdentifier GhcPkgId
+           , IsMutable
+           )
+       )
+addPackageDeps package = do
+  checkAndWarnForUnknownTools package
+  let pkgId = packageIdentifier package
+  deps <- processPackageDepsToList package (processDep pkgId)
+  pure $ case partitionEithers deps of
+    -- Note that the Monoid for 'IsMutable' means that if any is 'Mutable',
+    -- the result is 'Mutable'. Otherwise the result is 'Immutable'.
+    ([], pairs) -> Right $ mconcat pairs
+    (errs, _) ->
+      Left $ DependencyPlanFailures package (Map.fromList errs)
+
+-- | Given a dependency, yields either information for an error message or a
+-- triple indicating: (1) if the dependency is to be installed, its package
+-- identifier; (2) if the dependency is installed and a library, its package
+-- identifier and 'GhcPkgId'; and (3) if the dependency is, or will be when
+-- installed, mutable or immutable.
+processDep ::
+     PackageIdentifier
+     -- ^ The package which has the dependency being processed.
+  -> PackageName
+     -- ^ The name of the dependency.
+  -> DepValue
+     -- ^ The version range and dependency type of the dependency.
+  -> M ( Either
+           ( PackageName
+           , (VersionRange, Maybe (Version, BlobKey), BadDependency)
+           )
+           (Set PackageIdentifier, Map PackageIdentifier GhcPkgId, IsMutable)
+       )
+processDep pkgId name value = do
+  mLatestApplicable <- getLatestApplicableVersionAndRev name range
+  eRes <- getCachedDepOrAddDep name
+  case eRes of
+    Left e -> do
+      addParent
+      let bd = case e of
+            UnknownPackage name' -> assert (name' == name) NotInBuildPlan
+            DependencyCycleDetected names -> BDDependencyCycleDetected names
+            -- ultimately we won't show any information on this to the user,
+            -- we'll allow the dependency failures alone to display to avoid
+            -- spamming the user too much
+            DependencyPlanFailures _ _  ->
+              Couldn'tResolveItsDependencies version
+      pure $ Left (name, (range, mLatestApplicable, bd))
+    Right adr
+      | isDepTypeLibrary value.depType && not (adrHasLibrary adr) ->
+          pure $ Left (name, (range, Nothing, HasNoLibrary))
+    Right adr -> do
+      addParent
+      inRange <- adrInRange pkgId name range adr
+      pure $ if inRange
+        then Right $ processAdr adr
+        else Left
+          ( name
+          , ( range
+            , mLatestApplicable
+            , DependencyMismatch $ adrVersion adr
+            )
+          )
+ where
+  range = value.versionRange
+  version = pkgVersion pkgId
+  -- Update the parents map, for later use in plan construction errors
+  -- - see 'getShortestDepsPath'.
+  addParent =
+    let parentMap = Map.singleton name [(pkgId, range)]
+    in  tell mempty { wParents = MonoidMap parentMap }
+
+getLatestApplicableVersionAndRev ::
+     PackageName
+  -> VersionRange
+  -> M (Maybe (Version, BlobKey))
+getLatestApplicableVersionAndRev name range = do
+  ctx <- ask
+  vsAndRevs <- runRIO ctx $
+    getHackagePackageVersions YesRequireHackageIndex UsePreferredVersions name
+  pure $ do
+    lappVer <- latestApplicableVersion range $ Map.keysSet vsAndRevs
+    revs <- Map.lookup lappVer vsAndRevs
+    (cabalHash, _) <- Map.maxView revs
+    Just (lappVer, cabalHash)
+
+-- | Function to determine whether the result of 'addDep' is within range, given
+-- the version range of the dependency and taking into account Stack's
+-- @allow-newer@ configuration.
+adrInRange ::
+     PackageIdentifier
+     -- ^ The package which has the dependency.
+  -> PackageName
+     -- ^ The name of the dependency.
+  -> VersionRange
+     -- ^ The version range of the dependency.
+  -> AddDepRes
+     -- ^ The result of 'addDep'.
+  -> M Bool
+adrInRange pkgId name range adr = if adrVersion adr `withinRange` range
+  then pure True
+  else do
+    allowNewer <- view $ configL . to (.allowNewer)
+    allowNewerDeps <- view $ configL . to (.allowNewerDeps)
+    if allowNewer
+      then case allowNewerDeps of
+        Nothing -> do
+          warn_ True $
+            fillSep
+              [ style Shell "allow-newer"
+              , "enabled"
+              ]
+          pure True
+        Just boundsIgnoredDeps -> do
+          let pkgName' = fromPackageName pkgName
+              isBoundsIgnoreDep = pkgName `elem` boundsIgnoredDeps
+              reason = if isBoundsIgnoreDep
+                then fillSep
+                  [ style Current pkgName'
+                  , flow "is an"
+                  , style Shell "allow-newer-dep"
+                  , flow "and"
+                  , style Shell "allow-newer"
+                  , "enabled"
+                  ]
+                else fillSep
+                  [ style Current pkgName'
+                  , flow "is not an"
+                  , style Shell "allow-newer-dep"
+                  , flow "although"
+                  , style Shell "allow-newer"
+                  , "enabled"
+                  ]
+          warn_ isBoundsIgnoreDep reason
+          pure isBoundsIgnoreDep
+      else do
+        when (isJust allowNewerDeps) $
+          warn_ False $
+            fillSep
+              [ "although"
+              , style Shell "allow-newer-deps"
+              , flow "are specified,"
+              , style Shell "allow-newer"
+              , "is"
+              , style Shell "false"
+              ]
+        -- We ignore dependency information for packages in a snapshot
+        pkgInSnapshot <- inSnapshot pkgName version
+        adrInSnapshot <- inSnapshot name (adrVersion adr)
+        if pkgInSnapshot && adrInSnapshot
+          then do
+            warn_ True
+              ( flow "trusting snapshot over Cabal file dependency \
+                     \information"
+              )
+            pure True
+          else pure False
+ where
+  PackageIdentifier pkgName version = pkgId
+  warn_ isIgnoring reason = tell mempty { wWarnings = (msg:) }
+   where
+    msg = fillSep
+            [ if isIgnoring
+                then "Ignoring"
+                else flow "Not ignoring"
+            , style Current (fromPackageName pkgName) <> "'s"
+            , flow "bounds on"
+            , style Current (fromPackageName name)
+            , parens (fromString . T.unpack $ versionRangeText range)
+            , flow "and using"
+            , style
+                Current
+                (fromPackageId $ PackageIdentifier name (adrVersion adr)) <> "."
+            ]
+       <> line
+       <> fillSep
+            [ "Reason:"
+            , reason <> "."
+            ]
+
+-- | Given a result of 'addDep', yields a triple indicating: (1) if the
+-- dependency is to be installed, its package identifier; (2) if the dependency
+-- is installed and a library, its package identifier and 'GhcPkgId'; and (3) if
+-- the dependency is, or will be when installed, mutable or immutable.
+processAdr ::
+     AddDepRes
+  -> (Set PackageIdentifier, Map PackageIdentifier GhcPkgId, IsMutable)
+processAdr adr = case adr of
+  ADRToInstall task ->
+    (Set.singleton $ taskProvides task, Map.empty, taskTargetIsMutable task)
+  ADRFound loc (Executable _) ->
+    (Set.empty, Map.empty, installLocationIsMutable loc)
+  ADRFound loc (Library ident installedInfo) ->
+    ( Set.empty
+    , installedMapGhcPkgId ident installedInfo
+    , installLocationIsMutable loc
+    )
+
+checkDirtiness ::
+     PackageSource
+  -> Installed
+  -> Package
+  -> Map PackageIdentifier GhcPkgId
+  -> Bool
+     -- ^ Is Haddock documentation being built?
+  -> M Bool
+checkDirtiness ps installed package present buildHaddocks = do
+  ctx <- ask
+  moldOpts <- runRIO ctx $ tryGetFlagCache installed
+  let configureOpts = ConfigureOpts.configureOpts
+        (view envConfigL ctx)
+        ctx.baseConfigOpts
+        present
+        (psLocal ps)
+        (installLocationIsMutable $ psLocation ps) -- should be Local i.e. mutable always
+        package
+      components = case ps of
+        PSFilePath lp ->
+          Set.map (encodeUtf8 . renderComponent) lp.components
+        PSRemote{} -> Set.empty
+      wantConfigCache = ConfigCache
+        { configureOpts
+        , deps = Set.fromList $ Map.elems present
+        , components
+        , buildHaddocks
+        , pkgSrc = toCachePkgSrc ps
+        , pathEnvVar = ctx.pathEnvVar
+        }
+      config = view configL ctx
+  mreason <-
+    case moldOpts of
+      Nothing -> pure $ Just "old configure information not found"
+      Just oldOpts
+        | Just reason <- describeConfigDiff config oldOpts wantConfigCache ->
+            pure $ Just reason
+        | True <- psForceDirty ps -> pure $ Just "--force-dirty specified"
+        | otherwise -> do
+            dirty <- psDirty ps
+            pure $
+              case dirty of
+                Just files -> Just $
+                     "local file changes: "
+                  <> addEllipsis (T.pack $ unwords $ Set.toList files)
+                Nothing -> Nothing
+  case mreason of
+    Nothing -> pure False
+    Just reason -> do
+      tell mempty { wDirty = Map.singleton package.name reason }
+      pure True
+
+describeConfigDiff :: Config -> ConfigCache -> ConfigCache -> Maybe Text
+describeConfigDiff config old new
+  | old.pkgSrc /= new.pkgSrc = Just $
+      "switching from " <>
+      pkgSrcName old.pkgSrc <> " to " <>
+      pkgSrcName new.pkgSrc
+  | not (new.deps `Set.isSubsetOf` old.deps) =
+      Just "dependencies changed"
+  | not $ Set.null newComponents =
+      Just $ "components added: " `T.append` T.intercalate ", "
+          (map (decodeUtf8With lenientDecode) (Set.toList newComponents))
+  | not old.buildHaddocks && new.buildHaddocks =
+      Just "rebuilding with haddocks"
+  | oldOpts /= newOpts = Just $ T.pack $ concat
+      [ "flags changed from "
+      , show oldOpts
+      , " to "
+      , show newOpts
+      ]
+  | otherwise = Nothing
+ where
+  stripGhcOptions = go
+   where
+    go [] = []
+    go ("--ghc-option":x:xs) = go' Ghc x xs
+    go ("--ghc-options":x:xs) = go' Ghc x xs
+    go ((T.stripPrefix "--ghc-option=" -> Just x):xs) = go' Ghc x xs
+    go ((T.stripPrefix "--ghc-options=" -> Just x):xs) = go' Ghc x xs
+    go (x:xs) = x : go xs
+
+    go' wc x xs = checkKeepers wc x $ go xs
+
+    checkKeepers wc x xs =
+      case filter isKeeper $ T.words x of
+        [] -> xs
+        keepers -> T.pack (compilerOptionsCabalFlag wc) : T.unwords keepers : xs
+
+    -- GHC options which affect build results and therefore should always force
+    -- a rebuild
+    --
+    -- For the most part, we only care about options generated by Stack itself
+    isKeeper = (== "-fhpc") -- more to be added later
+
+  userOpts = filter (not . isStackOpt)
+           . (if config.rebuildGhcOptions
+                then id
+                else stripGhcOptions)
+           . map T.pack
+           . (\(ConfigureOpts x y) -> x ++ y)
+           . (.configureOpts)
+   where
+    -- options set by Stack
+    isStackOpt :: Text -> Bool
+    isStackOpt t = any (`T.isPrefixOf` t)
+      [ "--dependency="
+      , "--constraint="
+      , "--package-db="
+      , "--libdir="
+      , "--bindir="
+      , "--datadir="
+      , "--libexecdir="
+      , "--sysconfdir"
+      , "--docdir="
+      , "--htmldir="
+      , "--haddockdir="
+      , "--enable-tests"
+      , "--enable-benchmarks"
+      , "--exact-configuration"
+      -- Treat these as causing dirtiness, to resolve
+      -- https://github.com/commercialhaskell/stack/issues/2984
+      --
+      -- , "--enable-library-profiling"
+      -- , "--enable-executable-profiling"
+      -- , "--enable-profiling"
+      ] || t == "--user"
+
+  (oldOpts, newOpts) = removeMatching (userOpts old) (userOpts new)
+
+  removeMatching (x:xs) (y:ys)
+    | x == y = removeMatching xs ys
+  removeMatching xs ys = (xs, ys)
+
+  newComponents =
+    new.components `Set.difference` old.components
+
+  pkgSrcName (CacheSrcLocal fp) = T.pack fp
+  pkgSrcName CacheSrcUpstream = "upstream source"
+
+psForceDirty :: PackageSource -> Bool
+psForceDirty (PSFilePath lp) = lp.forceDirty
+psForceDirty PSRemote{} = False
+
+psDirty ::
+     (MonadIO m, HasEnvConfig env, MonadReader env m)
+  => PackageSource
+  -> m (Maybe (Set FilePath))
+psDirty (PSFilePath lp) = runMemoizedWith lp.dirtyFiles
+psDirty PSRemote {} = pure Nothing -- files never change in a remote package
+
+psLocal :: PackageSource -> Bool
+psLocal (PSFilePath _ ) = True
+psLocal PSRemote{} = False
+
+psLocation :: PackageSource -> InstallLocation
+psLocation (PSFilePath _) = Local
+psLocation PSRemote{} = Snap
+
+-- | For the given package, warn about any unknown tools that are not on the
+-- PATH and not one of the executables of the package.
+checkAndWarnForUnknownTools :: Package -> M ()
+checkAndWarnForUnknownTools p = do
+  let unknownTools = Set.toList $ packageUnknownTools p
+  -- Check whether the tool is on the PATH or a package executable before
+  -- warning about it.
+  warnings <-
+    fmap catMaybes $ forM unknownTools $ \toolName ->
+      runMaybeT $ notOnPath toolName *> notPackageExe toolName *> warn toolName
+  tell mempty { wWarnings = (map toolWarningText warnings ++) }
+  pure ()
+ where
+  -- From Cabal 2.0, build-tools can specify a pre-built executable that should
+  -- already be on the PATH.
+  notOnPath toolName = MaybeT $ do
+    let settings = minimalEnvSettings { includeLocals = True }
+    config <- view configL
+    menv <- liftIO $ config.processContextSettings settings
+    eFound <- runRIO menv $ findExecutable $ T.unpack toolName
+    skipIf $ isRight eFound
+  -- From Cabal 1.12, build-tools can specify another executable in the same
+  -- package.
+  notPackageExe toolName =
+    MaybeT $ skipIf $ collectionMember toolName p.executables
+  warn name = MaybeT . pure . Just $ ToolWarning (ExeName name) p.name
+  skipIf p' = pure $ if p' then Nothing else Just ()
+
+toolWarningText :: ToolWarning -> StyleDoc
+toolWarningText (ToolWarning (ExeName toolName) pkgName') = fillSep
+  [ flow "No packages found in snapshot which provide a"
+  , style PkgComponent (fromString $ show toolName)
+  , flow "executable, which is a build-tool dependency of"
+  , style Current (fromPackageName pkgName')
+  ]
+
+-- | Is the given package/version combo defined in the snapshot or in the global
+-- database?
+inSnapshot :: PackageName -> Version -> M Bool
+inSnapshot name version = do
+  ctx <- ask
+  pure $ fromMaybe False $ do
+    ps <- Map.lookup name ctx.combinedMap
+    case ps of
+      PIOnlySource (PSRemote _ srcVersion FromSnapshot _) ->
+        pure $ srcVersion == version
+      PIBoth (PSRemote _ srcVersion FromSnapshot _) _ ->
+        pure $ srcVersion == version
+      -- OnlyInstalled occurs for global database
+      PIOnlyInstalled loc (Library pid _) ->
+        assert (loc == Snap) $
+        assert (pkgVersion pid == version) $
+        Just True
+      _ -> pure False
+
+-- TODO: Consider intersecting version ranges for multiple deps on a
+-- package.  This is why VersionRange is in the parent map.
+
+logDebugPlanS ::
+     (HasCallStack, HasRunner env, MonadIO m, MonadReader env m)
+  => LogSource
+  -> Utf8Builder
+  -> m ()
+logDebugPlanS s msg = do
+  debugPlan <- view $ globalOptsL . to (.planInLog)
+  when debugPlan $ logDebugS s msg
+
+-- | A function to yield a 'PackageInfo' value from: (1) a 'PackageSource'
+-- value; and (2) a pair of an 'InstallLocation' value and an 'Installed' value.
+-- Checks that the version of the 'PackageSource' value and the version of the
+-- `Installed` value are the same.
+combineSourceInstalled :: PackageSource
+                       -> (InstallLocation, Installed)
+                       -> PackageInfo
+combineSourceInstalled ps (location, installed) =
+  assert (psVersion ps == installedVersion installed) $
+    case location of
+      -- Always trust something in the snapshot
+      Snap -> PIOnlyInstalled location installed
+      Local -> PIBoth ps installed
+
+-- | A function to yield a 'CombinedMap' value from: (1) a dictionary of package
+-- names, and where the source code of the named package is located; and (2) an
+-- 'InstalledMap' value.
+combineMap :: Map PackageName PackageSource -> InstalledMap -> CombinedMap
+combineMap = Map.merge
+  (Map.mapMissing (\_ s -> PIOnlySource s))
+  (Map.mapMissing (\_ i -> uncurry PIOnlyInstalled i))
+  (Map.zipWithMatched (\_ s i -> combineSourceInstalled s i))
src/Stack/Build/Execute.hs view
@@ -1,2734 +1,600 @@-{-# LANGUAGE NoImplicitPrelude     #-}
-{-# LANGUAGE DataKinds             #-}
-{-# LANGUAGE OverloadedStrings     #-}
-{-# LANGUAGE RecordWildCards       #-}
-{-# LANGUAGE TypeFamilies          #-}
-
--- | Perform a build
-module Stack.Build.Execute
-  ( printPlan
-  , preFetch
-  , executePlan
-  -- * Running Setup.hs
-  , ExecuteEnv
-  , withExecuteEnv
-  , withSingleContext
-  , ExcludeTHLoading (..)
-  , KeepOutputOpen (..)
-  ) where
-
-import           Control.Concurrent.Companion ( Companion, withCompanion )
-import           Control.Concurrent.Execute
-                   ( Action (..), ActionContext (..), ActionId (..)
-                   , ActionType (..)
-                   , Concurrency (..), runActions
-                   )
-import           Control.Concurrent.STM ( check )
-import           Crypto.Hash ( SHA256 (..), hashWith )
-import           Data.Attoparsec.Text ( char, choice, digit, parseOnly )
-import qualified Data.Attoparsec.Text as P ( string )
-import qualified Data.ByteArray as Mem ( convert )
-import qualified Data.ByteString as S
-import qualified Data.ByteString.Builder ( toLazyByteString )
-import qualified Data.ByteString.Lazy as BL
-import qualified Data.ByteString.Base64.URL as B64URL
-import           Data.Char ( isSpace )
-import           Conduit
-                   ( ConduitT, awaitForever, runConduitRes, sinkHandle
-                   , withSinkFile, withSourceFile, yield
-                   )
-import qualified Data.Conduit.Binary as CB
-import qualified Data.Conduit.Filesystem as CF
-import qualified Data.Conduit.List as CL
-import           Data.Conduit.Process.Typed ( createSource )
-import qualified Data.Conduit.Text as CT
-import qualified Data.List as L
-import           Data.List.NonEmpty ( nonEmpty )
-import qualified Data.List.NonEmpty as NonEmpty
-import           Data.List.Split ( chunksOf )
-import qualified Data.Map.Merge.Strict as Map
-import qualified Data.Map.Strict as Map
-import qualified Data.Set as Set
-import qualified Data.Text as T
-import           Data.Text.Encoding ( decodeUtf8 )
-import           Data.Tuple ( swap )
-import           Data.Time
-                   ( ZonedTime, getZonedTime, formatTime, defaultTimeLocale )
-import qualified Data.ByteString.Char8 as S8
-import qualified Distribution.PackageDescription as C
-import qualified Distribution.Simple.Build.Macros as C
-import           Distribution.System ( OS (Windows), Platform (Platform) )
-import qualified Distribution.Text as C
-import           Distribution.Types.MungedPackageName
-                   ( encodeCompatPackageName )
-import           Distribution.Types.PackageName ( mkPackageName )
-import           Distribution.Types.UnqualComponentName
-                   ( mkUnqualComponentName )
-import           Distribution.Verbosity ( showForCabal )
-import           Distribution.Version ( mkVersion )
-import           Path
-                   ( PathException, (</>), addExtension, filename
-                   , isProperPrefixOf, parent, parseRelDir, parseRelFile
-                   , stripProperPrefix
-                   )
-import           Path.CheckInstall ( warnInstallSearchPathIssues )
-import           Path.Extra
-                   ( forgivingResolveFile, rejectMissingFile
-                   , toFilePathNoTrailingSep
-                   )
-import           Path.IO
-                   ( copyFile, doesDirExist, doesFileExist, ensureDir
-                   , ignoringAbsence, removeDirRecur, removeFile, renameDir
-                   , renameFile
-                   )
-import           RIO.Process
-                   ( HasProcessContext, byteStringInput, doesExecutableExist
-                   , eceExitCode, findExecutable, getStderr, getStdout, inherit
-                   , modifyEnvVars, proc, runProcess_, setStderr, setStdin
-                   , setStdout, showProcessArgDebug, useHandleOpen, waitExitCode
-                   , withProcessWait, withWorkingDir
-                   )
-import           Stack.Build.Cache
-                   ( TestStatus (..), deleteCaches, getTestStatus
-                   , markExeInstalled, markExeNotInstalled, readPrecompiledCache
-                   , setTestStatus, tryGetCabalMod, tryGetConfigCache
-                   , tryGetPackageProjectRoot, tryGetSetupConfigMod
-                   , writeBuildCache, writeCabalMod, writeConfigCache
-                   , writeFlagCache, writePrecompiledCache
-                   , writePackageProjectRoot, writeSetupConfigMod
-                   )
-import           Stack.Build.Haddock
-                   ( generateDepsHaddockIndex, generateLocalHaddockIndex
-                   , generateSnapHaddockIndex, openHaddocksInBrowser
-                   )
-import           Stack.Build.Installed (  )
-import           Stack.Build.Source ( addUnlistedToBuildCache )
-import           Stack.Build.Target (  )
-import           Stack.Config ( checkOwnership )
-import           Stack.Config.ConfigureScript ( ensureConfigureScript )
-import           Stack.Constants
-                   ( bindirSuffix, cabalPackageName, compilerOptionsCabalFlag
-                   , relDirBuild, relDirDist, relDirSetup, relDirSetupExeCache
-                   , relDirSetupExeSrc, relFileBuildLock, relFileSetupHs
-                   , relFileSetupLhs, relFileSetupLower, relFileSetupMacrosH
-                   , setupGhciShimCode, stackProgName, testGhcEnvRelFile
-                   )
-import           Stack.Constants.Config
-                   ( distDirFromDir, distRelativeDir, hpcDirFromDir
-                   , hpcRelativeDir, setupConfigFromDir
-                   )
-import           Stack.Coverage
-                   ( deleteHpcReports, generateHpcMarkupIndex, generateHpcReport
-                   , generateHpcUnifiedReport, updateTixFile
-                   )
-import           Stack.GhcPkg ( ghcPkg, unregisterGhcPkgIds )
-import           Stack.Package ( buildLogPath )
-import           Stack.PackageDump ( conduitDumpPackage, ghcPkgDescribe )
-import           Stack.Prelude
-import           Stack.Types.ApplyGhcOptions ( ApplyGhcOptions (..) )
-import           Stack.Types.Build
-                   ( ConfigCache (..), Plan (..), PrecompiledCache (..)
-                   , Task (..), TaskConfigOpts (..), TaskType (..)
-                   , configCacheComponents, taskIsTarget, taskLocation
-                   )
-import           Stack.Types.Build.Exception
-                   ( BuildException (..), BuildPrettyException (..) )
-import           Stack.Types.BuildConfig
-                   ( BuildConfig (..), HasBuildConfig (..), projectRootL )
-import           Stack.Types.BuildOpts
-                   ( BenchmarkOpts (..), BuildOpts (..), BuildOptsCLI (..)
-                   , CabalVerbosity (..), HaddockOpts (..)
-                   , ProgressBarFormat (..), TestOpts (..)
-                   )
-import           Stack.Types.Compiler
-                   ( ActualCompiler (..), WhichCompiler (..)
-                   , compilerVersionString, getGhcVersion, whichCompilerL
-                   )
-import           Stack.Types.CompilerPaths
-                   ( CompilerPaths (..), GhcPkgExe (..), HasCompiler (..)
-                   , cabalVersionL, cpWhich, getCompilerPath, getGhcPkgExe
-                   )
-import           Stack.Types.Config
-                   ( Config (..), HasConfig (..), buildOptsL, stackRootL )
-import           Stack.Types.ConfigureOpts
-                   ( BaseConfigOpts (..), ConfigureOpts (..) )
-import           Stack.Types.DumpLogs ( DumpLogs (..) )
-import           Stack.Types.DumpPackage ( DumpPackage (..) )
-import           Stack.Types.EnvConfig
-                   ( HasEnvConfig (..), actualCompilerVersionL
-                   , appropriateGhcColorFlag, bindirCompilerTools
-                   , installationRootDeps, installationRootLocal
-                   , packageDatabaseLocal, platformGhcRelDir
-                   , shouldForceGhcColorFlag
-                   )
-import           Stack.Types.EnvSettings ( EnvSettings (..) )
-import           Stack.Types.GhcPkgId ( GhcPkgId, ghcPkgIdString, unGhcPkgId )
-import           Stack.Types.GlobalOpts ( GlobalOpts (..) )
-import           Stack.Types.IsMutable ( IsMutable (..) )
-import           Stack.Types.NamedComponent
-                   ( NamedComponent, benchComponents, exeComponents, isCBench
-                   , isCTest, renderComponent, testComponents
-                   )
-import           Stack.Types.Package
-                   ( InstallLocation (..), Installed (..), InstalledMap
-                   , LocalPackage (..), Package (..), PackageLibraries (..)
-                   , installedPackageIdentifier, packageIdent, packageIdentifier
-                   , runMemoizedWith
-                   )
-import           Stack.Types.PackageFile ( PackageWarning (..) )
-import           Stack.Types.Platform ( HasPlatform (..) )
-import           Stack.Types.Curator ( Curator (..) )
-import           Stack.Types.Runner ( HasRunner, globalOptsL, terminalL )
-import           Stack.Types.SourceMap ( Target )
-import           Stack.Types.Version ( withinRange )
-import qualified System.Directory as D
-import           System.Environment ( getExecutablePath, lookupEnv )
-import           System.FileLock
-                   ( SharedExclusive (Exclusive), withFileLock, withTryFileLock
-                   )
-import qualified System.FilePath as FP
-import           System.IO.Error ( isDoesNotExistError )
-import           System.PosixCompat.Files
-                   ( createLink, getFileStatus, modificationTime )
-import           System.Random ( randomIO )
-
--- | Has an executable been built or not?
-data ExecutableBuildStatus
-  = ExecutableBuilt
-  | ExecutableNotBuilt
-  deriving (Eq, Ord, Show)
-
--- | Fetch the packages necessary for a build, for example in combination with
--- a dry run.
-preFetch :: HasEnvConfig env => Plan -> RIO env ()
-preFetch plan
-  | Set.null pkgLocs = logDebug "Nothing to fetch"
-  | otherwise = do
-      logDebug $
-           "Prefetching: "
-        <> mconcat (L.intersperse ", " (display <$> Set.toList pkgLocs))
-      fetchPackages pkgLocs
- where
-  pkgLocs = Set.unions $ map toPkgLoc $ Map.elems $ planTasks plan
-
-  toPkgLoc task =
-    case taskType task of
-      TTLocalMutable{} -> Set.empty
-      TTRemotePackage _ _ pkgloc -> Set.singleton pkgloc
-
--- | Print a description of build plan for human consumption.
-printPlan :: (HasRunner env, HasTerm env) => Plan -> RIO env ()
-printPlan plan = do
-  case Map.elems $ planUnregisterLocal plan of
-    [] -> prettyInfo $
-               flow "No packages would be unregistered."
-            <> line
-    xs -> do
-      let unregisterMsg (ident, reason) = fillSep $
-              fromString (packageIdentifierString ident)
-            : [ parens $ flow (T.unpack reason) | not $ T.null reason ]
-      prettyInfo $
-           flow "Would unregister locally:"
-        <> line
-        <> bulletedList (map unregisterMsg xs)
-        <> line
-
-  case Map.elems $ planTasks plan of
-    [] -> prettyInfo $
-               flow "Nothing to build."
-            <> line
-    xs -> do
-      prettyInfo $
-           flow "Would build:"
-        <> line
-        <> bulletedList (map displayTask xs)
-        <> line
-
-  let hasTests = not . Set.null . testComponents . taskComponents
-      hasBenches = not . Set.null . benchComponents . taskComponents
-      tests = Map.elems $ Map.filter hasTests $ planFinals plan
-      benches = Map.elems $ Map.filter hasBenches $ planFinals plan
-
-  unless (null tests) $ do
-    prettyInfo $
-         flow "Would test:"
-      <> line
-      <> bulletedList (map displayTask tests)
-      <> line
-
-  unless (null benches) $ do
-    prettyInfo $
-         flow "Would benchmark:"
-      <> line
-      <> bulletedList (map displayTask benches)
-      <> line
-
-  case Map.toList $ planInstallExes plan of
-    [] -> prettyInfo $
-               flow "No executables to be installed."
-            <> line
-    xs -> do
-      let executableMsg (name, loc) = fillSep $
-              fromString (T.unpack name)
-            : "from"
-            : ( case loc of
-                  Snap -> "snapshot" :: StyleDoc
-                  Local -> "local" :: StyleDoc
-              )
-            : ["database."]
-      prettyInfo $
-           flow "Would install executables:"
-        <> line
-        <> bulletedList (map executableMsg xs)
-        <> line
-
--- | For a dry run
-displayTask :: Task -> StyleDoc
-displayTask task = fillSep $
-     [ fromString (packageIdentifierString (taskProvides task)) <> ":"
-     ,    "database="
-       <> ( case taskLocation task of
-              Snap -> "snapshot" :: StyleDoc
-              Local -> "local" :: StyleDoc
-          )
-       <> ","
-     ,    "source="
-       <> ( case taskType task of
-              TTLocalMutable lp -> pretty $ parent $ lpCabalFile lp
-              TTRemotePackage _ _ pl -> fromString $ T.unpack $ textDisplay pl
-          )
-       <> if Set.null missing
-            then mempty
-            else ","
-     ]
-  <> [ fillSep $
-           "after:"
-         : mkNarrativeList Nothing False
-             (map (fromString . packageIdentifierString) (Set.toList missing) :: [StyleDoc])
-     | not $ Set.null missing
-     ]
- where
-  missing = tcoMissing $ taskConfigOpts task
-
-data ExecuteEnv = ExecuteEnv
-  { eeConfigureLock  :: !(MVar ())
-  , eeInstallLock    :: !(MVar ())
-  , eeBuildOpts      :: !BuildOpts
-  , eeBuildOptsCLI   :: !BuildOptsCLI
-  , eeBaseConfigOpts :: !BaseConfigOpts
-  , eeGhcPkgIds      :: !(TVar (Map PackageIdentifier Installed))
-  , eeTempDir        :: !(Path Abs Dir)
-  , eeSetupHs        :: !(Path Abs File)
-    -- ^ Temporary Setup.hs for simple builds
-  , eeSetupShimHs    :: !(Path Abs File)
-    -- ^ Temporary SetupShim.hs, to provide access to initial-build-steps
-  , eeSetupExe       :: !(Maybe (Path Abs File))
-    -- ^ Compiled version of eeSetupHs
-  , eeCabalPkgVer    :: !Version
-  , eeTotalWanted    :: !Int
-  , eeLocals         :: ![LocalPackage]
-  , eeGlobalDB       :: !(Path Abs Dir)
-  , eeGlobalDumpPkgs :: !(Map GhcPkgId DumpPackage)
-  , eeSnapshotDumpPkgs :: !(TVar (Map GhcPkgId DumpPackage))
-  , eeLocalDumpPkgs  :: !(TVar (Map GhcPkgId DumpPackage))
-  , eeLogFiles       :: !(TChan (Path Abs Dir, Path Abs File))
-  , eeCustomBuilt    :: !(IORef (Set PackageName))
-    -- ^ Stores which packages with custom-setup have already had their
-    -- Setup.hs built.
-  , eeLargestPackageName :: !(Maybe Int)
-    -- ^ For nicer interleaved output: track the largest package name size
-  , eePathEnvVar :: !Text
-    -- ^ Value of the PATH environment variable
-  }
-
-buildSetupArgs :: [String]
-buildSetupArgs =
-  [ "-rtsopts"
-  , "-threaded"
-  , "-clear-package-db"
-  , "-global-package-db"
-  , "-hide-all-packages"
-  , "-package"
-  , "base"
-  , "-main-is"
-  , "StackSetupShim.mainOverride"
-  ]
-
-simpleSetupCode :: Builder
-simpleSetupCode = "import Distribution.Simple\nmain = defaultMain"
-
-simpleSetupHash :: String
-simpleSetupHash =
-    T.unpack
-  $ decodeUtf8
-  $ S.take 8
-  $ B64URL.encode
-  $ Mem.convert
-  $ hashWith SHA256
-  $ toStrictBytes
-  $ Data.ByteString.Builder.toLazyByteString
-  $  encodeUtf8Builder (T.pack (unwords buildSetupArgs))
-  <> setupGhciShimCode
-  <> simpleSetupCode
-
--- | Get a compiled Setup exe
-getSetupExe :: HasEnvConfig env
-            => Path Abs File -- ^ Setup.hs input file
-            -> Path Abs File -- ^ SetupShim.hs input file
-            -> Path Abs Dir -- ^ temporary directory
-            -> RIO env (Maybe (Path Abs File))
-getSetupExe setupHs setupShimHs tmpdir = do
-  wc <- view $ actualCompilerVersionL.whichCompilerL
-  platformDir <- platformGhcRelDir
-  config <- view configL
-  cabalVersionString <- view $ cabalVersionL.to versionString
-  actualCompilerVersionString <-
-    view $ actualCompilerVersionL.to compilerVersionString
-  platform <- view platformL
-  let baseNameS = concat
-        [ "Cabal-simple_"
-        , simpleSetupHash
-        , "_"
-        , cabalVersionString
-        , "_"
-        , actualCompilerVersionString
-        ]
-      exeNameS = baseNameS ++
-        case platform of
-          Platform _ Windows -> ".exe"
-          _ -> ""
-      outputNameS =
-        case wc of
-            Ghc -> exeNameS
-      setupDir =
-        view stackRootL config </>
-        relDirSetupExeCache </>
-        platformDir
-
-  exePath <- (setupDir </>) <$> parseRelFile exeNameS
-
-  exists <- liftIO $ D.doesFileExist $ toFilePath exePath
-
-  if exists
-    then pure $ Just exePath
-    else do
-      tmpExePath <- fmap (setupDir </>) $ parseRelFile $ "tmp-" ++ exeNameS
-      tmpOutputPath <-
-        fmap (setupDir </>) $ parseRelFile $ "tmp-" ++ outputNameS
-      ensureDir setupDir
-      let args = buildSetupArgs ++
-            [ "-package"
-            , "Cabal-" ++ cabalVersionString
-            , toFilePath setupHs
-            , toFilePath setupShimHs
-            , "-o"
-            , toFilePath tmpOutputPath
-            ]
-      compilerPath <- getCompilerPath
-      withWorkingDir (toFilePath tmpdir) $
-        proc (toFilePath compilerPath) args (\pc0 -> do
-          let pc = setStdout (useHandleOpen stderr) pc0
-          runProcess_ pc)
-            `catch` \ece ->
-              prettyThrowM $ SetupHsBuildFailure
-                (eceExitCode ece) Nothing compilerPath args Nothing []
-      renameFile tmpExePath exePath
-      pure $ Just exePath
-
--- | Execute a function that takes an 'ExecuteEnv'.
-withExecuteEnv ::
-     forall env a. HasEnvConfig env
-  => BuildOpts
-  -> BuildOptsCLI
-  -> BaseConfigOpts
-  -> [LocalPackage]
-  -> [DumpPackage] -- ^ global packages
-  -> [DumpPackage] -- ^ snapshot packages
-  -> [DumpPackage] -- ^ local packages
-  -> Maybe Int -- ^ largest package name, for nicer interleaved output
-  -> (ExecuteEnv -> RIO env a)
-  -> RIO env a
-withExecuteEnv bopts boptsCli baseConfigOpts locals globalPackages snapshotPackages localPackages mlargestPackageName inner =
-  createTempDirFunction stackProgName $ \tmpdir -> do
-    configLock <- liftIO $ newMVar ()
-    installLock <- liftIO $ newMVar ()
-    idMap <- liftIO $ newTVarIO Map.empty
-    config <- view configL
-
-    customBuiltRef <- newIORef Set.empty
-
-    -- Create files for simple setup and setup shim, if necessary
-    let setupSrcDir =
-            view stackRootL config </>
-            relDirSetupExeSrc
-    ensureDir setupSrcDir
-    setupFileName <- parseRelFile ("setup-" ++ simpleSetupHash ++ ".hs")
-    let setupHs = setupSrcDir </> setupFileName
-    setupHsExists <- doesFileExist setupHs
-    unless setupHsExists $ writeBinaryFileAtomic setupHs simpleSetupCode
-    setupShimFileName <-
-      parseRelFile ("setup-shim-" ++ simpleSetupHash ++ ".hs")
-    let setupShimHs = setupSrcDir </> setupShimFileName
-    setupShimHsExists <- doesFileExist setupShimHs
-    unless setupShimHsExists $
-      writeBinaryFileAtomic setupShimHs setupGhciShimCode
-    setupExe <- getSetupExe setupHs setupShimHs tmpdir
-
-    cabalPkgVer <- view cabalVersionL
-    globalDB <- view $ compilerPathsL.to cpGlobalDB
-    snapshotPackagesTVar <-
-      liftIO $ newTVarIO (toDumpPackagesByGhcPkgId snapshotPackages)
-    localPackagesTVar <-
-      liftIO $ newTVarIO (toDumpPackagesByGhcPkgId localPackages)
-    logFilesTChan <- liftIO $ atomically newTChan
-    let totalWanted = length $ filter lpWanted locals
-    pathEnvVar <- liftIO $ maybe mempty T.pack <$> lookupEnv "PATH"
-    inner ExecuteEnv
-      { eeBuildOpts = bopts
-      , eeBuildOptsCLI = boptsCli
-        -- Uncertain as to why we cannot run configures in parallel. This
-        -- appears to be a Cabal library bug. Original issue:
-        -- https://github.com/commercialhaskell/stack/issues/84. Ideally
-        -- we'd be able to remove this.
-      , eeConfigureLock = configLock
-      , eeInstallLock = installLock
-      , eeBaseConfigOpts = baseConfigOpts
-      , eeGhcPkgIds = idMap
-      , eeTempDir = tmpdir
-      , eeSetupHs = setupHs
-      , eeSetupShimHs = setupShimHs
-      , eeSetupExe = setupExe
-      , eeCabalPkgVer = cabalPkgVer
-      , eeTotalWanted = totalWanted
-      , eeLocals = locals
-      , eeGlobalDB = globalDB
-      , eeGlobalDumpPkgs = toDumpPackagesByGhcPkgId globalPackages
-      , eeSnapshotDumpPkgs = snapshotPackagesTVar
-      , eeLocalDumpPkgs = localPackagesTVar
-      , eeLogFiles = logFilesTChan
-      , eeCustomBuilt = customBuiltRef
-      , eeLargestPackageName = mlargestPackageName
-      , eePathEnvVar = pathEnvVar
-      } `finally` dumpLogs logFilesTChan totalWanted
- where
-  toDumpPackagesByGhcPkgId = Map.fromList . map (\dp -> (dpGhcPkgId dp, dp))
-
-  createTempDirFunction
-    | boptsKeepTmpFiles bopts = withKeepSystemTempDir
-    | otherwise = withSystemTempDir
-
-  dumpLogs :: TChan (Path Abs Dir, Path Abs File) -> Int -> RIO env ()
-  dumpLogs chan totalWanted = do
-    allLogs <- fmap reverse $ liftIO $ atomically drainChan
-    case allLogs of
-      -- No log files generated, nothing to dump
-      [] -> pure ()
-      firstLog:_ -> do
-        toDump <- view $ configL.to configDumpLogs
-        case toDump of
-          DumpAllLogs -> mapM_ (dumpLog "") allLogs
-          DumpWarningLogs -> mapM_ dumpLogIfWarning allLogs
-          DumpNoLogs
-              | totalWanted > 1 ->
-                  prettyInfoL
-                    [ flow "Build output has been captured to log files, use"
-                    , style Shell "--dump-logs"
-                    , flow "to see it on the console."
-                    ]
-              | otherwise -> pure ()
-        prettyInfoL
-          [ flow "Log files have been written to:"
-          , pretty (parent (snd firstLog))
-          ]
-
-    -- We only strip the colors /after/ we've dumped logs, so that we get pretty
-    -- colors in our dump output on the terminal.
-    colors <- shouldForceGhcColorFlag
-    when colors $ liftIO $ mapM_ (stripColors . snd) allLogs
-   where
-    drainChan :: STM [(Path Abs Dir, Path Abs File)]
-    drainChan = do
-      mx <- tryReadTChan chan
-      case mx of
-        Nothing -> pure []
-        Just x -> do
-          xs <- drainChan
-          pure $ x:xs
-
-  dumpLogIfWarning :: (Path Abs Dir, Path Abs File) -> RIO env ()
-  dumpLogIfWarning (pkgDir, filepath) = do
-    firstWarning <- withSourceFile (toFilePath filepath) $ \src ->
-         runConduit
-       $ src
-      .| CT.decodeUtf8Lenient
-      .| CT.lines
-      .| CL.map stripCR
-      .| CL.filter isWarning
-      .| CL.take 1
-    unless (null firstWarning) $ dumpLog " due to warnings" (pkgDir, filepath)
-
-  isWarning :: Text -> Bool
-  isWarning t = ": Warning:" `T.isSuffixOf` t -- prior to GHC 8
-             || ": warning:" `T.isInfixOf` t -- GHC 8 is slightly different
-             || "mwarning:" `T.isInfixOf` t -- colorized output
-
-  dumpLog :: String -> (Path Abs Dir, Path Abs File) -> RIO env ()
-  dumpLog msgSuffix (pkgDir, filepath) = do
-    prettyNote $
-         fillSep
-           ( ( fillSep
-                 ( flow "Dumping log file"
-                 : [ flow msgSuffix | not (L.null msgSuffix) ]
-                 )
-             <> ":"
-             )
-           : [ pretty filepath <> "." ]
-           )
-      <> line
-    compilerVer <- view actualCompilerVersionL
-    withSourceFile (toFilePath filepath) $ \src ->
-         runConduit
-       $ src
-      .| CT.decodeUtf8Lenient
-      .| mungeBuildOutput ExcludeTHLoading ConvertPathsToAbsolute pkgDir compilerVer
-      .| CL.mapM_ (logInfo . display)
-    prettyNote $
-         fillSep
-           [ flow "End of log file:"
-           , pretty filepath <> "."
-           ]
-      <> line
-
-  stripColors :: Path Abs File -> IO ()
-  stripColors fp = do
-    let colorfp = toFilePath fp ++ "-color"
-    withSourceFile (toFilePath fp) $ \src ->
-      withSinkFile colorfp $ \sink ->
-      runConduit $ src .| sink
-    withSourceFile colorfp $ \src ->
-      withSinkFile (toFilePath fp) $ \sink ->
-      runConduit $ src .| noColors .| sink
-
-   where
-    noColors = do
-      CB.takeWhile (/= 27) -- ESC
-      mnext <- CB.head
-      case mnext of
-        Nothing -> pure ()
-        Just x -> assert (x == 27) $ do
-          -- Color sequences always end with an m
-          CB.dropWhile (/= 109) -- m
-          CB.drop 1 -- drop the m itself
-          noColors
-
--- | Perform the actual plan
-executePlan :: HasEnvConfig env
-            => BuildOptsCLI
-            -> BaseConfigOpts
-            -> [LocalPackage]
-            -> [DumpPackage] -- ^ global packages
-            -> [DumpPackage] -- ^ snapshot packages
-            -> [DumpPackage] -- ^ local packages
-            -> InstalledMap
-            -> Map PackageName Target
-            -> Plan
-            -> RIO env ()
-executePlan boptsCli baseConfigOpts locals globalPackages snapshotPackages localPackages installedMap targets plan = do
-  logDebug "Executing the build plan"
-  bopts <- view buildOptsL
-  withExecuteEnv
-    bopts
-    boptsCli
-    baseConfigOpts
-    locals
-    globalPackages
-    snapshotPackages
-    localPackages
-    mlargestPackageName
-    (executePlan' installedMap targets plan)
-
-  copyExecutables (planInstallExes plan)
-
-  config <- view configL
-  menv' <- liftIO $ configProcessContextSettings config EnvSettings
-             { esIncludeLocals = True
-             , esIncludeGhcPackagePath = True
-             , esStackExe = True
-             , esLocaleUtf8 = False
-             , esKeepGhcRts = False
-             }
-  withProcessContext menv' $
-    forM_ (boptsCLIExec boptsCli) $ \(cmd, args) ->
-    proc cmd args runProcess_
- where
-  mlargestPackageName =
-    Set.lookupMax $
-    Set.map (length . packageNameString) $
-    Map.keysSet (planTasks plan) <> Map.keysSet (planFinals plan)
-
-copyExecutables ::
-       HasEnvConfig env
-    => Map Text InstallLocation
-    -> RIO env ()
-copyExecutables exes | Map.null exes = pure ()
-copyExecutables exes = do
-  snapBin <- (</> bindirSuffix) <$> installationRootDeps
-  localBin <- (</> bindirSuffix) <$> installationRootLocal
-  compilerSpecific <- boptsInstallCompilerTool <$> view buildOptsL
-  destDir <- if compilerSpecific
-               then bindirCompilerTools
-               else view $ configL.to configLocalBin
-  ensureDir destDir
-
-  destDir' <- liftIO . D.canonicalizePath . toFilePath $ destDir
-
-  platform <- view platformL
-  let ext =
-        case platform of
-          Platform _ Windows -> ".exe"
-          _ -> ""
-
-  currExe <- liftIO getExecutablePath -- needed for windows, see below
-
-  installed <- forMaybeM (Map.toList exes) $ \(name, loc) -> do
-    let bindir =
-            case loc of
-                Snap -> snapBin
-                Local -> localBin
-    mfp <- forgivingResolveFile bindir (T.unpack name ++ ext)
-      >>= rejectMissingFile
-    case mfp of
-      Nothing -> do
-        prettyWarnL
-          [ flow "Couldn't find executable"
-          , style Current (fromString $ T.unpack name)
-          , flow "in directory"
-          , pretty bindir <> "."
-          ]
-        pure Nothing
-      Just file -> do
-        let destFile = destDir' FP.</> T.unpack name ++ ext
-        prettyInfoL
-          [ flow "Copying from"
-          , pretty file
-          , "to"
-          , style File (fromString destFile) <> "."
-          ]
-
-        liftIO $ case platform of
-          Platform _ Windows | FP.equalFilePath destFile currExe ->
-              windowsRenameCopy (toFilePath file) destFile
-          _ -> D.copyFile (toFilePath file) destFile
-        pure $ Just (name <> T.pack ext)
-
-  unless (null installed) $ do
-    prettyInfo $
-         fillSep
-           [ flow "Copied executables to"
-           , pretty destDir <> ":"
-           ]
-      <> line
-      <> bulletedList
-           (map (fromString . T.unpack . textDisplay) installed :: [StyleDoc])
-  unless compilerSpecific $ warnInstallSearchPathIssues destDir' installed
-
-
--- | Windows can't write over the current executable. Instead, we rename the
--- current executable to something else and then do the copy.
-windowsRenameCopy :: FilePath -> FilePath -> IO ()
-windowsRenameCopy src dest = do
-  D.copyFile src new
-  D.renameFile dest old
-  D.renameFile new dest
- where
-  new = dest ++ ".new"
-  old = dest ++ ".old"
-
--- | Perform the actual plan (internal)
-executePlan' :: HasEnvConfig env
-             => InstalledMap
-             -> Map PackageName Target
-             -> Plan
-             -> ExecuteEnv
-             -> RIO env ()
-executePlan' installedMap0 targets plan ee@ExecuteEnv {..} = do
-  when (toCoverage $ boptsTestOpts eeBuildOpts) deleteHpcReports
-  cv <- view actualCompilerVersionL
-  case nonEmpty . Map.toList $ planUnregisterLocal plan of
-    Nothing -> pure ()
-    Just ids -> do
-      localDB <- packageDatabaseLocal
-      unregisterPackages cv localDB ids
-
-  liftIO $ atomically $ modifyTVar' eeLocalDumpPkgs $ \initMap ->
-    foldl' (flip Map.delete) initMap $ Map.keys (planUnregisterLocal plan)
-
-  run <- askRunInIO
-
-  -- If running tests concurrently with each other, then create an MVar
-  -- which is empty while each test is being run.
-  concurrentTests <- view $ configL.to configConcurrentTests
-  mtestLock <- if concurrentTests
-                 then pure Nothing
-                 else Just <$> liftIO (newMVar ())
-
-  let actions = concatMap (toActions installedMap' mtestLock run ee) $
-        Map.elems $ Map.merge
-          (Map.mapMissing (\_ b -> (Just b, Nothing)))
-          (Map.mapMissing (\_ f -> (Nothing, Just f)))
-          (Map.zipWithMatched (\_ b f -> (Just b, Just f)))
-          (planTasks plan)
-          (planFinals plan)
-  threads <- view $ configL.to configJobs
-  let keepGoing =
-        fromMaybe (not (Map.null (planFinals plan))) (boptsKeepGoing eeBuildOpts)
-  terminal <- view terminalL
-  terminalWidth <- view termWidthL
-  errs <- liftIO $ runActions threads keepGoing actions $
-    \doneVar actionsVar -> do
-      let total = length actions
-          loop prev
-            | prev == total =
-                run $ logStickyDone
-                  ( "Completed " <> display total <> " action(s).")
-            | otherwise = do
-                inProgress <- readTVarIO actionsVar
-                let packageNames = map
-                      (\(ActionId pkgID _) -> pkgName pkgID)
-                      (toList inProgress)
-                    nowBuilding :: [PackageName] -> Utf8Builder
-                    nowBuilding []    = ""
-                    nowBuilding names = mconcat $
-                        ": "
-                      : L.intersperse ", "
-                          (map (fromString . packageNameString) names)
-                    progressFormat = boptsProgressBar eeBuildOpts
-                    progressLine prev' total' =
-                         "Progress "
-                      <> display prev' <> "/" <> display total'
-                      <> if progressFormat == CountOnlyBar
-                           then mempty
-                           else nowBuilding packageNames
-                    ellipsize n text =
-                      if T.length text <= n || progressFormat /= CappedBar
-                        then text
-                        else T.take (n - 1) text <> "…"
-                when (terminal && progressFormat /= NoBar) $
-                  run $ logSticky $ display $ ellipsize terminalWidth $
-                    utf8BuilderToText $ progressLine prev total
-                done <- atomically $ do
-                  done <- readTVar doneVar
-                  check $ done /= prev
-                  pure done
-                loop done
-      when (total > 1) $ loop 0
-  when (toCoverage $ boptsTestOpts eeBuildOpts) $ do
-    generateHpcUnifiedReport
-    generateHpcMarkupIndex
-  unless (null errs) $
-    prettyThrowM $ ExecutionFailure errs
-  when (boptsHaddock eeBuildOpts) $ do
-    snapshotDumpPkgs <- liftIO (readTVarIO eeSnapshotDumpPkgs)
-    localDumpPkgs <- liftIO (readTVarIO eeLocalDumpPkgs)
-    generateLocalHaddockIndex eeBaseConfigOpts localDumpPkgs eeLocals
-    generateDepsHaddockIndex
-      eeBaseConfigOpts
-      eeGlobalDumpPkgs
-      snapshotDumpPkgs
-      localDumpPkgs
-      eeLocals
-    generateSnapHaddockIndex eeBaseConfigOpts eeGlobalDumpPkgs snapshotDumpPkgs
-    when (boptsOpenHaddocks eeBuildOpts) $ do
-      let planPkgs, localPkgs, installedPkgs, availablePkgs
-            :: Map PackageName (PackageIdentifier, InstallLocation)
-          planPkgs = Map.map (taskProvides &&& taskLocation) (planTasks plan)
-          localPkgs =
-            Map.fromList
-              [ (packageName p, (packageIdentifier p, Local))
-              | p <- map lpPackage eeLocals
-              ]
-          installedPkgs =
-            Map.map (swap . second installedPackageIdentifier) installedMap'
-          availablePkgs = Map.unions [planPkgs, localPkgs, installedPkgs]
-      openHaddocksInBrowser eeBaseConfigOpts availablePkgs (Map.keysSet targets)
- where
-  installedMap' = Map.difference installedMap0
-                $ Map.fromList
-                $ map (\(ident, _) -> (pkgName ident, ()))
-                $ Map.elems
-                $ planUnregisterLocal plan
-
-unregisterPackages ::
-     (HasCompiler env, HasPlatform env, HasProcessContext env, HasTerm env)
-  => ActualCompiler
-  -> Path Abs Dir
-  -> NonEmpty (GhcPkgId, (PackageIdentifier, Text))
-  -> RIO env ()
-unregisterPackages cv localDB ids = do
-  let logReason ident reason =
-        prettyInfoL
-          (  [ fromString (packageIdentifierString ident) <> ":"
-             , "unregistering"
-             ]
-          <> [ parens (flow $ T.unpack reason) | not $ T.null reason ]
-          )
-  let unregisterSinglePkg select (gid, (ident, reason)) = do
-        logReason ident reason
-        pkg <- getGhcPkgExe
-        unregisterGhcPkgIds True pkg localDB $ select ident gid :| []
-  case cv of
-    -- GHC versions >= 8.2.1 support batch unregistering of packages. See
-    -- https://gitlab.haskell.org/ghc/ghc/issues/12637
-    ACGhc v | v >= mkVersion [8, 2, 1] -> do
-      platform <- view platformL
-      -- According to
-      -- https://support.microsoft.com/en-us/help/830473/command-prompt-cmd-exe-command-line-string-limitation
-      -- the maximum command line length on Windows since XP is 8191 characters.
-      -- We use conservative batch size of 100 ids on this OS thus argument name
-      -- '-ipid', package name, its version and a hash should fit well into this
-      -- limit. On Unix-like systems we're limited by ARG_MAX which is normally
-      -- hundreds of kilobytes so batch size of 500 should work fine.
-      let batchSize = case platform of
-            Platform _ Windows -> 100
-            _ -> 500
-      let chunksOfNE size = mapMaybe nonEmpty . chunksOf size . NonEmpty.toList
-      for_ (chunksOfNE batchSize ids) $ \batch -> do
-        for_ batch $ \(_, (ident, reason)) -> logReason ident reason
-        pkg <- getGhcPkgExe
-        unregisterGhcPkgIds True pkg localDB $ fmap (Right . fst) batch
-
-    -- GHC versions >= 7.9 support unregistering of packages via their GhcPkgId.
-    ACGhc v | v >= mkVersion [7, 9] ->
-      for_ ids . unregisterSinglePkg $ \_ident gid -> Right gid
-
-    _ -> for_ ids . unregisterSinglePkg $ \ident _gid -> Left ident
-
-toActions :: HasEnvConfig env
-          => InstalledMap
-          -> Maybe (MVar ())
-          -> (RIO env () -> IO ())
-          -> ExecuteEnv
-          -> (Maybe Task, Maybe Task) -- build and final
-          -> [Action]
-toActions installedMap mtestLock runInBase ee (mbuild, mfinal) =
-  abuild ++ afinal
- where
-  abuild = case mbuild of
-    Nothing -> []
-    Just task@Task {..} ->
-      [ Action
-          { actionId = ActionId taskProvides ATBuild
-          , actionDeps =
-              Set.map (`ActionId` ATBuild) (tcoMissing taskConfigOpts)
-          , actionDo =
-              \ac -> runInBase $ singleBuild ac ee task installedMap False
-          , actionConcurrency = ConcurrencyAllowed
-          }
-      ]
-  afinal = case mfinal of
-    Nothing -> []
-    Just task@Task {..} ->
-      (if taskAllInOne then id else (:)
-          Action
-              { actionId = ActionId taskProvides ATBuildFinal
-              , actionDeps = addBuild
-                  (Set.map (`ActionId` ATBuild) (tcoMissing taskConfigOpts))
-              , actionDo =
-                  \ac -> runInBase $ singleBuild ac ee task installedMap True
-              , actionConcurrency = ConcurrencyAllowed
-              }) $
-      -- These are the "final" actions - running tests and benchmarks.
-      (if Set.null tests then id else (:)
-          Action
-              { actionId = ActionId taskProvides ATRunTests
-              , actionDeps = finalDeps
-              , actionDo = \ac -> withLock mtestLock $ runInBase $
-                  singleTest topts (Set.toList tests) ac ee task installedMap
-              -- Always allow tests tasks to run concurrently with
-              -- other tasks, particularly build tasks. Note that
-              -- 'mtestLock' can optionally make it so that only
-              -- one test is run at a time.
-              , actionConcurrency = ConcurrencyAllowed
-              }) $
-      (if Set.null benches then id else (:)
-          Action
-              { actionId = ActionId taskProvides ATRunBenchmarks
-              , actionDeps = finalDeps
-              , actionDo = \ac -> runInBase $
-                  singleBench
-                    beopts
-                    (Set.toList benches)
-                    ac
-                    ee
-                    task
-                    installedMap
-                -- Never run benchmarks concurrently with any other task, see
-                -- #3663
-              , actionConcurrency = ConcurrencyDisallowed
-              })
-      []
-     where
-      comps = taskComponents task
-      tests = testComponents comps
-      benches = benchComponents comps
-      finalDeps =
-        if taskAllInOne
-          then addBuild mempty
-          else Set.singleton (ActionId taskProvides ATBuildFinal)
-      addBuild =
-        case mbuild of
-          Nothing -> id
-          Just _ -> Set.insert $ ActionId taskProvides ATBuild
-  withLock Nothing f = f
-  withLock (Just lock) f = withMVar lock $ \() -> f
-  bopts = eeBuildOpts ee
-  topts = boptsTestOpts bopts
-  beopts = boptsBenchmarkOpts bopts
-
--- | Generate the ConfigCache
-getConfigCache ::
-     HasEnvConfig env
-  => ExecuteEnv
-  -> Task
-  -> InstalledMap
-  -> Bool
-  -> Bool
-  -> RIO env (Map PackageIdentifier GhcPkgId, ConfigCache)
-getConfigCache ExecuteEnv {..} task@Task {..} installedMap enableTest enableBench = do
-  let extra =
-        -- We enable tests if the test suite dependencies are already
-        -- installed, so that we avoid unnecessary recompilation based on
-        -- cabal_macros.h changes when switching between 'stack build' and
-        -- 'stack test'. See:
-        -- https://github.com/commercialhaskell/stack/issues/805
-        case taskType of
-          TTLocalMutable _ ->
-            -- FIXME: make this work with exact-configuration.
-            -- Not sure how to plumb the info atm. See
-            -- https://github.com/commercialhaskell/stack/issues/2049
-            [ "--enable-tests" | enableTest] ++
-            [ "--enable-benchmarks" | enableBench]
-          TTRemotePackage{} -> []
-  idMap <- liftIO $ readTVarIO eeGhcPkgIds
-  let getMissing ident =
-        case Map.lookup ident idMap of
-          Nothing
-              -- Expect to instead find it in installedMap if it's
-              -- an initialBuildSteps target.
-              | boptsCLIInitialBuildSteps eeBuildOptsCLI && taskIsTarget task,
-                Just (_, installed) <- Map.lookup (pkgName ident) installedMap
-                  -> installedToGhcPkgId ident installed
-          Just installed -> installedToGhcPkgId ident installed
-          _ -> throwM $ PackageIdMissingBug ident
-      installedToGhcPkgId ident (Library ident' x _) =
-        assert (ident == ident') $ Just (ident, x)
-      installedToGhcPkgId _ (Executable _) = Nothing
-      missing' = Map.fromList $ mapMaybe getMissing $ Set.toList missing
-      TaskConfigOpts missing mkOpts = taskConfigOpts
-      opts = mkOpts missing'
-      allDeps = Set.fromList $ Map.elems missing' ++ Map.elems taskPresent
-      cache = ConfigCache
-        { configCacheOpts = opts
-            { coNoDirs = coNoDirs opts ++ map T.unpack extra
-            }
-        , configCacheDeps = allDeps
-        , configCacheComponents =
-            case taskType of
-              TTLocalMutable lp ->
-                Set.map (encodeUtf8 . renderComponent) $ lpComponents lp
-              TTRemotePackage{} -> Set.empty
-        , configCacheHaddock = taskBuildHaddock
-        , configCachePkgSrc = taskCachePkgSrc
-        , configCachePathEnvVar = eePathEnvVar
-        }
-      allDepsMap = Map.union missing' taskPresent
-  pure (allDepsMap, cache)
-
--- | Ensure that the configuration for the package matches what is given
-ensureConfig :: HasEnvConfig env
-             => ConfigCache -- ^ newConfigCache
-             -> Path Abs Dir -- ^ package directory
-             -> ExecuteEnv
-             -> RIO env () -- ^ announce
-             -> (ExcludeTHLoading -> [String] -> RIO env ()) -- ^ cabal
-             -> Path Abs File -- ^ Cabal file
-             -> Task
-             -> RIO env Bool
-ensureConfig newConfigCache pkgDir ExecuteEnv {..} announce cabal cabalfp task = do
-  newCabalMod <-
-    liftIO $ modificationTime <$> getFileStatus (toFilePath cabalfp)
-  setupConfigfp <- setupConfigFromDir pkgDir
-  let getNewSetupConfigMod =
-        liftIO $ either (const Nothing) (Just . modificationTime) <$>
-        tryJust
-          (guard . isDoesNotExistError)
-          (getFileStatus (toFilePath setupConfigfp))
-  newSetupConfigMod <- getNewSetupConfigMod
-  newProjectRoot <- S8.pack . toFilePath <$> view projectRootL
-  -- See https://github.com/commercialhaskell/stack/issues/3554
-  taskAnyMissingHack <-
-    view $ actualCompilerVersionL.to getGhcVersion.to (< mkVersion [8, 4])
-  needConfig <-
-    if   boptsReconfigure eeBuildOpts
-      || (taskAnyMissing task && taskAnyMissingHack)
-      then pure True
-      else do
-        -- We can ignore the components portion of the config
-        -- cache, because it's just used to inform 'construct
-        -- plan that we need to plan to build additional
-        -- components. These components don't affect the actual
-        -- package configuration.
-        let ignoreComponents cc = cc { configCacheComponents = Set.empty }
-        -- Determine the old and new configuration in the local directory, to
-        -- determine if we need to reconfigure.
-        mOldConfigCache <- tryGetConfigCache pkgDir
-
-        mOldCabalMod <- tryGetCabalMod pkgDir
-
-        -- Cabal's setup-config is created per OS/Cabal version, multiple
-        -- projects using the same package could get a conflict because of this
-        mOldSetupConfigMod <- tryGetSetupConfigMod pkgDir
-        mOldProjectRoot <- tryGetPackageProjectRoot pkgDir
-
-        pure $
-                fmap ignoreComponents mOldConfigCache
-             /= Just (ignoreComponents newConfigCache)
-          || mOldCabalMod /= Just newCabalMod
-          || mOldSetupConfigMod /= newSetupConfigMod
-          || mOldProjectRoot /= Just newProjectRoot
-  let ConfigureOpts dirs nodirs = configCacheOpts newConfigCache
-
-  when (taskBuildTypeConfig task) $
-    -- When build-type is Configure, we need to have a configure script in the
-    -- local directory. If it doesn't exist, build it with autoreconf -i. See:
-    -- https://github.com/commercialhaskell/stack/issues/3534
-    ensureConfigureScript pkgDir
-
-  when needConfig $ withMVar eeConfigureLock $ \_ -> do
-    deleteCaches pkgDir
-    announce
-    cp <- view compilerPathsL
-    let (GhcPkgExe pkgPath) = cpPkg cp
-    let programNames =
-          case cpWhich cp of
-            Ghc ->
-              [ ("ghc", toFilePath (cpCompiler cp))
-              , ("ghc-pkg", toFilePath pkgPath)
-              ]
-    exes <- forM programNames $ \(name, file) -> do
-      mpath <- findExecutable file
-      pure $ case mpath of
-          Left _ -> []
-          Right x -> pure $ concat ["--with-", name, "=", x]
-    -- Configure cabal with arguments determined by
-    -- Stack.Types.Build.configureOpts
-    cabal KeepTHLoading $ "configure" : concat
-      [ concat exes
-      , dirs
-      , nodirs
-      ]
-    -- Only write the cache for local packages.  Remote packages are built in a
-    -- temporary directory so the cache would never be used anyway.
-    case taskType task of
-      TTLocalMutable{} -> writeConfigCache pkgDir newConfigCache
-      TTRemotePackage{} -> pure ()
-    writeCabalMod pkgDir newCabalMod
-    -- This file gets updated one more time by the configure step, so get the
-    -- most recent value. We could instead change our logic above to check if
-    -- our config mod file is newer than the file above, but this seems
-    -- reasonable too.
-    getNewSetupConfigMod >>= writeSetupConfigMod pkgDir
-    writePackageProjectRoot pkgDir newProjectRoot
-  pure needConfig
-
--- | Make a padded prefix for log messages
-packageNamePrefix :: ExecuteEnv -> PackageName -> String
-packageNamePrefix ee name' =
-  let name = packageNameString name'
-      paddedName =
-        case eeLargestPackageName ee of
-          Nothing -> name
-          Just len ->
-            assert (len >= length name) $ take len $ name ++ L.repeat ' '
-  in  paddedName <> "> "
-
-announceTask ::
-     HasLogFunc env
-  => ExecuteEnv
-  -> Task
-  -> Utf8Builder
-  -> RIO env ()
-announceTask ee task action = logInfo $
-  fromString (packageNamePrefix ee (pkgName (taskProvides task))) <> action
-
-prettyAnnounceTask ::
-     HasTerm env
-  => ExecuteEnv
-  -> Task
-  -> StyleDoc
-  -> RIO env ()
-prettyAnnounceTask ee task action = prettyInfo $
-  fromString (packageNamePrefix ee (pkgName (taskProvides task))) <> action
-
--- | Ensure we're the only action using the directory.  See
--- <https://github.com/commercialhaskell/stack/issues/2730>
-withLockedDistDir ::
-     forall env a. HasEnvConfig env
-  => (StyleDoc -> RIO env ()) -- ^ A pretty announce function
-  -> Path Abs Dir -- ^ root directory for package
-  -> RIO env a
-  -> RIO env a
-withLockedDistDir announce root inner = do
-  distDir <- distRelativeDir
-  let lockFP = root </> distDir </> relFileBuildLock
-  ensureDir $ parent lockFP
-
-  mres <-
-    withRunInIO $ \run ->
-    withTryFileLock (toFilePath lockFP) Exclusive $ \_lock ->
-    run inner
-
-  case mres of
-    Just res -> pure res
-    Nothing -> do
-      let complainer :: Companion (RIO env)
-          complainer delay = do
-            delay 5000000 -- 5 seconds
-            announce $ fillSep
-              [ flow "blocking for directory lock on"
-              , pretty lockFP
-              ]
-            forever $ do
-              delay 30000000 -- 30 seconds
-              announce $ fillSep
-                [ flow "still blocking for directory lock on"
-                , pretty lockFP <> ";"
-                , flow "maybe another Stack process is running?"
-                ]
-      withCompanion complainer $
-        \stopComplaining ->
-          withRunInIO $ \run ->
-            withFileLock (toFilePath lockFP) Exclusive $ \_ ->
-              run $ stopComplaining *> inner
-
--- | How we deal with output from GHC, either dumping to a log file or the
--- console (with some prefix).
-data OutputType
-  = OTLogFile !(Path Abs File) !Handle
-  | OTConsole !(Maybe Utf8Builder)
-
--- | This sets up a context for executing build steps which need to run
--- Cabal (via a compiled Setup.hs).  In particular it does the following:
---
--- * Ensures the package exists in the file system, downloading if necessary.
---
--- * Opens a log file if the built output shouldn't go to stderr.
---
--- * Ensures that either a simple Setup.hs is built, or the package's
---   custom setup is built.
---
--- * Provides the user a function with which run the Cabal process.
-withSingleContext ::
-     forall env a. HasEnvConfig env
-  => ActionContext
-  -> ExecuteEnv
-  -> Task
-  -> Map PackageIdentifier GhcPkgId
-     -- ^ All dependencies' package ids to provide to Setup.hs.
-  -> Maybe String
-  -> (  Package        -- Package info
-     -> Path Abs File  -- Cabal file path
-     -> Path Abs Dir   -- Package root directory file path
-        -- Note that the `Path Abs Dir` argument is redundant with the
-        -- `Path Abs File` argument, but we provide both to avoid recalculating
-        -- `parent` of the `File`.
-     -> (KeepOutputOpen -> ExcludeTHLoading -> [String] -> RIO env ())
-        -- Function to run Cabal with args
-     -> (Utf8Builder -> RIO env ())
-        -- An plain 'announce' function, for different build phases
-     -> OutputType
-     -> RIO env a)
-  -> RIO env a
-withSingleContext ActionContext {..} ee@ExecuteEnv {..} task@Task {..} allDeps msuffix inner0 =
-  withPackage $ \package cabalfp pkgDir ->
-  withOutputType pkgDir package $ \outputType ->
-  withCabal package pkgDir outputType $ \cabal ->
-  inner0 package cabalfp pkgDir cabal announce outputType
- where
-  announce = announceTask ee task
-  prettyAnnounce = prettyAnnounceTask ee task
-
-  wanted =
-    case taskType of
-      TTLocalMutable lp -> lpWanted lp
-      TTRemotePackage{} -> False
-
-  -- Output to the console if this is the last task, and the user asked to build
-  -- it specifically. When the action is a 'ConcurrencyDisallowed' action
-  -- (benchmarks), then we can also be sure to have exclusive access to the
-  -- console, so output is also sent to the console in this case.
-  --
-  -- See the discussion on #426 for thoughts on sending output to the console
-  --from concurrent tasks.
-  console =
-       (  wanted
-       && all
-            (\(ActionId ident _) -> ident == taskProvides)
-            (Set.toList acRemaining)
-       && eeTotalWanted == 1
-       )
-    || acConcurrency == ConcurrencyDisallowed
-
-  withPackage inner =
-    case taskType of
-      TTLocalMutable lp -> do
-        let root = parent $ lpCabalFile lp
-        withLockedDistDir prettyAnnounce root $
-          inner (lpPackage lp) (lpCabalFile lp) root
-      TTRemotePackage _ package pkgloc -> do
-          suffix <- parseRelDir $ packageIdentifierString $ packageIdent package
-          let dir = eeTempDir </> suffix
-          unpackPackageLocation dir pkgloc
-
-          -- See: https://github.com/commercialhaskell/stack/issues/157
-          distDir <- distRelativeDir
-          let oldDist = dir </> relDirDist
-              newDist = dir </> distDir
-          exists <- doesDirExist oldDist
-          when exists $ do
-            -- Previously used takeDirectory, but that got confused
-            -- by trailing slashes, see:
-            -- https://github.com/commercialhaskell/stack/issues/216
-            --
-            -- Instead, use Path which is a bit more resilient
-            ensureDir $ parent newDist
-            renameDir oldDist newDist
-
-          let name = pkgName taskProvides
-          cabalfpRel <- parseRelFile $ packageNameString name ++ ".cabal"
-          let cabalfp = dir </> cabalfpRel
-          inner package cabalfp dir
-
-  withOutputType pkgDir package inner
-    -- Not in interleaved mode. When building a single wanted package, dump
-    -- to the console with no prefix.
-    | console = inner $ OTConsole Nothing
-
-    -- If the user requested interleaved output, dump to the console with a
-    -- prefix.
-    | boptsInterleavedOutput eeBuildOpts = inner $
-        OTConsole $ Just $ fromString (packageNamePrefix ee $ packageName package)
-
-    -- Neither condition applies, dump to a file.
-    | otherwise = do
-        logPath <- buildLogPath package msuffix
-        ensureDir (parent logPath)
-        let fp = toFilePath logPath
-
-        -- We only want to dump logs for local non-dependency packages
-        case taskType of
-          TTLocalMutable lp | lpWanted lp ->
-              liftIO $ atomically $ writeTChan eeLogFiles (pkgDir, logPath)
-          _ -> pure ()
-
-        withBinaryFile fp WriteMode $ \h -> inner $ OTLogFile logPath h
-
-  withCabal ::
-       Package
-    -> Path Abs Dir
-    -> OutputType
-    -> (  (KeepOutputOpen -> ExcludeTHLoading -> [String] -> RIO env ())
-       -> RIO env a
-       )
-    -> RIO env a
-  withCabal package pkgDir outputType inner = do
-    config <- view configL
-    unless (configAllowDifferentUser config) $
-      checkOwnership (pkgDir </> configWorkDir config)
-    let envSettings = EnvSettings
-          { esIncludeLocals = taskLocation task == Local
-          , esIncludeGhcPackagePath = False
-          , esStackExe = False
-          , esLocaleUtf8 = True
-          , esKeepGhcRts = False
-          }
-    menv <- liftIO $ configProcessContextSettings config envSettings
-    distRelativeDir' <- distRelativeDir
-    esetupexehs <-
-      -- Avoid broken Setup.hs files causing problems for simple build
-      -- types, see:
-      -- https://github.com/commercialhaskell/stack/issues/370
-      case (packageBuildType package, eeSetupExe) of
-        (C.Simple, Just setupExe) -> pure $ Left setupExe
-        _ -> liftIO $ Right <$> getSetupHs pkgDir
-    inner $ \keepOutputOpen stripTHLoading args -> do
-      let cabalPackageArg
-            -- Omit cabal package dependency when building
-            -- Cabal. See
-            -- https://github.com/commercialhaskell/stack/issues/1356
-            | packageName package == mkPackageName "Cabal" = []
-            | otherwise =
-                ["-package=" ++ packageIdentifierString
-                                    (PackageIdentifier cabalPackageName
-                                                      eeCabalPkgVer)]
-          packageDBArgs =
-            ( "-clear-package-db"
-            : "-global-package-db"
-            : map
-                (("-package-db=" ++) . toFilePathNoTrailingSep)
-                (bcoExtraDBs eeBaseConfigOpts)
-            ) ++
-            ( (  "-package-db="
-              ++ toFilePathNoTrailingSep (bcoSnapDB eeBaseConfigOpts)
-              )
-            : (  "-package-db="
-              ++ toFilePathNoTrailingSep (bcoLocalDB eeBaseConfigOpts)
-              )
-            : ["-hide-all-packages"]
-            )
-
-          warnCustomNoDeps :: RIO env ()
-          warnCustomNoDeps =
-            case (taskType, packageBuildType package) of
-              (TTLocalMutable lp, C.Custom) | lpWanted lp ->
-                prettyWarnL
-                  [ flow "Package"
-                  , fromString $ packageNameString $ packageName package
-                  , flow "uses a custom Cabal build, but does not use a \
-                         \custom-setup stanza"
-                  ]
-              _ -> pure ()
-
-          getPackageArgs :: Path Abs Dir -> RIO env [String]
-          getPackageArgs setupDir =
-            case packageSetupDeps package of
-              -- The package is using the Cabal custom-setup
-              -- configuration introduced in Cabal 1.24. In
-              -- this case, the package is providing an
-              -- explicit list of dependencies, and we
-              -- should simply use all of them.
-              Just customSetupDeps -> do
-                unless (Map.member (mkPackageName "Cabal") customSetupDeps) $
-                  prettyWarnL
-                    [ fromString $ packageNameString $ packageName package
-                    , flow "has a setup-depends field, but it does not mention \
-                           \a Cabal dependency. This is likely to cause build \
-                           \errors."
-                    ]
-                matchedDeps <-
-                  forM (Map.toList customSetupDeps) $ \(name, range) -> do
-                    let matches (PackageIdentifier name' version) =
-                          name == name' &&
-                          version `withinRange` range
-                    case filter (matches . fst) (Map.toList allDeps) of
-                      x:xs -> do
-                        unless (null xs) $
-                          prettyWarnL
-                            [ flow "Found multiple installed packages for \
-                                   \custom-setup dep:"
-                            , style Current (fromString $ packageNameString name) <> "."
-                            ]
-                        pure ("-package-id=" ++ ghcPkgIdString (snd x), Just (fst x))
-                      [] -> do
-                        prettyWarnL
-                          [ flow "Could not find custom-setup dep:"
-                          , style Current (fromString $ packageNameString name) <> "."
-                          ]
-                        pure ("-package=" ++ packageNameString name, Nothing)
-                let depsArgs = map fst matchedDeps
-                -- Generate setup_macros.h and provide it to ghc
-                let macroDeps = mapMaybe snd matchedDeps
-                    cppMacrosFile = setupDir </> relFileSetupMacrosH
-                    cppArgs =
-                      ["-optP-include", "-optP" ++ toFilePath cppMacrosFile]
-                writeBinaryFileAtomic
-                  cppMacrosFile
-                  ( encodeUtf8Builder
-                      ( T.pack
-                          ( C.generatePackageVersionMacros
-                              (packageVersion package)
-                              macroDeps
-                          )
-                      )
-                  )
-                pure (packageDBArgs ++ depsArgs ++ cppArgs)
-
-              -- This branch is usually taken for builds, and is always taken
-              -- for `stack sdist`.
-              --
-              -- This approach is debatable. It adds access to the snapshot
-              -- package database for Cabal. There are two possible objections:
-              --
-              -- 1. This doesn't isolate the build enough; arbitrary other
-              -- packages available could cause the build to succeed or fail.
-              --
-              -- 2. This doesn't provide enough packages: we should also
-              -- include the local database when building local packages.
-              --
-              -- Currently, this branch is only taken via `stack sdist` or when
-              -- explicitly requested in the stack.yaml file.
-              Nothing -> do
-                warnCustomNoDeps
-                pure $
-                     cabalPackageArg
-                      -- NOTE: This is different from packageDBArgs above in
-                      -- that it does not include the local database and does
-                      -- not pass in the -hide-all-packages argument
-                  ++ ( "-clear-package-db"
-                     : "-global-package-db"
-                     : map
-                         (("-package-db=" ++) . toFilePathNoTrailingSep)
-                         (bcoExtraDBs eeBaseConfigOpts)
-                     ++ [    "-package-db="
-                          ++ toFilePathNoTrailingSep (bcoSnapDB eeBaseConfigOpts)
-                        ]
-                     )
-
-          setupArgs =
-            ("--builddir=" ++ toFilePathNoTrailingSep distRelativeDir') : args
-
-          runExe :: Path Abs File -> [String] -> RIO env ()
-          runExe exeName fullArgs = do
-            compilerVer <- view actualCompilerVersionL
-            runAndOutput compilerVer `catch` \ece -> do
-              (mlogFile, bss) <-
-                case outputType of
-                  OTConsole _ -> pure (Nothing, [])
-                  OTLogFile logFile h ->
-                    if keepOutputOpen == KeepOpen
-                    then
-                      pure (Nothing, []) -- expected failure build continues further
-                    else do
-                      liftIO $ hClose h
-                      fmap (Just logFile,) $ withSourceFile (toFilePath logFile) $
-                        \src ->
-                             runConduit
-                           $ src
-                          .| CT.decodeUtf8Lenient
-                          .| mungeBuildOutput
-                               stripTHLoading makeAbsolute pkgDir compilerVer
-                          .| CL.consume
-              prettyThrowM $ CabalExitedUnsuccessfully
-                (eceExitCode ece) taskProvides exeName fullArgs mlogFile bss
-           where
-            runAndOutput :: ActualCompiler -> RIO env ()
-            runAndOutput compilerVer = withWorkingDir (toFilePath pkgDir) $
-              withProcessContext menv $ case outputType of
-                OTLogFile _ h -> do
-                  let prefixWithTimestamps =
-                        if configPrefixTimestamps config
-                          then PrefixWithTimestamps
-                          else WithoutTimestamps
-                  void $ sinkProcessStderrStdout (toFilePath exeName) fullArgs
-                    (sinkWithTimestamps prefixWithTimestamps h)
-                    (sinkWithTimestamps prefixWithTimestamps h)
-                OTConsole mprefix ->
-                  let prefix = fold mprefix
-                  in  void $ sinkProcessStderrStdout
-                        (toFilePath exeName)
-                        fullArgs
-                        (outputSink KeepTHLoading LevelWarn compilerVer prefix)
-                        (outputSink stripTHLoading LevelInfo compilerVer prefix)
-            outputSink ::
-                 HasCallStack
-              => ExcludeTHLoading
-              -> LogLevel
-              -> ActualCompiler
-              -> Utf8Builder
-              -> ConduitM S.ByteString Void (RIO env) ()
-            outputSink excludeTH level compilerVer prefix =
-              CT.decodeUtf8Lenient
-              .| mungeBuildOutput excludeTH makeAbsolute pkgDir compilerVer
-              .| CL.mapM_ (logGeneric "" level . (prefix <>) . display)
-            -- If users want control, we should add a config option for this
-            makeAbsolute :: ConvertPathsToAbsolute
-            makeAbsolute = case stripTHLoading of
-              ExcludeTHLoading -> ConvertPathsToAbsolute
-              KeepTHLoading    -> KeepPathsAsIs
-
-      exeName <- case esetupexehs of
-        Left setupExe -> pure setupExe
-        Right setuphs -> do
-          distDir <- distDirFromDir pkgDir
-          let setupDir = distDir </> relDirSetup
-              outputFile = setupDir </> relFileSetupLower
-          customBuilt <- liftIO $ readIORef eeCustomBuilt
-          if Set.member (packageName package) customBuilt
-            then pure outputFile
-            else do
-              ensureDir setupDir
-              compilerPath <- view $ compilerPathsL.to cpCompiler
-              packageArgs <- getPackageArgs setupDir
-              runExe compilerPath $
-                [ "--make"
-                , "-odir", toFilePathNoTrailingSep setupDir
-                , "-hidir", toFilePathNoTrailingSep setupDir
-                , "-i", "-i."
-                ] ++ packageArgs ++
-                [ toFilePath setuphs
-                , toFilePath eeSetupShimHs
-                , "-main-is"
-                , "StackSetupShim.mainOverride"
-                , "-o", toFilePath outputFile
-                , "-threaded"
-                ] ++
-
-                -- Apply GHC options
-                -- https://github.com/commercialhaskell/stack/issues/4526
-                map
-                  T.unpack
-                  ( Map.findWithDefault
-                      []
-                      AGOEverything
-                      (configGhcOptionsByCat config)
-                  ++ case configApplyGhcOptions config of
-                       AGOEverything -> boptsCLIGhcOptions eeBuildOptsCLI
-                       AGOTargets -> []
-                       AGOLocals -> []
-                  )
-
-              liftIO $ atomicModifyIORef' eeCustomBuilt $
-                \oldCustomBuilt ->
-                  (Set.insert (packageName package) oldCustomBuilt, ())
-              pure outputFile
-      let cabalVerboseArg =
-            let CabalVerbosity cv = boptsCabalVerbose eeBuildOpts
-            in  "--verbose=" <> showForCabal cv
-      runExe exeName $ cabalVerboseArg:setupArgs
-
--- Implements running a package's build, used to implement 'ATBuild' and
--- 'ATBuildFinal' tasks.  In particular this does the following:
---
--- * Checks if the package exists in the precompiled cache, and if so,
---   add it to the database instead of performing the build.
---
--- * Runs the configure step if needed ('ensureConfig')
---
--- * Runs the build step
---
--- * Generates haddocks
---
--- * Registers the library and copies the built executables into the
---   local install directory. Note that this is literally invoking Cabal
---   with @copy@, and not the copying done by @stack install@ - that is
---   handled by 'copyExecutables'.
-singleBuild :: forall env. (HasEnvConfig env, HasRunner env)
-            => ActionContext
-            -> ExecuteEnv
-            -> Task
-            -> InstalledMap
-            -> Bool             -- ^ Is this a final build?
-            -> RIO env ()
-singleBuild ac@ActionContext {..} ee@ExecuteEnv {..} task@Task {..} installedMap isFinalBuild = do
-  (allDepsMap, cache) <-
-    getConfigCache ee task installedMap enableTests enableBenchmarks
-  mprecompiled <- getPrecompiled cache
-  minstalled <-
-    case mprecompiled of
-      Just precompiled -> copyPreCompiled precompiled
-      Nothing -> do
-        mcurator <- view $ buildConfigL.to bcCurator
-        realConfigAndBuild cache mcurator allDepsMap
-  case minstalled of
-    Nothing -> pure ()
-    Just installed -> do
-      writeFlagCache installed cache
-      liftIO $ atomically $
-        modifyTVar eeGhcPkgIds $ Map.insert taskProvides installed
- where
-  PackageIdentifier pname pversion = taskProvides
-  doHaddock mcurator package =
-       taskBuildHaddock
-    && not isFinalBuild
-       -- Works around haddock failing on bytestring-builder since it has no
-       -- modules when bytestring is new enough.
-    && packageHasExposedModules package
-       -- Special help for the curator tool to avoid haddocks that are known
-       -- to fail
-    && maybe True (Set.notMember pname . curatorSkipHaddock) mcurator
-  expectHaddockFailure =
-      maybe False (Set.member pname . curatorExpectHaddockFailure)
-  fulfillHaddockExpectations mcurator action
-    | expectHaddockFailure mcurator = do
-        eres <- tryAny $ action KeepOpen
-        case eres of
-          Right () -> prettyWarnL
-            [ style Current (fromString $ packageNameString pname) <> ":"
-            , flow "unexpected Haddock success."
-            ]
-          Left _ -> pure ()
-  fulfillHaddockExpectations _ action = action CloseOnException
-
-  buildingFinals = isFinalBuild || taskAllInOne
-  enableTests = buildingFinals && any isCTest (taskComponents task)
-  enableBenchmarks = buildingFinals && any isCBench (taskComponents task)
-
-  annSuffix executableBuildStatuses =
-    if result == "" then "" else " (" <> result <> ")"
-   where
-    result = T.intercalate " + " $ concat
-      [ ["lib" | taskAllInOne && hasLib]
-      , ["internal-lib" | taskAllInOne && hasSubLib]
-      , ["exe" | taskAllInOne && hasExe]
-      , ["test" | enableTests]
-      , ["bench" | enableBenchmarks]
-      ]
-    (hasLib, hasSubLib, hasExe) = case taskType of
-      TTLocalMutable lp ->
-        let package = lpPackage lp
-            hasLibrary =
-              case packageLibraries package of
-                NoLibraries -> False
-                HasLibraries _ -> True
-            hasSubLibrary = not . Set.null $ packageInternalLibraries package
-            hasExecutables =
-              not . Set.null $ exesToBuild executableBuildStatuses lp
-        in  (hasLibrary, hasSubLibrary, hasExecutables)
-      -- This isn't true, but we don't want to have this info for upstream deps.
-      _ -> (False, False, False)
-
-  getPrecompiled cache =
-    case taskType of
-      TTRemotePackage Immutable _ loc -> do
-        mpc <- readPrecompiledCache
-                 loc
-                 (configCacheOpts cache)
-                 (configCacheHaddock cache)
-                 (configCacheDeps cache)
-        case mpc of
-          Nothing -> pure Nothing
-          -- Only pay attention to precompiled caches that refer to packages
-          -- within the snapshot.
-          Just pc
-            | maybe False
-                (bcoSnapInstallRoot eeBaseConfigOpts `isProperPrefixOf`)
-                (pcLibrary pc) -> pure Nothing
-          -- If old precompiled cache files are left around but snapshots are
-          -- deleted, it is possible for the precompiled file to refer to the
-          -- very library we're building, and if flags are changed it may try to
-          -- copy the library to itself. This check prevents that from
-          -- happening.
-          Just pc -> do
-            let allM _ [] = pure True
-                allM f (x:xs) = do
-                  b <- f x
-                  if b then allM f xs else pure False
-            b <- liftIO $
-                   allM doesFileExist $ maybe id (:) (pcLibrary pc) $ pcExes pc
-            pure $ if b then Just pc else Nothing
-      _ -> pure Nothing
-
-  copyPreCompiled (PrecompiledCache mlib sublibs exes) = do
-    announceTask ee task "using precompiled package"
-
-    -- We need to copy .conf files for the main library and all sublibraries
-    -- which exist in the cache, from their old snapshot to the new one.
-    -- However, we must unregister any such library in the new snapshot, in case
-    -- it was built with different flags.
-    let
-      subLibNames = Set.toList $ case taskType of
-        TTLocalMutable lp -> packageInternalLibraries $ lpPackage lp
-        TTRemotePackage _ p _ -> packageInternalLibraries p
-      toMungedPackageId :: Text -> MungedPackageId
-      toMungedPackageId sublib =
-        let sublibName = LSubLibName $ mkUnqualComponentName $ T.unpack sublib
-        in  MungedPackageId (MungedPackageName pname sublibName) pversion
-      toPackageId :: MungedPackageId -> PackageIdentifier
-      toPackageId (MungedPackageId n v) =
-        PackageIdentifier (encodeCompatPackageName n) v
-      allToUnregister :: [Either PackageIdentifier GhcPkgId]
-      allToUnregister = mcons
-        (Left taskProvides <$ mlib)
-        (map (Left . toPackageId . toMungedPackageId) subLibNames)
-      allToRegister = mcons mlib sublibs
-
-    unless (null allToRegister) $
-      withMVar eeInstallLock $ \() -> do
-        -- We want to ignore the global and user package databases. ghc-pkg
-        -- allows us to specify --no-user-package-db and --package-db=<db> on
-        -- the command line.
-        let pkgDb = bcoSnapDB eeBaseConfigOpts
-        ghcPkgExe <- getGhcPkgExe
-        -- First unregister, silently, everything that needs to be unregistered.
-        unless (null allToUnregister) $
-          catchAny
-            ( unregisterGhcPkgIds False ghcPkgExe pkgDb $
-                NonEmpty.fromList allToUnregister
-            )
-            (const (pure ()))
-        -- Now, register the cached conf files.
-        forM_ allToRegister $ \libpath ->
-          ghcPkg ghcPkgExe [pkgDb] ["register", "--force", toFilePath libpath]
-
-    liftIO $ forM_ exes $ \exe -> do
-      ensureDir bindir
-      let dst = bindir </> filename exe
-      createLink (toFilePath exe) (toFilePath dst) `catchIO` \_ -> copyFile exe dst
-    case (mlib, exes) of
-      (Nothing, _:_) -> markExeInstalled (taskLocation task) taskProvides
-      _ -> pure ()
-
-    -- Find the package in the database
-    let pkgDbs = [bcoSnapDB eeBaseConfigOpts]
-
-    case mlib of
-      Nothing -> pure $ Just $ Executable taskProvides
-      Just _ -> do
-        mpkgid <- loadInstalledPkg pkgDbs eeSnapshotDumpPkgs pname
-
-        pure $ Just $
-          case mpkgid of
-            Nothing -> assert False $ Executable taskProvides
-            Just pkgid -> Library taskProvides pkgid Nothing
-   where
-    bindir = bcoSnapInstallRoot eeBaseConfigOpts </> bindirSuffix
-
-  realConfigAndBuild cache mcurator allDepsMap =
-    withSingleContext ac ee task allDepsMap Nothing $
-      \package cabalfp pkgDir cabal0 announce _outputType -> do
-        let cabal = cabal0 CloseOnException
-        executableBuildStatuses <- getExecutableBuildStatuses package pkgDir
-        when (  not (cabalIsSatisfied executableBuildStatuses)
-             && taskIsTarget task
-             ) $
-          prettyInfoL
-            [ flow "Building all executables for"
-            , style Current (fromString $ packageNameString $ packageName package)
-            , flow "once. After a successful build of all of them, only \
-                   \specified executables will be rebuilt."
-            ]
-        _neededConfig <-
-          ensureConfig
-            cache
-            pkgDir
-            ee
-            ( announce
-                (  "configure"
-                <> display (annSuffix executableBuildStatuses)
-                )
-            )
-            cabal
-            cabalfp
-            task
-        let installedMapHasThisPkg :: Bool
-            installedMapHasThisPkg =
-              case Map.lookup (packageName package) installedMap of
-                Just (_, Library ident _ _) -> ident == taskProvides
-                Just (_, Executable _) -> True
-                _ -> False
-
-        case ( boptsCLIOnlyConfigure eeBuildOptsCLI
-             , boptsCLIInitialBuildSteps eeBuildOptsCLI && taskIsTarget task) of
-          -- A full build is done if there are downstream actions,
-          -- because their configure step will require that this
-          -- package is built. See
-          -- https://github.com/commercialhaskell/stack/issues/2787
-          (True, _) | null acDownstream -> pure Nothing
-          (_, True) | null acDownstream || installedMapHasThisPkg -> do
-            initialBuildSteps executableBuildStatuses cabal announce
-            pure Nothing
-          _ -> fulfillCuratorBuildExpectations
-                 pname
-                 mcurator
-                 enableTests
-                 enableBenchmarks
-                 Nothing
-                 (Just <$>
-                    realBuild cache package pkgDir cabal0 announce executableBuildStatuses)
-
-  initialBuildSteps executableBuildStatuses cabal announce = do
-    announce
-      (  "initial-build-steps"
-      <> display (annSuffix executableBuildStatuses)
-      )
-    cabal KeepTHLoading ["repl", "stack-initial-build-steps"]
-
-  realBuild ::
-       ConfigCache
-    -> Package
-    -> Path Abs Dir
-    -> (KeepOutputOpen -> ExcludeTHLoading -> [String] -> RIO env ())
-    -> (Utf8Builder -> RIO env ())
-       -- ^ A plain 'announce' function
-    -> Map Text ExecutableBuildStatus
-    -> RIO env Installed
-  realBuild cache package pkgDir cabal0 announce executableBuildStatuses = do
-    let cabal = cabal0 CloseOnException
-    wc <- view $ actualCompilerVersionL.whichCompilerL
-
-    markExeNotInstalled (taskLocation task) taskProvides
-    case taskType of
-      TTLocalMutable lp -> do
-        when enableTests $ setTestStatus pkgDir TSUnknown
-        caches <- runMemoizedWith $ lpNewBuildCaches lp
-        mapM_
-          (uncurry (writeBuildCache pkgDir))
-          (Map.toList caches)
-      TTRemotePackage{} -> pure ()
-
-    -- FIXME: only output these if they're in the build plan.
-
-    let postBuildCheck _succeeded = do
-          mlocalWarnings <- case taskType of
-            TTLocalMutable lp -> do
-                warnings <- checkForUnlistedFiles taskType pkgDir
-                -- TODO: Perhaps only emit these warnings for non extra-dep?
-                pure (Just (lpCabalFile lp, warnings))
-            _ -> pure Nothing
-          -- NOTE: once
-          -- https://github.com/commercialhaskell/stack/issues/2649
-          -- is resolved, we will want to partition the warnings
-          -- based on variety, and output in different lists.
-          let showModuleWarning (UnlistedModulesWarning comp modules) =
-                "- In" <+>
-                fromString (T.unpack (renderComponent comp)) <>
-                ":" <> line <>
-                indent 4 ( mconcat
-                         $ L.intersperse line
-                         $ map
-                             (style Good . fromString . C.display)
-                             modules
-                         )
-          forM_ mlocalWarnings $ \(cabalfp, warnings) ->
-            unless (null warnings) $ prettyWarn $
-                 flow "The following modules should be added to \
-                      \exposed-modules or other-modules in" <+>
-                      pretty cabalfp
-              <> ":"
-              <> line
-              <> indent 4 ( mconcat
-                          $ L.intersperse line
-                          $ map showModuleWarning warnings
-                          )
-              <> blankLine
-              <> flow "Missing modules in the Cabal file are likely to cause \
-                      \undefined reference errors from the linker, along with \
-                      \other problems."
-
-    () <- announce
-      (  "build"
-      <> display (annSuffix executableBuildStatuses)
-      )
-    config <- view configL
-    extraOpts <- extraBuildOptions wc eeBuildOpts
-    let stripTHLoading
-          | configHideTHLoading config = ExcludeTHLoading
-          | otherwise                  = KeepTHLoading
-    cabal stripTHLoading (("build" :) $ (++ extraOpts) $
-        case (taskType, taskAllInOne, isFinalBuild) of
-            (_, True, True) -> throwM AllInOneBuildBug
-            (TTLocalMutable lp, False, False) ->
-              primaryComponentOptions executableBuildStatuses lp
-            (TTLocalMutable lp, False, True) -> finalComponentOptions lp
-            (TTLocalMutable lp, True, False) ->
-                 primaryComponentOptions executableBuildStatuses lp
-              ++ finalComponentOptions lp
-            (TTRemotePackage{}, _, _) -> [])
-      `catch` \ex -> case ex of
-        CabalExitedUnsuccessfully{} ->
-          postBuildCheck False >> prettyThrowM ex
-        _ -> throwM ex
-    postBuildCheck True
-
-    mcurator <- view $ buildConfigL.to bcCurator
-    when (doHaddock mcurator package) $ do
-      announce "haddock"
-      sourceFlag <- if not (boptsHaddockHyperlinkSource eeBuildOpts)
-        then pure []
-        else do
-          -- See #2429 for why the temp dir is used
-          ec
-            <- withWorkingDir (toFilePath eeTempDir)
-             $ proc "haddock" ["--hyperlinked-source"]
-             $ \pc -> withProcessWait
-               (setStdout createSource $ setStderr createSource pc) $ \p ->
-                 runConcurrently
-                   $ Concurrently (runConduit $ getStdout p .| CL.sinkNull)
-                  *> Concurrently (runConduit $ getStderr p .| CL.sinkNull)
-                  *> Concurrently (waitExitCode p)
-          case ec of
-            -- Fancy crosslinked source
-            ExitSuccess -> pure ["--haddock-option=--hyperlinked-source"]
-            -- Older hscolour colouring
-            ExitFailure _ -> do
-              hscolourExists <- doesExecutableExist "HsColour"
-              unless hscolourExists $
-                prettyWarnL
-                  [ flow "Warning: Haddock is not generating hyperlinked \
-                         \sources because 'HsColour' not found on PATH (use"
-                  , style Shell (flow "stack install hscolour")
-                  , flow "to install)."
-                  ]
-              pure ["--hyperlink-source" | hscolourExists]
-
-      -- For GHC 8.4 and later, provide the --quickjump option.
-      actualCompiler <- view actualCompilerVersionL
-      let quickjump =
-            case actualCompiler of
-              ACGhc ghcVer
-                | ghcVer >= mkVersion [8, 4] -> ["--haddock-option=--quickjump"]
-              _ -> []
-
-      fulfillHaddockExpectations mcurator $ \keep ->
-        cabal0 keep KeepTHLoading $ concat
-          [ [ "haddock"
-            , "--html"
-            , "--hoogle"
-            , "--html-location=../$pkg-$version/"
-            ]
-          , sourceFlag
-          , ["--internal" | boptsHaddockInternal eeBuildOpts]
-          , [ "--haddock-option=" <> opt
-            | opt <- hoAdditionalArgs (boptsHaddockOpts eeBuildOpts) ]
-          , quickjump
-          ]
-
-    let hasLibrary =
-          case packageLibraries package of
-            NoLibraries -> False
-            HasLibraries _ -> True
-        packageHasComponentSet f = not $ Set.null $ f package
-        hasInternalLibrary = packageHasComponentSet packageInternalLibraries
-        hasExecutables = packageHasComponentSet packageExes
-        shouldCopy =
-             not isFinalBuild
-          && (hasLibrary || hasInternalLibrary || hasExecutables)
-    when shouldCopy $ withMVar eeInstallLock $ \() -> do
-      announce "copy/register"
-      eres <- try $ cabal KeepTHLoading ["copy"]
-      case eres of
-        Left err@CabalExitedUnsuccessfully{} ->
-          throwM $ CabalCopyFailed
-                     (packageBuildType package == C.Simple)
-                     (displayException err)
-        _ -> pure ()
-      when hasLibrary $ cabal KeepTHLoading ["register"]
-
-    -- copy ddump-* files
-    case T.unpack <$> boptsDdumpDir eeBuildOpts of
-      Just ddumpPath | buildingFinals && not (null ddumpPath) -> do
-        distDir <- distRelativeDir
-        ddumpDir <- parseRelDir ddumpPath
-
-        logDebug $ fromString ("ddump-dir: " <> toFilePath ddumpDir)
-        logDebug $ fromString ("dist-dir: " <> toFilePath distDir)
-
-        runConduitRes
-          $ CF.sourceDirectoryDeep False (toFilePath distDir)
-         .| CL.filter (L.isInfixOf ".dump-")
-         .| CL.mapM_ (\src -> liftIO $ do
-              parentDir <- parent <$> parseRelDir src
-              destBaseDir <-
-                (ddumpDir </>) <$> stripProperPrefix distDir parentDir
-              -- exclude .stack-work dir
-              unless (".stack-work" `L.isInfixOf` toFilePath destBaseDir) $ do
-                ensureDir destBaseDir
-                src' <- parseRelFile src
-                copyFile src' (destBaseDir </> filename src'))
-      _ -> pure ()
-
-    let (installedPkgDb, installedDumpPkgsTVar) =
-          case taskLocation task of
-            Snap ->
-              ( bcoSnapDB eeBaseConfigOpts
-              , eeSnapshotDumpPkgs )
-            Local ->
-              ( bcoLocalDB eeBaseConfigOpts
-              , eeLocalDumpPkgs )
-    let ident = PackageIdentifier (packageName package) (packageVersion package)
-    -- only pure the sublibs to cache them if we also cache the main lib (that
-    -- is, if it exists)
-    (mpkgid, sublibsPkgIds) <- case packageLibraries package of
-      HasLibraries _ -> do
-        sublibsPkgIds <- fmap catMaybes $
-          forM (Set.toList $ packageInternalLibraries package) $ \sublib -> do
-            let sublibName = MungedPackageName
-                  (packageName package)
-                  (LSubLibName $ mkUnqualComponentName $ T.unpack sublib)
-            loadInstalledPkg
-              [installedPkgDb]
-              installedDumpPkgsTVar
-              (encodeCompatPackageName sublibName)
-
-        mpkgid <- loadInstalledPkg
-                    [installedPkgDb]
-                    installedDumpPkgsTVar
-                    (packageName package)
-        case mpkgid of
-          Nothing -> throwM $ Couldn'tFindPkgId $ packageName package
-          Just pkgid -> pure (Library ident pkgid Nothing, sublibsPkgIds)
-      NoLibraries -> do
-        markExeInstalled (taskLocation task) taskProvides -- TODO unify somehow
-                                                          -- with writeFlagCache?
-        pure (Executable ident, []) -- don't pure sublibs in this case
-
-    case taskType of
-      TTRemotePackage Immutable _ loc ->
-        writePrecompiledCache
-          eeBaseConfigOpts
-          loc
-          (configCacheOpts cache)
-          (configCacheHaddock cache)
-          (configCacheDeps cache)
-          mpkgid sublibsPkgIds (packageExes package)
-      _ -> pure ()
-
-    case taskType of
-      -- For packages from a package index, pkgDir is in the tmp directory. We
-      -- eagerly delete it if no other tasks require it, to reduce space usage
-      -- in tmp (#3018).
-      TTRemotePackage{} -> do
-        let remaining =
-              filter
-                (\(ActionId x _) -> x == taskProvides)
-                (Set.toList acRemaining)
-        when (null remaining) $ removeDirRecur pkgDir
-      TTLocalMutable{} -> pure ()
-
-    pure mpkgid
-
-  loadInstalledPkg ::
-       [Path Abs Dir]
-    -> TVar (Map GhcPkgId DumpPackage)
-    -> PackageName
-    -> RIO env (Maybe GhcPkgId)
-  loadInstalledPkg pkgDbs tvar name = do
-    pkgexe <- getGhcPkgExe
-    dps <- ghcPkgDescribe pkgexe name pkgDbs $ conduitDumpPackage .| CL.consume
-    case dps of
-      [] -> pure Nothing
-      [dp] -> do
-        liftIO $ atomically $ modifyTVar' tvar (Map.insert (dpGhcPkgId dp) dp)
-        pure $ Just (dpGhcPkgId dp)
-      _ -> throwM $ MultipleResultsBug name dps
-
--- | Get the build status of all the package executables. Do so by
--- testing whether their expected output file exists, e.g.
---
--- .stack-work/dist/x86_64-osx/Cabal-1.22.4.0/build/alpha/alpha
--- .stack-work/dist/x86_64-osx/Cabal-1.22.4.0/build/alpha/alpha.exe
--- .stack-work/dist/x86_64-osx/Cabal-1.22.4.0/build/alpha/alpha.jsexe/ (NOTE: a dir)
-getExecutableBuildStatuses ::
-     HasEnvConfig env
-  => Package
-  -> Path Abs Dir
-  -> RIO env (Map Text ExecutableBuildStatus)
-getExecutableBuildStatuses package pkgDir = do
-  distDir <- distDirFromDir pkgDir
-  platform <- view platformL
-  fmap
-    Map.fromList
-    (mapM (checkExeStatus platform distDir) (Set.toList (packageExes package)))
-
--- | Check whether the given executable is defined in the given dist directory.
-checkExeStatus ::
-     HasLogFunc env
-  => Platform
-  -> Path b Dir
-  -> Text
-  -> RIO env (Text, ExecutableBuildStatus)
-checkExeStatus platform distDir name = do
-  exename <- parseRelDir (T.unpack name)
-  exists <- checkPath (distDir </> relDirBuild </> exename)
-  pure
-    ( name
-    , if exists
-        then ExecutableBuilt
-        else ExecutableNotBuilt)
- where
-  checkPath base =
-    case platform of
-      Platform _ Windows -> do
-        fileandext <- parseRelFile (file ++ ".exe")
-        doesFileExist (base </> fileandext)
-      _ -> do
-        fileandext <- parseRelFile file
-        doesFileExist (base </> fileandext)
-   where
-    file = T.unpack name
-
--- | Check if any unlisted files have been found, and add them to the build cache.
-checkForUnlistedFiles ::
-     HasEnvConfig env
-  => TaskType
-  -> Path Abs Dir
-  -> RIO env [PackageWarning]
-checkForUnlistedFiles (TTLocalMutable lp) pkgDir = do
-  caches <- runMemoizedWith $ lpNewBuildCaches lp
-  (addBuildCache,warnings) <-
-    addUnlistedToBuildCache
-      (lpPackage lp)
-      (lpCabalFile lp)
-      (lpComponents lp)
-      caches
-  forM_ (Map.toList addBuildCache) $ \(component, newToCache) -> do
-    let cache = Map.findWithDefault Map.empty component caches
-    writeBuildCache pkgDir component $
-      Map.unions (cache : newToCache)
-  pure warnings
-checkForUnlistedFiles TTRemotePackage{} _ = pure []
-
--- | Implements running a package's tests. Also handles producing
--- coverage reports if coverage is enabled.
-singleTest :: HasEnvConfig env
-           => TestOpts
-           -> [Text]
-           -> ActionContext
-           -> ExecuteEnv
-           -> Task
-           -> InstalledMap
-           -> RIO env ()
-singleTest topts testsToRun ac ee task installedMap = do
-  -- FIXME: Since this doesn't use cabal, we should be able to avoid using a
-  -- full blown 'withSingleContext'.
-  (allDepsMap, _cache) <- getConfigCache ee task installedMap True False
-  mcurator <- view $ buildConfigL.to bcCurator
-  let pname = pkgName $ taskProvides task
-      expectFailure = expectTestFailure pname mcurator
-  withSingleContext ac ee task allDepsMap (Just "test") $
-    \package _cabalfp pkgDir _cabal announce outputType -> do
-      config <- view configL
-      let needHpc = toCoverage topts
-
-      toRun <-
-        if toDisableRun topts
-          then do
-            announce "Test running disabled by --no-run-tests flag."
-            pure False
-          else if toRerunTests topts
-            then pure True
-            else do
-              status <- getTestStatus pkgDir
-              case status of
-                TSSuccess -> do
-                  unless (null testsToRun) $
-                    announce "skipping already passed test"
-                  pure False
-                TSFailure
-                  | expectFailure -> do
-                      announce "skipping already failed test that's expected to fail"
-                      pure False
-                  | otherwise -> do
-                      announce "rerunning previously failed test"
-                      pure True
-                TSUnknown -> pure True
-
-      when toRun $ do
-        buildDir <- distDirFromDir pkgDir
-        hpcDir <- hpcDirFromDir pkgDir
-        when needHpc (ensureDir hpcDir)
-
-        let suitesToRun
-              = [ testSuitePair
-                | testSuitePair <- Map.toList $ packageTests package
-                , let testName = fst testSuitePair
-                , testName `elem` testsToRun
-                ]
-
-        errs <- fmap Map.unions $ forM suitesToRun $ \(testName, suiteInterface) -> do
-          let stestName = T.unpack testName
-          (testName', isTestTypeLib) <-
-            case suiteInterface of
-              C.TestSuiteLibV09{} -> pure (stestName ++ "Stub", True)
-              C.TestSuiteExeV10{} -> pure (stestName, False)
-              interface -> throwM (TestSuiteTypeUnsupported interface)
-
-          let exeName = testName' ++
-                case configPlatform config of
-                  Platform _ Windows -> ".exe"
-                  _ -> ""
-          tixPath <- fmap (pkgDir </>) $ parseRelFile $ exeName ++ ".tix"
-          exePath <-
-            fmap (buildDir </>) $ parseRelFile $
-              "build/" ++ testName' ++ "/" ++ exeName
-          exists <- doesFileExist exePath
-          -- in Stack.Package.packageFromPackageDescription we filter out
-          -- package itself of any dependencies so any tests requiring loading
-          -- of their own package library will fail so to prevent this we return
-          -- it back here but unfortunately unconditionally
-          installed <- case Map.lookup pname installedMap of
-            Just (_, installed) -> pure $ Just installed
-            Nothing -> do
-              idMap <- liftIO $ readTVarIO (eeGhcPkgIds ee)
-              pure $ Map.lookup (taskProvides task) idMap
-          let pkgGhcIdList = case installed of
-                               Just (Library _ ghcPkgId _) -> [ghcPkgId]
-                               _ -> []
-          -- doctest relies on template-haskell in QuickCheck-based tests
-          thGhcId <-
-            case L.find ((== "template-haskell") . pkgName . dpPackageIdent. snd)
-                   (Map.toList $ eeGlobalDumpPkgs ee) of
-              Just (ghcId, _) -> pure ghcId
-              Nothing -> throwIO TemplateHaskellNotFoundBug
-          -- env variable GHC_ENVIRONMENT is set for doctest so module names for
-          -- packages with proper dependencies should no longer get ambiguous
-          -- see e.g. https://github.com/doctest/issues/119
-          -- also we set HASKELL_DIST_DIR to a package dist directory so
-          -- doctest will be able to load modules autogenerated by Cabal
-          let setEnv f pc = modifyEnvVars pc $ \envVars ->
-                Map.insert "HASKELL_DIST_DIR" (T.pack $ toFilePath buildDir) $
-                Map.insert "GHC_ENVIRONMENT" (T.pack f) envVars
-              fp' = eeTempDir ee </> testGhcEnvRelFile
-          -- Add a random suffix to avoid conflicts between parallel jobs
-          -- See https://github.com/commercialhaskell/stack/issues/5024
-          randomInt <- liftIO (randomIO :: IO Int)
-          let randomSuffix = "." <> show (abs randomInt)
-          fp <- toFilePath <$> addExtension randomSuffix fp'
-          let snapDBPath =
-                toFilePathNoTrailingSep (bcoSnapDB $ eeBaseConfigOpts ee)
-              localDBPath =
-                toFilePathNoTrailingSep (bcoLocalDB $ eeBaseConfigOpts ee)
-              ghcEnv =
-                   "clear-package-db\n"
-                <> "global-package-db\n"
-                <> "package-db "
-                <> fromString snapDBPath
-                <> "\n"
-                <> "package-db "
-                <> fromString localDBPath
-                <> "\n"
-                <> foldMap
-                     ( \ghcId ->
-                            "package-id "
-                         <> display (unGhcPkgId ghcId)
-                         <> "\n"
-                     )
-                     (pkgGhcIdList ++ thGhcId:Map.elems allDepsMap)
-          writeFileUtf8Builder fp ghcEnv
-          menv <- liftIO $
-            setEnv fp =<< configProcessContextSettings config EnvSettings
-              { esIncludeLocals = taskLocation task == Local
-              , esIncludeGhcPackagePath = True
-              , esStackExe = True
-              , esLocaleUtf8 = False
-              , esKeepGhcRts = False
-              }
-          let emptyResult = Map.singleton testName Nothing
-          withProcessContext menv $ if exists
-            then do
-                -- We clear out the .tix files before doing a run.
-                when needHpc $ do
-                  tixexists <- doesFileExist tixPath
-                  when tixexists $
-                    prettyWarnL
-                      [ flow "Removing HPC file"
-                      , pretty tixPath <> "."
-                      ]
-                  liftIO $ ignoringAbsence (removeFile tixPath)
-
-                let args = toAdditionalArgs topts
-                    argsDisplay = case args of
-                      [] -> ""
-                      _ ->    ", args: "
-                           <> T.intercalate " " (map showProcessArgDebug args)
-                announce $
-                     "test (suite: "
-                  <> display testName
-                  <> display argsDisplay
-                  <> ")"
-
-                -- Clear "Progress: ..." message before
-                -- redirecting output.
-                case outputType of
-                  OTConsole _ -> do
-                    logStickyDone ""
-                    liftIO $ hFlush stdout
-                    liftIO $ hFlush stderr
-                  OTLogFile _ _ -> pure ()
-
-                let output = case outputType of
-                      OTConsole Nothing -> Nothing <$ inherit
-                      OTConsole (Just prefix) -> fmap
-                        ( \src -> Just $
-                               runConduit $ src
-                            .| CT.decodeUtf8Lenient
-                            .| CT.lines
-                            .| CL.map stripCR
-                            .| CL.mapM_ (\t -> logInfo $ prefix <> display t)
-                        )
-                        createSource
-                      OTLogFile _ h -> Nothing <$ useHandleOpen h
-                    optionalTimeout action
-                      | Just maxSecs <- toMaximumTimeSeconds topts, maxSecs > 0 =
-                          timeout (maxSecs * 1000000) action
-                      | otherwise = Just <$> action
-
-                mec <- withWorkingDir (toFilePath pkgDir) $
-                  optionalTimeout $ proc (toFilePath exePath) args $ \pc0 -> do
-                    changeStdin <-
-                      if isTestTypeLib
-                        then do
-                          logPath <- buildLogPath package (Just stestName)
-                          ensureDir (parent logPath)
-                          pure $
-                              setStdin
-                            $ byteStringInput
-                            $ BL.fromStrict
-                            $ encodeUtf8 $ fromString $
-                            show ( logPath
-                                 , mkUnqualComponentName (T.unpack testName)
-                                 )
-                        else do
-                          isTerminal <- view $ globalOptsL.to globalTerminal
-                          if toAllowStdin topts && isTerminal
-                            then pure id
-                            else pure $ setStdin $ byteStringInput mempty
-                    let pc = changeStdin
-                           $ setStdout output
-                           $ setStderr output
-                             pc0
-                    withProcessWait pc $ \p -> do
-                      case (getStdout p, getStderr p) of
-                        (Nothing, Nothing) -> pure ()
-                        (Just x, Just y) -> concurrently_ x y
-                        (x, y) -> assert False $
-                          concurrently_
-                            (fromMaybe (pure ()) x)
-                            (fromMaybe (pure ()) y)
-                      waitExitCode p
-                -- Add a trailing newline, incase the test
-                -- output didn't finish with a newline.
-                case outputType of
-                  OTConsole Nothing -> prettyInfo blankLine
-                  _ -> pure ()
-                -- Move the .tix file out of the package
-                -- directory into the hpc work dir, for
-                -- tidiness.
-                when needHpc $
-                  updateTixFile (packageName package) tixPath testName'
-                let announceResult result =
-                      announce $
-                           "Test suite "
-                        <> display testName
-                        <> " "
-                        <> result
-                case mec of
-                  Just ExitSuccess -> do
-                    announceResult "passed"
-                    pure Map.empty
-                  Nothing -> do
-                    announceResult "timed out"
-                    if expectFailure
-                    then pure Map.empty
-                    else pure $ Map.singleton testName Nothing
-                  Just ec -> do
-                    announceResult "failed"
-                    if expectFailure
-                    then pure Map.empty
-                    else pure $ Map.singleton testName (Just ec)
-              else do
-                unless expectFailure $
-                  logError $
-                    displayShow $ TestSuiteExeMissing
-                      (packageBuildType package == C.Simple)
-                      exeName
-                      (packageNameString (packageName package))
-                      (T.unpack testName)
-                pure emptyResult
-
-        when needHpc $ do
-          let testsToRun' = map f testsToRun
-              f tName =
-                  case Map.lookup tName (packageTests package) of
-                    Just C.TestSuiteLibV09{} -> tName <> "Stub"
-                    _ -> tName
-          generateHpcReport pkgDir package testsToRun'
-
-        bs <- liftIO $
-          case outputType of
-            OTConsole _ -> pure ""
-            OTLogFile logFile h -> do
-              hClose h
-              S.readFile $ toFilePath logFile
-
-        let succeeded = Map.null errs
-        unless (succeeded || expectFailure) $
-          throwM $ TestSuiteFailure
-            (taskProvides task)
-            errs
-            (case outputType of
-               OTLogFile fp _ -> Just fp
-               OTConsole _ -> Nothing)
-            bs
-
-        setTestStatus pkgDir $ if succeeded then TSSuccess else TSFailure
-
--- | Implements running a package's benchmarks.
-singleBench :: HasEnvConfig env
-            => BenchmarkOpts
-            -> [Text]
-            -> ActionContext
-            -> ExecuteEnv
-            -> Task
-            -> InstalledMap
-            -> RIO env ()
-singleBench beopts benchesToRun ac ee task installedMap = do
-  (allDepsMap, _cache) <- getConfigCache ee task installedMap False True
-  withSingleContext ac ee task allDepsMap (Just "bench") $
-    \_package _cabalfp _pkgDir cabal announce _outputType -> do
-      let args = map T.unpack benchesToRun <> maybe []
-                       ((:[]) . ("--benchmark-options=" <>))
-                       (beoAdditionalArgs beopts)
-
-      toRun <-
-        if beoDisableRun beopts
-          then do
-            announce "Benchmark running disabled by --no-run-benchmarks flag."
-            pure False
-          else pure True
-
-      when toRun $ do
-        announce "benchmarks"
-        cabal CloseOnException KeepTHLoading ("bench" : args)
-
-data ExcludeTHLoading
-  = ExcludeTHLoading
-  | KeepTHLoading
-
-data ConvertPathsToAbsolute
-  = ConvertPathsToAbsolute
-  | KeepPathsAsIs
-
--- | special marker for expected failures in curator builds, using those we need
--- to keep log handle open as build continues further even after a failure
-data KeepOutputOpen
-  = KeepOpen
-  | CloseOnException
-  deriving Eq
-
--- | Strip Template Haskell "Loading package" lines and making paths absolute.
-mungeBuildOutput ::
-     forall m. (MonadIO m, MonadUnliftIO m)
-  => ExcludeTHLoading       -- ^ exclude TH loading?
-  -> ConvertPathsToAbsolute -- ^ convert paths to absolute?
-  -> Path Abs Dir           -- ^ package's root directory
-  -> ActualCompiler         -- ^ compiler we're building with
-  -> ConduitM Text Text m ()
-mungeBuildOutput excludeTHLoading makeAbsolute pkgDir compilerVer = void $
-  CT.lines
-  .| CL.map stripCR
-  .| CL.filter (not . isTHLoading)
-  .| filterLinkerWarnings
-  .| toAbsolute
- where
-  -- | Is this line a Template Haskell "Loading package" line
-  -- ByteString
-  isTHLoading :: Text -> Bool
-  isTHLoading = case excludeTHLoading of
-    KeepTHLoading    -> const False
-    ExcludeTHLoading -> \bs ->
-      "Loading package " `T.isPrefixOf` bs &&
-      ("done." `T.isSuffixOf` bs || "done.\r" `T.isSuffixOf` bs)
-
-  filterLinkerWarnings :: ConduitM Text Text m ()
-  filterLinkerWarnings
-    -- Check for ghc 7.8 since it's the only one prone to producing
-    -- linker warnings on Windows x64
-    | getGhcVersion compilerVer >= mkVersion [7, 8] = doNothing
-    | otherwise = CL.filter (not . isLinkerWarning)
-
-  isLinkerWarning :: Text -> Bool
-  isLinkerWarning str =
-       (  "ghc.exe: warning:" `T.isPrefixOf` str
-       || "ghc.EXE: warning:" `T.isPrefixOf` str
-       )
-    && "is linked instead of __imp_" `T.isInfixOf` str
-
-  -- | Convert GHC error lines with file paths to have absolute file paths
-  toAbsolute :: ConduitM Text Text m ()
-  toAbsolute = case makeAbsolute of
-    KeepPathsAsIs          -> doNothing
-    ConvertPathsToAbsolute -> CL.mapM toAbsolutePath
-
-  toAbsolutePath :: Text -> m Text
-  toAbsolutePath bs = do
-    let (x, y) = T.break (== ':') bs
-    mabs <-
-      if isValidSuffix y
-        then
-          fmap (fmap ((T.takeWhile isSpace x <>) . T.pack . toFilePath)) $
-            forgivingResolveFile pkgDir (T.unpack $ T.dropWhile isSpace x) `catch`
-              \(_ :: PathException) -> pure Nothing
-        else pure Nothing
-    case mabs of
-      Nothing -> pure bs
-      Just fp -> pure $ fp `T.append` y
-
-  doNothing :: ConduitM Text Text m ()
-  doNothing = awaitForever yield
-
-  -- | Match the error location format at the end of lines
-  isValidSuffix = isRight . parseOnly lineCol
-  lineCol = char ':'
-    >> choice
-         [ num >> char ':' >> num >> optional (char '-' >> num) >> pure ()
-         , char '(' >> num >> char ',' >> num >> P.string ")-(" >> num >>
-           char ',' >> num >> char ')' >> pure ()
-         ]
-    >> char ':'
-    >> pure ()
-   where
-    num = some digit
-
--- | Whether to prefix log lines with timestamps.
-data PrefixWithTimestamps
-  = PrefixWithTimestamps
-  | WithoutTimestamps
-
--- | Write stream of lines to handle, but adding timestamps.
-sinkWithTimestamps ::
-     MonadIO m
-  => PrefixWithTimestamps
-  -> Handle
-  -> ConduitT ByteString Void m ()
-sinkWithTimestamps prefixWithTimestamps h =
-  case prefixWithTimestamps of
-    PrefixWithTimestamps ->
-      CB.lines .| CL.mapM addTimestamp .| CL.map (<> "\n") .| sinkHandle h
-    WithoutTimestamps -> sinkHandle h
- where
-  addTimestamp theLine = do
-    now <- liftIO getZonedTime
-    pure (formatZonedTimeForLog now <> " " <> theLine)
-
--- | Format a time in ISO8601 format. We choose ZonedTime over UTCTime
--- because a user expects to see logs in their local time, and would
--- be confused to see UTC time. Stack's debug logs also use the local
--- time zone.
-formatZonedTimeForLog :: ZonedTime -> ByteString
-formatZonedTimeForLog =
-  S8.pack . formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S%6Q"
-
--- | Find the Setup.hs or Setup.lhs in the given directory. If none exists,
--- throw an exception.
-getSetupHs :: Path Abs Dir -- ^ project directory
-           -> IO (Path Abs File)
-getSetupHs dir = do
-  exists1 <- doesFileExist fp1
-  if exists1
-    then pure fp1
-    else do
-      exists2 <- doesFileExist fp2
-      if exists2
-        then pure fp2
-        else throwM $ NoSetupHsFound dir
- where
-  fp1 = dir </> relFileSetupHs
-  fp2 = dir </> relFileSetupLhs
-
--- Do not pass `-hpcdir` as GHC option if the coverage is not enabled.
--- This helps running stack-compiled programs with dynamic interpreters like
--- `hint`. Cfr: https://github.com/commercialhaskell/stack/issues/997
-extraBuildOptions :: (HasEnvConfig env, HasRunner env)
-                  => WhichCompiler -> BuildOpts -> RIO env [String]
-extraBuildOptions wc bopts = do
-  colorOpt <- appropriateGhcColorFlag
-  let optsFlag = compilerOptionsCabalFlag wc
-      baseOpts = maybe "" (" " ++) colorOpt
-  if toCoverage (boptsTestOpts bopts)
-    then do
-      hpcIndexDir <- toFilePathNoTrailingSep <$> hpcRelativeDir
-      pure [optsFlag, "-hpcdir " ++ hpcIndexDir ++ baseOpts]
-    else
-      pure [optsFlag, baseOpts]
-
--- Library, internal and foreign libraries and executable build components.
-primaryComponentOptions ::
-     Map Text ExecutableBuildStatus
-  -> LocalPackage
-  -> [String]
-primaryComponentOptions executableBuildStatuses lp =
-  -- TODO: get this information from target parsing instead, which will allow
-  -- users to turn off library building if desired
-     ( case packageLibraries package of
-         NoLibraries -> []
-         HasLibraries names -> map
-           T.unpack
-           ( T.append "lib:" (T.pack (packageNameString (packageName package)))
-           : map (T.append "flib:") (Set.toList names)
-           )
-     )
-  ++ map
-       (T.unpack . T.append "lib:")
-       (Set.toList $ packageInternalLibraries package)
-  ++ map
-       (T.unpack . T.append "exe:")
-       (Set.toList $ exesToBuild executableBuildStatuses lp)
- where
-  package = lpPackage lp
-
--- | History of this function:
---
--- * Normally it would do either all executables or if the user specified
---   requested components, just build them. Afterwards, due to this Cabal bug
---   <https://github.com/haskell/cabal/issues/2780>, we had to make Stack build
---   all executables every time.
---
--- * In <https://github.com/commercialhaskell/stack/issues/3229> this was
---   flagged up as very undesirable behavior on a large project, hence the
---   behavior below that we build all executables once (modulo success), and
---   thereafter pay attention to user-wanted components.
---
-exesToBuild :: Map Text ExecutableBuildStatus -> LocalPackage -> Set Text
-exesToBuild executableBuildStatuses lp =
-  if cabalIsSatisfied executableBuildStatuses && lpWanted lp
-    then exeComponents (lpComponents lp)
-    else packageExes (lpPackage lp)
-
--- | Do the current executables satisfy Cabal's bugged out requirements?
-cabalIsSatisfied :: Map k ExecutableBuildStatus -> Bool
-cabalIsSatisfied = all (== ExecutableBuilt) . Map.elems
-
--- Test-suite and benchmark build components.
-finalComponentOptions :: LocalPackage -> [String]
-finalComponentOptions lp =
-  map (T.unpack . renderComponent) $
-  Set.toList $
-  Set.filter (\c -> isCTest c || isCBench c) (lpComponents lp)
-
-taskComponents :: Task -> Set NamedComponent
-taskComponents task =
-  case taskType task of
-    TTLocalMutable lp -> lpComponents lp -- FIXME probably just want lpWanted
-    TTRemotePackage{} -> Set.empty
-
-expectTestFailure :: PackageName -> Maybe Curator -> Bool
-expectTestFailure pname =
-  maybe False (Set.member pname . curatorExpectTestFailure)
-
-expectBenchmarkFailure :: PackageName -> Maybe Curator -> Bool
-expectBenchmarkFailure pname =
-  maybe False (Set.member pname . curatorExpectBenchmarkFailure)
-
-fulfillCuratorBuildExpectations ::
-     (HasCallStack, HasTerm env)
-  => PackageName
-  -> Maybe Curator
-  -> Bool
-  -> Bool
-  -> b
-  -> RIO env b
-  -> RIO env b
-fulfillCuratorBuildExpectations pname mcurator enableTests _ defValue action
-  | enableTests && expectTestFailure pname mcurator = do
-      eres <- tryAny action
-      case eres of
-        Right res -> do
-          prettyWarnL
-            [ style Current (fromString $ packageNameString pname) <> ":"
-            , flow "unexpected test build success."
-            ]
-          pure res
-        Left _ -> pure defValue
-fulfillCuratorBuildExpectations pname mcurator _ enableBench defValue action
-  | enableBench && expectBenchmarkFailure pname mcurator = do
-      eres <- tryAny action
-      case eres of
-        Right res -> do
-          prettyWarnL
-            [ style Current (fromString $ packageNameString pname) <> ":"
-            , flow "unexpected benchmark build success."
-            ]
-          pure res
-        Left _ -> pure defValue
-fulfillCuratorBuildExpectations _ _ _ _ _ action = action
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE TypeFamilies        #-}
+
+-- | Perform a build
+module Stack.Build.Execute
+  ( printPlan
+  , preFetch
+  , executePlan
+  -- * Running Setup.hs
+  , ExcludeTHLoading (..)
+  , KeepOutputOpen (..)
+  ) where
+
+import           Control.Concurrent.Execute
+                   ( Action (..), ActionId (..), ActionType (..)
+                   , Concurrency (..), runActions
+                   )
+import           Control.Concurrent.STM ( check )
+import qualified Data.List as L
+import           Data.List.Split ( chunksOf )
+import qualified Data.Map.Merge.Strict as Map
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Text as T
+import           Data.Tuple ( swap )
+import           Distribution.System ( OS (..), Platform (..) )
+import           Distribution.Version ( mkVersion )
+import           Path ( (</>),  parent )
+import           Path.CheckInstall ( warnInstallSearchPathIssues )
+import           Path.Extra ( forgivingResolveFile, rejectMissingFile )
+import           Path.IO ( ensureDir )
+import           RIO.NonEmpty ( nonEmpty )
+import qualified RIO.NonEmpty as NE
+import           RIO.Process ( HasProcessContext (..), proc, runProcess_ )
+import           Stack.Build.ExecuteEnv ( ExecuteEnv (..), withExecuteEnv )
+import           Stack.Build.ExecutePackage
+                   ( singleBench, singleBuild, singleTest )
+import           Stack.Build.Haddock
+                   ( generateDepsHaddockIndex
+                   , generateLocalHaddockForHackageArchives
+                   , generateLocalHaddockIndex, generateSnapHaddockIndex
+                   , openHaddocksInBrowser
+                   )
+import           Stack.Constants ( bindirSuffix )
+import           Stack.Coverage
+                   ( deleteHpcReports, generateHpcMarkupIndex
+                   , generateHpcUnifiedReport
+                   )
+import           Stack.GhcPkg ( unregisterGhcPkgIds )
+import           Stack.Prelude
+import           Stack.Types.Build
+                   ( ExcludeTHLoading (..), KeepOutputOpen (..), Plan (..)
+                   , Task (..), TaskConfigOpts (..), TaskType (..), taskLocation
+                   , taskProvides
+                   )
+import           Stack.Types.Build.Exception ( BuildPrettyException (..) )
+import           Stack.Types.BuildOpts ( BuildOpts (..), TestOpts (..)
+                   )
+import           Stack.Types.BuildOptsCLI ( BuildOptsCLI (..) )
+import           Stack.Types.BuildOptsMonoid ( ProgressBarFormat (..) )
+import           Stack.Types.Compiler ( ActualCompiler (..) )
+import           Stack.Types.CompilerPaths ( HasCompiler (..), getGhcPkgExe )
+import           Stack.Types.Config
+                   ( Config (..), HasConfig (..), buildOptsL )
+import           Stack.Types.ConfigureOpts
+                   ( BaseConfigOpts (..) )
+import           Stack.Types.DumpPackage ( DumpPackage (..) )
+import           Stack.Types.EnvConfig
+                   ( HasEnvConfig (..), actualCompilerVersionL
+                   , bindirCompilerTools, installationRootDeps
+                   , installationRootLocal, packageDatabaseLocal
+                   )
+import           Stack.Types.EnvSettings ( EnvSettings (..) )
+import           Stack.Types.GhcPkgId ( GhcPkgId )
+import           Stack.Types.Installed
+                   ( InstallLocation (..), InstalledMap
+                   , installedPackageIdentifier
+                   )
+import           Stack.Types.NamedComponent
+                   ( NamedComponent, benchComponents, testComponents )
+import           Stack.Types.Package
+                   ( LocalPackage (..), Package (..), packageIdentifier )
+import           Stack.Types.Platform ( HasPlatform (..) )
+import           Stack.Types.Runner ( HasRunner, terminalL )
+import           Stack.Types.SourceMap ( Target )
+import qualified System.Directory as D
+import           System.Environment ( getExecutablePath )
+import qualified System.FilePath as FP
+
+-- | Fetch the packages necessary for a build, for example in combination with
+-- a dry run.
+preFetch :: HasEnvConfig env => Plan -> RIO env ()
+preFetch plan
+  | Set.null pkgLocs = logDebug "Nothing to fetch"
+  | otherwise = do
+      logDebug $
+           "Prefetching: "
+        <> mconcat (L.intersperse ", " (display <$> Set.toList pkgLocs))
+      fetchPackages pkgLocs
+ where
+  pkgLocs = Set.unions $ map toPkgLoc $ Map.elems plan.tasks
+
+  toPkgLoc task =
+    case task.taskType of
+      TTLocalMutable{} -> Set.empty
+      TTRemotePackage _ _ pkgloc -> Set.singleton pkgloc
+
+-- | Print a description of build plan for human consumption.
+printPlan :: (HasRunner env, HasTerm env) => Plan -> RIO env ()
+printPlan plan = do
+  case Map.elems plan.unregisterLocal of
+    [] -> prettyInfo $
+               flow "No packages would be unregistered."
+            <> line
+    xs -> do
+      let unregisterMsg (ident, reason) = fillSep $
+              fromString (packageIdentifierString ident)
+            : [ parens $ flow (T.unpack reason) | not $ T.null reason ]
+      prettyInfo $
+           flow "Would unregister locally:"
+        <> line
+        <> bulletedList (map unregisterMsg xs)
+        <> line
+
+  case Map.elems plan.tasks of
+    [] -> prettyInfo $
+               flow "Nothing to build."
+            <> line
+    xs -> do
+      prettyInfo $
+           flow "Would build:"
+        <> line
+        <> bulletedList (map displayTask xs)
+        <> line
+
+  let hasTests = not . Set.null . testComponents . taskComponents
+      hasBenches = not . Set.null . benchComponents . taskComponents
+      tests = Map.elems $ Map.filter hasTests plan.finals
+      benches = Map.elems $ Map.filter hasBenches plan.finals
+
+  unless (null tests) $ do
+    prettyInfo $
+         flow "Would test:"
+      <> line
+      <> bulletedList (map displayTask tests)
+      <> line
+
+  unless (null benches) $ do
+    prettyInfo $
+         flow "Would benchmark:"
+      <> line
+      <> bulletedList (map displayTask benches)
+      <> line
+
+  case Map.toList plan.installExes of
+    [] -> prettyInfo $
+               flow "No executables to be installed."
+            <> line
+    xs -> do
+      let executableMsg (name, loc) = fillSep $
+              fromString (T.unpack name)
+            : "from"
+            : ( case loc of
+                  Snap -> "snapshot" :: StyleDoc
+                  Local -> "local" :: StyleDoc
+              )
+            : ["database."]
+      prettyInfo $
+           flow "Would install executables:"
+        <> line
+        <> bulletedList (map executableMsg xs)
+        <> line
+
+-- | For a dry run
+displayTask :: Task -> StyleDoc
+displayTask task = fillSep $
+     [ fromString (packageIdentifierString (taskProvides task)) <> ":"
+     ,    "database="
+       <> ( case taskLocation task of
+              Snap -> "snapshot" :: StyleDoc
+              Local -> "local" :: StyleDoc
+          )
+       <> ","
+     ,    "source="
+       <> ( case task.taskType of
+              TTLocalMutable lp -> pretty $ parent lp.cabalFP
+              TTRemotePackage _ _ pl -> fromString $ T.unpack $ textDisplay pl
+          )
+       <> if Set.null missing
+            then mempty
+            else ","
+     ]
+  <> [ fillSep $
+           "after:"
+         : mkNarrativeList Nothing False
+             (map fromPackageId (Set.toList missing) :: [StyleDoc])
+     | not $ Set.null missing
+     ]
+ where
+  missing = task.configOpts.missing
+
+-- | Perform the actual plan
+executePlan :: HasEnvConfig env
+            => BuildOptsCLI
+            -> BaseConfigOpts
+            -> [LocalPackage]
+            -> [DumpPackage] -- ^ global packages
+            -> [DumpPackage] -- ^ snapshot packages
+            -> [DumpPackage] -- ^ local packages
+            -> InstalledMap
+            -> Map PackageName Target
+            -> Plan
+            -> RIO env ()
+executePlan
+    boptsCli
+    baseConfigOpts
+    locals
+    globalPackages
+    snapshotPackages
+    localPackages
+    installedMap
+    targets
+    plan
+  = do
+    logDebug "Executing the build plan"
+    bopts <- view buildOptsL
+    withExecuteEnv
+      bopts
+      boptsCli
+      baseConfigOpts
+      locals
+      globalPackages
+      snapshotPackages
+      localPackages
+      mlargestPackageName
+      (executePlan' installedMap targets plan)
+
+    copyExecutables plan.installExes
+
+    config <- view configL
+    menv' <- liftIO $ config.processContextSettings EnvSettings
+               { includeLocals = True
+               , includeGhcPackagePath = True
+               , stackExe = True
+               , localeUtf8 = False
+               , keepGhcRts = False
+               }
+    withProcessContext menv' $
+      forM_ boptsCli.exec $ \(cmd, args) ->
+      proc cmd args runProcess_
+ where
+  mlargestPackageName =
+    Set.lookupMax $
+    Set.map (length . packageNameString) $
+    Map.keysSet plan.tasks <> Map.keysSet plan.finals
+
+copyExecutables ::
+       HasEnvConfig env
+    => Map Text InstallLocation
+    -> RIO env ()
+copyExecutables exes | Map.null exes = pure ()
+copyExecutables exes = do
+  snapBin <- (</> bindirSuffix) <$> installationRootDeps
+  localBin <- (</> bindirSuffix) <$> installationRootLocal
+  compilerSpecific <- (.installCompilerTool) <$> view buildOptsL
+  destDir <- if compilerSpecific
+               then bindirCompilerTools
+               else view $ configL . to (.localBin)
+  ensureDir destDir
+
+  destDir' <- liftIO . D.canonicalizePath . toFilePath $ destDir
+
+  platform <- view platformL
+  let ext =
+        case platform of
+          Platform _ Windows -> ".exe"
+          _ -> ""
+
+  currExe <- liftIO getExecutablePath -- needed for windows, see below
+
+  installed <- forMaybeM (Map.toList exes) $ \(name, loc) -> do
+    let bindir =
+            case loc of
+                Snap -> snapBin
+                Local -> localBin
+    mfp <- forgivingResolveFile bindir (T.unpack name ++ ext)
+      >>= rejectMissingFile
+    case mfp of
+      Nothing -> do
+        prettyWarnL
+          [ flow "Couldn't find executable"
+          , style Current (fromString $ T.unpack name)
+          , flow "in directory"
+          , pretty bindir <> "."
+          ]
+        pure Nothing
+      Just file -> do
+        let destFile = destDir' FP.</> T.unpack name ++ ext
+        prettyInfoL
+          [ flow "Copying from"
+          , pretty file
+          , "to"
+          , style File (fromString destFile) <> "."
+          ]
+
+        liftIO $ case platform of
+          Platform _ Windows | FP.equalFilePath destFile currExe ->
+              windowsRenameCopy (toFilePath file) destFile
+          _ -> D.copyFile (toFilePath file) destFile
+        pure $ Just (name <> T.pack ext)
+
+  unless (null installed) $ do
+    prettyInfo $
+         fillSep
+           [ flow "Copied executables to"
+           , pretty destDir <> ":"
+           ]
+      <> line
+      <> bulletedList
+           (map (fromString . T.unpack . textDisplay) installed :: [StyleDoc])
+  unless compilerSpecific $ warnInstallSearchPathIssues destDir' installed
+
+-- | Windows can't write over the current executable. Instead, we rename the
+-- current executable to something else and then do the copy.
+windowsRenameCopy :: FilePath -> FilePath -> IO ()
+windowsRenameCopy src dest = do
+  D.copyFile src new
+  D.renameFile dest old
+  D.renameFile new dest
+ where
+  new = dest ++ ".new"
+  old = dest ++ ".old"
+
+-- | Perform the actual plan (internal)
+executePlan' :: HasEnvConfig env
+             => InstalledMap
+             -> Map PackageName Target
+             -> Plan
+             -> ExecuteEnv
+             -> RIO env ()
+executePlan' installedMap0 targets plan ee = do
+  let !buildOpts = ee.buildOpts
+  let !testOpts = buildOpts.testOpts
+  when testOpts.coverage deleteHpcReports
+  cv <- view actualCompilerVersionL
+  case nonEmpty $ Map.toList plan.unregisterLocal of
+    Nothing -> pure ()
+    Just ids -> do
+      localDB <- packageDatabaseLocal
+      unregisterPackages cv localDB ids
+
+  liftIO $ atomically $ modifyTVar' ee.localDumpPkgs $ \initMap ->
+    foldl' (flip Map.delete) initMap $ Map.keys plan.unregisterLocal
+
+  run <- askRunInIO
+
+  -- If running tests concurrently with each other, then create an MVar
+  -- which is empty while each test is being run.
+  concurrentTests <- view $ configL . to (.concurrentTests)
+  mtestLock <- if concurrentTests
+                 then pure Nothing
+                 else Just <$> liftIO (newMVar ())
+
+  let actions = concatMap (toActions installedMap' mtestLock run ee) $
+        Map.elems $ Map.merge
+          (Map.mapMissing (\_ b -> (Just b, Nothing)))
+          (Map.mapMissing (\_ f -> (Nothing, Just f)))
+          (Map.zipWithMatched (\_ b f -> (Just b, Just f)))
+          plan.tasks
+          plan.finals
+  threads <- view $ configL . to (.jobs)
+  let keepGoing = fromMaybe
+        (not (Map.null plan.finals))
+        buildOpts.keepGoing
+  terminal <- view terminalL
+  terminalWidth <- view termWidthL
+  errs <- liftIO $ runActions threads keepGoing actions $
+    \doneVar actionsVar -> do
+      let total = length actions
+          loop prev
+            | prev == total =
+                run $ logStickyDone
+                  ( "Completed " <> display total <> " action(s).")
+            | otherwise = do
+                inProgress <- readTVarIO actionsVar
+                let packageNames = map
+                      (\(ActionId pkgID _) -> pkgName pkgID)
+                      (toList inProgress)
+                    nowBuilding :: [PackageName] -> Utf8Builder
+                    nowBuilding []    = ""
+                    nowBuilding names = mconcat $
+                        ": "
+                      : L.intersperse ", " (map fromPackageName names)
+                    progressFormat = buildOpts.progressBar
+                    progressLine prev' total' =
+                         "Progress "
+                      <> display prev' <> "/" <> display total'
+                      <> if progressFormat == CountOnlyBar
+                           then mempty
+                           else nowBuilding packageNames
+                    ellipsize n text =
+                      if T.length text <= n || progressFormat /= CappedBar
+                        then text
+                        else T.take (n - 1) text <> "…"
+                when (terminal && progressFormat /= NoBar) $
+                  run $ logSticky $ display $ ellipsize terminalWidth $
+                    utf8BuilderToText $ progressLine prev total
+                done <- atomically $ do
+                  done <- readTVar doneVar
+                  check $ done /= prev
+                  pure done
+                loop done
+      when (total > 1) $ loop 0
+  when testOpts.coverage $ do
+    generateHpcUnifiedReport
+    generateHpcMarkupIndex
+  unless (null errs) $
+    prettyThrowM $ ExecutionFailure errs
+  when buildOpts.buildHaddocks $ do
+    if buildOpts.haddockForHackage
+      then
+        generateLocalHaddockForHackageArchives ee.locals
+      else do
+        snapshotDumpPkgs <- liftIO (readTVarIO ee.snapshotDumpPkgs)
+        localDumpPkgs <- liftIO (readTVarIO ee.localDumpPkgs)
+        generateLocalHaddockIndex ee.baseConfigOpts localDumpPkgs ee.locals
+        generateDepsHaddockIndex
+          ee.baseConfigOpts
+          ee.globalDumpPkgs
+          snapshotDumpPkgs
+          localDumpPkgs
+          ee.locals
+        generateSnapHaddockIndex
+          ee.baseConfigOpts
+          ee.globalDumpPkgs
+          snapshotDumpPkgs
+        when buildOpts.openHaddocks $ do
+          let planPkgs, localPkgs, installedPkgs, availablePkgs
+                :: Map PackageName (PackageIdentifier, InstallLocation)
+              planPkgs =
+                Map.map (taskProvides &&& taskLocation) plan.tasks
+              localPkgs =
+                Map.fromList
+                  [ (p.name, (packageIdentifier p, Local))
+                  | p <- map (.package) ee.locals
+                  ]
+              installedPkgs =
+                Map.map (swap . second installedPackageIdentifier) installedMap'
+              availablePkgs = Map.unions [planPkgs, localPkgs, installedPkgs]
+          openHaddocksInBrowser
+            ee.baseConfigOpts
+            availablePkgs
+            (Map.keysSet targets)
+ where
+  installedMap' = Map.difference installedMap0
+                $ Map.fromList
+                $ map (\(ident, _) -> (pkgName ident, ()))
+                $ Map.elems plan.unregisterLocal
+
+unregisterPackages ::
+     (HasCompiler env, HasPlatform env, HasProcessContext env, HasTerm env)
+  => ActualCompiler
+  -> Path Abs Dir
+  -> NonEmpty (GhcPkgId, (PackageIdentifier, Text))
+  -> RIO env ()
+unregisterPackages cv localDB ids = do
+  let logReason ident reason =
+        prettyInfoL
+          (  [ fromString (packageIdentifierString ident) <> ":"
+             , "unregistering"
+             ]
+          <> [ parens (flow $ T.unpack reason) | not $ T.null reason ]
+          )
+  let unregisterSinglePkg select (gid, (ident, reason)) = do
+        logReason ident reason
+        pkg <- getGhcPkgExe
+        unregisterGhcPkgIds True pkg localDB $ select ident gid :| []
+  case cv of
+    -- GHC versions >= 8.2.1 support batch unregistering of packages. See
+    -- https://gitlab.haskell.org/ghc/ghc/issues/12637
+    ACGhc v | v >= mkVersion [8, 2, 1] -> do
+      platform <- view platformL
+      -- According to
+      -- https://support.microsoft.com/en-us/help/830473/command-prompt-cmd-exe-command-line-string-limitation
+      -- the maximum command line length on Windows since XP is 8191 characters.
+      -- We use conservative batch size of 100 ids on this OS thus argument name
+      -- '-ipid', package name, its version and a hash should fit well into this
+      -- limit. On Unix-like systems we're limited by ARG_MAX which is normally
+      -- hundreds of kilobytes so batch size of 500 should work fine.
+      let batchSize = case platform of
+            Platform _ Windows -> 100
+            _ -> 500
+      let chunksOfNE size = mapMaybe nonEmpty . chunksOf size . NE.toList
+      for_ (chunksOfNE batchSize ids) $ \batch -> do
+        for_ batch $ \(_, (ident, reason)) -> logReason ident reason
+        pkg <- getGhcPkgExe
+        unregisterGhcPkgIds True pkg localDB $ fmap (Right . fst) batch
+
+    -- GHC versions >= 7.9 support unregistering of packages via their GhcPkgId.
+    ACGhc v | v >= mkVersion [7, 9] ->
+      for_ ids . unregisterSinglePkg $ \_ident gid -> Right gid
+
+    _ -> for_ ids . unregisterSinglePkg $ \ident _gid -> Left ident
+
+toActions :: HasEnvConfig env
+          => InstalledMap
+          -> Maybe (MVar ())
+          -> (RIO env () -> IO ())
+          -> ExecuteEnv
+          -> (Maybe Task, Maybe Task) -- build and final
+          -> [Action]
+toActions installedMap mtestLock runInBase ee (mbuild, mfinal) =
+  abuild ++ afinal
+ where
+  abuild = case mbuild of
+    Nothing -> []
+    Just task ->
+      [ Action
+          { actionId = ActionId (taskProvides task) ATBuild
+          , actionDeps =
+              Set.map (`ActionId` ATBuild) task.configOpts.missing
+          , action =
+              \ac -> runInBase $ singleBuild ac ee task installedMap False
+          , concurrency = ConcurrencyAllowed
+          }
+      ]
+  afinal = case mfinal of
+    Nothing -> []
+    Just task ->
+      ( if task.allInOne
+          then id
+          else (:) Action
+            { actionId = ActionId pkgId ATBuildFinal
+            , actionDeps = addBuild
+                (Set.map (`ActionId` ATBuild) task.configOpts.missing)
+            , action =
+                \ac -> runInBase $ singleBuild ac ee task installedMap True
+            , concurrency = ConcurrencyAllowed
+            }
+      ) $
+      -- These are the "final" actions - running tests and benchmarks.
+      ( if Set.null tests
+          then id
+          else (:) Action
+            { actionId = ActionId pkgId ATRunTests
+            , actionDeps = finalDeps
+            , action = \ac -> withLock mtestLock $ runInBase $
+                singleTest topts (Set.toList tests) ac ee task installedMap
+              -- Always allow tests tasks to run concurrently with other tasks,
+              -- particularly build tasks. Note that 'mtestLock' can optionally
+              -- make it so that only one test is run at a time.
+            , concurrency = ConcurrencyAllowed
+            }
+      ) $
+      ( if Set.null benches
+          then id
+          else (:) Action
+            { actionId = ActionId pkgId ATRunBenchmarks
+            , actionDeps = finalDeps
+            , action = \ac -> runInBase $
+                singleBench
+                  beopts
+                  (Set.toList benches)
+                  ac
+                  ee
+                  task
+                  installedMap
+              -- Never run benchmarks concurrently with any other task, see
+              -- #3663
+            , concurrency = ConcurrencyDisallowed
+            }
+      )
+      []
+     where
+      pkgId = taskProvides task
+      comps = taskComponents task
+      tests = testComponents comps
+      benches = benchComponents comps
+      finalDeps =
+        if task.allInOne
+          then addBuild mempty
+          else Set.singleton (ActionId pkgId ATBuildFinal)
+      addBuild =
+        case mbuild of
+          Nothing -> id
+          Just _ -> Set.insert $ ActionId pkgId ATBuild
+  withLock Nothing f = f
+  withLock (Just lock) f = withMVar lock $ \() -> f
+  bopts = ee.buildOpts
+  topts = bopts.testOpts
+  beopts = bopts.benchmarkOpts
+
+taskComponents :: Task -> Set NamedComponent
+taskComponents task =
+  case task.taskType of
+    TTLocalMutable lp -> lp.components -- FIXME probably just want lpWanted
+    TTRemotePackage{} -> Set.empty
+ src/Stack/Build/ExecuteEnv.hs view
@@ -0,0 +1,1046 @@+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE TypeFamilies          #-}
+
+-- | Provides all the necessary types and functions for running cabal Setup.hs
+-- commands. Only used in the "Execute" and "ExecutePackage" modules
+module Stack.Build.ExecuteEnv
+  ( ExecuteEnv (..)
+  , withExecuteEnv
+  , withSingleContext
+  , ExcludeTHLoading (..)
+  , KeepOutputOpen (..)
+  , ExecutableBuildStatus (..)
+  , OutputType (..)
+  ) where
+
+import           Control.Concurrent.Companion ( Companion, withCompanion )
+import           Control.Concurrent.Execute
+                   ( ActionContext (..), ActionId (..), Concurrency (..) )
+import           Crypto.Hash ( SHA256 (..), hashWith )
+import           Data.Attoparsec.Text ( char, choice, digit, parseOnly )
+import qualified Data.Attoparsec.Text as P ( string )
+import qualified Data.ByteArray as Mem ( convert )
+import qualified Data.ByteString as S
+import qualified Data.ByteString.Base64.URL as B64URL
+import qualified Data.ByteString.Builder ( toLazyByteString )
+import qualified Data.ByteString.Char8 as S8
+import           Data.Char ( isSpace )
+import           Conduit
+                   ( ConduitT, awaitForever, sinkHandle, withSinkFile
+                   , withSourceFile, yield
+                   )
+import qualified Data.Conduit.Binary as CB
+import qualified Data.Conduit.List as CL
+import qualified Data.Conduit.Text as CT
+import qualified Data.List as L
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Text as T
+import           Data.Text.Encoding ( decodeUtf8 )
+import           Data.Time
+                   ( ZonedTime, defaultTimeLocale, formatTime, getZonedTime )
+import qualified Distribution.PackageDescription as C
+import qualified Distribution.Simple.Build.Macros as C
+import           Distribution.System ( OS (..), Platform (..) )
+import           Distribution.Types.PackageName ( mkPackageName )
+import           Distribution.Verbosity ( showForCabal )
+import           Distribution.Version ( mkVersion )
+import           Path
+                   ( PathException, (</>), parent, parseRelDir, parseRelFile )
+import           Path.Extra ( forgivingResolveFile, toFilePathNoTrailingSep )
+import           Path.IO
+                   ( doesDirExist, doesFileExist, ensureDir, ignoringAbsence
+                   , removeFile, renameDir, renameFile
+                   )
+import           RIO.Process
+                   ( eceExitCode, proc, runProcess_, setStdout, useHandleOpen
+                   , withWorkingDir
+                   )
+import           Stack.Config ( checkOwnership )
+import           Stack.Constants
+                   ( cabalPackageName, relDirDist, relDirSetup
+                   , relDirSetupExeCache, relDirSetupExeSrc, relFileBuildLock
+                   , relFileSetupHs, relFileSetupLhs, relFileSetupLower
+                   , relFileSetupMacrosH, setupGhciShimCode, stackProgName
+                   )
+import           Stack.Constants.Config ( distDirFromDir, distRelativeDir )
+import           Stack.Package ( buildLogPath )
+import           Stack.Prelude
+import           Stack.Types.ApplyGhcOptions ( ApplyGhcOptions (..) )
+import           Stack.Types.Build
+                   ( ConvertPathsToAbsolute (..), ExcludeTHLoading (..)
+                   , KeepOutputOpen (..), TaskType (..), taskTypeLocation
+                   , taskTypePackageIdentifier
+                   )
+import           Stack.Types.Build.Exception
+                   ( BuildException (..), BuildPrettyException (..) )
+import           Stack.Types.BuildOpts ( BuildOpts (..) )
+import           Stack.Types.BuildOptsCLI ( BuildOptsCLI (..) )
+import           Stack.Types.BuildOptsMonoid ( CabalVerbosity (..) )
+import           Stack.Types.Compiler
+                   ( ActualCompiler (..), WhichCompiler (..)
+                   , compilerVersionString, getGhcVersion, whichCompilerL
+                   )
+import           Stack.Types.CompilerPaths
+                   ( CompilerPaths (..), HasCompiler (..), cabalVersionL
+                   , getCompilerPath
+                   )
+import           Stack.Types.Config
+                   ( Config (..), HasConfig (..), stackRootL )
+import           Stack.Types.ConfigureOpts ( BaseConfigOpts (..) )
+import           Stack.Types.Dependency ( DepValue(..) )
+import           Stack.Types.DumpLogs ( DumpLogs (..) )
+import           Stack.Types.DumpPackage ( DumpPackage (..) )
+import           Stack.Types.EnvConfig
+                   ( HasEnvConfig (..), actualCompilerVersionL
+                   , platformGhcRelDir, shouldForceGhcColorFlag
+                   )
+import           Stack.Types.EnvSettings ( EnvSettings (..) )
+import           Stack.Types.GhcPkgId ( GhcPkgId, ghcPkgIdString )
+import           Stack.Types.Installed ( InstallLocation (..), Installed (..) )
+import           Stack.Types.Package
+                   ( LocalPackage (..), Package (..), packageIdentifier )
+import           Stack.Types.Platform ( HasPlatform (..) )
+import           Stack.Types.Version ( withinRange )
+import qualified System.Directory as D
+import           System.Environment ( lookupEnv )
+import           System.FileLock
+                   ( SharedExclusive (..), withFileLock, withTryFileLock )
+
+-- | Has an executable been built or not?
+data ExecutableBuildStatus
+  = ExecutableBuilt
+  | ExecutableNotBuilt
+  deriving (Eq, Ord, Show)
+
+data ExecuteEnv = ExecuteEnv
+  { installLock    :: !(MVar ())
+  , buildOpts      :: !BuildOpts
+  , buildOptsCLI   :: !BuildOptsCLI
+  , baseConfigOpts :: !BaseConfigOpts
+  , ghcPkgIds      :: !(TVar (Map PackageIdentifier Installed))
+  , tempDir        :: !(Path Abs Dir)
+  , setupHs        :: !(Path Abs File)
+    -- ^ Temporary Setup.hs for simple builds
+  , setupShimHs    :: !(Path Abs File)
+    -- ^ Temporary SetupShim.hs, to provide access to initial-build-steps
+  , setupExe       :: !(Maybe (Path Abs File))
+    -- ^ Compiled version of eeSetupHs
+  , cabalPkgVer    :: !Version
+  , totalWanted    :: !Int
+  , locals         :: ![LocalPackage]
+  , globalDB       :: !(Path Abs Dir)
+  , globalDumpPkgs :: !(Map GhcPkgId DumpPackage)
+  , snapshotDumpPkgs :: !(TVar (Map GhcPkgId DumpPackage))
+  , localDumpPkgs  :: !(TVar (Map GhcPkgId DumpPackage))
+  , logFiles       :: !(TChan (Path Abs Dir, Path Abs File))
+  , customBuilt    :: !(IORef (Set PackageName))
+    -- ^ Stores which packages with custom-setup have already had their
+    -- Setup.hs built.
+  , largestPackageName :: !(Maybe Int)
+    -- ^ For nicer interleaved output: track the largest package name size
+  , pathEnvVar :: !Text
+    -- ^ Value of the PATH environment variable
+  }
+
+buildSetupArgs :: [String]
+buildSetupArgs =
+  [ "-rtsopts"
+  , "-threaded"
+  , "-clear-package-db"
+  , "-global-package-db"
+  , "-hide-all-packages"
+  , "-package"
+  , "base"
+  , "-main-is"
+  , "StackSetupShim.mainOverride"
+  ]
+
+simpleSetupCode :: Builder
+simpleSetupCode = "import Distribution.Simple\nmain = defaultMain"
+
+simpleSetupHash :: String
+simpleSetupHash =
+    T.unpack
+  $ decodeUtf8
+  $ S.take 8
+  $ B64URL.encode
+  $ Mem.convert
+  $ hashWith SHA256
+  $ toStrictBytes
+  $ Data.ByteString.Builder.toLazyByteString
+  $  encodeUtf8Builder (T.pack (unwords buildSetupArgs))
+  <> setupGhciShimCode
+  <> simpleSetupCode
+
+-- | Get a compiled Setup exe
+getSetupExe :: HasEnvConfig env
+            => Path Abs File -- ^ Setup.hs input file
+            -> Path Abs File -- ^ SetupShim.hs input file
+            -> Path Abs Dir -- ^ temporary directory
+            -> RIO env (Maybe (Path Abs File))
+getSetupExe setupHs setupShimHs tmpdir = do
+  wc <- view $ actualCompilerVersionL . whichCompilerL
+  platformDir <- platformGhcRelDir
+  config <- view configL
+  cabalVersionString <- view $ cabalVersionL . to versionString
+  actualCompilerVersionString <-
+    view $ actualCompilerVersionL . to compilerVersionString
+  platform <- view platformL
+  let baseNameS = concat
+        [ "Cabal-simple_"
+        , simpleSetupHash
+        , "_"
+        , cabalVersionString
+        , "_"
+        , actualCompilerVersionString
+        ]
+      exeNameS = baseNameS ++
+        case platform of
+          Platform _ Windows -> ".exe"
+          _ -> ""
+      outputNameS =
+        case wc of
+            Ghc -> exeNameS
+      setupDir =
+        view stackRootL config </>
+        relDirSetupExeCache </>
+        platformDir
+
+  exePath <- (setupDir </>) <$> parseRelFile exeNameS
+
+  exists <- liftIO $ D.doesFileExist $ toFilePath exePath
+
+  if exists
+    then pure $ Just exePath
+    else do
+      tmpExePath <- fmap (setupDir </>) $ parseRelFile $ "tmp-" ++ exeNameS
+      tmpOutputPath <-
+        fmap (setupDir </>) $ parseRelFile $ "tmp-" ++ outputNameS
+      ensureDir setupDir
+      let args = buildSetupArgs ++
+            [ "-package"
+            , "Cabal-" ++ cabalVersionString
+            , toFilePath setupHs
+            , toFilePath setupShimHs
+            , "-o"
+            , toFilePath tmpOutputPath
+              -- See https://github.com/commercialhaskell/stack/issues/6267. As
+              -- corrupt *.hi and/or *.o files can be problematic, we aim to
+              -- to leave none behind. This can be dropped when Stack drops
+              -- support for the problematic versions of GHC.
+            , "-no-keep-hi-files"
+            , "-no-keep-o-files"
+            ]
+      compilerPath <- getCompilerPath
+      withWorkingDir (toFilePath tmpdir) $
+        proc (toFilePath compilerPath) args (\pc0 -> do
+          let pc = setStdout (useHandleOpen stderr) pc0
+          runProcess_ pc)
+            `catch` \ece ->
+              prettyThrowM $ SetupHsBuildFailure
+                (eceExitCode ece) Nothing compilerPath args Nothing []
+      renameFile tmpExePath exePath
+      pure $ Just exePath
+
+-- | Execute a function that takes an 'ExecuteEnv'.
+withExecuteEnv ::
+     forall env a. HasEnvConfig env
+  => BuildOpts
+  -> BuildOptsCLI
+  -> BaseConfigOpts
+  -> [LocalPackage]
+  -> [DumpPackage] -- ^ global packages
+  -> [DumpPackage] -- ^ snapshot packages
+  -> [DumpPackage] -- ^ local packages
+  -> Maybe Int -- ^ largest package name, for nicer interleaved output
+  -> (ExecuteEnv -> RIO env a)
+  -> RIO env a
+withExecuteEnv
+    buildOpts
+    buildOptsCLI
+    baseConfigOpts
+    locals
+    globalPackages
+    snapshotPackages
+    localPackages
+    largestPackageName
+    inner
+  = createTempDirFunction stackProgName $ \tempDir -> do
+      installLock <- liftIO $ newMVar ()
+      ghcPkgIds <- liftIO $ newTVarIO Map.empty
+      config <- view configL
+      customBuilt <- newIORef Set.empty
+      -- Create files for simple setup and setup shim, if necessary
+      let setupSrcDir =
+              view stackRootL config </>
+              relDirSetupExeSrc
+      ensureDir setupSrcDir
+      let setupStub = "setup-" ++ simpleSetupHash
+      setupFileName <- parseRelFile (setupStub ++ ".hs")
+      setupHiName <- parseRelFile (setupStub ++ ".hi")
+      setupOName <- parseRelFile (setupStub ++ ".o")
+      let setupHs = setupSrcDir </> setupFileName
+          setupHi = setupSrcDir </> setupHiName
+          setupO =  setupSrcDir </> setupOName
+      setupHsExists <- doesFileExist setupHs
+      unless setupHsExists $ writeBinaryFileAtomic setupHs simpleSetupCode
+      -- See https://github.com/commercialhaskell/stack/issues/6267. Remove any
+      -- historical *.hi or *.o files. This can be dropped when Stack drops
+      -- support for the problematic versions of GHC.
+      ignoringAbsence (removeFile setupHi)
+      ignoringAbsence (removeFile setupO)
+      let setupShimStub = "setup-shim-" ++ simpleSetupHash
+      setupShimFileName <- parseRelFile (setupShimStub ++ ".hs")
+      setupShimHiName <- parseRelFile (setupShimStub ++ ".hi")
+      setupShimOName <- parseRelFile (setupShimStub ++ ".o")
+      let setupShimHs = setupSrcDir </> setupShimFileName
+          setupShimHi = setupSrcDir </> setupShimHiName
+          setupShimO = setupSrcDir </> setupShimOName
+      setupShimHsExists <- doesFileExist setupShimHs
+      unless setupShimHsExists $
+        writeBinaryFileAtomic setupShimHs setupGhciShimCode
+      -- See https://github.com/commercialhaskell/stack/issues/6267. Remove any
+      -- historical *.hi or *.o files. This can be dropped when Stack drops
+      -- support for the problematic versions of GHC.
+      ignoringAbsence (removeFile setupShimHi)
+      ignoringAbsence (removeFile setupShimO)
+      setupExe <- getSetupExe setupHs setupShimHs tempDir
+      cabalPkgVer <- view cabalVersionL
+      globalDB <- view $ compilerPathsL . to (.globalDB)
+      let globalDumpPkgs = toDumpPackagesByGhcPkgId globalPackages
+      snapshotDumpPkgs <-
+        liftIO $ newTVarIO (toDumpPackagesByGhcPkgId snapshotPackages)
+      localDumpPkgs <-
+        liftIO $ newTVarIO (toDumpPackagesByGhcPkgId localPackages)
+      logFiles <- liftIO $ atomically newTChan
+      let totalWanted = length $ filter (.wanted) locals
+      pathEnvVar <- liftIO $ maybe mempty T.pack <$> lookupEnv "PATH"
+      inner ExecuteEnv
+        { buildOpts
+        , buildOptsCLI
+          -- Uncertain as to why we cannot run configures in parallel. This
+          -- appears to be a Cabal library bug. Original issue:
+          -- https://github.com/commercialhaskell/stack/issues/84. Ideally
+          -- we'd be able to remove this.
+        , installLock
+        , baseConfigOpts
+        , ghcPkgIds
+        , tempDir
+        , setupHs
+        , setupShimHs
+        , setupExe
+        , cabalPkgVer
+        , totalWanted
+        , locals
+        , globalDB
+        , globalDumpPkgs
+        , snapshotDumpPkgs
+        , localDumpPkgs
+        , logFiles
+        , customBuilt
+        , largestPackageName
+        , pathEnvVar
+        } `finally` dumpLogs logFiles totalWanted
+ where
+  toDumpPackagesByGhcPkgId = Map.fromList . map (\dp -> (dp.ghcPkgId, dp))
+
+  createTempDirFunction
+    | buildOpts.keepTmpFiles = withKeepSystemTempDir
+    | otherwise = withSystemTempDir
+
+  dumpLogs :: TChan (Path Abs Dir, Path Abs File) -> Int -> RIO env ()
+  dumpLogs chan totalWanted = do
+    allLogs <- fmap reverse $ liftIO $ atomically drainChan
+    case allLogs of
+      -- No log files generated, nothing to dump
+      [] -> pure ()
+      firstLog:_ -> do
+        toDump <- view $ configL . to (.dumpLogs)
+        case toDump of
+          DumpAllLogs -> mapM_ (dumpLog "") allLogs
+          DumpWarningLogs -> mapM_ dumpLogIfWarning allLogs
+          DumpNoLogs
+              | totalWanted > 1 ->
+                  prettyInfoL
+                    [ flow "Build output has been captured to log files, use"
+                    , style Shell "--dump-logs"
+                    , flow "to see it on the console."
+                    ]
+              | otherwise -> pure ()
+        prettyInfoL
+          [ flow "Log files have been written to:"
+          , pretty (parent (snd firstLog))
+          ]
+
+    -- We only strip the colors /after/ we've dumped logs, so that we get pretty
+    -- colors in our dump output on the terminal.
+    colors <- shouldForceGhcColorFlag
+    when colors $ liftIO $ mapM_ (stripColors . snd) allLogs
+   where
+    drainChan :: STM [(Path Abs Dir, Path Abs File)]
+    drainChan = do
+      mx <- tryReadTChan chan
+      case mx of
+        Nothing -> pure []
+        Just x -> do
+          xs <- drainChan
+          pure $ x:xs
+
+  dumpLogIfWarning :: (Path Abs Dir, Path Abs File) -> RIO env ()
+  dumpLogIfWarning (pkgDir, filepath) = do
+    firstWarning <- withSourceFile (toFilePath filepath) $ \src ->
+         runConduit
+       $ src
+      .| CT.decodeUtf8Lenient
+      .| CT.lines
+      .| CL.map stripCR
+      .| CL.filter isWarning
+      .| CL.take 1
+    unless (null firstWarning) $ dumpLog " due to warnings" (pkgDir, filepath)
+
+  isWarning :: Text -> Bool
+  isWarning t = ": Warning:" `T.isSuffixOf` t -- prior to GHC 8
+             || ": warning:" `T.isInfixOf` t -- GHC 8 is slightly different
+             || "mwarning:" `T.isInfixOf` t -- colorized output
+
+  dumpLog :: String -> (Path Abs Dir, Path Abs File) -> RIO env ()
+  dumpLog msgSuffix (pkgDir, filepath) = do
+    prettyNote $
+         fillSep
+           ( ( fillSep
+                 ( flow "Dumping log file"
+                 : [ flow msgSuffix | not (L.null msgSuffix) ]
+                 )
+             <> ":"
+             )
+           : [ pretty filepath <> "." ]
+           )
+      <> line
+    compilerVer <- view actualCompilerVersionL
+    withSourceFile (toFilePath filepath) $ \src ->
+         runConduit
+       $ src
+      .| CT.decodeUtf8Lenient
+      .| mungeBuildOutput ExcludeTHLoading ConvertPathsToAbsolute pkgDir compilerVer
+      .| CL.mapM_ (logInfo . display)
+    prettyNote $
+         fillSep
+           [ flow "End of log file:"
+           , pretty filepath <> "."
+           ]
+      <> line
+
+  stripColors :: Path Abs File -> IO ()
+  stripColors fp = do
+    let colorfp = toFilePath fp ++ "-color"
+    withSourceFile (toFilePath fp) $ \src ->
+      withSinkFile colorfp $ \sink ->
+      runConduit $ src .| sink
+    withSourceFile colorfp $ \src ->
+      withSinkFile (toFilePath fp) $ \sink ->
+      runConduit $ src .| noColors .| sink
+
+   where
+    noColors = do
+      CB.takeWhile (/= 27) -- ESC
+      mnext <- CB.head
+      case mnext of
+        Nothing -> pure ()
+        Just x -> assert (x == 27) $ do
+          -- Color sequences always end with an m
+          CB.dropWhile (/= 109) -- m
+          CB.drop 1 -- drop the m itself
+          noColors
+
+-- | Make a padded prefix for log messages
+packageNamePrefix :: ExecuteEnv -> PackageName -> String
+packageNamePrefix ee name' =
+  let name = packageNameString name'
+      paddedName =
+        case ee.largestPackageName of
+          Nothing -> name
+          Just len ->
+            assert (len >= length name) $ take len $ name ++ L.repeat ' '
+  in  paddedName <> "> "
+
+announceTask ::
+     HasLogFunc env
+  => ExecuteEnv
+  -> TaskType
+  -> Utf8Builder
+  -> RIO env ()
+announceTask ee taskType action = logInfo $
+     fromString
+       (packageNamePrefix ee (pkgName (taskTypePackageIdentifier taskType)))
+  <> action
+
+prettyAnnounceTask ::
+     HasTerm env
+  => ExecuteEnv
+  -> TaskType
+  -> StyleDoc
+  -> RIO env ()
+prettyAnnounceTask ee taskType action = prettyInfo $
+     fromString
+       (packageNamePrefix ee (pkgName (taskTypePackageIdentifier taskType)))
+  <> action
+
+-- | Ensure we're the only action using the directory.  See
+-- <https://github.com/commercialhaskell/stack/issues/2730>
+withLockedDistDir ::
+     forall env a. HasEnvConfig env
+  => (StyleDoc -> RIO env ()) -- ^ A pretty announce function
+  -> Path Abs Dir -- ^ root directory for package
+  -> RIO env a
+  -> RIO env a
+withLockedDistDir announce root inner = do
+  distDir <- distRelativeDir
+  let lockFP = root </> distDir </> relFileBuildLock
+  ensureDir $ parent lockFP
+
+  mres <-
+    withRunInIO $ \run ->
+    withTryFileLock (toFilePath lockFP) Exclusive $ \_lock ->
+    run inner
+
+  case mres of
+    Just res -> pure res
+    Nothing -> do
+      let complainer :: Companion (RIO env)
+          complainer delay = do
+            delay 5000000 -- 5 seconds
+            announce $ fillSep
+              [ flow "blocking for directory lock on"
+              , pretty lockFP
+              ]
+            forever $ do
+              delay 30000000 -- 30 seconds
+              announce $ fillSep
+                [ flow "still blocking for directory lock on"
+                , pretty lockFP <> ";"
+                , flow "maybe another Stack process is running?"
+                ]
+      withCompanion complainer $
+        \stopComplaining ->
+          withRunInIO $ \run ->
+            withFileLock (toFilePath lockFP) Exclusive $ \_ ->
+              run $ stopComplaining *> inner
+
+-- | How we deal with output from GHC, either dumping to a log file or the
+-- console (with some prefix).
+data OutputType
+  = OTLogFile !(Path Abs File) !Handle
+  | OTConsole !(Maybe Utf8Builder)
+
+-- | This sets up a context for executing build steps which need to run
+-- Cabal (via a compiled Setup.hs).  In particular it does the following:
+--
+-- * Ensures the package exists in the file system, downloading if necessary.
+--
+-- * Opens a log file if the built output shouldn't go to stderr.
+--
+-- * Ensures that either a simple Setup.hs is built, or the package's
+--   custom setup is built.
+--
+-- * Provides the user a function with which run the Cabal process.
+withSingleContext ::
+     forall env a. HasEnvConfig env
+  => ActionContext
+  -> ExecuteEnv
+  -> TaskType
+  -> Map PackageIdentifier GhcPkgId
+     -- ^ All dependencies' package ids to provide to Setup.hs.
+  -> Maybe String
+  -> (  Package        -- Package info
+     -> Path Abs File  -- Cabal file path
+     -> Path Abs Dir   -- Package root directory file path
+        -- Note that the `Path Abs Dir` argument is redundant with the
+        -- `Path Abs File` argument, but we provide both to avoid recalculating
+        -- `parent` of the `File`.
+     -> (KeepOutputOpen -> ExcludeTHLoading -> [String] -> RIO env ())
+        -- Function to run Cabal with args
+     -> (Utf8Builder -> RIO env ())
+        -- An plain 'announce' function, for different build phases
+     -> OutputType
+     -> RIO env a)
+  -> RIO env a
+withSingleContext
+    ac
+    ee
+    taskType
+    allDeps
+    msuffix
+    inner0
+  = withPackage $ \package cabalFP pkgDir ->
+      withOutputType pkgDir package $ \outputType ->
+        withCabal package pkgDir outputType $ \cabal ->
+          inner0 package cabalFP pkgDir cabal announce outputType
+ where
+  pkgId = taskTypePackageIdentifier taskType
+  announce = announceTask ee taskType
+  prettyAnnounce = prettyAnnounceTask ee taskType
+
+  wanted =
+    case taskType of
+      TTLocalMutable lp -> lp.wanted
+      TTRemotePackage{} -> False
+
+  -- Output to the console if this is the last task, and the user asked to build
+  -- it specifically. When the action is a 'ConcurrencyDisallowed' action
+  -- (benchmarks), then we can also be sure to have exclusive access to the
+  -- console, so output is also sent to the console in this case.
+  --
+  -- See the discussion on #426 for thoughts on sending output to the console
+  --from concurrent tasks.
+  console =
+       (  wanted
+       && all
+            (\(ActionId ident _) -> ident == pkgId)
+            (Set.toList ac.remaining)
+       && ee.totalWanted == 1
+       )
+    || ac.concurrency == ConcurrencyDisallowed
+
+  withPackage inner =
+    case taskType of
+      TTLocalMutable lp -> do
+        let root = parent lp.cabalFP
+        withLockedDistDir prettyAnnounce root $
+          inner lp.package lp.cabalFP root
+      TTRemotePackage _ package pkgloc -> do
+        suffix <-
+          parseRelDir $ packageIdentifierString $ packageIdentifier package
+        let dir = ee.tempDir </> suffix
+        unpackPackageLocation dir pkgloc
+
+        -- See: https://github.com/commercialhaskell/stack/issues/157
+        distDir <- distRelativeDir
+        let oldDist = dir </> relDirDist
+            newDist = dir </> distDir
+        exists <- doesDirExist oldDist
+        when exists $ do
+          -- Previously used takeDirectory, but that got confused
+          -- by trailing slashes, see:
+          -- https://github.com/commercialhaskell/stack/issues/216
+          --
+          -- Instead, use Path which is a bit more resilient
+          ensureDir $ parent newDist
+          renameDir oldDist newDist
+
+        let name = pkgName pkgId
+        cabalfpRel <- parseRelFile $ packageNameString name ++ ".cabal"
+        let cabalFP = dir </> cabalfpRel
+        inner package cabalFP dir
+
+  withOutputType pkgDir package inner
+    -- Not in interleaved mode. When building a single wanted package, dump
+    -- to the console with no prefix.
+    | console = inner $ OTConsole Nothing
+
+    -- If the user requested interleaved output, dump to the console with a
+    -- prefix.
+    | ee.buildOpts.interleavedOutput = inner $
+        OTConsole $ Just $ fromString (packageNamePrefix ee package.name)
+
+    -- Neither condition applies, dump to a file.
+    | otherwise = do
+        logPath <- buildLogPath package msuffix
+        ensureDir (parent logPath)
+        let fp = toFilePath logPath
+
+        -- We only want to dump logs for local non-dependency packages
+        case taskType of
+          TTLocalMutable lp | lp.wanted ->
+              liftIO $ atomically $ writeTChan ee.logFiles (pkgDir, logPath)
+          _ -> pure ()
+
+        withBinaryFile fp WriteMode $ \h -> inner $ OTLogFile logPath h
+
+  withCabal ::
+       Package
+    -> Path Abs Dir
+    -> OutputType
+    -> (  (KeepOutputOpen -> ExcludeTHLoading -> [String] -> RIO env ())
+       -> RIO env a
+       )
+    -> RIO env a
+  withCabal package pkgDir outputType inner = do
+    config <- view configL
+    unless config.allowDifferentUser $
+      checkOwnership (pkgDir </> config.workDir)
+    let envSettings = EnvSettings
+          { includeLocals = taskTypeLocation taskType == Local
+          , includeGhcPackagePath = False
+          , stackExe = False
+          , localeUtf8 = True
+          , keepGhcRts = False
+          }
+    menv <- liftIO $ config.processContextSettings envSettings
+    distRelativeDir' <- distRelativeDir
+    esetupexehs <-
+      -- Avoid broken Setup.hs files causing problems for simple build
+      -- types, see:
+      -- https://github.com/commercialhaskell/stack/issues/370
+      case (package.buildType, ee.setupExe) of
+        (C.Simple, Just setupExe) -> pure $ Left setupExe
+        _ -> liftIO $ Right <$> getSetupHs pkgDir
+    inner $ \keepOutputOpen stripTHLoading args -> do
+      let cabalPackageArg
+            -- Omit cabal package dependency when building
+            -- Cabal. See
+            -- https://github.com/commercialhaskell/stack/issues/1356
+            | package.name == mkPackageName "Cabal" = []
+            | otherwise =
+                ["-package=" ++ packageIdentifierString
+                                    (PackageIdentifier cabalPackageName
+                                                      ee.cabalPkgVer)]
+          packageDBArgs =
+            ( "-clear-package-db"
+            : "-global-package-db"
+            : map
+                (("-package-db=" ++) . toFilePathNoTrailingSep)
+                ee.baseConfigOpts.extraDBs
+            ) ++
+            ( (  "-package-db="
+              ++ toFilePathNoTrailingSep ee.baseConfigOpts.snapDB
+              )
+            : (  "-package-db="
+              ++ toFilePathNoTrailingSep ee.baseConfigOpts.localDB
+              )
+            : ["-hide-all-packages"]
+            )
+
+          warnCustomNoDeps :: RIO env ()
+          warnCustomNoDeps =
+            case (taskType, package.buildType) of
+              (TTLocalMutable lp, C.Custom) | lp.wanted ->
+                prettyWarnL
+                  [ flow "Package"
+                  , fromPackageName package.name
+                  , flow "uses a custom Cabal build, but does not use a \
+                         \custom-setup stanza"
+                  ]
+              _ -> pure ()
+
+          getPackageArgs :: Path Abs Dir -> RIO env [String]
+          getPackageArgs setupDir =
+            case package.setupDeps of
+              -- The package is using the Cabal custom-setup
+              -- configuration introduced in Cabal 1.24. In
+              -- this case, the package is providing an
+              -- explicit list of dependencies, and we
+              -- should simply use all of them.
+              Just customSetupDeps -> do
+                unless (Map.member (mkPackageName "Cabal") customSetupDeps) $
+                  prettyWarnL
+                    [ fromPackageName package.name
+                    , flow "has a setup-depends field, but it does not mention \
+                           \a Cabal dependency. This is likely to cause build \
+                           \errors."
+                    ]
+                matchedDeps <-
+                  forM (Map.toList customSetupDeps) $ \(name, depValue) -> do
+                    let matches (PackageIdentifier name' version) =
+                             name == name'
+                          && version `withinRange` depValue.versionRange
+                    case filter (matches . fst) (Map.toList allDeps) of
+                      x:xs -> do
+                        unless (null xs) $
+                          prettyWarnL
+                            [ flow "Found multiple installed packages for \
+                                   \custom-setup dep:"
+                            , style Current (fromPackageName name) <> "."
+                            ]
+                        pure ("-package-id=" ++ ghcPkgIdString (snd x), Just (fst x))
+                      [] -> do
+                        prettyWarnL
+                          [ flow "Could not find custom-setup dep:"
+                          , style Current (fromPackageName name) <> "."
+                          ]
+                        pure ("-package=" ++ packageNameString name, Nothing)
+                let depsArgs = map fst matchedDeps
+                -- Generate setup_macros.h and provide it to ghc
+                let macroDeps = mapMaybe snd matchedDeps
+                    cppMacrosFile = setupDir </> relFileSetupMacrosH
+                    cppArgs =
+                      ["-optP-include", "-optP" ++ toFilePath cppMacrosFile]
+                writeBinaryFileAtomic
+                  cppMacrosFile
+                  ( encodeUtf8Builder
+                      ( T.pack
+                          ( C.generatePackageVersionMacros
+                              package.version
+                              macroDeps
+                          )
+                      )
+                  )
+                pure (packageDBArgs ++ depsArgs ++ cppArgs)
+
+              -- This branch is usually taken for builds, and is always taken
+              -- for `stack sdist`.
+              --
+              -- This approach is debatable. It adds access to the snapshot
+              -- package database for Cabal. There are two possible objections:
+              --
+              -- 1. This doesn't isolate the build enough; arbitrary other
+              -- packages available could cause the build to succeed or fail.
+              --
+              -- 2. This doesn't provide enough packages: we should also
+              -- include the local database when building local packages.
+              --
+              -- Currently, this branch is only taken via `stack sdist` or when
+              -- explicitly requested in the stack.yaml file.
+              Nothing -> do
+                warnCustomNoDeps
+                pure $
+                     cabalPackageArg
+                      -- NOTE: This is different from packageDBArgs above in
+                      -- that it does not include the local database and does
+                      -- not pass in the -hide-all-packages argument
+                  ++ ( "-clear-package-db"
+                     : "-global-package-db"
+                     : map
+                         (("-package-db=" ++) . toFilePathNoTrailingSep)
+                         ee.baseConfigOpts.extraDBs
+                     ++ [    "-package-db="
+                          ++ toFilePathNoTrailingSep ee.baseConfigOpts.snapDB
+                        ]
+                     )
+
+          setupArgs =
+            ("--builddir=" ++ toFilePathNoTrailingSep distRelativeDir') : args
+
+          runExe :: Path Abs File -> [String] -> RIO env ()
+          runExe exeName fullArgs = do
+            compilerVer <- view actualCompilerVersionL
+            runAndOutput compilerVer `catch` \ece -> do
+              (mlogFile, bss) <-
+                case outputType of
+                  OTConsole _ -> pure (Nothing, [])
+                  OTLogFile logFile h ->
+                    if keepOutputOpen == KeepOpen
+                    then
+                      pure (Nothing, []) -- expected failure build continues further
+                    else do
+                      liftIO $ hClose h
+                      fmap (Just logFile,) $ withSourceFile (toFilePath logFile) $
+                        \src ->
+                             runConduit
+                           $ src
+                          .| CT.decodeUtf8Lenient
+                          .| mungeBuildOutput
+                               stripTHLoading makeAbsolute pkgDir compilerVer
+                          .| CL.consume
+              prettyThrowM $ CabalExitedUnsuccessfully
+                (eceExitCode ece) pkgId exeName fullArgs mlogFile bss
+           where
+            runAndOutput :: ActualCompiler -> RIO env ()
+            runAndOutput compilerVer = withWorkingDir (toFilePath pkgDir) $
+              withProcessContext menv $ case outputType of
+                OTLogFile _ h -> do
+                  let prefixWithTimestamps =
+                        if config.prefixTimestamps
+                          then PrefixWithTimestamps
+                          else WithoutTimestamps
+                  void $ sinkProcessStderrStdout (toFilePath exeName) fullArgs
+                    (sinkWithTimestamps prefixWithTimestamps h)
+                    (sinkWithTimestamps prefixWithTimestamps h)
+                OTConsole mprefix ->
+                  let prefix = fromMaybe mempty mprefix
+                  in  void $ sinkProcessStderrStdout
+                        (toFilePath exeName)
+                        fullArgs
+                        (outputSink KeepTHLoading LevelWarn compilerVer prefix)
+                        (outputSink stripTHLoading LevelInfo compilerVer prefix)
+            outputSink ::
+                 HasCallStack
+              => ExcludeTHLoading
+              -> LogLevel
+              -> ActualCompiler
+              -> Utf8Builder
+              -> ConduitM S.ByteString Void (RIO env) ()
+            outputSink excludeTH level compilerVer prefix =
+              CT.decodeUtf8Lenient
+              .| mungeBuildOutput excludeTH makeAbsolute pkgDir compilerVer
+              .| CL.mapM_ (logGeneric "" level . (prefix <>) . display)
+            -- If users want control, we should add a config option for this
+            makeAbsolute :: ConvertPathsToAbsolute
+            makeAbsolute = case stripTHLoading of
+              ExcludeTHLoading -> ConvertPathsToAbsolute
+              KeepTHLoading    -> KeepPathsAsIs
+
+      exeName <- case esetupexehs of
+        Left setupExe -> pure setupExe
+        Right setuphs -> do
+          distDir <- distDirFromDir pkgDir
+          let setupDir = distDir </> relDirSetup
+              outputFile = setupDir </> relFileSetupLower
+          customBuilt <- liftIO $ readIORef ee.customBuilt
+          if Set.member package.name customBuilt
+            then pure outputFile
+            else do
+              ensureDir setupDir
+              compilerPath <- view $ compilerPathsL . to (.compiler)
+              packageArgs <- getPackageArgs setupDir
+              runExe compilerPath $
+                [ "--make"
+                , "-odir", toFilePathNoTrailingSep setupDir
+                , "-hidir", toFilePathNoTrailingSep setupDir
+                , "-i", "-i."
+                ] ++ packageArgs ++
+                [ toFilePath setuphs
+                , toFilePath ee.setupShimHs
+                , "-main-is"
+                , "StackSetupShim.mainOverride"
+                , "-o", toFilePath outputFile
+                , "-threaded"
+                ] ++
+
+                -- Apply GHC options
+                -- https://github.com/commercialhaskell/stack/issues/4526
+                map
+                  T.unpack
+                  ( Map.findWithDefault
+                      []
+                      AGOEverything
+                      config.ghcOptionsByCat
+                  ++ case config.applyGhcOptions of
+                       AGOEverything -> ee.buildOptsCLI.ghcOptions
+                       AGOTargets -> []
+                       AGOLocals -> []
+                  )
+
+              liftIO $ atomicModifyIORef' ee.customBuilt $
+                \oldCustomBuilt ->
+                  (Set.insert package.name oldCustomBuilt, ())
+              pure outputFile
+      let cabalVerboseArg =
+            let CabalVerbosity cv = ee.buildOpts.cabalVerbose
+            in  "--verbose=" <> showForCabal cv
+      runExe exeName $ cabalVerboseArg:setupArgs
+
+-- | Strip Template Haskell "Loading package" lines and making paths absolute.
+mungeBuildOutput ::
+     forall m. (MonadIO m, MonadUnliftIO m)
+  => ExcludeTHLoading       -- ^ exclude TH loading?
+  -> ConvertPathsToAbsolute -- ^ convert paths to absolute?
+  -> Path Abs Dir           -- ^ package's root directory
+  -> ActualCompiler         -- ^ compiler we're building with
+  -> ConduitM Text Text m ()
+mungeBuildOutput excludeTHLoading makeAbsolute pkgDir compilerVer = void $
+  CT.lines
+  .| CL.map stripCR
+  .| CL.filter (not . isTHLoading)
+  .| filterLinkerWarnings
+  .| toAbsolute
+ where
+  -- | Is this line a Template Haskell "Loading package" line
+  -- ByteString
+  isTHLoading :: Text -> Bool
+  isTHLoading = case excludeTHLoading of
+    KeepTHLoading    -> const False
+    ExcludeTHLoading -> \bs ->
+      "Loading package " `T.isPrefixOf` bs &&
+      ("done." `T.isSuffixOf` bs || "done.\r" `T.isSuffixOf` bs)
+
+  filterLinkerWarnings :: ConduitM Text Text m ()
+  filterLinkerWarnings
+    -- Check for ghc 7.8 since it's the only one prone to producing
+    -- linker warnings on Windows x64
+    | getGhcVersion compilerVer >= mkVersion [7, 8] = doNothing
+    | otherwise = CL.filter (not . isLinkerWarning)
+
+  isLinkerWarning :: Text -> Bool
+  isLinkerWarning str =
+       (  "ghc.exe: warning:" `T.isPrefixOf` str
+       || "ghc.EXE: warning:" `T.isPrefixOf` str
+       )
+    && "is linked instead of __imp_" `T.isInfixOf` str
+
+  -- | Convert GHC error lines with file paths to have absolute file paths
+  toAbsolute :: ConduitM Text Text m ()
+  toAbsolute = case makeAbsolute of
+    KeepPathsAsIs          -> doNothing
+    ConvertPathsToAbsolute -> CL.mapM toAbsolutePath
+
+  toAbsolutePath :: Text -> m Text
+  toAbsolutePath bs = do
+    let (x, y) = T.break (== ':') bs
+    mabs <-
+      if isValidSuffix y
+        then
+          fmap (fmap ((T.takeWhile isSpace x <>) . T.pack . toFilePath)) $
+            forgivingResolveFile pkgDir (T.unpack $ T.dropWhile isSpace x) `catch`
+              \(_ :: PathException) -> pure Nothing
+        else pure Nothing
+    case mabs of
+      Nothing -> pure bs
+      Just fp -> pure $ fp `T.append` y
+
+  doNothing :: ConduitM Text Text m ()
+  doNothing = awaitForever yield
+
+  -- | Match the error location format at the end of lines
+  isValidSuffix = isRight . parseOnly lineCol
+  lineCol = char ':'
+    >> choice
+         [ num >> char ':' >> num >> optional (char '-' >> num) >> pure ()
+         , char '(' >> num >> char ',' >> num >> P.string ")-(" >> num >>
+           char ',' >> num >> char ')' >> pure ()
+         ]
+    >> char ':'
+    >> pure ()
+   where
+    num = some digit
+
+-- | Whether to prefix log lines with timestamps.
+data PrefixWithTimestamps
+  = PrefixWithTimestamps
+  | WithoutTimestamps
+
+-- | Write stream of lines to handle, but adding timestamps.
+sinkWithTimestamps ::
+     MonadIO m
+  => PrefixWithTimestamps
+  -> Handle
+  -> ConduitT ByteString Void m ()
+sinkWithTimestamps prefixWithTimestamps h =
+  case prefixWithTimestamps of
+    PrefixWithTimestamps ->
+      CB.lines .| CL.mapM addTimestamp .| CL.map (<> "\n") .| sinkHandle h
+    WithoutTimestamps -> sinkHandle h
+ where
+  addTimestamp theLine = do
+    now <- liftIO getZonedTime
+    pure (formatZonedTimeForLog now <> " " <> theLine)
+
+-- | Format a time in ISO8601 format. We choose ZonedTime over UTCTime
+-- because a user expects to see logs in their local time, and would
+-- be confused to see UTC time. Stack's debug logs also use the local
+-- time zone.
+formatZonedTimeForLog :: ZonedTime -> ByteString
+formatZonedTimeForLog =
+  S8.pack . formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S%6Q"
+
+-- | Find the Setup.hs or Setup.lhs in the given directory. If none exists,
+-- throw an exception.
+getSetupHs :: Path Abs Dir -- ^ project directory
+           -> IO (Path Abs File)
+getSetupHs dir = do
+  exists1 <- doesFileExist fp1
+  if exists1
+    then pure fp1
+    else do
+      exists2 <- doesFileExist fp2
+      if exists2
+        then pure fp2
+        else throwM $ NoSetupHsFound dir
+ where
+  fp1 = dir </> relFileSetupHs
+  fp2 = dir </> relFileSetupLhs
+ src/Stack/Build/ExecutePackage.hs view
@@ -0,0 +1,1428 @@+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE TypeFamilies          #-}
+
+-- | Perform a build
+module Stack.Build.ExecutePackage
+  ( singleBuild
+  , singleTest
+  , singleBench
+  ) where
+
+import           Control.Concurrent.Execute
+                   ( ActionContext (..), ActionId (..) )
+import           Control.Monad.Extra ( whenJust )
+import qualified Data.ByteString as S
+import qualified Data.ByteString.Char8 as S8
+import qualified Data.ByteString.Lazy as BL
+import           Conduit ( runConduitRes )
+import qualified Data.Conduit.Filesystem as CF
+import qualified Data.Conduit.List as CL
+import           Data.Conduit.Process.Typed ( createSource )
+import qualified Data.Conduit.Text as CT
+import qualified Data.List as L
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Text as T
+import qualified Distribution.PackageDescription as C
+import           Distribution.System ( OS (..), Platform (..) )
+import qualified Distribution.Text as C
+import           Distribution.Types.MungedPackageName
+                   ( encodeCompatPackageName )
+import           Distribution.Types.UnqualComponentName
+                   ( mkUnqualComponentName )
+import           Distribution.Version ( mkVersion )
+import           Path
+                   ( (</>), addExtension, filename, isProperPrefixOf, parent
+                   , parseRelDir, parseRelFile, stripProperPrefix
+                   )
+import           Path.Extra ( toFilePathNoTrailingSep )
+import           Path.IO
+                   ( copyFile, doesFileExist, ensureDir, ignoringAbsence
+                   , removeDirRecur, removeFile
+                   )
+import           RIO.NonEmpty ( nonEmpty )
+import           RIO.Process
+                   ( byteStringInput, findExecutable, getStderr, getStdout
+                   , inherit, modifyEnvVars, proc, setStderr, setStdin
+                   , setStdout, showProcessArgDebug, useHandleOpen, waitExitCode
+                   , withProcessWait, withWorkingDir, HasProcessContext
+                   )
+import           Stack.Build.Cache
+                   ( TestStatus (..), deleteCaches, getTestStatus
+                   , markExeInstalled, markExeNotInstalled, readPrecompiledCache
+                   , setTestStatus, tryGetCabalMod, tryGetConfigCache
+                   , tryGetPackageProjectRoot, tryGetSetupConfigMod
+                   , writeBuildCache, writeCabalMod, writeConfigCache
+                   , writeFlagCache, writePrecompiledCache
+                   , writePackageProjectRoot, writeSetupConfigMod
+                   )
+import           Stack.Build.ExecuteEnv
+                   ( ExcludeTHLoading (..), ExecutableBuildStatus (..)
+                   , ExecuteEnv (..), KeepOutputOpen (..), OutputType (..)
+                   , withSingleContext
+                   )
+import           Stack.Build.Source ( addUnlistedToBuildCache )
+import           Stack.Config.ConfigureScript ( ensureConfigureScript )
+import           Stack.Constants
+                   ( bindirSuffix, compilerOptionsCabalFlag, relDirBuild
+                   , testGhcEnvRelFile
+                   )
+import           Stack.Constants.Config
+                   ( distDirFromDir, distRelativeDir, hpcDirFromDir
+                   , hpcRelativeDir, setupConfigFromDir
+                   )
+import           Stack.Coverage ( generateHpcReport, updateTixFile )
+import           Stack.GhcPkg ( ghcPkg, unregisterGhcPkgIds )
+import           Stack.Package
+                   ( buildLogPath, buildableExes, buildableSubLibs
+                   , hasBuildableMainLibrary, mainLibraryHasExposedModules
+                   )
+import           Stack.PackageDump ( conduitDumpPackage, ghcPkgDescribe )
+import           Stack.Prelude
+import           Stack.Types.Build
+                   ( ConfigCache (..), PrecompiledCache (..), Task (..)
+                   , TaskConfigOpts (..), TaskType (..), taskAnyMissing
+                   , taskIsTarget, taskLocation, taskProvides
+                   , taskTargetIsMutable, taskTypePackageIdentifier
+                   )
+import qualified Stack.Types.Build as ConfigCache ( ConfigCache (..) )
+import           Stack.Types.Build.Exception
+                   ( BuildException (..), BuildPrettyException (..) )
+import           Stack.Types.BuildConfig
+                   ( BuildConfig (..), HasBuildConfig (..), projectRootL )
+import           Stack.Types.BuildOpts
+                   ( BenchmarkOpts (..), BuildOpts (..), HaddockOpts (..)
+                   , TestOpts (..)
+                   )
+import           Stack.Types.BuildOptsCLI ( BuildOptsCLI (..) )
+import           Stack.Types.CompCollection
+                   ( collectionKeyValueList, collectionLookup
+                   , foldComponentToAnotherCollection, getBuildableListText
+                   )
+import           Stack.Types.Compiler
+                   ( ActualCompiler (..), WhichCompiler (..), getGhcVersion
+                   , whichCompilerL
+                   )
+import           Stack.Types.CompilerPaths
+                   ( CompilerPaths (..), GhcPkgExe (..), HasCompiler (..)
+                   , cpWhich, getGhcPkgExe
+                   )
+import qualified Stack.Types.Component as Component
+import           Stack.Types.Config ( Config (..), HasConfig (..) )
+import           Stack.Types.ConfigureOpts
+                   ( BaseConfigOpts (..), ConfigureOpts (..) )
+import           Stack.Types.Curator ( Curator (..) )
+import           Stack.Types.DumpPackage ( DumpPackage (..) )
+import           Stack.Types.EnvConfig
+                   ( EnvConfig (..), HasEnvConfig (..), actualCompilerVersionL
+                   , appropriateGhcColorFlag
+                   )
+import           Stack.Types.EnvSettings ( EnvSettings (..) )
+import           Stack.Types.GhcPkgId ( GhcPkgId, unGhcPkgId )
+import           Stack.Types.GlobalOpts ( GlobalOpts (..) )
+import           Stack.Types.Installed
+                   ( InstallLocation (..), Installed (..), InstalledMap
+                   , InstalledLibraryInfo (..)
+                   )
+import           Stack.Types.IsMutable ( IsMutable (..) )
+import           Stack.Types.NamedComponent
+                   ( NamedComponent, exeComponents, isCBench, isCTest
+                   , renderComponent
+                   )
+import           Stack.Types.Package
+                   ( LocalPackage (..), Package (..), installedMapGhcPkgId
+                   , runMemoizedWith, simpleInstalledLib
+                   , toCabalMungedPackageName
+                   )
+import           Stack.Types.PackageFile ( PackageWarning (..) )
+import           Stack.Types.Platform ( HasPlatform (..) )
+import           Stack.Types.Runner ( HasRunner, globalOptsL )
+import           System.IO.Error ( isDoesNotExistError )
+import           System.PosixCompat.Files
+                   ( createLink, getFileStatus, modificationTime )
+import           System.Random ( randomIO )
+
+-- | Generate the ConfigCache
+getConfigCache ::
+     HasEnvConfig env
+  => ExecuteEnv
+  -> Task
+  -> InstalledMap
+  -> Bool
+  -> Bool
+  -> RIO env (Map PackageIdentifier GhcPkgId, ConfigCache)
+getConfigCache ee task installedMap enableTest enableBench = do
+  let extra =
+        -- We enable tests if the test suite dependencies are already
+        -- installed, so that we avoid unnecessary recompilation based on
+        -- cabal_macros.h changes when switching between 'stack build' and
+        -- 'stack test'. See:
+        -- https://github.com/commercialhaskell/stack/issues/805
+        case task.taskType of
+          TTLocalMutable _ ->
+            -- FIXME: make this work with exact-configuration.
+            -- Not sure how to plumb the info atm. See
+            -- https://github.com/commercialhaskell/stack/issues/2049
+            [ "--enable-tests" | enableTest] ++
+            [ "--enable-benchmarks" | enableBench]
+          TTRemotePackage{} -> []
+  idMap <- liftIO $ readTVarIO ee.ghcPkgIds
+  let getMissing ident =
+        case Map.lookup ident idMap of
+          Nothing
+              -- Expect to instead find it in installedMap if it's
+              -- an initialBuildSteps target.
+              | ee.buildOptsCLI.initialBuildSteps && taskIsTarget task
+              , Just (_, installed) <- Map.lookup (pkgName ident) installedMap
+                  -> pure $ installedToGhcPkgId ident installed
+          Just installed -> pure $ installedToGhcPkgId ident installed
+          _ -> throwM $ PackageIdMissingBug ident
+      installedToGhcPkgId ident (Library ident' libInfo) =
+        assert (ident == ident') (installedMapGhcPkgId ident libInfo)
+      installedToGhcPkgId _ (Executable _) = mempty
+      TaskConfigOpts missing mkOpts = task.configOpts
+  missingMapList <- traverse getMissing $ toList missing
+  let missing' = Map.unions missingMapList
+      configureOpts' = mkOpts missing'
+      configureOpts = configureOpts'
+        { nonPathRelated = configureOpts'.nonPathRelated ++ map T.unpack extra }
+      deps = Set.fromList $ Map.elems missing' ++ Map.elems task.present
+      components = case task.taskType of
+        TTLocalMutable lp ->
+          Set.map (encodeUtf8 . renderComponent) lp.components
+        TTRemotePackage{} -> Set.empty
+      cache = ConfigCache
+        { configureOpts
+        , deps
+        , components
+        , buildHaddocks = task.buildHaddocks
+        , pkgSrc = task.cachePkgSrc
+        , pathEnvVar = ee.pathEnvVar
+        }
+      allDepsMap = Map.union missing' task.present
+  pure (allDepsMap, cache)
+
+-- | Ensure that the configuration for the package matches what is given
+ensureConfig :: HasEnvConfig env
+             => ConfigCache -- ^ newConfigCache
+             -> Path Abs Dir -- ^ package directory
+             -> BuildOpts
+             -> RIO env () -- ^ announce
+             -> (ExcludeTHLoading -> [String] -> RIO env ()) -- ^ cabal
+             -> Path Abs File -- ^ Cabal file
+             -> Task
+             -> RIO env Bool
+ensureConfig newConfigCache pkgDir buildOpts announce cabal cabalFP task = do
+  newCabalMod <-
+    liftIO $ modificationTime <$> getFileStatus (toFilePath cabalFP)
+  setupConfigfp <- setupConfigFromDir pkgDir
+  let getNewSetupConfigMod =
+        liftIO $ either (const Nothing) (Just . modificationTime) <$>
+        tryJust
+          (guard . isDoesNotExistError)
+          (getFileStatus (toFilePath setupConfigfp))
+  newSetupConfigMod <- getNewSetupConfigMod
+  newProjectRoot <- S8.pack . toFilePath <$> view projectRootL
+  -- See https://github.com/commercialhaskell/stack/issues/3554. This can be
+  -- dropped when Stack drops support for GHC < 8.4.
+  taskAnyMissingHackEnabled <-
+    view $ actualCompilerVersionL . to getGhcVersion . to (< mkVersion [8, 4])
+  needConfig <-
+    if buildOpts.reconfigure
+          -- The reason 'taskAnyMissing' is necessary is a bug in Cabal. See:
+          -- <https://github.com/haskell/cabal/issues/4728#issuecomment-337937673>.
+          -- The problem is that Cabal may end up generating the same package ID
+          -- for a dependency, even if the ABI has changed. As a result, without
+          -- check, Stack would think that a reconfigure is unnecessary, when in
+          -- fact we _do_ need to reconfigure. The details here suck. We really
+          -- need proper hashes for package identifiers.
+       || (taskAnyMissingHackEnabled && taskAnyMissing task)
+      then pure True
+      else do
+        -- We can ignore the components portion of the config
+        -- cache, because it's just used to inform 'construct
+        -- plan that we need to plan to build additional
+        -- components. These components don't affect the actual
+        -- package configuration.
+        let ignoreComponents :: ConfigCache -> ConfigCache
+            ignoreComponents cc = cc { ConfigCache.components = Set.empty }
+        -- Determine the old and new configuration in the local directory, to
+        -- determine if we need to reconfigure.
+        mOldConfigCache <- tryGetConfigCache pkgDir
+
+        mOldCabalMod <- tryGetCabalMod pkgDir
+
+        -- Cabal's setup-config is created per OS/Cabal version, multiple
+        -- projects using the same package could get a conflict because of this
+        mOldSetupConfigMod <- tryGetSetupConfigMod pkgDir
+        mOldProjectRoot <- tryGetPackageProjectRoot pkgDir
+
+        pure $
+                fmap ignoreComponents mOldConfigCache
+             /= Just (ignoreComponents newConfigCache)
+          || mOldCabalMod /= Just newCabalMod
+          || mOldSetupConfigMod /= newSetupConfigMod
+          || mOldProjectRoot /= Just newProjectRoot
+
+  when task.buildTypeConfig $
+    -- When build-type is Configure, we need to have a configure script in the
+    -- local directory. If it doesn't exist, build it with autoreconf -i. See:
+    -- https://github.com/commercialhaskell/stack/issues/3534
+    ensureConfigureScript pkgDir
+
+  when needConfig $ do
+    deleteCaches pkgDir
+    announce
+    cp <- view compilerPathsL
+    let (GhcPkgExe pkgPath) = cp.pkg
+    let programNames =
+          case cpWhich cp of
+            Ghc ->
+              [ ("ghc", toFilePath cp.compiler)
+              , ("ghc-pkg", toFilePath pkgPath)
+              ]
+    exes <- forM programNames $ \(name, file) -> do
+      mpath <- findExecutable file
+      pure $ case mpath of
+          Left _ -> []
+          Right x -> pure $ concat ["--with-", name, "=", x]
+    -- Configure cabal with arguments determined by
+    -- Stack.Types.Build.ureOpts
+    cabal KeepTHLoading $ "configure" : concat
+      [ concat exes
+      , newConfigCache.configureOpts.pathRelated
+      , newConfigCache.configureOpts.nonPathRelated
+      ]
+    -- Only write the cache for local packages.  Remote packages are built in a
+    -- temporary directory so the cache would never be used anyway.
+    case task.taskType of
+      TTLocalMutable{} -> writeConfigCache pkgDir newConfigCache
+      TTRemotePackage{} -> pure ()
+    writeCabalMod pkgDir newCabalMod
+    -- This file gets updated one more time by the configure step, so get the
+    -- most recent value. We could instead change our logic above to check if
+    -- our config mod file is newer than the file above, but this seems
+    -- reasonable too.
+    getNewSetupConfigMod >>= writeSetupConfigMod pkgDir
+    writePackageProjectRoot pkgDir newProjectRoot
+  pure needConfig
+
+-- | Make a padded prefix for log messages
+packageNamePrefix :: ExecuteEnv -> PackageName -> String
+packageNamePrefix ee name' =
+  let name = packageNameString name'
+      paddedName =
+        case ee.largestPackageName of
+          Nothing -> name
+          Just len ->
+            assert (len >= length name) $ take len $ name ++ L.repeat ' '
+  in  paddedName <> "> "
+
+announceTask ::
+     HasLogFunc env
+  => ExecuteEnv
+  -> TaskType
+  -> Utf8Builder
+  -> RIO env ()
+announceTask ee taskType action = logInfo $
+     fromString
+       (packageNamePrefix ee (pkgName (taskTypePackageIdentifier taskType)))
+  <> action
+
+-- Implements running a package's build, used to implement 'ATBuild' and
+-- 'ATBuildFinal' tasks.  In particular this does the following:
+--
+-- * Checks if the package exists in the precompiled cache, and if so,
+--   add it to the database instead of performing the build.
+--
+-- * Runs the configure step if needed ('ensureConfig')
+--
+-- * Runs the build step
+--
+-- * Generates haddocks
+--
+-- * Registers the library and copies the built executables into the
+--   local install directory. Note that this is literally invoking Cabal
+--   with @copy@, and not the copying done by @stack install@ - that is
+--   handled by 'copyExecutables'.
+singleBuild :: forall env. (HasEnvConfig env, HasRunner env)
+            => ActionContext
+            -> ExecuteEnv
+            -> Task
+            -> InstalledMap
+            -> Bool             -- ^ Is this a final build?
+            -> RIO env ()
+singleBuild
+    ac
+    ee
+    task
+    installedMap
+    isFinalBuild
+  = do
+    cabalVersion <- view $ envConfigL . to (.compilerPaths.cabalVersion)
+    -- The old version of Cabal (the library) copy did not allow the components
+    -- to be copied to be specified.
+    let isOldCabalCopy = cabalVersion < mkVersion [2, 0]
+    (allDepsMap, cache) <-
+      getConfigCache ee task installedMap enableTests enableBenchmarks
+    let bcoSnapInstallRoot = ee.baseConfigOpts.snapInstallRoot
+    mprecompiled <- getPrecompiled cache task.taskType bcoSnapInstallRoot
+    minstalled <-
+      case mprecompiled of
+        Just precompiled -> copyPreCompiled ee task pkgId precompiled
+        Nothing -> do
+          curator <- view $ buildConfigL . to (.curator)
+          realConfigAndBuild isOldCabalCopy cache curator allDepsMap
+    case minstalled of
+      Nothing -> pure ()
+      Just installed -> do
+        writeFlagCache installed cache
+        liftIO $ atomically $
+          modifyTVar ee.ghcPkgIds $ Map.insert pkgId installed
+ where
+  pkgId = taskProvides task
+  PackageIdentifier pname _ = pkgId
+  doHaddock curator package =
+       task.buildHaddocks
+    && not isFinalBuild
+       -- Works around haddock failing on bytestring-builder since it has no
+       -- modules when bytestring is new enough.
+    && mainLibraryHasExposedModules package
+       -- Special help for the curator tool to avoid haddocks that are known
+       -- to fail
+    && maybe True (Set.notMember pname . (.skipHaddock)) curator
+
+  buildingFinals = isFinalBuild || task.allInOne
+  enableTests = buildingFinals && any isCTest (taskComponents task)
+  enableBenchmarks = buildingFinals && any isCBench (taskComponents task)
+
+  annSuffix isOldCabalCopy executableBuildStatuses =
+    if result == "" then "" else " (" <> result <> ")"
+   where
+    result = T.intercalate " + " $ concat
+      [ ["lib" | task.allInOne && hasLib]
+      , ["sub-lib" | task.allInOne && hasSubLib]
+      , ["exe" | task.allInOne && hasExe]
+      , ["test" | enableTests]
+      , ["bench" | enableBenchmarks]
+      ]
+    (hasLib, hasSubLib, hasExe) = case task.taskType of
+      TTLocalMutable lp ->
+        let package = lp.package
+            hasLibrary = hasBuildableMainLibrary package
+            hasSubLibraries = not $ null package.subLibraries
+            hasExecutables = not . Set.null $
+              exesToBuild isOldCabalCopy executableBuildStatuses lp
+        in  (hasLibrary, hasSubLibraries, hasExecutables)
+      -- This isn't true, but we don't want to have this info for upstream deps.
+      _ -> (False, False, False)
+
+  realConfigAndBuild isOldCabalCopy cache mcurator allDepsMap =
+    withSingleContext ac ee task.taskType allDepsMap Nothing $
+      \package cabalFP pkgDir cabal0 announce _outputType -> do
+        let cabal = cabal0 CloseOnException
+        executableBuildStatuses <- getExecutableBuildStatuses package pkgDir
+        when (  not (cabalIsSatisfied isOldCabalCopy executableBuildStatuses)
+             && taskIsTarget task
+             ) $
+          prettyInfoL
+            [ flow "Building all executables for"
+            , style Current (fromPackageName package.name)
+            , flow "once. After a successful build of all of them, only \
+                   \specified executables will be rebuilt."
+            ]
+        _neededConfig <-
+          ensureConfig
+            cache
+            pkgDir
+            ee.buildOpts
+            ( announce
+                (  "configure"
+                <> display (annSuffix isOldCabalCopy executableBuildStatuses)
+                )
+            )
+            cabal
+            cabalFP
+            task
+        let installedMapHasThisPkg :: Bool
+            installedMapHasThisPkg =
+              case Map.lookup package.name installedMap of
+                Just (_, Library ident _) -> ident == pkgId
+                Just (_, Executable _) -> True
+                _ -> False
+
+        case ( ee.buildOptsCLI.onlyConfigure
+             , ee.buildOptsCLI.initialBuildSteps && taskIsTarget task
+             ) of
+          -- A full build is done if there are downstream actions,
+          -- because their configure step will require that this
+          -- package is built. See
+          -- https://github.com/commercialhaskell/stack/issues/2787
+          (True, _) | null ac.downstream -> pure Nothing
+          (_, True) | null ac.downstream || installedMapHasThisPkg -> do
+            initialBuildSteps isOldCabalCopy executableBuildStatuses cabal announce
+            pure Nothing
+          _ -> fulfillCuratorBuildExpectations
+                 pname
+                 mcurator
+                 enableTests
+                 enableBenchmarks
+                 Nothing
+                 (Just <$>
+                    realBuild isOldCabalCopy cache package pkgDir cabal0 announce executableBuildStatuses)
+
+  initialBuildSteps isOldCabalCopy executableBuildStatuses cabal announce = do
+    announce
+      (  "initial-build-steps"
+      <> display (annSuffix isOldCabalCopy executableBuildStatuses)
+      )
+    cabal KeepTHLoading ["repl", "stack-initial-build-steps"]
+
+  realBuild ::
+       Bool
+       -- ^ Is Cabal copy limited to all libraries and executables?
+    -> ConfigCache
+    -> Package
+    -> Path Abs Dir
+    -> (KeepOutputOpen -> ExcludeTHLoading -> [String] -> RIO env ())
+    -> (Utf8Builder -> RIO env ())
+       -- ^ A plain 'announce' function
+    -> Map Text ExecutableBuildStatus
+    -> RIO env Installed
+  realBuild isOldCabalCopy cache package pkgDir cabal0 announce executableBuildStatuses = do
+    let cabal = cabal0 CloseOnException
+    wc <- view $ actualCompilerVersionL . whichCompilerL
+
+    markExeNotInstalled (taskLocation task) pkgId
+    case task.taskType of
+      TTLocalMutable lp -> do
+        when enableTests $ setTestStatus pkgDir TSUnknown
+        caches <- runMemoizedWith lp.newBuildCaches
+        mapM_
+          (uncurry (writeBuildCache pkgDir))
+          (Map.toList caches)
+      TTRemotePackage{} -> pure ()
+
+    -- FIXME: only output these if they're in the build plan.
+
+    let postBuildCheck _succeeded = do
+          mlocalWarnings <- case task.taskType of
+            TTLocalMutable lp -> do
+                warnings <- checkForUnlistedFiles task.taskType pkgDir
+                -- TODO: Perhaps only emit these warnings for non extra-dep?
+                pure (Just (lp.cabalFP, warnings))
+            _ -> pure Nothing
+          -- NOTE: once
+          -- https://github.com/commercialhaskell/stack/issues/2649
+          -- is resolved, we will want to partition the warnings
+          -- based on variety, and output in different lists.
+          let showModuleWarning (UnlistedModulesWarning comp modules) =
+                "- In" <+>
+                fromString (T.unpack (renderComponent comp)) <>
+                ":" <> line <>
+                indent 4 ( mconcat
+                         $ L.intersperse line
+                         $ map
+                             (style Good . fromString . C.display)
+                             modules
+                         )
+          forM_ mlocalWarnings $ \(cabalFP, warnings) ->
+            unless (null warnings) $ prettyWarn $
+                 flow "The following modules should be added to \
+                      \exposed-modules or other-modules in" <+>
+                      pretty cabalFP
+              <> ":"
+              <> line
+              <> indent 4 ( mconcat
+                          $ L.intersperse line
+                          $ map showModuleWarning warnings
+                          )
+              <> blankLine
+              <> flow "Missing modules in the Cabal file are likely to cause \
+                      \undefined reference errors from the linker, along with \
+                      \other problems."
+
+    actualCompiler <- view actualCompilerVersionL
+    () <- announce
+      (  "build"
+      <> display (annSuffix isOldCabalCopy executableBuildStatuses)
+      <> " with "
+      <> display actualCompiler
+      )
+    config <- view configL
+    extraOpts <- extraBuildOptions wc ee.buildOpts
+    let stripTHLoading
+          | config.hideTHLoading = ExcludeTHLoading
+          | otherwise                  = KeepTHLoading
+    (buildOpts, copyOpts) <-
+      case (task.taskType, task.allInOne, isFinalBuild) of
+        (_, True, True) -> throwM AllInOneBuildBug
+        (TTLocalMutable lp, False, False) ->
+          let componentOpts =
+                primaryComponentOptions isOldCabalCopy executableBuildStatuses lp
+          in  pure (componentOpts, componentOpts)
+        (TTLocalMutable lp, False, True) -> pure (finalComponentOptions lp, [])
+        (TTLocalMutable lp, True, False) ->
+          let componentOpts =
+                primaryComponentOptions isOldCabalCopy executableBuildStatuses lp
+          in pure (componentOpts <> finalComponentOptions lp, componentOpts)
+        (TTRemotePackage{}, _, _) -> pure ([], [])
+    cabal stripTHLoading ("build" : buildOpts <> extraOpts)
+      `catch` \ex -> case ex of
+        CabalExitedUnsuccessfully{} ->
+          postBuildCheck False >> prettyThrowM ex
+        _ -> throwM ex
+    postBuildCheck True
+
+    mcurator <- view $ buildConfigL . to (.curator)
+    when (doHaddock mcurator package) $ do
+      let isTaskTargetMutable = taskTargetIsMutable task == Mutable
+          isHaddockForHackage =
+            ee.buildOpts.haddockForHackage && isTaskTargetMutable
+      announce $ if isHaddockForHackage
+        then "haddock for Hackage"
+        else "haddock"
+
+      -- For GHC 8.4 and later, provide the --quickjump option.
+      let quickjump =
+            case actualCompiler of
+              ACGhc ghcVer
+                | ghcVer >= mkVersion [8, 4] -> ["--haddock-option=--quickjump"]
+              _ -> []
+
+      fulfillHaddockExpectations pname mcurator $ \keep -> do
+        let args = concat
+              (  if isHaddockForHackage
+                   then
+                     [ [ "--for-hackage" ] ]
+                   else
+                     [ [ "--html"
+                       , "--hoogle"
+                       , "--html-location=../$pkg-$version/"
+                       ]
+                     , [ "--haddock-option=--hyperlinked-source"
+                       | ee.buildOpts.haddockHyperlinkSource
+                       ]
+                     , [ "--internal" | ee.buildOpts.haddockInternal  ]
+                     , quickjump
+                     ]
+              <> [ [ "--haddock-option=" <> opt
+                   | opt <- ee.buildOpts.haddockOpts.additionalArgs
+                   ]
+                 ]
+              )
+
+        cabal0 keep KeepTHLoading $ "haddock" : args
+
+    let hasLibrary = hasBuildableMainLibrary package
+        hasSubLibraries = not $ null package.subLibraries
+        hasExecutables = not $ null package.executables
+        shouldCopy =
+             not isFinalBuild
+          && (hasLibrary || hasSubLibraries || hasExecutables)
+    when shouldCopy $ withMVar ee.installLock $ \() -> do
+      announce "copy/register"
+      let copyArgs = "copy" : if isOldCabalCopy then [] else copyOpts
+      eres <- try $ cabal KeepTHLoading copyArgs
+      case eres of
+        Left err@CabalExitedUnsuccessfully{} ->
+          throwM $ CabalCopyFailed
+                     (package.buildType == C.Simple)
+                     (displayException err)
+        _ -> pure ()
+      when (hasLibrary || hasSubLibraries) $ cabal KeepTHLoading ["register"]
+
+    copyDdumpFilesIfNeeded buildingFinals ee.buildOpts.ddumpDir
+    installedPkg <-
+      fetchAndMarkInstalledPackage ee (taskLocation task) package pkgId
+    postProcessRemotePackage
+      task.taskType
+      ac
+      cache
+      ee
+      installedPkg
+      package
+      pkgId
+      pkgDir
+    pure installedPkg
+
+-- | Action in the case that the task relates to a remote package.
+postProcessRemotePackage ::
+     (HasEnvConfig env)
+  => TaskType
+  -> ActionContext
+  -> ConfigCache
+  -> ExecuteEnv
+  -> Installed
+  -> Package
+  -> PackageIdentifier
+  -> Path b Dir
+  -> RIO env ()
+postProcessRemotePackage
+    taskType
+    ac
+    cache
+    ee
+    installedPackage
+    package
+    pkgId
+    pkgDir
+  = case taskType of
+      TTRemotePackage isMutable _ loc -> do
+        when (isMutable == Immutable) $ writePrecompiledCache
+          ee.baseConfigOpts
+          loc
+          cache.configureOpts
+          cache.buildHaddocks
+          installedPackage
+          (buildableExes package)
+        -- For packages from a package index, pkgDir is in the tmp directory. We
+        -- eagerly delete it if no other tasks require it, to reduce space usage
+        -- in tmp (#3018).
+        let remaining =
+              Set.filter
+                (\(ActionId x _) -> x == pkgId)
+                ac.remaining
+        when (null remaining) $ removeDirRecur pkgDir
+      _ -> pure ()
+
+-- | Once all the Cabal-related tasks have run for a package, we should be able
+-- to gather the information needed to create an 'Installed' package value. For
+-- now, either there's a main library (in which case we consider the 'GhcPkgId'
+-- values of the package's libraries) or we just consider it's an executable
+-- (and mark all the executables as installed, if any).
+--
+-- Note that this also modifies the installedDumpPkgsTVar which is used for
+-- generating Haddocks.
+--
+fetchAndMarkInstalledPackage ::
+     (HasTerm env, HasEnvConfig env)
+  => ExecuteEnv
+  -> InstallLocation
+  -> Package
+  -> PackageIdentifier
+  -> RIO env Installed
+fetchAndMarkInstalledPackage ee taskInstallLocation package pkgId = do
+  let baseConfigOpts = ee.baseConfigOpts
+      (installedPkgDb, installedDumpPkgsTVar) =
+        case taskInstallLocation of
+          Snap ->
+            ( baseConfigOpts.snapDB
+            , ee.snapshotDumpPkgs )
+          Local ->
+            ( baseConfigOpts.localDB
+            , ee.localDumpPkgs )
+  -- Only pure the sub-libraries to cache them if we also cache the main
+  -- library (that is, if it exists)
+  if hasBuildableMainLibrary package
+    then do
+      let getAndStoreGhcPkgId =
+            loadInstalledPkg [installedPkgDb] installedDumpPkgsTVar
+          foldSubLibToMap subLib mapInMonad = do
+            let mungedName = toCabalMungedPackageName package.name subLib.name
+            maybeGhcpkgId <-
+              getAndStoreGhcPkgId (encodeCompatPackageName mungedName)
+            mapInMonad <&> case maybeGhcpkgId of
+              Just v -> Map.insert subLib.name v
+              _ -> id
+      subLibsPkgIds <- foldComponentToAnotherCollection
+        package.subLibraries
+        foldSubLibToMap
+        mempty
+      mGhcPkgId <- getAndStoreGhcPkgId package.name
+      case mGhcPkgId of
+        Nothing -> throwM $ Couldn'tFindPkgId package.name
+        Just ghcPkgId -> pure $ simpleInstalledLib pkgId ghcPkgId subLibsPkgIds
+    else do
+      markExeInstalled taskInstallLocation pkgId -- TODO unify somehow
+                                                  -- with writeFlagCache?
+      pure $ Executable pkgId
+
+-- | Copy ddump-* files, if we are building finals and a non-empty ddump-dir
+-- has been specified.
+copyDdumpFilesIfNeeded :: HasEnvConfig env => Bool -> Maybe Text -> RIO env ()
+copyDdumpFilesIfNeeded buildingFinals mDdumpPath = when buildingFinals $
+  whenJust mDdumpPath $ \ddumpPath -> unless (T.null ddumpPath) $ do
+    distDir <- distRelativeDir
+    ddumpRelDir <- parseRelDir $ T.unpack ddumpPath
+    prettyDebugL
+      [ "ddump-dir:"
+      , pretty ddumpRelDir
+      ]
+    prettyDebugL
+      [ "dist-dir:"
+      , pretty distDir
+      ]
+    runConduitRes
+      $ CF.sourceDirectoryDeep False (toFilePath distDir)
+      .| CL.filter (L.isInfixOf ".dump-")
+      .| CL.mapM_ (\src -> liftIO $ do
+          parentDir <- parent <$> parseRelDir src
+          destBaseDir <-
+            (ddumpRelDir </>) <$> stripProperPrefix distDir parentDir
+          -- exclude .stack-work dir
+          unless (".stack-work" `L.isInfixOf` toFilePath destBaseDir) $ do
+            ensureDir destBaseDir
+            src' <- parseRelFile src
+            copyFile src' (destBaseDir </> filename src'))
+
+-- | Get the build status of all the package executables. Do so by
+-- testing whether their expected output file exists, e.g.
+--
+-- .stack-work/dist/x86_64-osx/Cabal-1.22.4.0/build/alpha/alpha
+-- .stack-work/dist/x86_64-osx/Cabal-1.22.4.0/build/alpha/alpha.exe
+-- .stack-work/dist/x86_64-osx/Cabal-1.22.4.0/build/alpha/alpha.jsexe/ (NOTE: a dir)
+getExecutableBuildStatuses ::
+     HasEnvConfig env
+  => Package
+  -> Path Abs Dir
+  -> RIO env (Map Text ExecutableBuildStatus)
+getExecutableBuildStatuses package pkgDir = do
+  distDir <- distDirFromDir pkgDir
+  platform <- view platformL
+  fmap
+    Map.fromList
+    (mapM (checkExeStatus platform distDir) (Set.toList (buildableExes package)))
+
+-- | Check whether the given executable is defined in the given dist directory.
+checkExeStatus ::
+     HasLogFunc env
+  => Platform
+  -> Path b Dir
+  -> Text
+  -> RIO env (Text, ExecutableBuildStatus)
+checkExeStatus platform distDir name = do
+  exename <- parseRelDir (T.unpack name)
+  exists <- checkPath (distDir </> relDirBuild </> exename)
+  pure
+    ( name
+    , if exists
+        then ExecutableBuilt
+        else ExecutableNotBuilt)
+ where
+  checkPath base =
+    case platform of
+      Platform _ Windows -> do
+        fileandext <- parseRelFile (file ++ ".exe")
+        doesFileExist (base </> fileandext)
+      _ -> do
+        fileandext <- parseRelFile file
+        doesFileExist (base </> fileandext)
+   where
+    file = T.unpack name
+
+getPrecompiled ::
+  (HasEnvConfig env)
+  => ConfigCache
+  -> TaskType
+  -> Path Abs Dir
+  -> RIO env (Maybe (PrecompiledCache Abs))
+getPrecompiled cache taskType bcoSnapInstallRoot =
+  case taskType of
+    TTRemotePackage Immutable _ loc -> do
+      mpc <- readPrecompiledCache
+                loc
+                cache.configureOpts
+                cache.buildHaddocks
+      case mpc of
+        Nothing -> pure Nothing
+        -- Only pay attention to precompiled caches that refer to packages
+        -- within the snapshot.
+        Just pc
+          | maybe False
+              (bcoSnapInstallRoot `isProperPrefixOf`)
+              pc.library -> pure Nothing
+        -- If old precompiled cache files are left around but snapshots are
+        -- deleted, it is possible for the precompiled file to refer to the
+        -- very library we're building, and if flags are changed it may try to
+        -- copy the library to itself. This check prevents that from
+        -- happening.
+        Just pc -> do
+          let allM _ [] = pure True
+              allM f (x:xs) = do
+                b <- f x
+                if b then allM f xs else pure False
+          b <- liftIO $
+                  allM doesFileExist $ maybe id (:) pc.library pc.exes
+          pure $ if b then Just pc else Nothing
+    _ -> pure Nothing
+
+copyPreCompiled ::
+  (HasLogFunc env, HasCompiler env, HasTerm env, HasProcessContext env, HasEnvConfig env) =>
+  ExecuteEnv
+  -> Task
+  -> PackageIdentifier
+  -> PrecompiledCache b0
+  -> RIO env (Maybe Installed)
+copyPreCompiled ee task pkgId (PrecompiledCache mlib subLibs exes) = do
+  let PackageIdentifier pname pversion = pkgId
+  announceTask ee task.taskType "using precompiled package"
+
+  -- We need to copy .conf files for the main library and all sub-libraries
+  -- which exist in the cache, from their old snapshot to the new one.
+  -- However, we must unregister any such library in the new snapshot, in case
+  -- it was built with different flags.
+  let
+    subLibNames = Set.toList $ buildableSubLibs $ case task.taskType of
+      TTLocalMutable lp -> lp.package
+      TTRemotePackage _ p _ -> p
+    toMungedPackageId :: Text -> MungedPackageId
+    toMungedPackageId subLib =
+      let subLibName = LSubLibName $ mkUnqualComponentName $ T.unpack subLib
+      in  MungedPackageId (MungedPackageName pname subLibName) pversion
+    toPackageId :: MungedPackageId -> PackageIdentifier
+    toPackageId (MungedPackageId n v) =
+      PackageIdentifier (encodeCompatPackageName n) v
+    allToUnregister :: [Either PackageIdentifier GhcPkgId]
+    allToUnregister = mcons
+      (Left pkgId <$ mlib)
+      (map (Left . toPackageId . toMungedPackageId) subLibNames)
+    allToRegister = mcons mlib subLibs
+
+  unless (null allToRegister) $
+    withMVar ee.installLock $ \() -> do
+      -- We want to ignore the global and user package databases. ghc-pkg
+      -- allows us to specify --no-user-package-db and --package-db=<db> on
+      -- the command line.
+      let pkgDb = ee.baseConfigOpts.snapDB
+      ghcPkgExe <- getGhcPkgExe
+      -- First unregister, silently, everything that needs to be unregistered.
+      case nonEmpty allToUnregister of
+        Nothing -> pure ()
+        Just allToUnregister' -> catchAny
+          (unregisterGhcPkgIds False ghcPkgExe pkgDb allToUnregister')
+          (const (pure ()))
+      -- Now, register the cached conf files.
+      forM_ allToRegister $ \libpath ->
+        ghcPkg ghcPkgExe [pkgDb] ["register", "--force", toFilePath libpath]
+
+  liftIO $ forM_ exes $ \exe -> do
+    ensureDir bindir
+    let dst = bindir </> filename exe
+    createLink (toFilePath exe) (toFilePath dst) `catchIO` \_ -> copyFile exe dst
+  case (mlib, exes) of
+    (Nothing, _:_) -> markExeInstalled (taskLocation task) pkgId
+    _ -> pure ()
+
+  -- Find the package in the database
+  let pkgDbs = [ee.baseConfigOpts.snapDB]
+
+  case mlib of
+    Nothing -> pure $ Just $ Executable pkgId
+    Just _ -> do
+      mpkgid <- loadInstalledPkg pkgDbs ee.snapshotDumpPkgs pname
+
+      pure $ Just $
+        case mpkgid of
+          Nothing -> assert False $ Executable pkgId
+          Just pkgid -> simpleInstalledLib pkgId pkgid mempty
+  where
+    bindir = ee.baseConfigOpts.snapInstallRoot </> bindirSuffix
+
+loadInstalledPkg ::
+  ( HasCompiler env, HasProcessContext env, HasTerm env )
+  => [Path Abs Dir]
+  -> TVar (Map GhcPkgId DumpPackage)
+  -> PackageName
+  -> RIO env (Maybe GhcPkgId)
+loadInstalledPkg pkgDbs tvar name = do
+  pkgexe <- getGhcPkgExe
+  dps <- ghcPkgDescribe pkgexe name pkgDbs $ conduitDumpPackage .| CL.consume
+  case dps of
+    [] -> pure Nothing
+    [dp] -> do
+      liftIO $ atomically $ modifyTVar' tvar (Map.insert dp.ghcPkgId dp)
+      pure $ Just dp.ghcPkgId
+    _ -> throwM $ MultipleResultsBug name dps
+
+fulfillHaddockExpectations :: (MonadUnliftIO m, HasTerm env, MonadReader env m)
+  => PackageName
+  -> Maybe Curator
+  -> (KeepOutputOpen -> m ())
+  -> m ()
+fulfillHaddockExpectations pname mcurator action
+  | expectHaddockFailure mcurator = do
+      eres <- tryAny $ action KeepOpen
+      case eres of
+        Right () -> prettyWarnL
+          [ style Current (fromPackageName pname) <> ":"
+          , flow "unexpected Haddock success."
+          ]
+        Left _ -> pure ()
+  where
+    expectHaddockFailure = maybe False (Set.member pname . (.expectHaddockFailure))
+fulfillHaddockExpectations _ _ action = action CloseOnException
+
+-- | Check if any unlisted files have been found, and add them to the build cache.
+checkForUnlistedFiles ::
+     HasEnvConfig env
+  => TaskType
+  -> Path Abs Dir
+  -> RIO env [PackageWarning]
+checkForUnlistedFiles (TTLocalMutable lp) pkgDir = do
+  caches <- runMemoizedWith lp.newBuildCaches
+  (addBuildCache,warnings) <-
+    addUnlistedToBuildCache
+      lp.package
+      lp.cabalFP
+      lp.components
+      caches
+  forM_ (Map.toList addBuildCache) $ \(component, newToCache) -> do
+    let cache = Map.findWithDefault Map.empty component caches
+    writeBuildCache pkgDir component $
+      Map.unions (cache : newToCache)
+  pure warnings
+checkForUnlistedFiles TTRemotePackage{} _ = pure []
+
+-- | Implements running a package's tests. Also handles producing
+-- coverage reports if coverage is enabled.
+singleTest :: HasEnvConfig env
+           => TestOpts
+           -> [Text]
+           -> ActionContext
+           -> ExecuteEnv
+           -> Task
+           -> InstalledMap
+           -> RIO env ()
+singleTest topts testsToRun ac ee task installedMap = do
+  -- FIXME: Since this doesn't use cabal, we should be able to avoid using a
+  -- full blown 'withSingleContext'.
+  (allDepsMap, _cache) <- getConfigCache ee task installedMap True False
+  mcurator <- view $ buildConfigL . to (.curator)
+  let pname = pkgName $ taskProvides task
+      expectFailure = expectTestFailure pname mcurator
+  withSingleContext ac ee task.taskType allDepsMap (Just "test") $
+    \package _cabalfp pkgDir _cabal announce outputType -> do
+      config <- view configL
+      let needHpc = topts.coverage
+      toRun <-
+        if topts.disableRun
+          then do
+            announce "Test running disabled by --no-run-tests flag."
+            pure False
+          else if topts.rerunTests
+            then pure True
+            else do
+              status <- getTestStatus pkgDir
+              case status of
+                TSSuccess -> do
+                  unless (null testsToRun) $
+                    announce "skipping already passed test"
+                  pure False
+                TSFailure
+                  | expectFailure -> do
+                      announce "skipping already failed test that's expected to fail"
+                      pure False
+                  | otherwise -> do
+                      announce "rerunning previously failed test"
+                      pure True
+                TSUnknown -> pure True
+
+      when toRun $ do
+        buildDir <- distDirFromDir pkgDir
+        hpcDir <- hpcDirFromDir pkgDir
+        when needHpc (ensureDir hpcDir)
+
+        let suitesToRun
+              = [ testSuitePair
+                | testSuitePair <-
+                    ((fmap . fmap) (.interface) <$> collectionKeyValueList)
+                      package.testSuites
+                , let testName = fst testSuitePair
+                , testName `elem` testsToRun
+                ]
+
+        errs <- fmap Map.unions $ forM suitesToRun $ \(testName, suiteInterface) -> do
+          let stestName = T.unpack testName
+          (testName', isTestTypeLib) <-
+            case suiteInterface of
+              C.TestSuiteLibV09{} -> pure (stestName ++ "Stub", True)
+              C.TestSuiteExeV10{} -> pure (stestName, False)
+              interface -> throwM (TestSuiteTypeUnsupported interface)
+
+          let exeName = testName' ++
+                case config.platform of
+                  Platform _ Windows -> ".exe"
+                  _ -> ""
+          tixPath <- fmap (pkgDir </>) $ parseRelFile $ exeName ++ ".tix"
+          exePath <-
+            fmap (buildDir </>) $ parseRelFile $
+              "build/" ++ testName' ++ "/" ++ exeName
+          exists <- doesFileExist exePath
+          -- in Stack.Package.packageFromPackageDescription we filter out
+          -- package itself of any dependencies so any tests requiring loading
+          -- of their own package library will fail so to prevent this we return
+          -- it back here but unfortunately unconditionally
+          installed <- case Map.lookup pname installedMap of
+            Just (_, installed) -> pure $ Just installed
+            Nothing -> do
+              idMap <- liftIO $ readTVarIO ee.ghcPkgIds
+              pure $ Map.lookup (taskProvides task) idMap
+          let pkgGhcIdList = case installed of
+                               Just (Library _ libInfo) -> [libInfo.ghcPkgId]
+                               _ -> []
+          -- doctest relies on template-haskell in QuickCheck-based tests
+          thGhcId <-
+            case L.find ((== "template-haskell") . pkgName . (.packageIdent) . snd)
+                   (Map.toList ee.globalDumpPkgs) of
+              Just (ghcId, _) -> pure ghcId
+              Nothing -> throwIO TemplateHaskellNotFoundBug
+          -- env variable GHC_ENVIRONMENT is set for doctest so module names for
+          -- packages with proper dependencies should no longer get ambiguous
+          -- see e.g. https://github.com/doctest/issues/119
+          -- also we set HASKELL_DIST_DIR to a package dist directory so
+          -- doctest will be able to load modules autogenerated by Cabal
+          let setEnv f pc = modifyEnvVars pc $ \envVars ->
+                Map.insert "HASKELL_DIST_DIR" (T.pack $ toFilePath buildDir) $
+                Map.insert "GHC_ENVIRONMENT" (T.pack f) envVars
+              fp' = ee.tempDir </> testGhcEnvRelFile
+          -- Add a random suffix to avoid conflicts between parallel jobs
+          -- See https://github.com/commercialhaskell/stack/issues/5024
+          randomInt <- liftIO (randomIO :: IO Int)
+          let randomSuffix = "." <> show (abs randomInt)
+          fp <- toFilePath <$> addExtension randomSuffix fp'
+          let snapDBPath =
+                toFilePathNoTrailingSep ee.baseConfigOpts.snapDB
+              localDBPath =
+                toFilePathNoTrailingSep ee.baseConfigOpts.localDB
+              ghcEnv =
+                   "clear-package-db\n"
+                <> "global-package-db\n"
+                <> "package-db "
+                <> fromString snapDBPath
+                <> "\n"
+                <> "package-db "
+                <> fromString localDBPath
+                <> "\n"
+                <> foldMap
+                     ( \ghcId ->
+                            "package-id "
+                         <> display (unGhcPkgId ghcId)
+                         <> "\n"
+                     )
+                     (pkgGhcIdList ++ thGhcId:Map.elems allDepsMap)
+          writeFileUtf8Builder fp ghcEnv
+          menv <- liftIO $
+            setEnv fp =<< config.processContextSettings EnvSettings
+              { includeLocals = taskLocation task == Local
+              , includeGhcPackagePath = True
+              , stackExe = True
+              , localeUtf8 = False
+              , keepGhcRts = False
+              }
+          let emptyResult = Map.singleton testName Nothing
+          withProcessContext menv $ if exists
+            then do
+                -- We clear out the .tix files before doing a run.
+                when needHpc $ do
+                  tixexists <- doesFileExist tixPath
+                  when tixexists $
+                    prettyWarnL
+                      [ flow "Removing HPC file"
+                      , pretty tixPath <> "."
+                      ]
+                  liftIO $ ignoringAbsence (removeFile tixPath)
+
+                let args = topts.additionalArgs
+                    argsDisplay = case args of
+                      [] -> ""
+                      _ ->    ", args: "
+                           <> T.intercalate " " (map showProcessArgDebug args)
+                announce $
+                     "test (suite: "
+                  <> display testName
+                  <> display argsDisplay
+                  <> ")"
+
+                -- Clear "Progress: ..." message before
+                -- redirecting output.
+                case outputType of
+                  OTConsole _ -> do
+                    logStickyDone ""
+                    liftIO $ hFlush stdout
+                    liftIO $ hFlush stderr
+                  OTLogFile _ _ -> pure ()
+
+                let output = case outputType of
+                      OTConsole Nothing -> Nothing <$ inherit
+                      OTConsole (Just prefix) -> fmap
+                        ( \src -> Just $
+                               runConduit $ src
+                            .| CT.decodeUtf8Lenient
+                            .| CT.lines
+                            .| CL.map stripCR
+                            .| CL.mapM_ (\t -> logInfo $ prefix <> display t)
+                        )
+                        createSource
+                      OTLogFile _ h -> Nothing <$ useHandleOpen h
+                    optionalTimeout action
+                      | Just maxSecs <- topts.maximumTimeSeconds, maxSecs > 0 =
+                          timeout (maxSecs * 1000000) action
+                      | otherwise = Just <$> action
+
+                mec <- withWorkingDir (toFilePath pkgDir) $
+                  optionalTimeout $ proc (toFilePath exePath) args $ \pc0 -> do
+                    changeStdin <-
+                      if isTestTypeLib
+                        then do
+                          logPath <- buildLogPath package (Just stestName)
+                          ensureDir (parent logPath)
+                          pure $
+                              setStdin
+                            $ byteStringInput
+                            $ BL.fromStrict
+                            $ encodeUtf8 $ fromString $
+                            show ( logPath
+                                 , mkUnqualComponentName (T.unpack testName)
+                                 )
+                        else do
+                          isTerminal <- view $ globalOptsL . to (.terminal)
+                          if topts.allowStdin && isTerminal
+                            then pure id
+                            else pure $ setStdin $ byteStringInput mempty
+                    let pc = changeStdin
+                           $ setStdout output
+                           $ setStderr output
+                             pc0
+                    withProcessWait pc $ \p -> do
+                      case (getStdout p, getStderr p) of
+                        (Nothing, Nothing) -> pure ()
+                        (Just x, Just y) -> concurrently_ x y
+                        (x, y) -> assert False $
+                          concurrently_
+                            (fromMaybe (pure ()) x)
+                            (fromMaybe (pure ()) y)
+                      waitExitCode p
+                -- Add a trailing newline, incase the test
+                -- output didn't finish with a newline.
+                case outputType of
+                  OTConsole Nothing -> prettyInfo blankLine
+                  _ -> pure ()
+                -- Move the .tix file out of the package
+                -- directory into the hpc work dir, for
+                -- tidiness.
+                when needHpc $
+                  updateTixFile package.name tixPath testName'
+                let announceResult result =
+                      announce $
+                           "Test suite "
+                        <> display testName
+                        <> " "
+                        <> result
+                case mec of
+                  Just ExitSuccess -> do
+                    announceResult "passed"
+                    pure Map.empty
+                  Nothing -> do
+                    announceResult "timed out"
+                    if expectFailure
+                    then pure Map.empty
+                    else pure $ Map.singleton testName Nothing
+                  Just ec -> do
+                    announceResult "failed"
+                    if expectFailure
+                    then pure Map.empty
+                    else pure $ Map.singleton testName (Just ec)
+              else do
+                unless expectFailure $
+                  logError $
+                    displayShow $ TestSuiteExeMissing
+                      (package.buildType == C.Simple)
+                      exeName
+                      (packageNameString package.name)
+                      (T.unpack testName)
+                pure emptyResult
+
+        when needHpc $ do
+          let testsToRun' = map f testsToRun
+              f tName =
+                case (.interface) <$> mComponent of
+                  Just C.TestSuiteLibV09{} -> tName <> "Stub"
+                  _ -> tName
+               where
+                mComponent = collectionLookup tName package.testSuites
+          generateHpcReport pkgDir package testsToRun'
+
+        bs <- liftIO $
+          case outputType of
+            OTConsole _ -> pure ""
+            OTLogFile logFile h -> do
+              hClose h
+              S.readFile $ toFilePath logFile
+
+        let succeeded = Map.null errs
+        unless (succeeded || expectFailure) $
+          throwM $ TestSuiteFailure
+            (taskProvides task)
+            errs
+            (case outputType of
+               OTLogFile fp _ -> Just fp
+               OTConsole _ -> Nothing)
+            bs
+
+        setTestStatus pkgDir $ if succeeded then TSSuccess else TSFailure
+
+-- | Implements running a package's benchmarks.
+singleBench :: HasEnvConfig env
+            => BenchmarkOpts
+            -> [Text]
+            -> ActionContext
+            -> ExecuteEnv
+            -> Task
+            -> InstalledMap
+            -> RIO env ()
+singleBench beopts benchesToRun ac ee task installedMap = do
+  (allDepsMap, _cache) <- getConfigCache ee task installedMap False True
+  withSingleContext ac ee task.taskType allDepsMap (Just "bench") $
+    \_package _cabalfp _pkgDir cabal announce _outputType -> do
+      let args = map T.unpack benchesToRun <> maybe []
+                       ((:[]) . ("--benchmark-options=" <>))
+                       beopts.additionalArgs
+
+      toRun <-
+        if beopts.disableRun
+          then do
+            announce "Benchmark running disabled by --no-run-benchmarks flag."
+            pure False
+          else pure True
+
+      when toRun $ do
+        announce "benchmarks"
+        cabal CloseOnException KeepTHLoading ("bench" : args)
+
+-- Do not pass `-hpcdir` as GHC option if the coverage is not enabled.
+-- This helps running stack-compiled programs with dynamic interpreters like
+-- `hint`. Cfr: https://github.com/commercialhaskell/stack/issues/997
+extraBuildOptions :: (HasEnvConfig env, HasRunner env)
+                  => WhichCompiler -> BuildOpts -> RIO env [String]
+extraBuildOptions wc bopts = do
+  colorOpt <- appropriateGhcColorFlag
+  let optsFlag = compilerOptionsCabalFlag wc
+      baseOpts = maybe "" (" " ++) colorOpt
+  if bopts.testOpts.coverage
+    then do
+      hpcIndexDir <- toFilePathNoTrailingSep <$> hpcRelativeDir
+      pure [optsFlag, "-hpcdir " ++ hpcIndexDir ++ baseOpts]
+    else
+      pure [optsFlag, baseOpts]
+
+-- Library, sub-library, foreign library and executable build components.
+primaryComponentOptions ::
+     Bool
+     -- ^ Is Cabal copy limited to all libraries and executables?
+  -> Map Text ExecutableBuildStatus
+  -> LocalPackage
+  -> [String]
+primaryComponentOptions isOldCabalCopy executableBuildStatuses lp =
+  -- TODO: get this information from target parsing instead, which will allow
+  -- users to turn off library building if desired
+     ( if hasBuildableMainLibrary package
+         then map T.unpack
+           $ T.append "lib:" (T.pack (packageNameString package.name))
+           : map
+               (T.append "flib:")
+               (getBuildableListText package.foreignLibraries)
+         else []
+     )
+  ++ map
+       (T.unpack . T.append "lib:")
+       (getBuildableListText package.subLibraries)
+  ++ map
+       (T.unpack . T.append "exe:")
+       (Set.toList $ exesToBuild isOldCabalCopy executableBuildStatuses lp)
+ where
+  package = lp.package
+
+-- | History of this function:
+--
+-- * Normally it would do either all executables or if the user specified
+--   requested components, just build them. Afterwards, due to this Cabal bug
+--   <https://github.com/haskell/cabal/issues/2780>, we had to make Stack build
+--   all executables every time.
+--
+-- * In <https://github.com/commercialhaskell/stack/issues/3229> this was
+--   flagged up as very undesirable behavior on a large project, hence the
+--   behavior below that we build all executables once (modulo success), and
+--   thereafter pay attention to user-wanted components.
+--
+-- * The Cabal bug was fixed, in that the copy command of later Cabal versions
+--   allowed components to be specified. Consequently, Cabal may be satisified,
+--   even if all of a package's executables have not yet been built.
+exesToBuild ::
+     Bool
+     -- ^ Is Cabal copy limited to all libraries and executables?
+  -> Map Text ExecutableBuildStatus
+  -> LocalPackage
+  -> Set Text
+exesToBuild isOldCabalCopy executableBuildStatuses lp =
+  if cabalIsSatisfied isOldCabalCopy executableBuildStatuses && lp.wanted
+    then exeComponents lp.components
+    else buildableExes lp.package
+
+-- | Do the current executables satisfy Cabal's requirements?
+cabalIsSatisfied ::
+     Bool
+     -- ^ Is Cabal copy limited to all libraries and executables?
+  -> Map k ExecutableBuildStatus
+  -> Bool
+cabalIsSatisfied False _ = True
+cabalIsSatisfied True executableBuildStatuses =
+  all (== ExecutableBuilt) $ Map.elems executableBuildStatuses
+
+-- Test-suite and benchmark build components.
+finalComponentOptions :: LocalPackage -> [String]
+finalComponentOptions lp =
+  map (T.unpack . renderComponent) $
+  Set.toList $
+  Set.filter (\c -> isCTest c || isCBench c) lp.components
+
+taskComponents :: Task -> Set NamedComponent
+taskComponents task =
+  case task.taskType of
+    TTLocalMutable lp -> lp.components -- FIXME probably just want lpWanted
+    TTRemotePackage{} -> Set.empty
+
+expectTestFailure :: PackageName -> Maybe Curator -> Bool
+expectTestFailure pname =
+  maybe False (Set.member pname . (.expectTestFailure))
+
+expectBenchmarkFailure :: PackageName -> Maybe Curator -> Bool
+expectBenchmarkFailure pname =
+  maybe False (Set.member pname . (.expectBenchmarkFailure))
+
+fulfillCuratorBuildExpectations ::
+     (HasCallStack, HasTerm env)
+  => PackageName
+  -> Maybe Curator
+  -> Bool
+  -> Bool
+  -> b
+  -> RIO env b
+  -> RIO env b
+fulfillCuratorBuildExpectations pname mcurator enableTests _ defValue action
+  | enableTests && expectTestFailure pname mcurator = do
+      eres <- tryAny action
+      case eres of
+        Right res -> do
+          prettyWarnL
+            [ style Current (fromPackageName pname) <> ":"
+            , flow "unexpected test build success."
+            ]
+          pure res
+        Left _ -> pure defValue
+fulfillCuratorBuildExpectations pname mcurator _ enableBench defValue action
+  | enableBench && expectBenchmarkFailure pname mcurator = do
+      eres <- tryAny action
+      case eres of
+        Right res -> do
+          prettyWarnL
+            [ style Current (fromPackageName pname) <> ":"
+            , flow "unexpected benchmark build success."
+            ]
+          pure res
+        Left _ -> pure defValue
+fulfillCuratorBuildExpectations _ _ _ _ _ action = action
src/Stack/Build/Haddock.hs view
@@ -1,5 +1,6 @@-{-# LANGUAGE NoImplicitPrelude     #-}
-{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 -- | Generate haddocks
 module Stack.Build.Haddock
@@ -9,15 +10,22 @@   , openHaddocksInBrowser
   , shouldHaddockDeps
   , shouldHaddockPackage
+  , generateLocalHaddockForHackageArchives
   ) where
 
+import qualified Codec.Archive.Tar as Tar
+import qualified Codec.Compression.GZip as GZip
 import qualified Data.Foldable as F
 import qualified Data.HashSet as HS
 import qualified Data.Map.Strict as Map
 import qualified Data.Set as Set
 import qualified Data.Text as T
 import           Data.Time ( UTCTime )
-import           Path ( (</>), parent, parseRelDir )
+import           Distribution.Text ( display )
+import           Path
+                   ( (</>), addExtension, fromAbsDir, fromAbsFile, fromRelDir
+                   , parent, parseRelDir, parseRelFile
+                   )
 import           Path.Extra
                    ( parseCollapsedAbsFile, toFilePathNoTrailingSep
                    , tryGetModificationTime
@@ -26,17 +34,21 @@                    ( copyDirRecur', doesFileExist, ensureDir, ignoringAbsence
                    , removeDirRecur
                    )
+import qualified RIO.ByteString.Lazy as BL
 import           RIO.List ( intercalate )
 import           RIO.Process ( HasProcessContext, withWorkingDir )
-import           Stack.Constants ( docDirSuffix, relDirAll, relFileIndexHtml )
-import           Stack.Prelude
+import           Stack.Constants
+                   ( docDirSuffix, htmlDirSuffix, relDirAll, relFileIndexHtml )
+import           Stack.Constants.Config ( distDirFromDir )
+import           Stack.Prelude hiding ( Display (..) )
 import           Stack.Types.Build.Exception ( BuildException (..) )
 import           Stack.Types.CompilerPaths
                    ( CompilerPaths (..), HasCompiler (..) )
 import           Stack.Types.ConfigureOpts ( BaseConfigOpts (..) )
-import           Stack.Types.BuildOpts
-                   ( BuildOpts (..), BuildOptsCLI (..), HaddockOpts (..) )
+import           Stack.Types.BuildOpts ( BuildOpts (..), HaddockOpts (..) )
+import           Stack.Types.BuildOptsCLI ( BuildOptsCLI (..) )
 import           Stack.Types.DumpPackage ( DumpPackage (..) )
+import           Stack.Types.EnvConfig ( HasEnvConfig (..) )
 import           Stack.Types.GhcPkgId ( GhcPkgId )
 import           Stack.Types.Package
                    ( InstallLocation (..), LocalPackage (..), Package (..) )
@@ -52,7 +64,7 @@   -- ^ Build targets as determined by 'Stack.Build.Source.loadSourceMap'
   -> RIO env ()
 openHaddocksInBrowser bco pkgLocations buildTargets = do
-  let cliTargets = (boptsCLITargets . bcoBuildOptsCLI) bco
+  let cliTargets = bco.buildOptsCLI.targetsCLI
       getDocIndex = do
         let localDocs = haddockIndexFile (localDepsDocDir bco)
         localExists <- doesFileExist localDocs
@@ -98,13 +110,12 @@   -> Bool
 shouldHaddockPackage bopts wanted name =
   if Set.member name wanted
-    then boptsHaddock bopts
+    then bopts.buildHaddocks
     else shouldHaddockDeps bopts
 
 -- | Determine whether to build haddocks for dependencies.
 shouldHaddockDeps :: BuildOpts -> Bool
-shouldHaddockDeps bopts =
-  fromMaybe (boptsHaddock bopts) (boptsHaddockDeps bopts)
+shouldHaddockDeps bopts = fromMaybe bopts.buildHaddocks bopts.haddockDeps
 
 -- | Generate Haddock index and contents for local packages.
 generateLocalHaddockIndex ::
@@ -116,10 +127,10 @@ generateLocalHaddockIndex bco localDumpPkgs locals = do
   let dumpPackages =
         mapMaybe
-          ( \LocalPackage{lpPackage = Package{packageName, packageVersion}} ->
+          ( \LocalPackage {package = Package {name, version}} ->
               F.find
-                ( \dp -> dpPackageIdent dp ==
-                         PackageIdentifier packageName packageVersion
+                ( \dp -> dp.packageIdent ==
+                         PackageIdentifier name version
                 )
                 localDumpPkgs
           )
@@ -157,10 +168,10 @@     depDocDir
  where
   getGhcPkgId :: LocalPackage -> Maybe GhcPkgId
-  getGhcPkgId LocalPackage{lpPackage = Package{packageName, packageVersion}} =
-    let pkgId = PackageIdentifier packageName packageVersion
-        mdpPkg = F.find (\dp -> dpPackageIdent dp == pkgId) localDumpPkgs
-    in  fmap dpGhcPkgId mdpPkg
+  getGhcPkgId LocalPackage {package = Package {name, version}} =
+    let pkgId = PackageIdentifier name version
+        mdpPkg = F.find (\dp -> dp.packageIdent == pkgId) localDumpPkgs
+    in  fmap (.ghcPkgId) mdpPkg
   findTransitiveDepends :: [GhcPkgId] -> [GhcPkgId]
   findTransitiveDepends = (`go` HS.empty) . HS.fromList
    where
@@ -170,7 +181,7 @@         (ghcPkgId:_) ->
           let deps = case lookupDumpPackage ghcPkgId allDumpPkgs of
                        Nothing -> HS.empty
-                       Just pkgDP -> HS.fromList (dpDepends pkgDP)
+                       Just pkgDP -> HS.fromList pkgDP.depends
               deps' = deps `HS.difference` checked
               todo' = HS.delete ghcPkgId (deps' `HS.union` todo)
               checked' = HS.insert ghcPkgId checked
@@ -213,48 +224,53 @@             Left _ -> True
             Right indexModTime ->
               or [mt > indexModTime | (_, mt, _, _) <- interfaceOpts]
+        prettyDescr = style Current (fromString $ T.unpack descr)
     if needUpdate
       then do
-        prettyInfoL
-          [ flow "Updating Haddock index for"
-          , style Current (fromString $ T.unpack descr)
-          , "in"
-          , pretty destIndexFile <> "."
-          ]
+        prettyInfo $
+             fillSep
+               [ flow "Updating Haddock index for"
+               , prettyDescr
+               , "in:"
+               ]
+          <> line
+          <> pretty destIndexFile
         liftIO (mapM_ copyPkgDocs interfaceOpts)
-        haddockExeName <- view $ compilerPathsL.to (toFilePath . cpHaddock)
+        haddockExeName <- view $ compilerPathsL . to (toFilePath . (.haddock))
         withWorkingDir (toFilePath destDir) $ readProcessNull
           haddockExeName
           ( map
               (("--optghc=-package-db=" ++ ) . toFilePathNoTrailingSep)
-                 [bcoSnapDB bco, bcoLocalDB bco]
-              ++ hoAdditionalArgs (boptsHaddockOpts (bcoBuildOpts bco))
+                 [bco.snapDB, bco.localDB]
+              ++ bco.buildOpts.haddockOpts.additionalArgs
               ++ ["--gen-contents", "--gen-index"]
               ++ [x | (xs, _, _, _) <- interfaceOpts, x <- xs]
           )
       else
-        prettyInfoL
-          [ flow "Haddock index for"
-          , style Current (fromString $ T.unpack descr)
-          , flow "already up to date at"
-          , pretty destIndexFile <> "."
-          ]
+        prettyInfo $
+             fillSep
+               [ flow "Haddock index for"
+               , prettyDescr
+               , flow "already up to date at:"
+               ]
+          <> line
+          <> pretty destIndexFile
  where
   toInterfaceOpt ::
        DumpPackage
     -> IO (Maybe ([String], UTCTime, Path Abs File, Path Abs File))
-  toInterfaceOpt DumpPackage {dpHaddockInterfaces, dpPackageIdent, dpHaddockHtml} =
-    case dpHaddockInterfaces of
+  toInterfaceOpt DumpPackage {haddockInterfaces, packageIdent, haddockHtml} =
+    case haddockInterfaces of
       [] -> pure Nothing
       srcInterfaceFP:_ -> do
         srcInterfaceAbsFile <- parseCollapsedAbsFile srcInterfaceFP
-        let (PackageIdentifier name _) = dpPackageIdent
+        let (PackageIdentifier name _) = packageIdent
             destInterfaceRelFP =
               docRelFP FP.</>
-              packageIdentifierString dpPackageIdent FP.</>
+              packageIdentifierString packageIdent FP.</>
               (packageNameString name FP.<.> "haddock")
             docPathRelFP =
-              fmap ((docRelFP FP.</>) . FP.takeFileName) dpHaddockHtml
+              fmap ((docRelFP FP.</>) . FP.takeFileName) haddockHtml
             interfaces = intercalate "," $ mcons docPathRelFP [srcInterfaceFP]
 
         destInterfaceAbsFile <-
@@ -306,7 +322,7 @@ 
 -- | Path of local packages documentation directory.
 localDocDir :: BaseConfigOpts -> Path Abs Dir
-localDocDir bco = bcoLocalInstallRoot bco </> docDirSuffix
+localDocDir bco = bco.localInstallRoot </> docDirSuffix
 
 -- | Path of documentation directory for the dependencies of local packages
 localDepsDocDir :: BaseConfigOpts -> Path Abs Dir
@@ -314,4 +330,65 @@ 
 -- | Path of snapshot packages documentation directory.
 snapDocDir :: BaseConfigOpts -> Path Abs Dir
-snapDocDir bco = bcoSnapInstallRoot bco </> docDirSuffix
+snapDocDir bco = bco.snapInstallRoot </> docDirSuffix
+
+generateLocalHaddockForHackageArchives ::
+     (HasEnvConfig env, HasTerm env)
+  => [LocalPackage]
+  -> RIO env ()
+generateLocalHaddockForHackageArchives =
+  mapM_
+    ( \lp ->
+        let pkg = lp.package
+            pkgId = PackageIdentifier pkg.name pkg.version
+            pkgDir = parent lp.cabalFP
+        in generateLocalHaddockForHackageArchive pkgDir pkgId
+    )
+
+-- | Generate an archive file containing local Haddock documentation for
+-- Hackage, in a form accepted by Hackage.
+generateLocalHaddockForHackageArchive ::
+     (HasEnvConfig env, HasTerm env)
+  => Path Abs Dir
+     -- ^ The package directory.
+  -> PackageIdentifier
+     -- ^ The package name and version.
+  -> RIO env ()
+generateLocalHaddockForHackageArchive pkgDir pkgId = do
+  distDir <- distDirFromDir pkgDir
+  let pkgIdName = display pkgId
+      name = pkgIdName <> "-docs"
+      (nameRelDir, tarGzFileName) = fromMaybe
+        (error "impossible")
+        ( do relDir <- parseRelDir name
+             nameRelFile <- parseRelFile name
+             tarGz <- addExtension ".gz" =<< addExtension ".tar" nameRelFile
+             pure (relDir, tarGz)
+        )
+      tarGzFile = distDir </> tarGzFileName
+      docDir = distDir </> docDirSuffix </> htmlDirSuffix
+  createTarGzFile tarGzFile docDir nameRelDir
+  prettyInfo $
+       fillSep
+         [ flow "Archive of Haddock documentation for Hackage for"
+         , style Current (fromString pkgIdName)
+         , flow "created at:"
+         ]
+    <> line
+    <> pretty tarGzFile
+
+createTarGzFile ::
+     Path Abs File
+     -- ^ Full path to archive file
+  -> Path Abs Dir
+     -- ^ Base directory
+  -> Path Rel Dir
+     -- ^ Directory to archive, relative to base directory
+  -> RIO env ()
+createTarGzFile tar base dir = do
+   entries <- liftIO $ Tar.pack base' [dir']
+   BL.writeFile tar' $ GZip.compress $ Tar.write entries
+ where
+  base' = fromAbsDir base
+  dir' = fromRelDir dir
+  tar' = fromAbsFile tar
src/Stack/Build/Installed.hs view
@@ -1,12 +1,12 @@ {-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 {-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE ScopedTypeVariables   #-}
 
 -- Determine which packages are already installed
 module Stack.Build.Installed
-  ( InstalledMap
-  , Installed (..)
-  , getInstalled
-  , InstallMap
+  ( getInstalled
   , toInstallMap
   ) where
 
@@ -21,15 +21,18 @@ import           Stack.Prelude
 import           Stack.SourceMap ( getPLIVersion, loadVersion )
 import           Stack.Types.CompilerPaths ( getGhcPkgExe )
-import           Stack.Types.DumpPackage ( DumpPackage (..) )
+import           Stack.Types.DumpPackage
+                   ( DumpPackage (..), SublibDump (..), dpParentLibIdent )
 import           Stack.Types.EnvConfig
                     ( HasEnvConfig, packageDatabaseDeps, packageDatabaseExtra
                     , packageDatabaseLocal
                     )
 import           Stack.Types.GhcPkgId ( GhcPkgId )
-import           Stack.Types.Package
+import           Stack.Types.Installed
                    ( InstallLocation (..), InstallMap, Installed (..)
-                   , InstalledMap, InstalledPackageLocation (..)
+                   , InstalledLibraryInfo (..), InstalledMap
+                   , InstalledPackageLocation (..), PackageDatabase (..)
+                   , PackageDbVariety (..), toPackageDbVariety
                    )
 import           Stack.Types.SourceMap
                    ( DepPackage (..), ProjectPackage (..), SourceMap (..) )
@@ -37,15 +40,15 @@ toInstallMap :: MonadIO m => SourceMap -> m InstallMap
 toInstallMap sourceMap = do
   projectInstalls <-
-    for (smProject sourceMap) $ \pp -> do
-      version <- loadVersion (ppCommon pp)
+    for sourceMap.project $ \pp -> do
+      version <- loadVersion pp.projectCommon
       pure (Local, version)
   depInstalls <-
-    for (smDeps sourceMap) $ \dp ->
-      case dpLocation dp of
+    for sourceMap.deps $ \dp ->
+      case dp.location of
         PLImmutable pli -> pure (Snap, getPLIVersion pli)
         PLMutable _ -> do
-          version <- loadVersion (dpCommon dp)
+          version <- loadVersion dp.depCommon
           pure (Local, version)
   pure $ projectInstalls <> depInstalls
 
@@ -66,16 +69,17 @@ 
   let loadDatabase' = loadDatabase {-opts mcache-} installMap
 
-  (installedLibs0, globalDumpPkgs) <- loadDatabase' Nothing []
+  (installedLibs0, globalDumpPkgs) <- loadDatabase' GlobalPkgDb []
   (installedLibs1, _extraInstalled) <-
     foldM (\lhs' pkgdb ->
-      loadDatabase' (Just (ExtraGlobal, pkgdb)) (fst lhs')
+      loadDatabase' (UserPkgDb ExtraPkgDb pkgdb) (fst lhs')
       ) (installedLibs0, globalDumpPkgs) extraDBPaths
   (installedLibs2, snapshotDumpPkgs) <-
-    loadDatabase' (Just (InstalledTo Snap, snapDBPath)) installedLibs1
+    loadDatabase' (UserPkgDb (InstalledTo Snap) snapDBPath) installedLibs1
   (installedLibs3, localDumpPkgs) <-
-    loadDatabase' (Just (InstalledTo Local, localDBPath)) installedLibs2
-  let installedLibs = Map.fromList $ map lhPair installedLibs3
+    loadDatabase' (UserPkgDb (InstalledTo Local) localDBPath) installedLibs2
+  let installedLibs =
+        foldr' gatherAndTransformSubLoadHelper mempty installedLibs3
 
   -- Add in the executables that are installed, making sure to only trust a
   -- listed installation under the right circumstances (see below)
@@ -114,74 +118,86 @@ --
 -- The goal is to ascertain that the dependencies for a package are present,
 -- that it has profiling if necessary, and that it matches the version and
--- location needed by the SourceMap
+-- location needed by the SourceMap.
 loadDatabase ::
-     HasEnvConfig env
-  => InstallMap -- ^ to determine which installed things we should include
-  -> Maybe (InstalledPackageLocation, Path Abs Dir)
-     -- ^ package database, Nothing for global
-  -> [LoadHelper] -- ^ from parent databases
+     forall env. HasEnvConfig env
+  => InstallMap
+     -- ^ to determine which installed things we should include
+  -> PackageDatabase
+     -- ^ package database.
+  -> [LoadHelper]
+     -- ^ from parent databases
   -> RIO env ([LoadHelper], [DumpPackage])
-loadDatabase installMap mdb lhs0 = do
+loadDatabase installMap db lhs0 = do
   pkgexe <- getGhcPkgExe
-  (lhs1', dps) <- ghcPkgDump pkgexe (fmap snd (maybeToList mdb)) $
-                    conduitDumpPackage .| sink
-  lhs1 <- mapMaybeM (processLoadResult mdb) lhs1'
-  let lhs = pruneDeps id lhId lhDeps const (lhs0 ++ lhs1)
-  pure (map (\lh -> lh { lhDeps = [] }) $ Map.elems lhs, dps)
+  (lhs1', dps) <- ghcPkgDump pkgexe pkgDb $ conduitDumpPackage .| sink
+  lhs1 <- mapMaybeM processLoadResult lhs1'
+  let lhs = pruneDeps id (.ghcPkgId) (.depsGhcPkgId) const (lhs0 ++ lhs1)
+  pure (map (\lh -> lh { depsGhcPkgId = [] }) $ Map.elems lhs, dps)
  where
-  mloc = fmap fst mdb
-  sinkDP =  CL.map (isAllowed installMap mloc &&& toLoadHelper mloc)
+  pkgDb = case db of
+    GlobalPkgDb -> []
+    UserPkgDb _ fp -> [fp]
+
+  sinkDP =  CL.map (isAllowed installMap db' &&& toLoadHelper db')
          .| CL.consume
+   where
+    db' = toPackageDbVariety db
   sink =   getZipSink $ (,)
        <$> ZipSink sinkDP
        <*> ZipSink CL.consume
 
-processLoadResult :: HasLogFunc env
-                  => Maybe (InstalledPackageLocation, Path Abs Dir)
-                  -> (Allowed, LoadHelper)
-                  -> RIO env (Maybe LoadHelper)
-processLoadResult _ (Allowed, lh) = pure (Just lh)
-processLoadResult mdb (reason, lh) = do
-  logDebug $
-       "Ignoring package "
-    <> fromString (packageNameString (fst (lhPair lh)))
-    <> maybe
-         mempty
-         ( \db ->    ", from "
-                  <> displayShow db
-                  <> ","
-         )
-         mdb
-    <> " due to"
-    <> case reason of
-         UnknownPkg -> " it being unknown to the resolver / extra-deps."
-         WrongLocation mloc loc -> " wrong location: " <> displayShow (mloc, loc)
-         WrongVersion actual wanted ->
-              " wanting version "
-          <> fromString (versionString wanted)
-          <> " instead of "
-          <> fromString (versionString actual)
-  pure Nothing
+  processLoadResult :: (Allowed, LoadHelper) -> RIO env (Maybe LoadHelper)
+  processLoadResult (Allowed, lh) = pure (Just lh)
+  processLoadResult (reason, lh) = do
+    logDebug $
+         "Ignoring package "
+      <> fromPackageName (fst lh.pair)
+      <> case db of
+           GlobalPkgDb -> mempty
+           UserPkgDb loc fp -> ", from " <> displayShow (loc, fp) <> ","
+      <> " due to"
+      <> case reason of
+           UnknownPkg -> " it being unknown to the resolver / extra-deps."
+           WrongLocation db' loc ->
+             " wrong location: " <> displayShow (db', loc)
+           WrongVersion actual wanted ->
+                " wanting version "
+            <> fromString (versionString wanted)
+            <> " instead of "
+            <> fromString (versionString actual)
+    pure Nothing
 
+-- | Type representing results of 'isAllowed'.
 data Allowed
   = Allowed
+    -- ^ The installed package can be included in the set of relevant installed
+    -- packages.
   | UnknownPkg
-  | WrongLocation (Maybe InstalledPackageLocation) InstallLocation
+    -- ^ The installed package cannot be included in the set of relevant
+    -- installed packages because the package is unknown.
+  | WrongLocation PackageDbVariety InstallLocation
+    -- ^ The installed package cannot be included in the set of relevant
+    -- installed packages because the package is in the wrong package database.
   | WrongVersion Version Version
+    -- ^ The installed package cannot be included in the set of relevant
+    -- installed packages because the package has the wrong version.
   deriving (Eq, Show)
 
--- | Check if a can be included in the set of installed packages or not, based
--- on the package selections made by the user. This does not perform any
--- dirtiness or flag change checks.
-isAllowed :: InstallMap
-          -> Maybe InstalledPackageLocation
-          -> DumpPackage
-          -> Allowed
-isAllowed installMap mloc dp = case Map.lookup name installMap of
+-- | Check if an installed package can be included in the set of relevant
+-- installed packages or not, based on the package selections made by the user.
+-- This does not perform any dirtiness or flag change checks.
+isAllowed ::
+     InstallMap
+  -> PackageDbVariety
+     -- ^ The package database providing the installed package.
+  -> DumpPackage
+     -- ^ The installed package to check.
+  -> Allowed
+isAllowed installMap pkgDb dp = case Map.lookup name installMap of
   Nothing ->
     -- If the sourceMap has nothing to say about this package,
-    -- check if it represents a sublibrary first
+    -- check if it represents a sub-library first
     -- See: https://github.com/commercialhaskell/stack/issues/3899
     case dpParentLibIdent dp of
       Just (PackageIdentifier parentLibName version') ->
@@ -193,53 +209,113 @@       Nothing -> checkNotFound
   Just pii -> checkFound pii
  where
-  PackageIdentifier name version = dpPackageIdent dp
+  PackageIdentifier name version = dp.packageIdent
   -- Ensure that the installed location matches where the sourceMap says it
-  -- should be installed
-  checkLocation Snap = True -- snapshot deps could become mutable after getting
-                            -- any mutable dependency
-  checkLocation Local =
-    mloc == Just (InstalledTo Local) || mloc == Just ExtraGlobal -- 'locally' installed snapshot packages can come from extra dbs
-  -- Check if a package is allowed if it is found in the sourceMap
+  -- should be installed.
+  checkLocation Snap =
+     -- snapshot deps could become mutable after getting any mutable dependency.
+    True
+  checkLocation Local = case pkgDb of
+    GlobalDb -> False
+    -- 'locally' installed snapshot packages can come from 'extra' package
+    -- databases.
+    ExtraDb -> True
+    WriteOnlyDb -> False
+    MutableDb -> True
+  -- Check if an installed package is allowed if it is found in the sourceMap.
   checkFound (installLoc, installVer)
-    | not (checkLocation installLoc) = WrongLocation mloc installLoc
+    | not (checkLocation installLoc) = WrongLocation pkgDb installLoc
     | version /= installVer = WrongVersion version installVer
     | otherwise = Allowed
-  -- check if a package is allowed if it is not found in the sourceMap
-  checkNotFound = case mloc of
-    -- The sourceMap has nothing to say about this global package, so we can use it
-    Nothing -> Allowed
-    Just ExtraGlobal -> Allowed
+  -- Check if an installed package is allowed if it is not found in the
+  -- sourceMap.
+  checkNotFound = case pkgDb of
+    -- The sourceMap has nothing to say about this global package, so we can use
+    -- it.
+    GlobalDb -> Allowed
+    ExtraDb -> Allowed
     -- For non-global packages, don't include unknown packages.
     -- See: https://github.com/commercialhaskell/stack/issues/292
-    Just _ -> UnknownPkg
+    WriteOnlyDb -> UnknownPkg
+    MutableDb -> UnknownPkg
 
+-- | Type representing certain information about an installed package.
 data LoadHelper = LoadHelper
-  { lhId   :: !GhcPkgId
-  , lhDeps :: ![GhcPkgId]
-  , lhPair :: !(PackageName, (InstallLocation, Installed))
+  { ghcPkgId :: !GhcPkgId
+    -- ^ The package's id.
+  , subLibDump :: !(Maybe SublibDump)
+  , depsGhcPkgId :: ![GhcPkgId]
+    -- ^ Unless the package's name is that of a 'wired-in' package, a list of
+    -- the ids of the installed packages that are the package's dependencies.
+  , pair :: !(PackageName, (InstallLocation, Installed))
+    -- ^ A pair of (a) the package's name and (b) a pair of the relevant
+    -- database (write-only or mutable) and information about the library
+    -- installed.
   }
   deriving Show
 
-toLoadHelper :: Maybe InstalledPackageLocation -> DumpPackage -> LoadHelper
-toLoadHelper mloc dp = LoadHelper
-  { lhId = gid
-  , lhDeps =
-      -- We always want to consider the wired in packages as having all of their
-      -- dependencies installed, since we have no ability to reinstall them.
-      -- This is especially important for using different minor versions of GHC,
-      -- where the dependencies of wired-in packages may change slightly and
-      -- therefore not match the snapshot.
-      if name `Set.member` wiredInPackages
-        then []
-        else dpDepends dp
-  , lhPair = (name, (toPackageLocation mloc, Library ident gid (Right <$> dpLicense dp)))
+toLoadHelper :: PackageDbVariety -> DumpPackage -> LoadHelper
+toLoadHelper pkgDb dp = LoadHelper
+  { ghcPkgId
+  , depsGhcPkgId
+  , subLibDump = dp.sublib
+  , pair
   }
  where
-  gid = dpGhcPkgId dp
-  ident@(PackageIdentifier name _) = dpPackageIdent dp
+  ghcPkgId = dp.ghcPkgId
+  ident@(PackageIdentifier name _) = dp.packageIdent
+  depsGhcPkgId =
+    -- We always want to consider the wired in packages as having all of their
+    -- dependencies installed, since we have no ability to reinstall them. This
+    -- is especially important for using different minor versions of GHC, where
+    -- the dependencies of wired-in packages may change slightly and therefore
+    -- not match the snapshot.
+    if name `Set.member` wiredInPackages
+      then []
+      else dp.depends
+  installedLibInfo = InstalledLibraryInfo ghcPkgId (Right <$> dp.license) mempty
 
-toPackageLocation :: Maybe InstalledPackageLocation -> InstallLocation
-toPackageLocation Nothing = Snap
-toPackageLocation (Just ExtraGlobal) = Snap
-toPackageLocation (Just (InstalledTo loc)) = loc
+  toInstallLocation :: PackageDbVariety -> InstallLocation
+  toInstallLocation GlobalDb = Snap
+  toInstallLocation ExtraDb = Snap
+  toInstallLocation WriteOnlyDb = Snap
+  toInstallLocation MutableDb = Local
+
+  pair = (name, (toInstallLocation pkgDb, Library ident installedLibInfo))
+
+-- | This is where sublibraries and main libraries are assembled into a single
+-- entity Installed package, where all ghcPkgId live.
+gatherAndTransformSubLoadHelper ::
+     LoadHelper
+  -> Map PackageName (InstallLocation, Installed)
+  -> Map PackageName (InstallLocation, Installed)
+gatherAndTransformSubLoadHelper lh =
+  Map.insertWith onPreviousLoadHelper key value
+ where
+  -- Here we assume that both have the same location which already was a prior
+  -- assumption in Stack.
+  onPreviousLoadHelper
+      (pLoc, Library pn incomingLibInfo)
+      (_, Library _ existingLibInfo)
+    = ( pLoc
+      , Library pn existingLibInfo
+          { subLib = Map.union
+              incomingLibInfo.subLib
+              existingLibInfo.subLib
+          , ghcPkgId = if isJust lh.subLibDump
+                      then existingLibInfo.ghcPkgId
+                      else incomingLibInfo.ghcPkgId
+          }
+      )
+  onPreviousLoadHelper newVal _oldVal = newVal
+  (key, value) = case lh.subLibDump of
+    Nothing -> (rawPackageName, rawValue)
+    Just sd -> (sd.packageName, updateAsSublib sd <$> rawValue)
+  (rawPackageName, rawValue) = lh.pair
+  updateAsSublib
+      sd
+      (Library (PackageIdentifier _sublibMungedPackageName version) libInfo)
+    = Library
+        (PackageIdentifier key version)
+        libInfo { subLib = Map.singleton sd.libraryName libInfo.ghcPkgId }
+  updateAsSublib _ v = v
src/Stack/Build/Source.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 {-# LANGUAGE OverloadedStrings     #-}
-{-# LANGUAGE RecordWildCards       #-}
 
 -- Load information on package sources
 module Stack.Build.Source
@@ -24,7 +25,11 @@ import qualified Pantry.SHA256 as SHA256
 import           Stack.Build.Cache ( tryGetBuildCache )
 import           Stack.Build.Haddock ( shouldHaddockDeps )
-import           Stack.Package ( resolvePackage )
+import           Stack.Package
+                   ( buildableBenchmarks, buildableExes, buildableTestSuites
+                   , hasBuildableMainLibrary, resolvePackage
+                   )
+import           Stack.PackageFile ( getPackageFile )
 import           Stack.Prelude
 import           Stack.SourceMap
                    ( DumpedGlobalPackage, checkFlagsUsedThrowing
@@ -35,9 +40,10 @@ import           Stack.Types.ApplyProgOptions ( ApplyProgOptions (..) )
 import           Stack.Types.BuildConfig
                    ( BuildConfig (..), HasBuildConfig (..) )
-import           Stack.Types.BuildOpts
-                   ( ApplyCLIFlag (..), BuildOpts (..), BuildOptsCLI (..)
-                   , TestOpts (..), boptsCLIAllProgOptions
+import           Stack.Types.BuildOpts ( BuildOpts (..), TestOpts (..) )
+import           Stack.Types.BuildOptsCLI
+                   ( ApplyCLIFlag (..), BuildOptsCLI (..)
+                   , boptsCLIAllProgOptions
                    )
 import           Stack.Types.CabalConfigKey ( CabalConfigKey (..) )
 import           Stack.Types.CompilerPaths ( HasCompiler, getCompilerPath )
@@ -49,13 +55,14 @@                    )
 import           Stack.Types.FileDigestCache ( readFileDigest )
 import           Stack.Types.NamedComponent
-                   ( NamedComponent (..), isCInternalLib )
+                   ( NamedComponent (..), isCSubLib, splitComponents )
 import           Stack.Types.Package
                    ( FileCacheInfo (..), LocalPackage (..), Package (..)
-                   , PackageConfig (..), PackageLibraries (..)
-                   , dotCabalGetPath, memoizeRefWith, runMemoizedWith
+                   , PackageConfig (..), dotCabalGetPath, memoizeRefWith
+                   , runMemoizedWith
                    )
-import           Stack.Types.PackageFile ( PackageWarning, getPackageFiles )
+import           Stack.Types.PackageFile
+                   ( PackageComponentFile (..), PackageWarning )
 import           Stack.Types.Platform ( HasPlatform (..) )
 import           Stack.Types.SourceMap
                    ( CommonPackage (..), DepPackage (..), ProjectPackage (..)
@@ -69,16 +76,16 @@ -- | loads and returns project packages
 projectLocalPackages :: HasEnvConfig env => RIO env [LocalPackage]
 projectLocalPackages = do
-  sm <- view $ envConfigL.to envConfigSourceMap
-  for (toList $ smProject sm) loadLocalPackage
+  sm <- view $ envConfigL . to (.sourceMap)
+  for (toList sm.project) loadLocalPackage
 
 -- | loads all local dependencies - project packages and local extra-deps
 localDependencies :: HasEnvConfig env => RIO env [LocalPackage]
 localDependencies = do
-  bopts <- view $ configL.to configBuild
-  sourceMap <- view $ envConfigL . to envConfigSourceMap
-  forMaybeM (Map.elems $ smDeps sourceMap) $ \dp ->
-    case dpLocation dp of
+  bopts <- view $ configL . to (.build)
+  sourceMap <- view $ envConfigL . to (.sourceMap)
+  forMaybeM (Map.elems sourceMap.deps) $ \dp ->
+    case dp.location of
       PLMutable dir -> do
         pp <- mkProjectPackage YesPrintWarnings dir (shouldHaddockDeps bopts)
         Just <$> loadLocalPackage pp
@@ -91,55 +98,54 @@               -> BuildOptsCLI
               -> SMActual DumpedGlobalPackage
               -> RIO env SourceMap
-loadSourceMap smt boptsCli sma = do
+loadSourceMap targets boptsCli sma = do
   bconfig <- view buildConfigL
-  let compiler = smaCompiler sma
-      project = M.map applyOptsFlagsPP $ smaProject sma
-      bopts = configBuild (bcConfig bconfig)
-      applyOptsFlagsPP p@ProjectPackage{ppCommon = c} =
-        p{ppCommon = applyOptsFlags (M.member (cpName c) (smtTargets smt)) True c}
-      deps0 = smtDeps smt <> smaDeps sma
+  let compiler = sma.compiler
+      project = M.map applyOptsFlagsPP sma.project
+      bopts = bconfig.config.build
+      applyOptsFlagsPP p@ProjectPackage{ projectCommon = c } = p
+        { projectCommon = applyOptsFlags (M.member c.name targets.targets) True c }
+      deps0 = targets.deps <> sma.deps
       deps = M.map applyOptsFlagsDep deps0
-      applyOptsFlagsDep d@DepPackage{dpCommon = c} =
-        d{dpCommon = applyOptsFlags (M.member (cpName c) (smtDeps smt)) False c}
+      applyOptsFlagsDep d@DepPackage{ depCommon = c } = d
+        { depCommon = applyOptsFlags (M.member c.name targets.deps) False c }
       applyOptsFlags isTarget isProjectPackage common =
-        let name = cpName common
+        let name = common.name
             flags = getLocalFlags boptsCli name
             ghcOptions =
               generalGhcOptions bconfig boptsCli isTarget isProjectPackage
             cabalConfigOpts =
-              generalCabalConfigOpts bconfig boptsCli (cpName common) isTarget isProjectPackage
+              generalCabalConfigOpts bconfig boptsCli common.name isTarget isProjectPackage
         in  common
-              { cpFlags =
+              { flags =
                   if M.null flags
-                    then cpFlags common
+                    then common.flags
                     else flags
-              , cpGhcOptions =
-                  ghcOptions ++ cpGhcOptions common
-              , cpCabalConfigOpts =
-                  cabalConfigOpts ++ cpCabalConfigOpts common
-              , cpHaddocks =
+              , ghcOptions =
+                  ghcOptions ++ common.ghcOptions
+              , cabalConfigOpts =
+                  cabalConfigOpts ++ common.cabalConfigOpts
+              , buildHaddocks =
                   if isTarget
-                    then boptsHaddock bopts
+                    then bopts.buildHaddocks
                     else shouldHaddockDeps bopts
               }
       packageCliFlags = Map.fromList $
         mapMaybe maybeProjectFlags $
-        Map.toList (boptsCLIFlags boptsCli)
+        Map.toList boptsCli.flags
       maybeProjectFlags (ACFByName name, fs) = Just (name, fs)
       maybeProjectFlags _ = Nothing
-      globals = pruneGlobals (smaGlobal sma) (Map.keysSet deps)
+      globalPkgs = pruneGlobals sma.globals (Map.keysSet deps)
   logDebug "Checking flags"
   checkFlagsUsedThrowing packageCliFlags FSCommandLine project deps
   logDebug "SourceMap constructed"
-  pure
-    SourceMap
-      { smTargets = smt
-      , smCompiler = compiler
-      , smProject = project
-      , smDeps = deps
-      , smGlobal = globals
-      }
+  pure SourceMap
+    { targets
+    , compiler
+    , project
+    , deps
+    , globalPkgs
+    }
 
 -- | Get a 'SourceMapHash' for a given 'SourceMap'
 --
@@ -168,7 +174,7 @@ hashSourceMapData boptsCli sm = do
   compilerPath <- getUtf8Builder . fromString . toFilePath <$> getCompilerPath
   compilerInfo <- getCompilerInfo
-  immDeps <- forM (Map.elems (smDeps sm)) depPackageHashableContent
+  immDeps <- forM (Map.elems sm.deps) depPackageHashableContent
   bc <- view buildConfigL
   let -- extra bytestring specifying GHC options supposed to be applied to GHC
       -- boot packages so we'll have different hashes when bare resolver
@@ -183,18 +189,18 @@   pure $ SourceMapHash (SHA256.hashLazyBytes hashedContent)
 
 depPackageHashableContent :: (HasConfig env) => DepPackage -> RIO env Builder
-depPackageHashableContent DepPackage {..} =
-  case dpLocation of
+depPackageHashableContent dp =
+  case dp.location of
     PLMutable _ -> pure ""
     PLImmutable pli -> do
       let flagToBs (f, enabled) =
             if enabled
               then ""
               else "-" <> fromString (C.unFlagName f)
-          flags = map flagToBs $ Map.toList (cpFlags dpCommon)
-          ghcOptions = map display (cpGhcOptions dpCommon)
-          cabalConfigOpts = map display (cpCabalConfigOpts dpCommon)
-          haddocks = if cpHaddocks dpCommon then "haddocks" else ""
+          flags = map flagToBs $ Map.toList dp.depCommon.flags
+          ghcOptions = map display dp.depCommon.ghcOptions
+          cabalConfigOpts = map display dp.depCommon.cabalConfigOpts
+          haddocks = if dp.depCommon.buildHaddocks then "haddocks" else ""
           hash = immutableLocSha pli
       pure
         $  hash
@@ -213,7 +219,7 @@   , Map.findWithDefault Map.empty ACFAllProjectPackages cliFlags
   ]
  where
-  cliFlags = boptsCLIFlags boptsCli
+  cliFlags = boptsCli.flags
 
 -- | Get the options to pass to @./Setup.hs configure@
 generalCabalConfigOpts ::
@@ -224,14 +230,14 @@   -> Bool
   -> [Text]
 generalCabalConfigOpts bconfig boptsCli name isTarget isLocal = concat
-  [ Map.findWithDefault [] CCKEverything (configCabalConfigOpts config)
+  [ Map.findWithDefault [] CCKEverything config.cabalConfigOpts
   , if isLocal
-      then Map.findWithDefault [] CCKLocals (configCabalConfigOpts config)
+      then Map.findWithDefault [] CCKLocals config.cabalConfigOpts
       else []
   , if isTarget
-      then Map.findWithDefault [] CCKTargets (configCabalConfigOpts config)
+      then Map.findWithDefault [] CCKTargets config.cabalConfigOpts
       else []
-  , Map.findWithDefault [] (CCKPackage name) (configCabalConfigOpts config)
+  , Map.findWithDefault [] (CCKPackage name) config.cabalConfigOpts
   , if includeExtraOptions
       then boptsCLIAllProgOptions boptsCli
       else []
@@ -239,7 +245,7 @@  where
   config = view configL bconfig
   includeExtraOptions =
-    case configApplyProgOptions config of
+    case config.applyProgOptions of
       APOTargets -> isTarget
       APOLocals -> isLocal
       APOEverything -> True
@@ -248,43 +254,31 @@ -- configuration and commandline.
 generalGhcOptions :: BuildConfig -> BuildOptsCLI -> Bool -> Bool -> [Text]
 generalGhcOptions bconfig boptsCli isTarget isLocal = concat
-  [ Map.findWithDefault [] AGOEverything (configGhcOptionsByCat config)
+  [ Map.findWithDefault [] AGOEverything config.ghcOptionsByCat
   , if isLocal
-      then Map.findWithDefault [] AGOLocals (configGhcOptionsByCat config)
+      then Map.findWithDefault [] AGOLocals config.ghcOptionsByCat
       else []
   , if isTarget
-      then Map.findWithDefault [] AGOTargets (configGhcOptionsByCat config)
+      then Map.findWithDefault [] AGOTargets config.ghcOptionsByCat
       else []
-  , concat [["-fhpc"] | isLocal && toCoverage (boptsTestOpts bopts)]
-  , if boptsLibProfile bopts || boptsExeProfile bopts
+  , concat [["-fhpc"] | isLocal && bopts.testOpts.coverage]
+  , if bopts.libProfile || bopts.exeProfile
       then ["-fprof-auto", "-fprof-cafs"]
       else []
-  , [ "-g" | not $ boptsLibStrip bopts || boptsExeStrip bopts ]
+  , [ "-g" | not $ bopts.libStrip || bopts.exeStrip ]
   , if includeExtraOptions
-      then boptsCLIGhcOptions boptsCli
+      then boptsCli.ghcOptions
       else []
   ]
  where
-  bopts = configBuild config
+  bopts =  config.build
   config = view configL bconfig
   includeExtraOptions =
-    case configApplyGhcOptions config of
+    case config.applyGhcOptions of
       AGOTargets -> isTarget
       AGOLocals -> isLocal
       AGOEverything -> True
 
-splitComponents :: [NamedComponent]
-                -> (Set Text, Set Text, Set Text)
-splitComponents =
-  go id id id
- where
-  go a b c [] = (Set.fromList $ a [], Set.fromList $ b [], Set.fromList $ c [])
-  go a b c (CLib:xs) = go a b c xs
-  go a b c (CInternalLib x:xs) = go (a . (x:)) b c xs
-  go a b c (CExe x:xs) = go (a . (x:)) b c xs
-  go a b c (CTest x:xs) = go a (b . (x:)) c xs
-  go a b c (CBench x:xs) = go a b (c . (x:)) xs
-
 loadCommonPackage ::
      forall env. (HasBuildConfig env, HasSourceMap env)
   => CommonPackage
@@ -292,10 +286,10 @@ loadCommonPackage common = do
   config <-
     getPackageConfig
-      (cpFlags common)
-      (cpGhcOptions common)
-      (cpCabalConfigOpts common)
-  gpkg <- liftIO $ cpGPD common
+      common.flags
+      common.ghcOptions
+      common.cabalConfigOpts
+  gpkg <- liftIO common.gpd
   pure $ resolvePackage config gpkg
 
 -- | Upgrade the initial project package info to a full-blown @LocalPackage@
@@ -306,31 +300,35 @@   -> RIO env LocalPackage
 loadLocalPackage pp = do
   sm <- view sourceMapL
-  let common = ppCommon pp
+  let common = pp.projectCommon
   bopts <- view buildOptsL
-  mcurator <- view $ buildConfigL.to bcCurator
+  mcurator <- view $ buildConfigL . to (.curator)
   config <- getPackageConfig
-              (cpFlags common)
-              (cpGhcOptions common)
-              (cpCabalConfigOpts common)
+              common.flags
+              common.ghcOptions
+              common.cabalConfigOpts
   gpkg <- ppGPD pp
-  let name = cpName common
-      mtarget = M.lookup name (smtTargets $ smTargets sm)
+  let name = common.name
+      mtarget = M.lookup name sm.targets.targets
       (exeCandidates, testCandidates, benchCandidates) =
         case mtarget of
-          Just (TargetComps comps) -> splitComponents $ Set.toList comps
+          Just (TargetComps comps) ->
+            -- Currently, a named library component (a sub-library) cannot be
+            -- specified as a build target.
+            let (_s, e, t, b) = splitComponents $ Set.toList comps
+            in  (e, t, b)
           Just (TargetAll _packageType) ->
-            ( packageExes pkg
-            , if    boptsTests bopts
-                 && maybe True (Set.notMember name . curatorSkipTest) mcurator
-                then Map.keysSet (packageTests pkg)
+            ( buildableExes pkg
+            , if    bopts.tests
+                 && maybe True (Set.notMember name . (.skipTest)) mcurator
+                then buildableTestSuites pkg
                 else Set.empty
-            , if    boptsBenchmarks bopts
+            , if    bopts.benchmarks
                  && maybe
                       True
-                      (Set.notMember name . curatorSkipBenchmark)
+                      (Set.notMember name . (.skipBenchmark))
                       mcurator
-                then packageBenchmarks pkg
+                then buildableBenchmarks pkg
                 else Set.empty
             )
           Nothing -> mempty
@@ -342,16 +340,12 @@         -- individual executables or library") is resolved, 'hasLibrary' is only
         -- relevant if the library is part of the target spec.
         Just _ ->
-          let hasLibrary =
-                case packageLibraries pkg of
-                  NoLibraries -> False
-                  HasLibraries _ -> True
-          in     hasLibrary
-              || not (Set.null nonLibComponents)
-              || not (Set.null $ packageInternalLibraries pkg)
+             hasBuildableMainLibrary pkg
+          || not (Set.null nonLibComponents)
+          || not (null pkg.subLibraries)
 
       filterSkippedComponents =
-        Set.filter (not . (`elem` boptsSkipComponents bopts))
+        Set.filter (not . (`elem` bopts.skipComponents))
 
       (exes, tests, benches) = ( filterSkippedComponents exeCandidates
                                , filterSkippedComponents testCandidates
@@ -367,8 +361,8 @@         ]
 
       btconfig = config
-        { packageConfigEnableTests = not $ Set.null tests
-        , packageConfigEnableBenchmarks = not $ Set.null benches
+        { enableTests = not $ Set.null tests
+        , enableBenchmarks = not $ Set.null benches
         }
 
       -- We resolve the package in 2 different configurations:
@@ -387,7 +381,7 @@         | otherwise = Just (resolvePackage btconfig gpkg)
 
   componentFiles <- memoizeRefWith $
-    fst <$> getPackageFilesForTargets pkg (ppCabalFP pp) nonLibComponents
+    fst <$> getPackageFilesForTargets pkg pp.cabalFP nonLibComponents
 
   checkCacheResults <- memoizeRefWith $ do
     componentFiles' <- runMemoizedWith componentFiles
@@ -412,26 +406,26 @@         M.fromList . map (\(c, (_, cache)) -> (c, cache)) <$> checkCacheResults
 
   pure LocalPackage
-    { lpPackage = pkg
-    , lpTestBench = btpkg
-    , lpComponentFiles = componentFiles
-    , lpBuildHaddocks = cpHaddocks (ppCommon pp)
-    , lpForceDirty = boptsForceDirty bopts
-    , lpDirtyFiles = dirtyFiles
-    , lpNewBuildCaches = newBuildCaches
-    , lpCabalFile = ppCabalFP pp
-    , lpWanted = isWanted
-    , lpComponents = nonLibComponents
+    { package = pkg
+    , testBench = btpkg
+    , componentFiles
+    , buildHaddocks = pp.projectCommon.buildHaddocks
+    , forceDirty = bopts.forceDirty
+    , dirtyFiles
+    , newBuildCaches
+    , cabalFP = pp.cabalFP
+    , wanted = isWanted
+    , components = nonLibComponents
       -- TODO: refactor this so that it's easier to be sure that these
       -- components are indeed unbuildable.
       --
       -- The reasoning here is that if the STLocalComps specification made it
       -- through component parsing, but the components aren't present, then they
       -- must not be buildable.
-    , lpUnbuildable = toComponents
-        (exes `Set.difference` packageExes pkg)
-        (tests `Set.difference` Map.keysSet (packageTests pkg))
-        (benches `Set.difference` packageBenchmarks pkg)
+    , unbuildable = toComponents
+        (exes `Set.difference` buildableExes pkg)
+        (tests `Set.difference` buildableTestSuites pkg)
+        (benches `Set.difference` buildableBenchmarks pkg)
     }
 
 -- | Compare the current filesystem state to the cached information, and
@@ -461,7 +455,7 @@   go fp _ _ | takeFileName fp == "cabal_macros.h" = pure (Set.empty, Map.empty)
   -- Common case where it's in the cache and on the filesystem.
   go fp (Just digest') (Just fci)
-      | fciHash fci == digest' = pure (Set.empty, Map.singleton fp fci)
+      | fci.hash == digest' = pure (Set.empty, Map.singleton fp fci)
       | otherwise =
           pure (Set.singleton fp, Map.singleton fp $ FileCacheInfo digest')
   -- Missing file. Add it to dirty files, but no FileCacheInfo.
@@ -507,10 +501,10 @@   -> Set NamedComponent
   -> RIO env (Map NamedComponent (Set (Path Abs File)), [PackageWarning])
 getPackageFilesForTargets pkg cabalFP nonLibComponents = do
-  (components',compFiles,otherFiles,warnings) <-
-    getPackageFiles (packageFiles pkg) cabalFP
+  PackageComponentFile components' compFiles otherFiles warnings <-
+    getPackageFile pkg cabalFP
   let necessaryComponents =
-        Set.insert CLib $ Set.filter isCInternalLib (M.keysSet components')
+        Set.insert CLib $ Set.filter isCSubLib (M.keysSet components')
       components = necessaryComponents `Set.union` nonLibComponents
       componentsFiles = M.map
         (\files ->
@@ -522,7 +516,7 @@ -- | Get file digest, if it exists
 getFileDigestMaybe :: HasEnvConfig env => FilePath -> RIO env (Maybe SHA256)
 getFileDigestMaybe fp = do
-  cache <- view $ envConfigL.to envConfigFileDigestCache
+  cache <- view $ envConfigL . to (.fileDigestCache)
   catch
     (Just <$> readFileDigest cache fp)
     (\e -> if isDoesNotExistError e then pure Nothing else throwM e)
@@ -538,11 +532,11 @@   platform <- view platformL
   compilerVersion <- view actualCompilerVersionL
   pure PackageConfig
-    { packageConfigEnableTests = False
-    , packageConfigEnableBenchmarks = False
-    , packageConfigFlags = flags
-    , packageConfigGhcOptions = ghcOptions
-    , packageConfigCabalConfigOpts = cabalConfigOpts
-    , packageConfigCompilerVersion = compilerVersion
-    , packageConfigPlatform = platform
+    { enableTests = False
+    , enableBenchmarks = False
+    , flags = flags
+    , ghcOptions = ghcOptions
+    , cabalConfigOpts = cabalConfigOpts
+    , compilerVersion = compilerVersion
+    , platform = platform
     }
src/Stack/Build/Target.hs view
@@ -1,9 +1,12 @@-{-# LANGUAGE NoImplicitPrelude   #-}
-{-# LANGUAGE DataKinds           #-}
-{-# LANGUAGE GADTs               #-}
-{-# LANGUAGE MultiWayIf          #-}
-{-# LANGUAGE OverloadedStrings   #-}
-{-# LANGUAGE ViewPatterns        #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GADTs                 #-}
+{-# LANGUAGE MultiWayIf            #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE ViewPatterns          #-}
 
 -- | Parsing command line targets
 --
@@ -76,7 +79,7 @@ import           Stack.Prelude
 import           Stack.Types.BuildConfig
                    ( BuildConfig (..), HasBuildConfig (..) )
-import           Stack.Types.BuildOpts ( BuildOptsCLI (..) )
+import           Stack.Types.BuildOptsCLI ( BuildOptsCLI (..) )
 import           Stack.Types.Config ( Config (..) )
 import           Stack.Types.NamedComponent
                    ( NamedComponent (..), renderComponent )
@@ -99,14 +102,14 @@ --------------------------------------------------------------------------------
 
 -- | Raw target information passed on the command line.
-newtype RawInput = RawInput { unRawInput :: Text }
+newtype RawInput = RawInput { rawInput :: Text }
 
 getRawInput ::
      BuildOptsCLI
   -> Map PackageName ProjectPackage
   -> ([Text], [RawInput])
 getRawInput boptscli locals =
-  let textTargets' = boptsCLITargets boptscli
+  let textTargets' = boptscli.targetsCLI
       textTargets =
         -- Handle the no targets case, which means we pass in the names of all
         -- project packages
@@ -233,13 +236,13 @@ --------------------------------------------------------------------------------
 
 data ResolveResult = ResolveResult
-  { rrName :: !PackageName
-  , rrRaw :: !RawInput
-  , rrComponent :: !(Maybe NamedComponent)
+  { name :: !PackageName
+  , rawInput :: !RawInput
+  , component :: !(Maybe NamedComponent)
     -- ^ Was a concrete component specified?
-  , rrAddedDep :: !(Maybe PackageLocationImmutable)
+  , addedDep :: !(Maybe PackageLocationImmutable)
     -- ^ Only if we're adding this as a dependency
-  , rrPackageType :: !PackageType
+  , packageType :: !PackageType
   }
 
 -- | Convert a 'RawTarget' into a 'ResolveResult' (see description on the
@@ -250,18 +253,19 @@   -> Map PackageName PackageLocation
   -> (RawInput, RawTarget)
   -> RIO env (Either StyleDoc ResolveResult)
-resolveRawTarget sma allLocs (ri, rt) =
+resolveRawTarget sma allLocs (rawInput, rt) =
   go rt
  where
-  locals = smaProject sma
-  deps = smaDeps sma
-  globals = smaGlobal sma
+  locals = sma.project
+  deps = sma.deps
+  globals = sma.globals
   -- Helper function: check if a 'NamedComponent' matches the given
   -- 'ComponentName'
   isCompNamed :: ComponentName -> NamedComponent -> Bool
   isCompNamed _ CLib = False
-  isCompNamed t1 (CInternalLib t2) = t1 == t2
+  isCompNamed t1 (CSubLib t2) = t1 == t2
   isCompNamed t1 (CExe t2) = t1 == t2
+  isCompNamed t1 (CFlib t2) = t1 == t2
   isCompNamed t1 (CTest t2) = t1 == t2
   isCompNamed t1 (CBench t2) = t1 == t2
 
@@ -280,12 +284,12 @@           , style Shell $ flow "stack ide targets"
           , flow "for a list of available targets."
           ]
-      [(name, comp)] -> Right ResolveResult
-        { rrName = name
-        , rrRaw = ri
-        , rrComponent = Just comp
-        , rrAddedDep = Nothing
-        , rrPackageType = PTProject
+      [(name, component)] -> Right ResolveResult
+        { name
+        , rawInput
+        , component = Just component
+        , addedDep = Nothing
+        , packageType = PTProject
         }
       matches -> Left $
            fillSep
@@ -302,7 +306,7 @@                          PkgComponent
                          (fromString $ T.unpack $ renderComponent nc)
                      , flow "of package"
-                     , style PkgComponent (fromString $ packageNameString pn)
+                     , style PkgComponent (fromPackageName pn)
                      ]
                  )
                  matches
@@ -313,48 +317,50 @@       Nothing -> pure $ Left $
         fillSep
           [ flow "Unknown local package:"
-          , style Target (fromString $ packageNameString name) <> "."
+          , style Target (fromPackageName name) <> "."
           ]
       Just pp -> do
         comps <- ppComponents pp
         pure $ case ucomp of
-          ResolvedComponent comp
-            | comp `Set.member` comps -> Right ResolveResult
-                { rrName = name
-                , rrRaw = ri
-                , rrComponent = Just comp
-                , rrAddedDep = Nothing
-                , rrPackageType = PTProject
+          ResolvedComponent component
+            | component `Set.member` comps -> Right ResolveResult
+                { name
+                , rawInput
+                , component = Just component
+                , addedDep = Nothing
+                , packageType = PTProject
                 }
             | otherwise -> Left $
                 fillSep
                   [ "Component"
-                  , style Target (fromString $ T.unpack $ renderComponent comp)
+                  , style
+                      Target
+                      (fromString $ T.unpack $ renderComponent component)
                   , flow "does not exist in package"
-                  , style Target (fromString $ packageNameString name) <> "."
+                  , style Target (fromPackageName name) <> "."
                   ]
-          UnresolvedComponent comp ->
-            case filter (isCompNamed comp) $ Set.toList comps of
+          UnresolvedComponent comp' ->
+            case filter (isCompNamed comp') $ Set.toList comps of
               [] -> Left $
                 fillSep
                   [ "Component"
-                  , style Target (fromString $ T.unpack comp)
+                  , style Target (fromString $ T.unpack comp')
                   , flow "does not exist in package"
-                  , style Target (fromString $ packageNameString name) <> "."
+                  , style Target (fromPackageName name) <> "."
                   ]
-              [x] -> Right ResolveResult
-                { rrName = name
-                , rrRaw = ri
-                , rrComponent = Just x
-                , rrAddedDep = Nothing
-                , rrPackageType = PTProject
+              [component] -> Right ResolveResult
+                { name
+                , rawInput
+                , component = Just component
+                , addedDep = Nothing
+                , packageType = PTProject
                 }
               matches -> Left $
                 fillSep
                   [ flow "Ambiguous component name"
-                  , style Target (fromString $ T.unpack comp)
+                  , style Target (fromString $ T.unpack comp')
                   , flow "for package"
-                  , style Target (fromString $ packageNameString name)
+                  , style Target (fromPackageName name)
                   , flow "matches components:"
                   , fillSep $
                       mkNarrativeList (Just PkgComponent) False
@@ -366,11 +372,11 @@ 
   go (RTPackage name)
     | Map.member name locals = pure $ Right ResolveResult
-        { rrName = name
-        , rrRaw = ri
-        , rrComponent = Nothing
-        , rrAddedDep = Nothing
-        , rrPackageType = PTProject
+        { name
+        , rawInput
+        , component = Nothing
+        , addedDep = Nothing
+        , packageType = PTProject
         }
     | Map.member name deps =
         pure $ deferToConstructPlan name
@@ -387,7 +393,7 @@   go (RTPackageIdentifier ident@(PackageIdentifier name version))
     | Map.member name locals = pure $ Left $
         fillSep
-          [ style Target (fromString $ packageNameString name)
+          [ style Target (fromPackageName name)
           , flow "target has a specific version number, but it is a local \
                  \package. To avoid confusion, we will not install the \
                  \specified version or build the local one. To build the \
@@ -413,7 +419,7 @@             fillSep
               [ flow "Package with identifier was targeted on the command \
                      \line:"
-              , style Target (fromString $ packageIdentifierString ident) <> ","
+              , style Target (fromPackageId ident) <> ","
               , flow "but it was specified from a non-index location:"
               , flow $ T.unpack $ textDisplay loc' <> "."
               , flow "Recommendation: add the correctly desired version to \
@@ -425,12 +431,12 @@             pure $ case mrev of
               Nothing -> deferToConstructPlan name
               Just (_rev, cfKey, treeKey) -> Right ResolveResult
-                { rrName = name
-                , rrRaw = ri
-                , rrComponent = Nothing
-                , rrAddedDep = Just $
+                { name
+                , rawInput
+                , component = Nothing
+                , addedDep = Just $
                     PLIHackage (PackageIdentifier name version) cfKey treeKey
-                , rrPackageType = PTDependency
+                , packageType = PTDependency
                 }
 
   hackageLatest name = do
@@ -440,11 +446,11 @@       Nothing -> deferToConstructPlan name
       Just loc ->
         Right ResolveResult
-          { rrName = name
-          , rrRaw = ri
-          , rrComponent = Nothing
-          , rrAddedDep = Just loc
-          , rrPackageType = PTDependency
+          { name
+          , rawInput
+          , component = Nothing
+          , addedDep = Just loc
+          , packageType = PTDependency
           }
 
   hackageLatestRevision name version = do
@@ -452,12 +458,12 @@     pure $ case mrev of
       Nothing -> deferToConstructPlan name
       Just (_rev, cfKey, treeKey) -> Right ResolveResult
-        { rrName = name
-        , rrRaw = ri
-        , rrComponent = Nothing
-        , rrAddedDep =
+        { name
+        , rawInput
+        , component = Nothing
+        , addedDep =
             Just $ PLIHackage (PackageIdentifier name version) cfKey treeKey
-        , rrPackageType = PTDependency
+        , packageType = PTDependency
         }
 
   -- This is actually an error case. We _could_ pure a Left value here, but it
@@ -465,11 +471,11 @@   -- it complain about the missing package so that we get more errors together,
   -- plus the fancy colored output from that module.
   deferToConstructPlan name = Right ResolveResult
-    { rrName = name
-    , rrRaw = ri
-    , rrComponent = Nothing
-    , rrAddedDep = Nothing
-    , rrPackageType = PTDependency
+    { name
+    , rawInput
+    , component = Nothing
+    , addedDep = Nothing
+    , packageType = PTDependency
     }
 --------------------------------------------------------------------------------
 -- Combine the ResolveResults
@@ -486,30 +492,30 @@        )
 combineResolveResults results = do
   addedDeps <- fmap Map.unions $ forM results $ \result ->
-    case rrAddedDep result of
+    case result.addedDep of
       Nothing -> pure Map.empty
-      Just pl -> pure $ Map.singleton (rrName result) pl
+      Just pl -> pure $ Map.singleton result.name pl
 
   let m0 = Map.unionsWith (++) $
-        map (\rr -> Map.singleton (rrName rr) [rr]) results
+        map (\rr -> Map.singleton rr.name [rr]) results
       (errs, ms) = partitionEithers $ flip map (Map.toList m0) $
         \(name, rrs) ->
-          let mcomps = map rrComponent rrs in
+          let mcomps = map (.component) rrs in
           -- Confirm that there is either exactly 1 with no component, or that
           -- all rrs are components
           case rrs of
             [] -> assert False $
               Left $
                 flow "Somehow got no rrComponent values, that can't happen."
-            [rr] | isNothing (rrComponent rr) ->
-              Right $ Map.singleton name $ TargetAll $ rrPackageType rr
+            [rr] | isNothing rr.component ->
+              Right $ Map.singleton name $ TargetAll rr.packageType
             _
               | all isJust mcomps ->
                   Right $ Map.singleton name $ TargetComps $ Set.fromList $
                     catMaybes mcomps
               | otherwise -> Left $ fillSep
                   [ flow "The package"
-                  , style Target $ fromString $ packageNameString name
+                  , style Target $ fromPackageName name
                   , flow "was specified in multiple, incompatible ways:"
                   , fillSep $
                       mkNarrativeList (Just Target) False
@@ -518,7 +524,7 @@   pure (errs, Map.unions ms, addedDeps)
  where
   rrToStyleDoc :: ResolveResult -> StyleDoc
-  rrToStyleDoc = fromString . T.unpack . unRawInput . rrRaw
+  rrToStyleDoc = fromString . T.unpack . (.rawInput.rawInput)
 
 --------------------------------------------------------------------------------
 -- OK, let's do it!
@@ -535,13 +541,13 @@   logDebug "Parsing the targets"
   bconfig <- view buildConfigL
   workingDir <- getCurrentDir
-  locals <- view $ buildConfigL.to (smwProject . bcSMWanted)
+  locals <- view $ buildConfigL . to (.smWanted.project)
   let (textTargets', rawInput) = getRawInput boptscli locals
 
   (errs1, concat -> rawTargets) <- fmap partitionEithers $ forM rawInput $
     parseRawTargetDirs workingDir locals
 
-  let depLocs = Map.map dpLocation $ smaDeps smActual
+  let depLocs = Map.map (.location) smActual.deps
 
   (errs2, resolveResults) <- fmap partitionEithers $ forM rawTargets $
     resolveRawTarget smActual depLocs
@@ -575,12 +581,12 @@   addedDeps' <- mapM (additionalDepPackage haddockDeps . PLImmutable) addedDeps
 
   pure SMTargets
-    { smtTargets = targets
-    , smtDeps = addedDeps'
+    { targets = targets
+    , deps = addedDeps'
     }
  where
   bcImplicitGlobal bconfig =
-    case configProject $ bcConfig bconfig of
+    case bconfig.config.project of
       PCProject _ -> False
       PCGlobalProject -> True
       PCNoProject _ -> False
src/Stack/BuildInfo.hs view
@@ -25,6 +25,7 @@ import           Options.Applicative.Simple ( simpleVersion )
 #endif
 import qualified Paths_stack as Meta
+import           Stack.Constants ( isStackUploadDisabled )
 import           Stack.Prelude
 #ifndef USE_GIT_INFO
 import           Stack.Types.Version ( showStackVersion )
@@ -51,6 +52,7 @@     , ' ' : Cabal.display buildArch
     , depsString
     , warningString
+    , stackUploadDisabledWarningString
     ]
   preReleaseString =
     case versionBranch Meta.version of
@@ -73,6 +75,14 @@     , "official build by running 'stack upgrade --force-download'."
     ]
 #endif
+  stackUploadDisabledWarningString = if isStackUploadDisabled
+    then unlines
+      [ ""
+      , "Warning: 'stack upload' is disabled and will not make HTTP request(s). It will"
+      , "output information about the HTTP request(s) that would have been made if it"
+      , "was enabled."
+      ]
+    else ""
 
 -- | Hpack version we're compiled against
 hpackVersion :: String
+ src/Stack/BuildOpts.hs view
@@ -0,0 +1,77 @@+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+
+-- | Default configuration options for building.
+module Stack.BuildOpts
+  ( defaultBuildOpts
+  , defaultTestOpts
+  , defaultHaddockOpts
+  , defaultBenchmarkOpts
+  ) where
+
+import           Distribution.Verbosity ( normal )
+import           Stack.Prelude
+import           Stack.Types.BuildOpts
+                   ( BenchmarkOpts (..), BuildOpts (..), HaddockOpts (..)
+                   , TestOpts (..)
+                   )
+import           Stack.Types.BuildOptsMonoid
+                   ( BuildOptsMonoid (..), CabalVerbosity (..)
+                   , ProgressBarFormat (..), TestOptsMonoid (..)
+                   )
+
+defaultBuildOpts :: BuildOpts
+defaultBuildOpts = BuildOpts
+  { libProfile = defaultFirstFalse buildMonoid.libProfile
+  , exeProfile = defaultFirstFalse buildMonoid.exeProfile
+  , libStrip = defaultFirstTrue buildMonoid.libStrip
+  , exeStrip = defaultFirstTrue buildMonoid.exeStrip
+  , buildHaddocks = False
+  , haddockOpts = defaultHaddockOpts
+  , openHaddocks = defaultFirstFalse buildMonoid.openHaddocks
+  , haddockDeps = Nothing
+  , haddockInternal = defaultFirstFalse buildMonoid.haddockInternal
+  , haddockHyperlinkSource = defaultFirstTrue buildMonoid.haddockHyperlinkSource
+  , haddockForHackage = defaultFirstFalse buildMonoid.haddockForHackage
+  , installExes = defaultFirstFalse buildMonoid.installExes
+  , installCompilerTool = defaultFirstFalse buildMonoid.installCompilerTool
+  , preFetch = defaultFirstFalse buildMonoid.preFetch
+  , keepGoing = Nothing
+  , keepTmpFiles = defaultFirstFalse buildMonoid.keepTmpFiles
+  , forceDirty = defaultFirstFalse buildMonoid.forceDirty
+  , tests = defaultFirstFalse buildMonoid.tests
+  , testOpts = defaultTestOpts
+  , benchmarks = defaultFirstFalse buildMonoid.benchmarks
+  , benchmarkOpts = defaultBenchmarkOpts
+  , reconfigure = defaultFirstFalse buildMonoid.reconfigure
+  , cabalVerbose = CabalVerbosity normal
+  , splitObjs = defaultFirstFalse buildMonoid.splitObjs
+  , skipComponents = []
+  , interleavedOutput = defaultFirstTrue buildMonoid.interleavedOutput
+  , progressBar = CappedBar
+  , ddumpDir = Nothing
+  }
+ where
+  buildMonoid = undefined :: BuildOptsMonoid
+
+defaultTestOpts :: TestOpts
+defaultTestOpts = TestOpts
+  { rerunTests = defaultFirstTrue toMonoid.rerunTests
+  , additionalArgs = []
+  , coverage = defaultFirstFalse toMonoid.coverage
+  , disableRun = defaultFirstFalse toMonoid.disableRun
+  , maximumTimeSeconds = Nothing
+  , allowStdin = defaultFirstTrue toMonoid.allowStdin
+  }
+ where
+  toMonoid = undefined :: TestOptsMonoid
+
+defaultHaddockOpts :: HaddockOpts
+defaultHaddockOpts = HaddockOpts { additionalArgs = [] }
+
+defaultBenchmarkOpts :: BenchmarkOpts
+defaultBenchmarkOpts = BenchmarkOpts
+  { additionalArgs = Nothing
+  , disableRun = False
+  }
src/Stack/BuildPlan.hs view
@@ -1,25 +1,25 @@-{-# LANGUAGE NoImplicitPrelude   #-}
-{-# LANGUAGE DataKinds           #-}
-{-# LANGUAGE GADTs               #-}
-{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GADTs                 #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
--- | Resolving a build plan for a set of packages in a given Stackage
--- snapshot.
+-- | Resolving a build plan for a set of packages in a given Stackage snapshot.
 
 module Stack.BuildPlan
-    ( BuildPlanException (..)
-    , BuildPlanCheck (..)
-    , checkSnapBuildPlan
-    , DepError (..)
-    , DepErrors
-    , removeSrcPkgDefaultFlags
-    , selectBestSnapshot
-    , showItems
-    ) where
+  ( BuildPlanException (..)
+  , BuildPlanCheck (..)
+  , checkSnapBuildPlan
+  , DepError (..)
+  , DepErrors
+  , removeSrcPkgDefaultFlags
+  , selectBestSnapshot
+  , showItems
+  ) where
 
 import qualified Data.Foldable as F
 import           Data.List (intercalate)
-import qualified Data.List.NonEmpty as NonEmpty
 import qualified Data.Map as Map
 import qualified Data.Set as Set
 import qualified Data.Text as T
@@ -34,10 +34,11 @@ import           Distribution.Types.UnqualComponentName
                    ( unUnqualComponentName )
 import qualified Distribution.Version as C
+import qualified RIO.NonEmpty as NE
 import           Stack.Constants ( wiredInPackages )
 import           Stack.Package
                    ( PackageConfig (..), packageDependencies
-                   , pdpModifiedBuildable, resolvePackageDescription
+                   , resolvePackageDescription
                    )
 import           Stack.Prelude hiding ( Display (..) )
 import           Stack.SourceMap
@@ -59,294 +60,304 @@ -- | Type representing exceptions thrown by functions exported by the
 -- "Stack.BuildPlan" module.
 data BuildPlanException
-    = UnknownPackages
-        (Path Abs File) -- stack.yaml file
-        (Map PackageName (Maybe Version, Set PackageName)) -- truly unknown
-        (Map PackageName (Set PackageIdentifier)) -- shadowed
-    | SnapshotNotFound SnapName
-    | NeitherCompilerOrResolverSpecified T.Text
-    | DuplicatePackagesBug
-    deriving (Show, Typeable)
+  = UnknownPackages
+      (Path Abs File) -- stack.yaml file
+      (Map PackageName (Maybe Version, Set PackageName)) -- truly unknown
+      (Map PackageName (Set PackageIdentifier)) -- shadowed
+  | SnapshotNotFound SnapName
+  | NeitherCompilerOrResolverSpecified T.Text
+  | DuplicatePackagesBug
+  deriving (Show, Typeable)
 
 instance Exception BuildPlanException where
-    displayException (SnapshotNotFound snapName) = unlines
-        [ "Error: [S-2045]"
-        , "SnapshotNotFound " ++ snapName'
-        , "Non existing resolver: " ++ snapName' ++ "."
-        , "For a complete list of available snapshots see https://www.stackage.org/snapshots"
+  displayException (SnapshotNotFound snapName) = unlines
+    [ "Error: [S-2045]"
+    , "SnapshotNotFound " ++ snapName'
+    , "Non existing resolver: " ++ snapName' ++ "."
+    , "For a complete list of available snapshots see https://www.stackage.org/snapshots"
+    ]
+   where
+    snapName' = show snapName
+  displayException (UnknownPackages stackYaml unknown shadowed) =
+    "Error: [S-7571]\n"
+    ++ unlines (unknown' ++ shadowed')
+   where
+    unknown' :: [String]
+    unknown'
+      | Map.null unknown = []
+      | otherwise = concat
+          [ ["The following packages do not exist in the build plan:"]
+          , map go (Map.toList unknown)
+          , case mapMaybe goRecommend $ Map.toList unknown of
+              [] -> []
+              rec ->
+                  ("Recommended action: modify the extra-deps field of " ++
+                  toFilePath stackYaml ++
+                  " to include the following:")
+                  : (rec
+                  ++ ["Note: further dependencies may need to be added"])
+          , case mapMaybe getNoKnown $ Map.toList unknown of
+              [] -> []
+              noKnown ->
+                  [ "There are no known versions of the following packages:"
+                  , intercalate ", " $ map packageNameString noKnown
+                  ]
+          ]
+     where
+      go (dep, (_, users)) | Set.null users = packageNameString dep
+      go (dep, (_, users)) = concat
+        [ packageNameString dep
+        , " (used by "
+        , intercalate ", " $ map packageNameString $ Set.toList users
+        , ")"
         ]
-      where
-        snapName' = show snapName
-    displayException (UnknownPackages stackYaml unknown shadowed) =
-        "Error: [S-7571]\n"
-        ++ unlines (unknown' ++ shadowed')
-      where
-        unknown' :: [String]
-        unknown'
-            | Map.null unknown = []
-            | otherwise = concat
-                [ ["The following packages do not exist in the build plan:"]
-                , map go (Map.toList unknown)
-                , case mapMaybe goRecommend $ Map.toList unknown of
-                    [] -> []
-                    rec ->
-                        ("Recommended action: modify the extra-deps field of " ++
-                        toFilePath stackYaml ++
-                        " to include the following:")
-                        : (rec
-                        ++ ["Note: further dependencies may need to be added"])
-                , case mapMaybe getNoKnown $ Map.toList unknown of
-                    [] -> []
-                    noKnown ->
-                        [ "There are no known versions of the following packages:"
-                        , intercalate ", " $ map packageNameString noKnown
-                        ]
-                ]
-          where
-            go (dep, (_, users)) | Set.null users = packageNameString dep
-            go (dep, (_, users)) = concat
-                [ packageNameString dep
-                , " (used by "
-                , intercalate ", " $ map packageNameString $ Set.toList users
-                , ")"
-                ]
 
-            goRecommend (name, (Just version, _)) =
-                Just $ "- " ++ packageIdentifierString (PackageIdentifier name version)
-            goRecommend (_, (Nothing, _)) = Nothing
-
-            getNoKnown (name, (Nothing, _)) = Just name
-            getNoKnown (_, (Just _, _)) = Nothing
+      goRecommend (name, (Just version, _)) =
+        Just $ "- " ++ packageIdentifierString (PackageIdentifier name version)
+      goRecommend (_, (Nothing, _)) = Nothing
 
-        shadowed' :: [String]
-        shadowed'
-            | Map.null shadowed = []
-            | otherwise = concat
-                [ ["The following packages are shadowed by local packages:"]
-                , map go (Map.toList shadowed)
-                , ["Recommended action: modify the extra-deps field of " ++
-                   toFilePath stackYaml ++
-                   " to include the following:"]
-                , extraDeps
-                , ["Note: further dependencies may need to be added"]
-                ]
-          where
-            go (dep, users) | Set.null users = packageNameString dep ++ " (internal Stack error: this should never be null)"
-            go (dep, users) = concat
-                [ packageNameString dep
-                , " (used by "
-                , intercalate ", "
-                    $ map (packageNameString . pkgName)
-                    $ Set.toList users
-                , ")"
-                ]
+      getNoKnown (name, (Nothing, _)) = Just name
+      getNoKnown (_, (Just _, _)) = Nothing
 
-            extraDeps = map (\ident -> "- " ++ packageIdentifierString ident)
-                      $ Set.toList
-                      $ Set.unions
-                      $ Map.elems shadowed
-    displayException (NeitherCompilerOrResolverSpecified url) = concat
-        [ "Error: [S-8559]\n"
-        , "Failed to load custom snapshot at "
-        , T.unpack url
-        , ", because no 'compiler' or 'resolver' is specified."
+    shadowed' :: [String]
+    shadowed'
+      | Map.null shadowed = []
+      | otherwise = concat
+          [ ["The following packages are shadowed by local packages:"]
+          , map go (Map.toList shadowed)
+          , ["Recommended action: modify the extra-deps field of " ++
+             toFilePath stackYaml ++
+             " to include the following:"]
+          , extraDeps
+          , ["Note: further dependencies may need to be added"]
+          ]
+     where
+      go (dep, users) | Set.null users = packageNameString dep ++ " (internal Stack error: this should never be null)"
+      go (dep, users) = concat
+        [ packageNameString dep
+        , " (used by "
+        , intercalate ", "
+            $ map (packageNameString . pkgName)
+            $ Set.toList users
+        , ")"
         ]
-    displayException DuplicatePackagesBug = bugReport "[S-5743]"
-        "Duplicate packages are not expected here."
 
+      extraDeps = map (\ident -> "- " ++ packageIdentifierString ident)
+                $ Set.toList
+                $ Set.unions
+                $ Map.elems shadowed
+  displayException (NeitherCompilerOrResolverSpecified url) = concat
+    [ "Error: [S-8559]\n"
+    , "Failed to load custom snapshot at "
+    , T.unpack url
+    , ", because no 'compiler' or 'resolver' is specified."
+    ]
+  displayException DuplicatePackagesBug = bugReport "[S-5743]"
+    "Duplicate packages are not expected here."
+
 gpdPackages :: [GenericPackageDescription] -> Map PackageName Version
 gpdPackages = Map.fromList . map (toPair . C.package . C.packageDescription)
-    where
-        toPair (C.PackageIdentifier name version) = (name, version)
+ where
+  toPair (C.PackageIdentifier name version) = (name, version)
 
 gpdPackageDeps ::
-       GenericPackageDescription
-    -> ActualCompiler
-    -> Platform
-    -> Map FlagName Bool
-    -> Map PackageName VersionRange
-gpdPackageDeps gpd ac platform flags =
-    Map.filterWithKey (const . not . isLocalLibrary) (packageDependencies pkgConfig pkgDesc)
-    where
-        isLocalLibrary name' = name' == name || name' `Set.member` subs
+     GenericPackageDescription
+  -> ActualCompiler
+  -> Platform
+  -> Map FlagName Bool
+  -> Map PackageName VersionRange
+gpdPackageDeps gpd compilerVersion platform flags =
+  Map.filterWithKey (const . not . isLocalLibrary) (packageDependencies pkgDesc)
+ where
+  isLocalLibrary name' = name' == name || name' `Set.member` subs
 
-        name = gpdPackageName gpd
-        subs = Set.fromList
-             $ map (C.mkPackageName . unUnqualComponentName . fst)
-             $ C.condSubLibraries gpd
+  name = gpdPackageName gpd
+  subs = Set.fromList
+       $ map (C.mkPackageName . unUnqualComponentName . fst)
+       $ C.condSubLibraries gpd
 
-        -- Since tests and benchmarks are both enabled, doesn't matter
-        -- if we choose modified or unmodified
-        pkgDesc = pdpModifiedBuildable $ resolvePackageDescription pkgConfig gpd
-        pkgConfig = PackageConfig
-            { packageConfigEnableTests = True
-            , packageConfigEnableBenchmarks = True
-            , packageConfigFlags = flags
-            , packageConfigGhcOptions = []
-            , packageConfigCabalConfigOpts = []
-            , packageConfigCompilerVersion = ac
-            , packageConfigPlatform = platform
-            }
+  -- Since tests and benchmarks are both enabled, doesn't matter if we choose
+  -- modified or unmodified
+  pkgDesc = resolvePackageDescription pkgConfig gpd
+  pkgConfig = PackageConfig
+    { enableTests = True
+    , enableBenchmarks = True
+    , flags
+    , ghcOptions = []
+    , cabalConfigOpts = []
+    , compilerVersion
+    , platform
+    }
 
 -- Remove any src package flags having default values
 -- Remove any package entries with no flags set
-removeSrcPkgDefaultFlags :: [C.GenericPackageDescription]
-                         -> Map PackageName (Map FlagName Bool)
-                         -> Map PackageName (Map FlagName Bool)
+removeSrcPkgDefaultFlags ::
+     [C.GenericPackageDescription]
+  -> Map PackageName (Map FlagName Bool)
+  -> Map PackageName (Map FlagName Bool)
 removeSrcPkgDefaultFlags gpds flags =
-    let defaults = Map.unions (map gpdDefaultFlags gpds)
-        flags'   = Map.differenceWith removeSame flags defaults
-    in  Map.filter (not . Map.null) flags'
-    where
-        removeSame f1 f2 =
-            let diff v v' = if v == v' then Nothing else Just v
-            in  Just $ Map.differenceWith diff f1 f2
+  let defaults = Map.unions (map gpdDefaultFlags gpds)
+      flags'   = Map.differenceWith removeSame flags defaults
+  in  Map.filter (not . Map.null) flags'
+ where
+  removeSame f1 f2 =
+    let diff v v' = if v == v' then Nothing else Just v
+    in  Just $ Map.differenceWith diff f1 f2
 
-        gpdDefaultFlags gpd =
-            let tuples = map getDefault (C.genPackageFlags gpd)
-            in  Map.singleton (gpdPackageName gpd) (Map.fromList tuples)
+  gpdDefaultFlags gpd =
+    let tuples = map getDefault (C.genPackageFlags gpd)
+    in  Map.singleton (gpdPackageName gpd) (Map.fromList tuples)
 
-        getDefault f
-            | C.flagDefault f = (C.flagName f, True)
-            | otherwise       = (C.flagName f, False)
+  getDefault f
+    | C.flagDefault f = (C.flagName f, True)
+    | otherwise       = (C.flagName f, False)
 
 -- | Find the set of @FlagName@s necessary to get the given
 -- @GenericPackageDescription@ to compile against the given @BuildPlan@. Will
 -- only modify non-manual flags, and will prefer default values for flags.
 -- Returns the plan which produces least number of dep errors
 selectPackageBuildPlan ::
-       Platform
-    -> ActualCompiler
-    -> Map PackageName Version
-    -> GenericPackageDescription
-    -> (Map PackageName (Map FlagName Bool), DepErrors)
+     Platform
+  -> ActualCompiler
+  -> Map PackageName Version
+  -> GenericPackageDescription
+  -> (Map PackageName (Map FlagName Bool), DepErrors)
 selectPackageBuildPlan platform compiler pool gpd =
-    (selectPlan . limitSearchSpace . NonEmpty.map makePlan) flagCombinations
-  where
-    selectPlan :: NonEmpty (a, DepErrors) -> (a, DepErrors)
-    selectPlan = F.foldr1 fewerErrors
-      where
-        fewerErrors p1 p2
-            | nErrors p1 == 0 = p1
-            | nErrors p1 <= nErrors p2 = p1
-            | otherwise = p2
-          where
-            nErrors = Map.size . snd
-
-    -- Avoid exponential complexity in flag combinations making us sad pandas.
-    -- See: https://github.com/commercialhaskell/stack/issues/543
-    limitSearchSpace :: NonEmpty a -> NonEmpty a
-    limitSearchSpace (x :| xs) = x :| take (maxFlagCombinations - 1) xs
+  (selectPlan . limitSearchSpace . NE.map makePlan) flagCombinations
+ where
+  selectPlan :: NonEmpty (a, DepErrors) -> (a, DepErrors)
+  selectPlan = F.foldr1 fewerErrors
+   where
+    fewerErrors p1 p2
+        | nErrors p1 == 0 = p1
+        | nErrors p1 <= nErrors p2 = p1
+        | otherwise = p2
      where
-      maxFlagCombinations = 128
+      nErrors = Map.size . snd
 
-    makePlan :: [(FlagName, Bool)] -> (Map PackageName (Map FlagName Bool), DepErrors)
-    makePlan flags = checkPackageBuildPlan platform compiler pool (Map.fromList flags) gpd
+  -- Avoid exponential complexity in flag combinations making us sad pandas.
+  -- See: https://github.com/commercialhaskell/stack/issues/543
+  limitSearchSpace :: NonEmpty a -> NonEmpty a
+  limitSearchSpace (x :| xs) = x :| take (maxFlagCombinations - 1) xs
+   where
+    maxFlagCombinations = 128
 
-    flagCombinations :: NonEmpty [(FlagName, Bool)]
-    flagCombinations = mapM getOptions (genPackageFlags gpd)
-      where
-        getOptions :: C.PackageFlag -> NonEmpty (FlagName, Bool)
-        getOptions f
-            | flagManual f = (fname, flagDefault f) :| []
-            | flagDefault f = (fname, True) :| [(fname, False)]
-            | otherwise = (fname, False) :| [(fname, True)]
-          where
-            fname = flagName f
+  makePlan ::
+       [(FlagName, Bool)]
+    -> (Map PackageName (Map FlagName Bool), DepErrors)
+  makePlan flags =
+    checkPackageBuildPlan platform compiler pool (Map.fromList flags) gpd
 
+  flagCombinations :: NonEmpty [(FlagName, Bool)]
+  flagCombinations = mapM getOptions (genPackageFlags gpd)
+   where
+    getOptions :: C.PackageFlag -> NonEmpty (FlagName, Bool)
+    getOptions f
+      | flagManual f = (fname, flagDefault f) :| []
+      | flagDefault f = (fname, True) :| [(fname, False)]
+      | otherwise = (fname, False) :| [(fname, True)]
+     where
+      fname = flagName f
+
 -- | Check whether with the given set of flags a package's dependency
 -- constraints can be satisfied against a given build plan or pool of packages.
 checkPackageBuildPlan ::
-       Platform
-    -> ActualCompiler
-    -> Map PackageName Version
-    -> Map FlagName Bool
-    -> GenericPackageDescription
-    -> (Map PackageName (Map FlagName Bool), DepErrors)
+     Platform
+  -> ActualCompiler
+  -> Map PackageName Version
+  -> Map FlagName Bool
+  -> GenericPackageDescription
+  -> (Map PackageName (Map FlagName Bool), DepErrors)
 checkPackageBuildPlan platform compiler pool flags gpd =
-    (Map.singleton pkg flags, errs)
-    where
-        pkg         = gpdPackageName gpd
-        errs        = checkPackageDeps pkg constraints pool
-        constraints = gpdPackageDeps gpd compiler platform flags
+  (Map.singleton pkg flags, errs)
+ where
+  pkg = gpdPackageName gpd
+  errs = checkPackageDeps pkg constraints pool
+  constraints = gpdPackageDeps gpd compiler platform flags
 
 -- | Checks if the given package dependencies can be satisfied by the given set
 -- of packages. Will fail if a package is either missing or has a version
 -- outside of the version range.
 checkPackageDeps ::
-       PackageName -- ^ package using dependencies, for constructing DepErrors
-    -> Map PackageName VersionRange -- ^ dependency constraints
-    -> Map PackageName Version -- ^ Available package pool or index
-    -> DepErrors
+     PackageName -- ^ package using dependencies, for constructing DepErrors
+  -> Map PackageName VersionRange -- ^ dependency constraints
+  -> Map PackageName Version -- ^ Available package pool or index
+  -> DepErrors
 checkPackageDeps myName deps packages =
-    Map.unionsWith combineDepError $ map go $ Map.toList deps
-  where
-    go :: (PackageName, VersionRange) -> DepErrors
-    go (name, range) =
-        case Map.lookup name packages of
-            Nothing -> Map.singleton name DepError
-                { deVersion = Nothing
-                , deNeededBy = Map.singleton myName range
-                }
-            Just v
-                | withinRange v range -> Map.empty
-                | otherwise -> Map.singleton name DepError
-                    { deVersion = Just v
-                    , deNeededBy = Map.singleton myName range
-                    }
+  Map.unionsWith combineDepError $ map go $ Map.toList deps
+ where
+  go :: (PackageName, VersionRange) -> DepErrors
+  go (name, range) =
+    case Map.lookup name packages of
+      Nothing -> Map.singleton name DepError
+        { version = Nothing
+        , neededBy = Map.singleton myName range
+        }
+      Just v
+        | withinRange v range -> Map.empty
+        | otherwise -> Map.singleton name DepError
+            { version = Just v
+            , neededBy = Map.singleton myName range
+            }
 
 type DepErrors = Map PackageName DepError
+
 data DepError = DepError
-    { deVersion :: !(Maybe Version)
-    , deNeededBy :: !(Map PackageName VersionRange)
-    }
-    deriving Show
+  { version :: !(Maybe Version)
+  , neededBy :: !(Map PackageName VersionRange)
+  }
+  deriving Show
 
 -- | Combine two 'DepError's for the same 'Version'.
 combineDepError :: DepError -> DepError -> DepError
 combineDepError (DepError a x) (DepError b y) =
-    assert (a == b) $ DepError a (Map.unionWith C.intersectVersionRanges x y)
+  assert (a == b) $ DepError a (Map.unionWith C.intersectVersionRanges x y)
 
 -- | Given a bundle of packages (a list of @GenericPackageDescriptions@'s) to
 -- build and an available package pool (snapshot) check whether the bundle's
 -- dependencies can be satisfied. If flags is passed as Nothing flag settings
 -- will be chosen automatically.
 checkBundleBuildPlan ::
-       Platform
-    -> ActualCompiler
-    -> Map PackageName Version
-    -> Maybe (Map PackageName (Map FlagName Bool))
-    -> [GenericPackageDescription]
-    -> (Map PackageName (Map FlagName Bool), DepErrors)
+     Platform
+  -> ActualCompiler
+  -> Map PackageName Version
+  -> Maybe (Map PackageName (Map FlagName Bool))
+  -> [GenericPackageDescription]
+  -> (Map PackageName (Map FlagName Bool), DepErrors)
 checkBundleBuildPlan platform compiler pool flags gpds =
-    (Map.unionsWith dupError (map fst plans)
-    , Map.unionsWith combineDepError (map snd plans))
-
-    where
-        plans = map (pkgPlan flags) gpds
-        pkgPlan Nothing gpd =
-            selectPackageBuildPlan platform compiler pool' gpd
-        pkgPlan (Just f) gpd =
-            checkPackageBuildPlan platform compiler pool' (flags' f gpd) gpd
-        flags' f gpd = fromMaybe Map.empty (Map.lookup (gpdPackageName gpd) f)
-        pool' = Map.union (gpdPackages gpds) pool
+  ( Map.unionsWith dupError (map fst plans)
+  , Map.unionsWith combineDepError (map snd plans)
+  )
+ where
+  plans = map (pkgPlan flags) gpds
+  pkgPlan Nothing gpd =
+    selectPackageBuildPlan platform compiler pool' gpd
+  pkgPlan (Just f) gpd =
+    checkPackageBuildPlan platform compiler pool' (flags' f gpd) gpd
+  flags' f gpd = fromMaybe Map.empty (Map.lookup (gpdPackageName gpd) f)
+  pool' = Map.union (gpdPackages gpds) pool
 
-        dupError _ _ = impureThrow DuplicatePackagesBug
+  dupError _ _ = impureThrow DuplicatePackagesBug
 
-data BuildPlanCheck =
-      BuildPlanCheckOk      (Map PackageName (Map FlagName Bool))
-    | BuildPlanCheckPartial (Map PackageName (Map FlagName Bool)) DepErrors
-    | BuildPlanCheckFail    (Map PackageName (Map FlagName Bool)) DepErrors
-                            ActualCompiler
+data BuildPlanCheck
+  = BuildPlanCheckOk (Map PackageName (Map FlagName Bool))
+  | BuildPlanCheckPartial (Map PackageName (Map FlagName Bool)) DepErrors
+  | BuildPlanCheckFail
+      (Map PackageName (Map FlagName Bool))
+      DepErrors
+      ActualCompiler
 
 -- | Compare 'BuildPlanCheck', where GT means a better plan.
 compareBuildPlanCheck :: BuildPlanCheck -> BuildPlanCheck -> Ordering
-compareBuildPlanCheck (BuildPlanCheckPartial _ e1) (BuildPlanCheckPartial _ e2) =
+compareBuildPlanCheck
+    (BuildPlanCheckPartial _ e1)
+    (BuildPlanCheckPartial _ e2)
+  =
     -- Note: order of comparison flipped, since it's better to have fewer errors.
     compare (Map.size e2) (Map.size e1)
 compareBuildPlanCheck (BuildPlanCheckFail _ e1 _) (BuildPlanCheckFail _ e2 _) =
-    let numUserPkgs e = Map.size $ Map.unions (Map.elems (fmap deNeededBy e))
-    in  compare (numUserPkgs e2) (numUserPkgs e1)
+  let numUserPkgs e = Map.size $ Map.unions (Map.elems (fmap (.neededBy) e))
+  in  compare (numUserPkgs e2) (numUserPkgs e1)
 compareBuildPlanCheck BuildPlanCheckOk{}      BuildPlanCheckOk{}      = EQ
 compareBuildPlanCheck BuildPlanCheckOk{}      BuildPlanCheckPartial{} = GT
 compareBuildPlanCheck BuildPlanCheckOk{}      BuildPlanCheckFail{}    = GT
@@ -354,183 +365,181 @@ compareBuildPlanCheck _                       _                       = LT
 
 instance Show BuildPlanCheck where
-    show BuildPlanCheckOk {} = ""
-    show (BuildPlanCheckPartial f e)  = T.unpack $ showDepErrors f e
-    show (BuildPlanCheckFail f e c) = T.unpack $ showCompilerErrors f e c
+  show BuildPlanCheckOk {} = ""
+  show (BuildPlanCheckPartial f e)  = T.unpack $ showDepErrors f e
+  show (BuildPlanCheckFail f e c) = T.unpack $ showCompilerErrors f e c
 
 -- | Check a set of 'GenericPackageDescription's and a set of flags against a
 -- given snapshot. Returns how well the snapshot satisfies the dependencies of
 -- the packages.
 checkSnapBuildPlan ::
-       (HasConfig env, HasGHCVariant env)
-    => [ResolvedPath Dir]
-    -> Maybe (Map PackageName (Map FlagName Bool))
-    -> SnapshotCandidate env
-    -> RIO env BuildPlanCheck
+     (HasConfig env, HasGHCVariant env)
+  => [ResolvedPath Dir]
+  -> Maybe (Map PackageName (Map FlagName Bool))
+  -> SnapshotCandidate env
+  -> RIO env BuildPlanCheck
 checkSnapBuildPlan pkgDirs flags snapCandidate = do
-    platform <- view platformL
-    sma <- snapCandidate pkgDirs
-    gpds <- liftIO $ forM (Map.elems $ smaProject sma) (cpGPD . ppCommon)
+  platform <- view platformL
+  sma <- snapCandidate pkgDirs
+  gpds <- liftIO $ forM (Map.elems sma.project) (.projectCommon.gpd)
 
-    let
-        compiler = smaCompiler sma
-        globalVersion (GlobalPackageVersion v) = v
-        depVersion dep | PLImmutable loc <- dpLocation dep =
-                           Just $ packageLocationVersion loc
-                       | otherwise =
-                           Nothing
-        snapPkgs = Map.union
-          (Map.mapMaybe depVersion $ smaDeps sma)
-          (Map.map globalVersion $ smaGlobal sma)
-        (f, errs) = checkBundleBuildPlan platform compiler snapPkgs flags gpds
-        cerrs = compilerErrors compiler errs
+  let compiler = sma.compiler
+      globalVersion (GlobalPackageVersion v) = v
+      depVersion dep
+        | PLImmutable loc <- dep.location = Just $ packageLocationVersion loc
+        | otherwise = Nothing
+      snapPkgs = Map.union
+        (Map.mapMaybe depVersion sma.deps)
+        (Map.map globalVersion sma.globals)
+      (f, errs) = checkBundleBuildPlan platform compiler snapPkgs flags gpds
+      cerrs = compilerErrors compiler errs
 
-    if Map.null errs then
-        pure $ BuildPlanCheckOk f
-    else if Map.null cerrs then
-            pure $ BuildPlanCheckPartial f errs
-        else
-            pure $ BuildPlanCheckFail f cerrs compiler
-    where
-        compilerErrors compiler errs
-            | whichCompiler compiler == Ghc = ghcErrors errs
-            | otherwise = Map.empty
+  if Map.null errs
+    then pure $ BuildPlanCheckOk f
+    else if Map.null cerrs
+      then pure $ BuildPlanCheckPartial f errs
+      else pure $ BuildPlanCheckFail f cerrs compiler
+ where
+  compilerErrors compiler errs
+    | whichCompiler compiler == Ghc = ghcErrors errs
+    | otherwise = Map.empty
 
-        isGhcWiredIn p _ = p `Set.member` wiredInPackages
-        ghcErrors = Map.filterWithKey isGhcWiredIn
+  isGhcWiredIn p _ = p `Set.member` wiredInPackages
+  ghcErrors = Map.filterWithKey isGhcWiredIn
 
 -- | Find a snapshot and set of flags that is compatible with and matches as
 -- best as possible with the given 'GenericPackageDescription's.
 selectBestSnapshot ::
-       (HasConfig env, HasGHCVariant env)
-    => [ResolvedPath Dir]
-    -> NonEmpty SnapName
-    -> RIO env (SnapshotCandidate env, RawSnapshotLocation, BuildPlanCheck)
+     (HasConfig env, HasGHCVariant env)
+  => [ResolvedPath Dir]
+  -> NonEmpty SnapName
+  -> RIO env (SnapshotCandidate env, RawSnapshotLocation, BuildPlanCheck)
 selectBestSnapshot pkgDirs snaps = do
-    prettyInfo $
-           fillSep
-             [ flow "Selecting the best among"
-             , fromString $ show (NonEmpty.length snaps)
-             , "snapshots..."
-             ]
-        <> line
-    F.foldr1 go (NonEmpty.map (getResult <=< snapshotLocation) snaps)
-    where
-        go mold mnew = do
-            old@(_snap, _loc, bpc) <- mold
-            case bpc of
-                BuildPlanCheckOk {} -> pure old
-                _ -> fmap (betterSnap old) mnew
+  prettyInfo $
+       fillSep
+         [ flow "Selecting the best among"
+         , fromString $ show (NE.length snaps)
+         , "snapshots..."
+         ]
+    <> line
+  F.foldr1 go (NE.map (getResult <=< snapshotLocation) snaps)
+ where
+  go mold mnew = do
+    old@(_snap, _loc, bpc) <- mold
+    case bpc of
+      BuildPlanCheckOk {} -> pure old
+      _ -> fmap (betterSnap old) mnew
 
-        getResult loc = do
-            candidate <- loadProjectSnapshotCandidate loc NoPrintWarnings False
-            result <- checkSnapBuildPlan pkgDirs Nothing candidate
-            reportResult result loc
-            pure (candidate, loc, result)
+  getResult loc = do
+    candidate <- loadProjectSnapshotCandidate loc NoPrintWarnings False
+    result <- checkSnapBuildPlan pkgDirs Nothing candidate
+    reportResult result loc
+    pure (candidate, loc, result)
 
-        betterSnap (s1, l1, r1) (s2, l2, r2)
-          | compareBuildPlanCheck r1 r2 /= LT = (s1, l1, r1)
-          | otherwise = (s2, l2, r2)
+  betterSnap (s1, l1, r1) (s2, l2, r2)
+    | compareBuildPlanCheck r1 r2 /= LT = (s1, l1, r1)
+    | otherwise = (s2, l2, r2)
 
-        reportResult BuildPlanCheckOk {} loc =
-            prettyNote $
-                   fillSep
-                      [ flow "Matches"
-                      , pretty $ PrettyRawSnapshotLocation loc
-                      ]
-                <> line
+  reportResult BuildPlanCheckOk {} loc =
+    prettyNote $
+         fillSep
+           [ flow "Matches"
+           , pretty $ PrettyRawSnapshotLocation loc
+           ]
+      <> line
 
-        reportResult r@BuildPlanCheckPartial {} loc =
-            prettyWarn $
-                   fillSep
-                     [ flow "Partially matches"
-                     , pretty $ PrettyRawSnapshotLocation loc
-                     ]
-                 <> blankLine
-                 <> indent 4 (string (show r))
+  reportResult r@BuildPlanCheckPartial {} loc =
+    prettyWarn $
+         fillSep
+           [ flow "Partially matches"
+           , pretty $ PrettyRawSnapshotLocation loc
+           ]
+      <> blankLine
+      <> indent 4 (string (show r))
 
-        reportResult r@BuildPlanCheckFail {} loc =
-            prettyWarn $
-                   fillSep
-                     [ flow "Rejected"
-                     , pretty $ PrettyRawSnapshotLocation loc
-                     ]
-                <> blankLine
-                <> indent 4 (string (show r))
+  reportResult r@BuildPlanCheckFail {} loc =
+    prettyWarn $
+         fillSep
+           [ flow "Rejected"
+           , pretty $ PrettyRawSnapshotLocation loc
+           ]
+      <> blankLine
+      <> indent 4 (string (show r))
 
 showItems :: [String] -> Text
 showItems items = T.concat (map formatItem items)
-    where
-        formatItem item = T.concat
-            [ "    - "
-            , T.pack item
-            , "\n"
-            ]
+ where
+  formatItem item = T.concat
+    [ "    - "
+    , T.pack item
+    , "\n"
+    ]
 
 showPackageFlags :: PackageName -> Map FlagName Bool -> Text
 showPackageFlags pkg fl =
-    if not $ Map.null fl then
-        T.concat
-            [ "    - "
-            , T.pack $ packageNameString pkg
-            , ": "
-            , T.pack $ intercalate ", "
-                     $ map formatFlags (Map.toList fl)
-            , "\n"
-            ]
+  if not $ Map.null fl
+    then
+      T.concat
+        [ "    - "
+        , T.pack $ packageNameString pkg
+        , ": "
+        , T.pack $ intercalate ", "
+                 $ map formatFlags (Map.toList fl)
+        , "\n"
+        ]
     else ""
-    where
-        formatFlags (f, v) = show f ++ " = " ++ show v
+ where
+  formatFlags (f, v) = show f ++ " = " ++ show v
 
 showMapPackages :: Map PackageName a -> Text
 showMapPackages mp = showItems $ map packageNameString $ Map.keys mp
 
 showCompilerErrors ::
-       Map PackageName (Map FlagName Bool)
-    -> DepErrors
-    -> ActualCompiler
-    -> Text
+     Map PackageName (Map FlagName Bool)
+  -> DepErrors
+  -> ActualCompiler
+  -> Text
 showCompilerErrors flags errs compiler =
-    T.concat
-        [ compilerVersionText compiler
-        , " cannot be used for these packages:\n"
-        , showMapPackages $ Map.unions (Map.elems (fmap deNeededBy errs))
-        , showDepErrors flags errs -- TODO only in debug mode
-        ]
+  T.concat
+    [ compilerVersionText compiler
+    , " cannot be used for these packages:\n"
+    , showMapPackages $ Map.unions (Map.elems (fmap (.neededBy) errs))
+    , showDepErrors flags errs -- TODO only in debug mode
+    ]
 
 showDepErrors :: Map PackageName (Map FlagName Bool) -> DepErrors -> Text
 showDepErrors flags errs =
-    T.concat
-        [ T.concat $ map formatError (Map.toList errs)
-        , if T.null flagVals then ""
-          else "Using package flags:\n" <> flagVals
-        ]
-    where
-        formatError (depName, DepError mversion neededBy) = T.concat
-            [ showDepVersion depName mversion
-            , T.concat (map showRequirement (Map.toList neededBy))
-            ]
+  T.concat
+    [ T.concat $ map formatError (Map.toList errs)
+    , if T.null flagVals then ""
+      else "Using package flags:\n" <> flagVals
+    ]
+ where
+  formatError (depName, DepError mversion neededBy) = T.concat
+    [ showDepVersion depName mversion
+    , T.concat (map showRequirement (Map.toList neededBy))
+    ]
 
-        showDepVersion depName mversion = T.concat
-            [ T.pack $ packageNameString depName
-            , case mversion of
-                Nothing -> " not found"
-                Just version -> T.concat
-                    [ " version "
-                    , T.pack $ versionString version
-                    , " found"
-                    ]
-            , "\n"
+  showDepVersion depName mversion = T.concat
+    [ T.pack $ packageNameString depName
+    , case mversion of
+        Nothing -> " not found"
+        Just version -> T.concat
+            [ " version "
+            , T.pack $ versionString version
+            , " found"
             ]
+    , "\n"
+    ]
 
-        showRequirement (user, range) = T.concat
-            [ "    - "
-            , T.pack $ packageNameString user
-            , " requires "
-            , T.pack $ display range
-            , "\n"
-            ]
+  showRequirement (user, range) = T.concat
+    [ "    - "
+    , T.pack $ packageNameString user
+    , " requires "
+    , T.pack $ display range
+    , "\n"
+    ]
 
-        flagVals = T.concat (map showFlags userPkgs)
-        userPkgs = Map.keys $ Map.unions (Map.elems (fmap deNeededBy errs))
-        showFlags pkg = maybe "" (showPackageFlags pkg) (Map.lookup pkg flags)
+  flagVals = T.concat (map showFlags userPkgs)
+  userPkgs = Map.keys $ Map.unions (Map.elems (fmap (.neededBy) errs))
+  showFlags pkg = maybe "" (showPackageFlags pkg) (Map.lookup pkg flags)
src/Stack/CLI.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 
 module Stack.CLI
   ( commandLineHandler
@@ -7,15 +8,18 @@ import           Data.Attoparsec.Interpreter ( getInterpreterArgs )
 import           Data.Char ( toLower )
 import qualified Data.List as L
+import           Data.List.NonEmpty ( prependList )
 import           Options.Applicative
                    ( Parser, ParserFailure, ParserHelp, ParserResult (..), flag, switch
                    , handleParseResult, help, helpError, idm, long, metavar
                    , overFailure, renderFailure, strArgument, switch )
 import           Options.Applicative.Help ( errorHelp, stringChunk, vcatChunks )
 import           Options.Applicative.Builder.Extra
-                   ( boolFlags, extraHelpOption, textOption )
+                   ( boolFlags, extraHelpOption )
 import           Options.Applicative.Complicated
                    ( addCommand, addSubCommands, complicatedOptions )
+import           RIO.NonEmpty ( (<|) )
+import qualified RIO.NonEmpty as NE
 import qualified RIO.Process ( exec )
 import           RIO.Process ( withProcessContextNoLogging )
 import           Stack.Build ( buildCmd )
@@ -27,7 +31,7 @@ import           Stack.Docker
                    ( dockerCmdName, dockerHelpOptName, dockerPullCmdName )
 import           Stack.DockerCmd ( dockerPullCmd, dockerResetCmd )
-import qualified Stack.Dot ( dot )
+import           Stack.Dot ( dotCmd )
 import           Stack.Exec ( SpecialExecCmd (..), execCmd )
 import           Stack.Eval ( evalCmd )
 import           Stack.Ghci ( ghciCmd )
@@ -56,6 +60,7 @@ import           Stack.Options.SDistParser ( sdistOptsParser )
 import           Stack.Options.ScriptParser ( scriptOptsParser )
 import           Stack.Options.SetupParser ( setupOptsParser )
+import           Stack.Options.UnpackParser ( unpackOptsParser )
 import           Stack.Options.UpgradeParser ( upgradeOptsParser )
 import           Stack.Options.UploadParser ( uploadOptsParser )
 import           Stack.Options.Utils ( GlobalOptsContext (..) )
@@ -69,7 +74,7 @@ import           Stack.SetupCmd ( setupCmd )
 import           Stack.Templates ( templatesCmd )
 import           Stack.Types.AddCommand ( AddCommand )
-import           Stack.Types.BuildOpts ( BuildCommand (..) )
+import           Stack.Types.BuildOptsCLI ( BuildCommand (..) )
 import           Stack.Types.GlobalOptsMonoid ( GlobalOptsMonoid (..) )
 import           Stack.Types.Runner ( Runner )
 import           Stack.Types.Version ( stackVersion )
@@ -82,6 +87,18 @@ import           System.Environment ( getProgName, withArgs )
 import           System.FilePath ( pathSeparator, takeDirectory )
 
+-- | Type representing \'pretty\' exceptions thrown by functions in the
+-- "Stack.CLI" module.
+data CliPrettyException
+  = NoArgumentsBug
+  deriving (Show, Typeable)
+
+instance Pretty CliPrettyException where
+  pretty NoArgumentsBug = bugPrettyReport "[S-4639]" $
+    flow "commandLineHandler: no command line arguments on event of failure."
+
+instance Exception CliPrettyException
+
 -- | Stack's command line handler.
 commandLineHandler ::
      FilePath
@@ -108,14 +125,20 @@   defaultGlobalOpts = if isInterpreter
     then
       -- Silent except when errors occur - see #2879
-      mempty { globalMonoidLogLevel = First (Just LevelError) }
+      mempty { logLevel = First (Just LevelError) }
     else mempty
   failureCallback f args =
     case L.stripPrefix "Invalid argument" (fst (renderFailure f "")) of
-      Just _ -> if isInterpreter
-                  then parseResultHandler args f
-                  else secondaryCommandHandler args f
-                      >>= interpreterHandler currentDir args
+      Just _ -> maybe
+        (prettyThrowIO NoArgumentsBug)
+        ( \args' -> if isInterpreter
+            then
+              parseResultHandler (NE.toList args') f
+            else
+              secondaryCommandHandler args' f
+                >>= interpreterHandler currentDir args'
+        )
+        (NE.nonEmpty args)
       Nothing -> parseResultHandler args f
 
   parseResultHandler args f =
@@ -230,7 +253,7 @@   dot = addCommand'
     "dot"
     "Visualize your project's dependency graph using Graphviz dot."
-    Stack.Dot.dot
+    dotCmd
     (dotOptsParser False) -- Default for --external is False.
 
   eval = addCommand'
@@ -431,12 +454,7 @@     "Run a Stack script."
     globalFooter
     scriptCmd
-    ( \so gom ->
-        gom
-          { globalMonoidResolverRoot =
-              First $ Just $ takeDirectory $ soFile so
-          }
-    )
+    (\so gom -> gom { resolverRoot = First $ Just $ takeDirectory so.file })
     (globalOpts OtherCmdGlobalOpts)
     scriptOptsParser
 
@@ -475,16 +493,9 @@ 
   unpack = addCommand'
     "unpack"
-    "Unpack one or more packages locally."
+    "Unpack one or more packages, or one or more package candidates, locally."
     unpackCmd
-    ( (,)
-        <$> some (strArgument $ metavar "PACKAGE")
-        <*> optional (textOption
-              (  long "to"
-              <> help "Optional path to unpack the package into (will \
-                      \unpack into subdirectory)."
-              ))
-    )
+    unpackOptsParser
 
   update = addCommand'
     "update"
@@ -511,7 +522,8 @@ 
   upload = addCommand'
     "upload"
-    "Upload a package to Hackage."
+    "Upload one or more packages, or documentation for one or more packages, \
+    \to Hackage."
     uploadCmd
     uploadOptsParser
 
@@ -612,12 +624,12 @@ -- (i.e. `stack something` looks for `stack-something` before
 -- failing with "Invalid argument `something'")
 secondaryCommandHandler ::
-     [String]
+     NonEmpty String
   -> ParserFailure ParserHelp
   -> IO (ParserFailure ParserHelp)
 secondaryCommandHandler args f =
   -- don't even try when the argument looks like a path or flag
-  if elem pathSeparator cmd || "-" `L.isPrefixOf` L.head args
+  if elem pathSeparator cmd || "-" `L.isPrefixOf` NE.head args
      then pure f
   else do
     mExternalExec <- D.findExecutable cmd
@@ -626,20 +638,20 @@         -- TODO show the command in verbose mode
         -- hPutStrLn stderr $ unwords $
         --   ["Running", "[" ++ ex, unwords (tail args) ++ "]"]
-        _ <- RIO.Process.exec ex (L.tail args)
+        _ <- RIO.Process.exec ex (NE.tail args)
         pure f
       Nothing -> pure $ fmap (vcatErrorHelp (noSuchCmd cmd)) f
  where
   -- FIXME this is broken when any options are specified before the command
   -- e.g. stack --verbosity silent cmd
-  cmd = stackProgName ++ "-" ++ L.head args
+  cmd = stackProgName <> "-" <> NE.head args
   noSuchCmd name = errorHelp $ stringChunk
     ("Auxiliary command not found in path '" ++ name ++ "'.")
 
 interpreterHandler ::
      Monoid t
   => FilePath
-  -> [String]
+  -> NonEmpty String
   -> ParserFailure ParserHelp
   -> IO (GlobalOptsMonoid, (RIO Runner (), t))
 interpreterHandler currentDir args f = do
@@ -652,25 +664,26 @@     (file:fileArgs') -> runInterpreterCommand file stackArgs fileArgs'
     [] -> parseResultHandler (errorCombine (noSuchFile firstArg))
  where
-  firstArg = L.head args
+  firstArg = NE.head args
 
-  spanM _ [] = pure ([], [])
-  spanM p xs@(x:xs') = do
+  spanM p xs@(x :| rest) = do
     r <- p x
     if r
-    then do
-      (ys, zs) <- spanM p xs'
-      pure (x:ys, zs)
-    else
-      pure ([], xs)
+      then case rest of
+        [] -> pure ([x], [])
+        (x': rest') -> do
+          (ys, zs) <- spanM p (x' :| rest')
+          pure (x : ys, zs)
+      else
+        pure ([], NE.toList xs)
 
   -- if the first argument contains a path separator then it might be a file,
   -- or a Stack option referencing a file. In that case we only show the
   -- interpreter error message and exclude the command related error messages.
   errorCombine =
     if pathSeparator `elem` firstArg
-    then overrideErrorHelp
-    else vcatErrorHelp
+      then overrideErrorHelp
+      else vcatErrorHelp
 
   overrideErrorHelp h1 h2 = h2 { helpError = helpError h1 }
 
@@ -684,14 +697,14 @@     let parseCmdLine = commandLineHandler currentDir progName True
         -- Implicit file arguments are put before other arguments that
         -- occur after "--". See #3658
-        cmdArgs = stackArgs ++ case break (== "--") iargs of
-          (beforeSep, []) -> beforeSep ++ ["--"] ++ [path] ++ fileArgs
+        cmdArgs = prependList stackArgs $ case NE.break (== "--") iargs of
+          (beforeSep, []) -> prependList beforeSep $ "--" <| path :| fileArgs
           (beforeSep, optSep : afterSep) ->
-            beforeSep ++ [optSep] ++ [path] ++ fileArgs ++ afterSep
+            prependList beforeSep $ optSep <| path :| fileArgs <> afterSep
      -- TODO show the command in verbose mode
      -- hPutStrLn stderr $ unwords $
      --   ["Running", "[" ++ progName, unwords cmdArgs ++ "]"]
-    (a,b) <- withArgs cmdArgs parseCmdLine
+    (a,b) <- withArgs (NE.toList cmdArgs) parseCmdLine
     pure (a,(b,mempty))
 
 -- Vertically combine only the error component of the first argument with the
src/Stack/Clean.hs view
@@ -1,5 +1,6 @@-{-# LANGUAGE NoImplicitPrelude  #-}
-{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 -- | Types and functions related to Stack's @clean@ and @purge@ commands.
 module Stack.Clean
@@ -55,8 +56,8 @@ 
 -- | Type representing Stack's cleaning commands.
 data CleanCommand
-    = Clean
-    | Purge
+  = Clean
+  | Purge
 
 -- | Function underlying the @stack clean@ command.
 cleanCmd :: CleanOpts -> RIO Runner ()
@@ -80,7 +81,7 @@ 
 dirsToDelete :: CleanOpts -> RIO BuildConfig [Path Abs Dir]
 dirsToDelete cleanOpts = do
-  packages <- view $ buildConfigL.to (smwProject . bcSMWanted)
+  packages <- view $ buildConfigL . to (.smWanted.project)
   case cleanOpts of
     CleanShallow [] ->
       -- Filter out packages listed as extra-deps
+ src/Stack/Component.hs view
@@ -0,0 +1,219 @@+{-# LANGUAGE NoImplicitPrelude        #-}
+{-# LANGUAGE ConstraintKinds          #-}
+{-# LANGUAGE DataKinds                #-}
+{-# LANGUAGE DisambiguateRecordFields #-}
+{-# LANGUAGE FlexibleContexts         #-}
+{-# LANGUAGE GADTs                    #-}
+{-# LANGUAGE OverloadedRecordDot      #-}
+{-# LANGUAGE OverloadedStrings        #-}
+{-# LANGUAGE ScopedTypeVariables      #-}
+
+-- | All utility functions for Components in Stack (library, internal library,
+-- foreign library, executable, tests, benchmarks). In particular, this module
+-- gathers all the Cabal-to-Stack component translations, which previously
+-- occurred in the "Stack.Package" module. See "Stack.Types.Component" for more
+-- details about the design choices.
+
+module Stack.Component
+  ( isComponentBuildable
+  , stackLibraryFromCabal
+  , stackExecutableFromCabal
+  , stackForeignLibraryFromCabal
+  , stackBenchmarkFromCabal
+  , stackTestFromCabal
+  , foldOnNameAndBuildInfo
+  , stackUnqualToQual
+  , componentDependencyMap
+  , fromCabalName
+  ) where
+
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+import           Data.Text ( pack )
+import           Distribution.PackageDescription
+                   ( Benchmark (..), Executable, ForeignLib, Library (..)
+                   , TestSuite (..)
+                   )
+import           Distribution.Types.BuildInfo ( BuildInfo )
+import           Distribution.Package ( mkPackageName )
+import qualified Distribution.PackageDescription as Cabal
+import           GHC.Records ( HasField )
+import           Stack.Prelude
+import           Stack.Types.Component
+                   ( HasBuildInfo, StackBenchmark (..), StackBuildInfo (..)
+                   , StackExecutable (..), StackForeignLibrary (..)
+                   , StackLibrary (..), StackTestSuite (..)
+                   , StackUnqualCompName (..)
+                   )
+import           Stack.Types.ComponentUtils ( fromCabalName )
+import           Stack.Types.Dependency ( cabalExeToStackDep, cabalToStackDep )
+import           Stack.Types.NamedComponent ( NamedComponent )
+
+stackUnqualToQual ::
+     (Text -> NamedComponent)
+  -> StackUnqualCompName
+  -> NamedComponent
+stackUnqualToQual c (StackUnqualCompName n) = c n
+
+foldOnNameAndBuildInfo ::
+     ( HasField "buildInfo" a StackBuildInfo
+     , HasField "name" a StackUnqualCompName
+     , Foldable c
+     )
+  => c a
+  -> (StackUnqualCompName -> StackBuildInfo -> t -> t)
+  -> t
+  -> t
+foldOnNameAndBuildInfo initialCollection accumulator input =
+  foldr' iterator input initialCollection
+ where
+  iterator comp = accumulator comp.name comp.buildInfo
+
+stackLibraryFromCabal :: Library -> StackLibrary
+stackLibraryFromCabal cabalLib = StackLibrary
+  { name = case cabalLib.libName of
+      LMainLibName -> StackUnqualCompName mempty
+      LSubLibName v -> fromCabalName v
+  , buildInfo = stackBuildInfoFromCabal cabalLib.libBuildInfo
+  , exposedModules = cabalLib.exposedModules
+  }
+
+stackExecutableFromCabal :: Executable -> StackExecutable
+stackExecutableFromCabal cabalExecutable = StackExecutable
+  { name = fromCabalName cabalExecutable.exeName
+  , buildInfo = stackBuildInfoFromCabal cabalExecutable.buildInfo
+  , modulePath = cabalExecutable.modulePath
+  }
+
+stackForeignLibraryFromCabal :: ForeignLib -> StackForeignLibrary
+stackForeignLibraryFromCabal cabalForeignLib = StackForeignLibrary
+  { name = fromCabalName cabalForeignLib.foreignLibName
+  , buildInfo=stackBuildInfoFromCabal cabalForeignLib.foreignLibBuildInfo
+  }
+
+stackBenchmarkFromCabal :: Benchmark -> StackBenchmark
+stackBenchmarkFromCabal cabalBenchmark = StackBenchmark
+  { name = fromCabalName cabalBenchmark.benchmarkName
+  , interface = cabalBenchmark.benchmarkInterface
+  , buildInfo = stackBuildInfoFromCabal cabalBenchmark.benchmarkBuildInfo
+  }
+
+stackTestFromCabal :: TestSuite -> StackTestSuite
+stackTestFromCabal cabalTest = StackTestSuite
+  { name = fromCabalName cabalTest.testName
+  , interface = cabalTest.testInterface
+  , buildInfo = stackBuildInfoFromCabal cabalTest.testBuildInfo
+  }
+
+isComponentBuildable :: HasBuildInfo component => component -> Bool
+isComponentBuildable componentRec = componentRec.buildInfo.buildable
+
+stackBuildInfoFromCabal :: BuildInfo -> StackBuildInfo
+stackBuildInfoFromCabal buildInfoV = gatherComponentToolsAndDepsFromCabal
+  buildInfoV.buildTools
+  buildInfoV.buildToolDepends
+  buildInfoV.targetBuildDepends
+  StackBuildInfo
+    { buildable = buildInfoV.buildable
+    , otherModules = buildInfoV.otherModules
+    , jsSources = buildInfoV.jsSources
+    , hsSourceDirs = buildInfoV.hsSourceDirs
+    , cSources = buildInfoV.cSources
+    , dependency = mempty
+    , unknownTools = mempty
+    , cppOptions = buildInfoV.cppOptions
+    , targetBuildDepends = buildInfoV.targetBuildDepends
+    , options = buildInfoV.options
+    , allLanguages = Cabal.allLanguages buildInfoV
+    , usedExtensions = Cabal.usedExtensions buildInfoV
+    , includeDirs = buildInfoV.includeDirs
+    , extraLibs = buildInfoV.extraLibs
+    , extraLibDirs = buildInfoV.extraLibDirs
+    , frameworks = buildInfoV.frameworks
+    }
+
+-- | Iterate on all three dependency list given, and transform and sort them
+-- between 'sbiUnknownTools' and legitimate 'DepValue' sbiDependency. Bear in
+-- mind that this only gathers the component level dependencies.
+gatherComponentToolsAndDepsFromCabal
+  :: [Cabal.LegacyExeDependency]
+     -- ^ Legacy build tools dependency from
+     -- 'Distribution.Types.BuildInfo.buildTools'.
+  -> [Cabal.ExeDependency]
+     -- ^ Build tools dependency from
+     -- `Distribution.Types.BuildInfo.buildToolDepends'.
+  -> [Cabal.Dependency]
+     -- ^ Cabal-syntax defines
+     -- 'Distribution.Types.BuildInfo.targetBuildDepends'. These are the
+     -- simplest dependencies for a component extracted from the Cabal file such
+     -- as:
+     -- @
+     --  build-depends:
+     --      foo ^>= 1.2.3.4,
+     --      bar ^>= 1
+     -- @
+  -> StackBuildInfo
+  -> StackBuildInfo
+gatherComponentToolsAndDepsFromCabal legacyBuildTools buildTools targetDeps =
+  gatherTargetDependency . gatherToolsDependency . gatherUnknownTools
+ where
+  gatherUnknownTools sbi = foldl' processLegacyExeDepency sbi legacyBuildTools
+  gatherToolsDependency sbi = foldl' processExeDependency sbi buildTools
+  gatherTargetDependency sbi = foldl' processDependency sbi targetDeps
+  -- This is similar to Cabal's
+  -- 'Distribution.Simple.BuildToolDepends.desugarBuildTool', however it uses
+  -- our own hard-coded map which drops tools shipped with GHC (like hsc2hs),
+  -- and includes some tools from Stackage.
+  processLegacyExeDepency sbi (Cabal.LegacyExeDependency exeName range) =
+    case isKnownLegacyExe exeName of
+      Just pName ->
+        processExeDependency
+          sbi
+          (Cabal.ExeDependency pName (Cabal.mkUnqualComponentName exeName) range)
+      Nothing -> sbi
+        { unknownTools = Set.insert (pack exeName) sbi.unknownTools }
+  processExeDependency sbi exeDep@(Cabal.ExeDependency pName _ _)
+    | isPreInstalledPackages pName = sbi
+    | otherwise = sbi
+        { dependency =
+            Map.insert pName (cabalExeToStackDep exeDep) sbi.dependency
+        }
+  processDependency sbi dep@(Cabal.Dependency pName _ _) = sbi
+    { dependency = Map.insert pName (cabalToStackDep dep) sbi.dependency }
+
+componentDependencyMap ::
+     (HasField "buildInfo" r1 r2, HasField "dependency" r2 a)
+  => r1
+  -> a
+componentDependencyMap component = component.buildInfo.dependency
+
+-- | A hard-coded map for tool dependencies. If a dependency is within this map
+-- it's considered "known" (the exe will be found at the execution stage). The
+-- corresponding Cabal function is
+-- 'Distribution.Simple.BuildToolDepends.desugarBuildTool'.
+isKnownLegacyExe :: String -> Maybe PackageName
+isKnownLegacyExe input = case input of
+  "alex" -> justPck "alex"
+  "happy" -> justPck "happy"
+  "cpphs" -> justPck "cpphs"
+  "greencard" -> justPck "greencard"
+  "c2hs" -> justPck "c2hs"
+  "hscolour" -> justPck "hscolour"
+  "hspec-discover" -> justPck "hspec-discover"
+  "hsx2hs" -> justPck "hsx2hs"
+  "gtk2hsC2hs" -> justPck "gtk2hs-buildtools"
+  "gtk2hsHookGenerator" -> justPck "gtk2hs-buildtools"
+  "gtk2hsTypeGen" -> justPck "gtk2hs-buildtools"
+  _ -> Nothing
+ where
+  justPck = Just . mkPackageName
+
+-- | Executable-only packages which come pre-installed with GHC and do not need
+-- to be built. Without this exception, we would either end up unnecessarily
+-- rebuilding these packages, or failing because the packages do not appear in
+-- the Stackage snapshot.
+isPreInstalledPackages :: PackageName -> Bool
+isPreInstalledPackages input = case input of
+  "hsc2hs" -> True
+  "haddock" -> True
+  _ -> False
src/Stack/ComponentFile.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE NoImplicitPrelude   #-}
 {-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 {-# LANGUAGE OverloadedStrings   #-}
 
 -- | A module which exports all component-level file-gathering logic. It also
@@ -7,15 +8,16 @@ 
 module Stack.ComponentFile
   ( resolveOrWarn
-  , libraryFiles
-  , executableFiles
-  , testFiles
-  , benchmarkFiles
   , componentOutputDir
   , componentBuildDir
   , packageAutogenDir
   , buildDir
   , componentAutogenDir
+  , ComponentFile (..)
+  , stackLibraryFiles
+  , stackExecutableFiles
+  , stackTestSuiteFiles
+  , stackBenchmarkFiles
   ) where
 
 import           Control.Exception ( throw )
@@ -27,13 +29,12 @@ import           Distribution.ModuleName ( ModuleName )
 import qualified Distribution.ModuleName as Cabal
 import           Distribution.PackageDescription
-                   ( Benchmark (..), BenchmarkInterface (..), BuildInfo (..)
-                   , Executable (..), Library (..), TestSuite (..)
-                   , TestSuiteInterface (..)
-                   )
+                   ( BenchmarkInterface (..), TestSuiteInterface (..) )
 import           Distribution.Text ( display )
-import           Distribution.Utils.Path ( getSymbolicPath )
+import           Distribution.Utils.Path
+                   ( PackageDir, SourceDir, SymbolicPath, getSymbolicPath )
 import           Distribution.Version ( mkVersion )
+import           GHC.Records ( HasField )
 import qualified HiFileParser as Iface
 import           Path
                    ( (</>), filename, isProperPrefixOf, parent, parseRelDir
@@ -50,6 +51,11 @@                    , relDirAutogen, relDirBuild, relDirGlobalAutogen
                    )
 import           Stack.Prelude hiding ( Display (..) )
+import           Stack.Types.Component
+                   ( StackBenchmark (..), StackBuildInfo (..)
+                   , StackExecutable (..), StackLibrary (..)
+                   , StackTestSuite (..), StackUnqualCompName (..)
+                   )
 import           Stack.Types.Config
                    ( Config (..), HasConfig (..), prettyStackDevL )
 import           Stack.Types.NamedComponent ( NamedComponent (..) )
@@ -61,84 +67,83 @@ import qualified System.Directory as D ( doesFileExist )
 import qualified System.FilePath as FilePath
 
+data ComponentFile = ComponentFile
+  { moduleFileMap :: !(Map ModuleName (Path Abs File))
+  , otherFile :: ![DotCabalPath]
+  , packageWarning :: ![PackageWarning]
+  }
+
 -- | Get all files referenced by the benchmark.
-benchmarkFiles ::
-     NamedComponent
-  -> Benchmark
-  -> RIO
-       GetPackageFileContext
-       (Map ModuleName (Path Abs File), [DotCabalPath], [PackageWarning])
-benchmarkFiles component bench =
-  resolveComponentFiles component build names
+stackBenchmarkFiles ::
+     StackBenchmark
+  -> RIO GetPackageFileContext (NamedComponent, ComponentFile)
+stackBenchmarkFiles bench =
+  resolveComponentFiles (CBench bench.name.unqualCompToText) build names
  where
   names = bnames <> exposed
   exposed =
-    case benchmarkInterface bench of
+    case bench.interface of
       BenchmarkExeV10 _ fp -> [DotCabalMain fp]
       BenchmarkUnsupported _ -> []
-  bnames = map DotCabalModule (otherModules build)
-  build = benchmarkBuildInfo bench
+  bnames = map DotCabalModule build.otherModules
+  build = bench.buildInfo
 
 -- | Get all files referenced by the test.
-testFiles ::
-     NamedComponent
-  -> TestSuite
-  -> RIO
-       GetPackageFileContext
-       (Map ModuleName (Path Abs File), [DotCabalPath], [PackageWarning])
-testFiles component test =
-  resolveComponentFiles component build names
+stackTestSuiteFiles ::
+     StackTestSuite
+  -> RIO GetPackageFileContext (NamedComponent, ComponentFile)
+stackTestSuiteFiles test =
+  resolveComponentFiles (CTest test.name.unqualCompToText) build names
  where
   names = bnames <> exposed
   exposed =
-    case testInterface test of
+    case test.interface of
       TestSuiteExeV10 _ fp -> [DotCabalMain fp]
       TestSuiteLibV09 _ mn -> [DotCabalModule mn]
       TestSuiteUnsupported _ -> []
-  bnames = map DotCabalModule (otherModules build)
-  build = testBuildInfo test
+  bnames = map DotCabalModule build.otherModules
+  build = test.buildInfo
 
 -- | Get all files referenced by the executable.
-executableFiles ::
-     NamedComponent
-  -> Executable
-  -> RIO
-       GetPackageFileContext
-       (Map ModuleName (Path Abs File), [DotCabalPath], [PackageWarning])
-executableFiles component exe =
-  resolveComponentFiles component build names
+stackExecutableFiles ::
+     StackExecutable
+  -> RIO GetPackageFileContext (NamedComponent, ComponentFile)
+stackExecutableFiles exe =
+  resolveComponentFiles (CExe exe.name.unqualCompToText) build names
  where
-  build = buildInfo exe
+  build = exe.buildInfo
   names =
-    map DotCabalModule (otherModules build) ++
-    [DotCabalMain (modulePath exe)]
+    map DotCabalModule build.otherModules ++ [DotCabalMain exe.modulePath]
 
--- | Get all files referenced by the library.
-libraryFiles ::
-     NamedComponent
-  -> Library
-  -> RIO
-       GetPackageFileContext
-       (Map ModuleName (Path Abs File), [DotCabalPath], [PackageWarning])
-libraryFiles component lib =
-  resolveComponentFiles component build names
+-- | Get all files referenced by the library. Handle all libraries (CLib and
+-- SubLib), based on empty name or not.
+stackLibraryFiles ::
+     StackLibrary
+  -> RIO GetPackageFileContext (NamedComponent, ComponentFile)
+stackLibraryFiles lib =
+  resolveComponentFiles componentName build names
  where
-  build = libBuildInfo lib
+  componentRawName = lib.name.unqualCompToText
+  componentName
+    | componentRawName == mempty = CLib
+    | otherwise = CSubLib componentRawName
+  build = lib.buildInfo
   names = bnames ++ exposed
-  exposed = map DotCabalModule (exposedModules lib)
-  bnames = map DotCabalModule (otherModules build)
+  exposed = map DotCabalModule lib.exposedModules
+  bnames = map DotCabalModule build.otherModules
 
 -- | Get all files referenced by the component.
 resolveComponentFiles ::
-     NamedComponent
-  -> BuildInfo
+     ( CAndJsSources rec
+     , HasField "hsSourceDirs" rec [SymbolicPath PackageDir SourceDir]
+     )
+  => NamedComponent
+  -> rec
   -> [DotCabalDescriptor]
-  -> RIO
-       GetPackageFileContext
-       (Map ModuleName (Path Abs File), [DotCabalPath], [PackageWarning])
+  -> RIO GetPackageFileContext (NamedComponent, ComponentFile)
 resolveComponentFiles component build names = do
-  dirs <- mapMaybeM (resolveDirOrWarn . getSymbolicPath) (hsSourceDirs build)
-  dir <- asks (parent . ctxFile)
+  dirs <- mapMaybeM (resolveDirOrWarn . getSymbolicPath) build.hsSourceDirs
+  dir <- asks (parent . (.file))
   agdirs <- autogenDirs
   (modules,files,warnings) <-
     resolveFilesAndDeps
@@ -146,19 +151,18 @@       ((if null dirs then [dir] else dirs) ++ agdirs)
       names
   cfiles <- buildOtherSources build
-  pure (modules, files <> cfiles, warnings)
+  pure (component, ComponentFile modules (files <> cfiles) warnings)
  where
   autogenDirs = do
-    cabalVer <- asks ctxCabalVer
-    distDir <- asks ctxDistDir
+    cabalVer <- asks (.cabalVer)
+    distDir <- asks (.distDir)
     let compDir = componentAutogenDir cabalVer component distDir
         pkgDir = maybeToList $ packageAutogenDir cabalVer distDir
     filterM doesDirExist $ compDir : pkgDir
 
--- | Try to resolve the list of base names in the given directory by
--- looking for unique instances of base names applied with the given
--- extensions, plus find any of their module and TemplateHaskell
--- dependencies.
+-- | Try to resolve the list of base names in the given directory by looking for
+-- unique instances of base names applied with the given extensions, plus find
+-- any of their module and TemplateHaskell dependencies.
 resolveFilesAndDeps ::
      NamedComponent       -- ^ Package component name
   -> [Path Abs Dir]       -- ^ Directories to look in.
@@ -267,8 +271,8 @@     DotCabalCFilePath{} -> pure (S.empty, M.empty)
  where
   readResolvedHi resolvedFile = do
-    dumpHIDir <- componentOutputDir component <$> asks ctxDistDir
-    dir <- asks (parent . ctxFile)
+    dumpHIDir <- componentOutputDir component <$> asks (.distDir)
+    dir <- asks (parent . (.file))
     let sourceDir = fromMaybe dir $ find (`isProperPrefixOf` resolvedFile) dirs
         stripSourceDir d = stripProperPrefix d resolvedFile
     case stripSourceDir sourceDir of
@@ -291,7 +295,7 @@      -- ^ The path to the *.hi file to be parsed
   -> RIO GetPackageFileContext (Set ModuleName, Map FilePath (Path Abs File))
 parseHI knownUsages hiPath = do
-  dir <- asks (parent . ctxFile)
+  dir <- asks (parent . (.file))
   result <-
     liftIO $ catchAnyDeep
       (Iface.fromFile hiPath)
@@ -332,7 +336,8 @@ componentOutputDir namedComponent distDir =
   case namedComponent of
     CLib -> buildDir distDir
-    CInternalLib name -> makeTmp name
+    CSubLib name -> makeTmp name
+    CFlib name -> makeTmp name
     CExe name -> makeTmp name
     CTest name -> makeTmp name
     CBench name -> makeTmp name
@@ -357,8 +362,8 @@   -> DotCabalDescriptor
   -> RIO GetPackageFileContext (Maybe DotCabalPath)
 findCandidate dirs name = do
-  pkg <- asks ctxFile >>= parsePackageNameFromFilePath
-  customPreprocessorExts <- view $ configL . to configCustomPreprocessorExts
+  pkg <- asks (.file) >>= parsePackageNameFromFilePath
+  customPreprocessorExts <- view $ configL . to (.customPreprocessorExts)
   let haskellPreprocessorExts =
         haskellDefaultPreprocessorExts ++ customPreprocessorExts
   candidates <- liftIO $ makeNameCandidates haskellPreprocessorExts
@@ -390,26 +395,25 @@                     -> IO [Path Abs File]
   makeDirCandidates haskellPreprocessorExts dir =
     case name of
-        DotCabalMain fp -> resolveCandidate dir fp
-        DotCabalFile fp -> resolveCandidate dir fp
-        DotCabalCFile fp -> resolveCandidate dir fp
-        DotCabalModule mn -> do
-          let perExt ext =
-                resolveCandidate
-                  dir (Cabal.toFilePath mn ++ "." ++ T.unpack ext)
-          withHaskellExts <- mapM perExt haskellFileExts
-          withPPExts <- mapM perExt haskellPreprocessorExts
-          pure $
-            case (concat withHaskellExts, concat withPPExts) of
-              -- If we have exactly 1 Haskell extension and exactly
-              -- 1 preprocessor extension, assume the former file is
-              -- generated from the latter
-              --
-              -- See https://github.com/commercialhaskell/stack/issues/4076
-              ([_], [y]) -> [y]
-
-              -- Otherwise, return everything
-              (xs, ys) -> xs ++ ys
+      DotCabalMain fp -> resolveCandidate dir fp
+      DotCabalFile fp -> resolveCandidate dir fp
+      DotCabalCFile fp -> resolveCandidate dir fp
+      DotCabalModule mn -> do
+        let perExt ext =
+              resolveCandidate
+                dir (Cabal.toFilePath mn ++ "." ++ T.unpack ext)
+        withHaskellExts <- mapM perExt haskellFileExts
+        withPPExts <- mapM perExt haskellPreprocessorExts
+        pure $
+          case (concat withHaskellExts, concat withPPExts) of
+            -- If we have exactly 1 Haskell extension and exactly
+            -- 1 preprocessor extension, assume the former file is
+            -- generated from the latter
+            --
+            -- See https://github.com/commercialhaskell/stack/issues/4076
+            ([_], [y]) -> [y]
+            -- Otherwise, return everything
+            (xs, ys) -> xs ++ ys
   resolveCandidate dir = fmap maybeToList . resolveDirFile dir
 
 -- | Log that we couldn't find a candidate, but there are
@@ -445,12 +449,18 @@       )
       dirs
 
+type CAndJsSources rec =
+  (HasField "cSources" rec [FilePath], HasField "jsSources" rec [FilePath])
+
 -- | Get all C sources and extra source files in a build.
-buildOtherSources :: BuildInfo -> RIO GetPackageFileContext [DotCabalPath]
+buildOtherSources ::
+     CAndJsSources rec
+  => rec
+  -> RIO GetPackageFileContext [DotCabalPath]
 buildOtherSources build = do
   cwd <- liftIO getCurrentDir
-  dir <- asks (parent . ctxFile)
-  file <- asks ctxFile
+  dir <- asks (parent . (.file))
+  file <- asks (.file)
   let resolveDirFiles files toCabalPath =
         forMaybeM files $ \fp -> do
           result <- resolveDirFile dir fp
@@ -459,19 +469,17 @@               warnMissingFile "File" cwd fp file
               pure Nothing
             Just p -> pure $ Just (toCabalPath p)
-  csources <- resolveDirFiles (cSources build) DotCabalCFilePath
-  jsources <- resolveDirFiles (targetJsSources build) DotCabalFilePath
+  csources <- resolveDirFiles build.cSources DotCabalCFilePath
+  jsources <- resolveDirFiles build.jsSources DotCabalFilePath
   pure (csources <> jsources)
 
--- | Get the target's JS sources.
-targetJsSources :: BuildInfo -> [FilePath]
-targetJsSources = jsSources
-
 -- | Resolve file as a child of a specified directory, symlinks
 -- don't get followed.
 resolveDirFile ::
      (MonadIO m, MonadThrow m)
-  => Path Abs Dir -> FilePath.FilePath -> m (Maybe (Path Abs File))
+  => Path Abs Dir
+  -> FilePath.FilePath
+  -> m (Maybe (Path Abs File))
 resolveDirFile x y = do
   -- The standard canonicalizePath does not work for this case
   p <- parseCollapsedAbsFile (toFilePath x FilePath.</> y)
@@ -543,7 +551,8 @@ -- component names.
 componentNameToDir :: Text -> Path Rel Dir
 componentNameToDir name =
-  fromMaybe (throw ComponentNotParsedBug) (parseRelDir (T.unpack name))
+  fromMaybe (throw $ ComponentNotParsedBug sName) (parseRelDir sName)
+  where sName = T.unpack name
 
 -- | See 'Distribution.Simple.LocalBuildInfo.componentBuildDir'
 componentBuildDir :: Version -> NamedComponent -> Path Abs Dir -> Path Abs Dir
@@ -552,20 +561,22 @@   | otherwise =
       case component of
         CLib -> buildDir distDir
-        CInternalLib name -> buildDir distDir </> componentNameToDir name
+        CSubLib name -> buildDir distDir </> componentNameToDir name
+        CFlib name -> buildDir distDir </> componentNameToDir name
         CExe name -> buildDir distDir </> componentNameToDir name
         CTest name -> buildDir distDir </> componentNameToDir name
         CBench name -> buildDir distDir </> componentNameToDir name
 
 -- Internal helper to define resolveFileOrWarn and resolveDirOrWarn
-resolveOrWarn :: Text
-              -> (Path Abs Dir -> String -> RIO GetPackageFileContext (Maybe a))
-              -> FilePath.FilePath
-              -> RIO GetPackageFileContext (Maybe a)
+resolveOrWarn ::
+     Text
+  -> (Path Abs Dir -> String -> RIO GetPackageFileContext (Maybe a))
+  -> FilePath.FilePath
+  -> RIO GetPackageFileContext (Maybe a)
 resolveOrWarn subject resolver path = do
   cwd <- liftIO getCurrentDir
-  file <- asks ctxFile
-  dir <- asks (parent . ctxFile)
+  file <- asks (.file)
+  dir <- asks (parent . (.file))
   result <- resolver dir path
   when (isNothing result) $ warnMissingFile subject cwd path file
   pure result
src/Stack/Config.hs view
@@ -1,9 +1,10 @@-{-# LANGUAGE NoImplicitPrelude   #-}
-{-# LANGUAGE DataKinds           #-}
-{-# LANGUAGE LambdaCase          #-}
-{-# LANGUAGE OverloadedStrings   #-}
-{-# LANGUAGE RecordWildCards     #-}
-{-# LANGUAGE TypeFamilies        #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE LambdaCase            #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE TypeFamilies          #-}
 
 -- | The general Stack configuration that starts everything off. This should
 -- be smart to fallback if there is no stack.yaml, instead relying on
@@ -29,6 +30,7 @@   , getProjectConfig
   , withBuildConfig
   , withNewLogFunc
+  , determineStackRootAndOwnership
   ) where
 
 import           Control.Monad.Extra ( firstJustM )
@@ -47,7 +49,7 @@ import qualified Data.Text as T
 import qualified Data.Yaml as Yaml
 import           Distribution.System
-                   ( Arch (OtherArch), OS (..), Platform (..), buildPlatform )
+                   ( Arch (..), OS (..), Platform (..), buildPlatform )
 import qualified Distribution.Text ( simpleParse )
 import           Distribution.Version ( simplifyVersionRange )
 import           GHC.Conc ( getNumProcessors )
@@ -89,6 +91,7 @@                    , stackDeveloperModeDefault, stackDotYaml, stackProgName
                    , stackRootEnvVar, stackWorkEnvVar, stackXdgEnvVar
                    )
+import qualified Stack.Constants as Constants
 import           Stack.Lock ( lockCachedWanted )
 import           Stack.Prelude
 import           Stack.SourceMap
@@ -116,13 +119,14 @@ import           Stack.Types.ConfigMonoid
                    ( ConfigMonoid (..), parseConfigMonoid )
 import           Stack.Types.Casa ( CasaOptsMonoid (..) )
-import           Stack.Types.Docker ( DockerOptsMonoid (..), dockerEnable )
+import           Stack.Types.Docker ( DockerOpts (..), DockerOptsMonoid (..) )
 import           Stack.Types.DumpLogs ( DumpLogs (..) )
 import           Stack.Types.GlobalOpts (  GlobalOpts (..) )
-import           Stack.Types.Nix ( nixEnable )
+import           Stack.Types.Nix ( NixOpts (..) )
 import           Stack.Types.Platform
                    ( PlatformVariant (..), platformOnlyRelDir )
 import           Stack.Types.Project ( Project (..) )
+import qualified Stack.Types.Project as Project ( Project (..) )
 import           Stack.Types.ProjectAndConfigMonoid
                    ( ProjectAndConfigMonoid (..), parseProjectAndConfigMonoid )
 import           Stack.Types.ProjectConfig ( ProjectConfig (..) )
@@ -140,7 +144,7 @@                    ( IntersectingVersionRange (..), VersionCheck (..)
                    , stackVersion, withinRange
                    )
-import           System.Console.ANSI ( hSupportsANSI, setSGRCode )
+import           System.Console.ANSI ( hNowSupportsANSI, setSGRCode )
 import           System.Environment ( getEnvironment, lookupEnv )
 import           System.Info.ShortPathName ( getShortPathName )
 import           System.PosixCompat.Files ( fileOwner, getFileStatus )
@@ -223,19 +227,19 @@         let fp = implicitGlobalDir </> stackDotYaml
         iopc <- loadConfigYaml (parseProjectAndConfigMonoid (parent fp)) fp
         ProjectAndConfigMonoid project _ <- liftIO iopc
-        pure $ projectResolver project
+        pure project.resolver
       ARLatestNightly ->
-        RSLSynonym . Nightly . snapshotsNightly <$> getSnapshots
+        RSLSynonym . Nightly . (.nightly) <$> getSnapshots
       ARLatestLTSMajor x -> do
         snapshots <- getSnapshots
-        case IntMap.lookup x $ snapshotsLts snapshots of
+        case IntMap.lookup x snapshots.lts of
           Nothing -> throwIO $ NoLTSWithMajorVersion x
           Just y -> pure $ RSLSynonym $ LTS x y
       ARLatestLTS -> do
         snapshots <- getSnapshots
-        if IntMap.null $ snapshotsLts snapshots
+        if IntMap.null snapshots.lts
           then throwIO NoLTSFound
-          else let (x, y) = IntMap.findMax $ snapshotsLts snapshots
+          else let (x, y) = IntMap.findMax snapshots.lts
                in  pure $ RSLSynonym $ LTS x y
   prettyInfoL
     [ flow "Selected resolver:"
@@ -248,8 +252,8 @@ getLatestResolver = do
   snapshots <- getSnapshots
   let mlts = uncurry LTS <$>
-             listToMaybe (reverse (IntMap.toList (snapshotsLts snapshots)))
-  pure $ RSLSynonym $ fromMaybe (Nightly (snapshotsNightly snapshots)) mlts
+             listToMaybe (reverse (IntMap.toList snapshots.lts))
+  pure $ RSLSynonym $ fromMaybe (Nightly snapshots.nightly) mlts
 
 -- Interprets ConfigMonoid options.
 configFromConfigMonoid ::
@@ -262,23 +266,23 @@   -> (Config -> RIO env a)
   -> RIO env a
 configFromConfigMonoid
-  configStackRoot
-  configUserConfigPath
-  configResolver
-  configProject
-  ConfigMonoid{..}
+  stackRoot
+  userConfigPath
+  resolver
+  project
+  configMonoid
   inner
   = do
     -- If --stack-work is passed, prefer it. Otherwise, if STACK_WORK
     -- is set, use that. If neither, use the default ".stack-work"
     mstackWorkEnv <- liftIO $ lookupEnv stackWorkEnvVar
     let mproject =
-          case configProject of
+          case project of
             PCProject pair -> Just pair
             PCGlobalProject -> Nothing
             PCNoProject _deps -> Nothing
-        configAllowLocals =
-          case configProject of
+        allowLocals =
+          case project of
             PCProject _ -> True
             PCGlobalProject -> True
             PCNoProject _ -> False
@@ -292,72 +296,64 @@                   _ -> throwIO e
               )
       in  maybe (pure relDirStackWork) (liftIO . parseStackWorkEnv) mstackWorkEnv
-    let configWorkDir = fromFirst configWorkDir0 configMonoidWorkDir
-        configLatestSnapshot = fromFirst
+    let workDir = fromFirst configWorkDir0 configMonoid.workDir
+        latestSnapshot = fromFirst
           "https://s3.amazonaws.com/haddock.stackage.org/snapshots.json"
-          configMonoidLatestSnapshot
-        clConnectionCount = fromFirst 8 configMonoidConnectionCount
-        configHideTHLoading = fromFirstTrue configMonoidHideTHLoading
-        configPrefixTimestamps = fromFirst False configMonoidPrefixTimestamps
-        configGHCVariant = getFirst configMonoidGHCVariant
-        configCompilerRepository = fromFirst
+          configMonoid.latestSnapshot
+        clConnectionCount = fromFirst 8 configMonoid.connectionCount
+        hideTHLoading = fromFirstTrue configMonoid.hideTHLoading
+        prefixTimestamps = fromFirst False configMonoid.prefixTimestamps
+        ghcVariant = getFirst configMonoid.ghcVariant
+        compilerRepository = fromFirst
           defaultCompilerRepository
-          configMonoidCompilerRepository
-        configGHCBuild = getFirst configMonoidGHCBuild
-        configInstallGHC = fromFirstTrue configMonoidInstallGHC
-        configSkipGHCCheck = fromFirstFalse configMonoidSkipGHCCheck
-        configSkipMsys = fromFirstFalse configMonoidSkipMsys
-        configExtraIncludeDirs = configMonoidExtraIncludeDirs
-        configExtraLibDirs = configMonoidExtraLibDirs
-        configCustomPreprocessorExts = configMonoidCustomPreprocessorExts
-        configOverrideGccPath = getFirst configMonoidOverrideGccPath
+          configMonoid.compilerRepository
+        ghcBuild = getFirst configMonoid.ghcBuild
+        installGHC = fromFirstTrue configMonoid.installGHC
+        skipGHCCheck = fromFirstFalse configMonoid.skipGHCCheck
+        skipMsys = fromFirstFalse configMonoid.skipMsys
+        extraIncludeDirs = configMonoid.extraIncludeDirs
+        extraLibDirs = configMonoid.extraLibDirs
+        customPreprocessorExts = configMonoid.customPreprocessorExts
+        overrideGccPath = getFirst configMonoid.overrideGccPath
         -- Only place in the codebase where platform is hard-coded. In theory in
         -- the future, allow it to be configured.
         (Platform defArch defOS) = buildPlatform
         arch = fromMaybe defArch
-          $ getFirst configMonoidArch >>= Distribution.Text.simpleParse
+          $ getFirst configMonoid.arch >>= Distribution.Text.simpleParse
         os = defOS
-        configPlatform = Platform arch os
-        configRequireStackVersion = simplifyVersionRange
-          (getIntersectingVersionRange configMonoidRequireStackVersion)
-        configCompilerCheck = fromFirst MatchMinor configMonoidCompilerCheck
-    case arch of
-      OtherArch "aarch64" -> pure ()
-      OtherArch unk ->
-        prettyWarnL
-          [ flow "Unknown value for architecture setting:"
-          , style Shell (fromString unk) <> "."
-          ]
-      _ -> pure ()
-    configPlatformVariant <- liftIO $
+        platform = Platform arch os
+        requireStackVersion = simplifyVersionRange
+          configMonoid.requireStackVersion.intersectingVersionRange
+        compilerCheck = fromFirst MatchMinor configMonoid.compilerCheck
+    platformVariant <- liftIO $
       maybe PlatformVariantNone PlatformVariant <$> lookupEnv platformVariantEnvVar
-    let configBuild = buildOptsFromMonoid configMonoidBuildOpts
-    configDocker <-
-      dockerOptsFromMonoid (fmap fst mproject) configResolver configMonoidDockerOpts
-    configNix <- nixOptsFromMonoid configMonoidNixOpts os
-    configSystemGHC <-
-      case (getFirst configMonoidSystemGHC, nixEnable configNix) of
+    let build = buildOptsFromMonoid configMonoid.buildOpts
+    docker <-
+      dockerOptsFromMonoid (fmap fst mproject) resolver configMonoid.dockerOpts
+    nix <- nixOptsFromMonoid configMonoid.nixOpts os
+    systemGHC <-
+      case (getFirst configMonoid.systemGHC, nix.enable) of
         (Just False, True) ->
           throwM NixRequiresSystemGhc
         _ ->
           pure
             (fromFirst
-              (dockerEnable configDocker || nixEnable configNix)
-              configMonoidSystemGHC)
-    when (isJust configGHCVariant && configSystemGHC) $
+              (docker.enable || nix.enable)
+              configMonoid.systemGHC)
+    when (isJust ghcVariant && systemGHC) $
       throwM ManualGHCVariantSettingsAreIncompatibleWithSystemGHC
     rawEnv <- liftIO getEnvironment
     pathsEnv <- either throwM pure
-      $ augmentPathMap (map toFilePath configMonoidExtraPath)
+      $ augmentPathMap (map toFilePath configMonoid.extraPath)
                        (Map.fromList (map (T.pack *** T.pack) rawEnv))
     origEnv <- mkProcessContext pathsEnv
-    let configProcessContextSettings _ = pure origEnv
-    configLocalProgramsBase <- case getFirst configMonoidLocalProgramsBase of
-      Nothing -> getDefaultLocalProgramsBase configStackRoot configPlatform origEnv
+    let processContextSettings _ = pure origEnv
+    localProgramsBase <- case getFirst configMonoid.localProgramsBase of
+      Nothing -> getDefaultLocalProgramsBase stackRoot platform origEnv
       Just path -> pure path
-    let localProgramsFilePath = toFilePath configLocalProgramsBase
+    let localProgramsFilePath = toFilePath localProgramsBase
     when (osIsWindows && ' ' `elem` localProgramsFilePath) $ do
-      ensureDir configLocalProgramsBase
+      ensureDir localProgramsBase
       -- getShortPathName returns the long path name when a short name does not
       -- exist.
       shortLocalProgramsFilePath <-
@@ -377,10 +373,10 @@                , style File (fromString localProgramsFilePath) <> "."
                ]
     platformOnlyDir <-
-      runReaderT platformOnlyRelDir (configPlatform, configPlatformVariant)
-    let configLocalPrograms = configLocalProgramsBase </> platformOnlyDir
-    configLocalBin <-
-      case getFirst configMonoidLocalBinPath of
+      runReaderT platformOnlyRelDir (platform, platformVariant)
+    let localPrograms = localProgramsBase </> platformOnlyDir
+    localBin <-
+      case getFirst configMonoid.localBinPath of
         Nothing -> do
           localDir <- getAppUserDataDir "local"
           pure $ localDir </> relDirBin
@@ -396,45 +392,54 @@           -- resolveDirMaybe.
           `catchAny`
           const (throwIO (NoSuchDirectory userPath))
-    configJobs <-
-      case getFirst configMonoidJobs of
+    jobs <-
+      case getFirst configMonoid.jobs of
         Nothing -> liftIO getNumProcessors
         Just i -> pure i
-    let configConcurrentTests = fromFirst True configMonoidConcurrentTests
-    let configTemplateParams = configMonoidTemplateParameters
-        configScmInit = getFirst configMonoidScmInit
-        configCabalConfigOpts = coerce configMonoidCabalConfigOpts
-        configGhcOptionsByName = coerce configMonoidGhcOptionsByName
-        configGhcOptionsByCat = coerce configMonoidGhcOptionsByCat
-        configSetupInfoLocations = configMonoidSetupInfoLocations
-        configSetupInfoInline = configMonoidSetupInfoInline
-        configPvpBounds =
-          fromFirst (PvpBounds PvpBoundsNone False) configMonoidPvpBounds
-        configModifyCodePage = fromFirstTrue configMonoidModifyCodePage
-        configRebuildGhcOptions = fromFirstFalse configMonoidRebuildGhcOptions
-        configApplyGhcOptions = fromFirst AGOLocals configMonoidApplyGhcOptions
-        configApplyProgOptions = fromFirst APOLocals configMonoidApplyProgOptions
-        configAllowNewer = fromFirst False configMonoidAllowNewer
-        configAllowNewerDeps = coerce configMonoidAllowNewerDeps
-        configDefaultTemplate = getFirst configMonoidDefaultTemplate
-        configDumpLogs = fromFirst DumpWarningLogs configMonoidDumpLogs
-        configSaveHackageCreds = fromFirst True configMonoidSaveHackageCreds
-        configHackageBaseUrl =
-          fromFirst "https://hackage.haskell.org/" configMonoidHackageBaseUrl
-        configHideSourcePaths = fromFirstTrue configMonoidHideSourcePaths
-        configRecommendUpgrade = fromFirstTrue configMonoidRecommendUpgrade
-        configNoRunCompile = fromFirstFalse configMonoidNoRunCompile
-    configAllowDifferentUser <-
-      case getFirst configMonoidAllowDifferentUser of
+    let concurrentTests =
+          fromFirst True configMonoid.concurrentTests
+        templateParams = configMonoid.templateParameters
+        scmInit = getFirst configMonoid.scmInit
+        cabalConfigOpts = coerce configMonoid.cabalConfigOpts
+        ghcOptionsByName = coerce configMonoid.ghcOptionsByName
+        ghcOptionsByCat = coerce configMonoid.ghcOptionsByCat
+        setupInfoLocations = configMonoid.setupInfoLocations
+        setupInfoInline = configMonoid.setupInfoInline
+        pvpBounds =
+          fromFirst (PvpBounds PvpBoundsNone False) configMonoid.pvpBounds
+        modifyCodePage = fromFirstTrue configMonoid.modifyCodePage
+        rebuildGhcOptions =
+          fromFirstFalse configMonoid.rebuildGhcOptions
+        applyGhcOptions =
+          fromFirst AGOLocals configMonoid.applyGhcOptions
+        applyProgOptions =
+          fromFirst APOLocals configMonoid.applyProgOptions
+        allowNewer = fromFirst False configMonoid.allowNewer
+        allowNewerDeps = coerce configMonoid.allowNewerDeps
+        defaultTemplate = getFirst configMonoid.defaultTemplate
+        dumpLogs = fromFirst DumpWarningLogs configMonoid.dumpLogs
+        saveHackageCreds =
+          fromFirst True configMonoid.saveHackageCreds
+        hackageBaseUrl =
+          fromFirst Constants.hackageBaseUrl configMonoid.hackageBaseUrl
+        hideSourcePaths = fromFirstTrue configMonoid.hideSourcePaths
+        recommendUpgrade = fromFirstTrue configMonoid.recommendUpgrade
+        notifyIfNixOnPath = fromFirstTrue configMonoid.notifyIfNixOnPath
+        notifyIfGhcUntested = fromFirstTrue configMonoid.notifyIfGhcUntested
+        notifyIfCabalUntested = fromFirstTrue configMonoid.notifyIfCabalUntested
+        notifyIfArchUnknown = fromFirstTrue configMonoid.notifyIfArchUnknown
+        noRunCompile = fromFirstFalse configMonoid.noRunCompile
+    allowDifferentUser <-
+      case getFirst configMonoid.allowDifferentUser of
         Just True -> pure True
         _ -> getInContainer
     configRunner' <- view runnerL
-    useAnsi <- liftIO $ hSupportsANSI stderr
+    useAnsi <- liftIO $ hNowSupportsANSI stderr
     let stylesUpdate' = (configRunner' ^. stylesUpdateL) <>
-          configMonoidStyles
-        useColor' = runnerUseColor configRunner'
+          configMonoid.styles
+        useColor' = configRunner'.useColor
         mUseColor = do
-          colorWhen <- getFirst configMonoidColorWhen
+          colorWhen <- getFirst configMonoid.colorWhen
           pure $ case colorWhen of
             ColorNever  -> False
             ColorAlways -> True
@@ -444,11 +449,11 @@           & processContextL .~ origEnv
           & stylesUpdateL .~ stylesUpdate'
           & useColorL .~ useColor''
-        go = runnerGlobalOpts configRunner'
+        go = configRunner'.globalOpts
     pic <-
-      case getFirst configMonoidPackageIndex of
+      case getFirst configMonoid.packageIndex of
         Nothing ->
-          case getFirst configMonoidPackageIndices of
+          case getFirst configMonoid.packageIndices of
             Nothing -> pure defaultPackageIndexConfig
             Just [pic] -> do
               prettyWarn packageIndicesWarning
@@ -462,9 +467,9 @@           case parseAbsDir dir of
             Nothing -> throwIO $ ParseAbsolutePathException pantryRootEnvVar dir
             Just x -> pure x
-        Nothing -> pure $ configStackRoot </> relDirPantry
+        Nothing -> pure $ stackRoot </> relDirPantry
     let snapLoc =
-          case getFirst configMonoidSnapshotLocation of
+          case getFirst configMonoid.snapshotLocation of
             Nothing -> defaultSnapshotLocation
             Just addr ->
               customSnapshotLocation
@@ -482,22 +487,24 @@                         <> "/" <> display day <> ".yaml"
                 mkRSLUrl builder = RSLUrl (utf8BuilderToText builder) Nothing
                 addr' = display $ T.dropWhileEnd (=='/') addr
-    let configStackDeveloperMode =
-          fromFirst stackDeveloperModeDefault configMonoidStackDeveloperMode
-        configCasa = if fromFirstTrue $ casaMonoidEnable configMonoidCasaOpts
-          then
-            let casaRepoPrefix = fromFirst
-                  (fromFirst defaultCasaRepoPrefix configMonoidCasaRepoPrefix)
-                  (casaMonoidRepoPrefix configMonoidCasaOpts)
-                casaMaxKeysPerRequest = fromFirst
-                  defaultCasaMaxPerRequest
-                  (casaMonoidMaxKeysPerRequest configMonoidCasaOpts)
-            in  Just (casaRepoPrefix, casaMaxKeysPerRequest)
-          else Nothing
+    let stackDeveloperMode = fromFirst
+          stackDeveloperModeDefault
+          configMonoid.stackDeveloperMode
+        casa =
+          if fromFirstTrue configMonoid.casaOpts.enable
+            then
+              let casaRepoPrefix = fromFirst
+                    (fromFirst defaultCasaRepoPrefix configMonoid.casaRepoPrefix)
+                    configMonoid.casaOpts.repoPrefix
+                  casaMaxKeysPerRequest = fromFirst
+                    defaultCasaMaxPerRequest
+                    configMonoid.casaOpts.maxKeysPerRequest
+              in  Just (casaRepoPrefix, casaMaxKeysPerRequest)
+            else Nothing
     withNewLogFunc go useColor'' stylesUpdate' $ \logFunc -> do
-      let configRunner = configRunner'' & logFuncL .~ logFunc
+      let runner = configRunner'' & logFuncL .~ logFunc
       withLocalLogFunc logFunc $ handleMigrationException $ do
-        logDebug $ case configCasa of
+        logDebug $ case casa of
           Nothing -> "Use of Casa server disabled."
           Just (repoPrefix, maxKeys) ->
                "Use of Casa server enabled: ("
@@ -508,13 +515,80 @@         withPantryConfig'
           pantryRoot
           pic
-          (maybe HpackBundled HpackCommand $ getFirst configMonoidOverrideHpack)
+          (maybe HpackBundled HpackCommand $ getFirst configMonoid.overrideHpack)
           clConnectionCount
-          configCasa
+          casa
           snapLoc
-          (\configPantryConfig -> initUserStorage
-            (configStackRoot </> relFileStorage)
-            (\configUserStorage -> inner Config {..}))
+          (\pantryConfig -> initUserStorage
+            (stackRoot </> relFileStorage)
+            ( \userStorage -> inner Config
+                { workDir
+                , userConfigPath
+                , build
+                , docker
+                , nix
+                , processContextSettings
+                , localProgramsBase
+                , localPrograms
+                , hideTHLoading
+                , prefixTimestamps
+                , platform
+                , platformVariant
+                , ghcVariant
+                , ghcBuild
+                , latestSnapshot
+                , systemGHC
+                , installGHC
+                , skipGHCCheck
+                , skipMsys
+                , compilerCheck
+                , compilerRepository
+                , localBin
+                , requireStackVersion
+                , jobs
+                , overrideGccPath
+                , extraIncludeDirs
+                , extraLibDirs
+                , customPreprocessorExts
+                , concurrentTests
+                , templateParams
+                , scmInit
+                , ghcOptionsByName
+                , ghcOptionsByCat
+                , cabalConfigOpts
+                , setupInfoLocations
+                , setupInfoInline
+                , pvpBounds
+                , modifyCodePage
+                , rebuildGhcOptions
+                , applyGhcOptions
+                , applyProgOptions
+                , allowNewer
+                , allowNewerDeps
+                , defaultTemplate
+                , allowDifferentUser
+                , dumpLogs
+                , project
+                , allowLocals
+                , saveHackageCreds
+                , hackageBaseUrl
+                , runner
+                , pantryConfig
+                , stackRoot
+                , resolver
+                , userStorage
+                , hideSourcePaths
+                , recommendUpgrade
+                , notifyIfNixOnPath
+                , notifyIfGhcUntested
+                , notifyIfCabalUntested
+                , notifyIfArchUnknown
+                , noRunCompile
+                , stackDeveloperMode
+                , casa
+                }
+            )
+          )
 
 -- | Runs the provided action with the given 'LogFunc' in the environment
 withLocalLogFunc :: HasLogFunc env => LogFunc -> RIO env a -> RIO env a
@@ -534,10 +608,10 @@         $ setLogLevelColors logLevelColors
         $ setLogSecondaryColor secondaryColor
         $ setLogAccentColors (const highlightColor)
-        $ setLogUseTime (globalTimeInLog go)
-        $ setLogMinLevel (globalLogLevel go)
-        $ setLogVerboseFormat (globalLogLevel go <= LevelDebug)
-        $ setLogTerminal (globalTerminal go)
+        $ setLogUseTime go.timeInLog
+        $ setLogMinLevel go.logLevel
+        $ setLogVerboseFormat (go.logLevel <= LevelDebug)
+        $ setLogTerminal go.terminal
           logOptions0
   withLogFunc logOptions inner
  where
@@ -567,8 +641,7 @@           Nothing ->
             throwM $ ParseAbsolutePathException "LOCALAPPDATA" t
           Just lad ->
-            pure $ lad </> relDirUpperPrograms </>
-                   relDirStackProgName
+            pure $ lad </> relDirUpperPrograms </> relDirStackProgName
         Nothing -> pure defaultBase
     _ -> pure defaultBase
  where
@@ -581,10 +654,10 @@   => (Config -> RIO env a)
   -> RIO env a
 loadConfig inner = do
-  mstackYaml <- view $ globalOptsL.to globalStackYaml
+  mstackYaml <- view $ globalOptsL . to (.stackYaml)
   mproject <- loadProjectConfig mstackYaml
-  mresolver <- view $ globalOptsL.to globalResolver
-  configArgs <- view $ globalOptsL.to globalConfigMonoid
+  mresolver <- view $ globalOptsL . to (.resolver)
+  configArgs <- view $ globalOptsL . to (.configMonoid)
   (configRoot, stackRoot, userOwnsStackRoot) <-
     determineStackRootAndOwnership configArgs
 
@@ -601,9 +674,7 @@         -- non-project config files' existence of a docker section should never
         -- default docker to enabled, so make it look like they didn't exist
         map
-          ( \c -> c {configMonoidDockerOpts =
-              (configMonoidDockerOpts c) {dockerMonoidDefaultEnable = Any False}}
-          )
+          (\c -> c {dockerOpts = c.dockerOpts { defaultEnable = Any False }})
           extraConfigs0
 
   let withConfig =
@@ -615,13 +686,25 @@           (mconcat $ configArgs : addConfigMonoid extraConfigs)
 
   withConfig $ \config -> do
-    unless (stackVersion `withinRange` configRequireStackVersion config)
-      (throwM (BadStackVersionException (configRequireStackVersion config)))
-    unless (configAllowDifferentUser config) $ do
+    let Platform arch _ = config.platform
+    case arch of
+      OtherArch unknownArch
+        | config.notifyIfArchUnknown ->
+            prettyWarnL
+              [ flow "Unknown value for architecture setting:"
+              , style Shell (fromString unknownArch) <> "."
+              , flow "To mute this message in future, set"
+              , style Shell (flow "notify-if-arch-unknown: false")
+              , flow "in Stack's configuration."
+              ]
+      _ -> pure ()
+    unless (stackVersion `withinRange` config.requireStackVersion)
+      (throwM (BadStackVersionException config.requireStackVersion))
+    unless config.allowDifferentUser $ do
       unless userOwnsStackRoot $
         throwM (UserDoesn'tOwnDirectory stackRoot)
       forM_ (configProjectRoot config) $ \dir ->
-        checkOwnership (dir </> configWorkDir config)
+        checkOwnership (dir </> config.workDir)
     inner config
 
 -- | Load the build configuration, adds build-specific values to config loaded
@@ -637,20 +720,20 @@   -- to properly deal with an AbstractResolver, we need a base directory (to
   -- deal with custom snapshot relative paths). We consider the current working
   -- directory to be the correct base. Let's calculate the mresolver first.
-  mresolver <- forM (configResolver config) $ \aresolver -> do
+  mresolver <- forM config.resolver $ \aresolver -> do
     logDebug ("Using resolver: " <> display aresolver <> " specified on command line")
     makeConcreteResolver aresolver
 
-  (project', stackYamlFP) <- case configProject config of
+  (project', stackYaml) <- case config.project of
     PCProject (project, fp) -> do
-      forM_ (projectUserMsg project) prettyWarnS
+      forM_ project.userMsg prettyWarnS
       pure (project, fp)
     PCNoProject extraDeps -> do
       p <-
         case mresolver of
           Nothing -> throwIO NoResolverWhenUsingNoProject
           Just _ -> getEmptyProject mresolver extraDeps
-      pure (p, configUserConfigPath config)
+      pure (p, config.userConfigPath)
     PCGlobalProject -> do
       logDebug "Run from outside a project, using implicit global project config"
       destDir <- getImplicitGlobalProjectDir config
@@ -665,13 +748,13 @@           iopc <- loadConfigYaml (parseProjectAndConfigMonoid destDir) dest
           ProjectAndConfigMonoid project _ <- liftIO iopc
           when (view terminalL config) $
-            case configResolver config of
+            case config.resolver of
               Nothing ->
                 logDebug $
-                  "Using resolver: " <>
-                  display (projectResolver project) <>
-                  " from implicit global project's config file: " <>
-                  fromString dest'
+                     "Using resolver: "
+                  <> display project.resolver
+                  <> " from implicit global project's config file: "
+                  <> fromString dest'
               Just _ -> pure ()
           pure (project, dest)
         else do
@@ -689,7 +772,7 @@               [ "# This is the implicit global project's config file, which is only used when\n"
               , "# 'stack' is run outside of a real project. Settings here do _not_ act as\n"
               , "# defaults for all projects. To change Stack's default settings, edit\n"
-              , "# '", encodeUtf8 (T.pack $ toFilePath $ configUserConfigPath config), "' instead.\n"
+              , "# '", encodeUtf8 (T.pack $ toFilePath config.userConfigPath), "' instead.\n"
               , "#\n"
               , "# For more information about Stack's configuration, see\n"
               , "# http://docs.haskellstack.org/en/stable/yaml_configuration/\n"
@@ -700,29 +783,30 @@               "used only when 'stack' is run\noutside of a " <>
               "real project.\n"
           pure (p, dest)
-  mcompiler <- view $ globalOptsL.to globalCompiler
-  let project = project'
-        { projectCompiler = mcompiler <|> projectCompiler project'
-        , projectResolver = fromMaybe (projectResolver project') mresolver
+  mcompiler <- view $ globalOptsL . to (.compiler)
+  let project :: Project
+      project = project'
+        { Project.compiler = mcompiler <|> project'.compiler
+        , Project.resolver = fromMaybe project'.resolver mresolver
         }
-  extraPackageDBs <- mapM resolveDir' (projectExtraPackageDBs project)
+  extraPackageDBs <- mapM resolveDir' project.extraPackageDBs
 
-  wanted <- lockCachedWanted stackYamlFP (projectResolver project) $
-    fillProjectWanted stackYamlFP config project
+  smWanted <- lockCachedWanted stackYaml project.resolver $
+    fillProjectWanted stackYaml config project
 
   -- Unfortunately redoes getProjectWorkDir, since we don't have a BuildConfig
   -- yet
   workDir <- view workDirL
-  let projectStorageFile = parent stackYamlFP </> workDir </> relFileStorage
+  let projectStorageFile = parent stackYaml </> workDir </> relFileStorage
 
   initProjectStorage projectStorageFile $ \projectStorage -> do
     let bc = BuildConfig
-          { bcConfig = config
-          , bcSMWanted = wanted
-          , bcExtraPackageDBs = extraPackageDBs
-          , bcStackYaml = stackYamlFP
-          , bcCurator = projectCurator project
-          , bcProjectStorage = projectStorage
+          { config
+          , smWanted
+          , extraPackageDBs
+          , stackYaml
+          , curator = project.curator
+          , projectStorage
           }
     runRIO bc inner
  where
@@ -747,16 +831,15 @@           ]
         pure r''
     pure Project
-      { projectUserMsg = Nothing
-      , projectPackages = []
-      , projectDependencies =
-          map (RPLImmutable . flip RPLIHackage Nothing) extraDeps
-      , projectFlags = mempty
-      , projectResolver = r
-      , projectCompiler = Nothing
-      , projectExtraPackageDBs = []
-      , projectCurator = Nothing
-      , projectDropPackages = mempty
+      { userMsg = Nothing
+      , packages = []
+      , extraDeps = map (RPLImmutable . flip RPLIHackage Nothing) extraDeps
+      , flagsByPkg = mempty
+      , resolver = r
+      , compiler = Nothing
+      , extraPackageDBs = []
+      , curator = Nothing
+      , dropPackages = mempty
       }
 
 fillProjectWanted ::
@@ -769,13 +852,13 @@   -> Map PackageName (Bool -> RIO env DepPackage)
   -> RIO env (SMWanted, [CompletedPLI])
 fillProjectWanted stackYamlFP config project locCache snapCompiler snapPackages = do
-  let bopts = configBuild config
+  let bopts = config.build
 
-  packages0 <- for (projectPackages project) $ \fp@(RelFilePath t) -> do
+  packages0 <- for project.packages $ \fp@(RelFilePath t) -> do
     abs' <- resolveDir (parent stackYamlFP) (T.unpack t)
     let resolved = ResolvedPath fp abs'
-    pp <- mkProjectPackage YesPrintWarnings resolved (boptsHaddock bopts)
-    pure (cpName $ ppCommon pp, pp)
+    pp <- mkProjectPackage YesPrintWarnings resolved bopts.buildHaddocks
+    pure (pp.projectCommon.name, pp)
 
   -- prefetch git repos to avoid cloning per subdirectory
   -- see https://github.com/commercialhaskell/stack/issues/5411
@@ -784,11 +867,11 @@             (RPLImmutable (RPLIRepo repo rpm)) -> Just (repo, rpm)
             _ -> Nothing
         )
-        (projectDependencies project)
+        project.extraDeps
   logDebug ("Prefetching git repos: " <> display (T.pack (show gitRepos)))
   fetchReposRaw gitRepos
 
-  (deps0, mcompleted) <- fmap unzip . forM (projectDependencies project) $ \rpl -> do
+  (deps0, mcompleted) <- fmap unzip . forM project.extraDeps $ \rpl -> do
     (pl, mCompleted) <- case rpl of
        RPLImmutable rpli -> do
          (compl, mcompl) <-
@@ -805,17 +888,17 @@        RPLMutable p ->
          pure (PLMutable p, Nothing)
     dp <- additionalDepPackage (shouldHaddockDeps bopts) pl
-    pure ((cpName $ dpCommon dp, dp), mCompleted)
+    pure ((dp.depCommon.name, dp), mCompleted)
 
   checkDuplicateNames $
-    map (second (PLMutable . ppResolvedDir)) packages0 ++
-    map (second dpLocation) deps0
+    map (second (PLMutable . (.resolvedDir))) packages0 ++
+    map (second (.location)) deps0
 
   let packages1 = Map.fromList packages0
       snPackages = snapPackages
         `Map.difference` packages1
         `Map.difference` Map.fromList deps0
-        `Map.withoutKeys` projectDropPackages project
+        `Map.withoutKeys` project.dropPackages
 
   snDeps <- for snPackages $ \getDep -> getDep (shouldHaddockDeps bopts)
 
@@ -823,19 +906,19 @@ 
   let mergeApply m1 m2 f =
         MS.merge MS.preserveMissing MS.dropMissing (MS.zipWithMatched f) m1 m2
-      pFlags = projectFlags project
-      packages2 = mergeApply packages1 pFlags $
-        \_ p flags -> p{ppCommon=(ppCommon p){cpFlags=flags}}
-      deps2 = mergeApply deps1 pFlags $
-        \_ d flags -> d{dpCommon=(dpCommon d){cpFlags=flags}}
+      pFlags = project.flagsByPkg
+      packages2 = mergeApply packages1 pFlags $ \_ p flags ->
+        p { projectCommon = p.projectCommon { flags = flags } }
+      deps2 = mergeApply deps1 pFlags $ \_ d flags ->
+        d { depCommon = d.depCommon { flags = flags } }
 
   checkFlagsUsedThrowing pFlags FSStackYaml packages1 deps1
 
-  let pkgGhcOptions = configGhcOptionsByName config
-      deps = mergeApply deps2 pkgGhcOptions $
-        \_ d options -> d{dpCommon=(dpCommon d){cpGhcOptions=options}}
-      packages = mergeApply packages2 pkgGhcOptions $
-        \_ p options -> p{ppCommon=(ppCommon p){cpGhcOptions=options}}
+  let pkgGhcOptions = config.ghcOptionsByName
+      deps = mergeApply deps2 pkgGhcOptions $ \_ d options ->
+        d { depCommon = d.depCommon { ghcOptions = options } }
+      packages = mergeApply packages2 pkgGhcOptions $ \_ p options ->
+        p { projectCommon = p.projectCommon { ghcOptions = options } }
       unusedPkgGhcOptions =
         pkgGhcOptions `Map.restrictKeys` Map.keysSet packages2
           `Map.restrictKeys` Map.keysSet deps2
@@ -844,10 +927,10 @@     throwM $ InvalidGhcOptionsSpecification (Map.keys unusedPkgGhcOptions)
 
   let wanted = SMWanted
-        { smwCompiler = fromMaybe snapCompiler (projectCompiler project)
-        , smwProject = packages
-        , smwDeps = deps
-        , smwSnapshotLocation = projectResolver project
+        { compiler = fromMaybe snapCompiler project.compiler
+        , project = packages
+        , deps = deps
+        , snapshotLocation = project.resolver
         }
 
   pure (wanted, catMaybes mcompleted)
@@ -875,7 +958,7 @@   -> m (Path Abs Dir, Path Abs Dir, Bool)
 determineStackRootAndOwnership clArgs = liftIO $ do
   (configRoot, stackRoot) <- do
-    case getFirst (configMonoidStackRoot clArgs) of
+    case getFirst clArgs.stackRoot of
       Just x -> pure (x, x)
       Nothing -> do
         mstackRoot <- lookupEnv stackRootEnvVar
@@ -984,7 +1067,8 @@ -- 'ParseConfigFileException' when there's a decoding error.
 loadConfigYaml ::
      HasLogFunc env
-  => (Value -> Yaml.Parser (WithJSONWarnings a)) -> Path Abs File -> RIO env a
+  => (Value -> Yaml.Parser (WithJSONWarnings a))
+  -> Path Abs File -> RIO env a
 loadConfigYaml parser path = do
   eres <- loadYaml parser path
   case eres of
src/Stack/Config/Build.hs view
@@ -1,73 +1,92 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 
 -- | Build configuration
 module Stack.Config.Build
- ( benchmarkOptsFromMonoid
- , buildOptsFromMonoid
+ ( buildOptsFromMonoid
  , haddockOptsFromMonoid
  , testOptsFromMonoid
+ , benchmarkOptsFromMonoid
  ) where
 
 import           Distribution.Verbosity ( normal )
+import           Stack.BuildOpts
+                   ( defaultBenchmarkOpts, defaultHaddockOpts, defaultTestOpts )
 import           Stack.Prelude
 import           Stack.Types.BuildOpts
-                   ( BenchmarkOpts (..), BenchmarkOptsMonoid (..)
-                   , BuildOpts (..), BuildOptsMonoid (..), CabalVerbosity (..)
-                   , HaddockOpts (..), HaddockOptsMonoid (..)
-                   , ProgressBarFormat (..), TestOpts (..), TestOptsMonoid (..)
-                   , defaultBenchmarkOpts, defaultHaddockOpts, defaultTestOpts
+                   ( BenchmarkOpts (..), BuildOpts (..), HaddockOpts (..)
+                   , TestOpts (..)
                    )
+import qualified Stack.Types.BuildOpts as BenchmarkOpts ( BenchmarkOpts (..) )
+import qualified Stack.Types.BuildOpts as HaddockOpts ( HaddockOpts (..) )
+import qualified Stack.Types.BuildOpts as TestOpts ( TestOpts (..) )
+import           Stack.Types.BuildOptsMonoid
+                   ( BenchmarkOptsMonoid (..), BuildOptsMonoid (..)
+                   , CabalVerbosity (..), HaddockOptsMonoid (..)
+                   , ProgressBarFormat (..), TestOptsMonoid (..)
+                   )
 
 -- | Interprets BuildOptsMonoid options.
 buildOptsFromMonoid :: BuildOptsMonoid -> BuildOpts
-buildOptsFromMonoid BuildOptsMonoid{..} = BuildOpts
-  { boptsLibProfile = fromFirstFalse
-      (buildMonoidLibProfile <>
-       FirstFalse (if tracing || profiling then Just True else Nothing))
-  , boptsExeProfile = fromFirstFalse
-      (buildMonoidExeProfile <>
-       FirstFalse (if tracing || profiling then Just True else Nothing))
-  , boptsLibStrip = fromFirstTrue
-      (buildMonoidLibStrip <>
-       FirstTrue (if noStripping then Just False else Nothing))
-  , boptsExeStrip = fromFirstTrue
-      (buildMonoidExeStrip <>
-       FirstTrue (if noStripping then Just False else Nothing))
-  , boptsHaddock = fromFirstFalse buildMonoidHaddock
-  , boptsHaddockOpts = haddockOptsFromMonoid buildMonoidHaddockOpts
-  , boptsOpenHaddocks = fromFirstFalse buildMonoidOpenHaddocks
-  , boptsHaddockDeps = getFirst buildMonoidHaddockDeps
-  , boptsHaddockInternal = fromFirstFalse buildMonoidHaddockInternal
-  , boptsHaddockHyperlinkSource =
-      fromFirstTrue buildMonoidHaddockHyperlinkSource
-  , boptsInstallExes = fromFirstFalse buildMonoidInstallExes
-  , boptsInstallCompilerTool = fromFirstFalse buildMonoidInstallCompilerTool
-  , boptsPreFetch = fromFirstFalse buildMonoidPreFetch
-  , boptsKeepGoing = getFirst buildMonoidKeepGoing
-  , boptsKeepTmpFiles = fromFirstFalse buildMonoidKeepTmpFiles
-  , boptsForceDirty = fromFirstFalse buildMonoidForceDirty
-  , boptsTests = fromFirstFalse buildMonoidTests
-  , boptsTestOpts =
-      testOptsFromMonoid buildMonoidTestOpts additionalArgs
-  , boptsBenchmarks = fromFirstFalse buildMonoidBenchmarks
-  , boptsBenchmarkOpts =
-      benchmarkOptsFromMonoid buildMonoidBenchmarkOpts additionalArgs
-  , boptsReconfigure = fromFirstFalse buildMonoidReconfigure
-  , boptsCabalVerbose =
-      fromFirst (CabalVerbosity normal) buildMonoidCabalVerbose
-  , boptsSplitObjs = fromFirstFalse buildMonoidSplitObjs
-  , boptsSkipComponents = buildMonoidSkipComponents
-  , boptsInterleavedOutput = fromFirstTrue buildMonoidInterleavedOutput
-  , boptsProgressBar = fromFirst CappedBar buildMonoidProgressBar
-  , boptsDdumpDir = getFirst buildMonoidDdumpDir
+buildOptsFromMonoid buildMonoid = BuildOpts
+  { libProfile = fromFirstFalse
+      (  buildMonoid.libProfile
+      <> FirstFalse (if tracing || profiling then Just True else Nothing)
+      )
+  , exeProfile = fromFirstFalse
+      (  buildMonoid.exeProfile
+      <> FirstFalse (if tracing || profiling then Just True else Nothing)
+      )
+  , libStrip = fromFirstTrue
+      (  buildMonoid.libStrip
+      <> FirstTrue (if noStripping then Just False else Nothing)
+      )
+  , exeStrip = fromFirstTrue
+      (  buildMonoid.exeStrip
+      <> FirstTrue (if noStripping then Just False else Nothing)
+      )
+  , buildHaddocks = fromFirstFalse buildMonoid.buildHaddocks
+  , haddockOpts = haddockOptsFromMonoid buildMonoid.haddockOpts
+  , openHaddocks =
+         not isHaddockFromHackage
+      && fromFirstFalse buildMonoid.openHaddocks
+  , haddockDeps = if isHaddockFromHackage
+      then Nothing
+      else getFirst buildMonoid.haddockDeps
+  , haddockInternal =
+         not isHaddockFromHackage
+      && fromFirstFalse buildMonoid.haddockInternal
+  , haddockHyperlinkSource =
+         isHaddockFromHackage
+      || fromFirstTrue buildMonoid.haddockHyperlinkSource
+  , haddockForHackage = isHaddockFromHackage
+  , installExes = fromFirstFalse buildMonoid.installExes
+  , installCompilerTool = fromFirstFalse buildMonoid.installCompilerTool
+  , preFetch = fromFirstFalse buildMonoid.preFetch
+  , keepGoing = getFirst buildMonoid.keepGoing
+  , keepTmpFiles = fromFirstFalse buildMonoid.keepTmpFiles
+  , forceDirty = isHaddockFromHackage || fromFirstFalse buildMonoid.forceDirty
+  , tests = fromFirstFalse buildMonoid.tests
+  , testOpts = testOptsFromMonoid buildMonoid.testOpts additionalArgs
+  , benchmarks = fromFirstFalse buildMonoid.benchmarks
+  , benchmarkOpts =
+      benchmarkOptsFromMonoid buildMonoid.benchmarkOpts additionalArgs
+  , reconfigure = fromFirstFalse buildMonoid.reconfigure
+  , cabalVerbose = fromFirst (CabalVerbosity normal) buildMonoid.cabalVerbose
+  , splitObjs = fromFirstFalse buildMonoid.splitObjs
+  , skipComponents = buildMonoid.skipComponents
+  , interleavedOutput = fromFirstTrue buildMonoid.interleavedOutput
+  , progressBar = fromFirst CappedBar buildMonoid.progressBar
+  , ddumpDir = getFirst buildMonoid.ddumpDir
   }
  where
+  isHaddockFromHackage = fromFirstFalse buildMonoid.haddockForHackage
   -- These options are not directly used in bopts, instead they
   -- transform other options.
-  tracing = getAny buildMonoidTrace
-  profiling = getAny buildMonoidProfile
-  noStripping = getAny buildMonoidNoStrip
+  tracing = getAny buildMonoid.trace
+  profiling = getAny buildMonoid.profile
+  noStripping = getAny buildMonoid.noStrip
   -- Additional args for tracing / profiling
   additionalArgs =
     if tracing || profiling
@@ -82,31 +101,37 @@       then Just "-p"
       else Nothing
 
+-- | Interprets HaddockOptsMonoid options.
 haddockOptsFromMonoid :: HaddockOptsMonoid -> HaddockOpts
-haddockOptsFromMonoid HaddockOptsMonoid{..} = defaultHaddockOpts
-  { hoAdditionalArgs = hoMonoidAdditionalArgs }
+haddockOptsFromMonoid hoMonoid = defaultHaddockOpts
+  { HaddockOpts.additionalArgs = hoMonoid.additionalArgs }
 
+-- | Interprets TestOptsMonoid options.
 testOptsFromMonoid :: TestOptsMonoid -> Maybe [String] -> TestOpts
-testOptsFromMonoid TestOptsMonoid{..} madditional = defaultTestOpts
-  { toRerunTests = fromFirstTrue toMonoidRerunTests
-  , toAdditionalArgs = fromMaybe [] madditional <> toMonoidAdditionalArgs
-  , toCoverage = fromFirstFalse toMonoidCoverage
-  , toDisableRun = fromFirstFalse toMonoidDisableRun
-  , toMaximumTimeSeconds =
-      fromFirst (toMaximumTimeSeconds defaultTestOpts) toMonoidMaximumTimeSeconds
-  , toAllowStdin = fromFirstTrue toMonoidAllowStdin
+testOptsFromMonoid toMonoid madditional = defaultTestOpts
+  { TestOpts.rerunTests = fromFirstTrue toMonoid.rerunTests
+  , TestOpts.additionalArgs =
+      fromMaybe [] madditional <> toMonoid.additionalArgs
+  , TestOpts.coverage = fromFirstFalse toMonoid.coverage
+  , TestOpts.disableRun = fromFirstFalse toMonoid.disableRun
+  , TestOpts.maximumTimeSeconds =
+      fromFirst
+        defaultTestOpts.maximumTimeSeconds
+        toMonoid.maximumTimeSeconds
+  , TestOpts.allowStdin = fromFirstTrue toMonoid.allowStdin
   }
 
+-- | Interprets BenchmarkOptsMonoid options.
 benchmarkOptsFromMonoid ::
      BenchmarkOptsMonoid
   -> Maybe [String]
   -> BenchmarkOpts
-benchmarkOptsFromMonoid BenchmarkOptsMonoid{..} madditional =
+benchmarkOptsFromMonoid beoMonoid madditional =
   defaultBenchmarkOpts
-    { beoAdditionalArgs =
+    { BenchmarkOpts.additionalArgs =
         fmap (\args -> unwords args <> " ") madditional <>
-        getFirst beoMonoidAdditionalArgs
-    , beoDisableRun = fromFirst
-        (beoDisableRun defaultBenchmarkOpts)
-        beoMonoidDisableRun
+        getFirst beoMonoid.additionalArgs
+    , BenchmarkOpts.disableRun = fromFirst
+        defaultBenchmarkOpts.disableRun
+        beoMonoid.disableRun
     }
src/Stack/Config/Docker.hs view
@@ -1,6 +1,7 @@-{-# LANGUAGE NoImplicitPrelude  #-}
-{-# LANGUAGE OverloadedStrings  #-}
-{-# LANGUAGE RecordWildCards    #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- | Docker configuration
 module Stack.Config.Docker
@@ -19,7 +20,7 @@                    , DockerOptsMonoid (..), dockerImageArgName
                    )
 import           Stack.Types.Resolver ( AbstractResolver (..) )
-import           Stack.Types.Version ( getIntersectingVersionRange )
+import           Stack.Types.Version ( IntersectingVersionRange (..) )
 
 -- | Type representing exceptions thrown by functions exported by the
 -- "Stack.Config.Docker" module.
@@ -38,7 +39,7 @@           (_, Just aresolver) ->
             T.unpack $ utf8BuilderToText $ display aresolver
           (Just project, Nothing) ->
-            T.unpack $ utf8BuilderToText $ display $ projectResolver project
+            T.unpack $ utf8BuilderToText $ display project.resolver
       , "\nUse an LTS resolver, or set the '"
       , T.unpack dockerImageArgName
       , "' explicitly, in your configuration file."]
@@ -56,7 +57,7 @@     Just (ARResolver (RSLSynonym lts@(LTS _ _))) -> pure lts
     Just _aresolver -> exc
     Nothing ->
-      case projectResolver <$> mproject of
+      case (.resolver) <$> mproject of
         Just (RSLSynonym lts@(LTS _ _)) -> pure lts
         _ -> exc
   pure $ base ++ ":" ++ show lts
@@ -68,38 +69,63 @@   -> Maybe AbstractResolver
   -> DockerOptsMonoid
   -> m DockerOpts
-dockerOptsFromMonoid mproject maresolver DockerOptsMonoid{..} = do
-  let dockerImage =
-        case getFirst dockerMonoidRepoOrImage of
+dockerOptsFromMonoid mproject maresolver dockerMonoid = do
+  let image =
+        case getFirst dockerMonoid.repoOrImage of
           Nothing -> addDefaultTag "fpco/stack-build" mproject maresolver
-          Just (DockerMonoidImage image) -> pure image
+          Just (DockerMonoidImage image') -> pure image'
           Just (DockerMonoidRepo repo) ->
             case find (`elem` (":@" :: String)) repo of
               Nothing -> addDefaultTag repo mproject maresolver
               -- Repo already specified a tag or digest, so don't append default
               Just _ -> pure repo
-  let dockerEnable =
-        fromFirst (getAny dockerMonoidDefaultEnable) dockerMonoidEnable
-      dockerRegistryLogin =
+  let enable =
         fromFirst
-          (isJust (emptyToNothing (getFirst dockerMonoidRegistryUsername)))
-          dockerMonoidRegistryLogin
-      dockerRegistryUsername = emptyToNothing (getFirst dockerMonoidRegistryUsername)
-      dockerRegistryPassword = emptyToNothing (getFirst dockerMonoidRegistryPassword)
-      dockerAutoPull = fromFirstTrue dockerMonoidAutoPull
-      dockerDetach = fromFirstFalse dockerMonoidDetach
-      dockerPersist = fromFirstFalse dockerMonoidPersist
-      dockerContainerName = emptyToNothing (getFirst dockerMonoidContainerName)
-      dockerNetwork = emptyToNothing (getFirst dockerMonoidNetwork)
-      dockerRunArgs = dockerMonoidRunArgs
-      dockerMount = dockerMonoidMount
-      dockerMountMode = emptyToNothing (getFirst dockerMonoidMountMode)
-      dockerEnv = dockerMonoidEnv
-      dockerSetUser = getFirst dockerMonoidSetUser
-      dockerRequireDockerVersion =
-        simplifyVersionRange (getIntersectingVersionRange dockerMonoidRequireDockerVersion)
-      dockerStackExe = getFirst dockerMonoidStackExe
-  pure DockerOpts{..}
+          (getAny dockerMonoid.defaultEnable)
+          dockerMonoid.enable
+      registryLogin =
+        fromFirst
+          (isJust (emptyToNothing (getFirst dockerMonoid.registryUsername)))
+          dockerMonoid.registryLogin
+      registryUsername =
+        emptyToNothing (getFirst dockerMonoid.registryUsername)
+      registryPassword =
+        emptyToNothing (getFirst dockerMonoid.registryPassword)
+      autoPull = fromFirstTrue dockerMonoid.autoPull
+      detach = fromFirstFalse dockerMonoid.detach
+      persist = fromFirstFalse dockerMonoid.persist
+      containerName =
+        emptyToNothing (getFirst dockerMonoid.containerName)
+      network = emptyToNothing (getFirst dockerMonoid.network)
+      runArgs = dockerMonoid.runArgs
+      mount = dockerMonoid.mount
+      mountMode =
+        emptyToNothing (getFirst dockerMonoid.mountMode)
+      env = dockerMonoid.env
+      setUser = getFirst dockerMonoid.setUser
+      requireDockerVersion =
+        simplifyVersionRange
+          dockerMonoid.requireDockerVersion.intersectingVersionRange
+      stackExe = getFirst dockerMonoid.stackExe
+  pure DockerOpts
+    { enable
+    , image
+    , registryLogin
+    , registryUsername
+    , registryPassword
+    , autoPull
+    , detach
+    , persist
+    , containerName
+    , network
+    , runArgs
+    , mount
+    , mountMode
+    , env
+    , stackExe
+    , setUser
+    , requireDockerVersion
+    }
  where
   emptyToNothing Nothing = Nothing
   emptyToNothing (Just s)
src/Stack/Config/Nix.hs view
@@ -1,6 +1,7 @@-{-# LANGUAGE NoImplicitPrelude  #-}
-{-# LANGUAGE OverloadedStrings  #-}
-{-# LANGUAGE RecordWildCards    #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- | Nix configuration
 module Stack.Config.Nix
@@ -46,22 +47,23 @@   => NixOptsMonoid
   -> OS
   -> RIO env NixOpts
-nixOptsFromMonoid NixOptsMonoid{..} os = do
+nixOptsFromMonoid nixMonoid os = do
   let defaultPure = case os of
         OSX -> False
         _ -> True
-      nixPureShell = fromFirst defaultPure nixMonoidPureShell
-      nixPackages = fromFirst [] nixMonoidPackages
-      nixInitFile = getFirst nixMonoidInitFile
-      nixShellOptions = fromFirst [] nixMonoidShellOptions
-                        ++ prefixAll (T.pack "-I") (fromFirst [] nixMonoidPath)
-      nixAddGCRoots   = fromFirstFalse nixMonoidAddGCRoots
+      pureShell = fromFirst defaultPure nixMonoid.pureShell
+      packages = fromFirst [] nixMonoid.packages
+      initFile = getFirst nixMonoid.initFile
+      shellOptions =
+           fromFirst [] nixMonoid.shellOptions
+        ++ prefixAll (T.pack "-I") (fromFirst [] nixMonoid.path)
+      addGCRoots   = fromFirstFalse nixMonoid.addGCRoots
 
   -- Enable Nix-mode by default on NixOS, unless Docker-mode was specified
   osIsNixOS <- isNixOS
-  let nixEnable0 = fromFirst osIsNixOS nixMonoidEnable
+  let nixEnable0 = fromFirst osIsNixOS nixMonoid.enable
 
-  nixEnable <-
+  enable <-
     if nixEnable0 && osIsWindows
       then do
         prettyNoteS
@@ -69,9 +71,16 @@         pure False
       else pure nixEnable0
 
-  when (not (null nixPackages) && isJust nixInitFile) $
+  when (not (null packages) && isJust initFile) $
     throwIO NixCannotUseShellFileAndPackagesException
-  pure NixOpts{..}
+  pure NixOpts
+    { enable
+    , pureShell
+    , packages
+    , initFile
+    , shellOptions
+    , addGCRoots
+    }
  where
   prefixAll p (x:xs) = p : x : prefixAll p xs
   prefixAll _ _      = []
src/Stack/ConfigCmd.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE NoImplicitPrelude   #-}
 {-# LANGUAGE GADTs               #-}
 {-# LANGUAGE OverloadedLists     #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 {-# LANGUAGE OverloadedStrings   #-}
 
 -- | Make changes to project or global configuration.
@@ -22,7 +23,6 @@                    , takeWhile
                    )
 import qualified Data.Map.Merge.Strict as Map
-import qualified Data.List.NonEmpty as NE
 import qualified Data.Text as T
 import qualified Data.Yaml as Yaml
 import qualified Options.Applicative as OA
@@ -31,6 +31,8 @@ import           Pantry ( loadSnapshot )
 import           Path ( (</>), parent )
 import qualified RIO.Map as Map
+import           RIO.NonEmpty ( nonEmpty )
+import qualified RIO.NonEmpty as NE
 import           RIO.Process ( envVarsL )
 import           Stack.Config
                    ( makeConcreteResolver, getProjectConfig
@@ -62,7 +64,8 @@     ++ "'config' command used when no project configuration available."
 
 data ConfigCmdSet
-  = ConfigCmdSetResolver !(Unresolved AbstractResolver)
+  = ConfigCmdSetSnapshot !(Unresolved AbstractResolver)
+  | ConfigCmdSetResolver !(Unresolved AbstractResolver)
   | ConfigCmdSetSystemGhc !CommandScope !Bool
   | ConfigCmdSetInstallGhc !CommandScope !Bool
   | ConfigCmdSetDownloadPrefix !CommandScope !Text
@@ -75,6 +78,7 @@     -- ^ Apply changes to the project @stack.yaml@.
 
 configCmdSetScope :: ConfigCmdSet -> CommandScope
+configCmdSetScope (ConfigCmdSetSnapshot _) = CommandScopeProject
 configCmdSetScope (ConfigCmdSetResolver _) = CommandScopeProject
 configCmdSetScope (ConfigCmdSetSystemGhc scope _) = scope
 configCmdSetScope (ConfigCmdSetInstallGhc scope _) = scope
@@ -88,7 +92,7 @@   configFilePath <-
     case configCmdSetScope cmd of
       CommandScopeProject -> do
-        mstackYamlOption <- view $ globalOptsL.to globalStackYaml
+        mstackYamlOption <- view $ globalOptsL . to (.stackYaml)
         mstackYaml <- getProjectConfig mstackYamlOption
         case mstackYaml of
           PCProject stackYaml -> pure stackYaml
@@ -96,7 +100,7 @@             fmap (</> stackDotYaml) (getImplicitGlobalProjectDir conf)
           PCNoProject _extraDeps -> throwIO NoProjectConfigAvailable
           -- maybe modify the ~/.stack/config.yaml file instead?
-      CommandScopeGlobal -> pure (configUserConfigPath conf)
+      CommandScopeGlobal -> pure conf.userConfigPath
   rawConfig <- liftIO (readFileUtf8 (toFilePath configFilePath))
   config <- either throwM pure (Yaml.decodeEither' $ encodeUtf8 rawConfig)
   newValue <- cfgCmdSetValue (parent configFilePath) cmd
@@ -130,20 +134,22 @@   --   key2:
   --     key3: value
   --
-  writeLines yamlLines spaces cmdKeys value = case NE.tail cmdKeys of
-    [] -> yamlLines <> [spaces <> NE.head cmdKeys <> ": " <> value]
-    ks -> writeLines (yamlLines <> [spaces <> NE.head cmdKeys <> ":"])
-                     (spaces <> "  ")
-                     (NE.fromList ks)
-                     value
+  writeLines yamlLines spaces cmdKeys value =
+    case nonEmpty $ NE.tail cmdKeys of
+      Nothing -> yamlLines <> [spaces <> NE.head cmdKeys <> ": " <> value]
+      Just ks -> writeLines
+                   (yamlLines <> [spaces <> NE.head cmdKeys <> ":"])
+                   (spaces <> "  ")
+                   ks
+                   value
 
   inConfig v cmdKeys = case v of
     Yaml.Object obj ->
       case KeyMap.lookup (Key.fromText (NE.head cmdKeys)) obj of
         Nothing -> Nothing
-        Just v' -> case NE.tail cmdKeys of
-          [] -> Just v'
-          ks -> inConfig v' (NE.fromList ks)
+        Just v' -> case nonEmpty $ NE.tail cmdKeys of
+          Nothing -> Just v'
+          Just ks -> inConfig v' ks
     _ -> Nothing
 
   switchLine file cmdKey _ searched [] = do
@@ -220,18 +226,27 @@      (HasConfig env, HasGHCVariant env)
   => Path Abs Dir -- ^ root directory of project
   -> ConfigCmdSet -> RIO env Yaml.Value
-cfgCmdSetValue root (ConfigCmdSetResolver newResolver) = do
-  newResolver' <- resolvePaths (Just root) newResolver
-  concreteResolver <- makeConcreteResolver newResolver'
-  -- Check that the snapshot actually exists
-  void $ loadSnapshot =<< completeSnapshotLocation concreteResolver
-  pure (Yaml.toJSON concreteResolver)
+cfgCmdSetValue root (ConfigCmdSetSnapshot newSnapshot) =
+  snapshotValue root newSnapshot
+cfgCmdSetValue root (ConfigCmdSetResolver newSnapshot) =
+  snapshotValue root newSnapshot
 cfgCmdSetValue _ (ConfigCmdSetSystemGhc _ bool') = pure $ Yaml.Bool bool'
 cfgCmdSetValue _ (ConfigCmdSetInstallGhc _ bool') = pure $ Yaml.Bool bool'
 cfgCmdSetValue _ (ConfigCmdSetDownloadPrefix _ url) = pure $ Yaml.String url
 
+snapshotValue ::
+     HasConfig env
+  => Path Abs Dir -- ^ root directory of project
+  -> Unresolved AbstractResolver -> RIO env Yaml.Value
+snapshotValue root snapshot = do
+  snapshot' <- resolvePaths (Just root) snapshot
+  concreteSnapshot <- makeConcreteResolver snapshot'
+  -- Check that the snapshot actually exists
+  void $ loadSnapshot =<< completeSnapshotLocation concreteSnapshot
+  pure (Yaml.toJSON concreteSnapshot)
 
 cfgCmdSetKeys :: ConfigCmdSet -> NonEmpty Text
+cfgCmdSetKeys (ConfigCmdSetSnapshot _) = ["snapshot"]
 cfgCmdSetKeys (ConfigCmdSetResolver _) = ["resolver"]
 cfgCmdSetKeys (ConfigCmdSetSystemGhc _ _) = [configMonoidSystemGHCName]
 cfgCmdSetKeys (ConfigCmdSetInstallGhc _ _) = [configMonoidInstallGHCName]
@@ -251,15 +266,24 @@ configCmdSetParser =
   OA.hsubparser $
     mconcat
-      [ OA.command "resolver"
+      [ OA.command "snapshot"
           ( OA.info
+              (   ConfigCmdSetSnapshot
+              <$> OA.argument
+                    readAbstractResolver
+                    (  OA.metavar "SNAPSHOT"
+                    <> OA.help "E.g. \"nightly\" or \"lts-22.8\"" ))
+              ( OA.progDesc
+                  "Change the snapshot of the current project." ))
+      , OA.command "resolver"
+          ( OA.info
               (   ConfigCmdSetResolver
               <$> OA.argument
                     readAbstractResolver
                     (  OA.metavar "SNAPSHOT"
-                    <> OA.help "E.g. \"nightly\" or \"lts-7.2\"" ))
+                    <> OA.help "E.g. \"nightly\" or \"lts-22.8\"" ))
               ( OA.progDesc
-                  "Change the resolver of the current project." ))
+                  "Change the resolver key of the current project." ))
       , OA.command (T.unpack configMonoidSystemGHCName)
           ( OA.info
               (   ConfigCmdSetSystemGhc
@@ -344,7 +368,7 @@ cfgCmdEnv :: EnvSettings -> RIO EnvConfig ()
 cfgCmdEnv es = do
   origEnv <- liftIO $ Map.fromList . map (first fromString) <$> getEnvironment
-  mkPC <- view $ configL.to configProcessContextSettings
+  mkPC <- view $ configL . to (.processContextSettings)
   pc <- liftIO $ mkPC es
   let newEnv = pc ^. envVarsL
       actions = Map.merge
src/Stack/Constants.hs view
@@ -12,6 +12,7 @@   , haskellDefaultPreprocessorExts
   , stackProgName
   , stackProgName'
+  , nixProgName
   , stackDotYaml
   , stackWorkEnvVar
   , stackRootEnvVar
@@ -51,6 +52,7 @@   , relDirGhciScript
   , relDirPantry
   , relDirPrograms
+  , relDirRoot
   , relDirUpperPrograms
   , relDirStackProgName
   , relDirStackWork
@@ -70,6 +72,7 @@   , relDirLoadedSnapshotCache
   , bindirSuffix
   , docDirSuffix
+  , htmlDirSuffix
   , relDirHpc
   , relDirLib
   , relDirShare
@@ -137,10 +140,12 @@   , testGhcEnvRelFile
   , relFileBuildLock
   , stackDeveloperModeDefault
+  , isStackUploadDisabled
   , globalFooter
   , gitHubBasicAuthType
   , gitHubTokenEnvVar
   , altGitHubTokenEnvVar
+  , hackageBaseUrl
   ) where
 
 import           Data.ByteString.Builder ( byteString )
@@ -173,6 +178,10 @@ stackProgName' :: Text
 stackProgName' = T.pack stackProgName
 
+-- | Name of the Nix package manager command
+nixProgName :: String
+nixProgName = "nix"
+
 -- | Extensions used for Haskell modules. Excludes preprocessor ones.
 haskellFileExts :: [Text]
 haskellFileExts = ["hs", "hsc", "lhs"]
@@ -244,7 +253,7 @@     [ "ghc-prim"
       -- A magic package
     , "integer-gmp"
-      -- No longer magic > 1.0.3.0. With GHC 9.4.7 at least, there seems to be
+      -- No longer magic > 1.0.3.0. With GHC 9.6.4 at least, there seems to be
       -- no problem in using it.
     , "integer-simple"
       -- A magic package
@@ -256,16 +265,16 @@       -- A magic package
     , "dph-seq"
       -- Deprecated in favour of dph-prim-seq, which does not appear to be
-      -- magic. With GHC 9.4.7 at least, there seems to be no problem in using
+      -- magic. With GHC 9.6.4 at least, there seems to be no problem in using
       -- it.
     , "dph-par"
       --  Deprecated in favour of dph-prim-par, which does not appear to be
-      -- magic. With GHC 9.4.7 at least, there seems to be no problem in using
+      -- magic. With GHC 9.6.4 at least, there seems to be no problem in using
       -- it.
     , "ghc"
       -- A magic package
     , "interactive"
-      -- Could not identify information about this package name. With GHC 9.4.7
+      -- Could not identify information about this package name. With GHC 9.6.4
       -- at least, there seems to be no problem in using it.
     , "ghc-bignum"
       -- A magic package
@@ -388,6 +397,9 @@ relDirPrograms :: Path Rel Dir
 relDirPrograms = $(mkRelDir "programs")
 
+relDirRoot :: Path Rel Dir
+relDirRoot = $(mkRelDir ".")
+
 relDirUpperPrograms :: Path Rel Dir
 relDirUpperPrograms = $(mkRelDir "Programs")
 
@@ -447,6 +459,10 @@ docDirSuffix :: Path Rel Dir
 docDirSuffix = $(mkRelDir "doc")
 
+-- | Suffix applied to a path to get the @html@ directory.
+htmlDirSuffix :: Path Rel Dir
+htmlDirSuffix = $(mkRelDir "html")
+
 relDirHpc :: Path Rel Dir
 relDirHpc = $(mkRelDir "hpc")
 
@@ -666,6 +682,10 @@ stackDeveloperModeDefault :: Bool
 stackDeveloperModeDefault = STACK_DEVELOPER_MODE_DEFAULT
 
+-- | What should the default be for stack-developer-mode
+isStackUploadDisabled :: Bool
+isStackUploadDisabled = STACK_DISABLE_STACK_UPLOAD
+
 -- | The footer to the help for Stack's subcommands
 globalFooter :: String
 globalFooter =
@@ -684,3 +704,6 @@ -- \'Basic\' authentication.
 altGitHubTokenEnvVar :: String
 altGitHubTokenEnvVar = "GITHUB_TOKEN"
+
+hackageBaseUrl :: Text
+hackageBaseUrl = "https://hackage.haskell.org/"
src/Stack/Coverage.hs view
@@ -1,5 +1,8 @@ {-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
 {-# LANGUAGE LambdaCase            #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 {-# LANGUAGE OverloadedStrings     #-}
 
 -- | Generate HPC (Haskell Program Coverage) reports
@@ -40,19 +43,21 @@                    , relFileHpcIndexHtml, relFileIndexHtml
                    )
 import           Stack.Constants.Config ( distDirFromDir, hpcRelativeDir )
+import           Stack.Package ( hasBuildableMainLibrary )
 import           Stack.Prelude
 import           Stack.Runners ( ShouldReexec (..), withConfig, withEnvConfig )
 import           Stack.Types.BuildConfig
                    ( BuildConfig (..), HasBuildConfig (..) )
 import           Stack.Types.Compiler ( getGhcVersion )
-import           Stack.Types.BuildOpts ( BuildOptsCLI (..), defaultBuildOptsCLI )
+import           Stack.Types.CompCollection ( getBuildableSetText )
+import           Stack.Types.BuildOptsCLI
+                   ( BuildOptsCLI (..), defaultBuildOptsCLI )
 import           Stack.Types.EnvConfig
                    ( EnvConfig (..), HasEnvConfig (..), actualCompilerVersionL
                    , hpcReportDir
                    )
 import           Stack.Types.NamedComponent ( NamedComponent (..) )
-import           Stack.Types.Package
-                   ( Package (..), PackageLibraries (..), packageIdentifier )
+import           Stack.Types.Package ( Package (..), packageIdentifier )
 import           Stack.Types.Runner ( Runner )
 import           Stack.Types.SourceMap
                    ( PackageType (..), SMTargets (..), SMWanted (..)
@@ -77,7 +82,7 @@     <> fillSep
          [ flow "Can't specify anything except test-suites as hpc report \
                 \targets"
-         , parens (style Target . fromString . packageNameString $ name)
+         , parens (style Target . fromPackageName $ name)
          , flow "is used with a non test-suite target."
          ]
   pretty NoTargetsOrTixSpecified =
@@ -90,7 +95,7 @@     <> line
     <> fillSep
          [ flow "Expected a local package, but"
-         , style Target . fromString . packageNameString $ name
+         , style Target . fromPackageName $ name
          , flow "is either an extra-dep or in the snapshot."
          ]
 
@@ -98,10 +103,10 @@ 
 -- | Type representing command line options for the @stack hpc report@ command.
 data HpcReportOpts = HpcReportOpts
-  { hroptsInputs :: [Text]
-  , hroptsAll :: Bool
-  , hroptsDestDir :: Maybe String
-  , hroptsOpenBrowser :: Bool
+  { inputs :: [Text]
+  , all :: Bool
+  , destDir :: Maybe String
+  , openBrowser :: Bool
   }
   deriving Show
 
@@ -109,9 +114,9 @@ hpcReportCmd :: HpcReportOpts -> RIO Runner ()
 hpcReportCmd hropts = do
   let (tixFiles, targetNames) =
-        L.partition (".tix" `T.isSuffixOf`) (hroptsInputs hropts)
+        L.partition (".tix" `T.isSuffixOf`) hropts.inputs
       boptsCLI = defaultBuildOptsCLI
-        { boptsCLITargets = if hroptsAll hropts then [] else targetNames }
+        { targetsCLI = if hropts.all then [] else targetNames }
   withConfig YesReexec $ withEnvConfig AllowNoTargets boptsCLI $
     generateHpcReportForTargets hropts tixFiles targetNames
 
@@ -178,19 +183,16 @@   compilerVersion <- view actualCompilerVersionL
   -- If we're using > GHC 7.10, the hpc 'include' parameter must specify a ghc package key. See
   -- https://github.com/commercialhaskell/stack/issues/785
-  let pkgId = packageIdentifierString (packageIdentifier package)
-      pkgName' = packageNameString $ packageName package
+  let pkgId = packageIdentifierString $ packageIdentifier package
+      pkgName' = packageNameString package.name
       ghcVersion = getGhcVersion compilerVersion
-      hasLibrary =
-        case packageLibraries package of
-          NoLibraries -> False
-          HasLibraries _ -> True
-      internalLibs = packageInternalLibraries package
+      hasLibrary = hasBuildableMainLibrary package
+      subLibs = package.subLibraries
   eincludeName <-
     -- Pre-7.8 uses plain PKG-version in tix files.
     if ghcVersion < mkVersion [7, 10] then pure $ Right $ Just [pkgId]
     -- We don't expect to find a package key if there is no library.
-    else if not hasLibrary && Set.null internalLibs then pure $ Right Nothing
+    else if not hasLibrary && null subLibs then pure $ Right Nothing
     -- Look in the inplace DB for the package key.
     -- See https://github.com/commercialhaskell/stack/issues/1181#issuecomment-148968986
     else do
@@ -201,7 +203,7 @@         findPackageFieldForBuiltPackage
           pkgDir
           (packageIdentifier package)
-          internalLibs
+          (getBuildableSetText subLibs)
           hpcNameField
       case eincludeName of
         Left err -> do
@@ -209,7 +211,7 @@           pure $ Left err
         Right includeNames -> pure $ Right $ Just $ map T.unpack includeNames
   forM_ tests $ \testName -> do
-    tixSrc <- tixFilePath (packageName package) (T.unpack testName)
+    tixSrc <- tixFilePath package.name (T.unpack testName)
     let report = fillSep
           [ flow "coverage report for"
           , style Current (fromString pkgName') <> "'s"
@@ -248,104 +250,113 @@   -> [String]
   -> [String]
   -> RIO env (Maybe (Path Abs File))
-generateHpcReportInternal tixSrc reportDir report reportHtml extraMarkupArgs extraReportArgs = do
-  -- If a .tix file exists, move it to the HPC output directory and generate a
-  -- report for it.
-  tixFileExists <- doesFileExist tixSrc
-  if not tixFileExists
-    then do
-      prettyError $
-        "[S-4634]"
-        <> line
-        <> flow "Didn't find"
-        <> style File ".tix"
-        <> "for"
-        <> report
-        <> flow "- expected to find it at"
-        <> pretty tixSrc <> "."
-      pure Nothing
-    else (`catch` \(err :: ProcessException) -> do
-           logError $ displayShow err
-           generateHpcErrorReport reportDir $ display $ sanitize $
-               displayException err
-           pure Nothing) $
-       (`onException`
-           prettyError
-             ( "[S-8215]"
-               <> line
-               <> flow "Error occurred while producing"
-               <> report <> "."
-             )) $ do
-      -- Directories for .mix files.
-      hpcRelDir <- hpcRelativeDir
-      -- Compute arguments used for both "hpc markup" and "hpc report".
-      pkgDirs <- view $ buildConfigL.to (map ppRoot . Map.elems . smwProject . bcSMWanted)
-      let args =
-            -- Use index files from all packages (allows cross-package coverage results).
-            concatMap (\x -> ["--srcdir", toFilePathNoTrailingSep x]) pkgDirs ++
-            -- Look for index files in the correct dir (relative to each pkgdir).
-            ["--hpcdir", toFilePathNoTrailingSep hpcRelDir, "--reset-hpcdirs"]
-      prettyInfoL
-        [ "Generating"
-        , report <> "."
-        ]
-      -- Strip @\r@ characters because Windows.
-      outputLines <- map (L8.filter (/= '\r')) . L8.lines . fst <$>
-        proc "hpc"
-        ( "report"
-        : toFilePath tixSrc
-        : (args ++ extraReportArgs)
-        )
-        readProcess_
-      if all ("(0/0)" `L8.isSuffixOf`) outputLines
+generateHpcReportInternal
+    tixSrc
+    reportDir
+    report
+    reportHtml
+    extraMarkupArgs
+    extraReportArgs
+  = do
+      -- If a .tix file exists, move it to the HPC output directory and generate
+      -- a report for it.
+      tixFileExists <- doesFileExist tixSrc
+      if not tixFileExists
         then do
-          let msgHtml =
-                   "Error: [S-6829]\n\
-                   \The "
-                <> display reportHtml
-                <> " did not consider any code. One possible cause of this is \
-                   \if your test-suite builds the library code (see Stack \
-                   \<a href='https://github.com/commercialhaskell/stack/issues/1008'>\
-                   \issue #1008\
-                   \</a>\
-                   \). It may also indicate a bug in Stack or the hpc program. \
-                   \Please report this issue if you think your coverage report \
-                   \should have meaningful results."
           prettyError $
-            "[S-6829]"
+            "[S-4634]"
             <> line
-            <> fillSep
-                 [ "The"
-                 , report
-                 , flow "did not consider any code. One possible cause of this \
-                        \is if your test-suite builds the library code (see \
-                        \Stack issue #1008). It may also indicate a bug in \
-                        \Stack or the hpc program. Please report this issue if \
-                        \you think your coverage report should have meaningful \
-                        \results."
-                 ]
-          generateHpcErrorReport reportDir msgHtml
+            <> flow "Didn't find"
+            <> style File ".tix"
+            <> "for"
+            <> report
+            <> flow "- expected to find it at"
+            <> pretty tixSrc <> "."
           pure Nothing
-        else do
-          let reportPath = reportDir </> relFileHpcIndexHtml
-          -- Print the summary report to the standard output stream.
-          putUtf8Builder =<< displayWithColor
-            (  fillSep
-                 [ "Summary"
-                 , report <> ":"
-                 ]
-            <> line
-            )
-          forM_ outputLines putStrLn
-          -- Generate the HTML markup.
-          void $ proc "hpc"
-            ( "markup"
+        else (`catch` \(err :: ProcessException) -> do
+               logError $ displayShow err
+               generateHpcErrorReport reportDir $ display $ sanitize $
+                   displayException err
+               pure Nothing) $
+           (`onException`
+               prettyError
+                 ( "[S-8215]"
+                   <> line
+                   <> flow "Error occurred while producing"
+                   <> report <> "."
+                 )) $ do
+          -- Directories for .mix files.
+          hpcRelDir <- hpcRelativeDir
+          -- Compute arguments used for both "hpc markup" and "hpc report".
+          pkgDirs <- view $ buildConfigL . to
+            (map ppRoot . Map.elems . (.smWanted.project))
+          let args =
+                -- Use index files from all packages (allows cross-package
+                -- coverage results).
+                concatMap (\x -> ["--srcdir", toFilePathNoTrailingSep x]) pkgDirs ++
+                -- Look for index files in the correct dir (relative to each pkgdir).
+                ["--hpcdir", toFilePathNoTrailingSep hpcRelDir, "--reset-hpcdirs"]
+          prettyInfoL
+            [ "Generating"
+            , report <> "."
+            ]
+          -- Strip @\r@ characters because Windows.
+          outputLines <- map (L8.filter (/= '\r')) . L8.lines . fst <$>
+            proc "hpc"
+            ( "report"
             : toFilePath tixSrc
-            : ("--destdir=" ++ toFilePathNoTrailingSep reportDir)
-            : (args ++ extraMarkupArgs)
+            : (args ++ extraReportArgs)
             )
             readProcess_
-          pure (Just reportPath)
+          if all ("(0/0)" `L8.isSuffixOf`) outputLines
+            then do
+              let msgHtml =
+                       "Error: [S-6829]\n\
+                       \The "
+                    <> display reportHtml
+                    <> " did not consider any code. One possible cause of this is \
+                       \if your test-suite builds the library code (see Stack \
+                       \<a href='https://github.com/commercialhaskell/stack/issues/1008'>\
+                       \issue #1008\
+                       \</a>\
+                       \). It may also indicate a bug in Stack or the hpc program. \
+                       \Please report this issue if you think your coverage report \
+                       \should have meaningful results."
+              prettyError $
+                "[S-6829]"
+                <> line
+                <> fillSep
+                     [ "The"
+                     , report
+                     , flow "did not consider any code. One possible cause of this \
+                            \is if your test-suite builds the library code (see \
+                            \Stack issue #1008). It may also indicate a bug in \
+                            \Stack or the hpc program. Please report this issue if \
+                            \you think your coverage report should have meaningful \
+                            \results."
+                     ]
+              generateHpcErrorReport reportDir msgHtml
+              pure Nothing
+            else do
+              let reportPath = reportDir </> relFileHpcIndexHtml
+              -- Print the summary report to the standard output stream.
+              putUtf8Builder =<< displayWithColor
+                (  fillSep
+                     [ "Summary"
+                     , report <> ":"
+                     ]
+                <> line
+                )
+              forM_ outputLines putStrLn
+              -- Generate the HTML markup.
+              void $ proc "hpc"
+                ( "markup"
+                : toFilePath tixSrc
+                : ("--destdir=" ++ toFilePathNoTrailingSep reportDir)
+                : (args ++ extraMarkupArgs)
+                )
+                readProcess_
+              pure (Just reportPath)
 
 generateHpcReportForTargets :: HasEnvConfig env
                             => HpcReportOpts -> [Text] -> [Text] -> RIO env ()
@@ -353,10 +364,10 @@   targetTixFiles <-
     -- When there aren't any package component arguments, and --all
     -- isn't passed, default to not considering any targets.
-    if not (hroptsAll opts) && null targetNames
+    if not opts.all && null targetNames
     then pure []
     else do
-      when (hroptsAll opts && not (null targetNames)) $
+      when (opts.all && not (null targetNames)) $
         prettyWarnL
           $ "Since"
           : style Shell "--all"
@@ -364,7 +375,7 @@           : mkNarrativeList (Just Target) False
               (map (fromString . T.unpack) targetNames :: [StyleDoc])
       targets <-
-        view $ envConfigL.to envConfigSourceMap.to smTargets.to smtTargets
+        view $ envConfigL . to (.sourceMap.targets.targets)
       fmap concat $ forM (Map.toList targets) $ \(name, target) ->
         case target of
           TargetAll PTDependency -> prettyThrowIO $ NotLocalPackage name
@@ -394,7 +405,7 @@     mapM (resolveFile' . T.unpack) tixFiles
   when (null tixPaths) $ prettyThrowIO NoTargetsOrTixSpecified
   outputDir <- hpcReportDir
-  reportDir <- case hroptsDestDir opts of
+  reportDir <- case opts.destDir of
     Nothing -> pure (outputDir </> relDirCombined </> relDirCustom)
     Just destDir -> do
       dest <- resolveDir' destDir
@@ -404,7 +415,7 @@       reportHtml = "combined coverage report"
   mreportPath <- generateUnionReport report reportHtml reportDir tixPaths
   forM_ mreportPath $ \reportPath ->
-    if hroptsOpenBrowser opts
+    if opts.openBrowser
       then do
         prettyInfo $ "Opening" <+> pretty reportPath <+> "in the browser."
         void $ liftIO $ openBrowser (toFilePath reportPath)
@@ -415,11 +426,12 @@   outputDir <- hpcReportDir
   ensureDir outputDir
   (dirs, _) <- listDir outputDir
-  tixFiles0 <- fmap (concat . concat) $ forM (filter (("combined" /=) . dirnameString) dirs) $ \dir -> do
-    (dirs', _) <- listDir dir
-    forM dirs' $ \dir' -> do
-      (_, files) <- listDir dir'
-      pure (filter ((".tix" `L.isSuffixOf`) . toFilePath) files)
+  tixFiles0 <-
+    fmap (concat . concat) $ forM (filter (("combined" /=) . dirnameString) dirs) $ \dir -> do
+      (dirs', _) <- listDir dir
+      forM dirs' $ \dir' -> do
+        (_, files) <- listDir dir'
+        pure (filter ((".tix" `L.isSuffixOf`) . toFilePath) files)
   extraTixFiles <- findExtraTixFiles
   let tixFiles = tixFiles0  ++ extraTixFiles
       reportDir = outputDir </> relDirCombined </> relDirAll
@@ -463,7 +475,8 @@          ]
     <> line
     <> bulletedList (map fromString errs :: [StyleDoc])
-  tixDest <- (reportDir </>) <$> parseRelFile (dirnameString reportDir ++ ".tix")
+  tixDest <-
+    (reportDir </>) <$> parseRelFile (dirnameString reportDir ++ ".tix")
   ensureDir (parent tixDest)
   liftIO $ writeTix (toFilePath tixDest) tix
   generateHpcReportInternal tixDest reportDir report reportHtml [] []
@@ -488,10 +501,11 @@            ]
   pure mtix
 
--- | Module names which contain '/' have a package name, and so they weren't built into the
--- executable.
+-- | Module names which contain '/' have a package name, and so they weren't
+-- built into the executable.
 removeExeModules :: Tix -> Tix
-removeExeModules (Tix ms) = Tix (filter (\(TixModule name _ _ _) -> '/' `elem` name) ms)
+removeExeModules (Tix ms) =
+  Tix (filter (\(TixModule name _ _ _) -> '/' `elem` name) ms)
 
 unionTixes :: [Tix] -> ([String], Tix)
 unionTixes tixes = (Map.keys errs, Tix (Map.elems outputs))
@@ -500,7 +514,8 @@   toMap (Tix ms) = Map.fromList (map (\x@(TixModule k _ _ _) -> (k, Right x)) ms)
   merge (Right (TixModule k hash1 len1 tix1))
       (Right (TixModule _ hash2 len2 tix2))
-    | hash1 == hash2 && len1 == len2 = Right (TixModule k hash1 len1 (zipWith (+) tix1 tix2))
+    | hash1 == hash2 && len1 == len2 =
+        Right (TixModule k hash1 len1 (zipWith (+) tix1 tix2))
   merge _ _ = Left ()
 
 generateHpcMarkupIndex :: HasEnvConfig env => RIO env ()
@@ -509,7 +524,7 @@   let outputFile = outputDir </> relFileIndexHtml
   ensureDir outputDir
   (dirs, _) <- listDir outputDir
-  rows <- fmap (catMaybes . concat) $ forM dirs $ \dir -> do
+  rows <- fmap (concatMap catMaybes) $ forM dirs $ \dir -> do
     (subdirs, _) <- listDir dir
     forM subdirs $ \subdir -> do
       let indexPath = subdir </> relFileHpcIndexHtml
@@ -593,7 +608,7 @@      HasEnvConfig env
   => Path Abs Dir -> PackageIdentifier -> Set.Set Text -> Text
   -> RIO env (Either Text [Text])
-findPackageFieldForBuiltPackage pkgDir pkgId internalLibs field = do
+findPackageFieldForBuiltPackage pkgDir pkgId subLibs field = do
   distDir <- distDirFromDir pkgDir
   let inplaceDir = distDir </> relDirPackageConfInplace
       pkgIdStr = packageIdentifierString pkgId
@@ -613,12 +628,13 @@   logDebug $ displayShow files
   -- From all the files obtained from the scanning process above, we need to
   -- identify which are .conf files and then ensure that there is at most one
-  -- .conf file for each library and internal library (some might be missing if
-  -- that component has not been built yet). We should error if there are more
-  -- than one .conf file for a component or if there are no .conf files at all
-  -- in the searched location.
+  -- .conf file for each library and sub-library (some might be missing if that
+  -- component has not been built yet). We should error if there are more than
+  -- one .conf file for a component or if there are no .conf files at all in the
+  -- searched location.
   let toFilename = T.pack . toFilePath . filename
-      -- strip known prefix and suffix from the found files to determine only the conf files
+      -- strip known prefix and suffix from the found files to determine only
+      -- the .conf files
       stripKnown =
         T.stripSuffix ".conf" <=< T.stripPrefix (T.pack (pkgIdStr ++ "-"))
       stripped =
@@ -629,7 +645,7 @@         in  if T.null z then "" else T.tail z
       matchedComponents = map (\(n, f) -> (stripHash n, [f])) stripped
       byComponents =
-        Map.restrictKeys (Map.fromListWith (++) matchedComponents) $ Set.insert "" internalLibs
+        Map.restrictKeys (Map.fromListWith (++) matchedComponents) $ Set.insert "" subLibs
   logDebug $ displayShow byComponents
   if Map.null $ Map.filter (\fs -> length fs > 1) byComponents
     then case concat $ Map.elems byComponents of
src/Stack/DefaultColorWhen.hs view
@@ -6,7 +6,7 @@ 
 import           Stack.Prelude ( stdout )
 import           Stack.Types.ColorWhen ( ColorWhen (..) )
-import           System.Console.ANSI ( hSupportsANSI )
+import           System.Console.ANSI ( hNowSupportsANSI )
 import           System.Environment ( lookupEnv )
 
 -- | The default adopts the standard proposed at http://no-color.org/, that
@@ -15,6 +15,6 @@ defaultColorWhen :: IO ColorWhen
 defaultColorWhen = lookupEnv "NO_COLOR" >>= \case
   Just _ -> pure ColorNever
-  _ -> hSupportsANSI stdout >>= \case
+  _ -> hNowSupportsANSI stdout >>= \case
     False -> pure ColorNever
     _ -> pure ColorAuto
+ src/Stack/DependencyGraph.hs view
@@ -0,0 +1,355 @@+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
+
+-- | Module exporting a function to create a pruned dependency graph given a
+-- 'DotOpts' value.
+module Stack.DependencyGraph
+  ( createPrunedDependencyGraph
+  , resolveDependencies
+  , pruneGraph
+  ) where
+
+import qualified Data.Foldable as F
+import qualified Data.Set as Set
+import qualified Data.Map as Map
+import qualified Data.Traversable as T
+import           Distribution.License ( License (..) )
+import qualified Distribution.PackageDescription as PD
+import           Distribution.Types.PackageName ( mkPackageName )
+import           Path ( parent )
+import           Stack.Build ( loadPackage )
+import           Stack.Build.Installed ( getInstalled, toInstallMap )
+import           Stack.Build.Source
+                   ( loadCommonPackage, loadLocalPackage, loadSourceMap )
+import           Stack.Build.Target( NeedTargets (..), parseTargets )
+import           Stack.Package ( Package (..), setOfPackageDeps )
+import           Stack.Prelude hiding ( Display (..), pkgName, loadPackage )
+import qualified Stack.Prelude ( pkgName )
+import           Stack.Runners
+                   ( ShouldReexec (..), withBuildConfig, withConfig
+                   , withEnvConfig
+                   )
+import           Stack.SourceMap
+                   ( globalsFromHints, mkProjectPackage, pruneGlobals )
+import           Stack.Types.BuildConfig
+                   ( BuildConfig (..), HasBuildConfig (..) )
+import           Stack.Types.BuildOptsCLI
+                   ( BuildOptsCLI (..), defaultBuildOptsCLI )
+import           Stack.Types.BuildOptsMonoid
+                   ( buildOptsMonoidBenchmarksL, buildOptsMonoidTestsL )
+import           Stack.Types.Compiler ( wantedToActual )
+import           Stack.Types.DependencyTree ( DotPayload (..) )
+import           Stack.Types.DotConfig ( DotConfig (..) )
+import           Stack.Types.DotOpts ( DotOpts (..) )
+import           Stack.Types.DumpPackage ( DumpPackage (..) )
+import           Stack.Types.EnvConfig ( EnvConfig (..), HasSourceMap (..) )
+import           Stack.Types.GhcPkgId
+                   ( GhcPkgId, ghcPkgIdString, parseGhcPkgId )
+import           Stack.Types.GlobalOpts ( globalOptsBuildOptsMonoidL )
+import           Stack.Types.Package ( LocalPackage (..) )
+import           Stack.Types.Runner ( Runner, globalOptsL )
+import           Stack.Types.SourceMap
+                   ( CommonPackage (..), DepPackage (..), ProjectPackage (..)
+                   , SMActual (..), SMWanted (..), SourceMap (..)
+                   )
+
+-- | Type representing exceptions thrown by functions exported by the
+-- "Stack.DependencyGraph" module.
+data DependencyGraphException
+  = DependencyNotFoundBug GhcPkgId
+  | PackageNotFoundBug PackageName
+  deriving (Show, Typeable)
+
+instance Exception DependencyGraphException where
+  displayException (DependencyNotFoundBug depId) = bugReport "[S-7071]" $ concat
+    [ "Expected to find "
+    , ghcPkgIdString depId
+    , " in global DB."
+    ]
+  displayException (PackageNotFoundBug pkgName) = bugReport "[S-7151]" $ concat
+    [ "The '"
+    , packageNameString pkgName
+    , "' package was not found in any of the dependency sources."
+    ]
+
+-- | Create the dependency graph and also prune it as specified in the dot
+-- options. Returns a set of local names and a map from package names to
+-- dependencies.
+createPrunedDependencyGraph ::
+     DotOpts
+  -> RIO
+       Runner
+       (Set PackageName, Map PackageName (Set PackageName, DotPayload))
+createPrunedDependencyGraph dotOpts = withDotConfig dotOpts $ do
+  localNames <-
+    view $ buildConfigL . to (Map.keysSet . (.smWanted.project))
+  logDebug "Creating dependency graph"
+  resultGraph <- createDependencyGraph dotOpts
+  let pkgsToPrune = if dotOpts.includeBase
+                      then dotOpts.prune
+                      else Set.insert "base" dotOpts.prune
+      prunedGraph = pruneGraph localNames pkgsToPrune resultGraph
+  logDebug "Returning pruned dependency graph"
+  pure (localNames, prunedGraph)
+
+-- Plumbing for --test and --bench flags
+withDotConfig ::
+     DotOpts
+  -> RIO DotConfig a
+  -> RIO Runner a
+withDotConfig opts inner =
+  local (over globalOptsL modifyGO) $
+    if opts.globalHints
+      then withConfig NoReexec $ withBuildConfig withGlobalHints
+      else withConfig YesReexec withReal
+ where
+  withGlobalHints = do
+    buildConfig <- view buildConfigL
+    globals <- globalsFromHints buildConfig.smWanted.compiler
+    fakeGhcPkgId <- parseGhcPkgId "ignored"
+    actual <- either throwIO pure $
+      wantedToActual buildConfig.smWanted.compiler
+    let smActual = SMActual
+          { compiler = actual
+          , project = buildConfig.smWanted.project
+          , deps =  buildConfig.smWanted.deps
+          , globals = Map.mapWithKey toDump globals
+          }
+        toDump :: PackageName -> Version -> DumpPackage
+        toDump name version = DumpPackage
+          { ghcPkgId = fakeGhcPkgId
+          , packageIdent = PackageIdentifier name version
+          , sublib = Nothing
+          , license = Nothing
+          , libDirs = []
+          , libraries = []
+          , hasExposedModules = True
+          , exposedModules = mempty
+          , depends = []
+          , haddockInterfaces = []
+          , haddockHtml = Nothing
+          , isExposed = True
+          }
+        actualPkgs =
+          Map.keysSet smActual.deps <> Map.keysSet smActual.project
+        prunedActual = smActual
+          { globals = pruneGlobals smActual.globals actualPkgs }
+    targets <- parseTargets NeedTargets False boptsCLI prunedActual
+    logDebug "Loading source map"
+    sourceMap <- loadSourceMap targets boptsCLI smActual
+    let dc = DotConfig
+                { buildConfig
+                , sourceMap
+                , globalDump = toList smActual.globals
+                }
+    logDebug "DotConfig fully loaded"
+    runRIO dc inner
+
+  withReal = withEnvConfig NeedTargets boptsCLI $ do
+    envConfig <- ask
+    let sourceMap = envConfig.sourceMap
+    installMap <- toInstallMap sourceMap
+    (_, globalDump, _, _) <- getInstalled installMap
+    let dc = DotConfig
+          { buildConfig = envConfig.buildConfig
+          , sourceMap
+          , globalDump
+          }
+    runRIO dc inner
+
+  boptsCLI = defaultBuildOptsCLI
+    { targetsCLI = opts.dotTargets
+    , flags = opts.flags
+    }
+  modifyGO =
+    (if opts.testTargets
+       then set (globalOptsBuildOptsMonoidL . buildOptsMonoidTestsL) (Just True)
+       else id) .
+    (if opts.benchTargets
+       then set (globalOptsBuildOptsMonoidL . buildOptsMonoidBenchmarksL) (Just True)
+       else id)
+
+-- | Create the dependency graph, the result is a map from a package
+-- name to a tuple of dependencies and payload if available. This
+-- function mainly gathers the required arguments for
+-- @resolveDependencies@.
+createDependencyGraph ::
+     DotOpts
+  -> RIO DotConfig (Map PackageName (Set PackageName, DotPayload))
+createDependencyGraph dotOpts = do
+  sourceMap <- view sourceMapL
+  locals <- for (toList sourceMap.project) loadLocalPackage
+  let graph =
+        Map.fromList $ projectPackageDependencies dotOpts (filter (.wanted) locals)
+  globalDump <- view $ to (.globalDump)
+  -- TODO: Can there be multiple entries for wired-in-packages? If so,
+  -- this will choose one arbitrarily..
+  let globalDumpMap = Map.fromList $
+        map (\dp -> (Stack.Prelude.pkgName dp.packageIdent, dp)) globalDump
+      globalIdMap =
+        Map.fromList $ map ((.ghcPkgId) &&& (.packageIdent)) globalDump
+  let depLoader =
+        createDepLoader sourceMap globalDumpMap globalIdMap loadPackageDeps
+      loadPackageDeps name version loc flags ghcOptions cabalConfigOpts
+        -- Skip packages that can't be loaded - see
+        -- https://github.com/commercialhaskell/stack/issues/2967
+        | name `elem` [mkPackageName "rts", mkPackageName "ghc"] =
+            pure ( Set.empty
+                 , DotPayload (Just version) (Just $ Right BSD3) Nothing )
+        | otherwise =
+            fmap (setOfPackageDeps &&& makePayload loc)
+                 (loadPackage loc flags ghcOptions cabalConfigOpts)
+  resolveDependencies dotOpts.dependencyDepth graph depLoader
+ where
+  makePayload loc pkg = DotPayload (Just pkg.version)
+                                   (Just pkg.license)
+                                   (Just $ PLImmutable loc)
+
+-- | Resolve the direct (depth 0) external dependencies of the given local
+-- packages (assumed to come from project packages)
+projectPackageDependencies ::
+     DotOpts
+  -> [LocalPackage]
+  -> [(PackageName, (Set PackageName, DotPayload))]
+projectPackageDependencies dotOpts locals =
+  map (\lp -> let pkg = localPackageToPackage lp
+                  pkgDir = parent lp.cabalFP
+                  packageDepsSet = setOfPackageDeps pkg
+                  loc = PLMutable $ ResolvedPath (RelFilePath "N/A") pkgDir
+              in  (pkg.name, (deps pkg packageDepsSet, lpPayload pkg loc)))
+      locals
+ where
+  deps pkg packageDepsSet = if dotOpts.includeExternal
+    then Set.delete pkg.name packageDepsSet
+    else Set.intersection localNames packageDepsSet
+  localNames = Set.fromList $ map (.package.name) locals
+  lpPayload pkg loc =
+    DotPayload (Just pkg.version)
+               (Just pkg.license)
+               (Just loc)
+
+-- | Given a SourceMap and a dependency loader, load the set of dependencies for
+-- a package
+createDepLoader ::
+     SourceMap
+  -> Map PackageName DumpPackage
+  -> Map GhcPkgId PackageIdentifier
+  -> (  PackageName
+     -> Version
+     -> PackageLocationImmutable
+     -> Map FlagName Bool
+     -> [Text]
+     -> [Text]
+     -> RIO DotConfig (Set PackageName, DotPayload)
+     )
+  -> PackageName
+  -> RIO DotConfig (Set PackageName, DotPayload)
+createDepLoader sourceMap globalDumpMap globalIdMap loadPackageDeps pkgName =
+  fromMaybe (throwIO $ PackageNotFoundBug pkgName)
+    (projectPackageDeps <|> dependencyDeps <|> globalDeps)
+ where
+  projectPackageDeps = loadDeps <$> Map.lookup pkgName sourceMap.project
+   where
+    loadDeps pp = do
+      pkg <- loadCommonPackage pp.projectCommon
+      pure (setOfPackageDeps pkg, payloadFromLocal pkg Nothing)
+
+  dependencyDeps =
+    loadDeps <$> Map.lookup pkgName sourceMap.deps
+   where
+    loadDeps DepPackage{ location = PLMutable dir } = do
+      pp <- mkProjectPackage YesPrintWarnings dir False
+      pkg <- loadCommonPackage pp.projectCommon
+      pure (setOfPackageDeps pkg, payloadFromLocal pkg (Just $ PLMutable dir))
+
+    loadDeps dp@DepPackage{ location = PLImmutable loc } = do
+      let common = dp.depCommon
+      gpd <- liftIO common.gpd
+      let PackageIdentifier name version = PD.package $ PD.packageDescription gpd
+          flags = common.flags
+          ghcOptions = common.ghcOptions
+          cabalConfigOpts = common.cabalConfigOpts
+      assert
+        (pkgName == name)
+        (loadPackageDeps pkgName version loc flags ghcOptions cabalConfigOpts)
+
+  -- If package is a global package, use info from ghc-pkg (#4324, #3084)
+  globalDeps =
+    pure . getDepsFromDump <$> Map.lookup pkgName globalDumpMap
+   where
+    getDepsFromDump dump = (Set.fromList deps, payloadFromDump dump)
+     where
+      deps = map ghcIdToPackageName dump.depends
+      ghcIdToPackageName depId =
+        maybe (impureThrow $ DependencyNotFoundBug depId)
+              Stack.Prelude.pkgName
+              (Map.lookup depId globalIdMap)
+
+  payloadFromLocal pkg =
+    DotPayload (Just pkg.version) (Just pkg.license)
+
+  payloadFromDump dp =
+    DotPayload (Just $ pkgVersion dp.packageIdent)
+               (Right <$> dp.license)
+               Nothing
+
+-- | Resolve the dependency graph up to (Just depth) or until fixpoint is reached
+resolveDependencies ::
+     (Applicative m, Monad m)
+  => Maybe Int
+  -> Map PackageName (Set PackageName, DotPayload)
+  -> (PackageName -> m (Set PackageName, DotPayload))
+  -> m (Map PackageName (Set PackageName, DotPayload))
+resolveDependencies (Just 0) graph _ = pure graph
+resolveDependencies limit graph loadPackageDeps = do
+  let values = Set.unions (fst <$> Map.elems graph)
+      keys = Map.keysSet graph
+      next = Set.difference values keys
+  if Set.null next
+     then pure graph
+     else do
+       x <- T.traverse (\name -> (name,) <$> loadPackageDeps name) (F.toList next)
+       resolveDependencies (subtract 1 <$> limit)
+                      (Map.unionWith unifier graph (Map.fromList x))
+                      loadPackageDeps
+ where
+  unifier (pkgs1,v1) (pkgs2,_) = (Set.union pkgs1 pkgs2, v1)
+
+-- | @pruneGraph dontPrune toPrune graph@ prunes all packages in
+-- @graph@ with a name in @toPrune@ and removes resulting orphans
+-- unless they are in @dontPrune@
+pruneGraph ::
+     (F.Foldable f, F.Foldable g, Eq a)
+  => f PackageName
+  -> g PackageName
+  -> Map PackageName (Set PackageName, a)
+  -> Map PackageName (Set PackageName, a)
+pruneGraph dontPrune names =
+  pruneUnreachable dontPrune . Map.mapMaybeWithKey (\pkg (pkgDeps,x) ->
+    if pkg `F.elem` names
+      then Nothing
+      else let filtered = Set.filter (`F.notElem` names) pkgDeps
+           in  if Set.null filtered && not (Set.null pkgDeps)
+                 then Nothing
+                 else Just (filtered,x))
+
+-- | Make sure that all unreachable nodes (orphans) are pruned
+pruneUnreachable ::
+     (Eq a, F.Foldable f)
+  => f PackageName
+  -> Map PackageName (Set PackageName, a)
+  -> Map PackageName (Set PackageName, a)
+pruneUnreachable dontPrune = fixpoint prune
+ where
+  fixpoint :: Eq a => (a -> a) -> a -> a
+  fixpoint f v = if f v == v then v else fixpoint f (f v)
+  prune graph' = Map.filterWithKey (\k _ -> reachable k) graph'
+   where
+    reachable k = k `F.elem` dontPrune || k `Set.member` reachables
+    reachables = F.fold (fst <$> graph')
+
+localPackageToPackage :: LocalPackage -> Package
+localPackageToPackage lp = fromMaybe lp.package lp.testBench
src/Stack/Docker.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 {-# LANGUAGE OverloadedStrings   #-}
-{-# LANGUAGE RecordWildCards     #-}
 
 -- | Run commands in Docker containers
 module Stack.Docker
@@ -75,7 +76,7 @@                   )
 import           Stack.Types.DockerEntrypoint
                    ( DockerEntrypoint (..), DockerUser (..) )
-import           Stack.Types.Runner ( terminalL )
+import           Stack.Types.Runner ( HasDockerEntrypointMVar (..), terminalL )
 import           Stack.Types.Version ( showStackVersion, withinRange )
 import           System.Environment
                    ( getArgs, getEnv, getEnvironment, getExecutablePath
@@ -83,7 +84,6 @@                    )
 import qualified System.FilePath as FP
 import           System.IO.Error ( isDoesNotExistError )
-import           System.IO.Unsafe ( unsafePerformIO )
 import qualified System.Posix.User as User
 import qualified System.PosixCompat.Files as Files
 import           System.Terminal ( hIsTerminalDeviceOrMinTTY )
@@ -98,26 +98,33 @@   -> RIO env (FilePath,[String],[(String,String)],[Mount])
 getCmdArgs docker imageInfo isRemoteDocker = do
     config <- view configL
-    deUser <-
-        if fromMaybe (not isRemoteDocker) (dockerSetUser docker)
+    user <-
+        if fromMaybe (not isRemoteDocker) docker.setUser
             then liftIO $ do
-              duUid <- User.getEffectiveUserID
-              duGid <- User.getEffectiveGroupID
-              duGroups <- nubOrd <$> User.getGroups
-              duUmask <- Files.setFileCreationMask 0o022
+              uid <- User.getEffectiveUserID
+              gid <- User.getEffectiveGroupID
+              groups <- nubOrd <$> User.getGroups
+              umask <- Files.setFileCreationMask 0o022
               -- Only way to get old umask seems to be to change it, so set it back afterward
-              _ <- Files.setFileCreationMask duUmask
-              pure (Just DockerUser{..})
+              _ <- Files.setFileCreationMask umask
+              pure $ Just DockerUser
+                { uid
+                , gid
+                , groups
+                , umask
+                }
             else pure Nothing
     args <-
         fmap
-            (["--" ++ reExecArgName ++ "=" ++ showStackVersion
-             ,"--" ++ dockerEntrypointArgName
-             ,show DockerEntrypoint{..}] ++)
-            (liftIO getArgs)
-    case dockerStackExe (configDocker config) of
+          (  [ "--" ++ reExecArgName ++ "=" ++ showStackVersion
+             , "--" ++ dockerEntrypointArgName
+             , show DockerEntrypoint { user }
+             ] ++
+          )
+          (liftIO getArgs)
+    case config.docker.stackExe of
         Just DockerStackExeHost
-          | configPlatform config == dockerContainerPlatform -> do
+          | config.platform == dockerContainerPlatform -> do
               exePath <- resolveFile' =<< liftIO getExecutablePath
               cmdArgs args exePath
           | otherwise -> throwIO UnsupportedStackExeHostPlatformException
@@ -127,13 +134,13 @@         Just (DockerStackExePath path) -> cmdArgs args path
         Just DockerStackExeDownload -> exeDownload args
         Nothing
-          | configPlatform config == dockerContainerPlatform -> do
-              (exePath,exeTimestamp,misCompatible) <-
+          | config.platform == dockerContainerPlatform -> do
+              (exePath, exeTimestamp, misCompatible) <-
                   do exePath <- resolveFile' =<< liftIO getExecutablePath
                      exeTimestamp <- getModificationTime exePath
                      isKnown <-
                          loadDockerImageExeCache
-                             (iiId imageInfo)
+                             imageInfo.iiId
                              exePath
                              exeTimestamp
                      pure (exePath, exeTimestamp, isKnown)
@@ -148,7 +155,7 @@                               [ "run"
                               , "-v"
                               , toFilePath exePath ++ ":" ++ "/tmp/stack"
-                              , T.unpack (iiId imageInfo)
+                              , T.unpack imageInfo.iiId
                               , "/tmp/stack"
                               , "--version"]
                               sinkNull
@@ -158,7 +165,7 @@                                   Left ExitCodeException{} -> False
                                   Right _ -> True
                       saveDockerImageExeCache
-                          (iiId imageInfo)
+                          imageInfo.iiId
                           exePath
                           exeTimestamp
                           compatible
@@ -193,9 +200,9 @@ runContainerAndExit :: HasConfig env => RIO env void
 runContainerAndExit = do
   config <- view configL
-  let docker = configDocker config
+  let docker = config.docker
   checkDockerVersion docker
-  (env,isStdinTerminal,isStderrTerminal,homeDir) <- liftIO $
+  (env, isStdinTerminal, isStderrTerminal, homeDir) <- liftIO $
     (,,,)
     <$> getEnvironment
     <*> hIsTerminalDeviceOrMinTTY stdin
@@ -210,17 +217,17 @@       muserEnv = lookup "USER" env
       isRemoteDocker = maybe False (isPrefixOf "tcp://") dockerHost
   mstackYaml <- for (lookup "STACK_YAML" env) RIO.Directory.makeAbsolute
-  image <- either throwIO pure (dockerImage docker)
+  image <- either throwIO pure docker.image
   when
     ( isRemoteDocker && maybe False (isInfixOf "boot2docker") dockerCertPath )
     ( prettyWarnS
         "Using boot2docker is NOT supported, and not likely to perform well."
     )
   maybeImageInfo <- inspect image
-  imageInfo@Inspect{..} <- case maybeImageInfo of
+  imageInfo <- case maybeImageInfo of
     Just ii -> pure ii
     Nothing
-      | dockerAutoPull docker -> do
+      | docker.autoPull -> do
           pullImage docker image
           mii2 <- inspect image
           case mii2 of
@@ -229,16 +236,16 @@       | otherwise -> throwM (NotPulledException image)
   projectRoot <- getProjectRoot
   sandboxDir <- projectDockerSandboxDir projectRoot
-  let ImageConfig {..} = iiConfig
-      imageEnvVars = map (break (== '=')) icEnv
+  let ic = imageInfo.config
+      imageEnvVars = map (break (== '=')) ic.env
       platformVariant = show $ hashRepoName image
       stackRoot = view stackRootL config
       sandboxHomeDir = sandboxDir </> homeDirName
-      isTerm = not (dockerDetach docker) &&
+      isTerm = not docker.detach &&
                isStdinTerminal &&
                isStdoutTerminal &&
                isStderrTerminal
-      keepStdinOpen = not (dockerDetach docker) &&
+      keepStdinOpen = not docker.detach &&
                       -- Workaround for https://github.com/docker/docker/issues/12319
                       -- This is fixed in Docker 1.9.1, but will leave the workaround
                       -- in place for now, for users who haven't upgraded yet.
@@ -271,7 +278,7 @@       (Files.createSymbolicLink
         (toFilePathNoTrailingSep sshDir)
         (toFilePathNoTrailingSep (sandboxHomeDir </> sshRelDir))))
-  let mountSuffix = maybe "" (":" ++) (dockerMountMode docker)
+  let mountSuffix = maybe "" (":" ++) docker.mountMode
   containerID <- withWorkingDir (toFilePath projectRoot) $
     trim . decodeUtf8 <$> readDockerProcess
       ( concat
@@ -296,7 +303,7 @@               toFilePathNoTrailingSep sandboxHomeDir ++ mountSuffix
           , "-w", toFilePathNoTrailingSep pwd
           ]
-        , case dockerNetwork docker of
+        , case docker.network of
             Nothing -> ["--net=host"]
             Just name -> ["--net=" ++ name]
         , case muserEnv of
@@ -317,19 +324,19 @@            -- Disable the deprecated entrypoint in FP Complete-generated images
         , [ "--entrypoint=/usr/bin/env"
           |  isJust (lookupImageEnv oldSandboxIdEnvVar imageEnvVars)
-          && (  icEntrypoint == ["/usr/local/sbin/docker-entrypoint"]
-             || icEntrypoint == ["/root/entrypoint.sh"]
+          && (  ic.entrypoint == ["/usr/local/sbin/docker-entrypoint"]
+             || ic.entrypoint == ["/root/entrypoint.sh"]
              )
           ]
         , concatMap (\(k,v) -> ["-e", k ++ "=" ++ v]) envVars
-        , concatMap (mountArg mountSuffix) (extraMount ++ dockerMount docker)
-        , concatMap (\nv -> ["-e", nv]) (dockerEnv docker)
-        , case dockerContainerName docker of
+        , concatMap (mountArg mountSuffix) (extraMount ++ docker.mount)
+        , concatMap (\nv -> ["-e", nv]) docker.env
+        , case docker.containerName of
             Just name -> ["--name=" ++ name]
             Nothing -> []
         , ["-t" | isTerm]
         , ["-i" | keepStdinOpen]
-        , dockerRunArgs docker
+        , docker.runArgs
         , [image]
         , [cmnd]
         , args
@@ -380,7 +387,7 @@       -- containing invalid UTF-8
       case eitherDecode (LBS.pack (filter isAscii (decodeUtf8 inspectOut))) of
         Left msg -> throwIO (InvalidInspectOutputException msg)
-        Right results -> pure (Map.fromList (map (\r -> (iiId r,r)) results))
+        Right results -> pure (Map.fromList (map (\r -> (r.iiId, r)) results))
     Left ece
       | any (`LBS.isPrefixOf` eceStderr ece) missingImagePrefixes ->
           pure Map.empty
@@ -392,9 +399,9 @@ pull :: HasConfig env => RIO env ()
 pull = do
   config <- view configL
-  let docker = configDocker config
+  let docker = config.docker
   checkDockerVersion docker
-  either throwIO (pullImage docker) (dockerImage docker)
+  either throwIO (pullImage docker) docker.image
 
 -- | Pull Docker image from registry.
 pullImage :: (HasProcessContext env, HasTerm env)
@@ -406,14 +413,14 @@     [ flow "Pulling image from registry:"
     , style Current (fromString image) <> "."
     ]
-  when (dockerRegistryLogin docker) $ do
+  when docker.registryLogin $ do
     prettyInfoS "You may need to log in."
     proc
       "docker"
       ( concat
           [ ["login"]
-          , maybe [] (\n -> ["--username=" ++ n]) (dockerRegistryUsername docker)
-          , maybe [] (\p -> ["--password=" ++ p]) (dockerRegistryPassword docker)
+          , maybe [] (\n -> ["--username=" ++ n]) docker.registryUsername
+          , maybe [] (\p -> ["--password=" ++ p]) docker.registryPassword
           , [takeWhile (/= '/') image]
           ]
       )
@@ -448,8 +455,8 @@             throwIO (DockerTooOldException minimumDockerVersion v')
           | v' `elem` prohibitedDockerVersions ->
             throwIO (DockerVersionProhibitedException prohibitedDockerVersions v')
-          | not (v' `withinRange` dockerRequireDockerVersion docker) ->
-            throwIO (BadDockerVersionException (dockerRequireDockerVersion docker) v')
+          | not (v' `withinRange` docker.requireDockerVersion) ->
+            throwIO (BadDockerVersionException docker.requireDockerVersion v')
           | otherwise ->
             pure ()
         _ -> throwIO InvalidVersionOutputException
@@ -476,11 +483,13 @@ -- | The Docker container "entrypoint": special actions performed when first
 -- entering a container, such as switching the UID/GID to the "outside-Docker"
 -- user's.
-entrypoint :: (HasProcessContext env, HasLogFunc env)
-           => Config
-           -> DockerEntrypoint
-           -> RIO env ()
-entrypoint config@Config{} DockerEntrypoint{..} =
+entrypoint ::
+     (HasDockerEntrypointMVar env, HasProcessContext env, HasLogFunc env)
+  => Config
+  -> DockerEntrypoint
+  -> RIO env ()
+entrypoint config@Config{} de = do
+  entrypointMVar <- view dockerEntrypointMVarL
   modifyMVar_ entrypointMVar $ \alreadyRan -> do
     -- Only run the entrypoint once
     unless alreadyRan $ do
@@ -490,7 +499,7 @@       estackUserEntry0 <- liftIO $ tryJust (guard . isDoesNotExistError) $
         User.getUserEntryForName stackUserName
       -- Switch UID/GID if needed, and update user's home directory
-      case deUser of
+      case de.user of
         Nothing -> pure ()
         Just (DockerUser 0 _ _ _) -> pure ()
         Just du -> withProcessContext envOverride $
@@ -515,52 +524,52 @@                 copyFile srcBuildPlan destBuildPlan
     pure True
  where
-  updateOrCreateStackUser estackUserEntry homeDir DockerUser{..} = do
+  updateOrCreateStackUser estackUserEntry homeDir du = do
     case estackUserEntry of
       Left _ -> do
         -- If no 'stack' user in image, create one with correct UID/GID and home
         -- directory
         readProcessNull "groupadd"
-          ["-o"
-          ,"--gid",show duGid
-          ,stackUserName]
+          [ "-o"
+          , "--gid",show du.gid
+          , stackUserName
+          ]
         readProcessNull "useradd"
-          ["-oN"
-          ,"--uid",show duUid
-          ,"--gid",show duGid
-          ,"--home",toFilePathNoTrailingSep homeDir
-          ,stackUserName]
+          [ "-oN"
+          , "--uid", show du.uid
+          , "--gid", show du.gid
+          , "--home", toFilePathNoTrailingSep homeDir
+          , stackUserName
+          ]
       Right _ -> do
         -- If there is already a 'stack' user in the image, adjust its UID/GID
         -- and home directory
         readProcessNull "usermod"
-          ["-o"
-          ,"--uid",show duUid
-          ,"--home",toFilePathNoTrailingSep homeDir
-          ,stackUserName]
+          [ "-o"
+          , "--uid", show du.uid
+          , "--home", toFilePathNoTrailingSep homeDir
+          , stackUserName
+          ]
         readProcessNull "groupmod"
-          ["-o"
-          ,"--gid",show duGid
-          ,stackUserName]
-    forM_ duGroups $ \gid ->
+          [ "-o"
+          , "--gid", show du.gid
+          , stackUserName
+          ]
+    forM_ du.groups $ \gid ->
       readProcessNull "groupadd"
-        ["-o"
-        ,"--gid",show gid
-        ,"group" ++ show gid]
+        [ "-o"
+        , "--gid", show gid
+        , "group" ++ show gid
+        ]
     -- 'setuid' to the wanted UID and GID
     liftIO $ do
-      User.setGroupID duGid
-      handleSetGroups duGroups
-      User.setUserID duUid
-      _ <- Files.setFileCreationMask duUmask
+      User.setGroupID du.gid
+      handleSetGroups du.groups
+      User.setUserID du.uid
+      _ <- Files.setFileCreationMask du.umask
       pure ()
   stackUserName = "stack" :: String
 
--- | MVar used to ensure the Docker entrypoint is performed exactly once
-entrypointMVar :: MVar Bool
-{-# NOINLINE entrypointMVar #-}
-entrypointMVar = unsafePerformIO (newMVar False)
-
 -- | Remove the contents of a directory, without removing the directory itself.
 -- This is used instead of 'FS.removeTree' to clear bind-mounted directories,
 -- since removing the root of the bind-mount won't work.
@@ -609,7 +618,7 @@ -- | Fail with friendly error if project root not set.
 getProjectRoot :: HasConfig env => RIO env (Path Abs Dir)
 getProjectRoot = do
-  mroot <- view $ configL.to configProjectRoot
+  mroot <- view $ configL . to configProjectRoot
   maybe (throwIO CannotDetermineProjectRootException) pure mroot
 
 -- | Environment variable that contained the old sandbox ID.
@@ -619,10 +628,10 @@ 
 -- | Parsed result of @docker inspect@.
 data Inspect = Inspect
-  { iiConfig      :: ImageConfig
-  , iiCreated     :: UTCTime
-  , iiId          :: Text
-  , iiVirtualSize :: Maybe Integer
+  { config      :: ImageConfig
+  , created     :: UTCTime
+  , iiId        :: Text
+  , virtualSize :: Maybe Integer
   }
   deriving Show
 
@@ -638,8 +647,8 @@ 
 -- | Parsed @Config@ section of @docker inspect@ output.
 data ImageConfig = ImageConfig
-  { icEnv :: [String]
-  , icEntrypoint :: [String]
+  { env :: [String]
+  , entrypoint :: [String]
   }
   deriving Show
 
src/Stack/Dot.hs view
@@ -1,501 +1,31 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
+-- | Functions related to Stack's @dot@ command.
 module Stack.Dot
-  ( dot
-  , listDependencies
-  , DotOpts (..)
-  , DotPayload (..)
-  , ListDepsOpts (..)
-  , ListDepsFormat (..)
-  , ListDepsFormatOpts (..)
-  , resolveDependencies
+  ( dotCmd
   , printGraph
-  , pruneGraph
   ) where
 
-import           Data.Aeson ( ToJSON (..), Value, (.=), encode, object )
-import qualified Data.ByteString.Lazy.Char8 as LBC8
 import qualified Data.Foldable as F
-import qualified Data.Sequence as Seq
 import qualified Data.Set as Set
 import qualified Data.Map as Map
 import qualified Data.Text as Text
 import qualified Data.Text.IO as Text
-import qualified Data.Traversable as T
-import           Distribution.License ( License (BSD3), licenseFromSPDX )
-import qualified Distribution.PackageDescription as PD
-import qualified Distribution.SPDX.License as SPDX
-import           Distribution.Text ( display )
-import           Distribution.Types.PackageName ( mkPackageName )
-import           Path ( parent )
-import           RIO.Process ( HasProcessContext (..) )
-import           Stack.Build ( loadPackage )
-import           Stack.Build.Installed ( getInstalled, toInstallMap )
-import           Stack.Build.Source
-                   ( loadCommonPackage, loadLocalPackage, loadSourceMap )
-import           Stack.Build.Target( NeedTargets (..), parseTargets )
 import           Stack.Constants ( wiredInPackages )
-import           Stack.Package ( Package (..) )
-import           Stack.Prelude hiding ( Display (..), pkgName, loadPackage )
-import qualified Stack.Prelude ( pkgName )
-import           Stack.Runners
-                   ( ShouldReexec (..), withBuildConfig, withConfig
-                   , withEnvConfig
-                   )
-import           Stack.SourceMap
-                   ( globalsFromHints, mkProjectPackage, pruneGlobals )
-import           Stack.Types.BuildConfig
-                   ( BuildConfig (..), HasBuildConfig (..) )
-import           Stack.Types.BuildOpts
-                   ( ApplyCLIFlag, BuildOptsCLI (..), buildOptsMonoidBenchmarksL
-                   , buildOptsMonoidTestsL, defaultBuildOptsCLI
-                   )
-import           Stack.Types.Compiler ( wantedToActual )
-import           Stack.Types.Config ( HasConfig (..) )
-import           Stack.Types.DumpPackage ( DumpPackage (..) )
-import           Stack.Types.EnvConfig ( EnvConfig (..), HasSourceMap (..) )
-import           Stack.Types.GHCVariant ( HasGHCVariant (..) )
-import           Stack.Types.GhcPkgId
-                   ( GhcPkgId, ghcPkgIdString, parseGhcPkgId )
-import           Stack.Types.GlobalOpts ( globalOptsBuildOptsMonoidL )
-import           Stack.Types.Package ( LocalPackage (..) )
-import           Stack.Types.Platform ( HasPlatform (..) )
-import           Stack.Types.Runner ( HasRunner (..), Runner, globalOptsL )
-import           Stack.Types.SourceMap
-                   ( CommonPackage (..), DepPackage (..), ProjectPackage (..)
-                   , SMActual (..), SMWanted (..), SourceMap (..)
-                   )
-
--- | Type representing exceptions thrown by functions exported by the
--- "Stack.Dot" module.
-data DotException
-  = DependencyNotFoundBug GhcPkgId
-  | PackageNotFoundBug PackageName
-  deriving (Show, Typeable)
-
-instance Exception DotException where
-  displayException (DependencyNotFoundBug depId) = bugReport "[S-7071]" $ concat
-    [ "Expected to find "
-    , ghcPkgIdString depId
-    , " in global DB."
-    ]
-  displayException (PackageNotFoundBug pkgName) = bugReport "[S-7151]" $ concat
-    [ "The '"
-    , packageNameString pkgName
-    , "' package was not found in any of the dependency sources."
-    ]
-
--- | Options record for @stack dot@
-data DotOpts = DotOpts
-  { dotIncludeExternal :: !Bool
-    -- ^ Include external dependencies
-  , dotIncludeBase :: !Bool
-    -- ^ Include dependencies on base
-  , dotDependencyDepth :: !(Maybe Int)
-    -- ^ Limit the depth of dependency resolution to (Just n) or continue until
-    -- fixpoint
-  , dotPrune :: !(Set PackageName)
-    -- ^ Package names to prune from the graph
-  , dotTargets :: [Text]
-    -- ^ Stack TARGETs to trace dependencies for
-  , dotFlags :: !(Map ApplyCLIFlag (Map FlagName Bool))
-    -- ^ Flags to apply when calculating dependencies
-  , dotTestTargets :: Bool
-    -- ^ Like the "--test" flag for build, affects the meaning of 'dotTargets'.
-  , dotBenchTargets :: Bool
-    -- ^ Like the "--bench" flag for build, affects the meaning of 'dotTargets'.
-  , dotGlobalHints :: Bool
-    -- ^ Use global hints instead of relying on an actual GHC installation.
-  }
-
-data ListDepsFormatOpts = ListDepsFormatOpts
-  { listDepsSep :: !Text
-    -- ^ Separator between the package name and details.
-  , listDepsLicense :: !Bool
-    -- ^ Print dependency licenses instead of versions.
-  }
-
-data ListDepsFormat
-  = ListDepsText ListDepsFormatOpts
-  | ListDepsTree ListDepsFormatOpts
-  | ListDepsJSON
-  | ListDepsConstraints
-
-data ListDepsOpts = ListDepsOpts
-  { listDepsFormat :: !ListDepsFormat
-    -- ^ Format of printing dependencies
-  , listDepsDotOpts :: !DotOpts
-    -- ^ The normal dot options.
-  }
+import           Stack.DependencyGraph ( createPrunedDependencyGraph )
+import           Stack.Prelude
+import           Stack.Types.DependencyTree ( DotPayload (..) )
+import           Stack.Types.DotOpts ( DotOpts (..) )
+import           Stack.Types.Runner ( Runner )
 
 -- | Visualize the project's dependencies as a graphviz graph
-dot :: DotOpts -> RIO Runner ()
-dot dotOpts = do
+dotCmd :: DotOpts -> RIO Runner ()
+dotCmd dotOpts = do
   (localNames, prunedGraph) <- createPrunedDependencyGraph dotOpts
   printGraph dotOpts localNames prunedGraph
 
--- | Information about a package in the dependency graph, when available.
-data DotPayload = DotPayload
-  { payloadVersion :: Maybe Version
-    -- ^ The package version.
-  , payloadLicense :: Maybe (Either SPDX.License License)
-    -- ^ The license the package was released under.
-  , payloadLocation :: Maybe PackageLocation
-    -- ^ The location of the package.
-  }
-  deriving (Eq, Show)
-
--- | Create the dependency graph and also prune it as specified in the dot
--- options. Returns a set of local names and a map from package names to
--- dependencies.
-createPrunedDependencyGraph ::
-     DotOpts
-  -> RIO
-       Runner
-       (Set PackageName, Map PackageName (Set PackageName, DotPayload))
-createPrunedDependencyGraph dotOpts = withDotConfig dotOpts $ do
-  localNames <- view $ buildConfigL.to (Map.keysSet . smwProject . bcSMWanted)
-  logDebug "Creating dependency graph"
-  resultGraph <- createDependencyGraph dotOpts
-  let pkgsToPrune = if dotIncludeBase dotOpts
-                      then dotPrune dotOpts
-                      else Set.insert "base" (dotPrune dotOpts)
-      prunedGraph = pruneGraph localNames pkgsToPrune resultGraph
-  logDebug "Returning pruned dependency graph"
-  pure (localNames, prunedGraph)
-
--- | Create the dependency graph, the result is a map from a package
--- name to a tuple of dependencies and payload if available. This
--- function mainly gathers the required arguments for
--- @resolveDependencies@.
-createDependencyGraph ::
-     DotOpts
-  -> RIO DotConfig (Map PackageName (Set PackageName, DotPayload))
-createDependencyGraph dotOpts = do
-  sourceMap <- view sourceMapL
-  locals <- for (toList $ smProject sourceMap) loadLocalPackage
-  let graph = Map.fromList $ projectPackageDependencies dotOpts (filter lpWanted locals)
-  globalDump <- view $ to dcGlobalDump
-  -- TODO: Can there be multiple entries for wired-in-packages? If so,
-  -- this will choose one arbitrarily..
-  let globalDumpMap = Map.fromList $ map (\dp -> (Stack.Prelude.pkgName (dpPackageIdent dp), dp)) globalDump
-      globalIdMap = Map.fromList $ map (dpGhcPkgId &&& dpPackageIdent) globalDump
-  let depLoader = createDepLoader sourceMap globalDumpMap globalIdMap loadPackageDeps
-      loadPackageDeps name version loc flags ghcOptions cabalConfigOpts
-        -- Skip packages that can't be loaded - see
-        -- https://github.com/commercialhaskell/stack/issues/2967
-        | name `elem` [mkPackageName "rts", mkPackageName "ghc"] =
-            pure ( Set.empty
-                 , DotPayload (Just version) (Just $ Right BSD3) Nothing )
-        | otherwise =
-            fmap (packageAllDeps &&& makePayload loc)
-                 (loadPackage loc flags ghcOptions cabalConfigOpts)
-  resolveDependencies (dotDependencyDepth dotOpts) graph depLoader
- where
-  makePayload loc pkg = DotPayload (Just $ packageVersion pkg)
-                                   (Just $ packageLicense pkg)
-                                   (Just $ PLImmutable loc)
-
-listDependencies :: ListDepsOpts -> RIO Runner ()
-listDependencies opts = do
-  let dotOpts = listDepsDotOpts opts
-  (pkgs, resultGraph) <- createPrunedDependencyGraph dotOpts
-  liftIO $ case listDepsFormat opts of
-    ListDepsTree treeOpts ->
-      Text.putStrLn "Packages"
-      >> printTree treeOpts dotOpts 0 [] (treeRoots opts pkgs) resultGraph
-    ListDepsJSON -> printJSON pkgs resultGraph
-    ListDepsText textOpts ->
-      void $ Map.traverseWithKey (go "" textOpts) (snd <$> resultGraph)
-    ListDepsConstraints -> do
-      let constraintOpts = ListDepsFormatOpts " ==" False
-      Text.putStrLn "constraints:"
-      void $ Map.traverseWithKey (go "  , " constraintOpts)
-                                 (snd <$> resultGraph)
- where
-  go prefix lineOpts name payload =
-    Text.putStrLn $ prefix <> listDepsLine lineOpts name payload
-
-data DependencyTree =
-  DependencyTree (Set PackageName)
-                 (Map PackageName (Set PackageName, DotPayload))
-
-instance ToJSON DependencyTree where
-  toJSON (DependencyTree _ dependencyMap) =
-    toJSON $ foldToList dependencyToJSON dependencyMap
-
-foldToList :: (k -> a -> b) -> Map k a -> [b]
-foldToList f = Map.foldrWithKey (\k a bs -> bs ++ [f k a]) []
-
-dependencyToJSON :: PackageName -> (Set PackageName, DotPayload) -> Value
-dependencyToJSON pkg (deps, payload) =
-  let fieldsAlwaysPresent = [ "name" .= packageNameString pkg
-                            , "license" .= licenseText payload
-                            , "version" .= versionText payload
-                            , "dependencies" .= Set.map packageNameString deps
-                            ]
-      loc = catMaybes
-              [("location" .=) . pkgLocToJSON <$> payloadLocation payload]
-  in  object $ fieldsAlwaysPresent ++ loc
-
-pkgLocToJSON :: PackageLocation -> Value
-pkgLocToJSON (PLMutable (ResolvedPath _ dir)) = object
-  [ "type" .= ("project package" :: Text)
-  , "url" .= ("file://" ++ toFilePath dir)
-  ]
-pkgLocToJSON (PLImmutable (PLIHackage pkgid _ _)) = object
-  [ "type" .= ("hackage" :: Text)
-  , "url" .= ("https://hackage.haskell.org/package/" ++ display pkgid)
-  ]
-pkgLocToJSON (PLImmutable (PLIArchive archive _)) =
-  let url = case archiveLocation archive of
-              ALUrl u -> u
-              ALFilePath (ResolvedPath _ path) ->
-                Text.pack $ "file://" ++ toFilePath path
-  in  object
-        [ "type" .= ("archive" :: Text)
-        , "url" .= url
-        , "sha256" .= archiveHash archive
-        , "size" .= archiveSize archive
-        ]
-pkgLocToJSON (PLImmutable (PLIRepo repo _)) = object
-  [ "type" .= case repoType repo of
-                RepoGit -> "git" :: Text
-                RepoHg -> "hg" :: Text
-  , "url" .= repoUrl repo
-  , "commit" .= repoCommit repo
-  , "subdir" .= repoSubdir repo
-  ]
-
-printJSON ::
-     Set PackageName
-  -> Map PackageName (Set PackageName, DotPayload)
-  -> IO ()
-printJSON pkgs dependencyMap =
-  LBC8.putStrLn $ encode $ DependencyTree pkgs dependencyMap
-
-treeRoots :: ListDepsOpts -> Set PackageName -> Set PackageName
-treeRoots opts projectPackages' =
-  let targets = dotTargets $ listDepsDotOpts opts
-  in  if null targets
-        then projectPackages'
-        else Set.fromList $ map (mkPackageName . Text.unpack) targets
-
-printTree ::
-     ListDepsFormatOpts
-  -> DotOpts
-  -> Int
-  -> [Int]
-  -> Set PackageName
-  -> Map PackageName (Set PackageName, DotPayload)
-  -> IO ()
-printTree opts dotOpts depth remainingDepsCounts packages dependencyMap =
-  F.sequence_ $ Seq.mapWithIndex go (toSeq packages)
- where
-  toSeq = Seq.fromList . Set.toList
-  go index name =
-    let newDepsCounts = remainingDepsCounts ++ [Set.size packages - index - 1]
-    in  case Map.lookup name dependencyMap of
-          Just (deps, payload) -> do
-            printTreeNode opts dotOpts depth newDepsCounts deps payload name
-            if Just depth == dotDependencyDepth dotOpts
-              then pure ()
-              else printTree opts dotOpts (depth + 1) newDepsCounts deps
-                     dependencyMap
-          -- TODO: Define this behaviour, maybe pure an error?
-          Nothing -> pure ()
-
-printTreeNode ::
-     ListDepsFormatOpts
-  -> DotOpts
-  -> Int
-  -> [Int]
-  -> Set PackageName
-  -> DotPayload
-  -> PackageName
-  -> IO ()
-printTreeNode opts dotOpts depth remainingDepsCounts deps payload name =
-  let remainingDepth = fromMaybe 999 (dotDependencyDepth dotOpts) - depth
-      hasDeps = not $ null deps
-  in  Text.putStrLn $
-        treeNodePrefix "" remainingDepsCounts hasDeps remainingDepth <> " " <>
-        listDepsLine opts name payload
-
-treeNodePrefix :: Text -> [Int] -> Bool -> Int -> Text
-treeNodePrefix t [] _ _      = t
-treeNodePrefix t [0] True  0 = t <> "└──"
-treeNodePrefix t [_] True  0 = t <> "├──"
-treeNodePrefix t [0] True  _ = t <> "└─┬"
-treeNodePrefix t [_] True  _ = t <> "├─┬"
-treeNodePrefix t [0] False _ = t <> "└──"
-treeNodePrefix t [_] False _ = t <> "├──"
-treeNodePrefix t (0:ns) d remainingDepth = treeNodePrefix (t <> "  ") ns d remainingDepth
-treeNodePrefix t (_:ns) d remainingDepth = treeNodePrefix (t <> "│ ") ns d remainingDepth
-
-listDepsLine :: ListDepsFormatOpts -> PackageName -> DotPayload -> Text
-listDepsLine opts name payload =
-  Text.pack (packageNameString name) <> listDepsSep opts <>
-  payloadText opts payload
-
-payloadText :: ListDepsFormatOpts -> DotPayload -> Text
-payloadText opts payload =
-  if listDepsLicense opts
-    then licenseText payload
-    else versionText payload
-
-licenseText :: DotPayload -> Text
-licenseText payload =
-  maybe "<unknown>" (Text.pack . display . either licenseFromSPDX id)
-                    (payloadLicense payload)
-
-versionText :: DotPayload -> Text
-versionText payload =
-  maybe "<unknown>" (Text.pack . display) (payloadVersion payload)
-
--- | @pruneGraph dontPrune toPrune graph@ prunes all packages in
--- @graph@ with a name in @toPrune@ and removes resulting orphans
--- unless they are in @dontPrune@
-pruneGraph ::
-     (F.Foldable f, F.Foldable g, Eq a)
-  => f PackageName
-  -> g PackageName
-  -> Map PackageName (Set PackageName, a)
-  -> Map PackageName (Set PackageName, a)
-pruneGraph dontPrune names =
-  pruneUnreachable dontPrune . Map.mapMaybeWithKey (\pkg (pkgDeps,x) ->
-    if pkg `F.elem` names
-      then Nothing
-      else let filtered = Set.filter (`F.notElem` names) pkgDeps
-           in  if Set.null filtered && not (Set.null pkgDeps)
-                 then Nothing
-                 else Just (filtered,x))
-
--- | Make sure that all unreachable nodes (orphans) are pruned
-pruneUnreachable ::
-     (Eq a, F.Foldable f)
-  => f PackageName
-  -> Map PackageName (Set PackageName, a)
-  -> Map PackageName (Set PackageName, a)
-pruneUnreachable dontPrune = fixpoint prune
- where
-  fixpoint :: Eq a => (a -> a) -> a -> a
-  fixpoint f v = if f v == v then v else fixpoint f (f v)
-  prune graph' = Map.filterWithKey (\k _ -> reachable k) graph'
-   where
-    reachable k = k `F.elem` dontPrune || k `Set.member` reachables
-    reachables = F.fold (fst <$> graph')
-
-
--- | Resolve the dependency graph up to (Just depth) or until fixpoint is reached
-resolveDependencies ::
-     (Applicative m, Monad m)
-  => Maybe Int
-  -> Map PackageName (Set PackageName, DotPayload)
-  -> (PackageName -> m (Set PackageName, DotPayload))
-  -> m (Map PackageName (Set PackageName, DotPayload))
-resolveDependencies (Just 0) graph _ = pure graph
-resolveDependencies limit graph loadPackageDeps = do
-  let values = Set.unions (fst <$> Map.elems graph)
-      keys = Map.keysSet graph
-      next = Set.difference values keys
-  if Set.null next
-     then pure graph
-     else do
-       x <- T.traverse (\name -> (name,) <$> loadPackageDeps name) (F.toList next)
-       resolveDependencies (subtract 1 <$> limit)
-                      (Map.unionWith unifier graph (Map.fromList x))
-                      loadPackageDeps
- where
-  unifier (pkgs1,v1) (pkgs2,_) = (Set.union pkgs1 pkgs2, v1)
-
--- | Given a SourceMap and a dependency loader, load the set of dependencies for
--- a package
-createDepLoader ::
-     SourceMap
-  -> Map PackageName DumpPackage
-  -> Map GhcPkgId PackageIdentifier
-  -> (  PackageName
-     -> Version
-     -> PackageLocationImmutable
-     -> Map FlagName Bool
-     -> [Text]
-     -> [Text]
-     -> RIO DotConfig (Set PackageName, DotPayload)
-     )
-  -> PackageName
-  -> RIO DotConfig (Set PackageName, DotPayload)
-createDepLoader sourceMap globalDumpMap globalIdMap loadPackageDeps pkgName =
-  fromMaybe (throwIO $ PackageNotFoundBug pkgName)
-    (projectPackageDeps <|> dependencyDeps <|> globalDeps)
- where
-  projectPackageDeps = loadDeps <$> Map.lookup pkgName (smProject sourceMap)
-   where
-    loadDeps pp = do
-      pkg <- loadCommonPackage (ppCommon pp)
-      pure (packageAllDeps pkg, payloadFromLocal pkg Nothing)
-
-  dependencyDeps =
-    loadDeps <$> Map.lookup pkgName (smDeps sourceMap)
-   where
-    loadDeps DepPackage{dpLocation=PLMutable dir} = do
-      pp <- mkProjectPackage YesPrintWarnings dir False
-      pkg <- loadCommonPackage (ppCommon pp)
-      pure (packageAllDeps pkg, payloadFromLocal pkg (Just $ PLMutable dir))
-
-    loadDeps dp@DepPackage{dpLocation=PLImmutable loc} = do
-      let common = dpCommon dp
-      gpd <- liftIO $ cpGPD common
-      let PackageIdentifier name version = PD.package $ PD.packageDescription gpd
-          flags = cpFlags common
-          ghcOptions = cpGhcOptions common
-          cabalConfigOpts = cpCabalConfigOpts common
-      assert
-        (pkgName == name)
-        (loadPackageDeps pkgName version loc flags ghcOptions cabalConfigOpts)
-
-  -- If package is a global package, use info from ghc-pkg (#4324, #3084)
-  globalDeps =
-    pure . getDepsFromDump <$> Map.lookup pkgName globalDumpMap
-   where
-    getDepsFromDump dump = (Set.fromList deps, payloadFromDump dump)
-     where
-      deps = map ghcIdToPackageName (dpDepends dump)
-      ghcIdToPackageName depId =
-        maybe (impureThrow $ DependencyNotFoundBug depId)
-              Stack.Prelude.pkgName
-              (Map.lookup depId globalIdMap)
-
-  payloadFromLocal pkg =
-    DotPayload (Just $ packageVersion pkg) (Just $ packageLicense pkg)
-
-  payloadFromDump dp =
-    DotPayload (Just $ pkgVersion $ dpPackageIdent dp)
-               (Right <$> dpLicense dp)
-               Nothing
-
--- | Resolve the direct (depth 0) external dependencies of the given local
--- packages (assumed to come from project packages)
-projectPackageDependencies ::
-     DotOpts
-  -> [LocalPackage]
-  -> [(PackageName, (Set PackageName, DotPayload))]
-projectPackageDependencies dotOpts locals =
-  map (\lp -> let pkg = localPackageToPackage lp
-                  pkgDir = parent $ lpCabalFile lp
-                  loc = PLMutable $ ResolvedPath (RelFilePath "N/A") pkgDir
-              in  (packageName pkg, (deps pkg, lpPayload pkg loc)))
-      locals
- where
-  deps pkg = if dotIncludeExternal dotOpts
-               then Set.delete (packageName pkg) (packageAllDeps pkg)
-               else Set.intersection localNames (packageAllDeps pkg)
-  localNames = Set.fromList $ map (packageName . lpPackage) locals
-  lpPayload pkg loc =
-    DotPayload (Just $ packageVersion pkg)
-               (Just $ packageLicense pkg)
-               (Just loc)
-
 -- | Print a graphviz graph of the edges in the Map and highlight the given
 -- local packages
 printGraph ::
@@ -512,7 +42,7 @@   liftIO $ Text.putStrLn "}"
  where
   filteredLocals =
-    Set.filter (\local' -> local' `Set.notMember` dotPrune dotOpts) locals
+    Set.filter (\local' -> local' `Set.notMember` dotOpts.prune) locals
 
 -- | Print the local nodes with a different style depending on options
 printLocalNodes ::
@@ -524,7 +54,7 @@   liftIO $ Text.putStrLn (Text.intercalate "\n" lpNodes)
  where
   applyStyle :: Text -> Text
-  applyStyle n = if dotIncludeExternal dotOpts
+  applyStyle n = if dotOpts.includeExternal
                    then n <> " [style=dashed];"
                    else n <> " [style=solid];"
   lpNodes :: [Text]
@@ -563,129 +93,3 @@ -- | Check if the package is wired in (shipped with) ghc
 isWiredIn :: PackageName -> Bool
 isWiredIn = (`Set.member` wiredInPackages)
-
-localPackageToPackage :: LocalPackage -> Package
-localPackageToPackage lp =
-  fromMaybe (lpPackage lp) (lpTestBench lp)
-
--- Plumbing for --test and --bench flags
-withDotConfig ::
-     DotOpts
-  -> RIO DotConfig a
-  -> RIO Runner a
-withDotConfig opts inner =
-  local (over globalOptsL modifyGO) $
-    if dotGlobalHints opts
-      then withConfig NoReexec $ withBuildConfig withGlobalHints
-      else withConfig YesReexec withReal
- where
-  withGlobalHints = do
-    bconfig <- view buildConfigL
-    globals <- globalsFromHints $ smwCompiler $ bcSMWanted bconfig
-    fakeGhcPkgId <- parseGhcPkgId "ignored"
-    actual <- either throwIO pure $
-              wantedToActual $ smwCompiler $
-              bcSMWanted bconfig
-    let smActual = SMActual
-          { smaCompiler = actual
-          , smaProject = smwProject $ bcSMWanted bconfig
-          , smaDeps = smwDeps $ bcSMWanted bconfig
-          , smaGlobal = Map.mapWithKey toDump globals
-          }
-        toDump :: PackageName -> Version -> DumpPackage
-        toDump name version = DumpPackage
-          { dpGhcPkgId = fakeGhcPkgId
-          , dpPackageIdent = PackageIdentifier name version
-          , dpParentLibIdent = Nothing
-          , dpLicense = Nothing
-          , dpLibDirs = []
-          , dpLibraries = []
-          , dpHasExposedModules = True
-          , dpExposedModules = mempty
-          , dpDepends = []
-          , dpHaddockInterfaces = []
-          , dpHaddockHtml = Nothing
-          , dpIsExposed = True
-          }
-        actualPkgs = Map.keysSet (smaDeps smActual) <>
-                     Map.keysSet (smaProject smActual)
-        prunedActual = smActual { smaGlobal = pruneGlobals (smaGlobal smActual) actualPkgs }
-    targets <- parseTargets NeedTargets False boptsCLI prunedActual
-    logDebug "Loading source map"
-    sourceMap <- loadSourceMap targets boptsCLI smActual
-    let dc = DotConfig
-                { dcBuildConfig = bconfig
-                , dcSourceMap = sourceMap
-                , dcGlobalDump = toList $ smaGlobal smActual
-                }
-    logDebug "DotConfig fully loaded"
-    runRIO dc inner
-
-  withReal = withEnvConfig NeedTargets boptsCLI $ do
-    envConfig <- ask
-    let sourceMap = envConfigSourceMap envConfig
-    installMap <- toInstallMap sourceMap
-    (_, globalDump, _, _) <- getInstalled installMap
-    let dc = DotConfig
-          { dcBuildConfig = envConfigBuildConfig envConfig
-          , dcSourceMap = sourceMap
-          , dcGlobalDump = globalDump
-          }
-    runRIO dc inner
-
-  boptsCLI = defaultBuildOptsCLI
-    { boptsCLITargets = dotTargets opts
-    , boptsCLIFlags = dotFlags opts
-    }
-  modifyGO =
-    (if dotTestTargets opts
-       then set (globalOptsBuildOptsMonoidL.buildOptsMonoidTestsL) (Just True)
-       else id) .
-    (if dotBenchTargets opts
-       then set (globalOptsBuildOptsMonoidL.buildOptsMonoidBenchmarksL) (Just True)
-       else id)
-
-data DotConfig = DotConfig
-  { dcBuildConfig :: !BuildConfig
-  , dcSourceMap :: !SourceMap
-  , dcGlobalDump :: ![DumpPackage]
-  }
-
-instance HasLogFunc DotConfig where
-  logFuncL = runnerL.logFuncL
-
-instance HasPantryConfig DotConfig where
-  pantryConfigL = configL.pantryConfigL
-
-instance HasTerm DotConfig where
-  useColorL = runnerL.useColorL
-  termWidthL = runnerL.termWidthL
-
-instance HasStylesUpdate DotConfig where
-  stylesUpdateL = runnerL.stylesUpdateL
-
-instance HasGHCVariant DotConfig where
-  ghcVariantL = configL.ghcVariantL
-  {-# INLINE ghcVariantL #-}
-
-instance HasPlatform DotConfig where
-  platformL = configL.platformL
-  {-# INLINE platformL #-}
-  platformVariantL = configL.platformVariantL
-  {-# INLINE platformVariantL #-}
-
-instance HasRunner DotConfig where
-  runnerL = configL.runnerL
-
-instance HasProcessContext DotConfig where
-  processContextL = runnerL.processContextL
-
-instance HasConfig DotConfig where
-  configL = buildConfigL.lens bcConfig (\x y -> x { bcConfig = y })
-  {-# INLINE configL #-}
-
-instance HasBuildConfig DotConfig where
-  buildConfigL = lens dcBuildConfig (\x y -> x { dcBuildConfig = y })
-
-instance HasSourceMap DotConfig where
-  sourceMapL = lens dcSourceMap (\x y -> x { dcSourceMap = y })
src/Stack/Eval.hs view
@@ -1,5 +1,7 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 
 -- | Types and functions related to Stack's @eval@ command.
 module Stack.Eval
@@ -16,18 +18,18 @@ 
 -- Type representing command line options for the @stack eval@ command.
 data EvalOpts = EvalOpts
-  { evalArg :: !String
-  , evalExtra :: !ExecOptsExtra
+  { arg :: !String
+  , extra :: !ExecOptsExtra
   }
   deriving Show
 
 -- | Function underlying the @stack eval@ command. Evaluate some Haskell code
 -- inline.
 evalCmd :: EvalOpts -> RIO Runner ()
-evalCmd EvalOpts {..} = execCmd execOpts
+evalCmd eval = execCmd execOpts
  where
-  execOpts =
-    ExecOpts { eoCmd = ExecGhc
-             , eoArgs = ["-e", evalArg]
-             , eoExtra = evalExtra
-             }
+  execOpts = ExecOpts
+    { cmd = ExecGhc
+    , args = ["-e", eval.arg]
+    , extra = eval.extra
+    }
src/Stack/Exec.hs view
@@ -1,6 +1,6 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 -- | Types and function related to Stack's @exec@, @ghc@, @run@, @runghc@ and
 -- @runhaskell@ commands.
@@ -15,6 +15,7 @@ import qualified Data.Map.Strict as Map
 import qualified Data.Set as Set
 import qualified Data.Text as T
+import           RIO.NonEmpty ( head, nonEmpty )
 import           RIO.Process ( exec )
 import           Stack.Build ( build )
 import           Stack.Build.Target ( NeedTargets (..) )
@@ -25,7 +26,7 @@ import           Stack.Runners ( ShouldReexec (..), withConfig, withEnvConfig )
 import           Stack.Types.BuildConfig
                    ( BuildConfig (..), HasBuildConfig (..) )
-import           Stack.Types.BuildOpts
+import           Stack.Types.BuildOptsCLI
                    ( BuildOptsCLI (..), defaultBuildOptsCLI )
 import           Stack.Types.CompilerPaths
                    ( CompilerPaths (..), HasCompiler (..), getGhcPkgExe )
@@ -56,17 +57,21 @@ data ExecPrettyException
   = PackageIdNotFoundBug !String
   | ExecutableToRunNotFound
+  | NoPackageIdReportedBug
   deriving (Show, Typeable)
 
 instance Pretty ExecPrettyException where
   pretty (PackageIdNotFoundBug name) = bugPrettyReport "[S-8251]" $
-    "Could not find the package id of the package" <+>
-      style Target (fromString name)
-    <> "."
+    fillSep
+      [ flow "Could not find the package id of the package"
+      , style Target (fromString name) <> "."
+      ]
   pretty ExecutableToRunNotFound =
        "[S-2483]"
     <> line
     <> flow "No executables found."
+  pretty NoPackageIdReportedBug = bugPrettyReport "S-8600" $
+    flow "execCmd: findGhcPkgField returned Just \"\"."
 
 instance Exception ExecPrettyException
 
@@ -79,56 +84,57 @@   deriving (Eq, Show)
 
 data ExecOptsExtra = ExecOptsExtra
-  { eoEnvSettings :: !EnvSettings
-  , eoPackages :: ![String]
-  , eoRtsOptions :: ![String]
-  , eoCwd :: !(Maybe FilePath)
+  { envSettings :: !EnvSettings
+  , packages :: ![String]
+  , rtsOptions :: ![String]
+  , cwd :: !(Maybe FilePath)
   }
   deriving Show
 
 -- Type representing options for Stack's execution commands.
 data ExecOpts = ExecOpts
-  { eoCmd :: !SpecialExecCmd
-  , eoArgs :: ![String]
-  , eoExtra :: !ExecOptsExtra
+  { cmd :: !SpecialExecCmd
+  , args :: ![String]
+  , extra :: !ExecOptsExtra
   }
   deriving Show
 
 -- | The function underlying Stack's @exec@, @ghc@, @run@, @runghc@ and
 -- @runhaskell@ commands. Execute a command.
 execCmd :: ExecOpts -> RIO Runner ()
-execCmd ExecOpts {..} =
+execCmd opts =
   withConfig YesReexec $ withEnvConfig AllowNoTargets boptsCLI $ do
     unless (null targets) $ build Nothing
 
     config <- view configL
-    menv <- liftIO $ configProcessContextSettings config eoEnvSettings
+    menv <- liftIO $ config.processContextSettings eo.envSettings
     withProcessContext menv $ do
       -- Add RTS options to arguments
-      let argsWithRts args = if null eoRtsOptions
+      let argsWithRts args = if null eo.rtsOptions
                   then args :: [String]
-                  else args ++ ["+RTS"] ++ eoRtsOptions ++ ["-RTS"]
-      (cmd, args) <- case (eoCmd, argsWithRts eoArgs) of
+                  else args ++ ["+RTS"] ++ eo.rtsOptions ++ ["-RTS"]
+      (cmd, args) <- case (opts.cmd, argsWithRts opts.args) of
         (ExecCmd cmd, args) -> pure (cmd, args)
         (ExecRun, args) -> getRunCmd args
-        (ExecGhc, args) -> getGhcCmd eoPackages args
-        (ExecRunGhc, args) -> getRunGhcCmd eoPackages args
+        (ExecGhc, args) -> getGhcCmd eo.packages args
+        (ExecRunGhc, args) -> getRunGhcCmd eo.packages args
 
-      runWithPath eoCwd $ exec cmd args
+      runWithPath eo.cwd $ exec cmd args
  where
-  ExecOptsExtra {..} = eoExtra
+  eo = opts.extra
 
-  targets = concatMap words eoPackages
-  boptsCLI = defaultBuildOptsCLI
-             { boptsCLITargets = map T.pack targets
-             }
+  targets = concatMap words eo.packages
+  boptsCLI = defaultBuildOptsCLI { targetsCLI = map T.pack targets }
 
   -- return the package-id of the first package in GHC_PACKAGE_PATH
   getPkgId name = do
     pkg <- getGhcPkgExe
     mId <- findGhcPkgField pkg [] name "id"
     case mId of
-      Just i -> pure (L.head $ words (T.unpack i))
+      Just i -> maybe
+        (prettyThrowIO NoPackageIdReportedBug)
+        (pure . head)
+        (nonEmpty $ words $ T.unpack i)
       -- should never happen as we have already installed the packages
       _      -> prettyThrowIO (PackageIdNotFoundBug name)
 
@@ -136,16 +142,16 @@     map ("-package-id=" ++) <$> mapM getPkgId pkgs
 
   getRunCmd args = do
-    packages <- view $ buildConfigL.to (smwProject . bcSMWanted)
+    packages <- view $ buildConfigL . to (.smWanted.project)
     pkgComponents <- for (Map.elems packages) ppComponents
     let executables = concatMap (filter isCExe . Set.toList) pkgComponents
     let (exe, args') = case args of
-                       []   -> (firstExe, args)
-                       x:xs -> case L.find (\y -> y == CExe (T.pack x)) executables of
-                               Nothing -> (firstExe, args)
-                               argExe -> (argExe, xs)
-                       where
-                          firstExe = listToMaybe executables
+          [] -> (firstExe, args)
+          x:xs -> case L.find (\y -> y == CExe (T.pack x)) executables of
+            Nothing -> (firstExe, args)
+            argExe -> (argExe, xs)
+         where
+          firstExe = listToMaybe executables
     case exe of
       Just (CExe exe') -> do
         withNewLocalBuildTargets [T.cons ':' exe'] $ build Nothing
@@ -154,12 +160,12 @@ 
   getGhcCmd pkgs args = do
     pkgopts <- getPkgOpts pkgs
-    compiler <- view $ compilerPathsL.to cpCompiler
+    compiler <- view $ compilerPathsL . to (.compiler)
     pure (toFilePath compiler, pkgopts ++ args)
 
   getRunGhcCmd pkgs args = do
     pkgopts <- getPkgOpts pkgs
-    interpret <- view $ compilerPathsL.to cpInterpreter
+    interpret <- view $ compilerPathsL . to (.interpreter)
     pure (toFilePath interpret, pkgopts ++ args)
 
   runWithPath :: Maybe FilePath -> RIO EnvConfig () -> RIO EnvConfig ()
src/Stack/GhcPkg.hs view
@@ -1,6 +1,7 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE DataKinds         #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 -- | Functions for the GHC package database.
 
@@ -168,7 +169,7 @@   -> NonEmpty (Either PackageIdentifier GhcPkgId)
   -> RIO env ()
 unregisterGhcPkgIds isWarn pkgexe pkgDb epgids = do
-  globalDb <- view $ compilerPathsL.to cpGlobalDB
+  globalDb <- view $ compilerPathsL . to (.globalDB)
   eres <- try $ do
     ghcPkgUnregisterForce globalDb pkgDb hasIpid pkgarg_strs
     -- ghcPkgUnregisterForce does not perform an effective
src/Stack/Ghci.hs view
@@ -1,6 +1,8 @@-{-# LANGUAGE NoImplicitPrelude  #-}
-{-# LANGUAGE OverloadedStrings  #-}
-{-# LANGUAGE RecordWildCards    #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- | Types and functions related to Stack's @ghci@ and @repl@ commands.
 module Stack.Ghci
@@ -16,23 +18,18 @@ import           Data.ByteString.Builder ( byteString )
 import qualified Data.ByteString.Char8 as S8
 import qualified Data.ByteString.Lazy as LBS
-import           Data.Foldable ( foldl )
 import qualified Data.List as L
-import qualified Data.List.NonEmpty as NE
+import           Data.List.Extra ( (!?) )
 import qualified Data.Map.Strict as M
 import qualified Data.Set as S
 import qualified Data.Text as T
-import qualified Data.Text.Lazy as TL
-import qualified Data.Text.Lazy.Encoding as TLE
 import qualified Distribution.PackageDescription as C
 import           Path ((</>), parent, parseRelFile )
 import           Path.Extra ( forgivingResolveFile', toFilePathNoTrailingSep )
 import           Path.IO
                    ( XdgDirectory (..), doesFileExist, ensureDir, getXdgDir )
-import           RIO.Process
-                   ( HasProcessContext, exec, proc, readProcess_
-                   , withWorkingDir
-                   )
+import           RIO.NonEmpty ( nonEmpty )
+import           RIO.Process ( exec, withWorkingDir )
 import           Stack.Build ( buildLocalTargets )
 import           Stack.Build.Installed ( getInstalled, toInstallMap )
 import           Stack.Build.Source
@@ -44,23 +41,28 @@                    )
 import           Stack.Constants.Config ( ghciDirL, objectInterfaceDirL )
 import           Stack.Ghci.Script
-                   ( GhciScript, ModuleName, cmdAdd, cmdCdGhc, cmdModule
+                   ( GhciScript, ModuleName, cmdAdd, cmdModule
                    , scriptToLazyByteString
                    )
 import           Stack.Package
-                   ( PackageDescriptionPair (..), packageFromPackageDescription
-                   , readDotBuildinfo, resolvePackageDescription
+                   ( buildableExes, buildableForeignLibs, getPackageOpts
+                   , hasBuildableMainLibrary, listOfPackageDeps
+                   , packageFromPackageDescription, readDotBuildinfo
+                   , resolvePackageDescription, topSortPackageComponent
                    )
+import           Stack.PackageFile ( getPackageFile )
 import           Stack.Prelude
 import           Stack.Runners ( ShouldReexec (..), withConfig, withEnvConfig )
 import           Stack.Types.Build.Exception
                    ( BuildPrettyException (..), pprintTargetParseErrors )
 import           Stack.Types.BuildConfig
                    ( BuildConfig (..), HasBuildConfig (..), stackYamlL )
-import           Stack.Types.BuildOpts
-                   ( ApplyCLIFlag, BenchmarkOpts (..), BuildOpts (..)
-                   , BuildOptsCLI (..), TestOpts (..), defaultBuildOptsCLI
-                   )
+import           Stack.Types.BuildOpts ( BuildOpts (..) )
+import qualified Stack.Types.BuildOpts as BenchmarkOpts ( BenchmarkOpts (..) )
+import qualified Stack.Types.BuildOpts as TestOpts ( TestOpts (..) )
+import           Stack.Types.BuildOptsCLI
+                   ( ApplyCLIFlag, BuildOptsCLI (..), defaultBuildOptsCLI )
+import           Stack.Types.CompCollection ( getBuildableListText )
 import           Stack.Types.CompilerPaths
                    ( CompilerPaths (..), HasCompiler (..) )
 import           Stack.Types.Config ( Config (..), HasConfig (..), buildOptsL )
@@ -69,15 +71,17 @@                    , shaPathForBytes
                    )
 import           Stack.Types.EnvSettings ( defaultEnvSettings )
+import           Stack.Types.Installed ( InstallMap, InstalledMap )
 import           Stack.Types.NamedComponent
-                   ( NamedComponent (..), isCLib, renderPkgComponent )
+                   ( NamedComponent (..), isCLib, isCSubLib, renderComponentTo
+                   , renderPkgComponent
+                   )
 import           Stack.Types.Package
-                   ( BuildInfoOpts (..), InstallMap, InstalledMap
-                   , LocalPackage (..), Package (..), PackageConfig (..)
-                   , PackageLibraries (..), dotCabalCFilePath, dotCabalGetPath
-                   , dotCabalMainPath, getPackageOpts
+                   ( BuildInfoOpts (..), LocalPackage (..), Package (..)
+                   , PackageConfig (..), dotCabalCFilePath, dotCabalGetPath
+                   , dotCabalMainPath
                    )
-import           Stack.Types.PackageFile ( getPackageFiles )
+import           Stack.Types.PackageFile ( PackageComponentFile (..) )
 import           Stack.Types.Platform ( HasPlatform (..) )
 import           Stack.Types.Runner ( HasRunner, Runner )
 import           Stack.Types.SourceMap
@@ -91,9 +95,9 @@ -- | Type representing exceptions thrown by functions exported by the
 -- "Stack.Ghci" module.
 data GhciException
-  = InvalidPackageOption String
+  = InvalidPackageOption !String
   | LoadingDuplicateModules
-  | MissingFileTarget String
+  | MissingFileTarget !String
   | Can'tSpecifyFilesAndTargets
   | Can'tSpecifyFilesAndMainIs
   deriving (Show, Typeable)
@@ -121,8 +125,9 @@ 
 -- | Type representing \'pretty\' exceptions thrown by functions exported by the
 -- "Stack.Ghci" module.
-newtype GhciPrettyException
-  = GhciTargetParseException [StyleDoc]
+data GhciPrettyException
+  = GhciTargetParseException ![StyleDoc]
+  | CandidatesIndexOutOfRangeBug
   deriving (Show, Typeable)
 
 instance Pretty GhciPrettyException where
@@ -135,25 +140,27 @@          , style Shell "--ghci-options"
          , "option."
          ]
+  pretty CandidatesIndexOutOfRangeBug = bugPrettyReport "[S-1939]" $
+    flow "figureOutMainFile: index out of range."
 
 instance Exception GhciPrettyException
 
 -- | Typre respresenting command line options for the @stack ghci@ and
 -- @stack repl@ commands.
 data GhciOpts = GhciOpts
-  { ghciTargets            :: ![Text]
-  , ghciArgs               :: ![String]
-  , ghciGhcOptions         :: ![String]
-  , ghciFlags              :: !(Map ApplyCLIFlag (Map FlagName Bool))
-  , ghciGhcCommand         :: !(Maybe FilePath)
-  , ghciNoLoadModules      :: !Bool
-  , ghciAdditionalPackages :: ![String]
-  , ghciMainIs             :: !(Maybe Text)
-  , ghciLoadLocalDeps      :: !Bool
-  , ghciSkipIntermediate   :: !Bool
-  , ghciHidePackages       :: !(Maybe Bool)
-  , ghciNoBuild            :: !Bool
-  , ghciOnlyMain           :: !Bool
+  { targets            :: ![Text]
+  , args               :: ![String]
+  , ghcOptions         :: ![String]
+  , flags              :: !(Map ApplyCLIFlag (Map FlagName Bool))
+  , ghcCommand         :: !(Maybe FilePath)
+  , noLoadModules      :: !Bool
+  , additionalPackages :: ![String]
+  , mainIs             :: !(Maybe Text)
+  , loadLocalDeps      :: !Bool
+  , skipIntermediate   :: !Bool
+  , hidePackages       :: !(Maybe Bool)
+  , noBuild            :: !Bool
+  , onlyMain           :: !Bool
   }
   deriving Show
 
@@ -162,29 +169,30 @@ -- NOTE: GhciPkgInfo has paths as list instead of a Set to preserve files order
 -- as a workaround for bug https://ghc.haskell.org/trac/ghc/ticket/13786
 data GhciPkgInfo = GhciPkgInfo
-  { ghciPkgName :: !PackageName
-  , ghciPkgOpts :: ![(NamedComponent, BuildInfoOpts)]
-  , ghciPkgDir :: !(Path Abs Dir)
-  , ghciPkgModules :: !ModuleMap
-  , ghciPkgCFiles :: ![Path Abs File] -- ^ C files.
-  , ghciPkgMainIs :: !(Map NamedComponent [Path Abs File])
-  , ghciPkgTargetFiles :: !(Maybe [Path Abs File])
-  , ghciPkgPackage :: !Package
+  { name :: !PackageName
+  , opts :: ![(NamedComponent, BuildInfoOpts)]
+  , dir :: !(Path Abs Dir)
+  , modules :: !ModuleMap
+  , cFiles :: ![Path Abs File] -- ^ C files.
+  , mainIs :: !(Map NamedComponent [Path Abs File])
+  , targetFiles :: !(Maybe [Path Abs File])
+  , package :: !Package
   }
   deriving Show
 
 -- | Type representing loaded package description and related information.
 data GhciPkgDesc = GhciPkgDesc
-  { ghciDescPkg :: !Package
-  , ghciDescCabalFp :: !(Path Abs File)
-  , ghciDescTarget :: !Target
+  { package :: !Package
+  , cabalFP :: !(Path Abs File)
+  , target :: !Target
   }
 
--- Mapping from a module name to a map with all of the paths that use
--- that name. Each of those paths is associated with a set of components
--- that contain it. Purpose of this complex structure is for use in
+-- Mapping from a module name to a map with all of the paths that use that name.
+-- Each of those paths is associated with a set of components that contain it.
+-- The purpose of this complex structure is for use in
 -- 'checkForDuplicateModules'.
-type ModuleMap = Map ModuleName (Map (Path Abs File) (Set (PackageName, NamedComponent)))
+type ModuleMap =
+  Map ModuleName (Map (Path Abs File) (Set (PackageName, NamedComponent)))
 
 unionModuleMaps :: [ModuleMap] -> ModuleMap
 unionModuleMaps = M.unionsWith (M.unionWith S.union)
@@ -195,18 +203,18 @@ ghciCmd ghciOpts =
   let boptsCLI = defaultBuildOptsCLI
         -- using only additional packages, targets then get overridden in `ghci`
-        { boptsCLITargets = map T.pack (ghciAdditionalPackages  ghciOpts)
-        , boptsCLIInitialBuildSteps = True
-        , boptsCLIFlags = ghciFlags ghciOpts
-        , boptsCLIGhcOptions = map T.pack (ghciGhcOptions ghciOpts)
+        { targetsCLI = map T.pack ghciOpts.additionalPackages
+        , initialBuildSteps = True
+        , flags = ghciOpts.flags
+        , ghcOptions = map T.pack ghciOpts.ghcOptions
         }
   in  withConfig YesReexec $ withEnvConfig AllowNoTargets boptsCLI $ do
         bopts <- view buildOptsL
         -- override env so running of tests and benchmarks is disabled
         let boptsLocal = bopts
-              { boptsTestOpts = (boptsTestOpts bopts) { toDisableRun = True }
-              , boptsBenchmarkOpts =
-                  (boptsBenchmarkOpts bopts) { beoDisableRun = True }
+              { testOpts = bopts.testOpts { TestOpts.disableRun = True }
+              , benchmarkOpts =
+                  bopts.benchmarkOpts { BenchmarkOpts.disableRun = True }
               }
         local (set buildOptsL boptsLocal) (ghci ghciOpts)
 
@@ -214,28 +222,28 @@ -- given options and configure it with the load paths and extensions
 -- of those targets.
 ghci :: HasEnvConfig env => GhciOpts -> RIO env ()
-ghci opts@GhciOpts{..} = do
+ghci opts = do
   let buildOptsCLI = defaultBuildOptsCLI
-        { boptsCLITargets = []
-        , boptsCLIFlags = ghciFlags
+        { targetsCLI = []
+        , flags = opts.flags
         }
-  sourceMap <- view $ envConfigL.to envConfigSourceMap
+  sourceMap <- view $ envConfigL . to (.sourceMap)
   installMap <- toInstallMap sourceMap
   locals <- projectLocalPackages
   depLocals <- localDependencies
   let localMap =
-        M.fromList [(packageName $ lpPackage lp, lp) | lp <- locals ++ depLocals]
+        M.fromList [(lp.package.name, lp) | lp <- locals ++ depLocals]
       -- FIXME:qrilka this looks wrong to go back to SMActual
       sma = SMActual
-        { smaCompiler = smCompiler sourceMap
-        , smaProject = smProject sourceMap
-        , smaDeps = smDeps sourceMap
-        , smaGlobal = smGlobal sourceMap
+        { compiler = sourceMap.compiler
+        , project = sourceMap.project
+        , deps = sourceMap.deps
+        , globals = sourceMap.globalPkgs
         }
   -- Parse --main-is argument.
-  mainIsTargets <- parseMainIsTargets buildOptsCLI sma ghciMainIs
+  mainIsTargets <- parseMainIsTargets buildOptsCLI sma opts.mainIs
   -- Parse to either file targets or build targets
-  etargets <- preprocessTargets buildOptsCLI sma ghciTargets
+  etargets <- preprocessTargets buildOptsCLI sma opts.targets
   (inputTargets, mfileTargets) <- case etargets of
     Right packageTargets -> pure (packageTargets, Nothing)
     Left rawFileTargets -> do
@@ -249,13 +257,18 @@   localTargets <- getAllLocalTargets opts inputTargets mainIsTargets localMap
   -- Get a list of all the non-local target packages.
   nonLocalTargets <- getAllNonLocalTargets inputTargets
+  let getInternalDependencies target localPackage =
+        topSortPackageComponent localPackage.package target False
+      internalDependencies =
+        M.intersectionWith getInternalDependencies inputTargets localMap
+      relevantDependencies = M.filter (any isCSubLib) internalDependencies
   -- Check if additional package arguments are sensible.
-  addPkgs <- checkAdditionalPackages ghciAdditionalPackages
+  addPkgs <- checkAdditionalPackages opts.additionalPackages
   -- Load package descriptions.
   pkgDescs <- loadGhciPkgDescs buildOptsCLI localTargets
   -- If necessary, ask user about which main module to load.
   bopts <- view buildOptsL
-  mainFile <- if ghciNoLoadModules
+  mainFile <- if opts.noLoadModules
     then pure Nothing
     else do
       -- Figure out package files, in order to ask the user about which main
@@ -284,6 +297,7 @@     pkgs
     (maybe [] snd mfileTargets)
     (nonLocalTargets ++ addPkgs)
+    relevantDependencies
 
 preprocessTargets ::
      HasEnvConfig env
@@ -309,7 +323,7 @@     else do
       -- Try parsing targets before checking if both file and
       -- module targets are specified (see issue#3342).
-      let boptsCLI = buildOptsCLI { boptsCLITargets = normalTargetsRaw }
+      let boptsCLI = buildOptsCLI { targetsCLI = normalTargetsRaw }
       normalTargets <- parseTargets AllowNoTargets False boptsCLI sma
         `catch` \pex@(PrettyException ex) ->
           case fromException $ toException ex of
@@ -317,7 +331,7 @@               prettyThrowM $ GhciTargetParseException xs
             _ -> throwM pex
       unless (null fileTargetsRaw) $ throwM Can'tSpecifyFilesAndTargets
-      pure (Right $ smtTargets normalTargets)
+      pure (Right normalTargets.targets)
 
 parseMainIsTargets ::
      HasEnvConfig env
@@ -326,9 +340,9 @@   -> Maybe Text
   -> RIO env (Maybe (Map PackageName Target))
 parseMainIsTargets buildOptsCLI sma mtarget = forM mtarget $ \target -> do
-  let boptsCLI = buildOptsCLI { boptsCLITargets = [target] }
+  let boptsCLI = buildOptsCLI { targetsCLI = [target] }
   targets <- parseTargets AllowNoTargets False boptsCLI sma
-  pure $ smtTargets targets
+  pure targets.targets
 
 -- | Display PackageName + NamedComponent
 displayPkgComponent :: (PackageName, NamedComponent) -> StyleDoc
@@ -342,12 +356,12 @@   -> RIO env (Map PackageName Target, Map PackageName [Path Abs File], [Path Abs File])
 findFileTargets locals fileTargets = do
   filePackages <- forM locals $ \lp -> do
-    (_,compFiles,_,_) <- getPackageFiles (packageFiles (lpPackage lp)) (lpCabalFile lp)
+    PackageComponentFile _ compFiles _ _ <- getPackageFile lp.package lp.cabalFP
     pure (lp, M.map (map dotCabalGetPath) compFiles)
   let foundFileTargetComponents :: [(Path Abs File, [(PackageName, NamedComponent)])]
       foundFileTargetComponents =
         map (\fp -> (fp, ) $ L.sort $
-                    concatMap (\(lp, files) -> map ((packageName (lpPackage lp), ) . fst)
+                    concatMap (\(lp, files) -> map ((lp.package.name,) . fst)
                                                    (filter (elem fp . snd) (M.toList files))
                               ) filePackages
             ) fileTargets
@@ -384,11 +398,11 @@         pure $ Right (fp, x)
   let (extraFiles, associatedFiles) = partitionEithers results
       targetMap =
-          foldl unionTargets M.empty $
+          foldl' unionTargets M.empty $
           map (\(_, (name, comp)) -> M.singleton name (TargetComps (S.singleton comp)))
               associatedFiles
       infoMap =
-          foldl (M.unionWith (<>)) M.empty $
+          foldl' (M.unionWith (<>)) M.empty $
           map (\(fp, (name, _)) -> M.singleton name [fp])
               associatedFiles
   pure (targetMap, infoMap, extraFiles)
@@ -400,29 +414,31 @@   -> Maybe (Map PackageName Target)
   -> Map PackageName LocalPackage
   -> RIO env [(PackageName, (Path Abs File, Target))]
-getAllLocalTargets GhciOpts{..} targets0 mainIsTargets localMap = do
+getAllLocalTargets ghciOpts targets0 mainIsTargets localMap = do
   -- Use the 'mainIsTargets' as normal targets, for CLI concision. See
   -- #1845. This is a little subtle - we need to do the target parsing
   -- independently in order to handle the case where no targets are
   -- specified.
   let targets = maybe targets0 (unionTargets targets0) mainIsTargets
-  packages <- view $ envConfigL.to envConfigSourceMap.to smProject
+  packages <- view $ envConfigL . to (.sourceMap.project)
   -- Find all of the packages that are directly demanded by the
   -- targets.
   let directlyWanted = flip mapMaybe (M.toList packages) $
         \(name, pp) ->
               case M.lookup name targets of
-                Just simpleTargets -> Just (name, (ppCabalFP pp, simpleTargets))
+                Just simpleTargets -> Just (name, (pp.cabalFP, simpleTargets))
                 Nothing -> Nothing
   -- Figure out
-  let extraLoadDeps = getExtraLoadDeps ghciLoadLocalDeps localMap directlyWanted
-  if (ghciSkipIntermediate && not ghciLoadLocalDeps) || null extraLoadDeps
+  let extraLoadDeps =
+        getExtraLoadDeps ghciOpts.loadLocalDeps localMap directlyWanted
+  if    (ghciOpts.skipIntermediate && not ghciOpts.loadLocalDeps)
+     || null extraLoadDeps
     then pure directlyWanted
     else do
       let extraList' =
-            map (fromString . packageNameString . fst) extraLoadDeps :: [StyleDoc]
+            map (fromPackageName . fst) extraLoadDeps :: [StyleDoc]
           extraList = mkNarrativeList (Just Current) False extraList'
-      if ghciLoadLocalDeps
+      if ghciOpts.loadLocalDeps
         then prettyInfo $
           fillSep $
                 [ flow "The following libraries will also be loaded into \
@@ -455,13 +471,13 @@   pure $ map fst $ filter (isNonLocal . snd) (M.toList targets)
 
 buildDepsAndInitialSteps :: HasEnvConfig env => GhciOpts -> [Text] -> RIO env ()
-buildDepsAndInitialSteps GhciOpts{..} localTargets = do
-  let targets = localTargets ++ map T.pack ghciAdditionalPackages
+buildDepsAndInitialSteps ghciOpts localTargets = do
+  let targets = localTargets ++ map T.pack ghciOpts.additionalPackages
   -- If necessary, do the build, for local packagee targets, only do
   -- 'initialBuildSteps'.
-  case NE.nonEmpty targets of
+  case nonEmpty targets of
     -- only new local targets could appear here
-    Just nonEmptyTargets | not ghciNoBuild -> do
+    Just nonEmptyTargets | not ghciOpts.noBuild -> do
       eres <- buildLocalTargets nonEmptyTargets
       case eres of
         Right () -> pure ()
@@ -487,100 +503,117 @@   -> [GhciPkgInfo]
   -> [Path Abs File]
   -> [PackageName]
+  -> Map PackageName (Seq NamedComponent)
   -> RIO env ()
-runGhci GhciOpts{..} targets mainFile pkgs extraFiles exposePackages = do
-    config <- view configL
-    let pkgopts = hidePkgOpts ++ genOpts ++ ghcOpts
-        shouldHidePackages =
-          fromMaybe (not (null pkgs && null exposePackages)) ghciHidePackages
-        hidePkgOpts =
-          if shouldHidePackages
-            then
-              ["-hide-all-packages"] ++
-              -- This is necessary, because current versions of ghci
-              -- will entirely fail to start if base isn't visible. This
-              -- is because it tries to use the interpreter to set
-              -- buffering options on standard IO.
-              (if null targets then ["-package", "base"] else []) ++
-              concatMap (\n -> ["-package", packageNameString n]) exposePackages
-            else []
-        oneWordOpts bio
-            | shouldHidePackages = bioOneWordOpts bio ++ bioPackageFlags bio
-            | otherwise = bioOneWordOpts bio
-        genOpts = nubOrd (concatMap (concatMap (oneWordOpts . snd) . ghciPkgOpts) pkgs)
-        (omittedOpts, ghcOpts) = L.partition badForGhci $
-            concatMap (concatMap (bioOpts . snd) . ghciPkgOpts) pkgs ++ map T.unpack
-              ( fold (configGhcOptionsByCat config) -- include everything, locals, and targets
-             ++ concatMap (getUserOptions . ghciPkgName) pkgs
-              )
-        getUserOptions pkg = M.findWithDefault [] pkg (configGhcOptionsByName config)
-        badForGhci x =
-            L.isPrefixOf "-O" x || elem x (words "-debug -threaded -ticky -static -Werror")
-    unless (null omittedOpts) $
-      prettyWarn $
-           fillSep
-             ( flow "The following GHC options are incompatible with GHCi and \
-                    \have not been passed to it:"
-             : mkNarrativeList (Just Current) False
-                 (map fromString (nubOrd omittedOpts) :: [StyleDoc])
-             )
-        <> line
-    oiDir <- view objectInterfaceDirL
-    let odir =
+runGhci
+    ghciOpts
+    targets
+    mainFile
+    pkgs
+    extraFiles
+    exposePackages
+    exposeInternalDep
+  = do
+      config <- view configL
+      let subDepsPackageUnhide pName deps =
+            if null deps then [] else ["-package", fromPackageName pName]
+          pkgopts = hidePkgOpts ++ genOpts ++ ghcOpts
+          shouldHidePackages = fromMaybe
+            (not (null pkgs && null exposePackages))
+            ghciOpts.hidePackages
+          hidePkgOpts =
+            if shouldHidePackages
+              then
+                   ["-hide-all-packages"]
+                -- This is necessary, because current versions of ghci will
+                -- entirely fail to start if base isn't visible. This is because
+                -- it tries to use the interpreter to set buffering options on
+                -- standard IO.
+                ++ (if null targets then ["-package", "base"] else [])
+                ++ concatMap
+                     (\n -> ["-package", packageNameString n])
+                     exposePackages
+                ++ M.foldMapWithKey subDepsPackageUnhide exposeInternalDep
+              else []
+          oneWordOpts bio
+            | shouldHidePackages = bio.oneWordOpts ++ bio.packageFlags
+            | otherwise = bio.oneWordOpts
+          genOpts = nubOrd
+            (concatMap (concatMap (oneWordOpts . snd) . (.opts)) pkgs)
+          (omittedOpts, ghcOpts) = L.partition badForGhci $
+               concatMap (concatMap ((.opts) . snd) . (.opts)) pkgs
+            ++ map
+                 T.unpack
+                 (  fold config.ghcOptionsByCat
+                    -- ^ include everything, locals, and targets
+                 ++ concatMap (getUserOptions . (.name)) pkgs
+                 )
+          getUserOptions pkg =
+            M.findWithDefault [] pkg config.ghcOptionsByName
+          badForGhci x =
+               L.isPrefixOf "-O" x
+            || elem x (words "-debug -threaded -ticky -static -Werror")
+      unless (null omittedOpts) $
+        prettyWarn $
+             fillSep
+               ( flow "The following GHC options are incompatible with GHCi \
+                      \and have not been passed to it:"
+               : mkNarrativeList (Just Current) False
+                   (map fromString (nubOrd omittedOpts) :: [StyleDoc])
+               )
+          <> line
+      oiDir <- view objectInterfaceDirL
+      let odir =
             [ "-odir=" <> toFilePathNoTrailingSep oiDir
-            , "-hidir=" <> toFilePathNoTrailingSep oiDir ]
-    prettyInfoL
-      ( flow "Configuring GHCi with the following packages:"
-      : mkNarrativeList (Just Current) False
-          (map (fromString . packageNameString . ghciPkgName) pkgs :: [StyleDoc])
-      )
-    compilerExeName <- view $ compilerPathsL.to cpCompiler.to toFilePath
-    let execGhci extras = do
-            menv <- liftIO $ configProcessContextSettings config defaultEnvSettings
+            , "-hidir=" <> toFilePathNoTrailingSep oiDir
+            ]
+      prettyInfoL
+        ( flow "Configuring GHCi with the following packages:"
+        : mkNarrativeList (Just Current) False
+            (map (fromPackageName . (.name)) pkgs :: [StyleDoc])
+        )
+      compilerExeName <-
+        view $ compilerPathsL . to (.compiler) . to toFilePath
+      let execGhci extras = do
+            menv <-
+              liftIO $ config.processContextSettings defaultEnvSettings
             withPackageWorkingDir $ withProcessContext menv $ exec
-                 (fromMaybe compilerExeName ghciGhcCommand)
-                 (("--interactive" : ) $
-                 -- This initial "-i" resets the include directories to
-                 -- not include CWD. If there aren't any packages, CWD
-                 -- is included.
-                  (if null pkgs then id else ("-i" : )) $
-                  odir <> pkgopts <> extras <> ghciGhcOptions <> ghciArgs)
-        withPackageWorkingDir =
+              (fromMaybe compilerExeName ghciOpts.ghcCommand)
+              ( ("--interactive" : ) $
+                -- This initial "-i" resets the include directories to not
+                -- include CWD. If there aren't any packages, CWD is included.
+                (if null pkgs then id else ("-i" : )) $
+                     odir
+                  <> pkgopts
+                  <> extras
+                  <> ghciOpts.ghcOptions
+                  <> ghciOpts.args
+              )
+          withPackageWorkingDir =
             case pkgs of
-              [pkg] -> withWorkingDir (toFilePath $ ghciPkgDir pkg)
+              [pkg] -> withWorkingDir (toFilePath pkg.dir)
               _ -> id
-        -- TODO: Consider optimizing this check. Perhaps if no
-        -- "with-ghc" is specified, assume that it is not using intero.
-        checkIsIntero =
-            -- Optimization dependent on the behavior of renderScript -
-            -- it doesn't matter if it's intero or ghci when loading
-            -- multiple packages.
-            case pkgs of
-                [_] -> do
-                    menv <- liftIO $ configProcessContextSettings config defaultEnvSettings
-                    output <- withProcessContext menv
-                            $ runGrabFirstLine (fromMaybe compilerExeName ghciGhcCommand) ["--version"]
-                    pure $ "Intero" `L.isPrefixOf` output
-                _ -> pure False
-    -- Since usage of 'exec' does not pure, we cannot do any cleanup
-    -- on ghci exit. So, instead leave the generated files. To make this
-    -- more efficient and avoid gratuitous generation of garbage, the
-    -- file names are determined by hashing. This also has the nice side
-    -- effect of making it possible to copy the ghci invocation out of
-    -- the log and have it still work.
-    tmpDirectory <- getXdgDir XdgCache $
-      Just (relDirStackProgName </> relDirGhciScript)
-    ghciDir <- view ghciDirL
-    ensureDir ghciDir
-    ensureDir tmpDirectory
-    macrosOptions <- writeMacrosFile ghciDir pkgs
-    if ghciNoLoadModules
+      -- Since usage of 'exec' does not pure, we cannot do any cleanup on ghci
+      -- exit. So, instead leave the generated files. To make this more
+      -- efficient and avoid gratuitous generation of garbage, the file names
+      -- are determined by hashing. This also has the nice side effect of making
+      -- it possible to copy the ghci invocation out of the log and have it
+      -- still work.
+      tmpDirectory <- getXdgDir XdgCache $
+        Just (relDirStackProgName </> relDirGhciScript)
+      ghciDir <- view ghciDirL
+      ensureDir ghciDir
+      ensureDir tmpDirectory
+      macrosOptions <- writeMacrosFile ghciDir pkgs
+      if ghciOpts.noLoadModules
         then execGhci macrosOptions
         else do
-            checkForDuplicateModules pkgs
-            isIntero <- checkIsIntero
-            scriptOptions <- writeGhciScript tmpDirectory (renderScript isIntero pkgs mainFile ghciOnlyMain extraFiles)
-            execGhci (macrosOptions ++ scriptOptions)
+          checkForDuplicateModules pkgs
+          scriptOptions <-
+            writeGhciScript
+              tmpDirectory
+              (renderScript pkgs mainFile ghciOpts.onlyMain extraFiles)
+          execGhci (macrosOptions ++ scriptOptions)
 
 writeMacrosFile ::
      HasTerm env
@@ -588,9 +621,9 @@   -> [GhciPkgInfo]
   -> RIO env [String]
 writeMacrosFile outputDirectory pkgs = do
-  fps <- fmap (nubOrd . catMaybes . concat) $
-    forM pkgs $ \pkg -> forM (ghciPkgOpts pkg) $ \(_, bio) -> do
-      let cabalMacros = bioCabalMacros bio
+  fps <- fmap (nubOrd . concatMap catMaybes) $
+    forM pkgs $ \pkg -> forM pkg.opts $ \(_, bio) -> do
+      let cabalMacros = bio.cabalMacros
       exists <- liftIO $ doesFileExist cabalMacros
       if exists
         then pure $ Just cabalMacros
@@ -600,7 +633,9 @@   files <- liftIO $ mapM (S8.readFile . toFilePath) fps
   if null files then pure [] else do
     out <- liftIO $ writeHashedFile outputDirectory relFileCabalMacrosH $
-      S8.concat $ map (<> "\n#undef CURRENT_PACKAGE_KEY\n#undef CURRENT_COMPONENT_ID\n") files
+      S8.concat $ map
+        (<> "\n#undef CURRENT_PACKAGE_KEY\n#undef CURRENT_COMPONENT_ID\n")
+        files
     pure ["-optP-include", "-optP" <> toFilePath out]
 
 writeGhciScript :: (MonadIO m) => Path Abs Dir -> GhciScript -> m [String]
@@ -627,39 +662,33 @@   pure outFile
 
 renderScript ::
-     Bool
-  -> [GhciPkgInfo]
+     [GhciPkgInfo]
   -> Maybe (Path Abs File)
   -> Bool
   -> [Path Abs File]
   -> GhciScript
-renderScript isIntero pkgs mainFile onlyMain extraFiles = do
-  let cdPhase = case (isIntero, pkgs) of
-        -- If only loading one package, set the cwd properly.
-        -- Otherwise don't try. See
-        -- https://github.com/commercialhaskell/stack/issues/3309
-        (True, [pkg]) -> cmdCdGhc (ghciPkgDir pkg)
-        _ -> mempty
-      addPhase = cmdAdd $ S.fromList (map Left allModules ++ addMain)
-      addMain = case mainFile of
-          Just path -> [Right path]
-          _ -> []
+renderScript pkgs mainFile onlyMain extraFiles = do
+  let addPhase = cmdAdd $ S.fromList (map Left allModules ++ addMain)
+      addMain = maybe [] (L.singleton . Right) mainFile
       modulePhase = cmdModule $ S.fromList allModules
-      allModules = nubOrd $ concatMap (M.keys . ghciPkgModules) pkgs
+      allModules = nubOrd $ concatMap (M.keys . (.modules)) pkgs
   case getFileTargets pkgs <> extraFiles of
     [] ->
       if onlyMain
-        then cdPhase <> if isJust mainFile then cmdAdd (S.fromList addMain) else mempty
-        else cdPhase <> addPhase <> modulePhase
+        then
+          if isJust mainFile
+            then cmdAdd (S.fromList addMain)
+            else mempty
+        else addPhase <> modulePhase
     fileTargets -> cmdAdd (S.fromList (map Right fileTargets))
 
 -- Hacky check if module / main phase should be omitted. This should be
 -- improved if / when we have a better per-component load.
 getFileTargets :: [GhciPkgInfo] -> [Path Abs File]
-getFileTargets = concatMap (concat . maybeToList . ghciPkgTargetFiles)
+getFileTargets = concatMap (concat . maybeToList . (.targetFiles))
 
--- | Figure out the main-is file to load based on the targets. Asks the
--- user for input if there is more than one candidate main-is file.
+-- | Figure out the main-is file to load based on the targets. Asks the user for
+-- input if there is more than one candidate main-is file.
 figureOutMainFile ::
      (HasRunner env, HasTerm env)
   => BuildOpts
@@ -668,93 +697,94 @@   -> [GhciPkgInfo]
   -> RIO env (Maybe (Path Abs File))
 figureOutMainFile bopts mainIsTargets targets0 packages =
-    case candidates of
-      [] -> pure Nothing
-      [c@(_,_,fp)] -> do
-        prettyInfo $
-             fillSep
-               [ "Using"
-               , style Current "main"
-               , "module:"
-               ]
-          <> line
-          <> renderCandidate c
-          <> line
-        pure (Just fp)
-      candidate:_ -> do
-        prettyWarn $
-             fillSep
-               [ "The"
-               , style Current "main"
-               , flow "module to load is ambiguous. Candidates are:"
-               ]
-          <> line
-          <> mconcat (L.intersperse line (map renderCandidate candidates))
-          <> blankLine
-          <> flow "You can specify which one to pick by:"
-          <> line
-          <> bulletedList
-               [ fillSep
-                   [ flow "Specifying targets to"
-                   , style Shell (flow "stack ghci")
-                   , "e.g."
-                   , style Shell ( fillSep
-                                     [ flow "stack ghci"
-                                     , sampleTargetArg candidate
-                                     ]
-                                 ) <> "."
-                   ]
-               , fillSep
-                   [ flow "Specifying what the"
-                   , style Current "main"
-                   , flow "is e.g."
-                   , style Shell ( fillSep
-                                     [ flow "stack ghci"
-                                     , sampleMainIsArg candidate
-                                     ]
-                                 ) <> "."
-                   ]
-                , flow
-                    $  "Choosing from the candidate above [1.."
-                    <> show (length candidates)
-                    <> "]."
-               ]
-          <> line
-        liftIO userOption
+  case candidates of
+    [] -> pure Nothing
+    [c@(_,_,fp)] -> do
+      prettyInfo $
+           fillSep
+             [ "Using"
+             , style Current "main"
+             , "module:"
+             ]
+        <> line
+        <> renderCandidate c
+        <> line
+      pure (Just fp)
+    candidate:_ -> do
+      prettyWarn $
+           fillSep
+             [ "The"
+             , style Current "main"
+             , flow "module to load is ambiguous. Candidates are:"
+             ]
+        <> line
+        <> mconcat (L.intersperse line (map renderCandidate candidates))
+        <> blankLine
+        <> flow "You can specify which one to pick by:"
+        <> line
+        <> bulletedList
+             [ fillSep
+                 [ flow "Specifying targets to"
+                 , style Shell (flow "stack ghci")
+                 , "e.g."
+                 , style Shell ( fillSep
+                                   [ flow "stack ghci"
+                                   , sampleTargetArg candidate
+                                   ]
+                               ) <> "."
+                 ]
+             , fillSep
+                 [ flow "Specifying what the"
+                 , style Current "main"
+                 , flow "is e.g."
+                 , style Shell ( fillSep
+                                   [ flow "stack ghci"
+                                   , sampleMainIsArg candidate
+                                   ]
+                               ) <> "."
+                 ]
+              , flow
+                  $  "Choosing from the candidate above [1.."
+                  <> show (length candidates)
+                  <> "]."
+             ]
+        <> line
+      liftIO userOption
  where
-  targets = fromMaybe (M.fromList $ map (\(k, (_, x)) -> (k, x)) targets0)
-                      mainIsTargets
+  targets = fromMaybe
+    (M.fromList $ map (\(k, (_, x)) -> (k, x)) targets0)
+    mainIsTargets
   candidates = do
     pkg <- packages
-    case M.lookup (ghciPkgName pkg) targets of
+    case M.lookup pkg.name targets of
       Nothing -> []
       Just target -> do
         (component,mains) <-
-            M.toList $
-            M.filterWithKey (\k _ -> k `S.member` wantedComponents)
-                            (ghciPkgMainIs pkg)
+          M.toList $
+          M.filterWithKey (\k _ -> k `S.member` wantedComponents)
+                          pkg.mainIs
         main <- mains
-        pure (ghciPkgName pkg, component, main)
+        pure (pkg.name, component, main)
        where
         wantedComponents =
-          wantedPackageComponents bopts target (ghciPkgPackage pkg)
+          wantedPackageComponents bopts target pkg.package
   renderCandidate c@(pkgName, namedComponent, mainIs) =
     let candidateIndex =
           fromString . show . (+1) . fromMaybe 0 . L.elemIndex c
-        pkgNameText = fromString $ packageNameString pkgName
+        pkgNameText = fromPackageName pkgName
     in  hang 4
           $  fill 4 ( candidateIndex candidates <> ".")
           <> fillSep
                [ "Package"
                , style Current pkgNameText <> ","
                , "component"
-        --       This is the format that can be directly copy-pasted as
-        --       an argument to `stack ghci`.
+                 -- This is the format that can be directly copy-pasted as an
+                 -- argument to `stack ghci`.
                , style
                    PkgComponent
                    (  pkgNameText
                    <> ":"
-                   <> renderComp namedComponent
+                   <> renderComponentTo namedComponent
                    )
                  <> ","
                , "with"
@@ -766,37 +796,33 @@   userOption = do
     option <- prompt "Specify main module to use (press enter to load none): "
     let selected = fromMaybe
-                    ((+1) $ length candidateIndices)
-                    (readMaybe (T.unpack option) :: Maybe Int)
-    case L.elemIndex selected candidateIndices  of
+          ((+1) $ length candidateIndices)
+          (readMaybe (T.unpack option) :: Maybe Int)
+    case L.elemIndex selected candidateIndices of
       Nothing -> do
         putStrLn
           "Not loading any main modules, as no valid module selected"
         putStrLn ""
         pure Nothing
       Just op -> do
-        let (_,_,fp) = candidates L.!! op
+        (_, _, fp) <- maybe
+          (prettyThrowIO CandidatesIndexOutOfRangeBug)
+          pure
+          (candidates !? op)
         putStrLn
           ("Loading main module from candidate " <>
           show (op + 1) <> ", --main-is " <>
           toFilePath fp)
         putStrLn ""
         pure $ Just fp
-  renderComp c =
-    case c of
-      CLib -> "lib"
-      CInternalLib name -> "internal-lib:" <> fromString (T.unpack name)
-      CExe name -> "exe:" <> fromString (T.unpack name)
-      CTest name -> "test:" <> fromString ( T.unpack name)
-      CBench name -> "bench:" <> fromString (T.unpack name)
   sampleTargetArg (pkg, comp, _) =
-       fromString (packageNameString pkg)
+       fromPackageName pkg
     <> ":"
-    <> renderComp comp
+    <> renderComponentTo comp
   sampleMainIsArg (pkg, comp, _) =
     fillSep
       [ "--main-is"
-      , fromString (packageNameString pkg) <> ":" <> renderComp comp
+      , fromPackageName pkg <> ":" <> renderComponentTo comp
       ]
 
 loadGhciPkgDescs ::
@@ -805,8 +831,8 @@   -> [(PackageName, (Path Abs File, Target))]
   -> RIO env [GhciPkgDesc]
 loadGhciPkgDescs buildOptsCLI localTargets =
-  forM localTargets $ \(name, (cabalfp, target)) ->
-    loadGhciPkgDesc buildOptsCLI name cabalfp target
+  forM localTargets $ \(name, (cabalFP, target)) ->
+    loadGhciPkgDesc buildOptsCLI name cabalFP target
 
 -- | Load package description information for a ghci target.
 loadGhciPkgDesc ::
@@ -816,63 +842,56 @@   -> Path Abs File
   -> Target
   -> RIO env GhciPkgDesc
-loadGhciPkgDesc buildOptsCLI name cabalfp target = do
+loadGhciPkgDesc buildOptsCLI name cabalFP target = do
   econfig <- view envConfigL
   compilerVersion <- view actualCompilerVersionL
-  let SourceMap{..} = envConfigSourceMap econfig
+  let sm = econfig.sourceMap
       -- Currently this source map is being build with
       -- the default targets
       sourceMapGhcOptions = fromMaybe [] $
-        (cpGhcOptions . ppCommon <$> M.lookup name smProject)
+        ((.projectCommon.ghcOptions) <$> M.lookup name sm.project)
         <|>
-        (cpGhcOptions . dpCommon <$> M.lookup name smDeps)
+        ((.depCommon.ghcOptions) <$> M.lookup name sm.deps)
       sourceMapCabalConfigOpts = fromMaybe [] $
-        (cpCabalConfigOpts . ppCommon <$> M.lookup name smProject)
+        ( (.projectCommon.cabalConfigOpts) <$> M.lookup name sm.project)
         <|>
-        (cpCabalConfigOpts . dpCommon <$> M.lookup name smDeps)
+        ((.depCommon.cabalConfigOpts) <$> M.lookup name sm.deps)
       sourceMapFlags =
-        maybe mempty (cpFlags . ppCommon) $ M.lookup name smProject
+        maybe mempty (.projectCommon.flags) $ M.lookup name sm.project
       config = PackageConfig
-        { packageConfigEnableTests = True
-        , packageConfigEnableBenchmarks = True
-        , packageConfigFlags =
+        { enableTests = True
+        , enableBenchmarks = True
+        , flags =
             getLocalFlags buildOptsCLI name `M.union` sourceMapFlags
-        , packageConfigGhcOptions = sourceMapGhcOptions
-        , packageConfigCabalConfigOpts = sourceMapCabalConfigOpts
-        , packageConfigCompilerVersion = compilerVersion
-        , packageConfigPlatform = view platformL econfig
+        , ghcOptions = sourceMapGhcOptions
+        , cabalConfigOpts = sourceMapCabalConfigOpts
+        , compilerVersion = compilerVersion
+        , platform = view platformL econfig
         }
-  -- TODO we've already parsed this information, otherwise we
-  -- wouldn't have figured out the cabalfp already. In the future:
-  -- retain that GenericPackageDescription in the relevant data
-  -- structures to avoid reparsing.
-  (gpdio, _name, _cabalfp) <-
-      loadCabalFilePath (Just stackProgName') (parent cabalfp)
+  -- TODO we've already parsed this information, otherwise we wouldn't have
+  -- figured out the cabalFP already. In the future: retain that
+  -- GenericPackageDescription in the relevant data structures to avoid
+  -- reparsing.
+  (gpdio, _name, _cabalFP) <-
+    loadCabalFilePath (Just stackProgName') (parent cabalFP)
   gpkgdesc <- liftIO $ gpdio YesPrintWarnings
 
   -- Source the package's *.buildinfo file created by configure if any. See
   -- https://www.haskell.org/cabal/users-guide/developing-packages.html#system-dependent-parameters
   buildinfofp <- parseRelFile (packageNameString name ++ ".buildinfo")
-  hasDotBuildinfo <- doesFileExist (parent cabalfp </> buildinfofp)
+  hasDotBuildinfo <- doesFileExist (parent cabalFP </> buildinfofp)
   let mbuildinfofp
-        | hasDotBuildinfo = Just (parent cabalfp </> buildinfofp)
+        | hasDotBuildinfo = Just (parent cabalFP </> buildinfofp)
         | otherwise = Nothing
   mbuildinfo <- forM mbuildinfofp readDotBuildinfo
   let pdp = resolvePackageDescription config gpkgdesc
-      pkg =
+      package =
         packageFromPackageDescription config (C.genPackageFlags gpkgdesc) $
-        maybe
-          pdp
-          (\bi ->
-           let PackageDescriptionPair x y = pdp
-           in  PackageDescriptionPair
-                 (C.updatePackageDescription bi x)
-                 (C.updatePackageDescription bi y))
-          mbuildinfo
+          maybe pdp (`C.updatePackageDescription` pdp) mbuildinfo
   pure GhciPkgDesc
-    { ghciDescPkg = pkg
-    , ghciDescCabalFp = cabalfp
-    , ghciDescTarget = target
+    { package
+    , cabalFP
+    , target
     }
 
 getGhciPkgInfos ::
@@ -885,9 +904,9 @@ getGhciPkgInfos installMap addPkgs mfileTargets localTargets = do
   (installedMap, _, _, _) <- getInstalled installMap
   let localLibs =
-        [ packageName (ghciDescPkg desc)
+        [ desc.package.name
         | desc <- localTargets
-        , hasLocalComp isCLib (ghciDescTarget desc)
+        , hasLocalComp isCLib desc.target
         ]
   forM localTargets $ \pkgDesc ->
     makeGhciPkgInfo installMap installedMap localLibs addPkgs mfileTargets pkgDesc
@@ -904,25 +923,31 @@   -> RIO env GhciPkgInfo
 makeGhciPkgInfo installMap installedMap locals addPkgs mfileTargets pkgDesc = do
   bopts <- view buildOptsL
-  let pkg = ghciDescPkg pkgDesc
-      cabalfp = ghciDescCabalFp pkgDesc
-      target = ghciDescTarget pkgDesc
-      name = packageName pkg
-  (mods,files,opts) <- getPackageOpts (packageOpts pkg) installMap installedMap locals addPkgs cabalfp
+  let pkg = pkgDesc.package
+      cabalFP = pkgDesc.cabalFP
+      target = pkgDesc.target
+      name = pkg.name
+  (mods, files, opts) <-
+    getPackageOpts pkg installMap installedMap locals addPkgs cabalFP
   let filteredOpts = filterWanted opts
       filterWanted = M.filterWithKey (\k _ -> k `S.member` allWanted)
       allWanted = wantedPackageComponents bopts target pkg
   pure GhciPkgInfo
-    { ghciPkgName = name
-    , ghciPkgOpts = M.toList filteredOpts
-    , ghciPkgDir = parent cabalfp
-    , ghciPkgModules = unionModuleMaps $
-      map (\(comp, mp) -> M.map (\fp -> M.singleton fp (S.singleton (packageName pkg, comp))) mp)
+    { name
+    , opts = M.toList filteredOpts
+    , dir = parent cabalFP
+    , modules = unionModuleMaps $
+        map
+          ( \(comp, mp) -> M.map
+              (\fp -> M.singleton fp (S.singleton (pkg.name, comp)))
+              mp
+          )
           (M.toList (filterWanted mods))
-    , ghciPkgMainIs = M.map (mapMaybe dotCabalMainPath) files
-    , ghciPkgCFiles = mconcat (M.elems (filterWanted (M.map (mapMaybe dotCabalCFilePath) files)))
-    , ghciPkgTargetFiles = mfileTargets >>= M.lookup name
-    , ghciPkgPackage = pkg
+    , mainIs = M.map (mapMaybe dotCabalMainPath) files
+    , cFiles = mconcat
+        (M.elems (filterWanted (M.map (mapMaybe dotCabalCFilePath) files)))
+    , targetFiles = mfileTargets >>= M.lookup name
+    , package = pkg
     }
 
 -- NOTE: this should make the same choices as the components code in
@@ -931,13 +956,20 @@ wantedPackageComponents :: BuildOpts -> Target -> Package -> Set NamedComponent
 wantedPackageComponents _ (TargetComps cs) _ = cs
 wantedPackageComponents bopts (TargetAll PTProject) pkg = S.fromList $
-  (case packageLibraries pkg of
-    NoLibraries -> []
-    HasLibraries names -> CLib : map CInternalLib (S.toList names)) ++
-  map CExe (S.toList (packageExes pkg)) <>
-  map CInternalLib (S.toList $ packageInternalLibraries pkg) <>
-  (if boptsTests bopts then map CTest (M.keys (packageTests pkg)) else []) <>
-  (if boptsBenchmarks bopts then map CBench (S.toList (packageBenchmarks pkg)) else [])
+     ( if hasBuildableMainLibrary pkg
+         then CLib : map CSubLib buildableForeignLibs'
+         else []
+     )
+  <> map CExe buildableExes'
+  <> map CSubLib buildableSubLibs
+  <> (if bopts.tests then map CTest buildableTestSuites else [])
+  <> (if bopts.benchmarks then map CBench buildableBenchmarks else [])
+ where
+  buildableForeignLibs' = S.toList $ buildableForeignLibs pkg
+  buildableSubLibs = getBuildableListText pkg.subLibraries
+  buildableExes' = S.toList $ buildableExes pkg
+  buildableTestSuites = getBuildableListText pkg.testSuites
+  buildableBenchmarks = getBuildableListText pkg.benchmarks
 wantedPackageComponents _ _ _ = S.empty
 
 checkForIssues :: HasTerm env => [GhciPkgInfo] -> RIO env ()
@@ -1031,8 +1063,8 @@       )
     ]
   mixedFlag (flag, msgs) =
-    let x = partitionComps (== flag) in
-    [ fillSep $ msgs ++ showWhich x | mixedSettings x ]
+    let x = partitionComps (== flag)
+    in  [ fillSep $ msgs ++ showWhich x | mixedSettings x ]
   mixedSettings (xs, ys) = xs /= [] && ys /= []
   showWhich (haveIt, don'tHaveIt) =
        [ flow "It is specified for:" ]
@@ -1045,11 +1077,11 @@    where
     (xs, ys) = L.partition (any f . snd) compsWithOpts
   compsWithOpts = map (\(k, bio) ->
-    (k, bioOneWordOpts bio ++ bioOpts bio)) compsWithBios
+    (k, bio.oneWordOpts ++ bio.opts)) compsWithBios
   compsWithBios =
-    [ ((ghciPkgName pkg, c), bio)
+    [ ((pkg.name, c), bio)
     | pkg <- pkgs
-    , (c, bio) <- ghciPkgOpts pkg
+    , (c, bio) <- pkg.opts
     ]
 
 -- TODO: Should this also tell the user the filepaths, not just the
@@ -1071,7 +1103,7 @@   duplicates =
     filter (\(_, mp) -> M.size mp > 1) $
     M.toList $
-    unionModuleMaps (map ghciPkgModules pkgs)
+    unionModuleMaps (map (.modules) pkgs)
   prettyDuplicate ::
        (ModuleName, Map (Path Abs File) (Set (PackageName, NamedComponent)))
     -> StyleDoc
@@ -1101,78 +1133,90 @@   unless (null nonLocalTargets) $
     prettyWarnL
       [ flow "Some targets"
-      , parens $ fillSep $ punctuate "," $ map (style Good . fromString . packageNameString) nonLocalTargets
-      , flow "are not local packages, and so cannot be directly loaded."
-      , flow "In future versions of Stack, this might be supported - see"
+      , parens $ fillSep $ punctuate "," $ map
+          (style Good . fromPackageName)
+          nonLocalTargets
+      , flow "are not local packages, and so cannot be directly loaded. In \
+             \future versions of Stack, this might be supported - see"
       , style Url "https://github.com/commercialhaskell/stack/issues/1441"
       , "."
-      , flow "It can still be useful to specify these, as they will be passed to ghci via -package flags."
+      , flow "It can still be useful to specify these, as they will be passed \
+             \to ghci via -package flags."
       ]
   when (null localTargets && isNothing mfileTargets) $ do
-      smWanted <- view $ buildConfigL.to bcSMWanted
-      stackYaml <- view stackYamlL
-      prettyNote $ vsep
-          [ flow "No local targets specified, so a plain ghci will be started with no package hiding or package options."
-          , ""
-          , flow $ T.unpack $ utf8BuilderToText $
-                   "You are using snapshot: " <>
-                   display (smwSnapshotLocation smWanted)
-          , ""
-          , flow "If you want to use package hiding and options, then you can try one of the following:"
-          , ""
-          , bulletedList
-              [ fillSep
-                  [ flow "If you want to start a different project configuration than" <+> pretty stackYaml <> ", then you can use"
-                  , style Shell "stack init"
-                  , flow "to create a new stack.yaml for the packages in the current directory."
-                  , line
-                  ]
-              , flow "If you want to use the project configuration at" <+> pretty stackYaml <> ", then you can add to its 'packages' field."
+    smWanted <- view $ buildConfigL . to (.smWanted)
+    stackYaml <- view stackYamlL
+    prettyNote $ vsep
+      [ flow "No local targets specified, so a plain ghci will be started with \
+             \no package hiding or package options."
+      , ""
+      , flow $ T.unpack $ utf8BuilderToText $
+               "You are using snapshot: " <>
+               display smWanted.snapshotLocation
+      , ""
+      , flow "If you want to use package hiding and options, then you can try \
+             \one of the following:"
+      , ""
+      , bulletedList
+          [ fillSep
+              [ flow "If you want to start a different project configuration \
+                     \than"
+              , pretty stackYaml <> ","
+              , flow "then you can use"
+              , style Shell "stack init"
+              , flow "to create a new stack.yaml for the packages in the \
+                     \current directory."
+              , line
               ]
-          , ""
+          , flow "If you want to use the project configuration at"
+          , pretty stackYaml <> ","
+          , flow "then you can add to its 'packages' field."
           ]
+      , ""
+      ]
 
--- Adds in intermediate dependencies between ghci targets. Note that it
--- will return a Lib component for these intermediate dependencies even
--- if they don't have a library (but that's fine for the usage within
--- this module).
+-- Adds in intermediate dependencies between ghci targets. Note that it will
+-- return a Lib component for these intermediate dependencies even if they don't
+-- have a library (but that's fine for the usage within this module).
 --
--- If 'True' is passed for loadAllDeps, this loads all local deps, even
--- if they aren't intermediate.
+-- If 'True' is passed for loadAllDeps, this loads all local deps, even if they
+-- aren't intermediate.
 getExtraLoadDeps ::
      Bool
   -> Map PackageName LocalPackage
   -> [(PackageName, (Path Abs File, Target))]
   -> [(PackageName, (Path Abs File, Target))]
 getExtraLoadDeps loadAllDeps localMap targets =
-    M.toList $
-    (\mp -> foldl' (flip M.delete) mp (map fst targets)) $
-    M.mapMaybe id $
-    execState (mapM_ (mapM_ go . getDeps . fst) targets)
-              (M.fromList (map (second Just) targets))
-  where
-    getDeps :: PackageName -> [PackageName]
-    getDeps name =
-        case M.lookup name localMap of
-            Just lp -> M.keys (packageDeps (lpPackage lp)) -- FIXME just Local?
-            _ -> []
-    go :: PackageName -> State (Map PackageName (Maybe (Path Abs File, Target))) Bool
-    go name = do
-        cache <- get
-        case (M.lookup name cache, M.lookup name localMap) of
-            (Just (Just _), _) -> pure True
-            (Just Nothing, _) | not loadAllDeps -> pure False
-            (_, Just lp) -> do
-                let deps = M.keys (packageDeps (lpPackage lp))
-                shouldLoad <- or <$> mapM go deps
-                if shouldLoad
-                    then do
-                        modify (M.insert name (Just (lpCabalFile lp, TargetComps (S.singleton CLib))))
-                        pure True
-                    else do
-                        modify (M.insert name Nothing)
-                        pure False
-            (_, _) -> pure False
+  M.toList $
+  (\mp -> foldl' (flip M.delete) mp (map fst targets)) $
+  M.mapMaybe id $
+  execState (mapM_ (mapM_ go . getDeps . fst) targets)
+            (M.fromList (map (second Just) targets))
+ where
+  getDeps :: PackageName -> [PackageName]
+  getDeps name =
+    case M.lookup name localMap of
+      Just lp -> listOfPackageDeps lp.package -- FIXME just Local?
+      _ -> []
+  go ::
+       PackageName
+    -> State (Map PackageName (Maybe (Path Abs File, Target))) Bool
+  go name = do
+    cache <- get
+    case (M.lookup name cache, M.lookup name localMap) of
+      (Just (Just _), _) -> pure True
+      (Just Nothing, _) | not loadAllDeps -> pure False
+      (_, Just lp) -> do
+        let deps = listOfPackageDeps lp.package
+        shouldLoad <- or <$> mapM go deps
+        if shouldLoad
+          then do
+            modify (M.insert name (Just (lp.cabalFP, TargetComps (S.singleton CLib))))
+            pure True
+          else do
+            modify (M.insert name Nothing)
+            pure False
+      (_, _) -> pure False
 
 unionTargets :: Ord k => Map k Target -> Map k Target -> Map k Target
 unionTargets = M.unionWith $ \l r -> case (l, r) of
@@ -1187,21 +1231,3 @@   TargetComps s -> any p (S.toList s)
   TargetAll PTProject -> True
   _ -> False
-
--- | Run a command and grab the first line of stdout, dropping
--- stderr's contexts completely.
-runGrabFirstLine ::
-     (HasProcessContext env, HasLogFunc env)
-  => String
-  -> [String]
-  -> RIO env String
-runGrabFirstLine cmd0 args =
-  proc cmd0 args $ \pc -> do
-    (out, _err) <- readProcess_ pc
-    pure
-      $ TL.unpack
-      $ TL.filter (/= '\r')
-      $ TL.concat
-      $ take 1
-      $ TL.lines
-      $ TLE.decodeUtf8With lenientDecode out
src/Stack/Ghci/Script.hs view
@@ -1,11 +1,11 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 module Stack.Ghci.Script
   ( GhciScript
   , ModuleName
   , cmdAdd
-  , cmdCdGhc
   , cmdModule
   , scriptToLazyByteString
   , scriptToBuilder
@@ -19,7 +19,7 @@ import           Stack.Prelude
 import           System.IO ( hSetBinaryMode )
 
-newtype GhciScript = GhciScript { unGhciScript :: [GhciCommand] }
+newtype GhciScript = GhciScript { ghciScript :: [GhciCommand] }
 
 instance Semigroup GhciScript where
   GhciScript xs <> GhciScript ys = GhciScript (ys <> xs)
@@ -30,16 +30,12 @@ 
 data GhciCommand
   = AddCmd (Set (Either ModuleName (Path Abs File)))
-  | CdGhcCmd (Path Abs Dir)
   | ModuleCmd (Set ModuleName)
   deriving Show
 
 cmdAdd :: Set (Either ModuleName (Path Abs File)) -> GhciScript
 cmdAdd = GhciScript . (:[]) . AddCmd
 
-cmdCdGhc :: Path Abs Dir -> GhciScript
-cmdCdGhc = GhciScript . (:[]) . CdGhcCmd
-
 cmdModule :: Set ModuleName -> GhciScript
 cmdModule = GhciScript . (:[]) . ModuleCmd
 
@@ -49,7 +45,7 @@ scriptToBuilder :: GhciScript -> Builder
 scriptToBuilder backwardScript = mconcat $ fmap commandToBuilder script
  where
-  script = reverse $ unGhciScript backwardScript
+  script = reverse backwardScript.ghciScript
 
 scriptToFile :: Path Abs File -> GhciScript -> IO ()
 scriptToFile path script =
@@ -78,9 +74,6 @@                  (S.toAscList modules)
          )
     <> "\n"
-
-commandToBuilder (CdGhcCmd path) =
-  ":cd-ghc " <> fromString (quoteFileName (toFilePath path)) <> "\n"
 
 commandToBuilder (ModuleCmd modules)
   | S.null modules = ":module +\n"
src/Stack/Hoogle.hs view
@@ -1,5 +1,7 @@-{-# LANGUAGE NoImplicitPrelude   #-}
-{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- | A wrapper around hoogle.
 module Stack.Hoogle
@@ -19,17 +21,16 @@ import qualified RIO.Map as Map
 import           RIO.Process ( findExecutable, proc, readProcess_, runProcess_)
 import qualified Stack.Build ( build )
-import           Stack.Build.Target ( NeedTargets (NeedTargets) )
+import           Stack.Build.Target ( NeedTargets (..) )
 import           Stack.Constants ( stackProgName' )
 import           Stack.Prelude
 import           Stack.Runners
                    ( ShouldReexec (..), withConfig, withDefaultEnvConfig
                    , withEnvConfig
                    )
-import           Stack.Types.BuildOpts
-                   ( BuildOptsCLI (..), buildOptsMonoidHaddockL
-                   , defaultBuildOptsCLI
-                   )
+import           Stack.Types.BuildOptsCLI
+                   ( BuildOptsCLI (..), defaultBuildOptsCLI )
+import           Stack.Types.BuildOptsMonoid ( buildOptsMonoidHaddockL )
 import           Stack.Types.Config
                    ( Config (..), HasConfig (..) )
 import           Stack.Types.EnvConfig
@@ -153,10 +154,10 @@   requiringHoogle :: Muted -> RIO EnvConfig x -> RIO EnvConfig x
   requiringHoogle muted f = do
     hoogleTarget <- do
-      sourceMap <- view $ sourceMapL . to smDeps
+      sourceMap <- view $ sourceMapL . to (.deps)
       case Map.lookup hooglePackageName sourceMap of
         Just hoogleDep ->
-          case dpLocation hoogleDep of
+          case hoogleDep.location of
             PLImmutable pli ->
               T.pack . packageIdentifierString <$>
                   restrictMinHoogleVersion muted (packageLocationIdent pli)
@@ -176,8 +177,7 @@               restrictMinHoogleVersion muted hoogleIdent
     config <- view configL
     let boptsCLI = defaultBuildOptsCLI
-            { boptsCLITargets =  [hoogleTarget]
-            }
+          { targetsCLI =  [hoogleTarget] }
     runRIO config $ withEnvConfig NeedTargets boptsCLI f
 
   restrictMinHoogleVersion ::
@@ -215,7 +215,7 @@   runHoogle :: Path Abs File -> [String] -> RIO EnvConfig ()
   runHoogle hooglePath hoogleArgs = do
     config <- view configL
-    menv <- liftIO $ configProcessContextSettings config envSettings
+    menv <- liftIO $ config.processContextSettings envSettings
     dbpath <- hoogleDatabasePath
     let databaseArg = ["--database=" ++ toFilePath dbpath]
     withProcessContext menv $ proc
@@ -230,7 +230,7 @@   ensureHoogleInPath :: RIO EnvConfig (Path Abs File)
   ensureHoogleInPath = do
     config <- view configL
-    menv <- liftIO $ configProcessContextSettings config envSettings
+    menv <- liftIO $ config.processContextSettings envSettings
     mHooglePath' <- eitherToMaybe <$> runRIO menv (findExecutable "hoogle")
     let mHooglePath'' =
           eitherToMaybe <$> requiringHoogle NotMuted (findExecutable "hoogle")
@@ -277,11 +277,10 @@             installHoogle
         | otherwise -> prettyThrowIO $ HoogleNotFound err
 
-  envSettings =
-    EnvSettings
-      { esIncludeLocals = True
-      , esIncludeGhcPackagePath = True
-      , esStackExe = True
-      , esLocaleUtf8 = False
-      , esKeepGhcRts = False
-      }
+  envSettings = EnvSettings
+    { includeLocals = True
+    , includeGhcPackagePath = True
+    , stackExe = True
+    , localeUtf8 = False
+    , keepGhcRts = False
+    }
src/Stack/IDE.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 {-# LANGUAGE OverloadedStrings   #-}
 
 -- | Types and functions related to Stack's @ide@ command.
@@ -72,12 +73,12 @@   -> ListPackagesCmd
   -> RIO env ()
 listPackages stream flag = do
-  packages <- view $ buildConfigL.to (smwProject . bcSMWanted)
+  packages <- view $ buildConfigL . to (.smWanted.project)
   let strs = case flag of
         ListPackageNames ->
           map packageNameString (Map.keys packages)
         ListPackageCabalFiles ->
-          map (toFilePath . ppCabalFP) (Map.elems packages)
+          map (toFilePath . (.cabalFP)) (Map.elems packages)
   mapM_ (outputFunc stream) strs
 
 -- | List the targets in the current project.
@@ -87,7 +88,7 @@   -> (NamedComponent -> Bool)
   -> RIO env ()
 listTargets stream isCompType = do
-  packages <- view $ buildConfigL.to (smwProject . bcSMWanted)
+  packages <- view $ buildConfigL . to (.smWanted.project)
   pairs <- concat <$> Map.traverseWithKey toNameAndComponent packages
   outputFunc stream $ T.unpack $ T.intercalate "\n" $
     map renderPkgComponent pairs
src/Stack/Init.hs view
@@ -1,4 +1,6 @@ {-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 {-# LANGUAGE OverloadedStrings     #-}
 {-# LANGUAGE TypeFamilies          #-}
 
@@ -15,7 +17,7 @@ import qualified Data.Foldable as F
 import qualified Data.IntMap as IntMap
 import           Data.List.Extra ( groupSortOn )
-import qualified Data.List.NonEmpty as NonEmpty
+import           Data.List.NonEmpty.Extra ( minimumBy1 )
 import qualified Data.Map.Strict as Map
 import qualified Data.Set as Set
 import qualified Data.Text as T
@@ -35,9 +37,10 @@                    )
 import qualified RIO.FilePath as FP
 import           RIO.List ( (\\), intercalate, isSuffixOf, isPrefixOf )
-import           RIO.List.Partial ( minimumBy )
+import           RIO.NonEmpty ( nonEmpty )
+import qualified RIO.NonEmpty as NE
 import           Stack.BuildPlan
-                   ( BuildPlanCheck (..), checkSnapBuildPlan, deNeededBy
+                   ( BuildPlanCheck (..), DepError (..), checkSnapBuildPlan
                    , removeSrcPkgDefaultFlags, selectBestSnapshot
                    )
 import           Stack.Config ( getSnapshots, makeConcreteResolver )
@@ -104,7 +107,7 @@                  , "as"
                  , style
                      File
-                     (fromString (packageNameString name) <> ".cabal")
+                     (fromPackageName name <> ".cabal")
                  ]
              )
              rels
@@ -137,7 +140,7 @@     <> flow "None of the following snapshots provides a compiler matching \
             \your package(s):"
     <> line
-    <> bulletedList (map (fromString . show) (NonEmpty.toList names))
+    <> bulletedList (map (fromString . show) (NE.toList names))
     <> blankLine
     <> resolveOptions
   pretty (ResolverMismatch resolver errDesc) =
@@ -178,8 +181,8 @@            ]
        , fillSep
            [ "Using"
-           , style Shell "--resolver"
-           , "to specify a matching snapshot/resolver."
+           , style Shell "--snapshot"
+           , "to specify a matching snapshot."
            ]
        ]
 
@@ -203,7 +206,7 @@   pwd <- getCurrentDir
   go <- view globalOptsL
   withGlobalProject $
-    withConfig YesReexec (initProject pwd initOpts (globalResolver go))
+    withConfig YesReexec (initProject pwd initOpts go.resolver)
 
 -- | Generate a @stack.yaml@ file.
 initProject ::
@@ -216,10 +219,10 @@   let dest = currDir </> stackDotYaml
   reldest <- toFilePath <$> makeRelativeToCurrentDir dest
   exists <- doesFileExist dest
-  when (not (forceOverwrite initOpts) && exists) $
+  when (not initOpts.forceOverwrite && exists) $
     prettyThrowIO $ ConfigFileAlreadyExists reldest
-  dirs <- mapM (resolveDir' . T.unpack) (searchDirs initOpts)
-  let find  = findCabalDirs (includeSubDirs initOpts)
+  dirs <- mapM (resolveDir' . T.unpack) initOpts.searchDirs
+  let find  = findCabalDirs initOpts.includeSubDirs
       dirs' = if null dirs then [currDir] else dirs
   prettyInfo $
        fillSep
@@ -278,16 +281,16 @@     PLImmutable . cplComplete <$>
       completePackageLocation
         (RPLIHackage (PackageIdentifierRevision n v CFILatest) Nothing)
-  let p = Project
-        { projectUserMsg = if userMsg == "" then Nothing else Just userMsg
-        , projectPackages = resolvedRelative <$> Map.elems rbundle
-        , projectDependencies = map toRawPL deps
-        , projectFlags = removeSrcPkgDefaultFlags gpds flags
-        , projectResolver = snapshotLoc
-        , projectCompiler = Nothing
-        , projectExtraPackageDBs = []
-        , projectCurator = Nothing
-        , projectDropPackages = mempty
+  let project = Project
+        { userMsg = if userMsg == "" then Nothing else Just userMsg
+        , packages = resolvedRelative <$> Map.elems rbundle
+        , extraDeps = map toRawPL deps
+        , flagsByPkg = removeSrcPkgDefaultFlags gpds flags
+        , resolver = snapshotLoc
+        , compiler = Nothing
+        , extraPackageDBs = []
+        , curator = Nothing
+        , dropPackages = mempty
         }
       makeRel = fmap toFilePath . makeRelativeToCurrentDir
   prettyInfoL
@@ -333,7 +336,7 @@         else "Writing configuration to"
     , style File (fromString reldest) <> "."
     ]
-  writeBinaryFileAtomic dest $ renderStackYaml p
+  writeBinaryFileAtomic dest $ renderStackYaml project
     (Map.elems $ fmap (makeRelDir . parent . fst) ignored)
     (map (makeRelDir . parent) dupPkgs)
   prettyInfoS
@@ -380,7 +383,7 @@   commentedPackages =
     let ignoredComment = commentHelp
           [ "The following packages have been ignored due to incompatibility with the"
-          , "resolver compiler, dependency conflicts with other packages"
+          , "snapshot compiler, dependency conflicts with other packages"
           , "or unsatisfied dependencies."
           ]
         dupComment = commentHelp
@@ -421,9 +424,9 @@     , "A snapshot resolver dictates the compiler version and the set of packages"
     , "to be used for project dependencies. For example:"
     , ""
-    , "resolver: lts-21.13"
-    , "resolver: nightly-2023-09-24"
-    , "resolver: ghc-9.6.2"
+    , "resolver: lts-22.7"
+    , "resolver: nightly-2024-01-20"
+    , "resolver: ghc-9.6.4"
     , ""
     , "The location of a snapshot can be provided as a file or url. Stack assumes"
     , "a snapshot provided as a file might change, whereas a url resource does not."
@@ -511,7 +514,7 @@     snaps <- fmap getRecommendedSnapshots getSnapshots'
     (c, l, r) <- selectBestSnapshot (Map.elems pkgDirs) snaps
     case r of
-      BuildPlanCheckFail {} | not (omitPackages initOpts)
+      BuildPlanCheckFail {} | not initOpts.omitPackages
               -> prettyThrowM $ NoMatchingSnapshot snaps
       _ -> pure (c, l)
 
@@ -557,8 +560,7 @@                 prettyWarn
                   (  flow "Ignoring the following packages:"
                   <> line
-                  <> bulletedList
-                       (map (fromString . packageNameString) ignored)
+                  <> bulletedList (map fromPackageName ignored)
                   )
               else
                 prettyWarnL
@@ -589,7 +591,7 @@   case result of
     BuildPlanCheckOk f -> pure $ Right (f, Map.empty)
     BuildPlanCheckPartial _f e -> do -- FIXME:qrilka unused f
-      if omitPackages initOpts
+      if initOpts.omitPackages
         then do
           warnPartial result
           prettyWarnS "Omitting packages with unsatisfied dependencies"
@@ -597,7 +599,7 @@         else
           prettyThrowM $ ResolverPartial snapshotLoc (show result)
     BuildPlanCheckFail _ e _
-      | omitPackages initOpts -> do
+      | initOpts.omitPackages -> do
           prettyWarn $
                fillSep
                  [ "Resolver compiler mismatch:"
@@ -618,18 +620,18 @@       <> line
       <> indent 4 (string $ show res)
 
-  failedUserPkgs e = Map.keys $ Map.unions (Map.elems (fmap deNeededBy e))
+  failedUserPkgs e = Map.keys $ Map.unions (Map.elems (fmap (.neededBy) e))
 
 getRecommendedSnapshots :: Snapshots -> NonEmpty SnapName
 getRecommendedSnapshots snapshots =
   -- in order - Latest LTS, Latest Nightly, all LTS most recent first
-  case NonEmpty.nonEmpty supportedLtss of
+  case nonEmpty supportedLtss of
     Just (mostRecent :| older) -> mostRecent :| (nightly : older)
     Nothing -> nightly :| []
  where
-  ltss = map (uncurry LTS) (IntMap.toDescList $ snapshotsLts snapshots)
+  ltss = map (uncurry LTS) (IntMap.toDescList snapshots.lts )
   supportedLtss = filter (>= minSupportedLts) ltss
-  nightly = Nightly (snapshotsNightly snapshots)
+  nightly = Nightly snapshots.nightly
 
 -- |Yields the minimum LTS supported by Stack.
 minSupportedLts :: SnapName
@@ -693,31 +695,30 @@     -- Pantry's 'loadCabalFilePath' throws 'MismatchedCabalName' (error
     -- [S-910]) if the Cabal file name does not match the package it
     -- defines.
-    (gpdio, _name, cabalfp) <- loadCabalFilePath (Just stackProgName') dir
+    (gpdio, _name, cabalFP) <- loadCabalFilePath (Just stackProgName') dir
     eres <- liftIO $ try (gpdio YesPrintWarnings)
     case eres :: Either PantryException C.GenericPackageDescription of
-      Right gpd -> pure $ Right (cabalfp, gpd)
+      Right gpd -> pure $ Right (cabalFP, gpd)
       Left (MismatchedCabalName fp name) -> pure $ Left (fp, name)
       Left e -> throwIO e
   let (nameMismatchPkgs, packages) = partitionEithers ePackages
   when (nameMismatchPkgs /= []) $
     prettyThrowIO $ PackageNameInvalid nameMismatchPkgs
-  let dupGroups = filter ((> 1) . length)
-                          . groupSortOn (gpdPackageName . snd)
-      dupAll    = concat $ dupGroups packages
+  let dupGroups = mapMaybe nonEmpty . groupSortOn (gpdPackageName . snd)
+      dupAll    = concatMap NE.toList $ dupGroups packages
       -- Among duplicates prefer to include the ones in upper level dirs
       pathlen     = length . FP.splitPath . toFilePath . fst
-      getmin      = minimumBy (compare `on` pathlen)
+      getmin      = minimumBy1 (compare `on` pathlen)
       dupSelected = map getmin (dupGroups packages)
       dupIgnored  = dupAll \\ dupSelected
       unique      = packages \\ dupIgnored
   when (dupIgnored /= []) $ do
-    dups <- mapM (mapM (prettyPath. fst)) (dupGroups packages)
+    dups <- mapM (mapM (prettyPath . fst)) (dupGroups packages)
     prettyWarn $
          flow "The following packages have duplicate package names:"
       <> line
       <> foldMap
-           ( \dup ->    bulletedList (map fromString dup)
+           ( \dup ->    bulletedList (map fromString (NE.toList dup))
                      <> line
            )
            dups
src/Stack/List.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 {-# LANGUAGE OverloadedStrings   #-}
 
 -- | Types and functions related to Stack's @list@ command.
@@ -34,7 +35,7 @@ -- | Function underlying the @stack list@ command. List packages.
 listCmd :: [String] -> RIO Runner ()
 listCmd names = withConfig NoReexec $ do
-  mresolver <- view $ globalOptsL.to globalResolver
+  mresolver <- view $ globalOptsL . to (.resolver)
   mSnapshot <- forM mresolver $ \resolver -> do
     concrete <- makeConcreteResolver resolver
     loc <- completeSnapshotLocation concrete
@@ -57,7 +58,7 @@   case errs1 ++ errs2 of
     [] -> pure ()
     errs -> prettyThrowM $ CouldNotParsePackageSelectors errs
-  mapM_ (Lazy.putStrLn . fromString . packageIdentifierString) locs
+  mapM_ (Lazy.putStrLn . fromPackageId) locs
  where
   toLoc | Just snapshot <- mSnapshot = toLocSnapshot snapshot
         | otherwise = toLocNoSnapshot
@@ -73,7 +74,7 @@           updated <-
             updateHackageIndex $ Just $
                  "Could not find package "
-              <> fromString (packageNameString name)
+              <> fromPackageName name
               <> ", updating"
           case updated of
             UpdateOccurred ->
@@ -87,14 +88,14 @@         candidates <- getHackageTypoCorrections name
         pure $ Left $ fillSep
           [ flow "Could not find package"
-          , style Current (fromString $ packageNameString name)
+          , style Current (fromPackageName name)
           , flow "on Hackage."
           , if null candidates
               then mempty
               else fillSep $
                   flow "Perhaps you meant one of:"
                 : mkNarrativeList (Just Good) False
-                    (map (fromString . packageNameString) candidates :: [StyleDoc])
+                    (map fromPackageName candidates :: [StyleDoc])
           ]
       Just loc -> pure $ Right (packageLocationIdent loc)
 
@@ -107,7 +108,7 @@       Nothing ->
         pure $ Left $ fillSep
           [ flow "Package does not appear in snapshot:"
-          , style Current (fromString $ packageNameString name) <> "."
+          , style Current (fromPackageName name) <> "."
           ]
       Just sp -> do
         loc <- cplComplete <$> completePackageLocation (rspLocation sp)
src/Stack/Lock.hs view
@@ -1,6 +1,7 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 module Stack.Lock
   ( lockCachedWanted
@@ -14,10 +15,10 @@                    , jsonSubWarningsT, logJSONWarnings, withObjectWarnings
                    )
 import           Data.ByteString.Builder ( byteString )
-import qualified Data.List.NonEmpty as NE
 import qualified Data.Map as Map
 import qualified Data.Text as T
 import qualified Data.Yaml as Yaml
+import qualified RIO.NonEmpty as NE
 import           Path ( parent )
 import           Path.Extended ( addExtension )
 import           Path.IO ( doesFileExist )
@@ -52,14 +53,14 @@ instance Exception LockPrettyException
 
 data LockedLocation a b = LockedLocation
-  { llOriginal :: a
-  , llCompleted :: b
+  { original :: a
+  , completed :: b
   }
   deriving (Eq, Show)
 
 instance (ToJSON a, ToJSON b) => ToJSON (LockedLocation a b) where
   toJSON ll =
-      object [ "original" .= llOriginal ll, "completed" .= llCompleted ll ]
+      object [ "original" .= ll.original, "completed" .= ll.completed ]
 
 instance ( FromJSON (WithJSONWarnings (Unresolved a))
          , FromJSON (WithJSONWarnings (Unresolved b))
@@ -75,7 +76,7 @@ -- serialization should not produce locations with multiple subdirs
 -- so we should be OK using just a head element
 newtype SingleRPLI
-  = SingleRPLI { unSingleRPLI :: RawPackageLocationImmutable}
+  = SingleRPLI { singleRPLI :: RawPackageLocationImmutable}
 
 instance FromJSON (WithJSONWarnings (Unresolved SingleRPLI)) where
   parseJSON v =
@@ -85,23 +86,24 @@       pure $ withWarnings $ SingleRPLI . NE.head <$> unresolvedRPLIs
 
 data Locked = Locked
-  { lckSnapshotLocations :: [LockedLocation RawSnapshotLocation SnapshotLocation]
-  , lckPkgImmutableLocations :: [LockedLocation RawPackageLocationImmutable PackageLocationImmutable]
+  { snapshotLocations :: [LockedLocation RawSnapshotLocation SnapshotLocation]
+  , pkgImmutableLocations :: [LockedLocation RawPackageLocationImmutable PackageLocationImmutable]
   }
   deriving (Eq, Show)
 
 instance ToJSON Locked where
-  toJSON Locked {..} =
+  toJSON lck =
     object
-      [ "snapshots" .= lckSnapshotLocations
-      , "packages" .= lckPkgImmutableLocations
+      [ "snapshots" .= lck.snapshotLocations
+      , "packages" .= lck.pkgImmutableLocations
       ]
 
 instance FromJSON (WithJSONWarnings (Unresolved Locked)) where
   parseJSON = withObjectWarnings "Locked" $ \o -> do
     snapshots <- jsonSubWarningsT $ o ..: "snapshots"
     packages <- jsonSubWarningsT $ o ..: "packages"
-    let unwrap ll = ll { llOriginal = unSingleRPLI (llOriginal ll) }
+    let unwrap :: LockedLocation SingleRPLI b -> LockedLocation RawPackageLocationImmutable b
+        unwrap ll = ll { original = ll.original.singleRPLI }
     pure $ Locked <$> sequenceA snapshots <*> (map unwrap <$> sequenceA packages)
 
 loadYamlThrow ::
@@ -150,9 +152,9 @@       logDebug "Not reading lock file"
       pure $ Locked [] []
   let toMap :: Ord a => [LockedLocation a b] -> Map a b
-      toMap =  Map.fromList . map (llOriginal &&& llCompleted)
-      slocCache = toMap $ lckSnapshotLocations locked
-      pkgLocCache = toMap $ lckPkgImmutableLocations locked
+      toMap =  Map.fromList . map ((.original) &&& (.completed))
+      slocCache = toMap locked.snapshotLocations
+      pkgLocCache = toMap locked.pkgImmutableLocations
   debugRSL <- view rslInLogL
   (snap, slocCompleted, pliCompleted) <-
     loadAndCompleteSnapshotRaw' debugRSL resolver slocCache pkgLocCache
@@ -166,8 +168,8 @@         | raw == toRawSL complete = Nothing
         | otherwise = Just $ LockedLocation raw complete
       newLocked = Locked
-        { lckSnapshotLocations = mapMaybe differentSnapLocs slocCompleted
-        , lckPkgImmutableLocations =
+        { snapshotLocations = mapMaybe differentSnapLocs slocCompleted
+        , pkgImmutableLocations =
           lockLocations $ pliCompleted <> prjCompleted
         }
   when (newLocked /= locked) $
src/Stack/Ls.hs view
@@ -1,23 +1,33 @@ {-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 {-# LANGUAGE OverloadedStrings   #-}
-{-# LANGUAGE RecordWildCards     #-}
 
 -- | Types and functions related to Stack's @ls@ command.
 module Stack.Ls
   ( LsCmdOpts (..)
   , LsCmds (..)
   , SnapshotOpts (..)
+  , LsView (..)
+  , ListDepsOpts (..)
+  , ListDepsFormat (..)
+  , ListDepsFormatOpts (..)
+  , ListDepsTextFilter (..)
   , ListStylesOpts (..)
   , ListToolsOpts (..)
-  , LsView (..)
   , lsCmd
   ) where
 
-import           Data.Aeson ( FromJSON, Value (..), (.:) )
+import           Data.Aeson ( FromJSON, Value (..), (.:), encode )
 import           Data.Array.IArray ( (//), elems )
+import qualified Data.ByteString.Lazy.Char8 as LBC8
 import           Distribution.Package ( mkPackageName )
 import qualified Data.Aeson.Types as A
+import qualified Data.Foldable as F
 import qualified Data.List as L
+import qualified Data.Map as Map
+import qualified Data.Sequence as Seq
+import qualified Data.Set as Set
 import           Data.Text ( isPrefixOf )
 import qualified Data.Text as T
 import qualified Data.Text.IO as T
@@ -29,13 +39,18 @@ import           Path ( parent )
 import           RIO.List ( sort )
 import           Stack.Constants ( osIsWindows )
-import           Stack.Dot ( ListDepsOpts, listDependencies )
+import           Stack.DependencyGraph ( createPrunedDependencyGraph )
 import           Stack.Prelude hiding ( Nightly, Snapshot )
 import           Stack.Runners
                    ( ShouldReexec (..), withConfig, withDefaultEnvConfig )
 import           Stack.Setup.Installed
                    ( Tool (..), filterTools, listInstalled, toolString )
 import           Stack.Types.Config ( Config (..), HasConfig (..) )
+import           Stack.Types.DependencyTree
+                   ( DependencyTree (..), DotPayload (..), licenseText
+                   , versionText
+                   )
+import           Stack.Types.DotOpts ( DotOpts (..) )
 import           Stack.Types.EnvConfig ( installationRootDeps )
 import           Stack.Types.Runner ( HasRunner, Runner, terminalL )
 import           System.Console.ANSI.Codes
@@ -56,6 +71,26 @@     ++ "Failure to parse values as a snapshot: "
     ++ show val
 
+-- | Type representing command line options for the @stack ls@ command.
+newtype LsCmdOpts
+  = LsCmdOpts { lsCmds :: LsCmds }
+
+-- | Type representing subcommands for the @stack ls@ command.
+data LsCmds
+  = LsSnapshot SnapshotOpts
+  | LsDependencies ListDepsOpts
+  | LsStyles ListStylesOpts
+  | LsTools ListToolsOpts
+
+-- | Type representing command line options for the @stack ls snapshots@
+-- command.
+data SnapshotOpts = SnapshotOpts
+  { viewType :: LsView
+  , ltsSnapView :: Bool
+  , nightlySnapView :: Bool
+  }
+  deriving (Eq, Ord, Show)
+
 -- | Type representing subcommands for the @stack ls snapshots@ command.
 data LsView
   = Local
@@ -70,67 +105,74 @@     -- ^ Stackage Nightly
   deriving (Eq, Ord, Show)
 
--- | Type representing command line options for the @stack ls snapshots@
--- command.
-data SnapshotOpts = SnapshotOpts
-  { soptViewType :: LsView
-  , soptLtsSnapView :: Bool
-  , soptNightlySnapView :: Bool
+data ListDepsOpts = ListDepsOpts
+  { format :: !ListDepsFormat
+    -- ^ Format of printing dependencies
+  , dotOpts :: !DotOpts
+    -- ^ The normal dot options.
   }
-  deriving (Eq, Ord, Show)
 
+data ListDepsFormat
+  = ListDepsText ListDepsFormatOpts [ListDepsTextFilter]
+  | ListDepsTree ListDepsFormatOpts
+  | ListDepsJSON
+  | ListDepsConstraints
+
+data ListDepsFormatOpts = ListDepsFormatOpts
+  { sep :: !Text
+    -- ^ Separator between the package name and details.
+  , license :: !Bool
+    -- ^ Print dependency licenses instead of versions.
+  }
+
+-- | Type representing items to filter the results of @stack ls dependencies@.
+data ListDepsTextFilter
+  = FilterPackage PackageName
+    -- ^ Item is a package name.
+  | FilterLocals
+    -- ^ Item represents all local packages.
+
 -- | Type representing command line options for the @stack ls stack-colors@ and
 -- @stack ls stack-colours@ commands.
 data ListStylesOpts = ListStylesOpts
-  { coptBasic   :: Bool
-  , coptSGR     :: Bool
-  , coptExample :: Bool
+  { basic   :: Bool
+  , sgr     :: Bool
+  , example :: Bool
   }
   deriving (Eq, Ord, Show)
 
 -- | Type representing command line options for the @stack ls tools@ command.
 newtype ListToolsOpts
-  = ListToolsOpts { toptFilter  :: String }
-
--- | Type representing subcommands for the @stack ls@ command.
-data LsCmds
-  = LsSnapshot SnapshotOpts
-  | LsDependencies ListDepsOpts
-  | LsStyles ListStylesOpts
-  | LsTools ListToolsOpts
-
--- | Type representing command line options for the @stack ls@ command.
-newtype LsCmdOpts
-  = LsCmdOpts { lsView :: LsCmds }
+  = ListToolsOpts { filter  :: String }
 
 data Snapshot = Snapshot
   { snapId :: Text
-  , snapTitle :: Text
-  , snapTime :: Text
+  , title :: Text
+  , time :: Text
   }
   deriving (Eq, Ord, Show)
 
+instance FromJSON Snapshot where
+  parseJSON o@(Array _) = parseSnapshot o
+  parseJSON _ = mempty
+
 data SnapshotData = SnapshotData
   { _snapTotalCounts :: Integer
   , snaps :: [[Snapshot]]
   }
   deriving (Eq, Ord, Show)
 
-instance FromJSON Snapshot where
-  parseJSON o@(Array _) = parseSnapshot o
-  parseJSON _ = mempty
-
 instance FromJSON SnapshotData where
   parseJSON (Object s) =
     SnapshotData <$> s .: "totalCount" <*> s .: "snapshots"
   parseJSON _ = mempty
 
 toSnapshot :: [Value] -> Snapshot
-toSnapshot [String sid, String stitle, String stime] =
+toSnapshot [String snapId, String title, String time] =
   Snapshot
-    { snapId = sid
-    , snapTitle = stitle
-    , snapTime = stime
+    { snapId
+    , title
+    , time
     }
 toSnapshot val = impureThrow $ ParseFailure val
 
@@ -138,11 +180,11 @@ parseSnapshot = A.withArray "array of snapshot" (pure . toSnapshot . V.toList)
 
 displayTime :: Snapshot -> [Text]
-displayTime Snapshot {..} = [snapTime]
+displayTime snap = [snap.time]
 
 displaySnap :: Snapshot -> [Text]
-displaySnap Snapshot {..} =
-  ["Resolver name: " <> snapId, "\n" <> snapTitle <> "\n\n"]
+displaySnap snap =
+  ["Resolver name: " <> snap.snapId, "\n" <> snap.title <> "\n\n"]
 
 displaySingleSnap :: [Snapshot] -> Text
 displaySingleSnap snapshots =
@@ -160,7 +202,7 @@ 
 displaySnapshotData :: Bool -> SnapshotData -> IO ()
 displaySnapshotData term sdata =
-  case L.reverse $ snaps sdata of
+  case L.reverse sdata.snaps of
     [] -> pure ()
     xs ->
       let snaps = T.concat $ L.map displaySingleSnap xs
@@ -170,12 +212,12 @@ filterSnapshotData sdata stype =
   sdata { snaps = filterSnapData }
  where
-  snapdata = snaps sdata
+  snapdata = sdata.snaps
   filterSnapData =
     case stype of
-      Lts -> L.map (L.filter (\x -> "lts" `isPrefixOf` snapId x)) snapdata
+      Lts -> L.map (L.filter (\x -> "lts" `isPrefixOf` x.snapId)) snapdata
       Nightly ->
-        L.map (L.filter (\x -> "nightly" `isPrefixOf` snapId x)) snapdata
+        L.map (L.filter (\x -> "nightly" `isPrefixOf` x.snapId)) snapdata
 
 displayLocalSnapshot :: Bool -> [String] -> IO ()
 displayLocalSnapshot term xs = renderData term (localSnaptoText xs)
@@ -194,9 +236,9 @@         | otherwise   = parent parentInstRoot
   snapData' <- liftIO $ listDirectory $ toFilePath snapRootDir
   let snapData = L.sort snapData'
-  case lsView lsOpts of
-    LsSnapshot SnapshotOpts {..} ->
-      case (soptLtsSnapView, soptNightlySnapView) of
+  case lsOpts.lsCmds of
+    LsSnapshot sopt ->
+      case (sopt.ltsSnapView, sopt.nightlySnapView) of
         (True, False) ->
           liftIO $
           displayLocalSnapshot isStdoutTerminal $
@@ -217,9 +259,9 @@   let req' = addRequestHeader hAccept "application/json" req
   result <- httpJSON req'
   let snapData = getResponseBody result
-  case lsView lsOpts of
-    LsSnapshot SnapshotOpts {..} ->
-      case (soptLtsSnapView, soptNightlySnapView) of
+  case lsOpts.lsCmds of
+    LsSnapshot sopt ->
+      case (sopt.ltsSnapView, sopt.nightlySnapView) of
         (True, False) ->
           liftIO $
           displaySnapshotData isStdoutTerminal $
@@ -237,9 +279,9 @@ 
 lsCmd :: LsCmdOpts -> RIO Runner ()
 lsCmd lsOpts =
-  case lsView lsOpts of
-    LsSnapshot SnapshotOpts {..} ->
-      case soptViewType of
+  case lsOpts.lsCmds of
+    LsSnapshot sopt ->
+      case sopt.viewType of
         Local -> handleLocal lsOpts
         Remote -> handleRemote lsOpts
     LsDependencies depOpts -> listDependencies depOpts
@@ -253,9 +295,9 @@   -- This is the same test as is used in Stack.Types.Runner.withRunner
   let useColor = view useColorL lc
       styles = elems $ defaultStyles // stylesUpdate (view stylesUpdateL lc)
-      isComplex = not (coptBasic opts)
-      showSGR = isComplex && coptSGR opts
-      showExample = isComplex && coptExample opts && useColor
+      isComplex = not opts.basic
+      showSGR = isComplex && opts.sgr
+      showExample = isComplex && opts.example && useColor
       styleReports = L.map (styleReport showSGR showExample) styles
   liftIO $
     T.putStrLn $ T.intercalate (if isComplex then "\n" else ":") styleReports
@@ -276,9 +318,9 @@ -- | List Stack's installed tools, sorted (see instance of 'Ord' for 'Tool').
 listToolsCmd :: ListToolsOpts -> RIO Config ()
 listToolsCmd opts = do
-  localPrograms <- view $ configL.to configLocalPrograms
+  localPrograms <- view $ configL . to (.localPrograms)
   installed <- sort <$> listInstalled localPrograms
-  let wanted = case toptFilter opts of
+  let wanted = case opts.filter of
         [] -> installed
         "ghc-git" -> [t | t@(ToolGhcGit _ _) <- installed]
         pkgName -> filtered pkgName installed
@@ -286,3 +328,111 @@  where
   filtered pkgName installed = Tool <$>
       filterTools (mkPackageName pkgName) (const True) installed
+
+listDependencies :: ListDepsOpts -> RIO Runner ()
+listDependencies opts = do
+  let dotOpts = opts.dotOpts
+  (pkgs, resultGraph) <- createPrunedDependencyGraph dotOpts
+  liftIO $ case opts.format of
+    ListDepsTree treeOpts ->
+      T.putStrLn "Packages"
+      >> printTree treeOpts dotOpts 0 [] (treeRoots opts pkgs) resultGraph
+    ListDepsJSON -> printJSON pkgs resultGraph
+    ListDepsText textOpts listDepsTextFilters -> do
+      let resultGraph' = Map.filterWithKey p resultGraph
+          p k _ =
+            Set.notMember k (exclude (Set.toList pkgs) listDepsTextFilters)
+      void $ Map.traverseWithKey (go "" textOpts) (snd <$> resultGraph')
+     where
+      exclude :: [PackageName] -> [ListDepsTextFilter] -> Set PackageName
+      exclude locals = Set.fromList . exclude' locals
+
+      exclude' :: [PackageName] -> [ListDepsTextFilter] -> [PackageName]
+      exclude' _ [] = []
+      exclude' locals (f:fs) = case f of
+        FilterPackage pkgName -> pkgName : exclude' locals fs
+        FilterLocals -> locals <> exclude' locals fs
+    ListDepsConstraints -> do
+      let constraintOpts = ListDepsFormatOpts " ==" False
+      T.putStrLn "constraints:"
+      void $ Map.traverseWithKey (go "  , " constraintOpts)
+                                 (snd <$> resultGraph)
+ where
+  go prefix lineOpts name payload =
+    T.putStrLn $ prefix <> listDepsLine lineOpts name payload
+
+treeRoots :: ListDepsOpts -> Set PackageName -> Set PackageName
+treeRoots opts projectPackages' =
+  let targets = opts.dotOpts.dotTargets
+  in  if null targets
+        then projectPackages'
+        else Set.fromList $ map (mkPackageName . T.unpack) targets
+
+printTree ::
+     ListDepsFormatOpts
+  -> DotOpts
+  -> Int
+  -> [Int]
+  -> Set PackageName
+  -> Map PackageName (Set PackageName, DotPayload)
+  -> IO ()
+printTree opts dotOpts depth remainingDepsCounts packages dependencyMap =
+  F.sequence_ $ Seq.mapWithIndex go (toSeq packages)
+ where
+  toSeq = Seq.fromList . Set.toList
+  go index name =
+    let newDepsCounts = remainingDepsCounts ++ [Set.size packages - index - 1]
+    in  case Map.lookup name dependencyMap of
+          Just (deps, payload) -> do
+            printTreeNode opts dotOpts depth newDepsCounts deps payload name
+            if Just depth == dotOpts.dependencyDepth
+              then pure ()
+              else printTree opts dotOpts (depth + 1) newDepsCounts deps
+                     dependencyMap
+          -- TODO: Define this behaviour, maybe pure an error?
+          Nothing -> pure ()
+
+printTreeNode ::
+     ListDepsFormatOpts
+  -> DotOpts
+  -> Int
+  -> [Int]
+  -> Set PackageName
+  -> DotPayload
+  -> PackageName
+  -> IO ()
+printTreeNode opts dotOpts depth remainingDepsCounts deps payload name =
+  let remainingDepth = fromMaybe 999 dotOpts.dependencyDepth - depth
+      hasDeps = not $ null deps
+  in  T.putStrLn $
+        treeNodePrefix "" remainingDepsCounts hasDeps remainingDepth <> " " <>
+        listDepsLine opts name payload
+
+treeNodePrefix :: Text -> [Int] -> Bool -> Int -> Text
+treeNodePrefix t [] _ _      = t
+treeNodePrefix t [0] True  0 = t <> "└──"
+treeNodePrefix t [_] True  0 = t <> "├──"
+treeNodePrefix t [0] True  _ = t <> "└─┬"
+treeNodePrefix t [_] True  _ = t <> "├─┬"
+treeNodePrefix t [0] False _ = t <> "└──"
+treeNodePrefix t [_] False _ = t <> "├──"
+treeNodePrefix t (0:ns) d remainingDepth = treeNodePrefix (t <> "  ") ns d remainingDepth
+treeNodePrefix t (_:ns) d remainingDepth = treeNodePrefix (t <> "│ ") ns d remainingDepth
+
+listDepsLine :: ListDepsFormatOpts -> PackageName -> DotPayload -> Text
+listDepsLine opts name payload =
+  T.pack (packageNameString name) <> opts.sep <>
+  payloadText opts payload
+
+payloadText :: ListDepsFormatOpts -> DotPayload -> Text
+payloadText opts payload =
+  if opts.license
+    then licenseText payload
+    else versionText payload
+
+printJSON ::
+     Set PackageName
+  -> Map PackageName (Set PackageName, DotPayload)
+  -> IO ()
+printJSON pkgs dependencyMap =
+  LBC8.putStrLn $ encode $ DependencyTree pkgs dependencyMap
src/Stack/New.hs view
@@ -1,13 +1,15 @@-{-# LANGUAGE NoImplicitPrelude         #-}
-{-# LANGUAGE OverloadedStrings         #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 -- | Types and functions related to Stack's @new@ command.
 module Stack.New
-    ( NewOpts (..)
-    , TemplateName
-    , newCmd
-    , new
-    ) where
+  ( NewOpts (..)
+  , TemplateName
+  , newCmd
+  , new
+  ) where
 
 import           Control.Monad.Trans.Writer.Strict ( execWriterT )
 import           Data.Aeson as A
@@ -182,11 +184,10 @@     <> fillSep
          ( flow "The names blocked by Stack are:"
          : mkNarrativeList Nothing False
-             (map toStyleDoc (L.sort $ S.toList wiredInPackages))
+             (map fromPackageName sortedWiredInPackages :: [StyleDoc])
          )
    where
-    toStyleDoc :: PackageName -> StyleDoc
-    toStyleDoc = fromString . packageNameString
+    sortedWiredInPackages = L.sort $ S.toList wiredInPackages
   pretty (AttemptedOverwrites name fps) =
     "[S-3113]"
     <> line
@@ -214,14 +215,16 @@ -- | Type representing command line options for the @stack new@ command (other
 -- than those applicable also to the @stack init@ command).
 data NewOpts = NewOpts
-  { newOptsProjectName  :: PackageName
-  -- ^ Name of the project to create.
-  , newOptsCreateBare   :: Bool
-  -- ^ Whether to create the project without a directory.
-  , newOptsTemplate     :: Maybe TemplateName
-  -- ^ Name of the template to use.
-  , newOptsNonceParams  :: Map Text Text
-  -- ^ Nonce parameters specified just for this invocation.
+  { projectName  :: PackageName
+    -- ^ Name of the project to create.
+  , createBare   :: Bool
+    -- ^ Whether to create the project without a directory.
+  , init         :: Bool
+    -- ^ Whether to initialise the project for use with Stack.
+  , template     :: Maybe TemplateName
+    -- ^ Name of the template to use.
+  , nonceParams  :: Map Text Text
+    -- ^ Nonce parameters specified just for this invocation.
   }
 
 -- | Function underlying the @stack new@ command. Create a project directory
@@ -229,11 +232,11 @@ newCmd :: (NewOpts, InitOpts) -> RIO Runner ()
 newCmd (newOpts, initOpts) =
   withGlobalProject $ withConfig YesReexec $ do
-    dir <- new newOpts (forceOverwrite initOpts)
+    dir <- new newOpts initOpts.forceOverwrite
     exists <- doesFileExist $ dir </> stackDotYaml
-    when (forceOverwrite initOpts || not exists) $ do
+    when (newOpts.init && (initOpts.forceOverwrite || not exists)) $ do
       go <- view globalOptsL
-      initProject dir initOpts (globalResolver go)
+      initProject dir initOpts go.resolver
 
 -- | Create a new project with the given options.
 new :: HasConfig env => NewOpts -> Bool -> RIO env (Path Abs Dir)
@@ -246,7 +249,7 @@               else do relDir <- parseRelDir (packageNameString project)
                       pure (pwd </> relDir)
   exists <- doesDirExist absDir
-  configTemplate <- view $ configL.to configDefaultTemplate
+  configTemplate <- view $ configL . to (.defaultTemplate)
   let template = fromMaybe defaultTemplateName $ asum [ cliOptionTemplate
                                                       , configTemplate
                                                       ]
@@ -258,7 +261,7 @@         applyTemplate
           project
           template
-          (newOptsNonceParams opts)
+          opts.nonceParams
           absDir
           templateText
       when (not forceOverwrite && bare) $
@@ -267,10 +270,10 @@       runTemplateInits absDir
       pure absDir
  where
-  cliOptionTemplate = newOptsTemplate opts
-  project = newOptsProjectName opts
+  cliOptionTemplate = opts.template
+  project = opts.projectName
   projectName = packageNameString project
-  bare = newOptsCreateBare opts
+  bare = opts.createBare
   logUsing absDir template templateFrom =
     let loading = case templateFrom of
                     LocalTemp -> flow "Loading local"
@@ -305,7 +308,7 @@   -> (TemplateFrom -> RIO env ())
   -> RIO env Text
 loadTemplate name logIt = do
-  templateDir <- view $ configL.to templatesDir
+  templateDir <- view $ configL . to templatesDir
   case templatePath name of
     AbsPath absFile ->
       logIt LocalTemp >> loadLocalFile absFile eitherByteStringToText
@@ -319,9 +322,9 @@             pure f)
         ( \(e :: PrettyException) -> do
             settings <- fromMaybe (throwM e) (relSettings rawParam)
-            let url = tplDownloadUrl settings
-                mBasicAuth = tplBasicAuth settings
-                extract = tplExtract settings
+            let url = settings.downloadUrl
+                mBasicAuth = settings.basicAuth
+                extract = settings.extract
             downloadTemplate url mBasicAuth extract (templateDir </> relFile)
         )
     RepoPath rtp -> do
@@ -354,10 +357,10 @@ 
   downloadFromUrl :: TemplateDownloadSettings -> Path Abs Dir -> RIO env Text
   downloadFromUrl settings templateDir = do
-    let url = tplDownloadUrl settings
-        mBasicAuth = tplBasicAuth settings
+    let url =  settings.downloadUrl
+        mBasicAuth = settings.basicAuth
         rel = fromMaybe backupUrlRelPath (parseRelFile url)
-    downloadTemplate url mBasicAuth (tplExtract settings) (templateDir </> rel)
+    downloadTemplate url mBasicAuth settings.extract (templateDir </> rel)
 
   downloadTemplate ::
        String
@@ -401,10 +404,10 @@ 
 -- | Type representing settings for the download of Stack project templates.
 data TemplateDownloadSettings = TemplateDownloadSettings
-  { tplDownloadUrl :: String
-  , tplBasicAuth :: Maybe (ByteString, ByteString)
+  { downloadUrl :: String
+  , basicAuth :: Maybe (ByteString, ByteString)
     -- ^ Optional HTTP 'Basic' authentication (type, credentials)
-  , tplExtract :: ByteString -> Either String Text
+  , extract :: ByteString -> Either String Text
   }
 
 eitherByteStringToText :: ByteString -> Either String Text
@@ -412,9 +415,9 @@ 
 asIsFromUrl :: String -> TemplateDownloadSettings
 asIsFromUrl url = TemplateDownloadSettings
-  { tplDownloadUrl = url
-  , tplBasicAuth = Nothing
-  , tplExtract = eitherByteStringToText
+  { downloadUrl = url
+  , basicAuth = Nothing
+  , extract = eitherByteStringToText
   }
 
 -- | Construct settings for downloading a Stack project template from a
@@ -443,15 +446,15 @@           basicAuthMsg altGitHubTokenEnvVar
           pure $ Just (gitHubBasicAuthType, fromString wantAltGitHubToken)
         else pure Nothing
-  pure $ TemplateDownloadSettings
-    { tplDownloadUrl = concat
+  pure TemplateDownloadSettings
+    { downloadUrl = concat
         [ "https://api.github.com/repos/"
         , T.unpack user
         , "/stack-templates/contents/"
         , T.unpack name
         ]
-    , tplBasicAuth = mBasicAuth
-    , tplExtract = \bs -> do
+    , basicAuth = mBasicAuth
+    , extract = \bs -> do
         decodedJson <- eitherDecode (LB.fromStrict bs)
         case decodedJson of
           Object o | Just (String content) <- KeyMap.lookup "content" o -> do
@@ -502,7 +505,7 @@         nameParams = M.fromList [ ("name", T.pack $ packageNameString project)
                                 , ("name-as-varid", nameAsVarId)
                                 , ("name-as-module", nameAsModule) ]
-        configParams = configTemplateParams config
+        configParams = config.templateParams
         yearParam = M.singleton "year" currentYear
   files :: Map FilePath LB.ByteString <-
     catch
@@ -518,7 +521,7 @@       template
       (flow "the template does not contain any files.")
 
-  let isPkgSpec f = ".cabal" `L.isSuffixOf` f || f == "package.yaml"
+  let isPkgSpec f = ".cabal" `L.isSuffixOf` f || "package.yaml" `L.isSuffixOf` f
   unless (any isPkgSpec . M.keys $ files) $
     prettyThrowM $ TemplateInvalid
       template
@@ -567,7 +570,7 @@     prettyNote $
       missingParameters
         missingKeys
-        (configUserConfigPath config)
+        config.userConfigPath
   pure $ M.fromList results
  where
   onlyMissingKeys (Mustache.VariableNotFound ks) = map T.unpack ks
@@ -612,7 +615,7 @@     <> style Shell
          ( fillSep
              [ flow "stack new"
-             , fromString (packageNameString project)
+             , fromPackageName project
              , fromString $ T.unpack (templateName template)
              , hsep $
                  map
@@ -657,7 +660,7 @@ runTemplateInits :: HasConfig env => Path Abs Dir -> RIO env ()
 runTemplateInits dir = do
   config <- view configL
-  case configScmInit config of
+  case config.scmInit of
     Nothing -> pure ()
     Just Git -> withWorkingDir (toFilePath dir) $
       catchAny
src/Stack/Nix.hs view
@@ -1,6 +1,7 @@-{-# LANGUAGE NoImplicitPrelude  #-}
-{-# LANGUAGE DataKinds          #-}
-{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 -- | Run commands in a nix-shell
 module Stack.Nix
@@ -57,7 +58,7 @@ 
     mshellFile <- case configProjectRoot config of
       Just projectRoot ->
-        traverse (resolveFile projectRoot) $ nixInitFile (configNix config)
+        traverse (resolveFile projectRoot) config.nix.initFile
       Nothing -> pure Nothing
 
     -- This will never result in double loading the build config, since:
@@ -70,11 +71,11 @@ 
     ghc <- either throwIO pure $ nixCompiler compilerVersion
     ghcVersion <- either throwIO pure $ nixCompilerVersion compilerVersion
-    let pkgsInConfig = nixPackages (configNix config)
+    let pkgsInConfig = config.nix.packages
         pkgs = pkgsInConfig ++ [ghc, "git", "gcc", "gmp"]
         pkgsStr = "[" <> T.intercalate " " pkgs <> "]"
-        pureShell = nixPureShell (configNix config)
-        addGCRoots = nixAddGCRoots (configNix config)
+        pureShell = config.nix.pureShell
+        addGCRoots = config.nix.addGCRoots
         nixopts = case mshellFile of
           Just fp ->
             [ toFilePath fp
@@ -121,12 +122,12 @@               then [ "--indirect"
                    , "--add-root"
                    , toFilePath
-                             (configWorkDir config)
+                             config.workDir
                        F.</> "nix-gc-symlinks"
                        F.</> "gc-root"
                    ]
               else []
-          , map T.unpack (nixShellOptions (configNix config))
+          , map T.unpack config.nix.shellOptions
           , nixopts
           , ["--run", unwords (cmnd:"$STACK_IN_NIX_EXTRA_ARGS":args')]
             -- Using --run instead of --command so we cannot end up in the
src/Stack/Options/BenchParser.hs view
@@ -10,7 +10,7 @@ import           Options.Applicative.Builder.Extra ( optionalFirst )
 import           Stack.Prelude
 import           Stack.Options.Utils ( hideMods )
-import           Stack.Types.BuildOpts ( BenchmarkOptsMonoid (..) )
+import           Stack.Types.BuildOptsMonoid ( BenchmarkOptsMonoid (..) )
 
 -- | Parser for bench arguments.
 -- FIXME hiding options
src/Stack/Options/BuildMonoidParser.hs view
@@ -23,7 +23,7 @@ import           Stack.Options.TestParser ( testOptsParser )
 import           Stack.Options.HaddockParser ( haddockOptsParser )
 import           Stack.Options.Utils ( GlobalOptsContext (..), hideMods )
-import           Stack.Types.BuildOpts
+import           Stack.Types.BuildOptsMonoid
                    ( BuildOptsMonoid (..), CabalVerbosity, readProgressBarFormat
                    , toFirstCabalVerbosity
                    )
@@ -43,6 +43,7 @@   <*> haddockDeps
   <*> haddockInternal
   <*> haddockHyperlinkSource
+  <*> haddockForHackage
   <*> copyBins
   <*> copyCompilerTool
   <*> preFetch
@@ -141,6 +142,11 @@     "haddock-hyperlink-source"
     "building hyperlinked source for Haddock documentation (like \
     \'haddock --hyperlinked-source')."
+    hide
+  haddockForHackage = firstBoolFlagsFalse
+    "haddock-for-hackage"
+    "building with flags to generate Haddock documentation suitable for upload \
+    \to Hackage."
     hide
   copyBins = firstBoolFlagsFalse
     "copy-bins"
src/Stack/Options/BuildParser.hs view
@@ -20,7 +20,7 @@                    ( flagCompleter, ghcOptsCompleter, targetCompleter )
 import           Stack.Options.PackageParser ( readFlag )
 import           Stack.Prelude
-import           Stack.Types.BuildOpts
+import           Stack.Types.BuildOptsCLI
                    ( ApplyCLIFlag, BuildCommand, BuildOptsCLI (..)
                    , BuildSubset (..), FileWatchOpts (..)
                    )
@@ -38,19 +38,24 @@             []
             ["-Wall", "-Werror"]
             (  long "pedantic"
-            <> help "Turn on -Wall and -Werror."
+            <> help "Pass the -Wall and -Werror flags to GHC, turning on all \
+                    \warnings that indicate potentially suspicious code and \
+                    \making all warnings into fatal errors. Can be overridden \
+                    \using Stack's --ghc-options option."
             )
       <*> flag
             []
             ["-O0"]
             (  long "fast"
-            <> help "Turn off optimizations (-O0)."
+            <> help "Pass a -O0 flag to GHC, turning off any GHC \
+                    \optimsations that have been set. Can be overridden using \
+                    \Stack's --ghc-options option."
             )
       <*> many (textOption
             (  long "ghc-options"
             <> metavar "OPTIONS"
             <> completer ghcOptsCompleter
-            <> help "Additional options passed to GHC (can be specified \
+            <> help "Additional options to be passed to GHC (can be specified \
                     \multiple times)."
             ))
       )
@@ -158,7 +163,7 @@     , "alex"
     , "c2hs"
     , "cpphs"
---  , "doctest -- Not present in Cabal-1.22.5.0.
+    , "doctest"
     , "greencard"
     , "happy"
     , "hsc2hs"
src/Stack/Options/Completion.hs view
@@ -1,5 +1,6 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 module Stack.Options.Completion
   ( ghcOptsCompleter
@@ -57,12 +58,12 @@     ('-': _) -> pure []
     _ -> do
       go' <- globalOptsFromMonoid False mempty
-      let go = go' { globalLogLevel = LevelOther "silent" }
+      let go = go' { logLevel = LevelOther "silent" }
       withRunnerGlobal go $ withConfig NoReexec $ withDefaultEnvConfig $ inner input
 
 targetCompleter :: Completer
 targetCompleter = buildConfigCompleter $ \input -> do
-  packages <- view $ buildConfigL.to (smwProject . bcSMWanted)
+  packages <- view $ buildConfigL . to (.smWanted.project)
   comps <- for packages ppComponents
   pure $
     concatMap
@@ -75,7 +76,7 @@ flagCompleter :: Completer
 flagCompleter = buildConfigCompleter $ \input -> do
   bconfig <- view buildConfigL
-  gpds <- for (smwProject $ bcSMWanted bconfig) ppGPD
+  gpds <- for bconfig.smWanted.project ppGPD
   let wildcardFlags
         = nubOrd
         $ concatMap (\(name, gpd) ->
@@ -90,8 +91,8 @@         let flname = C.unFlagName $ C.flagName fl
         in  (if flagEnabled name fl then "-" else "") ++ flname
       prjFlags =
-        case configProject (bcConfig bconfig) of
-          PCProject (p, _) -> projectFlags p
+        case bconfig.config.project of
+          PCProject (p, _) -> p.flagsByPkg
           PCGlobalProject -> mempty
           PCNoProject _ -> mempty
       flagEnabled name fl =
@@ -106,7 +107,7 @@ 
 projectExeCompleter :: Completer
 projectExeCompleter = buildConfigCompleter $ \input -> do
-  packages <- view $ buildConfigL.to (smwProject . bcSMWanted)
+  packages <- view $ buildConfigL . to (.smWanted.project)
   gpds <- Map.traverseWithKey (const ppGPD) packages
   pure
     $ filter (input `isPrefixOf`)
src/Stack/Options/ConfigParser.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
 
 module Stack.Options.ConfigParser
   ( configOptsParser
@@ -23,7 +24,7 @@ import           Stack.Options.GhcVariantParser ( ghcVariantParser )
 import           Stack.Options.NixParser ( nixOptsParser )
 import           Stack.Options.Utils ( GlobalOptsContext (..), hideMods )
-import           Stack.Prelude
+import           Stack.Prelude hiding ( snapshotLocation )
 import           Stack.Types.ColorWhen ( readColorWhen )
 import           Stack.Types.ConfigMonoid ( ConfigMonoid (..) )
 import           Stack.Types.DumpLogs ( DumpLogs (..) )
@@ -33,35 +34,36 @@ configOptsParser :: FilePath -> GlobalOptsContext -> Parser ConfigMonoid
 configOptsParser currentDir hide0 =
   ( \stackRoot workDir buildOpts dockerOpts nixOpts systemGHC installGHC arch
-     ghcVariant ghcBuild jobs includes libs preprocs overrideGccPath overrideHpack
-     skipGHCCheck skipMsys localBin setupInfoLocations modifyCodePage
-     allowDifferentUser dumpLogs colorWhen snapLoc noRunCompile -> mempty
-       { configMonoidStackRoot = stackRoot
-       , configMonoidWorkDir = workDir
-       , configMonoidBuildOpts = buildOpts
-       , configMonoidDockerOpts = dockerOpts
-       , configMonoidNixOpts = nixOpts
-       , configMonoidSystemGHC = systemGHC
-       , configMonoidInstallGHC = installGHC
-       , configMonoidSkipGHCCheck = skipGHCCheck
-       , configMonoidArch = arch
-       , configMonoidGHCVariant = ghcVariant
-       , configMonoidGHCBuild = ghcBuild
-       , configMonoidJobs = jobs
-       , configMonoidExtraIncludeDirs = includes
-       , configMonoidExtraLibDirs = libs
-       , configMonoidCustomPreprocessorExts = preprocs
-       , configMonoidOverrideGccPath = overrideGccPath
-       , configMonoidOverrideHpack = overrideHpack
-       , configMonoidSkipMsys = skipMsys
-       , configMonoidLocalBinPath = localBin
-       , configMonoidSetupInfoLocations = setupInfoLocations
-       , configMonoidModifyCodePage = modifyCodePage
-       , configMonoidAllowDifferentUser = allowDifferentUser
-       , configMonoidDumpLogs = dumpLogs
-       , configMonoidColorWhen = colorWhen
-       , configMonoidSnapshotLocation = snapLoc
-       , configMonoidNoRunCompile = noRunCompile
+     ghcVariant ghcBuild jobs extraIncludeDirs extraLibDirs
+     customPreprocessorExts overrideGccPath overrideHpack skipGHCCheck skipMsys
+     localBinPath setupInfoLocations modifyCodePage allowDifferentUser dumpLogs
+     colorWhen snapshotLocation noRunCompile -> mempty
+       { stackRoot
+       , workDir
+       , buildOpts
+       , dockerOpts
+       , nixOpts
+       , systemGHC
+       , installGHC
+       , skipGHCCheck
+       , arch
+       , ghcVariant
+       , ghcBuild
+       , jobs
+       , extraIncludeDirs
+       , extraLibDirs
+       , customPreprocessorExts
+       , overrideGccPath
+       , overrideHpack
+       , skipMsys
+       , localBinPath
+       , setupInfoLocations
+       , modifyCodePage
+       , allowDifferentUser
+       , dumpLogs
+       , colorWhen
+       , snapshotLocation
+       , noRunCompile
        }
   )
   <$> optionalFirst (absDirOption
@@ -77,7 +79,7 @@         <> completer
              ( pathCompleterWith
                ( defaultPathCompleterOpts
-                   { pcoAbsolute = False, pcoFileFilter = const False }
+                   { absolute = False, fileFilter = const False }
                )
              )
         <> help "Relative path to Stack's work directory. Overrides any \
@@ -100,7 +102,7 @@   <*> optionalFirst (strOption
         (  long "arch"
         <> metavar "ARCH"
-        <> help "System architecture, e.g. i386, x86_64."
+        <> help "System architecture, e.g. i386, x86_64, aarch64."
         <> hide
         ))
   <*> optionalFirst (ghcVariantParser (hide0 /= OuterGlobalOpts))
src/Stack/Options/DotParser.hs view
@@ -1,37 +1,24 @@ {-# LANGUAGE NoImplicitPrelude #-}
 {-# LANGUAGE OverloadedStrings #-}
 
+-- | Function to parse command line arguments for Stack's @dot@ command and
+-- certain command line arguments for Stack's @ls dependencies@ command.
 module Stack.Options.DotParser
   ( dotOptsParser
-  , formatSubCommand
-  , licenseParser
-  , listDepsConstraintsParser
-  , listDepsFormatOptsParser
-  , listDepsJsonParser
-  , listDepsOptsParser
-  , listDepsTextParser
-  , listDepsTreeParser
-  , separatorParser
-  , toListDepsOptsParser
   ) where
 
 import           Data.Char ( isSpace )
 import           Data.List.Split ( splitOn )
 import qualified Data.Set as Set
-import qualified Data.Text as T
 import           Distribution.Types.PackageName ( mkPackageName )
 import           Options.Applicative
-                   ( CommandFields, Mod, Parser, auto, command, help, idm, info
-                   , long, metavar, option, progDesc, showDefault, strOption
-                   , subparser, switch, value
-                   )
-import           Options.Applicative.Builder.Extra ( boolFlags, textOption )
-import           Stack.Dot
-                   ( DotOpts (..), ListDepsFormat (..), ListDepsFormatOpts (..)
-                   , ListDepsOpts (..)
+                   ( Parser, auto, help, idm, long, metavar, option, strOption
+                   , switch
                    )
+import           Options.Applicative.Builder.Extra ( boolFlags )
 import           Stack.Options.BuildParser ( flagsParser, targetsParser )
 import           Stack.Prelude
+import           Stack.Types.DotOpts ( DotOpts (..) )
 
 -- | Parser for arguments to `stack dot`
 dotOptsParser :: Bool -> Parser DotOpts
@@ -87,75 +74,3 @@     <> help "Do not require an install GHC; instead, use a hints file for \
             \global packages."
     )
-
-separatorParser :: Parser Text
-separatorParser = fmap
-  escapeSep
-  ( textOption
-      (  long "separator"
-      <> metavar "SEP"
-      <> help "Separator between package name and package version."
-      <> value " "
-      <> showDefault
-      )
-  )
- where
-  escapeSep s = T.replace "\\t" "\t" (T.replace "\\n" "\n" s)
-
-licenseParser :: Parser Bool
-licenseParser = boolFlags False
-  "license"
-  "printing of dependency licenses instead of versions."
-  idm
-
-listDepsFormatOptsParser :: Parser ListDepsFormatOpts
-listDepsFormatOptsParser = ListDepsFormatOpts
-  <$> separatorParser
-  <*> licenseParser
-
-listDepsTreeParser :: Parser ListDepsFormat
-listDepsTreeParser =  ListDepsTree <$> listDepsFormatOptsParser
-
-listDepsTextParser :: Parser ListDepsFormat
-listDepsTextParser = ListDepsText <$> listDepsFormatOptsParser
-
-listDepsJsonParser :: Parser ListDepsFormat
-listDepsJsonParser = pure ListDepsJSON
-
-listDepsConstraintsParser :: Parser ListDepsFormat
-listDepsConstraintsParser = pure ListDepsConstraints
-
-toListDepsOptsParser :: Parser ListDepsFormat -> Parser ListDepsOpts
-toListDepsOptsParser formatParser = ListDepsOpts
-  <$> formatParser
-  <*> dotOptsParser True
-
-formatSubCommand ::
-     String
-  -> String
-  -> Parser ListDepsFormat
-  -> Mod CommandFields ListDepsOpts
-formatSubCommand cmd desc formatParser =
-  command cmd (info (toListDepsOptsParser formatParser) (progDesc desc))
-
--- | Parser for arguments to `stack ls dependencies`.
-listDepsOptsParser :: Parser ListDepsOpts
-listDepsOptsParser = subparser
-      (  formatSubCommand
-           "text"
-           "Print dependencies as text (default)."
-           listDepsTextParser
-      <> formatSubCommand
-           "cabal"
-           "Print dependencies as exact Cabal constraints."
-           listDepsConstraintsParser
-      <> formatSubCommand
-           "tree"
-           "Print dependencies as tree."
-           listDepsTreeParser
-      <> formatSubCommand
-           "json"
-           "Print dependencies as JSON."
-           listDepsJsonParser
-      )
-  <|> toListDepsOptsParser listDepsTextParser
src/Stack/Options/ExecParser.hs view
@@ -74,7 +74,8 @@   eoRtsOptionsParser :: Parser [String]
   eoRtsOptionsParser = concat <$> many (argsOption
     ( long "rts-options"
-    <> help "Explicit RTS options to pass to application."
+    <> help "Explicit RTS options to pass to application (can be specified \
+            \multiple times)."
     <> metavar "RTSFLAG"
     ))
 
src/Stack/Options/GlobalParser.hs view
@@ -1,5 +1,6 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 
 -- | Functions to parse Stack's \'global\' command line arguments.
 module Stack.Options.GlobalParser
@@ -116,37 +117,38 @@   => Bool
   -> GlobalOptsMonoid
   -> m GlobalOpts
-globalOptsFromMonoid defaultTerminal GlobalOptsMonoid{..} = do
-  resolver <- for (getFirst globalMonoidResolver) $ \ur -> do
+globalOptsFromMonoid defaultTerminal globalMonoid = do
+  resolver <- for (getFirst globalMonoid.resolver) $ \ur -> do
     root <-
-      case globalMonoidResolverRoot of
+      case globalMonoid.resolverRoot of
         First Nothing -> getCurrentDir
         First (Just dir) -> resolveDir' dir
     resolvePaths (Just root) ur
   stackYaml <-
-    case getFirst globalMonoidStackYaml of
+    case getFirst globalMonoid.stackYaml of
       Nothing -> pure SYLDefault
       Just fp -> SYLOverride <$> resolveFile' fp
-  pure GlobalOpts
-    { globalReExecVersion = getFirst globalMonoidReExecVersion
-    , globalDockerEntrypoint = getFirst globalMonoidDockerEntrypoint
-    , globalLogLevel = fromFirst defaultLogLevel globalMonoidLogLevel
-    , globalTimeInLog = fromFirstTrue globalMonoidTimeInLog
-    , globalRSLInLog = fromFirstFalse globalMonoidRSLInLog
-    , globalPlanInLog = fromFirstFalse globalMonoidPlanInLog
-    , globalConfigMonoid = globalMonoidConfigMonoid
-    , globalResolver = resolver
-    , globalCompiler = getFirst globalMonoidCompiler
-    , globalTerminal = fromFirst defaultTerminal globalMonoidTerminal
-    , globalStylesUpdate = globalMonoidStyles
-    , globalTermWidth = getFirst globalMonoidTermWidth
-    , globalStackYaml = stackYaml
-    , globalLockFileBehavior =
+  let lockFileBehavior =
         let defLFB =
-              case getFirst globalMonoidResolver of
+              case getFirst globalMonoid.resolver of
                 Nothing -> LFBReadWrite
                 _ -> LFBReadOnly
-        in  fromFirst defLFB globalMonoidLockFileBehavior
+        in  fromFirst defLFB globalMonoid.lockFileBehavior
+  pure GlobalOpts
+    { reExecVersion = getFirst globalMonoid.reExecVersion
+    , dockerEntrypoint = getFirst globalMonoid.dockerEntrypoint
+    , logLevel = fromFirst defaultLogLevel globalMonoid.logLevel
+    , timeInLog = fromFirstTrue globalMonoid.timeInLog
+    , rslInLog = fromFirstFalse globalMonoid.rslInLog
+    , planInLog = fromFirstFalse globalMonoid.planInLog
+    , configMonoid = globalMonoid.configMonoid
+    , resolver
+    , compiler = getFirst globalMonoid.compiler
+    , terminal = fromFirst defaultTerminal globalMonoid.terminal
+    , stylesUpdate = globalMonoid.styles
+    , termWidthOpt = getFirst globalMonoid.termWidthOpt
+    , stackYaml
+    , lockFileBehavior
     }
 
 -- | Default logging level should be something useful but not crazy.
src/Stack/Options/HaddockParser.hs view
@@ -8,7 +8,7 @@ import           Options.Applicative.Args ( argsOption )
 import           Stack.Options.Utils ( hideMods )
 import           Stack.Prelude
-import           Stack.Types.BuildOpts ( HaddockOptsMonoid (..) )
+import           Stack.Types.BuildOptsMonoid ( HaddockOptsMonoid (..) )
 
 -- | Parser for haddock arguments.
 haddockOptsParser :: Bool -> Parser HaddockOptsMonoid
src/Stack/Options/LsParser.hs view
@@ -1,20 +1,23 @@ {-# LANGUAGE NoImplicitPrelude   #-}
 {-# LANGUAGE OverloadedStrings   #-}
 
--- | Functions to parse command line arguments for Stack's @ls@ command.
+-- | Function to parse command line arguments for Stack's @ls@ command.
 module Stack.Options.LsParser
   ( lsOptsParser
   ) where
 
+import qualified Data.Text as T
 import qualified Options.Applicative as OA
 import           Options.Applicative ( idm )
-import           Options.Applicative.Builder.Extra ( boolFlags )
+import           Options.Applicative.Builder.Extra ( boolFlags, textOption )
 import           Stack.Constants ( globalFooter )
 import           Stack.Ls
-                   ( ListStylesOpts (..), ListToolsOpts (..), LsCmdOpts (..)
+                   ( ListDepsFormat (..), ListDepsFormatOpts (..)
+                   , ListDepsOpts (..), ListDepsTextFilter (..)
+                   , ListStylesOpts (..), ListToolsOpts (..), LsCmdOpts (..)
                    , LsCmds (..), LsView (..), SnapshotOpts (..)
                    )
-import           Stack.Options.DotParser ( listDepsOptsParser )
+import           Stack.Options.DotParser ( dotOptsParser )
 import           Stack.Prelude
 
 -- | Parse command line arguments for Stack's @ls@ command.
@@ -22,59 +25,6 @@ lsOptsParser = LsCmdOpts
   <$> OA.hsubparser (lsSnapCmd <> lsDepsCmd <> lsStylesCmd <> lsToolsCmd)
 
-lsCmdOptsParser :: OA.Parser LsCmds
-lsCmdOptsParser = LsSnapshot <$> lsViewSnapCmd
-
-lsDepOptsParser :: OA.Parser LsCmds
-lsDepOptsParser = LsDependencies <$> listDepsOptsParser
-
-lsStylesOptsParser :: OA.Parser LsCmds
-lsStylesOptsParser = LsStyles <$> listStylesOptsParser
-
-lsToolsOptsParser :: OA.Parser LsCmds
-lsToolsOptsParser = LsTools <$> listToolsOptsParser
-
-listStylesOptsParser :: OA.Parser ListStylesOpts
-listStylesOptsParser = ListStylesOpts
-  <$> boolFlags False
-        "basic"
-        "a basic report of the styles used. The default is a fuller one."
-        idm
-  <*> boolFlags True
-        "sgr"
-        "the provision of the equivalent SGR instructions (provided by \
-        \default). Flag ignored for a basic report."
-        idm
-  <*> boolFlags True
-        "example"
-        "the provision of an example of the applied style (provided by default \
-        \for colored output). Flag ignored for a basic report."
-        idm
-
-listToolsOptsParser :: OA.Parser ListToolsOpts
-listToolsOptsParser = ListToolsOpts
-  <$> OA.strOption
-        (  OA.long "filter"
-        <> OA.metavar "TOOL_NAME"
-        <> OA.value ""
-        <> OA.help "Filter by a tool name (eg 'ghc', 'ghc-git' or 'msys2') \
-                   \- case sensitive. (default: no filter)"
-        )
-
-lsViewSnapCmd :: OA.Parser SnapshotOpts
-lsViewSnapCmd = SnapshotOpts
-  <$> ( OA.hsubparser (lsViewRemoteCmd <> lsViewLocalCmd) <|> pure Local)
-  <*> OA.switch
-        (  OA.long "lts"
-        <> OA.short 'l'
-        <> OA.help "Only show LTS Haskell snapshots."
-        )
-  <*> OA.switch
-        (  OA.long "nightly"
-        <> OA.short 'n'
-        <> OA.help "Only show Nightly snapshots."
-        )
-
 lsSnapCmd :: OA.Mod OA.CommandFields LsCmds
 lsSnapCmd = OA.command "snapshots" $
   OA.info lsCmdOptsParser $
@@ -106,12 +56,32 @@     (OA.info lsToolsOptsParser
              (OA.progDesc "View Stack's installed tools."))
 
-lsViewLocalCmd :: OA.Mod OA.CommandFields LsView
-lsViewLocalCmd = OA.command "local" $
-  OA.info (pure Local) $
-       OA.progDesc "View local snapshots."
-    <> OA.footer localSnapshotMsg
+lsCmdOptsParser :: OA.Parser LsCmds
+lsCmdOptsParser = LsSnapshot <$> lsViewSnapCmd
 
+lsDepOptsParser :: OA.Parser LsCmds
+lsDepOptsParser = LsDependencies <$> listDepsOptsParser
+
+lsStylesOptsParser :: OA.Parser LsCmds
+lsStylesOptsParser = LsStyles <$> listStylesOptsParser
+
+lsToolsOptsParser :: OA.Parser LsCmds
+lsToolsOptsParser = LsTools <$> listToolsOptsParser
+
+lsViewSnapCmd :: OA.Parser SnapshotOpts
+lsViewSnapCmd = SnapshotOpts
+  <$> ( OA.hsubparser (lsViewRemoteCmd <> lsViewLocalCmd) <|> pure Local)
+  <*> OA.switch
+        (  OA.long "lts"
+        <> OA.short 'l'
+        <> OA.help "Only show LTS Haskell snapshots."
+        )
+  <*> OA.switch
+        (  OA.long "nightly"
+        <> OA.short 'n'
+        <> OA.help "Only show Nightly snapshots."
+        )
+
 lsViewRemoteCmd :: OA.Mod OA.CommandFields LsView
 lsViewRemoteCmd = OA.command "remote" $
   OA.info (pure Remote) $
@@ -123,6 +93,131 @@   "On a terminal, uses a pager, if one is available. Respects the PAGER \
   \environment variable (subject to that, prefers pager 'less' to 'more')."
 
+lsViewLocalCmd :: OA.Mod OA.CommandFields LsView
+lsViewLocalCmd = OA.command "local" $
+  OA.info (pure Local) $
+       OA.progDesc "View local snapshots."
+    <> OA.footer localSnapshotMsg
+
 localSnapshotMsg :: String
 localSnapshotMsg =
   "A local snapshot is identified by a hash code. " <> pagerMsg
+
+-- | Parser for arguments to `stack ls dependencies`.
+listDepsOptsParser :: OA.Parser ListDepsOpts
+listDepsOptsParser = OA.subparser
+      (  formatSubCommand
+           "text"
+           "Print dependencies as text (default)."
+           listDepsTextParser
+      <> formatSubCommand
+           "cabal"
+           "Print dependencies as exact Cabal constraints."
+           listDepsConstraintsParser
+      <> formatSubCommand
+           "tree"
+           "Print dependencies as tree."
+           listDepsTreeParser
+      <> formatSubCommand
+           "json"
+           "Print dependencies as JSON."
+           listDepsJsonParser
+      )
+  <|> toListDepsOptsParser listDepsTextParser
+
+formatSubCommand ::
+     String
+  -> String
+  -> OA.Parser ListDepsFormat
+  -> OA.Mod OA.CommandFields ListDepsOpts
+formatSubCommand cmd desc formatParser =
+  OA.command
+    cmd
+    (OA.info (toListDepsOptsParser formatParser) (OA.progDesc desc))
+
+listDepsTextParser :: OA.Parser ListDepsFormat
+listDepsTextParser =
+  ListDepsText <$> listDepsFormatOptsParser <*> textFilterParser
+
+textFilterParser :: OA.Parser [ListDepsTextFilter]
+textFilterParser = many (OA.option parseListDepsTextFilter
+  (  OA.long "filter"
+  <> OA.metavar "ITEM"
+  <> OA.help "Item to be filtered out of the results, if present, being either \
+             \$locals (for all local packages) or a package name (can be \
+             \specified multiple times)."
+  ))
+
+parseListDepsTextFilter :: OA.ReadM ListDepsTextFilter
+parseListDepsTextFilter = OA.eitherReader $ \s ->
+  if s == "$locals"
+    then Right FilterLocals
+    else case parsePackageName s of
+      Just pkgName -> Right $ FilterPackage pkgName
+      Nothing -> Left $ s <> " is not a valid package name."
+
+listDepsConstraintsParser :: OA.Parser ListDepsFormat
+listDepsConstraintsParser = pure ListDepsConstraints
+
+listDepsTreeParser :: OA.Parser ListDepsFormat
+listDepsTreeParser =  ListDepsTree <$> listDepsFormatOptsParser
+
+listDepsJsonParser :: OA.Parser ListDepsFormat
+listDepsJsonParser = pure ListDepsJSON
+
+listDepsFormatOptsParser :: OA.Parser ListDepsFormatOpts
+listDepsFormatOptsParser = ListDepsFormatOpts
+  <$> separatorParser
+  <*> licenseParser
+
+separatorParser :: OA.Parser Text
+separatorParser = fmap
+  escapeSep
+  ( textOption
+      (  OA.long "separator"
+      <> OA.metavar "SEP"
+      <> OA.help "Separator between package name and package version."
+      <> OA.value " "
+      <> OA.showDefault
+      )
+  )
+ where
+  escapeSep s = T.replace "\\t" "\t" (T.replace "\\n" "\n" s)
+
+licenseParser :: OA.Parser Bool
+licenseParser = boolFlags False
+  "license"
+  "printing of dependency licenses instead of versions."
+  idm
+
+toListDepsOptsParser :: OA.Parser ListDepsFormat -> OA.Parser ListDepsOpts
+toListDepsOptsParser formatParser = ListDepsOpts
+  <$> formatParser
+  <*> dotOptsParser True
+
+listStylesOptsParser :: OA.Parser ListStylesOpts
+listStylesOptsParser = ListStylesOpts
+  <$> boolFlags False
+        "basic"
+        "a basic report of the styles used. The default is a fuller one."
+        idm
+  <*> boolFlags True
+        "sgr"
+        "the provision of the equivalent SGR instructions (provided by \
+        \default). Flag ignored for a basic report."
+        idm
+  <*> boolFlags True
+        "example"
+        "the provision of an example of the applied style (provided by default \
+        \for colored output). Flag ignored for a basic report."
+        idm
+
+listToolsOptsParser :: OA.Parser ListToolsOpts
+listToolsOptsParser = ListToolsOpts
+  <$> OA.strOption
+        (  OA.long "filter"
+        <> OA.metavar "TOOL_NAME"
+        <> OA.value ""
+        <> OA.help "Filter by a tool name (eg 'ghc', 'ghc-git' or 'msys2') \
+                   \- case sensitive. (default: no filter)"
+        )
src/Stack/Options/NewParser.hs view
@@ -6,7 +6,8 @@ 
 import qualified Data.Map.Strict as M
 import           Options.Applicative
-                   ( Parser, help, long, metavar, short, switch )
+                   ( Parser, help, idm, long, metavar, short, switch )
+import           Options.Applicative.Builder.Extra ( boolFlags )
 import           Stack.Init ( InitOpts )
 import           Stack.New ( NewOpts (..) )
 import           Stack.Options.InitParser ( initOptsParser )
@@ -28,6 +29,10 @@           (  long "bare"
           <> help "Do not create a subdirectory for the project."
           )
+    <*> boolFlags True
+          "init"
+          "the initialisation of the project for use with Stack."
+          idm
     <*> optional (templateNameArgument
           (  metavar "TEMPLATE_NAME"
           <> help "Name of a template - can take the form\
src/Stack/Options/NixParser.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 
 module Stack.Options.NixParser
   ( nixOptsParser
@@ -65,7 +66,7 @@  where
   hide = hideMods hide0
   overrideActivation m =
-    if fromFirst False (nixMonoidPureShell m)
-      then m { nixMonoidEnable = (First . Just . fromFirst True) (nixMonoidEnable m) }
+    if fromFirst False m.pureShell
+      then m { enable = (First . Just . fromFirst True) m.enable }
       else m
   textArgsOption = fmap (map T.pack) . argsOption
src/Stack/Options/PackageParser.hs view
@@ -8,7 +8,7 @@ import           Options.Applicative ( ReadM, readerError )
 import           Options.Applicative.Types ( readerAsk )
 import           Stack.Prelude
-import           Stack.Types.BuildOpts ( ApplyCLIFlag (..) )
+import           Stack.Types.BuildOptsCLI ( ApplyCLIFlag (..) )
 
 -- | Parser for package:[-]flag
 readFlag :: ReadM (Map ApplyCLIFlag (Map FlagName Bool))
src/Stack/Options/ResolverParser.hs view
@@ -15,12 +15,13 @@ import           Stack.Prelude
 import           Stack.Types.Resolver ( AbstractResolver, readAbstractResolver )
 
--- | Parser for the resolver
+-- | Parser for the snapshot
 abstractResolverOptsParser :: Bool -> Parser (Unresolved AbstractResolver)
 abstractResolverOptsParser hide = option readAbstractResolver
-  (  long "resolver"
-  <> metavar "RESOLVER"
-  <> help "Override resolver in project file."
+  (  long "snapshot"
+  <> long "resolver"
+  <> metavar "SNAPSHOT"
+  <> help "Override snapshot in the project configuration file."
   <> hideMods hide
   )
 
src/Stack/Options/SetupParser.hs view
@@ -19,7 +19,7 @@ setupOptsParser = SetupCmdOpts
   <$> OA.optional (OA.argument readVersion
         (  OA.metavar "GHC_VERSION"
-        <> OA.help "Version of GHC to install, e.g. 9.6.2. (default: install \
+        <> OA.help "Version of GHC to install, e.g. 9.6.4. (default: install \
                    \the version implied by the resolver)"
         ))
   <*> OA.boolFlags False
src/Stack/Options/TestParser.hs view
@@ -11,7 +11,7 @@                    ( firstBoolFlagsTrue, optionalFirst, optionalFirstFalse )
 import           Stack.Options.Utils ( hideMods )
 import           Stack.Prelude
-import           Stack.Types.BuildOpts ( TestOptsMonoid (..) )
+import           Stack.Types.BuildOptsMonoid ( TestOptsMonoid (..) )
 
 -- | Parser for test arguments.
 -- FIXME hide args
+ src/Stack/Options/UnpackParser.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- | Functions to parse command line arguments for Stack's @unpack@ command.
+module Stack.Options.UnpackParser
+  ( unpackOptsParser
+  ) where
+
+import qualified Data.Text as T
+import           Options.Applicative
+                   ( Parser, ReadM, argument, eitherReader, help, long, metavar
+                   , option, switch
+                   )
+import           Path ( SomeBase (..), parseSomeDir )
+import           Stack.Prelude
+import           Stack.Unpack ( UnpackOpts (..), UnpackTarget)
+
+-- | Parse command line arguments for Stack's @unpack@ command.
+unpackOptsParser :: Parser UnpackOpts
+unpackOptsParser = UnpackOpts
+  <$> some unpackTargetParser
+  <*> areCandidatesParser
+  <*> optional dirParser
+
+unpackTargetParser :: Parser UnpackTarget
+unpackTargetParser = argument unpackTargetReader
+  (  metavar "TARGET"
+  <> help "A package or package candidate (can be specified multiple times). A \
+          \package can be referred to by name only or by identifier \
+          \(including, optionally, a revision as '@rev:<number>' or \
+          \'@sha256:<sha>'). A package candidate is referred to by its \
+          \identifier."
+  )
+
+unpackTargetReader :: ReadM UnpackTarget
+unpackTargetReader = eitherReader $ \s ->
+  case parsePackageIdentifierRevision $ T.pack s of
+    Right pir -> Right (Right pir)
+    Left _ -> case parsePackageName s of
+      Just pn -> Right (Left pn)
+      Nothing ->
+        Left $ s <> " is an invalid way to refer to a package or package \
+                    \candidate to be unpacked."
+
+areCandidatesParser :: Parser Bool
+areCandidatesParser = switch
+  (  long "candidate"
+  <> help "Each target is a package candidate."
+  )
+
+dirParser :: Parser (SomeBase Dir)
+dirParser = option dirReader
+  (  long "to"
+  <> metavar "DIR"
+  <> help "Optionally, a directory to unpack into. A target will be unpacked \
+          \ into a subdirectory."
+  )
+
+dirReader :: ReadM (SomeBase Dir)
+dirReader = eitherReader $ \s ->
+  case parseSomeDir s of
+    Just dir -> Right dir
+    Nothing ->
+      Left $ s <> " is an invalid way to refer to a directory."
src/Stack/Options/UploadParser.hs view
@@ -5,19 +5,68 @@   ( uploadOptsParser
   ) where
 
-import           Options.Applicative ( Parser, flag, help, long )
-import           Stack.Options.SDistParser ( sdistOptsParser )
+import qualified Data.Text as T
+import           Options.Applicative
+                   ( Parser, completeWith, completer, flag, help, idm, long
+                   , metavar, option, readerError, short, strArgument, strOption
+                   , switch
+                   )
+import           Options.Applicative.Builder.Extra ( boolFlags, dirCompleter )
+import           Options.Applicative.Types ( readerAsk )
 import           Stack.Prelude
 import           Stack.Upload ( UploadOpts (..), UploadVariant (..) )
+import           Stack.Types.PvpBounds ( PvpBounds (..), parsePvpBounds )
 
 -- | Parse command line arguments for Stack's @upload@ command.
 uploadOptsParser :: Parser UploadOpts
-uploadOptsParser =
-  UploadOpts
-    <$> sdistOptsParser
-    <*> uploadVariant
-  where
-    uploadVariant = flag Publishing Candidate
-      (  long "candidate"
-      <> help "Upload as a package candidate."
+uploadOptsParser = UploadOpts
+  <$> itemsToWorkWithParser
+  <*> documentationParser
+  <*> optional pvpBoundsOption
+  <*> ignoreCheckSwitch
+  <*> buildPackageOption
+  <*> tarDirParser
+  <*> uploadVariantParser
+ where
+  itemsToWorkWithParser = many (strArgument
+    (  metavar "ITEM"
+    <> completer dirCompleter
+    <> help "A relative path to a package directory or, for package upload \
+            \only, an sdist tarball."
+    ))
+  documentationParser = flag False True
+    (  long "documentation"
+    <> short 'd'
+    <> help "Upload documentation for packages (not packages)."
+    )
+  pvpBoundsOption :: Parser PvpBounds
+  pvpBoundsOption = option readPvpBounds
+    (  long "pvp-bounds"
+    <> metavar "PVP-BOUNDS"
+    <> completeWith ["none", "lower", "upper", "both"]
+    <> help "For package upload, how PVP version bounds should be added to \
+            \Cabal file: none, lower, upper, both."
+    )
+   where
+    readPvpBounds = do
+      s <- readerAsk
+      case parsePvpBounds $ T.pack s of
+        Left e -> readerError e
+        Right v -> pure v
+  ignoreCheckSwitch = switch
+      (  long "ignore-check"
+      <> help "For package upload, do not check packages for common mistakes."
       )
+  buildPackageOption = boolFlags False
+      "test-tarball"
+      "building of the resulting sdist tarball(s), for package upload."
+      idm
+  tarDirParser = optional (strOption
+    (  long "tar-dir"
+    <> help "For package upload, if specified, copy all the tar to this \
+            \directory."
+    ))
+  uploadVariantParser = flag Publishing Candidate
+    (  long "candidate"
+    <> help "Upload as, or for, a package candidate."
+    )
src/Stack/Package.hs view
@@ -1,7 +1,9 @@-{-# LANGUAGE NoImplicitPrelude   #-}
-{-# LANGUAGE DataKinds           #-}
-{-# LANGUAGE OverloadedStrings   #-}
-{-# LANGUAGE RecordWildCards     #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE ScopedTypeVariables   #-}
 
 -- | Dealing with Cabal.
 
@@ -10,24 +12,35 @@   , resolvePackage
   , packageFromPackageDescription
   , Package (..)
-  , PackageDescriptionPair (..)
-  , GetPackageOpts (..)
   , PackageConfig (..)
   , buildLogPath
   , PackageException (..)
   , resolvePackageDescription
   , packageDependencies
   , applyForceCustomBuild
+  , hasBuildableMainLibrary
+  , mainLibraryHasExposedModules
+  , packageUnknownTools
+  , buildableForeignLibs
+  , buildableSubLibs
+  , buildableExes
+  , buildableTestSuites
+  , buildableBenchmarks
+  , getPackageOpts
+  , processPackageDepsToList
+  , listOfPackageDeps
+  , setOfPackageDeps
+  , topSortPackageComponent
   ) where
 
-import           Data.List ( unzip )
 import qualified Data.Map.Strict as M
 import qualified Data.Set as S
+import           Data.STRef ( STRef, modifySTRef', readSTRef, newSTRef )
 import qualified Data.Text as T
 import           Distribution.CabalSpecVersion ( cabalSpecToVersionDigits )
-import qualified Distribution.Compat.NonEmptySet as NES
 import           Distribution.Compiler
                    ( CompilerFlavor (..), PerCompilerFlavor (..) )
+import           Distribution.ModuleName ( ModuleName )
 import           Distribution.Package ( mkPackageName )
 import           Distribution.PackageDescription
                    ( Benchmark (..), BuildInfo (..), BuildType (..)
@@ -35,18 +48,15 @@                    , Dependency (..), Executable (..), ForeignLib (..)
                    , GenericPackageDescription (..), HookedBuildInfo
                    , Library (..), PackageDescription (..), PackageFlag (..)
-                   , SetupBuildInfo (..), TestSuite (..), allLanguages
-                   , allLibraries, buildType, depPkgName, depVerRange
-                   , libraryNameString, maybeToLibraryName, usedExtensions
+                   , SetupBuildInfo (..), TestSuite (..), allLibraries
+                   , buildType, depPkgName, depVerRange
                    )
-import           Distribution.Pretty ( prettyShow )
+import qualified Distribution.PackageDescription as Executable
+                   ( Executable (..) )
 import           Distribution.Simple.PackageDescription ( readHookedBuildInfo )
 import           Distribution.System ( OS (..), Arch, Platform (..) )
 import           Distribution.Text ( display )
 import qualified Distribution.Types.CondTree as Cabal
-import qualified Distribution.Types.ExeDependency as Cabal
-import qualified Distribution.Types.LegacyExeDependency as Cabal
-import qualified Distribution.Types.UnqualComponentName as Cabal
 import           Distribution.Utils.Path ( getSymbolicPath )
 import           Distribution.Verbosity ( silent )
 import           Distribution.Version
@@ -56,34 +66,58 @@                    , stripProperPrefix
                    )
 import           Path.Extra ( concatAndCollapseAbsDir, toFilePathNoTrailingSep )
-import           Stack.Constants (relFileCabalMacrosH, relDirLogs)
-import           Stack.Constants.Config ( distDirFromDir )
-import           Stack.Prelude hiding ( Display (..) )
+import           Stack.Component
+                   ( componentDependencyMap, foldOnNameAndBuildInfo
+                   , isComponentBuildable, stackBenchmarkFromCabal
+                   , stackExecutableFromCabal, stackForeignLibraryFromCabal
+                   , stackLibraryFromCabal, stackTestFromCabal
+                   , stackUnqualToQual
+                   )
 import           Stack.ComponentFile
                    ( buildDir, componentAutogenDir, componentBuildDir
                    , componentOutputDir, packageAutogenDir
                    )
+import           Stack.Constants (relFileCabalMacrosH, relDirLogs)
+import           Stack.Constants.Config ( distDirFromDir )
+import           Stack.PackageFile ( getPackageFile, stackPackageFileFromCabal )
+import           Stack.Prelude hiding ( Display (..) )
 import           Stack.Types.BuildConfig
                    ( HasBuildConfig (..), getProjectWorkDir )
-import           Stack.Types.Compiler ( ActualCompiler (..), getGhcVersion )
+import           Stack.Types.CompCollection
+                   ( CompCollection, collectionLookup, foldAndMakeCollection
+                   , foldComponentToAnotherCollection, getBuildableSetText
+                   )
+import           Stack.Types.Compiler ( ActualCompiler (..) )
 import           Stack.Types.CompilerPaths ( cabalVersionL )
+import           Stack.Types.Component
+                   ( HasBuildInfo, HasComponentInfo, StackUnqualCompName (..) )
+import qualified Stack.Types.Component as Component
 import           Stack.Types.Config ( Config (..), HasConfig (..) )
+import           Stack.Types.Dependency
+                   ( DepLibrary (..), DepType (..), DepValue (..)
+                   , cabalSetupDepsToStackDep, libraryDepFromVersionRange
+                   )
 import           Stack.Types.EnvConfig ( HasEnvConfig )
-import           Stack.Types.GhcPkgId ( ghcPkgIdString )
+import           Stack.Types.Installed
+                   ( InstallMap, Installed (..), InstalledMap
+                   , installedToPackageIdOpt
+                   )
 import           Stack.Types.NamedComponent
-                   ( NamedComponent (..), internalLibComponents )
+                   ( NamedComponent (..), isPotentialDependency
+                   , subLibComponents
+                   )
 import           Stack.Types.Package
-                   ( BuildInfoOpts (..), ExeName (..), GetPackageOpts (..)
-                   , InstallMap, Installed (..), InstalledMap, Package (..)
+                   ( BioInput(..), BuildInfoOpts (..), Package (..)
                    , PackageConfig (..), PackageException (..)
-                   , PackageLibraries (..), dotCabalCFilePath, packageIdentifier
+                   , dotCabalCFilePath, packageIdentifier
                    )
+import           Stack.Types.PackageFile
+                   ( DotCabalPath, PackageComponentFile (..) )
+import           Stack.Types.SourceMap (Target(..))
 import           Stack.Types.Version
                    ( VersionRange, intersectVersionRanges, withinRange )
 import           System.FilePath ( replaceExtension )
-import           Stack.Types.Dependency ( DepValue (..), DepType (..) )
-import           Stack.Types.PackageFile ( DotCabalPath , GetPackageFiles (..) )
-import           Stack.PackageFile ( getPackageFile )
+import           RIO.Seq ((|>))
 
 -- | Read @<package>.buildinfo@ ancillary files produced by some Setup.hs hooks.
 -- The file includes Cabal file syntax to be merged into the package description
@@ -106,139 +140,82 @@ packageFromPackageDescription ::
      PackageConfig
   -> [PackageFlag]
-  -> PackageDescriptionPair
+  -> PackageDescription
   -> Package
-packageFromPackageDescription packageConfig pkgFlags (PackageDescriptionPair pkgNoMod pkg) =
-  Package
-  { packageName = name
-  , packageVersion = pkgVersion pkgId
-  , packageLicense = licenseRaw pkg
-  , packageDeps = deps
-  , packageFiles = pkgFiles
-  , packageUnknownTools = unknownTools
-  , packageGhcOptions = packageConfigGhcOptions packageConfig
-  , packageCabalConfigOpts = packageConfigCabalConfigOpts packageConfig
-  , packageFlags = packageConfigFlags packageConfig
-  , packageDefaultFlags = M.fromList
-      [(flagName flag, flagDefault flag) | flag <- pkgFlags]
-  , packageAllDeps = M.keysSet deps
-  , packageSubLibDeps = subLibDeps
-  , packageLibraries =
-      let mlib = do
-            lib <- library pkg
-            guard $ buildable $ libBuildInfo lib
-            Just lib
-       in
-        case mlib of
-          Nothing -> NoLibraries
-          Just _ -> HasLibraries foreignLibNames
-  , packageInternalLibraries = subLibNames
-  , packageTests = M.fromList
-      [ (T.pack (Cabal.unUnqualComponentName $ testName t), testInterface t)
-      | t <- testSuites pkgNoMod
-      , buildable (testBuildInfo t)
-      ]
-  , packageBenchmarks = S.fromList
-      [ T.pack (Cabal.unUnqualComponentName $ benchmarkName b)
-      | b <- benchmarks pkgNoMod
-      , buildable (benchmarkBuildInfo b)
-      ]
-      -- Same comment about buildable applies here too.
-  , packageExes = S.fromList
-      [ T.pack (Cabal.unUnqualComponentName $ exeName biBuildInfo)
-      | biBuildInfo <- executables pkg
-      , buildable (buildInfo biBuildInfo)
-      ]
-  -- This is an action used to collect info needed for "stack ghci".
-  -- This info isn't usually needed, so computation of it is deferred.
-  , packageOpts = GetPackageOpts $
-      \installMap installedMap omitPkgs addPkgs cabalfp -> do
-        (componentsModules,componentFiles, _, _) <- getPackageFiles pkgFiles cabalfp
-        let internals =
-              S.toList $ internalLibComponents $ M.keysSet componentsModules
-        excludedInternals <- mapM (parsePackageNameThrowing . T.unpack) internals
-        mungedInternals <- mapM
-          (parsePackageNameThrowing . T.unpack . toInternalPackageMungedName)
-          internals
-        componentsOpts <- generatePkgDescOpts
-          installMap
-          installedMap
-          (excludedInternals ++ omitPkgs)
-          (mungedInternals ++ addPkgs)
-          cabalfp
-          pkg
-          componentFiles
-        pure (componentsModules, componentFiles, componentsOpts)
-  , packageHasExposedModules = maybe
-      False
-      (not . null . exposedModules)
-      (library pkg)
-  , packageBuildType = buildType pkg
-  , packageSetupDeps = msetupDeps
-  , packageCabalSpec = specVersion pkg
-  }
+packageFromPackageDescription
+    packageConfig
+    pkgFlags
+    pkg
+  = Package
+      { name = name
+      , version = pkgVersion pkgId
+      , license = licenseRaw pkg
+      , ghcOptions =  packageConfig.ghcOptions
+      , cabalConfigOpts =  packageConfig.cabalConfigOpts
+      , flags = packageConfig.flags
+      , defaultFlags = M.fromList
+          [(flagName flag, flagDefault flag) | flag <- pkgFlags]
+      , library = stackLibraryFromCabal <$> library pkg
+      , subLibraries =
+          foldAndMakeCollection stackLibraryFromCabal $ subLibraries pkg
+      , foreignLibraries =
+          foldAndMakeCollection stackForeignLibraryFromCabal $ foreignLibs pkg
+      , testSuites =
+          foldAndMakeCollection stackTestFromCabal $ testSuites pkg
+      , benchmarks =
+          foldAndMakeCollection stackBenchmarkFromCabal $ benchmarks pkg
+      , executables =
+          foldAndMakeCollection stackExecutableFromCabal $ executables pkg
+      , buildType = buildType pkg
+      , setupDeps = fmap cabalSetupDepsToStackDep (setupBuildInfo pkg)
+      , cabalSpec = specVersion pkg
+      , file = stackPackageFileFromCabal pkg
+      , testEnabled =  packageConfig.enableTests
+      , benchmarkEnabled = packageConfig.enableBenchmarks
+      }
  where
-  extraLibNames = S.union subLibNames foreignLibNames
-
-  subLibNames
-    = S.fromList
-    $ map (T.pack . Cabal.unUnqualComponentName)
-    $ mapMaybe (libraryNameString . libName) -- this is a design bug in the
-                                             -- Cabal API: this should
-                                             -- statically be known to exist
-    $ filter (buildable . libBuildInfo)
-    $ subLibraries pkg
-
-  foreignLibNames
-    = S.fromList
-    $ map (T.pack . Cabal.unUnqualComponentName . foreignLibName)
-    $ filter (buildable . foreignLibBuildInfo)
-    $ foreignLibs pkg
-
-  toInternalPackageMungedName
-    = T.pack . prettyShow . MungedPackageName (pkgName pkgId)
-    . maybeToLibraryName . Just . Cabal.mkUnqualComponentName . T.unpack
-
   -- Gets all of the modules, files, build files, and data files that constitute
   -- the package. This is primarily used for dirtiness checking during build, as
   -- well as use by "stack ghci"
-  pkgFiles = GetPackageFiles $ getPackageFile pkg
   pkgId = package pkg
   name = pkgName pkgId
 
-  (unknownTools, knownTools) = packageDescTools pkg
-
-  deps = M.filterWithKey (const . not . isMe) (M.unionsWith (<>)
-    [ asLibrary <$> packageDependencies packageConfig pkg
-    -- We include all custom-setup deps - if present - in the package deps
-    -- themselves. Stack always works with the invariant that there will be a
-    -- single installed package relating to a package name, and this applies at
-    -- the setup dependency level as well.
-    , asLibrary <$> fromMaybe M.empty msetupDeps
-    , knownTools
-    ])
-
-  msetupDeps = fmap
-    (M.fromList . map (depPkgName &&& depVerRange) . setupDepends)
-    (setupBuildInfo pkg)
-
-  subLibDeps = M.fromList $ concatMap
-    (\(Dependency n vr libs) -> mapMaybe (getSubLibName n vr) (NES.toList libs))
-    (concatMap targetBuildDepends (allBuildInfo' pkg))
-
-  getSubLibName pn vr lib@(LSubLibName _) =
-    Just (MungedPackageName pn lib, asLibrary vr)
-  getSubLibName _ _ _ = Nothing
-
-  asLibrary range = DepValue
-    { dvVersionRange = range
-    , dvType = AsLibrary
-    }
-
-  -- Is the package dependency mentioned here me: either the package name
-  -- itself, or the name of one of the sub libraries
-  isMe name' =  name' == name
-             || fromString (packageNameString name') `S.member` extraLibNames
+-- | This is an action used to collect info needed for "stack ghci". This info
+-- isn't usually needed, so computation of it is deferred.
+getPackageOpts ::
+     (HasEnvConfig env, MonadReader env m, MonadThrow m, MonadUnliftIO m )
+  => Package
+  -> InstallMap
+  -> InstalledMap
+  -> [PackageName]
+  -> [PackageName]
+  -> Path Abs File
+  -> m ( Map NamedComponent (Map ModuleName (Path Abs File))
+       , Map NamedComponent [DotCabalPath]
+       , Map NamedComponent BuildInfoOpts
+       )
+getPackageOpts
+    stackPackage
+    installMap
+    installedMap
+    omitPkgs
+    addPkgs
+    cabalFP
+  = do
+      PackageComponentFile !componentsModules componentFiles _ _ <-
+        getPackageFile stackPackage cabalFP
+      let subLibs =
+            S.toList $ subLibComponents $ M.keysSet componentsModules
+      excludedSubLibs <- mapM (parsePackageNameThrowing . T.unpack) subLibs
+      componentsOpts <- generatePkgDescOpts
+        installMap
+        installedMap
+        (excludedSubLibs ++ omitPkgs)
+        addPkgs
+        cabalFP
+        stackPackage
+        componentFiles
+      pure (componentsModules, componentFiles, componentsOpts)
 
 -- | Generate GHC options for the package's components, and a list of options
 -- which apply generally to the package, not one specific component.
@@ -251,97 +228,61 @@   -> [PackageName]
      -- ^ Packages to add to the "-package" flags
   -> Path Abs File
-  -> PackageDescription
+  -> Package
   -> Map NamedComponent [DotCabalPath]
   -> m (Map NamedComponent BuildInfoOpts)
-generatePkgDescOpts installMap installedMap omitPkgs addPkgs cabalfp pkg componentPaths = do
-  config <- view configL
-  cabalVer <- view cabalVersionL
-  distDir <- distDirFromDir cabalDir
-  let generate namedComponent binfo =
-        ( namedComponent
-        , generateBuildInfoOpts BioInput
-            { biInstallMap = installMap
-            , biInstalledMap = installedMap
-            , biCabalDir = cabalDir
-            , biDistDir = distDir
-            , biOmitPackages = omitPkgs
-            , biAddPackages = addPkgs
-            , biBuildInfo = binfo
-            , biDotCabalPaths =
-                fromMaybe [] (M.lookup namedComponent componentPaths)
-            , biConfigLibDirs = configExtraLibDirs config
-            , biConfigIncludeDirs = configExtraIncludeDirs config
-            , biComponentName = namedComponent
-            , biCabalVersion = cabalVer
+generatePkgDescOpts
+    installMap
+    installedMap
+    omitPackages
+    addPackages
+    cabalFP
+    pkg
+    componentPaths
+  = do
+      config <- view configL
+      cabalVersion <- view cabalVersionL
+      distDir <- distDirFromDir cabalDir
+      let generate componentName buildInfo = generateBuildInfoOpts BioInput
+            { installMap
+            , installedMap
+            , cabalDir
+            , distDir
+            , omitPackages
+            , addPackages
+            , buildInfo
+            , dotCabalPaths =
+                fromMaybe [] (M.lookup componentName componentPaths)
+            , configLibDirs = config.extraLibDirs
+            , configIncludeDirs = config.extraIncludeDirs
+            , componentName
+            , cabalVersion
             }
-        )
-  pure
-    ( M.fromList
-        ( concat
-            [ maybe
-                []
-                (pure . generate CLib . libBuildInfo)
-                (library pkg)
-            , mapMaybe
-                (\sublib -> do
-                  let maybeLib =
-                        CInternalLib . T.pack . Cabal.unUnqualComponentName <$>
-                          (libraryNameString . libName) sublib
-                  flip generate  (libBuildInfo sublib) <$> maybeLib
-                 )
-                (subLibraries pkg)
-            , fmap
-                (\exe ->
-                  generate
-                    (CExe (T.pack (Cabal.unUnqualComponentName (exeName exe))))
-                    (buildInfo exe)
-                )
-                (executables pkg)
-            , fmap
-                (\bench ->
-                  generate
-                    (CBench
-                      (T.pack (Cabal.unUnqualComponentName (benchmarkName bench)))
-                    )
-                    (benchmarkBuildInfo bench)
-                )
-                (benchmarks pkg)
-            , fmap
-                (\test ->
-                  generate
-                    (CTest (T.pack (Cabal.unUnqualComponentName (testName test))))
-                    (testBuildInfo test)
-                )
-                (testSuites pkg)
-            ]
-        )
-    )
+      let insertInMap name compVal = M.insert name (generate name compVal)
+      let translatedInsertInMap constructor name =
+            insertInMap (stackUnqualToQual constructor name)
+      let makeBuildInfoOpts selector constructor =
+            foldOnNameAndBuildInfo
+              (selector pkg)
+              (translatedInsertInMap constructor)
+      let aggregateAllBuildInfoOpts =
+              makeBuildInfoOpts (.library) (const CLib)
+            . makeBuildInfoOpts (.subLibraries) CSubLib
+            . makeBuildInfoOpts (.executables) CExe
+            . makeBuildInfoOpts (.benchmarks) CBench
+            . makeBuildInfoOpts (.testSuites) CTest
+      pure $ aggregateAllBuildInfoOpts mempty
  where
-  cabalDir = parent cabalfp
-
--- | Input to 'generateBuildInfoOpts'
-data BioInput = BioInput
-  { biInstallMap :: !InstallMap
-  , biInstalledMap :: !InstalledMap
-  , biCabalDir :: !(Path Abs Dir)
-  , biDistDir :: !(Path Abs Dir)
-  , biOmitPackages :: ![PackageName]
-  , biAddPackages :: ![PackageName]
-  , biBuildInfo :: !BuildInfo
-  , biDotCabalPaths :: ![DotCabalPath]
-  , biConfigLibDirs :: ![FilePath]
-  , biConfigIncludeDirs :: ![FilePath]
-  , biComponentName :: !NamedComponent
-  , biCabalVersion :: !Version
-  }
+  cabalDir = parent cabalFP
 
 -- | Generate GHC options for the target. Since Cabal also figures out these
 -- options, currently this is only used for invoking GHCI (via stack ghci).
 generateBuildInfoOpts :: BioInput -> BuildInfoOpts
-generateBuildInfoOpts BioInput {..} =
+generateBuildInfoOpts bi =
   BuildInfoOpts
-    { bioOpts = ghcOpts ++ fmap ("-optP" <>) (cppOptions biBuildInfo)
+    { opts =
+           ghcOpts
+        ++ fmap ("-optP" <>) bi.buildInfo.cppOptions
     -- NOTE for future changes: Due to this use of nubOrd (and other uses
     -- downstream), these generated options must not rely on multiple
     -- argument sequences.  For example, ["--main-is", "Foo.hs", "--main-
@@ -349,77 +290,82 @@     -- "--main-is" being removed.
     --
     -- See https://github.com/commercialhaskell/stack/issues/1255
-    , bioOneWordOpts = nubOrd $ concat
+    , oneWordOpts = nubOrd $ concat
         [extOpts, srcOpts, includeOpts, libOpts, fworks, cObjectFiles]
-    , bioPackageFlags = deps
-    , bioCabalMacros = componentAutogen </> relFileCabalMacrosH
+    , packageFlags = deps
+    , cabalMacros = componentAutogen </> relFileCabalMacrosH
     }
  where
-  cObjectFiles =
-    mapMaybe (fmap toFilePath .
-              makeObjectFilePathFromC biCabalDir biComponentName biDistDir)
-             cfiles
-  cfiles = mapMaybe dotCabalCFilePath biDotCabalPaths
+  cObjectFiles = mapMaybe
+    ( fmap toFilePath
+    . makeObjectFilePathFromC bi.cabalDir bi.componentName bi.distDir
+    )
+    cfiles
+  cfiles = mapMaybe dotCabalCFilePath bi.dotCabalPaths
   installVersion = snd
   -- Generates: -package=base -package=base16-bytestring-0.1.1.6 ...
   deps =
     concat
-      [ case M.lookup name biInstalledMap of
-          Just (_, Stack.Types.Package.Library _ident ipid _) ->
-            ["-package-id=" <> ghcPkgIdString ipid]
+      [ case M.lookup name bi.installedMap of
+          Just (_, Stack.Types.Installed.Library _ident installedInfo) ->
+            installedToPackageIdOpt installedInfo
           _ -> ["-package=" <> packageNameString name <>
             maybe "" -- This empty case applies to e.g. base.
               ((("-" <>) . versionString) . installVersion)
-              (M.lookup name biInstallMap)]
+              (M.lookup name bi.installMap)]
       | name <- pkgs
       ]
   pkgs =
-    biAddPackages ++
+    bi.addPackages ++
     [ name
-    | Dependency name _ _ <- targetBuildDepends biBuildInfo
-      -- TODO: cabal 3 introduced multiple public libraries in a single dependency
-    , name `notElem` biOmitPackages
+    | Dependency name _ _ <- bi.buildInfo.targetBuildDepends
+      -- TODO: Cabal 3.0 introduced multiple public libraries in a single
+      -- dependency
+    , name `notElem` bi.omitPackages
     ]
-  PerCompilerFlavor ghcOpts _ = options biBuildInfo
+  PerCompilerFlavor ghcOpts _ = bi.buildInfo.options
   extOpts =
-       map (("-X" ++) . display) (allLanguages biBuildInfo)
-    <> map (("-X" ++) . display) (usedExtensions biBuildInfo)
+       map (("-X" ++) . display) bi.buildInfo.allLanguages
+    <> map (("-X" ++) . display) bi.buildInfo.usedExtensions
   srcOpts =
     map (("-i" <>) . toFilePathNoTrailingSep)
       (concat
-        [ [ componentBuildDir biCabalVersion biComponentName biDistDir ]
-        , [ biCabalDir
-          | null (hsSourceDirs biBuildInfo)
+        [ [ componentBuildDir bi.cabalVersion bi.componentName bi.distDir ]
+        , [ bi.cabalDir
+          | null bi.buildInfo.hsSourceDirs
           ]
-        , mapMaybe (toIncludeDir . getSymbolicPath) (hsSourceDirs biBuildInfo)
+        , mapMaybe
+            (toIncludeDir . getSymbolicPath)
+            bi.buildInfo.hsSourceDirs
         , [ componentAutogen ]
-        , maybeToList (packageAutogenDir biCabalVersion biDistDir)
-        , [ componentOutputDir biComponentName biDistDir ]
+        , maybeToList (packageAutogenDir bi.cabalVersion bi.distDir)
+        , [ componentOutputDir bi.componentName bi.distDir ]
         ]) ++
-    [ "-stubdir=" ++ toFilePathNoTrailingSep (buildDir biDistDir) ]
-  componentAutogen = componentAutogenDir biCabalVersion biComponentName biDistDir
-  toIncludeDir "." = Just biCabalDir
-  toIncludeDir relDir = concatAndCollapseAbsDir biCabalDir relDir
+    [ "-stubdir=" ++ toFilePathNoTrailingSep (buildDir bi.distDir) ]
+  componentAutogen =
+    componentAutogenDir bi.cabalVersion bi.componentName bi.distDir
+  toIncludeDir "." = Just bi.cabalDir
+  toIncludeDir relDir = concatAndCollapseAbsDir bi.cabalDir relDir
   includeOpts =
-    map ("-I" <>) (biConfigIncludeDirs <> pkgIncludeOpts)
+    map ("-I" <>) (bi.configIncludeDirs <> pkgIncludeOpts)
   pkgIncludeOpts =
     [ toFilePathNoTrailingSep absDir
-    | dir <- includeDirs biBuildInfo
+    | dir <- bi.buildInfo.includeDirs
     , absDir <- handleDir dir
     ]
   libOpts =
-    map ("-l" <>) (extraLibs biBuildInfo) <>
-    map ("-L" <>) (biConfigLibDirs <> pkgLibDirs)
+    map ("-l" <>) bi.buildInfo.extraLibs <>
+    map ("-L" <>) (bi.configLibDirs <> pkgLibDirs)
   pkgLibDirs =
     [ toFilePathNoTrailingSep absDir
-    | dir <- extraLibDirs biBuildInfo
+    | dir <- bi.buildInfo.extraLibDirs
     , absDir <- handleDir dir
     ]
   handleDir dir = case (parseAbsDir dir, parseRelDir dir) of
     (Just ab, _       ) -> [ab]
-    (_      , Just rel) -> [biCabalDir </> rel]
+    (_      , Just rel) -> [bi.cabalDir </> rel]
     (Nothing, Nothing ) -> []
-  fworks = map ("-framework=" <>) (frameworks biBuildInfo)
+  fworks = map ("-framework=" <>) bi.buildInfo.frameworks
 
 -- | Make the .o path from the .c file path for a component. Example:
 --
@@ -461,111 +407,14 @@   pure (componentOutputDir namedComponent distDir </> relOFilePath)
 
 -- | Get all dependencies of the package (buildable targets only).
---
--- Note that for Cabal versions 1.22 and earlier, there is a bug where Cabal
--- requires dependencies for non-buildable components to be present. We're going
--- to use GHC version as a proxy for Cabal library version in this case for
--- simplicity, so we'll check for GHC being 7.10 or earlier. This obviously
--- makes our function a lot more fun to write...
 packageDependencies ::
-     PackageConfig
-  -> PackageDescription
+     PackageDescription
   -> Map PackageName VersionRange
-packageDependencies pkgConfig pkg' =
+packageDependencies pkg =
   M.fromListWith intersectVersionRanges $
-  map (depPkgName &&& depVerRange) $
-  concatMap targetBuildDepends (allBuildInfo' pkg) ++
-  maybe [] setupDepends (setupBuildInfo pkg)
- where
-  pkg
-    | getGhcVersion (packageConfigCompilerVersion pkgConfig) >= mkVersion [8, 0] = pkg'
-    -- Set all components to buildable. Only need to worry  library, exe, test,
-    -- and bench, since others didn't exist in older Cabal versions
-    | otherwise = pkg'
-      { library =
-          (\c -> c { libBuildInfo = go (libBuildInfo c) }) <$> library pkg'
-      , executables =
-          (\c -> c { buildInfo = go (buildInfo c) }) <$> executables pkg'
-      , testSuites =
-          if packageConfigEnableTests pkgConfig
-            then (\c -> c { testBuildInfo = go (testBuildInfo c) }) <$>
-                   testSuites pkg'
-            else testSuites pkg'
-      , benchmarks =
-          if packageConfigEnableBenchmarks pkgConfig
-            then (\c -> c { benchmarkBuildInfo = go (benchmarkBuildInfo c) }) <$>
-                   benchmarks pkg'
-            else benchmarks pkg'
-      }
-
-  go bi = bi { buildable = True }
-
--- | Get all dependencies of the package (buildable targets only).
---
--- This uses both the new 'buildToolDepends' and old 'buildTools' information.
-packageDescTools ::
-     PackageDescription
-  -> (Set ExeName, Map PackageName DepValue)
-packageDescTools pd =
-  (S.fromList $ concat unknowns, M.fromListWith (<>) $ concat knowns)
- where
-  (unknowns, knowns) = unzip $ map perBI $ allBuildInfo' pd
-
-  perBI :: BuildInfo -> ([ExeName], [(PackageName, DepValue)])
-  perBI bi =
-    (unknownTools, tools)
-   where
-    (unknownTools, knownTools) = partitionEithers $ map go1 (buildTools bi)
-
-    tools = mapMaybe go2 (knownTools ++ buildToolDepends bi)
-
-    -- This is similar to desugarBuildTool from Cabal, however it
-    -- uses our own hard-coded map which drops tools shipped with
-    -- GHC (like hsc2hs), and includes some tools from Stackage.
-    go1 :: Cabal.LegacyExeDependency -> Either ExeName Cabal.ExeDependency
-    go1 (Cabal.LegacyExeDependency name range) =
-      case M.lookup name hardCodedMap of
-        Just pkgName ->
-          Right $
-            Cabal.ExeDependency pkgName (Cabal.mkUnqualComponentName name) range
-        Nothing -> Left $ ExeName $ T.pack name
-
-    go2 :: Cabal.ExeDependency -> Maybe (PackageName, DepValue)
-    go2 (Cabal.ExeDependency pkg _name range)
-      | pkg `S.member` preInstalledPackages = Nothing
-      | otherwise = Just
-          ( pkg
-          , DepValue
-              { dvVersionRange = range
-              , dvType = AsBuildTool
-              }
-          )
-
--- | A hard-coded map for tool dependencies
-hardCodedMap :: Map String PackageName
-hardCodedMap = M.fromList
-  [ ("alex", Distribution.Package.mkPackageName "alex")
-  , ("happy", Distribution.Package.mkPackageName "happy")
-  , ("cpphs", Distribution.Package.mkPackageName "cpphs")
-  , ("greencard", Distribution.Package.mkPackageName "greencard")
-  , ("c2hs", Distribution.Package.mkPackageName "c2hs")
-  , ("hscolour", Distribution.Package.mkPackageName "hscolour")
-  , ("hspec-discover", Distribution.Package.mkPackageName "hspec-discover")
-  , ("hsx2hs", Distribution.Package.mkPackageName "hsx2hs")
-  , ("gtk2hsC2hs", Distribution.Package.mkPackageName "gtk2hs-buildtools")
-  , ("gtk2hsHookGenerator", Distribution.Package.mkPackageName "gtk2hs-buildtools")
-  , ("gtk2hsTypeGen", Distribution.Package.mkPackageName "gtk2hs-buildtools")
-  ]
-
--- | Executable-only packages which come pre-installed with GHC and do not need
--- to be built. Without this exception, we would either end up unnecessarily
--- rebuilding these packages, or failing because the packages do not appear in
--- the Stackage snapshot.
-preInstalledPackages :: Set PackageName
-preInstalledPackages = S.fromList
-  [ mkPackageName "hsc2hs"
-  , mkPackageName "haddock"
-  ]
+    map (depPkgName &&& depVerRange) $
+         concatMap targetBuildDepends (allBuildInfo' pkg)
+      <> maybe [] setupDepends (setupBuildInfo pkg)
 
 -- | Variant of 'allBuildInfo' from Cabal that, like versions before Cabal 2.2
 -- only includes buildable components.
@@ -586,68 +435,49 @@                                , let bi = benchmarkBuildInfo tst
                                , buildable bi ]
 
--- | A pair of package descriptions: one which modified the buildable values of
--- test suites and benchmarks depending on whether they are enabled, and one
--- which does not.
---
--- Fields are intentionally lazy, we may only need one or the other value.
---
--- Michael S Snoyman 2017-08-29: The very presence of this data type is terribly
--- ugly, it represents the fact that the Cabal 2.0 upgrade did _not_ go well.
--- Specifically, we used to have a field to indicate whether a component was
--- enabled in addition to buildable, but that's gone now, and this is an ugly
--- proxy. We should at some point clean up the mess of Package, LocalPackage,
--- etc, and probably pull in the definition of PackageDescription from Cabal
--- with our additionally needed metadata. But this is a good enough hack for the
--- moment. Odds are, you're reading this in the year 2024 and thinking "wtf?"
-data PackageDescriptionPair = PackageDescriptionPair
-  { pdpOrigBuildable :: PackageDescription
-  , pdpModifiedBuildable :: PackageDescription
-  }
-
 -- | Evaluates the conditions of a 'GenericPackageDescription', yielding
 -- a resolved 'PackageDescription'.
 resolvePackageDescription ::
      PackageConfig
   -> GenericPackageDescription
-  -> PackageDescriptionPair
+  -> PackageDescription
 resolvePackageDescription
-  packageConfig
-  ( GenericPackageDescription
-      desc _ defaultFlags mlib subLibs foreignLibs' exes tests benches
-  )
-  =
-  PackageDescriptionPair
-    { pdpOrigBuildable = go False
-    , pdpModifiedBuildable = go True
-    }
+    packageConfig
+    ( GenericPackageDescription
+        desc _ defaultFlags mlib subLibs foreignLibs' exes tests benches
+    )
+  = desc
+      { library = fmap (resolveConditions rc updateLibDeps) mlib
+      , subLibraries = map
+          ( \(n, v) ->
+              (resolveConditions rc updateLibDeps v){libName = LSubLibName n}
+          )
+          subLibs
+      , foreignLibs = map
+          ( \(n, v) ->
+              (resolveConditions rc updateForeignLibDeps v){foreignLibName = n}
+          )
+          foreignLibs'
+      , executables = map
+          ( \(n, v) -> (resolveConditions rc updateExeDeps v){exeName = n} )
+          exes
+      , testSuites = map
+          ( \(n, v) ->
+              (resolveConditions rc updateTestDeps v){testName = n}
+          )
+          tests
+      , benchmarks = map
+          ( \(n, v) ->
+              (resolveConditions rc updateBenchmarkDeps v){benchmarkName = n}
+          )
+          benches
+      }
  where
-  go modBuildable = desc
-    { library = fmap (resolveConditions rc updateLibDeps) mlib
-    , subLibraries = map
-        (\(n, v) -> (resolveConditions rc updateLibDeps v){libName=LSubLibName n})
-        subLibs
-    , foreignLibs = map
-        (\(n, v) -> (resolveConditions rc updateForeignLibDeps v){foreignLibName=n})
-        foreignLibs'
-    , executables = map
-        (\(n, v) -> (resolveConditions rc updateExeDeps v){exeName=n})
-        exes
-    , testSuites = map
-        (\(n, v) -> (resolveConditions rc (updateTestDeps modBuildable) v){testName=n})
-        tests
-    , benchmarks = map
-        (\(n, v) -> (resolveConditions rc (updateBenchmarkDeps modBuildable) v){benchmarkName=n})
-        benches
-    }
-
-  flags = M.union (packageConfigFlags packageConfig) (flagMap defaultFlags)
-
+  flags = M.union packageConfig.flags (flagMap defaultFlags)
   rc = mkResolveConditions
-         (packageConfigCompilerVersion packageConfig)
-         (packageConfigPlatform packageConfig)
+         packageConfig.compilerVersion
+         packageConfig.platform
          flags
-
   updateLibDeps lib deps = lib
     { libBuildInfo = (libBuildInfo lib) {targetBuildDepends = deps} }
   updateForeignLibDeps lib deps = lib
@@ -655,39 +485,13 @@         (foreignLibBuildInfo lib) {targetBuildDepends = deps}
     }
   updateExeDeps exe deps = exe
-    { buildInfo = (buildInfo exe) {targetBuildDepends = deps} }
-
-  -- Note that, prior to moving to Cabal 2.0, we would set testEnabled or
-  -- benchmarkEnabled here. These fields no longer exist, so we modify buildable
-  -- instead here. The only wrinkle in the Cabal 2.0 story is
-  -- https://github.com/haskell/cabal/issues/1725, where older versions of Cabal
-  -- (which may be used for actually building code) don't properly exclude
-  -- build-depends for non-buildable components. Testing indicates that
-  -- everything is working fine, and that this comment can be completely
-  -- ignored. I'm leaving the comment anyway in case something breaks and you,
-  -- poor reader, are investigating.
-  updateTestDeps modBuildable test deps =
-    let bi = testBuildInfo test
-        bi' = bi
-          { targetBuildDepends = deps
-          , buildable =
-                 buildable bi
-              && (  not modBuildable
-                 || packageConfigEnableTests packageConfig
-                 )
-          }
-    in  test { testBuildInfo = bi' }
-  updateBenchmarkDeps modBuildable benchmark deps =
-    let bi = benchmarkBuildInfo benchmark
-        bi' = bi
-          { targetBuildDepends = deps
-          , buildable =
-                 buildable bi
-              && (  not modBuildable
-                 || packageConfigEnableBenchmarks packageConfig
-                 )
-          }
-    in  benchmark { benchmarkBuildInfo = bi' }
+    { Executable.buildInfo = (buildInfo exe) {targetBuildDepends = deps} }
+  updateTestDeps test deps = test
+    { testBuildInfo = (testBuildInfo test) {targetBuildDepends = deps} }
+  updateBenchmarkDeps bench deps = bench
+    { benchmarkBuildInfo =
+        (benchmarkBuildInfo bench) {targetBuildDepends = deps}
+    }
 
 -- | Make a map from a list of flag specifications.
 --
@@ -699,10 +503,10 @@   pair = flagName &&& flagDefault
 
 data ResolveConditions = ResolveConditions
-  { rcFlags :: Map FlagName Bool
-  , rcCompilerVersion :: ActualCompiler
-  , rcOS :: OS
-  , rcArch :: Arch
+  { flags :: Map FlagName Bool
+  , compilerVersion :: ActualCompiler
+  , os :: OS
+  , arch :: Arch
   }
 
 -- | Generic a @ResolveConditions@ using sensible defaults.
@@ -712,10 +516,10 @@   -> Map FlagName Bool -- ^ enabled flags
   -> ResolveConditions
 mkResolveConditions compilerVersion (Platform arch os) flags = ResolveConditions
-  { rcFlags = flags
-  , rcCompilerVersion = compilerVersion
-  , rcOS = os
-  , rcArch = arch
+  { flags
+  , compilerVersion
+  , os
+  , arch
   }
 
 -- | Resolve the condition tree for the library.
@@ -743,13 +547,13 @@         CAnd cx cy -> condSatisfied cx && condSatisfied cy
     varSatisfied v =
       case v of
-        OS os -> os == rcOS rc
-        Arch arch -> arch == rcArch rc
-        PackageFlag flag -> fromMaybe False $ M.lookup flag (rcFlags rc)
+        OS os -> os == rc.os
+        Arch arch -> arch == rc.arch
+        PackageFlag flag -> fromMaybe False $ M.lookup flag rc.flags
         -- NOTE:  ^^^^^ This should never happen, as all flags which are used
         -- must be declared. Defaulting to False.
         Impl flavor range ->
-          case (flavor, rcCompilerVersion rc) of
+          case (flavor, rc.compilerVersion) of
             (GHC, ACGhc vghc) -> vghc `withinRange` range
             _ -> False
 
@@ -815,20 +619,236 @@ applyForceCustomBuild cabalVersion package
   | forceCustomBuild =
       package
-        { packageBuildType = Custom
-        , packageDeps =
-            M.insertWith (<>) "Cabal" (DepValue cabalVersionRange AsLibrary) $
-              packageDeps package
-        , packageSetupDeps = Just $ M.fromList
-            [ ("Cabal", cabalVersionRange)
-            , ("base", anyVersion)
+        { buildType = Custom
+        , setupDeps = Just $ M.fromList
+            [ ("Cabal", libraryDepFromVersionRange cabalVersionRange)
+            , ("base", libraryDepFromVersionRange anyVersion)
             ]
         }
   | otherwise = package
  where
   cabalVersionRange =
-    orLaterVersion $ mkVersion $ cabalSpecToVersionDigits $
-      packageCabalSpec package
-  forceCustomBuild =
-       packageBuildType package == Simple
+    orLaterVersion $ mkVersion $ cabalSpecToVersionDigits
+      package.cabalSpec
+  forceCustomBuild = package.buildType == Simple
     && not (cabalVersion `withinRange` cabalVersionRange)
+
+-- | Check if the package has a main library that is buildable.
+hasBuildableMainLibrary :: Package -> Bool
+hasBuildableMainLibrary package =
+  maybe False isComponentBuildable package.library
+
+-- | Check if the main library has any exposed modules.
+--
+-- This should become irrelevant at some point since there's nothing inherently
+-- wrong or different with packages exposing only modules in internal libraries
+-- (for instance).
+mainLibraryHasExposedModules :: Package -> Bool
+mainLibraryHasExposedModules package =
+  maybe False (not . null . (.exposedModules)) package.library
+
+-- | Aggregate all unknown tools from all components. Mostly meant for
+-- build tools specified in the legacy manner (build-tools:) that failed the
+-- hard-coded lookup. See 'Stack.Types.Component.unknownTools' for more
+-- information.
+packageUnknownTools :: Package -> Set Text
+packageUnknownTools pkg = lib (bench <> tests <> flib <> sublib <> exe)
+ where
+  lib setT = case pkg.library of
+    Just libV -> addUnknownTools libV setT
+    Nothing -> setT
+  bench = gatherUnknownTools pkg.benchmarks
+  tests = gatherUnknownTools pkg.testSuites
+  flib = gatherUnknownTools pkg.foreignLibraries
+  sublib = gatherUnknownTools pkg.subLibraries
+  exe = gatherUnknownTools pkg.executables
+  addUnknownTools :: HasBuildInfo x => x -> Set Text -> Set Text
+  addUnknownTools = (<>) . (.buildInfo.unknownTools)
+  gatherUnknownTools :: HasBuildInfo x => CompCollection x -> Set Text
+  gatherUnknownTools = foldr' addUnknownTools mempty
+
+buildableForeignLibs :: Package -> Set Text
+buildableForeignLibs pkg = getBuildableSetText pkg.foreignLibraries
+
+buildableSubLibs :: Package -> Set Text
+buildableSubLibs pkg = getBuildableSetText pkg.subLibraries
+
+buildableExes :: Package -> Set Text
+buildableExes pkg = getBuildableSetText pkg.executables
+
+buildableTestSuites :: Package -> Set Text
+buildableTestSuites pkg = getBuildableSetText pkg.testSuites
+
+buildableBenchmarks :: Package -> Set Text
+buildableBenchmarks pkg = getBuildableSetText pkg.benchmarks
+
+-- | Apply a generic processing function in a Monad over all of the Package's
+-- components.
+processPackageComponent ::
+     forall m a. (Monad m)
+  => Package
+  -> (forall component. HasComponentInfo component => component -> m a -> m a)
+     -- ^ Processing function with all the component's info.
+  -> m a
+     -- ^ Initial value.
+  -> m a
+processPackageComponent pkg componentFn = do
+  let componentKindProcessor ::
+           forall component. HasComponentInfo component
+        => (Package -> CompCollection component)
+        -> m a
+        -> m a
+      componentKindProcessor target =
+        foldComponentToAnotherCollection
+          (target pkg)
+          componentFn
+      processMainLib = maybe id componentFn pkg.library
+      processAllComp =
+        ( if pkg.benchmarkEnabled
+            then componentKindProcessor (.benchmarks)
+            else id
+        )
+        . ( if pkg.testEnabled
+              then componentKindProcessor (.testSuites)
+              else id
+          )
+        . componentKindProcessor (.foreignLibraries)
+        . componentKindProcessor (.executables)
+        . componentKindProcessor (.subLibraries)
+        . processMainLib
+  processAllComp
+
+-- | This is a function to iterate in a monad over all of a package's
+-- dependencies, and yield a collection of results (used with list and set).
+processPackageMapDeps ::
+     (Monad m)
+  => Package
+  -> (Map PackageName DepValue -> m a -> m a)
+  -> m a
+  -> m a
+processPackageMapDeps pkg fn = do
+  let packageSetupDepsProcessor resAction = case pkg.setupDeps of
+        Nothing -> resAction
+        Just v -> fn v resAction
+      processAllComp = processPackageComponent pkg (fn . componentDependencyMap)
+        . packageSetupDepsProcessor
+  processAllComp
+
+-- | This is a function to iterate in a monad over all of a package component's
+-- dependencies, and yield a collection of results.
+processPackageDeps ::
+     (Monad m, Monoid (targetedCollection resT))
+  => Package
+  -> (resT -> targetedCollection resT -> targetedCollection resT)
+  -> (PackageName -> DepValue -> m resT)
+  -> m (targetedCollection resT)
+  -> m (targetedCollection resT)
+processPackageDeps pkg combineResults fn = do
+  let asPackageNameSet accessor =
+        S.map (mkPackageName . T.unpack) $ getBuildableSetText $ accessor pkg
+      (!subLibNames, !foreignLibNames) =
+        ( asPackageNameSet (.subLibraries)
+        , asPackageNameSet (.foreignLibraries)
+        )
+      shouldIgnoreDep (packageNameV :: PackageName)
+        | packageNameV == pkg.name = True
+        | packageNameV `S.member` subLibNames = True
+        | packageNameV `S.member` foreignLibNames = True
+        | otherwise = False
+      innerIterator packageName depValue resListInMonad
+        | shouldIgnoreDep packageName = resListInMonad
+        | otherwise = do
+            resList <- resListInMonad
+            newResElement <- fn packageName depValue
+            pure $ combineResults newResElement resList
+  processPackageMapDeps pkg (flip (M.foldrWithKey' innerIterator))
+
+-- | Iterate/fold on all the package dependencies, components, setup deps and
+-- all.
+processPackageDepsToList ::
+     Monad m
+  => Package
+  -> (PackageName -> DepValue -> m resT)
+  -> m [resT]
+processPackageDepsToList pkg fn = processPackageDeps pkg (:) fn (pure [])
+
+-- | List all package's dependencies in a "free" context through the identity
+-- monad.
+listOfPackageDeps :: Package -> [PackageName]
+listOfPackageDeps pkg =
+  runIdentity $ processPackageDepsToList pkg (\pn _ -> pure pn)
+
+-- | The set of package's dependencies.
+setOfPackageDeps :: Package -> Set PackageName
+setOfPackageDeps pkg =
+  runIdentity $ processPackageDeps pkg S.insert (\pn _ -> pure pn) (pure mempty)
+
+-- | This implements a topological sort on all targeted components for the build
+-- and their dependencies. It's only targeting internal dependencies, so it's doing
+-- a topological sort on a subset of a package's components.
+--
+-- Note that in Cabal they use the Data.Graph struct to pursue the same goal. But dong this here
+-- would require a large number intermediate data structure.
+-- This is needed because we need to get the right GhcPkgId of the relevant internal dependencies
+-- of a component before building it as a component.
+topSortPackageComponent ::
+     Package
+  -> Target
+  -> Bool
+   -- ^ Include directTarget or not. False here means we won't
+   -- include the actual targets in the result, only their deps.
+   -- Using it with False here only in GHCi
+  -> Seq NamedComponent
+topSortPackageComponent package target includeDirectTarget = runST $ do
+  alreadyProcessedRef <- newSTRef (mempty :: Set NamedComponent)
+  let processInitialComponents c = case target of
+        TargetAll{} -> processComponent includeDirectTarget alreadyProcessedRef c
+        TargetComps targetSet -> if S.member c.qualifiedName targetSet
+          then processComponent includeDirectTarget alreadyProcessedRef c
+          else id
+  processPackageComponent package processInitialComponents (pure mempty)
+  where
+    processComponent :: forall s component. HasComponentInfo component
+      => Bool
+       -- ^ Finally add this component in the seq
+      -> STRef s (Set NamedComponent)
+      -> component
+      -> ST s (Seq NamedComponent)
+      -> ST s (Seq NamedComponent)
+    processComponent finallyAddComponent alreadyProcessedRef component res = do
+      let depMap = componentDependencyMap component
+          internalDep = M.lookup package.name depMap
+          processSubDep = processOneDep alreadyProcessedRef internalDep res
+          qualName = component.qualifiedName
+          processSubDepSaveName
+            | finallyAddComponent = (|> qualName) <$> processSubDep
+            | otherwise = processSubDep
+      -- This is an optimization, the only components we are likely to process
+      -- multiple times are the ones we can find in dependencies, otherwise we
+      -- only fold on a single version of each component by design.
+      if isPotentialDependency qualName
+        then do
+          alreadyProcessed <- readSTRef alreadyProcessedRef
+          if S.member qualName alreadyProcessed
+            then res
+            else modifySTRef' alreadyProcessedRef (S.insert qualName)
+                   >> processSubDepSaveName
+        else processSubDepSaveName
+    lookupLibName isMain name = if isMain
+      then package.library
+      else collectionLookup name package.subLibraries
+    processOneDep alreadyProcessed mDependency res =
+      case (.depType) <$> mDependency of
+        Just (AsLibrary (DepLibrary mainLibDep subLibDeps)) -> do
+          let processMainLibDep =
+                case (mainLibDep, lookupLibName True mempty) of
+                  (True, Just mainLib) ->
+                    processComponent True alreadyProcessed mainLib
+                  _ -> id
+              processSingleSubLib name =
+                case lookupLibName False name.unqualCompToText of
+                  Just lib -> processComponent True alreadyProcessed lib
+                  Nothing -> id
+              processSubLibDep r = foldr' processSingleSubLib r subLibDeps
+          processSubLibDep (processMainLibDep res)
+        _ -> res
src/Stack/PackageDump.hs view
@@ -1,5 +1,6 @@-{-# LANGUAGE NoImplicitPrelude  #-}
-{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 module Stack.PackageDump
   ( Line
@@ -21,13 +22,17 @@ import qualified Data.Map as Map
 import qualified Data.Set as Set
 import qualified Distribution.Text as C
+import           Distribution.Types.MungedPackageName
+                   ( decodeCompatPackageName )
 import           Path.Extra ( toFilePathNoTrailingSep )
 import           RIO.Process ( HasProcessContext )
 import qualified RIO.Text as T
+import           Stack.Component ( fromCabalName )
 import           Stack.GhcPkg ( createDatabase )
 import           Stack.Prelude
 import           Stack.Types.CompilerPaths ( GhcPkgExe (..), HasCompiler (..) )
-import           Stack.Types.DumpPackage ( DumpPackage (..) )
+import           Stack.Types.Component ( StackUnqualCompName(..) )
+import           Stack.Types.DumpPackage ( DumpPackage (..), SublibDump (..) )
 import           Stack.Types.GhcPkgId ( GhcPkgId, parseGhcPkgId )
 
 -- | Type representing exceptions thrown by functions exported by the
@@ -55,35 +60,48 @@     , "."
     ]
 
--- | Call ghc-pkg dump with appropriate flags and stream to the given @Sink@,
--- for a single database
+-- | Call @ghc-pkg dump@ with appropriate flags and stream to the given sink,
+-- using either the global package database or the given package databases.
 ghcPkgDump ::
      (HasProcessContext env, HasTerm env)
   => GhcPkgExe
-  -> [Path Abs Dir] -- ^ if empty, use global
+  -> [Path Abs Dir]
+     -- ^ A list of package databases. If empty, use the global package
+     -- database.
   -> ConduitM Text Void (RIO env) a
+     -- ^ Sink.
   -> RIO env a
 ghcPkgDump pkgexe = ghcPkgCmdArgs pkgexe ["dump"]
 
--- | Call ghc-pkg describe with appropriate flags and stream to the given
--- @Sink@, for a single database
+-- | Call @ghc-pkg describe@ with appropriate flags and stream to the given
+-- sink, using either the global package database or the given package
+-- databases.
 ghcPkgDescribe ::
      (HasCompiler env, HasProcessContext env, HasTerm env)
   => GhcPkgExe
   -> PackageName
-  -> [Path Abs Dir] -- ^ if empty, use global
+  -> [Path Abs Dir]
+     -- ^ A list of package databases. If empty, use the global package
+     -- database.
   -> ConduitM Text Void (RIO env) a
+     -- ^ Sink.
   -> RIO env a
 ghcPkgDescribe pkgexe pkgName' = ghcPkgCmdArgs
-  pkgexe ["describe", "--simple-output", packageNameString pkgName']
+  pkgexe
+  ["describe", "--simple-output", packageNameString pkgName']
 
--- | Call ghc-pkg and stream to the given @Sink@, for a single database
+-- | Call @ghc-pkg@ and stream to the given sink, using the either the global
+-- package database or the given package databases.
 ghcPkgCmdArgs ::
      (HasProcessContext env, HasTerm env)
   => GhcPkgExe
   -> [String]
-  -> [Path Abs Dir] -- ^ if empty, use global
+     -- ^ A list of commands.
+  -> [Path Abs Dir]
+     -- ^ A list of package databases. If empty, use the global package
+     -- database.
   -> ConduitM Text Void (RIO env) a
+     -- ^ Sink.
   -> RIO env a
 ghcPkgCmdArgs pkgexe@(GhcPkgExe pkgPath) cmd mpkgDbs sink = do
   case reverse mpkgDbs of
@@ -95,8 +113,11 @@   args = concat
     [ case mpkgDbs of
           [] -> ["--global", "--no-user-package-db"]
-          _ -> ["--user", "--no-user-package-db"] ++
-              concatMap (\pkgDb -> ["--package-db", toFilePathNoTrailingSep pkgDb]) mpkgDbs
+          _ ->   "--user"
+               : "--no-user-package-db"
+               : concatMap
+                   (\pkgDb -> ["--package-db", toFilePathNoTrailingSep pkgDb])
+                   mpkgDbs
     , cmd
     , ["--expand-pkgroot"]
     ]
@@ -149,14 +170,14 @@              -> ConduitM DumpPackage o m (Map PackageName DumpPackage)
 sinkMatching allowed =
     Map.fromList
-  . map (pkgName . dpPackageIdent &&& id)
+  . map (pkgName . (.packageIdent) &&& id)
   . Map.elems
   . pruneDeps
       id
-      dpGhcPkgId
-      dpDepends
+      (.ghcPkgId)
+      (.depends)
       const -- Could consider a better comparison in the future
-  <$> (CL.filter (isAllowed . dpPackageIdent) .| CL.consume)
+  <$> (CL.filter (isAllowed . (.packageIdent)) .| CL.consume)
  where
   isAllowed (PackageIdentifier name version) =
     case Map.lookup name allowed of
@@ -206,43 +227,46 @@               _ -> Nothing
       depends <- mapMaybeM parseDepend $ concatMap T.words $ parseM "depends"
 
-      -- Handle sublibs by recording the name of the parent library
-      -- If name of parent library is missing, this is not a sublib.
-      let mkParentLib n = PackageIdentifier n version
-          parentLib = mkParentLib <$> (parseS "package-name" >>=
-                                       parsePackageNameThrowing . T.unpack)
-
-      let parseQuoted key =
+      -- Handle sub-libraries by recording the name of the parent library
+      -- If name of parent library is missing, this is not a sub-library.
+      let maybePackageName :: Maybe PackageName =
+            parseS "package-name" >>= parsePackageNameThrowing . T.unpack
+          maybeLibName = parseS "lib-name"
+          getLibNameFromLegacyName = case decodeCompatPackageName name of
+            MungedPackageName _parentPackageName (LSubLibName libName') ->
+              fromCabalName libName'
+            MungedPackageName _parentPackageName _ -> ""
+          libName =
+            maybe getLibNameFromLegacyName StackUnqualCompName maybeLibName
+          sublib = flip SublibDump libName <$> maybePackageName
+          parseQuoted key =
             case mapM (P.parseOnly (argsParser NoEscaping)) val of
               Left{} -> throwM (Couldn'tParseField key val)
               Right dirs -> pure (concat dirs)
            where
             val = parseM key
-      libDirPaths <- parseQuoted libDirKey
+      libDirs <- parseQuoted libDirKey
       haddockInterfaces <- parseQuoted "haddock-interfaces"
-      haddockHtml <- parseQuoted "haddock-html"
-
+      haddockHtml <- listToMaybe <$> parseQuoted "haddock-html"
       pure $ Just DumpPackage
-        { dpGhcPkgId = ghcPkgId
-        , dpPackageIdent = PackageIdentifier name version
-        , dpParentLibIdent = parentLib
-        , dpLicense = license
-        , dpLibDirs = libDirPaths
-        , dpLibraries = T.words $ T.unwords libraries
-        , dpHasExposedModules = not (null libraries || null exposedModules)
-
-        -- Strip trailing commas from ghc package exposed-modules (looks buggy to me...).
-        -- Then try to parse the module names.
-        , dpExposedModules =
+        { ghcPkgId
+        , packageIdent = PackageIdentifier name version
+        , sublib
+        , license
+        , libDirs
+        , libraries = T.words $ T.unwords libraries
+        , hasExposedModules = not (null libraries || null exposedModules)
+        -- Strip trailing commas from ghc package exposed-modules (looks buggy
+        -- to me...). Then try to parse the module names.
+        , exposedModules =
               Set.fromList
             $ mapMaybe (C.simpleParse . T.unpack . T.dropSuffix ",")
             $ T.words
             $ T.unwords exposedModules
-
-        , dpDepends = depends
-        , dpHaddockInterfaces = haddockInterfaces
-        , dpHaddockHtml = listToMaybe haddockHtml
-        , dpIsExposed = exposed == ["True"]
+        , depends
+        , haddockInterfaces
+        , haddockHtml
+        , isExposed = exposed == ["True"]
         }
 
 stripPrefixText :: Text -> Text -> Maybe Text
src/Stack/PackageFile.hs view
@@ -1,31 +1,25 @@ {-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 {-# LANGUAGE OverloadedStrings   #-}
 
 -- | A module which exports all package-level file-gathering logic.
 module Stack.PackageFile
   ( getPackageFile
-  , packageDescModulesAndFiles
+  , stackPackageFileFromCabal
   ) where
 
 import qualified Data.Map.Strict as M
 import qualified Data.Set as S
-import qualified Data.Text as T
 import           Distribution.CabalSpecVersion ( CabalSpecVersion )
-import           Distribution.ModuleName ( ModuleName )
-import           Distribution.PackageDescription
-                   ( BuildType (..), PackageDescription, benchmarkName
-                   , benchmarks, buildType, dataDir, dataFiles, exeName
-                   , executables, extraSrcFiles, libName, library
-                   , libraryNameString, specVersion, subLibraries, testName
-                   , testSuites )
+import qualified Distribution.PackageDescription as Cabal
 import           Distribution.Simple.Glob ( matchDirFileGlob )
-import qualified Distribution.Types.UnqualComponentName as Cabal
 import           Path ( parent, (</>) )
 import           Path.Extra ( forgivingResolveFile, rejectMissingFile )
 import           Path.IO ( doesFileExist )
 import           Stack.ComponentFile
-                   ( benchmarkFiles, executableFiles, libraryFiles
-                   , resolveOrWarn, testFiles
+                   ( ComponentFile (..), resolveOrWarn, stackBenchmarkFiles
+                   , stackExecutableFiles, stackLibraryFiles
+                   , stackTestSuiteFiles
                    )
 import           Stack.Constants
                    ( relFileHpackPackageConfig, relFileSetupHs, relFileSetupLhs
@@ -34,11 +28,12 @@ import           Stack.Prelude
 import           Stack.Types.BuildConfig ( HasBuildConfig (..) )
 import           Stack.Types.CompilerPaths ( cabalVersionL )
-import           Stack.Types.EnvConfig ( HasEnvConfig )
+import           Stack.Types.EnvConfig ( HasEnvConfig (..) )
 import           Stack.Types.NamedComponent ( NamedComponent (..) )
+import           Stack.Types.Package ( Package(..) )
 import           Stack.Types.PackageFile
-                   ( DotCabalPath (..), GetPackageFileContext (..)
-                   , PackageWarning (..)
+                   ( GetPackageFileContext (..), PackageComponentFile (..)
+                   , StackPackageFile (..)
                    )
 import qualified System.FilePath as FilePath
 import           System.IO.Error ( isUserError )
@@ -53,70 +48,34 @@ 
 -- | Get all files referenced by the package.
 packageDescModulesAndFiles ::
-     PackageDescription
+     Package
   -> RIO
        GetPackageFileContext
-       ( Map NamedComponent (Map ModuleName (Path Abs File))
-       , Map NamedComponent [DotCabalPath]
-       , Set (Path Abs File)
-       , [PackageWarning]
-       )
+       PackageComponentFile
 packageDescModulesAndFiles pkg = do
-  (libraryMods, libDotCabalFiles, libWarnings) <-
-    maybe
-      (pure (M.empty, M.empty, []))
-      (asModuleAndFileMap libComponent libraryFiles)
-      (library pkg)
-  (subLibrariesMods, subLibDotCabalFiles, subLibWarnings) <-
-    fmap
-      foldTuples
-      ( mapM
-          (asModuleAndFileMap internalLibComponent libraryFiles)
-          (subLibraries pkg)
-      )
-  (executableMods, exeDotCabalFiles, exeWarnings) <-
-    fmap
-      foldTuples
-      ( mapM
-          (asModuleAndFileMap exeComponent executableFiles)
-          (executables pkg)
-      )
-  (testMods, testDotCabalFiles, testWarnings) <-
-    fmap
-      foldTuples
-      (mapM (asModuleAndFileMap testComponent testFiles) (testSuites pkg))
-  (benchModules, benchDotCabalPaths, benchWarnings) <-
-    fmap
-      foldTuples
-      ( mapM
-          (asModuleAndFileMap benchComponent benchmarkFiles)
-          (benchmarks pkg)
-      )
-  dfiles <- resolveGlobFiles
-              (specVersion pkg)
-              ( extraSrcFiles pkg
-                ++ map (dataDir pkg FilePath.</>) (dataFiles pkg)
-              )
-  let modules = libraryMods <> subLibrariesMods <> executableMods <> testMods <>
-                  benchModules
-      files = libDotCabalFiles <> subLibDotCabalFiles <> exeDotCabalFiles <>
-                testDotCabalFiles <> benchDotCabalPaths
-      warnings = libWarnings <> subLibWarnings <> exeWarnings <> testWarnings <>
-                   benchWarnings
-  pure (modules, files, dfiles, warnings)
- where
-  libComponent = const CLib
-  internalLibComponent =
-    CInternalLib . T.pack . maybe
-      "" Cabal.unUnqualComponentName . libraryNameString . libName
-  exeComponent = CExe . T.pack . Cabal.unUnqualComponentName . exeName
-  testComponent = CTest . T.pack . Cabal.unUnqualComponentName . testName
-  benchComponent = CBench . T.pack . Cabal.unUnqualComponentName . benchmarkName
-  asModuleAndFileMap label f lib = do
-    (a, b, c) <- f (label lib) lib
-    pure (M.singleton (label lib) a, M.singleton (label lib) b, c)
-  foldTuples = foldl' (<>) (M.empty, M.empty, [])
+  packageExtraFile <- resolveGlobFilesFromStackPackageFile
+              pkg.cabalSpec pkg.file
+  let initialValue = mempty{packageExtraFile=packageExtraFile}
+  let accumulator f comp st = (insertComponentFile <$> st) <*> f comp
+  let gatherCompFileCollection createCompFileFn getCompFn res =
+        foldr' (accumulator createCompFileFn) res (getCompFn pkg)
+  gatherCompFileCollection stackLibraryFiles (.library)
+    . gatherCompFileCollection stackLibraryFiles (.subLibraries)
+    . gatherCompFileCollection stackExecutableFiles (.executables)
+    . gatherCompFileCollection stackTestSuiteFiles (.testSuites)
+    . gatherCompFileCollection stackBenchmarkFiles (.benchmarks)
+    $ pure initialValue
 
+resolveGlobFilesFromStackPackageFile ::
+     CabalSpecVersion
+  -> StackPackageFile
+  -> RIO GetPackageFileContext (Set (Path Abs File))
+resolveGlobFilesFromStackPackageFile
+    csvV
+    (StackPackageFile extraSrcFilesV dataDirV dataFilesV)
+  = resolveGlobFiles
+      csvV
+      (extraSrcFilesV ++ map (dataDirV FilePath.</>) dataFilesV)
 
 -- | Resolve globbing of files (e.g. data files) to absolute paths.
 resolveGlobFiles ::
@@ -124,15 +83,14 @@   -> [String]
   -> RIO GetPackageFileContext (Set (Path Abs File))
 resolveGlobFiles cabalFileVersion =
-  fmap (S.fromList . catMaybes . concat) .
-  mapM resolve
+  fmap (S.fromList . concatMap catMaybes) . mapM resolve
  where
   resolve name =
     if '*' `elem` name
       then explode name
       else fmap pure (resolveFileOrWarn name)
   explode name = do
-    dir <- asks (parent . ctxFile)
+    dir <- asks (parent . (.file))
     names <- matchDirFileGlob' (toFilePath dir) name
     mapM resolveFileOrWarn names
   matchDirFileGlob' dir glob =
@@ -156,44 +114,59 @@ -- well as use by "stack ghci"
 getPackageFile ::
      ( HasEnvConfig s, MonadReader s m, MonadThrow m, MonadUnliftIO m )
-  => PackageDescription
+  => Package
   -> Path Abs File
-  -> m ( Map NamedComponent (Map ModuleName (Path Abs File))
-       , Map NamedComponent [DotCabalPath]
-       , Set (Path Abs File)
-       , [PackageWarning]
-       )
-getPackageFile pkg cabalfp =
-  debugBracket ("getPackageFiles" <+> pretty cabalfp) $ do
-    let pkgDir = parent cabalfp
+  -> m PackageComponentFile
+getPackageFile pkg cabalFP =
+  debugBracket ("getPackageFiles" <+> pretty cabalFP) $ do
+    let pkgDir = parent cabalFP
     distDir <- distDirFromDir pkgDir
     bc <- view buildConfigL
     cabalVer <- view cabalVersionL
-    (componentModules, componentFiles, dataFiles', warnings) <-
+    packageComponentFile <-
       runRIO
-        (GetPackageFileContext cabalfp distDir bc cabalVer)
+        (GetPackageFileContext cabalFP distDir bc cabalVer)
         (packageDescModulesAndFiles pkg)
     setupFiles <-
-      if buildType pkg == Custom
-      then do
-        let setupHsPath = pkgDir </> relFileSetupHs
-            setupLhsPath = pkgDir </> relFileSetupLhs
-        setupHsExists <- doesFileExist setupHsPath
-        if setupHsExists
-          then pure (S.singleton setupHsPath)
-          else do
-            setupLhsExists <- doesFileExist setupLhsPath
-            if setupLhsExists
-              then pure (S.singleton setupLhsPath)
-              else pure S.empty
-      else pure S.empty
-    buildFiles <- fmap (S.insert cabalfp . S.union setupFiles) $ do
+      if pkg.buildType == Cabal.Custom
+        then do
+          let setupHsPath = pkgDir </> relFileSetupHs
+              setupLhsPath = pkgDir </> relFileSetupLhs
+          setupHsExists <- doesFileExist setupHsPath
+          if setupHsExists
+            then pure (S.singleton setupHsPath)
+            else do
+              setupLhsExists <- doesFileExist setupLhsPath
+              if setupLhsExists
+                then pure (S.singleton setupLhsPath)
+                else pure S.empty
+        else pure S.empty
+    moreBuildFiles <- fmap (S.insert cabalFP . S.union setupFiles) $ do
       let hpackPath = pkgDir </> relFileHpackPackageConfig
       hpackExists <- doesFileExist hpackPath
       pure $ if hpackExists then S.singleton hpackPath else S.empty
-    pure
-      ( componentModules
-      , componentFiles
-      , buildFiles <> dataFiles'
-      , warnings
-      )
+    pure packageComponentFile
+      { packageExtraFile =
+          moreBuildFiles <> packageComponentFile.packageExtraFile
+      }
+
+stackPackageFileFromCabal :: Cabal.PackageDescription -> StackPackageFile
+stackPackageFileFromCabal cabalPkg =
+  StackPackageFile
+    (Cabal.extraSrcFiles cabalPkg)
+    (Cabal.dataDir cabalPkg)
+    (Cabal.dataFiles cabalPkg)
+
+insertComponentFile ::
+     PackageComponentFile
+  -> (NamedComponent, ComponentFile)
+  -> PackageComponentFile
+insertComponentFile packageCompFile (name, compFile) =
+  PackageComponentFile nCompFile nDotCollec packageExtraFile nWarnings
+ where
+  (ComponentFile moduleFileMap dotCabalFileList warningsCollec) = compFile
+  (PackageComponentFile modules files packageExtraFile warnings) =
+    packageCompFile
+  nCompFile = M.insert name moduleFileMap modules
+  nDotCollec = M.insert name dotCabalFileList files
+  nWarnings = warningsCollec ++ warnings
src/Stack/Path.hs view
@@ -1,6 +1,8 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- | Types and functions related to Stack's @path@ command.
 module Stack.Path
@@ -15,20 +17,21 @@ import           Path ( (</>), parent )
 import           Path.Extra ( toFilePathNoTrailingSep )
 import           RIO.Process ( HasProcessContext (..), exeSearchPathL )
+import           Stack.Config ( determineStackRootAndOwnership )
 import           Stack.Constants
                    ( docDirSuffix, stackGlobalConfigOptionName
                    , stackRootOptionName
                    )
 import           Stack.Constants.Config ( distRelativeDir )
 import           Stack.GhcPkg as GhcPkg
-import           Stack.Prelude
+import           Stack.Prelude hiding ( pi )
 import           Stack.Runners
                    ( ShouldReexec (..), withConfig, withDefaultEnvConfig )
 import           Stack.Types.BuildConfig
                    ( BuildConfig (..), HasBuildConfig (..), projectRootL
                    , stackYamlL
                    )
-import           Stack.Types.BuildOpts ( buildOptsMonoidHaddockL )
+import           Stack.Types.BuildOptsMonoid ( buildOptsMonoidHaddockL )
 import           Stack.Types.CompilerPaths
                    ( CompilerPaths (..), HasCompiler (..), getCompilerPath )
 import           Stack.Types.Config
@@ -36,19 +39,28 @@                    )
 import           Stack.Types.EnvConfig
                    ( EnvConfig, HasEnvConfig (..), bindirCompilerTools
-                   , hoogleRoot, hpcReportDir, installationRootDeps
-                   , installationRootLocal, packageDatabaseDeps
-                   , packageDatabaseExtra, packageDatabaseLocal
+                   , hpcReportDir, installationRootDeps, installationRootLocal
+                   , packageDatabaseDeps, packageDatabaseExtra
+                   , packageDatabaseLocal
                    )
+import qualified Stack.Types.EnvConfig as EnvConfig
 import           Stack.Types.GHCVariant ( HasGHCVariant (..) )
-import           Stack.Types.GlobalOpts ( globalOptsBuildOptsMonoidL )
+import           Stack.Types.GlobalOpts
+                   ( GlobalOpts (..), globalOptsBuildOptsMonoidL )
 import           Stack.Types.Platform ( HasPlatform (..) )
 import           Stack.Types.Runner ( HasRunner (..), Runner, globalOptsL )
 import qualified System.FilePath as FP
 
--- | Print out useful path information in a human-readable format (and
--- support others later).
+-- | Print out useful path information in a human-readable format (and support
+-- others later).
 path :: [Text] -> RIO Runner ()
+-- Distinguish a request for only the Stack root, as such a request does not
+-- require 'withDefaultEnvConfig'.
+path [key] | key == stackRootOptionName' = do
+  clArgs <- view $ globalOptsL . to (.configMonoid)
+  liftIO $ do
+    (_, stackRoot, _) <- determineStackRootAndOwnership clArgs
+    T.putStrLn $ T.pack $ toFilePathNoTrailingSep stackRoot
 path keys = do
   let -- filter the chosen paths in flags (keys), or show all of them if no
       -- specific paths chosen.
@@ -79,83 +91,97 @@     withDefaultEnvConfig action
  where
   modifyConfig = set
-    (globalOptsL.globalOptsBuildOptsMonoidL.buildOptsMonoidHaddockL) (Just x)
+    (globalOptsL . globalOptsBuildOptsMonoidL . buildOptsMonoidHaddockL)
+    (Just x)
 
 fillPathInfo :: HasEnvConfig env => RIO env PathInfo
 fillPathInfo = do
   -- We must use a BuildConfig from an EnvConfig to ensure that it contains the
   -- full environment info including GHC paths etc.
-  piBuildConfig <- view $ envConfigL.buildConfigL
+  buildConfig <- view $ envConfigL . buildConfigL
   -- This is the modified 'bin-path',
   -- including the local GHC or MSYS if not configured to operate on
   -- global GHC.
   -- It was set up in 'withBuildConfigAndLock -> withBuildConfigExt -> setupEnv'.
   -- So it's not the *minimal* override path.
-  piSnapDb <- packageDatabaseDeps
-  piLocalDb <- packageDatabaseLocal
-  piExtraDbs <- packageDatabaseExtra
-  piGlobalDb <- view $ compilerPathsL.to cpGlobalDB
-  piSnapRoot <- installationRootDeps
-  piLocalRoot <- installationRootLocal
-  piToolsDir <- bindirCompilerTools
-  piHoogleRoot <- hoogleRoot
-  piDistDir <- distRelativeDir
-  piHpcDir <- hpcReportDir
-  piCompiler <- getCompilerPath
-  pure PathInfo {..}
+  snapDb <- packageDatabaseDeps
+  localDb <- packageDatabaseLocal
+  extraDbs <- packageDatabaseExtra
+  globalDb <- view $ compilerPathsL . to (.globalDB)
+  snapRoot <- installationRootDeps
+  localRoot <- installationRootLocal
+  toolsDir <- bindirCompilerTools
+  hoogleRoot <- EnvConfig.hoogleRoot
+  distDir <- distRelativeDir
+  hpcDir <- hpcReportDir
+  compiler <- getCompilerPath
+  pure PathInfo
+    { buildConfig
+    , snapDb
+    , localDb
+    , globalDb
+    , snapRoot
+    , localRoot
+    , toolsDir
+    , hoogleRoot
+    , distDir
+    , hpcDir
+    , extraDbs
+    , compiler
+    }
 
 -- | Type representing information passed to all the path printers.
 data PathInfo = PathInfo
-  { piBuildConfig  :: !BuildConfig
-  , piSnapDb       :: !(Path Abs Dir)
-  , piLocalDb      :: !(Path Abs Dir)
-  , piGlobalDb     :: !(Path Abs Dir)
-  , piSnapRoot     :: !(Path Abs Dir)
-  , piLocalRoot    :: !(Path Abs Dir)
-  , piToolsDir     :: !(Path Abs Dir)
-  , piHoogleRoot   :: !(Path Abs Dir)
-  , piDistDir      :: Path Rel Dir
-  , piHpcDir       :: !(Path Abs Dir)
-  , piExtraDbs     :: ![Path Abs Dir]
-  , piCompiler     :: !(Path Abs File)
+  { buildConfig  :: !BuildConfig
+  , snapDb       :: !(Path Abs Dir)
+  , localDb      :: !(Path Abs Dir)
+  , globalDb     :: !(Path Abs Dir)
+  , snapRoot     :: !(Path Abs Dir)
+  , localRoot    :: !(Path Abs Dir)
+  , toolsDir     :: !(Path Abs Dir)
+  , hoogleRoot   :: !(Path Abs Dir)
+  , distDir      :: Path Rel Dir
+  , hpcDir       :: !(Path Abs Dir)
+  , extraDbs     :: ![Path Abs Dir]
+  , compiler     :: !(Path Abs File)
   }
 
 instance HasPlatform PathInfo where
-  platformL = configL.platformL
+  platformL = configL . platformL
   {-# INLINE platformL #-}
-  platformVariantL = configL.platformVariantL
+  platformVariantL = configL . platformVariantL
   {-# INLINE platformVariantL #-}
 
 instance HasLogFunc PathInfo where
-  logFuncL = configL.logFuncL
+  logFuncL = configL . logFuncL
 
 instance HasRunner PathInfo where
-  runnerL = configL.runnerL
+  runnerL = configL . runnerL
 
 instance HasStylesUpdate PathInfo where
-  stylesUpdateL = runnerL.stylesUpdateL
+  stylesUpdateL = runnerL . stylesUpdateL
 
 instance HasTerm PathInfo where
-  useColorL = runnerL.useColorL
-  termWidthL = runnerL.termWidthL
+  useColorL = runnerL . useColorL
+  termWidthL = runnerL . termWidthL
 
 instance HasGHCVariant PathInfo where
-  ghcVariantL = configL.ghcVariantL
+  ghcVariantL = configL . ghcVariantL
   {-# INLINE ghcVariantL #-}
 
 instance HasConfig PathInfo where
-  configL = buildConfigL.lens bcConfig (\x y -> x { bcConfig = y })
+  configL = buildConfigL . lens (.config) (\x y -> x { config = y })
   {-# INLINE configL #-}
 
 instance HasPantryConfig PathInfo where
-  pantryConfigL = configL.pantryConfigL
+  pantryConfigL = configL . pantryConfigL
 
 instance HasProcessContext PathInfo where
-  processContextL = configL.processContextL
+  processContextL = configL . processContextL
 
 instance HasBuildConfig PathInfo where
-  buildConfigL = lens piBuildConfig (\x y -> x { piBuildConfig = y })
-                 . buildConfigL
+  buildConfigL =
+    lens (.buildConfig) (\x y -> x { buildConfig = y }) . buildConfigL
 
 data UseHaddocks a
   = UseHaddocks a
@@ -172,17 +198,19 @@ paths :: [(String, Text, UseHaddocks (PathInfo -> Text))]
 paths =
   [ ( "Global Stack root directory"
-    , T.pack stackRootOptionName
-    , WithoutHaddocks $ view (stackRootL.to toFilePathNoTrailingSep.to T.pack))
+    , stackRootOptionName'
+    , WithoutHaddocks $
+        view (stackRootL . to toFilePathNoTrailingSep . to T.pack))
   , ( "Global Stack configuration file"
     , T.pack stackGlobalConfigOptionName
-    , WithoutHaddocks $ view (stackGlobalConfigL.to toFilePath.to T.pack))
+    , WithoutHaddocks $ view (stackGlobalConfigL . to toFilePath . to T.pack))
   , ( "Project root (derived from stack.yaml file)"
     , "project-root"
-    , WithoutHaddocks $ view (projectRootL.to toFilePathNoTrailingSep.to T.pack))
+    , WithoutHaddocks $
+        view (projectRootL . to toFilePathNoTrailingSep . to T.pack))
   , ( "Configuration location (where the stack.yaml file is)"
     , "config-location"
-    , WithoutHaddocks $ view (stackYamlL.to toFilePath.to T.pack))
+    , WithoutHaddocks $ view (stackYamlL . to toFilePath . to T.pack))
   , ( "PATH environment variable"
     , "bin-path"
     , WithoutHaddocks $
@@ -190,71 +218,75 @@   , ( "Install location for GHC and other core tools (see 'stack ls tools' command)"
     , "programs"
     , WithoutHaddocks $
-        view (configL.to configLocalPrograms.to toFilePathNoTrailingSep.to T.pack))
+        view (configL . to (.localPrograms) . to toFilePathNoTrailingSep . to T.pack))
   , ( "Compiler binary (e.g. ghc)"
     , "compiler-exe"
-    , WithoutHaddocks $ T.pack . toFilePath . piCompiler )
+    , WithoutHaddocks $ T.pack . toFilePath . (.compiler) )
   , ( "Directory containing the compiler binary (e.g. ghc)"
     , "compiler-bin"
-    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . parent . piCompiler )
-  , ( "Directory containing binaries specific to a particular compiler (e.g. intero)"
+    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . parent . (.compiler) )
+  , ( "Directory containing binaries specific to a particular compiler"
     , "compiler-tools-bin"
-    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . piToolsDir )
+    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . (.toolsDir) )
   , ( "Directory where Stack installs executables (e.g. ~/.local/bin (Unix-like OSs) or %APPDATA%\\local\\bin (Windows))"
     , "local-bin"
     , WithoutHaddocks $
-        view $ configL.to configLocalBin.to toFilePathNoTrailingSep.to T.pack)
+        view $ configL . to (.localBin) . to toFilePathNoTrailingSep . to T.pack)
   , ( "Extra include directories"
     , "extra-include-dirs"
     , WithoutHaddocks $
-        T.intercalate ", " . map T.pack . configExtraIncludeDirs . view configL )
+        T.intercalate ", " . map T.pack . (.extraIncludeDirs) . view configL )
   , ( "Extra library directories"
     , "extra-library-dirs"
     , WithoutHaddocks $
-        T.intercalate ", " . map T.pack . configExtraLibDirs . view configL )
+        T.intercalate ", " . map T.pack . (.extraLibDirs) . view configL )
   , ( "Snapshot package database"
     , "snapshot-pkg-db"
-    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . piSnapDb )
+    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . (.snapDb) )
   , ( "Local project package database"
     , "local-pkg-db"
-    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . piLocalDb )
+    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . (.localDb) )
   , ( "Global package database"
     , "global-pkg-db"
-    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . piGlobalDb )
+    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . (.globalDb) )
   , ( "GHC_PACKAGE_PATH environment variable"
     , "ghc-package-path"
     , WithoutHaddocks $
-        \pi' -> mkGhcPackagePath
-                  True
-                  (piLocalDb pi')
-                  (piSnapDb pi')
-                  (piExtraDbs pi')
-                  (piGlobalDb pi')
+        \pi -> mkGhcPackagePath
+                 True
+                 pi.localDb
+                 pi.snapDb
+                 pi.extraDbs
+                 pi.globalDb
     )
   , ( "Snapshot installation root"
     , "snapshot-install-root"
     , WithoutHaddocks $
-        T.pack . toFilePathNoTrailingSep . piSnapRoot )
+        T.pack . toFilePathNoTrailingSep . (.snapRoot) )
   , ( "Local project installation root"
     , "local-install-root"
-    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . piLocalRoot )
+    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . (.localRoot) )
   , ( "Snapshot documentation root"
     , "snapshot-doc-root"
     , UseHaddocks $
-        \pi' -> T.pack (toFilePathNoTrailingSep (piSnapRoot pi' </> docDirSuffix))
+        \pi -> T.pack (toFilePathNoTrailingSep (pi.snapRoot </> docDirSuffix))
     )
   , ( "Local project documentation root"
     , "local-doc-root"
     , UseHaddocks $
-        \pi' -> T.pack (toFilePathNoTrailingSep (piLocalRoot pi' </> docDirSuffix))
+        \pi -> T.pack (toFilePathNoTrailingSep (pi.localRoot </> docDirSuffix))
     )
   , ( "Local project documentation root"
     , "local-hoogle-root"
-    , UseHaddocks $ T.pack . toFilePathNoTrailingSep . piHoogleRoot)
+    , UseHaddocks $ T.pack . toFilePathNoTrailingSep . (.hoogleRoot))
   , ( "Dist work directory, relative to package directory"
     , "dist-dir"
-    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . piDistDir )
+    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . (.distDir) )
   , ( "Where HPC reports and tix files are stored"
     , "local-hpc-root"
-    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . piHpcDir )
+    , WithoutHaddocks $ T.pack . toFilePathNoTrailingSep . (.hpcDir) )
   ]
+
+-- | 'Text' equivalent of 'stackRootOptionName'.
+stackRootOptionName' :: Text
+stackRootOptionName' = T.pack stackRootOptionName
src/Stack/Prelude.hs view
@@ -1,5 +1,7 @@-{-# LANGUAGE NoImplicitPrelude         #-}
-{-# LANGUAGE OverloadedStrings         #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 module Stack.Prelude
   ( withSystemTempDir
@@ -52,6 +54,9 @@   , encloseSep
   , fill
   , fillSep
+  , foldr'
+  , fromPackageId
+  , fromPackageName
   , flow
   , hang
   , hcat
@@ -94,6 +99,7 @@ import qualified Data.Conduit.List as CL
 import           Data.Conduit.Process.Typed
                    ( byteStringInput, createSource, withLoggedProcess_ )
+import           Data.Foldable ( Foldable(foldr') )
 import qualified Data.Text.IO as T
 import           Distribution.Types.LibraryName ( LibraryName (..) )
 import           Distribution.Types.MungedPackageId ( MungedPackageId (..) )
@@ -267,7 +273,7 @@ 
 -- | Like @First Bool@, but the default is @True@.
 newtype FirstTrue
-  = FirstTrue { getFirstTrue :: Maybe Bool }
+  = FirstTrue { firstTrue :: Maybe Bool }
   deriving (Eq, Ord, Show)
 
 instance Semigroup FirstTrue where
@@ -280,15 +286,15 @@ 
 -- | Get the 'Bool', defaulting to 'True'
 fromFirstTrue :: FirstTrue -> Bool
-fromFirstTrue = fromMaybe True . getFirstTrue
+fromFirstTrue = fromMaybe True . (.firstTrue)
 
 -- | Helper for filling in default values
-defaultFirstTrue :: (a -> FirstTrue) -> Bool
+defaultFirstTrue :: FirstTrue -> Bool
 defaultFirstTrue _ = True
 
 -- | Like @First Bool@, but the default is @False@.
 newtype FirstFalse
-  = FirstFalse { getFirstFalse :: Maybe Bool }
+  = FirstFalse { firstFalse :: Maybe Bool }
   deriving (Eq, Ord, Show)
 
 instance Semigroup FirstFalse where
@@ -301,10 +307,10 @@ 
 -- | Get the 'Bool', defaulting to 'False'
 fromFirstFalse :: FirstFalse -> Bool
-fromFirstFalse = fromMaybe False . getFirstFalse
+fromFirstFalse = fromMaybe False . (.firstFalse)
 
 -- | Helper for filling in default values
-defaultFirstFalse :: (a -> FirstFalse) -> Bool
+defaultFirstFalse :: FirstFalse -> Bool
 defaultFirstFalse _ = False
 
 -- | Write a @Builder@ to a file and atomically rename.
@@ -362,3 +368,11 @@ -- | Write a 'Builder' to the standard output stream.
 putBuilder :: MonadIO m => Builder -> m ()
 putBuilder = hPutBuilder stdout
+
+-- | Convert a package identifier to a value of a string-like type.
+fromPackageId :: IsString a => PackageIdentifier -> a
+fromPackageId = fromString . packageIdentifierString
+
+-- | Convert a package name to a value of a string-like type.
+fromPackageName :: IsString a => PackageName -> a
+fromPackageName = fromString . packageNameString
src/Stack/Query.hs view
@@ -1,5 +1,6 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 -- | Types and functions related to Stack's @query@ command.
 module Stack.Query
@@ -107,8 +108,9 @@ rawBuildInfo :: HasEnvConfig env => RIO env Value
 rawBuildInfo = do
   locals <- projectLocalPackages
-  wantedCompiler <- view $ wantedCompilerVersionL.to (utf8BuilderToText . display)
-  actualCompiler <- view $ actualCompilerVersionL.to compilerVersionText
+  wantedCompiler <-
+    view $ wantedCompilerVersionL . to (utf8BuilderToText . display)
+  actualCompiler <- view $ actualCompilerVersionL . to compilerVersionText
   pure $ object
     [ "locals" .= Object (KeyMap.fromList $ map localToPair locals)
     , "compiler" .= object
@@ -118,10 +120,10 @@     ]
  where
   localToPair lp =
-    (Key.fromText $ T.pack $ packageNameString $ packageName p, value)
+    (Key.fromText $ T.pack $ packageNameString p.name, value)
    where
-    p = lpPackage lp
+    p = lp.package
     value = object
-      [ "version" .= CabalString (packageVersion p)
-      , "path" .= toFilePath (parent $ lpCabalFile lp)
+      [ "version" .= CabalString p.version
+      , "path" .= toFilePath (parent lp.cabalFP)
       ]
src/Stack/Runners.hs view
@@ -1,6 +1,8 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE DataKinds         #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- | Utilities for running stack commands.
 --
@@ -18,7 +20,11 @@   , ShouldReexec (..)
   ) where
 
-import           RIO.Process ( mkDefaultProcessContext )
+import qualified Data.ByteString.Lazy.Char8 as L8
+import           RIO.Process
+                   ( findExecutable, mkDefaultProcessContext, proc
+                   , readProcess
+                   )
 import           RIO.Time ( addUTCTime, getCurrentTime )
 import           Stack.Build.Target ( NeedTargets (..) )
 import           Stack.Config
@@ -26,28 +32,30 @@                    , withNewLogFunc
                    )
 import           Stack.Constants
-                   ( defaultTerminalWidth, maxTerminalWidth, minTerminalWidth )
+                   ( defaultTerminalWidth, maxTerminalWidth, minTerminalWidth
+                   , nixProgName
+                   )
 import           Stack.DefaultColorWhen ( defaultColorWhen )
 import qualified Stack.Docker as Docker
 import qualified Stack.Nix as Nix
 import           Stack.Prelude
 import           Stack.Setup ( setupEnv )
 import           Stack.Storage.User ( logUpgradeCheck, upgradeChecksSince )
-import           Stack.Types.BuildOpts
+import           Stack.Types.BuildOptsCLI
                    ( BuildOptsCLI, defaultBuildOptsCLI )
 import           Stack.Types.ColorWhen ( ColorWhen (..) )
 import           Stack.Types.Config ( Config (..) )
 import           Stack.Types.ConfigMonoid ( ConfigMonoid (..) )
-import           Stack.Types.Docker ( dockerEnable )
+import           Stack.Types.Docker ( DockerOpts (..) )
 import           Stack.Types.EnvConfig ( EnvConfig )
 import           Stack.Types.GlobalOpts ( GlobalOpts (..) )
-import           Stack.Types.Nix ( nixEnable )
+import           Stack.Types.Nix ( NixOpts (..) )
 import           Stack.Types.Runner
                    ( Runner (..), globalOptsL, reExecL, stackYamlLocL )
 import           Stack.Types.StackYamlLoc ( StackYamlLoc (..) )
 import           Stack.Types.Version
                    ( minorVersion, stackMinorVersion, stackVersion )
-import           System.Console.ANSI ( hSupportsANSI )
+import           System.Console.ANSI ( hNowSupportsANSI )
 import           System.Terminal ( getTerminalWidth )
 
 -- | Type representing exceptions thrown by functions exported by the
@@ -121,7 +129,7 @@     -- If we have been relaunched in a Docker container, perform in-container
     -- initialization (switch UID, etc.).  We do this after first loading the
     -- configuration since it must happen ASAP but needs a configuration.
-    view (globalOptsL.to globalDockerEntrypoint) >>=
+    view (globalOptsL . to (.dockerEntrypoint)) >>=
       traverse_ (Docker.entrypoint config)
     runRIO config $ do
       -- Catching all exceptions here, since we don't want this
@@ -139,8 +147,51 @@ -- action.
 reexec :: RIO Config a -> RIO Config a
 reexec inner = do
-  nixEnable' <- asks $ nixEnable . configNix
-  dockerEnable' <- asks $ dockerEnable . configDocker
+  nixEnable' <- asks $ (.nix.enable)
+  notifyIfNixOnPath <- asks (.notifyIfNixOnPath)
+  when (not nixEnable' && notifyIfNixOnPath) $ do
+    eNix <- findExecutable nixProgName
+    case eNix of
+      Left _ -> pure ()
+      Right nix -> proc nix ["--version"] $ \pc -> do
+        let nixProgName' = style Shell (fromString nixProgName)
+            muteMsg = fillSep
+              [ flow "To mute this message in future, set"
+              , style Shell (flow "notify-if-nix-on-path: false")
+              , flow "in Stack's configuration."
+              ]
+            reportErr errMsg = prettyWarn $
+                 fillSep
+                   [ nixProgName'
+                   , flow "is on the PATH"
+                   , parens (fillSep ["at", style File (fromString nix)])
+                   , flow "but Stack encountered the following error with"
+                   , nixProgName'
+                   , style Shell "--version" <> ":"
+                   ]
+              <> blankLine
+              <> errMsg
+              <> blankLine
+              <> muteMsg
+              <> line
+        res <- tryAny (readProcess pc)
+        case res of
+          Left e -> reportErr (ppException e)
+          Right (ec, out, err) -> case ec of
+            ExitFailure _ -> reportErr $ string (L8.unpack err)
+            ExitSuccess -> do
+              let trimFinalNewline str = case reverse str of
+                    '\n' : rest -> reverse rest
+                    _ -> str
+              prettyWarn $ fillSep
+                   [ fromString (trimFinalNewline $ L8.unpack out)
+                   , flow "is on the PATH"
+                   , parens (fillSep ["at", style File (fromString nix)])
+                   , flow "but Stack's Nix integration is disabled."
+                   , muteMsg
+                   ]
+                <> line
+  dockerEnable' <- asks (.docker.enable)
   case (nixEnable', dockerEnable') of
     (True, True) -> throwIO DockerAndNixInvalid
     (False, False) -> inner
@@ -171,23 +222,27 @@ withRunnerGlobal go inner = do
   colorWhen <-
     maybe defaultColorWhen pure $
-    getFirst $ configMonoidColorWhen $ globalConfigMonoid go
+    getFirst go.configMonoid.colorWhen
   useColor <- case colorWhen of
     ColorNever -> pure False
     ColorAlways -> pure True
-    ColorAuto -> hSupportsANSI stderr
+    ColorAuto -> hNowSupportsANSI stderr
   termWidth <- clipWidth <$> maybe (fromMaybe defaultTerminalWidth
                                     <$> getTerminalWidth)
-                                   pure (globalTermWidth go)
+                                   pure go.termWidthOpt
   menv <- mkDefaultProcessContext
-  let update = globalStylesUpdate go
-  withNewLogFunc go useColor update $ \logFunc -> runRIO Runner
-    { runnerGlobalOpts = go
-    , runnerUseColor = useColor
-    , runnerLogFunc = logFunc
-    , runnerTermWidth = termWidth
-    , runnerProcessContext = menv
-    } inner
+  -- MVar used to ensure the Docker entrypoint is performed exactly once.
+  dockerEntrypointMVar <- newMVar False
+  let update = go.stylesUpdate
+  withNewLogFunc go useColor update $ \logFunc -> do
+    runRIO Runner
+      { globalOpts = go
+      , useColor = useColor
+      , logFunc = logFunc
+      , termWidth = termWidth
+      , processContext = menv
+      , dockerEntrypointMVar = dockerEntrypointMVar
+      } inner
  where
   clipWidth w
     | w < minTerminalWidth = minTerminalWidth
@@ -198,9 +253,9 @@ shouldUpgradeCheck :: RIO Config ()
 shouldUpgradeCheck = do
   config <- ask
-  when (configRecommendUpgrade config) $ do
+  when config.recommendUpgrade $ do
     now <- getCurrentTime
-    let yesterday = addUTCTime (-24 * 60 * 60) now
+    let yesterday = addUTCTime (-(24 * 60 * 60)) now
     checks <- upgradeChecksSince yesterday
     when (checks == 0) $ do
       mversion <- getLatestHackageVersion NoRequireHackageIndex "stack" UsePreferredVersions
@@ -226,7 +281,7 @@                  [ flow "Tired of seeing this? Add"
                  , style Shell (flow "recommend-stack-upgrade: false")
                  , "to"
-                 , pretty (configUserConfigPath config) <> "."
+                 , pretty config.userConfigPath <> "."
                  ]
             <> blankLine
         _ -> pure ()
src/Stack/SDist.hs view
@@ -1,6 +1,9 @@-{-# LANGUAGE NoImplicitPrelude  #-}
-{-# LANGUAGE OverloadedStrings  #-}
-{-# LANGUAGE TypeFamilies       #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE TypeFamilies          #-}
 
 -- Types and functions related to Stack's @sdist@ command.
 module Stack.SDist
@@ -9,9 +12,10 @@   , getSDistTarball
   , checkSDistTarball
   , checkSDistTarball'
+  , readLocalPackage
   ) where
 
-import qualified Codec.Archive.Tar as Tar
+import qualified Codec.Archive.Tar.Utf8 as Tar
 import qualified Codec.Archive.Tar.Entry as Tar
 import qualified Codec.Compression.GZip as GZip
 import           Conduit ( runConduitRes, sourceLazy, sinkFileCautious )
@@ -23,7 +27,6 @@ import           Data.Char ( toLower )
 import           Data.Data ( cast )
 import qualified Data.List as List
-import qualified Data.List.NonEmpty as NE
 import qualified Data.Map.Strict as Map
 import qualified Data.Set as Set
 import qualified Data.Text as T
@@ -44,39 +47,37 @@                    )
 import           Path ( (</>), parent, parseRelDir, parseRelFile )
 import           Path.IO ( ensureDir, resolveDir' )
+import           RIO.NonEmpty ( nonEmpty )
+import qualified RIO.NonEmpty as NE
 import           Stack.Build ( mkBaseConfigOpts, build, buildLocalTargets )
 import           Stack.Build.Execute
-                   ( ExcludeTHLoading (..), KeepOutputOpen (..), withExecuteEnv
-                   , withSingleContext
-                   )
+                   ( ExcludeTHLoading (..), KeepOutputOpen (..) )
+import           Stack.Build.ExecuteEnv ( withExecuteEnv, withSingleContext )
 import           Stack.Build.Installed ( getInstalled, toInstallMap )
 import           Stack.Build.Source ( projectLocalPackages )
+import           Stack.BuildOpts ( defaultBuildOpts )
 import           Stack.Constants ( stackProgName, stackProgName' )
 import           Stack.Constants.Config ( distDirFromDir )
-import           Stack.Package
-                   ( PackageDescriptionPair (..), resolvePackage
-                   , resolvePackageDescription
-                   )
+import           Stack.Package ( resolvePackage, resolvePackageDescription )
 import           Stack.Prelude
 import           Stack.Runners
                    ( ShouldReexec (..), withConfig, withDefaultEnvConfig )
 import           Stack.SourceMap ( mkProjectPackage )
-import           Stack.Types.Build
-                   ( CachePkgSrc (..), Task (..), TaskConfigOpts (..)
-                   , TaskType (..)
-                   )
+import           Stack.Types.Build ( TaskType (..) )
 import           Stack.Types.BuildConfig
                    ( BuildConfig (..), HasBuildConfig (..), stackYamlL )
-import           Stack.Types.BuildOpts
-                   ( BuildOpts (..), defaultBuildOpts, defaultBuildOptsCLI )
+import           Stack.Types.BuildOpts ( BuildOpts (..) )
+import           Stack.Types.BuildOptsCLI ( defaultBuildOptsCLI )
 import           Stack.Types.Config ( Config (..), HasConfig (..) )
-import           Stack.Types.ConfigureOpts ( ConfigureOpts (..) )
 import           Stack.Types.EnvConfig
                    ( EnvConfig (..), HasEnvConfig (..), actualCompilerVersionL )
 import           Stack.Types.GhcPkgId ( GhcPkgId )
+import           Stack.Types.Installed
+                   ( InstallMap, Installed (..), InstalledMap
+                   , InstalledLibraryInfo (..), installedVersion
+                   )
 import           Stack.Types.Package
-                   ( InstallMap, Installed (..), InstalledMap, LocalPackage (..)
-                   , Package (..), PackageConfig (..), installedVersion
+                   ( LocalPackage (..), Package (..), PackageConfig (..)
                    , packageIdentifier
                    )
 import           Stack.Types.Platform ( HasPlatform (..) )
@@ -86,6 +87,7 @@                    ( CommonPackage (..), ProjectPackage (..), SMWanted (..)
                    , SourceMap (..), ppRoot
                    )
+import qualified Stack.Types.SourceMap as SourceMap ( SourceMap (..) )
 import           Stack.Types.Version
                    ( intersectVersionRanges, nextMajorVersion )
 import           System.Directory
@@ -109,15 +111,15 @@     <> flow "Package check reported the following errors:"
     <> line
     <> bulletedList (map (string . show) (NE.toList xs) :: [StyleDoc])
-  pretty (CabalFilePathsInconsistentBug cabalfp cabalfp') =
+  pretty (CabalFilePathsInconsistentBug cabalFP cabalFP') =
     "[S-9595]"
     <> line
     <> fillSep
          [ flow "The impossible happened! Two Cabal file paths are \
                 \inconsistent:"
-         , pretty cabalfp
+         , pretty cabalFP
          , "and"
-         , pretty cabalfp' <> "."
+         , pretty cabalFP' <> "."
          ]
   pretty (ToTarPathException e) =
     "[S-7875]"
@@ -128,16 +130,16 @@ 
 -- | Type representing command line options for @stack sdist@ command.
 data SDistOpts = SDistOpts
-  { sdoptsDirsToWorkWith :: [String]
-  -- ^ Directories to package
-  , sdoptsPvpBounds :: Maybe PvpBounds
-  -- ^ PVP Bounds overrides
-  , sdoptsIgnoreCheck :: Bool
-  -- ^ Whether to ignore check of the package for common errors
-  , sdoptsBuildTarball :: Bool
-  -- ^ Whether to build the tarball
-  , sdoptsTarPath :: Maybe FilePath
-  -- ^ Where to copy the tarball
+  { dirsToWorkWith :: [String]
+    -- ^ Directories to package
+  , pvpBounds :: Maybe PvpBounds
+    -- ^ PVP Bounds overrides
+  , ignoreCheck :: Bool
+    -- ^ Whether to ignore check of the package for common errors
+  , buildTarball :: Bool
+    -- ^ Whether to build the tarball
+  , tarPath :: Maybe FilePath
+    -- ^ Where to copy the tarball
   }
 
 -- | Function underlying the @stack sdist@ command.
@@ -145,10 +147,10 @@ sdistCmd sdistOpts =
   withConfig YesReexec $ withDefaultEnvConfig $ do
     -- If no directories are specified, build all sdist tarballs.
-    dirs' <- if null (sdoptsDirsToWorkWith sdistOpts)
+    dirs' <- if null sdistOpts.dirsToWorkWith
       then do
         dirs <- view $
-          buildConfigL.to (map ppRoot . Map.elems . smwProject . bcSMWanted)
+          buildConfigL . to (map ppRoot . Map.elems . (.smWanted.project))
         when (null dirs) $ do
           stackYaml <- view stackYamlL
           prettyErrorL
@@ -161,10 +163,10 @@             ]
           exitFailure
         pure dirs
-      else mapM resolveDir' (sdoptsDirsToWorkWith sdistOpts)
+      else mapM resolveDir' sdistOpts.dirsToWorkWith
     forM_ dirs' $ \dir -> do
       (tarName, tarBytes, _mcabalRevision) <-
-        getSDistTarball (sdoptsPvpBounds sdistOpts) dir
+        getSDistTarball sdistOpts.pvpBounds dir
       distDir <- distDirFromDir dir
       tarPath <- (distDir </>) <$> parseRelFile tarName
       ensureDir (parent tarPath)
@@ -176,7 +178,7 @@         , pretty tarPath <> "."
         ]
       checkSDistTarball sdistOpts tarPath
-      forM_ (sdoptsTarPath sdistOpts) $ copyTarToTarPath tarPath tarName
+      forM_ sdistOpts.tarPath $ copyTarToTarPath tarPath tarName
  where
   copyTarToTarPath tarPath tarName targetDir = liftIO $ do
     let targetTarPath = targetDir FP.</> tarName
@@ -193,17 +195,22 @@      -- ^ Override Config value
   -> Path Abs Dir
      -- ^ Path to local package
-  -> RIO env (FilePath, L.ByteString, Maybe (PackageIdentifier, L.ByteString))
+  -> RIO
+       env
+       ( FilePath
+       , L.ByteString
+       , Maybe (PackageIdentifier, L.ByteString)
+       )
      -- ^ Filename, tarball contents, and option Cabal file revision to upload
 getSDistTarball mpvpBounds pkgDir = do
   config <- view configL
   let PvpBounds pvpBounds asRevision =
-        fromMaybe (configPvpBounds config) mpvpBounds
+        fromMaybe config.pvpBounds mpvpBounds
       tweakCabal = pvpBounds /= PvpBoundsNone
       pkgFp = toFilePath pkgDir
   lp <- readLocalPackage pkgDir
-  forM_ (packageSetupDeps (lpPackage lp)) $ \customSetupDeps ->
-    case NE.nonEmpty (map (T.pack . packageNameString) (Map.keys customSetupDeps)) of
+  forM_ lp.package.setupDeps $ \customSetupDeps ->
+    case nonEmpty (map (T.pack . packageNameString) (Map.keys customSetupDeps)) of
       Just nonEmptyDepTargets -> do
         eres <- buildLocalTargets nonEmptyDepTargets
         case eres of
@@ -216,18 +223,18 @@             pure ()
       Nothing ->
         prettyWarnS "unexpected empty custom-setup dependencies."
-  sourceMap <- view $ envConfigL.to envConfigSourceMap
+  sourceMap <- view $ envConfigL . to (.sourceMap)
   installMap <- toInstallMap sourceMap
   (installedMap, _globalDumpPkgs, _snapshotDumpPkgs, _localDumpPkgs) <-
     getInstalled installMap
   let deps = Map.fromList
-        [ (pid, ghcPkgId)
-        | (_, Library pid ghcPkgId _) <- Map.elems installedMap]
+        [ (pid, libInfo.ghcPkgId)
+        | (_, Library pid libInfo) <- Map.elems installedMap]
   prettyInfoL
     [ flow "Getting the file list for"
     , style File (fromString  pkgFp) <> "."
     ]
-  (fileList, cabalfp) <- getSDistFileList lp deps
+  (fileList, cabalFP) <- getSDistFileList lp deps
   prettyInfoL
     [ flow "Building a compressed archive file in the sdist format for"
     , style File (fromString pkgFp) <> "."
@@ -243,7 +250,7 @@   -- prone and more predictable to read everything in at once, so that's what
   -- we're doing for now:
   let tarPath isDir fp =
-        case Tar.toTarPath isDir (forceUtf8Enc (pkgId FP.</> fp)) of
+        case Tar.toTarPath isDir (forceUtf8Enc (pkgIdName FP.</> fp)) of
           Left e -> prettyThrowIO $ ToTarPathException e
           Right tp -> pure tp
       -- convert a String of proper characters to a String of bytes in UTF8
@@ -256,20 +263,21 @@         -- This is a Cabal file, we're going to tweak it, but only tweak it as a
         -- revision.
         | tweakCabal && isCabalFp fp && asRevision = do
-            lbsIdent <- getCabalLbs pvpBounds (Just 1) cabalfp sourceMap
+            lbsIdent <- getCabalLbs pvpBounds (Just 1) cabalFP sourceMap
             liftIO (writeIORef cabalFileRevisionRef (Just lbsIdent))
             packWith packFileEntry False fp
         -- Same, except we'll include the Cabal file in the original tarball
         -- upload.
         | tweakCabal && isCabalFp fp = do
-            (_ident, lbs) <- getCabalLbs pvpBounds Nothing cabalfp sourceMap
+            (_ident, lbs) <- getCabalLbs pvpBounds Nothing cabalFP sourceMap
             currTime <- liftIO getPOSIXTime -- Seconds from UNIX epoch
             tp <- liftIO $ tarPath False fp
             pure $ (Tar.fileEntry tp lbs) { Tar.entryTime = floor currTime }
         | otherwise = packWith packFileEntry False fp
-      isCabalFp fp = toFilePath pkgDir FP.</> fp == toFilePath cabalfp
-      tarName = pkgId FP.<.> "tar.gz"
-      pkgId = packageIdentifierString (packageIdentifier (lpPackage lp))
+      isCabalFp fp = toFilePath pkgDir FP.</> fp == toFilePath cabalFP
+      tarName = pkgIdName FP.<.> "tar.gz"
+      pkgIdName = packageIdentifierString pkgId
+      pkgId = packageIdentifier lp.package
   dirEntries <- mapM packDir (dirsFromFiles files)
   fileEntries <- mapM packFile files
   mcabalFileRevision <- liftIO (readIORef cabalFileRevisionRef)
@@ -287,20 +295,20 @@   -> Path Abs File -- ^ Cabal file
   -> SourceMap
   -> RIO env (PackageIdentifier, L.ByteString)
-getCabalLbs pvpBounds mrev cabalfp sourceMap = do
-  (gpdio, _name, cabalfp') <-
-    loadCabalFilePath (Just stackProgName') (parent cabalfp)
+getCabalLbs pvpBounds mrev cabalFP sourceMap = do
+  (gpdio, _name, cabalFP') <-
+    loadCabalFilePath (Just stackProgName') (parent cabalFP)
   gpd <- liftIO $ gpdio NoPrintWarnings
-  unless (cabalfp == cabalfp') $
-    prettyThrowIO $ CabalFilePathsInconsistentBug cabalfp cabalfp'
+  unless (cabalFP == cabalFP') $
+    prettyThrowIO $ CabalFilePathsInconsistentBug cabalFP cabalFP'
   installMap <- toInstallMap sourceMap
   (installedMap, _, _, _) <- getInstalled installMap
-  let internalPackages = Set.fromList $
+  let subLibPackages = Set.fromList $
           gpdPackageName gpd
         : map
             (Cabal.unqualComponentNameToPackageName . fst)
             (Cabal.condSubLibraries gpd)
-      gpd' = gtraverseT (addBounds internalPackages installMap installedMap) gpd
+      gpd' = gtraverseT (addBounds subLibPackages installMap installedMap) gpd
       gpd'' =
         case mrev of
           Nothing -> gpd'
@@ -326,7 +334,7 @@            fillSep
              [ flow "Bug detected in Cabal library. ((parse . render . parse) \
                     \=== id) does not hold for the Cabal file at"
-             , pretty cabalfp
+             , pretty cabalFP
              ]
         <> blankLine
       (_warnings, eres) = Cabal.runParseResult
@@ -394,8 +402,8 @@     -> InstalledMap
     -> Dependency
     -> Dependency
-  addBounds internalPackages installMap installedMap dep =
-    if name `Set.member` internalPackages
+  addBounds subLibPackages installMap installedMap dep =
+    if name `Set.member` subLibPackages
       then dep
       else case foundVersion of
         Nothing -> dep
@@ -448,24 +456,24 @@ readLocalPackage :: HasEnvConfig env => Path Abs Dir -> RIO env LocalPackage
 readLocalPackage pkgDir = do
   config  <- getDefaultPackageConfig
-  (gpdio, _, cabalfp) <- loadCabalFilePath (Just stackProgName') pkgDir
+  (gpdio, _, cabalFP) <- loadCabalFilePath (Just stackProgName') pkgDir
   gpd <- liftIO $ gpdio YesPrintWarnings
   let package = resolvePackage config gpd
   pure LocalPackage
-    { lpPackage = package
-    , lpWanted = False -- HACK: makes it so that sdist output goes to a log
+    { package
+    , wanted = False -- HACK: makes it so that sdist output goes to a log
                        -- instead of a file.
-    , lpCabalFile = cabalfp
-    -- NOTE: these aren't the 'correct values, but aren't used in
-    -- the usage of this function in this module.
-    , lpTestBench = Nothing
-    , lpBuildHaddocks = False
-    , lpForceDirty = False
-    , lpDirtyFiles = pure Nothing
-    , lpNewBuildCaches = pure Map.empty
-    , lpComponentFiles = pure Map.empty
-    , lpComponents = Set.empty
-    , lpUnbuildable = Set.empty
+    , cabalFP
+    -- NOTE: these aren't the 'correct' values, but aren't used in the usage of
+    -- this function in this module.
+    , testBench = Nothing
+    , buildHaddocks = False
+    , forceDirty = False
+    , dirtyFiles = pure Nothing
+    , newBuildCaches = pure Map.empty
+    , componentFiles = pure Map.empty
+    , components = Set.empty
+    , unbuildable = Set.empty
     }
 
 -- | Returns a newline-separate list of paths, and the absolute path to the
@@ -485,33 +493,18 @@       [] [] [] Nothing -- provide empty list of globals. This is a hack around
                        -- custom Setup.hs files
       $ \ee ->
-      withSingleContext ac ee task deps (Just "sdist") $
-        \_package cabalfp _pkgDir cabal _announce _outputType -> do
+      withSingleContext ac ee taskType deps (Just "sdist") $
+        \_package cabalFP _pkgDir cabal _announce _outputType -> do
           let outFile = toFilePath tmpdir FP.</> "source-files-list"
           cabal
             CloseOnException
             KeepTHLoading
             ["sdist", "--list-sources", outFile]
           contents <- liftIO (S.readFile outFile)
-          pure (T.unpack $ T.decodeUtf8With T.lenientDecode contents, cabalfp)
+          pure (T.unpack $ T.decodeUtf8With T.lenientDecode contents, cabalFP)
  where
-  package = lpPackage lp
   ac = ActionContext Set.empty [] ConcurrencyAllowed
-  task = Task
-    { taskProvides =
-        PackageIdentifier (packageName package) (packageVersion package)
-    , taskType = TTLocalMutable lp
-    , taskConfigOpts = TaskConfigOpts
-        { tcoMissing = Set.empty
-        , tcoOpts = \_ -> ConfigureOpts [] []
-        }
-    , taskBuildHaddock = False
-    , taskPresent = Map.empty
-    , taskAllInOne = True
-    , taskCachePkgSrc = CacheSrcLocal (toFilePath (parent $ lpCabalFile lp))
-    , taskAnyMissing = True
-    , taskBuildTypeConfig = False
-    }
+  taskType = TTLocalMutable lp
 
 normalizeTarballPaths ::
      (HasRunner env, HasTerm env)
@@ -560,13 +553,13 @@   pkgDir <- (pkgDir' </>) <$>
     (parseRelDir . FP.takeBaseName . FP.takeBaseName . toFilePath $ tarball)
   --               ^ drop ".tar"     ^ drop ".gz"
-  when (sdoptsBuildTarball opts)
+  when opts.buildTarball
     ( buildExtractedTarball ResolvedPath
         { resolvedRelative = RelFilePath "this-is-not-used" -- ugly hack
         , resolvedAbsolute = pkgDir
         }
     )
-  unless (sdoptsIgnoreCheck opts) (checkPackageInExtractedTarball pkgDir)
+  unless opts.ignoreCheck (checkPackageInExtractedTarball pkgDir)
 
 checkPackageInExtractedTarball ::
      HasEnvConfig env
@@ -576,10 +569,10 @@   (gpdio, name, _cabalfp) <- loadCabalFilePath (Just stackProgName') pkgDir
   gpd <- liftIO $ gpdio YesPrintWarnings
   config <- getDefaultPackageConfig
-  let PackageDescriptionPair pkgDesc _ = resolvePackageDescription config gpd
+  let pkgDesc = resolvePackageDescription config gpd
   prettyInfoL
     [ flow "Checking package"
-    , style Current (fromString $ packageNameString name)
+    , style Current (fromPackageName name)
     , flow "for common mistakes."
     ]
   let pkgChecks =
@@ -607,7 +600,7 @@          flow "Package check reported the following warnings:"
       <> line
       <> bulletedList (map (fromString . show) warnings)
-  case NE.nonEmpty errors of
+  case nonEmpty errors of
     Nothing -> pure ()
     Just ne -> prettyThrowM $ CheckException ne
 
@@ -619,26 +612,23 @@   let isPathToRemove path = do
         localPackage <- readLocalPackage path
         pure
-          $  packageName (lpPackage localPackage)
-          == packageName (lpPackage localPackageToBuild)
+          $  localPackage.package.name
+          == localPackageToBuild.package.name
   pathsToKeep <- Map.fromList <$> filterM
-    (fmap not . isPathToRemove . resolvedAbsolute . ppResolvedDir . snd)
-    (Map.toList (smwProject (bcSMWanted (envConfigBuildConfig envConfig))))
+    (fmap not . isPathToRemove . resolvedAbsolute . (.resolvedDir) . snd)
+    (Map.toList envConfig.buildConfig.smWanted.project)
   pp <- mkProjectPackage YesPrintWarnings pkgDir False
   let adjustEnvForBuild env =
         let updatedEnvConfig = envConfig
-              { envConfigSourceMap =
-                  updatePackagesInSourceMap (envConfigSourceMap envConfig)
-              , envConfigBuildConfig =
-                  updateBuildConfig (envConfigBuildConfig envConfig)
+              { sourceMap = updatePackagesInSourceMap envConfig.sourceMap
+              , buildConfig = updateBuildConfig envConfig.buildConfig
               }
             updateBuildConfig bc = bc
-              { bcConfig = (bcConfig bc)
-                 { configBuild = defaultBuildOpts { boptsTests = True } }
+              { config = bc.config { build = defaultBuildOpts { tests = True } }
               }
         in  set envConfigL updatedEnvConfig env
       updatePackagesInSourceMap sm =
-        sm {smProject = Map.insert (cpName $ ppCommon pp) pp pathsToKeep}
+        sm { SourceMap.project = Map.insert pp.projectCommon.name pp pathsToKeep }
   local adjustEnvForBuild $ build Nothing
 
 -- | Version of 'checkSDistTarball' that first saves lazy bytestring to
@@ -700,11 +690,11 @@   platform <- view platformL
   compilerVersion <- view actualCompilerVersionL
   pure PackageConfig
-    { packageConfigEnableTests = False
-    , packageConfigEnableBenchmarks = False
-    , packageConfigFlags = mempty
-    , packageConfigGhcOptions = []
-    , packageConfigCabalConfigOpts = []
-    , packageConfigCompilerVersion = compilerVersion
-    , packageConfigPlatform = platform
+    { enableTests = False
+    , enableBenchmarks = False
+    , flags = mempty
+    , ghcOptions = []
+    , cabalConfigOpts = []
+    , compilerVersion
+    , platform
     }
src/Stack/Script.hs view
@@ -1,7 +1,9 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE LambdaCase        #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE LambdaCase            #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- Types and functions related to Stack's @script@ command.
 module Stack.Script
@@ -47,6 +49,7 @@                    ( CompilerPaths (..), GhcPkgExe (..), HasCompiler (..) )
 import           Stack.Types.Config ( Config (..), HasConfig (..), stackRootL )
 import           Stack.Types.ConfigMonoid ( ConfigMonoid (..) )
+import qualified Stack.Types.ConfigMonoid as ConfigMonoid ( ConfigMonoid (..) )
 import           Stack.Types.DumpPackage ( DumpPackage (..) )
 import           Stack.Types.EnvConfig
                    ( EnvConfig (..), HasEnvConfig (..), actualCompilerVersionL
@@ -118,14 +121,14 @@ 
 -- | Type representing command line options for the @stack script@ command.
 data ScriptOpts = ScriptOpts
-  { soPackages :: ![String]
-  , soFile :: !FilePath
-  , soArgs :: ![String]
-  , soCompile :: !ScriptExecute
-  , soUseRoot :: !Bool
-  , soGhcOptions :: ![String]
-  , soScriptExtraDeps :: ![PackageIdentifierRevision]
-  , soShouldRun :: !ShouldRun
+  { packages :: ![String]
+  , file :: !FilePath
+  , args :: ![String]
+  , compile :: !ScriptExecute
+  , useRoot :: !Bool
+  , ghcOptions :: ![String]
+  , scriptExtraDeps :: ![PackageIdentifierRevision]
+  , shouldRun :: !ShouldRun
   }
   deriving Show
 
@@ -136,7 +139,7 @@   -- Note that in this functions we use logError instead of logWarn because,
   -- when using the interpreter mode, only error messages are shown. See:
   -- https://github.com/commercialhaskell/stack/issues/3007
-  view (globalOptsL.to globalStackYaml) >>= \case
+  view (globalOptsL . to (.stackYaml)) >>= \case
     SYLOverride fp -> logError $
          "Ignoring override stack.yaml file for script command: "
       <> fromString (toFilePath fp)
@@ -144,25 +147,25 @@     SYLDefault -> pure ()
     SYLNoProject _ -> assert False (pure ())
 
-  file <- resolveFile' $ soFile opts
+  file <- resolveFile' opts.file
   let scriptFile = filename file
 
-  isNoRunCompile <- fromFirstFalse . configMonoidNoRunCompile <$>
-                           view (globalOptsL.to globalConfigMonoid)
+  isNoRunCompile <- fromFirstFalse . (.noRunCompile) <$>
+                           view (globalOptsL . to (.configMonoid))
 
   let scriptDir = parent file
       modifyGO go = go
-        { globalConfigMonoid = (globalConfigMonoid go)
-            { configMonoidInstallGHC = FirstTrue $ Just True
+        { configMonoid = go.configMonoid
+            { ConfigMonoid.installGHC = FirstTrue $ Just True
             }
-        , globalStackYaml = SYLNoProject $ soScriptExtraDeps opts
+        , stackYaml = SYLNoProject opts.scriptExtraDeps
         }
       (shouldRun, shouldCompile) = if isNoRunCompile
         then (NoRun, SECompile)
-        else (soShouldRun opts, soCompile opts)
+        else (opts.shouldRun, opts.compile)
 
   root <- withConfig NoReexec $ view stackRootL
-  outputDir <- if soUseRoot opts
+  outputDir <- if opts.useRoot
     then do
       scriptFileAsDir <- maybe
         (throwIO $ FailedToParseScriptFileAsDirBug scriptFile)
@@ -191,7 +194,7 @@   case shouldRun of
     YesRun -> pure ()
     NoRun -> do
-      unless (null $ soArgs opts) $ throwIO ArgumentsWithNoRunInvalid
+      unless (null opts.args) $ throwIO ArgumentsWithNoRunInvalid
       case shouldCompile of
         SEInterpret -> throwIO NoRunWithoutCompilationInvalid
         SECompile -> pure ()
@@ -208,7 +211,7 @@  where
   runCompiled shouldRun exe = do
     case shouldRun of
-      YesRun -> exec (fromAbsFile exe) (soArgs opts)
+      YesRun -> exec (fromAbsFile exe) opts.args
       NoRun -> prettyInfoL
         [ flow "Compilation finished, executable available at"
         , style File (fromString (fromAbsFile exe)) <> "."
@@ -226,13 +229,13 @@     withConfig YesReexec $
     withDefaultEnvConfig $ do
       config <- view configL
-      menv <- liftIO $ configProcessContextSettings config defaultEnvSettings
+      menv <- liftIO $ config.processContextSettings defaultEnvSettings
       withProcessContext menv $ do
         colorFlag <- appropriateGhcColorFlag
 
         targetsSet <-
-          case soPackages opts of
-            [] -> getPackagesFromImports (soFile opts) -- Using the import parser
+          case opts.packages of
+            [] -> getPackagesFromImports opts.file -- Using the import parser
             packages -> do
               let targets = concatMap wordsComma packages
               targets' <- mapM parsePackageNameThrowing targets
@@ -243,7 +246,7 @@           -- to check which packages are installed already. If all needed
           -- packages are available, we can skip the (rather expensive) build
           -- call below.
-          GhcPkgExe pkg <- view $ compilerPathsL.to cpPkg
+          GhcPkgExe pkg <- view $ compilerPathsL . to (.pkg)
           -- https://github.com/haskell/process/issues/251
           bss <- snd <$> sinkProcessStderrStdout (toFilePath pkg)
               ["list", "--simple-output"] CL.sinkNull CL.consume -- FIXME use the package info from envConfigPackages, or is that crazy?
@@ -272,8 +275,8 @@                   SEInterpret -> []
                   SECompile -> []
                   SEOptimize -> ["-O2"]
-              , soGhcOptions opts
-              , if soUseRoot opts
+              , opts.ghcOptions
+              , if opts.useRoot
                   then
                     [ "-outputdir=" ++ fromAbsDir (parent exe)
                     , "-o", fromAbsFile exe
@@ -282,9 +285,9 @@               ]
         case shouldCompile of
           SEInterpret -> do
-            interpret <- view $ compilerPathsL.to cpInterpreter
+            interpret <- view $ compilerPathsL . to (.interpreter)
             exec (toFilePath interpret)
-                (ghcArgs ++ toFilePath file : soArgs opts)
+                (ghcArgs ++ toFilePath file : opts.args)
           _ -> do
             -- Use readProcessStdout_ so that (1) if GHC does send any output
             -- to stdout, we capture it and stop it from being sent to our
@@ -292,7 +295,8 @@             -- exception, the standard output we did capture will be reported
             -- to the user.
             liftIO $ Dir.createDirectoryIfMissing True (fromAbsDir (parent exe))
-            compilerExeName <- view $ compilerPathsL.to cpCompiler.to toFilePath
+            compilerExeName <-
+              view $ compilerPathsL . to (.compiler) . to toFilePath
             withWorkingDir (fromAbsDir (parent file)) $ proc
               compilerExeName
               (ghcArgs ++ [toFilePath file])
@@ -329,12 +333,12 @@ 
 hashSnapshot :: RIO EnvConfig SnapshotCacheHash
 hashSnapshot = do
-  sourceMap <- view $ envConfigL . to envConfigSourceMap
+  sourceMap <- view $ envConfigL . to (.sourceMap)
   compilerInfo <- getCompilerInfo
   let eitherPliHash (pn, dep)
-        | PLImmutable pli <- dpLocation dep = Right $ immutableLocSha pli
+        | PLImmutable pli <- dep.location = Right $ immutableLocSha pli
         | otherwise = Left pn
-      deps = Map.toList (smDeps sourceMap)
+      deps = Map.toList sourceMap.deps
   case partitionEithers (map eitherPliHash deps) of
     ([], pliHashes) -> do
       let hashedContent = mconcat $ compilerInfo : pliHashes
@@ -345,18 +349,19 @@ 
 mapSnapshotPackageModules :: RIO EnvConfig (Map PackageName (Set ModuleName))
 mapSnapshotPackageModules = do
-  sourceMap <- view $ envConfigL . to envConfigSourceMap
+  sourceMap <- view $ envConfigL . to (.sourceMap)
   installMap <- toInstallMap sourceMap
   (_installedMap, globalDumpPkgs, snapshotDumpPkgs, _localDumpPkgs) <-
     getInstalled installMap
-  let globals = dumpedPackageModules (smGlobal sourceMap) globalDumpPkgs
-      notHidden = Map.filter (not . dpHidden)
-      notHiddenDeps = notHidden $ smDeps sourceMap
+  let globals = dumpedPackageModules sourceMap.globalPkgs globalDumpPkgs
+      notHidden = Map.filter (not . (.hidden))
+      notHiddenDeps = notHidden sourceMap.deps
       installedDeps = dumpedPackageModules notHiddenDeps snapshotDumpPkgs
-      dumpPkgs = Set.fromList $ map (pkgName . dpPackageIdent) snapshotDumpPkgs
+      dumpPkgs =
+        Set.fromList $ map (pkgName . (.packageIdent)) snapshotDumpPkgs
       notInstalledDeps = Map.withoutKeys notHiddenDeps dumpPkgs
   otherDeps <- for notInstalledDeps $ \dep -> do
-    gpd <- liftIO $ cpGPD (dpCommon dep)
+    gpd <- liftIO dep.depCommon.gpd
     Set.fromList <$> allExposedModules gpd
   -- source map construction process should guarantee unique package names in
   -- these maps
@@ -369,9 +374,9 @@ dumpedPackageModules pkgs dumpPkgs =
   let pnames = Map.keysSet pkgs `Set.difference` blacklist
   in  Map.fromList
-        [ (pn, dpExposedModules)
-        | DumpPackage {..} <- dumpPkgs
-        , let PackageIdentifier pn _ = dpPackageIdent
+        [ (pn, dp.exposedModules)
+        | dp <- dumpPkgs
+        , let PackageIdentifier pn _ = dp.packageIdent
         , pn `Set.member` pnames
         ]
 
src/Stack/Setup.hs view
@@ -1,10 +1,13 @@-{-# LANGUAGE NoImplicitPrelude   #-}
-{-# LANGUAGE DataKinds           #-}
-{-# LANGUAGE LambdaCase          #-}
-{-# LANGUAGE MultiWayIf          #-}
-{-# LANGUAGE OverloadedStrings   #-}
-{-# LANGUAGE TypeFamilies        #-}
-{-# LANGUAGE ViewPatterns        #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE LambdaCase            #-}
+{-# LANGUAGE MultiWayIf            #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE ViewPatterns          #-}
 
 module Stack.Setup
   ( setupEnv
@@ -43,7 +46,6 @@ import           Data.Conduit.Zlib ( ungzip )
 import           Data.List.Split ( splitOn )
 import qualified Data.Map as Map
-import           Data.Maybe ( fromJust )
 import qualified Data.Set as Set
 import qualified Data.Text as T
 import qualified Data.Text.Lazy as TL
@@ -66,8 +68,8 @@                    )
 import           Network.HTTP.Simple ( getResponseHeader )
 import           Path
-                   ( (</>), addExtension, filename, fromAbsDir, parent
-                   , parseAbsDir, parseAbsFile, parseRelDir, parseRelFile
+                   ( (</>), addExtension, filename, parent, parseAbsDir
+                   , parseAbsFile, parseRelDir, parseRelFile, takeDrive
                    , toFilePath
                    )
 import           Path.CheckInstall ( warnInstallSearchPathIssues )
@@ -111,19 +113,19 @@                    , mkGhcPackagePath )
 import           Stack.Prelude
 import           Stack.Setup.Installed
-                   ( Tool (..), extraDirs, filterTools, getCompilerVersion
-                   , installDir, listInstalled, markInstalled, tempInstallDir
+                   ( Tool (..), filterTools, getCompilerVersion, installDir
+                   , listInstalled, markInstalled, tempInstallDir,toolExtraDirs
                    , toolString, unmarkInstalled
                    )
 import           Stack.SourceMap
                    ( actualFromGhc, globalsFromDump, pruneGlobals )
 import           Stack.Storage.User ( loadCompilerPaths, saveCompilerPaths )
-import           Stack.Types.Build.Exception ( BuildException (..) )
+import           Stack.Types.Build.Exception ( BuildPrettyException (..) )
 import           Stack.Types.BuildConfig
                    ( BuildConfig (..), HasBuildConfig (..), projectRootL
                    , wantedCompilerVersionL
                    )
-import           Stack.Types.BuildOpts ( BuildOptsCLI (..) )
+import           Stack.Types.BuildOptsCLI ( BuildOptsCLI (..) )
 import           Stack.Types.Compiler
                    ( ActualCompiler (..), CompilerException (..)
                    , CompilerRepository (..), WhichCompiler (..)
@@ -146,7 +148,8 @@                    , packageDatabaseDeps, packageDatabaseExtra
                    , packageDatabaseLocal
                    )
-import           Stack.Types.EnvSettings ( EnvSettings (..), minimalEnvSettings )
+import           Stack.Types.EnvSettings
+                   ( EnvSettings (..), minimalEnvSettings )
 import           Stack.Types.ExtraDirs ( ExtraDirs (..) )
 import           Stack.Types.FileDigestCache ( newFileDigestCache )
 import           Stack.Types.GHCDownloadInfo ( GHCDownloadInfo (..) )
@@ -159,7 +162,8 @@                    , platformOnlyRelDir )
 import           Stack.Types.Runner ( HasRunner (..) )
 import           Stack.Types.SetupInfo ( SetupInfo (..) )
-import           Stack.Types.SourceMap ( SMActual (..), SourceMap (..) )
+import           Stack.Types.SourceMap
+                   ( SMActual (..), SMWanted (..), SourceMap (..) )
 import           Stack.Types.Version
                    ( VersionCheck, stackMinorVersion, stackVersion )
 import           Stack.Types.VersionedDownloadInfo
@@ -167,7 +171,7 @@ import qualified System.Directory as D
 import           System.Environment ( getExecutablePath, lookupEnv )
 import           System.IO.Error ( isPermissionError )
-import           System.FilePath ( searchPathSeparator, takeDrive )
+import           System.FilePath ( searchPathSeparator )
 import qualified System.FilePath as FP
 import           System.Permissions ( setFileExecutable )
 import           System.Uname ( getRelease )
@@ -199,7 +203,7 @@   | InvalidGhcAt !(Path Abs File) !SomeException
   | ExecutableNotFound ![Path Abs File]
   | SandboxedCompilerNotFound ![String] ![Path Abs Dir]
-  | UnsupportedSetupCombo !OS !Arch
+  | UnsupportedSetupCombo !OS !Arch !StyleDoc !StyleDoc !(Path Abs Dir)
   | MissingDependencies ![String]
   | UnknownCompilerVersion
       !(Set.Set Text)
@@ -311,17 +315,34 @@                 \tools, see the output of"
          , style Shell (flow "stack uninstall") <> "."
          ]
-  pretty (UnsupportedSetupCombo os arch) =
+  pretty (UnsupportedSetupCombo os arch tool toolDirAdvice programsDir) =
     "[S-1852]"
     <> line
     <> fillSep
-         [ flow "Stack does not know how to install GHC for the combination of \
-                \operating system"
-         , fromString $ show os
+         [ flow "Stack does not know how to install"
+         , tool
+         , flow "for the combination of operating system"
+         , style Shell (pretty os)
          , "and architecture"
-         , fromString $ show arch <> "."
+         , style Shell (pretty arch) <> "."
          , flow "Please install manually."
          ]
+    <> blankLine
+    <> fillSep
+         [ flow "To install manually the version of"
+         , tool <> ","
+         , flow "its root directory should be named"
+         , toolDirAdvice
+         , flow "and the directory should be accompanied by a file with the \
+                \same name and extension"
+         , style File ".installed"
+         , flow "(which marks the"
+         , tool
+         , flow "version as installed). Both items should be located in the \
+                \subdirectory for the specified platform in Stack's directory \
+                \for local tools"
+         , parens (pretty programsDir) <> "."
+         ]
   pretty (MissingDependencies tools) =
     "[S-2126]"
     <> line
@@ -550,14 +571,14 @@     <> line
     <> fillSep
          [ flow "Binary upgrade not yet supported on OS:"
-         , fromString (show os) <> "."
+         , pretty os <> "."
          ]
   pretty (BinaryUpgradeOnArchUnsupported arch) =
     "[S-3249]"
     <> line
     <> fillSep
          [ flow "Binary upgrade not yet supported on architecture:"
-         , fromString (show arch) <> "."
+         , pretty arch <> "."
          ]
   pretty (ExistingMSYS2NotDeleted destDir e) =
     "[S-4230]"
@@ -592,23 +613,23 @@   "https://raw.githubusercontent.com/commercialhaskell/stackage-content/master/stack/stack-setup-2.yaml"
 
 data SetupOpts = SetupOpts
-  { soptsInstallIfMissing :: !Bool
-  , soptsUseSystem :: !Bool
+  { installIfMissing :: !Bool
+  , useSystem :: !Bool
     -- ^ Should we use a system compiler installation, if available?
-  , soptsWantedCompiler :: !WantedCompiler
-  , soptsCompilerCheck :: !VersionCheck
-  , soptsStackYaml :: !(Maybe (Path Abs File))
+  , wantedCompiler :: !WantedCompiler
+  , compilerCheck :: !VersionCheck
+  , stackYaml :: !(Maybe (Path Abs File))
     -- ^ If we got the desired GHC version from that file
-  , soptsForceReinstall :: !Bool
-  , soptsSanityCheck :: !Bool
+  , forceReinstall :: !Bool
+  , sanityCheck :: !Bool
     -- ^ Run a sanity check on the selected GHC
-  , soptsSkipGhcCheck :: !Bool
+  , skipGhcCheck :: !Bool
     -- ^ Don't check for a compatible GHC version/architecture
-  , soptsSkipMsys :: !Bool
+  , skipMsys :: !Bool
     -- ^ Do not use a custom msys installation on Windows
-  , soptsResolveMissingGHC :: !(Maybe Text)
+  , resolveMissingGHC :: !(Maybe StyleDoc)
     -- ^ Message shown to user for how to resolve the missing GHC
-  , soptsGHCBindistURL :: !(Maybe String)
+  , ghcBindistURL :: !(Maybe String)
     -- ^ Alternate GHC binary distribution (requires custom GHCVariant)
   }
   deriving Show
@@ -618,66 +639,67 @@ setupEnv ::
      NeedTargets
   -> BuildOptsCLI
-  -> Maybe Text -- ^ Message to give user when necessary GHC is not available
+  -> Maybe StyleDoc
+     -- ^ Message to give user when necessary GHC is not available.
   -> RIO BuildConfig EnvConfig
-setupEnv needTargets boptsCLI mResolveMissingGHC = do
+setupEnv needTargets buildOptsCLI mResolveMissingGHC = do
   config <- view configL
   bc <- view buildConfigL
-  let stackYaml = bcStackYaml bc
+  let stackYaml = bc.stackYaml
   platform <- view platformL
   wcVersion <- view wantedCompilerVersionL
   wanted <- view wantedCompilerVersionL
   actual <- either throwIO pure $ wantedToActual wanted
   let wc = actual^.whichCompilerL
   let sopts = SetupOpts
-        { soptsInstallIfMissing = configInstallGHC config
-        , soptsUseSystem = configSystemGHC config
-        , soptsWantedCompiler = wcVersion
-        , soptsCompilerCheck = configCompilerCheck config
-        , soptsStackYaml = Just stackYaml
-        , soptsForceReinstall = False
-        , soptsSanityCheck = False
-        , soptsSkipGhcCheck = configSkipGHCCheck config
-        , soptsSkipMsys = configSkipMsys config
-        , soptsResolveMissingGHC = mResolveMissingGHC
-        , soptsGHCBindistURL = Nothing
+        { installIfMissing = config.installGHC
+        , useSystem = config.systemGHC
+        , wantedCompiler = wcVersion
+        , compilerCheck = config.compilerCheck
+        , stackYaml = Just stackYaml
+        , forceReinstall = False
+        , sanityCheck = False
+        , skipGhcCheck = config.skipGHCCheck
+        , skipMsys = config.skipMsys
+        , resolveMissingGHC = mResolveMissingGHC
+        , ghcBindistURL = Nothing
         }
 
   (compilerPaths, ghcBin) <- ensureCompilerAndMsys sopts
-  let compilerVer = cpCompilerVersion compilerPaths
+  let compilerVer = compilerPaths.compilerVersion
 
   -- Modify the initial environment to include the GHC path, if a local GHC
   -- is being used
   menv0 <- view processContextL
   env <- either throwM (pure . removeHaskellEnvVars)
            $ augmentPathMap
-               (map toFilePath $ edBins ghcBin)
+               (map toFilePath ghcBin.bins)
                (view envVarsL menv0)
   menv <- mkProcessContext env
 
   logDebug "Resolving package entries"
 
   (sourceMap, sourceMapHash) <- runWithGHC menv compilerPaths $ do
-    smActual <- actualFromGhc (bcSMWanted bc) compilerVer
-    let actualPkgs = Map.keysSet (smaDeps smActual) <>
-                     Map.keysSet (smaProject smActual)
+    smActual <- actualFromGhc bc.smWanted compilerVer
+    let actualPkgs = Map.keysSet smActual.deps <>
+                     Map.keysSet smActual.project
         prunedActual = smActual
-          { smaGlobal = pruneGlobals (smaGlobal smActual) actualPkgs }
-        haddockDeps = shouldHaddockDeps (configBuild config)
-    targets <- parseTargets needTargets haddockDeps boptsCLI prunedActual
-    sourceMap <- loadSourceMap targets boptsCLI smActual
-    sourceMapHash <- hashSourceMapData boptsCLI sourceMap
+          { globals = pruneGlobals smActual.globals actualPkgs }
+        haddockDeps = shouldHaddockDeps config.build
+    targets <- parseTargets needTargets haddockDeps buildOptsCLI prunedActual
+    sourceMap <- loadSourceMap targets buildOptsCLI smActual
+    sourceMapHash <- hashSourceMapData buildOptsCLI sourceMap
     pure (sourceMap, sourceMapHash)
 
   fileDigestCache <- newFileDigestCache
 
   let envConfig0 = EnvConfig
-        { envConfigBuildConfig = bc
-        , envConfigBuildOptsCLI = boptsCLI
-        , envConfigFileDigestCache = fileDigestCache
-        , envConfigSourceMap = sourceMap
-        , envConfigSourceMapHash = sourceMapHash
-        , envConfigCompilerPaths = compilerPaths
+        { buildConfig = bc
+        , buildOptsCLI
+        , fileDigestCache
+        , sourceMap
+        , sourceMapHash
+        , compilerPaths
         }
 
   -- extra installation bin directories
@@ -689,12 +711,12 @@     either throwM pure $ augmentPath (toFilePath <$> mkDirs True) mpath
 
   deps <- runRIO envConfig0 packageDatabaseDeps
-  runWithGHC menv compilerPaths $ createDatabase (cpPkg compilerPaths) deps
+  runWithGHC menv compilerPaths $ createDatabase compilerPaths.pkg deps
   localdb <- runRIO envConfig0 packageDatabaseLocal
-  runWithGHC menv compilerPaths $ createDatabase (cpPkg compilerPaths) localdb
+  runWithGHC menv compilerPaths $ createDatabase compilerPaths.pkg localdb
   extras <- runReaderT packageDatabaseExtra envConfig0
   let mkGPP locals =
-        mkGhcPackagePath locals localdb deps extras $ cpGlobalDB compilerPaths
+        mkGhcPackagePath locals localdb deps extras compilerPaths.globalDB
 
   distDir <- runReaderT distRelativeDir envConfig0 >>= canonicalizePath
 
@@ -713,23 +735,23 @@             eo <- mkProcessContext
               $ Map.insert
                   "PATH"
-                  (if esIncludeLocals es then localsPath else depsPath)
-              $ (if esIncludeGhcPackagePath es
+                  (if es.includeLocals then localsPath else depsPath)
+              $ (if es.includeGhcPackagePath
                    then
                      Map.insert
                        (ghcPkgPathEnvVar wc)
-                       (mkGPP (esIncludeLocals es))
+                       (mkGPP es.includeLocals)
                    else id)
 
-              $ (if esStackExe es
+              $ (if es.stackExe
                    then Map.insert "STACK_EXE" (T.pack executablePath)
                    else id)
 
-              $ (if esLocaleUtf8 es
+              $ (if es.localeUtf8
                    then Map.union utf8EnvVars
                    else id)
 
-              $ case (soptsSkipMsys sopts, platform) of
+              $ case (sopts.skipMsys, platform) of
                   (False, Platform Cabal.I386   Cabal.Windows) ->
                     Map.insert "MSYSTEM" "MINGW32"
                   (False, Platform Cabal.X86_64 Cabal.Windows) ->
@@ -737,7 +759,7 @@                   _ -> id
 
               -- See https://github.com/commercialhaskell/stack/issues/3444
-              $ case (esKeepGhcRts es, mGhcRtsEnvVar) of
+              $ case (es.keepGhcRts, mGhcRtsEnvVar) of
                   (True, Just ghcRts) -> Map.insert "GHCRTS" (T.pack ghcRts)
                   _ -> id
 
@@ -747,7 +769,7 @@                   "HASKELL_PACKAGE_SANDBOX"
                   (T.pack $ toFilePathNoTrailingSep deps)
               $ Map.insert "HASKELL_PACKAGE_SANDBOXES"
-                  (T.pack $ if esIncludeLocals es
+                  (T.pack $ if es.includeLocals
                     then intercalate [searchPathSeparator]
                            [ toFilePathNoTrailingSep localdb
                            , toFilePathNoTrailingSep deps
@@ -765,7 +787,7 @@                 -- are ignored, since we're setting up our
                 -- own package databases. See
                 -- https://github.com/commercialhaskell/stack/issues/4706
-              $ (case cpCompilerVersion compilerPaths of
+              $ (case compilerPaths.compilerVersion of
                   ACGhc version | version >= mkVersion [8, 4, 4] ->
                     Map.insert "GHC_ENVIRONMENT" "-"
                   _ -> id)
@@ -778,18 +800,18 @@ 
   envOverride <- liftIO $ getProcessContext' minimalEnvSettings
   pure EnvConfig
-    { envConfigBuildConfig = bc
-        { bcConfig = addIncludeLib ghcBin
-                   $ set processContextL envOverride
-                     (view configL bc)
-            { configProcessContextSettings = getProcessContext'
+    { buildConfig = bc
+        { config = addIncludeLib ghcBin
+                 $ set processContextL envOverride
+                    (view configL bc)
+            { processContextSettings = getProcessContext'
             }
         }
-    , envConfigBuildOptsCLI = boptsCLI
-    , envConfigFileDigestCache = fileDigestCache
-    , envConfigSourceMap = sourceMap
-    , envConfigSourceMapHash = sourceMapHash
-    , envConfigCompilerPaths = compilerPaths
+    , buildOptsCLI
+    , fileDigestCache
+    , sourceMap
+    , sourceMapHash
+    , compilerPaths
     }
 
 -- | A modified env which we know has an installed compiler on the PATH.
@@ -799,39 +821,39 @@ insideL = lens (\(WithGHC _ x) -> x) (\(WithGHC cp _) -> WithGHC cp)
 
 instance HasLogFunc env => HasLogFunc (WithGHC env) where
-  logFuncL = insideL.logFuncL
+  logFuncL = insideL . logFuncL
 
 instance HasRunner env => HasRunner (WithGHC env) where
-  runnerL = insideL.runnerL
+  runnerL = insideL . runnerL
 
 instance HasProcessContext env => HasProcessContext (WithGHC env) where
-  processContextL = insideL.processContextL
+  processContextL = insideL . processContextL
 
 instance HasStylesUpdate env => HasStylesUpdate (WithGHC env) where
-  stylesUpdateL = insideL.stylesUpdateL
+  stylesUpdateL = insideL . stylesUpdateL
 
 instance HasTerm env => HasTerm (WithGHC env) where
-  useColorL = insideL.useColorL
-  termWidthL = insideL.termWidthL
+  useColorL = insideL . useColorL
+  termWidthL = insideL . termWidthL
 
 instance HasPantryConfig env => HasPantryConfig (WithGHC env) where
-  pantryConfigL = insideL.pantryConfigL
+  pantryConfigL = insideL . pantryConfigL
 
 instance HasConfig env => HasPlatform (WithGHC env) where
-  platformL = configL.platformL
+  platformL = configL . platformL
   {-# INLINE platformL #-}
-  platformVariantL = configL.platformVariantL
+  platformVariantL = configL . platformVariantL
   {-# INLINE platformVariantL #-}
 
 instance HasConfig env => HasGHCVariant (WithGHC env) where
-  ghcVariantL = configL.ghcVariantL
+  ghcVariantL = configL . ghcVariantL
   {-# INLINE ghcVariantL #-}
 
 instance HasConfig env => HasConfig (WithGHC env) where
-  configL = insideL.configL
+  configL = insideL . configL
 
 instance HasBuildConfig env => HasBuildConfig (WithGHC env) where
-  buildConfigL = insideL.buildConfigL
+  buildConfigL = insideL . buildConfigL
 
 instance HasCompiler (WithGHC env) where
   compilerPathsL = to (\(WithGHC cp _) -> cp)
@@ -860,39 +882,39 @@ insideMSYSL = lens (\(WithMSYS x) -> x) (\(WithMSYS _) -> WithMSYS)
 
 instance HasLogFunc env => HasLogFunc (WithMSYS env) where
-  logFuncL = insideMSYSL.logFuncL
+  logFuncL = insideMSYSL . logFuncL
 
 instance HasRunner env => HasRunner (WithMSYS env) where
-  runnerL = insideMSYSL.runnerL
+  runnerL = insideMSYSL . runnerL
 
 instance HasProcessContext env => HasProcessContext (WithMSYS env) where
-  processContextL = insideMSYSL.processContextL
+  processContextL = insideMSYSL . processContextL
 
 instance HasStylesUpdate env => HasStylesUpdate (WithMSYS env) where
-  stylesUpdateL = insideMSYSL.stylesUpdateL
+  stylesUpdateL = insideMSYSL . stylesUpdateL
 
 instance HasTerm env => HasTerm (WithMSYS env) where
-  useColorL = insideMSYSL.useColorL
-  termWidthL = insideMSYSL.termWidthL
+  useColorL = insideMSYSL . useColorL
+  termWidthL = insideMSYSL . termWidthL
 
 instance HasPantryConfig env => HasPantryConfig (WithMSYS env) where
-  pantryConfigL = insideMSYSL.pantryConfigL
+  pantryConfigL = insideMSYSL . pantryConfigL
 
 instance HasConfig env => HasPlatform (WithMSYS env) where
-  platformL = configL.platformL
+  platformL = configL . platformL
   {-# INLINE platformL #-}
-  platformVariantL = configL.platformVariantL
+  platformVariantL = configL . platformVariantL
   {-# INLINE platformVariantL #-}
 
 instance HasConfig env => HasGHCVariant (WithMSYS env) where
-  ghcVariantL = configL.ghcVariantL
+  ghcVariantL = configL . ghcVariantL
   {-# INLINE ghcVariantL #-}
 
 instance HasConfig env => HasConfig (WithMSYS env) where
-  configL = insideMSYSL.configL
+  configL = insideMSYSL . configL
 
 instance HasBuildConfig env => HasBuildConfig (WithMSYS env) where
-  buildConfigL = insideMSYSL.buildConfigL
+  buildConfigL = insideMSYSL . buildConfigL
 
 -- | Set up a modified environment which includes the modified PATH that MSYS2
 -- can be found on.
@@ -909,7 +931,7 @@     Just msysPaths -> do
       envars <- either throwM pure $
         augmentPathMap
-          (map toFilePath $ edBins msysPaths)
+          (map toFilePath msysPaths.bins)
           (view envVarsL pc0)
       mkProcessContext envars
   let envMsys
@@ -928,20 +950,20 @@   -> BuildOptsCLI
   -> RIO env EnvConfig
 rebuildEnv envConfig needTargets haddockDeps boptsCLI = do
-  let bc = envConfigBuildConfig envConfig
-      cp = envConfigCompilerPaths envConfig
-      compilerVer = smCompiler $ envConfigSourceMap envConfig
+  let bc = envConfig.buildConfig
+      cp = envConfig.compilerPaths
+      compilerVer = envConfig.sourceMap.compiler
   runRIO (WithGHC cp bc) $ do
-    smActual <- actualFromGhc (bcSMWanted bc) compilerVer
+    smActual <- actualFromGhc bc.smWanted compilerVer
     let actualPkgs =
-          Map.keysSet (smaDeps smActual) <> Map.keysSet (smaProject smActual)
+          Map.keysSet smActual.deps <> Map.keysSet smActual.project
         prunedActual = smActual
-          { smaGlobal = pruneGlobals (smaGlobal smActual) actualPkgs }
+          { globals = pruneGlobals smActual.globals actualPkgs }
     targets <- parseTargets needTargets haddockDeps boptsCLI prunedActual
     sourceMap <- loadSourceMap targets boptsCLI smActual
     pure $ envConfig
-      { envConfigSourceMap = sourceMap
-      , envConfigBuildOptsCLI = boptsCLI
+      { sourceMap = sourceMap
+      , buildOptsCLI = boptsCLI
       }
 
 -- | Some commands (script, ghci and exec) set targets dynamically
@@ -953,21 +975,20 @@   -> RIO env a
 withNewLocalBuildTargets targets f = do
   envConfig <- view envConfigL
-  haddockDeps <- view $ configL.to configBuild.to shouldHaddockDeps
-  let boptsCLI = envConfigBuildOptsCLI envConfig
-  envConfig' <- rebuildEnv envConfig NeedTargets haddockDeps $
-                boptsCLI {boptsCLITargets = targets}
+  haddockDeps <- view $ configL . to (.build) . to shouldHaddockDeps
+  let boptsCLI = envConfig.buildOptsCLI
+  envConfig' <-
+    rebuildEnv envConfig NeedTargets haddockDeps $ boptsCLI
+      { targetsCLI = targets}
   local (set envConfigL envConfig') f
 
 -- | Add the include and lib paths to the given Config
 addIncludeLib :: ExtraDirs -> Config -> Config
-addIncludeLib (ExtraDirs _bins includes libs) config = config
-  { configExtraIncludeDirs =
-      configExtraIncludeDirs config ++
-      map toFilePathNoTrailingSep includes
-  , configExtraLibDirs =
-      configExtraLibDirs config ++
-      map toFilePathNoTrailingSep libs
+addIncludeLib extraDirs config = config
+  { extraIncludeDirs =
+      config.extraIncludeDirs ++ map toFilePathNoTrailingSep extraDirs.includes
+  , extraLibDirs =
+      config.extraLibDirs ++ map toFilePathNoTrailingSep extraDirs.libs
   }
 
 -- | Ensure both the compiler and the msys toolchain are installed and
@@ -979,8 +1000,8 @@ ensureCompilerAndMsys sopts = do
   getSetupInfo' <- memoizeRef getSetupInfo
   mmsys2Tool <- ensureMsys sopts getSetupInfo'
-  mmsysPaths <- maybe (pure Nothing) (fmap Just . extraDirs) mmsys2Tool
-  actual <- either throwIO pure $ wantedToActual $ soptsWantedCompiler sopts
+  mmsysPaths <- maybe (pure Nothing) (fmap Just . toolExtraDirs) mmsys2Tool
+  actual <- either throwIO pure $ wantedToActual sopts.wantedCompiler
   didWarn <- warnUnsupportedCompiler $ getGhcVersion actual
   -- Modify the initial environment to include the MSYS2 path, if MSYS2 is being
   -- used
@@ -992,8 +1013,12 @@   pure (cp, paths)
 
 -- | See <https://github.com/commercialhaskell/stack/issues/4246>
-warnUnsupportedCompiler :: HasTerm env => Version -> RIO env Bool
-warnUnsupportedCompiler ghcVersion =
+warnUnsupportedCompiler ::
+     (HasConfig env, HasTerm env)
+  => Version
+  -> RIO env Bool
+warnUnsupportedCompiler ghcVersion = do
+  notifyIfGhcUntested <- view $ configL . to (.notifyIfGhcUntested)
   if
     | ghcVersion < mkVersion [7, 8] -> do
         prettyWarnL
@@ -1006,10 +1031,10 @@           , style Url "https://github.com/commercialhaskell/stack/issues/648" <> "."
           ]
         pure True
-    | ghcVersion >= mkVersion [9, 7] -> do
+    | ghcVersion >= mkVersion [9, 9] && notifyIfGhcUntested -> do
         prettyWarnL
-          [ flow "Stack has not been tested with GHC versions 9.8 and above, and \
-                 \using"
+          [ flow "Stack has not been tested with GHC versions 9.10 and above, \
+                 \and using"
           , fromString (versionString ghcVersion) <> ","
           , flow "this may fail."
           ]
@@ -1020,15 +1045,15 @@ 
 -- | See <https://github.com/commercialhaskell/stack/issues/4246>
 warnUnsupportedCompilerCabal ::
-     HasTerm env
+     (HasConfig env, HasTerm env)
   => CompilerPaths
   -> Bool -- ^ already warned about GHC?
   -> RIO env ()
 warnUnsupportedCompilerCabal cp didWarn = do
   unless didWarn $
-    void $ warnUnsupportedCompiler $ getGhcVersion $ cpCompilerVersion cp
-  let cabalVersion = cpCabalVersion cp
-
+    void $ warnUnsupportedCompiler $ getGhcVersion cp.compilerVersion
+  let cabalVersion = cp.cabalVersion
+  notifyIfCabalUntested <- view $ configL . to (.notifyIfCabalUntested)
   if
     | cabalVersion < mkVersion [1, 24, 0] -> do
         prettyWarnL
@@ -1040,11 +1065,20 @@                  \resolver. Acceptable resolvers: lts-7.0/nightly-2016-05-26 \
                  \or later."
           ]
-    | cabalVersion >= mkVersion [3, 11] ->
+    | cabalVersion < mkVersion [2, 2, 0] -> do
         prettyWarnL
-          [ flow "Stack has not been tested with Cabal versions 3.12 and above, \
-                 \but version"
+          [ flow "Stack's support of Cabal versions below 2.2.0.0 is \
+                 \deprecated and may be removed from the next version of \
+                 \ Stack. Cabal version"
           , fromString (versionString cabalVersion)
+          , flow "was found. Consider using a resolver that is \
+                 \lts-12.0 or later or nightly-2018-03-13 or later."
+          ]
+    | cabalVersion >= mkVersion [3, 11] && notifyIfCabalUntested ->
+        prettyWarnL
+          [ flow "Stack has not been tested with Cabal versions 3.12 and \
+                 \above, but version"
+          , fromString (versionString cabalVersion)
           , flow "was found, this may fail."
           ]
     | otherwise -> pure ()
@@ -1058,30 +1092,34 @@   -> RIO env (Maybe Tool)
 ensureMsys sopts getSetupInfo' = do
   platform <- view platformL
-  localPrograms <- view $ configL.to configLocalPrograms
+  localPrograms <- view $ configL . to (.localPrograms)
   installed <- listInstalled localPrograms
 
   case platform of
-    Platform _ Cabal.Windows | not (soptsSkipMsys sopts) ->
+    Platform _ Cabal.Windows | not sopts.skipMsys ->
       case getInstalledTool installed (mkPackageName "msys2") (const True) of
         Just tool -> pure (Just tool)
         Nothing
-          | soptsInstallIfMissing sopts -> do
+          | sopts.installIfMissing -> do
               si <- runMemoized getSetupInfo'
-              osKey <- getOSKey platform
+              let msysDir = fillSep
+                    [ style Dir "msys2-yyyymmdd"
+                    , flow "(where yyyymmdd is the date-based version)"
+                    ]
+              osKey <- getOSKey "MSYS2" msysDir
               config <- view configL
               VersionedDownloadInfo version info <-
-                case Map.lookup osKey $ siMsys2 si of
+                case Map.lookup osKey si.msys2 of
                   Just x -> pure x
                   Nothing -> prettyThrowIO $ MSYS2NotFound osKey
               let tool = Tool (PackageIdentifier (mkPackageName "msys2") version)
               Just <$> downloadAndInstallTool
-                         (configLocalPrograms config)
+                         config.localPrograms
                          info
                          tool
                          (installMsys2Windows si)
           | otherwise -> do
-              prettyWarnS "Continuing despite missing tool: msys2"
+              prettyWarnS "Continuing despite missing tool: MSYS2."
               pure Nothing
     _ -> pure Nothing
 
@@ -1093,9 +1131,9 @@   -> RIO env (Tool, CompilerBuild)
 installGhcBindist sopts getSetupInfo' installed = do
   Platform expectedArch _ <- view platformL
-  let wanted = soptsWantedCompiler sopts
+  let wanted = sopts.wantedCompiler
       isWanted =
-        isWantedCompiler (soptsCompilerCheck sopts) (soptsWantedCompiler sopts)
+        isWantedCompiler sopts.compilerCheck sopts.wantedCompiler
   config <- view configL
   ghcVariant <- view ghcVariantL
   wc <- either throwIO (pure . whichCompiler) $ wantedToActual wanted
@@ -1112,7 +1150,7 @@           pure (getInstalledTool installed ghcPkgName (isWanted . ACGhc), ghcBuild)
   let existingCompilers = concatMap
         (\(installedCompiler, compilerBuild) ->
-          case (installedCompiler, soptsForceReinstall sopts) of
+          case (installedCompiler, sopts.forceReinstall) of
             (Just tool, False) -> [(tool, compilerBuild)]
             _ -> [])
         possibleCompilers
@@ -1122,34 +1160,45 @@   case existingCompilers of
     (tool, build_):_ -> pure (tool, build_)
     []
-      | soptsInstallIfMissing sopts -> do
+      | sopts.installIfMissing -> do
           si <- runMemoized getSetupInfo'
           downloadAndInstallPossibleCompilers
             (map snd possibleCompilers)
             si
-            (soptsWantedCompiler sopts)
-            (soptsCompilerCheck sopts)
-            (soptsGHCBindistURL sopts)
+            sopts.wantedCompiler
+            sopts.compilerCheck
+            sopts.ghcBindistURL
       | otherwise -> do
-          let suggestion = fromMaybe
-                (mconcat
-                  [ "To install the correct GHC into "
-                  , T.pack (toFilePath (configLocalPrograms config))
-                  , ", try running 'stack setup' or use the '--install-ghc' flag."
-                  , " To use your system GHC installation, run \
-                    \'stack config set system-ghc --global true', \
-                    \or use the '--system-ghc' flag."
-                  ])
-                (soptsResolveMissingGHC sopts)
-          throwM $ CompilerVersionMismatch
+          let suggestion =
+                fromMaybe defaultSuggestion sopts.resolveMissingGHC
+              defaultSuggestion = fillSep
+                [ flow "To install the correct version of GHC into the \
+                       \subdirectory for the specified platform in Stack's \
+                       \directory for local tools"
+                , parens (pretty config.localPrograms) <> ","
+                , flow "try running"
+                , style Shell (flow "stack setup")
+                , flow "or use the"
+                , style Shell "--install-ghc"
+                , flow "flag. To use your system GHC installation, run"
+                ,    style
+                       Shell
+                       (flow "stack config set system-ghc --global true")
+                  <> ","
+                , flow "or use the"
+                , style Shell "--system-ghc"
+                , "flag."
+                ]
+
+          prettyThrowM $ CompilerVersionMismatch
             Nothing -- FIXME ((\(x, y, _) -> (x, y)) <$> msystem)
-            (soptsWantedCompiler sopts, expectedArch)
+            (sopts.wantedCompiler, expectedArch)
             ghcVariant
             (case possibleCompilers of
               [] -> CompilerBuildStandard
               (_, compilerBuild):_ -> compilerBuild)
-            (soptsCompilerCheck sopts)
-            (soptsStackYaml sopts)
+            sopts.compilerCheck
+            sopts.stackYaml
             suggestion
 
 -- | Ensure compiler is installed.
@@ -1159,7 +1208,7 @@   -> Memoized SetupInfo
   -> RIO (WithMSYS env) (CompilerPaths, ExtraDirs)
 ensureCompiler sopts getSetupInfo' = do
-  let wanted = soptsWantedCompiler sopts
+  let wanted = sopts.wantedCompiler
   wc <- either throwIO (pure . whichCompiler) $ wantedToActual wanted
 
   hook <- ghcInstallHook
@@ -1171,13 +1220,13 @@   Platform expectedArch _ <- view platformL
 
   let canUseCompiler cp
-        | soptsSkipGhcCheck sopts = pure cp
-        | not $ isWanted $ cpCompilerVersion cp =
+        | sopts.skipGhcCheck = pure cp
+        | not $ isWanted cp.compilerVersion =
             prettyThrowIO UnwantedCompilerVersion
-        | cpArch cp /= expectedArch = prettyThrowIO UnwantedArchitecture
+        | cp.arch /= expectedArch = prettyThrowIO UnwantedArchitecture
         | otherwise = pure cp
       isWanted =
-        isWantedCompiler (soptsCompilerCheck sopts) (soptsWantedCompiler sopts)
+        isWantedCompiler sopts.compilerCheck sopts.wantedCompiler
 
   let checkCompiler :: Path Abs File -> RIO (WithMSYS env) (Maybe CompilerPaths)
       checkCompiler compiler = do
@@ -1194,7 +1243,7 @@           Right cp -> pure $ Just cp
 
   mcp <-
-    if | soptsUseSystem sopts -> do
+    if | sopts.useSystem -> do
           logDebug "Getting system compiler version"
           runConduit $
             sourceSystemCompilers wanted .|
@@ -1209,12 +1258,12 @@     Nothing -> ensureSandboxedCompiler sopts getSetupInfo'
     Just cp -> do
       let paths = ExtraDirs
-            { edBins = [parent $ cpCompiler cp]
-            , edInclude = [], edLib = []
+            { bins = [parent cp.compiler]
+            , includes = []
+            , libs = []
             }
       pure (cp, paths)
 
-
 -- | Runs @STACK_ROOT\/hooks\/ghc-install.sh@.
 --
 -- Reads and possibly validates the output of the process as the GHC binary and
@@ -1226,7 +1275,7 @@   -> RIO env (Maybe (Path Abs File))
 runGHCInstallHook sopts hook = do
   logDebug "Getting hook installed compiler version"
-  let wanted = soptsWantedCompiler sopts
+  let wanted = sopts.wantedCompiler
   menv0 <- view processContextL
   menv <- mkProcessContext (Map.union (wantedCompilerToEnv wanted) $
     removeHaskellEnvVars (view envVarsL menv0))
@@ -1236,7 +1285,7 @@       let ghcPath = stripNewline . TL.unpack . TL.decodeUtf8With T.lenientDecode $ out
       case parseAbsFile ghcPath of
         Just compiler -> do
-          when (soptsSanityCheck sopts) $ sanityCheck compiler
+          when sopts.sanityCheck $ sanityCheck compiler
           logDebug ("Using GHC compiler at: " <> fromString (toFilePath compiler))
           pure (Just compiler)
         Nothing -> do
@@ -1280,31 +1329,31 @@   -> Memoized SetupInfo
   -> RIO (WithMSYS env) (CompilerPaths, ExtraDirs)
 ensureSandboxedCompiler sopts getSetupInfo' = do
-  let wanted = soptsWantedCompiler sopts
+  let wanted = sopts.wantedCompiler
   -- List installed tools
   config <- view configL
-  let localPrograms = configLocalPrograms config
+  let localPrograms = config.localPrograms
   installed <- listInstalled localPrograms
   logDebug $
        "Installed tools: \n - "
     <> mconcat (intersperse "\n - " (map (fromString . toolString) installed))
   (compilerTool, compilerBuild) <-
-    case soptsWantedCompiler sopts of
+    case sopts.wantedCompiler of
      -- shall we build GHC from source?
      WCGhcGit commitId flavour ->
        buildGhcFromSource
          getSetupInfo'
          installed
-         (configCompilerRepository config)
+         config.compilerRepository
          commitId
          flavour
      _ -> installGhcBindist sopts getSetupInfo' installed
-  paths <- extraDirs compilerTool
+  paths <- toolExtraDirs compilerTool
 
   wc <- either throwIO (pure . whichCompiler) $ wantedToActual wanted
   menv0 <- view processContextL
   m <- either throwM pure
-     $ augmentPathMap (toFilePath <$> edBins paths) (view envVarsL menv0)
+     $ augmentPathMap (toFilePath <$> paths.bins) (view envVarsL menv0)
   menv <- mkProcessContext (removeHaskellEnvVars m)
 
   names <-
@@ -1318,10 +1367,10 @@   -- sandbox. This led to a specific issue on Windows with GHC 9.0.1. See
   -- https://gitlab.haskell.org/ghc/ghc/-/issues/20074. Instead, now, we look
   -- on the paths specified only.
-  let loop [] = prettyThrowIO $ SandboxedCompilerNotFound names (edBins paths)
+  let loop [] = prettyThrowIO $ SandboxedCompilerNotFound names paths.bins
       loop (x:xs) = do
         res <- liftIO $
-          D.findExecutablesInDirectories (map toFilePath (edBins paths)) x
+          D.findExecutablesInDirectories (map toFilePath paths.bins) x
         case res of
           [] -> loop xs
           compiler:rest -> do
@@ -1343,7 +1392,7 @@     -- Run this here to ensure that the sanity check uses the modified
     -- environment, otherwise we may infect GHC_PACKAGE_PATH and break sanity
     -- checks.
-    when (soptsSanityCheck sopts) $ sanityCheck compiler
+    when sopts.sanityCheck $ sanityCheck compiler
 
     pure compiler
 
@@ -1357,7 +1406,7 @@   -> Bool
   -> Path Abs File -- ^ executable filepath
   -> RIO env CompilerPaths
-pathsFromCompiler wc compilerBuild isSandboxed compiler =
+pathsFromCompiler wc build sandboxed compiler =
   withCache $ handleAny onErr $ do
     let dir = toFilePath $ parent compiler
 
@@ -1400,10 +1449,10 @@     haddock <- findHelper $
                \case
                   Ghc -> ["haddock", "haddock-ghc"]
-    infobs <- proc (toFilePath compiler) ["--info"]
+    ghcInfo <- proc (toFilePath compiler) ["--info"]
             $ fmap (toStrictBytes . fst) . readProcess_
     infotext <-
-      case decodeUtf8' infobs of
+      case decodeUtf8' ghcInfo of
         Left e -> prettyThrowIO $ GHCInfoNotValidUTF8 e
         Right info -> pure info
     infoPairs :: [(String, String)] <-
@@ -1425,7 +1474,7 @@             Nothing ->
               prettyThrowIO $ GHCInfoTargetPlatformInvalid targetPlatform
             Just arch -> pure arch
-    compilerVer <-
+    compilerVersion <-
       case wc of
         Ghc ->
           case Map.lookup "Project version" infoMap of
@@ -1433,7 +1482,7 @@               prettyWarnS "Key 'Project version' not found in GHC info."
               getCompilerVersion wc compiler
             Just versionString' -> ACGhc <$> parseVersionThrowing versionString'
-    globaldb <-
+    globalDB <-
       case eglobaldb of
         Left e -> do
           prettyWarn $
@@ -1448,30 +1497,30 @@         Right x -> pure x
 
     globalDump <- withProcessContext menv $ globalsFromDump pkg
-    cabalPkgVer <-
+    cabalVersion <-
       case Map.lookup cabalPackageName globalDump of
         Nothing -> prettyThrowIO $ CabalNotFound compiler
-        Just dp -> pure $ pkgVersion $ dpPackageIdent dp
+        Just dp -> pure $ pkgVersion dp.packageIdent
 
     pure CompilerPaths
-      { cpBuild = compilerBuild
-      , cpArch = arch
-      , cpSandboxed = isSandboxed
-      , cpCompilerVersion = compilerVer
-      , cpCompiler = compiler
-      , cpPkg = pkg
-      , cpInterpreter = interpreter
-      , cpHaddock = haddock
-      , cpCabalVersion = cabalPkgVer
-      , cpGlobalDB = globaldb
-      , cpGhcInfo = infobs
-      , cpGlobalDump = globalDump
+      { build
+      , arch
+      , sandboxed
+      , compilerVersion
+      , compiler
+      , pkg
+      , interpreter
+      , haddock
+      , cabalVersion
+      , globalDB
+      , ghcInfo
+      , globalDump
       }
  where
   onErr = throwIO . PrettyException . InvalidGhcAt compiler
 
   withCache inner = do
-    eres <- tryAny $ loadCompilerPaths compiler compilerBuild isSandboxed
+    eres <- tryAny $ loadCompilerPaths compiler build sandboxed
     mres <-
       case eres of
         Left e -> do
@@ -1514,7 +1563,7 @@         -- withRepo is guaranteed to set workingDirL, so let's get it
         mcwd <- traverse parseAbsDir =<< view workingDirL
         cwd <- maybe (throwIO WorkingDirectoryInvalidBug) pure mcwd
-        let threads = configJobs config
+        let threads = config.jobs
             relFileHadrianStackDotYaml' = toFilePath relFileHadrianStackDotYaml
             ghcBootScriptPath = cwd </> ghcBootScript
             boot = if osIsWindows
@@ -1527,8 +1576,8 @@               -- If a resolver is specified on the command line, Stack will
               -- apply it. This allows the resolver specified in Hadrian's
               -- stack.yaml file to be overridden.
-              args' = maybe args addResolver (configResolver config)
-              addResolver resolver = "--resolver=" <> show resolver : args
+              args' = maybe args addResolver config.resolver
+              addResolver resolver = "--snapshot=" <> show resolver : args
             happy = stack ["install", "happy"]
             alex = stack ["install", "alex"]
             -- Executed in the Stack environment, because GHC is required.
@@ -1590,19 +1639,19 @@           [bindist] -> do
             let bindist' = T.pack (toFilePath bindist)
                 dlinfo = DownloadInfo
-                          { downloadInfoUrl           = bindist'
-                            -- we can specify a filepath instead of a URL
-                          , downloadInfoContentLength = Nothing
-                          , downloadInfoSha1          = Nothing
-                          , downloadInfoSha256        = Nothing
-                          }
+                  { url = bindist'
+                    -- we can specify a filepath instead of a URL
+                  , contentLength = Nothing
+                  , sha1 = Nothing
+                  , sha256 = Nothing
+                  }
                 ghcdlinfo = GHCDownloadInfo mempty mempty dlinfo
                 installer
                    | osIsWindows = installGHCWindows
                    | otherwise   = installGHCPosix ghcdlinfo
             si <- runMemoized getSetupInfo'
             _ <- downloadAndInstallTool
-              (configLocalPrograms config)
+              config.localPrograms
               dlinfo
               compilerTool
               (installer si)
@@ -1616,7 +1665,7 @@ getGhcBuilds :: HasConfig env => RIO env [CompilerBuild]
 getGhcBuilds = do
   config <- view configL
-  case configGHCBuild config of
+  case config.ghcBuild of
     Just ghcBuild -> pure [ghcBuild]
     Nothing -> determineGhcBuild
  where
@@ -1679,7 +1728,7 @@               libD = fromString (toFilePath lib)
               libT = T.pack (toFilePath lib)
             hasMatches lib dirs = do
-              matches <- filterM (doesFileExist .(</> lib)) dirs
+              matches <- filterM (doesFileExist . (</> lib)) dirs
               case matches of
                 [] ->
                      logDebug
@@ -1809,7 +1858,7 @@   config <- view configL
   containerPlatformDir <-
     runReaderT platformOnlyRelDir (containerPlatform,PlatformVariantNone)
-  let programsPath = configLocalProgramsBase config </> containerPlatformDir
+  let programsPath = config.localProgramsBase </> containerPlatformDir
       tool = Tool (PackageIdentifier (mkPackageName "stack") stackVersion)
   stackExeDir <- installDir programsPath tool
   let stackExePath = stackExeDir </> relFileStack
@@ -1858,8 +1907,8 @@ getSetupInfo :: HasConfig env => RIO env SetupInfo
 getSetupInfo = do
   config <- view configL
-  let inlineSetupInfo = configSetupInfoInline config
-      locations' = configSetupInfoLocations config
+  let inlineSetupInfo = config.setupInfoInline
+      locations' = config.setupInfoLocations
       locations = if null locations' then [defaultSetupInfoYaml] else locations'
 
   resolvedSetupInfos <- mapM loadSetupInfo locations
@@ -1927,21 +1976,21 @@       pure
         ( version
         , GHCDownloadInfo mempty mempty DownloadInfo
-            { downloadInfoUrl = T.pack bindistURL
-            , downloadInfoContentLength = Nothing
-            , downloadInfoSha1 = Nothing
-            , downloadInfoSha256 = Nothing
+            { url = T.pack bindistURL
+            , contentLength = Nothing
+            , sha1 = Nothing
+            , sha256 = Nothing
             }
         )
     _ -> do
       ghcKey <- getGhcKey ghcBuild
-      case Map.lookup ghcKey $ siGHCs si of
+      case Map.lookup ghcKey si.ghcByVersion of
         Nothing -> throwM $ UnknownOSKey ghcKey
         Just pairs_ ->
           getWantedCompilerInfo ghcKey versionCheck wanted ACGhc pairs_
   config <- view configL
   let installer =
-        case configPlatform config of
+        case config.platform of
           Platform _ Cabal.Windows -> installGHCWindows
           _ -> installGHCPosix downloadInfo
   prettyInfo $
@@ -1960,8 +2009,8 @@     ("ghc" ++ ghcVariantSuffix ghcVariant ++ compilerBuildSuffix ghcBuild)
   let tool = Tool $ PackageIdentifier ghcPkgName selectedVersion
   downloadAndInstallTool
-    (configLocalPrograms config)
-    (gdiDownloadInfo downloadInfo)
+    config.localPrograms
+    downloadInfo.downloadInfo
     tool
     (installer si)
 
@@ -2041,20 +2090,38 @@       Right r -> pure (r, b)
 
 getGhcKey ::
-     (MonadReader env m, HasPlatform env, HasGHCVariant env, MonadThrow m)
+     (HasBuildConfig env, HasGHCVariant env)
   => CompilerBuild
-  -> m Text
+  -> RIO env Text
 getGhcKey ghcBuild = do
   ghcVariant <- view ghcVariantL
-  platform <- view platformL
-  osKey <- getOSKey platform
-  pure $
-       osKey
-    <> T.pack (ghcVariantSuffix ghcVariant)
-    <> T.pack (compilerBuildSuffix ghcBuild)
+  wantedComiler <- view $ buildConfigL . to (.smWanted.compiler)
+  ghcVersion <- case wantedComiler of
+        WCGhc version -> pure version
+        WCGhcjs _ _ ->  throwIO GhcjsNotSupported
+        WCGhcGit _ _ -> throwIO DownloadAndInstallCompilerError
+  let variantSuffix = ghcVariantSuffix ghcVariant
+      buildSuffix = compilerBuildSuffix ghcBuild
+      ghcDir = style Dir $ mconcat
+        [ "ghc"
+        , fromString variantSuffix
+        , fromString buildSuffix
+        , "-"
+        , fromString $ versionString ghcVersion
+        ]
+  osKey <- getOSKey "GHC" ghcDir
+  pure $ osKey <> T.pack variantSuffix <> T.pack buildSuffix
 
-getOSKey :: (MonadThrow m) => Platform -> m Text
-getOSKey platform =
+getOSKey ::
+     (HasConfig env, HasPlatform env)
+  => StyleDoc
+     -- ^ Description of the tool that is being set up.
+  -> StyleDoc
+     -- ^ Description of the root directory of the tool.
+  -> RIO env Text
+getOSKey tool toolDir = do
+  programsDir <- view $ configL . to (.localPrograms)
+  platform <- view platformL
   case platform of
     Platform I386                  Cabal.Linux   -> pure "linux32"
     Platform X86_64                Cabal.Linux   -> pure "linux64"
@@ -2071,7 +2138,8 @@     Platform Sparc                 Cabal.Linux   -> pure "linux-sparc"
     Platform AArch64               Cabal.OSX     -> pure "macosx-aarch64"
     Platform AArch64               Cabal.FreeBSD -> pure "freebsd-aarch64"
-    Platform arch os -> prettyThrowM $ UnsupportedSetupCombo os arch
+    Platform arch os ->
+      prettyThrowM $ UnsupportedSetupCombo os arch tool toolDir programsDir
 
 downloadOrUseLocal ::
      (HasTerm env, HasBuildConfig env)
@@ -2094,12 +2162,12 @@       pure (root </> path)
     _ -> prettyThrowIO $ URLInvalid url
  where
-  url = T.unpack $ downloadInfoUrl downloadInfo
+  url = T.unpack downloadInfo.url
   warnOnIgnoredChecks = do
     let DownloadInfo
-          { downloadInfoContentLength = contentLength
-          , downloadInfoSha1 = sha1
-          , downloadInfoSha256 = sha256
+          { contentLength
+          , sha1
+          , sha256
           } = downloadInfo
     when (isJust contentLength) $
       prettyWarnS
@@ -2136,7 +2204,7 @@   pure (localPath, archiveType)
 
  where
-  url = T.unpack $ downloadInfoUrl downloadInfo
+  url = T.unpack downloadInfo.url
   extension = loop url
    where
     loop fp
@@ -2210,7 +2278,7 @@ 
   dir <- expectSingleUnpackedDir archiveFile tempDir
 
-  mOverrideGccPath <- view $ configL.to configOverrideGccPath
+  mOverrideGccPath <- view $ configL . to (.overrideGccPath)
 
   -- The make application uses the CC environment variable to configure the
   -- program for compiling C programs
@@ -2220,14 +2288,14 @@ 
   -- Data.Map.union provides a left-biased union, so mGccEnv will prevail
   let ghcConfigureEnv =
-        fromMaybe Map.empty mGccEnv `Map.union` gdiConfigureEnv downloadInfo
+        fromMaybe Map.empty mGccEnv `Map.union` downloadInfo.configureEnv
 
   logSticky "Configuring GHC ..."
   runStep "configuring" dir
     ghcConfigureEnv
     (toFilePath $ dir </> relFileConfigure)
     ( ("--prefix=" ++ toFilePath destDir)
-    : map T.unpack (gdiConfigureOpts downloadInfo)
+    : map T.unpack downloadInfo.configureOpts
     )
 
   logSticky "Installing GHC ..."
@@ -2347,7 +2415,7 @@   -- filepath length of more than 260 characters, which can be problematic for
   -- 7-Zip even if Long Filepaths are enabled on Windows.
   let tmpName = "stack-tmp"
-      destDrive = fromJust $ parseAbsDir $ takeDrive $ fromAbsDir destDir
+      destDrive = takeDrive destDir
   ensureDir (parent destDir)
   withRunInIO $ \run ->
   -- We use a temporary directory in the same drive as that of 'destDir' to
@@ -2384,11 +2452,11 @@   => SetupInfo
   -> RIO env (Path Abs Dir -> Path Abs File -> m ())
 setup7z si = do
-  dir <- view $ configL.to configLocalPrograms
+  dir <- view $ configL . to (.localPrograms)
   ensureDir dir
   let exeDestination = dir </> relFile7zexe
       dllDestination = dir </> relFile7zdll
-  case (siSevenzDll si, siSevenzExe si) of
+  case (si.sevenzDll, si.sevenzExe) of
     (Just sevenzDll, Just sevenzExe) -> do
       _ <- downloadOrUseLocal "7z.dll" sevenzDll dllDestination
       exePath <- downloadOrUseLocal "7z.exe" sevenzExe exeDestination
@@ -2440,7 +2508,7 @@   -> Path Abs File -- ^ destination
   -> RIO env ()
 chattyDownload label downloadInfo path = do
-  let url = downloadInfoUrl downloadInfo
+  let url = downloadInfo.url
   req <- parseUrlThrow $ T.unpack url
   logSticky $
        "Preparing to download "
@@ -2453,8 +2521,8 @@     <> fromString (toFilePath path)
     <> " ..."
   hashChecks <- fmap catMaybes $ forM
-    [ ("sha1",   HashCheck SHA1,   downloadInfoSha1)
-    , ("sha256", HashCheck SHA256, downloadInfoSha256)
+    [ ("sha1", HashCheck SHA1, (.sha1))
+    , ("sha256", HashCheck SHA256, (.sha256))
     ]
     $ \(name, constr, getter) ->
       case getter downloadInfo of
@@ -2477,7 +2545,7 @@     then logStickyDone ("Downloaded " <> display label <> ".")
     else logStickyDone ("Already downloaded " <> display label <> ".")
  where
-  mtotalSize = downloadInfoContentLength downloadInfo
+  mtotalSize = downloadInfo.contentLength
 
 -- | Perform a basic sanity check of GHC
 sanityCheck ::
@@ -2664,8 +2732,8 @@     -- ^ Information on the latest available binary for the current platforms.
 
 data HaskellStackOrg = HaskellStackOrg
-  { hsoUrl :: !Text
-  , hsoVersion :: !Version
+  { url :: !Text
+  , version :: !Version
   }
   deriving Show
 
@@ -2736,8 +2804,8 @@                   -- We found a valid URL, let's use it!
                   Right version -> do
                     let hso = HaskellStackOrg
-                                { hsoUrl = loc
-                                , hsoVersion = version
+                                { url = loc
+                                , version
                                 }
                     logDebug $
                          "Downloading from haskellstack.org: "
@@ -2892,7 +2960,7 @@         String url <- KeyMap.lookup "browser_download_url" o
         Just url
     findMatch _ _ = Nothing
-  findArchive (SRIHaskellStackOrg hso) _ = pure $ hsoUrl hso
+  findArchive (SRIHaskellStackOrg hso) _ = pure hso.url
 
   handleTarball :: Path Abs File -> Bool -> T.Text -> IO ()
   handleTarball tmpFile isWindows url = do
@@ -3024,4 +3092,4 @@   String rawName <- KeyMap.lookup "name" o
   -- drop the "v" at the beginning of the name
   parseVersion $ T.unpack (T.drop 1 rawName)
-getDownloadVersion (SRIHaskellStackOrg hso) = Just $ hsoVersion hso
+getDownloadVersion (SRIHaskellStackOrg hso) = Just hso.version
src/Stack/Setup/Installed.hs view
@@ -1,8 +1,9 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE DataKinds         #-}
-{-# LANGUAGE LambdaCase        #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE ViewPatterns      #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE LambdaCase          #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE ViewPatterns        #-}
 
 module Stack.Setup.Installed
   ( getCompilerVersion
@@ -14,7 +15,7 @@   , toolNameString
   , parseToolText
   , filterTools
-  , extraDirs
+  , toolExtraDirs
   , installDir
   , tempInstallDir
   ) where
@@ -132,47 +133,47 @@   isValid c = c == '.' || isDigit c
 
 -- | Binary directories for the given installed package
-extraDirs :: HasConfig env => Tool -> RIO env ExtraDirs
-extraDirs tool = do
+toolExtraDirs :: HasConfig env => Tool -> RIO env ExtraDirs
+toolExtraDirs tool = do
   config <- view configL
-  dir <- installDir (configLocalPrograms config) tool
-  case (configPlatform config, toolNameString tool) of
+  dir <- installDir config.localPrograms tool
+  case (config.platform, toolNameString tool) of
     (Platform _ Cabal.Windows, isGHC -> True) -> pure mempty
-      { edBins =
+      { bins =
           [ dir </> relDirBin
           , dir </> relDirMingw </> relDirBin
           ]
       }
     (Platform Cabal.I386 Cabal.Windows, "msys2") -> pure mempty
-      { edBins =
+      { bins =
           [ dir </> relDirMingw32 </> relDirBin
           , dir </> relDirUsr </> relDirBin
           , dir </> relDirUsr </> relDirLocal </> relDirBin
           ]
-      , edInclude =
+      , includes =
           [ dir </> relDirMingw32 </> relDirInclude
           ]
-      , edLib =
+      , libs =
           [ dir </> relDirMingw32 </> relDirLib
           , dir </> relDirMingw32 </> relDirBin
           ]
       }
     (Platform Cabal.X86_64 Cabal.Windows, "msys2") -> pure mempty
-      { edBins =
+      { bins =
           [ dir </> relDirMingw64 </> relDirBin
           , dir </> relDirUsr </> relDirBin
           , dir </> relDirUsr </> relDirLocal </> relDirBin
           ]
-      , edInclude =
+      , includes =
           [ dir </> relDirMingw64 </> relDirInclude
           ]
-      , edLib =
+      , libs =
           [ dir </> relDirMingw64 </> relDirLib
           , dir </> relDirMingw64 </> relDirBin
           ]
       }
     (_, isGHC -> True) -> pure mempty
-      { edBins =
+      { bins =
           [ dir </> relDirBin
           ]
       }
src/Stack/SetupCmd.hs view
@@ -1,7 +1,9 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE DataKinds         #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- | Types and functions related to Stack's @setup@ command.
 module Stack.SetupCmd
@@ -24,26 +26,26 @@ 
 -- | Type representing command line options for the @stack setup@ command.
 data SetupCmdOpts = SetupCmdOpts
-  { scoCompilerVersion :: !(Maybe WantedCompiler)
-  , scoForceReinstall  :: !Bool
-  , scoGHCBindistURL   :: !(Maybe String)
-  , scoGHCJSBootOpts   :: ![String]
-  , scoGHCJSBootClean  :: !Bool
+  { compilerVersion :: !(Maybe WantedCompiler)
+  , forceReinstall  :: !Bool
+  , ghcBindistUrl   :: !(Maybe String)
+  , ghcjsBootOpts   :: ![String]
+  , ghcjsBootClean  :: !Bool
   }
 
 -- | Function underlying the @stack setup@ command.
 setupCmd :: SetupCmdOpts -> RIO Runner ()
-setupCmd sco@SetupCmdOpts{..} = withConfig YesReexec $ do
-  installGHC <- view $ configL.to configInstallGHC
+setupCmd sco = withConfig YesReexec $ do
+  installGHC <- view $ configL . to (.installGHC)
   if installGHC
     then
        withBuildConfig $ do
        (wantedCompiler, compilerCheck, mstack) <-
-         case scoCompilerVersion of
+         case sco.compilerVersion of
            Just v -> pure (v, MatchMinor, Nothing)
            Nothing -> (,,)
              <$> view wantedCompilerVersionL
-             <*> view (configL.to configCompilerCheck)
+             <*> view (configL . to (.compilerCheck))
              <*> (Just <$> view stackYamlL)
        setup sco wantedCompiler compilerCheck mstack
     else
@@ -62,20 +64,20 @@   -> VersionCheck
   -> Maybe (Path Abs File)
   -> RIO env ()
-setup SetupCmdOpts{..} wantedCompiler compilerCheck mstack = do
-  Config{..} <- view configL
-  sandboxedGhc <- cpSandboxed . fst <$> ensureCompilerAndMsys SetupOpts
-    { soptsInstallIfMissing = True
-    , soptsUseSystem = configSystemGHC && not scoForceReinstall
-    , soptsWantedCompiler = wantedCompiler
-    , soptsCompilerCheck = compilerCheck
-    , soptsStackYaml = mstack
-    , soptsForceReinstall = scoForceReinstall
-    , soptsSanityCheck = True
-    , soptsSkipGhcCheck = False
-    , soptsSkipMsys = configSkipMsys
-    , soptsResolveMissingGHC = Nothing
-    , soptsGHCBindistURL = scoGHCBindistURL
+setup sco wantedCompiler compilerCheck stackYaml = do
+  config <- view configL
+  sandboxedGhc <- (.sandboxed) . fst <$> ensureCompilerAndMsys SetupOpts
+    { installIfMissing = True
+    , useSystem = config.systemGHC && not sco.forceReinstall
+    , wantedCompiler
+    , compilerCheck
+    , stackYaml
+    , forceReinstall = sco.forceReinstall
+    , sanityCheck = True
+    , skipGhcCheck = False
+    , skipMsys = config.skipMsys
+    , resolveMissingGHC = Nothing
+    , ghcBindistURL = sco.ghcBindistUrl
     }
   let compiler = case wantedCompiler of
         WCGhc _ -> "GHC"
src/Stack/SourceMap.hs view
@@ -1,5 +1,7 @@-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 module Stack.SourceMap
   ( mkProjectPackage
@@ -56,21 +58,22 @@   => PrintWarnings
   -> ResolvedPath Dir
   -> Bool
+     -- ^ Should Haddock documentation be built for the package?
   -> RIO env ProjectPackage
-mkProjectPackage printWarnings dir buildHaddocks = do
-  (gpd, name, cabalfp) <-
-    loadCabalFilePath (Just stackProgName') (resolvedAbsolute dir)
+mkProjectPackage printWarnings resolvedDir buildHaddocks = do
+  (gpd, name, cabalFP) <-
+    loadCabalFilePath (Just stackProgName') (resolvedAbsolute resolvedDir)
   pure ProjectPackage
-    { ppCabalFP = cabalfp
-    , ppResolvedDir = dir
-    , ppCommon =
+    { cabalFP
+    , resolvedDir
+    , projectCommon =
         CommonPackage
-          { cpGPD = gpd printWarnings
-          , cpName = name
-          , cpFlags = mempty
-          , cpGhcOptions = mempty
-          , cpCabalConfigOpts = mempty
-          , cpHaddocks = buildHaddocks
+          { gpd = gpd printWarnings
+          , name
+          , flags = mempty
+          , ghcOptions = mempty
+          , cabalConfigOpts = mempty
+          , buildHaddocks
           }
     }
 
@@ -79,60 +82,62 @@ additionalDepPackage ::
      forall env. (HasPantryConfig env, HasLogFunc env, HasProcessContext env)
   => Bool
+     -- ^ Should Haddock documentation be built for the package?
   -> PackageLocation
   -> RIO env DepPackage
-additionalDepPackage buildHaddocks pl = do
-  (name, gpdio) <-
-    case pl of
+additionalDepPackage buildHaddocks location = do
+  (name, gpd) <-
+    case location of
       PLMutable dir -> do
-        (gpdio, name, _cabalfp) <-
+        (gpd, name, _cabalfp) <-
           loadCabalFilePath (Just stackProgName') (resolvedAbsolute dir)
-        pure (name, gpdio NoPrintWarnings)
+        pure (name, gpd NoPrintWarnings)
       PLImmutable pli -> do
         let PackageIdentifier name _ = packageLocationIdent pli
         run <- askRunInIO
         pure (name, run $ loadCabalFileImmutable pli)
   pure DepPackage
-    { dpLocation = pl
-    , dpHidden = False
-    , dpFromSnapshot = NotFromSnapshot
-    , dpCommon =
+    { location
+    , hidden = False
+    , fromSnapshot = NotFromSnapshot
+    , depCommon =
         CommonPackage
-          { cpGPD = gpdio
-          , cpName = name
-          , cpFlags = mempty
-          , cpGhcOptions = mempty
-          , cpCabalConfigOpts = mempty
-          , cpHaddocks = buildHaddocks
+          { gpd
+          , name
+          , flags = mempty
+          , ghcOptions = mempty
+          , cabalConfigOpts = mempty
+          , buildHaddocks
           }
     }
 
 snapToDepPackage ::
      forall env. (HasPantryConfig env, HasLogFunc env, HasProcessContext env)
   => Bool
+     -- ^ Should Haddock documentation be built for the package?
   -> PackageName
   -> SnapshotPackage
   -> RIO env DepPackage
-snapToDepPackage buildHaddocks name SnapshotPackage{..} = do
+snapToDepPackage buildHaddocks name sp = do
   run <- askRunInIO
   pure DepPackage
-    { dpLocation = PLImmutable spLocation
-    , dpHidden = spHidden
-    , dpFromSnapshot = FromSnapshot
-    , dpCommon =
+    { location = PLImmutable sp.spLocation
+    , hidden = sp.spHidden
+    , fromSnapshot = FromSnapshot
+    , depCommon =
         CommonPackage
-          { cpGPD = run $ loadCabalFileImmutable spLocation
-          , cpName = name
-          , cpFlags = spFlags
-          , cpGhcOptions = spGhcOptions
-          , cpCabalConfigOpts = [] -- No spCabalConfigOpts, not present in snapshots
-          , cpHaddocks = buildHaddocks
+          { gpd = run $ loadCabalFileImmutable sp.spLocation
+          , name
+          , flags = sp.spFlags
+          , ghcOptions = sp.spGhcOptions
+          , cabalConfigOpts = [] -- No spCabalConfigOpts, not present in snapshots
+          , buildHaddocks
           }
     }
 
 loadVersion :: MonadIO m => CommonPackage -> m Version
 loadVersion common = do
-  gpd <- liftIO $ cpGPD common
+  gpd <- liftIO common.gpd
   pure (pkgVersion $ PD.package $ PD.packageDescription gpd)
 
 getPLIVersion :: PackageLocationImmutable -> Version
@@ -146,9 +151,9 @@   -> RIO env (Map PackageName DumpedGlobalPackage)
 globalsFromDump pkgexe = do
   let pkgConduit =    conduitDumpPackage
-                   .| CL.foldMap (\dp -> Map.singleton (dpGhcPkgId dp) dp)
+                   .| CL.foldMap (\dp -> Map.singleton dp.ghcPkgId dp)
       toGlobals ds =
-        Map.fromList $ map (pkgName . dpPackageIdent &&& id) $ Map.elems ds
+        Map.fromList $ map (pkgName . (.packageIdent) &&& id) $ Map.elems ds
   toGlobals <$> ghcPkgDump pkgexe [] pkgConduit
 
 globalsFromHints ::
@@ -173,14 +178,14 @@   => SMWanted
   -> ActualCompiler
   -> RIO env (SMActual DumpedGlobalPackage)
-actualFromGhc smw ac = do
-  globals <- view $ compilerPathsL.to cpGlobalDump
+actualFromGhc smw compiler = do
+  globals <- view $ compilerPathsL . to (.globalDump)
   pure
     SMActual
-      { smaCompiler = ac
-      , smaProject = smwProject smw
-      , smaDeps = smwDeps smw
-      , smaGlobal = globals
+      { compiler
+      , project = smw.project
+      , deps = smw.deps
+      , globals
       }
 
 actualFromHints ::
@@ -188,14 +193,14 @@   => SMWanted
   -> ActualCompiler
   -> RIO env (SMActual GlobalPackageVersion)
-actualFromHints smw ac = do
-  globals <- globalsFromHints (actualToWanted ac)
+actualFromHints smw compiler = do
+  globals <- globalsFromHints (actualToWanted compiler)
   pure
     SMActual
-      { smaCompiler = ac
-      , smaProject = smwProject smw
-      , smaDeps = smwDeps smw
-      , smaGlobal = Map.map GlobalPackageVersion globals
+      { compiler
+      , project = smw.project
+      , deps = smw.deps
+      , globals = Map.map GlobalPackageVersion globals
       }
 
 -- | Simple cond check for boot packages - checks only OS and Arch
@@ -232,15 +237,15 @@   -> Map PackageName DepPackage
   -> m (Maybe UnusedFlags)
 getUnusedPackageFlags (name, userFlags) source prj deps =
-  let maybeCommon =     fmap ppCommon (Map.lookup name prj)
-                    <|> fmap dpCommon (Map.lookup name deps)
+  let maybeCommon =     fmap (.projectCommon) (Map.lookup name prj)
+                    <|> fmap (.depCommon) (Map.lookup name deps)
   in  case maybeCommon of
         -- Package is not available as project or dependency
         Nothing ->
           pure $ Just $ UFNoPackage source name
         -- Package exists, let's check if the flags are defined
         Just common -> do
-          gpd <- liftIO $ cpGPD common
+          gpd <- liftIO common.gpd
           let pname = pkgName $ PD.package $ PD.packageDescription gpd
               pkgFlags = Set.fromList $ map PD.flagName $ PD.genPackageFlags gpd
               unused = Map.keysSet $ Map.withoutKeys userFlags pkgFlags
@@ -256,13 +261,13 @@   -> Map PackageName GlobalPackage
 pruneGlobals globals deps =
   let (prunedGlobals, keptGlobals) =
-        partitionReplacedDependencies globals (pkgName . dpPackageIdent)
-          dpGhcPkgId dpDepends deps
-  in  Map.map (GlobalPackage . pkgVersion . dpPackageIdent) keptGlobals <>
+        partitionReplacedDependencies globals (pkgName . (.packageIdent))
+          (.ghcPkgId) (.depends) deps
+  in  Map.map (GlobalPackage . pkgVersion . (.packageIdent)) keptGlobals <>
       Map.map ReplacedGlobalPackage prunedGlobals
 
 getCompilerInfo :: (HasConfig env, HasCompiler env) => RIO env Builder
-getCompilerInfo = view $ compilerPathsL.to (byteString . cpGhcInfo)
+getCompilerInfo = view $ compilerPathsL . to (byteString . (.ghcInfo))
 
 immutableLocSha :: PackageLocationImmutable -> Builder
 immutableLocSha = byteString . treeKeyToBs . locationTreeKey
@@ -280,22 +285,25 @@   => RawSnapshotLocation
   -> PrintWarnings
   -> Bool
+     -- ^ Should Haddock documentation be build for the package?
   -> RIO env (SnapshotCandidate env)
 loadProjectSnapshotCandidate loc printWarnings buildHaddocks = do
   debugRSL <- view rslInLogL
-  (snapshot, _, _) <- loadAndCompleteSnapshotRaw' debugRSL loc Map.empty Map.empty
-  deps <- Map.traverseWithKey (snapToDepPackage False) (snapshotPackages snapshot)
+  (snapshot, _, _) <-
+    loadAndCompleteSnapshotRaw' debugRSL loc Map.empty Map.empty
+  deps <-
+    Map.traverseWithKey (snapToDepPackage False) (snapshotPackages snapshot)
   let wc = snapshotCompiler snapshot
   globals <- Map.map GlobalPackageVersion <$> globalsFromHints wc
   pure $ \projectPackages -> do
-    prjPkgs <- fmap Map.fromList . for projectPackages $ \resolved -> do
+    project <- fmap Map.fromList . for projectPackages $ \resolved -> do
       pp <- mkProjectPackage printWarnings resolved buildHaddocks
-      pure (cpName $ ppCommon pp, pp)
+      pure (pp.projectCommon.name, pp)
     compiler <- either throwIO pure $ wantedToActual $ snapshotCompiler snapshot
     pure
       SMActual
-        { smaCompiler = compiler
-        , smaProject = prjPkgs
-        , smaDeps = Map.difference deps prjPkgs
-        , smaGlobal = globals
+        { compiler
+        , project
+        , deps = Map.difference deps project
+        , globals
         }
src/Stack/Storage/Project.hs view
@@ -1,13 +1,13 @@-{-# LANGUAGE NoImplicitPrelude          #-}
-{-# LANGUAGE DataKinds                  #-}
-{-# LANGUAGE DerivingStrategies         #-}
-{-# LANGUAGE GADTs                      #-}
-{-# LANGUAGE OverloadedStrings          #-}
-{-# LANGUAGE QuasiQuotes                #-}
-{-# LANGUAGE RecordWildCards            #-}
-{-# LANGUAGE TemplateHaskell            #-}
-{-# LANGUAGE TypeFamilies               #-}
-{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE NoImplicitPrelude    #-}
+{-# LANGUAGE DataKinds            #-}
+{-# LANGUAGE DerivingStrategies   #-}
+{-# LANGUAGE GADTs                #-}
+{-# LANGUAGE OverloadedRecordDot  #-}
+{-# LANGUAGE OverloadedStrings    #-}
+{-# LANGUAGE QuasiQuotes          #-}
+{-# LANGUAGE TemplateHaskell      #-}
+{-# LANGUAGE TypeFamilies         #-}
+{-# LANGUAGE UndecidableInstances #-}
 {-# OPTIONS_GHC -Wno-unused-top-binds -Wno-identities #-}
 
 -- | Work with SQLite database used for caches across a single project.
@@ -98,7 +98,7 @@   => ReaderT SqlBackend (RIO env) a
   -> RIO env a
 withProjectStorage inner = do
-  storage <- view (buildConfigL . to bcProjectStorage . to unProjectStorage)
+  storage <- view (buildConfigL . to (.projectStorage.projectStorage))
   withStorage_ storage inner
 
 -- | Key used to retrieve configuration or flag cache
@@ -113,28 +113,38 @@      (HasBuildConfig env, HasLogFunc env)
   => Entity ConfigCacheParent
   -> ReaderT SqlBackend (RIO env) ConfigCache
-readConfigCache (Entity parentId ConfigCacheParent {..}) = do
-  let configCachePkgSrc = configCacheParentPkgSrc
-  coDirs <-
-    map (configCacheDirOptionValue . entityVal) <$>
+readConfigCache (Entity parentId configCacheParent) = do
+  let pkgSrc = configCacheParent.configCacheParentPkgSrc
+  pathRelated <-
+    map ((.configCacheDirOptionValue) . entityVal) <$>
     selectList
       [ConfigCacheDirOptionParent ==. parentId]
       [Asc ConfigCacheDirOptionIndex]
-  coNoDirs <-
-    map (configCacheNoDirOptionValue . entityVal) <$>
+  nonPathRelated <-
+    map ((.configCacheNoDirOptionValue) . entityVal) <$>
     selectList
       [ConfigCacheNoDirOptionParent ==. parentId]
       [Asc ConfigCacheNoDirOptionIndex]
-  let configCacheOpts = ConfigureOpts {..}
-  configCacheDeps <-
-    Set.fromList . map (configCacheDepValue . entityVal) <$>
+  let configureOpts = ConfigureOpts
+        { pathRelated
+        , nonPathRelated
+        }
+  deps <-
+    Set.fromList . map ((.configCacheDepValue) . entityVal) <$>
     selectList [ConfigCacheDepParent ==. parentId] []
-  configCacheComponents <-
-    Set.fromList . map (configCacheComponentValue . entityVal) <$>
+  components <-
+    Set.fromList . map ((.configCacheComponentValue) . entityVal) <$>
     selectList [ConfigCacheComponentParent ==. parentId] []
-  let configCachePathEnvVar = configCacheParentPathEnvVar
-  let configCacheHaddock = configCacheParentHaddock
-  pure ConfigCache {..}
+  let pathEnvVar = configCacheParent.configCacheParentPathEnvVar
+  let buildHaddocks = configCacheParent.configCacheParentHaddock
+  pure ConfigCache
+    { configureOpts
+    , deps
+    , components
+    , buildHaddocks
+    , pkgSrc
+    , pathEnvVar
+    }
 
 -- | Load 'ConfigCache' from the database.
 loadConfigCache ::
@@ -146,8 +156,8 @@     mparent <- getBy key
     case mparent of
       Nothing -> pure Nothing
-      Just parentEntity@(Entity _ ConfigCacheParent {..})
-        | configCacheParentActive ->
+      Just parentEntity@(Entity _ configCacheParent)
+        |  configCacheParent.configCacheParentActive ->
             Just <$> readConfigCache parentEntity
         | otherwise -> pure Nothing
 
@@ -168,18 +178,18 @@             ConfigCacheParent
               { configCacheParentDirectory = dir
               , configCacheParentType = type_
-              , configCacheParentPkgSrc = configCachePkgSrc new
+              , configCacheParentPkgSrc = new.pkgSrc
               , configCacheParentActive = True
-              , configCacheParentPathEnvVar = configCachePathEnvVar new
-              , configCacheParentHaddock = configCacheHaddock new
+              , configCacheParentPathEnvVar = new.pathEnvVar
+              , configCacheParentHaddock = new.buildHaddocks
               }
         Just parentEntity@(Entity parentId _) -> do
           old <- readConfigCache parentEntity
           update
             parentId
-            [ ConfigCacheParentPkgSrc =. configCachePkgSrc new
+            [ ConfigCacheParentPkgSrc =. new.pkgSrc
             , ConfigCacheParentActive =. True
-            , ConfigCacheParentPathEnvVar =. configCachePathEnvVar new
+            , ConfigCacheParentPathEnvVar =. new.pathEnvVar
             ]
           pure (parentId, Just old)
     updateList
@@ -187,29 +197,29 @@       ConfigCacheDirOptionParent
       parentId
       ConfigCacheDirOptionIndex
-      (maybe [] (coDirs . configCacheOpts) mold)
-      (coDirs $ configCacheOpts new)
+      (maybe [] (.configureOpts.pathRelated) mold)
+      new.configureOpts.pathRelated
     updateList
       ConfigCacheNoDirOption
       ConfigCacheNoDirOptionParent
       parentId
       ConfigCacheNoDirOptionIndex
-      (maybe [] (coNoDirs . configCacheOpts) mold)
-      (coNoDirs $ configCacheOpts new)
+      (maybe [] (.configureOpts.nonPathRelated) mold)
+      new.configureOpts.nonPathRelated
     updateSet
       ConfigCacheDep
       ConfigCacheDepParent
       parentId
       ConfigCacheDepValue
-      (maybe Set.empty configCacheDeps mold)
-      (configCacheDeps new)
+      (maybe Set.empty (.deps) mold)
+      new.deps
     updateSet
       ConfigCacheComponent
       ConfigCacheComponentParent
       parentId
       ConfigCacheComponentValue
-      (maybe Set.empty configCacheComponents mold)
-      (configCacheComponents new)
+      (maybe Set.empty (.components) mold)
+      new.components
 
 -- | Mark 'ConfigCache' as inactive in the database.
 -- We use a flag instead of deleting the records since, in most cases, the same
src/Stack/Storage/User.hs view
@@ -1,13 +1,14 @@-{-# LANGUAGE NoImplicitPrelude          #-}
-{-# LANGUAGE DataKinds                  #-}
-{-# LANGUAGE DerivingStrategies         #-}
-{-# LANGUAGE GADTs                      #-}
-{-# LANGUAGE OverloadedStrings          #-}
-{-# LANGUAGE QuasiQuotes                #-}
-{-# LANGUAGE RecordWildCards            #-}
-{-# LANGUAGE TemplateHaskell            #-}
-{-# LANGUAGE TypeFamilies               #-}
-{-# LANGUAGE UndecidableInstances       #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DerivingStrategies    #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE GADTs                 #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE QuasiQuotes           #-}
+{-# LANGUAGE TemplateHaskell       #-}
+{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE UndecidableInstances  #-}
 {-# OPTIONS_GHC -Wno-unused-top-binds -Wno-identities #-}
 
 -- | Work with SQLite database used for caches across an entire user account.
@@ -167,7 +168,7 @@   => ReaderT SqlBackend (RIO env) a
   -> RIO env a
 withUserStorage inner = do
-  storage <- view (configL . to configUserStorage . to unUserStorage)
+  storage <- view (configL . to (.userStorage.userStorage))
   withStorage_ storage inner
 
 -- | Key used to retrieve the precompiled cache
@@ -196,15 +197,23 @@                                          , PrecompiledCache Rel))
 readPrecompiledCache key = do
   mparent <- getBy key
-  forM mparent $ \(Entity parentId PrecompiledCacheParent {..}) -> do
-    pcLibrary <- mapM parseRelFile precompiledCacheParentLibrary
-    pcSubLibs <-
-      mapM (parseRelFile . precompiledCacheSubLibValue . entityVal) =<<
+  forM mparent $ \(Entity parentId precompiledCacheParent) -> do
+    library <-
+      mapM parseRelFile precompiledCacheParent.precompiledCacheParentLibrary
+    subLibs <-
+      mapM (parseRelFile . (.precompiledCacheSubLibValue) . entityVal) =<<
       selectList [PrecompiledCacheSubLibParent ==. parentId] []
-    pcExes <-
-      mapM (parseRelFile . precompiledCacheExeValue . entityVal) =<<
+    exes <-
+      mapM (parseRelFile . (.precompiledCacheExeValue) . entityVal) =<<
       selectList [PrecompiledCacheExeParent ==. parentId] []
-    pure (parentId, PrecompiledCache {..})
+    pure
+      ( parentId
+      , PrecompiledCache
+          { library
+          , subLibs
+          , exes
+          }
+      )
 
 -- | Load 'PrecompiledCache' from the database.
 loadPrecompiledCache ::
@@ -231,11 +240,19 @@       )
   new
   = withUserStorage $ do
-      let precompiledCacheParentLibrary = fmap toFilePath (pcLibrary new)
+      let precompiledCacheParentLibrary = fmap toFilePath new.library
       mIdOld <- readPrecompiledCache key
       (parentId, mold) <-
         case mIdOld of
-          Nothing -> (, Nothing) <$> insert PrecompiledCacheParent {..}
+          Nothing -> (, Nothing) <$> insert PrecompiledCacheParent
+            { precompiledCacheParentPlatformGhcDir
+            , precompiledCacheParentCompiler
+            , precompiledCacheParentCabalVersion
+            , precompiledCacheParentPackageKey
+            , precompiledCacheParentOptionsHash
+            , precompiledCacheParentHaddock
+            , precompiledCacheParentLibrary
+            }
           Just (parentId, old) -> do
             update
               parentId
@@ -248,15 +265,15 @@         PrecompiledCacheSubLibParent
         parentId
         PrecompiledCacheSubLibValue
-        (maybe Set.empty (toFilePathSet . pcSubLibs) mold)
-        (toFilePathSet $ pcSubLibs new)
+        (maybe Set.empty (toFilePathSet . (.subLibs)) mold)
+        (toFilePathSet new.subLibs)
       updateSet
         PrecompiledCacheExe
         PrecompiledCacheExeParent
         parentId
         PrecompiledCacheExeValue
-        (maybe Set.empty (toFilePathSet . pcExes) mold)
-        (toFilePathSet $ pcExes new)
+        (maybe Set.empty (toFilePathSet . (.exes)) mold)
+        (toFilePathSet new.exes)
  where
   toFilePathSet = Set.fromList . map toFilePath
 
@@ -268,7 +285,7 @@   -> UTCTime
   -> RIO env (Maybe Bool)
 loadDockerImageExeCache imageId exePath exeTimestamp = withUserStorage $
-  fmap (dockerImageExeCacheCompatible . entityVal) <$>
+  fmap ((.dockerImageExeCacheCompatible) . entityVal) <$>
   getBy (DockerImageExeCacheUnique imageId (toFilePath exePath) exeTimestamp)
 
 -- | Sets the record of whether an executable is compatible with a Docker image
@@ -310,19 +327,21 @@   -> RIO env (Maybe CompilerPaths)
 loadCompilerPaths compiler build sandboxed = do
   mres <- withUserStorage $ getBy $ UniqueCompilerInfo $ toFilePath compiler
-  for mres $ \(Entity _ CompilerCache {..}) -> do
+  for mres $ \(Entity _ compilerCache) -> do
     compilerStatus <- liftIO $ getFileStatus $ toFilePath compiler
     when
-      (  compilerCacheGhcSize /= sizeToInt64 (fileSize compilerStatus)
-      || compilerCacheGhcModified /=
+      (  compilerCache.compilerCacheGhcSize /=
+           sizeToInt64 (fileSize compilerStatus)
+      || compilerCache.compilerCacheGhcModified /=
            timeToInt64 (modificationTime compilerStatus)
       )
       (throwIO CompilerFileMetadataMismatch)
-    globalDbStatus <-
-      liftIO $ getFileStatus $ compilerCacheGlobalDb FP.</> "package.cache"
+    globalDbStatus <- liftIO $
+      getFileStatus $ compilerCache.compilerCacheGlobalDb FP.</> "package.cache"
     when
-      (  compilerCacheGlobalDbCacheSize /= sizeToInt64 (fileSize globalDbStatus)
-      || compilerCacheGlobalDbCacheModified /=
+      (  compilerCache.compilerCacheGlobalDbCacheSize /=
+           sizeToInt64 (fileSize globalDbStatus)
+      || compilerCache.compilerCacheGlobalDbCacheModified /=
            timeToInt64 (modificationTime globalDbStatus)
       )
       (throwIO GlobalPackageCacheFileMetadataMismatch)
@@ -330,34 +349,35 @@     -- We could use parseAbsFile instead of resolveFile' below to bypass some
     -- system calls, at the cost of some really wonky error messages in case
     -- someone screws up their GHC installation
-    pkgexe <- resolveFile' compilerCacheGhcPkgPath
-    runghc <- resolveFile' compilerCacheRunghcPath
-    haddock <- resolveFile' compilerCacheHaddockPath
-    globaldb <- resolveDir' compilerCacheGlobalDb
+    pkg <- GhcPkgExe <$> resolveFile' compilerCache.compilerCacheGhcPkgPath
+    interpreter <- resolveFile' compilerCache.compilerCacheRunghcPath
+    haddock <- resolveFile' compilerCache.compilerCacheHaddockPath
+    globalDB <- resolveDir' compilerCache.compilerCacheGlobalDb
 
-    cabalVersion <- parseVersionThrowing $ T.unpack compilerCacheCabalVersion
+    cabalVersion <- parseVersionThrowing $
+      T.unpack compilerCache.compilerCacheCabalVersion
     globalDump <-
-      case readMaybe $ T.unpack compilerCacheGlobalDump of
+      case readMaybe $ T.unpack compilerCache.compilerCacheGlobalDump of
         Nothing -> throwIO GlobalDumpParseFailure
         Just globalDump -> pure globalDump
     arch <-
-      case simpleParse $ T.unpack compilerCacheArch of
-        Nothing -> throwIO $ CompilerCacheArchitectureInvalid compilerCacheArch
+      case simpleParse $ T.unpack compilerCache.compilerCacheArch of
+        Nothing -> throwIO $
+          CompilerCacheArchitectureInvalid compilerCache.compilerCacheArch
         Just arch -> pure arch
-
     pure CompilerPaths
-      { cpCompiler = compiler
-      , cpCompilerVersion = compilerCacheActualVersion
-      , cpArch = arch
-      , cpBuild = build
-      , cpPkg = GhcPkgExe pkgexe
-      , cpInterpreter = runghc
-      , cpHaddock = haddock
-      , cpSandboxed = sandboxed
-      , cpCabalVersion = cabalVersion
-      , cpGlobalDB = globaldb
-      , cpGhcInfo = compilerCacheInfo
-      , cpGlobalDump = globalDump
+      { compiler
+      , compilerVersion = compilerCache.compilerCacheActualVersion
+      , arch
+      , build
+      , pkg
+      , interpreter
+      , haddock
+      , sandboxed
+      , cabalVersion
+      , globalDB
+      , ghcInfo = compilerCache.compilerCacheInfo
+      , globalDump
       }
 
 -- | Save compiler information. May throw exceptions!
@@ -365,29 +385,28 @@      HasConfig env
   => CompilerPaths
   -> RIO env ()
-saveCompilerPaths CompilerPaths {..} = withUserStorage $ do
-  deleteBy $ UniqueCompilerInfo $ toFilePath cpCompiler
-  compilerStatus <- liftIO $ getFileStatus $ toFilePath cpCompiler
-  globalDbStatus <-
-    liftIO $
-      getFileStatus $ toFilePath $ cpGlobalDB </> $(mkRelFile "package.cache")
-  let GhcPkgExe pkgexe = cpPkg
+saveCompilerPaths cp = withUserStorage $ do
+  deleteBy $ UniqueCompilerInfo $ toFilePath cp.compiler
+  compilerStatus <- liftIO $ getFileStatus $ toFilePath cp.compiler
+  globalDbStatus <- liftIO $
+    getFileStatus $ toFilePath $ cp.globalDB </> $(mkRelFile "package.cache")
+  let GhcPkgExe pkgexe = cp.pkg
   insert_ CompilerCache
-    { compilerCacheActualVersion = cpCompilerVersion
-    , compilerCacheGhcPath = toFilePath cpCompiler
+    { compilerCacheActualVersion = cp.compilerVersion
+    , compilerCacheGhcPath = toFilePath cp.compiler
     , compilerCacheGhcSize = sizeToInt64 $ fileSize compilerStatus
     , compilerCacheGhcModified = timeToInt64 $ modificationTime compilerStatus
     , compilerCacheGhcPkgPath = toFilePath pkgexe
-    , compilerCacheRunghcPath = toFilePath cpInterpreter
-    , compilerCacheHaddockPath = toFilePath cpHaddock
-    , compilerCacheCabalVersion = T.pack $ versionString cpCabalVersion
-    , compilerCacheGlobalDb = toFilePath cpGlobalDB
+    , compilerCacheRunghcPath = toFilePath cp.interpreter
+    , compilerCacheHaddockPath = toFilePath cp.haddock
+    , compilerCacheCabalVersion = T.pack $ versionString cp.cabalVersion
+    , compilerCacheGlobalDb = toFilePath cp.globalDB
     , compilerCacheGlobalDbCacheSize = sizeToInt64 $ fileSize globalDbStatus
     , compilerCacheGlobalDbCacheModified =
         timeToInt64 $ modificationTime globalDbStatus
-    , compilerCacheInfo = cpGhcInfo
-    , compilerCacheGlobalDump = tshow cpGlobalDump
-    , compilerCacheArch = T.pack $ Distribution.Text.display cpArch
+    , compilerCacheInfo = cp.ghcInfo
+    , compilerCacheGlobalDump = tshow cp.globalDump
+    , compilerCacheArch = T.pack $ Distribution.Text.display cp.arch
     }
 
 -- | How many upgrade checks have occurred since the given timestamp?
src/Stack/Types/Build.hs view
@@ -1,6 +1,9 @@-{-# LANGUAGE NoImplicitPrelude          #-}
-{-# LANGUAGE DataKinds                  #-}
-{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- | Build-specific types.
 
@@ -9,9 +12,13 @@   , Installed (..)
   , psVersion
   , Task (..)
+  , taskAnyMissing
   , taskIsTarget
   , taskLocation
+  , taskProvides
   , taskTargetIsMutable
+  , taskTypeLocation
+  , taskTypePackageIdentifier
   , LocalPackage (..)
   , Plan (..)
   , TestOpts (..)
@@ -30,6 +37,9 @@   , toCachePkgSrc
   , FileCacheInfo (..)
   , PrecompiledCache (..)
+  , ExcludeTHLoading (..)
+  , ConvertPathsToAbsolute (..)
+  , KeepOutputOpen (..)
   ) where
 
 import           Data.Aeson ( ToJSON, FromJSON )
@@ -42,18 +52,20 @@                    , PersistValue (PersistText), SqlType (SqlString)
                    )
 import           Path ( parent )
+import qualified RIO.Set as Set
+import           Stack.BuildOpts ( defaultBuildOpts )
 import           Stack.Prelude
 import           Stack.Types.BuildOpts
-                   ( BenchmarkOpts (..), BuildOpts (..), BuildSubset (..)
-                   , FileWatchOpts (..), TestOpts (..), defaultBuildOpts
-                   )
+                   ( BenchmarkOpts (..), BuildOpts (..), TestOpts (..) )
+import           Stack.Types.BuildOptsCLI
+                   ( BuildSubset (..), FileWatchOpts (..) )
 import           Stack.Types.ConfigureOpts ( ConfigureOpts, configureOpts )
 import           Stack.Types.GhcPkgId ( GhcPkgId )
 import           Stack.Types.IsMutable ( IsMutable (..) )
 import           Stack.Types.Package
                    ( FileCacheInfo (..), InstallLocation (..), Installed (..)
                    , LocalPackage (..), Package (..), PackageSource (..)
-                   , psVersion
+                   , packageIdentifier, psVersion
                    )
 
 -- | Package dependency oracle.
@@ -63,7 +75,7 @@ 
 -- | Stored on disk to know whether the files have changed.
 newtype BuildCache = BuildCache
-  { buildCacheTimes :: Map FilePath FileCacheInfo
+  { times :: Map FilePath FileCacheInfo
     -- ^ Modification times of files.
   }
   deriving (Eq, FromJSON, Generic, Show, ToJSON, Typeable)
@@ -72,21 +84,21 @@ 
 -- | Stored on disk to know whether the flags have changed.
 data ConfigCache = ConfigCache
-  { configCacheOpts :: !ConfigureOpts
-    -- ^ All options used for this package.
-  , configCacheDeps :: !(Set GhcPkgId)
+  { configureOpts :: !ConfigureOpts
+    -- ^ All Cabal configure options used for this package.
+  , deps :: !(Set GhcPkgId)
     -- ^ The GhcPkgIds of all of the dependencies. Since Cabal doesn't take
     -- the complete GhcPkgId (only a PackageIdentifier) in the configure
     -- options, just using the previous value is insufficient to know if
     -- dependencies have changed.
-  , configCacheComponents :: !(Set S.ByteString)
+  , components :: !(Set S.ByteString)
     -- ^ The components to be built. It's a bit of a hack to include this in
     -- here, as it's not a configure option (just a build option), but this
     -- is a convenient way to force compilation when the components change.
-  , configCacheHaddock :: !Bool
+  , buildHaddocks :: !Bool
     -- ^ Are haddocks to be built?
-  , configCachePkgSrc :: !CachePkgSrc
-  , configCachePathEnvVar :: !Text
+  , pkgSrc :: !CachePkgSrc
+  , pathEnvVar :: !Text
   -- ^ Value of the PATH env var, see
   -- <https://github.com/commercialhaskell/stack/issues/3138>
   }
@@ -117,32 +129,25 @@ 
 toCachePkgSrc :: PackageSource -> CachePkgSrc
 toCachePkgSrc (PSFilePath lp) =
-  CacheSrcLocal (toFilePath (parent (lpCabalFile lp)))
+  CacheSrcLocal (toFilePath (parent lp.cabalFP))
 toCachePkgSrc PSRemote{} = CacheSrcUpstream
 
--- | A task to perform when building
+-- | A type representing tasks to perform when building.
 data Task = Task
-  { taskProvides        :: !PackageIdentifier -- FIXME turn this into a function on taskType?
-    -- ^ the package/version to be built
-  , taskType            :: !TaskType
-    -- ^ the task type, telling us how to build this
-  , taskConfigOpts      :: !TaskConfigOpts
-  , taskBuildHaddock    :: !Bool
-  , taskPresent         :: !(Map PackageIdentifier GhcPkgId)
-    -- ^ GhcPkgIds of already-installed dependencies
-  , taskAllInOne        :: !Bool
+  { taskType        :: !TaskType
+    -- ^ The task type, telling us how to build this
+  , configOpts      :: !TaskConfigOpts
+    -- ^ A set of the package identifiers of dependencies for which 'GhcPkgId'
+    -- are missing and a function which yields configure options, given a
+    -- dictionary of those identifiers and their 'GhcPkgId'.
+  , buildHaddocks   :: !Bool
+  , present         :: !(Map PackageIdentifier GhcPkgId)
+    -- ^ A dictionary of the package identifiers of already-installed
+    -- dependencies, and their 'GhcPkgId'.
+  , allInOne        :: !Bool
     -- ^ indicates that the package can be built in one step
-  , taskCachePkgSrc     :: !CachePkgSrc
-  , taskAnyMissing      :: !Bool
-    -- ^ Were any of the dependencies missing? The reason this is necessary is...
-    -- hairy. And as you may expect, a bug in Cabal. See:
-    -- <https://github.com/haskell/cabal/issues/4728#issuecomment-337937673>.
-    -- The problem is that Cabal may end up generating the same package ID for a
-    -- dependency, even if the ABI has changed. As a result, without this field,
-    -- Stack would think that a reconfigure is unnecessary, when in fact we _do_
-    -- need to reconfigure. The details here suck. We really need proper hashes
-    -- for package identifiers.
-  , taskBuildTypeConfig :: !Bool
+  , cachePkgSrc     :: !CachePkgSrc
+  , buildTypeConfig :: !Bool
     -- ^ Is the build type of this package Configure. Check out
     -- ensureConfigureScript in Stack.Build.Execute for the motivation
   }
@@ -150,9 +155,9 @@ 
 -- | Given the IDs of any missing packages, produce the configure options
 data TaskConfigOpts = TaskConfigOpts
-  { tcoMissing :: !(Set PackageIdentifier)
+  { missing :: !(Set PackageIdentifier)
     -- ^ Dependencies for which we don't yet have an GhcPkgId
-  , tcoOpts    :: !(Map PackageIdentifier GhcPkgId -> ConfigureOpts)
+  , opts    :: !(Map PackageIdentifier GhcPkgId -> ConfigureOpts)
     -- ^ Produce the list of options given the missing @GhcPkgId@s
   }
 
@@ -164,29 +169,52 @@     , show $ f Map.empty
     ]
 
--- | The type of a task, either building local code or something from the
--- package index (upstream)
+-- | Type representing different types of task, depending on what is to be
+-- built.
 data TaskType
   = TTLocalMutable LocalPackage
+    -- ^ Building local source code.
   | TTRemotePackage IsMutable Package PackageLocationImmutable
+    -- ^ Building something from the package index (upstream).
   deriving Show
 
+-- | Were any of the dependencies missing?
+
+taskAnyMissing :: Task -> Bool
+taskAnyMissing task = not $ Set.null task.configOpts.missing
+
+-- | A function to yield the package name and version of a given 'TaskType'
+-- value.
+taskTypePackageIdentifier :: TaskType -> PackageIdentifier
+taskTypePackageIdentifier (TTLocalMutable lp) = packageIdentifier lp.package
+taskTypePackageIdentifier (TTRemotePackage _ p _) = packageIdentifier p
+
 taskIsTarget :: Task -> Bool
 taskIsTarget t =
-  case taskType t of
-    TTLocalMutable lp -> lpWanted lp
+  case t.taskType of
+    TTLocalMutable lp -> lp.wanted
     _ -> False
 
+-- | A function to yield the relevant database (write-only or mutable) of a
+-- given 'TaskType' value.
+taskTypeLocation :: TaskType -> InstallLocation
+taskTypeLocation (TTLocalMutable _) = Local
+taskTypeLocation (TTRemotePackage Mutable _ _) = Local
+taskTypeLocation (TTRemotePackage Immutable _ _) = Snap
+
+-- | A function to yield the relevant database (write-only or mutable) of the
+-- given task.
 taskLocation :: Task -> InstallLocation
-taskLocation task =
-  case taskType task of
-    TTLocalMutable _ -> Local
-    TTRemotePackage Mutable _ _ -> Local
-    TTRemotePackage Immutable _ _ -> Snap
+taskLocation = taskTypeLocation . (.taskType)
 
+-- | A function to yield the package name and version to be built by the given
+-- task.
+taskProvides :: Task -> PackageIdentifier
+taskProvides = taskTypePackageIdentifier . (.taskType)
+
 taskTargetIsMutable :: Task -> IsMutable
 taskTargetIsMutable task =
-  case taskType task of
+  case task.taskType of
     TTLocalMutable _ -> Mutable
     TTRemotePackage mutable _ _ -> mutable
 
@@ -196,24 +224,24 @@ 
 -- | A complete plan of what needs to be built and how to do it
 data Plan = Plan
-  { planTasks :: !(Map PackageName Task)
-  , planFinals :: !(Map PackageName Task)
+  { tasks :: !(Map PackageName Task)
+  , finals :: !(Map PackageName Task)
     -- ^ Final actions to be taken (test, benchmark, etc)
-  , planUnregisterLocal :: !(Map GhcPkgId (PackageIdentifier, Text))
+  , unregisterLocal :: !(Map GhcPkgId (PackageIdentifier, Text))
     -- ^ Text is reason we're unregistering, for display only
-  , planInstallExes :: !(Map Text InstallLocation)
+  , installExes :: !(Map Text InstallLocation)
     -- ^ Executables that should be installed after successful building
   }
   deriving Show
 
--- | Information on a compiled package: the library conf file (if relevant),
--- the sublibraries (if present) and all of the executable paths.
+-- | Information on a compiled package: the library .conf file (if relevant),
+-- the sub-libraries (if present) and all of the executable paths.
 data PrecompiledCache base = PrecompiledCache
-  { pcLibrary :: !(Maybe (Path base File))
+  { library :: !(Maybe (Path base File))
     -- ^ .conf file inside the package database
-  , pcSubLibs :: ![Path base File]
-    -- ^ .conf file inside the package database, for each of the sublibraries
-  , pcExes    :: ![Path base File]
+  , subLibs :: ![Path base File]
+    -- ^ .conf file inside the package database, for each of the sub-libraries
+  , exes    :: ![Path base File]
     -- ^ Full paths to executables
   }
   deriving (Eq, Generic, Show, Typeable)
@@ -221,3 +249,18 @@ instance NFData (PrecompiledCache Abs)
 
 instance NFData (PrecompiledCache Rel)
+
+data ExcludeTHLoading
+  = ExcludeTHLoading
+  | KeepTHLoading
+
+data ConvertPathsToAbsolute
+  = ConvertPathsToAbsolute
+  | KeepPathsAsIs
+
+-- | special marker for expected failures in curator builds, using those we need
+-- to keep log handle open as build continues further even after a failure
+data KeepOutputOpen
+  = KeepOpen
+  | CloseOnException
+  deriving Eq
+ src/Stack/Types/Build/ConstructPlan.hs view
@@ -0,0 +1,227 @@+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+
+-- | A module providing types and related helper functions used in module
+-- "Stack.Build.ConstructPlan".
+module Stack.Types.Build.ConstructPlan
+  ( PackageInfo (..)
+  , CombinedMap
+  , M
+  , W (..)
+  , AddDepRes (..)
+  , toTask
+  , adrVersion
+  , adrHasLibrary
+  , Ctx (..)
+  , UnregisterState (..)
+  , ToolWarning (..)
+  ) where
+
+import           Generics.Deriving.Monoid ( mappenddefault, memptydefault )
+import           RIO.Process ( HasProcessContext (..) )
+import           RIO.State ( StateT )
+import           RIO.Writer ( WriterT (..) )
+import           Stack.Package ( hasBuildableMainLibrary )
+import           Stack.Prelude hiding ( loadPackage )
+import           Stack.Types.Build
+                    ( Task (..), TaskType (..), taskProvides )
+import           Stack.Types.Build.Exception ( ConstructPlanException )
+import           Stack.Types.BuildConfig
+                   ( BuildConfig (..), HasBuildConfig(..) )
+import           Stack.Types.CompilerPaths ( HasCompiler (..) )
+import           Stack.Types.Config ( HasConfig (..) )
+import           Stack.Types.ConfigureOpts ( BaseConfigOpts )
+import           Stack.Types.Curator ( Curator )
+import           Stack.Types.DumpPackage ( DumpPackage )
+import           Stack.Types.EnvConfig
+                   ( EnvConfig (..), HasEnvConfig (..), HasSourceMap (..) )
+import           Stack.Types.GhcPkgId ( GhcPkgId )
+import           Stack.Types.GHCVariant ( HasGHCVariant (..) )
+import           Stack.Types.Installed
+                   ( InstallLocation, Installed (..), installedVersion )
+import           Stack.Types.Package
+                   ( ExeName (..), LocalPackage (..), Package (..)
+                   , PackageSource (..)
+                   )
+import           Stack.Types.ParentMap ( ParentMap )
+import           Stack.Types.Platform ( HasPlatform (..) )
+import           Stack.Types.Runner ( HasRunner (..) )
+
+-- | Type representing information about packages, namely information about
+-- whether or not a package is already installed and, unless the package is not
+-- to be built (global packages), where its source code is located.
+data PackageInfo
+  = PIOnlyInstalled InstallLocation Installed
+    -- ^ This indicates that the package is already installed, and that we
+    -- shouldn't build it from source. This is only the case for global
+    -- packages.
+  | PIOnlySource PackageSource
+    -- ^ This indicates that the package isn't installed, and we know where to
+    -- find its source.
+  | PIBoth PackageSource Installed
+    -- ^ This indicates that the package is installed and we know where to find
+    -- its source. We may want to reinstall from source.
+  deriving Show
+
+-- | A type synonym representing dictionaries of package names, and combined
+-- information about the package in respect of whether or not it is already
+-- installed and, unless the package is not to be built (global packages), where
+-- its source code is located.
+type CombinedMap = Map PackageName PackageInfo
+
+-- | Type synonym representing values used during the construction of a build
+-- plan. The type is an instance of 'Monad', hence its name.
+type M =
+  WriterT
+    W
+    -- ^ The output to be collected
+    ( StateT
+        (Map PackageName (Either ConstructPlanException AddDepRes))
+        -- ^ Library map
+        (RIO Ctx)
+    )
+
+-- | Type representing values used as the output to be collected during the
+-- construction of a build plan.
+data W = W
+  { wFinals :: !(Map PackageName (Either ConstructPlanException Task))
+    -- ^ A dictionary of package names, and either a final task to perform when
+    -- building the package or an exception.
+  , wInstall :: !(Map Text InstallLocation)
+    -- ^ A dictionary of executables to be installed, and location where the
+    -- executable's binary is placed.
+  , wDirty :: !(Map PackageName Text)
+    -- ^ A dictionary of local packages, and the reason why the local package is
+    -- considered dirty.
+  , wWarnings :: !([StyleDoc] -> [StyleDoc])
+    -- ^ Warnings.
+  , wParents :: !ParentMap
+    -- ^ A dictionary of package names, and a list of pairs of the identifier
+    -- of a package depending on the package and the version range specified for
+    -- the dependency by that package. Used in the reporting of failure to
+    -- construct a build plan.
+  }
+  deriving Generic
+
+instance Semigroup W where
+  (<>) = mappenddefault
+
+instance Monoid W where
+  mempty = memptydefault
+  mappend = (<>)
+
+-- | Type representing results of 'addDep'.
+data AddDepRes
+  = ADRToInstall Task
+    -- ^ A task must be performed to provide the package name.
+  | ADRFound InstallLocation Installed
+    -- ^ An existing installation provides the package name.
+  deriving Show
+
+toTask :: AddDepRes -> Maybe Task
+toTask (ADRToInstall task) = Just task
+toTask (ADRFound _ _) = Nothing
+
+adrVersion :: AddDepRes -> Version
+adrVersion (ADRToInstall task) = pkgVersion $ taskProvides task
+adrVersion (ADRFound _ installed) = installedVersion installed
+
+adrHasLibrary :: AddDepRes -> Bool
+adrHasLibrary (ADRToInstall task) = case task.taskType of
+  TTLocalMutable lp -> packageHasLibrary lp.package
+  TTRemotePackage _ p _ -> packageHasLibrary p
+ where
+  -- make sure we consider sub-libraries as libraries too
+  packageHasLibrary :: Package -> Bool
+  packageHasLibrary p =
+    hasBuildableMainLibrary p || not (null p.subLibraries)
+adrHasLibrary (ADRFound _ Library{}) = True
+adrHasLibrary (ADRFound _ Executable{}) = False
+
+-- | Type representing values used as the environment to be read from during the
+-- construction of a build plan (the \'context\').
+data Ctx = Ctx
+  { baseConfigOpts :: !BaseConfigOpts
+    -- ^ Basic information used to determine configure options
+  , loadPackage    :: !(  PackageLocationImmutable
+                       -> Map FlagName Bool
+                       -> [Text]
+                          -- ^ GHC options.
+                       -> [Text]
+                          -- ^ Cabal configure options.
+                       -> M Package
+                       )
+  , combinedMap    :: !CombinedMap
+    -- ^ A dictionary of package names, and combined information about the
+    -- package in respect of whether or not it is already installed and, unless
+    -- the package is not to be built (global packages), where its source code
+    -- is located.
+  , ctxEnvConfig   :: !EnvConfig
+    -- ^ Configuration after the environment has been setup.
+  , callStack      :: ![PackageName]
+  , wanted         :: !(Set PackageName)
+  , localNames     :: !(Set PackageName)
+  , curator       :: !(Maybe Curator)
+  , pathEnvVar     :: !Text
+  }
+
+instance HasPlatform Ctx where
+  platformL = configL . platformL
+  {-# INLINE platformL #-}
+  platformVariantL = configL . platformVariantL
+  {-# INLINE platformVariantL #-}
+
+instance HasGHCVariant Ctx where
+  ghcVariantL = configL . ghcVariantL
+  {-# INLINE ghcVariantL #-}
+
+instance HasLogFunc Ctx where
+  logFuncL = configL . logFuncL
+
+instance HasRunner Ctx where
+  runnerL = configL . runnerL
+
+instance HasStylesUpdate Ctx where
+  stylesUpdateL = runnerL . stylesUpdateL
+
+instance HasTerm Ctx where
+  useColorL = runnerL . useColorL
+  termWidthL = runnerL . termWidthL
+
+instance HasConfig Ctx where
+  configL = buildConfigL . lens (.config) (\x y -> x { config = y })
+  {-# INLINE configL #-}
+
+instance HasPantryConfig Ctx where
+  pantryConfigL = configL . pantryConfigL
+
+instance HasProcessContext Ctx where
+  processContextL = configL . processContextL
+
+instance HasBuildConfig Ctx where
+  buildConfigL = envConfigL . lens
+    (.buildConfig)
+    (\x y -> x { buildConfig = y })
+
+instance HasSourceMap Ctx where
+  sourceMapL = envConfigL . sourceMapL
+
+instance HasCompiler Ctx where
+  compilerPathsL = envConfigL . compilerPathsL
+
+instance HasEnvConfig Ctx where
+  envConfigL = lens (.ctxEnvConfig) (\x y -> x { ctxEnvConfig = y })
+
+-- | State to be maintained during the calculation of local packages to
+-- unregister.
+data UnregisterState = UnregisterState
+  { toUnregister :: !(Map GhcPkgId (PackageIdentifier, Text))
+  , toKeep :: ![DumpPackage]
+  , anyAdded :: !Bool
+  }
+
+-- | Warn about tools in the snapshot definition. States the tool name
+-- expected and the package name using it.
+data ToolWarning
+  = ToolWarning ExeName PackageName
+  deriving Show
src/Stack/Types/Build/Exception.hs view
@@ -1,6 +1,7 @@-{-# LANGUAGE NoImplicitPrelude          #-}
-{-# LANGUAGE DataKinds                  #-}
-{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 module Stack.Types.Build.Exception
   ( BuildException (..)
@@ -24,6 +25,7 @@ import           Distribution.Types.PackageName ( mkPackageName )
 import           Distribution.Types.TestSuiteInterface ( TestSuiteInterface )
 import qualified Distribution.Version as C
+import           RIO.NonEmpty ( nonEmpty )
 import           RIO.Process ( showProcessArgDebug )
 import           Stack.Constants
                    ( defaultUserConfigPath, wiredInPackages )
@@ -44,14 +46,6 @@ -- names beginning @Stack.Build@.
 data BuildException
   = Couldn'tFindPkgId PackageName
-  | CompilerVersionMismatch
-      (Maybe (ActualCompiler, Arch)) -- found
-      (WantedCompiler, Arch) -- expected
-      GHCVariant -- expected
-      CompilerBuild -- expected
-      VersionCheck
-      (Maybe (Path Abs File)) -- Path to the stack.yaml file
-      Text -- recommended resolution
   | Couldn'tParseTargets [Text]
   | UnknownTargets
       (Set PackageName) -- no known version
@@ -81,6 +75,7 @@   | TemplateHaskellNotFoundBug
   | HaddockIndexNotFound
   | ShowBuildErrorBug
+  | CallStackEmptyBug
   deriving (Show, Typeable)
 
 instance Exception BuildException where
@@ -91,34 +86,6 @@     , packageNameString name
     , ")."
     ]
-  displayException (CompilerVersionMismatch mactual (expected, eArch) ghcVariant ghcBuild check mstack resolution) = concat
-    [ "Error: [S-6362]\n"
-    , case mactual of
-        Nothing -> "No compiler found, expected "
-        Just (actual, arch) -> concat
-          [ "Compiler version mismatched, found "
-          , compilerVersionString actual
-          , " ("
-          , C.display arch
-          , ")"
-          , ", but expected "
-          ]
-    , case check of
-        MatchMinor -> "minor version match with "
-        MatchExact -> "exact version "
-        NewerMinor -> "minor version match or newer with "
-    , T.unpack $ utf8BuilderToText $ display expected
-    , " ("
-    , C.display eArch
-    , ghcVariantSuffix ghcVariant
-    , compilerBuildSuffix ghcBuild
-    , ") (based on "
-    , case mstack of
-        Nothing -> "command line arguments"
-        Just stack -> "resolver setting in " ++ toFilePath stack
-    , ").\n"
-    , T.unpack resolution
-    ]
   displayException (Couldn'tParseTargets targets) = unlines
     $ "Error: [S-3127]"
     : "The following targets could not be parsed as package names or \
@@ -251,6 +218,8 @@     ++ "No local or snapshot doc index found to open."
   displayException ShowBuildErrorBug = bugReport "[S-5452]"
     "Unexpected case in showBuildError."
+  displayException CallStackEmptyBug = bugReport "[S-2696]"
+    "addDep: call stack is empty."
 
 data BuildPrettyException
   = ConstructPlanFailed
@@ -281,6 +250,15 @@   | SomeTargetsNotBuildable [(PackageName, NamedComponent)]
   | InvalidFlagSpecification (Set UnusedFlags)
   | GHCProfOptionInvalid
+  | NotOnlyLocal [PackageName] [Text]
+  | CompilerVersionMismatch
+      (Maybe (ActualCompiler, Arch)) -- found
+      (WantedCompiler, Arch) -- expected
+      GHCVariant -- expected
+      CompilerBuild -- expected
+      VersionCheck
+      (Maybe (Path Abs File)) -- Path to the stack.yaml file
+      StyleDoc -- recommended resolution
   deriving (Show, Typeable)
 
 instance Pretty BuildPrettyException where
@@ -342,7 +320,7 @@     go :: UnusedFlags -> StyleDoc
     go (UFNoPackage src name) = fillSep
       [ "Package"
-      , style Error (fromString $ packageNameString name)
+      , style Error (fromPackageName name)
       , flow "not found"
       , showFlagSrc src
       ]
@@ -372,7 +350,7 @@       name = packageNameString pname
     go (UFSnapshot name) = fillSep
       [ flow "Attempted to set flag on snapshot package"
-      , style Current (fromString $ packageNameString name) <> ","
+      , style Current (fromPackageName name) <> ","
       , flow "please add the package to"
       , style Shell "extra-deps" <> "."
       ]
@@ -389,6 +367,66 @@          , flow "flags. See:"
          , style Url "https://github.com/commercialhaskell/stack/issues/1015" <> "."
          ]
+  pretty (NotOnlyLocal packages exes) =
+    "[S-1727]"
+    <> line
+    <> flow "Specified only-locals, but Stack needs to build snapshot contents:"
+    <> line
+    <> if null packages
+         then mempty
+         else
+              fillSep
+                ( "Packages:"
+                : mkNarrativeList Nothing False
+                    (map fromPackageName packages :: [StyleDoc])
+                )
+           <> line
+    <> if null exes
+         then mempty
+         else
+              fillSep
+                ( "Executables:"
+                : mkNarrativeList Nothing False
+                    (map (fromString . T.unpack) exes :: [StyleDoc])
+                )
+           <> line
+  pretty (CompilerVersionMismatch mactual (expected, eArch) ghcVariant ghcBuild check mstack resolution) =
+    "[S-6362]"
+    <> line
+    <> fillSep
+         [ case mactual of
+             Nothing -> flow "No compiler found, expected"
+             Just (actual, arch) -> fillSep
+               [ flow "Compiler version mismatched, found"
+               , fromString $ compilerVersionString actual
+               , parens (pretty arch) <> ","
+               , flow "but expected"
+               ]
+         , case check of
+             MatchMinor -> flow "minor version match with"
+             MatchExact -> flow "exact version"
+             NewerMinor -> flow "minor version match or newer with"
+         , fromString $ T.unpack $ utf8BuilderToText $ display expected
+         , parens $ mconcat
+             [ pretty eArch
+             , fromString $ ghcVariantSuffix ghcVariant
+             , fromString $ compilerBuildSuffix ghcBuild
+             ]
+         ,    parens
+                ( fillSep
+                    [ flow "based on"
+                    , case mstack of
+                        Nothing -> flow "command line arguments"
+                        Just stack -> fillSep
+                          [ flow "resolver setting in"
+                          , pretty stack
+                          ]
+                    ]
+                )
+          <> "."
+         ]
+    <> blankLine
+    <> resolution
 
 instance Exception BuildPrettyException
 
@@ -466,7 +504,7 @@            : flow "add these package names under"
            : style Shell "allow-newer-deps" <> ":"
            : mkNarrativeList (Just Shell) False
-               (map (fromString . packageNameString) (Set.elems pkgsWithMismatches) :: [StyleDoc])
+               (map fromPackageName (Set.elems pkgsWithMismatches) :: [StyleDoc])
        | not $ Set.null pkgsWithMismatches
        ]
     <> addExtraDepsRecommendations
@@ -556,12 +594,12 @@     , Set.Set PackageName
       -- ^ Set of names of packages with one or more DependencyMismatch errors.
     )
-  filterExceptions = L.foldl go acc0 exceptions'
+  filterExceptions = L.foldl' go acc0 exceptions'
    where
     acc0 = (True, False, Map.empty, Set.empty)
     go acc (DependencyPlanFailures pkg m) = Map.foldrWithKey go' acc m
      where
-      pkgName = packageName pkg
+      pkgName = pkg.name
       go' name (_, Just extra, NotInBuildPlan) (_, _, m', s) =
         (False, True, Map.insert name extra m', s)
       go' _ (_, _, NotInBuildPlan) (_, _, m', s) = (False, True, m', s)
@@ -577,58 +615,54 @@        flow "Dependency cycle detected in packages:"
     <> line
     <> indent 4
-         ( encloseSep "[" "]" ","
-             (map (style Error . fromString . packageNameString) pNames)
-         )
+         (encloseSep "[" "]" "," (map (style Error . fromPackageName) pNames))
   pprintException (DependencyPlanFailures pkg pDeps) =
     case mapMaybe pprintDep (Map.toList pDeps) of
       [] -> Nothing
       depErrors -> Just $
            fillSep
              [ flow "In the dependencies for"
-             , pkgIdent <> pprintFlags (packageFlags pkg) <> ":"
+             , pkgIdent <> pprintFlags pkg.flags <> ":"
              ]
         <> line
         <> indent 2 (bulletedList depErrors)
-        <> case getShortestDepsPath parentMap wanted' (packageName pkg) of
-             Nothing ->
-                  line
-               <> flow "needed for unknown reason - Stack invariant violated."
-             Just [] ->
-                  line
-               <> fillSep
-                    [ flow "needed since"
-                    , pkgName'
-                    , flow "is a build target."
-                    ]
-             Just (target:path) ->
-                  line
-               <> flow "needed due to" <+> encloseSep "" "" " -> " pathElems
-              where
-               pathElems =
-                    [style Target . fromString . packageIdentifierString $ target]
-                 <> map (fromString . packageIdentifierString) path
-                 <> [pkgIdent]
+        <> line
+        <> fillSep
+             ( flow "The above is/are needed"
+             : case getShortestDepsPath parentMap wanted' pkg.name of
+                 Nothing ->
+                   [flow "for unknown reason - Stack invariant violated."]
+                 Just [] ->
+                   [ "since"
+                   , pkgName'
+                   , flow "is a build target."
+                   ]
+                 Just (target:path) ->
+                   [ flow "due to"
+                   , encloseSep "" "" " -> " pathElems
+                   ]
+                  where
+                   pathElems =
+                        [style Target . fromPackageId $ target]
+                     <> map fromPackageId path
+                     <> [pkgIdent]
+             )
        where
-        pkgName' =
-          style Current . fromString . packageNameString $ packageName pkg
-        pkgIdent =
-          style
-            Current
-            (fromString . packageIdentifierString $ packageIdentifier pkg)
+        pkgName' = style Current (fromPackageName pkg.name)
+        pkgIdent = style Current (fromPackageId $ packageIdentifier pkg)
   -- Skip these when they are redundant with 'NotInBuildPlan' info.
   pprintException (UnknownPackage name)
     | name `Set.member` allNotInBuildPlan = Nothing
     | name `Set.member` wiredInPackages = Just $ fillSep
         [ flow "Can't build a package with same name as a wired-in-package:"
-        , style Current . fromString . packageNameString $ name
+        , style Current . fromPackageName $ name
         ]
     | Just pruned <- Map.lookup name prunedGlobalDeps =
         let prunedDeps =
-              map (style Current . fromString . packageNameString) pruned
+              map (style Current . fromPackageName) pruned
         in  Just $ fillSep
               [ flow "Can't use GHC boot package"
-              , style Current . fromString . packageNameString $ name
+              , style Current . fromPackageName $ name
               , flow "when it depends on a replaced boot package. You need to \
                      \add the following as explicit dependencies to the \
                      \project:"
@@ -637,7 +671,7 @@               ]
     | otherwise = Just $ fillSep
         [ flow "Unknown package:"
-        , style Current . fromString . packageNameString $ name
+        , style Current . fromPackageName $ name
         ]
 
   pprintFlags flags
@@ -670,7 +704,7 @@              ++ L.intercalate ", " (map packageNameString names)
       ]
    where
-    errorName = style Error . fromString . packageNameString $ name
+    errorName = style Error . fromPackageName $ name
     goodRange = style Good (fromString (C.display range))
     rangeMsg = if range == C.anyVersion
       then "needed,"
@@ -688,7 +722,7 @@     inconsistentMsg mVersion = fillSep
       [ style Error $ maybe
           ( flow "no version" )
-          ( fromString . packageIdentifierString . PackageIdentifier name )
+          ( fromPackageId . PackageIdentifier name )
           mVersion
       , flow "is in the Stack configuration"
       ]
@@ -833,7 +867,7 @@     then Just []
     else case M.lookup name parentsMap of
       Nothing -> Nothing
-      Just (_, parents) -> Just $ findShortest 256 paths0
+      Just parents -> Just $ findShortest 256 paths0
        where
         paths0 = M.fromList $
           map (\(ident, _) -> (pkgName ident, startDepsPath ident)) parents
@@ -849,10 +883,12 @@     ]
   findShortest _ paths | M.null paths = []
   findShortest fuel paths =
-    case targets of
-      [] -> findShortest (fuel - 1) $ M.fromListWith chooseBest $
+    case nonEmpty targets of
+      Nothing -> findShortest (fuel - 1) $ M.fromListWith chooseBest $
               concatMap extendPath recurses
-      _ -> let (DepsPath _ _ path) = L.minimum (map snd targets) in path
+      Just targets' ->
+        let (DepsPath _ _ path) = minimum (snd <$> targets')
+        in  path
    where
     (targets, recurses) =
       L.partition (\(n, _) -> n `Set.member` wanted') (M.toList paths)
@@ -863,7 +899,7 @@   extendPath (n, dp) =
     case M.lookup n parentsMap of
       Nothing -> []
-      Just (_, parents) ->
+      Just parents ->
         map (\(pkgId, _) -> (pkgName pkgId, extendDepsPath pkgId dp)) parents
 
 startDepsPath :: PackageIdentifier -> DepsPath
@@ -875,8 +911,8 @@ 
 extendDepsPath :: PackageIdentifier -> DepsPath -> DepsPath
 extendDepsPath ident dp = DepsPath
-  { dpLength = dpLength dp + 1
-  , dpNameLength = dpNameLength dp + length (packageNameString (pkgName ident))
+  { dpLength = dp.dpLength + 1
+  , dpNameLength = dp.dpNameLength + length (packageNameString (pkgName ident))
   , dpPath = [ident]
   }
 
src/Stack/Types/BuildConfig.hs view
@@ -1,7 +1,9 @@-{-# LANGUAGE NoImplicitPrelude     #-}
-{-# LANGUAGE DataKinds             #-}
-{-# LANGUAGE GADTs                 #-}
-{-# LANGUAGE TypeFamilies          #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE GADTs               #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE TypeFamilies        #-}
 
 module Stack.Types.BuildConfig
   ( BuildConfig (..)
@@ -29,52 +31,52 @@ --
 -- These are the components which know nothing about local configuration.
 data BuildConfig = BuildConfig
-  { bcConfig     :: !Config
-  , bcSMWanted :: !SMWanted
-  , bcExtraPackageDBs :: ![Path Abs Dir]
+  { config     :: !Config
+  , smWanted :: !SMWanted
+  , extraPackageDBs :: ![Path Abs Dir]
     -- ^ Extra package databases
-  , bcStackYaml  :: !(Path Abs File)
+  , stackYaml  :: !(Path Abs File)
     -- ^ Location of the stack.yaml file.
     --
     -- Note: if the STACK_YAML environment variable is used, this may be
     -- different from projectRootL </> "stack.yaml" if a different file
     -- name is used.
-  , bcProjectStorage :: !ProjectStorage
+  , projectStorage :: !ProjectStorage
   -- ^ Database connection pool for project Stack database
-  , bcCurator :: !(Maybe Curator)
+  , curator :: !(Maybe Curator)
   }
 
 instance HasPlatform BuildConfig where
-  platformL = configL.platformL
+  platformL = configL . platformL
   {-# INLINE platformL #-}
-  platformVariantL = configL.platformVariantL
+  platformVariantL = configL . platformVariantL
   {-# INLINE platformVariantL #-}
 
 instance HasGHCVariant BuildConfig where
-  ghcVariantL = configL.ghcVariantL
+  ghcVariantL = configL . ghcVariantL
   {-# INLINE ghcVariantL #-}
 
 instance HasProcessContext BuildConfig where
-  processContextL = configL.processContextL
+  processContextL = configL . processContextL
 
 instance HasPantryConfig BuildConfig where
-  pantryConfigL = configL.pantryConfigL
+  pantryConfigL = configL . pantryConfigL
 
 instance HasConfig BuildConfig where
-  configL = lens bcConfig (\x y -> x { bcConfig = y })
+  configL = lens (.config) (\x y -> x { config = y })
 
 instance HasRunner BuildConfig where
-  runnerL = configL.runnerL
+  runnerL = configL . runnerL
 
 instance HasLogFunc BuildConfig where
-  logFuncL = runnerL.logFuncL
+  logFuncL = runnerL . logFuncL
 
 instance HasStylesUpdate BuildConfig where
-  stylesUpdateL = runnerL.stylesUpdateL
+  stylesUpdateL = runnerL . stylesUpdateL
 
 instance HasTerm BuildConfig where
-  useColorL = runnerL.useColorL
-  termWidthL = runnerL.termWidthL
+  useColorL = runnerL . useColorL
+  termWidthL = runnerL . termWidthL
 
 class HasConfig env => HasBuildConfig env where
   buildConfigL :: Lens' env BuildConfig
@@ -84,11 +86,11 @@   {-# INLINE buildConfigL #-}
 
 stackYamlL :: HasBuildConfig env => Lens' env (Path Abs File)
-stackYamlL = buildConfigL.lens bcStackYaml (\x y -> x { bcStackYaml = y })
+stackYamlL = buildConfigL . lens (.stackYaml) (\x y -> x { stackYaml = y })
 
 -- | Directory containing the project's stack.yaml file
 projectRootL :: HasBuildConfig env => Getting r env (Path Abs Dir)
-projectRootL = stackYamlL.to parent
+projectRootL = stackYamlL . to parent
 
 -- | Per-project work dir
 getProjectWorkDir :: (HasBuildConfig env, MonadReader env m) => m (Path Abs Dir)
@@ -100,4 +102,4 @@ -- | The compiler specified by the @SnapshotDef@. This may be different from the
 -- actual compiler used!
 wantedCompilerVersionL :: HasBuildConfig s => Getting r s WantedCompiler
-wantedCompilerVersionL = buildConfigL.to (smwCompiler . bcSMWanted)
+wantedCompilerVersionL = buildConfigL . to (.smWanted.compiler)
src/Stack/Types/BuildOpts.hs view
@@ -1,626 +1,115 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 
 -- | Configuration options for building.
 module Stack.Types.BuildOpts
   ( BuildOpts (..)
-  , buildOptsHaddockL
-  , buildOptsInstallExesL
-  , BuildCommand (..)
-  , defaultBuildOpts
-  , defaultBuildOptsCLI
-  , BuildOptsCLI (..)
-  , boptsCLIAllProgOptions
-  , BuildOptsMonoid (..)
-  , ProgressBarFormat (..)
-  , readProgressBarFormat
-  , buildOptsMonoidBenchmarksL
-  , buildOptsMonoidHaddockL
-  , buildOptsMonoidInstallExesL
-  , buildOptsMonoidTestsL
-  , TestOpts (..)
-  , defaultTestOpts
-  , TestOptsMonoid (..)
   , HaddockOpts (..)
-  , defaultHaddockOpts
-  , HaddockOptsMonoid (..)
+  , TestOpts (..)
   , BenchmarkOpts (..)
-  , defaultBenchmarkOpts
-  , BenchmarkOptsMonoid (..)
-  , FileWatchOpts (..)
-  , BuildSubset (..)
-  , ApplyCLIFlag (..)
-  , boptsCLIFlagsByName
-  , CabalVerbosity (..)
-  , toFirstCabalVerbosity
+  , buildOptsHaddockL
+  , buildOptsInstallExesL
   ) where
 
-import           Data.Aeson.Types ( FromJSON (..), withText )
-import           Data.Aeson.WarningParser
-                   ( WithJSONWarnings, (..:?), (..!=), jsonSubWarnings
-                   , withObjectWarnings
-                   )
-import qualified Data.Map.Strict as Map
-import qualified Data.Text as T
-import           Distribution.Parsec ( Parsec (..), simpleParsec )
-import           Distribution.Verbosity ( Verbosity, normal, verbose )
-import           Generics.Deriving.Monoid ( mappenddefault, memptydefault )
 import           Stack.Prelude
+import           Stack.Types.BuildOptsMonoid
+                   ( CabalVerbosity (..), ProgressBarFormat (..) )
 
 -- | Build options that is interpreted by the build command. This is built up
 -- from BuildOptsCLI and BuildOptsMonoid
 data BuildOpts = BuildOpts
-  { boptsLibProfile :: !Bool
-  , boptsExeProfile :: !Bool
-  , boptsLibStrip :: !Bool
-  , boptsExeStrip :: !Bool
-  , boptsHaddock :: !Bool
-    -- ^ Build haddocks?
-  , boptsHaddockOpts :: !HaddockOpts
+  { libProfile :: !Bool
+  , exeProfile :: !Bool
+  , libStrip :: !Bool
+  , exeStrip :: !Bool
+  , buildHaddocks :: !Bool
+    -- ^ Build Haddock documentation?
+  , haddockOpts :: !HaddockOpts
     -- ^ Options to pass to haddock
-  , boptsOpenHaddocks :: !Bool
+  , openHaddocks :: !Bool
     -- ^ Open haddocks in the browser?
-  , boptsHaddockDeps :: !(Maybe Bool)
+  , haddockDeps :: !(Maybe Bool)
     -- ^ Build haddocks for dependencies?
-  , boptsHaddockInternal :: !Bool
+  , haddockInternal :: !Bool
     -- ^ Build haddocks for all symbols and packages, like
     -- @cabal haddock --internal@
-  , boptsHaddockHyperlinkSource :: !Bool
-    -- ^ Build hyperlinked source if possible. Fallback to @hscolour@. Disable
-    -- for no sources.
-  , boptsInstallExes :: !Bool
+  , haddockHyperlinkSource :: !Bool
+    -- ^ Build hyperlinked source. Disable for no sources.
+  , haddockForHackage :: !Bool
+    -- ^ Build with flags to generate Haddock documentation suitable to upload
+    -- to Hackage.
+  , installExes :: !Bool
     -- ^ Install executables to user path after building?
-  , boptsInstallCompilerTool :: !Bool
+  , installCompilerTool :: !Bool
     -- ^ Install executables to compiler tools path after building?
-  , boptsPreFetch :: !Bool
+  , preFetch :: !Bool
     -- ^ Fetch all packages immediately
     -- ^ Watch files for changes and automatically rebuild
-  , boptsKeepGoing :: !(Maybe Bool)
+  , keepGoing :: !(Maybe Bool)
     -- ^ Keep building/running after failure
-  , boptsKeepTmpFiles :: !Bool
+  , keepTmpFiles :: !Bool
     -- ^ Keep intermediate files and build directories
-  , boptsForceDirty :: !Bool
+  , forceDirty :: !Bool
     -- ^ Force treating all local packages as having dirty files
-  , boptsTests :: !Bool
+  , tests :: !Bool
     -- ^ Turn on tests for local targets
-  , boptsTestOpts :: !TestOpts
+  , testOpts :: !TestOpts
     -- ^ Additional test arguments
-  , boptsBenchmarks :: !Bool
+  , benchmarks :: !Bool
     -- ^ Turn on benchmarks for local targets
-  , boptsBenchmarkOpts :: !BenchmarkOpts
+  , benchmarkOpts :: !BenchmarkOpts
     -- ^ Additional test arguments
     -- ^ Commands (with arguments) to run after a successful build
     -- ^ Only perform the configure step when building
-  , boptsReconfigure :: !Bool
+  , reconfigure :: !Bool
     -- ^ Perform the configure step even if already configured
-  , boptsCabalVerbose :: !CabalVerbosity
+  , cabalVerbose :: !CabalVerbosity
     -- ^ Ask Cabal to be verbose in its builds
-  , boptsSplitObjs :: !Bool
+  , splitObjs :: !Bool
     -- ^ Whether to enable split-objs.
-  , boptsSkipComponents :: ![Text]
+  , skipComponents :: ![Text]
     -- ^ Which components to skip when building
-  , boptsInterleavedOutput :: !Bool
+  , interleavedOutput :: !Bool
     -- ^ Should we use the interleaved GHC output when building
     -- multiple packages?
-  , boptsProgressBar :: !ProgressBarFormat
+  , progressBar :: !ProgressBarFormat
     -- ^ Format of the progress bar
-  , boptsDdumpDir :: !(Maybe Text)
+  , ddumpDir :: !(Maybe Text)
   }
   deriving Show
 
-defaultBuildOpts :: BuildOpts
-defaultBuildOpts = BuildOpts
-  { boptsLibProfile = defaultFirstFalse buildMonoidLibProfile
-  , boptsExeProfile = defaultFirstFalse buildMonoidExeProfile
-  , boptsLibStrip = defaultFirstTrue buildMonoidLibStrip
-  , boptsExeStrip = defaultFirstTrue buildMonoidExeStrip
-  , boptsHaddock = False
-  , boptsHaddockOpts = defaultHaddockOpts
-  , boptsOpenHaddocks = defaultFirstFalse buildMonoidOpenHaddocks
-  , boptsHaddockDeps = Nothing
-  , boptsHaddockInternal = defaultFirstFalse buildMonoidHaddockInternal
-  , boptsHaddockHyperlinkSource =
-      defaultFirstTrue buildMonoidHaddockHyperlinkSource
-  , boptsInstallExes = defaultFirstFalse buildMonoidInstallExes
-  , boptsInstallCompilerTool = defaultFirstFalse buildMonoidInstallCompilerTool
-  , boptsPreFetch = defaultFirstFalse buildMonoidPreFetch
-  , boptsKeepGoing = Nothing
-  , boptsKeepTmpFiles = defaultFirstFalse buildMonoidKeepTmpFiles
-  , boptsForceDirty = defaultFirstFalse buildMonoidForceDirty
-  , boptsTests = defaultFirstFalse buildMonoidTests
-  , boptsTestOpts = defaultTestOpts
-  , boptsBenchmarks = defaultFirstFalse buildMonoidBenchmarks
-  , boptsBenchmarkOpts = defaultBenchmarkOpts
-  , boptsReconfigure = defaultFirstFalse buildMonoidReconfigure
-  , boptsCabalVerbose = CabalVerbosity normal
-  , boptsSplitObjs = defaultFirstFalse buildMonoidSplitObjs
-  , boptsSkipComponents = []
-  , boptsInterleavedOutput = defaultFirstTrue buildMonoidInterleavedOutput
-  , boptsProgressBar = CappedBar
-  , boptsDdumpDir = Nothing
-  }
-
-defaultBuildOptsCLI ::BuildOptsCLI
-defaultBuildOptsCLI = BuildOptsCLI
-  { boptsCLITargets = []
-  , boptsCLIDryrun = False
-  , boptsCLIFlags = Map.empty
-  , boptsCLIGhcOptions = []
-  , boptsCLIProgsOptions = []
-  , boptsCLIBuildSubset = BSAll
-  , boptsCLIFileWatch = NoFileWatch
-  , boptsCLIWatchAll = False
-  , boptsCLIExec = []
-  , boptsCLIOnlyConfigure = False
-  , boptsCLICommand = Build
-  , boptsCLIInitialBuildSteps = False
-  }
-
--- | How to apply a CLI flag
-data ApplyCLIFlag
-  = ACFAllProjectPackages
-    -- ^ Apply to all project packages which have such a flag name available.
-  | ACFByName !PackageName
-    -- ^ Apply to the specified package only.
-  deriving (Eq, Ord, Show)
-
--- | Only flags set via 'ACFByName'
-boptsCLIFlagsByName :: BuildOptsCLI -> Map PackageName (Map FlagName Bool)
-boptsCLIFlagsByName =
-  Map.fromList .
-  mapMaybe go .
-  Map.toList .
-  boptsCLIFlags
- where
-  go (ACFAllProjectPackages, _) = Nothing
-  go (ACFByName name, flags) = Just (name, flags)
-
--- | Build options that may only be specified from the CLI
-data BuildOptsCLI = BuildOptsCLI
-  { boptsCLITargets :: ![Text]
-  , boptsCLIDryrun :: !Bool
-  , boptsCLIGhcOptions :: ![Text]
-  , boptsCLIProgsOptions :: ![(Text, [Text])]
-  , boptsCLIFlags :: !(Map ApplyCLIFlag (Map FlagName Bool))
-  , boptsCLIBuildSubset :: !BuildSubset
-  , boptsCLIFileWatch :: !FileWatchOpts
-  , boptsCLIWatchAll :: !Bool
-  , boptsCLIExec :: ![(String, [String])]
-  , boptsCLIOnlyConfigure :: !Bool
-  , boptsCLICommand :: !BuildCommand
-  , boptsCLIInitialBuildSteps :: !Bool
+-- | Haddock Options
+newtype HaddockOpts = HaddockOpts
+  { additionalArgs :: [String] -- ^ Arguments passed to haddock program
   }
-  deriving Show
-
--- | Generate a list of --PROG-option="<argument>" arguments for all PROGs.
-boptsCLIAllProgOptions :: BuildOptsCLI -> [Text]
-boptsCLIAllProgOptions boptsCLI =
-  concatMap progOptionArgs (boptsCLIProgsOptions boptsCLI)
- where
-  -- Generate a list of --PROG-option="<argument>" arguments for a PROG.
-  progOptionArgs :: (Text, [Text]) -> [Text]
-  progOptionArgs (prog, opts) = map progOptionArg opts
-   where
-    -- Generate a --PROG-option="<argument>" argument for a PROG and option.
-    progOptionArg :: Text -> Text
-    progOptionArg opt = T.concat
-      [ "--"
-      , prog
-      , "-option=\""
-      , opt
-      , "\""
-      ]
-
--- | Command sum type for conditional arguments.
-data BuildCommand
-  = Build
-  | Test
-  | Haddock
-  | Bench
-  | Install
   deriving (Eq, Show)
 
--- | Build options that may be specified in the stack.yaml or from the CLI
-data BuildOptsMonoid = BuildOptsMonoid
-  { buildMonoidTrace :: !Any
-  , buildMonoidProfile :: !Any
-  , buildMonoidNoStrip :: !Any
-  , buildMonoidLibProfile :: !FirstFalse
-  , buildMonoidExeProfile :: !FirstFalse
-  , buildMonoidLibStrip :: !FirstTrue
-  , buildMonoidExeStrip :: !FirstTrue
-  , buildMonoidHaddock :: !FirstFalse
-  , buildMonoidHaddockOpts :: !HaddockOptsMonoid
-  , buildMonoidOpenHaddocks :: !FirstFalse
-  , buildMonoidHaddockDeps :: !(First Bool)
-  , buildMonoidHaddockInternal :: !FirstFalse
-  , buildMonoidHaddockHyperlinkSource :: !FirstTrue
-  , buildMonoidInstallExes :: !FirstFalse
-  , buildMonoidInstallCompilerTool :: !FirstFalse
-  , buildMonoidPreFetch :: !FirstFalse
-  , buildMonoidKeepGoing :: !(First Bool)
-  , buildMonoidKeepTmpFiles :: !FirstFalse
-  , buildMonoidForceDirty :: !FirstFalse
-  , buildMonoidTests :: !FirstFalse
-  , buildMonoidTestOpts :: !TestOptsMonoid
-  , buildMonoidBenchmarks :: !FirstFalse
-  , buildMonoidBenchmarkOpts :: !BenchmarkOptsMonoid
-  , buildMonoidReconfigure :: !FirstFalse
-  , buildMonoidCabalVerbose :: !(First CabalVerbosity)
-  , buildMonoidSplitObjs :: !FirstFalse
-  , buildMonoidSkipComponents :: ![Text]
-  , buildMonoidInterleavedOutput :: !FirstTrue
-  , buildMonoidProgressBar :: !(First ProgressBarFormat)
-  , buildMonoidDdumpDir :: !(First Text)
-  }
-  deriving (Generic, Show)
-
-instance FromJSON (WithJSONWarnings BuildOptsMonoid) where
-  parseJSON = withObjectWarnings "BuildOptsMonoid" $ \o -> do
-    let buildMonoidTrace = Any False
-        buildMonoidProfile = Any False
-        buildMonoidNoStrip = Any False
-    buildMonoidLibProfile <- FirstFalse <$> o ..:? buildMonoidLibProfileArgName
-    buildMonoidExeProfile <-FirstFalse <$>  o ..:? buildMonoidExeProfileArgName
-    buildMonoidLibStrip <- FirstTrue <$> o ..:? buildMonoidLibStripArgName
-    buildMonoidExeStrip <-FirstTrue <$>  o ..:? buildMonoidExeStripArgName
-    buildMonoidHaddock <- FirstFalse <$> o ..:? buildMonoidHaddockArgName
-    buildMonoidHaddockOpts <-
-      jsonSubWarnings (o ..:? buildMonoidHaddockOptsArgName ..!= mempty)
-    buildMonoidOpenHaddocks <-
-      FirstFalse <$> o ..:? buildMonoidOpenHaddocksArgName
-    buildMonoidHaddockDeps <- First <$> o ..:? buildMonoidHaddockDepsArgName
-    buildMonoidHaddockInternal <-
-      FirstFalse <$> o ..:? buildMonoidHaddockInternalArgName
-    buildMonoidHaddockHyperlinkSource <-
-      FirstTrue <$> o ..:? buildMonoidHaddockHyperlinkSourceArgName
-    buildMonoidInstallExes <-
-      FirstFalse <$> o ..:? buildMonoidInstallExesArgName
-    buildMonoidInstallCompilerTool <-
-      FirstFalse <$> o ..:? buildMonoidInstallCompilerToolArgName
-    buildMonoidPreFetch <- FirstFalse <$> o ..:? buildMonoidPreFetchArgName
-    buildMonoidKeepGoing <- First <$> o ..:? buildMonoidKeepGoingArgName
-    buildMonoidKeepTmpFiles <-
-      FirstFalse <$> o ..:? buildMonoidKeepTmpFilesArgName
-    buildMonoidForceDirty <- FirstFalse <$> o ..:? buildMonoidForceDirtyArgName
-    buildMonoidTests <- FirstFalse <$> o ..:? buildMonoidTestsArgName
-    buildMonoidTestOpts <-
-      jsonSubWarnings (o ..:? buildMonoidTestOptsArgName ..!= mempty)
-    buildMonoidBenchmarks <- FirstFalse <$> o ..:? buildMonoidBenchmarksArgName
-    buildMonoidBenchmarkOpts <-
-      jsonSubWarnings (o ..:? buildMonoidBenchmarkOptsArgName ..!= mempty)
-    buildMonoidReconfigure <-
-      FirstFalse <$> o ..:? buildMonoidReconfigureArgName
-    cabalVerbosity <- First <$> o ..:? buildMonoidCabalVerbosityArgName
-    cabalVerbose <- FirstFalse <$> o ..:? buildMonoidCabalVerboseArgName
-    let buildMonoidCabalVerbose =
-          cabalVerbosity <> toFirstCabalVerbosity cabalVerbose
-    buildMonoidSplitObjs <- FirstFalse <$> o ..:? buildMonoidSplitObjsName
-    buildMonoidSkipComponents <-
-      o ..:? buildMonoidSkipComponentsName ..!= mempty
-    buildMonoidInterleavedOutput <-
-      FirstTrue <$> o ..:? buildMonoidInterleavedOutputName
-    buildMonoidProgressBar <-
-      First <$> o ..:? buildMonoidProgressBarName
-    buildMonoidDdumpDir <- o ..:? buildMonoidDdumpDirName ..!= mempty
-    pure BuildOptsMonoid{..}
-
-buildMonoidLibProfileArgName :: Text
-buildMonoidLibProfileArgName = "library-profiling"
-
-buildMonoidExeProfileArgName :: Text
-buildMonoidExeProfileArgName = "executable-profiling"
-
-buildMonoidLibStripArgName :: Text
-buildMonoidLibStripArgName = "library-stripping"
-
-buildMonoidExeStripArgName :: Text
-buildMonoidExeStripArgName = "executable-stripping"
-
-buildMonoidHaddockArgName :: Text
-buildMonoidHaddockArgName = "haddock"
-
-buildMonoidHaddockOptsArgName :: Text
-buildMonoidHaddockOptsArgName = "haddock-arguments"
-
-buildMonoidOpenHaddocksArgName :: Text
-buildMonoidOpenHaddocksArgName = "open-haddocks"
-
-buildMonoidHaddockDepsArgName :: Text
-buildMonoidHaddockDepsArgName = "haddock-deps"
-
-buildMonoidHaddockInternalArgName :: Text
-buildMonoidHaddockInternalArgName = "haddock-internal"
-
-buildMonoidHaddockHyperlinkSourceArgName :: Text
-buildMonoidHaddockHyperlinkSourceArgName = "haddock-hyperlink-source"
-
-buildMonoidInstallExesArgName :: Text
-buildMonoidInstallExesArgName = "copy-bins"
-
-buildMonoidInstallCompilerToolArgName :: Text
-buildMonoidInstallCompilerToolArgName = "copy-compiler-tool"
-
-buildMonoidPreFetchArgName :: Text
-buildMonoidPreFetchArgName = "prefetch"
-
-buildMonoidKeepGoingArgName :: Text
-buildMonoidKeepGoingArgName = "keep-going"
-
-buildMonoidKeepTmpFilesArgName :: Text
-buildMonoidKeepTmpFilesArgName = "keep-tmp-files"
-
-buildMonoidForceDirtyArgName :: Text
-buildMonoidForceDirtyArgName = "force-dirty"
-
-buildMonoidTestsArgName :: Text
-buildMonoidTestsArgName = "test"
-
-buildMonoidTestOptsArgName :: Text
-buildMonoidTestOptsArgName = "test-arguments"
-
-buildMonoidBenchmarksArgName :: Text
-buildMonoidBenchmarksArgName = "bench"
-
-buildMonoidBenchmarkOptsArgName :: Text
-buildMonoidBenchmarkOptsArgName = "benchmark-opts"
-
-buildMonoidReconfigureArgName :: Text
-buildMonoidReconfigureArgName = "reconfigure"
-
-buildMonoidCabalVerbosityArgName :: Text
-buildMonoidCabalVerbosityArgName = "cabal-verbosity"
-
-buildMonoidCabalVerboseArgName :: Text
-buildMonoidCabalVerboseArgName = "cabal-verbose"
-
-buildMonoidSplitObjsName :: Text
-buildMonoidSplitObjsName = "split-objs"
-
-buildMonoidSkipComponentsName :: Text
-buildMonoidSkipComponentsName = "skip-components"
-
-buildMonoidInterleavedOutputName :: Text
-buildMonoidInterleavedOutputName = "interleaved-output"
-
-buildMonoidProgressBarName :: Text
-buildMonoidProgressBarName = "progress-bar"
-
-buildMonoidDdumpDirName :: Text
-buildMonoidDdumpDirName = "ddump-dir"
-
-instance Semigroup BuildOptsMonoid where
-  (<>) = mappenddefault
-
-instance Monoid BuildOptsMonoid where
-  mempty = memptydefault
-  mappend = (<>)
-
--- | Which subset of packages to build
-data BuildSubset
-  = BSAll
-  | BSOnlySnapshot
-    -- ^ Only install packages in the snapshot database, skipping
-    -- packages intended for the local database.
-  | BSOnlyDependencies
-  | BSOnlyLocals
-    -- ^ Refuse to build anything in the snapshot database, see
-    -- https://github.com/commercialhaskell/stack/issues/5272
-  deriving (Show, Eq)
-
 -- | Options for the 'FinalAction' 'DoTests'
 data TestOpts = TestOpts
-  { toRerunTests :: !Bool -- ^ Whether successful tests will be run gain
-  , toAdditionalArgs :: ![String] -- ^ Arguments passed to the test program
-  , toCoverage :: !Bool -- ^ Generate a code coverage report
-  , toDisableRun :: !Bool -- ^ Disable running of tests
-  , toMaximumTimeSeconds :: !(Maybe Int) -- ^ test suite timeout in seconds
-  , toAllowStdin :: !Bool -- ^ Whether to allow standard input
-  }
-  deriving (Eq, Show)
-
-defaultTestOpts :: TestOpts
-defaultTestOpts = TestOpts
-  { toRerunTests = defaultFirstTrue toMonoidRerunTests
-  , toAdditionalArgs = []
-  , toCoverage = defaultFirstFalse toMonoidCoverage
-  , toDisableRun = defaultFirstFalse toMonoidDisableRun
-  , toMaximumTimeSeconds = Nothing
-  , toAllowStdin = defaultFirstTrue toMonoidAllowStdin
-  }
-
-data TestOptsMonoid = TestOptsMonoid
-  { toMonoidRerunTests :: !FirstTrue
-  , toMonoidAdditionalArgs :: ![String]
-  , toMonoidCoverage :: !FirstFalse
-  , toMonoidDisableRun :: !FirstFalse
-  , toMonoidMaximumTimeSeconds :: !(First (Maybe Int))
-  , toMonoidAllowStdin :: !FirstTrue
-  }
-  deriving (Show, Generic)
-
-instance FromJSON (WithJSONWarnings TestOptsMonoid) where
-  parseJSON = withObjectWarnings "TestOptsMonoid" $ \o -> do
-    toMonoidRerunTests <- FirstTrue <$> o ..:? toMonoidRerunTestsArgName
-    toMonoidAdditionalArgs <- o ..:? toMonoidAdditionalArgsName ..!= []
-    toMonoidCoverage <- FirstFalse <$> o ..:? toMonoidCoverageArgName
-    toMonoidDisableRun <- FirstFalse <$> o ..:? toMonoidDisableRunArgName
-    toMonoidMaximumTimeSeconds <-
-      First <$> o ..:? toMonoidMaximumTimeSecondsArgName
-    toMonoidAllowStdin <- FirstTrue <$> o ..:? toMonoidTestsAllowStdinName
-    pure TestOptsMonoid{..}
-
-toMonoidRerunTestsArgName :: Text
-toMonoidRerunTestsArgName = "rerun-tests"
-
-toMonoidAdditionalArgsName :: Text
-toMonoidAdditionalArgsName = "additional-args"
-
-toMonoidCoverageArgName :: Text
-toMonoidCoverageArgName = "coverage"
-
-toMonoidDisableRunArgName :: Text
-toMonoidDisableRunArgName = "no-run-tests"
-
-toMonoidMaximumTimeSecondsArgName :: Text
-toMonoidMaximumTimeSecondsArgName = "test-suite-timeout"
-
-toMonoidTestsAllowStdinName :: Text
-toMonoidTestsAllowStdinName = "tests-allow-stdin"
-
-instance Semigroup TestOptsMonoid where
-  (<>) = mappenddefault
-
-instance Monoid TestOptsMonoid where
-  mempty = memptydefault
-  mappend = (<>)
-
--- | Haddock Options
-newtype HaddockOpts = HaddockOpts
-  { hoAdditionalArgs :: [String] -- ^ Arguments passed to haddock program
+  { rerunTests :: !Bool -- ^ Whether successful tests will be run gain
+  , additionalArgs :: ![String] -- ^ Arguments passed to the test program
+  , coverage :: !Bool -- ^ Generate a code coverage report
+  , disableRun :: !Bool -- ^ Disable running of tests
+  , maximumTimeSeconds :: !(Maybe Int) -- ^ test suite timeout in seconds
+  , allowStdin :: !Bool -- ^ Whether to allow standard input
   }
   deriving (Eq, Show)
 
-newtype HaddockOptsMonoid = HaddockOptsMonoid
-  { hoMonoidAdditionalArgs :: [String]
-  }
-  deriving (Generic, Show)
-
-defaultHaddockOpts :: HaddockOpts
-defaultHaddockOpts = HaddockOpts {hoAdditionalArgs = []}
-
-instance FromJSON (WithJSONWarnings HaddockOptsMonoid) where
-  parseJSON = withObjectWarnings "HaddockOptsMonoid" $ \o -> do
-    hoMonoidAdditionalArgs <- o ..:? hoMonoidAdditionalArgsName ..!= []
-    pure HaddockOptsMonoid{..}
-
-instance Semigroup HaddockOptsMonoid where
-  (<>) = mappenddefault
-
-instance Monoid HaddockOptsMonoid where
-  mempty = memptydefault
-  mappend = (<>)
-
-hoMonoidAdditionalArgsName :: Text
-hoMonoidAdditionalArgsName = "haddock-args"
-
 -- | Options for the 'FinalAction' 'DoBenchmarks'
 data BenchmarkOpts = BenchmarkOpts
-  { beoAdditionalArgs :: !(Maybe String)
+  { additionalArgs :: !(Maybe String)
     -- ^ Arguments passed to the benchmark program
-  , beoDisableRun :: !Bool
+  , disableRun :: !Bool
     -- ^ Disable running of benchmarks
   }
   deriving (Eq, Show)
 
-defaultBenchmarkOpts :: BenchmarkOpts
-defaultBenchmarkOpts = BenchmarkOpts
-  { beoAdditionalArgs = Nothing
-  , beoDisableRun = False
-  }
-
-data BenchmarkOptsMonoid = BenchmarkOptsMonoid
-  { beoMonoidAdditionalArgs :: !(First String)
-  , beoMonoidDisableRun :: !(First Bool)
-  }
-  deriving (Generic, Show)
-
-instance FromJSON (WithJSONWarnings BenchmarkOptsMonoid) where
-  parseJSON = withObjectWarnings "BenchmarkOptsMonoid" $ \o -> do
-    beoMonoidAdditionalArgs <- First <$> o ..:? beoMonoidAdditionalArgsArgName
-    beoMonoidDisableRun <- First <$> o ..:? beoMonoidDisableRunArgName
-    pure BenchmarkOptsMonoid{..}
-
-beoMonoidAdditionalArgsArgName :: Text
-beoMonoidAdditionalArgsArgName = "benchmark-arguments"
-
-beoMonoidDisableRunArgName :: Text
-beoMonoidDisableRunArgName = "no-run-benchmarks"
-
-instance Semigroup BenchmarkOptsMonoid where
-  (<>) = mappenddefault
-
-instance Monoid BenchmarkOptsMonoid where
-  mempty = memptydefault
-  mappend = (<>)
-
-data FileWatchOpts
-  = NoFileWatch
-  | FileWatch
-  | FileWatchPoll
-  deriving (Show, Eq)
-
-newtype CabalVerbosity
-  = CabalVerbosity Verbosity
-  deriving (Eq, Show)
-
-toFirstCabalVerbosity :: FirstFalse -> First CabalVerbosity
-toFirstCabalVerbosity vf = First $ getFirstFalse vf <&> \p ->
-  if p then verboseLevel else normalLevel
- where
-  verboseLevel = CabalVerbosity verbose
-  normalLevel  = CabalVerbosity normal
-
-instance FromJSON CabalVerbosity where
-
-  parseJSON = withText "CabalVerbosity" $ \t ->
-    let s = T.unpack t
-        errMsg = fail $ "Unrecognised Cabal verbosity: " ++ s
-    in  maybe errMsg pure (simpleParsec s)
-
-instance Parsec CabalVerbosity where
-  parsec = CabalVerbosity <$> parsec
-
-buildOptsMonoidHaddockL :: Lens' BuildOptsMonoid (Maybe Bool)
-buildOptsMonoidHaddockL =
-  lens (getFirstFalse . buildMonoidHaddock)
-    (\buildMonoid t -> buildMonoid {buildMonoidHaddock = FirstFalse t})
-
-buildOptsMonoidTestsL :: Lens' BuildOptsMonoid (Maybe Bool)
-buildOptsMonoidTestsL =
-  lens (getFirstFalse . buildMonoidTests)
-    (\buildMonoid t -> buildMonoid {buildMonoidTests = FirstFalse t})
-
-buildOptsMonoidBenchmarksL :: Lens' BuildOptsMonoid (Maybe Bool)
-buildOptsMonoidBenchmarksL =
-  lens (getFirstFalse . buildMonoidBenchmarks)
-    (\buildMonoid t -> buildMonoid {buildMonoidBenchmarks = FirstFalse t})
-
-buildOptsMonoidInstallExesL :: Lens' BuildOptsMonoid (Maybe Bool)
-buildOptsMonoidInstallExesL =
-  lens (getFirstFalse . buildMonoidInstallExes)
-    (\buildMonoid t -> buildMonoid {buildMonoidInstallExes = FirstFalse t})
-
 buildOptsInstallExesL :: Lens' BuildOpts Bool
 buildOptsInstallExesL =
-  lens boptsInstallExes
-    (\bopts t -> bopts {boptsInstallExes = t})
+  lens (.installExes) (\bopts t -> bopts {installExes = t})
 
 buildOptsHaddockL :: Lens' BuildOpts Bool
 buildOptsHaddockL =
-  lens boptsHaddock
-    (\bopts t -> bopts {boptsHaddock = t})
-
--- Type representing formats of Stack's progress bar when building.
-data ProgressBarFormat
-  = NoBar -- No progress bar at all.
-  | CountOnlyBar -- A bar that only counts packages.
-  | CappedBar -- A bar capped at a length equivalent to the terminal's width.
-  | FullBar -- A full progress bar.
-  deriving (Eq, Show)
-
-instance FromJSON ProgressBarFormat where
-  parseJSON = withText "ProgressBarFormat" $ \t -> either
-    fail
-    pure
-    (readProgressBarFormat $ T.unpack t)
-
--- | Parse ProgressBarFormat from a String.
-readProgressBarFormat :: String -> Either String ProgressBarFormat
-readProgressBarFormat s
-  | s == "none" = pure NoBar
-  | s == "count-only" = pure CountOnlyBar
-  | s == "capped" = pure CappedBar
-  | s == "full" = pure FullBar
-  | otherwise = Left $ "Invalid progress bar format: " ++ s
+  lens (.buildHaddocks) (\bopts t -> bopts {buildHaddocks = t})
+ src/Stack/Types/BuildOptsCLI.hs view
@@ -0,0 +1,114 @@+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
+
+-- | Configuration options for building from the command line only.
+module Stack.Types.BuildOptsCLI
+  ( BuildOptsCLI (..)
+  , defaultBuildOptsCLI
+  , ApplyCLIFlag (..)
+  , BuildSubset (..)
+  , FileWatchOpts (..)
+  , BuildCommand (..)
+  , boptsCLIAllProgOptions
+  , boptsCLIFlagsByName
+  ) where
+
+import qualified Data.Map.Strict as Map
+import qualified Data.Text as T
+import           Stack.Prelude
+
+-- | Build options that may only be specified from the CLI
+data BuildOptsCLI = BuildOptsCLI
+  { targetsCLI :: ![Text]
+  , dryrun :: !Bool
+  , ghcOptions :: ![Text]
+  , progsOptions :: ![(Text, [Text])]
+  , flags :: !(Map ApplyCLIFlag (Map FlagName Bool))
+  , buildSubset :: !BuildSubset
+  , fileWatch :: !FileWatchOpts
+  , watchAll :: !Bool
+  , exec :: ![(String, [String])]
+  , onlyConfigure :: !Bool
+  , command :: !BuildCommand
+  , initialBuildSteps :: !Bool
+  }
+  deriving Show
+
+defaultBuildOptsCLI ::BuildOptsCLI
+defaultBuildOptsCLI = BuildOptsCLI
+  { targetsCLI = []
+  , dryrun = False
+  , flags = Map.empty
+  , ghcOptions = []
+  , progsOptions = []
+  , buildSubset = BSAll
+  , fileWatch = NoFileWatch
+  , watchAll = False
+  , exec = []
+  , onlyConfigure = False
+  , command = Build
+  , initialBuildSteps = False
+  }
+
+-- | How to apply a CLI flag
+data ApplyCLIFlag
+  = ACFAllProjectPackages
+    -- ^ Apply to all project packages which have such a flag name available.
+  | ACFByName !PackageName
+    -- ^ Apply to the specified package only.
+  deriving (Eq, Ord, Show)
+
+-- | Which subset of packages to build
+data BuildSubset
+  = BSAll
+  | BSOnlySnapshot
+    -- ^ Only install packages in the snapshot database, skipping
+    -- packages intended for the local database.
+  | BSOnlyDependencies
+  | BSOnlyLocals
+    -- ^ Refuse to build anything in the snapshot database, see
+    -- https://github.com/commercialhaskell/stack/issues/5272
+  deriving (Show, Eq)
+
+data FileWatchOpts
+  = NoFileWatch
+  | FileWatch
+  | FileWatchPoll
+  deriving (Show, Eq)
+
+-- | Command sum type for conditional arguments.
+data BuildCommand
+  = Build
+  | Test
+  | Haddock
+  | Bench
+  | Install
+  deriving (Eq, Show)
+
+-- | Generate a list of --PROG-option="<argument>" arguments for all PROGs.
+boptsCLIAllProgOptions :: BuildOptsCLI -> [Text]
+boptsCLIAllProgOptions boptsCLI =
+  concatMap progOptionArgs boptsCLI.progsOptions
+ where
+  -- Generate a list of --PROG-option="<argument>" arguments for a PROG.
+  progOptionArgs :: (Text, [Text]) -> [Text]
+  progOptionArgs (prog, opts) = map progOptionArg opts
+   where
+    -- Generate a --PROG-option="<argument>" argument for a PROG and option.
+    progOptionArg :: Text -> Text
+    progOptionArg opt = T.concat
+      [ "--"
+      , prog
+      , "-option=\""
+      , opt
+      , "\""
+      ]
+
+-- | Only flags set via 'ACFByName'
+boptsCLIFlagsByName :: BuildOptsCLI -> Map PackageName (Map FlagName Bool)
+boptsCLIFlagsByName = Map.fromList . mapMaybe go . Map.toList . (.flags)
+ where
+  go (ACFAllProjectPackages, _) = Nothing
+  go (ACFByName name, flags) = Just (name, flags)
+ src/Stack/Types/BuildOptsMonoid.hs view
@@ -0,0 +1,397 @@+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
+
+-- | Configuration options for building from the command line and/or a
+-- configuration file.
+module Stack.Types.BuildOptsMonoid
+  ( BuildOptsMonoid (..)
+  , HaddockOptsMonoid (..)
+  , TestOptsMonoid (..)
+  , BenchmarkOptsMonoid (..)
+  , CabalVerbosity (..)
+  , ProgressBarFormat (..)
+  , buildOptsMonoidHaddockL
+  , buildOptsMonoidTestsL
+  , buildOptsMonoidBenchmarksL
+  , buildOptsMonoidInstallExesL
+  , toFirstCabalVerbosity
+  , readProgressBarFormat
+  ) where
+
+import           Data.Aeson.Types ( FromJSON (..), withText )
+import           Data.Aeson.WarningParser
+                   ( WithJSONWarnings, (..:?), (..!=), jsonSubWarnings
+                   , withObjectWarnings
+                   )
+import qualified Data.Text as T
+import           Distribution.Parsec ( Parsec (..), simpleParsec )
+import           Distribution.Verbosity ( Verbosity, normal, verbose )
+import           Generics.Deriving.Monoid ( mappenddefault, memptydefault )
+import           Stack.Prelude hiding ( trace )
+
+-- | Build options that may be specified in the stack.yaml or from the CLI
+data BuildOptsMonoid = BuildOptsMonoid
+  { trace :: !Any
+  , profile :: !Any
+  , noStrip :: !Any
+  , libProfile :: !FirstFalse
+  , exeProfile :: !FirstFalse
+  , libStrip :: !FirstTrue
+  , exeStrip :: !FirstTrue
+  , buildHaddocks :: !FirstFalse
+  , haddockOpts :: !HaddockOptsMonoid
+  , openHaddocks :: !FirstFalse
+  , haddockDeps :: !(First Bool)
+  , haddockInternal :: !FirstFalse
+  , haddockHyperlinkSource :: !FirstTrue
+  , haddockForHackage :: !FirstFalse
+  , installExes :: !FirstFalse
+  , installCompilerTool :: !FirstFalse
+  , preFetch :: !FirstFalse
+  , keepGoing :: !(First Bool)
+  , keepTmpFiles :: !FirstFalse
+  , forceDirty :: !FirstFalse
+  , tests :: !FirstFalse
+  , testOpts :: !TestOptsMonoid
+  , benchmarks :: !FirstFalse
+  , benchmarkOpts :: !BenchmarkOptsMonoid
+  , reconfigure :: !FirstFalse
+  , cabalVerbose :: !(First CabalVerbosity)
+  , splitObjs :: !FirstFalse
+  , skipComponents :: ![Text]
+  , interleavedOutput :: !FirstTrue
+  , progressBar :: !(First ProgressBarFormat)
+  , ddumpDir :: !(First Text)
+  }
+  deriving (Generic, Show)
+
+instance FromJSON (WithJSONWarnings BuildOptsMonoid) where
+  parseJSON = withObjectWarnings "BuildOptsMonoid" $ \o -> do
+    let trace = Any False
+        profile = Any False
+        noStrip = Any False
+    libProfile <- FirstFalse <$> o ..:? libProfileArgName
+    exeProfile <-FirstFalse <$>  o ..:? exeProfileArgName
+    libStrip <- FirstTrue <$> o ..:? libStripArgName
+    exeStrip <-FirstTrue <$>  o ..:? exeStripArgName
+    buildHaddocks <- FirstFalse <$> o ..:? haddockArgName
+    haddockOpts <- jsonSubWarnings (o ..:? haddockOptsArgName ..!= mempty)
+    openHaddocks <- FirstFalse <$> o ..:? openHaddocksArgName
+    haddockDeps <- First <$> o ..:? haddockDepsArgName
+    haddockInternal <- FirstFalse <$> o ..:? haddockInternalArgName
+    haddockHyperlinkSource <- FirstTrue <$> o ..:? haddockHyperlinkSourceArgName
+    haddockForHackage <-  FirstFalse <$> o ..:? haddockForHackageArgName
+    installExes <- FirstFalse <$> o ..:? installExesArgName
+    installCompilerTool <- FirstFalse <$> o ..:? installCompilerToolArgName
+    preFetch <- FirstFalse <$> o ..:? preFetchArgName
+    keepGoing <- First <$> o ..:? keepGoingArgName
+    keepTmpFiles <- FirstFalse <$> o ..:? keepTmpFilesArgName
+    forceDirty <- FirstFalse <$> o ..:? forceDirtyArgName
+    tests <- FirstFalse <$> o ..:? testsArgName
+    testOpts <- jsonSubWarnings (o ..:? testOptsArgName ..!= mempty)
+    benchmarks <- FirstFalse <$> o ..:? benchmarksArgName
+    benchmarkOpts <- jsonSubWarnings (o ..:? benchmarkOptsArgName ..!= mempty)
+    reconfigure <- FirstFalse <$> o ..:? reconfigureArgName
+    cabalVerbosity <- First <$> o ..:? cabalVerbosityArgName
+    cabalVerbose' <- FirstFalse <$> o ..:? cabalVerboseArgName
+    let cabalVerbose = cabalVerbosity <> toFirstCabalVerbosity cabalVerbose'
+    splitObjs <- FirstFalse <$> o ..:? splitObjsName
+    skipComponents <- o ..:? skipComponentsName ..!= mempty
+    interleavedOutput <- FirstTrue <$> o ..:? interleavedOutputName
+    progressBar <- First <$> o ..:? progressBarName
+    ddumpDir <- o ..:? ddumpDirName ..!= mempty
+    pure BuildOptsMonoid
+      { trace
+      , profile
+      , noStrip
+      , libProfile
+      , exeProfile
+      , libStrip
+      , exeStrip
+      , buildHaddocks
+      , haddockOpts
+      , openHaddocks
+      , haddockDeps
+      , haddockInternal
+      , haddockHyperlinkSource
+      , haddockForHackage
+      , installExes
+      , installCompilerTool
+      , preFetch
+      , keepGoing
+      , keepTmpFiles
+      , forceDirty
+      , tests
+      , testOpts
+      , benchmarks
+      , benchmarkOpts
+      , reconfigure
+      , cabalVerbose
+      , splitObjs
+      , skipComponents
+      , interleavedOutput
+      , progressBar
+      , ddumpDir
+      }
+
+libProfileArgName :: Text
+libProfileArgName = "library-profiling"
+
+exeProfileArgName :: Text
+exeProfileArgName = "executable-profiling"
+
+libStripArgName :: Text
+libStripArgName = "library-stripping"
+
+exeStripArgName :: Text
+exeStripArgName = "executable-stripping"
+
+haddockArgName :: Text
+haddockArgName = "haddock"
+
+haddockOptsArgName :: Text
+haddockOptsArgName = "haddock-arguments"
+
+openHaddocksArgName :: Text
+openHaddocksArgName = "open-haddocks"
+
+haddockDepsArgName :: Text
+haddockDepsArgName = "haddock-deps"
+
+haddockInternalArgName :: Text
+haddockInternalArgName = "haddock-internal"
+
+haddockHyperlinkSourceArgName :: Text
+haddockHyperlinkSourceArgName = "haddock-hyperlink-source"
+
+haddockForHackageArgName :: Text
+haddockForHackageArgName = "haddock-for-hackage"
+
+installExesArgName :: Text
+installExesArgName = "copy-bins"
+
+installCompilerToolArgName :: Text
+installCompilerToolArgName = "copy-compiler-tool"
+
+preFetchArgName :: Text
+preFetchArgName = "prefetch"
+
+keepGoingArgName :: Text
+keepGoingArgName = "keep-going"
+
+keepTmpFilesArgName :: Text
+keepTmpFilesArgName = "keep-tmp-files"
+
+forceDirtyArgName :: Text
+forceDirtyArgName = "force-dirty"
+
+testsArgName :: Text
+testsArgName = "test"
+
+testOptsArgName :: Text
+testOptsArgName = "test-arguments"
+
+benchmarksArgName :: Text
+benchmarksArgName = "bench"
+
+benchmarkOptsArgName :: Text
+benchmarkOptsArgName = "benchmark-opts"
+
+reconfigureArgName :: Text
+reconfigureArgName = "reconfigure"
+
+cabalVerbosityArgName :: Text
+cabalVerbosityArgName = "cabal-verbosity"
+
+cabalVerboseArgName :: Text
+cabalVerboseArgName = "cabal-verbose"
+
+splitObjsName :: Text
+splitObjsName = "split-objs"
+
+skipComponentsName :: Text
+skipComponentsName = "skip-components"
+
+interleavedOutputName :: Text
+interleavedOutputName = "interleaved-output"
+
+progressBarName :: Text
+progressBarName = "progress-bar"
+
+ddumpDirName :: Text
+ddumpDirName = "ddump-dir"
+
+instance Semigroup BuildOptsMonoid where
+  (<>) = mappenddefault
+
+instance Monoid BuildOptsMonoid where
+  mempty = memptydefault
+  mappend = (<>)
+
+data TestOptsMonoid = TestOptsMonoid
+  { rerunTests :: !FirstTrue
+  , additionalArgs :: ![String]
+  , coverage :: !FirstFalse
+  , disableRun :: !FirstFalse
+  , maximumTimeSeconds :: !(First (Maybe Int))
+  , allowStdin :: !FirstTrue
+  }
+  deriving (Show, Generic)
+
+instance FromJSON (WithJSONWarnings TestOptsMonoid) where
+  parseJSON = withObjectWarnings "TestOptsMonoid" $ \o -> do
+    rerunTests <- FirstTrue <$> o ..:? rerunTestsArgName
+    additionalArgs <- o ..:? testAdditionalArgsName ..!= []
+    coverage <- FirstFalse <$> o ..:? coverageArgName
+    disableRun <- FirstFalse <$> o ..:? testDisableRunArgName
+    maximumTimeSeconds <- First <$> o ..:? maximumTimeSecondsArgName
+    allowStdin <- FirstTrue <$> o ..:? testsAllowStdinName
+    pure TestOptsMonoid
+      { rerunTests
+      , additionalArgs
+      , coverage
+      , disableRun
+      , maximumTimeSeconds
+      , allowStdin
+      }
+
+rerunTestsArgName :: Text
+rerunTestsArgName = "rerun-tests"
+
+testAdditionalArgsName :: Text
+testAdditionalArgsName = "additional-args"
+
+coverageArgName :: Text
+coverageArgName = "coverage"
+
+testDisableRunArgName :: Text
+testDisableRunArgName = "no-run-tests"
+
+maximumTimeSecondsArgName :: Text
+maximumTimeSecondsArgName = "test-suite-timeout"
+
+testsAllowStdinName :: Text
+testsAllowStdinName = "tests-allow-stdin"
+
+instance Semigroup TestOptsMonoid where
+  (<>) = mappenddefault
+
+instance Monoid TestOptsMonoid where
+  mempty = memptydefault
+  mappend = (<>)
+
+newtype HaddockOptsMonoid = HaddockOptsMonoid
+  { additionalArgs :: [String]
+  }
+  deriving (Generic, Show)
+
+instance FromJSON (WithJSONWarnings HaddockOptsMonoid) where
+  parseJSON = withObjectWarnings "HaddockOptsMonoid" $ \o -> do
+    additionalArgs <- o ..:? haddockAdditionalArgsName ..!= []
+    pure HaddockOptsMonoid { additionalArgs }
+
+instance Semigroup HaddockOptsMonoid where
+  (<>) = mappenddefault
+
+instance Monoid HaddockOptsMonoid where
+  mempty = memptydefault
+  mappend = (<>)
+
+haddockAdditionalArgsName :: Text
+haddockAdditionalArgsName = "haddock-args"
+
+data BenchmarkOptsMonoid = BenchmarkOptsMonoid
+  { additionalArgs :: !(First String)
+  , disableRun :: !(First Bool)
+  }
+  deriving (Generic, Show)
+
+instance FromJSON (WithJSONWarnings BenchmarkOptsMonoid) where
+  parseJSON = withObjectWarnings "BenchmarkOptsMonoid" $ \o -> do
+    additionalArgs <- First <$> o ..:? benchmarkAdditionalArgsName
+    disableRun <- First <$> o ..:? benchmarkDisableRunArgName
+    pure BenchmarkOptsMonoid
+      { additionalArgs
+      , disableRun
+      }
+
+benchmarkAdditionalArgsName :: Text
+benchmarkAdditionalArgsName = "benchmark-arguments"
+
+benchmarkDisableRunArgName :: Text
+benchmarkDisableRunArgName = "no-run-benchmarks"
+
+instance Semigroup BenchmarkOptsMonoid where
+  (<>) = mappenddefault
+
+instance Monoid BenchmarkOptsMonoid where
+  mempty = memptydefault
+  mappend :: BenchmarkOptsMonoid -> BenchmarkOptsMonoid -> BenchmarkOptsMonoid
+  mappend = (<>)
+
+newtype CabalVerbosity
+  = CabalVerbosity Verbosity
+  deriving (Eq, Show)
+
+toFirstCabalVerbosity :: FirstFalse -> First CabalVerbosity
+toFirstCabalVerbosity vf = First $ vf.firstFalse <&> \p ->
+  if p then verboseLevel else normalLevel
+ where
+  verboseLevel = CabalVerbosity verbose
+  normalLevel  = CabalVerbosity normal
+
+instance FromJSON CabalVerbosity where
+
+  parseJSON = withText "CabalVerbosity" $ \t ->
+    let s = T.unpack t
+        errMsg = fail $ "Unrecognised Cabal verbosity: " ++ s
+    in  maybe errMsg pure (simpleParsec s)
+
+instance Parsec CabalVerbosity where
+  parsec = CabalVerbosity <$> parsec
+
+buildOptsMonoidHaddockL :: Lens' BuildOptsMonoid (Maybe Bool)
+buildOptsMonoidHaddockL =
+  lens (.buildHaddocks.firstFalse)
+    (\buildMonoid t -> buildMonoid {buildHaddocks = FirstFalse t})
+
+buildOptsMonoidTestsL :: Lens' BuildOptsMonoid (Maybe Bool)
+buildOptsMonoidTestsL =
+  lens (.tests.firstFalse)
+    (\buildMonoid t -> buildMonoid {tests = FirstFalse t})
+
+buildOptsMonoidBenchmarksL :: Lens' BuildOptsMonoid (Maybe Bool)
+buildOptsMonoidBenchmarksL =
+  lens (.benchmarks.firstFalse)
+    (\buildMonoid t -> buildMonoid {benchmarks = FirstFalse t})
+
+buildOptsMonoidInstallExesL :: Lens' BuildOptsMonoid (Maybe Bool)
+buildOptsMonoidInstallExesL =
+  lens (.installExes.firstFalse)
+    (\buildMonoid t -> buildMonoid {installExes = FirstFalse t})
+
+-- Type representing formats of Stack's progress bar when building.
+data ProgressBarFormat
+  = NoBar -- No progress bar at all.
+  | CountOnlyBar -- A bar that only counts packages.
+  | CappedBar -- A bar capped at a length equivalent to the terminal's width.
+  | FullBar -- A full progress bar.
+  deriving (Eq, Show)
+
+instance FromJSON ProgressBarFormat where
+  parseJSON = withText "ProgressBarFormat" $ \t -> either
+    fail
+    pure
+    (readProgressBarFormat $ T.unpack t)
+
+-- | Parse ProgressBarFormat from a String.
+readProgressBarFormat :: String -> Either String ProgressBarFormat
+readProgressBarFormat s
+  | s == "none" = pure NoBar
+  | s == "count-only" = pure CountOnlyBar
+  | s == "capped" = pure CappedBar
+  | s == "full" = pure FullBar
+  | otherwise = Left $ "Invalid progress bar format: " ++ s
src/Stack/Types/Casa.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE NoImplicitPrelude  #-}
+{-# LANGUAGE NoFieldSelectors   #-}
 {-# LANGUAGE OverloadedStrings  #-}
-{-# LANGUAGE RecordWildCards    #-}
 
 -- | Casa configuration types.
 
@@ -18,22 +18,23 @@ -- | An uninterpreted representation of Casa configuration options.
 -- Configurations may be "cascaded" using mappend (left-biased).
 data CasaOptsMonoid = CasaOptsMonoid
-  { casaMonoidEnable :: !FirstTrue
-  , casaMonoidRepoPrefix :: !(First CasaRepoPrefix)
-  , casaMonoidMaxKeysPerRequest :: !(First Int)
+  { enable :: !FirstTrue
+  , repoPrefix :: !(First CasaRepoPrefix)
+  , maxKeysPerRequest :: !(First Int)
   }
   deriving (Generic, Show)
 
 -- | Decode uninterpreted Casa configuration options from JSON/YAML.
 instance FromJSON (WithJSONWarnings CasaOptsMonoid) where
-  parseJSON = withObjectWarnings "CasaOptsMonoid"
-    ( \o -> do
-        casaMonoidEnable <- FirstTrue <$> o ..:? casaEnableName
-        casaMonoidRepoPrefix <- First <$> o ..:? casaRepoPrefixName
-        casaMonoidMaxKeysPerRequest <-
-          First <$> o ..:? casaMaxKeysPerRequestName
-        pure CasaOptsMonoid {..}
-    )
+  parseJSON = withObjectWarnings "CasaOptsMonoid" $ \o -> do
+    enable <- FirstTrue <$> o ..:? casaEnableName
+    repoPrefix <- First <$> o ..:? casaRepoPrefixName
+    maxKeysPerRequest <- First <$> o ..:? casaMaxKeysPerRequestName
+    pure CasaOptsMonoid
+      { enable
+      , repoPrefix
+      , maxKeysPerRequest
+      }
 
 -- | Left-biased combine Casa configuration options
 instance Semigroup CasaOptsMonoid where
+ src/Stack/Types/CompCollection.hs view
@@ -0,0 +1,183 @@+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE FlexibleContexts    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+-- | A module providing the type 'CompCollection' and associated helper
+-- functions.
+--
+-- The corresponding Cabal approach uses lists. See, for example, the
+-- 'Distribution.Types.PackageDescription.sublibraries',
+-- 'Distribution.Types.PackageDescription.foreignLibs',
+-- 'Distribution.Types.PackageDescription.executables',
+-- 'Distribution.Types.PackageDescription.testSuites', and
+-- 'Distribution.Types.PackageDescription.benchmarks' fields.
+--
+-- Cabal removes all the unbuildable components very early (at the cost of
+-- slightly worse error messages).
+module Stack.Types.CompCollection
+  ( CompCollection
+  , getBuildableSet
+  , getBuildableSetText
+  , getBuildableListText
+  , getBuildableListAs
+  , foldAndMakeCollection
+  , hasBuildableComponent
+  , collectionLookup
+  , collectionKeyValueList
+  , collectionMember
+  , foldComponentToAnotherCollection
+  ) where
+
+import qualified Data.Map as M
+import qualified Data.Set as Set
+import           Stack.Prelude
+import           Stack.Types.Component
+                   ( HasBuildInfo, HasName, StackBuildInfo (..)
+                   , StackUnqualCompName (..)
+                   )
+
+-- | A type representing collections of components, distinguishing buildable
+-- components and non-buildable components.
+data CompCollection component = CompCollection
+  { buildableOnes :: {-# UNPACK #-} !(InnerCollection component)
+  , unbuildableOnes :: Set StackUnqualCompName
+    -- ^ The field is lazy beacause it should only serve when users explicitely
+    -- require unbuildable components to be built. The field allows for
+    -- intelligible error messages.
+  }
+  deriving (Show)
+
+instance Semigroup (CompCollection component) where
+  a <> b = CompCollection
+    { buildableOnes = a.buildableOnes <> b.buildableOnes
+    , unbuildableOnes = a.unbuildableOnes <> b.unbuildableOnes
+    }
+
+instance Monoid (CompCollection component) where
+  mempty = CompCollection
+    { buildableOnes = mempty
+    , unbuildableOnes = mempty
+    }
+
+instance Foldable CompCollection where
+  foldMap fn collection = foldMap fn collection.buildableOnes
+  foldr' fn c collection = M.foldr' fn c collection.buildableOnes
+  null = M.null . (.buildableOnes)
+
+-- | The 'Data.HashMap.Strict.HashMap' type is a more suitable choice than 'Map'
+-- for 'Data.Text.Text' based keys in general (it scales better). However,
+-- constant factors are largely dominant for maps with less than 1000 keys.
+-- Packages with more than 100 components are extremely unlikely, so we use a
+-- 'Map'.
+type InnerCollection component = Map StackUnqualCompName component
+
+-- | A function to add a component to a collection of components. Ensures that
+-- both 'asNameMap' and 'asNameSet' are updated consistently.
+addComponent ::
+     HasName component
+  => component
+     -- ^ Component to add.
+  -> InnerCollection component
+     -- ^ Existing collection of components.
+  -> InnerCollection component
+addComponent component = M.insert component.name component
+
+-- | For the given function and foldable data structure of components of type
+-- @compA@, iterates on the elements of that structure and maps each element to
+-- a component of type @compB@ while building a 'CompCollection'.
+foldAndMakeCollection ::
+     (HasBuildInfo compB, HasName compB, Foldable sourceCollection)
+  => (compA -> compB)
+     -- ^ Function to apply to each element in the data struture.
+  -> sourceCollection compA
+     -- ^ Given foldable data structure of components of type @compA@.
+  -> CompCollection compB
+foldAndMakeCollection mapFn = foldl' compIterator mempty
+ where
+  compIterator existingCollection component =
+    compCreator existingCollection (mapFn component)
+  compCreator existingCollection component
+    | component.buildInfo.buildable = existingCollection
+        { buildableOnes =
+            addComponent component existingCollection.buildableOnes
+        }
+    | otherwise = existingCollection
+        { unbuildableOnes =
+            Set.insert component.name existingCollection.unbuildableOnes
+        }
+
+-- | Get the names of the buildable components in the given collection, as a
+-- 'Set' of 'StackUnqualCompName'.
+getBuildableSet :: CompCollection component -> Set StackUnqualCompName
+getBuildableSet = M.keysSet . (.buildableOnes)
+
+-- | Get the names of the buildable components in the given collection, as a
+-- 'Set' of 'Text'.
+getBuildableSetText :: CompCollection component -> Set Text
+getBuildableSetText = Set.mapMonotonic (.unqualCompToText) . getBuildableSet
+
+-- | Get the names of the buildable components in the given collection, as a
+-- list of 'Text.
+getBuildableListText :: CompCollection component -> [Text]
+getBuildableListText = getBuildableListAs (.unqualCompToText)
+
+-- | Apply the given function to the names of the buildable components in the
+-- given collection, yielding a list.
+getBuildableListAs ::
+     (StackUnqualCompName -> something)
+     -- ^ Function to apply to buildable components.
+  -> CompCollection component
+     -- ^ Collection of components.
+  -> [something]
+getBuildableListAs fn = Set.foldr' (\v l -> fn v:l) [] . getBuildableSet
+
+-- | Yields 'True' if, and only if, the given collection includes at least one
+-- buildable component.
+hasBuildableComponent :: CompCollection component -> Bool
+hasBuildableComponent = not . null . getBuildableSet
+
+-- | For the given name of a buildable component and the given collection of
+-- components, yields 'Just' @component@ if the collection includes a buildable
+-- component of that name, and 'Nothing' otherwise.
+collectionLookup ::
+     Text
+     -- ^ Name of the buildable component.
+  -> CompCollection component
+     -- ^ Collection of components.
+  -> Maybe component
+collectionLookup needle haystack =
+  M.lookup (StackUnqualCompName needle) haystack.buildableOnes
+
+-- | For a given collection of components, yields a list of pairs for buildable
+-- components of the name of the component and the component.
+collectionKeyValueList :: CompCollection component -> [(Text, component)]
+collectionKeyValueList haystack =
+      (\(StackUnqualCompName k, !v) -> (k, v))
+  <$> M.toList haystack.buildableOnes
+
+-- | Yields 'True' if, and only if, the given collection of components includes
+-- a buildable component with the given name.
+collectionMember ::
+     Text
+     -- ^ Name of the buildable component.
+  -> CompCollection component
+     -- ^ Collection of components.
+  -> Bool
+collectionMember needle haystack = isJust $ collectionLookup needle haystack
+
+-- | Reduce the buildable components of the given collection of components by
+-- applying the given binary operator to all buildable components, using the
+-- given starting value (typically the right-identity of the operator).
+foldComponentToAnotherCollection ::
+     (Monad m)
+  => CompCollection component
+     -- ^ Collection of components.
+  -> (component -> m a -> m a)
+     -- ^ Binary operator.
+  -> m a
+     -- ^ Starting value.
+  -> m a
+foldComponentToAnotherCollection collection fn initialValue =
+  M.foldr' fn initialValue collection.buildableOnes
src/Stack/Types/CompilerPaths.hs view
@@ -1,4 +1,6 @@-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 
 module Stack.Types.CompilerPaths
   ( CompilerPaths (..)
@@ -20,30 +22,30 @@ 
 -- | Paths on the filesystem for the compiler we're using
 data CompilerPaths = CompilerPaths
-  { cpCompilerVersion :: !ActualCompiler
-  , cpArch :: !Arch
-  , cpBuild :: !CompilerBuild
-  , cpCompiler :: !(Path Abs File)
-  , cpPkg :: !GhcPkgExe
+  { compilerVersion :: !ActualCompiler
+  , arch :: !Arch
+  , build :: !CompilerBuild
+  , compiler :: !(Path Abs File)
+  , pkg :: !GhcPkgExe
     -- ^ ghc-pkg or equivalent
-  , cpInterpreter :: !(Path Abs File)
+  , interpreter :: !(Path Abs File)
     -- ^ runghc
-  , cpHaddock :: !(Path Abs File)
+  , haddock :: !(Path Abs File)
     -- ^ haddock, in 'IO' to allow deferring the lookup
-  , cpSandboxed :: !Bool
+  , sandboxed :: !Bool
     -- ^ Is this a Stack-sandboxed installation?
-  , cpCabalVersion :: !Version
+  , cabalVersion :: !Version
     -- ^ This is the version of Cabal that Stack will use to compile Setup.hs
     -- files in the build process.
     --
     -- Note that this is not necessarily the same version as the one that Stack
     -- depends on as a library and which is displayed when running
     -- @stack ls dependencies | grep Cabal@ in the Stack project.
-  , cpGlobalDB :: !(Path Abs Dir)
+  , globalDB :: !(Path Abs Dir)
     -- ^ Global package database
-  , cpGhcInfo :: !ByteString
+  , ghcInfo :: !ByteString
     -- ^ Output of @ghc --info@
-  , cpGlobalDump :: !(Map PackageName DumpPackage)
+  , globalDump :: !(Map PackageName DumpPackage)
   }
   deriving Show
 
@@ -61,20 +63,20 @@   deriving Show
 
 cabalVersionL :: HasCompiler env => SimpleGetter env Version
-cabalVersionL = compilerPathsL.to cpCabalVersion
+cabalVersionL = compilerPathsL . to (.cabalVersion)
 
 compilerVersionL :: HasCompiler env => SimpleGetter env ActualCompiler
-compilerVersionL = compilerPathsL.to cpCompilerVersion
+compilerVersionL = compilerPathsL . to (.compilerVersion)
 
 cpWhich :: (MonadReader env m, HasCompiler env) => m WhichCompiler
-cpWhich = view $ compilerPathsL.to (whichCompiler.cpCompilerVersion)
+cpWhich = view $ compilerPathsL . to (whichCompiler . (.compilerVersion))
 
 -- | Get the path for the given compiler ignoring any local binaries.
 --
 -- https://github.com/commercialhaskell/stack/issues/1052
 getCompilerPath :: HasCompiler env => RIO env (Path Abs File)
-getCompilerPath = view $ compilerPathsL.to cpCompiler
+getCompilerPath = view $ compilerPathsL . to (.compiler)
 
 -- | Get the 'GhcPkgExe' from a 'HasCompiler' environment
 getGhcPkgExe :: HasCompiler env => RIO env GhcPkgExe
-getGhcPkgExe = view $ compilerPathsL.to cpPkg
+getGhcPkgExe = view $ compilerPathsL . to (.pkg)
+ src/Stack/Types/Component.hs view
@@ -0,0 +1,193 @@+{-# LANGUAGE NoImplicitPrelude          #-}
+{-# LANGUAGE ConstraintKinds            #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE DeriveGeneric              #-}
+{-# LANGUAGE DuplicateRecordFields      #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE NoFieldSelectors           #-}
+{-# LANGUAGE OverloadedRecordDot        #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+
+-- | A module providing the types that represent different sorts of components
+-- of a package (library and sub-library, foreign library, executable, test
+-- suite and benchmark).
+module Stack.Types.Component
+  ( StackLibrary (..)
+  , StackForeignLibrary (..)
+  , StackExecutable (..)
+  , StackTestSuite (..)
+  , StackBenchmark (..)
+  , StackUnqualCompName (..)
+  , StackBuildInfo (..)
+  , HasName
+  , HasBuildInfo
+  , HasComponentInfo
+  ) where
+
+import           Distribution.Compiler ( PerCompilerFlavor )
+import           Distribution.ModuleName ( ModuleName )
+import           Distribution.PackageDescription
+                   ( BenchmarkInterface, Dependency, TestSuiteInterface )
+import           Distribution.Simple ( Extension, Language )
+import           Distribution.Utils.Path ( PackageDir, SourceDir, SymbolicPath )
+import           GHC.Records ( HasField (..) )
+import           Stack.Prelude
+import           Stack.Types.ComponentUtils ( StackUnqualCompName (..) )
+import           Stack.Types.Dependency ( DepValue )
+import           Stack.Types.NamedComponent ( NamedComponent (..) )
+
+-- | A type representing (unnamed) main library or sub-library components of a
+-- package.
+--
+-- Cabal-syntax uses data constructors
+-- 'Distribution.Types.LibraryName.LMainLibName' and
+-- 'Distribution.Types.LibraryName.LSubLibName' to distinguish main libraries
+-- and sub-libraries. We do not do so, as the \'missing\' name in the case of a
+-- main library can be represented by the empty string.
+--
+-- The corresponding Cabal-syntax type is 'Distribution.Types.Library.Library'.
+data StackLibrary = StackLibrary
+  { name :: StackUnqualCompName
+  , buildInfo :: !StackBuildInfo
+  , exposedModules :: [ModuleName]
+    -- |^ This is only used for gathering the files related to this component.
+  }
+  deriving (Show, Typeable)
+
+-- | A type representing foreign library components of a package.
+--
+-- The corresponding Cabal-syntax type is
+-- 'Distribution.Types.Foreign.Libraries.ForeignLib'.
+data StackForeignLibrary = StackForeignLibrary
+  { name :: StackUnqualCompName
+  , buildInfo :: !StackBuildInfo
+  }
+  deriving (Show, Typeable)
+
+-- | A type representing executable components of a package.
+--
+-- The corresponding Cabal-syntax type is
+-- 'Distribution.Types.Executable.Executable'.
+data StackExecutable = StackExecutable
+  { name :: StackUnqualCompName
+  , buildInfo :: !StackBuildInfo
+  , modulePath :: FilePath
+  }
+  deriving (Show, Typeable)
+
+-- | A type representing test suite components of a package.
+--
+-- The corresponding Cabal-syntax type is
+-- 'Distribution.Types.TestSuite.TestSuite'.
+data StackTestSuite = StackTestSuite
+  { name :: StackUnqualCompName
+  , buildInfo :: !StackBuildInfo
+  , interface :: !TestSuiteInterface
+  }
+  deriving (Show, Typeable)
+
+-- | A type representing benchmark components of a package.
+--
+-- The corresponding Cabal-syntax type is
+-- 'Distribution.Types.Benchmark.Benchmark'.
+data StackBenchmark = StackBenchmark
+  { name :: StackUnqualCompName
+  , buildInfo :: StackBuildInfo
+  , interface :: BenchmarkInterface
+    -- ^ This is only used for gathering the files related to this component.
+  }
+  deriving (Show, Typeable)
+
+-- | Type representing the name of an executable.
+newtype ExeName = ExeName Text
+  deriving (Data, Eq, Hashable, IsString, Generic, NFData, Ord, Show, Typeable)
+
+-- | Type representing information needed to build. The file gathering-related
+-- fields are lazy because they are not always needed.
+--
+-- The corresponding Cabal-syntax type is
+-- 'Distribution.Types.BuildInfo.BuildInfo'.
+
+-- We don't use the Cabal-syntax type because Cabal provides a list of
+-- dependencies, and Stack needs a Map and only a small subset of all the
+-- information in Cabal-syntax type.
+data StackBuildInfo = StackBuildInfo
+  { buildable :: !Bool
+    -- ^ Corresponding to Cabal-syntax's
+    -- 'Distribution.Types.BuildInfo.buildable'. The component is buildable
+    -- here.
+  , dependency :: !(Map PackageName DepValue)
+    -- ^ Corresponding to Cabal-syntax's
+    -- 'Distribution.Types.BuildInfo.targetBuildDepends'. Dependencies specific
+    -- to a library or executable target.
+  , unknownTools :: Set Text
+    -- ^ From Cabal-syntax's 'Distribution.Types.BuildInfo.buildTools'. We only
+    -- keep the legacy build tool depends that we know (from a hardcoded list).
+    -- We only use the deduplication aspect of the Set here, as this field is
+    -- only used for error reporting in the end. This is lazy because it's an
+    -- error reporting field only.
+  , otherModules :: [ModuleName]
+    -- ^ Only used in file gathering. See usage in "Stack.ComponentFile" module.
+  , jsSources :: [FilePath]
+    -- ^ Only used in file gathering. See usage in "Stack.ComponentFile" module.
+  , hsSourceDirs :: [SymbolicPath PackageDir SourceDir]
+    -- ^ Only used in file & opts gathering. See usage in "Stack.ComponentFile"
+    -- module for fle gathering.
+  , cSources :: [FilePath]
+    -- ^ Only used in file gathering. See usage in "Stack.ComponentFile" module.
+  , cppOptions :: [String]
+    -- ^ Only used in opts gathering. See usage in "Stack.Package" module.
+  , targetBuildDepends :: [Dependency]
+    -- ^ Only used in opts gathering.
+  , options :: PerCompilerFlavor [String]
+    -- ^ Only used in opts gathering.
+  , allLanguages :: [Language]
+    -- ^ Only used in opts gathering.
+  , usedExtensions :: [Extension]
+    -- ^ Only used in opts gathering.
+  , includeDirs :: [FilePath]
+    -- ^ Only used in opts gathering.
+  , extraLibs :: [String]
+    -- ^ Only used in opts gathering.
+  , extraLibDirs :: [String]
+    -- ^ Only used in opts gathering.
+  , frameworks :: [String]
+    -- ^ Only used in opts gathering.
+  }
+  deriving (Show)
+
+-- | Type synonym for a 'HasField' constraint.
+type HasName component = HasField "name" component StackUnqualCompName
+
+-- | Type synonym for a 'HasField' constraint.
+type HasBuildInfo component = HasField "buildInfo" component StackBuildInfo
+
+instance HasField "qualifiedName" StackLibrary NamedComponent where
+  getField v
+    | rawName == mempty = CLib
+    | otherwise = CSubLib rawName
+    where
+      rawName = v.name.unqualCompToText
+
+instance HasField "qualifiedName" StackForeignLibrary NamedComponent where
+  getField = CFlib . (.name.unqualCompToText)
+
+instance HasField "qualifiedName" StackExecutable NamedComponent where
+  getField = CExe . (.name.unqualCompToText)
+
+instance HasField "qualifiedName" StackTestSuite NamedComponent where
+  getField = CTest . (.name.unqualCompToText)
+
+instance HasField "qualifiedName" StackBenchmark NamedComponent where
+  getField = CTest . (.name.unqualCompToText)
+
+-- | Type synonym for a 'HasField' constraint which represent a virtual field,
+-- computed from the type, the NamedComponent constructor and the name.
+type HasQualiName component = HasField "qualifiedName" component NamedComponent
+
+-- | Type synonym for a 'HasField' constraint for all the common component
+-- fields i.e. @name@, @buildInfo@ and @qualifiedName@.
+type HasComponentInfo component =
+  (HasName component, HasBuildInfo component, HasQualiName component)
+ src/Stack/Types/ComponentUtils.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE NoImplicitPrelude          #-}
+{-# LANGUAGE ConstraintKinds            #-}
+{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE DeriveDataTypeable         #-}
+{-# LANGUAGE DeriveGeneric              #-}
+{-# LANGUAGE DuplicateRecordFields      #-}
+{-# LANGUAGE FlexibleContexts           #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE ScopedTypeVariables        #-}
+
+-- | A module providing a type representing the name of an \'unqualified\'
+-- component and related helper functions.
+module Stack.Types.ComponentUtils
+  ( StackUnqualCompName (..)
+  , fromCabalName
+  , toCabalName
+  ) where
+
+import           Distribution.PackageDescription
+                   ( UnqualComponentName, mkUnqualComponentName
+                   , unUnqualComponentName
+                   )
+import           RIO.Text (pack, unpack)
+import           Stack.Prelude
+
+-- | Type representing the name of an \'unqualified\' component (that is, the
+-- component can be any sort - a (unnamed) main library or sub-library,
+-- an executable, etc. ).
+--
+-- The corresponding The Cabal-syntax type is
+-- 'Distribution.Types.UnqualComponentName.UnqualComponentName'.
+
+-- Ideally, we would use the Cabal-syntax type and not 'Text', to avoid
+-- unnecessary work, but there is no 'Hashable' instance for
+-- 'Distribution.Types.UnqualComponentName.UnqualComponentName' yet.
+newtype StackUnqualCompName = StackUnqualCompName
+  { unqualCompToText :: Text
+  }
+  deriving (Data, Eq, Generic, Hashable, IsString, NFData, Ord, Read, Show, Typeable)
+
+fromCabalName :: UnqualComponentName -> StackUnqualCompName
+fromCabalName unqualName =
+  StackUnqualCompName $ pack . unUnqualComponentName $ unqualName
+
+toCabalName :: StackUnqualCompName -> UnqualComponentName
+toCabalName (StackUnqualCompName unqualName) =
+  mkUnqualComponentName (unpack unqualName)
src/Stack/Types/Config.hs view
@@ -1,13 +1,14 @@-{-# LANGUAGE NoImplicitPrelude     #-}
-{-# LANGUAGE DataKinds             #-}
-{-# LANGUAGE DefaultSignatures     #-}
-{-# LANGUAGE GADTs                 #-}
-{-# LANGUAGE MultiWayIf            #-}
-{-# LANGUAGE OverloadedStrings     #-}
-{-# LANGUAGE QuasiQuotes           #-}
-{-# LANGUAGE RecordWildCards       #-}
-{-# LANGUAGE TypeFamilies          #-}
-{-# LANGUAGE ViewPatterns          #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE DefaultSignatures   #-}
+{-# LANGUAGE GADTs               #-}
+{-# LANGUAGE MultiWayIf          #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE QuasiQuotes         #-}
+{-# LANGUAGE TypeFamilies        #-}
+{-# LANGUAGE ViewPatterns        #-}
 
 module Stack.Types.Config
   (
@@ -57,152 +58,162 @@ 
 -- | The top-level Stackage configuration.
 data Config = Config
-  { configWorkDir             :: !(Path Rel Dir)
+  { workDir                :: !(Path Rel Dir)
     -- ^ this allows to override .stack-work directory
-  , configUserConfigPath      :: !(Path Abs File)
+  , userConfigPath         :: !(Path Abs File)
     -- ^ Path to user configuration file (usually ~/.stack/config.yaml)
-  , configBuild               :: !BuildOpts
+  , build                  :: !BuildOpts
     -- ^ Build configuration
-  , configDocker              :: !DockerOpts
+  , docker                 :: !DockerOpts
     -- ^ Docker configuration
-  , configNix                 :: !NixOpts
+  , nix                    :: !NixOpts
     -- ^ Execution environment (e.g nix-shell) configuration
-  , configProcessContextSettings :: !(EnvSettings -> IO ProcessContext)
+  , processContextSettings :: !(EnvSettings -> IO ProcessContext)
     -- ^ Environment variables to be passed to external tools
-  , configLocalProgramsBase   :: !(Path Abs Dir)
+  , localProgramsBase      :: !(Path Abs Dir)
     -- ^ Non-platform-specific path containing local installations
-  , configLocalPrograms       :: !(Path Abs Dir)
+  , localPrograms          :: !(Path Abs Dir)
     -- ^ Path containing local installations (mainly GHC)
-  , configHideTHLoading       :: !Bool
+  , hideTHLoading          :: !Bool
     -- ^ Hide the Template Haskell "Loading package ..." messages from the
     -- console
-  , configPrefixTimestamps    :: !Bool
+  , prefixTimestamps       :: !Bool
     -- ^ Prefix build output with timestamps for each line.
-  , configPlatform            :: !Platform
+  , platform               :: !Platform
     -- ^ The platform we're building for, used in many directory names
-  , configPlatformVariant     :: !PlatformVariant
+  , platformVariant        :: !PlatformVariant
     -- ^ Variant of the platform, also used in directory names
-  , configGHCVariant          :: !(Maybe GHCVariant)
+  , ghcVariant             :: !(Maybe GHCVariant)
     -- ^ The variant of GHC requested by the user.
-  , configGHCBuild            :: !(Maybe CompilerBuild)
+  , ghcBuild               :: !(Maybe CompilerBuild)
     -- ^ Override build of the compiler distribution (e.g. standard, gmp4,
     -- tinfo6)
-  , configLatestSnapshot      :: !Text
+  , latestSnapshot         :: !Text
     -- ^ URL of a JSON file providing the latest LTS and Nightly snapshots.
-  , configSystemGHC           :: !Bool
+  , systemGHC              :: !Bool
     -- ^ Should we use the system-installed GHC (on the PATH) if
     -- available? Can be overridden by command line options.
-  , configInstallGHC          :: !Bool
+  , installGHC             :: !Bool
     -- ^ Should we automatically install GHC if missing or the wrong
     -- version is available? Can be overridden by command line options.
-  , configSkipGHCCheck        :: !Bool
+  , skipGHCCheck           :: !Bool
     -- ^ Don't bother checking the GHC version or architecture.
-  , configSkipMsys            :: !Bool
+  , skipMsys               :: !Bool
     -- ^ On Windows: don't use a sandboxed MSYS
-  , configCompilerCheck       :: !VersionCheck
+  , compilerCheck          :: !VersionCheck
     -- ^ Specifies which versions of the compiler are acceptable.
-  , configCompilerRepository  :: !CompilerRepository
+  , compilerRepository     :: !CompilerRepository
     -- ^ Specifies the repository containing the compiler sources
-  , configLocalBin            :: !(Path Abs Dir)
+  , localBin               :: !(Path Abs Dir)
     -- ^ Directory we should install executables into
-  , configRequireStackVersion :: !VersionRange
+  , requireStackVersion    :: !VersionRange
     -- ^ Require a version of Stack within this range.
-  , configJobs                :: !Int
+  , jobs                   :: !Int
     -- ^ How many concurrent jobs to run, defaults to number of capabilities
-  , configOverrideGccPath     :: !(Maybe (Path Abs File))
+  , overrideGccPath        :: !(Maybe (Path Abs File))
     -- ^ Optional gcc override path
-  , configExtraIncludeDirs    :: ![FilePath]
+  , extraIncludeDirs       :: ![FilePath]
     -- ^ --extra-include-dirs arguments
-  , configExtraLibDirs        :: ![FilePath]
+  , extraLibDirs           :: ![FilePath]
     -- ^ --extra-lib-dirs arguments
-  , configCustomPreprocessorExts :: ![Text]
+  , customPreprocessorExts :: ![Text]
     -- ^ List of custom preprocessors to complete the hard coded ones
-  , configConcurrentTests     :: !Bool
+  , concurrentTests        :: !Bool
     -- ^ Run test suites concurrently
-  , configTemplateParams      :: !(Map Text Text)
+  , templateParams         :: !(Map Text Text)
     -- ^ Parameters for templates.
-  , configScmInit             :: !(Maybe SCM)
+  , scmInit                :: !(Maybe SCM)
     -- ^ Initialize SCM (e.g. git) when creating new projects.
-  , configGhcOptionsByName    :: !(Map PackageName [Text])
+  , ghcOptionsByName       :: !(Map PackageName [Text])
     -- ^ Additional GHC options to apply to specific packages.
-  , configGhcOptionsByCat     :: !(Map ApplyGhcOptions [Text])
+  , ghcOptionsByCat        :: !(Map ApplyGhcOptions [Text])
     -- ^ Additional GHC options to apply to categories of packages
-  , configCabalConfigOpts     :: !(Map CabalConfigKey [Text])
+  , cabalConfigOpts        :: !(Map CabalConfigKey [Text])
     -- ^ Additional options to be passed to ./Setup.hs configure
-  , configSetupInfoLocations  :: ![String]
+  , setupInfoLocations     :: ![String]
     -- ^ URLs or paths to stack-setup.yaml files, for finding tools.
     -- If none present, the default setup-info is used.
-  , configSetupInfoInline     :: !SetupInfo
+  , setupInfoInline        :: !SetupInfo
     -- ^ Additional SetupInfo to use to find tools.
-  , configPvpBounds           :: !PvpBounds
+  , pvpBounds              :: !PvpBounds
     -- ^ How PVP upper bounds should be added to packages
-  , configModifyCodePage      :: !Bool
+  , modifyCodePage         :: !Bool
     -- ^ Force the code page to UTF-8 on Windows
-  , configRebuildGhcOptions   :: !Bool
+  , rebuildGhcOptions      :: !Bool
     -- ^ Rebuild on GHC options changes
-  , configApplyGhcOptions     :: !ApplyGhcOptions
+  , applyGhcOptions        :: !ApplyGhcOptions
     -- ^ Which packages do --ghc-options on the command line apply to?
-  , configApplyProgOptions     :: !ApplyProgOptions
+  , applyProgOptions       :: !ApplyProgOptions
     -- ^ Which packages do all and any --PROG-option options on the command line
     -- apply to?
-  , configAllowNewer          :: !Bool
+  , allowNewer             :: !Bool
     -- ^ Ignore version ranges in .cabal files. Funny naming chosen to
     -- match cabal.
-  , configAllowNewerDeps      :: !(Maybe [PackageName])
+  , allowNewerDeps         :: !(Maybe [PackageName])
     -- ^ Ignore dependency upper and lower bounds only for specified
     -- packages. No effect unless allow-newer is enabled.
-  , configDefaultTemplate     :: !(Maybe TemplateName)
+  , defaultTemplate        :: !(Maybe TemplateName)
     -- ^ The default template to use when none is specified.
     -- (If Nothing, the 'default' default template is used.)
-  , configAllowDifferentUser  :: !Bool
+  , allowDifferentUser     :: !Bool
     -- ^ Allow users other than the Stack root owner to use the Stack
     -- installation.
-  , configDumpLogs            :: !DumpLogs
+  , dumpLogs               :: !DumpLogs
     -- ^ Dump logs of local non-dependencies when doing a build.
-  , configProject             :: !(ProjectConfig (Project, Path Abs File))
+  , project                :: !(ProjectConfig (Project, Path Abs File))
     -- ^ Project information and stack.yaml file location
-  , configAllowLocals         :: !Bool
+  , allowLocals            :: !Bool
     -- ^ Are we allowed to build local packages? The script
     -- command disallows this.
-  , configSaveHackageCreds    :: !Bool
+  , saveHackageCreds       :: !Bool
     -- ^ Should we save Hackage credentials to a file?
-  , configHackageBaseUrl      :: !Text
+  , hackageBaseUrl         :: !Text
     -- ^ Hackage base URL used when uploading packages
-  , configRunner              :: !Runner
-  , configPantryConfig        :: !PantryConfig
-  , configStackRoot           :: !(Path Abs Dir)
-  , configResolver            :: !(Maybe AbstractResolver)
+  , runner                 :: !Runner
+  , pantryConfig           :: !PantryConfig
+  , stackRoot              :: !(Path Abs Dir)
+  , resolver               :: !(Maybe AbstractResolver)
     -- ^ Any resolver override from the command line
-  , configUserStorage         :: !UserStorage
+  , userStorage            :: !UserStorage
     -- ^ Database connection pool for user Stack database
-  , configHideSourcePaths     :: !Bool
+  , hideSourcePaths        :: !Bool
     -- ^ Enable GHC hiding source paths?
-  , configRecommendUpgrade    :: !Bool
+  , recommendUpgrade       :: !Bool
     -- ^ Recommend a Stack upgrade?
-  , configNoRunCompile   :: !Bool
+  , notifyIfNixOnPath      :: !Bool
+    -- ^ Notify if the Nix package manager (nix) is on the PATH, but
+    -- Stack's Nix integration is not enabled?
+  , notifyIfGhcUntested    :: !Bool
+    -- ^ Notify if Stack has not been tested with the GHC version?
+  , notifyIfCabalUntested  :: !Bool
+    -- ^ Notify if Stack has not been tested with the Cabal version?
+  , notifyIfArchUnknown    :: !Bool
+    -- ^ Notify if the specified machine architecture is unknown to Cabal (the
+    -- library)?
+  , noRunCompile           :: !Bool
     -- ^ Use --no-run and --compile options when using `stack script`
-  , configStackDeveloperMode  :: !Bool
+  , stackDeveloperMode     :: !Bool
     -- ^ Turn on Stack developer mode for additional messages?
-  , configCasa                :: !(Maybe (CasaRepoPrefix, Int))
+  , casa                   :: !(Maybe (CasaRepoPrefix, Int))
     -- ^ Optional Casa configuration
   }
 
 -- | The project root directory, if in a project.
 configProjectRoot :: Config -> Maybe (Path Abs Dir)
 configProjectRoot c =
-  case configProject c of
+  case c.project of
     PCProject (_, fp) -> Just $ parent fp
     PCGlobalProject -> Nothing
     PCNoProject _deps -> Nothing
 
 -- | Get the URL to request the information on the latest snapshots
 askLatestSnapshotUrl :: (MonadReader env m, HasConfig env) => m Text
-askLatestSnapshotUrl = view $ configL.to configLatestSnapshot
+askLatestSnapshotUrl = view $ configL . to (.latestSnapshot)
 
 -- | @STACK_ROOT\/hooks\/@
 hooksDir :: HasConfig env => RIO env (Path Abs Dir)
 hooksDir = do
-  sr <- view $ configL.to configStackRoot
+  sr <- view $ configL . to (.stackRoot)
   pure (sr </> [reldir|hooks|])
 
 -- | @STACK_ROOT\/hooks\/ghc-install.sh@
@@ -230,67 +241,69 @@ -----------------------------------
 
 instance HasPlatform Config where
-  platformL = lens configPlatform (\x y -> x { configPlatform = y })
+  platformL = lens (.platform) (\x y -> x { platform = y })
   platformVariantL =
-    lens configPlatformVariant (\x y -> x { configPlatformVariant = y })
+    lens (.platformVariant) (\x y -> x { platformVariant = y })
 
 instance HasGHCVariant Config where
-  ghcVariantL = to $ fromMaybe GHCStandard . configGHCVariant
+  ghcVariantL = to $ fromMaybe GHCStandard . (.ghcVariant)
 
 instance HasProcessContext Config where
-  processContextL = runnerL.processContextL
+  processContextL = runnerL . processContextL
 
 instance HasPantryConfig Config where
-  pantryConfigL = lens configPantryConfig (\x y -> x { configPantryConfig = y })
+  pantryConfigL = lens
+    (.pantryConfig)
+    (\x y -> x { pantryConfig = y })
 
 instance HasConfig Config where
   configL = id
   {-# INLINE configL #-}
 
 instance HasRunner Config where
-  runnerL = lens configRunner (\x y -> x { configRunner = y })
+  runnerL = lens (.runner) (\x y -> x { runner = y })
 
 instance HasLogFunc Config where
-  logFuncL = runnerL.logFuncL
+  logFuncL = runnerL . logFuncL
 
 instance HasStylesUpdate Config where
-  stylesUpdateL = runnerL.stylesUpdateL
+  stylesUpdateL = runnerL . stylesUpdateL
 
 instance HasTerm Config where
-  useColorL = runnerL.useColorL
-  termWidthL = runnerL.termWidthL
+  useColorL = runnerL . useColorL
+  termWidthL = runnerL . termWidthL
 
 -----------------------------------
 -- Helper lenses
 -----------------------------------
 
 stackRootL :: HasConfig s => Lens' s (Path Abs Dir)
-stackRootL = configL.lens configStackRoot (\x y -> x { configStackRoot = y })
+stackRootL =
+  configL . lens (.stackRoot) (\x y -> x { stackRoot = y })
 
 stackGlobalConfigL :: HasConfig s => Lens' s (Path Abs File)
-stackGlobalConfigL =
-  configL.lens configUserConfigPath (\x y -> x { configUserConfigPath = y })
+stackGlobalConfigL = configL . lens
+  (.userConfigPath)
+  (\x y -> x { userConfigPath = y })
 
 buildOptsL :: HasConfig s => Lens' s BuildOpts
-buildOptsL = configL.lens
-  configBuild
-  (\x y -> x { configBuild = y })
+buildOptsL = configL . lens (.build) (\x y -> x { build = y })
 
 envOverrideSettingsL ::
      HasConfig env
   => Lens' env (EnvSettings -> IO ProcessContext)
-envOverrideSettingsL = configL.lens
-  configProcessContextSettings
-  (\x y -> x { configProcessContextSettings = y })
+envOverrideSettingsL = configL . lens
+  (.processContextSettings)
+  (\x y -> x { processContextSettings = y })
 
 -- | @".stack-work"@
 workDirL :: HasConfig env => Lens' env (Path Rel Dir)
-workDirL = configL.lens configWorkDir (\x y -> x { configWorkDir = y })
+workDirL = configL . lens (.workDir) (\x y -> x { workDir = y })
 
 -- | In dev mode, print as a warning, otherwise as debug
 prettyStackDevL :: HasConfig env => [StyleDoc] -> RIO env ()
 prettyStackDevL docs = do
   config <- view configL
-  if configStackDeveloperMode config
+  if config.stackDeveloperMode
     then prettyWarnL docs
     else prettyDebugL docs
src/Stack/Types/Config/Exception.hs view
@@ -222,7 +222,7 @@     go (name, dirs) =
          blankLine
       <> fillSep
-           [ style Error (fromString $ packageNameString name)
+           [ style Error (fromPackageName name)
            , flow "used in:"
            ]
       <> line
src/Stack/Types/ConfigMonoid.hs view
@@ -1,6 +1,8 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 module Stack.Types.ConfigMonoid
   ( ConfigMonoid (..)
@@ -28,11 +30,11 @@ import qualified Data.Yaml as Yaml
 import           Distribution.Version ( anyVersion )
 import           Generics.Deriving.Monoid ( mappenddefault, memptydefault )
-import           Stack.Prelude
+import           Stack.Prelude hiding ( snapshotLocation )
 import           Stack.Types.AllowNewerDeps ( AllowNewerDeps )
 import           Stack.Types.ApplyGhcOptions ( ApplyGhcOptions (..) )
 import           Stack.Types.ApplyProgOptions ( ApplyProgOptions (..) )
-import           Stack.Types.BuildOpts ( BuildOptsMonoid )
+import           Stack.Types.BuildOptsMonoid ( BuildOptsMonoid )
 import           Stack.Types.Casa ( CasaOptsMonoid )
 import           Stack.Types.CabalConfigKey ( CabalConfigKey )
 import           Stack.Types.ColorWhen ( ColorWhen )
@@ -55,128 +57,136 @@ -- | An uninterpreted representation of configuration options.
 -- Configurations may be "cascaded" using mappend (left-biased).
 data ConfigMonoid = ConfigMonoid
-  { configMonoidStackRoot          :: !(First (Path Abs Dir))
+  { stackRoot          :: !(First (Path Abs Dir))
     -- ^ See: 'clStackRoot'
-  , configMonoidWorkDir            :: !(First (Path Rel Dir))
+  , workDir            :: !(First (Path Rel Dir))
     -- ^ See: 'configWorkDir'.
-  , configMonoidBuildOpts          :: !BuildOptsMonoid
+  , buildOpts          :: !BuildOptsMonoid
     -- ^ build options.
-  , configMonoidDockerOpts         :: !DockerOptsMonoid
+  , dockerOpts         :: !DockerOptsMonoid
     -- ^ Docker options.
-  , configMonoidNixOpts            :: !NixOptsMonoid
+  , nixOpts            :: !NixOptsMonoid
     -- ^ Options for the execution environment (nix-shell or container)
-  , configMonoidConnectionCount    :: !(First Int)
+  , connectionCount    :: !(First Int)
     -- ^ See: 'configConnectionCount'
-  , configMonoidHideTHLoading      :: !FirstTrue
+  , hideTHLoading      :: !FirstTrue
     -- ^ See: 'configHideTHLoading'
-  , configMonoidPrefixTimestamps   :: !(First Bool)
+  , prefixTimestamps   :: !(First Bool)
     -- ^ See: 'configPrefixTimestamps'
-  , configMonoidLatestSnapshot     :: !(First Text)
+  , latestSnapshot     :: !(First Text)
     -- ^ See: 'configLatestSnapshot'
-  , configMonoidPackageIndex     :: !(First PackageIndexConfig)
+  , packageIndex     :: !(First PackageIndexConfig)
     -- ^ See: 'withPantryConfig'
-  , configMonoidPackageIndices     :: !(First [PackageIndexConfig])
+  , packageIndices     :: !(First [PackageIndexConfig])
     -- ^ Deprecated in favour of package-index
-  , configMonoidSystemGHC          :: !(First Bool)
+  , systemGHC          :: !(First Bool)
     -- ^ See: 'configSystemGHC'
-  , configMonoidInstallGHC          :: !FirstTrue
+  , installGHC          :: !FirstTrue
     -- ^ See: 'configInstallGHC'
-  , configMonoidSkipGHCCheck        :: !FirstFalse
+  , skipGHCCheck        :: !FirstFalse
     -- ^ See: 'configSkipGHCCheck'
-  , configMonoidSkipMsys            :: !FirstFalse
+  , skipMsys            :: !FirstFalse
     -- ^ See: 'configSkipMsys'
-  , configMonoidCompilerCheck       :: !(First VersionCheck)
+  , compilerCheck       :: !(First VersionCheck)
     -- ^ See: 'configCompilerCheck'
-  , configMonoidCompilerRepository  :: !(First CompilerRepository)
+  , compilerRepository  :: !(First CompilerRepository)
     -- ^ See: 'configCompilerRepository'
-  , configMonoidRequireStackVersion :: !IntersectingVersionRange
+  , requireStackVersion :: !IntersectingVersionRange
     -- ^ See: 'configRequireStackVersion'
-  , configMonoidArch                :: !(First String)
+  , arch                :: !(First String)
     -- ^ Used for overriding the platform
-  , configMonoidGHCVariant          :: !(First GHCVariant)
+  , ghcVariant          :: !(First GHCVariant)
     -- ^ Used for overriding the platform
-  , configMonoidGHCBuild            :: !(First CompilerBuild)
+  , ghcBuild            :: !(First CompilerBuild)
     -- ^ Used for overriding the GHC build
-  , configMonoidJobs                :: !(First Int)
+  , jobs                :: !(First Int)
     -- ^ See: 'configJobs'
-  , configMonoidExtraIncludeDirs    :: ![FilePath]
+  , extraIncludeDirs    :: ![FilePath]
     -- ^ See: 'configExtraIncludeDirs'
-  , configMonoidExtraLibDirs        :: ![FilePath]
+  , extraLibDirs        :: ![FilePath]
     -- ^ See: 'configExtraLibDirs'
-  , configMonoidCustomPreprocessorExts :: ![Text]
+  , customPreprocessorExts :: ![Text]
     -- ^ See: 'configCustomPreprocessorExts'
-  , configMonoidOverrideGccPath    :: !(First (Path Abs File))
+  , overrideGccPath    :: !(First (Path Abs File))
     -- ^ Allow users to override the path to gcc
-  , configMonoidOverrideHpack       :: !(First FilePath)
+  , overrideHpack       :: !(First FilePath)
     -- ^ Use Hpack executable (overrides bundled Hpack)
-  , configMonoidConcurrentTests     :: !(First Bool)
+  , concurrentTests     :: !(First Bool)
     -- ^ See: 'configConcurrentTests'
-  , configMonoidLocalBinPath        :: !(First FilePath)
+  , localBinPath        :: !(First FilePath)
     -- ^ Used to override the binary installation dir
-  , configMonoidTemplateParameters  :: !(Map Text Text)
+  , templateParameters  :: !(Map Text Text)
     -- ^ Template parameters.
-  , configMonoidScmInit             :: !(First SCM)
+  , scmInit             :: !(First SCM)
     -- ^ Initialize SCM (e.g. git init) when making new projects?
-  , configMonoidGhcOptionsByName    :: !(MonoidMap PackageName (Monoid.Dual [Text]))
+  , ghcOptionsByName    :: !(MonoidMap PackageName (Monoid.Dual [Text]))
     -- ^ See 'configGhcOptionsByName'. Uses 'Monoid.Dual' so that
     -- options from the configs on the right come first, so that they
     -- can be overridden.
-  , configMonoidGhcOptionsByCat     :: !(MonoidMap ApplyGhcOptions (Monoid.Dual [Text]))
+  , ghcOptionsByCat     :: !(MonoidMap ApplyGhcOptions (Monoid.Dual [Text]))
     -- ^ See 'configGhcOptionsAll'. Uses 'Monoid.Dual' so that options
     -- from the configs on the right come first, so that they can be
     -- overridden.
-  , configMonoidCabalConfigOpts     :: !(MonoidMap CabalConfigKey (Monoid.Dual [Text]))
+  , cabalConfigOpts     :: !(MonoidMap CabalConfigKey (Monoid.Dual [Text]))
     -- ^ See 'configCabalConfigOpts'.
-  , configMonoidExtraPath           :: ![Path Abs Dir]
+  , extraPath           :: ![Path Abs Dir]
     -- ^ Additional paths to search for executables in
-  , configMonoidSetupInfoLocations  :: ![String]
+  , setupInfoLocations  :: ![String]
     -- ^ See 'configSetupInfoLocations'
-  , configMonoidSetupInfoInline     :: !SetupInfo
+  , setupInfoInline     :: !SetupInfo
     -- ^ See 'configSetupInfoInline'
-  , configMonoidLocalProgramsBase   :: !(First (Path Abs Dir))
+  , localProgramsBase   :: !(First (Path Abs Dir))
     -- ^ Override the default local programs dir, where e.g. GHC is installed.
-  , configMonoidPvpBounds           :: !(First PvpBounds)
+  , pvpBounds           :: !(First PvpBounds)
     -- ^ See 'configPvpBounds'
-  , configMonoidModifyCodePage      :: !FirstTrue
+  , modifyCodePage      :: !FirstTrue
     -- ^ See 'configModifyCodePage'
-  , configMonoidRebuildGhcOptions   :: !FirstFalse
+  , rebuildGhcOptions   :: !FirstFalse
     -- ^ See 'configMonoidRebuildGhcOptions'
-  , configMonoidApplyGhcOptions     :: !(First ApplyGhcOptions)
+  , applyGhcOptions     :: !(First ApplyGhcOptions)
     -- ^ See 'configApplyGhcOptions'
-  , configMonoidApplyProgOptions     :: !(First ApplyProgOptions)
+  , applyProgOptions     :: !(First ApplyProgOptions)
     -- ^ See 'configApplyProgOptions'
-  , configMonoidAllowNewer          :: !(First Bool)
+  , allowNewer          :: !(First Bool)
     -- ^ See 'configMonoidAllowNewer'
-  , configMonoidAllowNewerDeps      :: !(Maybe AllowNewerDeps)
+  , allowNewerDeps      :: !(Maybe AllowNewerDeps)
     -- ^ See 'configMonoidAllowNewerDeps'
-  , configMonoidDefaultTemplate     :: !(First TemplateName)
+  , defaultTemplate     :: !(First TemplateName)
    -- ^ The default template to use when none is specified.
    -- (If Nothing, the 'default' default template is used.)
-  , configMonoidAllowDifferentUser :: !(First Bool)
+  , allowDifferentUser :: !(First Bool)
    -- ^ Allow users other than the Stack root owner to use the Stack
    -- installation.
-  , configMonoidDumpLogs           :: !(First DumpLogs)
+  , dumpLogs           :: !(First DumpLogs)
     -- ^ See 'configDumpLogs'
-  , configMonoidSaveHackageCreds   :: !(First Bool)
+  , saveHackageCreds   :: !(First Bool)
     -- ^ See 'configSaveHackageCreds'
-  , configMonoidHackageBaseUrl     :: !(First Text)
+  , hackageBaseUrl     :: !(First Text)
     -- ^ See 'configHackageBaseUrl'
-  , configMonoidColorWhen          :: !(First ColorWhen)
+  , colorWhen          :: !(First ColorWhen)
     -- ^ When to use 'ANSI' colors
-  , configMonoidStyles             :: !StylesUpdate
-  , configMonoidHideSourcePaths    :: !FirstTrue
+  , styles             :: !StylesUpdate
+  , hideSourcePaths    :: !FirstTrue
     -- ^ See 'configHideSourcePaths'
-  , configMonoidRecommendUpgrade   :: !FirstTrue
+  , recommendUpgrade   :: !FirstTrue
     -- ^ See 'configRecommendUpgrade'
-  , configMonoidCasaOpts :: !CasaOptsMonoid
+  , notifyIfNixOnPath  :: !FirstTrue
+    -- ^ See 'configNotifyIfNixOnPath'
+  , notifyIfGhcUntested  :: !FirstTrue
+    -- ^ See 'configNotifyIfGhcUntested'
+  , notifyIfCabalUntested  :: !FirstTrue
+    -- ^ See 'configNotifyIfCabalUntested'
+  , notifyIfArchUnknown  :: !FirstTrue
+    -- ^ See 'configNotifyIfArchUnknown'
+  , casaOpts :: !CasaOptsMonoid
     -- ^ Casa configuration options.
-  , configMonoidCasaRepoPrefix     :: !(First CasaRepoPrefix)
+  , casaRepoPrefix     :: !(First CasaRepoPrefix)
     -- ^ Casa repository prefix (deprecated).
-  , configMonoidSnapshotLocation :: !(First Text)
+  , snapshotLocation :: !(First Text)
     -- ^ Custom location of LTS/Nightly snapshots
-  , configMonoidNoRunCompile  :: !FirstFalse
+  , noRunCompile  :: !FirstFalse
     -- ^ See: 'configNoRunCompile'
-  , configMonoidStackDeveloperMode :: !(First Bool)
+  , stackDeveloperMode :: !(First Bool)
     -- ^ See 'configStackDeveloperMode'
   }
   deriving (Generic, Show)
@@ -200,23 +210,18 @@ parseConfigMonoidObject :: Path Abs Dir -> Object -> WarningParser ConfigMonoid
 parseConfigMonoidObject rootDir obj = do
   -- Parsing 'stackRoot' from 'stackRoot'/config.yaml would be nonsensical
-  let configMonoidStackRoot = First Nothing
-  configMonoidWorkDir <- First <$> obj ..:? configMonoidWorkDirName
-  configMonoidBuildOpts <-
-    jsonSubWarnings (obj ..:? configMonoidBuildOptsName ..!= mempty)
-  configMonoidDockerOpts <-
+  let stackRoot = First Nothing
+  workDir <- First <$> obj ..:? configMonoidWorkDirName
+  buildOpts <- jsonSubWarnings (obj ..:? configMonoidBuildOptsName ..!= mempty)
+  dockerOpts <-
     jsonSubWarnings (obj ..:? configMonoidDockerOptsName ..!= mempty)
-  configMonoidNixOpts <-
-    jsonSubWarnings (obj ..:? configMonoidNixOptsName ..!= mempty)
-  configMonoidConnectionCount <-
-    First <$> obj ..:? configMonoidConnectionCountName
-  configMonoidHideTHLoading <-
-    FirstTrue <$> obj ..:? configMonoidHideTHLoadingName
-  configMonoidPrefixTimestamps <-
-    First <$> obj ..:? configMonoidPrefixTimestampsName
+  nixOpts <- jsonSubWarnings (obj ..:? configMonoidNixOptsName ..!= mempty)
+  connectionCount <- First <$> obj ..:? configMonoidConnectionCountName
+  hideTHLoading <- FirstTrue <$> obj ..:? configMonoidHideTHLoadingName
+  prefixTimestamps <- First <$> obj ..:? configMonoidPrefixTimestampsName
 
   murls :: Maybe Value <- obj ..:? configMonoidUrlsName
-  configMonoidLatestSnapshot <-
+  latestSnapshot <-
     case murls of
       Nothing -> pure $ First Nothing
       Just urls -> jsonSubWarnings $ lift $ withObjectWarnings
@@ -224,52 +229,46 @@         (\o -> First <$> o ..:? "latest-snapshot" :: WarningParser (First Text))
         urls
 
-  configMonoidPackageIndex <-
+  packageIndex <-
     First <$> jsonSubWarningsT (obj ..:?  configMonoidPackageIndexName)
-  configMonoidPackageIndices <-
+  packageIndices <-
     First <$> jsonSubWarningsTT (obj ..:?  configMonoidPackageIndicesName)
-  configMonoidSystemGHC <- First <$> obj ..:? configMonoidSystemGHCName
-  configMonoidInstallGHC <- FirstTrue <$> obj ..:? configMonoidInstallGHCName
-  configMonoidSkipGHCCheck <-
-    FirstFalse <$> obj ..:? configMonoidSkipGHCCheckName
-  configMonoidSkipMsys <- FirstFalse <$> obj ..:? configMonoidSkipMsysName
-  configMonoidRequireStackVersion <-
-    IntersectingVersionRange . unVersionRangeJSON <$>
+  systemGHC <- First <$> obj ..:? configMonoidSystemGHCName
+  installGHC <- FirstTrue <$> obj ..:? configMonoidInstallGHCName
+  skipGHCCheck <- FirstFalse <$> obj ..:? configMonoidSkipGHCCheckName
+  skipMsys <- FirstFalse <$> obj ..:? configMonoidSkipMsysName
+  requireStackVersion <-
+    IntersectingVersionRange . (.versionRangeJSON) <$>
       ( obj ..:? configMonoidRequireStackVersionName
           ..!= VersionRangeJSON anyVersion
       )
-  configMonoidArch <- First <$> obj ..:? configMonoidArchName
-  configMonoidGHCVariant <- First <$> obj ..:? configMonoidGHCVariantName
-  configMonoidGHCBuild <- First <$> obj ..:? configMonoidGHCBuildName
-  configMonoidJobs <- First <$> obj ..:? configMonoidJobsName
-  configMonoidExtraIncludeDirs <- map (toFilePath rootDir FilePath.</>) <$>
+  arch <- First <$> obj ..:? configMonoidArchName
+  ghcVariant <- First <$> obj ..:? configMonoidGHCVariantName
+  ghcBuild <- First <$> obj ..:? configMonoidGHCBuildName
+  jobs <- First <$> obj ..:? configMonoidJobsName
+  extraIncludeDirs <- map (toFilePath rootDir FilePath.</>) <$>
     obj ..:?  configMonoidExtraIncludeDirsName ..!= []
-  configMonoidExtraLibDirs <- map (toFilePath rootDir FilePath.</>) <$>
+  extraLibDirs <- map (toFilePath rootDir FilePath.</>) <$>
     obj ..:?  configMonoidExtraLibDirsName ..!= []
-  configMonoidCustomPreprocessorExts <-
+  customPreprocessorExts <-
     obj ..:?  configMonoidCustomPreprocessorExtsName ..!= []
-  configMonoidOverrideGccPath <-
-    First <$> obj ..:? configMonoidOverrideGccPathName
-  configMonoidOverrideHpack <-
-    First <$> obj ..:? configMonoidOverrideHpackName
-  configMonoidConcurrentTests <-
-    First <$> obj ..:? configMonoidConcurrentTestsName
-  configMonoidLocalBinPath <- First <$> obj ..:? configMonoidLocalBinPathName
+  overrideGccPath <- First <$> obj ..:? configMonoidOverrideGccPathName
+  overrideHpack <- First <$> obj ..:? configMonoidOverrideHpackName
+  concurrentTests <- First <$> obj ..:? configMonoidConcurrentTestsName
+  localBinPath <- First <$> obj ..:? configMonoidLocalBinPathName
   templates <- obj ..:? "templates"
-  (configMonoidScmInit,configMonoidTemplateParameters) <-
+  (scmInit, templateParameters) <-
     case templates of
       Nothing -> pure (First Nothing,M.empty)
       Just tobj -> do
         scmInit <- tobj ..:? configMonoidScmInitName
         params <- tobj ..:? configMonoidTemplateParametersName
         pure (First scmInit,fromMaybe M.empty params)
-  configMonoidCompilerCheck <-
-    First <$> obj ..:? configMonoidCompilerCheckName
-  configMonoidCompilerRepository <-
-    First <$> (obj ..:? configMonoidCompilerRepositoryName)
+  compilerCheck <- First <$> obj ..:? configMonoidCompilerCheckName
+  compilerRepository <- First <$> (obj ..:? configMonoidCompilerRepositoryName)
 
-  options <-
-    Map.map unGhcOptions <$> obj ..:? configMonoidGhcOptionsName ..!= mempty
+  options <- Map.map (.ghcOptions) <$>
+    obj ..:? configMonoidGhcOptionsName ..!= (mempty :: Map GhcOptionKey GhcOptions)
 
   optionsEverything <-
     case (Map.lookup GOKOldEverything options, Map.lookup GOKEverything options) of
@@ -282,75 +281,119 @@         pure x
       (Nothing, Nothing) -> pure []
 
-  let configMonoidGhcOptionsByCat = coerce $ Map.fromList
+  let ghcOptionsByCat = coerce $ Map.fromList
         [ (AGOEverything, optionsEverything)
         , (AGOLocals, Map.findWithDefault [] GOKLocals options)
         , (AGOTargets, Map.findWithDefault [] GOKTargets options)
         ]
 
-      configMonoidGhcOptionsByName = coerce $ Map.fromList
+      ghcOptionsByName = coerce $ Map.fromList
           [(name, opts) | (GOKPackage name, opts) <- Map.toList options]
 
-  configMonoidCabalConfigOpts' <-
-    obj ..:? configMonoidConfigureOptionsName ..!= mempty
-  let configMonoidCabalConfigOpts =
-        coerce (configMonoidCabalConfigOpts' :: Map CabalConfigKey [Text])
-
-  configMonoidExtraPath <- obj ..:? configMonoidExtraPathName ..!= []
-  configMonoidSetupInfoLocations <-
-    obj ..:? configMonoidSetupInfoLocationsName ..!= []
-  configMonoidSetupInfoInline <-
+  cabalConfigOpts' <- obj ..:? configMonoidConfigureOptionsName ..!= mempty
+  let cabalConfigOpts = coerce (cabalConfigOpts' :: Map CabalConfigKey [Text])
+  extraPath <- obj ..:? configMonoidExtraPathName ..!= []
+  setupInfoLocations <- obj ..:? configMonoidSetupInfoLocationsName ..!= []
+  setupInfoInline <-
     jsonSubWarningsT (obj ..:? configMonoidSetupInfoInlineName) ..!= mempty
-  configMonoidLocalProgramsBase <-
-    First <$> obj ..:? configMonoidLocalProgramsBaseName
-  configMonoidPvpBounds <- First <$> obj ..:? configMonoidPvpBoundsName
-  configMonoidModifyCodePage <-
-    FirstTrue <$> obj ..:? configMonoidModifyCodePageName
-  configMonoidRebuildGhcOptions <-
-    FirstFalse <$> obj ..:? configMonoidRebuildGhcOptionsName
-  configMonoidApplyGhcOptions <-
-    First <$> obj ..:? configMonoidApplyGhcOptionsName
-  configMonoidApplyProgOptions <-
-    First <$> obj ..:? configMonoidApplyProgOptionsName
-  configMonoidAllowNewer <- First <$> obj ..:? configMonoidAllowNewerName
-  configMonoidAllowNewerDeps <- obj ..:? configMonoidAllowNewerDepsName
-  configMonoidDefaultTemplate <-
-    First <$> obj ..:? configMonoidDefaultTemplateName
-  configMonoidAllowDifferentUser <-
-    First <$> obj ..:? configMonoidAllowDifferentUserName
-  configMonoidDumpLogs <- First <$> obj ..:? configMonoidDumpLogsName
-  configMonoidSaveHackageCreds <-
-    First <$> obj ..:? configMonoidSaveHackageCredsName
-  configMonoidHackageBaseUrl <-
-    First <$> obj ..:? configMonoidHackageBaseUrlName
-
+  localProgramsBase <- First <$> obj ..:? configMonoidLocalProgramsBaseName
+  pvpBounds <- First <$> obj ..:? configMonoidPvpBoundsName
+  modifyCodePage <- FirstTrue <$> obj ..:? configMonoidModifyCodePageName
+  rebuildGhcOptions <- FirstFalse <$> obj ..:? configMonoidRebuildGhcOptionsName
+  applyGhcOptions <- First <$> obj ..:? configMonoidApplyGhcOptionsName
+  applyProgOptions <- First <$> obj ..:? configMonoidApplyProgOptionsName
+  allowNewer <- First <$> obj ..:? configMonoidAllowNewerName
+  allowNewerDeps <- obj ..:? configMonoidAllowNewerDepsName
+  defaultTemplate <- First <$> obj ..:? configMonoidDefaultTemplateName
+  allowDifferentUser <- First <$> obj ..:? configMonoidAllowDifferentUserName
+  dumpLogs <- First <$> obj ..:? configMonoidDumpLogsName
+  saveHackageCreds <- First <$> obj ..:? configMonoidSaveHackageCredsName
+  hackageBaseUrl <- First <$> obj ..:? configMonoidHackageBaseUrlName
   configMonoidColorWhenUS <- obj ..:? configMonoidColorWhenUSName
   configMonoidColorWhenGB <- obj ..:? configMonoidColorWhenGBName
-  let configMonoidColorWhen =  First $   configMonoidColorWhenUS
-                                     <|> configMonoidColorWhenGB
-
+  let colorWhen = First $ configMonoidColorWhenUS <|> configMonoidColorWhenGB
   configMonoidStylesUS <- obj ..:? configMonoidStylesUSName
   configMonoidStylesGB <- obj ..:? configMonoidStylesGBName
-  let configMonoidStyles = fromMaybe mempty $   configMonoidStylesUS
-                                            <|> configMonoidStylesGB
-
-  configMonoidHideSourcePaths <-
-    FirstTrue <$> obj ..:? configMonoidHideSourcePathsName
-  configMonoidRecommendUpgrade <-
-    FirstTrue <$> obj ..:? configMonoidRecommendUpgradeName
-  configMonoidCasaOpts <-
-    jsonSubWarnings (obj ..:? configMonoidCasaOptsName ..!= mempty)
-  configMonoidCasaRepoPrefix <-
-    First <$> obj ..:? configMonoidCasaRepoPrefixName
-  configMonoidSnapshotLocation <-
-    First <$> obj ..:? configMonoidSnapshotLocationName
-  configMonoidNoRunCompile <-
-    FirstFalse <$> obj ..:? configMonoidNoRunCompileName
-
-  configMonoidStackDeveloperMode <-
-    First <$> obj ..:? configMonoidStackDeveloperModeName
-
-  pure ConfigMonoid {..}
+  let styles = fromMaybe mempty $ configMonoidStylesUS <|> configMonoidStylesGB
+  hideSourcePaths <- FirstTrue <$> obj ..:? configMonoidHideSourcePathsName
+  recommendUpgrade <- FirstTrue <$> obj ..:? configMonoidRecommendUpgradeName
+  notifyIfNixOnPath <- FirstTrue <$> obj ..:? configMonoidNotifyIfNixOnPathName
+  notifyIfGhcUntested <-
+    FirstTrue <$> obj ..:? configMonoidNotifyIfGhcUntestedName
+  notifyIfCabalUntested <-
+    FirstTrue <$> obj ..:? configMonoidNotifyIfCabalUntestedName
+  notifyIfArchUnknown <-
+    FirstTrue <$> obj ..:? configMonoidNotifyIfArchUnknownName
+  casaOpts <- jsonSubWarnings (obj ..:? configMonoidCasaOptsName ..!= mempty)
+  casaRepoPrefix <- First <$> obj ..:? configMonoidCasaRepoPrefixName
+  snapshotLocation <- First <$> obj ..:? configMonoidSnapshotLocationName
+  noRunCompile <- FirstFalse <$> obj ..:? configMonoidNoRunCompileName
+  stackDeveloperMode <- First <$> obj ..:? configMonoidStackDeveloperModeName
+  pure ConfigMonoid
+    { stackRoot
+    , workDir
+    , buildOpts
+    , dockerOpts
+    , nixOpts
+    , connectionCount
+    , hideTHLoading
+    , prefixTimestamps
+    , latestSnapshot
+    , packageIndex
+    , packageIndices
+    , systemGHC
+    , installGHC
+    , skipGHCCheck
+    , skipMsys
+    , compilerCheck
+    , compilerRepository
+    , requireStackVersion
+    , arch
+    , ghcVariant
+    , ghcBuild
+    , jobs
+    , extraIncludeDirs
+    , extraLibDirs
+    , customPreprocessorExts
+    , overrideGccPath
+    , overrideHpack
+    , concurrentTests
+    , localBinPath
+    , templateParameters
+    , scmInit
+    , ghcOptionsByName
+    , ghcOptionsByCat
+    , cabalConfigOpts
+    , extraPath
+    , setupInfoLocations
+    , setupInfoInline
+    , localProgramsBase
+    , pvpBounds
+    , modifyCodePage
+    , rebuildGhcOptions
+    , applyGhcOptions
+    , applyProgOptions
+    , allowNewer
+    , allowNewerDeps
+    , defaultTemplate
+    , allowDifferentUser
+    , dumpLogs
+    , saveHackageCreds
+    , hackageBaseUrl
+    , colorWhen
+    , styles
+    , hideSourcePaths
+    , recommendUpgrade
+    , notifyIfNixOnPath
+    , notifyIfGhcUntested
+    , notifyIfCabalUntested
+    , notifyIfArchUnknown
+    , casaOpts
+    , casaRepoPrefix
+    , snapshotLocation
+    , noRunCompile
+    , stackDeveloperMode
+    }
 
 configMonoidWorkDirName :: Text
 configMonoidWorkDirName = "work-dir"
@@ -514,6 +557,18 @@ 
 configMonoidRecommendUpgradeName :: Text
 configMonoidRecommendUpgradeName = "recommend-stack-upgrade"
+
+configMonoidNotifyIfNixOnPathName :: Text
+configMonoidNotifyIfNixOnPathName = "notify-if-nix-on-path"
+
+configMonoidNotifyIfGhcUntestedName :: Text
+configMonoidNotifyIfGhcUntestedName = "notify-if-ghc-untested"
+
+configMonoidNotifyIfCabalUntestedName :: Text
+configMonoidNotifyIfCabalUntestedName = "notify-if-cabal-untested"
+
+configMonoidNotifyIfArchUnknownName :: Text
+configMonoidNotifyIfArchUnknownName = "notify-if-arch-unknown"
 
 configMonoidCasaOptsName :: Text
 configMonoidCasaOptsName = "casa"
src/Stack/Types/ConfigureOpts.hs view
@@ -1,12 +1,14 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 module Stack.Types.ConfigureOpts
   ( ConfigureOpts (..)
   , BaseConfigOpts (..)
   , configureOpts
-  , configureOptsDirs
-  , configureOptsNoDir
+  , configureOptsPathRelated
+  , configureOptsNonPathRelated
   ) where
 
 import qualified Data.Map as Map
@@ -24,7 +26,8 @@                    , relDirEtc, relDirLib, relDirLibexec, relDirShare
                    )
 import           Stack.Prelude
-import           Stack.Types.BuildOpts ( BuildOpts (..), BuildOptsCLI )
+import           Stack.Types.BuildOpts ( BuildOpts (..) )
+import           Stack.Types.BuildOptsCLI ( BuildOptsCLI )
 import           Stack.Types.Compiler ( getGhcVersion, whichCompiler )
 import           Stack.Types.Config
                    ( Config (..), HasConfig (..) )
@@ -36,39 +39,41 @@ 
 -- | Basic information used to calculate what the configure options are
 data BaseConfigOpts = BaseConfigOpts
-  { bcoSnapDB :: !(Path Abs Dir)
-  , bcoLocalDB :: !(Path Abs Dir)
-  , bcoSnapInstallRoot :: !(Path Abs Dir)
-  , bcoLocalInstallRoot :: !(Path Abs Dir)
-  , bcoBuildOpts :: !BuildOpts
-  , bcoBuildOptsCLI :: !BuildOptsCLI
-  , bcoExtraDBs :: ![Path Abs Dir]
+  { snapDB :: !(Path Abs Dir)
+  , localDB :: !(Path Abs Dir)
+  , snapInstallRoot :: !(Path Abs Dir)
+  , localInstallRoot :: !(Path Abs Dir)
+  , buildOpts :: !BuildOpts
+  , buildOptsCLI :: !BuildOptsCLI
+  , extraDBs :: ![Path Abs Dir]
   }
   deriving Show
 
 -- | Render a @BaseConfigOpts@ to an actual list of options
-configureOpts :: EnvConfig
-              -> BaseConfigOpts
-              -> Map PackageIdentifier GhcPkgId -- ^ dependencies
-              -> Bool -- ^ local non-extra-dep?
-              -> IsMutable
-              -> Package
-              -> ConfigureOpts
+configureOpts ::
+     EnvConfig
+  -> BaseConfigOpts
+  -> Map PackageIdentifier GhcPkgId -- ^ dependencies
+  -> Bool -- ^ local non-extra-dep?
+  -> IsMutable
+  -> Package
+  -> ConfigureOpts
 configureOpts econfig bco deps isLocal isMutable package = ConfigureOpts
-  { coDirs = configureOptsDirs bco isMutable package
-  , coNoDirs = configureOptsNoDir econfig bco deps isLocal package
+  { pathRelated = configureOptsPathRelated bco isMutable package
+  , nonPathRelated =
+      configureOptsNonPathRelated econfig bco deps isLocal package
   }
 
-
-configureOptsDirs :: BaseConfigOpts
-                  -> IsMutable
-                  -> Package
-                  -> [String]
-configureOptsDirs bco isMutable package = concat
+configureOptsPathRelated ::
+     BaseConfigOpts
+  -> IsMutable
+  -> Package
+  -> [String]
+configureOptsPathRelated bco isMutable package = concat
   [ ["--user", "--package-db=clear", "--package-db=global"]
   , map (("--package-db=" ++) . toFilePathNoTrailingSep) $ case isMutable of
-      Immutable -> bcoExtraDBs bco ++ [bcoSnapDB bco]
-      Mutable -> bcoExtraDBs bco ++ [bcoSnapDB bco] ++ [bcoLocalDB bco]
+      Immutable -> bco.extraDBs ++ [bco.snapDB]
+      Mutable -> bco.extraDBs ++ [bco.snapDB] ++ [bco.localDB]
   , [ "--libdir=" ++ toFilePathNoTrailingSep (installRoot </> relDirLib)
     , "--bindir=" ++ toFilePathNoTrailingSep (installRoot </> bindirSuffix)
     , "--datadir=" ++ toFilePathNoTrailingSep (installRoot </> relDirShare)
@@ -81,37 +86,37 @@  where
   installRoot =
     case isMutable of
-      Immutable -> bcoSnapInstallRoot bco
-      Mutable -> bcoLocalInstallRoot bco
+      Immutable -> bco.snapInstallRoot
+      Mutable -> bco.localInstallRoot
   docDir =
     case pkgVerDir of
       Nothing -> installRoot </> docDirSuffix
       Just dir -> installRoot </> docDirSuffix </> dir
   pkgVerDir = parseRelDir
     (  packageIdentifierString
-        (PackageIdentifier (packageName package) (packageVersion package))
+        (PackageIdentifier package.name package.version)
     ++ [pathSeparator]
     )
 
 -- | Same as 'configureOpts', but does not include directory path options
-configureOptsNoDir ::
+configureOptsNonPathRelated ::
      EnvConfig
   -> BaseConfigOpts
   -> Map PackageIdentifier GhcPkgId -- ^ Dependencies.
   -> Bool -- ^ Is this a local, non-extra-dep?
   -> Package
   -> [String]
-configureOptsNoDir econfig bco deps isLocal package = concat
+configureOptsNonPathRelated econfig bco deps isLocal package = concat
   [ depOptions
   , [ "--enable-library-profiling"
-    | boptsLibProfile bopts || boptsExeProfile bopts
+    | bopts.libProfile || bopts.exeProfile
     ]
-  , ["--enable-profiling" | boptsExeProfile bopts && isLocal]
-  , ["--enable-split-objs" | boptsSplitObjs bopts]
+  , ["--enable-profiling" | bopts.exeProfile && isLocal]
+  , ["--enable-split-objs" | bopts.splitObjs]
   , [ "--disable-library-stripping"
-    | not $ boptsLibStrip bopts || boptsExeStrip bopts
+    | not $ bopts.libStrip || bopts.exeStrip
     ]
-  , ["--disable-executable-stripping" | not (boptsExeStrip bopts) && isLocal]
+  , ["--disable-executable-stripping" | not bopts.exeStrip && isLocal]
   , map (\(name,enabled) ->
                      "-f" <>
                      (if enabled
@@ -119,14 +124,14 @@                         else "-") <>
                      flagNameString name)
                   (Map.toList flags)
-  , map T.unpack $ packageCabalConfigOpts package
-  , processGhcOptions (packageGhcOptions package)
-  , map ("--extra-include-dirs=" ++) (configExtraIncludeDirs config)
-  , map ("--extra-lib-dirs=" ++) (configExtraLibDirs config)
+  , map T.unpack package.cabalConfigOpts
+  , processGhcOptions package.ghcOptions
+  , map ("--extra-include-dirs=" ++) config.extraIncludeDirs
+  , map ("--extra-lib-dirs=" ++) config.extraLibDirs
   , maybe
       []
       (\customGcc -> ["--with-gcc=" ++ toFilePath customGcc])
-      (configOverrideGccPath config)
+      config.overrideGccPath
   , ["--exact-configuration"]
   , ["--ghc-option=-fhide-source-paths" | hideSourcePaths cv]
   ]
@@ -161,18 +166,18 @@         newArgs = concat [preRtsArgs, fullRtsArgs, postRtsArgs]
     in  concatMap (\x -> [compilerOptionsCabalFlag wc, T.unpack x]) newArgs
 
-  wc = view (actualCompilerVersionL.to whichCompiler) econfig
-  cv = view (actualCompilerVersionL.to getGhcVersion) econfig
+  wc = view (actualCompilerVersionL . to whichCompiler) econfig
+  cv = view (actualCompilerVersionL . to getGhcVersion) econfig
 
   hideSourcePaths ghcVersion =
-    ghcVersion >= C.mkVersion [8, 2] && configHideSourcePaths config
+    ghcVersion >= C.mkVersion [8, 2] && config.hideSourcePaths
 
   config = view configL econfig
-  bopts = bcoBuildOpts bco
+  bopts = bco.buildOpts
 
   -- Unioning atop defaults is needed so that all flags are specified with
   -- --exact-configuration.
-  flags = packageFlags package `Map.union` packageDefaultFlags package
+  flags = package.flags `Map.union` package.defaultFlags
 
   depOptions = map toDepOption $ Map.toList deps
 
@@ -189,13 +194,14 @@       LSubLibName cn ->
         unPackageName subPkgName <> ":" <> unUnqualComponentName cn
 
--- | Configure options to be sent to Setup.hs configure
+-- | Configure options to be sent to Setup.hs configure.
 data ConfigureOpts = ConfigureOpts
-  { coDirs :: ![String]
+  { pathRelated :: ![String]
     -- ^ Options related to various paths. We separate these out since they do
-    -- not have an impact on the contents of the compiled binary for checking
+    -- not have an effect on the contents of the compiled binary for checking
     -- if we can use an existing precompiled cache.
-  , coNoDirs :: ![String]
+  , nonPathRelated :: ![String]
+    -- ^ Options other than path-related options.
   }
   deriving (Data, Eq, Generic, Show, Typeable)
 
src/Stack/Types/Curator.hs view
@@ -1,6 +1,11 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
+-- | Module exporting the 'Curator' type, used to represent Stack's
+-- project-specific @curator@ option, which supports the needs of the
+-- [@curator@ tool](https://github.com/commercialhaskell/curator).
 module Stack.Types.Curator
   ( Curator (..)
   ) where
@@ -11,29 +16,35 @@ import qualified Data.Set as Set
 import           Stack.Prelude
 
--- | Extra configuration intended exclusively for usage by the curator tool. In
--- other words, this is /not/ part of the documented and exposed Stack API.
--- SUBJECT TO CHANGE.
+-- | Type representing configuration options which support the needs of the
+-- [@curator@ tool](https://github.com/commercialhaskell/curator).
 data Curator = Curator
-  { curatorSkipTest :: !(Set PackageName)
-  , curatorExpectTestFailure :: !(Set PackageName)
-  , curatorSkipBenchmark :: !(Set PackageName)
-  , curatorExpectBenchmarkFailure :: !(Set PackageName)
-  , curatorSkipHaddock :: !(Set PackageName)
-  , curatorExpectHaddockFailure :: !(Set PackageName)
+  { skipTest :: !(Set PackageName)
+    -- ^ Packages for which Stack should ignore test suites.
+  , expectTestFailure :: !(Set PackageName)
+    -- ^ Packages for which Stack should expect building test suites to fail.
+  , skipBenchmark :: !(Set PackageName)
+    -- ^ Packages for which Stack should ignore benchmarks.
+  , expectBenchmarkFailure :: !(Set PackageName)
+    -- ^ Packages for which Stack should expect building benchmarks to fail.
+  , skipHaddock :: !(Set PackageName)
+    -- ^ Packages for which Stack should ignore creating Haddock documentation.
+  , expectHaddockFailure :: !(Set PackageName)
+    -- ^ Packages for which Stack should expect creating Haddock documentation
+    -- to fail.
   }
   deriving Show
 
 instance ToJSON Curator where
-  toJSON c = object
-    [ "skip-test" .= Set.map CabalString (curatorSkipTest c)
-    , "expect-test-failure" .= Set.map CabalString (curatorExpectTestFailure c)
-    , "skip-bench" .= Set.map CabalString (curatorSkipBenchmark c)
+  toJSON curator = object
+    [ "skip-test" .= Set.map CabalString curator.skipTest
+    , "expect-test-failure" .= Set.map CabalString curator.expectTestFailure
+    , "skip-bench" .= Set.map CabalString curator.skipBenchmark
     , "expect-benchmark-failure" .=
-        Set.map CabalString (curatorExpectTestFailure c)
-    , "skip-haddock" .= Set.map CabalString (curatorSkipHaddock c)
-    , "expect-test-failure" .=
-        Set.map CabalString (curatorExpectHaddockFailure c)
+        Set.map CabalString curator.expectTestFailure
+    , "skip-haddock" .= Set.map CabalString curator.skipHaddock
+    , "expect-haddock-failure" .=
+        Set.map CabalString curator.expectHaddockFailure
     ]
 
 instance FromJSON (WithJSONWarnings Curator) where
@@ -41,6 +52,10 @@     <$> fmap (Set.map unCabalString) (o ..:? "skip-test" ..!= mempty)
     <*> fmap (Set.map unCabalString) (o ..:? "expect-test-failure" ..!= mempty)
     <*> fmap (Set.map unCabalString) (o ..:? "skip-bench" ..!= mempty)
-    <*> fmap (Set.map unCabalString) (o ..:? "expect-benchmark-failure" ..!= mempty)
+    <*> fmap
+          (Set.map unCabalString)
+          (o ..:? "expect-benchmark-failure" ..!= mempty)
     <*> fmap (Set.map unCabalString) (o ..:? "skip-haddock" ..!= mempty)
-    <*> fmap (Set.map unCabalString) (o ..:? "expect-haddock-failure" ..!= mempty)
+    <*> fmap
+          (Set.map unCabalString)
+          (o ..:? "expect-haddock-failure" ..!= mempty)
src/Stack/Types/Dependency.hs view
@@ -1,34 +1,84 @@-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 
 module Stack.Types.Dependency
   ( DepValue (..)
   , DepType (..)
+  , DepLibrary (..)
+  , cabalToStackDep
+  , cabalExeToStackDep
+  , cabalSetupDepsToStackDep
+  , libraryDepFromVersionRange
+  , isDepTypeLibrary
+  , getDepSublib
   ) where
 
+import qualified Data.Map as Map
+import qualified Data.Set as Set
+import qualified Distribution.PackageDescription as Cabal
 import           Distribution.Types.VersionRange ( VersionRange )
 import           Stack.Prelude
-import           Stack.Types.Version ( intersectVersionRanges )
-
+import           Stack.Types.ComponentUtils
+                   ( StackUnqualCompName (..), fromCabalName )
 
 -- | The value for a map from dependency name. This contains both the version
--- range and the type of dependency, and provides a semigroup instance.
+-- range and the type of dependency.
 data DepValue = DepValue
-  { dvVersionRange :: !VersionRange
-  , dvType :: !DepType
+  { versionRange :: !VersionRange
+  , depType :: !DepType
   }
   deriving (Show, Typeable)
 
-instance Semigroup DepValue where
-  DepValue a x <> DepValue b y = DepValue (intersectVersionRanges a b) (x <> y)
-
 -- | Is this package being used as a library, or just as a build tool? If the
 -- former, we need to ensure that a library actually exists. See
 -- <https://github.com/commercialhaskell/stack/issues/2195>
 data DepType
-  = AsLibrary
+  = AsLibrary !DepLibrary
   | AsBuildTool
   deriving (Eq, Show)
 
-instance Semigroup DepType where
-  AsLibrary <> _ = AsLibrary
-  AsBuildTool <> x = x
+data DepLibrary = DepLibrary
+  { main :: !Bool
+  , subLib :: Set StackUnqualCompName
+  }
+  deriving (Eq, Show)
+
+getDepSublib :: DepValue -> Maybe (Set StackUnqualCompName)
+getDepSublib val = case val.depType of
+  AsLibrary libVal -> Just libVal.subLib
+  _ -> Nothing
+
+defaultDepLibrary :: DepLibrary
+defaultDepLibrary = DepLibrary True mempty
+
+isDepTypeLibrary :: DepType -> Bool
+isDepTypeLibrary AsLibrary{} = True
+isDepTypeLibrary AsBuildTool = False
+
+cabalToStackDep :: Cabal.Dependency -> DepValue
+cabalToStackDep (Cabal.Dependency _ verRange libNameSet) =
+  DepValue { versionRange = verRange, depType = AsLibrary depLibrary }
+ where
+  depLibrary = DepLibrary finalHasMain filteredItems
+  (finalHasMain, filteredItems) = foldr' iterator (False, mempty) libNameSet
+  iterator LMainLibName (_, newLibNameSet) = (True, newLibNameSet)
+  iterator (LSubLibName libName) (hasMain, newLibNameSet) =
+    (hasMain, Set.insert (fromCabalName libName) newLibNameSet)
+
+cabalExeToStackDep :: Cabal.ExeDependency -> DepValue
+cabalExeToStackDep (Cabal.ExeDependency _ _name verRange) =
+  DepValue { versionRange = verRange, depType = AsBuildTool }
+
+cabalSetupDepsToStackDep :: Cabal.SetupBuildInfo -> Map PackageName DepValue
+cabalSetupDepsToStackDep setupInfo =
+  foldr' inserter mempty (Cabal.setupDepends setupInfo)
+ where
+  inserter d@(Cabal.Dependency packageName _ _) =
+    Map.insert packageName (cabalToStackDep d)
+
+libraryDepFromVersionRange :: VersionRange -> DepValue
+libraryDepFromVersionRange range = DepValue
+  { versionRange = range
+  , depType = AsLibrary defaultDepLibrary
+  }
+ src/Stack/Types/DependencyTree.hs view
@@ -0,0 +1,91 @@+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
+
+module Stack.Types.DependencyTree
+  ( DependencyTree (..)
+  , DotPayload (..)
+  , licenseText
+  , versionText
+  ) where
+
+import           Data.Aeson ( ToJSON (..), Value, (.=), object )
+import qualified Data.Set as Set
+import qualified Data.Map as Map
+import qualified Data.Text as Text
+import           Distribution.License ( License (..), licenseFromSPDX )
+import qualified Distribution.SPDX.License as SPDX
+import           Distribution.Text ( display )
+import           Stack.Prelude hiding ( Display (..), pkgName, loadPackage )
+
+-- | Information about a package in the dependency graph, when available.
+data DotPayload = DotPayload
+  { version :: Maybe Version
+    -- ^ The package version.
+  , license :: Maybe (Either SPDX.License License)
+    -- ^ The license the package was released under.
+  , location :: Maybe PackageLocation
+    -- ^ The location of the package.
+  }
+  deriving (Eq, Show)
+
+data DependencyTree =
+  DependencyTree (Set PackageName)
+                 (Map PackageName (Set PackageName, DotPayload))
+
+instance ToJSON DependencyTree where
+  toJSON (DependencyTree _ dependencyMap) =
+    toJSON $ foldToList dependencyToJSON dependencyMap
+
+foldToList :: (k -> a -> b) -> Map k a -> [b]
+foldToList f = Map.foldrWithKey (\k a bs -> bs ++ [f k a]) []
+
+dependencyToJSON :: PackageName -> (Set PackageName, DotPayload) -> Value
+dependencyToJSON pkg (deps, payload) =
+  let fieldsAlwaysPresent = [ "name" .= packageNameString pkg
+                            , "license" .= licenseText payload
+                            , "version" .= versionText payload
+                            , "dependencies" .= Set.map packageNameString deps
+                            ]
+      loc = catMaybes
+              [("location" .=) . pkgLocToJSON <$> payload.location]
+  in  object $ fieldsAlwaysPresent ++ loc
+
+pkgLocToJSON :: PackageLocation -> Value
+pkgLocToJSON (PLMutable (ResolvedPath _ dir)) = object
+  [ "type" .= ("project package" :: Text)
+  , "url" .= ("file://" ++ toFilePath dir)
+  ]
+pkgLocToJSON (PLImmutable (PLIHackage pkgid _ _)) = object
+  [ "type" .= ("hackage" :: Text)
+  , "url" .= ("https://hackage.haskell.org/package/" ++ display pkgid)
+  ]
+pkgLocToJSON (PLImmutable (PLIArchive archive _)) =
+  let url = case archiveLocation archive of
+              ALUrl u -> u
+              ALFilePath (ResolvedPath _ path) ->
+                Text.pack $ "file://" ++ toFilePath path
+  in  object
+        [ "type" .= ("archive" :: Text)
+        , "url" .= url
+        , "sha256" .= archiveHash archive
+        , "size" .= archiveSize archive
+        ]
+pkgLocToJSON (PLImmutable (PLIRepo repo _)) = object
+  [ "type" .= case repoType repo of
+                RepoGit -> "git" :: Text
+                RepoHg -> "hg" :: Text
+  , "url" .= repoUrl repo
+  , "commit" .= repoCommit repo
+  , "subdir" .= repoSubdir repo
+  ]
+
+licenseText :: DotPayload -> Text
+licenseText payload =
+  maybe "<unknown>" (Text.pack . display . either licenseFromSPDX id)
+                    payload.license
+
+versionText :: DotPayload -> Text
+versionText payload =
+  maybe "<unknown>" (Text.pack . display) payload.version
src/Stack/Types/Docker.hs view
@@ -1,6 +1,8 @@-{-# LANGUAGE NoImplicitPrelude  #-}
-{-# LANGUAGE OverloadedStrings  #-}
-{-# LANGUAGE RecordWildCards    #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- | Docker types.
 
@@ -240,41 +242,41 @@ 
 -- | Docker configuration.
 data DockerOpts = DockerOpts
-  { dockerEnable :: !Bool
+  { enable :: !Bool
      -- ^ Is using Docker enabled?
-  , dockerImage :: !(Either SomeException String)
+  , image :: !(Either SomeException String)
      -- ^ Exact Docker image tag or ID.  Overrides docker-repo-*/tag.
-  , dockerRegistryLogin :: !Bool
+  , registryLogin :: !Bool
      -- ^ Does registry require login for pulls?
-  , dockerRegistryUsername :: !(Maybe String)
+  , registryUsername :: !(Maybe String)
      -- ^ Optional username for Docker registry.
-  , dockerRegistryPassword :: !(Maybe String)
+  , registryPassword :: !(Maybe String)
      -- ^ Optional password for Docker registry.
-  , dockerAutoPull :: !Bool
+  , autoPull :: !Bool
      -- ^ Automatically pull new images.
-  , dockerDetach :: !Bool
+  , detach :: !Bool
      -- ^ Whether to run a detached container
-  , dockerPersist :: !Bool
+  , persist :: !Bool
      -- ^ Create a persistent container (don't remove it when finished). Implied
      -- by `dockerDetach`.
-  , dockerContainerName :: !(Maybe String)
+  , containerName :: !(Maybe String)
      -- ^ Container name to use, only makes sense from command-line with
      -- `dockerPersist` or `dockerDetach`.
-  , dockerNetwork :: !(Maybe String)
+  , network :: !(Maybe String)
     -- ^ The network docker uses.
-  , dockerRunArgs :: ![String]
+  , runArgs :: ![String]
      -- ^ Arguments to pass directly to @docker run@.
-  , dockerMount :: ![Mount]
+  , mount :: ![Mount]
      -- ^ Volumes to mount in the container.
-  , dockerMountMode :: !(Maybe String)
+  , mountMode :: !(Maybe String)
      -- ^ Volume mount mode
-  , dockerEnv :: ![String]
+  , env :: ![String]
      -- ^ Environment variables to set in the container.
-  , dockerStackExe :: !(Maybe DockerStackExe)
+  , stackExe :: !(Maybe DockerStackExe)
      -- ^ Location of container-compatible Stack executable
-  , dockerSetUser :: !(Maybe Bool)
+  , setUser :: !(Maybe Bool)
     -- ^ Set in-container user to match host's
-  , dockerRequireDockerVersion :: !VersionRange
+  , requireDockerVersion :: !VersionRange
     -- ^ Require a version of Docker within this range.
   }
   deriving Show
@@ -282,83 +284,100 @@ -- | An uninterpreted representation of docker options. Configurations may be
 -- "cascaded" using mappend (left-biased).
 data DockerOptsMonoid = DockerOptsMonoid
-  { dockerMonoidDefaultEnable :: !Any
+  { defaultEnable :: !Any
     -- ^ Should Docker be defaulted to enabled (does @docker:@ section exist in
     -- the config)?
-  , dockerMonoidEnable :: !(First Bool)
+  , enable :: !(First Bool)
     -- ^ Is using Docker enabled?
-  , dockerMonoidRepoOrImage :: !(First DockerMonoidRepoOrImage)
+  , repoOrImage :: !(First DockerMonoidRepoOrImage)
     -- ^ Docker repository name (e.g. @fpco/stack-build@ or
     -- @fpco/stack-full:lts-2.8@)
-  , dockerMonoidRegistryLogin :: !(First Bool)
+  , registryLogin :: !(First Bool)
     -- ^ Does registry require login for pulls?
-  , dockerMonoidRegistryUsername :: !(First String)
+  , registryUsername :: !(First String)
     -- ^ Optional username for Docker registry.
-  , dockerMonoidRegistryPassword :: !(First String)
+  , registryPassword :: !(First String)
     -- ^ Optional password for Docker registry.
-  , dockerMonoidAutoPull :: !FirstTrue
+  , autoPull :: !FirstTrue
     -- ^ Automatically pull new images.
-  , dockerMonoidDetach :: !FirstFalse
+  , detach :: !FirstFalse
     -- ^ Whether to run a detached container
-  , dockerMonoidPersist :: !FirstFalse
+  , persist :: !FirstFalse
     -- ^ Create a persistent container (don't remove it when finished). Implied
     -- by -- `dockerDetach`.
-  , dockerMonoidContainerName :: !(First String)
+  , containerName :: !(First String)
     -- ^ Container name to use, only makes sense from command-line with
     -- `dockerPersist` or `dockerDetach`.
-  , dockerMonoidNetwork :: !(First String)
+  , network :: !(First String)
     -- ^ See: 'dockerNetwork'
-  , dockerMonoidRunArgs :: ![String]
+  , runArgs :: ![String]
     -- ^ Arguments to pass directly to @docker run@
-  , dockerMonoidMount :: ![Mount]
+  , mount :: ![Mount]
     -- ^ Volumes to mount in the container
-  , dockerMonoidMountMode :: !(First String)
+  , mountMode :: !(First String)
     -- ^ Volume mount mode
-  , dockerMonoidEnv :: ![String]
+  , env :: ![String]
     -- ^ Environment variables to set in the container
-  , dockerMonoidStackExe :: !(First DockerStackExe)
+  , stackExe :: !(First DockerStackExe)
     -- ^ Location of container-compatible Stack executable
-  , dockerMonoidSetUser :: !(First Bool)
+  , setUser :: !(First Bool)
     -- ^ Set in-container user to match host's
-  , dockerMonoidRequireDockerVersion :: !IntersectingVersionRange
+  , requireDockerVersion :: !IntersectingVersionRange
     -- ^ See: 'dockerRequireDockerVersion'
   }
   deriving (Show, Generic)
 
 -- | Decode uninterpreted docker options from JSON/YAML.
 instance FromJSON (WithJSONWarnings DockerOptsMonoid) where
-  parseJSON = withObjectWarnings "DockerOptsMonoid"
-    ( \o -> do
-       let dockerMonoidDefaultEnable = Any True
-       dockerMonoidEnable <- First <$> o ..:? dockerEnableArgName
-       dockerMonoidRepoOrImage <- First <$>
-         (   (Just . DockerMonoidImage <$> o ..: dockerImageArgName)
-         <|> (Just . DockerMonoidRepo <$> o ..: dockerRepoArgName)
-         <|> pure Nothing
-         )
-       dockerMonoidRegistryLogin <- First <$> o ..:? dockerRegistryLoginArgName
-       dockerMonoidRegistryUsername <-
-         First <$> o ..:? dockerRegistryUsernameArgName
-       dockerMonoidRegistryPassword <-
-         First <$> o ..:? dockerRegistryPasswordArgName
-       dockerMonoidAutoPull <- FirstTrue <$> o ..:? dockerAutoPullArgName
-       dockerMonoidDetach <- FirstFalse <$> o ..:? dockerDetachArgName
-       dockerMonoidPersist <- FirstFalse <$> o ..:? dockerPersistArgName
-       dockerMonoidContainerName <- First <$> o ..:? dockerContainerNameArgName
-       dockerMonoidNetwork <- First <$> o ..:? dockerNetworkArgName
-       dockerMonoidRunArgs <- o ..:? dockerRunArgsArgName ..!= []
-       dockerMonoidMount <- o ..:? dockerMountArgName ..!= []
-       dockerMonoidMountMode <- First <$> o ..:? dockerMountModeArgName
-       dockerMonoidEnv <- o ..:? dockerEnvArgName ..!= []
-       dockerMonoidStackExe <- First <$> o ..:? dockerStackExeArgName
-       dockerMonoidSetUser <- First <$> o ..:? dockerSetUserArgName
-       dockerMonoidRequireDockerVersion <-
-         IntersectingVersionRange . unVersionRangeJSON <$>
-           ( o ..:? dockerRequireDockerVersionArgName
-             ..!= VersionRangeJSON anyVersion
-           )
-       pure DockerOptsMonoid {..}
-    )
+  parseJSON = withObjectWarnings "DockerOptsMonoid" $ \o -> do
+    let defaultEnable = Any True
+    enable <- First <$> o ..:? dockerEnableArgName
+    repoOrImage <- First <$>
+      (   (Just . DockerMonoidImage <$> o ..: dockerImageArgName)
+      <|> (Just . DockerMonoidRepo <$> o ..: dockerRepoArgName)
+      <|> pure Nothing
+      )
+    registryLogin <- First <$> o ..:? dockerRegistryLoginArgName
+    registryUsername <-
+      First <$> o ..:? dockerRegistryUsernameArgName
+    registryPassword <-
+      First <$> o ..:? dockerRegistryPasswordArgName
+    autoPull <- FirstTrue <$> o ..:? dockerAutoPullArgName
+    detach <- FirstFalse <$> o ..:? dockerDetachArgName
+    persist <- FirstFalse <$> o ..:? dockerPersistArgName
+    containerName <- First <$> o ..:? dockerContainerNameArgName
+    network <- First <$> o ..:? dockerNetworkArgName
+    runArgs <- o ..:? dockerRunArgsArgName ..!= []
+    mount <- o ..:? dockerMountArgName ..!= []
+    mountMode <- First <$> o ..:? dockerMountModeArgName
+    env <- o ..:? dockerEnvArgName ..!= []
+    stackExe <- First <$> o ..:? dockerStackExeArgName
+    setUser <- First <$> o ..:? dockerSetUserArgName
+    requireDockerVersion <-
+      IntersectingVersionRange . (.versionRangeJSON) <$>
+        ( o ..:? dockerRequireDockerVersionArgName
+          ..!= VersionRangeJSON anyVersion
+        )
+    pure DockerOptsMonoid
+      { defaultEnable
+      , enable
+      , repoOrImage
+      , registryLogin
+      , registryUsername
+      , registryPassword
+      , autoPull
+      , detach
+      , persist
+      , containerName
+      , network
+      , runArgs
+      , mount
+      , mountMode
+      , env
+      , stackExe
+      , setUser
+      , requireDockerVersion
+      }
 
 -- | Left-biased combine Docker options
 instance Semigroup DockerOptsMonoid where
@@ -425,7 +444,7 @@ 
 -- | Newtype for non-orphan FromJSON instance.
 newtype VersionRangeJSON =
-  VersionRangeJSON { unVersionRangeJSON :: VersionRange }
+  VersionRangeJSON { versionRangeJSON :: VersionRange }
 
 -- | Parse VersionRange.
 instance FromJSON VersionRangeJSON where
src/Stack/Types/DockerEntrypoint.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoFieldSelectors  #-}
 
 module Stack.Types.DockerEntrypoint
   ( DockerEntrypoint (..)
@@ -10,7 +11,7 @@ 
 -- | Data passed into Docker container for the Docker entrypoint's use
 newtype DockerEntrypoint = DockerEntrypoint
-  { deUser :: Maybe DockerUser
+  { user :: Maybe DockerUser
     -- ^ UID/GID/etc of host user, if we wish to perform UID/GID switch in
     -- container
   }
@@ -18,9 +19,9 @@ 
 -- | Docker host user info
 data DockerUser = DockerUser
-  { duUid :: UserID -- ^ uid
-  , duGid :: GroupID -- ^ gid
-  , duGroups :: [GroupID] -- ^ Supplemental groups
-  , duUmask :: FileMode -- ^ File creation mask }
+  { uid :: UserID -- ^ uid
+  , gid :: GroupID -- ^ gid
+  , groups :: [GroupID] -- ^ Supplemental groups
+  , umask :: FileMode -- ^ File creation mask }
   }
   deriving (Read, Show)
+ src/Stack/Types/DotConfig.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+
+module Stack.Types.DotConfig
+  ( DotConfig (..)
+  ) where
+
+import           RIO.Process ( HasProcessContext (..) )
+import           Stack.Prelude hiding ( Display (..), pkgName, loadPackage )
+import           Stack.Types.BuildConfig
+                   ( BuildConfig (..), HasBuildConfig (..) )
+import           Stack.Types.Config ( HasConfig (..) )
+import           Stack.Types.DumpPackage ( DumpPackage (..) )
+import           Stack.Types.EnvConfig ( HasSourceMap (..) )
+import           Stack.Types.GHCVariant ( HasGHCVariant (..) )
+import           Stack.Types.Platform ( HasPlatform (..) )
+import           Stack.Types.Runner ( HasRunner (..) )
+import           Stack.Types.SourceMap ( SourceMap (..) )
+
+data DotConfig = DotConfig
+  { buildConfig :: !BuildConfig
+  , sourceMap :: !SourceMap
+  , globalDump :: ![DumpPackage]
+  }
+
+instance HasLogFunc DotConfig where
+  logFuncL = runnerL . logFuncL
+
+instance HasPantryConfig DotConfig where
+  pantryConfigL = configL . pantryConfigL
+
+instance HasTerm DotConfig where
+  useColorL = runnerL . useColorL
+  termWidthL = runnerL . termWidthL
+
+instance HasStylesUpdate DotConfig where
+  stylesUpdateL = runnerL . stylesUpdateL
+
+instance HasGHCVariant DotConfig where
+  ghcVariantL = configL . ghcVariantL
+  {-# INLINE ghcVariantL #-}
+
+instance HasPlatform DotConfig where
+  platformL = configL . platformL
+  {-# INLINE platformL #-}
+  platformVariantL = configL . platformVariantL
+  {-# INLINE platformVariantL #-}
+
+instance HasRunner DotConfig where
+  runnerL = configL . runnerL
+
+instance HasProcessContext DotConfig where
+  processContextL = runnerL . processContextL
+
+instance HasConfig DotConfig where
+  configL = buildConfigL . lens (.config) (\x y -> x { config = y })
+  {-# INLINE configL #-}
+
+instance HasBuildConfig DotConfig where
+  buildConfigL = lens (.buildConfig) (\x y -> x { buildConfig = y })
+
+instance HasSourceMap DotConfig where
+  sourceMapL = lens (.sourceMap) (\x y -> x { sourceMap = y })
+ src/Stack/Types/DotOpts.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoFieldSelectors  #-}
+
+-- | Module exporting the `DotOpts` type used by Stack's @dot@ and
+-- @ls dependencies@ commands.
+module Stack.Types.DotOpts
+  ( DotOpts (..)
+  ) where
+
+import           Stack.Prelude
+import           Stack.Types.BuildOptsCLI ( ApplyCLIFlag )
+
+-- | Options record for @stack dot@ and @stack ls dependencies@
+data DotOpts = DotOpts
+  { includeExternal :: !Bool
+    -- ^ Include external dependencies
+  , includeBase :: !Bool
+    -- ^ Include dependencies on base
+  , dependencyDepth :: !(Maybe Int)
+    -- ^ Limit the depth of dependency resolution to (Just n) or continue until
+    -- fixpoint
+  , prune :: !(Set PackageName)
+    -- ^ Package names to prune from the graph
+  , dotTargets :: [Text]
+    -- ^ Stack TARGETs to trace dependencies for
+  , flags :: !(Map ApplyCLIFlag (Map FlagName Bool))
+    -- ^ Flags to apply when calculating dependencies
+  , testTargets :: Bool
+    -- ^ Like the "--test" flag for build, affects the meaning of 'dotTargets'.
+  , benchTargets :: Bool
+    -- ^ Like the "--bench" flag for build, affects the meaning of 'dotTargets'.
+  , globalHints :: Bool
+    -- ^ Use global hints instead of relying on an actual GHC installation.
+  }
src/Stack/Types/DownloadInfo.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoFieldSelectors  #-}
 {-# LANGUAGE OverloadedStrings #-}
 
 module Stack.Types.DownloadInfo
@@ -16,11 +17,11 @@ -- | Build of the compiler distribution (e.g. standard, gmp4, tinfo6)
 -- | Information for a file to download.
 data DownloadInfo = DownloadInfo
-  { downloadInfoUrl :: Text
+  { url :: Text
     -- ^ URL or absolute file path
-  , downloadInfoContentLength :: Maybe Int
-  , downloadInfoSha1 :: Maybe ByteString
-  , downloadInfoSha256 :: Maybe ByteString
+  , contentLength :: Maybe Int
+  , sha1 :: Maybe ByteString
+  , sha256 :: Maybe ByteString
   }
   deriving Show
 
@@ -34,10 +35,12 @@   contentLength <- o ..:? "content-length"
   sha1TextMay <- o ..:? "sha1"
   sha256TextMay <- o ..:? "sha256"
+  let sha1 = fmap encodeUtf8 sha1TextMay
+      sha256 = fmap encodeUtf8 sha256TextMay
   pure
     DownloadInfo
-    { downloadInfoUrl = url
-    , downloadInfoContentLength = contentLength
-    , downloadInfoSha1 = fmap encodeUtf8 sha1TextMay
-    , downloadInfoSha256 = fmap encodeUtf8 sha256TextMay
+    { url
+    , contentLength
+    , sha1
+    , sha256
     }
src/Stack/Types/DumpPackage.hs view
@@ -1,37 +1,59 @@-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 
 module Stack.Types.DumpPackage
   ( DumpPackage (..)
+  , SublibDump (..)
+  , dpParentLibIdent
   ) where
 
 import qualified Distribution.License as C
 import           Distribution.ModuleName ( ModuleName )
 import           Stack.Prelude
+import           Stack.Types.Component ( StackUnqualCompName )
 import           Stack.Types.GhcPkgId ( GhcPkgId )
 
 -- | Type representing dump information for a single package, as output by the
 -- @ghc-pkg describe@ command.
 data DumpPackage = DumpPackage
-  { dpGhcPkgId :: !GhcPkgId
+  { ghcPkgId :: !GhcPkgId
     -- ^ The @id@ field.
-  , dpPackageIdent :: !PackageIdentifier
+  , packageIdent :: !PackageIdentifier
     -- ^ The @name@ and @version@ fields. The @name@ field is the munged package
     -- name. If the package is not for a sub library, its munged name is its
     -- name.
-  , dpParentLibIdent :: !(Maybe PackageIdentifier)
-    -- ^ The @package-name@ and @version@ fields, if @package-name@ is present.
-    -- That field is present if the package is for a sub library.
-  , dpLicense :: !(Maybe C.License)
-  , dpLibDirs :: ![FilePath]
+  , sublib :: !(Maybe SublibDump)
+    -- ^ The sub library information if it's a sub-library.
+  , license :: !(Maybe C.License)
+  , libDirs :: ![FilePath]
     -- ^ The @library-dirs@ field.
-  , dpLibraries :: ![Text]
+  , libraries :: ![Text]
     -- ^ The @hs-libraries@ field.
-  , dpHasExposedModules :: !Bool
-  , dpExposedModules :: !(Set ModuleName)
-  , dpDepends :: ![GhcPkgId]
+  , hasExposedModules :: !Bool
+  , exposedModules :: !(Set ModuleName)
+  , depends :: ![GhcPkgId]
     -- ^ The @depends@ field (packages on which this package depends).
-  , dpHaddockInterfaces :: ![FilePath]
-  , dpHaddockHtml :: !(Maybe FilePath)
-  , dpIsExposed :: !Bool
+  , haddockInterfaces :: ![FilePath]
+  , haddockHtml :: !(Maybe FilePath)
+  , isExposed :: !Bool
   }
   deriving (Eq, Read, Show)
+
+-- | ghc-pkg has a notion of sublibraries when using ghc-pkg dump. We can only
+-- know it's different through the fields it shows.
+data SublibDump = SublibDump
+  { packageName :: PackageName
+    -- ^ "package-name" field from ghc-pkg
+  , libraryName :: StackUnqualCompName
+    -- ^ "lib-name" field from ghc-pkg
+  }
+  deriving (Eq, Read, Show)
+
+dpParentLibIdent :: DumpPackage -> Maybe PackageIdentifier
+dpParentLibIdent dp = case (dp.sublib, dp.packageIdent) of
+  (Nothing, _) -> Nothing
+  (Just sublibDump, PackageIdentifier _ v) ->
+    Just $ PackageIdentifier libParentPackageName v
+   where
+    SublibDump { packageName = libParentPackageName } = sublibDump
src/Stack/Types/EnvConfig.hs view
@@ -1,4 +1,6 @@-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 
 module Stack.Types.EnvConfig
   ( EnvConfig (..)
@@ -45,7 +47,7 @@ import           Stack.Prelude
 import           Stack.Types.BuildConfig
                     ( BuildConfig (..), HasBuildConfig (..), getProjectWorkDir )
-import           Stack.Types.BuildOpts ( BuildOptsCLI )
+import           Stack.Types.BuildOptsCLI ( BuildOptsCLI )
 import           Stack.Types.Compiler
                    ( ActualCompiler (..), compilerVersionString, getGhcVersion )
 import           Stack.Types.CompilerBuild ( compilerBuildSuffix )
@@ -62,54 +64,54 @@ 
 -- | Configuration after the environment has been setup.
 data EnvConfig = EnvConfig
-  { envConfigBuildConfig :: !BuildConfig
-  , envConfigBuildOptsCLI :: !BuildOptsCLI
-  , envConfigFileDigestCache :: !FileDigestCache
-  , envConfigSourceMap :: !SourceMap
-  , envConfigSourceMapHash :: !SourceMapHash
-  , envConfigCompilerPaths :: !CompilerPaths
+  { buildConfig :: !BuildConfig
+  , buildOptsCLI :: !BuildOptsCLI
+  , fileDigestCache :: !FileDigestCache
+  , sourceMap :: !SourceMap
+  , sourceMapHash :: !SourceMapHash
+  , compilerPaths :: !CompilerPaths
   }
 
 instance HasConfig EnvConfig where
-  configL = buildConfigL.lens bcConfig (\x y -> x { bcConfig = y })
+  configL = buildConfigL . lens (.config) (\x y -> x { config = y })
   {-# INLINE configL #-}
 
 instance HasBuildConfig EnvConfig where
-  buildConfigL = envConfigL.lens
-    envConfigBuildConfig
-    (\x y -> x { envConfigBuildConfig = y })
+  buildConfigL = envConfigL . lens
+    (.buildConfig)
+    (\x y -> x { buildConfig = y })
 
 instance HasPlatform EnvConfig where
-  platformL = configL.platformL
+  platformL = configL . platformL
   {-# INLINE platformL #-}
-  platformVariantL = configL.platformVariantL
+  platformVariantL = configL . platformVariantL
   {-# INLINE platformVariantL #-}
 
 instance HasGHCVariant EnvConfig where
-  ghcVariantL = configL.ghcVariantL
+  ghcVariantL = configL . ghcVariantL
   {-# INLINE ghcVariantL #-}
 
 instance HasProcessContext EnvConfig where
-  processContextL = configL.processContextL
+  processContextL = configL . processContextL
 
 instance HasPantryConfig EnvConfig where
-  pantryConfigL = configL.pantryConfigL
+  pantryConfigL = configL . pantryConfigL
 
 instance HasCompiler EnvConfig where
-  compilerPathsL = to envConfigCompilerPaths
+  compilerPathsL = to (.compilerPaths)
 
 instance HasRunner EnvConfig where
-  runnerL = configL.runnerL
+  runnerL = configL . runnerL
 
 instance HasLogFunc EnvConfig where
-  logFuncL = runnerL.logFuncL
+  logFuncL = runnerL . logFuncL
 
 instance HasStylesUpdate EnvConfig where
-  stylesUpdateL = runnerL.stylesUpdateL
+  stylesUpdateL = runnerL . stylesUpdateL
 
 instance HasTerm EnvConfig where
-  useColorL = runnerL.useColorL
-  termWidthL = runnerL.termWidthL
+  useColorL = runnerL . useColorL
+  termWidthL = runnerL . termWidthL
 
 class (HasBuildConfig env, HasSourceMap env, HasCompiler env) => HasEnvConfig env where
   envConfigL :: Lens' env EnvConfig
@@ -122,7 +124,7 @@   sourceMapL :: Lens' env SourceMap
 
 instance HasSourceMap EnvConfig where
-  sourceMapL = lens envConfigSourceMap (\x y -> x { envConfigSourceMap = y })
+  sourceMapL = lens (.sourceMap) (\x y -> x { sourceMap = y })
 
 shouldForceGhcColorFlag ::
      (HasEnvConfig env, HasRunner env)
@@ -176,7 +178,7 @@ platformSnapAndCompilerRel :: HasEnvConfig env => RIO env (Path Rel Dir)
 platformSnapAndCompilerRel = do
   platform <- platformGhcRelDir
-  smh <- view $ envConfigL.to envConfigSourceMapHash
+  smh <- view $ envConfigL . to (.sourceMapHash)
   name <- smRelDir smh
   ghc <- compilerVersionDir
   useShaPathOnWindows (platform </> name </> ghc)
@@ -187,7 +189,7 @@   => m (Path Rel Dir)
 platformGhcRelDir = do
   cp <- view compilerPathsL
-  let cbSuffix = compilerBuildSuffix $ cpBuild cp
+  let cbSuffix = compilerBuildSuffix cp.build
   verOnly <- platformGhcVerOnlyRelDirStr
   parseRelDir (mconcat [ verOnly, cbSuffix ])
 
@@ -239,7 +241,7 @@ packageDatabaseExtra ::
      (HasEnvConfig env, MonadReader env m)
   => m [Path Abs Dir]
-packageDatabaseExtra = view $ buildConfigL.to bcExtraPackageDBs
+packageDatabaseExtra = view $ buildConfigL . to (.extraPackageDBs)
 
 -- | Where HPC reports and tix files get stored.
 hpcReportDir :: HasEnvConfig env => RIO env (Path Abs Dir)
@@ -263,7 +265,7 @@ -- than that specified in the 'SnapshotDef' and returned by
 -- 'wantedCompilerVersionL'.
 actualCompilerVersionL :: HasSourceMap env => SimpleGetter env ActualCompiler
-actualCompilerVersionL = sourceMapL.to smCompiler
+actualCompilerVersionL = sourceMapL . to (.compiler)
 
 -- | Relative directory for the platform and GHC identifier without GHC bindist
 -- build
src/Stack/Types/EnvSettings.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoFieldSelectors  #-}
 
 module Stack.Types.EnvSettings
   ( EnvSettings (..)
@@ -11,15 +12,15 @@ 
 -- | Controls which version of the environment is used
 data EnvSettings = EnvSettings
-  { esIncludeLocals :: !Bool
+  { includeLocals :: !Bool
   -- ^ include local project bin directory, GHC_PACKAGE_PATH, etc
-  , esIncludeGhcPackagePath :: !Bool
+  , includeGhcPackagePath :: !Bool
   -- ^ include the GHC_PACKAGE_PATH variable
-  , esStackExe :: !Bool
+  , stackExe :: !Bool
   -- ^ set the STACK_EXE variable to the current executable name
-  , esLocaleUtf8 :: !Bool
+  , localeUtf8 :: !Bool
   -- ^ set the locale to C.UTF-8
-  , esKeepGhcRts :: !Bool
+  , keepGhcRts :: !Bool
   -- ^ if True, keep GHCRTS variable in environment
   }
   deriving (Eq, Ord, Show)
@@ -27,11 +28,11 @@ minimalEnvSettings :: EnvSettings
 minimalEnvSettings =
   EnvSettings
-  { esIncludeLocals = False
-  , esIncludeGhcPackagePath = False
-  , esStackExe = False
-  , esLocaleUtf8 = False
-  , esKeepGhcRts = False
+  { includeLocals = False
+  , includeGhcPackagePath = False
+  , stackExe = False
+  , localeUtf8 = False
+  , keepGhcRts = False
   }
 
 -- | Default @EnvSettings@ which includes locals and GHC_PACKAGE_PATH.
@@ -40,11 +41,11 @@ -- See https://github.com/commercialhaskell/stack/issues/3444
 defaultEnvSettings :: EnvSettings
 defaultEnvSettings = EnvSettings
-  { esIncludeLocals = True
-  , esIncludeGhcPackagePath = True
-  , esStackExe = True
-  , esLocaleUtf8 = False
-  , esKeepGhcRts = True
+  { includeLocals = True
+  , includeGhcPackagePath = True
+  , stackExe = True
+  , localeUtf8 = False
+  , keepGhcRts = True
   }
 
 -- | Environment settings which do not embellish the environment
@@ -53,9 +54,9 @@ -- See https://github.com/commercialhaskell/stack/issues/3444
 plainEnvSettings :: EnvSettings
 plainEnvSettings = EnvSettings
-  { esIncludeLocals = False
-  , esIncludeGhcPackagePath = False
-  , esStackExe = False
-  , esLocaleUtf8 = False
-  , esKeepGhcRts = True
+  { includeLocals = False
+  , includeGhcPackagePath = False
+  , stackExe = False
+  , localeUtf8 = False
+  , keepGhcRts = True
   }
src/Stack/Types/ExtraDirs.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoFieldSelectors  #-}
 
 module Stack.Types.ExtraDirs
   ( ExtraDirs (..)
@@ -8,9 +9,9 @@ import           Stack.Prelude
 
 data ExtraDirs = ExtraDirs
-  { edBins :: ![Path Abs Dir]
-  , edInclude :: ![Path Abs Dir]
-  , edLib :: ![Path Abs Dir]
+  { bins :: ![Path Abs Dir]
+  , includes :: ![Path Abs Dir]
+  , libs :: ![Path Abs Dir]
   }
   deriving (Show, Generic)
 
src/Stack/Types/GHCDownloadInfo.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoFieldSelectors  #-}
 {-# LANGUAGE OverloadedStrings #-}
 
 module Stack.Types.GHCDownloadInfo
@@ -13,9 +14,9 @@                    ( DownloadInfo, parseDownloadInfoFromObject )
 
 data GHCDownloadInfo = GHCDownloadInfo
-  { gdiConfigureOpts :: [Text]
-  , gdiConfigureEnv :: Map Text Text
-  , gdiDownloadInfo :: DownloadInfo
+  { configureOpts :: [Text]
+  , configureEnv :: Map Text Text
+  , downloadInfo :: DownloadInfo
   }
   deriving Show
 
@@ -25,7 +26,7 @@     configureEnv <- o ..:? "configure-env" ..!= mempty
     downloadInfo <- parseDownloadInfoFromObject o
     pure GHCDownloadInfo
-      { gdiConfigureOpts = configureOpts
-      , gdiConfigureEnv = configureEnv
-      , gdiDownloadInfo = downloadInfo
+      { configureOpts
+      , configureEnv
+      , downloadInfo
       }
src/Stack/Types/GhcOptions.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoFieldSelectors  #-}
 
 module Stack.Types.GhcOptions
   ( GhcOptions (..)
@@ -9,7 +10,7 @@ import qualified Data.Text as T
 import           Stack.Prelude
 
-newtype GhcOptions = GhcOptions { unGhcOptions :: [Text] }
+newtype GhcOptions = GhcOptions { ghcOptions :: [Text] }
 
 instance FromJSON GhcOptions where
   parseJSON = withText "GhcOptions" $ \t ->
src/Stack/Types/GhcPkgId.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE NoImplicitPrelude          #-}
+{-# LANGUAGE NoImplicitPrelude #-}
 
 -- | A ghc-pkg id.
 
@@ -12,13 +12,14 @@ 
 import           Data.Aeson.Types ( FromJSON (..), ToJSON (..), withText )
 import           Data.Attoparsec.Text
-                   ( Parser, choice, digit, endOfInput, letter, many1, parseOnly
+                   ( Parser, (<?>), choice, endOfInput, many1, parseOnly
                    , satisfy
                    )
+import           Data.Char ( isAlphaNum )
 import qualified Data.Text as T
 import           Database.Persist.Sql ( PersistField, PersistFieldSql )
-import           Prelude ( Read (..) )
 import           Stack.Prelude
+import           Text.Read ( Read (..) )
 
 -- | A parse fail.
 newtype GhcPkgIdParseFail
@@ -70,7 +71,12 @@ ghcPkgIdParser =
   let elements = "_.-" :: String
   in  GhcPkgId . T.pack <$>
-        many1 (choice [digit, letter, satisfy (`elem` elements)])
+        many1 (choice [alphaNum, satisfy (`elem` elements)])
+
+-- | Parse an alphanumerical character, as recognised by `isAlphaNum`.
+alphaNum :: Parser Char
+alphaNum = satisfy isAlphaNum <?> "alphanumeric"
+{-# INLINE alphaNum #-}
 
 -- | Get a string representation of GHC package id.
 ghcPkgIdString :: GhcPkgId -> String
src/Stack/Types/GlobalOpts.hs view
@@ -1,4 +1,6 @@-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 
 module Stack.Types.GlobalOpts
   ( GlobalOpts (..)
@@ -6,7 +8,7 @@   ) where
 
 import          Stack.Prelude
-import          Stack.Types.BuildOpts ( BuildOptsMonoid )
+import          Stack.Types.BuildOptsMonoid ( BuildOptsMonoid )
 import          Stack.Types.ConfigMonoid ( ConfigMonoid (..) )
 import          Stack.Types.DockerEntrypoint ( DockerEntrypoint )
 import          Stack.Types.LockFileBehavior ( LockFileBehavior )
@@ -15,36 +17,31 @@ 
 -- | Parsed global command-line options.
 data GlobalOpts = GlobalOpts
-  { globalReExecVersion :: !(Maybe String)
+  { reExecVersion :: !(Maybe String)
     -- ^ Expected re-exec in container version
-  , globalDockerEntrypoint :: !(Maybe DockerEntrypoint)
+  , dockerEntrypoint :: !(Maybe DockerEntrypoint)
     -- ^ Data used when Stack is acting as a Docker entrypoint (internal use
     -- only)
-  , globalLogLevel     :: !LogLevel -- ^ Log level
-  , globalTimeInLog    :: !Bool -- ^ Whether to include timings in logs.
-  , globalRSLInLog     :: !Bool
+  , logLevel     :: !LogLevel -- ^ Log level
+  , timeInLog    :: !Bool -- ^ Whether to include timings in logs.
+  , rslInLog     :: !Bool
     -- ^ Whether to include raw snapshot layer (RSL) in logs.
-  , globalPlanInLog :: !Bool
+  , planInLog :: !Bool
     -- ^ Whether to include debug information about the construction of the
     -- build plan in logs.
-  , globalConfigMonoid :: !ConfigMonoid
+  , configMonoid :: !ConfigMonoid
     -- ^ Config monoid, for passing into 'loadConfig'
-  , globalResolver     :: !(Maybe AbstractResolver) -- ^ Resolver override
-  , globalCompiler     :: !(Maybe WantedCompiler) -- ^ Compiler override
-  , globalTerminal     :: !Bool -- ^ We're in a terminal?
-  , globalStylesUpdate :: !StylesUpdate -- ^ SGR (Ansi) codes for styles
-  , globalTermWidth    :: !(Maybe Int) -- ^ Terminal width override
-  , globalStackYaml    :: !StackYamlLoc -- ^ Override project stack.yaml
-  , globalLockFileBehavior :: !LockFileBehavior
+  , resolver     :: !(Maybe AbstractResolver) -- ^ Resolver override
+  , compiler     :: !(Maybe WantedCompiler) -- ^ Compiler override
+  , terminal     :: !Bool -- ^ We're in a terminal?
+  , stylesUpdate :: !StylesUpdate -- ^ SGR (Ansi) codes for styles
+  , termWidthOpt  :: !(Maybe Int) -- ^ Terminal width override
+  , stackYaml    :: !StackYamlLoc -- ^ Override project stack.yaml
+  , lockFileBehavior :: !LockFileBehavior
   }
   deriving Show
 
 globalOptsBuildOptsMonoidL :: Lens' GlobalOpts BuildOptsMonoid
 globalOptsBuildOptsMonoidL =
-  lens
-    globalConfigMonoid
-    (\x y -> x { globalConfigMonoid = y })
-  .
-  lens
-    configMonoidBuildOpts
-    (\x y -> x { configMonoidBuildOpts = y })
+    lens (.configMonoid) (\x y -> x { configMonoid = y })
+  . lens (.buildOpts) (\x y -> x { buildOpts = y })
src/Stack/Types/GlobalOptsMonoid.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoFieldSelectors  #-}
 
 module Stack.Types.GlobalOptsMonoid
   ( GlobalOptsMonoid (..)
@@ -13,37 +14,37 @@ 
 -- | Parsed global command-line options monoid.
 data GlobalOptsMonoid = GlobalOptsMonoid
-  { globalMonoidReExecVersion :: !(First String)
+  { reExecVersion    :: !(First String)
     -- ^ Expected re-exec in container version
-  , globalMonoidDockerEntrypoint :: !(First DockerEntrypoint)
+  , dockerEntrypoint :: !(First DockerEntrypoint)
     -- ^ Data used when Stack is acting as a Docker entrypoint (internal use
     -- only)
-  , globalMonoidLogLevel     :: !(First LogLevel)
+  , logLevel         :: !(First LogLevel)
     -- ^ Log level
-  , globalMonoidTimeInLog    :: !FirstTrue
+  , timeInLog        :: !FirstTrue
     -- ^ Whether to include timings in logs.
-  , globalMonoidRSLInLog     :: !FirstFalse
+  , rslInLog         :: !FirstFalse
     -- ^ Whether to include raw snapshot layer (RSL) in logs.
-  , globalMonoidPlanInLog :: !FirstFalse
+  , planInLog        :: !FirstFalse
     -- ^ Whether to include debug information about the construction of the
     -- build plan in logs.
-  , globalMonoidConfigMonoid :: !ConfigMonoid
+  , configMonoid     :: !ConfigMonoid
     -- ^ Config monoid, for passing into 'loadConfig'
-  , globalMonoidResolver     :: !(First (Unresolved AbstractResolver))
+  , resolver         :: !(First (Unresolved AbstractResolver))
     -- ^ Resolver override
-  , globalMonoidResolverRoot :: !(First FilePath)
+  , resolverRoot     :: !(First FilePath)
     -- ^ root directory for resolver relative path
-  , globalMonoidCompiler     :: !(First WantedCompiler)
+  , compiler         :: !(First WantedCompiler)
     -- ^ Compiler override
-  , globalMonoidTerminal     :: !(First Bool)
+  , terminal         :: !(First Bool)
     -- ^ We're in a terminal?
-  , globalMonoidStyles       :: !StylesUpdate
+  , styles           :: !StylesUpdate
     -- ^ Stack's output styles
-  , globalMonoidTermWidth    :: !(First Int)
+  , termWidthOpt     :: !(First Int)
     -- ^ Terminal width override
-  , globalMonoidStackYaml    :: !(First FilePath)
+  , stackYaml        :: !(First FilePath)
     -- ^ Override project stack.yaml
-  , globalMonoidLockFileBehavior :: !(First LockFileBehavior)
+  , lockFileBehavior :: !(First LockFileBehavior)
     -- ^ See 'globalLockFileBehavior'
   }
   deriving Generic
+ src/Stack/Types/Installed.hs view
@@ -0,0 +1,154 @@+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+
+-- | This module contains all the types related to the idea of installing a
+-- package in the pkg-db or an executable on the file system.
+module Stack.Types.Installed
+  ( InstallLocation (..)
+  , InstalledPackageLocation (..)
+  , PackageDatabase (..)
+  , PackageDbVariety (..)
+  , InstallMap
+  , Installed (..)
+  , InstalledMap
+  , InstalledLibraryInfo (..)
+  , toPackageDbVariety
+  , installedLibraryInfoFromGhcPkgId
+  , simpleInstalledLib
+  , installedToPackageIdOpt
+  , installedPackageIdentifier
+  , installedVersion
+  , foldOnGhcPkgId'
+  ) where
+
+import qualified Data.Map as M
+import qualified Distribution.SPDX.License as SPDX
+import           Distribution.License ( License )
+import           Stack.Prelude
+import           Stack.Types.ComponentUtils ( StackUnqualCompName )
+import           Stack.Types.GhcPkgId ( GhcPkgId, ghcPkgIdString )
+
+-- | Type representing user package databases that packages can be installed
+-- into.
+data InstallLocation
+  = Snap
+    -- ^ The write-only package database, formerly known as the snapshot
+    -- database.
+  | Local
+    -- ^ The mutable package database, formerly known as the local database.
+  deriving (Eq, Show)
+
+instance Semigroup InstallLocation where
+  Local <> _ = Local
+  _ <> Local = Local
+  Snap <> Snap = Snap
+
+instance Monoid InstallLocation where
+  mempty = Snap
+  mappend = (<>)
+
+-- | Type representing user (non-global) package databases that can provide
+-- installed packages.
+data InstalledPackageLocation
+  = InstalledTo InstallLocation
+    -- ^ A package database that a package can be installed into.
+  | ExtraPkgDb
+    -- ^ An \'extra\' package database, specified by @extra-package-dbs@.
+  deriving (Eq, Show)
+
+-- | Type representing package databases that can provide installed packages.
+data PackageDatabase
+  = GlobalPkgDb
+    -- ^ GHC's global package database.
+  | UserPkgDb InstalledPackageLocation (Path Abs Dir)
+    -- ^ A user package database.
+  deriving (Eq, Show)
+
+-- | A function to yield the variety of package database for a given
+-- package database that can provide installed packages.
+toPackageDbVariety :: PackageDatabase -> PackageDbVariety
+toPackageDbVariety GlobalPkgDb = GlobalDb
+toPackageDbVariety (UserPkgDb ExtraPkgDb _) = ExtraDb
+toPackageDbVariety (UserPkgDb (InstalledTo Snap) _) = WriteOnlyDb
+toPackageDbVariety (UserPkgDb (InstalledTo Local) _) = MutableDb
+
+-- | Type representing varieties of package databases that can provide
+-- installed packages.
+data PackageDbVariety
+  = GlobalDb
+    -- ^ GHC's global package database.
+  | ExtraDb
+    -- ^ An \'extra\' package database, specified by @extra-package-dbs@.
+  | WriteOnlyDb
+    -- ^ The write-only package database, for immutable packages.
+  | MutableDb
+    -- ^ The mutable package database.
+  deriving (Eq, Show)
+
+-- | Type synonym representing dictionaries of package names for a project's
+-- packages and dependencies, and pairs of their relevant database (write-only
+-- or mutable) and package versions.
+type InstallMap = Map PackageName (InstallLocation, Version)
+
+-- | Type synonym representing dictionaries of package names, and a pair of in
+-- which package database the package is installed (write-only or mutable) and
+-- information about what is installed.
+type InstalledMap = Map PackageName (InstallLocation, Installed)
+
+data InstalledLibraryInfo = InstalledLibraryInfo
+  { ghcPkgId :: GhcPkgId
+  , license :: Maybe (Either SPDX.License License)
+  , subLib :: Map StackUnqualCompName GhcPkgId
+  }
+  deriving (Eq, Show)
+
+-- | Type representing information about what is installed.
+data Installed
+  = Library PackageIdentifier InstalledLibraryInfo
+    -- ^ A library, including its installed package id and, optionally, its
+    -- license.
+  | Executable PackageIdentifier
+    -- ^ An executable.
+  deriving (Eq, Show)
+
+installedLibraryInfoFromGhcPkgId :: GhcPkgId -> InstalledLibraryInfo
+installedLibraryInfoFromGhcPkgId ghcPkgId =
+  InstalledLibraryInfo ghcPkgId Nothing mempty
+
+simpleInstalledLib ::
+     PackageIdentifier
+  -> GhcPkgId
+  -> Map StackUnqualCompName GhcPkgId
+  -> Installed
+simpleInstalledLib pkgIdentifier ghcPkgId =
+  Library pkgIdentifier . InstalledLibraryInfo ghcPkgId Nothing
+
+installedToPackageIdOpt :: InstalledLibraryInfo -> [String]
+installedToPackageIdOpt libInfo =
+  M.foldr' (iterator (++)) (pure $ toStr libInfo.ghcPkgId) libInfo.subLib
+ where
+  toStr ghcPkgId = "-package-id=" <> ghcPkgIdString ghcPkgId
+  iterator op ghcPkgId acc = pure (toStr ghcPkgId) `op` acc
+
+installedPackageIdentifier :: Installed -> PackageIdentifier
+installedPackageIdentifier (Library pid _) = pid
+installedPackageIdentifier (Executable pid) = pid
+
+-- | A strict fold over the 'GhcPkgId' of the given installed package. This will
+-- iterate on both sub and main libraries, if any.
+foldOnGhcPkgId' ::
+     (Maybe StackUnqualCompName -> GhcPkgId -> resT -> resT)
+  -> Installed
+  -> resT
+  -> resT
+foldOnGhcPkgId' _ Executable{} res = res
+foldOnGhcPkgId' fn (Library _ libInfo) res =
+  M.foldrWithKey' (fn . Just) (base res) libInfo.subLib
+ where
+  base = fn Nothing libInfo.ghcPkgId
+
+-- | Get the installed Version.
+installedVersion :: Installed -> Version
+installedVersion i =
+  let PackageIdentifier _ version = installedPackageIdentifier i
+  in  version
src/Stack/Types/NamedComponent.hs view
@@ -1,38 +1,55 @@ {-# LANGUAGE NoImplicitPrelude #-}
 {-# LANGUAGE OverloadedStrings #-}
 
+-- | Module exporting the 'NamedComponent' type and related functions.
 module Stack.Types.NamedComponent
   ( NamedComponent (..)
   , renderComponent
+  , renderComponentTo
   , renderPkgComponents
   , renderPkgComponent
   , exeComponents
   , testComponents
   , benchComponents
-  , internalLibComponents
+  , subLibComponents
   , isCLib
-  , isCInternalLib
+  , isCSubLib
   , isCExe
   , isCTest
   , isCBench
+  , isPotentialDependency
+  , splitComponents
   ) where
 
 import qualified Data.Set as Set
 import qualified Data.Text as T
 import           Stack.Prelude
 
--- | A single, fully resolved component of a package
+-- | Type representing components of a fully-resolved Cabal package.
 data NamedComponent
   = CLib
-  | CInternalLib !Text
+    -- The \'main\' unnamed library component.
+  | CSubLib !Text
+    -- A named \'subsidiary\' or \'ancillary\` library component (sub-library).
+  | CFlib !Text
+    -- A foreign library.
   | CExe !Text
+    -- A named executable component.
   | CTest !Text
+    -- A named test-suite component.
   | CBench !Text
+    -- A named benchmark component.
   deriving (Eq, Ord, Show)
 
+-- | Render a component to anything with an "IsString" instance. For 'Text'
+-- prefer 'renderComponent'.
+renderComponentTo :: IsString a => NamedComponent -> a
+renderComponentTo = fromString . T.unpack . renderComponent
+
 renderComponent :: NamedComponent -> Text
 renderComponent CLib = "lib"
-renderComponent (CInternalLib x) = "internal-lib:" <> x
+renderComponent (CSubLib x) = "sub-lib:" <> x
+renderComponent (CFlib x) = "flib:" <> x
 renderComponent (CExe x) = "exe:" <> x
 renderComponent (CTest x) = "test:" <> x
 renderComponent (CBench x) = "bench:" <> x
@@ -42,7 +59,7 @@ 
 renderPkgComponent :: (PackageName, NamedComponent) -> Text
 renderPkgComponent (pkg, comp) =
-  fromString (packageNameString pkg) <> ":" <> renderComponent comp
+  fromPackageName pkg <> ":" <> renderComponent comp
 
 exeComponents :: Set NamedComponent -> Set Text
 exeComponents = Set.fromList . mapMaybe mExeName . Set.toList
@@ -62,19 +79,19 @@   mBenchName (CBench name) = Just name
   mBenchName _ = Nothing
 
-internalLibComponents :: Set NamedComponent -> Set Text
-internalLibComponents = Set.fromList . mapMaybe mInternalName . Set.toList
+subLibComponents :: Set NamedComponent -> Set Text
+subLibComponents = Set.fromList . mapMaybe mSubLibName . Set.toList
  where
-  mInternalName (CInternalLib name) = Just name
-  mInternalName _ = Nothing
+  mSubLibName (CSubLib name) = Just name
+  mSubLibName _ = Nothing
 
 isCLib :: NamedComponent -> Bool
 isCLib CLib{} = True
 isCLib _ = False
 
-isCInternalLib :: NamedComponent -> Bool
-isCInternalLib CInternalLib{} = True
-isCInternalLib _ = False
+isCSubLib :: NamedComponent -> Bool
+isCSubLib CSubLib{} = True
+isCSubLib _ = False
 
 isCExe :: NamedComponent -> Bool
 isCExe CExe{} = True
@@ -87,3 +104,35 @@ isCBench :: NamedComponent -> Bool
 isCBench CBench{} = True
 isCBench _ = False
+
+isPotentialDependency :: NamedComponent -> Bool
+isPotentialDependency v = isCLib v || isCSubLib v || isCExe v
+
+-- | A function to split the given list of components into sets of the names of
+-- the named components by the type of component (sub-libraries, executables,
+-- test-suites, benchmarks), ignoring any 'main' unnamed library component or
+-- foreign library component. This function should be used very sparingly; more
+-- often than not, you can keep/parse the components split from the start.
+splitComponents ::
+     [NamedComponent]
+  -> ( Set Text
+       -- ^ Sub-libraries.
+     , Set Text
+       -- ^ Executables.
+     , Set Text
+       -- ^ Test-suites.
+     , Set Text
+       -- ^ Benchmarks.
+     )
+splitComponents =
+  go id id id id
+ where
+  run c = Set.fromList $ c []
+  go s e t b [] = (run s, run e, run t, run b)
+  go s e t b (CLib : xs) = go s e t b xs
+  go s e t b (CSubLib x : xs) = go (s . (x:)) e t b xs
+  -- Ignore foreign libraries, for now.
+  go s e t b (CFlib _ : xs) = go s e t b xs
+  go s e t b (CExe x : xs) = go s (e . (x:)) t b xs
+  go s e t b (CTest x : xs) = go s e (t . (x:)) b xs
+  go s e t b (CBench x : xs) = go s e t (b . (x:)) xs
src/Stack/Types/Nix.hs view
@@ -1,6 +1,7 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- | Nix types.
 module Stack.Types.Nix
@@ -24,16 +25,16 @@ -- | Nix configuration. Parameterize by resolver type to avoid cyclic
 -- dependency.
 data NixOpts = NixOpts
-  { nixEnable :: !Bool
-  , nixPureShell :: !Bool
-  , nixPackages :: ![Text]
+  { enable :: !Bool
+  , pureShell :: !Bool
+  , packages :: ![Text]
     -- ^ The system packages to be installed in the environment before it runs
-  , nixInitFile :: !(Maybe FilePath)
+  , initFile :: !(Maybe FilePath)
     -- ^ The path of a file containing preconfiguration of the environment
     -- (e.g shell.nix)
-  , nixShellOptions :: ![Text]
+  , shellOptions :: ![Text]
     -- ^ Options to be given to the nix-shell command line
-  , nixAddGCRoots :: !Bool
+  , addGCRoots :: !Bool
     -- ^ Should we register gc roots so running nix-collect-garbage doesn't
     -- remove nix dependencies
   }
@@ -42,20 +43,20 @@ -- | An uninterpreted representation of nix options.
 -- Configurations may be "cascaded" using mappend (left-biased).
 data NixOptsMonoid = NixOptsMonoid
-  { nixMonoidEnable :: !(First Bool)
+  { enable :: !(First Bool)
     -- ^ Is using nix-shell enabled?
-  , nixMonoidPureShell :: !(First Bool)
+  , pureShell :: !(First Bool)
     -- ^ Should the nix-shell be pure
-  , nixMonoidPackages :: !(First [Text])
+  , packages :: !(First [Text])
     -- ^ System packages to use (given to nix-shell)
-  , nixMonoidInitFile :: !(First FilePath)
+  , initFile :: !(First FilePath)
     -- ^ The path of a file containing preconfiguration of the environment (e.g
     -- shell.nix)
-  , nixMonoidShellOptions :: !(First [Text])
+  , shellOptions :: !(First [Text])
     -- ^ Options to be given to the nix-shell command line
-  , nixMonoidPath :: !(First [Text])
+  , path :: !(First [Text])
     -- ^ Override parts of NIX_PATH (notably 'nixpkgs')
-  , nixMonoidAddGCRoots :: !FirstFalse
+  , addGCRoots :: !FirstFalse
     -- ^ Should we register gc roots so running nix-collect-garbage doesn't
     -- remove nix dependencies
   }
@@ -63,17 +64,23 @@ 
 -- | Decode uninterpreted nix options from JSON/YAML.
 instance FromJSON (WithJSONWarnings NixOptsMonoid) where
-  parseJSON = withObjectWarnings "NixOptsMonoid"
-    ( \o -> do
-        nixMonoidEnable        <- First <$> o ..:? nixEnableArgName
-        nixMonoidPureShell     <- First <$> o ..:? nixPureShellArgName
-        nixMonoidPackages      <- First <$> o ..:? nixPackagesArgName
-        nixMonoidInitFile      <- First <$> o ..:? nixInitFileArgName
-        nixMonoidShellOptions  <- First <$> o ..:? nixShellOptsArgName
-        nixMonoidPath          <- First <$> o ..:? nixPathArgName
-        nixMonoidAddGCRoots    <- FirstFalse <$> o ..:? nixAddGCRootsArgName
-        pure NixOptsMonoid{..}
-    )
+  parseJSON = withObjectWarnings "NixOptsMonoid" $ \o -> do
+    enable        <- First <$> o ..:? nixEnableArgName
+    pureShell     <- First <$> o ..:? nixPureShellArgName
+    packages      <- First <$> o ..:? nixPackagesArgName
+    initFile      <- First <$> o ..:? nixInitFileArgName
+    shellOptions  <- First <$> o ..:? nixShellOptsArgName
+    path          <- First <$> o ..:? nixPathArgName
+    addGCRoots    <- FirstFalse <$> o ..:? nixAddGCRootsArgName
+    pure NixOptsMonoid
+      { enable
+      , pureShell
+      , packages
+      , initFile
+      , shellOptions
+      , path
+      , addGCRoots
+      }
 
 -- | Left-biased combine Nix options
 instance Semigroup NixOptsMonoid where
src/Stack/Types/Package.hs view
@@ -1,23 +1,26 @@-{-# LANGUAGE NoImplicitPrelude          #-}
-{-# LANGUAGE DataKinds                  #-}
-{-# LANGUAGE OverloadedStrings          #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 module Stack.Types.Package
-  ( BuildInfoOpts (..)
+  ( BioInput (..)
+  , BuildInfoOpts (..)
   , ExeName (..)
   , FileCacheInfo (..)
-  , GetPackageOpts (..)
   , InstallLocation (..)
-  , InstallMap
   , Installed (..)
+  , InstalledLibraryInfo (..)
   , InstalledPackageLocation (..)
-  , InstalledMap
   , LocalPackage (..)
   , MemoizedWith (..)
   , Package (..)
   , PackageConfig (..)
+  , PackageDatabase (..)
+  , PackageDbVariety (..)
   , PackageException (..)
-  , PackageLibraries (..)
   , PackageSource (..)
   , dotCabalCFilePath
   , dotCabalGetPath
@@ -25,16 +28,17 @@   , dotCabalMainPath
   , dotCabalModule
   , dotCabalModulePath
-  , installedPackageIdentifier
-  , installedVersion
+  , installedMapGhcPkgId
   , lpFiles
   , lpFilesForComponents
   , memoizeRefWith
   , packageDefinedFlags
-  , packageIdent
   , packageIdentifier
   , psVersion
   , runMemoizedWith
+  , simpleInstalledLib
+  , toCabalMungedPackageName
+  , toPackageDbVariety
   ) where
 
 import           Data.Aeson
@@ -47,22 +51,36 @@ import qualified Distribution.SPDX.License as SPDX
 import           Distribution.License ( License )
 import           Distribution.ModuleName ( ModuleName )
-import           Distribution.PackageDescription
-                   ( TestSuiteInterface, BuildType )
+import           Distribution.PackageDescription ( BuildType )
 import           Distribution.System ( Platform (..) )
+import           Distribution.Types.MungedPackageName
+                   ( encodeCompatPackageName )
 import qualified RIO.Text as T
 import           Stack.Prelude
+import           Stack.Types.CompCollection ( CompCollection )
 import           Stack.Types.Compiler ( ActualCompiler )
+import           Stack.Types.Component
+                   ( StackBenchmark, StackBuildInfo, StackExecutable
+                   , StackForeignLibrary, StackLibrary, StackTestSuite
+                   , StackUnqualCompName
+                   )
+import           Stack.Types.ComponentUtils (toCabalName)
 import           Stack.Types.Dependency ( DepValue )
 import           Stack.Types.EnvConfig ( EnvConfig, HasEnvConfig (..) )
 import           Stack.Types.GhcPkgId ( GhcPkgId )
+import           Stack.Types.Installed
+                   ( InstallLocation (..), InstallMap, Installed (..)
+                   , InstalledLibraryInfo (..), InstalledMap
+                   , InstalledPackageLocation (..), PackageDatabase (..)
+                   , PackageDbVariety(..), simpleInstalledLib
+                   , toPackageDbVariety
+                   )
 import           Stack.Types.NamedComponent ( NamedComponent )
 import           Stack.Types.PackageFile
-                   ( GetPackageFiles (..), DotCabalDescriptor (..)
-                   , DotCabalPath (..)
+                   ( DotCabalDescriptor (..), DotCabalPath (..)
+                   , StackPackageFile
                    )
 import           Stack.Types.SourceMap ( CommonPackage, FromSnapshot )
-import           Stack.Types.Version ( VersionRange )
 
 -- | Type representing exceptions thrown by functions exported by the
 -- "Stack.Package" module.
@@ -75,7 +93,7 @@   | MismatchedCabalIdentifier !PackageIdentifierRevision !PackageIdentifier
   | CabalFileNameParseFail FilePath
   | CabalFileNameInvalidPackageName FilePath
-  | ComponentNotParsedBug
+  | ComponentNotParsedBug String
   deriving (Show, Typeable)
 
 instance Exception PackageException where
@@ -130,142 +148,110 @@       \extension, the following is invalid: "
     , fp
     ]
-  displayException ComponentNotParsedBug = bugReport "[S-4623]"
-    "Component names should always parse as directory names."
-
--- | Libraries in a package. Since Cabal 2.0, internal libraries are a
--- thing.
-data PackageLibraries
-  = NoLibraries
-  | HasLibraries !(Set Text)
-    -- ^ the foreign library names, sub libraries get built automatically
-    -- without explicit component name passing
- deriving (Show, Typeable)
+  displayException (ComponentNotParsedBug name) = bugReport "[S-4623]"
+    (  "Component names should always parse as directory names. The component \
+       \name without a directory is '"
+    <> name
+    <> "'."
+    )
 
 -- | Name of an executable.
 newtype ExeName
-  = ExeName { unExeName :: Text }
+  = ExeName { exeName :: Text }
   deriving (Data, Eq, Generic, Hashable, IsString, NFData, Ord, Show, Typeable)
 
 -- | Some package info.
 data Package = Package
-  { packageName :: !PackageName
+  { name :: !PackageName
     -- ^ Name of the package.
-  , packageVersion :: !Version
+  , version :: !Version
     -- ^ Version of the package
-  , packageLicense :: !(Either SPDX.License License)
+  , license :: !(Either SPDX.License License)
     -- ^ The license the package was released under.
-  , packageFiles :: !GetPackageFiles
-    -- ^ Get all files of the package.
-  , packageDeps :: !(Map PackageName DepValue)
-    -- ^ Packages that the package depends on, both as libraries and build tools.
-  , packageUnknownTools :: !(Set ExeName)
-    -- ^ Build tools specified in the legacy manner (build-tools:) that failed
-    -- the hard-coded lookup.
-  , packageAllDeps :: !(Set PackageName)
-    -- ^ Original dependencies (not sieved).
-  , packageSubLibDeps :: !(Map MungedPackageName DepValue)
-    -- ^ Original sub-library dependencies (not sieved).
-  , packageGhcOptions :: ![Text]
+  , ghcOptions :: ![Text]
     -- ^ Ghc options used on package.
-  , packageCabalConfigOpts :: ![Text]
+  , cabalConfigOpts :: ![Text]
     -- ^ Additional options passed to ./Setup.hs configure
-  , packageFlags :: !(Map FlagName Bool)
+  , flags :: !(Map FlagName Bool)
     -- ^ Flags used on package.
-  , packageDefaultFlags :: !(Map FlagName Bool)
+  , defaultFlags :: !(Map FlagName Bool)
     -- ^ Defaults for unspecified flags.
-  , packageLibraries :: !PackageLibraries
-    -- ^ does the package have a buildable library stanza?
-  , packageInternalLibraries :: !(Set Text)
-    -- ^ names of internal libraries
-  , packageTests :: !(Map Text TestSuiteInterface)
-    -- ^ names and interfaces of test suites
-  , packageBenchmarks :: !(Set Text)
-    -- ^ names of benchmarks
-  , packageExes :: !(Set Text)
-    -- ^ names of executables
-  , packageOpts :: !GetPackageOpts
-    -- ^ Args to pass to GHC.
-  , packageHasExposedModules :: !Bool
-    -- ^ Does the package have exposed modules?
-  , packageBuildType :: !BuildType
+  , library :: !(Maybe StackLibrary)
+    -- ^ Does the package have a buildable main library stanza?
+  , subLibraries :: !(CompCollection StackLibrary)
+    -- ^ The sub-libraries of the package.
+  , foreignLibraries :: !(CompCollection StackForeignLibrary)
+    -- ^ The foreign libraries of the package.
+  , testSuites :: !(CompCollection StackTestSuite)
+    -- ^ The test suites of the package.
+  , benchmarks :: !(CompCollection StackBenchmark)
+    -- ^ The benchmarks of the package.
+  , executables :: !(CompCollection StackExecutable)
+    -- ^ The executables of the package.
+  , buildType :: !BuildType
     -- ^ Package build-type.
-  , packageSetupDeps :: !(Maybe (Map PackageName VersionRange))
+  , setupDeps :: !(Maybe (Map PackageName DepValue))
     -- ^ If present: custom-setup dependencies
-  , packageCabalSpec :: !CabalSpecVersion
+  , cabalSpec :: !CabalSpecVersion
     -- ^ Cabal spec range
+  , file :: StackPackageFile
+    -- ^ The Cabal sourced files related to the package at the package level
+    -- The components may have file information in their own types
+  , testEnabled :: Bool
+    -- ^ This is a requirement because when tests are not enabled, Stack's
+    -- package dependencies should ignore test dependencies. Directly set from
+    -- 'packageConfigEnableTests'.
+  , benchmarkEnabled :: Bool
+    -- ^ This is a requirement because when benchmark are not enabled, Stack's
+    -- package dependencies should ignore benchmark dependencies. Directly set
+    -- from 'packageConfigEnableBenchmarks'.
   }
   deriving (Show, Typeable)
 
-packageIdent :: Package -> PackageIdentifier
-packageIdent p = PackageIdentifier (packageName p) (packageVersion p)
-
 packageIdentifier :: Package -> PackageIdentifier
-packageIdentifier pkg =
-  PackageIdentifier (packageName pkg) (packageVersion pkg)
+packageIdentifier p = PackageIdentifier p.name p.version
 
 packageDefinedFlags :: Package -> Set FlagName
-packageDefinedFlags = M.keysSet . packageDefaultFlags
-
-type InstallMap = Map PackageName (InstallLocation, Version)
-
--- | Files that the package depends on, relative to package directory.
--- Argument is the location of the Cabal file
-newtype GetPackageOpts = GetPackageOpts
-  { getPackageOpts :: forall env. HasEnvConfig env
-                   => InstallMap
-                   -> InstalledMap
-                   -> [PackageName]
-                   -> [PackageName]
-                   -> Path Abs File
-                   -> RIO env
-                        ( Map NamedComponent (Map ModuleName (Path Abs File))
-                        , Map NamedComponent [DotCabalPath]
-                        , Map NamedComponent BuildInfoOpts
-                        )
-  }
-
-instance Show GetPackageOpts where
-  show _ = "<GetPackageOpts>"
+packageDefinedFlags = M.keysSet . (.defaultFlags)
 
 -- | GHC options based on cabal information and ghc-options.
 data BuildInfoOpts = BuildInfoOpts
-  { bioOpts :: [String]
-  , bioOneWordOpts :: [String]
-  , bioPackageFlags :: [String]
+  { opts :: [String]
+  , oneWordOpts :: [String]
+  , packageFlags :: [String]
     -- ^ These options can safely have 'nubOrd' applied to them, as there are no
     -- multi-word options (see
     -- https://github.com/commercialhaskell/stack/issues/1255)
-  , bioCabalMacros :: Path Abs File
+  , cabalMacros :: Path Abs File
   }
   deriving Show
 
 -- | Package build configuration
 data PackageConfig = PackageConfig
-  { packageConfigEnableTests :: !Bool
+  { enableTests :: !Bool
     -- ^ Are tests enabled?
-  , packageConfigEnableBenchmarks :: !Bool
+  , enableBenchmarks :: !Bool
     -- ^ Are benchmarks enabled?
-  , packageConfigFlags :: !(Map FlagName Bool)
+  , flags :: !(Map FlagName Bool)
     -- ^ Configured flags.
-  , packageConfigGhcOptions :: ![Text]
+  , ghcOptions :: ![Text]
     -- ^ Configured ghc options.
-  , packageConfigCabalConfigOpts :: ![Text]
+  , cabalConfigOpts :: ![Text]
     -- ^ ./Setup.hs configure options
-  , packageConfigCompilerVersion :: ActualCompiler
+  , compilerVersion :: ActualCompiler
     -- ^ GHC version
-  , packageConfigPlatform :: !Platform
+  , platform :: !Platform
     -- ^ host platform
   }
  deriving (Show, Typeable)
 
 -- | Compares the package name.
 instance Ord Package where
-  compare = on compare packageName
+  compare = on compare (.name)
 
 -- | Compares the package name.
 instance Eq Package where
-  (==) = on (==) packageName
+  (==) = on (==) (.name)
 
 -- | Where the package's source is located: local directory or package index
 data PackageSource
@@ -285,41 +271,41 @@       , "<CommonPackage>"
       ]
 
-
 psVersion :: PackageSource -> Version
-psVersion (PSFilePath lp) = packageVersion $ lpPackage lp
+psVersion (PSFilePath lp) = lp.package.version
 psVersion (PSRemote _ v _ _) = v
 
--- | Information on a locally available package of source code
+-- | Information on a locally available package of source code.
 data LocalPackage = LocalPackage
-  { lpPackage       :: !Package
-     -- ^ The @Package@ info itself, after resolution with package flags,
-     -- with tests and benchmarks disabled
-  , lpComponents    :: !(Set NamedComponent)
+  { package       :: !Package
+     -- ^ The @Package@ info itself, after resolution with package flags, with
+     -- tests and benchmarks disabled
+  , components    :: !(Set NamedComponent)
     -- ^ Components to build, not including the library component.
-  , lpUnbuildable   :: !(Set NamedComponent)
+  , unbuildable   :: !(Set NamedComponent)
     -- ^ Components explicitly requested for build, that are marked
     -- "buildable: false".
-  , lpWanted        :: !Bool -- FIXME Should completely drop this "wanted"
+  , wanted        :: !Bool -- FIXME Should completely drop this "wanted"
                              -- terminology, it's unclear
     -- ^ Whether this package is wanted as a target.
-  , lpTestBench     :: !(Maybe Package)
-    -- ^ This stores the 'Package' with tests and benchmarks enabled, if
-    -- either is asked for by the user.
-  , lpCabalFile     :: !(Path Abs File)
-    -- ^ The Cabal file
-  , lpBuildHaddocks :: !Bool
-  , lpForceDirty    :: !Bool
-  , lpDirtyFiles    :: !(MemoizedWith EnvConfig (Maybe (Set FilePath)))
+  , testBench     :: !(Maybe Package)
+    -- ^ This stores the 'Package' with tests and benchmarks enabled, if either
+    -- is asked for by the user.
+  , cabalFP     :: !(Path Abs File)
+    -- ^ Absolute path to the Cabal file.
+  , buildHaddocks :: !Bool
+    -- ^ Is Haddock documentation being built for this package?
+  , forceDirty    :: !Bool
+  , dirtyFiles    :: !(MemoizedWith EnvConfig (Maybe (Set FilePath)))
     -- ^ Nothing == not dirty, Just == dirty. Note that the Set may be empty if
     -- we forced the build to treat packages as dirty. Also, the Set may not
     -- include all modified files.
-  , lpNewBuildCaches :: !( MemoizedWith
+  , newBuildCaches :: !( MemoizedWith
                              EnvConfig
                              (Map NamedComponent (Map FilePath FileCacheInfo))
                          )
     -- ^ current state of the files
-  , lpComponentFiles :: !( MemoizedWith
+  , componentFiles :: !( MemoizedWith
                              EnvConfig
                              (Map NamedComponent (Set (Path Abs File)))
                          )
@@ -328,7 +314,7 @@   deriving Show
 
 newtype MemoizedWith env a
-  = MemoizedWith { unMemoizedWith :: RIO env a }
+  = MemoizedWith { memoizedWith :: RIO env a }
   deriving (Applicative, Functor, Monad)
 
 memoizeRefWith :: MonadIO m => RIO env a -> m (MemoizedWith env a)
@@ -357,37 +343,18 @@   show _ = "<<MemoizedWith>>"
 
 lpFiles :: HasEnvConfig env => LocalPackage -> RIO env (Set.Set (Path Abs File))
-lpFiles = runMemoizedWith . fmap (Set.unions . M.elems) . lpComponentFiles
+lpFiles = runMemoizedWith . fmap (Set.unions . M.elems) . (.componentFiles)
 
 lpFilesForComponents :: HasEnvConfig env
                      => Set NamedComponent
                      -> LocalPackage
                      -> RIO env (Set.Set (Path Abs File))
 lpFilesForComponents components lp = runMemoizedWith $ do
-  componentFiles <- lpComponentFiles lp
+  componentFiles <- lp.componentFiles
   pure $ mconcat (M.elems (M.restrictKeys componentFiles components))
 
--- | A location to install a package into, either snapshot or local
-data InstallLocation
-  = Snap
-  | Local
-  deriving (Eq, Show)
-
-instance Semigroup InstallLocation where
-  Local <> _ = Local
-  _ <> Local = Local
-  Snap <> Snap = Snap
-
-instance Monoid InstallLocation where
-  mempty = Snap
-  mappend = (<>)
-
-data InstalledPackageLocation
-  = InstalledTo InstallLocation | ExtraGlobal
-  deriving (Eq, Show)
-
 newtype FileCacheInfo = FileCacheInfo
-  { fciHash :: SHA256
+  { hash :: SHA256
   }
   deriving (Eq, Generic, Show, Typeable)
 
@@ -437,19 +404,48 @@     DotCabalFilePath fp -> fp
     DotCabalCFilePath fp -> fp
 
-type InstalledMap = Map PackageName (InstallLocation, Installed)
+-- | Gathers all the GhcPkgId provided by a library into a map
+installedMapGhcPkgId ::
+     PackageIdentifier
+  -> InstalledLibraryInfo
+  -> Map PackageIdentifier GhcPkgId
+installedMapGhcPkgId pkgId@(PackageIdentifier pkgName version) installedLib =
+  finalMap
+ where
+  finalMap = M.insert pkgId installedLib.ghcPkgId baseMap
+  baseMap =
+    M.mapKeysMonotonic
+      (toCabalMungedPackageIdentifier pkgName version)
+      installedLib.subLib
 
-data Installed
-  = Library PackageIdentifier GhcPkgId (Maybe (Either SPDX.License License))
-  | Executable PackageIdentifier
-  deriving (Eq, Show)
+-- | Creates a 'MungedPackageName' identifier.
+toCabalMungedPackageIdentifier ::
+     PackageName
+  -> Version
+  -> StackUnqualCompName
+  -> PackageIdentifier
+toCabalMungedPackageIdentifier pkgName version = flip PackageIdentifier version
+  . encodeCompatPackageName . toCabalMungedPackageName pkgName
 
-installedPackageIdentifier :: Installed -> PackageIdentifier
-installedPackageIdentifier (Library pid _ _) = pid
-installedPackageIdentifier (Executable pid) = pid
+toCabalMungedPackageName ::
+     PackageName
+  -> StackUnqualCompName
+  -> MungedPackageName
+toCabalMungedPackageName pkgName =
+  MungedPackageName pkgName . LSubLibName . toCabalName
 
--- | Get the installed Version.
-installedVersion :: Installed -> Version
-installedVersion i =
-  let PackageIdentifier _ version = installedPackageIdentifier i
-  in  version
+-- | Type representing inputs to 'Stack.Package.generateBuildInfoOpts'.
+data BioInput = BioInput
+  { installMap :: !InstallMap
+  , installedMap :: !InstalledMap
+  , cabalDir :: !(Path Abs Dir)
+  , distDir :: !(Path Abs Dir)
+  , omitPackages :: ![PackageName]
+  , addPackages :: ![PackageName]
+  , buildInfo :: !StackBuildInfo
+  , dotCabalPaths :: ![DotCabalPath]
+  , configLibDirs :: ![FilePath]
+  , configIncludeDirs :: ![FilePath]
+  , componentName :: !NamedComponent
+  , cabalVersion :: !Version
+  }
src/Stack/Types/PackageFile.hs view
@@ -1,5 +1,7 @@-{-# LANGUAGE NoImplicitPrelude          #-}
-{-# LANGUAGE DataKinds                  #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE DataKinds           #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 
 -- | The facility for retrieving all files from the main Stack
 -- 'Stack.Types.Package' type. This was moved into its own module to allow
@@ -9,8 +11,9 @@   ( GetPackageFileContext (..)
   , DotCabalPath (..)
   , DotCabalDescriptor (..)
-  , GetPackageFiles (..)
   , PackageWarning (..)
+  , StackPackageFile (..)
+  , PackageComponentFile (..)
   ) where
 
 import           Distribution.ModuleName ( ModuleName )
@@ -19,54 +22,53 @@ import           Stack.Types.BuildConfig
                    ( BuildConfig (..), HasBuildConfig (..) )
 import           Stack.Types.Config ( HasConfig (..) )
-import           Stack.Types.EnvConfig ( HasEnvConfig )
 import           Stack.Types.GHCVariant ( HasGHCVariant (..) )
 import           Stack.Types.NamedComponent ( NamedComponent )
 import           Stack.Types.Platform ( HasPlatform (..) )
 import           Stack.Types.Runner ( HasRunner (..) )
 
 data GetPackageFileContext = GetPackageFileContext
-  { ctxFile :: !(Path Abs File)
-  , ctxDistDir :: !(Path Abs Dir)
-  , ctxBuildConfig :: !BuildConfig
-  , ctxCabalVer :: !Version
+  { file :: !(Path Abs File)
+  , distDir :: !(Path Abs Dir)
+  , buildConfig :: !BuildConfig
+  , cabalVer :: !Version
   }
 
 instance HasPlatform GetPackageFileContext where
-  platformL = configL.platformL
+  platformL = configL . platformL
   {-# INLINE platformL #-}
-  platformVariantL = configL.platformVariantL
+  platformVariantL = configL . platformVariantL
   {-# INLINE platformVariantL #-}
 
 instance HasGHCVariant GetPackageFileContext where
-  ghcVariantL = configL.ghcVariantL
+  ghcVariantL = configL . ghcVariantL
   {-# INLINE ghcVariantL #-}
 
 instance HasLogFunc GetPackageFileContext where
-  logFuncL = configL.logFuncL
+  logFuncL = configL . logFuncL
 
 instance HasRunner GetPackageFileContext where
-  runnerL = configL.runnerL
+  runnerL = configL . runnerL
 
 instance HasStylesUpdate GetPackageFileContext where
-  stylesUpdateL = runnerL.stylesUpdateL
+  stylesUpdateL = runnerL . stylesUpdateL
 
 instance HasTerm GetPackageFileContext where
-  useColorL = runnerL.useColorL
-  termWidthL = runnerL.termWidthL
+  useColorL = runnerL . useColorL
+  termWidthL = runnerL . termWidthL
 
 instance HasConfig GetPackageFileContext where
-  configL = buildConfigL.lens bcConfig (\x y -> x { bcConfig = y })
+  configL = buildConfigL . lens (.config) (\x y -> x { config = y })
   {-# INLINE configL #-}
 
 instance HasBuildConfig GetPackageFileContext where
-  buildConfigL = lens ctxBuildConfig (\x y -> x { ctxBuildConfig = y })
+  buildConfigL = lens (.buildConfig) (\x y -> x { buildConfig = y })
 
 instance HasPantryConfig GetPackageFileContext where
-  pantryConfigL = configL.pantryConfigL
+  pantryConfigL = configL . pantryConfigL
 
 instance HasProcessContext GetPackageFileContext where
-  processContextL = configL.processContextL
+  processContextL = configL . processContextL
 
 -- | A path resolved from the Cabal file, which is either main-is or
 -- an exposed/internal/referenced module.
@@ -91,21 +93,6 @@   | DotCabalCFile !FilePath
   deriving (Eq, Ord, Show)
 
--- | Files that the package depends on, relative to package directory.
--- Argument is the location of the Cabal file
-newtype GetPackageFiles = GetPackageFiles
-  { getPackageFiles :: forall env. HasEnvConfig env
-                    => Path Abs File
-                    -> RIO env
-                         ( Map NamedComponent (Map ModuleName (Path Abs File))
-                         , Map NamedComponent [DotCabalPath]
-                         , Set (Path Abs File)
-                         , [PackageWarning]
-                         )
-  }
-instance Show GetPackageFiles where
-  show _ = "<GetPackageFiles>"
-
 -- | Warning generated when reading a package
 data PackageWarning
   = UnlistedModulesWarning NamedComponent [ModuleName]
@@ -116,3 +103,27 @@   | MissingModulesWarning (Path Abs File) (Maybe String) [ModuleName]
     -- ^ Modules not found in file system, which are listed in Cabal file
   -}
+
+-- | This is the information from Cabal we need at the package level to track
+-- files.
+data StackPackageFile = StackPackageFile
+  { extraSrcFiles :: [FilePath]
+  , dataDir :: FilePath
+  , dataFiles :: [FilePath]
+  }
+  deriving (Show, Typeable)
+
+-- | Files that the package depends on, relative to package directory.
+data PackageComponentFile = PackageComponentFile
+  { modulePathMap :: Map NamedComponent (Map ModuleName (Path Abs File))
+  , cabalFileMap :: !(Map NamedComponent [DotCabalPath])
+  , packageExtraFile :: Set (Path Abs File)
+  , warnings :: [PackageWarning]
+  }
+
+instance Semigroup PackageComponentFile where
+  PackageComponentFile x1 x2 x3 x4 <> PackageComponentFile y1 y2 y3 y4 =
+    PackageComponentFile (x1 <> y1) (x2 <> y2) (x3 <> y3) (x4 <> y4)
+
+instance Monoid PackageComponentFile where
+  mempty = PackageComponentFile mempty mempty mempty mempty
src/Stack/Types/PackageName.hs view
@@ -22,9 +22,15 @@     case parsePackageName s of
       Just x -> Right x
       Nothing -> Left $ unlines
-        [ "Expected valid package name, but got: " ++ s
-        , "Package names consist of one or more alphanumeric words separated \
-          \by hyphens."
-        , "To avoid ambiguity with version numbers, each of these words must \
-          \contain at least one letter."
+        [ "Expected a package name acceptable to Cabal, but got: " ++ s ++ "\n"
+        , "An acceptable package name comprises an alphanumeric 'word'; or \
+          \two or more"
+        , "such words, with the words separated by a hyphen/minus character ('-'). A \
+          \word"
+        , "cannot be comprised only of the characters '0' to '9'. \n"
+        , "An alphanumeric character is one in one of the Unicode Letter \
+          \categories"
+        , "(Lu (uppercase), Ll (lowercase), Lt (titlecase), Lm (modifier), or \
+          \Lo (other))"
+        , "or Number categories (Nd (decimal), Nl (letter), or No (other))."
         ]
src/Stack/Types/ParentMap.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE NoImplicitPrelude #-}
 
+-- | Module exporting the 'ParentMap' type synonym.
 module Stack.Types.ParentMap
   ( ParentMap
   ) where
@@ -8,5 +9,8 @@ import           Stack.Prelude
 import           Stack.Types.Version ( VersionRange )
 
+-- | Type synonym representing dictionaries of package names, and a list of
+-- pairs of the identifier of a package depending on the package and the
+-- version range specified for the dependency by that package.
 type ParentMap =
-  MonoidMap PackageName (First Version, [(PackageIdentifier, VersionRange)])
+  MonoidMap PackageName [(PackageIdentifier, VersionRange)]
src/Stack/Types/Project.hs view
@@ -1,5 +1,7 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 module Stack.Types.Project
   ( Project (..)
@@ -14,43 +16,47 @@ -- | A project is a collection of packages. We can have multiple stack.yaml
 -- files, but only one of them may contain project information.
 data Project = Project
-  { projectUserMsg :: !(Maybe String)
+  { userMsg :: !(Maybe String)
     -- ^ A warning message to display to the user when the auto generated
     -- config may have issues.
-  , projectPackages :: ![RelFilePath]
+  , packages :: ![RelFilePath]
     -- ^ Packages which are actually part of the project (as opposed
     -- to dependencies).
-  , projectDependencies :: ![RawPackageLocation]
+  , extraDeps :: ![RawPackageLocation]
     -- ^ Dependencies defined within the stack.yaml file, to be applied on top
     -- of the snapshot.
-  , projectFlags :: !(Map PackageName (Map FlagName Bool))
+  , flagsByPkg :: !(Map PackageName (Map FlagName Bool))
     -- ^ Flags to be applied on top of the snapshot flags.
-  , projectResolver :: !RawSnapshotLocation
+  , resolver :: !RawSnapshotLocation
     -- ^ How we resolve which @Snapshot@ to use
-  , projectCompiler :: !(Maybe WantedCompiler)
+  , compiler :: !(Maybe WantedCompiler)
     -- ^ Override the compiler in 'projectResolver'
-  , projectExtraPackageDBs :: ![FilePath]
-  , projectCurator :: !(Maybe Curator)
+  , extraPackageDBs :: ![FilePath]
+  , curator :: !(Maybe Curator)
     -- ^ Extra configuration intended exclusively for usage by the curator tool.
     -- In other words, this is /not/ part of the documented and exposed Stack
     -- API. SUBJECT TO CHANGE.
-  , projectDropPackages :: !(Set PackageName)
+  , dropPackages :: !(Set PackageName)
     -- ^ Packages to drop from the 'projectResolver'.
   }
   deriving Show
 
 instance ToJSON Project where
   -- Expanding the constructor fully to ensure we don't miss any fields.
-  toJSON (Project userMsg packages extraDeps flags resolver mcompiler extraPackageDBs mcurator drops) = object $ concat
-    [ maybe [] (\cv -> ["compiler" .= cv]) mcompiler
-    , maybe [] (\msg -> ["user-message" .= msg]) userMsg
-    , [ "extra-package-dbs" .= extraPackageDBs | not (null extraPackageDBs) ]
-    , [ "extra-deps" .= extraDeps | not (null extraDeps) ]
-    , [ "flags" .= fmap toCabalStringMap (toCabalStringMap flags)
-      | not (Map.null flags)
+  toJSON project = object $ concat
+    [ maybe [] (\cv -> ["compiler" .= cv]) project.compiler
+    , maybe [] (\msg -> ["user-message" .= msg]) project.userMsg
+    , [ "extra-package-dbs" .= project.extraPackageDBs
+      | not (null project.extraPackageDBs)
       ]
-    , ["packages" .= packages]
-    , ["resolver" .= resolver]
-    , maybe [] (\c -> ["curator" .= c]) mcurator
-    , [ "drop-packages" .= Set.map CabalString drops | not (Set.null drops) ]
+    , [ "extra-deps" .= project.extraDeps | not (null project.extraDeps) ]
+    , [ "flags" .= fmap toCabalStringMap (toCabalStringMap project.flagsByPkg)
+      | not (Map.null project.flagsByPkg)
+      ]
+    , ["packages" .= project.packages]
+    , ["resolver" .= project.resolver]
+    , maybe [] (\c -> ["curator" .= c]) project.curator
+    , [ "drop-packages" .= Set.map CabalString project.dropPackages
+      | not (Set.null project.dropPackages)
+      ]
     ]
src/Stack/Types/ProjectAndConfigMonoid.hs view
@@ -30,29 +30,31 @@     packages <- o ..:? "packages" ..!= [RelFilePath "."]
     deps <- jsonSubWarningsTT (o ..:? "extra-deps") ..!= []
     flags' <- o ..:? "flags" ..!= mempty
-    let flags = unCabalStringMap <$> unCabalStringMap
+    let flagsByPkg = unCabalStringMap <$> unCabalStringMap
                 (flags' :: Map (CabalString PackageName) (Map (CabalString FlagName) Bool))
 
-    resolver <- jsonSubWarnings $ o ...: ["snapshot", "resolver"]
-    mcompiler <- o ..:? "compiler"
-    msg <- o ..:? "user-message"
+    resolver' <- jsonSubWarnings $ o ...: ["snapshot", "resolver"]
+    compiler <- o ..:? "compiler"
+    userMsg <- o ..:? "user-message"
     config <- parseConfigMonoidObject rootDir o
     extraPackageDBs <- o ..:? "extra-package-dbs" ..!= []
-    mcurator <- jsonSubWarningsT (o ..:? "curator")
+    curator <- jsonSubWarningsT (o ..:? "curator")
     drops <- o ..:? "drop-packages" ..!= mempty
+    let dropPackages = Set.map unCabalString drops
     pure $ do
       deps' <- mapM (resolvePaths (Just rootDir)) deps
-      resolver' <- resolvePaths (Just rootDir) resolver
+      let extraDeps =
+            concatMap toList (deps' :: [NonEmpty RawPackageLocation])
+      resolver <- resolvePaths (Just rootDir) resolver'
       let project = Project
-            { projectUserMsg = msg
-            , projectResolver = resolver'
-            , projectCompiler = mcompiler -- FIXME make sure resolver' isn't SLCompiler
-            , projectExtraPackageDBs = extraPackageDBs
-            , projectPackages = packages
-            , projectDependencies =
-                concatMap toList (deps' :: [NonEmpty RawPackageLocation])
-            , projectFlags = flags
-            , projectCurator = mcurator
-            , projectDropPackages = Set.map unCabalString drops
+            { userMsg
+            , resolver
+            , compiler -- FIXME make sure resolver' isn't SLCompiler
+            , extraPackageDBs
+            , packages
+            , extraDeps
+            , flagsByPkg
+            , curator
+            , dropPackages
             }
       pure $ ProjectAndConfigMonoid project config
src/Stack/Types/Resolver.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE NoImplicitPrelude    #-}
 {-# LANGUAGE DataKinds            #-}
 {-# LANGUAGE GADTs                #-}
+{-# LANGUAGE NoFieldSelectors     #-}
 {-# LANGUAGE OverloadedStrings    #-}
 {-# LANGUAGE TypeFamilies         #-}
 {-# LANGUAGE UndecidableInstances #-}
@@ -79,8 +80,8 @@ 
 -- | Most recent Nightly and newest LTS version per major release.
 data Snapshots = Snapshots
-  { snapshotsNightly :: !Day
-  , snapshotsLts     :: !(IntMap Int)
+  { nightly :: !Day
+  , lts     :: !(IntMap Int)
   }
   deriving Show
 
src/Stack/Types/Runner.hs view
@@ -1,8 +1,12 @@-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 
 module Stack.Types.Runner
   ( Runner (..)
   , HasRunner (..)
+  , HasDockerEntrypointMVar (..)
   , globalOptsL
   , stackYamlLocL
   , lockFileBehaviorL
@@ -12,60 +16,74 @@   ) where
 
 import           RIO.Process ( HasProcessContext (..), ProcessContext )
-import           Stack.Prelude
+import           Stack.Prelude hiding ( stylesUpdate )
 import           Stack.Types.GlobalOpts ( GlobalOpts (..) )
 import           Stack.Types.LockFileBehavior ( LockFileBehavior )
 import           Stack.Types.StackYamlLoc ( StackYamlLoc )
 
--- | The base environment that almost everything in Stack runs in,
--- based off of parsing command line options in 'GlobalOpts'. Provides
--- logging and process execution.
+-- | The base environment that almost everything in Stack runs in, based off of
+-- parsing command line options in 'GlobalOpts'. Provides logging, process
+-- execution, and the MVar used to ensure that the Docker entrypoint is
+-- performed exactly once.
 data Runner = Runner
-  { runnerGlobalOpts :: !GlobalOpts
-  , runnerUseColor   :: !Bool
-  , runnerLogFunc    :: !LogFunc
-  , runnerTermWidth  :: !Int
-  , runnerProcessContext :: !ProcessContext
+  { globalOpts           :: !GlobalOpts
+  , useColor             :: !Bool
+  , logFunc              :: !LogFunc
+  , termWidth            :: !Int
+  , processContext       :: !ProcessContext
+  , dockerEntrypointMVar :: !(MVar Bool)
   }
 
 instance HasLogFunc Runner where
-  logFuncL = lens runnerLogFunc (\x y -> x { runnerLogFunc = y })
+  logFuncL = lens (.logFunc) (\x y -> x { logFunc = y })
 
 instance HasProcessContext Runner where
   processContextL =
-    lens runnerProcessContext (\x y -> x { runnerProcessContext = y })
+    lens (.processContext) (\x y -> x { processContext = y })
 
 instance HasRunner Runner where
   runnerL = id
 
 instance HasStylesUpdate Runner where
-  stylesUpdateL = globalOptsL.
-                  lens globalStylesUpdate (\x y -> x { globalStylesUpdate = y })
+  stylesUpdateL :: Lens' Runner StylesUpdate
+  stylesUpdateL = globalOptsL . lens
+    (.stylesUpdate)
+    (\x y -> x { stylesUpdate = y })
+
 instance HasTerm Runner where
-  useColorL = lens runnerUseColor (\x y -> x { runnerUseColor = y })
-  termWidthL = lens runnerTermWidth (\x y -> x { runnerTermWidth = y })
+  useColorL = lens (.useColor) (\x y -> x { useColor = y })
+  termWidthL = lens (.termWidth) (\x y -> x { termWidth  = y })
 
+instance HasDockerEntrypointMVar Runner where
+  dockerEntrypointMVarL =
+    lens (.dockerEntrypointMVar) (\x y -> x { dockerEntrypointMVar = y })
+
 -- | Class for environment values which have a 'Runner'.
 class (HasProcessContext env, HasLogFunc env) => HasRunner env where
   runnerL :: Lens' env Runner
 
+-- | Class for environment values which have a Docker entrypoint 'MVar'.
+class HasRunner env => HasDockerEntrypointMVar env where
+  dockerEntrypointMVarL :: Lens' env (MVar Bool)
+
 stackYamlLocL :: HasRunner env => Lens' env StackYamlLoc
 stackYamlLocL =
-  globalOptsL.lens globalStackYaml (\x y -> x { globalStackYaml = y })
+  globalOptsL . lens (.stackYaml) (\x y -> x { stackYaml = y })
 
 lockFileBehaviorL :: HasRunner env => SimpleGetter env LockFileBehavior
-lockFileBehaviorL = globalOptsL.to globalLockFileBehavior
+lockFileBehaviorL = globalOptsL . to (.lockFileBehavior)
 
 globalOptsL :: HasRunner env => Lens' env GlobalOpts
-globalOptsL = runnerL.lens runnerGlobalOpts (\x y -> x { runnerGlobalOpts = y })
+globalOptsL = runnerL . lens (.globalOpts) (\x y -> x { globalOpts = y })
 
 -- | See 'globalTerminal'
 terminalL :: HasRunner env => Lens' env Bool
-terminalL = globalOptsL.lens globalTerminal (\x y -> x { globalTerminal = y })
+terminalL =
+  globalOptsL . lens (.terminal) (\x y -> x { terminal = y })
 
 -- | See 'globalReExecVersion'
 reExecL :: HasRunner env => SimpleGetter env Bool
-reExecL = globalOptsL.to (isJust . globalReExecVersion)
+reExecL = globalOptsL . to (isJust . (.reExecVersion))
 
 rslInLogL :: HasRunner env => SimpleGetter env Bool
-rslInLogL = globalOptsL.to globalRSLInLog
+rslInLogL = globalOptsL . to (.rslInLog)
src/Stack/Types/SetupInfo.hs view
@@ -1,7 +1,8 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
-{-# LANGUAGE ViewPatterns      #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE ViewPatterns        #-}
 
 
 module Stack.Types.SetupInfo
@@ -20,43 +21,50 @@ import           Stack.Types.GHCDownloadInfo ( GHCDownloadInfo )
 
 data SetupInfo = SetupInfo
-  { siSevenzExe :: Maybe DownloadInfo
-  , siSevenzDll :: Maybe DownloadInfo
-  , siMsys2 :: Map Text VersionedDownloadInfo
-  , siGHCs :: Map Text (Map Version GHCDownloadInfo)
-  , siStack :: Map Text (Map Version DownloadInfo)
+  { sevenzExe :: Maybe DownloadInfo
+  , sevenzDll :: Maybe DownloadInfo
+  , msys2 :: Map Text VersionedDownloadInfo
+  , ghcByVersion :: Map Text (Map Version GHCDownloadInfo)
+  , stackByVersion :: Map Text (Map Version DownloadInfo)
   }
   deriving Show
 
 instance FromJSON (WithJSONWarnings SetupInfo) where
   parseJSON = withObjectWarnings "SetupInfo" $ \o -> do
-    siSevenzExe <- jsonSubWarningsT (o ..:? "sevenzexe-info")
-    siSevenzDll <- jsonSubWarningsT (o ..:? "sevenzdll-info")
-    siMsys2 <- jsonSubWarningsT (o ..:? "msys2" ..!= mempty)
-    (fmap unCabalStringMap -> siGHCs) <-
+    sevenzExe <- jsonSubWarningsT (o ..:? "sevenzexe-info")
+    sevenzDll <- jsonSubWarningsT (o ..:? "sevenzdll-info")
+    msys2 <- jsonSubWarningsT (o ..:? "msys2" ..!= mempty)
+    (fmap unCabalStringMap -> ghcByVersion) <-
       jsonSubWarningsTT (o ..:? "ghc" ..!= mempty)
-    (fmap unCabalStringMap -> siStack) <-
+    (fmap unCabalStringMap -> stackByVersion) <-
       jsonSubWarningsTT (o ..:? "stack" ..!= mempty)
-    pure SetupInfo {..}
+    pure SetupInfo
+      { sevenzExe
+      , sevenzDll
+      , msys2
+      , ghcByVersion
+      , stackByVersion
+      }
 
 -- | For the @siGHCs@ field maps are deeply merged. For all fields the values
 -- from the first @SetupInfo@ win.
 instance Semigroup SetupInfo where
   l <> r =
     SetupInfo
-    { siSevenzExe = siSevenzExe l <|> siSevenzExe r
-    , siSevenzDll = siSevenzDll l <|> siSevenzDll r
-    , siMsys2 = siMsys2 l <> siMsys2 r
-    , siGHCs = Map.unionWith (<>) (siGHCs l) (siGHCs r)
-    , siStack = Map.unionWith (<>) (siStack l) (siStack r) }
+      { sevenzExe = l.sevenzExe <|> r.sevenzExe
+      , sevenzDll = l.sevenzDll <|> r.sevenzDll
+      , msys2 = l.msys2 <> r.msys2
+      , ghcByVersion = Map.unionWith (<>) l.ghcByVersion r.ghcByVersion
+      , stackByVersion = Map.unionWith (<>) l.stackByVersion r.stackByVersion
+      }
 
 instance Monoid SetupInfo where
   mempty =
     SetupInfo
-    { siSevenzExe = Nothing
-    , siSevenzDll = Nothing
-    , siMsys2 = Map.empty
-    , siGHCs = Map.empty
-    , siStack = Map.empty
-    }
+      { sevenzExe = Nothing
+      , sevenzDll = Nothing
+      , msys2 = Map.empty
+      , ghcByVersion = Map.empty
+      , stackByVersion = Map.empty
+      }
   mappend = (<>)
src/Stack/Types/SourceMap.hs view
@@ -1,4 +1,7 @@-{-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 
 -- | A sourcemap maps a package name to how it should be built, including source
 -- code, flags, options, etc. This module contains various stages of source map
@@ -38,16 +41,18 @@ import           Stack.Types.Compiler ( ActualCompiler )
 import           Stack.Types.NamedComponent ( NamedComponent (..) )
 
--- | Common settings for both dependency and project package.
+-- | Settings common to dependency packages ('Stack.Types.SourceMap.DepPackage')
+-- and project packages ('Stack.Types.SourceMap.ProjectPackage').
 data CommonPackage = CommonPackage
-  { cpGPD :: !(IO GenericPackageDescription)
-  , cpName :: !PackageName
-  , cpFlags :: !(Map FlagName Bool)
+  { gpd :: !(IO GenericPackageDescription)
+  , name :: !PackageName
+  , flags :: !(Map FlagName Bool)
     -- ^ overrides default flags
-  , cpGhcOptions :: ![Text]
+  , ghcOptions :: ![Text]
     -- also lets us know if we're doing profiling
-  , cpCabalConfigOpts :: ![Text]
-  , cpHaddocks :: !Bool
+  , cabalConfigOpts :: ![Text]
+  , buildHaddocks :: !Bool
+    -- ^ Should Haddock documentation be built for this package?
   }
 
 -- | Flag showing if package comes from a snapshot needed to ignore dependency
@@ -59,21 +64,21 @@ 
 -- | A view of a dependency package, specified in stack.yaml
 data DepPackage = DepPackage
-  { dpCommon :: !CommonPackage
-  , dpLocation :: !PackageLocation
-  , dpHidden :: !Bool
+  { depCommon :: !CommonPackage
+  , location :: !PackageLocation
+  , hidden :: !Bool
     -- ^ Should the package be hidden after registering? Affects the script
     -- interpreter's module name import parser.
-  , dpFromSnapshot :: !FromSnapshot
+  , fromSnapshot :: !FromSnapshot
     -- ^ Needed to ignore bounds between snapshot packages
     -- See https://github.com/commercialhaskell/stackage/issues/3185
   }
 
 -- | A view of a project package needed for resolving components
 data ProjectPackage = ProjectPackage
-  { ppCommon :: !CommonPackage
-  , ppCabalFP    :: !(Path Abs File)
-  , ppResolvedDir :: !(ResolvedPath Dir)
+  { projectCommon :: !CommonPackage
+  , cabalFP :: !(Path Abs File)
+  , resolvedDir :: !(ResolvedPath Dir)
   }
 
 -- | A view of a package installed in the global package database also could
@@ -97,10 +102,10 @@ -- Invariant: a @PackageName@ appears in either 'smwProject' or 'smwDeps', but
 -- not both.
 data SMWanted = SMWanted
-  { smwCompiler :: !WantedCompiler
-  , smwProject :: !(Map PackageName ProjectPackage)
-  , smwDeps :: !(Map PackageName DepPackage)
-  , smwSnapshotLocation :: !RawSnapshotLocation
+  { compiler :: !WantedCompiler
+  , project :: !(Map PackageName ProjectPackage)
+  , deps :: !(Map PackageName DepPackage)
+  , snapshotLocation :: !RawSnapshotLocation
     -- ^ Where this snapshot is loaded from.
   }
 
@@ -109,10 +114,10 @@ --
 -- Invariant: a @PackageName@ appears in only one of the @Map@s.
 data SMActual global = SMActual
-  { smaCompiler :: !ActualCompiler
-  , smaProject :: !(Map PackageName ProjectPackage)
-  , smaDeps :: !(Map PackageName DepPackage)
-  , smaGlobal :: !(Map PackageName global)
+  { compiler :: !ActualCompiler
+  , project :: !(Map PackageName ProjectPackage)
+  , deps :: !(Map PackageName DepPackage)
+  , globals :: !(Map PackageName global)
   }
 
 newtype GlobalPackageVersion
@@ -131,27 +136,27 @@ -- | Builds on an 'SMActual' by resolving the targets specified on the command
 -- line, potentially adding in new dependency packages in the process.
 data SMTargets = SMTargets
-  { smtTargets :: !(Map PackageName Target)
-  , smtDeps :: !(Map PackageName DepPackage)
+  { targets :: !(Map PackageName Target)
+  , deps :: !(Map PackageName DepPackage)
   }
 
 -- | The final source map, taking an 'SMTargets' and applying all command line
 -- flags and GHC options.
 data SourceMap = SourceMap
-  { smTargets :: !SMTargets
+  { targets :: !SMTargets
     -- ^ Doesn't need to be included in the hash, does not affect the source
     -- map.
-  , smCompiler :: !ActualCompiler
+  , compiler :: !ActualCompiler
     -- ^ Need to hash the compiler version _and_ its installation path. Ideally
     -- there would be some kind of output from GHC telling us some unique ID for
     -- the compiler itself.
-  , smProject :: !(Map PackageName ProjectPackage)
+  , project :: !(Map PackageName ProjectPackage)
     -- ^ Doesn't need to be included in hash, doesn't affect any of the packages
     -- that get stored in the snapshot database.
-  , smDeps :: !(Map PackageName DepPackage)
+  , deps :: !(Map PackageName DepPackage)
     -- ^ Need to hash all of the immutable dependencies, can ignore the mutable
     -- dependencies.
-  , smGlobal :: !(Map PackageName GlobalPackage)
+  , globalPkgs :: !(Map PackageName GlobalPackage)
     -- ^ Doesn't actually need to be hashed, implicitly captured by smCompiler.
     -- Can be broken if someone installs new global packages. We can document
     -- that as not supported, _or_ we could actually include all of this in the
@@ -167,11 +172,11 @@ smRelDir (SourceMapHash smh) = parseRelDir $ T.unpack $ SHA256.toHexText smh
 
 ppGPD :: MonadIO m => ProjectPackage -> m GenericPackageDescription
-ppGPD = liftIO . cpGPD . ppCommon
+ppGPD = liftIO . (.projectCommon.gpd)
 
 -- | Root directory for the given 'ProjectPackage'
 ppRoot :: ProjectPackage -> Path Abs Dir
-ppRoot = parent . ppCabalFP
+ppRoot = parent . (.cabalFP)
 
 -- | All components available in the given 'ProjectPackage'
 ppComponents :: MonadIO m => ProjectPackage -> m (Set NamedComponent)
src/Stack/Types/Storage.hs view
@@ -59,10 +59,10 @@ 
 -- | A bit of type safety to ensure we're talking to the right database.
 newtype UserStorage = UserStorage
-  { unUserStorage :: Storage
+  { userStorage :: Storage
   }
 
 -- | A bit of type safety to ensure we're talking to the right database.
 newtype ProjectStorage = ProjectStorage
-  { unProjectStorage :: Storage
+  { projectStorage :: Storage
   }
src/Stack/Types/TemplateName.hs view
@@ -54,9 +54,9 @@ 
 -- | Details for how to access a template from a remote repo.
 data RepoTemplatePath = RepoTemplatePath
-  { rtpService  :: RepoService
-  , rtpUser     :: Text
-  , rtpTemplate :: Text
+  { service  :: RepoService
+  , user     :: Text
+  , template :: Text
   }
   deriving (Eq, Ord, Show)
 
src/Stack/Types/Version.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoFieldSelectors  #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE ViewPatterns      #-}
 
@@ -34,7 +35,7 @@ import           Text.PrettyPrint ( render )
 
 newtype IntersectingVersionRange = IntersectingVersionRange
-  { getIntersectingVersionRange :: Cabal.VersionRange }
+  { intersectingVersionRange :: Cabal.VersionRange }
   deriving Show
 
 instance Semigroup IntersectingVersionRange where
src/Stack/Types/VersionedDownloadInfo.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude #-}
+{-# LANGUAGE NoFieldSelectors  #-}
 {-# LANGUAGE OverloadedStrings #-}
 
 module Stack.Types.VersionedDownloadInfo
@@ -13,8 +14,8 @@                    ( DownloadInfo, parseDownloadInfoFromObject )
 
 data VersionedDownloadInfo = VersionedDownloadInfo
-  { vdiVersion :: Version
-  , vdiDownloadInfo :: DownloadInfo
+  { version :: Version
+  , downloadInfo :: DownloadInfo
   }
   deriving Show
 
@@ -23,6 +24,6 @@     CabalString version <- o ..: "version"
     downloadInfo <- parseDownloadInfoFromObject o
     pure VersionedDownloadInfo
-      { vdiVersion = version
-      , vdiDownloadInfo = downloadInfo
+      { version
+      , downloadInfo
       }
src/Stack/Uninstall.hs view
@@ -1,5 +1,6 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings   #-}
 
 -- | Function related to Stack's @uninstall@ command.
 module Stack.Uninstall
@@ -10,9 +11,7 @@ import           Stack.Prelude
 import           Stack.Runners ( ShouldReexec (..), withConfig )
 import           Stack.Types.Config
-                   ( configL, configLocalBin, configLocalProgramsBase
-                   , stackGlobalConfigL, stackRootL
-                   )
+                   ( Config (..), configL, stackGlobalConfigL, stackRootL )
 import           Stack.Types.Runner ( Runner )
 
 -- | Function underlying the @stack uninstall@ command. Display help for the
@@ -21,8 +20,8 @@ uninstallCmd () = withConfig NoReexec $ do
   stackRoot <- view stackRootL
   globalConfig <- view stackGlobalConfigL
-  programsDir <- view $ configL.to configLocalProgramsBase
-  localBinDir <- view $ configL.to configLocalBin
+  programsDir <- view $ configL . to (.localProgramsBase)
+  localBinDir <- view $ configL . to (.localBin)
   let toStyleDoc = style Dir . fromString . toFilePath
       stackRoot' = toStyleDoc stackRoot
       globalConfig' = toStyleDoc globalConfig
src/Stack/Unpack.hs view
@@ -1,22 +1,29 @@ {-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 {-# LANGUAGE OverloadedStrings   #-}
 
 -- | Functions related to Stack's @unpack@ command.
 module Stack.Unpack
-  ( unpackCmd
+  ( UnpackOpts (..)
+  , UnpackTarget
+  , unpackCmd
   , unpackPackages
   ) where
 
-import           Path ( (</>), parseRelDir )
-import           Path.IO ( doesDirExist, resolveDir' )
+import           Data.List.Extra ( notNull )
+import           Path ( SomeBase (..), (</>), parseRelDir )
+import           Path.IO ( doesDirExist, getCurrentDir )
 import           Pantry ( loadSnapshot )
 import qualified RIO.Map as Map
 import           RIO.Process ( HasProcessContext )
 import qualified RIO.Set as Set
 import qualified RIO.Text as T
 import           Stack.Config ( makeConcreteResolver )
+import           Stack.Constants ( relDirRoot )
 import           Stack.Prelude
 import           Stack.Runners ( ShouldReexec (..), withConfig )
+import           Stack.Types.Config ( Config (..), HasConfig, configL )
 import           Stack.Types.GlobalOpts ( GlobalOpts (..) )
 import           Stack.Types.Runner ( Runner, globalOptsL )
 
@@ -25,6 +32,8 @@ data UnpackPrettyException
   = UnpackDirectoryAlreadyExists (Set (Path Abs Dir))
   | CouldNotParsePackageSelectors [StyleDoc]
+  | PackageCandidatesRequireVersions [PackageName]
+  | PackageLocationInvalid PackageIdentifierRevision
   deriving (Show, Typeable)
 
 instance Pretty UnpackPrettyException where
@@ -42,43 +51,110 @@             \identifiers:"
     <> line
     <> bulletedList errs
+  pretty (PackageCandidatesRequireVersions names) =
+    "[S-6114]"
+    <> line
+    <> flow "Package candidates to unpack cannot be identified by name only. \
+            \The following do not specify a version:"
+    <> line
+    <> bulletedList (map fromPackageName names)
+  pretty (PackageLocationInvalid pir) =
+    "[S-5170]"
+    <> line
+    <> fillSep
+         [ flow "While trying to unpack"
+         , style Target (fromString $ T.unpack $ textDisplay pir) <> ","
+         , flow "Stack encountered an error."
+         ]
 
 instance Exception UnpackPrettyException
 
--- | Function underlying the @stack unpack@ command. Unpack packages to the
--- filesystem.
+-- | Type synonymn representing packages to be unpacked by the @stack unpack@
+-- command, identified either by name only or by an identifier (including
+-- Hackage revision).
+type UnpackTarget = Either PackageName PackageIdentifierRevision
+
+-- | Type representing options for the @stack unpack@ command.
+data UnpackOpts = UnpackOpts
+  { targets :: [UnpackTarget]
+    -- ^ The packages or package candidates to be unpacked.
+  , areCandidates :: Bool
+    -- ^ Whether the targets are Hackage package candidates.
+  , dest :: Maybe (SomeBase Dir)
+    -- ^ The optional directory into which a target will be unpacked into a
+    -- subdirectory.
+  }
+
+-- | Function underlying the @stack unpack@ command. Unpack packages or package
+-- candidates to the filesystem.
 unpackCmd ::
-     ([String], Maybe Text)
-     -- ^ A pair of a list of names or identifiers and an optional destination
-     -- path.
+     UnpackOpts
   -> RIO Runner ()
-unpackCmd (names, Nothing) = unpackCmd (names, Just ".")
-unpackCmd (names, Just dstPath) = withConfig NoReexec $ do
-  mresolver <- view $ globalOptsL.to globalResolver
-  mSnapshot <- forM mresolver $ \resolver -> do
-    concrete <- makeConcreteResolver resolver
-    loc <- completeSnapshotLocation concrete
-    loadSnapshot loc
-  dstPath' <- resolveDir' $ T.unpack dstPath
-  unpackPackages mSnapshot dstPath' names
+unpackCmd (UnpackOpts targets areCandidates Nothing) =
+  unpackCmd (UnpackOpts targets areCandidates (Just $ Rel relDirRoot))
+unpackCmd (UnpackOpts targets areCandidates (Just dstPath)) =
+  withConfig NoReexec $ do
+    mresolver <- view $ globalOptsL . to (.resolver)
+    mSnapshot <- forM mresolver $ \resolver -> do
+      concrete <- makeConcreteResolver resolver
+      loc <- completeSnapshotLocation concrete
+      loadSnapshot loc
+    dstPath' <- case dstPath of
+      Abs path -> pure path
+      Rel path -> do
+        wd <- getCurrentDir
+        pure $ wd </> path
+    unpackPackages mSnapshot dstPath' targets areCandidates
 
 -- | Intended to work for the command line command.
 unpackPackages ::
-     forall env. (HasPantryConfig env, HasProcessContext env, HasTerm env)
+     forall env.
+       (HasConfig env, HasPantryConfig env, HasProcessContext env, HasTerm env)
   => Maybe RawSnapshot -- ^ When looking up by name, take from this build plan.
   -> Path Abs Dir -- ^ Destination.
-  -> [String] -- ^ Names or identifiers.
+  -> [UnpackTarget]
+  -> Bool
+     -- ^ Whether the targets are package candidates.
   -> RIO env ()
-unpackPackages mSnapshot dest input = do
-  let (errs1, (names, pirs1)) =
-        fmap partitionEithers $ partitionEithers $ map parse input
-  locs1 <- forM pirs1 $ \pir -> do
-    loc <- fmap cplComplete $ completePackageLocation $ RPLIHackage pir Nothing
+unpackPackages mSnapshot dest targets areCandidates = do
+  let (names, pirs) = partitionEithers targets
+      pisWithRevisions = any hasRevision pirs
+      hasRevision (PackageIdentifierRevision _ _ CFILatest) = False
+      hasRevision _ = True
+  when (areCandidates && notNull names) $
+    prettyThrowIO $ PackageCandidatesRequireVersions names
+  when (areCandidates && pisWithRevisions) $
+    prettyWarn $
+         flow "Package revisions are not meaningful for package candidates and \
+              \will be ignored."
+      <> line
+  locs1 <- forM pirs $ \pir -> do
+    hackageBaseUrl <- view $ configL . to (.hackageBaseUrl)
+    let rpli = if areCandidates
+          then
+            let -- Ignoring revisions for package candidates.
+                PackageIdentifierRevision candidateName candidateVersion _ = pir
+                candidatePkgId =
+                  PackageIdentifier candidateName candidateVersion
+                candidatePkgIdText =
+                  T.pack $ packageIdentifierString candidatePkgId
+                candidateUrl =
+                     hackageBaseUrl
+                  <> "package/"
+                  <> candidatePkgIdText
+                  <> "/candidate/"
+                  <> candidatePkgIdText
+                  <> ".tar.gz"
+                candidateLoc = ALUrl candidateUrl
+                candidateArchive = RawArchive candidateLoc Nothing Nothing ""
+                candidateMetadata = RawPackageMetadata Nothing Nothing Nothing
+            in RPLIArchive candidateArchive candidateMetadata
+          else RPLIHackage pir Nothing
+    loc <- cplComplete <$> completePackageLocation rpli
+      `catch` \(_ :: SomeException) -> prettyThrowIO $ PackageLocationInvalid pir
     pure (loc, packageLocationIdent loc)
-  (errs2, locs2) <- partitionEithers <$> traverse toLoc names
-  case errs1 ++ errs2 of
-    [] -> pure ()
-    errs -> prettyThrowM $ CouldNotParsePackageSelectors errs
+  (errs, locs2) <- partitionEithers <$> traverse toLoc names
+  unless (null errs) $ prettyThrowM $ CouldNotParsePackageSelectors errs
   locs <- Map.fromList <$> mapM
     (\(pir, ident) -> do
         suffix <- parseRelDir $ packageIdentifierString ident
@@ -100,46 +176,32 @@       , pretty dest' <> "."
       ]
  where
-  toLoc | Just snapshot <- mSnapshot = toLocSnapshot snapshot
-        | otherwise = toLocNoSnapshot
+  toLoc name | Just snapshot <- mSnapshot = toLocSnapshot snapshot name
+             | otherwise = do
+                 void $ updateHackageIndex $ Just "Updating the package index."
+                 toLocNoSnapshot name
 
   toLocNoSnapshot ::
        PackageName
     -> RIO env (Either StyleDoc (PackageLocationImmutable, PackageIdentifier))
   toLocNoSnapshot name = do
-    mloc1 <- getLatestHackageLocation
+    mLoc <- getLatestHackageLocation
       YesRequireHackageIndex
       name
       UsePreferredVersions
-    mloc <-
-      case mloc1 of
-        Just _ -> pure mloc1
-        Nothing -> do
-          updated <- updateHackageIndex
-            $ Just
-            $    "Could not find package "
-              <> fromString (packageNameString name)
-              <> ", updating"
-          case updated of
-            UpdateOccurred ->
-              getLatestHackageLocation
-                YesRequireHackageIndex
-                name
-                UsePreferredVersions
-            NoUpdateOccurred -> pure Nothing
-    case mloc of
+    case mLoc of
       Nothing -> do
         candidates <- getHackageTypoCorrections name
         pure $ Left $ fillSep
           [ flow "Could not find package"
-          , style Current (fromString $ packageNameString name)
+          , style Current (fromPackageName name)
           , flow "on Hackage."
           , if null candidates
               then mempty
               else fillSep $
                   flow "Perhaps you meant one of:"
                 : mkNarrativeList (Just Good) False
-                    (map (fromString . packageNameString) candidates :: [StyleDoc])
+                    (map fromPackageName candidates :: [StyleDoc])
           ]
       Just loc -> pure $ Right (loc, packageLocationIdent loc)
 
@@ -152,20 +214,8 @@       Nothing ->
         pure $ Left $ fillSep
           [ flow "Package does not appear in snapshot:"
-          , style Current (fromString $ packageNameString name) <> "."
+          , style Current (fromPackageName name) <> "."
           ]
       Just sp -> do
         loc <- cplComplete <$> completePackageLocation (rspLocation sp)
         pure $ Right (loc, packageLocationIdent loc)
-
-  -- Possible future enhancement: parse names as name + version range
-  parse s =
-    case parsePackageName s of
-      Just x -> Right $ Left x
-      Nothing ->
-        case parsePackageIdentifierRevision (T.pack s) of
-          Right x -> Right $ Right x
-          Left _ -> Left $ fillSep
-            [ flow "Could not parse as package name or identifier:"
-            , style Current (fromString s) <> "."
-            ]
src/Stack/Upgrade.hs view
@@ -1,4 +1,6 @@ {-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
 {-# LANGUAGE OverloadedStrings     #-}
 
 -- | Types and functions related to Stack's @upgrade@ command.
@@ -25,10 +27,9 @@                    ( downloadStackExe, downloadStackReleaseInfo
                    , getDownloadVersion, preferredPlatforms, stackVersion
                    )
-import           Stack.Types.BuildOpts
-                   ( BuildOptsCLI (..), buildOptsInstallExesL
-                   , defaultBuildOptsCLI
-                   )
+import           Stack.Types.BuildOpts ( buildOptsInstallExesL )
+import           Stack.Types.BuildOptsCLI
+                   ( BuildOptsCLI (..), defaultBuildOptsCLI )
 import           Stack.Types.Config ( Config (..), HasConfig (..), buildOptsL )
 import           Stack.Types.GlobalOpts ( GlobalOpts (..) )
 import           Stack.Types.Runner ( Runner, globalOptsL )
@@ -88,16 +89,16 @@ -- | Type representing options for upgrading Stack with a binary executable
 -- file.
 data BinaryOpts = BinaryOpts
-  { _boPlatform :: !(Maybe String)
-  , _boForce :: !Bool
+  { platform :: !(Maybe String)
+  , force :: !Bool
     -- ^ Force a download, even if the downloaded version is older than what we
     -- are.
-  , _boOnlyLocalBin :: !Bool
+  , onlyLocalBin :: !Bool
     -- ^ Only download to Stack's local binary directory.
-  , _boVersion :: !(Maybe String)
+  , version :: !(Maybe String)
     -- ^ Specific version to download
-  , _boGitHubOrg :: !(Maybe String)
-  , _boGitHubRepo :: !(Maybe String)
+  , gitHubOrg :: !(Maybe String)
+  , gitHubRepo :: !(Maybe String)
   }
   deriving Show
 
@@ -108,8 +109,8 @@ 
 -- | Type representing command line options for the @stack upgrade@ command.
 data UpgradeOpts = UpgradeOpts
-  { _uoBinary :: !(Maybe BinaryOpts)
-  , _uoSource :: !(Maybe SourceOpts)
+  { binary :: !(Maybe BinaryOpts)
+  , source :: !(Maybe SourceOpts)
   }
   deriving Show
 
@@ -117,7 +118,7 @@ upgradeCmd :: UpgradeOpts -> RIO Runner ()
 upgradeCmd upgradeOpts = do
   go <- view globalOptsL
-  case globalResolver go of
+  case go.resolver of
     Just _ -> prettyThrowIO ResolverOptionInvalid
     Nothing -> withGlobalProject $ upgrade maybeGitHash upgradeOpts
 
@@ -197,7 +198,7 @@     when toUpgrade $ do
       config <- view configL
       downloadStackExe
-        platforms0 archiveInfo (configLocalBin config) (not onlyLocalBin) $
+        platforms0 archiveInfo config.localBin (not onlyLocalBin) $
           \tmpFile -> do
             -- Sanity check!
             ec <- rawSystem (toFilePath tmpFile) ["--version"]
@@ -282,16 +283,14 @@                 pure $ Just dir
 
     let modifyGO dir go = go
-          { globalResolver = Nothing -- always use the resolver settings in the
+          { resolver = Nothing -- always use the resolver settings in the
                                      -- stack.yaml file
-          , globalStackYaml = SYLOverride $ dir </> stackDotYaml
-          }
-        boptsCLI = defaultBuildOptsCLI
-          { boptsCLITargets = ["stack"]
+          , stackYaml = SYLOverride $ dir </> stackDotYaml
           }
+        boptsCLI = defaultBuildOptsCLI { targetsCLI = ["stack"] }
     forM_ mdir $ \dir ->
       local (over globalOptsL (modifyGO dir))
         $ withConfig NoReexec
         $ withEnvConfig AllowNoTargets boptsCLI
-        $ local (set (buildOptsL.buildOptsInstallExesL) True)
+        $ local (set (buildOptsL . buildOptsInstallExesL) True)
         $ build Nothing
src/Stack/Upload.hs view
@@ -1,10 +1,15 @@-{-# LANGUAGE NoImplicitPrelude   #-}
-{-# LANGUAGE OverloadedStrings   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE NoFieldSelectors      #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 -- | Types and functions related to Stack's @upload@ command.
 module Stack.Upload
   ( -- * Upload
     UploadOpts (..)
+  , SDistOpts (..)
+  , UploadContent (..)
   , UploadVariant (..)
   , uploadCmd
   , upload
@@ -34,118 +39,240 @@                    ( Request, RequestBody (RequestBodyLBS), Response
                    , applyDigestAuth, displayDigestAuthException, formDataBody
                    , getGlobalManager, getResponseBody, getResponseStatusCode
-                   , httpNoBody, parseRequest, partBS, partFileRequestBody
-                   , partLBS, setRequestHeader, withResponse
+                   , httpNoBody, method, methodPost, methodPut, parseRequest
+                   , partBS, partFileRequestBody, partLBS, requestBody
+                   , setRequestHeader, setRequestHeaders, withResponse
                    )
+import           Path ( (</>), addExtension, parseRelFile )
 import           Path.IO ( resolveDir', resolveFile' )
+import qualified Path.IO as Path
+import           Stack.Constants ( isStackUploadDisabled )
+import           Stack.Constants.Config ( distDirFromDir )
 import           Stack.Prelude
 import           Stack.Runners
                    ( ShouldReexec (..), withConfig, withDefaultEnvConfig )
 import           Stack.SDist
                    ( SDistOpts (..), checkSDistTarball, checkSDistTarball'
-                   , getSDistTarball
+                   , getSDistTarball, readLocalPackage
                    )
 import           Stack.Types.Config ( Config (..), configL, stackRootL )
+import           Stack.Types.EnvConfig ( HasEnvConfig )
+import           Stack.Types.Package ( LocalPackage (..), packageIdentifier )
+import           Stack.Types.PvpBounds (PvpBounds)
 import           Stack.Types.Runner ( Runner )
 import           System.Directory
                    ( createDirectoryIfMissing, doesDirectoryExist, doesFileExist
                    , removeFile, renameFile
                    )
 import           System.Environment ( lookupEnv )
-import           System.FilePath ( (</>), takeDirectory, takeFileName )
+import qualified System.FilePath as FP
 import           System.PosixCompat.Files ( setFileMode )
 
 -- | Type representing \'pretty\' exceptions thrown by functions exported by the
 -- "Stack.Upload" module.
 data UploadPrettyException
   = AuthenticationFailure
-  | ArchiveUploadFailure Int [String] String
+  | ArchiveUploadFailure !Int ![String] !String
+  | DocsTarballInvalid ![(String, Path Abs File)]
+  | ItemsInvalid ![FilePath]
+  | NoItemSpecified !String
+  | PackageDirectoryInvalid ![FilePath]
+  | PackageIdNotSpecifiedForDocsUploadBug
+  | PackageIdSpecifiedForPackageUploadBug
+  | TarGzFileNameInvalidBug !String
   deriving (Show, Typeable)
 
 instance Pretty UploadPrettyException where
   pretty AuthenticationFailure =
-       "[S-2256]"
+    "[S-2256]"
     <> line
     <> flow "authentification failure"
     <> line
     <> flow "Authentication failure uploading to server"
   pretty (ArchiveUploadFailure code res tarName) =
-       "[S-6108]"
+    "[S-6108]"
     <> line
     <> flow "unhandled status code:" <+> fromString (show code)
     <> line
     <> flow "Upload failed on" <+> style File (fromString tarName)
     <> line
     <> vsep (map string res)
+  pretty (DocsTarballInvalid invalidItems) =
+    "[S-2837]"
+    <> line
+    <> flow "Stack can't find:"
+    <> line
+    <> invalidList
+   where
+    invalidItem (pkgIdName, tarGzFile) = fillSep
+      [ pretty tarGzFile
+      , "for"
+      , style Current (fromString pkgIdName) <> "."
+      ]
+    invalidList = bulletedList $ map invalidItem invalidItems
+  pretty (ItemsInvalid invalidItems) =
+    "[S-3179]"
+    <> line
+    <> flow "For package upload, Stack expects a list of relative paths to \
+            \tosdist tarballs or package directories. Stack can't find:"
+    <> line
+    <> invalidList
+   where
+    invalidList = bulletedList $ map (style File . fromString) invalidItems
+  pretty (NoItemSpecified subject) =
+    "[S-3030]"
+    <> line
+    <> fillSep
+         [ flow "An item must be specified. To upload"
+         , flow subject
+         , flow "please run"
+         , style Shell "stack upload ."
+         , flow "(with the period at the end)."
+         ]
+  pretty (PackageDirectoryInvalid invalidItems) =
+    "[S-5908]"
+    <> line
+    <> flow "For documentation upload, Stack expects a list of relative paths \
+            \to package directories. Stack can't find:"
+    <> line
+    <> invalidList
+   where
+    invalidList = bulletedList $ map (style Current . fromString) invalidItems
+  pretty PackageIdNotSpecifiedForDocsUploadBug = bugPrettyReport "[S-7274]" $
+    flow "uploadBytes: Documentation upload but package identifier not \
+         \specified."
+  pretty PackageIdSpecifiedForPackageUploadBug = bugPrettyReport "[S-5860]" $
+    flow "uploadBytes: Package upload but package identifier specified."
+  pretty (TarGzFileNameInvalidBug name) = bugPrettyReport "[S-5955]" $
+    fillSep
+      [ flow "uploadCmd: the name of the"
+      , fromString name <> ".tar.gz"
+      , flow "file could not be parsed."
+      ]
 
 instance Exception UploadPrettyException
 
--- Type representing variants for uploading to Hackage.
+-- | Type representing forms of content for upload to Hackage.
+data UploadContent
+  = SDist
+    -- ^ Content in the form of an sdist tarball.
+  | DocArchive
+    -- ^ Content in the form of an archive file of package documentation.
+
+-- | Type representing variants for uploading to Hackage.
 data UploadVariant
   = Publishing
-  -- ^ Publish the package
+    -- ^ Publish the package/a published package.
   | Candidate
-  -- ^ Create a package candidate
+    -- ^ Create a package candidate/a package candidate.
 
 -- | Type representing command line options for the @stack upload@ command.
 data UploadOpts = UploadOpts
-  { uoptsSDistOpts :: SDistOpts
-  , uoptsUploadVariant :: UploadVariant
-  -- ^ Says whether to publish the package or upload as a release candidate
+  { itemsToWorkWith :: ![String]
+    -- ^ The items to work with.
+  , documentation :: !Bool
+    -- ^ Uploading documentation for packages?
+  , pvpBounds :: !(Maybe PvpBounds)
+  , check :: !Bool
+  , buildPackage :: !Bool
+  , tarPath :: !(Maybe FilePath)
+  , uploadVariant :: !UploadVariant
   }
 
 -- | Function underlying the @stack upload@ command. Upload to Hackage.
 uploadCmd :: UploadOpts -> RIO Runner ()
-uploadCmd (UploadOpts (SDistOpts [] _ _ _ _) _) = do
-  prettyErrorL
-    [ flow "To upload the current package, please run"
-    , style Shell "stack upload ."
-    , flow "(with the period at the end)"
-    ]
-  liftIO exitFailure
-uploadCmd uploadOpts = do
-  let partitionM _ [] = pure ([], [])
-      partitionM f (x:xs) = do
-        r <- f x
-        (as, bs) <- partitionM f xs
-        pure $ if r then (x:as, bs) else (as, x:bs)
-      sdistOpts = uoptsSDistOpts uploadOpts
-  (files, nonFiles) <-
-    liftIO $ partitionM doesFileExist (sdoptsDirsToWorkWith sdistOpts)
-  (dirs, invalid) <- liftIO $ partitionM doesDirectoryExist nonFiles
-  withConfig YesReexec $ withDefaultEnvConfig $ do
-    unless (null invalid) $ do
-      let invalidList = bulletedList $ map (style File . fromString) invalid
-      prettyErrorL
-        [ style Shell "stack upload"
-        , flow "expects a list of sdist tarballs or package directories."
-        , flow "Can't find:"
-        , line <> invalidList
-        ]
-      exitFailure
-    when (null files && null dirs) $ do
-      prettyErrorL
-        [ style Shell "stack upload"
-        , flow "expects a list of sdist tarballs or package directories, but none were specified."
-        ]
-      exitFailure
-    config <- view configL
-    let hackageUrl = T.unpack $ configHackageBaseUrl config
-        uploadVariant = uoptsUploadVariant uploadOpts
-    getCreds <- memoizeRef $ loadAuth config
-    mapM_ (resolveFile' >=> checkSDistTarball sdistOpts) files
-    forM_ files $ \file -> do
-      tarFile <- resolveFile' file
-      creds <- runMemoized getCreds
-      upload hackageUrl creds (toFilePath tarFile) uploadVariant
-    forM_ dirs $ \dir -> do
+uploadCmd (UploadOpts [] uoDocumentation _ _ _ _ _) = do
+  let subject = if uoDocumentation
+        then "documentation for the current package,"
+        else "the current package,"
+  prettyThrowIO $ NoItemSpecified subject
+uploadCmd uo = withConfig YesReexec $ withDefaultEnvConfig $ do
+  config <- view configL
+  let hackageUrl = T.unpack config.hackageBaseUrl
+  if uo.documentation
+    then do
+      (dirs, invalid) <-
+        liftIO $ partitionM doesDirectoryExist uo.itemsToWorkWith
+      unless (null invalid) $
+        prettyThrowIO $ PackageDirectoryInvalid invalid
+      (failed, items) <- partitionEithers <$> forM dirs checkDocsTarball
+      unless (null failed) $ do
+        prettyThrowIO $ DocsTarballInvalid failed
+      getCreds <- memoizeRef $ loadAuth config
+      forM_ items $ \(pkgIdName, tarGzFile) -> do
+        creds <- runMemoized getCreds
+        upload
+          hackageUrl
+          creds
+          DocArchive
+          (Just pkgIdName)
+          (toFilePath tarGzFile)
+          uo.uploadVariant
+    else do
+      (files, nonFiles) <-
+        liftIO $ partitionM doesFileExist uo.itemsToWorkWith
+      (dirs, invalid) <- liftIO $ partitionM doesDirectoryExist nonFiles
+      unless (null invalid) $ do
+        prettyThrowIO $ ItemsInvalid invalid
+      let sdistOpts = SDistOpts
+            uo.itemsToWorkWith
+            uo.pvpBounds
+            uo.check
+            uo.buildPackage
+            uo.tarPath
+      getCreds <- memoizeRef $ loadAuth config
+      mapM_ (resolveFile' >=> checkSDistTarball sdistOpts) files
+      forM_ files $ \file -> do
+        tarFile <- resolveFile' file
+        creds <- runMemoized getCreds
+        upload
+          hackageUrl
+          creds
+          SDist
+          Nothing
+          (toFilePath tarFile)
+          uo.uploadVariant
+      forM_ dirs $ \dir -> do
+        pkgDir <- resolveDir' dir
+        (tarName, tarBytes, mcabalRevision) <-
+          getSDistTarball uo.pvpBounds pkgDir
+        checkSDistTarball' sdistOpts tarName tarBytes
+        creds <- runMemoized getCreds
+        uploadBytes
+          hackageUrl
+          creds
+          SDist
+          Nothing
+          tarName
+          uo.uploadVariant
+          tarBytes
+        forM_ mcabalRevision $ uncurry $ uploadRevision hackageUrl creds
+   where
+    checkDocsTarball ::
+         HasEnvConfig env
+      => FilePath
+      -> RIO env (Either (String, Path Abs File) (String, Path Abs File))
+    checkDocsTarball dir = do
       pkgDir <- resolveDir' dir
-      (tarName, tarBytes, mcabalRevision) <-
-        getSDistTarball (sdoptsPvpBounds sdistOpts) pkgDir
-      checkSDistTarball' sdistOpts tarName tarBytes
-      creds <- runMemoized getCreds
-      uploadBytes hackageUrl creds tarName uploadVariant tarBytes
-      forM_ mcabalRevision $ uncurry $ uploadRevision hackageUrl creds
+      distDir <- distDirFromDir pkgDir
+      lp <- readLocalPackage pkgDir
+      let pkgId = packageIdentifier lp.package
+          pkgIdName = packageIdentifierString pkgId
+          name = pkgIdName <> "-docs"
+      tarGzFileName <- maybe
+        (prettyThrowIO $ TarGzFileNameInvalidBug name)
+        pure
+        ( do nameRelFile <- parseRelFile name
+             addExtension ".gz" =<< addExtension ".tar" nameRelFile
+        )
+      let tarGzFile = distDir Path.</> tarGzFileName
+      isFile <- Path.doesFileExist tarGzFile
+      pure $ (if isFile then Right else Left) (pkgIdName, tarGzFile)
+    partitionM _ [] = pure ([], [])
+    partitionM f (x:xs) = do
+      r <- f x
+      (as, bs) <- partitionM f xs
+      pure $ if r then (x:as, bs) else (as, x:bs)
 
 newtype HackageKey = HackageKey Text
   deriving (Eq, Show)
@@ -154,9 +281,9 @@ --
 -- Since 0.1.0.0
 data HackageCreds = HackageCreds
-  { hcUsername :: !Text
-  , hcPassword :: !Text
-  , hcCredsFile :: !FilePath
+  { username :: !Text
+  , password :: !Text
+  , credsFile :: !FilePath
   }
   deriving (Eq, Show)
 
@@ -209,10 +336,13 @@       -- didn't do this
       writeFilePrivate fp $ lazyByteString lbs
 
-      unless (configSaveHackageCreds config) $ do
+      unless config.saveHackageCreds $ do
         prettyWarnL
-          [ flow "You've set save-hackage-creds to false. However, credentials \
-                 \ were found at:"
+          [ flow "You've set"
+          , style Shell "save-hackage-creds"
+          , "to"
+          , style Shell "false" <> "."
+          , flow "However, credentials were found at:"
           , style File (fromString fp) <> "."
           ]
       pure $ mkCreds fp
@@ -222,12 +352,12 @@     username <- liftIO $ withEnvVariable "HACKAGE_USERNAME" (prompt "Hackage username: ")
     password <- liftIO $ withEnvVariable "HACKAGE_PASSWORD" (promptPassword "Hackage password: ")
     let hc = HackageCreds
-          { hcUsername = username
-          , hcPassword = password
-          , hcCredsFile = fp
+          { username
+          , password
+          , credsFile = fp
           }
 
-    when (configSaveHackageCreds config) $ do
+    when config.saveHackageCreds $ do
       shouldSave <- promptBool $ T.pack $
         "Save Hackage credentials to file at " ++ fp ++ " [y/n]? "
       prettyNoteL
@@ -251,7 +381,7 @@ -- * https://github.com/commercialhaskell/stack/pull/4665
 writeFilePrivate :: MonadIO m => FilePath -> Builder -> m ()
 writeFilePrivate fp builder =
-  liftIO $ withTempFile (takeDirectory fp) (takeFileName fp) $ \fpTmp h -> do
+  liftIO $ withTempFile (FP.takeDirectory fp) (FP.takeFileName fp) $ \fpTmp h -> do
     -- Temp file is created such that only current user can read and write it.
     -- See docs for openTempFile:
     -- https://www.stackage.org/haddock/lts-13.14/base-4.12.0.0/System-IO.html#v:openTempFile
@@ -268,9 +398,9 @@ 
 credsFile :: Config -> IO FilePath
 credsFile config = do
-  let dir = toFilePath (view stackRootL config) </> "upload"
+  let dir = toFilePath (view stackRootL config) FP.</> "upload"
   createDirectoryIfMissing True dir
-  pure $ dir </> "credentials.json"
+  pure $ dir FP.</> "credentials.json"
 
 addAPIKey :: HackageKey -> Request -> Request
 addAPIKey (HackageKey key) = setRequestHeader
@@ -294,11 +424,16 @@   -> RIO m Request
 applyCreds creds req0 = do
   manager <- liftIO getGlobalManager
-  ereq <- liftIO $ applyDigestAuth
-    (encodeUtf8 $ hcUsername creds)
-    (encodeUtf8 $ hcPassword creds)
-    req0
-    manager
+  ereq <- if isStackUploadDisabled
+    then do
+      debugRequest "applyCreds" req0
+      pure (Left $ toException ExitSuccess )
+    else
+      liftIO $ applyDigestAuth
+        (encodeUtf8 creds.username)
+        (encodeUtf8 creds.password)
+        req0
+        manager
   case ereq of
     Left e -> do
       prettyWarn $
@@ -312,37 +447,68 @@       pure req0
     Right req -> pure req
 
--- | Upload a single tarball with the given @Uploader@.  Instead of
--- sending a file like 'upload', this sends a lazy bytestring.
+-- | Upload a single tarball with the given @Uploader@. Instead of sending a
+-- file like 'upload', this sends a lazy bytestring.
 --
 -- Since 0.1.2.1
-uploadBytes :: HasTerm m
-            => String -- ^ Hackage base URL
-            -> HackageAuth
-            -> String -- ^ tar file name
-            -> UploadVariant
-            -> L.ByteString -- ^ tar file contents
-            -> RIO m ()
-uploadBytes baseUrl auth tarName uploadVariant bytes = do
-  let req1 = setRequestHeader
-               "Accept"
-               ["text/plain"]
-               (fromString
-                  $  baseUrl
-                  <> "packages/"
-                  <> case uploadVariant of
-                       Publishing -> ""
-                       Candidate -> "candidates/"
-               )
-      formData = [partFileRequestBody "package" tarName (RequestBodyLBS bytes)]
-  req2 <- liftIO $ formDataBody formData req1
-  req3 <- applyAuth auth req2
+uploadBytes ::
+     HasTerm m
+  => String -- ^ Hackage base URL
+  -> HackageAuth
+  -> UploadContent
+     -- ^ Form of the content to be uploaded.
+  -> Maybe String
+     -- ^ Optional package identifier name, applies only to the upload of
+     -- documentation.
+  -> String -- ^ tar file name
+  -> UploadVariant
+  -> L.ByteString -- ^ tar file contents
+  -> RIO m ()
+uploadBytes baseUrl auth contentForm mPkgIdName tarName uploadVariant bytes = do
+  (url, headers, uploadMethod) <- case contentForm of
+    SDist -> do
+      unless (isNothing mPkgIdName) $
+        prettyThrowIO PackageIdSpecifiedForPackageUploadBug
+      let variant = case uploadVariant of
+            Publishing -> ""
+            Candidate -> "candidates/"
+      pure
+        ( baseUrl <> "packages/" <> variant
+        , [("Accept", "text/plain")]
+        , methodPost
+        )
+    DocArchive -> case mPkgIdName of
+      Nothing -> prettyThrowIO PackageIdNotSpecifiedForDocsUploadBug
+      Just pkgIdName -> do
+        let variant = case uploadVariant of
+              Publishing -> ""
+              Candidate -> "candidate/"
+        pure
+          ( baseUrl <> "package/" <> pkgIdName <> "/" <> variant <> "docs"
+          , [ ("Content-Type", "application/x-tar")
+            , ("Content-Encoding", "gzip")
+            ]
+          , methodPut
+          )
+  let req1 = setRequestHeaders headers (fromString url)
+      reqData = RequestBodyLBS bytes
+      formData = [partFileRequestBody "package" tarName reqData]
+
+  req2 <- case contentForm of
+    SDist -> liftIO $ formDataBody formData req1
+    DocArchive -> pure $ req1 { requestBody = reqData }
+  let req3 = req2 { method = uploadMethod }
+  req4 <- applyAuth auth req3
   prettyInfoL
     [ "Uploading"
     , style Current (fromString tarName) <> "..."
     ]
   hFlush stdout
-  withRunInIO $ \runInIO -> withResponse req3 (runInIO . inner)
+  if isStackUploadDisabled
+    then
+      debugRequest "uploadBytes" req4
+    else
+      withRunInIO $ \runInIO -> withResponse req4 (runInIO . inner)
  where
   inner :: HasTerm m => Response (ConduitM () S.ByteString IO ()) -> RIO m ()
   inner res =
@@ -353,7 +519,7 @@           HACreds creds ->
             handleIO
               (const $ pure ())
-              (liftIO $ removeFile (hcCredsFile creds))
+              (liftIO $ removeFile creds.credsFile)
           _ -> pure ()
         prettyThrowIO AuthenticationFailure
       403 -> do
@@ -389,22 +555,30 @@ -- | Upload a single tarball with the given @Uploader@.
 --
 -- Since 0.1.0.0
-upload :: (HasLogFunc m, HasTerm m)
-       => String -- ^ Hackage base URL
-       -> HackageAuth
-       -> FilePath
-       -> UploadVariant
-       -> RIO m ()
-upload baseUrl auth fp uploadVariant =
-  uploadBytes baseUrl auth (takeFileName fp) uploadVariant
-    =<< liftIO (L.readFile fp)
+upload ::
+     (HasLogFunc m, HasTerm m)
+  => String -- ^ Hackage base URL
+  -> HackageAuth
+  -> UploadContent
+  -> Maybe String
+     -- ^ Optional package identifier name, applies only to the upload of
+     -- documentation.
+  -> FilePath
+     -- ^ Path to archive file.
+  -> UploadVariant
+  -> RIO m ()
+upload baseUrl auth contentForm mPkgIdName fp uploadVariant =
+  uploadBytes
+    baseUrl auth contentForm mPkgIdName (FP.takeFileName fp) uploadVariant
+      =<< liftIO (L.readFile fp)
 
-uploadRevision :: (HasLogFunc m, HasTerm m)
-               => String -- ^ Hackage base URL
-               -> HackageAuth
-               -> PackageIdentifier
-               -> L.ByteString
-               -> RIO m ()
+uploadRevision ::
+     (HasLogFunc m, HasTerm m)
+  => String -- ^ Hackage base URL
+  -> HackageAuth
+  -> PackageIdentifier
+  -> L.ByteString
+  -> RIO m ()
 uploadRevision baseUrl auth ident@(PackageIdentifier name _) cabalFile = do
   req0 <- parseRequest $ concat
     [ baseUrl
@@ -420,4 +594,17 @@     ]
     req0
   req2 <- applyAuth auth req1
-  void $ httpNoBody req2
+  if isStackUploadDisabled
+    then
+      debugRequest "uploadRevision" req2
+    else
+      void $ httpNoBody req2
+
+debugRequest :: HasTerm env => String -> Request -> RIO env ()
+debugRequest callSite req = prettyInfo $
+     fillSep
+       [ fromString callSite <> ":"
+       , flow "When enabled, would apply the following request:"
+       ]
+  <> line
+  <> fromString (show req)
src/unix/Stack/Docker/Handlers.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 
 -- | The module of this name differs as between Windows and non-Windows builds.
 -- This is the non-Windows version.
@@ -46,13 +47,13 @@     pure (sig, oldHandler)
   let args' = concat
         [ ["start"]
-        , ["-a" | not (dockerDetach docker)]
+        , ["-a" | not docker.detach]
         , ["-i" | keepStdinOpen]
         , [containerID]
         ]
   finally
     (try $ proc "docker" args' $ runProcess_ . setDelegateCtlc False)
-    ( do unless (dockerPersist docker || dockerDetach docker) $
+    ( do unless (docker.persist || docker.detach) $
            readProcessNull "docker" ["rm", "-f", containerID]
              `catch` (\(_ :: ExitCodeException) -> pure ())
          forM_ oldHandlers $ \(sig, oldHandler) ->
src/windows/Stack/Docker/Handlers.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE NoImplicitPrelude   #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 
 -- | The module of this name differs as between Windows and non-Windows builds.
 -- This is the Windows version.
@@ -26,13 +27,13 @@ handleSignals docker keepStdinOpen containerID = do
   let args' = concat
         [ ["start"]
-        , ["-a" | not (dockerDetach docker)]
+        , ["-a" | not docker.detach]
         , ["-i" | keepStdinOpen]
         , [containerID]
         ]
   finally
     (try $ proc "docker" args' $ runProcess_ . setDelegateCtlc False)
-    ( unless (dockerPersist docker || dockerDetach docker) $
+    ( unless (docker.persist || docker.detach) $
         readProcessNull "docker" ["rm", "-f", containerID]
           `catch` (\(_ :: ExitCodeException) -> pure ())
     )
src/windows/System/Posix/User.hs view
@@ -1,3 +1,6 @@+{-# LANGUAGE NoFieldSelectors    #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+
 -- | The module of this name differs as between Windows and non-Windows builds.
 -- This is the Windows version. Non-Windows builds rely on the unix package,
 -- which exposes a module of the same name.
@@ -47,3 +50,6 @@     , homeDirectory :: String
     , userShell     :: String
     } deriving (Eq, Read, Show)
+
+homeDirectory :: UserEntry -> String
+homeDirectory ue = ue.homeDirectory
src/windows/System/Terminal.hs view
@@ -13,7 +13,6 @@ import           Foreign.Marshal.Alloc ( allocaBytes )
 import           Foreign.Ptr ( Ptr )
 import           Foreign.Storable ( peekByteOff )
-import           RIO.Partial ( read )
 import           Stack.Prelude
 import           System.IO ( hGetContents )
 import           System.Process
@@ -66,8 +65,8 @@             maybe (pure Nothing)
                   (\hSize -> do
                       sizeStr <- hGetContents hSize
-                      case map read $ words sizeStr :: [Int] of
-                        [_r, c] -> pure $ Just c
+                      case map readMaybe $ words sizeStr :: [Maybe Int] of
+                        [Just _r, Just c] -> pure $ Just c
                         _ -> pure Nothing
                   )
                   mbStdout
stack.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.0 name:               stack-version:            2.13.1+version:            2.15.1 license:            BSD3 license-file:       LICENSE maintainer:         manny@fpcomplete.com@@ -107,9 +107,9 @@  custom-setup     setup-depends:-        Cabal >=3.8.1.0 && <3.12,+        Cabal >=3.10.1.0 && <3.12,         base >=4.14.3.0 && <5,-        filepath >=1.4.2.2+        filepath >=1.4.200.1  flag developer-mode     description: By default, output extra developer information.@@ -123,6 +123,13 @@     default:     False     manual:      True +flag disable-stack-upload+    description:+        For use only during development and debugging. Disable 'stack upload' so that it does not make HTTP requests. Stack will output information about the HTTP request(s) that it would have made if the command was enabled.++    default:     False+    manual:      True+ flag hide-dependency-versions     description:         Hides dependency versions from 'stack --version'. Used only when building a Stack executable for official release. Note to packagers/distributors: DO NOT OVERRIDE THIS FLAG IF YOU ARE BUILDING STACK ANY OTHER WAY (e.g. using Cabal or from Hackage), as it makes debugging support requests more difficult.@@ -151,11 +158,13 @@  library     exposed-modules:+        Codec.Archive.Tar.Utf8         Control.Concurrent.Execute         Data.Attoparsec.Args         Data.Attoparsec.Combinators         Data.Attoparsec.Interpreter         Data.Monoid.Map+        GHC.Utils.GhcPkg.Main.Compat         Network.HTTP.StackClient         Options.Applicative.Args         Options.Applicative.Builder.Extra@@ -169,17 +178,22 @@         Stack.Build.Cache         Stack.Build.ConstructPlan         Stack.Build.Execute+        Stack.Build.ExecuteEnv+        Stack.Build.ExecutePackage         Stack.Build.Haddock         Stack.Build.Installed         Stack.Build.Source         Stack.Build.Target-        Stack.BuildPlan         Stack.BuildInfo+        Stack.BuildOpts+        Stack.BuildPlan         Stack.CLI         Stack.Clean+        Stack.Component         Stack.ComponentFile         Stack.Config         Stack.Config.Build+        Stack.Config.ConfigureScript         Stack.Config.Docker         Stack.Config.Nix         Stack.ConfigCmd@@ -188,6 +202,7 @@         Stack.Constants.StackProgName         Stack.Coverage         Stack.DefaultColorWhen+        Stack.DependencyGraph         Stack.Docker         Stack.DockerCmd         Stack.Dot@@ -233,6 +248,7 @@         Stack.Options.ScriptParser         Stack.Options.SetupParser         Stack.Options.TestParser+        Stack.Options.UnpackParser         Stack.Options.UpgradeParser         Stack.Options.UploadParser         Stack.Options.Utils@@ -258,30 +274,40 @@         Stack.Types.ApplyGhcOptions         Stack.Types.ApplyProgOptions         Stack.Types.Build+        Stack.Types.Build.ConstructPlan         Stack.Types.Build.Exception         Stack.Types.BuildConfig         Stack.Types.BuildOpts+        Stack.Types.BuildOptsCLI+        Stack.Types.BuildOptsMonoid         Stack.Types.CabalConfigKey         Stack.Types.Cache         Stack.Types.Casa         Stack.Types.ColorWhen+        Stack.Types.CompCollection         Stack.Types.CompilerBuild         Stack.Types.CompilerPaths         Stack.Types.Compiler+        Stack.Types.Component+        Stack.Types.ComponentUtils         Stack.Types.Config         Stack.Types.Config.Exception         Stack.Types.ConfigMonoid         Stack.Types.ConfigureOpts         Stack.Types.Curator         Stack.Types.Dependency+        Stack.Types.DependencyTree         Stack.Types.Docker         Stack.Types.DockerEntrypoint+        Stack.Types.DotConfig+        Stack.Types.DotOpts         Stack.Types.DownloadInfo         Stack.Types.DumpLogs         Stack.Types.DumpPackage         Stack.Types.EnvConfig         Stack.Types.EnvSettings         Stack.Types.ExtraDirs+        Stack.Types.FileDigestCache         Stack.Types.GHCDownloadInfo         Stack.Types.GHCVariant         Stack.Types.GhcOptionKey@@ -289,6 +315,7 @@         Stack.Types.GhcPkgId         Stack.Types.GlobalOpts         Stack.Types.GlobalOptsMonoid+        Stack.Types.Installed         Stack.Types.IsMutable         Stack.Types.LockFileBehavior         Stack.Types.NamedComponent@@ -326,11 +353,6 @@         Paths_stack      hs-source-dirs:   src-    other-modules:-        GHC.Utils.GhcPkg.Main.Compat-        Stack.Config.ConfigureScript-        Stack.Types.FileDigestCache-     autogen-modules:         Build_stack         Paths_stack@@ -343,72 +365,72 @@     build-depends:         Cabal >=3.8.1.0,         aeson >=2.0.3.0,-        aeson-warning-parser >=0.1.0,-        ansi-terminal >=1.0,-        array >=0.5.4.0,-        async >=2.2.4,+        aeson-warning-parser >=0.1.1,+        ansi-terminal >=1.0.2,+        array >=0.5.6.0,+        async >=2.2.5,         attoparsec >=0.14.4,-        base >=4.14.3.0 && <5,+        base >=4.16.0.0 && <5,         base64-bytestring >=1.2.1.0,-        bytestring >=0.11.5.2,+        bytestring >=0.11.5.3,         casa-client >=0.0.2,         companion >=0.1.0,         conduit >=1.3.5,         conduit-extra >=1.3.6,         containers >=0.6.7,-        crypton >=0.33,-        directory >=1.3.7.1,+        crypton >=0.34,+        directory >=1.3.8.1,         echo >=0.1.4,-        exceptions >=0.10.5,+        exceptions >=0.10.7,         extra >=1.7.14,         file-embed >=0.0.15.0,         filelock >=0.1.1.7,-        filepath >=1.4.2.2,+        filepath >=1.4.200.1,         fsnotify >=0.4.1,         generic-deriving >=1.14.5,-        ghc-boot >=9.4.7,-        hi-file-parser >=0.1.4.0,+        ghc-boot >=9.6.4,+        hi-file-parser >=0.1.6.0,         hpack >=0.36.0,-        hpc >=0.6.1.0,-        http-client >=0.7.14,+        hpc >=0.6.2.0,+        http-client >=0.7.16,         http-client-tls >=0.3.6.2,-        http-conduit >=2.3.8.1,+        http-conduit >=2.3.8.3,         http-download >=0.2.1.0,-        http-types >=0.12.3,+        http-types >=0.12.4,         memory >=0.18.0,         microlens >=0.4.13.1,-        mtl >=2.2.2,+        mtl >=2.3.1,         mustache >=2.4.2,-        neat-interpolation >=0.5.1.3,+        neat-interpolation >=0.5.1.4,         open-browser >=0.2.1.0,         optparse-applicative >=0.18.1.0,-        pantry >=0.9.2,-        path >=0.9.2,+        pantry >=0.9.3.1,+        path >=0.9.5,         path-io >=1.8.1,         persistent >=2.14.0.0 && <2.15,-        persistent-sqlite >=2.13.1.1,+        persistent-sqlite >=2.13.3.0,         pretty >=1.1.3.6,         process >=1.6.13.2,         project-template >=0.2.1.0,         random >=1.2.1.1,         rio >=0.1.22.0,-        rio-prettyprint >=0.1.7.0,-        split >=0.2.3.5,+        rio-prettyprint >=0.1.8.0,+        split >=0.2.5,         stm >=2.5.1.0,         tar >=0.5.1.1,-        template-haskell >=2.19.0.0,+        template-haskell >=2.20.0.0,         text >=2.0.2,         time >=1.12.2,-        transformers >=0.5.6.2,-        unix-compat >=0.7,-        unordered-containers >=0.2.19.1,-        vector >=0.13.0.0,+        transformers >=0.6.1.0,+        unix-compat >=0.7.1,+        unordered-containers >=0.2.20,+        vector >=0.13.1.0,         yaml >=0.11.11.2,         zlib >=0.6.3.0      if os(windows)         cpp-options:   -DWINDOWS-        build-depends: Win32 >=2.12.0.1+        build-depends: Win32 >=2.13.3.0      else         build-tool-depends: hsc2hs:hsc2hs@@ -423,6 +445,12 @@     else         cpp-options: -DSTACK_DEVELOPER_MODE_DEFAULT=False +    if flag(disable-stack-upload)+        cpp-options: -DSTACK_DISABLE_STACK_UPLOAD=True++    else+        cpp-options: -DSTACK_DISABLE_STACK_UPLOAD=False+     if os(windows)         hs-source-dirs: src/windows/         other-modules:@@ -464,73 +492,73 @@     build-depends:         Cabal >=3.8.1.0,         aeson >=2.0.3.0,-        aeson-warning-parser >=0.1.0,-        ansi-terminal >=1.0,-        array >=0.5.4.0,-        async >=2.2.4,+        aeson-warning-parser >=0.1.1,+        ansi-terminal >=1.0.2,+        array >=0.5.6.0,+        async >=2.2.5,         attoparsec >=0.14.4,-        base >=4.14.3.0 && <5,+        base >=4.16.0.0 && <5,         base64-bytestring >=1.2.1.0,-        bytestring >=0.11.5.2,+        bytestring >=0.11.5.3,         casa-client >=0.0.2,         companion >=0.1.0,         conduit >=1.3.5,         conduit-extra >=1.3.6,         containers >=0.6.7,-        crypton >=0.33,-        directory >=1.3.7.1,+        crypton >=0.34,+        directory >=1.3.8.1,         echo >=0.1.4,-        exceptions >=0.10.5,+        exceptions >=0.10.7,         extra >=1.7.14,         file-embed >=0.0.15.0,         filelock >=0.1.1.7,-        filepath >=1.4.2.2,+        filepath >=1.4.200.1,         fsnotify >=0.4.1,         generic-deriving >=1.14.5,-        ghc-boot >=9.4.7,-        hi-file-parser >=0.1.4.0,+        ghc-boot >=9.6.4,+        hi-file-parser >=0.1.6.0,         hpack >=0.36.0,-        hpc >=0.6.1.0,-        http-client >=0.7.14,+        hpc >=0.6.2.0,+        http-client >=0.7.16,         http-client-tls >=0.3.6.2,-        http-conduit >=2.3.8.1,+        http-conduit >=2.3.8.3,         http-download >=0.2.1.0,-        http-types >=0.12.3,+        http-types >=0.12.4,         memory >=0.18.0,         microlens >=0.4.13.1,-        mtl >=2.2.2,+        mtl >=2.3.1,         mustache >=2.4.2,-        neat-interpolation >=0.5.1.3,+        neat-interpolation >=0.5.1.4,         open-browser >=0.2.1.0,         optparse-applicative >=0.18.1.0,-        pantry >=0.9.2,-        path >=0.9.2,+        pantry >=0.9.3.1,+        path >=0.9.5,         path-io >=1.8.1,         persistent >=2.14.0.0 && <2.15,-        persistent-sqlite >=2.13.1.1,+        persistent-sqlite >=2.13.3.0,         pretty >=1.1.3.6,         process >=1.6.13.2,         project-template >=0.2.1.0,         random >=1.2.1.1,         rio >=0.1.22.0,-        rio-prettyprint >=0.1.7.0,-        split >=0.2.3.5,+        rio-prettyprint >=0.1.8.0,+        split >=0.2.5,         stack,         stm >=2.5.1.0,         tar >=0.5.1.1,-        template-haskell >=2.19.0.0,+        template-haskell >=2.20.0.0,         text >=2.0.2,         time >=1.12.2,-        transformers >=0.5.6.2,-        unix-compat >=0.7,-        unordered-containers >=0.2.19.1,-        vector >=0.13.0.0,+        transformers >=0.6.1.0,+        unix-compat >=0.7.1,+        unordered-containers >=0.2.20,+        vector >=0.13.1.0,         yaml >=0.11.11.2,         zlib >=0.6.3.0      if os(windows)         cpp-options:   -DWINDOWS-        build-depends: Win32 >=2.12.0.1+        build-depends: Win32 >=2.13.3.0      else         build-tool-depends: hsc2hs:hsc2hs@@ -545,6 +573,12 @@     else         cpp-options: -DSTACK_DEVELOPER_MODE_DEFAULT=False +    if flag(disable-stack-upload)+        cpp-options: -DSTACK_DISABLE_STACK_UPLOAD=True++    else+        cpp-options: -DSTACK_DISABLE_STACK_UPLOAD=False+     if flag(static)         ld-options: -static -pthread @@ -565,74 +599,74 @@     build-depends:         Cabal >=3.8.1.0,         aeson >=2.0.3.0,-        aeson-warning-parser >=0.1.0,-        ansi-terminal >=1.0,-        array >=0.5.4.0,-        async >=2.2.4,+        aeson-warning-parser >=0.1.1,+        ansi-terminal >=1.0.2,+        array >=0.5.6.0,+        async >=2.2.5,         attoparsec >=0.14.4,-        base >=4.14.3.0 && <5,+        base >=4.16.0.0 && <5,         base64-bytestring >=1.2.1.0,-        bytestring >=0.11.5.2,+        bytestring >=0.11.5.3,         casa-client >=0.0.2,         companion >=0.1.0,         conduit >=1.3.5,         conduit-extra >=1.3.6,         containers >=0.6.7,-        crypton >=0.33,-        directory >=1.3.7.1,+        crypton >=0.34,+        directory >=1.3.8.1,         echo >=0.1.4,-        exceptions >=0.10.5,+        exceptions >=0.10.7,         extra >=1.7.14,         file-embed >=0.0.15.0,         filelock >=0.1.1.7,-        filepath >=1.4.2.2,+        filepath >=1.4.200.1,         fsnotify >=0.4.1,         generic-deriving >=1.14.5,-        ghc-boot >=9.4.7,-        hi-file-parser >=0.1.4.0,+        ghc-boot >=9.6.4,+        hi-file-parser >=0.1.6.0,         hpack >=0.36.0,-        hpc >=0.6.1.0,-        hspec >=2.10.10,-        http-client >=0.7.14,+        hpc >=0.6.2.0,+        hspec >=2.11.7,+        http-client >=0.7.16,         http-client-tls >=0.3.6.2,-        http-conduit >=2.3.8.1,+        http-conduit >=2.3.8.3,         http-download >=0.2.1.0,-        http-types >=0.12.3,+        http-types >=0.12.4,         memory >=0.18.0,         microlens >=0.4.13.1,-        mtl >=2.2.2,+        mtl >=2.3.1,         mustache >=2.4.2,-        neat-interpolation >=0.5.1.3,+        neat-interpolation >=0.5.1.4,         open-browser >=0.2.1.0,         optparse-applicative >=0.18.1.0,-        optparse-generic >=1.5.1,-        pantry >=0.9.2,-        path >=0.9.2,+        optparse-generic >=1.5.2,+        pantry >=0.9.3.1,+        path >=0.9.5,         path-io >=1.8.1,         persistent >=2.14.0.0 && <2.15,-        persistent-sqlite >=2.13.1.1,+        persistent-sqlite >=2.13.3.0,         pretty >=1.1.3.6,         process >=1.6.13.2,         project-template >=0.2.1.0,         random >=1.2.1.1,         rio >=0.1.22.0,-        rio-prettyprint >=0.1.7.0,-        split >=0.2.3.5,+        rio-prettyprint >=0.1.8.0,+        split >=0.2.5,         stm >=2.5.1.0,         tar >=0.5.1.1,-        template-haskell >=2.19.0.0,+        template-haskell >=2.20.0.0,         text >=2.0.2,         time >=1.12.2,-        transformers >=0.5.6.2,-        unix-compat >=0.7,-        unordered-containers >=0.2.19.1,-        vector >=0.13.0.0,+        transformers >=0.6.1.0,+        unix-compat >=0.7.1,+        unordered-containers >=0.2.20,+        vector >=0.13.1.0,         yaml >=0.11.11.2,         zlib >=0.6.3.0      if os(windows)         cpp-options:   -DWINDOWS-        build-depends: Win32 >=2.12.0.1+        build-depends: Win32 >=2.13.3.0      else         build-tool-depends: hsc2hs:hsc2hs@@ -647,6 +681,12 @@     else         cpp-options: -DSTACK_DEVELOPER_MODE_DEFAULT=False +    if flag(disable-stack-upload)+        cpp-options: -DSTACK_DISABLE_STACK_UPLOAD=True++    else+        cpp-options: -DSTACK_DISABLE_STACK_UPLOAD=False+     if !flag(integration-tests)         buildable: False @@ -684,75 +724,75 @@         Cabal >=3.8.1.0,         QuickCheck >=2.14.3,         aeson >=2.0.3.0,-        aeson-warning-parser >=0.1.0,-        ansi-terminal >=1.0,-        array >=0.5.4.0,-        async >=2.2.4,+        aeson-warning-parser >=0.1.1,+        ansi-terminal >=1.0.2,+        array >=0.5.6.0,+        async >=2.2.5,         attoparsec >=0.14.4,-        base >=4.14.3.0 && <5,+        base >=4.16.0.0 && <5,         base64-bytestring >=1.2.1.0,-        bytestring >=0.11.5.2,+        bytestring >=0.11.5.3,         casa-client >=0.0.2,         companion >=0.1.0,         conduit >=1.3.5,         conduit-extra >=1.3.6,         containers >=0.6.7,-        crypton >=0.33,-        directory >=1.3.7.1,+        crypton >=0.34,+        directory >=1.3.8.1,         echo >=0.1.4,-        exceptions >=0.10.5,+        exceptions >=0.10.7,         extra >=1.7.14,         file-embed >=0.0.15.0,         filelock >=0.1.1.7,-        filepath >=1.4.2.2,+        filepath >=1.4.200.1,         fsnotify >=0.4.1,         generic-deriving >=1.14.5,-        ghc-boot >=9.4.7,-        hi-file-parser >=0.1.4.0,+        ghc-boot >=9.6.4,+        hi-file-parser >=0.1.6.0,         hpack >=0.36.0,-        hpc >=0.6.1.0,-        hspec >=2.10.10,-        http-client >=0.7.14,+        hpc >=0.6.2.0,+        hspec >=2.11.7,+        http-client >=0.7.16,         http-client-tls >=0.3.6.2,-        http-conduit >=2.3.8.1,+        http-conduit >=2.3.8.3,         http-download >=0.2.1.0,-        http-types >=0.12.3,+        http-types >=0.12.4,         memory >=0.18.0,         microlens >=0.4.13.1,-        mtl >=2.2.2,+        mtl >=2.3.1,         mustache >=2.4.2,-        neat-interpolation >=0.5.1.3,+        neat-interpolation >=0.5.1.4,         open-browser >=0.2.1.0,         optparse-applicative >=0.18.1.0,-        pantry >=0.9.2,-        path >=0.9.2,+        pantry >=0.9.3.1,+        path >=0.9.5,         path-io >=1.8.1,         persistent >=2.14.0.0 && <2.15,-        persistent-sqlite >=2.13.1.1,+        persistent-sqlite >=2.13.3.0,         pretty >=1.1.3.6,         process >=1.6.13.2,         project-template >=0.2.1.0,         random >=1.2.1.1,         raw-strings-qq >=1.1,         rio >=0.1.22.0,-        rio-prettyprint >=0.1.7.0,-        split >=0.2.3.5,+        rio-prettyprint >=0.1.8.0,+        split >=0.2.5,         stack,         stm >=2.5.1.0,         tar >=0.5.1.1,-        template-haskell >=2.19.0.0,+        template-haskell >=2.20.0.0,         text >=2.0.2,         time >=1.12.2,-        transformers >=0.5.6.2,-        unix-compat >=0.7,-        unordered-containers >=0.2.19.1,-        vector >=0.13.0.0,+        transformers >=0.6.1.0,+        unix-compat >=0.7.1,+        unordered-containers >=0.2.20,+        vector >=0.13.1.0,         yaml >=0.11.11.2,         zlib >=0.6.3.0      if os(windows)         cpp-options:   -DWINDOWS-        build-depends: Win32 >=2.12.0.1+        build-depends: Win32 >=2.13.3.0      else         build-tool-depends: hsc2hs:hsc2hs@@ -766,6 +806,12 @@      else         cpp-options: -DSTACK_DEVELOPER_MODE_DEFAULT=False++    if flag(disable-stack-upload)+        cpp-options: -DSTACK_DISABLE_STACK_UPLOAD=True++    else+        cpp-options: -DSTACK_DISABLE_STACK_UPLOAD=False      if os(windows)         hs-source-dirs: tests/unit/windows/
stack.yaml view
@@ -1,32 +1,15 @@-resolver: lts-21.13 # GHC 9.4.7
+snapshot: lts-22.7 # GHC 9.6.4
 
 extra-deps:
-- aeson-warning-parser-0.1.0@sha256:f2c1c42b73aa35d352060abcbb867c410cbbf57d0cb0fed607bcd1e2a74954ad,1308
-- ansi-terminal-1.0@sha256:640ffecfd95471388d939fcacb57bdc0cef15f0457746c234a12cdd5a6c6d1e8,2706
-# Required because ansi-wl-pprint-0.6.9 specifies ansi-terminal < 0.12. See:
-# https://github.com/ekmett/ansi-wl-pprint/issues/29
-- ansi-wl-pprint-1.0.2@sha256:b817853b5310b8e7847469847608b664c3e75b4b30c332f2cb8c0d00751ef9c1,1915
-- companion-0.1.0@sha256:99f6de52c832d433639232a6d77d33abbca3b3037e49b7db6242fb9f569a8a2b,1093
-- crypton-0.33@sha256:5e92f29b9b7104d91fcdda1dec9400c9ad1f1791c231cc41ceebd783fb517dee,18202
-- crypton-connection-0.3.1@sha256:4d0958537197956b536ea91718b1749949757022532f50b8f683290056a19021,1581
-- crypton-x509-1.7.6@sha256:c567657a705b6d6521f9dd2de999bf530d618ec00f3b939df76a41fb0fe94281,2339
-- crypton-x509-store-1.6.9@sha256:422b9b9f87a7382c66385d047615b16fc86a68c08ea22b1e0117c143a2d44050,1750
-- crypton-x509-system-1.6.7@sha256:023ed573d82983bc473a37a89e0434a085b413be9f68d07e085361056afd4637,1532
-- crypton-x509-validation-1.6.12@sha256:85989721b64be4b90de9f66ef641c26f57575cffed1a50d707065fb60176f386,2227
-# lts-21.13 specifies hpack-0.35.2
-- hpack-0.36.0@sha256:c2daa6556afc57367a5d1dbd878bf515d442d201e24b27473051359abd47ed08,5187
-- http-client-tls-0.3.6.3@sha256:a5909ce412ee65c141b8547f8fe22236f175186c95c708e86a46b5547394f910,2046
-- http-download-0.2.1.0@sha256:a97863e96f7d44efc3d0e3061db7fe2540b8374ca44ae90d0b56040140cb7506,1716
-- optparse-applicative-0.18.1.0@sha256:b4cf8d9018e5e67cb1f14edb5130b6d05ad8bc1b5f6bd4efaa6ec0b7f28f559d,5132
-- optparse-generic-1.5.1@sha256:c65a7d3429feedf870f5a9f7f0d2aaf75609888b52449f85f22871b5f5a7e95f,2204
-- pantry-0.9.2@sha256:e1c5444d1b4003435d860853abd21e91e5fc337f2b2e2c8c992a2bac04712dc0,7650
-- static-bytes-0.1.0@sha256:35dbf30f617baa0151682c97687042516be07872a39984f9fe31f78125b962bf,1627
-- tar-conduit-0.4.0@sha256:f333649770f5ec42a83a93b0d424cf6bb895d80dfbee05a54340395f81d036ae,3126
-- tls-1.9.0@sha256:8ad332dc0224decb1b137bf6c9678b4f786487b9aaa5c9068cd3ad19d42c39a7,5571
+# Cabal is pruned because process is a GHC boot package, and has to be specified
+# again.
+- Cabal-3.10.1.0@sha256:6d11adf7847d9734e7b02785ff831b5a0d11536bfbcefd6634b2b08411c63c94,12316
+# GHC 9.6.4 comes with process-1.6.17.0, which can segfault on macOS.
+- process-1.6.18.0@sha256:cd0a3e0376b5a8525983d3131a31e52f9ffefc278ce635eec45a9d3987b8be3e,3025
 
 docker:
   enable: false
-  repo: glcr.b-data.ch/ghc/ghc-musl:9.4.7
+  repo: quay.io/benz0li/ghc-musl:9.6.4
 
 nix:
   # --nix on the command-line to enable.
@@ -37,8 +20,5 @@ flags:
   hackage-security:
     cabal-syntax: true
-  # GHC 9.4.7's boot library is Win32-2.12.0.1
-  mintty:
-    win32-2-13-1: false
   stack:
     developer-mode: true
tests/integration/lib/StackTest.hs view
@@ -363,7 +363,7 @@ -- the main @stack.yaml@.
 --
 defaultResolverArg :: String
-defaultResolverArg = "--resolver=lts-21.13"
+defaultResolverArg = "--snapshot=lts-22.7"
 
 -- | Remove a file and ignore any warnings about missing files.
 removeFileIgnore :: HasCallStack => FilePath -> IO ()
tests/unit/Stack/Config/DockerSpec.hs view
@@ -14,7 +14,7 @@ spec :: Spec
 spec = do
   describe "addDefaultTag" $ do
-    it "succeeds fails no resolver" $ addDefaultTag "foo/bar" Nothing Nothing `shouldBe` Nothing
+    it "succeeds fails no snapshot resolver" $ addDefaultTag "foo/bar" Nothing Nothing `shouldBe` Nothing
     it "succeeds on LTS" $
       addDefaultTag
         "foo/bar"
tests/unit/Stack/ConfigSpec.hs view
@@ -1,6 +1,7 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
-{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 module Stack.ConfigSpec
   ( sampleConfig
@@ -28,9 +29,10 @@ import           Stack.Runners ( withBuildConfig, withRunnerGlobal )
 import           Stack.Types.BuildConfig ( BuildConfig (..), projectRootL )
 import           Stack.Types.BuildOpts
-                   ( BenchmarkOpts (..), BuildOpts (..), CabalVerbosity (..)
+                   ( BenchmarkOpts (..), BuildOpts (..), HaddockOpts (..)
                    , TestOpts (..)
                    )
+import           Stack.Types.BuildOptsMonoid ( CabalVerbosity (..), ProgressBarFormat (NoBar) )
 import           Stack.Types.Config ( Config (..) )
 import           Stack.Types.ConfigMonoid
                    ( ConfigMonoid (..), parseConfigMonoid )
@@ -51,23 +53,33 @@ 
 sampleConfig :: String
 sampleConfig =
-  "resolver: lts-19.22\n" ++
+  "snapshot: lts-22.7\n" ++
   "packages: ['.']\n"
 
 buildOptsConfig :: String
 buildOptsConfig =
-  "resolver: lts-19.22\n" ++
+  "snapshot: lts-22.7\n" ++
   "packages: ['.']\n" ++
   "build:\n" ++
   "  library-profiling: true\n" ++
   "  executable-profiling: true\n" ++
+  "  library-stripping: false\n" ++
+  "  executable-stripping: false\n" ++
   "  haddock: true\n" ++
+  "  haddock-arguments:\n" ++
+  "    haddock-args:\n" ++
+  "    - \"--css=/home/user/my-css\"\n" ++
+  "  open-haddocks: true\n" ++
   "  haddock-deps: true\n" ++
+  "  haddock-internal: true\n" ++
+  "  haddock-hyperlink-source: false\n" ++
+  "  haddock-for-hackage: false\n" ++
   "  copy-bins: true\n" ++
+  "  copy-compiler-tool: true\n" ++
   "  prefetch: true\n" ++
-  "  force-dirty: true\n" ++
   "  keep-going: true\n" ++
   "  keep-tmp-files: true\n" ++
+  "  force-dirty: true\n" ++
   "  test: true\n" ++
   "  test-arguments:\n" ++
   "    rerun-tests: true\n" ++
@@ -79,28 +91,47 @@   "    benchmark-arguments: -O2\n" ++
   "    no-run-benchmarks: true\n" ++
   "  reconfigure: true\n" ++
-  "  cabal-verbose: true\n"
+  "  cabal-verbosity: verbose\n" ++
+  "  cabal-verbose: true\n" ++
+  "  split-objs: true\n" ++
+  "  skip-components: ['my-test']\n" ++
+  "  interleaved-output: false\n" ++
+  "  progress-bar: none\n" ++
+  "  ddump-dir: my-ddump-dir\n"
 
+buildOptsHaddockForHackageConfig :: String
+buildOptsHaddockForHackageConfig =
+  "snapshot: lts-22.7\n" ++
+  "packages: ['.']\n" ++
+  "build:\n" ++
+  "  haddock: true\n" ++
+  "  open-haddocks: true\n" ++
+  "  haddock-deps: true\n" ++
+  "  haddock-internal: true\n" ++
+  "  haddock-hyperlink-source: false\n" ++
+  "  haddock-for-hackage: true\n" ++
+  "  force-dirty: false\n"
+
 hpackConfig :: String
 hpackConfig =
-  "resolver: lts-19.22\n" ++
+  "snapshot: lts-22.7\n" ++
   "with-hpack: /usr/local/bin/hpack\n" ++
   "packages: ['.']\n"
 
 resolverConfig :: String
 resolverConfig =
-  "resolver: lts-19.22\n" ++
+  "resolver: lts-22.7\n" ++
   "packages: ['.']\n"
 
 snapshotConfig :: String
 snapshotConfig =
-  "snapshot: lts-19.22\n" ++
+  "snapshot: lts-22.7\n" ++
   "packages: ['.']\n"
 
 resolverSnapshotConfig :: String
 resolverSnapshotConfig =
-  "resolver: lts-19.22\n" ++
-  "snapshot: lts-19.22\n" ++
+  "resolver: lts-22.7\n" ++
+  "snapshot: lts-22.7\n" ++
   "packages: ['.']\n"
 
 stackDotYaml :: Path Rel File
@@ -132,7 +163,7 @@   describe "parseProjectAndConfigMonoid" $ do
     let loadProject' fp inner = do
           globalOpts <- globalOptsFromMonoid False mempty
-          withRunnerGlobal globalOpts { globalLogLevel = logLevel } $ do
+          withRunnerGlobal globalOpts { logLevel = logLevel } $ do
               iopc <- loadConfigYaml (
                 parseProjectAndConfigMonoid (parent fp)
                 ) fp
@@ -149,12 +180,12 @@           loadProject' yamlAbs inner
 
     it "parses snapshot using 'resolver'" $ inTempDir $ do
-      loadProject resolverConfig $ \Project{..} ->
-        projectResolver `shouldBe` RSLSynonym (LTS 19 22)
+      loadProject resolverConfig $ \project ->
+        project.resolver `shouldBe` RSLSynonym (LTS 22 7)
 
     it "parses snapshot using 'snapshot'" $ inTempDir $ do
-      loadProject snapshotConfig $ \Project{..} ->
-        projectResolver `shouldBe` RSLSynonym (LTS 19 22)
+      loadProject snapshotConfig $ \project ->
+        project.resolver `shouldBe` RSLSynonym (LTS 22 7)
 
     it "throws if both 'resolver' and 'snapshot' are present" $ inTempDir $ do
       loadProject resolverSnapshotConfig (const (pure ()))
@@ -163,7 +194,7 @@   describe "loadConfig" $ do
     let loadConfig' inner = do
           globalOpts <- globalOptsFromMonoid False mempty
-          withRunnerGlobal globalOpts { globalLogLevel = logLevel } $
+          withRunnerGlobal globalOpts { logLevel = logLevel } $
             loadConfig inner
     -- TODO(danburton): make sure parent dirs also don't have config file
     it "works even if no config file exists" $ example $
@@ -188,33 +219,62 @@         liftIO $ configOverrideHpack config `shouldBe` HpackBundled
 
     it "parses build config options" $ inTempDir $ do
-     writeFile (toFilePath stackDotYaml) buildOptsConfig
-     loadConfig' $ \config -> liftIO $ do
-      let BuildOpts{..} = configBuild  config
-      boptsLibProfile `shouldBe` True
-      boptsExeProfile `shouldBe` True
-      boptsHaddock `shouldBe` True
-      boptsHaddockDeps `shouldBe` Just True
-      boptsInstallExes `shouldBe` True
-      boptsPreFetch `shouldBe` True
-      boptsKeepGoing `shouldBe` Just True
-      boptsKeepTmpFiles `shouldBe` True
-      boptsForceDirty `shouldBe` True
-      boptsTests `shouldBe` True
-      boptsTestOpts `shouldBe` TestOpts { toRerunTests = True
-                                        , toAdditionalArgs = ["-fprof"]
-                                        , toCoverage = True
-                                        , toDisableRun = True
-                                        , toMaximumTimeSeconds = Nothing
-                                        , toAllowStdin = True
-                                        }
-      boptsBenchmarks `shouldBe` True
-      boptsBenchmarkOpts `shouldBe` BenchmarkOpts { beoAdditionalArgs = Just "-O2"
-                                                  , beoDisableRun = True
-                                                  }
-      boptsReconfigure `shouldBe` True
-      boptsCabalVerbose `shouldBe` CabalVerbosity verbose
+      writeFile (toFilePath stackDotYaml) buildOptsConfig
+      loadConfig' $ \config -> liftIO $ do
+        let bopts = config.build
+        bopts.libProfile `shouldBe` True
+        bopts.exeProfile `shouldBe` True
+        bopts.libStrip `shouldBe` False
+        bopts.exeStrip `shouldBe` False
+        bopts.buildHaddocks `shouldBe` True
+        bopts.haddockOpts `shouldBe` HaddockOpts
+          { additionalArgs = ["--css=/home/user/my-css"]
+          }
+        bopts.openHaddocks `shouldBe` True
+        bopts.haddockDeps `shouldBe` Just True
+        bopts.haddockInternal `shouldBe` True
+        bopts.haddockHyperlinkSource `shouldBe` False
+        bopts.haddockForHackage `shouldBe` False
+        bopts.installExes `shouldBe` True
+        bopts.installCompilerTool `shouldBe` True
+        bopts.preFetch `shouldBe` True
+        bopts.keepGoing `shouldBe` Just True
+        bopts.keepTmpFiles `shouldBe` True
+        bopts.forceDirty `shouldBe` True
+        bopts.tests `shouldBe` True
+        bopts.testOpts `shouldBe` TestOpts
+          { rerunTests = True
+          , additionalArgs = ["-fprof"]
+          , coverage = True
+          , disableRun = True
+          , maximumTimeSeconds = Nothing
+          , allowStdin = True
+          }
+        bopts.benchmarks `shouldBe` True
+        bopts.benchmarkOpts `shouldBe` BenchmarkOpts
+           { additionalArgs = Just "-O2"
+           , disableRun = True
+           }
+        bopts.reconfigure `shouldBe` True
+        bopts.cabalVerbose `shouldBe` CabalVerbosity verbose
+        bopts.splitObjs `shouldBe` True
+        bopts.skipComponents `shouldBe` ["my-test"]
+        bopts.interleavedOutput `shouldBe` False
+        bopts.progressBar `shouldBe` NoBar
+        bopts.ddumpDir `shouldBe` Just "my-ddump-dir"
 
+    it "parses build config options with haddock-for-hackage" $ inTempDir $ do
+      writeFile (toFilePath stackDotYaml) buildOptsHaddockForHackageConfig
+      loadConfig' $ \config -> liftIO $ do
+        let bopts = config.build
+        bopts.buildHaddocks `shouldBe` True
+        bopts.openHaddocks `shouldBe` False
+        bopts.haddockDeps `shouldBe` Nothing
+        bopts.haddockInternal `shouldBe` False
+        bopts.haddockHyperlinkSource `shouldBe` True
+        bopts.haddockForHackage `shouldBe` True
+        bopts.forceDirty `shouldBe` True
+
     it "finds the config file in a parent directory" $ inTempDir $ do
       writeFile "package.yaml" "name: foo"
       writeFile (toFilePath stackDotYaml) sampleConfig
@@ -231,29 +291,34 @@         let stackYamlFp = toFilePath (dir </> stackDotYaml)
         writeFile stackYamlFp sampleConfig
         writeFile (toFilePath dir ++ "/package.yaml") "name: foo"
-        withEnvVar "STACK_YAML" stackYamlFp $ loadConfig' $ \config -> liftIO $ do
-          BuildConfig{..} <- runRIO config $ withBuildConfig ask
-          bcStackYaml `shouldBe` dir </> stackDotYaml
-          parent bcStackYaml `shouldBe` dir
+        withEnvVar "STACK_YAML" stackYamlFp $
+          loadConfig' $ \config -> liftIO $ do
+            bc <- runRIO config $ withBuildConfig ask
+            bc.stackYaml `shouldBe` dir </> stackDotYaml
+            parent bc.stackYaml `shouldBe` dir
 
     it "STACK_YAML can be relative" $ inTempDir $ do
         parentDir <- getCurrentDirectory >>= parseAbsDir
         let childRel = either impureThrow id (parseRelDir "child")
-            yamlRel = childRel </> either impureThrow id (parseRelFile "some-other-name.config")
+            yamlRel =
+              childRel </> either impureThrow id (parseRelFile "some-other-name.config")
             yamlAbs = parentDir </> yamlRel
-            packageYaml = childRel </> either impureThrow id (parseRelFile "package.yaml")
+            packageYaml =
+              childRel </> either impureThrow id (parseRelFile "package.yaml")
         createDirectoryIfMissing True $ toFilePath $ parent yamlAbs
-        writeFile (toFilePath yamlAbs) "resolver: ghc-9.0"
+        writeFile (toFilePath yamlAbs) "snapshot: ghc-9.6.4"
         writeFile (toFilePath packageYaml) "name: foo"
-        withEnvVar "STACK_YAML" (toFilePath yamlRel) $ loadConfig' $ \config -> liftIO $ do
-            BuildConfig{..} <- runRIO config $ withBuildConfig ask
-            bcStackYaml `shouldBe` yamlAbs
+        withEnvVar "STACK_YAML" (toFilePath yamlRel) $
+          loadConfig' $ \config -> liftIO $ do
+            bc <- runRIO config $ withBuildConfig ask
+            bc.stackYaml `shouldBe` yamlAbs
 
   describe "defaultConfigYaml" $
     it "is parseable" $ \_ -> do
-        curDir <- getCurrentDir
-        let parsed :: Either String (Either String (WithJSONWarnings ConfigMonoid))
-            parsed = parseEither (parseConfigMonoid curDir) <$> left show (decodeEither' defaultConfigYaml)
-        case parsed of
-            Right (Right _) -> pure () :: IO ()
-            _ -> fail "Failed to parse default config yaml"
+      curDir <- getCurrentDir
+      let parsed :: Either String (Either String (WithJSONWarnings ConfigMonoid))
+          parsed = parseEither
+            (parseConfigMonoid curDir) <$> left show (decodeEither' defaultConfigYaml)
+      case parsed of
+        Right (Right _) -> pure () :: IO ()
+        _ -> fail "Failed to parse default config yaml"
tests/unit/Stack/DotSpec.hs view
@@ -15,8 +15,9 @@ import qualified Data.Set as Set
 import           Distribution.License ( License (BSD3) )
 import qualified RIO.Text as T
-import           Stack.Dot ( DotPayload (..), pruneGraph, resolveDependencies )
+import           Stack.DependencyGraph ( pruneGraph, resolveDependencies )
 import           Stack.Prelude hiding ( pkgName )
+import           Stack.Types.DependencyTree ( DotPayload (..) )
 import           Test.Hspec ( Spec, describe, it, shouldBe )
 import           Test.Hspec.QuickCheck ( prop )
 import           Test.QuickCheck ( Gen, choose, forAll )
tests/unit/Stack/Ghci/ScriptSpec.hs view
@@ -10,14 +10,13 @@ 
 import qualified Data.Set as S
 import           Distribution.ModuleName
-import           Test.Hspec
-import qualified System.FilePath as FP
-import           Stack.Ghci.FakePaths
-import           Stack.Prelude hiding (fromString)
 import           Path
-import           Path.Extra (pathToLazyByteString)
-
+import           Path.Extra ( pathToLazyByteString )
+import           Stack.Ghci.FakePaths
 import           Stack.Ghci.Script
+import           Stack.Prelude hiding ( fromString )
+import qualified System.FilePath as FP
+import           Test.Hspec
 
 spec :: Spec
 spec = do
@@ -26,11 +25,10 @@ 
       describe "script" $ do
         it "should separate commands with a newline" $ do
-          let dir = $(mkAbsDir $ defaultDrive FP.</> "src" FP.</> "package-a")
-              script = cmdCdGhc dir
-                    <> cmdAdd [Left (fromString "Lib.A")]
+          let script =    cmdAdd [Left (fromString "Lib.A")]
+                       <> cmdAdd [Left (fromString "Lib.B")]
           scriptToLazyByteString script `shouldBe`
-            ":cd-ghc " <> pathToLazyByteString dir <> "\n:add Lib.A\n"
+            ":add Lib.A\n:add Lib.B\n"
 
       describe ":add" $ do
         it "should not render empty add commands" $ do
@@ -47,13 +45,6 @@               script = cmdAdd (S.fromList [Right file])
           scriptToLazyByteString script `shouldBe`
             ":add " <> pathToLazyByteString file <> "\n"
-
-      describe ":cd-ghc" $ do
-        it "should render a full absolute path" $ do
-          let dir = $(mkAbsDir $ defaultDrive FP.</> "Users" FP.</> "someone" FP.</> "src" FP.</> "project" FP.</> "package-a")
-              script = cmdCdGhc dir
-          scriptToLazyByteString script `shouldBe`
-            ":cd-ghc " <> pathToLazyByteString dir <> "\n"
 
       describe ":module" $ do
         it "should render empty module as ':module +'" $ do
tests/unit/Stack/GhciSpec.hs view
@@ -2,7 +2,7 @@ -- {-# LANGUAGE QuasiQuotes #-}
 -- {-# LANGUAGE TemplateHaskell #-}
 
--- | Test suite for GHCi like applications including both GHCi and Intero.
+-- | Test suite for GHCi-like applications including GHCi.
 module Stack.GhciSpec
   ( spec
   ) where
tests/unit/Stack/LockSpec.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE QuasiQuotes         #-}
+{-# LANGUAGE OverloadedRecordDot #-}
 {-# LANGUAGE OverloadedStrings   #-}
 
 module Stack.LockSpec
@@ -47,7 +48,7 @@ 
 spec :: Spec
 spec = do
-    it "parses lock file (empty with GHC resolver)" $ do
+    it "parses lock file (empty with GHC snapshot)" $ do
         let lockFile :: ByteString
             lockFile =
                 [r|#some
@@ -58,9 +59,9 @@     compiler: ghc-8.6.5
 packages: []
 |]
-        pkgImm <- lckPkgImmutableLocations <$> decodeLocked lockFile
+        pkgImm <- (.pkgImmutableLocations) <$> decodeLocked lockFile
         pkgImm `shouldBe` []
-    it "parses lock file (empty with LTS resolver)" $ do
+    it "parses lock file (empty with LTS snapshot)" $ do
         let lockFile :: ByteString
             lockFile =
                 [r|#some
@@ -76,7 +77,7 @@     compiler: ghc-8.6.5
 packages: []
 |]
-        pkgImm <- lckPkgImmutableLocations <$> decodeLocked lockFile
+        pkgImm <- (.pkgImmutableLocations) <$> decodeLocked lockFile
         pkgImm `shouldBe` []
     it "parses lock file (LTS, wai + warp)" $ do
         let lockFile :: ByteString
@@ -120,7 +121,7 @@       sha256: f808e075811b002563d24c393ce115be826bb66a317d38da22c513ee42b7443a
     commit: d11d63f1a6a92db8c637a8d33e7953ce6194a3e0
 |]
-        pkgImm <- lckPkgImmutableLocations <$> decodeLocked lockFile
+        pkgImm <- (.pkgImmutableLocations) <$> decodeLocked lockFile
         let waiSubdirRepo subdir =
               Repo { repoType = RepoGit
                    , repoUrl = "https://github.com/yesodweb/wai.git"
tests/unit/Stack/NixSpec.hs view
@@ -1,5 +1,7 @@-{-# LANGUAGE NoImplicitPrelude #-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NoImplicitPrelude     #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE OverloadedRecordDot   #-}
+{-# LANGUAGE OverloadedStrings     #-}
 
 module Stack.NixSpec
   ( sampleConfigNixEnabled
@@ -21,7 +23,7 @@ import           Stack.Runners ( withRunnerGlobal )
 import           Stack.Types.Config ( Config (..) )
 import           Stack.Types.ConfigMonoid ( ConfigMonoid (..) )
-import           Stack.Types.GlobalOpts ( GlobalOpts (..) )
+import qualified Stack.Types.GlobalOpts as GlobalOpts ( GlobalOpts (..) )
 import           Stack.Types.GlobalOptsMonoid ( GlobalOptsMonoid (..) )
 import           Stack.Types.Nix ( NixOpts (..) )
 import           System.Directory ( getCurrentDirectory, setCurrentDirectory )
@@ -30,7 +32,7 @@ 
 sampleConfigNixEnabled :: String
 sampleConfigNixEnabled =
-  "resolver: lts-19.22\n" ++
+  "snapshot: lts-19.22\n" ++
   "packages: ['.']\n" ++
   "system-ghc: true\n" ++
   "nix:\n" ++
@@ -39,7 +41,7 @@ 
 sampleConfigNixDisabled :: String
 sampleConfigNixDisabled =
-  "resolver: lts-19.22\n" ++
+  "snapshot: lts-19.22\n" ++
   "packages: ['.']\n" ++
   "nix:\n" ++
   "   enable: False"
@@ -51,8 +53,8 @@ spec = beforeAll setup $ do
   let loadConfig' :: ConfigMonoid -> (Config -> IO ()) -> IO ()
       loadConfig' cmdLineArgs inner = do
-        globalOpts <- globalOptsFromMonoid False mempty { globalMonoidConfigMonoid = cmdLineArgs }
-        withRunnerGlobal globalOpts { globalLogLevel = LevelOther "silent" } $
+        globalOpts <- globalOptsFromMonoid False mempty { configMonoid = cmdLineArgs }
+        withRunnerGlobal globalOpts { GlobalOpts.logLevel = LevelOther "silent" } $
           loadConfig (liftIO . inner)
       inTempDir test = do
         currentDirectory <- getCurrentDirectory
@@ -67,47 +69,47 @@         defaultPrefs
         (info (nixOptsParser False) mempty)
         cmdLineOpts
-      parseOpts cmdLineOpts = mempty { configMonoidNixOpts = parseNixOpts cmdLineOpts }
+      parseOpts cmdLineOpts = mempty { nixOpts = parseNixOpts cmdLineOpts }
   let trueOnNonWindows = not osIsWindows
   describe "nix disabled in config file" $
     around_ (withStackDotYaml sampleConfigNixDisabled) $ do
       it "sees that the nix shell is not enabled" $ loadConfig' mempty $ \config ->
-        nixEnable (configNix config) `shouldBe` False
+         config.nix.enable `shouldBe` False
       describe "--nix given on command line" $
         it "sees that the nix shell is enabled" $
           loadConfig' (parseOpts ["--nix"]) $ \config ->
-          nixEnable (configNix config) `shouldBe` trueOnNonWindows
+          config.nix.enable `shouldBe` trueOnNonWindows
       describe "--nix-pure given on command line" $
         it "sees that the nix shell is enabled" $
           loadConfig' (parseOpts ["--nix-pure"]) $ \config ->
-          nixEnable (configNix config) `shouldBe` trueOnNonWindows
+          config.nix.enable `shouldBe` trueOnNonWindows
       describe "--no-nix given on command line" $
         it "sees that the nix shell is not enabled" $
           loadConfig' (parseOpts ["--no-nix"]) $ \config ->
-          nixEnable (configNix config) `shouldBe` False
+          config.nix.enable `shouldBe` False
       describe "--no-nix-pure given on command line" $
         it "sees that the nix shell is not enabled" $
           loadConfig' (parseOpts ["--no-nix-pure"]) $ \config ->
-          nixEnable (configNix config) `shouldBe` False
+          config.nix.enable `shouldBe` False
   describe "nix enabled in config file" $
     around_ (withStackDotYaml sampleConfigNixEnabled) $ do
       it "sees that the nix shell is enabled" $
         loadConfig' mempty $ \config ->
-        nixEnable (configNix config) `shouldBe` trueOnNonWindows
+        config.nix.enable `shouldBe` trueOnNonWindows
       describe "--no-nix given on command line" $
         it "sees that the nix shell is not enabled" $
           loadConfig' (parseOpts ["--no-nix"]) $ \config ->
-          nixEnable (configNix config) `shouldBe` False
+          config.nix.enable `shouldBe` False
       describe "--nix-pure given on command line" $
         it "sees that the nix shell is enabled" $
           loadConfig' (parseOpts ["--nix-pure"]) $ \config ->
-          nixEnable (configNix config) `shouldBe` trueOnNonWindows
+          config.nix.enable `shouldBe` trueOnNonWindows
       describe "--no-nix-pure given on command line" $
         it "sees that the nix shell is enabled" $
           loadConfig' (parseOpts ["--no-nix-pure"]) $ \config ->
-          nixEnable (configNix config) `shouldBe` trueOnNonWindows
+          config.nix.enable `shouldBe` trueOnNonWindows
       it "sees that the only package asked for is glpk and asks for the correct GHC derivation" $ loadConfig' mempty $ \config -> do
-        nixPackages (configNix config) `shouldBe` ["glpk"]
+        config.nix.packages `shouldBe` ["glpk"]
         v <- parseVersionThrowing "9.0.2"
         ghc <- either throwIO pure $ nixCompiler (WCGhc v)
         ghc `shouldBe` "haskell.compiler.ghc902"
tests/unit/Stack/PackageDumpSpec.hs view
@@ -92,19 +92,19 @@                 , "base-4.7.0.2-bfd89587617e381ae01b8dd7b6c7f1c1"
                 , "ghc-prim-0.3.1.0-a24f9c14c632d75b683d0f93283aea37"
                 ]
-            haskell2010 { dpExposedModules = mempty } `shouldBe` DumpPackage
-                { dpGhcPkgId = ghcPkgId
-                , dpPackageIdent = packageIdent
-                , dpParentLibIdent = Nothing
-                , dpLicense = Just BSD3
-                , dpLibDirs = ["/opt/ghc/7.8.4/lib/ghc-7.8.4/haskell2010-1.1.2.0"]
-                , dpDepends = depends
-                , dpLibraries = ["HShaskell2010-1.1.2.0"]
-                , dpHasExposedModules = True
-                , dpHaddockInterfaces = ["/opt/ghc/7.8.4/share/doc/ghc/html/libraries/haskell2010-1.1.2.0/haskell2010.haddock"]
-                , dpHaddockHtml = Just "/opt/ghc/7.8.4/share/doc/ghc/html/libraries/haskell2010-1.1.2.0"
-                , dpIsExposed = False
-                , dpExposedModules = mempty
+            haskell2010 { exposedModules = mempty } `shouldBe` DumpPackage
+                { ghcPkgId = ghcPkgId
+                , packageIdent = packageIdent
+                , sublib = Nothing
+                , license = Just BSD3
+                , libDirs = ["/opt/ghc/7.8.4/lib/ghc-7.8.4/haskell2010-1.1.2.0"]
+                , depends = depends
+                , libraries = ["HShaskell2010-1.1.2.0"]
+                , hasExposedModules = True
+                , haddockInterfaces = ["/opt/ghc/7.8.4/share/doc/ghc/html/libraries/haskell2010-1.1.2.0/haskell2010.haddock"]
+                , haddockHtml = Just "/opt/ghc/7.8.4/share/doc/ghc/html/libraries/haskell2010-1.1.2.0"
+                , isExposed = False
+                , exposedModules = mempty
                 }
 
         it "ghc 7.10" $ do
@@ -134,19 +134,19 @@                 , "transformers-0.4.2.0-c1a7bb855a176fe475d7b665301cd48f"
                 , "unix-2.7.1.0-e5915eb989e568b732bc7286b0d0817f"
                 ]
-            haskell2010 { dpExposedModules = mempty } `shouldBe` DumpPackage
-                { dpGhcPkgId = ghcPkgId
-                , dpPackageIdent = pkgIdent
-                , dpParentLibIdent = Nothing
-                , dpLicense = Just BSD3
-                , dpLibDirs = ["/opt/ghc/7.10.1/lib/ghc-7.10.1/ghc_EMlWrQ42XY0BNVbSrKixqY"]
-                , dpHaddockInterfaces = ["/opt/ghc/7.10.1/share/doc/ghc/html/libraries/ghc-7.10.1/ghc.haddock"]
-                , dpHaddockHtml = Just "/opt/ghc/7.10.1/share/doc/ghc/html/libraries/ghc-7.10.1"
-                , dpDepends = depends
-                , dpLibraries = ["HSghc-7.10.1-EMlWrQ42XY0BNVbSrKixqY"]
-                , dpHasExposedModules = True
-                , dpIsExposed = False
-                , dpExposedModules = mempty
+            haskell2010 { exposedModules = mempty } `shouldBe` DumpPackage
+                { ghcPkgId = ghcPkgId
+                , packageIdent = pkgIdent
+                , sublib = Nothing
+                , license = Just BSD3
+                , libDirs = ["/opt/ghc/7.10.1/lib/ghc-7.10.1/ghc_EMlWrQ42XY0BNVbSrKixqY"]
+                , haddockInterfaces = ["/opt/ghc/7.10.1/share/doc/ghc/html/libraries/ghc-7.10.1/ghc.haddock"]
+                , haddockHtml = Just "/opt/ghc/7.10.1/share/doc/ghc/html/libraries/ghc-7.10.1"
+                , depends = depends
+                , libraries = ["HSghc-7.10.1-EMlWrQ42XY0BNVbSrKixqY"]
+                , hasExposedModules = True
+                , isExposed = False
+                , exposedModules = mempty
                 }
         it "ghc 7.8.4 (osx)" $ do
             hmatrix:_ <-
@@ -170,22 +170,22 @@                 , "storable-complex-0.2.2-e962c368d58acc1f5b41d41edc93da72"
                 , "vector-0.10.12.3-f4222db607fd5fdd7545d3e82419b307"]
             hmatrix `shouldBe` DumpPackage
-                { dpGhcPkgId = ghcPkgId
-                , dpPackageIdent = pkgId
-                , dpParentLibIdent = Nothing
-                , dpLicense = Just BSD3
-                , dpLibDirs =
+                { ghcPkgId = ghcPkgId
+                , packageIdent = pkgId
+                , sublib = Nothing
+                , license = Just BSD3
+                , libDirs =
                       [ "/Users/alexbiehl/.stack/snapshots/x86_64-osx/lts-2.13/7.8.4/lib/x86_64-osx-ghc-7.8.4/hmatrix-0.16.1.5"
                       , "/opt/local/lib/"
                       , "/usr/local/lib/"
                       ,  "C:/Program Files/Example/"]
-                , dpHaddockInterfaces = ["/Users/alexbiehl/.stack/snapshots/x86_64-osx/lts-2.13/7.8.4/doc/html/hmatrix.haddock"]
-                , dpHaddockHtml = Just "/Users/alexbiehl/.stack/snapshots/x86_64-osx/lts-2.13/7.8.4/doc/html"
-                , dpDepends = depends
-                , dpLibraries = ["HShmatrix-0.16.1.5"]
-                , dpHasExposedModules = True
-                , dpIsExposed = True
-                , dpExposedModules = Set.fromList ["Data.Packed","Data.Packed.Vector","Data.Packed.Matrix","Data.Packed.Foreign","Data.Packed.ST","Data.Packed.Development","Numeric.LinearAlgebra","Numeric.LinearAlgebra.LAPACK","Numeric.LinearAlgebra.Algorithms","Numeric.Container","Numeric.LinearAlgebra.Util","Numeric.LinearAlgebra.Devel","Numeric.LinearAlgebra.Data","Numeric.LinearAlgebra.HMatrix","Numeric.LinearAlgebra.Static"]
+                , haddockInterfaces = ["/Users/alexbiehl/.stack/snapshots/x86_64-osx/lts-2.13/7.8.4/doc/html/hmatrix.haddock"]
+                , haddockHtml = Just "/Users/alexbiehl/.stack/snapshots/x86_64-osx/lts-2.13/7.8.4/doc/html"
+                , depends = depends
+                , libraries = ["HShmatrix-0.16.1.5"]
+                , hasExposedModules = True
+                , isExposed = True
+                , exposedModules = Set.fromList ["Data.Packed","Data.Packed.Vector","Data.Packed.Matrix","Data.Packed.Foreign","Data.Packed.ST","Data.Packed.Development","Numeric.LinearAlgebra","Numeric.LinearAlgebra.LAPACK","Numeric.LinearAlgebra.Algorithms","Numeric.Container","Numeric.LinearAlgebra.Util","Numeric.LinearAlgebra.Devel","Numeric.LinearAlgebra.Data","Numeric.LinearAlgebra.HMatrix","Numeric.LinearAlgebra.Static"]
                 }
         it "ghc HEAD" $ do
           ghcBoot:_ <-
@@ -206,19 +206,19 @@             , "filepath-1.4.1.0"
             ]
           ghcBoot `shouldBe` DumpPackage
-            { dpGhcPkgId = ghcPkgId
-            , dpPackageIdent = pkgId
-            , dpParentLibIdent = Nothing
-            , dpLicense = Just BSD3
-            , dpLibDirs =
+            { ghcPkgId = ghcPkgId
+            , packageIdent = pkgId
+            , sublib = Nothing
+            , license = Just BSD3
+            , libDirs =
                   ["/opt/ghc/head/lib/ghc-7.11.20151213/ghc-boot-0.0.0.0"]
-            , dpHaddockInterfaces = ["/opt/ghc/head/share/doc/ghc/html/libraries/ghc-boot-0.0.0.0/ghc-boot.haddock"]
-            , dpHaddockHtml = Just "/opt/ghc/head/share/doc/ghc/html/libraries/ghc-boot-0.0.0.0"
-            , dpDepends = depends
-            , dpLibraries = ["HSghc-boot-0.0.0.0"]
-            , dpHasExposedModules = True
-            , dpIsExposed = True
-            , dpExposedModules = Set.fromList ["GHC.Lexeme", "GHC.PackageDb"]
+            , haddockInterfaces = ["/opt/ghc/head/share/doc/ghc/html/libraries/ghc-boot-0.0.0.0/ghc-boot.haddock"]
+            , haddockHtml = Just "/opt/ghc/head/share/doc/ghc/html/libraries/ghc-boot-0.0.0.0"
+            , depends = depends
+            , libraries = ["HSghc-boot-0.0.0.0"]
+            , hasExposedModules = True
+            , isExposed = True
+            , exposedModules = Set.fromList ["GHC.Lexeme", "GHC.PackageDb"]
             }