packages feed

hwm-0.5.0: src/HWM/Integrations/Toolchain/Package.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE NoImplicitPrelude #-}

module HWM.Integrations.Toolchain.Package
  ( syncPackages,
    syncPackagesByMode,
    validatePackages,
    addPkgDependency,
    newPackage,
    deriveDependencyGraph,
  )
where

import qualified Data.Text as T
import HWM.Core.Formatting (Status, StatusM, monadStatus)
import HWM.Core.Pkg (IsPkg (..), PackageIO (..), Pkg (..), PkgName (PkgName), PkgSource (..), cabalSource, getVersionIssues, hpackSource)
import HWM.Core.Result (Issue, MonadIssue (injectIssue))
import HWM.Core.Sync (SyncMode (..))
import HWM.Domain.Bounds (Bounds (Bounds))
import HWM.Domain.Config (getRegistryBounds)
import HWM.Domain.ConfigT (ConfigT, askVersion)
import HWM.Domain.Dependencies
  ( Dependencies,
    Dependency (..),
    DependencyBoundsIssue (..),
    DependencyMap (..),
    HasDependencies (..),
    MapDeps (..),
    buildDependencyGraph,
    collectDependencyIssues,
    fromDependencyList,
    reportDependencyIssues,
    singleDeps,
    toDependencyList,
  )
import qualified HWM.Domain.Dependencies as M
import HWM.Domain.Workspace (allPackages, forWorkspace)
import HWM.Integrations.Toolchain.Cabal (CabalPackage, newCabalPackage, readCabalPackage, validateCabalSourceInclusion, validateHackage)
import HWM.Integrations.Toolchain.Hpack (HpackPackage, newHpackPackage, readHpackPackage)
import Relude

newPackage :: FilePath -> PkgName -> ConfigT [(Text, Status)]
newPackage targetDir name = do
  let baseName = PkgName "base"
  version <- askVersion
  base <- fromMaybe (Bounds Nothing Nothing) <$> getRegistryBounds baseName
  let deps = M.singleDeps (Dependency baseName base)
  hpack <- newHpackPackage targetDir name version deps
  cabal <- newCabalPackage targetDir name version deps
  pure [("hpack", hpack), ("cabal", cabal)]

syncPackages :: ConfigT ()
syncPackages = forWorkspace $ updatePackage syncPackage syncPackage

syncPackagesByMode :: SyncMode -> ConfigT ()
syncPackagesByMode SyncModeSync = syncPackages
syncPackagesByMode SyncModeCheck = validatePackages
syncPackagesByMode SyncModeIgnore = pure ()

syncPackage :: (MapDeps a, IsPkg a, HasDependencies a) => PkgSource -> a -> ConfigT (Maybe a)
syncPackage pkg package = do
  noIssues <- hasNoIssues pkg package
  if noIssues
    then pure Nothing
    else
      Just <$> do
        result <- mapDeps (pkg, []) syncDeps package
        (`setVersion` result) <$> askVersion

syncDeps :: (PkgSource, [Text]) -> Dependencies -> ConfigT Dependencies
syncDeps (pkg, path) deps =
  fromDependencyList <$> do
    (issues, results) <- unzip <$> traverse syncDep (toDependencyList deps)
    reportDependencyIssues pkg (concat issues) $> results
  where
    syncDep (Dependency depName depBounds) = do
      bounds <- getRegistryBounds depName
      pure ([DependencyBoundsIssue (T.intercalate ":" path) depName depBounds Nothing | isNothing bounds], Dependency depName (fromMaybe depBounds bounds))

addDeps :: (MapDeps a) => Dependency -> PkgSource -> a -> ConfigT (Maybe a)
addDeps dependency pkg = fmap Just . mapDeps (pkg, []) onlyMain
  where
    onlyMain (_, ["dependencies"]) deps = pure (deps <> singleDeps dependency)
    onlyMain _ deps = pure deps

addPkgDependency :: Dependency -> Pkg -> StatusM ConfigT
addPkgDependency dep = updatePackage (addDeps dep) (addDeps dep)

forFormats :: (IsString a) => (PkgSource -> Pkg -> b) -> (PkgSource -> Pkg -> b) -> Pkg -> [(a, b)]
forFormats hpack cabal pkg =
  map (\s -> ("hpack", hpack s pkg)) (maybeToList $ hpackSource pkg)
    <> [("cabal", cabal (cabalSource pkg) pkg)]

updatePackage :: (PkgSource -> HpackPackage -> ConfigT (Maybe HpackPackage)) -> (PkgSource -> CabalPackage -> ConfigT (Maybe CabalPackage)) -> Pkg -> StatusM ConfigT
updatePackage mapHpack mapCabal = forFormats (rewritePackage . mapHpack) (rewritePackage . mapCabal)

deriveDependencyGraph :: ConfigT DependencyMap
deriveDependencyGraph = buildDependencyGraph (concatMap (toDependencyList . snd) . libDependencies) <$> (allPackages >>= traverse readCabalPackage)
  where
    libDependencies = filter (\x -> fst x == ["library"]) . collectDependencies []

validatePackages :: ConfigT ()
validatePackages = forWorkspace $ forFormats hpack cabal
  where
    hpack src pkg = readHpackPackage pkg >>= validatePackage src
    cabal _ pkg = do
      s1 <- readCabalPackage pkg >>= validatePackage (cabalSource pkg)
      s2 <- validateHackage pkg
      s3 <- validateCabalSourceInclusion pkg
      pure (s1 <> s2 <> s3)

hasNoIssues :: (IsPkg a, HasDependencies a) => PkgSource -> a -> ConfigT Bool
hasNoIssues source pkg = do
  (issues, depIssues) <- collectIssues source pkg
  let depIssues' = filter (isJust . depIssueRegistryBounds) depIssues
  pure (null issues && null depIssues')

collectIssues :: (IsPkg a, HasDependencies a) => PkgSource -> a -> ConfigT ([Issue], [DependencyBoundsIssue])
collectIssues source pkg = do
  versionIssues <- getVersionIssues source pkg
  depIssues <- collectDependencyIssues getRegistryBounds pkg
  pure (versionIssues, depIssues)

validatePackage :: (IsPkg a, HasDependencies a) => PkgSource -> a -> ConfigT Status
validatePackage source package = monadStatus $ do
  (issues, depIssues) <- collectIssues source package
  traverse_ injectIssue issues
  reportDependencyIssues source depIssues