hwm-0.0.1: src/HWM/Integrations/Toolchain/Package.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NoImplicitPrelude #-}
module HWM.Integrations.Toolchain.Package
( Package (..),
BoundsDiff,
syncPackages,
deriveRegistry,
packageDiffs,
validatePackage,
)
where
import Control.Monad.Except (MonadError (..))
import Data.Aeson (FromJSON (..), ToJSON (..), genericParseJSON, genericToJSON)
import qualified Data.Map as Map
import qualified Data.Set as Set
import HWM.Core.Formatting (Color (..), Format (..), chalk, displayStatus, genMaxLen, padDots, subPathSign)
import HWM.Core.Pkg (Pkg (..), PkgName, pkgMemberId, pkgYamlPath)
import HWM.Core.Result (Issue (..), IssueDetails (..), MonadIssue (..), Severity (..))
import HWM.Core.Version (Version)
import HWM.Domain.ConfigT (ConfigT, askVersion, askWorkspaceGroups)
import HWM.Domain.Dependencies (Dependencies, Dependency (Dependency), DependencyGraph (DependencyGraph), externalRegistry, normalizeDependencies, toDependencyList)
import HWM.Domain.Workspace (memberPkgs, pkgGroupName)
import HWM.Integrations.Toolchain.Cabal (syncCabal)
import HWM.Integrations.Toolchain.Lib
( BoundsDiff,
Libraries,
Library (..),
checkDependencies,
checkLibraries,
checkLibrary,
updateDependencies,
updateLibraries,
updateLibrary,
)
import HWM.Runtime.Files (aesonYAMLOptions, readYaml, rewrite_, statusM)
import HWM.Runtime.UI (putLine, sectionWorkspace)
import Relude
data Package = Package
{ name :: PkgName,
version :: Version,
library :: Maybe Library,
dependencies :: Dependencies,
tests :: Maybe Libraries,
executables :: Maybe Libraries,
benchmarks :: Maybe Libraries,
internalLibraries :: Maybe Libraries,
foreignLibraries :: Maybe Libraries
}
deriving (Show, Generic)
instance FromJSON Package where
parseJSON = genericParseJSON aesonYAMLOptions
instance ToJSON Package where
toJSON = genericToJSON aesonYAMLOptions
updatePackage :: Pkg -> Maybe Package -> ConfigT Package
updatePackage pkg Nothing =
throwError
$ Issue
{ issueTopic = pkgMemberId pkg,
issueMessage = "could not find package file",
issueSeverity = SeverityWarning,
issueDetails = Just GenericIssue {issueFile = pkgYamlPath pkg}
}
updatePackage pkg (Just Package {..}) = do
let path = pkgYamlPath pkg
pkgId = pkgMemberId pkg
newLibrary <- traverse (updateLibrary pkgId "library" path) library
newTests <- updateLibraries pkgId "tests" path tests
newExecutables <- updateLibraries pkgId "executables" path executables
newBenchmarks <- updateLibraries pkgId "benchmarks" path benchmarks
newInternalLibraries <- updateLibraries pkgId "internal" path internalLibraries
newForeignLibraries <- updateLibraries pkgId "foreign" path foreignLibraries
newDependencies <- updateDependencies pkgId "dependencies" path dependencies
newVersion <- askVersion
pure
$ Package
{ version = newVersion,
library = newLibrary,
tests = newTests,
executables = newExecutables,
benchmarks = newBenchmarks,
internalLibraries = newInternalLibraries,
foreignLibraries = newForeignLibraries,
dependencies = newDependencies,
..
}
-- | Determine whether a package already matches the expected configuration.
packageDiffs :: Text -> FilePath -> Package -> ConfigT [BoundsDiff]
packageDiffs memberId path Package {..} = do
depsDiffs <- checkDependencies memberId "dependencies" path dependencies
libraryDiffs <- traverseLibrary "library" library
testsDiffs <- traverseLibraries "tests" tests
executablesDiffs <- traverseLibraries "executables" executables
benchmarksDiffs <- traverseLibraries "benchmarks" benchmarks
internalDiffs <- traverseLibraries "internal" internalLibraries
foreignDiffs <- traverseLibraries "foreign" foreignLibraries
pure
( depsDiffs
<> libraryDiffs
<> testsDiffs
<> executablesDiffs
<> benchmarksDiffs
<> internalDiffs
<> foreignDiffs
)
where
traverseLibrary :: Text -> Maybe Library -> ConfigT [BoundsDiff]
traverseLibrary _ Nothing = pure []
traverseLibrary scope (Just lib) = checkLibrary memberId scope path lib
traverseLibraries :: Text -> Maybe Libraries -> ConfigT [BoundsDiff]
traverseLibraries _ Nothing = pure []
traverseLibraries scope (Just libs) = checkLibraries memberId scope path libs
syncPackages :: ConfigT ()
syncPackages = sectionWorkspace $ do
groups <- askWorkspaceGroups
for_ groups $ \g -> do
putLine ""
putLine $ "• " <> chalk Bold (pkgGroupName g)
dirs <- memberPkgs g
let maxLen = genMaxLen (map pkgMemberId dirs)
for_ dirs $ \pkg -> do
let path = pkgYamlPath pkg
package <- statusM path (rewrite_ path (updatePackage pkg))
cabal <- syncCabal pkg
putLine
( subPathSign
<> padDots maxLen (pkgMemberId pkg)
<> displayStatus [("pkg", package), ("cabal", cabal)]
)
collectPackageDependencies :: Package -> [Dependency]
collectPackageDependencies Package {..} =
normalizeDependencies
( toDependencyList dependencies
<> collectLibrary library
<> collectLibraries tests
<> collectLibraries executables
<> collectLibraries benchmarks
<> collectLibraries internalLibraries
<> collectLibraries foreignLibraries
)
where
collectLibrary :: Maybe Library -> [Dependency]
collectLibrary Nothing = []
collectLibrary (Just Library {dependencies = Nothing}) = []
collectLibrary (Just Library {dependencies = Just deps}) = toDependencyList deps
collectLibraries :: Maybe Libraries -> [Dependency]
collectLibraries Nothing = []
collectLibraries (Just libs) = concatMap (collectLibrary . Just) (Map.elems libs)
deriveRegistry :: (Monad m, MonadError Issue m, MonadIO m) => [Pkg] -> m (Dependencies, DependencyGraph)
deriveRegistry pkgs = do
packages <- traverse (readYaml . pkgYamlPath) pkgs
let graph = deriveDependencyGraph packages
let deps = externalRegistry (map pkgName pkgs) $ concatMap collectPackageDependencies packages
pure (deps, graph)
collectCriticalDependencies :: Package -> [Dependency]
collectCriticalDependencies Package {..} = normalizeDependencies (toDependencyList dependencies <> collectLibrary library)
where
collectLibrary :: Maybe Library -> [Dependency]
collectLibrary Nothing = []
collectLibrary (Just Library {dependencies = Nothing}) = []
collectLibrary (Just Library {dependencies = Just deps}) = toDependencyList deps
deriveDependencyGraph :: [Package] -> DependencyGraph
deriveDependencyGraph pkgs = DependencyGraph $ Map.fromList [(name pkg, internalDeps pkg) | pkg <- pkgs]
where
internalNames = Set.fromList (map name pkgs)
internalDeps pkg = mapMaybe selectInternal (collectCriticalDependencies pkg)
selectInternal (Dependency depName _) =
if Set.member depName internalNames then Just depName else Nothing
-- | Validate package against expected version and configuration
validatePackage :: Pkg -> ConfigT ()
validatePackage pkg = do
let path = pkgYamlPath pkg
pkgId = pkgMemberId pkg
currentPkg <- readYaml path :: ConfigT Package
expectedVersion <- askVersion
let currentVersion = version currentPkg
versionMatch = currentVersion == expectedVersion
diffs <- packageDiffs pkgId path currentPkg
unless versionMatch
$ injectIssue
Issue
{ issueTopic = pkgId,
issueMessage = "version mismatch: " <> format currentVersion <> " → " <> format expectedVersion,
issueSeverity = SeverityWarning,
issueDetails = Just GenericIssue {issueFile = path}
}
unless (null diffs)
$ injectIssue
Issue
{ issueTopic = pkgId,
issueMessage =
let baseMsg =
if versionMatch
then "package out of sync (run 'hwm sync' to fix)"
else "package configuration diverged from expected (run 'hwm sync')"
diffCount = length diffs
countSuffix = if diffCount > 0 then " (" <> show diffCount <> " dependencies differ)" else ""
in baseMsg <> countSuffix,
issueSeverity = SeverityWarning,
issueDetails =
Just
DependencyIssue
{ issueDependencies = map (\(scope, depName, actual, expected) -> (scope, format depName, format actual, format expected)) diffs,
issueFile = path
}
}