{-# 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 Data.Either (partitionEithers) import Control.Error import qualified Data.ByteString.Char8 as BS import Distribution.Verbosity (Verbosity, normal) 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 { outputDir :: FilePath , verbosity :: Verbosity , installTextBase :: Bool , useLocalDocs :: Bool } config = Config { outputDir = "./hoogle-index" , verbosity = normal , installTextBase = True , useLocalDocs = True } -- | An unpacked Cabal project newtype PackageTree = PkgTree FilePath deriving (Show) -- | Call a package 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 -> IO PackageTree unpack (PackageIdentifier (PackageName pkg) ver) = do callProcess "cabal" ["unpack", pkg++"=="++showVersion ver] return $ PkgTree $ pkg++"-"++showVersion ver -- | Remove an unpacked tree removeTree :: PackageTree -> IO () removeTree (PkgTree dir) = removeDirectoryRecursive 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 "runghc" ["Setup", "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 <- liftIO $ findTextBase ipkg case existing of Just path -> TextBase <$> liftIO (BS.readFile path) Nothing -> do pkgTree <- fmapLT show $ tryIO $ unpack pkg tb <- buildTextBase (pkgName pkg) pkgTree liftIO $ removeTree pkgTree -- install textbase if necessary case listToMaybe $ haddockHTMLs ipkg of Just docRoot | installTextBase cfg -> do let TextBase content = tb PackageName name = pkgName $ sourcePackageId ipkg tbPath = docRoot name++".txt" liftIO $ putStrLn $ "Installing textbase to "++tbPath liftIO $ BS.writeFile tbPath content return tb where pkg = sourcePackageId ipkg -- | A Hoogle database newtype Database = DB FilePath deriving (Show) -- | 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 let args = ["convert", tbf] ++ docRoot' ++ map (\(DB db)->"--merge="++db) merge fmapLT show $ tryIO $ callProcess "hoogle" args return $ DB $ replaceExtension tbf ".hoo" -- | Generate a Hoogle database for an installed package indexPackage :: Config -> InstalledPackageInfo -> EitherT String IO Database indexPackage cfg ipkg = do let pkg = sourcePackageId ipkg tb <- getTextBase cfg ipkg docRoot <- case haddockHTMLs ipkg of docRoot:_ | useLocalDocs cfg -> Just docRoot [] | useLocalDocs cfg -> do putStrLn $ "No local documentation for "++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 -- | Combine Hoogle databases combineDBs :: [Database] -> IO Database combineDBs dbs = do let out = "all.hoo" 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 createDirectoryIfMissing True (outputDir config) (compiler, _, progCfg) <- configure (verbosity config) Nothing Nothing defaultProgramConfiguration pkgIdx <- getInstalledPackages (verbosity config) [GlobalPackageDB, UserPackageDB] progCfg let pkgs = reverseTopologicalOrder pkgIdx maybeIndex :: InstalledPackageInfo -> IO (Either (PackageId, String) Database) maybeIndex pkg = runEitherT $ fmapLT (sourcePackageId pkg,) $ indexPackage config pkg (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 <- combineDBs idxs res <- runEitherT $ installDB compiler pkgIdx combined either print (const $ return ()) res