haskell-ci-0.12: src/HaskellCI/Bash.hs
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -Wno-unused-matches #-}
module HaskellCI.Bash (
makeBash
) where
import HaskellCI.Prelude
import qualified Data.Set as S
import qualified Distribution.Fields.Pretty as C
import qualified Distribution.Package as C
import qualified Distribution.Types.VersionRange as C
import qualified Distribution.Version as C
import Cabal.Project
import HaskellCI.Auxiliary
import HaskellCI.Bash.Template
import HaskellCI.Compiler
import HaskellCI.Config
import HaskellCI.Config.ConstraintSet
import HaskellCI.Config.Doctest
import HaskellCI.Config.Installed
import HaskellCI.Config.PackageScope
import HaskellCI.Jobs
import HaskellCI.List
import HaskellCI.Package
import HaskellCI.Sh
import HaskellCI.ShVersionRange
import HaskellCI.Tools
{-
Bash-specific notes:
* We use -j for parallelism, as the exact number of cores depends on the
particular machine the script is being run on.
-}
makeBash
:: [String]
-> Config
-> Project URI Void Package
-> JobVersions
-> Either ShError Z -- TODO: writer
makeBash _argv config@Config {..} prj jobs@JobVersions {..} = do
blocks <- traverse (fmap shsToList) $ buildList $ do
-- install doctest
when doctestEnabled $ step "install doctest" $ do
let range = Range (cfgDoctestEnabled cfgDoctest) /\ doctestJobVersionRange
comment "install doctest"
run_cmd_if range "$CABAL v2-install $ARG_COMPILER --ignore-project -j doctest --constraint='doctest ^>=0.17'"
run_cmd_if range "doctest --version"
-- install hlint
-- TODO
-- autoreconf ...
-- hmm, source is read-only...
step "initial cabal.project for sdist" $ do
change_dir "$BUILDDIR"
run_cmd "touch cabal.project"
for_ pkgs $ \pkg ->
echo_if_to (RangePoints $ pkgJobs pkg) "cabal.project" $ "packages: $SRCDIR/" ++ pkgDir pkg
run_cmd "cat cabal.project"
-- sdist
step "sdist" $ do
run_cmd "mkdir -p \"$BUILDDIR/sdist\""
-- TODO: check if cabal-install-3.4 can be run on read only system
run_cmd "$CABAL sdist all --output-dir \"$BUILDDIR/sdist\""
-- find and unpack sdist
step "unpack" $ do
change_dir "$BUILDDIR"
run_cmd "mkdir -p \"$BUILDDIR/unpacked\""
run_cmd "find \"$BUILDDIR/sdist\" -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C \"$BUILDDIR/unpacked\" -xzvf {} \\;"
-- write cabal.project
step "generate cabal.project" $ do
for_ pkgs $ \Pkg{pkgName} -> do
sh $ pkgNameDirVariable' pkgName ++ "=\"$(find \"$BUILDDIR/unpacked\" -maxdepth 1 -type d -regex '.*/" ++ pkgName ++ "-[0-9.]*')\""
run_cmd "touch cabal.project"
run_cmd "touch cabal.project.local"
for_ pkgs $ \pkg ->
echo_if_to (RangePoints $ pkgJobs pkg) "cabal.project" $ "packages: " ++ pkgNameDirVariable (pkgName pkg)
-- per package options
case cfgErrorMissingMethods of
PackageScopeNone -> pure ()
PackageScopeLocal -> for_ pkgs $ \Pkg{pkgName,pkgJobs} -> do
let range = Range (C.orLaterVersion (C.mkVersion [8,2])) /\ RangePoints pkgJobs
echo_if_to range "cabal.project" $ "package " ++ pkgName
echo_if_to range "cabal.project" $ " ghc-options: -Werror=missing-methods"
PackageScopeAll -> cat "cabal.project" $ unlines
[ "package *"
, " ghc-options: -Werror=missing-methods"
]
-- extra cabal.project fields
cat "cabal.project" $ C.showFields' (const []) (const id) 2 extraCabalProjectFields
-- also write cabal.project.local file with
-- @
-- constraints: base installed
-- constraints: array installed
-- ...
--
-- omitting any local package names
case normaliseInstalled cfgInstalled of
InstalledDiff pns -> sh $ unwords
[ "$HCPKG list --simple-output --names-only"
, "| perl -ne 'for (split /\\s+/) { print \"constraints: $_ installed\\n\" unless /" ++ re ++ "/; }'"
, ">> cabal.project.local"
]
where
pns' = S.map C.unPackageName pns `S.union` foldMap (S.singleton . pkgName) pkgs
re = "^(" ++ intercalate "|" (S.toList pns') ++ ")$"
InstalledOnly pns | not (null pns') -> cat "cabal.project.local" $ unlines
[ "constraints: " ++ pkg ++ " installed"
| pkg <- S.toList pns'
]
where
pns' = S.map C.unPackageName pns `S.difference` foldMap (S.singleton . pkgName) pkgs
-- otherwise: nothing
_ -> pure ()
run_cmd "cat cabal.project"
run_cmd "cat cabal.project.local"
-- dump install plan
step "dump install plan" $ do
run_cmd "$CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all"
run_cmd "cabal-plan"
-- install dependencies
when cfgInstallDeps $ step "install dependencies" $ do
run_cmd "$CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j all"
run_cmd "$CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j all"
unless (equivVersionRanges C.noVersion cfgNoTestsNoBench) $ step "build w/o tests" $ do
run_cmd "$CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all"
-- build
step "build" $ do
run_cmd "$CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all"
-- tests
step "tests" $ do
let range = RangeGHC /\ Range (cfgTests /\ cfgRunTests) /\ hasTests
run_cmd_if range $ "$CABAL v2-test $ARG_COMPILER $ARG_TESTS $ARG_BENCH all" ++ testShowDetails
-- doctest
when doctestEnabled $ step "doctest" $ do
let doctestOptions = unwords $ cfgDoctestOptions cfgDoctest
unless (null $ cfgDoctestFilterEnvPkgs cfgDoctest) $ do
-- cabal-install mangles unit ids on the OSX,
-- removing the vowels to make filepaths shorter
let manglePkgNames :: String -> [String]
manglePkgNames n
| null cfgOsx = [n]
| otherwise = [n, filter notVowel n]
where
notVowel c = notElem c ("aeiou" :: String)
let filterPkgs = intercalate "|" $ concatMap (manglePkgNames . C.unPackageName) $ cfgDoctestFilterEnvPkgs cfgDoctest
run_cmd $ "perl -i -e 'while (<ARGV>) { print unless /package-id\\s+(" ++ filterPkgs ++ ")-\\d+(\\.\\d+)*/; }' .ghc.environment.*"
for_ pkgs $ \Pkg{pkgName,pkgGpd,pkgJobs} ->
when (C.mkPackageName pkgName `notElem` cfgDoctestFilterSrcPkgs cfgDoctest) $ do
for_ (doctestArgs pkgGpd) $ \args -> do
let args' = unwords args
let vr = Range (cfgDoctestEnabled cfgDoctest)
/\ doctestJobVersionRange
/\ RangePoints pkgJobs
unless (null args) $ do
change_dir_if vr $ pkgNameDirVariable pkgName
run_cmd_if vr $ "doctest " ++ doctestOptions ++ " " ++ args'
-- hlint
-- TODO
-- cabal check
when cfgCheck $ step "cabal check" $ do
for_ pkgs $ \Pkg{pkgName,pkgJobs} -> do
let range = RangePoints pkgJobs
change_dir_if range $ pkgNameDirVariable pkgName
run_cmd_if range "${CABAL} -vnormal check"
change_dir "$BUILDDIR"
-- haddock
when (hasLibrary && not (equivVersionRanges C.noVersion cfgHaddock)) $ step "haddock" $ do
let range = RangeGHC /\ Range cfgHaddock
run_cmd_if range "$CABAL v2-haddock $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all"
-- unconstrained build
unless (equivVersionRanges C.noVersion cfgUnconstrainted) $ step "unconstrained build" $ do
let range = Range cfgUnconstrainted
run_cmd_if range "rm -f cabal.project.local"
run_cmd_if range "$CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all"
-- constraint sets
unless (null cfgConstraintSets) $ step "constraint sets" $ do
run_cmd "rm -f cabal.project.local"
for_ cfgConstraintSets $ \cs -> do
let name = csName cs
let run_cmd_cs = run_cmd_if (Range (csGhcVersions cs))
let run_cmd_cs' r = run_cmd_if (Range (csGhcVersions cs) /\ r)
let testFlag = if csTests cs then "--enable-tests" else "--disable-tests"
let benchFlag = if csBenchmarks cs then "--enable-benchmarks" else "--disable-benchmarks"
let constraintFlags = map (\x -> "--constraint='" ++ x ++ "'") (csConstraints cs)
let allFlags = unwords (testFlag : benchFlag : constraintFlags)
put_info $ "constraint set " ++ name
run_cmd_cs $ "$CABAL v2-build $ARG_COMPILER " ++ allFlags ++ " all"
when (csRunTests cs) $
run_cmd_cs' hasTests $ "$CABAL v2-test $ARG_COMPILER " ++ allFlags ++ " all"
when (hasLibrary && csHaddock cs) $
run_cmd_cs $ "$CABAL v2-haddock $ARG_COMPILER " ++ withHaddock ++ " " ++ allFlags ++ " all"
return defaultZ
{ zJobs =
[ prettyShow v
| GHC v <- reverse $ toList versions
]
, zBlocks = blocks
, zApt = S.toList cfgApt
, zTestsCond = compilerVersionArithPredicate versions $ Range cfgTests
, zBenchCond = compilerVersionArithPredicate versions $ Range cfgBenchmarks
}
where
Auxiliary {..} = auxiliary config prj jobs
-- TODO: this can be smart and do nothing is there are only comments in [Sh]
step :: String -> ShM () -> ListBuilder (Either ShError [Sh]) ()
step name action = item $ runSh $ do
comment name
put_info name
action
put_info :: String -> ShM ()
put_info s = sh $ "put_info " ++ show s
change_dir :: String -> ShM ()
change_dir dir = sh $ "change_dir " ++ show dir
change_dir_if :: CompilerRange -> String -> ShM ()
change_dir_if vr dir
| all (`compilerWithinRange` vr) versions = change_dir dir
| not $ any (`compilerWithinRange` vr) versions = pure ()
| otherwise = sh $ unwords
[ "change_dir_if"
, compilerVersionArithPredicate versions vr
, dir
]
run_cmd :: String -> ShM ()
run_cmd cmd = sh $ unwords
[ "run_cmd"
, cmd
]
run_cmd_if :: CompilerRange -> String -> ShM ()
run_cmd_if vr cmd
| all (`compilerWithinRange` vr) versions = run_cmd cmd
| not $ any (`compilerWithinRange` vr) versions = pure ()
| otherwise = sh $ unwords
[ "run_cmd_if"
, compilerVersionArithPredicate versions vr
, cmd
]
echo_if_to :: CompilerRange -> FilePath -> String -> ShM ()
echo_if_to vr path contents
| all (`compilerWithinRange` vr) versions = sh $ unwords
[ "echo_to"
, path
, show contents
]
| not $ any (`compilerWithinRange` vr) versions = pure ()
| otherwise = sh $ unwords
[ "echo_if_to"
, compilerVersionArithPredicate versions vr
, path
, show contents
]
-- Needed to work around haskell/cabal#6214
withHaddock :: String
withHaddock = "--with-haddock $HADDOCK"
shsToList :: [Sh] -> String
shsToList = unlines . map f where
f (Sh x) = x
f (Comment c) = "# " ++ c
cat :: FilePath -> String -> ShM ()
cat path contents = sh $ concat
[ "cat >> " ++ path ++ " <<EOF\n"
, contents
, "EOF"
]