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 , pbEnableHaddock :: Bool
66 , pbEnableLibProfiling :: Bool
68 , pbAllowNewer :: Bool
69 -- ^ Pass --allow-newer to cabal configure
72data PackageInfo = PackageInfo
73 { piPlan :: PackagePlan
74 , piName :: PackageName
75 , piResult :: TMVar Bool
78waitForDeps :: Map ExeName (Set PackageName)
79 -> Map PackageName PackageInfo
85waitForDeps toolMap packageMap activeComps bp pi action = do
87 mapM_ checkPackage $ Map.keys $ filterUnused $ sdPackages $ ppDesc $ piPlan pi
88 forM_ (Map.keys $ filterUnused $ sdTools $ ppDesc $ piPlan pi) $ \exe -> do
89 case lookup exe toolMap >>= fromNullable . map checkPackage . setToList of
91 | isCoreExe exe -> return ()
92 -- https://github.com/jgm/zip-archive/issues/23
93 -- | otherwise -> throwSTM $ ToolMissing exe
94 | otherwise -> return ()
95 Just packages -> ofoldl1' (<|>) packages
98 filterUnused :: Ord key => Map key DepInfo -> Map key DepInfo
100 mapFromList . filter (go . snd) . mapToList
102 go = not . null . intersection activeComps . diComponents
104 checkPackage package | package == piName pi = return ()
105 checkPackage package =
106 case lookup package packageMap of
108 | isCore package -> return ()
109 | otherwise -> throwSTM $ DependencyMissing package
111 res <- readTMVar $ piResult dep
112 unless res $ throwSTM $ DependencyFailed package
114 isCore = (`member` siCorePackages (bpSystemInfo bp))
115 isCoreExe = (`member` siCoreExecutables (bpSystemInfo bp))
117withCounter :: TVar Int -> IO a -> IO a
118withCounter counter = bracket_
119 (atomically $ modifyTVar counter (+ 1))
120 (atomically $ modifyTVar counter (subtract 1))
122withTSem :: TSem -> IO a -> IO a
123withTSem sem = bracket_ (atomically $ waitTSem sem) (atomically $ signalTSem sem)
125-- | Returns @Nothing@ if installing to a global database
126pbDatabase :: PerformBuild -> Maybe FilePath
128 | pbGlobalInstall pb = Nothing
129 | otherwise = Just $ pbInstallDest pb </> "pkgdb"
131pbBinDir, pbLibDir, pbDataDir, pbDocDir :: PerformBuild -> FilePath
132pbBinDir pb = pbInstallDest pb </> "bin"
133pbLibDir pb = pbInstallDest pb </> "lib"
134pbDataDir pb = pbInstallDest pb </> "share"
135pbDocDir pb = pbInstallDest pb </> "doc"
137performBuild :: PerformBuild -> IO [Text]
139 cwd <- getWorkingDirectory
141 { pbInstallDest = cwd </> pbInstallDest pb
142 , pbLogDir = cwd </> pbLogDir pb
145performBuild' :: PerformBuild -> IO [Text]
146performBuild' pb@PerformBuild {..} = withBuildDir $ \builddir -> do
147 -- First make sure to fetch all of the dependencies... just in case Hackage
148 -- has an outage. Don't feel like wasting hours of CPU time.
149 pbLog $ encodeUtf8 "Pre-fetching all packages\n"
150 let toDownload = flip map (mapToList $ bpPackages pbPlan)
151 $ \(name, plan) -> unpack $ concat
154 , display $ ppVersion plan
159 : "--no-dependencies"
161 $ \ClosedStream Inherited Inherited -> return ()
163 let removeTree' fp = whenM (isDirectory fp) (removeTree fp)
164 mapM_ removeTree' [pbInstallDest, pbLogDir]
166 forM_ (pbDatabase pb) $ \db -> do
167 createTree $ parent db
168 withCheckedProcess (proc "ghc-pkg" ["init", fpToString db])
169 $ \ClosedStream Inherited Inherited -> return ()
170 pbLog $ encodeUtf8 "Copying built-in Haddocks\n"
171 copyBuiltInHaddocks (pbDocDir pb)
173 sem <- atomically $ newTSem pbJobs
174 active <- newTVarIO (0 :: Int)
175 let toolMap = makeToolMap $ bpPackages pbPlan
176 packageMap <- fmap fold $ forM (mapToList $ bpPackages pbPlan)
177 $ \(name, plan) -> do
180 piResult <- newEmptyTMVarIO
181 return $ singletonMap name PackageInfo {..}
183 errsVar <- newTVarIO mempty
184 warningsVar <- newTVarIO id
186 env <- getEnvironment
187 haddockFiles <- newTVarIO mempty
189 forM_ packageMap $ \pi -> void $ async $ singleBuild pb SingleBuild
191 , sbErrsVar = errsVar
192 , sbWarningsVar = warningsVar
194 , sbToolMap = toolMap
195 , sbPackageMap = packageMap
196 , sbBuildDir = builddir
198 , sbRegisterMutex = mutex
199 , sbModifiedEnv = maybe
201 (\db -> (("HASKELL_PACKAGE_SANDBOX", fpToString db):))
203 (filter allowedEnv $ map fixEnv env)
204 , sbHaddockFiles = haddockFiles
207 void $ tryAny $ atomically $ readTVar active >>= checkSTM . (== 0)
209 warnings <- ($ []) <$> readTVarIO warningsVar
210 errs <- readTVarIO errsVar
211 when (not $ null errs) $ throwM $ BuildException errs warnings
214 withBuildDir f = withSystemTempDirectory "stackage-build" (f . fpFromString)
217 -- Thank you Windows having case-insensitive environment variables...
218 | toUpper p == "PATH" = (p, fpToString (pbBinDir pb) ++ pathSep : x)
221 allowedEnv (k, _) = k `notMember` bannedEnvs
223 -- | Separate for the PATH environment variable
225#ifdef mingw32_HOST_OS
231-- | Environment variables we don't allow to be passed on to child processes.
232bannedEnvs :: Set String
233bannedEnvs = setFromList
234 [ "STACKAGE_AUTH_TOKEN"
237data SingleBuild = SingleBuild
239 , sbErrsVar :: TVar (Map PackageName BuildFailure)
240 , sbWarningsVar :: TVar ([Text] -> [Text])
241 , sbActive :: TVar Int
242 , sbToolMap :: Map ExeName (Set PackageName)
243 , sbPackageMap :: Map PackageName PackageInfo
244 , sbBuildDir :: FilePath
245 , sbPackageInfo :: PackageInfo
246 , sbRegisterMutex :: MVar ()
247 , sbModifiedEnv :: [(String, String)]
248 , sbHaddockFiles :: TVar (Map Text FilePath) -- ^ package-version, .haddock file
251singleBuild :: PerformBuild -> SingleBuild -> IO ()
252singleBuild pb@PerformBuild {..} SingleBuild {..} =
255 $ (`finally` void (atomically $ tryPutTMVar (piResult sbPackageInfo) False))
258 libComps = setFromList [CompLibrary, CompExecutable]
259 testComps = insertSet CompTestSuite libComps
262 waitForDeps sbToolMap sbPackageMap comps pbPlan sbPackageInfo
264 wfd libComps buildLibrary
266 wfd testComps runTests
268 name = display $ piName sbPackageInfo
272 , display $ ppVersion $ piPlan sbPackageInfo
275 runIn wdir outH cmd args =
276 withCheckedProcess cp $ \ClosedStream UseProvidedHandle UseProvidedHandle ->
279 cp = (proc (unpack $ asText cmd) (map (unpack . asText) args))
280 { cwd = Just $ fpToString wdir
281 , std_out = UseHandle outH
282 , std_err = UseHandle outH
283 , env = Just sbModifiedEnv
285 runParent = runIn sbBuildDir
286 runChild = runIn childDir
287 childDir = sbBuildDir </> fpFromText namever
290 i <- readTVarIO sbActive
291 errs <- readTVarIO sbErrsVar
292 pbLog $ encodeUtf8 $ concat
297 , tshow $ length errs
300 libOut = pbLogDir </> fpFromText namever </> "build.out"
301 testOut = pbLogDir </> fpFromText namever </> "test.out"
302 testRunOut = pbLogDir </> fpFromText namever </> "test-run.out"
305 createTree $ parent fp
306 withBinaryFile (fpToString fp) WriteMode inner'
308 configArgs = ($ []) $ execWriter $ do
309 when pbAllowNewer $ tell' "--allow-newer"
310 tell' "--package-db=clear"
311 tell' "--package-db=global"
312 forM_ (pbDatabase pb) $ \db -> tell' $ "--package-db=" ++ fpToText db
313 tell' $ "--libdir=" ++ fpToText (pbLibDir pb)
314 tell' $ "--bindir=" ++ fpToText (pbBinDir pb)
315 tell' $ "--datadir=" ++ fpToText (pbDataDir pb)
316 tell' $ "--docdir=" ++ fpToText (pbDocDir pb)
317 tell' $ "--flags=" ++ flags
318 when (pbEnableLibProfiling && pcEnableLibProfile) $
319 tell' "--enable-library-profiling"
324 flags = unwords $ map go $ mapToList pcFlagOverrides
326 go (name', isOn) = concat
327 [ if isOn then "" else "-"
331 PackageConstraints {..} = ppConstraints $ piPlan sbPackageInfo
333 buildLibrary = wf libOut $ \outH -> do
334 let run a b = do when pbVerbose $ log' (unwords (a : b))
336 log' $ "Unpacking " ++ namever
337 runParent outH "cabal" ["unpack", namever]
339 log' $ "Configuring " ++ namever
340 run "cabal" $ "configure" : configArgs
342 log' $ "Building " ++ namever
343 run "cabal" ["build"]
345 log' $ "Copying/registering " ++ namever
347 withMVar sbRegisterMutex $ const $
348 run "cabal" ["register"]
350 -- Even if the tests later fail, we can allow other libraries to build
351 -- on top of our successful results
353 -- FIXME do we need to wait to do this until after Haddocks build?
354 -- otherwise, we could have a race condition and try to build a
355 -- dependency's haddocks before this finishes
356 atomically $ putTMVar (piResult sbPackageInfo) True
358 when (pbEnableHaddock && pcHaddocks /= Don'tBuild && not (null $ sdModules $ ppDesc $ piPlan sbPackageInfo)) $ do
359 log' $ "Haddocks " ++ namever
360 hfs <- readTVarIO sbHaddockFiles
361 let hfsOpts = flip map (mapToList hfs) $ \(pkgVer, hf) -> concat
362 [ "--haddock-options=--read-interface="
369 : "--hyperlink-source"
372 : "--html-location=../$pkg-$version/"
375 eres <- tryAny $ run "cabal" args
377 forM_ eres $ \() -> do
379 (childDir </> "dist" </> "doc" </> "html" </> fpFromText name)
380 (pbDocDir pb </> fpFromText namever)
385 </> fpFromText namever
386 </> fpFromText name <.> "haddock"
388 Left e -> warn $ tshow e
389 Right newPath -> atomically
390 $ modifyTVar sbHaddockFiles
391 $ insertMap namever newPath
393 case (eres, pcHaddocks) of
394 (Left e, ExpectSuccess) -> throwM e
395 (Right (), ExpectFailure) -> warn $ namever ++ ": unexpected Haddock success"
398 runTests = wf testOut $ \outH -> do
399 let run = runChild outH
401 when (pbEnableTests && pcTests /= Don'tBuild) $ do
402 log' $ "Test configure " ++ namever
403 run "cabal" $ "configure" : "--enable-tests" : configArgs
406 log' $ "Test build " ++ namever
407 run "cabal" ["build"]
409 log' $ "Test run " ++ namever
410 run "cabal" ["test", "--log=" ++ fpToText testRunOut]
412 case (eres, pcTests) of
413 (Left e, ExpectSuccess) -> throwM e
414 (Right (), ExpectFailure) -> warn $ namever ++ ": unexpected test success"
417 warn t = atomically $ modifyTVar sbWarningsVar (. (t:))
421 [ display (piName sbPackageInfo)
425 atomically $ modifyTVar sbErrsVar $ insertMap (piName sbPackageInfo) exc'
428 case fromException exc of
430 Nothing -> BuildFailureException exc
432renameOrCopy :: FilePath -> FilePath -> IO ()
433renameOrCopy src dest = rename src dest `catchIO` \_ -> copyDir src dest
435copyDir :: FilePath -> FilePath -> IO ()
437 runResourceT $ sourceDirectoryDeep False src $$ mapM_C go
440 go fp = forM_ (F.stripPrefix src' fp) $ \suffix -> do
441 let dest' = dest </> suffix
442 liftIO $ createTree $ parent dest'
443 sourceFile fp $$ (sinkFile dest' :: Sink ByteString (ResourceT IO) ())
445copyBuiltInHaddocks :: FilePath -> IO ()
446copyBuiltInHaddocks docdir = do
447 mghc <- findExecutable "ghc"
449 Nothing -> error "GHC not found on PATH"
451 src <- canonicalizePath
452 (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