cabal-hoogle-0.1.0.0: src/Hoogle/Cabal.hs
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
module Hoogle.Cabal (main) where
import Control.Exception (catch, throw)
import Control.Monad (unless)
import Data.Bifunctor (Bifunctor (second))
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 Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Traversable (forM)
import Distribution.InstalledPackageInfo (InstalledPackageInfo (haddockHTMLs, installedUnitId))
import Distribution.Simple.Configure (getPersistBuildConfig)
import Distribution.Simple.PackageIndex (allPackagesByName)
import Distribution.Types.LocalBuildInfo (LocalBuildInfo, installedPkgs, localPkgDescr)
import Distribution.Types.PackageDescription (PackageDescription (package))
import Distribution.Types.PackageId (pkgName)
import Distribution.Types.PackageName (PackageName, unPackageName)
import Hoogle.Cabal.CmdOptions
import System.Directory (createDirectoryIfMissing, createDirectoryLink, listDirectory, makeAbsolute, removeDirectoryLink, removeDirectoryRecursive, withCurrentDirectory)
import System.Exit (exitFailure)
import System.FilePath
import System.IO (stderr)
import System.IO.Error (isDoesNotExistError)
import System.Process.Typed
import Text.Regex.TDFA ((=~))
import Prelude hiding (log)
main :: IO ()
main = do
CmdOptions {..} <- readCmdOptions
localPackagesDir <- findLocalPackagesBuildDir cmdOptions_compiler cmdOptions_platform cmdOptions_builddir
let hoogleDir = localPackagesDir </> ".hoogle"
hoogleLocalPackagesDir = hoogleDir </> "local"
hoogleDependenciesDir = hoogleDir </> "dependencies"
case cmdOptions_command of
CommandGenerate -> do
catch (removeDirectoryRecursive hoogleDir) $ \(err :: IOError) ->
if isDoesNotExistError err then pure () else throw err
createDirectoryIfMissing True hoogleLocalPackagesDir
createDirectoryIfMissing True hoogleDependenciesDir
localPackages <- symlinkLocalPackages localPackagesDir hoogleLocalPackagesDir
let localPkgsName = fmap (pkgName . package . localPkgDescr) localPackages
dependenciesName <- symlinkDependencies localPackages hoogleDependenciesDir
let nameStrs = fmap unPackageName (localPkgsName <> dependenciesName)
withCurrentDirectory hoogleDir $ do
runProcess_ . proc "hoogle" $
["generate", databaseArg, "--local=local", "--local=dependencies"] ++ nameStrs
CommandRun hoogleArgs -> do
let hoogleArgs' = case hoogleArgs of
(x : xs) -> x : databaseArg : xs
[] -> [databaseArg]
withCurrentDirectory hoogleDir . runProcess_ . proc "hoogle" $
hoogleArgs'
where
databaseArg = "--database=all.hoo"
findLocalPackagesBuildDir ::
-- | compiler
Maybe Text ->
-- | platform
Maybe Text ->
-- | cabal project build dir (often dist-newstyle)
FilePath ->
IO FilePath
findLocalPackagesBuildDir compiler platform projectBuildDir = do
-- directory layout: dist-newstyle/build/x86_64-linux/ghc-9.2.3/$PACKAGE_NAME
projectBuildDir' <- makeAbsolute projectBuildDir
platformDir <- enterSubDir (projectBuildDir' </> "build") "platform" (fmap T.unpack platform)
enterSubDir platformDir "compiler" (fmap T.unpack compiler)
exitOnError :: Text -> IO a
exitOnError log = do
T.hPutStrLn stderr log
exitFailure
enterSubDir :: FilePath -> Text -> Maybe FilePath -> IO FilePath
enterSubDir baseDir realm givenSubDir = do
subDirs <- listDirectory baseDir
case givenSubDir >>= (\d -> if d `notElem` subDirs then Just d else Nothing) of
Just wrongSubDir -> exitOnError [i|specified #{realm} #{wrongSubDir} doesn't not exist, make sure to build first|]
Nothing -> case selectSubDir subDirs givenSubDir of
Just subDir -> pure $ baseDir </> subDir
_ -> exitOnError [i|failed to guess #{realm}, please specify one|]
where
selectSubDir :: [FilePath] -> Maybe FilePath -> Maybe FilePath
selectSubDir [exactlyOne] _ = Just exactlyOne
selectSubDir _ given = given
symlinkLocalPackages :: FilePath -> FilePath -> IO [LocalBuildInfo]
symlinkLocalPackages localPackagesDir destDir = do
localPackages <- filter (=~ packageNameRegex) <$> listDirectory localPackagesDir
forM localPackages $ \pkg -> do
catch (removeDirectoryLink (destDir </> pkg)) $ \(e :: IOError) ->
if isDoesNotExistError e then pure () else throw e
createDirectoryLink (localPackagesDir </> pkg) (destDir </> pkg)
getPersistBuildConfig (localPackagesDir </> pkg)
where
packageNameRegex :: String =
"[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*-[0-9]+([.][0-9]+)*"
symlinkDependencies :: [LocalBuildInfo] -> FilePath -> IO [PackageName]
symlinkDependencies 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) $
T.putStrLn [i|Warning: package #{name} has more than 1 version installed, this should not happen. all pkgs: #{fmap installedUnitId allPkgs}|]
case haddockHTMLs pkg of
[htmlDir] -> pure $ Just (name, htmlDir)
htmlDirs -> do
T.putStrLn [i|Warning: package #{name} doesn't have exactly one haddock html directory, actual: #{htmlDirs}|]
pure Nothing
forM pkgs $ \(name, dir) -> do
createDirectoryLink dir (hoogleDependenciesDir </> unPackageName name)
pure name
where
collectDependenciesForPkg pkg =
let depsWithName = allPackagesByName (installedPkgs pkg)
in fmap (second (NonEmpty.:| []))
. concatMap (\(name, pkgs) -> fmap (name,) pkgs)
$ depsWithName