{-# LANGUAGE DataKinds #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module HaskellCI (
main,
parseTravis,
travisFromConfigFile, Options (..), defaultOptions,
) where
import HaskellCI.Prelude
import Data.List (nubBy, sort, sortBy, (\\))
import System.Directory (canonicalizePath, doesFileExist, makeRelativeToCurrentDirectory, setCurrentDirectory)
import System.Environment (getArgs)
import System.Exit (ExitCode (..), exitFailure)
import System.FilePath.Posix (takeDirectory, takeFileName)
import System.IO (hClose, hFlush, hPutStr, hPutStrLn, stderr)
import System.IO.Temp (withSystemTempFile)
import System.Process (readProcessWithExitCode)
import Distribution.PackageDescription (GenericPackageDescription, package, packageDescription, testedWith)
import Distribution.Simple.Utils (fromUTF8BS)
import Distribution.Text
import Distribution.Version
import qualified Data.ByteString as BS
import qualified Data.Map as Map
import qualified Data.List.NonEmpty as NE
import qualified Data.Set as S
import qualified Data.Traversable as T
import qualified Distribution.Compiler as Compiler
import qualified Distribution.Package as Pkg
import qualified Options.Applicative as O
import Cabal.Parse
import Cabal.Project
import HaskellCI.Cli
import HaskellCI.Compiler
import HaskellCI.Config
import HaskellCI.Config.Dump
import HaskellCI.Diagnostics
import HaskellCI.Jobs
import HaskellCI.Package
import HaskellCI.TestedWith
import HaskellCI.Travis
import HaskellCI.YamlSyntax
import HaskellCI.VersionInfo
main :: IO ()
main = do
argv0 <- getArgs
(cmd, opts) <- O.execParser cliParserInfo
case cmd of
CommandListGHC -> do
putStrLn $ "Supported GHC versions:"
for_ groupedVersions $ \(v, vs) -> do
putStr $ prettyMajVersion v ++ ": "
putStrLn $ intercalate ", " (map display $ toList vs)
CommandDumpConfig -> do
putStr $ unlines $ runDG configGrammar
CommandRegenerate -> do
let fp = case optOutput opts of
Just (OutputFile fp') -> fp'
_ -> defaultTravisPath
contents <- fromUTF8BS <$> BS.readFile fp
absFp <- canonicalizePath fp
let dir = takeDirectory fp
setCurrentDirectory dir
newFp <- makeRelativeToCurrentDirectory absFp
case findArgv (lines contents) of
Nothing -> do
hPutStrLn stderr $ "Error: expected REGENDATA line in " ++ fp
exitFailure
Just (mversion, argv) -> do
for_ mversion $ \version -> for_ (simpleParsec haskellCIVerStr) $ \haskellCIVer ->
when (haskellCIVer < version) $ do
hPutStrLn stderr $ "Regenerating using older haskell-ci-" ++ haskellCIVerStr
hPutStrLn stderr $ "File generated using haskell-ci-" ++ prettyShow version
(f, opts') <- parseTravis argv
doTravis argv f (optionsWithOutputFile newFp <> opts' <> opts)
CommandTravis f -> doTravis argv0 f opts
CommandVersionInfo -> do
putStrLn $ "haskell-ci " ++ haskellCIVerStr ++ " with dependencies"
ifor_ dependencies $ \p v -> do
putStrLn $ " " ++ p ++ "-" ++ v
where
findArgv :: [String] -> Maybe (Maybe Version, [String])
findArgv ls = do
l <- findMaybe (afterInfix "REGENDATA") ls
first simpleParsec <$> (readMaybe l :: Maybe (String, [String]))
<|> (,) Nothing <$> (readMaybe l :: Maybe [String])
groupedVersions :: [(Version, NonEmpty Version)]
groupedVersions = map ((\vs -> (head vs, vs)) . NE.sortBy (flip compare))
. groupBy ((==) `on` ghcMajVer)
$ sort knownGhcVersions
prettyMajVersion :: Version -> String
prettyMajVersion v = case ghcMajVer v of
(x, y) -> show x ++ "." ++ show y
ifor_ :: Map.Map k v -> (k -> v -> IO a) -> IO ()
ifor_ xs f = Map.foldlWithKey' (\m k a -> m >> void (f k a)) (return ()) xs
defaultTravisPath :: FilePath
defaultTravisPath = ".travis.yml"
doTravis :: [String] -> FilePath -> Options -> IO ()
doTravis args path opts = do
ls <- travisFromConfigFile args opts path
let contents = unlines ls
case optOutput opts of
Nothing -> writeFile defaultTravisPath contents
Just OutputStdout -> putStr contents
Just (OutputFile fp) -> writeFile fp contents
travisFromConfigFile
:: forall m. (MonadIO m, MonadDiagnostics m, MonadMask m)
=> [String]
-> Options
-> FilePath
-> m [String]
travisFromConfigFile args opts path = do
cabalFiles <- getCabalFiles
config' <- maybe (return emptyConfig) readConfigFile (optConfig opts)
let config = optConfigMorphism opts config'
pkgs <- T.mapM (configFromCabalFile config) cabalFiles
(ghcs, prj) <- case checkVersions (cfgTestedWith config) pkgs of
Right x -> return x
Left [] -> putStrLnErr "panic: checkVersions failed without errors"
Left (e:es) -> putStrLnErrs (e :| es)
let prj' | cfgGhcHead config = over (mapped . field @"pkgJobs") (S.insert GHCHead) prj
| otherwise = prj
ls <- genTravisFromConfigs args config prj' ghcs
patchTravis config ls
where
isCabalProject :: Maybe FilePath
isCabalProject
| "cabal.project" `isPrefixOf` takeFileName path = Just path
| otherwise = Nothing
getCabalFiles :: m (Project URI Void (FilePath, GenericPackageDescription))
getCabalFiles
| isNothing isCabalProject = do
e <- liftIO $ readPackagesOfProject (emptyProject & field @"prjPackages" .~ [path])
either (putStrLnErr . renderParseError) return e
| otherwise = do
contents <- liftIO $ BS.readFile path
prj0 <- either (putStrLnErr . renderParseError) return $ parseProject path contents
prj1 <- either (putStrLnErr . renderResolveError) return =<< liftIO (resolveProject path prj0)
either (putStrLnErr . renderParseError) return =<< liftIO (readPackagesOfProject prj1)
genTravisFromConfigs
:: (Monad m, MonadDiagnostics m)
=> [String]
-> Config
-> Project URI Void Package
-> Set CompilerVersion
-> m [String]
genTravisFromConfigs argv config prj vs = do
let jobVersions = makeJobVersions config vs
case makeTravis argv config prj jobVersions of
Left err -> putStrLnErr $ displayException err
Right travis -> do
describeJobs (cfgTestedWith config) jobVersions (prjPackages prj)
return $
lines (prettyYaml id $ reann (travisHeader (cfgInsertVersion config) argv ++) $ toYaml travis)
++
[ ""
, "# REGENDATA " ++ if cfgInsertVersion config then show (haskellCIVerStr, argv) else show argv
, "# EOF"
]
patchTravis
:: (MonadIO m, MonadMask m)
=> Config -> [String] -> m [String]
patchTravis cfg ls
| null patches = pure ls
| otherwise =
withSystemTempFile ".travis.yml.tmp" $ \fp h -> liftIO $ do
hPutStr h $ unlines ls
hFlush h
for_ patches $ applyPatch fp
hClose h
lines . fromUTF8BS <$> BS.readFile fp
where
patches :: [FilePath]
patches = cfgTravisPatches cfg
applyPatch :: FilePath
-> FilePath
-> IO ()
applyPatch temp patch = do
exists <- doesFileExist patch
unless exists $ putStrLnErr $ "Cannot find " ++ patch
(ec, stdOut, stdErr) <- readProcessWithExitCode
"patch" [ "--input", patch
, "--silent"
, temp
] ""
case ec of
ExitSuccess -> pure ()
ExitFailure n -> putStrLnErr $ unlines
[ "patch returned exit code " ++ show n
, "Stdout: " ++ stdOut
, "Stderr: " ++ stdErr
]
configFromCabalFile
:: (MonadIO m, MonadDiagnostics m)
=> Config -> (FilePath, GenericPackageDescription) -> m Package
configFromCabalFile cfg (cabalFile, gpd) = do
let compilers = testedWith $ packageDescription gpd
pkgNameStr = display $ Pkg.pkgName $ package $ packageDescription gpd
let unknownComps = nub [ c | (c,_) <- compilers, c /= Compiler.GHC, c /= Compiler.GHCJS ]
ghcVerConstrs = [ vc | (Compiler.GHC,vc) <- compilers ]
ghcVerConstrs' = simplifyVersionRange $ foldr unionVersionRanges noVersion ghcVerConstrs
specificGhcVers = nub $ mapMaybe isSpecificVersion ghcVerConstrs
ghcjsVerConstrs = [ vc | (Compiler.GHCJS,vc) <- compilers ]
ghcjsVerConstrs' = simplifyVersionRange $ foldr unionVersionRanges noVersion ghcjsVerConstrs
specificGhcjsVers = nub $ mapMaybe isSpecificVersion ghcjsVerConstrs
twoDigitGhcVerConstrs = mapMaybe isTwoDigitGhcVersion ghcVerConstrs :: [Version]
unless (null twoDigitGhcVerConstrs) $ do
putStrLnWarn $ "'tested-with:' uses two digit GHC versions (which don't match any existing GHC version): " ++ intercalate ", " (map display twoDigitGhcVerConstrs)
putStrLnInfo $ "Either use wild-card format, for example 'tested-with: GHC ==7.10.*' or a specific existing version 'tested-with: GHC ==7.10.3'"
when (null compilers) $ do
putStrLnErr (unlines $
[ "empty or missing top-level 'tested-with:' definition in " ++ cabalFile ++ " file; example definition:"
, ""
, "tested-with: " ++ intercalate ", " [ "GHC==" ++ display v | v <- lastStableGhcVers ]
])
unless (null unknownComps) $ do
putStrLnWarn $ "ignoring unsupported compilers mentioned in tested-with: " ++ show unknownComps
when (null ghcVerConstrs) $ do
putStrLnErr "'tested-with:' doesn't mention any 'GHC' version"
when (isNoVersion ghcVerConstrs') $ do
putStrLnErr "'tested-with:' describes an empty version range for 'GHC'"
when (isAnyVersion ghcVerConstrs') $ do
putStrLnErr "'tested-with:' allows /any/ 'GHC' version"
let unknownGhcVers = sort $ specificGhcVers \\ knownGhcVersions
unless (null unknownGhcVers) $ do
putStrLnErr ("'tested-with:' specifically refers to unknown 'GHC' versions: "
++ intercalate ", " (map display unknownGhcVers) ++ "\n"
++ "Known GHC versions: " ++ intercalate ", " (map display knownGhcVersions))
let unknownGhcjsVers = sort $ specificGhcjsVers \\ knownGhcjsVersions
unless (null unknownGhcjsVers) $ do
putStrLnErr ("'tested-with:' specifically refers to unknown 'GHCJS' versions: "
++ intercalate ", " (map display unknownGhcjsVers) ++ "\n"
++ "Known GHCJS versions: " ++ intercalate ", " (map display knownGhcjsVersions))
let knownGhcVersions'
| cfgLastInSeries cfg = filterLastMajor knownGhcVersions
| otherwise = knownGhcVersions
let testedGhcVersions = filter (`withinRange` ghcVerConstrs') knownGhcVersions'
let testedGhcjsVersions = filter (`withinRange` ghcjsVerConstrs') knownGhcjsVersions
when (null testedGhcVersions) $ do
putStrLnErr "no known GHC version is allowed by the 'tested-with' specification"
let compilerRange :: Set CompilerVersion
compilerRange = S.fromList $
[ GHC v
| v <- testedGhcVersions
] ++
[ GHCJS v
| v <- testedGhcjsVersions
]
let pkg = Pkg pkgNameStr compilerRange (takeDirectory cabalFile) gpd
return pkg
where
lastStableGhcVers
= nubBy ((==) `on` ghcMajVer)
$ sortBy (flip compare)
$ filter (not . previewGHC defaultHeadHackage . GHC)
$ knownGhcVersions
isTwoDigitGhcVersion :: VersionRange -> Maybe Version
isTwoDigitGhcVersion vr = isSpecificVersion vr >>= t
where
t v | [_,_] <- versionNumbers v = Just v
t _ = Nothing
filterLastMajor = map maximum . groupBy ((==) `on` ghcMajVer)