stackage-curator-0.9.0: Stackage/CompleteBuild.hs
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
module Stackage.CompleteBuild
( BuildType (..)
, BumpType (..)
, BuildFlags (..)
, checkPlan
, getStackageAuthToken
, createPlan
, fetch
, makeBundle
, upload
, hackageDistro
, uploadGithub
, uploadDocs'
, checkTargetAvailable
) where
import System.Directory (getAppUserDataDirectory)
import Distribution.Package (Dependency)
import Filesystem (isDirectory, createTree, isFile, rename)
import Filesystem.Path (parent)
import Control.Concurrent (threadDelay, getNumCapabilities)
import Control.Concurrent.Async (withAsync)
import Data.Default.Class (def)
import Data.Semigroup (Max (..), Option (..))
import Data.Text.Read (decimal)
import Data.Time
import Data.Yaml (decodeFileEither, encodeFile, decodeEither')
import Network.HTTP.Client
import Network.HTTP.Client.Conduit (bodyReaderSource)
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Stackage.BuildConstraints
import Stackage.BuildPlan
import Stackage.CheckBuildPlan
import Stackage.PerformBuild
import Stackage.Prelude
import Stackage.ServerBundle
import Stackage.UpdateBuildPlan
import Stackage.Upload
import System.Environment (lookupEnv)
import System.IO (BufferMode (LineBuffering), hSetBuffering)
import Control.Monad.Trans.Unlift (askRunBase, MonadBaseUnlift)
import Data.Function (fix)
import Control.Concurrent.Async (Concurrently (..))
import Stackage.Curator.UploadDocs (uploadDocs)
-- | Flags passed in from the command line.
data BuildFlags = BuildFlags
{ bfEnableTests :: !Bool
, bfEnableHaddock :: !Bool
, bfDoUpload :: !Bool
, bfEnableLibProfile :: !Bool
, bfEnableExecDyn :: !Bool
, bfVerbose :: !Bool
, bfSkipCheck :: !Bool
, bfServer :: !StackageServer
, bfBuildHoogle :: !Bool
, bfBundleDest :: !(Maybe FilePath)
, bfGitPush :: !Bool
-- ^ push to Git (when doing an LTS build)
, bfJobs :: !(Maybe Int)
, bfPlanFile :: !(Maybe FilePath)
, bfPreBuild :: !Bool
, bfLoadPlan :: !Bool
} deriving (Show)
data BuildType = Nightly | LTS BumpType Text
deriving (Show, Read, Eq, Ord)
data BumpType = Major | Minor
deriving (Show, Read, Eq, Ord)
data Settings = Settings
{ plan :: BuildPlan
, planFile :: FilePath
, buildDir :: FilePath
, logDir :: FilePath
, title :: Text -> Text -- ^ GHC version -> title
, slug :: Text
, postBuild :: IO ()
, distroName :: Text -- ^ distro name on Hackage
, snapshotType :: SnapshotType
, bundleDest :: FilePath
}
nightlyPlanFile :: Text -- ^ day
-> FilePath
nightlyPlanFile day = fpFromText ("nightly-" ++ day) <.> "yaml"
nightlySettings :: Day
-> BuildFlags
-> BuildPlan
-> Settings
nightlySettings day' bf plan' = Settings
{ planFile = fromMaybe (nightlyPlanFile day) (bfPlanFile bf)
, buildDir = fpFromText $ "builds/nightly"
, logDir = fpFromText $ "logs/stackage-nightly-" ++ day
, title = \ghcVer -> concat
[ "Stackage Nightly "
, day
, ", GHC "
, ghcVer
]
, slug = slug'
, plan = plan'
, postBuild = return ()
, distroName = "Stackage"
, snapshotType = STNightly2 day'
, bundleDest = fromMaybe
(fpFromText $ "stackage-nightly-" ++ day ++ ".bundle")
(bfBundleDest bf)
}
where
slug' = "nightly-" ++ day
day = tshow day'
parseGoal :: MonadThrow m
=> BumpType
-> Text
-> m (LTSVer -> Bool)
parseGoal _ "" = return $ const True
parseGoal bumpType t =
case decimal t of
Right (major, "") -> return $ \(LTSVer major' _) ->
case bumpType of
-- For major bumps: specifying 2 means we want to ignore
-- anything in the 2.* range
Major -> major' < major
-- But for minor bumps, specifying 2 means we want to include
-- everything in 2.*, and start ignore 3.*
Minor -> major' <= major
_ ->
case parseLTSRaw t of
Nothing -> throwM $ ParseGoalFailure t
Just x -> return (< x)
data ParseGoalFailure = ParseGoalFailure Text
deriving (Show, Typeable)
instance Exception ParseGoalFailure
data LTSVer = LTSVer !Int !Int
deriving (Eq, Ord)
instance Show LTSVer where
show (LTSVer x y) = concat [show x, ".", show y]
incrLTSVer :: LTSVer -> LTSVer
incrLTSVer (LTSVer x y) = LTSVer x (y + 1)
parseLTSVer :: FilePath -> Maybe LTSVer
parseLTSVer fp = do
w <- stripPrefix "lts-" $ fpToText fp
x <- stripSuffix ".yaml" w
parseLTSRaw x
parseLTSRaw :: Text -> Maybe LTSVer
parseLTSRaw x = do
Right (major, y) <- Just $ decimal x
z <- stripPrefix "." y
Right (minor, "") <- Just $ decimal z
return $ LTSVer major minor
createPlan :: Target
-> FilePath
-> [Dependency] -- ^ additional constraints
-> IO ()
createPlan target dest constraints = withManager tlsManagerSettings $ \man -> do
putStrLn $ "Creating plan for: " ++ tshow target
bc <-
case target of
TargetLts x y | y /= 0 -> do
let url = concat
[ "https://raw.githubusercontent.com/fpco/lts-haskell/master/lts-"
, show x
, "."
, show (y - 1)
, ".yaml"
]
putStrLn $ "Downloading old plan from " ++ pack url
req <- parseUrl url
res <- httpLbs req man
oldplan <- either throwM return
$ decodeEither' (toStrict $ responseBody res)
return $ updateBuildConstraints oldplan
_ -> defaultBuildConstraints man
plan <- planFromConstraints $ setConstraints constraints bc
putStrLn $ "Writing build plan to " ++ fpToText dest
encodeFile (fpToString dest) plan
planFromConstraints bc = do
putStrLn "Creating build plan"
plans <- getLatestAllowedPlans bc
newBuildPlan plans bc
renderLTSVer :: LTSVer -> FilePath
renderLTSVer lts = fpFromText $ concat
[ "lts-"
, tshow lts
, ".yaml"
]
-- | Just print a message saying "still alive" every minute, to appease Travis.
stillAlive :: IO () -> IO ()
stillAlive inner =
withAsync (printer 1) $ const inner
where
printer i = forever $ do
threadDelay 60000000
putStrLn $ "Still alive: " ++ tshow i
printer $! i + 1
-- | Generate and check a new build plan, but do not execute it.
--
-- Since 0.3.1
checkPlan :: Maybe FilePath -> IO ()
checkPlan mfp = stillAlive $ withManager tlsManagerSettings $ \man -> do
plan <-
case mfp of
Nothing -> do
putStrLn "Loading default build constraints"
bc <- defaultBuildConstraints man
plan <- planFromConstraints bc
putStrLn $ "Writing build plan to check-plan.yaml"
encodeFile "check-plan.yaml" plan
return plan
Just fp -> do
putStrLn $ "Loading plan from " ++ fpToText fp
decodeFileEither (fpToString fp) >>= either throwM return
putStrLn "Checking plan"
checkBuildPlan plan
putStrLn "Plan seems valid!"
getPerformBuild :: BuildFlags -> Settings -> IO PerformBuild
getPerformBuild buildFlags Settings {..} = do
jobs <- maybe getNumCapabilities return $ bfJobs buildFlags
return PerformBuild
{ pbPlan = plan
, pbInstallDest = buildDir
, pbLogDir = logDir
, pbLog = hPut stdout
, pbJobs = jobs
, pbGlobalInstall = False
, pbEnableTests = bfEnableTests buildFlags
, pbEnableHaddock = bfEnableHaddock buildFlags
, pbEnableLibProfiling = bfEnableLibProfile buildFlags
, pbEnableExecDyn = bfEnableExecDyn buildFlags
, pbVerbose = bfVerbose buildFlags
, pbAllowNewer = bfSkipCheck buildFlags
, pbBuildHoogle = bfBuildHoogle buildFlags
}
{- FIXME remove
-- | Make a complete plan, build, test and upload bundle, docs and
-- distro.
completeBuild :: BuildType -> BuildFlags -> IO ()
completeBuild buildType buildFlags = withManager tlsManagerSettings $ \man -> do
hSetBuffering stdout LineBuffering
settings@Settings {..} <- if bfLoadPlan buildFlags
then
case bfPlanFile buildFlags of
Nothing -> error "When loading plan, plan file must be specified"
Just file -> do
putStrLn $ "Loading build plan from: " ++ fpToText file
getSettings man buildFlags buildType $ Just file
else do
putStrLn $ "Loading settings for: " ++ tshow buildType
settings@Settings {..} <- getSettings man buildFlags buildType Nothing
putStrLn $ "Writing build plan to: " ++ fpToText planFile
encodeFile (fpToString planFile) plan
if bfSkipCheck buildFlags
then putStrLn "Skipping build plan check"
else do
putStrLn "Checking build plan"
checkBuildPlan plan
return settings
pb <- getPerformBuild buildFlags settings
if bfPreBuild buildFlags
then prefetchPackages pb
else do
putStrLn "Performing build"
performBuild pb >>= mapM_ putStrLn
putStrLn $ "Creating bundle (v2) at: " ++ fpToText bundleDest
createBundleV2 CreateBundleV2
{ cb2Plan = plan
, cb2Type = snapshotType
, cb2DocsDir = pbDocDir pb
, cb2Dest = bundleDest
}
postBuild `catchAny` print
when (bfDoUpload buildFlags) $
finallyUpload
buildFlags
settings
man
-}
getStackageAuthToken :: IO Text
getStackageAuthToken = do
mtoken <- lookupEnv "STACKAGE_AUTH_TOKEN"
case mtoken of
Nothing -> decodeUtf8 <$> readFile "/auth-token"
Just token -> return $ pack token
{- FIXME remove
-- | The final part of the complete build process: uploading a bundle,
-- docs and a distro to hackage.
finallyUpload :: BuildFlags
-> Settings -> Manager -> IO ()
finallyUpload buildFlags settings@Settings{..} man = do
let server = bfServer buildFlags
pb <- getPerformBuild buildFlags settings
putStrLn "Uploading bundle to Stackage Server"
token <- getStackageAuthToken
res <- flip uploadBundleV2 man UploadBundleV2
{ ub2Server = server
, ub2AuthToken = token
, ub2Bundle = bundleDest
}
putStrLn $ "New snapshot available at: " ++ res
-}
hackageDistro
:: FilePath -- ^ plan file
-> Target
-> IO ()
hackageDistro planFile target = withManager tlsManagerSettings $ \man -> do
plan <- decodeFileEither (fpToString planFile) >>= either throwM return
ecreds <- tryIO $ readFile "/hackage-creds"
case map encodeUtf8 $ words $ decodeUtf8 $ either (const "") id ecreds of
[username, password] -> do
putStrLn $ "Uploading as Hackage distro: " ++ distroName
res2 <- uploadHackageDistro distroName plan username password man
putStrLn $ "Distro upload response: " ++ tshow res2
_ -> error "No Hackage creds found at /hackage-creds"
where
distroName =
case target of
TargetNightly _ -> "Stackage"
TargetLts _ _ -> "LTSHaskell"
checkoutRepo :: Target -> IO ([String] -> IO (), FilePath, FilePath)
checkoutRepo target = do
root <- fmap (</> "curator") $ fpFromString <$> getAppUserDataDirectory "stackage"
let repoDir =
case target of
TargetNightly _ -> root </> "stackage-nightly"
TargetLts _ _ -> root </> "lts-haskell"
runIn wdir cmd args = do
putStrLn $ concat
[ fpToText wdir
, ": "
, tshow (cmd:args)
]
withCheckedProcess
(proc cmd args)
{ cwd = Just $ fpToString wdir
} $ \ClosedStream Inherited Inherited -> return ()
git = runIn repoDir "git"
name =
case target of
TargetNightly day -> fpFromString $ concat
[ "nightly-"
, show day
, ".yaml"
]
TargetLts x y -> fpFromString $ concat
[ "lts-"
, show x
, "."
, show y
, ".yaml"
]
destFPPlan = repoDir </> name
destFPDocmap = repoDir </> "docs" </> name
exists <- isDirectory repoDir
if exists
then do
git ["fetch"]
git ["checkout", "origin/master"]
else do
createTree $ parent repoDir
runIn "." "git" ["clone", repoUrl, fpToString repoDir]
whenM (liftIO $ isFile destFPPlan)
$ error $ "File already exists: " ++ fpToString destFPPlan
whenM (liftIO $ isFile destFPDocmap)
$ error $ "File already exists: " ++ fpToString destFPDocmap
return (git, destFPPlan, destFPDocmap)
where
repoUrl =
case target of
TargetNightly _ -> "git@github.com:fpco/stackage-nightly"
TargetLts _ _ -> "git@github.com:fpco/lts-haskell"
uploadGithub
:: FilePath -- ^ plan file
-> FilePath -- ^ docmap file
-> Target
-> IO ()
uploadGithub planFile docmapFile target = do
(git, destFPPlan, destFPDocmap) <- checkoutRepo target
createTree $ parent destFPDocmap
runResourceT $ do
sourceFile planFile $$ (sinkFile destFPPlan :: Sink ByteString (ResourceT IO) ())
sourceFile docmapFile $$ (sinkFile destFPDocmap :: Sink ByteString (ResourceT IO) ())
git ["add", fpToString destFPPlan, fpToString destFPDocmap]
git ["commit", "-m", "Checking in " ++ fpToString (basename destFPPlan)]
git ["push", "origin", "HEAD:master"]
upload
:: FilePath -- ^ bundle file
-> StackageServer -- ^ server URL
-> IO ()
upload bundleFile server = withManager tlsManagerSettings $ \man -> do
putStrLn "Uploading bundle to Stackage Server"
token <- getStackageAuthToken
res <- flip uploadBundleV2 man UploadBundleV2
{ ub2Server = server
, ub2AuthToken = token
, ub2Bundle = bundleFile
}
putStrLn $ "New snapshot available at: " ++ res
uploadDocs' :: Target
-> FilePath -- ^ bundle file
-> IO ()
uploadDocs' target bundleFile = do
name <-
case target of
TargetNightly day -> return $ "nightly-" ++ tshow day
TargetLts x y -> return $ concat ["lts-", tshow x, ".", tshow y]
uploadDocs
(installDest target </> "doc")
bundleFile
name
"haddock.stackage.org"
installDest :: Target -> FilePath
installDest target =
case target of
TargetNightly _ -> "builds/nightly"
TargetLts x _ -> fpFromText $ "builds/lts-" ++ tshow x
makeBundle
:: FilePath -- ^ plan file
-> FilePath -- ^ docmap file
-> FilePath -- ^ bundle file
-> Target
-> Maybe Int -- ^ jobs
-> Bool -- ^ skip tests?
-> Bool -- ^ skip haddock?
-> Bool -- ^ skip hoogle?
-> Bool -- ^ enable library profiling?
-> Bool -- ^ enable executable dynamic?
-> Bool -- ^ verbose?
-> Bool -- ^ allow-newer?
-> IO ()
makeBundle
planFile docmapFile bundleFile target mjobs skipTests skipHaddocks skipHoogle
enableLibraryProfiling enableExecutableDynamic verbose allowNewer
= do
plan <- decodeFileEither (fpToString planFile) >>= either throwM return
jobs <- maybe getNumCapabilities return mjobs
let pb = PerformBuild
{ pbPlan = plan
, pbInstallDest = installDest target
, pbLog = hPut stdout
, pbLogDir =
case target of
TargetNightly _ -> "logs/nightly"
TargetLts x _ -> fpFromText $ "logs/lts-" ++ tshow x
, pbJobs = jobs
, pbGlobalInstall = False
, pbEnableTests = not skipTests
, pbEnableHaddock = not skipHaddocks
, pbEnableLibProfiling = enableLibraryProfiling
, pbEnableExecDyn = enableExecutableDynamic
, pbVerbose = verbose
, pbAllowNewer = allowNewer
, pbBuildHoogle = not skipHoogle
}
putStrLn "Performing build"
performBuild pb >>= mapM_ putStrLn
putStrLn $ "Creating bundle (v2) at: " ++ fpToText bundleFile
createBundleV2 CreateBundleV2
{ cb2Plan = plan
, cb2Type =
case target of
TargetNightly day -> STNightly2 day
TargetLts x y -> STLTS x y
, cb2DocsDir = pbDocDir pb
, cb2Dest = bundleFile
, cb2DocmapFile = docmapFile
}
fetch :: FilePath -> IO ()
fetch planFile = withManager tlsManagerSettings $ \man -> do
-- First make sure to fetch all of the dependencies... just in case Hackage
-- has an outage. Don't feel like wasting hours of CPU time.
putStrLn "Pre-fetching all packages"
plan <- decodeFileEither (fpToString planFile) >>= either throwM return
cabalDir <- fpFromString <$> getAppUserDataDirectory "cabal"
parMapM_ 8 (download man cabalDir) $ mapToList $ bpPackages plan
where
download man cabalDir (display -> name, display . ppVersion -> version) = do
unlessM (isFile fp) $ do
hPut stdout $ encodeUtf8 $ concat
[ "Downloading "
, name
, "-"
, version
, "\n"
]
createTree $ parent fp
req <- parseUrl url
withResponse req man $ \res -> do
let tmp = fp <.> "tmp"
runResourceT $ bodyReaderSource (responseBody res) $$ sinkFile tmp
rename tmp fp
where
url = unpack $ concat
[ "https://s3.amazonaws.com/hackage.fpcomplete.com/package/"
, name
, "-"
, version
, ".tar.gz"
]
fp = cabalDir </>
"packages" </>
"hackage.haskell.org" </>
fpFromText name </>
fpFromText version </>
fpFromText (concat [name, "-", version, ".tar.gz"])
parMapM_ :: (MonadIO m, MonadBaseUnlift IO m, MonoFoldable mono)
=> Int
-> (Element mono -> m ())
-> mono
-> m ()
parMapM_ (max 1 -> 1) f xs = mapM_ f xs
parMapM_ cnt f xs0 = do
var <- liftBase $ newTVarIO $ toList xs0
run <- askRunBase
let worker :: IO ()
worker = run $ fix $ \loop -> join $ atomically $ do
xs <- readTVar var
case xs of
[] -> return $ return ()
x:xs' -> do
writeTVar var xs'
return $ do
f x
loop
workers 1 = Concurrently worker
workers i = Concurrently worker *> workers (i - 1)
liftBase $ runConcurrently $ workers cnt
-- | Check if the given target is already used in the Github repos
checkTargetAvailable :: Target -> IO ()
checkTargetAvailable = void . checkoutRepo