packages feed

pantry 0.8.1 → 0.8.2

raw patch · 10 files changed

+1472/−193 lines, 10 filesdep +Win32dep +optparse-applicativedep +processnew-component:exe:test-pretty-exceptionsPVP ok

version bump matches the API change (PVP)

Dependencies added: Win32, optparse-applicative, process

API changes (from Hackage documentation)

+ Pantry: FRNameNotFound :: ![PackageName] -> FuzzyResults
+ Pantry: FRRevisionNotFound :: !NonEmpty PackageIdentifierRevision -> FuzzyResults
+ Pantry: FRVersionNotFound :: !NonEmpty PackageIdentifierRevision -> FuzzyResults
+ Pantry: Mismatch :: !a -> !a -> Mismatch a
+ Pantry: [mismatchActual] :: Mismatch a -> !a
+ Pantry: [mismatchExpected] :: Mismatch a -> !a
+ Pantry: data FuzzyResults
+ Pantry: data Mismatch a
+ Pantry: data SafeFilePath
+ Pantry: mkSafeFilePath :: Text -> Maybe SafeFilePath
+ Pantry.Internal.AesonExtended: AesonException :: String -> AesonException
+ Pantry.Internal.AesonExtended: newtype AesonException
+ Pantry.Internal.AesonExtended: throwDecode :: (FromJSON a, MonadThrow m) => ByteString -> m a
+ Pantry.Internal.AesonExtended: throwDecode' :: (FromJSON a, MonadThrow m) => ByteString -> m a
+ Pantry.Internal.AesonExtended: throwDecodeStrict :: (FromJSON a, MonadThrow m) => ByteString -> m a
+ Pantry.Internal.AesonExtended: throwDecodeStrict' :: (FromJSON a, MonadThrow m) => ByteString -> m a

Files

ChangeLog.md view
@@ -1,5 +1,13 @@ # Changelog for pantry
 
+## v0.8.2
+
+* `PantryException` is now an instance of the
+  `Text.PrettyPrint.Leijen.Extended.Pretty` class (provided by the
+  `rio-prettyprint` package).
+* Module `Pantry` now exports `FuzzyResults`, `Mismatch` and `SafeFilePath` (and
+  `mkSafeFilePath`), used in data constructors of `PantryException`.
+
 ## v0.8.1
 
 * Support `hpack-0.35.1`, and prettier `HpackLibraryException` error messages.
+ app/test-pretty-exceptions/Main.hs view
@@ -0,0 +1,415 @@+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards   #-}
+{-# LANGUAGE TupleSections     #-}
+
+-- | An executable to allow a person to inspect in a terminal the form of
+-- Pantry's pretty exceptions.
+module Main
+ ( main
+ ) where
+
+import qualified Data.Conduit.Tar as Tar
+import           Data.Maybe ( fromJust )
+import qualified Data.Text as T
+import qualified Distribution.Parsec.Error as C
+import qualified Distribution.Parsec.Position as C
+import qualified Distribution.Parsec.Warning as C
+import qualified Distribution.Types.PackageName as C
+import qualified Distribution.Types.Version as C
+import           Options.Applicative
+                   ( Parser, (<**>), auto, execParser, fullDesc, header, help
+                   , helper, info, long, metavar, option, progDesc, showDefault
+                   , strOption, value
+                   )
+import           Network.HTTP.Types.Status ( Status, mkStatus )
+import           Pantry
+                   ( ArchiveLocation (..), BlobKey (..), CabalFileInfo (..)
+                   , FileSize (..), FuzzyResults (..), Mismatch (..)
+                   , PackageName, PantryException (..), PackageIdentifier (..)
+                   , PackageIdentifierRevision (..), PackageMetadata (..)
+                   , RawPackageLocationImmutable (..), RawPackageMetadata (..)
+                   , RawSnapshotLocation (..), RelFilePath (..), Repo (..)
+                   , RepoType (..), ResolvedPath (..), Revision (..), SHA256
+                   , SafeFilePath, SimpleRepo (..), SnapName (..)
+                   , TreeKey (..), Version, WantedCompiler (..), mkSafeFilePath
+                   )
+import           Pantry.SHA256 ( hashBytes )
+import           Path ( File )
+import           PathAbsExamples
+                   ( pathAbsDirExample, pathAbsFileExample
+                   , pathAbsFileExamples
+                   )
+import           RIO
+import qualified RIO.List as L
+import           RIO.NonEmpty ( nonEmpty )
+import           RIO.PrettyPrint ( pretty, prettyError )
+import           RIO.PrettyPrint.Simple ( SimplePrettyApp, runSimplePrettyApp )
+import           RIO.PrettyPrint.StylesUpdate
+                   ( StylesUpdate, parseStylesUpdateFromString )
+import           RIO.Time ( fromGregorian )
+import           System.Terminal ( hIsTerminalDeviceOrMinTTY, getTerminalWidth )
+
+-- | Type representing options that can be specified at the command line
+data Options = Options
+  { colours :: String
+  , theme :: Theme
+  }
+
+-- | Type representing styles identified by a theme name
+data Theme
+  = Default
+  | SolarizedDark
+  deriving (Bounded, Enum, Read, Show)
+
+options :: Parser Options
+options = Options
+  <$> strOption
+        (  long "colours"
+        <> metavar "STYLES"
+        <> help "Specify the output styles; STYLES is a colon-delimited \
+                \sequence of key=value, where 'key' is a style name and \
+                \'value' is a semicolon-delimited list of 'ANSI' SGR (Select \
+                \Graphic Rendition) control codes (in decimal). In shells \
+                \where a semicolon is a command separator, enclose STYLES in \
+                \quotes."
+        <> value ""
+        )
+  <*> option auto
+        ( long "theme"
+        <> metavar "THEME"
+        <> help (  "Specify a theme for output styles. THEME is one of: "
+                <> showThemes <> "."
+                )
+        <> value Default
+        <> showDefault
+        )
+ where
+  showThemes = L.intercalate " " $ map show ([minBound .. maxBound] :: [Theme])
+
+fromTheme :: Theme -> StylesUpdate
+fromTheme Default = mempty
+fromTheme SolarizedDark = parseStylesUpdateFromString
+  "error=31:good=32:shell=35:dir=34:recommendation=32:target=95:module=35:package-component=95:secondary=92:highlight=32"
+
+main :: IO ()
+main = do
+  isTerminal <- hIsTerminalDeviceOrMinTTY stderr
+  if isTerminal
+    then do
+      terminalWidth <- fromMaybe 80 <$> getTerminalWidth
+      mainInTerminal terminalWidth =<< execParser opts
+    else
+      putStrLn "This executable is intended to be run with the standard error \
+               \ channel connected to a terminal. No terminal detected."
+ where
+  opts = info (options <**> helper)
+    (  fullDesc
+    <> progDesc "Allows a person to inspect in a terminal the form of Pantry's \
+                \pretty exceptions."
+    <> header "test-pretty-exceptions - test Pantry's pretty exceptions"
+    )
+
+mainInTerminal :: Int -> Options -> IO ()
+mainInTerminal terminalWidth Options{..} = do
+  let stylesUpdate = fromTheme theme <> parseStylesUpdateFromString colours
+  runSimplePrettyApp terminalWidth stylesUpdate action
+ where
+  action :: RIO SimplePrettyApp ()
+  action = mapM_ (prettyError . pretty) examples
+
+-- | The intention is that there shoud be examples for every data constructor of
+-- the PantryException type.
+examples :: [PantryException]
+examples = concat
+  [ [ PackageIdentifierRevisionParseFail hackageMsg ]
+  , [ InvalidCabalFile loc version pErrorExamples pWarningExamples
+    | loc <- map Left rawPackageLocationImmutableExamples <> [Right pathAbsFileExample]
+    , version <- [Nothing, Just versionExample]
+    ]
+  , [ TreeWithoutCabalFile rawPackageLocationImmutable
+    | rawPackageLocationImmutable <- rawPackageLocationImmutableExamples
+    ]
+  , [ TreeWithMultipleCabalFiles rawPackageLocationImmutable safeFilePathExamples
+    | rawPackageLocationImmutable <- rawPackageLocationImmutableExamples
+    ]
+  , [ MismatchedCabalName pathAbsFileExample packageNameExample ]
+  , [ NoCabalFileFound pathAbsDirExample ]
+  , [ MultipleCabalFilesFound pathAbsDirExample pathAbsFileExamples ]
+  , [ InvalidWantedCompiler "my-wanted-compiler" ]
+  , [ InvalidSnapshotLocation pathAbsDirExample rawPathExample ]
+  , [ InvalidOverrideCompiler wantedCompiler1 wantedCompiler2
+    | wantedCompiler1 <- wantedCompilerExamples
+    , wantedCompiler2 <- wantedCompilerExamples
+    ]
+  , [ InvalidFilePathSnapshot rawPathExample ]
+  , [ InvalidSnapshot rawSnapshotLocation someExceptionExample
+    | rawSnapshotLocation <- rawSnapshotLocationExamples
+    ]
+  , [ MismatchedPackageMetadata rawPackageLocationImmutable rawPackageMetadata treeKey packageIdentifierExample
+    | rawPackageLocationImmutable <- rawPackageLocationImmutableExamples
+    , rawPackageMetadata <- rawPackageMetadataExamples
+    , treeKey <- [Nothing, Just treeKeyExample]
+    ]
+  , [ Non200ResponseStatus statusExample ]
+  , [ InvalidBlobKey (Mismatch blobKeyExample blobKeyExample) ]
+  , [ Couldn'tParseSnapshot rawSnapshotLocation errorMessageExample
+    | rawSnapshotLocation <- rawSnapshotLocationExamples
+    ]
+  , [ WrongCabalFileName rawPackageLocationImmutable safeFilePathExample packageNameExample
+    | rawPackageLocationImmutable <- rawPackageLocationImmutableExamples
+    ]
+  , [ DownloadInvalidSHA256 urlExample (Mismatch sha256Example sha256Example) ]
+  , [ DownloadInvalidSize urlExample (Mismatch fileSizeExample fileSizeExample) ]
+  , [ DownloadTooLarge urlExample (Mismatch fileSizeExample fileSizeExample) ]
+  , [ LocalInvalidSHA256 pathAbsFileExample (Mismatch sha256Example sha256Example) ]
+  , [ LocalInvalidSize pathAbsFileExample (Mismatch fileSizeExample fileSizeExample) ]
+  , [ UnknownArchiveType archiveLocation
+    | archiveLocation <- archiveLocationExamples
+    ]
+  , [ InvalidTarFileType archiveLocation filePathExample fileTypeExample
+    | archiveLocation <- archiveLocationExamples
+    ]
+  , [ UnsupportedTarball archiveLocation (T.pack errorMessageExample)
+    | archiveLocation <- archiveLocationExamples
+    ]
+  , [ NoHackageCryptographicHash packageIdentifierExample ]
+  , [ FailedToCloneRepo simpleRepoExample ]
+  , [ TreeReferencesMissingBlob rawPackageLocationImmutable safeFilePathExample blobKeyExample
+    | rawPackageLocationImmutable <- rawPackageLocationImmutableExamples
+    ]
+  , [ CompletePackageMetadataMismatch rawPackageLocationImmutable packageMetadataExample
+    | rawPackageLocationImmutable <- rawPackageLocationImmutableExamples
+    ]
+  , [ CRC32Mismatch archiveLocation filePathExample (Mismatch 1024 1024 )
+    | archiveLocation <- archiveLocationExamples
+    ]
+  , [ UnknownHackagePackage packageIdentifierRevisionExample fuzzyResults
+    | packageIdentifierRevisionExample <- packageIdentifierRevisionExamples
+    , fuzzyResults <- fuzzyResultsExamples
+    ]
+  , [ CannotCompleteRepoNonSHA1 repoExample ]
+  , [ MutablePackageLocationFromUrl urlExample ]
+  , [ MismatchedCabalFileForHackage packageIdentifierRevision (Mismatch packageIdentifierExample packageIdentifierExample)
+    | packageIdentifierRevision <- packageIdentifierRevisionExamples
+    ]
+  , [ PackageNameParseFail rawPackageName ]
+  , [ PackageVersionParseFail rawPackageVersion ]
+  , [ InvalidCabalFilePath pathAbsFileExample ]
+  , [ DuplicatePackageNames sourceMsgExample duplicatePackageNamesExamples ]
+  , [ MigrationFailure descriptionExample pathAbsFileExample someExceptionExample ]
+  , [ InvalidTreeFromCasa blobKeyExample blobExample ]
+  , [ ParseSnapNameException rawSnapNameExample ]
+  , [ HpackLibraryException pathAbsFileExample errorMessageExample ]
+  , [ HpackExeException hpackCommandExample pathAbsDirExample someExceptionExample ]
+  ]
+
+hackageMsg :: Text
+hackageMsg = "<Example message from Hackage.>"
+
+pErrorExamples :: [C.PError]
+pErrorExamples =
+  [ C.PError (C.Position 10 20) "<Example error message1.>"
+  , C.PError (C.Position 12 10) "<Example error message2.>"
+  , C.PError (C.Position 14 30) "<Example error message3.>"
+  ]
+
+pWarningExamples :: [C.PWarning]
+pWarningExamples =
+  [ C.PWarning C.PWTOther (C.Position 10 20) "<Example warning message1.>"
+  , C.PWarning C.PWTOther (C.Position 12 10) "<Example warning message2.>"
+  , C.PWarning C.PWTOther (C.Position 14 30) "<Example warning message3.>"
+  ]
+
+packageNameExample :: PackageName
+packageNameExample = C.mkPackageName "my-package"
+
+versionExample :: Version
+versionExample = C.mkVersion [1, 0, 0]
+
+sha256Example :: SHA256
+sha256Example = hashBytes "example"
+
+fileSizeExample :: FileSize
+fileSizeExample = FileSize 1234
+
+revisionExample :: Revision
+revisionExample = Revision 1
+
+cabalFileInfoExamples :: [CabalFileInfo]
+cabalFileInfoExamples = concat
+  [ [CFILatest]
+  , [ CFIHash sha256Example fileSize
+    | fileSize <- [Nothing, Just fileSizeExample]
+    ]
+  , [CFIRevision revisionExample]
+  ]
+
+packageIdentifierRevisionExamples :: [PackageIdentifierRevision]
+packageIdentifierRevisionExamples =
+  [ PackageIdentifierRevision packageNameExample versionExample cabalFileInfo
+  | cabalFileInfo <- cabalFileInfoExamples
+  ]
+
+blobKeyExample :: BlobKey
+blobKeyExample = BlobKey sha256Example fileSizeExample
+
+treeKeyExample :: TreeKey
+treeKeyExample = TreeKey blobKeyExample
+
+rawPackageLocationImmutableExamples :: [RawPackageLocationImmutable]
+rawPackageLocationImmutableExamples = concat
+  [ [ RPLIHackage packageIdentifierRevision treeKey
+    | packageIdentifierRevision <- packageIdentifierRevisionExamples
+    , treeKey <- [Nothing, Just treeKeyExample]
+    ]
+--, RPLIArchive
+  , [ RPLIRepo repoExample rawPackageMetadata
+    | rawPackageMetadata <- rawPackageMetadataExamples
+    ]
+  ]
+
+safeFilePathExamples :: [SafeFilePath]
+safeFilePathExamples =
+  [ fromJust $ mkSafeFilePath "Users/jane/my-project-dir/example1.ext"
+  , fromJust $ mkSafeFilePath "Users/jane/my-project-dir/example2.ext"
+  , fromJust $ mkSafeFilePath "Users/jane/my-project-dir/example3.ext"
+  ]
+
+rawPathExample :: Text
+rawPathExample = "<Example raw path.>"
+
+wantedCompilerExamples :: [WantedCompiler]
+wantedCompilerExamples =
+  [ WCGhc versionExample
+  , WCGhcGit "<commit1>" "<commit2>"
+  , WCGhcjs versionExample versionExample
+  ]
+
+data ExceptionExample
+  = ExceptionExample !Text
+  deriving (Show, Typeable)
+
+instance Exception ExceptionExample where
+  displayException (ExceptionExample t) = T.unpack t
+
+errorMessageExample :: String
+errorMessageExample =
+  "This is the first line of some example text for the message in an exception \
+  \example. This is example text for an exception example.\n\
+  \This is the second line of some example text for the message in an exception \
+  \example. This is example text for an exception example."
+
+someExceptionExample :: SomeException
+someExceptionExample =
+  SomeException (ExceptionExample $ T.pack errorMessageExample)
+
+urlExample :: Text
+urlExample = "https://example.com"
+
+relFilePathExample :: RelFilePath
+relFilePathExample = RelFilePath "jane/my-project-dir"
+
+resolvedPathFileExample :: ResolvedPath File
+resolvedPathFileExample = ResolvedPath relFilePathExample pathAbsFileExample
+
+snapNameExamples :: [SnapName]
+snapNameExamples =
+  [ LTS 20 17
+  , Nightly $ fromGregorian 2023 4 5
+  ]
+
+rawSnapshotLocationExamples :: [RawSnapshotLocation]
+rawSnapshotLocationExamples = concat
+  [ [ RSLCompiler wantedCompiler
+    | wantedCompiler <- wantedCompilerExamples
+    ]
+  , [ RSLUrl urlExample blobKey
+    | blobKey <- [Nothing, Just blobKeyExample]
+    ]
+  , [ RSLFilePath resolvedPathFileExample ]
+  , [ RSLSynonym snapNameExample
+    | snapNameExample <- snapNameExamples
+    ]
+  ]
+
+rawPackageMetadataExamples :: [RawPackageMetadata]
+rawPackageMetadataExamples =
+  [ RawPackageMetadata name version treeKey
+  | name <- [ Nothing, Just packageNameExample]
+  , version <- [ Nothing, Just versionExample ]
+  , treeKey <- [Nothing, Just treeKeyExample]
+  ]
+
+statusExample :: Status
+statusExample = mkStatus 100 "<Example status message.>"
+
+safeFilePathExample :: SafeFilePath
+safeFilePathExample =
+  fromJust $ mkSafeFilePath "Users/jane/my-project-dir/example.ext"
+
+archiveLocationExamples :: [ArchiveLocation]
+archiveLocationExamples =
+  [ ALUrl urlExample
+  , ALFilePath resolvedPathFileExample
+  ]
+
+filePathExample :: FilePath
+filePathExample = "<file-path>"
+
+fileTypeExample :: Tar.FileType
+fileTypeExample = Tar.FTNormal
+
+commitExample :: Text
+commitExample = "b8b34bf5571de75909d97f687e3d37909b1dc9f7"
+
+simpleRepoExample :: SimpleRepo
+simpleRepoExample = SimpleRepo urlExample commitExample RepoGit
+
+packageIdentifierExample :: PackageIdentifier
+packageIdentifierExample = PackageIdentifier packageNameExample versionExample
+
+packageMetadataExample :: PackageMetadata
+packageMetadataExample = PackageMetadata packageIdentifierExample treeKeyExample
+
+fuzzyResultsExamples :: [FuzzyResults]
+fuzzyResultsExamples =
+  [ FRNameNotFound packageNameExamples
+  , FRVersionNotFound $ fromJust $ nonEmpty packageIdentifierRevisionExamples
+  , FRRevisionNotFound $ fromJust $ nonEmpty packageIdentifierRevisionExamples
+  ]
+
+repoExample :: Repo
+repoExample = Repo urlExample commitExample RepoGit "my-subdirectory"
+
+rawPackageName :: Text
+rawPackageName = "<raw-package-name>"
+
+rawPackageVersion :: Text
+rawPackageVersion = "<raw-package-version>"
+
+sourceMsgExample :: Utf8Builder
+sourceMsgExample = "<Example source message.>"
+
+packageNameExamples :: [PackageName]
+packageNameExamples =
+  [ C.mkPackageName "my-package1"
+  , C.mkPackageName "my-package2"
+  , C.mkPackageName "my-package3"
+  ]
+
+duplicatePackageNamesExamples :: [(PackageName, [RawPackageLocationImmutable])]
+duplicatePackageNamesExamples = map
+  ((, rawPackageLocationImmutableExamples))
+  packageNameExamples
+
+descriptionExample :: Text
+descriptionExample = "<Example description.>"
+
+blobExample :: ByteString
+blobExample = "b8b34bf5571de75909d97f687e3d37909b1dc9f7"
+
+rawSnapNameExample :: Text
+rawSnapNameExample = "<raw-snapshot-name>"
+
+hpackCommandExample :: FilePath
+hpackCommandExample = "<path-to-hpack>/hpack"
+ app/test-pretty-exceptions/unix/PathAbsExamples.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE QuasiQuotes #-}
+
+-- | The module of this name differs as between Windows and non-Windows builds.
+-- This is the non-Windows version.
+module PathAbsExamples
+  ( pathAbsDirExample
+  , pathAbsFileExample
+  , pathAbsFileExamples
+  ) where
+
+import           Path ( Abs, Dir, File, Path, absdir, absfile )
+
+pathAbsDirExample :: Path Abs Dir
+pathAbsDirExample = [absdir|/home/jane/my-project-dir|]
+
+pathAbsFileExample :: Path Abs File
+pathAbsFileExample = [absfile|/home/jane/my-project-dir/example.ext|]
+
+pathAbsFileExamples :: [Path Abs File]
+pathAbsFileExamples =
+  [ [absfile|/home/jane/my-project-dir/example1.ext|]
+  , [absfile|/home/jane/my-project-dir/example2.ext|]
+  , [absfile|/home/jane/my-project-dir/example3.ext|]
+  ]
+ app/test-pretty-exceptions/unix/System/Terminal.hsc view
@@ -0,0 +1,46 @@+{-# LANGUAGE CApiFFI #-}
+
+-- | The module of this name differs as between Windows and non-Windows builds.
+-- This is the non-Windows version.
+module System.Terminal
+( getTerminalWidth
+, hIsTerminalDeviceOrMinTTY
+) where
+
+import           Foreign
+import           Foreign.C.Types
+import           RIO (MonadIO, Handle, hIsTerminalDevice)
+
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+
+newtype WindowWidth = WindowWidth CUShort
+    deriving (Eq, Ord, Show)
+
+instance Storable WindowWidth where
+  sizeOf _ = (#size struct winsize)
+  alignment _ = (#alignment struct winsize)
+  peek p = WindowWidth <$> (#peek struct winsize, ws_col) p
+  poke p (WindowWidth w) = do
+    (#poke struct winsize, ws_col) p w
+
+-- `ioctl` is variadic, so `capi` is needed, see:
+-- https://www.haskell.org/ghc/blog/20210709-capi-usage.html
+foreign import capi "sys/ioctl.h ioctl"
+  ioctl :: CInt -> CInt -> Ptr WindowWidth -> IO CInt
+
+getTerminalWidth :: IO (Maybe Int)
+getTerminalWidth =
+  alloca $ \p -> do
+    errno <- ioctl (#const STDOUT_FILENO) (#const TIOCGWINSZ) p
+    if errno < 0
+    then return Nothing
+    else do
+      WindowWidth w <- peek p
+      return . Just . fromIntegral $ w
+
+-- | hIsTerminaDevice does not recognise handles to mintty terminals as terminal
+-- devices, but isMinTTYHandle does.
+hIsTerminalDeviceOrMinTTY :: MonadIO m => Handle -> m Bool
+hIsTerminalDeviceOrMinTTY = hIsTerminalDevice
+ app/test-pretty-exceptions/windows/PathAbsExamples.hs view
@@ -0,0 +1,24 @@+{-# LANGUAGE QuasiQuotes #-}
+
+-- | The module of this name differs as between Windows and non-Windows builds.
+-- This is the Windows version.
+module PathAbsExamples
+ ( pathAbsDirExample
+ , pathAbsFileExample
+ , pathAbsFileExamples
+ ) where
+
+import           Path ( Abs, Dir, File, Path, absdir, absfile )
+
+pathAbsDirExample :: Path Abs Dir
+pathAbsDirExample = [absdir|C:/Users/jane/my-project-dir|]
+
+pathAbsFileExample :: Path Abs File
+pathAbsFileExample = [absfile|C:/Users/jane/my-project-dir/example.ext|]
+
+pathAbsFileExamples :: [Path Abs File]
+pathAbsFileExamples =
+  [ [absfile|C:/Users/jane/my-project-dir/example1.ext|]
+  , [absfile|C:/Users/jane/my-project-dir/example2.ext|]
+  , [absfile|C:/Users/jane/my-project-dir/example3.ext|]
+  ]
+ app/test-pretty-exceptions/windows/System/Terminal.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE NoImplicitPrelude        #-}
+{-# LANGUAGE OverloadedStrings        #-}
+
+-- | The module of this name differs as between Windows and non-Windows builds.
+-- This is the Windows version.
+module System.Terminal
+  ( getTerminalWidth
+  , hIsTerminalDeviceOrMinTTY
+  ) where
+
+import           Foreign.Marshal.Alloc ( allocaBytes )
+import           Foreign.Ptr ( Ptr )
+import           Foreign.Storable ( peekByteOff )
+import           RIO
+import           RIO.Partial ( read )
+import           System.IO hiding ( hIsTerminalDevice )
+import           System.Process
+                   ( StdStream (..), createProcess, shell, std_err, std_in
+                   , std_out, waitForProcess
+                   )
+import           System.Win32 ( isMinTTYHandle, withHandleToHANDLE )
+
+type HANDLE = Ptr ()
+
+data CONSOLE_SCREEN_BUFFER_INFO
+
+sizeCONSOLE_SCREEN_BUFFER_INFO :: Int
+sizeCONSOLE_SCREEN_BUFFER_INFO = 22
+
+posCONSOLE_SCREEN_BUFFER_INFO_srWindow :: Int
+posCONSOLE_SCREEN_BUFFER_INFO_srWindow = 10 -- 4 x Word16 Left,Top,Right,Bottom
+
+c_STD_OUTPUT_HANDLE :: Int
+c_STD_OUTPUT_HANDLE = -11
+
+foreign import ccall unsafe "windows.h GetConsoleScreenBufferInfo"
+  c_GetConsoleScreenBufferInfo :: HANDLE -> Ptr CONSOLE_SCREEN_BUFFER_INFO -> IO Bool
+
+foreign import ccall unsafe "windows.h GetStdHandle"
+  c_GetStdHandle :: Int -> IO HANDLE
+
+
+getTerminalWidth :: IO (Maybe Int)
+getTerminalWidth = do
+  hdl <- c_GetStdHandle c_STD_OUTPUT_HANDLE
+  allocaBytes sizeCONSOLE_SCREEN_BUFFER_INFO $ \p -> do
+    b <- c_GetConsoleScreenBufferInfo hdl p
+    if not b
+      then do -- This could happen on Cygwin or MSYS
+        let stty = (shell "stty size") {
+              std_in  = UseHandle stdin
+            , std_out = CreatePipe
+            , std_err = CreatePipe
+            }
+        (_, mbStdout, _, rStty) <- createProcess stty
+        exStty <- waitForProcess rStty
+        case exStty of
+          ExitFailure _ -> pure Nothing
+          ExitSuccess ->
+            maybe (pure Nothing)
+                  (\hSize -> do
+                      sizeStr <- hGetContents hSize
+                      case map read $ words sizeStr :: [Int] of
+                        [_r, c] -> pure $ Just c
+                        _ -> pure Nothing
+                  )
+                  mbStdout
+      else do
+        [left,_top,right,_bottom] <- forM [0..3] $ \i -> do
+          v <- peekByteOff p (i * 2 + posCONSOLE_SCREEN_BUFFER_INFO_srWindow)
+          pure $ fromIntegral (v :: Word16)
+        pure $ Just (1 + right - left)
+
+-- | hIsTerminaDevice does not recognise handles to mintty terminals as terminal
+-- devices, but isMinTTYHandle does.
+hIsTerminalDeviceOrMinTTY :: MonadIO m => Handle -> m Bool
+hIsTerminalDeviceOrMinTTY h = do
+  isTD <- hIsTerminalDevice h
+  if isTD
+    then pure True
+    else liftIO $ withHandleToHANDLE h isMinTTYHandle
pantry.cabal view
@@ -1,186 +1,266 @@ cabal-version: 1.12
-
--- This file has been generated from package.yaml by hpack version 0.35.0.
---
--- see: https://github.com/sol/hpack
-
-name:           pantry
-version:        0.8.1
-synopsis:       Content addressable Haskell package management
-description:    Please see the README on GitHub at <https://github.com/commercialhaskell/pantry#readme>
-category:       Development
-homepage:       https://github.com/commercialhaskell/pantry#readme
-bug-reports:    https://github.com/commercialhaskell/pantry/issues
-author:         Michael Snoyman
-maintainer:     michael@snoyman.com
-copyright:      2018-2022 FP Complete
-license:        BSD3
-license-file:   LICENSE
-build-type:     Simple
-extra-source-files:
-    README.md
-    ChangeLog.md
-    attic/hpack-0.1.2.3.tar.gz
-    attic/package-0.1.2.3.tar.gz
-    attic/symlink-to-dir.tar.gz
-
-source-repository head
-  type: git
-  location: https://github.com/commercialhaskell/pantry
-
-library
-  exposed-modules:
-      Pantry
-      Pantry.SHA256
-      Pantry.Internal
-      Pantry.Internal.StaticBytes
-      Pantry.Internal.Stackage
-      Pantry.Internal.Companion
-      Pantry.Internal.AesonExtended
-  other-modules:
-      Hackage.Security.Client.Repository.HttpLib.HttpClient
-      Pantry.Archive
-      Pantry.HTTP
-      Pantry.HPack
-      Pantry.Hackage
-      Pantry.Repo
-      Pantry.SQLite
-      Pantry.Storage
-      Pantry.Casa
-      Pantry.Tree
-      Pantry.Types
-  hs-source-dirs:
-      src/
-  ghc-options: -Wall
-  build-depends:
-      Cabal >=3 && <3.9
-    , aeson
-    , ansi-terminal
-    , base >=4.10 && <5
-    , bytestring
-    , casa-client
-    , casa-types
-    , conduit
-    , conduit-extra
-    , containers
-    , cryptonite
-    , cryptonite-conduit
-    , digest
-    , filelock
-    , generic-deriving
-    , hackage-security
-    , hpack >=0.35.1
-    , http-client
-    , http-client-tls
-    , http-conduit
-    , http-download
-    , http-types
-    , memory
-    , mtl
-    , network-uri
-    , path
-    , path-io
-    , persistent
-    , persistent-sqlite >=2.9.3
-    , persistent-template
-    , primitive
-    , resourcet
-    , rio
-    , rio-orphans
-    , rio-prettyprint
-    , tar-conduit
-    , text
-    , text-metrics
-    , time
-    , transformers
-    , unix-compat
-    , unliftio
-    , unordered-containers
-    , vector
-    , yaml
-    , zip-archive
-  default-language: Haskell2010
-  if os(windows)
-    other-modules:
-        System.IsWindows
-    hs-source-dirs:
-        src/windows/
-  else
-    other-modules:
-        System.IsWindows
-    hs-source-dirs:
-        src/unix/
-
-test-suite spec
-  type: exitcode-stdio-1.0
-  main-is: Spec.hs
-  other-modules:
-      Pantry.ArchiveSpec
-      Pantry.BuildPlanSpec
-      Pantry.CabalSpec
-      Pantry.CasaSpec
-      Pantry.FileSpec
-      Pantry.GlobalHintsSpec
-      Pantry.HackageSpec
-      Pantry.Internal.StaticBytesSpec
-      Pantry.InternalSpec
-      Pantry.TreeSpec
-      Pantry.TypesSpec
-      Paths_pantry
-  hs-source-dirs:
-      test
-  ghc-options: -Wall
-  build-depends:
-      Cabal >=3 && <3.9
-    , QuickCheck
-    , aeson
-    , ansi-terminal
-    , base >=4.10 && <5
-    , bytestring
-    , casa-client
-    , casa-types
-    , conduit
-    , conduit-extra
-    , containers
-    , cryptonite
-    , cryptonite-conduit
-    , digest
-    , exceptions
-    , filelock
-    , generic-deriving
-    , hackage-security
-    , hedgehog
-    , hpack >=0.35.1
-    , hspec
-    , http-client
-    , http-client-tls
-    , http-conduit
-    , http-download
-    , http-types
-    , memory
-    , mtl
-    , network-uri
-    , pantry
-    , path
-    , path-io
-    , persistent
-    , persistent-sqlite >=2.9.3
-    , persistent-template
-    , primitive
-    , raw-strings-qq
-    , resourcet
-    , rio
-    , rio-orphans
-    , rio-prettyprint
-    , tar-conduit
-    , text
-    , text-metrics
-    , time
-    , transformers
-    , unix-compat
-    , unliftio
-    , unordered-containers
-    , vector
-    , yaml
-    , zip-archive
-  default-language: Haskell2010
++-- This file has been generated from package.yaml by hpack version 0.35.2.+--+-- see: https://github.com/sol/hpack++name:           pantry+version:        0.8.2+synopsis:       Content addressable Haskell package management+description:    Please see the README on GitHub at <https://github.com/commercialhaskell/pantry#readme>+category:       Development+homepage:       https://github.com/commercialhaskell/pantry#readme+bug-reports:    https://github.com/commercialhaskell/pantry/issues+author:         Michael Snoyman+maintainer:     michael@snoyman.com+copyright:      2018-2022 FP Complete+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    ChangeLog.md+    attic/hpack-0.1.2.3.tar.gz+    attic/package-0.1.2.3.tar.gz+    attic/symlink-to-dir.tar.gz++source-repository head+  type: git+  location: https://github.com/commercialhaskell/pantry++flag test-pretty-exceptions+  description: Build an executable to test pretty exceptions+  manual: False+  default: False++library+  exposed-modules:+      Pantry+      Pantry.SHA256+      Pantry.Internal+      Pantry.Internal.StaticBytes+      Pantry.Internal.Stackage+      Pantry.Internal.Companion+      Pantry.Internal.AesonExtended+  other-modules:+      Hackage.Security.Client.Repository.HttpLib.HttpClient+      Pantry.Archive+      Pantry.HTTP+      Pantry.HPack+      Pantry.Hackage+      Pantry.Repo+      Pantry.SQLite+      Pantry.Storage+      Pantry.Casa+      Pantry.Tree+      Pantry.Types+  hs-source-dirs:+      src/+  ghc-options: -Wall+  build-depends:+      Cabal >=3 && <3.11+    , aeson+    , ansi-terminal+    , base >=4.10 && <5+    , bytestring+    , casa-client+    , casa-types+    , conduit+    , conduit-extra+    , containers+    , cryptonite+    , cryptonite-conduit+    , digest+    , filelock+    , generic-deriving+    , hackage-security+    , hpack >=0.35.1+    , http-client+    , http-client-tls+    , http-conduit+    , http-download+    , http-types+    , memory+    , mtl+    , network-uri+    , path+    , path-io+    , persistent+    , persistent-sqlite >=2.9.3+    , persistent-template+    , primitive+    , resourcet+    , rio+    , rio-orphans+    , rio-prettyprint+    , tar-conduit+    , text+    , text-metrics+    , time+    , transformers+    , unix-compat+    , unliftio+    , unordered-containers+    , vector+    , yaml+    , zip-archive+  default-language: Haskell2010+  if os(windows)+    other-modules:+        System.IsWindows+    hs-source-dirs:+        src/windows/+  else+    other-modules:+        System.IsWindows+    hs-source-dirs:+        src/unix/++executable test-pretty-exceptions+  main-is: Main.hs+  other-modules:+      Paths_pantry+  hs-source-dirs:+      app/test-pretty-exceptions+  ghc-options: -Wall+  build-depends:+      Cabal >=3 && <3.11+    , aeson+    , ansi-terminal+    , base >=4.10 && <5+    , bytestring+    , casa-client+    , casa-types+    , conduit+    , conduit-extra+    , containers+    , cryptonite+    , cryptonite-conduit+    , digest+    , filelock+    , generic-deriving+    , hackage-security+    , hpack >=0.35.1+    , http-client+    , http-client-tls+    , http-conduit+    , http-download+    , http-types+    , memory+    , mtl+    , network-uri+    , optparse-applicative+    , pantry+    , path+    , path-io+    , persistent+    , persistent-sqlite >=2.9.3+    , persistent-template+    , primitive+    , resourcet+    , rio+    , rio-orphans+    , rio-prettyprint+    , tar-conduit+    , text+    , text-metrics+    , time+    , transformers+    , unix-compat+    , unliftio+    , unordered-containers+    , vector+    , yaml+    , zip-archive+  default-language: Haskell2010+  if !flag(test-pretty-exceptions)+    buildable: False+  if os(windows)+    other-modules:+        PathAbsExamples+        System.Terminal+    hs-source-dirs:+        app/test-pretty-exceptions/windows/+    build-depends:+        Win32+      , process+  else+    other-modules:+        PathAbsExamples+        System.Terminal+    hs-source-dirs:+        app/test-pretty-exceptions/unix/++test-suite spec+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Pantry.ArchiveSpec+      Pantry.BuildPlanSpec+      Pantry.CabalSpec+      Pantry.CasaSpec+      Pantry.FileSpec+      Pantry.GlobalHintsSpec+      Pantry.HackageSpec+      Pantry.Internal.StaticBytesSpec+      Pantry.InternalSpec+      Pantry.TreeSpec+      Pantry.TypesSpec+      Paths_pantry+  hs-source-dirs:+      test+  ghc-options: -Wall+  build-depends:+      Cabal >=3 && <3.11+    , QuickCheck+    , aeson+    , ansi-terminal+    , base >=4.10 && <5+    , bytestring+    , casa-client+    , casa-types+    , conduit+    , conduit-extra+    , containers+    , cryptonite+    , cryptonite-conduit+    , digest+    , exceptions+    , filelock+    , generic-deriving+    , hackage-security+    , hedgehog+    , hpack >=0.35.1+    , hspec+    , http-client+    , http-client-tls+    , http-conduit+    , http-download+    , http-types+    , memory+    , mtl+    , network-uri+    , pantry+    , path+    , path-io+    , persistent+    , persistent-sqlite >=2.9.3+    , persistent-template+    , primitive+    , raw-strings-qq+    , resourcet+    , rio+    , rio-orphans+    , rio-prettyprint+    , tar-conduit+    , text+    , text-metrics+    , time+    , transformers+    , unix-compat+    , unliftio+    , unordered-containers+    , vector+    , yaml+    , zip-archive+  default-language: Haskell2010
src/Pantry.hs view
@@ -35,6 +35,8 @@ 
     -- ** Exceptions
   , PantryException (..)
+  , Mismatch (..)
+  , FuzzyResults (..)
 
     -- ** Cabal types
   , PackageName
@@ -47,6 +49,8 @@   , RelFilePath (..)
   , ResolvedPath (..)
   , Unresolved
+  , SafeFilePath
+  , mkSafeFilePath
 
     -- ** Cryptography
   , SHA256
@@ -785,13 +789,19 @@                       logDebug $ "Hpack output unchanged in " <> cabalFile
                     Hpack.AlreadyGeneratedByNewerHpack -> logWarn $
                         cabalFile <>
-                        " was generated with a newer version of Hpack,\n" <>
-                        "please upgrade and try again."
+                        " was generated with a newer version of Hpack. Ignoring " <>
+                        fromString (toFilePath hpackFile) <>
+                        " in favor of the Cabal file.\n" <>
+                        "Either please upgrade and try again or, if you want to use the " <>
+                        fromString (toFilePath (filename hpackFile)) <>
+                        " file instead of the Cabal file,\n" <>
+                        "then please delete the Cabal file."
                     Hpack.ExistingCabalFileWasModifiedManually -> logWarn $
                         cabalFile <>
                         " was modified manually. Ignoring " <>
                         fromString (toFilePath hpackFile) <>
-                        " in favor of the Cabal file.\nIf you want to use the " <>
+                        " in favor of the Cabal file.\n" <>
+                        "If you want to use the " <>
                         fromString (toFilePath (filename hpackFile)) <>
                         " file instead of the Cabal file,\n" <>
                         "then please delete the Cabal file."
src/Pantry/HPack.hs view
@@ -57,16 +57,22 @@                     Hpack.OutputUnchanged -> logDebug $ "hpack output unchanged in " <> cabalFile
                     Hpack.AlreadyGeneratedByNewerHpack -> logWarn $
                         cabalFile <>
-                        " was generated with a newer version of hpack,\n" <>
-                        "please upgrade and try again."
+                        " was generated with a newer version of hpack. Ignoring " <>
+                        fromString (toFilePath hpackFile) <>
+                        " in favor of the Cabal file.\n" <>
+                        "Either please upgrade and try again or, if you want to use the " <>
+                        fromString (toFilePath (filename hpackFile)) <>
+                        " file instead of the Cabal file,\n" <>
+                        "then please delete the Cabal file."
                     Hpack.ExistingCabalFileWasModifiedManually -> logWarn $
                         cabalFile <>
                         " was modified manually. Ignoring " <>
                         fromString (toFilePath hpackFile) <>
-                        " in favor of the cabal file.\nIf you want to use the " <>
+                        " in favor of the Cabal file.\n" <>
+                        "If you want to use the " <>
                         fromString (toFilePath (filename hpackFile)) <>
-                        " file instead of the cabal file,\n" <>
-                        "then please delete the cabal file."
+                        " file instead of the Cabal file,\n" <>
+                        "then please delete the Cabal file."
             HpackCommand command ->
                 withWorkingDir (toFilePath pkgDir) $
                 proc command [] runProcess_
src/Pantry/Types.hs view
@@ -131,10 +131,18 @@ import RIO.List (intersperse, groupBy)
 import RIO.Time (toGregorian, Day, UTCTime)
 import qualified RIO.Map as Map
+import RIO.PrettyPrint (bulletedList, fillSep, flow, hang, line, mkNarrativeList, parens, string, style)
+import RIO.PrettyPrint.Types (Style (..))
 import qualified Data.Map.Strict as Map (mapKeysMonotonic)
 import qualified RIO.Set as Set
 import Data.Aeson.Types (toJSONKeyText, Parser)
 import Pantry.Internal.AesonExtended
+         ( FromJSON (..), FromJSONKey (..), FromJSONKeyFunction (..), Object
+         , ToJSON (..), ToJSONKey (..), ToJSONKeyFunction (..), Value (..)
+         , WarningParser, WithJSONWarnings, (..:), (..:?), (..!=), (.=), (.:)
+         , (...:?), jsonSubWarnings, jsonSubWarningsT, noJSONWarnings, object
+         , tellJSONField, withObject, withObjectWarnings, withText
+         )
 import Data.Aeson.Encoding.Internal (unsafeToEncoding)
 import Data.ByteString.Builder (toLazyByteString, byteString, wordDec)
 import Database.Persist
@@ -160,6 +168,7 @@ import Distribution.Types.Version (Version, mkVersion, nullVersion)
 import Network.HTTP.Client (parseRequest)
 import Network.HTTP.Types (Status, statusCode)
+import Text.PrettyPrint.Leijen.Extended (Pretty (..), StyleDoc)
 import Data.Text.Read (decimal)
 import Path (Path, Abs, Dir, File, toFilePath, filename, (</>), parseRelFile)
 import Path.IO (resolveFile, resolveDir)
@@ -392,6 +401,34 @@        then mempty
        else " in subdir " <> display (repoSubdir repo))
 
+instance Pretty RawPackageLocationImmutable where
+  pretty (RPLIHackage pir _tree) = fillSep
+    [ fromString . T.unpack $ textDisplay pir
+    , parens (flow "from Hackage")
+    ]
+  pretty (RPLIArchive archive _pm) = fillSep
+    [ flow "Archive from"
+    , pretty (raLocation archive)
+    , if T.null $ raSubdir archive
+        then mempty
+        else fillSep
+          [ flow "in subdir"
+          , style Dir (fromString $ T.unpack (raSubdir archive))
+          ]
+    ]
+  pretty (RPLIRepo repo _pm) = fillSep
+    [ flow "Repo from"
+    , style Url (fromString $ T.unpack (repoUrl repo)) <> ","
+    , "commit"
+    , fromString $ T.unpack (repoCommit repo)
+    , if T.null $ repoSubdir repo
+        then mempty
+        else fillSep
+          [ flow "in subdir"
+          , style Dir (fromString $ T.unpack (repoSubdir repo))
+          ]
+    ]
+
 -- | Location for remote packages or archives assumed to be immutable.
 --
 -- @since 0.1.0.0
@@ -400,6 +437,7 @@   | PLIArchive !Archive !PackageMetadata
   | PLIRepo    !Repo !PackageMetadata
   deriving (Generic, Show, Eq, Ord, Typeable)
+
 instance NFData PackageLocationImmutable
 
 instance Display PackageLocationImmutable where
@@ -983,6 +1021,9 @@ -- error messages generated by Pantry itself begin with an unique code in the
 -- form `[S-nnn]`, where `nnn` is a three-digit number in the range 100 to 999.
 -- The numbers are selected at random, not in sequence.
+--
+-- Prettier versions of these error messages are also provided. See the instance
+-- of Pretty.
 instance Display PantryException where
   display (InvalidTreeFromCasa blobKey _bs) =
     "Error: [S-258]\n"
@@ -1296,6 +1337,509 @@     <> "The error encountered was:\n\n"
     <> fromString (show err)
 
+-- See also the instance of Display. Although prettier, these messages are
+-- intended to be substantively the same as the corresponding 'black and white'
+-- versions.
+instance Pretty PantryException where
+  pretty (InvalidTreeFromCasa blobKey _bs) =
+    "[S-258]"
+    <> line
+    <> fillSep
+         [ flow "Invalid tree from casa:"
+         , fromString . T.unpack $ textDisplay blobKey
+         ]
+  pretty (PackageIdentifierRevisionParseFail text) =
+    "[S-360]"
+    <> line
+    <> fillSep
+         [ flow "Invalid package identifier (with optional revision):"
+         , fromString $ T.unpack text
+         ]
+  pretty (InvalidCabalFile loc mversion errs warnings) =
+    "[S-242]"
+    <> line
+    <> fillSep
+         [ flow "Unable to parse Cabal file from package"
+         , either pretty pretty loc <> ":"
+         ]
+    <> line
+    <> bulletedList
+         ( map (\(PError pos msg) -> fillSep
+             [ fromString (showPos pos) <> ":"
+             , fromString msg
+             ])
+             errs
+         )
+    <> line
+    <> bulletedList
+         ( map (\(PWarning _ pos msg) -> fillSep
+             [ fromString (showPos pos) <> ":"
+             , fromString msg
+             ])
+             warnings
+         )
+    <> ( case mversion of
+           Just version | version > cabalSpecLatestVersion ->
+                line
+             <> fillSep
+                  [ flow "The Cabal file uses the Cabal specification version"
+                  , style Current (fromString $ versionString version) <> ","
+                  , flow "but we only support up to version"
+                  , fromString (versionString cabalSpecLatestVersion) <> "."
+                  , flow "Recommended action: upgrade your build tool"
+                  , parens (fillSep
+                      [ "e.g."
+                      , style Shell (flow "stack upgrade")
+                      ]) <> "."
+                  ]
+           _ -> mempty
+       )
+  pretty (TreeWithoutCabalFile loc) =
+    "[S-654]"
+    <> line
+    <> fillSep
+         [ flow "No Cabal file found for"
+         , pretty loc <> "."
+         ]
+  pretty (TreeWithMultipleCabalFiles loc sfps) =
+    "[S-500]"
+    <> line
+    <> fillSep
+         ( flow "Multiple Cabal files found for"
+         : (pretty loc <> ":")
+         : mkNarrativeList (Just File) False
+             (map (fromString . T.unpack . textDisplay) sfps :: [StyleDoc])
+         )
+  pretty (MismatchedCabalName fp name) =
+    "[S-910]"
+    <> line
+    <> fillSep
+         [ flow "The Cabal file"
+         , pretty fp
+         , flow "is not named after the package that it defines. Please rename"
+         , flow "the file to"
+         , style File (fromString $ packageNameString name <> ".cabal") <> "."
+         , flow "Hackage rejects packages where the first part of the Cabal"
+         , flow "file name is not the package name."
+         ]
+  pretty (NoCabalFileFound dir) =
+    "[S-636]"
+    <> line
+    <> fillSep
+         [ flow "Stack looks for packages in the directories configured in the"
+         , style Shell "packages"
+         , "and"
+         , style Shell "extra-deps"
+         , flow "fields defined in your"
+         , style File "stack.yaml" <> "."
+         , flow "The current entry points to"
+         , pretty dir
+         , flow "but no Cabal file or"
+         , style File "package.yaml"
+         , flow "could be found there."
+         ]
+  pretty (MultipleCabalFilesFound dir files) =
+    "[S-368]"
+    <> line
+    <> fillSep
+         ( flow "Multiple Cabal files found in directory"
+         : (pretty dir <> ":")
+         : mkNarrativeList (Just File) False
+             (map (pretty . filename) files)
+         )
+  pretty (InvalidWantedCompiler t) =
+    "[S-204]"
+    <> line
+    <> fillSep
+         [ flow "Invalid wanted compiler:"
+         , style Current (fromString $ T.unpack t) <> "."
+         ]
+  pretty (InvalidSnapshotLocation dir t) =
+    "[S-935]"
+    <> line
+    <> fillSep
+         [ flow "Invalid snapshot location"
+         , style Current (fromString $ T.unpack t)
+         , flow "relative to directory"
+         , pretty dir <> "."
+         ]
+  pretty (InvalidOverrideCompiler x y) =
+    "[S-287]"
+    <> line
+    <> fillSep
+         [ flow "Specified compiler for a resolver"
+         , parens (style Shell (fromString . T.unpack $ textDisplay x))
+         , flow "but also specified an override compiler"
+         , parens (style Shell (fromString . T.unpack $ textDisplay y)) <> "."
+         ]
+  pretty (InvalidFilePathSnapshot t) =
+    "[S-617]"
+    <> line
+    <> fillSep
+         [ flow "Specified snapshot as file path with"
+         , style File (fromString $ T.unpack t) <> ","
+         , flow "but not reading from a local file."
+         ]
+  pretty (InvalidSnapshot loc err) =
+    "[S-775]"
+    <> line
+    <> fillSep
+         [ flow "Exception while reading snapshot from"
+         , pretty loc <> ":"
+         ]
+    <> blankLine
+    <> string (displayException err)
+  pretty (MismatchedPackageMetadata loc pm mtreeKey foundIdent) =
+    "[S-427]"
+    <> line
+    <> fillSep
+         [ flow "Mismatched package metadata for"
+         , pretty loc <> "."
+         ]
+    <> blankLine
+    <> hang 10 (fillSep
+         [ "Expected:"
+         , let t = textDisplay pm
+           in  if T.null t
+                 then "nothing."
+                 else fromString $ T.unpack t <> "."
+         ])
+    <> line
+    <> hang 10 (fillSep
+         [ "Found:   "
+         , fromString $ packageIdentifierString foundIdent <> case mtreeKey of
+             Nothing -> "."
+             _ -> mempty
+         , case mtreeKey of
+             Nothing -> mempty
+             Just treeKey -> fillSep
+               [ "with tree"
+               , fromString . T.unpack $ textDisplay treeKey <> "."
+               ]
+         ])
+  pretty (Non200ResponseStatus status) =
+    "[S-571]"
+    <> line
+    <> fillSep
+         [ flow "Unexpected non-200 HTTP status code:"
+         , (fromString . show $ statusCode status) <> "."
+         ]
+  pretty (InvalidBlobKey Mismatch{..}) =
+    "[S-236]"
+    <> line
+    <> fillSep
+         [ flow "Invalid blob key found, expected:"
+         , fromString . T.unpack $ textDisplay mismatchExpected <> ","
+         , "actual:"
+         , fromString . T.unpack $ textDisplay mismatchActual <> "."
+         ]
+  pretty (Couldn'tParseSnapshot sl err) =
+    "[S-645]"
+    <> line
+    <> fillSep
+         [ flow "Couldn't parse snapshot from"
+         , pretty sl <> ":"
+         ]
+    <> blankLine
+    <> string err
+  pretty (WrongCabalFileName loc sfp name) =
+    "[S-575]"
+    <> line
+    <> fillSep
+         [ flow "Wrong Cabal file name for package"
+         , pretty loc <> "."
+         , flow "The Cabal file is named"
+         , style File (fromString . T.unpack $ textDisplay sfp) <> ","
+         , flow "but package name is"
+         , fromString (packageNameString name) <> "."
+         , flow "For more information, see"
+         , style Url "https://github.com/commercialhaskell/stack/issues/317"
+         , "and"
+         , style Url "https://github.com/commercialhaskell/stack/issues/895" <> "."
+         ]
+  pretty (DownloadInvalidSHA256 url Mismatch {..}) =
+    "[S-394]"
+    <> line
+    <> fillSep
+         [ flow "Mismatched SHA256 hash from"
+         , style Url (fromString $ T.unpack url) <> "."
+         ]
+    <> blankLine
+    <> hang 10 (fillSep
+         [ "Expected:"
+         , fromString . T.unpack $ textDisplay mismatchExpected <> "."
+         ])
+    <> line
+    <> hang 10 (fillSep
+         [ "Actual:  "
+         , fromString . T.unpack $ textDisplay mismatchActual <> "."
+         ])
+  pretty (DownloadInvalidSize url Mismatch {..}) =
+    "[S-401]"
+    <> line
+    <> fillSep
+         [ flow "Mismatched download size from"
+         , style Url (fromString $ T.unpack url) <> "."
+         ]
+    <> blankLine
+    <> hang 10 (fillSep
+         [ "Expected:"
+         , fromString . T.unpack $ textDisplay mismatchExpected <> "."
+         ])
+    <> line
+    <> hang 10 (fillSep
+         [ "Actual:  "
+         , fromString . T.unpack $ textDisplay mismatchActual <> "."
+         ])
+  pretty (DownloadTooLarge url Mismatch {..}) =
+    "[S-113]"
+    <> line
+    <> fillSep
+         [ flow "Download from"
+         , style Url (fromString $ T.unpack url)
+         , flow "was too large. Expected:"
+         , fromString . T.unpack $ textDisplay mismatchExpected <> ","
+         , flow "stopped after receiving:"
+         , fromString . T.unpack $ textDisplay mismatchActual <> "."
+         ]
+  pretty (LocalInvalidSHA256 path Mismatch {..}) =
+    "[S-834]"
+    <> line
+    <> fillSep
+         [ flow "Mismatched SHA256 hash from"
+         , pretty path <> "."
+         ]
+    <> blankLine
+    <> hang 10 (fillSep
+         [ "Expected:"
+         , fromString . T.unpack $ textDisplay mismatchExpected <> "."
+         ])
+    <> line
+    <> hang 10 (fillSep
+         [ "Actual:  "
+         , fromString . T.unpack $ textDisplay mismatchActual <> "."
+         ])
+  pretty (LocalInvalidSize path Mismatch {..}) =
+    "[S-713]"
+    <> line
+    <> fillSep
+         [ flow "Mismatched file size from"
+         , pretty path <> "."
+         ]
+    <> blankLine
+    <> hang 10 (fillSep
+         [ "Expected:"
+         , fromString . T.unpack $ textDisplay mismatchExpected <> "."
+         ])
+    <> line
+    <> hang 10 (fillSep
+         [ "Actual:  "
+         , fromString . T.unpack $ textDisplay mismatchActual <> "."
+         ])
+  pretty (UnknownArchiveType loc) =
+    "[S-372]"
+    <> line
+    <> fillSep
+         [ flow "Unable to determine archive type of:"
+         , pretty loc <> "."
+         ]
+  pretty (InvalidTarFileType loc fp x) =
+    "[S-950]"
+    <> line
+    <> fillSep
+         [ flow "Unsupported tar file type in archive"
+         , pretty loc
+         , flow "at file"
+         , style File (fromString fp) <> ":"
+         , fromString $ show x <> "."
+         ]
+  pretty (UnsupportedTarball loc err) =
+    "[S-760]"
+    <> line
+    <> fillSep
+         [ flow "Unsupported tarball from"
+         , pretty loc <> ":"
+         ]
+    <> blankLine
+    <> string (T.unpack err)
+  pretty (NoHackageCryptographicHash ident) =
+    "[S-922]"
+    <> line
+    <> fillSep
+         [ flow "No cryptographic hash found for Hackage package"
+         , fromString (packageIdentifierString ident) <> "."
+         ]
+  pretty (FailedToCloneRepo repo) =
+    "[S-109]"
+    <> line
+    <> fillSep
+         [ flow "Failed to clone repository"
+         , fromString . T.unpack $ textDisplay repo
+         ]
+  pretty (TreeReferencesMissingBlob loc sfp key) =
+    "[S-237]"
+    <> line
+    <> fillSep
+         [ flow "The package"
+         , pretty loc
+         , flow "needs blob"
+         , fromString . T.unpack $ textDisplay key
+         , flow "for file path"
+         , style File (fromString . T.unpack $ textDisplay sfp) <> ","
+         , flow "but the blob is not available."
+         ]
+  pretty (CompletePackageMetadataMismatch loc pm) =
+    "[S-984]"
+    <> line
+    <> fillSep
+         [ flow "When completing package metadata for"
+         , pretty loc <> ","
+         , flow "some values changed in the new package metadata:"
+         , fromString . T.unpack $ textDisplay pm <> "."
+         ]
+  pretty (CRC32Mismatch loc fp Mismatch {..}) =
+    "[S-607]"
+    <> line
+    <> fillSep
+         [ flow "CRC32 mismatch in Zip file from"
+         , pretty loc
+         , flow "on internal file"
+         , style File (fromString fp)
+         ]
+    <> blankLine
+    <> hang 10 (fillSep
+         [ "Expected:"
+         , fromString . T.unpack $ textDisplay mismatchExpected <> "."
+         ])
+    <> line
+    <> hang 10 (fillSep
+         [ "Actual:  "
+         , fromString . T.unpack $ textDisplay mismatchActual <> "."
+         ])
+  pretty (UnknownHackagePackage pir fuzzy) =
+    "[S-476]"
+    <> line
+    <> fillSep
+         [ flow "Could not find"
+         , style Error (fromString . T.unpack $ textDisplay pir)
+         , flow "on Hackage."
+         ]
+    <> prettyFuzzy fuzzy
+  pretty (CannotCompleteRepoNonSHA1 repo) =
+    "[S-112]"
+    <> line
+    <> fillSep
+         [ flow "Cannot complete repo information for a non SHA1 commit due to"
+         , "non-reproducibility:"
+         , fromString . T.unpack $ textDisplay repo <> "."
+         ]
+  pretty (MutablePackageLocationFromUrl t) =
+    "[S-321]"
+    <> line
+    <> fillSep
+         [ flow "Cannot refer to a mutable package location from a URL:"
+         , style Url (fromString $ T.unpack t) <> "."
+         ]
+  pretty (MismatchedCabalFileForHackage pir Mismatch{..}) =
+    "[S-377]"
+    <> line
+    <> fillSep
+         [ flow "When processing Cabal file for Hackage package"
+         , fromString . T.unpack $ textDisplay pir <> ","
+         , flow "mismatched package identifier."
+         ]
+    <> blankLine
+    <> hang 10 (fillSep
+         [ "Expected:"
+         , fromString (packageIdentifierString mismatchExpected) <> "."
+         ])
+    <> line
+    <> hang 10 (fillSep
+         [ "Actual:  "
+         , fromString (packageIdentifierString mismatchActual) <> "."
+         ])
+  pretty (PackageNameParseFail t) =
+    "[S-580]"
+    <> line
+    <> fillSep
+         [ flow "Invalid package name:"
+         , fromString $ T.unpack t <> "."
+         ]
+  pretty (PackageVersionParseFail t) =
+    "[S-479]"
+    <> line
+    <> fillSep
+         [ flow "Invalid version:"
+         , fromString $ T.unpack t <> "."
+         ]
+  pretty (InvalidCabalFilePath fp) =
+    "[S-824]"
+    <> line
+    <> fillSep
+         [ flow "File path contains a name which is not a valid package name:"
+         , pretty fp <> "."
+         ]
+  pretty (DuplicatePackageNames source pairs') =
+    "[S-674]"
+    <> line
+    <> fillSep
+         [ flow "Duplicate package names"
+         , parens (fromString . T.unpack $ textDisplay source) <> ":"
+         ]
+    <> line
+    <> foldMap
+         ( \(name, locs) ->
+                fromString (packageNameString name) <> ":"
+             <> line
+             <> bulletedList (map pretty locs)
+             <> line
+         )
+         pairs'
+  pretty (MigrationFailure desc fp err) =
+    "[S-536]"
+    <> line
+    <> fillSep
+         [ flow "Encountered error while migrating database"
+         , fromString $ T.unpack desc
+         , flow "located at"
+         , pretty fp <> ":"
+         ]
+    <> blankLine
+    <> string (displayException err)
+  pretty (ParseSnapNameException t) =
+    "[S-994]"
+    <> line
+    <> fillSep
+         [ flow "Invalid snapshot name:"
+         , fromString $ T.unpack t <> "."
+         ]
+  pretty (HpackLibraryException file err) =
+    "[S-305]"
+    <> line
+    <> fillSep
+         [ flow "Failed to generate a Cabal file using the Hpack library on"
+         , "file:"
+         , pretty file <> "."
+         , flow "The error encountered was:"
+         ]
+    <> blankLine
+    <> string err
+  pretty (HpackExeException fp dir err) =
+    "[S-720]"
+    <> line
+    <> fillSep
+         [ flow "Failed to generate a Cabal file using the Hpack executable:"
+         , style File (fromString fp)
+         , flow "in directory:"
+         , pretty dir <> "."
+         , flow "The error encountered was:"
+         ]
+    <> blankLine
+    <> string (displayException err)
+
+blankLine :: StyleDoc
+blankLine = line <> line
+
 data FuzzyResults
   = FRNameNotFound ![PackageName]
   | FRVersionNotFound !(NonEmpty PackageIdentifierRevision)
@@ -1318,6 +1862,32 @@   commaSeparated (NE.map display pirs) <>
   "."
 
+prettyFuzzy :: FuzzyResults -> StyleDoc
+prettyFuzzy (FRNameNotFound names) =
+  case NE.nonEmpty names of
+    Nothing -> mempty
+    Just names' ->
+         line
+      <> fillSep
+           ( flow "Perhaps you meant one of"
+           : mkNarrativeList Nothing False
+               (NE.toList $ NE.map (fromString . packageNameString) names' :: [StyleDoc])
+           )
+prettyFuzzy (FRVersionNotFound pirs) =
+     line
+  <> fillSep
+       ( flow "Possible candidates:"
+       : mkNarrativeList Nothing False
+           (NE.toList $ NE.map (fromString . T.unpack . textDisplay) pirs :: [StyleDoc])
+       )
+prettyFuzzy (FRRevisionNotFound pirs) =
+     line
+  <> fillSep
+       ( flow "The specified revision was not found. Possible candidates:"
+       : mkNarrativeList Nothing False
+           (NE.toList $ NE.map (fromString . T.unpack . textDisplay) pirs :: [StyleDoc])
+       )
+
 orSeparated :: NonEmpty Utf8Builder -> Utf8Builder
 orSeparated xs
   | NE.length xs == 1 = NE.head xs
@@ -1729,6 +2299,10 @@   display (ALUrl url) = display url
   display (ALFilePath resolved) = fromString $ toFilePath $ resolvedAbsolute resolved
 
+instance Pretty ArchiveLocation where
+  pretty (ALUrl url) = style Url (fromString $ T.unpack url)
+  pretty (ALFilePath resolved) = pretty $ resolvedAbsolute resolved
+
 parseArchiveLocationObject :: Object -> WarningParser (Unresolved ArchiveLocation)
 parseArchiveLocationObject o =
     ((o ..: "url") >>= either (fail . T.unpack) pure . validateUrl) <|>
@@ -2269,6 +2843,17 @@   display (RSLFilePath resolved) = display (resolvedRelative resolved)
   display (RSLSynonym syn) = display syn
 
+instance Pretty RawSnapshotLocation where
+  pretty (RSLCompiler compiler) = fromString . T.unpack $ textDisplay compiler
+  pretty (RSLUrl url Nothing) = style Url (fromString $ T.unpack url)
+  pretty (RSLUrl url (Just blob)) = fillSep
+    [ style Url (fromString $ T.unpack  url)
+    , parens (fromString . T.unpack $ textDisplay blob)
+    ]
+  pretty (RSLFilePath resolved) =
+    style File (fromString . T.unpack $ textDisplay (resolvedRelative resolved))
+  pretty (RSLSynonym syn) =
+    style Shell (fromString . T.unpack $ textDisplay syn)
 
 instance ToJSON RawSnapshotLocation where
   toJSON (RSLCompiler compiler) = object ["compiler" .= compiler]