packages feed

build-env-1.0.0.0: app/Main.hs

module Main ( main ) where

-- base
import Control.Monad
  ( guard )
import Data.Foldable
  ( for_ )
import System.IO
  ( BufferMode(..), hSetBuffering, stdout )

-- bytestring
import qualified Data.ByteString.Lazy as Lazy.ByteString
  ( readFile, writeFile )

-- containers
import qualified Data.Map as Map
  ( empty )
import qualified Data.Set as Set
  ( empty, member )

-- directory
import System.Directory

-- text
import qualified Data.Text as Text
  ( pack )

-- build-env
import BuildEnv.Build
import BuildEnv.CabalPlan
import BuildEnv.Config
import BuildEnv.File
  ( parseCabalDotConfigPkgs, parseSeedFile )
import BuildEnv.Options
import BuildEnv.Parse
  ( runOptionsParser )

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

main :: IO ()
main = do
  currentDir <- getCurrentDirectory
  Opts { compiler, cabal, mode, verbosity, delTemp, workDir }
    <- runOptionsParser currentDir
  hSetBuffering stdout $ BlockBuffering Nothing
  withCurrentDirectory workDir $
    case mode of
      PlanMode { planModeInputs, planOutput } -> do
        CabalPlanBinary planBinary <-
          computePlanFromInputs delTemp verbosity workDir compiler cabal planModeInputs
        normalMsg verbosity $
          "Writing build plan to '" <> Text.pack planOutput <> "'"
        Lazy.ByteString.writeFile planOutput planBinary
      FetchMode ( FetchDescription { rawFetchDir, fetchInputPlan } ) newOrUpd -> do
        plan <- getPlan delTemp verbosity workDir compiler cabal fetchInputPlan
        fetchDir <- canonicalizePath rawFetchDir
        doFetch verbosity cabal fetchDir True newOrUpd plan
      BuildMode ( Build { buildBuildPlan
                        , buildStart
                        , buildStrategy
                        , buildRawPaths = rawPaths
                        , mbOnlyDepsOf
                        , userUnitArgs } ) -> do
        plan <- getPlan delTemp verbosity workDir compiler cabal buildBuildPlan
        ( pathsForPrep@( Paths { fetchDir }), pathsForBuild )
          <- canonicalizePaths compiler buildStrategy rawPaths
        case buildStart of
          Fetch newOrUpd -> doFetch verbosity cabal fetchDir False newOrUpd plan
          _              -> return ()
        let mbOnlyDepsOfUnits :: Maybe [UnitId]
            mbOnlyDepsOfUnits =
              case mbOnlyDepsOf of
                Nothing -> Nothing
                Just pkgs ->
                  let wantUnit :: PlanUnit -> Maybe UnitId
                      wantUnit pu = do
                        ConfiguredUnit { puPkgName, puId } <- configuredUnitMaybe pu
                        guard ( puPkgName `Set.member` pkgs )
                        return puId
                  in Just $ mapMaybePlanUnits wantUnit plan

        let resumeBuild = case buildStart of { Resume -> True; _ -> False }
        buildPlan verbosity workDir pathsForPrep pathsForBuild
          buildStrategy resumeBuild mbOnlyDepsOfUnits userUnitArgs plan

-- | Generate the contents of @pkg.cabal@ and @cabal.project@ files, using
--
--  - a seed file containing packages to build (with constraints, flags
--    and allow-newer),
--  - a @cabal.config@ freeze file,
--  - explicit packages and allow-newer specified as command-line arguments.
parsePlanInputs :: Verbosity -> FilePath -> PlanInputs -> IO CabalFilesContents
parsePlanInputs verbosity workDir (PlanInputs { planPins, planUnits, planAllowNewer })
  = do (pkgs, fileAllowNewer) <- parsePlanUnits verbosity planUnits
       let
         allAllowNewer = fileAllowNewer <> planAllowNewer
           -- NB: allow-newer specified in the command-line overrides
           -- the allow-newer included in the seed file.
         cabalContents = cabalFileContentsFromPackages pkgs
       projectContents <-
         case planPins of
           Nothing -> return $ cabalProjectContentsFromPackages workDir pkgs Map.empty allAllowNewer
           Just (FromFile pinCabalConfig) -> do
             normalMsg verbosity $
               "Reading 'cabal.config' file at '" <> Text.pack pinCabalConfig <> "'"
             pins <- parseCabalDotConfigPkgs pinCabalConfig
             return $ cabalProjectContentsFromPackages workDir pkgs pins allAllowNewer
           Just (Explicit pins) -> do
             return $ cabalProjectContentsFromPackages workDir pkgs pins allAllowNewer
       return $ CabalFilesContents { cabalContents, projectContents }

-- | Retrieve the seed units we want to build, either from a seed file
-- or from explicit command line arguments.
parsePlanUnits :: Verbosity -> PackageData UnitSpecs -> IO (UnitSpecs, AllowNewer)
parsePlanUnits _ (Explicit units) = return (units, AllowNewer Set.empty)
parsePlanUnits verbosity (FromFile fp) =
  do normalMsg verbosity $
       "Reading seed packages from '" <> Text.pack fp <> "'"
     parseSeedFile fp

-- | Compute a build plan by calling @cabal build --dry-run@ with the generated
-- @pkg.cabal@ and @cabal.project@ files.
computePlanFromInputs :: TempDirPermanence
                      -> Verbosity
                      -> FilePath
                      -> Compiler
                      -> Cabal
                      -> PlanInputs
                      -> IO CabalPlanBinary
computePlanFromInputs delTemp verbosity workDir comp cabal inputs
    = do cabalFileContents <- parsePlanInputs verbosity workDir inputs
         normalMsg verbosity "Computing build plan"
         computePlan delTemp verbosity comp cabal cabalFileContents

-- | Retrieve a cabal build plan, either by computing it or using
-- a pre-existing @plan.json@ file.
getPlan :: TempDirPermanence -> Verbosity -> FilePath -> Compiler -> Cabal -> Plan -> IO CabalPlan
getPlan delTemp verbosity workDir comp cabal planMode = do
   planBinary <-
     case planMode of
       ComputePlan planInputs mbPlanOutputPath -> do
        plan@(CabalPlanBinary planData) <-
          computePlanFromInputs delTemp verbosity workDir comp cabal planInputs
        for_ mbPlanOutputPath \ planOutputPath ->
          Lazy.ByteString.writeFile planOutputPath planData
        return plan
       UsePlan { planJSONPath } ->
         do
           normalMsg verbosity $
             "Reading build plan from '" <> Text.pack planJSONPath <> "'"
           CabalPlanBinary <$> Lazy.ByteString.readFile planJSONPath
   return $ parsePlanBinary planBinary

-- | Fetch all packages in a cabal build plan.
doFetch :: Verbosity
        -> Cabal
        -> FilePath
        -> Bool -- ^ True <=> we are fetching (not building)
                -- (only relevant for error messages)
        -> NewOrExisting
        -> CabalPlan
        -> IO ()
doFetch verbosity cabal fetchDir0 weAreFetching newOrUpd plan = do
  fetchDir       <- canonicalizePath fetchDir0
  fetchDirExists <- doesDirectoryExist fetchDir
  case newOrUpd of
    New | fetchDirExists ->
      error $ unlines $
       "Fetch directory already exists." : existsMsg
          ++ [ "Fetch directory: " <> fetchDir  ]
    Existing | not fetchDirExists ->
      error $ unlines
        [ "Fetch directory must already exist when using --update."
        , "Fetch directory: " <> fetchDir ]
    _ -> return ()
  createDirectoryIfMissing True fetchDir
  normalMsg verbosity $
    "Fetching sources from build plan into directory '" <> Text.pack fetchDir <> "'"
  fetchPlan verbosity cabal fetchDir plan

  where
    existsMsg
      | weAreFetching
      = [ "Use --update to update an existing directory." ]
      | otherwise
      = [ "Use --prefetched to build using a prefetched source directory,"
        , "or --update to continue fetching before building." ]