packages feed

stack-2.11.1: src/Stack/SDist.hs

{-# LANGUAGE NoImplicitPrelude  #-}
{-# LANGUAGE OverloadedStrings  #-}
{-# LANGUAGE TypeFamilies       #-}

-- Types and functions related to Stack's @sdist@ command.
module Stack.SDist
  ( SDistOpts (..)
  , sdistCmd
  , getSDistTarball
  , checkSDistTarball
  , checkSDistTarball'
  ) where

import qualified Codec.Archive.Tar as Tar
import qualified Codec.Archive.Tar.Entry as Tar
import qualified Codec.Compression.GZip as GZip
import           Conduit ( runConduitRes, sourceLazy, sinkFileCautious )
import           Control.Concurrent.Execute
                   ( ActionContext (..), Concurrency (..) )
import qualified Data.ByteString as S
import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy as L
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
import qualified Data.Text.Encoding as T
import qualified Data.Text.Encoding.Error as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TLE
import           Data.Time.Clock.POSIX ( getPOSIXTime, utcTimeToPOSIXSeconds )
import           Distribution.Package ( Dependency (..) )
import qualified Distribution.PackageDescription as Cabal
import qualified Distribution.PackageDescription.Check as Check
import qualified Distribution.PackageDescription.Parsec as Cabal
import           Distribution.PackageDescription.PrettyPrint
                   ( showGenericPackageDescription )
import           Distribution.Version
                   ( simplifyVersionRange, orLaterVersion, earlierVersion
                   , hasUpperBound, hasLowerBound
                   )
import           Path ( (</>), parent, parseRelDir, parseRelFile )
import           Path.IO ( ensureDir, resolveDir' )
import           Stack.Build ( mkBaseConfigOpts, build, buildLocalTargets )
import           Stack.Build.Execute
                   ( ExcludeTHLoading (..), KeepOutputOpen (..), withExecuteEnv
                   , withSingleContext
                   )
import           Stack.Build.Installed ( getInstalled, toInstallMap )
import           Stack.Build.Source ( projectLocalPackages )
import           Stack.Constants ( stackProgName, stackProgName' )
import           Stack.Constants.Config ( distDirFromDir )
import           Stack.Package
                   ( PackageDescriptionPair (..), 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.BuildConfig
                   ( BuildConfig (..), HasBuildConfig (..), stackYamlL )
import           Stack.Types.BuildOpts
                   ( BuildOpts (..), defaultBuildOpts, 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.Package
                   ( InstallMap, Installed (..), InstalledMap, LocalPackage (..)
                   , Package (..), PackageConfig (..), installedVersion
                   , packageIdentifier
                   )
import           Stack.Types.Platform ( HasPlatform (..) )
import           Stack.Types.PvpBounds ( PvpBounds (..), PvpBoundsType (..) )
import           Stack.Types.Runner ( HasRunner, Runner )
import           Stack.Types.SourceMap
                   ( CommonPackage (..), ProjectPackage (..), SMWanted (..)
                   , SourceMap (..), ppRoot
                   )
import           Stack.Types.Version
                   ( intersectVersionRanges, nextMajorVersion )
import           System.Directory
                   ( copyFile, createDirectoryIfMissing, executable
                   , getModificationTime, getPermissions
                   )
import qualified System.FilePath as FP

-- | Type representing exceptions thrown by functions exported by the
-- "Stack.SDist" module.
data SDistException
  = CheckException (NonEmpty Check.PackageCheck)
  | CabalFilePathsInconsistentBug (Path Abs File) (Path Abs File)
  | ToTarPathException String
  deriving (Show, Typeable)

instance Exception SDistException where
  displayException (CheckException xs) = unlines $
    [ "Error: [S-6439]"
    , "Package check reported the following errors:"
    ] <> fmap show (NE.toList xs)
  displayException (CabalFilePathsInconsistentBug cabalfp cabalfp') = concat
    [ "Error: [S-9595]\n"
    , "The impossible happened! Two Cabal file paths are inconsistent: "
    , show (cabalfp, cabalfp')
    ]
  displayException (ToTarPathException e) =
    "Error: [S-7875\n"
    ++ e
-- | 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
  }

-- | Function underlying the @stack sdist@ command.
sdistCmd :: SDistOpts -> RIO Runner ()
sdistCmd sdistOpts =
  withConfig YesReexec $ withDefaultEnvConfig $ do
    -- If no directories are specified, build all sdist tarballs.
    dirs' <- if null (sdoptsDirsToWorkWith sdistOpts)
      then do
        dirs <- view $ buildConfigL.to (map ppRoot . Map.elems . smwProject . bcSMWanted)
        when (null dirs) $ do
          stackYaml <- view stackYamlL
          prettyErrorL
            [ style Shell "stack sdist"
            , flow "expects a list of targets, and otherwise defaults to all of the project's packages."
            , flow "However, the configuration at"
            , pretty stackYaml
            , flow "contains no packages, so no sdist tarballs will be generated."
            ]
          exitFailure
        pure dirs
      else mapM resolveDir' (sdoptsDirsToWorkWith sdistOpts)
    forM_ dirs' $ \dir -> do
      (tarName, tarBytes, _mcabalRevision) <-
        getSDistTarball (sdoptsPvpBounds sdistOpts) dir
      distDir <- distDirFromDir dir
      tarPath <- (distDir </>) <$> parseRelFile tarName
      ensureDir (parent tarPath)
      runConduitRes $
        sourceLazy tarBytes .|
        sinkFileCautious (toFilePath tarPath)
      prettyInfoL
        [flow "Wrote sdist-format compressed archive to"
        , pretty tarPath <> "."
        ]
      checkSDistTarball sdistOpts tarPath
      forM_ (sdoptsTarPath sdistOpts) $ copyTarToTarPath tarPath tarName
 where
  copyTarToTarPath tarPath tarName targetDir = liftIO $ do
    let targetTarPath = targetDir FP.</> tarName
    createDirectoryIfMissing True $ FP.takeDirectory targetTarPath
    copyFile (toFilePath tarPath) targetTarPath

-- | Given the path to a local package, creates its source
-- distribution tarball.
--
-- While this yields a 'FilePath', the name of the tarball, this
-- tarball is not written to the disk and instead yielded as a lazy
-- bytestring.
getSDistTarball ::
     HasEnvConfig env
  => Maybe PvpBounds            -- ^ Override Config value
  -> Path Abs Dir               -- ^ Path to local package
  -> 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
      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
      Just nonEmptyDepTargets -> do
        eres <- buildLocalTargets nonEmptyDepTargets
        case eres of
          Left err ->
            logError $
              "Error: [S-8399]\n" <>
              "Error building custom-setup dependencies: " <>
              displayShow err
          Right _ ->
            pure ()
      Nothing ->
        prettyWarnS "unexpected empty custom-setup dependencies."
  sourceMap <- view $ envConfigL.to envConfigSourceMap
  installMap <- toInstallMap sourceMap
  (installedMap, _globalDumpPkgs, _snapshotDumpPkgs, _localDumpPkgs) <-
    getInstalled installMap
  let deps = Map.fromList
        [ (pid, ghcPkgId)
        | (_, Library pid ghcPkgId _) <- Map.elems installedMap]
  prettyInfoL
    [ flow "Getting the file list for"
    , style File (fromString  pkgFp) <> "."
    ]
  (fileList, cabalfp) <- getSDistFileList lp deps
  prettyInfoL
    [ flow "Building a compressed archive file in the sdist format for"
    , style File (fromString pkgFp) <> "."
    ]
  files <-
    normalizeTarballPaths (map (T.unpack . stripCR . T.pack) (lines fileList))
  -- We're going to loop below and eventually find the cabal
  -- file. When we do, we'll upload this reference, if the
  -- mpvpBounds value indicates that we should be uploading a cabal
  -- file revision.
  cabalFileRevisionRef <- liftIO (newIORef Nothing)
  -- NOTE: Could make this use lazy I/O to only read files as needed
  -- for upload (both GZip.compress and Tar.write are lazy).
  -- However, it seems less error 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
          Left e -> throwIO $ ToTarPathException e
          Right tp -> pure tp
      -- convert a String of proper characters to a String of bytes
      -- in UTF8 encoding masquerading as characters. This is
      -- necessary for tricking the tar package into proper
      -- character encoding.
      forceUtf8Enc = S8.unpack . T.encodeUtf8 . T.pack
      packWith f isDir fp = liftIO $ f (pkgFp FP.</> fp) =<< tarPath isDir fp
      packDir = packWith Tar.packDirectoryEntry True
      packFile fp
        -- 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
            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
            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))
  dirEntries <- mapM packDir (dirsFromFiles files)
  fileEntries <- mapM packFile files
  mcabalFileRevision <- liftIO (readIORef cabalFileRevisionRef)
  pure
    ( tarName
    , GZip.compress (Tar.write (dirEntries ++ fileEntries))
    , mcabalFileRevision
    )

-- | Get the PVP bounds-enabled version of the given Cabal file
getCabalLbs :: HasEnvConfig env
            => PvpBoundsType
            -> Maybe Int -- ^ optional revision
            -> 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)
  gpd <- liftIO $ gpdio NoPrintWarnings
  unless (cabalfp == cabalfp') $
    throwIO $ CabalFilePathsInconsistentBug cabalfp cabalfp'
  installMap <- toInstallMap sourceMap
  (installedMap, _, _, _) <- getInstalled installMap
  let internalPackages = Set.fromList $
        gpdPackageName gpd :
        map (Cabal.unqualComponentNameToPackageName . fst) (Cabal.condSubLibraries gpd)
      gpd' = gtraverseT (addBounds internalPackages installMap installedMap) gpd
      gpd'' =
        case mrev of
          Nothing -> gpd'
          Just rev -> gpd'
            { Cabal.packageDescription
             = (Cabal.packageDescription gpd')
                { Cabal.customFieldsPD
                = (("x-revision", show rev):)
                $ filter (\(x, _) -> map toLower x /= "x-revision")
                $ Cabal.customFieldsPD
                $ Cabal.packageDescription gpd'
                }
            }
      ident = Cabal.package $ Cabal.packageDescription gpd''
  -- Sanity rendering and reparsing the input, to ensure there are no
  -- cabal bugs, since there have been bugs here before, and currently
  -- are at the time of writing:
  --
  -- https://github.com/haskell/cabal/issues/1202
  -- https://github.com/haskell/cabal/issues/2353
  -- https://github.com/haskell/cabal/issues/4863 (current issue)
  let roundtripErrs =
        [ flow "Bug detected in Cabal library. ((parse . render . parse) === \
               \id) does not hold for the Cabal file at"
        <+> pretty cabalfp
        , ""
        ]
      (_warnings, eres) = Cabal.runParseResult
                        $ Cabal.parseGenericPackageDescription
                        $ T.encodeUtf8
                        $ T.pack
                        $ showGenericPackageDescription gpd
  case eres of
    Right roundtripped
      | roundtripped == gpd -> pure ()
      | otherwise ->
          prettyWarn $ vsep $ roundtripErrs ++
            [ "This seems to be fixed in development versions of Cabal, but \
              \at time of writing, the fix is not in any released versions."
            , ""
            ,  "Please see this GitHub issue for status:" <+> style Url "https://github.com/commercialhaskell/stack/issues/3549"
            , ""
            , fillSep
              [ flow "If the issue is closed as resolved, then you may be \
                     \able to fix this by upgrading to a newer version of \
                     \Stack via"
              , style Shell "stack upgrade"
              , flow "for latest stable version or"
              , style Shell "stack upgrade --git"
              , flow "for the latest development version."
              ]
            , ""
            , fillSep
              [ flow "If the issue is fixed, but updating doesn't solve the \
                     \problem, please check if there are similar open \
                     \issues, and if not, report a new issue to the Stack \
                     \issue tracker, at"
              , style Url "https://github.com/commercialhaskell/stack/issues/new"
              ]
            , ""
            , flow "If the issue is not fixed, feel free to leave a comment \
                   \on it indicating that you would like it to be fixed."
            , ""
            ]
    Left (_version, errs) ->
      prettyWarn $ vsep $ roundtripErrs ++
        [ flow "In particular, parsing the rendered Cabal file is yielding a \
               \parse error. Please check if there are already issues \
               \tracking this, and if not, please report new issues to the \
               \Stack and Cabal issue trackers, via"
        , bulletedList
          [ style Url "https://github.com/commercialhaskell/stack/issues/new"
          , style Url "https://github.com/haskell/cabal/issues/new"
          ]
        , flow $ "The parse error is: " ++ unlines (map show (toList errs))
        , ""
        ]
  pure
    ( ident
    , TLE.encodeUtf8 $ TL.pack $ showGenericPackageDescription gpd''
    )
 where
  addBounds :: Set PackageName -> InstallMap -> InstalledMap -> Dependency -> Dependency
  addBounds internalPackages installMap installedMap dep@(Dependency name range s) =
    if name `Set.member` internalPackages
      then dep
      else case foundVersion of
        Nothing -> dep
        Just version -> Dependency name (simplifyVersionRange
          $ (if toAddUpper && not (hasUpperBound range) then addUpper version else id)
          $ (if toAddLower && not (hasLowerBound range) then addLower version else id)
            range) s
   where
    foundVersion =
      case Map.lookup name installMap of
        Just (_, version) -> Just version
        Nothing ->
          case Map.lookup name installedMap of
            Just (_, installed) -> Just (installedVersion installed)
            Nothing -> Nothing

  addUpper version = intersectVersionRanges
      (earlierVersion $ nextMajorVersion version)
  addLower version = intersectVersionRanges (orLaterVersion version)

  (toAddLower, toAddUpper) =
    case pvpBounds of
      PvpBoundsNone  -> (False, False)
      PvpBoundsUpper -> (False, True)
      PvpBoundsLower -> (True,  False)
      PvpBoundsBoth  -> (True,  True)

-- | Traverse a data type.
gtraverseT :: (Data a,Typeable b) => (Typeable b => b -> b) -> a -> a
gtraverseT f =
  gmapT (\x -> case cast x of
                 Nothing -> gtraverseT f x
                 Just b  -> fromMaybe x (cast (f b)))

-- | Read in a 'LocalPackage' config.  This makes some default decisions
-- about 'LocalPackage' fields that might not be appropriate for other
-- use-cases.
readLocalPackage :: HasEnvConfig env => Path Abs Dir -> RIO env LocalPackage
readLocalPackage pkgDir = do
  config  <- getDefaultPackageConfig
  (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 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
    }

-- | Returns a newline-separate list of paths, and the absolute path to the
-- Cabal file.
getSDistFileList ::
     HasEnvConfig env
  => LocalPackage
  -> Map PackageIdentifier GhcPkgId
  -> RIO env (String, Path Abs File)
getSDistFileList lp deps =
  withSystemTempDir (stackProgName <> "-sdist") $ \tmpdir -> do
    let bopts = defaultBuildOpts
    let boptsCli = defaultBuildOptsCLI
    baseConfigOpts <- mkBaseConfigOpts boptsCli
    locals <- projectLocalPackages
    withExecuteEnv bopts boptsCli baseConfigOpts locals
      [] [] [] 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
          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)
 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
    }

normalizeTarballPaths ::
     (HasRunner env, HasTerm env)
  => [FilePath]
  -> RIO env [FilePath]
normalizeTarballPaths fps = do
  -- TODO: consider whether erroring out is better - otherwise the
  -- user might upload an incomplete tar?
  unless (null outsideDir) $
    prettyWarn $
         flow "These files are outside of the package directory, and will be \
              \omitted from the tarball:"
      <> line
      <> bulletedList (map (style File . fromString) outsideDir)
  pure (nubOrd files)
 where
  (outsideDir, files) = partitionEithers (map pathToEither fps)
  pathToEither fp = maybe (Left fp) Right (normalizePath fp)

normalizePath :: FilePath -> Maybe FilePath
normalizePath = fmap FP.joinPath . go . FP.splitDirectories . FP.normalise
 where
  go [] = Just []
  go ("..":_) = Nothing
  go (_:"..":xs) = go xs
  go (x:xs) = (x :) <$> go xs

dirsFromFiles :: [FilePath] -> [FilePath]
dirsFromFiles dirs = Set.toAscList (Set.delete "." results)
 where
  results = foldl' (\s -> go s . FP.takeDirectory) Set.empty dirs
  go s x
    | Set.member x s = s
    | otherwise = go (Set.insert x s) (FP.takeDirectory x)

-- | Check package in given tarball. This will log all warnings
-- and will throw an exception in case of critical errors.
--
-- Note that we temporarily decompress the archive to analyze it.
checkSDistTarball ::
     HasEnvConfig env
  => SDistOpts -- ^ The configuration of what to check
  -> Path Abs File -- ^ Absolute path to tarball
  -> RIO env ()
checkSDistTarball opts tarball = withTempTarGzContents tarball $ \pkgDir' -> do
  pkgDir  <- (pkgDir' </>) <$>
    (parseRelDir . FP.takeBaseName . FP.takeBaseName . toFilePath $ tarball)
  --               ^ drop ".tar"     ^ drop ".gz"
  when (sdoptsBuildTarball opts)
    ( buildExtractedTarball ResolvedPath
        { resolvedRelative = RelFilePath "this-is-not-used" -- ugly hack
        , resolvedAbsolute = pkgDir
        }
    )
  unless (sdoptsIgnoreCheck opts) (checkPackageInExtractedTarball pkgDir)

checkPackageInExtractedTarball ::
     HasEnvConfig env
  => Path Abs Dir -- ^ Absolute path to tarball
  -> RIO env ()
checkPackageInExtractedTarball pkgDir = do
  (gpdio, name, _cabalfp) <- loadCabalFilePath (Just stackProgName') pkgDir
  gpd <- liftIO $ gpdio YesPrintWarnings
  config  <- getDefaultPackageConfig
  let PackageDescriptionPair pkgDesc _ = resolvePackageDescription config gpd
  prettyInfoL
    [ flow "Checking package"
    , style Current (fromString $ packageNameString name)
    , flow "for common mistakes."
    ]
  let pkgChecks =
        -- MSS 2017-12-12: Try out a few different variants of
        -- pkgDesc to try and provoke an error or warning. I don't
        -- know why, but when using `Just pkgDesc`, it appears that
        -- Cabal does not detect that `^>=` is used with
        -- `cabal-version: 1.24` or earlier. It seems like pkgDesc
        -- (the one we create) does not populate the `buildDepends`
        -- field, whereas flattenPackageDescription from Cabal
        -- does. In any event, using `Nothing` seems more logical
        -- for this check anyway, and the fallback to `Just pkgDesc`
        -- is just a crazy sanity check.
        case Check.checkPackage gpd Nothing of
          [] -> Check.checkPackage gpd (Just pkgDesc)
          x -> x
  fileChecks <- liftIO $ Check.checkPackageFiles minBound pkgDesc (toFilePath pkgDir)
  let checks = pkgChecks ++ fileChecks
      (errors, warnings) =
        let criticalIssue (Check.PackageBuildImpossible _) = True
            criticalIssue (Check.PackageDistInexcusable _) = True
            criticalIssue _ = False
        in  List.partition criticalIssue checks
  unless (null warnings) $
    prettyWarn $
         flow "Package check reported the following warnings:"
      <> line
      <> bulletedList (map (fromString . show) warnings)
  case NE.nonEmpty errors of
    Nothing -> pure ()
    Just ne -> throwM $ CheckException ne

buildExtractedTarball :: HasEnvConfig env => ResolvedPath Dir -> RIO env ()
buildExtractedTarball pkgDir = do
  envConfig <- view envConfigL
  localPackageToBuild <- readLocalPackage $ resolvedAbsolute pkgDir
  -- We remove the path based on the name of the package
  let isPathToRemove path = do
        localPackage <- readLocalPackage path
        pure
          $  packageName (lpPackage localPackage)
          == packageName (lpPackage localPackageToBuild)
  pathsToKeep
    <- fmap Map.fromList
     $ flip filterM (Map.toList (smwProject (bcSMWanted (envConfigBuildConfig envConfig))))
     $ fmap not . isPathToRemove . resolvedAbsolute . ppResolvedDir . snd
  pp <- mkProjectPackage YesPrintWarnings pkgDir False
  let adjustEnvForBuild env =
        let updatedEnvConfig = envConfig
              { envConfigSourceMap =
                  updatePackagesInSourceMap (envConfigSourceMap envConfig)
              , envConfigBuildConfig =
                  updateBuildConfig (envConfigBuildConfig envConfig)
              }
            updateBuildConfig bc = bc
              { bcConfig = (bcConfig bc)
                 { configBuild = defaultBuildOpts { boptsTests = True } }
              }
        in  set envConfigL updatedEnvConfig env
      updatePackagesInSourceMap sm =
        sm {smProject = Map.insert (cpName $ ppCommon pp) pp pathsToKeep}
  local adjustEnvForBuild $ build Nothing

-- | Version of 'checkSDistTarball' that first saves lazy bytestring to
-- temporary directory and then calls 'checkSDistTarball' on it.
checkSDistTarball' ::
     HasEnvConfig env
  => SDistOpts
  -> String       -- ^ Tarball name
  -> L.ByteString -- ^ Tarball contents as a byte string
  -> RIO env ()
checkSDistTarball' opts name bytes = withSystemTempDir "stack" $ \tpath -> do
  npath   <- (tpath </>) <$> parseRelFile name
  liftIO $ L.writeFile (toFilePath npath) bytes
  checkSDistTarball opts npath

withTempTarGzContents ::
     Path Abs File
     -- ^ Location of tarball
  -> (Path Abs Dir -> RIO env a)
     -- ^ Perform actions given dir with tarball contents
  -> RIO env a
withTempTarGzContents apath f = withSystemTempDir "stack" $ \tpath -> do
  archive <- liftIO $ L.readFile (toFilePath apath)
  liftIO . Tar.unpack (toFilePath tpath) . Tar.read . GZip.decompress $ archive
  f tpath

--------------------------------------------------------------------------------

-- Copy+modified from the tar package to avoid issues with lazy IO ( see
-- https://github.com/commercialhaskell/stack/issues/1344 )

packFileEntry :: FilePath -- ^ Full path to find the file on the local disk
              -> Tar.TarPath  -- ^ Path to use for the tar Entry in the archive
              -> IO Tar.Entry
packFileEntry filepath tarpath = do
  mtime   <- getModTime filepath
  perms   <- getPermissions filepath
  content <- S.readFile filepath
  let size = fromIntegral (S.length content)
  pure (Tar.simpleEntry tarpath (Tar.NormalFile (L.fromStrict content) size)) {
    Tar.entryPermissions = if executable perms
                             then Tar.executableFilePermissions
                             else Tar.ordinaryFilePermissions,
    Tar.entryTime = mtime
  }

getModTime :: FilePath -> IO Tar.EpochTime
getModTime path = do
  t <- getModificationTime path
  pure $ floor . utcTimeToPOSIXSeconds $ t

getDefaultPackageConfig :: (MonadIO m, MonadReader env m, HasEnvConfig env)
  => m PackageConfig
getDefaultPackageConfig = do
  platform <- view platformL
  compilerVersion <- view actualCompilerVersionL
  pure PackageConfig
    { packageConfigEnableTests = False
    , packageConfigEnableBenchmarks = False
    , packageConfigFlags = mempty
    , packageConfigGhcOptions = []
    , packageConfigCabalConfigOpts = []
    , packageConfigCompilerVersion = compilerVersion
    , packageConfigPlatform = platform
    }