{-# LANGUAGE CPP #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedLabels #-}
module HaskellCI (
main,
Result (..),
Diagnostic (..),
parseTravis,
formatDiagnostic, formatDiagnostics,
travisFromConfigFile, MakeTravisOutput, Options (..), defaultOptions,
) where
import Prelude ()
import Prelude.Compat
import Control.DeepSeq (force)
import Control.Exception (evaluate)
import Control.Monad (when, unless, liftM, forM_, mzero)
import qualified Data.ByteString as BS
import qualified Data.Foldable as F
import qualified Data.Traversable as T
import Data.Function
import Data.List
import Data.Maybe
import Data.Set (Set)
import qualified Data.Set as S
import qualified Data.Map as M
import System.Directory (doesDirectoryExist, doesFileExist)
import System.Exit
import System.FilePath.Posix ((</>), takeDirectory, takeFileName, takeExtension)
import System.IO
import Control.Monad.IO.Class
import Control.Monad.Trans.Maybe
import System.Environment (getArgs)
import Control.Monad.Trans.Writer
import Text.Read (readMaybe)
import Distribution.Compat.ReadP (readP_to_S)
import qualified Options.Applicative as O
import Distribution.Compiler (CompilerFlavor(..))
import Distribution.Package hiding (Package, pkgName)
import qualified Distribution.Package as Pkg
import Distribution.PackageDescription (GenericPackageDescription,packageDescription, testedWith, package, condLibrary, condTestSuites)
import Distribution.PackageDescription.Configuration (flattenPackageDescription)
import qualified Distribution.PackageDescription as PD
import qualified Distribution.ParseUtils as PU
import Distribution.Text
import Distribution.Version
#if MIN_VERSION_Cabal(2,2,0)
import Distribution.PackageDescription.Parsec (readGenericPackageDescription)
#elif MIN_VERSION_Cabal(2,0,0)
import Distribution.PackageDescription.Parse (readGenericPackageDescription)
#else
import Distribution.PackageDescription.Parse (readPackageDescription)
import Distribution.Verbosity (Verbosity)
#endif
import qualified Distribution.FieldGrammar as C
import qualified Distribution.Fields.Pretty as C
import qualified Distribution.PackageDescription.FieldGrammar as C
import qualified Distribution.Types.SourceRepo as C
import qualified Distribution.Types.VersionRange as C
import qualified Text.PrettyPrint as PP
#if MIN_VERSION_base(4,9,0)
import Data.Semigroup (Semigroup (..))
#else
import Data.Monoid ((<>))
#endif
import Lens.Micro
import Data.Generics.Labels ()
import qualified Distribution.Types.BuildInfo.Lens as L
import qualified Distribution.Types.PackageDescription.Lens as L
import HaskellCI.Cli
import HaskellCI.Config
import HaskellCI.Config.CopyFields
import HaskellCI.Config.ConstraintSet
import HaskellCI.Config.Doctest
import HaskellCI.Config.Dump
import HaskellCI.Config.Folds
import HaskellCI.Config.HLint
import HaskellCI.Config.Installed
import HaskellCI.Config.Jobs
import HaskellCI.Extras
import HaskellCI.GHC
import HaskellCI.Glob
import HaskellCI.MakeTravisOutput
import HaskellCI.Optimization
import HaskellCI.Package
import HaskellCI.Project
import HaskellCI.TestedWith
import HaskellCI.Version
#ifndef CURRENT_PACKAGE_VERSION
#define CURRENT_PACKAGE_VERSION "???"
#endif
main :: IO ()
main = do
argv0 <- getArgs
(cmd, opts) <- O.execParser cliParserInfo
case cmd of
CommandListGHC -> do
putStrLn $ "Supported GHC versions:"
forM_ groupedVersions $ \(v, vs) -> do
putStr $ prettyMajVersion v ++ ": "
putStrLn $ intercalate ", " (map display vs)
CommandDumpConfig -> do
putStr $ unlines $ runDG configGrammar
CommandRegenerate -> do
let fp = ".travis.yml"
contents <- readFile fp
case findArgv (lines contents) of
Nothing -> do
hPutStrLn stderr $ "Error: expected REGENDATA line in " ++ fp
exitFailure
Just argv -> do
(f, opts') <- parseTravis argv
doTravis argv f (opts' <> opts)
CommandTravis f -> doTravis argv0 f opts
where
findArgv :: [String] -> Maybe [String]
findArgv ls = do
l <- findMaybe (afterInfix "REGENDATA") ls
readMaybe l
groupedVersions :: [(Version, [Version])]
groupedVersions = map ((\vs -> (head vs, vs)) . sortBy (flip compare))
. groupBy ((==) `on` ghcMajVer)
$ sort knownGhcVersions
prettyMajVersion :: Version -> String
prettyMajVersion v
| Just v == ghcAlpha = "alpha"
| otherwise = case ghcMajVer v of (x,y) -> show x ++ "." ++ show y
doTravis :: [String] -> FilePath -> Options -> IO ()
doTravis args path opts = do
runYamlWriter (optOutput opts) $ travisFromConfigFile args opts path
runYamlWriter :: Maybe FilePath -> YamlWriter IO () -> IO ()
runYamlWriter mfp m = do
result <- execWriterT (runMaybeT m)
case result of
Failure (formatDiagnostics -> errors) -> hPutStr stderr errors >> exitFailure
Success (formatDiagnostics -> warnings) (unlines -> contents) -> do
contents' <- evaluate (force contents)
hPutStr stderr warnings
case mfp of
Nothing -> putStr contents'
Just fp -> writeFile fp contents'
travisFromConfigFile
:: MonadIO m
=> [String]
-> Options
-> FilePath
-> YamlWriter m ()
travisFromConfigFile args opts path = do
cabalFiles <- getCabalFiles
config' <- maybe (return emptyConfig) readConfigFile (optConfig opts)
let config = optConfigMorphism opts config'
pkgs <- T.mapM (configFromCabalFile config opts) cabalFiles
(ghcs, prj) <- case checkVersions (cfgTestedWith config) pkgs of
Right x -> return x
Left errors -> putStrLnErrs errors >> mzero
genTravisFromConfigs args opts isCabalProject config prj ghcs
where
isCabalProject :: Maybe FilePath
isCabalProject
| "cabal.project" `isPrefixOf` takeFileName path = Just path
| otherwise = Nothing
getCabalFiles :: MonadIO m => YamlWriter m (Project FilePath)
getCabalFiles
| isNothing isCabalProject = return $ emptyProject & #prjPackages .~ [path]
| otherwise = do
contents <- liftIO $ BS.readFile path
pkgs <- either putStrLnErr return $ parseProjectFile path contents
over #prjPackages concat `liftM` T.mapM findProjectPackage pkgs
rootdir = takeDirectory path
findProjectPackage :: MonadIO m => String -> YamlWriter m [FilePath]
findProjectPackage pkglocstr = do
mfp <- checkisFileGlobPackage pkglocstr `mplusMaybeT`
checkIsSingleFilePackage pkglocstr
maybe (putStrLnErr $ "bad package location: " ++ pkglocstr) return mfp
checkIsSingleFilePackage pkglocstr = do
let abspath = rootdir </> pkglocstr
isFile <- liftIO $ doesFileExist abspath
isDir <- liftIO $ doesDirectoryExist abspath
if | isFile && takeExtension pkglocstr == ".cabal" -> return (Just [abspath])
| isDir -> checkisFileGlobPackage (pkglocstr </> "*.cabal")
| otherwise -> return Nothing
checkisFileGlobPackage pkglocstr =
case filter (null . snd) $ readP_to_S parseFilePathGlobRel pkglocstr of
[(g, "")] -> do
files <- liftIO $ expandRelGlob rootdir g
let files' = filter ((== ".cabal") . takeExtension) files
if null files' then return Nothing else return (Just files')
_ -> return Nothing
mplusMaybeT :: Monad m => m (Maybe a) -> m (Maybe a) -> m (Maybe a)
mplusMaybeT ma mb = do
mx <- ma
case mx of
Nothing -> mb
Just x -> return (Just x)
configFromCabalFile
:: MonadIO m => Config -> Options -> FilePath -> YamlWriter m (Package, Set Version)
configFromCabalFile cfg opts cabalFile = do
gpd <- liftIO $ readGenericPackageDescription maxBound cabalFile
let compilers = testedWith $ packageDescription gpd
pkgNameStr = display $ Pkg.pkgName $ package $ packageDescription gpd
let unknownComps = nub [ c | (c,_) <- compilers, c /= GHC ]
ghcVerConstrs = [ vc | (GHC,vc) <- compilers ]
ghcVerConstrs' = simplifyVersionRange $ foldr unionVersionRanges noVersion ghcVerConstrs
twoDigitGhcVerConstrs = mapMaybe isTwoDigitGhcVersion ghcVerConstrs :: [Version]
specificGhcVers = nub $ mapMaybe isSpecificVersion ghcVerConstrs
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 knownGhcVersions'
| cfgLastInSeries cfg = filterLastMajor knownGhcVersions
| otherwise = knownGhcVersions
let testedGhcVersions = filter (`withinRange` ghcVerConstrs') knownGhcVersions'
when (null testedGhcVersions) $ do
putStrLnErr "no known GHC version is allowed by the 'tested-with' specification"
forM_ (optCollections opts) $ \c -> do
let v = collToGhcVer c
unless (v `elem` testedGhcVersions) $
putStrLnErr $ unlines
[ "collection " ++ c ++ " requires GHC " ++ display v
, "add 'tested-width: GHC == " ++ display v ++ "' to your .cabal file"
]
let pkg = Pkg pkgNameStr anyVersion (takeDirectory cabalFile) gpd
return (pkg, S.fromList testedGhcVersions)
where
lastStableGhcVers
= nubBy ((==) `on` ghcMajVer)
$ sortBy (flip compare)
$ filter (not . previewGHC . Just)
$ 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)
genTravisFromConfigs
:: Monad m
=> [String]
-> Options
-> Maybe FilePath
-> Config
-> Project Package
-> Set Version
-> YamlWriter m ()
genTravisFromConfigs argv opts isCabalProject config prj@Project { prjPackages = pkgs } versions' = do
let folds = cfgFolds config
putStrLnInfo $
"Generating Travis-CI config for testing for GHC versions: " ++ ghcVersions
unless (null $ cfgOsx config) $ do
putStrLnInfo $ "Also OSX jobs for: " ++ ghcOsxVersions
unless (S.null omittedOsxVersions) $
putStrLnWarn $ "Not all GHC versions specified with --osx are generated: " ++ ghcOmittedOsxVersions
tellStrLns
[ "# This Travis job script has been generated by a script via"
, "#"
, rawRow $ "# haskell-ci " ++ unwords [ "'" ++ a ++ "'" | a <- argv ]
, "#"
, "# For more information, see https://github.com/haskell-CI/haskell-ci"
, "#"
, rawRow $ "# version: " ++ CURRENT_PACKAGE_VERSION
, "#"
, "language: c"
, "dist: xenial"
, ""
, "git:"
, " submodules: false # whether to recursively clone submodules"
, ""
]
let projectName = fromMaybe (pkgName $ head pkgs) (cfgProjectName config)
unless (null $ cfgIrcChannels config) $ tellStrLnsRaw $
[ "notifications:"
, " irc:"
, " channels:"
] ++
[ " - \"" ++ chan ++ "\"" | chan <- cfgIrcChannels config ] ++
[ " skip_join: true"
, " template:"
, " - \"\\x0313" ++ projectName ++ "\\x03/\\x0306%{branch}\\x03 \\x0314%{commit}\\x03 %{build_url} %{message}\""
, ""
]
unless (null $ cfgOnlyBranches config) $ tellStrLnsRaw $
[ "branches:"
, " only:"
] ++
[ " - " ++ branch
| branch <- cfgOnlyBranches config
] ++
[ ""
]
when (cfgCache config) $ tellStrLns
[ "cache:"
, " directories:"
, " - $HOME/.cabal/packages"
, " - $HOME/.cabal/store"
]
when (cfgCache config && not (null (cfgOsx config))) $ tellStrLns
[ " - $HOME/.ghc-install"
]
tellStrLn ""
when (cfgPostgres config) $ tellStrLns
[ "services:"
, "- postgresql"
, "addons:"
, " postgresql: '10'"
, ""
]
when (cfgCache config) $ tellStrLns
[ "before_cache:"
, " - rm -fv $CABALHOME/packages/hackage.haskell.org/build-reports.log"
, " # remove files that are regenerated by 'cabal update'"
, " - rm -fv $CABALHOME/packages/hackage.haskell.org/00-index.*"
, " - rm -fv $CABALHOME/packages/hackage.haskell.org/*.json"
, " - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.cache"
, " - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar"
, " - rm -fv $CABALHOME/packages/hackage.haskell.org/01-index.tar.idx"
, ""
, " - rm -rfv $CABALHOME/packages/head.hackage"
, ""
]
tellStrLn "matrix:"
tellStrLn " include:"
let colls = [ (collToGhcVer cid,cid) | cid <- reverse $ optCollections opts ]
let tellJob :: Monad m => Bool -> Maybe Version -> YamlWriter m ()
tellJob osx gv = do
let cvs = dispGhcVersion $ gv >> cfgCabalInstallVersion config
gvs = dispGhcVersion gv
xpkgs' = concatMap (',':) (S.toList $ cfgApt config)
colls' = [ cid | (v,cid) <- colls, Just v == gv ]
tellStrLnsRaw $ catMaybes
[ Just $ " - compiler: \"ghc-" <> gvs <> "\""
, if | Just e <- gv >>= \v -> M.lookup v (cfgEnv config)
-> Just $ " env: " ++ e
| previewGHC gv -> Just $ " env: GHCHEAD=true"
| null colls' -> Nothing
| otherwise -> Just $ " env: 'COLLECTIONS=" ++ intercalate "," colls' ++ "'"
, Just $ " addons: {apt: {packages: [ghc-ppa-tools,cabal-install-" <> cvs <> ",ghc-" <> gvs <> xpkgs' <> "], sources: [hvr-ghc]}}"
]
when osx $ tellStrLnsRaw
[ " os: osx"
]
F.forM_ (reverse $ S.toList versions) $ tellJob False
F.forM_ (reverse $ S.toList osxVersions) $ tellJob True . Just
let allowFailures = headGhcVers `S.union` S.map Just (S.filter (`C.withinRange` cfgAllowFailures config) versions')
unless (S.null allowFailures) $ do
tellStrLn ""
tellStrLn " allow_failures:"
F.forM_ allowFailures $ \gv -> do
let gvs = dispGhcVersion gv
tellStrLn $ concat [ " - compiler: \"ghc-", gvs, "\"" ]
tellStrLns
[ ""
, "before_install:"
, sh "HC=/opt/ghc/bin/${CC}"
, sh' [2034,2039] "HCPKG=${HC/ghc/ghc-pkg}"
, sh "unset CC"
, sh "CABAL=/opt/ghc/bin/cabal"
, sh "CABALHOME=$HOME/.cabal"
, sh "export PATH=\"$CABALHOME/bin:$PATH\""
, sh "ROOTDIR=$(pwd)"
]
let haskellOnMacos = "https://haskell.futurice.com/haskell-on-macos.py"
unless (null (cfgOsx config)) $ tellStrLns
[ sh $ "if [ \"$TRAVIS_OS_NAME\" = \"osx\" ]; then brew update; brew upgrade python@3; curl " ++ haskellOnMacos ++ " | python3 - --make-dirs --install-dir=$HOME/.ghc-install --cabal-alias=head install cabal-install-head ${TRAVIS_COMPILER}; fi"
, sh' [2034,2039] "if [ \"$TRAVIS_OS_NAME\" = \"osx\" ]; then HC=$HOME/.ghc-install/ghc/bin/$TRAVIS_COMPILER; HCPKG=${HC/ghc/ghc-pkg}; CABAL=$HOME/.ghc-install/ghc/bin/cabal; fi"
]
tellStrLns
[ sh $ "HCNUMVER=$(( $(${HC} --numeric-version|sed -E 's/([0-9]+)\\.([0-9]+)\\.([0-9]+).*/\\1 * 10000 + \\2 * 100 + \\3/') ))"
, sh "echo $HCNUMVER"
]
unless (null colls) $
tellStrLn " - IFS=', ' read -a COLLS <<< \"$COLLECTIONS\""
tellStrLns
[ ""
, "install:"
, sh "${CABAL} --version"
, sh "echo \"$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]\""
, sh "TEST=--enable-tests"
, shForJob versions' (invertVersionRange $ cfgTests config) "TEST=--disable-tests"
, sh "BENCH=--enable-benchmarks"
, shForJob versions' (invertVersionRange $ cfgBenchmarks config) "BENCH=--disable-benchmarks"
, sh "GHCHEAD=${GHCHEAD-false}"
]
tellStrLns
[ sh "travis_retry ${CABAL} update -v"
, sh "sed -i.bak 's/^jobs:/-- jobs:/' $CABALHOME/config"
, sh "rm -fv cabal.project cabal.project.local"
]
case cfgJobs config >>= cabalJobs of
Just n -> tellStrLns
[ sh $ "sed -i.bak 's/^-- jobs:.*/jobs: " ++ show n ++ "/' $CABALHOME/config"
]
_ -> return ()
case cfgJobs config >>= ghcJobs of
Just m -> tellStrLns
[ shForJob versions' (orLaterVersion (mkVersion [7,8])) $
"sed -i.bak 's/-- ghc-options:.*/ghc-options: -j" ++ show m ++ "/' $CABALHOME/config"
]
_ -> return ()
unless (S.null headGhcVers) $ tellStrLns
[ " # Overlay Hackage Package Index for GHC HEAD: https://github.com/hvr/head.hackage"
, " - |"
, " if $GHCHEAD; then"
, " sed -i 's/-- allow-newer: .*/allow-newer: *:base/' $CABALHOME/config"
, " for pkg in $($HCPKG list --simple-output); do pkg=$(echo $pkg | sed 's/-[^-]*$//'); sed -i \"s/allow-newer: /allow-newer: *:$pkg, /\" $CABALHOME/config; done"
, ""
, " echo 'repository head.hackage' >> $CABALHOME/config"
, " echo ' url: http://head.hackage.haskell.org/' >> $CABALHOME/config"
, " echo ' secure: True' >> $CABALHOME/config"
, " echo ' root-keys: 07c59cb65787dedfaef5bd5f987ceb5f7e5ebf88b904bbd4c5cbdeb2ff71b740' >> $CABALHOME/config"
, " echo ' 2e8555dde16ebd8df076f1a8ef13b8f14c66bad8eafefd7d9e37d0ed711821fb' >> $CABALHOME/config"
, " echo ' 8f79fd2389ab2967354407ec852cbe73f2e8635793ac446d09461ffb99527f6e' >> $CABALHOME/config"
, " echo ' key-threshold: 3' >> $CABALHOME.config"
, ""
, " grep -Ev -- '^\\s*--' $CABALHOME/config | grep -Ev '^\\s*$'"
, ""
, " ${CABAL} new-update head.hackage -v"
, " fi"
]
tellStrLns
[ sh "grep -Ev -- '^\\s*--' $CABALHOME/config | grep -Ev '^\\s*$'"
]
let doctestVersionConstraint
| isAnyVersion (cfgDoctestVersion doctestConfig) = ""
| otherwise = " --constraint='doctest " ++ display (cfgDoctestVersion doctestConfig) ++ "'"
when (cfgDoctestEnabled doctestConfig) $ tellStrLns
[ shForJob versions' doctestJobVersionRange $ "${CABAL} new-install -w ${HC} -j2 doctest" ++ doctestVersionConstraint
]
let hlintVersionConstraint
| isAnyVersion (cfgHLintVersion hlintConfig) = ""
| otherwise = " --constraint='hlint " ++ display (cfgHLintVersion hlintConfig) ++ "'"
when (cfgHLintEnabled hlintConfig) $ tellStrLns
[ shForJob versions' (hlintJobVersionRange versions (cfgHLintJob hlintConfig)) $
"${CABAL} new-install -w ${HC} -j2 hlint" ++ hlintVersionConstraint
]
generateCabalProject False
let pkgFilter = intercalate " | " $ map (wrap.pkgName) pkgs
wrap s = "grep -Fv \"" ++ s ++ " ==\""
unless (null colls) $ tellStrLnsRaw
[ " - for COLL in \"${COLLS[@]}\"; do"
, " echo \"== collection $COLL ==\";"
, " ghc-travis collection ${COLL} > /dev/null || break;"
, " ghc-travis collection ${COLL} | " ++ pkgFilter ++ " > cabal.project.freeze;"
, " grep ' collection-id' cabal.project.freeze;"
, " rm -rf dist-newstyle/;"
, " $(CABAL} new-build -w ${HC} ${TEST} ${BENCH} --project-file=\"" ++ projectFile ++ "\" --dep -j2 all;"
, " done"
, ""
]
forM_ pkgs $ \Pkg{pkgDir} -> tellStrLns
[ sh $ "if [ -f \"" ++ pkgDir ++ "/configure.ac\" ]; then (cd \"" ++ pkgDir ++ "\" && autoreconf -i); fi"
]
let quotedRmPaths =
".ghc.environment.*"
++ " " ++
unwords (quotedPaths (\Pkg{pkgDir} -> pkgDir ++ "/dist"))
tellStrLns
[ sh $ "rm -f cabal.project.freeze"
]
when (cfgInstallDeps config) $ do
tellStrLns
[ sh $ "${CABAL} new-freeze -w ${HC} ${TEST} ${BENCH} --project-file=\"" ++ projectFile ++"\" --dry"
, sh $ "cat \"" ++ projectFile ++ ".freeze\" | sed -E 's/^(constraints: *| *)//' | sed 's/any.//'"
, sh $ "rm \"" ++ projectFile ++ ".freeze\""
, sh $ "${CABAL} new-build -w ${HC} ${TEST} ${BENCH} --project-file=\"" ++ projectFile ++"\" --dep -j2 all"
]
tellStrLns
[ shForJob versions' (cfgNoTestsNoBench config) $ "${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks --project-file=\"" ++ projectFile ++ "\" --dep -j2 all"
]
tellStrLns
[ sh $ "rm -rf " ++ quotedRmPaths
, sh $ "DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)"
]
tellStrLns
[ ""
, "# Here starts the actual work to be performed for the package under test;"
, "# any command which exits with a non-zero exit code causes the build to fail."
, "script:"
, " # test that source-distributions can be generated"
]
foldedTellStrLns FoldSDist "Packaging..." folds $ do
tellStrLns
[ sh $ "${CABAL} new-sdist all"
]
let tarFiles = "dist-newstyle" </> "sdist" </> "*.tar.gz"
foldedTellStrLns FoldUnpack "Unpacking..." folds $ do
tellStrLns
[ sh $ "mv " ++ tarFiles ++ " ${DISTDIR}/"
, sh $ "cd ${DISTDIR} || false"
, sh $ "find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \\;"
]
generateCabalProject True
unless (equivVersionRanges noVersion $ cfgNoTestsNoBench config) $ foldedTellStrLns FoldBuild "Building..." folds $ tellStrLns
[ comment "this builds all libraries and executables (without tests/benchmarks)"
, shForJob versions' (cfgNoTestsNoBench config) $ "${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks all"
]
tellStrLns [""]
foldedTellStrLns FoldBuildEverything
"Building with tests and benchmarks..." folds $ tellStrLns
[ comment "build & run tests, build benchmarks"
, sh "${CABAL} new-build -w ${HC} ${TEST} ${BENCH} all"
]
when hasTests $
foldedTellStrLns FoldTest "Testing..." folds $ tellStrLns
[ sh $ mconcat
[ "if [ \"x$TEST\" = \"x--enable-tests\" ]; then "
, if cfgNoise config
then "${CABAL} "
else "(set -o pipefail; ${CABAL} -vnormal+nowrap+markoutput "
, "new-test -w ${HC} ${TEST} ${BENCH} all"
, if cfgNoise config
then ""
else " 2>&1 | sed '/^-----BEGIN CABAL OUTPUT-----$/,/^-----END CABAL OUTPUT-----$/d' )"
, "; fi"
]
]
tellStrLns [""]
when (cfgDoctestEnabled doctestConfig) $ do
let doctestOptions = unwords $ cfgDoctestOptions doctestConfig
tellStrLns [ comment "doctest" ]
foldedTellStrLns FoldDoctest "Doctest..." folds $ do
forM_ pkgs $ \Pkg{pkgName,pkgGpd,pkgJobs} -> do
forM_ (doctestArgs pkgGpd) $ \args -> do
let args' = unwords args
unless (null args) $ tellStrLns
[ shForJob versions' (doctestJobVersionRange `intersectVersionRanges` pkgJobs) $
"(cd " ++ pkgName ++ "-* && doctest " ++ doctestOptions ++ " " ++ args' ++ ")"
]
tellStrLns [ "" ]
when (cfgHLintEnabled hlintConfig) $ do
let "" <+> ys = ys
xs <+> "" = xs
xs <+> ys = xs ++ " " ++ ys
prependSpace "" = ""
prependSpace xs = " " ++ xs
let hlintOptions = prependSpace $ maybe "" ("-h ${ROOTDIR}/" ++) (cfgHLintYaml hlintConfig) <+> unwords (cfgHLintOptions hlintConfig)
tellStrLns [ comment "hlint" ]
foldedTellStrLns FoldHLint "HLint.." folds $ do
forM_ pkgs $ \Pkg{pkgName,pkgGpd,pkgJobs} -> do
forM_ (hlintArgs pkgGpd) $ \args -> do
let args' = unwords args
unless (null args) $ tellStrLns
[ shForJob versions' (hlintJobVersionRange versions (cfgHLintJob hlintConfig) `intersectVersionRanges` pkgJobs) $
"(cd " ++ pkgName ++ "-* && hlint" ++ hlintOptions ++ " " ++ args' ++ ")"
]
tellStrLns [ "" ]
when (cfgCheck config) $
foldedTellStrLns FoldCheck "cabal check..." folds $ do
tellStrLns [ comment "cabal check" ]
forM_ pkgs $ \Pkg{pkgName,pkgJobs} -> tellStrLns
[ shForJob versions' pkgJobs $
"(cd " ++ pkgName ++ "-* && ${CABAL} check)"
]
tellStrLns [ "" ]
when (hasLibrary && not (equivVersionRanges noVersion $ cfgHaddock config)) $
foldedTellStrLns FoldHaddock "Haddock..." folds $ tellStrLns
[ comment "haddock"
, shForJob versions' (cfgHaddock config) "${CABAL} new-haddock -w ${HC} ${TEST} ${BENCH} all"
, ""
]
unless (null colls) $
foldedTellStrLns FoldStackage "Stackage builds..." folds $ tellStrLnsRaw
[ " # try building & testing for package collections"
, " - for COLL in \"${COLLS[@]}\"; do"
, " echo \"== collection $COLL ==\";"
, " ghc-travis collection ${COLL} > /dev/null || break;"
, " ghc-travis collection ${COLL} | " ++ pkgFilter ++ " > cabal.project.freeze;"
, " grep ' collection-id' cabal.project.freeze;"
, " rm -rf dist-newstyle/;"
, " ${CABAL} new-build -w ${HC} ${TEST} ${BENCH} all || break;"
, " if [ \"x$TEST\" = \"x--enable-tests\" ]; then ${CABAL} new-test -w ${HC} ${TEST} ${BENCH} all || break; fi;"
, " done"
, ""
]
unless (equivVersionRanges noVersion $ cfgUnconstrainted config) $ foldedTellStrLns FoldBuildInstalled
"Building without installed constraints for packages in global-db..." folds $ tellStrLns
[ comment "Build without installed constraints for packages in global-db"
, shForJob versions' (cfgUnconstrainted config) "rm -f cabal.project.local; ${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks all;"
, ""
]
let constraintSets = cfgConstraintSets config
unless (null constraintSets) $ do
tellStrLns
[ comment "Constraint sets"
, sh "rm -rf cabal.project.local"
, ""
]
forM_ constraintSets $ \cs -> do
let name = csName cs
let constraintFlags = concatMap (\x -> " --constraint='" ++ x ++ "'") (csConstraints cs)
let cmd | csRunTests cs = "${CABAL} new-test -w ${HC} --enable-tests --enable-benchmarks"
| otherwise = "${CABAL} new-build -w ${HC} --disable-tests --disable-benchmarks"
tellStrLns [ comment $ "Constraint set " ++ name ]
foldedTellStrLns' FoldConstraintSets name ("Constraint set " ++ name) folds $ tellStrLns
[ shForJob versions' (csGhcVersions cs) $
cmd ++ " " ++ constraintFlags ++ " all"
, ""
]
tellStrLns [""]
tellStrLnsRaw
[ "# REGENDATA " ++ show argv
, "# EOF"
]
return ()
where
doctestConfig = cfgDoctest config
hlintConfig = cfgHLint config
hasTests = F.any (\Pkg{pkgGpd} -> not . null $ condTestSuites pkgGpd) pkgs
hasLibrary = F.any (\Pkg{pkgGpd} -> isJust $ condLibrary pkgGpd) pkgs
headGhcVers = S.filter previewGHC versions
generateCabalProject dist = do
tellStrLns
[ sh "rm -f cabal.project"
, sh "touch cabal.project"
]
F.forM_ pkgs $ \pkg -> do
let p | dist = pkgName pkg ++ "-*/*.cabal"
| otherwise = pkgDir pkg
tellStrLns $
[ shForJob versions' (pkgJobs pkg) $ "printf 'packages: \"" ++ p ++ "\"\\n' >> cabal.project"
]
case cfgCopyFields config of
CopyFieldsNone -> return ()
CopyFieldsAll -> unless (null (prjOrigFields prj)) $ tellStrLns
[ sh $ "echo '" ++ l ++ "' >> cabal.project"
| l <- lines $ C.showFields' 2 $ prjOrigFields prj
, not (null l)
]
CopyFieldsSome -> do
F.forM_ (prjConstraints prj) $ \xs -> do
let s = concat (lines xs)
tellStrLns
[ sh $ "echo 'constraints: " ++ s ++ "' >> cabal.project"
]
F.forM_ (prjAllowNewer prj) $ \xs -> do
let s = concat (lines xs)
tellStrLns
[ sh $ "echo 'allow-newer: " ++ s ++ "' >> cabal.project"
]
unless (null (cfgLocalGhcOptions config)) $ forM_ pkgs $ \Pkg{pkgName} -> do
let s = unwords $ map (show . PU.showToken) $ cfgLocalGhcOptions config
tellStrLns
[ sh $ "echo 'package " ++ pkgName ++ "' >> cabal.project"
, sh $ "echo ' ghc-options: " ++ s ++ "' >> cabal.project"
]
when (prjReorderGoals prj) $
tellStrLns
[ sh $ "echo 'reorder-goals: True' >> cabal.project"
]
F.forM_ (prjMaxBackjumps prj) $ \bj ->
tellStrLns
[ sh $ "echo 'max-backjumps: " ++ show bj ++ "' >> cabal.project"
]
case prjOptimization prj of
OptimizationOn -> return ()
OptimizationOff -> tellStrLns [ sh $ "echo 'optimization: False' >> cabal.project " ]
OptimizationLevel l -> tellStrLns [ sh $ "echo 'optimization: " ++ show l ++ "' >> cabal.project " ]
F.forM_ (prjSourceRepos prj) $ \repo -> do
let repo' = PP.render $ C.prettyFieldGrammar (C.sourceRepoFieldGrammar $ C.RepoKindUnknown "unused") repo
tellStrLns [ sh $ "echo 'source-repository-package' >> cabal.project" ]
tellStrLns [ sh $ "echo ' " ++ l ++ "' >> cabal.project" | l <- lines repo' ]
tellStrLns
[ sh $ "printf 'write-ghc-environment-files: always\\n' >> cabal.project"
]
unless (null (cfgRawProject config)) $ tellStrLns
[ sh $ "echo '" ++ l ++ "' >> cabal.project"
| l <- lines $ C.showFields' 2 $ cfgRawProject config
, not (null l)
]
case normaliseInstalled (cfgInstalled config) of
InstalledDiff pns -> tellStrLns
[ sh $ "touch cabal.project.local"
, sh $ unwords
[ "for pkg in $($HCPKG list --simple-output); do"
, "echo $pkg"
, "| sed 's/-[^-]*$//'"
, "| grep -vE -- " ++ re
, "| sed 's/^/constraints: /'"
, "| sed 's/$/ installed/'"
, ">> cabal.project.local; done"
]
]
where
pns' = S.map unPackageName pns `S.union` foldMap (S.singleton . pkgName) pkgs
re = "'^(" ++ intercalate "|" (S.toList pns') ++ ")$'"
InstalledOnly pns | not (null pns') -> tellStrLns
[ sh $ "touch cabal.project.local"
, sh' [2043] $ unwords
[ "for pkg in " ++ unwords (S.toList pns') ++ "; do"
, "echo \"constraints: $pkg installed\" >> cabal.project"
, ">> cabal.project.local; done"
]
]
where
pns' = S.map unPackageName pns `S.difference` foldMap (S.singleton . pkgName) pkgs
_ -> pure ()
tellStrLns
[ sh $ "cat cabal.project || true"
, sh $ "cat cabal.project.local || true"
]
projectFile :: FilePath
projectFile = fromMaybe "cabal.project" isCabalProject
quotedPaths :: (Package -> FilePath) -> [String]
quotedPaths f = map (f . quote) pkgs
where
quote pkg = pkg{ pkgDir = "\"" ++ pkgDir pkg ++ "\"" }
showVersions :: Set (Maybe Version) -> String
showVersions = unwords . map dispGhcVersion . S.toList
osxVersions' :: Set Version
osxVersions' = cfgOsx config
versions :: Set (Maybe Version)
versions
| cfgGhcHead config = S.insert Nothing $ S.map Just versions'
| otherwise = S.map Just versions'
ghcVersions :: String
ghcVersions = showVersions versions
osxVersions, omittedOsxVersions :: Set Version
(osxVersions, omittedOsxVersions) = S.partition (`S.member` versions') osxVersions'
ghcOsxVersions :: String
ghcOsxVersions = showVersions $ S.map Just osxVersions
ghcOmittedOsxVersions :: String
ghcOmittedOsxVersions = showVersions $ S.map Just omittedOsxVersions
collToGhcVer :: String -> Version
collToGhcVer cid = case simpleParse cid of
Nothing -> error ("invalid collection-id syntax " ++ show cid)
Just (PackageIdentifier n (versionNumbers -> v))
| display n /= "lts" -> error ("unknown collection " ++ show cid)
| isPrefixOf [0] v -> mkVersion [7,8,3]
| isPrefixOf [1] v -> mkVersion [7,8,4]
| isPrefixOf [2] v -> mkVersion [7,8,4]
| isPrefixOf [3] v -> mkVersion [7,10,2]
| isPrefixOf [4] v -> mkVersion [7,10,3]
| isPrefixOf [5] v -> mkVersion [7,10,3]
| isPrefixOf [6] v -> mkVersion [7,10,3]
| isPrefixOf [7] v -> mkVersion [8,0,1]
| otherwise -> error ("unknown collection " ++ show cid)
doctestJobVersionRange :: VersionRange
doctestJobVersionRange = orLaterVersion $ mkVersion [8,0]
doctestArgs :: GenericPackageDescription -> [[String]]
doctestArgs gpd =
[ libraryModuleArgs c
| c <- flattenPackageDescription gpd ^.. L.library . traverse
] ++
[ libraryModuleArgs c
| c <- flattenPackageDescription gpd ^.. L.subLibraries . traverse
]
libraryModuleArgs :: PD.Library -> [String]
libraryModuleArgs l
| null dirsOrMods = []
| otherwise = exts ++ dirsOrMods
where
bi = l ^. L.buildInfo
dirsOrMods
| null (PD.hsSourceDirs bi) = map display (PD.exposedModules l)
| otherwise = PD.hsSourceDirs bi
exts = map (("-X" ++) . display) (PD.defaultExtensions bi)
executableModuleArgs :: PD.Executable -> [String]
executableModuleArgs e
| null dirsOrMods = []
| otherwise = exts ++ dirsOrMods
where
bi = e ^. L.buildInfo
dirsOrMods
| null (PD.hsSourceDirs bi) = map display (PD.otherModules bi)
| otherwise = PD.hsSourceDirs bi
exts = map (("-X" ++) . display) (PD.defaultExtensions bi)
hlintJobVersionRange :: Set (Maybe Version) -> HLintJob -> VersionRange
hlintJobVersionRange vs HLintJobLatest = case S.maxView vs of
Just (Just v, _) -> thisVersion v
_ -> thisVersion $ mkVersion [8,6,3]
hlintJobVersionRange _ (HLintJob v) = thisVersion v
hlintArgs :: GenericPackageDescription -> [[String]]
hlintArgs gpd =
[ libraryModuleArgs c
| c <- flattenPackageDescription gpd ^.. L.library . traverse
] ++
[ libraryModuleArgs c
| c <- flattenPackageDescription gpd ^.. L.subLibraries . traverse
] ++
[ executableModuleArgs c
| c <- flattenPackageDescription gpd ^.. L.executables . traverse
]