module Stackage.PerformBuild
( performBuild
, PerformBuild (..)
, BuildException (..)
, pbDocDir
, sdistFilePath
) where
import Control.Concurrent.Async (async)
import Control.Concurrent.STM.TSem
import Control.Monad.Writer.Strict (execWriter, tell)
import qualified Data.ByteString as S
import Data.Generics (mkT, everywhere)
import qualified Data.Map as Map
import Data.NonNull (fromNullable)
import Distribution.PackageDescription (buildType, packageDescription, BuildType (Simple),
condTestSuites)
import Distribution.Package (Dependency (..))
import Distribution.PackageDescription.PrettyPrint (writeGenericPackageDescription)
import Distribution.Types.UnqualComponentName
import Distribution.Version (anyVersion)
import Filesystem (canonicalizePath, createTree,
getWorkingDirectory,
removeTree, rename, removeFile)
import Filesystem.Path (parent)
import qualified Filesystem.Path.CurrentOS as F
import Network.HTTP.Simple
import Stackage.BuildConstraints
import Stackage.BuildPlan
import Stackage.GhcPkg
import Stackage.PackageDescription
import Stackage.PackageIndex (gpdFromLBS)
import Stackage.Prelude hiding (pi)
import System.Directory (doesDirectoryExist, doesFileExist, findExecutable, getDirectoryContents)
import qualified System.Directory
import qualified System.FilePath as FP
import System.Environment (getEnvironment)
import System.Exit
import System.IO (IOMode (WriteMode),
openBinaryFile, hFlush)
import System.IO.Temp (withSystemTempDirectory, withSystemTempFile)
import System.Timeout (timeout)
data BuildException = BuildException (Map PackageName BuildFailure) [Text]
deriving Typeable
instance Exception BuildException
instance Show BuildException where
show (BuildException m warnings) =
unlines $ "" : "" : "" : map go (mapToList m) ++ map unpack warnings
where
go (unPackageName -> name, bf) = concat
[ name
, ": "
, take 500 $ show bf
]
data BuildFailure = DependencyFailed PackageName
| DependencyMissing PackageName
| ToolMissing ExeName
| NotImplemented
| BuildFailureException SomeException
deriving (Show, Typeable)
instance Exception BuildFailure
data PerformBuild = PerformBuild
{ pbPlan :: BuildPlan
, pbInstallDest :: FilePath
, pbLog :: ByteString -> IO ()
, pbLogDir :: FilePath
, pbJobs :: Int
, pbGlobalInstall :: Bool
, pbEnableTests :: Bool
, pbEnableBenches :: Bool
, pbEnableHaddock :: Bool
, pbEnableLibProfiling :: Bool
, pbEnableExecDyn :: Bool
, pbVerbose :: Bool
, pbAllowNewer :: Bool
, pbBuildHoogle :: Bool
, pbNoRebuildCabal :: !Bool
, pbCabalFromHead :: !Bool
}
data PackageInfo = PackageInfo
{ piPlan :: PackagePlan
, piName :: PackageName
, piResult :: TMVar Bool
}
waitForDeps :: Map ExeName (Set PackageName)
-> Map PackageName PackageInfo
-> Set Component
-> BuildPlan
-> PackageInfo
-> IO a
-> IO a
waitForDeps toolMap packageMap activeComps bp pi action = do
atomically $ do
mapM_ checkPackage $ addCabal $ Map.keysSet $ filterUnused $ sdPackages $ ppDesc $ piPlan pi
forM_ (Map.keys $ filterUnused $ sdTools $ ppDesc $ piPlan pi) $ \exe -> do
case lookup exe toolMap >>= fromNullable . map checkPackage . setToList of
Nothing
| isCoreExe exe -> return ()
| otherwise -> return ()
Just packages -> ofoldl1' (<|>) packages
action
where
filterUnused :: Ord key => Map key DepInfo -> Map key DepInfo
filterUnused =
mapFromList . filter (go . snd) . mapToList
where
go = not . null . intersection activeComps . diComponents
checkPackage package | package == piName pi = return ()
checkPackage package =
case lookup package packageMap of
Nothing
| isCore package -> return ()
| otherwise -> throwSTM $ DependencyMissing package
Just dep -> do
res <- readTMVar $ piResult dep
unless res $ throwSTM $ DependencyFailed package
isCore = (`member` siCorePackages (bpSystemInfo bp))
isCoreExe = (`member` siCoreExecutables (bpSystemInfo bp))
addCabal :: Set PackageName -> Set PackageName
addCabal = insertSet (mkPackageName "Cabal")
withCounter :: TVar Int -> IO a -> IO a
withCounter counter = bracket_
(atomically $ modifyTVar counter (+ 1))
(atomically $ modifyTVar counter (subtract 1))
withTSem :: TSem -> IO a -> IO a
withTSem sem = bracket_ (atomically $ waitTSem sem) (atomically $ signalTSem sem)
pbDatabase :: PerformBuild -> Maybe FilePath
pbDatabase pb
| pbGlobalInstall pb = Nothing
| otherwise = Just $ pbInstallDest pb </> "pkgdb"
pbBinDir, pbLibDir, pbDataDir, pbLibexecDir, pbSysconfDir, pbDocDir :: PerformBuild -> FilePath
pbBinDir pb = pbInstallDest pb </> "bin"
pbLibDir pb = pbInstallDest pb </> "lib"
pbDataDir pb = pbInstallDest pb </> "share"
pbLibexecDir pb = pbInstallDest pb </> "libexec"
pbSysconfDir pb = pbInstallDest pb </> "etc"
pbDocDir pb = pbInstallDest pb </> "doc"
pbPrevResDir :: PerformBuild -> FilePath
pbPrevResDir pb = pbInstallDest pb </> "prevres"
performBuild :: PerformBuild -> IO [Text]
performBuild pb = do
cwd <- getWorkingDirectory
performBuild' pb
{ pbInstallDest = F.encodeString cwd </> pbInstallDest pb
, pbLogDir = F.encodeString cwd </> pbLogDir pb
}
performBuild' :: PerformBuild -> IO [Text]
performBuild' pb@PerformBuild {..} = withBuildDir $ \builddir -> do
let removeTree' fp = whenM (doesDirectoryExist fp) (removeTree $ fromString fp)
removeTree' $ fromString pbLogDir
forM_ (pbDatabase pb) $ \db ->
unlessM (doesFileExist $ db </> "package.cache") $ do
createTree $ parent $ fromString db
withCheckedProcess (proc "ghc-pkg" ["init", db])
$ \ClosedStream Inherited Inherited -> return ()
pbLog $ encodeUtf8 "Copying built-in Haddocks\n"
copyBuiltInHaddocks (pbDocDir pb)
sem <- atomically $ newTSem pbJobs
active <- newTVarIO (0 :: Int)
let toolMap = makeToolMap (bpBuildToolOverrides pbPlan) (bpPackages pbPlan)
errsVar <- newTVarIO mempty
warningsVar <- newTVarIO id
mutex <- newMVar ()
env <- getEnvironment
registeredPackages <- setupPackageDatabase
(pbDatabase pb)
(pbDocDir pb)
pbLog
(ppVersion <$> bpPackages pbPlan)
(deletePreviousResults pb)
allPreviousResults <- getAllPreviousResults pb
packageMap' <- fmap fold $ forM (mapToList $ bpPackages pbPlan)
$ \(name, plan) -> do
let piPlan = plan
piName = name
piResult <- newEmptyTMVarIO
return $ asMap $ singletonMap name PackageInfo {..}
packageMap <- calculatePackageMap pb registeredPackages allPreviousResults packageMap'
pbLog "Collecting existing .haddock files\n"
haddockFiles <- getHaddockFiles pb >>= newTVarIO
haddockDeps <- newTVarIO mempty
forM_ packageMap $ \pi -> void $ Control.Concurrent.Async.async $ singleBuild pb registeredPackages
SingleBuild
{ sbSem = sem
, sbErrsVar = errsVar
, sbWarningsVar = warningsVar
, sbActive = active
, sbToolMap = toolMap
, sbPackageMap = packageMap
, sbBuildDir = builddir
, sbPackageInfo = pi
, sbRegisterMutex = mutex
, sbModifiedEnv = maybe
id
(\db -> (("HASKELL_PACKAGE_SANDBOX", db):))
(pbDatabase pb)
(filter allowedEnv $ map fixEnv env)
, sbHaddockFiles = haddockFiles
, sbHaddockDeps = haddockDeps
}
void $ tryAny $ atomically $ readTVar active >>= checkSTM . (== 0)
warnings <- ($ []) <$> readTVarIO warningsVar
errs <- readTVarIO errsVar
when (not $ null errs) $ throwM $ BuildException errs warnings
return warnings
where
withBuildDir f = withSystemTempDirectory "stackage-build" f
fixEnv (p, x)
| toUpper p == "PATH" = (p, pbBinDir pb ++ pathSep : x)
| otherwise = (p, x)
allowedEnv (k, _) = k `notMember` bannedEnvs
pathSep :: Char
#ifdef mingw32_HOST_OS
pathSep = ';'
#else
pathSep = ':'
#endif
bannedEnvs :: Set String
bannedEnvs = setFromList
[ "STACKAGE_AUTH_TOKEN"
]
data SingleBuild = SingleBuild
{ sbSem :: TSem
, sbErrsVar :: TVar (Map PackageName BuildFailure)
, sbWarningsVar :: TVar ([Text] -> [Text])
, sbActive :: TVar Int
, sbToolMap :: Map ExeName (Set PackageName)
, sbPackageMap :: Map PackageName PackageInfo
, sbBuildDir :: FilePath
, sbPackageInfo :: PackageInfo
, sbRegisterMutex :: MVar ()
, sbModifiedEnv :: [(String, String)]
, sbHaddockFiles :: TVar (Map Text FilePath)
, sbHaddockDeps :: TVar (Map PackageName (Set PackageName))
}
singleBuild :: PerformBuild
-> Map PackageName Version
-> SingleBuild -> IO ()
singleBuild pb@PerformBuild {..} registeredPackages SingleBuild {..} = do
withCounter sbActive
$ handle updateErrs
$ (`finally` void (atomically $ tryPutTMVar (piResult sbPackageInfo) False))
$ inner
where
libComps = setFromList [CompLibrary, CompExecutable]
testComps = insertSet CompTestSuite libComps
benchComps = insertSet CompBenchmark libComps
thisIsCabal = pname == mkPackageName "Cabal"
inner
| thisIsCabal && pbNoRebuildCabal =
atomically $ putTMVar (piResult sbPackageInfo) True
| otherwise = do
let wfd comps =
waitForDeps sbToolMap sbPackageMap comps pbPlan sbPackageInfo
. withTSem sbSem
withUnpacked <- wfd libComps buildLibrary
wfd testComps (runTests withUnpacked)
wfd benchComps (buildBenches withUnpacked)
pname = piName sbPackageInfo
pident = PackageIdentifier pname (ppVersion $ piPlan sbPackageInfo)
name = display pname
version = display $ ppVersion $ piPlan sbPackageInfo
namever = concat
[ name
, "-"
, version
]
nameverrevhash = concat
[ namever
, fromMaybe (assert False "") $ do
cfi <- ppCabalFileInfo $ piPlan sbPackageInfo
hash <- lookup "GitSHA1" $ cfiHashes cfi
Just $ "@gitsha1:" ++ hash
]
quote :: Text -> Text
quote s
| any special s = tshow s
| otherwise = s
where
special ' ' = True
special '\'' = True
special '"' = True
special _ = False
runIn :: FilePath -> IO Handle -> Text -> [Text] -> IO ()
runIn wdir getOutH cmd args = do
outH <- getOutH
S.hPut outH $ encodeUtf8 $ concat
[ "> "
, pack wdir
, "$ "
, unwords $ map quote $ cmd : args
, "\n"
]
hFlush outH
let cp' = cp outH
(ClosedStream, UseProvidedHandle, UseProvidedHandle, sph)
<- streamingProcess cp'
ec <- waitForStreamingProcess sph `onException` do
let ph = streamingProcessHandleRaw sph
terminateProcess ph
unless (ec == ExitSuccess) $ throwIO $ ProcessExitedUnsuccessfully cp' ec
where
cp outH = (proc (unpack cmd) (map unpack args))
{ cwd = Just wdir
, std_out = UseHandle outH
, std_err = UseHandle outH
, env = Just sbModifiedEnv
}
runParent = runIn sbBuildDir
log' t = do
i <- readTVarIO sbActive
errs <- readTVarIO sbErrsVar
pbLog $ encodeUtf8 $ concat
[ t
, " (pending: "
, tshow i
, ", failures: "
, tshow $ length errs
, ")\n"
]
libOut = pbLogDir </> unpack namever </> "build.out"
testOut = pbLogDir </> unpack namever </> "test.out"
benchOut = pbLogDir </> unpack namever </> "bench.out"
wf fp inner' = do
ref <- newIORef Nothing
let cleanup = do
mh <- readIORef ref
forM_ mh hClose
getH = do
mh <- readIORef ref
case mh of
Just h -> return h
Nothing -> mask_ $ do
createTree $ parent $ fromString fp
h <- openBinaryFile fp WriteMode
writeIORef ref $ Just h
return h
inner' getH `finally` cleanup
setup run args = do
_ <- run "ghc" $ runghcArgs ["Setup"]
run "./Setup" args
where
runghcArgs :: [Text] -> [Text]
runghcArgs rest =
"-clear-package-db"
: "-global-package-db"
: (case pbDatabase pb of
Nothing -> rest
Just db -> ("-package-db=" ++ pack db) : setupPackages ++ rest)
setupPackages :: [Text]
setupPackages =
case sdSetupDeps $ ppDesc $ piPlan sbPackageInfo of
Nothing -> []
Just pkgs -> "-hide-all-packages" : map (("-package=" ++) . display) (setToList pkgs)
ghcPkgArgs :: [Text] -> [Text]
ghcPkgArgs rest =
"--no-user-package-db"
: (case pbDatabase pb of
Nothing -> rest
Just db -> ("--package-db=" ++ pack db) : rest)
configArgs = ($ []) $ execWriter $ do
tell' "--package-db=clear"
tell' "--package-db=global"
forM_ (pbDatabase pb) $ \db -> tell' $ "--package-db=" ++ pack db
tell' $ "--libdir=" ++ pack (pbLibDir pb)
tell' $ "--bindir=" ++ pack (pbBinDir pb)
tell' $ "--datadir=" ++ pack (pbDataDir pb)
tell' $ "--libexecdir=" ++ pack (pbLibexecDir pb)
tell' $ "--sysconfdir=" ++ pack (pbSysconfDir pb)
tell' $ "--docdir=" ++ pack (pbDocDir pb </> unpack namever)
tell' $ "--htmldir=" ++ pack (pbDocDir pb </> unpack namever)
tell' $ "--haddockdir=" ++ pack (pbDocDir pb </> unpack namever)
tell' $ "--flags=" ++ flags
when (pbEnableLibProfiling && pcEnableLibProfile) $
tell' "--enable-library-profiling"
when pbEnableExecDyn $ tell' "--enable-executable-dynamic"
tell (toList pcConfigureArgs ++)
where
tell' x = tell (x:)
flags :: Text
flags = pack $ unwords $ map go $ mapToList pcFlagOverrides
where
go (name', isOn) = concat
[ if isOn then "" else "-"
, unFlagName name'
]
PackageConstraints {..} = ppConstraints $ piPlan sbPackageInfo
hasLib = not $ null $ sdModules $ ppDesc $ piPlan sbPackageInfo
buildLibrary = wf libOut $ \getOutH -> do
gpdRef <- newIORef Nothing
let withUnpacked inner' = do
mgpd <- readIORef gpdRef
(gpd, childDir) <-
case mgpd of
Just x -> return x
Nothing -> do
childDir <- if thisIsCabal && pbCabalFromHead
then do
log' "Getting most recent Cabal from Git"
runParent getOutH "git"
[ "clone"
, "https://github.com/haskell/cabal"
]
return $ sbBuildDir </> "cabal" </> "Cabal"
else do
log' $ "Unpacking " ++ nameverrevhash
case ppSourceUrl $ piPlan sbPackageInfo of
Nothing -> runParent getOutH "stack" ["unpack", nameverrevhash]
Just url -> unpackFromURL sbBuildDir url
return $ sbBuildDir </> unpack namever
gpd <- createSetupHs childDir name pbAllowNewer
writeIORef gpdRef $ Just (gpd, childDir)
return (gpd, childDir)
inner' gpd childDir
isConfiged <- newIORef False
let withConfiged inner' = withUnpacked $ \_gpd childDir -> do
let run a b = do when pbVerbose $ log' (unwords (a : b))
runIn childDir getOutH a b
cabal = setup run
unlessM (readIORef isConfiged) $ do
log' $ "Configuring " ++ namever
cabal $ "configure" : configArgs
writeIORef isConfiged True
inner' childDir cabal
prevBuildResult <- getPreviousResult pb Build pident
toBuild <- case () of
()
| pcSkipBuild -> return False
| prevBuildResult /= PRSuccess -> return True
| pname `notMember` registeredPackages && hasLib -> do
log' $ concat
[ "WARNING: Package "
, display pname
, " marked as build success, but not registered"
]
return True
| otherwise -> return False
when toBuild $ withConfiged $ \childDir cabal -> do
deletePreviousResults pb pident
log' $ "Building " ++ namever
cabal ["build"]
log' $ "Copying/registering " ++ namever
cabal ["copy"]
withMVar sbRegisterMutex $ const $ do
cabal ["register"]
when pcHide $ do
log' $ "Hiding " ++ namever
runIn childDir getOutH "ghc-pkg" $ ghcPkgArgs ["hide", namever]
savePreviousResult pb Build pident True
atomically $ putTMVar (piResult sbPackageInfo) True
prevHaddockResult <- getPreviousResult pb Haddock pident
let needHaddock = pbEnableHaddock
&& checkPrevResult prevHaddockResult pcHaddocks
&& not (null $ sdModules $ ppDesc $ piPlan sbPackageInfo)
&& not pcSkipBuild
when needHaddock $ withConfiged $ \childDir cabal -> do
log' $ "Haddocks " ++ namever
hfs <- readTVarIO sbHaddockFiles
haddockDeps <- atomically $ getHaddockDeps pbPlan sbHaddockDeps pname
(hyped, _, _) <- readProcessWithExitCode "haddock" ["--hyperlinked-source"] ""
let hfsOpts = map hfOpt
$ filter ((`member` haddockDeps) . toPackageName . fst)
$ mapToList hfs
toPackageName t =
case simpleParse t of
Just (PackageIdentifier x _) -> x
Nothing -> error $ "Invalid package identifier: " ++ unpack t
hfOpt (pkgVer, hf) = concat
[ "--haddock-options=--read-interface="
, "../"
, pkgVer
, "/,"
, pack hf
]
args = ($ hfsOpts) $ execWriter $ do
let tell' x = tell (x:)
tell' "haddock"
tell' $ if hyped == ExitSuccess
then "--haddock-option=--hyperlinked-source"
else "--hyperlink-source"
tell' "--html"
when pbBuildHoogle $ tell' "--hoogle"
tell' "--html-location=../$pkg-$version/"
eres <- tryAny $ cabal args
forM_ eres $ \() -> do
renameOrCopy
(childDir </> "dist" </> "doc" </> "html" </> unpack name)
(pbDocDir pb </> unpack namever)
enewPath <- tryIO'
$ canonicalizePath
$ fromString
$ pbDocDir pb
</> unpack namever
</> unpack name <.> "haddock"
case enewPath of
Left e -> warn $ tshow e
Right newPath -> atomically
$ modifyTVar sbHaddockFiles
$ insertMap namever (F.encodeString newPath)
savePreviousResult pb Haddock pident $ either (const False) (const True) eres
case (eres, pcHaddocks) of
(Left e, ExpectSuccess) -> throwM e
(Right (), ExpectFailure) -> warn $ namever ++ ": unexpected Haddock success"
_ -> return ()
return withUnpacked
runTests withUnpacked = wf testOut $ \getOutH -> do
prevTestResult <- getPreviousResult pb Test pident
let needTest = pbEnableTests
&& checkPrevResult prevTestResult pcTests
&& not pcSkipBuild
when needTest $ withUnpacked $ \gpd childDir -> do
let run = runIn childDir getOutH
cabal = setup run
log' $ "Test configure " ++ namever
cabal $ "configure" : "--enable-tests" : configArgs
eres <- tryAny $ do
log' $ "Test build " ++ namever
cabal ["build"]
let tests = map (unUnqualComponentName . fst) $ condTestSuites gpd
forM_ tests $ \test -> do
log' $ concat
[ "Test run "
, namever
, " ("
, pack test
, ")"
]
let exe = "dist/build" </> test </> test
exists <- liftIO $ doesFileExist $ childDir </> exe
if exists
then do
mres <- timeout maximumTestSuiteTime $ run (pack exe) []
case mres of
Just () -> return ()
Nothing -> error $ concat
[ "Test suite timed out: "
, unpack namever
, ":"
, test
]
else do
outH <- getOutH
hPut outH $ encodeUtf8 $ asText $ "Test suite not built: " ++ pack test
savePreviousResult pb Test pident $ either (const False) (const True) eres
case (eres, pcTests) of
(Left e, ExpectSuccess) -> throwM e
(Right (), ExpectFailure) -> warn $ namever ++ ": unexpected test success"
_ -> return ()
buildBenches withUnpacked = wf benchOut $ \getOutH -> do
prevBenchResult <- getPreviousResult pb Bench pident
let needTest = pbEnableBenches
&& checkPrevResult prevBenchResult pcBenches
&& not pcSkipBuild
when needTest $ withUnpacked $ \_gpd childDir -> do
let run = runIn childDir getOutH
cabal = setup run
log' $ "Benchmark configure " ++ namever
cabal $ "configure" : "--enable-benchmarks" : configArgs
eres <- tryAny $ do
log' $ "Benchmark build " ++ namever
cabal ["build"]
savePreviousResult pb Bench pident $ either (const False) (const True) eres
case (eres, pcBenches) of
(Left e, ExpectSuccess) -> throwM e
(Right (), ExpectFailure) -> warn $ namever ++ ": unexpected benchmark success"
_ -> return ()
warn t = atomically $ modifyTVar sbWarningsVar (. (t:))
updateErrs exc = do
log' $ concat
[ display (piName sbPackageInfo)
, ": "
, take 500 $ tshow exc
]
atomically $ modifyTVar sbErrsVar $ insertMap (piName sbPackageInfo) exc'
where
exc' =
case fromException exc of
Just bf -> bf
Nothing -> BuildFailureException exc
unpackFromURL :: MonadIO m
=> FilePath
-> Text
-> m ()
unpackFromURL destDir url = liftIO $ do
req <- parseRequest $ unpack url
withSystemTempFile "unpack-from-url.tar.gz" $ \fp h -> do
httpSink req (const $ sinkHandle h)
hClose h
let cp = (proc "tar" ["xf", fp])
{ cwd = Just destDir
}
withCheckedProcessCleanup cp
$ \ClosedStream ClosedStream ClosedStream -> return ()
maximumTestSuiteTime :: Int
maximumTestSuiteTime = 10 * 60 * 1000 * 1000
renameOrCopy :: FilePath -> FilePath -> IO ()
renameOrCopy src dest =
rename (fromString src) (fromString dest)
`catchIO'` \_ -> copyDir src dest
copyBuiltInHaddocks :: FilePath -> IO ()
copyBuiltInHaddocks docdir = do
mghc <- findExecutable "ghc"
case mghc of
Nothing -> error "GHC not found on PATH"
Just ghc -> do
let root = F.encodeString (parent (fromString ghc)) </>
"../share/doc"
names <- getDirectoryContents root
let hidden ('.':_) = True
hidden _ = False
name <-
case filter (not . hidden) names of
[x] -> return x
_ -> error $ concat
[ "Unexpected list of contents in "
, root
, ": "
, show names
]
src <- canonicalizePath $ fromString $
root </> name </> "html/libraries"
copyDir (F.encodeString src) docdir
data ResultType = Build | Haddock | Test | Bench
deriving (Show, Enum, Eq, Ord, Bounded, Read)
data PrevResult = PRNoResult | PRSuccess | PRFailure
deriving (Show, Enum, Eq, Ord, Bounded, Read)
checkPrevResult :: PrevResult -> TestState -> Bool
checkPrevResult _ Don'tBuild = False
checkPrevResult PRNoResult _ = True
checkPrevResult PRSuccess _ = False
checkPrevResult PRFailure ExpectSuccess = True
checkPrevResult PRFailure _ = False
withPRPath :: PerformBuild -> ResultType -> PackageIdentifier -> (FilePath -> IO a) -> IO a
withPRPath pb rt ident inner = do
createTree $ parent $ fromString fp
inner fp
where
fp = pbPrevResDir pb </> show rt </> unpack (display ident)
successBS, failureBS :: ByteString
successBS = "success"
failureBS = "failure"
getAllPreviousResults :: PerformBuild -> IO (PackageName -> ResultType -> Maybe (Version, PrevResult))
getAllPreviousResults pb = do
m <- fmap fold $ forM [minBound..maxBound] $ \rt -> asMap . singletonMap rt <$> go rt
return $ \pn rt -> lookup rt m >>= lookup pn
where
go :: ResultType -> IO (Map PackageName (Version, PrevResult))
go rt = do
exists <- doesDirectoryExist dir
allResults <- if exists then
runResourceT
$ sourceDirectory dir
$$ filterMC (liftIO . doesFileExist)
=$ mapMC (liftIO . toMap)
=$ foldlC (unionWith union) mempty
else return mempty
fmap concat $ mapM (uncurry removeDupes) $ mapToList allResults
where
dir = pbPrevResDir pb </> show rt
toMap :: FilePath -> IO (Map PackageName (Map Version PrevResult))
toMap fp = do
case simpleParse $ pack $ FP.takeFileName fp of
Nothing -> return mempty
Just (PackageIdentifier name version) -> do
eres <- tryIO' $ readFile fp
let mres =
case eres of
Right bs
| bs == successBS -> Just PRSuccess
| bs == failureBS -> Just PRFailure
_ -> Nothing
case mres of
Nothing -> return mempty
Just res -> return $ singletonMap name (singletonMap version res)
removeDupes :: PackageName -> Map Version PrevResult -> IO (Map PackageName (Version, PrevResult))
removeDupes name m =
case mapToList m of
[] -> assert False $ return mempty
[pair] -> return $ singletonMap name pair
_pairs -> do
removePreviousResults pb rt name
return mempty
getPreviousResult :: PerformBuild -> ResultType -> PackageIdentifier -> IO PrevResult
getPreviousResult w x y = withPRPath w x y $ \fp -> do
eres <- tryIO' $ readFile fp
return $ case eres of
Right bs
| bs == successBS -> PRSuccess
| bs == failureBS -> PRFailure
_ -> PRNoResult
removePreviousResults :: PerformBuild -> ResultType -> PackageName -> IO ()
removePreviousResults pb rt name =
whenM (doesDirectoryExist dir)
$ runResourceT
$ sourceDirectory dir
$$ filterC isOurPackage
=$ filterMC (liftIO . doesFileExist)
=$ mapM_C (liftIO . System.Directory.removeFile)
where
dir = pbPrevResDir pb </> show rt
prefix = display name ++ "-"
isOurPackage fp = fromMaybe False $ do
versionT <- stripPrefix prefix $ pack $ FP.takeFileName fp
_ :: Version <- simpleParse versionT
return True
savePreviousResult :: PerformBuild -> ResultType -> PackageIdentifier -> Bool -> IO ()
savePreviousResult pb rt ident@(PackageIdentifier name _version) res =
withPRPath pb rt ident $ \fp -> do
removePreviousResults pb rt name
writeFile fp $ if res then successBS else failureBS
deletePreviousResults :: PerformBuild -> PackageIdentifier -> IO ()
deletePreviousResults pb name =
forM_ [minBound..maxBound] $ \rt ->
withPRPath pb rt name $ \fp ->
void $ tryIO' $ removeFile $ fromString fp
getHaddockFiles :: PerformBuild -> IO (Map Text FilePath)
getHaddockFiles pb =
runResourceT
$ sourceDirectory (pbDocDir pb)
$$ foldMapMC (liftIO . go)
where
go :: FilePath -> IO (Map Text FilePath)
go dir =
case simpleParse nameVerText of
Nothing -> return mempty
Just (PackageIdentifier (unPackageName -> name) _) -> do
let fp = dir </> name <.> "haddock"
exists <- doesFileExist fp
return $ if exists
then singletonMap nameVerText fp
else mempty
where
nameVerText = pack $ FP.takeFileName dir
getHaddockDeps :: BuildPlan
-> TVar (Map PackageName (Set PackageName))
-> PackageName
-> STM (Set PackageName)
getHaddockDeps BuildPlan {..} var =
go
where
go :: PackageName -> STM (Set PackageName)
go name = do
m <- readTVar var
case lookup name m of
Just res -> return res
Nothing -> do
modifyTVar var $ insertMap name mempty
res' <- fmap fold $ mapM go $ setToList deps
let res = deps ++ res'
modifyTVar var $ insertMap name res
return res
where
deps =
case lookup name bpPackages of
Nothing -> mempty
Just PackagePlan {..} ->
asSet
$ setFromList
$ map fst
$ filter (isLibExe . snd)
$ mapToList
$ sdPackages ppDesc
isLibExe DepInfo {..} =
CompLibrary `member` diComponents ||
CompExecutable `member` diComponents
sdistFilePath :: IsString filepath
=> FilePath
-> Text
-> Text
-> filepath
sdistFilePath stackDir name version = fromString
$ stackDir
</> "indices"
</> "Hackage"
</> "packages"
</> unpack name
</> unpack version
</> unpack (concat [name, "-", version, ".tar.gz"])
createSetupHs :: FilePath
-> Text
-> Bool
-> IO GenericPackageDescription
createSetupHs dir name allowNewer = do
bs <- readFile cabalFP
gpd' <- gpdFromLBS cabalFP (fromStrict bs)
gpd <-
if allowNewer
then do
let gpd = stripVersionBounds gpd'
writeGenericPackageDescription cabalFP gpd
return gpd
else return gpd'
let simple = buildType (packageDescription gpd) == Just Simple
when simple $ do
_ <- tryIO' $ removeFile $ fromString setuplhs
writeFile setuphs $ asByteString "import Distribution.Simple\nmain = defaultMain\n"
return gpd
where
cabalFP = dir </> unpack name <.> "cabal"
setuphs = dir </> "Setup.hs"
setuplhs = dir </> "Setup.lhs"
stripVersionBounds :: GenericPackageDescription -> GenericPackageDescription
stripVersionBounds = everywhere $ mkT $ \(Dependency name _) -> Dependency name anyVersion
tryIO' :: IO a -> IO (Either IOException a)
tryIO' = try
catchIO' :: IO a -> (IOException -> IO a) -> IO a
catchIO' = catch
data BuildState = BSFullBuild | BSPartialBuild | NoBuild
deriving Show
calculatePackageMap :: PerformBuild
-> Map PackageName Version
-> (PackageName -> ResultType -> Maybe (Version, PrevResult))
-> Map PackageName PackageInfo
-> IO (Map PackageName PackageInfo)
calculatePackageMap pb registered prevRes allInfos =
loop initBuildStates
where
initBuildStates :: Map PackageName BuildState
initBuildStates =
foldMap go $ mapToList allInfos
where
go :: (PackageName, PackageInfo) -> Map PackageName BuildState
go (name, info)
| pcSkipBuild (ppConstraints plan) = singletonMap name NoBuild
| isLib && lookup name registered /= Just version = singletonMap name BSFullBuild
| Just (prevVer, prevRes') <- prevRes name Build
, prevVer == version && prevRes' == PRSuccess = mempty
| otherwise = singletonMap name BSFullBuild
where
isLib = not $ null $ sdModules $ ppDesc plan
version = ppVersion plan
plan = piPlan info
loop buildStates0 = do
buildStates1 <- foldM step' buildStates0 (mapToList allInfos)
when False $ putStrLn $ concat
[ "Debugging: added "
, tshow $ length buildStates1 length buildStates0
, " keys, new keys == "
, tshow (map display $ keys $ buildStates1 `Map.difference` buildStates0)
]
case (keys $ buildStates1 `difference` buildStates0, keys $ allInfos `Map.difference` buildStates1) of
(_:_, []) -> processBuildStates buildStates1
(_:_, _:_) -> loop buildStates1
([], []) -> processBuildStates buildStates1
([], noBuildState) -> error $ "calculatePackageMap: No change in build states, but haven't solved all packages: " ++ show (map display noBuildState)
where
step' buildStates (name, info) = do
res <- step buildStates name info
return $
case res of
Nothing -> buildStates
Just bs -> insertMap name bs buildStates
step :: Map PackageName BuildState -> PackageName -> PackageInfo -> IO (Maybe BuildState)
step states name pi =
go $ keys $ Map.filter ((CompLibrary `member`) . diComponents) $ sdPackages desc
where
plan = piPlan pi
desc = ppDesc plan
go (dep:deps) | dep == name = go deps
go (dep:deps) =
case lookup dep states of
Nothing
| dep `member` allInfos -> do
when False $ putStrLn $ concat
[ display name
, ": don't know state of dep: "
, tshow dep
]
return Nothing
| otherwise -> go deps
Just BSFullBuild -> do
putStrLn $ concat
[ "Rebuilding "
, display name
, " due to dependency "
, display dep
]
return $ Just BSFullBuild
Just BSPartialBuild -> go deps
Just NoBuild -> go deps
go [] = return $ Just BSPartialBuild
processBuildStates :: Map PackageName BuildState -> IO (Map PackageName PackageInfo)
processBuildStates buildStates =
fmap fold $ mapM go $ mapToList allInfos
where
go :: (PackageName, PackageInfo) -> IO (Map PackageName PackageInfo)
go (name, info) =
case lookup name buildStates of
Nothing -> error $ "processBuildStates: name not found " ++ show name
Just BSFullBuild -> do
putStrLn $ "Removing all previous results for " ++ display name
forM_ [minBound..maxBound] $ \rt -> removePreviousResults pb rt name
return $ singletonMap name info
Just BSPartialBuild -> return $ singletonMap name info
Just NoBuild -> return mempty