diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index d54310be613..116537cf2ea 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -17,3 +17,4 @@ jobs: Cabal/**/*.hs Cabal-syntax/**/*.hs cabal-install/**/*.hs + cabal-validate/**/*.hs diff --git a/Makefile b/Makefile index e0b239d80ad..12d38557de6 100644 --- a/Makefile +++ b/Makefile @@ -29,16 +29,16 @@ init: ## Set up git hooks and ignored revisions .PHONY: style style: ## Run the code styler - @fourmolu -q -i Cabal Cabal-syntax cabal-install + @fourmolu -q -i Cabal Cabal-syntax cabal-install cabal-validate .PHONY: style-modified style-modified: ## Run the code styler on modified files - @git ls-files --modified Cabal Cabal-syntax cabal-install \ + @git ls-files --modified Cabal Cabal-syntax cabal-install cabal-validate \ | grep '.hs$$' | xargs -P $(PROCS) -I {} fourmolu -q -i {} .PHONY: style-commit style-commit: ## Run the code styler on the previous commit - @git diff --name-only HEAD $(COMMIT) Cabal Cabal-syntax cabal-install \ + @git diff --name-only HEAD $(COMMIT) Cabal Cabal-syntax cabal-install cabal-validate \ | grep '.hs$$' | xargs -P $(PROCS) -I {} fourmolu -q -i {} .PHONY: whitespace diff --git a/cabal-validate/README.md b/cabal-validate/README.md new file mode 100644 index 00000000000..5f40e9d28f1 --- /dev/null +++ b/cabal-validate/README.md @@ -0,0 +1,23 @@ +# cabal-validate + +`cabal-validate` is a script that builds and tests `Cabal` and `cabal-install`. +`cabal-validate` can be run with `validate.sh` in the repository root; +arguments passed to `validate.sh` will be forwarded to `cabal-validate`. + +Notable arguments include: + +- `-v`/`--verbose` to display build and test output in real-time, instead of + only if commands fail. +- `-s`/`--step` to run a specific step (e.g. `-s build -s lib-tests` will only + run the `build` and `lib-tests` steps). +- `-p`/`--pattern` to filter tests by a pattern. + +## Hacking on cabal-validate + +Overview of important modules: + +- `Main.hs` encodes all the commands that are run for each step. +- `Cli.hs` parses the CLI arguments and resolves default values from the + environment, like determining which steps are run by default or the `--jobs` + argument to pass to test suites. +- `Step.hs` lists the available steps. diff --git a/cabal-validate/cabal-validate.cabal b/cabal-validate/cabal-validate.cabal new file mode 100644 index 00000000000..582cf67434a --- /dev/null +++ b/cabal-validate/cabal-validate.cabal @@ -0,0 +1,47 @@ +cabal-version: 3.0 +name: cabal-validate +version: 1.0.0 +copyright: 2024-2024, Cabal Development Team (see AUTHORS file) +license: BSD-3-Clause +author: Cabal Development Team +synopsis: An internal tool for building and testing the Cabal package manager +build-type: Simple + +common common + ghc-options: -Wall + + if impl(ghc <9.6) + -- Pattern exhaustiveness checker is not as good, misses a case. + ghc-options: -Wno-incomplete-patterns + + default-language: Haskell2010 + default-extensions: + OverloadedStrings + , TypeApplications + +executable cabal-validate + import: common + ghc-options: -O -threaded -rtsopts -with-rtsopts=-N + + main-is: Main.hs + hs-source-dirs: src + + other-modules: + , ANSI + , Cli + , ClockUtil + , OutputUtil + , ProcessUtil + , Step + + build-depends: + , base >=4 && <5 + , bytestring >=0.11 && <1 + , containers >=0.6 && <1 + , directory >=1.0 && <2 + , filepath >=1 && <2 + , optparse-applicative >=0.18 && <1 + , terminal-size >=0.3 && <1 + , text >=2 && <3 + , time >=1 && <2 + , typed-process >=0.2 && <1 diff --git a/cabal-validate/src/ANSI.hs b/cabal-validate/src/ANSI.hs new file mode 100644 index 00000000000..a0d9111d957 --- /dev/null +++ b/cabal-validate/src/ANSI.hs @@ -0,0 +1,105 @@ +-- | ANSI escape sequences. +-- +-- This is a stripped-down version of the parts of the @ansi-terminal@ package +-- we use. +-- +-- See: +module ANSI + ( SGR (..) + , setSGR + ) where + +-- | Render a single numeric SGR sequence. +rawSGR :: Int -> String +rawSGR code = "\x1b[" <> show code <> "m" + +-- | Render a series of `SGR` escape sequences. +setSGR :: [SGR] -> String +setSGR = concat . map renderSGR + +-- | All of the SGR sequences we want to use. +data SGR + = Reset + | Bold + | Dim + | Italic + | Underline + | Black + | Red + | Green + | Yellow + | Blue + | Magenta + | Cyan + | White + | Default + | OnBlack + | OnRed + | OnGreen + | OnYellow + | OnBlue + | OnMagenta + | OnCyan + | OnWhite + | OnDefault + | BrightBlack + | BrightRed + | BrightGreen + | BrightYellow + | BrightBlue + | BrightMagenta + | BrightCyan + | BrightWhite + | OnBrightBlack + | OnBrightRed + | OnBrightGreen + | OnBrightYellow + | OnBrightBlue + | OnBrightMagenta + | OnBrightCyan + | OnBrightWhite + deriving (Show) + +-- Render a single `SGR` sequence. +renderSGR :: SGR -> String +renderSGR code = + case code of + Reset -> rawSGR 0 + Bold -> rawSGR 1 + Dim -> rawSGR 2 + Italic -> rawSGR 3 + Underline -> rawSGR 4 + Black -> rawSGR 30 + Red -> rawSGR 31 + Green -> rawSGR 32 + Yellow -> rawSGR 33 + Blue -> rawSGR 34 + Magenta -> rawSGR 35 + Cyan -> rawSGR 36 + White -> rawSGR 37 + Default -> rawSGR 39 + OnBlack -> rawSGR 40 + OnRed -> rawSGR 41 + OnGreen -> rawSGR 42 + OnYellow -> rawSGR 43 + OnBlue -> rawSGR 44 + OnMagenta -> rawSGR 45 + OnCyan -> rawSGR 46 + OnWhite -> rawSGR 47 + OnDefault -> rawSGR 49 + BrightBlack -> rawSGR 90 + BrightRed -> rawSGR 91 + BrightGreen -> rawSGR 92 + BrightYellow -> rawSGR 93 + BrightBlue -> rawSGR 94 + BrightMagenta -> rawSGR 95 + BrightCyan -> rawSGR 96 + BrightWhite -> rawSGR 97 + OnBrightBlack -> rawSGR 100 + OnBrightRed -> rawSGR 101 + OnBrightGreen -> rawSGR 102 + OnBrightYellow -> rawSGR 103 + OnBrightBlue -> rawSGR 104 + OnBrightMagenta -> rawSGR 105 + OnBrightCyan -> rawSGR 106 + OnBrightWhite -> rawSGR 107 diff --git a/cabal-validate/src/Cli.hs b/cabal-validate/src/Cli.hs new file mode 100644 index 00000000000..ef01d907594 --- /dev/null +++ b/cabal-validate/src/Cli.hs @@ -0,0 +1,437 @@ +-- | Parse CLI arguments and resolve defaults from the environment. +module Cli + ( Opts (..) + , parseOpts + , HackageTests (..) + , Compiler (..) + , VersionParseException (..) + ) +where + +import Control.Applicative (Alternative (many, (<|>)), (<**>)) +import Control.Exception (Exception (displayException), throw) +import Control.Monad (forM_, when) +import Data.Data (Typeable) +import Data.Maybe (listToMaybe) +import qualified Data.Text as T +import qualified Data.Text.Lazy as T (toStrict) +import qualified Data.Text.Lazy.Encoding as T (decodeUtf8) +import Data.Version (Version, parseVersion) +import GHC.Conc (getNumCapabilities) +import Options.Applicative + ( FlagFields + , Mod + , Parser + , ParserInfo + , auto + , execParser + , flag + , flag' + , fullDesc + , help + , helper + , hidden + , info + , long + , maybeReader + , option + , progDesc + , short + , strOption + , switch + , value + ) +import qualified Options.Applicative as Opt +import System.Directory (getCurrentDirectory) +import System.Exit (exitSuccess) +import System.Info (arch, os) +import System.Process.Typed (proc, readProcessStdout_) +import Text.ParserCombinators.ReadP (readP_to_S) + +import ClockUtil (AbsoluteTime, getAbsoluteTime) +import Step (Step (..), displayStep, parseStep) + +-- | Command-line options, resolved with context from the environment. +data Opts = Opts + { verbose :: Bool + -- ^ Whether to display build and test output. + , jobs :: Int + -- ^ How many jobs to use when running tests. + -- + -- Defaults to the number of physical cores. + , cwd :: FilePath + -- ^ Current working directory when @cabal-validate@ was started. + , startTime :: AbsoluteTime + -- ^ System time when @cabal-validate@ was started. + -- + -- Used to determine the total test duration so far. + , compiler :: Compiler + -- ^ Compiler to build Cabal with. + -- + -- Defaults to @ghc@. + , extraCompilers :: [FilePath] + -- ^ Extra compilers to run @cabal-testsuite@ with. + , cabal :: FilePath + -- ^ @cabal-install@ to build Cabal with. + -- + -- Defaults to @cabal@. + , hackageTests :: HackageTests + -- ^ Whether to run tests on Hackage data, and if so how much. + -- + -- Defaults to `NoHackageTests`. + , archPath :: FilePath + -- ^ The path for this system's architecture within the build directory. + -- + -- Like @x86_64-windows@ or @aarch64-osx@ or @arm-linux@. + , projectFile :: FilePath + -- ^ Path to the @cabal.project@ file to use for running tests. + , tastyArgs :: [String] + -- ^ Extra arguments to pass to @tasty@ test suites. + -- + -- This defaults to @--hide-successes@ (which cannot yet be changed) and + -- includes the @--pattern@ argument if one is given. + , targets :: [String] + -- ^ Targets to build. + , steps :: [Step] + -- ^ Steps to run. + } + deriving (Show) + +-- | Whether to run tests on Hackage data, and if so how much. +data HackageTests + = -- | Run tests on complete Hackage data. + CompleteHackageTests + | -- | Run tests on partial Hackage data. + PartialHackageTests + | -- | Do not run tests on Hackage data. + NoHackageTests + deriving (Show) + +-- | A compiler executable and version number. +data Compiler = Compiler + { compilerExecutable :: FilePath + -- ^ The compiler's executable. + , compilerVersion :: Version + -- ^ The compiler's version number. + } + deriving (Show) + +-- | An `Exception` thrown when parsing @--numeric-version@ output from a compiler. +data VersionParseException = VersionParseException + { versionInput :: String + -- ^ The string we attempted to parse. + , versionExecutable :: FilePath + -- ^ The compiler which produced the string. + } + deriving (Typeable, Show) + +instance Exception VersionParseException where + displayException exception = + "Failed to parse `" + <> versionExecutable exception + <> " --numeric-version` output: " + <> show (versionInput exception) + +-- | Runs @ghc --numeric-version@ for the given executable to construct a +-- `Compiler`. +makeCompiler :: FilePath -> IO Compiler +makeCompiler executable = do + stdout <- + readProcessStdout_ $ + proc executable ["--numeric-version"] + let version = T.unpack $ T.strip $ T.toStrict $ T.decodeUtf8 stdout + parsedVersions = readP_to_S parseVersion version + -- Who needs error messages? Those aren't in the API. + maybeParsedVersion = + listToMaybe + [ parsed + | (parsed, []) <- parsedVersions + ] + parsedVersion = case maybeParsedVersion of + Just parsedVersion' -> parsedVersion' + Nothing -> + throw + VersionParseException + { versionInput = version + , versionExecutable = executable + } + + pure + Compiler + { compilerExecutable = executable + , compilerVersion = parsedVersion + } + +-- | Resolve options and default values from the environment. +-- +-- This makes the `Opts` type much nicer to deal with than `RawOpts`. +resolveOpts :: RawOpts -> IO Opts +resolveOpts opts = do + let optionals :: Bool -> [a] -> [a] + optionals True items = items + optionals False _ = [] + + optional :: Bool -> a -> [a] + optional keep item = optionals keep [item] + + steps' = + if not (null (rawSteps opts)) + then rawSteps opts + else + concat + [ + [ PrintConfig + , PrintToolVersions + , Build + ] + , optional (rawDoctest opts) Doctest + , optional (rawRunLibTests opts) LibTests + , optional (rawRunLibSuite opts) LibSuite + , optional (rawRunLibSuite opts && not (null (rawExtraCompilers opts))) LibSuiteExtras + , optional (rawRunCliTests opts && not (rawLibOnly opts)) CliTests + , optional (rawRunCliSuite opts && not (rawLibOnly opts)) CliSuite + , optionals (rawSolverBenchmarks opts) [SolverBenchmarksTests, SolverBenchmarksRun] + , [TimeSummary] + ] + + targets' = + concat + [ + [ "Cabal" + , "Cabal-hooks" + , "cabal-testsuite" + , "Cabal-tests" + , "Cabal-QuickCheck" + , "Cabal-tree-diff" + , "Cabal-described" + ] + , optionals + (not (rawLibOnly opts)) + [ "cabal-install" + , "cabal-install-solver" + , "cabal-benchmarks" + ] + , optionals + (rawSolverBenchmarks opts) + [ "solver-benchmarks" + , "solver-benchmarks:tests" + ] + ] + + archPath' = + let osPath = + case os of + "darwin" -> "osx" + "linux" -> "linux" + "mingw32" -> "windows" + _ -> os -- TODO: Warning? + in arch <> "-" <> osPath + + projectFile' = + if rawLibOnly opts + then "cabal.validate-libonly.project" + else "cabal.validate.project" + + tastyArgs' = + "--hide-successes" + : case rawTastyPattern opts of + Just tastyPattern -> ["--pattern", tastyPattern] + Nothing -> [] + + when (rawListSteps opts) $ do + -- TODO: This should probably list _all_ available steps, not just the selected ones! + putStrLn "Targets:" + forM_ targets' $ \target -> do + putStrLn $ " " <> target + putStrLn "Steps:" + forM_ steps' $ \step -> do + putStrLn $ " " <> displayStep step + exitSuccess + + startTime' <- getAbsoluteTime + jobs' <- maybe getNumCapabilities pure (rawJobs opts) + cwd' <- getCurrentDirectory + compiler' <- makeCompiler (rawCompiler opts) + + pure + Opts + { verbose = rawVerbose opts + , jobs = jobs' + , cwd = cwd' + , startTime = startTime' + , compiler = compiler' + , extraCompilers = rawExtraCompilers opts + , cabal = rawCabal opts + , archPath = archPath' + , projectFile = projectFile' + , hackageTests = rawHackageTests opts + , tastyArgs = tastyArgs' + , targets = targets' + , steps = steps' + } + +-- | Literate command-line options as supplied by the user, before resolving +-- defaults and other values from the environment. +data RawOpts = RawOpts + { rawVerbose :: Bool + , rawJobs :: Maybe Int + , rawCompiler :: FilePath + , rawCabal :: FilePath + , rawExtraCompilers :: [FilePath] + , rawTastyPattern :: Maybe String + , rawDoctest :: Bool + , rawSteps :: [Step] + , rawListSteps :: Bool + , rawLibOnly :: Bool + , rawRunLibTests :: Bool + , rawRunCliTests :: Bool + , rawRunLibSuite :: Bool + , rawRunCliSuite :: Bool + , rawSolverBenchmarks :: Bool + , rawHackageTests :: HackageTests + } + deriving (Show) + +-- | `Parser` for `RawOpts`. +-- +-- See: `fullRawOptsParser` +rawOptsParser :: Parser RawOpts +rawOptsParser = + RawOpts + <$> ( flag' + True + ( short 'v' + <> long "verbose" + <> help "Always display build and test output" + ) + <|> flag + False + False + ( short 'q' + <> long "quiet" + <> help "Silence build and test output" + ) + ) + <*> option + (Just <$> auto) + ( short 'j' + <> long "jobs" + <> help "Passed to `cabal build --jobs`" + <> value Nothing + ) + <*> strOption + ( short 'w' + <> long "with-compiler" + <> help "Build Cabal with the given compiler instead of `ghc`" + <> value "ghc" + ) + <*> strOption + ( long "with-cabal" + <> help "Test the given `cabal-install` (the `cabal` on your `$PATH` is used for builds)" + <> value "cabal" + ) + <*> many + ( strOption + ( long "extra-hc" + <> help "Extra compilers to run the test suites against" + ) + ) + <*> option + (Just <$> Opt.str) + ( short 'p' + <> long "pattern" + <> help "Pattern to filter tests by" + <> value Nothing + ) + <*> boolOption + False + "doctest" + ( help "Run doctest on the `Cabal` library" + ) + <*> many + ( option + (maybeReader parseStep) + ( short 's' + <> long "step" + <> help "Run only a specific step (can be specified multiple times)" + ) + ) + <*> switch + ( long "list-steps" + <> help "List the available steps and exit" + ) + <*> ( flag' + True + ( long "lib-only" + <> help "Test only `Cabal` (the library)" + ) + <|> flag + False + False + ( long "cli" + <> help "Test `cabal-install` (the executable) in addition to `Cabal` (the library)" + ) + ) + <*> boolOption + True + "run-lib-tests" + ( help "Run tests for the `Cabal` library" + ) + <*> boolOption + True + "run-cli-tests" + ( help "Run client tests for the `cabal-install` executable" + ) + <*> boolOption + False + "run-lib-suite" + ( help "Run `cabal-testsuite` with the `Cabal` library" + ) + <*> boolOption + False + "run-cli-suite" + ( help "Run `cabal-testsuite` with the `cabal-install` executable" + ) + <*> boolOption + False + "solver-benchmarks" + ( help "Build and trial run `solver-benchmarks`" + ) + <*> ( flag' + CompleteHackageTests + ( long "complete-hackage-tests" + <> help "Run `hackage-tests` on complete Hackage data" + ) + <|> flag + NoHackageTests + PartialHackageTests + ( long "partial-hackage-tests" + <> help "Run `hackage-tests` on parts of Hackage data" + ) + ) + +-- | Parse a boolean switch with separate names for the true and false options. +boolOption' :: Bool -> String -> String -> Mod FlagFields Bool -> Parser Bool +boolOption' defaultValue trueName falseName modifiers = + flag' True (modifiers <> long trueName) + <|> flag defaultValue False (modifiers <> hidden <> long falseName) + +-- | Parse a boolean switch with a @--no-*@ flag for setting the option to false. +boolOption :: Bool -> String -> Mod FlagFields Bool -> Parser Bool +boolOption defaultValue trueName = + boolOption' defaultValue trueName ("no-" <> trueName) + +-- | Full `Parser` for `RawOpts`, which includes a @--help@ argument and +-- information about the program. +fullRawOptsParser :: ParserInfo RawOpts +fullRawOptsParser = + info + (rawOptsParser <**> helper) + ( fullDesc + <> progDesc "Test suite runner for `Cabal` and `cabal-install` developers" + ) + +-- | Parse command-line arguments and resolve defaults from the environment, +-- producing `Opts`. +parseOpts :: IO Opts +parseOpts = execParser fullRawOptsParser >>= resolveOpts diff --git a/cabal-validate/src/ClockUtil.hs b/cabal-validate/src/ClockUtil.hs new file mode 100644 index 00000000000..2df7cdd9866 --- /dev/null +++ b/cabal-validate/src/ClockUtil.hs @@ -0,0 +1,33 @@ +-- | Utilities for dealing with times and durations. +module ClockUtil + ( DiffTime + , AbsoluteTime + , diffAbsoluteTime + , getAbsoluteTime + , formatDiffTime + ) where + +import Data.Time.Clock (DiffTime, secondsToDiffTime) +import Data.Time.Clock.System (getSystemTime, systemToTAITime) +import Data.Time.Clock.TAI (AbsoluteTime, diffAbsoluteTime) +import Data.Time.Format (defaultTimeLocale, formatTime) + +-- | Get the current time as an `AbsoluteTime`. +getAbsoluteTime :: IO AbsoluteTime +getAbsoluteTime = systemToTAITime <$> getSystemTime + +-- | Format a `DiffTime` nicely. +-- +-- Short durations are formatted like @16.34s@, durations longer than a minute +-- are formatted like @22:34.68@, durations longer than an hour are formatted +-- like @1:32:04.68@. +formatDiffTime :: DiffTime -> String +formatDiffTime delta = + let minute = secondsToDiffTime 60 + hour = 60 * minute + in if delta >= hour + then formatTime defaultTimeLocale "%h:%02M:%02ES" delta + else + if delta >= minute + then formatTime defaultTimeLocale "%m:%2ES" delta + else formatTime defaultTimeLocale "%2Ess" delta diff --git a/cabal-validate/src/Main.hs b/cabal-validate/src/Main.hs new file mode 100644 index 00000000000..428a8a7358d --- /dev/null +++ b/cabal-validate/src/Main.hs @@ -0,0 +1,425 @@ +-- | Entry-point to the @cabal-validate@ script. +-- +-- This module encodes all the commands that are run for each step in +-- `runStep`. +module Main + ( main + , runStep + ) where + +import Control.Monad (forM_) +import qualified Data.Text as T +import qualified Data.Text.IO as T +import qualified Data.Text.Lazy as T (toStrict) +import qualified Data.Text.Lazy.Encoding as T (decodeUtf8) +import Data.Version (makeVersion, showVersion) +import System.FilePath (()) +import System.Process.Typed (proc, readProcessStdout_) + +import ANSI (SGR (Bold, BrightCyan, Reset), setSGR) +import Cli (Compiler (..), HackageTests (..), Opts (..), parseOpts) +import ClockUtil (diffAbsoluteTime, formatDiffTime, getAbsoluteTime) +import OutputUtil (printHeader, withTiming) +import ProcessUtil (timed, timedWithCwd) +import Step (Step (..), displayStep) + +-- | Entry-point for @cabal-validate@. +main :: IO () +main = do + opts <- parseOpts + forM_ (steps opts) $ \step -> do + runStep opts step + +-- | Run a given `Step` with the given `Opts`. +runStep :: Opts -> Step -> IO () +runStep opts step = do + let title = displayStep step + printHeader title + let action = case step of + PrintConfig -> printConfig opts + PrintToolVersions -> printToolVersions opts + Build -> build opts + Doctest -> doctest opts + LibTests -> libTests opts + LibSuite -> libSuite opts + LibSuiteExtras -> libSuiteExtras opts + CliSuite -> cliSuite opts + CliTests -> cliTests opts + SolverBenchmarksTests -> solverBenchmarksTests opts + SolverBenchmarksRun -> solverBenchmarksRun opts + TimeSummary -> timeSummary opts + withTiming (startTime opts) title action + T.putStrLn "" + +-- | Compiler with version number like @ghc-9.6.6@. +baseHc :: Opts -> FilePath +baseHc opts = "ghc-" <> showVersion (compilerVersion $ compiler opts) + +-- | Base build directory for @cabal-validate@. +baseBuildDir :: Opts -> FilePath +baseBuildDir opts = "dist-newstyle-validate-" <> baseHc opts + +-- | Absolute path to the build directory for this architecture. +-- +-- This is a path nested fairly deeply under `baseBuildDir`. +buildDir :: Opts -> FilePath +buildDir opts = + cwd opts + baseBuildDir opts + "build" + archPath opts + baseHc opts + +-- | @--num-threads@ argument for test suites. +-- +-- This isn't always used because some test suites are finicky and only accept +-- @-j@. +jobsArgs :: Opts -> [String] +jobsArgs opts = ["--num-threads", show $ jobs opts] + +-- | Default arguments for invoking @cabal@. +cabalArgs :: Opts -> [String] +cabalArgs opts = + [ "--jobs=" <> show (jobs opts) + , "--with-compiler=" <> compilerExecutable (compiler opts) + , "--builddir=" <> baseBuildDir opts + , "--project-file=" <> projectFile opts + ] + +-- | The `buildDir` for @cabal-testsuite-3@. +cabalTestsuiteBuildDir :: Opts -> FilePath +cabalTestsuiteBuildDir opts = + buildDir opts + "cabal-testsuite-3" + +-- | Arguments for @cabal build@. +cabalNewBuildArgs :: Opts -> [String] +cabalNewBuildArgs opts = "build" : cabalArgs opts + +-- | Arguments for @cabal list-bin@. +-- +-- This is used to find the binaries for various test suites. +cabalListBinArgs :: Opts -> [String] +cabalListBinArgs opts = "list-bin" : cabalArgs opts + +-- | Get the binary for a given @cabal@ target by running @cabal list-bin@. +cabalListBin :: Opts -> String -> IO FilePath +cabalListBin opts target = do + let args = cabalListBinArgs opts ++ [target] + stdout <- + readProcessStdout_ $ + proc (cabal opts) args + + pure (T.unpack $ T.strip $ T.toStrict $ T.decodeUtf8 stdout) + +-- | Get the RTS arguments for invoking test suites. +-- +-- These seem to only be used for some of the test suites, I'm not sure why. +rtsArgs :: Opts -> [String] +rtsArgs opts = + case archPath opts of + "x86_64-windows" -> + -- See: https://github.com/haskell/cabal/issues/9571 + if compilerVersion (compiler opts) > makeVersion [9, 0, 2] + then ["+RTS", "--io-manager=native", "-RTS"] + else [] + _ -> [] + +-- | Run a binary built by @cabal@ and output timing information. +-- +-- This is used to run many of the test suites. +timedCabalBin :: Opts -> String -> String -> [String] -> IO () +timedCabalBin opts package component args = do + command <- cabalListBin opts (package <> ":" <> component) + timedWithCwd + opts + package + command + args + +-- | Print the configuration for CI logs. +printConfig :: Opts -> IO () +printConfig opts = do + putStr $ + unlines + [ "compiler: " + <> compilerExecutable (compiler opts) + , "cabal-install: " + <> cabal opts + , "jobs: " + <> show (jobs opts) + , "steps: " + <> unwords (map displayStep (steps opts)) + , "Hackage tests: " + <> show (hackageTests opts) + , "verbose: " + <> show (verbose opts) + , "extra compilers: " + <> unwords (extraCompilers opts) + , "extra RTS options: " + <> unwords (rtsArgs opts) + ] + +-- | Print the versions of tools being used. +printToolVersions :: Opts -> IO () +printToolVersions opts = do + timed opts (compilerExecutable (compiler opts)) ["--version"] + timed opts (cabal opts) ["--version"] + + forM_ (extraCompilers opts) $ \compiler' -> do + timed opts compiler' ["--version"] + +-- | Run the build step. +build :: Opts -> IO () +build opts = do + printHeader "build (dry run)" + timed + opts + (cabal opts) + ( cabalNewBuildArgs opts + ++ targets opts + ++ ["--dry-run"] + ) + + printHeader "build (full build plan; cached and to-be-built dependencies)" + timed + opts + "jq" + [ "-r" + , -- TODO: Maybe use `cabal-plan`? It's a heavy dependency though... + ".\"install-plan\" | map(.\"pkg-name\" + \"-\" + .\"pkg-version\" + \" \" + .\"component-name\") | join(\"\n\")" + , baseBuildDir opts "cache" "plan.json" + ] + + printHeader "build (actual build)" + timed + opts + (cabal opts) + (cabalNewBuildArgs opts ++ targets opts) + +-- | Run doctests. +-- +-- This doesn't work on my machine, maybe @cabal.nix@ needs some love to +-- support @cabal-env@? +doctest :: Opts -> IO () +doctest opts = do + timed + opts + "cabal-env" + [ "--name" + , "doctest-cabal" + , "--transitive" + , "QuickCheck" + ] + + timed + opts + "cabal-env" + [ "--name" + , "doctest-cabal" + , "array" + , "bytestring" + , "containers" + , "deepseq" + , "directory" + , "filepath" + , "pretty" + , "process" + , "time" + , "binary" + , "unix" + , "text" + , "parsec" + , "mtl" + ] + + timed + opts + "doctest" + [ "-package-env=doctest-Cabal" + , "--fast" + , "Cabal/Distribution" + , "Cabal/Language" + ] + +-- | Run tests for the @Cabal@ library, and also `runHackageTests` if those are +-- enabled. +libTests :: Opts -> IO () +libTests opts = do + let runCabalTests' suite extraArgs = + timedCabalBin + opts + "Cabal-tests" + ("test:" <> suite) + ( tastyArgs opts + ++ jobsArgs opts + ++ extraArgs + ) + + runCabalTests suite = runCabalTests' suite [] + + runCabalTests' "unit-tests" ["--with-ghc=" <> compilerExecutable (compiler opts)] + runCabalTests "check-tests" + runCabalTests "parser-tests" + runCabalTests "rpmvercmp" + runCabalTests "no-thunks-test" + + runHackageTests opts + +-- | Run Hackage tests, if enabled. +runHackageTests :: Opts -> IO () +runHackageTests opts + | NoHackageTests <- hackageTests opts = pure () + | otherwise = do + command <- cabalListBin opts "Cabal-tests:test:hackage-tests" + + let + -- See #10284 for why this value is pinned. + hackageTestsIndexState = "--index-state=2024-08-25" + + hackageTest args = + timedWithCwd + opts + "Cabal-tests" + command + (args ++ [hackageTestsIndexState]) + + hackageTest ["read-fields"] + + case hackageTests opts of + CompleteHackageTests -> do + hackageTest ["parsec"] + hackageTest ["roundtrip"] + PartialHackageTests -> do + hackageTest ["parsec", "d"] + hackageTest ["roundtrip", "k"] + +-- | Run @cabal-testsuite@ with the @Cabal@ library with a non-default GHC. +libSuiteWith :: Opts -> FilePath -> [String] -> IO () +libSuiteWith opts ghc extraArgs = + timedCabalBin + opts + "cabal-testsuite" + "exe:cabal-tests" + ( [ "--builddir=" <> cabalTestsuiteBuildDir opts + , "--with-ghc=" <> ghc + , -- This test suite doesn't support `--jobs` _or_ `--num-threads`! + "-j" <> show (jobs opts) + ] + ++ tastyArgs opts + ++ extraArgs + ) + +-- | Run @cabal-testsuite@ with the @Cabal@ library with the default GHC. +libSuite :: Opts -> IO () +libSuite opts = libSuiteWith opts (compilerExecutable (compiler opts)) (rtsArgs opts) + +-- | Run @cabal-testsuite@ with the @Cabal@ library with all extra GHCs. +libSuiteExtras :: Opts -> IO () +libSuiteExtras opts = forM_ (extraCompilers opts) $ \compiler' -> + libSuiteWith opts compiler' [] + +-- | Test the @cabal-install@ executable. +-- +-- These tests mostly run sequentially, so they're pretty slow as a result. +cliTests :: Opts -> IO () +cliTests opts = do + -- These are sorted in asc time used, quicker tests first. + timedCabalBin + opts + "cabal-install" + "test:long-tests" + ( jobsArgs opts + ++ tastyArgs opts + ) + + -- This doesn't work in parallel either. + timedCabalBin + opts + "cabal-install" + "test:unit-tests" + ( ["--num-threads", "1"] + ++ tastyArgs opts + ) + + -- Only single job, otherwise we fail with "Heap exhausted" + timedCabalBin + opts + "cabal-install" + "test:mem-use-tests" + ( ["--num-threads", "1"] + ++ tastyArgs opts + ) + + -- This test-suite doesn't like concurrency + timedCabalBin + opts + "cabal-install" + "test:integration-tests2" + ( [ "--num-threads" + , "1" + , "--with-ghc=" <> compilerExecutable (compiler opts) + ] + ++ tastyArgs opts + ) + +-- | Run @cabal-testsuite@ with the @cabal-install@ executable. +cliSuite :: Opts -> IO () +cliSuite opts = do + cabal' <- cabalListBin opts "cabal-install:exe:cabal" + + timedCabalBin + opts + "cabal-testsuite" + "exe:cabal-tests" + ( [ "--builddir=" <> cabalTestsuiteBuildDir opts + , "--with-cabal=" <> cabal' + , "--with-ghc=" <> compilerExecutable (compiler opts) + , "--intree-cabal-lib=" <> cwd opts + , "--test-tmp=" <> cwd opts "testdb" + , -- This test suite doesn't support `--jobs` _or_ `--num-threads`! + "-j" + , show (jobs opts) + ] + ++ tastyArgs opts + ++ rtsArgs opts + ) + +-- | Run the @solver-benchmarks@ unit tests. +solverBenchmarksTests :: Opts -> IO () +solverBenchmarksTests opts = do + command <- cabalListBin opts "solver-benchmarks:test:unit-tests" + + timedWithCwd + opts + "Cabal" + command + [] + +-- | Run the @solver-benchmarks@. +solverBenchmarksRun :: Opts -> IO () +solverBenchmarksRun opts = do + command <- cabalListBin opts "solver-benchmarks:exe:hackage-benchmark" + cabal' <- cabalListBin opts "cabal-install:exe:cabal" + + timedWithCwd + opts + "Cabal" + command + [ "--cabal1=" <> cabal opts + , "--cabal2=" <> cabal' + , "--trials=5" + , "--packages=Chart-diagrams" + , "--print-trials" + ] + +-- | Print the total time taken so far. +timeSummary :: Opts -> IO () +timeSummary opts = do + endTime <- getAbsoluteTime + let totalDuration = diffAbsoluteTime endTime (startTime opts) + putStrLn $ + setSGR [Bold, BrightCyan] + <> "!!! Validation completed in " + <> formatDiffTime totalDuration + <> setSGR [Reset] diff --git a/cabal-validate/src/OutputUtil.hs b/cabal-validate/src/OutputUtil.hs new file mode 100644 index 00000000000..576c6180433 --- /dev/null +++ b/cabal-validate/src/OutputUtil.hs @@ -0,0 +1,86 @@ +-- | Utilities for printing terminal output. +module OutputUtil + ( printHeader + , withTiming + ) where + +import Control.Exception (catch) +import qualified System.Console.Terminal.Size as Terminal +import System.Process.Typed (ExitCodeException) + +import ANSI (SGR (Bold, BrightCyan, BrightGreen, BrightRed, Reset), setSGR) +import ClockUtil (AbsoluteTime, diffAbsoluteTime, formatDiffTime, getAbsoluteTime) +import System.Exit (exitFailure) + +-- | Get the width of the current terminal, or 80 if no width can be determined. +getTerminalWidth :: IO Int +getTerminalWidth = maybe 80 Terminal.width <$> Terminal.size @Int + +-- | Print a header for a given step. +-- +-- This is colorful and hard to miss in the output. +printHeader + :: String + -- ^ Title to print. + -> IO () +printHeader title = do + columns <- getTerminalWidth + let left = 3 + right = columns - length title - left - 2 + header = + setSGR [Bold, BrightCyan] + <> replicate left '═' + <> " " + <> title + <> " " + <> replicate right '═' + <> setSGR [Reset] + putStrLn header + +-- | Run an `IO` action and print duration information after it finishes. +withTiming + :: AbsoluteTime + -- ^ Start time for the whole @cabal-validate@ run. + -> String + -- ^ Name for describing the action. + -- + -- Used in a sentence like "@title@ finished after 16.34s". + -> IO a + -- ^ Action to time. + -> IO a +withTiming startTime title action = do + startTime' <- getAbsoluteTime + + result <- + (Right <$> action) + `catch` (\exception -> pure (Left (exception :: ExitCodeException))) + + endTime <- getAbsoluteTime + + let duration = diffAbsoluteTime endTime startTime' + totalDuration = diffAbsoluteTime endTime startTime + + case result of + Right inner -> do + putStrLn $ + setSGR [Bold, BrightGreen] + <> title + <> " finished after " + <> formatDiffTime duration + <> "\nTotal time so far: " + <> formatDiffTime totalDuration + <> setSGR [Reset] + + pure inner + Left _procFailed -> do + putStrLn $ + setSGR [Bold, BrightRed] + <> title + <> " failed after " + <> formatDiffTime duration + <> "\nTotal time so far: " + <> formatDiffTime totalDuration + <> setSGR [Reset] + + -- TODO: `--keep-going` mode. + exitFailure diff --git a/cabal-validate/src/ProcessUtil.hs b/cabal-validate/src/ProcessUtil.hs new file mode 100644 index 00000000000..3e27f5517a1 --- /dev/null +++ b/cabal-validate/src/ProcessUtil.hs @@ -0,0 +1,137 @@ +-- | Utilities for running processes and timing them. +module ProcessUtil + ( timed + , timedWithCwd + ) where + +import Control.Exception (throwIO) +import Control.Monad (unless) +import Data.ByteString.Lazy (ByteString) +import qualified Data.ByteString.Lazy as ByteString +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.IO as T +import qualified Data.Text.Lazy as T (toStrict) +import qualified Data.Text.Lazy.Encoding as T (decodeUtf8) +import System.Directory (withCurrentDirectory) +import System.Exit (ExitCode (ExitFailure, ExitSuccess)) +import System.Process.Typed (ExitCodeException (..), proc, readProcess, runProcess) + +import ANSI (SGR (BrightBlue, BrightGreen, BrightRed, Reset), setSGR) +import Cli (Opts (..)) +import ClockUtil (diffAbsoluteTime, formatDiffTime, getAbsoluteTime) + +-- | Like `timed`, but runs the command in a given directory. +timedWithCwd + :: Opts + -- ^ @cabal-validate@ options. + -> FilePath + -- ^ Path to run the command in. + -> FilePath + -- ^ The command to run. + -> [String] + -- ^ Arguments to pass to the command. + -> IO () +timedWithCwd opts cdPath command args = + withCurrentDirectory cdPath (timed opts command args) + +-- | Run a command, displaying timing information after it finishes. +-- +-- This prints out the command to be executed before it's run, handles hiding +-- or showing output (according to the value of `verbose`), and throws an +-- `ExitCodeException` if the command fails. +timed + :: Opts + -- ^ @cabal-validate@ options. + -> FilePath + -- ^ The command to run. + -> [String] + -- ^ Arguments to pass to the command. + -> IO () +timed opts command args = do + let prettyCommand = displayCommand command args + process = proc command args + + startTime' <- getAbsoluteTime + + -- TODO: Replace `$HOME` or `opts.cwd` for brevity? + putStrLn $ + setSGR [BrightBlue] + <> "$ " + <> prettyCommand + <> setSGR [Reset] + + (exitCode, rawStdout, rawStderr) <- + if verbose opts + then do + exitCode <- runProcess process + pure (exitCode, ByteString.empty, ByteString.empty) + else readProcess process + + endTime <- getAbsoluteTime + + let duration = diffAbsoluteTime endTime startTime' + totalDuration = diffAbsoluteTime endTime (startTime opts) + + output = decodeStrip rawStdout <> "\n" <> decodeStrip rawStderr + linesLimit = 50 + outputLines = T.lines output + hiddenLines = length outputLines - linesLimit + tailLines = drop hiddenLines outputLines + + case exitCode of + ExitSuccess -> do + unless (verbose opts) $ do + if hiddenLines <= 0 + then T.putStrLn output + else + T.putStrLn $ + "(" + <> T.pack (show hiddenLines) + <> " lines hidden, use `--verbose` to show)\n" + <> "...\n" + <> T.unlines tailLines + + putStrLn $ + setSGR [BrightGreen] + <> "Finished after " + <> formatDiffTime duration + <> ": " + <> prettyCommand + <> "\nTotal time so far: " + <> formatDiffTime totalDuration + <> setSGR [Reset] + ExitFailure exitCode' -> do + unless (verbose opts) $ do + T.putStrLn output + + putStrLn $ + setSGR [BrightRed] + <> "Failed with exit code " + <> show exitCode' + <> " after " + <> formatDiffTime duration + <> ": " + <> prettyCommand + <> "\nTotal time so far: " + <> formatDiffTime totalDuration + <> setSGR [Reset] + + throwIO + ExitCodeException + { eceExitCode = exitCode + , eceProcessConfig = process + , eceStdout = rawStdout + , eceStderr = rawStderr + } + +-- | Decode `ByteString` output from a command and strip whitespace at the +-- start and end. +decodeStrip :: ByteString -> Text +decodeStrip = T.strip . T.toStrict . T.decodeUtf8 + +-- | Escape a shell command to display it to a user. +-- +-- TODO: Shell escaping +displayCommand :: String -> [String] -> String +displayCommand command args = command <> " " <> unwords args diff --git a/cabal-validate/src/Step.hs b/cabal-validate/src/Step.hs new file mode 100644 index 00000000000..2636f483a79 --- /dev/null +++ b/cabal-validate/src/Step.hs @@ -0,0 +1,62 @@ +-- | The steps that can be run by @cabal-validate@. +module Step + ( Step (..) + , displayStep + , nameToStep + , parseStep + ) where + +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as Map + +-- | A step to be run by @cabal-validate@. +data Step + = PrintConfig + | PrintToolVersions + | Build + | Doctest + | LibTests + | LibSuite + | LibSuiteExtras + | CliTests + | CliSuite + | SolverBenchmarksTests + | SolverBenchmarksRun + | TimeSummary + deriving (Eq, Enum, Bounded, Show) + +-- | Get the display identifier for a given `Step`. +-- +-- This is used to parse the @--step@ command-line argument. +-- +-- Note that these names are just kebab-case variants of the `Step` constructor +-- names; they do not attempt to describe the steps. +displayStep :: Step -> String +displayStep step = + case step of + PrintConfig -> "print-config" + PrintToolVersions -> "print-tool-versions" + Build -> "build" + Doctest -> "doctest" + LibTests -> "lib-tests" + LibSuite -> "lib-suite" + LibSuiteExtras -> "lib-suite-extras" + CliTests -> "cli-tests" + CliSuite -> "cli-suite" + SolverBenchmarksTests -> "solver-benchmarks-tests" + SolverBenchmarksRun -> "solver-benchmarks-run" + TimeSummary -> "time-summary" + +-- | A map from step names to `Steps`. +-- +-- This is an inverse of `displayStep`. +nameToStep :: Map String Step +nameToStep = + Map.fromList + [ (displayStep step, step) + | step <- [minBound .. maxBound] + ] + +-- | Parse a string as a `Step`. +parseStep :: String -> Maybe Step +parseStep step = Map.lookup step nameToStep diff --git a/project-cabal/pkgs/tests.config b/project-cabal/pkgs/tests.config index a9cec9c596f..75fe4af5ad7 100644 --- a/project-cabal/pkgs/tests.config +++ b/project-cabal/pkgs/tests.config @@ -2,3 +2,4 @@ packages: Cabal-QuickCheck , Cabal-tests , Cabal-tree-diff + , cabal-validate diff --git a/validate.sh b/validate.sh index b22e033f86e..b887b724e8f 100755 --- a/validate.sh +++ b/validate.sh @@ -1,554 +1,3 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2086 +#!/usr/bin/env sh -# default config -####################################################################### - -# We use the default ghc in PATH as default -# Use the ghc-x.y.z trigger several errors in windows: -# * It triggers the max path length issue: -# See https://github.com/haskell/cabal/issues/6271#issuecomment-1065102255 -# * It triggers a `createProcess: does not exist` error in units tests -# See https://github.com/haskell/cabal/issues/8049 -HC=ghc -CABAL=cabal -JOBS="" -LIBTESTS=true -CLITESTS=true -CABALSUITETESTS=true -LIBONLY=false -DEPSONLY=false -DOCTEST=false -BENCHMARKS=false -VERBOSE=false -HACKAGETESTSALL=false - -TARGETS="" -STEPS="" -EXTRAHCS="" - -LISTSTEPS=false - -# Help -####################################################################### - -show_usage() { -cat <&1 - else - "$@" > "$OUTPUT" 2>&1 - fi - # echo "MOCK" > "$OUTPUT" - RET=$? - - end_time=$(date +%s) - duration=$((end_time - start_time)) - tduration=$((end_time - JOB_START_TIME)) - - if [ $RET -eq 0 ]; then - if ! $VERBOSE; then - # if output is relatively short, show everything - if [ "$(wc -l < "$OUTPUT")" -le 50 ]; then - cat "$OUTPUT" - else - echo "..." - tail -n 20 "$OUTPUT" - fi - - rm -f "$OUTPUT" - fi - - green "<<< $PRETTYCMD" "($duration/$tduration sec)" - - # bottom-margin - echo "" - else - if ! $VERBOSE; then - cat "$OUTPUT" - fi - - red "<<< $PRETTYCMD" "($duration/$tduration sec, $RET)" - red "<<< $*" "($duration/$tduration sec, $RET)" - rm -f "$OUTPUT" - exit 1 - fi -} - -print_header() { - TITLE=$1 - TITLEPAT="$(echo "$TITLE"|sed 's:.:=:g')" - cyan "===X========================================================================== $(date +%T) ===" \ - | sed "s#X$TITLEPAT=# $TITLE #" - -} - -# getopt -####################################################################### - -while [ $# -gt 0 ]; do - arg=$1 - case $arg in - --help) - show_usage - exit - ;; - -j|--jobs) - JOBS="$2" - shift - shift - ;; - --lib-only) - LIBONLY=true - shift - ;; - --cli) - LIBONLY=false - shift - ;; - --run-lib-tests) - LIBTESTS=true - shift - ;; - --no-run-lib-tests) - LIBTESTS=false - shift - ;; - --run-cli-tests) - CLITESTS=true - shift - ;; - --no-run-cli-tests) - CLITESTS=false - shift - ;; - --run-lib-suite) - LIBSUITE=true - shift - ;; - --no-run-lib-suite) - LIBSUITE=false - shift - ;; - --run-cli-suite) - CLISUITE=true - shift - ;; - --no-run-cli-suite) - CLISUITE=false - shift - ;; - -w|--with-compiler) - HC=$2 - shift - shift - ;; - --with-cabal) - CABAL=$2 - shift - shift - ;; - --extra-hc) - EXTRAHCS="$EXTRAHCS $2" - shift - shift - ;; - --doctest) - DOCTEST=true - shift - ;; - --no-doctest) - DOCTEST=false - shift - ;; - --solver-benchmarks) - BENCHMARKS=true - shift - ;; - --no-solver-benchmarks) - BENCHMARKS=false - shift - ;; - --complete-hackage-tests) - HACKAGETESTSALL=true - shift - ;; - --partial-hackage-tests) - HACKAGETESTSALL=false - shift - ;; - -v|--verbose) - VERBOSE=true - shift - ;; - -q|--quiet) - VERBOSE=false - shift - ;; - -s|--step) - STEPS="$STEPS $2" - shift - shift - ;; - --list-steps) - LISTSTEPS=true - shift - ;; - *) - echo "Unknown option $arg" - exit 1 - esac -done - -# calculate steps and build targets -####################################################################### - -# If there are no explicit steps given calculate them -if $LIBONLY; then - CLITESTS=false - CLISUITE=false - BENCHMARKS=false -fi - -if [ -z "$STEPS" ]; then - STEPS="print-config print-tool-versions" - STEPS="$STEPS build" - if $DOCTEST; then STEPS="$STEPS doctest"; fi - if $LIBTESTS; then STEPS="$STEPS lib-tests"; fi - if $LIBSUITE; then STEPS="$STEPS lib-suite"; fi - if $LIBSUITE && [ -n "$EXTRAHCS" ]; - then STEPS="$STEPS lib-suite-extras"; fi - if $CLITESTS; then STEPS="$STEPS cli-tests"; fi - if $CLISUITE; then STEPS="$STEPS cli-suite"; fi - if $BENCHMARKS; then STEPS="$STEPS solver-benchmarks-tests solver-benchmarks-run"; fi - STEPS="$STEPS time-summary" -fi - -TARGETS="Cabal Cabal-hooks cabal-testsuite Cabal-tests Cabal-QuickCheck Cabal-tree-diff Cabal-described" -if ! $LIBONLY; then TARGETS="$TARGETS cabal-install cabal-install-solver cabal-benchmarks"; fi -if $BENCHMARKS; then TARGETS="$TARGETS solver-benchmarks"; fi - -if $LISTSTEPS; then - echo "Targets: $TARGETS" - echo "Steps: $STEPS" - exit -fi - -# Adjust runtime configuration -####################################################################### - -if [ -z "$JOBS" ]; then - if command -v nproc >/dev/null; then - JOBS=$(nproc) - else - echo "Warning: \`nproc\` not found, setting \`--jobs\` to default of 4." - JOBS=4 - fi -fi - -TESTSUITEJOBS="-j$JOBS" -JOBS="-j$JOBS" - -# assume compiler is GHC -RUNHASKELL=$(echo "$HC" | sed -E 's/ghc(-[0-9.]*)$/runghc\1/') - -ARCH=$(uname -m) - -case "$ARCH" in - arm64) - ARCH=aarch64 - ;; - x86_64) - ARCH=x86_64 - ;; - *) - echo "Warning: Unknown architecture '$ARCH'" - ;; -esac - -OS=$(uname) - -case "$OS" in - MINGW64*) - ARCH="$ARCH-windows" - ;; - Linux) - ARCH="$ARCH-linux" - ;; - Darwin) - ARCH="$ARCH-osx" - ;; - *) - echo "Warning: Unknown operating system '$OS'" - ARCH="$ARCH-$OS" - ;; -esac - -if $LIBONLY; then - PROJECTFILE=cabal.validate-libonly.project -else - PROJECTFILE=cabal.validate.project -fi - -BASEHC=ghc-$($HC --numeric-version) -BUILDDIR=dist-newstyle-validate-$BASEHC -CABAL_TESTSUITE_BDIR="$(pwd)/$BUILDDIR/build/$ARCH/$BASEHC/cabal-testsuite-3" - -CABALNEWBUILD="${CABAL} build $JOBS -w $HC --builddir=$BUILDDIR --project-file=$PROJECTFILE" -CABALLISTBIN="${CABAL} list-bin --builddir=$BUILDDIR --project-file=$PROJECTFILE" - -# See https://github.com/haskell/cabal/issues/9571 for why we set this for Windows -RTSOPTS="$([ $ARCH = "x86_64-windows" ] && [ "$($HC --numeric-version)" != "9.0.2" ] && [ "$(echo -e "$(ghc --numeric-version)\n9.0.2" | sort -V | head -n1)" = "9.0.2" ] && echo "+RTS --io-manager=native" || echo "")" - -# header -####################################################################### - -step_print_config() { -print_header print-config - -cat <