packages feed

hix-0.9.0: lib/Hix/Managed/Build/Adapt.hs

module Hix.Managed.Build.Adapt where

import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State.Strict (StateT, gets, modify')
import qualified Data.Map.Strict as Map
import Data.Map.Strict ((!?))
import qualified Data.Text as Text
import Distribution.Pretty (Pretty (..))
import Exon (exon)
import Text.PrettyPrint (brackets, (<+>))

import Hix.Class.Map (nInsert, (!!))
import qualified Hix.Color as Color
import Hix.Data.Monad (M)
import Hix.Data.Overrides (Override (..), Overrides)
import Hix.Data.PackageId (PackageId (..))
import Hix.Data.PackageName (PackageName)
import Hix.Data.Version (Version)
import qualified Hix.Log as Log
import Hix.Managed.Build.NixOutput.Analysis (FailureReason (..), analyzeEarlyFailure, analyzeLog)
import Hix.Managed.Data.NixOutput (PackageDerivation (..))
import Hix.Managed.Data.StageState (BuildFailure (..), BuildResult (..))
import Hix.Maybe (justIf)
import Hix.Monad (appContextT)
import Hix.Pretty (showP)

data FailedPackage =
  FailedPackage {
    package :: PackageName,
    version :: Maybe Version
  }
  deriving stock (Eq, Show)

failedPackageId :: FailedPackage -> Maybe PackageId
failedPackageId FailedPackage {..} =
  version <&> \ v -> PackageId {name = package, version = v}

data RetryPackage =
  RetryPackage {
    package :: PackageId,
    override :: Override
  }
  deriving stock (Eq, Show)

instance Pretty RetryPackage where
  pretty RetryPackage {..} =
    pretty package <+> brackets (shortOverride override)
    where
      shortOverride = \case
        Override {version} -> pretty version
        Jailbreak -> "jailbreak"
        Local -> "local"

data FailureCounts =
  FailureCounts {
    clear :: Word
  }
  deriving stock (Eq, Show)

incrementOrInit :: Maybe Word -> Maybe Word
incrementOrInit = \case
  Just old -> Just (old + 1)
  Nothing -> Just 1

recordRetry :: RetryPackage -> (Overrides, Map PackageId Word) -> (Overrides, Map PackageId Word)
recordRetry retry (overrides, counts) =
  (
    nInsert retry.package.name retry.override overrides,
    Map.alter incrementOrInit retry.package counts
  )

failureCounts :: NonEmpty FailureReason -> FailureCounts
failureCounts pkgs =
  FailureCounts {clear}
  where
    clear = fromIntegral $ length $ flip filter (toList pkgs) \case
      Unclear -> False
      _ -> True

manageableFailures :: BuildResult -> Maybe (NonEmpty (FailedPackage, FailureReason))
manageableFailures = \case
  BuildFailure (PackageFailure pkgs) ->
    Just [
      (FailedPackage {package = name, version = Just version, ..}, analyzeLog log)
      |
      PackageDerivation {package = PackageId {..}, ..} <- pkgs
    ]
  BuildFailure (UnexpectedFailure log) ->
    analyzeEarlyFailure log <&> \ (package, reason) -> pure (FailedPackage {version = Nothing, ..}, reason)
  _ -> Nothing

buildAdaptive ::
  (Overrides -> M BuildResult) ->
  (FailureCounts -> FailedPackage -> Maybe Override -> FailureReason -> M (Maybe RetryPackage)) ->
  StateT (Overrides, Map PackageId Word) M BuildResult
buildAdaptive runBuild suggestOverride = do
  build
  where
    build = do
      result <- lift . runBuild =<< gets fst
      collectRetries result >>= \case
        Just (Just retries) -> retryFailures retries
        Just Nothing -> result <$ lift (Log.verbose "Could not fix any failures heuristically")
        Nothing -> pure result

    collectRetries failure =
      for (manageableFailures failure) \ reasons -> do
        let counts = failureCounts (snd <$> reasons)
        potentialOverrides <- catMaybes <$> traverse (findOverride counts) (toList reasons)
        overrides <- catMaybes <$> traverse limitOverrides potentialOverrides
        pure (nonEmpty overrides)

    retryFailures :: NonEmpty RetryPackage -> StateT (Overrides, Map PackageId Word) M BuildResult
    retryFailures retries = do
      lift $ Log.verbose [exon|Some packages failed, retrying with overrides: #{ids}|]
      traverse_ (modify' . recordRetry) retries
      build
      where
        ids = Text.intercalate ", " (Color.package <$> toList retries)

    limitOverrides pkg =
      gets (\ (_, counts) -> justIf (fromMaybe 0 (counts !? pkg.package) < 4) pkg)

    findOverride counts (pkg, reason) =
      appContextT [exon|finding an override for #{Color.package pkg.package} (#{showP reason})|] do
        old <- gets \ (ovs, _) -> ovs !! pkg.package
        lift (suggestOverride counts pkg old reason)