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,
23import Filesystem.Path (parent)
24import qualified Filesystem.Path as F
25import Stackage.BuildConstraints
26import Stackage.BuildPlan
27import Stackage.PackageDescription
28import Stackage.Prelude hiding (pi)
29import System.Directory (findExecutable)
30import System.Environment (getEnvironment)
31import System.IO (IOMode (WriteMode),
33import System.IO.Temp (withSystemTempDirectory)
35data BuildException = BuildException (Map PackageName BuildFailure) [Text]
37instance Exception BuildException
38instance Show BuildException where
39 show (BuildException m warnings) =
40 unlines $ map go (mapToList m) ++ map unpack warnings
42 go (PackageName name, bf) = concat
48data BuildFailure = DependencyFailed PackageName
49 | DependencyMissing PackageName
52 | BuildFailureException SomeException
53 deriving (Show, Typeable)
54instance Exception BuildFailure
56data PerformBuild = PerformBuild
58 , pbInstallDest :: FilePath
59 , pbLog :: ByteString -> IO ()
60 , pbLogDir :: FilePath
62 , pbGlobalInstall :: Bool
63 -- ^ Register packages in the global database
64 , pbEnableTests :: Bool
65 , pbEnableLibProfiling :: Bool
68data PackageInfo = PackageInfo
69 { piPlan :: PackagePlan
70 , piName :: PackageName
71 , piResult :: TMVar Bool
74waitForDeps :: Map ExeName (Set PackageName)
75 -> Map PackageName PackageInfo
81waitForDeps toolMap packageMap activeComps bp pi action = do
83 mapM_ checkPackage $ Map.keys $ filterUnused $ sdPackages $ ppDesc $ piPlan pi
84 forM_ (Map.keys $ filterUnused $ sdTools $ ppDesc $ piPlan pi) $ \exe -> do
85 case lookup exe toolMap >>= fromNullable . map checkPackage . setToList of
87 | isCoreExe exe -> return ()
88 | otherwise -> throwSTM $ ToolMissing exe
89 Just packages -> ofoldl1' (<|>) packages
92 filterUnused :: Ord key => Map key DepInfo -> Map key DepInfo
94 mapFromList . filter (go . snd) . mapToList
96 go = not . null . intersection activeComps . diComponents
98 checkPackage package | package == piName pi = return ()
99 checkPackage package =
100 case lookup package packageMap of
102 | isCore package -> return ()
103 | otherwise -> throwSTM $ DependencyMissing package
105 res <- readTMVar $ piResult dep
106 unless res $ throwSTM $ DependencyFailed package
108 isCore = (`member` siCorePackages (bpSystemInfo bp))
109 isCoreExe = (`member` siCoreExecutables (bpSystemInfo bp))
111withCounter :: TVar Int -> IO a -> IO a
112withCounter counter = bracket_
113 (atomically $ modifyTVar counter (+ 1))
114 (atomically $ modifyTVar counter (subtract 1))
116withTSem :: TSem -> IO a -> IO a
117withTSem sem = bracket_ (atomically $ waitTSem sem) (atomically $ signalTSem sem)
119-- | Returns @Nothing@ if installing to a global database
120pbDatabase :: PerformBuild -> Maybe FilePath
122 | pbGlobalInstall pb = Nothing
123 | otherwise = Just $ pbInstallDest pb </> "pkgdb"
125pbBinDir, pbLibDir, pbDataDir, pbDocDir :: PerformBuild -> FilePath
126pbBinDir pb = pbInstallDest pb </> "bin"
127pbLibDir pb = pbInstallDest pb </> "lib"
128pbDataDir pb = pbInstallDest pb </> "share"
129pbDocDir pb = pbInstallDest pb </> "doc"
131performBuild :: PerformBuild -> IO [Text]
133 cwd <- getWorkingDirectory
135 { pbInstallDest = cwd </> pbInstallDest pb
136 , pbLogDir = cwd </> pbLogDir pb
139performBuild' :: PerformBuild -> IO [Text]
140performBuild' pb@PerformBuild {..} = withBuildDir $ \builddir -> do
141 -- First make sure to fetch all of the dependencies... just in case Hackage
142 -- has an outage. Don't feel like wasting hours of CPU time.
143 pbLog $ encodeUtf8 "Pre-fetching all packages\n"
144 let toDownload = flip map (mapToList $ bpPackages pbPlan)
145 $ \(name, plan) -> unpack $ concat
148 , display $ ppVersion plan
153 : "--no-dependencies"
155 $ \ClosedStream Inherited Inherited -> return ()
157 let removeTree' fp = whenM (isDirectory fp) (removeTree fp)
158 mapM_ removeTree' [pbInstallDest, pbLogDir]
160 forM_ (pbDatabase pb) $ \db -> do
161 createTree $ parent db
162 withCheckedProcess (proc "ghc-pkg" ["init", fpToString db])
163 $ \ClosedStream Inherited Inherited -> return ()
164 pbLog $ encodeUtf8 "Copying built-in Haddocks\n"
165 copyBuiltInHaddocks (pbDocDir pb)
167 sem <- atomically $ newTSem pbJobs
168 active <- newTVarIO (0 :: Int)
169 let toolMap = makeToolMap $ bpPackages pbPlan
170 packageMap <- fmap fold $ forM (mapToList $ bpPackages pbPlan)
171 $ \(name, plan) -> do
174 piResult <- newEmptyTMVarIO
175 return $ singletonMap name PackageInfo {..}
177 errsVar <- newTVarIO mempty
178 warningsVar <- newTVarIO id
180 env <- getEnvironment
181 haddockFiles <- newTVarIO mempty
183 forM_ packageMap $ \pi -> void $ async $ singleBuild pb SingleBuild
185 , sbErrsVar = errsVar
186 , sbWarningsVar = warningsVar
188 , sbToolMap = toolMap
189 , sbPackageMap = packageMap
190 , sbBuildDir = builddir
192 , sbRegisterMutex = mutex
193 , sbModifiedEnv = maybe
195 (\db -> (("HASKELL_PACKAGE_SANDBOX", fpToString db):))
198 , sbHaddockFiles = haddockFiles
201 void $ tryAny $ atomically $ readTVar active >>= checkSTM . (== 0)
203 warnings <- ($ []) <$> readTVarIO warningsVar
204 errs <- readTVarIO errsVar
205 when (not $ null errs) $ throwM $ BuildException errs warnings
208 withBuildDir f = withSystemTempDirectory "stackage-build" (f . fpFromString)
211 -- Thank you Windows having case-insensitive environment variables...
212 | toUpper p == "PATH" = (p, fpToString (pbBinDir pb) ++ pathSep : x)
215 -- | Separate for the PATH environment variable
217#ifdef mingw32_HOST_OS
223data SingleBuild = SingleBuild
225 , sbErrsVar :: TVar (Map PackageName BuildFailure)
226 , sbWarningsVar :: TVar ([Text] -> [Text])
227 , sbActive :: TVar Int
228 , sbToolMap :: Map ExeName (Set PackageName)
229 , sbPackageMap :: Map PackageName PackageInfo
230 , sbBuildDir :: FilePath
231 , sbPackageInfo :: PackageInfo
232 , sbRegisterMutex :: MVar ()
233 , sbModifiedEnv :: [(String, String)]
234 , sbHaddockFiles :: TVar (Map Text FilePath) -- ^ package-version, .haddock file
237singleBuild :: PerformBuild -> SingleBuild -> IO ()
238singleBuild pb@PerformBuild {..} SingleBuild {..} =
241 $ (`finally` void (atomically $ tryPutTMVar (piResult sbPackageInfo) False))
244 libComps = setFromList [CompLibrary, CompExecutable]
245 testComps = insertSet CompTestSuite libComps
248 waitForDeps sbToolMap sbPackageMap comps pbPlan sbPackageInfo
250 wfd libComps buildLibrary
252 wfd testComps runTests
254 name = display $ piName sbPackageInfo
258 , display $ ppVersion $ piPlan sbPackageInfo
261 runIn wdir outH cmd args =
262 withCheckedProcess cp $ \ClosedStream UseProvidedHandle UseProvidedHandle ->
265 cp = (proc (unpack $ asText cmd) (map (unpack . asText) args))
266 { cwd = Just $ fpToString wdir
267 , std_out = UseHandle outH
268 , std_err = UseHandle outH
269 , env = Just sbModifiedEnv
271 runParent = runIn sbBuildDir
272 runChild = runIn childDir
273 childDir = sbBuildDir </> fpFromText namever
276 i <- readTVarIO sbActive
277 errs <- readTVarIO sbErrsVar
278 pbLog $ encodeUtf8 $ concat
283 , tshow $ length errs
286 libOut = pbLogDir </> fpFromText namever </> "build.out"
287 testOut = pbLogDir </> fpFromText namever </> "test.out"
288 testRunOut = pbLogDir </> fpFromText namever </> "test-run.out"
291 createTree $ parent fp
292 withBinaryFile (fpToString fp) WriteMode inner'
294 configArgs = ($ []) $ execWriter $ do
295 tell' "--package-db=clear"
296 tell' "--package-db=global"
297 forM_ (pbDatabase pb) $ \db -> tell' $ "--package-db=" ++ fpToText db
298 tell' $ "--libdir=" ++ fpToText (pbLibDir pb)
299 tell' $ "--bindir=" ++ fpToText (pbBinDir pb)
300 tell' $ "--datadir=" ++ fpToText (pbDataDir pb)
301 tell' $ "--docdir=" ++ fpToText (pbDocDir pb)
302 tell' $ "--flags=" ++ flags
303 when (pbEnableLibProfiling && pcEnableLibProfile) $
304 tell' "--enable-library-profiling"
309 flags = unwords $ map go $ mapToList pcFlagOverrides
311 go (name', isOn) = concat
312 [ if isOn then "" else "-"
316 PackageConstraints {..} = ppConstraints $ piPlan sbPackageInfo
318 buildLibrary = wf libOut $ \outH -> do
319 let run = runChild outH
320 log' $ "Unpacking " ++ namever
321 runParent outH "cabal" ["unpack", namever]
323 log' $ "Configuring " ++ namever
324 run "cabal" $ "configure" : configArgs
326 log' $ "Building " ++ namever
327 run "cabal" ["build"]
329 log' $ "Copying/registering " ++ namever
331 withMVar sbRegisterMutex $ const $
332 run "cabal" ["register"]
334 -- Even if the tests later fail, we can allow other libraries to build
335 -- on top of our successful results
337 -- FIXME do we need to wait to do this until after Haddocks build?
338 -- otherwise, we could have a race condition and try to build a
339 -- dependency's haddocks before this finishes
340 atomically $ putTMVar (piResult sbPackageInfo) True
342 when (pcHaddocks /= Don'tBuild && not (null $ sdModules $ ppDesc $ piPlan sbPackageInfo)) $ do
343 log' $ "Haddocks " ++ namever
344 hfs <- readTVarIO sbHaddockFiles
345 let hfsOpts = flip map (mapToList hfs) $ \(pkgVer, hf) -> concat
346 [ "--haddock-options=--read-interface="
353 : "--hyperlink-source"
356 : "--html-location=../$pkg-$version/"
359 eres <- tryAny $ run "cabal" args
361 forM_ eres $ \() -> do
363 (childDir </> "dist" </> "doc" </> "html" </> fpFromText name)
364 (pbDocDir pb </> fpFromText namever)
369 </> fpFromText namever
370 </> fpFromText name <.> "haddock"
372 Left e -> warn $ tshow e
373 Right newPath -> atomically
374 $ modifyTVar sbHaddockFiles
375 $ insertMap namever newPath
377 case (eres, pcHaddocks) of
378 (Left e, ExpectSuccess) -> throwM e
379 (Right (), ExpectFailure) -> warn $ namever ++ ": unexpected Haddock success"
382 runTests = wf testOut $ \outH -> do
383 let run = runChild outH
385 when (pbEnableTests && pcTests /= Don'tBuild) $ do
386 log' $ "Test configure " ++ namever
387 run "cabal" $ "configure" : "--enable-tests" : configArgs
390 log' $ "Test build " ++ namever
391 run "cabal" ["build"]
393 log' $ "Test run " ++ namever
394 run "cabal" ["test", "--log=" ++ fpToText testRunOut]
396 case (eres, pcTests) of
397 (Left e, ExpectSuccess) -> throwM e
398 (Right (), ExpectFailure) -> warn $ namever ++ ": unexpected test success"
401 warn t = atomically $ modifyTVar sbWarningsVar (. (t:))
405 [ display (piName sbPackageInfo)
409 atomically $ modifyTVar sbErrsVar $ insertMap (piName sbPackageInfo) exc'
412 case fromException exc of
414 Nothing -> BuildFailureException exc
416renameOrCopy :: FilePath -> FilePath -> IO ()
417renameOrCopy src dest = rename src dest `catchIO` \_ -> copyDir src dest
419copyDir :: FilePath -> FilePath -> IO ()
421 runResourceT $ sourceDirectoryDeep False src $$ mapM_C go
424 go fp = forM_ (F.stripPrefix src' fp) $ \suffix -> do
425 let dest' = dest </> suffix
426 liftIO $ createTree $ parent dest'
427 sourceFile fp $$ (sinkFile dest' :: Sink ByteString (ResourceT IO) ())
429copyBuiltInHaddocks :: FilePath -> IO ()
430copyBuiltInHaddocks docdir = do
431 mghc <- findExecutable "ghc"
433 Nothing -> error "GHC not found on PATH"
435 src <- canonicalizePath
436 (parent (fpFromString ghc) </> "../share/doc/ghc/html/libraries")
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