packages feed

tilia-0.1.3.0: src/Tilia/Fixity/Cabal.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

-- | Reading a package's description out of its source tarball.
module Tilia.Fixity.Cabal
  ( packageModules,
    cabalFileInArchive,
    ownCabalFile,
    cabalFileAtTop,
    entryPosixPath,
  )
where

import Codec.Archive.Tar qualified as Tar
import Codec.Archive.Tar.Entry qualified as Tar
import Codec.Compression.GZip qualified as GZip
import Control.Applicative ((<|>))
import Control.DeepSeq (force)
import Control.Exception (evaluate)
import Data.ByteString.Lazy qualified as BL
import Data.List (isSuffixOf)
import Data.Text (Text)
import Distribution.PackageDescription (GenericPackageDescription)
import Tilia.Cabal.Package (declaredModules, parsePackageDescription)
import Tilia.Utils (quietly)

-- | The modules a package holds, read from the @.cabal@ file in its
-- source tarball.
--
-- 'Nothing' if the tarball cannot be read or holds no @.cabal@ file.
packageModules :: FilePath -> IO (Maybe [Text])
packageModules tarball = quietly Nothing $ do
  bytes <- BL.readFile tarball
  evaluate . force $
    fmap fst . declaredModules
      <$> cabalFileInArchive (Tar.read (GZip.decompress bytes))

-- | The first @.cabal@ file at the top level of an archive.
--
-- Stops as soon as it finds one. The archive is decompressed lazily, so a
-- @.cabal@ near the front costs a fraction of the whole file.
cabalFileInArchive :: Tar.Entries e -> Maybe GenericPackageDescription
cabalFileInArchive = \case
  Tar.Next entry rest -> ownCabalFile entry <|> cabalFileInArchive rest
  Tar.Done -> Nothing
  Tar.Fail _ -> Nothing

-- | What an archive's entry describes if it is the package's own @.cabal@
-- file and parses.
ownCabalFile :: Tar.Entry -> Maybe GenericPackageDescription
ownCabalFile entry
  | cabalFileAtTop path,
    Tar.NormalFile content _ <- Tar.entryContent entry =
      either (const Nothing) Just $
        parsePackageDescription path (BL.toStrict content)
  | otherwise = Nothing
  where
    path = entryPosixPath entry

-- | Where an entry sits in its archive, written the way a tar file writes
-- it.
entryPosixPath :: Tar.Entry -> FilePath
entryPosixPath = Tar.fromTarPathToPosixPath . Tar.entryTarPath

-- | Is this the path of a package's own @.cabal@ file?
cabalFileAtTop :: FilePath -> Bool
cabalFileAtTop path = ".cabal" `isSuffixOf` path && depth path == 2
  where
    depth = (1 +) . length . filter (== '/')