module Distribution.Simple.Haddock (
haddock, hscolour
) where
import Distribution.Package
( PackageIdentifier, Package(..), packageName )
import qualified Distribution.ModuleName as ModuleName
import Distribution.PackageDescription as PD
(PackageDescription(..), BuildInfo(..),
Library(..), hasLibs, withLib,
Executable(..), withExe)
import Distribution.Simple.Compiler
( Compiler(..), compilerVersion )
import Distribution.Simple.Program
( ConfiguredProgram(..), requireProgramVersion
, rawSystemProgram, rawSystemProgramStdoutConf, rawSystemProgramStdout
, hscolourProgram, haddockProgram, ghcProgram )
import Distribution.Simple.PreProcess (ppCpp', ppUnlit,
PPSuffixHandler, runSimplePreProcessor)
import Distribution.Simple.Setup
import Distribution.Simple.Build (initialBuildSteps)
import Distribution.Simple.InstallDirs (InstallDirs(..), PathTemplate,
PathTemplateVariable(..),
toPathTemplate, fromPathTemplate,
substPathTemplate,
initialPathTemplateEnv)
import Distribution.Simple.LocalBuildInfo
( LocalBuildInfo(..), externalPackageDeps
, ComponentLocalBuildInfo(..), withLibLBI, withExeLBI )
import Distribution.Simple.BuildPaths ( haddockName,
hscolourPref, autogenModulesDir,
)
import Distribution.Simple.PackageIndex (dependencyClosure)
import qualified Distribution.Simple.PackageIndex as PackageIndex
import qualified Distribution.InstalledPackageInfo as InstalledPackageInfo
( InstalledPackageInfo_(..) )
import Distribution.InstalledPackageInfo
( InstalledPackageInfo )
import Distribution.Simple.Utils
( die, warn, notice, intercalate, setupMessage
, createDirectoryIfMissingVerbose, withTempFile, copyFileVerbose
, withTempDirectory
, findFileWithExtension, findFile )
import Distribution.Simple.GHC (ghcOptions)
import Distribution.Text
( display, simpleParse )
import Distribution.Verbosity
import Language.Haskell.Extension
import System.Directory(removeFile, doesFileExist, createDirectoryIfMissing)
import Control.Monad ( when, guard )
import Control.Exception (assert)
import Data.Monoid
import Data.Maybe ( fromMaybe, listToMaybe )
import Data.Char (isSpace)
import System.FilePath((</>), (<.>), splitFileName, splitExtension,
normalise, splitPath, joinPath)
import System.IO (hClose, hPutStrLn)
import Distribution.Version
data HaddockArgs = HaddockArgs {
argInterfaceFile :: Flag FilePath,
argPackageName :: Flag PackageIdentifier,
argHideModules :: (All,[ModuleName.ModuleName]),
argIgnoreExports :: Any,
argLinkSource :: Flag (Template,Template),
argCssFile :: Flag FilePath,
argVerbose :: Any,
argOutput :: Flag Output,
argInterfaces :: [(FilePath, Maybe FilePath)],
argOutputDir :: Directory,
argTitle :: Flag String,
argPrologue :: Flag String,
argGhcFlags :: [String],
argGhcLibDir :: Flag FilePath,
argTargets :: [FilePath]
}
newtype Directory = Dir { unDir' :: FilePath } deriving (Read,Show,Eq,Ord)
unDir :: Directory -> FilePath
unDir = joinPath . filter (\p -> p /="./" && p /= ".") . splitPath . unDir'
type Template = String
data Output = Html | Hoogle
haddock :: PackageDescription -> LocalBuildInfo -> [PPSuffixHandler] -> HaddockFlags -> IO ()
haddock pkg_descr _ _ haddockFlags
| not (hasLibs pkg_descr)
&& not (fromFlag $ haddockExecutables haddockFlags) =
warn (fromFlag $ haddockVerbosity haddockFlags) $
"No documentation was generated as this package does not contain "
++ "a library. Perhaps you want to use the --executables flag."
haddock pkg_descr lbi suffixes flags = do
setupMessage verbosity "Running Haddock for" (packageId pkg_descr)
(confHaddock, version, _) <-
requireProgramVersion verbosity haddockProgram
(orLaterVersion (Version [0,6] [])) (withPrograms lbi)
let isVersion2 = version >= Version [2,0] []
when ( flag haddockHoogle
&& version > Version [2] []
&& version < Version [2,2] []) $
die "haddock 2.0 and 2.1 do not support the --hoogle flag."
when (flag haddockHscolour && version < Version [0,8] []) $
die "haddock --hyperlink-source requires Haddock version 0.8 or later"
when isVersion2 $ do
haddockGhcVersionStr <- rawSystemProgramStdout verbosity confHaddock
["--ghc-version"]
case simpleParse haddockGhcVersionStr of
Nothing -> die "Could not get GHC version from Haddock"
Just haddockGhcVersion
| haddockGhcVersion == ghcVersion -> return ()
| otherwise -> die $
"Haddock's internal GHC version must match the configured "
++ "GHC version.\n"
++ "The GHC version is " ++ display ghcVersion ++ " but "
++ "haddock is using GHC version " ++ display haddockGhcVersion
where ghcVersion = compilerVersion (compiler lbi)
initialBuildSteps (flag haddockDistPref) pkg_descr lbi verbosity suffixes
when (flag haddockHscolour) $ hscolour' pkg_descr lbi $
defaultHscolourFlags `mappend` haddockToHscolour flags
args <- fmap mconcat . sequence $
[ getInterfaces verbosity lbi (flagToMaybe (haddockHtmlLocation flags))
, getGhcLibDir verbosity lbi isVersion2 ]
++ map return
[ fromFlags flags
, fromPackageDescription pkg_descr ]
withLibLBI pkg_descr lbi $ \lib clbi -> do
let bi = libBuildInfo lib
libArgs <- fromLibrary lbi lib clbi
prepareSources verbosity lbi isVersion2 bi (args `mappend` libArgs) $
runHaddock verbosity confHaddock
when (flag haddockExecutables) $
withExeLBI pkg_descr lbi $ \exe clbi -> do
let bi = buildInfo exe
exeArgs <- fromExecutable lbi exe clbi
prepareSources verbosity lbi isVersion2 bi (args `mappend` exeArgs) $
runHaddock verbosity confHaddock
where
verbosity = flag haddockVerbosity
flag f = fromFlag $ f flags
prepareSources :: Verbosity
-> LocalBuildInfo
-> Bool
-> BuildInfo
-> HaddockArgs
-> (HaddockArgs -> IO a)
-> IO a
prepareSources verbosity lbi isVersion2 bi args@HaddockArgs{argTargets=files} k =
withTempDirectory verbosity (buildDir lbi) "tmp" $ \tmp ->
mapM (mockPP tmp) files >>= \targets -> k args {argTargets=targets}
where
mockPP pref file = do
let (filePref, fileName) = splitFileName file
targetDir = pref </> filePref
targetFile = targetDir </> fileName
(targetFileNoext, targetFileExt) = splitExtension $ targetFile
hsFile = targetFileNoext <.> "hs"
assert (targetFileExt `elem` [".lhs",".hs"]) $ return ()
createDirectoryIfMissing True targetDir
if needsCpp
then do
runSimplePreProcessor (ppCpp' defines bi lbi)
file targetFile verbosity
else
copyFileVerbose verbosity file targetFile
when (targetFileExt == ".lhs") $ do
runSimplePreProcessor ppUnlit targetFile hsFile verbosity
removeFile targetFile
return hsFile
needsCpp = CPP `elem` extensions bi
defines | isVersion2 = []
| otherwise = ["-D__HADDOCK__"]
fromFlags :: HaddockFlags -> HaddockArgs
fromFlags flags =
mempty {
argHideModules = (maybe mempty (All . not) $ flagToMaybe (haddockInternal flags), mempty),
argLinkSource = if fromFlag (haddockHscolour flags)
then Flag ("src/%{MODULE/./-}.html"
,"src/%{MODULE/./-}.html#%{NAME}")
else NoFlag,
argCssFile = haddockCss flags,
argVerbose = maybe mempty (Any . (>= deafening)) . flagToMaybe $ haddockVerbosity flags,
argOutput = fmap (\b -> if b then Hoogle else Html) $ haddockHoogle flags,
argOutputDir = maybe mempty Dir . flagToMaybe $ haddockDistPref flags
}
fromPackageDescription :: PackageDescription -> HaddockArgs
fromPackageDescription pkg_descr =
mempty {
argInterfaceFile = Flag $ haddockName pkg_descr,
argPackageName = Flag $ packageId $ pkg_descr,
argOutputDir = Dir $ "doc" </> "html" </> display (packageName pkg_descr),
argPrologue = Flag $ if null desc then synopsis pkg_descr else desc,
argTitle = Flag $ showPkg ++ subtitle
}
where
desc = PD.description pkg_descr
showPkg = display (packageId pkg_descr)
subtitle | null (synopsis pkg_descr) = ""
| otherwise = ": " ++ synopsis pkg_descr
fromLibrary :: LocalBuildInfo -> Library -> ComponentLocalBuildInfo -> IO HaddockArgs
fromLibrary lbi lib clbi =
do inFiles <- map snd `fmap` getLibSourceFiles lbi lib
return $ mempty {
argHideModules = (mempty,otherModules $ bi),
argGhcFlags = ghcOptions lbi bi clbi (buildDir lbi),
argTargets = inFiles
}
where
bi = libBuildInfo lib
fromExecutable :: LocalBuildInfo -> Executable -> ComponentLocalBuildInfo -> IO HaddockArgs
fromExecutable lbi exe clbi =
do inFiles <- map snd `fmap` getExeSourceFiles lbi exe
return $ mempty {
argGhcFlags = ghcOptions lbi bi clbi (buildDir lbi),
argOutputDir = Dir (exeName exe),
argTitle = Flag (exeName exe),
argTargets = inFiles
}
where
bi = buildInfo exe
getInterfaces :: Verbosity
-> LocalBuildInfo
-> Maybe String
-> IO HaddockArgs
getInterfaces verbosity lbi location = do
let htmlTemplate = fmap toPathTemplate $ location
(packageFlags, warnings) <- haddockPackageFlags lbi htmlTemplate
maybe (return ()) (warn verbosity) warnings
return $ mempty {
argInterfaces = packageFlags
}
getGhcLibDir :: Verbosity -> LocalBuildInfo
-> Bool
-> IO HaddockArgs
getGhcLibDir verbosity lbi isVersion2
| isVersion2 =
do l <- ghcLibDir verbosity lbi
return $ mempty { argGhcLibDir = Flag l }
| otherwise =
return mempty
runHaddock :: Verbosity -> ConfiguredProgram -> HaddockArgs -> IO ()
runHaddock verbosity confHaddock args = do
let haddockVersion = fromMaybe (error "unable to determine haddock version")
(programVersion confHaddock)
renderArgs verbosity haddockVersion args $ \(flags,result)-> do
rawSystemProgram verbosity confHaddock flags
notice verbosity $ "Documentation created: " ++ result
renderArgs :: Verbosity
-> Version
-> HaddockArgs
-> (([[Char]], FilePath) -> IO a)
-> IO a
renderArgs verbosity version args k = do
createDirectoryIfMissingVerbose verbosity True outputDir
withTempFile outputDir "haddock-prolog.txt" $ \prologFileName h -> do
do
hPutStrLn h $ fromFlag $ argPrologue args
hClose h
let pflag = (:[]).("--prologue="++) $ prologFileName
k $ (pflag ++ renderPureArgs version args, result)
where
isVersion2 = version >= Version [2,0] []
outputDir = (unDir $ argOutputDir args)
result = outputDir </>
case arg argOutput of
Html -> "index.html"
Hoogle -> (if isVersion2 then display $ packageName pkgid else display pkgid) <.> "txt"
where pkgid = arg argPackageName
arg f = fromFlag $ f args
renderPureArgs :: Version -> HaddockArgs -> [[Char]]
renderPureArgs version args = concat
[
(:[]) . (\f -> "--dump-interface="++ unDir (argOutputDir args) </> f)
. fromFlag . argInterfaceFile $ args,
(\pkgName -> if isVersion2
then ["--optghc=-package-name", "--optghc=" ++ pkgName]
else ["--package=" ++ pkgName]) . display . fromFlag . argPackageName $ args,
(\(All b,xs) -> bool (map (("--hide=" ++). display) xs) [] b) . argHideModules $ args,
bool ["--ignore-all-exports"] [] . getAny . argIgnoreExports $ args,
maybe [] (\(m,e) -> ["--source-module=" ++ m
,"--source-entity=" ++ e]) . flagToMaybe . argLinkSource $ args,
maybe [] ((:[]).("--css="++)) . flagToMaybe . argCssFile $ args,
bool [] [verbosityFlag] . getAny . argVerbose $ args,
(\o -> case o of Hoogle -> ["--hoogle"]; Html -> ["--html"]) . fromFlag . argOutput $ args,
renderInterfaces . argInterfaces $ args,
(:[]).("--odir="++) . unDir . argOutputDir $ args,
(:[]).("--title="++) . (bool (++" (internal documentation)") id (getAny $ argIgnoreExports args))
. fromFlag . argTitle $ args,
bool id (const []) isVersion2 . map ("--optghc=" ++) . argGhcFlags $ args,
maybe [] (\l -> ["-B"++l]) $ guard isVersion2 >> flagToMaybe (argGhcLibDir args),
argTargets $ args
]
where
renderInterfaces = map (\(i,mh) -> "--read-interface=" ++ maybe "" (++",") mh ++ i)
bool a b c = if c then a else b
isVersion2 = version >= Version [2,0] []
isVersion2_5 = version >= Version [2,5] []
verbosityFlag
| isVersion2_5 = "--verbosity=1"
| otherwise = "--verbose"
haddockPackageFlags :: LocalBuildInfo
-> Maybe PathTemplate
-> IO ([(FilePath,Maybe FilePath)], Maybe String)
haddockPackageFlags lbi htmlTemplate = do
let allPkgs = installedPkgs lbi
directDeps = map fst (externalPackageDeps lbi)
transitiveDeps <- case dependencyClosure allPkgs directDeps of
Left x -> return x
Right _ -> die "Can't find transitive deps for haddock"
interfaces <- sequence
[ case interfaceAndHtmlPath ipkg of
Nothing -> return (Left (packageId ipkg))
Just (interface, html) -> do
exists <- doesFileExist interface
if exists
then return (Right (interface, html))
else return (Left (packageId ipkg))
| ipkg <- PackageIndex.allPackages transitiveDeps ]
let missing = [ pkgid | Left pkgid <- interfaces ]
warning = "The documentation for the following packages are not "
++ "installed. No links will be generated to these packages: "
++ intercalate ", " (map display missing)
flags = [ (interface, if null html then Nothing else Just html)
| Right (interface, html) <- interfaces ]
return (flags, if null missing then Nothing else Just warning)
where
interfaceAndHtmlPath :: InstalledPackageInfo -> Maybe (FilePath, FilePath)
interfaceAndHtmlPath pkg = do
interface <- listToMaybe (InstalledPackageInfo.haddockInterfaces pkg)
html <- case htmlTemplate of
Nothing -> listToMaybe (InstalledPackageInfo.haddockHTMLs pkg)
Just htmlPathTemplate -> Just (expandTemplateVars htmlPathTemplate)
return (interface, html)
where expandTemplateVars = fromPathTemplate . substPathTemplate env
env = (PrefixVar, prefix (installDirTemplates lbi))
: initialPathTemplateEnv (packageId pkg) (compilerId (compiler lbi))
hscolour :: PackageDescription -> LocalBuildInfo -> [PPSuffixHandler] -> HscolourFlags -> IO ()
hscolour pkg_descr lbi suffixes flags = do
initialBuildSteps distPref pkg_descr lbi verbosity suffixes
hscolour' pkg_descr lbi flags
where
verbosity = fromFlag (hscolourVerbosity flags)
distPref = fromFlag $ hscolourDistPref flags
hscolour' :: PackageDescription
-> LocalBuildInfo
-> HscolourFlags
-> IO ()
hscolour' pkg_descr lbi flags = do
let distPref = fromFlag $ hscolourDistPref flags
(hscolourProg, _, _) <-
requireProgramVersion
verbosity hscolourProgram
(orLaterVersion (Version [1,8] [])) (withPrograms lbi)
setupMessage verbosity "Running hscolour for" (packageId pkg_descr)
createDirectoryIfMissingVerbose verbosity True $ hscolourPref distPref pkg_descr
withLib pkg_descr $ \lib -> do
let outputDir = hscolourPref distPref pkg_descr </> "src"
runHsColour hscolourProg outputDir =<< getLibSourceFiles lbi lib
when (fromFlag (hscolourExecutables flags)) $
withExe pkg_descr $ \exe -> do
let outputDir = hscolourPref distPref pkg_descr </> exeName exe </> "src"
runHsColour hscolourProg outputDir =<< getExeSourceFiles lbi exe
where
stylesheet = flagToMaybe (hscolourCSS flags)
verbosity = fromFlag (hscolourVerbosity flags)
runHsColour prog outputDir moduleFiles = do
createDirectoryIfMissingVerbose verbosity True outputDir
case stylesheet of
Nothing | programVersion prog >= Just (Version [1,9] []) ->
rawSystemProgram verbosity prog
["-print-css", "-o" ++ outputDir </> "hscolour.css"]
| otherwise -> return ()
Just s -> copyFileVerbose verbosity s (outputDir </> "hscolour.css")
flip mapM_ moduleFiles $ \(m, inFile) ->
rawSystemProgram verbosity prog
["-css", "-anchor", "-o" ++ outFile m, inFile]
where
outFile m = outputDir </> intercalate "-" (ModuleName.components m) <.> "html"
haddockToHscolour :: HaddockFlags -> HscolourFlags
haddockToHscolour flags =
HscolourFlags {
hscolourCSS = haddockHscolourCss flags,
hscolourExecutables = haddockExecutables flags,
hscolourVerbosity = haddockVerbosity flags,
hscolourDistPref = haddockDistPref flags
}
getLibSourceFiles :: LocalBuildInfo
-> Library
-> IO [(ModuleName.ModuleName, FilePath)]
getLibSourceFiles lbi lib = getSourceFiles searchpaths modules
where
bi = libBuildInfo lib
modules = PD.exposedModules lib ++ otherModules bi
searchpaths = autogenModulesDir lbi : buildDir lbi : hsSourceDirs bi
getExeSourceFiles :: LocalBuildInfo
-> Executable
-> IO [(ModuleName.ModuleName, FilePath)]
getExeSourceFiles lbi exe = do
moduleFiles <- getSourceFiles searchpaths modules
srcMainPath <- findFile (hsSourceDirs bi) (modulePath exe)
return ((ModuleName.main, srcMainPath) : moduleFiles)
where
bi = buildInfo exe
modules = otherModules bi
searchpaths = autogenModulesDir lbi : exeBuildDir lbi exe : hsSourceDirs bi
getSourceFiles :: [FilePath]
-> [ModuleName.ModuleName]
-> IO [(ModuleName.ModuleName, FilePath)]
getSourceFiles dirs modules = flip mapM modules $ \m -> fmap ((,) m) $
findFileWithExtension ["hs", "lhs"] dirs (ModuleName.toFilePath m)
>>= maybe (notFound m) (return . normalise)
where
notFound module_ = die $ "can't find source for module " ++ display module_
exeBuildDir :: LocalBuildInfo -> Executable -> FilePath
exeBuildDir lbi exe = buildDir lbi </> exeName exe </> exeName exe ++ "-tmp"
ghcLibDir :: Verbosity -> LocalBuildInfo -> IO FilePath
ghcLibDir verbosity lbi =
(reverse . dropWhile isSpace . reverse) `fmap`
rawSystemProgramStdoutConf verbosity ghcProgram (withPrograms lbi) ["--print-libdir"]
instance Monoid HaddockArgs where
mempty = HaddockArgs {
argInterfaceFile = mempty,
argPackageName = mempty,
argHideModules = mempty,
argIgnoreExports = mempty,
argLinkSource = mempty,
argCssFile = mempty,
argVerbose = mempty,
argOutput = mempty,
argInterfaces = mempty,
argOutputDir = mempty,
argTitle = mempty,
argPrologue = mempty,
argGhcFlags = mempty,
argGhcLibDir = mempty,
argTargets = mempty
}
mappend a b = HaddockArgs {
argInterfaceFile = mult argInterfaceFile,
argPackageName = mult argPackageName,
argHideModules = mult argHideModules,
argIgnoreExports = mult argIgnoreExports,
argLinkSource = mult argLinkSource,
argCssFile = mult argCssFile,
argVerbose = mult argVerbose,
argOutput = mult argOutput,
argInterfaces = mult argInterfaces,
argOutputDir = mult argOutputDir,
argTitle = mult argTitle,
argPrologue = mult argPrologue,
argGhcFlags = mult argGhcFlags,
argGhcLibDir = mult argGhcLibDir,
argTargets = mult argTargets
}
where mult f = f a `mappend` f b
instance Monoid Directory where
mempty = Dir "."
mappend (Dir m) (Dir n) = Dir $ m </> n