mirror of
https://github.com/commercialhaskell/stackage.git
synced 2026-03-11 11:16:34 +01:00
449 lines
17 KiB
Haskell
449 lines
17 KiB
Haskell
{-# LANGUAGE BangPatterns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
{-# LANGUAGE ExtendedDefaultRules #-}
|
|
|
|
-- | Build everything with Shake.
|
|
|
|
module Stackage.ShakeBuild where
|
|
|
|
import Stackage.BuildConstraints
|
|
import Stackage.BuildPlan
|
|
import Stackage.CheckBuildPlan
|
|
import Stackage.PackageDescription
|
|
import Stackage.PerformBuild (PerformBuild(..),copyBuiltInHaddocks,renameOrCopy,copyDir)
|
|
import Stackage.Prelude (unFlagName)
|
|
|
|
import Control.Concurrent
|
|
import Control.Concurrent.STM
|
|
import Control.Exception
|
|
import Control.Monad
|
|
import Control.Monad.IO.Class
|
|
import Data.Conduit
|
|
import qualified Data.Conduit.List as CL
|
|
import Data.Conduit.Process
|
|
import qualified Data.Conduit.Text as CT
|
|
import Data.List
|
|
import Data.Map.Strict (Map)
|
|
import qualified Data.Map.Strict as M
|
|
import Data.Maybe
|
|
import Data.Monoid
|
|
import qualified Data.Set as S
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import Data.Version
|
|
import Development.Shake.FilePath
|
|
import Distribution.Compat.ReadP
|
|
import Distribution.Package
|
|
import Distribution.Text (display)
|
|
import Distribution.Text (parse)
|
|
import qualified Filesystem as FP
|
|
import Filesystem.Path.CurrentOS (FilePath)
|
|
import qualified Filesystem.Path.CurrentOS as FP
|
|
import Prelude hiding (FilePath)
|
|
import System.Environment
|
|
import System.Exit
|
|
|
|
-- | Run the shake builder.
|
|
performBuild :: PerformBuild -> IO ()
|
|
performBuild pb' = do
|
|
cur <- FP.getWorkingDirectory
|
|
let shakeDir = cur <> "shake/"
|
|
FP.createTree shakeDir
|
|
haddockFiles <- liftIO (newTVarIO mempty)
|
|
registerLock <- liftIO (newMVar ())
|
|
pkgs <- getRegisteredPackages shakeDir
|
|
let !pb = pb'
|
|
{ pbInstallDest = cur <> pbInstallDest pb'
|
|
}
|
|
cleanOldPackages pb shakeDir pkgs
|
|
printNewPackages pb pkgs
|
|
startShake 2 shakeDir (shakePlan haddockFiles registerLock pb shakeDir)
|
|
|
|
-- | The complete build plan as far as Shake is concerned.
|
|
shakePlan :: TVar (Map String FilePath) -> MVar () -> PerformBuild -> FilePath -> Rules ()
|
|
shakePlan haddockFiles registerLock pb shakeDir = do
|
|
fetched <- target (targetForFetched shakeDir) $ fetchedTarget shakeDir pb
|
|
db <- target (targetForDb shakeDir) $
|
|
databaseTarget shakeDir pb
|
|
_ <- forM (mapMaybe (\p -> find ((==p) . fst) versionMappings) corePackages) $
|
|
\(name,version) ->
|
|
let fp = targetForPackage shakeDir name version
|
|
in target fp (makeTargetFile fp)
|
|
packageTargets <-
|
|
forM normalPackages $
|
|
\(name,plan) ->
|
|
target (targetForPackage shakeDir name (ppVersion plan)) $
|
|
do need [db, fetched]
|
|
packageTarget haddockFiles registerLock pb shakeDir name plan
|
|
haddockTargets <-
|
|
forM normalPackages $
|
|
\(name,plan) ->
|
|
target (targetForDocs shakeDir name (ppVersion plan)) $
|
|
do need [targetForPackage shakeDir name (ppVersion plan)]
|
|
packageDocs haddockFiles shakeDir pb plan name
|
|
want haddockTargets
|
|
where versionMappings = M.toList (M.map ppVersion (bpPackages (pbPlan pb)))
|
|
corePackages = M.keys $ siCorePackages $ bpSystemInfo $ pbPlan pb
|
|
normalPackages = filter (not . (`elem` corePackages) . fst) $
|
|
M.toList $ bpPackages $ pbPlan pb
|
|
|
|
-- | Generate haddock docs for the package.
|
|
packageDocs :: TVar (Map String FilePath) -> FilePath -> PerformBuild -> PackagePlan -> PackageName -> Action ()
|
|
packageDocs haddockFiles shakeDir pb plan name = do
|
|
pwd <- liftIO FP.getWorkingDirectory
|
|
env <- liftIO (fmap (Env . (++ defaultEnv pb shakeDir pwd)) getEnvironment)
|
|
when (haddocksFlag /= Don'tBuild &&
|
|
not (S.null $ sdModules $ ppDesc plan)) $
|
|
generateHaddocks
|
|
haddockFiles
|
|
pb
|
|
shakeDir
|
|
(pkgDir shakeDir name version)
|
|
env
|
|
name
|
|
version
|
|
haddocksFlag
|
|
makeTargetFile (targetForDocs shakeDir name (ppVersion plan))
|
|
where version = ppVersion plan
|
|
haddocksFlag = pcHaddocks $ ppConstraints plan
|
|
|
|
-- | Default environment for running commands.
|
|
defaultEnv :: PerformBuild -> FilePath -> FilePath -> [(String, String)]
|
|
defaultEnv pb shakeDir pwd =
|
|
[( "HASKELL_PACKAGE_SANDBOX"
|
|
, FP.encodeString (pwd <> buildDatabase shakeDir))
|
|
| pbGlobalInstall pb]
|
|
|
|
-- | Initialize the database if there one needs to be, and in any case
|
|
-- create the target file.
|
|
databaseTarget :: FilePath -> PerformBuild -> Action ()
|
|
databaseTarget shakeDir pb = do
|
|
if pbGlobalInstall pb
|
|
then return ()
|
|
else do
|
|
liftIO (FP.removeTree dir)
|
|
liftIO (FP.createTree dir)
|
|
() <- cmd "ghc-pkg" "init" (FP.encodeString dir)
|
|
liftIO $ copyBuiltInHaddocks $ pbDocDir pb
|
|
makeTargetFile (targetForDb shakeDir)
|
|
where dir = buildDatabase shakeDir
|
|
|
|
-- | Build, test and generate documentation for the package.
|
|
packageTarget :: TVar (Map String FilePath) -> MVar () -> PerformBuild -> FilePath -> PackageName -> PackagePlan -> Action ()
|
|
packageTarget haddockFiles registerLock pb shakeDir name plan = do
|
|
need $
|
|
map (\(name,version) -> targetForPackage shakeDir name version) $
|
|
mapMaybe (\p -> find ((==p) . fst) versionMappings) $
|
|
filter (/= name) $
|
|
M.keys $ M.filter libAndExe $ sdPackages $ ppDesc plan
|
|
pwd <- liftIO FP.getWorkingDirectory
|
|
env <- liftIO (fmap (Env . (++ defaultEnv pb shakeDir pwd)) getEnvironment)
|
|
unpack shakeDir name version
|
|
configure shakeDir dir env pb plan
|
|
() <- cmd cwd env "cabal" "build" "--ghc-options=-O0"
|
|
register dir env registerLock
|
|
makeTargetFile (targetForPackage shakeDir name version)
|
|
where dir = pkgDir shakeDir name version
|
|
version = ppVersion plan
|
|
versionMappings = M.toList (M.map ppVersion (bpPackages (pbPlan pb)))
|
|
cwd = Cwd (FP.encodeString dir)
|
|
|
|
-- | Make sure all package archives have been fetched.
|
|
fetchedTarget :: FilePath -> PerformBuild -> Action ()
|
|
fetchedTarget shakeDir pb = do
|
|
() <- cmd "cabal" "fetch" "--no-dependencies" $
|
|
map
|
|
(\(name,plan) -> display name ++ "-" ++ display (ppVersion plan)) $
|
|
M.toList $ bpPackages $ pbPlan pb
|
|
makeTargetFile (targetForFetched shakeDir)
|
|
|
|
-- | Unpack the package.
|
|
unpack :: FilePath -> PackageName -> Version -> Action ()
|
|
unpack shakeDir name version = do
|
|
unpacked <- liftIO $ FP.isFile $
|
|
pkgDir shakeDir name version <>
|
|
FP.decodeString
|
|
(display name ++ ".cabal")
|
|
unless unpacked $
|
|
do liftIO $ catch (FP.removeTree (pkgDir shakeDir name version)) $
|
|
\(_ :: IOException) -> return ()
|
|
cmd
|
|
(Cwd (FP.encodeString (shakeDir <> "packages")))
|
|
"cabal"
|
|
"unpack"
|
|
(nameVer name version)
|
|
|
|
-- | Configure the given package.
|
|
configure :: FilePath -> FilePath -> CmdOption -> PerformBuild -> PackagePlan -> Action ()
|
|
configure shakeDir pkgDir env pb plan = do
|
|
pwd <- liftIO FP.getWorkingDirectory
|
|
cmd (Cwd (FP.encodeString pkgDir)) env "cabal" "configure" (opts pwd)
|
|
where
|
|
opts pwd =
|
|
[ "--package-db=clear"
|
|
, "--package-db=global"
|
|
, "--libdir=" ++ FP.encodeString (pbLibDir pb)
|
|
, "--bindir=" ++ FP.encodeString (pbBinDir pb)
|
|
, "--datadir=" ++ FP.encodeString (pbDataDir pb)
|
|
, "--docdir=" ++ FP.encodeString (pbDocDir pb)
|
|
, "--flags=" ++ planFlags plan] ++
|
|
["--package-db=" ++ FP.encodeString (buildDatabase shakeDir) | not (pbGlobalInstall pb)]
|
|
|
|
-- | Register the package.
|
|
--
|
|
-- TODO: Do a mutex lock in here. Does Shake already support doing
|
|
-- this out of the box?
|
|
register :: FilePath -> CmdOption -> MVar () -> Action ()
|
|
register pkgDir env registerLock = do
|
|
() <- cmd cwd env "cabal" "copy"
|
|
liftIO (takeMVar registerLock)
|
|
() <- cmd cwd env "cabal" "register"
|
|
liftIO (putMVar registerLock ())
|
|
where cwd = Cwd (FP.encodeString pkgDir)
|
|
|
|
-- | Generate haddocks for the package.
|
|
generateHaddocks :: TVar (Map String FilePath) -> PerformBuild -> FilePath -> FilePath -> CmdOption -> PackageName -> Version -> TestState -> Action ()
|
|
generateHaddocks haddockFiles pb shakeDir pkgDir env name version expected = do
|
|
hfs <- liftIO $ readTVarIO haddockFiles
|
|
exitCode <-
|
|
cmd
|
|
(Cwd (FP.encodeString pkgDir))
|
|
env
|
|
"cabal"
|
|
"haddock"
|
|
"--hyperlink-source"
|
|
"--html"
|
|
"--hoogle"
|
|
"--html-location=../$pkg-$version/"
|
|
(map
|
|
(\(pkgVer,hf) ->
|
|
concat
|
|
[ "--haddock-options=--read-interface="
|
|
, "../"
|
|
, pkgVer
|
|
, "/,"
|
|
, FP.encodeString hf])
|
|
(M.toList hfs))
|
|
case (exitCode, expected) of
|
|
(ExitSuccess,ExpectFailure) -> return () -- FIXME: warn.
|
|
(ExitFailure{},ExpectSuccess) -> throw exitCode -- FIXME: report it
|
|
_ -> return ()
|
|
copy
|
|
where
|
|
ident = nameVer name version
|
|
copy = do
|
|
liftIO $
|
|
do let orig = pkgDocDir shakeDir name version
|
|
exists <- FP.isDirectory orig
|
|
when exists $
|
|
renameOrCopy
|
|
orig
|
|
(pbDocDir pb <> FP.decodeString ident)
|
|
enewPath <-
|
|
liftIO $
|
|
try $
|
|
FP.canonicalizePath
|
|
(pbDocDir pb <> FP.decodeString ident <>
|
|
FP.decodeString (display name ++ ".haddock"))
|
|
case enewPath of
|
|
Left (e :: IOException) -> return () -- FIXME: log it with Shake.
|
|
Right newPath -> liftIO $ atomically $ modifyTVar haddockFiles $
|
|
M.insert (ident) newPath
|
|
|
|
-- | Generate a flags string for the package plan.
|
|
planFlags :: PackagePlan -> String
|
|
planFlags plan = unwords $
|
|
map go $ M.toList (pcFlagOverrides (ppConstraints plan))
|
|
where go (name',isOn) = concat
|
|
[ if isOn then "" else "-" , T.unpack (unFlagName name')]
|
|
|
|
-- | Database location.
|
|
buildDatabase :: FilePath -> FilePath
|
|
buildDatabase shakeDir = shakeDir <> "pkgdb"
|
|
|
|
-- | Print the name and version.
|
|
nameVer :: PackageName -> Version -> String
|
|
nameVer name version = display name ++ "-" ++ display version
|
|
|
|
-- | The directory for the package's docs.
|
|
pkgDocDir :: FilePath -> PackageName -> Version -> FilePath
|
|
pkgDocDir shakeDir name version = pkgDir shakeDir name version <>
|
|
"dist" <>
|
|
"doc" <>
|
|
"html" <>
|
|
(FP.decodeString (display name))
|
|
|
|
-- | The package directory.
|
|
pkgDir :: FilePath -> PackageName -> Version -> FilePath
|
|
pkgDir shakeDir name version = shakeDir <> "packages" <>
|
|
(FP.decodeString (nameVer name version))
|
|
|
|
-- | Get the target file for confirming that all packages have been
|
|
-- pre-fetched.
|
|
targetForFetched :: FilePath -> Target
|
|
targetForFetched shakeDir =
|
|
Target (shakeDir <> "packages-fetched")
|
|
|
|
-- | Get the target file for a package.
|
|
targetForPackage :: FilePath -> PackageName -> Version -> Target
|
|
targetForPackage shakeDir name version = Target $
|
|
shakeDir <> "packages" <>
|
|
FP.decodeString
|
|
(nameVer name version) <>
|
|
"dist" <>
|
|
"shake-build"
|
|
|
|
-- | Get the target file for a package.
|
|
targetForDocs :: FilePath -> PackageName -> Version -> Target
|
|
targetForDocs shakeDir name version = Target $
|
|
shakeDir <> "packages" <>
|
|
FP.decodeString
|
|
(nameVer name version) <>
|
|
"dist" <>
|
|
"shake-docs"
|
|
|
|
-- | Target for the complete, copied build under builds/date/.
|
|
targetForBuild :: PerformBuild -> Target
|
|
targetForBuild pb = Target $ (pbInstallDest pb) <> "shake-built"
|
|
|
|
-- | Get a package database path.
|
|
targetForDb :: FilePath -> Target
|
|
targetForDb shakeDir =
|
|
Target $ shakeDir <> "pkgdb-initialized"
|
|
|
|
-- | Make a file of this name.
|
|
makeTargetFile :: Target -> Action ()
|
|
makeTargetFile fp = liftIO $ FP.writeFile (unTarget fp) ""
|
|
|
|
pbBinDir, pbLibDir, pbDataDir, pbDocDir :: PerformBuild -> FilePath
|
|
pbBinDir root = (pbInstallDest root) <> "bin"
|
|
pbLibDir root = (pbInstallDest root) <> "lib"
|
|
pbDataDir root = (pbInstallDest root) <> "share"
|
|
pbDocDir root = (pbInstallDest root) <> "doc"
|
|
|
|
-- | Reason for purging a package.
|
|
data PurgeReason
|
|
= NoLongerIncluded
|
|
| Replaced Version
|
|
| Broken
|
|
|
|
-- | Print the new packages.
|
|
printNewPackages :: PerformBuild -> [PackageIdentifier] -> IO (Map PackageName Version)
|
|
printNewPackages pb pkgs = do
|
|
unless
|
|
(M.null new)
|
|
(do putStrLn
|
|
("There are " ++
|
|
show (M.size new) ++
|
|
" packages to build and install: ")
|
|
forM_
|
|
(take maxDisplay (M.toList new))
|
|
(\(name,ver) ->
|
|
putStrLn (display name))
|
|
when (M.size new > maxDisplay)
|
|
(putStrLn ("And " ++ show (M.size new - maxDisplay) ++ " more.")))
|
|
return new
|
|
where maxDisplay = 10
|
|
new = M.filterWithKey
|
|
(\name ver ->
|
|
isNothing (find ((== name) . pkgName) pkgs))
|
|
versions
|
|
versions = (M.map ppVersion .
|
|
M.filter (not . S.null . sdModules . ppDesc) .
|
|
bpPackages . pbPlan) pb
|
|
|
|
-- | Clean up old versions of packages that are no longer in use.
|
|
cleanOldPackages :: PerformBuild -> FilePath -> [PackageIdentifier] -> IO ()
|
|
cleanOldPackages pb shakeDir pkgs = do
|
|
putStrLn "Collecting garbage"
|
|
pkgs <- getRegisteredPackages shakeDir
|
|
let toRemove = mapMaybe
|
|
(\(PackageIdentifier name version) ->
|
|
case M.lookup name versions of
|
|
Just version'
|
|
| version' == version ->
|
|
Nothing
|
|
Just newVersion -> Just
|
|
(name, version, (Replaced newVersion))
|
|
Nothing -> Just (name, version, NoLongerIncluded))
|
|
pkgs
|
|
unless (null toRemove)
|
|
(putStrLn ("There are " ++ show (length toRemove) ++ " packages to be purged."))
|
|
when (length toRemove > 0)
|
|
(do putStrLn "Waiting 3 seconds before proceeding to remove ..."
|
|
threadDelay (1000 * 1000 * 3))
|
|
forM_ pkgs $
|
|
\(PackageIdentifier name version) ->
|
|
case M.lookup name versions of
|
|
Just version'
|
|
| version' == version ->
|
|
return ()
|
|
Just newVersion -> purgePackage
|
|
shakeDir
|
|
name
|
|
version
|
|
(Replaced newVersion)
|
|
Nothing -> purgePackage shakeDir name version NoLongerIncluded
|
|
broken <- getBrokenPackages shakeDir
|
|
unless (null broken)
|
|
(putStrLn ("There are " ++ show (length broken) ++ " broken packages to be purged."))
|
|
when (length broken > 0)
|
|
(do putStrLn "Waiting 3 seconds before proceeding to remove ..."
|
|
threadDelay (1000 * 1000 * 3))
|
|
forM_
|
|
broken
|
|
(\(PackageIdentifier name version) ->
|
|
purgePackage shakeDir name version Broken)
|
|
where versions = (M.map ppVersion . bpPackages . pbPlan) pb
|
|
|
|
-- | Purge the given package and version.
|
|
purgePackage :: FilePath -> PackageName -> Version -> PurgeReason -> IO ()
|
|
purgePackage shakeDir name version reason = do
|
|
putStr $ "Purging package: " ++ ident ++ " (" ++ showReason ++ ") ... "
|
|
unregister
|
|
delete
|
|
putStrLn "done."
|
|
where showReason =
|
|
case reason of
|
|
Replaced version' -> "replaced by " ++ ordinal ++ " " ++ display version'
|
|
where ordinal | version' > version = "newer"
|
|
| otherwise = "older"
|
|
NoLongerIncluded -> "no longer included"
|
|
Broken -> "broken"
|
|
ident = nameVer name version
|
|
unregister = do
|
|
void (readProcessWithExitCode
|
|
"ghc-pkg"
|
|
["unregister", "-f", FP.encodeString (buildDatabase shakeDir), "--force", ident]
|
|
"")
|
|
delete = FP.removeTree $
|
|
pkgDir shakeDir name version
|
|
|
|
-- | Get broken packages.
|
|
getBrokenPackages :: FilePath -> IO [PackageIdentifier]
|
|
getBrokenPackages shakeDir = do
|
|
(_,ps) <- sourceProcessWithConsumer
|
|
(proc
|
|
"ghc-pkg"
|
|
["check", "--simple-output", "-f", FP.encodeString (buildDatabase shakeDir)])
|
|
(CT.decodeUtf8 $= CT.lines $= CL.consume)
|
|
return (mapMaybe parsePackageIdent (T.words (T.unlines ps)))
|
|
|
|
-- | Get available packages.
|
|
getRegisteredPackages :: FilePath -> IO [PackageIdentifier]
|
|
getRegisteredPackages shakeDir = do
|
|
(_,ps) <- sourceProcessWithConsumer
|
|
(proc
|
|
"ghc-pkg"
|
|
["list", "--simple-output", "-f", FP.encodeString (buildDatabase shakeDir)])
|
|
(CT.decodeUtf8 $= CT.lines $= CL.consume)
|
|
return (mapMaybe parsePackageIdent (T.words (T.unlines ps)))
|
|
|
|
-- | Parse a package identifier: foo-1.2.3
|
|
parsePackageIdent :: Text -> Maybe PackageIdentifier
|
|
parsePackageIdent = fmap fst .
|
|
listToMaybe .
|
|
filter (null . snd) .
|
|
readP_to_S parse . T.unpack
|