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
66data PackageInfo = PackageInfo
67 { piPlan :: PackagePlan
68 , piName :: PackageName
69 , piResult :: TMVar Bool
72waitForDeps :: Map ExeName (Set PackageName)
73 -> Map PackageName PackageInfo
79waitForDeps toolMap packageMap activeComps bp pi action = do
81 mapM_ checkPackage $ Map.keys $ filterUnused $ sdPackages $ ppDesc $ piPlan pi
82 forM_ (Map.keys $ filterUnused $ sdTools $ ppDesc $ piPlan pi) $ \exe -> do
83 case lookup exe toolMap >>= fromNullable . map checkPackage . setToList of
85 | isCoreExe exe -> return ()
86 | otherwise -> throwSTM $ ToolMissing exe
87 Just packages -> ofoldl1' (<|>) packages
90 filterUnused :: Ord key => Map key DepInfo -> Map key DepInfo
92 mapFromList . filter (go . snd) . mapToList
94 go = not . null . intersection activeComps . diComponents
96 checkPackage package | package == piName pi = return ()
97 checkPackage package =
98 case lookup package packageMap of
100 | isCore package -> return ()
101 | otherwise -> throwSTM $ DependencyMissing package
103 res <- readTMVar $ piResult dep
104 unless res $ throwSTM $ DependencyFailed package
106 isCore = (`member` siCorePackages (bpSystemInfo bp))
107 isCoreExe = (`member` siCoreExecutables (bpSystemInfo bp))
109withCounter :: TVar Int -> IO a -> IO a
110withCounter counter = bracket_
111 (atomically $ modifyTVar counter (+ 1))
112 (atomically $ modifyTVar counter (subtract 1))
114withTSem :: TSem -> IO a -> IO a
115withTSem sem = bracket_ (atomically $ waitTSem sem) (atomically $ signalTSem sem)
117-- | Returns @Nothing@ if installing to a global database
118pbDatabase :: PerformBuild -> Maybe FilePath
120 | pbGlobalInstall pb = Nothing
121 | otherwise = Just $ pbInstallDest pb </> "pkgdb"
123pbBinDir, pbLibDir, pbDataDir, pbDocDir :: PerformBuild -> FilePath
124pbBinDir pb = pbInstallDest pb </> "bin"
125pbLibDir pb = pbInstallDest pb </> "lib"
126pbDataDir pb = pbInstallDest pb </> "share"
127pbDocDir pb = pbInstallDest pb </> "doc"
129performBuild :: PerformBuild -> IO [Text]
131 cwd <- getWorkingDirectory
133 { pbInstallDest = cwd </> pbInstallDest pb
134 , pbLogDir = cwd </> pbLogDir pb
137performBuild' :: PerformBuild -> IO [Text]
138performBuild' pb@PerformBuild {..} = withBuildDir $ \builddir -> do
139 let removeTree' fp = whenM (isDirectory fp) (removeTree fp)
140 mapM_ removeTree' [pbInstallDest, pbLogDir]
142 forM_ (pbDatabase pb) $ \db -> do
143 createTree $ parent db
144 withCheckedProcess (proc "ghc-pkg" ["init", fpToString db])
145 $ \ClosedStream Inherited Inherited -> return ()
146 pbLog $ encodeUtf8 "Copying built-in Haddocks\n"
147 copyBuiltInHaddocks (pbDocDir pb)
149 sem <- atomically $ newTSem pbJobs
150 active <- newTVarIO (0 :: Int)
151 let toolMap = makeToolMap $ bpPackages pbPlan
152 packageMap <- fmap fold $ forM (mapToList $ bpPackages pbPlan)
153 $ \(name, plan) -> do
156 piResult <- newEmptyTMVarIO
157 return $ singletonMap name PackageInfo {..}
159 errsVar <- newTVarIO mempty
160 warningsVar <- newTVarIO id
162 env <- getEnvironment
163 haddockFiles <- newTVarIO mempty
165 forM_ packageMap $ \pi -> void $ async $ singleBuild pb SingleBuild
167 , sbErrsVar = errsVar
168 , sbWarningsVar = warningsVar
170 , sbToolMap = toolMap
171 , sbPackageMap = packageMap
172 , sbBuildDir = builddir
174 , sbRegisterMutex = mutex
175 , sbModifiedEnv = maybe
177 (\db -> (("HASKELL_PACKAGE_SANDBOX", fpToString db):))
180 , sbHaddockFiles = haddockFiles
183 void $ tryAny $ atomically $ readTVar active >>= checkSTM . (== 0)
185 warnings <- ($ []) <$> readTVarIO warningsVar
186 errs <- readTVarIO errsVar
187 when (not $ null errs) $ throwM $ BuildException errs warnings
190 withBuildDir f = withSystemTempDirectory "stackage-build" (f . fpFromString)
193 -- Thank you Windows having case-insensitive environment variables...
194 | toUpper p == "PATH" = (p, fpToString (pbBinDir pb) ++ pathSep : x)
197 -- | Separate for the PATH environment variable
199#ifdef mingw32_HOST_OS
205data SingleBuild = SingleBuild
207 , sbErrsVar :: TVar (Map PackageName BuildFailure)
208 , sbWarningsVar :: TVar ([Text] -> [Text])
209 , sbActive :: TVar Int
210 , sbToolMap :: Map ExeName (Set PackageName)
211 , sbPackageMap :: Map PackageName PackageInfo
212 , sbBuildDir :: FilePath
213 , sbPackageInfo :: PackageInfo
214 , sbRegisterMutex :: MVar ()
215 , sbModifiedEnv :: [(String, String)]
216 , sbHaddockFiles :: TVar (Map Text FilePath) -- ^ package-version, .haddock file
219singleBuild :: PerformBuild -> SingleBuild -> IO ()
220singleBuild pb@PerformBuild {..} SingleBuild {..} =
223 $ (`finally` void (atomically $ tryPutTMVar (piResult sbPackageInfo) False))
226 libComps = setFromList [CompLibrary, CompExecutable]
227 testComps = insertSet CompTestSuite libComps
230 waitForDeps sbToolMap sbPackageMap comps pbPlan sbPackageInfo
232 wfd libComps buildLibrary
234 wfd testComps runTests
236 name = display $ piName sbPackageInfo
240 , display $ ppVersion $ piPlan sbPackageInfo
243 runIn wdir outH cmd args =
244 withCheckedProcess cp $ \ClosedStream UseProvidedHandle UseProvidedHandle ->
247 cp = (proc (unpack $ asText cmd) (map (unpack . asText) args))
248 { cwd = Just $ fpToString wdir
249 , std_out = UseHandle outH
250 , std_err = UseHandle outH
251 , env = Just sbModifiedEnv
253 runParent = runIn sbBuildDir
254 runChild = runIn childDir
255 childDir = sbBuildDir </> fpFromText namever
258 i <- readTVarIO sbActive
259 errs <- readTVarIO sbErrsVar
260 pbLog $ encodeUtf8 $ concat
265 , tshow $ length errs
268 libOut = pbLogDir </> fpFromText namever </> "build.out"
269 testOut = pbLogDir </> fpFromText namever </> "test.out"
270 testRunOut = pbLogDir </> fpFromText namever </> "test-run.out"
273 createTree $ parent fp
274 withBinaryFile (fpToString fp) WriteMode inner'
276 configArgs = ($ []) $ execWriter $ do
277 tell' "--package-db=clear"
278 tell' "--package-db=global"
279 forM_ (pbDatabase pb) $ \db -> tell' $ "--package-db=" ++ fpToText db
280 tell' $ "--libdir=" ++ fpToText (pbLibDir pb)
281 tell' $ "--bindir=" ++ fpToText (pbBinDir pb)
282 tell' $ "--datadir=" ++ fpToText (pbDataDir pb)
283 tell' $ "--docdir=" ++ fpToText (pbDocDir pb)
284 tell' $ "--flags=" ++ flags
289 flags = unwords $ map go $ mapToList pcFlagOverrides
291 go (name', isOn) = concat
292 [ if isOn then "" else "-"
296 PackageConstraints {..} = ppConstraints $ piPlan sbPackageInfo
298 buildLibrary = wf libOut $ \outH -> do
299 let run = runChild outH
300 log' $ "Unpacking " ++ namever
301 runParent outH "cabal" ["unpack", namever]
303 log' $ "Configuring " ++ namever
304 run "cabal" $ "configure" : configArgs
306 log' $ "Building " ++ namever
307 run "cabal" ["build"]
309 log' $ "Copying/registering " ++ namever
311 withMVar sbRegisterMutex $ const $
312 run "cabal" ["register"]
314 -- Even if the tests later fail, we can allow other libraries to build
315 -- on top of our successful results
317 -- FIXME do we need to wait to do this until after Haddocks build?
318 -- otherwise, we could have a race condition and try to build a
319 -- dependency's haddocks before this finishes
320 atomically $ putTMVar (piResult sbPackageInfo) True
322 when (pcHaddocks /= Don'tBuild && not (null $ sdModules $ ppDesc $ piPlan sbPackageInfo)) $ do
323 log' $ "Haddocks " ++ namever
324 hfs <- readTVarIO sbHaddockFiles
325 let hfsOpts = flip map (mapToList hfs) $ \(pkgVer, hf) -> concat
326 [ "--haddock-options=--read-interface="
333 : "--hyperlink-source"
336 : "--html-location=../$pkg-$version/"
339 eres <- tryAny $ run "cabal" args
341 forM_ eres $ \() -> do
343 (childDir </> "dist" </> "doc" </> "html" </> fpFromText name)
344 (pbDocDir pb </> fpFromText namever)
349 </> fpFromText namever
350 </> fpFromText name <.> "haddock"
352 Left e -> warn $ tshow e
353 Right newPath -> atomically
354 $ modifyTVar sbHaddockFiles
355 $ insertMap namever newPath
357 case (eres, pcHaddocks) of
358 (Left e, ExpectSuccess) -> throwM e
359 (Right (), ExpectFailure) -> warn $ namever ++ ": unexpected Haddock success"
362 runTests = wf testOut $ \outH -> do
363 let run = runChild outH
365 when (pcTests /= Don'tBuild) $ do
366 log' $ "Test configure " ++ namever
367 run "cabal" $ "configure" : "--enable-tests" : configArgs
370 log' $ "Test build " ++ namever
371 run "cabal" ["build"]
373 log' $ "Test run " ++ namever
374 run "cabal" ["test", "--log=" ++ fpToText testRunOut]
376 case (eres, pcTests) of
377 (Left e, ExpectSuccess) -> throwM e
378 (Right (), ExpectFailure) -> warn $ namever ++ ": unexpected test success"
381 warn t = atomically $ modifyTVar sbWarningsVar (. (t:))
385 [ display (piName sbPackageInfo)
389 atomically $ modifyTVar sbErrsVar $ insertMap (piName sbPackageInfo) exc'
392 case fromException exc of
394 Nothing -> BuildFailureException exc
396renameOrCopy :: FilePath -> FilePath -> IO ()
397renameOrCopy src dest = rename src dest `catchIO` \_ -> copyDir src dest
399copyDir :: FilePath -> FilePath -> IO ()
401 runResourceT $ sourceDirectoryDeep False src $$ mapM_C go
404 go fp = forM_ (F.stripPrefix src' fp) $ \suffix -> do
405 let dest' = dest </> suffix
406 liftIO $ createTree $ parent dest'
407 sourceFile fp $$ (sinkFile dest' :: Sink ByteString (ResourceT IO) ())
409copyBuiltInHaddocks :: FilePath -> IO ()
410copyBuiltInHaddocks docdir = do
411 mghc <- findExecutable "ghc"
413 Nothing -> error "GHC not found on PATH"
415 src <- canonicalizePath
416 (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