packages feed

hix-0.9.0: lib/Hix/Managed/Build/NixOutput/Analysis.hs

module Hix.Managed.Build.NixOutput.Analysis where

import Data.List.Extra (firstJust, trim)
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Text as Text
import Distribution.Compat.CharParsing (anyChar, char, digit, string)
import qualified Distribution.Compat.Parsing as P
import Distribution.Compat.Parsing (manyTill, sepEndByNonEmpty, skipMany, try)
import Distribution.Parsec (Parsec (..), simpleParsec)
import Distribution.Pretty (Pretty (..))
import Distribution.Simple (Dependency)
import Exon (exon)

import qualified Hix.Data.Dep as Dep
import Hix.Data.Dep (Dep)
import Hix.Data.PackageName (PackageName)
import Hix.Managed.Report (pluralLength)
import Hix.Pretty (prettyText, showP)

newtype BuildErrorId =
  BuildErrorId Text
  deriving stock (Eq, Show)
  deriving newtype (IsString, Ord)

instance Parsec BuildErrorId where
  parsec = do
    manyTill anyChar (try (string "error"))
    manyTill anyChar (char ':')
    manyTill anyChar (try (string "[GHC-"))
    maybe (fail "impossible: invalid digits") (pure . BuildErrorId) . readMaybe =<< (P.some digit <* skipMany anyChar)

newtype UnknownDepMessage =
  UnknownDepMessage PackageName
  deriving stock (Eq, Show)
  deriving newtype (IsString, Ord)

-- TODO maybe we can add a module option that toggles errors in json or something, with which we can reliably extract
-- this information
instance Parsec UnknownDepMessage where
  parsec = do
    _ <- manyTill anyChar (try (string "The Cabal config for '"))
    _local <- tillQuote
    string " in the env '"
    _env <- tillQuote
    string " has a dependency on the nonexistent package '"
    name <- tillQuote
    many anyChar
    pure (fromString name)
    where
      tillQuote = manyTill anyChar (char '\'')

data FailureReason =
  Unclear
  |
  BuildError (NonEmpty BuildErrorId)
  |
  BoundsError (NonEmpty Dep)
  |
  UnknownDep
  deriving stock (Eq, Show)

listThree :: NonEmpty Text -> Text
listThree things =
  [exon|#{examples}#{ellipsis}|]
  where
    (three, rest) = NonEmpty.splitAt 3 things
    examples = Text.intercalate ", " three
    ellipsis = if null rest then "" else "..."

instance Pretty FailureReason where
  pretty = \case
    Unclear -> "unclear"
    BuildError ids ->
      prettyText [exon|build error#{pluralLength ids} #{listThree (coerce ids)}|]
    BoundsError deps ->
      prettyText [exon|bounds error#{pluralLength deps} #{listThree (showP . (.package) <$> deps)}|]
    UnknownDep ->
      "unknown package"

boundsMarker1 :: Text
boundsMarker1 = "Encountered missing or private dependencies:"

boundsMarker2 :: Text
boundsMarker2 = "Error: Setup: " <> boundsMarker1

boundsMarkers :: [Text]
boundsMarkers = [boundsMarker1, boundsMarker2]

newtype CommaSeparatedDeps =
  CommaSeparatedDeps { deps :: NonEmpty Dependency }
  deriving stock (Eq, Show)

instance Parsec CommaSeparatedDeps where
  parsec =
    CommaSeparatedDeps <$> sepEndByNonEmpty parsec (char ',' *> many (char ' ')) <* skipMany anyChar

analyzeLog :: [Text] -> FailureReason
analyzeLog log =
  fromMaybe Unclear variants
  where
    variants =
      (BoundsError <$> bounds)
      <|>
      (BuildError <$> buildErrors)

    buildErrors = nonEmpty (mapMaybe simpleParsec logStrings)

    bounds = nonEmpty (concat (catMaybes (takeWhile isJust (parseDeps <$> afterBoundsMarker))))

    afterBoundsMarker = drop 1 (dropWhile (not . flip elem boundsMarkers) log)

    parseDeps line = do
      CommaSeparatedDeps {deps} <- simpleParsec @CommaSeparatedDeps (trim (toString line))
      pure (Dep.fromCabal <$> toList deps)

    logStrings = toString <$> log

analyzeEarlyFailure :: NonEmpty Text -> Maybe (PackageName, FailureReason)
analyzeEarlyFailure log =
  firstJust simpleParsec (toList logStrings) <&> \ (UnknownDepMessage name) -> (name, UnknownDep)
  where
    logStrings = toString <$> log