stack-2.15.1: src/Stack/Build/Haddock.hs
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
-- | Generate haddocks
module Stack.Build.Haddock
( generateDepsHaddockIndex
, generateLocalHaddockIndex
, generateSnapHaddockIndex
, openHaddocksInBrowser
, shouldHaddockDeps
, shouldHaddockPackage
, generateLocalHaddockForHackageArchives
) where
import qualified Codec.Archive.Tar as Tar
import qualified Codec.Compression.GZip as GZip
import qualified Data.Foldable as F
import qualified Data.HashSet as HS
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Text as T
import Data.Time ( UTCTime )
import Distribution.Text ( display )
import Path
( (</>), addExtension, fromAbsDir, fromAbsFile, fromRelDir
, parent, parseRelDir, parseRelFile
)
import Path.Extra
( parseCollapsedAbsFile, toFilePathNoTrailingSep
, tryGetModificationTime
)
import Path.IO
( copyDirRecur', doesFileExist, ensureDir, ignoringAbsence
, removeDirRecur
)
import qualified RIO.ByteString.Lazy as BL
import RIO.List ( intercalate )
import RIO.Process ( HasProcessContext, withWorkingDir )
import Stack.Constants
( docDirSuffix, htmlDirSuffix, relDirAll, relFileIndexHtml )
import Stack.Constants.Config ( distDirFromDir )
import Stack.Prelude hiding ( Display (..) )
import Stack.Types.Build.Exception ( BuildException (..) )
import Stack.Types.CompilerPaths
( CompilerPaths (..), HasCompiler (..) )
import Stack.Types.ConfigureOpts ( BaseConfigOpts (..) )
import Stack.Types.BuildOpts ( BuildOpts (..), HaddockOpts (..) )
import Stack.Types.BuildOptsCLI ( BuildOptsCLI (..) )
import Stack.Types.DumpPackage ( DumpPackage (..) )
import Stack.Types.EnvConfig ( HasEnvConfig (..) )
import Stack.Types.GhcPkgId ( GhcPkgId )
import Stack.Types.Package
( InstallLocation (..), LocalPackage (..), Package (..) )
import qualified System.FilePath as FP
import Web.Browser ( openBrowser )
openHaddocksInBrowser ::
HasTerm env
=> BaseConfigOpts
-> Map PackageName (PackageIdentifier, InstallLocation)
-- ^ Available packages and their locations for the current project
-> Set PackageName
-- ^ Build targets as determined by 'Stack.Build.Source.loadSourceMap'
-> RIO env ()
openHaddocksInBrowser bco pkgLocations buildTargets = do
let cliTargets = bco.buildOptsCLI.targetsCLI
getDocIndex = do
let localDocs = haddockIndexFile (localDepsDocDir bco)
localExists <- doesFileExist localDocs
if localExists
then pure localDocs
else do
let snapDocs = haddockIndexFile (snapDocDir bco)
snapExists <- doesFileExist snapDocs
if snapExists
then pure snapDocs
else throwIO HaddockIndexNotFound
docFile <-
case (cliTargets, map (`Map.lookup` pkgLocations) (Set.toList buildTargets)) of
([_], [Just (pkgId, iloc)]) -> do
pkgRelDir <- (parseRelDir . packageIdentifierString) pkgId
let docLocation =
case iloc of
Snap -> snapDocDir bco
Local -> localDocDir bco
let docFile = haddockIndexFile (docLocation </> pkgRelDir)
exists <- doesFileExist docFile
if exists
then pure docFile
else do
prettyWarnL
[ flow "Expected to find documentation at"
, pretty docFile <> ","
, flow "but that file is missing. Opening doc index instead."
]
getDocIndex
_ -> getDocIndex
prettyInfo $ "Opening" <+> pretty docFile <+> "in the browser."
_ <- liftIO $ openBrowser (toFilePath docFile)
pure ()
-- | Determine whether we should haddock for a package.
shouldHaddockPackage ::
BuildOpts
-> Set PackageName
-- ^ Packages that we want to generate haddocks for in any case (whether or
-- not we are going to generate haddocks for dependencies)
-> PackageName
-> Bool
shouldHaddockPackage bopts wanted name =
if Set.member name wanted
then bopts.buildHaddocks
else shouldHaddockDeps bopts
-- | Determine whether to build haddocks for dependencies.
shouldHaddockDeps :: BuildOpts -> Bool
shouldHaddockDeps bopts = fromMaybe bopts.buildHaddocks bopts.haddockDeps
-- | Generate Haddock index and contents for local packages.
generateLocalHaddockIndex ::
(HasCompiler env, HasProcessContext env, HasTerm env)
=> BaseConfigOpts
-> Map GhcPkgId DumpPackage -- ^ Local package dump
-> [LocalPackage]
-> RIO env ()
generateLocalHaddockIndex bco localDumpPkgs locals = do
let dumpPackages =
mapMaybe
( \LocalPackage {package = Package {name, version}} ->
F.find
( \dp -> dp.packageIdent ==
PackageIdentifier name version
)
localDumpPkgs
)
locals
generateHaddockIndex
"local packages"
bco
dumpPackages
"."
(localDocDir bco)
-- | Generate Haddock index and contents for local packages and their
-- dependencies.
generateDepsHaddockIndex ::
(HasCompiler env, HasProcessContext env, HasTerm env)
=> BaseConfigOpts
-> Map GhcPkgId DumpPackage -- ^ Global dump information
-> Map GhcPkgId DumpPackage -- ^ Snapshot dump information
-> Map GhcPkgId DumpPackage -- ^ Local dump information
-> [LocalPackage]
-> RIO env ()
generateDepsHaddockIndex bco globalDumpPkgs snapshotDumpPkgs localDumpPkgs locals = do
let deps = ( mapMaybe
(`lookupDumpPackage` allDumpPkgs)
. nubOrd
. findTransitiveDepends
. mapMaybe getGhcPkgId
) locals
depDocDir = localDepsDocDir bco
generateHaddockIndex
"local packages and dependencies"
bco
deps
".."
depDocDir
where
getGhcPkgId :: LocalPackage -> Maybe GhcPkgId
getGhcPkgId LocalPackage {package = Package {name, version}} =
let pkgId = PackageIdentifier name version
mdpPkg = F.find (\dp -> dp.packageIdent == pkgId) localDumpPkgs
in fmap (.ghcPkgId) mdpPkg
findTransitiveDepends :: [GhcPkgId] -> [GhcPkgId]
findTransitiveDepends = (`go` HS.empty) . HS.fromList
where
go todo checked =
case HS.toList todo of
[] -> HS.toList checked
(ghcPkgId:_) ->
let deps = case lookupDumpPackage ghcPkgId allDumpPkgs of
Nothing -> HS.empty
Just pkgDP -> HS.fromList pkgDP.depends
deps' = deps `HS.difference` checked
todo' = HS.delete ghcPkgId (deps' `HS.union` todo)
checked' = HS.insert ghcPkgId checked
in go todo' checked'
allDumpPkgs = [localDumpPkgs, snapshotDumpPkgs, globalDumpPkgs]
-- | Generate Haddock index and contents for all snapshot packages.
generateSnapHaddockIndex ::
(HasCompiler env, HasProcessContext env, HasTerm env)
=> BaseConfigOpts
-> Map GhcPkgId DumpPackage -- ^ Global package dump
-> Map GhcPkgId DumpPackage -- ^ Snapshot package dump
-> RIO env ()
generateSnapHaddockIndex bco globalDumpPkgs snapshotDumpPkgs =
generateHaddockIndex
"snapshot packages"
bco
(Map.elems snapshotDumpPkgs ++ Map.elems globalDumpPkgs)
"."
(snapDocDir bco)
-- | Generate Haddock index and contents for specified packages.
generateHaddockIndex ::
(HasCompiler env, HasProcessContext env, HasTerm env)
=> Text
-> BaseConfigOpts
-> [DumpPackage]
-> FilePath
-> Path Abs Dir
-> RIO env ()
generateHaddockIndex descr bco dumpPackages docRelFP destDir = do
ensureDir destDir
interfaceOpts <-
(liftIO . fmap nubOrd . mapMaybeM toInterfaceOpt) dumpPackages
unless (null interfaceOpts) $ do
let destIndexFile = haddockIndexFile destDir
eindexModTime <- liftIO (tryGetModificationTime destIndexFile)
let needUpdate =
case eindexModTime of
Left _ -> True
Right indexModTime ->
or [mt > indexModTime | (_, mt, _, _) <- interfaceOpts]
prettyDescr = style Current (fromString $ T.unpack descr)
if needUpdate
then do
prettyInfo $
fillSep
[ flow "Updating Haddock index for"
, prettyDescr
, "in:"
]
<> line
<> pretty destIndexFile
liftIO (mapM_ copyPkgDocs interfaceOpts)
haddockExeName <- view $ compilerPathsL . to (toFilePath . (.haddock))
withWorkingDir (toFilePath destDir) $ readProcessNull
haddockExeName
( map
(("--optghc=-package-db=" ++ ) . toFilePathNoTrailingSep)
[bco.snapDB, bco.localDB]
++ bco.buildOpts.haddockOpts.additionalArgs
++ ["--gen-contents", "--gen-index"]
++ [x | (xs, _, _, _) <- interfaceOpts, x <- xs]
)
else
prettyInfo $
fillSep
[ flow "Haddock index for"
, prettyDescr
, flow "already up to date at:"
]
<> line
<> pretty destIndexFile
where
toInterfaceOpt ::
DumpPackage
-> IO (Maybe ([String], UTCTime, Path Abs File, Path Abs File))
toInterfaceOpt DumpPackage {haddockInterfaces, packageIdent, haddockHtml} =
case haddockInterfaces of
[] -> pure Nothing
srcInterfaceFP:_ -> do
srcInterfaceAbsFile <- parseCollapsedAbsFile srcInterfaceFP
let (PackageIdentifier name _) = packageIdent
destInterfaceRelFP =
docRelFP FP.</>
packageIdentifierString packageIdent FP.</>
(packageNameString name FP.<.> "haddock")
docPathRelFP =
fmap ((docRelFP FP.</>) . FP.takeFileName) haddockHtml
interfaces = intercalate "," $ mcons docPathRelFP [srcInterfaceFP]
destInterfaceAbsFile <-
parseCollapsedAbsFile (toFilePath destDir FP.</> destInterfaceRelFP)
esrcInterfaceModTime <- tryGetModificationTime srcInterfaceAbsFile
pure $
case esrcInterfaceModTime of
Left _ -> Nothing
Right srcInterfaceModTime ->
Just
( [ "-i", interfaces ]
, srcInterfaceModTime
, srcInterfaceAbsFile
, destInterfaceAbsFile
)
copyPkgDocs :: (a, UTCTime, Path Abs File, Path Abs File) -> IO ()
copyPkgDocs (_, srcInterfaceModTime, srcInterfaceAbsFile, destInterfaceAbsFile) = do
-- Copy dependencies' haddocks to documentation directory. This way,
-- relative @../$pkg-$ver@ links work and it's easy to upload docs to a web
-- server or otherwise view them in a non-local-filesystem context. We copy
-- instead of symlink for two reasons: (1) symlinks aren't reliably supported
-- on Windows, and (2) the filesystem containing dependencies' docs may not be
-- available where viewing the docs (e.g. if building in a Docker container).
edestInterfaceModTime <- tryGetModificationTime destInterfaceAbsFile
case edestInterfaceModTime of
Left _ -> doCopy
Right destInterfaceModTime
| destInterfaceModTime < srcInterfaceModTime -> doCopy
| otherwise -> pure ()
where
doCopy = do
ignoringAbsence (removeDirRecur destHtmlAbsDir)
ensureDir destHtmlAbsDir
onException
(copyDirRecur' (parent srcInterfaceAbsFile) destHtmlAbsDir)
(ignoringAbsence (removeDirRecur destHtmlAbsDir))
destHtmlAbsDir = parent destInterfaceAbsFile
-- | Find first DumpPackage matching the GhcPkgId
lookupDumpPackage :: GhcPkgId
-> [Map GhcPkgId DumpPackage]
-> Maybe DumpPackage
lookupDumpPackage ghcPkgId dumpPkgs =
listToMaybe $ mapMaybe (Map.lookup ghcPkgId) dumpPkgs
-- | Path of haddock index file.
haddockIndexFile :: Path Abs Dir -> Path Abs File
haddockIndexFile destDir = destDir </> relFileIndexHtml
-- | Path of local packages documentation directory.
localDocDir :: BaseConfigOpts -> Path Abs Dir
localDocDir bco = bco.localInstallRoot </> docDirSuffix
-- | Path of documentation directory for the dependencies of local packages
localDepsDocDir :: BaseConfigOpts -> Path Abs Dir
localDepsDocDir bco = localDocDir bco </> relDirAll
-- | Path of snapshot packages documentation directory.
snapDocDir :: BaseConfigOpts -> Path Abs Dir
snapDocDir bco = bco.snapInstallRoot </> docDirSuffix
generateLocalHaddockForHackageArchives ::
(HasEnvConfig env, HasTerm env)
=> [LocalPackage]
-> RIO env ()
generateLocalHaddockForHackageArchives =
mapM_
( \lp ->
let pkg = lp.package
pkgId = PackageIdentifier pkg.name pkg.version
pkgDir = parent lp.cabalFP
in generateLocalHaddockForHackageArchive pkgDir pkgId
)
-- | Generate an archive file containing local Haddock documentation for
-- Hackage, in a form accepted by Hackage.
generateLocalHaddockForHackageArchive ::
(HasEnvConfig env, HasTerm env)
=> Path Abs Dir
-- ^ The package directory.
-> PackageIdentifier
-- ^ The package name and version.
-> RIO env ()
generateLocalHaddockForHackageArchive pkgDir pkgId = do
distDir <- distDirFromDir pkgDir
let pkgIdName = display pkgId
name = pkgIdName <> "-docs"
(nameRelDir, tarGzFileName) = fromMaybe
(error "impossible")
( do relDir <- parseRelDir name
nameRelFile <- parseRelFile name
tarGz <- addExtension ".gz" =<< addExtension ".tar" nameRelFile
pure (relDir, tarGz)
)
tarGzFile = distDir </> tarGzFileName
docDir = distDir </> docDirSuffix </> htmlDirSuffix
createTarGzFile tarGzFile docDir nameRelDir
prettyInfo $
fillSep
[ flow "Archive of Haddock documentation for Hackage for"
, style Current (fromString pkgIdName)
, flow "created at:"
]
<> line
<> pretty tarGzFile
createTarGzFile ::
Path Abs File
-- ^ Full path to archive file
-> Path Abs Dir
-- ^ Base directory
-> Path Rel Dir
-- ^ Directory to archive, relative to base directory
-> RIO env ()
createTarGzFile tar base dir = do
entries <- liftIO $ Tar.pack base' [dir']
BL.writeFile tar' $ GZip.compress $ Tar.write entries
where
base' = fromAbsDir base
dir' = fromRelDir dir
tar' = fromAbsFile tar