{-# LANGUAGE TupleSections #-}
import Control.Monad (forM, when)
import Control.Monad.IO.Class
import Control.Applicative
import Data.Version
import Data.Maybe (listToMaybe)
import Data.Ord (comparing)
import Data.List (sortBy)
import System.Directory
import System.Exit
import System.Process
import System.FilePath
import System.IO
import qualified Data.Set as S
import Data.Either (partitionEithers)
import Control.Error
import Options.Applicative
import qualified Data.ByteString.Char8 as BS
import System.IO.Temp
import Distribution.Verbosity (Verbosity, normal, verbose)
import Distribution.Simple.Compiler (Compiler (compilerId), compilerFlavor)
import Distribution.Simple.GHC
import Distribution.Simple.Program (defaultProgramConfiguration)
import Distribution.Simple.Compiler (PackageDB (..))
import Distribution.Simple.PackageIndex ( PackageIndex, lookupPackageName
, reverseTopologicalOrder
, searchByNameSubstring
)
import Distribution.System (buildPlatform)
import Distribution.InstalledPackageInfo (InstalledPackageInfo_ (..), InstalledPackageInfo)
import Distribution.Package (PackageId, PackageName (..), PackageIdentifier (..))
import qualified Distribution.Simple.InstallDirs as IDirs
-- | Various configuration
data Config = Config { verbosity :: Verbosity
, installTextBase :: Bool
, useLocalDocs :: Bool
, ignoreExistingTextBases :: Bool
, otherPackageDbs :: [PackageDB]
}
opts =
Config <$> flag normal verbose
( short 'v' <> long "verbose"
<> help "Enable verbose output"
)
<*> switch
( short 'i' <> long "install"
<> help "Install generated textbases for future use"
)
<*> switch
( short 'l' <> long "local"
<> help "Use local Haddock documentation when available"
)
<*> switch
( long "ignore-existing"
<> help "Always regenerate textbases even if one already exists"
)
<*> many (option (fmap SpecificPackageDB . pure)
( short 'f' <> long "package-db"
<> help "Add an addition package database (e.g. a Cabal sandbox)"
))
-- | An unpacked Cabal project
newtype PackageTree = PkgTree FilePath
deriving (Show)
-- | Call a process
callProcessE :: FilePath -> [String] -> EitherT String IO ()
callProcessE exec args = fmapLT show $ tryIO $ callProcess exec args
-- | Call a process from the given directory
callProcessIn :: FilePath -> FilePath -> [String] -> EitherT String IO ()
callProcessIn dir exec args = do
let cp = (proc exec args) { cwd = Just dir }
(_, _, _, ph) <- liftIO $ createProcess cp
code <- liftIO $ waitForProcess ph
case code of
ExitSuccess -> return ()
ExitFailure e -> left $ "Process "++exec++" failed with error "++show e
-- | Unpack a package
unpack :: PackageId -> EitherT String IO PackageTree
unpack (PackageIdentifier (PackageName pkg) ver) = do
tmpDir <- liftIO getTemporaryDirectory
dir <- liftIO $ createTempDirectory tmpDir "hoogle-index.pkg"
callProcessIn dir "cabal" ["unpack", pkg++"=="++showVersion ver]
return $ PkgTree $ dir </> pkg++"-"++showVersion ver
-- | Remove an unpacked tree
removeTree :: PackageTree -> IO ()
removeTree (PkgTree dir) =
removeDirectoryRecursive $ takeDirectory dir
-- | A Haddock textbase
newtype TextBase = TextBase BS.ByteString
-- | Write a Haddock textbase to a file
writeTextBase :: TextBase -> FilePath -> IO ()
writeTextBase (TextBase content) path = BS.writeFile path content
-- | A file containing a Haddock textbase
type TextBaseFile = FilePath
-- | Find a pre-installed textbase corresponding to a package (if one exists)
findTextBase :: InstalledPackageInfo -> IO (Maybe TextBaseFile)
findTextBase ipkg = do
listToMaybe . catMaybes <$> mapM checkDir (haddockHTMLs ipkg)
where
PackageName name = pkgName $ sourcePackageId ipkg
checkDir root = do
let path = root </> name++".txt"
putStrLn $ "Looking for "++path
exists <- doesFileExist path
if exists
then return $ Just path
else return Nothing
-- | Build a textbase from source
buildTextBase :: PackageName -> PackageTree -> EitherT String IO TextBase
buildTextBase (PackageName pkg) (PkgTree dir) = do
callProcessIn dir "cabal" ["configure"]
callProcessIn dir "cabal" ["haddock", "--hoogle"]
let path = dir </> "dist" </> "doc" </> "html" </> pkg </> (pkg++".txt")
TextBase <$> liftIO (BS.readFile path)
getTextBase :: Config -> InstalledPackageInfo -> EitherT String IO TextBase
getTextBase cfg ipkg = do
existing <- if ignoreExistingTextBases cfg
then return Nothing
else liftIO $ findTextBase ipkg
case existing of
Just path -> TextBase <$> liftIO (BS.readFile path)
Nothing -> do
pkgTree <- unpack pkg
tb <- buildTextBase (pkgName pkg) pkgTree
liftIO $ removeTree pkgTree
-- install textbase if necessary
case listToMaybe $ haddockHTMLs ipkg of
Just docRoot | installTextBase cfg -> fmapLT show $ tryIO $ do
let TextBase content = tb
PackageName name = pkgName $ sourcePackageId ipkg
tbPath = docRoot </> name++".txt"
putStrLn $ "Installing textbase to "++tbPath
createDirectoryIfMissing True docRoot
BS.writeFile tbPath content
Nothing | installTextBase cfg -> do
liftIO $ putStrLn $ "Can't install textbase due to missing documentation directory"
_ -> return ()
return tb
where
pkg = sourcePackageId ipkg
-- | A Hoogle database
newtype Database = DB FilePath
deriving (Show)
-- | Delete a database file
removeDB :: Database -> IO ()
removeDB (DB path) = removeFile path
-- | Convert a textbase to a Hoogle database
convert :: TextBaseFile -> Maybe FilePath -> [Database] -> EitherT String IO Database
convert tbf docRoot merge = do
let docRoot' = maybe [] (\d->["--haddock", "--doc="++d]) docRoot
(tb,h) <- liftIO $ openTempFile "/tmp" "db.hoo"
liftIO $ hClose h
let args = ["convert", tbf, tb] ++ docRoot'
++ map (\(DB db)->"--merge="++db) merge
fmapLT show $ tryIO $ callProcess "hoogle" args
return $ DB tb
-- | Generate a Hoogle database for an installed package
indexPackage :: Config -> InstalledPackageInfo -> EitherT String IO Database
indexPackage cfg ipkg
| pkgName pkg `S.member` downloadPackages = do
tmpDir <- liftIO getTemporaryDirectory
callProcessE "hoogle" ["data", "--datadir="++tmpDir, name]
return $ DB $ tmpDir </> name++".hoo"
| pkgName pkg `S.member` ignorePackages =
left $ "Can't build documentation for "++name
| otherwise = do
tb <- getTextBase cfg ipkg
docRoot <- case haddockHTMLs ipkg of
docRoot:_ | useLocalDocs cfg -> return $ Just docRoot
[] | useLocalDocs cfg -> do
liftIO $ putStrLn $ "No local documentation for "++show pkg
return Nothing
_ -> return Nothing
(tbf,h) <- liftIO $ openTempFile "/tmp" "textbase.txt"
liftIO $ hClose h
liftIO $ writeTextBase tb tbf
db <- convert tbf docRoot []
liftIO $ removeFile tbf
return db
where
downloadPackages = S.fromList $ map PackageName
["base"]
ignorePackages = S.fromList $ map PackageName
["rts", "ghc-prim", "integer-gmp"]
pkg = sourcePackageId ipkg
PackageName name = pkgName pkg
-- | Combine Hoogle databases
combineDBs :: [Database] -> EitherT String IO Database
combineDBs dbs = fmapLT show $ tryIO $ do
tmpDir <- getTemporaryDirectory
(out, h) <- openTempFile tmpDir "combined.hoo"
hClose h
callProcess "hoogle" (["combine", "--outfile="++out] ++ map (\(DB db)->db) dbs)
return (DB out)
-- | Install a database in Hoogle's database directory
installDB :: Compiler -> PackageIndex -> Database -> EitherT String IO ()
installDB compiler pkgIdx (DB db) = do
hooglePkg <- case sortBy (comparing fst) $ lookupPackageName pkgIdx (PackageName "hoogle") of
(_, (pkg:_)):_ -> return pkg
_ -> left "Hoogle not installed"
template <- liftIO $ IDirs.defaultInstallDirs (compilerFlavor compiler) True False
let installDirs = IDirs.absoluteInstallDirs (sourcePackageId hooglePkg)
(compilerId compiler)
IDirs.NoCopyDest
buildPlatform template
let dbDir = IDirs.datadir installDirs </> "databases"
dest = dbDir </> "default.hoo"
liftIO $ copyFile db dest
liftIO $ putStrLn $ "Installed Hoogle index to "++dest
main :: IO ()
main = do
config <- execParser
$ info (helper <*> opts)
( fullDesc
<> progDesc "Generate Hoogle indexes for locally install packages"
<> header "hoogle-index - Painless local Hoogle indexing"
)
(compiler, _, progCfg) <- configure (verbosity config)
Nothing Nothing
defaultProgramConfiguration
let pkgDbs = [GlobalPackageDB, UserPackageDB] ++ otherPackageDbs config
pkgIdx <- getInstalledPackages (verbosity config) pkgDbs progCfg
let pkgs = reverseTopologicalOrder pkgIdx
maybeIndex :: InstalledPackageInfo -> IO (Either (PackageId, String) Database)
maybeIndex pkg = do
putStrLn ""
result <- runEitherT $ indexPackage config pkg
case result of
Left e -> do
let pkgId = sourcePackageId pkg
PackageName name = pkgName pkgId
putStrLn $ "Error while indexing "++name++": "++e
return $ Left (pkgId, e)
Right r -> return $ Right r
(failed, idxs) <- fmap partitionEithers $ mapM maybeIndex pkgs
when (not $ null failed) $ do
putStrLn "Failed to build the following indexes:"
let failedMsg (pkgId, reason) = " "++show (pkgName pkgId)++"\t"++reason
putStrLn $ unlines $ map failedMsg failed
combined <- runEitherT (combineDBs idxs)
case combined of
Left e -> putStrLn $ "Error while combining databases: "++e
Right db -> do
res <- runEitherT $ installDB compiler pkgIdx db
either print (const $ return ()) res
mapM_ removeDB idxs