packages feed

hix-0.7.0: lib/Hix/Hackage.hs

module Hix.Hackage where

import Control.Monad.Extra (fromMaybeM)
import Data.Aeson (FromJSON (parseJSON), eitherDecodeStrict', withObject, (.:))
import Data.IORef (IORef, modifyIORef', readIORef)
import qualified Data.Map.Strict as Map
import Data.Map.Strict ((!?))
import qualified Data.Text as Text
import Distribution.Parsec (eitherParsec)
import Hix.Data.Version (Version)
import Exon (exon)
import Network.HTTP.Client (Manager, Request (..), Response (..), defaultRequest, httpLbs)
import Network.HTTP.Types (
  Status (statusCode, statusMessage),
  hAccept,
  statusIsClientError,
  statusIsServerError,
  statusIsSuccessful,
  )
import System.Exit (ExitCode (ExitFailure, ExitSuccess))
import System.Process.Typed (proc, readProcess)

import Hix.Data.Error (Error (Fatal))
import Hix.Data.PackageId (PackageId, renderPackage)
import Hix.Data.PackageName (PackageName)
import Hix.Data.Version (SourceHash (SourceHash))
import qualified Hix.Log as Log
import Hix.Monad (M, throwM, tryIOM)
import Hix.Pretty (showP)

data HackageVersions =
  HackageVersions {
    versions :: [String]
  }
  deriving stock (Eq, Show, Generic)

instance FromJSON HackageVersions where
  parseJSON = withObject "HackageVersions" \ o -> HackageVersions <$> o .: "normal-version"

parseVersion :: String -> Either (String, String) Version
parseVersion s = first (s,) (eitherParsec s)

parseResult :: LByteString -> M (Either Text [Version])
parseResult body =
  case eitherDecodeStrict' (toStrict body) of
    Left err ->
      noVersion [exon|Hackage response parse error: #{toText err}|]
    Right (HackageVersions []) ->
      noVersion "No versions on Hackage"
    Right (HackageVersions versions) ->
      case traverse parseVersion versions of
        Left (v, err) -> noVersion (toText [exon|Version '#{v}' has invalid format (#{err})|])
        Right vs -> pure (Right vs)
  where
    noVersion = pure . Left

versionsHackage :: Manager -> PackageName -> M [Version]
versionsHackage manager pkg = do
  res <- liftIO (httpLbs request manager)
  let
    body = responseBody res

    status = responseStatus res

    errorStatus category = noVersion [exon|#{category} (#{decodeUtf8 (statusMessage status)})|]

  if
    | statusIsSuccessful status -> leftA noVersion =<< parseResult body
    | statusCode status == 404 -> noVersion "PackageId does not exist"
    | statusIsClientError status -> errorStatus "Client error"
    | statusIsServerError status -> errorStatus "Server error"
    | otherwise -> errorStatus "Weird error"

  where
    request =
      defaultRequest {
        host = "hackage.haskell.org",
        secure = False,
        method = "GET",
        path = [exon|/package/##{pkg}/preferred|],
        requestHeaders = [(hAccept, "application/json")]
      }

    noVersion msg =
      [] <$ Log.error [exon|Hackage request for '##{pkg}' failed: #{msg}|]

latestVersionHackage :: Manager -> PackageName -> M (Maybe Version)
latestVersionHackage manager pkg =
  head <$> versionsHackage manager pkg

fetchHashHackage ::
  PackageId ->
  M SourceHash
fetchHashHackage package = do
  Log.debug [exon|Fetching hash for '##{slug}' from ##{url}|]
  tryIOM (readProcess conf) >>= \case
    (ExitFailure _, _, err) ->
      throwM (Fatal [exon|Prefetching source of '##{slug}' from hackage failed: #{decodeUtf8 err}|])
    (ExitSuccess, hash, _) ->
      pure (SourceHash (Text.stripEnd (decodeUtf8 hash)))
  where
    conf = proc "nix-prefetch-url" ["--unpack", url]
    url = [exon|https://hackage.haskell.org/package/#{slug}/#{slug}.tar.gz|]
    slug = showP package

fetchHashHackageCached ::
  IORef (Map Text SourceHash) ->
  PackageId ->
  M SourceHash
fetchHashHackageCached cacheRef package =
  liftIO (readIORef cacheRef) >>= \ cache ->
    fromMaybeM fetch (pure (cache !? cacheKey))
  where
    fetch = do
      hash <- fetchHashHackage package
      hash <$ addToCache hash

    addToCache hash = liftIO (modifyIORef' cacheRef (Map.insert cacheKey hash))

    cacheKey = renderPackage package