1-- | Perform an actual build, generate a binary package database and a
2-- documentation directory in the process.
4{-# LANGUAGE DeriveDataTypeable #-}
5{-# LANGUAGE NoImplicitPrelude #-}
6{-# LANGUAGE OverloadedStrings #-}
7{-# LANGUAGE RecordWildCards #-}
8module Stackage.PerformBuild
15import Control.Concurrent.Async (async)
16import Control.Concurrent.STM.TSem
17import Control.Monad.Writer.Strict (execWriter, tell)
18import qualified Data.Map as Map
19import Data.NonNull (fromNullable)
20import Filesystem (canonicalizePath, createTree,
21 getWorkingDirectory, isDirectory,
22 removeTree, rename, isFile, removeFile)
23import Filesystem.Path (parent)
24import qualified Filesystem.Path as F
25import Stackage.BuildConstraints
26import Stackage.BuildPlan
28import Stackage.PackageDescription
29import Stackage.Prelude hiding (pi)
30import System.Directory (findExecutable)
31import System.Environment (getEnvironment)
32import System.IO (IOMode (WriteMode),
34import System.IO.Temp (withSystemTempDirectory)
36data BuildException = BuildException (Map PackageName BuildFailure) [Text]
38instance Exception BuildException
39instance Show BuildException where
40 show (BuildException m warnings) =
41 unlines $ map go (mapToList m) ++ map unpack warnings
43 go (PackageName name, bf) = concat
49data BuildFailure = DependencyFailed PackageName
50 | DependencyMissing PackageName
53 | BuildFailureException SomeException
54 deriving (Show, Typeable)
55instance Exception BuildFailure
57data PerformBuild = PerformBuild
59 , pbInstallDest :: FilePath
60 , pbLog :: ByteString -> IO ()
61 , pbLogDir :: FilePath
63 , pbGlobalInstall :: Bool
64 -- ^ Register packages in the global database
65 , pbEnableTests :: Bool
66 , pbEnableHaddock :: Bool
67 , pbEnableLibProfiling :: Bool
68 , pbEnableExecDyn :: Bool
70 , pbAllowNewer :: Bool
71 -- ^ Pass --allow-newer to cabal configure
74data PackageInfo = PackageInfo
75 { piPlan :: PackagePlan
76 , piName :: PackageName
77 , piResult :: TMVar Bool
80waitForDeps :: Map ExeName (Set PackageName)
81 -> Map PackageName PackageInfo
87waitForDeps toolMap packageMap activeComps bp pi action = do
89 mapM_ checkPackage $ Map.keys $ filterUnused $ sdPackages $ ppDesc $ piPlan pi
90 forM_ (Map.keys $ filterUnused $ sdTools $ ppDesc $ piPlan pi) $ \exe -> do
91 case lookup exe toolMap >>= fromNullable . map checkPackage . setToList of
93 | isCoreExe exe -> return ()
94 -- https://github.com/jgm/zip-archive/issues/23
95 -- | otherwise -> throwSTM $ ToolMissing exe
96 | otherwise -> return ()
97 Just packages -> ofoldl1' (<|>) packages
100 filterUnused :: Ord key => Map key DepInfo -> Map key DepInfo
102 mapFromList . filter (go . snd) . mapToList
104 go = not . null . intersection activeComps . diComponents
106 checkPackage package | package == piName pi = return ()
107 checkPackage package =
108 case lookup package packageMap of
110 | isCore package -> return ()
111 | otherwise -> throwSTM $ DependencyMissing package
113 res <- readTMVar $ piResult dep
114 unless res $ throwSTM $ DependencyFailed package
116 isCore = (`member` siCorePackages (bpSystemInfo bp))
117 isCoreExe = (`member` siCoreExecutables (bpSystemInfo bp))
119withCounter :: TVar Int -> IO a -> IO a
120withCounter counter = bracket_
121 (atomically $ modifyTVar counter (+ 1))
122 (atomically $ modifyTVar counter (subtract 1))
124withTSem :: TSem -> IO a -> IO a
125withTSem sem = bracket_ (atomically $ waitTSem sem) (atomically $ signalTSem sem)
127-- | Returns @Nothing@ if installing to a global database
128pbDatabase :: PerformBuild -> Maybe FilePath
130 | pbGlobalInstall pb = Nothing
131 | otherwise = Just $ pbInstallDest pb </> "pkgdb"
133pbBinDir, pbLibDir, pbDataDir, pbDocDir :: PerformBuild -> FilePath
134pbBinDir pb = pbInstallDest pb </> "bin"
135pbLibDir pb = pbInstallDest pb </> "lib"
136pbDataDir pb = pbInstallDest pb </> "share"
137pbDocDir pb = pbInstallDest pb </> "doc"
139-- | Directory keeping previous result info
140pbPrevResDir :: PerformBuild -> FilePath
141pbPrevResDir pb = pbInstallDest pb </> "prevres"
143performBuild :: PerformBuild -> IO [Text]
145 cwd <- getWorkingDirectory
147 { pbInstallDest = cwd </> pbInstallDest pb
148 , pbLogDir = cwd </> pbLogDir pb
151performBuild' :: PerformBuild -> IO [Text]
152performBuild' pb@PerformBuild {..} = withBuildDir $ \builddir -> do
153 -- First make sure to fetch all of the dependencies... just in case Hackage
154 -- has an outage. Don't feel like wasting hours of CPU time.
155 pbLog $ encodeUtf8 "Pre-fetching all packages\n"
156 let toDownload = flip map (mapToList $ bpPackages pbPlan)
157 $ \(name, plan) -> unpack $ concat
160 , display $ ppVersion plan
165 : "--no-dependencies"
167 $ \ClosedStream Inherited Inherited -> return ()
169 let removeTree' fp = whenM (isDirectory fp) (removeTree fp)
172 forM_ (pbDatabase pb) $ \db ->
173 unlessM (isFile $ db </> "package.cache") $ do
174 createTree $ parent db
175 withCheckedProcess (proc "ghc-pkg" ["init", fpToString db])
176 $ \ClosedStream Inherited Inherited -> return ()
177 pbLog $ encodeUtf8 "Copying built-in Haddocks\n"
178 copyBuiltInHaddocks (pbDocDir pb)
180 sem <- atomically $ newTSem pbJobs
181 active <- newTVarIO (0 :: Int)
182 let toolMap = makeToolMap $ bpPackages pbPlan
183 packageMap <- fmap fold $ forM (mapToList $ bpPackages pbPlan)
184 $ \(name, plan) -> do
187 piResult <- newEmptyTMVarIO
188 return $ singletonMap name PackageInfo {..}
190 errsVar <- newTVarIO mempty
191 warningsVar <- newTVarIO id
193 env <- getEnvironment
194 haddockFiles <- newTVarIO mempty
196 registeredPackages <- setupPackageDatabase
200 (ppVersion <$> bpPackages pbPlan)
201 (deletePreviousResults pb)
203 forM_ packageMap $ \pi -> void $ async $ singleBuild pb registeredPackages
206 , sbErrsVar = errsVar
207 , sbWarningsVar = warningsVar
209 , sbToolMap = toolMap
210 , sbPackageMap = packageMap
211 , sbBuildDir = builddir
213 , sbRegisterMutex = mutex
214 , sbModifiedEnv = maybe
216 (\db -> (("HASKELL_PACKAGE_SANDBOX", fpToString db):))
218 (filter allowedEnv $ map fixEnv env)
219 , sbHaddockFiles = haddockFiles
222 void $ tryAny $ atomically $ readTVar active >>= checkSTM . (== 0)
224 warnings <- ($ []) <$> readTVarIO warningsVar
225 errs <- readTVarIO errsVar
226 when (not $ null errs) $ throwM $ BuildException errs warnings
229 withBuildDir f = withSystemTempDirectory "stackage-build" (f . fpFromString)
232 -- Thank you Windows having case-insensitive environment variables...
233 | toUpper p == "PATH" = (p, fpToString (pbBinDir pb) ++ pathSep : x)
236 allowedEnv (k, _) = k `notMember` bannedEnvs
238 -- | Separate for the PATH environment variable
240#ifdef mingw32_HOST_OS
246-- | Environment variables we don't allow to be passed on to child processes.
247bannedEnvs :: Set String
248bannedEnvs = setFromList
249 [ "STACKAGE_AUTH_TOKEN"
252data SingleBuild = SingleBuild
254 , sbErrsVar :: TVar (Map PackageName BuildFailure)
255 , sbWarningsVar :: TVar ([Text] -> [Text])
256 , sbActive :: TVar Int
257 , sbToolMap :: Map ExeName (Set PackageName)
258 , sbPackageMap :: Map PackageName PackageInfo
259 , sbBuildDir :: FilePath
260 , sbPackageInfo :: PackageInfo
261 , sbRegisterMutex :: MVar ()
262 , sbModifiedEnv :: [(String, String)]
263 , sbHaddockFiles :: TVar (Map Text FilePath) -- ^ package-version, .haddock file
266singleBuild :: PerformBuild
267 -> Set PackageName -- ^ registered packages
268 -> SingleBuild -> IO ()
269singleBuild pb@PerformBuild {..} registeredPackages SingleBuild {..} =
272 $ (`finally` void (atomically $ tryPutTMVar (piResult sbPackageInfo) False))
275 libComps = setFromList [CompLibrary, CompExecutable]
276 testComps = insertSet CompTestSuite libComps
279 waitForDeps sbToolMap sbPackageMap comps pbPlan sbPackageInfo
281 withUnpacked <- wfd libComps buildLibrary
283 wfd testComps (runTests withUnpacked)
285 pname = piName sbPackageInfo
286 pident = PackageIdentifier pname (ppVersion $ piPlan sbPackageInfo)
291 , display $ ppVersion $ piPlan sbPackageInfo
294 runIn wdir getOutH cmd args = do
296 withCheckedProcess (cp outH) $ \ClosedStream UseProvidedHandle UseProvidedHandle ->
299 cp outH = (proc (unpack $ asText cmd) (map (unpack . asText) args))
300 { cwd = Just $ fpToString wdir
301 , std_out = UseHandle outH
302 , std_err = UseHandle outH
303 , env = Just sbModifiedEnv
305 runParent = runIn sbBuildDir
306 runChild = runIn childDir
307 childDir = sbBuildDir </> fpFromText namever
310 i <- readTVarIO sbActive
311 errs <- readTVarIO sbErrsVar
312 pbLog $ encodeUtf8 $ concat
317 , tshow $ length errs
320 libOut = pbLogDir </> fpFromText namever </> "build.out"
321 testOut = pbLogDir </> fpFromText namever </> "test.out"
322 testRunOut = pbLogDir </> fpFromText namever </> "test-run.out"
325 ref <- newIORef Nothing
333 Nothing -> mask_ $ do
334 createTree $ parent fp
335 h <- openBinaryFile (fpToString fp) WriteMode
336 writeIORef ref $ Just h
339 inner' getH `finally` cleanup
341 configArgs = ($ []) $ execWriter $ do
342 when pbAllowNewer $ tell' "--allow-newer"
343 tell' "--package-db=clear"
344 tell' "--package-db=global"
345 forM_ (pbDatabase pb) $ \db -> tell' $ "--package-db=" ++ fpToText db
346 tell' $ "--libdir=" ++ fpToText (pbLibDir pb)
347 tell' $ "--bindir=" ++ fpToText (pbBinDir pb)
348 tell' $ "--datadir=" ++ fpToText (pbDataDir pb)
349 tell' $ "--docdir=" ++ fpToText (pbDocDir pb)
350 tell' $ "--flags=" ++ flags
351 when (pbEnableLibProfiling && pcEnableLibProfile) $
352 tell' "--enable-library-profiling"
353 when pbEnableExecDyn $ tell' "--enable-executable-dynamic"
358 flags = unwords $ map go $ mapToList pcFlagOverrides
360 go (name', isOn) = concat
361 [ if isOn then "" else "-"
365 PackageConstraints {..} = ppConstraints $ piPlan sbPackageInfo
367 buildLibrary = wf libOut $ \getOutH -> do
368 let run a b = do when pbVerbose $ log' (unwords (a : b))
371 isUnpacked <- newIORef False
372 let withUnpacked inner = do
373 unlessM (readIORef isUnpacked) $ do
374 log' $ "Unpacking " ++ namever
375 runParent getOutH "cabal" ["unpack", namever]
376 writeIORef isUnpacked True
379 isConfiged <- newIORef False
380 let withConfiged inner = withUnpacked $ do
381 unlessM (readIORef isConfiged) $ do
382 log' $ "Configuring " ++ namever
383 run "cabal" $ "configure" : configArgs
384 writeIORef isConfiged True
387 prevBuildResult <- getPreviousResult pb Build pident
388 unless (prevBuildResult == PRSuccess) $ withConfiged $ do
389 assert (pname `notMember` registeredPackages) $ do
390 deletePreviousResults pb pident
392 log' $ "Building " ++ namever
393 run "cabal" ["build"]
395 log' $ "Copying/registering " ++ namever
397 withMVar sbRegisterMutex $ const $
398 run "cabal" ["register"]
400 savePreviousResult pb Build pident True
402 -- Even if the tests later fail, we can allow other libraries to build
403 -- on top of our successful results
405 -- FIXME do we need to wait to do this until after Haddocks build?
406 -- otherwise, we could have a race condition and try to build a
407 -- dependency's haddocks before this finishes
408 atomically $ putTMVar (piResult sbPackageInfo) True
410 prevHaddockResult <- getPreviousResult pb Haddock pident
411 let needHaddock = pbEnableHaddock
412 && checkPrevResult prevHaddockResult pcHaddocks
413 && not (null $ sdModules $ ppDesc $ piPlan sbPackageInfo)
414 when needHaddock $ withConfiged $ do
415 log' $ "Haddocks " ++ namever
416 hfs <- readTVarIO sbHaddockFiles
417 let hfsOpts = flip map (mapToList hfs) $ \(pkgVer, hf) -> concat
418 [ "--haddock-options=--read-interface="
425 : "--hyperlink-source"
428 : "--html-location=../$pkg-$version/"
431 eres <- tryAny $ run "cabal" args
433 forM_ eres $ \() -> do
435 (childDir </> "dist" </> "doc" </> "html" </> fpFromText name)
436 (pbDocDir pb </> fpFromText namever)
441 </> fpFromText namever
442 </> fpFromText name <.> "haddock"
444 Left e -> warn $ tshow e
445 Right newPath -> atomically
446 $ modifyTVar sbHaddockFiles
447 $ insertMap namever newPath
449 savePreviousResult pb Haddock pident $ either (const False) (const True) eres
450 case (eres, pcHaddocks) of
451 (Left e, ExpectSuccess) -> throwM e
452 (Right (), ExpectFailure) -> warn $ namever ++ ": unexpected Haddock success"
457 runTests withUnpacked = wf testOut $ \getOutH -> do
458 let run = runChild getOutH
460 prevTestResult <- getPreviousResult pb Test pident
461 let needTest = pbEnableTests
462 && checkPrevResult prevTestResult pcTests
463 when needTest $ withUnpacked $ do
464 log' $ "Test configure " ++ namever
465 run "cabal" $ "configure" : "--enable-tests" : configArgs
468 log' $ "Test build " ++ namever
469 run "cabal" ["build"]
471 log' $ "Test run " ++ namever
472 run "cabal" ["test", "--log=" ++ fpToText testRunOut]
474 savePreviousResult pb Test pident $ either (const False) (const True) eres
475 case (eres, pcTests) of
476 (Left e, ExpectSuccess) -> throwM e
477 (Right (), ExpectFailure) -> warn $ namever ++ ": unexpected test success"
480 warn t = atomically $ modifyTVar sbWarningsVar (. (t:))
484 [ display (piName sbPackageInfo)
488 atomically $ modifyTVar sbErrsVar $ insertMap (piName sbPackageInfo) exc'
491 case fromException exc of
493 Nothing -> BuildFailureException exc
495renameOrCopy :: FilePath -> FilePath -> IO ()
496renameOrCopy src dest = rename src dest `catchIO` \_ -> copyDir src dest
498copyDir :: FilePath -> FilePath -> IO ()
500 runResourceT $ sourceDirectoryDeep False src $$ mapM_C go
503 go fp = forM_ (F.stripPrefix src' fp) $ \suffix -> do
504 let dest' = dest </> suffix
505 liftIO $ createTree $ parent dest'
506 sourceFile fp $$ (sinkFile dest' :: Sink ByteString (ResourceT IO) ())
508copyBuiltInHaddocks :: FilePath -> IO ()
509copyBuiltInHaddocks docdir = do
510 mghc <- findExecutable "ghc"
512 Nothing -> error "GHC not found on PATH"
514 src <- canonicalizePath
515 (parent (fpFromString ghc) </> "../share/doc/ghc/html/libraries")
518------------- Previous results
520-- | The previous actions that can be run
521data ResultType = Build | Haddock | Test
522 deriving (Show, Enum, Eq, Ord, Bounded, Read)
524-- | The result generated on a previous run
525data PrevResult = PRNoResult | PRSuccess | PRFailure
526 deriving (Show, Enum, Eq, Ord, Bounded, Read)
528-- | Check if we should rerun based on a PrevResult and the expected status
529checkPrevResult :: PrevResult -> TestState -> Bool
530checkPrevResult _ Don'tBuild = False
531checkPrevResult PRNoResult _ = True
532checkPrevResult PRSuccess _ = False
533checkPrevResult PRFailure ExpectSuccess = True
534checkPrevResult PRFailure _ = False
536withPRPath :: PerformBuild -> ResultType -> PackageIdentifier -> (FilePath -> IO a) -> IO a
537withPRPath pb rt ident inner = do
538 createTree $ parent fp
541 fp = pbPrevResDir pb </> fpFromString (show rt) </> fpFromText (display ident)
543successBS, failureBS :: ByteString
547getPreviousResult :: PerformBuild -> ResultType -> PackageIdentifier -> IO PrevResult
548getPreviousResult w x y = withPRPath w x y $ \fp -> do
549 eres <- tryIO $ readFile fp
550 return $ case eres of
552 | bs == successBS -> PRSuccess
553 | bs == failureBS -> PRFailure
556savePreviousResult :: PerformBuild -> ResultType -> PackageIdentifier -> Bool -> IO ()
557savePreviousResult pb rt ident res =
558 withPRPath pb rt ident $ \fp -> writeFile fp $
559 if res then successBS else failureBS
561deletePreviousResults :: PerformBuild -> PackageIdentifier -> IO ()
562deletePreviousResults pb name =
563 forM_ [minBound..maxBound] $ \rt ->
564 withPRPath pb rt name $ \fp ->
565 void $ tryIO $ removeFile fp
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list