packages feed

stackage2nix-0.7.0: src/Stack/Config.hs

{-# LANGUAGE ViewPatterns #-}

module Stack.Config where

import Control.Lens
import Data.Bifunctor
import Data.ByteString as BS
import Data.Coerce
import Data.Foldable as F
import Data.List.NonEmpty as NE
import Data.Maybe
import Data.Semigroup
import Data.Text as T
import Data.Yaml
import Network.URI
import Stack.Config.Yaml as Yaml
import Stack.Types
import System.FilePath as FilePath


newtype StackResolver = StackResolver { fromStackResolver :: Text }
  deriving (Eq, Ord, Show)

makePrisms ''StackResolver

unStackResolver :: StackResolver -> String
unStackResolver = T.unpack . fromStackResolver

newtype Vcs = Vcs { fromVcs :: Text }
  deriving (Eq, Ord, Show)

data Repo = Repo
  { _rVcs    :: Vcs
  , _rUri    :: Text
  , _rCommit :: Text
  } deriving (Eq, Ord, Show)

makeLenses ''Repo

data PackageRevision
  = PrSha256 Text
  | PrRev Text
  deriving (Eq, Ord, Show)

makePrisms ''PackageRevision

data PackageIndex = PackageIndex
  { _piNameVersion :: Text
  , _piRevision    :: Maybe PackageRevision
  } deriving (Eq, Ord, Show)

makeLenses ''PackageIndex

data PackageLocation
  = PlIndex PackageIndex
  | PlFilePath FilePath
  | PlUri URI
  | PlRepo Repo
  deriving (Eq, Ord, Show)

makePrisms ''PackageLocation

data StackPackage = StackPackage
  { _spLocation :: PackageLocation
  , _spExtraDep :: Bool
    -- | Subdirectory containing the cabal file if any specified.
  , _spDir      :: Maybe FilePath
  } deriving (Eq, Ord, Show)

makeLenses ''StackPackage

data StackConfig = StackConfig
  { _scResolver  :: StackResolver
  , _scPackages  :: NonEmpty StackPackage
  } deriving (Eq, Ord, Show)

makeLenses ''StackConfig

fromYamlConfig :: Yaml.Config -> StackConfig
fromYamlConfig c = StackConfig{..}
  where
    _scResolver = coerce $ c ^. cResolver
    -- 'NE.nub' replicates Stack behavior of having a Set of packages rather than list
    _scPackages = NE.nub $ F.foldl' (<>) nePackages yamlExtraDeps
    nePackages = fromMaybe (pure defaultPackage)
      $ fmap sconcat
      $ NE.nonEmpty yamlPackages
    yamlPackages   = fromYamlPackage packagesSimplePath False
      <$> fromMaybe mempty (c ^. cPackages)
    yamlExtraDeps  = fromYamlPackage extraDepsSimplePath True
      <$> fromMaybe mempty (c ^. cExtraDeps)
    defaultPackage = StackPackage (PlFilePath ".") False Nothing

-- | Simple string in a packages section can be an URI or FilePath location
packagesSimplePath :: Text -> StackPackage
packagesSimplePath t = StackPackage (parseSimplePath t) False Nothing

-- | Parse location as URI or a FilePath
parseSimplePath :: Text -> PackageLocation
parseSimplePath (T.unpack -> p) = maybe (PlFilePath p) PlUri $ parseURI p

-- | Simple string in an extra-deps section can be an URI or FilePath or PackageIndex name-version
extraDepsSimplePath :: Text -> StackPackage
extraDepsSimplePath t = StackPackage loc True Nothing
  where
    loc = maybe (parsePackageIndex t) PlUri $ parseURI (T.unpack t)

-- TODO: support new package index format with hash and revision
parsePackageIndex :: Text -> PackageLocation
parsePackageIndex t = if isFilePath t
  then PlFilePath (T.unpack t)
  else PlIndex $ PackageIndex t Nothing

-- Determine wether package is file path
isFilePath :: Text -> Bool
isFilePath = T.any (== FilePath.pathSeparator)

-- | A single 'Yaml.Package' can result in multiple actual
-- packages if it has multiple subdirs specified.
fromYamlPackage
  :: (Text -> StackPackage)
  -> Bool
  -> Yaml.Package
  -> NonEmpty StackPackage
fromYamlPackage fromSimple isExtraDep = \case
  Yaml.PSimple p                                ->
    pure (fromSimple p)
  Yaml.PLocationSimple (Location p extraDep ms) ->
    unroll ms $ StackPackage (parseSimplePath p) (fromMaybe False extraDep)
  Yaml.PLocationGit loc                         ->
    mkStackPackageRepo fromYamlGit loc
  Yaml.PLocationHg loc                          ->
    mkStackPackageRepo fromYamlHg loc
  Yaml.PArchive a                               ->
    unroll (a ^. aSubdirs) $ StackPackage (a ^. aArchive . to parseSimplePath) isExtraDep
  Yaml.PNewGit ng                               -> unroll (ng ^. ngSubdirs)
    $ StackPackage (PlRepo $ fromYamlNewGit ng) isExtraDep
  Yaml.PNewHg nh                                -> unroll (nh ^. nhSubdirs)
    $ StackPackage (PlRepo $ fromYamlNewHg nh) isExtraDep
  where
    mkStackPackageRepo f loc = unroll (loc ^. lSubdirs)
      $ StackPackage (PlRepo (loc ^. lLocation . to f)) (fromMaybe isExtraDep $ loc ^. lExtraDep)
    -- Each package gets a single directory with cabal file in it. If
    -- it's not specified, path is empty.
    unroll subs p = case subs of
      Just (x : xs) -> NE.map (p . Just) (x :| xs)
      _ -> p Nothing :| []

fromYamlGit :: Yaml.Git -> Repo
fromYamlGit yg = Repo
  { _rVcs    = Vcs "git"
  , _rUri    = yg ^. gGit
  , _rCommit = yg ^. gCommit }

fromYamlHg :: Yaml.Hg -> Repo
fromYamlHg yh = Repo
  { _rVcs    = Vcs "hg"
  , _rUri    = yh ^. hHg
  , _rCommit = yh ^. hCommit }

fromYamlNewGit :: NewGit -> Repo
fromYamlNewGit ng = Repo
  { _rVcs    = Vcs "git"
  , _rUri    = ng ^. ngGit
  , _rCommit = ng ^. ngCommit }

fromYamlNewHg :: NewHg -> Repo
fromYamlNewHg nh = Repo
  { _rVcs    = Vcs "hg"
  , _rUri    = nh ^. nhHg
  , _rCommit = nh ^. nhCommit }

readStackConfig :: StackYaml -> IO (Either String StackConfig)
readStackConfig stackYaml = do
  let
    relativeToStackYaml = \case
      PlFilePath p -> PlFilePath $ stackYaml ^. syDirName </> p
      packageLocation -> packageLocation
    mkStackConfig = over (scPackages . traversed . spLocation) relativeToStackYaml
      . fromYamlConfig
  second mkStackConfig . decodeEither <$> BS.readFile (stackYaml ^. syFilePath)