cabal-hoogle-3.10.0.0: src/Hoogle/Cabal/Command/Generate.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
module Hoogle.Cabal.Command.Generate
( Log,
command,
Command,
action,
)
where
import Control.Exception (catch, throw)
import Control.Monad (unless)
import Control.Monad.IO.Class
import Control.Monad.Trans.Maybe
import Data.Bifunctor (Bifunctor (second))
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.List.NonEmpty.Extra as NonEmpty
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes)
import Data.String.Interpolate (i)
import qualified Data.Text as T
import Data.Traversable (forM)
import Distribution.Client.CmdBuild
( buildAction,
)
import Distribution.Client.DistDirLayout (DistDirLayout (distBuildDirectory))
import qualified Distribution.Client.InstallPlan as InstallPlan
import Distribution.Client.ProjectOrchestration
( ProjectBaseContext (distDirLayout),
ProjectBuildContext (elaboratedPlanToExecute, elaboratedShared, targetsMap),
)
import Distribution.Client.ProjectPlanning (ElaboratedConfiguredPackage)
import Distribution.Client.ProjectPlanning.Types (elabDistDirParams)
import Distribution.InstalledPackageInfo (InstalledPackageInfo (haddockHTMLs, installedUnitId, pkgRoot))
import Distribution.Simple (UnitId)
import Distribution.Simple.Configure (ConfigStateFileError, tryGetPersistBuildConfig)
import Distribution.Simple.PackageIndex (allPackagesByName)
import Distribution.Types.LocalBuildInfo (LocalBuildInfo)
import qualified Distribution.Types.LocalBuildInfo as LocalBuildInfo
import qualified Distribution.Types.PackageDescription as PackageDescription
import qualified Distribution.Types.PackageId as PackageId
import Distribution.Types.PackageName (PackageName)
import qualified Distribution.Types.PackageName as PackageName
import qualified Hoogle
import Hoogle.Cabal.Command.Common (Context (..), GlobalOptions (..), hoogleDatabaseArg, readContext)
import Hoogle.Cabal.Logger
import qualified Options.Applicative as OptParse
import System.Directory
( createDirectoryIfMissing,
createDirectoryLink,
removeDirectoryLink,
removeDirectoryRecursive,
withCurrentDirectory,
)
import System.FilePath
import System.IO.Error (isDoesNotExistError)
import Prelude hiding (log)
data Log
= LogBadInstallPlan UnitId (Maybe (InstallPlan.GenericPlanPackage InstalledPackageInfo ElaboratedConfiguredPackage))
| LogCanNotReadSetupConfig String ConfigStateFileError
| LogPkgMoreThan1Version PackageName (NonEmpty UnitId)
| LogPkgBadHaddockHtml PackageName [FilePath]
instance Show Log where
show (LogBadInstallPlan unitId Nothing) = "can not find " <> show unitId <> " from install plan"
show (LogBadInstallPlan unitId (Just (InstallPlan.PreExisting _))) = show unitId <> " is PreExisting"
show (LogBadInstallPlan unitId (Just (InstallPlan.Configured _))) = show unitId <> " is Configured"
show (LogBadInstallPlan unitId (Just (InstallPlan.Installed _))) = show unitId <> " is Installed"
show (LogCanNotReadSetupConfig pkg configStateFileErr) =
[i|Can not read setup-config file from #{pkg}, error: #{configStateFileErr}|]
show (LogPkgMoreThan1Version pkgName unitIds) =
[i|Warning: package #{pkgName} has more than 1 version installed, this should not happen. all pkgs: #{unitIds}|]
show (LogPkgBadHaddockHtml name htmlDirs) =
[i|Warning: package #{name} doesn't have exactly one haddock html directory, actual: #{htmlDirs}|]
newtype Command = Command
{ _targets :: [String]
}
deriving (Show, Eq)
command :: (Command -> a) -> OptParse.Mod OptParse.CommandFields a
command f =
OptParse.command "generate" $
OptParse.info
(fmap f commandParser)
(OptParse.progDesc "Generate hoogle database")
commandParser :: OptParse.Parser Command
commandParser =
Command
<$> (OptParse.many . OptParse.strArgument) (OptParse.metavar "TARGETS")
action :: Logger Log -> GlobalOptions -> Command -> IO ()
action logger globalOptions (Command targets) = do
(Context baseCtx buildCtx hoogleDir targetStrings flags globalFlags) <- readContext globalOptions targets
buildAction flags targetStrings globalFlags
let targetIds = Map.keys (targetsMap buildCtx)
installPlan = elaboratedPlanToExecute buildCtx
hoogleLocalPackagesDir = hoogleDir </> "local"
hoogleDependenciesDir = hoogleDir </> "dependencies"
localPackages <- fmap catMaybes . forM targetIds $ \targetId ->
let handlePkg pkg = pure . Just $ distBuildDirectory (distDirLayout baseCtx) $ elabDistDirParams (elaboratedShared buildCtx) pkg
in case InstallPlan.lookup installPlan targetId of
Just (InstallPlan.Installed pkg) -> handlePkg pkg
Just (InstallPlan.Configured pkg) -> handlePkg pkg
pkg -> do
logWith logger Error (LogBadInstallPlan targetId pkg)
pure Nothing
catch (removeDirectoryRecursive hoogleDir) $ \(err :: IOError) ->
if isDoesNotExistError err then pure () else throw err
createDirectoryIfMissing True hoogleLocalPackagesDir
createDirectoryIfMissing True hoogleDependenciesDir
localPackagesBuildInfo <- symlinkLocalPackages logger localPackages hoogleLocalPackagesDir
let localPkgsName = fmap fst localPackagesBuildInfo
dependenciesName <- symlinkDependencies logger (fmap snd localPackagesBuildInfo) hoogleDependenciesDir
let nameStrs = localPkgsName <> fmap PackageName.unPackageName dependenciesName
withCurrentDirectory hoogleDir $
Hoogle.hoogle $
["generate", hoogleDatabaseArg, "--local=local", "--local=dependencies"] ++ nameStrs
symlinkLocalPackages :: Logger Log -> [FilePath] -> FilePath -> IO [(String, LocalBuildInfo)]
symlinkLocalPackages logger pkgsPath destDir = do
fmap catMaybes . forM pkgsPath $ \pkgPath -> runMaybeT $ do
lbiEither <- liftIO $ tryGetPersistBuildConfig pkgPath
lbi <- MaybeT $ case lbiEither of
Left configStateFileErr -> do
logWith logger Error $ LogCanNotReadSetupConfig pkgPath configStateFileErr
pure Nothing
Right lbi -> pure $ Just lbi
let pkgName =
PackageName.unPackageName
. PackageId.pkgName
. PackageDescription.package
. LocalBuildInfo.localPkgDescr
$ lbi
liftIO $ catch (removeDirectoryLink (destDir </> pkgName)) $ \(e :: IOError) ->
if isDoesNotExistError e then pure () else throw e
liftIO $ createDirectoryLink pkgPath (destDir </> pkgName)
pure (pkgName, lbi)
symlinkDependencies :: Logger Log -> [LocalBuildInfo] -> FilePath -> IO [PackageName]
symlinkDependencies logger localPackages hoogleDependenciesDir = do
let nameToPkgs =
fmap (NonEmpty.nubOrdOn installedUnitId) . Map.fromListWith (<>) $
concatMap collectDependenciesForPkg localPackages
pkgs <- fmap catMaybes . forM (Map.toList nameToPkgs) $ \(name, allPkgs@(pkg NonEmpty.:| pkgs)) -> do
unless (null pkgs) $
logWith logger Warning $
LogPkgMoreThan1Version name (fmap installedUnitId allPkgs)
case haddockHTMLs' pkg of
[htmlDir] -> pure $ Just (name, htmlDir)
htmlDirs -> do
logWith logger Warning $ LogPkgBadHaddockHtml name htmlDirs
pure Nothing
forM pkgs $ \(name, dir) -> do
createDirectoryLink dir (hoogleDependenciesDir </> PackageName.unPackageName name)
pure name
where
collectDependenciesForPkg :: LocalBuildInfo -> [(PackageName, NonEmpty InstalledPackageInfo)]
collectDependenciesForPkg pkg =
let depsWithName = allPackagesByName (LocalBuildInfo.installedPkgs pkg)
in fmap (second (NonEmpty.:| []))
. concatMap (\(name, pkgs) -> fmap (name,) pkgs)
$ depsWithName
haddockHTMLs' :: InstalledPackageInfo -> [FilePath]
haddockHTMLs' pkg =
fmap
( case pkgRoot pkg of
Nothing -> id
Just pkgRoot' -> T.unpack . T.replace "${pkgroot}" (T.pack pkgRoot') . T.pack
)
. haddockHTMLs
$ pkg